Feature: Advanced Navigation & Composite Scoring
- Added direct pattern jump input field (Ctrl+J behavior without hotkey) - Implemented Composite Score: 35% match, 55% excl match, 10% brightness - Added 'Prefer darkness' toggle to invert brightness score - Added checkmarks to 'Prefer darkness' and 'Toggle free-draw' menus - Added dynamic Darkness/Brightness text parsing to UI and CSV export
This commit is contained in:
parent
acfcf99d15
commit
7f219885bf
|
|
@ -10,6 +10,8 @@
|
||||||
"toolbar.undo_exclude" = "Letzten Ausschluss entfernen"
|
"toolbar.undo_exclude" = "Letzten Ausschluss entfernen"
|
||||||
"toolbar.reset_sliders" = "Slider zurücksetzen"
|
"toolbar.reset_sliders" = "Slider zurücksetzen"
|
||||||
"toolbar.toggle_theme" = "Theme umschalten"
|
"toolbar.toggle_theme" = "Theme umschalten"
|
||||||
|
"toolbar.open_app_folder" = "Programmordner öffnen"
|
||||||
|
"toolbar.prefer_dark" = "Dunkelheit bevorzugen"
|
||||||
"status.no_file" = "Keine Datei geladen."
|
"status.no_file" = "Keine Datei geladen."
|
||||||
"status.defaults_restored" = "Standardwerte aktiv."
|
"status.defaults_restored" = "Standardwerte aktiv."
|
||||||
"status.free_draw_enabled" = "Freihand-Ausschluss aktiviert."
|
"status.free_draw_enabled" = "Freihand-Ausschluss aktiviert."
|
||||||
|
|
@ -41,7 +43,9 @@
|
||||||
"sliders.val_max" = "Helligkeit Max (%)"
|
"sliders.val_max" = "Helligkeit Max (%)"
|
||||||
"sliders.alpha" = "Overlay Alpha"
|
"sliders.alpha" = "Overlay Alpha"
|
||||||
"stats.placeholder" = "Markierungen (mit Ausschlüssen): —"
|
"stats.placeholder" = "Markierungen (mit Ausschlüssen): —"
|
||||||
"stats.summary" = "Markierungen (mit Ausschlüssen): {with_pct:.2f}% | Markierungen (ohne Ausschlüsse): {without_pct:.2f}% | Ausgeschlossen: {excluded_pct:.2f}% der Pixel, davon {excluded_match_pct:.2f}% markiert"
|
"stats.summary" = "Score: {score:.2f}% | Markierungen (m. Ausschl.): {with_pct:.2f}% | Markierungen: {without_pct:.2f}% | {brightness_label}: {brightness:.1f}% | Ausgeschlossen: {excluded_pct:.2f}%"
|
||||||
|
"stats.brightness_label" = "Helligkeit"
|
||||||
|
"stats.darkness_label" = "Dunkelheit"
|
||||||
"menu.copy" = "Kopieren"
|
"menu.copy" = "Kopieren"
|
||||||
"dialog.info_title" = "Info"
|
"dialog.info_title" = "Info"
|
||||||
"dialog.error_title" = "Fehler"
|
"dialog.error_title" = "Fehler"
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
"toolbar.undo_exclude" = "Undo last exclusion"
|
"toolbar.undo_exclude" = "Undo last exclusion"
|
||||||
"toolbar.reset_sliders" = "Reset sliders"
|
"toolbar.reset_sliders" = "Reset sliders"
|
||||||
"toolbar.toggle_theme" = "Toggle theme"
|
"toolbar.toggle_theme" = "Toggle theme"
|
||||||
|
"toolbar.open_app_folder" = "Open application folder"
|
||||||
|
"toolbar.prefer_dark" = "Prefer darkness"
|
||||||
"status.no_file" = "No file loaded."
|
"status.no_file" = "No file loaded."
|
||||||
"status.defaults_restored" = "Defaults restored."
|
"status.defaults_restored" = "Defaults restored."
|
||||||
"status.free_draw_enabled" = "Free-draw exclusion mode enabled."
|
"status.free_draw_enabled" = "Free-draw exclusion mode enabled."
|
||||||
|
|
@ -41,7 +43,9 @@
|
||||||
"sliders.val_max" = "Value max (%)"
|
"sliders.val_max" = "Value max (%)"
|
||||||
"sliders.alpha" = "Overlay alpha"
|
"sliders.alpha" = "Overlay alpha"
|
||||||
"stats.placeholder" = "Matches (with exclusions): —"
|
"stats.placeholder" = "Matches (with exclusions): —"
|
||||||
"stats.summary" = "Matches (with exclusions): {with_pct:.2f}% | Matches (without exclusions): {without_pct:.2f}% | Excluded: {excluded_pct:.2f}% of pixels, {excluded_match_pct:.2f}% marked"
|
"stats.summary" = "Score: {score:.2f}% | Matches (w/ excl.): {with_pct:.2f}% | Matches: {without_pct:.2f}% | {brightness_label}: {brightness:.1f}% | Excluded: {excluded_pct:.2f}%"
|
||||||
|
"stats.brightness_label" = "Brightness"
|
||||||
|
"stats.darkness_label" = "Darkness"
|
||||||
"menu.copy" = "Copy"
|
"menu.copy" = "Copy"
|
||||||
"dialog.info_title" = "Info"
|
"dialog.info_title" = "Info"
|
||||||
"dialog.error_title" = "Error"
|
"dialog.error_title" = "Error"
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,20 @@ class Stats:
|
||||||
total_keep: int = 0
|
total_keep: int = 0
|
||||||
matches_excl: int = 0
|
matches_excl: int = 0
|
||||||
total_excl: int = 0
|
total_excl: int = 0
|
||||||
|
brightness_score: float = 0.0
|
||||||
|
prefer_dark: bool = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effective_brightness(self) -> float:
|
||||||
|
"""Returns inverted brightness when prefer_dark is on."""
|
||||||
|
return (100.0 - self.brightness_score) if self.prefer_dark else self.brightness_score
|
||||||
|
|
||||||
|
@property
|
||||||
|
def composite_score(self) -> float:
|
||||||
|
"""Weighted composite: 35% match_all + 55% match_keep + 10% brightness."""
|
||||||
|
pct_all = (self.matches_all / self.total_all * 100) if self.total_all else 0.0
|
||||||
|
pct_keep = (self.matches_keep / self.total_keep * 100) if self.total_keep else 0.0
|
||||||
|
return 0.35 * pct_all + 0.55 * pct_keep + 0.10 * self.effective_brightness
|
||||||
|
|
||||||
def summary(self, translate) -> str:
|
def summary(self, translate) -> str:
|
||||||
if self.total_all == 0:
|
if self.total_all == 0:
|
||||||
|
|
@ -29,13 +43,15 @@ class Stats:
|
||||||
with_pct = (self.matches_keep / self.total_keep * 100) if self.total_keep else 0.0
|
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
|
without_pct = (self.matches_all / self.total_all * 100) if self.total_all else 0.0
|
||||||
excluded_pct = (self.total_excl / self.total_all * 100) if self.total_all else 0.0
|
excluded_pct = (self.total_excl / self.total_all * 100) if self.total_all else 0.0
|
||||||
excluded_match_pct = (self.matches_excl / self.total_excl * 100) if self.total_excl else 0.0
|
brightness_label = translate("stats.darkness_label") if self.prefer_dark else translate("stats.brightness_label")
|
||||||
return translate(
|
return translate(
|
||||||
"stats.summary",
|
"stats.summary",
|
||||||
|
score=self.composite_score,
|
||||||
with_pct=with_pct,
|
with_pct=with_pct,
|
||||||
without_pct=without_pct,
|
without_pct=without_pct,
|
||||||
|
brightness_label=brightness_label,
|
||||||
|
brightness=self.effective_brightness,
|
||||||
excluded_pct=excluded_pct,
|
excluded_pct=excluded_pct,
|
||||||
excluded_match_pct=excluded_match_pct,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -108,6 +124,7 @@ class QtImageProcessor:
|
||||||
self._cached_mask: np.ndarray | None = None
|
self._cached_mask: np.ndarray | None = None
|
||||||
self._cached_mask_size: Tuple[int, int] | None = None
|
self._cached_mask_size: Tuple[int, int] | None = None
|
||||||
self.exclude_ref_size: Tuple[int, int] | None = None
|
self.exclude_ref_size: Tuple[int, int] | None = None
|
||||||
|
self.prefer_dark: bool = False
|
||||||
|
|
||||||
def set_defaults(self, defaults: dict) -> None:
|
def set_defaults(self, defaults: dict) -> None:
|
||||||
for key in self.defaults:
|
for key in self.defaults:
|
||||||
|
|
@ -221,6 +238,10 @@ class QtImageProcessor:
|
||||||
matches_excl = int(excl_match[visible].sum())
|
matches_excl = int(excl_match[visible].sum())
|
||||||
total_excl = int((visible & excl_mask).sum())
|
total_excl = int((visible & excl_mask).sum())
|
||||||
|
|
||||||
|
# Brightness: mean Value (0-100) of ALL non-excluded visible pixels
|
||||||
|
keep_visible = visible & ~excl_mask
|
||||||
|
brightness = float(val[keep_visible].mean()) if keep_visible.any() else 0.0
|
||||||
|
|
||||||
# Build overlay image
|
# Build overlay image
|
||||||
overlay_arr = np.zeros((base.height, base.width, 4), dtype=np.uint8)
|
overlay_arr = np.zeros((base.height, base.width, 4), dtype=np.uint8)
|
||||||
overlay_arr[keep_match, 0] = self.overlay_r
|
overlay_arr[keep_match, 0] = self.overlay_r
|
||||||
|
|
@ -236,6 +257,8 @@ class QtImageProcessor:
|
||||||
total_keep=total_keep,
|
total_keep=total_keep,
|
||||||
matches_excl=matches_excl,
|
matches_excl=matches_excl,
|
||||||
total_excl=total_excl,
|
total_excl=total_excl,
|
||||||
|
brightness_score=brightness,
|
||||||
|
prefer_dark=self.prefer_dark,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_stats_headless(self, image: Image.Image) -> Stats:
|
def get_stats_headless(self, image: Image.Image) -> Stats:
|
||||||
|
|
@ -273,13 +296,19 @@ class QtImageProcessor:
|
||||||
excl_match = match_mask & excl_mask
|
excl_match = match_mask & excl_mask
|
||||||
visible = alpha_ch > 0
|
visible = alpha_ch > 0
|
||||||
|
|
||||||
|
matches_keep_count = int(keep_match[visible].sum())
|
||||||
|
keep_visible = visible & ~excl_mask
|
||||||
|
brightness = float(val[keep_visible].mean()) if keep_visible.any() else 0.0
|
||||||
|
|
||||||
return Stats(
|
return Stats(
|
||||||
matches_all=int(match_mask[visible].sum()),
|
matches_all=int(match_mask[visible].sum()),
|
||||||
total_all=int(visible.sum()),
|
total_all=int(visible.sum()),
|
||||||
matches_keep=int(keep_match[visible].sum()),
|
matches_keep=matches_keep_count,
|
||||||
total_keep=int((visible & ~excl_mask).sum()),
|
total_keep=int((visible & ~excl_mask).sum()),
|
||||||
matches_excl=int(excl_match[visible].sum()),
|
matches_excl=int(excl_match[visible].sum()),
|
||||||
total_excl=int((visible & excl_mask).sum()),
|
total_excl=int((visible & excl_mask).sum()),
|
||||||
|
brightness_score=brightness,
|
||||||
|
prefer_dark=self.prefer_dark,
|
||||||
)
|
)
|
||||||
|
|
||||||
# helpers ----------------------------------------------------------------
|
# helpers ----------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import re
|
import re
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.error
|
import urllib.error
|
||||||
|
|
@ -637,13 +638,24 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
||||||
tools_menu = self.menu_bar.addMenu(self._t("menu.tools"))
|
tools_menu = self.menu_bar.addMenu(self._t("menu.tools"))
|
||||||
tools_menu.addAction("🎨 " + self._t("toolbar.choose_color"), lambda: self._invoke_action("choose_color"))
|
tools_menu.addAction("🎨 " + self._t("toolbar.choose_color"), lambda: self._invoke_action("choose_color"))
|
||||||
tools_menu.addAction("🖱 " + self._t("toolbar.pick_from_image"), lambda: self._invoke_action("pick_from_image"))
|
tools_menu.addAction("🖱 " + self._t("toolbar.pick_from_image"), lambda: self._invoke_action("pick_from_image"))
|
||||||
tools_menu.addAction("△ " + self._t("toolbar.toggle_free_draw"), lambda: self._invoke_action("toggle_free_draw"))
|
self.free_draw_action = QtGui.QAction("△ " + self._t("toolbar.toggle_free_draw"), self)
|
||||||
|
self.free_draw_action.setCheckable(True)
|
||||||
|
self.free_draw_action.setChecked(False)
|
||||||
|
self.free_draw_action.triggered.connect(lambda: self._invoke_action("toggle_free_draw"))
|
||||||
|
tools_menu.addAction(self.free_draw_action)
|
||||||
tools_menu.addSeparator()
|
tools_menu.addSeparator()
|
||||||
tools_menu.addAction("📥 " + self._t("toolbar.pull_patterns"), lambda: self._invoke_action("pull_patterns"))
|
tools_menu.addAction("📥 " + self._t("toolbar.pull_patterns"), lambda: self._invoke_action("pull_patterns"))
|
||||||
|
|
||||||
# View Menu
|
# View Menu
|
||||||
view_menu = self.menu_bar.addMenu(self._t("menu.view"))
|
view_menu = self.menu_bar.addMenu(self._t("menu.view"))
|
||||||
view_menu.addAction("🌓 " + self._t("toolbar.toggle_theme"), lambda: self._invoke_action("toggle_theme"))
|
view_menu.addAction("🌓 " + self._t("toolbar.toggle_theme"), lambda: self._invoke_action("toggle_theme"))
|
||||||
|
self.prefer_dark_action = QtGui.QAction("🌑 " + self._t("toolbar.prefer_dark"), self)
|
||||||
|
self.prefer_dark_action.setCheckable(True)
|
||||||
|
self.prefer_dark_action.setChecked(False)
|
||||||
|
self.prefer_dark_action.triggered.connect(lambda: self._invoke_action("toggle_prefer_dark"))
|
||||||
|
view_menu.addAction(self.prefer_dark_action)
|
||||||
|
view_menu.addSeparator()
|
||||||
|
view_menu.addAction("📁 " + self._t("toolbar.open_app_folder"), lambda: self._invoke_action("open_app_folder"))
|
||||||
|
|
||||||
# Status label logic remains but moved to palette layout or kept minimal
|
# Status label logic remains but moved to palette layout or kept minimal
|
||||||
# We will add it to the palette layout so that it stays on top
|
# We will add it to the palette layout so that it stays on top
|
||||||
|
|
@ -737,9 +749,38 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
||||||
layout.setSpacing(8)
|
layout.setSpacing(8)
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
self.filename_label = QtWidgets.QLabel("—")
|
# Status row container
|
||||||
self.filename_label.setAlignment(QtCore.Qt.AlignCenter)
|
status_row_layout = QtWidgets.QHBoxLayout()
|
||||||
layout.addWidget(self.filename_label)
|
status_row_layout.setSpacing(4)
|
||||||
|
status_row_layout.setAlignment(QtCore.Qt.AlignCenter)
|
||||||
|
|
||||||
|
self.filename_prefix_label = QtWidgets.QLabel(self._t("status.loaded", name="", dimensions="", position="").split("{name}")[0])
|
||||||
|
self.filename_prefix_label.setStyleSheet("color: " + THEMES["dark"]["text_muted"] + "; font-weight: 500;")
|
||||||
|
status_row_layout.addWidget(self.filename_prefix_label)
|
||||||
|
|
||||||
|
self.pattern_input = QtWidgets.QLineEdit("—")
|
||||||
|
self.pattern_input.setAlignment(QtCore.Qt.AlignCenter)
|
||||||
|
self.pattern_input.setFixedWidth(100)
|
||||||
|
self.pattern_input.setStyleSheet(
|
||||||
|
"QLineEdit {"
|
||||||
|
" background: rgba(255, 255, 255, 0.05);"
|
||||||
|
" border: 1px solid rgba(255, 255, 255, 0.1);"
|
||||||
|
" border-radius: 4px;"
|
||||||
|
" color: " + THEMES["dark"]["text"] + ";"
|
||||||
|
" font-weight: 600;"
|
||||||
|
"}"
|
||||||
|
"QLineEdit:focus {"
|
||||||
|
" border: 1px solid " + THEMES["dark"]["accent"] + ";"
|
||||||
|
"}"
|
||||||
|
)
|
||||||
|
self.pattern_input.returnPressed.connect(self._jump_to_pattern)
|
||||||
|
status_row_layout.addWidget(self.pattern_input)
|
||||||
|
|
||||||
|
self.filename_suffix_label = QtWidgets.QLabel("")
|
||||||
|
self.filename_suffix_label.setStyleSheet("color: " + THEMES["dark"]["text"] + "; font-weight: 600;")
|
||||||
|
status_row_layout.addWidget(self.filename_suffix_label)
|
||||||
|
|
||||||
|
layout.addLayout(status_row_layout)
|
||||||
|
|
||||||
self.ratio_label = QtWidgets.QLabel(self._t("stats.placeholder"))
|
self.ratio_label = QtWidgets.QLabel(self._t("stats.placeholder"))
|
||||||
self.ratio_label.setAlignment(QtCore.Qt.AlignCenter)
|
self.ratio_label.setAlignment(QtCore.Qt.AlignCenter)
|
||||||
|
|
@ -763,6 +804,8 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
||||||
"undo_exclude": self.undo_exclusion,
|
"undo_exclude": self.undo_exclusion,
|
||||||
"reset_sliders": self._reset_sliders,
|
"reset_sliders": self._reset_sliders,
|
||||||
"toggle_theme": self.toggle_theme,
|
"toggle_theme": self.toggle_theme,
|
||||||
|
"toggle_prefer_dark": self.toggle_prefer_dark,
|
||||||
|
"open_app_folder": self.open_app_folder,
|
||||||
"show_previous_image": self.show_previous_image,
|
"show_previous_image": self.show_previous_image,
|
||||||
"show_next_image": self.show_next_image,
|
"show_next_image": self.show_next_image,
|
||||||
"pull_patterns": self.open_pattern_puller,
|
"pull_patterns": self.open_pattern_puller,
|
||||||
|
|
@ -773,6 +816,41 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
||||||
if action:
|
if action:
|
||||||
action()
|
action()
|
||||||
|
|
||||||
|
def _jump_to_pattern(self) -> None:
|
||||||
|
if not self.processor.preview_paths:
|
||||||
|
return
|
||||||
|
|
||||||
|
target_text = self.pattern_input.text().strip()
|
||||||
|
if not target_text:
|
||||||
|
# Restore current text if empty
|
||||||
|
if self._current_image_path:
|
||||||
|
self.pattern_input.setText(self._current_image_path.stem)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Try to find exactly this name or stem
|
||||||
|
target_stem_lower = target_text.lower()
|
||||||
|
found_idx = -1
|
||||||
|
|
||||||
|
for i, path in enumerate(self.processor.preview_paths):
|
||||||
|
if path.stem.lower() == target_stem_lower or path.name.lower() == target_stem_lower:
|
||||||
|
found_idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if found_idx != -1:
|
||||||
|
# Found it, jump!
|
||||||
|
self.processor.current_index = found_idx
|
||||||
|
try:
|
||||||
|
loaded_path = self.processor._load_image_at_current()
|
||||||
|
self._current_image_path = loaded_path
|
||||||
|
self._refresh_views()
|
||||||
|
except Exception as e:
|
||||||
|
QtWidgets.QMessageBox.warning(self, self._t("dialog.error_title"), str(e))
|
||||||
|
else:
|
||||||
|
# Not found, just restore text
|
||||||
|
if self._current_image_path:
|
||||||
|
self.pattern_input.setText(self._current_image_path.stem)
|
||||||
|
QtWidgets.QMessageBox.information(self, self._t("dialog.info_title"), f"Pattern '{target_text}' not found in current folder.")
|
||||||
|
|
||||||
# Image handling ---------------------------------------------------------
|
# Image handling ---------------------------------------------------------
|
||||||
|
|
||||||
def open_image(self) -> None:
|
def open_image(self) -> None:
|
||||||
|
|
@ -924,12 +1002,15 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
||||||
delimiter = ";"
|
delimiter = ";"
|
||||||
decimal = ","
|
decimal = ","
|
||||||
|
|
||||||
|
brightness_col = "Darkness Score" if self.processor.prefer_dark else "Brightness Score"
|
||||||
headers = [
|
headers = [
|
||||||
"Filename",
|
"Filename",
|
||||||
"Color",
|
"Color",
|
||||||
"Matching Pixels",
|
"Matching Pixels",
|
||||||
"Matching Pixels w/ Exclusions",
|
"Matching Pixels w/ Exclusions",
|
||||||
"Excluded Pixels"
|
"Excluded Pixels",
|
||||||
|
brightness_col,
|
||||||
|
"Composite Score"
|
||||||
]
|
]
|
||||||
rows = [headers]
|
rows = [headers]
|
||||||
|
|
||||||
|
|
@ -945,6 +1026,8 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
||||||
pct_all_str = f"{pct_all:.2f}".replace(".", decimal)
|
pct_all_str = f"{pct_all:.2f}".replace(".", decimal)
|
||||||
pct_keep_str = f"{pct_keep:.2f}".replace(".", decimal)
|
pct_keep_str = f"{pct_keep:.2f}".replace(".", decimal)
|
||||||
pct_excl_str = f"{pct_excl:.2f}".replace(".", decimal)
|
pct_excl_str = f"{pct_excl:.2f}".replace(".", decimal)
|
||||||
|
brightness_str = f"{s.effective_brightness:.2f}".replace(".", decimal)
|
||||||
|
composite_str = f"{s.composite_score:.2f}".replace(".", decimal)
|
||||||
|
|
||||||
img.close()
|
img.close()
|
||||||
return [
|
return [
|
||||||
|
|
@ -952,10 +1035,12 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
||||||
self._current_color,
|
self._current_color,
|
||||||
pct_all_str,
|
pct_all_str,
|
||||||
pct_keep_str,
|
pct_keep_str,
|
||||||
pct_excl_str
|
pct_excl_str,
|
||||||
|
brightness_str,
|
||||||
|
composite_str
|
||||||
]
|
]
|
||||||
except Exception:
|
except Exception:
|
||||||
return [img_path.name, self._current_color, "Error", "Error", "Error"]
|
return [img_path.name, self._current_color, "Error", "Error", "Error", "Error", "Error"]
|
||||||
|
|
||||||
results = [None] * total
|
results = [None] * total
|
||||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||||
|
|
@ -1197,9 +1282,17 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
||||||
def toggle_free_draw(self) -> None:
|
def toggle_free_draw(self) -> None:
|
||||||
self.exclude_mode = "free" if self.exclude_mode == "rect" else "rect"
|
self.exclude_mode = "free" if self.exclude_mode == "rect" else "rect"
|
||||||
self.image_view.set_mode(self.exclude_mode)
|
self.image_view.set_mode(self.exclude_mode)
|
||||||
|
self.free_draw_action.setChecked(self.exclude_mode == "free")
|
||||||
message_key = "status.free_draw_enabled" if self.exclude_mode == "free" else "status.free_draw_disabled"
|
message_key = "status.free_draw_enabled" if self.exclude_mode == "free" else "status.free_draw_disabled"
|
||||||
self.status_label.setText(self._t(message_key))
|
self.status_label.setText(self._t(message_key))
|
||||||
|
|
||||||
|
def toggle_prefer_dark(self) -> None:
|
||||||
|
self.processor.prefer_dark = not self.processor.prefer_dark
|
||||||
|
self.prefer_dark_action.setChecked(self.processor.prefer_dark)
|
||||||
|
if self.processor.preview_img:
|
||||||
|
self.processor._rebuild_overlay()
|
||||||
|
self._refresh_overlay_only()
|
||||||
|
|
||||||
def clear_exclusions(self) -> None:
|
def clear_exclusions(self) -> None:
|
||||||
self.image_view.clear_shapes()
|
self.image_view.clear_shapes()
|
||||||
self.processor.set_exclusions([])
|
self.processor.set_exclusions([])
|
||||||
|
|
@ -1214,6 +1307,10 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
||||||
self.current_theme = "light" if self.current_theme == "dark" else "dark"
|
self.current_theme = "light" if self.current_theme == "dark" else "dark"
|
||||||
self._apply_theme(self.current_theme)
|
self._apply_theme(self.current_theme)
|
||||||
|
|
||||||
|
def open_app_folder(self) -> None:
|
||||||
|
path = os.getcwd()
|
||||||
|
QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(path))
|
||||||
|
|
||||||
def _apply_theme(self, mode: str) -> None:
|
def _apply_theme(self, mode: str) -> None:
|
||||||
colors = THEMES[mode]
|
colors = THEMES[mode]
|
||||||
self.content.setStyleSheet(f"background-color: {colors['window_bg']};")
|
self.content.setStyleSheet(f"background-color: {colors['window_bg']};")
|
||||||
|
|
@ -1229,7 +1326,20 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
||||||
self.current_label.setStyleSheet(f"color: {colors['text_muted']}; font-weight: 500;")
|
self.current_label.setStyleSheet(f"color: {colors['text_muted']}; font-weight: 500;")
|
||||||
self.current_color_label.setStyleSheet(f"color: {colors['text_dim']};")
|
self.current_color_label.setStyleSheet(f"color: {colors['text_dim']};")
|
||||||
self.more_label.setStyleSheet(f"color: {colors['text_muted']}; font-weight: 500;")
|
self.more_label.setStyleSheet(f"color: {colors['text_muted']}; font-weight: 500;")
|
||||||
self.filename_label.setStyleSheet(f"color: {colors['text']}; font-weight: 600;")
|
self.filename_prefix_label.setStyleSheet(f"color: {colors['text_muted']}; font-weight: 500;")
|
||||||
|
self.filename_suffix_label.setStyleSheet(f"color: {colors['text']}; font-weight: 600;")
|
||||||
|
self.pattern_input.setStyleSheet(
|
||||||
|
f"QLineEdit {{"
|
||||||
|
f" background: rgba(255, 255, 255, 0.05);"
|
||||||
|
f" border: 1px solid {colors['border']};"
|
||||||
|
f" border-radius: 4px;"
|
||||||
|
f" color: {colors['text']};"
|
||||||
|
f" font-weight: 600;"
|
||||||
|
f"}}"
|
||||||
|
f"QLineEdit:focus {{"
|
||||||
|
f" border: 1px solid {colors['accent']};"
|
||||||
|
f"}}"
|
||||||
|
)
|
||||||
self.ratio_label.setStyleSheet(f"color: {colors['highlight']}; font-weight: 600;")
|
self.ratio_label.setStyleSheet(f"color: {colors['highlight']}; font-weight: 600;")
|
||||||
|
|
||||||
# Style MenuBar
|
# Style MenuBar
|
||||||
|
|
@ -1319,12 +1429,22 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
||||||
total = len(self.processor.preview_paths)
|
total = len(self.processor.preview_paths)
|
||||||
position = f" [{self.processor.current_index + 1}/{total}]" if total > 1 else ""
|
position = f" [{self.processor.current_index + 1}/{total}]" if total > 1 else ""
|
||||||
dimensions = f"{width}×{height}"
|
dimensions = f"{width}×{height}"
|
||||||
|
|
||||||
|
# Status label for top right layout
|
||||||
self.status_label.setText(
|
self.status_label.setText(
|
||||||
self._t("status.loaded", name=self._current_image_path.name, dimensions=dimensions, position=position)
|
self._t("status.loaded", name=self._current_image_path.name, dimensions=dimensions, position=position)
|
||||||
)
|
)
|
||||||
self.filename_label.setText(
|
|
||||||
self._t("status.filename_label", name=self._current_image_path.name, dimensions=dimensions, position=position)
|
# Pattern input
|
||||||
)
|
self.pattern_input.setText(self._current_image_path.stem)
|
||||||
|
|
||||||
|
# Update suffix label
|
||||||
|
suffix_text = f"{self._current_image_path.suffix} — {dimensions}{position}"
|
||||||
|
self.filename_suffix_label.setText(suffix_text)
|
||||||
|
|
||||||
|
# Update prefix translation correctly
|
||||||
|
prefix = self._t("status.loaded", name="X", dimensions="Y", position="Z").split("X")[0]
|
||||||
|
self.filename_prefix_label.setText(prefix)
|
||||||
self.ratio_label.setText(self.processor.stats.summary(self._t))
|
self.ratio_label.setText(self.processor.stats.summary(self._t))
|
||||||
|
|
||||||
def _refresh_overlay_only(self) -> None:
|
def _refresh_overlay_only(self) -> None:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue