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

785 lines
32 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 -*-
"""
AzaDiktatMixin Diktat-Fenster (nur Transkription, keine KG).
"""
import os
import threading
import wave
import tkinter as tk
from tkinter import messagebox
from tkinter.scrolledtext import ScrolledText
from aza_persistence import (
load_diktat_geometry,
save_diktat_geometry,
save_to_ablage,
_win_clipboard_set,
sanitize_markdown_for_plain_text,
is_autocopy_after_diktat_enabled,
is_global_right_click_paste_enabled,
save_autocopy_prefs,
)
from aza_ui_helpers import (
center_window,
add_resize_grip,
add_font_scale_control,
add_text_font_size_control,
add_tool_pin_button,
apply_tool_window_pin,
RoundedButton,
)
from aza_audio import AudioRecorder
class AzaDiktatMixin:
"""Mixin für das Diktat-Fenster (nur Aufnahme + Transkription)."""
def open_diktat_window(self):
"""Unabhängiges Fenster: nur Diktat aufnehmen und transkribieren (keine KG). Text wird automatisch kopiert."""
if not self.ensure_ready():
return
DIKTAT_MIN_W, DIKTAT_MIN_H = 420, 380
_BG = "#E8F4FA"
_HDR_BG = "#1A4D6D"
_HDR_FG = "#FFFFFF"
_CARD_BG = "#FFFFFF"
_CARD_BD = "#C8D8E8"
_TEXT = "#1A3D55"
_TEXT_SUB = "#607890"
_BTN_PRI = "#1A4D6D"
_BTN_PRI_H = "#4A7A9E"
_BTN_SEC = "#FFFFFF"
_BTN_SEC_BD = "#C8D8E8"
_STATUS_REC = "#C86B2A"
win = tk.Toplevel(self)
win.title("Diktat")
win.minsize(DIKTAT_MIN_W, DIKTAT_MIN_H)
win.configure(bg=_BG)
self._diktat_window = win
self._register_window(win)
# Fensterposition: gespeichert laden oder zentrieren
saved_geom = load_diktat_geometry()
if saved_geom:
# Gespeicherte Position verwenden (Position wird beibehalten!)
win.geometry(saved_geom)
else:
# Keine gespeicherte Position → zentrieren
win.geometry("300x290")
center_window(win, 300, 290)
def on_diktat_close():
try:
if is_recording[0]:
rec = diktat_recorder[0]
is_recording[0] = False
self._diktat_recording_active = False
try:
if rec is not None and hasattr(rec, "stop_and_save_wav"):
wav_path = rec.stop_and_save_wav()
if wav_path and os.path.exists(wav_path):
os.remove(wav_path)
except Exception:
pass
diktat_recorder[0] = None
self._diktat_recorder = None
except Exception:
pass
try:
geom = win.geometry()
save_diktat_geometry(geom)
except Exception:
pass
try:
apply_tool_window_pin(win, False)
except Exception:
pass
try:
from aza_mini_diktat_window import close_mini_diktat_window
close_mini_diktat_window(self, restore_diktat=False)
except Exception:
pass
self._diktat_window = None
self._diktat_recording_active = False
self._diktat_recorder = None
if hasattr(self, "_aza_windows"):
self._aza_windows.discard(win)
win.destroy()
if getattr(self, "_main_hidden", False):
try:
self.destroy()
except Exception:
pass
win.protocol("WM_DELETE_WINDOW", on_diktat_close)
# Speichere Position auch während Verschieben/Resize
_diktat_geom_after_id = [None]
def on_diktat_configure(e):
if e.widget is win and _diktat_geom_after_id[0]:
win.after_cancel(_diktat_geom_after_id[0])
if e.widget is win:
_diktat_geom_after_id[0] = win.after(400, lambda: save_diktat_geometry(win.geometry()))
win.bind("<Configure>", on_diktat_configure)
add_resize_grip(win, DIKTAT_MIN_W, DIKTAT_MIN_H)
add_font_scale_control(win)
# ─── Header (AzA Add-on Beta Stil) ───
diktat_header = tk.Frame(win, bg=_HDR_BG)
diktat_header.pack(fill="x")
tk.Label(diktat_header, text="Diktat", font=("Segoe UI", 12, "bold"),
bg=_HDR_BG, fg=_HDR_FG).pack(side="left", padx=(14, 6), pady=10)
_dik_minimized = [False]
_dik_geom_before = [None]
_dik_restoring = [False]
# Mini buttons + status (shown when minimized)
_mini_rec_btn = [None]
_mini_neu_btn = [None]
_mini_status_lbl = [None]
def _mini_toggle_record():
"""Start/Stop Aufnahme im minimierten Zustand."""
toggle_diktat()
if _mini_rec_btn[0]:
if is_recording[0]:
_mini_rec_btn[0].configure(text="", fg="#D04040")
else:
_mini_rec_btn[0].configure(text="", fg="#5A90B0")
def _mini_new_diktat():
"""Neu: Text leeren, laufende Aufnahme verwerfen, sofort neue Aufnahme starten."""
do_neu()
if _mini_rec_btn[0]:
if is_recording[0]:
_mini_rec_btn[0].configure(text="", fg="#D04040")
else:
_mini_rec_btn[0].configure(text="", fg="#5A90B0")
def _restore_diktat_content():
if not _dik_minimized[0]:
return
_dik_restoring[0] = True
if _mini_rec_btn[0]:
_mini_rec_btn[0].pack_forget()
if _mini_neu_btn[0]:
_mini_neu_btn[0].pack_forget()
if _mini_status_lbl[0]:
_mini_status_lbl[0]._parent_bar.pack_forget()
body_outer.pack(fill="both", expand=True)
_dik_minimized[0] = False
win.minsize(DIKTAT_MIN_W, DIKTAT_MIN_H)
win.after(200, lambda: _dik_restoring.__setitem__(0, False))
def _toggle_minimize_diktat():
if _dik_minimized[0]:
_restore_diktat_content()
if _dik_geom_before[0]:
try:
win.geometry(_dik_geom_before[0])
except Exception:
pass
else:
_dik_geom_before[0] = win.geometry()
body_outer.pack_forget()
_dik_minimized[0] = True
win.minsize(200, 74)
w_cur = win.winfo_width()
win.geometry(f"{w_cur}x74")
# Show mini record button
rec_text = "" if is_recording[0] else ""
rec_fg = "#D04040" if is_recording[0] else "#5A90B0"
if not _mini_rec_btn[0]:
_mini_rec_btn[0] = tk.Label(diktat_header, text=rec_text, font=("Segoe UI", 14, "bold"),
bg=_HDR_BG, fg=rec_fg, cursor="hand2", padx=4)
_mini_rec_btn[0].bind("<Button-1>", lambda e: _mini_toggle_record())
_mini_rec_btn[0].bind("<Enter>", lambda e: _mini_rec_btn[0].configure(fg="#A8D4F0"))
_mini_rec_btn[0].bind("<Leave>", lambda e: _mini_rec_btn[0].configure(
fg="#D04040" if is_recording[0] else "#A0C4D8"))
else:
_mini_rec_btn[0].configure(text=rec_text, fg=rec_fg)
_mini_rec_btn[0].pack(side="left", padx=(0, 4))
if not _mini_neu_btn[0]:
_mini_neu_btn[0] = tk.Label(diktat_header, text="Neu", font=("Segoe UI", 9),
bg=_HDR_BG, fg="#A0C4D8", cursor="hand2", padx=4)
_mini_neu_btn[0].bind("<Button-1>", lambda e: _mini_new_diktat())
_mini_neu_btn[0].bind("<Enter>", lambda e: _mini_neu_btn[0].configure(fg="#FFFFFF"))
_mini_neu_btn[0].bind("<Leave>", lambda e: _mini_neu_btn[0].configure(fg="#A0C4D8"))
_mini_neu_btn[0].pack(side="left", padx=(0, 4))
# Show mini status bar below header
if not _mini_status_lbl[0]:
_mini_status_bar = tk.Frame(win, bg=_BG, height=22, padx=10, pady=2)
_mini_status_lbl[0] = tk.Label(
_mini_status_bar, textvariable=status_var,
fg=_STATUS_REC, bg=_BG,
font=("Segoe UI", 8), anchor="w",
)
_mini_status_lbl[0].pack(side="left", fill="x", expand=True)
_mini_status_lbl[0]._parent_bar = _mini_status_bar
_mini_status_lbl[0]._parent_bar.pack(fill="x")
_mini_status_lbl[0].pack(side="left", fill="x", expand=True)
def _on_dik_configure(e):
if e.widget is not win:
return
if _dik_minimized[0] and not _dik_restoring[0] and e.height > 95:
_restore_diktat_content()
win.bind("<Configure>", _on_dik_configure, add="+")
def _open_mini_diktat_mode(evt=None):
try:
from aza_mini_diktat_window import open_mini_diktat_window
ax = ay = None
if evt is not None:
ax, ay = evt.x_root, evt.y_root
open_mini_diktat_window(self, win, anchor_x=ax, anchor_y=ay)
except Exception:
pass
btn_mini_diktat = tk.Label(
diktat_header, text="Mini-Diktat", font=("Segoe UI", 9),
bg=_HDR_BG, fg="#A0C4D8", cursor="hand2", padx=6,
)
btn_mini_diktat.pack(side="right", padx=(0, 4))
btn_mini_diktat.bind("<Button-1>", _open_mini_diktat_mode)
btn_mini_diktat.bind("<Enter>", lambda e: btn_mini_diktat.configure(fg="#FFFFFF"))
btn_mini_diktat.bind("<Leave>", lambda e: btn_mini_diktat.configure(fg="#A0C4D8"))
win._aza_mini_diktat_btn = btn_mini_diktat
add_tool_pin_button(diktat_header, win, bg=_HDR_BG, side="right", padx=(0, 4))
if getattr(win, "_tool_pin_btn", None) is not None:
try:
win._tool_pin_btn.configure(fg="#E8F4FA", font=("Segoe UI Emoji", 12), padx=8)
except Exception:
pass
win._aza_minimize = _toggle_minimize_diktat
win._aza_is_minimized = lambda: _dik_minimized[0]
win._aza_restore_diktat_content = _restore_diktat_content
if hasattr(self, "_aza_windows"):
self._aza_windows.add(win)
body_outer = tk.Frame(win, bg=_BG, padx=12, pady=10)
body_outer.pack(fill="both", expand=True)
main_f = tk.Frame(body_outer, bg=_CARD_BG,
highlightbackground=_CARD_BD, highlightthickness=1)
main_f.pack(fill="both", expand=True, padx=0, pady=0)
main_inner = tk.Frame(main_f, bg=_CARD_BG, padx=12, pady=10)
main_inner.pack(fill="both", expand=True)
label_frame = tk.Frame(main_inner, bg=_CARD_BG)
label_frame.pack(fill="x", anchor="w")
tk.Label(label_frame, text="Transkript", font=("Segoe UI", 9, "bold"),
bg=_CARD_BG, fg=_TEXT).pack(side="left")
# Modus auch direkt im Diktat-Fenster: Medizin vs. Allgemein (exklusiv)
if not hasattr(self, "_transcribe_medical_var"):
self._transcribe_medical_var = tk.BooleanVar(value=True)
if not hasattr(self, "_transcribe_general_var"):
self._transcribe_general_var = tk.BooleanVar(value=False)
if not hasattr(self, "_transcribe_toggle_guard"):
self._transcribe_toggle_guard = False
def _set_mode(domain_value: str):
if hasattr(self, "_set_transcribe_domain"):
self._set_transcribe_domain(domain_value)
return
# Fallback ohne Hauptmethode
if domain_value == "general":
self._transcribe_medical_var.set(False)
self._transcribe_general_var.set(True)
else:
self._transcribe_medical_var.set(True)
self._transcribe_general_var.set(False)
def _on_med_toggle():
if self._transcribe_medical_var.get():
_set_mode("medical")
elif not self._transcribe_general_var.get():
_set_mode("medical")
def _on_gen_toggle():
if self._transcribe_general_var.get():
_set_mode("general")
elif not self._transcribe_medical_var.get():
_set_mode("medical")
tk.Checkbutton(
label_frame, text="Medizin", variable=self._transcribe_medical_var,
command=_on_med_toggle, bg=_CARD_BG, fg=_TEXT,
activebackground=_CARD_BG, selectcolor="#E8F4FA",
font=("Segoe UI", 8),
).pack(side="left", padx=(10, 4))
tk.Checkbutton(
label_frame, text="Allgemein", variable=self._transcribe_general_var,
command=_on_gen_toggle, bg=_CARD_BG, fg=_TEXT,
activebackground=_CARD_BG, selectcolor="#E8F4FA",
font=("Segoe UI", 8),
).pack(side="left")
diktat_font = ("Segoe UI", 8)
txt = ScrolledText(main_inner, wrap="word", font=diktat_font, bg="#FAFCFE",
height=8, relief="flat", bd=1,
highlightbackground=_CARD_BD, highlightthickness=1)
txt.pack(fill="both", expand=True, pady=(6, 6))
add_text_font_size_control(label_frame, txt, initial_size=8, bg_color=_CARD_BG, save_key="diktat_window")
self._bind_textblock_pending(txt)
status_var = tk.StringVar(value="Bereit.")
lbl_status = tk.Label(
main_inner, textvariable=status_var, fg=_TEXT_SUB, bg=_CARD_BG,
font=("Segoe UI", 8), anchor="w",
)
lbl_status.pack(fill="x", pady=(0, 4))
cb_row = tk.Frame(main_inner, bg=_CARD_BG)
cb_row.pack(fill="x", pady=(2, 0))
_diktat_autocopy_var = tk.BooleanVar(value=is_autocopy_after_diktat_enabled())
tk.Checkbutton(
cb_row, text="Autokopie nach Transkription",
variable=_diktat_autocopy_var,
command=lambda: save_autocopy_prefs(autocopy=_diktat_autocopy_var.get()),
bg=_CARD_BG, fg=_TEXT, activebackground=_CARD_BG,
selectcolor="#FFFFFF", font=("Segoe UI", 8),
).pack(side="left")
_diktat_rclick_var = tk.BooleanVar(value=is_global_right_click_paste_enabled())
tk.Checkbutton(
cb_row, text="Rechtsklick = Einfügen",
variable=_diktat_rclick_var,
command=lambda: save_autocopy_prefs(global_right_click=_diktat_rclick_var.get()),
bg=_CARD_BG, fg=_TEXT, activebackground=_CARD_BG,
selectcolor="#FFFFFF", font=("Segoe UI", 8),
).pack(side="left", padx=(12, 0))
btn_row = tk.Frame(main_inner, bg=_CARD_BG)
btn_row.pack(fill="x", pady=(6, 0))
btn_row2 = tk.Frame(main_inner, bg=_CARD_BG)
btn_row2.pack(fill="x", pady=(4, 0))
diktat_recorder = [None]
is_recording = [False]
append_next_transcript = [False]
def _recorder_hardware_active(rec) -> bool:
if rec is None:
return False
if getattr(rec, "_recording", False):
return True
proc = getattr(rec, "_ffmpeg_proc", None)
if proc is not None and proc.poll() is None:
return True
stream = getattr(rec, "_stream", None)
if stream is not None:
try:
return bool(stream.active)
except Exception:
pass
return False
def _diktat_recorder_phase() -> str:
"""Zentrale Zustandsquelle: idle | recording | finalizing | error."""
if getattr(self, "_diktat_neu_busy", False):
return "finalizing"
if is_recording[0]:
return "recording"
if _recorder_hardware_active(diktat_recorder[0]):
return "recording"
return "idle"
def _sync_mini_diktat_ui():
try:
from aza_mini_diktat_window import sync_mini_diktat_window
sync_mini_diktat_window(self, diktat_win=win)
except Exception:
pass
def toggle_diktat():
if getattr(self, "_diktat_neu_busy", False):
return
if not diktat_recorder[0]:
diktat_recorder[0] = AudioRecorder()
rec = diktat_recorder[0]
if not is_recording[0]:
try:
rec.start()
is_recording[0] = True
self._diktat_recording_active = True
self._diktat_recorder = rec
lbl_status.configure(fg=_STATUS_REC)
status_var.set("Aufnahme läuft…")
_refresh_diktat_controls()
except Exception as e:
messagebox.showerror("Aufnahme-Fehler", str(e))
status_var.set("Bereit.")
else:
is_recording[0] = False
self._diktat_recording_active = False
# Finalizing-Sperre: blockiert Doppelklick/zweiten Start/zweiten Stop
# (toggle_diktat, _diktat_weiterfahren, _diktat_neu_von_logo pruefen das Flag),
# bis die Transkription abgeschlossen ist. Wird IMMER (finally) zurueckgesetzt.
self._diktat_neu_busy = True
lbl_status.configure(fg=_TEXT_SUB)
status_var.set("Aufnahme wird abgeschlossen …")
_refresh_diktat_controls()
def worker():
def _safe_after(fn):
try:
if self.winfo_exists():
self.after(0, fn)
except Exception:
pass
def _after_finalize():
self._diktat_neu_busy = False
try:
_refresh_diktat_controls()
except Exception:
pass
try:
wav_path = rec.stop_and_save_wav()
try:
with wave.open(wav_path, 'rb') as wf:
frames = wf.getnframes()
framerate = wf.getframerate()
duration = frames / float(framerate)
if duration < 0.3:
try:
if os.path.exists(wav_path):
os.remove(wav_path)
except Exception:
pass
diktat_recorder[0] = None
_safe_after(lambda: status_var.set("Kein Audio erkannt."))
return
except Exception:
pass
transcript_result = self.transcribe_wav(wav_path)
if hasattr(transcript_result, 'text'):
transcript_text = transcript_result.text
elif isinstance(transcript_result, str):
transcript_text = transcript_result
else:
transcript_text = ""
try:
if os.path.exists(wav_path):
os.remove(wav_path)
except Exception:
pass
if not transcript_text or not transcript_text.strip():
diktat_recorder[0] = None
_safe_after(lambda: status_var.set("Kein Text erkannt."))
return
transcript_text = self._diktat_postprocess_transcript(transcript_text)
if not transcript_text or not transcript_text.strip():
diktat_recorder[0] = None
_safe_after(lambda: status_var.set("Kein Text erkannt."))
return
_safe_after(lambda: _done(transcript_text))
except Exception as e:
def _show_err(err=e):
try:
if win.winfo_exists():
messagebox.showerror("Fehler", str(err), parent=win)
status_var.set("Fehler.")
except Exception:
pass
_safe_after(_show_err)
finally:
# Busy-Flag immer zuruecksetzen (Stop garantiert beendet Finalizing).
_safe_after(_after_finalize)
def _done(text):
diktat_recorder[0] = None
self._diktat_recorder = None
self._diktat_recording_active = False
try:
if not win.winfo_exists():
return
txt.configure(state="normal")
if append_next_transcript[0]:
append_next_transcript[0] = False
existing = txt.get("1.0", "end").strip()
merged = text or ""
if existing and merged:
sep = "\n\n" if existing.endswith("\n") else (
" " if not existing.endswith((" ", "-", ":", ";")) else ""
)
merged = existing + sep + merged
txt.delete("1.0", "end")
txt.insert("1.0", merged)
full = merged.strip()
else:
idx = txt.index(tk.INSERT)
txt.insert(idx, text)
full = txt.get("1.0", "end").strip()
copied = False
if _diktat_autocopy_var.get() and full:
if not _win_clipboard_set(full):
try:
self.clipboard_clear()
self.clipboard_append(sanitize_markdown_for_plain_text(full))
except Exception:
pass
copied = True
if full:
try:
save_to_ablage("Diktat", full)
except Exception:
pass
if copied:
self.set_status("Diktat transkribiert und kopiert.")
status_var.set("Fertig. Kopiert.")
else:
self.set_status("Diktat transkribiert.")
status_var.set("Fertig.")
_refresh_diktat_controls()
except Exception:
pass
threading.Thread(target=worker, daemon=True).start()
def _refresh_diktat_controls():
phase = _diktat_recorder_phase()
try:
if phase == "finalizing":
btn_diktat_record.configure(
text="Diktat stoppen", bg="#C86B2A", active_bg="#A85520",
command=lambda: None,
)
elif phase == "recording":
btn_diktat_record.configure(
text="Diktat stoppen", bg="#C86B2A", active_bg="#A85520",
command=toggle_diktat,
)
else:
btn_diktat_record.configure(
text="Diktat starten", bg=_BTN_PRI, active_bg=_BTN_PRI_H,
command=toggle_diktat,
)
except Exception:
pass
_sync_mini_diktat_ui()
win._aza_diktat_toggle = toggle_diktat
win._aza_diktat_phase = _diktat_recorder_phase
win._aza_diktat_refresh = _refresh_diktat_controls
win._aza_diktat_is_recording = lambda: _diktat_recorder_phase() == "recording"
win._aza_diktat_status_var = status_var
win._aza_diktat_text = txt
def _preserve_diktat_text_if_any() -> bool:
"""Bestehenden Text sichern (Kopie/Ablage) bevor das Feld geleert wird."""
existing = txt.get("1.0", "end").strip()
if not existing:
return False
copied = False
if _diktat_autocopy_var.get():
if not _win_clipboard_set(existing):
try:
self.clipboard_clear()
self.clipboard_append(sanitize_markdown_for_plain_text(existing))
except Exception:
pass
copied = True
try:
save_to_ablage("Diktat", existing)
except Exception:
pass
if copied:
status_var.set("Vorheriges Diktat gesichert. Kopiert.")
else:
status_var.set("Vorheriges Diktat gesichert.")
return True
def _stop_recording_async_then(on_ready, busy_status):
"""Stoppt die laufende Aufnahme im Hintergrund (kein Tk-Mainthread-Block)
und ruft danach on_ready() im Mainthread auf.
Root-Cause-Fix: rec.stop_and_save_wav() macht ffmpeg_proc.wait(timeout=30)
und blockierte bisher den UI-Thread (Logo "Neues Diktat" -> Programm haengt).
self._diktat_neu_busy schuetzt zugleich gegen Doppelklick/Doppelstart
(toggle_diktat, _diktat_weiterfahren und do_neu pruefen dieses Flag)."""
rec = diktat_recorder[0]
is_recording[0] = False
self._diktat_recording_active = False
self._diktat_neu_busy = True
try:
lbl_status.configure(fg=_TEXT_SUB)
except Exception:
pass
status_var.set(busy_status)
_refresh_diktat_controls()
diktat_recorder[0] = None
self._diktat_recorder = None
def _bg():
ok = True
try:
if rec is not None:
wav_path = rec.stop_and_save_wav()
try:
if wav_path and os.path.exists(wav_path):
os.remove(wav_path)
except Exception:
pass
except Exception:
ok = False
def _fin():
self._diktat_neu_busy = False
if ok:
try:
on_ready()
except Exception:
pass
else:
status_var.set("Aufnahme-Fehler. Bestehender Text erhalten.")
_refresh_diktat_controls()
try:
if self.winfo_exists():
self.after(0, _fin)
else:
self._diktat_neu_busy = False
except Exception:
self._diktat_neu_busy = False
threading.Thread(target=_bg, daemon=True).start()
def _diktat_weiterfahren():
if getattr(self, "_diktat_neu_busy", False):
return
if _diktat_recorder_phase() == "recording":
toggle_diktat()
return
append_next_transcript[0] = True
try:
txt.focus_set()
txt.mark_set(tk.INSERT, tk.END)
except Exception:
pass
if not is_recording[0]:
toggle_diktat()
def _diktat_neu_von_logo():
if getattr(self, "_diktat_neu_busy", False):
return
if is_recording[0]:
if not messagebox.askyesno(
"Aufnahme läuft",
"Es läuft gerade eine Aufnahme.\n"
"Möchtest du die aktuelle Aufnahme verwerfen und ein neues Diktat starten?",
parent=win,
):
return
def _ready_logo():
append_next_transcript[0] = False
_preserve_diktat_text_if_any()
try:
txt.configure(state="normal")
txt.delete("1.0", "end")
except Exception:
pass
status_var.set("Aufnahme läuft …")
toggle_diktat()
_stop_recording_async_then(_ready_logo, "Aktuelle Aufnahme wird abgeschlossen …")
return
append_next_transcript[0] = False
_preserve_diktat_text_if_any()
txt.configure(state="normal")
txt.delete("1.0", "end")
status_var.set("Aufnahme läuft …")
toggle_diktat()
def _diktat_korrigieren():
_diktat_weiterfahren()
win._aza_diktat_weiterfahren = _diktat_weiterfahren
win._aza_diktat_neu_von_logo = _diktat_neu_von_logo
win._aza_diktat_korrigieren = _diktat_korrigieren
def do_neu():
if getattr(self, "_diktat_neu_busy", False):
return
if is_recording[0]:
if not messagebox.askyesno(
"Aufnahme läuft",
"Es läuft gerade eine Aufnahme.\n"
"Möchtest du die aktuelle Aufnahme wirklich verwerfen und neu starten?",
parent=win
):
return
def _ready_neu():
try:
txt.configure(state="normal")
txt.delete("1.0", "end")
except Exception:
pass
status_var.set("Bereit.")
toggle_diktat()
_stop_recording_async_then(_ready_neu, "Aktuelle Aufnahme wird abgeschlossen …")
return
txt.configure(state="normal")
txt.delete("1.0", "end")
status_var.set("Bereit.")
toggle_diktat()
def do_kopieren():
t = txt.get("1.0", "end").strip()
if t:
if not _win_clipboard_set(t):
self.clipboard_clear()
self.clipboard_append(sanitize_markdown_for_plain_text(t))
self.set_status("Diktat kopiert.")
else:
self.set_status("Nichts zum Kopieren.")
def do_clear_text():
txt.configure(state="normal")
txt.delete("1.0", "end")
status_var.set("Diktatfeld geleert.")
self.set_status("Diktatfeld geleert.")
btn_diktat_record = RoundedButton(
btn_row, "Diktat starten", command=toggle_diktat,
width=148, height=30, canvas_bg=_CARD_BG,
bg=_BTN_PRI, fg="#FFFFFF", active_bg=_BTN_PRI_H,
)
btn_diktat_record.pack(side="left")
RoundedButton(
btn_row, "Neu", command=do_neu,
width=72, height=30, canvas_bg=_CARD_BG,
bg=_BTN_SEC, fg=_BTN_PRI, active_bg="#E8F4FA",
).pack(side="left", padx=(8, 0))
RoundedButton(
btn_row2, "Kopieren", command=do_kopieren,
width=100, height=30, canvas_bg=_CARD_BG,
bg=_BTN_SEC, fg=_BTN_PRI, active_bg="#E8F4FA",
).pack(side="left")
RoundedButton(
btn_row2, "Leeren", command=do_clear_text,
width=80, height=30, canvas_bg=_CARD_BG,
bg=_BTN_SEC, fg=_BTN_PRI, active_bg="#E8F4FA",
).pack(side="left", padx=(8, 0))
_refresh_diktat_controls()