Compare commits
15 Commits
pull_image
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
f09da5018f | |
|
|
8053dc297a | |
|
|
f65c37407c | |
|
|
3e6650eb2e | |
|
|
2bf9776076 | |
|
|
1984145043 | |
|
|
8ed1acc32d | |
|
|
183b769000 | |
|
|
07d7679889 | |
|
|
01a9b2707c | |
|
|
f2db62036b | |
|
|
fb37da7e40 | |
|
|
50b9aa723c | |
|
|
27c0e55711 | |
|
|
f467e0b2e5 |
88
app/app.py
88
app/app.py
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import platform
|
||||
import tkinter as tk
|
||||
from importlib import resources
|
||||
|
||||
from .gui import ColorPickerMixin, ExclusionMixin, ThemeMixin, UIBuilderMixin
|
||||
from .i18n import I18nMixin
|
||||
|
|
@ -73,11 +76,94 @@ class ICRAApp(
|
|||
self.bring_to_front()
|
||||
|
||||
def _setup_window(self) -> None:
|
||||
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._use_overrideredirect = True
|
||||
self.root.geometry(f"{screen_width}x{screen_height}+0+0")
|
||||
self.root.configure(bg="#f2f2f7")
|
||||
try:
|
||||
self.root.overrideredirect(True)
|
||||
except Exception:
|
||||
try:
|
||||
self.root.attributes("-type", "splash")
|
||||
except Exception:
|
||||
pass
|
||||
self._window_icon_ref = None
|
||||
self._apply_window_icon()
|
||||
self._init_window_chrome()
|
||||
|
||||
def _ensure_taskbar_entry(self) -> None:
|
||||
"""Force the borderless window to show up in the Windows taskbar."""
|
||||
try:
|
||||
if platform.system() != "Windows":
|
||||
return
|
||||
hwnd = self.root.winfo_id()
|
||||
if not hwnd:
|
||||
self.root.after(50, self._ensure_taskbar_entry)
|
||||
return
|
||||
|
||||
GWL_EXSTYLE = -20
|
||||
WS_EX_TOOLWINDOW = 0x00000080
|
||||
WS_EX_APPWINDOW = 0x00040000
|
||||
SWP_NOSIZE = 0x0001
|
||||
SWP_NOMOVE = 0x0002
|
||||
SWP_NOZORDER = 0x0004
|
||||
SWP_FRAMECHANGED = 0x0020
|
||||
|
||||
user32 = ctypes.windll.user32 # type: ignore[attr-defined]
|
||||
shell32 = ctypes.windll.shell32 # type: ignore[attr-defined]
|
||||
|
||||
style = user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
|
||||
new_style = (style & ~WS_EX_TOOLWINDOW) | WS_EX_APPWINDOW
|
||||
if new_style != style:
|
||||
user32.SetWindowLongW(hwnd, GWL_EXSTYLE, new_style)
|
||||
user32.SetWindowPos(
|
||||
hwnd,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED,
|
||||
)
|
||||
|
||||
app_id = ctypes.c_wchar_p("ICRA.App")
|
||||
shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _apply_window_icon(self) -> None:
|
||||
try:
|
||||
icon_resource = resources.files("app.assets").joinpath("logo.png")
|
||||
with resources.as_file(icon_resource) as icon_path:
|
||||
icon = tk.PhotoImage(file=str(icon_path))
|
||||
self.root.iconphoto(False, icon)
|
||||
self._window_icon_ref = icon
|
||||
except Exception:
|
||||
self._window_icon_ref = None
|
||||
|
||||
def _init_window_chrome(self) -> None:
|
||||
"""Configure a borderless window while retaining a taskbar entry."""
|
||||
try:
|
||||
self.root.bind("<Map>", self._restore_borderless)
|
||||
self.root.after(0, self._restore_borderless)
|
||||
self.root.after(0, self._ensure_taskbar_entry)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _restore_borderless(self, _event=None) -> None:
|
||||
try:
|
||||
if self._use_overrideredirect:
|
||||
self.root.overrideredirect(True)
|
||||
self._ensure_taskbar_entry()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def start_app() -> None:
|
||||
|
|
|
|||
201
app/gui/ui.py
201
app/gui/ui.py
|
|
@ -422,45 +422,172 @@ 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 _monitor_work_area(self) -> tuple[int, int, int, int] | None:
|
||||
try:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
user32 = ctypes.windll.user32 # type: ignore[attr-defined]
|
||||
root_x = self.root.winfo_rootx()
|
||||
root_y = self.root.winfo_rooty()
|
||||
width = max(self.root.winfo_width(), 1)
|
||||
height = max(self.root.winfo_height(), 1)
|
||||
center_x = root_x + width // 2
|
||||
center_y = root_y + height // 2
|
||||
|
||||
class MONITORINFO(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cbSize", wintypes.DWORD),
|
||||
("rcMonitor", wintypes.RECT),
|
||||
("rcWork", wintypes.RECT),
|
||||
("dwFlags", wintypes.DWORD),
|
||||
]
|
||||
|
||||
monitor = user32.MonitorFromPoint(
|
||||
wintypes.POINT(center_x, center_y), 2 # MONITOR_DEFAULTTONEAREST
|
||||
)
|
||||
info = MONITORINFO()
|
||||
info.cbSize = ctypes.sizeof(MONITORINFO)
|
||||
if not user32.GetMonitorInfoW(monitor, ctypes.byref(info)):
|
||||
return None
|
||||
work = info.rcWork
|
||||
return work.left, work.top, work.right, work.bottom
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _maximize_window(self) -> None:
|
||||
self._remember_window_geometry()
|
||||
work_area = self._monitor_work_area()
|
||||
if work_area is None:
|
||||
screen_width = self.root.winfo_screenwidth()
|
||||
screen_height = self.root.winfo_screenheight()
|
||||
left = 0
|
||||
top = 0
|
||||
width = screen_width
|
||||
height = screen_height
|
||||
else:
|
||||
left, top, right, bottom = work_area
|
||||
width = max(1, right - left)
|
||||
height = max(1, bottom - top)
|
||||
self.root.geometry(f"{width}x{height}+{left}+{top}")
|
||||
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._remember_window_geometry()
|
||||
use_or = getattr(self, "_use_overrideredirect", False)
|
||||
if use_or and hasattr(self.root, "overrideredirect"):
|
||||
try:
|
||||
self.root.overrideredirect(False)
|
||||
except Exception:
|
||||
pass
|
||||
self.root.iconify()
|
||||
if use_or:
|
||||
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:
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -194,7 +194,9 @@ class ImageProcessingMixin:
|
|||
return
|
||||
width, height = self.orig_img.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)))
|
||||
self.preview_img = self.orig_img.resize(size, Image.LANCZOS)
|
||||
self.preview_tk = ImageTk.PhotoImage(self.preview_img)
|
||||
|
|
|
|||
Loading…
Reference in New Issue