Files
aza/AzA march 2026/aza_mini_record_window.py
2026-06-13 22:47:31 +02:00

443 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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("<ButtonPress-1>", _on_press)
widget.bind("<B1-Motion>", _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(
"<Button-1>",
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("<Button-1>", _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("<Button-1>", _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")