ScanSlicer/scanslicer/main.py
dan 426a18f342 chore: prune boilerplate and refresh stale docs
- Remove 257 redundant per-definition comment stubs across the package
- Drop dead nonlocal declarations in autoslicer_detection.py
- Document the swapped flag and swap_left_right in architecture docs
- Remove unsupported 'legacy override' claims from autotune/runtime docs
2026-09-11 23:07:30 +02:00

460 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()