Compare commits

...

2 Commits

Author SHA1 Message Date
lm 27c0e55711 Upscale small previews to fill canvas 2025-10-19 18:10:24 +02:00
lm f467e0b2e5 Enhance window controls
Add minimize/maximize buttons, double-click maximize behaviour, and proper window state handling for the custom title bar.
2025-10-19 18:03:07 +02:00
3 changed files with 120 additions and 38 deletions

View File

@ -76,8 +76,15 @@ class ICRAApp(
self.root.overrideredirect(True) self.root.overrideredirect(True)
screen_width = self.root.winfo_screenwidth() screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight() 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.geometry(f"{screen_width}x{screen_height}+0+0")
self.root.configure(bg="#f2f2f7") self.root.configure(bg="#f2f2f7")
self.root.bind("<Map>", lambda _e: self.root.overrideredirect(True))
def start_app() -> None: def start_app() -> None:

View File

@ -422,28 +422,41 @@ class UIBuilderMixin:
) )
title_label.pack(side=tk.LEFT, padx=6) title_label.pack(side=tk.LEFT, padx=6)
close_btn = tk.Button( btn_kwargs = {
title_bar, "bg": bar_bg,
text="", "fg": "#f5f5f5",
command=self._close_app, "activebackground": "#3a3a40",
bg=bar_bg, "activeforeground": "#ffffff",
fg="#f5f5f5", "borderwidth": 0,
activebackground="#ff3b30", "highlightthickness": 0,
activeforeground="#ffffff", "relief": "flat",
borderwidth=0, "font": ("Segoe UI", 10, "bold"),
highlightthickness=0, "cursor": "hand2",
relief="flat", "width": 3,
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.pack(side=tk.RIGHT, padx=8, pady=4)
close_btn.bind("<Enter>", lambda _e: close_btn.configure(bg="#cf212f")) close_btn.bind("<Enter>", lambda _e: close_btn.configure(bg="#cf212f"))
close_btn.bind("<Leave>", lambda _e: close_btn.configure(bg=bar_bg)) close_btn.bind("<Leave>", lambda _e: close_btn.configure(bg=bar_bg))
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): for widget in (title_bar, title_label):
widget.bind("<ButtonPress-1>", self._start_window_drag) widget.bind("<ButtonPress-1>", self._start_window_drag)
widget.bind("<B1-Motion>", self._perform_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: def _close_app(self) -> None:
try: try:
@ -452,6 +465,14 @@ class UIBuilderMixin:
pass pass
def _start_window_drag(self, event) -> None: 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()) self._drag_offset = (event.x_root - self.root.winfo_rootx(), event.y_root - self.root.winfo_rooty())
def _perform_window_drag(self, event) -> None: def _perform_window_drag(self, event) -> None:
@ -461,6 +482,58 @@ class UIBuilderMixin:
x = event.x_root - offset[0] x = event.x_root - offset[0]
y = event.y_root - offset[1] y = event.y_root - offset[1]
self.root.geometry(f"+{x}+{y}") 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: def _maybe_focus_window(self, _event) -> None:
try: try:

View File

@ -194,7 +194,9 @@ class ImageProcessingMixin:
return return
width, height = self.orig_img.size width, height = self.orig_img.size
max_w, max_h = PREVIEW_MAX_SIZE max_w, max_h = PREVIEW_MAX_SIZE
scale = min(max_w / width, max_h / height, 1.0) scale = min(max_w / width, max_h / height)
if scale <= 0:
scale = 1.0
size = (max(1, int(width * scale)), max(1, int(height * scale))) size = (max(1, int(width * scale)), max(1, int(height * scale)))
self.preview_img = self.orig_img.resize(size, Image.LANCZOS) self.preview_img = self.orig_img.resize(size, Image.LANCZOS)
self.preview_tk = ImageTk.PhotoImage(self.preview_img) self.preview_tk = ImageTk.PhotoImage(self.preview_img)