Compare commits
No commits in common. "8ed1acc32da14052889975df4263b50e97a1a4e1" and "07d76798896cc73fa9a6b73f2c83c274e3927561" have entirely different histories.
8ed1acc32d
...
07d7679889
67
app/app.py
67
app/app.py
|
|
@ -76,6 +76,9 @@ class ICRAApp(
|
||||||
self.bring_to_front()
|
self.bring_to_front()
|
||||||
|
|
||||||
def _setup_window(self) -> None:
|
def _setup_window(self) -> None:
|
||||||
|
system = platform.system()
|
||||||
|
self.use_native_titlebar = system == "Windows"
|
||||||
|
|
||||||
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_width = int(screen_width * 0.8)
|
||||||
|
|
@ -83,10 +86,16 @@ class ICRAApp(
|
||||||
default_x = (screen_width - default_width) // 2
|
default_x = (screen_width - default_width) // 2
|
||||||
default_y = (screen_height - default_height) // 4
|
default_y = (screen_height - default_height) // 4
|
||||||
self._window_geometry = f"{default_width}x{default_height}+{default_x}+{default_y}"
|
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")
|
if self.use_native_titlebar:
|
||||||
|
self._is_maximized = False
|
||||||
|
self.root.geometry(self._window_geometry)
|
||||||
|
self.root.after(0, lambda: self.root.state("zoomed"))
|
||||||
|
else:
|
||||||
|
self.root.overrideredirect(True)
|
||||||
|
self._is_maximized = True
|
||||||
|
self.root.geometry(f"{screen_width}x{screen_height}+0+0")
|
||||||
self.root.configure(bg="#f2f2f7")
|
self.root.configure(bg="#f2f2f7")
|
||||||
self.root.overrideredirect(True)
|
|
||||||
self._window_icon_ref = None
|
self._window_icon_ref = None
|
||||||
self._apply_window_icon()
|
self._apply_window_icon()
|
||||||
self._init_window_chrome()
|
self._init_window_chrome()
|
||||||
|
|
@ -142,21 +151,55 @@ class ICRAApp(
|
||||||
self._window_icon_ref = None
|
self._window_icon_ref = None
|
||||||
|
|
||||||
def _init_window_chrome(self) -> None:
|
def _init_window_chrome(self) -> None:
|
||||||
"""Configure a borderless window while retaining a taskbar entry."""
|
"""Configure window chrome based on platform."""
|
||||||
try:
|
try:
|
||||||
self.root.bind("<Map>", self._restore_borderless)
|
if self.use_native_titlebar:
|
||||||
self.root.after(0, self._restore_borderless)
|
if platform.system() == "Windows":
|
||||||
self.root.after(0, self._ensure_taskbar_entry)
|
self.root.after(0, self._ensure_taskbar_entry)
|
||||||
|
initial_mode = getattr(self, "theme", "light")
|
||||||
|
self.root.after(0, lambda: self._apply_os_titlebar_theme(initial_mode))
|
||||||
|
self.root.bind(
|
||||||
|
"<Map>",
|
||||||
|
lambda _e: self._apply_os_titlebar_theme(getattr(self, "theme", "light")),
|
||||||
|
add="+",
|
||||||
|
)
|
||||||
|
self.root.bind("<Map>", lambda _e: self._ensure_taskbar_entry(), add="+")
|
||||||
|
else:
|
||||||
|
self.root.bind("<Map>", self._restore_borderless)
|
||||||
|
self.root.after(0, self._restore_borderless)
|
||||||
|
self.root.after(0, self._ensure_taskbar_entry)
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
self.root.overrideredirect(True)
|
||||||
self.root.overrideredirect(True)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _restore_borderless(self, _event=None) -> None:
|
def _restore_borderless(self, _event=None) -> None:
|
||||||
try:
|
try:
|
||||||
self.root.overrideredirect(True)
|
self.root.overrideredirect(True)
|
||||||
self._ensure_taskbar_entry()
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _apply_os_titlebar_theme(self, mode: str | None = None) -> None:
|
||||||
|
try:
|
||||||
|
if platform.system() != "Windows":
|
||||||
|
return
|
||||||
|
|
||||||
|
hwnd = self.root.winfo_id()
|
||||||
|
if not hwnd:
|
||||||
|
self.root.after(50, lambda: self._apply_os_titlebar_theme(mode))
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
dwmapi = ctypes.windll.dwmapi # type: ignore[attr-defined]
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
attribute = ctypes.c_uint(20) # DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||||
|
value = ctypes.c_int(1 if (mode or self.theme) == "dark" else 0)
|
||||||
|
dwmapi.DwmSetWindowAttribute(
|
||||||
|
hwnd,
|
||||||
|
attribute,
|
||||||
|
ctypes.byref(value),
|
||||||
|
ctypes.sizeof(value),
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,10 @@ class ThemeMixin:
|
||||||
if callable(canvas_refresher):
|
if callable(canvas_refresher):
|
||||||
canvas_refresher()
|
canvas_refresher()
|
||||||
|
|
||||||
|
os_theme_hook = getattr(self, "_apply_os_titlebar_theme", None)
|
||||||
|
if callable(os_theme_hook):
|
||||||
|
os_theme_hook(self.theme)
|
||||||
|
|
||||||
def detect_system_theme(self) -> str:
|
def detect_system_theme(self) -> str:
|
||||||
"""Best-effort detection of the OS theme preference."""
|
"""Best-effort detection of the OS theme preference."""
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ class UIBuilderMixin:
|
||||||
"""Constructs the Tkinter UI and common widgets."""
|
"""Constructs the Tkinter UI and common widgets."""
|
||||||
|
|
||||||
def setup_ui(self) -> None:
|
def setup_ui(self) -> None:
|
||||||
self._create_titlebar()
|
self._custom_titlebar = not getattr(self, "use_native_titlebar", False)
|
||||||
|
if self._custom_titlebar:
|
||||||
|
self._create_titlebar()
|
||||||
|
|
||||||
toolbar = ttk.Frame(self.root)
|
toolbar = ttk.Frame(self.root)
|
||||||
toolbar.pack(fill=tk.X, padx=12, pady=(4, 2))
|
toolbar.pack(fill=tk.X, padx=12, pady=(4, 2))
|
||||||
|
|
@ -465,6 +467,8 @@ class UIBuilderMixin:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _start_window_drag(self, event) -> None:
|
def _start_window_drag(self, event) -> None:
|
||||||
|
if not getattr(self, "_custom_titlebar", True):
|
||||||
|
return
|
||||||
if getattr(self, "_is_maximized", False):
|
if getattr(self, "_is_maximized", False):
|
||||||
cursor_x, cursor_y = event.x_root, event.y_root
|
cursor_x, cursor_y = event.x_root, event.y_root
|
||||||
self._toggle_maximize_window(force_state=False)
|
self._toggle_maximize_window(force_state=False)
|
||||||
|
|
@ -476,6 +480,8 @@ class UIBuilderMixin:
|
||||||
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:
|
||||||
|
if not getattr(self, "_custom_titlebar", True):
|
||||||
|
return
|
||||||
offset = getattr(self, "_drag_offset", None)
|
offset = getattr(self, "_drag_offset", None)
|
||||||
if offset is None:
|
if offset is None:
|
||||||
return
|
return
|
||||||
|
|
@ -525,6 +531,8 @@ class UIBuilderMixin:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _maximize_window(self) -> None:
|
def _maximize_window(self) -> None:
|
||||||
|
if not getattr(self, "_custom_titlebar", True):
|
||||||
|
return
|
||||||
self._remember_window_geometry()
|
self._remember_window_geometry()
|
||||||
work_area = self._monitor_work_area()
|
work_area = self._monitor_work_area()
|
||||||
if work_area is None:
|
if work_area is None:
|
||||||
|
|
@ -543,6 +551,8 @@ class UIBuilderMixin:
|
||||||
self._update_maximize_button()
|
self._update_maximize_button()
|
||||||
|
|
||||||
def _restore_window(self) -> None:
|
def _restore_window(self) -> None:
|
||||||
|
if not getattr(self, "_custom_titlebar", True):
|
||||||
|
return
|
||||||
geometry = getattr(self, "_window_geometry", None)
|
geometry = getattr(self, "_window_geometry", None)
|
||||||
if not geometry:
|
if not geometry:
|
||||||
screen_width = self.root.winfo_screenwidth()
|
screen_width = self.root.winfo_screenwidth()
|
||||||
|
|
@ -557,6 +567,8 @@ class UIBuilderMixin:
|
||||||
self._update_maximize_button()
|
self._update_maximize_button()
|
||||||
|
|
||||||
def _toggle_maximize_window(self, force_state: bool | None = None) -> None:
|
def _toggle_maximize_window(self, force_state: bool | None = None) -> None:
|
||||||
|
if not getattr(self, "_custom_titlebar", True):
|
||||||
|
return
|
||||||
desired = force_state if force_state is not None else not getattr(self, "_is_maximized", False)
|
desired = force_state if force_state is not None else not getattr(self, "_is_maximized", False)
|
||||||
if desired:
|
if desired:
|
||||||
self._maximize_window()
|
self._maximize_window()
|
||||||
|
|
@ -564,19 +576,11 @@ class UIBuilderMixin:
|
||||||
self._restore_window()
|
self._restore_window()
|
||||||
|
|
||||||
def _minimize_window(self) -> None:
|
def _minimize_window(self) -> None:
|
||||||
|
if not getattr(self, "_custom_titlebar", True):
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
self._remember_window_geometry()
|
self._remember_window_geometry()
|
||||||
if hasattr(self.root, "overrideredirect"):
|
|
||||||
try:
|
|
||||||
self.root.overrideredirect(False)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self.root.iconify()
|
self.root.iconify()
|
||||||
restorer = getattr(self, "_restore_borderless", None)
|
|
||||||
if callable(restorer):
|
|
||||||
self.root.after(120, restorer)
|
|
||||||
elif hasattr(self.root, "overrideredirect"):
|
|
||||||
self.root.after(120, lambda: self.root.overrideredirect(True)) # type: ignore[arg-type]
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue