diff --git a/.gitignore b/.gitignore
index 087c8ca..3d59451 100644
Binary files a/.gitignore and b/.gitignore differ
diff --git a/README.md b/README.md
index 99cf33f..0c4e604 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Interactive Color Range Analyzer is being reimagined with a PySide6 user interface.
- This branch focuses on building a native desktop shell with modern window controls before porting the colour-analysis features.
+ This branch focuses on building a native desktop shell with modern window controls before porting the color-analysis features.
diff --git a/app/__init__.py b/app/__init__.py
index f3c94c3..24a9a05 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -2,14 +2,8 @@
from __future__ import annotations
-try: # Legacy Tk support remains optional
- from .app import ICRAApp, start_app as start_tk_app # type: ignore[attr-defined]
-except Exception: # pragma: no cover
- ICRAApp = None # type: ignore[assignment]
- start_tk_app = None # type: ignore[assignment]
-
from .qt import create_application as create_qt_app, run as run_qt_app
start_app = run_qt_app
-__all__ = ["ICRAApp", "start_tk_app", "create_qt_app", "run_qt_app", "start_app"]
+__all__ = ["create_qt_app", "run_qt_app", "start_app"]
diff --git a/app/app.py b/app/app.py
deleted file mode 100644
index bbfe85c..0000000
--- a/app/app.py
+++ /dev/null
@@ -1,176 +0,0 @@
-"""Application composition root."""
-
-from __future__ import annotations
-
-import ctypes
-import platform
-import tkinter as tk
-from importlib import resources
-
-from .gui import ColorPickerMixin, ExclusionMixin, ThemeMixin, UIBuilderMixin
-from .i18n import I18nMixin
-from .logic import DEFAULTS, LANGUAGE, RESET_EXCLUSIONS_ON_IMAGE_CHANGE, ImageProcessingMixin, ResetMixin
-
-
-class ICRAApp(
- I18nMixin,
- ThemeMixin,
- UIBuilderMixin,
- ImageProcessingMixin,
- ExclusionMixin,
- ColorPickerMixin,
- ResetMixin,
-):
- """Tkinter based application for isolating configurable colour ranges."""
-
- def __init__(self, root: tk.Tk):
- self.root = root
- self.init_i18n(LANGUAGE)
- self.root.title(self._t("app.title"))
- self._setup_window()
-
- # Theme and styling
- self.init_theme()
-
- # Tkinter state variables
- self.DEFAULTS = DEFAULTS.copy()
- self.hue_min = tk.DoubleVar(value=self.DEFAULTS["hue_min"])
- self.hue_max = tk.DoubleVar(value=self.DEFAULTS["hue_max"])
- self.sat_min = tk.DoubleVar(value=self.DEFAULTS["sat_min"])
- self.val_min = tk.DoubleVar(value=self.DEFAULTS["val_min"])
- self.val_max = tk.DoubleVar(value=self.DEFAULTS["val_max"])
- self.alpha = tk.IntVar(value=self.DEFAULTS["alpha"])
- self.ref_hue = None
-
- # Debounce for heavy preview updates
- self.update_delay_ms = 400
- self._update_job = None
-
- # Exclusion rectangles (preview coordinates)
- self.exclude_shapes: list[dict[str, object]] = []
- self._rubber_start = None
- self._rubber_id = None
- self._stroke_preview_id = None
- self.exclude_mode = "rect"
- self.reset_exclusions_on_switch = RESET_EXCLUSIONS_ON_IMAGE_CHANGE
- self._exclude_mask = None
- self._exclude_mask_dirty = True
- self._exclude_mask_px = None
- self._exclude_canvas_ids: list[int] = []
- self._current_stroke: list[tuple[int, int]] | None = None
- self.free_draw_width = 14
- self.pick_mode = False
-
- # Image references
- self.image_path = None
- self.orig_img = None
- self.preview_img = None
- self.preview_tk = None
- self.overlay_tk = None
- self.image_paths = []
- self.current_image_index = -1
-
- # Build UI
- self.setup_ui()
- self._init_copy_menu()
- self.bring_to_front()
-
- def _setup_window(self) -> None:
- screen_width = self.root.winfo_screenwidth()
- screen_height = self.root.winfo_screenheight()
- default_width = int(screen_width * 0.8)
- default_height = int(screen_height * 0.8)
- default_x = (screen_width - default_width) // 2
- default_y = (screen_height - default_height) // 4
- self._window_geometry = f"{default_width}x{default_height}+{default_x}+{default_y}"
- self._is_maximized = True
- self._use_overrideredirect = True
- self.root.geometry(f"{screen_width}x{screen_height}+0+0")
- self.root.configure(bg="#f2f2f7")
- try:
- self.root.overrideredirect(True)
- except Exception:
- try:
- self.root.attributes("-type", "splash")
- except Exception:
- pass
- self._window_icon_ref = None
- self._apply_window_icon()
- self._init_window_chrome()
-
- def _ensure_taskbar_entry(self) -> None:
- """Force the borderless window to show up in the Windows taskbar."""
- try:
- if platform.system() != "Windows":
- return
- hwnd = self.root.winfo_id()
- if not hwnd:
- self.root.after(50, self._ensure_taskbar_entry)
- return
-
- GWL_EXSTYLE = -20
- WS_EX_TOOLWINDOW = 0x00000080
- WS_EX_APPWINDOW = 0x00040000
- SWP_NOSIZE = 0x0001
- SWP_NOMOVE = 0x0002
- SWP_NOZORDER = 0x0004
- SWP_FRAMECHANGED = 0x0020
-
- user32 = ctypes.windll.user32 # type: ignore[attr-defined]
- shell32 = ctypes.windll.shell32 # type: ignore[attr-defined]
-
- style = user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
- new_style = (style & ~WS_EX_TOOLWINDOW) | WS_EX_APPWINDOW
- if new_style != style:
- user32.SetWindowLongW(hwnd, GWL_EXSTYLE, new_style)
- user32.SetWindowPos(
- hwnd,
- 0,
- 0,
- 0,
- 0,
- 0,
- SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED,
- )
-
- app_id = ctypes.c_wchar_p("ICRA.App")
- shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
- except Exception:
- pass
-
- def _apply_window_icon(self) -> None:
- try:
- icon_resource = resources.files("app.assets").joinpath("logo.png")
- with resources.as_file(icon_resource) as icon_path:
- icon = tk.PhotoImage(file=str(icon_path))
- self.root.iconphoto(False, icon)
- self._window_icon_ref = icon
- except Exception:
- self._window_icon_ref = None
-
- def _init_window_chrome(self) -> None:
- """Configure a borderless window while retaining a taskbar entry."""
- try:
- self.root.bind("