diff --git a/src/__init__.py b/src/__init__.py index e69de29..2e5f5ba 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -0,0 +1,10 @@ +"""Core package exposing primary interfaces for Spycer.""" + +try: # pragma: no cover - optional Qt dependencies + from .window import MainWindow + from .controller import MainController +except Exception: # when Qt is missing during lightweight imports + MainWindow = None # type: ignore + MainController = None # type: ignore + +__all__ = ["MainWindow", "MainController"] diff --git a/src/controller/__init__.py b/src/controller/__init__.py new file mode 100644 index 0000000..9612243 --- /dev/null +++ b/src/controller/__init__.py @@ -0,0 +1,5 @@ +"""Controller package exposing the main application controller.""" + +from .main import MainController + +__all__ = ["MainController"] diff --git a/src/controller/file_management.py b/src/controller/file_management.py new file mode 100644 index 0000000..c1b5cac --- /dev/null +++ b/src/controller/file_management.py @@ -0,0 +1,175 @@ +"""File-related controller mixins.""" + +import os +import shutil +from os import path +from pathlib import Path +from shutil import copy2 +import logging + +from PyQt5.QtWidgets import QFileDialog, QMessageBox + +from src import gui_utils, locales +from src.gui_utils import showErrorDialog +from src.process import Process +from src.settings import ( + sett, + load_settings, + PathBuilder, + create_temporary_project_files, + update_last_open_project, + get_recent_projects, + delete_temporary_project_files, +) + +logger = logging.getLogger(__name__) + + +class FileManagementMixin: + """Encapsulates file handling logic for controllers.""" + + # Methods below are copied from the legacy ``MainController`` + # to isolate responsibilities. + def open_file(self): + try: + filename = str(self.view.open_dialog(self.view.locale.OpenModel)) + if filename != "": + file_ext = os.path.splitext(filename)[1].upper() + filename = str(Path(filename)) + if file_ext == ".STL": + self.reset_settings() + s = sett() + stl_full_path = PathBuilder.stl_model_temp() + shutil.copyfile(filename, stl_full_path) + s.slicing.stl_filename = path.basename(filename) + s.slicing.stl_file = path.basename(stl_full_path) + self.save_settings("vip") + self.update_interface(filename) + self.view.model_centering_box.setChecked(False) + if os.path.isfile(s.colorizer.copy_stl_file): + os.remove(s.colorizer.copy_stl_file) + self.load_stl(stl_full_path) + elif file_ext == ".GCODE": + s = sett() + self.save_settings("vip") + self.load_gcode(filename, False) + self.update_interface(filename) + else: + showErrorDialog("This file format isn't supported:" + file_ext) + except IOError as e: + showErrorDialog("Error during file opening:" + str(e)) + + def save_gcode_file(self): + try: + name = str(self.view.save_gcode_dialog()) + if name != "": + if not name.endswith(".gcode"): + name += ".gcode" + copy2(PathBuilder.gcode_file(), name) + except IOError as e: + showErrorDialog("Error during file saving:" + str(e)) + + def save_settings_file(self): + try: + directory = ( + "Settings_" + os.path.basename(sett().slicing.stl_file).split(".")[0] + ) + filename = str( + self.view.save_dialog( + self.view.locale.SaveSettings, "YAML (*.yaml *.YAML)", directory + ) + ) + if filename != "": + if not (filename.endswith(".yaml") or filename.endswith(".YAML")): + filename += ".yaml" + self.save_settings("vip", filename) + except IOError as e: + showErrorDialog("Error during file saving:" + str(e)) + + def save_project_files(self, save_path=""): + if save_path == "": + self.save_settings("vip", PathBuilder.settings_file()) + if os.path.isfile(PathBuilder.stl_model_temp()): + shutil.copy2(PathBuilder.stl_model_temp(), PathBuilder.stl_model()) + else: + self.save_settings("vip", path.join(save_path, "settings.yaml")) + if os.path.isfile(PathBuilder.stl_model_temp()): + shutil.copy2( + PathBuilder.stl_model_temp(), path.join(save_path, "model.stl") + ) + + def save_project(self): + try: + self.save_project_files() + create_temporary_project_files() + self.successful_saving_project() + except IOError as e: + showErrorDialog("Error during project saving: " + str(e)) + + def save_project_as(self): + project_path = PathBuilder.project_path() + try: + save_directory = str( + QFileDialog.getExistingDirectory( + self.view, locales.getLocale().SavingProject + ) + ) + if not save_directory: + return + self.save_project_files(save_directory) + sett().project_path = save_directory + self.save_settings("vip", PathBuilder.settings_file()) + create_temporary_project_files() + delete_temporary_project_files(project_path) + recent_projects = get_recent_projects() + update_last_open_project(recent_projects, save_directory) + self.successful_saving_project() + except IOError as e: + sett().project_path = project_path + self.save_settings("vip") + showErrorDialog("Error during project saving: " + str(e)) + + def successful_saving_project(self): + message_box = QMessageBox(parent=self.view) + message_box.setWindowTitle(locales.getLocale().SavingProject) + message_box.setText(locales.getLocale().ProjectSaved) + message_box.setIcon(QMessageBox.Information) + message_box.exec_() + + def load_settings_file(self): + try: + filename = str( + self.view.open_dialog( + self.view.locale.LoadSettings, "YAML (*.yaml *.YAML)" + ) + ) + if filename != "": + file_ext = os.path.splitext(filename)[1].upper() + filename = str(Path(filename)) + if file_ext == ".YAML": + try: + old_project_path = sett().project_path + load_settings(filename) + sett().project_path = old_project_path + self.display_settings() + except Exception as e: + showErrorDialog("Error during reading settings file: " + str(e)) + else: + showErrorDialog("This file format isn't supported:" + file_ext) + except IOError as e: + showErrorDialog("Error during file opening:" + str(e)) + + def display_settings(self): + self.view.setts.reload() + + def colorize_model(self): + shutil.copyfile(PathBuilder.stl_model_temp(), PathBuilder.colorizer_stl()) + self.save_settings("vip", PathBuilder.settings_file_temp()) + p = Process(PathBuilder.colorizer_cmd()).wait() + if p.returncode: + logging.error(f"error: <{p.stdout}>") + gui_utils.showErrorDialog(p.stdout) + return + lastMove = self.view.stlActor.lastMove + self.load_stl(PathBuilder.colorizer_stl(), colorize=True) + self.view.stlActor.lastMove = lastMove diff --git a/src/controller/hardware.py b/src/controller/hardware.py new file mode 100644 index 0000000..40c225a --- /dev/null +++ b/src/controller/hardware.py @@ -0,0 +1,55 @@ +"""Hardware helpers for controllers.""" + +import logging + +from src.settings import PathBuilder + +logger = logging.getLogger(__name__) + + +def create_printer(): + """Load printer implementation lazily.""" + try: + from src.hardware import printer + + return printer.EpitPrinter() + except Exception as e: + logger.warning("printer is not initialized: %s", e) + return None + + +def create_service(view, printer): + """Instantiate service tool for the given printer.""" + if not printer: + return None, None + + try: + from src.hardware import service + + panel = service.ServicePanel(view) + panel.setModal(True) + controller = service.ServiceController(panel, service.ServiceModel(printer)) + return panel, controller + except Exception as e: + logger.warning("service tool is unavailable: %s", e) + return None, None + + +def create_calibration(view, printer): + """Instantiate calibration tool for the given printer.""" + if not printer: + return None, None + + try: + from src.hardware import calibration + + panel = calibration.CalibrationPanel(view) + panel.setModal(True) + controller = calibration.CalibrationController( + panel, + calibration.CalibrationModel(printer, PathBuilder.calibration_file()), + ) + return panel, controller + except Exception as e: + logger.warning("calibration tool is unavailable: %s", e) + return None, None diff --git a/src/controller.py b/src/controller/main.py similarity index 70% rename from src/controller.py rename to src/controller/main.py index a46ed83..847e5c8 100644 --- a/src/controller.py +++ b/src/controller/main.py @@ -4,17 +4,14 @@ from os import path import subprocess import time -import sys -from functools import partial from pathlib import Path import shutil -from shutil import copy2 from typing import Dict, List, Union from vtkmodules.vtkCommonMath import vtkMatrix4x4 import vtk from PyQt5 import QtCore -from PyQt5.QtCore import QSettings, QUrl +from PyQt5.QtCore import QUrl from PyQt5.QtWidgets import QFileDialog, QInputDialog, QMessageBox from PyQt5.QtGui import QDesktopServices @@ -33,16 +30,15 @@ sett, save_settings, save_splanes_to_file, - load_settings, get_color, PathBuilder, - create_temporary_project_files, - update_last_open_project, - get_recent_projects, - delete_temporary_project_files, ) import src.settings as settings +from .hardware import create_printer, create_service, create_calibration +from .ui_wiring import connect_signals +from .file_management import FileManagementMixin + logger = logging.getLogger(__name__) try: @@ -51,55 +47,7 @@ logger.warning("bug reporting is unavailable") -def create_printer(): - """Load printer implementation lazily.""" - try: - from src.hardware import printer - - return printer.EpitPrinter() - except Exception as e: - logger.warning("printer is not initialized: %s", e) - return None - - -def create_service(view, printer): - """Instantiate service tool for the given printer.""" - if not printer: - return None, None - - try: - from src.hardware import service - - panel = service.ServicePanel(view) - panel.setModal(True) - controller = service.ServiceController(panel, service.ServiceModel(printer)) - return panel, controller - except Exception as e: - logger.warning("service tool is unavailable: %s", e) - return None, None - - -def create_calibration(view, printer): - """Instantiate calibration tool for the given printer.""" - if not printer: - return None, None - - try: - from src.hardware import calibration - - panel = calibration.CalibrationPanel(view) - panel.setModal(True) - controller = calibration.CalibrationController( - panel, - calibration.CalibrationModel(printer, PathBuilder.calibration_file()), - ) - return panel, controller - except Exception as e: - logger.warning("calibration tool is unavailable: %s", e) - return None, None - - -class MainController: +class MainController(FileManagementMixin): def __init__(self, view, model, printer=None, service=None, calibration=None): self.view = view self.model = model @@ -126,69 +74,7 @@ def __init__(self, view, model, printer=None, service=None, calibration=None): self.bugReportDialog = bugReportDialog(self) except Exception: logger.warning("bug reporting is unavailable") - self._connect_signals() - - def _connect_signals(self): - self.view.open_action.triggered.connect(self.open_file) - self.view.save_gcode_action.triggered.connect(partial(self.save_gcode_file)) - self.view.save_sett_action.triggered.connect(self.save_settings_file) - self.view.save_project_action.triggered.connect(self.save_project) - self.view.save_project_as_action.triggered.connect(self.save_project_as) - self.view.load_sett_action.triggered.connect(self.load_settings_file) - self.view.slicing_info_action.triggered.connect(self.get_slicer_version) - self.view.documentation_action.triggered.connect(self.show_online_documentation) - self.view.check_updates_action.triggered.connect(self.open_updater) - - if self.calibrationPanel is not None: - self.view.calibration_action.triggered.connect(self.calibration_action_show) - else: - self.view.calibration_action.triggered.connect( - lambda: showInfoDialog(locales.getLocale().ErrorHardwareModule) - ) - - try: - self.view.bug_report.triggered.connect(self.bugReportDialog.show) - except: - self.view.bug_report.triggered.connect( - lambda: showInfoDialog(locales.getLocale().ErrorBugModule) - ) - - # right panel - self.view.setts.get_element("printer_path", "add_btn").clicked.connect( - self.create_printer - ) - self.view.setts.edit("printer_path").clicked.connect(self.choose_printer_path) - - self.view.model_switch_box.stateChanged.connect(self.view.switch_stl_gcode) - self.view.model_centering_box.stateChanged.connect(self.view.model_centering) - self.view.picture_slider.valueChanged.connect(self.change_layer_view) - self.view.move_button.clicked.connect(self.move_model) - self.view.place_button.clicked.connect(self.place_model) - self.view.cancel_action.clicked.connect(partial(self.view.shift_state, True)) - self.view.return_action.clicked.connect(partial(self.view.shift_state, False)) - self.view.load_model_button.clicked.connect(self.open_file) - self.view.slice3a_button.clicked.connect(partial(self.slice_stl, "3axes")) - self.view.slice_vip_button.clicked.connect(partial(self.slice_stl, "vip")) - self.view.save_gcode_button.clicked.connect(self.save_gcode_file) - self.view.color_model_button.clicked.connect(self.colorize_model) - - # bottom panel - self.view.add_plane_button.clicked.connect(self.add_splane) - self.view.add_cone_button.clicked.connect(self.add_cone) - self.view.edit_figure_button.clicked.connect(self.change_figure_parameters) - self.view.save_planes_button.clicked.connect(self.save_planes) - self.view.download_planes_button.clicked.connect(self.download_planes) - self.view.remove_plane_button.clicked.connect(self.remove_splane) - self.view.splanes_tree.itemClicked.connect(self.change_splanes_tree) - self.view.splanes_tree.itemChanged.connect(self.change_figure_check_state) - self.view.splanes_tree.currentItemChanged.connect(self.change_combo_select) - self.view.splanes_tree.model().rowsInserted.connect(self.moving_figure) - - self.view.hide_checkbox.stateChanged.connect(self.view.hide_splanes) - - # on close of window we save current planes to project file - self.view.before_closing_signal.connect(self.save_planes_on_close) - self.view.save_project_signal.connect(self.save_project) + connect_signals(self) def calibration_action_show(self): if not self.calibrationPanel: @@ -565,43 +451,6 @@ def move_model(self): def place_model(self): self.view.stlActor.ResetColorize() - def open_file(self): - try: - filename = str(self.view.open_dialog(self.view.locale.OpenModel)) - if filename != "": - file_ext = os.path.splitext(filename)[1].upper() - filename = str(Path(filename)) - if file_ext == ".STL": - self.reset_settings() - s = sett() - # copy stl file to project directory - - stl_full_path = PathBuilder.stl_model_temp() - shutil.copyfile(filename, stl_full_path) - # relative path inside project - s.slicing.stl_filename = path.basename(filename) - s.slicing.stl_file = path.basename(stl_full_path) - - self.save_settings("vip") - self.update_interface(filename) - - self.view.model_centering_box.setChecked(False) - - if os.path.isfile(s.colorizer.copy_stl_file): - os.remove(s.colorizer.copy_stl_file) - - self.load_stl(stl_full_path) - elif file_ext == ".GCODE": - s = sett() - # s.slicing.stl_file = filename # TODO optimize - self.save_settings("vip") - self.load_gcode(filename, False) - self.update_interface(filename) - else: - showErrorDialog("This file format isn't supported:" + file_ext) - except IOError as e: - showErrorDialog("Error during file opening:" + str(e)) - def reset_settings(self): s = sett() s.slicing.originx, s.slicing.originy, s.slicing.originz = 0, 0, 0 @@ -788,133 +637,6 @@ def save_settings(self, slicing_type, filename=""): if filename != "": save_settings(filename) - def save_gcode_file(self): - try: - name = str(self.view.save_gcode_dialog()) - if name != "": - if not name.endswith(".gcode"): - name += ".gcode" - copy2(PathBuilder.gcode_file(), name) - except IOError as e: - showErrorDialog("Error during file saving:" + str(e)) - - def save_settings_file(self): - try: - directory = ( - "Settings_" + os.path.basename(sett().slicing.stl_file).split(".")[0] - ) - filename = str( - self.view.save_dialog( - self.view.locale.SaveSettings, "YAML (*.yaml *.YAML)", directory - ) - ) - if filename != "": - if not (filename.endswith(".yaml") or filename.endswith(".YAML")): - filename += ".yaml" - self.save_settings("vip", filename) - except IOError as e: - showErrorDialog("Error during file saving:" + str(e)) - - def save_project_files(self, save_path=""): - if save_path == "": - self.save_settings("vip", PathBuilder.settings_file()) - if os.path.isfile(PathBuilder.stl_model_temp()): - shutil.copy2(PathBuilder.stl_model_temp(), PathBuilder.stl_model()) - else: - self.save_settings("vip", path.join(save_path, "settings.yaml")) - if os.path.isfile(PathBuilder.stl_model_temp()): - shutil.copy2( - PathBuilder.stl_model_temp(), path.join(save_path, "model.stl") - ) - - def save_project(self): - try: - self.save_project_files() - create_temporary_project_files() - self.successful_saving_project() - except IOError as e: - showErrorDialog("Error during project saving: " + str(e)) - - def save_project_as(self): - project_path = PathBuilder.project_path() - - try: - save_directory = str( - QFileDialog.getExistingDirectory( - self.view, locales.getLocale().SavingProject - ) - ) - - if not save_directory: - return - - self.save_project_files(save_directory) - sett().project_path = save_directory - self.save_settings("vip", PathBuilder.settings_file()) - create_temporary_project_files() - delete_temporary_project_files(project_path) - - recent_projects = get_recent_projects() - update_last_open_project(recent_projects, save_directory) - - self.successful_saving_project() - - except IOError as e: - sett().project_path = project_path - self.save_settings("vip") - showErrorDialog("Error during project saving: " + str(e)) - - def successful_saving_project(self): - message_box = QMessageBox(parent=self.view) - message_box.setWindowTitle(locales.getLocale().SavingProject) - message_box.setText(locales.getLocale().ProjectSaved) - message_box.setIcon(QMessageBox.Information) - message_box.exec_() - - def load_settings_file(self): - try: - filename = str( - self.view.open_dialog( - self.view.locale.LoadSettings, "YAML (*.yaml *.YAML)" - ) - ) - if filename != "": - file_ext = os.path.splitext(filename)[1].upper() - filename = str(Path(filename)) - if file_ext == ".YAML": - try: - # TODO: right now to maintain good transfer - # we need to copy the project_path setting manually - # everything else will work alright - old_project_path = sett().project_path - load_settings(filename) - sett().project_path = old_project_path - self.display_settings() - except Exception as e: - showErrorDialog("Error during reading settings file: " + str(e)) - else: - showErrorDialog("This file format isn't supported:" + file_ext) - except IOError as e: - showErrorDialog("Error during file opening:" + str(e)) - - def display_settings(self): - self.view.setts.reload() - - def colorize_model(self): - shutil.copyfile(PathBuilder.stl_model_temp(), PathBuilder.colorizer_stl()) - self.save_settings("vip", PathBuilder.settings_file_temp()) - - p = Process(PathBuilder.colorizer_cmd()).wait() - if p.returncode: - logging.error(f"error: <{p.stdout}>") - gui_utils.showErrorDialog(p.stdout) - return - - lastMove = self.view.stlActor.lastMove - self.load_stl(PathBuilder.colorizer_stl(), colorize=True) - self.view.stlActor.lastMove = lastMove - # self.model.opened_stl = s.slicing.stl_file - # ######################bottom panel def add_splane(self): diff --git a/src/controller/ui_wiring.py b/src/controller/ui_wiring.py new file mode 100644 index 0000000..7b93420 --- /dev/null +++ b/src/controller/ui_wiring.py @@ -0,0 +1,65 @@ +"""UI signal wiring for controllers.""" + +from functools import partial + +from src import locales +from src.gui_utils import showInfoDialog + + +def connect_signals(controller): + view = controller.view + view.open_action.triggered.connect(controller.open_file) + view.save_gcode_action.triggered.connect(partial(controller.save_gcode_file)) + view.save_sett_action.triggered.connect(controller.save_settings_file) + view.save_project_action.triggered.connect(controller.save_project) + view.save_project_as_action.triggered.connect(controller.save_project_as) + view.load_sett_action.triggered.connect(controller.load_settings_file) + view.slicing_info_action.triggered.connect(controller.get_slicer_version) + view.documentation_action.triggered.connect(controller.show_online_documentation) + view.check_updates_action.triggered.connect(controller.open_updater) + + if controller.calibrationPanel is not None: + view.calibration_action.triggered.connect(controller.calibration_action_show) + else: + view.calibration_action.triggered.connect( + lambda: showInfoDialog(locales.getLocale().ErrorHardwareModule) + ) + + try: + view.bug_report.triggered.connect(controller.bugReportDialog.show) + except Exception: + view.bug_report.triggered.connect( + lambda: showInfoDialog(locales.getLocale().ErrorBugModule) + ) + + view.setts.get_element("printer_path", "add_btn").clicked.connect( + controller.create_printer + ) + view.setts.edit("printer_path").clicked.connect(controller.choose_printer_path) + view.model_switch_box.stateChanged.connect(view.switch_stl_gcode) + view.model_centering_box.stateChanged.connect(view.model_centering) + view.picture_slider.valueChanged.connect(controller.change_layer_view) + view.move_button.clicked.connect(controller.move_model) + view.place_button.clicked.connect(controller.place_model) + view.cancel_action.clicked.connect(partial(view.shift_state, True)) + view.return_action.clicked.connect(partial(view.shift_state, False)) + view.load_model_button.clicked.connect(controller.open_file) + view.slice3a_button.clicked.connect(partial(controller.slice_stl, "3axes")) + view.slice_vip_button.clicked.connect(partial(controller.slice_stl, "vip")) + view.save_gcode_button.clicked.connect(controller.save_gcode_file) + view.color_model_button.clicked.connect(controller.colorize_model) + + view.add_plane_button.clicked.connect(controller.add_splane) + view.add_cone_button.clicked.connect(controller.add_cone) + view.edit_figure_button.clicked.connect(controller.change_figure_parameters) + view.save_planes_button.clicked.connect(controller.save_planes) + view.download_planes_button.clicked.connect(controller.download_planes) + view.remove_plane_button.clicked.connect(controller.remove_splane) + view.splanes_tree.itemClicked.connect(controller.change_splanes_tree) + view.splanes_tree.itemChanged.connect(controller.change_figure_check_state) + view.splanes_tree.currentItemChanged.connect(controller.change_combo_select) + view.splanes_tree.model().rowsInserted.connect(controller.moving_figure) + + view.hide_checkbox.stateChanged.connect(view.hide_splanes) + view.before_closing_signal.connect(controller.save_planes_on_close) + view.save_project_signal.connect(controller.save_project) diff --git a/src/window/__init__.py b/src/window/__init__.py new file mode 100644 index 0000000..1b392fc --- /dev/null +++ b/src/window/__init__.py @@ -0,0 +1,5 @@ +"""Window package providing the main application window.""" + +from .main import MainWindow, TreeWidget + +__all__ = ["MainWindow", "TreeWidget"] diff --git a/src/window/dialogs.py b/src/window/dialogs.py new file mode 100644 index 0000000..578fb6f --- /dev/null +++ b/src/window/dialogs.py @@ -0,0 +1,40 @@ +"""Dialog helpers for :mod:`src.window`.""" + +import os +import os.path as path +from PyQt5.QtWidgets import QFileDialog + +from src.settings import sett + + +def _base_dir(): + base_dir = getattr(sett(), "project_path", "") or os.getcwd() + if not base_dir or not path.isdir(base_dir): + base_dir = path.expanduser("~") + return base_dir + + +def save_dialog( + parent, caption, format="STL (*.stl *.STL);;Gcode (*.gcode)", directory="" +): + base_dir = _base_dir() + if directory: + directory = ( + directory if path.isabs(directory) else path.join(base_dir, directory) + ) + else: + directory = base_dir + return QFileDialog.getSaveFileName(parent, caption, directory, format)[0] + + +def open_dialog( + parent, caption, format="STL (*.stl *.STL);;Gcode (*.gcode)", directory="" +): + base_dir = _base_dir() + if directory: + directory = ( + directory if path.isabs(directory) else path.join(base_dir, directory) + ) + else: + directory = base_dir + return QFileDialog.getOpenFileName(parent, caption, directory, format)[0] diff --git a/src/window.py b/src/window/main.py similarity index 85% rename from src/window.py rename to src/window/main.py index a3c2daf..375656a 100644 --- a/src/window.py +++ b/src/window/main.py @@ -1,34 +1,29 @@ from typing import Optional import vtk -from PyQt5 import QtCore -from PyQt5 import QtGui +from PyQt5 import QtCore, QtGui from PyQt5.QtCore import Qt, QEvent from PyQt5.QtWidgets import ( QMainWindow, QWidget, QLabel, - QComboBox, QGridLayout, QSlider, QCheckBox, QVBoxLayout, QPushButton, - QFileDialog, QScrollArea, QGroupBox, - QAction, QDialog, + QFileDialog, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QTabWidget, QMessageBox, ) -from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor from src import locales, gui_utils -from src.InteractorAroundActivePlane import InteractionAroundActivePlane from src.gui_utils import plane_tf, Plane, Cone, showErrorDialog from src.settings import ( sett, @@ -39,8 +34,7 @@ ) from src.figure_editor import StlMovePanel from src.settings_widget import SettingsWidget -import os -import os.path as path +from . import menu, rendering, dialogs import logging NothingState = "nothing" @@ -112,46 +106,12 @@ def __init__(self, parent=None): self.locale = locales.getLocale() # Menu - bar = self.menuBar() - file_menu = bar.addMenu(self.locale.File) - self.open_action = QAction(self.locale.Open, self) - file_menu.addAction(self.open_action) - # file_menu.addAction(close_action) - - self.save_gcode_action = QAction(self.locale.SaveGCode, self) - self.save_project_action = QAction(self.locale.SaveProject, self) - file_menu.addAction(self.save_project_action) - self.save_project_as_action = QAction(self.locale.SaveProjectAs, self) - file_menu.addAction(self.save_project_as_action) - file_menu.addAction(self.save_gcode_action) - self.save_sett_action = QAction(self.locale.SaveSettings, self) - file_menu.addAction(self.save_sett_action) - self.load_sett_action = QAction(self.locale.LoadSettings, self) - file_menu.addAction(self.load_sett_action) - - self.slicing_info_action = QAction(self.locale.SlicerInfo, self) - file_menu.addAction(self.slicing_info_action) - - tools_menu = bar.addMenu(self.locale.Tools) - self.calibration_action = QAction(self.locale.Calibration, self) - tools_menu.addAction(self.calibration_action) - self.bug_report = QAction(self.locale.SubmitBugReport, self) - tools_menu.addAction(self.bug_report) - - self.check_updates_action = QAction(self.locale.CheckUpdates, self) - tools_menu.addAction(self.check_updates_action) - - help_menu = bar.addMenu(self.locale.Help) - self.slicing_info_action = QAction(self.locale.SlicerInfo, self) - help_menu.addAction(self.slicing_info_action) - - self.documentation_action = QAction(self.locale.Documentation, self) - help_menu.addAction(self.documentation_action) + menu.setup_menus(self) # main parts central_widget = QWidget() main_grid = QGridLayout() - self.widget3d = self.init3d_widget() + self.widget3d = rendering.init_3d_widget(self) main_grid.addWidget(self.widget3d, 0, 0, 20, 5) main_grid.addWidget(self.init_right_panel(), 0, 5, 21, 2) @@ -260,79 +220,6 @@ def projectChangeDialog(self): return message_box.exec() - def init3d_widget(self): - widget3d = QVTKRenderWindowInteractor(self) - widget3d.installEventFilter(self) - - self.render = vtk.vtkRenderer() - self.render.SetBackground(get_color("white")) - - widget3d.GetRenderWindow().AddRenderer(self.render) - self.interactor = widget3d.GetRenderWindow().GetInteractor() - self.interactor.SetInteractorStyle(None) - - self.interactor.Initialize() - self.interactor.Start() - - # self.render.ResetCamera() - # self.render.GetActiveCamera().AddObserver('ModifiedEvent', CameraModifiedCallback) - - # set position of camera to (5, 5, 5) and look at (0, 0, 0) and z-axis is looking up - self.render.GetActiveCamera().SetPosition(5, 5, 5) - self.render.GetActiveCamera().SetFocalPoint(0, 0, 0) - self.render.GetActiveCamera().SetViewUp(0, 0, 1) - - self.customInteractor = InteractionAroundActivePlane( - self.interactor, self.render - ) - self.interactor.AddObserver( - "MouseWheelBackwardEvent", self.customInteractor.middleBtnPress - ) - self.interactor.AddObserver( - "MouseWheelForwardEvent", self.customInteractor.middleBtnPress - ) - self.interactor.AddObserver( - "RightButtonPressEvent", self.customInteractor.rightBtnPress - ) - self.interactor.AddObserver( - "RightButtonReleaseEvent", self.customInteractor.rightBtnPress - ) - self.interactor.AddObserver( - "LeftButtonPressEvent", - lambda obj, event: self.customInteractor.leftBtnPress(obj, event, self), - ) - self.interactor.AddObserver( - "LeftButtonReleaseEvent", self.customInteractor.leftBtnPress - ) - self.interactor.AddObserver( - "MouseMoveEvent", - lambda obj, event: self.customInteractor.mouseMove(obj, event, self), - ) - - # self.actor_interactor_style = interactor_style.ActorInteractorStyle(self.updateTransform) - # self.actor_interactor_style.SetDefaultRenderer(self.render) - # self.interactor.SetInteractorStyle(style) - # self.camera_interactor_style = interactor_style.CameraInteractorStyle() - # self.camera_interactor_style.SetDefaultRenderer(self.render) - - self.axesWidget = gui_utils.createAxes(self.interactor) - - self.planeActor = gui_utils.createPlaneActorCircle() - self.planeTransform = vtk.vtkTransform() - self.render.AddActor(self.planeActor) - - self.add_legend() - - self.splanes_actors = [] - - # self.render.ResetCamera() - # self.render.SetUseDepthPeeling(True) - - widget3d.Initialize() - widget3d.Start() - - return widget3d - def add_legend(self): hackData = vtk.vtkPolyData() # it is hack to pass value to legend hackData.SetPoints(vtk.vtkPoints()) @@ -359,17 +246,6 @@ def init_right_panel(self): right_panel.setColumnStretch(3, 1) right_panel.setColumnStretch(4, 1) - сolumn2_number_of_cells = 4 - - validatorLocale = QtCore.QLocale("Englishs") - intValidator = QtGui.QIntValidator(0, 9000) - - doubleValidator = QtGui.QDoubleValidator(0.00, 9000.00, 2) - doubleValidator.setLocale(validatorLocale) - - doublePercentValidator = QtGui.QDoubleValidator(0.00, 9000.00, 2) - doublePercentValidator.setLocale(validatorLocale) - # Front-end development at its best self.cur_row = 1 @@ -632,7 +508,7 @@ def model_centering(self): self.stlActor.SetUserTransform(transform) - if not self.boxWidget is None: + if self.boxWidget is not None: self.boxWidget.SetTransform(transform) self.updateTransform() @@ -823,7 +699,7 @@ def shift_state(self, cancel=True): transform = movements[current_index][1] self.stlActor.SetUserTransform(transform) - if not self.boxWidget is None: + if self.boxWidget is not None: self.boxWidget.SetTransform(transform) self.updateTransform() @@ -849,42 +725,14 @@ def updateTransform(self): self.xyz_orient_value.setText(f"Orientation: {i:.2f} {j:.2f} {k:.2f}") def save_dialog( - self, - caption, - format="STL (*.stl *.STL);;Gcode (*.gcode)", - directory="", + self, caption, format="STL (*.stl *.STL);;Gcode (*.gcode)", directory="" ): - base_dir = getattr(sett(), "project_path", "") or os.getcwd() - if not base_dir or not path.isdir(base_dir): - base_dir = path.expanduser("~") - - if directory: - directory = ( - directory if path.isabs(directory) else path.join(base_dir, directory) - ) - else: - directory = base_dir - - return QFileDialog.getSaveFileName(None, caption, directory, format)[0] + return dialogs.save_dialog(self, caption, format, directory) def open_dialog( - self, - caption, - format="STL (*.stl *.STL);;Gcode (*.gcode)", - directory="", + self, caption, format="STL (*.stl *.STL);;Gcode (*.gcode)", directory="" ): - base_dir = getattr(sett(), "project_path", "") or os.getcwd() - if not base_dir or not path.isdir(base_dir): - base_dir = path.expanduser("~") - - if directory: - directory = ( - directory if path.isabs(directory) else path.join(base_dir, directory) - ) - else: - directory = base_dir - - return QFileDialog.getOpenFileName(None, caption, directory, format)[0] + return dialogs.open_dialog(self, caption, format, directory) def load_stl(self, stl_actor): self.clear_scene() @@ -952,7 +800,7 @@ def _recreate_splanes(self, splanes): ) row = self.splanes_tree.topLevelItem(i) - if row != None: + if row is not None: if ( row.checkState(0) == QtCore.Qt.CheckState.Checked ) or self.hide_checkbox.isChecked(): diff --git a/src/window/menu.py b/src/window/menu.py new file mode 100644 index 0000000..01e74c2 --- /dev/null +++ b/src/window/menu.py @@ -0,0 +1,49 @@ +"""Menu helpers for :mod:`src.window`.""" + +from PyQt5.QtWidgets import QAction + + +def setup_menus(window): + """Populate application menus and actions for ``window``. + + Parameters + ---------- + window: + Instance of :class:`~PyQt5.QtWidgets.QMainWindow` to populate. + """ + locale = window.locale + bar = window.menuBar() + + file_menu = bar.addMenu(locale.File) + window.open_action = QAction(locale.Open, window) + file_menu.addAction(window.open_action) + + window.save_gcode_action = QAction(locale.SaveGCode, window) + window.save_project_action = QAction(locale.SaveProject, window) + file_menu.addAction(window.save_project_action) + window.save_project_as_action = QAction(locale.SaveProjectAs, window) + file_menu.addAction(window.save_project_as_action) + file_menu.addAction(window.save_gcode_action) + window.save_sett_action = QAction(locale.SaveSettings, window) + file_menu.addAction(window.save_sett_action) + window.load_sett_action = QAction(locale.LoadSettings, window) + file_menu.addAction(window.load_sett_action) + + window.slicing_info_action = QAction(locale.SlicerInfo, window) + file_menu.addAction(window.slicing_info_action) + + tools_menu = bar.addMenu(locale.Tools) + window.calibration_action = QAction(locale.Calibration, window) + tools_menu.addAction(window.calibration_action) + window.bug_report = QAction(locale.SubmitBugReport, window) + tools_menu.addAction(window.bug_report) + + window.check_updates_action = QAction(locale.CheckUpdates, window) + tools_menu.addAction(window.check_updates_action) + + help_menu = bar.addMenu(locale.Help) + window.slicing_info_action = QAction(locale.SlicerInfo, window) + help_menu.addAction(window.slicing_info_action) + + window.documentation_action = QAction(locale.Documentation, window) + help_menu.addAction(window.documentation_action) diff --git a/src/window/rendering.py b/src/window/rendering.py new file mode 100644 index 0000000..bf43226 --- /dev/null +++ b/src/window/rendering.py @@ -0,0 +1,71 @@ +"""3D rendering utilities for :mod:`src.window`.""" + +import vtk +from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor + +from src.InteractorAroundActivePlane import InteractionAroundActivePlane +from src import gui_utils +from src.settings import get_color + + +def init_3d_widget(window): + """Create and initialize the VTK rendering widget for ``window``.""" + widget3d = QVTKRenderWindowInteractor(window) + widget3d.installEventFilter(window) + + window.render = vtk.vtkRenderer() + window.render.SetBackground(get_color("white")) + + widget3d.GetRenderWindow().AddRenderer(window.render) + window.interactor = widget3d.GetRenderWindow().GetInteractor() + window.interactor.SetInteractorStyle(None) + + window.interactor.Initialize() + window.interactor.Start() + + # set position of camera to (5, 5, 5) and look at (0, 0, 0) + window.render.GetActiveCamera().SetPosition(5, 5, 5) + window.render.GetActiveCamera().SetFocalPoint(0, 0, 0) + window.render.GetActiveCamera().SetViewUp(0, 0, 1) + + window.customInteractor = InteractionAroundActivePlane( + window.interactor, window.render + ) + window.interactor.AddObserver( + "MouseWheelBackwardEvent", window.customInteractor.middleBtnPress + ) + window.interactor.AddObserver( + "MouseWheelForwardEvent", window.customInteractor.middleBtnPress + ) + window.interactor.AddObserver( + "RightButtonPressEvent", window.customInteractor.rightBtnPress + ) + window.interactor.AddObserver( + "RightButtonReleaseEvent", window.customInteractor.rightBtnPress + ) + window.interactor.AddObserver( + "LeftButtonPressEvent", + lambda obj, event: window.customInteractor.leftBtnPress(obj, event, window), + ) + window.interactor.AddObserver( + "LeftButtonReleaseEvent", window.customInteractor.leftBtnPress + ) + window.interactor.AddObserver( + "MouseMoveEvent", + lambda obj, event: window.customInteractor.mouseMove(obj, event, window), + ) + + window.axesWidget = gui_utils.createAxes(window.interactor) + + window.planeActor = gui_utils.createPlaneActorCircle() + window.planeTransform = vtk.vtkTransform() + window.render.AddActor(window.planeActor) + + window.add_legend() + + window.splanes_actors = [] + + widget3d.Initialize() + widget3d.Start() + + return widget3d