# -*- 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("", 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("", lambda e: _mini_toggle_record()) _mini_rec_btn[0].bind("", lambda e: _mini_rec_btn[0].configure(fg="#A8D4F0")) _mini_rec_btn[0].bind("", 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("", lambda e: _mini_new_diktat()) _mini_neu_btn[0].bind("", lambda e: _mini_neu_btn[0].configure(fg="#FFFFFF")) _mini_neu_btn[0].bind("", 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("", _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("", _open_mini_diktat_mode) btn_mini_diktat.bind("", lambda e: btn_mini_diktat.configure(fg="#FFFFFF")) btn_mini_diktat.bind("", 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()