diff --git a/BlocksScreen/lib/files.py b/BlocksScreen/lib/files.py index 412f0648..454c0ca0 100644 --- a/BlocksScreen/lib/files.py +++ b/BlocksScreen/lib/files.py @@ -165,6 +165,12 @@ class Files(QtCore.QObject): - full_refresh_needed: Root changed """ + _EVT_WS_OPEN: typing.ClassVar[QtCore.QEvent.Type] = events.WebSocketOpen.type() + _EVT_KLIPPER_DISC: typing.ClassVar[QtCore.QEvent.Type] = ( + events.KlippyDisconnected.type() + ) + _EVT_FILE_DATA: typing.ClassVar[QtCore.QEvent.Type] = ReceivedFileData.type() + # Signals for API requests request_file_list = QtCore.pyqtSignal([], [str], name="api_get_files_list") request_dir_info = QtCore.pyqtSignal( @@ -225,10 +231,14 @@ def _connect_signals(self) -> None: self.request_file_metadata.connect(self.ws.api.get_gcode_metadata) def _install_event_filter(self) -> None: - """Install event filter on application instance.""" - app = QtWidgets.QApplication.instance() - if app: - app.installEventFilter(self) + """Install event filter on parent to limit scope to mainWindow events only.""" + parent = self.parent() + if parent is not None: + parent.installEventFilter(self) + else: + app = QtWidgets.QApplication.instance() + if app: + app.installEventFilter(self) @property def file_list(self) -> list[dict]: @@ -657,19 +667,18 @@ def get_dir_information( def eventFilter(self, obj: QtCore.QObject, event: QtCore.QEvent) -> bool: """Handle application-level events.""" - if event.type() == events.WebSocketOpen.type(): + etype = event.type() + if etype == self._EVT_WS_OPEN: self.initial_load() return False - - if event.type() == events.KlippyDisconnected.type(): + if etype == self._EVT_KLIPPER_DISC: self._clear_all_data() return False - return super().eventFilter(obj, event) def event(self, event: QtCore.QEvent) -> bool: """Handle object-level events.""" - if event.type() == ReceivedFileData.type(): + if event.type() == self._EVT_FILE_DATA: if isinstance(event, ReceivedFileData): self.handle_message_received(event.method, event.data, event.params) return True diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 2eb9c0e2..104221f5 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -117,6 +117,18 @@ class MainWindow(QtWidgets.QMainWindow): call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") + _EVT_WS_MSG: typing.ClassVar[QtCore.QEvent.Type] = ( + events.WebSocketMessageReceived.type() + ) + _EVT_PRINT_START: typing.ClassVar[QtCore.QEvent.Type] = events.PrintStart.type() + _EVT_PRINT_ERROR: typing.ClassVar[QtCore.QEvent.Type] = events.PrintError.type() + _EVT_PRINT_COMPLETE: typing.ClassVar[QtCore.QEvent.Type] = ( + events.PrintComplete.type() + ) + _EVT_PRINT_CANCELLED: typing.ClassVar[QtCore.QEvent.Type] = ( + events.PrintCancelled.type() + ) + def __init__(self): """Set up UI, instantiate subsystems, and wire all inter-component signals.""" super(MainWindow, self).__init__() @@ -1131,12 +1143,13 @@ def closeEvent(self, a0: QtGui.QCloseEvent | None) -> None: def event(self, event: QtCore.QEvent) -> bool: """Receives PyQt Events, reimplemented method from the QEvent class""" - if event.type() == events.WebSocketMessageReceived.type(): + etype = event.type() + if etype == self._EVT_WS_MSG: if isinstance(event, events.WebSocketMessageReceived): self.messageReceivedEvent(event) return True return False - if event.type() == events.PrintStart.type(): + if etype == self._EVT_PRINT_START: self.print_status = "printing" self.disable_tab_bar() self.ui.extruder_temp_display.clicked.disconnect() @@ -1155,13 +1168,13 @@ def event(self, event: QtCore.QEvent) -> bool: ) return False - if event.type() in ( - events.PrintError.type(), - events.PrintComplete.type(), - events.PrintCancelled.type(), + if etype in ( + self._EVT_PRINT_ERROR, + self._EVT_PRINT_COMPLETE, + self._EVT_PRINT_CANCELLED, ): self.print_status = "idle" - if event.type() == events.PrintCancelled.type(): + if etype == self._EVT_PRINT_CANCELLED: self.handle_cancel_print() self.enable_tab_bar() self.ui.extruder_temp_display.clicked.disconnect() diff --git a/BlocksScreen/lib/panels/widgets/filesPage.py b/BlocksScreen/lib/panels/widgets/filesPage.py index 969399ac..ee7aea8d 100644 --- a/BlocksScreen/lib/panels/widgets/filesPage.py +++ b/BlocksScreen/lib/panels/widgets/filesPage.py @@ -45,6 +45,7 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget] = None) -> None: self._file_list: list[dict] = [] self._files_data: dict[str, dict] = {} # filename -> metadata dict + self._display_name_to_key: dict[str, str] = {} # display_name -> filename key self._directories: list[dict] = [] self._curr_dir: str = "" self._pending_action: bool = False @@ -86,6 +87,7 @@ def reload_gcodes_folder(self) -> None: def clear_files_data(self) -> None: """Clear all cached file data.""" self._files_data.clear() + self._display_name_to_key.clear() self._pending_metadata_requests.clear() self._metadata_retry_count.clear() @@ -155,6 +157,7 @@ def on_fileinfo(self, filedata: dict) -> None: # Cache the file data self._files_data[filename] = filedata + self._display_name_to_key[self._get_display_name(filename)] = filename # Remove from pending requests and reset retry count (success) self._pending_metadata_requests.discard(filename) @@ -237,10 +240,7 @@ def _find_file_insert_position(self, modified_time: float) -> int: """ insert_pos = 0 - for i in range(self._model.rowCount()): - index = self._model.index(i) - item = self._model.data(index, QtCore.Qt.ItemDataRole.UserRole) - + for i, item in enumerate(self._model.entries): if not item: continue @@ -265,10 +265,7 @@ def _find_file_insert_position(self, modified_time: float) -> int: def _find_file_key_by_display_name(self, display_name: str) -> typing.Optional[str]: """Find the file key in _files_data by its display name.""" - for key in self._files_data: - if self._get_display_name(key) == display_name: - return key - return None + return self._display_name_to_key.get(display_name) @QtCore.pyqtSlot(dict, name="on_file_added") def on_file_added(self, file_data: dict) -> None: @@ -344,6 +341,7 @@ def on_file_removed(self, filepath: str) -> None: current = self._curr_dir.removeprefix("/") # Always clean up cache + self._display_name_to_key.pop(self._get_display_name(filepath), None) self._files_data.pop(filepath, None) self._pending_metadata_requests.discard(filepath) self._metadata_retry_count.pop(filepath, None) diff --git a/BlocksScreen/lib/panels/widgets/loadWidget.py b/BlocksScreen/lib/panels/widgets/loadWidget.py index 1119bd14..4fcb3da4 100644 --- a/BlocksScreen/lib/panels/widgets/loadWidget.py +++ b/BlocksScreen/lib/panels/widgets/loadWidget.py @@ -168,10 +168,16 @@ def resizeEvent(self, a0: QtGui.QResizeEvent | None) -> None: self.gifshow.setGeometry(gifshow_x, gifshow_y, size, size) - def show(self) -> None: - """Re-implemented method, show widget""" - self.repaint() - return super().show() + def setVisible(self, visible: bool) -> None: + """Re-implemented method, pause animation timer when hidden""" + if self.anim_type == LoadingOverlayWidget.AnimationGIF.DEFAULT: + if visible: + self.timer.start(16) + else: + self.timer.stop() + if visible: + self.repaint() + super().setVisible(visible) def _setupUI(self) -> None: self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True) diff --git a/BlocksScreen/lib/utils/blocks_button.py b/BlocksScreen/lib/utils/blocks_button.py index 8100bb5f..a5c949b4 100644 --- a/BlocksScreen/lib/utils/blocks_button.py +++ b/BlocksScreen/lib/utils/blocks_button.py @@ -33,6 +33,9 @@ def __init__( self.text_color: QtGui.QColor = QtGui.QColor(*ButtonColors.TEXT_COLOR.value) self._show_notification: bool = False self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) + self._icon_cache: QtGui.QPixmap = QtGui.QPixmap() + self._icon_cache_size: QtCore.QSize = QtCore.QSize() + self._cached_path_size: QtCore.QSize = QtCore.QSize() def setShowNotification(self, show: bool) -> None: """Set notification on button""" @@ -62,6 +65,8 @@ def setText(self, text: str) -> None: def setPixmap(self, pixmap: QtGui.QPixmap) -> None: """Set button pixmap""" self.icon_pixmap = pixmap + self._icon_cache = QtGui.QPixmap() + self._icon_cache_size = QtCore.QSize() self.update() def mousePressEvent(self, e: QtGui.QMouseEvent) -> None: @@ -118,19 +123,14 @@ def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): return # Flat button control opt = QtWidgets.QStyleOptionButton() - draw_frame = ( + if ( not self._is_flat or self.underMouse() or opt.state & QtWidgets.QStyle.StateFlag.State_Sunken - ) - if draw_frame: + ): _style.drawControl( QtWidgets.QStyle.ControlElement.CE_PushButtonLabel, opt, painter, self ) - _style.drawControl( - QtWidgets.QStyle.ControlElement.CE_PushButtonLabel, opt, painter, self - ) - self.setStyle(_style) # Determine background and text colors based on state if not self.isEnabled(): bg_color_tuple = ButtonColors.DISABLED_BG.value @@ -144,30 +144,21 @@ def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): bg_color = QtGui.QColor(*bg_color_tuple) - path = QtGui.QPainterPath() - xRadius = self.rect().toRectF().normalized().height() / 2.0 - yRadius = self.rect().toRectF().normalized().height() / 2.0 painter.setBackgroundMode(QtCore.Qt.BGMode.TransparentMode) - path.addRoundedRect( - 0, - 0, - self.rect().toRectF().normalized().width(), - self.rect().toRectF().normalized().height(), - xRadius, - yRadius, - QtCore.Qt.SizeMode.AbsoluteSize, - ) - icon_path = QtGui.QPainterPath() - self.button_ellipse = QtCore.QRectF( - self.rect().toRectF().normalized().left() - + self.rect().toRectF().normalized().height() * 0.05, - self.rect().toRectF().normalized().top() - + self.rect().toRectF().normalized().height() * 0.05, - (self.rect().toRectF().normalized().height() * 0.90), - (self.rect().toRectF().normalized().height() * 0.90), - ) - icon_path.addEllipse(self.button_ellipse) - self.button_background = path.subtracted(icon_path) + current_size = _rect.size() + if current_size != self._cached_path_size: + h = float(_rect.height()) + w = float(_rect.width()) + radius = h / 2.0 + path = QtGui.QPainterPath() + path.addRoundedRect( + 0, 0, w, h, radius, radius, QtCore.Qt.SizeMode.AbsoluteSize + ) + self.button_ellipse = QtCore.QRectF(h * 0.05, h * 0.05, h * 0.90, h * 0.90) + icon_path = QtGui.QPainterPath() + icon_path.addEllipse(self.button_ellipse) + self.button_background = path.subtracted(icon_path) + self._cached_path_size = current_size painter.setPen(QtCore.Qt.PenStyle.NoPen) painter.setBrush(bg_color) painter.fillPath(self.button_background, bg_color) @@ -179,11 +170,15 @@ def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): _parent_rect.height() * 0.80, ) if not self.icon_pixmap.isNull(): - _icon_scaled = self.icon_pixmap.scaled( - _icon_rect.size().toSize(), - QtCore.Qt.AspectRatioMode.KeepAspectRatio, - QtCore.Qt.TransformationMode.SmoothTransformation, - ) + target_size = _icon_rect.size().toSize() + if target_size != self._icon_cache_size: + self._icon_cache = self.icon_pixmap.scaled( + target_size, + QtCore.Qt.AspectRatioMode.KeepAspectRatio, + QtCore.Qt.TransformationMode.SmoothTransformation, + ) + self._icon_cache_size = target_size + _icon_scaled = self._icon_cache scaled_width = _icon_scaled.width() scaled_height = _icon_scaled.height() adjusted_x = (_icon_rect.width() - scaled_width) / 2.0 @@ -194,8 +189,6 @@ def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): scaled_width, scaled_height, ) - tinted_icon_pixmap = QtGui.QPixmap(_icon_scaled.size()) - tinted_icon_pixmap.fill(QtCore.Qt.GlobalColor.transparent) if not self.isEnabled(): tinted_icon_pixmap = QtGui.QPixmap(_icon_scaled.size()) tinted_icon_pixmap.fill(QtCore.Qt.GlobalColor.transparent) diff --git a/BlocksScreen/lib/utils/display_button.py b/BlocksScreen/lib/utils/display_button.py index 1bc7b723..053bb64b 100644 --- a/BlocksScreen/lib/utils/display_button.py +++ b/BlocksScreen/lib/utils/display_button.py @@ -21,6 +21,21 @@ def __init__(self, parent: typing.Optional["QtWidgets.QWidget"] = None) -> None: self._name: str = "" self.text_color: QtGui.QColor = QtGui.QColor(0, 0, 0) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) + self._path_cache: dict[tuple[int, int], QtGui.QPainterPath] = {} + self._icon_cache: QtGui.QPixmap = QtGui.QPixmap() + self._icon_cache_size: QtCore.QSize = QtCore.QSize() + self._icon_secondary_cache: QtGui.QPixmap = QtGui.QPixmap() + self._icon_secondary_cache_size: QtCore.QSize = QtCore.QSize() + self._font_cache: dict[int, QtGui.QFont] = {} + self._color_bg = QtGui.QColor(177, 196, 203, 75) + self._color_secondary_text = QtGui.QColor("#b6b0b0") + _hc = QtGui.QColor(self.highlight_color) + self._highlight_c1 = QtGui.QColor(_hc) + self._highlight_c1.setAlpha(40) + self._highlight_c2 = QtGui.QColor(_hc) + self._highlight_c2.setAlpha(35) + self._highlight_c3 = QtGui.QColor(_hc) + self._highlight_c3.setAlpha(1) @property def name(self): @@ -30,13 +45,26 @@ def name(self): def setPixmap(self, pixmap: QtGui.QPixmap) -> None: """Set widget pixmap""" self.icon_pixmap = pixmap + self._icon_cache = QtGui.QPixmap() + self._icon_cache_size = QtCore.QSize() self.repaint() def setSecondaryPixmap(self, pixmap: QtGui.QPixmap) -> None: """Set secondary widget pixmap""" self.icon_pixmap_secondary = pixmap + self._icon_secondary_cache = QtGui.QPixmap() + self._icon_secondary_cache_size = QtCore.QSize() self.repaint() + def _get_font(self, point_size: int) -> QtGui.QFont: + cached = self._font_cache.get(point_size) + if cached is None: + f = QtGui.QFont() + f.setPointSize(point_size) + f.setFamily("Momcake-bold") + self._font_cache[point_size] = cached = f + return cached + @property def button_type(self) -> str: """Widget button type""" @@ -81,7 +109,6 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: painter = QtWidgets.QStylePainter(self) painter.setRenderHint(painter.RenderHint.Antialiasing, True) painter.setRenderHint(painter.RenderHint.SmoothPixmapTransform, True) - painter.setRenderHint(painter.RenderHint.LosslessImageRendering, True) _rect = self.rect() _style = self.style() @@ -89,16 +116,16 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: if not _style or _rect is None: return margin = _style.pixelMetric(_style.PixelMetric.PM_ButtonMargin, opt, self) - # Rounded background edges - path = QtGui.QPainterPath() - path.addRoundedRect( - self.rect().toRectF(), - 10.0, - 10.0, - QtCore.Qt.SizeMode.AbsoluteSize, - ) + path_key = (_rect.width(), _rect.height()) + path = self._path_cache.get(path_key) + if path is None: + path = QtGui.QPainterPath() + path.addRoundedRect( + _rect.toRectF(), 10.0, 10.0, QtCore.Qt.SizeMode.AbsoluteSize + ) + self._path_cache[path_key] = path - painter.fillPath(path, QtGui.QColor(177, 196, 203, 75)) + painter.fillPath(path, self._color_bg) painter.setPen(QtCore.Qt.PenStyle.SolidLine) painter.setPen(QtCore.Qt.GlobalColor.white) @@ -107,24 +134,16 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: _pen.setStyle(QtCore.Qt.PenStyle.SolidLine) _pen.setJoinStyle(QtCore.Qt.PenJoinStyle.RoundJoin) _pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap) - _color = QtGui.QColor(self.highlight_color) - _color2 = QtGui.QColor(self.highlight_color) - _color3 = QtGui.QColor(self.highlight_color) - _color.setAlpha(40) - _color2.setAlpha(35) - _color3.setAlpha(1) - _pen.setColor(_color) + _pen.setColor(self._highlight_c1) + _rect_f = _rect.toRectF() _gradient = QtGui.QRadialGradient( - QtCore.QPointF( - self.rect().toRectF().left() + 2, - self.rect().toRectF().top(), - ), + QtCore.QPointF(_rect_f.left() + 2, _rect_f.top()), 150.0, - self.rect().toRectF().center(), + _rect_f.center(), ) - _gradient.setColorAt(0, _color) - _gradient.setColorAt(0.5, _color2) - _gradient.setColorAt(1, _color3) + _gradient.setColorAt(0, self._highlight_c1) + _gradient.setColorAt(0.5, self._highlight_c2) + _gradient.setColorAt(1, self._highlight_c3) _pen.setBrush(_gradient) painter.fillPath(path, _pen.brush()) @@ -140,12 +159,17 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: _rect.height() - 5, ) - _icon_scaled = self.icon_pixmap.scaled( - int(_icon_rect.width()), - int(_icon_rect.height()), - QtCore.Qt.AspectRatioMode.KeepAspectRatio, - QtCore.Qt.TransformationMode.SmoothTransformation, + target_size = QtCore.QSize( + int(_icon_rect.width()), int(_icon_rect.height()) ) + if target_size != self._icon_cache_size: + self._icon_cache = self.icon_pixmap.scaled( + target_size, + QtCore.Qt.AspectRatioMode.KeepAspectRatio, + QtCore.Qt.TransformationMode.SmoothTransformation, + ) + self._icon_cache_size = target_size + _icon_scaled = self._icon_cache scaled_width = _icon_scaled.width() scaled_height = _icon_scaled.height() @@ -193,10 +217,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: int(_mtl.width() / 2.0), _rect.height(), ) - font = QtGui.QFont() - font.setPointSize(12) - font.setFamily("Momcake-bold") - painter.setFont(font) + painter.setFont(self._get_font(12)) painter.drawText( _ptl_rect, QtCore.Qt.TextFlag.TextShowMnemonic @@ -237,10 +258,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: _mtl.width() - margin * 2, (_mtl.height() * 0.5) // 2, ) - font = QtGui.QFont() - font.setPointSize(20) - font.setFamily("Momcake-bold") - painter.setFont(font) + painter.setFont(self._get_font(20)) painter.setCompositionMode( painter.CompositionMode.CompositionMode_SourceAtop ) @@ -250,9 +268,8 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: | QtCore.Qt.AlignmentFlag.AlignVCenter, self.text(), ) - font.setPointSize(14) - painter.setPen(QtGui.QColor("#b6b0b0")) - painter.setFont(font) + painter.setPen(self._color_secondary_text) + painter.setFont(self._get_font(14)) _ = painter.drawText( _downer_rect, @@ -279,12 +296,15 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: _icon_size, _icon_size, ) - _first_icon_scaled = self.icon_pixmap.scaled( - int(_icon_size), - int(_icon_size), - QtCore.Qt.AspectRatioMode.KeepAspectRatio, - QtCore.Qt.TransformationMode.SmoothTransformation, - ) + _dual_icon_size = QtCore.QSize(int(_icon_size), int(_icon_size)) + if _dual_icon_size != self._icon_cache_size: + self._icon_cache = self.icon_pixmap.scaled( + _dual_icon_size, + QtCore.Qt.AspectRatioMode.KeepAspectRatio, + QtCore.Qt.TransformationMode.SmoothTransformation, + ) + self._icon_cache_size = _dual_icon_size + _first_icon_scaled = self._icon_cache _first_adjusted_icon_rect = QtCore.QRectF( _first_icon_rect.x() + (_icon_size - _first_icon_scaled.width()) / 2.0, @@ -305,10 +325,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: _rect.width() - _icon_size - _margin * 3, _row_height, ) - font = QtGui.QFont() - font.setPointSize(20) - font.setFamily("Momcake-bold") - painter.setFont(font) + painter.setFont(self._get_font(20)) painter.drawText( _first_text_rect, QtCore.Qt.TextFlag.TextShowMnemonic @@ -324,12 +341,14 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: _icon_size, _icon_size, ) - _second_icon_scaled = self.icon_pixmap_secondary.scaled( - int(_icon_size), - int(_icon_size), - QtCore.Qt.AspectRatioMode.KeepAspectRatio, - QtCore.Qt.TransformationMode.SmoothTransformation, - ) + if _dual_icon_size != self._icon_secondary_cache_size: + self._icon_secondary_cache = self.icon_pixmap_secondary.scaled( + _dual_icon_size, + QtCore.Qt.AspectRatioMode.KeepAspectRatio, + QtCore.Qt.TransformationMode.SmoothTransformation, + ) + self._icon_secondary_cache_size = _dual_icon_size + _second_icon_scaled = self._icon_secondary_cache _second_adjusted_icon_rect = QtCore.QRectF( _second_icon_rect.x() + (_icon_size - _second_icon_scaled.width()) / 2.0, @@ -349,9 +368,8 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: _rect.width() - _icon_size - _margin * 3, _row_height, ) - font.setPointSize(15) - painter.setPen(QtGui.QColor("#b6b0b0")) - painter.setFont(font) + painter.setPen(self._color_secondary_text) + painter.setFont(self._get_font(15)) painter.drawText( _second_text_rect, QtCore.Qt.TextFlag.TextShowMnemonic @@ -360,10 +378,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: str(self.secondary_text) if self.secondary_text else "?", ) else: - font = QtGui.QFont() - font.setPointSize(12) - font.setFamily("Momcake-bold") - painter.setFont(font) + painter.setFont(self._get_font(12)) _ = painter.drawText( _mtl, QtCore.Qt.TextFlag.TextShowMnemonic diff --git a/BlocksScreen/lib/utils/list_model.py b/BlocksScreen/lib/utils/list_model.py index 5d0696a1..207670b7 100644 --- a/BlocksScreen/lib/utils/list_model.py +++ b/BlocksScreen/lib/utils/list_model.py @@ -32,7 +32,6 @@ class ListItem: height: int = 60 notificate: bool = False - # stores width and heitgh of the button so we dont need to recalculate it every time _cache: typing.Dict[int, int] = field(default_factory=dict) def clear_cache(self): @@ -300,6 +299,22 @@ def __init__(self) -> None: self.prev_index: int = 0 self.height: int = 60 self._scaled_cache: dict[tuple[int, int, int], QtGui.QPixmap] = {} + self._tinted_cache: dict[tuple[int, str], QtGui.QPixmap] = {} + self._font_cache: dict[int, tuple[QtGui.QFont, QtGui.QFontMetrics]] = {} + self._path_cache: dict[tuple[int, int], QtGui.QPainterPath] = {} + # Pre-computed colors — avoids QColor allocation + setAlpha per paint frame + self._color_pressed_sel = QtGui.QColor("#1A8FBF") + self._color_pressed_sel.setAlpha(90) + self._color_pressed_unsel = QtGui.QColor("#1A8FBF") + self._color_pressed_unsel.setAlpha(20) + self._color_text = QtGui.QColor(255, 255, 255) + self._color_secondary = QtGui.QColor(160, 160, 160) + self._color_notification = QtGui.QColor(226, 31, 31) + # Arrow icons loaded once — avoids QPixmap(resource) parse per paint frame + self._arrow_down = QtGui.QPixmap(":/arrow_icons/media/btn_icons/arrow_down.svg") + self._arrow_right = QtGui.QPixmap( + ":/arrow_icons/media/btn_icons/arrow_right.svg" + ) def _get_scaled( self, @@ -325,12 +340,46 @@ def _get_scaled( # Prevent unbounded growth — 64 entries covers all wifi # bar variants × protected/open × left/right icons easily. if len(self._scaled_cache) > 64: - # Drop oldest half keys = list(self._scaled_cache) for k in keys[:32]: del self._scaled_cache[k] return scaled + def _get_font_metrics( + self, base_font: QtGui.QFont, point_size: int + ) -> tuple[QtGui.QFont, QtGui.QFontMetrics]: + cached = self._font_cache.get(point_size) + if cached is None: + if point_size != base_font.pointSize(): + f = QtGui.QFont(base_font) + f.setPointSize(point_size) + else: + f = base_font + cached = (f, QtGui.QFontMetrics(f)) + self._font_cache[point_size] = cached + return cached + + def _get_tinted(self, pixmap: QtGui.QPixmap, color: str) -> QtGui.QPixmap: + key = (pixmap.cacheKey(), color) + cached = self._tinted_cache.get(key) + if cached is None: + tinted = QtGui.QPixmap(pixmap.size()) + tinted.fill(QtCore.Qt.GlobalColor.transparent) + p = QtGui.QPainter(tinted) + p.drawPixmap(0, 0, pixmap) + p.setCompositionMode( + QtGui.QPainter.CompositionMode.CompositionMode_SourceIn + ) + p.fillRect(tinted.rect(), QtGui.QColor(color)) + p.end() + cached = tinted + self._tinted_cache[key] = cached + if len(self._tinted_cache) > 32: + keys = list(self._tinted_cache) + for k in keys[:16]: + del self._tinted_cache[k] + return cached + def clear(self) -> None: """Clears delegate indexing""" self.prev_index = 0 @@ -390,7 +439,6 @@ def sizeHint( ) final_height = max(item.height, text_rect.height() - 1) - # Cache it item._cache[target_width] = final_height + 20 return QtCore.QSize(target_width, int(final_height * 1.2)) @@ -406,118 +454,89 @@ def paint( painter.setRenderHint(QtGui.QPainter.RenderHint.SmoothPixmapTransform, True) item = index.data(QtCore.Qt.ItemDataRole.UserRole) - rect = option.rect.adjusted(2, 2, -2, -2) - - path = QtGui.QPainterPath() - path.addRoundedRect(QtCore.QRectF(rect), 12, 12) - if item.not_clickable: painter.restore() return + + rect = option.rect.adjusted(2, 2, -2, -2) + w, h = rect.width(), rect.height() + # Translate so all geometry is relative to (0,0) — enables path cache hits + # across items (all rows share the same w/h in a fixed-width list view). + painter.translate(rect.x(), rect.y()) + + path_key = (w, h) + path = self._path_cache.get(path_key) + if path is None: + path = QtGui.QPainterPath() + path.addRoundedRect(QtCore.QRectF(0, 0, w, h), 12, 12) + self._path_cache[path_key] = path + if item.allow_expand and item.needs_expansion: item.right_icon = ( - QtGui.QPixmap(":/arrow_icons/media/btn_icons/arrow_down.svg") - if item.is_expanded - else QtGui.QPixmap(":/arrow_icons/media/btn_icons/arrow_right.svg") + self._arrow_down if item.is_expanded else self._arrow_right ) - # Background Color - pressed_color = QtGui.QColor("#1A8FBF") - pressed_color.setAlpha(90 if item.selected else 20) - + pressed_color = ( + self._color_pressed_sel if item.selected else self._color_pressed_unsel + ) painter.setPen(QtCore.Qt.PenStyle.NoPen) painter.setBrush(pressed_color) painter.fillPath(path, pressed_color) - # Geometry Calc - - # ICON SPACEEE ellipse_size = item.height * 0.8 ellipse_margin = (item.height - ellipse_size) / 2 ellipse_rect = QtCore.QRectF( - rect.right() - ellipse_margin - ellipse_size, - rect.top() + ellipse_margin, + w - ellipse_margin - ellipse_size, + ellipse_margin, ellipse_size, ellipse_size, ) if item.right_icon: - icon_scaled = item.right_icon.scaled( - ellipse_rect.size().toSize(), - QtCore.Qt.AspectRatioMode.KeepAspectRatio, - QtCore.Qt.TransformationMode.SmoothTransformation, - ) - painter.drawPixmap( - ellipse_rect.toRect(), - icon_scaled, + icon_scaled = self._get_scaled( + item.right_icon, ellipse_rect.size().toSize() ) + painter.drawPixmap(ellipse_rect.toRect(), icon_scaled) left_margin = 10 left_icon_rect = QtCore.QRectF( - rect.left() + ellipse_margin, - rect.top() + ellipse_margin, + ellipse_margin, + ellipse_margin, ellipse_size, ellipse_size, ) if item.left_icon: - l_icon_scaled = item.left_icon.scaled( - int(left_icon_rect.width()), - int(left_icon_rect.height()), - QtCore.Qt.AspectRatioMode.KeepAspectRatio, - QtCore.Qt.TransformationMode.SmoothTransformation, + l_icon_scaled = self._get_scaled( + item.left_icon, + QtCore.QSize(int(left_icon_rect.width()), int(left_icon_rect.height())), ) - if item.color_left_icon: - tinted = QtGui.QPixmap(l_icon_scaled.size()) - tinted.fill(QtCore.Qt.GlobalColor.transparent) - p2 = QtGui.QPainter(tinted) - p2.drawPixmap(0, 0, l_icon_scaled) - p2.setCompositionMode( - QtGui.QPainter.CompositionMode.CompositionMode_SourceIn - ) - p2.fillRect(tinted.rect(), QtGui.QColor(item.color)) - p2.end() painter.drawPixmap( left_icon_rect.toRect(), - tinted, + self._get_tinted(l_icon_scaled, item.color), ) else: - painter.drawPixmap( - left_icon_rect.toRect(), - l_icon_scaled, - ) + painter.drawPixmap(left_icon_rect.toRect(), l_icon_scaled) - text_margin = int( - rect.right() - ellipse_size - ellipse_margin - rect.height() * 0.10 - ) + text_margin = int(w - ellipse_size - ellipse_margin - h * 0.10) + icon_w = left_icon_rect.width() if item.left_icon else 0 text_rect = QtCore.QRectF( - rect.left() - + left_margin - + (left_icon_rect.width() if item.left_icon else 0), - rect.top(), - text_margin - - rect.left() - - left_margin - - (left_icon_rect.width() if item.left_icon else 0), - rect.height(), + left_margin + icon_w, + 0, + text_margin - left_margin - icon_w, + h, ) - painter.setPen(QtGui.QColor(255, 255, 255)) + painter.setPen(self._color_text) _font = painter.font() - if item._lfontsize > 0: - _font.setPointSize(item._lfontsize) - painter.setFont(_font) - - metrics = QtGui.QFontMetrics(_font) - - right_font = QtGui.QFont(_font) - if item._rfontsize > 0: - right_font.setPointSize(item._rfontsize) - - right_metrics = QtGui.QFontMetrics(right_font) + lps = item._lfontsize if item._lfontsize > 0 else _font.pointSize() + rps = item._rfontsize if item._rfontsize > 0 else _font.pointSize() + font, metrics = self._get_font_metrics(_font, lps) + right_font, right_metrics = self._get_font_metrics(_font, rps) + painter.setFont(font) right_text_x = ( ellipse_rect.right() @@ -527,7 +546,6 @@ def paint( ) text = item.text.replace("\n", "") - # Logic: If not expanded, OR if expansion is not needed, draw single line if not item.is_expanded: max_main_text_width = right_text_x - left_margin text = metrics.elidedText( @@ -535,13 +553,8 @@ def paint( QtCore.Qt.TextElideMode.ElideRight, int(max_main_text_width), ) - painter.drawText( - text_rect, - QtCore.Qt.AlignmentFlag.AlignVCenter, - text, - ) + painter.drawText(text_rect, QtCore.Qt.AlignmentFlag.AlignVCenter, text) else: - # Expanded mode painter.drawText( text_rect, QtCore.Qt.AlignmentFlag.AlignLeft @@ -552,7 +565,7 @@ def paint( if item.right_text: painter.setFont(right_font) - painter.setPen(QtGui.QColor(160, 160, 160)) + painter.setPen(self._color_secondary) painter.drawText( int(right_text_x), int( @@ -563,13 +576,11 @@ def paint( ) if item.notificate: - dot_diameter = rect.height() * 0.3 - dot_x = rect.width() - dot_diameter - 5 - notification_color = QtGui.QColor(226, 31, 31) - painter.setBrush(notification_color) + dot_diameter = h * 0.3 + dot_x = w - dot_diameter - 5 + painter.setBrush(self._color_notification) painter.setPen(QtCore.Qt.PenStyle.NoPen) - dot_rect = QtCore.QRectF(dot_x, rect.top(), dot_diameter, dot_diameter) - painter.drawEllipse(dot_rect) + painter.drawEllipse(QtCore.QRectF(dot_x, 0, dot_diameter, dot_diameter)) painter.restore() @@ -602,8 +613,6 @@ def editorEvent( # pylint: disable=invalid-name ) pos = event.position() - # --- Logic Check --- - # Only allow toggle if allow_expand AND text actually needs expansion if ( ellipse_rect.contains(pos) and item.allow_expand diff --git a/BlocksScreen/lib/utils/toggleAnimatedButton.py b/BlocksScreen/lib/utils/toggleAnimatedButton.py index b9555876..f0aa2ebf 100644 --- a/BlocksScreen/lib/utils/toggleAnimatedButton.py +++ b/BlocksScreen/lib/utils/toggleAnimatedButton.py @@ -34,6 +34,8 @@ def __init__(self, parent) -> None: ) self.icon_pixmap: QtGui.QPixmap = QtGui.QPixmap() + self._icon_cache: QtGui.QPixmap = QtGui.QPixmap() + self._icon_cache_size: QtCore.QSize = QtCore.QSize() self._backgroundColor: QtGui.QColor = QtGui.QColor(223, 223, 223) self._handleColor: QtGui.QColor = QtGui.QColor(255, 100, 10) @@ -163,7 +165,8 @@ def showEvent(self, a0: QtGui.QShowEvent) -> None: def setPixmap(self, pixmap: QtGui.QPixmap) -> None: """Set widget pixmap""" self.icon_pixmap = pixmap - # self.repaint() + self._icon_cache = QtGui.QPixmap() + self._icon_cache_size = QtCore.QSize() self.update() @QtCore.pyqtSlot(name="clicked") @@ -197,19 +200,18 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: _rect = self.contentsRect() bg_color = self.backgroundColor - self.handlePath: QtGui.QPainterPath = QtGui.QPainterPath() self.handle_ellipseRect = QtCore.QRectF( self._handle_position, ((_rect.toRectF().normalized().height() * 0.20) // 2), (_rect.toRectF().normalized().height() * 0.80), (_rect.toRectF().normalized().height() * 0.80), ) + self.handlePath.clear() self.handlePath.addEllipse(self.handle_ellipseRect) painter = QtGui.QPainter(self) painter.setRenderHint(painter.RenderHint.Antialiasing) painter.setRenderHint(painter.RenderHint.SmoothPixmapTransform) painter.setBackgroundMode(QtCore.Qt.BGMode.TransparentMode) - painter.setRenderHint(painter.RenderHint.LosslessImageRendering) rect_norm = _rect.toRectF().normalized() min_x = rect_norm.x() @@ -217,7 +219,6 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: progress = (self._handle_position - min_x) / (max_x - min_x) progress = max(0.0, min(1.0, progress)) - # Inline color interpolation (no separate functions) r = ( self._handleOFFcolor.red() + (self._handleONcolor.red() - self._handleOFFcolor.red()) * progress @@ -235,7 +236,8 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: + (self._handleONcolor.alpha() - self._handleOFFcolor.alpha()) * progress ) - self.handleColor = QtGui.QColor(int(r), int(g), int(b), int(a)) + computed_handle_color = QtGui.QColor(int(r), int(g), int(b), int(a)) + self._handleColor = computed_handle_color painter.fillPath( self.trailPath, @@ -243,7 +245,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: ) painter.fillPath( self.handlePath, - self.handleColor if self.isEnabled() else self.disable_handle_color, + computed_handle_color if self.isEnabled() else self.disable_handle_color, ) if not self.icon_pixmap.isNull(): @@ -254,12 +256,15 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: self.handle_ellipseRect.width() * 0.90, self.handle_ellipseRect.height() * 0.90, ) - _icon_scaled = self.icon_pixmap.scaled( - _icon_rect.size().toSize(), - QtCore.Qt.AspectRatioMode.KeepAspectRatio, - QtCore.Qt.TransformationMode.SmoothTransformation, - ) - # Calculate the actual QRect for the scaled pixmap (centering it if needed) + target_size = _icon_rect.size().toSize() + if target_size != self._icon_cache_size: + self._icon_cache = self.icon_pixmap.scaled( + target_size, + QtCore.Qt.AspectRatioMode.KeepAspectRatio, + QtCore.Qt.TransformationMode.SmoothTransformation, + ) + self._icon_cache_size = target_size + _icon_scaled = self._icon_cache scaled_width = _icon_scaled.width() scaled_height = _icon_scaled.height() adjusted_x = (_icon_rect.width() - scaled_width) // 2.0 @@ -271,8 +276,8 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: scaled_height, ) painter.drawPixmap( - adjusted_icon_rect, # Target area (center adjusted) - _icon_scaled, # Scaled pixmap - _icon_scaled.rect().toRectF(), # Entire source (scaled) pixmap + adjusted_icon_rect, + _icon_scaled, + _icon_scaled.rect().toRectF(), ) painter.end() diff --git a/BlocksScreen/screensaver.py b/BlocksScreen/screensaver.py index de02ba02..96524a26 100644 --- a/BlocksScreen/screensaver.py +++ b/BlocksScreen/screensaver.py @@ -8,6 +8,16 @@ class ScreenSaver(QtCore.QObject): dpms_suspend_timeout = helper_methods.get_dpms_timeouts().get("suspend_timeout") dpms_standby_timeout = helper_methods.get_dpms_timeouts().get("standby_timeout") touch_blocked: bool = False + _TOUCH_INTS: frozenset = frozenset( + e.value + for e in ( + QtCore.QEvent.Type.TouchBegin, + QtCore.QEvent.Type.TouchUpdate, + QtCore.QEvent.Type.TouchEnd, + QtCore.QEvent.Type.MouseButtonPress, + QtCore.QEvent.Type.MouseButtonDblClick, + ) + ) def __init__(self, parent) -> None: super().__init__() @@ -31,31 +41,26 @@ def __init__(self, parent) -> None: def eventFilter(self, object, event) -> bool: """Filter touch events considering DPMS Screen state""" - if event.type() in ( # Block Touch Filter and Wake Touch Filter - QtCore.QEvent.Type.TouchBegin, - QtCore.QEvent.Type.TouchUpdate, - QtCore.QEvent.Type.TouchEnd, - QtCore.QEvent.Type.MouseButtonPress, - QtCore.QEvent.Type.MouseButtonDblClick, - ): - dpms_info = helper_methods.get_dpms_info() - if ( - dpms_info.get("state") - in ( - helper_methods.DPMSState.OFF, - helper_methods.DPMSState.STANDBY, - helper_methods.DPMSState.SUSPEND, - ) - or self.touch_blocked - ): - if not self.timer.isActive(): - self.touch_blocked = False - helper_methods.set_dpms_mode(helper_methods.DPMSState.ON) - self.timer.start() - return True # filter out the event, block touch events on the application - else: - self.timer.stop() - self.timer.start() + if event.type().value in self._TOUCH_INTS: + if self.touch_blocked: + # Screen may be off — query DPMS only in this rare state + dpms_info = helper_methods.get_dpms_info() + if ( + dpms_info.get("state") + in ( + helper_methods.DPMSState.OFF, + helper_methods.DPMSState.STANDBY, + helper_methods.DPMSState.SUSPEND, + ) + or self.touch_blocked + ): + if not self.timer.isActive(): + self.touch_blocked = False + helper_methods.set_dpms_mode(helper_methods.DPMSState.ON) + self.timer.start() + return True # filter out the event, block touch events on the application + self.timer.stop() + self.timer.start() return False def check_dpms(self) -> None: