Add tree context menu hotkeys and support renaming lines in GUI

- Добавлены локальные горячие клавиши для действий в дереве: F2 для переименования, Ctrl+Del для удаления, Ctrl+M для объединения блоков.
- Изменен дефолтный хоткей для удаления всех фиксаций (clear_all) на Shift+Del.
- Реализована функция rename_line для переименования линий по аналогии с блоками.
- В контекстное меню дерева добавлено отображение горячих клавиш справа от названий команд.
- Добавлены интеграционные тесты для верификации переименования линий и регистрации хоткеев.
This commit is contained in:
Данил Ярош 2026-07-31 12:35:15 +03:00
parent c15fe6e510
commit 65d7580977
3 changed files with 107 additions and 17 deletions

View File

@ -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:

View File

@ -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",

View File

@ -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"),