Pull gnome-specific portions of UI out so UI package can be reused for breezy-box

This commit is contained in:
wheaney 2026-06-12 13:18:13 -07:00
parent 254fff37ff
commit ece35f9af0
18 changed files with 400 additions and 111 deletions

View File

@ -97,7 +97,7 @@ popd
UI_BUILD_ARTIFACT=$UI_DIR/out/breezyUI-$ARCH.tar.gz UI_BUILD_ARTIFACT=$UI_DIR/out/breezyUI-$ARCH.tar.gz
if [ ! -e "$UI_BUILD_ARTIFACT" ] || [ "$1" == "--rebuild-ui" ] || [ "$1" == "--rebuild-all" ]; then if [ ! -e "$UI_BUILD_ARTIFACT" ] || [ "$1" == "--rebuild-ui" ] || [ "$1" == "--rebuild-all" ]; then
pushd $UI_DIR pushd $UI_DIR
bin/package $ARCH RUNTIME_DIR=$GNOME_DIR/ui bin/package $ARCH
popd popd
fi fi
tar -xf $UI_BUILD_ARTIFACT -C $PACKAGE_DIR tar -xf $UI_BUILD_ARTIFACT -C $PACKAGE_DIR

View File

@ -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

View File

@ -2,7 +2,7 @@ import logging
import os import os
import subprocess import subprocess
from .files import get_bin_home from ..files import get_bin_home
logger = logging.getLogger('breezy_ui') logger = logging.getLogger('breezy_ui')

View File

@ -12,7 +12,7 @@ logger = logging.getLogger('breezy_ui')
gi.require_version('GLib', '2.0') gi.require_version('GLib', '2.0')
from gi.repository import GLib, GObject from gi.repository import GLib, GObject
from .files import get_bin_home from ..files import get_bin_home
class VirtualDisplayManager(GObject.GObject): class VirtualDisplayManager(GObject.GObject):
__gproperties__ = { __gproperties__ = {

View File

@ -6,6 +6,18 @@ set -e
ARCH=${ARCH:-$(uname -m)} ARCH=${ARCH:-$(uname -m)}
echo "Building Breezy UI for $ARCH" 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 BUILD_PATH=build
UI_BUILD_PATH=$BUILD_PATH/ui UI_BUILD_PATH=$BUILD_PATH/ui
PACKAGE_DIR=$BUILD_PATH/breezy_ui PACKAGE_DIR=$BUILD_PATH/breezy_ui
@ -38,6 +50,12 @@ mkdir -p $PACKAGE_SCHEMAS_DIR
cp src/*.py $PACKAGE_BREEZY_SRC_DIR cp src/*.py $PACKAGE_BREEZY_SRC_DIR
cp -r lib $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 -L modules/PyXRLinuxDriverIPC/xrdriveripc.py $PACKAGE_BREEZY_SRC_DIR
cp $UI_BUILD_PATH/src/breezydesktop.gresource $PACKAGE_BREEZY_DIR cp $UI_BUILD_PATH/src/breezydesktop.gresource $PACKAGE_BREEZY_DIR
cp -r po/mo/* $PACKAGE_LOCALE_DIR cp -r po/mo/* $PACKAGE_LOCALE_DIR

View File

@ -2,14 +2,12 @@ from gi.repository import Gio, GLib, Gtk, GObject
from .configmanager import ConfigManager from .configmanager import ConfigManager
from .customresolutiondialog import CustomResolutionDialog from .customresolutiondialog import CustomResolutionDialog
from .displaydistancedialog import DisplayDistanceDialog from .displaydistancedialog import DisplayDistanceDialog
from .extensionsmanager import ExtensionsManager
from .files import get_state_dir from .files import get_state_dir
from .license import BREEZY_GNOME_FEATURES from .license import BREEZY_GNOME_FEATURES
from .runtimeenvironment import RuntimeEnvironment
from .settingsmanager import SettingsManager from .settingsmanager import SettingsManager
from .shortcutdialog import bind_shortcut_settings from .shortcutdialog import bind_shortcut_settings
from .statemanager import StateManager from .statemanager import StateManager
from .virtualdisplaymanager import VirtualDisplayManager
from .virtualdisplay import is_screencast_available
from .virtualdisplayrow import VirtualDisplayRow from .virtualdisplayrow import VirtualDisplayRow
from .xrdriveripc import XRDriverIPC from .xrdriveripc import XRDriverIPC
@ -115,8 +113,8 @@ class ConnectedDevice(Gtk.Box):
self.settings = SettingsManager.get_instance().settings self.settings = SettingsManager.get_instance().settings
self.desktop_settings = SettingsManager.get_instance().desktop_settings self.desktop_settings = SettingsManager.get_instance().desktop_settings
self.ipc = XRDriverIPC.get_instance() self.ipc = XRDriverIPC.get_instance()
self.virtual_display_manager = VirtualDisplayManager.get_instance() self.runtime = RuntimeEnvironment.get_instance()
self.extensions_manager = ExtensionsManager.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.bind('disable-physical-displays', self.disable_physical_displays_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
self.settings.connect('changed::display-distance', self._handle_display_distance) 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_device_supports_sbs(self.state_manager, None)
self._handle_enabled_config(None, None) self._handle_enabled_config(None, None)
self._refresh_use_optimal_monitor_config(self.use_optimal_monitor_config_switch, 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 self._settings_displays_app_info = None
@ -229,9 +227,6 @@ class ConnectedDevice(Gtk.Box):
for id in self._custom_resolution_options: for id in self._custom_resolution_options:
self.add_virtual_display_menu.insert(self._default_resolution_options_count, id, id) 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): def _bind_scale_to_config(self, scale, config_key):
self.config_manager.bind_property(config_key, scale, 'value', Gio.SettingsBindFlags.DEFAULT) self.config_manager.bind_property(config_key, scale, 'value', Gio.SettingsBindFlags.DEFAULT)
scale.set_value(self.config_manager.get_property(config_key)) 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) # self.widescreen_mode_row.set_subtitle(subtitle)
def _handle_enabled_config(self, object, val): 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(): if enabled != self.effect_enable_switch.get_active():
self.effect_enable_switch.set_active(enabled) 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 # never turn off the extension, disabling the effect is done via configs only
if requesting_enabled: 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) self.config_manager.set_property('breezy-desktop-enabled', requesting_enabled)
for widget in self.all_enabled_state_inputs: for widget in self.all_enabled_state_inputs:
widget.set_sensitive(requesting_enabled) 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( 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.")) _("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) self.add_virtual_display_button.set_sensitive(False)

View File

@ -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

View File

@ -10,8 +10,11 @@ def get_config_dir():
return os.path.expanduser(config_home) return os.path.expanduser(config_home)
def get_state_dir(): 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') 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(): def get_data_home():
data_home = os.environ.get('XDG_DATA_HOME', '~/.local/share') data_home = os.environ.get('XDG_DATA_HOME', '~/.local/share')

View File

@ -42,7 +42,6 @@ breezydesktop_sources = [
'connecteddevice.py', 'connecteddevice.py',
'customresolutiondialog.py', 'customresolutiondialog.py',
'customresolutiondialogcontent.py', 'customresolutiondialogcontent.py',
'extensionsmanager.py',
'displaydistancedialog.py', 'displaydistancedialog.py',
'displaydistancedialogcontent.py', 'displaydistancedialogcontent.py',
'failedverification.py', 'failedverification.py',
@ -57,16 +56,18 @@ breezydesktop_sources = [
'nodriver.py', 'nodriver.py',
'noextension.py', 'noextension.py',
'nolicense.py', 'nolicense.py',
'runtime.py',
'runtimeenvironment.py',
'settingsmanager.py', 'settingsmanager.py',
'shortcutdialog.py', 'shortcutdialog.py',
'statemanager.py', 'statemanager.py',
'time.py', 'time.py',
'updatechecker.py',
'virtualdisplay.py',
'virtualdisplaymanager.py',
'verify.py',
'window.py' 'window.py'
] ]
install_data(breezydesktop_sources, install_dir: moduledir) install_data(breezydesktop_sources, install_dir: moduledir)
install_subdir('../lib', install_dir: moduledir) 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')

View File

@ -1,6 +1,6 @@
from gi.repository import Gio, Gtk from gi.repository import Gio, Gtk
from .configmanager import ConfigManager from .configmanager import ConfigManager
from .extensionsmanager import ExtensionsManager from .runtimeenvironment import RuntimeEnvironment
from .settingsmanager import SettingsManager from .settingsmanager import SettingsManager
from .statemanager import StateManager from .statemanager import StateManager
from .xrdriveripc import XRDriverIPC from .xrdriveripc import XRDriverIPC
@ -18,7 +18,7 @@ class NoDevice(Gtk.Box):
self.init_template() self.init_template()
self.ipc = XRDriverIPC.get_instance() self.ipc = XRDriverIPC.get_instance()
self.extensions_manager = ExtensionsManager.get_instance() self.runtime = RuntimeEnvironment.get_instance()
self.settings = SettingsManager.get_instance().settings self.settings = SettingsManager.get_instance().settings
self.config_manager = ConfigManager.get_instance() self.config_manager = ConfigManager.get_instance()
self.config_manager.connect('notify::breezy-desktop-enabled', self._handle_enabled_config) 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) self._handle_enabled_config(self.config_manager, None)
def _handle_enabled_config(self, config_manager, val): 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(): if enabled != self.effect_enable_switch.get_active():
self.effect_enable_switch.set_active(enabled) self.effect_enable_switch.set_active(enabled)
def _handle_switch_enabled_state(self, switch, param): def _handle_switch_enabled_state(self, switch, param):
requesting_enabled = switch.get_active() requesting_enabled = switch.get_active()
# never turn off the extension, disabling the effect is done via configs only # never turn off the extension, disabling the effect is done via configs only
if requesting_enabled: 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) self.config_manager.set_property('breezy-desktop-enabled', requesting_enabled)

63
ui/src/runtime.py Normal file
View File

@ -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

View File

@ -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()

View File

@ -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.
"""

View File

@ -85,8 +85,8 @@ def create_display(width, height, framerate):
sys.exit(1) sys.exit(1)
if __name__ == "__main__": if __name__ == "__main__":
from breezydesktop import virtualdisplay from breezydesktop.runtimes import virtualdisplay
from breezydesktop.virtualdisplay import VirtualDisplay from breezydesktop.runtimes.virtualdisplay import VirtualDisplay
global virtual_display_instance global virtual_display_instance
global loop global loop

View File

@ -1,5 +1,5 @@
from gi.repository import Adw, Gtk from gi.repository import Adw, Gtk
from .virtualdisplaymanager import VirtualDisplayManager from .runtimeenvironment import RuntimeEnvironment
import gettext import gettext
@ -28,4 +28,4 @@ class VirtualDisplayRow(Adw.ActionRow):
self.remove_virtual_display_button.connect('clicked', self._remove_virtual_display) self.remove_virtual_display_button.connect('clicked', self._remove_virtual_display)
def _remove_virtual_display(self, widget): def _remove_virtual_display(self, widget):
VirtualDisplayManager.get_instance().destroy_virtual_display(self.pid) RuntimeEnvironment.get_instance().virtual_display_manager.destroy_virtual_display(self.pid)

View File

@ -1,7 +1,7 @@
from gi.repository import Gtk, GLib from gi.repository import Gtk, GLib
from .extensionsmanager import ExtensionsManager
from .license import BREEZY_GNOME_FEATURES from .license import BREEZY_GNOME_FEATURES
from .licensedialog import LicenseDialog from .licensedialog import LicenseDialog
from .runtimeenvironment import RuntimeEnvironment
from .statemanager import StateManager from .statemanager import StateManager
from .settingsmanager import SettingsManager from .settingsmanager import SettingsManager
from .connecteddevice import ConnectedDevice from .connecteddevice import ConnectedDevice
@ -10,8 +10,6 @@ from .nodevice import NoDevice
from .nodriver import NoDriver from .nodriver import NoDriver
from .noextension import NoExtension from .noextension import NoExtension
from .nolicense import NoLicense 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') @Gtk.Template(resource_path='/com/xronlinux/BreezyDesktop/gtk/window.ui')
class BreezydesktopWindow(Gtk.ApplicationWindow): class BreezydesktopWindow(Gtk.ApplicationWindow):
@ -29,6 +27,7 @@ class BreezydesktopWindow(Gtk.ApplicationWindow):
def __init__(self, version, skip_verification, **kwargs): def __init__(self, version, skip_verification, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.runtime = RuntimeEnvironment.get_instance()
self.connected_device = ConnectedDevice() self.connected_device = ConnectedDevice()
self.failed_verification = FailedVerification() self.failed_verification = FailedVerification()
self.no_device = NoDevice() self.no_device = NoDevice()
@ -57,7 +56,7 @@ class BreezydesktopWindow(Gtk.ApplicationWindow):
self.connect("destroy", self._on_window_destroy) 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): def _handle_settings_update(self, settings_manager, key):
self._handle_state_update(self.state_manager, None) self._handle_state_update(self.state_manager, None)
@ -82,19 +81,19 @@ class BreezydesktopWindow(Gtk.ApplicationWindow):
if self.settings.get_boolean('debug-no-device'): if self.settings.get_boolean('debug-no-device'):
self.main_content.append(self.connected_device) self.main_content.append(self.connected_device)
self.connected_device.set_device_name('Fake 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) 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) self.main_content.append(self.no_extension)
elif not self.state_manager.driver_running: elif not self.state_manager.driver_running:
self.main_content.append(self.no_driver) self.main_content.append(self.no_driver)
elif not self.state_manager.license_present: elif not self.state_manager.license_present:
self.main_content.append(self.no_license) 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) self.main_content.append(self.no_device)
else: else:
self.main_content.append(self.connected_device) 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_resizable(True)
self.set_default_size(1, 1) self.set_default_size(1, 1)