# -*- 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)