# -*- coding: utf-8 -*- """ AzaNotizenMixin – Projekt-Notizen-Fenster mit Tabs, Auto-Save und Diktat. Eigene Notizen: dynamische Sub-Tabs mit + / − (Titel, Diktat je Tab). """ import json import os import re import html import threading import wave import tkinter as tk import tkinter.font as tkfont from tkinter import ttk, simpledialog, messagebox from tkinter.scrolledtext import ScrolledText from aza_ui_helpers import ( add_resize_grip, add_font_scale_control, add_text_font_size_control, setup_window_geometry_saving, save_toplevel_geometry, RoundedButton, ) from aza_audio import AudioRecorder from aza_persistence import _win_clipboard_set, sanitize_markdown_for_plain_text _NOTIZEN_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "notizen") _EIGENE_DIR = os.path.join(_NOTIZEN_DIR, "eigene") _TABS_CONFIG = os.path.join(_NOTIZEN_DIR, "eigene_notizen_tabs.json") _FIXED_TABS = [ ("Projektstatus", "projekt_status.md"), ("Changelog", "changelog.md"), ("Sonnet Prompt", "sonnet_prompt.txt"), ] def _load_file(filepath: str) -> str: try: if os.path.isfile(filepath): with open(filepath, "r", encoding="utf-8") as f: return f.read() except Exception: pass return "" def _save_file(filepath: str, content: str): os.makedirs(os.path.dirname(filepath), exist_ok=True) with open(filepath, "w", encoding="utf-8") as f: f.write(content) def _load_eigene_tabs() -> list: try: if os.path.isfile(_TABS_CONFIG): with open(_TABS_CONFIG, "r", encoding="utf-8") as f: data = json.load(f) if isinstance(data, list) and data: return data except Exception: pass old_file = os.path.join(_NOTIZEN_DIR, "eigene_notizen.md") if os.path.isfile(old_file): new_path = os.path.join(_EIGENE_DIR, "eigene_notizen.md") os.makedirs(_EIGENE_DIR, exist_ok=True) if not os.path.isfile(new_path): try: import shutil shutil.move(old_file, new_path) except Exception: pass return [{"title": "Allgemein", "filename": "eigene_notizen.md"}] return [{"title": "Allgemein", "filename": "eigene_notizen.md"}] def _save_eigene_tabs(tabs: list): os.makedirs(_NOTIZEN_DIR, exist_ok=True) with open(_TABS_CONFIG, "w", encoding="utf-8") as f: json.dump(tabs, f, ensure_ascii=False, indent=2) def _ensure_rich_tags(txt_widget): if getattr(txt_widget, "_aza_rich_tags_ready", False): return try: base = tkfont.Font(font=txt_widget.cget("font")) except Exception: base = tkfont.nametofont("TkTextFont").copy() base_size = int(base.cget("size")) if str(base.cget("size")).lstrip("-").isdigit() else 10 f_bold = base.copy() f_bold.configure(weight="bold") f_h1 = base.copy() f_h1.configure(weight="bold", size=max(base_size + 6, 16)) f_h2 = base.copy() f_h2.configure(weight="bold", size=max(base_size + 4, 14)) f_h3 = base.copy() f_h3.configure(weight="bold", size=max(base_size + 2, 12)) txt_widget.tag_configure("rt_bold", font=f_bold) txt_widget.tag_configure("rt_h1", font=f_h1, spacing1=6, spacing3=4) txt_widget.tag_configure("rt_h2", font=f_h2, spacing1=4, spacing3=3) txt_widget.tag_configure("rt_h3", font=f_h3, spacing1=3, spacing3=2) txt_widget.tag_configure("rt_li", lmargin1=22, lmargin2=36) txt_widget._aza_rich_fonts = (f_bold, f_h1, f_h2, f_h3) txt_widget._aza_rich_tags_ready = True def _insert_markdown_inline(txt_widget, text: str): pos = 0 for m in re.finditer(r"\*\*(.+?)\*\*|__(.+?)__", text or ""): plain = text[pos:m.start()] if plain: txt_widget.insert(tk.INSERT, plain) chunk = m.group(1) if m.group(1) is not None else (m.group(2) or "") if chunk: s = txt_widget.index(tk.INSERT) txt_widget.insert(tk.INSERT, chunk) e = txt_widget.index(tk.INSERT) txt_widget.tag_add("rt_bold", s, e) pos = m.end() tail = (text or "")[pos:] if tail: txt_widget.insert(tk.INSERT, tail) def _insert_markdown_as_rich_text(txt_widget, raw_text: str): _ensure_rich_tags(txt_widget) lines = (raw_text or "").replace("\r\n", "\n").replace("\r", "\n").split("\n") for i, raw_line in enumerate(lines): stripped = raw_line.strip() tag_name = None text_to_insert = raw_line is_list = False if stripped: m_head = re.match(r"^(#{1,3})\s+(.*)$", stripped) m_list = re.match(r"^[-*•]\s+(.*)$", stripped) if m_head: level = len(m_head.group(1)) text_to_insert = m_head.group(2) tag_name = {1: "rt_h1", 2: "rt_h2", 3: "rt_h3"}.get(level, "rt_h3") elif m_list: text_to_insert = m_list.group(1) is_list = True line_start = txt_widget.index(tk.INSERT) if is_list: txt_widget.insert(tk.INSERT, "• ") _insert_markdown_inline(txt_widget, text_to_insert) line_end = txt_widget.index(tk.INSERT) if tag_name and txt_widget.compare(line_end, ">", line_start): txt_widget.tag_add(tag_name, line_start, line_end) if is_list and txt_widget.compare(line_end, ">", line_start): txt_widget.tag_add("rt_li", line_start, line_end) if i < len(lines) - 1: txt_widget.insert(tk.INSERT, "\n") def _set_widget_from_markdown(txt_widget, raw_text: str): txt_widget.delete("1.0", "end") if raw_text: _insert_markdown_as_rich_text(txt_widget, raw_text) def _range_to_markdown_inline(txt_widget, start: str, end: str) -> str: idx = start out = [] bold_on = False while txt_widget.compare(idx, "<", end): ch = txt_widget.get(idx, f"{idx}+1c") is_bold = "rt_bold" in txt_widget.tag_names(idx) if is_bold and not bold_on: out.append("**") bold_on = True elif not is_bold and bold_on: out.append("**") bold_on = False out.append(ch) idx = f"{idx}+1c" if bold_on: out.append("**") return "".join(out) def _widget_to_markdown(txt_widget) -> str: end_idx = txt_widget.index("end-1c") try: total_lines = int(end_idx.split(".")[0]) except Exception: return "" out_lines = [] for line_no in range(1, total_lines + 1): line_start = f"{line_no}.0" line_end = f"{line_no}.end" line_text = txt_widget.get(line_start, line_end) line_md = _range_to_markdown_inline(txt_widget, line_start, line_end) tags = txt_widget.tag_names(line_start) prefix = "" if "rt_h1" in tags: prefix = "# " elif "rt_h2" in tags: prefix = "## " elif "rt_h3" in tags: prefix = "### " elif "rt_li" in tags: stripped = line_text.lstrip() if stripped.startswith("•"): stripped = stripped[1:].lstrip() line_md = stripped prefix = "- " out_lines.append((prefix + line_md) if (prefix or line_md) else "") return "\n".join(out_lines) def _range_to_html_inline(txt_widget, start: str, end: str) -> str: idx = start out = [] bold_on = False while txt_widget.compare(idx, "<", end): ch = txt_widget.get(idx, f"{idx}+1c") is_bold = "rt_bold" in txt_widget.tag_names(idx) if is_bold and not bold_on: out.append("") bold_on = True elif not is_bold and bold_on: out.append("") bold_on = False out.append(html.escape(ch)) idx = f"{idx}+1c" if bold_on: out.append("") return "".join(out) def _selection_to_html_fragment(txt_widget) -> str: try: sel_start = txt_widget.index("sel.first") sel_end = txt_widget.index("sel.last") except Exception: return "" sel_text = txt_widget.get(sel_start, sel_end) if not sel_text: return "" lines = sel_text.split("\n") cursor = sel_start html_parts = [] in_ul = False for i, line in enumerate(lines): line_start = cursor line_end = f"{line_start}+{len(line)}c" line_html = _range_to_html_inline(txt_widget, line_start, line_end) tags = txt_widget.tag_names(line_start) is_h1 = "rt_h1" in tags is_h2 = "rt_h2" in tags is_h3 = "rt_h3" in tags is_li = "rt_li" in tags if not line: if in_ul: html_parts.append("") in_ul = False html_parts.append("
") elif is_li: if not in_ul: html_parts.append("") in_ul = False if is_h1: html_parts.append(f"

{line_html}

") elif is_h2: html_parts.append(f"

{line_html}

") elif is_h3: html_parts.append(f"

{line_html}

") else: html_parts.append(f"

{line_html}

") if i < len(lines) - 1: cursor = f"{line_end}+1c" if in_ul: html_parts.append("") return "".join(html_parts) class AzaNotizenMixin: """Mixin: Notizen-Fenster mit Reitern für Projektdokumentation.""" _notizen_window = None def open_notizen_window(self): if self._notizen_window is not None: try: if self._notizen_window.winfo_exists(): self._notizen_window.lift() self._notizen_window.focus_force() return except Exception: pass self._notizen_window = None win = tk.Toplevel(self) win.title("Projekt-Notizen") win.minsize(650, 480) win.configure(bg="#B9ECFA") win.attributes("-topmost", True) self._notizen_window = win self._register_window(win) setup_window_geometry_saving(win, "notizen", 820, 620) add_resize_grip(win, 650, 480) add_font_scale_control(win) header = tk.Frame(win, bg="#B9ECFA") header.pack(fill="x") tk.Label( header, text="📋 Projekt-Notizen", font=("Segoe UI", 12, "bold"), bg="#B9ECFA", fg="#1a4d6d", ).pack(side="left", padx=10, pady=6) notebook = ttk.Notebook(win) notebook.pack(fill="both", expand=True, padx=8, pady=(0, 4)) os.makedirs(_NOTIZEN_DIR, exist_ok=True) editors = {} auto_save_ids = {} # ──────────────────────────────────────────── # Feste Tabs (Projektstatus, Changelog, Prompt) # ──────────────────────────────────────────── for tab_label, filename in _FIXED_TABS: frame = ttk.Frame(notebook, padding=6) notebook.add(frame, text=tab_label) filepath = os.path.join(_NOTIZEN_DIR, filename) top_bar = ttk.Frame(frame) top_bar.pack(fill="x", pady=(0, 4)) ttk.Label(top_bar, text=filename, font=("Segoe UI", 9, "italic")).pack(side="left") txt = ScrolledText(frame, wrap="word", font=("Segoe UI", 10), bg="#F5FCFF", undo=True) txt.pack(fill="both", expand=True) _ensure_rich_tags(txt) _enable_direct_paste(txt) add_text_font_size_control(top_bar, txt, initial_size=10, bg_color="#B9ECFA", save_key=f"notizen_{filename}") content = _load_file(filepath) if content: _set_widget_from_markdown(txt, content) txt.edit_reset() save_label = tk.Label(top_bar, text="", fg="#388E3C", bg="#B9ECFA", font=("Segoe UI", 8)) save_label.pack(side="right", padx=6) editors[filename] = txt auto_save_ids[filename] = None _bind_auto_save(win, txt, filename, editors, auto_save_ids, lambda fn, sl: lambda: _auto_save(win, fn, sl, editors, _NOTIZEN_DIR), save_label) # ──────────────────────────────────────────── # "Eigene Notizen" – Container-Tab mit Sub-Notebook # ──────────────────────────────────────────── eigene_outer = ttk.Frame(notebook, padding=4) notebook.add(eigene_outer, text="Eigene Notizen") eigene_btn_bar = tk.Frame(eigene_outer, bg="#B9ECFA") eigene_btn_bar.pack(fill="x", pady=(0, 4)) sub_notebook = ttk.Notebook(eigene_outer) sub_notebook.pack(fill="both", expand=True) eigene_tabs_data = _load_eigene_tabs() os.makedirs(_EIGENE_DIR, exist_ok=True) sub_editors = {} sub_auto_save_ids = {} sub_recorders = {} sub_recording = {} sub_rec_buttons = {} sub_status_vars = {} def _build_eigene_tab(tab_info: dict, select=False): title = tab_info["title"] filename = tab_info["filename"] filepath = os.path.join(_EIGENE_DIR, filename) frame = ttk.Frame(sub_notebook, padding=6) sub_notebook.add(frame, text=title) top_bar = ttk.Frame(frame) top_bar.pack(fill="x", pady=(0, 4)) ttk.Label(top_bar, text=filename, font=("Segoe UI", 9, "italic")).pack(side="left") txt = ScrolledText(frame, wrap="word", font=("Segoe UI", 10), bg="#F5FCFF", undo=True) txt.pack(fill="both", expand=True) _ensure_rich_tags(txt) _enable_direct_paste(txt) add_text_font_size_control(top_bar, txt, initial_size=10, bg_color="#B9ECFA", save_key=f"notizen_eigene_{filename}") content = _load_file(filepath) if content: _set_widget_from_markdown(txt, content) txt.edit_reset() save_label = tk.Label(top_bar, text="", fg="#388E3C", bg="#B9ECFA", font=("Segoe UI", 8)) save_label.pack(side="right", padx=6) sub_editors[filename] = txt sub_auto_save_ids[filename] = None _bind_auto_save(win, txt, filename, sub_editors, sub_auto_save_ids, lambda fn, sl: lambda: _auto_save(win, fn, sl, sub_editors, _EIGENE_DIR), save_label) # Diktat + Status pro Sub-Tab bottom = ttk.Frame(frame, padding=(0, 4, 0, 0)) bottom.pack(fill="x") status_var = tk.StringVar(value="") sub_status_vars[filename] = status_var sub_recorders[filename] = None sub_recording[filename] = False def _toggle_diktat(fn=filename): if not hasattr(self, 'ensure_ready') or not self.ensure_ready(): return if sub_recorders[fn] is None: sub_recorders[fn] = AudioRecorder() rec = sub_recorders[fn] if not sub_recording[fn]: try: rec.start() sub_recording[fn] = True sub_rec_buttons[fn].configure(text="⏹ Stopp") sub_status_vars[fn].set("Aufnahme läuft…") except Exception as e: sub_status_vars[fn].set(f"Fehler: {e}") else: sub_recording[fn] = False sub_rec_buttons[fn].configure(text="⏺ Diktieren") sub_status_vars[fn].set("Transkribiere…") def worker(fn_w=fn): def _safe(callback): try: if self.winfo_exists(): self.after(0, callback) except Exception: pass try: wav_path = sub_recorders[fn_w].stop_and_save_wav() try: with wave.open(wav_path, 'rb') as wf: if wf.getnframes() / float(wf.getframerate()) < 0.3: try: os.remove(wav_path) except Exception: pass sub_recorders[fn_w] = None _safe(lambda: sub_status_vars[fn_w].set("Kein Audio erkannt.")) return except Exception: pass result = self.transcribe_wav(wav_path) text = result.text if hasattr(result, 'text') else (result if isinstance(result, str) else "") try: os.remove(wav_path) except Exception: pass if not text or not text.strip(): sub_recorders[fn_w] = None _safe(lambda: sub_status_vars[fn_w].set("Kein Text erkannt.")) return if hasattr(self, '_diktat_apply_punctuation'): text = self._diktat_apply_punctuation(text) def _insert(t=text): sub_recorders[fn_w] = None tw = sub_editors.get(fn_w) try: if tw and tw.winfo_exists(): tw.configure(state="normal") cur = tw.get("1.0", "end-1c") if cur and not cur.endswith("\n") and not cur.endswith(" "): tw.insert(tk.INSERT, " ") tw.insert(tk.INSERT, t) sub_status_vars[fn_w].set("Diktat eingefügt.") except Exception: pass _safe(_insert) except Exception as e: def _err(err=e): sub_recorders[fn_w] = None sub_status_vars[fn_w].set(f"Fehler: {err}") _safe(_err) threading.Thread(target=worker, daemon=True).start() btn_r = RoundedButton(bottom, "⏺ Diktieren", command=_toggle_diktat, width=140, height=26, canvas_bg="#B9ECFA") btn_r.pack(side="left") sub_rec_buttons[filename] = btn_r status_bar = tk.Frame(bottom, bg="#FFE4CC", height=20, padx=6, pady=2) status_bar.pack(side="left", fill="x", expand=True, padx=(8, 0)) status_bar.pack_propagate(False) tk.Label(status_bar, textvariable=status_var, fg="#BD4500", bg="#FFE4CC", font=("Segoe UI", 8), anchor="w").pack(side="left", fill="x", expand=True) if select: sub_notebook.select(frame) for tab_info in eigene_tabs_data: _build_eigene_tab(tab_info) def _add_eigene_tab(): title = simpledialog.askstring("Neue Notiz", "Titel für die neue Notiz:", parent=win) if not title or not title.strip(): return title = title.strip() safe_name = "".join(c if c.isalnum() or c in " _-" else "_" for c in title).strip().replace(" ", "_").lower() if not safe_name: safe_name = "notiz" existing_fns = {t["filename"] for t in eigene_tabs_data} fn = f"{safe_name}.md" counter = 2 while fn in existing_fns: fn = f"{safe_name}_{counter}.md" counter += 1 new_tab = {"title": title, "filename": fn} eigene_tabs_data.append(new_tab) _save_eigene_tabs(eigene_tabs_data) _build_eigene_tab(new_tab, select=True) def _remove_eigene_tab(): if not sub_notebook.tabs(): return idx = sub_notebook.index(sub_notebook.select()) if idx < 0 or idx >= len(eigene_tabs_data): return tab_info = eigene_tabs_data[idx] if len(eigene_tabs_data) <= 1: messagebox.showinfo("Hinweis", "Die letzte Notiz kann nicht gelöscht werden.", parent=win) return if not messagebox.askyesno( "Notiz löschen", f"Notiz «{tab_info['title']}» wirklich löschen?\n\nDie Datei wird nicht gelöscht, nur der Reiter entfernt.", parent=win, ): return fn = tab_info["filename"] sub_notebook.forget(idx) eigene_tabs_data.pop(idx) sub_editors.pop(fn, None) sub_auto_save_ids.pop(fn, None) sub_recorders.pop(fn, None) sub_recording.pop(fn, None) sub_rec_buttons.pop(fn, None) sub_status_vars.pop(fn, None) _save_eigene_tabs(eigene_tabs_data) def _rename_eigene_tab(): if not sub_notebook.tabs(): return idx = sub_notebook.index(sub_notebook.select()) if idx < 0 or idx >= len(eigene_tabs_data): return tab_info = eigene_tabs_data[idx] new_title = simpledialog.askstring( "Notiz umbenennen", f"Neuer Titel für «{tab_info['title']}»:", parent=win, initialvalue=tab_info["title"], ) if not new_title or not new_title.strip(): return tab_info["title"] = new_title.strip() sub_notebook.tab(idx, text=new_title.strip()) _save_eigene_tabs(eigene_tabs_data) RoundedButton( eigene_btn_bar, "+ Neue Notiz", command=_add_eigene_tab, width=110, height=26, canvas_bg="#B9ECFA", bg="#A8D8B9", fg="#1a4d3d", active_bg="#8CC8A5", ).pack(side="left", padx=(0, 4)) RoundedButton( eigene_btn_bar, "− Entfernen", command=_remove_eigene_tab, width=100, height=26, canvas_bg="#B9ECFA", bg="#E8B4B4", fg="#5A1A1A", active_bg="#D4A0A0", ).pack(side="left", padx=(0, 4)) RoundedButton( eigene_btn_bar, "✏ Umbenennen", command=_rename_eigene_tab, width=110, height=26, canvas_bg="#B9ECFA", ).pack(side="left", padx=(0, 4)) # ──────────────────────────────────────────── # Globale Buttons (unter dem Hauptnotebook) # ──────────────────────────────────────────── global_btn_frame = tk.Frame(win, bg="#B9ECFA") global_btn_frame.pack(fill="x", padx=8, pady=(0, 6)) def _reload_current(): main_idx = notebook.index(notebook.select()) if main_idx < len(_FIXED_TABS): fn = _FIXED_TABS[main_idx][1] tw = editors.get(fn) if tw is None: return fpath = os.path.join(_NOTIZEN_DIR, fn) content = _load_file(fpath) try: _set_widget_from_markdown(tw, content) tw.edit_reset() tw.edit_modified(False) except Exception: pass else: if not sub_notebook.tabs(): return sub_idx = sub_notebook.index(sub_notebook.select()) if sub_idx < 0 or sub_idx >= len(eigene_tabs_data): return fn = eigene_tabs_data[sub_idx]["filename"] tw = sub_editors.get(fn) if tw is None: return fpath = os.path.join(_EIGENE_DIR, fn) content = _load_file(fpath) try: _set_widget_from_markdown(tw, content) tw.edit_reset() tw.edit_modified(False) except Exception: pass RoundedButton( global_btn_frame, "↻ Neu laden", command=_reload_current, width=100, height=28, canvas_bg="#B9ECFA", ).pack(side="left", padx=(0, 8)) def _save_all(): for fn, tw in editors.items(): try: if not tw.winfo_exists(): continue _save_file(os.path.join(_NOTIZEN_DIR, fn), _widget_to_markdown(tw)) except Exception: pass for fn, tw in sub_editors.items(): try: if not tw.winfo_exists(): continue _save_file(os.path.join(_EIGENE_DIR, fn), _widget_to_markdown(tw)) except Exception: pass RoundedButton( global_btn_frame, "💾 Alle speichern", command=_save_all, width=130, height=28, canvas_bg="#B9ECFA", ).pack(side="left") def _on_notizen_close(): _save_all() self._notizen_window = None if hasattr(self, "_aza_windows"): self._aza_windows.discard(win) try: save_toplevel_geometry("notizen", win.geometry()) except Exception: pass win.destroy() win.protocol("WM_DELETE_WINDOW", _on_notizen_close) def update_notizen_file(self, filename: str, append_text: str): """Von aussen aufrufbar: Text an eine Notiz-Datei anhängen.""" fpath = os.path.join(_NOTIZEN_DIR, filename) os.makedirs(_NOTIZEN_DIR, exist_ok=True) try: existing = _load_file(fpath) or "" with open(fpath, "w", encoding="utf-8") as f: f.write(existing.rstrip("\n") + "\n" + append_text + "\n") except Exception: pass def _bind_auto_save(win, txt_widget, filename, editors_dict, save_ids_dict, save_factory, save_label): def _on_change(event=None): aid = save_ids_dict.get(filename) if aid is not None: try: win.after_cancel(aid) except Exception: pass save_ids_dict[filename] = win.after(1500, save_factory(filename, save_label)) def _on_modified(event=None): if txt_widget.edit_modified(): _on_change() txt_widget.edit_modified(False) txt_widget.bind("<>", _on_modified) txt_widget.bind("", _on_change) def _enable_direct_paste(txt_widget): """Ermöglicht direktes Kopieren/Einfügen in Notizfelder (Tastatur + Rechtsklick).""" _ensure_rich_tags(txt_widget) def _toggle_bold(event=None): try: start = txt_widget.index("sel.first") end = txt_widget.index("sel.last") except Exception: return "break" idx = start fully_bold = True while txt_widget.compare(idx, "<", end): if "rt_bold" not in txt_widget.tag_names(idx): fully_bold = False break idx = f"{idx}+1c" if fully_bold: txt_widget.tag_remove("rt_bold", start, end) else: txt_widget.tag_add("rt_bold", start, end) return "break" def _copy_selection(event=None): try: sel = txt_widget.get("sel.first", "sel.last") html_fragment = _selection_to_html_fragment(txt_widget) except Exception: return "break" if sel: if not _win_clipboard_set(sel, html_fragment=html_fragment): try: txt_widget.clipboard_clear() txt_widget.clipboard_append(sanitize_markdown_for_plain_text(sel)) except Exception: pass return "break" def _paste_from_clipboard(event=None): try: txt = txt_widget.clipboard_get() try: if txt_widget.tag_ranges("sel"): txt_widget.delete("sel.first", "sel.last") except Exception: pass _insert_markdown_as_rich_text(txt_widget, txt) except Exception: pass return "break" def _show_context_menu(event): try: menu = tk.Menu(txt_widget, tearoff=0) menu.add_command(label="Kopieren", command=lambda: _copy_selection()) menu.add_command(label="Einfügen", command=lambda: _paste_from_clipboard()) menu.add_command(label="Fett umschalten", command=lambda: _toggle_bold()) menu.tk_popup(event.x_root, event.y_root) finally: try: menu.grab_release() except Exception: pass return "break" txt_widget.bind("", _copy_selection) txt_widget.bind("", _copy_selection) txt_widget.bind("", _copy_selection) txt_widget.bind("", _toggle_bold) txt_widget.bind("", _toggle_bold) txt_widget.bind("", _paste_from_clipboard) txt_widget.bind("", _paste_from_clipboard) txt_widget.bind("", _paste_from_clipboard) txt_widget.bind("", _show_context_menu) def _auto_save(win, filename, label_widget, editors_dict, base_dir): tw = editors_dict.get(filename) if tw is None: return try: if not tw.winfo_exists(): return except Exception: return content = _widget_to_markdown(tw) fpath = os.path.join(base_dir, filename) try: _save_file(fpath, content) try: if label_widget.winfo_exists(): label_widget.configure(text="gespeichert ✓", fg="#388E3C") win.after(3000, lambda: _clear_label(label_widget)) except Exception: pass except Exception: try: if label_widget.winfo_exists(): label_widget.configure(text="Fehler beim Speichern", fg="#D32F2F") except Exception: pass def _clear_label(lbl): try: if lbl.winfo_exists(): lbl.configure(text="") except Exception: pass