2026-03-25 22:03:39 +01:00
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""
|
|
|
|
|
|
AzaDiktatMixin – Diktat-Fenster (nur Transkription, keine KG).
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
import threading
|
|
|
|
|
|
import wave
|
|
|
|
|
|
import tkinter as tk
|
2026-06-13 22:47:31 +02:00
|
|
|
|
from tkinter import messagebox
|
2026-03-25 22:03:39 +01:00
|
|
|
|
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,
|
2026-06-13 22:47:31 +02:00
|
|
|
|
add_tool_pin_button,
|
|
|
|
|
|
apply_tool_window_pin,
|
2026-03-25 22:03:39 +01:00
|
|
|
|
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
|
2026-06-10 22:55:03 +02:00
|
|
|
|
_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"
|
2026-03-25 22:03:39 +01:00
|
|
|
|
win = tk.Toplevel(self)
|
2026-06-10 22:55:03 +02:00
|
|
|
|
win.title("Diktat")
|
2026-03-25 22:03:39 +01:00
|
|
|
|
win.minsize(DIKTAT_MIN_W, DIKTAT_MIN_H)
|
2026-06-10 22:55:03 +02:00
|
|
|
|
win.configure(bg=_BG)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
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():
|
2026-06-10 22:55:03 +02:00
|
|
|
|
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
|
2026-03-25 22:03:39 +01:00
|
|
|
|
try:
|
|
|
|
|
|
geom = win.geometry()
|
|
|
|
|
|
save_diktat_geometry(geom)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
2026-06-13 22:47:31 +02:00
|
|
|
|
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
|
2026-03-25 22:03:39 +01:00
|
|
|
|
self._diktat_window = None
|
2026-06-10 22:55:03 +02:00
|
|
|
|
self._diktat_recording_active = False
|
|
|
|
|
|
self._diktat_recorder = None
|
2026-03-25 22:03:39 +01:00
|
|
|
|
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)
|
|
|
|
|
|
|
2026-06-10 22:55:03 +02:00
|
|
|
|
# ─── Header (AzA Add-on Beta Stil) ───
|
|
|
|
|
|
diktat_header = tk.Frame(win, bg=_HDR_BG)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
diktat_header.pack(fill="x")
|
2026-06-10 22:55:03 +02:00
|
|
|
|
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)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
_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()
|
2026-06-10 22:55:03 +02:00
|
|
|
|
body_outer.pack(fill="both", expand=True)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
_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()
|
2026-06-10 22:55:03 +02:00
|
|
|
|
body_outer.pack_forget()
|
2026-03-25 22:03:39 +01:00
|
|
|
|
_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"),
|
2026-06-10 22:55:03 +02:00
|
|
|
|
bg=_HDR_BG, fg=rec_fg, cursor="hand2", padx=4)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
_mini_rec_btn[0].bind("<Button-1>", lambda e: _mini_toggle_record())
|
2026-06-10 22:55:03 +02:00
|
|
|
|
_mini_rec_btn[0].bind("<Enter>", lambda e: _mini_rec_btn[0].configure(fg="#A8D4F0"))
|
2026-03-25 22:03:39 +01:00
|
|
|
|
_mini_rec_btn[0].bind("<Leave>", lambda e: _mini_rec_btn[0].configure(
|
2026-06-10 22:55:03 +02:00
|
|
|
|
fg="#D04040" if is_recording[0] else "#A0C4D8"))
|
2026-03-25 22:03:39 +01:00
|
|
|
|
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),
|
2026-06-10 22:55:03 +02:00
|
|
|
|
bg=_HDR_BG, fg="#A0C4D8", cursor="hand2", padx=4)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
_mini_neu_btn[0].bind("<Button-1>", lambda e: _mini_new_diktat())
|
2026-06-10 22:55:03 +02:00
|
|
|
|
_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"))
|
2026-03-25 22:03:39 +01:00
|
|
|
|
_mini_neu_btn[0].pack(side="left", padx=(0, 4))
|
|
|
|
|
|
# Show mini status bar below header
|
|
|
|
|
|
if not _mini_status_lbl[0]:
|
2026-06-10 22:55:03 +02:00
|
|
|
|
_mini_status_bar = tk.Frame(win, bg=_BG, height=22, padx=10, pady=2)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
_mini_status_lbl[0] = tk.Label(
|
|
|
|
|
|
_mini_status_bar, textvariable=status_var,
|
2026-06-10 22:55:03 +02:00
|
|
|
|
fg=_STATUS_REC, bg=_BG,
|
2026-03-25 22:03:39 +01:00
|
|
|
|
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="+")
|
|
|
|
|
|
|
2026-06-13 22:47:31 +02:00
|
|
|
|
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
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
win._aza_minimize = _toggle_minimize_diktat
|
|
|
|
|
|
win._aza_is_minimized = lambda: _dik_minimized[0]
|
2026-06-13 22:47:31 +02:00
|
|
|
|
win._aza_restore_diktat_content = _restore_diktat_content
|
2026-03-25 22:03:39 +01:00
|
|
|
|
if hasattr(self, "_aza_windows"):
|
|
|
|
|
|
self._aza_windows.add(win)
|
|
|
|
|
|
|
2026-06-10 22:55:03 +02:00
|
|
|
|
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)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
2026-06-10 22:55:03 +02:00
|
|
|
|
label_frame = tk.Frame(main_inner, bg=_CARD_BG)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
label_frame.pack(fill="x", anchor="w")
|
2026-06-10 22:55:03 +02:00
|
|
|
|
tk.Label(label_frame, text="Transkript", font=("Segoe UI", 9, "bold"),
|
|
|
|
|
|
bg=_CARD_BG, fg=_TEXT).pack(side="left")
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
# 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,
|
2026-06-10 22:55:03 +02:00
|
|
|
|
command=_on_med_toggle, bg=_CARD_BG, fg=_TEXT,
|
|
|
|
|
|
activebackground=_CARD_BG, selectcolor="#E8F4FA",
|
|
|
|
|
|
font=("Segoe UI", 8),
|
2026-03-25 22:03:39 +01:00
|
|
|
|
).pack(side="left", padx=(10, 4))
|
|
|
|
|
|
tk.Checkbutton(
|
|
|
|
|
|
label_frame, text="Allgemein", variable=self._transcribe_general_var,
|
2026-06-10 22:55:03 +02:00
|
|
|
|
command=_on_gen_toggle, bg=_CARD_BG, fg=_TEXT,
|
|
|
|
|
|
activebackground=_CARD_BG, selectcolor="#E8F4FA",
|
|
|
|
|
|
font=("Segoe UI", 8),
|
2026-03-25 22:03:39 +01:00
|
|
|
|
).pack(side="left")
|
|
|
|
|
|
|
|
|
|
|
|
diktat_font = ("Segoe UI", 8)
|
2026-06-10 22:55:03 +02:00
|
|
|
|
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))
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
2026-06-10 22:55:03 +02:00
|
|
|
|
add_text_font_size_control(label_frame, txt, initial_size=8, bg_color=_CARD_BG, save_key="diktat_window")
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
self._bind_textblock_pending(txt)
|
|
|
|
|
|
status_var = tk.StringVar(value="Bereit.")
|
|
|
|
|
|
lbl_status = tk.Label(
|
2026-06-10 22:55:03 +02:00
|
|
|
|
main_inner, textvariable=status_var, fg=_TEXT_SUB, bg=_CARD_BG,
|
2026-03-25 22:03:39 +01:00
|
|
|
|
font=("Segoe UI", 8), anchor="w",
|
|
|
|
|
|
)
|
2026-06-10 22:55:03 +02:00
|
|
|
|
lbl_status.pack(fill="x", pady=(0, 4))
|
|
|
|
|
|
cb_row = tk.Frame(main_inner, bg=_CARD_BG)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
cb_row.pack(fill="x", pady=(2, 0))
|
|
|
|
|
|
_diktat_autocopy_var = tk.BooleanVar(value=is_autocopy_after_diktat_enabled())
|
2026-06-13 22:47:31 +02:00
|
|
|
|
tk.Checkbutton(
|
2026-03-25 22:03:39 +01:00
|
|
|
|
cb_row, text="Autokopie nach Transkription",
|
|
|
|
|
|
variable=_diktat_autocopy_var,
|
|
|
|
|
|
command=lambda: save_autocopy_prefs(autocopy=_diktat_autocopy_var.get()),
|
2026-06-13 22:47:31 +02:00
|
|
|
|
bg=_CARD_BG, fg=_TEXT, activebackground=_CARD_BG,
|
|
|
|
|
|
selectcolor="#FFFFFF", font=("Segoe UI", 8),
|
2026-03-25 22:03:39 +01:00
|
|
|
|
).pack(side="left")
|
|
|
|
|
|
_diktat_rclick_var = tk.BooleanVar(value=is_global_right_click_paste_enabled())
|
2026-06-13 22:47:31 +02:00
|
|
|
|
tk.Checkbutton(
|
2026-03-25 22:03:39 +01:00
|
|
|
|
cb_row, text="Rechtsklick = Einfügen",
|
|
|
|
|
|
variable=_diktat_rclick_var,
|
|
|
|
|
|
command=lambda: save_autocopy_prefs(global_right_click=_diktat_rclick_var.get()),
|
2026-06-13 22:47:31 +02:00
|
|
|
|
bg=_CARD_BG, fg=_TEXT, activebackground=_CARD_BG,
|
|
|
|
|
|
selectcolor="#FFFFFF", font=("Segoe UI", 8),
|
2026-03-25 22:03:39 +01:00
|
|
|
|
).pack(side="left", padx=(12, 0))
|
|
|
|
|
|
|
2026-06-10 22:55:03 +02:00
|
|
|
|
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))
|
2026-03-25 22:03:39 +01:00
|
|
|
|
diktat_recorder = [None]
|
|
|
|
|
|
is_recording = [False]
|
2026-06-13 22:47:31 +02:00
|
|
|
|
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
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
def toggle_diktat():
|
2026-06-13 22:47:31 +02:00
|
|
|
|
if getattr(self, "_diktat_neu_busy", False):
|
|
|
|
|
|
return
|
2026-03-25 22:03:39 +01:00
|
|
|
|
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
|
2026-06-10 22:55:03 +02:00
|
|
|
|
self._diktat_recording_active = True
|
|
|
|
|
|
self._diktat_recorder = rec
|
|
|
|
|
|
lbl_status.configure(fg=_STATUS_REC)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
status_var.set("Aufnahme läuft…")
|
2026-06-13 22:47:31 +02:00
|
|
|
|
_refresh_diktat_controls()
|
2026-03-25 22:03:39 +01:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
messagebox.showerror("Aufnahme-Fehler", str(e))
|
|
|
|
|
|
status_var.set("Bereit.")
|
|
|
|
|
|
else:
|
|
|
|
|
|
is_recording[0] = False
|
2026-06-10 22:55:03 +02:00
|
|
|
|
self._diktat_recording_active = False
|
2026-06-13 22:47:31 +02:00
|
|
|
|
# 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
|
2026-06-10 22:55:03 +02:00
|
|
|
|
lbl_status.configure(fg=_TEXT_SUB)
|
2026-06-13 22:47:31 +02:00
|
|
|
|
status_var.set("Aufnahme wird abgeschlossen …")
|
|
|
|
|
|
_refresh_diktat_controls()
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
def worker():
|
|
|
|
|
|
def _safe_after(fn):
|
|
|
|
|
|
try:
|
|
|
|
|
|
if self.winfo_exists():
|
|
|
|
|
|
self.after(0, fn)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2026-06-13 22:47:31 +02:00
|
|
|
|
def _after_finalize():
|
|
|
|
|
|
self._diktat_neu_busy = False
|
|
|
|
|
|
try:
|
|
|
|
|
|
_refresh_diktat_controls()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2026-03-25 22:03:39 +01:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-06-13 22:47:31 +02:00
|
|
|
|
transcript_text = self._diktat_postprocess_transcript(transcript_text)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
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)
|
2026-06-13 22:47:31 +02:00
|
|
|
|
finally:
|
|
|
|
|
|
# Busy-Flag immer zuruecksetzen (Stop garantiert beendet Finalizing).
|
|
|
|
|
|
_safe_after(_after_finalize)
|
2026-03-25 22:03:39 +01:00
|
|
|
|
|
|
|
|
|
|
def _done(text):
|
|
|
|
|
|
diktat_recorder[0] = None
|
2026-06-10 22:55:03 +02:00
|
|
|
|
self._diktat_recorder = None
|
|
|
|
|
|
self._diktat_recording_active = False
|
2026-03-25 22:03:39 +01:00
|
|
|
|
try:
|
|
|
|
|
|
if not win.winfo_exists():
|
|
|
|
|
|
return
|
|
|
|
|
|
txt.configure(state="normal")
|
2026-06-13 22:47:31 +02:00
|
|
|
|
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()
|
2026-03-25 22:03:39 +01:00
|
|
|
|
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.")
|
2026-06-13 22:47:31 +02:00
|
|
|
|
_refresh_diktat_controls()
|
2026-03-25 22:03:39 +01:00
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
threading.Thread(target=worker, daemon=True).start()
|
|
|
|
|
|
|
2026-06-13 22:47:31 +02:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-03-25 22:03:39 +01:00
|
|
|
|
def do_neu():
|
2026-06-13 22:47:31 +02:00
|
|
|
|
if getattr(self, "_diktat_neu_busy", False):
|
|
|
|
|
|
return
|
2026-03-25 22:03:39 +01:00
|
|
|
|
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
|
2026-06-13 22:47:31 +02:00
|
|
|
|
|
|
|
|
|
|
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
|
2026-03-25 22:03:39 +01:00
|
|
|
|
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(
|
2026-06-10 22:55:03 +02:00
|
|
|
|
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,
|
2026-03-25 22:03:39 +01:00
|
|
|
|
)
|
|
|
|
|
|
btn_diktat_record.pack(side="left")
|
|
|
|
|
|
RoundedButton(
|
|
|
|
|
|
btn_row, "Neu", command=do_neu,
|
2026-06-10 22:55:03 +02:00
|
|
|
|
width=72, height=30, canvas_bg=_CARD_BG,
|
|
|
|
|
|
bg=_BTN_SEC, fg=_BTN_PRI, active_bg="#E8F4FA",
|
|
|
|
|
|
).pack(side="left", padx=(8, 0))
|
2026-03-25 22:03:39 +01:00
|
|
|
|
RoundedButton(
|
|
|
|
|
|
btn_row2, "Kopieren", command=do_kopieren,
|
2026-06-10 22:55:03 +02:00
|
|
|
|
width=100, height=30, canvas_bg=_CARD_BG,
|
|
|
|
|
|
bg=_BTN_SEC, fg=_BTN_PRI, active_bg="#E8F4FA",
|
2026-03-25 22:03:39 +01:00
|
|
|
|
).pack(side="left")
|
|
|
|
|
|
RoundedButton(
|
2026-06-10 22:55:03 +02:00
|
|
|
|
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))
|
2026-06-13 22:47:31 +02:00
|
|
|
|
_refresh_diktat_controls()
|