628 lines
19 KiB
Python
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)
|