From 49ddefed47313ff6ae6ceb72143577ed789bd587 Mon Sep 17 00:00:00 2001 From: zwimer Date: Wed, 30 Nov 2022 19:10:38 -0700 Subject: [PATCH 01/11] Add system theme support --- angrmanagement/ui/dialogs/preferences.py | 30 ++--- angrmanagement/ui/main_window.py | 2 + angrmanagement/ui/theme.py | 138 +++++++++++++++++++++++ setup.cfg | 1 + 4 files changed, 150 insertions(+), 21 deletions(-) create mode 100644 angrmanagement/ui/theme.py diff --git a/angrmanagement/ui/dialogs/preferences.py b/angrmanagement/ui/dialogs/preferences.py index 6096b5c0f..4d76985d0 100644 --- a/angrmanagement/ui/dialogs/preferences.py +++ b/angrmanagement/ui/dialogs/preferences.py @@ -4,13 +4,11 @@ QSizePolicy, QDialogButtonBox from PySide6.QtCore import QSize -from ..widgets.qcolor_option import QColorOption -from ...config.config_manager import ENTRIES -from ...config.color_schemes import COLOR_SCHEMES -from ...config import Conf, save_config -from ...logic.url_scheme import AngrUrlScheme -from ..css import refresh_theme - +from angrmanagement.ui.widgets.qcolor_option import QColorOption +from angrmanagement.logic.url_scheme import AngrUrlScheme +from angrmanagement.config import Conf, save_config +from angrmanagement.config.config_manager import ENTRIES +from angrmanagement.ui.theme import Theme class Page(QWidget): """ @@ -93,8 +91,8 @@ class ThemeAndColors(Page): def __init__(self, parent=None): super().__init__(parent=parent) + self._theme = Theme.get() - self._to_save = {} self._schemes_combo: QComboBox = None self._init_widgets() @@ -109,7 +107,7 @@ def _init_widgets(self): self._schemes_combo = QComboBox(self) current_theme_idx = 0 - for idx, name in enumerate(["Current"] + list(sorted(COLOR_SCHEMES))): + for idx, name in enumerate(self._theme.themes): if name == Conf.theme_name: current_theme_idx = idx self._schemes_combo.addItem(name) @@ -127,7 +125,6 @@ def _init_widgets(self): continue row = QColorOption(getattr(Conf, ce.name), ce.name) edit_colors_layout.addWidget(row) - self._to_save[ce.name] = (ce, row) frame = QFrame() frame.setLayout(edit_colors_layout) @@ -142,21 +139,12 @@ def _init_widgets(self): self.setLayout(page_layout) - def _load_color_scheme(self, name): - for prop, value in COLOR_SCHEMES[name].items(): - row = self._to_save[prop][1] - row.set_color(value) - def _on_load_scheme_clicked(self): - self._load_color_scheme(self._schemes_combo.currentText()) + self._theme.set(self._schemes_combo.currentText()) self.save_config() def save_config(self): - # pylint: disable=assigning-non-slot - Conf.theme_name = self._schemes_combo.currentText() - for ce, row in self._to_save.values(): - setattr(Conf, ce.name, row.color.am_obj) - refresh_theme() + self._theme.update_config_cache() class Preferences(QDialog): diff --git a/angrmanagement/ui/main_window.py b/angrmanagement/ui/main_window.py index d7a0c5527..53ac34ca6 100644 --- a/angrmanagement/ui/main_window.py +++ b/angrmanagement/ui/main_window.py @@ -52,6 +52,7 @@ from .dialogs.preferences import Preferences from .toolbars import FileToolbar, DebugToolbar from .toolbar_manager import ToolbarManager +from .theme import Theme if TYPE_CHECKING: from PySide6.QtWidgets import QApplication @@ -111,6 +112,7 @@ def __init__(self, app: Optional['QApplication'] = None, parent=None, show=True, self._run_daemon(use_daemon=use_daemon) + self._theme = Theme.create(self) # I'm ready to show off! if show: self.showMaximized() diff --git a/angrmanagement/ui/theme.py b/angrmanagement/ui/theme.py new file mode 100644 index 000000000..197f4918a --- /dev/null +++ b/angrmanagement/ui/theme.py @@ -0,0 +1,138 @@ +from typing import Optional +import logging + +import darkdetect + +from PySide6.QtGui import QColor +from PySide6.QtCore import QTimer + +from angrmanagement.config.color_schemes import COLOR_SCHEMES +from angrmanagement.config.config_manager import ENTRIES +from angrmanagement.ui.widgets.qcolor_option import QColorOption +from angrmanagement.ui.css import refresh_theme + +from angrmanagement.config import Conf + + +_l = logging.getLogger(__name__) + + +class Theme: + """ + A singleton global theme class + """ + _object: Optional["Theme"] = None + _system: str = "System" + + # + # Public methods + # + + @classmethod + def create(cls, parent: Optional["QObject"]): + """ + Create the singleton global theme object + """ + if cls._object is not None: + raise RuntimeError(f"Refusing to create a second {cls.__name__}") + cls._object = cls(parent, _caller=cls.create) + return cls._object + + @classmethod + def get(cls): + """ + Get the singleton global theme object + """ + if cls._object is None: + raise RuntimeError(f"No existing {cls.__name__}") + return cls._object + + @property + def themes(self): + """ + The supported themes + """ + return [self._system] + list(sorted(COLOR_SCHEMES)) + + def current(self) -> Optional[str]: + """ + Get the current theme + """ + return self._system if self._tracking else self._underlying + + def set(self, name): + """ + Set the theme + """ + _l.debug("Setting theme to: ", name) + if name == self._system: + self._set_system() + self._system_tracking(True) + else: + self._system_tracking(False) + self._set_underlying(name) + self._underlying = name + + def update_config_cache(self): + """ + Edit the cached config to reflect the theme changes; does not save the config! + """ + + # + # Private methods + # + + def __init__(self, parent, *, _caller=None): + """ + This method is not public + """ + if _caller != self.create: + raise RuntimeError("Use .create(parent) or .get(); this is a singleton") + # For config file + self._to_save = {} + for ce in ENTRIES: + if ce.type_ is not QColor: + continue + row = QColorOption(getattr(Conf, ce.name), ce.name) + self._to_save[ce.name] = (ce, row) + # Init + self._underlying: Optional[str] = None + self._tracking: bool = False + self._timer = QTimer(parent) + self._timer.start(50) + # Load default theme + self.set(Conf.theme_name) + + def _set_underlying(self, name): + """ + Set the underling theme to 'name' + """ + _l.debug("Underling color theme set to: ", name) + self._underlying = name + for prop, value in COLOR_SCHEMES[name].items(): + row = self._to_save[prop][1] + row.set_color(value) + Conf.theme_name = self.current() + for ce, row in self._to_save.values(): + setattr(Conf, ce.name, row.color.am_obj) + refresh_theme() + + def _set_system(self): + """ + Set the underlying theme according to the system theme if needed + """ + new: str = darkdetect.theme() + if new != self._underlying: + self._set_underlying(new) + + def _system_tracking(self, enabled: bool): + """ + Connect system tracking slots as needed + """ + if enabled == self._tracking: + return + self._tracking = enabled + if enabled: + self._timer.timeout.connect(self._set_system) + else: + self._timer.timeout.disconnect(self._set_system) diff --git a/setup.cfg b/setup.cfg index a52c11ee0..95720d847 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,7 @@ install_requires = qtterm getmac QtAwesome + darkdetect pyobjc-framework-Cocoa; platform_system == "Darwin" python_requires = >= 3.8 From 6a05e7b4832aba29270b925b9d4825c8966268c4 Mon Sep 17 00:00:00 2001 From: zwimer Date: Wed, 30 Nov 2022 21:20:07 -0700 Subject: [PATCH 02/11] Bug fixes --- angrmanagement/config/config_manager.py | 1 + angrmanagement/ui/dialogs/preferences.py | 44 +++++-- angrmanagement/ui/main_window.py | 9 +- angrmanagement/ui/theme.py | 138 --------------------- angrmanagement/utils/track_system_theme.py | 102 +++++++++++++++ 5 files changed, 145 insertions(+), 149 deletions(-) delete mode 100644 angrmanagement/ui/theme.py create mode 100644 angrmanagement/utils/track_system_theme.py diff --git a/angrmanagement/config/config_manager.py b/angrmanagement/config/config_manager.py index 0e5b83336..0b12f349b 100644 --- a/angrmanagement/config/config_manager.py +++ b/angrmanagement/config/config_manager.py @@ -114,6 +114,7 @@ def bool_serializer(config_option, value: bool) -> str: CE('code_font', QFont, QFont("Source Code Pro", 10)), CE('theme_name', str, "Light"), + CE('theme_track_system', bool, True), CE('disasm_view_minimap_viewport_color', QColor, QColor(0xFF, 0x00, 0x00)), CE('disasm_view_operand_color', QColor, QColor(0x00, 0x00, 0x80)), CE('disasm_view_operand_constant_color', QColor, QColor(0x00, 0x00, 0x80)), diff --git a/angrmanagement/ui/dialogs/preferences.py b/angrmanagement/ui/dialogs/preferences.py index 4d76985d0..75e78c5b8 100644 --- a/angrmanagement/ui/dialogs/preferences.py +++ b/angrmanagement/ui/dialogs/preferences.py @@ -2,13 +2,16 @@ from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QListWidget, QListView, QStackedWidget, QWidget, \ QGroupBox, QLabel, QCheckBox, QPushButton, QLineEdit, QListWidgetItem, QScrollArea, QFrame, QComboBox, \ QSizePolicy, QDialogButtonBox -from PySide6.QtCore import QSize +from PySide6.QtCore import QSize, Qt -from angrmanagement.ui.widgets.qcolor_option import QColorOption -from angrmanagement.logic.url_scheme import AngrUrlScheme -from angrmanagement.config import Conf, save_config from angrmanagement.config.config_manager import ENTRIES -from angrmanagement.ui.theme import Theme +from angrmanagement.config.color_schemes import COLOR_SCHEMES +from angrmanagement.config import Conf, save_config +from angrmanagement.logic.url_scheme import AngrUrlScheme +from angrmanagement.ui.widgets.qcolor_option import QColorOption +from angrmanagement.ui.css import refresh_theme +from angrmanagement.utils.track_system_theme import TrackSystemTheme + class Page(QWidget): """ @@ -91,8 +94,9 @@ class ThemeAndColors(Page): def __init__(self, parent=None): super().__init__(parent=parent) - self._theme = Theme.get() + self._to_save = {} + self._auto = TrackSystemTheme.get() self._schemes_combo: QComboBox = None self._init_widgets() @@ -107,7 +111,7 @@ def _init_widgets(self): self._schemes_combo = QComboBox(self) current_theme_idx = 0 - for idx, name in enumerate(self._theme.themes): + for idx, name in enumerate(list(sorted(COLOR_SCHEMES))): if name == Conf.theme_name: current_theme_idx = idx self._schemes_combo.addItem(name) @@ -125,6 +129,7 @@ def _init_widgets(self): continue row = QColorOption(getattr(Conf, ce.name), ce.name) edit_colors_layout.addWidget(row) + self._to_save[ce.name] = (ce, row) frame = QFrame() frame.setLayout(edit_colors_layout) @@ -137,14 +142,35 @@ def _init_widgets(self): page_layout.addLayout(scroll_layout) + self._track_system = QCheckBox("Override: Track System Theme", self) + self._track_system.setCheckState(Qt.CheckState.Checked if self._auto.enabled() else Qt.CheckState.Unchecked) + self._track_system.stateChanged.connect(self._toggle_system_tracking) + page_layout.addWidget(self._track_system) + self.setLayout(page_layout) + def _toggle_system_tracking(self, state: int): + self._auto.set_enabled(state == Qt.CheckState.Checked.value) + if state == Qt.CheckState.Unchecked.value: + self._on_load_scheme_clicked() + self._on_load_scheme_clicked() + + def _load_color_scheme(self, name): + for prop, value in COLOR_SCHEMES[name].items(): + row = self._to_save[prop][1] + row.set_color(value) + def _on_load_scheme_clicked(self): - self._theme.set(self._schemes_combo.currentText()) + self._load_color_scheme(self._schemes_combo.currentText()) self.save_config() def save_config(self): - self._theme.update_config_cache() + # pylint: disable=assigning-non-slot + Conf.theme_name = self._schemes_combo.currentText() + for ce, row in self._to_save.values(): + setattr(Conf, ce.name, row.color.am_obj) + Conf.theme_track_system = self._auto.enabled() + self._auto.refresh_theme() class Preferences(QDialog): diff --git a/angrmanagement/ui/main_window.py b/angrmanagement/ui/main_window.py index 53ac34ca6..55b5c9683 100644 --- a/angrmanagement/ui/main_window.py +++ b/angrmanagement/ui/main_window.py @@ -38,6 +38,7 @@ from ..config import IMG_LOCATION, Conf, save_config from ..utils.io import isurl, download_url from ..utils.env import is_pyinstaller, app_root +from ..utils.track_system_theme import TrackSystemTheme from ..errors import InvalidURLError, UnexpectedStatusCodeError from .menus.file_menu import FileMenu from .menus.analyze_menu import AnalyzeMenu @@ -52,7 +53,6 @@ from .dialogs.preferences import Preferences from .toolbars import FileToolbar, DebugToolbar from .toolbar_manager import ToolbarManager -from .theme import Theme if TYPE_CHECKING: from PySide6.QtWidgets import QApplication @@ -112,7 +112,12 @@ def __init__(self, app: Optional['QApplication'] = None, parent=None, show=True, self._run_daemon(use_daemon=use_daemon) - self._theme = Theme.create(self) + # Allow system theme-ing + self._track_system_theme = TrackSystemTheme.create(self) + if Conf.theme_track_system: + self._track_system_theme.set_enabled(True) + self._track_system_theme.refresh_theme() + # I'm ready to show off! if show: self.showMaximized() diff --git a/angrmanagement/ui/theme.py b/angrmanagement/ui/theme.py deleted file mode 100644 index 197f4918a..000000000 --- a/angrmanagement/ui/theme.py +++ /dev/null @@ -1,138 +0,0 @@ -from typing import Optional -import logging - -import darkdetect - -from PySide6.QtGui import QColor -from PySide6.QtCore import QTimer - -from angrmanagement.config.color_schemes import COLOR_SCHEMES -from angrmanagement.config.config_manager import ENTRIES -from angrmanagement.ui.widgets.qcolor_option import QColorOption -from angrmanagement.ui.css import refresh_theme - -from angrmanagement.config import Conf - - -_l = logging.getLogger(__name__) - - -class Theme: - """ - A singleton global theme class - """ - _object: Optional["Theme"] = None - _system: str = "System" - - # - # Public methods - # - - @classmethod - def create(cls, parent: Optional["QObject"]): - """ - Create the singleton global theme object - """ - if cls._object is not None: - raise RuntimeError(f"Refusing to create a second {cls.__name__}") - cls._object = cls(parent, _caller=cls.create) - return cls._object - - @classmethod - def get(cls): - """ - Get the singleton global theme object - """ - if cls._object is None: - raise RuntimeError(f"No existing {cls.__name__}") - return cls._object - - @property - def themes(self): - """ - The supported themes - """ - return [self._system] + list(sorted(COLOR_SCHEMES)) - - def current(self) -> Optional[str]: - """ - Get the current theme - """ - return self._system if self._tracking else self._underlying - - def set(self, name): - """ - Set the theme - """ - _l.debug("Setting theme to: ", name) - if name == self._system: - self._set_system() - self._system_tracking(True) - else: - self._system_tracking(False) - self._set_underlying(name) - self._underlying = name - - def update_config_cache(self): - """ - Edit the cached config to reflect the theme changes; does not save the config! - """ - - # - # Private methods - # - - def __init__(self, parent, *, _caller=None): - """ - This method is not public - """ - if _caller != self.create: - raise RuntimeError("Use .create(parent) or .get(); this is a singleton") - # For config file - self._to_save = {} - for ce in ENTRIES: - if ce.type_ is not QColor: - continue - row = QColorOption(getattr(Conf, ce.name), ce.name) - self._to_save[ce.name] = (ce, row) - # Init - self._underlying: Optional[str] = None - self._tracking: bool = False - self._timer = QTimer(parent) - self._timer.start(50) - # Load default theme - self.set(Conf.theme_name) - - def _set_underlying(self, name): - """ - Set the underling theme to 'name' - """ - _l.debug("Underling color theme set to: ", name) - self._underlying = name - for prop, value in COLOR_SCHEMES[name].items(): - row = self._to_save[prop][1] - row.set_color(value) - Conf.theme_name = self.current() - for ce, row in self._to_save.values(): - setattr(Conf, ce.name, row.color.am_obj) - refresh_theme() - - def _set_system(self): - """ - Set the underlying theme according to the system theme if needed - """ - new: str = darkdetect.theme() - if new != self._underlying: - self._set_underlying(new) - - def _system_tracking(self, enabled: bool): - """ - Connect system tracking slots as needed - """ - if enabled == self._tracking: - return - self._tracking = enabled - if enabled: - self._timer.timeout.connect(self._set_system) - else: - self._timer.timeout.disconnect(self._set_system) diff --git a/angrmanagement/utils/track_system_theme.py b/angrmanagement/utils/track_system_theme.py new file mode 100644 index 000000000..eb4e3baa2 --- /dev/null +++ b/angrmanagement/utils/track_system_theme.py @@ -0,0 +1,102 @@ +from typing import Optional +import logging + +import darkdetect + +from PySide6.QtCore import QTimer + +from angrmanagement.config.color_schemes import COLOR_SCHEMES +from angrmanagement.ui.css import refresh_theme + +from angrmanagement.config import Conf + + +_l = logging.getLogger(__name__) + + +class TrackSystemTheme: + """ + A singleton global theme class + """ + _object: Optional["TrackSystemTheme"] = None + _system: str = "System" + + # + # Public methods + # + + @classmethod + def create(cls, parent: Optional["QObject"]): + """ + Create the singleton global theme object + """ + if cls._object is not None: + raise RuntimeError(f"Refusing to create a second {cls.__name__}") + cls._object = cls(parent, _caller=cls.create) + return cls._object + + @classmethod + def get(cls): + """ + Get the singleton global theme object + """ + if cls._object is None: + raise RuntimeError(f"No existing {cls.__name__}") + return cls._object + + def set_enabled(self, enabled: bool): + """ + Connect system tracking slots as needed + Note: This will not update the theme until the system state changes + """ + if enabled == self._enabled: + return + self._enabled = enabled + if enabled: + self._timer.timeout.connect(self._set_system) + else: + self._timer.timeout.disconnect(self._set_system) + + def enabled(self) -> bool: + """ + Return True iff system theme tracking is enabled + """ + return self._enabled + + def refresh_theme(self): + """ + Force a refresh of the theme + """ + if self._enabled: + self._set_system(force=True) + else: + refresh_theme() + + # + # Private methods + # + + def __init__(self, parent, *, _caller=None): + """ + This method is not public + """ + if _caller != self.create: + raise RuntimeError("Use .create(parent) or .get(); this is a singleton") + # Init + self._underlying: str = darkdetect.theme() + self._enabled: bool = False + self._timer = QTimer(parent) + self._timer.start(50) + + def _set_system(self, *, force: bool = False): + """ + Set the underlying theme according to the system theme if needed + """ + new: str = darkdetect.theme() + if force or new != self._underlying: + self._underlying = new + _l.debug("Underling color theme set to: ", new) + Conf.theme_name = self._system if self._enabled else self._underlying + for prop, value in COLOR_SCHEMES[new].items(): + setattr(Conf, prop, value) + refresh_theme() From 7dfc3ca13a0f9c0dafc8c95b89bfef4ce491ffb4 Mon Sep 17 00:00:00 2001 From: zwimer Date: Wed, 30 Nov 2022 21:30:36 -0700 Subject: [PATCH 03/11] Respect save button --- angrmanagement/ui/dialogs/preferences.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/angrmanagement/ui/dialogs/preferences.py b/angrmanagement/ui/dialogs/preferences.py index 75e78c5b8..2a9857c23 100644 --- a/angrmanagement/ui/dialogs/preferences.py +++ b/angrmanagement/ui/dialogs/preferences.py @@ -9,7 +9,6 @@ from angrmanagement.config import Conf, save_config from angrmanagement.logic.url_scheme import AngrUrlScheme from angrmanagement.ui.widgets.qcolor_option import QColorOption -from angrmanagement.ui.css import refresh_theme from angrmanagement.utils.track_system_theme import TrackSystemTheme @@ -21,6 +20,9 @@ class Page(QWidget): def save_config(self): raise NotImplementedError() + def revert_unsaved(self): + raise NotImplementedError() + NAME = NotImplemented @@ -84,6 +86,9 @@ def save_config(self): # the current OS is not supported pass + def revert_unsaved(self): + pass + class ThemeAndColors(Page): """ @@ -97,6 +102,7 @@ def __init__(self, parent=None): self._to_save = {} self._auto = TrackSystemTheme.get() + self._auto_original: bool = self._auto.enabled() self._schemes_combo: QComboBox = None self._init_widgets() @@ -164,6 +170,9 @@ def _on_load_scheme_clicked(self): self._load_color_scheme(self._schemes_combo.currentText()) self.save_config() + def revert_unsaved(self): + self._auto.set_enabled(self._auto_original) + def save_config(self): # pylint: disable=assigning-non-slot Conf.theme_name = self._schemes_combo.currentText() @@ -231,6 +240,11 @@ def item_changed(item: QListWidgetItem): self.setLayout(main_layout) + def close(self, *args, **kwargs): + for page in self._pages: + page.revert_unsaved() + super().close(*args, **kwargs) + def _on_ok_clicked(self): for page in self._pages: page.save_config() From c43c420af1bfa86999691e7de804c29c8a2dab0c Mon Sep 17 00:00:00 2001 From: zwimer Date: Wed, 30 Nov 2022 22:18:28 -0700 Subject: [PATCH 04/11] lint --- angrmanagement/utils/track_system_theme.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/angrmanagement/utils/track_system_theme.py b/angrmanagement/utils/track_system_theme.py index eb4e3baa2..8a91c7227 100644 --- a/angrmanagement/utils/track_system_theme.py +++ b/angrmanagement/utils/track_system_theme.py @@ -80,7 +80,7 @@ def __init__(self, parent, *, _caller=None): """ This method is not public """ - if _caller != self.create: + if _caller != self.create: # pylint: disable=comparison-with-callable raise RuntimeError("Use .create(parent) or .get(); this is a singleton") # Init self._underlying: str = darkdetect.theme() @@ -95,7 +95,7 @@ def _set_system(self, *, force: bool = False): new: str = darkdetect.theme() if force or new != self._underlying: self._underlying = new - _l.debug("Underling color theme set to: ", new) + _l.debug("Underling color theme set to: %s", new) Conf.theme_name = self._system if self._enabled else self._underlying for prop, value in COLOR_SCHEMES[new].items(): setattr(Conf, prop, value) From 7ebefd6a4765188c83002538fb6c003175d3c27d Mon Sep 17 00:00:00 2001 From: zwimer Date: Wed, 18 Jan 2023 14:10:41 -0700 Subject: [PATCH 05/11] Update to use upcoming darkdetect API --- angrmanagement/ui/dialogs/preferences.py | 6 +- angrmanagement/utils/track_system_theme.py | 88 +++++++++++++++------- 2 files changed, 64 insertions(+), 30 deletions(-) diff --git a/angrmanagement/ui/dialogs/preferences.py b/angrmanagement/ui/dialogs/preferences.py index fc57014d2..785bdc4ae 100644 --- a/angrmanagement/ui/dialogs/preferences.py +++ b/angrmanagement/ui/dialogs/preferences.py @@ -122,7 +122,6 @@ def __init__(self, parent=None): self._to_save = {} self._auto = TrackSystemTheme.get() - self._auto_original: bool = self._auto.enabled() self._schemes_combo: QComboBox = None self._init_widgets() @@ -191,7 +190,7 @@ def _on_load_scheme_clicked(self): self.save_config() def revert_unsaved(self): - self._auto.set_enabled(self._auto_original) + pass def save_config(self): # pylint: disable=assigning-non-slot @@ -250,6 +249,9 @@ def save_config(self): for i in self._font_options: i.update() + def revert_unsaved(self): + pass + class Preferences(QDialog): """ diff --git a/angrmanagement/utils/track_system_theme.py b/angrmanagement/utils/track_system_theme.py index 8a91c7227..c9c410e13 100644 --- a/angrmanagement/utils/track_system_theme.py +++ b/angrmanagement/utils/track_system_theme.py @@ -1,19 +1,44 @@ -from typing import Optional +from typing import Optional, Callable +from threading import Lock import logging +from PySide6.QtCore import QObject, QThread import darkdetect -from PySide6.QtCore import QTimer - from angrmanagement.config.color_schemes import COLOR_SCHEMES +from angrmanagement.logic.threads import gui_thread_schedule_async from angrmanagement.ui.css import refresh_theme - from angrmanagement.config import Conf _l = logging.getLogger(__name__) +class _QListener(QObject): + """ + A QObject wrapper around a darkdetect Listener + """ + + def __init__(self, callback: Callable[[str], None]): + """ + :param callback: The callback to be invoked on theme change + """ + self._listener = darkdetect.Listener(callback) + super().__init__() + + def listen(self) -> None: + """ + Start listening + """ + self._listener.listen() + + def stop(self, timeout: Optional[int]) -> None: + """ + Stop listening + """ + self._listener.stop(timeout) + + class TrackSystemTheme: """ A singleton global theme class @@ -26,9 +51,10 @@ class TrackSystemTheme: # @classmethod - def create(cls, parent: Optional["QObject"]): + def create(cls, parent: Optional[QObject]): """ Create the singleton global theme object + This function is not thread safe until after its first run """ if cls._object is not None: raise RuntimeError(f"Refusing to create a second {cls.__name__}") @@ -49,54 +75,60 @@ def set_enabled(self, enabled: bool): Connect system tracking slots as needed Note: This will not update the theme until the system state changes """ - if enabled == self._enabled: - return - self._enabled = enabled - if enabled: - self._timer.timeout.connect(self._set_system) - else: - self._timer.timeout.disconnect(self._set_system) + with self._lock: + if enabled == self.enabled(): + return + if enabled: + self._thread = QThread(self._parent) # Keep a reference to keep the thread alive + self._listener = _QListener(self._set_system) + self._listener.moveToThread(self._thread) + self._thread.started.connect(self._listener.listen) + self._thread.start() + else: + self._listener.stop(.1) # .1 to give a moment to clean up + self._thread.terminate() + self._listener = None + self._thread = None # Remove reference counted reference + assert enabled == self.enabled(), "sanity" def enabled(self) -> bool: """ Return True iff system theme tracking is enabled """ - return self._enabled + return self._listener is not None def refresh_theme(self): """ Force a refresh of the theme """ - if self._enabled: - self._set_system(force=True) + if self.enabled(): + self._set_system(darkdetect.theme(), force=True) else: - refresh_theme() + gui_thread_schedule_async(refresh_theme) # # Private methods # - def __init__(self, parent, *, _caller=None): + def __init__(self, parent: Optional[QObject], *, _caller=None): """ This method is not public """ if _caller != self.create: # pylint: disable=comparison-with-callable raise RuntimeError("Use .create(parent) or .get(); this is a singleton") # Init + self._lock = Lock() + self._parent = parent self._underlying: str = darkdetect.theme() - self._enabled: bool = False - self._timer = QTimer(parent) - self._timer.start(50) + self._listener: Optional[Listener] = None - def _set_system(self, *, force: bool = False): + def _set_system(self, theme: str, *, force: bool = False): """ Set the underlying theme according to the system theme if needed """ - new: str = darkdetect.theme() - if force or new != self._underlying: - self._underlying = new - _l.debug("Underling color theme set to: %s", new) - Conf.theme_name = self._system if self._enabled else self._underlying - for prop, value in COLOR_SCHEMES[new].items(): + if force or theme != self._underlying: + self._underlying = theme + Conf.theme_name = self._system if self.enabled() else self._underlying + for prop, value in COLOR_SCHEMES[theme].items(): setattr(Conf, prop, value) - refresh_theme() + gui_thread_schedule_async(refresh_theme) From d2474ae814a7f493e1566633421fca81d97ddabc Mon Sep 17 00:00:00 2001 From: zwimer Date: Wed, 18 Jan 2023 14:57:51 -0700 Subject: [PATCH 06/11] Bug fixes --- angrmanagement/ui/dialogs/preferences.py | 11 ++++----- angrmanagement/utils/track_system_theme.py | 28 ++++++++++------------ 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/angrmanagement/ui/dialogs/preferences.py b/angrmanagement/ui/dialogs/preferences.py index 785bdc4ae..2e611ccc7 100644 --- a/angrmanagement/ui/dialogs/preferences.py +++ b/angrmanagement/ui/dialogs/preferences.py @@ -176,9 +176,8 @@ def _init_widgets(self): def _toggle_system_tracking(self, state: int): self._auto.set_enabled(state == Qt.CheckState.Checked.value) - if state == Qt.CheckState.Unchecked.value: - self._on_load_scheme_clicked() - self._on_load_scheme_clicked() + Conf.theme_track_system = self._auto.enabled() + (self._auto.refresh_theme if Conf.theme_track_system else self._on_load_scheme_clicked)() def _load_color_scheme(self, name): for prop, value in COLOR_SCHEMES[name].items(): @@ -193,12 +192,12 @@ def revert_unsaved(self): pass def save_config(self): - # pylint: disable=assigning-non-slot + if Conf.theme_track_system: + return Conf.theme_name = self._schemes_combo.currentText() for ce, row in self._to_save.values(): setattr(Conf, ce.name, row.color.am_obj) - Conf.theme_track_system = self._auto.enabled() - self._auto.refresh_theme() + refresh_theme() class Style(Page): diff --git a/angrmanagement/utils/track_system_theme.py b/angrmanagement/utils/track_system_theme.py index c9c410e13..3b7acd466 100644 --- a/angrmanagement/utils/track_system_theme.py +++ b/angrmanagement/utils/track_system_theme.py @@ -23,20 +23,14 @@ def __init__(self, callback: Callable[[str], None]): """ :param callback: The callback to be invoked on theme change """ - self._listener = darkdetect.Listener(callback) + self.listener = darkdetect.Listener(callback) super().__init__() def listen(self) -> None: """ Start listening """ - self._listener.listen() - - def stop(self, timeout: Optional[int]) -> None: - """ - Stop listening - """ - self._listener.stop(timeout) + self.listener.listen() class TrackSystemTheme: @@ -78,31 +72,31 @@ def set_enabled(self, enabled: bool): with self._lock: if enabled == self.enabled(): return + self._enabled = enabled if enabled: self._thread = QThread(self._parent) # Keep a reference to keep the thread alive - self._listener = _QListener(self._set_system) + self._listener = _QListener(self._set_theme) self._listener.moveToThread(self._thread) self._thread.started.connect(self._listener.listen) self._thread.start() else: - self._listener.stop(.1) # .1 to give a moment to clean up + self._listener.listener.stop(.05) # .05 to give a moment to clean up self._thread.terminate() self._listener = None self._thread = None # Remove reference counted reference - assert enabled == self.enabled(), "sanity" def enabled(self) -> bool: """ Return True iff system theme tracking is enabled """ - return self._listener is not None + return self._enabled def refresh_theme(self): """ Force a refresh of the theme """ if self.enabled(): - self._set_system(darkdetect.theme(), force=True) + self._set_theme(darkdetect.theme(), force=True) else: gui_thread_schedule_async(refresh_theme) @@ -120,15 +114,17 @@ def __init__(self, parent: Optional[QObject], *, _caller=None): self._lock = Lock() self._parent = parent self._underlying: str = darkdetect.theme() - self._listener: Optional[Listener] = None + self._enabled: bool = False + self._listener: Optional[_QListener] = None + self._thread: Optional[QThread] = None - def _set_system(self, theme: str, *, force: bool = False): + def _set_theme(self, theme: str, *, force: bool = False): """ Set the underlying theme according to the system theme if needed """ if force or theme != self._underlying: self._underlying = theme - Conf.theme_name = self._system if self.enabled() else self._underlying + Conf.theme_name = self._underlying for prop, value in COLOR_SCHEMES[theme].items(): setattr(Conf, prop, value) gui_thread_schedule_async(refresh_theme) From ce422a347cb469aa6b8e053b06087482167524e7 Mon Sep 17 00:00:00 2001 From: zwimer Date: Tue, 25 Apr 2023 15:46:45 -0700 Subject: [PATCH 07/11] Use darkdetect-angr --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 1c3be795b..13ac71282 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,7 +29,7 @@ install_requires = qtterm requests[socks] tomlkit - darkdetect + darkdetect-angr[macos-listener] pyobjc-framework-Cocoa;platform_system == "Darwin" thefuzz[speedup] python_requires = >=3.8 From 0811d4befb7e656fee4282cba16e9ddb34b50a7f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 25 Apr 2023 22:47:15 +0000 Subject: [PATCH 08/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- angrmanagement/utils/track_system_theme.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/angrmanagement/utils/track_system_theme.py b/angrmanagement/utils/track_system_theme.py index 3b7acd466..aa0b5090c 100644 --- a/angrmanagement/utils/track_system_theme.py +++ b/angrmanagement/utils/track_system_theme.py @@ -37,6 +37,7 @@ class TrackSystemTheme: """ A singleton global theme class """ + _object: Optional["TrackSystemTheme"] = None _system: str = "System" @@ -80,7 +81,7 @@ def set_enabled(self, enabled: bool): self._thread.started.connect(self._listener.listen) self._thread.start() else: - self._listener.listener.stop(.05) # .05 to give a moment to clean up + self._listener.listener.stop(0.05) # .05 to give a moment to clean up self._thread.terminate() self._listener = None self._thread = None # Remove reference counted reference From 5fc4cdd196f351440455901a9f5169a0e37893bb Mon Sep 17 00:00:00 2001 From: zwimer Date: Tue, 25 Apr 2023 16:39:13 -0700 Subject: [PATCH 09/11] Bug fix in instance that format string with duplicate output is saved in config --- angrmanagement/ui/dialogs/preferences.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/angrmanagement/ui/dialogs/preferences.py b/angrmanagement/ui/dialogs/preferences.py index 959a4a4fd..dba6dd1be 100644 --- a/angrmanagement/ui/dialogs/preferences.py +++ b/angrmanagement/ui/dialogs/preferences.py @@ -226,7 +226,8 @@ def _init_widgets(self): fmt: str = Conf.log_timestamp_format ts = datetime.now() # pylint: disable=use-sequence-for-iteration - self._fmt_map = bidict({ts.strftime(i): i for i in {fmt, "%X", "%c"}}) # set also dedups + self._fmt_map = bidict({ts.strftime(i): i for i in ("%X", "%c")}) + self._fmt_map.forceput(ts.strftime(fmt), fmt) # Ensure fmt is in the dict for i in self._fmt_map: self.log_format_entry.addItem(i) # pylint: disable=unsubscriptable-object From 5ee69bbeeedbe5bd63db57ef6ccf9331aafbfc5d Mon Sep 17 00:00:00 2001 From: zwimer Date: Tue, 25 Apr 2023 17:02:19 -0700 Subject: [PATCH 10/11] Lint --- angrmanagement/utils/track_system_theme.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/angrmanagement/utils/track_system_theme.py b/angrmanagement/utils/track_system_theme.py index aa0b5090c..140719b5f 100644 --- a/angrmanagement/utils/track_system_theme.py +++ b/angrmanagement/utils/track_system_theme.py @@ -1,15 +1,14 @@ -from typing import Optional, Callable -from threading import Lock import logging +from threading import Lock +from typing import Callable, Optional -from PySide6.QtCore import QObject, QThread import darkdetect +from PySide6.QtCore import QObject, QThread +from angrmanagement.config import Conf from angrmanagement.config.color_schemes import COLOR_SCHEMES from angrmanagement.logic.threads import gui_thread_schedule_async from angrmanagement.ui.css import refresh_theme -from angrmanagement.config import Conf - _l = logging.getLogger(__name__) From 1b1092f6b39c4b663ace3dcde0fab9255a18ef3a Mon Sep 17 00:00:00 2001 From: zwimer Date: Wed, 26 Apr 2023 13:44:08 -0700 Subject: [PATCH 11/11] Move init to top of class --- angrmanagement/utils/track_system_theme.py | 58 +++++++++++----------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/angrmanagement/utils/track_system_theme.py b/angrmanagement/utils/track_system_theme.py index 140719b5f..8ea8d78c5 100644 --- a/angrmanagement/utils/track_system_theme.py +++ b/angrmanagement/utils/track_system_theme.py @@ -40,6 +40,35 @@ class TrackSystemTheme: _object: Optional["TrackSystemTheme"] = None _system: str = "System" + # + # Private methods + # + + def __init__(self, parent: Optional[QObject], *, _caller=None): + """ + This method is not public + """ + if _caller != self.create: # pylint: disable=comparison-with-callable + raise RuntimeError("Use .create(parent) or .get(); this is a singleton") + # Init + self._lock = Lock() + self._parent = parent + self._underlying: str = darkdetect.theme() + self._enabled: bool = False + self._listener: Optional[_QListener] = None + self._thread: Optional[QThread] = None + + def _set_theme(self, theme: str, *, force: bool = False): + """ + Set the underlying theme according to the system theme if needed + """ + if force or theme != self._underlying: + self._underlying = theme + Conf.theme_name = self._underlying + for prop, value in COLOR_SCHEMES[theme].items(): + setattr(Conf, prop, value) + gui_thread_schedule_async(refresh_theme) + # # Public methods # @@ -99,32 +128,3 @@ def refresh_theme(self): self._set_theme(darkdetect.theme(), force=True) else: gui_thread_schedule_async(refresh_theme) - - # - # Private methods - # - - def __init__(self, parent: Optional[QObject], *, _caller=None): - """ - This method is not public - """ - if _caller != self.create: # pylint: disable=comparison-with-callable - raise RuntimeError("Use .create(parent) or .get(); this is a singleton") - # Init - self._lock = Lock() - self._parent = parent - self._underlying: str = darkdetect.theme() - self._enabled: bool = False - self._listener: Optional[_QListener] = None - self._thread: Optional[QThread] = None - - def _set_theme(self, theme: str, *, force: bool = False): - """ - Set the underlying theme according to the system theme if needed - """ - if force or theme != self._underlying: - self._underlying = theme - Conf.theme_name = self._underlying - for prop, value in COLOR_SCHEMES[theme].items(): - setattr(Conf, prop, value) - gui_thread_schedule_async(refresh_theme)