# -*- 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, filedialog 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 from aza_config import get_writable_data_dir try: from PIL import Image, ImageTk _HAS_PIL = True except ImportError: _HAS_PIL = False _NOTIZEN_DIR = os.path.join(get_writable_data_dir(), "notizen") _EIGENE_DIR = os.path.join(_NOTIZEN_DIR, "eigene") _IMAGES_DIR = os.path.join(_EIGENE_DIR, "images") _TABS_CONFIG = os.path.join(_NOTIZEN_DIR, "eigene_notizen_tabs.json") _MAX_IMG_WIDTH = 600 _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 _copy_image_to_store(src_path: str) -> str: """Kopiert ein Bild nach eigene/images/ und gibt den neuen Dateinamen zurueck.""" import shutil os.makedirs(_IMAGES_DIR, exist_ok=True) basename = os.path.basename(src_path) name, ext = os.path.splitext(basename) dest = os.path.join(_IMAGES_DIR, basename) counter = 2 while os.path.exists(dest): dest = os.path.join(_IMAGES_DIR, f"{name}_{counter}{ext}") counter += 1 shutil.copy2(src_path, dest) return os.path.basename(dest) def _load_and_insert_image(txt_widget, rel_filename: str): """Laedt ein Bild aus images/ und bettet es an der INSERT-Position ein.""" if not _HAS_PIL: txt_widget.insert(tk.INSERT, f"[Bild: {rel_filename}]") return fpath = os.path.join(_IMAGES_DIR, rel_filename) if not os.path.isfile(fpath): txt_widget.insert(tk.INSERT, f"[Bild nicht gefunden: {rel_filename}]") return try: img = Image.open(fpath) img.load() w, h = img.size if w > _MAX_IMG_WIDTH: ratio = _MAX_IMG_WIDTH / w img = img.resize((int(w * ratio), int(h * ratio)), Image.LANCZOS) photo = ImageTk.PhotoImage(img) except Exception: txt_widget.insert(tk.INSERT, f"[Bild-Fehler: {rel_filename}]") return if not hasattr(txt_widget, "_aza_images"): txt_widget._aza_images = [] txt_widget._aza_images.append(photo) if not hasattr(txt_widget, "_aza_image_map"): txt_widget._aza_image_map = {} img_name = txt_widget.image_create(tk.INSERT, image=photo) txt_widget._aza_image_map[img_name] = rel_filename def _insert_image_at_cursor(txt_widget, rel_filename: str): """Fuegt ein Bild an der Cursor-Position ein und sorgt fuer eigene Zeile.""" cur = txt_widget.index(tk.INSERT) col = int(cur.split(".")[1]) if col > 0: txt_widget.insert(tk.INSERT, "\n") _load_and_insert_image(txt_widget, rel_filename) txt_widget.insert(tk.INSERT, "\n") def _image_dialog_insert(txt_widget): """Oeffnet Dateidialog und fuegt das gewaehlte Bild ein.""" if not _HAS_PIL: messagebox.showinfo("Hinweis", "Pillow (PIL) wird für Bilder benötigt.", parent=txt_widget.winfo_toplevel()) return path = filedialog.askopenfilename( title="Bild einfügen", filetypes=[("Bilder", "*.png *.jpg *.jpeg *.gif *.bmp *.webp"), ("Alle Dateien", "*.*")], parent=txt_widget.winfo_toplevel(), ) if not path: return stored_fn = _copy_image_to_store(path) if stored_fn: _insert_image_at_cursor(txt_widget, stored_fn) 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() m_img = re.match(r'^!\[.*?\]\(images/(.+?)\)$', stripped) if m_img and _HAS_PIL: _load_and_insert_image(txt_widget, m_img.group(1)) if i < len(lines) - 1: txt_widget.insert(tk.INSERT, "\n") continue 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 "" img_map = getattr(txt_widget, "_aza_image_map", {}) out_lines = [] for line_no in range(1, total_lines + 1): line_start = f"{line_no}.0" line_end = f"{line_no}.end" if img_map: try: line_dump = txt_widget.dump(line_start, line_end, image=True) if line_dump: imgs = [img_map[e[1]] for e in line_dump if len(e) >= 2 and e[0] == "image" and e[1] in img_map] if imgs: for ifn in imgs: out_lines.append(f"![](images/{ifn})") continue except Exception: pass 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(700, 520) win.configure(bg="#B9ECFA") win.attributes("-topmost", True) self._notizen_window = win self._register_window(win) setup_window_geometry_saving(win, "notizen", 900, 700) add_resize_grip(win, 700, 520) 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) txt._aza_images_enabled = True _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) 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 if _HAS_PIL: RoundedButton( bottom, "Bild einfügen", command=lambda t=txt: _image_dialog_insert(t), width=120, height=26, canvas_bg="#B9ECFA", ).pack(side="left", padx=(6, 0)) 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 _try_paste_image_from_clipboard(txt_widget) -> bool: """Versucht ein Bild aus der Windows-Zwischenablage einzufuegen. Gibt True bei Erfolg.""" if not _HAS_PIL: return False import uuid # --- Methode 1: Win32 CF_DIB direkt lesen (robustester Weg) --- try: import ctypes from ctypes import wintypes import io import struct CF_DIB = 8 user32 = ctypes.windll.user32 kernel32 = ctypes.windll.kernel32 user32.OpenClipboard.argtypes = [wintypes.HWND] user32.OpenClipboard.restype = wintypes.BOOL user32.GetClipboardData.argtypes = [wintypes.UINT] user32.GetClipboardData.restype = wintypes.HANDLE user32.CloseClipboard.restype = wintypes.BOOL kernel32.GlobalLock.argtypes = [wintypes.HGLOBAL] kernel32.GlobalLock.restype = ctypes.c_void_p kernel32.GlobalUnlock.argtypes = [wintypes.HGLOBAL] kernel32.GlobalSize.argtypes = [wintypes.HGLOBAL] kernel32.GlobalSize.restype = ctypes.c_size_t if user32.IsClipboardFormatAvailable(CF_DIB): if user32.OpenClipboard(None): try: h = user32.GetClipboardData(CF_DIB) if h: ptr = kernel32.GlobalLock(h) if ptr: try: size = kernel32.GlobalSize(h) if size >= 40: raw = ctypes.string_at(ptr, size) hdr_size = struct.unpack_from("", _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