diff --git a/beeref/main_controls.py b/beeref/main_controls.py index 4848a2f..0344681 100644 --- a/beeref/main_controls.py +++ b/beeref/main_controls.py @@ -23,10 +23,10 @@ from beeref.items import BeePixmapItem from beeref import fileio -logger = logging.getLogger(__name__) +logger = logging.getLogger(__name__) # 主控件日志记录器,用于记录主控件的日志信息 -class MainControlsMixin: +class MainControlsMixin: # 主控件混合类,用于处理主窗口和欢迎叠加层的基本控件 """Basic controls shared by the main view and the welcome overlay: * Right-click menu @@ -34,114 +34,114 @@ class MainControlsMixin: * Moving the window without title bar """ - def init_main_controls(self, main_window): - self.main_window = main_window - self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self.customContextMenuRequested.connect( - self.control_target.on_context_menu) - self.setAcceptDrops(True) - self.movewin_active = False + def init_main_controls(self, main_window): # 初始化主控件,用于处理主窗口和欢迎叠加层的基本控件 + self.main_window = main_window # 主窗口引用,用于访问主窗口的属性和方法 + self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) # 设置上下文菜单策略为自定义上下文菜单,用于处理右键点击事件 + self.customContextMenuRequested.connect( # 自定义上下文菜单请求信号槽函数,用于处理右键点击事件 + self.control_target.on_context_menu) # 自定义上下文菜单请求信号槽函数,用于处理右键点击事件,调用控制目标的上下文菜单槽函数 + self.setAcceptDrops(True) # 设置接受拖放事件,用于处理文件拖放事件 + self.movewin_active = False - def on_action_movewin_mode(self): - if self.movewin_active: + def on_action_movewin_mode(self): # 移动窗口模式槽函数,用于处理移动窗口模式的切换 + if self.movewin_active: # 如果移动窗口模式已激活 # Pressing the same shortcut again should end the action - self.exit_movewin_mode() + self.exit_movewin_mode() # 退出移动窗口模式槽函数,用于处理移动窗口模式的切换 else: - self.enter_movewin_mode() + self.enter_movewin_mode() # 进入移动窗口模式槽函数,用于处理移动窗口模式的切换 - @property - def viewport_or_self(self): - if hasattr(self, 'viewport'): - return self.viewport() - return self + @property # viewport_or_self属性,用于返回视口或自身,用于处理移动窗口模式的切换 + def viewport_or_self(self): # viewport_or_self属性,用于返回视口或自身,用于处理移动窗口模式的切换 + if hasattr(self, 'viewport'): # 如果主控件有视口属性 + return self.viewport() # 返回视口 + return self # 返回自身 - def enter_movewin_mode(self): - logger.debug('Entering movewin mode') - self.setMouseTracking(True) - self.movewin_active = True - self.viewport_or_self.setCursor(Qt.CursorShape.SizeAllCursor) - self.event_start = QtCore.QPointF(self.cursor().pos()) - if hasattr(self, 'disable_mouse_events'): - self.disable_mouse_events() + def enter_movewin_mode(self): # 进入移动窗口模式槽函数,用于处理移动窗口模式的切换 + logger.debug('Entering movewin mode') # 进入移动窗口模式槽函数,用于处理移动窗口模式的切换 + self.setMouseTracking(True) # 设置鼠标跟踪,用于处理移动窗口模式的切换 + self.movewin_active = True # 移动窗口模式已激活 + self.viewport_or_self.setCursor(Qt.CursorShape.SizeAllCursor) # 设置视口或自身的光标为大小调整光标,用于处理移动窗口模式的切换 + self.event_start = QtCore.QPointF(self.cursor().pos()) # 记录鼠标点击位置,用于处理移动窗口模式的切换 + if hasattr(self, 'disable_mouse_events'): # 如果主控件有禁用鼠标事件属性 + self.disable_mouse_events() # 禁用鼠标事件,用于处理移动窗口模式的切换 - def exit_movewin_mode(self): - logger.debug('Exiting movewin mode') - self.setMouseTracking(False) - self.movewin_active = False - self.viewport_or_self.unsetCursor() - if hasattr(self, 'enable_mouse_events'): - self.enable_mouse_events() + def exit_movewin_mode(self): # 退出移动窗口模式槽函数,用于处理移动窗口模式的切换 + logger.debug('Exiting movewin mode') # 退出移动窗口模式槽函数,用于处理移动窗口模式的切换 + self.setMouseTracking(False) # 退出移动窗口模式槽函数,用于处理移动窗口模式的切换 + self.movewin_active = False # 移动窗口模式未激活 + self.viewport_or_self.unsetCursor() # 退出移动窗口模式槽函数,用于处理移动窗口模式的切换 + if hasattr(self, 'enable_mouse_events'): # 如果主控件有启用鼠标事件属性 + self.enable_mouse_events() # 启用鼠标事件,用于处理移动窗口模式的切换 - def dragEnterEvent(self, event): - mimedata = event.mimeData() - logger.debug(f'Drag enter event: {mimedata.formats()}') - if mimedata.hasUrls(): - event.acceptProposedAction() - elif mimedata.hasImage(): - event.acceptProposedAction() + def dragEnterEvent(self, event): # 拖入事件槽函数,用于处理拖入事件 + mimedata = event.mimeData() # 获取拖入事件的MIME数据,用于处理拖入事件 + logger.debug(f'Drag enter event: {mimedata.formats()}') # 拖入事件槽函数,用于处理拖入事件,记录拖入事件的MIME数据格式 + if mimedata.hasUrls(): # 如果拖入事件的MIME数据包含URL + event.acceptProposedAction() # 接受拖入事件的建议操作,用于处理拖入事件 + elif mimedata.hasImage(): # 如果拖入事件的MIME数据包含图像 + event.acceptProposedAction() # 接受拖入事件的建议操作,用于处理拖入事件 else: - msg = 'Attempted drop not an image or image too big' - logger.info(msg) - widgets.BeeNotification(self.control_target, msg) + msg = 'Attempted drop not an image or image too big' # 拖入事件槽函数,用于处理拖入事件,记录拖入事件的MIME数据格式 + logger.info(msg) # 拖入事件槽函数,用于处理拖入事件,记录拖入事件的MIME数据格式 + widgets.BeeNotification(self.control_target, msg) # 拖入事件槽函数,用于处理拖入事件,显示拖入事件的MIME数据格式 - def dragMoveEvent(self, event): - event.acceptProposedAction() + def dragMoveEvent(self, event): # 拖动移动事件槽函数,用于处理拖动移动事件 + event.acceptProposedAction() # 接受拖动移动事件的建议操作,用于处理拖动移动事件 - def dropEvent(self, event): - mimedata = event.mimeData() - logger.debug(f'Handling file drop: {mimedata.formats()}') - pos = QtCore.QPoint(round(event.position().x()), - round(event.position().y())) - if mimedata.hasUrls(): - logger.debug(f'Found dropped urls: {mimedata.urls()}') - if not self.control_target.scene.items(): + def dropEvent(self, event): # 拖放事件槽函数,用于处理拖放事件 + mimedata = event.mimeData() # 获取拖放事件的MIME数据,用于处理拖放事件 + logger.debug(f'Handling file drop: {mimedata.formats()}') # 拖放事件槽函数,用于处理拖放事件,记录拖放事件的MIME数据格式 + pos = QtCore.QPoint(round(event.position().x()), # 记录拖放事件的位置,用于处理拖放事件 + round(event.position().y())) # 记录拖放事件的位置,用于处理拖放事件 + if mimedata.hasUrls(): # 如果拖放事件的MIME数据包含URL + logger.debug(f'Found dropped urls: {mimedata.urls()}') # 拖放事件槽函数,用于处理拖放事件,记录拖放事件的URL + if not self.control_target.scene.items(): # 如果场景中没有项目(即没有插入的图片项) # Check if we have a bee file we can open directly - path = mimedata.urls()[0] - if (path.isLocalFile() - and fileio.is_bee_file(path.toLocalFile())): - self.control_target.open_from_file(path.toLocalFile()) - return - self.control_target.do_insert_images(mimedata.urls(), pos) - elif mimedata.hasImage(): - img = QtGui.QImage(mimedata.imageData()) - item = BeePixmapItem(img) - pos = self.control_target.mapToScene(pos) + path = mimedata.urls()[0] # 获取拖放事件的URL,用于处理拖放事件 + if (path.isLocalFile() # 如果拖放事件的URL是本地文件 + and fileio.is_bee_file(path.toLocalFile())): # 如果拖放事件的URL是本地文件,且是BeeRef文件 + self.control_target.open_from_file(path.toLocalFile()) # 打开拖放事件的URL,用于处理拖放事件 + return # 如果拖放事件的URL是本地文件,且是BeeRef文件,直接打开文件,不进行插入图片项操作 + self.control_target.do_insert_images(mimedata.urls(), pos) # 插入图片项槽函数,用于处理拖放事件,插入拖放事件的URL对应的图片项 + elif mimedata.hasImage(): # 如果拖放事件的MIME数据包含图像 + img = QtGui.QImage(mimedata.imageData()) # 获取拖放事件的图像数据,用于处理拖放事件 + item = BeePixmapItem(img) # 创建图片项,用于处理拖放事件 + pos = self.control_target.mapToScene(pos) # 记录拖放事件的位置,用于处理拖放事件 self.control_target.undo_stack.push( - commands.InsertItems(self.control_target.scene, [item], pos)) + commands.InsertItems(self.control_target.scene, [item], pos)) # 插入图片项槽函数,用于处理拖放事件,插入拖放事件的URL对应的图片项 else: - logger.info('Drop not an image') + logger.info('Drop not an image') # 拖放事件槽函数,用于处理拖放事件,记录拖放事件的MIME数据格式 - def mousePressEventMainControls(self, event): - if self.movewin_active: - self.exit_movewin_mode() - event.accept() - return True + def mousePressEventMainControls(self, event): # 鼠标按下事件槽函数,用于处理鼠标按下事件 + if self.movewin_active: # 如果移动窗口模式激活 + self.exit_movewin_mode() # 退出移动窗口模式槽函数,用于处理移动窗口模式的切换 + event.accept() # 接受鼠标按下事件的建议操作,用于处理鼠标按下事件 + return True # 如果移动窗口模式激活,直接返回True,不进行其他操作 action, inverted =\ - self.control_target.keyboard_settings.mouse_action_for_event(event) + self.control_target.keyboard_settings.mouse_action_for_event(event) # 获取鼠标按下事件对应的操作,用于处理鼠标按下事件 if action == 'movewindow': - self.enter_movewin_mode() - event.accept() - return True + self.enter_movewin_mode() # 进入移动窗口模式槽函数,用于处理移动窗口模式的切换 + event.accept() # 接受鼠标按下事件的建议操作,用于处理鼠标按下事件 + return True # 如果移动窗口模式激活,直接返回True,不进行其他操作 - def mouseMoveEventMainControls(self, event): - if self.movewin_active: - pos = self.mapToGlobal(event.position()) - delta = pos - self.event_start - self.event_start = pos - self.main_window.move(self.main_window.x() + int(delta.x()), - self.main_window.y() + int(delta.y())) - event.accept() - return True + def mouseMoveEventMainControls(self, event): # 鼠标移动事件槽函数,用于处理鼠标移动事件 + if self.movewin_active: # 如果移动窗口模式激活 + pos = self.mapToGlobal(event.position()) # 获取鼠标移动事件的全局位置,用于处理鼠标移动事件 + delta = pos - self.event_start # 计算鼠标移动事件的 delta 向量,用于处理鼠标移动事件 + self.event_start = pos # 更新鼠标移动事件的起始位置,用于处理鼠标移动事件 + self.main_window.move(self.main_window.x() + int(delta.x()), # 移动主窗口槽函数,用于处理移动窗口模式的切换 + self.main_window.y() + int(delta.y())) # 移动主窗口槽函数,用于处理移动窗口模式的切换 + event.accept() # 接受鼠标移动事件的建议操作,用于处理鼠标移动事件 + return True # 如果移动窗口模式激活,直接返回True,不进行其他操作 - def mouseReleaseEventMainControls(self, event): - if self.movewin_active: - self.exit_movewin_mode() - event.accept() - return True + def mouseReleaseEventMainControls(self, event): # 鼠标释放事件槽函数,用于处理鼠标释放事件 + if self.movewin_active: # 如果移动窗口模式激活 + self.exit_movewin_mode() # 退出移动窗口模式槽函数,用于处理移动窗口模式的切换 + event.accept() # 接受鼠标释放事件的建议操作,用于处理鼠标释放事件 + return True # 如果移动窗口模式激活,直接返回True,不进行其他操作 - def keyPressEventMainControls(self, event): - if self.movewin_active: - self.exit_movewin_mode() - event.accept() - return True + def keyPressEventMainControls(self, event): # 键盘按下事件槽函数,用于处理键盘按下事件 + if self.movewin_active: # 如果移动窗口模式激活 + self.exit_movewin_mode() # 退出移动窗口模式槽函数,用于处理移动窗口模式的切换 + event.accept() # 接受键盘按下事件的建议操作,用于处理键盘按下事件 + return True # 如果移动窗口模式激活,直接返回True,不进行其他操作 diff --git a/beeref/view.py b/beeref/view.py index 9e40532..a83be7c 100644 --- a/beeref/view.py +++ b/beeref/view.py @@ -34,737 +34,754 @@ from beeref.main_controls import MainControlsMixin from beeref.scene import BeeGraphicsScene from beeref.utils import get_file_extension_from_format, qcolor_to_hex - +# 全局命令行参数 commandline_args = CommandlineArgs() logger = logging.getLogger(__name__) - +# 主视图类,继承自QGraphicsView并混入自定义功能 +# 负责处理用户界面交互、图形渲染和场景管理 class BeeGraphicsView(MainControlsMixin, QtWidgets.QGraphicsView, ActionsMixin): - - PAN_MODE = 1 - ZOOM_MODE = 2 - SAMPLE_COLOR_MODE = 3 + + # 视图模式常量 + PAN_MODE = 1 # 平移模式 + ZOOM_MODE = 2 # 缩放模式 + SAMPLE_COLOR_MODE = 3 # 取色模式 def __init__(self, app, parent=None): - super().__init__(parent) - self.app = app - self.parent = parent - self.settings = BeeSettings() - self.keyboard_settings = KeyboardSettings() - self.welcome_overlay = widgets.welcome_overlay.WelcomeOverlay(self) + + super().__init__(parent)# 初始化父类 + self.app = app# 应用程序对象 + self.parent = parent # 父窗口部件 + self.settings = BeeSettings() # 应用程序设置 + self.keyboard_settings = KeyboardSettings() # 键盘设置 + self.welcome_overlay = widgets.welcome_overlay.WelcomeOverlay(self) # 欢迎 overlay 层 self.setBackgroundBrush( - QtGui.QBrush(QtGui.QColor(*constants.COLORS['Scene:Canvas']))) - self.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) - self.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - - self.undo_stack = QtGui.QUndoStack(self) - self.undo_stack.setUndoLimit(100) - self.undo_stack.canRedoChanged.connect(self.on_can_redo_changed) + QtGui.QBrush(QtGui.QColor(*constants.COLORS['Scene:Canvas']))) # 设置场景背景为画布颜色 + self.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) # 开启反锯齿渲染 + self.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) # 移除边框 + + self.undo_stack = QtGui.QUndoStack(self) # 初始化撤销栈 + self.undo_stack.setUndoLimit(100) # 设置撤销栈最大操作数为 100 + self.undo_stack.canRedoChanged.connect(self.on_can_redo_changed) + # 连接撤销栈的 canRedoChanged 信号到 on_can_redo_changed 槽函数 self.undo_stack.canUndoChanged.connect(self.on_can_undo_changed) + # 连接撤销栈的 canUndoChanged 信号到 on_can_undo_changed 槽函数 self.undo_stack.cleanChanged.connect(self.on_undo_clean_changed) + # 连接撤销栈的 cleanChanged 信号到 on_undo_clean_changed 槽函数 - self.filename = None - self.previous_transform = None - self.active_mode = None + self.filename = None# 当前打开的文件名 + self.previous_transform = None # 上一次的变换矩阵 + self.active_mode = None # 当前激活的模式 - self.scene = BeeGraphicsScene(self.undo_stack) - self.scene.changed.connect(self.on_scene_changed) - self.scene.selectionChanged.connect(self.on_selection_changed) - self.scene.cursor_changed.connect(self.on_cursor_changed) - self.scene.cursor_cleared.connect(self.on_cursor_cleared) - self.setScene(self.scene) + self.scene = BeeGraphicsScene(self.undo_stack) # 初始化场景 + self.scene.changed.connect(self.on_scene_changed) + # 连接场景的 changed 信号到 on_scene_changed 槽函数 + self.scene.selectionChanged.connect(self.on_selection_changed) + # 连接场景的 selectionChanged 信号到 on_selection_changed 槽函数 + self.scene.cursor_changed.connect(self.on_cursor_changed) + # 连接场景的 cursor_changed 信号到 on_cursor_changed 槽函数 + self.scene.cursor_cleared.connect(self.on_cursor_cleared) + # 连接场景的 cursor_cleared 信号到 on_cursor_cleared 槽函数 + self.setScene(self.scene) # 设置场景 # Context menu and actions - self.build_menu_and_actions() - self.control_target = self - self.init_main_controls(main_window=parent) + self.build_menu_and_actions() # 构建菜单和操作 + self.control_target = self # 设置控制目标为自身 + self.init_main_controls(main_window=parent)# 初始化主控件 # Load files given via command line - if commandline_args.filenames: - fn = commandline_args.filenames[0] - if os.path.splitext(fn)[1] == '.bee': - self.open_from_file(fn) + if commandline_args.filenames: # 如果命令行参数有文件名 + fn = commandline_args.filenames[0] # 获取第一个文件名 + if os.path.splitext(fn)[1] == '.bee': # 如果文件扩展名是 .bee + self.open_from_file(fn) # 从文件打开 else: - self.do_insert_images(commandline_args.filenames) + self.do_insert_images(commandline_args.filenames) # 否则插入图像 - self.update_window_title() + self.update_window_title() # 更新窗口标题 - @property - def filename(self): - return self._filename + @property # 文件名属性 + def filename(self): # 获取当前打开的文件名 + return self._filename # 返回当前打开的文件名 - @filename.setter - def filename(self, value): - self._filename = value - self.update_window_title() + @filename.setter # 文件名属性设置器 + def filename(self, value): # 设置当前打开的文件名 + self._filename = value # 设置当前打开的文件名 + self.update_window_title() # 更新窗口标题 if value: - self.settings.update_recent_files(value) - self.update_menu_and_actions() + self.settings.update_recent_files(value) # 更新最近打开的文件列表 + self.update_menu_and_actions() # 更新菜单和操作 - def cancel_active_modes(self): - self.scene.cancel_active_modes() - self.cancel_sample_color_mode() - self.active_mode = None + def cancel_active_modes(self): # 取消所有激活的模式 + self.scene.cancel_active_modes() # 取消场景中所有激活的模式 + self.cancel_sample_color_mode() # 取消取色模式 + self.active_mode = None # 重置当前激活的模式为 None - def cancel_sample_color_mode(self): - logger.debug('Cancel sample color mode') - self.active_mode = None - self.viewport().unsetCursor() + def cancel_sample_color_mode(self): # 取消取色模式 + logger.debug('Cancel sample color mode') # 调试日志:取消取色模式 + self.active_mode = None # 重置当前激活的模式为 None + self.viewport().unsetCursor() # 取消设置视口光标 if hasattr(self, 'sample_color_widget'): - self.sample_color_widget.hide() - del self.sample_color_widget + self.sample_color_widget.hide() # 隐藏取色小部件 + del self.sample_color_widget # 删除取色小部件 if self.scene.has_multi_selection(): - self.scene.multi_select_item.bring_to_front() + self.scene.multi_select_item.bring_to_front()# 将多选项 bring to front - def update_window_title(self): - clean = self.undo_stack.isClean() - if clean and not self.filename: - title = constants.APPNAME + def update_window_title(self): # 更新窗口标题 + clean = self.undo_stack.isClean() # 获取撤销栈是否为干净状态 + if clean and not self.filename: # 如果撤销栈为干净状态且没有文件名 + title = constants.APPNAME # 窗口标题为应用程序名称 else: - name = os.path.basename(self.filename or '[Untitled]') - clean = '' if clean else '*' - title = f'{name}{clean} - {constants.APPNAME}' - self.parent.setWindowTitle(title) + name = os.path.basename(self.filename or '[Untitled]') # 获取文件名(如果有),否则为 '[Untitled]' + clean = '' if clean else '*' # 如果撤销栈为干净状态,则不显示 '*',否则显示 '*' + title = f'{name}{clean} - {constants.APPNAME}' + # 窗口标题为文件名(如果有),否则为 '[Untitled]',后跟 '*'(如果撤销栈为不干净状态),最后为应用程序名称 + self.parent.setWindowTitle(title) # 设置父窗口标题为更新后的标题 - def on_scene_changed(self, region): - if not self.scene.items(): - logger.debug('No items in scene') - self.setTransform(QtGui.QTransform()) - self.welcome_overlay.setFocus() - self.clearFocus() - self.welcome_overlay.show() - self.actiongroup_set_enabled('active_when_items_in_scene', False) + def on_scene_changed(self, region): # 场景变化槽函数 + if not self.scene.items(): # 如果场景中没有物品 + logger.debug('No items in scene') # 调试日志:场景中没有物品 + self.setTransform(QtGui.QTransform()) # 重置变换矩阵为单位矩阵 + self.welcome_overlay.setFocus() # 设置欢迎叠加层为焦点 + self.clearFocus() # 清除当前焦点 + self.welcome_overlay.show() # 显示欢迎叠加层 + self.actiongroup_set_enabled('active_when_items_in_scene', False) # 禁用场景中物品相关操作 else: - self.setFocus() - self.welcome_overlay.clearFocus() - self.welcome_overlay.hide() - self.actiongroup_set_enabled('active_when_items_in_scene', True) - self.recalc_scene_rect() + self.setFocus(QtCore.Qt.PopupFocusReason) # 设置场景为焦点,使用 PopupFocusReason 原因 + self.welcome_overlay.clearFocus()# 清除欢迎叠加层焦点 + self.welcome_overlay.hide() # 隐藏欢迎叠加层 + self.actiongroup_set_enabled('active_when_items_in_scene', True) # 启用场景中物品相关操作 + self.recalc_scene_rect() # 重新计算场景矩形 - def on_can_redo_changed(self, can_redo): - self.actiongroup_set_enabled('active_when_can_redo', can_redo) + def on_can_redo_changed(self, can_redo): # 可以重做变化槽函数 + self.actiongroup_set_enabled('active_when_can_redo', can_redo) # 根据 can_redo 启用或禁用 'active_when_can_redo' 操作组 - def on_can_undo_changed(self, can_undo): - self.actiongroup_set_enabled('active_when_can_undo', can_undo) + def on_can_undo_changed(self, can_undo): # 可以撤销变化槽函数 + self.actiongroup_set_enabled('active_when_can_undo', can_undo) # 根据 can_undo 启用或禁用 'active_when_can_undo' 操作组 - def on_undo_clean_changed(self, clean): - self.update_window_title() + def on_undo_clean_changed(self, clean): # 撤销栈干净状态变化槽函数 + self.update_window_title() # 更新窗口标题 - def on_context_menu(self, point): - self.context_menu.exec(self.mapToGlobal(point)) + def on_context_menu(self, point): # 上下文菜单槽函数 + self.context_menu.exec(self.mapToGlobal(point)) # 在全局坐标中执行上下文菜单 - def get_supported_image_formats(self, cls): + def get_supported_image_formats(self, cls): # 获取支持的图像格式 formats = [] - for f in cls.supportedImageFormats(): - string = f'*.{f.data().decode()}' - formats.extend((string, string.upper())) - return ' '.join(formats) + for f in cls.supportedImageFormats(): # 遍历支持的图像格式 + string = f'*.{f.data().decode()}' # 转换为字符串格式 + formats.extend((string, string.upper())) # 扩展格式列表,包含小写和大写格式 + return ' '.join(formats) # 返回空格分隔的格式字符串 - def get_view_center(self): + def get_view_center(self): # 获取视图中心坐标 return QtCore.QPoint(round(self.size().width() / 2), - round(self.size().height() / 2)) + round(self.size().height() / 2)) # 返回视图中心坐标 - def clear_scene(self): - logging.debug('Clearing scene...') - self.cancel_active_modes() - self.scene.clear() - self.undo_stack.clear() - self.filename = None - self.setTransform(QtGui.QTransform()) + def clear_scene(self): # 清除场景 + logging.debug('Clearing scene...') # 调试日志:清除场景 + self.cancel_active_modes() # 取消所有激活的模式 + self.scene.clear() # 清除场景中的所有物品 + self.undo_stack.clear() # 清除撤销栈 + self.filename = None # 重置文件名为 None + self.setTransform(QtGui.QTransform()) # 重置变换矩阵为单位矩阵 - def reset_previous_transform(self, toggle_item=None): - if (self.previous_transform - and self.previous_transform['toggle_item'] != toggle_item): - self.previous_transform = None + def reset_previous_transform(self, toggle_item=None): # 重置之前的变换 + if (self.previous_transform # 如果有之前的变换 + and self.previous_transform['toggle_item'] != toggle_item): # 如果之前的变换与当前物品不同 + self.previous_transform = None # 重置之前的变换为 None + # 如果之前的变换与当前物品不同,将之前的变换重置为 None - def fit_rect(self, rect, toggle_item=None): - if toggle_item and self.previous_transform: - logger.debug('Fit view: Reset to previous') - self.setTransform(self.previous_transform['transform']) - self.centerOn(self.previous_transform['center']) - self.previous_transform = None + def fit_rect(self, rect, toggle_item=None): # 适合矩形 + if toggle_item and self.previous_transform: # 如果有之前的变换且与当前物品不同 + logger.debug('Fit view: Reset to previous') # 调试日志:重置到之前的变换 + self.setTransform(self.previous_transform['transform']) # 设置变换矩阵为之前的变换矩阵 + self.centerOn(self.previous_transform['center']) # 中心视图到之前的变换中心 + self.previous_transform = None # 重置之前的变换为 None return - if toggle_item: - self.previous_transform = { - 'toggle_item': toggle_item, - 'transform': QtGui.QTransform(self.transform()), - 'center': self.mapToScene(self.get_view_center()), + if toggle_item: # 如果有物品 + self.previous_transform = { # 存储当前变换 + 'toggle_item': toggle_item, # 存储当前物品 + 'transform': QtGui.QTransform(self.transform()), # 存储当前变换矩阵 + 'center': self.mapToScene(self.get_view_center()), # 存储当前视图中心坐标 } else: - self.previous_transform = None + self.previous_transform = None # 重置之前的变换为 None - logger.debug(f'Fit view: {rect}') - self.fitInView(rect, Qt.AspectRatioMode.KeepAspectRatio) - self.recalc_scene_rect() + logger.debug(f'Fit view: {rect}') # 调试日志:适合矩形 + self.fitInView(rect, Qt.AspectRatioMode.KeepAspectRatio) # 适合矩形,保持纵横比 + self.recalc_scene_rect() # 重新计算场景矩形 # It seems to be more reliable when we fit a second time # Sometimes a changing scene rect can mess up the fitting - self.fitInView(rect, Qt.AspectRatioMode.KeepAspectRatio) + self.fitInView(rect, Qt.AspectRatioMode.KeepAspectRatio) # 适合矩形,保持纵横比 + self.recalc_scene_rect() # 重新计算场景矩形 logger.trace('Fit view done') - def get_confirmation_unsaved_changes(self, msg): - confirm = self.settings.valueOrDefault('Save/confirm_close_unsaved') - if confirm and not self.undo_stack.isClean(): - answer = QtWidgets.QMessageBox.question( + def get_confirmation_unsaved_changes(self, msg): # 获取未保存更改确认 + confirm = self.settings.valueOrDefault('Save/confirm_close_unsaved')# 获取是否确认关闭未保存更改 + if confirm and not self.undo_stack.isClean(): # 如果确认关闭未保存更改且撤销栈不为干净状态 + answer = QtWidgets.QMessageBox.question( # 获取未保存更改确认 self, - 'Discard unsaved changes?', + 'Discard unsaved changes?', # 确认关闭未保存更改标题 msg, - QtWidgets.QMessageBox.StandardButton.Yes | - QtWidgets.QMessageBox.StandardButton.Cancel) + QtWidgets.QMessageBox.StandardButton.Yes | + QtWidgets.QMessageBox.StandardButton.Cancel) return answer == QtWidgets.QMessageBox.StandardButton.Yes return True - def on_action_new_scene(self): - confirm = self.get_confirmation_unsaved_changes( - 'There are unsaved changes. ' - 'Are you sure you want to open a new scene?') - if confirm: - self.clear_scene() + def on_action_new_scene(self): # 新建场景槽函数 + confirm = self.get_confirmation_unsaved_changes( # 获取新建场景确认 + 'There are unsaved changes. ' # 确认关闭未保存更改消息 + 'Are you sure you want to open a new scene?') # 确认新建场景消息 + if confirm: # 如果确认新建场景 + self.clear_scene() # 清除场景 - def on_action_fit_scene(self): - self.fit_rect(self.scene.itemsBoundingRect()) + def on_action_fit_scene(self): # 适合场景槽函数 + self.fit_rect(self.scene.itemsBoundingRect()) # 适合场景矩形 - def on_action_fit_selection(self): - self.fit_rect(self.scene.itemsBoundingRect(selection_only=True)) + def on_action_fit_selection(self): # 适合选择槽函数 + self.fit_rect(self.scene.itemsBoundingRect(selection_only=True)) # 适合选择矩形 - def on_action_fullscreen(self, checked): - if checked: - self.parent.showFullScreen() + def on_action_fullscreen(self, checked): # 全屏槽函数 + if checked: # 如果选中全屏 + self.parent.showFullScreen() # 显示全屏 else: - self.parent.showNormal() + self.parent.showNormal() # 显示正常 - def on_action_always_on_top(self, checked): - self.parent.setWindowFlag( - Qt.WindowType.WindowStaysOnTopHint, on=checked) - self.parent.destroy() - self.parent.create() - self.parent.show() + def on_action_always_on_top(self, checked): # 总是置顶槽函数 + self.parent.setWindowFlag( # 设置窗口标志 + Qt.WindowType.WindowStaysOnTopHint, on=checked) # 设置窗口置顶标志 + self.parent.destroy() # 销毁窗口 + self.parent.create() # 创建窗口 + self.parent.show() # 显示窗口 - def on_action_show_scrollbars(self, checked): - if checked: - self.setHorizontalScrollBarPolicy( - Qt.ScrollBarPolicy.ScrollBarAsNeeded) - self.setVerticalScrollBarPolicy( - Qt.ScrollBarPolicy.ScrollBarAsNeeded) - else: - self.setHorizontalScrollBarPolicy( - Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - self.setVerticalScrollBarPolicy( - Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + def on_action_show_scrollbars(self, checked): # 显示滚动条槽函数 + if checked: # 如果选中显示滚动条 + self.setHorizontalScrollBarPolicy( # 设置水平滚动条策略 + Qt.ScrollBarPolicy.ScrollBarAsNeeded) # 设置水平滚动条为按需显示 + self.setVerticalScrollBarPolicy( # 设置垂直滚动条策略 + Qt.ScrollBarPolicy.ScrollBarAsNeeded) # 设置垂直滚动条为按需显示 + else: # 如果未选中显示滚动条 + self.setHorizontalScrollBarPolicy( # 设置水平滚动条策略 + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) # 设置水平滚动条为始终关闭 + self.setVerticalScrollBarPolicy( # 设置垂直滚动条策略 + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) # 设置垂直滚动条为始终关闭 - def on_action_show_menubar(self, checked): - if checked: - self.parent.setMenuBar(self.create_menubar()) - else: - self.parent.setMenuBar(None) + def on_action_show_menubar(self, checked): # 显示菜单条槽函数 + if checked: # 如果选中显示菜单条 + self.parent.setMenuBar(self.create_menubar()) # 设置菜单条为创建的菜单条 + else: # 如果未选中显示菜单条 + self.parent.setMenuBar(None) # 设置菜单条为 None - def on_action_show_titlebar(self, checked): - self.parent.setWindowFlag( - Qt.WindowType.FramelessWindowHint, on=not checked) - self.parent.destroy() - self.parent.create() - self.parent.show() + def on_action_show_titlebar(self, checked): # 显示标题栏槽函数 + self.parent.setWindowFlag( # 设置窗口标志 + Qt.WindowType.FramelessWindowHint, on=not checked) # 设置窗口无框架标志 + self.parent.destroy() # 销毁窗口 + self.parent.create() # 创建窗口 + self.parent.show() # 显示窗口 - def on_action_move_window(self): - if self.welcome_overlay.isHidden(): - self.on_action_movewin_mode() - else: - self.welcome_overlay.on_action_movewin_mode() + def on_action_move_window(self): # 移动窗口槽函数 + if self.welcome_overlay.isHidden(): # 如果欢迎覆盖层隐藏 + self.on_action_movewin_mode() # 执行移动窗口模式 + else: # 如果欢迎覆盖层显示 + self.welcome_overlay.on_action_movewin_mode() # 执行欢迎覆盖层移动窗口模式 - def on_action_undo(self): - logger.debug('Undo: %s' % self.undo_stack.undoText()) + def on_action_undo(self): # 撤销槽函数 + logger.debug('Undo: %s' % self.undo_stack.undoText()) # 调试输出撤销文本 self.cancel_active_modes() self.undo_stack.undo() - def on_action_redo(self): - logger.debug('Redo: %s' % self.undo_stack.redoText()) + def on_action_redo(self): # 重做槽函数 + logger.debug('Redo: %s' % self.undo_stack.redoText()) # 调试输出重做文本 self.cancel_active_modes() self.undo_stack.redo() - def on_action_select_all(self): - self.scene.select_all_items() + def on_action_select_all(self): # 选择所有槽函数 + self.scene.select_all_items() # 选择所有项 - def on_action_deselect_all(self): - self.scene.deselect_all_items() + def on_action_deselect_all(self): # 取消选择所有槽函数 + self.scene.deselect_all_items() # 取消选择所有项 - def on_action_delete_items(self): - logger.debug('Deleting items...') - self.cancel_active_modes() - self.undo_stack.push( - commands.DeleteItems( - self.scene, self.scene.selectedItems(user_only=True))) + def on_action_delete_items(self): # 删除项槽函数 + logger.debug('Deleting items...') # 调试输出删除项消息 + self.cancel_active_modes() # 取消活动模式 + self.undo_stack.push( # 推送撤销栈 + commands.DeleteItems( # 删除项命令 + self.scene, self.scene.selectedItems(user_only=True))) # 删除用户选择的项 - def on_action_cut(self): - logger.debug('Cutting items...') - self.on_action_copy() - self.undo_stack.push( - commands.DeleteItems( - self.scene, self.scene.selectedItems(user_only=True))) + def on_action_cut(self): # 剪切槽函数 + logger.debug('Cutting items...') # 调试输出剪切项消息 + self.on_action_copy() # 执行复制操作 + self.undo_stack.push( # 推送撤销栈 + commands.DeleteItems( # 删除项命令 + self.scene, self.scene.selectedItems(user_only=True))) # 删除用户选择的项 - def on_action_raise_to_top(self): - self.scene.raise_to_top() + def on_action_raise_to_top(self): # Bring to Front槽函数 + self.scene.raise_to_top() # Bring to Front项 - def on_action_lower_to_bottom(self): - self.scene.lower_to_bottom() + def on_action_lower_to_bottom(self): # Send to Back槽函数 + self.scene.lower_to_bottom() # Send to Back项 - def on_action_normalize_height(self): - self.scene.normalize_height() + def on_action_normalize_height(self): # 归一化高度槽函数 + self.scene.normalize_height() # 归一化高度 - def on_action_normalize_width(self): - self.scene.normalize_width() + def on_action_normalize_width(self): # 归一化宽度槽函数 + self.scene.normalize_width() # 归一化宽度 - def on_action_normalize_size(self): - self.scene.normalize_size() + def on_action_normalize_size(self): # 归一化大小槽函数 + self.scene.normalize_size() # 归一化大小 - def on_action_arrange_horizontal(self): - self.scene.arrange() + def on_action_arrange_horizontal(self): # 水平排列槽函数 + self.scene.arrange() # 水平排列项 - def on_action_arrange_vertical(self): - self.scene.arrange(vertical=True) + def on_action_arrange_vertical(self): # 垂直排列槽函数 + self.scene.arrange(vertical=True) # 垂直排列项 - def on_action_arrange_optimal(self): - self.scene.arrange_optimal() + def on_action_arrange_optimal(self): # 最优排列槽函数 + self.scene.arrange_optimal() # 最优排列项 - def on_action_arrange_square(self): - self.scene.arrange_square() + def on_action_arrange_square(self): # 方排列槽函数 + self.scene.arrange_square() # 方排列项 - def on_action_change_opacity(self): - images = list(filter( - lambda item: item.is_image, - self.scene.selectedItems(user_only=True))) - widgets.ChangeOpacityDialog(self, images, self.undo_stack) + def on_action_change_opacity(self): # 改变不透明度槽函数 + images = list(filter( # 过滤出用户选择的图像项 + lambda item: item.is_image, # 过滤出图像项 + self.scene.selectedItems(user_only=True))) # 过滤出用户选择的图像项 + widgets.ChangeOpacityDialog(self, images, self.undo_stack) # 显示改变不透明度对话框 - def on_action_grayscale(self, checked): - images = list(filter( - lambda item: item.is_image, - self.scene.selectedItems(user_only=True))) + def on_action_grayscale(self, checked): # 灰度槽函数 + images = list(filter( # 过滤出用户选择的图像项 + lambda item: item.is_image, # 过滤出图像项 + self.scene.selectedItems(user_only=True))) # 过滤出用户选择的图像项 if images: self.undo_stack.push( - commands.ToggleGrayscale(images, checked)) + commands.ToggleGrayscale(images, checked)) # 推送撤销栈,执行切换灰度命令 - def on_action_crop(self): - self.scene.crop_items() + def on_action_crop(self): # 裁剪槽函数 + self.scene.crop_items() # 裁剪项 - def on_action_flip_horizontally(self): - self.scene.flip_items(vertical=False) + def on_action_flip_horizontally(self): # 水平翻转槽函数 + self.scene.flip_items(vertical=False) # 水平翻转项 - def on_action_flip_vertically(self): - self.scene.flip_items(vertical=True) + def on_action_flip_vertically(self): # 垂直翻转槽函数 + self.scene.flip_items(vertical=True) # 垂直翻转项 - def on_action_reset_scale(self): - self.cancel_active_modes() - self.undo_stack.push(commands.ResetScale( - self.scene.selectedItems(user_only=True))) + def on_action_reset_scale(self): # 重置缩放槽函数 + self.cancel_active_modes() # 取消活动模式 + self.undo_stack.push(commands.ResetScale( # 推送撤销栈,执行重置缩放命令 + self.scene.selectedItems(user_only=True))) # 推送撤销栈,执行重置缩放命令 + def on_action_reset_rotation(self): # 重置旋转槽函数 + self.cancel_active_modes() # 取消活动模式 + self.undo_stack.push(commands.ResetRotation( # 推送撤销栈,执行重置旋转命令 + self.scene.selectedItems(user_only=True))) # 推送撤销栈,执行重置旋转命令 - def on_action_reset_rotation(self): - self.cancel_active_modes() - self.undo_stack.push(commands.ResetRotation( - self.scene.selectedItems(user_only=True))) - - def on_action_reset_flip(self): - self.cancel_active_modes() - self.undo_stack.push(commands.ResetFlip( - self.scene.selectedItems(user_only=True))) - - def on_action_reset_crop(self): - self.cancel_active_modes() - self.undo_stack.push(commands.ResetCrop( - self.scene.selectedItems(user_only=True))) + def on_action_reset_flip(self): # 重置翻转槽函数 + self.cancel_active_modes() # 取消活动模式 + self.undo_stack.push(commands.ResetFlip( # 推送撤销栈,执行重置翻转命令 + self.scene.selectedItems(user_only=True))) # 推送撤销栈,执行重置翻转命令 + def on_action_reset_crop(self): # 重置裁剪槽函数 + self.cancel_active_modes() # 取消活动模式 + self.undo_stack.push(commands.ResetCrop( # 推送撤销栈,执行重置裁剪命令 + self.scene.selectedItems(user_only=True))) # 推送撤销栈,执行重置裁剪命令 def on_action_reset_transforms(self): - self.cancel_active_modes() - self.undo_stack.push(commands.ResetTransforms( - self.scene.selectedItems(user_only=True))) + self.cancel_active_modes() # 取消活动模式 + self.undo_stack.push(commands.ResetTransforms( # 推送撤销栈,执行重置变换命令 + self.scene.selectedItems(user_only=True))) # 推送撤销栈,执行重置变换命令 - def on_action_show_color_gamut(self): - widgets.color_gamut.GamutDialog(self, self.scene.selectedItems()[0]) + def on_action_show_color_gamut(self): # 显示颜色 gamut 槽函数 + widgets.color_gamut.GamutDialog(self, self.scene.selectedItems()[0]) # 显示颜色 gamut 对话框 - def on_action_sample_color(self): - self.cancel_active_modes() - logger.debug('Entering sample color mode') - self.viewport().setCursor(Qt.CursorShape.CrossCursor) - self.active_mode = self.SAMPLE_COLOR_MODE + def on_action_sample_color(self): # 采样颜色槽函数 + self.cancel_active_modes() # 取消活动模式 + logger.debug('Entering sample color mode') # 调试输出采样颜色消息 + self.viewport().setCursor(Qt.CursorShape.CrossCursor) # 设置鼠标光标为十字光标 + self.active_mode = self.SAMPLE_COLOR_MODE # 设置活动模式为采样颜色模式 - if self.scene.has_multi_selection(): + if self.scene.has_multi_selection(): # 如果有多个选择项 # We don't want to sample the multi select item, so # temporarily send it to the back: - self.scene.multi_select_item.lower_behind_selection() + self.scene.multi_select_item.lower_behind_selection() # 临时将多选项发送到后台 - pos = self.mapFromGlobal(self.cursor().pos()) + pos = self.mapFromGlobal(self.cursor().pos()) # 获取鼠标光标在场景中的位置 self.sample_color_widget = widgets.SampleColorWidget( self, pos, - self.scene.sample_color_at(self.mapToScene(pos))) + self.scene.sample_color_at(self.mapToScene(pos))) # 创建采样颜色小部件 - def on_items_loaded(self, value): - logger.debug('On items loaded: add queued items') - self.scene.add_queued_items() + def on_items_loaded(self, value): # 加载项槽函数 + logger.debug('On items loaded: add queued items') # 调试输出加载项消息 + self.scene.add_queued_items() # 添加队列中的项 - def on_loading_finished(self, filename, errors): - if errors: - QtWidgets.QMessageBox.warning( + def on_loading_finished(self, filename, errors): # 加载完成槽函数 + if errors: # 如果有错误 + QtWidgets.QMessageBox.warning( # 显示警告对话框 self, 'Problem loading file', ('
Problem loading file %s
' - 'Not accessible or not a proper bee file
') % filename) + 'Not accessible or not a proper bee file
') % filename) # 显示警告对话框,提示文件不可访问或不是 proper bee 文件 else: - self.filename = filename - self.scene.add_queued_items() - self.on_action_fit_scene() + self.filename = filename # 设置文件名 + self.scene.add_queued_items() # 添加队列中的项 + self.on_action_fit_scene() # 拟合场景 - def on_action_open_recent_file(self, filename): - confirm = self.get_confirmation_unsaved_changes( + def on_action_open_recent_file(self, filename):# 打开最近文件槽函数 + confirm = self.get_confirmation_unsaved_changes( # 获取确认未保存更改消息 'There are unsaved changes. ' 'Are you sure you want to open a new scene?') - if confirm: - self.open_from_file(filename) + if confirm: # 如果确认 + self.open_from_file(filename) # 从文件打开场景 - def open_from_file(self, filename): - logger.info(f'Opening file {filename}') - self.clear_scene() - self.worker = fileio.ThreadedIO( - fileio.load_bee, filename, self.scene) - self.worker.progress.connect(self.on_items_loaded) - self.worker.finished.connect(self.on_loading_finished) - self.progress = widgets.BeeProgressDialog( - f'Loading {filename}', - worker=self.worker, - parent=self) - self.worker.start() + def open_from_file(self, filename): # 从文件打开场景槽函数 + logger.info(f'Opening file {filename}') # 调试输出打开文件消息 + self.clear_scene() # 清除场景 + self.worker = fileio.ThreadedIO( # 创建线程化 IO 工作器 + fileio.load_bee, filename, self.scene) # 加载 bee 文件,将项添加到场景中 + self.worker.progress.connect(self.on_items_loaded) # 连接进度信号到加载项槽函数 + self.worker.finished.connect(self.on_loading_finished) # 连接完成信号到加载完成槽函数 + self.progress = widgets.BeeProgressDialog( # 创建进度对话框 + f'Loading {filename}', # 进度对话框标题 + worker=self.worker, # 关联线程化 IO 工作器 + parent=self) # 设置进度对话框父窗口为主窗口 + self.worker.start() # 启动线程化 IO 工作器,开始加载文件 - def on_action_open(self): - confirm = self.get_confirmation_unsaved_changes( - 'There are unsaved changes. ' - 'Are you sure you want to open a new scene?') - if not confirm: - return + def on_action_open(self): # 打开文件槽函数 + confirm = self.get_confirmation_unsaved_changes( # 获取确认未保存更改消息 + 'There are unsaved changes. ' # 提示有未保存更改 + 'Are you sure you want to open a new scene?') # 提示确认打开新场景 + if not confirm: # 如果取消 + return # 如果取消,返回 - self.cancel_active_modes() - filename, f = QtWidgets.QFileDialog.getOpenFileName( - parent=self, - caption='Open file', - filter=f'{constants.APPNAME} File (*.bee)') - if filename: - filename = os.path.normpath(filename) - self.open_from_file(filename) - self.filename = filename + self.cancel_active_modes() # 取消活动模式 + filename, f = QtWidgets.QFileDialog.getOpenFileName( # 获取打开文件对话框 + parent=self, # 设置进度对话框父窗口为主窗口 + caption='Open file', # 打开文件对话框标题 + filter=f'{constants.APPNAME} File (*.bee)') # 打开文件对话框筛选器,仅显示 bee 文件 + if filename: # 如果选择了文件 + filename = os.path.normpath(filename) # 规范文件名路径 + self.open_from_file(filename) # 从文件打开场景 + self.filename = filename # 设置文件名 - def on_saving_finished(self, filename, errors): - if errors: - QtWidgets.QMessageBox.warning( - self, - 'Problem saving file', - ('Problem saving file %s
' - 'File/directory not accessible
') % filename) + def on_saving_finished(self, filename, errors): # 保存完成槽函数 + if errors: # 如果有错误 + QtWidgets.QMessageBox.warning( # 显示警告对话框 + self, # 警告对话框父窗口为主窗口 + 'Problem saving file', # 警告对话框标题 + ('Problem saving file %s
' # 警告对话框内容,提示保存文件时出现问题 + 'File/directory not accessible
') % filename) # 警告对话框内容,提示文件或目录不可访问 else: - self.filename = filename - self.undo_stack.setClean() + self.filename = filename # 设置文件名 + self.undo_stack.setClean() # 设置撤销栈为干净状态 - def do_save(self, filename, create_new): - if not fileio.is_bee_file(filename): - filename = f'{filename}.bee' - self.worker = fileio.ThreadedIO( - fileio.save_bee, filename, self.scene, create_new=create_new) - self.worker.finished.connect(self.on_saving_finished) - self.progress = widgets.BeeProgressDialog( - f'Saving {filename}', - worker=self.worker, - parent=self) - self.worker.start() + def do_save(self, filename, create_new): # 保存场景槽函数 + if not fileio.is_bee_file(filename): # 如果不是 bee 文件 + filename = f'{filename}.bee' # 添加 bee 文件扩展名 + self.worker = fileio.ThreadedIO( # 创建线程化 IO 工作器 + fileio.save_bee, filename, self.scene, create_new=create_new) # 保存 bee 文件,将场景项保存到文件中 + self.worker.finished.connect(self.on_saving_finished) # 连接完成信号到保存完成槽函数 + self.progress = widgets.BeeProgressDialog( # 创建进度对话框 + f'Saving {filename}', # 进度对话框标题 + worker=self.worker, # 关联线程化 IO 工作器 + parent=self) # 设置进度对话框父窗口为主窗口 + self.worker.start() # 启动线程化 IO 工作器,开始保存文件 - def on_action_save_as(self): - self.cancel_active_modes() - directory = os.path.dirname(self.filename) if self.filename else None - filename, f = QtWidgets.QFileDialog.getSaveFileName( - parent=self, - caption='Save file', - directory=directory, - filter=f'{constants.APPNAME} File (*.bee)') - if filename: - self.do_save(filename, create_new=True) + def on_action_save_as(self): # 保存为槽函数 + self.cancel_active_modes() # 取消活动模式 + directory = os.path.dirname(self.filename) if self.filename else None # 如果有文件名,获取文件名所在目录,否则为 None + filename, f = QtWidgets.QFileDialog.getSaveFileName( # 获取保存文件对话框 + parent=self, # 设置进度对话框父窗口为主窗口 + caption='Save file', # 保存文件对话框标题 + directory=directory, # 保存文件对话框默认目录为当前文件名所在目录 + filter=f'{constants.APPNAME} File (*.bee)') # 保存文件对话框筛选器,仅显示 bee 文件 + if filename: # 如果选择了文件 + self.do_save(filename, create_new=True) # 保存场景为新文件 - def on_action_save(self): - self.cancel_active_modes() - if not self.filename: - self.on_action_save_as() + def on_action_save(self): # 保存槽函数 + self.cancel_active_modes() # 取消活动模式 + if not self.filename: # 如果没有文件名 + self.on_action_save_as() # 调用保存为槽函数 else: - self.do_save(self.filename, create_new=False) + self.do_save(self.filename, create_new=False) # 保存场景到当前文件名,不创建新文件 - def on_action_export_scene(self): - directory = os.path.dirname(self.filename) if self.filename else None - filename, formatstr = QtWidgets.QFileDialog.getSaveFileName( - parent=self, - caption='Export Scene to Image', - directory=directory, + def on_action_export_scene(self): # 导出场景槽函数 + directory = os.path.dirname(self.filename) if self.filename else None # 如果有文件名,获取文件名所在目录,否则为 None + filename, formatstr = QtWidgets.QFileDialog.getSaveFileName( # 获取保存文件对话框 + parent=self, # 设置进度对话框父窗口为主窗口 + caption='Export Scene to Image', # 导出场景对话框标题 + directory=directory, # 导出场景对话框默认目录为当前文件名所在目录 filter=';;'.join(('Image Files (*.png *.jpg *.jpeg *.svg)', 'PNG (*.png)', 'JPEG (*.jpg *.jpeg)', 'SVG (*.svg)'))) - if not filename: - return + if not filename: # 如果没有文件名 + return # 如果没有文件名,返回 - name, ext = os.path.splitext(filename) - if not ext: - ext = get_file_extension_from_format(formatstr) - filename = f'{filename}.{ext}' - logger.debug(f'Got export filename {filename}') + name, ext = os.path.splitext(filename) # 从文件名中分离出文件名和扩展名 + if not ext: # 如果没有扩展名 + ext = get_file_extension_from_format(formatstr) # 从格式字符串中获取文件扩展名 + filename = f'{filename}.{ext}' # 添加文件扩展名 + logger.debug(f'Got export filename {filename}') # 调试日志,提示导出文件名 - exporter_cls = exporter_registry[ext] - exporter = exporter_cls(self.scene) - if not exporter.get_user_input(self): - return + exporter_cls = exporter_registry[ext] # 从导出器注册表中获取导出器类 + exporter = exporter_cls(self.scene) # 创建导出器实例,将场景项传递给导出器 + if not exporter.get_user_input(self): # 如果导出器需要用户输入 + return # 如果导出器需要用户输入,返回 - self.worker = fileio.ThreadedIO(exporter.export, filename) - self.worker.finished.connect(self.on_export_finished) - self.progress = widgets.BeeProgressDialog( - f'Exporting {filename}', - worker=self.worker, - parent=self) - self.worker.start() + self.worker = fileio.ThreadedIO(exporter.export, filename) # 创建线程化 IO 工作器,将导出函数和文件名传递给工作器 + self.worker.finished.connect(self.on_export_finished) # 连接完成信号到导出完成槽函数 + self.progress = widgets.BeeProgressDialog( # 创建进度对话框 + f'Exporting {filename}', # 进度对话框标题 + worker=self.worker, # 关联线程化 IO 工作器 + parent=self) # 设置进度对话框父窗口为主窗口 + self.worker.start() # 启动线程化 IO 工作器,开始导出文件 - def on_export_finished(self, filename, errors): - if errors: - err_msg = ''.join(str(errors)) - QtWidgets.QMessageBox.warning( - self, - 'Problem writing file', - f'Problem writing file {filename}
{err_msg}
') + def on_export_finished(self, filename, errors): # 导出完成槽函数 + if errors: # 如果有错误 + err_msg = ''.join(str(errors)) # 将错误列表转换为字符串,每个错误之间用换行符分隔 + QtWidgets.QMessageBox.warning( # 显示警告对话框 + self, # 警告对话框父窗口为主窗口 + 'Problem writing file', # 警告对话框标题 + f'Problem writing file {filename}
{err_msg}
') # 警告对话框内容,提示导出文件时出现问题,包含文件名和错误信息 - def on_action_export_images(self): - directory = os.path.dirname(self.filename) if self.filename else None - directory = QtWidgets.QFileDialog.getExistingDirectory( - parent=self, - caption='Export Images', - directory=directory) + def on_action_export_images(self): # 导出图像槽函数 + directory = os.path.dirname(self.filename) if self.filename else None # 如果有文件名,获取文件名所在目录,否则为 None + directory = QtWidgets.QFileDialog.getExistingDirectory( # 获取导出图像对话框 + parent=self, # 设置进度对话框父窗口为主窗口 + caption='Export Images', # 导出图像对话框标题 + directory=directory) # 获取导出图像对话框默认目录为当前文件名所在目录 - if not directory: - return + if not directory: # 如果没有目录 + return # 如果没有目录,返回 - logger.debug(f'Got export directory {directory}') - self.exporter = ImagesToDirectoryExporter(self.scene, directory) - self.worker = fileio.ThreadedIO(self.exporter.export) - self.worker.user_input_required.connect( - self.on_export_images_file_exists) - self.worker.finished.connect(self.on_export_finished) - self.progress = widgets.BeeProgressDialog( - f'Exporting to {directory}', - worker=self.worker, - parent=self) - self.worker.start() + logger.debug(f'Got export directory {directory}') # 调试日志,提示导出目录 + self.exporter = ImagesToDirectoryExporter(self.scene, directory) # 创建导出器实例,将场景项和目录传递给导出器 + self.worker = fileio.ThreadedIO(self.exporter.export) # 创建线程化 IO 工作器,将导出函数传递给工作器 + self.worker.user_input_required.connect( # 连接用户输入信号到导出图像文件存在槽函数 + self.on_export_images_file_exists) # 连接用户输入信号到导出图像文件存在槽函数 + self.worker.finished.connect(self.on_export_finished) # 连接完成信号到导出完成槽函数 + self.progress = widgets.BeeProgressDialog( # 创建进度对话框 + f'Exporting to {directory}', # 进度对话框标题 + worker=self.worker, # 关联线程化 IO 工作器 + parent=self) # 设置进度对话框父窗口为主窗口 + self.worker.start() # 启动线程化 IO 工作器,开始导出文件 - def on_export_images_file_exists(self, filename): - dlg = widgets.ExportImagesFileExistsDialog(self, filename) - if dlg.exec() == QtWidgets.QDialog.DialogCode.Accepted: - self.exporter.handle_existing = dlg.get_answer() - directory = self.exporter.dirname - self.progress = widgets.BeeProgressDialog( - f'Exporting to {directory}', - worker=self.worker, - parent=self) - self.worker.start() + def on_export_images_file_exists(self, filename): # 导出图像文件存在槽函数 + dlg = widgets.ExportImagesFileExistsDialog(self, filename) # 创建导出图像文件存在对话框,将主窗口和文件名传递给对话框 + if dlg.exec() == QtWidgets.QDialog.DialogCode.Accepted: # 如果用户点击了接受按钮 + self.exporter.handle_existing = dlg.get_answer() # 获取用户选择的处理方式,将其赋值给导出器的 handle_existing 属性 + directory = self.exporter.dirname # 获取导出器的目录名,将其赋值给 directory 变量 + self.progress = widgets.BeeProgressDialog( # 创建进度对话框 + f'Exporting to {directory}', # 进度对话框标题 + worker=self.worker, # 关联线程化 IO 工作器 + parent=self) # 设置进度对话框父窗口为主窗口 + self.worker.start() # 启动线程化 IO 工作器,开始导出文件 - def on_action_quit(self): - confirm = self.get_confirmation_unsaved_changes( - 'There are unsaved changes. Are you sure you want to quit?') - if confirm: - logger.info('User quit. Exiting...') - self.app.quit() + def on_action_quit(self): # 退出槽函数 + confirm = self.get_confirmation_unsaved_changes( # 获取确认未保存更改对话框 + 'There are unsaved changes. Are you sure you want to quit?') # 获取确认未保存更改对话框,将提示信息传递给对话框 + if confirm: # 如果用户点击了接受按钮 + logger.info('User quit. Exiting...') # 调试日志,提示用户退出 + self.app.quit() # 退出应用程序 - def on_action_settings(self): - widgets.settings.SettingsDialog(self) + def on_action_settings(self): # 设置槽函数 + widgets.settings.SettingsDialog(self) # 创建设置对话框,将主窗口传递给对话框 - def on_action_keyboard_settings(self): - widgets.controls.ControlsDialog(self) + def on_action_keyboard_settings(self): # 键盘设置槽函数 + widgets.controls.ControlsDialog(self) # 创建键盘设置对话框,将主窗口传递给对话框 - def on_action_help(self): - widgets.HelpDialog(self) + def on_action_help(self): # 帮助槽函数 + widgets.HelpDialog(self) # 创建帮助对话框,将主窗口传递给对话框 - def on_action_about(self): - QtWidgets.QMessageBox.about( - self, - f'About {constants.APPNAME}', - (f'{constants.APPNAME_FULL}
' - f'{constants.COPYRIGHT}
' - f'' - f'Visit the {constants.APPNAME} website
')) + def on_action_about(self): # 关于槽函数 + QtWidgets.QMessageBox.about( # 显示关于对话框 + self, # 关于对话框父窗口为主窗口 + f'About {constants.APPNAME}', # 关于对话框标题 + (f'{constants.APPNAME_FULL}
' # 关于对话框内容,包含应用程序完整名称 + f'{constants.COPYRIGHT}
' # 关于对话框内容,包含应用程序版权信息 + f'' # 关于对话框内容,包含应用程序网站链接 + f'Visit the {constants.APPNAME} website
')) # 关于对话框内容,包含应用程序网站链接 - def on_action_debuglog(self): - widgets.DebugLogDialog(self) + def on_action_debuglog(self): # 调试日志槽函数 + widgets.DebugLogDialog(self) # 创建设置对话框,将主窗口传递给对话框 - def on_insert_images_finished(self, new_scene, filename, errors): - """Callback for when loading of images is finished. + def on_insert_images_finished(self, new_scene, filename, errors): # 插入图像完成槽函数 + """Callback for when loading of images is finished. # :param new_scene: True if the scene was empty before, else False :param filename: Not used, for compatibility only :param errors: List of filenames that couldn't be loaded """ + + logger.debug('Insert images finished. Errors: %s', errors) # 调试日志,提示插入图像完成,包含错误文件名列表 + if errors: # 如果有错误文件名 + errornames = [ # 生成错误文件名列表,每个文件名前添加