1281 lines
40 KiB
Python
1281 lines
40 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
UI-Hilfsklassen und -Funktionen für KG-Diktat Desktop.
|
||
ToolTip, RoundedButton, Geometrie-Verwaltung, Schriftgrößen-Steuerung.
|
||
"""
|
||
|
||
import os
|
||
import json
|
||
import tkinter as tk
|
||
import tkinter.font as tkfont
|
||
from tkinter import ttk
|
||
from tkinter.scrolledtext import ScrolledText
|
||
|
||
from aza_config import (
|
||
FIXED_FONT_SCALE, FIXED_BUTTON_SCALE,
|
||
_ALL_WINDOWS,
|
||
FONT_SIZES_CONFIG_FILENAME, PANED_POSITIONS_CONFIG_FILENAME,
|
||
get_writable_data_dir,
|
||
)
|
||
|
||
|
||
# ─── Tooltip ───
|
||
|
||
class ToolTip:
|
||
"""Zeigt Tooltips beim Überfahren mit der Maus."""
|
||
def __init__(self, widget, text):
|
||
self.widget = widget
|
||
self.text = text
|
||
self.tooltip = None
|
||
self.widget.bind("<Enter>", self.show_tooltip)
|
||
self.widget.bind("<Leave>", self.hide_tooltip)
|
||
|
||
def show_tooltip(self, event=None):
|
||
try:
|
||
x = self.widget.winfo_rootx() + 25
|
||
y = self.widget.winfo_rooty() + 25
|
||
self.tooltip = tk.Toplevel(self.widget)
|
||
self.tooltip.wm_overrideredirect(True)
|
||
self.tooltip.wm_geometry(f"+{x}+{y}")
|
||
label = tk.Label(
|
||
self.tooltip,
|
||
text=self.text,
|
||
background="#FFFACD",
|
||
foreground="#000000",
|
||
relief="solid",
|
||
borderwidth=1,
|
||
font=("Segoe UI", 9),
|
||
padx=8,
|
||
pady=4
|
||
)
|
||
label.pack()
|
||
except Exception:
|
||
pass
|
||
|
||
def hide_tooltip(self, event=None):
|
||
if self.tooltip:
|
||
self.tooltip.destroy()
|
||
self.tooltip = None
|
||
|
||
|
||
def add_tooltip(widget, text):
|
||
"""Fügt einem Widget einen Tooltip hinzu."""
|
||
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):
|
||
"""Zentriert ein Fenster auf dem Bildschirm."""
|
||
window.update_idletasks()
|
||
if width is None or height is None:
|
||
geom = window.geometry().split('+')[0].split('x')
|
||
if width is None:
|
||
width = int(geom[0])
|
||
if height is None:
|
||
height = int(geom[1])
|
||
screen_width = window.winfo_screenwidth()
|
||
screen_height = window.winfo_screenheight()
|
||
x = (screen_width - width) // 2
|
||
y = (screen_height - height) // 2
|
||
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:
|
||
config_file = os.path.join(
|
||
get_writable_data_dir(),
|
||
f"kg_diktat_{window_name}_geometry.txt"
|
||
)
|
||
with open(config_file, "w", encoding="utf-8") as f:
|
||
f.write(geometry)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def load_toplevel_geometry(window_name: str) -> str:
|
||
"""Lädt die gespeicherte Geometrie eines Toplevel-Fensters."""
|
||
try:
|
||
config_file = os.path.join(
|
||
get_writable_data_dir(),
|
||
f"kg_diktat_{window_name}_geometry.txt"
|
||
)
|
||
if os.path.isfile(config_file):
|
||
with open(config_file, "r", encoding="utf-8") as f:
|
||
return f.read().strip()
|
||
except Exception:
|
||
pass
|
||
return None
|
||
|
||
|
||
def setup_window_geometry_saving(window, window_name: str, default_width: int, default_height: int):
|
||
"""Richtet automatisches Speichern der Fensterposition ein und lädt gespeicherte Position."""
|
||
saved_geom = load_toplevel_geometry(window_name)
|
||
if saved_geom:
|
||
try:
|
||
window.geometry(saved_geom)
|
||
except Exception:
|
||
window.geometry(f"{default_width}x{default_height}")
|
||
center_window(window, default_width, default_height)
|
||
else:
|
||
window.geometry(f"{default_width}x{default_height}")
|
||
center_window(window, default_width, default_height)
|
||
|
||
def on_close():
|
||
try:
|
||
geom = window.geometry()
|
||
save_toplevel_geometry(window_name, geom)
|
||
except Exception:
|
||
pass
|
||
try:
|
||
window.destroy()
|
||
except Exception:
|
||
pass
|
||
window.protocol("WM_DELETE_WINDOW", on_close)
|
||
|
||
|
||
# ─── Resize-Griff ───
|
||
|
||
def add_resize_grip(win, min_w=None, min_h=None):
|
||
"""Fuegt unten rechts einen sichtbaren Resize-Griff hinzu.
|
||
|
||
Verwendet place() mit lift(), damit der Grip garantiert ueber allen
|
||
pack/grid-Inhalten schwebt und nicht ueberdeckt wird.
|
||
Ein after()-Aufruf stellt sicher, dass der Grip nach dem Layout
|
||
nochmals nach vorn gebracht wird.
|
||
"""
|
||
mw, mh = win.minsize() if win.minsize() else (300, 200)
|
||
min_w = min_w if min_w is not None else mw
|
||
min_h = min_h if min_h is not None else mh
|
||
|
||
grip = tk.Frame(win, width=28, height=28, bg="#C8C8C8", cursor="size_nw_se",
|
||
highlightthickness=1, highlightbackground="#A0A0A0")
|
||
grip.place(relx=1.0, rely=1.0, anchor="se")
|
||
grip.pack_propagate(False)
|
||
lbl = tk.Label(grip, text="\u22F0", font=("Segoe UI", 12), bg="#C8C8C8",
|
||
fg="#555", cursor="size_nw_se")
|
||
lbl.pack(fill="both", expand=True)
|
||
|
||
grip.lift()
|
||
|
||
def _ensure_on_top():
|
||
try:
|
||
grip.lift()
|
||
except Exception:
|
||
pass
|
||
win.after(100, _ensure_on_top)
|
||
win.after(500, _ensure_on_top)
|
||
|
||
data = [0, 0, 0, 0]
|
||
|
||
def on_press(e):
|
||
data[0], data[1] = e.x_root, e.y_root
|
||
data[2], data[3] = win.winfo_width(), win.winfo_height()
|
||
|
||
def on_motion(e):
|
||
nw = max(min_w, data[2] + (e.x_root - data[0]))
|
||
nh = max(min_h, data[3] + (e.y_root - data[1]))
|
||
win.geometry(f"{int(nw)}x{int(nh)}")
|
||
|
||
for w in (grip, lbl):
|
||
w.bind("<ButtonPress-1>", on_press)
|
||
w.bind("<B1-Motion>", on_motion)
|
||
|
||
|
||
# ─── Abgerundeter Button ───
|
||
|
||
def _round_rect(canvas, x1, y1, x2, y2, r=8, **kw):
|
||
"""Zeichnet ein abgerundetes Rechteck auf dem Canvas."""
|
||
fill = kw.get("fill", kw.get("outline", "gray"))
|
||
if r <= 0:
|
||
canvas.create_rectangle(x1, y1, x2, y2, **kw)
|
||
return
|
||
canvas.create_rectangle(x1 + r, y1, x2 - r, y2, **kw)
|
||
canvas.create_rectangle(x1, y1 + r, x2, y2 - r, **kw)
|
||
canvas.create_arc(x1, y1, x1 + 2 * r, y1 + 2 * r, start=90, extent=90, style=tk.PIESLICE, **kw)
|
||
canvas.create_arc(x2 - 2 * r, y1, x2, y1 + 2 * r, start=0, extent=90, style=tk.PIESLICE, **kw)
|
||
canvas.create_arc(x2 - 2 * r, y2 - 2 * r, x2, y2, start=270, extent=90, style=tk.PIESLICE, **kw)
|
||
canvas.create_arc(x1, y2 - 2 * r, x1 + 2 * r, y2, start=180, extent=90, style=tk.PIESLICE, **kw)
|
||
|
||
|
||
def _blend_color(hex1: str, hex2: str, factor: float) -> str:
|
||
"""Mischt zwei Hex-Farben. factor=0 → hex1, factor=1 → hex2."""
|
||
factor = max(0.0, min(1.0, factor))
|
||
try:
|
||
r1, g1, b1 = int(hex1[1:3], 16), int(hex1[3:5], 16), int(hex1[5:7], 16)
|
||
r2, g2, b2 = int(hex2[1:3], 16), int(hex2[3:5], 16), int(hex2[5:7], 16)
|
||
r = int(r1 + (r2 - r1) * factor)
|
||
g = int(g1 + (g2 - g1) * factor)
|
||
b = int(b1 + (b2 - b1) * factor)
|
||
return f"#{r:02x}{g:02x}{b:02x}"
|
||
except Exception:
|
||
return hex1
|
||
|
||
|
||
# Pastell-Teal-Orange: warm, aber weicher/sanfter.
|
||
_USAGE_HEAT_TARGET_BG = "#FDC8A3"
|
||
_USAGE_HEAT_TARGET_ACTIVE = "#F7BB91"
|
||
_USAGE_HEAT_STEP = 0.10
|
||
_USAGE_HEAT_FILE = os.path.join(get_writable_data_dir(), "kg_diktat_button_heat.json")
|
||
_button_heat_data: dict = {}
|
||
_button_heat_session_used: set = set()
|
||
_all_rounded_buttons: list = []
|
||
|
||
|
||
def _load_button_heat():
|
||
global _button_heat_data
|
||
try:
|
||
if os.path.isfile(_USAGE_HEAT_FILE):
|
||
with open(_USAGE_HEAT_FILE, "r", encoding="utf-8") as f:
|
||
_button_heat_data = json.load(f)
|
||
except Exception:
|
||
_button_heat_data = {}
|
||
|
||
|
||
def save_button_heat():
|
||
"""Beim App-Beenden aufrufen: unbenutzte Buttons um 1 reduzieren, dann speichern."""
|
||
for key in list(_button_heat_data.keys()):
|
||
if key not in _button_heat_session_used:
|
||
_button_heat_data[key] = max(0, _button_heat_data[key] - 1)
|
||
if _button_heat_data[key] <= 0:
|
||
del _button_heat_data[key]
|
||
try:
|
||
with open(_USAGE_HEAT_FILE, "w", encoding="utf-8") as f:
|
||
json.dump(_button_heat_data, f, indent=2, ensure_ascii=False)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def reset_button_heat():
|
||
"""Setzt alle Button-Farben auf Originalzustand zurück."""
|
||
global _button_heat_data, _button_heat_session_used
|
||
_button_heat_data.clear()
|
||
_button_heat_session_used.clear()
|
||
try:
|
||
if os.path.isfile(_USAGE_HEAT_FILE):
|
||
os.remove(_USAGE_HEAT_FILE)
|
||
except Exception:
|
||
pass
|
||
for btn in _all_rounded_buttons:
|
||
try:
|
||
if btn.winfo_exists():
|
||
btn._click_count = 0
|
||
btn._bg = btn._orig_bg
|
||
btn._active_bg = btn._orig_active_bg
|
||
btn._draw()
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
_load_button_heat()
|
||
|
||
|
||
class RoundedButton(tk.Canvas):
|
||
"""Button mit abgerundeten Ecken (gleiche Nutzung wie ttk.Button)."""
|
||
|
||
def __init__(self, parent, text, command=None, bg="#7EC8E3", fg="#1a4d6d", active_bg="#5AB9E8", radius=8, width=None, height=None, canvas_bg=None, **kw):
|
||
self._base_width = width if width is not None else 120
|
||
self._base_height = height if height is not None else 32
|
||
kw.setdefault("highlightthickness", 0)
|
||
if canvas_bg is not None:
|
||
kw["bg"] = canvas_bg
|
||
super().__init__(parent, width=self._base_width, height=self._base_height, **kw)
|
||
self._command = command
|
||
self._orig_bg = bg
|
||
self._orig_active_bg = active_bg
|
||
self._bg, self._fg = bg, fg
|
||
self._active_bg = active_bg
|
||
self._radius = radius
|
||
self._text = text
|
||
self._heat_key = text
|
||
self._base_font_size = 11
|
||
self._font_size_scale = 1.0
|
||
self._button_size_scale = 1.0
|
||
self._click_count = _button_heat_data.get(self._heat_key, 0)
|
||
if self._click_count > 0:
|
||
self._apply_usage_heat()
|
||
_all_rounded_buttons.append(self)
|
||
self.bind("<Button-1>", self._on_click)
|
||
self.bind("<Enter>", self._on_enter)
|
||
self.bind("<Leave>", self._on_leave)
|
||
self.bind("<Configure>", lambda e: self._draw())
|
||
self._draw()
|
||
|
||
def set_font_size_scale(self, scale: float):
|
||
self._font_size_scale = scale
|
||
self._draw()
|
||
|
||
def set_button_size_scale(self, scale: float):
|
||
self._button_size_scale = scale
|
||
new_width = int(self._base_width * scale)
|
||
new_height = int(self._base_height * scale)
|
||
self.configure(width=new_width, height=new_height)
|
||
self._draw()
|
||
|
||
def set_font_scale(self, scale: float):
|
||
self.set_font_size_scale(scale)
|
||
self.set_button_size_scale(scale)
|
||
|
||
def _apply_usage_heat(self):
|
||
"""Farbe Richtung Orange verschieben basierend auf Klickanzahl."""
|
||
if getattr(self, "lock_color", False):
|
||
return
|
||
factor = min(self._click_count * _USAGE_HEAT_STEP, 1.0)
|
||
self._bg = _blend_color(self._orig_bg, _USAGE_HEAT_TARGET_BG, factor)
|
||
self._active_bg = _blend_color(self._orig_active_bg, _USAGE_HEAT_TARGET_ACTIVE, factor)
|
||
|
||
def _draw(self, active=False):
|
||
self.delete("all")
|
||
w, h = self.winfo_width(), self.winfo_height()
|
||
if w <= 1:
|
||
w = int(self._base_width * self._button_size_scale)
|
||
if h <= 1:
|
||
h = int(self._base_height * self._button_size_scale)
|
||
fill = self._active_bg if active else self._bg
|
||
_round_rect(self, 0, 0, w, h, self._radius, fill=fill, outline=fill)
|
||
font_size = max(5, int(16 * self._font_size_scale))
|
||
self.create_text(w // 2, h // 2, text=self._text, fill=self._fg, font=("Segoe UI", font_size))
|
||
|
||
def _on_click(self, event):
|
||
self._click_count += 1
|
||
_button_heat_data[self._heat_key] = self._click_count
|
||
_button_heat_session_used.add(self._heat_key)
|
||
self._apply_usage_heat()
|
||
self._draw(active=True)
|
||
if self._command:
|
||
self._command()
|
||
|
||
def _on_enter(self, event):
|
||
self._draw(active=True)
|
||
|
||
def _on_leave(self, event):
|
||
self._draw(active=False)
|
||
|
||
def configure(self, **kw):
|
||
if "command" in kw:
|
||
self._command = kw.pop("command")
|
||
redraw = False
|
||
if "text" in kw:
|
||
self._text = kw.pop("text")
|
||
redraw = True
|
||
# Eigene Style-Optionen abfangen (gleiche Namen wie im Konstruktor),
|
||
# damit sie NICHT an tk.Canvas weitergereicht werden ("unknown option").
|
||
if "bg" in kw:
|
||
self._bg = kw.pop("bg")
|
||
self._orig_bg = self._bg
|
||
redraw = True
|
||
if "fg" in kw:
|
||
self._fg = kw.pop("fg")
|
||
redraw = True
|
||
if "active_bg" in kw:
|
||
self._active_bg = kw.pop("active_bg")
|
||
self._orig_active_bg = self._active_bg
|
||
redraw = True
|
||
if kw:
|
||
super().configure(**kw)
|
||
if redraw:
|
||
self._draw()
|
||
|
||
config = configure
|
||
|
||
|
||
# ─── Schriftgrößen-Skalierung ───
|
||
|
||
def add_font_scale_control(win, on_change_callback=None):
|
||
"""Wendet fixierte optimale Schrift- und Button-Größen an."""
|
||
if win not in _ALL_WINDOWS:
|
||
_ALL_WINDOWS.append(win)
|
||
|
||
def on_close():
|
||
try:
|
||
if win in _ALL_WINDOWS:
|
||
_ALL_WINDOWS.remove(win)
|
||
except Exception:
|
||
pass
|
||
try:
|
||
win.destroy()
|
||
except Exception:
|
||
pass
|
||
win.protocol("WM_DELETE_WINDOW", on_close)
|
||
|
||
win.after(50, lambda: scale_window_fonts(win, FIXED_FONT_SCALE))
|
||
win.after(100, lambda: scale_window_buttons(win, FIXED_BUTTON_SCALE))
|
||
return None, None, None
|
||
|
||
|
||
def scale_window_fonts(win, scale: float):
|
||
"""Skaliert nur die Schriftgrößen in Text-Widgets und Labels."""
|
||
try:
|
||
def scale_recursive(widget):
|
||
try:
|
||
if isinstance(widget, (tk.Text, ScrolledText)):
|
||
try:
|
||
current_font = widget.cget("font")
|
||
if isinstance(current_font, str):
|
||
current_font = tkfont.Font(font=current_font)
|
||
elif isinstance(current_font, tuple):
|
||
base_size = 16
|
||
new_size = max(5, int(base_size * scale))
|
||
widget.configure(font=(current_font[0], new_size))
|
||
except Exception:
|
||
pass
|
||
elif isinstance(widget, tk.Label):
|
||
try:
|
||
current_font = widget.cget("font")
|
||
if isinstance(current_font, tuple) and len(current_font) >= 2:
|
||
base_size = 16
|
||
new_size = max(5, int(base_size * scale))
|
||
widget.configure(font=(current_font[0], new_size))
|
||
except Exception:
|
||
pass
|
||
elif isinstance(widget, RoundedButton):
|
||
widget.set_font_size_scale(scale)
|
||
for child in widget.winfo_children():
|
||
scale_recursive(child)
|
||
except Exception:
|
||
pass
|
||
scale_recursive(win)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def scale_window_buttons(win, scale: float):
|
||
"""Skaliert nur die Button-Größen."""
|
||
try:
|
||
def scale_recursive(widget):
|
||
try:
|
||
if isinstance(widget, RoundedButton):
|
||
widget.set_button_size_scale(scale)
|
||
for child in widget.winfo_children():
|
||
scale_recursive(child)
|
||
except Exception:
|
||
pass
|
||
scale_recursive(win)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def scale_window_widgets(win, scale: float):
|
||
"""Legacy-Funktion für Rückwärtskompatibilität."""
|
||
scale_window_fonts(win, scale)
|
||
scale_window_buttons(win, scale)
|
||
|
||
|
||
def apply_initial_scaling_to_window(win):
|
||
"""Wendet initiale Skalierung auf ein neu erstelltes Fenster an."""
|
||
from aza_persistence import load_font_scale, load_button_scale
|
||
try:
|
||
if win not in _ALL_WINDOWS:
|
||
_ALL_WINDOWS.append(win)
|
||
font_scale = load_font_scale()
|
||
button_scale = load_button_scale()
|
||
win.after(100, lambda: scale_window_fonts(win, font_scale))
|
||
win.after(150, lambda: scale_window_buttons(win, button_scale))
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
# ─── Textfeld-Schriftgrößen (per-window) ───
|
||
|
||
def _font_sizes_config_path():
|
||
return os.path.join(get_writable_data_dir(), FONT_SIZES_CONFIG_FILENAME)
|
||
|
||
|
||
def load_paned_positions():
|
||
"""Lädt gespeicherte PanedWindow-Positionen."""
|
||
try:
|
||
path = os.path.join(get_writable_data_dir(), PANED_POSITIONS_CONFIG_FILENAME)
|
||
if os.path.isfile(path):
|
||
with open(path, "r", encoding="utf-8") as f:
|
||
return json.load(f)
|
||
except Exception:
|
||
pass
|
||
return {}
|
||
|
||
|
||
def save_paned_positions(positions):
|
||
"""Speichert PanedWindow-Positionen."""
|
||
try:
|
||
path = os.path.join(get_writable_data_dir(), PANED_POSITIONS_CONFIG_FILENAME)
|
||
with open(path, "w", encoding="utf-8") as f:
|
||
json.dump(positions, f, indent=2)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def load_text_font_size(key: str, default: int = 9) -> int:
|
||
"""Lädt gespeicherte Schriftgröße für einen bestimmten Text-Widget."""
|
||
try:
|
||
path = _font_sizes_config_path()
|
||
if os.path.isfile(path):
|
||
with open(path, "r", encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
return int(data.get(key, default))
|
||
except Exception:
|
||
pass
|
||
return default
|
||
|
||
|
||
def save_text_font_size(key: str, size: int):
|
||
"""Speichert Schriftgröße für einen bestimmten Text-Widget."""
|
||
try:
|
||
path = _font_sizes_config_path()
|
||
data = {}
|
||
if os.path.isfile(path):
|
||
with open(path, "r", encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
data[key] = int(size)
|
||
with open(path, "w", encoding="utf-8") as f:
|
||
json.dump(data, f, indent=2)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def add_text_font_size_control(parent_frame, text_widget, initial_size=10, label="Aa", bg_color="#F5FCFF", save_key=None):
|
||
"""Fügt ▲▼-Pfeile für Textfeld-Schriftgrößen hinzu (5–20pt)."""
|
||
if save_key:
|
||
initial_size = load_text_font_size(save_key, initial_size)
|
||
|
||
_size = [max(5, min(20, initial_size))]
|
||
_fg = "#8AAFC0"
|
||
_fg_hover = "#1a4d6d"
|
||
|
||
control_frame = tk.Frame(parent_frame, bg=bg_color, highlightthickness=0, bd=0)
|
||
control_frame.pack(side="right", padx=4)
|
||
|
||
lbl = tk.Label(control_frame, text=label, font=("Segoe UI", 8),
|
||
bg=bg_color, fg=_fg)
|
||
lbl.pack(side="left", padx=(0, 1))
|
||
|
||
size_lbl = tk.Label(control_frame, text=str(_size[0]), font=("Segoe UI", 8),
|
||
bg=bg_color, fg=_fg, width=2, anchor="center")
|
||
size_lbl.pack(side="left")
|
||
|
||
def _apply(new_size):
|
||
new_size = max(5, min(20, new_size))
|
||
_size[0] = new_size
|
||
size_lbl.configure(text=str(new_size))
|
||
text_widget.configure(font=("Segoe UI", new_size))
|
||
if save_key:
|
||
save_text_font_size(save_key, new_size)
|
||
|
||
text_widget.configure(font=("Segoe UI", _size[0]))
|
||
|
||
btn_up = tk.Label(control_frame, text="\u25B2", font=("Segoe UI", 7),
|
||
bg=bg_color, fg=_fg, cursor="hand2",
|
||
bd=0, highlightthickness=0, padx=0, pady=0)
|
||
btn_up.pack(side="left", padx=(2, 0))
|
||
|
||
btn_down = tk.Label(control_frame, text="\u25BC", font=("Segoe UI", 7),
|
||
bg=bg_color, fg=_fg, cursor="hand2",
|
||
bd=0, highlightthickness=0, padx=0, pady=0)
|
||
btn_down.pack(side="left", padx=(0, 0))
|
||
|
||
btn_up.bind("<Button-1>", lambda e: _apply(_size[0] + 1))
|
||
btn_down.bind("<Button-1>", lambda e: _apply(_size[0] - 1))
|
||
|
||
for w in (btn_up, btn_down):
|
||
w.bind("<Enter>", lambda e, ww=w: ww.configure(fg=_fg_hover))
|
||
w.bind("<Leave>", lambda e, ww=w: ww.configure(fg=_fg))
|
||
|
||
return _size
|