diff --git a/src/script_chainer/config/script_config.py b/src/script_chainer/config/script_config.py index fd10e4f..2cb1066 100644 --- a/src/script_chainer/config/script_config.py +++ b/src/script_chainer/config/script_config.py @@ -451,15 +451,84 @@ def delete_one(self, index: int) -> None: self.init_idx() self.save() + def duplicate_one(self, index: int) -> ScriptConfig | None: + """复制一个配置,插入到原配置(及其挂靠脚本)之后。 + + 内部管理的 Python 脚本会同时复制一份 .py 文件,外部脚本共用同一文件。 + 副本不继承挂靠关系。 + + Args: + index: 要复制的配置下标。 + + Returns: + 新创建的 ScriptConfig,下标非法时返回 None。 + """ + if index < 0 or index >= len(self.script_list): + return None + new_config = self.script_list[index].copy() + new_config.attach_direction = AttachDirection.NONE + + if ( + new_config.script_type == ScriptType.PYTHON + and new_config.script_path + and self._is_managed_script(new_config.script_path) + ): + code = self.get_python_script_content(index) + num = self._next_python_script_number() + p = self._get_python_scripts_dir() / f'{self.module_name}_{num}.py' + p.write_text(code, encoding='utf-8') + new_config.script_path = str(p) + + # 跳过挂靠在原配置上的脚本,避免副本插进挂靠组中间 + pos = index + 1 + while self.is_attached_to_prev(pos): + pos += 1 + self.script_list.insert(pos, new_config) + self.init_idx() + self.save() + return new_config + + def move_one(self, index: int, new_index: int) -> None: + """移动一个配置到新位置(用于上移、下移、置顶)。 + + Args: + index: 当前下标。 + new_index: 目标下标。 + """ + n = len(self.script_list) + if index < 0 or index >= n or new_index < 0 or new_index >= n or index == new_index: + return + new_order = list(self.script_list) + new_order.insert(new_index, new_order.pop(index)) + self.reorder(new_order) + def reorder(self, new_order: list[ScriptConfig]) -> None: """按新顺序重排脚本列表(用于拖拽排序)。 + 挂靠目标因重排而改变的 Python 脚本会取消挂靠,防止"漂移"到新邻居。 + Args: new_order: 新顺序的脚本列表。 """ if len(new_order) != len(self.script_list): return + old_target_of = { + id(config): target + for config, target in zip(self.script_list, self.compute_attach_targets(), strict=True) + } self.script_list = list(new_order) + # 取消挂靠后该脚本会成为新的挂靠目标,可能导致其他脚本的目标连带变化,需反复检查直至稳定 + changed = True + while changed: + changed = False + for config, target in zip(self.script_list, self.compute_attach_targets(), strict=True): + if ( + config.script_type == ScriptType.PYTHON + and config.attach_direction != AttachDirection.NONE + and old_target_of.get(id(config)) is not target + ): + config.attach_direction = AttachDirection.NONE + changed = True self.init_idx() self.save() diff --git a/src/script_chainer/gui/page/script_setting_cards.py b/src/script_chainer/gui/page/script_setting_cards.py index 548b2bd..8860dec 100644 --- a/src/script_chainer/gui/page/script_setting_cards.py +++ b/src/script_chainer/gui/page/script_setting_cards.py @@ -3,10 +3,17 @@ from PySide6.QtCore import Signal from PySide6.QtGui import QIcon from PySide6.QtWidgets import QHBoxLayout -from qfluentwidgets import Dialog, FluentIcon, SwitchButton, TransparentToolButton +from qfluentwidgets import ( + Action, + Dialog, + FluentIcon, + RoundMenu, + SwitchButton, + TransparentToolButton, +) from one_dragon.utils.os_utils import reveal_in_file_manager -from one_dragon_qt.widgets.draggable_list import DraggableListItem +from one_dragon_qt.widgets.draggable_list import DraggableList, DraggableListItem from one_dragon_qt.widgets.setting_card.code_editor_setting_card import ( PythonCodeEditorDialog, ) @@ -35,6 +42,9 @@ class ScriptCardMixin: config: ScriptConfig value_changed: Signal deleted: Signal + copy_requested: Signal + move_requested: Signal + index: int def _setup_common_widgets(self) -> None: self.enable_switch = SwitchButton() @@ -75,6 +85,24 @@ def leaveEvent(self, event) -> None: super().leaveEvent(event) self._rename_btn.setIcon(QIcon()) + def contextMenuEvent(self, event) -> None: + """右键菜单:复制、置顶、上移、下移。""" + parent = self.parentWidget() + count = parent.get_item_count() if isinstance(parent, DraggableList) else 0 + idx = self.index + + menu = RoundMenu(parent=self) + menu.addAction(Action(FluentIcon.COPY, '复制', triggered=lambda: self.copy_requested.emit(idx))) + menu.addSeparator() + top_action = Action(FluentIcon.PIN, '置顶', triggered=lambda: self.move_requested.emit(idx, 0)) + up_action = Action(FluentIcon.UP, '上移', triggered=lambda: self.move_requested.emit(idx, idx - 1)) + down_action = Action(FluentIcon.DOWN, '下移', triggered=lambda: self.move_requested.emit(idx, idx + 1)) + top_action.setEnabled(idx > 0) + up_action.setEnabled(idx > 0) + down_action.setEnabled(idx < count - 1) + menu.addActions([top_action, up_action, down_action]) + menu.exec(event.globalPos()) + def on_enable_changed(self, checked: bool) -> None: self.config.enabled = checked self.value_changed.emit(self.config) @@ -108,6 +136,8 @@ class ScriptSettingCard(ScriptCardMixin, DraggableListItem): value_changed = Signal(ScriptConfig) deleted = Signal(int) + copy_requested = Signal(int) + move_requested = Signal(int, int) edit_requested = Signal(object) def __init__(self, config: ScriptConfig, chain_name: str, index: int = 0, parent=None, @@ -194,6 +224,8 @@ class PythonScriptSettingCard(ScriptCardMixin, DraggableListItem): value_changed = Signal(ScriptConfig) deleted = Signal(int) + copy_requested = Signal(int) + move_requested = Signal(int, int) attach_changed = Signal() def __init__(self, config: ScriptConfig, chain_config: ScriptChainConfig, diff --git a/src/script_chainer/gui/page/script_setting_interface.py b/src/script_chainer/gui/page/script_setting_interface.py index 1fa5ad1..8e523e3 100644 --- a/src/script_chainer/gui/page/script_setting_interface.py +++ b/src/script_chainer/gui/page/script_setting_interface.py @@ -261,6 +261,8 @@ def update_chain_display(self) -> None: card.value_changed.connect(self.script_config_changed) card.deleted.connect(self.script_config_deleted) + card.copy_requested.connect(self.script_config_copied) + card.move_requested.connect(self.script_config_moved) self._update_attach_margins() @@ -306,15 +308,6 @@ def on_order_changed(self, new_data_list: list) -> None: if self.chosen_config is None: return - old_target_of = { - id(config): target - for config, target in zip( - self.chosen_config.script_list, - self.chosen_config.compute_attach_targets(), - strict=False, - ) - } - # 更新卡片列表的顺序和索引 new_card_list: list[DraggableListItem] = [] for data in new_data_list: @@ -325,25 +318,8 @@ def on_order_changed(self, new_data_list: list) -> None: self.script_card_list = new_card_list self.chosen_config.reorder(new_data_list) - new_target_of = { - id(config): target - for config, target in zip( - self.chosen_config.script_list, - self.chosen_config.compute_attach_targets(), - strict=False, - ) - } - for idx, card in enumerate(self.script_card_list): - config = card.data - if isinstance(config, ScriptConfig) and config.script_type == ScriptType.PYTHON: - old_target = old_target_of.get(id(config)) - new_target = new_target_of.get(id(config)) - if old_target is not new_target: - config.attach_direction = AttachDirection.NONE - card.data.idx = idx card.update_item(card.data, idx) - self.chosen_config.save() self._update_attach_margins() def _update_attach_margins(self) -> None: @@ -391,6 +367,22 @@ def script_config_deleted(self, idx: int) -> None: self.chosen_config.delete_one(idx) self.update_chain_display() + def script_config_copied(self, idx: int) -> None: + """复制脚本配置""" + if self.chosen_config is None: + return + + self.chosen_config.duplicate_one(idx) + self.update_chain_display() + + def script_config_moved(self, idx: int, new_idx: int) -> None: + """移动脚本配置(上移、下移、置顶)""" + if self.chosen_config is None: + return + + self.chosen_config.move_one(idx, new_idx) + self.update_chain_display() + class ScriptSettingInterface(BaseInterface):