# -*- coding: utf-8 -*- """AzA Mini-Aufnahmefenster — kompaktes Widget ohne Windows-Titelleiste.""" from __future__ import annotations import os import sys import tkinter as tk from typing import Any _FF = "Segoe UI" _BG_IDLE = "#effeff" _BG_ACTIVE = "#ddfaff" _BTN_BLUE = "#5B8DB3" _CHROME_BG = "#F0F8FC" _MAIN_LOGO_PX = 82 _MINI_LOGO_PX = int(_MAIN_LOGO_PX * 1.3) _MINI_WIN_W, _MINI_WIN_H = 236, 210 _MINI_WIN_MIN_W, _MINI_WIN_MIN_H = 220, 190 _LOGO_CANDIDATES = ("logo7.png", "Logo7.png") def _mini_win(app: Any) -> tk.Toplevel | None: win = getattr(app, "_mini_record_win", None) if win is None: return None try: return win if win.winfo_exists() else None except Exception: return None def is_mini_record_window_open(app: Any) -> bool: return _mini_win(app) is not None def _app_base_dirs() -> list[str]: dirs: list[str] = [] try: if getattr(sys, "frozen", False): dirs.append(getattr(sys, "_MEIPASS", "") or "") except Exception: pass dirs.append(os.path.dirname(os.path.abspath(__file__))) return [d for d in dirs if d] def _resolve_asset_path(candidates: tuple[str, ...]) -> str | None: for base in _app_base_dirs(): assets_dir = os.path.join(base, "assets") if not os.path.isdir(assets_dir): continue try: names = {n.lower(): n for n in os.listdir(assets_dir)} except Exception: names = {} for cand in candidates: key = cand.lower() if key in names: return os.path.join(assets_dir, names[key]) for cand in candidates: path = os.path.join(assets_dir, cand) if os.path.isfile(path): return path return None def _load_logo_photo(path: str | None) -> Any: if not path or not os.path.isfile(path): return None try: from PIL import Image, ImageTk img = Image.open(path).convert("RGBA") img = img.resize((_MINI_LOGO_PX, _MINI_LOGO_PX), Image.Resampling.LANCZOS) return ImageTk.PhotoImage(img) except Exception: return None def _ensure_mini_logo(app: Any) -> Any | None: """logo7 einmal laden — gleiche Referenz für idle und recording.""" photo = getattr(app, "_mini_record_photo_idle", None) if photo is not None: return photo logo_path = _resolve_asset_path(_LOGO_CANDIDATES) if not logo_path: logo_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logo.png") photo = _load_logo_photo(logo_path) app._mini_record_photo_idle = photo app._mini_record_photo_active = photo app._mini_record_logo_path = logo_path return photo def _raise_mini_window_topmost(win: tk.Toplevel) -> None: """Mini-Fenster dauerhaft in den Vordergrund (Always-on-top wie Pin/Nadel).""" try: win.deiconify() win.attributes("-topmost", True) win.lift() win.update_idletasks() win.focus_force() except Exception: pass def _clear_mini_window_topmost(win: tk.Toplevel) -> None: try: if win.winfo_exists(): win.attributes("-topmost", False) except Exception: pass def _apply_mini_bg(app: Any, recording: bool) -> None: bg = _BG_ACTIVE if recording else _BG_IDLE win = _mini_win(app) if win is None: return try: win.configure(bg=bg) except Exception: pass for attr in ("_mini_record_chrome_hdr", "_mini_record_logo_bg", "_mini_record_status_lbl"): w = getattr(app, attr, None) if w is not None: try: w.configure(bg=bg if attr != "_mini_record_chrome_hdr" else _CHROME_BG) except Exception: pass logo_lbl = getattr(app, "_mini_record_logo_lbl", None) if logo_lbl is not None: try: logo_lbl.configure(bg=bg) except Exception: pass def sync_mini_record_window(app: Any, status: str | None = None) -> None: """Aktualisiert Mini-Fenster-Anzeige (Logo, Hintergrund, Status).""" win = _mini_win(app) if win is None: return try: recording = bool(getattr(app, "is_recording", False)) _apply_mini_bg(app, recording) photo = _ensure_mini_logo(app) logo_lbl = getattr(app, "_mini_record_logo_lbl", None) if logo_lbl is not None and photo is not None: try: logo_lbl.configure(image=photo) except Exception: pass status_var = getattr(app, "_mini_record_status_var", None) if status_var is not None: if status is not None: status_var.set(status) elif recording: mode = getattr(app, "_recording_mode", "new") status_var.set( "Korrektur-Aufnahme läuft …" if mode == "append" else "Aufnahme läuft …" ) elif not status_var.get() or status_var.get().endswith("…"): status_var.set("Bereit") except Exception: pass def close_mini_record_window( app: Any, *, restore_main: bool = True, cursor_x: int | None = None, cursor_y: int | None = None, ) -> None: """Schliesst Mini-Fenster und stellt das Hauptfenster wieder her.""" win = _mini_win(app) if win is None: if restore_main: _restore_main_window(app, cursor_x, cursor_y) return if bool(getattr(app, "is_recording", False)): try: stop_fn = getattr(app, "_stop_and_process_recording", None) if callable(stop_fn): stop_fn() else: toggle = getattr(app, "toggle_record", None) if callable(toggle): toggle() except Exception: pass if restore_main: cx, cy = cursor_x, cursor_y if cx is None or cy is None: try: cx = int(win.winfo_pointerx()) cy = int(win.winfo_pointery()) except Exception: cx = cy = None _restore_main_window(app, cx, cy) for attr in ( "_mini_record_win", "_mini_record_status_var", "_mini_record_logo_lbl", "_mini_record_logo_bg", "_mini_record_chrome_hdr", "_mini_record_status_lbl", "_mini_record_photo_idle", "_mini_record_photo_active", ): try: setattr(app, attr, None) except Exception: pass try: _clear_mini_window_topmost(win) win.destroy() except Exception: pass def _restore_main_window( app: Any, cursor_x: int | None = None, cursor_y: int | None = None, ) -> None: try: if cursor_x is not None and cursor_y is not None: from aza_ui_helpers import restore_main_window_at_cursor restore_main_window_at_cursor(app, int(cursor_x), int(cursor_y)) return except Exception: pass try: app.deiconify() app.lift() if hasattr(app, "_apply_main_topmost_state"): app._apply_main_topmost_state() else: from aza_ui_helpers import bring_tool_window_to_front bring_tool_window_to_front(app) try: app.focus_force() except Exception: pass except Exception: try: app.deiconify() except Exception: pass def _save_main_window_restore_state(app: Any) -> None: """Merkt Geometrie/Zoom vor dem Verbergen des Hauptfensters.""" try: app.update_idletasks() app._mini_restore_was_zoomed = str(app.state()) == "zoomed" app._mini_restore_geometry = app.geometry() except Exception: app._mini_restore_was_zoomed = False app._mini_restore_geometry = getattr(app, "_mini_restore_geometry", "") or "" def _bind_window_drag(widget: tk.Misc, win: tk.Toplevel) -> None: """Verschieben per Drag auf Header-/Freifläche (nicht Logo).""" def _on_press(event): win._mini_drag_off_x = event.x_root - win.winfo_x() # type: ignore[attr-defined] win._mini_drag_off_y = event.y_root - win.winfo_y() # type: ignore[attr-defined] def _on_motion(event): try: off_x = win._mini_drag_off_x # type: ignore[attr-defined] off_y = win._mini_drag_off_y # type: ignore[attr-defined] except Exception: return x = event.x_root - off_x y = event.y_root - off_y try: win.geometry(f"+{x}+{y}") except Exception: pass widget.bind("", _on_press) widget.bind("", _on_motion) def _place_mini_window( win: tk.Toplevel, width: int, height: int, *, anchor_x: int | None = None, anchor_y: int | None = None, ) -> None: from aza_ui_helpers import clamp_window_position, center_tool_window if anchor_x is None or anchor_y is None: center_tool_window(win, width, height, parent=None, bring_to_front=False, y_ratio=0.08) return try: win.update_idletasks() sw = win.winfo_screenwidth() sh = win.winfo_screenheight() except Exception: sw, sh = 1920, 1080 x, y = clamp_window_position(anchor_x + 4, anchor_y + 4, width, height, screen_w=sw, screen_h=sh) try: win.geometry(f"{width}x{height}+{x}+{y}") except Exception: center_tool_window(win, width, height, parent=None, bring_to_front=False, y_ratio=0.08) def open_mini_record_window( app: Any, *, anchor_x: int | None = None, anchor_y: int | None = None, ) -> None: """Öffnet das Mini-Aufnahmefenster (Singleton) und verbirgt das Hauptfenster.""" existing = _mini_win(app) if existing is not None: if anchor_x is not None and anchor_y is not None: try: sh = existing.winfo_screenheight() h = min(_MINI_WIN_H, max(_MINI_WIN_MIN_H, sh - 80)) except Exception: h = _MINI_WIN_H _place_mini_window(existing, _MINI_WIN_W, h, anchor_x=anchor_x, anchor_y=anchor_y) _raise_mini_window_topmost(existing) try: app.after(80, lambda w=existing: _raise_mini_window_topmost(w)) except Exception: pass return try: _save_main_window_restore_state(app) app.withdraw() except Exception: pass win = tk.Toplevel(app) app._mini_record_win = win win.title("AzA Mini") win.configure(bg=_BG_IDLE) win.resizable(False, False) try: win.overrideredirect(True) except Exception: pass try: sh = win.winfo_screenheight() h = min(_MINI_WIN_H, max(_MINI_WIN_MIN_H, sh - 80)) except Exception: h = _MINI_WIN_H win.minsize(_MINI_WIN_MIN_W, _MINI_WIN_MIN_H) _place_mini_window(win, _MINI_WIN_W, h, anchor_x=anchor_x, anchor_y=anchor_y) _raise_mini_window_topmost(win) try: app.after(80, lambda w=win: _raise_mini_window_topmost(w)) except Exception: pass if hasattr(app, "_register_window"): try: app._register_window(win) except Exception: pass chrome_hdr = tk.Frame(win, bg=_CHROME_BG, highlightbackground="#C8DCE8", highlightthickness=1) chrome_hdr.pack(side="top", fill="x") app._mini_record_chrome_hdr = chrome_hdr _bind_window_drag(chrome_hdr, win) def _on_korrigieren(): fn = getattr(app, "_toggle_record_append", None) if callable(fn): fn() tk.Button( chrome_hdr, text="Korrigieren", command=_on_korrigieren, font=(_FF, 8, "bold"), bg=_BTN_BLUE, fg="#FFFFFF", relief="flat", bd=0, padx=8, pady=3, cursor="hand2", ).pack(side="left", padx=(8, 4), pady=5) drag_hint = tk.Label( chrome_hdr, text="AzA Mini", font=(_FF, 8), bg=_CHROME_BG, fg="#5A7A8F", cursor="fleur", ) drag_hint.pack(side="left", expand=True) _bind_window_drag(drag_hint, win) close_lbl = tk.Label( chrome_hdr, text="×", font=(_FF, 12), bg=_CHROME_BG, fg="#8B3A3A", cursor="hand2", padx=6, ) close_lbl.pack(side="right", padx=(4, 8), pady=2) close_lbl.bind( "", lambda e: close_mini_record_window(app, cursor_x=e.x_root, cursor_y=e.y_root), ) logo_bg = tk.Frame(win, bg=_BG_IDLE) logo_bg.pack(side="top", fill="both", expand=True, padx=10, pady=6) app._mini_record_logo_bg = logo_bg idle_photo = _ensure_mini_logo(app) def _on_logo_click(_evt=None): fn = getattr(app, "toggle_record", None) if callable(fn): fn() if idle_photo is not None: logo_lbl = tk.Label(logo_bg, image=idle_photo, bg=_BG_IDLE, cursor="hand2") logo_lbl.pack(expand=True) logo_lbl.bind("", _on_logo_click) app._mini_record_logo_lbl = logo_lbl else: logo_lbl = tk.Label( logo_bg, text="AzA", font=(_FF, 22, "bold"), bg=_BG_IDLE, fg="#2E6F8F", cursor="hand2", ) logo_lbl.pack(expand=True) logo_lbl.bind("", _on_logo_click) app._mini_record_logo_lbl = logo_lbl status_var = tk.StringVar(value="Bereit") app._mini_record_status_var = status_var status_lbl = tk.Label( win, textvariable=status_var, font=(_FF, 8), bg=_BG_IDLE, fg="#4A6070", pady=4, ) status_lbl.pack(side="bottom", fill="x") app._mini_record_status_lbl = status_lbl sync_mini_record_window(app, status="Bereit")