update
This commit is contained in:
831
APP/backup 24.2.26/aza_notizen_mixin.py
Normal file
831
APP/backup 24.2.26/aza_notizen_mixin.py
Normal file
@@ -0,0 +1,831 @@
|
||||
# -*- 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("<strong>")
|
||||
bold_on = True
|
||||
elif not is_bold and bold_on:
|
||||
out.append("</strong>")
|
||||
bold_on = False
|
||||
out.append(html.escape(ch))
|
||||
idx = f"{idx}+1c"
|
||||
if bold_on:
|
||||
out.append("</strong>")
|
||||
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("</ul>")
|
||||
in_ul = False
|
||||
html_parts.append("<br>")
|
||||
elif is_li:
|
||||
if not in_ul:
|
||||
html_parts.append("<ul>")
|
||||
in_ul = True
|
||||
clean = line_html.strip()
|
||||
if clean.startswith("•"):
|
||||
clean = clean[1:].lstrip()
|
||||
html_parts.append(f"<li>{clean}</li>")
|
||||
else:
|
||||
if in_ul:
|
||||
html_parts.append("</ul>")
|
||||
in_ul = False
|
||||
if is_h1:
|
||||
html_parts.append(f"<h1>{line_html}</h1>")
|
||||
elif is_h2:
|
||||
html_parts.append(f"<h2>{line_html}</h2>")
|
||||
elif is_h3:
|
||||
html_parts.append(f"<h3>{line_html}</h3>")
|
||||
else:
|
||||
html_parts.append(f"<p>{line_html}</p>")
|
||||
|
||||
if i < len(lines) - 1:
|
||||
cursor = f"{line_end}+1c"
|
||||
|
||||
if in_ul:
|
||||
html_parts.append("</ul>")
|
||||
|
||||
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("<<Modified>>", _on_modified)
|
||||
txt_widget.bind("<KeyRelease>", _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("<Control-c>", _copy_selection)
|
||||
txt_widget.bind("<Control-C>", _copy_selection)
|
||||
txt_widget.bind("<Control-Insert>", _copy_selection)
|
||||
txt_widget.bind("<Control-b>", _toggle_bold)
|
||||
txt_widget.bind("<Control-B>", _toggle_bold)
|
||||
txt_widget.bind("<Control-v>", _paste_from_clipboard)
|
||||
txt_widget.bind("<Control-V>", _paste_from_clipboard)
|
||||
txt_widget.bind("<Shift-Insert>", _paste_from_clipboard)
|
||||
txt_widget.bind("<Button-3>", _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
|
||||
Reference in New Issue
Block a user