diff --git a/LEDMapper.spec b/LEDMapper.spec index ad89e27..89d8fe7 100644 --- a/LEDMapper.spec +++ b/LEDMapper.spec @@ -1,11 +1,41 @@ # -*- mode: python ; coding: utf-8 -*- +import subprocess from pathlib import Path spec_dir = Path(SPECPATH) +def git_build_ref(): + try: + tag = subprocess.check_output( + ['git', 'describe', '--tags', '--exact-match', 'HEAD'], + cwd=spec_dir, + stderr=subprocess.DEVNULL, + text=True, + ).strip() + if tag: + return tag + except (OSError, subprocess.CalledProcessError): + pass + try: + commit = subprocess.check_output( + ['git', 'rev-parse', '--short', 'HEAD'], + cwd=spec_dir, + stderr=subprocess.DEVNULL, + text=True, + ).strip() + if commit: + return commit + except (OSError, subprocess.CalledProcessError): + pass + return 'untagged' + + +app_name = f'LEDMapper-{git_build_ref()}' + + a = Analysis( [str(spec_dir / 'main.py')], pathex=[str(spec_dir), str(spec_dir.parent)], @@ -34,7 +64,7 @@ exe = EXE( a.binaries, a.datas, [], - name='LEDMapper', + name=app_name, debug=False, bootloader_ignore_signals=False, strip=False, diff --git a/README.md b/README.md index bd02dcb..eeb8ae8 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ python -m venv .venv | Очистить выбор | `Esc` | | Показать всю схему | `Ctrl+0` | | Сетка 34 мм | `G` | +| Стрелки направления | `Ctrl+Shift+A` | | Скрыть/показать боковую панель | `F9` | | Новая линия | `Ctrl+L` | | Автоблоки | `Ctrl+Shift+B` | diff --git a/canvas.py b/canvas.py index c5e00ae..b28d520 100644 --- a/canvas.py +++ b/canvas.py @@ -66,6 +66,7 @@ class LedCanvas(QGraphicsView): self.items_by_id: dict[int, LedItem] = {} self.grid_items: list[QGraphicsLineItem] = [] self.connection_items: list[QGraphicsPathItem] = [] + self._direction_arrows_visible = True self._point_radius = 4.5 self._brush_radius = 18.0 self._brush_zone_item: QGraphicsEllipseItem | None = None @@ -100,6 +101,15 @@ class LedCanvas(QGraphicsView): self._grid_visible = visible self._refresh_grid() + def set_direction_arrows_visible(self, visible: bool) -> None: + if self._direction_arrows_visible == visible: + return + self._direction_arrows_visible = visible + self.refresh_styles(redraw_connections=True) + + def direction_arrows_visible(self) -> bool: + return self._direction_arrows_visible + def set_brush_enabled(self, enabled: bool) -> None: self._brush_enabled = enabled self._is_brush_stroke = False @@ -537,9 +547,11 @@ class LedCanvas(QGraphicsView): ordered_items = [self.items_by_id[point_id] for point_id in block.point_ids if point_id in self.items_by_id] for first, second in zip(ordered_items, ordered_items[1:]): self._append_connection(inner_path, first.pos(), second.pos()) - self._append_arrow_marker(inner_arrows, first.pos(), second.pos()) + if self._direction_arrows_visible: + self._append_arrow_marker(inner_arrows, first.pos(), second.pos()) self._add_connection_path(inner_path, line.color, inner_width, inner_alpha) - self._add_connection_path(inner_arrows, line.color, arrow_width, arrow_alpha, z_value=2) + if self._direction_arrows_visible: + self._add_connection_path(inner_arrows, line.color, arrow_width, arrow_alpha, z_value=2) previous_block_end = None bridge_path = QPainterPath() @@ -551,11 +563,13 @@ class LedCanvas(QGraphicsView): block_end = self.items_by_id.get(block.point_ids[-1]) if block_start is not None and previous_block_end is not None: self._append_connection(bridge_path, previous_block_end.pos(), block_start.pos()) - self._append_arrow_marker(bridge_arrows, previous_block_end.pos(), block_start.pos()) + if self._direction_arrows_visible: + self._append_arrow_marker(bridge_arrows, previous_block_end.pos(), block_start.pos()) if block_end is not None: previous_block_end = block_end self._add_connection_path(bridge_path, line.color, bridge_width, bridge_alpha) - self._add_connection_path(bridge_arrows, line.color, arrow_width, bridge_alpha, z_value=2) + if self._direction_arrows_visible: + self._add_connection_path(bridge_arrows, line.color, arrow_width, bridge_alpha, z_value=2) @staticmethod def _append_connection(path: QPainterPath, start: QPointF, end: QPointF) -> None: diff --git a/icons.py b/icons.py index 31d74ff..281e0a2 100644 --- a/icons.py +++ b/icons.py @@ -7,6 +7,7 @@ from PySide6.QtSvg import QSvgRenderer LUCIDE_PATHS = { "archive-restore": '', + "arrow-right": '', "brush": '', "chevron-right": '', "eraser": '', diff --git a/main_window.py b/main_window.py index 1d0e8d8..7c5c155 100644 --- a/main_window.py +++ b/main_window.py @@ -209,6 +209,16 @@ class MainWindow(QMainWindow): 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 мм") grid_action.setCheckable(True) + arrows_action = add_action( + "direction_arrows", + "Стрелки направления", + self.toggle_direction_arrows, + "arrow-right", + QKeySequence("Ctrl+Shift+A"), + "Показать или скрыть стрелки направления на линиях", + ) + arrows_action.setCheckable(True) + arrows_action.setChecked(True) add_action("toggle_sidebar", "Скрыть боковую панель", self.toggle_sidebar, "chevron-right", QKeySequence("F9")) self._build_menu_bar() self.addAction(self.delete_action) @@ -234,6 +244,7 @@ class MainWindow(QMainWindow): view_menu = menu_bar.addMenu("Вид") view_menu.addAction(self.actions["fit_view"]) view_menu.addAction(self.actions["grid"]) + view_menu.addAction(self.actions["direction_arrows"]) window_menu = menu_bar.addMenu("Окно") window_menu.addAction(self.actions["toggle_sidebar"]) @@ -349,6 +360,11 @@ class MainWindow(QMainWindow): else: self.collapse_sidebar() + def toggle_direction_arrows(self) -> None: + action = self.actions.get("direction_arrows") + visible = True if action is None else action.isChecked() + self.canvas.set_direction_arrows_visible(visible) + def collapse_sidebar(self) -> None: if self.sidebar_splitter is None or self.sidebar_stack is None or self._sidebar_collapsed: return diff --git a/qa_smoke.py b/qa_smoke.py index 52d0cf5..0bbeb49 100644 --- a/qa_smoke.py +++ b/qa_smoke.py @@ -82,6 +82,7 @@ def main() -> None: test_grid_toggle_adds_grid_items, test_canvas_connects_new_block_to_previous_non_empty_block, test_canvas_draws_direction_arrows_on_connections, + test_canvas_can_toggle_direction_arrows, test_canvas_rebuild_drops_deleted_selection_items, test_canvas_refresh_does_not_accumulate_connection_items, test_main_window_has_classic_menu_bar, @@ -90,6 +91,7 @@ def main() -> None: test_undo_redo_roundtrip_for_line_creation, test_main_window_actions_use_lucide_icons, test_main_window_action_shortcuts_are_assigned, + test_main_window_direction_arrow_action_toggles_canvas, test_lucide_icon_names_are_declared, test_table_hides_empty_line_index_columns, test_sidebar_collapses_and_expands, @@ -794,6 +796,37 @@ def test_canvas_draws_direction_arrows_on_connections() -> None: assert sum(path.elementCount() for path in arrow_paths) >= 6 +def test_canvas_can_toggle_direction_arrows() -> None: + if LedCanvas is None or QApplication is None: + return + app = QApplication.instance() or QApplication([]) + with tempfile.TemporaryDirectory() as temp_dir: + path = Path(temp_dir) / "arrows-toggle.json" + points = [ + {"id": 0, "x": 0.0, "y": 0.0, "row": 0, "col": 0}, + {"id": 1, "x": 34.0, "y": 0.0, "row": 0, "col": 1}, + {"id": 2, "x": 68.0, "y": 0.0, "row": 0, "col": 2}, + ] + path.write_text(json.dumps({"circles": points}), encoding="utf-8") + doc = MappingDocument() + doc.load(path) + line = doc.add_line() + doc.add_block(line.id, [0, 1, 2]) + + canvas = LedCanvas() + canvas.set_document(doc) + canvas.set_active_line(line.id) + assert any(item.zValue() == 2 for item in canvas.connection_items) + + canvas.set_direction_arrows_visible(False) + assert canvas.direction_arrows_visible() is False + assert not any(item.zValue() == 2 for item in canvas.connection_items) + + canvas.set_direction_arrows_visible(True) + assert canvas.direction_arrows_visible() is True + assert any(item.zValue() == 2 for item in canvas.connection_items) + + def test_canvas_rebuild_drops_deleted_selection_items() -> None: if LedCanvas is None or QApplication is None: return @@ -855,7 +888,7 @@ def test_classic_menu_contains_expected_actions() -> None: expected = { "Файл": ["Открыть", "Сохранить", "Сохранить как", "Сгенерировать скрипт"], "Правка": ["Отменить", "Повторить", "Удалить фиксацию", "Удалить все", "Очистить выбор"], - "Вид": ["Показать всё", "Сетка 34 мм"], + "Вид": ["Показать всё", "Сетка 34 мм", "Стрелки направления"], "Окно": ["Скрыть боковую панель"], "Инструменты": ["Новая линия", "Сгенерировать скрипт", "Автоблоки", "Кисть"], } @@ -950,6 +983,7 @@ def test_main_window_actions_use_lucide_icons() -> None: "clear_selection", "fit_view", "grid", + "direction_arrows", "toggle_sidebar", } missing = [key for key in expected_icon_actions if window.actions[key].icon().isNull()] @@ -970,6 +1004,7 @@ def test_main_window_action_shortcuts_are_assigned() -> None: "clear_selection": "Esc", "fit_view": "Ctrl+0", "grid": "G", + "direction_arrows": "Ctrl+Shift+A", "toggle_sidebar": "F9", "add_line": "Ctrl+L", "auto_blocks": "Ctrl+Shift+B", @@ -981,9 +1016,31 @@ def test_main_window_action_shortcuts_are_assigned() -> None: window.close() +def test_main_window_direction_arrow_action_toggles_canvas() -> None: + if MainWindow is None or QApplication is None: + return + app = QApplication.instance() or QApplication([]) + window = MainWindow() + app.processEvents() + + action = window.actions["direction_arrows"] + assert action.isChecked() + assert window.canvas.direction_arrows_visible() is True + + action.trigger() + assert not action.isChecked() + assert window.canvas.direction_arrows_visible() is False + + action.trigger() + assert action.isChecked() + assert window.canvas.direction_arrows_visible() is True + window.close() + + def test_lucide_icon_names_are_declared() -> None: expected_names = { "archive-restore", + "arrow-right", "chevron-right", "eraser", "eye",