From 65d7580977a7eea92d5cc0e040568c7d06d74e8f Mon Sep 17 00:00:00 2001 From: Danil Iarosh Date: Fri, 31 Jul 2026 12:35:15 +0300 Subject: [PATCH] Add tree context menu hotkeys and support renaming lines in GUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлены локальные горячие клавиши для действий в дереве: F2 для переименования, Ctrl+Del для удаления, Ctrl+M для объединения блоков. - Изменен дефолтный хоткей для удаления всех фиксаций (clear_all) на Shift+Del. - Реализована функция rename_line для переименования линий по аналогии с блоками. - В контекстное меню дерева добавлено отображение горячих клавиш справа от названий команд. - Добавлены интеграционные тесты для верификации переименования линий и регистрации хоткеев. --- main_window.py | 97 ++++++++++++++++++++++++++++++++++++++++++-------- qa_smoke.py | 25 ++++++++++++- settings.py | 2 +- 3 files changed, 107 insertions(+), 17 deletions(-) diff --git a/main_window.py b/main_window.py index 24cb517..c1ec983 100644 --- a/main_window.py +++ b/main_window.py @@ -245,6 +245,27 @@ class MainWindow(QMainWindow): self.tree.customContextMenuRequested.connect(self._show_tree_menu) self.tree.itemDoubleClicked.connect(self._on_tree_item_double_clicked) + self.tree_rename_action = QAction(self) + self.tree_rename_action.setShortcut(QKeySequence("F2")) + self.tree_rename_action.setShortcutContext(Qt.WidgetShortcut) + self.tree_rename_action.triggered.connect(self.rename_selected_tree_item) + self.tree.addAction(self.tree_rename_action) + self.actions["tree_rename"] = self.tree_rename_action + + self.tree_delete_action = QAction(self) + self.tree_delete_action.setShortcut(QKeySequence("Ctrl+Del")) + self.tree_delete_action.setShortcutContext(Qt.WidgetShortcut) + self.tree_delete_action.triggered.connect(self.remove_selected_tree_item) + self.tree.addAction(self.tree_delete_action) + self.actions["tree_delete"] = self.tree_delete_action + + self.tree_merge_action = QAction(self) + self.tree_merge_action.setShortcut(QKeySequence("Ctrl+M")) + self.tree_merge_action.setShortcutContext(Qt.WidgetShortcut) + self.tree_merge_action.triggered.connect(self.merge_selected_blocks) + self.tree.addAction(self.tree_merge_action) + self.actions["tree_merge"] = self.tree_merge_action + self.table = QTableWidget(0, 8) self.table.setHorizontalHeaderLabels( ["ID", "X", "Y", "Ряд", "Кол.", "Линия", "Блок", "UID"] @@ -360,7 +381,7 @@ class MainWindow(QMainWindow): QKeySequence("Ctrl+Del"), "Полностью удалить выбранные LED-точки из схемы", ) - add_action("clear_all", "Удалить все", self.clear_all_assignments, "eraser", QKeySequence("Ctrl+Shift+Del")) + add_action("clear_all", "Удалить все", self.clear_all_assignments, "eraser", QKeySequence("Shift+Del")) add_action("clear_selection", "Очистить выбор", lambda: self.canvas.select_points([]), "archive-restore", QKeySequence("Esc")) add_action("fit_view", "Показать всё", self.canvas.fit_to_content, "eye", QKeySequence("Ctrl+0")) grid_action = add_action("grid", "Сетка 34 мм", self.toggle_grid, "grid-2x2", QKeySequence("G"), "Показать или скрыть сетку 34 мм") @@ -1701,32 +1722,39 @@ class MainWindow(QMainWindow): self.tree.setCurrentItem(item) item.setSelected(True) menu = QMenu(self) - merge_action = menu.addAction("Объединить блоки") + merge_action = menu.addAction("Объединить блоки\tCtrl+M") merge_action.setEnabled(self._can_merge_selected_tree_blocks()) - rename_block_action = None + rename_action = None data = None if item is not None: data = item.data(0, Qt.UserRole) - if data and data[0] == "block": - rename_block_action = menu.addAction("Переименовать блок") - label = "Удалить выбранную фиксацию" + if data: + if data[0] == "block": + rename_action = menu.addAction("Переименовать блок\tF2") + elif data[0] == "line": + rename_action = menu.addAction("Переименовать линию\tF2") + label = "Удалить выбранную фиксацию\tCtrl+Del" if item is not None and data is not None: if data[0] == "line": - label = "Удалить линию" + label = "Удалить линию\tCtrl+Del" elif data[0] == "block": - label = "Удалить блок" + label = "Удалить блок\tCtrl+Del" delete_action = menu.addAction(label) delete_action.setEnabled(item is not None) - clear_all_action = menu.addAction("Удалить все фиксации") + clear_all_action = menu.addAction("Удалить все фиксации\tShift+Del") clear_all_action.setEnabled(bool(self.document.lines)) fit_action = menu.addAction("Показать все точки") fit_action.setEnabled(bool(self.document.points)) chosen = menu.exec(self.tree.viewport().mapToGlobal(pos)) if chosen == merge_action: self.merge_selected_blocks() - elif rename_block_action and chosen == rename_block_action: - line_id, block_id = data[1], data[2] - self.rename_block(line_id, block_id) + elif rename_action and chosen == rename_action: + if data[0] == "block": + line_id, block_id = data[1], data[2] + self.rename_block(line_id, block_id) + elif data[0] == "line": + line_id = data[1] + self.rename_line(line_id) elif chosen == delete_action: self.remove_selected_tree_item() elif chosen == clear_all_action: @@ -1736,9 +1764,25 @@ class MainWindow(QMainWindow): def _on_tree_item_double_clicked(self, item: QTreeWidgetItem, column: int) -> None: data = item.data(0, Qt.UserRole) - if data and data[0] == "block": - line_id, block_id = data[1], data[2] - self.rename_block(line_id, block_id) + if data: + if data[0] == "block": + line_id, block_id = data[1], data[2] + self.rename_block(line_id, block_id) + elif data[0] == "line": + line_id = data[1] + self.rename_line(line_id) + + def rename_selected_tree_item(self) -> None: + item = self.tree.currentItem() + if item is not None: + data = item.data(0, Qt.UserRole) + if data: + if data[0] == "block": + line_id, block_id = data[1], data[2] + self.rename_block(line_id, block_id) + elif data[0] == "line": + line_id = data[1] + self.rename_line(line_id) def rename_block(self, line_id: int, block_id: int) -> None: block = self._find_block_or_none(line_id, block_id) @@ -1763,6 +1807,29 @@ class MainWindow(QMainWindow): self.refresh_tree() self._refresh_ui_state() + def rename_line(self, line_id: int) -> None: + line = self._get_line_or_none(line_id) + if line is None: + return + self._disable_shortcuts() + try: + new_name, ok = QInputDialog.getText( + self, + "Переименовать линию", + "Введите новое имя линии:", + QLineEdit.EchoMode.Normal, + line.name + ) + finally: + self._enable_shortcuts() + if ok and new_name.strip() and new_name.strip() != line.name: + self._push_undo() + line.name = new_name.strip() + self._dirty = True + self.status.setText(f"Линия переименована в '{line.name}'.") + self.refresh_tree() + self._refresh_ui_state() + def merge_selected_blocks(self) -> None: selected_blocks = self._selected_tree_blocks() if len(selected_blocks) < 2: diff --git a/qa_smoke.py b/qa_smoke.py index 5c82511..cb77ada 100644 --- a/qa_smoke.py +++ b/qa_smoke.py @@ -120,6 +120,7 @@ def main() -> None: test_panel_mapping_generation_from_document, test_panel_mapping_generation_with_strategies, test_block_renaming_and_naming_strategy, + test_line_renaming_and_tree_shortcuts, test_save_preserves_unknown_source_fields, test_ordered_json_preserves_document_and_remaps_uids, test_background_load_save_roundtrip, @@ -1093,6 +1094,28 @@ def test_block_renaming_and_naming_strategy() -> None: assert "getBlockIndexByName" in legacy_ino +def test_line_renaming_and_tree_shortcuts() -> None: + if MainWindow is None or QApplication is None: + return + app = QApplication.instance() or QApplication([]) + window = MainWindow() + + line = window.document.add_line() + assert line.name == f"Линия {line.id}" + + line.name = "Верхняя Линия" + assert line.name == "Верхняя Линия" + + assert "tree_rename" in window.actions + assert "tree_delete" in window.actions + assert "tree_merge" in window.actions + assert window.actions["tree_rename"].shortcut().toString() == "F2" + assert window.actions["tree_delete"].shortcut().toString() == "Ctrl+Del" + assert window.actions["tree_merge"].shortcut().toString() == "Ctrl+M" + + window.close() + + def test_save_preserves_unknown_source_fields() -> None: with tempfile.TemporaryDirectory() as temp_dir: source = Path(temp_dir) / "custom.json" @@ -1954,7 +1977,7 @@ def test_main_window_action_shortcuts_are_assigned() -> None: "generate_script": "Ctrl+G", "settings": "Ctrl+,", "delete_points": "Ctrl+Del", - "clear_all": "Ctrl+Shift+Del", + "clear_all": "Shift+Del", "clear_selection": "Esc", "fit_view": "Ctrl+0", "grid": "G", diff --git a/settings.py b/settings.py index d2d27a0..59981fd 100644 --- a/settings.py +++ b/settings.py @@ -18,7 +18,7 @@ SHORTCUT_DEFINITIONS: list[tuple[str, str, str]] = [ ("redo", "Повторить", "Ctrl+Y"), ("delete", "Удалить фиксацию", "Del"), ("delete_points", "Удалить точки", "Ctrl+Del"), - ("clear_all", "Удалить все", "Ctrl+Shift+Del"), + ("clear_all", "Удалить все", "Shift+Del"), ("clear_selection", "Очистить выбор", "Esc"), ("fit_view", "Показать всё", "Ctrl+0"), ("grid", "Сетка 34 мм", "G"),