Reintroduce translations and theme handling
This commit is contained in:
parent
91bdf37512
commit
213b35bf20
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
|||
import colorsys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Tuple
|
||||
from typing import Dict, Iterable, Tuple
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
from PySide6 import QtGui
|
||||
|
|
@ -20,15 +20,12 @@ class Stats:
|
|||
matches_keep: int = 0
|
||||
total_keep: int = 0
|
||||
|
||||
def summary(self) -> str:
|
||||
def summary(self, translate) -> str:
|
||||
if self.total_all == 0:
|
||||
return "Matches with exclusions: —"
|
||||
return translate("stats.placeholder")
|
||||
with_pct = (self.matches_keep / self.total_keep * 100) if self.total_keep else 0.0
|
||||
without_pct = (self.matches_all / self.total_all * 100) if self.total_all else 0.0
|
||||
return (
|
||||
f"Matches with exclusions: {with_pct:.1f}% · "
|
||||
f"Matches overall: {without_pct:.1f}%"
|
||||
)
|
||||
return translate("stats.summary", with_pct=with_pct, without_pct=without_pct, excluded_pct=0.0, excluded_match_pct=0.0)
|
||||
|
||||
|
||||
class QtImageProcessor:
|
||||
|
|
@ -42,16 +39,30 @@ class QtImageProcessor:
|
|||
self.current_index: int = -1
|
||||
self.stats = Stats()
|
||||
|
||||
# HSV thresholds and overlay alpha
|
||||
self.hue_min = 0
|
||||
self.hue_max = 360
|
||||
self.sat_min = 25
|
||||
self.val_min = 15
|
||||
self.val_max = 100
|
||||
self.alpha = 120
|
||||
self.defaults: Dict[str, int] = {
|
||||
"hue_min": 0,
|
||||
"hue_max": 360,
|
||||
"sat_min": 25,
|
||||
"val_min": 15,
|
||||
"val_max": 100,
|
||||
"alpha": 120,
|
||||
}
|
||||
self.hue_min = self.defaults["hue_min"]
|
||||
self.hue_max = self.defaults["hue_max"]
|
||||
self.sat_min = self.defaults["sat_min"]
|
||||
self.val_min = self.defaults["val_min"]
|
||||
self.val_max = self.defaults["val_max"]
|
||||
self.alpha = self.defaults["alpha"]
|
||||
|
||||
self.exclude_shapes: list[dict[str, object]] = []
|
||||
|
||||
def set_defaults(self, defaults: dict) -> None:
|
||||
for key in self.defaults:
|
||||
if key in defaults:
|
||||
self.defaults[key] = int(defaults[key])
|
||||
for key, value in self.defaults.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
# thresholds -------------------------------------------------------------
|
||||
|
||||
def set_threshold(self, key: str, value: int) -> None:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Main PySide6 window emulating the legacy Tk interface."""
|
||||
"""Main PySide6 window emulating the legacy Tk interface with translations and themes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -7,33 +7,62 @@ from typing import Callable, Dict, List, Tuple
|
|||
|
||||
from PySide6 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from app.i18n import I18nMixin
|
||||
from app.logic import SUPPORTED_IMAGE_EXTENSIONS
|
||||
from .image_processor import QtImageProcessor
|
||||
|
||||
DEFAULT_COLOUR = "#763e92"
|
||||
|
||||
PRESET_COLOURS: List[Tuple[str, str]] = [
|
||||
("Red", "#ff3b30"),
|
||||
("Orange", "#ff9500"),
|
||||
("Yellow", "#ffd60a"),
|
||||
("Green", "#34c759"),
|
||||
("Teal", "#5ac8fa"),
|
||||
("Blue", "#0a84ff"),
|
||||
("Violet", "#af52de"),
|
||||
("Magenta", "#ff2d55"),
|
||||
("White", "#ffffff"),
|
||||
("Grey", "#8e8e93"),
|
||||
("Black", "#000000"),
|
||||
("palette.swatch.red", "#ff3b30"),
|
||||
("palette.swatch.orange", "#ff9500"),
|
||||
("palette.swatch.yellow", "#ffd60a"),
|
||||
("palette.swatch.green", "#34c759"),
|
||||
("palette.swatch.teal", "#5ac8fa"),
|
||||
("palette.swatch.blue", "#0a84ff"),
|
||||
("palette.swatch.violet", "#af52de"),
|
||||
("palette.swatch.magenta", "#ff2d55"),
|
||||
("palette.swatch.white", "#ffffff"),
|
||||
("palette.swatch.grey", "#8e8e93"),
|
||||
("palette.swatch.black", "#000000"),
|
||||
]
|
||||
|
||||
SLIDER_SPECS: List[Tuple[str, str, int, int, int]] = [
|
||||
("Hue min", "hue_min", 0, 360, 0),
|
||||
("Hue max", "hue_max", 0, 360, 360),
|
||||
("Sat min", "sat_min", 0, 100, 25),
|
||||
("Value min", "val_min", 0, 100, 15),
|
||||
("Value max", "val_max", 0, 100, 100),
|
||||
("Overlay alpha", "alpha", 0, 255, 120),
|
||||
SLIDER_SPECS: List[Tuple[str, str, int, int]] = [
|
||||
("sliders.hue_min", "hue_min", 0, 360),
|
||||
("sliders.hue_max", "hue_max", 0, 360),
|
||||
("sliders.sat_min", "sat_min", 0, 100),
|
||||
("sliders.val_min", "val_min", 0, 100),
|
||||
("sliders.val_max", "val_max", 0, 100),
|
||||
("sliders.alpha", "alpha", 0, 255),
|
||||
]
|
||||
|
||||
THEMES: Dict[str, Dict[str, str]] = {
|
||||
"dark": {
|
||||
"window_bg": "#111216",
|
||||
"panel_bg": "#16171d",
|
||||
"text": "#f7f7fb",
|
||||
"text_muted": "rgba(255,255,255,0.68)",
|
||||
"text_dim": "rgba(255,255,255,0.45)",
|
||||
"accent": "#5168ff",
|
||||
"accent_secondary": "#9a4dff",
|
||||
"titlebar_bg": "#16171d",
|
||||
"border": "rgba(255,255,255,0.08)",
|
||||
"highlight": "#e6b84b",
|
||||
},
|
||||
"light": {
|
||||
"window_bg": "#f3f4fb",
|
||||
"panel_bg": "#ffffff",
|
||||
"text": "#1d1e24",
|
||||
"text_muted": "rgba(29,30,36,0.78)",
|
||||
"text_dim": "rgba(29,30,36,0.55)",
|
||||
"accent": "#5168ff",
|
||||
"accent_secondary": "#9a4dff",
|
||||
"titlebar_bg": "#e9ebf5",
|
||||
"border": "rgba(29,30,36,0.12)",
|
||||
"highlight": "#c56217",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class ToolbarButton(QtWidgets.QPushButton):
|
||||
"""Rounded toolbar button inspired by the legacy design."""
|
||||
|
|
@ -62,6 +91,26 @@ class ToolbarButton(QtWidgets.QPushButton):
|
|||
)
|
||||
self.clicked.connect(callback)
|
||||
|
||||
def apply_theme(self, colours: Dict[str, str]) -> None:
|
||||
self.setStyleSheet(
|
||||
f"""
|
||||
QPushButton {{
|
||||
padding: 8px 16px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid {colours['border']};
|
||||
background-color: rgba(255, 255, 255, 0.04);
|
||||
color: {colours['text']};
|
||||
font-weight: 600;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
}}
|
||||
QPushButton:pressed {{
|
||||
background-color: rgba(255, 255, 255, 0.18);
|
||||
}}
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
class ColourSwatch(QtWidgets.QPushButton):
|
||||
"""Clickable palette swatch."""
|
||||
|
|
@ -69,10 +118,14 @@ class ColourSwatch(QtWidgets.QPushButton):
|
|||
def __init__(self, name: str, hex_code: str, callback: Callable[[str, str], None], parent: QtWidgets.QWidget | None = None):
|
||||
super().__init__(parent)
|
||||
self.hex_code = hex_code
|
||||
self.name = name
|
||||
self.setToolTip(f"{name} ({hex_code})")
|
||||
self.name_key = name
|
||||
self.callback = callback
|
||||
self.setCursor(QtCore.Qt.PointingHandCursor)
|
||||
self.setFixedSize(28, 28)
|
||||
self._apply_colour(hex_code)
|
||||
self.clicked.connect(lambda: callback(hex_code, self.name_key))
|
||||
|
||||
def _apply_colour(self, hex_code: str) -> None:
|
||||
self.setStyleSheet(
|
||||
f"""
|
||||
QPushButton {{
|
||||
|
|
@ -85,7 +138,20 @@ class ColourSwatch(QtWidgets.QPushButton):
|
|||
}}
|
||||
"""
|
||||
)
|
||||
self.clicked.connect(lambda: callback(hex_code, name))
|
||||
|
||||
def apply_theme(self, colours: Dict[str, str]) -> None:
|
||||
self.setStyleSheet(
|
||||
f"""
|
||||
QPushButton {{
|
||||
background-color: {self.hex_code};
|
||||
border: 2px solid {colours['border']};
|
||||
border-radius: 6px;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
border-color: {colours['accent']};
|
||||
}}
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
class SliderControl(QtWidgets.QWidget):
|
||||
|
|
@ -103,12 +169,10 @@ class SliderControl(QtWidgets.QWidget):
|
|||
|
||||
header = QtWidgets.QHBoxLayout()
|
||||
header.setContentsMargins(0, 0, 0, 0)
|
||||
title_label = QtWidgets.QLabel(title)
|
||||
title_label.setStyleSheet("color: rgba(255,255,255,0.85); font-weight: 500;")
|
||||
header.addWidget(title_label)
|
||||
self.title_label = QtWidgets.QLabel(title)
|
||||
header.addWidget(self.title_label)
|
||||
header.addStretch(1)
|
||||
self.value_label = QtWidgets.QLabel(str(initial))
|
||||
self.value_label.setStyleSheet("color: rgba(255,255,255,0.6);")
|
||||
header.addWidget(self.value_label)
|
||||
layout.addLayout(header)
|
||||
|
||||
|
|
@ -146,36 +210,26 @@ class SliderControl(QtWidgets.QWidget):
|
|||
self.slider.blockSignals(False)
|
||||
self.value_label.setText(str(value))
|
||||
|
||||
|
||||
class ImageView(QtWidgets.QLabel):
|
||||
"""Aspect-ratio aware image view."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self._pixmap: QtGui.QPixmap | None = None
|
||||
|
||||
def set_image(self, pixmap: QtGui.QPixmap | None) -> None:
|
||||
self._pixmap = pixmap
|
||||
self._rescale()
|
||||
|
||||
def resizeEvent(self, event: QtGui.QResizeEvent) -> None:
|
||||
super().resizeEvent(event)
|
||||
self._rescale()
|
||||
|
||||
def _rescale(self) -> None:
|
||||
if self._pixmap is None:
|
||||
self.setPixmap(QtGui.QPixmap())
|
||||
self.setText("<No image loaded>")
|
||||
self.setStyleSheet("color: rgba(255,255,255,0.45); font-size: 14px;")
|
||||
return
|
||||
target = self._pixmap.scaled(
|
||||
self.size(),
|
||||
QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.SmoothTransformation,
|
||||
def apply_theme(self, colours: Dict[str, str]) -> None:
|
||||
self.title_label.setStyleSheet(f"color: {colours['text_muted']}; font-weight: 500;")
|
||||
self.value_label.setStyleSheet(f"color: {colours['text_dim']};")
|
||||
self.slider.setStyleSheet(
|
||||
f"""
|
||||
QSlider::groove:horizontal {{
|
||||
border: 1px solid {colours['border']};
|
||||
height: 6px;
|
||||
background: rgba(255,255,255,0.14);
|
||||
border-radius: 4px;
|
||||
}}
|
||||
QSlider::handle:horizontal {{
|
||||
background: {colours['accent_secondary']};
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
width: 14px;
|
||||
margin: -5px 0;
|
||||
border-radius: 7px;
|
||||
}}
|
||||
"""
|
||||
)
|
||||
self.setPixmap(target)
|
||||
self.setStyleSheet("")
|
||||
|
||||
|
||||
class TitleBar(QtWidgets.QWidget):
|
||||
|
|
@ -190,10 +244,6 @@ class TitleBar(QtWidgets.QWidget):
|
|||
self.setCursor(QtCore.Qt.ArrowCursor)
|
||||
self.setAutoFillBackground(True)
|
||||
|
||||
palette = self.palette()
|
||||
palette.setColor(QtGui.QPalette.Window, QtGui.QColor("#16171d"))
|
||||
self.setPalette(palette)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(12, 8, 12, 8)
|
||||
layout.setSpacing(8)
|
||||
|
|
@ -201,16 +251,17 @@ class TitleBar(QtWidgets.QWidget):
|
|||
logo_path = Path(__file__).resolve().parents[1] / "assets" / "logo.png"
|
||||
if logo_path.exists():
|
||||
pixmap = QtGui.QPixmap(str(logo_path))
|
||||
logo_label = QtWidgets.QLabel()
|
||||
logo_label.setPixmap(pixmap.scaled(26, 26, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))
|
||||
layout.addWidget(logo_label)
|
||||
self.logo_label = QtWidgets.QLabel()
|
||||
self.logo_label.setPixmap(pixmap.scaled(26, 26, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))
|
||||
layout.addWidget(self.logo_label)
|
||||
else:
|
||||
self.logo_label = None
|
||||
|
||||
title_label = QtWidgets.QLabel("Interactive Color Range Analyzer")
|
||||
title_label.setStyleSheet("color: #f7f7fb; font-weight: 600;")
|
||||
layout.addWidget(title_label)
|
||||
self.title_label = QtWidgets.QLabel()
|
||||
layout.addWidget(self.title_label)
|
||||
layout.addStretch(1)
|
||||
|
||||
self.min_btn = self._create_button("–", "Minimise window")
|
||||
self.min_btn = self._create_button("–", "Minimise")
|
||||
self.min_btn.clicked.connect(window.showMinimized)
|
||||
layout.addWidget(self.min_btn)
|
||||
|
||||
|
|
@ -218,15 +269,9 @@ class TitleBar(QtWidgets.QWidget):
|
|||
self.max_btn.clicked.connect(window.toggle_maximise)
|
||||
layout.addWidget(self.max_btn)
|
||||
|
||||
close_btn = self._create_button("✕", "Close")
|
||||
close_btn.clicked.connect(window.close)
|
||||
close_btn.setStyleSheet(
|
||||
"""
|
||||
QPushButton { background: transparent; color: #f7f7fb; border: none; padding: 4px 10px; }
|
||||
QPushButton:hover { background-color: #d0342c; }
|
||||
"""
|
||||
)
|
||||
layout.addWidget(close_btn)
|
||||
self.close_btn = self._create_button("✕", "Close")
|
||||
self.close_btn.clicked.connect(window.close)
|
||||
layout.addWidget(self.close_btn)
|
||||
|
||||
def _create_button(self, text: str, tooltip: str) -> QtWidgets.QPushButton:
|
||||
btn = QtWidgets.QPushButton(text)
|
||||
|
|
@ -248,6 +293,41 @@ class TitleBar(QtWidgets.QWidget):
|
|||
)
|
||||
return btn
|
||||
|
||||
def apply_theme(self, colours: Dict[str, str]) -> None:
|
||||
palette = self.palette()
|
||||
palette.setColor(QtGui.QPalette.Window, QtGui.QColor(colours["titlebar_bg"]))
|
||||
self.setPalette(palette)
|
||||
self.title_label.setStyleSheet(f"color: {colours['text']}; font-weight: 600;")
|
||||
hover_bg = "#d0342c" if colours["titlebar_bg"] != "#e9ebf5" else "#e6675a"
|
||||
self.close_btn.setStyleSheet(
|
||||
f"""
|
||||
QPushButton {{
|
||||
background-color: transparent;
|
||||
color: {colours['text']};
|
||||
border: none;
|
||||
padding: 4px 10px;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background-color: {hover_bg};
|
||||
color: #ffffff;
|
||||
}}
|
||||
"""
|
||||
)
|
||||
for btn in (self.min_btn, self.max_btn):
|
||||
btn.setStyleSheet(
|
||||
f"""
|
||||
QPushButton {{
|
||||
background-color: transparent;
|
||||
color: {colours['text']};
|
||||
border: none;
|
||||
padding: 4px 10px;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}}
|
||||
"""
|
||||
)
|
||||
|
||||
def mouseDoubleClickEvent(self, event: QtGui.QMouseEvent) -> None:
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self.window.toggle_maximise()
|
||||
|
|
@ -260,11 +340,13 @@ class TitleBar(QtWidgets.QWidget):
|
|||
super().mousePressEvent(event)
|
||||
|
||||
|
||||
class MainWindow(QtWidgets.QMainWindow):
|
||||
class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
||||
"""Main application window containing all controls."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, language: str, defaults: dict, reset_exclusions: bool) -> None:
|
||||
super().__init__()
|
||||
self.init_i18n(language)
|
||||
self.setWindowTitle(self._t("app.title"))
|
||||
self.setWindowFlag(QtCore.Qt.FramelessWindowHint)
|
||||
self.setWindowFlag(QtCore.Qt.Window)
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground, False)
|
||||
|
|
@ -276,19 +358,19 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
container_layout.setSpacing(0)
|
||||
|
||||
self.title_bar = TitleBar(self)
|
||||
self.title_bar.title_label.setText(self._t("app.title"))
|
||||
container_layout.addWidget(self.title_bar)
|
||||
|
||||
self.content = QtWidgets.QWidget()
|
||||
self.content.setStyleSheet("background-color: #111216;")
|
||||
content_layout = QtWidgets.QVBoxLayout(self.content)
|
||||
content_layout.setContentsMargins(24, 24, 24, 24)
|
||||
content_layout.setSpacing(18)
|
||||
self.content_layout = QtWidgets.QVBoxLayout(self.content)
|
||||
self.content_layout.setContentsMargins(24, 24, 24, 24)
|
||||
self.content_layout.setSpacing(18)
|
||||
|
||||
content_layout.addLayout(self._build_toolbar())
|
||||
content_layout.addLayout(self._build_palette())
|
||||
content_layout.addLayout(self._build_sliders())
|
||||
content_layout.addWidget(self._build_previews(), 1)
|
||||
content_layout.addLayout(self._build_status_section())
|
||||
self.content_layout.addLayout(self._build_toolbar())
|
||||
self.content_layout.addLayout(self._build_palette())
|
||||
self.content_layout.addLayout(self._build_sliders())
|
||||
self.content_layout.addWidget(self._build_previews(), 1)
|
||||
self.content_layout.addLayout(self._build_status_section())
|
||||
|
||||
container_layout.addWidget(self.content, 1)
|
||||
self.setCentralWidget(container)
|
||||
|
|
@ -297,11 +379,16 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
self._current_image_path: Path | None = None
|
||||
self._current_colour = DEFAULT_COLOUR
|
||||
self._toolbar_actions: Dict[str, Callable[[], None]] = {}
|
||||
self._slider_controls: Dict[str, SliderControl] = {}
|
||||
self._register_default_actions()
|
||||
self._update_colour_display(DEFAULT_COLOUR, "Default colour")
|
||||
|
||||
self.processor = QtImageProcessor()
|
||||
self.processor.set_defaults(defaults)
|
||||
self.processor.reset_exclusions_on_switch = reset_exclusions
|
||||
self._sync_sliders_from_processor()
|
||||
self._update_colour_display(DEFAULT_COLOUR, self._t("palette.current"))
|
||||
|
||||
self.current_theme = "dark"
|
||||
self._apply_theme(self.current_theme)
|
||||
|
||||
# Window control helpers -------------------------------------------------
|
||||
|
||||
|
|
@ -330,26 +417,26 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
layout.setSpacing(12)
|
||||
|
||||
buttons = [
|
||||
("open_image", "🖼", "Open image…"),
|
||||
("open_folder", "📂", "Open folder…"),
|
||||
("choose_color", "🎨", "Choose colour"),
|
||||
("pick_from_image", "🖱", "Pick from image"),
|
||||
("save_overlay", "💾", "Save overlay"),
|
||||
("toggle_free_draw", "△", "Toggle free-draw"),
|
||||
("clear_excludes", "🧹", "Clear exclusions"),
|
||||
("undo_exclude", "↩", "Undo last exclusion"),
|
||||
("reset_sliders", "🔄", "Reset sliders"),
|
||||
("toggle_theme", "🌓", "Toggle theme"),
|
||||
("open_image", "🖼", "toolbar.open_image"),
|
||||
("open_folder", "📂", "toolbar.open_folder"),
|
||||
("choose_color", "🎨", "toolbar.choose_color"),
|
||||
("pick_from_image", "🖱", "toolbar.pick_from_image"),
|
||||
("save_overlay", "💾", "toolbar.save_overlay"),
|
||||
("toggle_free_draw", "△", "toolbar.toggle_free_draw"),
|
||||
("clear_excludes", "🧹", "toolbar.clear_excludes"),
|
||||
("undo_exclude", "↩", "toolbar.undo_exclude"),
|
||||
("reset_sliders", "🔄", "toolbar.reset_sliders"),
|
||||
("toggle_theme", "🌓", "toolbar.toggle_theme"),
|
||||
]
|
||||
self._toolbar_buttons: Dict[str, ToolbarButton] = {}
|
||||
for key, icon_txt, label in buttons:
|
||||
for key, icon_txt, text_key in buttons:
|
||||
label = self._t(text_key)
|
||||
button = ToolbarButton(icon_txt, label, lambda k=key: self._invoke_action(k))
|
||||
layout.addWidget(button)
|
||||
self._toolbar_buttons[key] = button
|
||||
|
||||
layout.addStretch(1)
|
||||
self.status_label = QtWidgets.QLabel("No image loaded")
|
||||
self.status_label.setStyleSheet("color: rgba(255,255,255,0.7); font-weight: 500;")
|
||||
self.status_label = QtWidgets.QLabel(self._t("status.no_file"))
|
||||
layout.addWidget(self.status_label, 0, QtCore.Qt.AlignRight)
|
||||
return layout
|
||||
|
||||
|
|
@ -360,9 +447,8 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
|
||||
current_group = QtWidgets.QHBoxLayout()
|
||||
current_group.setSpacing(8)
|
||||
current_label = QtWidgets.QLabel("Current colour")
|
||||
current_label.setStyleSheet("color: rgba(255,255,255,0.75); font-weight: 500;")
|
||||
current_group.addWidget(current_label)
|
||||
self.current_label = QtWidgets.QLabel(self._t("palette.current"))
|
||||
current_group.addWidget(self.current_label)
|
||||
|
||||
self.current_colour_swatch = QtWidgets.QLabel()
|
||||
self.current_colour_swatch.setFixedSize(28, 28)
|
||||
|
|
@ -370,19 +456,19 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
current_group.addWidget(self.current_colour_swatch)
|
||||
|
||||
self.current_colour_label = QtWidgets.QLabel(f"({DEFAULT_COLOUR})")
|
||||
self.current_colour_label.setStyleSheet("color: rgba(255,255,255,0.65);")
|
||||
current_group.addWidget(self.current_colour_label)
|
||||
layout.addLayout(current_group)
|
||||
|
||||
more_label = QtWidgets.QLabel("More colours")
|
||||
more_label.setStyleSheet("color: rgba(255,255,255,0.65); font-weight: 500;")
|
||||
layout.addWidget(more_label)
|
||||
self.more_label = QtWidgets.QLabel(self._t("palette.more"))
|
||||
layout.addWidget(self.more_label)
|
||||
|
||||
swatch_container = QtWidgets.QHBoxLayout()
|
||||
swatch_container.setSpacing(8)
|
||||
for name, hex_code in PRESET_COLOURS:
|
||||
swatch = ColourSwatch(name, hex_code, self._update_colour_display)
|
||||
self.swatch_buttons: List[ColourSwatch] = []
|
||||
for name_key, hex_code in PRESET_COLOURS:
|
||||
swatch = ColourSwatch(self._t(name_key), hex_code, self._update_colour_display)
|
||||
swatch_container.addWidget(swatch)
|
||||
self.swatch_buttons.append(swatch)
|
||||
layout.addLayout(swatch_container)
|
||||
layout.addStretch(1)
|
||||
return layout
|
||||
|
|
@ -390,12 +476,13 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
def _build_sliders(self) -> QtWidgets.QHBoxLayout:
|
||||
layout = QtWidgets.QHBoxLayout()
|
||||
layout.setSpacing(16)
|
||||
self._slider_controls.clear()
|
||||
for title, key, minimum, maximum, initial in SLIDER_SPECS:
|
||||
control = SliderControl(title, key, minimum, maximum, initial)
|
||||
self._slider_controls: Dict[str, SliderControl] = {}
|
||||
for key, attr, minimum, maximum in SLIDER_SPECS:
|
||||
initial = int(getattr(self.processor, attr))
|
||||
control = SliderControl(self._t(key), attr, minimum, maximum, initial)
|
||||
control.value_changed.connect(self._on_slider_change)
|
||||
layout.addWidget(control)
|
||||
self._slider_controls[key] = control
|
||||
self._slider_controls[attr] = control
|
||||
return layout
|
||||
|
||||
def _build_previews(self) -> QtWidgets.QWidget:
|
||||
|
|
@ -403,41 +490,26 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
layout = QtWidgets.QGridLayout(container)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setHorizontalSpacing(16)
|
||||
layout.setVerticalSpacing(0)
|
||||
|
||||
self.prev_button = QtWidgets.QToolButton()
|
||||
self.prev_button.setText("◀")
|
||||
self.prev_button.setCursor(QtCore.Qt.PointingHandCursor)
|
||||
self.prev_button.setFixedSize(44, 44)
|
||||
self.prev_button.setStyleSheet(
|
||||
"""
|
||||
QToolButton {
|
||||
border-radius: 22px;
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
color: #f7f7fb;
|
||||
font-size: 18px;
|
||||
}
|
||||
QToolButton:hover {
|
||||
background-color: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
"""
|
||||
)
|
||||
self.prev_button.clicked.connect(lambda: self._invoke_action("show_previous_image"))
|
||||
layout.addWidget(self.prev_button, 0, 0, QtCore.Qt.AlignVCenter)
|
||||
|
||||
self.image_view = ImageView()
|
||||
self.image_view.setStyleSheet("border: 1px solid rgba(255,255,255,0.08); border-radius: 12px;")
|
||||
self.image_view = QtWidgets.QLabel("<No image loaded>")
|
||||
self.image_view.setAlignment(QtCore.Qt.AlignCenter)
|
||||
layout.addWidget(self.image_view, 0, 1)
|
||||
|
||||
self.overlay_view = ImageView()
|
||||
self.overlay_view.setStyleSheet("border: 1px solid rgba(255,255,255,0.08); border-radius: 12px;")
|
||||
self.overlay_view = QtWidgets.QLabel("<No image loaded>")
|
||||
self.overlay_view.setAlignment(QtCore.Qt.AlignCenter)
|
||||
layout.addWidget(self.overlay_view, 0, 2)
|
||||
|
||||
self.next_button = QtWidgets.QToolButton()
|
||||
self.next_button.setText("▶")
|
||||
self.next_button.setCursor(QtCore.Qt.PointingHandCursor)
|
||||
self.next_button.setFixedSize(44, 44)
|
||||
self.next_button.setStyleSheet(self.prev_button.styleSheet())
|
||||
self.next_button.clicked.connect(lambda: self._invoke_action("show_next_image"))
|
||||
layout.addWidget(self.next_button, 0, 3, QtCore.Qt.AlignVCenter)
|
||||
layout.setColumnStretch(1, 1)
|
||||
|
|
@ -451,12 +523,10 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
|
||||
self.filename_label = QtWidgets.QLabel("—")
|
||||
self.filename_label.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.filename_label.setStyleSheet("color: rgba(255,255,255,0.85); font-weight: 600;")
|
||||
layout.addWidget(self.filename_label)
|
||||
|
||||
self.ratio_label = QtWidgets.QLabel("Matches with exclusions: —")
|
||||
self.ratio_label = QtWidgets.QLabel(self._t("stats.placeholder"))
|
||||
self.ratio_label.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.ratio_label.setStyleSheet("color: #e6b84b; font-weight: 600;")
|
||||
layout.addWidget(self.ratio_label)
|
||||
return layout
|
||||
|
||||
|
|
@ -473,7 +543,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
"clear_excludes": self._coming_soon,
|
||||
"undo_exclude": self._coming_soon,
|
||||
"reset_sliders": self._reset_sliders,
|
||||
"toggle_theme": self._coming_soon,
|
||||
"toggle_theme": self.toggle_theme,
|
||||
"show_previous_image": self.show_previous_image,
|
||||
"show_next_image": self.show_next_image,
|
||||
}
|
||||
|
|
@ -487,20 +557,20 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
|
||||
def open_image(self) -> None:
|
||||
filters = "Images (*.png *.jpg *.jpeg *.bmp *.webp)"
|
||||
path_str, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select image", "", filters)
|
||||
path_str, _ = QtWidgets.QFileDialog.getOpenFileName(self, self._t("dialog.open_image_title"), "", filters)
|
||||
if not path_str:
|
||||
return
|
||||
path = Path(path_str)
|
||||
try:
|
||||
self.processor.load_single_image(path)
|
||||
except Exception as exc:
|
||||
QtWidgets.QMessageBox.warning(self, "ICRA", f"Unable to open the selected image.\n{exc}")
|
||||
except Exception as exc: # pragma: no cover - user feedback
|
||||
QtWidgets.QMessageBox.warning(self, self._t("dialog.error_title"), str(exc))
|
||||
return
|
||||
self._current_image_path = path
|
||||
self._refresh_views()
|
||||
|
||||
def open_folder(self) -> None:
|
||||
directory = QtWidgets.QFileDialog.getExistingDirectory(self, "Select image folder")
|
||||
directory = QtWidgets.QFileDialog.getExistingDirectory(self, self._t("dialog.open_folder_title"))
|
||||
if not directory:
|
||||
return
|
||||
folder = Path(directory)
|
||||
|
|
@ -509,19 +579,19 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
key=lambda p: p.name.lower(),
|
||||
)
|
||||
if not paths:
|
||||
QtWidgets.QMessageBox.information(self, "ICRA", "No supported image files found in the selected folder.")
|
||||
QtWidgets.QMessageBox.information(self, self._t("dialog.info_title"), self._t("dialog.folder_empty"))
|
||||
return
|
||||
try:
|
||||
self.processor.load_folder(paths)
|
||||
except ValueError as exc:
|
||||
QtWidgets.QMessageBox.information(self, "ICRA", str(exc))
|
||||
QtWidgets.QMessageBox.information(self, self._t("dialog.info_title"), str(exc))
|
||||
return
|
||||
self._current_image_path = paths[0]
|
||||
self._refresh_views()
|
||||
|
||||
def show_previous_image(self) -> None:
|
||||
if not self.processor.preview_paths:
|
||||
self._coming_soon()
|
||||
QtWidgets.QMessageBox.information(self, self._t("dialog.info_title"), self._t("dialog.no_image_loaded"))
|
||||
return
|
||||
self.processor.previous_image()
|
||||
self._current_image_path = self.processor.preview_paths[self.processor.current_index]
|
||||
|
|
@ -529,7 +599,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
|
||||
def show_next_image(self) -> None:
|
||||
if not self.processor.preview_paths:
|
||||
self._coming_soon()
|
||||
QtWidgets.QMessageBox.information(self, self._t("dialog.info_title"), self._t("dialog.no_image_loaded"))
|
||||
return
|
||||
self.processor.next_image()
|
||||
self._current_image_path = self.processor.preview_paths[self.processor.current_index]
|
||||
|
|
@ -544,46 +614,87 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
self.status_label.setText(f"{label}: {hex_code}")
|
||||
|
||||
def _on_slider_change(self, key: str, value: int) -> None:
|
||||
formatted = key.replace("_", " ").title()
|
||||
self.processor.set_threshold(key, value)
|
||||
self.status_label.setText(f"{formatted} → {value}")
|
||||
formatted = self._t("status.defaults_restored")
|
||||
self.status_label.setText(f"{formatted} ({key} → {value})")
|
||||
self._refresh_overlay_only()
|
||||
|
||||
def _reset_sliders(self) -> None:
|
||||
for _, key, _, _, initial in SLIDER_SPECS:
|
||||
control = self._slider_controls.get(key)
|
||||
for _, attr, _, _ in SLIDER_SPECS:
|
||||
control = self._slider_controls.get(attr)
|
||||
if control:
|
||||
control.set_value(initial)
|
||||
self.processor.set_threshold(key, initial)
|
||||
self.status_label.setText("Sliders reset to defaults")
|
||||
default_value = int(getattr(self.processor, attr))
|
||||
control.set_value(default_value)
|
||||
self.status_label.setText(self._t("status.defaults_restored"))
|
||||
self._refresh_overlay_only()
|
||||
|
||||
def _coming_soon(self) -> None:
|
||||
QtWidgets.QMessageBox.information(
|
||||
self,
|
||||
"Coming soon",
|
||||
"This action is not yet wired up in the PySide6 migration prototype.",
|
||||
self._t("dialog.info_title"),
|
||||
"Feature coming soon in the PySide6 migration.",
|
||||
)
|
||||
|
||||
# view refresh -----------------------------------------------------------
|
||||
def toggle_theme(self) -> None:
|
||||
self.current_theme = "light" if self.current_theme == "dark" else "dark"
|
||||
self._apply_theme(self.current_theme)
|
||||
|
||||
def _apply_theme(self, mode: str) -> None:
|
||||
colours = THEMES[mode]
|
||||
self.content.setStyleSheet(f"background-color: {colours['window_bg']};")
|
||||
self.image_view.setStyleSheet(f"border: 1px solid {colours['border']}; border-radius: 12px; color: {colours['text_dim']};")
|
||||
self.overlay_view.setStyleSheet(f"border: 1px solid {colours['border']}; border-radius: 12px; color: {colours['text_dim']};")
|
||||
|
||||
self.status_label.setStyleSheet(f"color: {colours['text_muted']}; font-weight: 500;")
|
||||
self.current_label.setStyleSheet(f"color: {colours['text_muted']}; font-weight: 500;")
|
||||
self.current_colour_label.setStyleSheet(f"color: {colours['text_dim']};")
|
||||
self.more_label.setStyleSheet(f"color: {colours['text_muted']}; font-weight: 500;")
|
||||
self.filename_label.setStyleSheet(f"color: {colours['text']}; font-weight: 600;")
|
||||
self.ratio_label.setStyleSheet(f"color: {colours['highlight']}; font-weight: 600;")
|
||||
|
||||
for button in self._toolbar_buttons.values():
|
||||
button.apply_theme(colours)
|
||||
for swatch in self.swatch_buttons:
|
||||
swatch.apply_theme(colours)
|
||||
for control in self._slider_controls.values():
|
||||
control.apply_theme(colours)
|
||||
|
||||
style_button = (
|
||||
f"QToolButton {{ border-radius: 22px; background-color: {colours['panel_bg']}; "
|
||||
f"border: 1px solid {colours['border']}; color: {colours['text']}; }}"
|
||||
f"QToolButton:hover {{ background-color: {colours['accent_secondary']}; color: white; }}"
|
||||
)
|
||||
self.prev_button.setStyleSheet(style_button)
|
||||
self.next_button.setStyleSheet(style_button)
|
||||
|
||||
self.title_bar.apply_theme(colours)
|
||||
|
||||
def _sync_sliders_from_processor(self) -> None:
|
||||
for _, attr, _, _ in SLIDER_SPECS:
|
||||
control = self._slider_controls.get(attr)
|
||||
if control:
|
||||
control.set_value(int(getattr(self.processor, attr)))
|
||||
|
||||
def _refresh_views(self) -> None:
|
||||
preview_pix = self.processor.preview_pixmap()
|
||||
overlay_pix = self.processor.overlay_pixmap()
|
||||
self.image_view.set_image(preview_pix)
|
||||
self.overlay_view.set_image(overlay_pix)
|
||||
self.image_view.setPixmap(preview_pix)
|
||||
self.overlay_view.setPixmap(overlay_pix)
|
||||
if self._current_image_path and self.processor.preview_img:
|
||||
width, height = self.processor.preview_img.size
|
||||
total = len(self.processor.preview_paths)
|
||||
position = f" [{self.processor.current_index + 1}/{total}]" if total > 1 else ""
|
||||
dimensions = f"{width}×{height}"
|
||||
self.status_label.setText(
|
||||
f"{self._current_image_path.name} · {width}×{height}"
|
||||
self._t("status.loaded", name=self._current_image_path.name, dimensions=dimensions, position=position)
|
||||
)
|
||||
self.filename_label.setText(
|
||||
f"{self._current_image_path.name} ({width}×{height})"
|
||||
self._t("status.filename_label", name=self._current_image_path.name, dimensions=dimensions, position=position)
|
||||
)
|
||||
self.ratio_label.setText(self.processor.stats.summary())
|
||||
self.ratio_label.setText(self.processor.stats.summary(self._t))
|
||||
|
||||
def _refresh_overlay_only(self) -> None:
|
||||
if self.processor.preview_img is None:
|
||||
return
|
||||
self.overlay_view.set_image(self.processor.overlay_pixmap())
|
||||
self.ratio_label.setText(self.processor.stats.summary())
|
||||
self.overlay_view.setPixmap(self.processor.overlay_pixmap())
|
||||
self.ratio_label.setText(self.processor.stats.summary(self._t))
|
||||
|
|
|
|||
Loading…
Reference in New Issue