diff --git a/beeref/items.py b/beeref/items.py index c42044a..cac87fb 100644 --- a/beeref/items.py +++ b/beeref/items.py @@ -17,31 +17,31 @@ text). """ -from collections import defaultdict -from functools import cached_property -import logging -import os.path +from collections import defaultdict # 导入collections模块中的defaultdict类,用于创建默认值的字典 +from functools import cached_property # 导入functools模块中的cached_property类,用于缓存属性的计算结果 +import logging # 导入logging模块,用于记录日志 +import os.path # 导入os.path模块,用于处理文件路径 -from PyQt6 import QtCore, QtGui, QtWidgets -from PyQt6.QtCore import Qt +from PyQt6 import QtCore, QtGui, QtWidgets # 导入PyQt6模块中的QtCore、QtGui、QtWidgets类,用于处理Qt的核心功能、图形界面和小部件 +from PyQt6.QtCore import Qt # 导入PyQt6.QtCore模块中的Qt类,用于处理Qt的核心功能 -from beeref import commands -from beeref.config import BeeSettings -from beeref.constants import COLORS -from beeref.selection import SelectableMixin +from beeref import commands # 导入beeref模块中的commands类,用于处理用户命令 +from beeref.config import BeeSettings # 导入beeref模块中的BeeSettings类,用于处理应用程序的配置 +from beeref.constants import COLORS # 导入beeref模块中的COLORS常量,用于定义颜色 +from beeref.selection import SelectableMixin # 导入beeref模块中的SelectableMixin类,用于处理可选择的项 -logger = logging.getLogger(__name__) +logger = logging.getLogger(__name__) # 创建一个日志记录器,用于记录当前模块的日志信息 -item_registry = {} +item_registry = {} # 创建一个空字典,用于存储注册的项类型和对应的类 -def register_item(cls): - item_registry[cls.TYPE] = cls - return cls +def register_item(cls): # 定义一个函数,用于注册项类型和对应的类 + item_registry[cls.TYPE] = cls # 将项类型作为键,对应的类作为值,存储到item_registry字典中 + return cls # 返回注册的类,用于支持链式调用 -def sort_by_filename(items): +def sort_by_filename(items): # 定义一个函数,用于根据文件名对项进行排序 """Order items by filename. Items with a filename (ordered by filename) first, then items @@ -50,114 +50,114 @@ def sort_by_filename(items): been inserted into the scene. """ - items_by_filename = [] - items_by_save_id = [] - items_remaining = [] + items_by_filename = [] # 创建一个空列表,用于存储有文件名的项 + items_by_save_id = [] # 创建一个空列表,用于存储有save_id的项 + items_remaining = [] # 创建一个空列表,用于存储剩余的项 - for item in items: - if getattr(item, 'filename', None): - items_by_filename.append(item) - elif getattr(item, 'save_id', None): - items_by_save_id.append(item) + for item in items: # 遍历所有项 + if getattr(item, 'filename', None): # 如果项有文件名属性 + items_by_filename.append(item) # 将项添加到有文件名的项列表中 + elif getattr(item, 'save_id', None): # 如果项有save_id属性 + items_by_save_id.append(item) # 将项添加到有save_id的项列表中 else: - items_remaining.append(item) + items_remaining.append(item) # 将项添加到剩余的项列表中 - items_by_filename.sort(key=lambda x: x.filename) - items_by_save_id.sort(key=lambda x: x.save_id) - return items_by_filename + items_by_save_id + items_remaining + items_by_filename.sort(key=lambda x: x.filename) # 对有文件名的项列表进行排序,根据文件名属性进行排序 + items_by_save_id.sort(key=lambda x: x.save_id) # 对有save_id的项列表进行排序,根据save_id属性进行排序 + return items_by_filename + items_by_save_id + items_remaining # 返回排序后的项列表,先有文件名的项,然后有save_id的项,最后是剩余的项 -class BeeItemMixin(SelectableMixin): +class BeeItemMixin(SelectableMixin): # 定义一个基类,用于所有用户添加的项 """Base for all items added by the user.""" - def set_pos_center(self, pos): + def set_pos_center(self, pos): # 定义一个方法,用于设置项的位置,使用项的中心作为原点 """Sets the position using the item's center as the origin point.""" - self.setPos(pos - self.center_scene_coords) + self.setPos(pos - self.center_scene_coords) # 将项的位置设置为传入的位置减去项的中心场景坐标 def has_selection_outline(self): - return self.isSelected() + return self.isSelected() # 返回项是否被选中 - def has_selection_handles(self): - return (self.isSelected() - and self.scene() - and self.scene().has_single_selection()) + def has_selection_handles(self): # 定义一个方法,用于判断项是否有选择句柄 + return (self.isSelected() # 返回项是否被选中 + and self.scene() # 返回项所属的场景 + and self.scene().has_single_selection()) # 返回场景是否只有一个项被选中 - def selection_action_items(self): + def selection_action_items(self): # 定义一个方法,用于返回项受选择操作影响的项列表 """The items affected by selection actions like scaling and rotating. """ - return [self] + return [self] # 返回项受选择操作影响的项列表,这里只有项本身 - def on_selected_change(self, value): - if (value and self.scene() - and not self.scene().has_selection() - and not self.scene().active_mode is None): - self.bring_to_front() + def on_selected_change(self, value): # 定义一个方法,用于处理项的选中状态变化 + if (value and self.scene() # 如果项被选中,并且项所属的场景存在 + and not self.scene().has_selection() # 场景没有其他项被选中 + and not self.scene().active_mode is None): # 场景没有活动模式 + self.bring_to_front() # 将项 bring to front - def update_from_data(self, **kwargs): - self.save_id = kwargs.get('save_id', self.save_id) - self.setPos(kwargs.get('x', self.pos().x()), - kwargs.get('y', self.pos().y())) - self.setZValue(kwargs.get('z', self.zValue())) - self.setScale(kwargs.get('scale', self.scale())) - self.setRotation(kwargs.get('rotation', self.rotation())) - if kwargs.get('flip', 1) != self.flip(): - self.do_flip() + def update_from_data(self, **kwargs): # 定义一个方法,用于从数据更新项的属性 + self.save_id = kwargs.get('save_id', self.save_id) # 更新项的save_id属性,使用传入的save_id值或当前值 + self.setPos(kwargs.get('x', self.pos().x()), + kwargs.get('y', self.pos().y())) # 更新项的位置属性,使用传入的x、y值或当前值 + self.setZValue(kwargs.get('z', self.zValue())) # 更新项的z值属性,使用传入的z值或当前值 + self.setScale(kwargs.get('scale', self.scale())) # 更新项的缩放属性,使用传入的scale值或当前值 + self.setRotation(kwargs.get('rotation', self.rotation())) # 更新项的旋转属性,使用传入的rotation值或当前值 + if kwargs.get('flip', 1) != self.flip(): # 如果传入的flip值与当前值不同 + self.do_flip() # 执行项的翻转操作 @register_item -class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem): +class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem): # 定义一个类,用于表示用户添加的图像项 """Class for images added by the user.""" - TYPE = 'pixmap' - CROP_HANDLE_SIZE = 15 + TYPE = 'pixmap' # 定义项的类型为'pixmap' + CROP_HANDLE_SIZE = 15 # 定义项的裁剪句柄大小为15 - def __init__(self, image, filename=None, **kwargs): - super().__init__(QtGui.QPixmap.fromImage(image)) - self.save_id = None - self.filename = filename - self.reset_crop() - logger.debug(f'Initialized {self}') - self.is_image = True - self.crop_mode = False - self.init_selectable() - self.settings = BeeSettings() - self.grayscale = False + def __init__(self, image, filename=None, **kwargs): # 定义类的初始化方法,用于创建图像项 + super().__init__(QtGui.QPixmap.fromImage(image)) # 调用父类的初始化方法,传入图像项的QPixmap对象 + self.save_id = None # 初始化项的save_id属性为None + self.filename = filename # 初始化项的filename属性为传入的filename值 + self.reset_crop() # 调用项的reset_crop方法,重置项的裁剪区域 + logger.debug(f'Initialized {self}') # 记录项的初始化信息 + self.is_image = True # 初始化项的is_image属性为True + self.crop_mode = False # 初始化项的crop_mode属性为False + self.init_selectable() # 调用项的init_selectable方法,初始化项的选择状态 + self.settings = BeeSettings() # 初始化项的settings属性为BeeSettings()对象 + self.grayscale = False # 初始化项的grayscale属性为False @classmethod - def create_from_data(self, **kwargs): - item = kwargs.pop('item') - data = kwargs.pop('data', {}) - item.filename = item.filename or data.get('filename') - if 'crop' in data: - item.crop = QtCore.QRectF(*data['crop']) - item.setOpacity(data.get('opacity', 1)) - item.grayscale = data.get('grayscale', False) - return item + def create_from_data(self, **kwargs): # 定义一个类方法,用于从数据创建图像项 + item = kwargs.pop('item') # 从kwargs中弹出项对象,赋值给item变量 + data = kwargs.pop('data', {}) # 从kwargs中弹出数据字典,赋值给data变量,默认值为空字典 + item.filename = item.filename or data.get('filename') # 如果项的filename属性为None,或者数据字典中没有'filename'键,将项的filename属性设置为数据字典中的'filename'值 + if 'crop' in data: # 如果数据字典中包含'crop'键 + item.crop = QtCore.QRectF(*data['crop']) # 将项的crop属性设置为数据字典中'crop'键对应的值,使用QRectF对象表示裁剪区域 + item.setOpacity(data.get('opacity', 1)) # 将项的不透明度属性设置为数据字典中'opacity'键对应的值,默认值为1 + item.grayscale = data.get('grayscale', False) # 将项的grayscale属性设置为数据字典中'grayscale'键对应的值,默认值为False + return item # 返回创建的项对象 - def __str__(self): - size = self.pixmap().size() - return (f'Image "{self.filename}" {size.width()} x {size.height()}') + def __str__(self): # 定义项的字符串表示方法 + size = self.pixmap().size() # 获取项的QPixmap对象的大小 + return (f'Image "{self.filename}" {size.width()} x {size.height()}') # 返回项的字符串表示,包含文件名和图像大小 @property - def crop(self): - return self._crop + def crop(self): # 定义项的裁剪区域属性的getter方法 + return self._crop # 返回项的裁剪区域属性值 @crop.setter - def crop(self, value): - logger.debug(f'Setting crop for {self} to {value}') - self.prepareGeometryChange() - self._crop = value - self.update() + def crop(self, value): # 定义项的裁剪区域属性的setter方法 + logger.debug(f'Setting crop for {self} to {value}') # 记录项的裁剪区域属性设置信息 + self.prepareGeometryChange() # 准备项的几何变化,通知场景项的位置、大小或旋转等属性已改变 + self._crop = value # 更新项的裁剪区域属性值为传入的value值 + self.update() # 更新项的显示,触发项的重绘操作 @property - def grayscale(self): - return self._grayscale + def grayscale(self): # 定义项的灰度属性的getter方法 + return self._grayscale # 返回项的灰度属性值 @grayscale.setter - def grayscale(self, value): - logger.debug('Setting grayscale for {self} to {value}') - self._grayscale = value + def grayscale(self, value): # 定义项的灰度属性的setter方法 + logger.debug('Setting grayscale for {self} to {value}') # 记录项的灰度属性设置信息 + self._grayscale = value # 更新项的灰度属性值为传入的value值 if value is True: # Using the grayscale image format to convert to grayscale # loses an image's tranparency. So the straightworward @@ -169,12 +169,12 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem): # overlaps other images. The way we do it here only works # as long as the canvas colour is itself grayscale, # though. - img = QtGui.QImage( + img = QtGui.QImage( # 创建一个QImage对象,用于存储转换后的灰度图像 self.pixmap().size(), QtGui.QImage.Format.Format_Grayscale8) - img.fill(QtGui.QColor(*COLORS['Scene:Canvas'])) - painter = QtGui.QPainter(img) - painter.drawPixmap(0, 0, self.pixmap()) - painter.end() + img.fill(QtGui.QColor(*COLORS['Scene:Canvas'])) # 用当前场景的画布颜色填充图像的背景 + painter = QtGui.QPainter(img) # 创建一个QPainter对象,用于绘制图像 + painter.drawPixmap(0, 0, self.pixmap()) # 绘制项的QPixmap对象到图像上 + painter.end() # 结束绘制操作 self._grayscale_pixmap = QtGui.QPixmap.fromImage(img) # Alternative methods that have their own issues: @@ -192,114 +192,114 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem): # # 3. Going through every pixel and doing it manually — bad # performance. - else: - self._grayscale_pixmap = None + else: # 如果灰度属性为False + self._grayscale_pixmap = None # 将项的灰度Pixmap属性设置为None - self.update() + self.update() # 更新项的显示,触发项的重绘操作 - def sample_color_at(self, pos): - ipos = self.mapFromScene(pos) - if self.grayscale: - pm = self._grayscale_pixmap - else: - pm = self.pixmap() - img = pm.toImage() + def sample_color_at(self, pos): # 定义项的采样颜色方法,用于获取项在指定位置的颜色值 + ipos = self.mapFromScene(pos) # 将场景坐标转换为项坐标 + if self.grayscale: # 如果项的灰度属性为True + pm = self._grayscale_pixmap # 将项的灰度Pixmap赋值给pm变量 + else: # 如果项的灰度属性为False + pm = self.pixmap() # 将项的QPixmap对象赋值给pm变量 + img = pm.toImage() # 将项的Pixmap对象转换为QImage对象 - color = img.pixelColor(int(ipos.x()), int(ipos.y())) - if color.alpha(): - return color + color = img.pixelColor(int(ipos.x()), int(ipos.y())) # 获取图像中指定位置的颜色值 + if color.alpha() > 0: # 如果颜色的alpha值大于0,即颜色不是完全透明的 + return color # 返回颜色值 - def bounding_rect_unselected(self): - if self.crop_mode: - return QtWidgets.QGraphicsPixmapItem.boundingRect(self) - else: - return self.crop + def bounding_rect_unselected(self): # 定义项的未选中状态下的边界矩形方法 + if self.crop_mode: # 如果项的裁剪模式为True + return QtWidgets.QGraphicsPixmapItem.boundingRect(self) # 返回项的QPixmap对象的边界矩形 + else: # 如果项的裁剪模式为False + return self.crop # 返回项的裁剪区域属性值 - def get_extra_save_data(self): + def get_extra_save_data(self): # 定义项的额外保存数据方法,用于获取项的额外保存信息 return {'filename': self.filename, 'opacity': self.opacity(), 'grayscale': self.grayscale, 'crop': [self.crop.topLeft().x(), self.crop.topLeft().y(), self.crop.width(), - self.crop.height()]} + self.crop.height()]} # 返回项的裁剪区域属性值的列表,包含左上角的x、y坐标、宽度和高度 - def get_filename_for_export(self, imgformat, save_id_default=None): - save_id = self.save_id or save_id_default - assert save_id is not None + def get_filename_for_export(self, imgformat, save_id_default=None): # 定义项的导出文件名方法,用于获取项导出时的文件名 + save_id = self.save_id or save_id_default # 如果项的保存ID为None,则使用默认值 + assert save_id is not None # 断言项的保存ID不为None - if self.filename: - basename = os.path.splitext(os.path.basename(self.filename))[0] - return f'{save_id:04}-{basename}.{imgformat}' + if self.filename: # 如果项的文件名属性不为None + basename = os.path.splitext(os.path.basename(self.filename))[0] # 从项的文件名属性中提取文件名部分,不包含扩展名 + return f'{save_id:04}-{basename}.{imgformat}' # 返回导出文件名,格式为"{save_id:04}-{basename}.{imgformat}",其中save_id为项的保存ID,basename为项的文件名部分,imgformat为导出的图像格式 else: - return f'{save_id:04}.{imgformat}' + return f'{save_id:04}.{imgformat}' # 返回导出文件名,格式为"{save_id:04}.{imgformat}",其中save_id为项的保存ID,imgformat为导出的图像格式 - def get_imgformat(self, img): + def get_imgformat(self, img): # 定义项的图像格式方法,用于根据图像属性确定存储格式 """Determines the format for storing this image.""" - formt = self.settings.valueOrDefault('Items/image_storage_format') + formt = self.settings.valueOrDefault('Items/image_storage_format') # 从项的设置中获取图像存储格式选项,默认值为'best' - if formt == 'best': + if formt == 'best': # 如果图像存储格式选项为'best' # Images with alpha channel and small images are stored as png - if (img.hasAlphaChannel() - or (img.height() < 500 and img.width() < 500)): - formt = 'png' + if (img.hasAlphaChannel() # 如果图像有alpha通道 + or (img.height() < 500 and img.width() < 500)): # 如果图像高度或宽度小于500像素 + formt = 'png' # 如果图像有alpha通道或高度或宽度小于500像素,则存储为png格式 else: - formt = 'jpg' + formt = 'jpg' # 如果图像没有alpha通道且高度和宽度都大于等于500像素,则存储为jpg格式 - logger.debug(f'Found format {formt} for {self}') - return formt + logger.debug(f'Found format {formt} for {self}') # 记录调试信息,显示项的图像存储格式选项和项的文件名属性 + return formt # 返回项的图像存储格式选项 - def pixmap_to_bytes(self, apply_grayscale=False, apply_crop=False): + def pixmap_to_bytes(self, apply_grayscale=False, apply_crop=False): # 定义项的Pixmap转换为字节字符串方法,用于将项的Pixmap数据转换为PNG格式的字节字符串 """Convert the pixmap data to PNG bytestring.""" - barray = QtCore.QByteArray() - buffer = QtCore.QBuffer(barray) - buffer.open(QtCore.QIODevice.OpenModeFlag.WriteOnly) + barray = QtCore.QByteArray() # 创建一个空的QByteArray对象,用于存储转换后的字节字符串 + buffer = QtCore.QBuffer(barray) # 创建一个QBuffer对象,将barray作为参数传入,用于将Pixmap数据写入字节字符串 + buffer.open(QtCore.QIODevice.OpenModeFlag.WriteOnly) # 以只写模式打开QBuffer对象,用于将Pixmap数据写入字节字符串 if apply_grayscale and self.grayscale: - pm = self._grayscale_pixmap + pm = self._grayscale_pixmap # 如果应用灰度属性为True且项的灰度属性为True,则将项的灰度Pixmap赋值给pm变量 else: - pm = self.pixmap() + pm = self.pixmap() # 如果应用灰度属性为False或项的灰度属性为False,则将项的QPixmap对象赋值给pm变量 - if apply_crop: - pm = pm.copy(self.crop.toRect()) + if apply_crop: # 如果应用裁剪属性为True + pm = pm.copy(self.crop.toRect()) # 将项的Pixmap对象裁剪为指定区域,赋值给pm变量 - img = pm.toImage() - imgformat = self.get_imgformat(img) - img.save(buffer, imgformat.upper(), quality=90) - return (barray.data(), imgformat) + img = pm.toImage() # 将项的Pixmap对象转换为QImage对象 + imgformat = self.get_imgformat(img) # 根据图像属性确定存储格式 + img.save(buffer, imgformat.upper(), quality=90) # 将图像保存到QBuffer对象中,格式为PNG,质量为90 + return (barray.data(), imgformat) # 返回转换后的字节字符串和图像格式 - def setPixmap(self, pixmap): - super().setPixmap(pixmap) - self.reset_crop() + def setPixmap(self, pixmap): # 定义项的Pixmap设置方法,用于设置项的Pixmap对象 + super().setPixmap(pixmap) # 调用父类的setPixmap方法,将项的Pixmap对象设置为传入的Pixmap对象 + self.reset_crop() # 调用项的重置裁剪方法,将项的裁剪区域属性值重置为默认值 - def pixmap_from_bytes(self, data): + def pixmap_from_bytes(self, data): # 定义项的Pixmap从字节字符串设置方法,用于将项的Pixmap数据从PNG格式的字节字符串中设置 """Set image pimap from a bytestring.""" - pixmap = QtGui.QPixmap() - pixmap.loadFromData(data) - self.setPixmap(pixmap) + pixmap = QtGui.QPixmap() # 创建一个空的QPixmap对象,用于存储从字节字符串中加载的Pixmap数据 + pixmap.loadFromData(data) # 从字节字符串中加载Pixmap数据到QPixmap对象中 + self.setPixmap(pixmap) # 将项的Pixmap对象设置为加载的Pixmap对象 - def create_copy(self): - item = BeePixmapItem(QtGui.QImage(), self.filename) - item.setPixmap(self.pixmap()) - item.setPos(self.pos()) - item.setZValue(self.zValue()) - item.setScale(self.scale()) - item.setRotation(self.rotation()) - item.setOpacity(self.opacity()) - item.grayscale = self.grayscale + def create_copy(self): # 定义项的复制方法,用于创建项的副本 + item = BeePixmapItem(QtGui.QImage(), self.filename) # 创建一个空的Pixmap项对象,用于存储复制的Pixmap数据 + item.setPixmap(self.pixmap()) # 将项的Pixmap对象复制到新创建的Pixmap项对象中 + item.setPos(self.pos()) # 将项的位置属性值复制到新创建的Pixmap项对象中 + item.setZValue(self.zValue()) # 将项的Z值属性值复制到新创建的Pixmap项对象中 + item.setScale(self.scale()) # 将项的缩放属性值复制到新创建的Pixmap项对象中 + item.setRotation(self.rotation()) # 将项的旋转属性值复制到新创建的Pixmap项对象中 + item.setOpacity(self.opacity()) # 将项的不透明度属性值复制到新创建的Pixmap项对象中 + item.grayscale = self.grayscale # 将项的灰度属性值复制到新创建的Pixmap项对象中 if self.flip() == -1: - item.do_flip() - item.crop = self.crop - return item + item.do_flip() # 如果项的翻转属性值为-1,则调用项的翻转方法,将项的Pixmap对象水平翻转 + item.crop = self.crop # 将项的裁剪区域属性值复制到新创建的Pixmap项对象中 + return item # 返回新创建的Pixmap项对象 - @cached_property - def color_gamut(self): - logger.debug(f'Calculating color gamut for {self}') - gamut = defaultdict(int) - img = self.pixmap().toImage() + @cached_property + def color_gamut(self): # 定义项的颜色 Gamut 计算方法,用于计算项的颜色 Gamut + logger.debug(f'Calculating color gamut for {self}') # 记录调试信息,显示项的颜色 Gamut 计算方法和项的文件名属性 + gamut = defaultdict(int) # 创建一个默认值为整数的字典对象,用于存储项的颜色 Gamut 值 + img = self.pixmap().toImage() # 将项的Pixmap对象转换为QImage对象 # Don't evaluate every pixel for larger images: - step = max(1, int(max(img.width(), img.height()) / 1000)) - logger.debug(f'Considering every {step}. row/column') + step = max(1, int(max(img.width(), img.height()) / 1000)) # 计算步长,用于减少计算量,避免处理过大的图像 + logger.debug(f'Considering every {step}. row/column') # 记录调试信息,显示项的颜色 Gamut 计算方法和项的文件名属性 # Not actually faster than solution below :( # ptr = img.bits() @@ -314,425 +314,425 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem): # rgb = QtGui.QColor(r, g, b) # gamut[rgb.hue(), rgb.saturation()] += 1 - for i in range(0, img.width(), step): - for j in range(0, img.height(), step): - rgb = img.pixelColor(i, j) - rgbtuple = (rgb.red(), rgb.blue(), rgb.green()) - if (5 < rgb.alpha() - and min(rgbtuple) < 250 and max(rgbtuple) > 5): + for i in range(0, img.width(), step):# 遍历图像的每一行,步长为step + for j in range(0, img.height(), step):# 遍历图像的每一列,步长为step + rgb = img.pixelColor(i, j) # 获取图像在(i, j)位置的像素颜色 + rgbtuple = (rgb.red(), rgb.blue(), rgb.green()) # 将像素颜色转换为RGB元组 + if (5 < rgb.alpha() # 如果像素的alpha值大于5 + and min(rgbtuple) < 250 and max(rgbtuple) > 5): # 如果像素的RGB值都在5到250之间,且不是接近透明、白色或黑色 # Only consider pixels that aren't close to # transparent, white or black - gamut[rgb.hue(), rgb.saturation()] += 1 + gamut[rgb.hue(), rgb.saturation()] += 1 # 如果像素的RGB值都在5到250之间,且不是接近透明、白色或黑色,则将像素的Hue值和Saturation值作为键,将1作为值,添加到颜色 Gamut 字典中 - logger.debug(f'Got {len(gamut)} color gamut values') - return gamut + logger.debug(f'Got {len(gamut)} color gamut values') # 记录调试信息,显示项的颜色 Gamut 计算方法和项的文件名属性 + return gamut # 返回项的颜色 Gamut 字典对象 - def copy_to_clipboard(self, clipboard): - clipboard.setPixmap(self.pixmap()) + def copy_to_clipboard(self, clipboard): # 定义项的复制到剪贴板方法,用于将项的Pixmap数据复制到剪贴板中 + clipboard.setPixmap(self.pixmap()) # 将项的Pixmap对象复制到剪贴板中 def reset_crop(self): self.crop = QtCore.QRectF( - 0, 0, self.pixmap().size().width(), self.pixmap().size().height()) + 0, 0, self.pixmap().size().width(), self.pixmap().size().height()) # 将项的裁剪区域属性值重置为默认值,即整个Pixmap图像 @property - def crop_handle_size(self): - return self.fixed_length_for_viewport(self.CROP_HANDLE_SIZE) + def crop_handle_size(self): # 定义项的裁剪区域句柄大小属性方法,用于计算项的裁剪区域句柄大小 + return self.fixed_length_for_viewport(self.CROP_HANDLE_SIZE) # 返回项的裁剪区域句柄大小属性值,根据当前视口大小进行缩放 - def crop_handle_topleft(self): - topleft = self.crop_temp.topLeft() - return QtCore.QRectF( + def crop_handle_topleft(self): # 定义项的裁剪区域句柄左上角方法,用于计算项的裁剪区域句柄左上角位置 + topleft = self.crop_temp.topLeft() # 获取项的裁剪区域临时属性值的左上角位置 + return QtCore.QRectF( # 返回项的裁剪区域句柄左上角位置的矩形对象 topleft.x(), topleft.y(), self.crop_handle_size, - self.crop_handle_size) + self.crop_handle_size) # 返回项的裁剪区域句柄左上角位置的矩形对象,宽度和高度都等于项的裁剪区域句柄大小属性值 - def crop_handle_bottomleft(self): - bottomleft = self.crop_temp.bottomLeft() - return QtCore.QRectF( + def crop_handle_bottomleft(self): # 定义项的裁剪区域句柄左下角方法,用于计算项的裁剪区域句柄左下角位置 + bottomleft = self.crop_temp.bottomLeft() # 获取项的裁剪区域临时属性值的左下角位置 + return QtCore.QRectF( # 返回项的裁剪区域句柄左下角位置的矩形对象 bottomleft.x(), bottomleft.y() - self.crop_handle_size, self.crop_handle_size, self.crop_handle_size) - def crop_handle_bottomright(self): - bottomright = self.crop_temp.bottomRight() - return QtCore.QRectF( + def crop_handle_bottomright(self): # 定义项的裁剪区域句柄右下角方法,用于计算项的裁剪区域句柄右下角位置 + bottomright = self.crop_temp.bottomRight() # 获取项的裁剪区域临时属性值的右下角位置 + return QtCore.QRectF( # 返回项的裁剪区域句柄右下角位置的矩形对象 bottomright.x() - self.crop_handle_size, bottomright.y() - self.crop_handle_size, self.crop_handle_size, self.crop_handle_size) - def crop_handle_topright(self): - topright = self.crop_temp.topRight() - return QtCore.QRectF( + def crop_handle_topright(self): # 定义项的裁剪区域句柄右上角方法,用于计算项的裁剪区域句柄右上角位置 + topright = self.crop_temp.topRight() # 获取项的裁剪区域临时属性值的右上角位置 + return QtCore.QRectF( # 返回项的裁剪区域句柄右上角位置的矩形对象 topright.x() - self.crop_handle_size, topright.y(), self.crop_handle_size, self.crop_handle_size) - def crop_handles(self): - return (self.crop_handle_topleft, - self.crop_handle_bottomleft, - self.crop_handle_bottomright, - self.crop_handle_topright) + def crop_handles(self): # 定义项的裁剪区域句柄方法,用于返回项的裁剪区域句柄列表 + return (self.crop_handle_topleft, # 返回项的裁剪区域句柄左上角方法 + self.crop_handle_bottomleft, # 返回项的裁剪区域句柄左下角方法 + self.crop_handle_bottomright, # 返回项的裁剪区域句柄右下角方法 + self.crop_handle_topright) # 返回项的裁剪区域句柄右上角方法 - def crop_edge_top(self): - topleft = self.crop_temp.topLeft() - return QtCore.QRectF( - topleft.x() + self.crop_handle_size, - topleft.y(), - self.crop_temp.width() - 2 * self.crop_handle_size, - self.crop_handle_size) + def crop_edge_top(self): # 定义项的裁剪区域顶部边缘方法,用于计算项的裁剪区域顶部边缘位置 + topleft = self.crop_temp.topLeft() # 获取项的裁剪区域临时属性值的顶部边缘位置 + return QtCore.QRectF( # 返回项的裁剪区域顶部边缘位置的矩形对象 + topleft.x() + self.crop_handle_size, # 顶部边缘位置的横坐标等于项的裁剪区域句柄大小属性值加上项的裁剪区域临时属性值的顶部边缘位置的横坐标 + topleft.y(), # 顶部边缘位置的纵坐标等于项的裁剪区域临时属性值的顶部边缘位置的纵坐标 + self.crop_temp.width() - 2 * self.crop_handle_size, # 顶部边缘位置的宽度等于项的裁剪区域临时属性值的宽度减去2倍的项的裁剪区域句柄大小属性值 + self.crop_handle_size) # 返回项的裁剪区域顶部边缘位置的矩形对象,高度等于项的裁剪区域句柄大小属性值 - def crop_edge_left(self): - topleft = self.crop_temp.topLeft() - return QtCore.QRectF( - topleft.x(), - topleft.y() + self.crop_handle_size, - self.crop_handle_size, - self.crop_temp.height() - 2 * self.crop_handle_size) + def crop_edge_left(self): # 定义项的裁剪区域左侧边缘方法,用于计算项的裁剪区域左侧边缘位置 + topleft = self.crop_temp.topLeft() # 获取项的裁剪区域临时属性值的左侧边缘位置 + return QtCore.QRectF( # 返回项的裁剪区域左侧边缘位置的矩形对象 + topleft.x(), # 左侧边缘位置的横坐标等于项的裁剪区域临时属性值的左侧边缘位置的横坐标 + topleft.y() + self.crop_handle_size, # 左侧边缘位置的纵坐标等于项的裁剪区域句柄大小属性值加上项的裁剪区域临时属性值的左侧边缘位置的纵坐标 + self.crop_handle_size, # 左侧边缘位置的宽度等于项的裁剪区域句柄大小属性值 + self.crop_temp.height() - 2 * self.crop_handle_size) # 返回项的裁剪区域左侧边缘位置的矩形对象,高度等于项的裁剪区域临时属性值的高度减去2倍的项的裁剪区域句柄大小属性值 - def crop_edge_bottom(self): - bottomleft = self.crop_temp.bottomLeft() - return QtCore.QRectF( - bottomleft.x() + self.crop_handle_size, - bottomleft.y() - self.crop_handle_size, - self.crop_temp.width() - 2 * self.crop_handle_size, - self.crop_handle_size) + def crop_edge_bottom(self): # 定义项的裁剪区域底部边缘方法,用于计算项的裁剪区域底部边缘位置 + bottomleft = self.crop_temp.bottomLeft() # 获取项的裁剪区域临时属性值的底部边缘位置 + return QtCore.QRectF( # 返回项的裁剪区域底部边缘位置的矩形对象 + bottomleft.x() + self.crop_handle_size, # 底部边缘位置的横坐标等于项的裁剪区域句柄大小属性值加上项的裁剪区域临时属性值的底部边缘位置的横坐标 + bottomleft.y() - self.crop_handle_size, # 底部边缘位置的纵坐标等于项的裁剪区域句柄大小属性值减去项的裁剪区域临时属性值的底部边缘位置的纵坐标 + self.crop_temp.width() - 2 * self.crop_handle_size, # 底部边缘位置的宽度等于项的裁剪区域临时属性值的宽度减去2倍的项的裁剪区域句柄大小属性值 + self.crop_handle_size) # 返回项的裁剪区域底部边缘位置的矩形对象,高度等于项的裁剪区域句柄大小属性值 - def crop_edge_right(self): - topright = self.crop_temp.topRight() - return QtCore.QRectF( - topright.x() - self.crop_handle_size, + def crop_edge_right(self): # 定义项的裁剪区域右侧边缘方法,用于计算项的裁剪区域右侧边缘位置 + topright = self.crop_temp.topRight() # 获取项的裁剪区域临时属性值的右侧边缘位置 + return QtCore.QRectF( # 返回项的裁剪区域右侧边缘位置的矩形对象 + topright.x() - self.crop_handle_size, # 右侧边缘位置的横坐标等于项的裁剪区域句柄大小属性值减去项的裁剪区域临时属性值的右侧边缘位置的横坐标 topright.y() + self.crop_handle_size, self.crop_handle_size, self.crop_temp.height() - 2 * self.crop_handle_size) - def crop_edges(self): - return (self.crop_edge_top, - self.crop_edge_left, - self.crop_edge_bottom, - self.crop_edge_right) + def crop_edges(self): # 定义项的裁剪区域边缘方法,用于返回项的裁剪区域边缘列表 + return (self.crop_edge_top, # 返回项的裁剪区域顶部边缘方法 + self.crop_edge_left, # 返回项的裁剪区域左侧边缘方法 + self.crop_edge_bottom, # 返回项的裁剪区域底部边缘方法 + self.crop_edge_right) # 返回项的裁剪区域右侧边缘方法 - def get_crop_handle_cursor(self, handle): + def get_crop_handle_cursor(self, handle): # 定义项的裁剪区域句柄光标方法,用于返回项的裁剪区域句柄光标 """Gets the crop cursor for the given handle.""" - is_topleft_or_bottomright = handle in ( + is_topleft_or_bottomright = handle in ( # 判断项的裁剪区域句柄是否为左上角或右下角句柄 self.crop_handle_topleft, self.crop_handle_bottomright) - return self.get_diag_cursor(is_topleft_or_bottomright) + return self.get_diag_cursor(is_topleft_or_bottomright) # 返回项的裁剪区域句柄光标,根据项的裁剪区域句柄是否为左上角或右下角句柄来判断光标类型 - def get_crop_edge_cursor(self, edge): + def get_crop_edge_cursor(self, edge): # 定义项的裁剪区域边缘光标方法,用于返回项的裁剪区域边缘光标 """Gets the crop edge cursor for the given edge.""" top_or_bottom = edge in ( - self.crop_edge_top, self.crop_edge_bottom) - sideways = (45 < self.rotation() < 135 + self.crop_edge_top, self.crop_edge_bottom) # 判断项的裁剪区域边缘是否为顶部或底部边缘 + sideways = (45 < self.rotation() < 135 # 判断项的旋转角度是否为45度到135度或225度到315度之间 or 225 < self.rotation() < 315) - if top_or_bottom is sideways: - return Qt.CursorShape.SizeHorCursor + if top_or_bottom is sideways: # 如果项的裁剪区域边缘为顶部或底部边缘且项的旋转角度为45度到135度或225度到315度之间 + return Qt.CursorShape.SizeHorCursor # 返回水平光标类型 else: - return Qt.CursorShape.SizeVerCursor + return Qt.CursorShape.SizeVerCursor # 返回垂直光标类型 - def draw_crop_rect(self, painter, rect): + def draw_crop_rect(self, painter, rect): # 定义项的裁剪区域矩形绘制方法,用于绘制项的裁剪区域矩形 """Paint a dotted rectangle for the cropping UI.""" - pen = QtGui.QPen(QtGui.QColor(255, 255, 255)) - pen.setWidth(2) - pen.setCosmetic(True) - painter.setPen(pen) - painter.drawRect(rect) - pen.setColor(QtGui.QColor(0, 0, 0)) - pen.setStyle(Qt.PenStyle.DotLine) - painter.setPen(pen) - painter.drawRect(rect) + pen = QtGui.QPen(QtGui.QColor(255, 255, 255)) # 创建白色画笔对象 + pen.setWidth(2) # 设置画笔宽度为2像素 + pen.setCosmetic(True) # 设置画笔为无宽度模式,用于绘制虚线矩形 + painter.setPen(pen) # 设置画笔为白色画笔对象 + painter.drawRect(rect) # 绘制项的裁剪区域矩形 + pen.setColor(QtGui.QColor(0, 0, 0)) # 设置画笔颜色为黑色 + pen.setStyle(Qt.PenStyle.DotLine) # 设置画笔样式为虚线 + painter.setPen(pen) # 设置画笔为黑色虚线画笔对象 + painter.drawRect(rect) # 绘制项的裁剪区域矩形 - def paint(self, painter, option, widget): - if abs(painter.combinedTransform().m11()) < 2: + def paint(self, painter, option, widget): # 定义项的绘制方法,用于绘制项 + if abs(painter.combinedTransform().m11()) < 2: # 检查缩放比例是否小于2倍 # We want image smoothing, but only for images where we # are not zoomed in a lot. This is to ensure that for # example icons and pixel sprites can be viewed correctly. - painter.setRenderHint(painter.RenderHint.SmoothPixmapTransform) + painter.setRenderHint(painter.RenderHint.SmoothPixmapTransform) # 设置图像平滑渲染提示 - if self.crop_mode: - self.paint_debug(painter, option, widget) + if self.crop_mode: # 如果处于裁剪模式 + self.paint_debug(painter, option, widget) # 绘制调试信息 # Darken image outside of cropped area - painter.drawPixmap(0, 0, self.pixmap()) - path = QtWidgets.QGraphicsPixmapItem.shape(self) - path.addRect(self.crop_temp) - color = QtGui.QColor(0, 0, 0) - color.setAlpha(100) - painter.setBrush(QtGui.QBrush(color)) - painter.setPen(Qt.PenStyle.NoPen) - painter.drawPath(path) - painter.setBrush(QtGui.QBrush()) + painter.drawPixmap(0, 0, self.pixmap()) # 绘制完整图像 + path = QtWidgets.QGraphicsPixmapItem.shape(self) # 获取项目形状路径 + path.addRect(self.crop_temp) # 向路径添加临时裁剪矩形 + color = QtGui.QColor(0, 0, 0) # 创建黑色 + color.setAlpha(100) # 设置透明度 + painter.setBrush(QtGui.QBrush(color)) # 设置画刷 + painter.setPen(Qt.PenStyle.NoPen) # 设置无笔 + painter.drawPath(path) # 绘制路径(使裁剪区域外变暗) + painter.setBrush(QtGui.QBrush()) # 重置画刷 - for handle in self.crop_handles(): - self.draw_crop_rect(painter, handle()) - self.draw_crop_rect(painter, self.crop_temp) - else: - pm = self._grayscale_pixmap if self.grayscale else self.pixmap() - painter.drawPixmap(self.crop, pm, self.crop) - self.paint_selectable(painter, option, widget) + for handle in self.crop_handles(): # 遍历所有裁剪手柄 + self.draw_crop_rect(painter, handle()) # 绘制裁剪手柄矩形 + self.draw_crop_rect(painter, self.crop_temp) # 绘制临时裁剪矩形 + else: # 非裁剪模式 + pm = self._grayscale_pixmap if self.grayscale else self.pixmap() # 根据灰度模式选择像素图 + painter.drawPixmap(self.crop, pm, self.crop) # 绘制裁剪区域 + self.paint_selectable(painter, option, widget) # 绘制选择效果 - def enter_crop_mode(self): - logger.debug(f'Entering crop mode on {self}') - self.prepareGeometryChange() - self.crop_mode = True - self.crop_temp = QtCore.QRectF(self.crop) - self.crop_mode_move = None - self.crop_mode_event_start = None - self.grabKeyboard() - self.update() - self.scene().crop_item = self + def enter_crop_mode(self): # 进入裁剪模式 + logger.debug(f'Entering crop mode on {self}') # 记录进入裁剪模式的日志 + self.prepareGeometryChange() # 通知Qt几何形状将要改变 + self.crop_mode = True # 启用裁剪模式 + self.crop_temp = QtCore.QRectF(self.crop) # 复制当前裁剪区域作为临时裁剪区域 + self.crop_mode_move = None # 初始化裁剪移动类型 + self.crop_mode_event_start = None # 初始化裁剪事件起始位置 + self.grabKeyboard() # 抓取键盘输入 + self.update() # 更新项目显示 + self.scene().crop_item = self # 设置场景的当前裁剪项目 - def exit_crop_mode(self, confirm): - logger.debug(f'Exiting crop mode with {confirm} on {self}') - if confirm and self.crop != self.crop_temp: - self.scene().undo_stack.push( + def exit_crop_mode(self, confirm): # 退出裁剪模式,参数confirm表示是否确认裁剪 + logger.debug(f'Exiting crop mode with {confirm} on {self}') # 记录退出裁剪模式的日志 + if confirm and self.crop != self.crop_temp: # 如果确认且裁剪区域有变化 + self.scene().undo_stack.push( # 将裁剪操作添加到撤销栈 commands.CropItem(self, self.crop_temp)) - self.prepareGeometryChange() - self.crop_mode = False - self.crop_temp = None - self.crop_mode_move = None - self.crop_mode_event_start = None - self.ungrabKeyboard() - self.update() - self.scene().crop_item = None + self.prepareGeometryChange() # 通知Qt几何形状将要改变 + self.crop_mode = False # 禁用裁剪模式 + self.crop_temp = None # 清除临时裁剪区域 + self.crop_mode_move = None # 清除裁剪移动类型 + self.crop_mode_event_start = None # 清除裁剪事件起始位置 + self.ungrabKeyboard() # 释放键盘抓取 + self.update() # 更新项目显示 + self.scene().crop_item = None # 清除场景的当前裁剪项目 - def keyPressEvent(self, event): - if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter): - self.exit_crop_mode(confirm=True) - elif event.key() == Qt.Key.Key_Escape: - self.exit_crop_mode(confirm=False) - else: - super().keyPressEvent(event) + def keyPressEvent(self, event): # 键盘按键事件处理 + if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter): # 如果按下Enter键 + self.exit_crop_mode(confirm=True) # 确认并退出裁剪模式 + elif event.key() == Qt.Key.Key_Escape: # 如果按下Escape键 + self.exit_crop_mode(confirm=False) # 取消并退出裁剪模式 + else: # 其他按键 + super().keyPressEvent(event) # 调用父类处理 - def hoverMoveEvent(self, event): - if not self.crop_mode: - return super().hoverMoveEvent(event) + def hoverMoveEvent(self, event): # 鼠标悬停移动事件处理 + if not self.crop_mode: # 如果不是裁剪模式 + return super().hoverMoveEvent(event) # 调用父类处理 - for handle in self.crop_handles(): - if handle().contains(event.pos()): - self.set_cursor(self.get_crop_handle_cursor(handle)) + for handle in self.crop_handles(): # 遍历所有裁剪手柄 + if handle().contains(event.pos()): # 如果鼠标在手柄内 + self.set_cursor(self.get_crop_handle_cursor(handle)) # 设置对应的光标 return - for edge in self.crop_edges(): - if edge().contains(event.pos()): - self.set_cursor(self.get_crop_edge_cursor(edge)) + for edge in self.crop_edges(): # 遍历所有裁剪边缘 + if edge().contains(event.pos()): # 如果鼠标在边缘内 + self.set_cursor(self.get_crop_edge_cursor(edge)) # 设置对应的光标 return - self.unset_cursor() + self.unset_cursor() # 重置光标 - def mousePressEvent(self, event): - if not self.crop_mode: - return super().mousePressEvent(event) + def mousePressEvent(self, event): # 鼠标按下事件处理 + if not self.crop_mode: # 如果不是裁剪模式 + return super().mousePressEvent(event) # 调用父类处理 - event.accept() - for handle in self.crop_handles(): + event.accept() # 接受事件 + for handle in self.crop_handles(): # 遍历所有裁剪手柄 # Click into a handle? - if handle().contains(event.pos()): - self.crop_mode_event_start = event.pos() - self.crop_mode_move = handle + if handle().contains(event.pos()): # 如果点击位置在手柄内 + self.crop_mode_event_start = event.pos() # 记录事件起始位置 + self.crop_mode_move = handle # 设置移动类型为该手柄 return - for edge in self.crop_edges(): + for edge in self.crop_edges(): # 遍历所有裁剪边缘 # Click into an edge handle? - if edge().contains(event.pos()): - self.crop_mode_event_start = event.pos() - self.crop_mode_move = edge + if edge().contains(event.pos()): # 如果点击位置在边缘内 + self.crop_mode_event_start = event.pos() # 记录事件起始位置 + self.crop_mode_move = edge # 设置移动类型为该边缘 return - # Click not in handle, end cropping mode: + # Click not in handle, end cropping mode: 如果点击不在手柄或边缘上,结束裁剪模式 self.exit_crop_mode( - confirm=self.crop_temp.contains(event.pos())) + confirm=self.crop_temp.contains(event.pos())) # 根据点击位置决定是否确认裁剪 - def ensure_point_within_crop_bounds(self, point, handle): + def ensure_point_within_crop_bounds(self, point, handle): # 确保点在裁剪边界内 """Returns the point, or the nearest point within the pixmap.""" - if handle == self.crop_handle_topleft: - topleft = QtCore.QPointF(0, 0) - bottomright = self.crop_temp.bottomRight() - if handle == self.crop_handle_bottomleft: - topleft = QtCore.QPointF(0, self.crop_temp.top()) - bottomright = QtCore.QPointF( + if handle == self.crop_handle_topleft: # 如果是左上角手柄 + topleft = QtCore.QPointF(0, 0) # 左上角边界 + bottomright = self.crop_temp.bottomRight() # 右下角边界(临时裁剪区域的右下角) + if handle == self.crop_handle_bottomleft: # 如果是左下角手柄 + topleft = QtCore.QPointF(0, self.crop_temp.top()) # 左上角边界 + bottomright = QtCore.QPointF( # 右下角边界 self.crop_temp.right(), self.pixmap().size().height()) - if handle == self.crop_handle_bottomright: - topleft = self.crop_temp.topLeft() - bottomright = QtCore.QPointF( + if handle == self.crop_handle_bottomright: # 如果是右下角手柄 + topleft = self.crop_temp.topLeft() # 左上角边界(临时裁剪区域的左上角) + bottomright = QtCore.QPointF( # 右下角边界(整个像素图的右下角) self.pixmap().size().width(), self.pixmap().size().height()) - if handle == self.crop_handle_topright: - topleft = QtCore.QPointF(self.crop_temp.left(), 0) - bottomright = QtCore.QPointF( + if handle == self.crop_handle_topright: # 如果是右上角手柄 + topleft = QtCore.QPointF(self.crop_temp.left(), 0) # 左上角边界 + bottomright = QtCore.QPointF( # 右下角边界 self.pixmap().size().width(), self.crop_temp.bottom()) - if handle == self.crop_edge_top: - topleft = QtCore.QPointF(0, 0) - bottomright = QtCore.QPointF( + if handle == self.crop_edge_top: # 如果是顶部边缘 + topleft = QtCore.QPointF(0, 0) # 左上角边界 + bottomright = QtCore.QPointF( # 右下角边界 self.pixmap().size().width(), self.crop_temp.bottom()) - if handle == self.crop_edge_bottom: - topleft = QtCore.QPointF(0, self.crop_temp.top()) - bottomright = QtCore.QPointF( + if handle == self.crop_edge_bottom: # 如果是底部边缘 + topleft = QtCore.QPointF(0, self.crop_temp.top()) # 左上角边界 + bottomright = QtCore.QPointF( # 右下角边界 self.pixmap().size().width(), self.pixmap().size().height()) - if handle == self.crop_edge_left: - topleft = QtCore.QPointF(0, 0) - bottomright = QtCore.QPointF( + if handle == self.crop_edge_left: # 如果是左侧边缘 + topleft = QtCore.QPointF(0, 0) # 左上角边界 + bottomright = QtCore.QPointF( # 右下角边界 self.crop_temp.right(), self.pixmap().size().height()) - if handle == self.crop_edge_right: - topleft = QtCore.QPointF(self.crop_temp.left(), 0) - bottomright = QtCore.QPointF( + if handle == self.crop_edge_right: # 如果是右侧边缘 + topleft = QtCore.QPointF(self.crop_temp.left(), 0) # 左上角边界 + bottomright = QtCore.QPointF( # 右下角边界 self.pixmap().size().width(), self.pixmap().size().height()) - point.setX(min(bottomright.x(), max(topleft.x(), point.x()))) - point.setY(min(bottomright.y(), max(topleft.y(), point.y()))) + point.setX(min(bottomright.x(), max(topleft.x(), point.x()))) # 限制点的X坐标在边界内 + point.setY(min(bottomright.y(), max(topleft.y(), point.y()))) # 限制点的Y坐标在边界内 - return point + return point # 返回限制后的点 - def mouseMoveEvent(self, event): - if self.crop_mode and self.crop_mode_event_start: - diff = event.pos() - self.crop_mode_event_start - if self.crop_mode_move == self.crop_handle_topleft: - new = self.ensure_point_within_crop_bounds( + def mouseMoveEvent(self, event): # 鼠标移动事件处理 + if self.crop_mode and self.crop_mode_event_start: # 如果处于裁剪模式且有起始位置 + diff = event.pos() - self.crop_mode_event_start # 计算位置差异 + if self.crop_mode_move == self.crop_handle_topleft: # 如果移动左上角手柄 + new = self.ensure_point_within_crop_bounds( # 确保新位置在边界内 self.crop_temp.topLeft() + diff, self.crop_mode_move) - self.crop_temp.setTopLeft(new) - if self.crop_mode_move == self.crop_handle_bottomleft: - new = self.ensure_point_within_crop_bounds( + self.crop_temp.setTopLeft(new) # 更新左上角 + if self.crop_mode_move == self.crop_handle_bottomleft: # 如果移动左下角手柄 + new = self.ensure_point_within_crop_bounds( # 确保新位置在边界内 self.crop_temp.bottomLeft() + diff, self.crop_mode_move) - self.crop_temp.setBottomLeft(new) - if self.crop_mode_move == self.crop_handle_bottomright: - new = self.ensure_point_within_crop_bounds( + self.crop_temp.setBottomLeft(new) # 更新左下角 + if self.crop_mode_move == self.crop_handle_bottomright: # 如果移动右下角手柄 + new = self.ensure_point_within_crop_bounds( # 确保新位置在边界内 self.crop_temp.bottomRight() + diff, self.crop_mode_move) - self.crop_temp.setBottomRight(new) - if self.crop_mode_move == self.crop_handle_topright: - new = self.ensure_point_within_crop_bounds( + self.crop_temp.setBottomRight(new) # 更新右下角 + if self.crop_mode_move == self.crop_handle_topright: # 如果移动右上角手柄 + new = self.ensure_point_within_crop_bounds( # 确保新位置在边界内 self.crop_temp.topRight() + diff, self.crop_mode_move) - self.crop_temp.setTopRight(new) - if self.crop_mode_move == self.crop_edge_top: - new = self.ensure_point_within_crop_bounds( + self.crop_temp.setTopRight(new) # 更新右上角 + if self.crop_mode_move == self.crop_edge_top: # 如果移动顶部边缘 + new = self.ensure_point_within_crop_bounds( # 确保新位置在边界内 self.crop_temp.topLeft() + diff, self.crop_mode_move) - self.crop_temp.setTop(new.y()) - if self.crop_mode_move == self.crop_edge_left: - new = self.ensure_point_within_crop_bounds( + self.crop_temp.setTop(new.y()) # 更新顶部 + if self.crop_mode_move == self.crop_edge_left: # 如果移动左侧边缘 + new = self.ensure_point_within_crop_bounds( # 确保新位置在边界内 self.crop_temp.topLeft() + diff, self.crop_mode_move) - self.crop_temp.setLeft(new.x()) - if self.crop_mode_move == self.crop_edge_bottom: - new = self.ensure_point_within_crop_bounds( + self.crop_temp.setLeft(new.x()) # 更新左侧 + if self.crop_mode_move == self.crop_edge_bottom: # 如果移动底部边缘 + new = self.ensure_point_within_crop_bounds( # 确保新位置在边界内 self.crop_temp.bottomLeft() + diff, self.crop_mode_move) - self.crop_temp.setBottom(new.y()) - if self.crop_mode_move == self.crop_edge_right: - new = self.ensure_point_within_crop_bounds( + self.crop_temp.setBottom(new.y()) # 更新底部 + if self.crop_mode_move == self.crop_edge_right: # 如果移动右侧边缘 + new = self.ensure_point_within_crop_bounds( # 确保新位置在边界内 self.crop_temp.topRight() + diff, self.crop_mode_move) - self.crop_temp.setRight(new.x()) - self.update() - self.crop_mode_event_start = event.pos() - event.accept() - else: - super().mouseMoveEvent(event) + self.crop_temp.setRight(new.x()) # 更新右侧 + self.update() # 更新项目显示 + self.crop_mode_event_start = event.pos() # 更新事件起始位置 + event.accept() # 接受事件 + else: # 非裁剪模式或没有起始位置 + super().mouseMoveEvent(event) # 调用父类处理 - def mouseReleaseEvent(self, event): - if self.crop_mode: - self.crop_mode_move = None - self.crop_mode_event_start = None - event.accept() - else: - super().mouseReleaseEvent(event) + def mouseReleaseEvent(self, event): # 鼠标释放事件处理 + if self.crop_mode: # 如果处于裁剪模式 + self.crop_mode_move = None # 清除移动类型 + self.crop_mode_event_start = None # 清除事件起始位置 + event.accept() # 接受事件 + else: # 非裁剪模式 + super().mouseReleaseEvent(event) # 调用父类处理 -@register_item -class BeeTextItem(BeeItemMixin, QtWidgets.QGraphicsTextItem): +@register_item # 使用装饰器注册该类 +class BeeTextItem(BeeItemMixin, QtWidgets.QGraphicsTextItem): # 文本项目类,继承自BeeItemMixin和QtWidgets.QGraphicsTextItem """Class for text added by the user.""" - TYPE = 'text' + TYPE = 'text' # 项目类型标识符 - def __init__(self, text=None, **kwargs): - super().__init__(text or "Text") - self.save_id = None - logger.debug(f'Initialized {self}') - self.is_image = False - self.init_selectable() - self.is_editable = True - self.edit_mode = False - self.setDefaultTextColor(QtGui.QColor(*COLORS['Scene:Text'])) + def __init__(self, text=None, **kwargs): # 初始化文本项目 + super().__init__(text or "Text") # 调用父类构造函数,默认文本为"Text" + self.save_id = None # 保存ID,用于标识项目 + logger.debug(f'Initialized {self}') # 记录初始化日志 + self.is_image = False # 标记为非图像类型 + self.init_selectable() # 初始化可选择属性 + self.is_editable = True # 标记为可编辑 + self.edit_mode = False # 编辑模式标志 + self.setDefaultTextColor(QtGui.QColor(*COLORS['Scene:Text'])) # 设置默认文本颜色 - @classmethod + @classmethod # 类方法,从数据创建项目 def create_from_data(cls, **kwargs): - data = kwargs.get('data', {}) - item = cls(**data) - return item + data = kwargs.get('data', {}) # 获取项目数据 + item = cls(**data) # 创建项目实例 + return item # 返回创建的项目 - def __str__(self): - txt = self.toPlainText()[:40] - return (f'Text "{txt}"') + def __str__(self): # 返回项目的字符串表示 + txt = self.toPlainText()[:40] # 获取文本前40个字符 + return (f'Text "{txt}"') # 返回格式化的字符串 - def get_extra_save_data(self): - return {'text': self.toPlainText()} + def get_extra_save_data(self): # 获取需要保存的额外数据 + return {'text': self.toPlainText()} # 返回文本内容 - def contains(self, point): - return self.boundingRect().contains(point) + def contains(self, point): # 检查点是否在项目内 + return self.boundingRect().contains(point) # 检查点是否在边界矩形内 - def paint(self, painter, option, widget): - painter.setPen(Qt.PenStyle.NoPen) - color = QtGui.QColor(0, 0, 0) - color.setAlpha(40) - brush = QtGui.QBrush(color) - painter.setBrush(brush) - painter.drawRect(QtWidgets.QGraphicsTextItem.boundingRect(self)) - option.state = QtWidgets.QStyle.StateFlag.State_Enabled - super().paint(painter, option, widget) - self.paint_selectable(painter, option, widget) + def paint(self, painter, option, widget): # 绘制项目 + painter.setPen(Qt.PenStyle.NoPen) # 设置无笔 + color = QtGui.QColor(0, 0, 0) # 创建黑色 + color.setAlpha(40) # 设置透明度 + brush = QtGui.QBrush(color) # 创建画刷 + painter.setBrush(brush) # 设置画刷 + painter.drawRect(QtWidgets.QGraphicsTextItem.boundingRect(self)) # 绘制背景矩形 + option.state = QtWidgets.QStyle.StateFlag.State_Enabled # 设置选项状态为启用 + super().paint(painter, option, widget) # 调用父类绘制文本 + self.paint_selectable(painter, option, widget) # 绘制选择效果 - def create_copy(self): - item = BeeTextItem(self.toPlainText()) - item.setPos(self.pos()) - item.setZValue(self.zValue()) - item.setScale(self.scale()) - item.setRotation(self.rotation()) - if self.flip() == -1: - item.do_flip() - return item + def create_copy(self): # 创建项目的副本 + item = BeeTextItem(self.toPlainText()) # 创建新项目,文本内容相同 + item.setPos(self.pos()) # 设置相同的位置 + item.setZValue(self.zValue()) # 设置相同的Z值 + item.setScale(self.scale()) # 设置相同的缩放比例 + item.setRotation(self.rotation()) # 设置相同的旋转角度 + if self.flip() == -1: # 如果当前是翻转状态 + item.do_flip() # 翻转新项目 + return item # 返回创建的副本 - def enter_edit_mode(self): - logger.debug(f'Entering edit mode on {self}') - self.edit_mode = True - self.old_text = self.toPlainText() - self.setTextInteractionFlags( + def enter_edit_mode(self): # 进入编辑模式 + logger.debug(f'Entering edit mode on {self}') # 记录进入编辑模式的日志 + self.edit_mode = True # 启用编辑模式 + self.old_text = self.toPlainText() # 保存当前文本,用于取消编辑 + self.setTextInteractionFlags( # 设置文本交互标志为编辑交互 Qt.TextInteractionFlag.TextEditorInteraction) - self.scene().edit_item = self + self.scene().edit_item = self # 设置场景的当前编辑项目 - def exit_edit_mode(self, commit=True): - logger.debug(f'Exiting edit mode on {self}') - self.edit_mode = False - # reset selection: - self.setTextCursor(QtGui.QTextCursor(self.document())) - self.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction) - self.scene().edit_item = None - if commit: - self.scene().undo_stack.push( + def exit_edit_mode(self, commit=True): # 退出编辑模式,参数commit表示是否确认修改 + logger.debug(f'Exiting edit mode on {self}') # 记录退出编辑模式的日志 + self.edit_mode = False # 禁用编辑模式 + # reset selection: 重置选择 + self.setTextCursor(QtGui.QTextCursor(self.document())) # 重置文本光标 + self.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction) # 禁用文本交互 + self.scene().edit_item = None # 清除场景的当前编辑项目 + if commit: # 如果确认编辑 + self.scene().undo_stack.push( # 将文本更改添加到撤销栈 commands.ChangeText(self, self.toPlainText(), self.old_text)) - if not self.toPlainText().strip(): - logger.debug('Removing empty text item') - self.scene().undo_stack.push( + if not self.toPlainText().strip(): # 如果文本为空 + logger.debug('Removing empty text item') # 记录删除空文本项目的日志 + self.scene().undo_stack.push( # 将删除操作添加到撤销栈 commands.DeleteItems(self.scene(), [self])) - else: - self.setPlainText(self.old_text) + else: # 如果取消编辑 + self.setPlainText(self.old_text) # 恢复旧文本 - def has_selection_handles(self): + def has_selection_handles(self): # 检查项目是否有选择手柄(编辑模式下没有选择手柄) return super().has_selection_handles() and not self.edit_mode - def keyPressEvent(self, event): - if (event.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return) + def keyPressEvent(self, event): # 键盘按键事件处理 + if (event.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return) # 如果按下Enter键且没有修饰键 and event.modifiers() == Qt.KeyboardModifier.NoModifier): - self.exit_edit_mode() + self.exit_edit_mode() # 退出编辑模式并确认 event.accept() return - if (event.key() == Qt.Key.Key_Escape + if (event.key() == Qt.Key.Key_Escape # 如果按下Escape键且没有修饰键 and event.modifiers() == Qt.KeyboardModifier.NoModifier): - self.exit_edit_mode(commit=False) + self.exit_edit_mode(commit=False) # 退出编辑模式并取消 event.accept() return - super().keyPressEvent(event) + super().keyPressEvent(event) # 其他按键,调用父类处理 - def copy_to_clipboard(self, clipboard): - clipboard.setText(self.toPlainText()) + def copy_to_clipboard(self, clipboard): # 将项目复制到剪贴板 + clipboard.setText(self.toPlainText()) # 设置剪贴板内容为文本 -@register_item -class BeeErrorItem(BeeItemMixin, QtWidgets.QGraphicsTextItem): +@register_item # 使用装饰器注册该类 +class BeeErrorItem(BeeItemMixin, QtWidgets.QGraphicsTextItem): # 错误项目类,继承自BeeItemMixin和QtWidgets.QGraphicsTextItem """Class for displaying error messages when an item can't be loaded from a bee file. @@ -742,65 +742,65 @@ class BeeErrorItem(BeeItemMixin, QtWidgets.QGraphicsTextItem): is saved. """ - TYPE = 'error' + TYPE = 'error' # 项目类型标识符 - def __init__(self, text=None, **kwargs): - super().__init__(text or "Text") - self.original_save_id = None - logger.debug(f'Initialized {self}') - self.is_image = False - self.init_selectable() - self.is_editable = False - self.setDefaultTextColor(QtGui.QColor(*COLORS['Scene:Text'])) + def __init__(self, text=None, **kwargs): # 初始化错误项目 + super().__init__(text or "Text") # 调用父类构造函数 + self.original_save_id = None # 原始项目的保存ID + logger.debug(f'Initialized {self}') # 记录初始化日志 + self.is_image = False # 标记为非图像类型 + self.init_selectable() # 初始化可选择属性 + self.is_editable = False # 标记为不可编辑 + self.setDefaultTextColor(QtGui.QColor(*COLORS['Scene:Text'])) # 设置默认文本颜色 - @classmethod + @classmethod # 类方法,从数据创建项目 def create_from_data(cls, **kwargs): - data = kwargs.get('data', {}) - item = cls(**data) - return item + data = kwargs.get('data', {}) # 获取项目数据 + item = cls(**data) # 创建项目实例 + return item # 返回创建的项目 - def __str__(self): - txt = self.toPlainText()[:40] - return (f'Error "{txt}"') + def __str__(self): # 返回项目的字符串表示 + txt = self.toPlainText()[:40] # 获取文本前40个字符 + return (f'Error "{txt}"') # 返回格式化的字符串 - def contains(self, point): - return self.boundingRect().contains(point) + def contains(self, point): # 检查点是否在项目内 + return self.boundingRect().contains(point) # 检查点是否在边界矩形内 - def paint(self, painter, option, widget): - painter.setPen(Qt.PenStyle.NoPen) - color = QtGui.QColor(200, 0, 0) - brush = QtGui.QBrush(color) - painter.setBrush(brush) - painter.drawRect(QtWidgets.QGraphicsTextItem.boundingRect(self)) - option.state = QtWidgets.QStyle.StateFlag.State_Enabled - super().paint(painter, option, widget) - self.paint_selectable(painter, option, widget) + def paint(self, painter, option, widget): # 绘制项目 + painter.setPen(Qt.PenStyle.NoPen) # 设置无笔 + color = QtGui.QColor(200, 0, 0) # 创建红色 + brush = QtGui.QBrush(color) # 创建画刷 + painter.setBrush(brush) # 设置画刷 + painter.drawRect(QtWidgets.QGraphicsTextItem.boundingRect(self)) # 绘制红色背景矩形 + option.state = QtWidgets.QStyle.StateFlag.State_Enabled # 设置选项状态为启用 + super().paint(painter, option, widget) # 调用父类绘制文本 + self.paint_selectable(painter, option, widget) # 绘制选择效果 - def update_from_data(self, **kwargs): - self.original_save_id = kwargs.get('save_id', self.original_save_id) - self.setPos(kwargs.get('x', self.pos().x()), - kwargs.get('y', self.pos().y())) - self.setZValue(kwargs.get('z', self.zValue())) - self.setScale(kwargs.get('scale', self.scale())) - self.setRotation(kwargs.get('rotation', self.rotation())) + def update_from_data(self, **kwargs): # 从数据更新项目属性 + self.original_save_id = kwargs.get('save_id', self.original_save_id) # 更新原始保存ID + self.setPos(kwargs.get('x', self.pos().x()), # 更新X坐标 + kwargs.get('y', self.pos().y())) # 更新Y坐标 + self.setZValue(kwargs.get('z', self.zValue())) # 更新Z值 + self.setScale(kwargs.get('scale', self.scale())) # 更新缩放比例 + self.setRotation(kwargs.get('rotation', self.rotation())) # 更新旋转角度 - def create_copy(self): - item = BeeErrorItem(self.toPlainText()) - item.setPos(self.pos()) - item.setZValue(self.zValue()) - item.setScale(self.scale()) - item.setRotation(self.rotation()) - return item + def create_copy(self): # 创建项目的副本 + item = BeeErrorItem(self.toPlainText()) # 创建新项目,文本内容相同 + item.setPos(self.pos()) # 设置相同的位置 + item.setZValue(self.zValue()) # 设置相同的Z值 + item.setScale(self.scale()) # 设置相同的缩放比例 + item.setRotation(self.rotation()) # 设置相同的旋转角度 + return item # 返回创建的副本 - def flip(self, *args, **kwargs): + def flip(self, *args, **kwargs): # 返回翻转值(错误项目永远不翻转) """Returns the flip value (1 or -1)""" # Never display error messages flipped return 1 - def do_flip(self, *args, **kwargs): + def do_flip(self, *args, **kwargs): # 翻转项目(错误项目永远不翻转) """Flips the item.""" # Never flip error messages pass - def copy_to_clipboard(self, clipboard): - clipboard.setText(self.toPlainText()) + def copy_to_clipboard(self, clipboard): # 将项目复制到剪贴板 + clipboard.setText(self.toPlainText()) # 设置剪贴板内容为文本 \ No newline at end of file diff --git a/beeref/scene.py b/beeref/scene.py index 56325c2..eeda235 100644 --- a/beeref/scene.py +++ b/beeref/scene.py @@ -13,552 +13,555 @@ # You should have received a copy of the GNU General Public License # along with BeeRef. If not, see . -from functools import partial -import logging -import math -from queue import Queue +from functools import partial # 导入偏函数,用于固定函数参数创建新函数 +import logging # 导入日志模块,用于记录程序运行时的日志信息 +import math # 导入数学模块,用于数学计算(如三角函数、平方根等) +from queue import Queue # 导入队列数据结构,用于线程间通信或任务排队 -from PyQt6 import QtCore, QtWidgets, QtGui -from PyQt6.QtCore import Qt +from PyQt6 import QtCore, QtWidgets, QtGui # 导入PyQt6的核心模块、控件模块和GUI模块 +from PyQt6.QtCore import Qt # 导入Qt常量定义(如对齐方式、事件类型等) -import rpack +import rpack # 导入矩形打包库,用于高效排列矩形元素 -from beeref import commands -from beeref.config import BeeSettings -from beeref.items import item_registry, BeeErrorItem, sort_by_filename -from beeref.selection import MultiSelectItem, RubberbandItem +from beeref import commands # 导入beeref项目的命令模块,处理用户操作的命令实现 +from beeref.config import BeeSettings # 导入配置设置类,管理应用程序的用户配置 +from beeref.items import item_registry, BeeErrorItem, sort_by_filename # 导入项目项相关组件:注册表、错误项和文件名排序函数 +from beeref.selection import MultiSelectItem, RubberbandItem # 导入选择相关类:多选项和橡皮筋选择框 -logger = logging.getLogger(__name__) +logger = logging.getLogger(__name__) # 创建当前模块的日志记录器 -class BeeGraphicsScene(QtWidgets.QGraphicsScene): - cursor_changed = QtCore.pyqtSignal(QtGui.QCursor) - cursor_cleared = QtCore.pyqtSignal() +class BeeGraphicsScene(QtWidgets.QGraphicsScene): # 定义场景类,继承自QGraphicsScene + cursor_changed = QtCore.pyqtSignal(QtGui.QCursor) # 定义光标变化信号 + cursor_cleared = QtCore.pyqtSignal() # 定义光标清除信号 - MOVE_MODE = 1 - RUBBERBAND_MODE = 2 + MOVE_MODE = 1 # 定义移动模式常量 + RUBBERBAND_MODE = 2 # 定义橡皮筋选择模式常量 - def __init__(self, undo_stack): - super().__init__() - self.active_mode = None - self.undo_stack = undo_stack - self.max_z = 0 - self.min_z = 0 - self.Z_STEP = 0.001 - self.selectionChanged.connect(self.on_selection_change) - self.changed.connect(self.on_change) - self.items_to_add = Queue() - self.edit_item = None - self.crop_item = None - self.settings = BeeSettings() - self.clear() - self._clear_ongoing = False + def __init__(self, undo_stack): # 初始化方法,接收撤销栈参数 + super().__init__() # 调用父类的初始化方法 + self.active_mode = None # 初始化当前活动模式为None + self.undo_stack = undo_stack # 保存撤销栈引用 + self.max_z = 0 # 初始化最大Z轴值 + self.min_z = 0 # 初始化最小Z轴值 + self.Z_STEP = 0.001 # 设置Z轴步长 + self.selectionChanged.connect(self.on_selection_change) # 连接选择变化信号到处理函数 + self.changed.connect(self.on_change) # 连接场景变化信号到处理函数 + self.items_to_add = Queue() # 初始化待添加项目队列 + self.edit_item = None # 初始化当前编辑项目为None + self.crop_item = None # 初始化裁剪项目为None + self.settings = BeeSettings() # 初始化设置对象 + self.clear() # 清除场景内容 + self._clear_ongoing = False # 初始化清除操作标志为False def clear(self): - self._clear_ongoing = True - super().clear() - self.internal_clipboard = [] - self.rubberband_item = RubberbandItem() - self.multi_select_item = MultiSelectItem() - self._clear_ongoing = False + self._clear_ongoing = True # 设置清理进行中标志,避免清理过程中触发其他依赖状态的逻辑 + super().clear() # 调用父类的清除方法,清除场景中的所有项目 + self.internal_clipboard = [] # 初始化内部剪贴板为空列表,用于存储复制到剪贴板的项目 + self.rubberband_item = RubberbandItem() # 重新创建橡皮筋选择框实例,重置选择框状态 + self.multi_select_item = MultiSelectItem() # 重新创建多选项实例,重置多选项状态 + self._clear_ongoing = False # 清除操作完成,重置清除进行中标志 - def addItem(self, item): - logger.debug(f'Adding item {item}') - super().addItem(item) + def addItem(self, item): # 添加项目到场景 + logger.debug(f'Adding item {item}') # 记录添加项目的调试日志 + super().addItem(item) # 调用父类的添加项目方法,将项目添加到场景中 - def removeItem(self, item): - logger.debug(f'Removing item {item}') - super().removeItem(item) + def removeItem(self, item): # 从场景中移除项目 + logger.debug(f'Removing item {item}') # 记录移除项目的调试日志 + super().removeItem(item) # 调用父类的移除项目方法,将项目从场景中移除 - def cancel_active_modes(self): + def cancel_active_modes(self): # 取消所有活动模式 """Cancels ongoing crop modes, rubberband modes etc, if there are any. """ - self.cancel_crop_mode() - self.end_rubberband_mode() + self.cancel_crop_mode() # 取消当前的裁剪模式 + self.end_rubberband_mode() # 结束当前的橡皮筋选择模式 - def end_rubberband_mode(self): - if self.rubberband_item.scene(): - logger.debug('Ending rubberband selection') - self.removeItem(self.rubberband_item) - self.active_mode = None + def end_rubberband_mode(self): # 结束橡皮筋选择模式 + if self.rubberband_item.scene(): # 如果橡皮筋选择框存在于场景中 + logger.debug('Ending rubberband selection') # 记录结束橡皮筋选择模式的调试日志 + self.removeItem(self.rubberband_item) # 从场景中移除橡皮筋选择框 + self.active_mode = None # 重置当前活动模式为None - def cancel_crop_mode(self): - """Cancels an ongoing crop mode, if there is any.""" - if self.crop_item: - self.crop_item.exit_crop_mode(confirm=False) + def cancel_crop_mode(self): # 取消当前的裁剪模式 + """Cancels an ongoing crop mode, if there is any.""" + if self.crop_item: # 如果存在裁剪项目 + self.crop_item.exit_crop_mode(confirm=False) # 退出裁剪模式,不确认裁剪 - def copy_selection_to_internal_clipboard(self): - self.internal_clipboard = [] - for item in self.selectedItems(user_only=True): - self.internal_clipboard.append(item) + def copy_selection_to_internal_clipboard(self): # 复制当前选择的项目到内部剪贴板 + self.internal_clipboard = [] # 清空内部剪贴板 + for item in self.selectedItems(user_only=True): # 遍历当前用户选择的项目 + self.internal_clipboard.append(item) # 将项目添加到内部剪贴板 - def paste_from_internal_clipboard(self, position): - copies = [] + def paste_from_internal_clipboard(self, position): # 从内部剪贴板粘贴项目到指定位置 + copies = [] # 创建一个空列表,用于存储复制的项目 for item in self.internal_clipboard: - copy = item.create_copy() - copies.append(copy) - self.undo_stack.push(commands.InsertItems(self, copies, position)) + copy = item.create_copy() # 创建当前项目的副本 + copies.append(copy) # 将副本添加到复制列表中 + self.undo_stack.push(commands.InsertItems(self, copies, position)) # 将复制操作添加到撤销栈中 - def raise_to_top(self): - self.cancel_active_modes() - items = self.selectedItems(user_only=True) - z_values = map(lambda i: i.zValue(), items) - delta = self.max_z + self.Z_STEP - min(z_values) - logger.debug(f'Raise to top, delta: {delta}') - for item in items: - item.setZValue(item.zValue() + delta) + def raise_to_top(self): # 将当前选择的项目提升到顶部 + self.cancel_active_modes() # 取消所有活动模式,确保没有正在进行的操作干扰 + items = self.selectedItems(user_only=True) # 获取当前用户选择的项目 + z_values = map(lambda i: i.zValue(), items) # 提取项目的Z轴值 + delta = self.max_z + self.Z_STEP - min(z_values) # 计算需要提升的Z轴值增量 + logger.debug(f'Raise to top, delta: {delta}') # 记录提升到顶部的调试日志 + for item in items: # 遍历选择的项目 + item.setZValue(item.zValue() + delta) # 提升项目的Z轴值,将其提升到顶部 - def lower_to_bottom(self): - self.cancel_active_modes() - items = self.selectedItems(user_only=True) - z_values = map(lambda i: i.zValue(), items) - delta = self.min_z - self.Z_STEP - max(z_values) - logger.debug(f'Lower to bottom, delta: {delta}') + def lower_to_bottom(self): # 将当前选择的项目降低到底部 + self.cancel_active_modes() # 取消所有活动模式,确保没有正在进行的操作干扰 + items = self.selectedItems(user_only=True) # 获取当前用户选择的项目 + z_values = map(lambda i: i.zValue(), items) # 提取项目的Z轴值 + delta = self.min_z - self.Z_STEP - max(z_values) # 计算需要降低的Z轴值增量 + logger.debug(f'Lower to bottom, delta: {delta}') # 记录降低到底部的调试日志 - for item in items: - item.setZValue(item.zValue() + delta) + for item in items: # 遍历选择的项目 + item.setZValue(item.zValue() + delta) # 降低项目的Z轴值,将其降低到底部 - def normalize_width_or_height(self, mode): + def normalize_width_or_height(self, mode): # 归一化选择项目的宽度或高度 """Scale the selected images to have the same width or height, as specified by ``mode``. :param mode: "width" or "height". """ - self.cancel_active_modes() - values = [] - items = self.selectedItems(user_only=True) - for item in items: - rect = self.itemsBoundingRect(items=[item]) - values.append(getattr(rect, mode)()) - if len(values) < 2: - return - avg = sum(values) / len(values) - logger.debug(f'Calculated average {mode} {avg}') + self.cancel_active_modes() # 取消所有活动模式,确保没有正在进行的操作干扰 + values = [] # 创建一个空列表,用于存储选择项目的宽度或高度值 + items = self.selectedItems(user_only=True) # 获取当前用户选择的项目 + for item in items: # 遍历选择的项目 + rect = self.itemsBoundingRect(items=[item]) # 获取项目的边界矩形 + values.append(getattr(rect, mode)()) # 提取矩形的宽度或高度值,并添加到列表中 + if len(values) < 2: # 如果选择的项目数量小于2 + return # 直接返回,无需归一化 + avg = sum(values) / len(values) # 计算宽度或高度的平均值 + logger.debug(f'Calculated average {mode} {avg}') # 记录归一化的平均值调试日志 - scale_factors = [] - for item in items: - rect = self.itemsBoundingRect(items=[item]) - scale_factors.append(avg / getattr(rect, mode)()) + scale_factors = [] # 创建一个空列表,用于存储每个项目的归一化缩放因子 + for item in items: # 遍历选择的项目 + rect = self.itemsBoundingRect(items=[item]) # 获取项目的边界矩形 + scale_factors.append(avg / getattr(rect, mode)()) # 计算项目的归一化缩放因子,并添加到列表中 self.undo_stack.push( - commands.NormalizeItems(items, scale_factors)) + commands.NormalizeItems(items, scale_factors)) # 将归一化操作添加到撤销栈中 - def normalize_height(self): + def normalize_height(self): # 归一化选择项目的高度 """Scale selected images to the same height.""" - return self.normalize_width_or_height('height') + return self.normalize_width_or_height('height') # 归一化选择项目的高度 - def normalize_width(self): + def normalize_width(self): # 归一化选择项目的宽度 """Scale selected images to the same width.""" - return self.normalize_width_or_height('width') + return self.normalize_width_or_height('width') # 归一化选择项目的宽度 - def normalize_size(self): + def normalize_size(self): # 归一化选择项目的大小 """Scale selected images to the same size. Size meaning the area = widh * height. """ - self.cancel_active_modes() - sizes = [] - items = self.selectedItems(user_only=True) - for item in items: - rect = self.itemsBoundingRect(items=[item]) - sizes.append(rect.width() * rect.height()) + self.cancel_active_modes() # 取消所有活动模式,确保没有正在进行的操作干扰 + sizes = [] # 创建一个空列表,用于存储选择项目的面积值 + items = self.selectedItems(user_only=True) # 获取当前用户选择的项目 + for item in items: # 遍历选择的项目 + rect = self.itemsBoundingRect(items=[item]) # 获取项目的边界矩形 + sizes.append(rect.width() * rect.height()) # 计算项目的面积值,并添加到列表中 - if len(sizes) < 2: - return + if len(sizes) < 2: # 如果选择的项目数量小于2 + return # 直接返回,无需归一化 - avg = sum(sizes) / len(sizes) - logger.debug(f'Calculated average size {avg}') + avg = sum(sizes) / len(sizes) # 计算选择项目的面积平均值 + logger.debug(f'Calculated average size {avg}') # 记录归一化的平均值调试日志 - scale_factors = [] - for item in items: - rect = self.itemsBoundingRect(items=[item]) - scale_factors.append(math.sqrt(avg / rect.width() / rect.height())) + scale_factors = [] # 创建一个空列表,用于存储每个项目的归一化缩放因子 + for item in items: # 遍历选择的项目 + rect = self.itemsBoundingRect(items=[item]) # 获取项目的边界矩形 + scale_factors.append(math.sqrt(avg / rect.width() / rect.height())) # 计算项目的归一化缩放因子,并添加到列表中 self.undo_stack.push( - commands.NormalizeItems(items, scale_factors)) + commands.NormalizeItems(items, scale_factors)) # 将归一化操作添加到撤销栈中 - def arrange_default(self): - default = self.settings.valueOrDefault('Items/arrange_default') + + + def arrange_default(self): # 按默认方式排列选择的项目 + default = self.settings.valueOrDefault('Items/arrange_default') # 获取默认的排列方式 MAPPING = { - 'optimal': self.arrange_optimal, - 'horizontal': self.arrange, - 'vertical': partial(self.arrange, vertical=True), - 'square': self.arrange_square, + 'optimal': self.arrange_optimal, # 最优排列方式 + 'horizontal': self.arrange, # 水平排列方式 + 'vertical': partial(self.arrange, vertical=True), # 垂直排列方式 + 'square': self.arrange_square, # 正方形排列方式 } - MAPPING[default]() + MAPPING[default]() # 调用默认的排列方式 - def arrange(self, vertical=False): - """Arrange items in a line (horizontally or vertically).""" + def arrange(self, vertical=False): # 按指定方式排列选择的项目 + """Arrange items in a line (horizontally or vertically).""" # 按指定方式排列选择的项目 - self.cancel_active_modes() + self.cancel_active_modes() # 取消所有活动模式,确保没有正在进行的操作干扰 - items = sort_by_filename(self.selectedItems(user_only=True)) - if len(items) < 2: - return + items = sort_by_filename(self.selectedItems(user_only=True)) # 获取当前用户选择的项目,并按文件名排序 + if len(items) < 2:# 如果选择的项目数量小于2 + return # 直接返回,无需排列 - gap = self.settings.valueOrDefault('Items/arrange_gap') - center = self.get_selection_center() - positions = [] - rects = [] - for item in items: - rects.append({ - 'rect': self.itemsBoundingRect(items=[item]), - 'item': item}) + gap = self.settings.valueOrDefault('Items/arrange_gap') # 获取项目之间的间距 + center = self.get_selection_center() # 获取选择项目的中心位置 + positions = [] # 创建一个空列表,用于存储每个项目的新位置 + rects = [] # 创建一个空列表,用于存储每个项目的边界矩形和项目本身 + for item in items: # 遍历选择的项目 + rects.append({ # 为每个项目创建一个字典,包含项目的边界矩形和项目本身 + 'rect': self.itemsBoundingRect(items=[item]), # 获取项目的边界矩形 + 'item': item}) # 包含项目的边界矩形和项目本身 - if vertical: - rects.sort(key=lambda r: r['rect'].topLeft().y()) - sum_height = sum(map(lambda r: r['rect'].height(), rects)) - y = round(center.y() - sum_height/2) - for rect in rects: - positions.append( - QtCore.QPointF( - round(center.x() - rect['rect'].width()/2), y)) - y += rect['rect'].height() + gap + if vertical: # 如果按垂直方向排列 + rects.sort(key=lambda r: r['rect'].topLeft().y()) # 按项目的顶部坐标排序 + sum_height = sum(map(lambda r: r['rect'].height(), rects)) # 计算所有项目的总高度 + y = round(center.y() - sum_height/2) # 计算垂直方向上的起始位置 + for rect in rects: # 遍历每个项目 + positions.append( # 为每个项目计算新的位置 + QtCore.QPointF( # 计算每个项目的新位置 + round(center.x() - rect['rect'].width()/2), y)) # 计算每个项目的新位置 + y += rect['rect'].height() + gap # 更新垂直方向上的位置,考虑项目高度和间距 - else: - rects.sort(key=lambda r: r['rect'].topLeft().x()) - sum_width = sum(map(lambda r: r['rect'].width(), rects)) - x = round(center.x() - sum_width/2) - for rect in rects: - positions.append( - QtCore.QPointF( - x, round(center.y() - rect['rect'].height()/2))) - x += rect['rect'].width() + gap + else: # 如果按水平方向排列 + rects.sort(key=lambda r: r['rect'].topLeft().x()) # 按项目的左侧坐标排序 + sum_width = sum(map(lambda r: r['rect'].width(), rects)) # 计算所有项目的总宽度 + x = round(center.x() - sum_width/2) # 计算水平方向上的起始位置 + for rect in rects: # 遍历每个项目 + positions.append( # 为每个项目计算新的位置 + QtCore.QPointF( # 计算每个项目的新位置 + x, round(center.y() - rect['rect'].height()/2))) # 计算每个项目的新位置 + x += rect['rect'].width() + gap # 更新水平方向上的位置,考虑项目宽度和间距 - self.undo_stack.push( - commands.ArrangeItems(self, - [r['item'] for r in rects], - positions)) + self.undo_stack.push( # 将排列操作添加到撤销栈中 + commands.ArrangeItems(self, # 创建一个排列项目的命令对象 + [r['item'] for r in rects], # 包含所有要排列的项目 + positions)) # 包含所有项目的新位置 - def arrange_optimal(self): - self.cancel_active_modes() + def arrange_optimal(self): # 按最优方式排列选择的项目 + self.cancel_active_modes() # 取消所有活动模式,确保没有正在进行的操作干扰 - items = self.selectedItems(user_only=True) - if len(items) < 2: - return + items = self.selectedItems(user_only=True) # 获取当前用户选择的项目 + if len(items) < 2: # 如果选择的项目数量小于2 + return # 直接返回,无需排列 - gap = self.settings.valueOrDefault('Items/arrange_gap') + gap = self.settings.valueOrDefault('Items/arrange_gap') # 获取项目之间的间距 - sizes = [] - for item in items: - rect = self.itemsBoundingRect(items=[item]) - sizes.append((round(rect.width() + gap), - round(rect.height() + gap))) + sizes = [] # 创建一个空列表,用于存储每个项目的宽度和高度 + for item in items: # 遍历选择的项目 + rect = self.itemsBoundingRect(items=[item]) # 获取项目的边界矩形 + sizes.append((round(rect.width() + gap), # 包含每个项目的宽度和高度 + round(rect.height() + gap))) # 包含每个项目的宽度和高度 # The minimal area the items need if they could be packed optimally; # we use this as a starting shape for the packing algorithm - min_area = sum(map(lambda s: s[0] * s[1], sizes)) - width = math.ceil(math.sqrt(min_area)) + min_area = sum(map(lambda s: s[0] * s[1], sizes)) # 计算所有项目的最小面积 + width = math.ceil(math.sqrt(min_area)) # 计算最小面积的平方根,向上取整作为初始宽度 - positions = None - while not positions: - try: - positions = rpack.pack( + positions = None # 初始化位置列表为None + while not positions: # 循环直到成功打包项目 + try: # 尝试使用当前宽度打包项目 + positions = rpack.pack( sizes, max_width=width, max_height=width) except rpack.PackingImpossibleError: - width = math.ceil(width * 1.2) + width = math.ceil(width * 1.2) # 如果打包失败,将宽度增加20%,并向上取整 # 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() + item.setSelected(True) # 设置项目实例为选中状态 + item.bring_to_front() # 将项目实例 bring_to_front 到最前面