463 lines
19 KiB
Python
463 lines
19 KiB
Python
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()
|