diff --git a/beeref/scene.py b/beeref/scene.py index 22d7e02..eeda235 100644 --- a/beeref/scene.py +++ b/beeref/scene.py @@ -265,302 +265,303 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene): # 定义场景类,继承自 # We want the items to center around the selection's center, # not (0, 0) - center = self.get_selection_center() - bounds = rpack.bbox_size(sizes, positions) - diff = center - QtCore.QPointF(bounds[0]/2, bounds[1]/2) - positions = [QtCore.QPointF(*pos) + diff for pos in positions] + center = self.get_selection_center() # 获取选择项目的中心位置 + bounds = rpack.bbox_size(sizes, positions) # 计算打包后的边界矩形大小 + diff = center - QtCore.QPointF(bounds[0]/2, bounds[1]/2) # 计算中心位置与边界矩形中心位置的差值 + positions = [QtCore.QPointF(*pos) + diff for pos in positions] # 为每个项目计算新的位置,考虑中心位置的偏移 - self.undo_stack.push(commands.ArrangeItems(self, items, positions)) + self.undo_stack.push(commands.ArrangeItems(self, items, positions)) # 将排列操作添加到撤销栈中 - def arrange_square(self): - self.cancel_active_modes() - max_width = 0 - max_height = 0 - gap = self.settings.valueOrDefault('Items/arrange_gap') - items = sort_by_filename(self.selectedItems(user_only=True)) + def arrange_square(self): # 按正方形方式排列选择的项目 + self.cancel_active_modes() # 取消所有活动模式,确保没有正在进行的操作干扰 + max_width = 0 # 初始化最大宽度为0 + max_height = 0 # 初始化最大高度为0 + gap = self.settings.valueOrDefault('Items/arrange_gap') # 获取项目之间的间距 + items = sort_by_filename(self.selectedItems(user_only=True)) # 获取当前用户选择的项目,并按文件名排序 - if len(items) < 2: - return + if len(items) < 2: # 如果选择的项目数量小于2 + return # 直接返回,无需排列 - for item in items: - rect = self.itemsBoundingRect(items=[item]) - max_width = max(max_width, rect.width() + gap) - max_height = max(max_height, rect.height() + gap) + for item in items: # 遍历选择的项目 + rect = self.itemsBoundingRect(items=[item]) # 获取项目的边界矩形 + max_width = max(max_width, rect.width() + gap) # 更新最大宽度,考虑项目宽度和间距 + max_height = max(max_height, rect.height() + gap) # 更新最大高度,考虑项目高度和间距 # We want the items to center around the selection's center, # not (0, 0) - num_rows = math.ceil(math.sqrt(len(items))) - center = self.get_selection_center() - diff = center - num_rows/2 * QtCore.QPointF(max_width, max_height) + num_rows = math.ceil(math.sqrt(len(items))) # 计算需要的行数,向上取整 + center = self.get_selection_center() # 获取选择项目的中心位置 + diff = center - num_rows/2 * QtCore.QPointF(max_width, max_height) # 计算中心位置与边界矩形中心位置的差值 - iter_items = iter(items) - positions = [] - for j in range(num_rows): - for i in range(num_rows): + iter_items = iter(items) # 创建一个迭代器,用于遍历选择的项目 + positions = [] # 创建一个空列表,用于存储每个项目的新位置 + for j in range(num_rows):# 遍历行数 + for i in range(num_rows):# 遍历列数 try: - item = next(iter_items) - rect = self.itemsBoundingRect(items=[item]) - point = QtCore.QPointF( - i * max_width + (max_width - rect.width())/2, - j * max_height + (max_height - rect.height())/2) - positions.append(point + diff) - except StopIteration: - break + item = next(iter_items) # 从迭代器中获取下一个项目 + rect = self.itemsBoundingRect(items=[item]) # 获取项目的边界矩形 + point = QtCore.QPointF( # 计算项目的新位置 + i * max_width + (max_width - rect.width())/2, # 计算项目在当前行的水平位置 + j * max_height + (max_height - rect.height())/2) # 计算项目在当前列的垂直位置 + positions.append(point + diff) # 计算项目的新位置,考虑中心位置的偏移 + except StopIteration:# 当迭代器没有更多项目时,跳出循环 + break # 跳出当前列的循环,继续下一行 - self.undo_stack.push(commands.ArrangeItems(self, items, positions)) + self.undo_stack.push(commands.ArrangeItems(self, items, positions)) # 将排列操作添加到撤销栈中 - def flip_items(self, vertical=False): + def flip_items(self, vertical=False): # 垂直或水平翻转选择的项目 """Flip selected items.""" - self.cancel_active_modes() + self.cancel_active_modes() # 取消所有活动模式,确保没有正在进行的操作干扰 self.undo_stack.push( commands.FlipItems(self.selectedItems(user_only=True), self.get_selection_center(), - vertical=vertical)) + vertical=vertical)) # 将翻转操作添加到撤销栈中 - def crop_items(self): + def crop_items(self): # 对选择的项目进行裁剪 """Crop selected item.""" - if self.crop_item: + if self.crop_item:## 如果当前有项目正在裁剪状态 return - if self.has_single_image_selection(): + if self.has_single_image_selection():# 如果选择的项目是单个图像 item = self.selectedItems(user_only=True)[0] if item.is_image: - item.enter_crop_mode() + item.enter_crop_mode() # 进入图像裁剪模式 - def sample_color_at(self, position): - item_at_pos = self.itemAt(position, self.views()[0].transform()) + def sample_color_at(self, position): # 采样指定位置的颜色 + item_at_pos = self.itemAt(position, self.views()[0].transform()) # 获取指定位置上的项目 if item_at_pos: - return item_at_pos.sample_color_at(position) + return item_at_pos.sample_color_at(position) # 如果项目存在,采样该位置的颜色 - def select_all_items(self): - self.cancel_active_modes() - path = QtGui.QPainterPath() - path.addRect(self.itemsBoundingRect()) + def select_all_items(self): # 选择所有项目 + self.cancel_active_modes() # 取消所有活动模式,确保没有正在进行的操作干扰 + path = QtGui.QPainterPath() # 创建一个空的绘图路径 + path.addRect(self.itemsBoundingRect()) # 创建一个矩形路径,包含所有项目的边界矩形 + path.addRect(self.itemsBoundingRect()) + path.addRect(self.itemsBoundingRect()) # This is faster than looping through all items and calling setSelected - self.setSelectionArea(path) + self.setSelectionArea(path)# 设置选择区域为创建的路径,包含所有项目的边界矩形 - def deselect_all_items(self): - self.cancel_active_modes() - self.clearSelection() + def deselect_all_items(self):# 取消所有项目的选择 + self.cancel_active_modes() # 取消所有活动模式,确保没有正在进行的操作干扰 + self.clearSelection() # 清除所有项目的选择状态 - def has_selection(self): + def has_selection(self):# 检查是否有项目被选择 """Checks whether there are currently items selected.""" - return bool(self.selectedItems(user_only=True)) + return bool(self.selectedItems(user_only=True)) # 返回当前用户选择的项目是否为空 - def has_single_selection(self): + def has_single_selection(self):# 检查是否有单个项目被选择 """Checks whether there's currently exactly one item selected.""" - return len(self.selectedItems(user_only=True)) == 1 + return len(self.selectedItems(user_only=True)) == 1 # 返回当前用户选择的项目是否只有一个 - def has_multi_selection(self): + def has_multi_selection(self):# 检查是否有多个项目被选择 """Checks whether there are currently more than one items selected.""" - return len(self.selectedItems(user_only=True)) > 1 + return len(self.selectedItems(user_only=True)) > 1 # 返回当前用户选择的项目是否有多个 - def has_single_image_selection(self): + def has_single_image_selection(self):# 检查是否有单个图像项目被选择 """Checks whether the current selection is a single image.""" - if self.has_single_selection(): - return self.selectedItems(user_only=True)[0].is_image - return False + if self.has_single_selection(): # 如果只有一个项目被选择 + return self.selectedItems(user_only=True)[0].is_image # 检查该项目是否为图像 + return False # 如果不是图像或没有项目被选择,返回False - def mousePressEvent(self, event): - if event.button() == Qt.MouseButton.RightButton: + def mousePressEvent(self, event):# 处理鼠标按下事件 + if event.button() == Qt.MouseButton.RightButton: # 如果是右键点击 # Right-click invokes the context menu on the # GraphicsView. We don't need it here. return - if event.button() == Qt.MouseButton.LeftButton: - self.event_start = event.scenePos() + if event.button() == Qt.MouseButton.LeftButton: # 如果是左键点击 + self.event_start = event.scenePos() # 记录鼠标按下时的场景位置 item_at_pos = self.itemAt( - event.scenePos(), self.views()[0].transform()) + event.scenePos(), self.views()[0].transform()) # 获取当前鼠标位置上的项目 - if self.edit_item: - if item_at_pos != self.edit_item: - self.edit_item.exit_edit_mode() - else: - super().mousePressEvent(event) - return - if self.crop_item: - if item_at_pos != self.crop_item: - self.cancel_crop_mode() - else: - super().mousePressEvent(event) - return - if item_at_pos: - self.active_mode = self.MOVE_MODE - elif self.items(): - self.active_mode = self.RUBBERBAND_MODE + if self.edit_item and item_at_pos != self.edit_item: # 如果当前有项目正在编辑状态,且点击的项目不是当前编辑项目 + self.edit_item.exit_edit_mode() # 退出当前编辑项目的编辑状态 + elif self.edit_item and item_at_pos == self.edit_item: # 如果当前有项目正在编辑状态,且点击的项目是当前编辑项目 + super().mousePressEvent(event) # 调用父类的鼠标按下事件处理方法,确保正常编辑操作 + return # 结束当前方法,避免后续重复处理 + if self.crop_item and item_at_pos != self.crop_item: # 如果当前有项目正在裁剪状态,且点击的项目不是当前裁剪项目 + self.cancel_crop_mode() # 取消当前裁剪项目的裁剪状态 + elif self.crop_item and item_at_pos == self.crop_item: # 如果当前有项目正在裁剪状态,且点击的项目是当前裁剪项目 + super().mousePressEvent(event) # 调用父类的鼠标按下事件处理方法,确保正常裁剪操作 + return # 结束当前方法,避免后续重复处理 + if item_at_pos: # 如果点击的项目存在 + self.active_mode = self.MOVE_MODE # 切换到移动模式 + elif self.items(): # 如果场景中存在其他项目 + self.active_mode = self.RUBBERBAND_MODE # 切换到橡皮筋选择模式 - super().mousePressEvent(event) + super().mousePressEvent(event)# 调用父类的鼠标按下事件处理方法,确保正常处理 - def mouseDoubleClickEvent(self, event): - self.cancel_active_modes() - item = self.itemAt(event.scenePos(), self.views()[0].transform()) - if item: - if not item.isSelected(): - item.setSelected(True) - if item.is_editable: - item.enter_edit_mode() - self.mousePressEvent(event) - else: + def mouseDoubleClickEvent(self, event):# 处理鼠标双击事件 + self.cancel_active_modes() # 取消所有活动模式,确保没有正在进行的操作干扰 + item = self.itemAt(event.scenePos(), self.views()[0].transform()) # 获取当前鼠标位置上的项目 + if item: # 如果点击的项目存在 + if not item.isSelected(): # 如果项目未被选择 + item.setSelected(True) # 选择该项目 + if item.is_editable: # 如果项目可编辑 + item.enter_edit_mode() # 进入编辑模式 + self.mousePressEvent(event) # 模拟鼠标按下事件,确保编辑模式正常启动 + else: # 如果项目不可编辑 self.views()[0].fit_rect( self.itemsBoundingRect(items=[item]), - toggle_item=item) + toggle_item=item) # 调整场景视图,确保点击的项目在可见区域 return - super().mouseDoubleClickEvent(event) + super().mouseDoubleClickEvent(event) # 调用父类的鼠标双击事件处理方法,确保正常处理 - def mouseMoveEvent(self, event): - if self.active_mode == self.RUBBERBAND_MODE: - if not self.rubberband_item.scene(): - logger.debug('Activating rubberband selection') - self.addItem(self.rubberband_item) - self.rubberband_item.bring_to_front() - self.rubberband_item.fit(self.event_start, event.scenePos()) - self.setSelectionArea(self.rubberband_item.shape()) - self.views()[0].reset_previous_transform() - super().mouseMoveEvent(event) + def mouseMoveEvent(self, event): # 处理鼠标移动事件 + if self.active_mode == self.RUBBERBAND_MODE: # 如果当前模式是橡皮筋选择模式 + if not self.rubberband_item.scene(): # 如果橡皮筋选择项目不在场景中 + logger.debug('Activating rubberband selection') # 记录调试信息,激活橡皮筋选择模式 + self.addItem(self.rubberband_item) # 将橡皮筋选择项目添加到场景中 + self.rubberband_item.bring_to_front() # 将橡皮筋选择项目 bring to front,确保可见 + self.rubberband_item.fit(self.event_start, event.scenePos()) # 调整橡皮筋选择项目的形状,以适应鼠标移动 + self.setSelectionArea(self.rubberband_item.shape()) # 设置场景的选择区域为橡皮筋选择项目的形状 + self.views()[0].reset_previous_transform() # 重置场景视图的前一个变换,确保正常显示 + super().mouseMoveEvent(event) # 调用父类的鼠标移动事件处理方法,确保正常处理 - def mouseReleaseEvent(self, event): - if self.active_mode == self.RUBBERBAND_MODE: - self.end_rubberband_mode() - if (self.active_mode == self.MOVE_MODE - and self.has_selection() - and self.multi_select_item.active_mode is None - and self.selectedItems()[0].active_mode is None): - delta = event.scenePos() - self.event_start - if not delta.isNull(): + def mouseReleaseEvent(self, event): # 处理鼠标释放事件 + if self.active_mode == self.RUBBERBAND_MODE: # 如果当前模式是橡皮筋选择模式 + self.end_rubberband_mode() # 结束橡皮筋选择模式 + if (self.active_mode == self.MOVE_MODE # 如果当前模式是移动模式 + and self.has_selection() # 如果场景中存在选择项目 + and self.multi_select_item.active_mode is None # 如果多选项目的活动模式为空 + and self.selectedItems()[0].active_mode is None): # 如果选择项目的活动模式为空 + delta = event.scenePos() - self.event_start # 计算鼠标移动的距离 + if not delta.isNull(): # 如果鼠标移动距离不为零 self.undo_stack.push( commands.MoveItemsBy(self.selectedItems(), delta, - ignore_first_redo=True)) - self.active_mode = None - super().mouseReleaseEvent(event) + ignore_first_redo=True)) # 记录移动操作到撤销栈,忽略第一次重做 + self.active_mode = None # 切换到无活动模式 + super().mouseReleaseEvent(event) # 调用父类的鼠标释放事件处理方法,确保正常处理 - def selectedItems(self, user_only=False): + def selectedItems(self, user_only=False): # 返回场景中当前选择的项目 """If ``user_only`` is set to ``True``, only return items added by the user (i.e. no multi select outlines and other UI items). User items are items that have a ``save_id`` attribute. """ - items = super().selectedItems() + items = super().selectedItems() # 获取场景中当前选择的所有项目 if user_only: - return list(filter(lambda i: hasattr(i, 'save_id'), items)) - return items + return list(filter(lambda i: hasattr(i, 'save_id'), items)) # 如果只返回用户项目,过滤掉没有 save_id 属性的项目 + return items # 返回所有选择项目 - def items_by_type(self, itype): + def items_by_type(self, itype): # 返回场景中所有指定类型的项目 """Returns all items of the given type.""" return filter(lambda i: getattr(i, 'TYPE', None) == itype, - self.items()) - - def items_for_save(self): + self.items()) # 返回所有类型为 itype 的项目 + def items_for_save(self): # 返回场景中所有可保存的项目 """Returns the items that are to be saved. Items to be saved are items that have a save_id attribute. """ return filter(lambda i: hasattr(i, 'save_id'), - self.items(order=Qt.SortOrder.AscendingOrder)) + self.items(order=Qt.SortOrder.AscendingOrder)) # 返回所有有 save_id 属性的项目 - def clear_save_ids(self): - for item in self.items_for_save(): - item.save_id = None + def clear_save_ids(self): # 清除场景中所有项目的 save_id 属性 + for item in self.items_for_save():# 遍历所有可保存项目 + item.save_id = None # 清除项目的 save_id 属性 - def on_view_scale_change(self): - for item in self.selectedItems(): - item.on_view_scale_change() + def on_view_scale_change(self): # 处理场景视图缩放变化事件 + for item in self.selectedItems(): # 遍历当前选择的项目 + item.on_view_scale_change() # 调用项目的视图缩放变化处理方法,确保正常显示 - def itemsBoundingRect(self, selection_only=False, items=None): + def itemsBoundingRect(self, selection_only=False, items=None): # 返回场景中项目的边界矩形 """Returns the bounding rect of the scene's items; either all of them or only selected ones, or the items givin in ``items``. Re-implemented to not include the items's selection handles. """ - def filter_user_items(ilist): - return list(filter(lambda i: hasattr(i, 'save_id'), ilist)) + def filter_user_items(ilist):# 过滤出场景中所有用户项目(有 save_id 属性的项目) + return list(filter(lambda i: hasattr(i, 'save_id'), ilist)) # 过滤出场景中所有用户项目(有 save_id 属性的项目) - if selection_only: - base = filter_user_items(self.selectedItems()) - elif items: - base = items - else: - base = filter_user_items(self.items()) - if not base: - return QtCore.QRectF(0, 0, 0, 0) - x = [] - y = [] + if selection_only: # 如果只返回选择项目的边界矩形 + base = filter_user_items(self.selectedItems()) # 过滤出当前选择的用户项目 + elif items: # 如果指定了项目列表 + base = items # 使用指定的项目列表 + else: # 如果没有指定项目列表 + base = filter_user_items(self.items()) # 过滤出所有用户项目 - for item in base: - for corner in item.corners_scene_coords: - x.append(corner.x()) - y.append(corner.y()) + if not base: # 如果没有项目可计算边界矩形 + return QtCore.QRectF(0, 0, 0, 0) # 返回空矩形 + + x = [] # 存储所有项目的 x 坐标 + y = [] # 存储所有项目的 y 坐标 + + for item in base: # 遍历所有项目 + for corner in item.corners_scene_coords: # 遍历项目的所有场景坐标 + x.append(corner.x()) # 存储项目的 x 坐标 + y.append(corner.y()) # 存储项目的 y 坐标 return QtCore.QRectF( - QtCore.QPointF(min(x), min(y)), - QtCore.QPointF(max(x), max(y))) + QtCore.QPointF(min(x), min(y)), # 计算所有项目的最小坐标点 + QtCore.QPointF(max(x), max(y))) # 计算所有项目的最大坐标点 - def get_selection_center(self): - rect = self.itemsBoundingRect(selection_only=True) - return (rect.topLeft() + rect.bottomRight()) / 2 + def get_selection_center(self): # 返回场景中当前选择项目的中心坐标 + rect = self.itemsBoundingRect(selection_only=True) # 获取当前选择项目的边界矩形 + return (rect.topLeft() + rect.bottomRight()) / 2 # 返回矩形的中心坐标 - def on_selection_change(self): + def on_selection_change(self): # 处理场景选择变化事件 if self._clear_ongoing: # Ignore events while clearing the scene since the # multiselect item will get cleared, too return - if self.has_multi_selection(): - self.multi_select_item.fit_selection_area( - self.itemsBoundingRect(selection_only=True)) - if self.has_multi_selection() and not self.multi_select_item.scene(): - self.addItem(self.multi_select_item) - self.multi_select_item.bring_to_front() - if not self.has_multi_selection() and self.multi_select_item.scene(): - self.removeItem(self.multi_select_item) + if self.has_multi_selection():# 如果有多个项目被选择 + self.multi_select_item.fit_selection_area( + self.itemsBoundingRect(selection_only=True)) # 调整多选框的大小以适应选择项目的边界矩形 + if self.has_multi_selection() and not self.multi_select_item.scene(): # 如果有多个项目被选择且多选框不在场景中 + self.addItem(self.multi_select_item) # 将多选框添加到场景中 + self.multi_select_item.bring_to_front() # 将多选框 bring_to_front 到最前面 + if not self.has_multi_selection() and self.multi_select_item.scene(): # 如果没有项目被选择且多选框在场景中 + self.removeItem(self.multi_select_item) # 从场景中移除多选框 - def on_change(self, region): - if self._clear_ongoing: + def on_change(self, region): # 处理场景变化事件 + if self._clear_ongoing: # 如果场景正在清除中 # Ignore events while clearing the scene since the # multiselect item will get cleared, too return - if (self.multi_select_item.scene() - and self.multi_select_item.active_mode is None): + if (self.multi_select_item.scene() # 如果多选框在场景中 + and self.multi_select_item.active_mode is None): # 且多选框未激活任何模式 self.multi_select_item.fit_selection_area( - self.itemsBoundingRect(selection_only=True)) + self.itemsBoundingRect(selection_only=True)) # 调整多选框的大小以适应选择项目的边界矩形 - def add_item_later(self, itemdata, selected=False): + def add_item_later(self, itemdata, selected=False): # 保持一个项目稍后添加,通过 ``add_queued_items`` 添加 """Keep an item for adding later via ``add_queued_items`` :param dict itemdata: Defines the item's data :param bool selected: Whether the item is initialised as selected """ - self.items_to_add.put((itemdata, selected)) + self.items_to_add.put((itemdata, selected)) # 将项目数据和选择状态添加到稍后添加队列中 - def add_queued_items(self): + def add_queued_items(self): # 添加稍后添加队列中的项目 """Adds items added via ``add_item_later``""" - while not self.items_to_add.empty(): - data, selected = self.items_to_add.get() - typ = data.pop('type') - cls = item_registry.get(typ) - if not cls: + while not self.items_to_add.empty(): # 循环直到稍后添加队列为空 + data, selected = self.items_to_add.get() # 从稍后添加队列中获取项目数据和选择状态 + typ = data.pop('type') # 从项目数据中弹出项目类型 + cls = item_registry.get(typ) # 从项目类型中获取项目类 + if not cls: # 如果项目类型未知 # Just in case we add new item types in future versions - logger.warning(f'Encountered item of unknown type: {typ}') - cls = BeeErrorItem - data['data'] = {'text': f'Item of unknown type: {typ}'} - item = cls.create_from_data(**data) + logger.warning(f'Encountered item of unknown type: {typ}') # 记录警告日志,提示未知项目类型 + cls = BeeErrorItem # 默认使用 BeeErrorItem 类 + data['data'] = {'text': f'Item of unknown type: {typ}'} # 为未知项目类型添加默认文本数据 + item = cls.create_from_data(**data) # 创建项目实例 # Set the values common to all item types: - item.update_from_data(**data) - self.addItem(item) + item.update_from_data(**data) # 更新项目实例的属性值 + self.addItem(item) # 将项目实例添加到场景中 # Force recalculation of min/max z values: - item.setZValue(item.zValue()) + item.setZValue(item.zValue()) # 强制重新计算项目实例的 z 值 if selected: - item.setSelected(True) - item.bring_to_front() \ No newline at end of file + item.setSelected(True) # 设置项目实例为选中状态 + item.bring_to_front() # 将项目实例 bring_to_front 到最前面