Source code for frontend.window_utils

"""Window management utilities for Tkinter."""

from __future__ import annotations

import tkinter as tk


[docs] def center_window( window: tk.Tk | tk.Toplevel, width: int | None = None, height: int | None = None, *, preserve_size: bool = False, max_width_ratio: float = 0.85, max_height_ratio: float = 0.85, resizable: bool = False, y_offset_up: int = 40, ) -> None: """Center *window* on the screen. When *preserve_size* is ``True`` the window is sized to its requested (content-driven) dimensions instead of the supplied *width*/*height*. Maximum dimensions are clamped to *max_width_ratio* / *max_height_ratio* of the screen. Args: window: The Tk or Toplevel window. width: Desired minimum width (ignored when *preserve_size* is set). height: Desired minimum height (ignored when *preserve_size* is set). preserve_size: Use the widget-requested size instead of explicit dims. max_width_ratio: Max fraction of screen width. max_height_ratio: Max fraction of screen height. resizable: Whether the window can be resized by the user. y_offset_up: Pixels to shift the window up from center (avoids bottom overflow). """ window.update_idletasks() screen_w = window.winfo_screenwidth() screen_h = window.winfo_screenheight() max_w = int(screen_w * max_width_ratio) max_h = int(screen_h * max_height_ratio) if preserve_size: w = max(1, window.winfo_reqwidth()) h = max(1, window.winfo_reqheight()) else: w = width or max(1, window.winfo_reqwidth()) h = height or max(1, window.winfo_reqheight()) w = min(w, max_w) h = min(h, max_h) x = max(0, (screen_w - w) // 2) y = max(0, (screen_h - h) // 2 - y_offset_up) window.geometry(f"{w}x{h}+{x}+{y}") window.resizable(resizable, resizable)
[docs] def fit_and_center( window: tk.Tk | tk.Toplevel, min_width: int = 400, min_height: int = 300, padding: int = 40, *, max_ratio: float = 0.9, **center_kwargs: object, ) -> None: """Size *window* to fit its content (with minimums) and center it. Computes dimensions from the window's requested size, clamps to ``[min_width, screen * max_ratio]``, then delegates to :func:`center_window`. Args: window: The Tk or Toplevel window. min_width: Minimum window width in pixels. min_height: Minimum window height in pixels. padding: Extra pixels added to the requested size. max_ratio: Maximum fraction of the screen for each dimension. **center_kwargs: Forwarded to :func:`center_window`. """ window.update_idletasks() req_w = window.winfo_reqwidth() + padding req_h = window.winfo_reqheight() + padding screen_w = window.winfo_screenwidth() screen_h = window.winfo_screenheight() w = min(max(req_w, min_width), int(screen_w * max_ratio)) h = min(max(req_h, min_height), int(screen_h * max_ratio)) center_window( window, w, h, max_width_ratio=max_ratio, max_height_ratio=max_ratio, **center_kwargs, )
[docs] def make_modal(dialog: tk.Toplevel, parent: tk.Tk | tk.Toplevel) -> None: """Make a Toplevel dialog modal relative to *parent*. Args: dialog: The dialog window. parent: The parent window to block. """ dialog.transient(parent) dialog.grab_set() dialog.focus_force()
[docs] def bind_wraplength( frame: tk.Widget, label_or_labels: tk.Widget | list[tk.Widget], pad: int = 20, min_wrap: int = 200, debounce_ms: int = 50, ) -> None: """Bind label(s) wraplength to the width of a frame. Automatically adjusts wraplength when the frame is resized, ensuring text wraps nicely within the available space. Supports debouncing to avoid excessive updates during rapid resize. Args: frame: The frame whose width determines the wraplength. label_or_labels: Single label widget or list of labels to update. pad: Padding in pixels to subtract from frame width. min_wrap: Minimum wraplength in pixels. debounce_ms: Debounce delay for Configure events (0 = no debounce). """ labels = [label_or_labels] if isinstance(label_or_labels, tk.Widget) else list(label_or_labels) def _update(event: object | None = None) -> None: w = frame.winfo_width() if w > 100: wrap = max(min_wrap, w - pad) for lbl in labels: if lbl.winfo_exists(): lbl.configure(wraplength=wrap) if debounce_ms > 0: _job: str | None = None def _debounced(event: object | None = None) -> None: nonlocal _job if _job is not None: try: frame.after_cancel(_job) except tk.TclError: pass def _run() -> None: nonlocal _job _job = None _update(event) _job = frame.after(debounce_ms, _run) frame.bind("<Configure>", _debounced) else: frame.bind("<Configure>", _update) frame.after(100, _update)