Seine Ansage: "rechnungsdaten oben links, darunter rechnung an kunde, den tabellen kasten rechts - oder noch besser dnd einbauen" und dann genauer: "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". - Seine Anordnung ist die Vorgabe (flaeche.STANDARD). - Verschieben muss man EINSCHALTEN. Ohne den Knopf sitzt alles fest, damit beim taeglichen Arbeiten nichts verrutscht. Im Modus stehen "Speichern" und "Verwerfen" daneben; erst Speichern schreibt in die config. - Waehrend des Ziehens haengt ein Etikett am Zeiger und der gegriffene Kasten wird blass - vorher sah man nur das Ziel und es wirkte "statisch" (sein Fund). - Getauscht statt frei abgelegt: 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. - Der freie Platz folgt dem Kasten mit der langen Liste. Feste Gewichte gingen nicht: die Liste wandert, und der Kundenkasten wurde unten abgeschnitten. - Eine unbrauchbare Anordnung in der config faellt auf die Vorgabe zurueck. Icons: die Material Symbols vom Hub (M:/projekte/material-symbols), einmal nach PNG gewandelt und zur Laufzeit eingefaerbt (icons.py) - eine Datei je Icon, nicht drei Fassungen fuer hell/dunkel/Akzent. ⚠️ Ein PhotoImage gehoert GENAU EINEM Tk-Fenster. Der Icon-Cache reichte Bilder an das naechste weiter, sobald ein Pruefstand Journal und Rechnungstool nacheinander faehrt: 'image "pyimage1" doesn't exist'. Das Fenster wird jetzt mitgegeben, tk._default_root reicht dafuer nicht. Neu: pruef_flaeche.py. Alle 15 Pruefstaende gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
282 lines
11 KiB
Python
282 lines
11 KiB
Python
# -*- 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("<Button-1>", lambda e, n=name: self._greifen(n))
|
|
kopf.bind("<B1-Motion>", lambda e, n=name: self._ziehen(e, n))
|
|
kopf.bind("<ButtonRelease-1>", 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)
|