diff --git a/BlocksScreen/lib/files.py b/BlocksScreen/lib/files.py index f080e6d7..0eda561d 100644 --- a/BlocksScreen/lib/files.py +++ b/BlocksScreen/lib/files.py @@ -54,14 +54,12 @@ def __init__( @property def file_list(self): - """Get the current list of files""" + """Available files list""" return self.files def handle_message_received(self, method: str, data, params: dict) -> None: """Handle file related messages received by moonraker""" if "server.files.list" in method: - # Get all files in root and its subdirectories and - # request their metadata self.files.clear() self.files = data [self.request_file_metadata.emit(item["path"]) for item in self.files] @@ -73,8 +71,6 @@ def handle_message_received(self, method: str, data, params: dict) -> None: else: self.files_metadata[data["filename"]] = data elif "server.files.get_directory" in method: - # Emit here the files for each directory so the - # ui can build the files list self.directories = data.get("dirs", {}) self.files.clear() self.files = data.get("files", []) @@ -99,7 +95,7 @@ def on_request_fileinfo(self, filename: str) -> None: """Requests metadata for a file Args: - filename (str): file + filename (str): file to get metadata from """ _data: dict = { "thumbnail_images": list, @@ -134,7 +130,6 @@ def on_request_fileinfo(self, filename: str) -> None: ) _thumbnail_images = list(map(lambda path: QtGui.QImage(path), _thumbnail_paths)) _data.update({"thumbnail_images": _thumbnail_images}) - _data.update({"filament_total": _file_metadata.get("filament_total", "?")}) _data.update({"estimated_time": _file_metadata.get("estimated_time", 0)}) _data.update({"layer_count": _file_metadata.get("layer_count", -1.0)}) @@ -165,18 +160,15 @@ def on_request_fileinfo(self, filename: str) -> None: self.fileinfo.emit(_data) def eventFilter(self, a0: QtCore.QObject, a1: QtCore.QEvent) -> bool: - """Filter Klippy related events""" + """Handle Websocket and Klippy events""" + if a1.type() == events.WebSocketOpen.type(): + self.request_file_list.emit() + self.request_dir_info[str, bool].emit("", False) + return False if a1.type() == events.KlippyDisconnected.type(): self.files_metadata.clear() self.files.clear() return False - if a1.type() == events.KlippyReady.type(): - # Request all files including in subdirectories - # in order to get all metadata - self.request_file_list.emit() - # List and directory build is depended only on this signal - self.request_dir_info[str, bool].emit("", False) - return False return super().eventFilter(a0, a1) def event(self, a0: QtCore.QEvent) -> bool: diff --git a/BlocksScreen/lib/panels/widgets/confirmPage.py b/BlocksScreen/lib/panels/widgets/confirmPage.py index c09f31b5..12432449 100644 --- a/BlocksScreen/lib/panels/widgets/confirmPage.py +++ b/BlocksScreen/lib/panels/widgets/confirmPage.py @@ -11,7 +11,7 @@ class ConfirmWidget(QtWidgets.QWidget): on_accept: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - str, list, name="on_accept" + str, name="on_accept" ) request_back: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( name="request-back" @@ -31,7 +31,7 @@ def __init__(self, parent) -> None: self.filename = "" self.confirm_button.clicked.connect( lambda: self.on_accept.emit( - str(os.path.join(self.directory, self.filename)), self._thumbnails + str(os.path.join(self.directory, self.filename)) ) ) self.back_btn.clicked.connect(self.request_back.emit) diff --git a/BlocksScreen/lib/panels/widgets/jobStatusPage.py b/BlocksScreen/lib/panels/widgets/jobStatusPage.py index a09bdcd7..0d7c4e29 100644 --- a/BlocksScreen/lib/panels/widgets/jobStatusPage.py +++ b/BlocksScreen/lib/panels/widgets/jobStatusPage.py @@ -1,6 +1,5 @@ import logging import typing - import events from helper_methods import calculate_current_layer, estimate_print_time from lib.panels.widgets import dialogPage @@ -10,22 +9,19 @@ from lib.utils.display_button import DisplayButton from PyQt6 import QtCore, QtGui, QtWidgets +logger = logging.getLogger("logs/BlocksScreen.log") -class ClickableGraphicsView(QtWidgets.QGraphicsView): - """Re-implementation of QGraphicsView that adds clicked signal""" - clicked = QtCore.pyqtSignal() +class JobStatusWidget(QtWidgets.QWidget): + """Job status widget page, page shown when there is a active print job. + Enables mid print printer tuning and inspection of print progress. -def mousePressEvent(self, event: QtGui.QMouseEvent) -> None: - """Filter mouse press events""" - if event.button() == QtCore.Qt.MouseButton.LeftButton: - self.clicked.emit() - return True # Issue event handled - super(ClickableGraphicsView, self).mousePressEvent(event) + Args: + QtWidgets (QtWidgets.QWidget): Parent widget + """ -class JobStatusWidget(QtWidgets.QWidget): print_start: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( str, name="print_start" ) @@ -61,62 +57,90 @@ class JobStatusWidget(QtWidgets.QWidget): def __init__(self, parent) -> None: super().__init__(parent) - - self.cancel_print_dialog = dialogPage.DialogPage(self) + self.thumbnail_graphics = [] self._setupUI() + self.cancel_print_dialog = dialogPage.DialogPage(self) self.tune_menu_btn.clicked.connect(self.tune_clicked.emit) self.pause_printing_btn.clicked.connect(self.pause_resume_print) self.stop_printing_btn.clicked.connect(self.handleCancel) - self.CBVSmallThumbnail.clicked.connect(self.showthumbnail) - self.CBVBigThumbnail.clicked.connect(self.hidethumbnail) - - self.smalthumbnail = QtGui.QImage( - "BlocksScreen/lib/ui/resources/media/smalltest.png" - ) - self.bigthumbnail = QtGui.QImage( - "BlocksScreen/lib/ui/resources/media/thumbnailmissing.png" - ) - self.CBVSmallThumbnail.installEventFilter(self) - self.CBVBigThumbnail.installEventFilter(self) + @QtCore.pyqtSlot(name="toggle-thumbnail-expansion") + def toggle_thumbnail_expansion(self) -> None: + """Toggle thumbnail expansion""" + if not self.thumbnail_view.scene(): + return + if not self.thumbnail_view.isVisible(): + self.thumbnail_view.show() + self.progressWidget.hide() + self.contentWidget.hide() + self.printing_progress_bar.hide() + self.btnWidget.hide() + self.headerWidget.hide() + return + self.thumbnail_view.hide() + self.progressWidget.show() + self.contentWidget.show() + self.printing_progress_bar.show() + self.btnWidget.show() + self.headerWidget.show() + self.show() - def eventFilter(self, source, event): - """Re-implemented method, filter events""" - if ( - source == self.CBVSmallThumbnail - and event.type() == QtCore.QEvent.Type.MouseButtonPress - ): - if event.button() == QtCore.Qt.MouseButton.LeftButton: - self.showthumbnail() + def eventFilter(self, sender_obj: QtCore.QObject, event: events.QEvent) -> bool: + """Filter events, + currently only filters events from `self.thumbnail_view` QGraphicsView widget + """ if ( - source == self.CBVBigThumbnail + sender_obj == self.thumbnail_view and event.type() == QtCore.QEvent.Type.MouseButtonPress ): - if event.button() == QtCore.Qt.MouseButton.LeftButton: - self.hidethumbnail() - - return super().eventFilter(source, event) - - @QtCore.pyqtSlot(name="show-thumbnail") - def showthumbnail(self): - """Show print job fullscreen thumbnail""" - self.contentWidget.hide() - self.progressWidget.hide() - self.headerWidget.hide() - self.btnWidget.hide() - self.smallthumb_widget.hide() - self.bigthumb_widget.show() - - @QtCore.pyqtSlot(name="hide-thumbnail") - def hidethumbnail(self): - """Hide print job fullscreen thumbnail""" - self.contentWidget.show() - self.progressWidget.show() - self.headerWidget.show() - self.btnWidget.show() - self.smallthumb_widget.show() - self.bigthumb_widget.hide() + self.toggle_thumbnail_expansion() + return True + return super().eventFilter(sender_obj, event) + + def _load_thumbnails(self, *thumbnails) -> None: + """Pre-load available thumbnails for the current print object""" + self.thumbnail_graphics = list( + filter( + lambda thumb: not thumb.isNull(), + [QtGui.QPixmap(thumb) for thumb in thumbnails], + ) + ) + if not self.thumbnail_graphics: + logger.debug("Unable to load thumbnails, no thumbnails provided") + return + self.create_thumbnail_widget() + self.thumbnail_view.installEventFilter( + self + ) # Filter events on this widget, for clicks + scene = QtWidgets.QGraphicsScene() + _biggest_thumb = self.thumbnail_graphics[-1] + self.thumbnail_view.setSceneRect( + QtCore.QRectF( + self.rect().x(), + self.rect().y(), + _biggest_thumb.width(), + _biggest_thumb.height(), + ) + ) + scaled = QtGui.QPixmap(_biggest_thumb).scaled( + _biggest_thumb.width(), + _biggest_thumb.height(), + QtCore.Qt.AspectRatioMode.KeepAspectRatio, + QtCore.Qt.TransformationMode.SmoothTransformation, + ) + item = QtWidgets.QGraphicsPixmapItem(scaled) + scene.addItem(item) + self.thumbnail_view.setFrameRect( + QtCore.QRect( + 0, 0, self.contentsRect().width(), self.contentsRect().height() + ) + ) + self.thumbnail_view.setScene(scene) + self.printing_progress_bar.set_inner_pixmap(self.thumbnail_graphics[-1]) + self.printing_progress_bar.thumbnail_clicked.connect( + self.toggle_thumbnail_expansion + ) @QtCore.pyqtSlot(name="handle-cancel") def handleCancel(self) -> None: @@ -127,23 +151,16 @@ def handleCancel(self) -> None: self.cancel_print_dialog.accepted.connect(self.print_cancel) self.cancel_print_dialog.open() - @QtCore.pyqtSlot(str, list, name="on_print_start") - def on_print_start(self, file: str, thumbnails: list) -> None: + @QtCore.pyqtSlot(str, name="on_print_start") + def on_print_start(self, file: str) -> None: """Start a print job, show job status page""" self._current_file_name = file self.js_file_name_label.setText(self._current_file_name) self.layer_display_button.setText("?") self.print_time_display_button.setText("?") - if thumbnails: - self.smalthumbnail = thumbnails[0] - self.bigthumbnail = thumbnails[1] - self.printing_progress_bar.reset() self._internal_print_status = "printing" - self.request_file_info.emit( - file - ) # Request file metadata (or file info whatever) - + self.request_file_info.emit(file) self.print_start.emit(file) print_start_event = events.PrintStart( self._current_file_name, self.file_metadata @@ -155,24 +172,16 @@ def on_print_start(self, file: str, thumbnails: list) -> None: else: raise TypeError("QApplication.instance expected non None value") except Exception as e: - logging.debug(f"Unexpected error while posting print job start event: {e}") + logger.debug(f"Unexpected error while posting print job start event: {e}") @QtCore.pyqtSlot(dict, name="on_fileinfo") def on_fileinfo(self, fileinfo: dict) -> None: """Handle received file information/metadata""" self.total_layers = str(fileinfo.get("layer_count", "?")) self.layer_display_button.setText("?") - if ( - fileinfo.get("thumbnail_images", []) - and len(fileinfo.get("thumbnail_images", [])) > 0 - ): - self.smalthumbnail = fileinfo["thumbnail_images"][1] - self.bigthumbnail = fileinfo["thumbnail_images"][ - -1 - ] # Last 'biggest' element - self.layer_display_button.secondary_text = str(self.total_layers) self.file_metadata = fileinfo + self._load_thumbnails(*fileinfo.get("thumbnail_images", [])) @QtCore.pyqtSlot(name="pause_resume_print") def pause_resume_print(self) -> None: @@ -234,6 +243,8 @@ def on_print_stats_update(self, field: str, value: dict | float | str) -> None: self.total_layers = "?" self.file_metadata.clear() self.hide_request.emit() + self.thumbnail_view.deleteLater() + self.thumbnail_view_layout.deleteLater() if hasattr(events, str("Print" + value.capitalize())): event_obj = getattr(events, str("Print" + value.capitalize())) @@ -247,7 +258,7 @@ def on_print_stats_update(self, field: str, value: dict | float | str) -> None: "QApplication.instance expected non None value" ) except Exception as e: - logging.info( + logger.info( f"Unexpected error while posting print job start event: {e}" ) @@ -280,24 +291,19 @@ def on_print_stats_update(self, field: str, value: dict | float | str) -> None: @QtCore.pyqtSlot(str, list, name="on_gcode_move_update") def on_gcode_move_update(self, field: str, value: list) -> None: """Handle gcode move""" - if isinstance(value, list): - if "gcode_position" in field: # Without offsets - if self._internal_print_status == "printing": - _current_layer = calculate_current_layer( - z_position=value[2], - object_height=float( - self.file_metadata.get("object_height", -1.0) - ), - layer_height=float( - self.file_metadata.get("layer_height", -1.0) - ), - first_layer_height=float( - self.file_metadata.get("first_layer_height", -1.0) - ), - ) - self.layer_display_button.setText( - f"{int(_current_layer)}" if _current_layer != -1 else "?" - ) + if "gcode_position" in field: # Without offsets + if self._internal_print_status == "printing": + _current_layer = calculate_current_layer( + z_position=value[2], + object_height=float(self.file_metadata.get("object_height", -1.0)), + layer_height=float(self.file_metadata.get("layer_height", -1.0)), + first_layer_height=float( + self.file_metadata.get("first_layer_height", -1.0) + ), + ) + self.layer_display_button.setText( + f"{int(_current_layer)}" if _current_layer != -1 else "?" + ) @QtCore.pyqtSlot(str, float, name="virtual_sdcard_update") @QtCore.pyqtSlot(str, bool, name="virtual_sdcard_update") @@ -309,72 +315,11 @@ def virtual_sdcard_update(self, field: str, value: float | bool) -> None: value (float | bool): The updated information for the corresponding field """ if isinstance(value, bool): - self.sdcard_read = value - elif isinstance(value, float): - if "progress" == field: - self.print_progress = value - self.printing_progress_bar.setValue(self.print_progress) - - def paintEvent(self, a0: QtGui.QPaintEvent) -> None: - """Re-implemented method, paint widget""" - _scene = QtWidgets.QGraphicsScene() - if not self.smalthumbnail.isNull(): - _graphics_rect = self.CBVSmallThumbnail.rect().toRectF() - _image_rect = self.smalthumbnail.rect() - - scaled_width = _image_rect.width() - scaled_height = _image_rect.height() - adjusted_x = (_graphics_rect.width() - scaled_width) // 2.0 - adjusted_y = (_graphics_rect.height() - scaled_height) // 2.0 - - adjusted_rect = QtCore.QRectF( - _image_rect.x() + adjusted_x, - _image_rect.y() + adjusted_y, - scaled_width, - scaled_height, - ) - _scene.setSceneRect(adjusted_rect) - _item_scaled = QtWidgets.QGraphicsPixmapItem( - QtGui.QPixmap.fromImage(self.smalthumbnail).scaled( - int(scaled_width), - int(scaled_height), - QtCore.Qt.AspectRatioMode.KeepAspectRatio, - QtCore.Qt.TransformationMode.SmoothTransformation, - ) - ) - _scene.addItem(_item_scaled) - self.CBVSmallThumbnail.setScene(_scene) - - else: - self.request_file_info.emit(self.js_file_name_label.text()) - _scene = QtWidgets.QGraphicsScene() - - if not self.bigthumbnail.isNull(): - _graphics_rect = self.CBVBigThumbnail.rect().toRectF() - _image_rect = self.bigthumbnail.rect() - - scaled_width = _image_rect.width() - scaled_height = _image_rect.height() - adjusted_x = (_graphics_rect.width() - scaled_width) // 2.0 - adjusted_y = (_graphics_rect.height() - scaled_height) // 2.0 - - adjusted_rect = QtCore.QRectF( - _image_rect.x() + adjusted_x, - _image_rect.y() + adjusted_y, - scaled_width, - scaled_height, - ) - _scene.setSceneRect(adjusted_rect) - _item_scaled = QtWidgets.QGraphicsPixmapItem( - QtGui.QPixmap.fromImage(self.bigthumbnail).scaled( - int(scaled_width), - int(scaled_height), - QtCore.Qt.AspectRatioMode.KeepAspectRatio, - QtCore.Qt.TransformationMode.SmoothTransformation, - ) - ) - _scene.addItem(_item_scaled) - self.CBVBigThumbnail.setScene(_scene) + ... + if "progress" == field: + self.printing_progress_bar.setValue(value) + if "file_position" == field: + ... def _setupUI(self) -> None: """Setup widget ui""" @@ -391,73 +336,41 @@ def _setupUI(self) -> None: self.setMinimumSize(QtCore.QSize(710, 420)) self.setMaximumSize(QtCore.QSize(720, 420)) self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - - # ---------------------------------Widgets - self.bigthumb_widget = QtWidgets.QWidget(self) - self.bigthumb_widget.setGeometry( - QtCore.QRect(0, 0, self.width(), self.height()) - ) - self.bigthumb_widget.setObjectName("bigthumb_widget") - self.headerWidget = QtWidgets.QWidget(self) self.headerWidget.setGeometry(QtCore.QRect(11, 11, 691, 62)) self.headerWidget.setObjectName("headerWidget") - self.btnWidget = QtWidgets.QWidget(self) self.btnWidget.setGeometry(QtCore.QRect(10, 80, 691, 90)) self.btnWidget.setObjectName("btnWidget") - self.progressWidget = QtWidgets.QWidget(self) self.progressWidget.setGeometry(QtCore.QRect(10, 170, 471, 241)) self.progressWidget.setObjectName("progressWidget") - self.contentWidget = QtWidgets.QWidget(self) self.contentWidget.setGeometry(QtCore.QRect(480, 170, 221, 241)) self.contentWidget.setObjectName("contentWidget") - - self.smallthumb_widget = QtWidgets.QLabel(self) - self.smallthumb_widget.setGeometry(QtCore.QRect(10, 170, 471, 241)) - self.smallthumb_widget.setObjectName("smallthumb_widget") - - # ---------------------------------layout - - self.smalllayout = QtWidgets.QHBoxLayout(self.smallthumb_widget) - - self.biglayout = QtWidgets.QHBoxLayout(self.bigthumb_widget) - self.job_status_header_layout = QtWidgets.QHBoxLayout(self.headerWidget) self.job_status_header_layout.setSpacing(20) self.job_status_header_layout.setObjectName("job_status_header_layout") - self.job_status_progress_layout = QtWidgets.QVBoxLayout(self.progressWidget) self.job_status_progress_layout.setSizeConstraint( QtWidgets.QLayout.SizeConstraint.SetMinimumSize ) - self.job_status_btn_layout = QtWidgets.QHBoxLayout(self.btnWidget) self.job_status_btn_layout.setSizeConstraint( QtWidgets.QLayout.SizeConstraint.SetMinimumSize ) - self.job_content_layout = QtWidgets.QVBoxLayout(self.contentWidget) self.job_content_layout.setObjectName("job_content_layout") - self.job_status_btn_layout.setContentsMargins(5, 5, 5, 5) self.job_status_btn_layout.setSpacing(5) self.job_status_btn_layout.setObjectName("job_status_btn_layout") - self.job_stats_display_layout = QtWidgets.QVBoxLayout() self.job_stats_display_layout.setObjectName("job_stats_display_layout") - - # -----------------------------Fonts font = QtGui.QFont() font.setFamily("Montserrat") font.setPointSize(14) - # ------------------------------Header - self.js_file_name_icon = BlocksLabel(parent=self) - self.js_file_name_icon.setSizePolicy(sizePolicy) self.js_file_name_icon.setMinimumSize(QtCore.QSize(60, 60)) self.js_file_name_icon.setMaximumSize(QtCore.QSize(60, 60)) @@ -476,19 +389,14 @@ def _setupUI(self) -> None: self.js_file_name_label.setSizePolicy(sizePolicy) self.js_file_name_label.setMinimumSize(QtCore.QSize(200, 80)) self.js_file_name_label.setMaximumSize(QtCore.QSize(16777215, 60)) - self.js_file_name_label.setFont(font) self.js_file_name_label.setStyleSheet("background: transparent; color: white;") self.js_file_name_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.js_file_name_label.setObjectName("js_file_name_label") - self.job_status_header_layout.addWidget(self.js_file_name_icon) self.job_status_header_layout.addWidget(self.js_file_name_label) - # -----------------------------buttons - font.setPointSize(18) - self.pause_printing_btn = BlocksCustomButton(self) self.pause_printing_btn.setSizePolicy(sizePolicy) self.pause_printing_btn.setMinimumSize(QtCore.QSize(200, 80)) @@ -498,108 +406,37 @@ def _setupUI(self) -> None: "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/pause.svg") ) self.pause_printing_btn.setObjectName("pause_printing_btn") - self.stop_printing_btn = BlocksCustomButton(self) self.stop_printing_btn.setSizePolicy(sizePolicy) self.stop_printing_btn.setMinimumSize(QtCore.QSize(200, 80)) self.stop_printing_btn.setMaximumSize(QtCore.QSize(200, 80)) - self.stop_printing_btn.setFont(font) self.stop_printing_btn.setProperty( "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/stop.svg") ) self.stop_printing_btn.setObjectName("stop_printing_btn") - self.tune_menu_btn = BlocksCustomButton(self) self.tune_menu_btn.setSizePolicy(sizePolicy) - self.tune_menu_btn.setMinimumSize(QtCore.QSize(200, 60)) self.tune_menu_btn.setMaximumSize(QtCore.QSize(200, 80)) - self.tune_menu_btn.setFont(font) self.tune_menu_btn.setProperty( "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/tune.svg") ) self.tune_menu_btn.setObjectName("tune_menu_btn") - self.job_status_btn_layout.addWidget(self.pause_printing_btn) self.job_status_btn_layout.addWidget(self.stop_printing_btn) self.job_status_btn_layout.addWidget(self.tune_menu_btn) - self.tune_menu_btn.setText("Tune") self.stop_printing_btn.setText("Cancel") self.pause_printing_btn.setText("Pause") - # -----------------------------Progress bar - - self.printing_progress_bar = CustomProgressBar() + self.printing_progress_bar = CustomProgressBar(self) self.printing_progress_bar.setMinimumHeight(150) - self.printing_progress_bar.setObjectName("printing_progress_bar") self.printing_progress_bar.setSizePolicy(sizePolicy) - self.job_status_progress_layout.addWidget(self.printing_progress_bar) - # -----------------------------SMALL-THUMBNAIL - - self.CBVSmallThumbnail = ClickableGraphicsView(self.smallthumb_widget) - self.CBVSmallThumbnail.setSizePolicy(sizePolicy) - self.CBVSmallThumbnail.setMaximumSize(QtCore.QSize(48, 48)) - self.CBVSmallThumbnail.setStyleSheet( - "QGraphicsView{\nbackground-color:transparent;\n}" - ) - self.CBVSmallThumbnail.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - self.CBVSmallThumbnail.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) - self.CBVSmallThumbnail.setSizeAdjustPolicy( - QtWidgets.QAbstractScrollArea.SizeAdjustPolicy.AdjustIgnored - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) - self.CBVSmallThumbnail.setBackgroundBrush(brush) - self.CBVSmallThumbnail.setRenderHints( - QtGui.QPainter.RenderHint.Antialiasing - | QtGui.QPainter.RenderHint.SmoothPixmapTransform - | QtGui.QPainter.RenderHint.TextAntialiasing - ) - self.CBVSmallThumbnail.setObjectName("CBVSmallThumbnail") - - self.smalllayout.addWidget(self.CBVSmallThumbnail) - - # -----------------------------Big-Thumbnail - self.CBVBigThumbnail = ClickableGraphicsView() - self.CBVBigThumbnail.setSizePolicy(sizePolicy) - self.CBVBigThumbnail.setMaximumSize(QtCore.QSize(300, 300)) - self.CBVBigThumbnail.setStyleSheet( - "QGraphicsView{\nbackground-color:transparent;\n}" - ) - # "QGraphicsView{\nbackground-color:grey;border-radius:10px;\n}" grey background - self.CBVBigThumbnail.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - self.CBVBigThumbnail.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) - self.CBVBigThumbnail.setVerticalScrollBarPolicy( - QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff - ) - self.CBVBigThumbnail.setHorizontalScrollBarPolicy( - QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff - ) - self.CBVBigThumbnail.setSizeAdjustPolicy( - QtWidgets.QAbstractScrollArea.SizeAdjustPolicy.AdjustIgnored - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) - self.CBVBigThumbnail.setBackgroundBrush(brush) - self.CBVBigThumbnail.setRenderHints( - QtGui.QPainter.RenderHint.Antialiasing - | QtGui.QPainter.RenderHint.SmoothPixmapTransform - | QtGui.QPainter.RenderHint.TextAntialiasing - ) - self.CBVBigThumbnail.setViewportUpdateMode( - QtWidgets.QGraphicsView.ViewportUpdateMode.SmartViewportUpdate - ) - - self.CBVBigThumbnail.setObjectName("CBVBigThumbnail") - self.biglayout.addWidget(self.CBVBigThumbnail) - self.bigthumb_widget.hide() - # -----------------------------display buttons self.layer_display_button = DisplayButton(self) @@ -638,3 +475,39 @@ def _setupUI(self) -> None: QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) self.job_content_layout.addLayout(self.job_stats_display_layout) + + def create_thumbnail_widget(self) -> None: + """Create thumbnail graphics view widget""" + self.thumbnail_view = QtWidgets.QGraphicsView() + self.thumbnail_view.setMinimumSize(QtCore.QSize(48, 48)) + # self.thumbnail_view.setMaximumSize(QtCore.QSize(300, 300)) + self.thumbnail_view.setAttribute( + QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True + ) + self.thumbnail_view.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + self.thumbnail_view.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) + self.thumbnail_view.setWindowFlags(QtCore.Qt.WindowType.FramelessWindowHint) + self.thumbnail_view.setObjectName("thumbnail_scene") + _thumbnail_palette = QtGui.QPalette() + _thumbnail_palette.setColor( + QtGui.QPalette.ColorRole.Window, QtGui.QColor(0, 0, 0, 0) + ) + _thumbnail_palette.setColor( + QtGui.QPalette.ColorRole.Base, QtGui.QColor(0, 0, 0, 0) + ) + self.thumbnail_view.setPalette(_thumbnail_palette) + _thumbnail_brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) + _thumbnail_brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) + self.thumbnail_view.setBackgroundBrush(_thumbnail_brush) + self.thumbnail_view.setRenderHints( + QtGui.QPainter.RenderHint.Antialiasing + | QtGui.QPainter.RenderHint.SmoothPixmapTransform + | QtGui.QPainter.RenderHint.LosslessImageRendering + ) + self.thumbnail_view.setViewportUpdateMode( + QtWidgets.QGraphicsView.ViewportUpdateMode.SmartViewportUpdate + ) + self.thumbnail_view.setObjectName("thumbnail_scene") + self.thumbnail_view_layout = QtWidgets.QHBoxLayout(self) + self.thumbnail_view_layout.addWidget(self.thumbnail_view) + self.thumbnail_view.hide() diff --git a/BlocksScreen/lib/utils/blocks_progressbar.py b/BlocksScreen/lib/utils/blocks_progressbar.py index 98f7cc52..a05aafa5 100644 --- a/BlocksScreen/lib/utils/blocks_progressbar.py +++ b/BlocksScreen/lib/utils/blocks_progressbar.py @@ -1,92 +1,192 @@ +import typing from PyQt6 import QtWidgets, QtGui, QtCore class CustomProgressBar(QtWidgets.QProgressBar): + """Custom circular progress bar for tracking print jobs + + Args: + QtWidgets (QtWidget): Parent widget + + Raises: + ValueError: Thrown when setting progress is not between 0.0 and 1.0 + ValueError: Thrown when setting bar color is not between 0 and 255. + + """ + + thumbnail_clicked: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + name="thumbnail-clicked" + ) + def __init__(self, parent=None): super().__init__(parent) self.progress_value = 0 - self.bar_color = QtGui.QColor(223, 223, 223) + self._pen_width = 20 + self._padding = 50 + self._pixmap: QtGui.QPixmap = QtGui.QPixmap() + self._pixmap_cached: QtGui.QPixmap = QtGui.QPixmap() + self._pixmap_dirty: bool = True + self._bar_color = QtGui.QColor(223, 223, 223) self.setMinimumSize(100, 100) - self.set_padding(50) - self.set_pen_width(20) + self._inner_rect: QtCore.QRectF = QtCore.QRectF() - def set_padding(self, value): + def set_padding(self, value) -> None: """Set widget padding""" - self.padding = value + self._padding = value self.update() - def set_pen_width(self, value): + def set_pen_width(self, value) -> None: """Set widget text pen width""" - self.pen_width = value + self._pen_width = value self.update() - def paintEvent(self, event): - """Re-implemented method, paint widget""" - painter = QtGui.QPainter(self) - painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) + def set_inner_pixmap(self, pixmap: QtGui.QPixmap) -> None: + """Set the inner icon pixmap on the progress bar + circumference. + """ + self._pixmap = pixmap + self.update() - self._draw_circular_bar(painter, self.width(), self.height()) + def resizeEvent(self, a0) -> None: + """Re-implemented method, handle widget resize Events + + Currently rescales the set pixmap so it has the optimal + size. + """ + self._inner_rect = self._calculate_inner_geometry() + self._pixmap_cached = self._pixmap.scaled( + self._inner_rect.size().toSize(), + QtCore.Qt.AspectRatioMode.KeepAspectRatio, + QtCore.Qt.TransformationMode.SmoothTransformation, + ) + self.update() + return super().resizeEvent(a0) + + def sizeHint(self) -> QtCore.QSize: + """Re-implemented method, preferable widget size""" + self._inner_rect = self._calculate_inner_geometry() + return QtCore.QSize(100, 100) + + def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: + """Re-implemented method, check if thumbnail was clicked, + filter clicks inside inner section of the widget, + if a mouse event happens there we know that the thumbnail + was pressed. + """ + if self._inner_rect.contains(a0.pos().x(), a0.pos().y()): + self.thumbnail_clicked.emit() + return super().mousePressEvent(a0) + + def minimumSizeHint(self) -> QtCore.QSize: + """Re-implemented method, minimum widget size""" + self._inner_rect = self._calculate_inner_geometry() + return QtCore.QSize(100, 100) + + def setValue(self, value: float) -> None: + """Set progress value + + Args: + value (float): Progress value between 0.0 and 1.0 + + Raises: + ValueError: If provided value in not between 0.0 and 1.0 + """ + if not (0 <= value <= 100): + raise ValueError("Argument `value` expected value between 0.0 and 1.0 ") + value *= 100 + self.progress_value = value + self.update() - def _draw_circular_bar(self, painter, width, height): - size = min(width, height) - (self.padding * 1.3) - x = (width - size) / 2 - y = (height - size) / 2 - arc_rect = QtCore.QRectF(x, y, size, size) + def set_bar_color(self, red: int, green: int, blue: int) -> None: + """Set widget progress bar color + + Args: + red (int): red component value between 0 and 255 + green (int): green component value between 0 and 255 + blue (int): blue component value between 0 and 255 + + Raises: + ValueError: Raised if any provided argument value is not between 0 and 255 + """ + if not (0 <= red <= 255 and 0 <= green <= 255 and 0 <= blue <= 255): + raise ValueError("Color values must be between 0 and 255.") + self._bar_color = QtGui.QColor(red, green, blue) + self.update() + + def _calculate_inner_geometry(self) -> QtCore.QRectF: + size = min(self.width(), self.height()) - (self._padding * 1.3) + x = (self.width() - size) // 2 + y = (self.height() - size) // 2 + return QtCore.QRectF( + x + self._pen_width // 2, + y + self._pen_width // 2, + size - self._pen_width, + size - self._pen_width, + ) - arc1_start = 236 * 16 - arc1_span = -290 * 16 + def _draw_cached_pixmap( + self, painter: QtGui.QPainter, pixmap: QtGui.QPixmap, inner_rect: QtCore.QRectF + ) -> None: + """Internal method draw already scaled pixmap on the widget inner section""" + if pixmap.isNull(): + return + scaled_width = pixmap.width() + scaled_height = pixmap.height() + adjusted_x = (inner_rect.width() - scaled_width) // 2.0 + adjusted_y = (inner_rect.height() - scaled_height) // 2.0 + adjusted_icon = QtCore.QRectF( + inner_rect.x() + adjusted_x, + inner_rect.y() + adjusted_y, + scaled_width, + scaled_height, + ) + painter.drawPixmap(adjusted_icon, pixmap, pixmap.rect().toRectF()) + + def _draw_circular_bar( + self, + painter: QtGui.QPainter, + ) -> None: + size = min(self.width(), self.height()) - (self._padding * 1.3) + x = (self.width() - size) / 2 + y = (self.height() - size) / 2 + arc_rect = QtCore.QRectF(x, y, size, size) + arc_start = 236 * 16 + arc_span = -290 * 16 bg_pen = QtGui.QPen(QtGui.QColor(20, 20, 20)) - bg_pen.setWidth(self.pen_width) + bg_pen.setWidth(self._pen_width) bg_pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap) painter.setPen(bg_pen) - painter.drawArc(arc_rect, arc1_start, arc1_span) - + painter.drawArc(arc_rect, arc_start, arc_span) if self.progress_value is not None: gradient = QtGui.QConicalGradient(arc_rect.center(), -90) - gradient.setColorAt(0.0, self.bar_color) + gradient.setColorAt(0.0, self._bar_color) gradient.setColorAt(1.0, QtGui.QColor(100, 100, 100)) - progress_pen = QtGui.QPen() - progress_pen.setWidth(self.pen_width) + progress_pen.setWidth(self._pen_width) progress_pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap) progress_pen.setBrush(QtGui.QBrush(gradient)) painter.setPen(progress_pen) - - # scale only over arc1’s span - progress_span = int(arc1_span * self.progress_value / 100) - painter.drawArc(arc_rect, arc1_start, progress_span) - + # scale only over arc span + progress_span = int(arc_span * self.progress_value / 100) + painter.drawArc(arc_rect, arc_start, progress_span) progress_text = f"{int(self.progress_value)}%" painter.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0))) font = painter.font() font.setPointSize(16) - bg_pen = QtGui.QPen(QtGui.QColor(255, 255, 255)) painter.setPen(bg_pen) painter.setFont(font) - text_x = arc_rect.center().x() text_y = arc_rect.center().y() - - # Draw centered text text_rect = QtCore.QRectF( text_x - 30, text_y + arc_rect.height() / 2 - 25, 60, 40 ) painter.drawText(text_rect, QtCore.Qt.AlignmentFlag.AlignCenter, progress_text) - def setValue(self, value): - """Set value""" - value *= 100 - if 0 <= value <= 101: - self.progress_value = value - self.update() - else: - raise ValueError("Progress must be between 0.0 and 1.0.") - - def set_bar_color(self, red, green, blue): - """Set bar color""" - if 0 <= red <= 255 and 0 <= green <= 255 and 0 <= blue <= 255: - self.bar_color = QtGui.QColor(red, green, blue) - self.update() - else: - raise ValueError("Color values must be between 0 and 255.") + def paintEvent(self, _) -> None: + """Re-implemented method, paint widget""" + painter = QtGui.QPainter(self) + painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) + self._draw_circular_bar(painter) + self._draw_cached_pixmap(painter, self._pixmap_cached, self._inner_rect) + painter.end()