This commit is contained in:
2026-06-13 22:47:31 +02:00
parent add3da5177
commit d1446fc452
8032 changed files with 2650751 additions and 1551 deletions

View File

@@ -63,6 +63,124 @@ def add_tooltip(widget, text):
return ToolTip(widget, text)
# ─── AzA-Werkzeugfenster: Action-Buttons (lesbarer Disabled-State) ───
#
# Regel für AzA-Toolfenster:
# - Buttons immer lesbar (aktiv + deaktiviert)
# - Disabled: heller Hintergrund, dunkler Text (kein ausgewaschenes Weiss)
# - Blau = normale Aktion, Rot = destruktiv
# - Untere Actionbar fix; Schliessen gut sichtbar
TOOL_ACTION_BTN_STYLES: dict[str, dict[str, dict[str, str]]] = {
"primary": {
"enabled": {
"bg": "#5B8DB3",
"fg": "#FFFFFF",
"activebackground": "#4A7A9C",
"activeforeground": "#FFFFFF",
},
"disabled": {
"bg": "#DDE8F2",
"fg": "#2F5570",
"disabledforeground": "#2F5570",
"activebackground": "#DDE8F2",
"activeforeground": "#2F5570",
},
},
"secondary": {
"enabled": {
"bg": "#3A6F8F",
"fg": "#FFFFFF",
"activebackground": "#2F5E7A",
"activeforeground": "#FFFFFF",
},
"disabled": {
"bg": "#DDE8F2",
"fg": "#3A5F75",
"disabledforeground": "#3A5F75",
"activebackground": "#DDE8F2",
"activeforeground": "#3A5F75",
},
},
"danger": {
"enabled": {
"bg": "#C04040",
"fg": "#FFFFFF",
"activebackground": "#A83838",
"activeforeground": "#FFFFFF",
},
"disabled": {
"bg": "#F0DDDD",
"fg": "#8B3535",
"disabledforeground": "#8B3535",
"activebackground": "#F0DDDD",
"activeforeground": "#8B3535",
},
},
"close": {
"enabled": {
"bg": "#5B8DB3",
"fg": "#FFFFFF",
"activebackground": "#4A7A9C",
"activeforeground": "#FFFFFF",
},
"disabled": {
"bg": "#DDE8F2",
"fg": "#2F5570",
"disabledforeground": "#2F5570",
"activebackground": "#DDE8F2",
"activeforeground": "#2F5570",
},
},
}
def create_tool_action_button(
parent,
text: str,
command,
*,
kind: str = "primary",
font=None,
padx: int = 10,
pady: int = 5,
width=None,
) -> tk.Button:
"""Action-Button für AzA-Werkzeugfenster mit lesbarem Disabled-State."""
styles = TOOL_ACTION_BTN_STYLES.get(kind, TOOL_ACTION_BTN_STYLES["primary"])
kw: dict = {
"text": text,
"command": command,
"font": font or ("Segoe UI", 8, "bold"),
"relief": "flat",
"bd": 0,
"padx": padx,
"pady": pady,
"cursor": "hand2",
**styles["enabled"],
}
if width is not None:
kw["width"] = width
btn = tk.Button(parent, **kw)
btn._aza_btn_kind = kind # type: ignore[attr-defined]
return btn
def set_tool_action_button_enabled(btn: tk.Button, enabled: bool) -> None:
"""Aktiviert/deaktiviert Action-Button — Text bleibt lesbar."""
kind = str(getattr(btn, "_aza_btn_kind", "primary") or "primary")
styles = TOOL_ACTION_BTN_STYLES.get(kind, TOOL_ACTION_BTN_STYLES["primary"])
style = styles["enabled"] if enabled else styles["disabled"]
try:
btn.config(
state="normal" if enabled else "disabled",
cursor="hand2" if enabled else "arrow",
**style,
)
except Exception:
pass
# ─── Fenster-Geometrie ───
def center_window(window, width=None, height=None):
@@ -81,6 +199,586 @@ def center_window(window, width=None, height=None):
window.geometry(f"{width}x{height}+{x}+{y}")
def _safe_untopmost_tool(window) -> None:
try:
if window.winfo_exists():
window.attributes("-topmost", False)
except Exception:
pass
def clamp_window_position(
x: int,
y: int,
width: int,
height: int,
*,
screen_w: int | None = None,
screen_h: int | None = None,
margin: int = 8,
) -> tuple[int, int]:
"""Begrenzt Fensterposition auf sichtbaren Bildschirmbereich."""
sw = screen_w if screen_w is not None else 1920
sh = screen_h if screen_h is not None else 1080
max_x = max(margin, sw - width - margin)
max_y = max(margin, sh - height - margin)
return max(margin, min(int(x), max_x)), max(margin, min(int(y), max_y))
def get_work_area_at_point(x: int, y: int) -> tuple[int, int, int, int]:
"""Arbeitsfläche (left, top, right, bottom) des Monitors unter dem Punkt."""
try:
import ctypes
from ctypes import wintypes
user32 = ctypes.windll.user32
MONITOR_DEFAULTTONEAREST = 2
class RECT(ctypes.Structure):
_fields_ = [
("left", wintypes.LONG),
("top", wintypes.LONG),
("right", wintypes.LONG),
("bottom", wintypes.LONG),
]
class MONITORINFO(ctypes.Structure):
_fields_ = [
("cbSize", wintypes.DWORD),
("rcMonitor", RECT),
("rcWork", RECT),
("dwFlags", wintypes.DWORD),
]
class POINT(ctypes.Structure):
_fields_ = [("x", wintypes.LONG), ("y", wintypes.LONG)]
pt = POINT(int(x), int(y))
hmon = user32.MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST)
mi = MONITORINFO()
mi.cbSize = ctypes.sizeof(MONITORINFO)
user32.GetMonitorInfoW(hmon, ctypes.byref(mi))
r = mi.rcWork
return (int(r.left), int(r.top), int(r.right), int(r.bottom))
except Exception:
return (0, 0, 1920, 1080)
def clamp_window_position_to_work_area(
x: int,
y: int,
width: int,
height: int,
work_area: tuple[int, int, int, int],
*,
margin: int = 8,
) -> tuple[int, int]:
"""Begrenzt Position auf Monitor-Arbeitsfläche (Taskleiste berücksichtigt)."""
left, top, right, bottom = work_area
work_w = max(1, int(right) - int(left))
work_h = max(1, int(bottom) - int(top))
rel_x = int(x) - int(left)
rel_y = int(y) - int(top)
rel_x, rel_y = clamp_window_position(
rel_x, rel_y, width, height, screen_w=work_w, screen_h=work_h, margin=margin,
)
return int(left) + rel_x, int(top) + rel_y
def parse_geometry_size(geometry: str) -> tuple[int, int]:
"""Liest Breite/Höhe aus Tk-Geometry-String."""
try:
part = str(geometry or "").split("+", 1)[0]
w_s, h_s = part.split("x", 1)
return max(1, int(w_s)), max(1, int(h_s))
except Exception:
return 680, 560
def _widget_anchor_offset_in_toplevel(app: tk.Misc, anchor_widget: tk.Misc) -> tuple[int, int] | None:
"""Relativer Ankerpunkt (Mitte) eines Widgets im Hauptfenster."""
try:
if anchor_widget is None or not anchor_widget.winfo_exists():
return None
app.update_idletasks()
anchor_widget.update_idletasks()
ax = int(anchor_widget.winfo_rootx()) + max(1, int(anchor_widget.winfo_width())) // 2
ay = int(anchor_widget.winfo_rooty()) + max(1, int(anchor_widget.winfo_height())) // 2
ox = ax - int(app.winfo_rootx())
oy = ay - int(app.winfo_rooty())
return ox, oy
except Exception:
return None
def restore_main_window_at_cursor(
app: tk.Misc,
cursor_x: int,
cursor_y: int,
*,
anchor_widget: tk.Misc | None = None,
fallback_anchor: str = "top_right",
) -> None:
"""Hauptfenster an Cursorposition wiederherstellen (Größe beibehalten)."""
cx, cy = int(cursor_x), int(cursor_y)
was_zoomed = bool(getattr(app, "_mini_restore_was_zoomed", False))
try:
app.deiconify()
except Exception:
pass
if was_zoomed:
try:
work = get_work_area_at_point(cx, cy)
left, top = int(work[0]), int(work[1])
width, height = parse_geometry_size(
str(getattr(app, "_mini_restore_geometry", "") or "")
)
app.geometry(f"{width}x{height}+{left}+{top}")
app.update_idletasks()
app.state("zoomed")
except Exception:
try:
app.state("zoomed")
except Exception:
pass
try:
if hasattr(app, "_apply_main_topmost_state"):
app._apply_main_topmost_state()
app.lift()
app.focus_force()
except Exception:
pass
return
geom = getattr(app, "_mini_restore_geometry", None) or ""
width, height = parse_geometry_size(str(geom) if geom else "")
if width <= 1 or height <= 1:
try:
app.update_idletasks()
width = max(680, int(app.winfo_width() or 680))
height = max(560, int(app.winfo_height() or 560))
except Exception:
width, height = 680, 560
offset = _widget_anchor_offset_in_toplevel(app, anchor_widget)
if offset is None:
btn = getattr(app, "_btn_mini_record", None)
offset = _widget_anchor_offset_in_toplevel(app, btn)
if offset is None:
btn = getattr(app, "_btn_minimize", None)
offset = _widget_anchor_offset_in_toplevel(app, btn)
if offset is None:
if fallback_anchor == "top_left":
offset = (24, 24)
else:
offset = (max(width - 24, 24), 24)
win_x = cx - int(offset[0])
win_y = cy - int(offset[1])
work = get_work_area_at_point(cx, cy)
win_x, win_y = clamp_window_position_to_work_area(
win_x, win_y, width, height, work,
)
try:
app.geometry(f"{width}x{height}+{win_x}+{win_y}")
except Exception:
pass
try:
app.update_idletasks()
except Exception:
pass
try:
if hasattr(app, "_apply_main_topmost_state"):
app._apply_main_topmost_state()
elif hasattr(app, "lift"):
app.lift()
app.lift()
app.focus_force()
except Exception:
try:
app.lift()
except Exception:
pass
def restore_tool_window_at_cursor(
window: tk.Misc,
cursor_x: int,
cursor_y: int,
*,
anchor_widget: tk.Misc | None = None,
fallback_anchor: str = "top_right",
) -> None:
"""Tool-Fenster (Diktat etc.) an Cursorposition wiederherstellen (Größe beibehalten)."""
cx, cy = int(cursor_x), int(cursor_y)
try:
window.deiconify()
except Exception:
pass
geom = getattr(window, "_mini_restore_geometry", None) or ""
width, height = parse_geometry_size(str(geom) if geom else "")
if width <= 1 or height <= 1:
try:
window.update_idletasks()
width = max(int(window.winfo_width() or 420), 420)
height = max(int(window.winfo_height() or 380), 380)
except Exception:
width, height = 420, 380
offset = _widget_anchor_offset_in_toplevel(window, anchor_widget)
if offset is None:
if fallback_anchor == "top_left":
offset = (24, 24)
else:
offset = (max(width - 24, 24), 24)
win_x = cx - int(offset[0])
win_y = cy - int(offset[1])
work = get_work_area_at_point(cx, cy)
win_x, win_y = clamp_window_position_to_work_area(
win_x, win_y, width, height, work,
)
try:
window.geometry(f"{width}x{height}+{win_x}+{win_y}")
except Exception:
pass
try:
window.update_idletasks()
except Exception:
pass
try:
if bool(getattr(window, "_tool_pinned", False)):
apply_tool_window_pin(window, True)
window.lift()
window.focus_force()
except Exception:
try:
window.lift()
except Exception:
pass
def apply_tool_window_pin(window: tk.Misc, pinned: bool) -> None:
"""Tool-Fenster-Pin (eigener State, unabhängig vom Hauptfenster)."""
try:
window._tool_pinned = bool(pinned)
window.attributes("-topmost", bool(pinned))
if pinned:
window.lift()
except Exception:
pass
def toggle_tool_window_pin(window: tk.Misc) -> bool:
new_val = not bool(getattr(window, "_tool_pinned", False))
apply_tool_window_pin(window, new_val)
return new_val
def refresh_tool_pin_button(btn: tk.Misc | None, pinned: bool) -> None:
if btn is None:
return
try:
btn.configure(
text=("📌" if pinned else "📍"),
fg=("#FFFFFF" if pinned else "#E8F4FA"),
)
except Exception:
pass
def _restore_tool_topmost(tool_win: tk.Misc | None) -> None:
if tool_win is None:
return
try:
if not tool_win.winfo_exists():
return
except Exception:
return
if getattr(tool_win, "_tool_pinned", None) is not None:
apply_tool_window_pin(tool_win, bool(getattr(tool_win, "_tool_pinned", False)))
def add_tool_pin_button(
header: tk.Misc,
window: tk.Misc,
*,
bg: str,
side: str = "right",
padx: tuple[int, int] | int = (0, 4),
) -> tk.Label:
"""Pinnnadel für Tool-Fenster (Session-State, nicht persistent)."""
window._tool_pinned = False
btn = tk.Label(
header,
text="📍",
font=("Segoe UI Emoji", 12),
bg=bg,
fg="#E8F4FA",
cursor="hand2",
padx=8,
)
btn.pack(side=side, padx=padx)
def _toggle(_evt=None):
pinned = toggle_tool_window_pin(window)
refresh_tool_pin_button(btn, pinned)
btn.bind("<Button-1>", _toggle)
tip = ToolTip(btn, "Fenster immer im Vordergrund")
def _refresh_tip(_evt=None):
pinned = bool(getattr(window, "_tool_pinned", False))
tip.text = "Fixierung lösen" if pinned else "Fenster immer im Vordergrund"
btn.bind("<Enter>", _refresh_tip, add="+")
window._tool_pin_btn = btn
return btn
def bring_tool_window_to_front(window, *, flash_ms: int = 300) -> None:
"""Kurz in den Vordergrund (topmost-Flash), ohne dauerhaftes Always-on-top."""
if getattr(window, "_main_pinned", None) is not None:
try:
window.lift()
if hasattr(window, "_apply_main_topmost_state"):
window._apply_main_topmost_state()
else:
window.attributes("-topmost", bool(getattr(window, "_main_pinned", False)))
window.focus_force()
except Exception:
pass
return
try:
window.lift()
window.attributes("-topmost", True)
if flash_ms > 0:
window.after(flash_ms, lambda: _safe_untopmost_tool(window))
window.focus_force()
except Exception:
pass
def center_tool_window(
window,
width: int | None = None,
height: int | None = None,
*,
parent=None,
y_ratio: float = 0.08,
vertical_center: bool = False,
bring_to_front: bool = True,
) -> None:
"""Zentriert AzA-Werkzeugfenster horizontal; optional vertikal mittig."""
try:
window.update_idletasks()
except Exception:
pass
if width is None or height is None:
try:
geom = window.geometry().split("+")[0].split("x")
if width is None:
width = int(geom[0])
if height is None:
height = int(geom[1])
except Exception:
width = width or 400
height = height or 300
if parent is not None:
try:
window.transient(parent)
except Exception:
pass
try:
anchor = parent if parent is not None else window
sw = anchor.winfo_screenwidth()
sh = anchor.winfo_screenheight()
sx = anchor.winfo_rootx()
sy = anchor.winfo_rooty()
pw = max(int(anchor.winfo_width() or 0), 1)
ph = max(int(anchor.winfo_height() or 0), 1)
if parent is not None and pw > 1 and ph > 1:
x = sx + max(0, (pw - width) // 2)
if vertical_center:
y = sy + max(0, (ph - height) // 2)
else:
y = sy + max(0, int(ph * y_ratio))
else:
x = sx + max(0, (sw - width) // 2)
if vertical_center:
y = sy + max(0, (sh - height) // 2)
else:
y = sy + max(0, int(sh * y_ratio))
x, y = clamp_window_position(x, y, width, height, sw, sh)
window.geometry(f"{width}x{height}+{x}+{y}")
except Exception:
try:
window.geometry(f"{width}x{height}")
except Exception:
pass
if bring_to_front:
bring_tool_window_to_front(window)
# ─── Modale Dialoge / Messageboxes (immer sichtbar über gepinntem Hauptfenster) ───
def _resolve_toplevel_parent(widget) -> tk.Misc | None:
if widget is None:
return None
try:
if widget.winfo_exists():
return widget.winfo_toplevel()
except Exception:
pass
return widget
def _find_main_app_window(widget) -> tk.Misc | None:
w = widget
while w is not None:
if getattr(w, "_main_pinned", None) is not None:
return w
try:
w = w.master
except Exception:
break
return None
def _restore_main_topmost(main) -> None:
if main is None:
return
try:
if hasattr(main, "_apply_main_topmost_state"):
main._apply_main_topmost_state()
elif getattr(main, "_main_pinned", None) is not None:
main.attributes("-topmost", bool(main._main_pinned))
except Exception:
pass
def prepare_dialog_parent(parent) -> tk.Misc | None:
"""Parent-Toplevel für Dialog sichtbar nach vorne holen."""
parent_tl = _resolve_toplevel_parent(parent)
if parent_tl is None:
return None
try:
parent_tl.lift()
parent_tl.focus_force()
parent_tl.update_idletasks()
except Exception:
pass
return parent_tl
def make_modal_topmost(dialog, parent=None, *, grab: bool = True) -> None:
"""Modaler Toplevel-Dialog: transient, grab, lift, topmost (über Pin-Hauptfenster)."""
parent_tl = prepare_dialog_parent(parent)
if parent_tl is not None:
try:
dialog.transient(parent_tl)
except Exception:
pass
try:
dialog.update_idletasks()
dialog.attributes("-topmost", True)
dialog.lift()
dialog.focus_force()
if grab:
dialog.grab_set()
except Exception:
pass
def release_modal_dialog(dialog, parent=None) -> None:
"""Grab/Topmost lösen; Hauptfenster-Pin-State wiederherstellen."""
try:
if dialog.winfo_exists():
dialog.grab_release()
dialog.attributes("-topmost", False)
except Exception:
pass
main = _find_main_app_window(_resolve_toplevel_parent(parent or dialog))
_restore_main_topmost(main)
def _run_with_visible_parent(parent, func):
"""Messagebox/simpledialog: Parent kurz topmost, danach Pin wiederherstellen."""
parent_tl = prepare_dialog_parent(parent)
main = _find_main_app_window(parent_tl) if parent_tl else None
if parent_tl is not None:
try:
parent_tl.attributes("-topmost", True)
parent_tl.lift()
parent_tl.focus_force()
parent_tl.update_idletasks()
except Exception:
pass
try:
return func()
finally:
if parent_tl is not None:
try:
parent_tl.attributes("-topmost", False)
except Exception:
pass
_restore_tool_topmost(parent_tl)
_restore_main_topmost(main)
def aza_showinfo(title, message, *, parent=None):
from tkinter import messagebox
return _run_with_visible_parent(
parent, lambda: messagebox.showinfo(title, message, parent=parent),
)
def aza_showwarning(title, message, *, parent=None):
from tkinter import messagebox
return _run_with_visible_parent(
parent, lambda: messagebox.showwarning(title, message, parent=parent),
)
def aza_showerror(title, message, *, parent=None):
from tkinter import messagebox
return _run_with_visible_parent(
parent, lambda: messagebox.showerror(title, message, parent=parent),
)
def aza_askyesno(title, message, *, parent=None):
from tkinter import messagebox
return _run_with_visible_parent(
parent, lambda: messagebox.askyesno(title, message, parent=parent),
)
def aza_askokcancel(title, message, *, parent=None):
from tkinter import messagebox
return _run_with_visible_parent(
parent, lambda: messagebox.askokcancel(title, message, parent=parent),
)
def aza_askstring(title, prompt, *, parent=None, **kwargs):
from tkinter import simpledialog
return _run_with_visible_parent(
parent, lambda: simpledialog.askstring(title, prompt, parent=parent, **kwargs),
)
def save_toplevel_geometry(window_name: str, geometry: str) -> None:
"""Speichert die Geometrie eines Toplevel-Fensters."""
try: