# -*- coding: utf-8 -*- """AzA Mini-Diktat — gleicher Aufbau wie AzA Mini (Logo3, Weiterfahren, Status).""" from __future__ import annotations import os import sys import tkinter as tk from typing import Any from aza_mini_record_window import ( _BG_ACTIVE, _BG_IDLE, _BTN_BLUE, _CHROME_BG, _FF, _MINI_WIN_H, _MINI_WIN_MIN_H, _MINI_WIN_MIN_W, _MINI_WIN_W, _bind_window_drag, _place_mini_window, ) _MAIN_LOGO_PX = 82 _MINI_LOGO_PX = int(_MAIN_LOGO_PX * 1.3) _LOGO_CANDIDATES = ("Logo3.png", "logo3.png") # Eigene Breite: Weiterfahren + Titel + sichtbares X (236px Mini-Aufnahme reicht nicht). _MINI_DICTAT_WIN_W = 268 _MINI_DICTAT_MIN_W = 248 def _mini_win(app: Any) -> tk.Toplevel | None: win = getattr(app, "_mini_diktat_win", None) if win is None: return None try: return win if win.winfo_exists() else None except Exception: return None def is_mini_diktat_window_open(app: Any) -> bool: return _mini_win(app) is not None def _diktat_target(app: Any) -> tk.Misc | None: win = getattr(app, "_mini_diktat_target_win", None) if win is None: return None try: return win if win.winfo_exists() else None except Exception: return 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: photo = getattr(app, "_mini_diktat_photo", None) if photo is not None: return photo logo_path = _resolve_asset_path(_LOGO_CANDIDATES) photo = _load_logo_photo(logo_path) app._mini_diktat_photo = photo app._mini_diktat_logo_path = logo_path return photo def _diktat_phase(diktat_win: tk.Misc | None) -> str: if diktat_win is None: return "idle" fn = getattr(diktat_win, "_aza_diktat_phase", None) if callable(fn): try: phase = str(fn() or "idle").strip().lower() if phase in ("idle", "recording", "finalizing", "error"): return phase except Exception: pass legacy = getattr(diktat_win, "_aza_diktat_is_recording", None) if callable(legacy): try: return "recording" if legacy() else "idle" except Exception: pass return "idle" def _is_diktat_recording(diktat_win: tk.Misc | None) -> bool: return _diktat_phase(diktat_win) == "recording" def _diktat_status_text(diktat_win: tk.Misc | None) -> str: if diktat_win is None: return "Bereit" status_var = getattr(diktat_win, "_aza_diktat_status_var", None) if status_var is not None: try: return str(status_var.get() or "Bereit").strip() or "Bereit" except Exception: pass return "Aufnahme läuft …" if _is_diktat_recording(diktat_win) else "Bereit" 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_diktat_chrome_hdr", "_mini_diktat_logo_bg", "_mini_diktat_status_lbl"): w = getattr(app, attr, None) if w is not None: try: w.configure(bg=bg if attr != "_mini_diktat_chrome_hdr" else _CHROME_BG) except Exception: pass logo_lbl = getattr(app, "_mini_diktat_logo_lbl", None) if logo_lbl is not None: try: logo_lbl.configure(bg=bg) except Exception: pass def sync_mini_diktat_window(app: Any, *, diktat_win: tk.Misc | None = None) -> None: """Aktualisiert Mini-Diktat-Anzeige (Logo, Hintergrund, Status).""" win = _mini_win(app) if win is None: return target = diktat_win or _diktat_target(app) try: recording = _diktat_phase(target) == "recording" _apply_mini_bg(app, recording) photo = _ensure_mini_logo(app) logo_lbl = getattr(app, "_mini_diktat_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_diktat_status_var", None) if status_var is not None: status_var.set(_diktat_status_text(target)) # Weiterfahren/Stoppen: Text UND Command muessen zum echten Zustand passen. btn = getattr(app, "_mini_diktat_weiter_btn", None) stop_cmd = getattr(app, "_mini_diktat_on_stop", None) weiter_cmd = getattr(app, "_mini_diktat_on_weiterfahren", None) phase = _diktat_phase(target) if btn is not None: try: if phase == "finalizing": btn.configure(text="Abschluss…", state="disabled", command=lambda: None) elif phase == "recording": btn.configure( text="Stoppen", state="normal", command=stop_cmd if callable(stop_cmd) else (lambda: None), ) else: btn.configure( text="Weiterfahren", state="normal", command=weiter_cmd if callable(weiter_cmd) else (lambda: None), ) except Exception: pass logo_lbl = getattr(app, "_mini_diktat_logo_lbl", None) if logo_lbl is not None: try: if phase in ("recording", "finalizing"): logo_lbl.configure(cursor="arrow") else: logo_lbl.configure(cursor="hand2") except Exception: pass app._mini_diktat_logo_active = phase not in ("recording", "finalizing") except Exception: pass def _paste_into_diktat_text(diktat_win: tk.Misc) -> None: txt = getattr(diktat_win, "_aza_diktat_text", None) if txt is None: return try: clip = str(diktat_win.clipboard_get() or "") except tk.TclError: return if not str(clip).strip(): return try: txt.configure(state="normal") try: if txt.tag_ranges(tk.SEL): txt.delete(tk.SEL_FIRST, tk.SEL_LAST) except Exception: pass try: idx = txt.index(tk.INSERT) except Exception: idx = tk.END txt.insert(idx, clip) except Exception: try: txt.configure(state="normal") txt.insert(tk.END, clip) except Exception: pass def _bind_mini_diktat_context_paste(app: Any, mini_win: tk.Toplevel, diktat_win: tk.Misc, *widgets: tk.Misc) -> None: menu = tk.Menu(mini_win, tearoff=0) def _do_paste() -> None: _paste_into_diktat_text(diktat_win) sync_mini_diktat_window(app, diktat_win=diktat_win) menu.add_command(label="Einfügen", command=_do_paste) def _popup(evt) -> None: try: menu.tk_popup(evt.x_root, evt.y_root) finally: try: menu.grab_release() except Exception: pass for w in widgets: if w is not None: w.bind("", _popup) def _raise_mini_window_topmost(win: tk.Toplevel) -> None: 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 _save_diktat_restore_state(diktat_win: tk.Misc) -> None: try: diktat_win.update_idletasks() diktat_win._mini_restore_geometry = diktat_win.geometry() except Exception: diktat_win._mini_restore_geometry = getattr(diktat_win, "_mini_restore_geometry", "") or "" def _restore_diktat_window( app: Any, cursor_x: int | None = None, cursor_y: int | None = None, ) -> None: diktat_win = _diktat_target(app) if diktat_win is None: return try: if cursor_x is not None and cursor_y is not None: from aza_ui_helpers import restore_tool_window_at_cursor restore_tool_window_at_cursor( diktat_win, int(cursor_x), int(cursor_y), anchor_widget=getattr(diktat_win, "_aza_mini_diktat_btn", None), ) return except Exception: pass try: diktat_win.deiconify() if bool(getattr(diktat_win, "_tool_pinned", False)): from aza_ui_helpers import apply_tool_window_pin apply_tool_window_pin(diktat_win, True) diktat_win.lift() diktat_win.focus_force() except Exception: try: diktat_win.deiconify() except Exception: pass def close_mini_diktat_window( app: Any, *, restore_diktat: bool = True, cursor_x: int | None = None, cursor_y: int | None = None, _skip_stop: bool = False, ) -> None: """Schliesst Mini-Diktat und stellt das normale Diktatfenster wieder her.""" win = _mini_win(app) target = _diktat_target(app) if not _skip_stop and target is not None: phase = _diktat_phase(target) if phase == "recording": toggle_fn = getattr(target, "_aza_diktat_toggle", None) if callable(toggle_fn): try: toggle_fn() except Exception: pass def _wait_recording_stopped() -> None: p = _diktat_phase(target) if p in ("recording", "finalizing"): try: app.after(150, _wait_recording_stopped) except Exception: pass return close_mini_diktat_window( app, restore_diktat=restore_diktat, cursor_x=cursor_x, cursor_y=cursor_y, _skip_stop=True, ) try: app.after(150, _wait_recording_stopped) except Exception: pass return if phase == "finalizing": def _wait_finalize() -> None: if _diktat_phase(target) == "finalizing": try: app.after(150, _wait_finalize) except Exception: pass return close_mini_diktat_window( app, restore_diktat=restore_diktat, cursor_x=cursor_x, cursor_y=cursor_y, _skip_stop=True, ) try: app.after(150, _wait_finalize) except Exception: pass return if win is None: if restore_diktat: _restore_diktat_window(app, cursor_x, cursor_y) return if restore_diktat: 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_diktat_window(app, cx, cy) for attr in ( "_mini_diktat_win", "_mini_diktat_logo_lbl", "_mini_diktat_logo_bg", "_mini_diktat_chrome_hdr", "_mini_diktat_status_lbl", "_mini_diktat_photo", "_mini_diktat_status_var", "_mini_diktat_target_win", ): try: setattr(app, attr, None) except Exception: pass try: _clear_mini_window_topmost(win) win.destroy() except Exception: pass def open_mini_diktat_window( app: Any, diktat_win: tk.Misc, *, anchor_x: int | None = None, anchor_y: int | None = None, ) -> None: """Öffnet Mini-Diktat (Singleton) und verbirgt das normale Diktatfenster.""" if diktat_win is None: return try: if not diktat_win.winfo_exists(): return except Exception: return restore_fn = getattr(diktat_win, "_aza_restore_diktat_content", None) is_min = getattr(diktat_win, "_aza_is_minimized", None) if callable(is_min) and is_min() and callable(restore_fn): try: restore_fn() except Exception: pass app._mini_diktat_target_win = diktat_win 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_DICTAT_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 sync_mini_diktat_window(app, diktat_win=diktat_win) return _save_diktat_restore_state(diktat_win) try: diktat_win.withdraw() except Exception: pass win = tk.Toplevel(app) app._mini_diktat_win = win win.title("Mini-Diktat") 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_DICTAT_MIN_W, _MINI_WIN_MIN_H) _place_mini_window(win, _MINI_DICTAT_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_diktat_chrome_hdr = chrome_hdr _bind_window_drag(chrome_hdr, win) close_btn = tk.Button( chrome_hdr, text="X", command=lambda: close_mini_diktat_window(app), font=(_FF, 9, "bold"), width=2, bg=_CHROME_BG, fg="#8B3A3A", activebackground="#E8D0D0", activeforeground="#6B1A1A", relief="flat", bd=0, cursor="hand2", padx=2, pady=0, ) close_btn.pack(side="right", padx=(2, 6), pady=3) app._mini_diktat_close_btn = close_btn try: from aza_ui_helpers import ToolTip ToolTip(close_btn, "Mini-Diktat schliessen") except Exception: pass def _on_mini_stop() -> None: if getattr(app, "_diktat_neu_busy", False): return stop_fn = getattr(diktat_win, "_aza_diktat_toggle", None) if callable(stop_fn): stop_fn() sync_mini_diktat_window(app, diktat_win=diktat_win) def _on_mini_weiterfahren() -> None: if getattr(app, "_diktat_neu_busy", False): return if _diktat_phase(diktat_win) == "recording": _on_mini_stop() return fn = getattr(diktat_win, "_aza_diktat_weiterfahren", None) or getattr( diktat_win, "_aza_diktat_korrigieren", None ) if callable(fn): fn() sync_mini_diktat_window(app, diktat_win=diktat_win) app._mini_diktat_on_stop = _on_mini_stop app._mini_diktat_on_weiterfahren = _on_mini_weiterfahren btn_weiter = tk.Button( chrome_hdr, text="Weiterfahren", command=_on_mini_weiterfahren, font=(_FF, 8, "bold"), bg=_BTN_BLUE, fg="#FFFFFF", relief="flat", bd=0, padx=8, pady=3, cursor="hand2", ) btn_weiter.pack(side="left", padx=(8, 4), pady=5) app._mini_diktat_weiter_btn = btn_weiter try: from aza_ui_helpers import ToolTip ToolTip(btn_weiter, "Aufnahme anhängen / während Aufnahme: Stoppen") except Exception: pass drag_hint = tk.Label( chrome_hdr, text="Mini-Diktat", font=(_FF, 8), bg=_CHROME_BG, fg="#5A7A8F", cursor="fleur", ) drag_hint.pack(side="left", expand=True, fill="x") _bind_window_drag(drag_hint, win) logo_bg = tk.Frame(win, bg=_BG_IDLE) logo_bg.pack(side="top", fill="both", expand=True, padx=10, pady=6) app._mini_diktat_logo_bg = logo_bg idle_photo = _ensure_mini_logo(app) def _on_logo_click(_evt=None): if not getattr(app, "_mini_diktat_logo_active", True): if _diktat_phase(diktat_win) == "recording": _on_mini_stop() return fn = getattr(diktat_win, "_aza_diktat_neu_von_logo", None) if not callable(fn): fn = getattr(diktat_win, "_aza_diktat_toggle", None) if callable(fn): fn() sync_mini_diktat_window(app, diktat_win=diktat_win) 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_diktat_logo_lbl = logo_lbl try: from aza_ui_helpers import ToolTip ToolTip(logo_lbl, "Neues Diktat starten") except Exception: pass else: logo_lbl = tk.Label( logo_bg, text="Diktat", font=(_FF, 22, "bold"), bg=_BG_IDLE, fg="#2E6F8F", cursor="hand2", ) logo_lbl.pack(expand=True) logo_lbl.bind("", _on_logo_click) app._mini_diktat_logo_lbl = logo_lbl status_var = tk.StringVar(value=_diktat_status_text(diktat_win)) app._mini_diktat_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_diktat_status_lbl = status_lbl _bind_mini_diktat_context_paste(app, win, diktat_win, logo_bg, status_lbl, win) sync_mini_diktat_window(app, diktat_win=diktat_win)