rechnungstool/ordnerwahl.py
TheMockTv 688dfb53c4 Popups gehen mittig ueber dem Programmfenster auf
Seine Ansage
  "da gehen noch die popups auf dem video monitor auf"
  "einfach so programmieren das die popup auf dem fenster mittig vom main
   fenster auf gehen"

Was falsch war
  An sechs Stellen stand `max(0, x)`. Das sollte verhindern, dass ein Dialog
  ausserhalb des Bildschirms aufgeht - klemmt aber jede NEGATIVE Koordinate
  auf null. Bei ihm steht der zweite Monitor links (x = -1920): jedes Popup
  sprang damit zurueck auf den Hauptbildschirm, obwohl das Programm nebenan
  lief. Dazu kamen feste Versaetze wie "+120+140", die mit der Fenstergroesse
  nichts zu tun hatten.

theme.mittig(dialog, ueber)
  Eine Stelle fuer alle Dialoge: mittig ueber dem Elternfenster, etwas
  oberhalb der Mitte (ein Drittel statt der Haelfte), damit der Blick dort
  ist und die Mitte des Formulars frei bleibt.

  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 muss. Die Sonderbehandlung im Testlauf konnte
  deshalb entfallen; geblieben ist dort nur der Verzicht auf grab_set.

theme.auf_bildschirm(x, y, breite, hoehe)
  Begrenzt eine Position auf den GESAMTEN Desktop ueber alle Monitore
  (SM_*VIRTUALSCREEN) statt auf den Hauptmonitor. Das war die eigentliche
  Absicht hinter dem alten max(0, ...).

Gemessen statt geglaubt: Dialogmitte und Fenstermitte liegen 8 Pixel
auseinander (Fensterrahmen), der Dialog liegt innerhalb des Fensters. Steht
als Pruefung in pruef_darstellung.py.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
2026-09-07 00:52:58 +02:00

211 lines
8.8 KiB
Python
Raw 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.

# -*- coding: utf-8 -*-
"""Den gemeinsamen Ordner abfragen - in beiden Programmen gleich.
Rechnungstool und Steuerjournal fuehren ein gemeinsames Nummernbuch
(siehe gemeinsam.py). Damit sie sich finden, muss BEIDEN derselbe Ordner
genannt werden - deshalb fragt jedes Programm beim ersten Start danach und
sagt ausdruecklich dazu, dass es derselbe sein muss.
Der Weg ist bewusst umstaendlich: auswaehlen, dann OK, dann noch einmal
bestaetigen. Ein falscher Ordner faellt sonst erst auf, wenn eine
Rechnungsnummer zum zweiten Mal vergeben wurde.
Wer jetzt keinen Ordner hat, kommt mit "Spaeter einrichten" weiter - das
Programm laeuft dann ohne das gemeinsame Buch und sagt das in der Fussleiste.
⚠ Das Fenster bekommt seine Groesse ausdruecklich gesetzt. Ohne das war es zu
schmal, der Knopf zum Auswaehlen lag ausserhalb - und der Dialog war nicht zu
bedienen (gefunden am 03.09.2026, bevor es jemand benutzen musste).
Diese Datei liegt in BEIDEN Programmen gleich. Wer sie aendert, kopiert sie
hinueber.
"""
import os
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import theme
import gemeinsam
BREITE = 780
class OrdnerWahl(tk.Toplevel):
"""Modaler Dialog. Nach dem Schliessen steht das Ergebnis in `.ergebnis`
(gewaehlter Ordner) oder es ist None (spaeter/abgebrochen)."""
def __init__(self, master, programm: str, vorgabe: str = ""):
super().__init__(master)
self.ergebnis = None
self.title("Gemeinsamer Ordner für das Nummernbuch")
self.transient(master)
# Dieses Fenster stand als einziges noch in Weiss da: es kam nie durch
# _style_dialog. Farben holt es sich jetzt vom Hauptfenster, damit es
# nicht wieder auseinanderlaeuft, wenn dort etwas geaendert wird.
self._bg = getattr(master, "col_bg", "#0d0e10")
self._fg = getattr(master, "col_fg", "#e8e8ea")
self._warn = getattr(master, "col_gefahr", "#e85a5a")
self._gut = getattr(master, "col_accent", "#06C6A4")
self._muted = getattr(master, "col_muted", "#8a8e96")
if hasattr(master, "_style_dialog"):
master._style_dialog(self)
else:
self.configure(bg=self._bg)
self.pfad = tk.StringVar(value=vorgabe or "")
rahmen = ttk.Frame(self, padding=18)
rahmen.pack(fill="both", expand=True)
rahmen.columnconfigure(0, weight=1)
breite = BREITE - 60
ttk.Label(rahmen, text="Bitte den gemeinsamen Ordner auswählen",
font=(theme.SCHRIFT_NAME, 13, "bold")).grid(row=0, column=0, sticky="w")
ttk.Label(rahmen, wraplength=breite, justify="left", text=(
"Rechnungstool und Steuerjournal führen zusammen ein Nummernbuch. Darin steht "
"jede vergebene Rechnungsnummer – auch die aus der alten Excel-Tabelle, zu der "
"es gar keine PDF gibt.\n\n"
"Ohne dieses Buch kennt jedes Programm nur seine eigenen Nummern und vergibt "
"eine Nummer ein zweites Mal. Jede Nummer darf es aber nur einmal geben "
"(§ 14 Abs. 4 Nr. 4 UStG).")).grid(row=1, column=0, sticky="w", pady=(8, 12))
tk.Label(rahmen, wraplength=breite, justify="left", fg=self._warn, bg=self._bg,
font=(theme.SCHRIFT_NAME, 10, "bold"),
text="WICHTIG: In BEIDEN Programmen muss derselbe Ordner stehen – am besten "
"der Ordner, in dem die Programme liegen.").grid(
row=2, column=0, sticky="w", pady=(0, 14))
wahl = ttk.Frame(rahmen)
wahl.grid(row=3, column=0, sticky="ew")
wahl.columnconfigure(0, weight=1)
self.feld = ttk.Entry(wahl, textvariable=self.pfad, state="readonly")
self.feld.grid(row=0, column=0, sticky="ew")
ttk.Button(wahl, text="Install-Ordner auswählen…", command=self.waehlen,
width=26).grid(row=0, column=1, sticky="w", padx=(10, 0))
self.stand = tk.Label(rahmen, text="", wraplength=breite, justify="left",
bg=self._bg,
font=(theme.SCHRIFT_NAME, 9))
self.stand.grid(row=4, column=0, sticky="w", pady=(10, 0))
knoepfe = ttk.Frame(rahmen)
knoepfe.grid(row=5, column=0, sticky="ew", pady=(18, 0))
self.ok = ttk.Button(knoepfe, text="OK", command=self.uebernehmen, width=14)
self.ok.pack(side="right")
ttk.Button(knoepfe, text="Später einrichten", command=self.spaeter,
width=18).pack(side="left")
self._stand_zeigen()
self.protocol("WM_DELETE_WINDOW", self.spaeter)
self.bind("<Escape>", lambda _e: self.spaeter())
# Groesse ausdruecklich setzen: sonst schneidet Tk das Fenster an der
# zweiten Spalte ab und der Auswahl-Knopf ist nicht erreichbar.
self.update_idletasks()
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}")
theme.mittig(self, master)
self.minsize(BREITE, hoehe)
self.grab_set()
self.lift()
self.focus_force()
# ------------------------------------------------------------------ Weg
def waehlen(self):
# Der Griff (grab_set) muss waehrend des Windows-Ordnerdialogs los sein:
# sonst kann der native Dialog hinter dem gesperrten Fenster landen und
# nimmt keine Eingabe an - man klickt und es passiert nichts.
try:
self.grab_release()
except tk.TclError:
pass
try:
ordner = filedialog.askdirectory(
title="Gemeinsamen Ordner wählen (in beiden Programmen derselbe!)",
initialdir=self.pfad.get() or os.path.expanduser("~"), parent=self)
finally:
try:
self.grab_set()
except tk.TclError:
pass
if ordner:
self.pfad.set(os.path.abspath(ordner))
self._stand_zeigen()
def _stand_zeigen(self):
"""Sagt, was in dem gewaehlten Ordner schon liegt.
Ein bereits vorhandenes Nummernbuch ist das beste Zeichen, dass es der
richtige Ordner ist - dann hat das andere Programm ihn schon.
"""
ordner = self.pfad.get().strip()
if not ordner:
self.stand.configure(text="Noch kein Ordner gewählt.", fg=self._warn)
self.ok.state(["disabled"])
return
taugt, grund = gemeinsam.ordner_taugt(ordner)
if not taugt:
self.stand.configure(text=grund, fg=self._warn)
self.ok.state(["disabled"])
return
self.ok.state(["!disabled"])
if os.path.exists(gemeinsam.pfad_zur_datei(ordner)):
try:
anzahl = len(gemeinsam.Nummernbuch(ordner).nummern())
except Exception: # noqa: BLE001
anzahl = "?"
self.stand.configure(
text=f"Hier liegt schon ein Nummernbuch mit {anzahl} Rechnungsnummern – "
"das ist der Ordner des anderen Programms. Genau richtig.",
fg=self._gut)
else:
self.stand.configure(
text="Hier ist noch kein Nummernbuch – es wird neu angelegt. Wähle im "
"anderen Programm denselben Ordner.", fg=self._muted)
def _melden(self, titel, text):
"""Meldung im Theme des Hauptfensters - sonst wie bisher."""
eltern = self.master
if hasattr(eltern, "melden_warnung"):
return eltern.melden_warnung(titel, text)
return messagebox.showwarning(titel, text, parent=self)
def _frage(self, titel, text):
eltern = self.master
if hasattr(eltern, "frage"):
return eltern.frage(titel, text)
return messagebox.askyesno(titel, text, parent=self)
def uebernehmen(self):
ordner = self.pfad.get().strip()
taugt, grund = gemeinsam.ordner_taugt(ordner)
if not taugt:
self._melden("Ordner geht nicht", grund or "Bitte einen Ordner wählen.")
return
if not self._frage(
"Sind Sie sicher?",
f"Gemeinsamer Ordner:\n\n{ordner}\n\n"
"Steht im anderen Programm derselbe Ordner? Nur dann kennen beide "
"dieselben Rechnungsnummern.\n\n"
"Ja = Ordner übernehmen\n"
"Nein = zurück zur Auswahl"):
# Zurueck ins Auswahlfeld - der Dialog bleibt stehen, nichts ist gesetzt.
self.feld.focus_set()
return
self.ergebnis = ordner
self.destroy()
def spaeter(self):
self.ergebnis = None
self.destroy()
def ordner_abfragen(fenster, programm, vorgabe=""):
"""Dialog zeigen und den gewaehlten Ordner zurueckgeben (oder None)."""
d = OrdnerWahl(fenster, programm, vorgabe)
fenster.wait_window(d)
return d.ergebnis