443 lines
13 KiB
Python
443 lines
13 KiB
Python
# -*- 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")
|