From 7843d4d38531033b7457603cb912639b05ffc8af Mon Sep 17 00:00:00 2001
From: Wayne Heaney <42350981+wheaney@users.noreply.github.com>
Date: Mon, 8 Jul 2024 15:02:04 -0700
Subject: [PATCH] Widescreen/SBS support (#28)
Squashed 28 commits
---
bin/breezy_gnome_setup | 12 +-
gnome/bin/dev/use_local_extension.sh | 11 +-
gnome/src/cursormanager.js | 158 ++++--
gnome/src/extension.js | 283 +++++++--
gnome/src/ipc.js | 12 +-
gnome/src/monitormanager.js | 198 ++++++-
gnome/src/time.js | 4 +
gnome/src/xrEffect.js | 291 +++++++---
modules/XRLinuxDriver | 2 +-
modules/sombrero | 2 +-
.../com.xronlinux.BreezyDesktop.gschema.xml | 63 +++
ui/modules/PyXRLinuxDriverIPC | 2 +-
ui/src/connecteddevice.py | 40 +-
ui/src/gtk/connected-device.ui | 535 ++++++++++++------
ui/src/gtk/license-dialog.ui | 4 +-
ui/src/gtk/window.ui | 4 +
ui/src/licensefeaturerow.py | 8 +-
ui/src/licensetierrow.py | 2 +-
ui/src/main.py | 6 +
ui/src/statemanager.py | 6 +
20 files changed, 1230 insertions(+), 413 deletions(-)
diff --git a/bin/breezy_gnome_setup b/bin/breezy_gnome_setup
index 798b3a7..c3714cd 100755
--- a/bin/breezy_gnome_setup
+++ b/bin/breezy_gnome_setup
@@ -18,21 +18,23 @@ tmp_dir=$(mktemp -d -t breezy-gnome-XXXXXXXXXX)
pushd $tmp_dir > /dev/null
echo "Created temp directory: ${tmp_dir}"
-# if the first argument is "-v" then the second argument is metrics version, and the third argument is binary path
-# otherwise, if the first argument is present, it's the binary path
+binary_download_url="https://github.com/wheaney/breezy-desktop/releases/latest/download/breezyGNOME.tar.gz"
if [ "$1" = "-v" ]
then
metrics_version="$2"
binary_path_arg="$3"
+elif [ "$1" = "--tag" ] && [ -n "$2" ]
+then
+ binary_download_url="https://github.com/wheaney/breezy-desktop/releases/download/$2/breezyGNOME.tar.gz"
else
binary_path_arg="$1"
fi
if [ -z "$binary_path_arg" ]
then
- # download and unzip the latest driver
- echo "Downloading latest release to: ${tmp_dir}/breezyGNOME.tar.gz"
- curl -L -O https://github.com/wheaney/breezy-desktop/releases/latest/download/breezyGNOME.tar.gz
+ # download and unzip the binary
+ echo "Downloading to: ${tmp_dir}/breezyGNOME.tar.gz"
+ curl -L -O $binary_download_url
else
if [[ "$binary_path_arg" = /* ]]; then
abs_path="$binary_path_arg"
diff --git a/gnome/bin/dev/use_local_extension.sh b/gnome/bin/dev/use_local_extension.sh
index 54e9577..f959f13 100755
--- a/gnome/bin/dev/use_local_extension.sh
+++ b/gnome/bin/dev/use_local_extension.sh
@@ -6,6 +6,7 @@ SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
if [ -z "$XDG_DATA_HOME" ]; then
XDG_DATA_HOME="$USER_HOME/.local/share"
fi
+DATA_DIR="$XDG_DATA_HOME/breezy_gnome"
# if $XDG_DATA_HOME/gnome-shell/extensions/breezydesktop@xronlinux.com exists
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
cp -rL $SCRIPT_DIR/../../src $extension_path
-glib-compile-schemas $extension_path/schemas
\ No newline at end of file
+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
diff --git a/gnome/src/cursormanager.js b/gnome/src/cursormanager.js
index 545619f..c2f5e82 100644
--- a/gnome/src/cursormanager.js
+++ b/gnome/src/cursormanager.js
@@ -1,4 +1,5 @@
import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
import Meta from 'gi://Meta';
import * as PointerWatcher from 'resource:///org/gnome/shell/ui/pointerWatcher.js';
import { MouseSpriteContent } from './cursor.js';
@@ -6,10 +7,9 @@ import Globals from './globals.js';
// Taken from https://github.com/jkitching/soft-brightness-plus
export class CursorManager {
- constructor(mainActor) {
+ constructor(mainActor, refreshRate) {
this._mainActor = mainActor;
-
- this._changeHookFn = null;
+ this._refreshRate = refreshRate;
// Set/destroyed by _enableCloningMouse/_disableCloningMouse
this._cursorWantedVisible = null;
@@ -26,7 +26,7 @@ export class CursorManager {
this._cursorWatch = null;
this._cursorChangedConnection = null;
this._cursorVisibilityChangedConnection = null;
- this._cursorPositionInvalidatedConnection = null;
+ this._redraw_timeline = null;
}
enable() {
@@ -71,11 +71,7 @@ export class CursorManager {
this._cursorSprite.content = new MouseSpriteContent();
this._cursorActor = new Clutter.Actor();
- if (Clutter.Container === undefined) {
- this._cursorActor.add_child(this._cursorSprite);
- } else {
- this._cursorActor.add_actor(this._cursorSprite);
- }
+ this._cursorActor.add_child(this._cursorSprite);
this._cursorWatcher = PointerWatcher.getPointerWatcher();
this._cursorSeat = Clutter.get_default_backend().get_default_seat();
}
@@ -92,6 +88,11 @@ export class CursorManager {
Meta.CursorTracker.prototype.set_pointer_visible = this._cursorTrackerSetPointerVisible;
this._cursorTracker.set_pointer_visible(this._cursorWantedVisible);
+ if (this._cursorSprite) {
+ this._cursorSprite.content = null;
+ if (this._cursorActor) this._cursorActor.remove_child(this._cursorSprite);
+ }
+
this._cursorWantedVisible = null;
this._cursorTracker = null;
this._cursorTrackerSetPointerVisible = null;
@@ -123,23 +124,32 @@ export class CursorManager {
// prereqs: setup in _enableCloningMouse, _cursorWantedVisible is true
_startCloningMouse() {
Globals.logger.log_debug('CursorManager _startCloningMouse');
- if (this._cursorWatch == null) {
- if (Clutter.Container === undefined) {
- this._mainActor.add_child(this._cursorActor);
- } else {
- 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));
+ this._mainActor.add_child(this._cursorActor);
+ this._cursorChangedConnection = this._cursorTracker.connect('cursor-changed', this._queueSpriteUpdate.bind(this));
+ this._cursorVisibilityChangedConnection = this._cursorTracker.connect('visibility-changed', this._queueVisibilityUpdate.bind(this));
- const interval = 1000 / 250;
- this._cursorWatch = this._cursorWatcher.addWatch(interval, this._updateMousePosition.bind(this));
+ const interval = 1000.0 / this._refreshRate;
+ this._redraw_timeline = Clutter.Timeline.new_for_actor(this._mainActor, interval * 10);
+ this._redraw_timeline.connect('new-frame', (() => {
+ this.handleNewFrame();
+ }).bind(this));
- const [x, y] = global.get_pointer();
- this._updateMousePosition(x, y);
- this._updateMouseSprite();
- }
+ // 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._redraw_timeline.connect('completed', (() => {
+ this._periodicReset();
+ }).bind(this));
+
+ this._redraw_timeline.set_repeat_count(-1);
+ this._redraw_timeline.start();
+
+ this._cursorWatch = this._cursorWatcher.addWatch(interval, this._queuePositionUpdate.bind(this));
+
+ const [x, y] = global.get_pointer();
+ this._queuePositionUpdate(x, y);
+ this._queueSpriteUpdate();
if (this._cursorTracker.set_keep_focus_while_hidden) {
this._cursorTracker.set_keep_focus_while_hidden(true);
@@ -163,27 +173,25 @@ export class CursorManager {
if (this._cursorWatch != null) {
this._cursorWatch.remove();
this._cursorWatch = null;
+ }
+ if (this._cursorChangedConnection) {
this._cursorTracker.disconnect(this._cursorChangedConnection);
this._cursorChangedConnection = null;
+ }
+ if (this._cursorVisibilityChangedConnection) {
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) {
- this._cursorTracker.set_keep_focus_while_hidden(false);
+ if (this._redraw_timeline) {
+ this._redraw_timeline.stop();
+ this._redraw_timeline = null;
}
+ if (this._cursorActor) this._mainActor.remove_child(this._cursorActor);
+
if (this._cursorUnfocusInhibited) {
Globals.logger.log_debug('uninhibit_unfocus');
this._cursorSeat.uninhibit_unfocus();
@@ -191,31 +199,71 @@ export class CursorManager {
}
}
- _updateMousePosition(x, y) {
- this._cursorActor.set_position(x, y);
+ _queuePositionUpdate(x, y) {
+ this._queued_cursor_position = [x, y];
}
- _updateMouseSprite() {
- const sprite = this._cursorTracker.get_sprite();
- if (sprite) {
- this._cursorSprite.content.texture = sprite;
- this._cursorSprite.show();
- } else {
- this._cursorSprite.hide();
- }
+ _queueSpriteUpdate() {
+ this._queued_sprite_update = true;
+ }
- const [xHot, yHot] = this._cursorTracker.get_hot();
- this._cursorSprite.set({
- translation_x: -xHot,
- translation_y: -yHot,
- });
- this._mainActor.set_child_above_sibling(this._cursorActor, null);
+ _queueVisibilityUpdate() {
+ this._queued_visibility_update = true;
this._cursorTrackerSetPointerVisibleBound(false);
+ this._queueSpriteUpdate();
+ }
- // some other processes are uninhibiting when they shouldn't, so we need to re-inhibit here
- if (!this._cursorSeat.is_unfocus_inhibited() && this._cursorUnfocusInhibited) {
- Globals.logger.log_debug('reinhibiting');
- this._cursorSeat.inhibit_unfocus();
+ // updates the stacking and other attributes that are hard to track and may periodically get out of sync
+ _periodicReset() {
+ this._queue_reset = true;
+ this._queueVisibilityUpdate();
+ }
+
+ handleNewFrame() {
+ let redraw = false;
+ if (this._queued_cursor_position) {
+ const [x, y] = this._queued_cursor_position;
+ this._cursorActor.set_position(x, y);
+ this._queued_cursor_position = null;
+ redraw = true;
}
+
+ if (this._queued_sprite_update) {
+ const sprite = this._cursorTracker.get_sprite();
+ if (sprite) {
+ this._cursorSprite.content.texture = sprite;
+ this._cursorSprite.show();
+ } else {
+ this._cursorSprite.hide();
+ }
+
+ const [xHot, yHot] = this._cursorTracker.get_hot();
+ this._cursorSprite.set({
+ translation_x: -xHot,
+ translation_y: -yHot,
+ });
+ this._queued_sprite_update = false;
+ redraw = true;
+ }
+
+ if (this._queued_visibility_update) {
+ this._queued_visibility_update = false;
+ redraw = true;
+ }
+
+ if (this._queue_reset) {
+ if (this._mainActor.get_last_child() !== this._cursorActor)
+ this._mainActor.set_child_above_sibling(this._cursorActor, null);
+
+ // some other processes are uninhibiting when they shouldn't, so we need to re-inhibit here
+ if (!this._cursorSeat.is_unfocus_inhibited() && this._cursorUnfocusInhibited) {
+ Globals.logger.log_debug('reinhibiting');
+ this._cursorSeat.inhibit_unfocus();
+ }
+ this._queue_reset = false;
+ redraw = true;
+ }
+
+ return redraw;
}
}
\ No newline at end of file
diff --git a/gnome/src/extension.js b/gnome/src/extension.js
index 4f56a51..62cdcac 100644
--- a/gnome/src/extension.js
+++ b/gnome/src/extension.js
@@ -8,7 +8,8 @@ import St from 'gi://St';
import { CursorManager } from './cursormanager.js';
import Globals from './globals.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 {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
@@ -20,8 +21,7 @@ const SUPPORTED_MONITOR_PRODUCTS = [
'Air',
'Air 2', // guessing this one
'Air 2 Pro',
- 'SmartGlasses', // TCL/RayNeo
- 'MetaMonitor' // nested mode dummy monitor
+ 'SmartGlasses' // TCL/RayNeo
];
export default class BreezyDesktopExtension extends Extension {
@@ -40,8 +40,17 @@ export default class BreezyDesktopExtension extends Extension {
this._distance_binding = null;
this._distance_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._end_binding = null;
+ this._curved_display_binding = null;
+ this._display_size_binding = null;
+ this._look_ahead_override_binding = null;
+ this._disable_anti_aliasing_binding = null;
+ this._optimal_monitor_config_binding = null;
+ this._headset_as_primary_binding = null;
if (!Globals.logger) {
Globals.logger = new Logger({
@@ -59,13 +68,22 @@ export default class BreezyDesktopExtension extends Extension {
Globals.extension_dir = this.path;
this.settings.bind('debug', Globals.logger, 'debug', Gio.SettingsBindFlags.DEFAULT);
- this._monitor_manager = new MonitorManager(this.path);
- this._monitor_manager.setChangeHook(this._setup.bind(this));
+ this._monitor_manager = new MonitorManager({
+ 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._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();
} catch (e) {
- Globals.logger.log(`ERROR: BreezyDesktopExtension enable ${e.message}`, e.stack);
+ Globals.logger.log(`ERROR: BreezyDesktopExtension enable ${e.message}\n${e.stack}`);
}
}
@@ -74,12 +92,21 @@ export default class BreezyDesktopExtension extends Extension {
var target_monitor = this._target_monitor;
var is_effect_running = this._is_effect_running;
this._running_poller_id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, (() => {
- if (is_effect_running) return GLib.SOURCE_REMOVE;
+ if (is_effect_running) {
+ this._running_poller_id = undefined;
+ return GLib.SOURCE_REMOVE;
+ }
const is_driver_running = this._check_driver_running();
if (is_driver_running && target_monitor) {
- Globals.logger.log('Driver is running, supported monitor connected. Enabling XR effect.');
- this._effect_enable();
+ // Don't enable the effect yet if monitor updates are needed.
+ // _setup will be triggered again since a !ready result means it will trigger monitor changes,
+ // so we can remove this timeout_add no matter what.
+ if (this._target_monitor_ready(target_monitor)) {
+ Globals.logger.log('Driver is running, supported monitor connected. Enabling XR effect.');
+ this._effect_enable();
+ }
+ this._running_poller_id = undefined;
return GLib.SOURCE_REMOVE;
} else {
return GLib.SOURCE_CONTINUE;
@@ -93,57 +120,113 @@ export default class BreezyDesktopExtension extends Extension {
const target_monitor = this._monitor_manager.getMonitorPropertiesList()?.find(
monitor => SUPPORTED_MONITOR_PRODUCTS.includes(monitor.product));
if (target_monitor !== undefined) {
+ Globals.logger.log_debug(`BreezyDesktopExtension _find_supported_monitor - Identified supported monitor: ${target_monitor.connector}`);
return {
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')) {
+ Globals.logger.log_debug('BreezyDesktopExtension _find_supported_monitor - Using dummy monitor');
// allow testing XR devices with just USB, no video needed
return {
monitor: this._monitor_manager.getMonitors()[0],
+ connector: 'dummy',
refreshRate: 60,
+ is_dummy: true
};
}
+ Globals.logger.log_debug('BreezyDesktopExtension _find_supported_monitor - No supported monitor found');
return null;
} 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;
}
}
+ // Assumes target_monitor is set, and was returned by _find_supported_monitor.
+ // A false result means we'll expect _handle_monitor_change to be triggered, so active polling
+ // can be disabled.
+ _target_monitor_ready(target_monitor) {
+ if (target_monitor.is_dummy) {
+ // do this so it updates the state, if needed, but don't worry about the result since
+ // it won't trigger a monitor change
+ this._needs_widescreen_monitor_update();
+ return true;
+ }
+
+ // widescreen check should always be first since it changes the type of monitor, and we wouldn't
+ // want to update the optimal mode if the monitor configs are going to change again
+ return !this._needs_widescreen_monitor_update() &&
+ !this._monitor_manager.needsOptimalModeCheck(target_monitor.connector);
+ }
+
_setup() {
Globals.logger.log_debug('BreezyDesktopExtension _setup');
if (this._is_effect_running) {
- Globals.logger.log('Monitors changed, disabling XR effect');
- this._effect_disable();
+ Globals.logger.log('Reset triggered, disabling XR effect');
+ this._effect_disable(true);
}
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 && this._running_poller_id === undefined) {
- this._target_monitor = target_monitor.monitor;
- this._refresh_rate = target_monitor.refreshRate;
+ this._target_monitor = target_monitor;
if (this._check_driver_running()) {
- Globals.logger.log('Ready, enabling XR effect');
- this._effect_enable();
+ // Don't enable the effect yet if monitor updates are needed.
+ // _setup will be triggered again since a !ready result means it will trigger monitor changes
+ if (this._target_monitor_ready(target_monitor)) {
+ Globals.logger.log('Ready, enabling XR effect');
+ this._effect_enable();
+ } else {
+ Globals.logger.log_debug('BreezyDesktopExtension _setup - driver running but async monitor action needed');
+ }
} else {
+ Globals.logger.log_debug('BreezyDesktopExtension _setup - driver not running, starting poller');
this._poll_for_ready();
}
+ } else {
+ if (!target_monitor) {
+ Globals.logger.log_debug(`BreezyDesktopExtension _setup - Doing nothing, no supported monitor found`);
+ } else {
+ Globals.logger.log_debug(`BreezyDesktopExtension _setup - Doing nothing, target monitor found, waiting for poller to pick it up`);
+ }
}
}
_check_driver_running() {
try {
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) {
- Globals.logger.log(`ERROR: BreezyDesktopExtension _check_driver_running ${e.message}`, e.stack);
- return false;
+ Globals.logger.log(`ERROR: BreezyDesktopExtension _check_driver_running ${e.message}\n${e.stack}`);
}
+
+ return false;
+ }
+
+ _needs_widescreen_monitor_update() {
+ Globals.logger.log_debug('BreezyDesktopExtension _needs_widescreen_monitor_update');
+ const state = this._read_state(['sbs_mode_enabled']);
+ const sbs_enabled = state['sbs_mode_enabled'] === 'true';
+ const widescreen_setting_enabled = this.settings.get_boolean('widescreen-mode');
+ if (widescreen_setting_enabled !== sbs_enabled) {
+ Globals.logger.log_debug('BreezyDesktopExtension _needs_widescreen_monitor_update - true');
+ this._write_control('sbs_mode', widescreen_setting_enabled ? 'enable' : 'disable');
+ return true;
+ }
+
+ return false;
}
_effect_enable() {
@@ -153,23 +236,21 @@ export default class BreezyDesktopExtension extends Extension {
this._is_effect_running = true;
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);
this._cursor_manager.enable();
- this._overlay = new St.Bin({ style: 'background-color: rgba(0, 0, 0, 1);'});
+ this._overlay = new St.Bin();
this._overlay.opacity = 255;
- this._overlay.set_position(this._target_monitor.x, this._target_monitor.y);
- this._overlay.set_size(this._target_monitor.width, this._target_monitor.height);
+ this._overlay.set_position(targetMonitor.x, targetMonitor.y);
+ this._overlay.set_size(targetMonitor.width, targetMonitor.height);
const overlayContent = new Clutter.Actor({clip_to_allocation: true});
const uiClone = new Clutter.Clone({ source: Main.layoutManager.uiGroup, clip_to_allocation: true });
- uiClone.x = -this._target_monitor.x;
- uiClone.y = -this._target_monitor.y;
- if (Clutter.Container === undefined) {
- overlayContent.add_child(uiClone);
- } else {
- overlayContent.add_actor(uiClone);
- }
+ uiClone.x = -targetMonitor.x;
+ uiClone.y = -targetMonitor.y;
+ overlayContent.add_child(uiClone);
this._overlay.set_child(overlayContent);
@@ -177,21 +258,31 @@ export default class BreezyDesktopExtension extends Extension {
Shell.util_set_hidden_from_pick(this._overlay, true);
this._xr_effect = new XREffect({
- target_monitor: this._target_monitor,
- target_framerate: this._refresh_rate ?? 60,
+ target_monitor: targetMonitor,
+ target_framerate: refreshRate,
display_distance: this.settings.get_double('display-distance'),
toggle_display_distance_start: this.settings.get_double('toggle-display-distance-start'),
toggle_display_distance_end: this.settings.get_double('toggle-display-distance-end'),
+ look_ahead_override: this.settings.get_int('look-ahead-override'),
+ disable_anti_aliasing: this.settings.get_boolean('disable-anti-aliasing')
});
- this._update_display_distance(this.settings);
this._update_follow_threshold(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_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._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._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._disable_anti_aliasing_binding = this.settings.bind('disable-anti-aliasing', this._xr_effect, 'disable-anti-aliasing', Gio.SettingsBindFlags.DEFAULT);
this._overlay.add_effect_with_name('xr-desktop', this._xr_effect);
Meta.disable_unredirect_for_display(global.display);
@@ -200,7 +291,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-follow-shortcut', this._toggle_follow_mode.bind(this));
} 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();
}
}
@@ -231,11 +322,11 @@ export default class BreezyDesktopExtension extends Extension {
bind_to_function
);
} 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) {
- 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}`);
}
}
@@ -246,6 +337,27 @@ export default class BreezyDesktopExtension extends Extension {
stream.close(null);
}
+ _read_state(keys) {
+ const state = {};
+ const file = Gio.file_new_for_path('/dev/shm/xr_driver_state');
+ if (file.query_exists(null)) {
+ const data = file.load_contents(null);
+
+ if (data[0]) {
+ const bytes = new Uint8Array(data[1]);
+ const decoder = new TextDecoder();
+ const contents = decoder.decode(bytes);
+
+ const lines = contents.split('\n');
+ for (const line of lines) {
+ const [k, v] = line.split('=');
+ if (keys.includes(k)) state[k] = v;
+ }
+ }
+ }
+ return state;
+ }
+
_update_display_distance(settings, event) {
const value = settings.get_double('display-distance');
Globals.logger.log_debug(`BreezyDesktopExtension _update_display_distance ${value}`);
@@ -258,6 +370,40 @@ export default class BreezyDesktopExtension extends Extension {
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() {
Globals.logger.log_debug('BreezyDesktopExtension _recenter_display');
this._write_control('recenter_screen', 'true');
@@ -268,12 +414,16 @@ export default class BreezyDesktopExtension extends Extension {
this._write_control('toggle_breezy_desktop_smooth_follow', 'true');
}
- _effect_disable() {
+ // for_setup should be true if our intention is to immediately re-enable the extension
+ _effect_disable(for_setup = false) {
try {
Globals.logger.log_debug('BreezyDesktopExtension _effect_disable');
this._is_effect_running = false;
- if (this._running_poller_id) GLib.source_remove(this._running_poller_id);
+ if (this._running_poller_id) {
+ GLib.source_remove(this._running_poller_id);
+ this._running_poller_id = undefined;
+ }
Main.wm.removeKeybinding('recenter-display-shortcut');
Main.wm.removeKeybinding('toggle-display-distance-shortcut');
@@ -281,12 +431,13 @@ export default class BreezyDesktopExtension extends Extension {
Meta.enable_unredirect_for_display(global.display);
if (this._overlay) {
- global.stage.remove_child(this._overlay);
+ if (this._xr_effect) this._xr_effect.cleanup();
this._overlay.remove_effect_by_name('xr-desktop');
+
+ global.stage.remove_child(this._overlay);
this._overlay.destroy();
this._overlay = null;
}
-
if (this._distance_binding) {
this.settings.unbind(this._distance_binding);
this._distance_binding = null;
@@ -299,6 +450,10 @@ export default class BreezyDesktopExtension extends Extension {
this.settings.disconnect(this._follow_threshold_connection);
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) {
this.settings.unbind(this._start_binding);
this._start_binding = null;
@@ -307,17 +462,46 @@ export default class BreezyDesktopExtension extends Extension {
this.settings.unbind(this._end_binding);
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._disable_anti_aliasing_binding) {
+ this.settings.unbind(this._disable_anti_aliasing_binding);
+ this._disable_anti_aliasing_binding = null;
+ }
if (this._xr_effect) {
- this._xr_effect.cleanup();
+ 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 = null;
}
-
if (this._cursor_manager) {
this._cursor_manager.disable();
this._cursor_manager = null;
}
+
+ // this should always be done at the end of this function after the widescreen settings binding is removed,
+ // so it doesn't reset the setting to false
+ if (!for_setup && this.settings.get_boolean('widescreen-mode')) {
+ Globals.logger.log('Disabling SBS mode due to disabling effect');
+ this._write_control('sbs_mode', 'disable');
+ }
} 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 +511,20 @@ export default class BreezyDesktopExtension extends Extension {
this._effect_disable();
this._target_monitor = null;
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 = null;
}
} catch (e) {
- Globals.logger.log(`ERROR: BreezyDesktopExtension disable ${e.message}`, e.stack);
+ Globals.logger.log(`ERROR: BreezyDesktopExtension disable ${e.message}\n${e.stack}`);
}
}
}
diff --git a/gnome/src/ipc.js b/gnome/src/ipc.js
index 77d8fa1..a729368 100644
--- a/gnome/src/ipc.js
+++ b/gnome/src/ipc.js
@@ -24,7 +24,7 @@ export function dataViewBigUint(dataView, dataViewInfo) {
return Number(dataView.getBigUint64(dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX], true));
}
-export function dataViewUintArray(dataView, dataViewInfo) {
+export function dataViewUint32Array(dataView, dataViewInfo) {
const uintArray = []
let offset = dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX];
for (let i = 0; i < dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX]; i++) {
@@ -34,6 +34,16 @@ export function dataViewUintArray(dataView, dataViewInfo) {
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) {
return dataView.getFloat32(dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX], true);
}
diff --git a/gnome/src/monitormanager.js b/gnome/src/monitormanager.js
index 0eacea5..8126ecf 100644
--- a/gnome/src/monitormanager.js
+++ b/gnome/src/monitormanager.js
@@ -17,6 +17,7 @@
// along with this program. If not, see .
import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
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) {
- displayConfigProxy.GetResourcesRemote((result) => {
- if (result.length <= 2) {
- callback(null, 'Cannot get DisplayConfig: No outputs in GetResources()');
+function getMonitorConfig(displayConfigProxy, callback) {
+ displayConfigProxy.GetResourcesRemote((result, error) => {
+ if (error) {
+ callback(null, `GetResourcesRemote failed: ${error}`);
} else {
const monitors = [];
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
-export default class MonitorManager {
- constructor(extPath) {
- this._extPath = extPath;
+export const MonitorManager = GObject.registerClass({
+ Properties: {
+ '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._displayConfigProxy = null;
this._backendManager = null;
this._monitorProperties = null;
this._changeHookFn = null;
+ this._needsConfigCheck = this.use_optimal_monitor_config;
}
enable() {
+ Globals.logger.log_debug('MonitorManager enable');
this._backendManager = global.backend.get_monitor_manager();
- newDisplayConfig(this._extPath, (proxy, error) => {
+ newDisplayConfig(this.extension_path, ((proxy, error) => {
if (error) {
return;
}
this._displayConfigProxy = proxy;
this._on_monitors_change();
- });
+ }).bind(this));
this._monitorsChangedConnection = Main.layoutManager.connect('monitors-changed', this._on_monitors_change.bind(this));
}
disable() {
+ Globals.logger.log_debug('MonitorManager disable');
Main.layoutManager.disconnect(this._monitorsChangedConnection);
this._monitorsChangedConnection = null;
@@ -123,10 +255,6 @@ export default class MonitorManager {
this._changeHookFn = fn;
}
- setPostCallback(callback) {
- this._postCallback = callback;
- }
-
getMonitors() {
return Main.layoutManager.monitors;
}
@@ -135,11 +263,45 @@ export default class MonitorManager {
return this._monitorProperties;
}
+ // returns true if a check is needed, caller should wait for the next change hook call
+ needsOptimalModeCheck(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() {
+ Globals.logger.log_debug('MonitorManager _on_monitors_change');
if (this._displayConfigProxy == null) {
return;
}
- getMonitorConfig(this._displayConfigProxy, (result, error) => {
+ this._needsConfigCheck = this.use_optimal_monitor_config;
+ getMonitorConfig(this._displayConfigProxy, ((result, error) => {
if (error) {
return;
}
@@ -161,9 +323,11 @@ export default class MonitorManager {
}
}
this._monitorProperties = monitorProperties;
- if (this._changeHookFn !== null) {
+ if (!!this._changeHookFn) {
this._changeHookFn();
+ } else {
+ Globals.logger.log('MonitorManager _on_monitors_change: can\'t trigger change hook, no hook set!');
}
- });
+ }).bind(this));
}
-}
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/gnome/src/time.js b/gnome/src/time.js
index 6c3259c..7883b9b 100644
--- a/gnome/src/time.js
+++ b/gnome/src/time.js
@@ -4,4 +4,8 @@ export function getEpochSec() {
export function toSec(milliseconds) {
return Math.floor(milliseconds / 1000);
+}
+
+export function isValidKeepAlive(dateSec, strictCheck = false) {
+ return Math.abs(toSec(Date.now()) - dateSec) <= (strictCheck ? 1 : 5);
}
\ No newline at end of file
diff --git a/gnome/src/xrEffect.js b/gnome/src/xrEffect.js
index 0c0b15b..2680cae 100644
--- a/gnome/src/xrEffect.js
+++ b/gnome/src/xrEffect.js
@@ -11,7 +11,8 @@ import {
dataViewEnd,
dataViewUint8,
dataViewBigUint,
- dataViewUintArray,
+ dataViewUint32Array,
+ dataViewUint8Array,
dataViewFloat,
dataViewFloatArray,
BOOL_SIZE,
@@ -23,12 +24,12 @@ import {
} from "./ipc.js";
import { degreeToRadian } from "./math.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";
// 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]
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 EPOCH_MS = [dataViewEnd(CUSTOM_BANNER_ENABLED), UINT_SIZE, 2];
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
const shaderUniformLocations = {
@@ -50,20 +52,23 @@ const shaderUniformLocations = {
'imu_quat_data': null,
'look_ahead_cfg': null,
'look_ahead_ms': null,
- 'stage_aspect_ratio': null,
- 'display_aspect_ratio': null,
'trim_width_percent': null,
'trim_height_percent': null,
- 'display_zoom': null,
+ 'display_size': null,
'display_north_offset': null,
- 'lens_distance_ratio': null,
+ 'lens_vector': null,
+ 'lens_vector_r': null, // only used if sbs_enabled is true
+ 'texcoord_x_limits': null, // index 0: min; index 1: max
+ 'texcoord_x_limits_r': null, // only used if sbs_enabled is true
'sbs_enabled': null,
- 'sbs_content': null,
'custom_banner_enabled': null,
'half_fov_z_rads': null,
'half_fov_y_rads': null,
- 'screen_distance': null,
- 'display_res': null
+ 'fov_half_widths': null,
+ 'fov_widths': null,
+ 'display_resolution': null,
+ 'source_to_display_ratio': null,
+ 'curved_display': null
};
function setUniformFloat(effect, locationName, dataViewInfo, value) {
@@ -109,67 +114,121 @@ function lookAheadMS(dataView) {
// most uniforms don't change frequently, this function should be called periodically
function setIntermittentUniformVariables() {
- const dataView = this._dataView;
+ try {
+ const dataView = this._dataView;
- if (dataView.byteLength === DATA_VIEW_LENGTH) {
- const version = dataViewUint8(dataView, VERSION);
- const imuDateMS = dataViewBigUint(dataView, EPOCH_MS);
- const currentDateMS = Date.now();
- const validKeepalive = Math.abs(toSec(currentDateMS) - toSec(imuDateMS)) < 5;
- 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 enabled = dataViewUint8(dataView, ENABLED) !== 0 && version === DATA_LAYOUT_VERSION && validKeepalive;
+ if (dataView.byteLength === DATA_VIEW_LENGTH) {
+ const version = dataViewUint8(dataView, VERSION);
+ const imuDateMs = dataViewBigUint(dataView, EPOCH_MS);
+ const validKeepalive = isValidKeepAlive(toSec(imuDateMs));
+ 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 enabled = dataViewUint8(dataView, ENABLED) !== 0 && version === DATA_LAYOUT_VERSION && validKeepalive;
+ const displayRes = dataViewUint32Array(dataView, DISPLAY_RES);
+ const sbsEnabled = dataViewUint8(dataView, SBS_ENABLED) !== 0;
- if (enabled) {
- const displayRes = dataViewUintArray(dataView, DISPLAY_RES);
- const displayFov = dataViewFloat(dataView, DISPLAY_FOV);
- const lensDistanceRatio = dataViewFloat(dataView, LENS_DISTANCE_RATIO);
+ if (enabled) {
+ const displayFov = dataViewFloat(dataView, DISPLAY_FOV);
- // compute these values once, they only change when the XR device changes
- const displayAspectRatio = displayRes[0] / displayRes[1];
- const stageAspectRatio = this.target_monitor.width / this.target_monitor.height;
- const diagToVertRatio = Math.sqrt(Math.pow(stageAspectRatio, 2) + 1);
- const halfFovZRads = degreeToRadian(displayFov / diagToVertRatio) / 2;
- const halfFovYRads = halfFovZRads * stageAspectRatio;
- const screenDistance = 1.0 - lensDistanceRatio;
+ // TODO - drive these values from settings
+ const sbsContent = false;
+ const sbsModeStretched = true;
- // 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
- const trimWidthPercent = 3.0 / this.target_monitor.width;
- const trimHeightPercent = 3.0 / this.target_monitor.height;
-
- // all these values are transferred directly, unmodified from the driver
- transferUniformFloat(this, 'look_ahead_cfg', dataView, LOOK_AHEAD_CFG);
- transferUniformFloat(this, 'lens_distance_ratio', dataView, LENS_DISTANCE_RATIO);
+ // compute these values once, they only change when the XR device changes
+ const displayAspectRatio = displayRes[0] / displayRes[1];
+ const diagToVertRatio = Math.sqrt(Math.pow(displayAspectRatio, 2) + 1);
+ const halfFovZRads = degreeToRadian(displayFov / diagToVertRatio) / 2;
+ const halfFovYRads = halfFovZRads * displayAspectRatio;
+ const fovHalfWidths = [Math.tan(halfFovYRads), Math.tan(halfFovZRads)];
+ const fovWidths = [fovHalfWidths[0] * 2, fovHalfWidths[1] * 2];
+ let texcoordXLimits = [0.0, 1.0];
+ let texcoordXLimitsRight = [0.0, 1.0];
+ if (sbsEnabled) {
+ if (sbsContent) {
+ texcoordXLimits[1] = 0.5;
+ texcoordXLimitsRight[0] = 0.5;
+ if (!sbsModeStretched) {
+ texcoordXLimits[0] = 0.25;
+ texcoordXLimitsRight[1] = 0.75;
+ }
+ } else if (!sbsModeStretched) {
+ texcoordXLimits[0] = 0.25;
+ texcoordXLimits[1] = 0.75;
+ }
+ }
+ const lensDistanceRatio = dataViewFloat(dataView, LENS_DISTANCE_RATIO);
+ const lensFromCenter = lensDistanceRatio / 3.0;
+ const lensVector = [lensDistanceRatio, lensFromCenter, 0.0];
+ const lensVectorRight = [lensDistanceRatio, -lensFromCenter, 0.0];
- // computed values with no dataViewInfo, so we set these manually
- setSingleFloat(this, 'stage_aspect_ratio', stageAspectRatio);
- setSingleFloat(this, 'display_aspect_ratio', displayAspectRatio);
- setSingleFloat(this, 'trim_width_percent', trimWidthPercent);
- setSingleFloat(this, 'trim_height_percent', trimHeightPercent);
- setSingleFloat(this, 'half_fov_z_rads', halfFovZRads);
- setSingleFloat(this, 'half_fov_y_rads', halfFovYRads);
- setSingleFloat(this, 'screen_distance', screenDistance);
+ // our overlay doesn't quite cover the full screen texture, which allows us to see some of the real desktop
+ // underneath, so we trim three pixels around the entire edge of the texture
+ const trimWidthPercent = 3.0 / this.target_monitor.width;
+ const trimHeightPercent = 3.0 / this.target_monitor.height;
+
+ // all these values are transferred directly, unmodified from the driver
+ transferUniformFloat(this, 'look_ahead_cfg', dataView, LOOK_AHEAD_CFG);
+ transferUniformFloat(this, 'lens_distance_ratio', dataView, LENS_DISTANCE_RATIO);
- // TOOD - drive from settings
- setSingleFloat(this, 'display_zoom', 1.0);
+ // computed values with no dataViewInfo, so we set these manually
+ setSingleFloat(this, 'trim_width_percent', trimWidthPercent);
+ setSingleFloat(this, 'trim_height_percent', trimHeightPercent);
+ setSingleFloat(this, 'half_fov_z_rads', halfFovZRads);
+ setSingleFloat(this, 'half_fov_y_rads', halfFovYRads);
+ this.set_uniform_float(shaderUniformLocations['fov_half_widths'], 2, fovHalfWidths);
+ this.set_uniform_float(shaderUniformLocations['fov_widths'], 2, fovWidths);
+ setSingleFloat(this, 'curved_display', this.curved_display ? 1.0 : 0.0);
+ this.set_uniform_float(shaderUniformLocations['texcoord_x_limits'], 2, texcoordXLimits);
+ this.set_uniform_float(shaderUniformLocations['texcoord_x_limits_r'], 2, texcoordXLimitsRight);
+ this.set_uniform_float(shaderUniformLocations['lens_vector'], 3, lensVector);
+ this.set_uniform_float(shaderUniformLocations['lens_vector_r'], 3, lensVectorRight);
+ }
+
+ // update the supported device detected property if the state changes, trigger "notify::" events
+ 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
+ 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_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_to_display_ratio'], 2, [this.target_monitor.width/displayRes[0], this.target_monitor.height/displayRes[1]]);
+ } else if (dataView.byteLength !== 0) {
+ throw new Error(`Invalid dataView.byteLength: ${dataView.byteLength} !== ${DATA_VIEW_LENGTH}`);
}
-
- // 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); // 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}`)
+ } catch (e) {
+ Globals.logger.log(`ERROR: xrEffect.js setIntermittentUniformVariables ${e.message}\n${e.stack}`);
}
}
+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({
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',
'Target Monitor',
@@ -191,6 +250,15 @@ export const XREffect = GObject.registerClass({
2.5,
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',
'Display distance start',
@@ -208,14 +276,42 @@ export const XREffect = GObject.registerClass({
0.2,
2.5,
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
+ ),
+ 'disable-anti-aliasing': GObject.ParamSpec.boolean(
+ 'disable-anti-aliasing',
+ 'Disable anti-aliasing',
+ 'Disable anti-aliasing for the effect',
+ GObject.ParamFlags.READWRITE,
+ false
)
}
}, class XREffect extends Shell.GLSLEffect {
constructor(params = {}) {
super(params);
- this._frametime = Math.floor(1000 / this.target_framerate);
-
this._is_display_distance_at_end = false;
this._distance_ease_timeline = null;
@@ -255,14 +351,11 @@ export const XREffect = GObject.registerClass({
}
vfunc_paint_target(node, paintContext) {
- var now = Date.now();
- var lastPaint = this._last_paint || 0;
- var frametime = this._frametime;
var calibratingImage = this.calibratingImage;
var customBannerImage = this.customBannerImage;
- const data = Globals.ipc_file.load_contents(null);
+ let data = Globals.ipc_file.load_contents(null);
if (data[0]) {
- const buffer = new Uint8Array(data[1]).buffer;
+ let buffer = new Uint8Array(data[1]).buffer;
this._dataView = new DataView(buffer);
if (!this._initialized) {
this.set_uniform_float(this.get_uniform_location('uDesktopTexture'), 1, [0]);
@@ -278,10 +371,12 @@ export const XREffect = GObject.registerClass({
this.setIntermittentUniformVariables = setIntermittentUniformVariables.bind(this);
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._redraw_timeline = Clutter.Timeline.new_for_actor(this.get_actor(), 1000);
+ this._redraw_timeline.connect('new-frame', (() => {
+ this.queue_repaint();
+ }).bind(this));
+ this._redraw_timeline.set_repeat_count(-1);
+ this._redraw_timeline.start();
this._uniforms_timeout_id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, (() => {
this.setIntermittentUniformVariables();
@@ -291,30 +386,48 @@ export const XREffect = GObject.registerClass({
this._initialized = true;
}
- if (this._dataView.byteLength === DATA_VIEW_LENGTH) {
- setSingleFloat(this, 'display_north_offset', this.display_distance);
- setSingleFloat(this, 'look_ahead_ms', lookAheadMS(this._dataView));
- setUniformMatrix(this, 'imu_quat_data', 4, this._dataView, IMU_QUAT_DATA);
- } else if (this._dataView.byteLength !== 0) {
- Globals.logger.log(`ERROR: Invalid dataView.byteLength: ${this._dataView.byteLength} !== ${DATA_VIEW_LENGTH}`)
+ let success = false;
+ let attempts = 0;
+ while (!success && attempts < 2) {
+ if (this._dataView.byteLength === DATA_VIEW_LENGTH) {
+ if (checkParityByte(this._dataView)) {
+ 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
- this.get_pipeline().set_layer_filters (
- 0,
- Cogl.PipelineFilter.LINEAR_MIPMAP_LINEAR,
- Cogl.PipelineFilter.LINEAR
- );
-
- super.vfunc_paint_target(node, paintContext);
- } else {
- super.vfunc_paint_target(node, paintContext);
+ if (!this.disable_anti_aliasing) {
+ // improves sampling quality for smooth text and edges
+ this.get_pipeline().set_layer_filters(
+ 0,
+ Cogl.PipelineFilter.LINEAR_MIPMAP_LINEAR,
+ Cogl.PipelineFilter.LINEAR
+ );
+ }
}
- this._last_paint = now;
+ super.vfunc_paint_target(node, paintContext);
}
cleanup() {
- if (this._redraw_timeout_id) GLib.source_remove(this._redraw_timeout_id);
+ if (this._redraw_timeline) {
+ this._redraw_timeline.stop();
+ this._redraw_timeline = null;
+ }
if (this._uniforms_timeout_id) GLib.source_remove(this._uniforms_timeout_id);
}
});
\ No newline at end of file
diff --git a/modules/XRLinuxDriver b/modules/XRLinuxDriver
index f45b8fb..b97faa8 160000
--- a/modules/XRLinuxDriver
+++ b/modules/XRLinuxDriver
@@ -1 +1 @@
-Subproject commit f45b8fbaa7ef0b3f824a3452a91dc74bffb6ea38
+Subproject commit b97faa82a7767e4270e46886d68dd1c70b71abca
diff --git a/modules/sombrero b/modules/sombrero
index 17ebe10..ec3f82a 160000
--- a/modules/sombrero
+++ b/modules/sombrero
@@ -1 +1 @@
-Subproject commit 17ebe10ad9a006cd6a51d5705d6ceddca4464369
+Subproject commit ec3f82aa4834847adf1f0c640eba4b87b6f3ff65
diff --git a/ui/data/com.xronlinux.BreezyDesktop.gschema.xml b/ui/data/com.xronlinux.BreezyDesktop.gschema.xml
index a1babc3..7b33ad7 100644
--- a/ui/data/com.xronlinux.BreezyDesktop.gschema.xml
+++ b/ui/data/com.xronlinux.BreezyDesktop.gschema.xml
@@ -64,6 +64,69 @@
End distance when using the "toggle display distance" shortcut.
+
+
+ false
+
+ Widescreen mode
+
+ Enable widescreen/SBS mode
+
+
+
+
+ 1.0
+
+ Display size
+
+ The size of the display
+
+
+
+
+ false
+
+ Curved display
+
+ Enable curved display mode
+
+
+
+
+ -1
+
+ Look-ahead override
+
+ Manually override the look-ahead calculation
+
+
+
+
+ true
+
+ Use optimal monitor configuration
+
+ Automatically set the optimal monitor configuration upon connection
+
+
+
+
+ true
+
+ Headset as primary
+
+ Automatically set the headset as the primary display upon connection
+
+
+
+
+ false
+
+ Disable anti-aliasing
+
+ Disable anti-aliasing
+
+
false
diff --git a/ui/modules/PyXRLinuxDriverIPC b/ui/modules/PyXRLinuxDriverIPC
index 50ce02f..34a349d 160000
--- a/ui/modules/PyXRLinuxDriverIPC
+++ b/ui/modules/PyXRLinuxDriverIPC
@@ -1 +1 @@
-Subproject commit 50ce02fc9e341d417785bd26abeee9f7305bee6c
+Subproject commit 34a349d39efc02f4b65550debd193d29d95a84f9
diff --git a/ui/src/connecteddevice.py b/ui/src/connecteddevice.py
index 16f9ad4..241c4d5 100644
--- a/ui/src/connecteddevice.py
+++ b/ui/src/connecteddevice.py
@@ -10,13 +10,17 @@ from .xrdriveripc import XRDriverIPC
class ConnectedDevice(Gtk.Box):
__gtype_name__ = "ConnectedDevice"
+ device_label = Gtk.Template.Child()
effect_enable_switch = Gtk.Template.Child()
display_distance_scale = 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_adjustment = 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_end_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()
reassign_toggle_follow_shortcut_button = 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):
super(Gtk.Box, self).__init__()
self.init_template()
self.all_enabled_state_inputs = [
self.display_distance_scale,
+ self.display_size_scale,
self.follow_mode_switch,
self.follow_threshold_scale,
+ self.widescreen_mode_switch,
+ self.curved_display_switch,
self.set_toggle_display_distance_start_button,
self.set_toggle_display_distance_end_button,
self.reassign_recenter_display_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
@@ -45,7 +60,13 @@ class ConnectedDevice(Gtk.Box):
self.extensions_manager = ExtensionsManager.get_instance()
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('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(), [
[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.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._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.connect("destroy", self._on_widget_destroy)
@@ -98,7 +122,8 @@ class ConnectedDevice(Gtk.Box):
for widget in self.all_enabled_state_inputs:
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):
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()
})
+ 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):
self.device_label.set_markup(f"{name}")
@@ -120,7 +150,9 @@ class ConnectedDevice(Gtk.Box):
def _on_widget_destroy(self, widget):
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-size', self.display_size_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')
def reload_display_distance_toggle_button(widget):
@@ -131,4 +163,4 @@ def on_set_display_distance_toggle(widget):
settings = SettingsManager.get_instance().settings
distance = settings.get_double('display-distance')
settings.set_double(widget.get_name(), distance)
- reload_display_distance_toggle_button(widget)
\ No newline at end of file
+ reload_display_distance_toggle_button(widget)
diff --git a/ui/src/gtk/connected-device.ui b/ui/src/gtk/connected-device.ui
index 5617700..215ca1c 100644
--- a/ui/src/gtk/connected-device.ui
+++ b/ui/src/gtk/connected-device.ui
@@ -24,201 +24,364 @@
-
+
+
+ shortcuts
+ Keyboard Shortcuts
+ preferences-desktop-keyboard-shortcuts-symbolic
+
+
+
+
+ Keyboard Shortcuts
+
+
+ Re-center display shortcut
+ Pin the virtual display to the current position.
+ 2
+
+
+ 30
+ 30
+
+
+ 3
+
+
+
+
+
+
+ recenter-display-shortcut
+ 3
+ Change
+
+
+
+
+
+
+
+
+ Display distance shortcut
+ Quickly toggle between two predefined distances.
+ 2
+
+
+ 30
+ 30
+
+
+ 3
+
+
+
+
+
+
+ toggle-display-distance-shortcut
+ 3
+ Change
+
+
+
+
+
+
+
+
+ Toggle follow mode shortcut
+ Quickly toggle follow mode.
+ 2
+
+
+ 30
+ 30
+
+
+ 3
+
+
+
+
+
+
+ toggle-follow-shortcut
+ 3
+ Change
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ advanced
+ Advanced Settings
+ applications-system-symbolic
+
+
+
+
+ Advanced Settings
+
+
+ Find optimal display config
+ Automatically modify the glasses display configuration for maximum resolution and best scaling when plugged in.
+
+
+ 3
+
+
+
+
+
+
+ Always primary display
+ Automatically set the glasses as the primary display when plugged in.
+
+
+ 3
+
+
+
+
+
+
+ Movement look-ahead
+ 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.
+
+
+ 3
+ false
+ 0
+ 0
+ 350
+ false
+
+
+ -1
+ 40
+ 1
+ -1
+
+
+
+ Default
+ 10ms
+ 20ms
+ 30ms
+ 40ms
+
+
+
+
+
+
+
+
+
-
- Shortcuts
- Modify keyboard shortcuts and how they work
-
-
- Re-center display shortcut
- Pin the virtual display to the current position
- 2
-
-
- 30
- 30
-
-
- 3
-
-
-
-
-
-
- recenter-display-shortcut
- 3
- Change
-
-
-
-
-
-
-
-
- Display distance shortcut
- Quickly toggle between two predefined distances
- 2
-
-
- 30
- 30
-
-
- 3
-
-
-
-
-
-
- toggle-display-distance-shortcut
- 3
- Change
-
-
-
-
-
-
-
-
- Display distance start and end
- Use the buttons to capture the current display distance as start and end points.
- 2
-
-
- 30
- 150
- 30
-
-
- toggle-display-distance-start
- 3
-
-
-
-
- toggle-display-distance-end
- 3
-
-
-
-
-
-
-
-
- Toggle follow mode shortcut
- Quickly toggle follow mode
- 2
-
-
- 30
- 30
-
-
- 3
-
-
-
-
-
-
- toggle-follow-shortcut
- 3
- Change
-
-
-
-
-
-
+
+ stack
+ wide
diff --git a/ui/src/gtk/license-dialog.ui b/ui/src/gtk/license-dialog.ui
index 896b618..2954559 100644
--- a/ui/src/gtk/license-dialog.ui
+++ b/ui/src/gtk/license-dialog.ui
@@ -57,11 +57,11 @@
-
+
-
+
diff --git a/ui/src/gtk/window.ui b/ui/src/gtk/window.ui
index d06e8c7..40717e0 100644
--- a/ui/src/gtk/window.ui
+++ b/ui/src/gtk/window.ui
@@ -55,6 +55,10 @@
License Details
app.license
+ -
+ Force Reset
+ app.reset_driver
+
-
_About BreezyDesktop
app.about
diff --git a/ui/src/licensefeaturerow.py b/ui/src/licensefeaturerow.py
index 5f4904a..7d55ed8 100644
--- a/ui/src/licensefeaturerow.py
+++ b/ui/src/licensefeaturerow.py
@@ -3,10 +3,10 @@ from gi.repository import Adw
from .time import time_remaining_text
FEATURE_NAMES = {
- 'sbs': 'Side-by-side mode (for gaming)',
- 'smooth_follow': 'Smooth Follow',
- 'productivity_basic': 'Breezy Desktop',
- 'productivity_pro': 'Breezy Desktop w/ multiple monitors',
+ 'sbs': 'Side-by-side mode (gaming)',
+ 'smooth_follow': 'Smooth Follow (gaming)',
+ 'productivity_basic': 'Breezy Desktop (productivity)',
+ 'productivity_pro': 'Breezy Desktop Pro (productivity)',
}
class LicenseFeatureRow(Adw.ActionRow):
diff --git a/ui/src/licensetierrow.py b/ui/src/licensetierrow.py
index 9ee6cc7..9f5917c 100644
--- a/ui/src/licensetierrow.py
+++ b/ui/src/licensetierrow.py
@@ -53,7 +53,7 @@ class LicenseTierRow(Adw.ExpanderRow):
elif active_period is not None:
amount_text += " to upgrade"
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:
row_widget = Adw.ActionRow(title=period.capitalize())
diff --git a/ui/src/main.py b/ui/src/main.py
index f77a4dc..a2a2281 100644
--- a/ui/src/main.py
+++ b/ui/src/main.py
@@ -64,6 +64,7 @@ class BreezydesktopApplication(Adw.Application):
self.create_action('quit', self.on_quit_action, ['q'])
self.create_action('about', self.on_about_action)
self.create_action('license', self.on_license_action)
+ self.create_action('reset_driver', self.on_reset_driver_action)
def do_activate(self):
"""Called when the application is activated.
@@ -94,6 +95,11 @@ class BreezydesktopApplication(Adw.Application):
dialog.set_transient_for(self.props.active_window)
dialog.present()
+ def on_reset_driver_action(self, widget, _):
+ XRDriverIPC.get_instance().write_control_flags({
+ 'force_quit': True
+ })
+
def create_action(self, name, callback, shortcuts=None):
"""Add an application action.
diff --git a/ui/src/statemanager.py b/ui/src/statemanager.py
index a8aa977..8c2497f 100644
--- a/ui/src/statemanager.py
+++ b/ui/src/statemanager.py
@@ -22,6 +22,7 @@ class StateManager(GObject.GObject):
__gproperties__ = {
'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),
+ '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-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),
@@ -94,12 +95,15 @@ class StateManager(GObject.GObject):
self.set_property('license-present', False)
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()
def do_set_property(self, prop, value):
if prop.name == 'follow-mode':
self.follow_mode = value
+ if prop.name == 'widescreen-mode':
+ self.widescreen_mode = value
if prop.name == 'license-action-needed':
self.license_action_needed = value
if prop.name == 'license-present':
@@ -110,6 +114,8 @@ class StateManager(GObject.GObject):
def do_get_property(self, prop):
if prop.name == 'follow-mode':
return self.follow_mode
+ if prop.name == 'widescreen-mode':
+ return self.widescreen_mode
if prop.name == 'license-action-needed':
return self.license_action_needed
if prop.name == 'license-present':