-
Notifications
You must be signed in to change notification settings - Fork 119
Add system theme support #806
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 13 commits
49ddefe
6a05e7b
7dfc3ca
c43c420
8e6a4e9
00c8d96
7ebefd6
d2474ae
5dcac4b
ce422a3
0811d4b
5fc4cdd
5ee69bb
1b1092f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
from datetime import datetime | ||
|
||
from bidict import bidict | ||
from PySide6.QtCore import QSize | ||
from PySide6.QtCore import QSize, Qt | ||
from PySide6.QtGui import QColor | ||
from PySide6.QtWidgets import ( | ||
QCheckBox, | ||
|
@@ -31,6 +31,7 @@ | |
from angrmanagement.ui.css import refresh_theme | ||
from angrmanagement.ui.widgets.qcolor_option import QColorOption | ||
from angrmanagement.ui.widgets.qfont_option import QFontOption | ||
from angrmanagement.utils.track_system_theme import TrackSystemTheme | ||
|
||
|
||
class Page(QWidget): | ||
|
@@ -41,6 +42,9 @@ class Page(QWidget): | |
def save_config(self): | ||
raise NotImplementedError | ||
|
||
def revert_unsaved(self): | ||
raise NotImplementedError | ||
|
||
NAME = NotImplemented | ||
|
||
|
||
|
@@ -104,6 +108,9 @@ def save_config(self): | |
# the current OS is not supported | ||
pass | ||
|
||
def revert_unsaved(self): | ||
pass | ||
|
||
|
||
class ThemeAndColors(Page): | ||
""" | ||
|
@@ -116,6 +123,7 @@ def __init__(self, parent=None): | |
super().__init__(parent=parent) | ||
|
||
self._to_save = {} | ||
self._auto = TrackSystemTheme.get() | ||
self._schemes_combo: QComboBox = None | ||
|
||
self._init_widgets() | ||
|
@@ -130,7 +138,7 @@ def _init_widgets(self): | |
|
||
self._schemes_combo = QComboBox(self) | ||
current_theme_idx = 0 | ||
for idx, name in enumerate(["Current"] + sorted(COLOR_SCHEMES)): | ||
for idx, name in enumerate(sorted(COLOR_SCHEMES)): | ||
if name == Conf.theme_name: | ||
current_theme_idx = idx | ||
self._schemes_combo.addItem(name) | ||
|
@@ -161,8 +169,18 @@ 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) | ||
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(): | ||
row = self._to_save[prop][1] | ||
|
@@ -172,11 +190,16 @@ def _on_load_scheme_clicked(self): | |
self._load_color_scheme(self._schemes_combo.currentText()) | ||
self.save_config() | ||
|
||
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) | ||
refresh_theme() | ||
|
||
|
||
class Style(Page): | ||
|
@@ -203,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")}) | ||
zwimer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
|
@@ -235,6 +259,9 @@ def save_config(self): | |
for i in self._font_options: | ||
i.update() | ||
|
||
def revert_unsaved(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Identical to base class implemantation There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The base class raises There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I guess I was looking at the wrong thing Why not have the default implementation do nothing instead of raising an error? Or just drop the method because there's not an implementation that does anything? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was generally thinking of the page class as abstract and the functions that |
||
pass | ||
|
||
|
||
class Preferences(QDialog): | ||
""" | ||
|
@@ -294,6 +321,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() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import logging | ||
from threading import Lock | ||
from typing import Callable, Optional | ||
|
||
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 | ||
|
||
_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() | ||
|
||
|
||
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 | ||
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__}") | ||
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 | ||
""" | ||
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_theme) | ||
self._listener.moveToThread(self._thread) | ||
self._thread.started.connect(self._listener.listen) | ||
self._thread.start() | ||
else: | ||
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 | ||
|
||
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_theme(darkdetect.theme(), force=True) | ||
else: | ||
gui_thread_schedule_async(refresh_theme) | ||
|
||
# | ||
# Private methods | ||
# | ||
|
||
def __init__(self, parent: Optional[QObject], *, _caller=None): | ||
zwimer marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
""" | ||
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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,7 @@ install_requires = | |
qtterm | ||
requests[socks] | ||
tomlkit | ||
darkdetect-angr[macos-listener] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The majority of the fork has a PR into the main repo, so once that is merged we might not need to hopefully. TBD. I've tested the PR'd version on all three OS's, though less throughly on Windows as I lack said OS. The version we are using is mostly the same so it ought to work. Feel free to test them yourself but the relevant logic that distinguished between OSes wasn't really the code that changed between the PR'd version and the hardfork version. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
But not the forked version with extra changes?
I don't plan to test this PR. Will you please test it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The extra changes are copyright, README, and There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you are curious, here is the diff stats:
Note the 2 line changes are copyright comments. I don't have access to a Windows device at the moment so I can't really test these changes easily, but they aren't really code changes just the changes necessary to hard-fork a project (changing the name on pypi, copyright, documentation, and version). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not know how to automate GUI test cases on windows changing system theme and detecting of the colors properly all changed over. Feel free. The actions to test this would be launch app, open preferences, select 'Track System Theme', then toggle the Windows System Theme and visually ensure it tracks the theme. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't need to "visually ensure" it, you just need to make sure after the theme is changed the correct behavior occurs in the library. I'm sure applescript and powershell each have ways to adjust the system theme. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you want to write these test cases, I can tell you how to do it in applescript (via System Events), I don't know powershell. I imagine for Gnome specifically there is a gsettings option you could find? For detecting if it worked, you'll have to query the QApplication to check that all the colors and such have been updated and also ensure that any visual refresh functions have been called (i.e. redrawing the GUI app). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's on the angr management side, but not necessary for the library itself which is is the subject of this thread. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll happily take a PR for them here: https://github.com/zwimer/darkdetect but I don't have plans to write tests myself for them at the moment. If you do PR them, I'll also merge them into the branch that is PR-ing into the original repo https://github.com/albertosottile/darkdetect so as to avoid actively diverging the hard fork from the original more than necessary; my plan is to get rid of the fork once the PR albertosottile/darkdetect#32 in the original is merged. Right now that PR has been tested on all three OSes and my fork's master branch doesn't meaningfully diverge from said code. I know the maintainer desired to do more thorough testing before merging the PR though, so I'm sure the test cases would be appreciated by all parties. |
||
pyobjc-framework-Cocoa;platform_system == "Darwin" | ||
thefuzz[speedup] | ||
python_requires = >=3.8 | ||
|
Uh oh!
There was an error while loading. Please reload this page.