diff --git a/ui/canvas.py b/ui/canvas.py index 00912904..76d79aca 100644 --- a/ui/canvas.py +++ b/ui/canvas.py @@ -342,51 +342,57 @@ def _set_scene_scale(self, scale: float): self.baseLayer.setScale(scale) self.setSceneRect(0, 0, self.baseLayer.sceneBoundingRect().width(), self.baseLayer.sceneBoundingRect().height()) - def render_result_img(self): - - self.inpaintLayer.hide() - tlayer_opacity_before = self.textLayer.opacity() - tlayer_visible = self.textLayer.isVisible() - if tlayer_opacity_before != 1: - self.textLayer.setOpacity(1) - if not tlayer_visible: - self.textLayer.show() - scale_before = self.scale_factor - if scale_before != 1: - hb_pos = self.hscroll_bar.value() - vb_pos = self.vscroll_bar.value() - self._set_scene_scale(1) - self.clearSelection() - if self.textEditMode() and self.txtblkShapeControl.blk_item is not None: - blk_item = self.txtblkShapeControl.blk_item - if blk_item.is_editting(): - blk_item.endEdit(keep_focus=False) - if blk_item.isSelected(): - blk_item.setSelected(False) - - result = ndarray2pixmap(self.imgtrans_proj.inpainted_array, return_qimg=True) - canvas_sz = self.img_window_size() - painter = QPainter(result) - painter.setRenderHint(QPainter.RenderHint.Antialiasing) - - rect = QRectF(0, 0, canvas_sz.width(), canvas_sz.height()) - self.render(painter, rect, rect) # produce blurred result if target/source rect not specified #320 - painter.end() - - if tlayer_opacity_before != 1: - self.textLayer.setOpacity(tlayer_opacity_before) - if not tlayer_visible: - self.textLayer.hide() - if scale_before != 1: - self._set_scene_scale(scale_before) - if self.hscroll_bar.value() != hb_pos: - self.hscroll_bar.setValue(hb_pos) - if self.vscroll_bar.value() != vb_pos: - self.vscroll_bar.setValue(vb_pos) - self.inpaintLayer.show() - - return result + def render_result_img(self): + # 设置渲染标志位 + TextBlkItem.is_rendering_output = True + try: + self.inpaintLayer.hide() + tlayer_opacity_before = self.textLayer.opacity() + tlayer_visible = self.textLayer.isVisible() + if tlayer_opacity_before != 1: + self.textLayer.setOpacity(1) + if not tlayer_visible: + self.textLayer.show() + scale_before = self.scale_factor + if scale_before != 1: + hb_pos = self.hscroll_bar.value() + vb_pos = self.vscroll_bar.value() + self._set_scene_scale(1) + + self.clearSelection() + if self.textEditMode() and self.txtblkShapeControl.blk_item is not None: + blk_item = self.txtblkShapeControl.blk_item + if blk_item.is_editting(): + blk_item.endEdit(keep_focus=False) + if blk_item.isSelected(): + blk_item.setSelected(False) + + result = ndarray2pixmap(self.imgtrans_proj.inpainted_array, return_qimg=True) + canvas_sz = self.img_window_size() + painter = QPainter(result) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + + rect = QRectF(0, 0, canvas_sz.width(), canvas_sz.height()) + self.render(painter, rect, rect) + painter.end() + + if tlayer_opacity_before != 1: + self.textLayer.setOpacity(tlayer_opacity_before) + if not tlayer_visible: + self.textLayer.hide() + if scale_before != 1: + self._set_scene_scale(scale_before) + if self.hscroll_bar.value() != hb_pos: + self.hscroll_bar.setValue(hb_pos) + if self.vscroll_bar.value() != vb_pos: + self.vscroll_bar.setValue(vb_pos) + self.inpaintLayer.show() + + return result + finally: + # 确保在函数结束时恢复标志位 + TextBlkItem.is_rendering_output = False def updateLayers(self): diff --git a/ui/configpanel.py b/ui/configpanel.py index 8d361c8b..03120977 100644 --- a/ui/configpanel.py +++ b/ui/configpanel.py @@ -2,11 +2,12 @@ from qtpy.QtWidgets import QPushButton, QKeySequenceEdit, QLayout, QGridLayout, QHBoxLayout, QVBoxLayout, QTreeView, QWidget, QLabel, QSizePolicy, QSpacerItem, QCheckBox, QSplitter, QScrollArea, QLineEdit from qtpy.QtCore import Qt, Signal, QSize, QEvent, QItemSelection -from qtpy.QtGui import QStandardItem, QStandardItemModel, QMouseEvent, QFont, QIntValidator, QValidator, QFocusEvent +from qtpy.QtGui import QStandardItem, QStandardItemModel, QMouseEvent, QFont, QIntValidator, QValidator, QFocusEvent, QDoubleValidator from .custom_widget import ConfigComboBox, Widget from utils.config import pcfg from utils import shared as C +from . import shared_widget as shared from utils.shared import CONFIG_FONTSIZE_CONTENT, CONFIG_FONTSIZE_HEADER, CONFIG_FONTSIZE_TABLE, CONFIG_COMBOBOX_SHORT, CONFIG_COMBOBOX_LONG, CONFIG_COMBOBOX_MIDEAN from .module_parse_widgets import InpaintConfigPanel, TextDetectConfigPanel, TranslatorConfigPanel, OCRConfigPanel @@ -453,6 +454,35 @@ def __init__(self, *args, **kwargs) -> None: discription=self.tr('Split translation into multi-lines according to the extracted balloon region.')) self.let_autolayout_checker.stateChanged.connect(self.on_autolayout_changed) + # <--- 在这里添加新的显示ID控件 ---> + + # 1. 创建一个水平布局来容纳ID相关的控件 + id_display_layout = QHBoxLayout() + + # 2. 添加“显示文本框ID”的复选框 + self.show_text_id_checker, sublock_id = generalConfigPanel.addCheckBox(self.tr('Show Textblock ID')) + self.show_text_id_checker.stateChanged.connect(self.on_show_text_id_changed) + + # 3. 添加“ID字体大小”的输入框 + id_size_label = QLabel(self.tr("ID Font Size:")) + self.text_id_size_edit = QLineEdit() + self.text_id_size_edit.setValidator(QDoubleValidator(5.0, 30.0, 1)) # 限制大小范围为 5.0 - 30.0,1位小数 + self.text_id_size_edit.setFixedWidth(60) + self.text_id_size_edit.editingFinished.connect(self.on_text_id_size_changed) + + # 4. 将控件添加到水平布局中 + id_display_layout.addWidget(id_size_label) + id_display_layout.addWidget(self.text_id_size_edit) + id_display_layout.addStretch() + + # 5. 将这个水平布局添加到复选框所在的 sublock 中 + sublock_id.layout().addLayout(id_display_layout) + + # 6. 初始时,根据复选框状态决定大小输入框是否可用 + self.text_id_size_edit.setEnabled(self.show_text_id_checker.isChecked()) + + # <--- 添加显示ID控件代码结束 ---> + self.let_uppercase_checker, _ = generalConfigPanel.addCheckBox(self.tr('To uppercase')) self.let_uppercase_checker.stateChanged.connect(self.on_uppercase_changed) @@ -510,6 +540,33 @@ def __init__(self, *args, **kwargs) -> None: self.configTable.expandAll() + # <--- 新增ID显示控件函数 ---> + def _force_redraw_text_items(self): + """遍历并强制重绘所有文本框""" + if shared.canvas and shared.st_manager: + for item in shared.st_manager.textblk_item_list: + item.update() + + def on_show_text_id_changed(self): + is_checked = self.show_text_id_checker.isChecked() + pcfg.show_text_id = is_checked + self.text_id_size_edit.setEnabled(is_checked) + # 使用新的强制刷新方法 + self._force_redraw_text_items() + + def on_text_id_size_changed(self): + try: + size = float(self.text_id_size_edit.text()) + # 只有当值确实改变时才更新和重绘 + if pcfg.text_id_font_size != size: + pcfg.text_id_font_size = size + # 使用新的强制刷新方法 + self._force_redraw_text_items() + except ValueError: + # 如果输入无效,可以恢复默认值 + self.text_id_size_edit.setText(str(pcfg.text_id_font_size)) + # <--- 新增ID显示控件函数结束 ---> + def on_load_model_changed(self): pcfg.module.load_model_on_demand = self.load_model_checker.isChecked() @@ -649,4 +706,9 @@ def setupConfig(self): self.empty_runcache_checker.setChecked(pcfg.module.empty_runcache) self.let_show_only_custom_fonts.setChecked(pcfg.let_show_only_custom_fonts_flag) + # <--- 在 setupConfig 方法的末尾( blockSignals(False) 之前)添加以下代码 ---> + self.show_text_id_checker.setChecked(pcfg.show_text_id) + self.text_id_size_edit.setText(str(pcfg.text_id_font_size)) + self.text_id_size_edit.setEnabled(pcfg.show_text_id) + self.blockSignals(False) \ No newline at end of file diff --git a/ui/keywordsubwidget.py b/ui/keywordsubwidget.py index 8b444f9d..01ab9d3b 100644 --- a/ui/keywordsubwidget.py +++ b/ui/keywordsubwidget.py @@ -51,6 +51,20 @@ def loadCfgSublist(self, sublist: List): for sub in sublist: self.add_subpair(**sub, save2sublist=False) + # --- 2026.02.13 新增代码开始 --- + def appendCfgSublist(self, sublist_to_append: List): + """在现有列表后追加新的替换规则""" + for sub in sublist_to_append: + self.add_subpair(**sub, save2sublist=True) + # --- 2026.02.13 新增代码结束 --- + + def clearSublist(self): + """清空替换列表和模型""" + self.changing_rows = True + self.sublist.clear() + self.submodel.removeRows(0, self.submodel.rowCount()) + self.changing_rows = False + def on_new_subpair(self): self.add_subpair() @@ -115,7 +129,7 @@ def sub_text(self, text: str) -> str: k = subpair['keyword'] if k == '': continue - + regexr = k flag = re.DOTALL if not subpair['case_sens']: @@ -129,4 +143,4 @@ def sub_text(self, text: str) -> str: LOGGER.error(traceback.format_exc()) continue - return text \ No newline at end of file + return text diff --git a/ui/mainwindow.py b/ui/mainwindow.py index f54544ac..f4ba8307 100644 --- a/ui/mainwindow.py +++ b/ui/mainwindow.py @@ -5,6 +5,7 @@ import subprocess from functools import partial import time +import json import cv2 from tqdm import tqdm @@ -71,7 +72,7 @@ class MainWindow(mainwindow_cls): restart_signal = Signal() create_errdialog = Signal(str, str, str) create_infodialog = Signal(dict) - + def __init__(self, app: QApplication, config: ProgramConfig, open_dir='', **exec_args) -> None: super().__init__() @@ -151,6 +152,11 @@ def setupUi(self): self.leftBar.export_trans_md.connect(lambda : self.on_export_txt(dump_target='translation', suffix='.md')) self.leftBar.import_trans_txt.connect(self.on_import_trans_txt) + # --- 2026.02.13 新增代码开始 --- + self.leftBar.export_replacement_text.connect(self.on_export_replacement_text) + self.leftBar.import_replacement_text.connect(self.on_import_replacement_text) + # --- 2026.02.13 新增代码结束 --- + self.pageList = PageListView() self.pageList.reveal_file.connect(self.on_reveal_file) self.pageList.setHidden(True) @@ -165,9 +171,9 @@ def setupUi(self): self.imsave_thread.img_writed.connect(self.global_search_widget.on_img_writed) self.global_search_widget.search_tree.result_item_clicked.connect(self.on_search_result_item_clicked) self.leftStackWidget.addWidget(self.global_search_widget) - + self.centralStackWidget = QStackedWidget(self) - + self.titleBar = TitleBar(self) self.titleBar.closebtn_clicked.connect(self.on_closebtn_clicked) self.titleBar.display_lang_changed.connect(self.on_display_lang_changed) @@ -197,7 +203,7 @@ def setupUi(self): self.bottomBar.originalSlider.valueChanged.connect(self.canvas.setOriginalTransparencyBySlider) self.bottomBar.textlayerSlider.valueChanged.connect(self.canvas.setTextLayerTransparencyBySlider) - + self.drawingPanel = DrawingPanel(self.canvas, self.configPanel.inpaint_config_panel) self.textPanel = TextPanel(self.app) self.textPanel.formatpanel.foldTextBtn.checkStateChanged.connect(self.fold_textarea) @@ -298,7 +304,7 @@ def on_finish_settranslator(self): LOGGER.info('Translator set to {}'.format(name)) else: LOGGER.error('invalid translator') - + def on_enable_module(self, idx, checked): if idx == 0: pcfg.module.enable_detect = checked @@ -347,7 +353,7 @@ def setupConfig(self): self.leftBar.imgTransChecker.setChecked(True) self.st_manager.formatpanel.global_format = pcfg.global_fontformat self.st_manager.formatpanel.set_active_format(pcfg.global_fontformat) - + self.rightComicTransStackPanel.setHidden(True) self.st_manager.setTextEditMode(False) self.st_manager.formatpanel.foldTextBtn.setChecked(pcfg.fold_textarea) @@ -453,8 +459,13 @@ def OpenProj(self, proj_path: str): self.openDir(proj_path) else: self.openJsonProj(proj_path) +<<<<<<< HEAD + + if pcfg.let_textstyle_indep_flag and not shared.HEADLESS: +======= if pcfg.let_textstyle_indep_flag and not (shared.HEADLESS or shared.HEADLESS_CONTINUOUS): +>>>>>>> upstream/dev self.load_textstyle_from_proj_dir(from_proj=True) def load_textstyle_from_proj_dir(self, from_proj=False): @@ -500,21 +511,21 @@ def generate_tif_thumbnails(self, directory: str): from utils.io_utils import create_thumbnail, find_tif_files # 查找目录中的所有TIF文件 tif_files = find_tif_files(directory) - + # 为每个TIF文件生成预览图 for tif_file in tif_files: tif_path = osp.join(directory, tif_file) # 检查是否已经存在对应的预览图 base_path = Path(tif_path) thumb_path = base_path.parent / f"{base_path.stem}_thumb.jpg" - + # 如果预览图不存在,则生成预览图 if not osp.exists(thumb_path): create_thumbnail(tif_path, max_width=1000) - + except Exception as e: LOGGER.error(f"Failed to generate TIF thumbnails: {e}") - + def dropOpenDir(self, directory: str): if isinstance(directory, str) and osp.exists(directory): self.leftBar.updateRecentProjList(directory) @@ -532,7 +543,7 @@ def openJsonProj(self, json_path: str): except Exception as e: self.opening_dir = False create_error_dialog(e, self.tr('Failed to load project from') + json_path) - + def updatePageList(self): if self.pageList.count() != 0: self.pageList.clear() @@ -582,7 +593,7 @@ def changeEvent(self, event: QEvent): self.canvas.on_activation_changed() super().changeEvent(event) - + def retranslateUI(self): # according to https://stackoverflow.com/questions/27635068/how-to-retranslate-dynamically-created-widgets # we got to do it manually ... I'd rather restart the program @@ -605,7 +616,7 @@ def conditional_save(self, keep_exist_as_backup=False): save_rst_only = not self.canvas.draw_change_unsaved() if not save_rst_only: save_proj = True - + self.saveCurrentPage(update_scene_text, save_proj, restore_interface=True, save_rst_only=save_rst_only, keep_exist_as_backup=keep_exist_as_backup) def pageListCurrentItemChanged(self): @@ -621,7 +632,7 @@ def pageListCurrentItemChanged(self): self.titleBar.setTitleContent(page_name=self.imgtrans_proj.current_img) self.module_manager.handle_page_changed() self.drawingPanel.handle_page_changed() - + self.page_changing = False def setupShortcuts(self): @@ -824,7 +835,7 @@ def on_global_search(self): cursor = se.textCursor() cursor.select(QTextCursor.SelectionType.Document) se.setTextCursor(cursor) - + self.global_search_widget.commit_search() def show_pre_MT_keyword_window(self): @@ -844,11 +855,11 @@ def on_open_merge_tool(self): from qtpy.QtCore import QThread from qtpy.QtWidgets import QProgressDialog from utils import merger - + self.merge_dialog = MergeDialog(self) self.merge_dialog.run_current_clicked.connect(lambda: self.run_merge_task(on_current=True)) self.merge_dialog.run_all_clicked.connect(lambda: self.run_merge_task(on_current=False)) - + if self.merge_dialog.isVisible(): self.merge_dialog.raise_() self.merge_dialog.activateWindow() @@ -859,39 +870,39 @@ def run_merge_task(self, on_current=False): """执行区域合并任务""" from utils import merger from qtpy.QtWidgets import QMessageBox - + if self.imgtrans_proj.is_empty: QMessageBox.warning(self, "警告", "请先打开一个项目") return - + config = self.merge_dialog.get_config() - + if on_current: # 对当前文件运行 - 直接在内存中操作,不读写文件 from utils.textblock import TextBlock - + current_img = self.imgtrans_proj.current_img if not current_img: QMessageBox.warning(self, "警告", "没有当前文件") return - + # 直接从内存获取当前页面的文本框 if current_img not in self.imgtrans_proj.pages: QMessageBox.warning(self, "警告", "当前页面数据不存在") return - + textblocks = self.imgtrans_proj.pages[current_img] if not textblocks: QMessageBox.warning(self, "提示", "当前页面没有文本框") return - + # 将 TextBlock 对象转换为字典格式(merger 需要字典) initial_shapes = [blk.to_dict() for blk in textblocks] - + initial_count = len(initial_shapes) mode = config.get("MERGE_MODE", "NONE") total_merged = 0 - + # 在内存中执行合并 if mode == "VERTICAL": final_shapes, count = merger.perform_merge(initial_shapes, "VERTICAL", config) @@ -909,7 +920,7 @@ def run_merge_task(self, on_current=False): total_merged += (count1 + count2) else: final_shapes = initial_shapes - + if total_merged > 0: # 将字典转回 TextBlock 对象并更新内存 self.imgtrans_proj.pages[current_img] = [TextBlock(**blk_dict) for blk_dict in final_shapes] @@ -934,48 +945,48 @@ def run_merge_task(self, on_current=False): if not img_list: QMessageBox.warning(self, "警告", "项目中没有图片") return - + # 使用项目的 JSON 文件路径 json_path = self.imgtrans_proj.proj_path if not json_path or not osp.exists(json_path): QMessageBox.warning(self, "警告", f"找不到项目 JSON 文件: {json_path}") return - + # 使用后台线程执行合并 self.run_merge_all_async(json_path, img_list, config) - + def run_merge_all_async(self, json_path, img_list, config): """异步执行所有文件的合并""" from .io_thread import MergeThread - + # 创建合并线程(如果不存在) if not hasattr(self, 'merge_thread'): self.merge_thread = MergeThread() self.merge_thread.progress_changed.connect(self.on_merge_progress) self.merge_thread.merge_finished.connect(self.on_merge_finished) self.merge_thread.progress_bar.stop_clicked.connect(self.on_merge_stop) - + # 启动合并 if self.merge_thread.runMerge(json_path, img_list, config): # 显示进度对话框 self.merge_thread.progress_bar.zero_progress() self.merge_thread.progress_bar.show() - + def on_merge_progress(self, current, total): """合并进度更新""" progress = int(current / total * 100) self.merge_thread.progress_bar.updateTaskProgress(progress, f' {current}/{total}') - + def on_merge_stop(self): """停止合并""" if hasattr(self, 'merge_thread'): self.merge_thread.requestStop() self.merge_thread.progress_bar.hide() - + def on_merge_finished(self, success_count, fail_count): """合并完成""" self.merge_thread.progress_bar.hide() - + # 重新加载整个项目 try: json_path = self.imgtrans_proj.proj_path @@ -987,7 +998,7 @@ def on_merge_finished(self, success_count, fail_count): self.st_manager.updateSceneTextitems() except: pass - + # 显示结果 total = success_count + fail_count QMessageBox.information(self, "完成", f"区域合并完成\n成功: {success_count}/{total}\n失败: {fail_count}/{total}") @@ -1069,10 +1080,10 @@ def manual_save(self): self.saveCurrentPage(update_scene_text=True, save_proj=True, restore_interface=True, save_rst_only=False) def saveCurrentPage(self, update_scene_text=True, save_proj=True, restore_interface=False, save_rst_only=False, keep_exist_as_backup=False): - + if not self.imgtrans_proj.img_valid: return - + if restore_interface: set_canvas_focus = self.canvas.hasFocus() sel_textitem = self.canvas.selected_text_items() @@ -1080,10 +1091,10 @@ def saveCurrentPage(self, update_scene_text=True, save_proj=True, restore_interf editing_textitem = None if n_sel_textitems == 1 and sel_textitem[0].isEditing(): editing_textitem = sel_textitem[0] - + if update_scene_text: self.st_manager.updateTextBlkList() - + if self.rightComicTransStackPanel.isHidden(): self.bottomBar.texteditChecker.click() @@ -1128,7 +1139,7 @@ def saveCurrentPage(self, update_scene_text=True, save_proj=True, restore_interf self.imsave_thread.saveImg(imsave_path, img, self.imgtrans_proj.current_img, save_params={'ext': pcfg.imgsave_ext, 'quality': pcfg.imgsave_quality}, keep_alpha=self.imgtrans_proj.current_has_alpha()) except Exception as e: LOGGER.error(f"Failed to render and save result image: {e}") - + self.canvas.setProjSaveState(False) self.canvas.update_saved_undostep() @@ -1147,7 +1158,7 @@ def saveCurrentPage(self, update_scene_text=True, save_proj=True, restore_interf self.canvas.block_selection_signal = False if editing_textitem is not None: editing_textitem.startEdit() - + def to_trans_config(self): self.leftBar.configChecker.setChecked(True) self.configPanel.focusOnTranslator() @@ -1233,7 +1244,7 @@ def on_transpagebtn_pressed(self, run_target: bool): if len(blkitem_list) < 1: return - + self.translateBlkitemList(blkitem_list, -1) @@ -1243,12 +1254,12 @@ def translateBlkitemList(self, blkitem_list: List, mode: int) -> bool: if tgt_img is None: return False tgt_mask = self.imgtrans_proj.mask_array - + if len(blkitem_list) < 1: return False - + self.global_search_widget.set_document_edited() - + im_h, im_w = tgt_img.shape[:2] blk_list, blk_ids = [], [] @@ -1310,7 +1321,7 @@ def on_pagtrans_finished(self, page_index: int): ffmt_list: List[FontFormat] = self.backup_blkstyles[page_index] self.postprocess_translations(blk_list) - + # override font format if necessary override_fnt_size = pcfg.let_fntsize_flag == 1 override_fnt_stroke = pcfg.let_fntstroke_flag == 1 @@ -1324,7 +1335,7 @@ def on_pagtrans_finished(self, page_index: int): inpaint_only = pcfg.module.enable_inpaint inpaint_only = inpaint_only and not (pcfg.module.enable_detect or pcfg.module.enable_ocr or pcfg.module.enable_translate) - + if not inpaint_only: for ii, blk in enumerate(blk_list): if self._run_imgtrans_wo_textstyle_update and ffmt_list is not None: @@ -1359,7 +1370,7 @@ def on_pagtrans_finished(self, page_index: int): blk.font_family = gf.font_family if blk.rich_text: blk.rich_text = set_html_family(blk.rich_text, gf.font_family) - + blk.line_spacing = gf.line_spacing blk.letter_spacing = gf.letter_spacing blk.italic = gf.italic @@ -1371,7 +1382,7 @@ def on_pagtrans_finished(self, page_index: int): self.st_manager.auto_textlayout_flag = pcfg.let_autolayout_flag and \ (pcfg.module.enable_detect or pcfg.module.enable_translate) - + if page_index != self.pageList.currentIndex().row(): self.pageList.setCurrentRow(page_index) else: @@ -1407,7 +1418,7 @@ def on_blktrans_finished(self, mode: int, blk_ids: List[int]): if len(blk_ids) < 1: return - + blkitem_list = [self.st_manager.textblk_item_list[idx] for idx in blk_ids] pairw_list = [] @@ -1436,7 +1447,7 @@ def on_display_lang_changed(self, lang: str): if lang != pcfg.display_lang: pcfg.display_lang = lang self.set_display_lang(lang) - + def run_imgtrans(self): if not self.imgtrans_proj.is_all_pages_no_text and not pcfg.module.keep_exist_textlines: # 创建自定义消息框,添加"继续运行"选项 @@ -1444,15 +1455,15 @@ def run_imgtrans(self): msgBox.setIcon(QMessageBox.Question) msgBox.setWindowTitle(self.tr('Confirmation')) msgBox.setText(self.tr('\"Run\" will clear previous results, \"Continue\" will try to run from previous progress')) - + # 添加三个按钮(直接使用中文) restart_btn = msgBox.addButton(self.tr('Run'), QMessageBox.YesRole) continue_btn = msgBox.addButton(self.tr('Continue'), QMessageBox.AcceptRole) cancel_btn = msgBox.addButton(self.tr('Cancel'), QMessageBox.RejectRole) - + msgBox.setDefaultButton(continue_btn) msgBox.exec_() - + clicked_button = msgBox.clickedButton() if clicked_button == cancel_btn: return # 取消,不执行任何操作 @@ -1475,9 +1486,9 @@ def on_run_imgtrans(self, continue_mode=False): self.postprocess_mt_toggle = False all_disabled = pcfg.module.all_stages_disabled() - + pages_to_process = [] - + # 继续模式:先检查哪些页面需要处理 if continue_mode: for page_name in self.imgtrans_proj.pages: @@ -1488,7 +1499,7 @@ def on_run_imgtrans(self, continue_mode=False): else: for page_name in self.imgtrans_proj.pages: self.imgtrans_proj.set_page_progress(page_name, 0) - + if pcfg.module.enable_detect: for page in self.imgtrans_proj.pages: if not pcfg.module.keep_exist_textlines: @@ -1502,7 +1513,7 @@ def on_run_imgtrans(self, continue_mode=False): # 如果指定了pages_to_process,跳过不需要处理的页面 if pages_to_process and page_name not in pages_to_process: continue - + ffmt_list = [] self.backup_blkstyles.append(ffmt_list) for textblk in blklist: @@ -1517,7 +1528,7 @@ def on_run_imgtrans(self, continue_mode=False): if pcfg.module.enable_translate or (all_disabled and not self._run_imgtrans_wo_textstyle_update) or pcfg.module.enable_ocr: textblk.rich_text = '' textblk.vertical = textblk.src_is_vertical - + # 如果有指定pages_to_process或者是continue_mode,则传递页面列表 self.module_manager.runImgtransPipeline(pages_to_process if (pages_to_process or continue_mode) else None) @@ -1623,7 +1634,7 @@ def on_import_trans_txt(self): for pagename in matched_pages: for blk in self.imgtrans_proj.pages[pagename]: blk.translation = self.mtSubWidget.sub_text(blk.translation) - + create_info_dialog(msg) except Exception as e: @@ -1702,7 +1713,7 @@ def translate_preprocess(self, translations: List[str] = None, textblocks: List[ def translate_postprocess(self, translations: List[str] = None, textblocks: List[TextBlock] = None, translator = None): if not self.postprocess_mt_toggle: return - + for ii, tr in enumerate(translations): translations[ii] = self.mtSubWidget.sub_text(tr) @@ -1710,7 +1721,7 @@ def on_copy_src(self): blks = self.canvas.selected_text_items() if len(blks) == 0: return - + if isinstance(self.module_manager.translator, GPTTranslator): src_list = [self.st_manager.pairwidget_list[blk.idx].e_source.toPlainText() for blk in blks] src_txt = '' @@ -1730,16 +1741,16 @@ def on_paste_src(self): src_widget_list = [self.st_manager.pairwidget_list[blk.idx].e_source for blk in blks] text_list = self.st_manager.app_clipborad.text().split('\n') - + n_paragraph = min(len(src_widget_list), len(text_list)) if n_paragraph < 1: return - + src_widget_list = src_widget_list[:n_paragraph] text_list = text_list[:n_paragraph] self.canvas.push_undo_command(PasteSrcItemsCommand(src_widget_list, text_list)) - + def run_batch(self, exec_dirs: Union[List, str], **kwargs): if not isinstance(exec_dirs, List): exec_dirs = exec_dirs.split(',') @@ -1771,7 +1782,7 @@ def run_next_dir(self): self.app.quit() return d = self.exec_dirs.pop(0) - + LOGGER.info(f'translating {d} ...') self.openDir(d) shared.pbar = {} @@ -1844,4 +1855,100 @@ def on_hide_view_widget(self, cfg_name: str): widget.setVisible(False) action: QAction = d['action'] action.setChecked(False) - setattr(pcfg, cfg_name, False) \ No newline at end of file + setattr(pcfg, cfg_name, False) + + # --- 2026.02.13 在 MainWindow 类的末尾添加以下新方法 --- + + def on_export_replacement_text(self): + """导出所有替换文本列表到一个JSON文件""" + try: + default_path = osp.join(self.imgtrans_proj.directory or os.getcwd(), "replacement_text.json") + save_path, _ = QFileDialog.getSaveFileName(self, self.tr("Export Replacement Text"), default_path, + "JSON Files (*.json)") + + if not save_path: + return + + data_to_save = { + "ocr": pcfg.ocr_sublist, + "pre_mt": pcfg.pre_mt_sublist, + "mt": pcfg.mt_sublist, + } + + with open(save_path, 'w', encoding='utf-8') as f: + json.dump(data_to_save, f, indent=4, ensure_ascii=False) + + create_info_dialog(self.tr(f'Replacement text successfully exported to {save_path}')) + + except Exception as e: + create_error_dialog(e, self.tr('Failed to export replacement text.')) + + def on_import_replacement_text(self): + """从一个JSON文件导入所有替换文本列表,并询问是追加还是替换。""" + try: + default_dir = self.imgtrans_proj.directory or os.getcwd() + file_path, _ = QFileDialog.getOpenFileName(self, self.tr("Import Replacement Text"), default_dir, + "JSON Files (*.json)") + + if not file_path: + return + + with open(file_path, 'r', encoding='utf-8') as f: + loaded_data = json.load(f) + + if not ("ocr" in loaded_data and "pre_mt" in loaded_data and "mt" in loaded_data): + raise ValueError(self.tr("Invalid file format. The file must contain 'ocr', 'pre_mt', and 'mt' keys.")) + + # --- 新增交互对话框 --- + msgBox = QMessageBox(self) + msgBox.setWindowTitle(self.tr("Import Mode")) + msgBox.setText(self.tr("How do you want to import the replacement text?")) + msgBox.setInformativeText(self.tr( + "Append: Add new rules to the existing list.\nReplace: Clear the existing list and import the new one.")) + + append_btn = msgBox.addButton(self.tr("Append"), QMessageBox.ActionRole) + replace_btn = msgBox.addButton(self.tr("Replace"), QMessageBox.ActionRole) + cancel_btn = msgBox.addButton(QMessageBox.Cancel) + + msgBox.exec_() + + clicked_button = msgBox.clickedButton() + + if clicked_button == cancel_btn: + return # 用户取消操作 + + elif clicked_button == append_btn: + # --- 追加逻辑 --- + pcfg.ocr_sublist.extend(loaded_data["ocr"]) + pcfg.pre_mt_sublist.extend(loaded_data["pre_mt"]) + pcfg.mt_sublist.extend(loaded_data["mt"]) + + self.ocrSubWidget.appendCfgSublist(loaded_data["ocr"]) + self.mtPreSubWidget.appendCfgSublist(loaded_data["pre_mt"]) + self.mtSubWidget.appendCfgSublist(loaded_data["mt"]) + + success_message = self.tr('Replacement text successfully appended.') + + elif clicked_button == replace_btn: + # --- 替换逻辑 (原逻辑) --- + pcfg.ocr_sublist = loaded_data["ocr"] + pcfg.pre_mt_sublist = loaded_data["pre_mt"] + pcfg.mt_sublist = loaded_data["mt"] + + self.ocrSubWidget.clearSublist() + self.ocrSubWidget.loadCfgSublist(pcfg.ocr_sublist) + + self.mtPreSubWidget.clearSublist() + self.mtPreSubWidget.loadCfgSublist(pcfg.pre_mt_sublist) + + self.mtSubWidget.clearSublist() + self.mtSubWidget.loadCfgSublist(pcfg.mt_sublist) + + success_message = self.tr('Replacement text successfully replaced.') + + # 保存配置并显示成功消息 + self.save_config() + create_info_dialog(success_message) + + except Exception as e: + create_error_dialog(e, self.tr('Failed to import replacement text.')) diff --git a/ui/mainwindowbars.py b/ui/mainwindowbars.py index 60b58034..e3b1c4ad 100644 --- a/ui/mainwindowbars.py +++ b/ui/mainwindowbars.py @@ -46,7 +46,7 @@ def mousePressEvent(self, event: QMouseEvent) -> None: self.setChecked(True) elif self.uncheckable: self.setChecked(False) - + def setChecked(self, check: bool) -> None: check_state = self.isChecked() super().setChecked(check) @@ -79,7 +79,7 @@ def __init__(self, mainwindow, *args, **kwargs) -> None: self.imgTransChecker = StateChecker('imgtrans') self.imgTransChecker.setObjectName('ImgTransChecker') self.imgTransChecker.checked.connect(self.stateCheckerChanged) - + self.configChecker = StateChecker('config', uncheckable=True) self.configChecker.setObjectName('ConfigChecker') self.configChecker.checked.connect(self.stateCheckerChanged) @@ -114,8 +114,15 @@ def __init__(self, mainwindow, *args, **kwargs) -> None: actionImportTranslationTxt = QAction(self.tr("Import translation from TXT/markdown"), self) self.import_trans_txt = actionImportTranslationTxt.triggered + # --- 2026.02.13 新增导入导出替换文本开始 --- + actionExportReplacementText = QAction(self.tr("Export Replacement Text"), self) + self.export_replacement_text = actionExportReplacementText.triggered + actionImportReplacementText = QAction(self.tr("Import Replacement Text"), self) + self.import_replacement_text = actionImportReplacementText.triggered + # --- 2026.02.13 新增导入导出替换文本代码结束 --- + self.recentMenu = QMenu(self.tr("Open Recent"), self) - + openMenu = QMenu(self) openMenu.addActions([actionOpenFolder, actionOpenProj]) openMenu.addMenu(self.recentMenu) @@ -129,16 +136,20 @@ def __init__(self, mainwindow, *args, **kwargs) -> None: actionExportSrcMD, actionExportTranslationMD, actionImportTranslationTxt, + # --- 2026.02.13 新增代码开始 --- + actionExportReplacementText, + actionImportReplacementText, + # --- 2026.02.13 新增代码结束 --- ]) self.openBtn = OpenBtn() self.openBtn.setFixedSize(LEFTBTN_WIDTH, LEFTBTN_WIDTH) self.openBtn.setMenu(openMenu) self.openBtn.setPopupMode(QToolButton.InstantPopup) - + openBtnToolBar = QToolBar(self) openBtnToolBar.setFixedSize(LEFTBTN_WIDTH, LEFTBTN_WIDTH) openBtnToolBar.addWidget(self.openBtn) - + self.runImgtransBtn = QPushButton() self.runImgtransBtn.setObjectName('RunButton') self.runImgtransBtn.setText(self.tr('Run')) @@ -148,7 +159,7 @@ def __init__(self, mainwindow, *args, **kwargs) -> None: self.runImgtransBtn.setFixedSize(LEFTBTN_WIDTH, LEFTBTN_WIDTH) self.run_imgtrans_clicked = self.runImgtransBtn.clicked self.runImgtransBtn.setFixedSize(LEFTBTN_WIDTH, LEFTBTN_WIDTH) - + vlayout = QVBoxLayout(self) vlayout.addWidget(openBtnToolBar) vlayout.addWidget(self.showPageListLabel) @@ -225,9 +236,9 @@ def recentActionTriggered(self): else: self.recent_proj_list.remove(path) self.recentMenu.removeAction(self.sender()) - + def onOpenFolder(self) -> None: - + d = None if len(self.recent_proj_list) > 0: for projp in self.recent_proj_list: @@ -236,7 +247,7 @@ def onOpenFolder(self) -> None: if osp.exists(projp): d = projp break - + dialog = QFileDialog() folder_path = str(dialog.getExistingDirectory(self, self.tr("Select Directory"), d)) if osp.exists(folder_path): @@ -259,7 +270,7 @@ def stateCheckerChanged(self, checker_type: str): self.configChecked.emit() else: self.imgTransChecker.setChecked(True) - + def needleftStackWidget(self) -> bool: return self.showPageListLabel.isChecked() or self.globalSearchChecker.isChecked() @@ -371,12 +382,12 @@ def __init__(self, parent, *args, **kwargs) -> None: # 工具菜单 self.toolsToolBtn = TitleBarToolBtn(self) self.toolsToolBtn.setText(self.tr('Tools')) - + # 区域合并工具 mergeToolAction = QAction('区域合并工具', self) mergeToolAction.setShortcut(QKeySequence('Ctrl+Shift+M')) self.merge_tool_trigger = mergeToolAction.triggered - + toolsMenu = QMenu(self.toolsToolBtn) toolsMenu.addAction(mergeToolAction) self.toolsToolBtn.setMenu(toolsMenu) @@ -418,7 +429,7 @@ def __init__(self, parent, *args, **kwargs) -> None: self.titleLabel = QLabel('BallonTranslator') self.titleLabel.setObjectName('TitleLabel') self.titleLabel.setAlignment(Qt.AlignmentFlag.AlignCenter) - + hlayout = QHBoxLayout(self) hlayout.setAlignment(Qt.AlignmentFlag.AlignCenter) hlayout.addWidget(self.iconLabel) @@ -546,7 +557,7 @@ def __init__(self, selector_name: str, add_cfg_btn=True, *args, **kwargs) -> Non super().__init__(*args, **kwargs) label = ConfigClickableLabel(text=selector_name) label.clicked.connect(self.cfg_clicked) - + self.selector = SmallComboBox() self.cfg_btn = None @@ -574,18 +585,18 @@ def leaveEvent(self, event: QEvent) -> None: if self.cfg_btn is not None: self.cfg_btn.setIcon(QIcon()) return super().leaveEvent(event) - + def blockSignals(self, block: bool): self.selector.blockSignals(block) super().blockSignals(block) - + def setSelectedValue(self, value: str, block_signals=True): if block_signals: self.blockSignals(True) self.selector.setCurrentText(value) if block_signals: self.blockSignals(False) - + class TranslatorSelectionWidget(Widget): @@ -599,7 +610,7 @@ def __init__(self) -> None: label_src.clicked.connect(self.cfg_clicked) label_tgt = ConfigClickableLabel(text=self.tr('Target')) label_tgt.clicked.connect(self.cfg_clicked) - + self.selector = SmallComboBox() self.src_selector = SmallComboBox() self.tgt_selector = SmallComboBox() @@ -626,13 +637,13 @@ def leaveEvent(self, event: QEvent) -> None: if self.cfg_btn is not None: self.cfg_btn.setIcon(QIcon()) return super().leaveEvent(event) - + def blockSignals(self, block: bool): self.src_selector.blockSignals(block) self.tgt_selector.blockSignals(block) self.selector.blockSignals(block) super().blockSignals(block) - + def finishSetTranslator(self, translator: BaseTranslator): self.blockSignals(True) self.src_selector.clear() @@ -647,7 +658,7 @@ def finishSetTranslator(self, translator: BaseTranslator): class BottomBar(Widget): - + textedit_checkchanged = Signal() paintmode_checkchanged = Signal() textblock_checkchanged = Signal() @@ -657,7 +668,7 @@ def __init__(self, mainwindow: QMainWindow, *args, **kwargs) -> None: self.setFixedHeight(BOTTOMBAR_HEIGHT) self.setMouseTracking(True) self.mainwindow = mainwindow - + self.textdet_selector = SelectionWithConfigWidget(self.tr('Text Detector')) self.ocr_selector = SelectionWithConfigWidget(self.tr('OCR')) self.inpaint_selector = SelectionWithConfigWidget(self.tr('Inpaint')) @@ -675,7 +686,7 @@ def __init__(self, mainwindow: QMainWindow, *args, **kwargs) -> None: self.textblockChecker = QCheckBox() self.textblockChecker.setObjectName('TextblockChecker') self.textblockChecker.clicked.connect(self.onTextblockCheckerClicked) - + self.originalSlider = PaintQSlider(self.tr("Original image opacity"), Qt.Orientation.Horizontal, self) self.originalSlider.setFixedWidth(150) self.originalSlider.setRange(0, 100) @@ -684,7 +695,7 @@ def __init__(self, mainwindow: QMainWindow, *args, **kwargs) -> None: self.textlayerSlider.setFixedWidth(150) self.textlayerSlider.setValue(100) self.textlayerSlider.setRange(0, 100) - + self.hlayout.addWidget(self.textdet_selector) self.hlayout.addWidget(self.ocr_selector) self.hlayout.addWidget(self.inpaint_selector) @@ -716,4 +727,4 @@ def onTextEditCheckerPressed(self): self.textedit_checkchanged.emit() def onTextblockCheckerClicked(self): - self.textblock_checkchanged.emit() \ No newline at end of file + self.textblock_checkchanged.emit() diff --git a/ui/scenetext_manager.py b/ui/scenetext_manager.py index 0fa083b2..1731818a 100644 --- a/ui/scenetext_manager.py +++ b/ui/scenetext_manager.py @@ -1073,8 +1073,16 @@ def updateTextBlkItemIdx(self, sel_ids: set = None): for ii, blk_item in enumerate(self.textblk_item_list): if sel_ids is not None and ii not in sel_ids: continue - blk_item.idx = ii - self.pairwidget_list[ii].updateIndex(ii) + + # 只有当索引确实发生变化时才更新,并触发重绘 + if blk_item.idx != ii: + blk_item.idx = ii + self.pairwidget_list[ii].updateIndex(ii) + blk_item.update() # <--- 这是关键的新增行 + else: + # 即使索引没变,也确保右侧面板的ID是最新的 + self.pairwidget_list[ii].updateIndex(ii) + cl = self.textEditList.checked_list if len(cl) != 0: cl.sort(key=lambda x: x.idx) diff --git a/ui/textedit_area.py b/ui/textedit_area.py index cd405904..5e1a76cf 100644 --- a/ui/textedit_area.py +++ b/ui/textedit_area.py @@ -106,7 +106,7 @@ def setFold(self, fold: bool): else: self.min_height = 45 self.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth) - + def contextMenuEvent(self, event): menu = self.createStandardContextMenu() @@ -167,23 +167,23 @@ def on_content_changed(self): self.text_content_changed = False if not self.highlighting: self.text_changed.emit() - + if self.hasFocus() and not self.pre_editing and not self.highlighting and not self.in_acts: self.handle_content_change() def handle_content_change(self): if not self.in_redo_undo: - + change_from = self.change_from added_text = '' - + if self.paste_flag: self.paste_flag = False cursor = self.textCursor() cursor.setPosition(change_from) cursor.setPosition(self.textCursor().position(), QTextCursor.MoveMode.KeepAnchor) added_text = cursor.selectedText() - + else: if self.input_method_from != -1: added_text = self.input_method_text @@ -295,7 +295,7 @@ def setPlainTextAndKeepUndoStack(self, text: str): cursor.select(QTextCursor.SelectionType.Document) cursor.insertText(text) - + class TransTextEdit(SourceTextEdit): pass @@ -303,7 +303,7 @@ class TransTextEdit(SourceTextEdit): class RowIndexEditor(QLineEdit): focus_out = Signal() - + def __init__(self, parent=None): super().__init__(parent=parent) self.setValidator(QIntValidator()) @@ -317,11 +317,11 @@ def focusOutEvent(self, e: QFocusEvent) -> None: def minimumSizeHint(self): size = super().minimumSizeHint() return QSize(1, size.height()) - + def sizeHint(self): size = super().sizeHint() return QSize(1, size.height()) - + class RowIndexLabel(QStackedWidget): @@ -366,7 +366,7 @@ def try_update_idx(self): idx = int(idx_str) self.lineedit.setReadOnly(True) self.submmit_idx.emit(idx) - + except Exception as e: LOGGER.warning(f'Invalid index str: {idx}') @@ -389,7 +389,7 @@ def on_lineedit_focusout(self): def mousePressEvent(self, e: QMouseEvent) -> None: e.ignore() return super().mousePressEvent(e) - + class TransPairWidget(Widget): @@ -435,14 +435,14 @@ def dragEnterEvent(self, e: QDragEnterEvent) -> None: if isinstance(e.source(), TransPairWidget): e.accept() return super().dragEnterEvent(e) - + def handle_drag(self, pos: QPoint): y = pos.y() to_pos = self.idx if y > self.size().height() / 2: to_pos += 1 self.drag_move.emit(to_pos) - + def dragMoveEvent(self, e: QDragEnterEvent) -> None: if isinstance(e.source(), TransPairWidget): e.accept() @@ -559,7 +559,7 @@ def mouseMoveEvent(self, e: QMouseEvent) -> None: pass return super().mouseMoveEvent(e) - + def set_drag_style(self, pos: int, clear_style: bool = False): if pos == len(self.pairwidget_list): pos -= 1 @@ -573,7 +573,7 @@ def set_drag_style(self, pos: int, clear_style: bool = False): style += STYLE_TRANSPAIR_CHECKED style = "TransPairWidget{" + style + "}" pw.setStyleSheet(style) - + def clearDrag(self): self.drag_to_pos = -1 if self.drag is not None: @@ -582,7 +582,7 @@ def clearDrag(self): except RuntimeError: pass self.drag = None - + def handle_drag_pos(self, to_pos: int): if self.drag_to_pos != to_pos: if self.drag_to_pos is not None: @@ -600,7 +600,7 @@ def on_pw_dropped(self): num_drags = len(self.checked_list) if num_pw < 2 or num_drags == num_pw: return - + tgt_pos = to_pos drags = [] for pw in self.checked_list: @@ -638,7 +638,7 @@ def on_idx_edited(self, src_idx: int, tgt_idx: int): if src_idx == tgt_idx: return ids_ori, ids_tgt = [src_idx], [tgt_idx] - + if src_idx < tgt_idx: for idx in range(src_idx+1, tgt_idx+1): ids_ori.append(idx) @@ -665,7 +665,7 @@ def insertPairWidget(self, pairwidget: TransPairWidget, idx: int): def on_widget_checkstate_changed(self, pwc: TransPairWidget, shift_pressed: bool, ctrl_pressed: bool): if self.drag is not None: return - + idx = pwc.idx if shift_pressed: checked = True @@ -685,7 +685,8 @@ def on_widget_checkstate_changed(self, pwc: TransPairWidget, shift_pressed: bool if ctrl_pressed: sel_min, sel_max = min(old_idx_list[0], tgt_w.idx), max(old_idx_list[-1], tgt_w.idx) else: - sel_min, sel_max = min(self.sel_anchor_widget.idx, tgt_w.idx), max(self.sel_anchor_widget.idx, tgt_w.idx) + if hasattr(self.sel_anchor_widget,'idx') and hasattr(self.sel_anchor_widget,'idx'): + sel_min, sel_max = min(self.sel_anchor_widget.idx, tgt_w.idx), max(self.sel_anchor_widget.idx, tgt_w.idx) new_check_list = list(range(sel_min, sel_max + 1)) elif ctrl_pressed: new_check_set = set(old_idx_list) @@ -706,7 +707,7 @@ def on_widget_checkstate_changed(self, pwc: TransPairWidget, shift_pressed: bool checked = True if checked: new_check_list.append(idx) - + new_check_set = set(new_check_list) check_changed = False for oidx in old_idx_set: @@ -721,7 +722,7 @@ def on_widget_checkstate_changed(self, pwc: TransPairWidget, shift_pressed: bool check_changed = True pw._set_checked_state(True) self.checked_list.append(pw) - + num_new = len(new_check_list) if num_new == 0: self.sel_anchor_widget = None @@ -770,11 +771,11 @@ def removeWidget(self, widget: TransPairWidget, remove_checked: bool = True): widget._set_checked_state(False) self.checked_list.remove(widget) self.vlayout.removeWidget(widget) - + def focusOutEvent(self, e: QFocusEvent) -> None: self.focus_out.emit() super().focusOutEvent(e) - + def setFoldTextarea(self, fold: bool): for pw in self.pairwidget_list: pw.e_trans.setFold(fold) @@ -788,4 +789,4 @@ def setSourceVisible(self, show: bool): def setTransVisible(self, show: bool): self.trans_visible = show for pw in self.pairwidget_list: - pw.e_trans.setVisible(show) \ No newline at end of file + pw.e_trans.setVisible(show) diff --git a/ui/textitem.py b/ui/textitem.py index d26c46a7..284fd4b3 100644 --- a/ui/textitem.py +++ b/ui/textitem.py @@ -6,11 +6,12 @@ from qtpy.QtCore import Qt, QRect, QRectF, QPointF, Signal, QSizeF from qtpy.QtGui import (QGradient, QKeyEvent, QFont, QTextCursor, QPixmap, QPainterPath, QTextDocument, QInputMethodEvent, QPainter, QPen, QColor, QTextCharFormat, QTextDocument, QLinearGradient, - QBrush, QPalette, QAbstractTextDocumentLayout) + QBrush, QPalette, QAbstractTextDocumentLayout, QFontMetricsF) from utils.textblock import TextBlock, FontFormat, TextAlignment, LineSpacingType from utils.imgproc_utils import xywh2xyxypoly, rotate_polygons from utils.fontformat import FontFormat, px2pt, pt2px +from utils.config import pcfg from .misc import td_pattern, table_pattern from .scene_textlayout import VerticalTextDocumentLayout, HorizontalTextDocumentLayout, SceneTextLayout from .text_graphical_effect import apply_shadow_effect @@ -18,8 +19,8 @@ TEXTRECT_SHOW_COLOR = QColor(30, 147, 229, 170) TEXTRECT_SELECTED_COLOR = QColor(248, 64, 147, 170) - class TextBlkItem(QGraphicsTextItem): + is_rendering_output = False # 保存图片信号 begin_edit = Signal(int) end_edit = Signal(int) @@ -37,6 +38,7 @@ class TextBlkItem(QGraphicsTextItem): push_undo_stack = Signal(int, bool) propagate_user_edited = Signal(int, str, bool) + def __init__(self, blk: TextBlock = None, idx: int = 0, set_format=True, show_rect=False, *args, **kwargs): super().__init__(*args, **kwargs) self.pre_editing = False @@ -294,6 +296,7 @@ def boundingRect(self) -> QRectF: if self._display_rect is not None: br.setHeight(self._display_rect.height()) br.setWidth(self._display_rect.width()) + return br def padding(self) -> float: @@ -468,6 +471,71 @@ def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, widget: QWi self._draw_accessories(painter) painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver) + # --- 从这里开始是新增和修改的代码 --- + # --- 使用新的逻辑绘制ID标签 --- + if not TextBlkItem.is_rendering_output and pcfg.show_text_id: # 没有渲染输出和开关使能才绘制id框 + painter.save() # <--- 在 try 之前调用 save() + try: + # 1. 获取当前的缩放比例 + scale = self.scene_scale_factor() + if scale == 0: # 避免除以零的错误 + painter.restore() + return + idx = self.idx + 1 + + # 2. 定义基础大小(在100%缩放时的大小) + base_font_size = pcfg.text_id_font_size # <--- 从 pcfg 读取字体大小 + base_margin = base_font_size * 0.3 # 边距可以和字体大小关联 + + # 根据缩放比例调整字体和边距,使其在屏幕上看起来大小恒定 + # 这是实现等比例缩放的关键:我们在一个已经被缩放的画布上,用一个反向缩放的字体来绘制 + scaled_font_size = base_font_size / scale + margin = base_margin / scale + + # 3. 设置动态调整后的字体 + id_font = QFont("Arial", 0) # 使用点数大小浮点型,更精确 + id_font.setPointSizeF(scaled_font_size) + painter.setFont(id_font) + + # 准备要显示的 ID 文本 + id_text = f"{idx}" + + # 使用新字体计算文本所需区域 + fm = painter.fontMetrics() + text_rect = fm.boundingRect(id_text) + + # 4. 计算背景和位置 + # 背景矩形的大小 + bg_width = text_rect.width() + 2 * margin + bg_height = text_rect.height() + margin + + # 获取文本框的“外部”矩形 + unpadded_rect = self.unpadRect(self.boundingRect()) + + # 将背景矩形放置在文本框的“外部”左上角(正上方) + + # 将背景矩形放置在文本框的“外部”左侧,并垂直居中 + # bg_x 计算:从左边框向左移动背景框的宽度 + bg_x = unpadded_rect.left() + + # bg_y 计算:垂直居中对齐 + # 注意:这里我们让ID标签的中心和文本框的垂直中心对齐 + bg_y = unpadded_rect.top() + + bg_rect = QRectF(bg_x, bg_y, bg_width, bg_height) + + # 5. 绘制背景和文本 + painter.setBrush(QColor(0, 0, 0, 160)) # 半透明黑色背景 + painter.setPen(Qt.NoPen) + painter.drawRect(bg_rect) + + painter.setPen(Qt.white) # 白色文本 + painter.drawText(bg_rect, Qt.AlignCenter, id_text) + + finally: + painter.restore() # 恢复 painter 的状态 + # --- 新增代码到此结束 --- + def _draw_accessories(self, painter: QPainter): br = self.boundingRect() diff --git a/utils/config.py b/utils/config.py index d60414bd..22da5178 100644 --- a/utils/config.py +++ b/utils/config.py @@ -164,6 +164,8 @@ class ProgramConfig(Config): expand_teffect_panel: bool = True text_advanced_format_panel: bool = True expand_tadvanced_panel: bool = True + show_text_id: bool = True # 控制是否显示文本框ID + text_id_font_size: float = 10.0 # ID标签的基础字体大小(点数) @staticmethod def load(cfg_path: str):