# -*- 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"")
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("
{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("<