"""Menüs im Windows-95-Stil (Menüleiste, Aufklapp- und Kontextmenüs). Die nativen Windows-Menüs sind weiß und wechseln nicht mit, wenn man bei offenem Menü mit der Maus zum nächsten Menüleisten-Eintrag fährt. Diese Menüs sind selbst gezeichnet: - grau, Markierung in Navy, Tastenkürzel rechtsbündig, Trennlinien eingelassen - bei offenem Menü wechselt das Menü beim Überfahren der Menüleiste - Tastatur: Pfeil hoch/runter wählen, Enter ausführen, Esc schließen, Pfeil links/rechts benachbartes Menü - Klick daneben oder Wechsel in ein anderes Programm schließt das Menü Die Schnittstelle (add_command, add_separator, tk_popup) entspricht tk.Menu. """ from __future__ import annotations import tkinter as tk from provtool.gui import win95 as w class Popup: """Ein Aufklapp- bzw. Kontextmenü.""" def __init__(self, master: tk.Misc): self.master = master self.eintraege: list[dict] = [] self.fenster: tk.Toplevel | None = None self.zeilen: list[tuple[dict, tk.Frame, list[tk.Label]]] = [] self.aktiv = -1 self.leiste: Menueleiste | None = None self._pruefer = None # ------------------------------------------------------------- tk.Menu-kompatibel def add_command(self, label: str, command=None, accelerator: str = "", state: str = "normal"): self.eintraege.append(dict(art="befehl", label=label, command=command, accelerator=accelerator, state=state)) def add_separator(self): self.eintraege.append(dict(art="trenner")) def tk_popup(self, x: int, y: int): self.oeffnen(x, y) # ------------------------------------------------------------- Anzeige @property def offen(self) -> bool: try: return self.fenster is not None and bool(self.fenster.winfo_exists()) except tk.TclError: return False def oeffnen(self, x: int, y: int, leiste: Menueleiste | None = None): self.schliessen() self.leiste = leiste root = self.master._root() f = self.fenster = tk.Toplevel(root, bg=w.FACE, bd=2, relief="raised") f.withdraw() f.overrideredirect(True) self.zeilen = [] for e in self.eintraege: if e["art"] == "trenner": t = tk.Frame(f, bg=w.FACE, height=8) t.pack(fill="x") linie = w.trenner(t, vertikal=False) linie.place(relx=0, rely=0.5, relwidth=1, y=-1) continue zeile = tk.Frame(f, bg=w.FACE) zeile.pack(fill="x") grau = e["state"] == "disabled" text = tk.Label(zeile, text=e["label"], bg=w.FACE, anchor="w", padx=18, pady=2, fg=w.SHADOW if grau else w.DARK) text.pack(side="left", fill="x", expand=True) labels = [text] if e["accelerator"]: acc = tk.Label(zeile, text=e["accelerator"], bg=w.FACE, anchor="e", padx=12, pady=2, fg=w.SHADOW if grau else w.DARK) acc.pack(side="right") labels.append(acc) idx = len(self.zeilen) for wdg in [zeile] + labels: wdg.bind("", lambda ev, i=idx: self._markieren(i)) wdg.bind("", lambda ev, i=idx: self._ausfuehren(i)) self.zeilen.append((e, zeile, labels)) f.update_idletasks() bw, bh = f.winfo_reqwidth(), f.winfo_reqheight() sw, sh = f.winfo_screenwidth(), f.winfo_screenheight() x = max(0, min(x, sw - bw)) y = max(0, min(y, sh - bh)) # Größe UND Position zusammen setzen: eine reine Positionsangabe ignoriert Tk unter Windows bei # rahmenlosen Fenstern – das Menü landete sonst oben links in der Bildschirmecke. f.geometry(f"{bw}x{bh}+{x}+{y}") f.deiconify() f.lift() f.update_idletasks() try: from provtool.gui import win32 win32.besitzer_setzen(f, root) # bleibt über dem Hauptfenster except Exception: pass # Alle Maus-Ereignisse der Anwendung an das Menü (lokale Tk-Sperre): Klick daneben schließt, # Bewegung über die Menüleiste wechselt das Menü. try: f.grab_set() except tk.TclError: pass f.focus_force() f.bind("", self._klick) f.bind("", self._bewegung) f.bind("", lambda e: self.schliessen()) f.bind("", lambda e: self._schritt(-1)) f.bind("", lambda e: self._schritt(1)) f.bind("", lambda e: self._ausfuehren(self.aktiv)) f.bind("", lambda e: self._ausfuehren(self.aktiv)) f.bind("", lambda e: self.leiste and self.leiste.nachbar(-1)) f.bind("", lambda e: self.leiste and self.leiste.nachbar(1)) self.aktiv = -1 self._pruefer = f.after(250, self._fokus_pruefen) def schliessen(self): if self._pruefer is not None and self.fenster is not None: try: self.fenster.after_cancel(self._pruefer) except tk.TclError: pass self._pruefer = None if self.offen: try: self.fenster.grab_release() except tk.TclError: pass self.fenster.destroy() self.fenster = None if self.leiste is not None: self.leiste._geschlossen(self) # ------------------------------------------------------------- Bedienung def _innerhalb(self, x_root: int, y_root: int) -> bool: """Liegt der Punkt über diesem Menü? Fragt das Fenstersystem, welches Fenster dort liegt – die Geometrie rahmenloser Fenster meldet Tk unter Windows nicht zuverlässig.""" f = self.fenster try: unter = f.winfo_containing(x_root, y_root) except (tk.TclError, KeyError): unter = None if unter is not None: return unter.winfo_toplevel() is f return (f.winfo_rootx() <= x_root < f.winfo_rootx() + f.winfo_width() and f.winfo_rooty() <= y_root < f.winfo_rooty() + f.winfo_height()) def _klick(self, e): if self._innerhalb(e.x_root, e.y_root): return if self.leiste is not None and self.leiste.knopf_bei(e.x_root, e.y_root) is not None: knopf = self.leiste.knopf_bei(e.x_root, e.y_root) if knopf is self.leiste.offener_knopf: self.schliessen() # Klick auf den eigenen Menütitel klappt zu else: self.leiste.oeffnen(knopf) return "break" self.schliessen() return "break" def _bewegung(self, e): if self.leiste is None or self._innerhalb(e.x_root, e.y_root): return knopf = self.leiste.knopf_bei(e.x_root, e.y_root) if knopf is not None and knopf is not self.leiste.offener_knopf: self.leiste.oeffnen(knopf) def _markieren(self, i: int): self.aktiv = i for j, (e, zeile, labels) in enumerate(self.zeilen): an = j == i and e.get("state") != "disabled" bg = w.NAVY if an else w.FACE fg = w.WHITE if an else (w.SHADOW if e.get("state") == "disabled" else w.DARK) zeile.configure(bg=bg) for lbl in labels: lbl.configure(bg=bg, fg=fg) def _schritt(self, richtung: int): if not self.zeilen: return i = self.aktiv for _ in range(len(self.zeilen)): i = (i + richtung) % len(self.zeilen) if self.zeilen[i][0].get("state") != "disabled": break self._markieren(i) def _ausfuehren(self, i: int): if not (0 <= i < len(self.zeilen)): return e = self.zeilen[i][0] if e.get("state") == "disabled": return self.schliessen() if e.get("command"): # nach dem Schließen ausführen (der Befehl öffnet evtl. einen Dialog) self.master.after_idle(e["command"]) def _fokus_pruefen(self): """In ein anderes Programm gewechselt -> Menü schließen.""" self._pruefer = None if not self.offen: return try: weg = self.fenster.focus_get() is None except (tk.TclError, KeyError): weg = False if weg: self.schliessen() else: self._pruefer = self.fenster.after(250, self._fokus_pruefen) class Menueleiste(tk.Frame): """Graue Menüleiste mit Popup-Menüs (die native von Windows 10/11 ist weiß).""" def __init__(self, master, root: tk.Misc): super().__init__(master, bg=w.FACE) self.root = root self.knoepfe: dict[str, tk.Label] = {} self.menues: dict[tk.Label, Popup] = {} self.offener_knopf: tk.Label | None = None def menue(self, label: str, underline: int | None = 0) -> Popup: knopf = tk.Label(self, text=label, bg=w.FACE, padx=7, pady=2, bd=1, relief="flat", underline=-1 if underline is None else underline) knopf.pack(side="left") popup = Popup(self) self.knoepfe[label] = knopf self.menues[knopf] = popup knopf.bind("", lambda e, k=knopf: self._klick(k)) knopf.bind("", lambda e, k=knopf: self._hover(k, True)) knopf.bind("", lambda e, k=knopf: self._hover(k, False)) if underline is not None: taste = label[underline].lower() self.root.bind_all(f"", lambda e, k=knopf: self._alt(k)) return popup # ---- Zustand def knopf_bei(self, x_root: int, y_root: int) -> tk.Label | None: for k in self.knoepfe.values(): if (k.winfo_rootx() <= x_root < k.winfo_rootx() + k.winfo_width() and k.winfo_rooty() <= y_root < k.winfo_rooty() + k.winfo_height()): return k return None def _darstellen(self): for k in self.knoepfe.values(): if k is self.offener_knopf: k.configure(bg=w.NAVY, fg=w.WHITE, relief="flat") else: k.configure(bg=w.FACE, fg=w.DARK, relief="flat") # ---- Bedienung def oeffnen(self, knopf: tk.Label): from provtool.gui import modal if modal.offen(): return alt = self.offener_knopf if alt is not None and alt is not knopf: self.menues[alt].leiste = None # ohne Rückmeldung schließen, wir öffnen gleich das nächste self.menues[alt].schliessen() self.offener_knopf = knopf self._darstellen() self.menues[knopf].oeffnen(knopf.winfo_rootx(), knopf.winfo_rooty() + knopf.winfo_height(), leiste=self) def schliessen(self): if self.offener_knopf is not None: self.menues[self.offener_knopf].schliessen() def _geschlossen(self, popup: Popup): if self.offener_knopf is not None and self.menues.get(self.offener_knopf) is popup: self.offener_knopf = None self._darstellen() def nachbar(self, richtung: int): liste = list(self.knoepfe.values()) if self.offener_knopf in liste: i = (liste.index(self.offener_knopf) + richtung) % len(liste) self.oeffnen(liste[i]) def _klick(self, knopf): if self.offener_knopf is knopf: self.schliessen() else: self.oeffnen(knopf) return "break" def _hover(self, knopf, drin: bool): if self.offener_knopf is None: knopf.configure(relief="raised" if drin else "flat") def _alt(self, knopf): self.oeffnen(knopf) if self.offener_knopf is knopf: self.menues[knopf]._schritt(1) return "break"