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

628 lines
19 KiB
Python

# -*- 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("<Button-3>", _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("<Button-1>", _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("<Button-1>", _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)