"""Application composition root.""" from __future__ import annotations import tkinter as tk 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: self.root.overrideredirect(True) 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.root.geometry(f"{screen_width}x{screen_height}+0+0") self.root.configure(bg="#f2f2f7") self.root.bind("", self._restore_borderless) self.root.after(0, self._ensure_taskbar_entry) def _restore_borderless(self, _event=None) -> None: try: self.root.overrideredirect(True) self._ensure_taskbar_entry() except Exception: pass def _ensure_taskbar_entry(self) -> None: """Force the borderless window to show up in the Windows taskbar.""" try: import platform if platform.system() != "Windows": return import ctypes hwnd = self.root.winfo_id() if not hwnd: 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 start_app() -> None: """Entry point used by the CLI script.""" root = tk.Tk() app = ICRAApp(root) root.mainloop() __all__ = ["ICRAApp", "start_app"]