Skip to content

Commit 14d28e2

Browse files
authored
Refactor window and controller into modular packages (#199)
* Refactor window and controller into modular packages * Fix ruff lint warnings * Format controller and window modules
1 parent 3f7cc80 commit 14d28e2

File tree

11 files changed

+494
-449
lines changed

11 files changed

+494
-449
lines changed

src/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Core package exposing primary interfaces for Spycer."""
2+
3+
try: # pragma: no cover - optional Qt dependencies
4+
from .window import MainWindow
5+
from .controller import MainController
6+
except Exception: # when Qt is missing during lightweight imports
7+
MainWindow = None # type: ignore
8+
MainController = None # type: ignore
9+
10+
__all__ = ["MainWindow", "MainController"]

src/controller/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Controller package exposing the main application controller."""
2+
3+
from .main import MainController
4+
5+
__all__ = ["MainController"]

src/controller/file_management.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"""File-related controller mixins."""
2+
3+
import os
4+
import shutil
5+
from os import path
6+
from pathlib import Path
7+
from shutil import copy2
8+
import logging
9+
10+
from PyQt5.QtWidgets import QFileDialog, QMessageBox
11+
12+
from src import gui_utils, locales
13+
from src.gui_utils import showErrorDialog
14+
from src.process import Process
15+
from src.settings import (
16+
sett,
17+
load_settings,
18+
PathBuilder,
19+
create_temporary_project_files,
20+
update_last_open_project,
21+
get_recent_projects,
22+
delete_temporary_project_files,
23+
)
24+
25+
logger = logging.getLogger(__name__)
26+
27+
28+
class FileManagementMixin:
29+
"""Encapsulates file handling logic for controllers."""
30+
31+
# Methods below are copied from the legacy ``MainController``
32+
# to isolate responsibilities.
33+
def open_file(self):
34+
try:
35+
filename = str(self.view.open_dialog(self.view.locale.OpenModel))
36+
if filename != "":
37+
file_ext = os.path.splitext(filename)[1].upper()
38+
filename = str(Path(filename))
39+
if file_ext == ".STL":
40+
self.reset_settings()
41+
s = sett()
42+
stl_full_path = PathBuilder.stl_model_temp()
43+
shutil.copyfile(filename, stl_full_path)
44+
s.slicing.stl_filename = path.basename(filename)
45+
s.slicing.stl_file = path.basename(stl_full_path)
46+
self.save_settings("vip")
47+
self.update_interface(filename)
48+
self.view.model_centering_box.setChecked(False)
49+
if os.path.isfile(s.colorizer.copy_stl_file):
50+
os.remove(s.colorizer.copy_stl_file)
51+
self.load_stl(stl_full_path)
52+
elif file_ext == ".GCODE":
53+
s = sett()
54+
self.save_settings("vip")
55+
self.load_gcode(filename, False)
56+
self.update_interface(filename)
57+
else:
58+
showErrorDialog("This file format isn't supported:" + file_ext)
59+
except IOError as e:
60+
showErrorDialog("Error during file opening:" + str(e))
61+
62+
def save_gcode_file(self):
63+
try:
64+
name = str(self.view.save_gcode_dialog())
65+
if name != "":
66+
if not name.endswith(".gcode"):
67+
name += ".gcode"
68+
copy2(PathBuilder.gcode_file(), name)
69+
except IOError as e:
70+
showErrorDialog("Error during file saving:" + str(e))
71+
72+
def save_settings_file(self):
73+
try:
74+
directory = (
75+
"Settings_" + os.path.basename(sett().slicing.stl_file).split(".")[0]
76+
)
77+
filename = str(
78+
self.view.save_dialog(
79+
self.view.locale.SaveSettings, "YAML (*.yaml *.YAML)", directory
80+
)
81+
)
82+
if filename != "":
83+
if not (filename.endswith(".yaml") or filename.endswith(".YAML")):
84+
filename += ".yaml"
85+
self.save_settings("vip", filename)
86+
except IOError as e:
87+
showErrorDialog("Error during file saving:" + str(e))
88+
89+
def save_project_files(self, save_path=""):
90+
if save_path == "":
91+
self.save_settings("vip", PathBuilder.settings_file())
92+
if os.path.isfile(PathBuilder.stl_model_temp()):
93+
shutil.copy2(PathBuilder.stl_model_temp(), PathBuilder.stl_model())
94+
else:
95+
self.save_settings("vip", path.join(save_path, "settings.yaml"))
96+
if os.path.isfile(PathBuilder.stl_model_temp()):
97+
shutil.copy2(
98+
PathBuilder.stl_model_temp(), path.join(save_path, "model.stl")
99+
)
100+
101+
def save_project(self):
102+
try:
103+
self.save_project_files()
104+
create_temporary_project_files()
105+
self.successful_saving_project()
106+
except IOError as e:
107+
showErrorDialog("Error during project saving: " + str(e))
108+
109+
def save_project_as(self):
110+
project_path = PathBuilder.project_path()
111+
try:
112+
save_directory = str(
113+
QFileDialog.getExistingDirectory(
114+
self.view, locales.getLocale().SavingProject
115+
)
116+
)
117+
if not save_directory:
118+
return
119+
self.save_project_files(save_directory)
120+
sett().project_path = save_directory
121+
self.save_settings("vip", PathBuilder.settings_file())
122+
create_temporary_project_files()
123+
delete_temporary_project_files(project_path)
124+
recent_projects = get_recent_projects()
125+
update_last_open_project(recent_projects, save_directory)
126+
self.successful_saving_project()
127+
except IOError as e:
128+
sett().project_path = project_path
129+
self.save_settings("vip")
130+
showErrorDialog("Error during project saving: " + str(e))
131+
132+
def successful_saving_project(self):
133+
message_box = QMessageBox(parent=self.view)
134+
message_box.setWindowTitle(locales.getLocale().SavingProject)
135+
message_box.setText(locales.getLocale().ProjectSaved)
136+
message_box.setIcon(QMessageBox.Information)
137+
message_box.exec_()
138+
139+
def load_settings_file(self):
140+
try:
141+
filename = str(
142+
self.view.open_dialog(
143+
self.view.locale.LoadSettings, "YAML (*.yaml *.YAML)"
144+
)
145+
)
146+
if filename != "":
147+
file_ext = os.path.splitext(filename)[1].upper()
148+
filename = str(Path(filename))
149+
if file_ext == ".YAML":
150+
try:
151+
old_project_path = sett().project_path
152+
load_settings(filename)
153+
sett().project_path = old_project_path
154+
self.display_settings()
155+
except Exception as e:
156+
showErrorDialog("Error during reading settings file: " + str(e))
157+
else:
158+
showErrorDialog("This file format isn't supported:" + file_ext)
159+
except IOError as e:
160+
showErrorDialog("Error during file opening:" + str(e))
161+
162+
def display_settings(self):
163+
self.view.setts.reload()
164+
165+
def colorize_model(self):
166+
shutil.copyfile(PathBuilder.stl_model_temp(), PathBuilder.colorizer_stl())
167+
self.save_settings("vip", PathBuilder.settings_file_temp())
168+
p = Process(PathBuilder.colorizer_cmd()).wait()
169+
if p.returncode:
170+
logging.error(f"error: <{p.stdout}>")
171+
gui_utils.showErrorDialog(p.stdout)
172+
return
173+
lastMove = self.view.stlActor.lastMove
174+
self.load_stl(PathBuilder.colorizer_stl(), colorize=True)
175+
self.view.stlActor.lastMove = lastMove

src/controller/hardware.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Hardware helpers for controllers."""
2+
3+
import logging
4+
5+
from src.settings import PathBuilder
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
def create_printer():
11+
"""Load printer implementation lazily."""
12+
try:
13+
from src.hardware import printer
14+
15+
return printer.EpitPrinter()
16+
except Exception as e:
17+
logger.warning("printer is not initialized: %s", e)
18+
return None
19+
20+
21+
def create_service(view, printer):
22+
"""Instantiate service tool for the given printer."""
23+
if not printer:
24+
return None, None
25+
26+
try:
27+
from src.hardware import service
28+
29+
panel = service.ServicePanel(view)
30+
panel.setModal(True)
31+
controller = service.ServiceController(panel, service.ServiceModel(printer))
32+
return panel, controller
33+
except Exception as e:
34+
logger.warning("service tool is unavailable: %s", e)
35+
return None, None
36+
37+
38+
def create_calibration(view, printer):
39+
"""Instantiate calibration tool for the given printer."""
40+
if not printer:
41+
return None, None
42+
43+
try:
44+
from src.hardware import calibration
45+
46+
panel = calibration.CalibrationPanel(view)
47+
panel.setModal(True)
48+
controller = calibration.CalibrationController(
49+
panel,
50+
calibration.CalibrationModel(printer, PathBuilder.calibration_file()),
51+
)
52+
return panel, controller
53+
except Exception as e:
54+
logger.warning("calibration tool is unavailable: %s", e)
55+
return None, None

0 commit comments

Comments
 (0)