diff --git a/BlocksScreen.cfg b/BlocksScreen.cfg index 42ab150b..065a900b 100644 --- a/BlocksScreen.cfg +++ b/BlocksScreen.cfg @@ -7,3 +7,7 @@ timeout: 5000 [usb_manager] gcodes_dir: ~/printer_data/gcodes/ + +[filament_presence] +object: cutter_sensor +name: extruder_cutter \ No newline at end of file diff --git a/BlocksScreen/lib/moonrakerComm.py b/BlocksScreen/lib/moonrakerComm.py index 5f889d9f..73920e72 100644 --- a/BlocksScreen/lib/moonrakerComm.py +++ b/BlocksScreen/lib/moonrakerComm.py @@ -280,9 +280,10 @@ def on_message(self, *args) -> None: metadata=_entry, ) elif "method" in response: - if ( - str(response["method"]).lower() == "notify_klippy_disconnected" - ): # Checkout for notify_klippy_disconnect + if str(response["method"]).lower() in ( + "notify_klippy_disconnected", + "notify_klippy_shutdown", + ): self.evaluate_klippy_status() message_event = ( diff --git a/BlocksScreen/lib/panels/controlTab.py b/BlocksScreen/lib/panels/controlTab.py index be84c8bc..bf9c5f64 100644 --- a/BlocksScreen/lib/panels/controlTab.py +++ b/BlocksScreen/lib/panels/controlTab.py @@ -1,8 +1,8 @@ from __future__ import annotations - import re import typing from functools import partial +import logging from helper_methods import normalize from lib.moonrakerComm import MoonWebSocket @@ -18,6 +18,9 @@ from PyQt6 import QtCore, QtGui, QtWidgets +_logger = logging.getLogger(__name__) + + class ControlTab(QtWidgets.QStackedWidget): """Printer Control Stacked Widget""" @@ -37,6 +40,9 @@ class ControlTab(QtWidgets.QStackedWidget): disable_popups: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( bool, name="disable-popups" ) + lock_ui: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + bool, name="lock-ui" + ) request_numpad: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( [str, int, "PyQt_PyObject"], [str, int, "PyQt_PyObject", int, int], @@ -79,6 +85,7 @@ def __init__( self.probe_helper_page = ProbeHelper(self) self.probe_helper_page.toggle_conn_page.connect(self.toggle_conn_page) self.probe_helper_page.disable_popups.connect(self.disable_popups) + self.probe_helper_page.lock_ui.connect(self.lock_ui) self.addWidget(self.probe_helper_page) self.probe_helper_page.call_load_panel.connect(self.call_load_panel) self.printcores_page = SwapPrintcorePage(self) @@ -252,24 +259,6 @@ def __init__( self.numpadPage.request_back.connect(self.request_back_button) self.addWidget(self.numpadPage) - self.panel.extruder_temp_display.clicked.connect( - lambda: self.request_numpad[str, int, "PyQt_PyObject", int, int].emit( - "Extruder Temperature", - int(round(float(self.panel.extruder_temp_display.secondary_text))), - self.on_numpad_change, - 0, - 370, # TODO: Get this value from printer objects - ) - ) - self.panel.bed_temp_display.clicked.connect( - lambda: self.request_numpad[str, int, "PyQt_PyObject", int, int].emit( - "Bed Temperature", - int(round(float(self.panel.bed_temp_display.secondary_text))), - self.on_numpad_change, - 0, - 120, # TODO: Get this value from printer objects - ) - ) self.request_numpad[str, int, "PyQt_PyObject", int, int].connect( self.on_numpad_request ) @@ -304,6 +293,8 @@ def __init__( self.printer.fan_update[str, str, float].connect(self.on_fan_object_update) self.printer.fan_update[str, str, int].connect(self.on_fan_object_update) + self.printer.printer_config.connect(self.on_printer_config) + def _handle_z_tilt_object_update(self, value, state): if state: self.call_load_panel.emit(False, "") @@ -470,6 +461,44 @@ def _handle_gcode_response(self, messages: list): f"Retries: {retries_done}/{retries_total} | Range: {probed_range:.6f} | Tolerance: {tolerance:.6f}", ) + @QtCore.pyqtSlot(dict, name="printer_config") + def on_printer_config(self, config: dict) -> None: + """Slot that receives the full printer configuration, + + Additionally, this method configures the signal connections + between controllable heaters and numpad calls + """ + try: + self.panel.extruder_temp_display.clicked.disconnect() + self.panel.bed_temp_display.clicked.disconnect() + except Exception: + _logger.debug("Signals were not connected") + extruder = config.get("extruder", None) or {} + bed = config.get("heater_bed", None) or {} + e_min_temp = extruder.get("min_temp", 0) + e_max_temp = extruder.get("max_temp", 300) + b_max_temp = bed.get("max_temp", 100) + b_min_temp = bed.get("min_temp", 0) + # Configure numpads + self.panel.extruder_temp_display.clicked.connect( + lambda: self.request_numpad[str, int, "PyQt_PyObject", int, int].emit( + "Extruder Temperature", + int(round(float(self.panel.extruder_temp_display.secondary_text))), + self.on_numpad_change, + int(e_min_temp), + int(e_max_temp), + ) + ) + self.panel.bed_temp_display.clicked.connect( + lambda: self.request_numpad[str, int, "PyQt_PyObject", int, int].emit( + "Bed Temperature", + int(round(float(self.panel.bed_temp_display.secondary_text))), + self.on_numpad_change, + int(b_min_temp), + int(b_max_temp), + ) + ) + def handle_ztilt(self): """Handle Z-Tilt Adjustment""" self.call_load_panel.emit(True, "Please wait, performing Z-axis calibration.") diff --git a/BlocksScreen/lib/panels/filamentTab.py b/BlocksScreen/lib/panels/filamentTab.py index 04fc5ef4..06c5d23b 100644 --- a/BlocksScreen/lib/panels/filamentTab.py +++ b/BlocksScreen/lib/panels/filamentTab.py @@ -1,27 +1,36 @@ import enum from functools import partial - +import logging from lib.printer import Printer from lib.filament import Filament from lib.ui.filamentStackedWidget_ui import Ui_filamentStackedWidget - from lib.panels.widgets.popupDialogWidget import Popup from PyQt6 import QtCore, QtGui, QtWidgets +logger = logging.getLogger(__name__) + + +class FilamentTypes(enum.Enum): + PLA = Filament(name="PLA", temperature=220) + PETG = Filament(name="PETG", temperature=240) + ABS = Filament(name="ABS", temperature=250) + HIPS = Filament(name="HIPS", temperature=250) + NYLON = Filament(name="NYLON", temperature=270) + TPU = Filament(name="TPU", temperature=230) + UNKNOWN = Filament(name="UNKNOWN", temperature=250) + class FilamentTab(QtWidgets.QStackedWidget): request_filament_change_page = QtCore.pyqtSignal(name="filament_change_page") request_filament_load = QtCore.pyqtSignal(name="filament_load_t1") request_back = QtCore.pyqtSignal(name="request_back") request_change_page = QtCore.pyqtSignal(int, int, name="request_change_page") + request_change_tab = QtCore.pyqtSignal(int, name="request_change_tab") request_toolhead_count = QtCore.pyqtSignal(int, name="toolhead_number_received") run_gcode = QtCore.pyqtSignal(str, name="run_gcode") call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") - class FilamentTypes(enum.Enum): - PLA = Filament(name="PLA", temperature=220) - class FilamentStates(enum.Enum): LOADED = enum.auto() UNLOADED = enum.auto() @@ -30,7 +39,7 @@ class FilamentStates(enum.Enum): def __repr__(self) -> str: return "<%s.%s>" % (self.__class__.__name__, self._name_) - def __init__(self, parent: QtWidgets.QWidget, printer: Printer, ws, /) -> None: + def __init__(self, parent, printer: Printer, ws, config, /) -> None: super().__init__(parent) self.panel = Ui_filamentStackedWidget() self.panel.setupUi(self) @@ -43,8 +52,14 @@ def __init__(self, parent: QtWidgets.QWidget, printer: Printer, ws, /) -> None: self.popup = Popup(self) self.has_load_unload_objects = None self._filament_state = self.FilamentStates.UNKNOWN - self._sensor_states = {} - self.filament_type: Filament | None = None + self.filament_type = FilamentTypes.UNKNOWN + + cfg = config + if cfg.has_section("filament_presence"): + i = cfg.get_section("filament_presence", None) + self.filament_sensor = i.get("name", str, None) + else: + self.filament_sensor = None self.panel.filament_page_load_btn.clicked.connect( partial(self.change_page, self.indexOf(self.panel.load_page)) ) @@ -52,28 +67,30 @@ def __init__(self, parent: QtWidgets.QWidget, printer: Printer, ws, /) -> None: self.panel.load_custom_btn.hide() self.panel.load_header_back_button.clicked.connect(self.back_button) self.panel.load_pla_btn.clicked.connect( - partial(self.load_filament, toolhead=0, temp=220) + partial(self.load_filament, toolhead=0, filament=FilamentTypes.PLA) ) self.panel.load_petg_btn.clicked.connect( - partial(self.load_filament, toolhead=0, temp=240) + partial(self.load_filament, toolhead=0, filament=FilamentTypes.PETG) ) self.panel.load_abs_btn.clicked.connect( - partial(self.load_filament, toolhead=0, temp=250) + partial(self.load_filament, toolhead=0, filament=FilamentTypes.ABS) ) self.panel.load_hips_btn.clicked.connect( - partial(self.load_filament, toolhead=0, temp=250) + partial(self.load_filament, toolhead=0, filament=FilamentTypes.HIPS) ) self.panel.load_nylon_btn.clicked.connect( - partial(self.load_filament, toolhead=0, temp=270) + partial(self.load_filament, toolhead=0, filament=FilamentTypes.NYLON) ) self.panel.load_tpu_btn.clicked.connect( - partial(self.load_filament, toolhead=0, temp=230) + partial(self.load_filament, toolhead=0, filament=FilamentTypes.TPU) ) self.panel.filament_page_unload_btn.clicked.connect( lambda: self.unload_filament(toolhead=0, temp=250) ) + self.panel.main_back_button.clicked.connect( + lambda: self.request_change_tab.emit(0) + ) self.run_gcode.connect(self.ws.api.run_gcode) - self.printer.extruder_update.connect(self.on_extruder_update) self.printer.unload_filament_update.connect(self.on_unload_filament) self.printer.load_filament_update.connect(self.on_load_filament) self.printer.filament_switch_sensor_update.connect( @@ -84,8 +101,18 @@ def __init__(self, parent: QtWidgets.QWidget, printer: Printer, ws, /) -> None: self.printer.print_stats_update[str, dict].connect(self.on_print_stats_update) self.printer.print_stats_update[str, float].connect(self.on_print_stats_update) - self.loadignore = True - self.unloadignore = True + self.printer.save_variables_update.connect(self.on_save_variables_update) + self.state = "standby" + + def on_save_variables_update(self, save_variables: dict): + """Handle query response""" + for i in FilamentTypes: + if i.value.name in save_variables["variables"]["filament_type"]: + self.filament_type = i + break + else: + self.filament_type = FilamentTypes.UNKNOWN + self.panel.label_2.setText(self.filament_type.value.name) @QtCore.pyqtSlot(str, dict, name="on_print_stats_update") @QtCore.pyqtSlot(str, float, name="on_print_stats_update") @@ -94,9 +121,23 @@ def on_print_stats_update(self, field: str, value: dict | float | str) -> None: """Handle print stats object update""" if isinstance(value, str): if "state" in field: + self.state = value + if value in ("printing", "pausing", "paused", "resuming"): + self.panel.main_back_button.show() + self.panel.spacerItem1.changeSize( + 60, + 0, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) if value in ("standby"): - self.loadignore = True - self.unloadignore = True + self.panel.main_back_button.hide() + self.panel.spacerItem1.changeSize( + 0, + 0, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) @QtCore.pyqtSlot(str, str, bool, name="on_filament_sensor_update") def on_filament_sensor_update(self, sensor_name: str, parameter: str, value: bool): @@ -106,78 +147,46 @@ def on_filament_sensor_update(self, sensor_name: str, parameter: str, value: boo self._filament_state = self.FilamentStates.UNKNOWN self.handle_filament_state() return - self._sensor_states[sensor_name] = value - if not self._sensor_states: - new_state = self.FilamentStates.UNKNOWN - elif all(self._sensor_states.values()): - new_state = self.FilamentStates.LOADED - else: - new_state = self.FilamentStates.UNLOADED - if self._filament_state != new_state: - self._filament_state = new_state - self.handle_filament_state() + if sensor_name == self.filament_sensor: + if value: + self._filament_state = self.FilamentStates.LOADED + else: + self._filament_state = self.FilamentStates.UNLOADED + return + self.handle_filament_state() - @QtCore.pyqtSlot(str, str, float, name="on_extruder_update") - def on_extruder_update( - self, extruder_name: str, field: str, new_value: float - ) -> None: - """Handle extruder update""" - if not self.isVisible: - return - if not self.loadignore or not self.unloadignore: - if self.target_temp != 0: - if self.current_temp == self.target_temp: - if self.isVisible: - self.call_load_panel.emit( - True, "Extruder heated up \n Please wait" - ) - return - if field == "temperature": - self.current_temp = round(new_value, 0) - if self.isVisible: - self.call_load_panel.emit( - True, - f"Heating up ({new_value}/{self.target_temp}) \n Please wait", - ) - if field == "target": - self.target_temp = round(new_value, 0) - if self.isVisible: - self.call_load_panel.emit(True, "Heating up \n Please wait") - - @QtCore.pyqtSlot(bool, name="on_load_filament") - def on_load_filament(self, status: bool): + @QtCore.pyqtSlot(dict, name="on_load_filament") + def on_load_filament(self, status: dict): """Handle load filament object updated""" - if not self.isVisible: - return - if self.loadignore: - return - if status: - self.call_load_panel.emit(True, "Loading Filament") - else: - self.loadignore = True - self.target_temp = 0 - self.call_load_panel.emit(False, "") - self._filament_state = self.FilamentStates.LOADED + if "state" in status.keys(): + if not status["state"]: + self.target_temp = 0 + self.call_load_panel.emit(False, "") + if self.state == "paused": + self.request_change_tab.emit(0) + return + self.call_load_panel.emit( + True, f"Loading Filament\n{status['step'].capitalize()}" + ) self.handle_filament_state() - @QtCore.pyqtSlot(bool, name="on_unload_filament") - def on_unload_filament(self, status: bool): + @QtCore.pyqtSlot(dict, name="on_unload_filament") + def on_unload_filament(self, status: dict): """Handle unload filament object updated""" - if not self.isVisible: - return - if self.unloadignore: - return - if status: - self.call_load_panel.emit(True, "Unloading Filament") - else: - self.unloadignore = True - self.call_load_panel.emit(False, "") - self.target_temp = 0 - self._filament_state = self.FilamentStates.UNLOADED + if "state" in status.keys(): + if not status["state"]: + self.target_temp = 0 + self.call_load_panel.emit(False, "") + return + self.call_load_panel.emit( + True, f"Unloading Filament\n{status['step'].capitalize()}" + ) self.handle_filament_state() @QtCore.pyqtSlot(int, int, name="load_filament") - def load_filament(self, toolhead: int = 0, temp: int = 220) -> None: + def load_filament( + self, toolhead: int = 0, filament: FilamentTypes = FilamentTypes.UNKNOWN + ) -> None: """Handle load filament buttons clicked""" if not self.isVisible: return @@ -194,9 +203,11 @@ def load_filament(self, toolhead: int = 0, temp: int = 220) -> None: message="Filament is already loaded.", ) return - self.loadignore = False self.call_load_panel.emit(True, "Loading Filament") - self.run_gcode.emit(f"LOAD_FILAMENT TOOLHEAD=load_toolhead TEMPERATURE={temp}") + self.run_gcode.emit( + f"""SAVE_VARIABLE VARIABLE=filament_type VALUE='"{filament.value.name}"'""" + ) + self.run_gcode.emit(f"LOAD_FILAMENT TEMPERATURE={filament.value.temperature}") @QtCore.pyqtSlot(str, int, name="unload_filament") def unload_filament(self, toolhead: int = 0, temp: int = 220) -> None: @@ -218,21 +229,23 @@ def unload_filament(self, toolhead: int = 0, temp: int = 220) -> None: return self.find_routine_objects() - self.unloadignore = False self.call_load_panel.emit(True, "Unloading Filament") + self.run_gcode.emit( + f"""SAVE_VARIABLE VARIABLE=filament_type VALUE='"{FilamentTypes.UNKNOWN.value.name}"'""" + ) self.run_gcode.emit(f"UNLOAD_FILAMENT TEMPERATURE={temp}") def handle_filament_state(self): """Handle ui changes on filament states""" if self._filament_state == self.FilamentStates.LOADED: - self.panel.filament_page_load_btn.setDisabled(True) - self.panel.filament_page_load_btn.setDisabled(False) + self.panel.filament_page_unload_btn.setEnabled(True) + self.panel.filament_page_load_btn.setEnabled(False) elif self._filament_state == self.FilamentStates.UNLOADED: - self.panel.filament_page_unload_btn.setDisabled(True) - self.panel.filament_page_unload_btn.setDisabled(False) + self.panel.filament_page_unload_btn.setEnabled(False) + self.panel.filament_page_load_btn.setEnabled(True) else: - self.panel.filament_page_load_btn.setDisabled(False) - self.panel.filament_page_unload_btn.setDisabled(False) + self.panel.filament_page_load_btn.setEnabled(True) + self.panel.filament_page_unload_btn.setEnabled(True) @property def filament_state(self): diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 12fbc628..1a84effe 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -34,6 +34,12 @@ _logger = logging.getLogger(__name__) +_GCODE_POPUP_MESSAGES: tuple[tuple[str, str], ...] = ( + ("filament runout", "Filament Runout"), + ("no filament", "No Filament Detected"), + ("sensor not in valid range", "Eddy Current Sensor:\nnot in valid range"), +) + def api_handler(func): """Decorator for methods that handle api responses""" @@ -112,6 +118,7 @@ def __init__(self): self.ui.setupUi(self) self.screensaver = ScreenSaver(self) self._popup_toggle: bool = False + self._klippy_ready: bool = False self.ui.main_content_widget.setCurrentIndex(0) usb_config = self.config.get_section("usb_manager", fallback=None) @@ -135,7 +142,9 @@ def __init__(self): self.ui.printTab, self.file_data, self.ws, self.printer ) QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.CursorShape.BlankCursor) - self.filamentPanel = FilamentTab(self.ui.filamentTab, self.printer, self.ws) + self.filamentPanel = FilamentTab( + self.ui.filamentTab, self.printer, self.ws, self.config + ) self.controlPanel = ControlTab(self.ui.controlTab, self.ws, self.printer) self.utilitiesPanel = UtilitiesTab(self.ui.utilitiesTab, self.ws, self.printer) self.networkPanel = NetworkControlWindow(self) @@ -145,6 +154,7 @@ def __init__(self): self.conn_window.on_websocket_connection_achieved ) self.ws.connection_lost.connect(self.conn_window.on_websocket_connection_lost) + self.ws.klippy_state_signal.connect(self._on_klippy_state) self.printer.webhooks_update.connect(self.conn_window.webhook_update) self.printPanel.request_back.connect(slot=self.global_back) self.printPanel.on_cancel_print.connect(slot=self.on_cancel_print) @@ -175,7 +185,7 @@ def __init__(self): self.ui.filament_type_icon.clicked.connect( lambda: self.global_change_page( self.ui.main_content_widget.indexOf(self.ui.filamentTab), - self.filamentPanel.indexOf(self.filamentPanel.panel.load_page), + self.filamentPanel.indexOf(self.filamentPanel), ) ) self.ui.filament_type_icon.setText("PLA") @@ -214,7 +224,11 @@ def __init__(self): self.handle_error_response.connect( self.controlPanel.probe_helper_page.handle_error_response ) + self.controlPanel.probe_helper_page.show_notifications.connect( + self.notiPage.new_notication + ) self.controlPanel.disable_popups.connect(self.popup_toggle) + self.controlPanel.lock_ui.connect(self.set_ui_lock) self.on_update_message.connect(self.update_page.handle_update_message) self.update_page.request_full_update.connect(self.ws.api.full_update) self.update_page.request_recover_repo[str].connect( @@ -241,11 +255,14 @@ def __init__(self): self.ui.extruder_temp_display.display_format = "upper_downer" self.ui.bed_temp_display.display_format = "upper_downer" - self.controlPanel.call_load_panel.connect(self.show_LoadScreen) - self.filamentPanel.call_load_panel.connect(self.show_LoadScreen) - self.printPanel.call_load_panel.connect(self.show_LoadScreen) - self.utilitiesPanel.call_load_panel.connect(self.show_LoadScreen) - self.conn_window.call_load_panel.connect(self.show_LoadScreen) + self.controlPanel.call_load_panel.connect(self.show_loadscreen) + self.filamentPanel.call_load_panel.connect(self.show_loadscreen) + self.printPanel.call_load_panel.connect(self.show_loadscreen) + self.utilitiesPanel.call_load_panel.connect(self.show_loadscreen) + self.conn_window.call_load_panel.connect(self.show_loadscreen) + + self.filamentPanel.request_change_tab.connect(self.global_change_tab) + self.printPanel.request_change_tab.connect(self.global_change_tab) self.loadscreen = BasePopup(self, floating=False, dialog=False) self.loadwidget = LoadingOverlayWidget( @@ -268,6 +285,8 @@ def __init__(self): self.file_data.fileinfo.connect(self.cancelpage._show_screen_thumbnail) self.printPanel.call_cancel_panel.connect(self.handle_cancel_print) + self.print_status = "idle" + if self.config.has_section("server"): self.bo_ws_startup.emit() self.reset_tab_indexes() @@ -286,13 +305,9 @@ def handle_cancel_print(self, show: bool = True): self.cancelpage.show() @QtCore.pyqtSlot(bool, str, name="show-load-page") - def show_LoadScreen(self, show: bool = True, msg: str = ""): + def show_loadscreen(self, show: bool = True, msg: str = ""): """Show or hide the loading overlay, guarded by the calling panel's visibility.""" _sender = self.sender() - - if _sender == self.filamentPanel: - if not self.filamentPanel.isVisible(): - return if _sender == self.controlPanel: if not self.controlPanel.isVisible(): return @@ -302,7 +317,6 @@ def show_LoadScreen(self, show: bool = True, msg: str = ""): if _sender == self.utilitiesPanel: if not self.utilitiesPanel.isVisible(): return - self.loadwidget.set_status_message(msg) if show: self.loadscreen.show() @@ -336,8 +350,6 @@ def on_cancel_print(self): self.enable_tab_bar() self.ui.extruder_temp_display.clicked.disconnect() self.ui.bed_temp_display.clicked.disconnect() - self.ui.filament_type_icon.setDisabled(False) - self.ui.nozzle_size_icon.setDisabled(False) self.ui.extruder_temp_display.clicked.connect( lambda: self.global_change_page( self.ui.main_content_widget.indexOf(self.ui.controlTab), @@ -366,9 +378,6 @@ def enable_tab_bar(self) -> bool: bool: True if the TabBar was disabled """ - self.ui.main_content_widget.setTabEnabled( - self.ui.main_content_widget.indexOf(self.ui.filamentTab), True - ) self.ui.main_content_widget.setTabEnabled( self.ui.main_content_widget.indexOf(self.ui.controlTab), True ) @@ -378,9 +387,6 @@ def enable_tab_bar(self) -> bool: self.ui.header_main_layout.setEnabled(True) return all( [ - not self.ui.main_content_widget.isTabEnabled( - self.ui.main_content_widget.indexOf(self.ui.filamentTab) - ), not self.ui.main_content_widget.isTabEnabled( self.ui.main_content_widget.indexOf(self.ui.controlTab) ), @@ -402,9 +408,6 @@ def disable_tab_bar(self) -> bool: Returns: boolean: True if the TabBar was disabled """ - self.ui.main_content_widget.setTabEnabled( - self.ui.main_content_widget.indexOf(self.ui.filamentTab), False - ) self.ui.main_content_widget.setTabEnabled( self.ui.main_content_widget.indexOf(self.ui.controlTab), False ) @@ -414,9 +417,6 @@ def disable_tab_bar(self) -> bool: self.ui.header_main_layout.setEnabled(False) return all( [ - not self.ui.main_content_widget.isTabEnabled( - self.ui.main_content_widget.indexOf(self.ui.filamentTab) - ), not self.ui.main_content_widget.isTabEnabled( self.ui.main_content_widget.indexOf(self.ui.controlTab) ), @@ -432,17 +432,41 @@ def popup_toggle(self, toggle: bool) -> None: """Toggles app popups""" self._popup_toggle = toggle + @QtCore.pyqtSlot(bool, name="set-ui-lock") + def set_ui_lock(self, locked: bool) -> None: + """Lock or unlock navigation during calibration. + + Disables all tabs except controlTab (where calibration lives) and + the header, so the user cannot navigate away mid-calibration. + """ + for tab in (self.ui.printTab, self.ui.filamentTab, self.ui.utilitiesTab): + self.ui.main_content_widget.setTabEnabled( + self.ui.main_content_widget.indexOf(tab), not locked + ) + self.ui.header_main_layout.setEnabled(not locked) + + @QtCore.pyqtSlot(str, name="on-klippy-state") + def _on_klippy_state(self, state: str) -> None: + """Track Klippy readiness to suppress spurious error popups during disconnect.""" + self._klippy_ready = state == "ready" + def reset_tab_indexes(self): """ Used to grantee all tabs reset to their first page once the user leaves the tab """ - self.update_page.hide() - self.printPanel.setCurrentIndex(0) self.filamentPanel.setCurrentIndex(0) + + if self.print_status == "printing": + self.printPanel.setCurrentIndex( + self.printPanel.indexOf(self.printPanel.jobStatusPage_widget) + ) + return + self.printPanel.setCurrentIndex(0) self.controlPanel.setCurrentIndex(0) self.utilitiesPanel.setCurrentIndex(0) self.networkPanel.setCurrentIndex(0) + self.update_page.hide() def current_panel_index(self) -> int: """Helper function to get the index of the current page in the current tab @@ -503,7 +527,7 @@ def global_change_page(self, tab_index: int, panel_index: int) -> None: "Panel page index expected type int, %s", str(type(panel_index)) ) - self.show_LoadScreen(False) + self.show_loadscreen(False) current_page = [ self.ui.main_content_widget.currentIndex(), self.current_panel_index(), @@ -519,6 +543,22 @@ def global_change_page(self, tab_index: int, panel_index: int) -> None: f"Requested page change -> Tab index : {requested_page[0]} | panel index : {requested_page[1]}", ) + def global_change_tab(self, tab_index: int) -> None: + """Changes the current tab while keeping the current panel page index if possible + + Args: + tab_index (int): The index of the tab to change to + """ + if not isinstance(tab_index, int): + _logger.debug( + "Tab index argument expected type int, got %s", str(type(tab_index)) + ) + return + self.ui.main_content_widget.setCurrentIndex(tab_index) + _logger.debug( + f"Requested tab change -> Tab index : {tab_index}", + ) + @QtCore.pyqtSlot(name="request-back") def global_back(self) -> None: """Requests to go back a page globally""" @@ -709,10 +749,18 @@ def _handle_notify_gcode_response_message(self, method, data, metadata) -> None: if self._popup_toggle: return _gcode_msg_type, _message = str(_gcode_response[0]).split(" ", maxsplit=1) - popupWhitelist = ["filament runout", "no filament"] - if _message.lower() not in popupWhitelist or _gcode_msg_type != "!!": + _msg_lower = _message.lower() + _display = next( + ( + fmt + for pattern, fmt in _GCODE_POPUP_MESSAGES + if pattern in _msg_lower + ), + None, + ) + if _gcode_msg_type != "!!" or _display is None: return - self.show_notifications.emit("mainwindow", _message, 3, True) + self.show_notifications.emit("mainwindow", _display, 3, True) @api_handler def _handle_error_message(self, method, data, metadata) -> None: @@ -721,6 +769,11 @@ def _handle_error_message(self, method, data, metadata) -> None: if self._popup_toggle: return + # Suppress error popups while Klippy is disconnected/shutting down. + # Those errors are side-effects of the disconnect, not actionable by the user. + if not self._klippy_ready: + return + text = data.get("message", str(data)) if isinstance(data, dict) else str(data) lower_text = text.lower() @@ -825,11 +878,10 @@ def event(self, event: QtCore.QEvent) -> bool: return True return False if event.type() == events.PrintStart.type(): + self.print_status = "printing" self.disable_tab_bar() self.ui.extruder_temp_display.clicked.disconnect() self.ui.bed_temp_display.clicked.disconnect() - self.ui.filament_type_icon.setDisabled(True) - self.ui.nozzle_size_icon.setDisabled(True) self.ui.extruder_temp_display.clicked.connect( lambda: self.global_change_page( self.ui.main_content_widget.indexOf(self.ui.printTab), @@ -849,13 +901,12 @@ def event(self, event: QtCore.QEvent) -> bool: events.PrintComplete.type(), events.PrintCancelled.type(), ): + self.print_status = "idle" if event.type() == events.PrintCancelled.type(): self.handle_cancel_print() self.enable_tab_bar() self.ui.extruder_temp_display.clicked.disconnect() self.ui.bed_temp_display.clicked.disconnect() - self.ui.filament_type_icon.setDisabled(False) - self.ui.nozzle_size_icon.setDisabled(False) self.ui.extruder_temp_display.clicked.connect( lambda: self.global_change_page( self.ui.main_content_widget.indexOf(self.ui.controlTab), @@ -874,4 +925,4 @@ def event(self, event: QtCore.QEvent) -> bool: def sizeHint(self) -> QtCore.QSize: """Sets default size for the widget""" self.adjustSize() - return super().sizeHint(QtCore.QSize(800, 480)) + return QtCore.QSize(800, 480) diff --git a/BlocksScreen/lib/panels/printTab.py b/BlocksScreen/lib/panels/printTab.py index 65927aae..8d63c8ed 100644 --- a/BlocksScreen/lib/panels/printTab.py +++ b/BlocksScreen/lib/panels/printTab.py @@ -56,18 +56,22 @@ class PrintTab(QtWidgets.QStackedWidget): int, int, name="request_change_page" ) + request_change_tab: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + int, name="request_change_tab" + ) + run_gcode_signal: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( str, name="run_gcode" ) on_cancel_print: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( name="on_cancel_print" ) - call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") - - call_cancel_panel = QtCore.pyqtSignal(bool, name="call-load-panel") - _z_offset: float = 0.0 - _active_z_offset: float = 0.0 - _finish_print_handled: bool = False + call_load_panel: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + bool, str, name="call-load-panel" + ) + call_cancel_panel: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + bool, name="call-load-panel" + ) def __init__( self, @@ -77,6 +81,11 @@ def __init__( printer: Printer, ) -> None: super().__init__(parent) + self._active_z_offset: float = 0.0 + self._pending_save_offset: float = 0.0 + self._finish_print_handled: bool = False + self._cancel_z_snapshot: float = 0.0 + self._z_apply_command: str = "Z_OFFSET_APPLY_ENDSTOP" self.setupMainPrintPage() self.ws: MoonWebSocket = ws @@ -201,10 +210,14 @@ def __init__( self.addWidget(self.babystepPage) self.tune_page = TuneWidget(self) self.addWidget(self.tune_page) + self.tune_page.tune_change_filament_btn.clicked.connect( + lambda: self.request_change_tab.emit(1) + ) self.jobStatusPage_widget.tune_clicked.connect( lambda: self.change_page(self.indexOf(self.tune_page)) ) self.tune_page.request_back.connect(self.back_button) + self.printer.printer_config.connect(self.tune_page.on_printer_config) self.printer.extruder_update.connect( self.tune_page.on_extruder_temperature_change ) @@ -223,6 +236,9 @@ def __init__( self.printer.gcode_move_update[str, list].connect( self.babystepPage.on_gcode_move_update ) + self.printer.print_stats_update[str, str].connect( + self.babystepPage.on_print_state_update + ) self.printer.gcode_move_update[str, list].connect(self.activate_save_button) self.tune_page.run_gcode.connect(self.ws.api.run_gcode) self.tune_page.request_sliderPage[str, int, "PyQt_PyObject"].connect( @@ -267,7 +283,7 @@ def __init__( self.confirmPage_widget.on_delete.connect(self.delete_file) self.change_page(self.indexOf(self.print_page)) # force set the initial page self.save_config_btn.clicked.connect(self.save_config) - self.BasePopup_z_offset.accepted.connect(self.update_configuration_file) + self.ws.klippy_state_signal.connect(self.on_klippy_state) @QtCore.pyqtSlot(str, dict, name="on_print_stats_update") @QtCore.pyqtSlot(str, float, name="on_print_stats_update") @@ -276,10 +292,14 @@ def on_print_stats_update(self, field: str, value: dict | float | str) -> None: """ unblocks tabs if on standby """ - if isinstance(value, str): - if "state" in field: - if value in ("standby"): - self.on_cancel_print.emit() + if isinstance(value, str) and "state" in field and value == "standby": + self.on_cancel_print.emit() + if not self._finish_print_handled and self._cancel_z_snapshot != 0: + self._active_z_offset = self._cancel_z_snapshot + self.save_config() + self._finish_print_handled = True + self.save_config_btn.setVisible(True) + self._cancel_z_snapshot = 0.0 @QtCore.pyqtSlot(str, int, "PyQt_PyObject", name="on_numpad_request") @QtCore.pyqtSlot(str, int, "PyQt_PyObject", int, int, name="on_numpad_request") @@ -292,6 +312,10 @@ def on_numpad_request( max_value: int = 100, ) -> None: """Handle numpad request""" + try: + self.numpadPage.value_selected.disconnect() + except (RuntimeError, TypeError): + pass self.numpadPage.value_selected.connect(callback) self.numpadPage.set_name(name) self.numpadPage.set_value(current_value) @@ -311,6 +335,10 @@ def on_slidePage_request( max_value: int = 100, ) -> None: """Handle slider page request""" + try: + self.sliderPage.value_selected.disconnect() + except (RuntimeError, TypeError): + pass self.sliderPage.value_selected.connect(callback) self.sliderPage.set_name(name) self.sliderPage.set_slider_position(int(current_value)) @@ -321,8 +349,12 @@ def on_slidePage_request( @QtCore.pyqtSlot(str, str, name="delete_file") @QtCore.pyqtSlot(str, name="delete_file") def delete_file(self, filename: str, directory: str = "gcodes") -> None: - """Handle Delete file signal, shows confirmation dialog""" + """Handle Delete file signal, shows confirmation dialog.""" self.BasePopup.set_message("Are you sure you want to delete this file?") + try: + self.BasePopup.accepted.disconnect() + except (RuntimeError, TypeError): + pass self.BasePopup.accepted.connect( lambda: self._on_delete_file_confirmed(filename, directory) ) @@ -330,42 +362,62 @@ def delete_file(self, filename: str, directory: str = "gcodes") -> None: def save_config(self) -> None: """Handle Save configuration behaviour, shows confirmation dialog""" - if self._finish_print_handled: - self.run_gcode_signal.emit("Z_OFFSET_APPLY_PROBE") - self._z_offset = self._active_z_offset - self.babystepPage.bbp_z_offset_title_label.setText( - f"Z: {self._z_offset:.3f}mm" - ) + self._pending_save_offset = self._active_z_offset self.BasePopup_z_offset.set_message( - f"The Z‑Offset is now {self._active_z_offset:.3f} mm.\n" + f"The Z-Offset is now {self._pending_save_offset + 0.0:.3f} mm.\n" "Would you like to save this change permanently?\n" "The machine will restart." ) self.BasePopup_z_offset.cancel_button_text("Later") + try: + self.BasePopup_z_offset.accepted.disconnect(self.update_configuration_file) + except (RuntimeError, TypeError): + pass + self.BasePopup_z_offset.accepted.connect(self.update_configuration_file) self.BasePopup_z_offset.open() - def update_configuration_file(self): + def update_configuration_file(self) -> None: """Runs the `SAVE_CONFIG` gcode""" - self.run_gcode_signal.emit("Z_OFFSET_APPLY_PROBE") + try: + self.BasePopup_z_offset.accepted.disconnect(self.update_configuration_file) + except (RuntimeError, TypeError): + pass + self.run_gcode_signal.emit( + f"SET_GCODE_OFFSET Z={self._pending_save_offset:.3f} MOVE=0" + ) + self.run_gcode_signal.emit(self._z_apply_command) self.run_gcode_signal.emit("SAVE_CONFIG") - self.BasePopup_z_offset.disconnect() + self.babystepPage.bbp_z_offset_title_label.setText( + f"Z: {self._pending_save_offset + 0.0:.3f}mm" + ) + self.save_config_btn.setVisible(False) + + @QtCore.pyqtSlot(str, name="on_klippy_state") + def on_klippy_state(self, state: str) -> None: + """Dismiss the Z-offset save popup and reset save state on unexpected shutdown.""" + if state in ("ready", "startup"): + return + self.BasePopup_z_offset.reject() + self.save_config_btn.setVisible(False) + self.babystepPage.baby_stepchange = False @QtCore.pyqtSlot(str, list, name="activate_save_button") def activate_save_button(self, name: str, value: list) -> None: """Sync the `Save config` popup with the save_config_pending state""" - if not value: + if not value or name != "homing_origin" or len(value) <= 2: return - - if name == "homing_origin": - self._active_z_offset = value[2] - self.save_config_btn.setVisible(value[2] != 0) + self._active_z_offset = value[2] + self.save_config_btn.setVisible(round(value[2], 3) != 0) def _on_delete_file_confirmed(self, filename: str, directory: str) -> None: - """Handle confirmed file deletion after user accepted the dialog""" + """Handle confirmed file deletion after user accepted the dialog.""" self.file_data.on_request_delete_file(filename, directory) self.request_back.emit() self.filesPage_widget.reset_dir() - self.BasePopup.disconnect() + try: + self.BasePopup.accepted.disconnect() + except (RuntimeError, TypeError): + pass def setProperty(self, name: str, value: typing.Any) -> bool: """Intercept the set property method @@ -383,6 +435,12 @@ def setProperty(self, name: str, value: typing.Any) -> bool: def handle_cancel_print(self) -> None: """Handles the print cancel action""" + if ( + not self._finish_print_handled + and self._active_z_offset != 0 + and self.babystepPage.baby_stepchange + ): + self._cancel_z_snapshot = self._active_z_offset self.ws.api.cancel_print() self.call_load_panel.emit(True, "Cancelling print...\nPlease wait") @@ -404,6 +462,18 @@ def klipper_ready_signal(self) -> None: """React to klipper ready signal""" self.babystepPage.baby_stepchange = False self._finish_print_handled = False + self._cancel_z_snapshot = 0.0 + self.printer.on_subscribe_config("stepper_z", self._on_stepper_z_config) + + def _on_stepper_z_config(self, config: dict | list) -> None: + """Select the correct Z-offset apply command based on endstop type.""" + if not isinstance(config, dict): + return + stepper_z = config.get("stepper_z", {}) + if stepper_z.get("endstop_pin") == "probe:z_virtual_endstop": + self._z_apply_command = "Z_OFFSET_APPLY_PROBE" + else: + self._z_apply_command = "Z_OFFSET_APPLY_ENDSTOP" @QtCore.pyqtSlot(name="finish_print_signal") def finish_print_signal(self) -> None: @@ -413,6 +483,7 @@ def finish_print_signal(self) -> None: if self._active_z_offset != 0 and self.babystepPage.baby_stepchange: self.save_config() self._finish_print_handled = True + self.save_config_btn.setVisible(round(self._active_z_offset, 3) != 0) def setupMainPrintPage(self) -> None: """Setup UI for print page""" diff --git a/BlocksScreen/lib/panels/widgets/babystepPage.py b/BlocksScreen/lib/panels/widgets/babystepPage.py index 273e8f9c..1b632c17 100644 --- a/BlocksScreen/lib/panels/widgets/babystepPage.py +++ b/BlocksScreen/lib/panels/widgets/babystepPage.py @@ -1,3 +1,4 @@ +import logging import typing from lib.utils.blocks_label import BlocksLabel @@ -5,8 +6,20 @@ from lib.utils.icon_button import IconButton from PyQt6 import QtCore, QtGui, QtWidgets +logger = logging.getLogger(__name__) + +# Button definitions: (label, value, object_name, initially_checked) +_OFFSET_STEPS: list[tuple[str, float, str, bool]] = [ + ("0.010 mm", 0.01, "bbp_nozzle_offset_01", True), + ("0.025 mm", 0.025, "bbp_nozzle_offset_025", False), + ("0.050 mm", 0.05, "bbp_nozzle_offset_05", False), + ("0.100 mm", 0.1, "bbp_nozzle_offset_1", False), +] + class BabystepPage(QtWidgets.QWidget): + """Page for adjusting Z offset in small increments during a print.""" + request_back: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( name="request_back" ) @@ -14,117 +27,131 @@ class BabystepPage(QtWidgets.QWidget): str, name="run_gcode" ) - _z_offset: float = 0.1 - def __init__(self, parent) -> None: super().__init__(parent) + self._z_offset: float = _OFFSET_STEPS[0][1] self.setObjectName("babystepPage") self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_MouseTracking, True) self.setTabletTracking(True) self.setMouseTracking(True) - self.setupUI() + self._baby_stepchange = False + self._z_offset_text: float = 0.0 + self._pending_z_offset: float = 0.0 + + self._setupUI() self.bbp_mvup.clicked.connect(self.on_move_nozzle_close) self.bbp_mvdown.clicked.connect(self.on_move_nozzle_away) + self.bbp_mvup.setEnabled(False) + self.bbp_mvdown.setEnabled(False) self.babystep_back_btn.clicked.connect(self.request_back.emit) - self.bbp_nozzle_offset_01.toggled.connect(self.handle_z_offset_change) - self.bbp_nozzle_offset_025.toggled.connect(self.handle_z_offset_change) - self.bbp_nozzle_offset_05.toggled.connect(self.handle_z_offset_change) - self.bbp_nozzle_offset_1.toggled.connect(self.handle_z_offset_change) - self._baby_stepchange = False @property def baby_stepchange(self): - """Returns if the babystep was changed during print""" + """Returns if the babystep was changed during print.""" return self._baby_stepchange @baby_stepchange.setter def baby_stepchange(self, value: bool) -> None: - if not isinstance(value, bool): - raise ValueError("Value must be a bool") + """Set the babystep-changed flag.""" self._baby_stepchange = value @QtCore.pyqtSlot(name="on_move_nozzle_close") def on_move_nozzle_close(self) -> None: - """Move the nozzle closer to the print plate - by the amount set in **` self._z_offset`** - """ - self.run_gcode.emit( - f"SET_GCODE_OFFSET Z_ADJUST=-{self._z_offset} MOVE=1" # Z_ADJUST adds the value to the existing offset + """Move the nozzle closer to the print plate.""" + self.run_gcode.emit(f"SET_GCODE_OFFSET Z_ADJUST=-{self._z_offset} MOVE=1") + self._pending_z_offset -= self._z_offset + self.bbp_z_offset_current_value.setText( + f"Z: {round(self._pending_z_offset, 3) or 0.0:.3f} mm" ) self._baby_stepchange = True @QtCore.pyqtSlot(name="on_move_nozzle_away") def on_move_nozzle_away(self) -> None: - """Slot for Babystep button to get far from the - bed by **` self._z_offset`** amount - """ - self.run_gcode.emit( - f"SET_GCODE_OFFSET Z_ADJUST=+{self._z_offset} MOVE=1" # Z_ADJUST adds the value to the existing offset + """Move the nozzle away from the print plate.""" + self.run_gcode.emit(f"SET_GCODE_OFFSET Z_ADJUST=+{self._z_offset} MOVE=1") + self._pending_z_offset += self._z_offset + self.bbp_z_offset_current_value.setText( + f"Z: {round(self._pending_z_offset, 3) or 0.0:.3f} mm" ) self._baby_stepchange = True - @QtCore.pyqtSlot(name="handle_z_offset_change") - def handle_z_offset_change(self) -> None: - """Helper method for changing the value for Babystep. - - When a button is clicked, and the button has the mm value i the text, - it'll change the internal value **z_offset** to the same has the button + @QtCore.pyqtSlot(str, str, name="on_print_state_update") + def on_print_state_update(self, field: str, value: str) -> None: + """Enable move buttons only while the printer is actively printing.""" + if "state" in field: + printing = value == "printing" + self.bbp_mvup.setEnabled(printing) + self.bbp_mvdown.setEnabled(printing) - *** - - Possible values are: 0.01, 0.025, 0.05, 0.1 **mm** - """ - _sender: QtCore.QObject | None = self.sender() - if self._z_offset == float(_sender.text()[:-3]): - return - self._z_offset = float(_sender.text()[:-3]) + def _set_z_offset(self, value: float) -> None: + """Update the active step size.""" + if self._z_offset != value: + self._z_offset = value + @QtCore.pyqtSlot(str, list, name="on_gcode_move_update") def on_gcode_move_update(self, name: str, value: list) -> None: - """Handle gcode move updates""" + """Handle gcode move updates from Klipper.""" if not value: return - if name == "homing_origin": - self._z_offset_text = value[2] - self.bbp_z_offset_current_value.setText(f"Z: {self._z_offset_text:.3f}mm") - - def setupUI(self): - """Setup babystep page ui""" - self.bbp_offset_value_selector_group = QtWidgets.QButtonGroup(self) - self.bbp_offset_value_selector_group.setExclusive(True) - sizePolicy = QtWidgets.QSizePolicy( + if name == "homing_origin" and len(value) > 2: + confirmed = value[2] + self._z_offset_text = confirmed + self.bbp_z_offset_title_label.setText( + f"Z: {round(confirmed, 3) or 0.0:.3f} mm" + ) + # Always sync pending offset to Klipper's confirmed value + self._pending_z_offset = confirmed + self.bbp_z_offset_current_value.setText( + f"Z: {round(confirmed, 3) or 0.0:.3f} mm" + ) + + def _create_offset_button( + self, + parent: QtWidgets.QWidget, + label: str, + obj_name: str, + checked: bool, + font: QtGui.QFont, + ) -> BlocksCustomCheckButton: + """Create a single offset-step check button.""" + btn = BlocksCustomCheckButton(parent=parent) + btn.setMinimumSize(QtCore.QSize(100, 70)) + btn.setMaximumSize(QtCore.QSize(100, 70)) + btn.setText(label) + btn.setFont(font) + btn.setCheckable(True) + btn.setChecked(checked) + btn.setFlat(True) + btn.setProperty("button_type", "") + btn.setObjectName(obj_name) + return btn + + def _setupUI(self) -> None: + """Setup babystep page UI.""" + btn_group = QtWidgets.QButtonGroup(self) + btn_group.setExclusive(True) + + size_policy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding, ) - sizePolicy.setHorizontalStretch(1) - sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) - self.setSizePolicy(sizePolicy) + size_policy.setHorizontalStretch(1) + size_policy.setVerticalStretch(1) + size_policy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) + self.setSizePolicy(size_policy) self.setMinimumSize(QtCore.QSize(710, 400)) - self.setMaximumSize( - QtCore.QSize(720, 420) - ) # This sets the maximum width of the entire page + self.setMaximumSize(QtCore.QSize(720, 420)) self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - # Main Vertical Layout for the entire page - self.verticalLayout = QtWidgets.QVBoxLayout(self) - self.verticalLayout.setObjectName("verticalLayout") + main_vlayout = QtWidgets.QVBoxLayout(self) - # Header Layout - self.bbp_header_layout = QtWidgets.QHBoxLayout() - self.bbp_header_layout.setObjectName("bbp_header_layout") - self.bbp_header_title = QtWidgets.QLabel(parent=self) - sizePolicy.setHeightForWidth( - self.bbp_header_title.sizePolicy().hasHeightForWidth() - ) - self.bbp_header_title.setSizePolicy(sizePolicy) - self.bbp_header_title.setMinimumSize(QtCore.QSize(200, 60)) - self.bbp_header_title.setMaximumSize(QtCore.QSize(16777215, 60)) - font = QtGui.QFont() - font.setPointSize(22) - self.bbp_header_title.setFont(font) + header = QtWidgets.QHBoxLayout() + + title_font = QtGui.QFont() + title_font.setPointSize(22) palette = QtGui.QPalette() palette.setColor( palette.ColorGroup.All, @@ -136,187 +163,65 @@ def setupUI(self): palette.ColorRole.WindowText, QtGui.QColor("#FFFFFF"), ) + + self.bbp_header_title = QtWidgets.QLabel("Babystep", parent=self) + self.bbp_header_title.setMinimumSize(QtCore.QSize(200, 60)) + self.bbp_header_title.setMaximumSize(QtCore.QSize(16777215, 60)) + self.bbp_header_title.setFont(title_font) self.bbp_header_title.setAutoFillBackground(True) self.bbp_header_title.setBackgroundRole(palette.ColorRole.Window) self.bbp_header_title.setPalette(palette) - self.bbp_header_title.setText("Babystep") self.bbp_header_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.bbp_header_title.setObjectName("bbp_header_title") - - spacerItem = QtWidgets.QSpacerItem( - 60, - 20, - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - self.bbp_header_layout.addItem(spacerItem) + header.addWidget(self.bbp_header_title, 1) - self.bbp_header_layout.addWidget( - self.bbp_header_title, - 0, - QtCore.Qt.AlignmentFlag.AlignCenter, - ) self.babystep_back_btn = IconButton(parent=self) - sizePolicy.setHeightForWidth( - self.babystep_back_btn.sizePolicy().hasHeightForWidth() - ) - self.babystep_back_btn.setSizePolicy(sizePolicy) + self.babystep_back_btn.setSizePolicy(size_policy) self.babystep_back_btn.setMinimumSize(QtCore.QSize(60, 60)) self.babystep_back_btn.setMaximumSize(QtCore.QSize(60, 60)) - self.babystep_back_btn.setText("") self.babystep_back_btn.setFlat(True) self.babystep_back_btn.setPixmap(QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) - self.babystep_back_btn.setObjectName("babystep_back_btn") - - self.bbp_header_layout.addWidget( - self.babystep_back_btn, - 0, - QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - self.bbp_header_layout.setStretch(0, 1) - self.verticalLayout.addLayout(self.bbp_header_layout) - - self.main_content_horizontal_layout = QtWidgets.QHBoxLayout() - self.main_content_horizontal_layout.setObjectName( - "main_content_horizontal_layout" - ) - - # Offset Steps Buttons Group Box (LEFT side of main_content_horizontal_layout) - self.bbp_offset_steps_buttons_group_box = QtWidgets.QGroupBox(self) - font = QtGui.QFont() - font.setPointSize(14) - self.bbp_offset_steps_buttons_group_box.setFont(font) - self.bbp_offset_steps_buttons_group_box.setFlat(True) - # Add stylesheet to explicitly remove any border from the QGroupBox - self.bbp_offset_steps_buttons_group_box.setStyleSheet( - "QGroupBox { border: none; }" - ) - self.bbp_offset_steps_buttons_group_box.setObjectName( - "bbp_offset_steps_buttons_group_box" - ) - - self.bbp_offset_steps_buttons = QtWidgets.QVBoxLayout( - self.bbp_offset_steps_buttons_group_box - ) - self.bbp_offset_steps_buttons.setContentsMargins(9, 9, 9, 9) - self.bbp_offset_steps_buttons.setObjectName("bbp_offset_steps_buttons") - - # 0.1mm button - self.bbp_nozzle_offset_1 = BlocksCustomCheckButton( - parent=self.bbp_offset_steps_buttons_group_box - ) - self.bbp_nozzle_offset_1.setMinimumSize(QtCore.QSize(100, 70)) - self.bbp_nozzle_offset_1.setMaximumSize(QtCore.QSize(100, 70)) - self.bbp_nozzle_offset_1.setText("0.1 mm") - - font = QtGui.QFont() - font.setPointSize(14) - self.bbp_nozzle_offset_1.setFont(font) - self.bbp_nozzle_offset_1.setCheckable(True) - self.bbp_nozzle_offset_1.setChecked(True) # Set as initially checked - self.bbp_nozzle_offset_1.setFlat(True) - self.bbp_nozzle_offset_1.setProperty("button_type", "") - self.bbp_nozzle_offset_1.setObjectName("bbp_nozzle_offset_1") - self.bbp_offset_value_selector_group.addButton(self.bbp_nozzle_offset_1) - self.bbp_offset_steps_buttons.addWidget( - self.bbp_nozzle_offset_1, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - - # 0.05mm button - self.bbp_nozzle_offset_05 = BlocksCustomCheckButton( - parent=self.bbp_offset_steps_buttons_group_box - ) - self.bbp_nozzle_offset_05.setMinimumSize(QtCore.QSize(100, 70)) - self.bbp_nozzle_offset_05.setMaximumSize( - QtCore.QSize(100, 70) - ) # Increased max width by 5 pixels - self.bbp_nozzle_offset_05.setText("0.05 mm") - - font = QtGui.QFont() - font.setPointSize(14) - self.bbp_nozzle_offset_05.setFont(font) - self.bbp_nozzle_offset_05.setCheckable(True) - self.bbp_nozzle_offset_05.setFlat(True) - self.bbp_nozzle_offset_05.setProperty("button_type", "") - self.bbp_nozzle_offset_05.setObjectName("bbp_nozzle_offset_05") - self.bbp_offset_value_selector_group.addButton(self.bbp_nozzle_offset_05) - self.bbp_offset_steps_buttons.addWidget( - self.bbp_nozzle_offset_05, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - - # Line separator for 0.1mm - set size policy to expanding horizontally - - # 0.01mm button - self.bbp_nozzle_offset_01 = BlocksCustomCheckButton( - parent=self.bbp_offset_steps_buttons_group_box - ) - self.bbp_nozzle_offset_01.setMinimumSize(QtCore.QSize(100, 70)) - self.bbp_nozzle_offset_01.setMaximumSize( - QtCore.QSize(100, 70) - ) # Increased max width by 5 pixels - self.bbp_nozzle_offset_01.setText("0.01 mm") - - font = QtGui.QFont() - font.setPointSize(14) - self.bbp_nozzle_offset_01.setFont(font) - self.bbp_nozzle_offset_01.setCheckable(True) - self.bbp_nozzle_offset_01.setFlat(True) - self.bbp_nozzle_offset_01.setProperty("button_type", "") - self.bbp_nozzle_offset_01.setObjectName("bbp_nozzle_offset_01") - self.bbp_offset_value_selector_group.addButton(self.bbp_nozzle_offset_01) - self.bbp_offset_steps_buttons.addWidget( - self.bbp_nozzle_offset_01, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - - # 0.025mm button - self.bbp_nozzle_offset_025 = BlocksCustomCheckButton( - parent=self.bbp_offset_steps_buttons_group_box - ) - self.bbp_nozzle_offset_025.setMinimumSize(QtCore.QSize(100, 70)) - self.bbp_nozzle_offset_025.setMaximumSize( - QtCore.QSize(100, 70) - ) # Increased max width by 5 pixels - self.bbp_nozzle_offset_025.setText("0.025 mm") - - font = QtGui.QFont() - font.setPointSize(14) - self.bbp_nozzle_offset_025.setFont(font) - self.bbp_nozzle_offset_025.setCheckable(True) - self.bbp_nozzle_offset_025.setFlat(True) - self.bbp_nozzle_offset_025.setProperty("button_type", "") - self.bbp_nozzle_offset_025.setObjectName("bbp_nozzle_offset_025") - self.bbp_offset_value_selector_group.addButton(self.bbp_nozzle_offset_025) - self.bbp_offset_steps_buttons.addWidget( - self.bbp_nozzle_offset_025, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - - # Line separator for 0.025mm - set size policy to expanding horizontally - - # Set the layout for the group box - self.bbp_offset_steps_buttons_group_box.setLayout(self.bbp_offset_steps_buttons) - # Add the group box to the main content horizontal layout FIRST for left placement - self.main_content_horizontal_layout.addWidget( - self.bbp_offset_steps_buttons_group_box - ) - - # Graphic and Current Value Frame (This will now be in the MIDDLE) - self.frame_2 = QtWidgets.QFrame(parent=self) - sizePolicy.setHeightForWidth(self.frame_2.sizePolicy().hasHeightForWidth()) - self.frame_2.setSizePolicy(sizePolicy) - self.frame_2.setMinimumSize(QtCore.QSize(350, 160)) - self.frame_2.setMaximumSize(QtCore.QSize(350, 160)) - self.frame_2.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - self.frame_2.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_2.setObjectName("frame_2") - self.bbp_babystep_graphic = QtWidgets.QLabel(parent=self.frame_2) + header.addWidget(self.babystep_back_btn, 0) + main_vlayout.addLayout(header) + + # --- Main content (3 columns) --- + content = QtWidgets.QHBoxLayout() + + # Column 1: offset step buttons (highest → lowest) + group_box = QtWidgets.QGroupBox(self) + btn_font = QtGui.QFont() + btn_font.setPointSize(14) + group_box.setFont(btn_font) + group_box.setFlat(True) + group_box.setStyleSheet("QGroupBox { border: none; }") + + steps_layout = QtWidgets.QVBoxLayout(group_box) + steps_layout.setContentsMargins(9, 9, 9, 9) + + center = ( + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter + ) + for label, value, obj_name, checked in _OFFSET_STEPS: + btn = self._create_offset_button( + group_box, label, obj_name, checked, btn_font + ) + btn.toggled.connect( + lambda checked_state, v=value: checked_state and self._set_z_offset(v) + ) + setattr(self, obj_name, btn) + btn_group.addButton(btn) + steps_layout.addWidget(btn, 0, center) + + content.addWidget(group_box) + + # Column 2: graphic + Z offset labels + frame = QtWidgets.QFrame(parent=self) + frame.setSizePolicy(size_policy) + frame.setMinimumSize(QtCore.QSize(350, 160)) + frame.setMaximumSize(QtCore.QSize(350, 160)) + frame.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + + self.bbp_babystep_graphic = QtWidgets.QLabel(parent=frame) self.bbp_babystep_graphic.setGeometry(QtCore.QRect(0, 30, 371, 121)) self.bbp_babystep_graphic.setLayoutDirection( QtCore.Qt.LayoutDirection.RightToLeft @@ -326,129 +231,92 @@ def setupUI(self): ) self.bbp_babystep_graphic.setScaledContents(False) self.bbp_babystep_graphic.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.bbp_babystep_graphic.setObjectName("bbp_babystep_graphic") - # === NEW LABEL ADDED HERE === - # This is the title label that appears above the red value box. + grey_font = QtGui.QFont() + grey_font.setPointSize(12) self.bbp_z_offset_title_label = QtWidgets.QLabel(parent=self) - # Position it just above the red box. Red box is at y=70, so y=40 is appropriate. - self.bbp_z_offset_title_label.setGeometry(QtCore.QRect(100, 40, 200, 30)) - font = QtGui.QFont() - font.setPointSize(12) - - self.bbp_z_offset_title_label.setFont(font) - # Set color to white to be visible on the dark background + self.bbp_z_offset_title_label.setFont(grey_font) self.bbp_z_offset_title_label.setStyleSheet( "color: gray; background: transparent;" ) - self.bbp_z_offset_title_label.setObjectName("bbp_z_offset_title_label") - self.bbp_z_offset_title_label.setText("Z: 0.000mm") + self.bbp_z_offset_title_label.setText( + f"Z: {round(self._z_offset_text, 3) or 0.0:.3f} mm" + ) self.bbp_z_offset_title_label.setGeometry(420, 270, 200, 30) - # === END OF NEW LABEL === - - self.bbp_z_offset_current_value = BlocksLabel(parent=self.frame_2) + white_font = QtGui.QFont() + white_font.setPointSize(14) + self.bbp_z_offset_current_value = BlocksLabel(parent=frame) self.bbp_z_offset_current_value.setGeometry(QtCore.QRect(100, 70, 200, 60)) - sizePolicy.setHeightForWidth( - self.bbp_z_offset_current_value.sizePolicy().hasHeightForWidth() - ) - self.bbp_z_offset_current_value.setSizePolicy(sizePolicy) + self.bbp_z_offset_current_value.setSizePolicy(size_policy) self.bbp_z_offset_current_value.setMinimumSize(QtCore.QSize(150, 60)) self.bbp_z_offset_current_value.setMaximumSize(QtCore.QSize(200, 60)) - font = QtGui.QFont() - font.setPointSize(14) - self.bbp_z_offset_current_value.setFont(font) + self.bbp_z_offset_current_value.setFont(white_font) self.bbp_z_offset_current_value.setStyleSheet( "background: transparent; color: white;" ) - self.bbp_z_offset_current_value.setText(f"Z: {self._z_offset:.2f}mm") + self.bbp_z_offset_current_value.setText( + f"Z: {round(self._pending_z_offset, 3) or 0.0:.3f} mm" + ) self.bbp_z_offset_current_value.setPixmap( QtGui.QPixmap(":/graphics/media/btn_icons/z_offset_adjust.svg") ) self.bbp_z_offset_current_value.setAlignment( QtCore.Qt.AlignmentFlag.AlignCenter ) - self.bbp_z_offset_current_value.setObjectName("bbp_z_offset_current_value") - # Add graphic frame AFTER the offset buttons group box - self.main_content_horizontal_layout.addWidget( - self.frame_2, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, + + content.addWidget(frame, 0, center) + + # Spacer before move buttons + content.addItem( + QtWidgets.QSpacerItem( + 40, + 20, + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Minimum, + ) ) - # Move Buttons Layout (This will now be on the RIGHT) - self.bbp_buttons_layout = QtWidgets.QVBoxLayout() - self.bbp_buttons_layout.setContentsMargins(5, 5, 5, 5) - self.bbp_buttons_layout.setObjectName("bbp_buttons_layout") + # Column 3: move up/down buttons + move_layout = QtWidgets.QVBoxLayout() + move_layout.setContentsMargins(5, 5, 5, 5) + self.bbp_mvup = IconButton(parent=self) - sizePolicy.setHeightForWidth(self.bbp_mvup.sizePolicy().hasHeightForWidth()) - self.bbp_mvup.setSizePolicy(sizePolicy) + self.bbp_mvup.setSizePolicy(size_policy) self.bbp_mvup.setMinimumSize(QtCore.QSize(80, 80)) self.bbp_mvup.setMaximumSize(QtCore.QSize(80, 80)) - self.bbp_mvup.setText("") self.bbp_mvup.setFlat(True) self.bbp_mvup.setPixmap( QtGui.QPixmap(":/baby_step/media/btn_icons/move_nozzle_close.svg") ) - self.bbp_mvup.setObjectName("bbp_away_from_bed") - self.bbp_option_button_group = QtWidgets.QButtonGroup(self) - self.bbp_option_button_group.setObjectName("bbp_option_button_group") - self.bbp_option_button_group.addButton(self.bbp_mvup) - self.bbp_buttons_layout.addWidget( - self.bbp_mvup, 0, QtCore.Qt.AlignmentFlag.AlignRight - ) + move_layout.addWidget(self.bbp_mvup, 0, QtCore.Qt.AlignmentFlag.AlignRight) + self.bbp_mvdown = IconButton(parent=self) - sizePolicy.setHeightForWidth(self.bbp_mvdown.sizePolicy().hasHeightForWidth()) - self.bbp_mvdown.setSizePolicy(sizePolicy) + self.bbp_mvdown.setSizePolicy(size_policy) self.bbp_mvdown.setMinimumSize(QtCore.QSize(80, 80)) self.bbp_mvdown.setMaximumSize(QtCore.QSize(80, 80)) - self.bbp_mvdown.setText("") self.bbp_mvdown.setFlat(True) self.bbp_mvdown.setPixmap( QtGui.QPixmap(":/baby_step/media/btn_icons/move_nozzle_away.svg") ) - self.bbp_mvdown.setObjectName("bbp_close_to_bed") - self.bbp_option_button_group.addButton(self.bbp_mvdown) - self.bbp_buttons_layout.addWidget( - self.bbp_mvdown, 0, QtCore.Qt.AlignmentFlag.AlignRight - ) - spacerItem = QtWidgets.QSpacerItem( - 40, - 20, - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - self.main_content_horizontal_layout.addItem(spacerItem) + move_layout.addWidget(self.bbp_mvdown, 0, QtCore.Qt.AlignmentFlag.AlignRight) - # Add move buttons layout LAST for right placement - self.main_content_horizontal_layout.addLayout(self.bbp_buttons_layout) + content.addLayout(move_layout) - spacerItem = QtWidgets.QSpacerItem( - 40, - 20, - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Minimum, + # Trailing spacer + content.addItem( + QtWidgets.QSpacerItem( + 40, + 20, + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Minimum, + ) ) - self.main_content_horizontal_layout.addItem(spacerItem) - - # Set stretch factors for main content horizontal layout - # This will distribute space: offset buttons, graphic frame, move buttons - self.main_content_horizontal_layout.setStretch( - 0, 1 - ) # offset_steps_buttons_group_box - self.main_content_horizontal_layout.setStretch( - 1, 2 - ) # frame_2 (graphic and current value) - self.main_content_horizontal_layout.setStretch( - 2, 0 - ) # bbp_buttons_layout (move buttons) - - # Add the main content horizontal layout to the vertical layout - self.verticalLayout.addLayout(self.main_content_horizontal_layout) - - # Set stretch factors for vertical layout (adjust as needed for overall sizing) - self.verticalLayout.setStretch( - 1, 1 - ) # This stretch applies to main_content_horizontal_layout - - self.setLayout(self.verticalLayout) + + content.setStretch(0, 1) # offset buttons + content.setStretch(1, 2) # graphic frame + content.setStretch(2, 0) # move buttons + + main_vlayout.addLayout(content) + main_vlayout.setStretch(1, 1) + self.setLayout(main_vlayout) diff --git a/BlocksScreen/lib/panels/widgets/probeHelperPage.py b/BlocksScreen/lib/panels/widgets/probeHelperPage.py index 219adb6a..8170b8ba 100644 --- a/BlocksScreen/lib/panels/widgets/probeHelperPage.py +++ b/BlocksScreen/lib/panels/widgets/probeHelperPage.py @@ -1,3 +1,5 @@ +import enum +import logging import typing from lib.panels.widgets.optionCardWidget import OptionCard @@ -7,6 +9,36 @@ from lib.utils.icon_button import IconButton from PyQt6 import QtCore, QtGui, QtWidgets +logger = logging.getLogger(__name__) + +_PROBE_MOVE_STEPS: list[tuple[str, float, str, bool]] = [ + ("0.010 mm", 0.010, "move_option_1", True), + ("0.025 mm", 0.025, "move_option_2", False), + ("0.100 mm", 0.100, "move_option_3", False), + ("0.500 mm", 0.500, "move_option_4", False), + ("1.000 mm", 1.000, "move_option_5", False), +] + +_TRACKED_GCODES: frozenset[str] = frozenset( + { + "PROBE_CALIBRATE", + "PROBE_EDDY_CURRENT_CALIBRATE", + "LDC_CALIBRATE_DRIVE_CURRENT", + "Z_ENDSTOP_CALIBRATE", + "MANUAL_PROBE", + "CLEAN_NOZZLE", + } +) + + +class _CalibPhase(enum.Enum): + IDLE = "idle" + PROBE_ACTIVE = "probe_active" # non-eddy: CLEAN_NOZZLE/homing before probe session + EDDY_PHASE1 = "eddy_phase1" # LDC drive-current calibration → first SAVE_CONFIG + EDDY_PHASE1_RESTART = "eddy_phase1_restart" # post-Phase1 restart, awaiting standby + EDDY_PHASE2 = "eddy_phase2" # Z offset calibration → second SAVE_CONFIG + SAVE_RESTART = "save_restart" # non-eddy SAVE_CONFIG restart + class ProbeHelper(QtWidgets.QWidget): request_back: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( @@ -33,26 +65,36 @@ class ProbeHelper(QtWidgets.QWidget): request_page_view: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( name="request_page_view" ) - call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") - - toggle_conn_page = QtCore.pyqtSignal(bool, name="toggles-conn-panel") + call_load_panel: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + bool, str, name="call-load-panel" + ) + toggle_conn_page: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + bool, name="toggles-conn-panel" + ) disable_popups: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( bool, name="disable-popups" ) - - distances = ["0.01", ".025", "0.1", "0.5", "1"] - _calibration_commands: list = [] - helper_start: bool = False - helper_initialize: bool = False - _zhop_height: float = float(distances[0]) - card_options: dict = {} - z_offset_method_type: str = "" - z_offset_config_method: tuple = () - z_offset_calibration_speed: int = 100 + lock_ui: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + bool, name="lock-ui" + ) + show_notifications: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, str, int, bool, name="show-notifications" + ) def __init__(self, parent: QtWidgets.QWidget) -> None: super().__init__(parent) + self.helper_start: bool = False + self.helper_initialize: bool = False + self._zhop_height: float = _PROBE_MOVE_STEPS[0][1] + self.z_offset_method_type: str = "" + self.z_offset_config_method: tuple = () + self.z_offset_calibration_speed: int = 100 + self.z_offsets: tuple = () + self._calibration_commands: set = set() + self.card_options: dict = {} + self.z_offset_config_type: str = "" + self._eddy_command: str = "" self.setObjectName("probe_offset_page") self._setupUi() @@ -65,97 +107,108 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: ) self.eddy_icon = QtGui.QPixmap(":/z_levelling/media/btn_icons/eddy_mech.svg") self._toggle_tool_buttons(False) - self._setup_move_option_buttons() - self.move_option_1.toggled.connect( - lambda: self.handle_zhopHeight_change(new_value=float(self.distances[0])) - ) - self.move_option_2.toggled.connect( - lambda: self.handle_zhopHeight_change(new_value=float(self.distances[1])) - ) - self.move_option_3.toggled.connect( - lambda: self.handle_zhopHeight_change(new_value=float(self.distances[2])) - ) - self.move_option_4.toggled.connect( - lambda: self.handle_zhopHeight_change(new_value=float(self.distances[3])) - ) - self.move_option_5.toggled.connect( - lambda: self.handle_zhopHeight_change(new_value=float(self.distances[4])) - ) self.mb_raise_nozzle.clicked.connect(lambda: self.handle_nozzle_move("raise")) self.mb_lower_nozzle.clicked.connect(lambda: self.handle_nozzle_move("lower")) self.po_back_button.clicked.connect(self.request_back) self.accept_button.clicked.connect(self.handle_accept) self.abort_button.clicked.connect(self.handle_abort) - self.update() self.block_z = False self.block_list = False self.target_temp = 0 self.current_temp = 0 - self._eddy_calibration_state = False + self._calib_phase = _CalibPhase.IDLE + self._active_calibration_tool: str = "" @QtCore.pyqtSlot(str, dict, name="on_print_stats_update") @QtCore.pyqtSlot(str, float, name="on_print_stats_update") @QtCore.pyqtSlot(str, str, name="on_print_stats_update") def on_print_stats_update(self, field: str, value: dict | float | str) -> None: """Handle print stats object update""" - if isinstance(value, str): - if "state" in field: - if value in ("standby"): - if self._eddy_calibration_state: - self.run_gcode_signal.emit("G28\nM400") - self._move_to_pos( - self.z_offset_safe_xy[0], self.z_offset_safe_xy[1], 100 - ) - self.call_load_panel.emit(True, "Almost done...\nPlease wait") - self.run_gcode_signal.emit(self._eddy_command) - - self.request_page_view.emit() - - self.disable_popups.emit(False) - self.toggle_conn_page.emit(True) - - self._eddy_calibration_state = False - - def on_klippy_status(self, state: str): + if isinstance(value, str) and "state" in field and value == "standby": + if self._calib_phase in ( + _CalibPhase.EDDY_PHASE1, + _CalibPhase.EDDY_PHASE1_RESTART, + ): + self.call_load_panel.emit( + True, "Running Z offset calibration\nMoving to position..." + ) + self.run_gcode_signal.emit(self._eddy_command) + self.request_page_view.emit() + self.disable_popups.emit(False) + self.toggle_conn_page.emit(True) + self._calib_phase = _CalibPhase.IDLE + elif self._calib_phase in ( + _CalibPhase.EDDY_PHASE2, + _CalibPhase.SAVE_RESTART, + ): + self._calib_phase = _CalibPhase.IDLE + self.run_gcode_signal.emit("G28") + self.request_page_view.emit() + self._restore_ui() + + def on_klippy_status(self, state: str) -> None: """Handle Klippy status event change""" - if state.lower() == "standby": + _state = state.lower() + if _state == "disconnected": + if self._calib_phase in ( + _CalibPhase.EDDY_PHASE1, + _CalibPhase.EDDY_PHASE1_RESTART, + _CalibPhase.EDDY_PHASE2, + _CalibPhase.SAVE_RESTART, + ): + self.helper_start = False + self.helper_initialize = False + match self._calib_phase: + case _CalibPhase.EDDY_PHASE2: + msg = "Saving calibration data\nRestarting Klipper..." + case _CalibPhase.SAVE_RESTART: + msg = "Saving configuration\nRestarting Klipper..." + case _: + msg = "Restarting Klipper..." + self.call_load_panel.emit(True, msg) + else: + self._cancel_calibration() + elif _state == "ready": + match self._calib_phase: + case _CalibPhase.EDDY_PHASE1: + self._calib_phase = _CalibPhase.EDDY_PHASE1_RESTART + self.call_load_panel.emit( + True, + "Wait for the toolhead to park.\nPlace a sheet of paper under the nozzle." + "\nAdjust until it drags slightly.", + ) + case _CalibPhase.EDDY_PHASE2: + self.call_load_panel.emit( + True, "Calibration saved\nHoming printer..." + ) + case _CalibPhase.SAVE_RESTART: + self.call_load_panel.emit( + True, "Configuration saved\nHoming printer..." + ) + elif _state == "shutdown": + if self._calib_phase != _CalibPhase.IDLE: + self._cancel_calibration() + elif _state == "standby": self.block_z = False self.block_list = False - # Safely remove all items (widgets, spacers, sub-layouts) from the layout. - layout = self.main_content_horizontal_layout - if layout is not None: - while layout.count(): - item = layout.takeAt(0) - if item is None: - continue - widget = item.widget() - if widget is not None: - # Remove widget from layout and schedule for deletion - widget.setParent(None) - widget.deleteLater() - continue - child_layout = item.layout() - if child_layout is not None: - # Clear child layouts recursively - while child_layout.count(): - child_item = child_layout.takeAt(0) - if child_item is None: - continue - child_widget = child_item.widget() - if child_widget is not None: - child_widget.setParent(None) - child_widget.deleteLater() - - def handle_nozzle_move(self, direction: str): + for card in list(self.card_options.values()): + self.main_content_horizontal_layout.removeWidget(card) + card.setParent(None) + card.deleteLater() + self.card_options.clear() + + def handle_nozzle_move(self, direction: str) -> None: """Handle move z buttons click""" if direction == "raise": - self._pending_gcode = f"TESTZ Z={self._zhop_height}" + _gcode = f"TESTZ Z={self._zhop_height}" elif direction == "lower": - self._pending_gcode = f"TESTZ Z=-{self._zhop_height}" + _gcode = f"TESTZ Z=-{self._zhop_height}" + else: + return self.accept_button.show() self.abort_button.show() - self.run_gcode_signal.emit(self._pending_gcode) + self.run_gcode_signal.emit(_gcode) self.update() def _configure_option_cards(self, probes_list: list[str]) -> None: @@ -181,27 +234,28 @@ def _configure_option_cards(self, probes_list: list[str]) -> None: _card_text = "Endstop Calibration" _icon = self.endstop_icon - _card = OptionCard(self, _card_text, str(probe), _icon) # type: ignore - _card.setObjectName(str(probe)) - self.card_options.update({str(probe): _card}) + _card = OptionCard(self, _card_text, probe, _icon) # type: ignore + if not hasattr(_card, "continue_clicked"): + _card.deleteLater() + continue + _card.setObjectName(probe) + self.card_options[probe] = _card self.main_content_horizontal_layout.addWidget( _card, alignment=QtCore.Qt.AlignmentFlag.AlignHCenter ) - if not hasattr(self.card_options.get(probe), "continue_clicked"): - del _card - self.card_options.pop(probe) - return - - self.card_options.get(probe).continue_clicked.connect( # type: ignore - self.handle_start_tool - ) - self.update() + _card.continue_clicked.connect(self.handle_start_tool) # type: ignore + self.update() def _hide_option_cards(self) -> None: - list(map(lambda x: x[1].hide(), self.card_options.items())) + """Hide all probe option cards.""" + for card in self.card_options.values(): + card.hide() def _show_option_cards(self) -> None: - list(map(lambda x: x[1].show(), self.card_options.items())) + """Show and re-enable all probe option cards.""" + for card in self.card_options.values(): + card.setEnabled(True) + card.show() def _init_probe_config(self) -> None: """Initialize internal probe tracking""" @@ -209,12 +263,10 @@ def _init_probe_config(self) -> None: return if self.z_offset_config_type != "endstop": self.z_offsets = tuple( - map( - lambda axis: self.z_offset_config_method[1].get(f"{axis}_offset"), - ["x", "y", "z"], - ) + self.z_offset_config_method[0].get(f"{axis}_offset") + for axis in ("x", "y", "z") ) - self.z_offset_calibration_speed = self.z_offset_config_method[1].get( + self.z_offset_calibration_speed = self.z_offset_config_method[0].get( "speed" ) @@ -230,22 +282,16 @@ def on_object_config(self, config: dict | list) -> None: if not config: return - # BUG: If i don't add if not self.probe_config i'll just receive the configuration a bunch of times if isinstance(config, list): if self.block_list: return - else: - self.block_list = True - - _keys = [] - if not isinstance(config, list): - return + self.block_list = True - list(map(lambda item: _keys.extend(item.keys()), config)) + _keys = [k for item in config for k in item] probe, *_ = config[0].items() self.z_offset_method_type = probe[0] # The one found first - self.z_offset_method_config = ( + self.z_offset_config_method = ( probe[1], "PROBE_CALIBRATE", "Z_OFFSET_APPLY_PROBE", @@ -256,16 +302,12 @@ def on_object_config(self, config: dict | list) -> None: self._configure_option_cards(_keys) elif isinstance(config, dict): - if config.get("stepper_z"): + if _config := config.get("stepper_z"): if self.block_z: return - else: - self.block_z = True + self.block_z = True _virtual_endstop = "probe:z_virtual_endstop" - _config = config.get("stepper_z") - if not _config: - return if _config.get("endstop_pin") == _virtual_endstop: # home with probe return self.z_offset_config_type = "endstop" @@ -276,39 +318,8 @@ def on_object_config(self, config: dict | list) -> None: ) self._configure_option_cards(["endstop"]) - if config.get("safe_z_home"): - _config = config.get("safe_z_home") - if not _config: - return - if _config.get("home_xy_position"): - if not _config.get("home_xy_position"): - return - self.z_offset_safe_xy = tuple( - map( - lambda value: float(value), - _config.get("home_xy_position").split(","), - ) - ) - return - if config.get("bed_mesh"): - # TODO: This configuration needs to be prioritized over the safe_z_home - # If available always use the zero reference xy - # position for the probe calibration - _config = config.get("bed_mesh") - if not _config: - return - if not _config.get("zero_reference_position"): - return - self.z_offset_safe_xy = tuple( - map( - lambda value: float(value), - _config.get("zero_reference_position").split(","), - ) - ) - return - @QtCore.pyqtSlot(dict, name="on_printer_config") - def on_printer_config(self, config: dict) -> None: + def on_printer_config(self, _config: dict) -> None: """Handle received printer config""" _probe_types = [ "probe", @@ -321,29 +332,13 @@ def on_printer_config(self, config: dict) -> None: _probe_types, self.on_object_config ) self.subscribe_config[str, "PyQt_PyObject"].emit( - str("stepper_z"), self.on_object_config - ) - self.subscribe_config[str, "PyQt_PyObject"].emit( - str("safe_z_home"), self.on_object_config - ) - self.subscribe_config[str, "PyQt_PyObject"].emit( - str("bed_mesh"), self.on_object_config + "stepper_z", self.on_object_config ) @QtCore.pyqtSlot(dict, name="on_available_gcode_cmds") def on_available_gcode_cmds(self, gcode_cmds: dict) -> None: """Setup available probe calibration commands""" - _available_commands = gcode_cmds.keys() - if "PROBE_CALIBRATE" in _available_commands: - self._calibration_commands.append("PROBE_CALIBRATE") - if "PROBE_EDDY_CURRENT_CALIBRATE" in _available_commands: - self._calibration_commands.append("PROBE_EDDY_CURRENT_CALIBRATE") - if "LDC_CALIBRATE_DRIVE_CURRENT" in _available_commands: - self._calibration_commands.append("LDC_CALIBRATE_DRIVE_CURRENT") - if "Z_ENDSTOP_CALIBRATE" in _available_commands: - self._calibration_commands.append("Z_ENDSTOP_CALIBRATE") - if "MANUAL_PROBE" in _available_commands: - self._calibration_commands.append("MANUAL_PROBE") + self._calibration_commands = gcode_cmds.keys() & _TRACKED_GCODES def _verify_gcode(self, gcode: str) -> bool: """Check if the specified gcode exists @@ -360,29 +355,23 @@ def _verify_gcode(self, gcode: str) -> bool: return gcode in self._calibration_commands def _build_calibration_command(self, tool: str) -> str: + """Return the calibration gcode command for the given tool name, or empty string if unavailable.""" if not tool: return "" if tool == "endstop": if self._verify_gcode("Z_ENDSTOP_CALIBRATE"): return "Z_ENDSTOP_CALIBRATE" elif "eddy" in tool: - if self._verify_gcode("PROBE_EDDY_CURRENT_CALIBRATE"): - _name = tool.split(" ")[1] - # if not _name: - # return "" - # return ( - # f"PROBE_EDDY_CURRENT_CALIBRATE CHIP={tool.split(' ')[1]}" - # ) - return ( - f"PROBE_EDDY_CURRENT_CALIBRATE CHIP={tool.split(' ')[1]}" - * bool(_name) - ) + ("" * ~bool(_name)) - - elif "probe" in tool or "bltouch" in tool: + parts = tool.split(" ", 1) + if len(parts) < 2 or not parts[1]: + return "" + if self._verify_gcode("LDC_CALIBRATE_DRIVE_CURRENT"): + return f"LDC_CALIBRATE_DRIVE_CURRENT CHIP={parts[1]}" + elif "probe" in tool or "bltouch" in tool or "smart_effector" in tool: if self._verify_gcode("PROBE_CALIBRATE"): - return "PROBE_CALIBRATE" + ( - str(" ") + f"SPEED={self.z_offset_calibration_speed}" - ) * bool(self.z_offset_calibration_speed) + if self.z_offset_calibration_speed: + return f"PROBE_CALIBRATE SPEED={self.z_offset_calibration_speed}" + return "PROBE_CALIBRATE" return "" @QtCore.pyqtSlot(float, name="handle_zhopHeight_change") @@ -403,7 +392,7 @@ def handle_zhopHeight_change(self, new_value: float) -> None: self._zhop_height = new_value @QtCore.pyqtSlot("PyQt_PyObject", name="handle_start_tool") - def handle_start_tool(self, sender: typing.Type[OptionCard]) -> None: + def handle_start_tool(self, sender: OptionCard) -> None: """Handle probe tool helper start by sending the correct gcode command according to the clicked option card. This is achieved by @@ -414,73 +403,83 @@ def handle_start_tool(self, sender: typing.Type[OptionCard]) -> None: sender. Args: - sender (typing.Type[OptionCard]): The clicked OptionCard object + sender (OptionCard): The clicked OptionCard instance """ if not sender: return + _name: str = sender.name # type: ignore + _cmd = self._build_calibration_command(_name) + if not _cmd: + return + + self._active_calibration_tool = _name for i in self.card_options.values(): i.setDisabled(True) - self.helper_initialize = True - _timer = QtCore.QTimer() - _timer.setSingleShot(True) - _timer.timeout.connect( - lambda: self.query_printer_object.emit({"manual_probe": None}) + QtCore.QTimer.singleShot( + 300, lambda: self.query_printer_object.emit({"manual_probe": None}) ) - _timer.start(int(300)) - # self.query_printer_object.emit({"manual_probe": None}) - _cmd = self._build_calibration_command(sender.name) # type:ignore - if not _cmd: - return - self.disable_popups.emit(True) - self.run_gcode_signal.emit("G28\nM400") - if "eddy" in sender.name: # type:ignore - self.call_load_panel.emit(True, "Preparing Eddy Current Calibration...") + self.lock_ui.emit(True) + _clean_nozzle = self._verify_gcode("CLEAN_NOZZLE") + if "eddy" in _name: + _name_parts = _name.split(" ", 1) + if len(_name_parts) < 2: + return + if _clean_nozzle: + self.call_load_panel.emit(True, "Cleaning nozzle...\nPlease wait") + self.run_gcode_signal.emit("CLEAN_NOZZLE") + else: + self.call_load_panel.emit( + True, "Calibrating drive current\nHoming axes..." + ) self.toggle_conn_page.emit(False) - self._move_to_pos(self.z_offset_safe_xy[0], self.z_offset_safe_xy[1], 100) - self.run_gcode_signal.emit( - f"LDC_CALIBRATE_DRIVE_CURRENT CHIP={sender.name.split(' ')[1]}" # type:ignore - ) - self.run_gcode_signal.emit("M400\nSAVE_CONFIG") - - self._eddy_command = _cmd - self._eddy_calibration_state = True + self.run_gcode_signal.emit(_cmd) + self._eddy_command = f"PROBE_EDDY_CURRENT_CALIBRATE CHIP={_name_parts[1]}" + self._calib_phase = _CalibPhase.EDDY_PHASE1 return + if _clean_nozzle and _cmd != "Z_ENDSTOP_CALIBRATE": + self.call_load_panel.emit(True, "Cleaning nozzle...\nPlease wait") + self.run_gcode_signal.emit("CLEAN_NOZZLE") else: - if self.z_offset_safe_xy: - self.call_load_panel.emit(True, "Homing Axes...") - self._move_to_pos( - self.z_offset_safe_xy[0], self.z_offset_safe_xy[1], 100 - ) + self.call_load_panel.emit(True, "Starting calibration\nHoming axes...") + self._calib_phase = _CalibPhase.PROBE_ACTIVE self.run_gcode_signal.emit(_cmd) @QtCore.pyqtSlot(str, str, float, name="on_extruder_update") def on_extruder_update( - self, extruder_name: str, field: str, new_value: float + self, _extruder_name: str, field: str, new_value: float ) -> None: """Handle extruder update""" - if not self.helper_initialize: + if self._calib_phase == _CalibPhase.IDLE: return - if self._eddy_calibration_state: + if field == "target": + prev_temp = self.target_temp + self.target_temp = round(new_value, 0) + if self.isVisible(): + if self.target_temp > 0: + self.call_load_panel.emit( + True, "Heating nozzle\nCleaning before calibration..." + ) + elif prev_temp > 0: + # Heater turned off — brushing is starting + self.call_load_panel.emit(True, "Cleaning nozzle...\nPlease wait") return if self.target_temp != 0: if self.current_temp == self.target_temp: - if self.isVisible: - self.call_load_panel.emit(True, "Extruder heated up \n Please wait") + if self.isVisible(): + self.call_load_panel.emit( + True, "Nozzle at temperature\nCleaning nozzle..." + ) return if field == "temperature": self.current_temp = round(new_value, 0) - if self.isVisible: + if self.isVisible(): self.call_load_panel.emit( True, - f"Heating up ({new_value}/{self.target_temp}) \n Please wait", + f"Heating nozzle ({new_value}/{self.target_temp}°C)\nPlease wait...", ) - if field == "target": - self.target_temp = round(new_value, 0) - if self.isVisible: - self.call_load_panel.emit(True, "Cleaning the nozzle \n Please wait") @QtCore.pyqtSlot(name="handle_accept") def handle_accept(self) -> None: @@ -489,37 +488,51 @@ def handle_accept(self) -> None: return self.helper_start = False self._toggle_tool_buttons(False) - self._show_option_cards() - self.run_gcode_signal.emit(self.z_offset_config_method[2]) - self.run_gcode_signal.emit("M400") - self.run_gcode_signal.emit( - "SAVE_CONFIG" - ) # Immediately save the new value and restart the host + if "eddy" in self.z_offset_method_type.lower(): + self._calib_phase = _CalibPhase.EDDY_PHASE2 + self.call_load_panel.emit( + True, + "Finalising Eddy calibration...\nThis may take a few minutes", + ) + else: + self._show_option_cards() + self._calib_phase = _CalibPhase.SAVE_RESTART + self.call_load_panel.emit( + True, "Saving configuration...\nMachine will restart" + ) + self.toggle_conn_page.emit(False) + self.run_gcode_signal.emit("ACCEPT") + self.run_gcode_signal.emit("SAVE_CONFIG") @QtCore.pyqtSlot(name="handle_abort") def handle_abort(self) -> None: """Aborts the calibration procedure""" if not self.helper_start: return - self.helper_start = False - self._toggle_tool_buttons(False) - self._show_option_cards() + self._cancel_calibration() self.run_gcode_signal.emit("ABORT") @QtCore.pyqtSlot(str, list, name="on_gcode_move_update") - def on_gcode_move_update(self, name: str, value: list) -> None: - """Handle gcode move update""" - if not value: + def on_gcode_move_update(self, _name: str, _value: list) -> None: + """Update loading message once homing completes after nozzle cleaning.""" + if ( + self._calib_phase + not in ( + _CalibPhase.EDDY_PHASE1, + _CalibPhase.PROBE_ACTIVE, + ) + or self.target_temp != 0 + ): return - - _fields = [ - "absolute_coordinates", - "absolute_extrude", - "homing_origin", - "position", - "gcode_position", - ] - ... + if _name == "homing_origin" and self.isVisible(): + if self._calib_phase == _CalibPhase.EDDY_PHASE1: + self.call_load_panel.emit( + True, "Calibrating drive current\nPlease wait..." + ) + else: + self.call_load_panel.emit( + True, "Moving to calibration position\nPlease wait..." + ) @QtCore.pyqtSlot(dict, name="on_manual_probe_update") def on_manual_probe_update(self, update: dict) -> None: @@ -527,27 +540,31 @@ def on_manual_probe_update(self, update: dict) -> None: if not update: return - # if update.get("z_position_lower"): - # f"{update.get('z_position_lower'):.4f} mm" - is_active = update.get("is_active", None) - if update.get("z_position_upper"): - self.old_offset_info.setText(f"{update.get('z_position_upper'):.4f} mm") - if update.get("z_position"): - self.current_offset_info.setText(f"{update.get('z_position'):.4f} mm") + if (z_upper := update.get("z_position_upper")) is not None: + self.old_offset_info.setText(f"{round(z_upper, 3) or 0.0:.3f} mm") + if (z_pos := update.get("z_position")) is not None: + self.current_offset_info.setText(f"{round(z_pos, 3) or 0.0:.3f} mm") - if not is_active: + if is_active is None: return if not self.isVisible(): self.request_page_view.emit() # Shared state updates self.helper_initialize = False + _was_active = self.helper_start self.helper_start = is_active + if is_active and self._calib_phase == _CalibPhase.PROBE_ACTIVE: + # Probe session started — CLEAN_NOZZLE/homing phase is over. + self._calib_phase = _CalibPhase.IDLE + elif not is_active and _was_active: + # A manual probe session ended (external abort or normal completion). + self._calib_phase = _CalibPhase.IDLE # UI updates self._toggle_tool_buttons(is_active) if is_active: self._hide_option_cards() - else: + elif self._calib_phase == _CalibPhase.IDLE: self._show_option_cards() @QtCore.pyqtSlot(list, name="handle_gcode_response") @@ -558,6 +575,8 @@ def handle_gcode_response(self, data: list) -> None: data (list): A list containing the gcode that originated the response and the response """ + if not data: + return if self.isVisible(): if data[0].startswith("!!"): # An error occurred if "already in a manual z probe" in data[0].strip("!! ").lower(): @@ -568,35 +587,60 @@ def handle_gcode_response(self, data: list) -> None: self._show_option_cards() self.helper_start = False self._toggle_tool_buttons(False) - - # elif data[0].startswith("// "): ... + error_msg = data[0].removeprefix("!! ") + self.show_notifications.emit("probe_helper", error_msg, 3, True) @QtCore.pyqtSlot(list, name="handle_error_response") def handle_error_response(self, data: list) -> None: """Handle received error response""" - ... - # _data, _metadata, *extra = data + [None] * max(0, 2 - len(data)) - - def _move_to_pos(self, x, y, speed) -> None: - self.run_gcode_signal.emit(f"G91\nG1 Z5 F{10 * 60}\nM400") - self.run_gcode_signal.emit(f"G90\nG1 X{x} Y{y} F{speed * 60}\nM400") - return + if not data: + return + raw = data[0] + if isinstance(raw, dict): + error_msg = raw.get("message", "Unknown error") + else: + error_msg = str(raw) + # Eddy phase 1 and phase 2 both end with SAVE_CONFIG which restarts Klipper. + if ( + not self.helper_start + and self._calib_phase != _CalibPhase.IDLE + and (self._calib_phase != _CalibPhase.EDDY_PHASE1 or self._eddy_command) + ): + logger.debug( + "Suppressing error during eddy phase-1 SAVE_CONFIG restart: %s", + error_msg, + ) + return + logger.error("Error Response: %s", error_msg) + self._cancel_calibration() + self.show_notifications.emit("probe_helper", error_msg, 3, True) - def _setup_move_option_buttons(self) -> None: - """Change move_option_x buttons text for configured - zhop values in stored in the class variable `distances` + def _reset_calibration_state(self) -> None: + """Reset all calibration state and temp tracking to idle.""" + self.helper_start = False + self.helper_initialize = False + self._calib_phase = _CalibPhase.IDLE + self._eddy_command = "" + self._active_calibration_tool = "" + self.target_temp = 0 + self.current_temp = 0 - `distances` Has the values from lowest to maximum zhop - """ - if self.distances: - return - self.move_option_1.setText(str(self.distances[0])) - self.move_option_2.setText(str(self.distances[1])) - self.move_option_3.setText(str(self.distances[2])) - self.move_option_4.setText(str(self.distances[3])) - self.move_option_5.setText(str(self.distances[4])) + def _restore_ui(self) -> None: + """Dismiss loading overlay and re-enable navigation.""" + self._show_option_cards() + self.call_load_panel.emit(False, "") + self.disable_popups.emit(False) + self.lock_ui.emit(False) + self.toggle_conn_page.emit(True) + + def _cancel_calibration(self) -> None: + """Full reset: clear state, hide tool buttons, restore UI.""" + self._reset_calibration_state() + self._toggle_tool_buttons(False) + self._restore_ui() def _toggle_tool_buttons(self, state: bool) -> None: + """Show/hide and enable/disable calibration tool buttons based on active state.""" self.mb_lower_nozzle.setEnabled(state) self.mb_raise_nozzle.setEnabled(state) self.accept_button.setEnabled(state) @@ -606,6 +650,7 @@ def _toggle_tool_buttons(self, state: bool) -> None: if state: for i in self.card_options.values(): i.setDisabled(False) + self.lock_ui.emit(True) self.call_load_panel.emit(False, "") self.po_back_button.setEnabled(False) self.po_back_button.hide() @@ -615,6 +660,7 @@ def _toggle_tool_buttons(self, state: bool) -> None: self.old_offset_info.show() self.bbp_offset_steps_buttons_group_box.show() self.current_offset_info.show() + self.abort_button.show() self.tool_image.show() self.mb_raise_nozzle.show() self.mb_lower_nozzle.show() @@ -633,7 +679,9 @@ def _toggle_tool_buttons(self, state: bool) -> None: self.po_header_title.show() self.separator_line.show() self.bbp_offset_steps_buttons_group_box.hide() + self.old_offset_info.setText("0.000 mm") self.old_offset_info.hide() + self.current_offset_info.setText("0.000 mm") self.current_offset_info.hide() self.tool_image.hide() self.mb_raise_nozzle.hide() @@ -647,9 +695,30 @@ def _toggle_tool_buttons(self, state: bool) -> None: ) self.update() - return + + def _create_move_button( + self, + parent: QtWidgets.QWidget, + label: str, + obj_name: str, + checked: bool, + font: QtGui.QFont, + ) -> BlocksCustomCheckButton: + """Create a single move-step check button.""" + btn = BlocksCustomCheckButton(parent=parent) + btn.setMinimumSize(QtCore.QSize(100, 60)) + btn.setMaximumSize(QtCore.QSize(100, 60)) + btn.setText(label) + btn.setFont(font) + btn.setCheckable(True) + btn.setChecked(checked) + btn.setFlat(True) + btn.setProperty("button_type", "") + btn.setObjectName(obj_name) + return btn def _setupUi(self) -> None: + """Build and lay out all UI elements for the probe helper page.""" self.bbp_offset_value_selector_group = QtWidgets.QButtonGroup(self) self.bbp_offset_value_selector_group.setExclusive(True) sizePolicy = QtWidgets.QSizePolicy( @@ -714,7 +783,7 @@ def _setupUi(self) -> None: self.abort_button = BlocksCustomButton(self) self.abort_button.setGeometry(QtCore.QRect(300, 340, 170, 60)) self.abort_button.setText("Abort") - self.abort_button.setObjectName("accept_button") + self.abort_button.setObjectName("abort_button") self.abort_button.setPixmap(QtGui.QPixmap(":/dialog/media/btn_icons/no.svg")) self.abort_button.setVisible(False) font = QtGui.QFont() @@ -786,126 +855,27 @@ def _setupUi(self) -> None: self.bbp_offset_steps_buttons.setContentsMargins(9, 9, 9, 9) self.bbp_offset_steps_buttons.setObjectName("bbp_offset_steps_buttons") - # 0.1mm button - self.move_option_1 = BlocksCustomCheckButton( - parent=self.bbp_offset_steps_buttons_group_box - ) - self.move_option_1.setMinimumSize(QtCore.QSize(100, 60)) - self.move_option_1.setMaximumSize(QtCore.QSize(100, 60)) - self.move_option_1.setText("0.01 mm") - - font = QtGui.QFont() - font.setPointSize(14) - self.move_option_1.setFont(font) - self.move_option_1.setCheckable(True) - self.move_option_1.setChecked(True) # Set as initially checked - self.move_option_1.setFlat(True) - self.move_option_1.setProperty("button_type", "") - self.move_option_1.setObjectName("move_option_1") - self.bbp_offset_value_selector_group.addButton(self.move_option_1) - self.bbp_offset_steps_buttons.addWidget( - self.move_option_1, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - - # 0.01mm button - self.move_option_2 = BlocksCustomCheckButton( - parent=self.bbp_offset_steps_buttons_group_box - ) - self.move_option_2.setMinimumSize(QtCore.QSize(100, 60)) - self.move_option_2.setMaximumSize( - QtCore.QSize(100, 60) - ) # Increased max width by 5 pixels - self.move_option_2.setText("0.25 mm") - - font = QtGui.QFont() - font.setPointSize(14) - self.move_option_2.setFont(font) - self.move_option_2.setCheckable(True) - self.move_option_2.setFlat(True) - self.move_option_2.setProperty("button_type", "") - self.move_option_2.setObjectName("move_option_2") - self.bbp_offset_value_selector_group.addButton(self.move_option_2) - self.bbp_offset_steps_buttons.addWidget( - self.move_option_2, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - - # 0.05mm button - self.move_option_3 = BlocksCustomCheckButton( - parent=self.bbp_offset_steps_buttons_group_box - ) - self.move_option_3.setMinimumSize(QtCore.QSize(100, 60)) - self.move_option_3.setMaximumSize( - QtCore.QSize(100, 60) - ) # Increased max width by 5 pixels - self.move_option_3.setText("0.1 mm") - - font = QtGui.QFont() - font.setPointSize(14) - self.move_option_3.setFont(font) - self.move_option_3.setCheckable(True) - self.move_option_3.setFlat(True) - self.move_option_3.setProperty("button_type", "") - self.move_option_3.setObjectName("move_option_3") - self.bbp_offset_value_selector_group.addButton(self.move_option_3) - self.bbp_offset_steps_buttons.addWidget( - self.move_option_3, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - - # 0.025mm button - self.move_option_4 = BlocksCustomCheckButton( - parent=self.bbp_offset_steps_buttons_group_box - ) - self.move_option_4.setMinimumSize(QtCore.QSize(100, 60)) - self.move_option_4.setMaximumSize( - QtCore.QSize(100, 60) - ) # Increased max width by 5 pixels - self.move_option_4.setText("0.5 mm") - - font = QtGui.QFont() - font.setPointSize(14) - self.move_option_4.setFont(font) - self.move_option_4.setCheckable(True) - self.move_option_4.setFlat(True) - self.move_option_4.setProperty("button_type", "") - self.move_option_4.setObjectName("move_option_4") - self.bbp_offset_value_selector_group.addButton(self.move_option_4) - self.bbp_offset_steps_buttons.addWidget( - self.move_option_4, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - - # 0.01mm button - self.move_option_5 = BlocksCustomCheckButton( - parent=self.bbp_offset_steps_buttons_group_box - ) - self.move_option_5.setMinimumSize(QtCore.QSize(100, 60)) - self.move_option_5.setMaximumSize( - QtCore.QSize(100, 60) - ) # Increased max width by 5 pixels - self.move_option_5.setText("1 mm") - - font = QtGui.QFont() - font.setPointSize(14) - self.move_option_5.setFont(font) - self.move_option_5.setCheckable(True) - self.move_option_5.setFlat(True) - self.move_option_5.setProperty("button_type", "") - self.move_option_5.setObjectName("move_option_4") - self.bbp_offset_value_selector_group.addButton(self.move_option_5) - self.bbp_offset_steps_buttons.addWidget( - self.move_option_5, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - - # Line separator for 0.025mm - set size policy to expanding horizontally + move_font = QtGui.QFont() + move_font.setPointSize(14) + center = ( + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter + ) + for label, value, obj_name, checked in _PROBE_MOVE_STEPS: + btn = self._create_move_button( + self.bbp_offset_steps_buttons_group_box, + label, + obj_name, + checked, + move_font, + ) + btn.toggled.connect( + lambda checked_state, v=value: ( + checked_state and self.handle_zhopHeight_change(new_value=v) + ) + ) + setattr(self, obj_name, btn) + self.bbp_offset_value_selector_group.addButton(btn) + self.bbp_offset_steps_buttons.addWidget(btn, 0, center) # Set the layout for the group box self.bbp_offset_steps_buttons_group_box.setLayout(self.bbp_offset_steps_buttons) @@ -944,7 +914,6 @@ def _setupUi(self) -> None: self.old_offset_info.setFont(font) # Set color to white to be visible on the dark background self.old_offset_info.setStyleSheet("color: gray; background: transparent;") - self.old_offset_info.setText("Z-Offset") self.old_offset_info.setObjectName("old_offset_info") self.old_offset_info.setText("0 mm") @@ -1021,8 +990,6 @@ def _setupUi(self) -> None: QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum, ) - self.main_content_horizontal_layout.addItem(self.spacerItem) - # Add move buttons layout LAST for right placement self.main_content_horizontal_layout.addLayout(self.bbp_buttons_layout) diff --git a/BlocksScreen/lib/panels/widgets/tunePage.py b/BlocksScreen/lib/panels/widgets/tunePage.py index 301c5cd1..1b68c2f7 100644 --- a/BlocksScreen/lib/panels/widgets/tunePage.py +++ b/BlocksScreen/lib/panels/widgets/tunePage.py @@ -1,5 +1,6 @@ import re import typing +import logging from helper_methods import normalize from lib.utils.blocks_button import BlocksCustomButton @@ -8,6 +9,9 @@ from PyQt6 import QtCore, QtGui, QtWidgets +_logger = logging.getLogger(__name__) + + class TuneWidget(QtWidgets.QWidget): request_back: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( name="request_back_page" @@ -45,31 +49,51 @@ def __init__(self, parent) -> None: self.sensors_menu_btn.clicked.connect(self.request_sensorsPage.emit) self.tune_babystep_menu_btn.clicked.connect(self.request_bbpPage.emit) self.tune_back_btn.clicked.connect(self.request_back) - self.bed_display.clicked.connect( - lambda: self.request_numpad[str, int, "PyQt_PyObject", int, int].emit( - "Bed", - int(round(self.bed_target)), - self.on_numpad_change, - 0, - 120, # TODO: Get this value from printer objects + self.speed_display.clicked.connect( + lambda: self.request_sliderPage[str, int, "PyQt_PyObject", int, int].emit( + "Speed", + int(self.speed_factor_override * 100), + self.on_slider_change, + 10, + 300, ) ) + + @QtCore.pyqtSlot(dict, name="printer_config") + def on_printer_config(self, config: dict) -> None: + """Slot that receives the full printer configuration, + + Additionally, this method configures the signal connections + between controllable heaters and numpad calls + """ + try: + self.extruder_display.clicked.disconnect() + self.bed_display.clicked.disconnect() + except Exception: + _logger.debug("Signals were not connected") + extruder = config.get("extruder", None) or {} + bed = config.get("heater_bed", None) or {} + e_min_temp = extruder.get("min_temp", 0) + e_max_temp = extruder.get("max_temp", 300) + b_max_temp = bed.get("max_temp", 100) + b_min_temp = bed.get("min_temp", 0) + # Configure numpads self.extruder_display.clicked.connect( lambda: self.request_numpad[str, int, "PyQt_PyObject", int, int].emit( "Extruder", int(round(self.extruder_target)), self.on_numpad_change, - 0, - 300, # TODO: Get this value from printer objects + int(e_min_temp), + int(e_max_temp), ) ) - self.speed_display.clicked.connect( - lambda: self.request_sliderPage[str, int, "PyQt_PyObject", int, int].emit( - "Speed", - int(self.speed_factor_override * 100), - self.on_slider_change, - 10, - 300, + self.bed_display.clicked.connect( + lambda: self.request_numpad[str, int, "PyQt_PyObject", int, int].emit( + "Bed", + int(round(self.bed_target)), + self.on_numpad_change, + int(b_min_temp), + int(b_max_temp), ) ) diff --git a/BlocksScreen/lib/printer.py b/BlocksScreen/lib/printer.py index c6c76fbc..67aecb55 100644 --- a/BlocksScreen/lib/printer.py +++ b/BlocksScreen/lib/printer.py @@ -30,6 +30,8 @@ class Printer(QtCore.QObject): [str, float], [str, str], name="idle_timeout_update" ) + save_variables_update = QtCore.pyqtSignal(dict, name="save_variables_update") + gcode_move_update = QtCore.pyqtSignal( [str, list], [str, float], [str, bool], name="gcode_move_update" ) @@ -62,8 +64,8 @@ class Printer(QtCore.QObject): gcode_macro_update = QtCore.pyqtSignal(str, dict, name="gcode_macro_update") webhooks_update = QtCore.pyqtSignal(str, str, name="webhooks_update") - load_filament_update = QtCore.pyqtSignal(bool, name="load_filament_update") - unload_filament_update = QtCore.pyqtSignal(bool, name="unload_filament_update") + load_filament_update = QtCore.pyqtSignal(dict, name="load_filament_update") + unload_filament_update = QtCore.pyqtSignal(dict, name="unload_filament_update") query_printer_object = QtCore.pyqtSignal(dict, name="query_printer_object") save_config_pending: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( @@ -328,6 +330,11 @@ def _webhooks_object_updated(self, value: dict, name: str = "webhooks") -> None: e, ) + def _save_variables_object_updated( + self, value: dict, name: str = "saved_variables" + ) -> None: + self.save_variables_update.emit(value) + def _gcode_move_object_updated(self, value: dict, name: str = "gcode_move") -> None: if "speed_factor" in value.keys(): self.gcode_move_update[str, float].emit( @@ -734,9 +741,7 @@ def _temperature_probe_object_updated(self, values: dict, name: str) -> None: # TODO: testing needed here idk if does work def _unload_filament_object_updated(self, values: dict, name: str) -> None: - if "state" in values.keys(): - self.unload_filament_update[bool].emit(values["state"]) + self.unload_filament_update[dict].emit(values) def _load_filament_object_updated(self, values: dict, name: str) -> None: - if "state" in values.keys(): - self.load_filament_update[bool].emit(values["state"]) + self.load_filament_update[dict].emit(values) diff --git a/BlocksScreen/lib/ui/filamentStackedWidget.ui b/BlocksScreen/lib/ui/filamentStackedWidget.ui index e9d359a6..c418ca80 100644 --- a/BlocksScreen/lib/ui/filamentStackedWidget.ui +++ b/BlocksScreen/lib/ui/filamentStackedWidget.ui @@ -75,10 +75,26 @@ + + + + Qt::Horizontal + + + QSizePolicy::Minimum + + + + 60 + 0 + + + + - + 0 0 @@ -118,6 +134,69 @@ + + + + + 0 + 0 + + + + + 60 + 60 + + + + + 60 + 60 + + + + + Momcake + 20 + false + PreferAntialias + + + + false + + + true + + + Qt::NoContextMenu + + + Qt::LeftToRight + + + + + + Back + + + false + + + true + + + menu_btn + + + icon + + + :/ui/media/btn_icons/back.svg + + + @@ -137,125 +216,6 @@ - - - - - 0 - 0 - - - - - 600 - 80 - - - - - 600 - 80 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 20 - 20 - 201 - 41 - - - - - 0 - 0 - - - - - 280 - 60 - - - - - 15 - - - - background: transparent; color:white ; - - - Filament Name - - - Qt::AlignCenter - - - Qt::NoTextInteraction - - - - - - 260 - 10 - 321 - 60 - - - - - 0 - 60 - - - - - 16777215 - 60 - - - - - 13 - - - - color:white - - - ... - - - Qt::AlignCenter - - - - - - 240 - 10 - 3 - 61 - - - - color:white - - - Qt::Vertical - - - - @@ -1624,6 +1584,8 @@ Filament + + diff --git a/BlocksScreen/lib/ui/filamentStackedWidget_ui.py b/BlocksScreen/lib/ui/filamentStackedWidget_ui.py index bd545139..8ec597b2 100644 --- a/BlocksScreen/lib/ui/filamentStackedWidget_ui.py +++ b/BlocksScreen/lib/ui/filamentStackedWidget_ui.py @@ -1,6 +1,6 @@ -# Form implementation generated from reading ui file '/home/levi/main/Blocks_Screen/BlocksScreen/lib/ui/filamentStackedWidget.ui' +# Form implementation generated from reading ui file 'filamentStackedWidget.ui' # -# Created by: PyQt6 UI code generator 6.7.1 +# Created by: PyQt6 UI code generator 6.10.0 # # WARNING: Any manual changes made to this file will be lost when pyuic6 is # run again. Do not edit this file unless you know what you are doing. @@ -36,8 +36,10 @@ def setupUi(self, filamentStackedWidget): self.verticalLayout.addItem(spacerItem) self.filament_page_header_layout = QtWidgets.QHBoxLayout() self.filament_page_header_layout.setObjectName("filament_page_header_layout") + self.spacerItem1 = QtWidgets.QSpacerItem(60, 0, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + self.filament_page_header_layout.addItem(self.spacerItem1) self.filament_page_header_title = QtWidgets.QLabel(parent=self.filament_control_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Maximum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.filament_page_header_title.sizePolicy().hasHeightForWidth()) @@ -53,54 +55,39 @@ def setupUi(self, filamentStackedWidget): self.filament_page_header_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.filament_page_header_title.setObjectName("filament_page_header_title") self.filament_page_header_layout.addWidget(self.filament_page_header_title, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignVCenter) + self.main_back_button = IconButton(parent=self.filament_control_page) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.main_back_button.sizePolicy().hasHeightForWidth()) + self.main_back_button.setSizePolicy(sizePolicy) + self.main_back_button.setMinimumSize(QtCore.QSize(60, 60)) + self.main_back_button.setMaximumSize(QtCore.QSize(60, 60)) + font = QtGui.QFont() + font.setFamily("Momcake") + font.setPointSize(20) + font.setItalic(False) + font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) + self.main_back_button.setFont(font) + self.main_back_button.setMouseTracking(False) + self.main_back_button.setTabletTracking(True) + self.main_back_button.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.main_back_button.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + self.main_back_button.setStyleSheet("") + self.main_back_button.setAutoDefault(False) + self.main_back_button.setFlat(True) + self.main_back_button.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) + self.main_back_button.setObjectName("main_back_button") + self.filament_page_header_layout.addWidget(self.main_back_button) self.verticalLayout.addLayout(self.filament_page_header_layout) - spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - self.verticalLayout.addItem(spacerItem1) + spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) + self.verticalLayout.addItem(spacerItem2) self.verticalLayout_4 = QtWidgets.QVBoxLayout() self.verticalLayout_4.setObjectName("verticalLayout_4") self.verticalLayout_3 = QtWidgets.QVBoxLayout() self.verticalLayout_3.setObjectName("verticalLayout_3") - self.frame_8 = BlocksCustomFrame(parent=self.filament_control_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.frame_8.sizePolicy().hasHeightForWidth()) - self.frame_8.setSizePolicy(sizePolicy) - self.frame_8.setMinimumSize(QtCore.QSize(600, 80)) - self.frame_8.setMaximumSize(QtCore.QSize(600, 80)) - self.frame_8.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_8.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_8.setObjectName("frame_8") - self.filament_page_info_content_6 = QtWidgets.QLabel(parent=self.frame_8) - self.filament_page_info_content_6.setGeometry(QtCore.QRect(20, 20, 201, 41)) - self.filament_page_info_content_6.setMinimumSize(QtCore.QSize(0, 0)) - self.filament_page_info_content_6.setMaximumSize(QtCore.QSize(280, 60)) - font = QtGui.QFont() - font.setPointSize(15) - self.filament_page_info_content_6.setFont(font) - self.filament_page_info_content_6.setStyleSheet("background: transparent; color:white ;") - self.filament_page_info_content_6.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.filament_page_info_content_6.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.NoTextInteraction) - self.filament_page_info_content_6.setObjectName("filament_page_info_content_6") - self.label = QtWidgets.QLabel(parent=self.frame_8) - self.label.setGeometry(QtCore.QRect(260, 10, 321, 60)) - self.label.setMinimumSize(QtCore.QSize(0, 60)) - self.label.setMaximumSize(QtCore.QSize(16777215, 60)) - font = QtGui.QFont() - font.setPointSize(13) - self.label.setFont(font) - self.label.setStyleSheet("color:white") - self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.label.setObjectName("label") - self.line = QtWidgets.QFrame(parent=self.frame_8) - self.line.setGeometry(QtCore.QRect(240, 10, 3, 61)) - self.line.setStyleSheet("color:white") - self.line.setFrameShape(QtWidgets.QFrame.Shape.VLine) - self.line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line.setObjectName("line") - self.verticalLayout_3.addWidget(self.frame_8, 0, QtCore.Qt.AlignmentFlag.AlignHCenter) - spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - self.verticalLayout_3.addItem(spacerItem2) + spacerItem3 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) + self.verticalLayout_3.addItem(spacerItem3) self.frame_7 = BlocksCustomFrame(parent=self.filament_control_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) @@ -138,8 +125,8 @@ def setupUi(self, filamentStackedWidget): self.line_2.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) self.line_2.setObjectName("line_2") self.verticalLayout_3.addWidget(self.frame_7, 0, QtCore.Qt.AlignmentFlag.AlignHCenter) - spacerItem3 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - self.verticalLayout_3.addItem(spacerItem3) + spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) + self.verticalLayout_3.addItem(spacerItem4) self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout.setObjectName("horizontalLayout") self.filament_page_load_btn = BlocksCustomButton(parent=self.filament_control_page) @@ -193,10 +180,10 @@ def setupUi(self, filamentStackedWidget): self.verticalLayout_3.addLayout(self.horizontalLayout) self.verticalLayout_4.addLayout(self.verticalLayout_3) self.verticalLayout.addLayout(self.verticalLayout_4) - spacerItem4 = QtWidgets.QSpacerItem(20, 26, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.verticalLayout.addItem(spacerItem4) - spacerItem5 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) + spacerItem5 = QtWidgets.QSpacerItem(20, 26, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) self.verticalLayout.addItem(spacerItem5) + spacerItem6 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) + self.verticalLayout.addItem(spacerItem6) filamentStackedWidget.addWidget(self.filament_control_page) self.load_page = QtWidgets.QWidget() sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding) @@ -210,12 +197,12 @@ def setupUi(self, filamentStackedWidget): self.load_page.setObjectName("load_page") self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.load_page) self.verticalLayout_2.setObjectName("verticalLayout_2") - spacerItem6 = QtWidgets.QSpacerItem(20, 24, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.verticalLayout_2.addItem(spacerItem6) + spacerItem7 = QtWidgets.QSpacerItem(20, 24, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + self.verticalLayout_2.addItem(spacerItem7) self.load_page_header_layout = QtWidgets.QHBoxLayout() self.load_page_header_layout.setObjectName("load_page_header_layout") - spacerItem7 = QtWidgets.QSpacerItem(60, 20, QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.Minimum) - self.load_page_header_layout.addItem(spacerItem7) + spacerItem8 = QtWidgets.QSpacerItem(60, 20, QtWidgets.QSizePolicy.Policy.Maximum, QtWidgets.QSizePolicy.Policy.Minimum) + self.load_page_header_layout.addItem(spacerItem8) self.load_header_page_title = QtWidgets.QLabel(parent=self.load_page) self.load_header_page_title.setMinimumSize(QtCore.QSize(0, 60)) font = QtGui.QFont() @@ -251,8 +238,8 @@ def setupUi(self, filamentStackedWidget): self.load_header_back_button.setObjectName("load_header_back_button") self.load_page_header_layout.addWidget(self.load_header_back_button) self.verticalLayout_2.addLayout(self.load_page_header_layout) - spacerItem8 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - self.verticalLayout_2.addItem(spacerItem8) + spacerItem9 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) + self.verticalLayout_2.addItem(spacerItem9) self.load_page_content_layout = QtWidgets.QGridLayout() self.load_page_content_layout.setContentsMargins(5, 5, 5, 5) self.load_page_content_layout.setHorizontalSpacing(6) @@ -428,8 +415,8 @@ def setupUi(self, filamentStackedWidget): self.load_custom_btn.setObjectName("load_custom_btn") self.load_page_footer_layout.addWidget(self.load_custom_btn) self.verticalLayout_2.addLayout(self.load_page_footer_layout) - spacerItem9 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - self.verticalLayout_2.addItem(spacerItem9) + spacerItem10 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) + self.verticalLayout_2.addItem(spacerItem10) filamentStackedWidget.addWidget(self.load_page) self.custom_filament_page = QtWidgets.QWidget() self.custom_filament_page.setMinimumSize(QtCore.QSize(710, 400)) @@ -587,8 +574,9 @@ def retranslateUi(self, filamentStackedWidget): filamentStackedWidget.setWindowTitle(_translate("filamentStackedWidget", "StackedWidget")) self.filament_page_header_title.setText(_translate("filamentStackedWidget", "Filament Control")) self.filament_page_header_title.setProperty("class", _translate("filamentStackedWidget", "title_text")) - self.filament_page_info_content_6.setText(_translate("filamentStackedWidget", "Filament Name")) - self.label.setText(_translate("filamentStackedWidget", "...")) + self.main_back_button.setText(_translate("filamentStackedWidget", "Back")) + self.main_back_button.setProperty("class", _translate("filamentStackedWidget", "menu_btn")) + self.main_back_button.setProperty("button_type", _translate("filamentStackedWidget", "icon")) self.filament_page_info_title_6.setText(_translate("filamentStackedWidget", "Loaded Filament Type")) self.label_2.setText(_translate("filamentStackedWidget", "...")) self.filament_page_load_btn.setText(_translate("filamentStackedWidget", "Load")) @@ -638,3 +626,13 @@ def retranslateUi(self, filamentStackedWidget): from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_frame import BlocksCustomFrame from lib.utils.icon_button import IconButton + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + filamentStackedWidget = QtWidgets.QStackedWidget() + ui = Ui_filamentStackedWidget() + ui.setupUi(filamentStackedWidget) + filamentStackedWidget.show() + sys.exit(app.exec())