ScanSlicer/scanslicer/main.py

461 lines
19 KiB
Python
Raw Permalink Normal View History

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)
2026-02-24 12:46:56 +00:00
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()