sysmon_pytk.style_manager
Tk Style Manager.
1# SPDX-FileCopyrightText: © 2024 Stacey Adams <stacey.belle.rose@gmail.com> 2# SPDX-License-Identifier: MIT 3 4""" 5Tk Style Manager. 6""" 7 8from __future__ import annotations 9 10import re 11import tkinter as tk 12from tkinter import font, ttk 13from typing import TYPE_CHECKING, TypeVar 14 15import darkdetect 16 17import azure 18 19from . import font_utils 20 21if TYPE_CHECKING: 22 from tkinter.font import Font 23 24 from .settings import Settings 25 26T = TypeVar("T") 27 28_DARK_CUTOFF_SQR = 127.5 * 127.5 29_LEN_HEXCOLOR = len("#FFFFFF") 30 31 32def is_dark(hexcolor: str) -> bool: 33 """ 34 Determine whether a given hex color is light or dark. 35 36 Parameters 37 ---------- 38 hexcolor: str 39 a string with the format "#xxxxxx" where x is a hex digit (0-9, A-F) 40 41 Returns 42 ------- 43 bool 44 True when the color is determined to be dark; False otherwise. 45 46 Examples 47 -------- 48 >>> is_dark("#000000") 49 True 50 >>> is_dark("#ffffff") 51 False 52 >>> is_dark("#123456") 53 True 54 >>> is_dark("#449F55") 55 False 56 """ 57 assert len(hexcolor) == _LEN_HEXCOLOR # nosec B101 58 assert hexcolor[:1] == "#" # nosec B101 59 if re.search(r"^#[\dA-Fa-f]{6}$", hexcolor) is None: 60 msg = "hexcolor must start with '#' and have 6 hexadecimal digits" 61 raise ValueError(msg) 62 r = int(hexcolor[1:3], 16) 63 g = int(hexcolor[3:5], 16) 64 b = int(hexcolor[5:7], 16) 65 # calculate the square of the luminance and compare it to a cutoff value of 127.5² 66 # this way, sqrt() doesn't need to be called 67 hsp = 0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b) 68 return hsp < _DARK_CUTOFF_SQR 69 70 71class StyleManager: 72 """ 73 Tk Style Manager. 74 """ 75 76 @classmethod 77 def get_dark_mode(cls, settings: Settings) -> bool: 78 """ 79 Determine dark mode by checking settings or detecting the system theme. 80 """ 81 if settings.theme == "Dark": 82 return True 83 if settings.theme == "Light": 84 return False 85 return darkdetect.isDark() 86 87 @classmethod 88 def init_theme(cls, root: tk.Tk, settings: Settings) -> None: 89 """ 90 Initialize theme and styles for the application. 91 """ 92 azure.init_theme(root) # pylint: disable=W0212 93 cls.update_by_dark_mode(root, settings) 94 cls.init_fonts(settings) 95 root.option_add("*TCombobox*Listbox.font", "TkDefaultFont") 96 root.option_add("*tearOff", False) # Fix menus 97 root.bind_class("Menu", "<<ThemeChanged>>", cls.update_menu) 98 99 @classmethod 100 def init_fonts(cls, settings: Settings) -> None: 101 """ 102 Initialize the fonts to be used. 103 104 The following tk named fonts are updated based on Settings: 105 106 * TkDefaultFont - set to Regular Font from Settings 107 * TkTextFont - set to Regular Font from Settings 108 * TkMenuFont - set to Regular Font from Settings 109 * TkFixedFont - set to Fixed Font from Settings 110 111 In addition, widgets are configured to use TkDefaultFont. 112 """ 113 base_font = font.nametofont("TkDefaultFont") 114 text_font = font.nametofont("TkTextFont") 115 menu_font = font.nametofont("TkMenuFont") 116 fixed_font = font.nametofont("TkFixedFont") 117 if settings.regular_font.name: 118 settings.regular_font.configure_font(base_font) 119 settings.regular_font.configure_font(text_font) 120 settings.regular_font.configure_font(menu_font) 121 else: 122 base_font.configure(family=font_utils.MAIN_FONT_FAMILY, size=12) 123 text_font.configure(family=font_utils.MAIN_FONT_FAMILY, size=12) 124 menu_font.configure(family=font_utils.MAIN_FONT_FAMILY, size=12) 125 if settings.fixed_font.name: 126 settings.fixed_font.configure_font(fixed_font) 127 else: 128 fixed_font.configure(family=font_utils.FIXED_FONT_FAMILY, size=12) 129 style = ttk.Style() 130 style.configure("TButton", font="TkDefaultFont") 131 style.configure("TRadiobutton", font="TkDefaultFont") 132 style.configure("TLabelFrame", font="TkDefaultFont") 133 style.configure("TNotebook", font="TkDefaultFont") 134 style.configure("TNotebook.Tab", font="TkDefaultFont") 135 style.configure("Switch.TCheckbutton", font="TkDefaultFont") 136 style.configure("System.TLabel", font="TkDefaultFont") 137 style.configure("URL.TLabel", foreground="#0066cc") 138 139 @classmethod 140 def update_by_dark_mode(cls, root: tk.Tk, settings: Settings) -> None: 141 """ 142 Update styles based on dark mode. 143 144 Colors are updated for various styles and widgets: 145 146 * Safe.TLabel style (green) 147 * Warn.TLabel style (yellow) 148 * Alert.TLabel style (red) 149 * Combobox Listbox background (grey) 150 """ 151 dark_mode = cls.get_dark_mode(settings) 152 azure.set_theme("dark" if dark_mode else "light", root) 153 root.event_generate("<<ThemeChanged>>") 154 style = ttk.Style() 155 if dark_mode: 156 style.configure("Safe.TLabel", foreground="#00aa00") 157 style.configure("Warn.TLabel", foreground="#ffff22") 158 style.configure("Alert.TLabel", foreground="#ff2222") 159 root.option_add("*TCombobox*Listbox.background", "#444444") 160 else: 161 style.configure("Safe.TLabel", foreground="#009900") 162 style.configure("Warn.TLabel", foreground="#aaaa00") 163 style.configure("Alert.TLabel", foreground="#cc0000") 164 root.option_add("*TCombobox*Listbox.background", "#dddddd") 165 # ComboboxPopdownFrame must be reset every time the theme changes 166 style.configure("ComboboxPopdownFrame", relief=tk.FLAT) 167 168 @classmethod 169 def test_dark_mode(cls, trueval: T, falseval: T) -> T: 170 """ 171 If currently in dark mode, return `trueval`; otherwise return `falseval`. 172 """ 173 style = ttk.Style() 174 background = style.lookup("TLabel", "background") 175 if is_dark(f"{background}"): 176 return trueval 177 return falseval 178 179 @classmethod 180 def update_menu(cls, event: tk.Event) -> None: 181 """ 182 Update the foreground and background colors of a menu, based on dark mode. 183 """ 184 if isinstance(event.widget, tk.Menu): 185 event.widget.configure( 186 background=cls.get_menu_background(), 187 foreground=cls.get_menu_foreground() 188 ) 189 190 @classmethod 191 def get_menu_background(cls) -> str: 192 """ 193 Get the background color for menus, based on dark mode. 194 """ 195 return cls.test_dark_mode("#444444", "#dddddd") 196 197 @classmethod 198 def get_menu_foreground(cls) -> str: 199 """ 200 Get the foreground color for menus, based on dark mode. 201 """ 202 return cls.test_dark_mode("#ffffff", "#000000") 203 204 @classmethod 205 def get_base_font(cls) -> Font: 206 """ 207 Get the base font for use in the application. Returns `TkDefaultFont`. 208 """ 209 return font.nametofont("TkDefaultFont") 210 211 @classmethod 212 def get_large_font(cls) -> Font: 213 """ 214 Get the large font for use in the application. 215 216 Returns a modified `TkDefaultFont` with font size increased by 4. 217 """ 218 return font_utils.modify_named_font( 219 "TkDefaultFont", size=cls.get_base_font().actual()["size"]+4 220 ) 221 222 @classmethod 223 def get_small_font(cls) -> Font: 224 """ 225 Get the small font for use in the application. 226 227 Returns a modified `TkDefaultFont` with font size decreased by 2. 228 """ 229 return font_utils.modify_named_font( 230 "TkDefaultFont", size=cls.get_base_font().actual()["size"]-2 231 ) 232 233 @classmethod 234 def get_bold_font(cls) -> Font: 235 """ 236 Get the bold font for use in the application. 237 238 Returns a modified `TkDefaultFont` with font weight set to bold. 239 """ 240 return font_utils.modify_named_font("TkDefaultFont", weight="bold") 241 242 @classmethod 243 def get_fixed_font(cls) -> Font: 244 """ 245 Get the monospace font for use in the application. Returns `TkFixedFont`. 246 """ 247 return font.nametofont("TkFixedFont")
33def is_dark(hexcolor: str) -> bool: 34 """ 35 Determine whether a given hex color is light or dark. 36 37 Parameters 38 ---------- 39 hexcolor: str 40 a string with the format "#xxxxxx" where x is a hex digit (0-9, A-F) 41 42 Returns 43 ------- 44 bool 45 True when the color is determined to be dark; False otherwise. 46 47 Examples 48 -------- 49 >>> is_dark("#000000") 50 True 51 >>> is_dark("#ffffff") 52 False 53 >>> is_dark("#123456") 54 True 55 >>> is_dark("#449F55") 56 False 57 """ 58 assert len(hexcolor) == _LEN_HEXCOLOR # nosec B101 59 assert hexcolor[:1] == "#" # nosec B101 60 if re.search(r"^#[\dA-Fa-f]{6}$", hexcolor) is None: 61 msg = "hexcolor must start with '#' and have 6 hexadecimal digits" 62 raise ValueError(msg) 63 r = int(hexcolor[1:3], 16) 64 g = int(hexcolor[3:5], 16) 65 b = int(hexcolor[5:7], 16) 66 # calculate the square of the luminance and compare it to a cutoff value of 127.5² 67 # this way, sqrt() doesn't need to be called 68 hsp = 0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b) 69 return hsp < _DARK_CUTOFF_SQR
Determine whether a given hex color is light or dark.
Parameters
- hexcolor (str): a string with the format "#xxxxxx" where x is a hex digit (0-9, A-F)
Returns
- bool: True when the color is determined to be dark; False otherwise.
Examples
>>> is_dark("#000000")
True
>>> is_dark("#ffffff")
False
>>> is_dark("#123456")
True
>>> is_dark("#449F55")
False
72class StyleManager: 73 """ 74 Tk Style Manager. 75 """ 76 77 @classmethod 78 def get_dark_mode(cls, settings: Settings) -> bool: 79 """ 80 Determine dark mode by checking settings or detecting the system theme. 81 """ 82 if settings.theme == "Dark": 83 return True 84 if settings.theme == "Light": 85 return False 86 return darkdetect.isDark() 87 88 @classmethod 89 def init_theme(cls, root: tk.Tk, settings: Settings) -> None: 90 """ 91 Initialize theme and styles for the application. 92 """ 93 azure.init_theme(root) # pylint: disable=W0212 94 cls.update_by_dark_mode(root, settings) 95 cls.init_fonts(settings) 96 root.option_add("*TCombobox*Listbox.font", "TkDefaultFont") 97 root.option_add("*tearOff", False) # Fix menus 98 root.bind_class("Menu", "<<ThemeChanged>>", cls.update_menu) 99 100 @classmethod 101 def init_fonts(cls, settings: Settings) -> None: 102 """ 103 Initialize the fonts to be used. 104 105 The following tk named fonts are updated based on Settings: 106 107 * TkDefaultFont - set to Regular Font from Settings 108 * TkTextFont - set to Regular Font from Settings 109 * TkMenuFont - set to Regular Font from Settings 110 * TkFixedFont - set to Fixed Font from Settings 111 112 In addition, widgets are configured to use TkDefaultFont. 113 """ 114 base_font = font.nametofont("TkDefaultFont") 115 text_font = font.nametofont("TkTextFont") 116 menu_font = font.nametofont("TkMenuFont") 117 fixed_font = font.nametofont("TkFixedFont") 118 if settings.regular_font.name: 119 settings.regular_font.configure_font(base_font) 120 settings.regular_font.configure_font(text_font) 121 settings.regular_font.configure_font(menu_font) 122 else: 123 base_font.configure(family=font_utils.MAIN_FONT_FAMILY, size=12) 124 text_font.configure(family=font_utils.MAIN_FONT_FAMILY, size=12) 125 menu_font.configure(family=font_utils.MAIN_FONT_FAMILY, size=12) 126 if settings.fixed_font.name: 127 settings.fixed_font.configure_font(fixed_font) 128 else: 129 fixed_font.configure(family=font_utils.FIXED_FONT_FAMILY, size=12) 130 style = ttk.Style() 131 style.configure("TButton", font="TkDefaultFont") 132 style.configure("TRadiobutton", font="TkDefaultFont") 133 style.configure("TLabelFrame", font="TkDefaultFont") 134 style.configure("TNotebook", font="TkDefaultFont") 135 style.configure("TNotebook.Tab", font="TkDefaultFont") 136 style.configure("Switch.TCheckbutton", font="TkDefaultFont") 137 style.configure("System.TLabel", font="TkDefaultFont") 138 style.configure("URL.TLabel", foreground="#0066cc") 139 140 @classmethod 141 def update_by_dark_mode(cls, root: tk.Tk, settings: Settings) -> None: 142 """ 143 Update styles based on dark mode. 144 145 Colors are updated for various styles and widgets: 146 147 * Safe.TLabel style (green) 148 * Warn.TLabel style (yellow) 149 * Alert.TLabel style (red) 150 * Combobox Listbox background (grey) 151 """ 152 dark_mode = cls.get_dark_mode(settings) 153 azure.set_theme("dark" if dark_mode else "light", root) 154 root.event_generate("<<ThemeChanged>>") 155 style = ttk.Style() 156 if dark_mode: 157 style.configure("Safe.TLabel", foreground="#00aa00") 158 style.configure("Warn.TLabel", foreground="#ffff22") 159 style.configure("Alert.TLabel", foreground="#ff2222") 160 root.option_add("*TCombobox*Listbox.background", "#444444") 161 else: 162 style.configure("Safe.TLabel", foreground="#009900") 163 style.configure("Warn.TLabel", foreground="#aaaa00") 164 style.configure("Alert.TLabel", foreground="#cc0000") 165 root.option_add("*TCombobox*Listbox.background", "#dddddd") 166 # ComboboxPopdownFrame must be reset every time the theme changes 167 style.configure("ComboboxPopdownFrame", relief=tk.FLAT) 168 169 @classmethod 170 def test_dark_mode(cls, trueval: T, falseval: T) -> T: 171 """ 172 If currently in dark mode, return `trueval`; otherwise return `falseval`. 173 """ 174 style = ttk.Style() 175 background = style.lookup("TLabel", "background") 176 if is_dark(f"{background}"): 177 return trueval 178 return falseval 179 180 @classmethod 181 def update_menu(cls, event: tk.Event) -> None: 182 """ 183 Update the foreground and background colors of a menu, based on dark mode. 184 """ 185 if isinstance(event.widget, tk.Menu): 186 event.widget.configure( 187 background=cls.get_menu_background(), 188 foreground=cls.get_menu_foreground() 189 ) 190 191 @classmethod 192 def get_menu_background(cls) -> str: 193 """ 194 Get the background color for menus, based on dark mode. 195 """ 196 return cls.test_dark_mode("#444444", "#dddddd") 197 198 @classmethod 199 def get_menu_foreground(cls) -> str: 200 """ 201 Get the foreground color for menus, based on dark mode. 202 """ 203 return cls.test_dark_mode("#ffffff", "#000000") 204 205 @classmethod 206 def get_base_font(cls) -> Font: 207 """ 208 Get the base font for use in the application. Returns `TkDefaultFont`. 209 """ 210 return font.nametofont("TkDefaultFont") 211 212 @classmethod 213 def get_large_font(cls) -> Font: 214 """ 215 Get the large font for use in the application. 216 217 Returns a modified `TkDefaultFont` with font size increased by 4. 218 """ 219 return font_utils.modify_named_font( 220 "TkDefaultFont", size=cls.get_base_font().actual()["size"]+4 221 ) 222 223 @classmethod 224 def get_small_font(cls) -> Font: 225 """ 226 Get the small font for use in the application. 227 228 Returns a modified `TkDefaultFont` with font size decreased by 2. 229 """ 230 return font_utils.modify_named_font( 231 "TkDefaultFont", size=cls.get_base_font().actual()["size"]-2 232 ) 233 234 @classmethod 235 def get_bold_font(cls) -> Font: 236 """ 237 Get the bold font for use in the application. 238 239 Returns a modified `TkDefaultFont` with font weight set to bold. 240 """ 241 return font_utils.modify_named_font("TkDefaultFont", weight="bold") 242 243 @classmethod 244 def get_fixed_font(cls) -> Font: 245 """ 246 Get the monospace font for use in the application. Returns `TkFixedFont`. 247 """ 248 return font.nametofont("TkFixedFont")
Tk Style Manager.
77 @classmethod 78 def get_dark_mode(cls, settings: Settings) -> bool: 79 """ 80 Determine dark mode by checking settings or detecting the system theme. 81 """ 82 if settings.theme == "Dark": 83 return True 84 if settings.theme == "Light": 85 return False 86 return darkdetect.isDark()
Determine dark mode by checking settings or detecting the system theme.
88 @classmethod 89 def init_theme(cls, root: tk.Tk, settings: Settings) -> None: 90 """ 91 Initialize theme and styles for the application. 92 """ 93 azure.init_theme(root) # pylint: disable=W0212 94 cls.update_by_dark_mode(root, settings) 95 cls.init_fonts(settings) 96 root.option_add("*TCombobox*Listbox.font", "TkDefaultFont") 97 root.option_add("*tearOff", False) # Fix menus 98 root.bind_class("Menu", "<<ThemeChanged>>", cls.update_menu)
Initialize theme and styles for the application.
100 @classmethod 101 def init_fonts(cls, settings: Settings) -> None: 102 """ 103 Initialize the fonts to be used. 104 105 The following tk named fonts are updated based on Settings: 106 107 * TkDefaultFont - set to Regular Font from Settings 108 * TkTextFont - set to Regular Font from Settings 109 * TkMenuFont - set to Regular Font from Settings 110 * TkFixedFont - set to Fixed Font from Settings 111 112 In addition, widgets are configured to use TkDefaultFont. 113 """ 114 base_font = font.nametofont("TkDefaultFont") 115 text_font = font.nametofont("TkTextFont") 116 menu_font = font.nametofont("TkMenuFont") 117 fixed_font = font.nametofont("TkFixedFont") 118 if settings.regular_font.name: 119 settings.regular_font.configure_font(base_font) 120 settings.regular_font.configure_font(text_font) 121 settings.regular_font.configure_font(menu_font) 122 else: 123 base_font.configure(family=font_utils.MAIN_FONT_FAMILY, size=12) 124 text_font.configure(family=font_utils.MAIN_FONT_FAMILY, size=12) 125 menu_font.configure(family=font_utils.MAIN_FONT_FAMILY, size=12) 126 if settings.fixed_font.name: 127 settings.fixed_font.configure_font(fixed_font) 128 else: 129 fixed_font.configure(family=font_utils.FIXED_FONT_FAMILY, size=12) 130 style = ttk.Style() 131 style.configure("TButton", font="TkDefaultFont") 132 style.configure("TRadiobutton", font="TkDefaultFont") 133 style.configure("TLabelFrame", font="TkDefaultFont") 134 style.configure("TNotebook", font="TkDefaultFont") 135 style.configure("TNotebook.Tab", font="TkDefaultFont") 136 style.configure("Switch.TCheckbutton", font="TkDefaultFont") 137 style.configure("System.TLabel", font="TkDefaultFont") 138 style.configure("URL.TLabel", foreground="#0066cc")
Initialize the fonts to be used.
The following tk named fonts are updated based on Settings:
- TkDefaultFont - set to Regular Font from Settings
- TkTextFont - set to Regular Font from Settings
- TkMenuFont - set to Regular Font from Settings
- TkFixedFont - set to Fixed Font from Settings
In addition, widgets are configured to use TkDefaultFont.
140 @classmethod 141 def update_by_dark_mode(cls, root: tk.Tk, settings: Settings) -> None: 142 """ 143 Update styles based on dark mode. 144 145 Colors are updated for various styles and widgets: 146 147 * Safe.TLabel style (green) 148 * Warn.TLabel style (yellow) 149 * Alert.TLabel style (red) 150 * Combobox Listbox background (grey) 151 """ 152 dark_mode = cls.get_dark_mode(settings) 153 azure.set_theme("dark" if dark_mode else "light", root) 154 root.event_generate("<<ThemeChanged>>") 155 style = ttk.Style() 156 if dark_mode: 157 style.configure("Safe.TLabel", foreground="#00aa00") 158 style.configure("Warn.TLabel", foreground="#ffff22") 159 style.configure("Alert.TLabel", foreground="#ff2222") 160 root.option_add("*TCombobox*Listbox.background", "#444444") 161 else: 162 style.configure("Safe.TLabel", foreground="#009900") 163 style.configure("Warn.TLabel", foreground="#aaaa00") 164 style.configure("Alert.TLabel", foreground="#cc0000") 165 root.option_add("*TCombobox*Listbox.background", "#dddddd") 166 # ComboboxPopdownFrame must be reset every time the theme changes 167 style.configure("ComboboxPopdownFrame", relief=tk.FLAT)
Update styles based on dark mode.
Colors are updated for various styles and widgets:
- Safe.TLabel style (green)
- Warn.TLabel style (yellow)
- Alert.TLabel style (red)
- Combobox Listbox background (grey)
169 @classmethod 170 def test_dark_mode(cls, trueval: T, falseval: T) -> T: 171 """ 172 If currently in dark mode, return `trueval`; otherwise return `falseval`. 173 """ 174 style = ttk.Style() 175 background = style.lookup("TLabel", "background") 176 if is_dark(f"{background}"): 177 return trueval 178 return falseval
If currently in dark mode, return trueval; otherwise return falseval.
205 @classmethod 206 def get_base_font(cls) -> Font: 207 """ 208 Get the base font for use in the application. Returns `TkDefaultFont`. 209 """ 210 return font.nametofont("TkDefaultFont")
Get the base font for use in the application. Returns TkDefaultFont.
212 @classmethod 213 def get_large_font(cls) -> Font: 214 """ 215 Get the large font for use in the application. 216 217 Returns a modified `TkDefaultFont` with font size increased by 4. 218 """ 219 return font_utils.modify_named_font( 220 "TkDefaultFont", size=cls.get_base_font().actual()["size"]+4 221 )
Get the large font for use in the application.
Returns a modified TkDefaultFont with font size increased by 4.
223 @classmethod 224 def get_small_font(cls) -> Font: 225 """ 226 Get the small font for use in the application. 227 228 Returns a modified `TkDefaultFont` with font size decreased by 2. 229 """ 230 return font_utils.modify_named_font( 231 "TkDefaultFont", size=cls.get_base_font().actual()["size"]-2 232 )
Get the small font for use in the application.
Returns a modified TkDefaultFont with font size decreased by 2.
234 @classmethod 235 def get_bold_font(cls) -> Font: 236 """ 237 Get the bold font for use in the application. 238 239 Returns a modified `TkDefaultFont` with font weight set to bold. 240 """ 241 return font_utils.modify_named_font("TkDefaultFont", weight="bold")
Get the bold font for use in the application.
Returns a modified TkDefaultFont with font weight set to bold.
243 @classmethod 244 def get_fixed_font(cls) -> Font: 245 """ 246 Get the monospace font for use in the application. Returns `TkFixedFont`. 247 """ 248 return font.nametofont("TkFixedFont")
Get the monospace font for use in the application. Returns TkFixedFont.