From f467e0b2e501fda4489faaeb2b0ca358b7185128 Mon Sep 17 00:00:00 2001 From: lm Date: Sun, 19 Oct 2025 18:03:07 +0200 Subject: [PATCH] Enhance window controls Add minimize/maximize buttons, double-click maximize behaviour, and proper window state handling for the custom title bar. --- app/app.py | 7 +++ app/gui/ui.py | 147 +++++++++++++++++++++++++++++++++++++------------- 2 files changed, 117 insertions(+), 37 deletions(-) diff --git a/app/app.py b/app/app.py index ede1227..78a37b4 100644 --- a/app/app.py +++ b/app/app.py @@ -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("", lambda _e: self.root.overrideredirect(True)) def start_app() -> None: diff --git a/app/gui/ui.py b/app/gui/ui.py index 9ca82e4..ab483d6 100644 --- a/app/gui/ui.py +++ b/app/gui/ui.py @@ -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("", lambda _e: close_btn.configure(bg="#cf212f")) close_btn.bind("", lambda _e: close_btn.configure(bg=bar_bg)) - - for widget in (title_bar, title_label): - widget.bind("", self._start_window_drag) - widget.bind("", 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("", lambda _e: max_btn.configure(bg="#2c2c32")) + max_btn.bind("", 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("", lambda _e: min_btn.configure(bg="#2c2c32")) + min_btn.bind("", lambda _e: min_btn.configure(bg=bar_bg)) + + for widget in (title_bar, title_label): + widget.bind("", self._start_window_drag) + widget.bind("", self._perform_window_drag) + widget.bind("", 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: