Merge pull request #1 from THE-FIFTH-TEAM111/feature/zmy/code-review

将Feature/zmy/code review分支提交到develop分支上
This commit is contained in:
ZMY051025 2025-12-17 14:52:24 +08:00 committed by GitHub
commit 6b7d2d47d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 875 additions and 872 deletions

File diff suppressed because it is too large Load diff

View file

@ -13,552 +13,555 @@
# You should have received a copy of the GNU General Public License
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
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 到最前面