"""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("", self._restore_borderless) self.root.after(0, self._restore_borderless) self.root.after(0, self._ensure_taskbar_entry) except Exception: pass def _restore_borderless(self, _event=None) -> None: try: if self._use_overrideredirect: self.root.overrideredirect(True) self._ensure_taskbar_entry() 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"]