9fc22cc0 by 柴进

软件支持图片拖动和图片粘贴

1 parent afae62c5
...@@ -3,5 +3,5 @@ ...@@ -3,5 +3,5 @@
3 <component name="Black"> 3 <component name="Black">
4 <option name="sdkName" value="Python 3.9 (GoogleNanoBananaApp) (2)" /> 4 <option name="sdkName" value="Python 3.9 (GoogleNanoBananaApp) (2)" />
5 </component> 5 </component>
6 <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (GoogleNanoBananaApp)" project-jdk-type="Python SDK" /> 6 <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (Nano_Banana_App)" project-jdk-type="Python SDK" />
7 </project> 7 </project>
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -12,8 +12,8 @@ from PySide6.QtWidgets import ( ...@@ -12,8 +12,8 @@ from PySide6.QtWidgets import (
12 QListWidget, QListWidgetItem, QTabWidget, QSplitter, 12 QListWidget, QListWidgetItem, QTabWidget, QSplitter,
13 QMenu, QProgressBar 13 QMenu, QProgressBar
14 ) 14 )
15 from PySide6.QtCore import Qt, QThread, Signal, QSize, QTimer 15 from PySide6.QtCore import Qt, QThread, Signal, QSize, QTimer, QMimeData
16 from PySide6.QtGui import QPixmap, QFont, QIcon, QDesktopServices, QAction, QImage 16 from PySide6.QtGui import QPixmap, QFont, QIcon, QDesktopServices, QAction, QImage, QDragEnterEvent, QDropEvent
17 from PySide6.QtCore import QUrl 17 from PySide6.QtCore import QUrl
18 18
19 import base64 19 import base64
...@@ -109,6 +109,7 @@ def hash_password(password: str) -> str: ...@@ -109,6 +109,7 @@ def hash_password(password: str) -> str:
109 109
110 class DatabaseManager: 110 class DatabaseManager:
111 """数据库连接管理类""" 111 """数据库连接管理类"""
112
112 def __init__(self, db_config): 113 def __init__(self, db_config):
113 self.config = db_config 114 self.config = db_config
114 self.logger = logging.getLogger(__name__) 115 self.logger = logging.getLogger(__name__)
...@@ -317,7 +318,7 @@ class HistoryManager: ...@@ -317,7 +318,7 @@ class HistoryManager:
317 # 保存参考图片 318 # 保存参考图片
318 reference_image_paths = [] 319 reference_image_paths = []
319 for i, ref_img_bytes in enumerate(reference_images): 320 for i, ref_img_bytes in enumerate(reference_images):
320 ref_path = record_dir / f"reference_{i+1}.png" 321 ref_path = record_dir / f"reference_{i + 1}.png"
321 with open(ref_path, 'wb') as f: 322 with open(ref_path, 'wb') as f:
322 f.write(ref_img_bytes) 323 f.write(ref_img_bytes)
323 reference_image_paths.append(ref_path) 324 reference_image_paths.append(ref_path)
...@@ -830,6 +831,123 @@ class LoginDialog(QDialog): ...@@ -830,6 +831,123 @@ class LoginDialog(QDialog):
830 return getattr(self, 'current_password_hash', '') 831 return getattr(self, 'current_password_hash', '')
831 832
832 833
834 class DragDropScrollArea(QScrollArea):
835 """自定义支持拖拽的图像滚动区域"""
836
837 def __init__(self, parent=None):
838 super().__init__(parent)
839 self.setAcceptDrops(True)
840 self.parent_window = parent
841 self.setStyleSheet("""
842 QScrollArea {
843 border: 2px dashed #e5e5e5;
844 border-radius: 8px;
845 background-color: #fafafa;
846 }
847 QScrollArea:hover {
848 border-color: #007AFF;
849 background-color: #f0f8ff;
850 }
851 """)
852
853 def dragEnterEvent(self, event: QDragEnterEvent):
854 """拖拽进入事件处理"""
855 mime_data = event.mimeData()
856
857 # 检查是否包含文件URL
858 if mime_data.hasUrls():
859 urls = mime_data.urls()
860 for url in urls:
861 if url.isLocalFile():
862 file_path = url.toLocalFile()
863 if self.is_valid_image_file(file_path):
864 event.acceptProposedAction()
865 self.setStyleSheet("""
866 QScrollArea {
867 border: 2px dashed #007AFF;
868 border-radius: 8px;
869 background-color: #e6f3ff;
870 }
871 """)
872 return
873
874 # 检查剪贴板图像
875 if mime_data.hasImage():
876 event.acceptProposedAction()
877 self.setStyleSheet("""
878 QScrollArea {
879 border: 2px dashed #007AFF;
880 border-radius: 8px;
881 background-color: #e6f3ff;
882 }
883 """)
884 return
885
886 event.ignore()
887
888 def dragLeaveEvent(self, event):
889 """拖拽离开事件处理"""
890 self.setStyleSheet("""
891 QScrollArea {
892 border: 2px dashed #e5e5e5;
893 border-radius: 8px;
894 background-color: #fafafa;
895 }
896 QScrollArea:hover {
897 border-color: #007AFF;
898 background-color: #f0f8ff;
899 }
900 """)
901
902 def dropEvent(self, event: QDropEvent):
903 """拖拽放置事件处理"""
904 mime_data = event.mimeData()
905
906 # 重置样式
907 self.setStyleSheet("""
908 QScrollArea {
909 border: 2px dashed #e5e5e5;
910 border-radius: 8px;
911 background-color: #fafafa;
912 }
913 QScrollArea:hover {
914 border-color: #007AFF;
915 background-color: #f0f8ff;
916 }
917 """)
918
919 # 处理文件拖拽
920 if mime_data.hasUrls():
921 urls = mime_data.urls()
922 image_files = []
923
924 for url in urls:
925 if url.isLocalFile():
926 file_path = url.toLocalFile()
927 if self.is_valid_image_file(file_path):
928 image_files.append(file_path)
929
930 if image_files:
931 self.parent_window.add_image_files(image_files)
932 event.acceptProposedAction()
933 return
934
935 # 处理剪贴板图像拖拽
936 if mime_data.hasImage():
937 image = mime_data.imageData()
938 if image and not image.isNull():
939 self.parent_window.add_clipboard_image(image)
940 event.acceptProposedAction()
941 return
942
943 event.ignore()
944
945 def is_valid_image_file(self, file_path: str) -> bool:
946 """检查文件是否为有效的图像文件"""
947 valid_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
948 return Path(file_path).suffix.lower() in valid_extensions
949
950
833 class ImageGeneratorWindow(QMainWindow): 951 class ImageGeneratorWindow(QMainWindow):
834 """Qt-based main application window""" 952 """Qt-based main application window"""
835 953
...@@ -1013,7 +1131,8 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -1013,7 +1131,8 @@ class ImageGeneratorWindow(QMainWindow):
1013 print(f"Failed to load bundled config: {e}") 1131 print(f"Failed to load bundled config: {e}")
1014 1132
1015 if not self.api_key: 1133 if not self.api_key:
1016 QMessageBox.warning(self, "警告", f"未找到API密钥\n配置文件位置: {config_path}\n\n请在应用中输入API密钥或手动编辑配置文件") 1134 QMessageBox.warning(self, "警告",
1135 f"未找到API密钥\n配置文件位置: {config_path}\n\n请在应用中输入API密钥或手动编辑配置文件")
1017 1136
1018 def save_config(self, last_user=None): 1137 def save_config(self, last_user=None):
1019 """Save configuration to file""" 1138 """Save configuration to file"""
...@@ -1100,13 +1219,41 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -1100,13 +1219,41 @@ class ImageGeneratorWindow(QMainWindow):
1100 upload_btn.clicked.connect(self.upload_images) 1219 upload_btn.clicked.connect(self.upload_images)
1101 upload_header.addWidget(upload_btn) 1220 upload_header.addWidget(upload_btn)
1102 1221
1222 # Paste button
1223 paste_btn = QPushButton("📋 粘贴图片")
1224 paste_btn.clicked.connect(self.paste_from_clipboard)
1225 paste_btn.setStyleSheet("""
1226 QPushButton {
1227 background-color: #f0f0f0;
1228 border: 1px solid #d0d0d0;
1229 padding: 8px 16px;
1230 border-radius: 4px;
1231 font-size: 12px;
1232 min-width: 80px;
1233 }
1234 QPushButton:hover {
1235 background-color: #e8e8e8;
1236 border-color: #007AFF;
1237 }
1238 QPushButton:pressed {
1239 background-color: #d0d0d0;
1240 }
1241 """)
1242 upload_header.addWidget(paste_btn)
1243
1103 self.image_count_label = QLabel("已选择 0 张") 1244 self.image_count_label = QLabel("已选择 0 张")
1104 upload_header.addWidget(self.image_count_label) 1245 upload_header.addWidget(self.image_count_label)
1105 upload_header.addStretch() 1246 upload_header.addStretch()
1106 ref_layout.addLayout(upload_header) 1247 ref_layout.addLayout(upload_header)
1107 1248
1108 # Image preview scroll area 1249 # Drag and drop hint
1109 self.img_scroll = QScrollArea() 1250 hint_label = QLabel("💡 提示:可以直接拖拽图片到下方区域,或使用 Ctrl+V 粘贴截图")
1251 hint_label.setStyleSheet("QLabel { color: #666666; font-size: 11px; margin: 2px 0; }")
1252 hint_label.setWordWrap(True)
1253 ref_layout.addWidget(hint_label)
1254
1255 # Image preview scroll area with drag-and-drop support
1256 self.img_scroll = DragDropScrollArea(self)
1110 self.img_scroll.setWidgetResizable(True) 1257 self.img_scroll.setWidgetResizable(True)
1111 self.img_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) 1258 self.img_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
1112 self.img_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 1259 self.img_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
...@@ -1299,7 +1446,8 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -1299,7 +1446,8 @@ class ImageGeneratorWindow(QMainWindow):
1299 1446
1300 self.prompt_display = QLabel("请选择一个历史记录查看详情") 1447 self.prompt_display = QLabel("请选择一个历史记录查看详情")
1301 self.prompt_display.setWordWrap(True) 1448 self.prompt_display.setWordWrap(True)
1302 self.prompt_display.setStyleSheet("QLabel { padding: 8px; background-color: #f9f9f9; border: 1px solid #ddd; border-radius: 4px; }") 1449 self.prompt_display.setStyleSheet(
1450 "QLabel { padding: 8px; background-color: #f9f9f9; border: 1px solid #ddd; border-radius: 4px; }")
1303 prompt_layout.addWidget(self.prompt_display) 1451 prompt_layout.addWidget(self.prompt_display)
1304 1452
1305 prompt_group.setLayout(prompt_layout) 1453 prompt_group.setLayout(prompt_layout)
...@@ -1355,7 +1503,8 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -1355,7 +1503,8 @@ class ImageGeneratorWindow(QMainWindow):
1355 self.generated_image_label.setAlignment(Qt.AlignCenter) 1503 self.generated_image_label.setAlignment(Qt.AlignCenter)
1356 self.generated_image_label.setMinimumSize(200, 200) # Larger size for generated image 1504 self.generated_image_label.setMinimumSize(200, 200) # Larger size for generated image
1357 self.generated_image_label.setMaximumSize(300, 300) 1505 self.generated_image_label.setMaximumSize(300, 300)
1358 self.generated_image_label.setStyleSheet("QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }") 1506 self.generated_image_label.setStyleSheet(
1507 "QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
1359 self.generated_image_label.mouseDoubleClickEvent = self.open_generated_image_from_history 1508 self.generated_image_label.mouseDoubleClickEvent = self.open_generated_image_from_history
1360 1509
1361 gen_layout.addWidget(self.generated_image_label) 1510 gen_layout.addWidget(self.generated_image_label)
...@@ -1445,6 +1594,121 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -1445,6 +1594,121 @@ class ImageGeneratorWindow(QMainWindow):
1445 self.status_label.setText(f"● 已添加 {len(files)} 张参考图片") 1594 self.status_label.setText(f"● 已添加 {len(files)} 张参考图片")
1446 self.status_label.setStyleSheet("QLabel { color: #34C759; }") 1595 self.status_label.setStyleSheet("QLabel { color: #34C759; }")
1447 1596
1597 def add_image_files(self, file_paths):
1598 """添加图像文件到上传列表(用于拖拽功能)"""
1599 if not file_paths:
1600 return
1601
1602 added_count = 0
1603 for file_path in file_paths:
1604 try:
1605 if self.validate_image_file(file_path):
1606 self.uploaded_images.append(file_path)
1607 added_count += 1
1608 else:
1609 self.logger.warning(f"无效的图像文件: {file_path}")
1610 except Exception as e:
1611 self.logger.error(f"添加图片失败: {file_path}, 错误: {str(e)}")
1612
1613 if added_count > 0:
1614 self.update_image_preview()
1615 self.image_count_label.setText(f"已选择 {len(self.uploaded_images)} 张")
1616 self.status_label.setText(f"● 已通过拖拽添加 {added_count} 张参考图片")
1617 self.status_label.setStyleSheet("QLabel { color: #34C759; }")
1618 else:
1619 QMessageBox.warning(self, "警告", "没有找到有效的图片文件")
1620
1621 def add_clipboard_image(self, image):
1622 """添加剪贴板图像(用于拖拽和粘贴功能)"""
1623 try:
1624 # 生成临时文件名
1625 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1626 temp_dir = Path(tempfile.gettempdir()) / "nano_banana_app"
1627 temp_dir.mkdir(exist_ok=True)
1628
1629 # 根据图像格式选择文件扩展名
1630 file_extension = ".png" # 默认使用PNG格式
1631 if image.format() == QImage.Format_RGB32:
1632 file_extension = ".bmp"
1633 elif image.format() == QImage.Format_RGB888:
1634 file_extension = ".jpg"
1635
1636 temp_file_path = temp_dir / f"clipboard_{timestamp}{file_extension}"
1637
1638 # 保存图像到临时文件
1639 if image.save(str(temp_file_path)):
1640 self.uploaded_images.append(str(temp_file_path))
1641 self.update_image_preview()
1642 self.image_count_label.setText(f"已选择 {len(self.uploaded_images)} 张")
1643 self.status_label.setText("● 已添加剪贴板图片")
1644 self.status_label.setStyleSheet("QLabel { color: #34C759; }")
1645 self.logger.info(f"剪贴板图片已保存到: {temp_file_path}")
1646 else:
1647 QMessageBox.critical(self, "错误", "无法保存剪贴板图片")
1648
1649 except Exception as e:
1650 self.logger.error(f"添加剪贴板图片失败: {str(e)}")
1651 QMessageBox.critical(self, "错误", f"添加剪贴板图片失败: {str(e)}")
1652
1653 def paste_from_clipboard(self):
1654 """从剪贴板粘贴图像"""
1655 clipboard = QApplication.clipboard()
1656
1657 # 检查剪贴板中是否有图像
1658 if clipboard.mimeData().hasImage():
1659 image = clipboard.image()
1660 if not image.isNull():
1661 self.add_clipboard_image(image)
1662 else:
1663 QMessageBox.information(self, "信息", "剪贴板中没有有效的图片")
1664 else:
1665 QMessageBox.information(self, "信息", "剪贴板中没有图片,请先复制一张图片")
1666
1667 def validate_image_file(self, file_path: str) -> bool:
1668 """验证图像文件"""
1669 try:
1670 # 检查文件是否存在
1671 if not Path(file_path).exists():
1672 return False
1673
1674 # 检查文件扩展名
1675 valid_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
1676 if Path(file_path).suffix.lower() not in valid_extensions:
1677 return False
1678
1679 # 尝试加载图像以验证文件完整性
1680 pixmap = QPixmap(file_path)
1681 if pixmap.isNull():
1682 return False
1683
1684 # 检查文件大小(限制为10MB)
1685 file_size = Path(file_path).stat().st_size
1686 if file_size > 10 * 1024 * 1024: # 10MB
1687 QMessageBox.warning(self, "警告", f"图片文件过大: {file_path}\n请选择小于10MB的图片")
1688 return False
1689
1690 return True
1691
1692 except Exception as e:
1693 self.logger.error(f"图像文件验证失败: {file_path}, 错误: {str(e)}")
1694 return False
1695
1696 def keyPressEvent(self, event):
1697 """处理键盘事件"""
1698 # Ctrl+V 粘贴
1699 if event.key() == Qt.Key_V and event.modifiers() == Qt.ControlModifier:
1700 self.paste_from_clipboard()
1701 event.accept()
1702 return
1703
1704 # Cmd+V 粘贴 (macOS)
1705 elif event.key() == Qt.Key_V and event.modifiers() == Qt.MetaModifier:
1706 self.paste_from_clipboard()
1707 event.accept()
1708 return
1709
1710 super().keyPressEvent(event)
1711
1448 def update_image_preview(self): 1712 def update_image_preview(self):
1449 """Update image preview thumbnails""" 1713 """Update image preview thumbnails"""
1450 # Clear existing previews 1714 # Clear existing previews
...@@ -1707,7 +1971,6 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -1707,7 +1971,6 @@ class ImageGeneratorWindow(QMainWindow):
1707 except Exception as e: 1971 except Exception as e:
1708 QMessageBox.critical(self, "错误", f"保存失败: {str(e)}") 1972 QMessageBox.critical(self, "错误", f"保存失败: {str(e)}")
1709 1973
1710
1711 def refresh_history(self): 1974 def refresh_history(self):
1712 """Refresh the history list""" 1975 """Refresh the history list"""
1713 self.history_list.clear() 1976 self.history_list.clear()
...@@ -1954,18 +2217,22 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -1954,18 +2217,22 @@ class ImageGeneratorWindow(QMainWindow):
1954 Qt.SmoothTransformation 2217 Qt.SmoothTransformation
1955 ) 2218 )
1956 self.generated_image_label.setPixmap(scaled_pixmap) 2219 self.generated_image_label.setPixmap(scaled_pixmap)
1957 self.generated_image_label.setStyleSheet("QLabel { border: 1px solid #ddd; background-color: white; }") 2220 self.generated_image_label.setStyleSheet(
2221 "QLabel { border: 1px solid #ddd; background-color: white; }")
1958 self.current_generated_image_path = image_path 2222 self.current_generated_image_path = image_path
1959 else: 2223 else:
1960 self.generated_image_label.setText("图片加载失败") 2224 self.generated_image_label.setText("图片加载失败")
1961 self.generated_image_label.setStyleSheet("QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }") 2225 self.generated_image_label.setStyleSheet(
2226 "QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
1962 except Exception as e: 2227 except Exception as e:
1963 print(f"Failed to load generated image {image_path}: {e}") 2228 print(f"Failed to load generated image {image_path}: {e}")
1964 self.generated_image_label.setText("图片加载失败") 2229 self.generated_image_label.setText("图片加载失败")
1965 self.generated_image_label.setStyleSheet("QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }") 2230 self.generated_image_label.setStyleSheet(
2231 "QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
1966 else: 2232 else:
1967 self.generated_image_label.setText("图片文件不存在") 2233 self.generated_image_label.setText("图片文件不存在")
1968 self.generated_image_label.setStyleSheet("QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }") 2234 self.generated_image_label.setStyleSheet(
2235 "QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
1969 2236
1970 def clear_details_panel(self): 2237 def clear_details_panel(self):
1971 """Clear the details panel""" 2238 """Clear the details panel"""
...@@ -1986,7 +2253,8 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -1986,7 +2253,8 @@ class ImageGeneratorWindow(QMainWindow):
1986 2253
1987 self.generated_image_label.setText("请选择一个历史记录查看生成图片") 2254 self.generated_image_label.setText("请选择一个历史记录查看生成图片")
1988 self.generated_image_label.setPixmap(QPixmap()) 2255 self.generated_image_label.setPixmap(QPixmap())
1989 self.generated_image_label.setStyleSheet("QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }") 2256 self.generated_image_label.setStyleSheet(
2257 "QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
1990 2258
1991 def copy_prompt_text(self): 2259 def copy_prompt_text(self):
1992 """Copy the prompt text to clipboard""" 2260 """Copy the prompt text to clipboard"""
...@@ -2027,7 +2295,8 @@ class ImageGeneratorWindow(QMainWindow): ...@@ -2027,7 +2295,8 @@ class ImageGeneratorWindow(QMainWindow):
2027 2295
2028 class ImageGenerationWorker(QThread): 2296 class ImageGenerationWorker(QThread):
2029 """Worker thread for image generation""" 2297 """Worker thread for image generation"""
2030 finished = Signal(bytes, str, list, str, str, str) # image_bytes, prompt, reference_images, aspect_ratio, image_size, model 2298 finished = Signal(bytes, str, list, str, str,
2299 str) # image_bytes, prompt, reference_images, aspect_ratio, image_size, model
2031 error = Signal(str) 2300 error = Signal(str)
2032 progress = Signal(str) 2301 progress = Signal(str)
2033 2302
......