From ece35f9af0f11d8cf415c4cd8d0f842a19de2dde Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:18:13 -0700 Subject: [PATCH] Pull gnome-specific portions of UI out so UI package can be reused for breezy-box --- bin/package_gnome | 2 +- gnome/ui/breezygnomeruntimeenvironment.py | 106 +++++++++++ {ui/src => gnome/ui}/updatechecker.py | 0 {ui/src => gnome/ui}/verify.py | 2 +- {ui/src => gnome/ui}/virtualdisplay.py | 0 {ui/src => gnome/ui}/virtualdisplaymanager.py | 2 +- ui/bin/package | 18 ++ ui/src/connecteddevice.py | 19 +- ui/src/extensionsmanager.py | 72 -------- ui/src/files.py | 5 +- ui/src/meson.build | 13 +- ui/src/nodevice.py | 10 +- ui/src/runtime.py | 63 +++++++ ui/src/runtimeenvironment.py | 169 ++++++++++++++++++ ui/src/runtimes/__init__.py | 7 + ui/src/virtualdisplay.in | 4 +- ui/src/virtualdisplayrow.py | 4 +- ui/src/window.py | 15 +- 18 files changed, 400 insertions(+), 111 deletions(-) create mode 100644 gnome/ui/breezygnomeruntimeenvironment.py rename {ui/src => gnome/ui}/updatechecker.py (100%) rename {ui/src => gnome/ui}/verify.py (95%) rename {ui/src => gnome/ui}/virtualdisplay.py (100%) rename {ui/src => gnome/ui}/virtualdisplaymanager.py (98%) delete mode 100644 ui/src/extensionsmanager.py create mode 100644 ui/src/runtime.py create mode 100644 ui/src/runtimeenvironment.py create mode 100644 ui/src/runtimes/__init__.py diff --git a/bin/package_gnome b/bin/package_gnome index fdc74df..54dc930 100755 --- a/bin/package_gnome +++ b/bin/package_gnome @@ -97,7 +97,7 @@ popd UI_BUILD_ARTIFACT=$UI_DIR/out/breezyUI-$ARCH.tar.gz if [ ! -e "$UI_BUILD_ARTIFACT" ] || [ "$1" == "--rebuild-ui" ] || [ "$1" == "--rebuild-all" ]; then pushd $UI_DIR - bin/package $ARCH + RUNTIME_DIR=$GNOME_DIR/ui bin/package $ARCH popd fi tar -xf $UI_BUILD_ARTIFACT -C $PACKAGE_DIR diff --git a/gnome/ui/breezygnomeruntimeenvironment.py b/gnome/ui/breezygnomeruntimeenvironment.py new file mode 100644 index 0000000..3014294 --- /dev/null +++ b/gnome/ui/breezygnomeruntimeenvironment.py @@ -0,0 +1,106 @@ +"""GNOME Shell runtime environment for Breezy Desktop. + +This is the reference RuntimeEnvironment implementation. It is packaged into the +UI's ``runtimes`` subpackage by the GNOME package script (see bin/package_gnome +-> ui/bin/package), so its imports are relative to the installed +``breezydesktop`` package. +""" + +import logging +import os + +import pydbus + +from ..runtimeenvironment import RuntimeEnvironment + +logger = logging.getLogger('breezy_ui') + +BREEZY_DESKTOP_UUID = "breezydesktop@xronlinux.com" +EXTENSION_STATE_ENABLED = 1 + + +class BreezyGNOMERuntimeEnvironment(RuntimeEnvironment): + """Runs Breezy Desktop as a GNOME Shell extension. + + Enablement is backed by the GNOME Shell extension state, verification runs + the breezy_gnome_verify binary, updates are checked against GitHub, and + virtual displays are created via the Mutter ScreenCast portal. + """ + + APP_NAMESPACE = 'breezy_gnome' + + def __init__(self): + super().__init__() + + self.bus = pydbus.SessionBus() + self.gnome_shell_extensions = self.bus.get("org.gnome.Shell.Extensions") + self.gnome_shell_extensions.ExtensionStateChanged.connect(self._handle_extension_state_change) + + self._breezy_enabled = self.is_enabled() + + def _handle_extension_state_change(self, extension_uuid, state): + if extension_uuid == BREEZY_DESKTOP_UUID: + enabled = state.get('state') == EXTENSION_STATE_ENABLED + # update internal state first so do_set_property doesn't re-trigger + # an extension enable/disable; this just emits the notify + self._breezy_enabled = enabled + self.set_property('breezy-enabled', enabled) + + # --- effect enablement ------------------------------------------------ + + def is_installed(self): + extensions_result = self.gnome_shell_extensions.ListExtensions() + for extension in extensions_result: + if extension == BREEZY_DESKTOP_UUID: + return True + return False + + def is_enabled(self): + return self.gnome_shell_extensions.GetExtensionInfo(BREEZY_DESKTOP_UUID).get('state') == EXTENSION_STATE_ENABLED + + def enable(self): + if not self.gnome_shell_extensions.UserExtensionsEnabled: + self.gnome_shell_extensions.UserExtensionsEnabled = True + self.gnome_shell_extensions.EnableExtension(BREEZY_DESKTOP_UUID) + self._breezy_enabled = True + + def disable(self): + self.gnome_shell_extensions.DisableExtension(BREEZY_DESKTOP_UUID) + self._breezy_enabled = False + + # --- verification / updates ------------------------------------------- + + def verify(self): + from .verify import verify_installation + return verify_installation() + + def check_for_update(self, current_version, callback): + from .updatechecker import check_for_update + return check_for_update(current_version, callback) + + # --- optional views --------------------------------------------------- + + @property + def shows_no_device_view(self): + return True + + # --- virtual displays ------------------------------------------------- + + def is_virtual_display_supported(self): + # wayland + the Mutter ScreenCast portal are required to create displays + from .virtualdisplay import is_screencast_available + return is_screencast_available() and "WAYLAND_DISPLAY" in os.environ + + def _create_virtual_display_manager(self): + from .virtualdisplaymanager import VirtualDisplayManager + return VirtualDisplayManager.get_instance() + + # --- GObject property plumbing ---------------------------------------- + + def do_set_property(self, prop, value): + if prop.name == 'breezy-enabled' and value != self._breezy_enabled: + self.enable() if value else self.disable() + + def do_get_property(self, prop): + if prop.name == 'breezy-enabled': + return self._breezy_enabled diff --git a/ui/src/updatechecker.py b/gnome/ui/updatechecker.py similarity index 100% rename from ui/src/updatechecker.py rename to gnome/ui/updatechecker.py diff --git a/ui/src/verify.py b/gnome/ui/verify.py similarity index 95% rename from ui/src/verify.py rename to gnome/ui/verify.py index ac2b560..6f6e908 100644 --- a/ui/src/verify.py +++ b/gnome/ui/verify.py @@ -2,7 +2,7 @@ import logging import os import subprocess -from .files import get_bin_home +from ..files import get_bin_home logger = logging.getLogger('breezy_ui') diff --git a/ui/src/virtualdisplay.py b/gnome/ui/virtualdisplay.py similarity index 100% rename from ui/src/virtualdisplay.py rename to gnome/ui/virtualdisplay.py diff --git a/ui/src/virtualdisplaymanager.py b/gnome/ui/virtualdisplaymanager.py similarity index 98% rename from ui/src/virtualdisplaymanager.py rename to gnome/ui/virtualdisplaymanager.py index 1ee3fd9..0493571 100644 --- a/ui/src/virtualdisplaymanager.py +++ b/gnome/ui/virtualdisplaymanager.py @@ -12,7 +12,7 @@ logger = logging.getLogger('breezy_ui') gi.require_version('GLib', '2.0') from gi.repository import GLib, GObject -from .files import get_bin_home +from ..files import get_bin_home class VirtualDisplayManager(GObject.GObject): __gproperties__ = { diff --git a/ui/bin/package b/ui/bin/package index f41fe2d..d5b2fa6 100755 --- a/ui/bin/package +++ b/ui/bin/package @@ -6,6 +6,18 @@ set -e ARCH=${ARCH:-$(uname -m)} echo "Building Breezy UI for $ARCH" +# Directory containing the RuntimeEnvironment implementation(s) to bundle. Every +# *.py in here is copied into the UI's runtimes/ package; the first +# RuntimeEnvironment subclass found at startup becomes the active environment. +# Defaults to the reference GNOME implementation so a standalone UI build (e.g. +# local dev) still has a runtime. +RUNTIME_DIR=${RUNTIME_DIR:-$(realpath "$(dirname "$0")/../../gnome/ui")} +echo "Bundling runtime environment from $RUNTIME_DIR" +if [ ! -d "$RUNTIME_DIR" ]; then + echo "Runtime directory $RUNTIME_DIR does not exist" + exit 1 +fi + BUILD_PATH=build UI_BUILD_PATH=$BUILD_PATH/ui PACKAGE_DIR=$BUILD_PATH/breezy_ui @@ -38,6 +50,12 @@ mkdir -p $PACKAGE_SCHEMAS_DIR cp src/*.py $PACKAGE_BREEZY_SRC_DIR cp -r lib $PACKAGE_BREEZY_SRC_DIR + +# bundle the runtimes subpackage and the selected runtime implementation(s) +PACKAGE_RUNTIMES_DIR=$PACKAGE_BREEZY_SRC_DIR/runtimes +mkdir -p $PACKAGE_RUNTIMES_DIR +cp src/runtimes/__init__.py $PACKAGE_RUNTIMES_DIR +cp $RUNTIME_DIR/*.py $PACKAGE_RUNTIMES_DIR cp -L modules/PyXRLinuxDriverIPC/xrdriveripc.py $PACKAGE_BREEZY_SRC_DIR cp $UI_BUILD_PATH/src/breezydesktop.gresource $PACKAGE_BREEZY_DIR cp -r po/mo/* $PACKAGE_LOCALE_DIR diff --git a/ui/src/connecteddevice.py b/ui/src/connecteddevice.py index f1bf08d..a76570a 100644 --- a/ui/src/connecteddevice.py +++ b/ui/src/connecteddevice.py @@ -2,14 +2,12 @@ from gi.repository import Gio, GLib, Gtk, GObject from .configmanager import ConfigManager from .customresolutiondialog import CustomResolutionDialog from .displaydistancedialog import DisplayDistanceDialog -from .extensionsmanager import ExtensionsManager from .files import get_state_dir from .license import BREEZY_GNOME_FEATURES +from .runtimeenvironment import RuntimeEnvironment from .settingsmanager import SettingsManager from .shortcutdialog import bind_shortcut_settings from .statemanager import StateManager -from .virtualdisplaymanager import VirtualDisplayManager -from .virtualdisplay import is_screencast_available from .virtualdisplayrow import VirtualDisplayRow from .xrdriveripc import XRDriverIPC @@ -115,8 +113,8 @@ class ConnectedDevice(Gtk.Box): self.settings = SettingsManager.get_instance().settings self.desktop_settings = SettingsManager.get_instance().desktop_settings self.ipc = XRDriverIPC.get_instance() - self.virtual_display_manager = VirtualDisplayManager.get_instance() - self.extensions_manager = ExtensionsManager.get_instance() + self.runtime = RuntimeEnvironment.get_instance() + self.virtual_display_manager = self.runtime.virtual_display_manager self.settings.bind('disable-physical-displays', self.disable_physical_displays_switch, 'active', Gio.SettingsBindFlags.DEFAULT) self.settings.connect('changed::display-distance', self._handle_display_distance) @@ -207,7 +205,7 @@ class ConnectedDevice(Gtk.Box): self._handle_device_supports_sbs(self.state_manager, None) self._handle_enabled_config(None, None) self._refresh_use_optimal_monitor_config(self.use_optimal_monitor_config_switch, None) - self.extensions_manager.connect('notify::breezy-enabled', self._handle_enabled_config) + self.runtime.connect('notify::breezy-enabled', self._handle_enabled_config) self._settings_displays_app_info = None @@ -229,9 +227,6 @@ class ConnectedDevice(Gtk.Box): for id in self._custom_resolution_options: self.add_virtual_display_menu.insert(self._default_resolution_options_count, id, id) - # wayland is required to create virtual displays - self.is_wayland = "WAYLAND_DISPLAY" in os.environ - def _bind_scale_to_config(self, scale, config_key): self.config_manager.bind_property(config_key, scale, 'value', Gio.SettingsBindFlags.DEFAULT) scale.set_value(self.config_manager.get_property(config_key)) @@ -285,7 +280,7 @@ class ConnectedDevice(Gtk.Box): # self.widescreen_mode_row.set_subtitle(subtitle) def _handle_enabled_config(self, object, val): - enabled = self.config_manager.get_property('breezy-desktop-enabled') and self.extensions_manager.get_property('breezy-enabled') + enabled = self.config_manager.get_property('breezy-desktop-enabled') and self.runtime.get_property('breezy-enabled') if enabled != self.effect_enable_switch.get_active(): self.effect_enable_switch.set_active(enabled) @@ -297,14 +292,14 @@ class ConnectedDevice(Gtk.Box): # never turn off the extension, disabling the effect is done via configs only if requesting_enabled: - self.extensions_manager.set_property('breezy-enabled', True) + self.runtime.set_property('breezy-enabled', True) self.config_manager.set_property('breezy-desktop-enabled', requesting_enabled) for widget in self.all_enabled_state_inputs: widget.set_sensitive(requesting_enabled) - if not is_screencast_available() or not self.is_wayland: + if not self.runtime.is_virtual_display_supported(): self.virtual_displays_row.set_subtitle( _("Unable to add virtual displays on this machine. Wayland, xdg-desktop-portal, and the pipewire GStreamer plugin are required.")) self.add_virtual_display_button.set_sensitive(False) diff --git a/ui/src/extensionsmanager.py b/ui/src/extensionsmanager.py deleted file mode 100644 index 397686a..0000000 --- a/ui/src/extensionsmanager.py +++ /dev/null @@ -1,72 +0,0 @@ -import pydbus -from gi.repository import GObject - -BREEZY_DESKTOP_UUID = "breezydesktop@xronlinux.com" -EXTENSION_STATE_ENABLED = 1 - -class ExtensionsManager(GObject.GObject): - __gproperties__ = { - 'breezy-enabled': (bool, 'Breezy Enabled', 'Whether the Breezy Desktop GNOME extension is enabled', False, GObject.ParamFlags.READWRITE) - } - - _instance = None - - @staticmethod - def get_instance(): - if ExtensionsManager._instance is None: - ExtensionsManager._instance = ExtensionsManager() - return ExtensionsManager._instance - - def __init__(self): - GObject.GObject.__init__(self) - - self.bus = pydbus.SessionBus() - self.gnome_shell_extensions = self.bus.get("org.gnome.Shell.Extensions") - self.gnome_shell_extensions.ExtensionStateChanged.connect(self._handle_extension_state_change) - - self.remote_extension_state = self.is_enabled() - - def _handle_extension_state_change(self, extension_uuid, state): - if extension_uuid == BREEZY_DESKTOP_UUID: - self.remote_extension_state = state.get('state') == EXTENSION_STATE_ENABLED - self.set_property('breezy-enabled', self.remote_extension_state) - - def is_installed(self): - return self._is_installed(BREEZY_DESKTOP_UUID) - - def enable(self): - self._enable_extension(BREEZY_DESKTOP_UUID) - - def disable(self): - self._disable_extension(BREEZY_DESKTOP_UUID) - - def is_enabled(self): - return self._is_enabled(BREEZY_DESKTOP_UUID) - - def _is_installed(self, extension_uuid): - extensions_result = self.gnome_shell_extensions.ListExtensions() - for extension in extensions_result: - if extension == extension_uuid: - return True - - return False - - def _enable_extension(self, extension_uuid): - if not self.gnome_shell_extensions.UserExtensionsEnabled: - self.gnome_shell_extensions.UserExtensionsEnabled = True - - self.gnome_shell_extensions.EnableExtension(extension_uuid) - - def _disable_extension(self, extension_uuid): - self.gnome_shell_extensions.DisableExtension(extension_uuid) - - def _is_enabled(self, extension_uuid): - return self.gnome_shell_extensions.GetExtensionInfo(extension_uuid).get('state') == EXTENSION_STATE_ENABLED - - def do_set_property(self, prop, value): - if prop.name == 'breezy-enabled' and value != self.remote_extension_state: - self.enable() if value == True else self.disable() - - def do_get_property(self, prop): - if prop.name == 'breezy-enabled': - return self.remote_extension_state diff --git a/ui/src/files.py b/ui/src/files.py index 367a39d..6cf3c05 100644 --- a/ui/src/files.py +++ b/ui/src/files.py @@ -10,8 +10,11 @@ def get_config_dir(): return os.path.expanduser(config_home) def get_state_dir(): + # imported lazily to avoid an import cycle (runtime discovery imports + # modules that import this one) + from .runtime import runtime_namespace state_home = os.environ.get('XDG_STATE_HOME', '~/.local/state') - return os.path.join(os.path.expanduser(state_home), 'breezy_gnome') + return os.path.join(os.path.expanduser(state_home), runtime_namespace()) def get_data_home(): data_home = os.environ.get('XDG_DATA_HOME', '~/.local/share') diff --git a/ui/src/meson.build b/ui/src/meson.build index 95187b7..5d5b4e6 100644 --- a/ui/src/meson.build +++ b/ui/src/meson.build @@ -42,7 +42,6 @@ breezydesktop_sources = [ 'connecteddevice.py', 'customresolutiondialog.py', 'customresolutiondialogcontent.py', - 'extensionsmanager.py', 'displaydistancedialog.py', 'displaydistancedialogcontent.py', 'failedverification.py', @@ -57,16 +56,18 @@ breezydesktop_sources = [ 'nodriver.py', 'noextension.py', 'nolicense.py', + 'runtime.py', + 'runtimeenvironment.py', 'settingsmanager.py', 'shortcutdialog.py', 'statemanager.py', 'time.py', - 'updatechecker.py', - 'virtualdisplay.py', - 'virtualdisplaymanager.py', - 'verify.py', 'window.py' ] install_data(breezydesktop_sources, install_dir: moduledir) -install_subdir('../lib', install_dir: moduledir) \ No newline at end of file +install_subdir('../lib', install_dir: moduledir) + +# the runtimes subpackage marker; a concrete RuntimeEnvironment implementation +# is copied in per-build by the package script (see ui/bin/package). +install_data('runtimes/__init__.py', install_dir: moduledir / 'runtimes') \ No newline at end of file diff --git a/ui/src/nodevice.py b/ui/src/nodevice.py index fa56bfe..70ea98b 100644 --- a/ui/src/nodevice.py +++ b/ui/src/nodevice.py @@ -1,6 +1,6 @@ from gi.repository import Gio, Gtk from .configmanager import ConfigManager -from .extensionsmanager import ExtensionsManager +from .runtimeenvironment import RuntimeEnvironment from .settingsmanager import SettingsManager from .statemanager import StateManager from .xrdriveripc import XRDriverIPC @@ -18,7 +18,7 @@ class NoDevice(Gtk.Box): self.init_template() self.ipc = XRDriverIPC.get_instance() - self.extensions_manager = ExtensionsManager.get_instance() + self.runtime = RuntimeEnvironment.get_instance() self.settings = SettingsManager.get_instance().settings self.config_manager = ConfigManager.get_instance() self.config_manager.connect('notify::breezy-desktop-enabled', self._handle_enabled_config) @@ -30,16 +30,16 @@ class NoDevice(Gtk.Box): self._handle_enabled_config(self.config_manager, None) def _handle_enabled_config(self, config_manager, val): - enabled = config_manager.get_property('breezy-desktop-enabled') and self.extensions_manager.get_property('breezy-enabled') + enabled = config_manager.get_property('breezy-desktop-enabled') and self.runtime.get_property('breezy-enabled') if enabled != self.effect_enable_switch.get_active(): self.effect_enable_switch.set_active(enabled) - + def _handle_switch_enabled_state(self, switch, param): requesting_enabled = switch.get_active() # never turn off the extension, disabling the effect is done via configs only if requesting_enabled: - self.extensions_manager.set_property('breezy-enabled', True) + self.runtime.set_property('breezy-enabled', True) self.config_manager.set_property('breezy-desktop-enabled', requesting_enabled) diff --git a/ui/src/runtime.py b/ui/src/runtime.py new file mode 100644 index 0000000..5ea3228 --- /dev/null +++ b/ui/src/runtime.py @@ -0,0 +1,63 @@ +"""Runtime environment discovery. + +Exactly one concrete :class:`~breezydesktop.runtimeenvironment.RuntimeEnvironment` +implementation is bundled into the ``runtimes`` subpackage at package time. This +module finds it (the first one it sees) and exposes both the class and a cheap +way to read its namespace without constructing the (potentially side-effectful) +instance. +""" + +import importlib +import inspect +import logging +import pkgutil + +from .runtimeenvironment import RuntimeEnvironment, DEFAULT_APP_NAMESPACE + +logger = logging.getLogger('breezy_ui') + +_runtime_class = None + + +def get_runtime_class(): + """Return the active RuntimeEnvironment subclass. + + Scans the bundled ``runtimes`` subpackage and returns the first concrete + RuntimeEnvironment subclass found. The result is cached. Raises + RuntimeError if no implementation is bundled. + """ + global _runtime_class + if _runtime_class is not None: + return _runtime_class + + from . import runtimes + + for module_info in pkgutil.iter_modules(runtimes.__path__, runtimes.__name__ + '.'): + try: + module = importlib.import_module(module_info.name) + except Exception as e: + logger.error("Failed to import runtime module %s: %s", module_info.name, e) + continue + + for _, obj in inspect.getmembers(module, inspect.isclass): + if issubclass(obj, RuntimeEnvironment) and obj is not RuntimeEnvironment \ + and obj.__module__ == module_info.name: + logger.info("Using runtime environment %s", obj.__name__) + _runtime_class = obj + return _runtime_class + + raise RuntimeError( + "No RuntimeEnvironment implementation was found in the 'runtimes' package. " + "A runtime implementation must be bundled at package time.") + + +def runtime_namespace(): + """Return the active runtime's application namespace. + + Falls back to the default namespace if no runtime is bundled, so early + bootstrap paths (e.g. log directory setup) never fail. + """ + try: + return get_runtime_class().app_namespace() + except RuntimeError: + return DEFAULT_APP_NAMESPACE diff --git a/ui/src/runtimeenvironment.py b/ui/src/runtimeenvironment.py new file mode 100644 index 0000000..d365136 --- /dev/null +++ b/ui/src/runtimeenvironment.py @@ -0,0 +1,169 @@ +import gettext + +from gi.repository import GObject + +_ = gettext.gettext + +# Default namespace used for XDG directories, application identity, etc. A +# concrete RuntimeEnvironment should override APP_NAMESPACE. +DEFAULT_APP_NAMESPACE = 'breezy_gnome' + + +class NullVirtualDisplayManager(GObject.GObject): + """A no-op virtual display manager. + + Provides the same interface (the 'displays' property + change + notifications, plus create/destroy methods) that the UI binds to, but + never creates anything. Runtime environments that don't support virtual + displays can use this so the UI degrades gracefully. + """ + __gproperties__ = { + 'displays': (object, 'Displays', 'A list of the displays', GObject.ParamFlags.READWRITE) + } + + def __init__(self): + GObject.GObject.__init__(self) + self._displays = [] + + def create_virtual_display(self, width, height, framerate): + return None + + def destroy_virtual_display(self, pid): + return False + + def do_set_property(self, prop, value): + if prop.name == 'displays': + self._displays = value + + def do_get_property(self, prop): + if prop.name == 'displays': + return self._displays + + +class RuntimeEnvironment(GObject.GObject): + """Abstraction over the host environment the UI is running in. + + A RuntimeEnvironment encapsulates everything that differs between the + environments Breezy can run in (e.g. GNOME Shell vs. a headless Breezy Box): + how the effect is enabled, how the installation is verified, whether/how + updates are checked, how virtual displays are managed, and which optional + views and fields the UI should present. + + The first concrete subclass discovered in the bundled ``runtimes`` package + (see :mod:`breezydesktop.runtime`) is instantiated as the active + environment, so a build can swap behavior simply by packaging a different + implementation. + + Subclasses inherit the ``breezy-enabled`` GObject property; override + :meth:`enable`/:meth:`disable`/:meth:`is_enabled` (and, if needed, + ``do_set_property``/``do_get_property``) to back it with real state. + """ + + # The application/namespace identifier, e.g. 'breezy_gnome' or 'breezy_box'. + # Used for namespacing XDG directories and similar. Subclasses must set + # this. + APP_NAMESPACE = None + + __gproperties__ = { + 'breezy-enabled': (bool, 'Breezy Enabled', 'Whether the Breezy Desktop effect is enabled', False, GObject.ParamFlags.READWRITE) + } + + _instance = None + + @classmethod + def get_instance(cls): + if RuntimeEnvironment._instance is None: + from .runtime import get_runtime_class + RuntimeEnvironment._instance = get_runtime_class()() + return RuntimeEnvironment._instance + + def __init__(self): + GObject.GObject.__init__(self) + self._breezy_enabled = False + self._virtual_display_manager = None + + # --- identity --------------------------------------------------------- + + @classmethod + def app_namespace(cls): + return cls.APP_NAMESPACE or DEFAULT_APP_NAMESPACE + + # --- effect enablement (backs the 'breezy-enabled' property) ---------- + + def is_installed(self): + """Whether the supporting components for this environment are installed. + + Environments with no separate component to install (e.g. a headless + box where the runtime is always present) should return True. + """ + return True + + def is_enabled(self): + return self._breezy_enabled + + def enable(self): + self._breezy_enabled = True + + def disable(self): + self._breezy_enabled = False + + # --- verification ----------------------------------------------------- + + def verify(self): + """Verify the installation. Return True when verification passes (or + when the environment has no verification step).""" + return True + + # --- update checking -------------------------------------------------- + + def check_for_update(self, current_version, callback): + """Asynchronously check for a newer version. + + Implementations that support updates should invoke + ``callback(latest_version_str)`` when a newer version is available. + The default is a no-op (no update prompt). + """ + return None + + # --- optional views / fields ------------------------------------------ + + @property + def shows_no_device_view(self): + """Whether a dedicated "no device connected" view should be shown. + + When False, the connected-device view is always shown and + :meth:`no_device_label` supplies the label used when no device is + actually connected. + """ + return False + + def no_device_label(self): + """Label shown in place of a device name when no device is connected + (only relevant when :attr:`shows_no_device_view` is False).""" + return _("No supported glasses connected") + + # --- virtual displays ------------------------------------------------- + + def is_virtual_display_supported(self): + return False + + def _create_virtual_display_manager(self): + """Build the virtual display manager for this environment. Override to + provide a real implementation.""" + return NullVirtualDisplayManager() + + @property + def virtual_display_manager(self): + if self._virtual_display_manager is None: + self._virtual_display_manager = self._create_virtual_display_manager() + return self._virtual_display_manager + + # --- GObject property plumbing ---------------------------------------- + + def do_set_property(self, prop, value): + if prop.name == 'breezy-enabled' and value != self.is_enabled(): + self.enable() if value else self.disable() + + def do_get_property(self, prop): + if prop.name == 'breezy-enabled': + return self.is_enabled() diff --git a/ui/src/runtimes/__init__.py b/ui/src/runtimes/__init__.py new file mode 100644 index 0000000..69f7dba --- /dev/null +++ b/ui/src/runtimes/__init__.py @@ -0,0 +1,7 @@ +"""Bundled runtime environment implementations. + +Exactly one concrete RuntimeEnvironment implementation module is copied into +this package at package time (see ui/bin/package). The implementation is +selected per-build from a runtime source directory (e.g. gnome/ui), so behavior +can be swapped without touching the core UI. +""" diff --git a/ui/src/virtualdisplay.in b/ui/src/virtualdisplay.in index a84e98b..132a966 100755 --- a/ui/src/virtualdisplay.in +++ b/ui/src/virtualdisplay.in @@ -85,8 +85,8 @@ def create_display(width, height, framerate): sys.exit(1) if __name__ == "__main__": - from breezydesktop import virtualdisplay - from breezydesktop.virtualdisplay import VirtualDisplay + from breezydesktop.runtimes import virtualdisplay + from breezydesktop.runtimes.virtualdisplay import VirtualDisplay global virtual_display_instance global loop diff --git a/ui/src/virtualdisplayrow.py b/ui/src/virtualdisplayrow.py index ef4593a..de0dc42 100644 --- a/ui/src/virtualdisplayrow.py +++ b/ui/src/virtualdisplayrow.py @@ -1,5 +1,5 @@ from gi.repository import Adw, Gtk -from .virtualdisplaymanager import VirtualDisplayManager +from .runtimeenvironment import RuntimeEnvironment import gettext @@ -28,4 +28,4 @@ class VirtualDisplayRow(Adw.ActionRow): self.remove_virtual_display_button.connect('clicked', self._remove_virtual_display) def _remove_virtual_display(self, widget): - VirtualDisplayManager.get_instance().destroy_virtual_display(self.pid) \ No newline at end of file + RuntimeEnvironment.get_instance().virtual_display_manager.destroy_virtual_display(self.pid) \ No newline at end of file diff --git a/ui/src/window.py b/ui/src/window.py index 3a5cf47..fca8d42 100644 --- a/ui/src/window.py +++ b/ui/src/window.py @@ -1,7 +1,7 @@ from gi.repository import Gtk, GLib -from .extensionsmanager import ExtensionsManager from .license import BREEZY_GNOME_FEATURES from .licensedialog import LicenseDialog +from .runtimeenvironment import RuntimeEnvironment from .statemanager import StateManager from .settingsmanager import SettingsManager from .connecteddevice import ConnectedDevice @@ -10,8 +10,6 @@ from .nodevice import NoDevice from .nodriver import NoDriver from .noextension import NoExtension from .nolicense import NoLicense -from .updatechecker import check_for_update -from .verify import verify_installation @Gtk.Template(resource_path='/com/xronlinux/BreezyDesktop/gtk/window.ui') class BreezydesktopWindow(Gtk.ApplicationWindow): @@ -29,6 +27,7 @@ class BreezydesktopWindow(Gtk.ApplicationWindow): def __init__(self, version, skip_verification, **kwargs): super().__init__(**kwargs) + self.runtime = RuntimeEnvironment.get_instance() self.connected_device = ConnectedDevice() self.failed_verification = FailedVerification() self.no_device = NoDevice() @@ -57,7 +56,7 @@ class BreezydesktopWindow(Gtk.ApplicationWindow): self.connect("destroy", self._on_window_destroy) - check_for_update(version, self._on_update_check_result) + self.runtime.check_for_update(version, self._on_update_check_result) def _handle_settings_update(self, settings_manager, key): self._handle_state_update(self.state_manager, None) @@ -82,19 +81,19 @@ class BreezydesktopWindow(Gtk.ApplicationWindow): if self.settings.get_boolean('debug-no-device'): self.main_content.append(self.connected_device) self.connected_device.set_device_name('Fake device') - elif not self._skip_verification and not verify_installation(): + elif not self._skip_verification and not self.runtime.verify(): self.main_content.append(self.failed_verification) - elif not ExtensionsManager.get_instance().is_installed(): + elif not self.runtime.is_installed(): self.main_content.append(self.no_extension) elif not self.state_manager.driver_running: self.main_content.append(self.no_driver) elif not self.state_manager.license_present: self.main_content.append(self.no_license) - elif not state_manager.connected_device_name: + elif not state_manager.connected_device_name and self.runtime.shows_no_device_view: self.main_content.append(self.no_device) else: self.main_content.append(self.connected_device) - self.connected_device.set_device_name(state_manager.connected_device_name) + self.connected_device.set_device_name(state_manager.connected_device_name or self.runtime.no_device_label()) self.set_resizable(True) self.set_default_size(1, 1)