From 56699885b43774bb59d328fd1f93792c257b3cd3 Mon Sep 17 00:00:00 2001 From: dmMaze Date: Fri, 28 Jun 2024 14:16:12 +0800 Subject: [PATCH] Editable line number, close #439 --- ui/drawingpanel.py | 2 +- ui/fontformatpanel.py | 7 +- ui/scenetext_manager.py | 55 ++++++++++---- ui/stylewidgets.py | 5 -- ui/textedit_area.py | 155 +++++++++++++++++++++++++++++++++++++--- 5 files changed, 191 insertions(+), 33 deletions(-) diff --git a/ui/drawingpanel.py b/ui/drawingpanel.py index d0c747f6..8b4da13c 100644 --- a/ui/drawingpanel.py +++ b/ui/drawingpanel.py @@ -338,7 +338,7 @@ def __init__(self, canvas: Canvas, inpainter_panel: InpaintConfigPanel, *args, * self.setPenToolColor([0, 0, 0, 127]) self.toolConfigStackwidget = QStackedWidget() - self.toolConfigStackwidget.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + self.toolConfigStackwidget.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Minimum) self.toolConfigStackwidget.addWidget(self.inpaintConfigPanel) self.toolConfigStackwidget.addWidget(self.penConfigPanel) self.toolConfigStackwidget.addWidget(self.rectPanel) diff --git a/ui/fontformatpanel.py b/ui/fontformatpanel.py index f40434ae..c9fda166 100644 --- a/ui/fontformatpanel.py +++ b/ui/fontformatpanel.py @@ -425,7 +425,7 @@ def __init__(self, style_name: str = '', parent: Widget = None, fontfmt: FontFor self.active_stylename_edited = active_stylename_edited self.stylelabel = StyleLabel(style_name, parent=self) self.stylelabel.edit_finished.connect(self.on_style_name_edited) - self.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + self.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Maximum) self.setToolTip(self.tr('Click to set as Global format. Double click to edit name.')) self.setCursor(Qt.CursorShape.PointingHandCursor) @@ -642,8 +642,7 @@ def __init__(self, parent: Widget = None): self.flayout.addWidget(self.new_btn) self.flayout.addWidget(self.clear_btn) - # self.scrollContent.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) - self.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Maximum) ScrollBar(Qt.Orientation.Vertical, self) self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) @@ -880,7 +879,7 @@ def __init__(self, text=None, parent=None, expanded=True, *args, **kwargs): self.style_area.hide() self.title_label.clicked.connect(self.on_title_label_clicked) - self.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Maximum) def expand(self): if not self.title_label.expanded: diff --git a/ui/scenetext_manager.py b/ui/scenetext_manager.py index de321ab3..d8c9e404 100644 --- a/ui/scenetext_manager.py +++ b/ui/scenetext_manager.py @@ -248,32 +248,56 @@ class RearrangeBlksCommand(QUndoCommand): def __init__(self, rmap: Tuple, ctrl, parent=None): super().__init__(parent) self.ctrl: SceneTextManager = ctrl - self.src_ids, self.tgt_ids = rmap + self.src_ids, self.tgt_ids = rmap[0], rmap[1] + self.nr = len(self.src_ids) + self.src2tgt = {} + self.tgt2src = {} + for s, t in zip(self.src_ids, self.tgt_ids): + self.src2tgt[s] = t + self.tgt2src[t] = s + self.visible_ = None + self.redo_visible_idx = self.undo_visible_idx = None + if len(rmap) > 2: + self.redo_visible_idx, self.undo_visible_idx = rmap[2] def redo(self): - self.rearrage_blk_ids(self.src_ids, self.tgt_ids) + self.rearange_blk_ids(self.src_ids, self.tgt_ids, self.redo_visible_idx) def undo(self): - self.rearrage_blk_ids(self.tgt_ids, self.src_ids) + self.rearange_blk_ids(self.tgt_ids, self.src_ids, self.undo_visible_idx) + + def rearange_blk_ids(self, src_ids, tgt_ids, visible_idx = None): + src_ids = np.array(src_ids) + tgt_ids = np.array(tgt_ids) + src_order_ids = np.argsort(src_ids)[::-1] - def rearrage_blk_ids(self, src_ids, tgt_ids): + src_ids = src_ids[src_order_ids] + tgt_ids = tgt_ids[src_order_ids] + blks: List[TextBlkItem] = [] pws: List[TransPairWidget] = [] - for ii, src_idx in enumerate(src_ids): - pos = src_idx - ii + for pos, pos_tgt in zip(src_ids, tgt_ids): pw = self.ctrl.pairwidget_list.pop(pos) - self.ctrl.textEditList.removeWidget(pw, remove_checked=False) + if visible_idx == pos_tgt: + pw.hide() blk = self.ctrl.textblk_item_list.pop(pos) pws.append(pw) blks.append(blk) - for ii, tgt_idx in enumerate(tgt_ids): - self.ctrl.textblk_item_list.insert(tgt_idx, blks[ii]) - self.ctrl.pairwidget_list.insert(tgt_idx, pws[ii]) - self.ctrl.textEditList.insertPairWidget(pws[ii], tgt_idx) + tgt_order_ids = np.argsort(tgt_ids) + for ii in tgt_order_ids: + pos = tgt_ids[ii] + self.ctrl.textblk_item_list.insert(pos, blks[ii]) + + self.ctrl.textEditList.insertPairWidget(pws[ii], pos) + self.ctrl.pairwidget_list.insert(pos, pws[ii]) - self.ctrl.updateTextBlkItemIdx() # some optimization could be done here + self.ctrl.updateTextBlkItemIdx(set(tgt_ids)) + if visible_idx is not None: + pw_ct = self.ctrl.pairwidget_list[visible_idx] + pw_ct.show() + self.ctrl.textEditList.ensureWidgetVisible(pw_ct, yMargin=pw.height()) class TextPanel(Widget): def __init__(self, app: QApplication, *args, **kwargs) -> None: @@ -404,6 +428,7 @@ def addTextBlock(self, blk: Union[TextBlock, TextBlkItem] = None) -> TextBlkItem pair_widget.e_trans.show_select_menu.connect(self.on_show_select_menu) pair_widget.e_trans.focus_out.connect(self.on_pairw_focusout) pair_widget.drag_move.connect(self.textEditList.handle_drag_pos) + pair_widget.idx_edited.connect(self.textEditList.on_idx_edited) self.new_textblk.emit(blk_item.idx) return blk_item @@ -941,6 +966,7 @@ def apply_fontformat(self, fontformat: FontFormat): def on_transwidget_selection_changed(self): selitems = self.canvas.selected_text_items() selset = {pw.idx: pw for pw in self.textEditList.checked_list} + self.canvas.block_selection_signal = True for blkitem in selitems: if blkitem.idx not in selset: blkitem.setSelected(False) @@ -948,6 +974,7 @@ def on_transwidget_selection_changed(self): selset.pop(blkitem.idx) for idx in selset: self.textblk_item_list[idx].setSelected(True) + self.canvas.block_selection_signal = False def on_textedit_list_focusout(self): fw = self.app.focusWidget() @@ -966,8 +993,10 @@ def on_apply_effect(self): if len(selected_blks) > 0: self.canvas.push_undo_command(ApplyEffectCommand(selected_blks, ffmt)) - def updateTextBlkItemIdx(self): + 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) cl = self.textEditList.checked_list diff --git a/ui/stylewidgets.py b/ui/stylewidgets.py index 895a120f..459cec32 100644 --- a/ui/stylewidgets.py +++ b/ui/stylewidgets.py @@ -528,12 +528,7 @@ def mousePressEvent(self, e: QMouseEvent) -> None: if e.button() == Qt.MouseButton.LeftButton: self.clicked.emit() return super().mousePressEvent(e) - -class IgnoreMouseLabel(QLabel): - def mousePressEvent(self, e: QMouseEvent) -> None: - e.ignore() - return super().mousePressEvent(e) class CheckableLabel(QLabel): diff --git a/ui/textedit_area.py b/ui/textedit_area.py index f67eded3..7946627e 100644 --- a/ui/textedit_area.py +++ b/ui/textedit_area.py @@ -1,15 +1,16 @@ from typing import List, Union -from qtpy.QtWidgets import QTextEdit, QScrollArea, QGraphicsDropShadowEffect, QVBoxLayout, QApplication, QHBoxLayout, QSizePolicy -from qtpy.QtCore import Signal, Qt, QMimeData, QEvent, QPoint -from qtpy.QtGui import QColor, QFocusEvent, QInputMethodEvent, QDragEnterEvent, QDragMoveEvent, QDropEvent, QKeyEvent, QTextCursor, QMouseEvent, QDrag, QPixmap, QKeySequence +from qtpy.QtWidgets import QStackedWidget, QSizePolicy, QTextEdit, QScrollArea, QGraphicsDropShadowEffect, QVBoxLayout, QApplication, QHBoxLayout, QSizePolicy, QLabel, QLineEdit +from qtpy.QtCore import Signal, Qt, QMimeData, QEvent, QPoint, QSize +from qtpy.QtGui import QIntValidator, QColor, QFocusEvent, QInputMethodEvent, QDragEnterEvent, QDragMoveEvent, QDropEvent, QKeyEvent, QTextCursor, QMouseEvent, QDrag, QPixmap, QKeySequence import keyboard import webbrowser import numpy as np -from .stylewidgets import Widget, SeparatorWidget, ClickableLabel, IgnoreMouseLabel, ScrollBar +from .stylewidgets import Widget, SeparatorWidget, ClickableLabel, ScrollBar from .textitem import TextBlock from utils.config import pcfg +from utils.logger import logger as LOGGER STYLE_TRANSPAIR_CHECKED = "background-color: rgba(30, 147, 229, 20%);" @@ -304,19 +305,110 @@ class TransTextEdit(SourceTextEdit): pass +class RowIndexEditor(QLineEdit): + focus_out = Signal() + + def __init__(self, parent=None): + super().__init__(parent=parent) + self.setValidator(QIntValidator()) + self.setReadOnly(True) + self.setTextMargins(0, 0, 0, 0) + + def focusOutEvent(self, e: QFocusEvent) -> None: + super().focusOutEvent(e) + self.focus_out.emit() + + 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): + + submmit_idx = Signal(int) + + def __init__(self, text: str = None, parent=None): + super().__init__(parent=parent) + self.lineedit = RowIndexEditor(parent=self) + self.lineedit.focus_out.connect(self.on_lineedit_focusout) + + self.show_label = QLabel(self) + self.text = self.show_label.text + + self.addWidget(self.show_label) + self.addWidget(self.lineedit) + self.setCurrentIndex(0) + + if text is not None: + self.setText(text) + self.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Maximum) + + def setText(self, text): + if isinstance(text, int): + text = str(text) + self.show_label.setText(text) + self.lineedit.setText(text) + + def keyPressEvent(self, e: QKeyEvent) -> None: + super().keyPressEvent(e) + + key = e.key() + if key == Qt.Key.Key_Return: + self.try_update_idx() + + def try_update_idx(self): + idx_str = self.lineedit.text().strip() + if not idx_str: + return + if self.text() == idx_str: + return + try: + idx = int(idx_str) + self.lineedit.setReadOnly(True) + self.submmit_idx.emit(idx) + + except Exception as e: + LOGGER.warning(f'Invalid index str: {idx}') + + def mouseDoubleClickEvent(self, e: QMouseEvent) -> None: + self.startEdit() + return super().mouseDoubleClickEvent(e) + + def startEdit(self) -> None: + self.setCurrentIndex(1) + self.lineedit.setReadOnly(False) + self.lineedit.setFocus() + + def on_lineedit_focusout(self): + edited = not self.lineedit.isReadOnly() + self.lineedit.setReadOnly(True) + self.setCurrentIndex(0) + if edited: + self.try_update_idx() + + def mousePressEvent(self, e: QMouseEvent) -> None: + e.ignore() + return super().mousePressEvent(e) + class TransPairWidget(Widget): check_state_changed = Signal(object, bool, bool) drag_move = Signal(int) + idx_edited = Signal(int, int) def __init__(self, textblock: TextBlock = None, idx: int = None, fold: bool = False, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.e_source = SourceTextEdit(idx, self, fold) self.e_trans = TransTextEdit(idx, self, fold) - self.idx_label = IgnoreMouseLabel(self) + self.idx_label = RowIndexLabel(idx, self) self.idx_label.setText(str(idx + 1).zfill(2)) # showed index start from 1! + self.submmit_idx = self.idx_label.submmit_idx.connect(self.on_idx_edited) self.textblock = textblock self.idx = idx self.checked = False @@ -339,6 +431,10 @@ def __init__(self, textblock: TextBlock = None, idx: int = None, fold: bool = Fa self.setAcceptDrops(True) + def on_idx_edited(self, new_idx: int): + new_idx -= 1 + self.idx_edited.emit(self.idx, new_idx) + def dragEnterEvent(self, e: QDragEnterEvent) -> None: if isinstance(e.source(), TransPairWidget): e.accept() @@ -369,7 +465,7 @@ def _set_checked_state(self, checked: bool): else: self.setStyleSheet("") - def mouseReleaseEvent(self, e: QMouseEvent) -> None: + def update_checkstate_by_mousevent(self, e: QMouseEvent): if e.button() == Qt.MouseButton.LeftButton: modifiers = e.modifiers() if modifiers & Qt.KeyboardModifier.ShiftModifier and modifiers & Qt.KeyboardModifier.ControlModifier: @@ -378,7 +474,11 @@ def mouseReleaseEvent(self, e: QMouseEvent) -> None: shift_pressed = modifiers == Qt.KeyboardModifier.ShiftModifier ctrl_pressed = modifiers == Qt.KeyboardModifier.ControlModifier self.check_state_changed.emit(self, shift_pressed, ctrl_pressed) - return super().mouseReleaseEvent(e) + + def mousePressEvent(self, e: QMouseEvent) -> None: + if not self.checked: + self.update_checkstate_by_mousevent(e) + return super().mousePressEvent(e) def updateIndex(self, idx: int): if self.idx != idx: @@ -464,7 +564,21 @@ def mouseMoveEvent(self, e: QMouseEvent) -> None: new_maps = np.where(drags != new_pos) if len(new_maps) == 0: return + drags_ori, drags_tgt = drags[new_maps], new_pos[new_maps] + result_list = list(range(len(self.pairwidget_list))) + to_insert = [] + for ii, src_idx in enumerate(drags_ori): + pos = src_idx - ii + to_insert.append(result_list.pop(pos)) + for ii, tgt_idx in enumerate(drags_tgt): + result_list.insert(tgt_idx, to_insert[ii]) + drags_ori, drags_tgt = [], [] + for ii, idx in enumerate(result_list): + if ii != idx: + drags_ori.append(idx) + drags_tgt.append(ii) + self.rearrange_blks.emit((drags_ori, drags_tgt)) return super().mouseMoveEvent(e) @@ -486,12 +600,14 @@ def set_drag_style(self, pos: int, clear_style: bool = False): def clearDrag(self): self.drag_to_pos = -1 if self.drag is not None: - self.drag.cancel() + try: + self.drag.cancel() + except RuntimeError: + pass self.drag = None def dragMoveEvent(self, e: QDragMoveEvent) -> None: e.accept() - e.position() return super().dragMoveEvent(e) def dragEnterEvent(self, e: QDragEnterEvent): @@ -508,7 +624,27 @@ def handle_drag_pos(self, to_pos: int): self.set_drag_style(self.drag_to_pos, True) self.drag_to_pos = to_pos self.set_drag_style(to_pos) + + def on_idx_edited(self, src_idx: int, tgt_idx: int): + src_idx_ori = tgt_idx + tgt_idx = max(min(tgt_idx, len(self.pairwidget_list) - 1), 0) + if src_idx_ori != tgt_idx: + self.pairwidget_list[src_idx].idx_label.setText(str(src_idx + 1).zfill(2)) + 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) + ids_tgt.append(idx-1) + else: + for idx in range(tgt_idx, src_idx): + ids_ori.append(idx) + ids_tgt.append(idx+1) + self.rearrange_blks.emit((ids_ori, ids_tgt, (tgt_idx, src_idx))) + # self.ensureVisible + def addPairWidget(self, pairwidget: TransPairWidget): self.vlayout.insertWidget(pairwidget.idx, pairwidget) pairwidget.check_state_changed.connect(self.on_widget_checkstate_changed) @@ -612,7 +748,6 @@ def set_selected_list(self, selection_indices: List): if idx == 0: self.sel_anchor_widget = pw - def clearAllSelected(self, emit_signal=True): self.sel_anchor_widget = None if len(self.checked_list) > 0: