"""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: system = platform.system() self.use_native_titlebar = system == "Windows" 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}" if self.use_native_titlebar: self._is_maximized = False self.root.geometry(self._window_geometry) self.root.after(0, lambda: self.root.state("zoomed")) else: self.root.overrideredirect(True) self._is_maximized = True self.root.geometry(f"{screen_width}x{screen_height}+0+0") self.root.configure(bg="#f2f2f7") 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 window chrome based on platform.""" try: if self.use_native_titlebar: if platform.system() == "Windows": self.root.after(0, self._ensure_taskbar_entry) initial_mode = getattr(self, "theme", "light") self.root.after(0, lambda: self._apply_os_titlebar_theme(initial_mode)) self.root.bind( "", lambda _e: self._apply_os_titlebar_theme(getattr(self, "theme", "light")), add="+", ) self.root.bind("", lambda _e: self._ensure_taskbar_entry(), add="+") else: self.root.bind("", self._restore_borderless) self.root.after(0, self._restore_borderless) self.root.after(0, self._ensure_taskbar_entry) except Exception: self.root.overrideredirect(True) def _restore_borderless(self, _event=None) -> None: try: self.root.overrideredirect(True) except Exception: pass def _apply_os_titlebar_theme(self, mode: str | None = None) -> None: try: if platform.system() != "Windows": return hwnd = self.root.winfo_id() if not hwnd: self.root.after(50, lambda: self._apply_os_titlebar_theme(mode)) return try: dwmapi = ctypes.windll.dwmapi # type: ignore[attr-defined] except Exception: return attribute = ctypes.c_uint(20) # DWMWA_USE_IMMERSIVE_DARK_MODE value = ctypes.c_int(1 if (mode or self.theme) == "dark" else 0) dwmapi.DwmSetWindowAttribute( hwnd, attribute, ctypes.byref(value), ctypes.sizeof(value), ) except Exception: pass def start_app() -> None: """Entry point used by the CLI script.""" root = tk.Tk() app = ICRAApp(root) root.mainloop() __all__ = ["ICRAApp", "start_app"]