Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e14dc70
bugfix: fix 30+ Pi runtime crashes across UI panels and widgets
gmmcosta15 Apr 13, 2026
bc9f17c
test: update networkWindow QTimer patch path to QtCore.QTimer
gmmcosta15 Apr 13, 2026
965193b
fix: add set_filament_state to SensorWidget; remove dead change_fil_s…
gmmcosta15 Apr 13, 2026
f1ebc12
Merge branch 'dev' into bugfix/ui-panel-crashes
gmmcosta15 May 25, 2026
4686a2b
Merge remote-tracking branch 'origin/dev' into bugfix/ui-panel-crashes
gmmcosta15 Jun 16, 2026
ae97e93
fix:resolve conflicts in filamentTab/mainWindow/connectionPage/notifi…
gmmcosta15 Jun 16, 2026
61c40c5
Merge branch 'dev' into bugfix/ui-panel-crashes
gmmcosta15 Jun 16, 2026
e26bdae
Merge branch 'dev' into bugfix/ui-panel-crashes
gmmcosta15 Jun 16, 2026
fca4c48
Merge branch 'dev' into bugfix/ui-panel-crashes
gmmcosta15 Jun 17, 2026
ecde9eb
Merge remote-tracking branch 'origin/dev' into bugfix/ui-panel-crashes
gmmcosta15 Jun 19, 2026
23c0ffe
Merge branch 'dev' into bugfix/ui-panel-crashes
gmmcosta15 Jun 19, 2026
49c65e1
Merge remote-tracking branch 'origin/dev' into bugfix/ui-panel-crashes
gmmcosta15 Jun 30, 2026
f11c672
Merge remote-tracking branch 'origin/dev' into bugfix/ui-panel-crashes
gmmcosta15 Aug 20, 2026
6e14cb1
Merge remote-tracking branch 'origin/dev' into bugfix/ui-panel-crashes
gmmcosta15 Sep 23, 2026
02eff12
fix(ui): drop dead confirm/cancel page code and leave sensor state re…
gmmcosta15 Sep 23, 2026
6e2951c
Merge branch 'dev' into bugfix/ui-panel-crashes
gmmcosta15 Sep 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion BlocksScreen/lib/panels/filamentTab.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def handle_moonraker_components(self):
"""Build the pre-gate popup pages once, choosing spoolman vs. manual-entry order."""
if self.moonraker_run:
components = self.ws._moonRest.get_server_info()
if "spoolman" not in components["result"].get("components", []):
if "spoolman" not in components.get("result", {}).get("components", []):
self.fp_button_2.hide()
self._popup_stack.addWidget(self._build_form_page())
self._popup_stack.addWidget(self._build_spool_page())
Expand Down
19 changes: 11 additions & 8 deletions BlocksScreen/lib/panels/mainWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from devices.amu import AMUManager
from devices.storage import USBManager
from lib.files import Files
from lib.klipper_message_filter import ( # noqa: F405
from lib.klipper_message_filter import (
MessageSource,
Severity,
match_message,
Expand Down Expand Up @@ -321,7 +321,7 @@ def __init__(self):
)
self.loadscreen.add_widget(self.loadwidget)
self.controlPanel.toggle_conn_page.connect(self.conn_window.set_toggle)
self.cancelpage = CancelPage(self, ws=self.ws)
self.cancelpage = CancelPage(self)
self.cancelpage.request_file_info.connect(self.file_data.on_request_fileinfo)
self.cancelpage.run_gcode.connect(self.ws.api.run_gcode)
self.printer.print_stats_update[str, str].connect(
Expand Down Expand Up @@ -389,11 +389,14 @@ def show_loadscreen(
if not force:
if _sender is self.update_page:
self._update_in_progress = show
if not show and self._post_update_reconnect:
return
elif not show and self._update_in_progress:
return
elif not show and self._klipper_auto_restart_pending:
if (
not show
and self._post_update_reconnect
or not show
and self._update_in_progress
or not show
and self._klipper_auto_restart_pending
):
return

if _sender == self.filamentPanel:
Expand Down Expand Up @@ -1053,7 +1056,7 @@ def _on_probe_notification(
@api_handler
def _handle_notify_gcode_response_message(self, method, data, metadata) -> None:
"""Handle websocket gcode responses messages"""
_gcode_response = data.get("params")
_gcode_response = data.get("params", [])
self.gcode_response[list].emit(_gcode_response)
if _gcode_response:
if self._popup_toggle:
Expand Down
43 changes: 24 additions & 19 deletions BlocksScreen/lib/panels/networkWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
from lib.utils.icon_button import IconButton
from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import QTimer, pyqtSlot

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -255,15 +254,15 @@ def _prefill_ip_from_os(self) -> None:
except OSError:
continue

@pyqtSlot()
@QtCore.pyqtSlot()
def _on_reconnect_complete(self) -> None:
"""Navigate back to the main panel after a static-IP or DHCP-reset operation."""
logger.debug("reconnect_complete received — navigating to main_network_page")
self.setCurrentIndex(self.indexOf(self.main_network_page))

def _init_timers(self) -> None:
"""Initialize timers."""
self._load_timer = QTimer(self)
self._load_timer = QtCore.QTimer(self)
self._load_timer.setSingleShot(True)
self._load_timer.timeout.connect(self._handle_load_timeout)

Expand All @@ -277,7 +276,7 @@ def _init_model_view(self) -> None:
self._entry_delegate.item_selected.connect(self._on_ssid_item_clicked)
self._configure_list_view_palette()

@pyqtSlot(NetworkState)
@QtCore.pyqtSlot(NetworkState)
def _on_network_state_changed(self, state: NetworkState) -> None:
"""React to a NetworkState update: sync toggles, populate header and connection info."""
logger.debug(
Expand Down Expand Up @@ -438,7 +437,7 @@ def _on_network_state_changed(self, state: NetworkState) -> None:
self._emit_status_icon(state)
self._sync_active_network_list_icon(state)

@pyqtSlot(list)
@QtCore.pyqtSlot(list)
def _on_scan_complete(self, networks: list[NetworkInfo]) -> None:
"""Receive scan results, filter/sort them, and rebuild the SSID list view.

Expand All @@ -457,9 +456,11 @@ def _on_scan_complete(self, networks: list[NetworkInfo]) -> None:
# Stamp the connected AP as ACTIVE so the list is correct on first
# render even when the scan ran before the connection fully settled.
filtered = [
replace(net, network_status=NetworkStatus.ACTIVE)
if net.ssid == current_ssid
else net
(
replace(net, network_status=NetworkStatus.ACTIVE)
if net.ssid == current_ssid
else net
)
for net in filtered
]
active = next((n for n in filtered if n.ssid == current_ssid), None)
Expand All @@ -478,12 +479,12 @@ def _on_scan_complete(self, networks: list[NetworkInfo]) -> None:
state = self._nm.current_state
self._emit_status_icon(state)

@pyqtSlot(list)
@QtCore.pyqtSlot(list)
def _on_saved_networks_loaded(self, networks: list[SavedNetwork]) -> None:
"""Receive saved-network data and update the priority spinbox for the active SSID."""
logger.debug("Loaded %d saved networks", len(networks))

@pyqtSlot(ConnectionResult)
@QtCore.pyqtSlot(ConnectionResult)
def _on_operation_complete(self, result: ConnectionResult) -> None:
"""Handle network operation completion."""
logger.debug("Operation: success=%s, msg=%s", result.success, result.message)
Expand Down Expand Up @@ -570,15 +571,15 @@ def _on_operation_complete(self, result: ConnectionResult) -> None:
result.message,
)
ssid = self._target_ssid
QTimer.singleShot(
QtCore.QTimer.singleShot(
2000, lambda _ssid=ssid: self._nm.connect_network(_ssid)
)
return # Keep loading visible; state machine handles completion

self._clear_loading()
self._show_error_popup(result.message)

@pyqtSlot(str, str)
@QtCore.pyqtSlot(str, str)
def _on_network_error(self, operation: str, message: str) -> None:
"""Log network errors and surface critical failures in the info box."""
logger.error("Network error [%s]: %s", operation, message)
Expand Down Expand Up @@ -658,13 +659,15 @@ def _sync_active_network_list_icon(self, state: NetworkState) -> None:

# Update the cached entry with the authoritative signal and status
updated = [
replace(
net,
signal_strength=self._active_signal,
network_status=NetworkStatus.ACTIVE,
(
replace(
net,
signal_strength=self._active_signal,
network_status=NetworkStatus.ACTIVE,
)
if net.ssid == state.current_ssid
else net
)
if net.ssid == state.current_ssid
else net
for net in self._cached_scan_networks
]

Expand Down Expand Up @@ -1063,7 +1066,9 @@ def _handle_wifi_toggle(self, is_on: bool) -> None:
# Non-blocking: disable hotspot then connect
self._nm.toggle_hotspot(False)
_ssid_to_connect = self._target_ssid
QTimer.singleShot(500, lambda: self._nm.connect_network(_ssid_to_connect))
QtCore.QTimer.singleShot(
500, lambda: self._nm.connect_network(_ssid_to_connect)
)

def _handle_hotspot_toggle(self, is_on: bool) -> None:
"""Enable or disable the hotspot, enforcing the ethernet/Wi-Fi mutual-exclusion rule."""
Expand Down
10 changes: 7 additions & 3 deletions BlocksScreen/lib/panels/utilitiesTab.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import re
import typing
from dataclasses import dataclass
Expand All @@ -15,6 +16,8 @@
from lib.utils.toggleAnimatedButton import ToggleAnimatedButton
from PyQt6 import QtCore, QtGui, QtWidgets

logger = logging.getLogger(__name__)


@dataclass
class LedState:
Expand Down Expand Up @@ -254,8 +257,9 @@ def handle_gcode_response(self, data: list[str]) -> None:
"""

if not isinstance(data, list) or len(data) != 1 or not isinstance(data[0], str):
print(
f"WARNING: Invalid input format. Expected a list with one string. Received: {data}"
logger.warning(
"handle_gcode_response: invalid input format. Expected list[str], received: %r",
data,
)
return

Expand Down Expand Up @@ -322,7 +326,7 @@ def handle_gcode_response(self, data: list[str]) -> None:

self.is_page.set_type_dictionary(self.is_types)
first_key = next(iter(reordered.keys()), None)
for key in reordered.keys():
for key in reordered:
if key == first_key:
self.is_page.add_type_entry(key, "Recommended type")
else:
Expand Down
30 changes: 10 additions & 20 deletions BlocksScreen/lib/panels/widgets/basePopup.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,40 +45,33 @@ def __init__(
self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True)
self.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
else:
self.setStyleSheet(
"""
self.setStyleSheet("""
#MyParent {
background-image: url(:/background/media/1st_background.png);
}
"""
)
""")

def _update_button_style(self) -> None:
"""Applies the current color variables and adds the central border to the stylesheets."""
if not self.dialog:
return

if not self.floating:
self.confirm_button.setStyleSheet(
f"""
self.confirm_button.setStyleSheet(f"""
background-color: {self.confirm_bk_color};
color: {self.confirm_ft_color};
border: none;
padding: 10px;
"""
)
""")

self.cancel_button.setStyleSheet(
f"""
self.cancel_button.setStyleSheet(f"""
background-color: {self.cancel_bk_color};
color: {self.cancel_ft_color};
border: none;
padding: 10px;
"""
)
""")
else:
self.confirm_button.setStyleSheet(
f"""
self.confirm_button.setStyleSheet(f"""
background-color: {self.confirm_bk_color};
color: {self.confirm_ft_color};
border-top: none;
Expand All @@ -87,20 +80,17 @@ def _update_button_style(self) -> None:
border-right: 1px solid #80807e;
border-bottom-left-radius: 16px;
padding: 10px;
"""
)
""")

self.cancel_button.setStyleSheet(
f"""
self.cancel_button.setStyleSheet(f"""
background-color: {self.cancel_bk_color};
color: {self.cancel_ft_color};
border-left: 1px solid #80807e;;
border-bottom: 2px solid #80807e;
border-right: 2px solid #80807e;
border-bottom-right-radius: 16px;
padding: 10px;
"""
)
""")

def set_message(self, message: str) -> None:
self.label.setText(message)
Expand Down
56 changes: 27 additions & 29 deletions BlocksScreen/lib/panels/widgets/cancelPage.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import logging
import typing

from lib.utils.blocks_button import BlocksCustomButton
from lib.utils.blocks_frame import BlocksCustomFrame
from lib.utils.blocks_label import BlocksLabel
from PyQt6 import QtCore, QtGui, QtWidgets
import typing

from lib.moonrakerComm import MoonWebSocket
logger = logging.getLogger(__name__)


class CancelPage(QtWidgets.QWidget):
"""Update GUI Page,
retrieves from moonraker available clients and adds functionality
for updating or recovering them
"""
"""Displayed when a print is cancelled; offers reprint or ignore."""

request_file_info: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal(
str, name="request_file_info"
Expand All @@ -23,14 +22,12 @@ class CancelPage(QtWidgets.QWidget):
str, name="run_gcode"
)

def __init__(self, parent: QtWidgets.QWidget, ws: MoonWebSocket) -> None:
def __init__(self, parent: QtWidgets.QWidget) -> None:
super().__init__(parent)
self.ws: MoonWebSocket = ws
self._setupUI()
self.filename = ""

self.confirm_button.clicked.connect(lambda: self._handle_accept())

self.refuse_button.clicked.connect(lambda: self._handle_refuse())

self.setAttribute(QtCore.Qt.WidgetAttribute.WA_StyledBackground, True)
Expand All @@ -57,7 +54,7 @@ def on_print_stats_update(self, field: str, value: dict | float | str) -> None:
if isinstance(value, str):
if "filename" in field:
self.filename = value
if self.isVisible:
if self.isVisible():
self.set_file_name(value)
elif "state" in field and value in self._REASON_HEADERS:
self.cf_info_tf.setText(self._REASON_HEADERS[value])
Expand Down Expand Up @@ -97,22 +94,25 @@ def set_pixmap(self, pixmap: QtGui.QPixmap) -> None:
def set_file_name(self, file_name: str) -> None:
self.cf_file_name.setText(file_name)

def _show_screen_thumbnail(self, dict):
try:
thumbnails = dict["thumbnail_images"]
def _show_screen_thumbnail(self, metadata: dict | None) -> None:
"""Display the largest thumbnail from file metadata.

last_thumb = QtGui.QPixmap.fromImage(thumbnails[-1])

if last_thumb.isNull():
last_thumb = QtGui.QPixmap(
"BlocksScreen/lib/ui/resources/media/logoblocks400x300.png"
)
except Exception as e:
print(e)
last_thumb = QtGui.QPixmap(
"BlocksScreen/lib/ui/resources/media/logoblocks400x300.png"
)
self.set_pixmap(last_thumb)
``thumbnail_images`` values are pre-loaded ``QImage``
objects produced by ``Files._process_metadata``.
"""
fallback = QtGui.QPixmap(
"BlocksScreen/lib/ui/resources/media/logoblocks400x300.png"
)
thumbnails = metadata.get("thumbnail_images", []) if metadata else []
if not thumbnails:
self.set_pixmap(fallback)
return

last_thumb = thumbnails[-1]
if isinstance(last_thumb, QtGui.QImage) and not last_thumb.isNull():
self.set_pixmap(QtGui.QPixmap.fromImage(last_thumb))
else:
self.set_pixmap(fallback)

def _setupUI(self) -> None:
"""Setup widget ui"""
Expand All @@ -125,11 +125,9 @@ def _setupUI(self) -> None:
sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth())
self.setSizePolicy(sizePolicy)
self.setObjectName("cancelPage")
self.setStyleSheet(
"""#cancelPage {
self.setStyleSheet("""#cancelPage {
background-image: url(:/background/media/1st_background.png);
}"""
)
}""")
self.setMinimumSize(QtCore.QSize(800, 480))
self.setMaximumSize(QtCore.QSize(800, 480))
self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight)
Expand Down
Loading
Loading