Enhance window controls

Add minimize/maximize buttons, double-click maximize behaviour, and proper window state handling for the custom title bar.
This commit is contained in:
lm 2025-10-19 18:03:07 +02:00
parent 362383faf6
commit f467e0b2e5
2 changed files with 117 additions and 37 deletions

View File

@ -76,8 +76,15 @@ class ICRAApp(
self.root.overrideredirect(True)
screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight()
default_width = int(screen_width * 0.8)
default_height = int(screen_height * 0.8)
default_x = (screen_width - default_width) // 2
default_y = (screen_height - default_height) // 4
self._window_geometry = f"{default_width}x{default_height}+{default_x}+{default_y}"
self._is_maximized = True
self.root.geometry(f"{screen_width}x{screen_height}+0+0")
self.root.configure(bg="#f2f2f7")
self.root.bind("<Map>", lambda _e: self.root.overrideredirect(True))
def start_app() -> None:

View File

@ -422,45 +422,118 @@ class UIBuilderMixin:
)
title_label.pack(side=tk.LEFT, padx=6)
close_btn = tk.Button(
title_bar,
text="",
command=self._close_app,
bg=bar_bg,
fg="#f5f5f5",
activebackground="#ff3b30",
activeforeground="#ffffff",
borderwidth=0,
highlightthickness=0,
relief="flat",
font=("Segoe UI", 10, "bold"),
cursor="hand2",
width=3,
)
close_btn.pack(side=tk.RIGHT, padx=8, pady=4)
btn_kwargs = {
"bg": bar_bg,
"fg": "#f5f5f5",
"activebackground": "#3a3a40",
"activeforeground": "#ffffff",
"borderwidth": 0,
"highlightthickness": 0,
"relief": "flat",
"font": ("Segoe UI", 10, "bold"),
"cursor": "hand2",
"width": 3,
}
close_btn = tk.Button(title_bar, text="", command=self._close_app, **btn_kwargs)
close_btn.pack(side=tk.RIGHT, padx=6, pady=4)
close_btn.bind("<Enter>", lambda _e: close_btn.configure(bg="#cf212f"))
close_btn.bind("<Leave>", lambda _e: close_btn.configure(bg=bar_bg))
for widget in (title_bar, title_label):
widget.bind("<ButtonPress-1>", self._start_window_drag)
widget.bind("<B1-Motion>", self._perform_window_drag)
def _close_app(self) -> None:
try:
self.root.destroy()
except Exception:
pass
def _start_window_drag(self, event) -> None:
self._drag_offset = (event.x_root - self.root.winfo_rootx(), event.y_root - self.root.winfo_rooty())
def _perform_window_drag(self, event) -> None:
offset = getattr(self, "_drag_offset", None)
if offset is None:
return
x = event.x_root - offset[0]
y = event.y_root - offset[1]
self.root.geometry(f"+{x}+{y}")
max_btn = tk.Button(title_bar, text="", command=self._toggle_maximize_window, **btn_kwargs)
max_btn.pack(side=tk.RIGHT, padx=0, pady=4)
max_btn.bind("<Enter>", lambda _e: max_btn.configure(bg="#2c2c32"))
max_btn.bind("<Leave>", lambda _e: max_btn.configure(bg=bar_bg))
self._max_button = max_btn
min_btn = tk.Button(title_bar, text="", command=self._minimize_window, **btn_kwargs)
min_btn.pack(side=tk.RIGHT, padx=0, pady=4)
min_btn.bind("<Enter>", lambda _e: min_btn.configure(bg="#2c2c32"))
min_btn.bind("<Leave>", lambda _e: min_btn.configure(bg=bar_bg))
for widget in (title_bar, title_label):
widget.bind("<ButtonPress-1>", self._start_window_drag)
widget.bind("<B1-Motion>", self._perform_window_drag)
widget.bind("<Double-Button-1>", lambda _e: self._toggle_maximize_window())
self._update_maximize_button()
def _close_app(self) -> None:
try:
self.root.destroy()
except Exception:
pass
def _start_window_drag(self, event) -> None:
if getattr(self, "_is_maximized", False):
cursor_x, cursor_y = event.x_root, event.y_root
self._toggle_maximize_window(force_state=False)
self.root.update_idletasks()
new_x = self.root.winfo_rootx()
new_y = self.root.winfo_rooty()
self._drag_offset = (cursor_x - new_x, cursor_y - new_y)
return
self._drag_offset = (event.x_root - self.root.winfo_rootx(), event.y_root - self.root.winfo_rooty())
def _perform_window_drag(self, event) -> None:
offset = getattr(self, "_drag_offset", None)
if offset is None:
return
x = event.x_root - offset[0]
y = event.y_root - offset[1]
self.root.geometry(f"+{x}+{y}")
if not getattr(self, "_is_maximized", False):
self._remember_window_geometry()
def _remember_window_geometry(self) -> None:
try:
self._window_geometry = self.root.geometry()
except Exception:
pass
def _maximize_window(self) -> None:
self._remember_window_geometry()
screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight()
self.root.geometry(f"{screen_width}x{screen_height}+0+0")
self._is_maximized = True
self._update_maximize_button()
def _restore_window(self) -> None:
geometry = getattr(self, "_window_geometry", None)
if not geometry:
screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight()
width = int(screen_width * 0.8)
height = int(screen_height * 0.8)
x = (screen_width - width) // 2
y = (screen_height - height) // 4
geometry = f"{width}x{height}+{x}+{y}"
self.root.geometry(geometry)
self._is_maximized = False
self._update_maximize_button()
def _toggle_maximize_window(self, force_state: bool | None = None) -> None:
desired = force_state if force_state is not None else not getattr(self, "_is_maximized", False)
if desired:
self._maximize_window()
else:
self._restore_window()
def _minimize_window(self) -> None:
try:
self.root.overrideredirect(False)
self.root.iconify()
self.root.after(50, lambda: self.root.overrideredirect(True))
except Exception:
pass
def _update_maximize_button(self) -> None:
button = getattr(self, "_max_button", None)
if button is None:
return
symbol = "" if getattr(self, "_is_maximized", False) else ""
button.configure(text=symbol)
def _maybe_focus_window(self, _event) -> None:
try: