Implement dark mode theme (#744)
Co-authored-by: DevilXD <4180725+DevilXD@users.noreply.github.com>
This commit is contained in:
1 parent
f990437731
commit
2737936739
23 files changed
+265
-16
No files matched your search
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user