Merge branch 'main' into main

This commit is contained in:
Wayne Heaney 2024-07-18 16:49:20 -07:00 committed by GitHub
commit b731abaf53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 1379 additions and 444 deletions

View File

@ -8,7 +8,7 @@
This repo contains a collection of tools to enable virtual desktop environments for gaming and productivity on Linux using [supported XR glasses](https://github.com/wheaney/XRLinuxDriver#supported-devices).
There are two installations at the moment:
There are two installations at the moment. **Note: Only install one of these at a time, as they invalidate each other's installations. This is only temporary.**
* [Breezy GNOME](#breezy-gnome) for desktop support, primarily in GNOME Linux desktop environments
* [Breezy Vulkan](#breezy-vulkan) primarily for gaming but would work with pretty much any application that uses Vulkan rendering.
@ -16,9 +16,11 @@ There are two installations at the moment:
Breezy GNOME is a virtual workspace solution for Linux desktops that use the GNOME desktop environment (requires GNOME 45+ on an x86_64 system); see [non-GNOME setup](#non-gnome-setup) if you want to try it without a GNOME desktop environment. It currently supports one virtual monitor and multiple physical monitors, but it will soon support multiple virtual monitors. See [upcoming features](#upcoming-features) for more improvements on the horizon.
### GNOME Setup
1. Download the Breezy GNOME [setup script](https://github.com/wheaney/breezy-desktop/releases/latest/download/breezy_gnome_setup) and set the execute flag (e.g. from the terminal: `chmod +x ~/Downloads/breezy_gnome_setup`)
2. Run the setup script (e.g. `~/Downloads/breezy_gnome_setup`)
3. You'll have an application called `Breezy Desktop` installed. Launch that and follow any instructions. You will need to log out and back in at least once to get the GNOME extension working.
1. Ensure you have the latest graphics drivers installed for your distro.
2. Download the Breezy GNOME [setup script](https://github.com/wheaney/breezy-desktop/releases/latest/download/breezy_gnome_setup) and set the execute flag (e.g. from the terminal: `chmod +x ~/Downloads/breezy_gnome_setup`)
3. Run the setup script (e.g. `~/Downloads/breezy_gnome_setup`)
4. You'll have an application called `Breezy Desktop` installed. Launch that and follow any instructions. You will need to log out and back in at least once to get the GNOME extension working.
5. For a double-wide screen, enable "widescreen mode" using the toggle in the Breezy Desktop application. **Note: this can be significantly more resource intensive than non-widescreen, you may notice performance dips on older hardware**
### Non-GNOME Setup
A workable solution (with some [QoL improvements needed](#upcoming-features)) is to use your preferred desktop environment with a GNOME window open in nested mode. To do this:
@ -32,11 +34,41 @@ All controls are provided through the Breezy Desktop application. You can also c
### Upcoming Features
1. Port to GNOME 43/44
2. ARM/AARCH64 build
3. Multiple virtual monitors + multiple physical monitors
4. Better nested support (clipboard sync, borderless window, snap window to correct display automatically)
5. SBS display depth
6. SBS support for 3D content
7. Port to other popular desktop environments: Cinnamon, KWin
3. Port to KWin Effect (KDE Plasma support)
4. Multiple virtual monitors + multiple physical monitors
5. Supported nested or Distrobox deployment
### Breezy GNOME Pricing (Productivity Tier)
Breezy GNOME comes with 2 free trial months. After that, it requires an active Productivity Tier license. Payments are currently only accepted via [Ko-fi](https://ko-fi.com/wheaney). Here's the pricing structure:
| Payment period | Price | Upgrade window \* |
| -------------- | ------------------ | ------------------------------------- |
| Monthly | $5 USD, recurring | Within 7 days to upgrade to yearly |
| Yearly | $50 USD, recurring | Within 90 days to upgrade to lifetime |
| Lifetime | $125 USD, one-time | — |
\* If you pay for a plan and decide to upgrade to a longer-term plan, you may pay the difference within this window.
If you have enough funds, your license will renew automatically within 7 days of expiration so you never experience an unexpected outage. Your device is never required to be online to continue using Productivity Tier features when enabled, but if your access expires while offline (even if you have enough funds), the features will be disabled until the next time your device goes online and the license can be refreshed. Be sure to check for expiration warnings prior to travel.
#### Free Productivity Tier
To make Breezy widely accessible, Productivity Tier is currently free of charge for qualified individuals using it for non-commercial purposes. Eligible groups include:
* Students
* Public school educators
* Active duty service members and veterans of the U.S. Armed Forces
* Individuals experiencing financial hardship or special circumstances that make electronic payments prohibitive
* Individuals affected by active war zones or humanitarian crises (e.g. Ukrainian citizens)
If you believe you qualify, please email wayne@xronlinux.com. You may be asked to provide documentation to verify your eligibility.
#### Unlocking Productivity Tier
After your first payment, you should immediately receive an email (to your Ko-fi email address) with a verification token. Once you receive that, enter it in the `License Details` view of the `Breezy Desktop` application, available from the menu in the top window bar.
If you don't receive a token, you can request one in the `License Details` view by entering your email address.
## Breezy Vulkan
@ -70,26 +102,18 @@ To see all the configuration options available to you, type `~/bin/xreal_driver_
#### Multi-tap to re-center or re-calibrate
I've implemented an experimental multi-tap detection feature for screen **re-centering (2 taps)** and **re-calibrating the device (3 taps)**. To perform a multi-tap, you'll want to give decent taps on the top of the glasses. I tend to do this on the corner, right on top of the hinge. It should be a firm, sharp tap, and wait just a split second to do the second tap, as it needs to detect a slight pause in between (but it also shouldn't take more than a half a second between taps so don't wait too long).
### Troubleshooting
#### Screen drag or flickering
Framerate is really important here, because individual frames are static, so moving your head quickly may produce a noticeable flicker as it moves the screen. Higher framerates will produce an overall better experience (less flicker and smoother follow), but lower framerates should still be totally usable.
#### Unexpected screen movement or drift
It's important that your glasses are either on your head or sitting on a flat surface when they're first plugged in and calibrated. If you notice that your screen is constantly drifting in one direction or continues to move for several seconds after a head movement, almost as if the screen has some momentum that takes time to slow down, then you'll want to re-calibrate them. To do this, do a triple-tap as described in the Multi-tap section above.
#### Display size
If the screen appears very small in your view, you may be playing at the Deck screen's native resolution, and not at the glasses' native
resolution. To fix this:
1. Go to the game details in Steam, hit the Settings/cog icon, and open `Properties`, then for `Game Resolution` choose `Native`.
2. After launching the game, if it's still small, go into the game options, and in the graphics or video settings, change the resolution (the glasses run at 1920x1080).
If you *WANT* to keep a low resolution, then you can just use the `Zoom` setting to make the screen appear larger. For now this is done through the config script: `~/bin/xreal_driver_config -z 1.0`. Larger numbers zoom in (e.g. `2.0` doubles the screen size) and smaller numbers zoom out (e.g. `0.5` is half the screen size).
### Supporter Tier
Supporter Tier features are enhancments to core functionality, offered as a way to reward those who have [supported the project](https://ko-fi.com/wheaney). Core features -- like Virtual Display mode, VR-Lite mouse/joystick modes, and Follow mode's display positioning/resizing settings -- will always remain available to everyone regardless of supporter status. Donating $10 gets you a year, and $25 gets you lifetime of Supporter Tier access. If you have enough funds, your access will renew automatically within 7 days of expiration so you never experience an unexpected outage. Your device is never required to be online to continue using Supporter Tier features when enabled, but if your access expires while offline (even if you have enough funds), the features will be disabled until the next time your device goes online and the license can be refreshed. Be sure to check for expiration warnings prior to travel.
Breezy Vulkan's Supporter Tier features are enhancments to core functionality, offered as a way to reward those who have [supported the project](https://ko-fi.com/wheaney). Core features -- like Virtual Display mode, VR-Lite mouse/joystick modes, and Follow mode's display positioning/resizing settings -- will always remain available to everyone regardless of supporter status. Here's the pricing structure:
| Payment period | Price | Upgrade window \* |
| -------------- | ------------------ | ------------------------------------- |
| Yearly | $10 USD, recurring | Within 90 days to upgrade to lifetime |
| Lifetime | $25 USD, one-time | — |
\* If you pay for a plan and decide to upgrade to a longer-term plan, you may pay the difference within this window.
If you have enough funds, your access will renew automatically within 7 days of expiration so you never experience an unexpected outage. Your device is never required to be online to continue using Supporter Tier features when enabled, but if your access expires while offline (even if you have enough funds), the features will be disabled until the next time your device goes online and the license can be refreshed. Be sure to check for expiration warnings prior to travel.
Features currently offered:
* Smooth Follow (in Follow mode)

View File

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

View File

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

@ -6,10 +6,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 +25,7 @@ export class CursorManager {
this._cursorWatch = null;
this._cursorChangedConnection = null;
this._cursorVisibilityChangedConnection = null;
this._cursorPositionInvalidatedConnection = null;
this._redraw_timeline = null;
}
enable() {
@ -71,11 +70,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 +87,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 +123,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 +172,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 +198,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;
}
}

View File

@ -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,23 @@ 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'),
use_highest_refresh_rate: this.settings.get_boolean('use-highest-refresh-rate'),
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 +93,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 +121,107 @@ 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(`Identified supported monitor: ${target_monitor.product} on ${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) return true;
const needs_sbs_mode_switch = this.settings.get_boolean('fast-sbs-mode-switching') &&
this._needs_widescreen_monitor_update();
return !needs_sbs_mode_switch && !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._request_sbs_mode_change(widescreen_setting_enabled);
return true;
}
return false;
}
_effect_enable() {
@ -153,23 +231,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 +253,35 @@ 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 gets triggered before _effect_enable if in fast-sbs-mode-switching mode
if (!this.settings.get_boolean('fast-sbs-mode-switching'))
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_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 +290,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 +321,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 +336,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 +369,65 @@ export default class BreezyDesktopExtension extends Extension {
if (value !== undefined) this._write_control('breezy_desktop_follow_threshold', value);
}
// requests sbs_mode change and monitors to ensure the state reflects the setting
_request_sbs_mode_change(value) {
this._write_control('sbs_mode', value ? 'enable' : 'disable');
if (!this._sbs_mode_update_timeout) {
var attempts = 0;
this._sbs_mode_update_timeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 10000, (() => {
if (attempts++ < 3) {
this._write_control('sbs_mode', value ? 'enable' : 'disable');
return GLib.SOURCE_CONTINUE;
}
// the state never updated to reflect our request, revert the setting
this.settings.set_boolean('widescreen-mode', !value);
this._sbs_mode_update_timeout = undefined;
return GLib.SOURCE_REMOVE;
}).bind(this));
}
}
_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._request_sbs_mode_change(value);
} else
Globals.logger.log_debug('effect.widescreen_mode_state already matched setting');
}
_update_widescreen_mode_from_state(effect, _pspec) {
// kill our state checker if it's running
if (this._sbs_mode_update_timeout) {
GLib.source_remove(this._sbs_mode_update_timeout);
this._sbs_mode_update_timeout = undefined;
}
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 +438,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 +455,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 +474,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 +486,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 +535,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}`);
}
}
}

View File

@ -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);
}

View File

@ -3,6 +3,7 @@
"name": "Breezy GNOME XR Desktop",
"description": "XR virtual desktop for GNOME.",
"settings-schema": "com.xronlinux.BreezyDesktop",
"session-modes": ["user", "unlock-dialog"],
"shell-version": [
"45", "46"
],

View File

@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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,179 @@ 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, useHighestRefreshRate, 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, availableModes, monProperties] = monitor;
const [connector, vendor, product, monitorSerial] = details;
const isOurMonitor = connector == connectorName;
let modes = availableModes;
if (isOurMonitor) {
ourMonitor = monitor;
if (!useHighestRefreshRate) {
const currentMode = modes.find((mode) => !!mode[6]['is-current']);
// filter modes to only include the current refresh rate
modes = availableModes.filter((mode) => mode[3] === currentMode[3]);
}
}
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
),
'use-highest-refresh-rate': GObject.ParamSpec.boolean(
'use-highest-refresh-rate',
'Use highest refresh rate',
'Set the highest refresh rate which choosing optimal configs',
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 +271,6 @@ export default class MonitorManager {
this._changeHookFn = fn;
}
setPostCallback(callback) {
this._postCallback = callback;
}
getMonitors() {
return Main.layoutManager.monitors;
}
@ -135,11 +279,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, this.use_highest_refresh_rate, ((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;
}
@ -147,7 +325,7 @@ export default class MonitorManager {
for (let i = 0; i < result.length; i++) {
const [monitorName, connectorName, vendor, product, serial, refreshRate] = result[i];
const monitorIndex = this._backendManager.get_monitor_for_connector(connectorName);
Globals.logger.log(`Found monitor ${monitorName}, vendor ${vendor}, product ${product}, serial ${serial}, connector ${connectorName}, index ${monitorIndex}`);
Globals.logger.log_debug(`Found monitor ${monitorName}, vendor ${vendor}, product ${product}, serial ${serial}, connector ${connectorName}, index ${monitorIndex}`);
if (monitorIndex >= 0) {
monitorProperties[monitorIndex] = {
index: monitorIndex,
@ -161,9 +339,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));
}
}
});

View File

@ -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);
}

View File

@ -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);
}
});

@ -1 +1 @@
Subproject commit f45b8fbaa7ef0b3f824a3452a91dc74bffb6ea38
Subproject commit b97faa82a7767e4270e46886d68dd1c70b71abca

@ -1 +1 @@
Subproject commit 26ece497d36bbe2a7445ea2f4e1cce31c35daa3a
Subproject commit ec3f82aa4834847adf1f0c640eba4b87b6f3ff65

View File

@ -16,12 +16,12 @@ check_command "flatpak-builder"
# https://stackoverflow.com/a/246128
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
TMP_DIR=$(mktemp -d -t breezy-ui-flatpak-XXXXXXXXXX)
TMP_DIR=$(mktemp -d -t --tmpdir=$SCRIPT_DIR/.. breezy-ui-flatpak-XXXXXXXXXX)
OUT_DIR=$SCRIPT_DIR/../out
rm -rf $OUT_DIR
mkdir -p $OUT_DIR
flatpak-builder --force-clean $TMP_DIR/build $SCRIPT_DIR/../com.xronlinux.BreezyDesktop.json
flatpak-builder --force-clean --delete-build-dirs $TMP_DIR/build $SCRIPT_DIR/../com.xronlinux.BreezyDesktop.json
flatpak build-export $TMP_DIR/export $TMP_DIR/build
flatpak build-bundle $TMP_DIR/export $OUT_DIR/com.xronlinux.BreezyDesktop.flatpak com.xronlinux.BreezyDesktop --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo

View File

@ -64,6 +64,87 @@
End distance when using the "toggle display distance" shortcut.
</description>
</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="use-highest-refresh-rate" type="b">
<default>
true
</default>
<summary>Use highest refresh rate</summary>
<description>
Automatically set the highest refresh rate upon connection
</description>
</key>
<key name="fast-sbs-mode-switching" type="b">
<default>
true
</default>
<summary>Fast SBS mode switching</summary>
<description>
Enable fast SBS mode switching
</description>
</key>
<key name="disable-anti-aliasing" type="b">
<default>
false
</default>
<summary>Disable anti-aliasing</summary>
<description>
Disable anti-aliasing
</description>
</key>
<key name="developer-mode" type="b">
<default>
false

View File

@ -3,7 +3,13 @@
<id>com.xronlinux.BreezyDesktop.desktop</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-or-later</project_license>
<name>Breezy Desktop</name>
<summary>XR Desktop Control Panel</summary>
<description>
<p>No description</p>
<p>XR Desktop Control Panel</p>
</description>
<categories>
<category>Office</category>
<category>Development</category>
</categories>
</component>

@ -1 +1 @@
Subproject commit 50ce02fc9e341d417785bd26abeee9f7305bee6c
Subproject commit 34a349d39efc02f4b65550debd193d29d95a84f9

View File

@ -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,34 @@ 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()
use_highest_refresh_rate_switch = Gtk.Template.Child()
fast_sbs_mode_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.use_highest_refresh_rate_switch,
self.fast_sbs_mode_switch,
self.movement_look_ahead_scale
]
self.settings = SettingsManager.get_instance().settings
@ -45,7 +64,15 @@ 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('use-highest-refresh-rate', self.use_highest_refresh_rate_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
self.settings.bind('fast-sbs-mode-switching', self.fast_sbs_mode_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 +95,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 +128,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 +140,13 @@ 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())
self.use_highest_refresh_rate_switch.set_sensitive(switch.get_active())
if not switch.get_active():
self.headset_as_primary_switch.set_active(False)
self.use_highest_refresh_rate_switch.set_active(False)
def set_device_name(self, name):
self.device_label.set_markup(f"<b>{name}</b>")
@ -120,7 +158,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 +171,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)
reload_display_distance_toggle_button(widget)

View File

@ -24,201 +24,386 @@
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="true">Settings</property>
<object class="AdwViewStack" id="stack">
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Effect enabled</property>
<property name="subtitle" translatable="true">Turn on or off the XR desktop effect</property>
<child>
<object class="GtkSwitch" id="effect_enable_switch">
<property name="valign">3</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Display distance</property>
<child>
<object class="GtkScale" id="display_distance_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_distance_adjustment">
<property name="lower">0.2</property>
<property name="upper">2.5</property>
<property name="step-increment">0.01</property>
<property name="value">1.05</property>
<object class="AdwViewStackPage">
<property name="name">general</property>
<property name="title">General Settings</property>
<property name="icon-name">applications-system-symbolic</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">1</property>
<property name="margin-start">20</property>
<property name="margin-end">20</property>
<property name="spacing">20</property>
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="true">Features</property>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">XR effect</property>
<property name="subtitle" translatable="true">Enables the Breezy Desktop XR effect.</property>
<child>
<object class="GtkSwitch" id="effect_enable_switch">
<property name="valign">3</property>
</object>
</child>
</object>
</child>
<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>
</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">Follow enabled</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">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>
</child>
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="true">Adjustments</property>
<child>
<object class="AdwActionRow" id="display_distance_row">
<property name="title" translatable="true">Display distance</property>
<property name="subtitle" translatable="true">Closer appears larger, further appears smaller. Controls depth when in widescreen mode.</property>
<child>
<object class="GtkScale" id="display_distance_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_distance_adjustment">
<property name="lower">0.2</property>
<property name="upper">2.5</property>
<property name="step-increment">0.01</property>
<property name="value">1.05</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 size</property>
<property name="subtitle" translatable="true">Combine with display distance to achieve a comfortable level of depth and size.</property>
<child>
<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>
</property>
</child>
</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">Use highest refresh rate</property>
<property name="subtitle" translatable="true">Refresh rate may affect performance, disable this to set it manually.</property>
<child>
<object class="GtkSwitch" id="use_highest_refresh_rate_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">Fast SBS mode switching</property>
<property name="subtitle" translatable="true">Switches glasses to SBS mode immediately when plugged in, if widescreen mode is on. May cause instability.</property>
<child>
<object class="GtkSwitch" id="fast_sbs_mode_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>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="true">Shortcuts</property>
<property name="description" translatable="true">Modify keyboard shortcuts and how they work</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 class="AdwViewSwitcher" id="switcher">
<property name="stack">stack</property>
<property name="policy">wide</property>
</object>
</child>
</template>

View File

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

View File

@ -55,6 +55,10 @@
<attribute name="label" translatable="yes">License Details</attribute>
<attribute name="action">app.license</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Force Reset</attribute>
<attribute name="action">app.reset_driver</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_About BreezyDesktop</attribute>
<attribute name="action">app.about</attribute>

View File

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

View File

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

View File

@ -65,6 +65,7 @@ class BreezydesktopApplication(Adw.Application):
self.create_action('quit', self.on_quit_action, ['<primary>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)
self._skip_verification = skip_verification
def do_activate(self):
@ -96,6 +97,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.

View File

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