import os import sys from PySide6.QtCore import Qt from PySide6.QtGui import QColor, QFont, QFontInfo, QIcon, QPalette from PySide6.QtWidgets import ( QApplication, QGridLayout, QHBoxLayout, QLabel, QMainWindow, QPushButton, QWidget, ) from .autoslicer import Autoslicer, AutoslicerParams from .controller import ScanSlicerControllerMixin from .ui_builders import ScanSlicerUiBuilderMixin def _unwrap_qvariant(value): unwrapped = value for _ in range(4): if hasattr(unwrapped, "variant"): try: candidate = unwrapped.variant() except Exception: candidate = unwrapped if candidate is not unwrapped: unwrapped = candidate continue if hasattr(unwrapped, "value"): try: candidate = unwrapped.value() except TypeError: candidate = unwrapped.value except Exception: candidate = unwrapped if candidate is not unwrapped: unwrapped = candidate continue break return unwrapped def _read_portal_dark_preference(): try: from PySide6.QtDBus import QDBusConnection, QDBusInterface, QDBusMessage except Exception: return None bus = QDBusConnection.sessionBus() if not bus.isConnected(): return None iface = QDBusInterface( "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.Settings", bus, ) if not iface.isValid(): return None for method in ("ReadOne", "Read"): reply = iface.call(method, "org.freedesktop.appearance", "color-scheme") if reply.type() == QDBusMessage.MessageType.ErrorMessage: continue args = reply.arguments() if not args: continue raw_value = _unwrap_qvariant(args[0]) if isinstance(raw_value, dict) and "color-scheme" in raw_value: raw_value = _unwrap_qvariant(raw_value["color-scheme"]) if isinstance(raw_value, (list, tuple)) and raw_value: raw_value = _unwrap_qvariant(raw_value[0]) try: color_scheme = int(raw_value) except Exception: continue # XDG portal color-scheme values: # 0 = no preference, 1 = prefer dark, 2 = prefer light if color_scheme == 1: return True if color_scheme == 2: return False return None def apply_dark_theme_if_requested(app): theme_value = os.environ.get("GTK_THEME", "").lower() dark_theme_requested = "dark" in theme_value if not dark_theme_requested: portal_pref = _read_portal_dark_preference() if portal_pref is not None: dark_theme_requested = portal_pref if not dark_theme_requested: try: dark_theme_requested = app.styleHints().colorScheme() == Qt.ColorScheme.Dark except Exception: dark_theme_requested = False if not dark_theme_requested: return # Some container/desktop combinations do not propagate GTK dark theme # correctly to Qt widgets; enforce a dark palette when dark mode is requested. app.setStyle("Fusion") palette = QPalette() palette.setColor(QPalette.Window, QColor(53, 53, 53)) palette.setColor(QPalette.WindowText, QColor(255, 255, 255)) palette.setColor(QPalette.Base, QColor(35, 35, 35)) palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53)) palette.setColor(QPalette.ToolTipBase, QColor(35, 35, 35)) palette.setColor(QPalette.ToolTipText, QColor(255, 255, 255)) palette.setColor(QPalette.Text, QColor(255, 255, 255)) palette.setColor(QPalette.Button, QColor(53, 53, 53)) palette.setColor(QPalette.ButtonText, QColor(255, 255, 255)) palette.setColor(QPalette.BrightText, QColor(255, 0, 0)) palette.setColor(QPalette.Highlight, QColor(42, 130, 218)) palette.setColor(QPalette.HighlightedText, QColor(255, 255, 255)) app.setPalette(palette) def apply_global_text_size_offset(app, pixel_offset=2): font = app.font() info = QFontInfo(font) pixel_size = info.pixelSize() if pixel_size > 0: font.setPixelSize(pixel_size + pixel_offset) else: point_size = info.pointSizeF() if point_size <= 0: point_size = 10.0 font.setPointSizeF(point_size + (pixel_offset * 0.75)) app.setFont(font) def resolve_app_icon(): icon = QIcon.fromTheme("scanslicer") if not icon.isNull(): return icon appdir = os.environ.get("APPDIR", "").strip() if not appdir: return QIcon() candidates = ( os.path.join(appdir, "usr", "share", "icons", "scanslicer.png"), os.path.join(appdir, "usr", "share", "icons", "scanslicer.svg"), os.path.join(appdir, "usr", "share", "pixmaps", "scanslicer.png"), os.path.join(appdir, "usr", "share", "pixmaps", "scanslicer.svg"), os.path.join(appdir, ".DirIcon"), ) for candidate_path in candidates: if not os.path.exists(candidate_path): continue candidate_icon = QIcon(candidate_path) if not candidate_icon.isNull(): return candidate_icon return QIcon() class QtScanSlicer(QMainWindow, ScanSlicerUiBuilderMixin, ScanSlicerControllerMixin): CONTROL_PANEL_WIDTH = 370 STEPPER_WIDTH = 75 PANEL_CONTENT_MARGIN = 10 VERTICAL_SPACER_HEIGHT = 8 SECTION_TITLE_FONT_PX = 18 SECTION_TITLE_VPAD = 5 CHOICE_PARAM_OPTIONS = { "detect_pipeline": [("Threshold", 0), ("Canny", 1)], "bw_method": [("Simple", 0), ("Gauss", 1), ("Otsu", 2)], "invert_mode": [("Auto", 0), ("Off", 1), ("On", 2)], "morph_op": [("None", 0), ("Dilate", 1), ("Erode", 2), ("Open", 3), ("Close", 4)], } ADVANCED_PARAMS = { "detect_pipeline", "clahe_clip", "gaussian", "bw_adaptive_c", "invert_mode", "contour_approx_pct", "bbox_max_size", "aspect_ratio_min", "aspect_ratio_max", "border_ignore", "iou_merge_thresh", } LOCK_ICON_DATA_URL = ( "data:image/svg+xml;base64," "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" "NpZGUgbHVjaWRlLWxvY2staWNvbiBsdWNpZGUtbG9jayI+PHJlY3Qgd2lkdGg9IjE4IiBoZWlnaHQ9IjExIiB4" "PSIzIiB5PSIxMSIgcng9IjIiIHJ5PSIyIi8+PHBhdGggZD0iTTcgMTFWN2E1IDUgMCAwIDEgMTAgMHY0Ii8+PC" "9zdmc+" ) SQUARIZE_ICON_DATA_URL = ( "data:image/svg+xml;base64," "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" "NpZGUgbHVjaWRlLXNxdWFyZS1pY29uIGx1Y2lkZS1zcXVhcmUiPjxyZWN0IHdpZHRoPSIxOCIgaGVpZ2h0PSIx" "OCIgeD0iMyIgeT0iMyIgcng9IjIiLz48L3N2Zz4=" ) ROTATE_ICON_DATA_URL = ( "data:image/svg+xml;base64," "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" "NpZGUgbHVjaWRlLXJlZnJlc2gtY2N3LWRvdC1pY29uIGx1Y2lkZS1yZWZyZXNoLWNjdy1kb3QiPjxwYXRoIGQ9" "Ik0yMSAxMmE5IDkgMCAwIDAtOS05IDkuNzUgOS43NSAwIDAgMC02Ljc0IDIuNzRMMyA4Ii8+PHBhdGggZD0i" "TTMgM3Y1aDUiLz48cGF0aCBkPSJNMyAxMmE5IDkgMCAwIDAgOSA5IDkuNzUgOS43NSAwIDAgMCA2Ljc0LTIu" "NzRMMjEgMTYiLz48cGF0aCBkPSJNMTYgMTZoNXY1Ii8+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMSIv" "Pjwvc3ZnPg==" ) SWAP_LR_ICON_DATA_URL = ( "data:image/svg+xml;base64," "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9" "Ik04IDMgNCA3bDQgNCIvPjxwYXRoIGQ9Ik00IDdoMTYiLz48cGF0aCBkPSJtMTYgMjEgNC00LTQtNCIvPjxw" "YXRoIGQ9Ik0yMCAxN0g0Ii8+PC9zdmc+" ) RESET_ROTATION_ICON_DATA_URL = ( "data:image/svg+xml;base64," "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" "NpZGUgbHVjaWRlLXJvdGF0ZS1jdy1zcXVhcmUtaWNvbiBsdWNpZGUtcm90YXRlLWN3LXNxdWFyZSI+PHBhdGgg" "ZD0iTTEyIDVINmEyIDIgMCAwIDAtMiAydjMiLz48cGF0aCBkPSJtOSA4IDMtMy0zLTMiLz48cGF0aCBkPSJNNC" "AxNHY0YTIgMiAwIDAgMCAyIDJoMTJhMiAyIDAgMCAwIDItMlY3YTIgMiAwIDAgMC0yLTJoLTIiLz48L3N2Zz4=" ) REMOVE_ICON_DATA_URL = ( "data:image/svg+xml;base64," "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" "NpZGUgbHVjaWRlLXNxdWFyZS14LWljb24gbHVjaWRlLXNxdWFyZS14Ij48cmVjdCB3aWR0aD0iMTgiIGhlaWdo" "dD0iMTgiIHg9IjMiIHk9IjMiIHJ4PSIyIiByeT0iMiIvPjxwYXRoIGQ9Im0xNSA5LTYgNiIvPjxwYXRoIGQ9" "Im05IDkgNiA2Ii8+PC9zdmc+" ) LOCK_ALL_ICON_DATA_URL = ( "data:image/svg+xml;base64," "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" "NpZGUgbHVjaWRlLWxvY2sta2V5aG9sZS1pY29uIGx1Y2lkZS1sb2NrLWtleWhvbGUiPjxjaXJjbGUgY3g9IjEy" "IiBjeT0iMTYiIHI9IjEiLz48cmVjdCB4PSIzIiB5PSIxMCIgd2lkdGg9IjE4IiBoZWlnaHQ9IjEyIiByeD0i" "MiIvPjxwYXRoIGQ9Ik03IDEwVjdhNSA1IDAgMCAxIDEwIDB2MyIvPjwvc3ZnPg==" ) DETECT_ICON_DATA_URL = ( "data:image/svg+xml;base64," "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" "NpZGUgbHVjaWRlLWV5ZS1pY29uIGx1Y2lkZS1leWUiPjxwYXRoIGQ9Ik0yLjA2MiAxMi4zNDhhMSAxIDAgMCAx" "IDAtLjY5NiAxMC43NSAxMC43NSAwIDAgMSAxOS44NzYgMCAxIDEgMCAwIDEgMCAuNjk2IDEwLjc1IDEwLjc1ID" "AgMCAxLTE5Ljg3NiAwIi8+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyIvPjwvc3ZnPg==" ) PREVIEW_FILTER_ICON_DATA_URL = ( "data:image/svg+xml;base64," "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" "NpZGUgbHVjaWRlLWV5ZS1vZmYtaWNvbiBsdWNpZGUtZXllLW9mZiI+PHBhdGggZD0iTTEwLjczMyA1LjA3NmEx" "MC43NDQgMTAuNzQ0IDAgMCAxIDExLjIwNSA2LjU3NSAxIDEgMCAwIDEgMCAuNjk2IDEwLjc0NyAxMC43NDcgMC" "AwIDEtMS40NDQgMi40OSIvPjxwYXRoIGQ9Ik0xNC4wODQgMTQuMTU4YTMgMyAwIDAgMS00LjI0Mi00LjI0MiIv" "PjxwYXRoIGQ9Ik0xNy40NzkgMTcuNDk5YTEwLjc1IDEwLjc1IDAgMCAxLTE1LjQxNy01LjE1MSAxIDEgMCAwID" "EgMC0uNjk2IDEwLjc1IDEwLjc1IDAgMCAxIDQuNDQ2LTUuMTQzIi8+PHBhdGggZD0ibTIgMiAyMCAyMCIvPjwv" "c3ZnPg==" ) ADD_BOX_ICON_DATA_URL = ( "data:image/svg+xml;base64," "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" "NpZGUgbHVjaWRlLXNxdWFyZS1wbHVzLWljb24gbHVjaWRlLXNxdWFyZS1wbHVzIj48cmVjdCB3aWR0aD0iMTgi" "IGhlaWdodD0iMTgiIHg9IjMiIHk9IjMiIHJ4PSIyIi8+PHBhdGggZD0iTTggMTJoOCIvPjxwYXRoIGQ9Ik0x" "MiA4djgiLz48L3N2Zz4=" ) RESET_VIEW_ICON_DATA_URL = ( "data:image/svg+xml;base64," "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" "NpZGUgbHVjaWRlLXZpZXctaWNvbiBsdWNpZGUtdmlldyI+PHBhdGggZD0iTTIxIDE3djJhMiAyIDAgMCAxLTIg" "Mkg1YTIgMiAwIDAgMS0yLTJ2LTIiLz48cGF0aCBkPSJNMjEgN1Y1YTIgMiAwIDAgMC0yLTJINWEyIDIgMCAwID" "AtMiAydjIiLz48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxIi8+PHBhdGggZD0iTTE4Ljk0NCAxMi4zM2Ex" "IDEgMCAwIDAgMC0uNjYgNy41IDcuNSAwIDAgMC0xMy44ODggMCAxIDEgMCAwIDAgMCAuNjYgNy41IDcuNSAwID" "AgMCAxMy44ODggMCIvPjwvc3ZnPg==" ) PREV_IMAGE_ICON_DATA_URL = ( "data:image/svg+xml;base64," "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" "NpZGUgbHVjaWRlLWNpcmNsZS1hcnJvdy1sZWZ0LWljb24gbHVjaWRlLWNpcmNsZS1hcnJvdy1sZWZ0Ij48Y2ly" "Y2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxMCIvPjxwYXRoIGQ9Im0xMiA4LTQgNCA0IDQiLz48cGF0aCBkPSJN" "MTYgMTJIOCIvPjwvc3ZnPg==" ) NEXT_IMAGE_ICON_DATA_URL = ( "data:image/svg+xml;base64," "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg" "dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp" "ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW" "NpZGUgbHVjaWRlLWNpcmNsZS1hcnJvdy1yaWdodC1pY29uIGx1Y2lkZS1jaXJjbGUtYXJyb3ctcmlnaHQiPjxj" "aXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIi8+PHBhdGggZD0ibTEyIDE2IDQtNC00LTQiLz48cGF0aCBk" "PSJNOCAxMmg4Ii8+PC9zdmc+" ) def __init__(self, startup_dir=None): super().__init__() self.setWindowTitle("ScanSlicer") app_icon = resolve_app_icon() if not app_icon.isNull(): self.setWindowIcon(app_icon) self.setStyleSheet( "QPushButton { padding: 5px; }" "QSpinBox { padding: 3px; }" ) self.source_images = [] self.source_index = None self.input_dir = None self.output_dir = None self.save_format = "png" self.jpg_quality = 95 self.png_compression = 3 self.sort_mode = "name" self.preview_filter_enabled = False self.last_detected_boxes = None self.last_preview_normal_image = None self.last_preview_filter_image = None self.params = AutoslicerParams() self.autoslicer = Autoslicer(self.params) self.autotune_profile = self.autoslicer.default_autotune_profile_name() self.param_controls = {} self.param_blocks = {} self.show_advanced_params = False self.updating_param_controls = False self.processing_busy = False self.controls_scroll = None self.preview_toolbar = None self.status_log_path = None self.status_logging_enabled = True self._status_last_event_pump_ts = 0.0 self._is_closing = False self._pending_detection_new_image = False self._worker_pool = None self._detection_task = None self._autotune_task = None self._detection_running = False self._autotune_running = False self._detection_task_id = 0 self._autotune_task_id = 0 self._active_detection_task_id = None self._active_autotune_task_id = None root = QWidget(self) self.setCentralWidget(root) layout = QGridLayout(root) header = self._build_header_controls(root) layout.addWidget(header, 0, 0, 1, 2) self.controls_scroll = self._build_controls_panel(root) preview_panel = self._build_preview_panel(root) layout.addWidget(self.controls_scroll, 1, 0) layout.addWidget(preview_panel, 1, 1) layout.setColumnStretch(1, 1) layout.setRowStretch(1, 1) self._build_status_bar() self.initialize_status_log() self.update_statusbar("Ready.") self.update_quality_controls_state() self.update_parameter_visibility() self.update_output_dir_label() self.refresh_active_box_toolbar() if startup_dir: self.open_directory(startup_dir) def _build_header_controls(self, parent): header = QWidget(parent) header_layout = QHBoxLayout(header) header_layout.setContentsMargins(0, 0, 0, 0) header_layout.setSpacing(6) title_label = QLabel("ScanSlicer") title_font = QFont() title_font.setPointSize(16) title_font.setBold(True) title_label.setFont(title_font) title_label.setAlignment(Qt.AlignCenter) header_layout.addStretch(1) header_layout.addWidget(title_label) header_layout.addStretch(1) self.button_minimize = QPushButton("−", header) self.button_minimize.setToolTip("Minimize") self.button_minimize.clicked.connect(self.showMinimized) header_layout.addWidget(self.button_minimize) self.button_maximize = QPushButton("□", header) self.button_maximize.setToolTip("Maximize/Restore") self.button_maximize.clicked.connect(self.toggle_maximize_restore) header_layout.addWidget(self.button_maximize) self.button_quit = QPushButton("✕", header) self.button_quit.setToolTip("Quit") self.button_quit.clicked.connect(self.close) header_layout.addWidget(self.button_quit) return header def toggle_maximize_restore(self): if self.isMaximized(): self.showNormal() return self.showMaximized() def closeEvent(self, event): self._is_closing = True if hasattr(self, "autoslicer"): self.autoslicer.abort_operation() if hasattr(self, "_close_status_log"): self._close_status_log() super().closeEvent(event) def run_app(startup_dir=None): app = QApplication.instance() if app is None: app = QApplication(sys.argv) apply_dark_theme_if_requested(app) apply_global_text_size_offset(app, 2) app.setApplicationName("ScanSlicer") app.setApplicationDisplayName("ScanSlicer") app.setDesktopFileName("scanslicer") app_icon = resolve_app_icon() if not app_icon.isNull(): app.setWindowIcon(app_icon) scan_slicer_window = QtScanSlicer(startup_dir) scan_slicer_window.showMaximized() return app.exec()