diff --git a/app.py b/app.py index cd3fddf..e02548c 100644 --- a/app.py +++ b/app.py @@ -39,6 +39,8 @@ from bestand import (hoechster_zaehler_im_ordner, naechste_freie_nummer, vorhandene_rechnungen) from einzelinstanz import (eigene_pid_merken, einzelinstanz_pruefen, fenster_nach_vorn, laufende_pid_lesen) +import flaeche +import icons import reiter import theme from theme import MeldungMixin, ThemeMixin @@ -102,9 +104,11 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, self._apply_theme() # Farben nach Windows hell/dunkel self._build_menu() - self._build_kopf() self._build_summen() # untere Leiste (Summen + Buttons) zuerst -> bleibt immer sichtbar - self._build_positionen() # Positionsliste fuellt den Rest und scrollt + self._build_flaeche() # Werkzeugleiste + verschiebbare Kaesten + self._build_kopf() # Rechnungsdaten + Kunde als Kaesten + self._build_positionen() # Leistungen als Kasten, fuellt den Rest + self.flaeche.anordnen() self._neue_nummer_vorschlagen() self.recalc() @@ -211,15 +215,62 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, pass self._open_pop = None + # ------------------------------------------------------- Arbeitsflaeche + def _build_flaeche(self): + """Die Kaesten und der Knopf, mit dem man sie verschieben darf. + + Wie bei OPNsense: erst "Anordnen" druecken, dann schieben, dann + speichern. Ohne diesen Schritt sitzt alles fest - beim taeglichen + Arbeiten soll nichts verrutschen. + """ + leiste = ttk.Frame(self, padding=(16, 8, 16, 0)) + leiste.pack(side="top", fill="x") + self.werkzeuge = ttk.Frame(leiste) + self.werkzeuge.pack(side="right") + + self._ic_anordnen = icons.hole("dashboard_customize", 16, self.col_muted, master=self) + self._ic_speichern = icons.hole("check", 16, self.col_accent_hell, master=self) + self._ic_verwerfen = icons.hole("close", 16, self.col_muted, master=self) + + self.knopf_anordnen = ttk.Button(self.werkzeuge, text=" Anordnen", + image=self._ic_anordnen, compound="left", + command=self._anordnen_umschalten) + self.knopf_anordnen.pack(side="left") + self.knopf_speichern = ttk.Button(self.werkzeuge, text=" Speichern", + image=self._ic_speichern, compound="left", + style="Haupt.TButton", + command=self._anordnen_speichern) + self.knopf_verwerfen = ttk.Button(self.werkzeuge, text=" Verwerfen", + image=self._ic_verwerfen, compound="left", + command=self._anordnen_verwerfen) + + self.flaeche = flaeche.Arbeitsflaeche(self, farben=self) + self.flaeche.pack(side="top", fill="both", expand=True, padx=10, pady=(4, 0)) + + def _anordnen_umschalten(self): + self.flaeche.anordnen_starten() + self.knopf_anordnen.pack_forget() + self.knopf_verwerfen.pack(side="left", padx=(0, 6)) + self.knopf_speichern.pack(side="left") + + def _anordnen_fertig(self): + self.knopf_speichern.pack_forget() + self.knopf_verwerfen.pack_forget() + self.knopf_anordnen.pack(side="left") + + def _anordnen_speichern(self): + self.flaeche.anordnen_speichern() + self._anordnen_fertig() + + def _anordnen_verwerfen(self): + self.flaeche.anordnen_verwerfen() + self._anordnen_fertig() + # ---------------------------------------------------------------- Kopf def _build_kopf(self): - top = ttk.Frame(self, padding=(16, 14, 16, 6)) - top.pack(fill="x") - - # Kunde (links) - kunde = ttk.LabelFrame(top, text="RECHNUNG AN (KUNDE)", padding=14, - style="Karte.TLabelframe") - kunde.grid(row=0, column=0, sticky="nsew", padx=(0, 14)) + # Beide Bloecke sind Kaesten der Arbeitsflaeche - sie lassen sich + # verschieben, deshalb platziert sie niemand mehr von Hand. + kunde = self.flaeche.kasten("kunde", "Rechnung an (Kunde)") self.k_anrede = tk.StringVar() self.k_vorname = tk.StringVar() self.k_name = tk.StringVar() # Nachname oder Firma – die Zuordnung im Steuerjournal @@ -249,10 +300,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, row=len(felder), column=0, columnspan=2, sticky="w", pady=(6, 0)) kunde.columnconfigure(1, weight=1) - # Rechnungsinfos (rechts) - info = ttk.LabelFrame(top, text="RECHNUNGSDATEN", padding=14, - style="Karte.TLabelframe") - info.grid(row=0, column=1, sticky="nsew") + info = self.flaeche.kasten("daten", "Rechnungsdaten") self.r_nummer = tk.StringVar() self.r_datum = tk.StringVar(value=datetime.date.today().strftime("%d.%m.%Y")) self.r_anreise = tk.StringVar() @@ -296,8 +344,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, self.r_anreise.trace_add("write", lambda *a: self._update_naechte()) self.r_abreise.trace_add("write", lambda *a: self._update_naechte()) - top.columnconfigure(0, weight=1) - top.columnconfigure(1, weight=0) + def _auto_datum_evt(self, event): """Punkte automatisch setzen, Cursor ans Ende. Nach Tag+Monat das aktuelle @@ -378,8 +425,9 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, # Eigene Reiterleiste statt ttk.Notebook: unter clam liess sich der # ruhende Reiter nicht dunkel bekommen, er sah aus wie der offene. # Nach aussen verhaelt sie sich wie ein Notebook (siehe reiter.py). - self.blaetter = reiter.Reiterleiste(self, farben=self) - self.blaetter.pack(side="top", fill="both", expand=True, padx=16, pady=(4, 6)) + leistungen = self.flaeche.kasten("leistungen", "Leistungen") + self.blaetter = reiter.Reiterleiste(leistungen, farben=self) + self.blaetter.pack(side="top", fill="both", expand=True) self.blaetter.bind("<>", lambda _e: self._blatt_gewechselt()) camping = ttk.Frame(self.blaetter.bereich, padding=(2, 6)) diff --git a/assets/icons/check.png b/assets/icons/check.png new file mode 100644 index 0000000..7dcb1a5 Binary files /dev/null and b/assets/icons/check.png differ diff --git a/assets/icons/close.png b/assets/icons/close.png new file mode 100644 index 0000000..ca051d4 Binary files /dev/null and b/assets/icons/close.png differ diff --git a/assets/icons/dashboard_customize.png b/assets/icons/dashboard_customize.png new file mode 100644 index 0000000..80f2a65 Binary files /dev/null and b/assets/icons/dashboard_customize.png differ diff --git a/assets/icons/drag_indicator.png b/assets/icons/drag_indicator.png new file mode 100644 index 0000000..1b060c1 Binary files /dev/null and b/assets/icons/drag_indicator.png differ diff --git a/assets/icons/save.png b/assets/icons/save.png new file mode 100644 index 0000000..bffa440 Binary files /dev/null and b/assets/icons/save.png differ diff --git a/flaeche.py b/flaeche.py new file mode 100644 index 0000000..1cc2f35 --- /dev/null +++ b/flaeche.py @@ -0,0 +1,282 @@ +# -*- coding: utf-8 -*- +"""Arbeitsflaeche: die Kaesten des Fensters lassen sich verschieben. + +Seine Ansage: *"rechnungsdaten oben links und darunter rechnung an kunde und den +tabellen kasten rechts - oder noch besser dnd einbauen, das man die kaesten +selber schieben kann"*. Beides ist hier drin: die genannte Anordnung ist die +Vorgabe, und wer sie anders haben will, zieht die Kaesten um. + +WIE ES FUNKTIONIERT +------------------- +Wie bei OPNsense: normalerweise sitzt alles fest. Ein kleiner Knopf oben rechts +schaltet das ANORDNEN ein - dann zeigen die Kaesten einen Griff, lassen sich +ziehen, und daneben stehen "Speichern" und "Verwerfen". Erst mit Speichern +steht die neue Anordnung; Verwerfen holt die alte zurueck. + +So kann beim taeglichen Arbeiten nichts verrutschen - man muss das Verschieben +erst absichtlich einschalten. + +Die Flaeche ist ein Raster aus zwei Spalten. Jeder Kasten sitzt in einer Zelle +und kann sich ueber beide Zeilen strecken. Gezogen wird am KOPF des Kastens; +der Kasten unter dem Zeiger wird hervorgehoben, beim Loslassen tauschen die +beiden ihre Plaetze. + +Getauscht wird, nicht "irgendwo abgelegt": ein freies Verschieben auf Pixel +braucht Regeln fuer Ueberlappung, Groesse und leere Zellen - und am Ende steht +das Fenster schief. Ein Tausch ist in einem Griff erledigt und kann nichts +kaputt machen. + +Die Anordnung steht in der config (`fenster_layout`) und ueberlebt den Neustart. +""" +import tkinter as tk +from tkinter import ttk + +import config + +# Vorgabe: seine Anordnung. Je Kasten (Spalte, Zeile, Zeilen hoch). +STANDARD = { + "daten": (0, 0, 1), + "kunde": (0, 1, 1), + "leistungen": (1, 0, 2), +} + + +class Arbeitsflaeche(ttk.Frame): + def __init__(self, master, farben=None, beim_umbauen=None): + super().__init__(master) + self.f = farben if farben is not None else master + self.beim_umbauen = beim_umbauen # wird nach jedem Tausch gerufen + self.kaesten = {} # name -> dict(rahmen, kopf, titel) + self.layout = {} + self._vorher = {} # Stand vor dem Anordnen + self.anordnen_an = False # Verschieben eingeschaltet? + self._zieht = None + self._ziel = None + # Die Gewichte setzt `anordnen()` - sie haengen davon ab, WO die lange + # Liste gerade steht. + + # ---- Aufbau ----------------------------------------------------------- + def kasten(self, name, titel): + """Legt einen Kasten an und gibt seinen Inhaltsrahmen zurueck.""" + aussen = ttk.Frame(self, style="Karte.TFrame") + kopf = tk.Label(aussen, text=titel.upper(), anchor="w", cursor="fleur", + bg=self.f.col_card, fg=self.f.col_accent, + font=(self._schrift(), 9, "bold"), padx=12, pady=8) + kopf.pack(side="top", fill="x") + inhalt = ttk.Frame(aussen, style="Karte.TFrame", padding=(12, 0, 12, 12)) + inhalt.pack(side="top", fill="both", expand=True) + + kopf.bind("", lambda e, n=name: self._greifen(n)) + kopf.bind("", lambda e, n=name: self._ziehen(e, n)) + kopf.bind("", lambda e, n=name: self._loslassen(n)) + self.kaesten[name] = {"aussen": aussen, "kopf": kopf, "inhalt": inhalt, + "titel": titel} + return inhalt + + def anordnen(self, layout=None): + """Kaesten ins Raster setzen. Ohne Angabe: was in der config steht.""" + self.layout = dict(layout or self._gespeichert()) + for name, kasten in self.kaesten.items(): + spalte, zeile, hoch = self.layout.get(name, STANDARD.get(name, (0, 0, 1))) + kasten["aussen"].grid(row=zeile, column=spalte, rowspan=hoch, + sticky="nsew", padx=6, pady=6) + self._platz_verteilen() + + def _platz_verteilen(self): + """Der freie Platz gehoert dem Kasten mit der langen Liste. + + Feste Gewichte gehen nicht: die Liste wandert beim Verschieben, und wer + das Gewicht an einer festen Zelle laesst, schneidet danach den + Kundenkasten unten ab - genau das ist beim ersten Versuch passiert. + """ + for z in range(2): + self.rowconfigure(z, weight=0, minsize=0) + for sp in range(2): + self.columnconfigure(sp, weight=1, uniform="spalten") + lang = self.layout.get("leistungen") + if not lang: + return + spalte, zeile, hoch = lang + for z in range(zeile, zeile + hoch): + self.rowconfigure(z, weight=1) + # Die Spalte mit der Liste bekommt mehr Breite als die andere. + self.columnconfigure(spalte, weight=4, uniform="spalten") + self.columnconfigure(1 - spalte, weight=3, uniform="spalten") + # Steht die Liste nur in EINER Zeile, braucht die andere trotzdem Luft + # fuer ihren Inhalt - sonst wird sie zusammengequetscht. + if hoch == 1: + self.rowconfigure(1 - zeile, weight=1, minsize=170) + + def zuruecksetzen(self): + self.anordnen(STANDARD) + self._speichern() + self._koepfe_zeigen() + + # ---- Ziehen ----------------------------------------------------------- + # ---- Anordnen ein- und ausschalten ------------------------------------ + def anordnen_starten(self): + self._vorher = dict(self.layout) + self.anordnen_an = True + self._koepfe_zeigen() + + def anordnen_speichern(self): + self.anordnen_an = False + self._speichern() + self._koepfe_zeigen() + + def anordnen_verwerfen(self): + self.anordnen_an = False + if self._vorher: + self.anordnen(self._vorher) + self._koepfe_zeigen() + + def _koepfe_zeigen(self): + """Im Anordnen-Modus tragen die Koepfe einen Griff und einen Zeiger.""" + import icons + for kasten in self.kaesten.values(): + kopf = kasten["kopf"] + if self.anordnen_an: + bild = icons.hole("drag_indicator", 16, self.f.col_accent, master=self) + kopf.configure(image=bild, compound="left", cursor="fleur", + bg=self.f.col_sel) + kopf.bild = bild # sonst raeumt Python das Bild weg + else: + kopf.configure(image="", compound="none", cursor="", + bg=self.f.col_card) + kopf.bild = None + + def _greifen(self, name): + if not self.anordnen_an: + return + self._zieht = name + self.kaesten[name]["kopf"].configure(bg=self.f.col_sel) + # Der gegriffene Kasten wird blass - man sieht, WAS man in der Hand hat. + self.kaesten[name]["aussen"].configure(style="Gegriffen.TFrame") + self._etikett_zeigen(name) + + def _ziehen(self, ereignis, name): + if self._zieht is None or not self.anordnen_an: + return + self._etikett_bewegen() + ziel = self._kasten_unter(ereignis) + if ziel == self._ziel: + return + self._marke(self._ziel, False) + self._ziel = ziel if ziel != name else None + self._marke(self._ziel, True) + + def _loslassen(self, name): + if self._zieht is None: + return + self._etikett_weg() + self.kaesten[name]["aussen"].configure(style="Karte.TFrame") + self.kaesten[name]["kopf"].configure(bg=self.f.col_sel) + self._marke(self._ziel, False) + ziel, self._zieht, self._ziel = self._ziel, None, None + if ziel and ziel != name: + # Getauscht wird sofort sichtbar - gespeichert erst mit dem Knopf. + self.layout[name], self.layout[ziel] = self.layout[ziel], self.layout[name] + self.anordnen(self.layout) + self._koepfe_zeigen() + if self.beim_umbauen: + self.beim_umbauen() + + # ---- Das Etikett am Zeiger ------------------------------------------- + def _etikett_zeigen(self, name): + """Ein kleines Schild haengt am Zeiger, solange gezogen wird. + + Ohne das sieht man nur, WOHIN es geht, aber nicht, dass man ueberhaupt + etwas in der Hand hat - es wirkte "statisch". Ein Widget selbst kann in + Tk nicht mitwandern; ein randloses Fensterchen kann es. + """ + import icons + self._etikett_weg() + try: + schild = tk.Toplevel(self) + schild.overrideredirect(True) + schild.attributes("-topmost", True) + try: + schild.attributes("-alpha", 0.85) + except tk.TclError: + pass + rahmen = tk.Frame(schild, bg=self.f.col_accent, padx=1, pady=1) + rahmen.pack() + bild = icons.hole("drag_indicator", 16, self.f.col_accent, master=self) + inhalt = tk.Label(rahmen, text=" " + self.kaesten[name]["titel"], + image=bild, compound="left", + bg=self.f.col_card, fg=self.f.col_accent, + font=(self._schrift(), 10, "bold"), padx=12, pady=8) + inhalt.bild = bild + inhalt.pack() + self._etikett = schild + self._etikett_bewegen() + except tk.TclError as e: + print("[flaeche] Etikett nicht moeglich:", e) + self._etikett = None + + def _etikett_bewegen(self): + schild = getattr(self, "_etikett", None) + if schild is None: + return + try: + schild.geometry(f"+{self.winfo_pointerx() + 14}+{self.winfo_pointery() + 14}") + except tk.TclError: + self._etikett = None + + def _etikett_weg(self): + schild = getattr(self, "_etikett", None) + if schild is not None: + try: + schild.destroy() + except tk.TclError: + pass + self._etikett = None + + def _marke(self, name, an): + if not name: + return + self.kaesten[name]["kopf"].configure( + fg=self.f.col_accent_hell if an else self.f.col_accent) + self.kaesten[name]["aussen"].configure( + style="Ziel.TFrame" if an else "Karte.TFrame") + + def _kasten_unter(self, ereignis): + """Welcher Kasten liegt unter dem Zeiger?""" + x = self.winfo_pointerx() - self.winfo_rootx() + y = self.winfo_pointery() - self.winfo_rooty() + for name, kasten in self.kaesten.items(): + w = kasten["aussen"] + lx, ly = w.winfo_x(), w.winfo_y() + if lx <= x <= lx + w.winfo_width() and ly <= y <= ly + w.winfo_height(): + return name + return None + + # ---- Merken ----------------------------------------------------------- + def _schrift(self): + import theme + return theme.SCHRIFT_NAME + + def _gespeichert(self): + cfg = getattr(self.f, "cfg", {}) or {} + roh = cfg.get("fenster_layout") or {} + aus = {} + for name in self.kaesten: + wert = roh.get(name) + # Nur uebernehmen, was auch wirklich passt - eine von Hand + # verstellte config darf das Fenster nicht zerlegen. + if (isinstance(wert, (list, tuple)) and len(wert) == 3 + and all(isinstance(z, int) for z in wert)): + aus[name] = tuple(wert) + else: + aus[name] = STANDARD.get(name, (0, 0, 1)) + return aus + + def _speichern(self): + cfg = getattr(self.f, "cfg", None) + if cfg is None: + return + cfg["fenster_layout"] = {n: list(v) for n, v in self.layout.items()} + try: + config.save_config(cfg) + except OSError as e: + print("[flaeche] Anordnung nicht gespeichert:", e) diff --git a/icons.py b/icons.py new file mode 100644 index 0000000..3cd0c7b --- /dev/null +++ b/icons.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +"""Icons aus dem Hub-Vorrat (Material Symbols), eingefaerbt zur Laufzeit. + +Die SVGs liegen auf dem Hub (`M:/projekte/material-symbols`) und wurden einmal +nach PNG gewandelt - als weisse Silhouette mit Transparenz. Hier bekommen sie +ihre Farbe: dieselbe Datei taugt fuer die Akzentfarbe, fuer gedaempfte Schrift +und fuer das helle Thema, ohne dass jemand drei Fassungen pflegt. + +Ohne Pillow oder ohne Datei gibt es einfach kein Bild zurueck - der Aufrufer +zeigt dann seinen Text. Ein fehlendes Icon darf kein Fenster verhindern. +""" +import os +import tkinter as tk + +import config + +# ⚠️ Ein PhotoImage gehoert zu GENAU EINEM Tk-Fenster. Wird ein zweites +# aufgemacht (ein Pruefstand faehrt Journal und Rechnungstool nacheinander), +# zeigt ein weitergereichtes Bild ins Leere: 'image "pyimage1" doesn't exist'. +# Deshalb steht das Fenster mit im Schluessel. +_zwischenlager = {} # (fenster, name, groesse, farbe) -> PhotoImage + + +def _pfad(name): + for basis in (config.BASE_DIR, config.RESOURCE_DIR, + os.path.dirname(os.path.abspath(__file__))): + p = os.path.join(basis, "assets", "icons", name + ".png") + if os.path.exists(p): + return p + return "" + + +def hole(name, groesse=18, farbe="#e8e8ea", master=None): + """Icon als PhotoImage - oder None, wenn es nicht geht. + + `master` ist das Widget, in dem das Bild landet. Es MUSS mitgegeben werden, + wenn mehr als ein Tk-Fenster laeuft: `tk._default_root` zeigt dann auf das + zuerst geoeffnete, und das Bild gehoert dem falschen Fenster. + """ + fenster = master if master is not None else tk._default_root + if fenster is None: + return None + schluessel = (id(fenster), name, groesse, farbe) + if schluessel in _zwischenlager: + return _zwischenlager[schluessel] + pfad = _pfad(name) + if not pfad: + print(f"[icons] nicht gefunden: {name}") + return None + try: + from PIL import Image, ImageTk + bild = Image.open(pfad).convert("RGBA") + bild = bild.resize((groesse, groesse), Image.LANCZOS) + # Die Silhouette steckt im Alphakanal - die Farbe kommt hier drueber. + flaeche = Image.new("RGBA", bild.size, farbe) + flaeche.putalpha(bild.getchannel("A")) + fertig = ImageTk.PhotoImage(flaeche, master=fenster) + except Exception as e: # noqa: BLE001 - ein Icon darf nichts blockieren + print(f"[icons] {name} nicht ladbar: {e}") + return None + _zwischenlager[schluessel] = fertig + return fertig diff --git a/pruef_flaeche.py b/pruef_flaeche.py new file mode 100644 index 0000000..1333794 --- /dev/null +++ b/pruef_flaeche.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +"""Pruefstand: die verschiebbaren Kaesten (Arbeitsflaeche). + +Seine Ansage: *"wie bei opnsense hast so einen kleinen knopf, dann drueckst +drauf, ab da kannst du schieben, dann hast du einen save button - und dann ist +die fest"*. + +Geprueft wird genau diese Reihenfolge: + +1. Solange nicht "Anordnen" gedrueckt ist, laesst sich NICHTS verschieben - + beim taeglichen Arbeiten darf nichts verrutschen. +2. Im Anordnen-Modus haengt ein Etikett am Zeiger (man soll sehen, dass man + etwas in der Hand hat) und zwei Kaesten tauschen ihre Plaetze. +3. "Verwerfen" holt die Anordnung von vorher zurueck. +4. "Speichern" schreibt sie in die config - erst dann steht sie fest. +5. Der freie Platz gehoert immer dem Kasten mit der langen Liste, egal wo er + gerade sitzt. Sonst wird der Kundenkasten unten abgeschnitten. +""" +import json +import os +import shutil +import sys +import tempfile +import threading + +QUELLE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, QUELLE) + +os.environ["CAMPINGHOF_TEST"] = "1" +tmp = tempfile.mkdtemp(prefix="flaeche_pruef_") +ordner = os.path.join(tmp, "rechnungen") +os.makedirs(ordner) + +import config # noqa: E402 +shutil.copy(os.path.join(QUELLE, "config.json"), os.path.join(tmp, "config.json")) +config.BASE_DIR = tmp +config.CONFIG_PATH = os.path.join(tmp, "config.json") +roh = json.load(open(config.CONFIG_PATH, encoding="utf-8")) +roh["output_dir"] = ordner +roh["nummer_eingerichtet"] = True +roh.pop("fenster_layout", None) # ohne gespeicherte Anordnung starten +json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False) + +import app as A # noqa: E402 +import flaeche as FL # noqa: E402 +from tkinter import messagebox # noqa: E402 + + +def _reissleine(): + print("ABBRUCH | Pruefstand haengt (Zeitgrenze)") + os._exit(2) + + +_wecker = threading.Timer(120, _reissleine) +_wecker.daemon = True +_wecker.start() + +fehler = [] + + +def pruefe(bedingung, text): + zeile = (" OK | " if bedingung else "FEHLER | ") + text + k = sys.stdout.encoding or "utf-8" + print(zeile.encode(k, "replace").decode(k)) + if not bedingung: + fehler.append(text) + + +messagebox.showinfo = messagebox.showwarning = messagebox.showerror = lambda *a, **k: None +messagebox.askyesno = lambda *a, **k: True + +f = A.RechnungsApp() +f.update() +fl = f.flaeche + +# ---- 1. Die Vorgabe ist seine Anordnung ---------------------------------- +pruefe(fl.layout == FL.STANDARD, + f"Vorgabe: Rechnungsdaten oben links, Kunde darunter, Leistungen rechts ({fl.layout})") + +# ---- 2. Ohne den Knopf laesst sich nichts schieben ----------------------- +pruefe(not fl.anordnen_an, "beim Start ist das Verschieben AUS") +fl._greifen("kunde") +pruefe(fl._zieht is None, "ohne 'Anordnen' greift ein Zug ins Leere") +vorgabe = dict(fl.layout) + +# ---- 3. Anordnen an: Etikett und Tausch ---------------------------------- +f._anordnen_umschalten() +pruefe(fl.anordnen_an, "nach dem Knopf ist das Verschieben AN") +fl._greifen("kunde") +pruefe(fl._zieht == "kunde", "jetzt haelt man den Kasten") +pruefe(getattr(fl, "_etikett", None) is not None, + "und am Zeiger haengt ein Etikett - man SIEHT, dass man zieht") +fl._ziel = "daten" +fl._loslassen("kunde") +pruefe(getattr(fl, "_etikett", None) is None, "nach dem Loslassen ist das Etikett weg") +pruefe(fl.layout["kunde"] == vorgabe["daten"] and fl.layout["daten"] == vorgabe["kunde"], + f"die beiden haben die Plaetze getauscht ({fl.layout})") + +# Gespeichert ist noch NICHTS - erst der Knopf schreibt. +in_datei = json.load(open(config.CONFIG_PATH, encoding="utf-8")).get("fenster_layout") +pruefe(in_datei is None, f"vor dem Speichern steht nichts in der config ({in_datei})") + +# ---- 4. Verwerfen holt die alte Anordnung zurueck ------------------------ +f._anordnen_verwerfen() +pruefe(fl.layout == vorgabe, f"Verwerfen stellt die Anordnung wieder her ({fl.layout})") +pruefe(not fl.anordnen_an, "und schaltet das Verschieben wieder aus") + +# ---- 5. Speichern macht es fest ------------------------------------------ +f._anordnen_umschalten() +fl._greifen("daten") +fl._ziel = "leistungen" +fl._loslassen("daten") +neu = dict(fl.layout) +f._anordnen_speichern() +in_datei = json.load(open(config.CONFIG_PATH, encoding="utf-8")).get("fenster_layout") +pruefe(in_datei == {n: list(v) for n, v in neu.items()}, + f"Speichern schreibt die Anordnung in die config ({in_datei})") +pruefe(not fl.anordnen_an, "und schaltet das Verschieben aus") + +# ---- 6. Der Platz folgt der langen Liste --------------------------------- +spalte, zeile, hoch = fl.layout["leistungen"] +gewicht_liste = fl.grid_rowconfigure(zeile)["weight"] +pruefe(gewicht_liste > 0, + f"die Zeile mit den Leistungen darf wachsen (Gewicht {gewicht_liste})") +breite_liste = fl.grid_columnconfigure(spalte)["weight"] +breite_andere = fl.grid_columnconfigure(1 - spalte)["weight"] +pruefe(breite_liste > breite_andere, + f"und ihre Spalte ist breiter ({breite_liste} zu {breite_andere})") + +# Eine kaputte Anordnung in der config darf das Fenster nicht zerlegen. +f.cfg["fenster_layout"] = {"kunde": "kaputt", "daten": [0], "leistungen": None} +fl.anordnen() +pruefe(fl.layout == FL.STANDARD, + f"eine unbrauchbare Anordnung faellt auf die Vorgabe zurueck ({fl.layout})") + +f.destroy() +print() +if fehler: + print(f"FEHLGESCHLAGEN: {len(fehler)}") + for x in fehler: + print(" -", x) + sys.exit(1) +shutil.rmtree(tmp, ignore_errors=True) +print("ALLES GRUEN") diff --git a/theme.py b/theme.py index 4dfca80..99d35d8 100644 --- a/theme.py +++ b/theme.py @@ -167,6 +167,14 @@ class ThemeMixin: # Ein Abschnitt ist eine Kachel: eigene Flaeche, feiner Rand, die # Ueberschrift in Akzentfarbe - genau wie die Kacheln in den # Web-Oberflaechen. + # Der Kasten, den man gerade in der Hand hat: blass, gestrichelt. + style.configure("Gegriffen.TFrame", background=self.col_bg, + bordercolor=self.col_muted, relief="solid", borderwidth=1, + lightcolor=self.col_muted, darkcolor=self.col_muted) + # Der Kasten, auf den beim Anordnen gerade gezielt wird. + style.configure("Ziel.TFrame", background=self.col_card, + bordercolor=accent, relief="solid", borderwidth=2, + lightcolor=accent, darkcolor=accent) style.configure("Karte.TLabelframe", background=self.col_card, bordercolor=self.col_line, relief="solid", borderwidth=1, lightcolor=self.col_card, darkcolor=self.col_card)