diff --git a/app.py b/app.py index e2c9186..c26bd2b 100644 --- a/app.py +++ b/app.py @@ -574,7 +574,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, ttk.Separator(rahmen, orient="horizontal").pack(fill="x", pady=(12, 10)) ttk.Button(rahmen, text="Schließen", width=14, command=d.destroy).pack(anchor="e") - d.geometry(f"+{self.winfo_rootx() + 120}+{self.winfo_rooty() + 140}") + theme.mittig(d, self) return d def _umbruch_nachziehen(self, runde=0, vorige_breite=None): @@ -1078,7 +1078,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, d.bind("", lambda _e: d.destroy()) d.update_idletasks() x = self.winfo_rootx() + max(0, (self.winfo_width() - d.winfo_reqwidth()) // 2) - d.geometry(f"+{max(0, x)}+{self.winfo_rooty() + 110}") + theme.mittig(d, self) d.grab_set() self.wait_window(d) if fertig["ok"]: @@ -1428,7 +1428,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, d.bind("", lambda _e: d.destroy()) d.update_idletasks() x = self.winfo_rootx() + max(0, (self.winfo_width() - d.winfo_reqwidth()) // 2) - d.geometry(f"+{max(0, x)}+{self.winfo_rooty() + 130}") + theme.mittig(d, self) d.grab_set() self.wait_window(d) return wahl["art"] diff --git a/korrektur.py b/korrektur.py index 45b71e1..cbbdb82 100644 --- a/korrektur.py +++ b/korrektur.py @@ -513,7 +513,7 @@ class KorrekturMixin: d.bind("", lambda _e: d.destroy()) d.update_idletasks() x = self.winfo_rootx() + max(0, (self.winfo_width() - d.winfo_reqwidth()) // 2) - d.geometry(f"+{max(0, x)}+{self.winfo_rooty() + 110}") + theme.mittig(d, self) d.grab_set() self.wait_window(d) if wahl["art"] == "vorhanden": diff --git a/ordnerwahl.py b/ordnerwahl.py index 6cc89b3..367c470 100644 --- a/ordnerwahl.py +++ b/ordnerwahl.py @@ -106,7 +106,8 @@ class OrdnerWahl(tk.Toplevel): hoehe = max(self.winfo_reqheight(), 300) x = master.winfo_rootx() + max(0, (master.winfo_width() - BREITE) // 2) y = master.winfo_rooty() + 90 - self.geometry(f"{BREITE}x{hoehe}+{max(0, x)}+{max(0, y)}") + self.geometry(f"{BREITE}x{hoehe}") + theme.mittig(self, master) self.minsize(BREITE, hoehe) self.grab_set() self.lift() diff --git a/pruef_darstellung.py b/pruef_darstellung.py index be96ebd..3c076b6 100644 --- a/pruef_darstellung.py +++ b/pruef_darstellung.py @@ -167,6 +167,28 @@ for wunsch, soll_dunkel in (("hell", False), ("dunkel", True)): f"in '{wunsch}' ist keine Farbe der anderen Darstellung " f"uebrig ({reste[:3]})") +# ---- Dialoge gehen mittig ueber dem Programmfenster auf ------------------ +# Seine Ansage: "einfach so programmieren das die popup auf dem fenster mittig +# vom main fenster auf gehen". Das ist zugleich die Antwort auf die +# Monitorfrage - steht das Programm auf dem zweiten Bildschirm, geht der +# Dialog dort auf. Vorher klemmte ein max(0, x) jede negative Koordinate auf +# null und riss die Fenster auf den Hauptmonitor zurueck. +d = f.dlg_darstellung() +pumpen(1.2) +d.update_idletasks() +mitte_dialog = d.winfo_rootx() + d.winfo_width() // 2 +mitte_fenster = f.winfo_rootx() + f.winfo_width() // 2 +pruefe(abs(mitte_dialog - mitte_fenster) <= 30, + f"der Dialog steht mittig ueber dem Fenster " + f"({mitte_dialog} vs. {mitte_fenster})") +pruefe(d.winfo_rootx() >= f.winfo_rootx() - 40 + and d.winfo_rootx() + d.winfo_width() <= f.winfo_rootx() + f.winfo_width() + 40, + "und liegt waagerecht innerhalb des Fensters") +pruefe(d.winfo_rooty() >= f.winfo_rooty() - 40, + "und nicht oberhalb davon") +d.destroy() +pumpen(0.3) + # ---- Zurueck auf automatisch -------------------------------------------- f.cfg["darstellung"] = "auto" config.save_config(f.cfg) diff --git a/pruef_release.py b/pruef_release.py index 0616fa8..6722570 100644 --- a/pruef_release.py +++ b/pruef_release.py @@ -64,7 +64,15 @@ if dieses is None: print("ROT | ohne Release ist der Rest nicht pruefbar") sys.exit(1) -anhaenge = {a.get("name"): a for a in dieses.get("assets") or []} +roh = dieses.get("assets") or [] +anhaenge = {a.get("name"): a for a in roh} +# Doppelte Namen: Forgejo laesst zwei Anhaenge mit demselben Namen zu. Beim +# Ersetzen einer Datei blieb die alte einmal stehen (07.09.2026), und welche +# von beiden der Updater dann laedt, ist Glueckssache - unter Umstaenden die +# alte, mit der falschen Pruefsumme daneben. +doppelt = sorted({a["name"] for a in roh + if sum(1 for b in roh if b["name"] == a["name"]) > 1}) +pruefe(not doppelt, f"kein Anhang haengt doppelt am Release ({doppelt})") print(f" (Anhaenge: {', '.join(sorted(anhaenge)) or 'keine'})") pruefe(version.EXE_NAME in anhaenge, diff --git a/pruef_start_exe.py b/pruef_start_exe.py index 04f02fb..adccc6b 100644 --- a/pruef_start_exe.py +++ b/pruef_start_exe.py @@ -57,7 +57,12 @@ if not os.path.exists(EXE): sys.exit(0) u = ctypes.windll.user32 -p = subprocess.Popen([EXE]) +# Mit --test: dann geht das Fenster auf dem ZWEITEN Monitor auf und nicht +# mitten auf dem Hauptbildschirm, wo gerade jemand arbeitet. Seine Ansage am +# 07.09.2026 - und dieser Pruefstand war der eine, der sie noch nicht befolgte, +# weil er die gebaute EXE ganz normal startet. Sichtbar bleibt das Fenster, +# und genau das wird hier ja geprueft. +p = subprocess.Popen([EXE, "--test"]) print(f" (gestartet: {os.path.basename(EXE)}, PID {p.pid})") RUECKRUF = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM) diff --git a/theme.py b/theme.py index 25502ef..309f261 100644 --- a/theme.py +++ b/theme.py @@ -121,6 +121,63 @@ def pruefplatz(): return "" +def auf_bildschirm(x, y, breite=0, hoehe=0): + """Eine Fensterposition auf den sichtbaren Bereich begrenzen. + + Frueher stand an diesen Stellen `max(0, x)`. Das sollte verhindern, dass + ein Dialog ausserhalb des Bildschirms aufgeht - klemmt aber jede NEGATIVE + Koordinate auf null. Steht der zweite Monitor links (bei ihm x = -1920), + sprang damit jedes Popup zurueck auf den Hauptbildschirm, obwohl das + Programm nebenan lief. Sein Befund am 07.09.2026: *"da gehen noch die + popups auf dem video monitor auf"*. + + Begrenzt wird deshalb auf den GESAMTEN Desktop ueber alle Monitore + (SM_*VIRTUALSCREEN), nicht auf den Hauptmonitor. + """ + if not sys.platform.startswith("win"): + return int(x), int(y) + try: + import ctypes + hole = ctypes.windll.user32.GetSystemMetrics + links, oben = hole(76), hole(77) + rechts, unten = links + hole(78), oben + hole(79) + if breite: + x = min(x, rechts - breite) + if hoehe: + y = min(y, unten - hoehe) + return int(max(links, x)), int(max(oben, y)) + except Exception: # noqa: BLE001 - dann eben ungebremst + return int(x), int(y) + + +def mittig(dialog, ueber): + """Einen Dialog mittig ueber sein Elternfenster setzen. + + Seine Ansage am 07.09.2026: *"einfach so programmieren das die popup auf + dem fenster mittig vom main fenster auf gehen"*. + + Das ist zugleich die Antwort auf die Monitorfrage: steht das Programm auf + dem zweiten Bildschirm, geht der Dialog dort auf - ohne dass irgendwo ein + Monitor gesucht werden muesste. Vorher standen an den Dialogen feste + Versaetze ("+120+140") und ein `max(0, x)`, das jede negative Koordinate + auf null klemmte und die Fenster damit auf den Hauptmonitor zurueckriss. + + Etwas oberhalb der Mitte (ein Drittel statt der Haelfte): so sitzt der + Dialog dort, wo der Blick ohnehin ist, und verdeckt nicht die Mitte des + Formulars darunter. + """ + try: + dialog.update_idletasks() + breite = dialog.winfo_reqwidth() or dialog.winfo_width() + hoehe = dialog.winfo_reqheight() or dialog.winfo_height() + x = ueber.winfo_rootx() + (ueber.winfo_width() - breite) // 2 + y = ueber.winfo_rooty() + (ueber.winfo_height() - hoehe) // 3 + x, y = auf_bildschirm(x, y, breite, hoehe) + dialog.geometry(f"+{x}+{y}") + except tk.TclError: + pass # Fenster schon zu - dann ist die Position gleichgueltig + + def testlauf(): """Faehrt gerade ein Pruefstand das Programm? @@ -532,11 +589,12 @@ class ThemeMixin: und Tastatur fest, waehrend nebenan jemand schreibt. """ d.configure(bg=self.col_bg) + # Mittig ueber dem Programmfenster - und damit automatisch auf dem + # Bildschirm, auf dem das Programm gerade steht. + d.after(1, lambda: mittig(d, self)) if testlauf(): - # Wie das Hauptfenster: auf den zweiten Monitor, und ohne grab_set - - # ein Dialog, der Maus und Tastatur festhaelt, blockiert den - # Menschen, der nebenher arbeitet. - d.geometry(pruefplatz() or f"+{d.winfo_screenwidth() + 20}+40") + # Ohne grab_set: ein Dialog, der Maus und Tastatur festhaelt, + # blockiert den Menschen, der nebenher arbeitet. d.grab_set = lambda *a, **k: None try: d.attributes("-toolwindow", True) # nicht in die Taskleiste @@ -639,10 +697,7 @@ class MeldungMixin: d.bind("", lambda _e: schliessen(False)) d.protocol("WM_DELETE_WINDOW", lambda: schliessen(False)) - d.update_idletasks() - x = self.winfo_rootx() + max(0, (self.winfo_width() - d.winfo_reqwidth()) // 2) - y = self.winfo_rooty() + 140 - d.geometry(f"+{max(0, x)}+{max(0, y)}") + mittig(d, self) # Nach vorn holen und den Tastaturfokus nehmen. Ohne das kann der Dialog # beim Start hinter dem Hauptfenster liegen - der Nutzer sieht nur ein # Fenster, das nicht reagiert, und haelt es fuer haengen geblieben.