Files
Kerim AtilganandClaude Opus 5.5 851fd6600a Modernes Design (Standard, Win95 als Legacy), Zeitraum abrechnen, Abrechnung erledigt, Linux-Build
- Design: „modern“ (hell, flach, wie Windows 11, normaler Windows-Rahmen) ist Standard;
  „win95“ bleibt unter Datei > Einstellungen wählbar (Einstellung „design“).
- Export: Zeitraum von–bis (nach Aktivierungsdatum oder Abrechnungszeitraum des Anbieters),
  exportierte Positionen werden als abgerechnet gekennzeichnet (Betrag/Händler eingefroren) und
  per Fingerabdruck auch in späteren Dateien/Abrechnungen nicht noch einmal abgerechnet.
- Datei > Abrechnung erledigt / wieder öffnen: erledigte Abrechnungen sind gegen Änderungen gesperrt.
- DB-Schema v4 (Migration inkl. Fingerabdrücke für alte Positionen).
- Linux-Build (python:3.12-bookworm) als zweiter Pipeline-Job mit Tests und Selbsttest;
  Release-Upload für beide Jobs über build/release_hochladen.sh.
- Behoben: normaler Windows-Rahmen stürzte beim Start ab (Hauptrahmen.nativ fehlte).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:47:00 +01:00

305 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Menüs im Windows-95-Stil (Menüleiste, Aufklapp- und Kontextmenüs).
Die nativen Windows-Menüs sind weiß und wechseln nicht mit, wenn man bei offenem Menü mit der
Maus zum nächsten Menüleisten-Eintrag fährt. Diese Menüs sind selbst gezeichnet:
- grau, Markierung in Navy, Tastenkürzel rechtsbündig, Trennlinien eingelassen
- bei offenem Menü wechselt das Menü beim Überfahren der Menüleiste
- Tastatur: Pfeil hoch/runter wählen, Enter ausführen, Esc schließen, Pfeil links/rechts benachbartes Menü
- Klick daneben oder Wechsel in ein anderes Programm schließt das Menü
Die Schnittstelle (add_command, add_separator, tk_popup) entspricht tk.Menu.
"""
from __future__ import annotations
import tkinter as tk
from provtool.gui import win95 as w
class Popup:
"""Ein Aufklapp- bzw. Kontextmenü."""
def __init__(self, master: tk.Misc):
self.master = master
self.eintraege: list[dict] = []
self.fenster: tk.Toplevel | None = None
self.zeilen: list[tuple[dict, tk.Frame, list[tk.Label]]] = []
self.aktiv = -1
self.leiste: Menueleiste | None = None
self._pruefer = None
# ------------------------------------------------------------- tk.Menu-kompatibel
def add_command(self, label: str, command=None, accelerator: str = "", state: str = "normal"):
self.eintraege.append(dict(art="befehl", label=label, command=command, accelerator=accelerator,
state=state))
def add_separator(self):
self.eintraege.append(dict(art="trenner"))
def tk_popup(self, x: int, y: int):
self.oeffnen(x, y)
# ------------------------------------------------------------- Anzeige
@property
def offen(self) -> bool:
try:
return self.fenster is not None and bool(self.fenster.winfo_exists())
except tk.TclError:
return False
def oeffnen(self, x: int, y: int, leiste: Menueleiste | None = None):
self.schliessen()
self.leiste = leiste
root = self.master._root()
f = self.fenster = tk.Toplevel(root, bg=w.FACE, bd=2, relief="raised")
if w.MODERN:
f.configure(bg=w.WHITE, bd=0, relief="flat", highlightthickness=1, highlightbackground=w.RAND)
f.withdraw()
f.overrideredirect(True)
self.zeilen = []
grund, py = (w.WHITE, 4) if w.MODERN else (w.FACE, 2)
for e in self.eintraege:
if e["art"] == "trenner":
t = tk.Frame(f, bg=grund, height=8)
t.pack(fill="x")
linie = w.trenner(t, vertikal=False)
linie.place(relx=0, rely=0.5, relwidth=1, y=-1)
continue
zeile = tk.Frame(f, bg=grund)
zeile.pack(fill="x")
grau = e["state"] == "disabled"
text = tk.Label(zeile, text=e["label"], bg=grund, anchor="w", padx=18, pady=py,
fg=w.SHADOW if grau else w.DARK)
text.pack(side="left", fill="x", expand=True)
labels = [text]
if e["accelerator"]:
acc = tk.Label(zeile, text=e["accelerator"], bg=grund, anchor="e", padx=12, pady=py,
fg=w.SHADOW if grau else w.DARK)
acc.pack(side="right")
labels.append(acc)
idx = len(self.zeilen)
for wdg in [zeile] + labels:
wdg.bind("<Enter>", lambda ev, i=idx: self._markieren(i))
wdg.bind("<ButtonRelease-1>", lambda ev, i=idx: self._ausfuehren(i))
self.zeilen.append((e, zeile, labels))
f.update_idletasks()
bw, bh = f.winfo_reqwidth(), f.winfo_reqheight()
sw, sh = f.winfo_screenwidth(), f.winfo_screenheight()
x = max(0, min(x, sw - bw))
y = max(0, min(y, sh - bh))
# Größe UND Position zusammen setzen: eine reine Positionsangabe ignoriert Tk unter Windows bei
# rahmenlosen Fenstern – das Menü landete sonst oben links in der Bildschirmecke.
f.geometry(f"{bw}x{bh}+{x}+{y}")
f.deiconify()
f.lift()
f.update_idletasks()
try:
from provtool.gui import win32
win32.besitzer_setzen(f, root) # bleibt über dem Hauptfenster
except Exception:
pass
# Alle Maus-Ereignisse der Anwendung an das Menü (lokale Tk-Sperre): Klick daneben schließt,
# Bewegung über die Menüleiste wechselt das Menü.
try:
f.grab_set()
except tk.TclError:
pass
f.focus_force()
f.bind("<ButtonPress>", self._klick)
f.bind("<Motion>", self._bewegung)
f.bind("<Escape>", lambda e: self.schliessen())
f.bind("<Up>", lambda e: self._schritt(-1))
f.bind("<Down>", lambda e: self._schritt(1))
f.bind("<Return>", lambda e: self._ausfuehren(self.aktiv))
f.bind("<KP_Enter>", lambda e: self._ausfuehren(self.aktiv))
f.bind("<Left>", lambda e: self.leiste and self.leiste.nachbar(-1))
f.bind("<Right>", lambda e: self.leiste and self.leiste.nachbar(1))
self.aktiv = -1
self._pruefer = f.after(250, self._fokus_pruefen)
def schliessen(self):
if self._pruefer is not None and self.fenster is not None:
try:
self.fenster.after_cancel(self._pruefer)
except tk.TclError:
pass
self._pruefer = None
if self.offen:
try:
self.fenster.grab_release()
except tk.TclError:
pass
self.fenster.destroy()
self.fenster = None
if self.leiste is not None:
self.leiste._geschlossen(self)
# ------------------------------------------------------------- Bedienung
def _innerhalb(self, x_root: int, y_root: int) -> bool:
"""Liegt der Punkt über diesem Menü? Fragt das Fenstersystem, welches Fenster dort liegt –
die Geometrie rahmenloser Fenster meldet Tk unter Windows nicht zuverlässig."""
f = self.fenster
try:
unter = f.winfo_containing(x_root, y_root)
except (tk.TclError, KeyError):
unter = None
if unter is not None:
return unter.winfo_toplevel() is f
return (f.winfo_rootx() <= x_root < f.winfo_rootx() + f.winfo_width()
and f.winfo_rooty() <= y_root < f.winfo_rooty() + f.winfo_height())
def _klick(self, e):
if self._innerhalb(e.x_root, e.y_root):
return
if self.leiste is not None and self.leiste.knopf_bei(e.x_root, e.y_root) is not None:
knopf = self.leiste.knopf_bei(e.x_root, e.y_root)
if knopf is self.leiste.offener_knopf:
self.schliessen() # Klick auf den eigenen Menütitel klappt zu
else:
self.leiste.oeffnen(knopf)
return "break"
self.schliessen()
return "break"
def _bewegung(self, e):
if self.leiste is None or self._innerhalb(e.x_root, e.y_root):
return
knopf = self.leiste.knopf_bei(e.x_root, e.y_root)
if knopf is not None and knopf is not self.leiste.offener_knopf:
self.leiste.oeffnen(knopf)
def _markieren(self, i: int):
self.aktiv = i
for j, (e, zeile, labels) in enumerate(self.zeilen):
an = j == i and e.get("state") != "disabled"
bg = w.SEL_BG if an else (w.WHITE if w.MODERN else w.FACE)
fg = w.SEL_FG if an else (w.SHADOW if e.get("state") == "disabled" else w.DARK)
zeile.configure(bg=bg)
for lbl in labels:
lbl.configure(bg=bg, fg=fg)
def _schritt(self, richtung: int):
if not self.zeilen:
return
i = self.aktiv
for _ in range(len(self.zeilen)):
i = (i + richtung) % len(self.zeilen)
if self.zeilen[i][0].get("state") != "disabled":
break
self._markieren(i)
def _ausfuehren(self, i: int):
if not (0 <= i < len(self.zeilen)):
return
e = self.zeilen[i][0]
if e.get("state") == "disabled":
return
self.schliessen()
if e.get("command"):
# nach dem Schließen ausführen (der Befehl öffnet evtl. einen Dialog)
self.master.after_idle(e["command"])
def _fokus_pruefen(self):
"""In ein anderes Programm gewechselt -> Menü schließen."""
self._pruefer = None
if not self.offen:
return
try:
weg = self.fenster.focus_get() is None
except (tk.TclError, KeyError):
weg = False
if weg:
self.schliessen()
else:
self._pruefer = self.fenster.after(250, self._fokus_pruefen)
class Menueleiste(tk.Frame):
"""Graue Menüleiste mit Popup-Menüs (die native von Windows 10/11 ist weiß)."""
def __init__(self, master, root: tk.Misc):
super().__init__(master, bg=w.FACE)
self.root = root
self.knoepfe: dict[str, tk.Label] = {}
self.menues: dict[tk.Label, Popup] = {}
self.offener_knopf: tk.Label | None = None
def menue(self, label: str, underline: int | None = 0) -> Popup:
knopf = tk.Label(self, text=label, bg=w.FACE, padx=7, pady=2, bd=1, relief="flat",
underline=-1 if underline is None else underline)
knopf.pack(side="left")
popup = Popup(self)
self.knoepfe[label] = knopf
self.menues[knopf] = popup
knopf.bind("<ButtonPress-1>", lambda e, k=knopf: self._klick(k))
knopf.bind("<Enter>", lambda e, k=knopf: self._hover(k, True))
knopf.bind("<Leave>", lambda e, k=knopf: self._hover(k, False))
if underline is not None:
taste = label[underline].lower()
self.root.bind_all(f"<Alt-KeyPress-{taste}>", lambda e, k=knopf: self._alt(k))
return popup
# ---- Zustand
def knopf_bei(self, x_root: int, y_root: int) -> tk.Label | None:
for k in self.knoepfe.values():
if (k.winfo_rootx() <= x_root < k.winfo_rootx() + k.winfo_width()
and k.winfo_rooty() <= y_root < k.winfo_rooty() + k.winfo_height()):
return k
return None
def _darstellen(self):
for k in self.knoepfe.values():
if k is self.offener_knopf:
k.configure(bg=w.SEL_BG, fg=w.SEL_FG, relief="flat")
else:
k.configure(bg=w.FACE, fg=w.DARK, relief="flat")
# ---- Bedienung
def oeffnen(self, knopf: tk.Label):
from provtool.gui import modal
if modal.offen():
return
alt = self.offener_knopf
if alt is not None and alt is not knopf:
self.menues[alt].leiste = None # ohne Rückmeldung schließen, wir öffnen gleich das nächste
self.menues[alt].schliessen()
self.offener_knopf = knopf
self._darstellen()
self.menues[knopf].oeffnen(knopf.winfo_rootx(), knopf.winfo_rooty() + knopf.winfo_height(), leiste=self)
def schliessen(self):
if self.offener_knopf is not None:
self.menues[self.offener_knopf].schliessen()
def _geschlossen(self, popup: Popup):
if self.offener_knopf is not None and self.menues.get(self.offener_knopf) is popup:
self.offener_knopf = None
self._darstellen()
def nachbar(self, richtung: int):
liste = list(self.knoepfe.values())
if self.offener_knopf in liste:
i = (liste.index(self.offener_knopf) + richtung) % len(liste)
self.oeffnen(liste[i])
def _klick(self, knopf):
if self.offener_knopf is knopf:
self.schliessen()
else:
self.oeffnen(knopf)
return "break"
def _hover(self, knopf, drin: bool):
if self.offener_knopf is None:
if w.MODERN:
knopf.configure(bg=w.LIGHT if drin else w.FACE)
else:
knopf.configure(relief="raised" if drin else "flat")
def _alt(self, knopf):
self.oeffnen(knopf)
if self.offener_knopf is knopf:
self.menues[knopf]._schritt(1)
return "break"