mirror of
https://github.com/rbreu/beeref.git
synced 2026-03-11 08:54:28 +00:00
Merge branch 'develop' into feature/lyy/code-review
This commit is contained in:
commit
191486a8c6
10 changed files with 2357 additions and 2179 deletions
|
|
@ -13,92 +13,130 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
# 导入必要的模块
|
||||
import logging # 用于日志记录
|
||||
|
||||
from PyQt6 import QtCore
|
||||
from PyQt6 import QtCore # 导入PyQt6的QtCore模块,用于处理Qt的核心功能
|
||||
|
||||
from beeref import commands
|
||||
from beeref.fileio.errors import BeeFileIOError
|
||||
from beeref.fileio.image import load_image
|
||||
from beeref.fileio.sql import SQLiteIO, is_bee_file
|
||||
from beeref.items import BeePixmapItem
|
||||
from beeref import commands # 导入commands命令模块,用于实现撤销和重做功能
|
||||
from beeref.fileio.errors import BeeFileIOError # 导入自定义的文件IO错误类
|
||||
from beeref.fileio.image import load_image # 导入加载图片的函数
|
||||
from beeref.fileio.sql import SQLiteIO, is_bee_file # 导入SQLite数据库相关的类和函数
|
||||
from beeref.items import BeePixmapItem # 导入自定义的图片项类
|
||||
|
||||
|
||||
# 定义公共API,指定从该模块导入时可访问的名称
|
||||
__all__ = [
|
||||
'is_bee_file',
|
||||
'load_bee',
|
||||
'save_bee',
|
||||
'load_images',
|
||||
'ThreadedLoader',
|
||||
'BeeFileIOError',
|
||||
'is_bee_file', # 判断文件是否为BeeRef格式的函数
|
||||
'load_bee', # 加载BeeRef原生文件的函数
|
||||
'save_bee', # 保存BeeRef原生文件的函数
|
||||
'load_images', # 加载图像函数
|
||||
'ThreadedLoader', # 线程加载器类
|
||||
'BeeFileIOError', # 自定义的文件IO错误类
|
||||
]
|
||||
|
||||
# 创建日志记录器
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 加载BeeRef原生文件函数
|
||||
def load_bee(filename, scene, worker=None):
|
||||
"""Load BeeRef native file."""
|
||||
logger.info(f'Loading from file {filename}...')
|
||||
io = SQLiteIO(filename, scene, readonly=True, worker=worker)
|
||||
return io.read()
|
||||
"""
|
||||
参数:
|
||||
filename: 要加载的文件路径
|
||||
scene: 目标场景对象
|
||||
worker: 可选的工作线程对象,用于报告进度
|
||||
"""
|
||||
logger.info(f'Loading from file {filename}...') # 记录加载文件的信息日志
|
||||
io = SQLiteIO(filename, scene, readonly=True, worker=worker) # 创建SQLiteIO对象,用于读取文件,设置为只读模式
|
||||
return io.read() # 调用read方法读取文件内容并返回
|
||||
|
||||
|
||||
# 保存BeeRef原生文件函数
|
||||
def save_bee(filename, scene, create_new=False, worker=None):
|
||||
"""Save BeeRef native file."""
|
||||
logger.info(f'Saving to file {filename}...')
|
||||
logger.debug(f'Create new: {create_new}')
|
||||
io = SQLiteIO(filename, scene, create_new, worker=worker)
|
||||
io.write()
|
||||
logger.info('End save')
|
||||
"""
|
||||
参数:
|
||||
filename: 要保存的文件路径
|
||||
scene: 要保存的场景对象
|
||||
create_new: 是否创建新文件
|
||||
worker: 可选的工作线程对象,用于报告进度
|
||||
"""
|
||||
logger.info(f'Saving to file {filename}...') # 记录保存文件的信息日志
|
||||
logger.debug(f'Create new: {create_new}') # 记录创建新文件的调试日志
|
||||
io = SQLiteIO(filename, scene, create_new, worker=worker) # 创建SQLiteIO对象,用于写入文件,根据create_new参数确定是否创建新文件
|
||||
io.write() # 调用write方法将场景数据写入文件
|
||||
logger.info('End save') # 记录保存结束的信息日志
|
||||
|
||||
|
||||
|
||||
# 加载图像到现有场景函数
|
||||
def load_images(filenames, pos, scene, worker):
|
||||
"""Add images to existing scene."""
|
||||
|
||||
errors = []
|
||||
items = []
|
||||
worker.begin_processing.emit(len(filenames))
|
||||
"""
|
||||
参数:
|
||||
filenames: 图像文件路径列表
|
||||
pos: 图像在场景中的位置
|
||||
scene: 目标场景对象
|
||||
worker: 工作线程对象,用于报告进度和处理取消操作
|
||||
"""
|
||||
errors = [] # 存储加载失败的图像文件路径
|
||||
items = [] # 存储成功加载的图片项对象
|
||||
worker.begin_processing.emit(len(filenames)) # 发送开始处理信号,参数为图像文件数量
|
||||
# 遍历图像文件路径列表
|
||||
for i, filename in enumerate(filenames):
|
||||
logger.info(f'Loading image from file {filename}')
|
||||
img, filename = load_image(filename)
|
||||
worker.progress.emit(i)
|
||||
if img.isNull():
|
||||
logger.info(f'Could not load file {filename}')
|
||||
errors.append(filename)
|
||||
continue
|
||||
logger.info(f'Loading image from file {filename}') # 记录加载图像文件的信息日志
|
||||
img, filename = load_image(filename) # 调用load_image函数加载图像文件,返回图像对象和文件名
|
||||
worker.progress.emit(i) # 发送进度更新信号
|
||||
if img.isNull(): # 检查图像是否加载失败
|
||||
logger.info(f'Could not load file {filename}') # 记录加载失败的图像文件路径的信息日志
|
||||
errors.append(filename) # 将加载失败的图像文件路径添加到errors列表中
|
||||
continue # 继续处理下一个图像文件
|
||||
|
||||
item = BeePixmapItem(img, filename)
|
||||
item.set_pos_center(pos)
|
||||
scene.add_item_later({'item': item, 'type': 'pixmap'}, selected=True)
|
||||
items.append(item)
|
||||
item = BeePixmapItem(img, filename) # 创建BeePixmapItem对象,参数为加载的图像对象和文件名
|
||||
item.set_pos_center(pos) # 设置图片项的位置为指定的中心位置
|
||||
scene.add_item_later({'item': item, 'type': 'pixmap'}, selected=True) # 将图片项添加到场景中,参数为字典,包含项对象和类型
|
||||
items.append(item) # 将图片项对象添加到items列表中
|
||||
if worker.canceled:
|
||||
break
|
||||
break # 如果工作线程被取消,跳出循环
|
||||
# Give main thread time to process items:
|
||||
worker.msleep(10)
|
||||
worker.msleep(10) # 线程休眠10毫秒,允许主线程处理其他任务
|
||||
|
||||
# 将添加项的操作添加到撤销栈
|
||||
scene.undo_stack.push(
|
||||
commands.InsertItems(scene, items, ignore_first_redo=True))
|
||||
worker.finished.emit('', errors)
|
||||
worker.finished.emit('', errors) # 发送完成信号,传递错误列表
|
||||
|
||||
|
||||
# 用于加载和保存的专用线程类
|
||||
class ThreadedIO(QtCore.QThread):
|
||||
"""Dedicated thread for loading and saving."""
|
||||
|
||||
progress = QtCore.pyqtSignal(int)
|
||||
finished = QtCore.pyqtSignal(str, list)
|
||||
begin_processing = QtCore.pyqtSignal(int)
|
||||
user_input_required = QtCore.pyqtSignal(str)
|
||||
# 定义信号
|
||||
progress = QtCore.pyqtSignal(int) # 进度更新信号
|
||||
finished = QtCore.pyqtSignal(str, list) # 完成信号,传递文件名和和错误列表
|
||||
begin_processing = QtCore.pyqtSignal(int) # 开始处理信号,传递总处理数量
|
||||
user_input_required = QtCore.pyqtSignal(str) # 用户输入required信号,参数为提示信息
|
||||
|
||||
# 初始化线程,设置要执行的函数和参数
|
||||
def __init__(self, func, *args, **kwargs):
|
||||
super().__init__()
|
||||
self.func = func
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.kwargs['worker'] = self
|
||||
self.canceled = False
|
||||
|
||||
"""
|
||||
参数:
|
||||
func: 要在线程中执行的函数
|
||||
*args: 传递给func的位置参数
|
||||
**kwargs: 传递给func的关键字参数
|
||||
"""
|
||||
super().__init__() # 调用父类QThread构造函数
|
||||
self.func = func # 存储要执行的函数
|
||||
self.args = args # 存储要传递给函数的位置参数
|
||||
self.kwargs = kwargs # 存储要传递给函数的关键字参数
|
||||
self.kwargs['worker'] = self # 将自身作为worker参数传递给函数
|
||||
self.canceled = False # 取消标志,初始化为False
|
||||
# 线程执行函数
|
||||
def run(self):
|
||||
self.func(*self.args, **self.kwargs)
|
||||
self.func(*self.args, **self.kwargs) # 调用指定的函数执行IO操作
|
||||
|
||||
# 处理取消操作的方法
|
||||
def on_canceled(self):
|
||||
self.canceled = True
|
||||
self.canceled = True # 设置取消标志为True
|
||||
|
|
|
|||
|
|
@ -1,25 +1,30 @@
|
|||
# This file is part of BeeRef.
|
||||
# This file is part of BeeRef. # 此文件是 BeeRef 项目的一部分
|
||||
#
|
||||
# BeeRef is free software: you can redistribute it and/or modify # BeeRef 是自由软件:您可以重新分发和/或修改它
|
||||
# it under the terms of the GNU General Public License as published by # 根据 GNU 通用公共许可证的条款,由 Free Software Foundation 发布,
|
||||
# the Free Software Foundation, either version 3 of the License, or # 无论是版本 3 的许可证,还是(在您选择的情况下)任何更高版本。
|
||||
#
|
||||
# BeeRef is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BeeRef is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# BeeRef is distributed in the hope that it will be useful, # BeeRef 是分发在希望它会有用的基础上
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of # 但没有任何保修;甚至没有对适销性或特定用途适用性的暗示保证。
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # 有关更多详细信息,请参阅 GNU 通用公共许可证
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
# You should have received a copy of the GNU General Public License # 您应该已经收到了 GNU 通用公共许可证的副本
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>. # 如果没有,请参阅 <https://www.gnu.org/licenses/>
|
||||
|
||||
|
||||
# 图片加载错误的提示消息,当图片格式未知或过大时显示
|
||||
IMG_LOADING_ERROR_MSG = (
|
||||
'Unknown format or too big?\n'
|
||||
'Check Settings -> Images & Items -> Maximum Image Size')
|
||||
'Unknown format or too big?\n' # 未知格式或文件太大?
|
||||
'Check Settings -> Images & Items -> Maximum Image Size') # 请检查设置 -> 图像和项目 -> 最大图像大小
|
||||
|
||||
|
||||
# 自定义的文件IO错误类,继承自Python内置的Exception类
|
||||
class BeeFileIOError(Exception):
|
||||
# 构造函数,初始化错误消息和文件名
|
||||
def __init__(self, msg, filename):
|
||||
self.msg = msg
|
||||
self.filename = filename
|
||||
self.msg = msg # 保存错误消息
|
||||
self.filename = filename # 保存发生错误的文件名
|
||||
|
||||
|
|
|
|||
|
|
@ -13,269 +13,307 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import pathlib
|
||||
from xml.etree import ElementTree as ET
|
||||
# 导入必要的模块和类
|
||||
import base64 # 用于编码图片数据
|
||||
import logging # 用于记录日志
|
||||
import pathlib # 用于处理文件路径
|
||||
from xml.etree import ElementTree as ET # 用于创建XML文档
|
||||
|
||||
from PyQt6 import QtCore, QtGui
|
||||
from PyQt6 import QtCore, QtGui # 用于处理Qt的核心功能和图形用户界面
|
||||
|
||||
from .errors import BeeFileIOError
|
||||
from beeref import constants, widgets
|
||||
from beeref.items import BeePixmapItem
|
||||
from .errors import BeeFileIOError # 自定义异常类,用于导出时的错误处理
|
||||
from beeref import constants, widgets # 导入 BeeRef 常量和小部件
|
||||
from beeref.items import BeePixmapItem # 导入 BeeRef 图片项类
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# 创建日志记录器
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExporterRegistry(dict):
|
||||
# 导出器注册类,继承自字典类
|
||||
class ExporterRegistry(dict):
|
||||
|
||||
DEFAULT_TYPE = 0
|
||||
DEFAULT_TYPE = 0 # 默认导出器类型
|
||||
|
||||
def __getitem__(self, key):
|
||||
key = key.removeprefix('.')
|
||||
exp = self.get(key, super().__getitem__(self.DEFAULT_TYPE))
|
||||
logger.debug(f'Exporter for type {key}: {exp}')
|
||||
return exp
|
||||
def __getitem__(self, key): # 获取指定类型的导出器
|
||||
key = key.removeprefix('.') # 移除类型前缀的点号
|
||||
exp = self.get(key, super().__getitem__(self.DEFAULT_TYPE)) # 获取指定类型的导出器,若不存在则返回默认导出器
|
||||
logger.debug(f'Exporter for type {key}: {exp}') # 记录获取的导出器
|
||||
return exp # 返回获取的导出器
|
||||
|
||||
|
||||
exporter_registry = ExporterRegistry()
|
||||
# 创建导出器注册表实例
|
||||
exporter_registry = ExporterRegistry()
|
||||
|
||||
|
||||
def register_exporter(cls):
|
||||
exporter_registry[cls.TYPE] = cls
|
||||
return cls
|
||||
# 导出器注册装饰器,用于注册导出器类
|
||||
def register_exporter(cls):
|
||||
exporter_registry[cls.TYPE] = cls # 将导出器类注册到导出器注册实例中
|
||||
return cls # 返回导出器类注册装饰器,用于注册导出器类
|
||||
|
||||
|
||||
# 导出器基类,用于定义导出器的通用方法
|
||||
class ExporterBase:
|
||||
|
||||
# 发送开始处理信号
|
||||
def emit_begin_processing(self, worker, start):
|
||||
if worker:
|
||||
worker.begin_processing.emit(start)
|
||||
|
||||
if worker: # 如果有工作线程
|
||||
worker.begin_processing.emit(start) # 触发工作线程的开始处理信号
|
||||
else: # 若没有工作线程
|
||||
logger.debug(f'No worker, emit begin processing signal: {start}') # 若没有工作线程,则记录日志
|
||||
# 发送进度更新信号
|
||||
def emit_progress(self, worker, progress):
|
||||
if worker:
|
||||
worker.progress.emit(progress)
|
||||
if worker: # 若有工作线程
|
||||
worker.progress.emit(progress) # 若有工作线程,则触发工作线程的进度更新信号
|
||||
|
||||
# 发送完成信号
|
||||
def emit_finished(self, worker, filename, errors):
|
||||
filename = str(filename)
|
||||
if worker:
|
||||
worker.finished.emit(filename, errors)
|
||||
filename = str(filename) # 确保文件名是字符串类型
|
||||
if worker: # 若有工作线程
|
||||
worker.finished.emit(filename, errors) # 若有工作线程,则触发工作线程的完成信号
|
||||
|
||||
# 发送需要用户输入信号
|
||||
def emit_user_input_required(self, worker, msg):
|
||||
if worker:
|
||||
worker.user_input_required.emit(msg)
|
||||
if worker: # 若有工作线程
|
||||
worker.user_input_required.emit(msg) # 若有工作线程,则触发工作线程的用户输入要求信号
|
||||
|
||||
# 处理导出错误
|
||||
def handle_export_error(self, filename, error, worker):
|
||||
filename = str(filename)
|
||||
logger.debug(f'Export failed: {error}')
|
||||
if worker:
|
||||
worker.finished.emit(filename, [str(error)])
|
||||
return
|
||||
else:
|
||||
e = error if isinstance(error, Exception) else None
|
||||
raise BeeFileIOError(msg=str(error), filename=filename) from e
|
||||
filename = str(filename) # 确保文件名是字符串类型
|
||||
logger.debug(f'Export failed: {error}') # 记录导出失败的日志
|
||||
if worker: # 若有工作线程
|
||||
worker.finished.emit(filename, [str(error)]) # 若有工作线程,则触发工作线程的完成信号,包含错误信息
|
||||
return # 若有工作线程,则返回错误信息
|
||||
else: # 若没有工作线程
|
||||
e = error if isinstance(error, Exception) else None # 若错误不是异常类型,则设为None
|
||||
raise BeeFileIOError(msg=str(error), filename=filename) from e # 抛出自定义异常,包含错误信息和文件名
|
||||
|
||||
|
||||
# 场景导出器基类,用于定义场景导出器为单个图像
|
||||
class SceneExporterBase(ExporterBase):
|
||||
"""For exporting the scene to a single image."""
|
||||
"""For exporting the scene to a single image.""" # 场景导出器基类,用于定义场景导出器的通用方法
|
||||
|
||||
# 场景导出器基类的初始化方法
|
||||
def __init__(self, scene):
|
||||
self.scene = scene
|
||||
self.scene.cancel_active_modes()
|
||||
self.scene.deselect_all_items()
|
||||
self.scene = scene # 场景导出器基类的初始化方法,接收场景作为参数
|
||||
self.scene.cancel_active_modes() # 取消场景中所有活动模式
|
||||
self.scene.deselect_all_items() # 取消场景中所有项的选择
|
||||
# Selection outlines/handles will be rendered to the exported
|
||||
# image, so deselect first. (Alternatively, pass an attribute
|
||||
# to paint functions to not paint them?)
|
||||
rect = self.scene.itemsBoundingRect()
|
||||
logger.trace(f'Items bounding rect: {rect}')
|
||||
size = QtCore.QSize(int(rect.width()), int(rect.height()))
|
||||
logger.trace(f'Export size without margins: {size}')
|
||||
self.margin = max(size.width(), size.height()) * 0.03
|
||||
self.default_size = size.grownBy(
|
||||
QtCore.QMargins(*([int(self.margin)] * 4)))
|
||||
logger.debug(f'Default export margin: {self.margin}')
|
||||
logger.debug(f'Default export size with margins: {self.default_size}')
|
||||
rect = self.scene.itemsBoundingRect() # 获取场景中所有项的边界矩形
|
||||
logger.trace(f'Items bounding rect: {rect}') # 记录场景中所有项的边界矩形
|
||||
size = QtCore.QSize(int(rect.width()), int(rect.height())) # 计算导出图像的大小,取项边界矩形的宽度和高度
|
||||
logger.trace(f'Export size without margins: {size}') # 记录导出图像的大小,不包含边距
|
||||
self.margin = max(size.width(), size.height()) * 0.03 # 计算导出图像的边距,取宽度和高度的较大值乘以0.03
|
||||
self.default_size = size.grownBy( # 计算导出图像的大小,包含边距
|
||||
QtCore.QMargins(*([int(self.margin)] * 4))) # 计算导出图像的大小,包含边距
|
||||
logger.debug(f'Default export margin: {self.margin}') # 记录导出图像的边距
|
||||
logger.debug(f'Default export size with margins: {self.default_size}')# 记录导出图像的大小,包含边距
|
||||
|
||||
|
||||
# 注册场景到像素图导出器
|
||||
@register_exporter
|
||||
class SceneToPixmapExporter(SceneExporterBase):
|
||||
class SceneToPixmapExporter(SceneExporterBase): # 场景导出器基类,用于定义场景导出器的通用方法
|
||||
|
||||
TYPE = ExporterRegistry.DEFAULT_TYPE
|
||||
TYPE = ExporterRegistry.DEFAULT_TYPE # 设置为默认导出类型
|
||||
|
||||
# 场景导出器基类的用户输入方法,用于获取用户输入的导出大小
|
||||
def get_user_input(self, parent):
|
||||
"""Ask user for final export size."""
|
||||
"""Ask user for final export size.""" # 场景导出器基类的用户输入方法,用于获取用户输入的导出大小
|
||||
|
||||
dialog = widgets.SceneToPixmapExporterDialog(
|
||||
parent=parent,
|
||||
default_size=self.default_size,
|
||||
# 创建导出对话框,让用户输入最终导出尺寸
|
||||
dialog = widgets.SceneToPixmapExporterDialog( # 创建场景导出器对话框实例
|
||||
parent=parent, # 场景导出器对话框的父窗口
|
||||
default_size=self.default_size, # 场景导出器对话框的默认导出大小
|
||||
)
|
||||
if dialog.exec():
|
||||
size = dialog.value()
|
||||
logger.debug(f'Got export size {size}')
|
||||
self.size = size
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
if dialog.exec(): # 若场景导出器对话框执行成功
|
||||
size = dialog.value() # 获取场景导出器对话框中用户输入的导出大小
|
||||
logger.debug(f'Got export size {size}') # 记录用户输入的导出大小
|
||||
self.size = size # 将用户输入的导出大小赋值给场景导出器基类的属性
|
||||
return True # 返回True表示用户输入成功
|
||||
else: # 若场景导出器对话框执行失败
|
||||
return False # 返回False表示用户输入失败
|
||||
|
||||
# 场景导出器基类的渲染方法,用于将场景渲染为图像
|
||||
def render_to_image(self):
|
||||
logger.debug(f'Final export size: {self.size}')
|
||||
margin = self.margin * self.size.width() / self.default_size.width()
|
||||
logger.debug(f'Final export margin: {margin}')
|
||||
logger.debug(f'Final export size: {self.size}') # 记录最终导出图像的大小
|
||||
# 按比例调整边距
|
||||
margin = self.margin * self.size.width() / self.default_size.width() # 计算最终导出图像的边距,取宽度和高度的较大值乘以0.03
|
||||
logger.debug(f'Final export margin: {margin}') # 记录最终导出图像的边距
|
||||
|
||||
image = QtGui.QImage(self.size, QtGui.QImage.Format.Format_RGB32)
|
||||
image.fill(QtGui.QColor(*constants.COLORS['Scene:Canvas']))
|
||||
painter = QtGui.QPainter(image)
|
||||
image = QtGui.QImage(self.size, QtGui.QImage.Format.Format_RGB32) # 创建导出图像,大小为最终导出大小,格式为RGB32
|
||||
image.fill(QtGui.QColor(*constants.COLORS['Scene:Canvas'])) # 用画布颜色作为导出图像的背景颜色
|
||||
painter = QtGui.QPainter(image) # 创建画家对象
|
||||
# 定义目标矩形
|
||||
target_rect = QtCore.QRectF(
|
||||
margin,
|
||||
margin,
|
||||
self.size.width() - 2 * margin,
|
||||
self.size.height() - 2 * margin)
|
||||
logger.trace(f'Final export target_rect: {target_rect}')
|
||||
self.scene.render(painter,
|
||||
source=self.scene.itemsBoundingRect(),
|
||||
target=target_rect)
|
||||
painter.end()
|
||||
return image
|
||||
margin, # 取导出图像的边距作为偏移量
|
||||
margin, # 取导出图像的边距作为偏移量
|
||||
self.size.width() - 2 * margin, # 取导出图像的宽度减去2倍边距作为矩形宽度
|
||||
self.size.height() - 2 * margin) # 取导出图像的高度减去2倍边距作为矩形高度
|
||||
logger.trace(f'Final export target_rect: {target_rect}') # 记录最终导出图像的目标矩形
|
||||
self.scene.render(painter, # 渲染场景到导出图像的绘图器
|
||||
source=self.scene.itemsBoundingRect(), # 获取场景中所有项的边界矩形
|
||||
target=target_rect) # 将场景渲染到最终导出图像的目标矩形
|
||||
painter.end() # 结束导出图像的绘图器
|
||||
return image # 返回导出图像
|
||||
|
||||
|
||||
# 场景导出器基类的导出方法,用于将场景导出为图像文件
|
||||
def export(self, filename, worker=None):
|
||||
logger.debug(f'Exporting scene to {filename}')
|
||||
self.emit_begin_processing(worker, 1)
|
||||
image = self.render_to_image()
|
||||
logger.debug(f'Exporting scene to {filename}') # 记录导出场景的文件名
|
||||
self.emit_begin_processing(worker, 1) # 发送导出开始信号,参数为导出器实例和导出任务数量
|
||||
image = self.render_to_image() # 调用场景导出器基类的渲染方法,渲染场景到图像
|
||||
|
||||
# 若导出器实例存在且取消导出标志为True
|
||||
if worker and worker.canceled:
|
||||
logger.debug('Export canceled')
|
||||
self.emit_finished(worker, filename, [])
|
||||
return
|
||||
logger.debug('Export canceled') # 记录导出被取消的信息
|
||||
self.emit_finished(worker, filename, []) # 发送导出完成信号,参数为导出器实例、导出文件名和空列表
|
||||
return # 若导出器实例存在且取消导出标志为True,则直接返回,不导出图像
|
||||
|
||||
# 若导出图像保存失败
|
||||
if not image.save(filename, quality=90):
|
||||
self.handle_export_error(filename, 'Error writing file', worker)
|
||||
return
|
||||
|
||||
logger.debug('Export finished')
|
||||
self.emit_progress(worker, 1)
|
||||
self.emit_finished(worker, filename, [])
|
||||
self.handle_export_error(filename, 'Error writing file', worker) # 处理导出错误,参数为导出文件名、错误信息和导出器实例
|
||||
return # 返回,不继续导出
|
||||
|
||||
logger.debug('Export finished') # 记录导出完成的信息
|
||||
self.emit_progress(worker, 1) # 发送导出进度信号,参数为导出器实例和导出进度值1
|
||||
self.emit_finished(worker, filename, []) # 发送导出完成信号,参数为导出器实例、导出文件名和空列表
|
||||
|
||||
# 注册场景到SVG,
|
||||
@register_exporter
|
||||
class SceneToSVGExporter(SceneExporterBase):
|
||||
|
||||
TYPE = 'svg'
|
||||
TYPE = 'svg' # 场景导出器基类的导出类型属性,值为'svg',表示导出为SVG格式
|
||||
|
||||
# 场景导出器基类的用户输入方法,用于获取用户输入的导出大小
|
||||
def get_user_input(self, parent):
|
||||
self.size = self.default_size
|
||||
return True
|
||||
self.size = self.default_size # 将场景导出器基类的默认导出大小赋值给场景导出器基类的属性
|
||||
return True # 返回True表示用户输入成功
|
||||
|
||||
# 场景导出器基类的文本样式方法,用于获取项的字体样式
|
||||
def _get_textstyles(self, item):
|
||||
# 字体样式映射字典,将QFont的字体样式枚举值映射为SVG的字体样式字符串
|
||||
fontstylemap = {
|
||||
QtGui.QFont.Style.StyleNormal: 'normal',
|
||||
QtGui.QFont.Style.StyleItalic: 'italic',
|
||||
QtGui.QFont.Style.StyleOblique: 'oblique',
|
||||
QtGui.QFont.Style.StyleNormal: 'normal', # 将QFont的字体样式枚举值StyleNormal映射为SVG的字体样式字符串normal
|
||||
QtGui.QFont.Style.StyleItalic: 'italic', # 将QFont的字体样式枚举值StyleItalic映射为SVG的字体样式字符串italic
|
||||
QtGui.QFont.Style.StyleOblique: 'oblique', # 将QFont的字体样式枚举值StyleOblique映射为SVG的字体样式字符串oblique
|
||||
}
|
||||
|
||||
font = item.font()
|
||||
fontsize = font.pointSize() * item.scale()
|
||||
families = ', '.join(font.families())
|
||||
fontstyle = fontstylemap[font.style()]
|
||||
font = item.font() # 获取项的字体
|
||||
fontsize = font.pointSize() * item.scale() # 计算项的字体大小,取字体点大小乘以项的缩放比例
|
||||
families = ', '.join(font.families()) # 取字体的所有字体家族,用逗号分隔
|
||||
fontstyle = fontstylemap[font.style()] # 取字体的字体样式,根据字体样式映射表映射为SVG的字体样式字符串
|
||||
|
||||
# 返回CSS样式元组
|
||||
return ('white-space:pre',
|
||||
f'font-size:{fontsize}pt',
|
||||
f'font-family:{families}',
|
||||
f'font-weight:{font.weight()}',
|
||||
f'font-stretch:{font.stretch()}',
|
||||
f'font-style:{fontstyle}')
|
||||
f'font-size:{fontsize}pt', # 取项的字体大小,取字体点大小乘以项的缩放比例
|
||||
f'font-family:{families}', # 取项的字体家族,用逗号分隔
|
||||
f'font-weight:{font.weight()}', # 取项的字体重量
|
||||
f'font-stretch:{font.stretch()}', # 取项的字体拉伸
|
||||
f'font-style:{fontstyle}') # 取项的字体样式,根据字体样式映射表映射为SVG的字体样式字符串
|
||||
|
||||
# 场景导出器基类的导出方法,用于将场景导出为SVG文件
|
||||
def render_to_svg(self, worker=None):
|
||||
svg = ET.Element(
|
||||
svg = ET.Element( # 创建SVG元素,参数为元素标签名svg
|
||||
'svg',
|
||||
attrib={'width': str(self.size.width()),
|
||||
'height': str(self.size.height()),
|
||||
'xmlns': 'http://www.w3.org/2000/svg',
|
||||
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
|
||||
attrib={'width': str(self.size.width()), # 设置SVG元素的宽度属性,值为场景导出器基类的属性size的宽度
|
||||
'height': str(self.size.height()), # 设置SVG元素的高度属性,值为场景导出器基类的属性size的高度
|
||||
'xmlns': 'http://www.w3.org/2000/svg', # 设置SVG元素的XML命名空间属性,值为http://www.w3.org/2000/svg
|
||||
'xmlns:xlink': 'http://www.w3.org/1999/xlink', # 设置SVG元素的XML链接命名空间属性,值为http://www.w3.org/1999/xlink
|
||||
})
|
||||
|
||||
rect = self.scene.itemsBoundingRect()
|
||||
offset = rect.topLeft() - QtCore.QPointF(self.margin, self.margin)
|
||||
rect = self.scene.itemsBoundingRect() # 获取场景中所有项的边界矩形
|
||||
offset = rect.topLeft() - QtCore.QPointF(self.margin, self.margin)# 计算项的位置偏移量,取场景中所有项的边界矩形的左上角坐标减去导出器基类的属性margin
|
||||
|
||||
# 按z值排序项目并遍历
|
||||
for i, item in enumerate(sorted(self.scene.items(),
|
||||
key=lambda x: x.zValue())):
|
||||
# z order in SVG specified via the order of elements in the tree
|
||||
pos = item.pos() - offset
|
||||
anchor = pos
|
||||
pos = item.pos() - offset # 计算项的位置,取项的位置减去项的位置偏移量
|
||||
anchor = pos # 设置项的变换锚点,取项的位置
|
||||
|
||||
if item.TYPE == 'text':
|
||||
styles = self._get_textstyles(item)
|
||||
element = ET.Element(
|
||||
'text',
|
||||
attrib={'style': ';'.join(styles),
|
||||
'dominant-baseline': 'hanging'})
|
||||
element.text = item.toPlainText()
|
||||
if item.TYPE == 'pixmap':
|
||||
width = item.width * item.scale()
|
||||
height = item.height * item.scale()
|
||||
pixmap, imgformat = item.pixmap_to_bytes(
|
||||
apply_grayscale=True,
|
||||
apply_crop=True)
|
||||
pixmap = base64.b64encode(pixmap).decode('ascii')
|
||||
element = ET.Element(
|
||||
'image',
|
||||
attrib={
|
||||
'xlink:href':
|
||||
f'data:image/{imgformat};base64,{pixmap}',
|
||||
'width': str(width),
|
||||
'height': str(height),
|
||||
'image-rendering': ('crisp-edges' if item.scale() > 2
|
||||
# 处理文本项
|
||||
if item.TYPE == 'text': # 若项的类型为文本项
|
||||
styles = self._get_textstyles(item) # 获取项的字体样式
|
||||
element = ET.Element( # 创建文本元素,参数为元素标签名text
|
||||
'text', # 设置文本元素的标签名,值为text
|
||||
attrib={'style': ';'.join(styles), # 将样式列表用分号连接成CSS样式字符串,设置为元素的style属性
|
||||
'dominant-baseline': 'hanging'}) # 设置文本元素的属性,值为hanging,指定文本的基线位置为悬挂基线
|
||||
element.text = item.toPlainText() # 设置文本元素的文本内容,值为项的纯文本内容
|
||||
# 处理图片项
|
||||
if item.TYPE == 'pixmap': # 若项的类型为图片项
|
||||
width = item.width * item.scale() # 计算项的宽度,取项的宽度乘以项的缩放比例
|
||||
height = item.height * item.scale() # 计算项的高度,取项的高度乘以项的缩放比例
|
||||
pixmap, imgformat = item.pixmap_to_bytes( # 将项的像素图转换为字节数组,参数为项的像素图,是否应用灰度化,是否应用裁剪
|
||||
apply_grayscale=True, # 是否应用灰度化,值为True
|
||||
apply_crop=True) # 是否应用裁剪,值为True
|
||||
pixmap = base64.b64encode(pixmap).decode('ascii')# 将项的像素图字节数组转换为Base64编码的字符串
|
||||
element = ET.Element( # 创建图片元素,参数为元素标签名image
|
||||
'image', # 设置图片元素的标签名,值为image
|
||||
attrib={ # 设置图片元素的属性,值为项的像素图Base64编码的字符串
|
||||
'xlink:href': # 设置图片元素的XML链接属性,值为项的像素图Base64编码的字符串
|
||||
f'data:image/{imgformat};base64,{pixmap}',# 设置图片元素的XML链接属性,值为项的像素图Base64编码的字符串
|
||||
'width': str(width), # 设置图片元素的宽度属性,值为项的宽度乘以项的缩放比例
|
||||
'height': str(height), # 设置图片元素的高度属性,值为项的高度乘以项的缩放比例
|
||||
'image-rendering': ('crisp-edges' if item.scale() > 2# 设置图片元素的渲染属性,值为crisp-edges或optimizeQuality,根据项的缩放比例判断
|
||||
else 'optimizeQuality')})
|
||||
pos = pos + item.crop.topLeft()
|
||||
pos = pos + item.crop.topLeft() # 计算项的位置,取项的位置加上项的裁剪区域的左上角坐标
|
||||
|
||||
transforms = []
|
||||
if item.flip() == -1:
|
||||
#设置变换属性
|
||||
transforms = [] # 创建变换列表,用于存储项的变换操作
|
||||
if item.flip() == -1: # 若项的翻转方向为-1
|
||||
# The following is not recognised by Inkscape and not an
|
||||
# official standard:
|
||||
# element.set('transform-origin', f'{anchor.x()} {anchor.y()}')
|
||||
# Thus we need to fix the origin manually
|
||||
transforms.append(f'translate({anchor.x()} {anchor.y()})')
|
||||
transforms.append(f'scale({item.flip()} 1)')
|
||||
transforms.append(f'translate(-{anchor.x()} -{anchor.y()})')
|
||||
transforms.append(f'translate({anchor.x()} {anchor.y()})') # 若项的翻转方向为-1,添加变换操作,将项的锚点平移到项的位置
|
||||
transforms.append(f'scale({item.flip()} 1)') # 添加变换操作,将项的像素图水平翻转
|
||||
transforms.append(f'translate(-{anchor.x()} -{anchor.y()})') # 添加变换操作,将项的锚点平移回项的位置
|
||||
|
||||
#添加变换操作,将项的像素图旋转项的旋转角度
|
||||
transforms.append(
|
||||
f'rotate({item.rotation()} {anchor.x()} {anchor.y()})')
|
||||
f'rotate({item.rotation()} {anchor.x()} {anchor.y()})') #围绕项的锚点旋转项的旋转角度
|
||||
|
||||
element.set('transform', ' '.join(transforms))
|
||||
element.set('x', str(pos.x()))
|
||||
element.set('y', str(pos.y()))
|
||||
element.set('opacity', str(item.opacity()))
|
||||
#应用变换
|
||||
element.set('transform', ' '.join(transforms)) # 设置元素的变换属性,值为变换列表用空格连接成的字符串
|
||||
element.set('x', str(pos.x())) # 设置元素的x属性,值为项的位置的x坐标
|
||||
element.set('y', str(pos.y())) # 设置元素的y属性,值为项的位置的y坐标
|
||||
element.set('opacity', str(item.opacity())) # 设置元素的透明度属性,值为项的透明度
|
||||
|
||||
svg.append(element)
|
||||
self.emit_progress(worker, i)
|
||||
if worker and worker.canceled:
|
||||
return
|
||||
svg.append(element) # 将元素添加到SVG根元素中
|
||||
self.emit_progress(worker, i) # 发送进度信号,参数为项的索引i
|
||||
if worker and worker.canceled: # 若导出任务被取消
|
||||
return # 若导出任务被取消,直接返回
|
||||
|
||||
return svg
|
||||
return svg # 返回SVG根元素
|
||||
|
||||
#导出场景到SVG文件
|
||||
def export(self, filename, worker=None):
|
||||
logger.debug(f'Exporting scene to {filename}')
|
||||
self.emit_begin_processing(worker, len(self.scene.items()))
|
||||
svg = self.render_to_svg(worker)
|
||||
logger.debug(f'Exporting scene to {filename}') # 调试日志,输出导出场景到SVG文件的文件名
|
||||
self.emit_begin_processing(worker, len(self.scene.items())) # 发送开始处理信号,设置总进度为项的数量
|
||||
svg = self.render_to_svg(worker) # 渲染场景到SVG元素,参数为导出任务的工作线程
|
||||
|
||||
if worker and worker.canceled:
|
||||
logger.debug('Export canceled')
|
||||
worker.finished.emit(filename, [])
|
||||
return
|
||||
if worker and worker.canceled: # 若导出任务被取消
|
||||
logger.debug('Export canceled') # 调试日志,输出导出任务被取消
|
||||
worker.finished.emit(filename, []) # 发送导出任务完成信号,参数为文件名和空列表
|
||||
return # 若导出任务被取消,直接返回
|
||||
|
||||
tree = ET.ElementTree(svg)
|
||||
ET.indent(tree, space=' ')
|
||||
tree = ET.ElementTree(svg) # 创建XML元素树,参数为SVG根元素
|
||||
ET.indent(tree, space=' ') # 缩进XML元素树,参数为空格字符串
|
||||
|
||||
try:
|
||||
with open(filename, 'w') as f:
|
||||
tree.write(f, encoding='unicode', xml_declaration=True)
|
||||
except OSError as e:
|
||||
self.handle_export_error(filename, e, worker)
|
||||
return
|
||||
with open(filename, 'w') as f: # 以UTF-8编码打开文件,参数为文件名和写入模式
|
||||
tree.write(f, encoding='unicode', xml_declaration=True) # 将XML元素树写入文件,参数为文件对象,编码为unicode,XML声明为True
|
||||
except OSError as e: # 若导出任务发生OS错误
|
||||
self.handle_export_error(filename, e, worker) # 处理导出任务错误,参数为文件名、错误对象和导出任务的工作线程
|
||||
return # 若导出任务发生OS错误,直接返回
|
||||
|
||||
logger.debug('Export finished')
|
||||
self.emit_finished(worker, filename, [])
|
||||
logger.debug('Export finished') # 调试日志,输出导出任务完成
|
||||
self.emit_finished(worker, filename, []) # 发送导出任务完成信号,参数为导出任务的工作线程、文件名和空列表
|
||||
|
||||
|
||||
# 图像到目录导出器
|
||||
class ImagesToDirectoryExporter(ExporterBase):
|
||||
"""Export all images to a folder.
|
||||
|
||||
|
|
@ -283,75 +321,82 @@ class ImagesToDirectoryExporter(ExporterBase):
|
|||
not auto-detected by file extension.
|
||||
"""
|
||||
|
||||
#初始化图像到目录导出器
|
||||
def __init__(self, scene, dirname):
|
||||
self.scene = scene
|
||||
self.dirname = dirname
|
||||
self.items = list(self.scene.items_by_type(BeePixmapItem.TYPE))
|
||||
self.max_save_id = 0
|
||||
for item in self.items:
|
||||
if item.save_id:
|
||||
self.max_save_id = max(self.max_save_id, item.save_id)
|
||||
self.num_total = len(self.items)
|
||||
self.start_from = 0
|
||||
self.handle_existing = None
|
||||
self.scene = scene # 场景对象
|
||||
self.dirname = dirname # 目录名
|
||||
self.items = list(self.scene.items_by_type(BeePixmapItem.TYPE)) # 获取所有像素图项的列表
|
||||
self.max_save_id = 0 # 最大保存ID
|
||||
#查找当前最大保存ID
|
||||
for item in self.items: # 遍历所有像素图项
|
||||
if item.save_id: # 若项有保存ID
|
||||
self.max_save_id = max(self.max_save_id, item.save_id) # 更新最大保存ID为项的保存ID和当前最大保存ID中的较大值
|
||||
self.num_total = len(self.items) # 总项数
|
||||
self.start_from = 0 # 起始索引
|
||||
self.handle_existing = None # 处理已存在文件的方式,初始值为None
|
||||
|
||||
#导出场景中的图像到目录
|
||||
def export(self, worker=None):
|
||||
logger.debug(f'Exporting images to {self.dirname}')
|
||||
logger.debug(f'Starting at {self.start_from}')
|
||||
|
||||
self.emit_begin_processing(worker, self.num_total)
|
||||
self.emit_progress(worker, self.start_from)
|
||||
logger.debug(f'Exporting images to {self.dirname}') # 调试日志,输出导出图像到目录的目录名
|
||||
logger.debug(f'Starting at {self.start_from}') # 调试日志,输出导出任务的起始索引
|
||||
|
||||
self.emit_begin_processing(worker, self.num_total) # 发送开始处理信号,参数为项的数量
|
||||
self.emit_progress(worker, self.start_from) # 发送当前进度信号,参数为起始索引
|
||||
|
||||
#遍历项目
|
||||
for i, item in enumerate(
|
||||
self.items[self.start_from:], start=self.start_from):
|
||||
if worker and worker.canceled:
|
||||
logger.debug('Export canceled')
|
||||
worker.finished.emit(self.dirname, [])
|
||||
return
|
||||
self.items[self.start_from:], start=self.start_from): # 遍历从起始索引开始的项
|
||||
self.emit_progress(worker, i) # 发送当前进度信号,参数为项的索引i
|
||||
if worker and worker.canceled: # 若导出任务被取消
|
||||
logger.debug('Export canceled') # 调试日志,输出导出任务被取消
|
||||
worker.finished.emit(self.dirname, []) # 发送导出任务完成信号,参数为目录名和空列表
|
||||
return # 若导出任务被取消,直接返回
|
||||
|
||||
pixmap, imgformat = item.pixmap_to_bytes()
|
||||
pixmap, imgformat = item.pixmap_to_bytes() # 获取项的像素图和图像格式
|
||||
|
||||
if item.save_id:
|
||||
filename = item.get_filename_for_export(imgformat)
|
||||
else:
|
||||
self.max_save_id += 1
|
||||
save_id = self.max_save_id
|
||||
filename = item.get_filename_for_export(imgformat, save_id)
|
||||
#确定文件名
|
||||
if item.save_id: # 若项有保存ID
|
||||
filename = item.get_filename_for_export(imgformat) # 获取项的导出文件名,参数为图像格式
|
||||
else: # 若项没有保存ID
|
||||
self.max_save_id += 1 # 最大保存ID加1
|
||||
save_id = self.max_save_id # 保存ID为最大保存ID
|
||||
filename = item.get_filename_for_export(imgformat, save_id) # 获取项的导出文件名,参数为图像格式和保存ID
|
||||
|
||||
try:
|
||||
path = pathlib.Path(self.dirname) / filename
|
||||
path_exists = path.exists()
|
||||
except OSError as e:
|
||||
self.handle_export_error(self.dirname, e, worker)
|
||||
return
|
||||
path = pathlib.Path(self.dirname) / filename # 构建文件路径,参数为目录名和文件名
|
||||
path_exists = path.exists() # 检查文件路径是否存在
|
||||
except OSError as e: # 若导出任务发生OS错误
|
||||
self.handle_export_error(self.dirname, e, worker) # 处理导出任务错误,参数为目录名、错误对象和导出任务的工作线程
|
||||
return # 直接返回
|
||||
|
||||
if path_exists:
|
||||
logger.debug(f'File already exists: {path}')
|
||||
if self.handle_existing is None:
|
||||
self.start_from = i
|
||||
self.emit_user_input_required(worker, str(path))
|
||||
return
|
||||
else:
|
||||
if self.handle_existing == 'skip':
|
||||
self.handle_existing = None
|
||||
logger.debug('Skipping file')
|
||||
continue
|
||||
elif self.handle_existing == 'skip_all':
|
||||
logger.debug('Skipping file')
|
||||
continue
|
||||
elif self.handle_existing == 'overwrite':
|
||||
self.handle_existing = None
|
||||
logger.debug('Overwrite file')
|
||||
elif self.handle_existing == 'overwrite_all':
|
||||
logger.debug('Overwrite file')
|
||||
#处理文件已存在的情况
|
||||
if path_exists: # 若文件路径已存在
|
||||
logger.debug(f'File already exists: {path}') # 调试日志,输出文件已存在的路径
|
||||
if self.handle_existing is None: # 若处理已存在文件的方式为None
|
||||
self.start_from = i # 更新起始索引为当前项的索引,记录当前位置
|
||||
self.emit_user_input_required(worker, str(path)) # 发送用户输入要求信号,请求用户输入
|
||||
return # 若导出任务发生OS错误,直接返回
|
||||
else: # 若处理已存在文件的方式不为None
|
||||
if self.handle_existing == 'skip': # 若处理已存在文件的方式为跳过
|
||||
self.handle_existing = None # 处理已存在文件的方式设为None,记录当前位置
|
||||
logger.debug('Skipping file') # 调试日志,输出跳过文件
|
||||
continue # 若处理已存在文件的方式为跳过,继续下一项
|
||||
elif self.handle_existing == 'skip_all': # 若处理已存在文件的方式为跳过所有
|
||||
logger.debug('Skipping file') # 调试日志,输出跳过文件
|
||||
continue # 若处理已存在文件的方式为跳过所有,继续下一项
|
||||
elif self.handle_existing == 'overwrite': # 若处理已存在文件的方式为覆盖
|
||||
self.handle_existing = None # 处理已存在文件的方式设为None,记录当前位置
|
||||
logger.debug('Overwrite file') # 调试日志,输出覆盖文件
|
||||
elif self.handle_existing == 'overwrite_all':# 若处理已存在文件的方式为覆盖所有
|
||||
logger.debug('Overwrite file') # 调试日志,输出覆盖文件
|
||||
|
||||
logger.debug(f'Writing file: {path}')
|
||||
logger.debug(f'Writing file: {path}') # 调试日志,输出写入文件的路径
|
||||
try:
|
||||
path.write_bytes(pixmap)
|
||||
except OSError as e:
|
||||
self.handle_export_error(path, e, worker)
|
||||
return
|
||||
path.write_bytes(pixmap) # 写入像素图到文件路径,参数为像素图
|
||||
except OSError as e: # 若写入像素图到文件路径发生OS错误
|
||||
self.handle_export_error(path, e, worker) # 处理导出任务错误,参数为文件路径、错误对象和导出任务的工作线程
|
||||
return # 若导出任务发生OS错误,直接返回
|
||||
|
||||
self.emit_progress(worker, i)
|
||||
self.emit_progress(worker, i) # 发送进度信号,参数为当前项的索引,记录当前位置
|
||||
|
||||
self.emit_finished(worker, self.dirname, [])
|
||||
self.emit_finished(worker, self.dirname, []) # 发送导出任务完成信号,参数为目录名和空列表
|
||||
|
|
|
|||
|
|
@ -13,101 +13,118 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import os.path
|
||||
import tempfile
|
||||
from urllib.error import URLError
|
||||
from urllib import parse, request
|
||||
# 导入必要的模块
|
||||
import logging # 导入日志模块,用于记录运行时信息和错误
|
||||
import os.path # 导入os.path模块,用于处理文件路径
|
||||
import tempfile # 导入tempfile模块,用于创建临时文件
|
||||
from urllib.error import URLError # 导入URLError类,用于处理URL错误
|
||||
from urllib import parse, request # 导入urllib模块中的parse和request子模块,用于处理URL解析和请求
|
||||
|
||||
from PyQt6 import QtGui
|
||||
|
||||
import exif
|
||||
from lxml import etree
|
||||
import plum
|
||||
from PyQt6 import QtGui # 导入QtGui模块,用于处理图像和绘图
|
||||
|
||||
import exif # 导入exif模块,用于处理图像的EXIF数据
|
||||
from lxml import etree # 导入lxml模块中的etree子模块,用于解析HTML
|
||||
import plum # 导入plum模块,用于处理图像的二进制数据
|
||||
|
||||
# 创建日志记录器实例
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 定义函数exif_rotated_image,用于根据图像的EXIF数据进行旋转和镜像变换
|
||||
def exif_rotated_image(path=None):
|
||||
"""Returns a QImage that is transformed according to the source's
|
||||
orientation EXIF data.
|
||||
"""
|
||||
|
||||
# 创建QImage对象
|
||||
img = QtGui.QImage(path)
|
||||
# 检查图像是否为空(无法加载)
|
||||
if img.isNull():
|
||||
return img
|
||||
return img # 如果图像为空,直接返回
|
||||
|
||||
# 尝试读取EXIF数据
|
||||
with open(path, 'rb') as f:
|
||||
try:
|
||||
exifimg = exif.Image(f)
|
||||
except (plum.exceptions.UnpackError, NotImplementedError):
|
||||
logger.exception(f'Exif parser failed on image: {path}')
|
||||
return img
|
||||
exifimg = exif.Image(f) # 从文件对象创建EXIF图像对象
|
||||
except (plum.exceptions.UnpackError, NotImplementedError): # 处理EXIF解析错误
|
||||
logger.exception(f'Exif parser failed on image: {path}') # 记录EXIF解析错误日志
|
||||
return img # 如果EXIF解析错误,直接返回原始图像
|
||||
|
||||
try:
|
||||
if 'orientation' in exifimg.list_all():
|
||||
orientation = exifimg.orientation
|
||||
if 'orientation' in exifimg.list_all(): # 检查是否存在方向EXIF信息
|
||||
orientation = exifimg.orientation # 获取方向EXIF信息
|
||||
else:
|
||||
return img
|
||||
except (NotImplementedError, ValueError):
|
||||
logger.exception(f'Exif failed reading orientation of image: {path}')
|
||||
return img
|
||||
return img # 如果不存在方向EXIF信息,直接返回原始图像
|
||||
except (NotImplementedError, ValueError): # 处理其他EXIF读取错误
|
||||
logger.exception(f'Exif failed reading orientation of image: {path}') # 记录EXIF读取错误日志
|
||||
return img # 如果EXIF读取错误,直接返回原始图像
|
||||
|
||||
# 创建变换对象
|
||||
transform = QtGui.QTransform()
|
||||
|
||||
if orientation == exif.Orientation.TOP_RIGHT:
|
||||
# 根据EXIF方向值进行相应的变换
|
||||
if orientation == exif.Orientation.TOP_RIGHT: # 方向值2:方向为顶部右侧,需要水平镜像
|
||||
return img.mirrored(horizontal=True, vertical=False)
|
||||
if orientation == exif.Orientation.BOTTOM_RIGHT:
|
||||
if orientation == exif.Orientation.BOTTOM_RIGHT: # 方向值3:方向为底部右侧,需要旋转180度
|
||||
transform.rotate(180)
|
||||
return img.transformed(transform)
|
||||
if orientation == exif.Orientation.BOTTOM_LEFT:
|
||||
if orientation == exif.Orientation.BOTTOM_LEFT: # 方向值4:方向为底部左侧,需要垂直镜像
|
||||
return img.mirrored(horizontal=False, vertical=True)
|
||||
if orientation == exif.Orientation.LEFT_TOP:
|
||||
if orientation == exif.Orientation.LEFT_TOP: # 方向值5:方向为左侧顶部,需要顺时针旋转90度
|
||||
transform.rotate(90)
|
||||
return img.transformed(transform).mirrored(
|
||||
horizontal=True, vertical=False)
|
||||
if orientation == exif.Orientation.RIGHT_TOP:
|
||||
if orientation == exif.Orientation.RIGHT_TOP: # 方向值6:方向为右侧顶部,需要逆时针旋转90度
|
||||
transform.rotate(90)
|
||||
return img.transformed(transform)
|
||||
if orientation == exif.Orientation.RIGHT_BOTTOM:
|
||||
if orientation == exif.Orientation.RIGHT_BOTTOM: # 方向值7:方向为右侧底部,需要顺时针旋转270度
|
||||
transform.rotate(270)
|
||||
return img.transformed(transform).mirrored(
|
||||
horizontal=True, vertical=False)
|
||||
if orientation == exif.Orientation.LEFT_BOTTOM:
|
||||
if orientation == exif.Orientation.LEFT_BOTTOM: # 方向值8:方向为左侧底部,需要逆时针旋转270度
|
||||
transform.rotate(270)
|
||||
return img.transformed(transform)
|
||||
|
||||
return img
|
||||
return img # 方向值1或其他未知值,返回原始图像
|
||||
|
||||
|
||||
# 加载图像的主要函数
|
||||
def load_image(path):
|
||||
if isinstance(path, str):
|
||||
path = os.path.normpath(path)
|
||||
return (exif_rotated_image(path), path)
|
||||
if path.isLocalFile():
|
||||
path = os.path.normpath(path.toLocalFile())
|
||||
return (exif_rotated_image(path), path)
|
||||
|
||||
# 处理本地文件路径(字符串形式)
|
||||
if isinstance(path, str): # 检查路径是否为字符串类型
|
||||
path = os.path.normpath(path) # 规范路径格式,处理路径中的特殊字符和点号
|
||||
return (exif_rotated_image(path), path) # 加载图像并处理EXIF方向
|
||||
|
||||
# 处理QUrl对象指向的本地文件
|
||||
if path.isLocalFile(): # 检查路径是否为本地文件路径
|
||||
path = os.path.normpath(path.toLocalFile()) # 转化为本地路径并规范本地文件路径格式
|
||||
return (exif_rotated_image(path), path) # 加载图像并处理EXIF方向
|
||||
|
||||
url = bytes(path.toEncoded()).decode()
|
||||
domain = '.'.join(parse.urlparse(url).netloc.split(".")[-2:])
|
||||
img = exif_rotated_image()
|
||||
# 处理远程URL
|
||||
url = bytes(path.toEncoded()).decode() # 将QUrl对象转化为URL字符串并解码
|
||||
domain = '.'.join(parse.urlparse(url).netloc.split(".")[-2:]) # 从URL中提取域名(例如:'pinterest.com')
|
||||
img = exif_rotated_image() # 创建一个空的QImage对象
|
||||
|
||||
# 对Pinterest特定处理
|
||||
if domain == 'pinterest.com':
|
||||
try:
|
||||
page_data = request.urlopen(url).read()
|
||||
root = etree.HTML(page_data)
|
||||
url = root.xpath("//img")[0].get('src')
|
||||
except Exception as e:
|
||||
logger.debug(f'Pinterest image download failed: {e}')
|
||||
try: # 尝试下载Pinterest页面HTML内容
|
||||
page_data = request.urlopen(url).read() # 下载Pinterest页面HTML内容
|
||||
root = etree.HTML(page_data) # 解析HTML内容为ElementTree对象
|
||||
url = root.xpath("//img")[0].get('src') # 从HTML中提取第一个img标签的src属性值(图像URL)
|
||||
except Exception as e: # 处理下载Pinterest页面HTML内容时的异常
|
||||
logger.debug(f'Pinterest image download failed: {e}') # 如果Pinterest图片下载失败,记录Pinterest图像下载失败日志
|
||||
|
||||
# 尝试下载图像数据
|
||||
try:
|
||||
imgdata = request.urlopen(url).read()
|
||||
except URLError as e:
|
||||
logger.debug(f'Downloading image failed: {e.reason}')
|
||||
imgdata = request.urlopen(url).read() # 下载图像数据
|
||||
except URLError as e: # 处理下载图像数据时的异常
|
||||
logger.debug(f'Downloading image failed: {e.reason}') # 如果图像下载失败,记录图像下载失败日志
|
||||
else:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
fname = os.path.join(tmp, 'img')
|
||||
with open(fname, 'wb') as f:
|
||||
f.write(imgdata)
|
||||
logger.debug(f'Temporarily saved in: {fname}')
|
||||
img = exif_rotated_image(fname)
|
||||
return (img, url)
|
||||
with tempfile.TemporaryDirectory() as tmp: # 创建一个临时文件用于存储下载成功的图像数据
|
||||
fname = os.path.join(tmp, 'img') # 构建临时文件路径(例如:'/tmp/img')
|
||||
with open(fname, 'wb') as f: # 以二进制写入模式打开临时文件
|
||||
f.write(imgdata) # 将图像数据写入临时文件
|
||||
logger.debug(f'Temporarily saved in: {fname}') # 记录临时保存的图像路径
|
||||
img = exif_rotated_image(fname) # 加载临时图像文件并处理EXIF方向
|
||||
return (img, url) # 返回处理后的图像对象和原始URL
|
||||
|
|
|
|||
|
|
@ -1,41 +1,48 @@
|
|||
# 数据库版本号,用于跟踪数据库结构的更改
|
||||
USER_VERSION = 2
|
||||
# SQLite数据库的应用程序ID,用于标识特定应用程序的数据库文件
|
||||
APPLICATION_ID = 2060242126
|
||||
|
||||
|
||||
# 数据库架构定义,包含创建表的SQL语句列表
|
||||
SCHEMA = [
|
||||
# 创建items表,存储应用中的所有项目信息
|
||||
"""
|
||||
CREATE TABLE items (
|
||||
id INTEGER PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
x REAL DEFAULT 0,
|
||||
y REAL DEFAULT 0,
|
||||
z REAL DEFAULT 0,
|
||||
scale REAL DEFAULT 1,
|
||||
rotation REAL DEFAULT 0,
|
||||
flip INTEGER DEFAULT 1,
|
||||
data JSON
|
||||
id INTEGER PRIMARY KEY, # 主键,唯一标识每个项目
|
||||
type TEXT NOT NULL, # 项目类型,如图片、文本等
|
||||
x REAL DEFAULT 0, # X坐标位置
|
||||
y REAL DEFAULT 0, # Y坐标位置
|
||||
z REAL DEFAULT 0, # Z轴位置,用于确定项目的显示顺序
|
||||
scale REAL DEFAULT 1, # 缩放比例
|
||||
rotation REAL DEFAULT 0, # 旋转角度(以度为单位)
|
||||
flip INTEGER DEFAULT 1, # 翻转状态,用于水平或垂直翻转项目
|
||||
data JSON # JSON格式的数据,存储项目的其他属性
|
||||
)
|
||||
""",
|
||||
# 创建sqlar表,用于存储与项目关联的二进制数据
|
||||
"""
|
||||
CREATE TABLE sqlar (
|
||||
name TEXT PRIMARY KEY,
|
||||
item_id INTEGER NOT NULL UNIQUE,
|
||||
mode INT,
|
||||
mtime INT default current_timestamp,
|
||||
sz INT,
|
||||
data BLOB,
|
||||
FOREIGN KEY (item_id)
|
||||
name TEXT PRIMARY KEY, # 文件名作为主键
|
||||
item_id INTEGER NOT NULL UNIQUE, # 关联的items表中的项目ID
|
||||
mode INT, # 文件模式
|
||||
mtime INT default current_timestamp, # 修改时间戳
|
||||
sz INT, # 文件大小
|
||||
data BLOB, # 二进制数据内容
|
||||
FOREIGN KEY (item_id) # 外键约束,关联items表
|
||||
REFERENCES items (id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE CASCADE # 级联删除,当items表中的记录被删除时,相关的sqlar记录也会被删除
|
||||
ON UPDATE NO ACTION # 禁止更新关联字段
|
||||
)
|
||||
""",
|
||||
]
|
||||
|
||||
|
||||
# 数据库迁移定义,键为目标版本号,值为实现该版本升级所需的SQL语句列表
|
||||
MIGRATIONS = {
|
||||
# 从版本1迁移到版本2的SQL语句
|
||||
2: [
|
||||
"ALTER TABLE items ADD COLUMN data JSON",
|
||||
"UPDATE items SET data = json_object('filename', filename)",
|
||||
"ALTER TABLE items ADD COLUMN data JSON", # 添加JSON类型的data列
|
||||
"UPDATE items SET data = json_object('filename', filename)", # 将原有的filename数据迁移到新的JSON格式中
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,308 +23,354 @@ https://www.sqlite.org/appfileformat.html
|
|||
https://www.sqlite.org/sqlar.html
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import sqlite3
|
||||
import tempfile
|
||||
# 导入必要的模块
|
||||
import json # 导入 JSON 模块,用于处理 JSON 数据
|
||||
import logging # 导入日志模块,用于记录程序运行时的信息
|
||||
import os # 导入 os 模块,用于处理文件路径
|
||||
import pathlib # 导入 pathlib 模块,用于处理文件路径
|
||||
import shutil # 导入 shutil 模块,用于文件操作
|
||||
import sqlite3 # 导入 sqlite3 模块,用于操作 SQLite 数据库
|
||||
import tempfile # 导入 tempfile 模块,用于创建临时文件
|
||||
|
||||
from PyQt6 import QtGui
|
||||
|
||||
from beeref import constants
|
||||
from beeref.items import BeePixmapItem, BeeErrorItem
|
||||
from .errors import BeeFileIOError, IMG_LOADING_ERROR_MSG
|
||||
from .schema import SCHEMA, USER_VERSION, MIGRATIONS, APPLICATION_ID
|
||||
from PyQt6 import QtGui # 导入 PyQt6 模块,用于 GUI 编程
|
||||
|
||||
from beeref import constants # 导入 BeeRef 模块,用于定义常量
|
||||
from beeref.items import BeePixmapItem, BeeErrorItem # 导入 BeeRef 模块,用于定义项
|
||||
from .errors import BeeFileIOError, IMG_LOADING_ERROR_MSG # 导入 BeeRef 模块,用于定义错误
|
||||
from .schema import SCHEMA, USER_VERSION, MIGRATIONS, APPLICATION_ID # 导入 BeeRef 模块,用于定义数据库模式
|
||||
|
||||
# 配置日志记录器
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 检查文件是否为bee文件
|
||||
def is_bee_file(path):
|
||||
"""Check whether the file at the given path is a bee file."""
|
||||
|
||||
return os.path.splitext(path)[1] == '.bee'
|
||||
|
||||
return os.path.splitext(path)[1] == '.bee' # 使用os.path.splitext分割文件名和扩展名
|
||||
|
||||
|
||||
# 处理SQLite错误
|
||||
def handle_sqlite_errors(func):
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
func(self, *args, **kwargs)
|
||||
except Exception as e:
|
||||
logger.exception(f'Error while reading/writing {self.filename}')
|
||||
try:
|
||||
# 尝试执行原始函数
|
||||
func(self, *args, **kwargs) # 执行函数
|
||||
except Exception as e: # 捕获所有异常
|
||||
logger.exception(f'Error while reading/writing {self.filename}')# 记录异常日志,包含文件名信息
|
||||
try: # 尝试回滚事务
|
||||
# Try to roll back transaction if there is any
|
||||
if (hasattr(self, '_connection')
|
||||
and self._connection.in_transaction):
|
||||
self.ex('ROLLBACK')
|
||||
logger.debug('Transaction rolled back')
|
||||
except sqlite3.Error:
|
||||
pass
|
||||
self._close_connection()
|
||||
if self.worker:
|
||||
self.worker.finished.emit(self.filename, [str(e)])
|
||||
else:
|
||||
raise BeeFileIOError(msg=str(e), filename=self.filename) from e
|
||||
|
||||
return wrapper
|
||||
if (hasattr(self, '_connection') # 检查是否有数据库连接属性
|
||||
and self._connection.in_transaction): # 检查是否有未提交的事务
|
||||
self.ex('ROLLBACK') # 执行SQL回滚命令
|
||||
logger.debug('Transaction rolled back') # 记录回滚信息
|
||||
except sqlite3.Error as e: # 捕获SQLite错误
|
||||
pass # 如果回滚失败,忽略SQLite错误
|
||||
self._close_connection() # 关闭数据库连接
|
||||
if self.worker: # 如果有工作线程,则向其发送完成信号并附带错误信息
|
||||
self.worker.finished.emit(self.filename, [str(e)]) # 发射工作线程完成信号
|
||||
else: # 如果没有工作线程
|
||||
raise BeeFileIOError(msg=str(e), filename=self.filename) from e# 抛出 BeeFileIOError 异常
|
||||
|
||||
return wrapper # 返回包装后的函数
|
||||
|
||||
# 定义SQLiteIO类,用于操作SQLite数据库文件
|
||||
class SQLiteIO:
|
||||
|
||||
# 初始化SQLiteIO类
|
||||
def __init__(self, filename, scene, create_new=False, readonly=False,
|
||||
worker=None):
|
||||
self.scene = scene
|
||||
self.create_new = create_new
|
||||
self.filename = filename
|
||||
self.readonly = readonly
|
||||
self.worker = worker
|
||||
self.retry = False
|
||||
self.scene = scene # 保存场景引用,用于添加或获取项目
|
||||
self.create_new = create_new # 设置是否创建新文件的标志
|
||||
self.filename = filename # 保存数据库文件名
|
||||
self.readonly = readonly # 设置是否为只读模式
|
||||
self.worker = worker # 保存工作线程引用,用于进度更新
|
||||
self.retry = False # 初始化重试标志,用于错误恢复
|
||||
|
||||
# 析构函数
|
||||
def __del__(self):
|
||||
self._close_connection()
|
||||
self._close_connection() # 在对象被销毁时调用,确保资源被正确释放
|
||||
|
||||
# 关闭数据库连接
|
||||
def _close_connection(self):
|
||||
if hasattr(self, '_connection'):
|
||||
self._connection.close()
|
||||
delattr(self, '_connection')
|
||||
if hasattr(self, '_cursor'):
|
||||
delattr(self, '_cursor')
|
||||
if hasattr(self, '_tmpdir'):
|
||||
self._tmpdir.cleanup()
|
||||
delattr(self, '_tmpdir')
|
||||
# 检查并关闭数据库连接
|
||||
if hasattr(self, '_connection'): # 检查是否有数据库连接属性
|
||||
self._connection.close() # 关闭数据库连接
|
||||
delattr(self, '_connection') # 删除数据库连接属性
|
||||
# 检查并删除游标引用
|
||||
if hasattr(self, '_cursor'): # 检查是否有游标属性
|
||||
delattr(self, '_cursor') # 删除游标属性
|
||||
# 检查并清理临时目录
|
||||
if hasattr(self, '_tmpdir'): # 检查是否有临时目录属性
|
||||
self._tmpdir.cleanup() # 清理临时目录
|
||||
delattr(self, '_tmpdir') # 删除临时目录属性
|
||||
|
||||
# 建立数据库连接
|
||||
def _establish_connection(self):
|
||||
if (self.create_new
|
||||
and not self.readonly
|
||||
and os.path.exists(self.filename)):
|
||||
os.remove(self.filename)
|
||||
# 如果需要创建新文件且文件已存在,则删除现有文件
|
||||
if (self.create_new # 检查是否创建新文件
|
||||
and not self.readonly # 检查是否为只读模式
|
||||
and os.path.exists(self.filename)): # 检查文件是否已存在
|
||||
os.remove(self.filename) # 删除已存在的文件
|
||||
|
||||
if self.create_new:
|
||||
self.scene.clear_save_ids()
|
||||
# 如果创建新文件,清除场景中所有项目的保存I
|
||||
if self.create_new: # 检查是否创建新文件
|
||||
self.scene.clear_save_ids() # 清除场景中所有项目的保存ID
|
||||
|
||||
uri = pathlib.Path(self.filename).resolve().as_uri()
|
||||
if self.readonly:
|
||||
uri = f'{uri}?mode=rw'
|
||||
self._connection = sqlite3.connect(uri, uri=True)
|
||||
self._cursor = self.connection.cursor()
|
||||
if not self.create_new:
|
||||
uri = pathlib.Path(self.filename).resolve().as_uri()# 将文件路径转换为URI格式,确保跨平台兼容性
|
||||
if self.readonly: # 检查是否为只读模式
|
||||
uri = f'{uri}?mode=rw' # 如果是只读模式,添加查询参数以指定为读写模式
|
||||
self._connection = sqlite3.connect(uri, uri=True) # 建立数据库连接,使用URI格式
|
||||
self._cursor = self._connection.cursor() # 创建游标对象,用于执行SQL语句
|
||||
# 如果不是创建新文件,则尝试迁移数据库
|
||||
if not self.create_new: # 检查是否不是创建新文件
|
||||
try:
|
||||
self._migrate()
|
||||
except Exception:
|
||||
self._migrate() # 尝试迁移数据库
|
||||
except Exception as e: # 捕获所有异常
|
||||
# Updating a file failed; try creating it from scratch instead
|
||||
logger.exception('Error migrating bee file')
|
||||
self.create_new = True
|
||||
self._establish_connection()
|
||||
logger.exception('Error migrating bee file: %s', e) # 记录异常日志,包含异常信息
|
||||
self.create_new = True # 标记为创建新文件
|
||||
self._establish_connection() # 重新建立数据库连接
|
||||
|
||||
# 迁移数据库
|
||||
def _migrate(self):
|
||||
"""Migrate database if necessary."""
|
||||
|
||||
version = self.fetchone('PRAGMA user_version')[0]
|
||||
logger.debug(f'Found bee file version: {version}')
|
||||
if version >= USER_VERSION:
|
||||
logger.debug('Version ok; no migrations necessary')
|
||||
return
|
||||
|
||||
version = self.fetchone('PRAGMA user_version')[0] # 获取数据库用户版本号
|
||||
logger.debug(f'Found bee file version: {version}') # 记录数据库版本号
|
||||
if version >= USER_VERSION: # 检查数据库版本号是否大于等于用户版本号
|
||||
logger.debug('Version ok; no migrations necessary') # 如果版本号大于等于用户版本号,无需迁移
|
||||
return # 如果版本号大于等于用户版本号,无需迁移
|
||||
# 如果版本已经是最新的,不需要迁移
|
||||
if self.readonly:
|
||||
try:
|
||||
# See whether file is writable so we can migrate it directly
|
||||
self.ex('PRAGMA application_id=%s' % APPLICATION_ID)
|
||||
except sqlite3.Error:
|
||||
logger.debug('File not writable; use temporary copy instead')
|
||||
self._connection.close()
|
||||
self._tmpdir = tempfile.TemporaryDirectory(
|
||||
prefix=constants.APPNAME)
|
||||
tmpname = os.path.join(self._tmpdir.name, 'mig.bee')
|
||||
shutil.copyfile(self.filename, tmpname)
|
||||
self._connection = sqlite3.connect(tmpname)
|
||||
self._cursor = self.connection.cursor()
|
||||
self.ex('PRAGMA application_id=%s' % APPLICATION_ID) # 设置应用程序ID,确保数据库与应用程序兼容
|
||||
except sqlite3.Error as e: # 捕获 SQLite 错误异常
|
||||
logger.debug('File not writable; use temporary copy instead')# 记录文件不可写日志,建议使用临时副本
|
||||
self._connection.close() # 关闭数据库连接
|
||||
self._tmpdir = tempfile.TemporaryDirectory( # 创建临时目录,用于存储迁移后的数据库文件
|
||||
prefix=constants.APPNAME) # 设置临时目录前缀,确保与应用程序名称一致
|
||||
tmpname = os.path.join(self._tmpdir.name, 'mig.bee') # 构建临时数据库文件名
|
||||
shutil.copyfile(self.filename, tmpname) # 复制原始数据库文件到临时目录
|
||||
self._connection = sqlite3.connect(tmpname) # 连接到临时数据库文件
|
||||
self._cursor = self.connection.cursor() # 创建游标对象,用于执行SQL语句
|
||||
|
||||
self.ex('BEGIN TRANSACTION')
|
||||
for i in range(version, USER_VERSION):
|
||||
logger.debug(f'Migrating from version {i} to {i + 1}...')
|
||||
for migration in MIGRATIONS[i + 1]:
|
||||
self.ex(migration)
|
||||
self.write_meta()
|
||||
self.connection.commit()
|
||||
logger.debug('Migration finished')
|
||||
self.ex('BEGIN TRANSACTION') # 开始事务,确保迁移的原子性
|
||||
for i in range(version, USER_VERSION): # 遍历需要迁移的版本号范围
|
||||
logger.debug(f'Migrating from version {i} to {i + 1}...') # 记录迁移信息,包含当前版本号和目标版本号
|
||||
for migration in MIGRATIONS[i + 1]: # 遍历当前版本号对应的迁移SQL语句列表
|
||||
self.ex(migration) # 执行迁移SQL语句
|
||||
self.write_meta() # 写入数据库元数据,包括应用程序ID和用户版本号
|
||||
self.connection.commit() # 提交事务,确保所有迁移操作都被永久保存
|
||||
logger.debug('Migration finished') # 记录迁移完成日志
|
||||
|
||||
@property
|
||||
# 获取数据库连接属性
|
||||
def connection(self):
|
||||
if not hasattr(self, '_connection'):
|
||||
self._establish_connection()
|
||||
return self._connection
|
||||
if not hasattr(self, '_connection'): # 检查是否没有数据库连接属性
|
||||
self._establish_connection() # 如果没有数据库连接属性,建立数据库连接
|
||||
return self._connection # 返回数据库连接属性
|
||||
|
||||
@property
|
||||
# 获取数据库游标属性
|
||||
def cursor(self):
|
||||
if not hasattr(self, '_cursor'):
|
||||
self._establish_connection()
|
||||
return self._cursor
|
||||
if not hasattr(self, '_cursor'): # 检查是否没有数据库游标属性
|
||||
self._establish_connection() # 如果没有数据库游标属性,建立数据库连接
|
||||
return self._cursor # 返回数据库游标属性
|
||||
|
||||
# 执行SQL语句,返回执行结果
|
||||
def ex(self, *args, **kwargs):
|
||||
return self.cursor.execute(*args, **kwargs)
|
||||
|
||||
# 执行批量SQL语句,返回执行结果
|
||||
def exmany(self, *args, **kwargs):
|
||||
return self.cursor.executemany(*args, **kwargs)
|
||||
|
||||
# 从数据库游标中获取一条记录,返回记录元组
|
||||
def fetchone(self, *args, **kwargs):
|
||||
self.ex(*args, **kwargs)
|
||||
return self.cursor.fetchone()
|
||||
self.ex(*args, **kwargs) # 执行SQL语句,传入参数和关键字参数
|
||||
return self.cursor.fetchone() # 返回查询结果的第一行
|
||||
|
||||
# 从数据库游标中获取所有记录,返回记录列表
|
||||
def fetchall(self, *args, **kwargs):
|
||||
self.ex(*args, **kwargs)
|
||||
return self.cursor.fetchall()
|
||||
self.ex(*args, **kwargs) # 执行SQL语句,传入参数和关键字参数
|
||||
return self.cursor.fetchall() # 从游标中获取所有记录,返回记录列表
|
||||
|
||||
# 写入数据库元数据,包括应用程序ID和用户版本号
|
||||
def write_meta(self):
|
||||
self.ex('PRAGMA application_id=%s' % APPLICATION_ID)
|
||||
self.ex('PRAGMA user_version=%s' % USER_VERSION)
|
||||
self.ex('PRAGMA foreign_keys=ON')
|
||||
self.ex('PRAGMA application_id=%s' % APPLICATION_ID) # 设置应用程序ID,确保数据库与应用程序兼容
|
||||
self.ex('PRAGMA user_version=%s' % USER_VERSION) # 设置用户版本,用于版本控制
|
||||
self.ex('PRAGMA foreign_keys=ON') # 启用外键约束,确保数据完整性
|
||||
|
||||
# 创建新文件时创建数据库架构
|
||||
def create_schema_on_new(self):
|
||||
if self.create_new:
|
||||
self.write_meta()
|
||||
for schema in SCHEMA:
|
||||
self.ex(schema)
|
||||
if self.create_new: # 仅在创建新文件时执行
|
||||
self.write_meta() # 写入数据库元数据
|
||||
for schema in SCHEMA: # 执行所有架构创建SQL语句
|
||||
self.ex(schema) # 执行架构创建SQL语句
|
||||
|
||||
# 从数据库中读取所有项,包括图片和文本项
|
||||
@handle_sqlite_errors
|
||||
def read(self):
|
||||
# 读取所有图像项目及其关联的二进制数据
|
||||
rows = self.fetchall(
|
||||
'SELECT items.id, type, x, y, z, scale, rotation, flip, '
|
||||
'items.data, sqlar.data '
|
||||
'FROM sqlar JOIN items on sqlar.item_id = items.id')
|
||||
# Avoid OUTER JOIN for performance reasons; fetch text items
|
||||
# separately instead
|
||||
# 读取所有文本项目
|
||||
rows.extend(self.fetchall(
|
||||
'SELECT items.id, type, x, y, z, scale, rotation, flip, '
|
||||
' items.data, null as data '
|
||||
'FROM items '
|
||||
'WHERE items.type = "text"'))
|
||||
if self.worker:
|
||||
self.worker.begin_processing.emit(len(rows))
|
||||
if self.worker: # 如果有工作线程
|
||||
self.worker.begin_processing.emit(len(rows)) # 发送开始处理信号,包含项数
|
||||
|
||||
# 遍历所有读取的项目数据
|
||||
for i, row in enumerate(rows):
|
||||
# 构建项目数据字典
|
||||
data = {
|
||||
'save_id': row[0],
|
||||
'type': row[1],
|
||||
'x': row[2],
|
||||
'y': row[3],
|
||||
'z': row[4],
|
||||
'scale': row[5],
|
||||
'rotation': row[6],
|
||||
'flip': row[7],
|
||||
'data': json.loads(row[8]),
|
||||
'save_id': row[0], # 项目ID
|
||||
'type': row[1], # 项目类型
|
||||
'x': row[2], # X坐标
|
||||
'y': row[3], # Y坐标
|
||||
'z': row[4], # Z坐标
|
||||
'scale': row[5], # 缩放比例
|
||||
'rotation': row[6], # 旋转角度
|
||||
'flip': row[7], # 是否翻转
|
||||
'data': json.loads(row[8]), # 项目数据
|
||||
}
|
||||
|
||||
if data['type'] == 'pixmap':
|
||||
item = BeePixmapItem(QtGui.QImage())
|
||||
item.pixmap_from_bytes(row[9])
|
||||
# 处理像素图项目
|
||||
if data['type'] == 'pixmap': # 如果项目类型为像素图
|
||||
item = BeePixmapItem(QtGui.QImage()) # 创建空的像素图项目
|
||||
item.pixmap_from_bytes(row[9]) # 从二进制数据创建像素图
|
||||
# 检查图像是否加载成功
|
||||
if item.pixmap().isNull():
|
||||
item = data['data']['text'] = (
|
||||
# 如果图像加载失败,创建错误项目
|
||||
item = data['data']['text'] = (
|
||||
f'Image could not be loaded: {item.filename}\n'
|
||||
+ IMG_LOADING_ERROR_MSG)
|
||||
data['type'] = BeeErrorItem.TYPE
|
||||
data['item'] = item
|
||||
|
||||
self.scene.add_item_later(data)
|
||||
data['type'] = BeeErrorItem.TYPE # 将项目类型设置为错误项类型
|
||||
data['item'] = item # 将项目添加到数据字典中
|
||||
|
||||
self.scene.add_item_later(data) # 稍后将项目添加到场景中
|
||||
# 如果有工作线程,更新进度并检查是否取消操作
|
||||
if self.worker:
|
||||
logger.trace(f'Emit progress: {i}')
|
||||
self.worker.progress.emit(i)
|
||||
if self.worker.canceled:
|
||||
self.worker.finished.emit('', [])
|
||||
return
|
||||
logger.trace(f'Emit progress: {i}') # 记录进度信息
|
||||
self.worker.progress.emit(i) # 发送进度信号,包含当前项索引
|
||||
if self.worker.canceled: # 如果工作线程已取消
|
||||
self.worker.finished.emit('', []) # 发送完成信号,包含空字符串和空列表
|
||||
return # 退出循环,停止处理
|
||||
# Give main thread time to process items:
|
||||
self.worker.msleep(10)
|
||||
if self.worker:
|
||||
self.worker.finished.emit(self.filename, [])
|
||||
self.worker.msleep(10) # 暂停10毫秒,允许主线程处理事件
|
||||
if self.worker: # 如果有工作线程
|
||||
self.worker.finished.emit(self.filename, [])# 发送完成信号,包含文件名和空列表
|
||||
|
||||
@handle_sqlite_errors
|
||||
# 写入数据库中的所有项,包括图片和文本项
|
||||
def write(self):
|
||||
if self.readonly:
|
||||
raise sqlite3.OperationalError(
|
||||
# 检查是否为只读模式
|
||||
if self.readonly: # 如果数据库为只读模式
|
||||
raise sqlite3.OperationalError( # 抛出操作错误异常
|
||||
'Attempt to write to a readonly database')
|
||||
try:
|
||||
self.create_schema_on_new()
|
||||
self.write_data()
|
||||
except Exception:
|
||||
if self.retry:
|
||||
try: # 尝试执行写入操作
|
||||
self.create_schema_on_new() # 创建新文件时创建数据库架构
|
||||
self.write_data() # 写入所有数据
|
||||
except Exception as e: # 捕获所有异常
|
||||
#处理写入错误
|
||||
if self.retry: # 如果允许重试
|
||||
# Trying to recover failed
|
||||
raise
|
||||
raise # 如果已经重试过一次,则放弃并抛出异常
|
||||
else:
|
||||
self.retry = True
|
||||
self.retry = True # 第一次失败后,标记为需要重试
|
||||
# Try creating file from scratch and save again
|
||||
logger.exception(
|
||||
f'Updating to existing file {self.filename} failed')
|
||||
self.create_new = True
|
||||
self._close_connection()
|
||||
self.write()
|
||||
logger.exception( # 尝试从头创建文件并再次保存
|
||||
f'Updating to existing file {self.filename} failed') # 记录更新失败的异常信息
|
||||
self.create_new = True # 标记为创建新文件
|
||||
self._close_connection() # 关闭现有连接,以便重新建立连接
|
||||
self.write() # 递归调用写入方法,尝试重新创建文件并保存
|
||||
|
||||
# 写入场景数据到数据库
|
||||
def write_data(self):
|
||||
to_delete = {row[0] for row in self.fetchall('SELECT id from ITEMS')}
|
||||
to_delete = {row[0] for row in self.fetchall('SELECT id from ITEMS')} # 获取所有需要删除的项目ID
|
||||
# We don't want to touch existing items that are displayed as errors:
|
||||
keep = {item.original_save_id
|
||||
for item in self.scene.items_by_type(BeeErrorItem.TYPE)}
|
||||
logger.debug(f'Not saving error items: {keep}')
|
||||
to_delete = to_delete - keep
|
||||
for item in self.scene.items_by_type(BeeErrorItem.TYPE)} # 获取所有错误项的原始保存ID
|
||||
logger.debug(f'Not saving error items: {keep}') # 记录不保存错误项的ID
|
||||
to_delete = to_delete - keep # 从待删除列表中移除需要保留的项目
|
||||
|
||||
# 获取需要保存的项目列表
|
||||
to_save = list(self.scene.items_for_save())
|
||||
# 如果有工作线程,发送开始处理信号
|
||||
if self.worker:
|
||||
self.worker.begin_processing.emit(len(to_save))
|
||||
# 遍历所有需要保存的项目
|
||||
for i, item in enumerate(to_save):
|
||||
logger.debug(f'Saving {item} with id {item.save_id}')
|
||||
if item.save_id:
|
||||
self.update_item(item)
|
||||
to_delete.remove(item.save_id)
|
||||
else:
|
||||
self.insert_item(item)
|
||||
logger.debug(f'Saving {item} with id {item.save_id}') # 记录正在保存的项目信息
|
||||
if item.save_id: # 如果项目已存在数据库中
|
||||
self.update_item(item) # 更新项目数据
|
||||
to_delete.remove(item.save_id) # 从待删除列表中移除已更新的项目ID
|
||||
else: # 如果项目不在数据库中
|
||||
self.insert_item(item) # 插入新项目到数据库中
|
||||
# 如果有工作线程,发送进度信号,包含当前项索引
|
||||
if self.worker:
|
||||
self.worker.progress.emit(i)
|
||||
# 如果工作线程已取消,跳出循环,停止处理
|
||||
if self.worker.canceled:
|
||||
break
|
||||
self.delete_items(to_delete)
|
||||
self.ex('VACUUM')
|
||||
self.connection.commit()
|
||||
if self.worker:
|
||||
self.worker.finished.emit(self.filename, [])
|
||||
self.delete_items(to_delete) # 删除不再需要的项目
|
||||
self.ex('VACUUM') # 执行VACUUM命令,优化数据库空间
|
||||
self.connection.commit() # 提交事务,确保所有更改生效
|
||||
if self.worker: # 如果有工作线程
|
||||
self.worker.finished.emit(self.filename, []) # 发送完成信号,包含文件名和空列表
|
||||
|
||||
# 从数据库中删除指定的项目
|
||||
def delete_items(self, to_delete):
|
||||
to_delete = [(pk,) for pk in to_delete]
|
||||
self.exmany('DELETE FROM items WHERE id=?', to_delete)
|
||||
self.exmany('DELETE FROM sqlar WHERE item_id=?', to_delete)
|
||||
self.connection.commit()
|
||||
to_delete = [(pk,) for pk in to_delete] # 将待删除项目ID转换为元组列表
|
||||
self.exmany('DELETE FROM items WHERE id=?', to_delete) # 执行批量删除项目的SQL语句
|
||||
self.exmany('DELETE FROM sqlar WHERE item_id=?', to_delete) # 执行批量删除关联文件的SQL语句
|
||||
self.connection.commit() # 提交事务,确保所有更改生效
|
||||
|
||||
# 向数据库中插入新项目
|
||||
def insert_item(self, item):
|
||||
# 插入项目基本信息到items表
|
||||
self.ex(
|
||||
'INSERT INTO items (type, x, y, z, scale, rotation, flip, '
|
||||
'data) '
|
||||
'VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
(item.TYPE, item.pos().x(), item.pos().y(), item.zValue(),
|
||||
item.scale(), item.rotation(), item.flip(),
|
||||
json.dumps(item.get_extra_save_data())))
|
||||
item.save_id = self.cursor.lastrowid
|
||||
|
||||
(item.TYPE, item.pos().x(), item.pos().y(), item.zValue(), # 插入项目的类型、位置、Z值、缩放比例、旋转角度和翻转状态
|
||||
item.scale(), item.rotation(), item.flip(), # 插入项目的缩放比例、旋转角度和翻转状态
|
||||
json.dumps(item.get_extra_save_data()))) # 插入项目的额外保存数据
|
||||
item.save_id = self.cursor.lastrowid # 获取新插入项目的ID
|
||||
# 如果项目有像素图数据,也保存它
|
||||
if hasattr(item, 'pixmap_to_bytes'):
|
||||
pixmap, imgformat = item.pixmap_to_bytes()
|
||||
name = item.get_filename_for_export(imgformat)
|
||||
pixmap, imgformat = item.pixmap_to_bytes() # 获取像素图的二进制数据和图像格式
|
||||
name = item.get_filename_for_export(imgformat) # 获取导出文件名
|
||||
# 将二进制数据插入到sqlar表
|
||||
self.ex(
|
||||
'INSERT INTO sqlar (item_id, name, mode, sz, data) '
|
||||
'VALUES (?, ?, ?, ?, ?)',
|
||||
(item.save_id, name, 0o644, len(pixmap), pixmap))
|
||||
self.connection.commit()
|
||||
(item.save_id, name, 0o644, len(pixmap), pixmap)) # 插入像素图数据到sqlar表
|
||||
self.connection.commit() # 提交事务,确保所有更改生效
|
||||
|
||||
# 更新数据库中的项目数据
|
||||
def update_item(self, item):
|
||||
"""Update item data.
|
||||
|
||||
We only update the item data, not the pixmap data, as pixmap
|
||||
data never changes and is also time-consuming to save.
|
||||
"""
|
||||
# 更新items表中的项目数据
|
||||
self.ex(
|
||||
'UPDATE items SET x=?, y=?, z=?, scale=?, rotation=?, flip=?, '
|
||||
'data=? '
|
||||
'WHERE id=?',
|
||||
(item.pos().x(), item.pos().y(), item.zValue(), item.scale(),
|
||||
item.rotation(), item.flip(),
|
||||
json.dumps(item.get_extra_save_data()),
|
||||
item.save_id))
|
||||
self.connection.commit()
|
||||
(item.pos().x(), item.pos().y(), item.zValue(), item.scale(), # 更新项目的位置、Z值、缩放比例
|
||||
item.rotation(), item.flip(), # 更新项目的旋转角度和翻转状态
|
||||
json.dumps(item.get_extra_save_data()), # 更新项目的额外保存数据
|
||||
item.save_id)) # 更新项目的ID
|
||||
self.connection.commit() # 提交事务,确保所有更改生效
|
||||
|
|
|
|||
1018
beeref/items.py
1018
beeref/items.py
File diff suppressed because it is too large
Load diff
|
|
@ -23,10 +23,10 @@ from beeref.items import BeePixmapItem
|
|||
from beeref import fileio
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger(__name__) # 主控件日志记录器,用于记录主控件的日志信息
|
||||
|
||||
|
||||
class MainControlsMixin:
|
||||
class MainControlsMixin: # 主控件混合类,用于处理主窗口和欢迎叠加层的基本控件
|
||||
"""Basic controls shared by the main view and the welcome overlay:
|
||||
|
||||
* Right-click menu
|
||||
|
|
@ -34,114 +34,114 @@ class MainControlsMixin:
|
|||
* Moving the window without title bar
|
||||
"""
|
||||
|
||||
def init_main_controls(self, main_window):
|
||||
self.main_window = main_window
|
||||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(
|
||||
self.control_target.on_context_menu)
|
||||
self.setAcceptDrops(True)
|
||||
self.movewin_active = False
|
||||
def init_main_controls(self, main_window): # 初始化主控件,用于处理主窗口和欢迎叠加层的基本控件
|
||||
self.main_window = main_window # 主窗口引用,用于访问主窗口的属性和方法
|
||||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) # 设置上下文菜单策略为自定义上下文菜单,用于处理右键点击事件
|
||||
self.customContextMenuRequested.connect( # 自定义上下文菜单请求信号槽函数,用于处理右键点击事件
|
||||
self.control_target.on_context_menu) # 自定义上下文菜单请求信号槽函数,用于处理右键点击事件,调用控制目标的上下文菜单槽函数
|
||||
self.setAcceptDrops(True) # 设置接受拖放事件,用于处理文件拖放事件
|
||||
self.movewin_active = False
|
||||
|
||||
def on_action_movewin_mode(self):
|
||||
if self.movewin_active:
|
||||
def on_action_movewin_mode(self): # 移动窗口模式槽函数,用于处理移动窗口模式的切换
|
||||
if self.movewin_active: # 如果移动窗口模式已激活
|
||||
# Pressing the same shortcut again should end the action
|
||||
self.exit_movewin_mode()
|
||||
self.exit_movewin_mode() # 退出移动窗口模式槽函数,用于处理移动窗口模式的切换
|
||||
else:
|
||||
self.enter_movewin_mode()
|
||||
self.enter_movewin_mode() # 进入移动窗口模式槽函数,用于处理移动窗口模式的切换
|
||||
|
||||
@property
|
||||
def viewport_or_self(self):
|
||||
if hasattr(self, 'viewport'):
|
||||
return self.viewport()
|
||||
return self
|
||||
@property # viewport_or_self属性,用于返回视口或自身,用于处理移动窗口模式的切换
|
||||
def viewport_or_self(self): # viewport_or_self属性,用于返回视口或自身,用于处理移动窗口模式的切换
|
||||
if hasattr(self, 'viewport'): # 如果主控件有视口属性
|
||||
return self.viewport() # 返回视口
|
||||
return self # 返回自身
|
||||
|
||||
def enter_movewin_mode(self):
|
||||
logger.debug('Entering movewin mode')
|
||||
self.setMouseTracking(True)
|
||||
self.movewin_active = True
|
||||
self.viewport_or_self.setCursor(Qt.CursorShape.SizeAllCursor)
|
||||
self.event_start = QtCore.QPointF(self.cursor().pos())
|
||||
if hasattr(self, 'disable_mouse_events'):
|
||||
self.disable_mouse_events()
|
||||
def enter_movewin_mode(self): # 进入移动窗口模式槽函数,用于处理移动窗口模式的切换
|
||||
logger.debug('Entering movewin mode') # 进入移动窗口模式槽函数,用于处理移动窗口模式的切换
|
||||
self.setMouseTracking(True) # 设置鼠标跟踪,用于处理移动窗口模式的切换
|
||||
self.movewin_active = True # 移动窗口模式已激活
|
||||
self.viewport_or_self.setCursor(Qt.CursorShape.SizeAllCursor) # 设置视口或自身的光标为大小调整光标,用于处理移动窗口模式的切换
|
||||
self.event_start = QtCore.QPointF(self.cursor().pos()) # 记录鼠标点击位置,用于处理移动窗口模式的切换
|
||||
if hasattr(self, 'disable_mouse_events'): # 如果主控件有禁用鼠标事件属性
|
||||
self.disable_mouse_events() # 禁用鼠标事件,用于处理移动窗口模式的切换
|
||||
|
||||
def exit_movewin_mode(self):
|
||||
logger.debug('Exiting movewin mode')
|
||||
self.setMouseTracking(False)
|
||||
self.movewin_active = False
|
||||
self.viewport_or_self.unsetCursor()
|
||||
if hasattr(self, 'enable_mouse_events'):
|
||||
self.enable_mouse_events()
|
||||
def exit_movewin_mode(self): # 退出移动窗口模式槽函数,用于处理移动窗口模式的切换
|
||||
logger.debug('Exiting movewin mode') # 退出移动窗口模式槽函数,用于处理移动窗口模式的切换
|
||||
self.setMouseTracking(False) # 退出移动窗口模式槽函数,用于处理移动窗口模式的切换
|
||||
self.movewin_active = False # 移动窗口模式未激活
|
||||
self.viewport_or_self.unsetCursor() # 退出移动窗口模式槽函数,用于处理移动窗口模式的切换
|
||||
if hasattr(self, 'enable_mouse_events'): # 如果主控件有启用鼠标事件属性
|
||||
self.enable_mouse_events() # 启用鼠标事件,用于处理移动窗口模式的切换
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
mimedata = event.mimeData()
|
||||
logger.debug(f'Drag enter event: {mimedata.formats()}')
|
||||
if mimedata.hasUrls():
|
||||
event.acceptProposedAction()
|
||||
elif mimedata.hasImage():
|
||||
event.acceptProposedAction()
|
||||
def dragEnterEvent(self, event): # 拖入事件槽函数,用于处理拖入事件
|
||||
mimedata = event.mimeData() # 获取拖入事件的MIME数据,用于处理拖入事件
|
||||
logger.debug(f'Drag enter event: {mimedata.formats()}') # 拖入事件槽函数,用于处理拖入事件,记录拖入事件的MIME数据格式
|
||||
if mimedata.hasUrls(): # 如果拖入事件的MIME数据包含URL
|
||||
event.acceptProposedAction() # 接受拖入事件的建议操作,用于处理拖入事件
|
||||
elif mimedata.hasImage(): # 如果拖入事件的MIME数据包含图像
|
||||
event.acceptProposedAction() # 接受拖入事件的建议操作,用于处理拖入事件
|
||||
else:
|
||||
msg = 'Attempted drop not an image or image too big'
|
||||
logger.info(msg)
|
||||
widgets.BeeNotification(self.control_target, msg)
|
||||
msg = 'Attempted drop not an image or image too big' # 拖入事件槽函数,用于处理拖入事件,记录拖入事件的MIME数据格式
|
||||
logger.info(msg) # 拖入事件槽函数,用于处理拖入事件,记录拖入事件的MIME数据格式
|
||||
widgets.BeeNotification(self.control_target, msg) # 拖入事件槽函数,用于处理拖入事件,显示拖入事件的MIME数据格式
|
||||
|
||||
def dragMoveEvent(self, event):
|
||||
event.acceptProposedAction()
|
||||
def dragMoveEvent(self, event): # 拖动移动事件槽函数,用于处理拖动移动事件
|
||||
event.acceptProposedAction() # 接受拖动移动事件的建议操作,用于处理拖动移动事件
|
||||
|
||||
def dropEvent(self, event):
|
||||
mimedata = event.mimeData()
|
||||
logger.debug(f'Handling file drop: {mimedata.formats()}')
|
||||
pos = QtCore.QPoint(round(event.position().x()),
|
||||
round(event.position().y()))
|
||||
if mimedata.hasUrls():
|
||||
logger.debug(f'Found dropped urls: {mimedata.urls()}')
|
||||
if not self.control_target.scene.items():
|
||||
def dropEvent(self, event): # 拖放事件槽函数,用于处理拖放事件
|
||||
mimedata = event.mimeData() # 获取拖放事件的MIME数据,用于处理拖放事件
|
||||
logger.debug(f'Handling file drop: {mimedata.formats()}') # 拖放事件槽函数,用于处理拖放事件,记录拖放事件的MIME数据格式
|
||||
pos = QtCore.QPoint(round(event.position().x()), # 记录拖放事件的位置,用于处理拖放事件
|
||||
round(event.position().y())) # 记录拖放事件的位置,用于处理拖放事件
|
||||
if mimedata.hasUrls(): # 如果拖放事件的MIME数据包含URL
|
||||
logger.debug(f'Found dropped urls: {mimedata.urls()}') # 拖放事件槽函数,用于处理拖放事件,记录拖放事件的URL
|
||||
if not self.control_target.scene.items(): # 如果场景中没有项目(即没有插入的图片项)
|
||||
# Check if we have a bee file we can open directly
|
||||
path = mimedata.urls()[0]
|
||||
if (path.isLocalFile()
|
||||
and fileio.is_bee_file(path.toLocalFile())):
|
||||
self.control_target.open_from_file(path.toLocalFile())
|
||||
return
|
||||
self.control_target.do_insert_images(mimedata.urls(), pos)
|
||||
elif mimedata.hasImage():
|
||||
img = QtGui.QImage(mimedata.imageData())
|
||||
item = BeePixmapItem(img)
|
||||
pos = self.control_target.mapToScene(pos)
|
||||
path = mimedata.urls()[0] # 获取拖放事件的URL,用于处理拖放事件
|
||||
if (path.isLocalFile() # 如果拖放事件的URL是本地文件
|
||||
and fileio.is_bee_file(path.toLocalFile())): # 如果拖放事件的URL是本地文件,且是BeeRef文件
|
||||
self.control_target.open_from_file(path.toLocalFile()) # 打开拖放事件的URL,用于处理拖放事件
|
||||
return # 如果拖放事件的URL是本地文件,且是BeeRef文件,直接打开文件,不进行插入图片项操作
|
||||
self.control_target.do_insert_images(mimedata.urls(), pos) # 插入图片项槽函数,用于处理拖放事件,插入拖放事件的URL对应的图片项
|
||||
elif mimedata.hasImage(): # 如果拖放事件的MIME数据包含图像
|
||||
img = QtGui.QImage(mimedata.imageData()) # 获取拖放事件的图像数据,用于处理拖放事件
|
||||
item = BeePixmapItem(img) # 创建图片项,用于处理拖放事件
|
||||
pos = self.control_target.mapToScene(pos) # 记录拖放事件的位置,用于处理拖放事件
|
||||
self.control_target.undo_stack.push(
|
||||
commands.InsertItems(self.control_target.scene, [item], pos))
|
||||
commands.InsertItems(self.control_target.scene, [item], pos)) # 插入图片项槽函数,用于处理拖放事件,插入拖放事件的URL对应的图片项
|
||||
else:
|
||||
logger.info('Drop not an image')
|
||||
logger.info('Drop not an image') # 拖放事件槽函数,用于处理拖放事件,记录拖放事件的MIME数据格式
|
||||
|
||||
def mousePressEventMainControls(self, event):
|
||||
if self.movewin_active:
|
||||
self.exit_movewin_mode()
|
||||
event.accept()
|
||||
return True
|
||||
def mousePressEventMainControls(self, event): # 鼠标按下事件槽函数,用于处理鼠标按下事件
|
||||
if self.movewin_active: # 如果移动窗口模式激活
|
||||
self.exit_movewin_mode() # 退出移动窗口模式槽函数,用于处理移动窗口模式的切换
|
||||
event.accept() # 接受鼠标按下事件的建议操作,用于处理鼠标按下事件
|
||||
return True # 如果移动窗口模式激活,直接返回True,不进行其他操作
|
||||
|
||||
action, inverted =\
|
||||
self.control_target.keyboard_settings.mouse_action_for_event(event)
|
||||
self.control_target.keyboard_settings.mouse_action_for_event(event) # 获取鼠标按下事件对应的操作,用于处理鼠标按下事件
|
||||
if action == 'movewindow':
|
||||
self.enter_movewin_mode()
|
||||
event.accept()
|
||||
return True
|
||||
self.enter_movewin_mode() # 进入移动窗口模式槽函数,用于处理移动窗口模式的切换
|
||||
event.accept() # 接受鼠标按下事件的建议操作,用于处理鼠标按下事件
|
||||
return True # 如果移动窗口模式激活,直接返回True,不进行其他操作
|
||||
|
||||
def mouseMoveEventMainControls(self, event):
|
||||
if self.movewin_active:
|
||||
pos = self.mapToGlobal(event.position())
|
||||
delta = pos - self.event_start
|
||||
self.event_start = pos
|
||||
self.main_window.move(self.main_window.x() + int(delta.x()),
|
||||
self.main_window.y() + int(delta.y()))
|
||||
event.accept()
|
||||
return True
|
||||
def mouseMoveEventMainControls(self, event): # 鼠标移动事件槽函数,用于处理鼠标移动事件
|
||||
if self.movewin_active: # 如果移动窗口模式激活
|
||||
pos = self.mapToGlobal(event.position()) # 获取鼠标移动事件的全局位置,用于处理鼠标移动事件
|
||||
delta = pos - self.event_start # 计算鼠标移动事件的 delta 向量,用于处理鼠标移动事件
|
||||
self.event_start = pos # 更新鼠标移动事件的起始位置,用于处理鼠标移动事件
|
||||
self.main_window.move(self.main_window.x() + int(delta.x()), # 移动主窗口槽函数,用于处理移动窗口模式的切换
|
||||
self.main_window.y() + int(delta.y())) # 移动主窗口槽函数,用于处理移动窗口模式的切换
|
||||
event.accept() # 接受鼠标移动事件的建议操作,用于处理鼠标移动事件
|
||||
return True # 如果移动窗口模式激活,直接返回True,不进行其他操作
|
||||
|
||||
def mouseReleaseEventMainControls(self, event):
|
||||
if self.movewin_active:
|
||||
self.exit_movewin_mode()
|
||||
event.accept()
|
||||
return True
|
||||
def mouseReleaseEventMainControls(self, event): # 鼠标释放事件槽函数,用于处理鼠标释放事件
|
||||
if self.movewin_active: # 如果移动窗口模式激活
|
||||
self.exit_movewin_mode() # 退出移动窗口模式槽函数,用于处理移动窗口模式的切换
|
||||
event.accept() # 接受鼠标释放事件的建议操作,用于处理鼠标释放事件
|
||||
return True # 如果移动窗口模式激活,直接返回True,不进行其他操作
|
||||
|
||||
def keyPressEventMainControls(self, event):
|
||||
if self.movewin_active:
|
||||
self.exit_movewin_mode()
|
||||
event.accept()
|
||||
return True
|
||||
def keyPressEventMainControls(self, event): # 键盘按下事件槽函数,用于处理键盘按下事件
|
||||
if self.movewin_active: # 如果移动窗口模式激活
|
||||
self.exit_movewin_mode() # 退出移动窗口模式槽函数,用于处理移动窗口模式的切换
|
||||
event.accept() # 接受键盘按下事件的建议操作,用于处理键盘按下事件
|
||||
return True # 如果移动窗口模式激活,直接返回True,不进行其他操作
|
||||
|
|
|
|||
729
beeref/scene.py
729
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 <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 到最前面
|
||||
|
|
|
|||
1353
beeref/view.py
1353
beeref/view.py
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue