Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions GUI/controls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
from dm40.types import ThemePalette


class UIControls:
def __init__(self, master, style, theme: ThemePalette):
def __init__(self, master, style, theme):
self.master = master
self.style = style
self.theme = theme
Expand All @@ -11,7 +8,7 @@ def __init__(self, master, style, theme: ThemePalette):
self._init_layouts()
self.apply_theme()

def use_theme(self, theme: ThemePalette) -> None:
def use_theme(self, theme) -> None:
self.theme = theme
self.apply_theme()

Expand Down
38 changes: 9 additions & 29 deletions GUI/theme_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
import tkinter as tk
from tkinter import ttk

from dm40.types import ThemePalette
from dm40.theme_store import deserialize_theme_store_palettes
from GUI.widgets.helpers import theme_title_bar
from shared.theme_store import deserialize_theme_store_palettes
from GUI.widgets.helpers import center_on_parent, theme_title_bar
from GUI.widgets.themed_button import ThemedButton

_PREVIEW_FRAME = "ThemePreview.TFrame"
Expand Down Expand Up @@ -34,7 +33,7 @@ def __init__(
self.style = style
self._on_apply = on_apply

self._themes: list[ThemePalette] = deserialize_theme_store_palettes(_DEFAULT_STORE)
self._themes = deserialize_theme_store_palettes(_DEFAULT_STORE)
self._active_theme_idx = self._read_active_index()

self._dialog = None
Expand Down Expand Up @@ -64,7 +63,7 @@ def list_theme_names(self) -> list[str]:
def get_active_theme_index(self) -> int:
return self._active_theme_idx

def get_active_theme(self) -> ThemePalette:
def get_active_theme(self):
return self._themes[self._active_theme_idx]

def activate_theme_index(self, theme_idx: int):
Expand Down Expand Up @@ -234,7 +233,7 @@ def _apply_selected_theme(self):
self._select_listbox_index(self._active_theme_idx)
self._update_preview_from_selection()

def _activate_by_index(self, idx: int) -> tuple[ThemePalette | None, bool]:
def _activate_by_index(self, idx: int):
if idx < 0 or idx >= len(self._themes):
return None, False
if self._active_theme_idx == idx:
Expand All @@ -246,7 +245,7 @@ def _activate_by_index(self, idx: int) -> tuple[ThemePalette | None, bool]:
self._update_listbox_active(old_idx)
return self._themes[idx], True

def _apply_dialog_chrome(self, theme: ThemePalette):
def _apply_dialog_chrome(self, theme):
dialog = self._dialog
if not dialog or not dialog.winfo_exists():
return
Expand All @@ -261,7 +260,7 @@ def _init_preview_styles(self):
self.style.layout(_PREVIEW_BORDER, self.style.layout("Border.TFrame"))
self.style.configure(_PREVIEW_BORDER, relief="solid")

def _apply_preview_colors(self, theme: ThemePalette):
def _apply_preview_colors(self, theme):
self.style.configure(
_PREVIEW_FRAME,
background=theme.bg,
Expand Down Expand Up @@ -319,24 +318,5 @@ def _apply_preview_colors(self, theme: ThemePalette):
)

def _center_dialog(self):
dialog = self._dialog
master = self.master
if not dialog or not master:
return

dialog.update_idletasks()
master.update_idletasks()

if master.winfo_exists():
x0 = master.winfo_rootx()
y0 = master.winfo_rooty()
w0 = master.winfo_width()
h0 = master.winfo_height()

w = dialog.winfo_reqwidth()
h = dialog.winfo_reqheight()

x = x0 + (w0 - w) // 2
y = y0 + (h0 - h) // 2

dialog.geometry(f"{w}x{h}+{x}+{y}")
if self._dialog and self.master and self.master.winfo_exists():
center_on_parent(self._dialog, self.master)
42 changes: 9 additions & 33 deletions GUI/themed_messagebox.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import tkinter as tk

from GUI.widgets.helpers import theme_title_bar
from GUI.widgets.helpers import center_on_parent, theme_title_bar
from GUI.widgets.themed_button import ThemedButton

INFO_ICON = 0
ERROR_ICON = 1

_ICON_LIST = ("i", "✕")
_ERROR_ICON = "\u2715"


class _ThemedDialog(tk.Toplevel):
Expand All @@ -17,7 +14,7 @@ def __init__(
message,
*,
theme,
icon=ERROR_ICON,
icon=_ERROR_ICON,
detail=None,
buttons=None,
default=None,
Expand Down Expand Up @@ -55,12 +52,11 @@ def _build_ui(self, message, icon, detail, buttons, default, cancel_value):
if detail:
message = f"{message}\n\n{detail}\n" if message else detail
wrap_length = 260
icon_text = _ICON_LIST[icon]

if icon_text:
if icon:
icon_label = tk.Label(
container,
text=icon_text,
text=icon,
font=("Segoe UI", 18, "bold"),
)
icon_label.grid(row=0, column=0, sticky="n", padx=(0, 12))
Expand Down Expand Up @@ -97,8 +93,6 @@ def _build_ui(self, message, icon, detail, buttons, default, cancel_value):

def _apply_minsize(self):
container = self._layout_root
if not container:
return
try:
container.update_idletasks()
required_w = container.winfo_reqwidth()
Expand All @@ -116,28 +110,10 @@ def _apply_minsize(self):
self._target_size = (min_w, min_h)

def _center(self, parent):
try:
self.update_idletasks()
except tk.TclError:
pass

try:
parent.update_idletasks()
parent_x = parent.winfo_rootx()
parent_y = parent.winfo_rooty()
parent_w = parent.winfo_width()
parent_h = parent.winfo_height()
except tk.TclError:
parent_x = parent_y = 0
parent_w = self.winfo_screenwidth()
parent_h = self.winfo_screenheight()

target_w, target_h = self._target_size
w = target_w if target_w else self.winfo_width()
h = target_h if target_h else self.winfo_height()
x = parent_x + (parent_w - w) // 2
y = parent_y + (parent_h - h) // 2
self.geometry(f"{w}x{h}+{x}+{y}")
w = target_w if target_w else None
h = target_h if target_h else None
center_on_parent(self, parent, w, h)

def _finish(self, value):
self._result = value
Expand Down Expand Up @@ -184,7 +160,7 @@ def show_error(parent, title, message, *, theme: tuple, detail=None):
title,
message,
theme=theme,
icon=ERROR_ICON,
icon=_ERROR_ICON,
buttons=[("OK", True)],
default=True,
cancel_value=True,
Expand Down
5 changes: 1 addition & 4 deletions GUI/widgets/autoscrollbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@ def grid(self, **kwargs):

def _show(self):
if not self.winfo_ismapped():
if self._grid_kwargs:
super().grid(**self._grid_kwargs)
else:
super().grid()
super().grid(**self._grid_kwargs)

def _hide(self):
if self.winfo_ismapped():
Expand Down
6 changes: 3 additions & 3 deletions GUI/widgets/find_popup.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import tkinter as tk
from tkinter import ttk

from dm40.types import ThemePalette



class FindPopup:
Expand All @@ -15,7 +15,7 @@ def __init__(
self,
parent: tk.Misc,
text: tk.Text,
colors: ThemePalette,
colors,
*,
grid_opts: dict | None = None,
):
Expand Down Expand Up @@ -69,7 +69,7 @@ def __init__(
self.set_tag_colors(self._colors)
self.hide(clear=False)

def set_tag_colors(self, colors: ThemePalette) -> None:
def set_tag_colors(self, colors) -> None:
self._colors = colors
text_fg = colors.text
match_bg = colors.outline
Expand Down
69 changes: 31 additions & 38 deletions GUI/widgets/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,9 @@
_USER32 = ctypes.windll.user32
_DWMAPI = ctypes.windll.dwmapi
_SHCORE = getattr(ctypes.windll, "shcore", None)
_HAS_DPI_CONTEXT = hasattr(_USER32, "SetProcessDpiAwarenessContext")
_HAS_DPI_AWARENESS = _SHCORE is not None and hasattr(
_SHCORE, "SetProcessDpiAwareness"
)
_HAS_DPI_AWARE = hasattr(_USER32, "SetProcessDPIAware")
except (ImportError, AttributeError, OSError) as exc:
raise ImportError("Required Windows APIs are not available") from exc

DWMWA_BORDER_COLOR = 34
DWMWA_CAPTION_COLOR = 35

class intptr_t(ctypes._SimpleCData):
_type_ = "P"
_csize_ = ctypes.sizeof(ctypes.c_void_p)
Expand All @@ -26,13 +18,6 @@ class int32(ctypes._SimpleCData):
_type_ = "i"
_csize_ = 4

_DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = intptr_t(-4)


def _hex_to_rgb(value):
b = bytes.fromhex(value[1:])
return b[0], b[1], b[2]


def _set_window_attribute(hwnd, attribute, value):
if not hwnd:
Expand All @@ -45,52 +30,60 @@ def _set_window_attribute(hwnd, attribute, value):
return True


def _colorref_from_hex(value):
r, g, b = _hex_to_rgb(value)
return (b << 16) | (g << 8) | r
def _colorref_from_hex(value, _bf=bytes.fromhex, _ifb=int.from_bytes):
return _ifb(_bf(value[1:]), 'little')


def theme_title_bar(window: tk.Tk | tk.Toplevel, *, border_color: str | None = None,
caption_color: str | None = None) -> bool:
"""Apply theme-aligned border/caption colors to the window title bar."""
window.update_idletasks()
frame = window.wm_frame()
hwnd = int(frame, 16)
hwnd = int(window.wm_frame(), 16)
if border_color is not None:
if not set_title_bar_border_color(border_color, hwnd=hwnd):
val = int32(_colorref_from_hex(border_color)) if border_color else int32(0)
if not _set_window_attribute(hwnd, 34, val):
return False
if caption_color is None:
caption_color = border_color
if caption_color is not None:
if not set_title_bar_caption_color(caption_color, hwnd=hwnd):
val = int32(_colorref_from_hex(caption_color)) if caption_color else int32(0)
if not _set_window_attribute(hwnd, 35, val):
return False
return True


def set_title_bar_border_color(color_hex: str | None, hwnd=None) -> bool:
if not color_hex:
return _set_window_attribute(hwnd, DWMWA_BORDER_COLOR, int32(0))
return _set_window_attribute(hwnd, DWMWA_BORDER_COLOR, int32(_colorref_from_hex(color_hex)))

def center_on_parent(child, parent, w=None, h=None):
child.update_idletasks()
try:
parent.update_idletasks()
px = parent.winfo_rootx()
py = parent.winfo_rooty()
pw = parent.winfo_width()
ph = parent.winfo_height()
except tk.TclError:
px = py = 0
pw = child.winfo_screenwidth()
ph = child.winfo_screenheight()
if w is None:
w = child.winfo_reqwidth()
if h is None:
h = child.winfo_reqheight()
x = px + (pw - w) // 2
y = py + (ph - h) // 2
child.geometry("%dx%d+%d+%d" % (w, h, x, y))

def set_title_bar_caption_color(color_hex: str | None, hwnd=None) -> bool:
if not color_hex:
return _set_window_attribute(hwnd, DWMWA_CAPTION_COLOR, int32(0))
return _set_window_attribute(hwnd, DWMWA_CAPTION_COLOR, int32(_colorref_from_hex(color_hex)))

def ensure_dpi_awareness():
if _HAS_DPI_CONTEXT:
_USER32.SetProcessDpiAwarenessContext(
_DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
)
if hasattr(_USER32, "SetProcessDpiAwarenessContext"):
_USER32.SetProcessDpiAwarenessContext(intptr_t(-4))
return
if _HAS_DPI_AWARENESS and _SHCORE:
if _SHCORE is not None and hasattr(_SHCORE, "SetProcessDpiAwareness"):
_SHCORE.SetProcessDpiAwareness(2)
elif _HAS_DPI_AWARE:
elif hasattr(_USER32, "SetProcessDPIAware"):
_USER32.SetProcessDPIAware()


__all__ = [
'center_on_parent',
'ensure_dpi_awareness',
'theme_title_bar'
]
Loading