ScanSlicer/scanslicer/main.py
2026-02-24 12:46:56 +00:00

463 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
# _unwrap_qvariant: function definition for ScanSlicer behavior.
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
# _read_portal_dark_preference: function definition for ScanSlicer behavior.
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
# apply_dark_theme_if_requested: function definition for ScanSlicer behavior.
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)
# apply_global_text_size_offset: function definition for ScanSlicer behavior.
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)
# resolve_app_icon: function definition for ScanSlicer behavior.
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()
# QtScanSlicer: class definition for ScanSlicer behavior.
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=="
)
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+"
)
# __init__: function definition for ScanSlicer behavior.
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)
# _build_header_controls: function definition for ScanSlicer behavior.
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
# toggle_maximize_restore: function definition for ScanSlicer behavior.
def toggle_maximize_restore(self):
if self.isMaximized():
self.showNormal()
return
self.showMaximized()
# closeEvent: function definition for ScanSlicer behavior.
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)
# run_app: function definition for ScanSlicer behavior.
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()