Compare commits

...

13 Commits

17 changed files with 1026 additions and 375 deletions

2
.gitmodules vendored
View File

@ -5,9 +5,11 @@
[submodule "modules/XRLinuxDriver"] [submodule "modules/XRLinuxDriver"]
path = modules/XRLinuxDriver path = modules/XRLinuxDriver
url = https://github.com/wheaney/XRLinuxDriver.git url = https://github.com/wheaney/XRLinuxDriver.git
branch = breezy_sbs
[submodule "modules/sombrero"] [submodule "modules/sombrero"]
path = modules/sombrero path = modules/sombrero
url = https://github.com/wheaney/sombrero.git url = https://github.com/wheaney/sombrero.git
branch = breezy_sbs
[submodule "ui/modules/PyXRLinuxDriverIPC"] [submodule "ui/modules/PyXRLinuxDriverIPC"]
path = ui/modules/PyXRLinuxDriverIPC path = ui/modules/PyXRLinuxDriverIPC
url = https://github.com/wheaney/PyXRLinuxDriverIPC.git url = https://github.com/wheaney/PyXRLinuxDriverIPC.git

View File

@ -6,6 +6,7 @@ SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
if [ -z "$XDG_DATA_HOME" ]; then if [ -z "$XDG_DATA_HOME" ]; then
XDG_DATA_HOME="$USER_HOME/.local/share" XDG_DATA_HOME="$USER_HOME/.local/share"
fi fi
DATA_DIR="$XDG_DATA_HOME/breezy_gnome"
# if $XDG_DATA_HOME/gnome-shell/extensions/breezydesktop@xronlinux.com exists # if $XDG_DATA_HOME/gnome-shell/extensions/breezydesktop@xronlinux.com exists
extension_path="$XDG_DATA_HOME/gnome-shell/extensions/breezydesktop@xronlinux.com" extension_path="$XDG_DATA_HOME/gnome-shell/extensions/breezydesktop@xronlinux.com"
@ -17,4 +18,12 @@ fi
# recursively copy the $SCRIPT_DIR/../../src to extension_path, don't preserve symlinks # recursively copy the $SCRIPT_DIR/../../src to extension_path, don't preserve symlinks
cp -rL $SCRIPT_DIR/../../src $extension_path cp -rL $SCRIPT_DIR/../../src $extension_path
glib-compile-schemas $extension_path/schemas glib-compile-schemas $extension_path/schemas
pushd $extension_path
GNOME_MANIFEST_LINE=$(find -L . -type f ! -name "*.compiled" -exec sha256sum {} \; | sort | sha256sum | sed 's/ .*//')
popd
pushd $DATA_DIR
echo -e "$GNOME_MANIFEST_LINE breezydesktop@xronlinux.com" > manifest
popd

View File

@ -1,4 +1,5 @@
import Clutter from 'gi://Clutter'; import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import Meta from 'gi://Meta'; import Meta from 'gi://Meta';
import * as PointerWatcher from 'resource:///org/gnome/shell/ui/pointerWatcher.js'; import * as PointerWatcher from 'resource:///org/gnome/shell/ui/pointerWatcher.js';
import { MouseSpriteContent } from './cursor.js'; import { MouseSpriteContent } from './cursor.js';
@ -6,8 +7,9 @@ import Globals from './globals.js';
// Taken from https://github.com/jkitching/soft-brightness-plus // Taken from https://github.com/jkitching/soft-brightness-plus
export class CursorManager { export class CursorManager {
constructor(mainActor) { constructor(mainActor, refreshRate) {
this._mainActor = mainActor; this._mainActor = mainActor;
this._refreshRate = refreshRate;
this._changeHookFn = null; this._changeHookFn = null;
@ -18,15 +20,14 @@ export class CursorManager {
this._cursorTrackerSetPointerVisibleBound = null; this._cursorTrackerSetPointerVisibleBound = null;
this._cursorSprite = null; this._cursorSprite = null;
this._cursorActor = null; this._cursorActor = null;
this._cursorWatcher = null;
this._cursorSeat = null; this._cursorSeat = null;
this._cursorUnfocusInhibited = false; this._cursorUnfocusInhibited = false;
// Set/destroyed by _startCloningMouse / _stopCloningMouse // Set/destroyed by _startCloningMouse / _stopCloningMouse
this._cursorWatch = null;
this._cursorChangedConnection = null; this._cursorChangedConnection = null;
this._cursorVisibilityChangedConnection = null; this._cursorVisibilityChangedConnection = null;
this._cursorPositionInvalidatedConnection = null; this._moveToTopTimeout = null;
this._redraw_timeline = null;
} }
enable() { enable() {
@ -76,7 +77,6 @@ export class CursorManager {
} else { } else {
this._cursorActor.add_actor(this._cursorSprite); this._cursorActor.add_actor(this._cursorSprite);
} }
this._cursorWatcher = PointerWatcher.getPointerWatcher();
this._cursorSeat = Clutter.get_default_backend().get_default_seat(); this._cursorSeat = Clutter.get_default_backend().get_default_seat();
} }
@ -98,7 +98,6 @@ export class CursorManager {
this._cursorTrackerSetPointerVisibleBound = null; this._cursorTrackerSetPointerVisibleBound = null;
this._cursorSprite = null; this._cursorSprite = null;
this._cursorActor = null; this._cursorActor = null;
this._cursorWatcher = null;
this._cursorSeat = null; this._cursorSeat = null;
} }
@ -123,23 +122,41 @@ export class CursorManager {
// prereqs: setup in _enableCloningMouse, _cursorWantedVisible is true // prereqs: setup in _enableCloningMouse, _cursorWantedVisible is true
_startCloningMouse() { _startCloningMouse() {
Globals.logger.log_debug('CursorManager _startCloningMouse'); Globals.logger.log_debug('CursorManager _startCloningMouse');
if (this._cursorWatch == null) { if (Clutter.Container === undefined) {
if (Clutter.Container === undefined) { this._mainActor.add_child(this._cursorActor);
this._mainActor.add_child(this._cursorActor); } else {
} else { this._mainActor.add_actor(this._cursorActor);
this._mainActor.add_actor(this._cursorActor);
}
this._cursorChangedConnection = this._cursorTracker.connect('cursor-changed', this._updateMouseSprite.bind(this));
this._cursorVisibilityChangedConnection = this._cursorTracker.connect('visibility-changed', this._updateMouseSprite.bind(this));
this._cursorPositionInvalidatedConnection = this._cursorTracker.connect('position-invalidated', this._updateMouseSprite.bind(this));
const interval = 1000 / 250;
this._cursorWatch = this._cursorWatcher.addWatch(interval, this._updateMousePosition.bind(this));
const [x, y] = global.get_pointer();
this._updateMousePosition(x, y);
this._updateMouseSprite();
} }
this._cursorChangedConnection = this._cursorTracker.connect('cursor-changed', this._updateMouseSprite.bind(this));
this._cursorVisibilityChangedConnection = this._cursorTracker.connect('visibility-changed', this._handleVisibilityChanged.bind(this));
// Some elements will occasionally appear above the cursor, so we periodically reset the actor stacking.
// This could theoretically be fixed "better" by attaching to all events that might affect actor ordering,
// but finding a comprehensive list is difficult and not future proof. So this ugly solution helps us
// catch everything.
this._moveToTopTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, (() => {
this._moveToTop()
return GLib.SOURCE_CONTINUE;
}).bind(this));
const refreshInterval = 1000 / this._refreshRate;
// we'll force repaint the cursor every frame,
// this keeps the cursor up-to-date and is sort of a hack that's a critical part of making sure
// the XR Effect refreshes even if nothing on-screen has changed (bypass the texture caching)
this._redraw_timeline = Clutter.Timeline.new_for_actor(this._cursorActor, refreshInterval);
this._redraw_timeline.set_repeat_count(-1);
var on = false;
this._redraw_timeline.connect('completed', (() => {
this._cursorActor.set_opacity(this._cursorActor.opacity + (on ? 1 : -1));
const [x, y] = global.get_pointer();
this._cursorActor.set_position(x, y);
on = !on;
}).bind(this));
this._redraw_timeline.start();
this._updateMouseSprite();
if (this._cursorTracker.set_keep_focus_while_hidden) { if (this._cursorTracker.set_keep_focus_while_hidden) {
this._cursorTracker.set_keep_focus_while_hidden(true); this._cursorTracker.set_keep_focus_while_hidden(true);
@ -160,28 +177,30 @@ export class CursorManager {
// completely reverts _startCloningMouse // completely reverts _startCloningMouse
_stopCloningMouse() { _stopCloningMouse() {
Globals.logger.log_debug('CursorManager _stopCloningMouse'); Globals.logger.log_debug('CursorManager _stopCloningMouse');
if (this._cursorWatch != null) { if (this._redraw_timeline) {
this._cursorWatch.remove(); this._redraw_timeline.stop();
this._cursorWatch = null; this._redraw_timeline = null;
this._cursorTracker.disconnect(this._cursorChangedConnection);
this._cursorChangedConnection = null;
this._cursorTracker.disconnect(this._cursorVisibilityChangedConnection);
this._cursorVisibilityChangedConnection = null;
this._cursorTracker.disconnect(this._cursorPositionInvalidatedConnection);
this._cursorPositionInvalidatedConnection = null;
if (Clutter.Container === undefined) {
this._mainActor.remove_child(this._cursorActor);
} else {
this._mainActor.remove_actor(this._cursorActor);
}
} }
if (this._cursorTracker.set_keep_focus_while_hidden) { if (this._cursorChangedConnection) {
this._cursorTracker.set_keep_focus_while_hidden(false); this._cursorTracker.disconnect(this._cursorChangedConnection);
this._cursorChangedConnection = null;
}
if (this._cursorVisibilityChangedConnection) {
this._cursorTracker.disconnect(this._cursorVisibilityChangedConnection);
this._cursorVisibilityChangedConnection = null;
}
if (Clutter.Container === undefined) {
this._mainActor.remove_child(this._cursorActor);
} else {
this._mainActor.remove_actor(this._cursorActor);
}
if (this._moveToTopTimeout) {
GLib.source_remove(this._moveToTopTimeout);
this._moveToTopTimeout = null;
} }
if (this._cursorUnfocusInhibited) { if (this._cursorUnfocusInhibited) {
@ -191,10 +210,6 @@ export class CursorManager {
} }
} }
_updateMousePosition(x, y) {
this._cursorActor.set_position(x, y);
}
_updateMouseSprite() { _updateMouseSprite() {
const sprite = this._cursorTracker.get_sprite(); const sprite = this._cursorTracker.get_sprite();
if (sprite) { if (sprite) {
@ -209,8 +224,6 @@ export class CursorManager {
translation_x: -xHot, translation_x: -xHot,
translation_y: -yHot, translation_y: -yHot,
}); });
this._mainActor.set_child_above_sibling(this._cursorActor, null);
this._cursorTrackerSetPointerVisibleBound(false);
// some other processes are uninhibiting when they shouldn't, so we need to re-inhibit here // some other processes are uninhibiting when they shouldn't, so we need to re-inhibit here
if (!this._cursorSeat.is_unfocus_inhibited() && this._cursorUnfocusInhibited) { if (!this._cursorSeat.is_unfocus_inhibited() && this._cursorUnfocusInhibited) {
@ -218,4 +231,12 @@ export class CursorManager {
this._cursorSeat.inhibit_unfocus(); this._cursorSeat.inhibit_unfocus();
} }
} }
_handleVisibilityChanged() {
this._cursorTrackerSetPointerVisibleBound(false);
}
_moveToTop() {
this._mainActor.set_child_above_sibling(this._cursorActor, null);
}
} }

View File

@ -8,7 +8,8 @@ import St from 'gi://St';
import { CursorManager } from './cursormanager.js'; import { CursorManager } from './cursormanager.js';
import Globals from './globals.js'; import Globals from './globals.js';
import { Logger } from './logger.js'; import { Logger } from './logger.js';
import MonitorManager from './monitormanager.js'; import { MonitorManager } from './monitormanager.js';
import { isValidKeepAlive } from './time.js';
import { IPC_FILE_PATH, XREffect } from './xrEffect.js'; import { IPC_FILE_PATH, XREffect } from './xrEffect.js';
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js'; import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
@ -40,8 +41,16 @@ export default class BreezyDesktopExtension extends Extension {
this._distance_binding = null; this._distance_binding = null;
this._distance_connection = null; this._distance_connection = null;
this._follow_threshold_connection = null; this._follow_threshold_connection = null;
this._widescreen_mode_settings_connection = null;
this._widescreen_mode_effect_state_connection = null;
this._supported_device_detected_connected = null;
this._start_binding = null; this._start_binding = null;
this._end_binding = null; this._end_binding = null;
this._curved_display_binding = null;
this._display_size_binding = null;
this._look_ahead_override_binding = null;
this._optimal_monitor_config_binding = null;
this._headset_as_primary_binding = null;
if (!Globals.logger) { if (!Globals.logger) {
Globals.logger = new Logger({ Globals.logger = new Logger({
@ -59,13 +68,22 @@ export default class BreezyDesktopExtension extends Extension {
Globals.extension_dir = this.path; Globals.extension_dir = this.path;
this.settings.bind('debug', Globals.logger, 'debug', Gio.SettingsBindFlags.DEFAULT); this.settings.bind('debug', Globals.logger, 'debug', Gio.SettingsBindFlags.DEFAULT);
this._monitor_manager = new MonitorManager(this.path); this._monitor_manager = new MonitorManager({
this._monitor_manager.setChangeHook(this._setup.bind(this)); use_optimal_monitor_config: this.settings.get_boolean('use-optimal-monitor-config'),
headset_as_primary: this.settings.get_boolean('headset-as-primary'),
extension_path: this.path
});
this._monitor_manager.setChangeHook(this._handle_monitor_change.bind(this));
this._monitor_manager.enable(); this._monitor_manager.enable();
this._optimal_monitor_config_binding = this.settings.bind('use-optimal-monitor-config',
this._monitor_manager, 'use-optimal-monitor-config', Gio.SettingsBindFlags.DEFAULT);
this._headset_as_primary_binding = this.settings.bind('headset-as-primary',
this._monitor_manager, 'headset-as-primary', Gio.SettingsBindFlags.DEFAULT);
this._setup(); this._setup();
} catch (e) { } catch (e) {
Globals.logger.log(`ERROR: BreezyDesktopExtension enable ${e.message}`, e.stack); Globals.logger.log(`ERROR: BreezyDesktopExtension enable ${e.message}\n${e.stack}`);
} }
} }
@ -78,8 +96,12 @@ export default class BreezyDesktopExtension extends Extension {
const is_driver_running = this._check_driver_running(); const is_driver_running = this._check_driver_running();
if (is_driver_running && target_monitor) { if (is_driver_running && target_monitor) {
Globals.logger.log('Driver is running, supported monitor connected. Enabling XR effect.'); // don't enable the effect yet if an async optimal mode check is needed,
this._effect_enable(); // _setup will be triggered after a mode change so we can remove this timeout_add
if (target_monitor.is_dummy || !this._monitor_manager.checkOptimalMode(target_monitor.connector)) {
Globals.logger.log('Driver is running, supported monitor connected. Enabling XR effect.');
this._effect_enable();
}
return GLib.SOURCE_REMOVE; return GLib.SOURCE_REMOVE;
} else { } else {
return GLib.SOURCE_CONTINUE; return GLib.SOURCE_CONTINUE;
@ -93,23 +115,29 @@ export default class BreezyDesktopExtension extends Extension {
const target_monitor = this._monitor_manager.getMonitorPropertiesList()?.find( const target_monitor = this._monitor_manager.getMonitorPropertiesList()?.find(
monitor => SUPPORTED_MONITOR_PRODUCTS.includes(monitor.product)); monitor => SUPPORTED_MONITOR_PRODUCTS.includes(monitor.product));
if (target_monitor !== undefined) { if (target_monitor !== undefined) {
Globals.logger.log_debug(`BreezyDesktopExtension _find_supported_monitor - Identified supported monitor: ${target_monitor.connector}`);
return { return {
monitor: this._monitor_manager.getMonitors()[target_monitor.index], monitor: this._monitor_manager.getMonitors()[target_monitor.index],
refreshRate: target_monitor.refreshRate, connector: target_monitor.connector,
refreshRate: target_monitor.refreshRate
}; };
} }
if (this.settings.get_boolean('developer-mode')) { if (this.settings.get_boolean('developer-mode')) {
Globals.logger.log_debug('BreezyDesktopExtension _find_supported_monitor - Using dummy monitor');
// allow testing XR devices with just USB, no video needed // allow testing XR devices with just USB, no video needed
return { return {
monitor: this._monitor_manager.getMonitors()[0], monitor: this._monitor_manager.getMonitors()[0],
connector: 'dummy',
refreshRate: 60, refreshRate: 60,
is_dummy: true
}; };
} }
Globals.logger.log_debug('BreezyDesktopExtension _find_supported_monitor - No supported monitor found');
return null; return null;
} catch (e) { } catch (e) {
Globals.logger.log(`ERROR: BreezyDesktopExtension _find_supported_monitor ${e.message}`, e.stack); Globals.logger.log(`ERROR: BreezyDesktopExtension _find_supported_monitor ${e.message}\n${e.stack}`);
return null; return null;
} }
} }
@ -117,20 +145,26 @@ export default class BreezyDesktopExtension extends Extension {
_setup() { _setup() {
Globals.logger.log_debug('BreezyDesktopExtension _setup'); Globals.logger.log_debug('BreezyDesktopExtension _setup');
if (this._is_effect_running) { if (this._is_effect_running) {
Globals.logger.log('Monitors changed, disabling XR effect'); Globals.logger.log('Reset triggered, disabling XR effect');
this._effect_disable(); this._effect_disable();
} }
const target_monitor = this._find_supported_monitor(); const target_monitor = this._find_supported_monitor();
// if target_monitor isn't set, do nothing and wait for MonitorManager to call this again // if target_monitor isn't set, do nothing and wait for MonitorManager to call this again
if (target_monitor && this._running_poller_id === undefined) { if (target_monitor && this._running_poller_id === undefined) {
this._target_monitor = target_monitor.monitor; this._target_monitor = target_monitor;
this._refresh_rate = target_monitor.refreshRate;
if (this._check_driver_running()) { if (this._check_driver_running()) {
Globals.logger.log('Ready, enabling XR effect'); // don't enable the effect yet if an async optimal mode check is needed,
this._effect_enable(); // _setup will be triggered again after a mode change
if (target_monitor.is_dummy || !this._monitor_manager.checkOptimalMode(target_monitor.connector)) {
Globals.logger.log('Ready, enabling XR effect');
this._effect_enable();
} else {
Globals.logger.log_debug('BreezyDesktopExtension _setup - driver running but optimal mode check needed');
}
} else { } else {
Globals.logger.log_debug('BreezyDesktopExtension _setup - driver no running, starting poller');
this._poll_for_ready(); this._poll_for_ready();
} }
} }
@ -139,11 +173,18 @@ export default class BreezyDesktopExtension extends Extension {
_check_driver_running() { _check_driver_running() {
try { try {
if (!Globals.ipc_file) Globals.ipc_file = Gio.file_new_for_path(IPC_FILE_PATH); if (!Globals.ipc_file) Globals.ipc_file = Gio.file_new_for_path(IPC_FILE_PATH);
return Globals.ipc_file.query_exists(null); if (Globals.ipc_file.query_exists(null)) {
const file_info = Globals.ipc_file.query_info(Gio.FILE_ATTRIBUTE_TIME_MODIFIED, Gio.FileQueryInfoFlags.NONE, null);
const file_modified_time = file_info.get_attribute_uint64(Gio.FILE_ATTRIBUTE_TIME_MODIFIED);
// when the driver is running, the IMU file is updated at least 60x per second, do a strict check
return isValidKeepAlive(file_modified_time, true);
}
} catch (e) { } catch (e) {
Globals.logger.log(`ERROR: BreezyDesktopExtension _check_driver_running ${e.message}`, e.stack); Globals.logger.log(`ERROR: BreezyDesktopExtension _check_driver_running ${e.message}\n${e.stack}`);
return false;
} }
return false;
} }
_effect_enable() { _effect_enable() {
@ -153,18 +194,20 @@ export default class BreezyDesktopExtension extends Extension {
this._is_effect_running = true; this._is_effect_running = true;
try { try {
this._cursor_manager = new CursorManager(Main.layoutManager.uiGroup); const targetMonitor = this._target_monitor.monitor;
const refreshRate = targetMonitor.refreshRate ?? 60;
this._cursor_manager = new CursorManager(Main.layoutManager.uiGroup, refreshRate * 1.05);
this._cursor_manager.enable(); this._cursor_manager.enable();
this._overlay = new St.Bin({ style: 'background-color: rgba(0, 0, 0, 1);'}); this._overlay = new St.Bin({ style: 'background-color: rgba(0, 0, 0, 1);'});
this._overlay.opacity = 255; this._overlay.opacity = 255;
this._overlay.set_position(this._target_monitor.x, this._target_monitor.y); this._overlay.set_position(targetMonitor.x, targetMonitor.y);
this._overlay.set_size(this._target_monitor.width, this._target_monitor.height); this._overlay.set_size(targetMonitor.width, targetMonitor.height);
const overlayContent = new Clutter.Actor({clip_to_allocation: true}); const overlayContent = new Clutter.Actor({clip_to_allocation: true});
const uiClone = new Clutter.Clone({ source: Main.layoutManager.uiGroup, clip_to_allocation: true }); const uiClone = new Clutter.Clone({ source: Main.layoutManager.uiGroup, clip_to_allocation: true });
uiClone.x = -this._target_monitor.x; uiClone.x = -targetMonitor.x;
uiClone.y = -this._target_monitor.y; uiClone.y = -targetMonitor.y;
if (Clutter.Container === undefined) { if (Clutter.Container === undefined) {
overlayContent.add_child(uiClone); overlayContent.add_child(uiClone);
} else { } else {
@ -177,21 +220,29 @@ export default class BreezyDesktopExtension extends Extension {
Shell.util_set_hidden_from_pick(this._overlay, true); Shell.util_set_hidden_from_pick(this._overlay, true);
this._xr_effect = new XREffect({ this._xr_effect = new XREffect({
target_monitor: this._target_monitor, target_monitor: targetMonitor,
target_framerate: this._refresh_rate ?? 60, target_framerate: refreshRate,
display_distance: this.settings.get_double('display-distance'), display_distance: this.settings.get_double('display-distance'),
toggle_display_distance_start: this.settings.get_double('toggle-display-distance-start'), toggle_display_distance_start: this.settings.get_double('toggle-display-distance-start'),
toggle_display_distance_end: this.settings.get_double('toggle-display-distance-end'), toggle_display_distance_end: this.settings.get_double('toggle-display-distance-end'),
look_ahead_override: this.settings.get_double('look-ahead-override'),
}); });
this._update_display_distance(this.settings);
this._update_follow_threshold(this.settings); this._update_follow_threshold(this.settings);
this._update_widescreen_mode_from_settings(this.settings);
this._widescreen_mode_effect_state_connection = this._xr_effect.connect('notify::widescreen-mode-state', this._update_widescreen_mode_from_state.bind(this));
this._supported_device_detected_connected = this._xr_effect.connect('notify::supported-device-detected', this._handle_supported_device_change.bind(this));
this._distance_binding = this.settings.bind('display-distance', this._xr_effect, 'display-distance', Gio.SettingsBindFlags.DEFAULT) this._distance_binding = this.settings.bind('display-distance', this._xr_effect, 'display-distance', Gio.SettingsBindFlags.DEFAULT)
this._distance_connection = this.settings.connect('changed::display-distance', this._update_display_distance.bind(this)) this._distance_connection = this.settings.connect('changed::display-distance', this._update_display_distance.bind(this))
this._follow_threshold_connection = this.settings.connect('changed::follow-threshold', this._update_follow_threshold.bind(this)) this._follow_threshold_connection = this.settings.connect('changed::follow-threshold', this._update_follow_threshold.bind(this))
this._widescreen_mode_settings_connection = this.settings.connect('changed::widescreen-mode', this._update_widescreen_mode_from_settings.bind(this))
this._start_binding = this.settings.bind('toggle-display-distance-start', this._xr_effect, 'toggle-display-distance-start', Gio.SettingsBindFlags.DEFAULT) this._start_binding = this.settings.bind('toggle-display-distance-start', this._xr_effect, 'toggle-display-distance-start', Gio.SettingsBindFlags.DEFAULT)
this._end_binding = this.settings.bind('toggle-display-distance-end', this._xr_effect, 'toggle-display-distance-end', Gio.SettingsBindFlags.DEFAULT) this._end_binding = this.settings.bind('toggle-display-distance-end', this._xr_effect, 'toggle-display-distance-end', Gio.SettingsBindFlags.DEFAULT)
this._curved_display_binding = this.settings.bind('curved-display', this._xr_effect, 'curved-display', Gio.SettingsBindFlags.DEFAULT)
this._display_size_binding = this.settings.bind('display-size', this._xr_effect, 'display-size', Gio.SettingsBindFlags.DEFAULT);
this._look_ahead_override_binding = this.settings.bind('look-ahead-override', this._xr_effect, 'look-ahead-override', Gio.SettingsBindFlags.DEFAULT);
this._overlay.add_effect_with_name('xr-desktop', this._xr_effect); this._overlay.add_effect_with_name('xr-desktop', this._xr_effect);
Meta.disable_unredirect_for_display(global.display); Meta.disable_unredirect_for_display(global.display);
@ -200,7 +251,7 @@ export default class BreezyDesktopExtension extends Extension {
this._add_settings_keybinding('toggle-display-distance-shortcut', this._xr_effect._change_distance.bind(this._xr_effect)); this._add_settings_keybinding('toggle-display-distance-shortcut', this._xr_effect._change_distance.bind(this._xr_effect));
this._add_settings_keybinding('toggle-follow-shortcut', this._toggle_follow_mode.bind(this)); this._add_settings_keybinding('toggle-follow-shortcut', this._toggle_follow_mode.bind(this));
} catch (e) { } catch (e) {
Globals.logger.log(`ERROR: BreezyDesktopExtension _effect_enable ${e.message}`, e.stack); Globals.logger.log(`ERROR: BreezyDesktopExtension _effect_enable ${e.message}\n${e.stack}`);
this._effect_disable(); this._effect_disable();
} }
} }
@ -231,11 +282,11 @@ export default class BreezyDesktopExtension extends Extension {
bind_to_function bind_to_function
); );
} catch (e) { } catch (e) {
Globals.logger.log(`ERROR: BreezyDesktopExtension _add_settings_keybinding settings binding lambda ${e.message}`, e.stack); Globals.logger.log(`ERROR: BreezyDesktopExtension _add_settings_keybinding settings binding lambda ${e.message}\n${e.stack}`);
} }
}); });
} catch (e) { } catch (e) {
Globals.logger.log(`ERROR: BreezyDesktopExtension _add_settings_keybinding ${e.message}`, e.stack); Globals.logger.log(`ERROR: BreezyDesktopExtension _add_settings_keybinding ${e.message}\n${e.stack}`);
} }
} }
@ -258,6 +309,40 @@ export default class BreezyDesktopExtension extends Extension {
if (value !== undefined) this._write_control('breezy_desktop_follow_threshold', value); if (value !== undefined) this._write_control('breezy_desktop_follow_threshold', value);
} }
_update_widescreen_mode_from_settings(settings, event) {
const value = settings.get_boolean('widescreen-mode');
Globals.logger.log_debug(`BreezyDesktopExtension _update_widescreen_mode_from_settings ${value}`);
if (value !== undefined && value != this._xr_effect.widescreen_mode_state)
this._write_control('sbs_mode', value ? 'enable' : 'disable');
else
Globals.logger.log_debug('effect.widescreen_mode_state already matched setting');
}
_update_widescreen_mode_from_state(effect, _pspec) {
const value = effect.widescreen_mode_state;
Globals.logger.log_debug(`BreezyDesktopExtension _update_widescreen_mode_from_state ${value}`);
if (value !== this.settings.get_boolean('widescreen-mode'))
this.settings.set_boolean('widescreen-mode', value);
else
Globals.logger.log_debug('settings.widescreen-mode already matched state');
}
_handle_monitor_change() {
Globals.logger.log('Monitor change detected');
this._setup();
}
_handle_supported_device_change(effect, _pspec) {
const value = effect.supported_device_detected;
Globals.logger.log_debug(`BreezyDesktopExtension _handle_supported_device_change ${value}`);
// this will disable the effect and begin polling for a ready state again
if (!value && this._is_effect_running) {
Globals.logger.log('Supported device disconnected');
this._setup();
}
}
_recenter_display() { _recenter_display() {
Globals.logger.log_debug('BreezyDesktopExtension _recenter_display'); Globals.logger.log_debug('BreezyDesktopExtension _recenter_display');
this._write_control('recenter_screen', 'true'); this._write_control('recenter_screen', 'true');
@ -299,6 +384,10 @@ export default class BreezyDesktopExtension extends Extension {
this.settings.disconnect(this._follow_threshold_connection); this.settings.disconnect(this._follow_threshold_connection);
this._follow_threshold_connection = null; this._follow_threshold_connection = null;
} }
if (this._widescreen_mode_settings_connection) {
this.settings.disconnect(this._widescreen_mode_settings_connection);
this._widescreen_mode_settings_connection = null;
}
if (this._start_binding) { if (this._start_binding) {
this.settings.unbind(this._start_binding); this.settings.unbind(this._start_binding);
this._start_binding = null; this._start_binding = null;
@ -307,7 +396,27 @@ export default class BreezyDesktopExtension extends Extension {
this.settings.unbind(this._end_binding); this.settings.unbind(this._end_binding);
this._end_binding = null; this._end_binding = null;
} }
if (this._curved_display_binding) {
this.settings.unbind(this._curved_display_binding);
this._curved_display_binding = null;
}
if (this._display_size_binding) {
this.settings.unbind(this._display_size_binding);
this._display_size_binding = null;
}
if (this._look_ahead_override_binding) {
this.settings.unbind(this._look_ahead_override_binding);
this._look_ahead_override_binding = null;
}
if (this._xr_effect) { if (this._xr_effect) {
if (this._widescreen_mode_effect_state_connection) {
this._xr_effect.disconnect(this._widescreen_mode_effect_state_connection);
this._widescreen_mode_effect_state_connection = null;
}
if (this._supported_device_detected_connected) {
this._xr_effect.disconnect(this._supported_device_detected_connected);
this._supported_device_detected_connected = null;
}
this._xr_effect.cleanup(); this._xr_effect.cleanup();
this._xr_effect = null; this._xr_effect = null;
} }
@ -317,7 +426,7 @@ export default class BreezyDesktopExtension extends Extension {
this._cursor_manager = null; this._cursor_manager = null;
} }
} catch (e) { } catch (e) {
Globals.logger.log(`ERROR: BreezyDesktopExtension _effect_disable ${e.message}`, e.stack); Globals.logger.log(`ERROR: BreezyDesktopExtension _effect_disable ${e.message}\n${e.stack}`);
} }
} }
@ -327,11 +436,20 @@ export default class BreezyDesktopExtension extends Extension {
this._effect_disable(); this._effect_disable();
this._target_monitor = null; this._target_monitor = null;
if (this._monitor_manager) { if (this._monitor_manager) {
if (this._optimal_monitor_config_binding) {
this.settings.unbind(this._optimal_monitor_config_binding);
this._optimal_monitor_config_binding = null
}
if (this._headset_as_primary_binding) {
this.settings.unbind(this._headset_as_primary_binding);
this._headset_as_primary_binding = null;
}
this._monitor_manager.disable(); this._monitor_manager.disable();
this._monitor_manager = null; this._monitor_manager = null;
} }
} catch (e) { } catch (e) {
Globals.logger.log(`ERROR: BreezyDesktopExtension disable ${e.message}`, e.stack); Globals.logger.log(`ERROR: BreezyDesktopExtension disable ${e.message}\n${e.stack}`);
} }
} }
} }

View File

@ -24,7 +24,7 @@ export function dataViewBigUint(dataView, dataViewInfo) {
return Number(dataView.getBigUint64(dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX], true)); return Number(dataView.getBigUint64(dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX], true));
} }
export function dataViewUintArray(dataView, dataViewInfo) { export function dataViewUint32Array(dataView, dataViewInfo) {
const uintArray = [] const uintArray = []
let offset = dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX]; let offset = dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX];
for (let i = 0; i < dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX]; i++) { for (let i = 0; i < dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX]; i++) {
@ -34,6 +34,16 @@ export function dataViewUintArray(dataView, dataViewInfo) {
return uintArray; return uintArray;
} }
export function dataViewUint8Array(dataView, dataViewInfo) {
const uintArray = []
let offset = dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX];
for (let i = 0; i < dataViewInfo[DATA_VIEW_INFO_SIZE_INDEX] * dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX]; i++) {
uintArray.push(dataView.getUint8(offset));
offset += UINT8_SIZE;
}
return uintArray;
}
export function dataViewFloat(dataView, dataViewInfo) { export function dataViewFloat(dataView, dataViewInfo) {
return dataView.getFloat32(dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX], true); return dataView.getFloat32(dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX], true);
} }

View File

@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import Gio from 'gi://Gio'; import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import * as Main from 'resource:///org/gnome/shell/ui/main.js';
@ -52,10 +53,10 @@ export function newDisplayConfig(extPath, callback) {
); );
} }
export function getMonitorConfig(displayConfigProxy, callback) { function getMonitorConfig(displayConfigProxy, callback) {
displayConfigProxy.GetResourcesRemote((result) => { displayConfigProxy.GetResourcesRemote((result, error) => {
if (result.length <= 2) { if (error) {
callback(null, 'Cannot get DisplayConfig: No outputs in GetResources()'); callback(null, `GetResourcesRemote failed: ${error}`);
} else { } else {
const monitors = []; const monitors = [];
for (let i = 0; i < result[2].length; i++) { for (let i = 0; i < result[2].length; i++) {
@ -84,32 +85,163 @@ export function getMonitorConfig(displayConfigProxy, callback) {
}); });
} }
// triggers callback with true result if an an async monitor config change was triggered, false if no config change needed
function performOptimalModeCheck(displayConfigProxy, connectorName, headsetAsPrimary, callback) {
Globals.logger.log_debug(`monitormanager.js performOptimalModeCheck for ${connectorName}`);
displayConfigProxy.GetCurrentStateRemote((result, error) => {
if (error) {
callback(null, `GetCurrentState failed: ${error}`);
} else {
Globals.logger.log_debug(`monitormanager.js performOptimalModeCheck GetCurrentState result: ${JSON.stringify(result)}`);
const [serial, monitors, logicalMonitors, properties] = result;
// iterate over all monitors at least once, collecting the best fit mode for our monitor, and mode information
// for each monitor
let ourMonitor = undefined;
let monitorToModeIdMap = {};
let bestFitMode = undefined;
for (let monitor of monitors) {
const [details, modes, monProperties] = monitor;
const [connector, vendor, product, monitorSerial] = details;
const isOurMonitor = connector == connectorName;
if (isOurMonitor) ourMonitor = monitor;
for (let mode of modes) {
const [modeId, width, height, refreshRate, preferredScale, supportedScales, modeProperites] = mode;
const isCurrent = !!modeProperites['is-current'];
if (isCurrent) monitorToModeIdMap[connector] = modeId;
if (isOurMonitor && (!bestFitMode || (
width >= bestFitMode.width &&
height >= bestFitMode.height &&
refreshRate >= bestFitMode.refreshRate))) {
// find the scale that is closest to 1.0
const bestScale = supportedScales.reduce((prev, curr) => {
return Math.abs(curr - 1.0) < Math.abs(prev - 1.0) ? curr : prev;
});
bestFitMode = {
modeId,
width,
height,
refreshRate,
bestScale,
isCurrent
};
}
}
}
if (!!ourMonitor) {
let anyMonitorsChanged = false;
if (!!bestFitMode) {
// map from original logical monitors schema to a(iiduba(ssa{sv})) for ApplyMonitorsConfig call
const updatedLogicalMonitors = logicalMonitors.map((logicalMonitor) => {
const [x, y, scale, transform, primary, monitors, logMonProperties] = logicalMonitor;
const hasOurMonitor = !!monitors.some((monitor) => monitor[0] === connectorName);
anyMonitorsChanged |= hasOurMonitor && bestFitMode.bestScale !== scale;
// there can only be one primary monitor, so we need to set all other monitors to not primary and glasses to primary,
// if headsetAsPrimary is true
anyMonitorsChanged |= headsetAsPrimary && ((hasOurMonitor && !primary) || (!hasOurMonitor && primary));
return [
x,
y,
hasOurMonitor ? bestFitMode.bestScale : scale,
transform,
headsetAsPrimary ? hasOurMonitor : primary,
monitors.map((monitor) => {
const monitorConnector = monitor[0];
const isOurMonitor = monitorConnector === connectorName;
anyMonitorsChanged |= isOurMonitor && !bestFitMode.isCurrent;
return [
monitorConnector,
isOurMonitor ? bestFitMode.modeId : monitorToModeIdMap[monitorConnector],
{} // properties
];
})
];
});
// if our monitor is already properly configured, we can skip the ApplyMonitorsConfig call
if (anyMonitorsChanged) {
Globals.logger.log_debug(`monitormanager.js performOptimalModeCheck updatedLogicalMonitors: ${JSON.stringify(updatedLogicalMonitors)}`);
displayConfigProxy.ApplyMonitorsConfigRemote(
serial,
1, // "temporary" config -- "permanent" might be pointless since we always do this check
updatedLogicalMonitors,
{}, // properties
(_result, error) => {
if (error) {
callback(null, `ApplyMonitorsConfig failed: ${error}`);
} else {
callback(true, null);
}
}
);
}
}
if (!anyMonitorsChanged) callback(false, null);
} else {
callback(null, `Monitor ${connectorName} not found in GetCurrentState result`);
}
}
});
}
// Monitor change handling // Monitor change handling
export default class MonitorManager { export const MonitorManager = GObject.registerClass({
constructor(extPath) { Properties: {
this._extPath = extPath; 'use-optimal-monitor-config': GObject.ParamSpec.boolean(
'use-optimal-monitor-config',
'Use optimal monitor configuration',
'Automatically set the optimal monitor configuration upon connection',
GObject.ParamFlags.READWRITE,
true
),
'headset-as-primary': GObject.ParamSpec.boolean(
'headset-as-primary',
'Use headset as primary monitor',
'Automatically set the headset as the primary display upon connection',
GObject.ParamFlags.READWRITE,
true
),
'extension-path': GObject.ParamSpec.string(
'extension-path',
'Extension path',
'Path to the extension directory',
GObject.ParamFlags.READWRITE,
''
)
}
}, class MonitorManager extends GObject.Object {
constructor(params = {}) {
super(params);
this._monitorsChangedConnection = null; this._monitorsChangedConnection = null;
this._displayConfigProxy = null; this._displayConfigProxy = null;
this._backendManager = null; this._backendManager = null;
this._monitorProperties = null; this._monitorProperties = null;
this._changeHookFn = null; this._changeHookFn = null;
this._needsConfigCheck = this.use_optimal_monitor_config;
} }
enable() { enable() {
Globals.logger.log_debug('MonitorManager enable');
this._backendManager = global.backend.get_monitor_manager(); this._backendManager = global.backend.get_monitor_manager();
newDisplayConfig(this._extPath, (proxy, error) => { newDisplayConfig(this.extension_path, ((proxy, error) => {
if (error) { if (error) {
return; return;
} }
this._displayConfigProxy = proxy; this._displayConfigProxy = proxy;
this._on_monitors_change(); this._on_monitors_change();
}); }).bind(this));
this._monitorsChangedConnection = Main.layoutManager.connect('monitors-changed', this._on_monitors_change.bind(this)); this._monitorsChangedConnection = Main.layoutManager.connect('monitors-changed', this._on_monitors_change.bind(this));
} }
disable() { disable() {
Globals.logger.log_debug('MonitorManager disable');
Main.layoutManager.disconnect(this._monitorsChangedConnection); Main.layoutManager.disconnect(this._monitorsChangedConnection);
this._monitorsChangedConnection = null; this._monitorsChangedConnection = null;
@ -123,10 +255,6 @@ export default class MonitorManager {
this._changeHookFn = fn; this._changeHookFn = fn;
} }
setPostCallback(callback) {
this._postCallback = callback;
}
getMonitors() { getMonitors() {
return Main.layoutManager.monitors; return Main.layoutManager.monitors;
} }
@ -135,11 +263,45 @@ export default class MonitorManager {
return this._monitorProperties; return this._monitorProperties;
} }
// returns true if a check is needed, caller should wait for the next change hook call
checkOptimalMode(monitorConnector) {
Globals.logger.log_debug(`MonitorManager checkOptimalMode: ${monitorConnector}`);
if (this._displayConfigProxy == null) {
Globals.logger.log('MonitorManager checkOptimalMode: _displayConfigProxy not set!');
return false;
}
if (this._needsConfigCheck) {
performOptimalModeCheck(this._displayConfigProxy, monitorConnector, this.headset_as_primary, ((configChanged, error) => {
this._needsConfigCheck = false;
if (error) {
Globals.logger.log(`Failed to switch to optimal mode for monitor ${monitorConnector}: ${error}`);
} else {
if (configChanged) {
Globals.logger.log(`Switched to optimal mode for monitor ${monitorConnector}`);
} else if (!!this._changeHookFn) {
Globals.logger.log_debug('MonitorManager checkOptimalMode: no config change');
// no config change means this won't be triggered automatically, so trigger it manually
this._changeHookFn();
} else {
Globals.logger.log('MonitorManager checkOptimalMode: can\'t trigger change hook, no hook set!');
}
}
}).bind(this));
} else {
Globals.logger.log_debug('MonitorManager checkOptimalMode: skipping config check');
}
return this._needsConfigCheck;
}
_on_monitors_change() { _on_monitors_change() {
Globals.logger.log_debug('MonitorManager _on_monitors_change');
if (this._displayConfigProxy == null) { if (this._displayConfigProxy == null) {
return; return;
} }
getMonitorConfig(this._displayConfigProxy, (result, error) => { this._needsConfigCheck = this.use_optimal_monitor_config;
getMonitorConfig(this._displayConfigProxy, ((result, error) => {
if (error) { if (error) {
return; return;
} }
@ -161,9 +323,11 @@ export default class MonitorManager {
} }
} }
this._monitorProperties = monitorProperties; this._monitorProperties = monitorProperties;
if (this._changeHookFn !== null) { if (!!this._changeHookFn) {
this._changeHookFn(); this._changeHookFn();
} else {
Globals.logger.log('MonitorManager _on_monitors_change: can\'t trigger change hook, no hook set!');
} }
}); }).bind(this));
} }
} });

View File

@ -4,4 +4,8 @@ export function getEpochSec() {
export function toSec(milliseconds) { export function toSec(milliseconds) {
return Math.floor(milliseconds / 1000); return Math.floor(milliseconds / 1000);
}
export function isValidKeepAlive(dateSec, strictCheck = false) {
return Math.abs(toSec(Date.now()) - dateSec) <= (strictCheck ? 1 : 5);
} }

View File

@ -11,7 +11,8 @@ import {
dataViewEnd, dataViewEnd,
dataViewUint8, dataViewUint8,
dataViewBigUint, dataViewBigUint,
dataViewUintArray, dataViewUint32Array,
dataViewUint8Array,
dataViewFloat, dataViewFloat,
dataViewFloatArray, dataViewFloatArray,
BOOL_SIZE, BOOL_SIZE,
@ -23,12 +24,12 @@ import {
} from "./ipc.js"; } from "./ipc.js";
import { degreeToRadian } from "./math.js"; import { degreeToRadian } from "./math.js";
import { getShaderSource } from "./shader.js"; import { getShaderSource } from "./shader.js";
import { toSec } from "./time.js"; import { isValidKeepAlive, toSec } from "./time.js";
export const IPC_FILE_PATH = "/dev/shm/breezy_desktop_imu"; export const IPC_FILE_PATH = "/dev/shm/breezy_desktop_imu";
// the driver should be using the same data layout version // the driver should be using the same data layout version
const DATA_LAYOUT_VERSION = 2; const DATA_LAYOUT_VERSION = 3;
// DataView info: [offset, size, count] // DataView info: [offset, size, count]
const VERSION = [0, UINT8_SIZE, 1]; const VERSION = [0, UINT8_SIZE, 1];
@ -41,7 +42,8 @@ const SBS_ENABLED = [dataViewEnd(LENS_DISTANCE_RATIO), BOOL_SIZE, 1];
const CUSTOM_BANNER_ENABLED = [dataViewEnd(SBS_ENABLED), BOOL_SIZE, 1]; const CUSTOM_BANNER_ENABLED = [dataViewEnd(SBS_ENABLED), BOOL_SIZE, 1];
const EPOCH_MS = [dataViewEnd(CUSTOM_BANNER_ENABLED), UINT_SIZE, 2]; const EPOCH_MS = [dataViewEnd(CUSTOM_BANNER_ENABLED), UINT_SIZE, 2];
const IMU_QUAT_DATA = [dataViewEnd(EPOCH_MS), FLOAT_SIZE, 16]; const IMU_QUAT_DATA = [dataViewEnd(EPOCH_MS), FLOAT_SIZE, 16];
const DATA_VIEW_LENGTH = dataViewEnd(IMU_QUAT_DATA); const IMU_PARITY_BYTE = [dataViewEnd(IMU_QUAT_DATA), UINT8_SIZE, 1];
const DATA_VIEW_LENGTH = dataViewEnd(IMU_PARITY_BYTE);
// cached after first retrieval // cached after first retrieval
const shaderUniformLocations = { const shaderUniformLocations = {
@ -50,20 +52,20 @@ const shaderUniformLocations = {
'imu_quat_data': null, 'imu_quat_data': null,
'look_ahead_cfg': null, 'look_ahead_cfg': null,
'look_ahead_ms': null, 'look_ahead_ms': null,
'stage_aspect_ratio': null,
'display_aspect_ratio': null,
'trim_width_percent': null, 'trim_width_percent': null,
'trim_height_percent': null, 'trim_height_percent': null,
'display_zoom': null, 'display_size': null,
'display_north_offset': null, 'display_north_offset': null,
'lens_distance_ratio': null, 'lens_distance_ratio': null,
'sbs_enabled': null, 'sbs_enabled': null,
'sbs_content': null, 'sbs_content': null,
'sbs_mode_stretched': null,
'custom_banner_enabled': null, 'custom_banner_enabled': null,
'half_fov_z_rads': null, 'half_fov_z_rads': null,
'half_fov_y_rads': null, 'half_fov_y_rads': null,
'screen_distance': null, 'source_resolution': null,
'display_res': null 'display_resolution': null,
'curved_display': null
}; };
function setUniformFloat(effect, locationName, dataViewInfo, value) { function setUniformFloat(effect, locationName, dataViewInfo, value) {
@ -109,67 +111,92 @@ function lookAheadMS(dataView) {
// most uniforms don't change frequently, this function should be called periodically // most uniforms don't change frequently, this function should be called periodically
function setIntermittentUniformVariables() { function setIntermittentUniformVariables() {
const dataView = this._dataView; try {
const dataView = this._dataView;
if (dataView.byteLength === DATA_VIEW_LENGTH) { if (dataView.byteLength === DATA_VIEW_LENGTH) {
const version = dataViewUint8(dataView, VERSION); const version = dataViewUint8(dataView, VERSION);
const imuDateMS = dataViewBigUint(dataView, EPOCH_MS); const imuDateMs = dataViewBigUint(dataView, EPOCH_MS);
const currentDateMS = Date.now(); const validKeepalive = isValidKeepAlive(toSec(imuDateMs));
const validKeepalive = Math.abs(toSec(currentDateMS) - toSec(imuDateMS)) < 5; const imuData = dataViewFloatArray(dataView, IMU_QUAT_DATA);
const imuData = dataViewFloatArray(dataView, IMU_QUAT_DATA); const imuResetState = validKeepalive && imuData[0] === 0.0 && imuData[1] === 0.0 && imuData[2] === 0.0 && imuData[3] === 1.0;
const imuResetState = validKeepalive && imuData[0] === 0.0 && imuData[1] === 0.0 && imuData[2] === 0.0 && imuData[3] === 1.0; const enabled = dataViewUint8(dataView, ENABLED) !== 0 && version === DATA_LAYOUT_VERSION && validKeepalive;
const enabled = dataViewUint8(dataView, ENABLED) !== 0 && version === DATA_LAYOUT_VERSION && validKeepalive; const displayRes = dataViewUint32Array(dataView, DISPLAY_RES);
if (enabled) { if (enabled) {
const displayRes = dataViewUintArray(dataView, DISPLAY_RES); const displayFov = dataViewFloat(dataView, DISPLAY_FOV);
const displayFov = dataViewFloat(dataView, DISPLAY_FOV);
const lensDistanceRatio = dataViewFloat(dataView, LENS_DISTANCE_RATIO);
// compute these values once, they only change when the XR device changes // compute these values once, they only change when the XR device changes
const displayAspectRatio = displayRes[0] / displayRes[1]; const displayAspectRatio = displayRes[0] / displayRes[1];
const stageAspectRatio = this.target_monitor.width / this.target_monitor.height; const diagToVertRatio = Math.sqrt(Math.pow(displayAspectRatio, 2) + 1);
const diagToVertRatio = Math.sqrt(Math.pow(stageAspectRatio, 2) + 1); const halfFovZRads = degreeToRadian(displayFov / diagToVertRatio) / 2;
const halfFovZRads = degreeToRadian(displayFov / diagToVertRatio) / 2; const halfFovYRads = halfFovZRads * displayAspectRatio;
const halfFovYRads = halfFovZRads * stageAspectRatio;
const screenDistance = 1.0 - lensDistanceRatio;
// our overlay doesn't quite cover the full screen texture, which allows us to see some of the real desktop // our overlay doesn't quite cover the full screen texture, which allows us to see some of the real desktop
// underneath, so we trim two pixels around the entire edge of the texture // underneath, so we trim three pixels around the entire edge of the texture
const trimWidthPercent = 3.0 / this.target_monitor.width; const trimWidthPercent = 3.0 / this.target_monitor.width;
const trimHeightPercent = 3.0 / this.target_monitor.height; const trimHeightPercent = 3.0 / this.target_monitor.height;
// all these values are transferred directly, unmodified from the driver // all these values are transferred directly, unmodified from the driver
transferUniformFloat(this, 'look_ahead_cfg', dataView, LOOK_AHEAD_CFG); transferUniformFloat(this, 'look_ahead_cfg', dataView, LOOK_AHEAD_CFG);
transferUniformFloat(this, 'lens_distance_ratio', dataView, LENS_DISTANCE_RATIO); transferUniformFloat(this, 'lens_distance_ratio', dataView, LENS_DISTANCE_RATIO);
// computed values with no dataViewInfo, so we set these manually // computed values with no dataViewInfo, so we set these manually
setSingleFloat(this, 'stage_aspect_ratio', stageAspectRatio); setSingleFloat(this, 'trim_width_percent', trimWidthPercent);
setSingleFloat(this, 'display_aspect_ratio', displayAspectRatio); setSingleFloat(this, 'trim_height_percent', trimHeightPercent);
setSingleFloat(this, 'trim_width_percent', trimWidthPercent); setSingleFloat(this, 'half_fov_z_rads', halfFovZRads);
setSingleFloat(this, 'trim_height_percent', trimHeightPercent); setSingleFloat(this, 'half_fov_y_rads', halfFovYRads);
setSingleFloat(this, 'half_fov_z_rads', halfFovZRads); setSingleFloat(this, 'curved_display', this.curved_display ? 1.0 : 0.0);
setSingleFloat(this, 'half_fov_y_rads', halfFovYRads); }
setSingleFloat(this, 'screen_distance', screenDistance);
// TOOD - drive from settings // update the supported device detected property if the state changes, trigger "notify::" events
setSingleFloat(this, 'display_zoom', 1.0); if (this.supported_device_detected !== validKeepalive) this.supported_device_detected = validKeepalive;
// update the widescreen property if the state changes while still enabled, trigger "notify::" events
const sbsEnabled = dataViewUint8(dataView, SBS_ENABLED) !== 0;
if (enabled && this.widescreen_mode_state !== sbsEnabled) this.widescreen_mode_state = sbsEnabled;
// these variables are always in play, even if enabled is false
setSingleFloat(this, 'enabled', enabled ? 1.0 : 0.0);
setSingleFloat(this, 'show_banner', imuResetState ? 1.0 : 0.0);
setSingleFloat(this, 'sbs_content', 0.0); // TODO - drive from settings
setSingleFloat(this, 'sbs_mode_stretched', 1.0); // content always fills the whole display
setSingleFloat(this, 'sbs_enabled', sbsEnabled ? 1.0 : 0.0);
setSingleFloat(this, 'custom_banner_enabled', dataViewUint8(dataView, CUSTOM_BANNER_ENABLED) !== 0 ? 1.0 : 0.0);
this.set_uniform_float(shaderUniformLocations['display_resolution'], 2, displayRes);
this.set_uniform_float(shaderUniformLocations['source_resolution'], 2, [this.target_monitor.width, this.target_monitor.height]);
} else if (dataView.byteLength !== 0) {
throw new Error(`Invalid dataView.byteLength: ${dataView.byteLength} !== ${DATA_VIEW_LENGTH}`);
} }
} catch (e) {
// these variables are always in play, even if enabled is false Globals.logger.log(`ERROR: xrEffect.js setIntermittentUniformVariables ${e.message}\n${e.stack}`);
setSingleFloat(this, 'enabled', enabled ? 1.0 : 0.0);
setSingleFloat(this, 'show_banner', imuResetState ? 1.0 : 0.0);
setSingleFloat(this, 'sbs_content', 0.0); // TOOD - drive from settings
setSingleFloat(this, 'sbs_enabled', dataViewUint8(dataView, SBS_ENABLED) !== 0 ? 1.0 : 0.0);
setSingleFloat(this, 'custom_banner_enabled', dataViewUint8(dataView, CUSTOM_BANNER_ENABLED) !== 0 ? 1.0 : 0.0);
this.set_uniform_float(shaderUniformLocations['display_res'], 2, [this.target_monitor.width, this.target_monitor.height]);
} else if (dataView.byteLength !== 0) {
Globals.logger.log(`ERROR: Invalid dataView.byteLength: ${dataView.byteLength} !== ${DATA_VIEW_LENGTH}`)
} }
} }
function checkParityByte(dataView) {
const parityByte = dataViewUint8(dataView, IMU_PARITY_BYTE);
let parity = 0;
const epochUint8 = dataViewUint8Array(dataView, EPOCH_MS);
const imuDataUint8 = dataViewUint8Array(dataView, IMU_QUAT_DATA);
for (let i = 0; i < epochUint8.length; i++) {
parity ^= epochUint8[i];
}
for (let i = 0; i < imuDataUint8.length; i++) {
parity ^= imuDataUint8[i];
}
return parityByte === parity;
}
export const XREffect = GObject.registerClass({ export const XREffect = GObject.registerClass({
Properties: { Properties: {
'supported-device-detected': GObject.ParamSpec.boolean(
'supported-device-detected',
'Supported device detected',
'Whether a supported device is connected',
GObject.ParamFlags.READWRITE,
false
),
'target-monitor': GObject.ParamSpec.jsobject( 'target-monitor': GObject.ParamSpec.jsobject(
'target-monitor', 'target-monitor',
'Target Monitor', 'Target Monitor',
@ -191,6 +218,15 @@ export const XREffect = GObject.registerClass({
2.5, 2.5,
1.05 1.05
), ),
'display-size': GObject.ParamSpec.double(
'display-size',
'Display size',
'Size of the display',
GObject.ParamFlags.READWRITE,
0.2,
2.5,
1.0
),
'toggle-display-distance-start': GObject.ParamSpec.double( 'toggle-display-distance-start': GObject.ParamSpec.double(
'toggle-display-distance-start', 'toggle-display-distance-start',
'Display distance start', 'Display distance start',
@ -208,13 +244,38 @@ export const XREffect = GObject.registerClass({
0.2, 0.2,
2.5, 2.5,
1.05 1.05
),
'curved-display': GObject.ParamSpec.boolean(
'curved-display',
'Curved Display',
'Whether the display is curved',
GObject.ParamFlags.READWRITE,
false
),
'widescreen-mode-state': GObject.ParamSpec.boolean(
'widescreen-mode-state',
'Widescreen mode state',
'The state of widescreen mode from the perspective of the driver',
GObject.ParamFlags.READWRITE,
false
),
'look-ahead-override': GObject.ParamSpec.int(
'look-ahead-override',
'Look ahead override',
'Override the look ahead value',
GObject.ParamFlags.READWRITE,
-1,
45,
-1
) )
} }
}, class XREffect extends Shell.GLSLEffect { }, class XREffect extends Shell.GLSLEffect {
constructor(params = {}) { constructor(params = {}) {
super(params); super(params);
this._frametime = Math.floor(1000 / this.target_framerate); // target a slightly lower framerate than the monitor's refresh rate to prevent repainting too frequently
const frameTimeFramerate = this.target_framerate * 0.9;
this._frametime = Math.floor(1000 / frameTimeFramerate);
this._is_display_distance_at_end = false; this._is_display_distance_at_end = false;
this._distance_ease_timeline = null; this._distance_ease_timeline = null;
@ -255,14 +316,12 @@ export const XREffect = GObject.registerClass({
} }
vfunc_paint_target(node, paintContext) { vfunc_paint_target(node, paintContext) {
var now = Date.now();
var lastPaint = this._last_paint || 0;
var frametime = this._frametime; var frametime = this._frametime;
var calibratingImage = this.calibratingImage; var calibratingImage = this.calibratingImage;
var customBannerImage = this.customBannerImage; var customBannerImage = this.customBannerImage;
const data = Globals.ipc_file.load_contents(null); let data = Globals.ipc_file.load_contents(null);
if (data[0]) { if (data[0]) {
const buffer = new Uint8Array(data[1]).buffer; let buffer = new Uint8Array(data[1]).buffer;
this._dataView = new DataView(buffer); this._dataView = new DataView(buffer);
if (!this._initialized) { if (!this._initialized) {
this.set_uniform_float(this.get_uniform_location('uDesktopTexture'), 1, [0]); this.set_uniform_float(this.get_uniform_location('uDesktopTexture'), 1, [0]);
@ -278,11 +337,6 @@ export const XREffect = GObject.registerClass({
this.setIntermittentUniformVariables = setIntermittentUniformVariables.bind(this); this.setIntermittentUniformVariables = setIntermittentUniformVariables.bind(this);
this.setIntermittentUniformVariables(); this.setIntermittentUniformVariables();
this._redraw_timeout_id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._frametime, () => {
if ((now - lastPaint) > frametime) global.stage.queue_redraw();
return GLib.SOURCE_CONTINUE;
});
this._uniforms_timeout_id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, (() => { this._uniforms_timeout_id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, (() => {
this.setIntermittentUniformVariables(); this.setIntermittentUniformVariables();
return GLib.SOURCE_CONTINUE; return GLib.SOURCE_CONTINUE;
@ -291,12 +345,29 @@ export const XREffect = GObject.registerClass({
this._initialized = true; this._initialized = true;
} }
if (this._dataView.byteLength === DATA_VIEW_LENGTH) { let success = false;
setSingleFloat(this, 'display_north_offset', this.display_distance); let attempts = 0;
setSingleFloat(this, 'look_ahead_ms', lookAheadMS(this._dataView)); while (!success && attempts < 2) {
setUniformMatrix(this, 'imu_quat_data', 4, this._dataView, IMU_QUAT_DATA); if (this._dataView.byteLength === DATA_VIEW_LENGTH) {
} else if (this._dataView.byteLength !== 0) { if (checkParityByte(this._dataView)) {
Globals.logger.log(`ERROR: Invalid dataView.byteLength: ${this._dataView.byteLength} !== ${DATA_VIEW_LENGTH}`) setSingleFloat(this, 'display_north_offset', this.display_distance);
setSingleFloat(this, 'look_ahead_ms',
this.look_ahead_override === -1 ? lookAheadMS(this._dataView) : this.look_ahead_override);
setUniformMatrix(this, 'imu_quat_data', 4, this._dataView, IMU_QUAT_DATA);
setSingleFloat(this, 'display_size', this.display_size);
success = true;
}
} else if (this._dataView.byteLength !== 0) {
Globals.logger.log(`ERROR: Invalid dataView.byteLength: ${this._dataView.byteLength} !== ${DATA_VIEW_LENGTH}`)
}
if (!success && ++attempts < 3) {
data = Globals.ipc_file.load_contents(null);
if (data[0]) {
buffer = new Uint8Array(data[1]).buffer;
this._dataView = new DataView(buffer);
}
}
} }
// improves sampling quality for smooth text and edges // improves sampling quality for smooth text and edges
@ -305,16 +376,11 @@ export const XREffect = GObject.registerClass({
Cogl.PipelineFilter.LINEAR_MIPMAP_LINEAR, Cogl.PipelineFilter.LINEAR_MIPMAP_LINEAR,
Cogl.PipelineFilter.LINEAR Cogl.PipelineFilter.LINEAR
); );
super.vfunc_paint_target(node, paintContext);
} else {
super.vfunc_paint_target(node, paintContext);
} }
this._last_paint = now; super.vfunc_paint_target(node, paintContext);
} }
cleanup() { cleanup() {
if (this._redraw_timeout_id) GLib.source_remove(this._redraw_timeout_id);
if (this._uniforms_timeout_id) GLib.source_remove(this._uniforms_timeout_id); if (this._uniforms_timeout_id) GLib.source_remove(this._uniforms_timeout_id);
} }
}); });

@ -1 +1 @@
Subproject commit f45b8fbaa7ef0b3f824a3452a91dc74bffb6ea38 Subproject commit 67fd43300a89c7ab45af702a5161e9e0719343b0

@ -1 +1 @@
Subproject commit 17ebe10ad9a006cd6a51d5705d6ceddca4464369 Subproject commit b45a3d9c925681aa8b70bbc38ad6919750b16e6d

View File

@ -64,6 +64,60 @@
End distance when using the "toggle display distance" shortcut. End distance when using the "toggle display distance" shortcut.
</description> </description>
</key> </key>
<key name="widescreen-mode" type="b">
<default>
false
</default>
<summary>Widescreen mode</summary>
<description>
Enable widescreen/SBS mode
</description>
</key>
<key name="display-size" type="d">
<default>
1.0
</default>
<summary>Display size</summary>
<description>
The size of the display
</description>
</key>
<key name="curved-display" type="b">
<default>
false
</default>
<summary>Curved display</summary>
<description>
Enable curved display mode
</description>
</key>
<key name="look-ahead-override" type="i">
<default>
-1
</default>
<summary>Look-ahead override</summary>
<description>
Manually override the look-ahead calculation
</description>
</key>
<key name="use-optimal-monitor-config" type="b">
<default>
true
</default>
<summary>Use optimal monitor configuration</summary>
<description>
Automatically set the optimal monitor configuration upon connection
</description>
</key>
<key name="headset-as-primary" type="b">
<default>
true
</default>
<summary>Headset as primary</summary>
<description>
Automatically set the headset as the primary display upon connection
</description>
</key>
<key name="developer-mode" type="b"> <key name="developer-mode" type="b">
<default> <default>
false false

View File

@ -10,13 +10,17 @@ from .xrdriveripc import XRDriverIPC
class ConnectedDevice(Gtk.Box): class ConnectedDevice(Gtk.Box):
__gtype_name__ = "ConnectedDevice" __gtype_name__ = "ConnectedDevice"
device_label = Gtk.Template.Child()
effect_enable_switch = Gtk.Template.Child() effect_enable_switch = Gtk.Template.Child()
display_distance_scale = Gtk.Template.Child() display_distance_scale = Gtk.Template.Child()
display_distance_adjustment = Gtk.Template.Child() display_distance_adjustment = Gtk.Template.Child()
display_size_scale = Gtk.Template.Child()
display_size_adjustment = Gtk.Template.Child()
follow_threshold_scale = Gtk.Template.Child() follow_threshold_scale = Gtk.Template.Child()
follow_threshold_adjustment = Gtk.Template.Child() follow_threshold_adjustment = Gtk.Template.Child()
follow_mode_switch = Gtk.Template.Child() follow_mode_switch = Gtk.Template.Child()
device_label = Gtk.Template.Child() widescreen_mode_switch = Gtk.Template.Child()
curved_display_switch = Gtk.Template.Child()
set_toggle_display_distance_start_button = Gtk.Template.Child() set_toggle_display_distance_start_button = Gtk.Template.Child()
set_toggle_display_distance_end_button = Gtk.Template.Child() set_toggle_display_distance_end_button = Gtk.Template.Child()
reassign_recenter_display_shortcut_button = Gtk.Template.Child() reassign_recenter_display_shortcut_button = Gtk.Template.Child()
@ -25,19 +29,30 @@ class ConnectedDevice(Gtk.Box):
toggle_display_distance_shortcut_label = Gtk.Template.Child() toggle_display_distance_shortcut_label = Gtk.Template.Child()
reassign_toggle_follow_shortcut_button = Gtk.Template.Child() reassign_toggle_follow_shortcut_button = Gtk.Template.Child()
toggle_follow_shortcut_label = Gtk.Template.Child() toggle_follow_shortcut_label = Gtk.Template.Child()
headset_as_primary_switch = Gtk.Template.Child()
use_optimal_monitor_config_switch = Gtk.Template.Child()
movement_look_ahead_scale = Gtk.Template.Child()
movement_look_ahead_adjustment = Gtk.Template.Child()
def __init__(self): def __init__(self):
super(Gtk.Box, self).__init__() super(Gtk.Box, self).__init__()
self.init_template() self.init_template()
self.all_enabled_state_inputs = [ self.all_enabled_state_inputs = [
self.display_distance_scale, self.display_distance_scale,
self.display_size_scale,
self.follow_mode_switch, self.follow_mode_switch,
self.follow_threshold_scale, self.follow_threshold_scale,
self.widescreen_mode_switch,
self.curved_display_switch,
self.set_toggle_display_distance_start_button, self.set_toggle_display_distance_start_button,
self.set_toggle_display_distance_end_button, self.set_toggle_display_distance_end_button,
self.reassign_recenter_display_shortcut_button, self.reassign_recenter_display_shortcut_button,
self.reassign_toggle_display_distance_shortcut_button, self.reassign_toggle_display_distance_shortcut_button,
self.reassign_toggle_follow_shortcut_button self.reassign_toggle_follow_shortcut_button,
self.headset_as_primary_switch,
self.use_optimal_monitor_config_switch,
self.movement_look_ahead_scale
] ]
self.settings = SettingsManager.get_instance().settings self.settings = SettingsManager.get_instance().settings
@ -45,7 +60,13 @@ class ConnectedDevice(Gtk.Box):
self.extensions_manager = ExtensionsManager.get_instance() self.extensions_manager = ExtensionsManager.get_instance()
self.settings.bind('display-distance', self.display_distance_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT) self.settings.bind('display-distance', self.display_distance_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
self.settings.bind('display-size', self.display_size_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
self.settings.bind('follow-threshold', self.follow_threshold_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT) self.settings.bind('follow-threshold', self.follow_threshold_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
self.settings.bind('widescreen-mode', self.widescreen_mode_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
self.settings.bind('curved-display', self.curved_display_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
self.settings.bind('headset-as-primary', self.headset_as_primary_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
self.settings.bind('use-optimal-monitor-config', self.use_optimal_monitor_config_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
self.settings.bind('look-ahead-override', self.movement_look_ahead_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
bind_shortcut_settings(self.get_parent(), [ bind_shortcut_settings(self.get_parent(), [
[self.reassign_recenter_display_shortcut_button, self.recenter_display_shortcut_label], [self.reassign_recenter_display_shortcut_button, self.recenter_display_shortcut_label],
@ -68,8 +89,11 @@ class ConnectedDevice(Gtk.Box):
self.effect_enable_switch.set_active(self._is_config_enabled(self.ipc.retrieve_config()) and self.extensions_manager.is_enabled()) self.effect_enable_switch.set_active(self._is_config_enabled(self.ipc.retrieve_config()) and self.extensions_manager.is_enabled())
self.effect_enable_switch.connect('notify::active', self._refresh_inputs_for_enabled_state) self.effect_enable_switch.connect('notify::active', self._refresh_inputs_for_enabled_state)
self.use_optimal_monitor_config_switch.connect('notify::active', self._refresh_use_optimal_monitor_config)
self._handle_enabled_features(self.state_manager, None) self._handle_enabled_features(self.state_manager, None)
self._refresh_inputs_for_enabled_state(self.effect_enable_switch, None) self._refresh_inputs_for_enabled_state(self.effect_enable_switch, None)
self._refresh_use_optimal_monitor_config(self.use_optimal_monitor_config_switch, None)
self.extensions_manager.bind_property('breezy-enabled', self.effect_enable_switch, 'active', GObject.BindingFlags.BIDIRECTIONAL) self.extensions_manager.bind_property('breezy-enabled', self.effect_enable_switch, 'active', GObject.BindingFlags.BIDIRECTIONAL)
self.connect("destroy", self._on_widget_destroy) self.connect("destroy", self._on_widget_destroy)
@ -98,7 +122,8 @@ class ConnectedDevice(Gtk.Box):
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 requesting_enabled: self._refresh_follow_mode(self.follow_mode_switch, None) if requesting_enabled:
self._refresh_follow_mode(self.follow_mode_switch, None)
def _refresh_follow_mode(self, switch, param): def _refresh_follow_mode(self, switch, param):
self.follow_threshold_scale.set_sensitive(switch.get_active()) self.follow_threshold_scale.set_sensitive(switch.get_active())
@ -109,6 +134,11 @@ class ConnectedDevice(Gtk.Box):
'enable_breezy_desktop_smooth_follow': switch.get_active() 'enable_breezy_desktop_smooth_follow': switch.get_active()
}) })
def _refresh_use_optimal_monitor_config(self, switch, param):
self.headset_as_primary_switch.set_sensitive(switch.get_active())
if not switch.get_active():
self.headset_as_primary_switch.set_active(False)
def set_device_name(self, name): def set_device_name(self, name):
self.device_label.set_markup(f"<b>{name}</b>") self.device_label.set_markup(f"<b>{name}</b>")
@ -120,7 +150,9 @@ class ConnectedDevice(Gtk.Box):
def _on_widget_destroy(self, widget): def _on_widget_destroy(self, widget):
self.state_manager.unbind_property('follow-mode', self.follow_mode_switch, 'active') self.state_manager.unbind_property('follow-mode', self.follow_mode_switch, 'active')
self.settings.unbind('display-distance', self.display_distance_adjustment, 'value') self.settings.unbind('display-distance', self.display_distance_adjustment, 'value')
self.settings.unbind('display-size', self.display_size_adjustment, 'value')
self.settings.unbind('follow-threshold', self.follow_threshold_adjustment, 'value') self.settings.unbind('follow-threshold', self.follow_threshold_adjustment, 'value')
self.settings.unbind('widescreen-mode', self.widescreen_mode_switch, 'active')
self.extensions_manager.unbind_property('breezy-enabled', self.effect_enable_switch, 'active') self.extensions_manager.unbind_property('breezy-enabled', self.effect_enable_switch, 'active')
def reload_display_distance_toggle_button(widget): def reload_display_distance_toggle_button(widget):
@ -131,4 +163,4 @@ def on_set_display_distance_toggle(widget):
settings = SettingsManager.get_instance().settings settings = SettingsManager.get_instance().settings
distance = settings.get_double('display-distance') distance = settings.get_double('display-distance')
settings.set_double(widget.get_name(), distance) settings.set_double(widget.get_name(), distance)
reload_display_distance_toggle_button(widget) reload_display_distance_toggle_button(widget)

View File

@ -24,201 +24,366 @@
</object> </object>
</child> </child>
<child> <child>
<object class="AdwPreferencesGroup"> <object class="AdwViewStack" id="stack">
<property name="title" translatable="true">Settings</property>
<child> <child>
<object class="AdwActionRow"> <object class="AdwViewStackPage">
<property name="title" translatable="true">Effect enabled</property> <property name="name">general</property>
<property name="subtitle" translatable="true">Turn on or off the XR desktop effect</property> <property name="title">General Settings</property>
<child> <property name="icon-name">applications-system-symbolic</property>
<object class="GtkSwitch" id="effect_enable_switch"> <property name="child">
<property name="valign">3</property> <object class="GtkBox">
</object> <property name="orientation">1</property>
</child> <property name="margin-top">20</property>
</object> <property name="margin-bottom">20</property>
</child> <property name="margin-start">20</property>
<child> <property name="margin-end">20</property>
<object class="AdwActionRow"> <property name="spacing">20</property>
<property name="title" translatable="true">Display distance</property> <child>
<child> <object class="AdwPreferencesGroup">
<object class="GtkScale" id="display_distance_scale"> <property name="title" translatable="true">Features</property>
<property name="valign">3</property> <child>
<property name="draw-value">true</property> <object class="AdwActionRow">
<property name="value-pos">0</property> <property name="title" translatable="true">XR effect</property>
<property name="digits">2</property> <property name="subtitle" translatable="true">Enables the Breezy Desktop XR effect.</property>
<property name="width-request">350</property> <child>
<property name="has-origin">false</property> <object class="GtkSwitch" id="effect_enable_switch">
<property name="adjustment"> <property name="valign">3</property>
<object class="GtkAdjustment" id="display_distance_adjustment"> </object>
<property name="lower">0.2</property> </child>
<property name="upper">2.5</property> </object>
<property name="step-increment">0.01</property> </child>
<property name="value">1.05</property> <child>
<object class="AdwActionRow">
<property name="title" translatable="true">Widescreen mode</property>
<property name="subtitle" translatable="true">Switches your glasses into side-by-side mode and doubles the width of the display.</property>
<property name="valign">2</property>
<child>
<object class="GtkSwitch" id="widescreen_mode_switch">
<property name="valign">3</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Follow mode</property>
<property name="subtitle" translatable="true">Keep the virtual display near the center of your view.</property>
<child>
<object class="GtkSwitch" id="follow_mode_switch">
<property name="valign">3</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Curved display</property>
<property name="subtitle" translatable="true">Switch between flat and curved displays.</property>
<property name="valign">2</property>
<child>
<object class="GtkSwitch" id="curved_display_switch">
<property name="valign">3</property>
</object>
</child>
</object>
</child>
</object> </object>
</property> </child>
<marks> <child>
<mark value="0.2" position="bottom"></mark> <object class="AdwPreferencesGroup">
<mark value="1.0" position="bottom"></mark> <property name="title" translatable="true">Adjustments</property>
<mark value="2.5" position="bottom"></mark> <child>
</marks> <object class="AdwActionRow" id="display_distance_row">
</object> <property name="title" translatable="true">Display distance</property>
</child> <property name="subtitle" translatable="true">Closer appears larger, further appears smaller. Controls depth when in widescreen mode.</property>
</object> <child>
</child> <object class="GtkScale" id="display_distance_scale">
<child> <property name="valign">3</property>
<object class="AdwActionRow"> <property name="draw-value">true</property>
<property name="title" translatable="true">Follow enabled</property> <property name="value-pos">0</property>
<property name="subtitle" translatable="true">Keep the virtual display near the center of your view</property> <property name="digits">2</property>
<child> <property name="width-request">350</property>
<object class="GtkSwitch" id="follow_mode_switch"> <property name="has-origin">false</property>
<property name="valign">3</property> <property name="adjustment">
</object> <object class="GtkAdjustment" id="display_distance_adjustment">
</child> <property name="lower">0.2</property>
</object> <property name="upper">2.5</property>
</child> <property name="step-increment">0.01</property>
<child> <property name="value">1.05</property>
<object class="AdwActionRow"> </object>
<property name="title" translatable="true">Follow threshold</property> </property>
<property name="subtitle" translatable="true">How far away you can look before the display follows</property> <marks>
<child> <mark value="0.2" position="bottom"></mark>
<object class="GtkScale" id="follow_threshold_scale"> <mark value="1.0" position="bottom"></mark>
<property name="valign">3</property> <mark value="2.5" position="bottom"></mark>
<property name="draw-value">true</property> </marks>
<property name="value-pos">0</property> </object>
<property name="digits">0</property> </child>
<property name="width-request">350</property> </object>
<property name="has-origin">false</property> </child>
<property name="adjustment"> <child>
<object class="GtkAdjustment" id="follow_threshold_adjustment"> <object class="AdwActionRow">
<property name="lower">1</property> <property name="title" translatable="true">Display size</property>
<property name="upper">45</property> <property name="subtitle" translatable="true">Combine with display distance to achieve a comfortable level of depth and size.</property>
<property name="step-increment">1</property> <child>
<property name="value">15</property> <object class="GtkScale" id="display_size_scale">
<property name="valign">3</property>
<property name="draw-value">true</property>
<property name="value-pos">0</property>
<property name="digits">2</property>
<property name="width-request">350</property>
<property name="has-origin">false</property>
<property name="adjustment">
<object class="GtkAdjustment" id="display_size_adjustment">
<property name="lower">0.2</property>
<property name="upper">2.5</property>
<property name="step-increment">0.01</property>
<property name="value">1.0</property>
</object>
</property>
<marks>
<mark value="0.2" position="bottom"></mark>
<mark value="1.0" position="bottom"></mark>
<mark value="2.5" position="bottom"></mark>
</marks>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Display toggle distances</property>
<property name="subtitle" translatable="true">Use the buttons to capture the current display distance for use with the keyboard shortcut.</property>
<property name="valign">2</property>
<child>
<object class="GtkBox">
<property name="spacing">30</property>
<property name="width-request">150</property>
<property name="margin-start">30</property>
<child>
<object class="GtkButton" id="set_toggle_display_distance_start_button">
<property name="name">toggle-display-distance-start</property>
<property name="valign">3</property>
</object>
</child>
<child>
<object class="GtkButton" id="set_toggle_display_distance_end_button">
<property name="name">toggle-display-distance-end</property>
<property name="valign">3</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Follow threshold</property>
<property name="subtitle" translatable="true">How far away you can look before the display follows.</property>
<child>
<object class="GtkScale" id="follow_threshold_scale">
<property name="valign">3</property>
<property name="draw-value">true</property>
<property name="value-pos">0</property>
<property name="digits">0</property>
<property name="width-request">350</property>
<property name="has-origin">false</property>
<property name="adjustment">
<object class="GtkAdjustment" id="follow_threshold_adjustment">
<property name="lower">1</property>
<property name="upper">45</property>
<property name="step-increment">1</property>
<property name="value">15</property>
</object>
</property>
</object>
</child>
</object>
</child>
</object> </object>
</property> </child>
</object> </object>
</child> </property>
</object>
</child>
<child>
<object class="AdwViewStackPage">
<property name="name">shortcuts</property>
<property name="title">Keyboard Shortcuts</property>
<property name="icon-name">preferences-desktop-keyboard-shortcuts-symbolic</property>
<property name="child">
<object class="GtkBox">
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="true">Keyboard Shortcuts</property>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Re-center display shortcut</property>
<property name="subtitle" translatable="true">Pin the virtual display to the current position.</property>
<property name="valign">2</property>
<child>
<object class="GtkBox">
<property name="spacing">30</property>
<property name="margin-start">30</property>
<child>
<object class="GtkShortcutLabel" id="recenter_display_shortcut_label">
<property name="valign">3</property>
<property name="accelerator"></property>
</object>
</child>
<child>
<object class="GtkButton" id="reassign_recenter_display_shortcut_button">
<style>
<class name="row-button"/>
</style>
<property name="name">recenter-display-shortcut</property>
<property name="valign">3</property>
<property name="label" translatable="true">Change</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Display distance shortcut</property>
<property name="subtitle" translatable="true">Quickly toggle between two predefined distances.</property>
<property name="valign">2</property>
<child>
<object class="GtkBox">
<property name="spacing">30</property>
<property name="margin-start">30</property>
<child>
<object class="GtkShortcutLabel" id="toggle_display_distance_shortcut_label">
<property name="valign">3</property>
<property name="accelerator"></property>
</object>
</child>
<child>
<object class="GtkButton" id="reassign_toggle_display_distance_shortcut_button">
<style>
<class name="row-button"/>
</style>
<property name="name">toggle-display-distance-shortcut</property>
<property name="valign">3</property>
<property name="label" translatable="true">Change</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Toggle follow mode shortcut</property>
<property name="subtitle" translatable="true">Quickly toggle follow mode.</property>
<property name="valign">2</property>
<child>
<object class="GtkBox">
<property name="spacing">30</property>
<property name="margin-start">30</property>
<child>
<object class="GtkShortcutLabel" id="toggle_follow_shortcut_label">
<property name="valign">3</property>
<property name="accelerator"></property>
</object>
</child>
<child>
<object class="GtkButton" id="reassign_toggle_follow_shortcut_button">
<style>
<class name="row-button"/>
</style>
<property name="name">toggle-follow-shortcut</property>
<property name="valign">3</property>
<property name="label" translatable="true">Change</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
<child>
<object class="AdwViewStackPage">
<property name="name">advanced</property>
<property name="title">Advanced Settings</property>
<property name="icon-name">applications-system-symbolic</property>
<property name="child">
<object class="GtkBox">
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="true">Advanced Settings</property>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Find optimal display config</property>
<property name="subtitle" translatable="true">Automatically modify the glasses display configuration for maximum resolution and best scaling when plugged in.</property>
<child>
<object class="GtkSwitch" id="use_optimal_monitor_config_switch">
<property name="valign">3</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Always primary display</property>
<property name="subtitle" translatable="true">Automatically set the glasses as the primary display when plugged in.</property>
<child>
<object class="GtkSwitch" id="headset_as_primary_switch">
<property name="valign">3</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Movement look-ahead</property>
<property name="subtitle" translatable="true">Counteracts input lag by predicting head-tracking position ahead of render time. Stick with default unless virtual display drags behind your head movements, jumps ahead, or is very shaky.</property>
<child>
<object class="GtkScale" id="movement_look_ahead_scale">
<property name="valign">3</property>
<property name="draw-value">false</property>
<property name="value-pos">0</property>
<property name="digits">0</property>
<property name="width-request">350</property>
<property name="has-origin">false</property>
<property name="adjustment">
<object class="GtkAdjustment" id="movement_look_ahead_adjustment">
<property name="lower">-1</property>
<property name="upper">40</property>
<property name="step-increment">1</property>
<property name="value">-1</property>
</object>
</property>
<marks>
<mark value="-1" position="bottom">Default</mark>
<mark value="10" position="bottom">10ms</mark>
<mark value="20" position="bottom">20ms</mark>
<mark value="30" position="bottom">30ms</mark>
<mark value="40" position="bottom">40ms</mark>
</marks>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</property>
</object> </object>
</child> </child>
</object> </object>
</child> </child>
<child> <child>
<object class="AdwPreferencesGroup"> <object class="AdwViewSwitcher" id="switcher">
<property name="title" translatable="true">Shortcuts</property> <property name="stack">stack</property>
<property name="description" translatable="true">Modify keyboard shortcuts and how they work</property> <property name="policy">wide</property>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Re-center display shortcut</property>
<property name="subtitle" translatable="true">Pin the virtual display to the current position</property>
<property name="valign">2</property>
<child>
<object class="GtkBox">
<property name="spacing">30</property>
<property name="margin-start">30</property>
<child>
<object class="GtkShortcutLabel" id="recenter_display_shortcut_label">
<property name="valign">3</property>
<property name="accelerator"></property>
</object>
</child>
<child>
<object class="GtkButton" id="reassign_recenter_display_shortcut_button">
<style>
<class name="row-button"/>
</style>
<property name="name">recenter-display-shortcut</property>
<property name="valign">3</property>
<property name="label" translatable="true">Change</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Display distance shortcut</property>
<property name="subtitle" translatable="true">Quickly toggle between two predefined distances</property>
<property name="valign">2</property>
<child>
<object class="GtkBox">
<property name="spacing">30</property>
<property name="margin-start">30</property>
<child>
<object class="GtkShortcutLabel" id="toggle_display_distance_shortcut_label">
<property name="valign">3</property>
<property name="accelerator"></property>
</object>
</child>
<child>
<object class="GtkButton" id="reassign_toggle_display_distance_shortcut_button">
<style>
<class name="row-button"/>
</style>
<property name="name">toggle-display-distance-shortcut</property>
<property name="valign">3</property>
<property name="label" translatable="true">Change</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Display distance start and end</property>
<property name="subtitle" translatable="true">Use the buttons to capture the current display distance as start and end points.</property>
<property name="valign">2</property>
<child>
<object class="GtkBox">
<property name="spacing">30</property>
<property name="width-request">150</property>
<property name="margin-start">30</property>
<child>
<object class="GtkButton" id="set_toggle_display_distance_start_button">
<property name="name">toggle-display-distance-start</property>
<property name="valign">3</property>
</object>
</child>
<child>
<object class="GtkButton" id="set_toggle_display_distance_end_button">
<property name="name">toggle-display-distance-end</property>
<property name="valign">3</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Toggle follow mode shortcut</property>
<property name="subtitle" translatable="true">Quickly toggle follow mode</property>
<property name="valign">2</property>
<child>
<object class="GtkBox">
<property name="spacing">30</property>
<property name="margin-start">30</property>
<child>
<object class="GtkShortcutLabel" id="toggle_follow_shortcut_label">
<property name="valign">3</property>
<property name="accelerator"></property>
</object>
</child>
<child>
<object class="GtkButton" id="reassign_toggle_follow_shortcut_button">
<style>
<class name="row-button"/>
</style>
<property name="name">toggle-follow-shortcut</property>
<property name="valign">3</property>
<property name="label" translatable="true">Change</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object> </object>
</child> </child>
</template> </template>

View File

@ -57,11 +57,11 @@
</object> </object>
</child> </child>
<child> <child>
<object class="GtkBox" id="tiers"> <object class="GtkBox" id="features">
</object> </object>
</child> </child>
<child> <child>
<object class="GtkBox" id="features"> <object class="GtkBox" id="tiers">
</object> </object>
</child> </child>
</object> </object>

View File

@ -3,10 +3,10 @@ from gi.repository import Adw
from .time import time_remaining_text from .time import time_remaining_text
FEATURE_NAMES = { FEATURE_NAMES = {
'sbs': 'Side-by-side mode (for gaming)', 'sbs': 'Side-by-side mode (gaming)',
'smooth_follow': 'Smooth Follow', 'smooth_follow': 'Smooth Follow (gaming)',
'productivity_basic': 'Breezy Desktop', 'productivity_basic': 'Breezy Desktop (productivity)',
'productivity_pro': 'Breezy Desktop w/ multiple monitors', 'productivity_pro': 'Breezy Desktop Pro (productivity)',
} }
class LicenseFeatureRow(Adw.ActionRow): class LicenseFeatureRow(Adw.ActionRow):

View File

@ -53,7 +53,7 @@ class LicenseTierRow(Adw.ExpanderRow):
elif active_period is not None: elif active_period is not None:
amount_text += " to upgrade" amount_text += " to upgrade"
elif active_period is not None and PERIOD_RANKS[period] >= PERIOD_RANKS[active_period]: elif active_period is not None and PERIOD_RANKS[period] >= PERIOD_RANKS[active_period]:
amount_text = "Ready to auto-renew" amount_text = "Paid through next renewal period"
if amount_text is not None: if amount_text is not None:
row_widget = Adw.ActionRow(title=period.capitalize()) row_widget = Adw.ActionRow(title=period.capitalize())

View File

@ -22,6 +22,7 @@ class StateManager(GObject.GObject):
__gproperties__ = { __gproperties__ = {
'follow-mode': (bool, 'Follow Mode', 'Whether the follow mode is enabled', False, GObject.ParamFlags.READWRITE), 'follow-mode': (bool, 'Follow Mode', 'Whether the follow mode is enabled', False, GObject.ParamFlags.READWRITE),
'follow-threshold': (float, 'Follow Threshold', 'The follow threshold', 1.0, 45.0, 15.0, GObject.ParamFlags.READWRITE), 'follow-threshold': (float, 'Follow Threshold', 'The follow threshold', 1.0, 45.0, 15.0, GObject.ParamFlags.READWRITE),
'widescreen-mode': (bool, 'Widescreen Mode', 'Whether widescreen mode is enabled', False, GObject.ParamFlags.READWRITE),
'license-action-needed': (bool, 'License Action Needed', 'Whether the license needs attention', False, GObject.ParamFlags.READWRITE), 'license-action-needed': (bool, 'License Action Needed', 'Whether the license needs attention', False, GObject.ParamFlags.READWRITE),
'license-present': (bool, 'License Present', 'Whether a license is present', False, GObject.ParamFlags.READWRITE), 'license-present': (bool, 'License Present', 'Whether a license is present', False, GObject.ParamFlags.READWRITE),
'enabled-features-list': (object, 'Enabled Features List', 'A list of the enabled features', GObject.ParamFlags.READWRITE), 'enabled-features-list': (object, 'Enabled Features List', 'A list of the enabled features', GObject.ParamFlags.READWRITE),
@ -94,12 +95,15 @@ class StateManager(GObject.GObject):
self.set_property('license-present', False) self.set_property('license-present', False)
self.set_property('follow-mode', self.state.get('breezy_desktop_smooth_follow_enabled')) self.set_property('follow-mode', self.state.get('breezy_desktop_smooth_follow_enabled'))
self.set_property('widescreen-mode', self.state.get('sbs_mode_enabled'))
if self.running: threading.Timer(1.0, self._refresh_state).start() if self.running: threading.Timer(1.0, self._refresh_state).start()
def do_set_property(self, prop, value): def do_set_property(self, prop, value):
if prop.name == 'follow-mode': if prop.name == 'follow-mode':
self.follow_mode = value self.follow_mode = value
if prop.name == 'widescreen-mode':
self.widescreen_mode = value
if prop.name == 'license-action-needed': if prop.name == 'license-action-needed':
self.license_action_needed = value self.license_action_needed = value
if prop.name == 'license-present': if prop.name == 'license-present':
@ -110,6 +114,8 @@ class StateManager(GObject.GObject):
def do_get_property(self, prop): def do_get_property(self, prop):
if prop.name == 'follow-mode': if prop.name == 'follow-mode':
return self.follow_mode return self.follow_mode
if prop.name == 'widescreen-mode':
return self.widescreen_mode
if prop.name == 'license-action-needed': if prop.name == 'license-action-needed':
return self.license_action_needed return self.license_action_needed
if prop.name == 'license-present': if prop.name == 'license-present':