1047 lines
39 KiB
Python
1047 lines
39 KiB
Python
|
|
# -*- 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("<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(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("<<Modified>>", _on_modified)
|
|||
|
|
txt_widget.bind("<KeyRelease>", _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("<I", raw, 0)[0]
|
|||
|
|
|
|||
|
|
bmp_file_hdr = struct.pack(
|
|||
|
|
"<2sIHHI", b"BM",
|
|||
|
|
14 + len(raw), 0, 0, 14 + hdr_size
|
|||
|
|
)
|
|||
|
|
img = Image.open(io.BytesIO(bmp_file_hdr + raw))
|
|||
|
|
img = img.convert("RGBA")
|
|||
|
|
|
|||
|
|
fn = f"paste_{uuid.uuid4().hex[:8]}.png"
|
|||
|
|
os.makedirs(_IMAGES_DIR, exist_ok=True)
|
|||
|
|
img.save(os.path.join(_IMAGES_DIR, fn), "PNG")
|
|||
|
|
_insert_image_at_cursor(txt_widget, fn)
|
|||
|
|
return True
|
|||
|
|
finally:
|
|||
|
|
kernel32.GlobalUnlock(h)
|
|||
|
|
finally:
|
|||
|
|
user32.CloseClipboard()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# --- Methode 2: PIL ImageGrab (Fallback) ---
|
|||
|
|
try:
|
|||
|
|
from PIL import ImageGrab
|
|||
|
|
clip = ImageGrab.grabclipboard()
|
|||
|
|
if clip is not None:
|
|||
|
|
if isinstance(clip, Image.Image):
|
|||
|
|
fn = f"paste_{uuid.uuid4().hex[:8]}.png"
|
|||
|
|
os.makedirs(_IMAGES_DIR, exist_ok=True)
|
|||
|
|
clip.save(os.path.join(_IMAGES_DIR, fn), "PNG")
|
|||
|
|
_insert_image_at_cursor(txt_widget, fn)
|
|||
|
|
return True
|
|||
|
|
if isinstance(clip, list):
|
|||
|
|
_IMG_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".tiff"}
|
|||
|
|
for fpath in clip:
|
|||
|
|
if isinstance(fpath, str) and os.path.isfile(fpath):
|
|||
|
|
if os.path.splitext(fpath)[1].lower() in _IMG_EXTS:
|
|||
|
|
stored_fn = _copy_image_to_store(fpath)
|
|||
|
|
if stored_fn:
|
|||
|
|
_insert_image_at_cursor(txt_widget, stored_fn)
|
|||
|
|
return True
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
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):
|
|||
|
|
if getattr(txt_widget, "_aza_images_enabled", False) and _HAS_PIL:
|
|||
|
|
txt_widget.after(15, _deferred_smart_paste)
|
|||
|
|
return "break"
|
|||
|
|
_do_text_paste()
|
|||
|
|
return "break"
|
|||
|
|
|
|||
|
|
def _do_text_paste():
|
|||
|
|
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
|
|||
|
|
|
|||
|
|
def _deferred_smart_paste():
|
|||
|
|
try:
|
|||
|
|
if not txt_widget.winfo_exists():
|
|||
|
|
return
|
|||
|
|
except Exception:
|
|||
|
|
return
|
|||
|
|
pasted = _try_paste_image_from_clipboard(txt_widget)
|
|||
|
|
if not pasted:
|
|||
|
|
_do_text_paste()
|
|||
|
|
|
|||
|
|
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())
|
|||
|
|
if getattr(txt_widget, "_aza_images_enabled", False) and _HAS_PIL:
|
|||
|
|
menu.add_separator()
|
|||
|
|
menu.add_command(label="Bild einfügen…", command=lambda: _image_dialog_insert(txt_widget))
|
|||
|
|
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
|