diff --git a/dzgui/views/dialogs/boot.py b/dzgui/views/dialogs/boot.py index 8873b7e..34fb8fe 100644 --- a/dzgui/views/dialogs/boot.py +++ b/dzgui/views/dialogs/boot.py @@ -4,6 +4,9 @@ from typing import Any, Literal, Self, TYPE_CHECKING # import time from enum import Enum + +from dzgui.views.mixins.colorscheme import ColorAwareApp + # TODO: import dialog titles from dzgui.api.mods import remove_stale_signatures as remove_stale from dzgui.config.ipdb import get_ipdb @@ -37,7 +40,7 @@ class Success(Enum): # TODO: strings for "Running", "Failed", etc. # TODO: add margins to tree -class BootDialog(Gtk.Dialog): +class BootDialog(ColorAwareApp, Gtk.Dialog): # type: ignore def __init__(self, parent: "BootWindow", xdg: "Xdg", version: str) -> None: super().__init__( title=dialog_header, diff --git a/dzgui/views/dialogs/early_alert.py b/dzgui/views/dialogs/early_alert.py index e55c76b..64042d0 100644 --- a/dzgui/views/dialogs/early_alert.py +++ b/dzgui/views/dialogs/early_alert.py @@ -3,6 +3,7 @@ import sys from typing import Self from dzgui.util.strings import dialog_error, dialog_header +from dzgui.views.mixins.colorscheme import ColorAwareApp import gi @@ -10,7 +11,7 @@ gi.require_version("Gtk", "3.0") from gi.repository import Gtk # noqa E402 -class AbortDialog(Gtk.MessageDialog): +class AbortDialog(ColorAwareApp, Gtk.MessageDialog): def __init__(self, string: str, buttons: Gtk.ButtonsType) -> None: super().__init__( title=dialog_header, diff --git a/dzgui/views/dialogs/uninstall.py b/dzgui/views/dialogs/uninstall.py index 9098692..3e7d2e7 100644 --- a/dzgui/views/dialogs/uninstall.py +++ b/dzgui/views/dialogs/uninstall.py @@ -13,6 +13,7 @@ from dzgui.util.css import load_css from dzgui.util.format import format_exception from dzgui.views.components.box import HBox from dzgui.views.dialogs.wizard import ScrolledWizardPage, CheckboxWithLabel +from dzgui.views.mixins.colorscheme import ColorAwareApp import gi @@ -217,7 +218,7 @@ class UninstallPage(ScrolledWizardPage): pass -class UninstallWizard(Gtk.Application): +class UninstallWizard(ColorAwareApp, Gtk.Application): # type: ignore def __init__(self, is_deck: bool, paths: dict[str, str]) -> None: super().__init__() config = Path(paths["XDG_CONFIG_HOME"]) diff --git a/dzgui/views/dialogs/wizard.py b/dzgui/views/dialogs/wizard.py index d0f0aa0..033f7a1 100644 --- a/dzgui/views/dialogs/wizard.py +++ b/dzgui/views/dialogs/wizard.py @@ -29,6 +29,7 @@ from dzgui.util.css import add_class, load_css from dzgui.views.components.buttons import WebButton from dzgui.views.components.entry import APIEntry from dzgui.views.components.misc import ClientCombo +from dzgui.views.mixins.colorscheme import ColorAwareApp import gi @@ -706,7 +707,7 @@ class SteamPathPage(EnumeratedWizardPage): self.err_box.set_visible(False) -class SetupWizard(Gtk.Application): +class SetupWizard(ColorAwareApp, Gtk.Application): # type: ignore def __init__(self, is_deck: bool, XDG: "Xdg") -> None: super().__init__() GLib.set_prgname(APP_NAME) diff --git a/dzgui/views/mixins/colorscheme.py b/dzgui/views/mixins/colorscheme.py new file mode 100644 index 0000000..2e0be9c --- /dev/null +++ b/dzgui/views/mixins/colorscheme.py @@ -0,0 +1,37 @@ +from typing import Any + +from dzgui.const.constants import ( + GIO_SETTINGS_KEY, + GIO_SETTINGS_INTERFACE, + GIO_SETTINGS_PROP, + GIO_SETTINGS_VAL, +) + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gio # noqa + + +class ColorAwareApp: + """ + Polls the local theme's color scheme settings. + Intended to be consumed by Gtk.Applications (or ephemeral widgets like EarlyAlertDialogs) + on initialization, prior to being realized. Applying scheme changes too late in the chain + may cause a blinking effect. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.interface_settings = Gio.Settings.new(GIO_SETTINGS_INTERFACE) + self.interface_settings.connect( + "changed::color-scheme", self._on_color_scheme_changed + ) + self.default_settings = Gtk.Settings.get_default() + self._on_color_scheme_changed(self.interface_settings, GIO_SETTINGS_PROP) + + def _on_color_scheme_changed(self, settings: Gio.Settings, prop: str) -> None: + state = self.interface_settings.get_string(prop) == GIO_SETTINGS_VAL + if self.default_settings is None: + return + self.default_settings.set_property(GIO_SETTINGS_KEY, state) diff --git a/tests/test_colorscheme.py b/tests/test_colorscheme.py new file mode 100644 index 0000000..fd02526 --- /dev/null +++ b/tests/test_colorscheme.py @@ -0,0 +1,59 @@ +import pytest +import subprocess + +from dzgui.const.constants import GIO_SETTINGS_KEY +from dzgui.views.mixins.colorscheme import ColorAwareApp + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gio, GLib # noqa + + +class TestWidget(ColorAwareApp, Gtk.Button): + def __init__(self) -> None: + super().__init__() + + self.state: bool + + def check_theme(self) -> None: + self.state = self.get_settings().get_property(GIO_SETTINGS_KEY) + Gtk.main_quit() + + +@pytest.fixture +def widget() -> TestWidget: + return TestWidget() + + +@pytest.mark.integration +@pytest.mark.parametrize("key, expect", [("default", False), ("prefer-dark", True)]) +def test_light_mode(widget, key: str, expect: bool) -> None: + local_pref = subprocess.run( + ["gsettings", "get", "org.gnome.desktop.interface", "color-scheme"], + capture_output=True, + text=True, + ).stdout + subprocess.run( + [ + "gsettings", + "set", + "org.gnome.desktop.interface", + "color-scheme", + key, + ], + ) + + # NOTE: allow changes to propagate + GLib.idle_add(widget.check_theme) + Gtk.main() + subprocess.run( + [ + "gsettings", + "set", + "org.gnome.desktop.interface", + "color-scheme", + local_pref, + ] + ) + assert widget.state is expect