Implement dark mode theme (#744)

Co-authored-by: DevilXD <4180725+DevilXD@users.noreply.github.com>
This commit is contained in:
kWAYandDevilXD authored and GitHub committed 2025-08-27 17:19:32 +02:00
1 parent f990437731
commit 2737936739
23 files changed
+265 -16

No files matched your search

+229 -11
View File
@@ -264,6 +264,10 @@ class PaddedListbox(tk.Listbox):
# because 'config = configure' makes mypy complain
self.configure(*args, **kwargs)
def configure_theme(self, *, bg: str, fg: str, sel_bg: str, sel_fg: str):
# Apply basic colors for dark/light mode
super().config(bg=bg, fg=fg, selectbackground=sel_bg, selectforeground=sel_fg)
class MouseOverLabel(ttk.Label):
def __init__(self, *args, alt_text: str = '', reverse: bool = False, **kwargs) -> None:
@@ -819,6 +823,16 @@ class ConsoleOutput:
self._text.see("end") # scroll to the newly added line
self._text.config(state="disabled")
def configure_theme(self, *, bg: str, fg: str, sel_bg: str, sel_fg: str):
# Apply colors to the Tk Text widget used for console output
self._text.config(
bg=bg,
fg=fg,
insertbackground=fg,
selectbackground=sel_bg,
selectforeground=sel_fg,
)
class _Buttons(TypedDict):
frame: ttk.Frame
@@ -1291,6 +1305,10 @@ class InventoryOverview:
self._campaigns: dict[DropsCampaign, CampaignDisplay] = {}
self._drops: dict[str, ttk.Label] = {}
def configure_theme(self, *, bg: str):
# Canvas background needs manual control
self._canvas.configure(bg=bg)
def _update_visibility(self, campaign: DropsCampaign):
# True if the campaign is supposed to show, False makes it hidden.
frame = self._campaigns[campaign]["frame"]
@@ -1528,6 +1546,7 @@ class _SettingsVars(TypedDict):
tray: IntVar
proxy: StringVar
autostart: IntVar
dark_mode: IntVar
language: StringVar
priority_mode: StringVar
tray_notifications: IntVar
@@ -1550,7 +1569,7 @@ class SettingsPanel:
}
def __init__(self, manager: GUIManager, master: ttk.Widget):
self._twitch = manager._twitch
self._manager = manager
self._settings: Settings = manager._twitch.settings
priority_mode = self._settings.priority_mode
if priority_mode not in self.PRIORITY_MODES:
@@ -1561,6 +1580,7 @@ class SettingsPanel:
"language": StringVar(master, _.current),
"proxy": StringVar(master, str(self._settings.proxy)),
"tray": IntVar(master, self._settings.autostart_tray),
"dark_mode": IntVar(master, int(self._settings.dark_mode)),
"priority_mode": StringVar(master, self.PRIORITY_MODES[priority_mode]),
"tray_notifications": IntVar(master, self._settings.tray_notifications),
}
@@ -1616,6 +1636,14 @@ class SettingsPanel:
variable=self._vars["tray_notifications"],
command=self.update_notifications,
).grid(column=1, row=irow, sticky="w")
ttk.Label(
checkboxes_frame, text=_("gui", "settings", "general", "dark_mode")
).grid(column=0, row=(irow := irow + 1), sticky="e")
ttk.Checkbutton(
checkboxes_frame,
variable=self._vars["dark_mode"],
command=self.update_dark_mode,
).grid(column=1, row=irow, sticky="w")
ttk.Label(
checkboxes_frame, text=_("gui", "settings", "general", "priority_mode")
).grid(column=0, row=(irow := irow + 1), sticky="e")
@@ -1719,7 +1747,7 @@ class SettingsPanel:
ttk.Button(
reload_frame,
text=_("gui", "settings", "reload"),
command=self._twitch.state_change(State.INVENTORY_FETCH),
command=self._manager._twitch.state_change(State.INVENTORY_FETCH),
).grid(column=1, row=0)
self._vars["autostart"].set(self._query_autostart())
@@ -1728,6 +1756,11 @@ class SettingsPanel:
self._priority_list.selection_clear(0, "end")
self._exclude_list.selection_clear(0, "end")
def update_dark_mode(self) -> None:
self._settings.dark_mode = bool(self._vars["dark_mode"].get())
self._settings.alter()
self._manager.apply_theme(self._settings.dark_mode)
def update_notifications(self) -> None:
self._settings.tray_notifications = bool(self._vars["tray_notifications"].get())
@@ -2025,7 +2058,6 @@ class GUIManager:
# style adjustements
self._style = style = ttk.Style(root)
default_font = nametofont("TkDefaultFont")
# theme
theme = ''
# theme = style.theme_names()[6]
@@ -2057,17 +2089,23 @@ class GUIManager:
style.configure("green.TLabel", foreground="green")
style.configure("yellow.TLabel", foreground="goldenrod")
style.configure("red.TLabel", foreground="red")
# fonts
default_font = nametofont("TkDefaultFont")
self._fonts: dict[str, Font] = {
"default": default_font,
"large": default_font.copy(),
"monospaced": default_font.copy(),
"underlined": default_font.copy(),
}
self._fonts["large"].config(size=10)
self._fonts["underlined"].config(underline=True)
self._fonts["monospaced"].config(family="Courier New", size=10)
# label style with a monospace font
monospaced_font = Font(root, family="Courier New", size=10)
style.configure("MS.TLabel", font=monospaced_font)
style.configure("MS.TLabel", font=self._fonts["monospaced"])
# button style with a larger font
large_font = default_font.copy()
large_font.config(size=10)
style.configure("Large.TButton", font=large_font)
style.configure("Large.TButton", font=self._fonts["large"])
# label style that mimics links
link_font = default_font.copy()
link_font.config(underline=True)
style.configure("Link.TLabel", font=link_font, foreground="blue")
style.configure("Link.TLabel", font=self._fonts["underlined"], foreground="blue")
# end of style changes
root_frame = ttk.Frame(root, padding=8)
@@ -2133,6 +2171,12 @@ class GUIManager:
# use old-style window closing protocol for non-windows platforms
root.protocol("WM_DELETE_WINDOW", self.close)
root.protocol("WM_DESTROY_WINDOW", self.close)
# Save current theme and apply palette after widgets are created
try:
self._orig_theme_name = self._style.theme_use()
except Exception:
self._orig_theme_name = ''
self.apply_theme(self._twitch.settings.dark_mode)
# stay hidden in tray if needed, otherwise show the window when everything's ready
if self._twitch.settings.tray:
# NOTE: this starts the tray icon thread
@@ -2277,6 +2321,179 @@ class GUIManager:
# print to our custom output
self.output.print(message)
def apply_theme(self, dark: bool) -> None:
"""
Apply dark/light palette to ttk styles and Tk widgets in a minimal, non-invasive way.
"""
# Palette
if dark:
# Switch to a configurable ttk theme for better color control
if self._style.theme_use() != "clam":
self._style.theme_use("clam")
bg = "#1e1e1e"
fg = "#e6e6e6"
sel_bg = "#094771"
sel_fg = "#ffffff"
link = "#4ea3ff"
surface = "#252525"
header = "#2a2a2a"
fieldbg = "#2b2b2b"
border = "#3c3c3c"
muted = "#b3b3b3"
accent = "#0d99ff"
else:
# Restore original theme if we changed it
if getattr(self, "_orig_theme_name", '') and self._style.theme_use() == "clam":
self._style.theme_use(self._orig_theme_name)
# Use platform defaults but ensure toggling back is readable
bg = "#f0f0f0"
fg = "#000000"
sel_bg = "#cce5ff"
sel_fg = "#000000"
link = "blue"
surface = "#ffffff"
header = "#eeeeee"
fieldbg = "#ffffff"
border = "#cccccc"
muted = "#404040"
accent = "#0a84ff"
s = self._style
# Base containers and labels
s.configure("TFrame", background=bg, foreground=fg)
s.configure("TLabel", background=bg, foreground=fg)
s.configure("TLabelframe", background=bg, foreground=fg)
s.configure("TLabelframe.Label", background=bg, foreground=fg)
s.configure("MS.TLabel", background=bg, foreground=fg)
s.configure("green.TLabel", background=bg)
s.configure("yellow.TLabel", background=bg)
s.configure("red.TLabel", background=bg)
s.configure("Link.TLabel", font=self._fonts["underlined"], background=bg, foreground=link)
# Buttons and checks
s.configure("TButton", background=surface, foreground=fg, bordercolor=border)
s.configure("Large.TButton", background=surface, foreground=fg, bordercolor=border)
s.map(
"TButton",
background=[("active", header), ("pressed", border)],
foreground=[("disabled", muted)],
)
s.configure(
"TCheckbutton",
background=bg,
foreground=fg,
focuscolor=bg,
bordercolor=border,
)
s.map(
"TCheckbutton",
# Remove hover visuals by mapping active/pressed to the base background
background=[
("active", bg),
("pressed", bg),
],
foreground=[("disabled", muted)],
indicatorcolor=[
("selected", accent if dark else fg),
("!selected", border),
],
)
# Notebook
s.configure("TNotebook", background=bg, bordercolor=border)
s.configure("TNotebook.Tab", background=surface, foreground=fg, bordercolor=border)
s.map(
"TNotebook.Tab",
background=[("selected", header), ("active", header)],
foreground=[("disabled", muted)],
)
# Entries/Combos
s.configure(
"TEntry", fieldbackground=fieldbg, background=fieldbg, foreground=fg, insertcolor=fg
)
s.configure(
"TCombobox", fieldbackground=fieldbg, background=fieldbg, foreground=fg, arrowcolor=fg
)
# Ensure readability for readonly comboboxes (Language, Priority mode)
s.map(
"TCombobox",
foreground=[("readonly", fg), ("disabled", muted)],
fieldbackground=[("readonly", fieldbg)],
background=[("readonly", fieldbg)],
arrowcolor=[("readonly", fg)],
)
s.map("TEntry", foreground=[("disabled", muted)])
# Treeview
s.configure(
"Treeview",
background=surface,
fieldbackground=surface,
foreground=fg,
bordercolor=border,
)
s.map(
"Treeview",
background=[("selected", sel_bg)],
foreground=[("selected", sel_fg)],
)
s.configure("Treeview.Heading", background=header, foreground=fg, bordercolor=border)
# Progressbar
s.configure("TProgressbar", background=accent, troughcolor=surface)
# Scrollbars
s.configure(
"Vertical.TScrollbar",
background=surface,
troughcolor=bg,
arrowcolor=fg,
bordercolor=border,
)
s.configure(
"Horizontal.TScrollbar",
background=surface,
troughcolor=bg,
arrowcolor=fg,
bordercolor=border,
)
# Pure Tk widgets
# Console text
self.output.configure_theme(bg=surface, fg=fg, sel_bg=sel_bg, sel_fg=sel_fg)
# Listboxes
self.settings._priority_list.configure_theme(
bg=surface, fg=fg, sel_bg=sel_bg, sel_fg=sel_fg
)
self.settings._exclude_list.configure_theme(
bg=surface, fg=fg, sel_bg=sel_bg, sel_fg=sel_fg
)
# Inventory canvas
self.inv.configure_theme(bg=bg)
# Tk option database for selection/popup list readability (affects Tk-backed widgets)
# Global selection colors and listbox defaults (covers Combobox dropdown)
self._root.option_add("*selectBackground", sel_bg)
self._root.option_add("*selectForeground", sel_fg)
# Combobox dropdown list (Tk Listbox)
for key in (
"*TCombobox*Listbox.background",
"*TCombobox*Listbox.Background",
"*Listbox.background",
):
self._root.option_add(key, surface)
for key in (
"*TCombobox*Listbox.foreground",
"*TCombobox*Listbox.Foreground",
"*Listbox.foreground",
):
self._root.option_add(key, fg)
for key in (
"*TCombobox*Listbox.selectBackground",
"*Listbox.selectBackground",
):
self._root.option_add(key, sel_bg)
for key in (
"*TCombobox*Listbox.selectForeground",
"*Listbox.selectForeground",
):
self._root.option_add(key, sel_fg)
###################
# GUI MANAGER END #
@@ -2403,6 +2620,7 @@ if __name__ == "__main__":
tray=False,
priority=[],
proxy=URL(),
dark_mode=False,
alter=lambda: None,
language="English",
autostart_tray=False,