From 62e285b52eb7a08770b6f96158bc2062b76c007b Mon Sep 17 00:00:00 2001 From: Athanasius Date: Mon, 5 Dec 2022 11:52:29 +0000 Subject: [PATCH 1/3] themes: Use defined constants for which theme throughout This has been relying on knowledge of the magic numbers for far too long. As part of this, remove all the obfuscating "oh, default is 0, and we want that or any other theme, so treat this like a boolean" nonsense. Also, stop assuming that "> 1" is a synonym for "transparent theme". Just Do The Equality Check. --- EDMarketConnector.py | 10 ++-- prefs.py | 15 ++++-- theme.py | 107 +++++++++++++++++++++++++++++-------------- 3 files changed, 87 insertions(+), 45 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 60e9f546..74ce8ace 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -1799,18 +1799,14 @@ class AppWindow(object): def onenter(self, event=None) -> None: """Handle when our window gains focus.""" - # TODO: This assumes that 1) transparent is at least 2, 2) there are - # no new themes added after that. - if config.get_int('theme') > 1: + if config.get_int('theme') == theme.THEME_TRANSPARENT: self.w.attributes("-transparentcolor", '') self.blank_menubar.grid_remove() self.theme_menubar.grid(row=0, columnspan=2, sticky=tk.NSEW) def onleave(self, event=None) -> None: """Handle when our window loses focus.""" - # TODO: This assumes that 1) transparent is at least 2, 2) there are - # no new themes added after that. - if config.get_int('theme') > 1 and event.widget == self.w: + if config.get_int('theme') == theme.THEME_TRANSPARENT and event.widget == self.w: self.w.attributes("-transparentcolor", 'grey4') self.theme_menubar.grid_remove() self.blank_menubar.grid(row=0, columnspan=2, sticky=tk.NSEW) @@ -1887,7 +1883,7 @@ sys.path: {sys.path}''' ) if args.reset_ui: - config.set('theme', 0) # 'Default' theme uses ID 0 + config.set('theme', theme.THEME_DEFAULT) config.set('ui_transparency', 100) # 100 is completely opaque config.delete('font', suppress=True) config.delete('font_size', suppress=True) diff --git a/prefs.py b/prefs.py index 376b6c67..985ef581 100644 --- a/prefs.py +++ b/prefs.py @@ -308,6 +308,7 @@ class PreferencesDialog(tk.Toplevel): button.bind("", lambda event: self.apply()) self.protocol("WM_DELETE_WINDOW", self._destroy) + # FIXME: Why are these being called when *creating* the Settings window? # Selectively disable buttons depending on output settings self.cmdrchanged() self.themevarchanged() @@ -733,13 +734,14 @@ class PreferencesDialog(tk.Toplevel): # Appearance theme and language setting nb.Radiobutton( # LANG: Label for 'Default' theme radio button - appearance_frame, text=_('Default'), variable=self.theme, value=0, command=self.themevarchanged + appearance_frame, text=_('Default'), variable=self.theme, + value=theme.THEME_DEFAULT, command=self.themevarchanged ).grid(columnspan=3, padx=self.BUTTONX, sticky=tk.W, row=row.get()) # Appearance theme setting nb.Radiobutton( # LANG: Label for 'Dark' theme radio button - appearance_frame, text=_('Dark'), variable=self.theme, value=1, command=self.themevarchanged + appearance_frame, text=_('Dark'), variable=self.theme, value=theme.THEME_DARK, command=self.themevarchanged ).grid(columnspan=3, padx=self.BUTTONX, sticky=tk.W, row=row.get()) if sys.platform == 'win32': @@ -748,7 +750,7 @@ class PreferencesDialog(tk.Toplevel): # LANG: Label for 'Transparent' theme radio button text=_('Transparent'), # Appearance theme setting variable=self.theme, - value=2, + value=theme.THEME_TRANSPARENT, command=self.themevarchanged ).grid(columnspan=3, padx=self.BUTTONX, sticky=tk.W, row=row.get()) @@ -1149,7 +1151,12 @@ class PreferencesDialog(tk.Toplevel): """Update theme examples.""" self.theme_button_0['foreground'], self.theme_button_1['foreground'] = self.theme_colors - state = tk.NORMAL if self.theme.get() else tk.DISABLED + if self.theme.get() == theme.THEME_DEFAULT: + state = tk.DISABLED # type: ignore + + else: + state = tk.NORMAL # type: ignore + self.theme_label_0['state'] = state self.theme_label_1['state'] = state self.theme_button_0['state'] = state diff --git a/theme.py b/theme.py index 2707ad42..503f2dc0 100644 --- a/theme.py +++ b/theme.py @@ -259,24 +259,7 @@ class _Theme(object): if not config.get_str('dark_highlight'): config.set('dark_highlight', 'white') - if theme: - # Dark - (r, g, b) = root.winfo_rgb(config.get_str('dark_text')) - self.current = { - 'background': 'grey4', # OSX inactive dark titlebar color - 'foreground': config.get_str('dark_text'), - 'activebackground': config.get_str('dark_text'), - 'activeforeground': 'grey4', - 'disabledforeground': f'#{int(r/384):02x}{int(g/384):02x}{int(b/384):02x}', - 'highlight': config.get_str('dark_highlight'), - # Font only supports Latin 1 / Supplement / Extended, and a - # few General Punctuation and Mathematical Operators - # LANG: Label for commander name in main window - 'font': (theme > 1 and not 0x250 < ord(_('Cmdr')[0]) < 0x3000 and - tk_font.Font(family='Euro Caps', size=10, weight=tk_font.NORMAL) or - 'TkDefaultFont'), - } - else: + if theme == self.THEME_DEFAULT: # (Mostly) system colors style = ttk.Style() self.current = { @@ -292,9 +275,30 @@ class _Theme(object): 'font': 'TkDefaultFont', } - # Apply current theme to a widget and its children, and register it for future updates + else: # Dark *or* Transparent + (r, g, b) = root.winfo_rgb(config.get_str('dark_text')) + self.current = { + 'background': 'grey4', # OSX inactive dark titlebar color + 'foreground': config.get_str('dark_text'), + 'activebackground': config.get_str('dark_text'), + 'activeforeground': 'grey4', + 'disabledforeground': f'#{int(r/384):02x}{int(g/384):02x}{int(b/384):02x}', + 'highlight': config.get_str('dark_highlight'), + # Font only supports Latin 1 / Supplement / Extended, and a + # few General Punctuation and Mathematical Operators + # LANG: Label for commander name in main window + 'font': (theme > 1 and not 0x250 < ord(_('Cmdr')[0]) < 0x3000 and + tk_font.Font(family='Euro Caps', size=10, weight=tk_font.NORMAL) or + 'TkDefaultFont'), + } def update(self, widget: tk.Widget) -> None: + """ + Apply current theme to a widget and its children. + + Also, register it for future updates. + :param widget: Target widget. + """ assert isinstance(widget, tk.Widget) or isinstance(widget, tk.BitmapImage), widget if not self.current: return # No need to call this for widgets created in plugin_app() @@ -375,8 +379,7 @@ class _Theme(object): # Apply configured theme - def apply(self, root: tk.Tk) -> None: # noqa: CCR001 - + def apply(self, root: tk.Tk) -> None: # noqa: CCR001, C901 theme = config.get_int('theme') self._colors(root, theme) @@ -392,25 +395,31 @@ class _Theme(object): for widget in pair: widget.grid_remove() if isinstance(pair[0], tk.Menu): - if theme: + if theme == self.THEME_DEFAULT: + root['menu'] = pair[0] + + else: # Dark *or* Transparent root['menu'] = '' pair[theme].grid(**gridopts) - else: - root['menu'] = pair[0] + else: pair[theme].grid(**gridopts) if self.active == theme: return # Don't need to mess with the window manager + else: self.active = theme if sys.platform == 'darwin': from AppKit import NSAppearance, NSApplication, NSMiniaturizableWindowMask, NSResizableWindowMask root.update_idletasks() # need main window to be created - appearance = NSAppearance.appearanceNamed_(theme and - 'NSAppearanceNameDarkAqua' or - 'NSAppearanceNameAqua') + if theme == self.THEME_DEFAULT: + appearance = NSAppearance.appearanceNamed_('NSAppearanceNameAqua') + + else: # Dark (Transparent only on win32) + appearance = NSAppearance.appearanceNamed_('NSAppearanceNameDarkAqua') + for window in NSApplication.sharedApplication().windows(): window.setStyleMask_(window.styleMask() & ~( NSMiniaturizableWindowMask | NSResizableWindowMask)) # disable zoom @@ -426,14 +435,30 @@ class _Theme(object): GetWindowLongW = ctypes.windll.user32.GetWindowLongW # noqa: N806 # ctypes SetWindowLongW = ctypes.windll.user32.SetWindowLongW # noqa: N806 # ctypes - root.overrideredirect(theme and True or False) - root.attributes("-transparentcolor", theme > 1 and 'grey4' or '') + # FIXME: Lose the "treat this like a boolean" bullshit + if theme == self.THEME_DEFAULT: + root.overrideredirect(False) + + else: + root.overrideredirect(True) + + if theme == self.THEME_TRANSPARENT: + root.attributes("-transparentcolor", 'grey4') + + else: + root.attributes("-transparentcolor", '') + root.withdraw() root.update_idletasks() # Size and windows styles get recalculated here hwnd = ctypes.windll.user32.GetParent(root.winfo_id()) SetWindowLongW(hwnd, GWL_STYLE, GetWindowLongW(hwnd, GWL_STYLE) & ~WS_MAXIMIZEBOX) # disable maximize - SetWindowLongW(hwnd, GWL_EXSTYLE, theme > 1 and WS_EX_APPWINDOW | - WS_EX_LAYERED or WS_EX_APPWINDOW) # Add to taskbar + + if theme == self.THEME_TRANSPARENT: + SetWindowLongW(hwnd, GWL_EXSTYLE, WS_EX_APPWINDOW | WS_EX_LAYERED) # Add to taskbar + + else: + SetWindowLongW(hwnd, GWL_EXSTYLE, WS_EX_APPWINDOW) # Add to taskbar + root.deiconify() root.wait_visibility() # need main window to be displayed before returning @@ -446,11 +471,25 @@ class _Theme(object): children = Window() nchildren = c_uint() XQueryTree(dpy, root.winfo_id(), byref(xroot), byref(parent), byref(children), byref(nchildren)) - XChangeProperty(dpy, parent, motif_wm_hints_property, motif_wm_hints_property, 32, - PropModeReplace, theme and motif_wm_hints_dark or motif_wm_hints_normal, 5) + if theme == self.THEME_DEFAULT: + wm_hints = motif_wm_hints_normal + + else: # Dark *or* Transparent + wm_hints = motif_wm_hints_dark + + XChangeProperty( + dpy, parent, motif_wm_hints_property, motif_wm_hints_property, 32, PropModeReplace, wm_hints, 5 + ) + XFlush(dpy) + else: - root.overrideredirect(theme and 1 or 0) + if theme == self.THEME_DEFAULT: + root.overrideredirect(False) + + else: # Dark *or* Transparent + root.overrideredirect(True) + root.deiconify() root.wait_visibility() # need main window to be displayed before returning From 09d632bb34d00bd72c046ff365be1be703b7dac3 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Mon, 5 Dec 2022 12:10:30 +0000 Subject: [PATCH 2/3] theme.py: Fix up some typing issues We're passing around *either* tk.Widget *or* tk.BitmapImage in places, but tk.BitmapImage really isn't the same. It doesn't have some functions, can't be treated as a Dict etc. --- theme.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/theme.py b/theme.py index 503f2dc0..e06ce27b 100644 --- a/theme.py +++ b/theme.py @@ -133,14 +133,14 @@ class _Theme(object): def __init__(self) -> None: self.active = None # Starts out with no theme self.minwidth: Optional[int] = None - self.widgets: Dict[tk.Widget, Set] = {} + self.widgets: Dict[tk.Widget | tk.BitmapImage, Set] = {} self.widgets_pair: List = [] self.defaults: Dict = {} self.current: Dict = {} self.default_ui_scale = None # None == not yet known self.startup_ui_scale = None - def register(self, widget: tk.Widget) -> None: # noqa: CCR001, C901 + def register(self, widget: tk.Widget | tk.BitmapImage) -> None: # noqa: CCR001, C901 # Note widget and children for later application of a theme. Note if # the widget has explicit fg or bg attributes. assert isinstance(widget, tk.Widget) or isinstance(widget, tk.BitmapImage), widget @@ -310,9 +310,19 @@ class _Theme(object): self._update_widget(child) # Apply current theme to a single widget - def _update_widget(self, widget: tk.Widget) -> None: # noqa: CCR001, C901 + def _update_widget(self, widget: tk.Widget | tk.BitmapImage) -> None: # noqa: CCR001, C901 if widget not in self.widgets: - assert_str = f'{widget.winfo_class()} {widget} "{"text" in widget.keys() and widget["text"]}"' + if type(widget) == tk.Widget: + w_class = widget.winfo_class() + w_keys: List[str] = widget.keys() + + else: + # There is no tk.BitmapImage.winfo_class() + w_class = '' + # There is no tk.BitmapImage.keys() + w_keys = [] + + assert_str = f'{w_class} {widget} "{"text" in w_keys and widget["text"]}"' raise AssertionError(assert_str) attribs: Set = self.widgets.get(widget, set()) From 45043d9359abae25d8d17ef1d7765c6d3837e644 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Mon, 5 Dec 2022 12:20:39 +0000 Subject: [PATCH 3/3] theme.py: Some more tk.BitmapImage guarding, but using isinstance() `type(...)` is probably incorrect, and `isinstance(...)` gets mypy to take account of subclasses properly. --- theme.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/theme.py b/theme.py index e06ce27b..e9ace26f 100644 --- a/theme.py +++ b/theme.py @@ -312,7 +312,7 @@ class _Theme(object): # Apply current theme to a single widget def _update_widget(self, widget: tk.Widget | tk.BitmapImage) -> None: # noqa: CCR001, C901 if widget not in self.widgets: - if type(widget) == tk.Widget: + if isinstance(widget, tk.Widget): w_class = widget.winfo_class() w_keys: List[str] = widget.keys() @@ -403,7 +403,9 @@ class _Theme(object): # Switch menus for pair, gridopts in self.widgets_pair: for widget in pair: - widget.grid_remove() + if isinstance(widget, tk.Widget): + widget.grid_remove() + if isinstance(pair[0], tk.Menu): if theme == self.THEME_DEFAULT: root['menu'] = pair[0]