feat: add LED copy and paste in edit mode
- добавить команды копирования и вставки выбранных светодиодов в режиме редактирования - назначить на новые команды настраиваемые сочетания Ctrl+C и Ctrl+V - создавать свободные копии с уникальными идентификаторами и каскадным смещением по шагу сетки - сохранять взаимное расположение и пользовательские поля копируемых точек - объединить вставку группы в одну операцию Undo с восстановлением выделения через Redo - не перехватывать стандартное копирование и вставку при редактировании текста и координат - добавить пункты меню, иконки, документацию и проверки, расширив QA до 164 сценариев
This commit is contained in:
parent
0d4552ef35
commit
0b24cbd83c
@ -89,6 +89,8 @@ python -m venv .venv
|
||||
|
||||
Чтобы удалить несколько блоков, выделите их в дереве с `Ctrl` или `Shift`, затем нажмите `Ctrl+Del` или выберите удаление в контекстном меню. Приложение показывает одно подтверждение и создаёт одну операцию Undo.
|
||||
|
||||
В режиме редактирования выбранные светодиоды можно скопировать через `Ctrl+C` и вставить через `Ctrl+V`. Вставленные LED остаются свободными, сохраняют взаимное расположение и пользовательские поля исходных точек. Первая копия смещается на один шаг сетки вправо и вниз, а следующие вставки образуют каскад. Вся группа вставляется одной операцией Undo и сразу становится текущим выделением.
|
||||
|
||||
Если не назначен ни один светодиод, генерация не запускается. Если назначены не все светодиоды, приложение показывает предупреждение и предлагает либо сгенерировать неполный маппинг, либо продолжить редактирование.
|
||||
|
||||
## Горячие клавиши
|
||||
@ -101,6 +103,8 @@ python -m venv .venv
|
||||
| Сгенерировать скрипт | `Ctrl+G` |
|
||||
| Отменить | `Ctrl+Z` |
|
||||
| Повторить | `Ctrl+Y` |
|
||||
| Копировать выбранные LED | `Ctrl+C` |
|
||||
| Вставить скопированные LED | `Ctrl+V` |
|
||||
| Удалить фиксацию | `Del` |
|
||||
| Удалить выбранные точки или блоки | `Ctrl+Del` |
|
||||
| Удалить все | `Ctrl+Shift+Del` |
|
||||
|
||||
2
icons.py
2
icons.py
@ -10,6 +10,8 @@ LUCIDE_PATHS = {
|
||||
"arrow-right": '<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>',
|
||||
"brush": '<path d="m9.06 11.9 8.07-8.06a2.85 2.85 0 1 1 4.03 4.03l-8.06 8.08"/><path d="M7.07 14.94c-1.66 0-3 1.35-3 3.01 0 1.1-.89 2-2 2 1.4 1.4 3.67 1.4 5.07 0l2.94-2.94a2.93 2.93 0 0 0-4.14-4.14z"/>',
|
||||
"chevron-right": '<path d="m9 18 6-6-6-6"/>',
|
||||
"clipboard-paste": '<rect width="8" height="4" x="8" y="2" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v5M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6"/><path d="m16 19 3-3-3-3"/><path d="M19 16h-7"/>',
|
||||
"copy": '<rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>',
|
||||
"eraser": '<path d="m7 21-4.3-4.3c-.9-.9-.9-2.4 0-3.3L14 2l8 8-11 11Z"/><path d="M22 21H7"/><path d="m5 11 8 8"/>',
|
||||
"eye": '<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>',
|
||||
"file-plus": '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><path d="M14 2v6h6"/><path d="M12 18v-6"/><path d="M9 15h6"/>',
|
||||
|
||||
@ -54,7 +54,7 @@ try:
|
||||
show_information,
|
||||
show_warning,
|
||||
)
|
||||
from .models import LedBlock, LedLine, MappingDocument
|
||||
from .models import LedBlock, LedLine, LedPoint, MappingDocument
|
||||
from .panel_mapping_generator import generate_panel_mapping_from_document
|
||||
from .settings import DEFAULT_SHORTCUTS, AppSettings
|
||||
from .settings_dialog import SettingsDialog
|
||||
@ -73,7 +73,7 @@ except ImportError:
|
||||
show_information,
|
||||
show_warning,
|
||||
)
|
||||
from models import LedBlock, LedLine, MappingDocument
|
||||
from models import LedBlock, LedLine, LedPoint, MappingDocument
|
||||
from panel_mapping_generator import generate_panel_mapping_from_document
|
||||
from settings import DEFAULT_SHORTCUTS, AppSettings
|
||||
from settings_dialog import SettingsDialog
|
||||
@ -204,6 +204,8 @@ class MainWindow(QMainWindow):
|
||||
self._undo_stack: list[tuple[MappingDocument, int | None, list[int], bool]] = []
|
||||
self._redo_stack: list[tuple[MappingDocument, int | None, list[int], bool]] = []
|
||||
self._pending_background_undo_state: tuple[MappingDocument, int | None, list[int], bool] | None = None
|
||||
self._copied_led_points: list[LedPoint] = []
|
||||
self._paste_sequence = 0
|
||||
self.settings = AppSettings.load()
|
||||
self.document.grid_step_mm = self.settings.grid_step_mm
|
||||
apply_app_theme(self.settings.theme)
|
||||
@ -384,6 +386,22 @@ class MainWindow(QMainWindow):
|
||||
brush_action.setCheckable(True)
|
||||
add_action("undo", "Отменить", self.undo_last_change, "undo-2", QKeySequence.Undo)
|
||||
add_action("redo", "Повторить", self.redo_last_change, "redo-2", QKeySequence.Redo)
|
||||
add_action(
|
||||
"copy_leds",
|
||||
"Копировать светодиоды",
|
||||
self.copy_selected_leds,
|
||||
"copy",
|
||||
QKeySequence.Copy,
|
||||
"Скопировать выбранные LED в режиме редактирования",
|
||||
)
|
||||
add_action(
|
||||
"paste_leds",
|
||||
"Вставить светодиоды",
|
||||
self.paste_copied_leds,
|
||||
"clipboard-paste",
|
||||
QKeySequence.Paste,
|
||||
"Вставить свободные копии LED со смещением на шаг сетки",
|
||||
)
|
||||
self.delete_action = add_action(
|
||||
"delete",
|
||||
"Удалить фиксацию",
|
||||
@ -478,6 +496,9 @@ class MainWindow(QMainWindow):
|
||||
edit_menu.addAction(self.actions["undo"])
|
||||
edit_menu.addAction(self.actions["redo"])
|
||||
edit_menu.addSeparator()
|
||||
edit_menu.addAction(self.actions["copy_leds"])
|
||||
edit_menu.addAction(self.actions["paste_leds"])
|
||||
edit_menu.addSeparator()
|
||||
edit_menu.addAction(self.actions["delete"])
|
||||
edit_menu.addAction(self.actions["delete_points"])
|
||||
edit_menu.addAction(self.actions["clear_all"])
|
||||
@ -1120,9 +1141,13 @@ class MainWindow(QMainWindow):
|
||||
self.canvas.set_instant_space_pan(self.settings.instant_space_pan)
|
||||
|
||||
def eventFilter(self, obj, event) -> bool: # noqa: ANN001, N802
|
||||
if event.type() == QEvent.Type.ShortcutOverride and self._is_tree_bulk_delete_event(event):
|
||||
event.accept()
|
||||
return self.remove_selected_tree_item()
|
||||
if event.type() == QEvent.Type.ShortcutOverride:
|
||||
if self._text_input_owns_copy_paste(event):
|
||||
event.accept()
|
||||
return True
|
||||
if self._is_tree_bulk_delete_event(event):
|
||||
event.accept()
|
||||
return self.remove_selected_tree_item()
|
||||
if event.type() == QEvent.Type.KeyPress and not event.isAutoRepeat():
|
||||
if self._trigger_layout_independent_shortcut(event):
|
||||
return True
|
||||
@ -1147,6 +1172,8 @@ class MainWindow(QMainWindow):
|
||||
modifiers = event.modifiers()
|
||||
if isinstance(focus, QKeySequenceEdit):
|
||||
return False
|
||||
if self._text_input_owns_copy_paste(event):
|
||||
return False
|
||||
text_input_has_focus = isinstance(focus, (QLineEdit, QTextEdit, QPlainTextEdit, QKeySequenceEdit))
|
||||
if text_input_has_focus and not (modifiers & (Qt.ControlModifier | Qt.AltModifier)):
|
||||
return False
|
||||
@ -1161,6 +1188,13 @@ class MainWindow(QMainWindow):
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _text_input_owns_copy_paste(event) -> bool: # noqa: ANN001
|
||||
focus = QApplication.focusWidget()
|
||||
if not isinstance(focus, (QLineEdit, QTextEdit, QPlainTextEdit)):
|
||||
return False
|
||||
return event.matches(QKeySequence.StandardKey.Copy) or event.matches(QKeySequence.StandardKey.Paste)
|
||||
|
||||
def _physical_key_sequence(self, event) -> str: # noqa: ANN001
|
||||
key_name = physical_key_name(event)
|
||||
if not key_name:
|
||||
@ -1652,6 +1686,56 @@ class MainWindow(QMainWindow):
|
||||
self.status.setText(f"Добавлен свободный LED ID {point.source_id}: X={point.x:.3f}, Y={point.y:.3f}.")
|
||||
self._refresh_ui_state()
|
||||
|
||||
def copy_selected_leds(self) -> bool:
|
||||
if not self._edit_mode_is_enabled():
|
||||
self.status.setText("Копирование LED доступно только в режиме редактирования.")
|
||||
self._refresh_ui_state()
|
||||
return False
|
||||
point_ids = self.canvas.selected_point_ids()
|
||||
if not point_ids:
|
||||
self.status.setText("Выберите светодиоды для копирования.")
|
||||
self._refresh_ui_state()
|
||||
return False
|
||||
self._copied_led_points = [deepcopy(self.document.get_point(point_id)) for point_id in point_ids]
|
||||
self._paste_sequence = 0
|
||||
self.status.setText(f"Скопировано светодиодов: {len(self._copied_led_points)}.")
|
||||
self._refresh_ui_state(point_ids)
|
||||
return True
|
||||
|
||||
def paste_copied_leds(self) -> bool:
|
||||
if not self._edit_mode_is_enabled():
|
||||
self.status.setText("Вставка LED доступна только в режиме редактирования.")
|
||||
self._refresh_ui_state()
|
||||
return False
|
||||
if not self._copied_led_points:
|
||||
self.status.setText("Сначала скопируйте выбранные светодиоды.")
|
||||
self._refresh_ui_state()
|
||||
return False
|
||||
self._push_undo()
|
||||
self._paste_sequence += 1
|
||||
offset = float(self.document.grid_step_in_document_units()) * self._paste_sequence
|
||||
pasted = self.document.add_point_copies(
|
||||
self._copied_led_points,
|
||||
offset_x=offset,
|
||||
offset_y=-offset,
|
||||
)
|
||||
pasted_ids = [point.uid for point in pasted]
|
||||
self._dirty = True
|
||||
self.canvas.set_document(self.document, reset_view=False)
|
||||
self.canvas.select_points(pasted_ids)
|
||||
self.refresh_tree()
|
||||
self._on_selection_changed(pasted_ids)
|
||||
self.status.setText(
|
||||
f"Вставлено свободных светодиодов: {len(pasted)}. "
|
||||
f"Смещение: {self._paste_sequence} шаг(а) сетки вправо и вниз."
|
||||
)
|
||||
self._refresh_ui_state(pasted_ids)
|
||||
return True
|
||||
|
||||
def _edit_mode_is_enabled(self) -> bool:
|
||||
action = self.actions.get("toggle_edit_mode")
|
||||
return bool(action is not None and action.isChecked())
|
||||
|
||||
def edit_point_coordinates(self, point_id: int) -> None:
|
||||
if not 0 <= point_id < len(self.document.points):
|
||||
return
|
||||
@ -2683,6 +2767,7 @@ class MainWindow(QMainWindow):
|
||||
has_visible_background = has_loaded_background and self.document.background.visible
|
||||
has_active_line = self._get_line_or_none(self.active_line_id) is not None
|
||||
has_selection = bool(point_ids)
|
||||
edit_mode_enabled = self._edit_mode_is_enabled()
|
||||
selected_has_assignment = any(
|
||||
self.document.get_point(point_id).line_id is not None
|
||||
or self.document.get_point(point_id).block_id is not None
|
||||
@ -2716,6 +2801,8 @@ class MainWindow(QMainWindow):
|
||||
"generate_script": has_points,
|
||||
"undo": bool(self._undo_stack),
|
||||
"redo": bool(self._redo_stack),
|
||||
"copy_leds": edit_mode_enabled and has_selection,
|
||||
"paste_leds": edit_mode_enabled and bool(self._copied_led_points),
|
||||
"add_line": has_points and len(self.document.lines) < MAX_LINES,
|
||||
"add_led": True,
|
||||
"add_block": has_points and has_active_line,
|
||||
|
||||
32
models.py
32
models.py
@ -10,6 +10,7 @@ import os
|
||||
import re
|
||||
import tempfile
|
||||
from collections import deque
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass, field, replace
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@ -583,6 +584,37 @@ class MappingDocument:
|
||||
self.points.append(point)
|
||||
return point
|
||||
|
||||
def add_point_copies(
|
||||
self,
|
||||
templates: list[LedPoint],
|
||||
*,
|
||||
offset_x: float,
|
||||
offset_y: float,
|
||||
) -> list[LedPoint]:
|
||||
if not templates:
|
||||
return []
|
||||
next_source_id = max((point.source_id for point in self.points), default=-1) + 1
|
||||
copies: list[LedPoint] = []
|
||||
for template in list(templates):
|
||||
x = float(template.x) + float(offset_x)
|
||||
y = float(template.y) + float(offset_y)
|
||||
row, col = self._grid_cell_for_coordinate(x, y)
|
||||
point = LedPoint(
|
||||
uid=len(self.points),
|
||||
source_id=next_source_id,
|
||||
x=x,
|
||||
y=y,
|
||||
row=row,
|
||||
col=col,
|
||||
coordinate_unit=self._coordinate_unit_for_save(),
|
||||
manual=True,
|
||||
extra_payload=deepcopy(template.extra_payload),
|
||||
)
|
||||
self.points.append(point)
|
||||
copies.append(point)
|
||||
next_source_id += 1
|
||||
return copies
|
||||
|
||||
def update_point_position(self, point_id: int, x: float | None = None, y: float | None = None) -> None:
|
||||
point = self.get_point(point_id)
|
||||
if x is not None:
|
||||
|
||||
160
qa_smoke.py
160
qa_smoke.py
@ -10,6 +10,7 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
@ -42,7 +43,7 @@ try:
|
||||
from PySide6.QtCore import QPoint, QPointF, QRectF, Qt
|
||||
from PySide6.QtGui import QColor, QImage, QKeySequence
|
||||
from PySide6.QtTest import QTest
|
||||
from PySide6.QtWidgets import QApplication, QDialogButtonBox, QGraphicsItem, QMessageBox, QToolBar
|
||||
from PySide6.QtWidgets import QApplication, QDialogButtonBox, QGraphicsItem, QLineEdit, QMessageBox, QToolBar
|
||||
except ImportError:
|
||||
canvas_module = None
|
||||
LedCanvas = None
|
||||
@ -73,6 +74,7 @@ except ImportError:
|
||||
QApplication = None
|
||||
QDialogButtonBox = None
|
||||
QGraphicsItem = None
|
||||
QLineEdit = None
|
||||
QTest = None
|
||||
QMessageBox = None
|
||||
QColor = None
|
||||
@ -110,6 +112,7 @@ def main() -> None:
|
||||
test_legacy_line_without_block_id_is_preserved,
|
||||
test_points_without_grid_fields_load,
|
||||
test_manual_led_save_load_roundtrip,
|
||||
test_add_point_copies_preserves_geometry_and_metadata,
|
||||
test_coordinate_unit_and_exact_positions_survive_span_change,
|
||||
test_failed_atomic_save_preserves_existing_file,
|
||||
test_grid_step_roundtrip_and_meter_layout,
|
||||
@ -144,6 +147,7 @@ def main() -> None:
|
||||
test_line_renaming_and_tree_shortcuts,
|
||||
test_new_file_action,
|
||||
test_scheme_editing_mode_actions,
|
||||
test_copy_paste_selected_leds_in_edit_mode,
|
||||
test_menu_bar_tools_buttons,
|
||||
test_export_mapping_to_image,
|
||||
test_save_preserves_unknown_source_fields,
|
||||
@ -193,6 +197,7 @@ def main() -> None:
|
||||
test_theme_resolution_and_canvas_apply,
|
||||
test_menu_disabled_items_are_visually_distinct,
|
||||
test_shortcut_settings_are_editable_and_layout_independent,
|
||||
test_copy_paste_shortcuts_preserve_text_editing,
|
||||
test_standard_dialog_buttons_are_localized,
|
||||
test_lucide_icon_names_are_declared,
|
||||
test_table_uses_single_line_index_column,
|
||||
@ -668,6 +673,37 @@ def test_manual_led_save_load_roundtrip() -> None:
|
||||
assert loaded.points[0].block_id is None
|
||||
|
||||
|
||||
def test_add_point_copies_preserves_geometry_and_metadata() -> None:
|
||||
doc = MappingDocument()
|
||||
doc.coordinate_unit = "mm"
|
||||
doc.grid_step_mm = 10.0
|
||||
doc.points = [
|
||||
LedPoint(0, 10, 0.0, 0.0, 0, 0, extra_payload={"custom": {"value": 1}}),
|
||||
LedPoint(1, 20, 10.0, 5.0, 1, 1, line_id=1, block_id=1),
|
||||
]
|
||||
copies = doc.add_point_copies(doc.points[:], offset_x=20.0, offset_y=-10.0)
|
||||
|
||||
assert [point.uid for point in copies] == [2, 3]
|
||||
assert [point.source_id for point in copies] == [21, 22]
|
||||
assert [(point.x, point.y) for point in copies] == [(20.0, -10.0), (30.0, -5.0)]
|
||||
assert copies[1].x - copies[0].x == 10.0
|
||||
assert copies[1].y - copies[0].y == 5.0
|
||||
assert all(point.manual for point in copies)
|
||||
assert all(point.line_id is None and point.block_id is None for point in copies)
|
||||
assert copies[0].extra_payload == doc.points[0].extra_payload
|
||||
copies[0].extra_payload["custom"]["value"] = 2
|
||||
assert doc.points[0].extra_payload["custom"]["value"] == 1
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
saved = Path(temp_dir) / "copied-points.json"
|
||||
doc.save(saved)
|
||||
reloaded = MappingDocument()
|
||||
reloaded.load(saved)
|
||||
assert [(point.x, point.y) for point in reloaded.points[2:4]] == [(20.0, -10.0), (30.0, -5.0)]
|
||||
assert [point.source_id for point in reloaded.points[2:4]] == [21, 22]
|
||||
assert all(point.manual for point in reloaded.points[2:4])
|
||||
assert reloaded.points[2].extra_payload["custom"]["value"] == 2
|
||||
|
||||
|
||||
def test_coordinate_unit_and_exact_positions_survive_span_change() -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
source = Path(temp_dir) / "legacy_without_unit.json"
|
||||
@ -1497,6 +1533,71 @@ def test_scheme_editing_mode_actions() -> None:
|
||||
window.close()
|
||||
|
||||
|
||||
def test_copy_paste_selected_leds_in_edit_mode() -> None:
|
||||
if MainWindow is None or QApplication is None:
|
||||
return
|
||||
app = QApplication.instance() or QApplication([])
|
||||
window = MainWindow()
|
||||
doc = MappingDocument()
|
||||
doc.coordinate_unit = "mm"
|
||||
doc.grid_step_mm = 10.0
|
||||
doc.points = [
|
||||
LedPoint(0, 0, 10.0, 20.0, 0, 0, extra_payload={"label": "A"}),
|
||||
LedPoint(1, 1, 15.0, 10.0, 1, 1, extra_payload={"label": "B"}),
|
||||
LedPoint(2, 2, 30.0, 40.0, 2, 2),
|
||||
]
|
||||
line = doc.add_line()
|
||||
doc.add_block(line.id, [0, 1])
|
||||
window.document = doc
|
||||
window.canvas.set_document(doc, reset_view=True)
|
||||
window._refresh_ui_state()
|
||||
|
||||
window.canvas.select_points([0, 1])
|
||||
assert not window.actions["copy_leds"].isEnabled()
|
||||
assert not window.actions["paste_leds"].isEnabled()
|
||||
|
||||
edit_action = window.actions["toggle_edit_mode"]
|
||||
edit_action.setChecked(True)
|
||||
window.toggle_edit_mode()
|
||||
assert window.actions["copy_leds"].isEnabled()
|
||||
assert not window.actions["paste_leds"].isEnabled()
|
||||
assert window.copy_selected_leds()
|
||||
assert window.actions["paste_leds"].isEnabled()
|
||||
|
||||
assert window.paste_copied_leds()
|
||||
assert len(window.document.points) == 5
|
||||
assert window.canvas.selected_point_ids() == [3, 4]
|
||||
first_paste = window.document.points[3:5]
|
||||
assert [(point.x, point.y) for point in first_paste] == [(20.0, 10.0), (25.0, 0.0)]
|
||||
assert all(point.manual for point in first_paste)
|
||||
assert all(point.line_id is None and point.block_id is None for point in first_paste)
|
||||
assert [point.source_id for point in first_paste] == [3, 4]
|
||||
assert [point.extra_payload.get("label") for point in first_paste] == ["A", "B"]
|
||||
assert window.document.lines[0].blocks[0].point_ids == [0, 1]
|
||||
|
||||
assert window.paste_copied_leds()
|
||||
assert len(window.document.points) == 7
|
||||
assert window.canvas.selected_point_ids() == [5, 6]
|
||||
assert [(point.x, point.y) for point in window.document.points[5:7]] == [(30.0, 0.0), (35.0, -10.0)]
|
||||
|
||||
window.undo_last_change()
|
||||
assert len(window.document.points) == 5
|
||||
assert window.canvas.selected_point_ids() == [3, 4]
|
||||
window.undo_last_change()
|
||||
assert len(window.document.points) == 3
|
||||
assert window.canvas.selected_point_ids() == [0, 1]
|
||||
window.redo_last_change()
|
||||
assert len(window.document.points) == 5
|
||||
assert window.canvas.selected_point_ids() == [3, 4]
|
||||
|
||||
edit_action.setChecked(False)
|
||||
window.toggle_edit_mode()
|
||||
assert not window.actions["copy_leds"].isEnabled()
|
||||
assert not window.actions["paste_leds"].isEnabled()
|
||||
window._dirty = False
|
||||
window.close()
|
||||
|
||||
|
||||
def test_menu_bar_tools_buttons() -> None:
|
||||
if MainWindow is None or QApplication is None:
|
||||
return
|
||||
@ -2583,7 +2684,16 @@ def test_classic_menu_contains_expected_actions() -> None:
|
||||
|
||||
expected = {
|
||||
"Файл": ["Создать...", "Открыть", "Сохранить", "Сохранить как", "Экспортировать в изображение...", "Сгенерировать скрипт", "Параметры"],
|
||||
"Правка": ["Отменить", "Повторить", "Удалить фиксацию", "Удалить точки", "Удалить все", "Очистить выбор"],
|
||||
"Правка": [
|
||||
"Отменить",
|
||||
"Повторить",
|
||||
"Копировать светодиоды",
|
||||
"Вставить светодиоды",
|
||||
"Удалить фиксацию",
|
||||
"Удалить точки",
|
||||
"Удалить все",
|
||||
"Очистить выбор",
|
||||
],
|
||||
"Вид": [
|
||||
"Показать всё",
|
||||
"Сетка",
|
||||
@ -2695,6 +2805,8 @@ def test_main_window_actions_use_lucide_icons() -> None:
|
||||
"brush",
|
||||
"undo",
|
||||
"redo",
|
||||
"copy_leds",
|
||||
"paste_leds",
|
||||
"delete",
|
||||
"delete_points",
|
||||
"clear_all",
|
||||
@ -2728,6 +2840,8 @@ def test_main_window_action_shortcuts_are_assigned() -> None:
|
||||
"new": "Ctrl+N",
|
||||
"generate_script": "Ctrl+G",
|
||||
"settings": "Ctrl+,",
|
||||
"copy_leds": "Ctrl+C",
|
||||
"paste_leds": "Ctrl+V",
|
||||
"delete_points": "Ctrl+Del",
|
||||
"clear_all": "Shift+Del",
|
||||
"clear_selection": "Esc",
|
||||
@ -2904,6 +3018,46 @@ def test_shortcut_settings_are_editable_and_layout_independent() -> None:
|
||||
window.close()
|
||||
|
||||
|
||||
def test_copy_paste_shortcuts_preserve_text_editing() -> None:
|
||||
if QApplication is None or MainWindow is None or QLineEdit is None or QTest is None:
|
||||
return
|
||||
app = QApplication.instance() or QApplication([])
|
||||
window = MainWindow()
|
||||
doc = MappingDocument()
|
||||
doc.points = [LedPoint(0, 0, 0.0, 0.0, 0, 0)]
|
||||
window.document = doc
|
||||
window.canvas.set_document(doc)
|
||||
edit_action = window.actions["toggle_edit_mode"]
|
||||
edit_action.setChecked(True)
|
||||
window.toggle_edit_mode()
|
||||
window.canvas.select_points([0])
|
||||
|
||||
editor = QLineEdit("текст", window)
|
||||
editor.show()
|
||||
window.show()
|
||||
editor.setFocus()
|
||||
editor.selectAll()
|
||||
app.processEvents()
|
||||
window._copied_led_points = []
|
||||
QTest.keyClick(editor, Qt.Key_C, Qt.ControlModifier)
|
||||
app.processEvents()
|
||||
assert not window._copied_led_points
|
||||
assert app.clipboard().text() == "текст"
|
||||
|
||||
window._copied_led_points = [deepcopy(doc.points[0])]
|
||||
window._refresh_ui_state()
|
||||
app.clipboard().setText("вставка")
|
||||
editor.clear()
|
||||
editor.setFocus()
|
||||
app.processEvents()
|
||||
QTest.keyClick(editor, Qt.Key_V, Qt.ControlModifier)
|
||||
app.processEvents()
|
||||
assert editor.text() == "вставка"
|
||||
assert len(window.document.points) == 1
|
||||
window._dirty = False
|
||||
window.close()
|
||||
|
||||
|
||||
def test_standard_dialog_buttons_are_localized() -> None:
|
||||
if (
|
||||
QApplication is None
|
||||
@ -2933,6 +3087,8 @@ def test_lucide_icon_names_are_declared() -> None:
|
||||
"archive-restore",
|
||||
"arrow-right",
|
||||
"chevron-right",
|
||||
"clipboard-paste",
|
||||
"copy",
|
||||
"eraser",
|
||||
"eye",
|
||||
"file-plus",
|
||||
|
||||
@ -18,6 +18,8 @@ SHORTCUT_DEFINITIONS: list[tuple[str, str, str]] = [
|
||||
("settings", "Параметры", "Ctrl+,"),
|
||||
("undo", "Отменить", "Ctrl+Z"),
|
||||
("redo", "Повторить", "Ctrl+Y"),
|
||||
("copy_leds", "Копировать светодиоды", "Ctrl+C"),
|
||||
("paste_leds", "Вставить светодиоды", "Ctrl+V"),
|
||||
("delete", "Удалить фиксацию", "Del"),
|
||||
("delete_points", "Удалить точки", "Ctrl+Del"),
|
||||
("clear_all", "Удалить все", "Shift+Del"),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user