From c3b98d7fe82f5410e161e2b1a7cd85600b02d0ad Mon Sep 17 00:00:00 2001 From: Danil Iarosh Date: Fri, 31 Jul 2026 14:54:27 +0300 Subject: [PATCH] Add new empty scheme creation action and support empty JSON load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлено действие Создать... (Ctrl+N) в меню Файл для инициализации нового файла схемы. - Реализована автоматическая запись дефолтной схемы circles при открытии пустого или содержащего только пробелы json-файла. - Добавлены соответствующие автоматические тесты в qa_smoke.py. --- main_window.py | 39 +++++++++++++++++++++++++++++++ models.py | 10 ++++++++ qa_smoke.py | 62 +++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 110 insertions(+), 1 deletion(-) diff --git a/main_window.py b/main_window.py index 2686a15..6b0940d 100644 --- a/main_window.py +++ b/main_window.py @@ -336,6 +336,7 @@ class MainWindow(QMainWindow): self.actions[key] = action return action + add_action("new", "Создать...", self.new_file, "file-plus", QKeySequence.New) add_action("open", "Открыть", self.open_file, "folder-open", QKeySequence.Open) add_action("save", "Сохранить", self.save_file, "save", QKeySequence.Save) add_action("save_as", "Сохранить как", self.save_file_as, "save-all", QKeySequence.SaveAs) @@ -446,6 +447,7 @@ class MainWindow(QMainWindow): menu_bar = self.menuBar() menu_bar.clear() file_menu = menu_bar.addMenu("Файл") + file_menu.addAction(self.actions["new"]) file_menu.addAction(self.actions["open"]) file_menu.addAction(self.actions["save"]) file_menu.addAction(self.actions["save_as"]) @@ -904,6 +906,43 @@ class MainWindow(QMainWindow): action.setText("Скрыть боковую панель") action.setToolTip("Скрыть боковую панель") + def new_file(self) -> None: + if not self._confirm_discard_changes(): + return + path, _ = QFileDialog.getSaveFileName( + self, + "Создать новую схему", + str(APP_DIR), + "JSON files (*.json);;All files (*.*)", + ) + if not path: + return + p = Path(path) + if not p.suffix: + p = p.with_suffix(".json") + try: + p.write_text('{"circles": []}', encoding="utf-8") + except Exception as exc: + show_critical(self, "Ошибка создания файла", str(exc)) + return + loaded = MappingDocument() + try: + loaded.load(p) + except Exception as exc: + show_critical(self, "Ошибка загрузки нового файла", str(exc)) + return + self.document = loaded + self._dirty = False + self._undo_stack.clear() + self._redo_stack.clear() + self.active_line_id = None + self.canvas.set_document(self.document) + self.canvas.set_active_line(self.active_line_id) + self.refresh_tree() + self._sync_canvas_to_active_line() + self.status.setText(f"Создана новая пустая схема: {p}") + self._refresh_ui_state() + def open_file(self) -> None: if not self._confirm_discard_changes(): return diff --git a/models.py b/models.py index 9a75b4c..df07c37 100644 --- a/models.py +++ b/models.py @@ -43,6 +43,16 @@ _LED_POINT_KNOWN_FIELDS = { def load_json_document(path: str | Path) -> dict[str, Any]: path = Path(path) + if path.exists(): + try: + if path.stat().st_size == 0: + path.write_text('{"circles": []}', encoding="utf-8") + else: + content = path.read_bytes() + if not content.strip(): + path.write_text('{"circles": []}', encoding="utf-8") + except Exception: + pass text, was_converted = _read_text_as_utf8(path) if was_converted: path.write_text(text, encoding="utf-8") diff --git a/qa_smoke.py b/qa_smoke.py index 1f48948..23a83ee 100644 --- a/qa_smoke.py +++ b/qa_smoke.py @@ -69,6 +69,13 @@ except ImportError: QDialogButtonBox = None QGraphicsItem = None QMessageBox = None + QColor = None + QImage = None + QKeySequence = None + QApplication = None + QDialogButtonBox = None + QGraphicsItem = None + QMessageBox = None QToolBar = None @@ -77,6 +84,7 @@ PROJECT_ROOT = Path(__file__).resolve().parent.parent def main() -> None: tests: list[Callable[[], None]] = [ + test_load_empty_json_file, test_load_supported_json_files, test_load_raw_json_with_trailing_commas_and_bare_decimal_points, test_non_utf8_json_is_converted_to_utf8_on_load, @@ -121,6 +129,7 @@ def main() -> None: test_panel_mapping_generation_with_strategies, test_block_renaming_and_naming_strategy, test_line_renaming_and_tree_shortcuts, + test_new_file_action, test_export_mapping_to_image, test_save_preserves_unknown_source_fields, test_ordered_json_preserves_document_and_remaps_uids, @@ -221,6 +230,25 @@ def main() -> None: print(f"All {len(tests)} QA smoke checks passed.") +def test_load_empty_json_file() -> None: + with tempfile.TemporaryDirectory() as temp_dir: + # Test 1: Completely empty file (0 bytes) + path = Path(temp_dir) / "empty.json" + path.write_bytes(b"") + doc = MappingDocument() + doc.load(path) + assert len(doc.points) == 0 + assert path.read_text(encoding="utf-8") == '{"circles": []}' + + # Test 2: Whitespace-only file + path_ws = Path(temp_dir) / "whitespace.json" + path_ws.write_text(" \n \t ", encoding="utf-8") + doc_ws = MappingDocument() + doc_ws.load(path_ws) + assert len(doc_ws.points) == 0 + assert path_ws.read_text(encoding="utf-8") == '{"circles": []}' + + def test_load_supported_json_files() -> None: fixtures = [ PROJECT_ROOT / "circles.json", @@ -1132,6 +1160,31 @@ def test_line_renaming_and_tree_shortcuts() -> None: window.close() +def test_new_file_action() -> None: + if MainWindow is None or QApplication is None: + return + app = QApplication.instance() or QApplication([]) + window = MainWindow() + + from PySide6.QtWidgets import QFileDialog + with tempfile.TemporaryDirectory() as temp_dir: + new_path = Path(temp_dir) / "created_scheme.json" + + original_getSaveFileName = QFileDialog.getSaveFileName + QFileDialog.getSaveFileName = lambda *args, **kwargs: (str(new_path), "JSON files (*.json)") + try: + window.new_file() + finally: + QFileDialog.getSaveFileName = original_getSaveFileName + + assert new_path.exists() + assert new_path.stat().st_size > 0 + assert len(window.document.points) == 0 + assert window.document.path == new_path + + window.close() + + def test_export_mapping_to_image() -> None: if QApplication is None: @@ -1856,6 +1909,11 @@ def test_canvas_refresh_does_not_accumulate_connection_items() -> None: assert len(canvas.items_by_id) == len(doc.points) +def test_main_window_has_classic_menu_bar() -> None: + if MainWindow is None or QApplication is None: + return + app = QApplication.instance() or QApplication([]) + window = MainWindow() def test_main_window_has_classic_menu_bar() -> None: if MainWindow is None or QApplication is None: return @@ -1877,7 +1935,7 @@ def test_classic_menu_contains_expected_actions() -> None: app.processEvents() expected = { - "Файл": ["Открыть", "Сохранить", "Сохранить как", "Сгенерировать скрипт", "Параметры"], + "Файл": ["Создать...", "Открыть", "Сохранить", "Сохранить как", "Сгенерировать скрипт", "Параметры"], "Правка": ["Отменить", "Повторить", "Удалить фиксацию", "Удалить точки", "Удалить все", "Очистить выбор"], "Вид": [ "Показать всё", @@ -1974,6 +2032,7 @@ def test_main_window_actions_use_lucide_icons() -> None: app.processEvents() expected_icon_actions = { + "new", "open", "save", "save_as", @@ -2017,6 +2076,7 @@ def test_main_window_action_shortcuts_are_assigned() -> None: app.processEvents() expected = { + "new": "Ctrl+N", "generate_script": "Ctrl+G", "settings": "Ctrl+,", "delete_points": "Ctrl+Del",