diff --git a/.gitmodules b/.gitmodules
index e7a2906..32df75b 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -11,3 +11,4 @@
[submodule "ui/modules/PyXRLinuxDriverIPC"]
path = ui/modules/PyXRLinuxDriverIPC
url = https://github.com/wheaney/PyXRLinuxDriverIPC.git
+ branch = v1.1
diff --git a/VERSION b/VERSION
index a5ba932..359a5b9 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.1.7
\ No newline at end of file
+2.0.0
\ No newline at end of file
diff --git a/gnome/bin/dev/use_local_extension.sh b/gnome/bin/dev/use_local_extension.sh
index f959f13..12465b3 100755
--- a/gnome/bin/dev/use_local_extension.sh
+++ b/gnome/bin/dev/use_local_extension.sh
@@ -7,6 +7,7 @@ if [ -z "$XDG_DATA_HOME" ]; then
XDG_DATA_HOME="$USER_HOME/.local/share"
fi
DATA_DIR="$XDG_DATA_HOME/breezy_gnome"
+mkdir -p $DATA_DIR
# if $XDG_DATA_HOME/gnome-shell/extensions/breezydesktop@xronlinux.com exists
extension_path="$XDG_DATA_HOME/gnome-shell/extensions/breezydesktop@xronlinux.com"
diff --git a/gnome/src/cursormanager.js b/gnome/src/cursormanager.js
index 52d1341..b8470d1 100644
--- a/gnome/src/cursormanager.js
+++ b/gnome/src/cursormanager.js
@@ -4,11 +4,10 @@ import * as PointerWatcher from 'resource:///org/gnome/shell/ui/pointerWatcher.j
import { MouseSpriteContent } from './cursor.js';
import Globals from './globals.js';
-
// Taken from https://github.com/jkitching/soft-brightness-plus
export class CursorManager {
- constructor(overlay, refreshRate) {
- this._overlay = overlay;
+ constructor(targetMonitors, refreshRate) {
+ this._targetMonitors = targetMonitors;
this._refreshRate = refreshRate;
// Set/destroyed by _enableCloningMouse/_disableCloningMouse
@@ -53,7 +52,7 @@ export class CursorManager {
// and will trigger _startCloningMouse when the cursor should be shown
_enableCloningMouse() {
Globals.logger.log_debug('CursorManager _enableCloningMouse');
- this._cursorTracker = Meta.CursorTracker.get_for_display(global.display);
+ this._cursorTracker = global.backend.get_cursor_tracker?.() ?? Meta.CursorTracker.get_for_display(global.display);
this._mouseSprite = new Clutter.Actor({ request_mode: Clutter.RequestMode.CONTENT_SIZE });
this._mouseSprite.content = new MouseSpriteContent();
@@ -62,13 +61,17 @@ export class CursorManager {
this._cursorRoot.add_child(this._mouseSprite);
}
+ _backend() {
+ return global.stage.get_context?.().get_backend() ?? Clutter.get_default_backend();
+ }
+
_hideSystemCursor() {
this._systemCursorShown = false;
this._cursorRoot.show();
if (!this._cursorUnfocusInhibited) {
- Clutter.get_default_backend().get_default_seat().inhibit_unfocus();
+ this._backend().get_default_seat().inhibit_unfocus();
this._cursorUnfocusInhibited = true;
}
@@ -111,20 +114,16 @@ export class CursorManager {
// prereqs: setup in _enableCloningMouse
_startCloningMouse() {
Globals.logger.log_debug('CursorManager _startCloningMouse');
- this._overlay.mainActor().add_child(this._cursorRoot);
this._updateMouseSprite();
this._cursorTracker.connectObject('cursor-changed', this._updateMouseSprite.bind(this), this);
- Meta.disable_unredirect_for_display(global.display);
+ Meta.Compositor?.disable_unredirect?.() ?? Meta.disable_unredirect_for_display(global.display);
// cap the refresh rate for performance reasons
const interval = 1000.0 / Math.min(this._refreshRate, 60);
this._cursorWatch = PointerWatcher.getPointerWatcher().addWatch(interval, this._updateMousePosition.bind(this));
this._updateMousePosition();
-
- const [xMouse, yMouse] = global.get_pointer();
- if (this._overlay.isWithinBounds(xMouse, yMouse)) this._hideSystemCursor();
}
// After this:
@@ -142,9 +141,8 @@ export class CursorManager {
if (this._cursorTracker) this._cursorTracker.disconnectObject(this);
if (this._mouseSprite?.content?.texture) this._mouseSprite.content.texture = null;
- Meta.enable_unredirect_for_display(global.display);
+ Meta.Compositor?.enable_unredirect?.() ?? Meta.enable_unredirect_for_display(global.display);
- if (this._cursorRoot) this._overlay.mainActor().remove_child(this._cursorRoot);
if (!this._systemCursorShown) this._showSystemCursor();
}
@@ -154,7 +152,7 @@ export class CursorManager {
if (this._cursorRoot) this._cursorRoot.hide();
if (this._cursorUnfocusInhibited) {
- Clutter.get_default_backend().get_default_seat().uninhibit_unfocus();
+ this._backend().get_default_seat().uninhibit_unfocus();
this._cursorUnfocusInhibited = false;
}
@@ -168,23 +166,56 @@ export class CursorManager {
_updateMousePosition(...args) {
const [xMouse, yMouse] = args.length ? args : global.get_pointer();
- const inBounds = this._overlay.isWithinBounds(xMouse, yMouse);
- const [xRel, yRel] = this._overlay.getRelativePosition(xMouse, yMouse);
+ let onMonitorIndex;
+ let xRel;
+ let yRel;
- if (xRel === this.xMouse && yRel === this.yMouse)
- return;
+ const inBoundsCheck = (monitorObj, index) => {
+ const inBoundsCoordinates = this._getInBoundsCoordinates(xMouse, yMouse, monitorObj.monitor);
+ if (inBoundsCoordinates) {
+ onMonitorIndex = index;
+ xRel = inBoundsCoordinates.xRel;
+ yRel = inBoundsCoordinates.yRel;
+ return true;
+ }
+ return false;
+ }
- if (inBounds) {
+ // check the previously in-bounds monitor first to avoid iterating over the whole list in the likely case that the cursor
+ // is still on the same monitor
+ if (this.onMonitorIndex === undefined || !inBoundsCheck(this._targetMonitors[this.onMonitorIndex], this.onMonitorIndex)) {
+ for (let i = 0; i < this._targetMonitors.length; i++) {
+ if (this.onMonitorIndex === i) continue;
+ if (inBoundsCheck(this._targetMonitors[i], i)) break;
+ }
+ }
+
+ if (this.onMonitorIndex !== onMonitorIndex) {
+ try {
+ if (this.onMonitorIndex !== undefined) this._targetMonitors[this.onMonitorIndex].actor.remove_child(this._cursorRoot);
+
+ this.onMonitorIndex = onMonitorIndex;
+ if (this.onMonitorIndex !== undefined) {
+ const actor = this._targetMonitors[this.onMonitorIndex].actor;
+ actor.add_child(this._cursorRoot);
+ actor.set_child_above_sibling(this._cursorRoot, null);
+ }
+ } catch (e) {
+ Globals.logger.log_debug(e);
+ }
+ }
+
+ if (this.onMonitorIndex !== undefined) {
if (this._systemCursorShown) this._hideSystemCursor();
this._cursorRoot.set_position(xRel, yRel);
- } else if (!this._systemCursorShown && !inBounds) {
+ } else if (!this._systemCursorShown) {
this._showSystemCursor();
}
- this.xMouse = xRel;
- this.yMouse = yRel;
+ this.xRel = xRel;
+ this.xRel = xRel;
- const seat = Clutter.get_default_backend().get_default_seat();
+ const seat = this._backend().get_default_seat();
if (this._cursorUnfocusInhibited && !seat.is_unfocus_inhibited()) {
Globals.logger.log_debug('reinhibiting');
seat.inhibit_unfocus();
@@ -210,4 +241,17 @@ export class CursorManager {
this._mouseSprite.hide();
}
}
+
+ _getInBoundsCoordinates(x, y, monitor) {
+ const xRel = x - monitor.x;
+ const yRel = y - monitor.y;
+ if (xRel >= 0 && xRel < monitor.width && yRel >= 0 && yRel < monitor.height) {
+ return {
+ xRel,
+ yRel,
+ }
+ }
+
+ return null;
+ }
}
\ No newline at end of file
diff --git a/gnome/src/devicedatastream.js b/gnome/src/devicedatastream.js
new file mode 100644
index 0000000..aae3706
--- /dev/null
+++ b/gnome/src/devicedatastream.js
@@ -0,0 +1,316 @@
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+
+import Globals from './globals.js';
+import {
+ dataViewEnd,
+ dataViewUint8,
+ dataViewBigUint,
+ dataViewUint32Array,
+ dataViewUint8Array,
+ dataViewFloat,
+ dataViewFloatArray,
+ BOOL_SIZE,
+ FLOAT_SIZE,
+ UINT_SIZE,
+ UINT8_SIZE
+} from "./ipc.js";
+import { isValidKeepAlive, getEpochSec, toSec } from "./time.js";
+
+const IPC_FILE_PATH = "/dev/shm/breezy_desktop_imu";
+const KEEPALIVE_REFRESH_INTERVAL_SEC = 1;
+
+// the driver should be using the same data layout version
+const DATA_LAYOUT_VERSION = 4;
+
+// DataView info: [offset, size, count]
+const VERSION = [0, UINT8_SIZE, 1];
+const ENABLED = [dataViewEnd(VERSION), BOOL_SIZE, 1];
+const LOOK_AHEAD_CFG = [dataViewEnd(ENABLED), FLOAT_SIZE, 4];
+const DISPLAY_RES = [dataViewEnd(LOOK_AHEAD_CFG), UINT_SIZE, 2];
+const DISPLAY_FOV = [dataViewEnd(DISPLAY_RES), FLOAT_SIZE, 1];
+const LENS_DISTANCE_RATIO = [dataViewEnd(DISPLAY_FOV), FLOAT_SIZE, 1];
+const SBS_ENABLED = [dataViewEnd(LENS_DISTANCE_RATIO), BOOL_SIZE, 1];
+const CUSTOM_BANNER_ENABLED = [dataViewEnd(SBS_ENABLED), BOOL_SIZE, 1];
+const SMOOTH_FOLLOW_ENABLED = [dataViewEnd(CUSTOM_BANNER_ENABLED), BOOL_SIZE, 1];
+const SMOOTH_FOLLOW_ORIGIN_DATA = [dataViewEnd(SMOOTH_FOLLOW_ENABLED), FLOAT_SIZE, 16];
+const EPOCH_MS = [dataViewEnd(SMOOTH_FOLLOW_ORIGIN_DATA), UINT_SIZE, 2];
+const IMU_QUAT_DATA = [dataViewEnd(EPOCH_MS), FLOAT_SIZE, 16];
+const IMU_PARITY_BYTE = [dataViewEnd(IMU_QUAT_DATA), UINT8_SIZE, 1];
+const DATA_VIEW_LENGTH = dataViewEnd(IMU_PARITY_BYTE);
+
+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;
+}
+
+const COUNTER_MAX = 300;
+function nextDebugIMUQuaternion(counter) {
+ const angle = counter / COUNTER_MAX * 2 * Math.PI;
+ const yaw = 10 * Math.PI / 180 * Math.cos(angle);
+ const roll = 0;
+ const pitch = 10 * Math.PI / 180 * Math.sin(angle);
+
+ const cy = Math.cos(yaw * 0.5);
+ const sy = Math.sin(yaw * 0.5);
+ const cp = Math.cos(pitch * 0.5);
+ const sp = Math.sin(pitch * 0.5);
+ const cr = Math.cos(roll * 0.5);
+ const sr = Math.sin(roll * 0.5);
+
+ const w = cr * cp * cy + sr * sp * sy;
+ const x = sr * cp * cy - cr * sp * sy;
+ const y = cr * sp * cy + sr * cp * sy;
+ const z = cr * cp * sy - sr * sp * cy;
+
+ return [x, y, z, w];
+}
+
+export const DeviceDataStream = GObject.registerClass({
+ Properties: {
+ 'breezy-desktop-running': GObject.ParamSpec.boolean(
+ 'breezy-desktop-running',
+ 'Breezy Desktop running',
+ 'Whether Breezy Desktop mode is enabled in xr_driver and supported glasses are connected',
+ 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
+ ),
+ 'imu-snapshots': GObject.ParamSpec.jsobject(
+ 'imu-snapshots',
+ 'IMU Snapshots',
+ 'Latest IMU quaternion snapshots and epoch timestamp for when it was collected',
+ GObject.ParamFlags.READWRITE
+ ),
+ 'smooth-follow-enabled': GObject.ParamSpec.boolean(
+ 'smooth-follow-enabled',
+ 'Smooth follow enabled',
+ 'Whether smooth follow is enabled',
+ GObject.ParamFlags.READWRITE,
+ false
+ ),
+ 'show-banner': GObject.ParamSpec.boolean(
+ 'show-banner',
+ 'Show banner',
+ 'Whether the banner should be displayed',
+ GObject.ParamFlags.READWRITE,
+ false
+ ),
+ 'custom-banner-enabled': GObject.ParamSpec.boolean(
+ 'custom-banner-enabled',
+ 'Custom banner enabled',
+ 'Whether the custom banner should be displayed',
+ GObject.ParamFlags.READWRITE,
+ false
+ ),
+ 'debug-no-device': GObject.ParamSpec.boolean(
+ 'debug-no-device',
+ 'Debug without device',
+ 'Debug mode that allows for testing with moving IMU values without a device connected',
+ GObject.ParamFlags.READWRITE,
+ false
+ )
+ }
+}, class DeviceDataStream extends GObject.Object {
+ constructor(params = {}) {
+ super(params);
+
+ // this may be set true if debug_no_device is set, so it doesn't mean a device is actually connected
+ this.breezy_desktop_running = false;
+
+ // use this property to set whether breezy is being operated by a real device
+ this.breezy_desktop_actually_running = false;
+
+ this._ipc_file = Gio.file_new_for_path(IPC_FILE_PATH);
+ this._running = false;
+ this.device_data = null;
+ }
+
+ start() {
+ this._running = true;
+ this._poll();
+ }
+
+ stop() {
+ this._running = false;
+ this.device_data = null;
+ this.imu_snapshots = null;
+ }
+
+ // polling is just intended to keep breezy_desktop_running current, anything needing up-to-date imu data should
+ // trigger a refresh with the default flag
+ _poll() {
+ if (this._running) {
+ this.refresh_data(true);
+ setTimeout(this._poll.bind(this), 1000);
+ }
+ }
+
+ // Refresh the data from the IPC file. if keepalive_only is true, we'll only check and update breezy_desktop_running if it
+ // hasn't been checked within KEEPALIVE_REFRESH_INTERVAL_SEC.
+ refresh_data(keepalive_only = false) {
+ if (!this.debug_no_device && this.was_debug_no_device) {
+ this.was_debug_no_device = false;
+ if (!this.breezy_desktop_actually_running) {
+ this.device_data = null;
+ this.breezy_desktop_running = false;
+ this.breezy_desktop_actually_running = false;
+ this.imu_snapshots = null;
+ }
+ }
+
+ if (this._ipc_file.query_exists(null) && (
+ !this.device_data?.imuData ||
+ !keepalive_only ||
+ getEpochSec() - toSec(this.device_data?.imuDateMs ?? 0) > KEEPALIVE_REFRESH_INTERVAL_SEC
+ )) {
+ let data = this._ipc_file.load_contents(null);
+ if (data[0]) {
+ let buffer = new Uint8Array(data[1]).buffer;
+ let dataView = new DataView(buffer);
+ if (dataView.byteLength === DATA_VIEW_LENGTH) {
+ let imuDateMs = dataViewBigUint(dataView, EPOCH_MS);
+ const displayFov = dataViewFloat(dataView, DISPLAY_FOV);
+ const validKeepAlive = isValidKeepAlive(toSec(imuDateMs));
+ const validData = validKeepAlive && displayFov !== 0.0;
+ const version = dataViewUint8(dataView, VERSION);
+ const enabled = dataViewUint8(dataView, ENABLED) !== 0 && version === DATA_LAYOUT_VERSION && validData;
+ let imuData = dataViewFloatArray(dataView, IMU_QUAT_DATA);
+ let smoothFollowEnabled = dataViewUint8(dataView, SMOOTH_FOLLOW_ENABLED) !== 0;
+ let smoothFollowOrigin = dataViewFloatArray(dataView, SMOOTH_FOLLOW_ORIGIN_DATA);
+ const imuResetState = enabled && validData && imuData[0] === 0.0 && imuData[1] === 0.0 && imuData[2] === 0.0 && imuData[3] === 1.0;
+ const customBannerEnabled = dataViewUint8(dataView, CUSTOM_BANNER_ENABLED) !== 0;
+ const sbsEnabled = dataViewUint8(dataView, SBS_ENABLED) !== 0;
+
+ if (validKeepAlive && !validData) Globals.logger.log('[ERROR] Received invalid device data');
+
+ // trigger "notify::" events for properties we want to check on every cycle
+ if (enabled && this.widescreen_mode_state !== sbsEnabled) this.widescreen_mode_state = sbsEnabled;
+ if (this.show_banner !== imuResetState) this.show_banner = imuResetState;
+ if (this.custom_banner_enabled !== customBannerEnabled) this.custom_banner_enabled = customBannerEnabled;
+
+ let success = keepalive_only;
+ if (validData) {
+ if (!this.device_data) {
+ this.device_data = {
+ version,
+ enabled,
+ imuResetState,
+ displayRes: dataViewUint32Array(dataView, DISPLAY_RES),
+ sbsEnabled,
+ displayFov,
+ lookAheadCfg: dataViewFloatArray(dataView, LOOK_AHEAD_CFG),
+ lensDistanceRatio: dataViewFloat(dataView, LENS_DISTANCE_RATIO)
+ };
+ } else if (keepalive_only) {
+ this.device_data = {
+ ...this.device_data,
+ imuResetState,
+ enabled,
+ sbsEnabled
+ }
+ }
+
+ if (smoothFollowEnabled !== this.smooth_follow_enabled) {
+ Globals.logger.log_debug(`Smooth follow enabled: ${smoothFollowEnabled}`);
+ this.smooth_follow_enabled = smoothFollowEnabled;
+ }
+ this.imu_snapshots = {
+ ...(this.imu_snapshots ?? {}),
+ smooth_follow_origin: smoothFollowOrigin
+ }
+
+ let attempts = 0;
+ while (!success && attempts < 2) {
+ if (dataView.byteLength === DATA_VIEW_LENGTH) {
+ if (checkParityByte(dataView)) {
+
+ this.imu_snapshots = {
+ imu_data: imuData,
+ timestamp_ms: imuDateMs,
+ smooth_follow_origin: smoothFollowOrigin
+ };
+ success = true;
+ }
+ } else if (dataView.byteLength !== 0) {
+ Globals.logger.log(`[ERROR] Invalid dataView.byteLength: ${dataView.byteLength} !== ${DATA_VIEW_LENGTH}`)
+ }
+
+ if (!success && ++attempts < 2) {
+ data = this._ipc_file.load_contents(null);
+ if (data[0]) {
+ buffer = new Uint8Array(data[1]).buffer;
+ dataView = new DataView(buffer);
+ imuDateMs = dataViewBigUint(dataView, EPOCH_MS);
+ imuData = dataViewFloatArray(dataView, IMU_QUAT_DATA);
+ }
+ }
+ }
+ }
+
+ this.breezy_desktop_actually_running = success && enabled && validData;
+ } else {
+ this.breezy_desktop_actually_running = false;
+ }
+ } else {
+ this.breezy_desktop_actually_running = false;
+ }
+ }
+
+ if (this.debug_no_device && !this.breezy_desktop_actually_running) {
+ if (!this.device_data || !this.was_debug_no_device) {
+ this.device_data = {
+ version: 1.0,
+ enabled: true,
+ imuResetState: false,
+ displayRes: [1920.0, 1080.0],
+ sbsEnabled: false,
+ displayFov: 46.0,
+ lookAheadCfg: [0.0, 0.0, 0.0, 0.0],
+ lensDistanceRatio: 0.05
+ }
+ }
+ this.was_debug_no_device = true;
+
+ if (!keepalive_only) {
+ this._counter = ((this._counter ?? -1)+1)%COUNTER_MAX;
+
+ const imuDataFirst = nextDebugIMUQuaternion(this._counter);
+ const imuData = [
+ ...imuDataFirst,
+ ...imuDataFirst,
+ ...imuDataFirst,
+ 2.0, 1.0, 0.0, 0.0
+ ]
+ this.imu_snapshots = {
+ imu_data: imuData,
+ timestamp_ms: Date.now(),
+ smooth_follow_origin: [0.0, 0.0, 0.0, 1.0]
+ };
+ }
+ this.breezy_desktop_running = true;
+ } else if (this.breezy_desktop_running !== this.breezy_desktop_actually_running) {
+ // update the breezy_desktop_running property if the state changes to trigger "notify::" events
+ this.breezy_desktop_running = this.breezy_desktop_actually_running;
+ if (!this.breezy_desktop_running && keepalive_only) {
+ this.device_data = null;
+ this.imu_snapshots = null;
+ }
+ }
+ }
+});
\ No newline at end of file
diff --git a/gnome/src/extension.js b/gnome/src/extension.js
index 08c4c5d..ea30261 100644
--- a/gnome/src/extension.js
+++ b/gnome/src/extension.js
@@ -1,34 +1,19 @@
-import Clutter from 'gi://Clutter'
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
+import St from 'gi://St';
import { CursorManager } from './cursormanager.js';
+import { DeviceDataStream } from './devicedatastream.js';
import Globals from './globals.js';
import { Logger } from './logger.js';
-import { MonitorManager } from './monitormanager.js';
-import { Overlay } from './overlay.js';
-import { isValidKeepAlive } from './time.js';
-import { IPC_FILE_PATH, XREffect } from './xrEffect.js';
+import { MonitorManager, NESTED_MONITOR_PRODUCT, SUPPORTED_MONITOR_PRODUCTS, VIRTUAL_MONITOR_PRODUCT } from './monitormanager.js';
+import { VirtualDisplaysActor } from './virtualdisplaysactor.js';
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
-const NESTED_MONITOR_PRODUCT = 'MetaMonitor';
-const SUPPORTED_MONITOR_PRODUCTS = [
- 'VITURE',
- 'nreal air',
- 'Air',
- 'Air 2',
- 'Air 2 Pro',
- 'Air 2 Ultra',
- 'SmartGlasses', // TCL/RayNeo
- 'Rokid Max',
- 'Rokid Air',
- NESTED_MONITOR_PRODUCT
-];
-
const BIN_HOME = GLib.getenv('XDG_BIN_HOME') || GLib.build_filenamev([GLib.get_home_dir(), '.local', 'bin']);
const XDG_CLI_PATH = GLib.build_filenamev([BIN_HOME, 'xr_driver_cli']);
const ALT_CLI_PATH = '/usr/bin/xr_driver_cli';
@@ -42,26 +27,17 @@ export default class BreezyDesktopExtension extends Extension {
// Set/destroyed by enable/disable
this._cursor_manager = null;
this._monitor_manager = null;
- this._xr_effect = null;
- this._overlay = null;
+ this._virtual_displays_actor = null;
+ this._virtual_displays_overlay = null;
this._target_monitor = null;
this._is_effect_running = false;
- this._distance_binding = null;
+ this._effect_settings_bindings = [];
+ this._data_stream_bindings = [];
+ this._show_banner_connection = null;
this._distance_connection = null;
+ this._focused_monitor_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;
- this._actor_added_connection = null;
- this._actor_removed_connection = null;
+ this._breezy_desktop_running_connection = null;
if (!Globals.logger) {
Globals.logger = new Logger({
@@ -70,6 +46,12 @@ export default class BreezyDesktopExtension extends Extension {
});
Globals.logger.logVersion();
}
+
+ if (!Globals.data_stream) {
+ Globals.data_stream = new DeviceDataStream({
+ debug_no_device: this.settings.get_boolean('debug-no-device')
+ });
+ }
}
enable() {
@@ -77,21 +59,27 @@ export default class BreezyDesktopExtension extends Extension {
try {
Globals.extension_dir = this.path;
- this.settings.bind('debug', Globals.logger, 'debug', Gio.SettingsBindFlags.DEFAULT);
+
+ Globals.data_stream.start();
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'),
+ disable_physical_displays: this.settings.get_boolean('disable-physical-displays'),
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.settings.bind('debug', Globals.logger, 'debug', Gio.SettingsBindFlags.DEFAULT);
+ this.settings.bind('use-optimal-monitor-config',this._monitor_manager, 'use-optimal-monitor-config', Gio.SettingsBindFlags.DEFAULT);
+ this.settings.bind('headset-as-primary', this._monitor_manager, 'headset-as-primary', Gio.SettingsBindFlags.DEFAULT);
+ this.settings.bind('disable-physical-displays', this._monitor_manager, 'disable-physical-displays', Gio.SettingsBindFlags.DEFAULT);
+ this.settings.bind('debug-no-device', Globals.data_stream, 'debug-no-device', Gio.SettingsBindFlags.DEFAULT);
+
+ this._breezy_desktop_running_connection = Globals.data_stream.connect('notify::breezy-desktop-running',
+ this._handle_breezy_desktop_running_change.bind(this));
this._cli_file = Gio.file_new_for_path(XDG_CLI_PATH);
if (!this._cli_file.query_exists(null)) {
@@ -108,62 +96,52 @@ export default class BreezyDesktopExtension extends Extension {
}
}
- _poll_for_ready() {
- Globals.logger.log_debug('BreezyDesktopExtension _poll_for_ready');
- 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, (() => {
- try {
- if (is_effect_running) {
- this._running_poller_id = undefined;
- return GLib.SOURCE_REMOVE;
- }
-
- if (this._check_driver_running() && target_monitor) {
- // 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;
- }
- } catch (e) {
- Globals.logger.log(`[ERROR] BreezyDesktopExtension _poll_for_ready ${e.message}\n${e.stack}`);
- this._running_poller_id = undefined;
- return GLib.SOURCE_REMOVE;
+ _find_virtual_monitors() {
+ try {
+ Globals.logger.log_debug('BreezyDesktopExtension _find_virtual_monitors');
+ const virtual_monitors = this._monitor_manager.getMonitorPropertiesList()?.filter(
+ monitor => monitor && monitor.product === VIRTUAL_MONITOR_PRODUCT);
+ if (virtual_monitors.length > 0) {
+ Globals.logger.log(`Found ${virtual_monitors.length} virtual monitors`);
+ return virtual_monitors.map(monitor => {
+ return this._monitor_manager.getMonitors()[monitor.index];
+ });
}
- }).bind(this));
+
+ Globals.logger.log_debug('BreezyDesktopExtension _find_virtual_monitors - No virtual monitors found');
+ } catch (e) {
+ Globals.logger.log(`[ERROR] BreezyDesktopExtension _find_virtual_monitors ${e.message}\n${e.stack}`)
+ }
+
+ return [];
}
_find_supported_monitor() {
+ if (!this._monitor_manager.getMonitorPropertiesList()) return null;
+
try {
Globals.logger.log_debug('BreezyDesktopExtension _find_supported_monitor');
- const target_monitor = this._monitor_manager.getMonitorPropertiesList()?.find(
- monitor => SUPPORTED_MONITOR_PRODUCTS.includes(monitor.product) ||
- this.settings.get_string('custom-monitor-product') === monitor.product);
+ let target_monitor = this._monitor_manager.getMonitorPropertiesList()?.find(
+ monitor => monitor && (SUPPORTED_MONITOR_PRODUCTS.includes(monitor.product) ||
+ this.settings.get_string('custom-monitor-product') === monitor.product));
+ let is_dummy = target_monitor?.product === NESTED_MONITOR_PRODUCT;
+
+ if (target_monitor === undefined && this.settings.get_boolean('developer-mode')) {
+ Globals.logger.log_debug('BreezyDesktopExtension _find_supported_monitor - Using dummy monitor');
+ // find the first of the physical monitors
+ target_monitor = this._monitor_manager.getMonitorPropertiesList()?.find(
+ monitor => monitor && monitor.product !== VIRTUAL_MONITOR_PRODUCT);
+ is_dummy = true;
+ }
+
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],
connector: target_monitor.connector,
refreshRate: target_monitor.refreshRate,
- is_dummy: target_monitor.product === NESTED_MONITOR_PRODUCT
- };
- }
-
- 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
+ is_dummy: is_dummy,
+ is_virtual: target_monitor.product === VIRTUAL_MONITOR_PRODUCT
};
}
@@ -176,8 +154,7 @@ export default class BreezyDesktopExtension extends Extension {
}
// 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.
+ // A false result means we'll expect _handle_monitor_change to be triggered
_target_monitor_ready(target_monitor) {
if (target_monitor.is_dummy) return true;
@@ -194,49 +171,26 @@ export default class BreezyDesktopExtension extends Extension {
Globals.logger.log('Reset triggered, disabling XR effect');
this._effect_disable(!for_disable);
}
- 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;
-
- if (this._check_driver_running()) {
+ this._target_monitor = this._find_supported_monitor();
+ if (this._target_monitor) {
+ if (Globals.data_stream.breezy_desktop_running) {
// 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)) {
+ if (this._target_monitor_ready(this._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');
+ Globals.logger.log_debug('BreezyDesktopExtension _setup - breezy desktop enabled, but async monitor action needed');
}
} else {
- Globals.logger.log_debug('BreezyDesktopExtension _setup - driver not running, starting poller');
- this._poll_for_ready();
+ Globals.logger.log_debug('BreezyDesktopExtension _setup - Doing nothing, target monitor found, but device stream not being received');
}
} 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`);
- }
+ Globals.logger.log_debug(`BreezyDesktopExtension _setup - Doing nothing, no supported monitor found, breezy_desktop_running: ${Globals.data_stream.breezy_desktop_running}`);
}
}
- _check_driver_running() {
- try {
- if (!Globals.ipc_file) Globals.ipc_file = Gio.file_new_for_path(IPC_FILE_PATH);
- 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);
- return isValidKeepAlive(file_modified_time);
- }
- } catch (e) {
- 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();
@@ -253,79 +207,105 @@ export default class BreezyDesktopExtension extends Extension {
_effect_enable() {
Globals.logger.log_debug('BreezyDesktopExtension _effect_enable');
- this._running_poller_id = undefined;
if (!this._is_effect_running) {
this._is_effect_running = true;
try {
const targetMonitor = this._target_monitor.monitor;
+ const virtualMonitors = this._find_virtual_monitors();
const refreshRate = targetMonitor.refreshRate ?? 60;
- this._overlay = new Overlay(targetMonitor);
- this._cursor_manager = new CursorManager(this._overlay, refreshRate);
- this._cursor_manager.enable();
+ // use rgba(255, 4, 144, 1) for chroma key background
+ this._virtual_displays_overlay = new St.Bin({ style: 'background-color: rgba(0, 0, 0, 1);', clip_to_allocation: true });
+ this._virtual_displays_overlay.opacity = 255;
+ this._virtual_displays_overlay.set_position(targetMonitor.x, targetMonitor.y);
+ this._virtual_displays_overlay.set_size(targetMonitor.width, targetMonitor.height);
- // In GS 45, use of "actor" was renamed to "child".
- const clutterContainer = Clutter.Container !== undefined;
- this._actor_added_connection = global.stage.connect(
- clutterContainer ? 'actor-added' : 'child-added',
- this._handle_sibling_update.bind(this),
- );
- this._actor_removed_connection = global.stage.connect(
- clutterContainer ? 'actor-removed' : 'child-removed',
- this._handle_sibling_update.bind(this),
- );
-
- this._xr_effect = new XREffect({
+ Globals.data_stream.refresh_data();
+ this._virtual_displays_actor = new VirtualDisplaysActor({
+ width: targetMonitor.width,
+ height: targetMonitor.height,
target_monitor: targetMonitor,
- target_framerate: refreshRate,
+ virtual_monitors: virtualMonitors,
+ monitor_wrapping_scheme: this.settings.get_string('monitor-wrapping-scheme'),
+ monitor_spacing: this.settings.get_int('monitor-spacing'),
+ viewport_offset_x: this.settings.get_double('viewport-offset-x'),
+ viewport_offset_y: this.settings.get_double('viewport-offset-y'),
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')
+ framerate_cap: this.settings.get_double('framerate-cap'),
+ imu_snapshots: Globals.data_stream.imu_snapshots,
+ show_banner: Globals.data_stream.show_banner,
+ custom_banner_enabled: Globals.data_stream.custom_banner_enabled
});
+ this._virtual_displays_overlay.set_child(this._virtual_displays_actor);
+ this._virtual_displays_actor.renderMonitors();
+
+ Shell.util_set_hidden_from_pick(this._virtual_displays_overlay, true);
+ global.stage.add_child(this._virtual_displays_overlay);
+
+ const cursor_manager_monitor_objs = this._virtual_displays_actor.monitor_actors.map(monitor => {
+ return {
+ monitor: monitor.monitorDetails,
+ actor: monitor.containerActor
+ };
+ });
+
+ this._cursor_manager = new CursorManager(cursor_manager_monitor_objs, refreshRate);
+ this._cursor_manager.enable();
+
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._data_stream_bindings = [
+ 'show-banner',
+ 'custom-banner-enabled',
+ 'smooth-follow-enabled'
+ ].map(data_stream_key =>
+ Globals.data_stream.bind_property(data_stream_key, this._virtual_displays_actor, data_stream_key, Gio.SettingsBindFlags.DEFAULT)
+ );
- 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._show_banner_connection = Globals.data_stream.connect('notify::show-banner', this._handle_show_banner_update.bind(this));
+ this._was_show_banner = Globals.data_stream.show_banner;
+ if (!this._was_show_banner) this._recenter_display();
- this._overlay.mainActor().add_effect_with_name('xr-desktop', this._xr_effect);
- Meta.disable_unredirect_for_display(global.display);
+ this._effect_settings_bindings = [
+ 'monitor-wrapping-scheme',
+ 'viewport-offset-x',
+ 'viewport-offset-y',
+ 'monitor-spacing',
+ 'display-distance',
+ 'toggle-display-distance-start',
+ 'toggle-display-distance-end',
+ 'display-size',
+ 'framerate-cap',
+ 'look-ahead-override',
+ 'disable-anti-aliasing'
+ ]
+ this._effect_settings_bindings.forEach(settings_key =>
+ this.settings.bind(settings_key, this._virtual_displays_actor, settings_key, Gio.SettingsBindFlags.DEFAULT)
+ );
+
+ this._distance_connection = this.settings.connect('changed::display-distance', this._update_display_distance.bind(this));
+ this._focused_monitor_distance_connection =
+ this._virtual_displays_actor.connect('notify::focused-monitor-details', this._update_display_distance.bind(this));
+ this._follow_threshold_connection = this.settings.connect('changed::follow-threshold', this._update_follow_threshold.bind(this));
+
+ Meta.Compositor?.disable_unredirect?.() ?? Meta.disable_unredirect_for_display(global.display);
this._add_settings_keybinding('toggle-xr-effect-shortcut', this._toggle_xr_effect.bind(this));
this._add_settings_keybinding('recenter-display-shortcut', this._recenter_display.bind(this));
- this._add_settings_keybinding('toggle-display-distance-shortcut', this._xr_effect._change_distance.bind(this._xr_effect));
+ this._add_settings_keybinding('toggle-display-distance-shortcut', this._virtual_displays_actor._change_distance.bind(this._virtual_displays_actor));
this._add_settings_keybinding('toggle-follow-shortcut', this._toggle_follow_mode.bind(this));
} catch (e) {
+ Globals.logger.log(`[ERROR] BreezyDesktopExtension _effect_enable ${e.message}\n${e.stack}`);
Globals.logger.log(`[ERROR] BreezyDesktopExtension _effect_enable ${e.message}\n${e.stack}`);
this._effect_disable();
}
}
}
- _handle_sibling_update() {
- Globals.logger.log_debug('BreezyDesktopExtension _handle_sibling_update()');
- global.stage.set_child_above_sibling(this._overlay.mainActor(), null);
- }
-
_add_settings_keybinding(settings_key, bind_to_function) {
try {
Main.wm.addKeybinding(
@@ -361,10 +341,15 @@ export default class BreezyDesktopExtension extends Extension {
_write_control(key, value) {
try {
- const file = Gio.file_new_for_path('/dev/shm/xr_driver_control');
- const stream = file.replace(null, false, Gio.FileCreateFlags.NONE, null);
- stream.write(`${key}=${value}`, null);
- stream.close(null);
+ Globals.logger.log_debug(`BreezyDesktopExtension _write_control ${key} ${value}`);
+ let proc = Gio.Subprocess.new(
+ ['bash', '-c', `echo "${key}=${value}" > /dev/shm/xr_driver_control`],
+ Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
+ );
+
+ let [success, stdout, stderr] = proc.communicate_utf8(null, null);
+ if (!success || !!stderr)
+ throw new Error(`Failed to write control: ${stderr}`);
} catch (e) {
Globals.logger.log(`[ERROR] BreezyDesktopExtension _write_control ${e.message}\n${e.stack}`);
}
@@ -395,10 +380,19 @@ export default class BreezyDesktopExtension extends Extension {
return state;
}
- _update_display_distance(settings, event) {
- const value = settings.get_double('display-distance');
+ _update_display_distance(object, event) {
+ const value = this.settings.get_double('display-distance');
Globals.logger.log_debug(`BreezyDesktopExtension _update_display_distance ${value}`);
- if (value !== undefined) this._write_control('breezy_desktop_display_distance', value);
+ if (value !== undefined) {
+ let focusedMonitorSizeAdjustment = 1.0;
+ if (this._virtual_displays_actor?.focused_monitor_details && this._target_monitor) {
+ const fovMonitor = this._target_monitor.monitor;
+ const focusedMonitor = this._virtual_displays_actor.focused_monitor_details;
+ focusedMonitorSizeAdjustment =
+ Math.max(focusedMonitor.width / fovMonitor.width, focusedMonitor.height / fovMonitor.height);
+ }
+ this._write_control('breezy_desktop_display_distance', value / focusedMonitorSizeAdjustment);
+ }
}
_update_follow_threshold(settings, event) {
@@ -421,7 +415,7 @@ export default class BreezyDesktopExtension extends Extension {
this._sbs_mode_update_timeout = undefined;
if (this.settings.get_boolean('fast-sbs-mode-switching')) {
- // setup and polling were halted if this is enabled, so we have to re-trigger setup
+ // setup was halted if this is enabled, so we have to re-trigger it now
this._setup();
}
@@ -443,12 +437,12 @@ export default class BreezyDesktopExtension extends Extension {
}
_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');
+ // 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) {
@@ -472,17 +466,22 @@ export default class BreezyDesktopExtension extends Extension {
this._setup();
}
- _handle_supported_device_change(effect, _pspec) {
- const device_connected = effect.supported_device_detected;
- Globals.logger.log_debug(`BreezyDesktopExtension _handle_supported_device_change ${device_connected}`);
+ _handle_breezy_desktop_running_change(datastream, _pspec) {
+ Globals.logger.log_debug(`BreezyDesktopExtension _handle_breezy_desktop_running_change ${datastream.breezy_desktop_running}`);
- // this will disable the effect and begin polling for a ready state again
- if (!device_connected && this._is_effect_running) {
- Globals.logger.log('Supported device disconnected');
- this._setup(true);
+ if (datastream.breezy_desktop_running !== this._is_effect_running) {
+ if (!datastream.breezy_desktop_running) Globals.logger.log('Breezy desktop disabled');
+ this._setup(!datastream.breezy_desktop_running);
}
}
+ _handle_show_banner_update(datastream, _pspec) {
+ Globals.logger.log_debug(`BreezyDesktopExtension _handle_show_banner_update ${datastream.show_banner}`);
+ if (this._was_show_banner && !datastream.show_banner) this._recenter_display();
+
+ this._was_show_banner = datastream.show_banner;
+ }
+
_toggle_xr_effect() {
if (!this._cli_file) return;
@@ -521,6 +520,7 @@ export default class BreezyDesktopExtension extends Extension {
_toggle_follow_mode() {
Globals.logger.log_debug('BreezyDesktopExtension _toggle_follow_mode');
+ if (!!this._virtual_displays_actor) this._virtual_displays_actor.set_property('smooth-follow-toggle-epoch-ms', Date.now());
this._write_control('toggle_breezy_desktop_smooth_follow', 'true');
}
@@ -530,85 +530,51 @@ export default class BreezyDesktopExtension extends Extension {
Globals.logger.log_debug('BreezyDesktopExtension _effect_disable');
this._is_effect_running = false;
- if (this._running_poller_id) {
- const poller_id = this._running_poller_id;
- this._running_poller_id = undefined;
- GLib.source_remove(poller_id);
- }
+ if (Globals.data_stream.smooth_follow_enabled) this._toggle_follow_mode();
Main.wm.removeKeybinding('recenter-display-shortcut');
Main.wm.removeKeybinding('toggle-display-distance-shortcut');
Main.wm.removeKeybinding('toggle-follow-shortcut');
- Meta.enable_unredirect_for_display(global.display);
+ Meta.Compositor?.enable_unredirect?.() ?? Meta.enable_unredirect_for_display(global.display);
- if (this._actor_added_connection) {
- global.stage.disconnect(this._actor_added_connection);
- this._actor_added_connection = null;
+ for (let settings_key of this._effect_settings_bindings) {
+ Gio.Settings.unbind(this.settings, settings_key);
}
- if (this._actor_removed_connection) {
- global.stage.disconnect(this._actor_removed_connection);
- this._actor_removed_connection = null;
- }
- if (this._distance_binding) {
- this.settings.unbind(this._distance_binding);
- this._distance_binding = null;
+ this._effect_settings_bindings = [];
+ this._data_stream_bindings.forEach(binding => binding.unbind());
+ this._data_stream_bindings = [];
+
+ if (this._show_banner_connection) {
+ Globals.data_stream.disconnect(this._show_banner_connection);
+ this._show_banner_connection = null;
}
if (this._distance_connection) {
this.settings.disconnect(this._distance_connection);
this._distance_connection = null;
}
+ if (this._focused_monitor_distance_connection) {
+ this._virtual_displays_actor.disconnect(this._focused_monitor_distance_connection);
+ this._focused_monitor_distance_connection = null;
+ }
if (this._follow_threshold_connection) {
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;
- }
- if (this._end_binding) {
- 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) {
- 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._virtual_displays_overlay) {
+ if (this._virtual_displays_actor) {
+ this._virtual_displays_overlay.remove_child(this._virtual_displays_actor);
+ this._virtual_displays_actor.destroy();
+ this._virtual_displays_actor = null;
}
- if (this._supported_device_detected_connected) {
- this._xr_effect.disconnect(this._supported_device_detected_connected);
- this._supported_device_detected_connected = null;
- }
- this._xr_effect.cleanup();
- this._xr_effect = null;
+
+ global.stage.remove_child(this._virtual_displays_overlay);
+ this._virtual_displays_overlay.destroy();
+ this._virtual_displays_overlay = null;
}
if (this._cursor_manager) {
this._cursor_manager.disable();
this._cursor_manager = null;
}
- if (this._overlay) {
- this._overlay.mainActor().remove_effect_by_name('xr-desktop');
- this._overlay.destroy();
- }
// 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
@@ -624,18 +590,22 @@ export default class BreezyDesktopExtension extends Extension {
disable() {
try {
Globals.logger.log_debug('BreezyDesktopExtension disable');
- 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._effect_disable();
+ Globals.data_stream.stop();
+ this._target_monitor = null;
+
+ if (this._breezy_desktop_running_connection) {
+ Globals.data_stream.disconnect(this._breezy_desktop_running_connection);
+ this._breezy_desktop_running_connection = null;
+ }
+ Gio.Settings.unbind(this.settings, 'debug');
+ Gio.Settings.unbind(this.settings, 'use-optimal-monitor-config');
+ Gio.Settings.unbind(this.settings, 'headset-as-primary');
+ Gio.Settings.unbind(this.settings, 'disable-physical-displays');
+ Gio.Settings.unbind(this.settings, 'debug-no-device');
+
+ if (this._monitor_manager) {
this._monitor_manager.disable();
this._monitor_manager = null;
}
diff --git a/gnome/src/globals.js b/gnome/src/globals.js
index 124d2e1..f11451c 100644
--- a/gnome/src/globals.js
+++ b/gnome/src/globals.js
@@ -1,6 +1,7 @@
const Globals = {
logger: null,
ipc_file: null, // Gio.File instance, file exists if set
- extension_dir: null // string path
+ extension_dir: null, // string path
+ data_stream: null, // DeviceDataStream instance
}
export default Globals;
\ No newline at end of file
diff --git a/gnome/src/math.js b/gnome/src/math.js
index 497274e..f1c9983 100644
--- a/gnome/src/math.js
+++ b/gnome/src/math.js
@@ -1,3 +1,20 @@
export function degreeToRadian(degree) {
return degree * Math.PI / 180;
+}
+
+// FOV in radians is spherical, so doesn't follow Pythagoras' theorem
+export function diagonalToCrossFOVs(diagonalFOVRadians, aspectRatio) {
+ // first convert from a spherical FOV to a diagonal FOV on a flat plane at a generic distance of 1.0
+ const flatDiagonalFOV = 2 * Math.tan(diagonalFOVRadians / 2);
+
+ // then convert to flat plane horizontal and vertical FOVs
+ const flatVerticalFOV = flatDiagonalFOV / Math.sqrt(1 + aspectRatio * aspectRatio);
+ const flatHorizontalFOV = flatVerticalFOV * aspectRatio;
+
+ // then convert back to spherical FOV
+ return {
+ diagonal: diagonalFOVRadians,
+ horizontal: 2 * Math.atan(Math.tan(flatHorizontalFOV / 2)),
+ vertical: 2 * Math.atan(Math.tan(flatVerticalFOV / 2))
+ }
}
\ No newline at end of file
diff --git a/gnome/src/monitormanager.js b/gnome/src/monitormanager.js
index 6cf5532..4c57171 100644
--- a/gnome/src/monitormanager.js
+++ b/gnome/src/monitormanager.js
@@ -23,6 +23,21 @@ import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import Globals from './globals.js';
+export const NESTED_MONITOR_PRODUCT = 'MetaMonitor';
+export const VIRTUAL_MONITOR_PRODUCT = 'Virtual remote monitor';
+export const SUPPORTED_MONITOR_PRODUCTS = [
+ 'VITURE',
+ 'nreal air',
+ 'Air',
+ 'Air 2',
+ 'Air 2 Pro',
+ 'Air 2 Ultra',
+ 'SmartGlasses', // TCL/RayNeo
+ 'Rokid Max',
+ 'Rokid Air',
+ NESTED_MONITOR_PRODUCT
+];
+
let cachedDisplayConfigProxy = null;
function getDisplayConfigProxy(extPath) {
@@ -35,7 +50,7 @@ function getDisplayConfigProxy(extPath) {
xml = new TextDecoder().decode(bytes);
}
} catch (e) {
- Globals.logger.log('ERROR: failed to load DisplayConfig interface XML');
+ Globals.logger.log('[ERROR] failed to load DisplayConfig interface XML');
throw e;
}
cachedDisplayConfigProxy = Gio.DBusProxy.makeProxyWrapper(xml);
@@ -54,40 +69,35 @@ export function newDisplayConfig(extPath, callback) {
}
function getMonitorConfig(displayConfigProxy, callback) {
- displayConfigProxy.GetResourcesRemote((result, error) => {
+ displayConfigProxy.GetCurrentStateRemote((result, error) => {
if (error) {
- callback(null, `GetResourcesRemote failed: ${error}`);
+ callback(null, `GetCurrentState failed: ${error}`);
} else {
- const monitors = [];
- for (let i = 0; i < result[2].length; i++) {
- const output = result[2][i];
- if (output.length <= 7) {
- callback(null, 'Cannot get DisplayConfig: No properties on output #' + i);
- return;
- }
- const props = output[7];
- const displayName = props['display-name'].get_string()[0];
- const connectorName = output[4];
- if (!displayName || displayName == '') {
- const displayName = 'Monitor on output ' + connectorName;
- }
- const vendor = props['vendor'].get_string()[0];
- const product = props['product'].get_string()[0];
- const serial = props['serial'].get_string()[0];
+ Globals.logger.log_debug(`monitormanager.js getMonitorConfig GetCurrentState result: ${JSON.stringify(result)}`);
+
+ const allMonitors = [];
+ const [serial, monitors, logicalMonitors, properties] = result;
+ for (let monitor of monitors) {
+ const [details, modes, monProperties] = monitor;
+ const [connector, vendor, product, monitorSerial] = details;
+ const displayName = monProperties['display-name'].get_string()[0];
- // grab refresh rate from the modes array
- const refreshRate = result[3][i][4];
-
- monitors.push([displayName, connectorName, vendor, product, serial, refreshRate]);
+ for (let mode of modes) {
+ const [modeId, width, height, refreshRate, preferredScale, supportedScales, modeProperites] = mode;
+ const isCurrent = !!modeProperites['is-current'];
+ if (isCurrent) {
+ allMonitors.push([displayName, connector, vendor, product, serial, refreshRate]);
+ }
+ }
}
- callback(monitors, null);
+ callback(allMonitors, null);
}
});
}
// 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, allowConfigUpdateFn) {
+ disablePhysicalDisplays, callback, allowConfigUpdateFn) {
Globals.logger.log_debug(`monitormanager.js performOptimalModeCheck for ${connectorName}`);
displayConfigProxy.GetCurrentStateRemote((result, error) => {
@@ -162,9 +172,11 @@ function performOptimalModeCheck(displayConfigProxy, connectorName, headsetAsPri
logicalMonitors.sort((a, b) => a[0] - b[0]);
// map from original logical monitors schema to a(iiduba(ssa{sv})) for ApplyMonitorsConfig call
- const updatedLogicalMonitors = logicalMonitors.map((logicalMonitor) => {
+ const removeMonitorIndexes = [];
+ const updatedLogicalMonitors = logicalMonitors.map((logicalMonitor, index) => {
const [x, y, scale, transform, primary, monitors, logMonProperties] = logicalMonitor;
const hasOurMonitor = !!monitors.some((monitor) => monitor[0] === connectorName);
+ const hasVirtualMonitor = monitors.some((monitor) => monitor[2] === VIRTUAL_MONITOR_PRODUCT);
const newScale = (!skipScaleUpdate && hasOurMonitor) ? bestFitMode.bestScale : scale;
anyMonitorsChanged |= newScale !== scale;
@@ -172,6 +184,11 @@ function performOptimalModeCheck(displayConfigProxy, connectorName, headsetAsPri
// if headsetAsPrimary is true
anyMonitorsChanged |= headsetAsPrimary && ((hasOurMonitor && !primary) || (!hasOurMonitor && primary));
+ if (disablePhysicalDisplays && !hasVirtualMonitor && !hasOurMonitor) {
+ removeMonitorIndexes.push(index);
+ anyMonitorsChanged = true;
+ }
+
// we need to figure out if the deltaX applies to this logical monitor,
// i.e. if it is within the same row as our monitor and to the right of it
let thisDeltaX = deltaX;
@@ -218,6 +235,112 @@ function performOptimalModeCheck(displayConfigProxy, connectorName, headsetAsPri
// if our monitor is already properly configured, we can skip the ApplyMonitorsConfig call
if (anyMonitorsChanged) {
+ if (removeMonitorIndexes.length > 0) {
+ let removedPrimary = false;
+
+ // remove monitors that are not virtual or our monitor
+ removeMonitorIndexes.reverse().forEach((index) => {
+ const [x, y, scale, transform, primary, monitors, logMonProperties] = updatedLogicalMonitors[index];
+ if (primary) removedPrimary = true;
+ updatedLogicalMonitors.splice(index, 1);
+ });
+
+ // collect sizes based on modes of attached monitors
+ const logicalMonitorsWithSizes = updatedLogicalMonitors.map((logicalMonitor) => {
+ const [x, y, scale, transform, primary, monitors, logMonProperties] = logicalMonitor;
+ const {width, height} = monitors.reduce(({width, height}, monitor) => {
+ const monitorConnector = monitor[0];
+ const currentMode = monitorToCurrentModeMap[monitorConnector];
+ const currentWidth = currentMode[1];
+ const currentHeight = currentMode[2];
+ return {
+ width: Math.max(width, currentWidth),
+ height: Math.max(height, currentHeight)
+ };
+ }, {width: 0, height: 0});
+
+ return {
+ logicalMonitor,
+ width,
+ height,
+ xEnd: x + width,
+ yEnd: y + height
+ }
+ });
+ logicalMonitorsWithSizes.sort((a, b) => a.x === b.x ? a.y - b.y : a.x - b.x);
+
+ // this array will track monitors we've already corrected, but we'll toss it out since we're modifying the
+ // objects in the original array
+ const processedLogicalMonitors = [];
+
+ // make sure all monitors have a monitor adjacent
+ for (let i = 0; i < logicalMonitorsWithSizes.length; i++) {
+ const thisMonitor = logicalMonitorsWithSizes[i];
+ const [x, y, scale, transform, primary, monitors, logMonProperties] = thisMonitor.logicalMonitor;
+ const {xEnd, yEnd} = thisMonitor;
+
+ const hasOurMonitor = !!monitors.some((monitor) => monitor[0] === connectorName);
+ if (removedPrimary && hasOurMonitor) {
+ // if we removed the primary monitor, we need to set the glasses monitor as the new primary
+ thisMonitor.logicalMonitor[4] = true;
+ }
+
+ if (logicalMonitorsWithSizes.some((monitor, index) => {
+ if (index === i) return false;
+
+ const [monX, monY, monScale, monTransform, monPrimary, monMonitors, monLogMonProperties] = monitor.logicalMonitor;
+ const monXEnd = monitor.xEnd;
+ const monYEnd = monitor.yEnd;
+ const xOverlap = x < monXEnd && xEnd > monX;
+ const yOverlap = y < monYEnd && yEnd > monY;
+
+ // use top or left sides to determine if it's already adjacent
+ return (x === monXEnd && yOverlap) || (y === monYEnd && xOverlap);
+ })) {
+ // this monitor is already adjacent to another monitor, leave it as-is
+ processedLogicalMonitors.push(thisMonitor);
+ } else {
+ let newX = undefined;
+ let newY = undefined;
+
+ // move the monitor left until it runs into one
+ const procMonitorsByXEndDesc = [...processedLogicalMonitors].sort((a, b) => b.xEnd - a.xEnd);
+ for (let j = 0; j < procMonitorsByXEndDesc.length; j++) {
+ const procMonitor = procMonitorsByXEndDesc[j];
+ const [procX, procY, procScale, procTransform, procPrimary, procMonitors, procLogMonProperties] = procMonitor.logicalMonitor;
+ if (procMonitor.xEnd <= x && procY < yEnd && procMonitor.yEnd > y) {
+ newX = procMonitor.xEnd;
+ newY = y;
+ break;
+ }
+ }
+
+ if (newX === undefined) {
+ newX = 0;
+
+ // we didn't find an adjacent monitor to the left, now move it up until it runs into one
+ const procMonitorsByYEndDesc = [...processedLogicalMonitors].sort((a, b) => b.yEnd - a.yEnd);
+ for (let j = 0; j < procMonitorsByYEndDesc.length; j++) {
+ const procMonitor = procMonitorsByYEndDesc[j];
+ const [procX, procY, procScale, procTransform, procPrimary, procMonitors, procLogMonProperties] = procMonitor.logicalMonitor;
+ if (procMonitor.yEnd <= y && procX < thisMonitor.width && procMonitor.xEnd > 0) {
+ newY = procMonitor.yEnd;
+ break;
+ }
+ }
+
+ // if nothing found, put at origin
+ if (newY === undefined) newY = 0;
+ }
+ thisMonitor.logicalMonitor[0] = newX;
+ thisMonitor.logicalMonitor[1] = newY;
+ thisMonitor.xEnd = newX + thisMonitor.width;
+ thisMonitor.yEnd = newY + thisMonitor.height;
+ processedLogicalMonitors.push(thisMonitor);
+ }
+ }
+ }
+
Globals.logger.log_debug(`monitormanager.js performOptimalModeCheck updatedLogicalMonitors: ${JSON.stringify(updatedLogicalMonitors)}`);
displayConfigProxy.ApplyMonitorsConfigRemote(
serial,
@@ -264,6 +387,13 @@ export const MonitorManager = GObject.registerClass({
'Use headset as primary monitor',
'Automatically set the headset as the primary display upon connection',
GObject.ParamFlags.READWRITE,
+ false
+ ),
+ 'disable-physical-displays': GObject.ParamSpec.boolean(
+ 'disable-physical-displays',
+ 'Disable physical displays',
+ 'Disable physical displays when a virtual display is connected',
+ GObject.ParamFlags.READWRITE,
true
),
'extension-path': GObject.ParamSpec.string(
@@ -282,11 +412,12 @@ export const MonitorManager = GObject.registerClass({
this._displayConfigProxy = null;
this._monitorProperties = null;
this._changeHookFn = null;
- this._needsConfigCheck = this.use_optimal_monitor_config;
+ this._needsConfigCheck = this.use_optimal_monitor_config || this.headset_as_primary || this.disable_physical_displays;
// help prevent certain actions from taking place multiple times in the event of rapid monitor updates
this._asyncRequestsInFlight = 0;
this._configCheckRequestsCount = 0;
+ this._enabled = false;
}
enable() {
@@ -300,12 +431,17 @@ export const MonitorManager = GObject.registerClass({
}).bind(this));
this._monitorsChangedConnection = Main.layoutManager.connect('monitors-changed', this._on_monitors_change.bind(this));
+ this._disable_physical_displays_connection = this.connect('notify::disable-physical-displays', this._on_disable_physical_displays_change.bind(this));
+ this._enabled = true;
}
disable() {
Globals.logger.log_debug('MonitorManager disable');
+ this.disconnect(this._disable_physical_displays_connection);
Main.layoutManager.disconnect(this._monitorsChangedConnection);
+ this._enabled = false;
+ this._disable_physical_displays_connection = null;
this._monitorsChangedConnection = null;
this._displayConfigProxy = null;
this._monitorProperties = null;
@@ -343,7 +479,7 @@ export const MonitorManager = GObject.registerClass({
return this._asyncRequestsInFlight === 1 && this._configCheckRequestsCount === configCheckCountSnapshot;
}).bind(this);
- performOptimalModeCheck(this._displayConfigProxy, monitorConnector, this.headset_as_primary, this.use_highest_refresh_rate, ((configChanged, error) => {
+ performOptimalModeCheck(this._displayConfigProxy, monitorConnector, this.headset_as_primary, this.use_highest_refresh_rate, this.disable_physical_displays, ((configChanged, error) => {
if (--this._asyncRequestsInFlight > 0) {
Globals.logger.log_debug(`MonitorManager needsOptimalModeCheck: ${this._asyncRequestsInFlight} async requests still pending, skipping change hook`);
return;
@@ -354,7 +490,7 @@ export const MonitorManager = GObject.registerClass({
this._needsConfigCheck = false;
if (error) {
- Globals.logger.log(`Failed to switch to optimal mode for monitor ${monitorConnector}: ${error}`);
+ Globals.logger.log(`[ERROR] Failed to switch to optimal mode for monitor ${monitorConnector}: ${error}`);
// tell the extension to proceed, this should result in another config check
this._changeHookFn();
@@ -380,11 +516,13 @@ export const MonitorManager = GObject.registerClass({
}
_on_monitors_change() {
+ if (!this._enabled) return;
+
Globals.logger.log_debug('MonitorManager _on_monitors_change');
if (this._displayConfigProxy == null) {
return;
}
- if (this.use_optimal_monitor_config) {
+ if (this.use_optimal_monitor_config || this.headset_as_primary || this.disable_physical_displays) {
this._needsConfigCheck = true;
this._configCheckRequestsCount++;
}
@@ -392,7 +530,7 @@ export const MonitorManager = GObject.registerClass({
getMonitorConfig(this._displayConfigProxy, ((result, error) => {
this._asyncRequestsInFlight--;
if (error) {
- Globals.logger.log(error);
+ Globals.logger.log(`[ERROR] Failed _on_monitors_change getMonitorConfig: ${error}`);
return;
}
@@ -425,4 +563,13 @@ export const MonitorManager = GObject.registerClass({
}
}).bind(this));
}
+
+ _on_disable_physical_displays_change() {
+ if (this._enabled && this.disable_physical_displays && !!this._changeHookFn) {
+ Globals.logger.log_debug('MonitorManager _on_disable_physical_displays_change triggering change hook');
+ this._needsConfigCheck = true;
+ this._configCheckRequestsCount++;
+ this._changeHookFn();
+ }
+ }
});
\ No newline at end of file
diff --git a/gnome/src/overlay.js b/gnome/src/overlay.js
deleted file mode 100644
index c42af7c..0000000
--- a/gnome/src/overlay.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import Clutter from 'gi://Clutter'
-import Shell from 'gi://Shell';
-import St from 'gi://St';
-
-import { SystemBackground } from './systembackground.js';
-import * as Main from 'resource:///org/gnome/shell/ui/main.js';
-
-export class Overlay {
- constructor(targetMonitor) {
- this._overlayContent = new Clutter.Actor({clip_to_allocation: true});
- this._overlay = new St.Bin({
- child: this._overlayContent
- });
- this._overlay.set_position(targetMonitor.x, targetMonitor.y);
- this._overlay.set_size(targetMonitor.width, targetMonitor.height);
-
- global.stage.add_child(this._overlay);
- Shell.util_set_hidden_from_pick(this._overlay, true);
-
- this._background = new SystemBackground();
- this._overlayContent.add_child(this._background);
-
- this._uiClone = new Clutter.Clone({ source: Main.layoutManager.uiGroup, clip_to_allocation: true });
- this._uiClone.x = -targetMonitor.x;
- this._uiClone.y = -targetMonitor.y;
- this._overlayContent.add_child(this._uiClone);
-
- this._targetMonitor = targetMonitor;
- }
-
- isWithinBounds(x, y) {
- return x >= this._targetMonitor.x && x < this._targetMonitor.x + this._targetMonitor.width &&
- y >= this._targetMonitor.y && y < this._targetMonitor.y + this._targetMonitor.height;
- }
-
- getRelativePosition(x, y) {
- return [x - this._targetMonitor.x, y - this._targetMonitor.y];
- }
-
- mainActor() {
- return this._overlayContent;
- }
-
- destroy() {
- global.stage.remove_child(this._overlay);
- this._overlay.destroy();
- this._overlay = null;
- }
-}
\ No newline at end of file
diff --git a/gnome/src/systembackground.js b/gnome/src/systembackground.js
deleted file mode 100644
index cf7a3bb..0000000
--- a/gnome/src/systembackground.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import Clutter from 'gi://Clutter';
-import Cogl from 'gi://Cogl';
-import GLib from 'gi://GLib';
-import GObject from 'gi://GObject';
-import Meta from 'gi://Meta';
-
-const DEFAULT_BACKGROUND_COLOR = Clutter.Color?.from_pixel(0x2e3436ff) || new Cogl.Color({red: 40, green: 40, blue: 40, alpha: 255});
-
-let _systemBackground;
-
-export const SystemBackground = GObject.registerClass({
- Signals: {'loaded': {}},
-}, class SystemBackground extends Meta.BackgroundActor {
- _init() {
- if (_systemBackground == null) {
- _systemBackground = new Meta.Background({meta_display: global.display});
- _systemBackground.set_color(DEFAULT_BACKGROUND_COLOR);
- }
-
- super._init({
- meta_display: global.display,
- monitor: 0,
- });
- this.content.background = _systemBackground;
-
- let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
- this.emit('loaded');
- return GLib.SOURCE_REMOVE;
- });
- GLib.Source.set_name_by_id(id, '[gnome-shell] SystemBackground.loaded');
- }
-});
diff --git a/gnome/src/virtualdisplayeffect.js b/gnome/src/virtualdisplayeffect.js
new file mode 100644
index 0000000..7fb013b
--- /dev/null
+++ b/gnome/src/virtualdisplayeffect.js
@@ -0,0 +1,523 @@
+import Clutter from 'gi://Clutter'
+import Cogl from 'gi://Cogl';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Shell from 'gi://Shell';
+
+import Globals from './globals.js';
+import { degreeToRadian, diagonalToCrossFOVs } from './math.js';
+
+
+// these need to mirror the values in XRLinuxDriver
+// https://github.com/wheaney/XRLinuxDriver/blob/main/src/plugins/smooth_follow.c#L31
+export const SMOOTH_FOLLOW_SLERP_TIMELINE_MS = 1000;
+const SMOOTH_FOLLOW_SLERP_FACTOR = Math.pow(1-0.999, 1/SMOOTH_FOLLOW_SLERP_TIMELINE_MS);
+
+// this mirror's how the driver's slerp function progresses so our effect will match it
+function smoothFollowSlerpProgress(elapsedMs) {
+ return 1 - Math.pow(SMOOTH_FOLLOW_SLERP_FACTOR, elapsedMs);
+}
+
+// how far to look ahead is how old the IMU data is plus a constant that is either the default for this device or an override
+function lookAheadMS(imuDateMs, lookAheadCfg, override) {
+ // how stale the imu data is
+ const dataAge = Date.now() - imuDateMs;
+
+ return (override === -1 ? lookAheadCfg[0] : override) + dataAge;
+}
+
+export const VirtualDisplayEffect = GObject.registerClass({
+ Properties: {
+ 'monitor-index': GObject.ParamSpec.int(
+ 'monitor-index',
+ 'Monitor Index',
+ 'Index of the monitor that this effect is applied to',
+ GObject.ParamFlags.READWRITE,
+ 0, 100, 0
+ ),
+ 'monitor-placements': GObject.ParamSpec.jsobject(
+ 'monitor-placements',
+ 'Monitor Placements',
+ 'Target and virtual monitor placement details, as relevant to rendering',
+ GObject.ParamFlags.READWRITE
+ ),
+ 'target-monitor': GObject.ParamSpec.jsobject(
+ 'target-monitor',
+ 'Target Monitor',
+ 'Details about the monitor being used as a viewport',
+ GObject.ParamFlags.READWRITE
+ ),
+ 'imu-snapshots': GObject.ParamSpec.jsobject(
+ 'imu-snapshots',
+ 'IMU Snapshots',
+ 'Latest IMU quaternion snapshots and epoch timestamp for when it was collected',
+ GObject.ParamFlags.READWRITE
+ ),
+ 'smooth-follow-enabled': GObject.ParamSpec.boolean(
+ 'smooth-follow-enabled',
+ 'Smooth follow enabled',
+ 'Whether smooth follow is enabled',
+ GObject.ParamFlags.READWRITE,
+ false
+ ),
+ 'smooth-follow-toggle-epoch-ms': GObject.ParamSpec.uint64(
+ 'smooth-follow-toggle-epoch-ms',
+ 'Smooth follow toggle epoch time',
+ 'ms since epoch when smooth follow was toggled',
+ GObject.ParamFlags.READWRITE,
+ 0, Number.MAX_SAFE_INTEGER, 0
+ ),
+ 'width': GObject.ParamSpec.int(
+ 'width',
+ 'Width',
+ 'Width of the viewport',
+ GObject.ParamFlags.READWRITE,
+ 1, 10000, 1920
+ ),
+ 'height': GObject.ParamSpec.int(
+ 'height',
+ 'Height',
+ 'Height of the viewport',
+ GObject.ParamFlags.READWRITE,
+ 1, 10000, 1080
+ ),
+ 'focused-monitor-index': GObject.ParamSpec.int(
+ 'focused-monitor-index',
+ 'Focused Monitor Index',
+ 'Index of the monitor that is currently focused',
+ GObject.ParamFlags.READWRITE,
+ -1, 100, -1
+ ),
+ 'display-zoom-on-focus': GObject.ParamSpec.boolean(
+ 'display-zoom-on-focus',
+ 'Display zoom on focus',
+ 'Automatically move a display closer when it becomes focused.',
+ GObject.ParamFlags.READWRITE,
+ true
+ ),
+ 'display-distance': GObject.ParamSpec.double(
+ 'display-distance',
+ 'Display Distance',
+ 'Distance of the display from the camera',
+ GObject.ParamFlags.READWRITE,
+ 0.0,
+ 2.5,
+ 1.0
+ ),
+ 'display-distance-default': GObject.ParamSpec.double(
+ 'display-distance-default',
+ 'Display distance default',
+ 'Distance to use when not explicitly set, or when reset',
+ GObject.ParamFlags.READWRITE,
+ 0.2,
+ 2.5,
+ 1.0
+ ),
+ 'show-banner': GObject.ParamSpec.boolean(
+ 'show-banner',
+ 'Show banner',
+ 'Whether the banner should be displayed',
+ GObject.ParamFlags.READWRITE,
+ false
+ ),
+ 'lens-vector': GObject.ParamSpec.jsobject(
+ 'lens-vector',
+ 'Lens Vector',
+ 'Vector representing the offset of the lens from the pivot point',
+ GObject.ParamFlags.READWRITE
+ ),
+ 'actor-to-display-ratios': GObject.ParamSpec.jsobject(
+ 'actor-to-display-ratios',
+ 'Actor to Display Ratios',
+ 'Ratios to convert actor coordinates to display coordinates',
+ GObject.ParamFlags.READWRITE
+ ),
+ 'actor-to-display-offsets': GObject.ParamSpec.jsobject(
+ 'actor-to-display-offsets',
+ 'Actor to Display Offsets',
+ 'Offsets to convert actor coordinates to display coordinates',
+ GObject.ParamFlags.READWRITE
+ ),
+ 'is-closest': GObject.ParamSpec.boolean(
+ 'is-closest',
+ 'Is Closest',
+ 'Whether this monitor is the closest to the camera',
+ GObject.ParamFlags.READWRITE,
+ false
+ ),
+ 'disable-anti-aliasing': GObject.ParamSpec.boolean(
+ 'disable-anti-aliasing',
+ 'Disable anti-aliasing',
+ 'Disable anti-aliasing for the effect',
+ GObject.ParamFlags.READWRITE,
+ false
+ ),
+ 'look-ahead-override': GObject.ParamSpec.int(
+ 'look-ahead-override',
+ 'Look ahead override',
+ 'Override the look ahead value',
+ GObject.ParamFlags.READWRITE,
+ -1,
+ 45,
+ -1
+ ),
+ }
+}, class VirtualDisplayEffect extends Shell.GLSLEffect {
+ constructor(params = {}) {
+ super(params);
+
+ this._current_display_distance = this._is_focused() ? this.display_distance : this.display_distance_default;
+ this.no_distance_ease = false;
+ this._current_follow_ease_progress = 0.0;
+ this._use_smooth_follow_origin = false;
+
+ this.connect('notify::display-distance', this._update_display_distance.bind(this));
+ this.connect('notify::focused-monitor-index', this._update_display_distance.bind(this));
+ this.connect('notify::monitor-placements', this._update_display_position_uniforms.bind(this));
+ this.connect('notify::monitor-wrapping-scheme', this._update_display_position_uniforms.bind(this));
+ this.connect('notify::show-banner', this._handle_banner_update.bind(this));
+ this.connect('notify::smooth-follow-enabled', this._handle_smooth_follow_enabled_update.bind(this));
+ }
+
+ _is_focused() {
+ return this.focused_monitor_index === this.monitor_index;
+ }
+
+ _update_display_distance() {
+ const desired_distance = this._is_focused() ? this.display_distance : this.display_distance_default;
+ if (this._distance_ease_timeline?.is_playing()) {
+ // we're already easing towards the desired distance, do nothing
+ if (this._distance_ease_target === desired_distance) return;
+
+ this._distance_ease_timeline.stop();
+ }
+
+ if (this.no_distance_ease) {
+ this._current_display_distance = desired_distance;
+ this._update_display_position_uniforms();
+ this.no_distance_ease = false;
+ return;
+ }
+
+ // if we're the focused display, we'll double the timeline and wait for the first half to let other
+ // displays ease out first
+ this._distance_ease_focus = this._is_focused();
+ const ease_out_timeline_ms = 150;
+ const pause_ms = 50;
+ const ease_in_timeline_ms = 500; // includes ease out and pause
+ const ease_in_begin_pct = (ease_out_timeline_ms + pause_ms) / ease_in_timeline_ms;
+ const timeline_ms = this._distance_ease_focus ?
+ ease_in_timeline_ms :
+ ease_out_timeline_ms;
+
+ this._distance_ease_start = this._current_display_distance;
+ this._distance_ease_timeline = Clutter.Timeline.new_for_actor(this.get_actor(), timeline_ms);
+
+ this._distance_ease_target = desired_distance;
+ this._distance_ease_timeline.connect('new-frame', (() => {
+ let progress = this._distance_ease_timeline.get_progress();
+ if (this._distance_ease_focus) {
+ // if we're the focused display, wait for the first half of the timeline to pass
+ if (progress < ease_in_begin_pct) return;
+
+ // treat the second half of the timeline as its own full progression
+ progress = (progress - ease_in_begin_pct) / (1 - ease_in_begin_pct);
+
+ // put this display in front as it starts to easy in
+ this.is_closest = true;
+ } else {
+ this.is_closest = false;
+ }
+
+ this._current_display_distance = this._distance_ease_start +
+ (1 - Math.cos(progress * Math.PI)) / 2 * (this._distance_ease_target - this._distance_ease_start);
+ this._update_display_position_uniforms();
+ }).bind(this));
+
+ this._distance_ease_timeline.start();
+
+ if (this.smooth_follow_enabled) this._handle_smooth_follow_enabled_update();
+ }
+
+ _handle_smooth_follow_enabled_update() {
+ // we'll re-trigger this once a monitor becomes focused
+ if (this.focused_monitor_index === -1) return;
+
+ this._use_smooth_follow_origin = false;
+
+ if (this._follow_ease_timeline?.is_playing()) this._follow_ease_timeline.stop();
+
+ const from = this._current_follow_ease_progress;
+ const to = this.smooth_follow_enabled && this._is_focused() ? 1.0 : 0.0;
+ const toggleTime = this.smooth_follow_toggle_epoch_ms === 0 ? Date.now() : this.smooth_follow_toggle_epoch_ms;
+
+ // would have been a slight delay between request and slerp actually starting
+ const toggleDelayMs = (Date.now() - toggleTime) * 0.75;
+ const slerpStartTime = toggleTime + toggleDelayMs;
+
+ if (to !== from) {
+ this._follow_ease_timeline = Clutter.Timeline.new_for_actor(
+ this.get_actor(),
+ SMOOTH_FOLLOW_SLERP_TIMELINE_MS - toggleDelayMs
+ );
+ this._follow_ease_timeline.connect('new-frame', ((timeline, elapsed_ms) => {
+ const toggleTimeOffsetMs = Date.now() - slerpStartTime;
+
+ // this relies on the slerp function tuned to reach 100% in about 1 second
+ const progress = smoothFollowSlerpProgress(toggleTimeOffsetMs);
+ this._current_follow_ease_progress = from + (to - from) * progress;
+ this._update_display_position_uniforms();
+ }).bind(this));
+
+ this._follow_ease_timeline.connect('completed', (() => {
+ this._current_follow_ease_progress = to;
+ this._use_smooth_follow_origin = false;
+ this.smooth_follow_toggle_epoch_ms = 0;
+ this._update_display_position_uniforms();
+ }).bind(this));
+
+ this._follow_ease_timeline.start();
+ } else if (!this.smooth_follow_enabled) {
+ // smooth follow has been turned off and this screen wasn't the focus,
+ // continue to use the smooth_follow_origin data for 1 more second
+ this._use_smooth_follow_origin = true;
+ GLib.timeout_add(
+ GLib.PRIORITY_DEFAULT,
+ SMOOTH_FOLLOW_SLERP_TIMELINE_MS - toggleDelayMs,
+ (() => {
+ this._use_smooth_follow_origin = false;
+ this.smooth_follow_toggle_epoch_ms = 0;
+ this._current_follow_ease_progress = to;
+ return GLib.SOURCE_REMOVE;
+ }).bind(this)
+ );
+ }
+ }
+
+ // follow_ease transitions this from a rotated display (0.0) to a centered/focused display (1.0)
+ _update_display_position_uniforms() {
+ // this is in NWU coordinates
+ const monitorPlacement = this.monitor_placements[this.monitor_index];
+
+ // use the center vector with the distance applied to determine how much to move each coordinate, so they all move uniformly
+ const inverseAppliedDistance = 1.0 - this._current_display_distance / this.display_distance_default;
+ const distanceDelta = monitorPlacement.centerNoRotate.map(coord => coord * inverseAppliedDistance);
+ const noRotationVector = monitorPlacement.topLeftNoRotate.map((coord, index) => coord - distanceDelta[index]);
+
+ // convert to CoGL's east-down-south coordinates and apply display distance
+ const inverse_follow_ease = 1.0 - this._current_follow_ease_progress;
+ if (this._current_follow_ease_progress === 0.0) {
+ this.set_uniform_float(this.get_uniform_location("u_display_position"), 3,
+ [-noRotationVector[1], -noRotationVector[2], -noRotationVector[0]]);
+ } else {
+ const focusDistanceNorth = monitorPlacement.centerOrigin[0] * inverseAppliedDistance;
+ const centerOriginVector = {...monitorPlacement.centerOrigin};
+ centerOriginVector[0] -= focusDistanceNorth;
+
+ // slerp from the rotated display to the centered display
+ const followVector = noRotationVector.map((coord, index) => coord * inverse_follow_ease + centerOriginVector[index] * this._current_follow_ease_progress);
+ this.set_uniform_float(this.get_uniform_location("u_display_position"), 3,
+ [-followVector[1], -followVector[2], -followVector[0]]);
+ }
+
+ const rotation_radians = this.monitor_placements[this.monitor_index].rotationAngleRadians;
+ this.set_uniform_float(this.get_uniform_location("u_rotation_x_radians"), 1, [rotation_radians.x * inverse_follow_ease]);
+ this.set_uniform_float(this.get_uniform_location("u_rotation_y_radians"), 1, [rotation_radians.y * inverse_follow_ease]);
+ }
+
+ _handle_banner_update() {
+ this.set_uniform_float(this.get_uniform_location("u_show_banner"), 1, [this.show_banner ? 1.0 : 0.0]);
+ }
+
+ perspective(fovHorizontalRadians, aspect, near, far) {
+ const f = 1.0 / Math.tan(fovHorizontalRadians / 2.0);
+ const range = far - near;
+
+ return [
+ f / aspect, 0, 0, 0,
+ 0, f, 0, 0,
+ 0, 0, - (far + near) / range, -1,
+ 0, 0, - (2.0 * near * far) / range, 0
+ ];
+ }
+
+ vfunc_build_pipeline() {
+ const declarations = `
+ uniform bool u_show_banner;
+ uniform mat4 u_imu_data;
+ uniform float u_look_ahead_ms;
+ uniform vec4 u_look_ahead_cfg;
+ uniform mat4 u_projection_matrix;
+ uniform float u_fov_vertical_radians;
+ uniform vec3 u_display_position;
+ uniform float u_rotation_x_radians;
+ uniform float u_rotation_y_radians;
+ uniform vec2 u_display_resolution;
+ uniform vec3 u_lens_vector;
+
+ // vector positions are relative to the width and height of the entire stage
+ uniform vec2 u_actor_to_display_ratios;
+ uniform vec2 u_actor_to_display_offsets;
+
+ // discovered through trial and error, no idea the significance
+ float cogl_position_mystery_factor = 29.09 * 2;
+
+ float look_ahead_ms_cap = 45.0;
+
+ vec4 quatConjugate(vec4 q) {
+ return vec4(-q.xyz, q.w);
+ }
+
+ vec3 applyQuaternionToVector(vec3 v, vec4 q) {
+ vec3 t = 2.0 * cross(q.xyz, v);
+ return v + q.w * t + cross(q.xyz, t);
+ }
+
+ vec3 applyXRotationToVector(vec3 v, float angle) {
+ float c = cos(angle);
+ float s = sin(angle);
+ return vec3(v.x, v.y * c - v.z * s, v.y * s + v.z * c);
+ }
+
+ vec3 applyYRotationToVector(vec3 v, float angle) {
+ float c = cos(angle);
+ float s = sin(angle);
+ return vec3(v.x * c + v.z * s, v.y, v.z * c - v.x * s);
+ }
+
+ vec4 nwuToESU(vec4 v) {
+ return vec4(-v.y, v.z, -v.x, v.w);
+ }
+
+ // returns the rate of change between the two vectors, in same time units as delta_time
+ // e.g. if delta_time is in ms, then the rate of change is "per ms"
+ vec3 rateOfChange(vec3 v1, vec3 v2, float delta_time) {
+ return (v1-v2) / delta_time;
+ }
+
+ // attempt to figure out where the current position should be based on previous position and velocity.
+ // velocity and time values should use the same time units (secs, ms, etc...)
+ vec3 applyLookAhead(vec3 position, vec3 velocity, float look_ahead_ms) {
+ return position + velocity * look_ahead_ms;
+ }
+
+ // project the vector onto a flat surface, return it's vertical position relative to the vertical fov, where 0.0 is
+ // the top and 1.0 is the bottom. vectors that project outside the vertical range of the display will have values
+ // outside this range, but capped
+ float vectorToScanline(float fovVerticalRadians, vec3 v) {
+ return clamp(1.0 - (-v.y / (tan(fovVerticalRadians / 2.0) * v.z) + 1.0) / 2.0, -1.5, 2.5);
+ }
+ `;
+
+ const main = `
+ vec4 world_pos = cogl_position_in;
+
+ if (!u_show_banner) {
+ float aspect_ratio = u_display_resolution.x / u_display_resolution.y;
+
+ float cogl_position_width = cogl_position_mystery_factor * aspect_ratio / u_actor_to_display_ratios.y;
+ float cogl_position_height = cogl_position_width / aspect_ratio;
+
+ float pos_z_factor = aspect_ratio / u_actor_to_display_ratios.y;
+ vec3 pos_factors = vec3(
+ cogl_position_width / u_display_resolution.x,
+ cogl_position_height / u_display_resolution.y,
+ cogl_position_mystery_factor * pos_z_factor / u_display_resolution.x
+ );
+ world_pos.x -= u_display_position.x * pos_factors.x;
+ world_pos.y -= u_display_position.y * pos_factors.y;
+ world_pos.z = u_display_position.z * pos_factors.z;
+
+ // if the perspective includes more than just our viewport actor, move vertices towards the center of the perspective so they'll be properly rotated
+ world_pos.x += u_actor_to_display_offsets.x * cogl_position_width / 2;
+ world_pos.y -= u_actor_to_display_offsets.y * cogl_position_height / 2;
+
+ vec3 complete_vector = applyXRotationToVector(world_pos.xyz, u_rotation_x_radians);
+ complete_vector = applyYRotationToVector(complete_vector, u_rotation_y_radians);
+
+ vec4 quat_t0 = nwuToESU(quatConjugate(u_imu_data[0]));
+ vec3 rotated_vector_t0 = applyQuaternionToVector(complete_vector, quat_t0);
+ vec3 rotated_vector_t1 = applyQuaternionToVector(complete_vector, nwuToESU(quatConjugate(u_imu_data[1])));
+ float delta_time_t0 = u_imu_data[3][0] - u_imu_data[3][1];
+ vec3 velocity_t0 = rateOfChange(rotated_vector_t0, rotated_vector_t1, delta_time_t0);
+
+ // compute the capped look ahead with scanline adjustments
+ float look_ahead_scanline_ms = u_look_ahead_ms == 0.0 ? 0.0 : vectorToScanline(u_fov_vertical_radians, rotated_vector_t0) * u_look_ahead_cfg[2];
+ float effective_look_ahead_ms = min(min(u_look_ahead_ms, look_ahead_ms_cap), u_look_ahead_cfg[3]) + look_ahead_scanline_ms;
+
+ vec3 look_ahead_vector = applyLookAhead(rotated_vector_t0, velocity_t0, effective_look_ahead_ms);
+
+ vec3 adjusted_lens_vector = u_lens_vector * pos_factors;
+ world_pos = vec4(look_ahead_vector - adjusted_lens_vector, world_pos.w);
+
+ world_pos.z /= pos_z_factor;
+
+ world_pos.x *= u_actor_to_display_ratios.y / u_actor_to_display_ratios.x;
+
+ world_pos = u_projection_matrix * world_pos;
+
+ // if the perspective includes more than just our viewport actor, move the vertices back to just the area we can see.
+ // this needs to be done after the projection matrix multiplication so it will be projected as if centered in our vision
+ world_pos.x -= (u_actor_to_display_offsets.x / u_actor_to_display_ratios.x) * world_pos.w;
+ world_pos.y += (u_actor_to_display_offsets.y / u_actor_to_display_ratios.y) * world_pos.w;
+ } else {
+ world_pos = cogl_modelview_matrix * world_pos;
+ world_pos = cogl_projection_matrix * world_pos;
+ }
+
+ cogl_position_out = world_pos;
+ cogl_tex_coord_out[0] = cogl_tex_coord_in;
+ `
+
+ this.add_glsl_snippet(Shell.SnippetHook.VERTEX, declarations, main, false);
+ }
+
+ vfunc_paint_target(node, paintContext) {
+ if (!this._initialized) {
+ const aspect = this.target_monitor.width / this.target_monitor.height;
+ const fovRadians = diagonalToCrossFOVs(degreeToRadian(Globals.data_stream.device_data.displayFov), aspect);
+ const projection_matrix = this.perspective(
+ fovRadians.horizontal,
+ aspect,
+ 0.0001,
+ 1000.0
+ );
+ this.set_uniform_matrix(this.get_uniform_location("u_projection_matrix"), false, 4, projection_matrix);
+ this.set_uniform_float(this.get_uniform_location("u_fov_vertical_radians"), 1, [fovRadians.vertical]);
+ this.set_uniform_float(this.get_uniform_location("u_display_resolution"), 2, [this.target_monitor.width, this.target_monitor.height]);
+ this.set_uniform_float(this.get_uniform_location("u_look_ahead_cfg"), 4, Globals.data_stream.device_data.lookAheadCfg);
+ this.set_uniform_float(this.get_uniform_location("u_actor_to_display_ratios"), 2, this.actor_to_display_ratios);
+ this.set_uniform_float(this.get_uniform_location("u_actor_to_display_offsets"), 2, this.actor_to_display_offsets);
+ this.set_uniform_float(this.get_uniform_location("u_lens_vector"), 3, this.lens_vector);
+ this._update_display_position_uniforms();
+ this._handle_banner_update();
+ this._initialized = true;
+ }
+
+ let lookAheadSet = false;
+ if (!this._use_smooth_follow_origin && (!this.smooth_follow_enabled || this._is_focused() || this._current_follow_ease_progress > 0.0)) {
+ if (this._current_follow_ease_progress > 0.0 && this._current_follow_ease_progress < 1.0) {
+ // don't apply look-ahead while the display is slerping
+ this.set_uniform_float(this.get_uniform_location('u_look_ahead_ms'), 1, [0.0]);
+ lookAheadSet = true;
+ }
+ this.set_uniform_matrix(this.get_uniform_location("u_imu_data"), false, 4, this.imu_snapshots.imu_data);
+ } else {
+ this.set_uniform_matrix(this.get_uniform_location("u_imu_data"), false, 4, this.imu_snapshots.smooth_follow_origin);
+ }
+ if (!lookAheadSet) {
+ this.set_uniform_float(this.get_uniform_location('u_look_ahead_ms'), 1, [lookAheadMS(this.imu_snapshots.timestamp_ms, Globals.data_stream.device_data.lookAheadCfg, this.look_ahead_override)]);
+ }
+
+ 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
+ );
+ }
+
+ super.vfunc_paint_target(node, paintContext);
+ }
+});
\ No newline at end of file
diff --git a/gnome/src/virtualdisplaysactor.js b/gnome/src/virtualdisplaysactor.js
new file mode 100644
index 0000000..cab3c47
--- /dev/null
+++ b/gnome/src/virtualdisplaysactor.js
@@ -0,0 +1,907 @@
+import Clutter from 'gi://Clutter'
+import Cogl from 'gi://Cogl';
+import GdkPixbuf from 'gi://GdkPixbuf';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Mtk from 'gi://Mtk';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+import { VirtualDisplayEffect, SMOOTH_FOLLOW_SLERP_TIMELINE_MS } from './virtualdisplayeffect.js';
+import { degreeToRadian, diagonalToCrossFOVs } from './math.js';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+
+import Globals from './globals.js';
+
+function applyQuaternionToVector(vector, quaternion) {
+ const t = [
+ 2.0 * (quaternion[1] * vector[2] - quaternion[2] * vector[1]),
+ 2.0 * (quaternion[2] * vector[0] - quaternion[0] * vector[2]),
+ 2.0 * (quaternion[0] * vector[1] - quaternion[1] * vector[0])
+ ];
+ return [
+ vector[0] + quaternion[3] * t[0] + quaternion[1] * t[2] - quaternion[2] * t[1],
+ vector[1] + quaternion[3] * t[1] + quaternion[2] * t[0] - quaternion[0] * t[2],
+ vector[2] + quaternion[3] * t[2] + quaternion[0] * t[1] - quaternion[1] * t[0]
+ ];
+}
+
+// if nothing is in focus, take it as soon as it crosses into the monitor's bounds
+const FOCUS_THRESHOLD = 0.95 / 2.0;
+
+// if we leave the monitor with some margin, unfocus even if no other monitor is in focus
+const UNFOCUS_THRESHOLD = 1.2 / 2.0;
+
+/**
+ * Find the vector in the array that's closest to the quaternion rotation
+ *
+ * @param {number[]} quaternion - Reference quaternion [x, y, z, w]
+ * @param {number[][]} monitorVectors - Array of monitor vectors [x, y, z] to search from
+ * @param {number} currentFocusedIndex - Index of the currently focused monitor
+ * @param {number} focusedMonitorDistance - Distance to the focused monitor, < 1.0 if zoomed in
+ * @param {boolean} smoothFollowEnabled - If true, always keep the current monitor in focus or choose the closest
+ * @param {Object} fovDetails - Contains reference widthPixels, heightPixels, horizontal and vertical radians, and pixel distance to the center of the screen
+ * @param {Object[]} monitorsDetails - Contains x, y, width, height (coordinates from top-left) for each monitor
+ * @returns {number} Index of the closest vector, if it surpasses the previous closest index by a certain margin, otherwise the previous index
+ */
+function findFocusedMonitor(quaternion, monitorVectors, currentFocusedIndex, focusedMonitorDistance, smoothFollowEnabled, fovDetails, monitorsDetails) {
+ const lookVector = [1.0, 0.0, 0.0]; // NWU vector pointing to the center of the screen
+ const rotatedLookVector = applyQuaternionToVector(lookVector, quaternion);
+
+ const xzMagnitude = Math.sqrt(rotatedLookVector[0]*rotatedLookVector[0] + rotatedLookVector[2]*rotatedLookVector[2]);
+ const lookUpTheta = Math.atan2(rotatedLookVector[2], rotatedLookVector[0]);
+
+ let closestIndex = -1;
+ let closestDistance = Infinity;
+ let currentFocusedDistance = Infinity;
+
+ // find the vector closest to the rotated look vector
+ monitorVectors.forEach((vector, index) => {
+ const monitor = monitorsDetails[index];
+ const monitorAspectRatio = monitor.width / monitor.height;
+
+ // weight the rotation about the y-axis between the two vectors, by the aspect ratio
+ const vectorUpTheta = Math.atan2(vector[2], vector[0]);
+ const upDelta = lookUpTheta - vectorUpTheta;
+ const newLookUpTheta = Math.tan(Math.max(
+ -Math.PI,
+ Math.min(
+ Math.PI,
+ upDelta * monitorAspectRatio + vectorUpTheta
+ )
+ ));
+ const weightedLookVector = [
+ xzMagnitude * Math.cos(newLookUpTheta),
+ rotatedLookVector[1],
+ xzMagnitude * Math.sin(newLookUpTheta)
+ ];
+
+ // find the distance between the monitor vector and weighted look vector
+ const distance = Math.acos(
+ Math.min(1.0, Math.max(-1.0,
+ vector[0] * weightedLookVector[0] +
+ vector[1] * weightedLookVector[1] +
+ vector[2] * weightedLookVector[2]
+ ))
+ );
+
+ // TODO - this assumes the display is facing towards us, need to account for looking in the "flat" direction
+ const distanceFromCenterPixels = fovDetails.completeScreenDistancePixels * Math.tan(distance);
+ const distanceFromCenterSizeRatio = distanceFromCenterPixels / monitor.width;
+
+ if (currentFocusedIndex === index) {
+ currentFocusedDistance = distanceFromCenterSizeRatio * focusedMonitorDistance;
+ }
+
+ if (distanceFromCenterSizeRatio < closestDistance) {
+ closestIndex = index;
+ closestDistance = distanceFromCenterSizeRatio;
+ }
+ });
+
+ const keepCurrent = currentFocusedIndex !== -1 && (smoothFollowEnabled || currentFocusedDistance < UNFOCUS_THRESHOLD);
+ if (!keepCurrent) {
+ if (smoothFollowEnabled || closestDistance < FOCUS_THRESHOLD) return closestIndex;
+
+ // neither the current nor the closest will take focus, unfocus all displays
+ return -1;
+ }
+
+ return currentFocusedIndex;
+}
+
+/***
+ * @returns {Object} - containing `begin`, `center`, and `end` radians for rotating the given monitor
+ */
+function monitorWrap(cachedMonitorRadians, radiusPixels, monitorSpacingPixels, monitorBeginPixel, monitorLengthPixels) {
+ let closestWrapPixel = monitorBeginPixel;
+ let closestWrap = cachedMonitorRadians[monitorBeginPixel];
+ if (closestWrap === undefined) {
+ closestWrapPixel = Object.keys(cachedMonitorRadians).reduce((previousPixel, currentPixel) => {
+ if (previousPixel === undefined) return currentPixel;
+
+ const currentDelta = currentPixel - monitorBeginPixel;
+ const previousDelta = previousPixel - monitorBeginPixel;
+
+ // always prefer an exact monitor width match
+ if (previousDelta % monitorLengthPixels !== 0) {
+ if (currentDelta % monitorLengthPixels === 0) return currentPixel;
+
+ // prefer placing a monitor to the right or below, even if there's a closer placement to the left or above
+ if (previousDelta < 0 && currentDelta > 0) return currentPixel;
+
+ // otherwise, just prefer the closest one
+ if (Math.abs(currentDelta) < Math.abs(previousDelta)) return currentPixel;
+ }
+
+ return previousPixel;
+ }, undefined);
+ closestWrap = cachedMonitorRadians[closestWrapPixel];
+ }
+
+ const spacingRadians = Math.asin(monitorSpacingPixels / 2 / radiusPixels) * 2;
+ if (closestWrapPixel !== monitorBeginPixel) {
+ // there's a gap between the cached wrap value and this one
+ const gapPixels = monitorBeginPixel - closestWrapPixel;
+ const gapHalfRadians = Math.asin(gapPixels / 2 / radiusPixels);
+ const gapRadians = gapHalfRadians * 2;
+
+ // use Math.floor so if it's negative (this monitor is to the left of or above the closest) it will always
+ // compenstate for the spacing that's needed at the right/bottom
+ const appliedSpacingRadians = Math.floor(gapPixels / monitorLengthPixels) * spacingRadians;
+
+ // update the closestWrap value and cache it
+ closestWrap = closestWrap + gapRadians + appliedSpacingRadians;
+ closestWrapPixel = monitorBeginPixel;
+ cachedMonitorRadians[closestWrapPixel] = closestWrap;
+ }
+
+ const monitorHalfRadians = Math.asin(monitorLengthPixels / 2 / radiusPixels);
+ const centerRadians = closestWrap + monitorHalfRadians;
+ const endRadians = centerRadians + monitorHalfRadians;
+
+ // since we're computing the end values for this monitor, cache them too in case they line up with a future monitor
+ const nextMonitorPixel = monitorBeginPixel + monitorLengthPixels;
+ if (cachedMonitorRadians[nextMonitorPixel] === undefined)
+ cachedMonitorRadians[nextMonitorPixel] = endRadians + spacingRadians;
+
+ return {
+ begin: closestWrap,
+ center: centerRadians,
+ end: endRadians
+ }
+}
+
+/**
+ * Convert the given monitor details into NWU vectors describing the center of the fully placed monitor,
+ * and the top-left of the partially placed monitor (minus only a single-axis rotation)
+ *
+ * @param {Object} fovDetails - contains reference widthPixels, heightPixels, horizontal and vertical radians,
+* and distance to the center of the screen
+ * @param {Object[]} monitorDetailsList - contains x, y, width, height (coordinates from top-left)
+ * @param {string} monitorWrappingScheme - horizontal, vertical, none
+ * @returns {Object[]} - contains NWU vectors pointing to `topLeftNoRotate` and `center` of each monitor
+ * and a `rotation` angle for the given wrapping scheme
+ */
+function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingScheme, monitorSpacing) {
+ const monitorPlacements = [];
+ const cachedMonitorRadians = {};
+
+ Globals.logger.log_debug(`\t\t\tFOV Details: ${JSON.stringify(fovDetails)}, Monitor Wrapping Scheme: ${monitorWrappingScheme}`);
+
+ if (monitorWrappingScheme === 'horizontal') {
+ // monitors wrap around us horizontally
+
+ // distance to a horizontal edge is the hypothenuse of the triangle where the opposite side is half the width of the reference fov screen
+ const sideEdgeRadius = Math.sqrt(Math.pow(fovDetails.widthPixels / 2, 2) + Math.pow(fovDetails.completeScreenDistancePixels, 2));
+ const monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels;
+
+ cachedMonitorRadians[0] = -fovDetails.defaultDistanceHorizontalRadians / 2;
+ monitorDetailsList.forEach(monitorDetails => {
+ const monitorWrapDetails = monitorWrap(cachedMonitorRadians, sideEdgeRadius, monitorSpacingPixels, monitorDetails.x, monitorDetails.width);
+ const monitorCenterRadius = Math.sqrt(Math.pow(sideEdgeRadius, 2) - Math.pow(monitorDetails.width / 2, 2));
+ const upTopPixels = monitorDetails.y + (monitorDetails.y / fovDetails.heightPixels) * monitorSpacingPixels;
+
+ // how to place the monitors at the origin (0, 0)
+ const westCenterOriginPixels = (monitorDetails.width - fovDetails.widthPixels) / 2;
+ const upCenterOriginPixels = (monitorDetails.height - fovDetails.heightPixels) / 2;
+
+ const upCenterPixels = upTopPixels + upCenterOriginPixels;
+
+ monitorPlacements.push({
+ topLeftNoRotate: [
+ monitorCenterRadius,
+
+ // west stays aligned with the origin, will apply rotationAngleRadians value during rendering
+ -westCenterOriginPixels,
+
+ // up is flat when wrapping horizontally, apply it here as a constant, not touched by rendering
+ -upTopPixels
+ ],
+ centerNoRotate: [
+ monitorCenterRadius,
+
+ // west centered about the FOV center
+ 0,
+
+ // up is flat when wrapping horizontally
+ -upCenterPixels
+ ],
+ centerOrigin: [
+ monitorCenterRadius,
+ -westCenterOriginPixels,
+ upCenterOriginPixels
+ ],
+ centerLook: [
+ // north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians
+ monitorCenterRadius * Math.cos(monitorWrapDetails.center),
+
+ // west is opposite where radius is the hypotenuse, using monitorWrapDetails.center as the radians
+ -monitorCenterRadius * Math.sin(monitorWrapDetails.center),
+
+ // up is flat when wrapping horizontally
+ -upCenterPixels
+ ],
+ rotationAngleRadians: {
+ x: 0,
+ y: -monitorWrapDetails.center
+ }
+ });
+ });
+ } else if (monitorWrappingScheme === 'vertical') {
+ // monitors wrap around us vertically
+
+ // distance to the top edge is the hypothenuse of the triangle where the opposite side is half the height of the reference fov screen
+ const topEdgeRadius = Math.sqrt(Math.pow(fovDetails.heightPixels / 2, 2) + Math.pow(fovDetails.completeScreenDistancePixels, 2));
+ const monitorSpacingPixels = monitorSpacing * fovDetails.heightPixels;
+
+ cachedMonitorRadians[0] = -fovDetails.defaultDistanceVerticalRadians / 2;
+ monitorDetailsList.forEach(monitorDetails => {
+ const monitorWrapDetails = monitorWrap(cachedMonitorRadians, topEdgeRadius, monitorSpacingPixels, monitorDetails.y, monitorDetails.height);
+ const monitorCenterRadius = Math.sqrt(Math.pow(topEdgeRadius, 2) - Math.pow(monitorDetails.height / 2, 2));
+ const westPixels = monitorDetails.x + (monitorDetails.x / fovDetails.widthPixels) * monitorSpacingPixels;
+
+ // how to place the monitors at the origin (0, 0)
+ const westCenterOriginPixels = (monitorDetails.width - fovDetails.widthPixels) / 2;
+ const upCenterOriginPixels = (monitorDetails.height - fovDetails.heightPixels) / 2;
+
+ const westCenterPixels = westPixels + westCenterOriginPixels;
+
+ monitorPlacements.push({
+ topLeftNoRotate: [
+ monitorCenterRadius,
+
+ // west is flat when wrapping vertically, apply it here as a constant, not touched by rendering
+ westPixels,
+
+ // up stays aligned with the origin, will apply rotationAngleRadians value during rendering
+ upCenterOriginPixels
+ ],
+ centerNoRotate: [
+ monitorCenterRadius,
+
+ // west is flat when wrapping horizontally
+ westCenterPixels,
+
+ // west centered about the FOV center
+ 0
+ ],
+ centerOrigin: [
+ monitorCenterRadius,
+ -westCenterOriginPixels,
+ upCenterOriginPixels
+ ],
+ centerLook: [
+ // north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians
+ monitorCenterRadius * Math.cos(monitorWrapDetails.center),
+
+ // west is flat when wrapping vertically
+ -westCenterPixels,
+
+ // up is opposite where radius is the hypotenuse, using monitorWrapDetails.center as the radians
+ -monitorCenterRadius * Math.sin(monitorWrapDetails.center)
+ ],
+ rotationAngleRadians: {
+ x: -monitorWrapDetails.center,
+ y: 0
+ }
+ });
+ });
+ } else {
+ const monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels;
+
+ // monitors make a flat wall in front of us, no wrapping
+ monitorDetailsList.forEach(monitorDetails => {
+ const upPixels = monitorDetails.y + (monitorDetails.y / fovDetails.heightPixels) * monitorSpacingPixels;
+ const westPixels = monitorDetails.x + (monitorDetails.x / fovDetails.widthPixels) * monitorSpacingPixels;
+
+ // how to place the monitors at the origin (0, 0)
+ const westCenterOriginPixels = (monitorDetails.width - fovDetails.widthPixels) / 2;
+ const upCenterOriginPixels = (monitorDetails.height - fovDetails.heightPixels) / 2;
+
+ const westCenterPixels = westPixels + westCenterOriginPixels;
+ const upCenterPixels = upPixels + upCenterOriginPixels;
+
+ monitorPlacements.push({
+ topLeftNoRotate: [
+ fovDetails.completeScreenDistancePixels,
+ westPixels,
+ -upPixels
+ ],
+ centerNoRotate: [
+ fovDetails.completeScreenDistancePixels,
+ westCenterPixels,
+ -upCenterPixels
+ ],
+ centerOrigin: [
+ fovDetails.completeScreenDistancePixels,
+ -westCenterOriginPixels,
+ upCenterOriginPixels
+ ],
+ centerLook: [
+ fovDetails.completeScreenDistancePixels,
+ -westCenterPixels,
+ -upCenterPixels
+ ],
+ rotationAngleRadians: {
+ x: 0,
+ y: 0
+ }
+ });
+ });
+ }
+
+ Globals.logger.log_debug(`\t\t\tMonitor placements: ${JSON.stringify(monitorPlacements)}, cached values: ${JSON.stringify(cachedMonitorRadians)}`);
+
+ return monitorPlacements;
+}
+
+export const VirtualDisplaysActor = GObject.registerClass({
+ Properties: {
+ 'target-monitor': GObject.ParamSpec.jsobject(
+ 'target-monitor',
+ 'Target Monitor',
+ 'Details about the monitor being used as a viewport',
+ GObject.ParamFlags.READWRITE
+ ),
+ 'virtual-monitors': GObject.ParamSpec.jsobject(
+ 'virtual-monitors',
+ 'Virtual Monitors',
+ 'Details about the virtual monitors',
+ GObject.ParamFlags.READWRITE
+ ),
+ 'monitor-wrapping-scheme': GObject.ParamSpec.string(
+ 'monitor-wrapping-scheme',
+ 'Monitor Wrapping Scheme',
+ 'How the monitors are wrapped around the viewport',
+ GObject.ParamFlags.READWRITE,
+ 'horizontal', ['horizontal', 'vertical', 'none']
+ ),
+ 'monitor-spacing': GObject.ParamSpec.int(
+ 'monitor-spacing',
+ 'Monitor Spacing',
+ 'Visual spacing between monitors, units are 0.001 of the viewport width',
+ GObject.ParamFlags.READWRITE,
+ 0, 100, 0
+ ),
+ 'viewport-offset-x': GObject.ParamSpec.double(
+ 'viewport-offset-x',
+ 'Viewport Offset x',
+ 'Offset to apply to the viewport',
+ GObject.ParamFlags.READWRITE,
+ -2.5, 2.5, 0.0
+ ),
+ 'viewport-offset-y': GObject.ParamSpec.double(
+ 'viewport-offset-y',
+ 'Viewport Offset y',
+ 'Offset to apply to the viewport',
+ GObject.ParamFlags.READWRITE,
+ -2.5, 2.5, 0.0
+ ),
+ 'monitor-placements': GObject.ParamSpec.jsobject(
+ 'monitor-placements',
+ 'Monitor Placements',
+ 'Target and virtual monitor placement details, as relevant to rendering',
+ GObject.ParamFlags.READWRITE
+ ),
+ 'monitor-actors': GObject.ParamSpec.jsobject(
+ 'monitor-actors',
+ 'Monitor Actors',
+ 'Tracking actors and details for each monitor',
+ GObject.ParamFlags.READWRITE
+ ),
+ 'imu-snapshots': GObject.ParamSpec.jsobject(
+ 'imu-snapshots',
+ 'IMU Snapshots',
+ 'Latest IMU quaternion snapshots and epoch timestamp for when it was collected',
+ GObject.ParamFlags.READWRITE
+ ),
+ 'smooth-follow-enabled': GObject.ParamSpec.boolean(
+ 'smooth-follow-enabled',
+ 'Smooth follow enabled',
+ 'Whether smooth follow is enabled',
+ GObject.ParamFlags.READWRITE,
+ false
+ ),
+ 'smooth-follow-toggle-epoch-ms': GObject.ParamSpec.uint64(
+ 'smooth-follow-toggle-epoch-ms',
+ 'Smooth follow toggle epoch time',
+ 'ms since epoch when smooth follow was toggled',
+ GObject.ParamFlags.READWRITE,
+ 0, Number.MAX_SAFE_INTEGER, 0
+ ),
+ 'show-banner': GObject.ParamSpec.boolean(
+ 'show-banner',
+ 'Show banner',
+ 'Whether the banner should be displayed',
+ GObject.ParamFlags.READWRITE,
+ false
+ ),
+ 'custom-banner-enabled': GObject.ParamSpec.boolean(
+ 'custom-banner-enabled',
+ 'Custom banner enabled',
+ 'Whether the custom banner should be displayed',
+ GObject.ParamFlags.READWRITE,
+ false
+ ),
+ 'focused-monitor-index': GObject.ParamSpec.int(
+ 'focused-monitor-index',
+ 'Focused Monitor Index',
+ 'Index of the monitor that is currently focused',
+ GObject.ParamFlags.READWRITE,
+ -1, 100, -1
+ ),
+ 'focused-monitor-details': GObject.ParamSpec.jsobject(
+ 'focused-monitor-details',
+ 'Focused Monitor Details',
+ 'Details about the monitor that is currently focused',
+ GObject.ParamFlags.READWRITE
+ ),
+ 'display-size': GObject.ParamSpec.double(
+ 'display-size',
+ 'Display size',
+ 'Size of the display',
+ GObject.ParamFlags.READWRITE,
+ 0.2,
+ 2.5,
+ 1.0
+ ),
+ 'display-zoom-on-focus': GObject.ParamSpec.boolean(
+ 'display-zoom-on-focus',
+ 'Display zoom on focus',
+ 'Automatically move a display closer when it becomes focused.',
+ GObject.ParamFlags.READWRITE,
+ true
+ ),
+ 'display-distance': GObject.ParamSpec.double(
+ 'display-distance',
+ 'Display Distance',
+ 'Distance of the display from the camera',
+ GObject.ParamFlags.READWRITE,
+ 0.2,
+ 2.5,
+ 1.05
+ ),
+ 'lens-vector': GObject.ParamSpec.jsobject(
+ 'lens-vector',
+ 'Lens Vector',
+ 'Vector representing the offset of the lens from the pivot point',
+ GObject.ParamFlags.READWRITE
+ ),
+ 'toggle-display-distance-start': GObject.ParamSpec.double(
+ 'toggle-display-distance-start',
+ 'Display distance start',
+ 'Start distance when using the "change distance" shortcut.',
+ GObject.ParamFlags.READWRITE,
+ 0.2,
+ 2.5,
+ 1.05
+ ),
+ 'toggle-display-distance-end': GObject.ParamSpec.double(
+ 'toggle-display-distance-end',
+ 'Display distance end',
+ 'End distance when using the "change distance" shortcut.',
+ GObject.ParamFlags.READWRITE,
+ 0.2,
+ 2.5,
+ 1.05
+ ),
+ 'framerate-cap': GObject.ParamSpec.double(
+ 'framerate-cap',
+ 'Framerate Cap',
+ 'Maximum framerate to render at',
+ GObject.ParamFlags.READWRITE,
+ 0.0, 240.0, 0.0
+ ),
+ '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 VirtualDisplaysActor extends Clutter.Actor {
+ constructor(params = {}) {
+ super(params);
+
+ this._all_monitors = [
+ this.target_monitor,
+ ...this.virtual_monitors
+ ]
+
+ const bannerTextureClippingRect = new Mtk.Rectangle({
+ x: 0,
+ y: 0,
+ width: 800,
+ height: 200
+ });
+
+ const calibratingBanner = GdkPixbuf.Pixbuf.new_from_file(`${Globals.extension_dir}/textures/calibrating.png`);
+ const calibratingImage = new Clutter.Image();
+ calibratingImage.set_data(calibratingBanner.get_pixels(), Cogl.PixelFormat.RGB_888,
+ calibratingBanner.width, calibratingBanner.height, calibratingBanner.rowstride);
+ this.bannerContent = Clutter.TextureContent.new_from_texture(calibratingImage.get_texture(), bannerTextureClippingRect);
+
+ const customBanner = GdkPixbuf.Pixbuf.new_from_file(`${Globals.extension_dir}/textures/custom_banner.png`);
+ const customBannerImage = new Clutter.Image();
+ customBannerImage.set_data(customBanner.get_pixels(), Cogl.PixelFormat.RGB_888,
+ customBanner.width, customBanner.height, customBanner.rowstride);
+ this.customBannerContent = Clutter.TextureContent.new_from_texture(customBannerImage.get_texture(), bannerTextureClippingRect);
+
+ this.bannerActor = new Clutter.Actor({
+ width: calibratingBanner.width,
+ height: calibratingBanner.height,
+ reactive: false
+ });
+ this.bannerActor.set_position(
+ (this.target_monitor.width - this.bannerActor.width) / 2,
+ this.target_monitor.height * 0.75 - this.bannerActor.height / 2
+ );
+ this.bannerActor.set_content(this.custom_banner_enabled ? this.customBannerContent : this.bannerContent);
+ this.bannerActor.hide();
+
+ this.monitor_actors = [];
+ }
+
+ renderMonitors() {
+ // collect bindings and connections to clean up on dispose
+ this._property_bindings = [];
+ this._property_connections = [];
+
+ const notifyToFunction = ((property, fn) => {
+ this._property_connections.push(this.connect(`notify::${property}`, fn.bind(this)));
+ }).bind(this);
+
+ this._distance_ease_timeline = null;
+ notifyToFunction('toggle-display-distance-start', this._handle_display_distance_properties_change);
+ notifyToFunction('toggle-display-distance-end', this._handle_display_distance_properties_change);
+ notifyToFunction('display-distance', this._handle_display_distance_properties_change);
+ notifyToFunction('monitor-wrapping-scheme', this._update_monitor_placements);
+ notifyToFunction('monitor-spacing', this._update_monitor_placements);
+ notifyToFunction('viewport-offset-x', this._update_monitor_placements);
+ notifyToFunction('viewport-offset-y', this._update_monitor_placements);
+ notifyToFunction('show-banner', this._handle_banner_update);
+ notifyToFunction('custom-banner-enabled', this._handle_banner_update);
+ notifyToFunction('framerate-cap', this._handle_frame_rate_cap_change);
+ notifyToFunction('smooth-follow-enabled', this._handle_smooth_follow_enabled_change);
+ this._handle_display_distance_properties_change();
+ this._handle_frame_rate_cap_change();
+
+ const actorToDisplayRatios = [
+ global.stage.width / this.target_monitor.width,
+ global.stage.height / this.target_monitor.height
+ ];
+
+ // how far this viewport actor's center is from the center of the whole stage
+ const actorMidX = this.target_monitor.x + this.target_monitor.width / 2;
+ const actorMidY = this.target_monitor.y + this.target_monitor.height / 2;
+ const actorToDisplayOffsets = [
+ (global.stage.width / 2 - (actorMidX - global.stage.x)) * 2 / this.target_monitor.width,
+ (global.stage.height / 2 - (actorMidY - global.stage.y)) * 2 / this.target_monitor.height
+ ];
+
+ Globals.logger.log_debug(`\t\t\tActor to display ratios: ${actorToDisplayRatios}, offsets: ${actorToDisplayOffsets}`);
+
+ this._sorted_monitors.forEach(((monitor, index) => {
+ Globals.logger.log_debug(`\t\t\tMonitor ${index}: ${monitor.x}, ${monitor.y}, ${monitor.width}, ${monitor.height}`);
+
+ const containerActor = new Clutter.Actor({
+ clip_to_allocation: true
+ });
+ const viewport = new St.Bin({
+ child: containerActor,
+ width: monitor.width,
+ height: monitor.height
+ });
+
+ // Create a clone of the stage content for this monitor
+ const monitorClone = new Clutter.Clone({
+ source: Main.layoutManager.uiGroup,
+ clip_to_allocation: true,
+ x: -monitor.x,
+ y: -monitor.y
+ });
+
+ // Add the monitor actor to the scene
+ containerActor.add_child(monitorClone);
+ const effect = new VirtualDisplayEffect({
+ focused_monitor_index: this.focused_monitor_index,
+ imu_snapshots: this.imu_snapshots,
+ monitor_index: index,
+ monitor_placements: this.monitor_placements,
+ target_monitor: this.target_monitor,
+ display_distance: this.display_distance,
+ display_distance_default: this._display_distance_default(),
+ actor_to_display_ratios: actorToDisplayRatios,
+ actor_to_display_offsets: actorToDisplayOffsets,
+ lens_vector: this.lens_vector,
+ show_banner: this.show_banner
+ });
+ viewport.add_effect_with_name('viewport-effect', effect);
+ this.add_child(viewport);
+ Shell.util_set_hidden_from_pick(viewport, true);
+
+ this.monitor_actors.push({
+ viewport,
+ containerActor,
+ monitorClone,
+ effect,
+ monitorDetails: monitor
+ });
+
+ // do this so the primary monitor is always on top at first, before the focused monitor logic comes into play
+ this.set_child_below_sibling(viewport, null);
+
+ [
+ 'monitor-placements',
+ 'imu-snapshots',
+ 'smooth-follow-enabled',
+ 'smooth-follow-toggle-epoch-ms',
+ 'focused-monitor-index',
+ 'lens-vector',
+ 'look-ahead-override',
+ 'disable-anti-aliasing',
+ 'show-banner'
+ ].forEach((property => {
+ this._property_bindings.push(this.bind_property(property, effect, property, GObject.BindingFlags.DEFAULT));
+ }));
+
+ const updateEffectDistanceDefault = (() => {
+ effect.no_distance_ease = Math.abs(this.display_distance - effect.display_distance) <= 0.05;
+ effect.display_distance = this.display_distance;
+ effect.display_distance_default = this._display_distance_default();
+ }).bind(this);
+ this._property_connections.push(this.connect('notify::display-distance', updateEffectDistanceDefault));
+ this._property_connections.push(this.connect('notify::toggle-display-distance-start', updateEffectDistanceDefault));
+ this._property_connections.push(this.connect('notify::toggle-display-distance-end', updateEffectDistanceDefault));
+
+ // in addition to rendering distance properly in the shader, the parent actor determines overlap based on child ordering
+ effect.connect('notify::is-closest', ((actor, _pspec) => {
+ if (!this._is_disposed && actor.is_closest) {
+ this.set_child_above_sibling(viewport, null);
+ if (this.show_banner) this.set_child_above_sibling(this.bannerActor, null);
+ }
+ }).bind(this));
+ }).bind(this));
+
+ this.add_child(this.bannerActor);
+ if (this.show_banner) {
+ this.set_child_above_sibling(this.bannerActor, null);
+ this.bannerActor.show();
+ }
+
+ GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, (() => {
+ if (this._is_disposed) return GLib.SOURCE_REMOVE;
+
+ if (this.show_banner) {
+ this.focused_monitor_index = -1;
+ this.focused_monitor_details = null;
+ } else if (this.imu_snapshots && (!this._smooth_follow_slerping || this.focused_monitor_index === -1)) {
+ // if smooth follow is enabled, use the origin IMU data to inform the initial focused monitor
+ // since it reflects where the user is looking in relation to the original monitor positions
+ const currentPoseQuat = this.smooth_follow_enabled ?
+ this.imu_snapshots.smooth_follow_origin.splice(0, 4) :
+ this.imu_snapshots.imu_data.splice(0, 4);
+
+ const focusedMonitorIndex = findFocusedMonitor(
+ currentPoseQuat,
+ this._monitorsAsNormalizedVectors,
+ this.focused_monitor_index,
+ this.display_distance / this._display_distance_default(),
+ this.smooth_follow_enabled,
+ this._fov_details(),
+ this._sorted_monitors
+ );
+
+ if (this.focused_monitor_index !== focusedMonitorIndex) {
+ Globals.logger.log_debug(`Switching to monitor ${focusedMonitorIndex}`);
+ this.focused_monitor_index = focusedMonitorIndex;
+ this.focused_monitor_details = this._sorted_monitors[focusedMonitorIndex];
+ }
+ }
+
+ return GLib.SOURCE_CONTINUE;
+ }).bind(this));
+
+ this._redraw_timeline = Clutter.Timeline.new_for_actor(global.stage, 1000);
+ this._redraw_timeline.connect('new-frame', (() => {
+ // let's try to cap the forced redraw rate
+ if (this._is_disposed || this._last_redraw !== undefined && Date.now() - this._last_redraw < this._cap_frametime_ms) return;
+
+ Globals.data_stream.refresh_data();
+ this.imu_snapshots = Globals.data_stream.imu_snapshots;
+ this.monitor_actors.forEach(({ monitorClone }) => monitorClone.queue_redraw());
+ this._last_redraw = Date.now();
+ }).bind(this));
+ this._redraw_timeline.set_repeat_count(-1);
+ this._redraw_timeline.start();
+ }
+
+ _display_distance_default() {
+ return Math.max(this.display_distance, this.toggle_display_distance_start, this.toggle_display_distance_end);
+ }
+
+ _fov_details() {
+ const aspect = this.target_monitor.width / this.target_monitor.height;
+ const fovRadians = diagonalToCrossFOVs(degreeToRadian(Globals.data_stream.device_data.displayFov), aspect);
+ const defaultDistanceVerticalRadians = 2 * Math.atan(Math.tan(fovRadians.vertical / 2) / this._display_distance_default());
+ const defaultDistanceHorizontalRadians = 2 * Math.atan(Math.tan(fovRadians.horizontal / 2) / this._display_distance_default());
+
+ // distance needed for the FOV-sized monitor to fill up the screen
+ const fullScreenDistance = this.target_monitor.height / 2 / Math.tan(fovRadians.vertical / 2);
+ const lensDistancePixels = fullScreenDistance / (1.0 - Globals.data_stream.device_data.lensDistanceRatio) - fullScreenDistance;
+
+ // distance of a display at the default (most zoomed out) distance, plus the lens distance constant
+ const lensToScreenDistance = this.target_monitor.height / 2 / Math.tan(defaultDistanceVerticalRadians / 2);
+ const completeScreenDistancePixels = lensToScreenDistance + lensDistancePixels;
+
+ return {
+ widthPixels: this.target_monitor.width,
+ heightPixels: this.target_monitor.height,
+ defaultDistanceVerticalRadians,
+ defaultDistanceHorizontalRadians,
+ lensDistancePixels,
+ completeScreenDistancePixels
+ };
+ }
+
+ _horizontal_monitor_sort() {
+ return [...this._all_monitors].sort((a, b) => {
+ // First compare by y-coordinate to form rows (top to bottom)
+ if (a.y !== b.y) {
+ return a.y - b.y;
+ }
+ // Then compare by x-coordinate within the same row (left to right)
+ return a.x - b.x;
+ });
+ }
+
+ _vertical_monitor_sort() {
+ return [...this._all_monitors].sort((a, b) => {
+ // First compare by x-coordinate to form columns (left to right)
+ if (a.x !== b.x) {
+ return a.x - b.x;
+ }
+ // Then compare by y-coordinate within the same column (top to bottom)
+ return a.y - b.y;
+ });
+ }
+
+ _update_monitor_placements() {
+ // collect minimum and maximum x and y values of monitors
+ let actualWrapScheme = this.monitor_wrapping_scheme;
+ if (actualWrapScheme === 'automatic') {
+ const minX = Math.min(...this._all_monitors.map(monitor => monitor.x));
+ const minY = Math.min(...this._all_monitors.map(monitor => monitor.y));
+ const maxX = Math.max(...this._all_monitors.map(monitor => monitor.x + monitor.width));
+ const maxY = Math.max(...this._all_monitors.map(monitor => monitor.y + monitor.height));
+
+ // check if there are more monitors in the horizontal or vertical direction, prefer horizontal if equal
+ if ((maxX - minX) / this.target_monitor.width >= (maxY - minY) / this.target_monitor.height) {
+ actualWrapScheme = 'horizontal';
+ } else {
+ actualWrapScheme = 'vertical';
+ }
+ }
+
+ // use horizontal in all cases but vertical wrapping
+ this._sorted_monitors = actualWrapScheme === 'vertical' ?
+ this._vertical_monitor_sort() :
+ this._horizontal_monitor_sort();
+
+ const fovDetails = this._fov_details();
+ this.lens_vector = [0.0, 0.0, -fovDetails.lensDistancePixels];
+ this.monitor_placements = monitorsToPlacements(
+ fovDetails,
+
+ // shift all monitors so they center around the target monitor, then adjusted by the offsets
+ this._sorted_monitors.map(monitor => ({
+ x: monitor.x - this.target_monitor.x - this.viewport_offset_x * this.target_monitor.width,
+ y: monitor.y - this.target_monitor.y + this.viewport_offset_y * this.target_monitor.height,
+ width: monitor.width,
+ height: monitor.height
+ })),
+ actualWrapScheme,
+ this.monitor_spacing / 1000.0
+ );
+
+ // normalize the center vectors
+ this._monitorsAsNormalizedVectors = this.monitor_placements.map(monitorVectors => {
+ const vector = monitorVectors.centerLook;
+ const length = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]);
+ return [vector[0] / length, vector[1] / length, vector[2] / length];
+ });
+ }
+
+ _handle_display_distance_properties_change() {
+ const distance_from_end = Math.abs(this.display_distance - this.toggle_display_distance_end);
+ const distance_from_start = Math.abs(this.display_distance - this.toggle_display_distance_start);
+ this._is_display_distance_at_end = distance_from_end < distance_from_start;
+ this._update_monitor_placements();
+ }
+
+ _handle_banner_update() {
+ if (this.show_banner) {
+ this.bannerActor.set_content(this.custom_banner_enabled ? this.customBannerContent : this.bannerContent);
+ this.bannerActor.show();
+ } else {
+ this.bannerActor.hide();
+ }
+ }
+
+ _handle_frame_rate_cap_change() {
+ // add a margin to the cap time so we don't cut off frames that come in close
+ const frametime_margin = 0.75;
+ this._cap_frametime_ms = this.framerate_cap === 0 ? 0.0 : Math.floor(1000 * frametime_margin / this.framerate_cap);
+ }
+
+ _handle_smooth_follow_enabled_change() {
+ if (this._smooth_follow_timeout_id !== undefined) GLib.source_remove(this._smooth_follow_timeout_id);
+
+ this._smooth_follow_slerping = true;
+ this._smooth_follow_timeout_id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, SMOOTH_FOLLOW_SLERP_TIMELINE_MS, (() => {
+ this._smooth_follow_slerping = false;
+ this._smooth_follow_timeout_id = undefined;
+ return GLib.SOURCE_REMOVE;
+ }).bind(this));
+ }
+
+ _change_distance() {
+ this.display_distance = this._is_display_distance_at_end ?
+ this.toggle_display_distance_start : this.toggle_display_distance_end;
+ }
+
+ vfunc_dispose() {
+ Globals.logger.log_debug(`Disposing VirtualMonitorsActor`);
+ this._is_disposed = true;
+
+ if (this._redraw_timeline) {
+ this._redraw_timeline.stop();
+ this._redraw_timeline = null;
+ }
+
+ this.monitor_actors.forEach(({ viewport, containerActor, monitorClone, effect }) => {
+ viewport.remove_effect(effect);
+ containerActor.remove_child(monitorClone);
+ viewport.remove_child(containerActor);
+ this.remove_child(viewport);
+ });
+ this.monitor_actors = [];
+
+ this._property_bindings.forEach(binding => binding.unbind());
+ this._property_bindings = [];
+
+ this._property_connections.forEach(connection => this.disconnect(connection));
+ }
+});
\ No newline at end of file
diff --git a/gnome/src/xrEffect.js b/gnome/src/xrEffect.js
deleted file mode 100644
index 19533f1..0000000
--- a/gnome/src/xrEffect.js
+++ /dev/null
@@ -1,453 +0,0 @@
-import Clutter from 'gi://Clutter';
-import Cogl from 'gi://Cogl';
-import GdkPixbuf from 'gi://GdkPixbuf';
-import GLib from 'gi://GLib';
-import GObject from 'gi://GObject';
-import Shell from 'gi://Shell';
-
-import Globals from './globals.js';
-
-import {
- dataViewEnd,
- dataViewUint8,
- dataViewBigUint,
- dataViewUint32Array,
- dataViewUint8Array,
- dataViewFloat,
- dataViewFloatArray,
- BOOL_SIZE,
- DATA_VIEW_INFO_COUNT_INDEX,
- DATA_VIEW_INFO_OFFSET_INDEX,
- FLOAT_SIZE,
- UINT_SIZE,
- UINT8_SIZE
-} from "./ipc.js";
-import { degreeToRadian } from "./math.js";
-import { getShaderSource } from "./shader.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 = 3;
-
-// DataView info: [offset, size, count]
-const VERSION = [0, UINT8_SIZE, 1];
-const ENABLED = [dataViewEnd(VERSION), BOOL_SIZE, 1];
-const LOOK_AHEAD_CFG = [dataViewEnd(ENABLED), FLOAT_SIZE, 4];
-const DISPLAY_RES = [dataViewEnd(LOOK_AHEAD_CFG), UINT_SIZE, 2];
-const DISPLAY_FOV = [dataViewEnd(DISPLAY_RES), FLOAT_SIZE, 1];
-const LENS_DISTANCE_RATIO = [dataViewEnd(DISPLAY_FOV), FLOAT_SIZE, 1];
-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 IMU_PARITY_BYTE = [dataViewEnd(IMU_QUAT_DATA), UINT8_SIZE, 1];
-const DATA_VIEW_LENGTH = dataViewEnd(IMU_PARITY_BYTE);
-
-// cached after first retrieval
-const shaderUniformLocations = {
- 'virtual_display_enabled': null,
- 'show_banner': null,
- 'imu_quat_data': null,
- 'look_ahead_cfg': null,
- 'look_ahead_ms': null,
- 'trim_percent': null,
- 'display_size': null,
- 'display_north_offset': 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,
- 'custom_banner_enabled': null,
- 'half_fov_z_rads': null,
- 'half_fov_y_rads': null,
- 'fov_half_widths': null,
- 'fov_widths': null,
- 'display_resolution': null,
- 'source_to_display_ratio': null,
- 'curved_display': null,
-
- // only used by the reshade integration, but needs to be set to a default value by this effect
- 'frametime': null,
- 'sideview_enabled': null,
- 'sideview_position': null,
- 'sideview_display_size': null
-};
-
-function setUniformFloat(effect, locationName, dataViewInfo, value) {
- effect.set_uniform_float(shaderUniformLocations[locationName], dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX], value);
-}
-
-function transferUniformFloat(effect, locationName, dataView, dataViewInfo) {
- setUniformFloat(effect, locationName, dataViewInfo, dataViewFloatArray(dataView, dataViewInfo));
-}
-
-function setSingleFloat(effect, locationName, value) {
- effect.set_uniform_float(shaderUniformLocations[locationName], 1, [value]);
-}
-
-function setUniformMatrix(effect, locationName, components, dataView, dataViewInfo) {
- const numValues = dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX];
- if (numValues / components !== components) {
- throw new Error('Invalid matrix size');
- }
-
- const floatArray = [].fill(0, 0, numValues);
- let offset = dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX];
- for (let i = 0; i < numValues; i++) {
- // GLSL uses column-major order, so we need to transpose the matrix
- const row = i % components;
- const column = Math.floor(i / components);
-
- floatArray[row * components + column] = dataView.getFloat32(offset, true);
- offset += FLOAT_SIZE;
- }
- effect.set_uniform_matrix(shaderUniformLocations[locationName], true, components, floatArray);
-}
-
-function lookAheadMS(dataView) {
- const lookAheadCfg = dataViewFloatArray(dataView, LOOK_AHEAD_CFG);
- const imuDateMS = dataViewBigUint(dataView, EPOCH_MS);
-
- // how stale the imu data is
- const dataAge = Date.now() - imuDateMS;
-
- return lookAheadCfg[0] + dataAge;
-}
-
-// most uniforms don't change frequently, this function should be called periodically
-function setIntermittentUniformVariables() {
- try {
- const dataView = this._dataView;
-
- 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 displayFov = dataViewFloat(dataView, DISPLAY_FOV);
-
- // TODO - drive these values from settings
- const sbsContent = false;
- const sbsModeStretched = true;
-
- // 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];
- const lensDistanceRatio = dataViewFloat(dataView, LENS_DISTANCE_RATIO);
- let lensFromCenter = 0.0;
- let texcoordXLimits = [0.0, 1.0];
- let texcoordXLimitsRight = [0.0, 1.0];
- if (sbsEnabled) {
- lensFromCenter = lensDistanceRatio / 3.0;
- 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;
- texcoordXLimitsRight[0] = 0.25;
- texcoordXLimitsRight[1] = 0.75;
- }
- }
- const lensVector = [lensDistanceRatio, lensFromCenter, 0.0];
- const lensVectorRight = [lensDistanceRatio, -lensFromCenter, 0.0];
-
- // 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);
-
- // computed values with no dataViewInfo, so we set these manually
- this.set_uniform_float(shaderUniformLocations['trim_percent'], 2, [trimWidthPercent, 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, 'virtual_display_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);
- setSingleFloat(this, 'frametime', 0.0);
-
- setSingleFloat(this, 'sideview_enabled', 0.0);
- setSingleFloat(this, 'sideview_position', 0.0);
- setSingleFloat(this, 'sideview_display_size', 1.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}`);
- }
- } 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',
- 'Geometry of the target monitor for this effect',
- GObject.ParamFlags.READWRITE
- ),
- 'target-framerate': GObject.ParamSpec.uint(
- 'target-framerate',
- 'Target Framerate',
- 'Target framerate for this effect',
- GObject.ParamFlags.READWRITE, 30, 240, 60
- ),
- 'display-distance': GObject.ParamSpec.double(
- 'display-distance',
- 'Display Distance',
- 'How far away the display appears',
- GObject.ParamFlags.READWRITE,
- 0.2,
- 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',
- 'Start distance when using the "change distance" shortcut.',
- GObject.ParamFlags.READWRITE,
- 0.2,
- 2.5,
- 1.05
- ),
- 'toggle-display-distance-end': GObject.ParamSpec.double(
- 'toggle-display-distance-end',
- 'Display distance end',
- 'End distance when using the "change distance" shortcut.',
- GObject.ParamFlags.READWRITE,
- 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._distance_ease_timeline = null;
- this.connect('notify::toggle-display-distance-start', this._handle_display_distance_properties_change.bind(this));
- this.connect('notify::toggle-display-distance-end', this._handle_display_distance_properties_change.bind(this));
- this.connect('notify::display-distance', this._handle_display_distance_properties_change.bind(this));
- this._handle_display_distance_properties_change();
-
- const calibrating = GdkPixbuf.Pixbuf.new_from_file(`${Globals.extension_dir}/textures/calibrating.png`);
- this.calibratingImage = new Clutter.Image();
- this.calibratingImage.set_data(calibrating.get_pixels(), Cogl.PixelFormat.RGB_888,
- calibrating.width, calibrating.height, calibrating.rowstride);
-
- const customBanner = GdkPixbuf.Pixbuf.new_from_file(`${Globals.extension_dir}/textures/custom_banner.png`);
- this.customBannerImage = new Clutter.Image();
- this.customBannerImage.set_data(customBanner.get_pixels(), Cogl.PixelFormat.RGB_888,
- customBanner.width, customBanner.height, customBanner.rowstride);
- }
-
- _handle_display_distance_properties_change() {
- const distance_from_end = Math.abs(this.display_distance - this.toggle_display_distance_end);
- const distance_from_start = Math.abs(this.display_distance - this.toggle_display_distance_start);
- this._is_display_distance_at_end = distance_from_end < distance_from_start;
- }
-
- _change_distance() {
- if (this._distance_ease_timeline?.is_playing()) this._distance_ease_timeline.stop();
-
- this._distance_ease_start = this.display_distance;
- this._distance_ease_timeline = Clutter.Timeline.new_for_actor(this.get_actor(), 250);
-
- const toggle_display_distance_target = this._is_display_distance_at_end ?
- this.toggle_display_distance_start : this.toggle_display_distance_end;
- this._distance_ease_timeline.connect('new-frame', () => {
- this.display_distance = this._distance_ease_start +
- this._distance_ease_timeline.get_progress() *
- (toggle_display_distance_target - this._distance_ease_start);
- });
-
- this._distance_ease_timeline.start();
- }
-
- vfunc_build_pipeline() {
- const code = getShaderSource(`${Globals.extension_dir}/Sombrero.frag`);
- const main = 'PS_Sombrero(virtual_display_enabled, false, source_to_display_ratio, show_banner, cogl_tex_coord_in[0].xy, cogl_color_out);';
- this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, code, main, false);
- }
-
- vfunc_paint_target(node, paintContext) {
- var calibratingImage = this.calibratingImage;
- var customBannerImage = this.customBannerImage;
- let data = Globals.ipc_file.load_contents(null);
- if (data[0]) {
- let buffer = new Uint8Array(data[1]).buffer;
- this._dataView = new DataView(buffer);
- if (!this._initialized) {
- this.set_uniform_float(this.get_uniform_location('screenTexture'), 1, [0]);
-
- this.get_pipeline().set_layer_texture(1, calibratingImage.get_texture());
- this.get_pipeline().set_layer_texture(2, customBannerImage.get_texture());
- this.get_pipeline().set_uniform_1i(this.get_uniform_location('calibratingTexture'), 1);
- this.get_pipeline().set_uniform_1i(this.get_uniform_location('customBannerTexture'), 2);
-
- for (let key in shaderUniformLocations) {
- shaderUniformLocations[key] = this.get_uniform_location(key);
- }
- this.setIntermittentUniformVariables = setIntermittentUniformVariables.bind(this);
- this.setIntermittentUniformVariables();
-
- 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();
- return GLib.SOURCE_CONTINUE;
- }).bind(this));
-
- this._initialized = true;
- }
-
- 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);
- }
- }
- }
-
- 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
- );
- }
- }
- super.vfunc_paint_target(node, paintContext);
- }
-
- cleanup() {
- if (this._redraw_timeline) {
- this._redraw_timeline.stop();
- this._redraw_timeline = null;
- }
- if (this._uniforms_timeout_id) GLib.source_remove(this._uniforms_timeout_id);
- }
-});
\ No newline at end of file
diff --git a/modules/XRLinuxDriver b/modules/XRLinuxDriver
index 53002af..a66c9ae 160000
--- a/modules/XRLinuxDriver
+++ b/modules/XRLinuxDriver
@@ -1 +1 @@
-Subproject commit 53002afde8406eef7fe309206268ad6dc39e2da6
+Subproject commit a66c9aee82989ea7e98da5a05fde621ffb47af83
diff --git a/ui/bin/package b/ui/bin/package
index d66e731..f41fe2d 100755
--- a/ui/bin/package
+++ b/ui/bin/package
@@ -43,6 +43,7 @@ cp $UI_BUILD_PATH/src/breezydesktop.gresource $PACKAGE_BREEZY_DIR
cp -r po/mo/* $PACKAGE_LOCALE_DIR
cp data/com.xronlinux.BreezyDesktop.gschema.xml $PACKAGE_SCHEMAS_DIR
cp $UI_BUILD_PATH/src/breezydesktop $PACKAGE_BIN_DIR
+cp $UI_BUILD_PATH/src/virtualdisplay $PACKAGE_BIN_DIR
cp $UI_BUILD_PATH/data/com.xronlinux.BreezyDesktop.desktop $PACKAGE_APPS_DIR
mkdir -p $PACKAGE_ICONS_DIR/64x64/apps
diff --git a/ui/data/com.xronlinux.BreezyDesktop.gschema.xml b/ui/data/com.xronlinux.BreezyDesktop.gschema.xml
index b5aafdd..77ebb59 100644
--- a/ui/data/com.xronlinux.BreezyDesktop.gschema.xml
+++ b/ui/data/com.xronlinux.BreezyDesktop.gschema.xml
@@ -91,6 +91,42 @@
The size of the display
+
+
+ 0.0
+
+ Viewport offset x
+
+ How far to offset the viewport from the target monitor in the x direction
+
+
+
+
+ 0.0
+
+ Viewport offset y
+
+ How far to offset the viewport from the target monitor in the y direction
+
+
+
+
+ "automatic"
+
+ Monitor wrapping scheme
+
+ How the monitors are wrapped around the viewport
+
+
+
+
+ 0
+
+ Monitor spacing
+
+ How far apart the monitors are visually (not logically)
+
+
false
@@ -120,7 +156,7 @@
- true
+ false
Headset as primary
@@ -154,6 +190,15 @@
Disable anti-aliasing
+
+
+ true
+
+ Disable physical displays
+
+ Disable physical displays when XR effect is enabled
+
+
false
@@ -172,6 +217,15 @@
Log debug messages
+
+
+ false
+
+ Debug no device
+
+ Debug no device
+
+
""
@@ -180,6 +234,15 @@
Custom monitor product
+
+
+
+ 0.0
+
+ Framerate cap
+
+ Framerate cap
+
diff --git a/ui/modules/PyXRLinuxDriverIPC b/ui/modules/PyXRLinuxDriverIPC
index c3b0dda..b012396 160000
--- a/ui/modules/PyXRLinuxDriverIPC
+++ b/ui/modules/PyXRLinuxDriverIPC
@@ -1 +1 @@
-Subproject commit c3b0ddab302e11e517b57b621ff334faf03173de
+Subproject commit b01239646d7ea6f78a823f591946855b880f0484
diff --git a/ui/po/breezydesktop.pot b/ui/po/breezydesktop.pot
index 9e6475c..9534c35 100644
--- a/ui/po/breezydesktop.pot
+++ b/ui/po/breezydesktop.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-23 11:32-0700\n"
+"POT-Creation-Date: 2025-03-05 12:28-0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -17,16 +17,58 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: src/connecteddevice.py:17
+#: src/connecteddevice.py:23
msgid ""
"Switches your glasses into side-by-side mode and doubles the width of the "
"display."
msgstr ""
-#: src/connecteddevice.py:18
+#: src/connecteddevice.py:24
msgid "This feature is not currently supported for your device."
msgstr ""
+#: src/connecteddevice.py:124
+msgid "Set Focused Display Distance"
+msgstr ""
+
+#: src/connecteddevice.py:125
+msgid "Use a closer value so the display zooms in when you look at it."
+msgstr ""
+
+#: src/connecteddevice.py:132
+msgid "Set All Displays Distance"
+msgstr ""
+
+#: src/connecteddevice.py:133
+msgid "Use a farther value so the displays are zoomed out when you look away."
+msgstr ""
+
+#: src/connecteddevice.py:231
+msgid ""
+"Unable to add virtual displays on this machine. xdg-desktop-portal is "
+"required."
+msgstr ""
+
+#: src/connecteddevice.py:265
+msgid "Focused display"
+msgstr ""
+
+#: src/connecteddevice.py:271
+msgid "All displays"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:41
+msgid "closer"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:45
+msgid "fullscreen"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:48
+msgid "farther"
+msgstr ""
+
#: src/licensedialogcontent.py:63
msgid "Paid Tier Status"
msgstr ""
@@ -35,7 +77,7 @@ msgstr ""
msgid "Feature Availability"
msgstr ""
-#: src/licensefeaturerow.py:15 src/shortcutdialog.py:107
+#: src/licensefeaturerow.py:15 src/shortcutdialog.py:104
msgid "Disabled"
msgstr ""
@@ -164,173 +206,235 @@ msgid "Enables the Breezy Desktop XR effect."
msgstr ""
#: src/gtk/connected-device.ui:55
-msgid "Widescreen mode"
+msgid "Zoom on focus"
msgstr ""
-#: src/gtk/connected-device.ui:66
+#: src/gtk/connected-device.ui:56
+msgid ""
+"Automatically move a display closer when you look at it.\n"
+"Set your preferred focused and unfocused distances in the Adjustments "
+"section.\n"
+" "
+msgstr ""
+
+#: src/gtk/connected-device.ui:69
msgid "Follow mode"
msgstr ""
-#: src/gtk/connected-device.ui:67
-msgid "Keep the virtual display near the center of your view."
+#: src/gtk/connected-device.ui:70
+msgid "Keep the focused display near the center of your view."
msgstr ""
-#: src/gtk/connected-device.ui:77
-msgid "Curved display"
+#: src/gtk/connected-device.ui:80 src/gtk/no-device.ui:34
+msgid "Disable physical displays"
msgstr ""
-#: src/gtk/connected-device.ui:78
-msgid "Switch between flat and curved displays."
+#: src/gtk/connected-device.ui:81 src/gtk/no-device.ui:35
+msgid ""
+"Automatically disable all physical displays when the XR effect is enabled."
msgstr ""
#: src/gtk/connected-device.ui:91
+msgid "Virtual displays"
+msgstr ""
+
+#: src/gtk/connected-device.ui:103
+msgid "Rearrange displays"
+msgstr ""
+
+#: src/gtk/connected-device.ui:115
+msgid "1080p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:116
+msgid "1440p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:140
msgid "Adjustments"
msgstr ""
-#: src/gtk/connected-device.ui:94
-msgid "Display distance"
+#: src/gtk/connected-device.ui:143
+msgid "Display distances"
msgstr ""
-#: src/gtk/connected-device.ui:95
-msgid ""
-"Closer appears larger, further appears smaller. Controls depth when in "
-"widescreen mode."
+#: src/gtk/connected-device.ui:144
+msgid "Set how close you want displays to appear."
msgstr ""
-#: src/gtk/connected-device.ui:123
-msgid "Display size"
-msgstr ""
-
-#: src/gtk/connected-device.ui:124
-msgid ""
-"Combine with display distance to achieve a comfortable level of depth and "
-"size."
-msgstr ""
-
-#: src/gtk/connected-device.ui:152
-msgid "Display toggle distances"
-msgstr ""
-
-#: src/gtk/connected-device.ui:153
-msgid ""
-"Use the buttons to capture the current display distance for use with the "
-"keyboard shortcut."
-msgstr ""
-
-#: src/gtk/connected-device.ui:178
+#: src/gtk/connected-device.ui:199
msgid "Follow threshold"
msgstr ""
-#: src/gtk/connected-device.ui:179
+#: src/gtk/connected-device.ui:200
msgid "How far away you can look before the display follows."
msgstr ""
-#: src/gtk/connected-device.ui:209 src/gtk/connected-device.ui:215
+#: src/gtk/connected-device.ui:223
+msgid "Display angling"
+msgstr ""
+
+#: src/gtk/connected-device.ui:224
+msgid ""
+"When there are multiple displays, choose how they should angle towards you."
+msgstr ""
+
+#: src/gtk/connected-device.ui:238
+msgid "Automatic"
+msgstr ""
+
+#: src/gtk/connected-device.ui:239
+msgid "Side-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:240
+msgid "Top-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:241
+msgid "Flat"
+msgstr ""
+
+#: src/gtk/connected-device.ui:251
+msgid "Display spacing"
+msgstr ""
+
+#: src/gtk/connected-device.ui:252
+msgid "Put empty space between displays, when there are multiple."
+msgstr ""
+
+#: src/gtk/connected-device.ui:282
+msgid "Viewport horizontal offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:283
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport to the left or right."
+msgstr ""
+
+#: src/gtk/connected-device.ui:312
+msgid "Viewport vertical offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:313
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport up or down."
+msgstr ""
+
+#: src/gtk/connected-device.ui:349 src/gtk/connected-device.ui:355
msgid "Keyboard Shortcuts"
msgstr ""
-#: src/gtk/connected-device.ui:218
+#: src/gtk/connected-device.ui:358
msgid "XR Effect on/off shortcut"
msgstr ""
-#: src/gtk/connected-device.ui:219
+#: src/gtk/connected-device.ui:359
msgid ""
"Quickly enable or disable the XR Effect. You may need to enable the effect "
"manually once in order to enable the shortcut."
msgstr ""
-#: src/gtk/connected-device.ui:238 src/gtk/connected-device.ui:267
-#: src/gtk/connected-device.ui:296 src/gtk/connected-device.ui:325
+#: src/gtk/connected-device.ui:378 src/gtk/connected-device.ui:407
+#: src/gtk/connected-device.ui:436 src/gtk/connected-device.ui:465
msgid "Change"
msgstr ""
-#: src/gtk/connected-device.ui:247
+#: src/gtk/connected-device.ui:387
msgid "Re-center display shortcut"
msgstr ""
-#: src/gtk/connected-device.ui:248
+#: src/gtk/connected-device.ui:388
msgid "Pin the virtual display to the current position."
msgstr ""
-#: src/gtk/connected-device.ui:276
-msgid "Display distance shortcut"
+#: src/gtk/connected-device.ui:416
+msgid "Toggle zoom on focus shortcut"
msgstr ""
-#: src/gtk/connected-device.ui:277
-msgid "Quickly toggle between two predefined distances."
+#: src/gtk/connected-device.ui:417
+msgid "Quickly toggle zoom on focus mode."
msgstr ""
-#: src/gtk/connected-device.ui:305
+#: src/gtk/connected-device.ui:445
msgid "Toggle follow mode shortcut"
msgstr ""
-#: src/gtk/connected-device.ui:306
+#: src/gtk/connected-device.ui:446
msgid "Quickly toggle follow mode."
msgstr ""
-#: src/gtk/connected-device.ui:341 src/gtk/connected-device.ui:347
+#: src/gtk/connected-device.ui:481 src/gtk/connected-device.ui:487
msgid "Advanced Settings"
msgstr ""
-#: src/gtk/connected-device.ui:350
+#: src/gtk/connected-device.ui:490
msgid "Find optimal display config"
msgstr ""
-#: src/gtk/connected-device.ui:351
+#: src/gtk/connected-device.ui:491
msgid ""
"Automatically modify the glasses display configuration for maximum "
"resolution and best scaling when plugged in."
msgstr ""
-#: src/gtk/connected-device.ui:361
+#: src/gtk/connected-device.ui:501
msgid "Use highest refresh rate"
msgstr ""
-#: src/gtk/connected-device.ui:362
+#: src/gtk/connected-device.ui:502
msgid "Refresh rate may affect performance, disable this to set it manually."
msgstr ""
-#: src/gtk/connected-device.ui:372
+#: src/gtk/connected-device.ui:512
msgid "Always primary display"
msgstr ""
-#: src/gtk/connected-device.ui:373
+#: src/gtk/connected-device.ui:513
msgid "Automatically set the glasses as the primary display when plugged in."
msgstr ""
-#: src/gtk/connected-device.ui:383
-msgid "Fast SBS mode switching"
-msgstr ""
-
-#: src/gtk/connected-device.ui:384
-msgid ""
-"Switches glasses to SBS mode immediately when plugged in, if widescreen mode "
-"is on. May cause instability."
-msgstr ""
-
-#: src/gtk/connected-device.ui:394
+#: src/gtk/connected-device.ui:523
msgid "Movement look-ahead"
msgstr ""
-#: src/gtk/connected-device.ui:395
+#: src/gtk/connected-device.ui:524
msgid ""
"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."
msgstr ""
-#: src/gtk/connected-device.ui:413
+#: src/gtk/connected-device.ui:542
msgid "Default"
msgstr ""
-#: src/gtk/connected-device.ui:425
+#: src/gtk/connected-device.ui:554
msgid "Text Scaling"
msgstr ""
-#: src/gtk/connected-device.ui:426
+#: src/gtk/connected-device.ui:555
msgid "Scaling text below 1.0 will simulate a higher resolution display"
msgstr ""
+#: src/gtk/connected-device.ui:585
+msgid "Enable multi-tap detection"
+msgstr ""
+
+#: src/gtk/connected-device.ui:586
+msgid "Enables double-tap to recenter and triple-tap to recalibrate."
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:9
+msgid "Show full range"
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:18
+msgid "Done"
+msgstr ""
+
#: src/gtk/failed-verification.ui:13
msgid "Breezy Desktop GNOME invalid setup"
msgstr ""
@@ -375,14 +479,6 @@ msgid ""
"connected."
msgstr ""
-#: src/gtk/no-device.ui:34
-msgid "Start in widescreen mode"
-msgstr ""
-
-#: src/gtk/no-device.ui:35
-msgid "Widescreen mode is not supported for all glasses."
-msgstr ""
-
#: src/gtk/no-driver.ui:13
msgid "No driver running"
msgstr ""
diff --git a/ui/po/de.po b/ui/po/de.po
index 7bec3f2..14d88e9 100644
--- a/ui/po/de.po
+++ b/ui/po/de.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-30 09:36-0700\n"
+"POT-Creation-Date: 2025-03-05 12:28-0800\n"
"PO-Revision-Date: 2024-08-02 20:54-0700\n"
"Last-Translator: \n"
"Language-Team: German \n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: src/connecteddevice.py:17
+#: src/connecteddevice.py:23
msgid ""
"Switches your glasses into side-by-side mode and doubles the width of the "
"display."
@@ -25,10 +25,56 @@ msgstr ""
"Schaltet Ihre Brille in den Side-by-Side-Modus und verdoppelt die Breite des "
"Displays."
-#: src/connecteddevice.py:18
+#: src/connecteddevice.py:24
msgid "This feature is not currently supported for your device."
msgstr "Diese Funktion wird von Ihrem Gerät derzeit nicht unterstützt."
+#: src/connecteddevice.py:124
+#, fuzzy
+msgid "Set Focused Display Distance"
+msgstr "Display-Entfernung"
+
+#: src/connecteddevice.py:125
+msgid "Use a closer value so the display zooms in when you look at it."
+msgstr ""
+
+#: src/connecteddevice.py:132
+#, fuzzy
+msgid "Set All Displays Distance"
+msgstr "Display-Entfernung"
+
+#: src/connecteddevice.py:133
+msgid "Use a farther value so the displays are zoomed out when you look away."
+msgstr ""
+
+#: src/connecteddevice.py:231
+msgid ""
+"Unable to add virtual displays on this machine. xdg-desktop-portal is "
+"required."
+msgstr ""
+
+#: src/connecteddevice.py:265
+#, fuzzy
+msgid "Focused display"
+msgstr "Display-Entfernung"
+
+#: src/connecteddevice.py:271
+#, fuzzy
+msgid "All displays"
+msgstr "Gebogenes Display"
+
+#: src/displaydistancedialogcontent.py:41
+msgid "closer"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:45
+msgid "fullscreen"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:48
+msgid "farther"
+msgstr ""
+
#: src/licensedialogcontent.py:63
msgid "Paid Tier Status"
msgstr "Bezahlter Tarifstatus"
@@ -37,7 +83,7 @@ msgstr "Bezahlter Tarifstatus"
msgid "Feature Availability"
msgstr "Funktionsverfügbarkeit"
-#: src/licensefeaturerow.py:15 src/shortcutdialog.py:107
+#: src/licensefeaturerow.py:15 src/shortcutdialog.py:104
msgid "Disabled"
msgstr "Deaktiviert"
@@ -166,82 +212,138 @@ msgid "Enables the Breezy Desktop XR effect."
msgstr "Aktiviert den Breezy Desktop XR-Effekt."
#: src/gtk/connected-device.ui:55
-msgid "Widescreen mode"
-msgstr "Breitbildmodus"
+msgid "Zoom on focus"
+msgstr ""
-#: src/gtk/connected-device.ui:66
+#: src/gtk/connected-device.ui:56
+msgid ""
+"Automatically move a display closer when you look at it.\n"
+"Set your preferred focused and unfocused distances in the Adjustments "
+"section.\n"
+" "
+msgstr ""
+
+#: src/gtk/connected-device.ui:69
msgid "Follow mode"
msgstr "Folgemodus"
-#: src/gtk/connected-device.ui:67
-msgid "Keep the virtual display near the center of your view."
+#: src/gtk/connected-device.ui:70
+#, fuzzy
+msgid "Keep the focused display near the center of your view."
msgstr "Halten Sie das virtuelle Display Nahe der Mitte Ihres Blickfelds."
-#: src/gtk/connected-device.ui:77
-msgid "Curved display"
-msgstr "Gebogenes Display"
+#: src/gtk/connected-device.ui:80 src/gtk/no-device.ui:34
+msgid "Disable physical displays"
+msgstr ""
-#: src/gtk/connected-device.ui:78
-msgid "Switch between flat and curved displays."
-msgstr "Zwischen flachen und gebogenen Displays wechseln."
+#: src/gtk/connected-device.ui:81 src/gtk/no-device.ui:35
+msgid ""
+"Automatically disable all physical displays when the XR effect is enabled."
+msgstr ""
#: src/gtk/connected-device.ui:91
+#, fuzzy
+msgid "Virtual displays"
+msgstr "Gebogenes Display"
+
+#: src/gtk/connected-device.ui:103
+#, fuzzy
+msgid "Rearrange displays"
+msgstr "Gebogenes Display"
+
+#: src/gtk/connected-device.ui:115
+msgid "1080p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:116
+msgid "1440p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:140
msgid "Adjustments"
msgstr "Einstellungen"
-#: src/gtk/connected-device.ui:94
-msgid "Display distance"
+#: src/gtk/connected-device.ui:143
+#, fuzzy
+msgid "Display distances"
msgstr "Display-Entfernung"
-#: src/gtk/connected-device.ui:95
-msgid ""
-"Closer appears larger, further appears smaller. Controls depth when in "
-"widescreen mode."
+#: src/gtk/connected-device.ui:144
+msgid "Set how close you want displays to appear."
msgstr ""
-"Näher erscheint größer, weiter erscheint kleiner. Steuert die Tiefe, wenn im "
-"Breitbildmodus."
-#: src/gtk/connected-device.ui:123
-msgid "Display size"
-msgstr "Display-Größe"
-
-#: src/gtk/connected-device.ui:124
-msgid ""
-"Combine with display distance to achieve a comfortable level of depth and "
-"size."
-msgstr ""
-"Kombinieren Sie mit der Display-Entfernung, um eine bequeme Ebene der Tiefe "
-"und Größe zu erreichen."
-
-#: src/gtk/connected-device.ui:152
-msgid "Display toggle distances"
-msgstr "Umschalten der Display-Entfernungen"
-
-#: src/gtk/connected-device.ui:153
-msgid ""
-"Use the buttons to capture the current display distance for use with the "
-"keyboard shortcut."
-msgstr ""
-"Verwenden Sie die Schaltflächen, um die aktuelle Display-Entfernung für die "
-"Verwendung mit der Tastenkombination aufzufangen."
-
-#: src/gtk/connected-device.ui:178
+#: src/gtk/connected-device.ui:199
msgid "Follow threshold"
msgstr "Folgemodus-Schwelle"
-#: src/gtk/connected-device.ui:179
+#: src/gtk/connected-device.ui:200
msgid "How far away you can look before the display follows."
msgstr "Wie weit Sie wegschauen können, bevor das Display folgt."
-#: src/gtk/connected-device.ui:209 src/gtk/connected-device.ui:215
+#: src/gtk/connected-device.ui:223
+#, fuzzy
+msgid "Display angling"
+msgstr "Display-Größe"
+
+#: src/gtk/connected-device.ui:224
+msgid ""
+"When there are multiple displays, choose how they should angle towards you."
+msgstr ""
+
+#: src/gtk/connected-device.ui:238
+msgid "Automatic"
+msgstr ""
+
+#: src/gtk/connected-device.ui:239
+msgid "Side-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:240
+msgid "Top-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:241
+msgid "Flat"
+msgstr ""
+
+#: src/gtk/connected-device.ui:251
+#, fuzzy
+msgid "Display spacing"
+msgstr "Display-Größe"
+
+#: src/gtk/connected-device.ui:252
+msgid "Put empty space between displays, when there are multiple."
+msgstr ""
+
+#: src/gtk/connected-device.ui:282
+msgid "Viewport horizontal offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:283
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport to the left or right."
+msgstr ""
+
+#: src/gtk/connected-device.ui:312
+msgid "Viewport vertical offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:313
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport up or down."
+msgstr ""
+
+#: src/gtk/connected-device.ui:349 src/gtk/connected-device.ui:355
msgid "Keyboard Shortcuts"
msgstr "Tastenkombinationen"
-#: src/gtk/connected-device.ui:218
+#: src/gtk/connected-device.ui:358
msgid "XR Effect on/off shortcut"
msgstr "XR-Effekt an/aus Tastenkombination"
-#: src/gtk/connected-device.ui:219
+#: src/gtk/connected-device.ui:359
msgid ""
"Quickly enable or disable the XR Effect. You may need to enable the effect "
"manually once in order to enable the shortcut."
@@ -250,44 +352,46 @@ msgstr ""
"Effekt einmalig manuell aktiviert werden, um die Tastenkombination zu "
"aktivieren."
-#: src/gtk/connected-device.ui:238 src/gtk/connected-device.ui:267
-#: src/gtk/connected-device.ui:296 src/gtk/connected-device.ui:325
+#: src/gtk/connected-device.ui:378 src/gtk/connected-device.ui:407
+#: src/gtk/connected-device.ui:436 src/gtk/connected-device.ui:465
msgid "Change"
msgstr "Ändern"
-#: src/gtk/connected-device.ui:247
+#: src/gtk/connected-device.ui:387
msgid "Re-center display shortcut"
msgstr "Display-Zentrierung-Tastenkombination"
-#: src/gtk/connected-device.ui:248
+#: src/gtk/connected-device.ui:388
msgid "Pin the virtual display to the current position."
msgstr "Heften Sie das virtuelle Display an die aktuelle Position an."
-#: src/gtk/connected-device.ui:276
-msgid "Display distance shortcut"
-msgstr "Display-Entfernung-Tastenkombination"
+#: src/gtk/connected-device.ui:416
+#, fuzzy
+msgid "Toggle zoom on focus shortcut"
+msgstr "Folgemodus-Tastenkombination umschalten"
-#: src/gtk/connected-device.ui:277
-msgid "Quickly toggle between two predefined distances."
-msgstr "Schnell zwischen zwei vordefinierten Entfernungen wechseln."
+#: src/gtk/connected-device.ui:417
+#, fuzzy
+msgid "Quickly toggle zoom on focus mode."
+msgstr "Schnell den Folgemodus wechseln."
-#: src/gtk/connected-device.ui:305
+#: src/gtk/connected-device.ui:445
msgid "Toggle follow mode shortcut"
msgstr "Folgemodus-Tastenkombination umschalten"
-#: src/gtk/connected-device.ui:306
+#: src/gtk/connected-device.ui:446
msgid "Quickly toggle follow mode."
msgstr "Schnell den Folgemodus wechseln."
-#: src/gtk/connected-device.ui:341 src/gtk/connected-device.ui:347
+#: src/gtk/connected-device.ui:481 src/gtk/connected-device.ui:487
msgid "Advanced Settings"
msgstr "Erweiterte Einstellungen"
-#: src/gtk/connected-device.ui:350
+#: src/gtk/connected-device.ui:490
msgid "Find optimal display config"
msgstr "Optimale Display-Konfiguration finden"
-#: src/gtk/connected-device.ui:351
+#: src/gtk/connected-device.ui:491
msgid ""
"Automatically modify the glasses display configuration for maximum "
"resolution and best scaling when plugged in."
@@ -295,43 +399,31 @@ msgstr ""
"Ändern Sie die Display-Konfiguration der Brille automatisch zur maximalen "
"Auflösung und besten Skalierung, wenn sie angeschlossen ist."
-#: src/gtk/connected-device.ui:361
+#: src/gtk/connected-device.ui:501
msgid "Use highest refresh rate"
msgstr "Höchste Bildwiederholrate verwenden"
-#: src/gtk/connected-device.ui:362
+#: src/gtk/connected-device.ui:502
msgid "Refresh rate may affect performance, disable this to set it manually."
msgstr ""
"Die Bildwiederholrate kann die Leistung beeinflussen, deaktivieren Sie dies, "
"um sie manuell festzulegen."
-#: src/gtk/connected-device.ui:372
+#: src/gtk/connected-device.ui:512
msgid "Always primary display"
msgstr "Immer primäres Display"
-#: src/gtk/connected-device.ui:373
+#: src/gtk/connected-device.ui:513
msgid "Automatically set the glasses as the primary display when plugged in."
msgstr ""
"Setzen Sie die Brille automatisch als primäres Display, wenn sie "
"angeschlossen ist."
-#: src/gtk/connected-device.ui:383
-msgid "Fast SBS mode switching"
-msgstr "Schnelles SBS-Modus-Wechseln"
-
-#: src/gtk/connected-device.ui:384
-msgid ""
-"Switches glasses to SBS mode immediately when plugged in, if widescreen mode "
-"is on. May cause instability."
-msgstr ""
-"Schaltet die Brille sofort in den SBS-Modus, wenn sie angeschlossen ist, "
-"wenn der Breitbildmodus aktiviert ist. Kann zu Instabilität führen."
-
-#: src/gtk/connected-device.ui:394
+#: src/gtk/connected-device.ui:523
msgid "Movement look-ahead"
msgstr "Bewegungsvorausschau"
-#: src/gtk/connected-device.ui:395
+#: src/gtk/connected-device.ui:524
msgid ""
"Counteracts input lag by predicting head-tracking position ahead of render "
"time. Stick with default unless virtual display drags behind your head "
@@ -342,18 +434,34 @@ msgstr ""
"es sei denn, das virtuelle Display hängt hinter Ihren Kopfbewegungen "
"hinterher, springt vor oder ist sehr wackelig."
-#: src/gtk/connected-device.ui:413
+#: src/gtk/connected-device.ui:542
msgid "Default"
msgstr "Standard"
-#: src/gtk/connected-device.ui:425
+#: src/gtk/connected-device.ui:554
msgid "Text Scaling"
msgstr "Textskalierung"
-#: src/gtk/connected-device.ui:426
+#: src/gtk/connected-device.ui:555
msgid "Scaling text below 1.0 will simulate a higher resolution display"
msgstr "Text unter 1.0 skalieren simuliert ein höher aufgelöstes Display"
+#: src/gtk/connected-device.ui:585
+msgid "Enable multi-tap detection"
+msgstr ""
+
+#: src/gtk/connected-device.ui:586
+msgid "Enables double-tap to recenter and triple-tap to recalibrate."
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:9
+msgid "Show full range"
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:18
+msgid "Done"
+msgstr ""
+
#: src/gtk/failed-verification.ui:13
msgid "Breezy Desktop GNOME invalid setup"
msgstr "Ungültige Breezy Desktop GNOME-Installation"
@@ -403,14 +511,6 @@ msgstr ""
"Automatisch den Breezy Desktop XR-Effekt aktivieren, wenn unterstützte "
"Gläser verbunden werden"
-#: src/gtk/no-device.ui:34
-msgid "Start in widescreen mode"
-msgstr "Im Breitbildmodus starten"
-
-#: src/gtk/no-device.ui:35
-msgid "Widescreen mode is not supported for all glasses."
-msgstr "Breitbildmodus wird nicht für alle Gläsern unterstützt"
-
#: src/gtk/no-driver.ui:13
msgid "No driver running"
msgstr "Kein Treiber wird ausgeführt"
@@ -522,3 +622,59 @@ msgstr "Zurrücksetzung erwzingen"
#: src/gtk/window.ui:99
msgid "About BreezyDesktop"
msgstr "Über BreezyDesktop"
+
+#~ msgid "Fast SBS mode switching"
+#~ msgstr "Schnelles SBS-Modus-Wechseln"
+
+#~ msgid ""
+#~ "Switches glasses to SBS mode immediately when plugged in, if widescreen "
+#~ "mode is on. May cause instability."
+#~ msgstr ""
+#~ "Schaltet die Brille sofort in den SBS-Modus, wenn sie angeschlossen ist, "
+#~ "wenn der Breitbildmodus aktiviert ist. Kann zu Instabilität führen."
+
+#, fuzzy
+#~ msgid "Set Unfocused Displays Distance"
+#~ msgstr "Display-Entfernung"
+
+#~ msgid "Widescreen mode"
+#~ msgstr "Breitbildmodus"
+
+#~ msgid "Switch between flat and curved displays."
+#~ msgstr "Zwischen flachen und gebogenen Displays wechseln."
+
+#~ msgid ""
+#~ "Closer appears larger, further appears smaller. Controls depth when in "
+#~ "widescreen mode."
+#~ msgstr ""
+#~ "Näher erscheint größer, weiter erscheint kleiner. Steuert die Tiefe, wenn "
+#~ "im Breitbildmodus."
+
+#~ msgid ""
+#~ "Combine with display distance to achieve a comfortable level of depth and "
+#~ "size."
+#~ msgstr ""
+#~ "Kombinieren Sie mit der Display-Entfernung, um eine bequeme Ebene der "
+#~ "Tiefe und Größe zu erreichen."
+
+#~ msgid "Display toggle distances"
+#~ msgstr "Umschalten der Display-Entfernungen"
+
+#~ msgid ""
+#~ "Use the buttons to capture the current display distance for use with the "
+#~ "keyboard shortcut."
+#~ msgstr ""
+#~ "Verwenden Sie die Schaltflächen, um die aktuelle Display-Entfernung für "
+#~ "die Verwendung mit der Tastenkombination aufzufangen."
+
+#~ msgid "Display distance shortcut"
+#~ msgstr "Display-Entfernung-Tastenkombination"
+
+#~ msgid "Quickly toggle between two predefined distances."
+#~ msgstr "Schnell zwischen zwei vordefinierten Entfernungen wechseln."
+
+#~ msgid "Start in widescreen mode"
+#~ msgstr "Im Breitbildmodus starten"
+
+#~ msgid "Widescreen mode is not supported for all glasses."
+#~ msgstr "Breitbildmodus wird nicht für alle Gläsern unterstützt"
diff --git a/ui/po/es.po b/ui/po/es.po
index 1ab354e..b3f84bb 100644
--- a/ui/po/es.po
+++ b/ui/po/es.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-22 13:27-0700\n"
+"POT-Creation-Date: 2025-03-05 12:28-0800\n"
"PO-Revision-Date: 2024-08-02 20:55-0700\n"
"Last-Translator: \n"
"Language-Team: Spanish \n"
@@ -17,17 +17,63 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: src/connecteddevice.py:17
+#: src/connecteddevice.py:23
msgid ""
"Switches your glasses into side-by-side mode and doubles the width of the "
"display."
msgstr ""
"Cambia tus gafas al modo lado a lado y duplica el ancho de la pantalla."
-#: src/connecteddevice.py:18
+#: src/connecteddevice.py:24
msgid "This feature is not currently supported for your device."
msgstr "Esta función no es compatible con tu dispositivo en este momento."
+#: src/connecteddevice.py:124
+#, fuzzy
+msgid "Set Focused Display Distance"
+msgstr "Distancia de la pantalla"
+
+#: src/connecteddevice.py:125
+msgid "Use a closer value so the display zooms in when you look at it."
+msgstr ""
+
+#: src/connecteddevice.py:132
+#, fuzzy
+msgid "Set All Displays Distance"
+msgstr "Distancia de la pantalla"
+
+#: src/connecteddevice.py:133
+msgid "Use a farther value so the displays are zoomed out when you look away."
+msgstr ""
+
+#: src/connecteddevice.py:231
+msgid ""
+"Unable to add virtual displays on this machine. xdg-desktop-portal is "
+"required."
+msgstr ""
+
+#: src/connecteddevice.py:265
+#, fuzzy
+msgid "Focused display"
+msgstr "Distancia de la pantalla"
+
+#: src/connecteddevice.py:271
+#, fuzzy
+msgid "All displays"
+msgstr "Pantalla curvada"
+
+#: src/displaydistancedialogcontent.py:41
+msgid "closer"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:45
+msgid "fullscreen"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:48
+msgid "farther"
+msgstr ""
+
#: src/licensedialogcontent.py:63
msgid "Paid Tier Status"
msgstr "Estado del Nivel de Membresía Pagada"
@@ -36,7 +82,7 @@ msgstr "Estado del Nivel de Membresía Pagada"
msgid "Feature Availability"
msgstr "Disponibilidad de Características"
-#: src/licensefeaturerow.py:15 src/shortcutdialog.py:107
+#: src/licensefeaturerow.py:15 src/shortcutdialog.py:104
msgid "Disabled"
msgstr "Deshabilitado"
@@ -165,127 +211,185 @@ msgid "Enables the Breezy Desktop XR effect."
msgstr "Habilita el efecto XR de Breezy Desktop."
#: src/gtk/connected-device.ui:55
-msgid "Widescreen mode"
-msgstr "Modo pantalla ancha"
+msgid "Zoom on focus"
+msgstr ""
-#: src/gtk/connected-device.ui:66
+#: src/gtk/connected-device.ui:56
+msgid ""
+"Automatically move a display closer when you look at it.\n"
+"Set your preferred focused and unfocused distances in the Adjustments "
+"section.\n"
+" "
+msgstr ""
+
+#: src/gtk/connected-device.ui:69
msgid "Follow mode"
msgstr "Modo de seguimiento"
-#: src/gtk/connected-device.ui:67
-msgid "Keep the virtual display near the center of your view."
+#: src/gtk/connected-device.ui:70
+#, fuzzy
+msgid "Keep the focused display near the center of your view."
msgstr "Mantenga la pantalla virtual cerca del centro de su vista."
-#: src/gtk/connected-device.ui:77
-msgid "Curved display"
-msgstr "Pantalla curvada"
+#: src/gtk/connected-device.ui:80 src/gtk/no-device.ui:34
+msgid "Disable physical displays"
+msgstr ""
-#: src/gtk/connected-device.ui:78
-msgid "Switch between flat and curved displays."
-msgstr "Cambiar entre pantallas planas y curvadas."
+#: src/gtk/connected-device.ui:81 src/gtk/no-device.ui:35
+msgid ""
+"Automatically disable all physical displays when the XR effect is enabled."
+msgstr ""
#: src/gtk/connected-device.ui:91
+#, fuzzy
+msgid "Virtual displays"
+msgstr "Pantalla curvada"
+
+#: src/gtk/connected-device.ui:103
+#, fuzzy
+msgid "Rearrange displays"
+msgstr "Pantalla curvada"
+
+#: src/gtk/connected-device.ui:115
+msgid "1080p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:116
+msgid "1440p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:140
msgid "Adjustments"
msgstr "Ajustes"
-#: src/gtk/connected-device.ui:94
-msgid "Display distance"
+#: src/gtk/connected-device.ui:143
+#, fuzzy
+msgid "Display distances"
msgstr "Distancia de la pantalla"
-#: src/gtk/connected-device.ui:95
-msgid ""
-"Closer appears larger, further appears smaller. Controls depth when in "
-"widescreen mode."
+#: src/gtk/connected-device.ui:144
+msgid "Set how close you want displays to appear."
msgstr ""
-"Más cerca parece más grande, más lejos parece más pequeño. Controla la "
-"profundidad cuando está en modo de pantalla ancha."
-#: src/gtk/connected-device.ui:123
-msgid "Display size"
-msgstr "Tamaño de la pantalla"
-
-#: src/gtk/connected-device.ui:124
-msgid ""
-"Combine with display distance to achieve a comfortable level of depth and "
-"size."
-msgstr ""
-"Combínalo con la distancia de la pantalla para lograr un nivel cómodo de "
-"profundidad y tamaño."
-
-#: src/gtk/connected-device.ui:152
-msgid "Display toggle distances"
-msgstr "Distancias de alternancia de pantalla"
-
-#: src/gtk/connected-device.ui:153
-msgid ""
-"Use the buttons to capture the current display distance for use with the "
-"keyboard shortcut."
-msgstr ""
-"Utilice los botones para capturar la distancia actual de la pantalla para su "
-"uso con el método abreviado de teclado."
-
-#: src/gtk/connected-device.ui:178
+#: src/gtk/connected-device.ui:199
msgid "Follow threshold"
msgstr "Umbral de seguimiento"
-#: src/gtk/connected-device.ui:179
+#: src/gtk/connected-device.ui:200
msgid "How far away you can look before the display follows."
msgstr "Hasta qué punto puedes mirar antes de que la pantalla siga."
-#: src/gtk/connected-device.ui:209 src/gtk/connected-device.ui:215
+#: src/gtk/connected-device.ui:223
+#, fuzzy
+msgid "Display angling"
+msgstr "Tamaño de la pantalla"
+
+#: src/gtk/connected-device.ui:224
+msgid ""
+"When there are multiple displays, choose how they should angle towards you."
+msgstr ""
+
+#: src/gtk/connected-device.ui:238
+msgid "Automatic"
+msgstr ""
+
+#: src/gtk/connected-device.ui:239
+msgid "Side-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:240
+msgid "Top-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:241
+msgid "Flat"
+msgstr ""
+
+#: src/gtk/connected-device.ui:251
+#, fuzzy
+msgid "Display spacing"
+msgstr "Tamaño de la pantalla"
+
+#: src/gtk/connected-device.ui:252
+msgid "Put empty space between displays, when there are multiple."
+msgstr ""
+
+#: src/gtk/connected-device.ui:282
+msgid "Viewport horizontal offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:283
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport to the left or right."
+msgstr ""
+
+#: src/gtk/connected-device.ui:312
+msgid "Viewport vertical offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:313
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport up or down."
+msgstr ""
+
+#: src/gtk/connected-device.ui:349 src/gtk/connected-device.ui:355
msgid "Keyboard Shortcuts"
msgstr "Atajos de teclado"
-#: src/gtk/connected-device.ui:218
+#: src/gtk/connected-device.ui:358
msgid "XR Effect on/off shortcut"
msgstr "Atajo encendido/apagado Efecto XR"
-#: src/gtk/connected-device.ui:219
+#: src/gtk/connected-device.ui:359
msgid ""
"Quickly enable or disable the XR Effect. You may need to enable the effect "
"manually once in order to enable the shortcut."
msgstr ""
-"Activa o desactiva rápidamente el Efecto XR. Es posible que necesites activar "
-"el efecto manualmente una vez para habilitar el atajo."
+"Activa o desactiva rápidamente el Efecto XR. Es posible que necesites "
+"activar el efecto manualmente una vez para habilitar el atajo."
-#: src/gtk/connected-device.ui:238 src/gtk/connected-device.ui:267
-#: src/gtk/connected-device.ui:296 src/gtk/connected-device.ui:325
+#: src/gtk/connected-device.ui:378 src/gtk/connected-device.ui:407
+#: src/gtk/connected-device.ui:436 src/gtk/connected-device.ui:465
msgid "Change"
msgstr "Cambiar"
-#: src/gtk/connected-device.ui:247
+#: src/gtk/connected-device.ui:387
msgid "Re-center display shortcut"
msgstr "Atajo para recentrar la pantalla"
-#: src/gtk/connected-device.ui:248
+#: src/gtk/connected-device.ui:388
msgid "Pin the virtual display to the current position."
msgstr "Fije la pantalla virtual en la posición actual."
-#: src/gtk/connected-device.ui:276
-msgid "Display distance shortcut"
-msgstr "Atajo de distancia de pantalla"
+#: src/gtk/connected-device.ui:416
+#, fuzzy
+msgid "Toggle zoom on focus shortcut"
+msgstr "Atajo para alternar el modo de seguimiento"
-#: src/gtk/connected-device.ui:277
-msgid "Quickly toggle between two predefined distances."
-msgstr "Cambiar rápidamente entre dos distancias predefinidas."
+#: src/gtk/connected-device.ui:417
+#, fuzzy
+msgid "Quickly toggle zoom on focus mode."
+msgstr "Activar el modo de seguimiento."
-#: src/gtk/connected-device.ui:305
+#: src/gtk/connected-device.ui:445
msgid "Toggle follow mode shortcut"
msgstr "Atajo para alternar el modo de seguimiento"
-#: src/gtk/connected-device.ui:306
+#: src/gtk/connected-device.ui:446
msgid "Quickly toggle follow mode."
msgstr "Activar el modo de seguimiento."
-#: src/gtk/connected-device.ui:341 src/gtk/connected-device.ui:347
+#: src/gtk/connected-device.ui:481 src/gtk/connected-device.ui:487
msgid "Advanced Settings"
msgstr "Configuración Avanzada"
-#: src/gtk/connected-device.ui:350
+#: src/gtk/connected-device.ui:490
msgid "Find optimal display config"
msgstr "Encuentre la configuración de pantalla óptima"
-#: src/gtk/connected-device.ui:351
+#: src/gtk/connected-device.ui:491
msgid ""
"Automatically modify the glasses display configuration for maximum "
"resolution and best scaling when plugged in."
@@ -294,43 +398,30 @@ msgstr ""
"obtener la máxima resolución y la mejor escalabilidad cuando estén "
"enchufadas."
-#: src/gtk/connected-device.ui:361
+#: src/gtk/connected-device.ui:501
msgid "Use highest refresh rate"
msgstr "Utilizar la frecuencia de actualización más alta"
-#: src/gtk/connected-device.ui:362
+#: src/gtk/connected-device.ui:502
msgid "Refresh rate may affect performance, disable this to set it manually."
msgstr ""
"La frecuencia de actualización puede afectar el rendimiento, deshabilite "
"esto para configurarlo manualmente."
-#: src/gtk/connected-device.ui:372
+#: src/gtk/connected-device.ui:512
msgid "Always primary display"
msgstr "Siempre como pantalla principal"
-#: src/gtk/connected-device.ui:373
+#: src/gtk/connected-device.ui:513
msgid "Automatically set the glasses as the primary display when plugged in."
msgstr ""
"Automáticamente configurar las gafas como pantalla principal al enchufarse."
-#: src/gtk/connected-device.ui:383
-msgid "Fast SBS mode switching"
-msgstr "Cambio rápido de modo SBS"
-
-#: src/gtk/connected-device.ui:384
-msgid ""
-"Switches glasses to SBS mode immediately when plugged in, if widescreen mode "
-"is on. May cause instability."
-msgstr ""
-"Cambia las gafas al modo SBS (lado a lado) inmediatamente cuando se "
-"enchufan, si el modo de pantalla ancha está activado. Puede causar "
-"inestabilidad."
-
-#: src/gtk/connected-device.ui:394
+#: src/gtk/connected-device.ui:523
msgid "Movement look-ahead"
msgstr "Anticipación de movimiento"
-#: src/gtk/connected-device.ui:395
+#: src/gtk/connected-device.ui:524
msgid ""
"Counteracts input lag by predicting head-tracking position ahead of render "
"time. Stick with default unless virtual display drags behind your head "
@@ -341,17 +432,35 @@ msgstr ""
"predeterminado a menos que la pantalla virtual se retrase detrás de los "
"movimientos de la cabeza, se adelante o sea muy inestable."
-#: src/gtk/connected-device.ui:413
+#: src/gtk/connected-device.ui:542
msgid "Default"
msgstr "Predeterminado"
-#: src/gtk/connected-device.ui:425
+#: src/gtk/connected-device.ui:554
msgid "Text Scaling"
msgstr "Escalado de Texto"
-#: src/gtk/connected-device.ui:426
+#: src/gtk/connected-device.ui:555
msgid "Scaling text below 1.0 will simulate a higher resolution display"
-msgstr "Escalando el texto por debajo de 1.0 simulará una pantalla de mayor resolución"
+msgstr ""
+"Escalando el texto por debajo de 1.0 simulará una pantalla de mayor "
+"resolución"
+
+#: src/gtk/connected-device.ui:585
+msgid "Enable multi-tap detection"
+msgstr ""
+
+#: src/gtk/connected-device.ui:586
+msgid "Enables double-tap to recenter and triple-tap to recalibrate."
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:9
+msgid "Show full range"
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:18
+msgid "Done"
+msgstr ""
#: src/gtk/failed-verification.ui:13
msgid "Breezy Desktop GNOME invalid setup"
@@ -398,16 +507,8 @@ msgid ""
"Automatically enable the Breezy Desktop XR effect when supported glasses are "
"connected."
msgstr ""
-"Habilita automáticamente el efecto Breezy Desktop XR cuando se conectan gafas "
-"compatibles."
-
-#: src/gtk/no-device.ui:34
-msgid "Start in widescreen mode"
-msgstr "Empezar en modo pantalla ancha"
-
-#: src/gtk/no-device.ui:35
-msgid "Widescreen mode is not supported for all glasses."
-msgstr "El modo pantalla ancha no está soportado en todas las gafas."
+"Habilita automáticamente el efecto Breezy Desktop XR cuando se conectan "
+"gafas compatibles."
#: src/gtk/no-driver.ui:13
msgid "No driver running"
@@ -518,3 +619,60 @@ msgstr "Reinicio forzoso"
#: src/gtk/window.ui:99
msgid "About BreezyDesktop"
msgstr "Acerca de BreezyDesktop"
+
+#~ msgid "Fast SBS mode switching"
+#~ msgstr "Cambio rápido de modo SBS"
+
+#~ msgid ""
+#~ "Switches glasses to SBS mode immediately when plugged in, if widescreen "
+#~ "mode is on. May cause instability."
+#~ msgstr ""
+#~ "Cambia las gafas al modo SBS (lado a lado) inmediatamente cuando se "
+#~ "enchufan, si el modo de pantalla ancha está activado. Puede causar "
+#~ "inestabilidad."
+
+#, fuzzy
+#~ msgid "Set Unfocused Displays Distance"
+#~ msgstr "Distancia de la pantalla"
+
+#~ msgid "Widescreen mode"
+#~ msgstr "Modo pantalla ancha"
+
+#~ msgid "Switch between flat and curved displays."
+#~ msgstr "Cambiar entre pantallas planas y curvadas."
+
+#~ msgid ""
+#~ "Closer appears larger, further appears smaller. Controls depth when in "
+#~ "widescreen mode."
+#~ msgstr ""
+#~ "Más cerca parece más grande, más lejos parece más pequeño. Controla la "
+#~ "profundidad cuando está en modo de pantalla ancha."
+
+#~ msgid ""
+#~ "Combine with display distance to achieve a comfortable level of depth and "
+#~ "size."
+#~ msgstr ""
+#~ "Combínalo con la distancia de la pantalla para lograr un nivel cómodo de "
+#~ "profundidad y tamaño."
+
+#~ msgid "Display toggle distances"
+#~ msgstr "Distancias de alternancia de pantalla"
+
+#~ msgid ""
+#~ "Use the buttons to capture the current display distance for use with the "
+#~ "keyboard shortcut."
+#~ msgstr ""
+#~ "Utilice los botones para capturar la distancia actual de la pantalla para "
+#~ "su uso con el método abreviado de teclado."
+
+#~ msgid "Display distance shortcut"
+#~ msgstr "Atajo de distancia de pantalla"
+
+#~ msgid "Quickly toggle between two predefined distances."
+#~ msgstr "Cambiar rápidamente entre dos distancias predefinidas."
+
+#~ msgid "Start in widescreen mode"
+#~ msgstr "Empezar en modo pantalla ancha"
+
+#~ msgid "Widescreen mode is not supported for all glasses."
+#~ msgstr "El modo pantalla ancha no está soportado en todas las gafas."
diff --git a/ui/po/fr.po b/ui/po/fr.po
index b743075..102df1c 100644
--- a/ui/po/fr.po
+++ b/ui/po/fr.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-22 13:27-0700\n"
+"POT-Creation-Date: 2025-03-05 12:28-0800\n"
"PO-Revision-Date: 2024-08-02 20:54-0700\n"
"Last-Translator: \n"
"Language-Team: French \n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
-#: src/connecteddevice.py:17
+#: src/connecteddevice.py:23
msgid ""
"Switches your glasses into side-by-side mode and doubles the width of the "
"display."
@@ -25,12 +25,58 @@ msgstr ""
"Basculez vos lunettes en mode Side-By-Side (Côte-à-Côte) et doublez la "
"largeur de l'écran."
-#: src/connecteddevice.py:18
+#: src/connecteddevice.py:24
msgid "This feature is not currently supported for your device."
msgstr ""
"Cette fonctionnalité n'est actuellement pas prise en charge par votre "
"appareil."
+#: src/connecteddevice.py:124
+#, fuzzy
+msgid "Set Focused Display Distance"
+msgstr "Distance d'affichage"
+
+#: src/connecteddevice.py:125
+msgid "Use a closer value so the display zooms in when you look at it."
+msgstr ""
+
+#: src/connecteddevice.py:132
+#, fuzzy
+msgid "Set All Displays Distance"
+msgstr "Distance d'affichage"
+
+#: src/connecteddevice.py:133
+msgid "Use a farther value so the displays are zoomed out when you look away."
+msgstr ""
+
+#: src/connecteddevice.py:231
+msgid ""
+"Unable to add virtual displays on this machine. xdg-desktop-portal is "
+"required."
+msgstr ""
+
+#: src/connecteddevice.py:265
+#, fuzzy
+msgid "Focused display"
+msgstr "Distance d'affichage"
+
+#: src/connecteddevice.py:271
+#, fuzzy
+msgid "All displays"
+msgstr "Affichage incurvé"
+
+#: src/displaydistancedialogcontent.py:41
+msgid "closer"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:45
+msgid "fullscreen"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:48
+msgid "farther"
+msgstr ""
+
#: src/licensedialogcontent.py:63
msgid "Paid Tier Status"
msgstr "Statut de l'abonnement payant"
@@ -39,7 +85,7 @@ msgstr "Statut de l'abonnement payant"
msgid "Feature Availability"
msgstr "Disponibilité des fonctionnalités"
-#: src/licensefeaturerow.py:15 src/shortcutdialog.py:107
+#: src/licensefeaturerow.py:15 src/shortcutdialog.py:104
msgid "Disabled"
msgstr "Désactivé"
@@ -168,126 +214,185 @@ msgid "Enables the Breezy Desktop XR effect."
msgstr "Active l'effet Breezy Desktop XR."
#: src/gtk/connected-device.ui:55
-msgid "Widescreen mode"
-msgstr "Mode grand écran"
+msgid "Zoom on focus"
+msgstr ""
-#: src/gtk/connected-device.ui:66
+#: src/gtk/connected-device.ui:56
+msgid ""
+"Automatically move a display closer when you look at it.\n"
+"Set your preferred focused and unfocused distances in the Adjustments "
+"section.\n"
+" "
+msgstr ""
+
+#: src/gtk/connected-device.ui:69
msgid "Follow mode"
msgstr "Mode de suivi"
-#: src/gtk/connected-device.ui:67
-msgid "Keep the virtual display near the center of your view."
+#: src/gtk/connected-device.ui:70
+#, fuzzy
+msgid "Keep the focused display near the center of your view."
msgstr "Gardez l'affichage virtuel près du centre de votre champ de vision."
-#: src/gtk/connected-device.ui:77
-msgid "Curved display"
-msgstr "Affichage incurvé"
+#: src/gtk/connected-device.ui:80 src/gtk/no-device.ui:34
+msgid "Disable physical displays"
+msgstr ""
-#: src/gtk/connected-device.ui:78
-msgid "Switch between flat and curved displays."
-msgstr "Basculez entre les affichages plats et incurvés."
+#: src/gtk/connected-device.ui:81 src/gtk/no-device.ui:35
+msgid ""
+"Automatically disable all physical displays when the XR effect is enabled."
+msgstr ""
#: src/gtk/connected-device.ui:91
+#, fuzzy
+msgid "Virtual displays"
+msgstr "Affichage incurvé"
+
+#: src/gtk/connected-device.ui:103
+#, fuzzy
+msgid "Rearrange displays"
+msgstr "Affichage incurvé"
+
+#: src/gtk/connected-device.ui:115
+msgid "1080p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:116
+msgid "1440p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:140
msgid "Adjustments"
msgstr "Réglages"
-#: src/gtk/connected-device.ui:94
-msgid "Display distance"
+#: src/gtk/connected-device.ui:143
+#, fuzzy
+msgid "Display distances"
msgstr "Distance d'affichage"
-#: src/gtk/connected-device.ui:95
-msgid ""
-"Closer appears larger, further appears smaller. Controls depth when in "
-"widescreen mode."
+#: src/gtk/connected-device.ui:144
+msgid "Set how close you want displays to appear."
msgstr ""
-"Plus proche apparaît plus grand, plus éloigné apparaît plus petit. Contrôle "
-"la profondeur lorsque vous êtes en mode grand écran."
-#: src/gtk/connected-device.ui:123
-msgid "Display size"
-msgstr "Taille de l'affichage"
-
-#: src/gtk/connected-device.ui:124
-msgid ""
-"Combine with display distance to achieve a comfortable level of depth and "
-"size."
-msgstr ""
-"Combinez avec la distance d'affichage pour obtenir un niveau de profondeur "
-"et de taille confortable."
-
-#: src/gtk/connected-device.ui:152
-msgid "Display toggle distances"
-msgstr "Distances de basculement de l'affichage"
-
-#: src/gtk/connected-device.ui:153
-msgid ""
-"Use the buttons to capture the current display distance for use with the "
-"keyboard shortcut."
-msgstr ""
-"Utilisez les boutons pour capturer la distance d'affichage actuelle pour une "
-"utilisation avec le raccourci clavier."
-
-#: src/gtk/connected-device.ui:178
+#: src/gtk/connected-device.ui:199
msgid "Follow threshold"
msgstr "Seuil de suivi"
-#: src/gtk/connected-device.ui:179
+#: src/gtk/connected-device.ui:200
msgid "How far away you can look before the display follows."
msgstr "Distance jusqu'où vous pouvez regarder avant que l'affichage ne suive."
-#: src/gtk/connected-device.ui:209 src/gtk/connected-device.ui:215
+#: src/gtk/connected-device.ui:223
+#, fuzzy
+msgid "Display angling"
+msgstr "Taille de l'affichage"
+
+#: src/gtk/connected-device.ui:224
+msgid ""
+"When there are multiple displays, choose how they should angle towards you."
+msgstr ""
+
+#: src/gtk/connected-device.ui:238
+msgid "Automatic"
+msgstr ""
+
+#: src/gtk/connected-device.ui:239
+msgid "Side-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:240
+msgid "Top-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:241
+msgid "Flat"
+msgstr ""
+
+#: src/gtk/connected-device.ui:251
+#, fuzzy
+msgid "Display spacing"
+msgstr "Taille de l'affichage"
+
+#: src/gtk/connected-device.ui:252
+msgid "Put empty space between displays, when there are multiple."
+msgstr ""
+
+#: src/gtk/connected-device.ui:282
+msgid "Viewport horizontal offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:283
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport to the left or right."
+msgstr ""
+
+#: src/gtk/connected-device.ui:312
+msgid "Viewport vertical offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:313
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport up or down."
+msgstr ""
+
+#: src/gtk/connected-device.ui:349 src/gtk/connected-device.ui:355
msgid "Keyboard Shortcuts"
msgstr "Raccourcis clavier"
-#: src/gtk/connected-device.ui:218
+#: src/gtk/connected-device.ui:358
msgid "XR Effect on/off shortcut"
msgstr "Raccourci on/off de l'effet XR"
-#: src/gtk/connected-device.ui:219
+#: src/gtk/connected-device.ui:359
msgid ""
"Quickly enable or disable the XR Effect. You may need to enable the effect "
"manually once in order to enable the shortcut."
-msgstr "Activez ou désactivez rapidement l'effet XR. Il se peut que vous deviez "
+msgstr ""
+"Activez ou désactivez rapidement l'effet XR. Il se peut que vous deviez "
"activer l'effet manuellement une fois pour activer le raccourci."
-#: src/gtk/connected-device.ui:238 src/gtk/connected-device.ui:267
-#: src/gtk/connected-device.ui:296 src/gtk/connected-device.ui:325
+#: src/gtk/connected-device.ui:378 src/gtk/connected-device.ui:407
+#: src/gtk/connected-device.ui:436 src/gtk/connected-device.ui:465
msgid "Change"
msgstr "Changer"
-#: src/gtk/connected-device.ui:247
+#: src/gtk/connected-device.ui:387
msgid "Re-center display shortcut"
msgstr "Raccourci de recentrage de l'affichage"
-#: src/gtk/connected-device.ui:248
+#: src/gtk/connected-device.ui:388
msgid "Pin the virtual display to the current position."
msgstr "Fixez l'affichage virtuel à la position actuelle."
-#: src/gtk/connected-device.ui:276
-msgid "Display distance shortcut"
-msgstr "Raccourci de distance d'affichage"
+#: src/gtk/connected-device.ui:416
+#, fuzzy
+msgid "Toggle zoom on focus shortcut"
+msgstr "Raccourci de basculement du mode de suivi"
-#: src/gtk/connected-device.ui:277
-msgid "Quickly toggle between two predefined distances."
-msgstr "Basculez rapidement entre deux distances prédéfinies."
+#: src/gtk/connected-device.ui:417
+#, fuzzy
+msgid "Quickly toggle zoom on focus mode."
+msgstr "Basculer rapidement le mode de suivi."
-#: src/gtk/connected-device.ui:305
+#: src/gtk/connected-device.ui:445
msgid "Toggle follow mode shortcut"
msgstr "Raccourci de basculement du mode de suivi"
-#: src/gtk/connected-device.ui:306
+#: src/gtk/connected-device.ui:446
msgid "Quickly toggle follow mode."
msgstr "Basculer rapidement le mode de suivi."
-#: src/gtk/connected-device.ui:341 src/gtk/connected-device.ui:347
+#: src/gtk/connected-device.ui:481 src/gtk/connected-device.ui:487
msgid "Advanced Settings"
msgstr "Paramètres avancés"
-#: src/gtk/connected-device.ui:350
+#: src/gtk/connected-device.ui:490
msgid "Find optimal display config"
msgstr "Trouver la configuration d'affichage optimale"
-#: src/gtk/connected-device.ui:351
+#: src/gtk/connected-device.ui:491
msgid ""
"Automatically modify the glasses display configuration for maximum "
"resolution and best scaling when plugged in."
@@ -296,43 +401,31 @@ msgstr ""
"résolution maximale et une meilleure mise à l'échelle lorsque elles sont "
"branchées."
-#: src/gtk/connected-device.ui:361
+#: src/gtk/connected-device.ui:501
msgid "Use highest refresh rate"
msgstr "Utiliser le taux de rafraîchissement le plus élevé"
-#: src/gtk/connected-device.ui:362
+#: src/gtk/connected-device.ui:502
msgid "Refresh rate may affect performance, disable this to set it manually."
msgstr ""
"Un taux de rafraîchissement élevé peut affecter les performances, désactivez-"
"le pour le définir manuellement."
-#: src/gtk/connected-device.ui:372
+#: src/gtk/connected-device.ui:512
msgid "Always primary display"
msgstr "Affichage principal en permanence"
-#: src/gtk/connected-device.ui:373
+#: src/gtk/connected-device.ui:513
msgid "Automatically set the glasses as the primary display when plugged in."
msgstr ""
"Définissez automatiquement les lunettes comme affichage principal "
"lorsqu'elles sont branchées."
-#: src/gtk/connected-device.ui:383
-msgid "Fast SBS mode switching"
-msgstr "Bascule rapide vers le mode SBS"
-
-#: src/gtk/connected-device.ui:384
-msgid ""
-"Switches glasses to SBS mode immediately when plugged in, if widescreen mode "
-"is on. May cause instability."
-msgstr ""
-"Bascule les lunettes vers le mode SBS immédiatement lorsqu'elles sont "
-"branchées si le mode grand écran est activé. Peut provoquer des instabilités."
-
-#: src/gtk/connected-device.ui:394
+#: src/gtk/connected-device.ui:523
msgid "Movement look-ahead"
msgstr "Prédiction de mouvement"
-#: src/gtk/connected-device.ui:395
+#: src/gtk/connected-device.ui:524
msgid ""
"Counteracts input lag by predicting head-tracking position ahead of render "
"time. Stick with default unless virtual display drags behind your head "
@@ -342,20 +435,36 @@ msgstr ""
"le temps de rendu. Restez sur la valeur par défaut à moins que l'affichage "
"virtuel ne soit lent, ne saute pas ou ne soit très instable."
-#: src/gtk/connected-device.ui:413
+#: src/gtk/connected-device.ui:542
msgid "Default"
msgstr "Par défaut"
-#: src/gtk/connected-device.ui:425
+#: src/gtk/connected-device.ui:554
msgid "Text Scaling"
msgstr "Mise à l'échelle du texte"
-#: src/gtk/connected-device.ui:426
+#: src/gtk/connected-device.ui:555
msgid "Scaling text below 1.0 will simulate a higher resolution display"
msgstr ""
"Une mise à l'échelle du texte en dessous de 1.0 simulera un affichage de "
"plus haute résolution"
+#: src/gtk/connected-device.ui:585
+msgid "Enable multi-tap detection"
+msgstr ""
+
+#: src/gtk/connected-device.ui:586
+msgid "Enables double-tap to recenter and triple-tap to recalibrate."
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:9
+msgid "Show full range"
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:18
+msgid "Done"
+msgstr ""
+
#: src/gtk/failed-verification.ui:13
msgid "Breezy Desktop GNOME invalid setup"
msgstr "Configuration Breezy Desktop GNOME non valide"
@@ -401,17 +510,10 @@ msgstr "Activation automatique de l'effet XR"
msgid ""
"Automatically enable the Breezy Desktop XR effect when supported glasses are "
"connected."
-msgstr "Activer automatiquement l'effet Breezy Desktop XR lorsque des lunettes "
+msgstr ""
+"Activer automatiquement l'effet Breezy Desktop XR lorsque des lunettes "
"compatibles sont connectées."
-#: src/gtk/no-device.ui:34
-msgid "Start in widescreen mode"
-msgstr "Démarer en mode grand écran"
-
-#: src/gtk/no-device.ui:35
-msgid "Widescreen mode is not supported for all glasses."
-msgstr "Le mode écran large n'est pas compatible avec toutes les lunettes."
-
#: src/gtk/no-driver.ui:13
msgid "No driver running"
msgstr "Aucun pilote en cours d'exécution"
@@ -521,3 +623,60 @@ msgstr "Réinitialiser"
#: src/gtk/window.ui:99
msgid "About BreezyDesktop"
msgstr "À propos de BreezyDesktop"
+
+#~ msgid "Fast SBS mode switching"
+#~ msgstr "Bascule rapide vers le mode SBS"
+
+#~ msgid ""
+#~ "Switches glasses to SBS mode immediately when plugged in, if widescreen "
+#~ "mode is on. May cause instability."
+#~ msgstr ""
+#~ "Bascule les lunettes vers le mode SBS immédiatement lorsqu'elles sont "
+#~ "branchées si le mode grand écran est activé. Peut provoquer des "
+#~ "instabilités."
+
+#, fuzzy
+#~ msgid "Set Unfocused Displays Distance"
+#~ msgstr "Distance d'affichage"
+
+#~ msgid "Widescreen mode"
+#~ msgstr "Mode grand écran"
+
+#~ msgid "Switch between flat and curved displays."
+#~ msgstr "Basculez entre les affichages plats et incurvés."
+
+#~ msgid ""
+#~ "Closer appears larger, further appears smaller. Controls depth when in "
+#~ "widescreen mode."
+#~ msgstr ""
+#~ "Plus proche apparaît plus grand, plus éloigné apparaît plus petit. "
+#~ "Contrôle la profondeur lorsque vous êtes en mode grand écran."
+
+#~ msgid ""
+#~ "Combine with display distance to achieve a comfortable level of depth and "
+#~ "size."
+#~ msgstr ""
+#~ "Combinez avec la distance d'affichage pour obtenir un niveau de "
+#~ "profondeur et de taille confortable."
+
+#~ msgid "Display toggle distances"
+#~ msgstr "Distances de basculement de l'affichage"
+
+#~ msgid ""
+#~ "Use the buttons to capture the current display distance for use with the "
+#~ "keyboard shortcut."
+#~ msgstr ""
+#~ "Utilisez les boutons pour capturer la distance d'affichage actuelle pour "
+#~ "une utilisation avec le raccourci clavier."
+
+#~ msgid "Display distance shortcut"
+#~ msgstr "Raccourci de distance d'affichage"
+
+#~ msgid "Quickly toggle between two predefined distances."
+#~ msgstr "Basculez rapidement entre deux distances prédéfinies."
+
+#~ msgid "Start in widescreen mode"
+#~ msgstr "Démarer en mode grand écran"
+
+#~ msgid "Widescreen mode is not supported for all glasses."
+#~ msgstr "Le mode écran large n'est pas compatible avec toutes les lunettes."
diff --git a/ui/po/it.po b/ui/po/it.po
index a52e46a..39c70d1 100644
--- a/ui/po/it.po
+++ b/ui/po/it.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-22 13:27-0700\n"
+"POT-Creation-Date: 2025-03-05 12:28-0800\n"
"PO-Revision-Date: 2024-08-02 21:14-0700\n"
"Last-Translator: \n"
"Language-Team: Italian \n"
@@ -17,17 +17,64 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: src/connecteddevice.py:17
+#: src/connecteddevice.py:23
msgid ""
"Switches your glasses into side-by-side mode and doubles the width of the "
"display."
msgstr ""
-"Passa i tuoi occhiali in modalità side-by-side e raddoppia la larghezza del display."
+"Passa i tuoi occhiali in modalità side-by-side e raddoppia la larghezza del "
+"display."
-#: src/connecteddevice.py:18
+#: src/connecteddevice.py:24
msgid "This feature is not currently supported for your device."
msgstr "Questa funzione non è attualmente supportata sul tuo dispositivo."
+#: src/connecteddevice.py:124
+#, fuzzy
+msgid "Set Focused Display Distance"
+msgstr "Distanza del display"
+
+#: src/connecteddevice.py:125
+msgid "Use a closer value so the display zooms in when you look at it."
+msgstr ""
+
+#: src/connecteddevice.py:132
+#, fuzzy
+msgid "Set All Displays Distance"
+msgstr "Distanza del display"
+
+#: src/connecteddevice.py:133
+msgid "Use a farther value so the displays are zoomed out when you look away."
+msgstr ""
+
+#: src/connecteddevice.py:231
+msgid ""
+"Unable to add virtual displays on this machine. xdg-desktop-portal is "
+"required."
+msgstr ""
+
+#: src/connecteddevice.py:265
+#, fuzzy
+msgid "Focused display"
+msgstr "Distanza del display"
+
+#: src/connecteddevice.py:271
+#, fuzzy
+msgid "All displays"
+msgstr "Display curvo"
+
+#: src/displaydistancedialogcontent.py:41
+msgid "closer"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:45
+msgid "fullscreen"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:48
+msgid "farther"
+msgstr ""
+
#: src/licensedialogcontent.py:63
msgid "Paid Tier Status"
msgstr "Stato del livello a pagamento"
@@ -36,7 +83,7 @@ msgstr "Stato del livello a pagamento"
msgid "Feature Availability"
msgstr "Disponibilità delle funzionalità"
-#: src/licensefeaturerow.py:15 src/shortcutdialog.py:107
+#: src/licensefeaturerow.py:15 src/shortcutdialog.py:104
msgid "Disabled"
msgstr "Disabilitato"
@@ -165,178 +212,257 @@ msgid "Enables the Breezy Desktop XR effect."
msgstr "Abilita l'effetto XR di Breezy Desktop."
#: src/gtk/connected-device.ui:55
-msgid "Widescreen mode"
-msgstr "Modalità widescreen"
+msgid "Zoom on focus"
+msgstr ""
-#: src/gtk/connected-device.ui:66
+#: src/gtk/connected-device.ui:56
+msgid ""
+"Automatically move a display closer when you look at it.\n"
+"Set your preferred focused and unfocused distances in the Adjustments "
+"section.\n"
+" "
+msgstr ""
+
+#: src/gtk/connected-device.ui:69
msgid "Follow mode"
msgstr "Modalità di inseguimento"
-#: src/gtk/connected-device.ui:67
-msgid "Keep the virtual display near the center of your view."
+#: src/gtk/connected-device.ui:70
+#, fuzzy
+msgid "Keep the focused display near the center of your view."
msgstr "Mantieni il display virtuale vicino al centro del tuo campo visivo."
-#: src/gtk/connected-device.ui:77
-msgid "Curved display"
-msgstr "Display curvo"
+#: src/gtk/connected-device.ui:80 src/gtk/no-device.ui:34
+msgid "Disable physical displays"
+msgstr ""
-#: src/gtk/connected-device.ui:78
-msgid "Switch between flat and curved displays."
-msgstr "Passa da display piatti a curvi."
+#: src/gtk/connected-device.ui:81 src/gtk/no-device.ui:35
+msgid ""
+"Automatically disable all physical displays when the XR effect is enabled."
+msgstr ""
#: src/gtk/connected-device.ui:91
+#, fuzzy
+msgid "Virtual displays"
+msgstr "Display curvo"
+
+#: src/gtk/connected-device.ui:103
+#, fuzzy
+msgid "Rearrange displays"
+msgstr "Display curvo"
+
+#: src/gtk/connected-device.ui:115
+msgid "1080p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:116
+msgid "1440p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:140
msgid "Adjustments"
msgstr "Regolazioni"
-#: src/gtk/connected-device.ui:94
-msgid "Display distance"
+#: src/gtk/connected-device.ui:143
+#, fuzzy
+msgid "Display distances"
msgstr "Distanza del display"
-#: src/gtk/connected-device.ui:95
-msgid ""
-"Closer appears larger, further appears smaller. Controls depth when in "
-"widescreen mode."
+#: src/gtk/connected-device.ui:144
+msgid "Set how close you want displays to appear."
msgstr ""
-"Più vicino appare più grande, più lontano appare più piccolo. Controlla la profondità quando sei in modalità widescreen."
-#: src/gtk/connected-device.ui:123
-msgid "Display size"
-msgstr "Dimensione del display"
-
-#: src/gtk/connected-device.ui:124
-msgid ""
-"Combine with display distance to achieve a comfortable level of depth and "
-"size."
-msgstr ""
-"Da combinare con la distanza del display per ottenere un livello confortevole di profondità e dimensione."
-
-#: src/gtk/connected-device.ui:152
-msgid "Display toggle distances"
-msgstr "Distanze di attivazione del cambio display"
-
-#: src/gtk/connected-device.ui:153
-msgid ""
-"Use the buttons to capture the current display distance for use with the "
-"keyboard shortcut."
-msgstr ""
-"Usa i pulsanti per acquisire la distanza attuale del display da utilizzare con la scorciatoia da tastiera."
-
-#: src/gtk/connected-device.ui:178
+#: src/gtk/connected-device.ui:199
msgid "Follow threshold"
msgstr "Soglia di inseguimento"
-#: src/gtk/connected-device.ui:179
+#: src/gtk/connected-device.ui:200
msgid "How far away you can look before the display follows."
msgstr "Quanto lontano puoi guardare prima che il display ti segua."
-#: src/gtk/connected-device.ui:209 src/gtk/connected-device.ui:215
+#: src/gtk/connected-device.ui:223
+#, fuzzy
+msgid "Display angling"
+msgstr "Dimensione del display"
+
+#: src/gtk/connected-device.ui:224
+msgid ""
+"When there are multiple displays, choose how they should angle towards you."
+msgstr ""
+
+#: src/gtk/connected-device.ui:238
+msgid "Automatic"
+msgstr ""
+
+#: src/gtk/connected-device.ui:239
+msgid "Side-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:240
+msgid "Top-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:241
+msgid "Flat"
+msgstr ""
+
+#: src/gtk/connected-device.ui:251
+#, fuzzy
+msgid "Display spacing"
+msgstr "Dimensione del display"
+
+#: src/gtk/connected-device.ui:252
+msgid "Put empty space between displays, when there are multiple."
+msgstr ""
+
+#: src/gtk/connected-device.ui:282
+msgid "Viewport horizontal offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:283
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport to the left or right."
+msgstr ""
+
+#: src/gtk/connected-device.ui:312
+msgid "Viewport vertical offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:313
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport up or down."
+msgstr ""
+
+#: src/gtk/connected-device.ui:349 src/gtk/connected-device.ui:355
msgid "Keyboard Shortcuts"
msgstr "Scorciatoie da tastiera"
-#: src/gtk/connected-device.ui:218
+#: src/gtk/connected-device.ui:358
msgid "XR Effect on/off shortcut"
msgstr "Scorciatoia per on/off dell'effetto XR"
-#: src/gtk/connected-device.ui:219
+#: src/gtk/connected-device.ui:359
msgid ""
"Quickly enable or disable the XR Effect. You may need to enable the effect "
"manually once in order to enable the shortcut."
-msgstr "Abilitazione/disabilitazione rapida dell'effetto XR. Può essere necessario abilitare l'effetto manualmente almeno una volta per abilitare la scorciatoia da tastiera."
+msgstr ""
+"Abilitazione/disabilitazione rapida dell'effetto XR. Può essere necessario "
+"abilitare l'effetto manualmente almeno una volta per abilitare la "
+"scorciatoia da tastiera."
-#: src/gtk/connected-device.ui:238 src/gtk/connected-device.ui:267
-#: src/gtk/connected-device.ui:296 src/gtk/connected-device.ui:325
+#: src/gtk/connected-device.ui:378 src/gtk/connected-device.ui:407
+#: src/gtk/connected-device.ui:436 src/gtk/connected-device.ui:465
msgid "Change"
msgstr "Cambia"
-#: src/gtk/connected-device.ui:247
+#: src/gtk/connected-device.ui:387
msgid "Re-center display shortcut"
msgstr "Scorciatoia per ricentrare il display"
-#: src/gtk/connected-device.ui:248
+#: src/gtk/connected-device.ui:388
msgid "Pin the virtual display to the current position."
msgstr "Fissa il display virtuale alla posizione attuale."
-#: src/gtk/connected-device.ui:276
-msgid "Display distance shortcut"
-msgstr "Scorciatoia per la distanza del display"
+#: src/gtk/connected-device.ui:416
+#, fuzzy
+msgid "Toggle zoom on focus shortcut"
+msgstr "Scorciatoia per attivare/disattivare la modalità di inseguimento"
-#: src/gtk/connected-device.ui:277
-msgid "Quickly toggle between two predefined distances."
-msgstr "Scambio rapido tra due distanze predefinite."
+#: src/gtk/connected-device.ui:417
+#, fuzzy
+msgid "Quickly toggle zoom on focus mode."
+msgstr "Attivazione/disattivazione rapida della modalità di inseguimento."
-#: src/gtk/connected-device.ui:305
+#: src/gtk/connected-device.ui:445
msgid "Toggle follow mode shortcut"
msgstr "Scorciatoia per attivare/disattivare la modalità di inseguimento"
-#: src/gtk/connected-device.ui:306
+#: src/gtk/connected-device.ui:446
msgid "Quickly toggle follow mode."
msgstr "Attivazione/disattivazione rapida della modalità di inseguimento."
-#: src/gtk/connected-device.ui:341 src/gtk/connected-device.ui:347
+#: src/gtk/connected-device.ui:481 src/gtk/connected-device.ui:487
msgid "Advanced Settings"
msgstr "Impostazioni avanzate"
-#: src/gtk/connected-device.ui:350
+#: src/gtk/connected-device.ui:490
msgid "Find optimal display config"
msgstr "Trova la configurazione ottimale del display"
-#: src/gtk/connected-device.ui:351
+#: src/gtk/connected-device.ui:491
msgid ""
"Automatically modify the glasses display configuration for maximum "
"resolution and best scaling when plugged in."
msgstr ""
-"Modifica automaticamente la configurazione del display degli occhiali per ottenere la massima risoluzione e la migliore scalabilità quando collegati."
+"Modifica automaticamente la configurazione del display degli occhiali per "
+"ottenere la massima risoluzione e la migliore scalabilità quando collegati."
-#: src/gtk/connected-device.ui:361
+#: src/gtk/connected-device.ui:501
msgid "Use highest refresh rate"
msgstr "Usa la frequenza di aggiornamento più elevata"
-#: src/gtk/connected-device.ui:362
+#: src/gtk/connected-device.ui:502
msgid "Refresh rate may affect performance, disable this to set it manually."
-msgstr "La frequenza di aggiornamento potrebbe influire sulle prestazioni, disabilitala per impostarla manualmente."
+msgstr ""
+"La frequenza di aggiornamento potrebbe influire sulle prestazioni, "
+"disabilitala per impostarla manualmente."
-#: src/gtk/connected-device.ui:372
+#: src/gtk/connected-device.ui:512
msgid "Always primary display"
msgstr "Imposta sempre come display primario"
-#: src/gtk/connected-device.ui:373
+#: src/gtk/connected-device.ui:513
msgid "Automatically set the glasses as the primary display when plugged in."
-msgstr "Imposta automaticamente gli occhiali come display primario quando sono collegati."
-
-#: src/gtk/connected-device.ui:383
-msgid "Fast SBS mode switching"
-msgstr "Passaggio rapido alla modalità SBS"
-
-#: src/gtk/connected-device.ui:384
-msgid ""
-"Switches glasses to SBS mode immediately when plugged in, if widescreen mode "
-"is on. May cause instability."
msgstr ""
-"Passa immediatamente gli occhiali alla modalità SBS quando vengono collegati, se la modalità widescreen è attiva. Potrebbe causare instabilità."
+"Imposta automaticamente gli occhiali come display primario quando sono "
+"collegati."
-#: src/gtk/connected-device.ui:394
+#: src/gtk/connected-device.ui:523
msgid "Movement look-ahead"
msgstr "Anticipo del movimento"
-#: src/gtk/connected-device.ui:395
+#: src/gtk/connected-device.ui:524
msgid ""
"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."
msgstr ""
-"Compensa il ritardo di input prevedendo la posizione del tracciamento della testa prima del tempo di rendering. Rimani con il valore predefinito a meno che il display virtuale non rimanga indietro rispetto ai tuoi movimenti, non salti in avanti o sia molto tremolante."
+"Compensa il ritardo di input prevedendo la posizione del tracciamento della "
+"testa prima del tempo di rendering. Rimani con il valore predefinito a meno "
+"che il display virtuale non rimanga indietro rispetto ai tuoi movimenti, non "
+"salti in avanti o sia molto tremolante."
-#: src/gtk/connected-device.ui:413
+#: src/gtk/connected-device.ui:542
msgid "Default"
msgstr "Predefinito"
-#: src/gtk/connected-device.ui:425
+#: src/gtk/connected-device.ui:554
msgid "Text Scaling"
msgstr "Ridimensionamento del testo"
-#: src/gtk/connected-device.ui:426
+#: src/gtk/connected-device.ui:555
msgid "Scaling text below 1.0 will simulate a higher resolution display"
-msgstr "Ridimensionando il testo sotto a 1.0 si simula una maggiore risoluzione del display"
+msgstr ""
+"Ridimensionando il testo sotto a 1.0 si simula una maggiore risoluzione del "
+"display"
+
+#: src/gtk/connected-device.ui:585
+msgid "Enable multi-tap detection"
+msgstr ""
+
+#: src/gtk/connected-device.ui:586
+msgid "Enables double-tap to recenter and triple-tap to recalibrate."
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:9
+msgid "Show full range"
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:18
+msgid "Done"
+msgstr ""
#: src/gtk/failed-verification.ui:13
msgid "Breezy Desktop GNOME invalid setup"
@@ -347,7 +473,9 @@ msgid ""
"Your Breezy GNOME setup is invalid or incomplete. Please re-run the setup "
"script. Report this issue if it persists."
msgstr ""
-"La tua configurazione di Breezy GNOME non è valida o è incompleta. Ripeti l'esecuzione dello script di configurazione. Segnala questo problema se persiste."
+"La tua configurazione di Breezy GNOME non è valida o è incompleta. Ripeti "
+"l'esecuzione dello script di configurazione. Segnala questo problema se "
+"persiste."
#: src/gtk/license-dialog-content.ui:15
msgid "Donate"
@@ -381,15 +509,9 @@ msgstr "Abilitazione automatica effetto XR"
msgid ""
"Automatically enable the Breezy Desktop XR effect when supported glasses are "
"connected."
-msgstr "Abilita automaticamente l'effetto XR di Breezy Desktop quando vengono connessi degli occhiali supportati"
-
-#: src/gtk/no-device.ui:34
-msgid "Start in widescreen mode"
-msgstr "Si attiva in modalità widescreen"
-
-#: src/gtk/no-device.ui:35
-msgid "Widescreen mode is not supported for all glasses."
-msgstr "Breezy Desktop non è riuscito a rilevare alcun dispositivo XR supportato."
+msgstr ""
+"Abilita automaticamente l'effetto XR di Breezy Desktop quando vengono "
+"connessi degli occhiali supportati"
#: src/gtk/no-driver.ui:13
msgid "No driver running"
@@ -407,10 +529,12 @@ msgid ""
" "
msgstr ""
"\n"
-" Se hai installato tramite AUR, assicurati di aver eseguito il comando di post-installazione consigliato:\n"
+" Se hai installato tramite AUR, assicurati di aver eseguito il "
+"comando di post-installazione consigliato:\n"
" systemctl --user enable --now xr-driver.service\n"
"\n"
-" In caso contrario, segnala il problema su GitHub o crea un nuovo thread nel canale #troubleshooting su Discord.\n"
+" In caso contrario, segnala il problema su GitHub o crea un nuovo "
+"thread nel canale #troubleshooting su Discord.\n"
" "
#: src/gtk/no-extension.ui:13
@@ -422,7 +546,9 @@ msgid ""
"If you have just run the setup, then you may need to log out and back in to "
"use it. Otherwise, please follow the Breezy GNOME setup instructions."
msgstr ""
-"Se hai appena eseguito la configurazione, potrebbe essere necessario disconnettersi e riconnettersi per utilizzarla. In caso contrario, segui le istruzioni di configurazione di Breezy GNOME."
+"Se hai appena eseguito la configurazione, potrebbe essere necessario "
+"disconnettersi e riconnettersi per utilizzarla. In caso contrario, segui le "
+"istruzioni di configurazione di Breezy GNOME."
#: src/gtk/no-license.ui:13
msgid "No license file was found"
@@ -444,11 +570,16 @@ msgid ""
" "
msgstr ""
"\n"
-" La prima volta che utilizzi Breezy Desktop, è necessaria una connessione Internet per recuperare la licenza del tuo dispositivo.\n"
+" La prima volta che utilizzi Breezy Desktop, è necessaria una "
+"connessione Internet per recuperare la licenza del tuo dispositivo.\n"
" \n"
-" Non preoccuparti, non è obbligatorio donare adesso. Se non lo hai già fatto, ti verrà fornita una licenza di prova in modo da poter decidere se Breezy Desktop soddisfa le tue esigenze.\n"
+" Non preoccuparti, non è obbligatorio donare adesso. Se non lo hai "
+"già fatto, ti verrà fornita una licenza di prova in modo da poter decidere "
+"se Breezy Desktop soddisfa le tue esigenze.\n"
" \n"
-" Una volta ottenuta una licenza, di prova o altra, puoi utilizzare Breezy Desktop offline fino alla scadenza delle funzionalità (o a tempo indeterminato, se hai scelto l'accesso perpetuo).\n"
+" Una volta ottenuta una licenza, di prova o altra, puoi utilizzare "
+"Breezy Desktop offline fino alla scadenza delle funzionalità (o a tempo "
+"indeterminato, se hai scelto l'accesso perpetuo).\n"
" "
#: src/gtk/no-license.ui:27
@@ -490,3 +621,61 @@ msgstr "Reset forzato"
#: src/gtk/window.ui:99
msgid "About BreezyDesktop"
msgstr "Informazioni su BreezyDesktop"
+
+#~ msgid "Fast SBS mode switching"
+#~ msgstr "Passaggio rapido alla modalità SBS"
+
+#~ msgid ""
+#~ "Switches glasses to SBS mode immediately when plugged in, if widescreen "
+#~ "mode is on. May cause instability."
+#~ msgstr ""
+#~ "Passa immediatamente gli occhiali alla modalità SBS quando vengono "
+#~ "collegati, se la modalità widescreen è attiva. Potrebbe causare "
+#~ "instabilità."
+
+#, fuzzy
+#~ msgid "Set Unfocused Displays Distance"
+#~ msgstr "Distanza del display"
+
+#~ msgid "Widescreen mode"
+#~ msgstr "Modalità widescreen"
+
+#~ msgid "Switch between flat and curved displays."
+#~ msgstr "Passa da display piatti a curvi."
+
+#~ msgid ""
+#~ "Closer appears larger, further appears smaller. Controls depth when in "
+#~ "widescreen mode."
+#~ msgstr ""
+#~ "Più vicino appare più grande, più lontano appare più piccolo. Controlla "
+#~ "la profondità quando sei in modalità widescreen."
+
+#~ msgid ""
+#~ "Combine with display distance to achieve a comfortable level of depth and "
+#~ "size."
+#~ msgstr ""
+#~ "Da combinare con la distanza del display per ottenere un livello "
+#~ "confortevole di profondità e dimensione."
+
+#~ msgid "Display toggle distances"
+#~ msgstr "Distanze di attivazione del cambio display"
+
+#~ msgid ""
+#~ "Use the buttons to capture the current display distance for use with the "
+#~ "keyboard shortcut."
+#~ msgstr ""
+#~ "Usa i pulsanti per acquisire la distanza attuale del display da "
+#~ "utilizzare con la scorciatoia da tastiera."
+
+#~ msgid "Display distance shortcut"
+#~ msgstr "Scorciatoia per la distanza del display"
+
+#~ msgid "Quickly toggle between two predefined distances."
+#~ msgstr "Scambio rapido tra due distanze predefinite."
+
+#~ msgid "Start in widescreen mode"
+#~ msgstr "Si attiva in modalità widescreen"
+
+#~ msgid "Widescreen mode is not supported for all glasses."
+#~ msgstr ""
+#~ "Breezy Desktop non è riuscito a rilevare alcun dispositivo XR supportato."
diff --git a/ui/po/ja.po b/ui/po/ja.po
index 5f735f9..05b152d 100644
--- a/ui/po/ja.po
+++ b/ui/po/ja.po
@@ -11,7 +11,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-22 13:27-0700\n"
+"POT-Creation-Date: 2025-03-05 12:28-0800\n"
"PO-Revision-Date: 2024-08-02 20:55-0700\n"
"Last-Translator: \n"
"Language-Team: Japanese \n"
@@ -21,16 +21,62 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
-#: src/connecteddevice.py:17
+#: src/connecteddevice.py:23
msgid ""
"Switches your glasses into side-by-side mode and doubles the width of the "
"display."
msgstr "メガネを3Dモードに切り替え、表示の幅を2倍にします。"
-#: src/connecteddevice.py:18
+#: src/connecteddevice.py:24
msgid "This feature is not currently supported for your device."
msgstr "現在接続されているデバイスはこの機能に対応していません。"
+#: src/connecteddevice.py:124
+#, fuzzy
+msgid "Set Focused Display Distance"
+msgstr "ディスプレイ距離"
+
+#: src/connecteddevice.py:125
+msgid "Use a closer value so the display zooms in when you look at it."
+msgstr ""
+
+#: src/connecteddevice.py:132
+#, fuzzy
+msgid "Set All Displays Distance"
+msgstr "ディスプレイ距離"
+
+#: src/connecteddevice.py:133
+msgid "Use a farther value so the displays are zoomed out when you look away."
+msgstr ""
+
+#: src/connecteddevice.py:231
+msgid ""
+"Unable to add virtual displays on this machine. xdg-desktop-portal is "
+"required."
+msgstr ""
+
+#: src/connecteddevice.py:265
+#, fuzzy
+msgid "Focused display"
+msgstr "ディスプレイ距離"
+
+#: src/connecteddevice.py:271
+#, fuzzy
+msgid "All displays"
+msgstr "曲面ディスプレイ"
+
+#: src/displaydistancedialogcontent.py:41
+msgid "closer"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:45
+msgid "fullscreen"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:48
+msgid "farther"
+msgstr ""
+
#: src/licensedialogcontent.py:63
msgid "Paid Tier Status"
msgstr "有料ティアの状態"
@@ -39,7 +85,7 @@ msgstr "有料ティアの状態"
msgid "Feature Availability"
msgstr "利用できる機能"
-#: src/licensefeaturerow.py:15 src/shortcutdialog.py:107
+#: src/licensefeaturerow.py:15 src/shortcutdialog.py:104
msgid "Disabled"
msgstr "無効"
@@ -168,82 +214,138 @@ msgid "Enables the Breezy Desktop XR effect."
msgstr "Breezy DesktopのXRエフェクトを有効にします。"
#: src/gtk/connected-device.ui:55
-msgid "Widescreen mode"
-msgstr "ワイドスクリーンモード"
+msgid "Zoom on focus"
+msgstr ""
-#: src/gtk/connected-device.ui:66
+#: src/gtk/connected-device.ui:56
+msgid ""
+"Automatically move a display closer when you look at it.\n"
+"Set your preferred focused and unfocused distances in the Adjustments "
+"section.\n"
+" "
+msgstr ""
+
+#: src/gtk/connected-device.ui:69
msgid "Follow mode"
msgstr "フォローモード"
-#: src/gtk/connected-device.ui:67
-msgid "Keep the virtual display near the center of your view."
+#: src/gtk/connected-device.ui:70
+#, fuzzy
+msgid "Keep the focused display near the center of your view."
msgstr "仮想ディスプレイを視界の中心近くに保ちます。"
-#: src/gtk/connected-device.ui:77
-msgid "Curved display"
-msgstr "曲面ディスプレイ"
+#: src/gtk/connected-device.ui:80 src/gtk/no-device.ui:34
+msgid "Disable physical displays"
+msgstr ""
-#: src/gtk/connected-device.ui:78
-msgid "Switch between flat and curved displays."
-msgstr "平面と曲面ディスプレイを切り替えます。"
+#: src/gtk/connected-device.ui:81 src/gtk/no-device.ui:35
+msgid ""
+"Automatically disable all physical displays when the XR effect is enabled."
+msgstr ""
#: src/gtk/connected-device.ui:91
+#, fuzzy
+msgid "Virtual displays"
+msgstr "曲面ディスプレイ"
+
+#: src/gtk/connected-device.ui:103
+#, fuzzy
+msgid "Rearrange displays"
+msgstr "曲面ディスプレイ"
+
+#: src/gtk/connected-device.ui:115
+msgid "1080p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:116
+msgid "1440p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:140
msgid "Adjustments"
msgstr "調整"
-#: src/gtk/connected-device.ui:94
-msgid "Display distance"
+#: src/gtk/connected-device.ui:143
+#, fuzzy
+msgid "Display distances"
msgstr "ディスプレイ距離"
-#: src/gtk/connected-device.ui:95
-msgid ""
-"Closer appears larger, further appears smaller. Controls depth when in "
-"widescreen mode."
+#: src/gtk/connected-device.ui:144
+msgid "Set how close you want displays to appear."
msgstr ""
-"近づけると大きく見え、遠ざけると小さく見えます。\n"
-"ワイドスクリーンモードの奥行きを制御します。"
-#: src/gtk/connected-device.ui:123
-msgid "Display size"
-msgstr "ディスプレイサイズ"
-
-#: src/gtk/connected-device.ui:124
-msgid ""
-"Combine with display distance to achieve a comfortable level of depth and "
-"size."
-msgstr ""
-"ディスプレイ距離と組み合わせて、\n"
-"快適な奥行きとサイズを実現します。"
-
-#: src/gtk/connected-device.ui:152
-msgid "Display toggle distances"
-msgstr "ディスプレイ距離の切り替え"
-
-#: src/gtk/connected-device.ui:153
-msgid ""
-"Use the buttons to capture the current display distance for use with the "
-"keyboard shortcut."
-msgstr ""
-"2個のボタンを使用して、キーボードショートカットで使用する現在のディスプレイ距"
-"離をそれぞれ設定します。"
-
-#: src/gtk/connected-device.ui:178
+#: src/gtk/connected-device.ui:199
msgid "Follow threshold"
msgstr "フォローしきい値"
-#: src/gtk/connected-device.ui:179
+#: src/gtk/connected-device.ui:200
msgid "How far away you can look before the display follows."
msgstr "ディスプレイがフォロー(追従)を開始するまでの距離"
-#: src/gtk/connected-device.ui:209 src/gtk/connected-device.ui:215
+#: src/gtk/connected-device.ui:223
+#, fuzzy
+msgid "Display angling"
+msgstr "ディスプレイサイズ"
+
+#: src/gtk/connected-device.ui:224
+msgid ""
+"When there are multiple displays, choose how they should angle towards you."
+msgstr ""
+
+#: src/gtk/connected-device.ui:238
+msgid "Automatic"
+msgstr ""
+
+#: src/gtk/connected-device.ui:239
+msgid "Side-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:240
+msgid "Top-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:241
+msgid "Flat"
+msgstr ""
+
+#: src/gtk/connected-device.ui:251
+#, fuzzy
+msgid "Display spacing"
+msgstr "ディスプレイサイズ"
+
+#: src/gtk/connected-device.ui:252
+msgid "Put empty space between displays, when there are multiple."
+msgstr ""
+
+#: src/gtk/connected-device.ui:282
+msgid "Viewport horizontal offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:283
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport to the left or right."
+msgstr ""
+
+#: src/gtk/connected-device.ui:312
+msgid "Viewport vertical offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:313
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport up or down."
+msgstr ""
+
+#: src/gtk/connected-device.ui:349 src/gtk/connected-device.ui:355
msgid "Keyboard Shortcuts"
msgstr "キーボードショートカット"
-#: src/gtk/connected-device.ui:218
+#: src/gtk/connected-device.ui:358
msgid "XR Effect on/off shortcut"
msgstr "XRエフェクトの切り替え"
-#: src/gtk/connected-device.ui:219
+#: src/gtk/connected-device.ui:359
msgid ""
"Quickly enable or disable the XR Effect. You may need to enable the effect "
"manually once in order to enable the shortcut."
@@ -251,44 +353,46 @@ msgstr ""
"XRエフェクトの有効・無効を切り替えます。このショートカットを有効にするために"
"手動で1回XRエフェクトを有効にする必要があります。"
-#: src/gtk/connected-device.ui:238 src/gtk/connected-device.ui:267
-#: src/gtk/connected-device.ui:296 src/gtk/connected-device.ui:325
+#: src/gtk/connected-device.ui:378 src/gtk/connected-device.ui:407
+#: src/gtk/connected-device.ui:436 src/gtk/connected-device.ui:465
msgid "Change"
msgstr "変更"
-#: src/gtk/connected-device.ui:247
+#: src/gtk/connected-device.ui:387
msgid "Re-center display shortcut"
msgstr "ディスプレイを中央へ移動"
-#: src/gtk/connected-device.ui:248
+#: src/gtk/connected-device.ui:388
msgid "Pin the virtual display to the current position."
msgstr "仮想ディスプレイを現在の視点に固定します。"
-#: src/gtk/connected-device.ui:276
-msgid "Display distance shortcut"
-msgstr "ディスプレイ距離の切り替え"
+#: src/gtk/connected-device.ui:416
+#, fuzzy
+msgid "Toggle zoom on focus shortcut"
+msgstr "フォローモードの切り替え"
-#: src/gtk/connected-device.ui:277
-msgid "Quickly toggle between two predefined distances."
-msgstr "設定してある2個の距離をすばやく切り替えます。"
+#: src/gtk/connected-device.ui:417
+#, fuzzy
+msgid "Quickly toggle zoom on focus mode."
+msgstr "フォローモードのオン/オフをすばやく切り替えます。"
-#: src/gtk/connected-device.ui:305
+#: src/gtk/connected-device.ui:445
msgid "Toggle follow mode shortcut"
msgstr "フォローモードの切り替え"
-#: src/gtk/connected-device.ui:306
+#: src/gtk/connected-device.ui:446
msgid "Quickly toggle follow mode."
msgstr "フォローモードのオン/オフをすばやく切り替えます。"
-#: src/gtk/connected-device.ui:341 src/gtk/connected-device.ui:347
+#: src/gtk/connected-device.ui:481 src/gtk/connected-device.ui:487
msgid "Advanced Settings"
msgstr "詳細設定"
-#: src/gtk/connected-device.ui:350
+#: src/gtk/connected-device.ui:490
msgid "Find optimal display config"
msgstr "ディスプレイ設定を最適化する"
-#: src/gtk/connected-device.ui:351
+#: src/gtk/connected-device.ui:491
msgid ""
"Automatically modify the glasses display configuration for maximum "
"resolution and best scaling when plugged in."
@@ -296,41 +400,29 @@ msgstr ""
"メガネ接続時、最大解像度と最適なスケーリングのためにディスプレイ設定を自動的"
"に変更します。"
-#: src/gtk/connected-device.ui:361
+#: src/gtk/connected-device.ui:501
msgid "Use highest refresh rate"
msgstr "最大のリフレッシュレートを使用する"
-#: src/gtk/connected-device.ui:362
+#: src/gtk/connected-device.ui:502
msgid "Refresh rate may affect performance, disable this to set it manually."
msgstr ""
"リフレッシュレートはパフォーマンスに影響を与える可能性があります。手動で設定"
"する場合は無効にしてください。"
-#: src/gtk/connected-device.ui:372
+#: src/gtk/connected-device.ui:512
msgid "Always primary display"
msgstr "常にプライマリディスプレイにする"
-#: src/gtk/connected-device.ui:373
+#: src/gtk/connected-device.ui:513
msgid "Automatically set the glasses as the primary display when plugged in."
msgstr "メガネ接続時、自動的にプライマリディスプレイにします。"
-#: src/gtk/connected-device.ui:383
-msgid "Fast SBS mode switching"
-msgstr "高速SBSモード切り替え"
-
-#: src/gtk/connected-device.ui:384
-msgid ""
-"Switches glasses to SBS mode immediately when plugged in, if widescreen mode "
-"is on. May cause instability."
-msgstr ""
-"ワイドスクリーンモードがオンになっている場合、メガネ接続時にたたちにSBSモード"
-"に切り替えます。不安定になる可能性があります。"
-
-#: src/gtk/connected-device.ui:394
+#: src/gtk/connected-device.ui:523
msgid "Movement look-ahead"
msgstr "動きの先読み"
-#: src/gtk/connected-device.ui:395
+#: src/gtk/connected-device.ui:524
msgid ""
"Counteracts input lag by predicting head-tracking position ahead of render "
"time. Stick with default unless virtual display drags behind your head "
@@ -340,20 +432,36 @@ msgstr ""
"ます。仮想ディスプレイが頭の動きに遅れたり、先に進んだり、非常に揺れたりする"
"場合を除き、デフォルトのままで問題ありません。"
-#: src/gtk/connected-device.ui:413
+#: src/gtk/connected-device.ui:542
msgid "Default"
msgstr "デフォルト"
-#: src/gtk/connected-device.ui:425
+#: src/gtk/connected-device.ui:554
msgid "Text Scaling"
msgstr "テキストスケーリング"
-#: src/gtk/connected-device.ui:426
+#: src/gtk/connected-device.ui:555
msgid "Scaling text below 1.0 will simulate a higher resolution display"
msgstr ""
"テキストを1.0未満にスケーリングすると、高解像度ディスプレイをシミュレートしま"
"す。"
+#: src/gtk/connected-device.ui:585
+msgid "Enable multi-tap detection"
+msgstr ""
+
+#: src/gtk/connected-device.ui:586
+msgid "Enables double-tap to recenter and triple-tap to recalibrate."
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:9
+msgid "Show full range"
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:18
+msgid "Done"
+msgstr ""
+
#: src/gtk/failed-verification.ui:13
msgid "Breezy Desktop GNOME invalid setup"
msgstr "Breezy Desktop GNOMEの無効なセットアップ"
@@ -398,15 +506,8 @@ msgstr "XRエフェクトの自動有効化"
msgid ""
"Automatically enable the Breezy Desktop XR effect when supported glasses are "
"connected."
-msgstr "対応メガネを接続するとBreezy DesktopのXRエフェクトを自動的に開始します。"
-
-#: src/gtk/no-device.ui:34
-msgid "Start in widescreen mode"
-msgstr "ワイドスクリーンモードで起動"
-
-#: src/gtk/no-device.ui:35
-msgid "Widescreen mode is not supported for all glasses."
-msgstr "ワイドスクリーンモードはすべてのメガネで対応しているわけではありません。"
+msgstr ""
+"対応メガネを接続するとBreezy DesktopのXRエフェクトを自動的に開始します。"
#: src/gtk/no-driver.ui:13
msgid "No driver running"
@@ -517,3 +618,60 @@ msgstr "強制リセット"
#: src/gtk/window.ui:99
msgid "About BreezyDesktop"
msgstr "Breezy Desktopについて"
+
+#~ msgid "Fast SBS mode switching"
+#~ msgstr "高速SBSモード切り替え"
+
+#~ msgid ""
+#~ "Switches glasses to SBS mode immediately when plugged in, if widescreen "
+#~ "mode is on. May cause instability."
+#~ msgstr ""
+#~ "ワイドスクリーンモードがオンになっている場合、メガネ接続時にたたちにSBS"
+#~ "モードに切り替えます。不安定になる可能性があります。"
+
+#, fuzzy
+#~ msgid "Set Unfocused Displays Distance"
+#~ msgstr "ディスプレイ距離"
+
+#~ msgid "Widescreen mode"
+#~ msgstr "ワイドスクリーンモード"
+
+#~ msgid "Switch between flat and curved displays."
+#~ msgstr "平面と曲面ディスプレイを切り替えます。"
+
+#~ msgid ""
+#~ "Closer appears larger, further appears smaller. Controls depth when in "
+#~ "widescreen mode."
+#~ msgstr ""
+#~ "近づけると大きく見え、遠ざけると小さく見えます。\n"
+#~ "ワイドスクリーンモードの奥行きを制御します。"
+
+#~ msgid ""
+#~ "Combine with display distance to achieve a comfortable level of depth and "
+#~ "size."
+#~ msgstr ""
+#~ "ディスプレイ距離と組み合わせて、\n"
+#~ "快適な奥行きとサイズを実現します。"
+
+#~ msgid "Display toggle distances"
+#~ msgstr "ディスプレイ距離の切り替え"
+
+#~ msgid ""
+#~ "Use the buttons to capture the current display distance for use with the "
+#~ "keyboard shortcut."
+#~ msgstr ""
+#~ "2個のボタンを使用して、キーボードショートカットで使用する現在のディスプレ"
+#~ "イ距離をそれぞれ設定します。"
+
+#~ msgid "Display distance shortcut"
+#~ msgstr "ディスプレイ距離の切り替え"
+
+#~ msgid "Quickly toggle between two predefined distances."
+#~ msgstr "設定してある2個の距離をすばやく切り替えます。"
+
+#~ msgid "Start in widescreen mode"
+#~ msgstr "ワイドスクリーンモードで起動"
+
+#~ msgid "Widescreen mode is not supported for all glasses."
+#~ msgstr ""
+#~ "ワイドスクリーンモードはすべてのメガネで対応しているわけではありません。"
diff --git a/ui/po/mo/de/LC_MESSAGES/breezydesktop.mo b/ui/po/mo/de/LC_MESSAGES/breezydesktop.mo
index b0e713e..812e950 100644
Binary files a/ui/po/mo/de/LC_MESSAGES/breezydesktop.mo and b/ui/po/mo/de/LC_MESSAGES/breezydesktop.mo differ
diff --git a/ui/po/mo/es/LC_MESSAGES/breezydesktop.mo b/ui/po/mo/es/LC_MESSAGES/breezydesktop.mo
index e63ac9c..10a23bb 100644
Binary files a/ui/po/mo/es/LC_MESSAGES/breezydesktop.mo and b/ui/po/mo/es/LC_MESSAGES/breezydesktop.mo differ
diff --git a/ui/po/mo/fr/LC_MESSAGES/breezydesktop.mo b/ui/po/mo/fr/LC_MESSAGES/breezydesktop.mo
index d2edf14..c8d36cb 100644
Binary files a/ui/po/mo/fr/LC_MESSAGES/breezydesktop.mo and b/ui/po/mo/fr/LC_MESSAGES/breezydesktop.mo differ
diff --git a/ui/po/mo/it/LC_MESSAGES/breezydesktop.mo b/ui/po/mo/it/LC_MESSAGES/breezydesktop.mo
index 0485f79..56522b1 100644
Binary files a/ui/po/mo/it/LC_MESSAGES/breezydesktop.mo and b/ui/po/mo/it/LC_MESSAGES/breezydesktop.mo differ
diff --git a/ui/po/mo/ja/LC_MESSAGES/breezydesktop.mo b/ui/po/mo/ja/LC_MESSAGES/breezydesktop.mo
index b039e79..2cc0d3a 100644
Binary files a/ui/po/mo/ja/LC_MESSAGES/breezydesktop.mo and b/ui/po/mo/ja/LC_MESSAGES/breezydesktop.mo differ
diff --git a/ui/po/mo/pt_BR/LC_MESSAGES/breezydesktop.mo b/ui/po/mo/pt_BR/LC_MESSAGES/breezydesktop.mo
index 18c9d92..3b1c11c 100644
Binary files a/ui/po/mo/pt_BR/LC_MESSAGES/breezydesktop.mo and b/ui/po/mo/pt_BR/LC_MESSAGES/breezydesktop.mo differ
diff --git a/ui/po/mo/ru/LC_MESSAGES/breezydesktop.mo b/ui/po/mo/ru/LC_MESSAGES/breezydesktop.mo
index 17b4db3..337a833 100644
Binary files a/ui/po/mo/ru/LC_MESSAGES/breezydesktop.mo and b/ui/po/mo/ru/LC_MESSAGES/breezydesktop.mo differ
diff --git a/ui/po/mo/sv/LC_MESSAGES/breezydesktop.mo b/ui/po/mo/sv/LC_MESSAGES/breezydesktop.mo
index 2629fc1..aafc036 100644
Binary files a/ui/po/mo/sv/LC_MESSAGES/breezydesktop.mo and b/ui/po/mo/sv/LC_MESSAGES/breezydesktop.mo differ
diff --git a/ui/po/mo/uk_UA/LC_MESSAGES/breezydesktop.mo b/ui/po/mo/uk_UA/LC_MESSAGES/breezydesktop.mo
index d795448..f9f6b67 100644
Binary files a/ui/po/mo/uk_UA/LC_MESSAGES/breezydesktop.mo and b/ui/po/mo/uk_UA/LC_MESSAGES/breezydesktop.mo differ
diff --git a/ui/po/mo/zh_CN/LC_MESSAGES/breezydesktop.mo b/ui/po/mo/zh_CN/LC_MESSAGES/breezydesktop.mo
index 9a1698a..2373235 100644
Binary files a/ui/po/mo/zh_CN/LC_MESSAGES/breezydesktop.mo and b/ui/po/mo/zh_CN/LC_MESSAGES/breezydesktop.mo differ
diff --git a/ui/po/pl.po b/ui/po/pl.po
index 0b599c8..2bb7084 100644
--- a/ui/po/pl.po
+++ b/ui/po/pl.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-22 13:27-0700\n"
+"POT-Creation-Date: 2025-03-05 12:28-0800\n"
"PO-Revision-Date: 2024-08-16 10:26-0700\n"
"Last-Translator: \n"
"Language-Team: Polish \n"
@@ -18,16 +18,58 @@ msgstr ""
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"
-#: src/connecteddevice.py:17
+#: src/connecteddevice.py:23
msgid ""
"Switches your glasses into side-by-side mode and doubles the width of the "
"display."
msgstr ""
-#: src/connecteddevice.py:18
+#: src/connecteddevice.py:24
msgid "This feature is not currently supported for your device."
msgstr ""
+#: src/connecteddevice.py:124
+msgid "Set Focused Display Distance"
+msgstr ""
+
+#: src/connecteddevice.py:125
+msgid "Use a closer value so the display zooms in when you look at it."
+msgstr ""
+
+#: src/connecteddevice.py:132
+msgid "Set All Displays Distance"
+msgstr ""
+
+#: src/connecteddevice.py:133
+msgid "Use a farther value so the displays are zoomed out when you look away."
+msgstr ""
+
+#: src/connecteddevice.py:231
+msgid ""
+"Unable to add virtual displays on this machine. xdg-desktop-portal is "
+"required."
+msgstr ""
+
+#: src/connecteddevice.py:265
+msgid "Focused display"
+msgstr ""
+
+#: src/connecteddevice.py:271
+msgid "All displays"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:41
+msgid "closer"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:45
+msgid "fullscreen"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:48
+msgid "farther"
+msgstr ""
+
#: src/licensedialogcontent.py:63
msgid "Paid Tier Status"
msgstr ""
@@ -36,7 +78,7 @@ msgstr ""
msgid "Feature Availability"
msgstr ""
-#: src/licensefeaturerow.py:15 src/shortcutdialog.py:107
+#: src/licensefeaturerow.py:15 src/shortcutdialog.py:104
msgid "Disabled"
msgstr ""
@@ -165,173 +207,235 @@ msgid "Enables the Breezy Desktop XR effect."
msgstr ""
#: src/gtk/connected-device.ui:55
-msgid "Widescreen mode"
+msgid "Zoom on focus"
msgstr ""
-#: src/gtk/connected-device.ui:66
+#: src/gtk/connected-device.ui:56
+msgid ""
+"Automatically move a display closer when you look at it.\n"
+"Set your preferred focused and unfocused distances in the Adjustments "
+"section.\n"
+" "
+msgstr ""
+
+#: src/gtk/connected-device.ui:69
msgid "Follow mode"
msgstr ""
-#: src/gtk/connected-device.ui:67
-msgid "Keep the virtual display near the center of your view."
+#: src/gtk/connected-device.ui:70
+msgid "Keep the focused display near the center of your view."
msgstr ""
-#: src/gtk/connected-device.ui:77
-msgid "Curved display"
+#: src/gtk/connected-device.ui:80 src/gtk/no-device.ui:34
+msgid "Disable physical displays"
msgstr ""
-#: src/gtk/connected-device.ui:78
-msgid "Switch between flat and curved displays."
+#: src/gtk/connected-device.ui:81 src/gtk/no-device.ui:35
+msgid ""
+"Automatically disable all physical displays when the XR effect is enabled."
msgstr ""
#: src/gtk/connected-device.ui:91
+msgid "Virtual displays"
+msgstr ""
+
+#: src/gtk/connected-device.ui:103
+msgid "Rearrange displays"
+msgstr ""
+
+#: src/gtk/connected-device.ui:115
+msgid "1080p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:116
+msgid "1440p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:140
msgid "Adjustments"
msgstr ""
-#: src/gtk/connected-device.ui:94
-msgid "Display distance"
+#: src/gtk/connected-device.ui:143
+msgid "Display distances"
msgstr ""
-#: src/gtk/connected-device.ui:95
-msgid ""
-"Closer appears larger, further appears smaller. Controls depth when in "
-"widescreen mode."
+#: src/gtk/connected-device.ui:144
+msgid "Set how close you want displays to appear."
msgstr ""
-#: src/gtk/connected-device.ui:123
-msgid "Display size"
-msgstr ""
-
-#: src/gtk/connected-device.ui:124
-msgid ""
-"Combine with display distance to achieve a comfortable level of depth and "
-"size."
-msgstr ""
-
-#: src/gtk/connected-device.ui:152
-msgid "Display toggle distances"
-msgstr ""
-
-#: src/gtk/connected-device.ui:153
-msgid ""
-"Use the buttons to capture the current display distance for use with the "
-"keyboard shortcut."
-msgstr ""
-
-#: src/gtk/connected-device.ui:178
+#: src/gtk/connected-device.ui:199
msgid "Follow threshold"
msgstr ""
-#: src/gtk/connected-device.ui:179
+#: src/gtk/connected-device.ui:200
msgid "How far away you can look before the display follows."
msgstr ""
-#: src/gtk/connected-device.ui:209 src/gtk/connected-device.ui:215
+#: src/gtk/connected-device.ui:223
+msgid "Display angling"
+msgstr ""
+
+#: src/gtk/connected-device.ui:224
+msgid ""
+"When there are multiple displays, choose how they should angle towards you."
+msgstr ""
+
+#: src/gtk/connected-device.ui:238
+msgid "Automatic"
+msgstr ""
+
+#: src/gtk/connected-device.ui:239
+msgid "Side-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:240
+msgid "Top-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:241
+msgid "Flat"
+msgstr ""
+
+#: src/gtk/connected-device.ui:251
+msgid "Display spacing"
+msgstr ""
+
+#: src/gtk/connected-device.ui:252
+msgid "Put empty space between displays, when there are multiple."
+msgstr ""
+
+#: src/gtk/connected-device.ui:282
+msgid "Viewport horizontal offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:283
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport to the left or right."
+msgstr ""
+
+#: src/gtk/connected-device.ui:312
+msgid "Viewport vertical offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:313
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport up or down."
+msgstr ""
+
+#: src/gtk/connected-device.ui:349 src/gtk/connected-device.ui:355
msgid "Keyboard Shortcuts"
msgstr ""
-#: src/gtk/connected-device.ui:218
+#: src/gtk/connected-device.ui:358
msgid "XR Effect on/off shortcut"
msgstr ""
-#: src/gtk/connected-device.ui:219
+#: src/gtk/connected-device.ui:359
msgid ""
"Quickly enable or disable the XR Effect. You may need to enable the effect "
"manually once in order to enable the shortcut."
msgstr ""
-#: src/gtk/connected-device.ui:238 src/gtk/connected-device.ui:267
-#: src/gtk/connected-device.ui:296 src/gtk/connected-device.ui:325
+#: src/gtk/connected-device.ui:378 src/gtk/connected-device.ui:407
+#: src/gtk/connected-device.ui:436 src/gtk/connected-device.ui:465
msgid "Change"
msgstr ""
-#: src/gtk/connected-device.ui:247
+#: src/gtk/connected-device.ui:387
msgid "Re-center display shortcut"
msgstr ""
-#: src/gtk/connected-device.ui:248
+#: src/gtk/connected-device.ui:388
msgid "Pin the virtual display to the current position."
msgstr ""
-#: src/gtk/connected-device.ui:276
-msgid "Display distance shortcut"
+#: src/gtk/connected-device.ui:416
+msgid "Toggle zoom on focus shortcut"
msgstr ""
-#: src/gtk/connected-device.ui:277
-msgid "Quickly toggle between two predefined distances."
+#: src/gtk/connected-device.ui:417
+msgid "Quickly toggle zoom on focus mode."
msgstr ""
-#: src/gtk/connected-device.ui:305
+#: src/gtk/connected-device.ui:445
msgid "Toggle follow mode shortcut"
msgstr ""
-#: src/gtk/connected-device.ui:306
+#: src/gtk/connected-device.ui:446
msgid "Quickly toggle follow mode."
msgstr ""
-#: src/gtk/connected-device.ui:341 src/gtk/connected-device.ui:347
+#: src/gtk/connected-device.ui:481 src/gtk/connected-device.ui:487
msgid "Advanced Settings"
msgstr ""
-#: src/gtk/connected-device.ui:350
+#: src/gtk/connected-device.ui:490
msgid "Find optimal display config"
msgstr ""
-#: src/gtk/connected-device.ui:351
+#: src/gtk/connected-device.ui:491
msgid ""
"Automatically modify the glasses display configuration for maximum "
"resolution and best scaling when plugged in."
msgstr ""
-#: src/gtk/connected-device.ui:361
+#: src/gtk/connected-device.ui:501
msgid "Use highest refresh rate"
msgstr ""
-#: src/gtk/connected-device.ui:362
+#: src/gtk/connected-device.ui:502
msgid "Refresh rate may affect performance, disable this to set it manually."
msgstr ""
-#: src/gtk/connected-device.ui:372
+#: src/gtk/connected-device.ui:512
msgid "Always primary display"
msgstr ""
-#: src/gtk/connected-device.ui:373
+#: src/gtk/connected-device.ui:513
msgid "Automatically set the glasses as the primary display when plugged in."
msgstr ""
-#: src/gtk/connected-device.ui:383
-msgid "Fast SBS mode switching"
-msgstr ""
-
-#: src/gtk/connected-device.ui:384
-msgid ""
-"Switches glasses to SBS mode immediately when plugged in, if widescreen mode "
-"is on. May cause instability."
-msgstr ""
-
-#: src/gtk/connected-device.ui:394
+#: src/gtk/connected-device.ui:523
msgid "Movement look-ahead"
msgstr ""
-#: src/gtk/connected-device.ui:395
+#: src/gtk/connected-device.ui:524
msgid ""
"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."
msgstr ""
-#: src/gtk/connected-device.ui:413
+#: src/gtk/connected-device.ui:542
msgid "Default"
msgstr ""
-#: src/gtk/connected-device.ui:425
+#: src/gtk/connected-device.ui:554
msgid "Text Scaling"
msgstr ""
-#: src/gtk/connected-device.ui:426
+#: src/gtk/connected-device.ui:555
msgid "Scaling text below 1.0 will simulate a higher resolution display"
msgstr ""
+#: src/gtk/connected-device.ui:585
+msgid "Enable multi-tap detection"
+msgstr ""
+
+#: src/gtk/connected-device.ui:586
+msgid "Enables double-tap to recenter and triple-tap to recalibrate."
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:9
+msgid "Show full range"
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:18
+msgid "Done"
+msgstr ""
+
#: src/gtk/failed-verification.ui:13
msgid "Breezy Desktop GNOME invalid setup"
msgstr ""
@@ -376,14 +480,6 @@ msgid ""
"connected."
msgstr ""
-#: src/gtk/no-device.ui:34
-msgid "Start in widescreen mode"
-msgstr ""
-
-#: src/gtk/no-device.ui:35
-msgid "Widescreen mode is not supported for all glasses."
-msgstr ""
-
#: src/gtk/no-driver.ui:13
msgid "No driver running"
msgstr ""
diff --git a/ui/po/pt_BR.po b/ui/po/pt_BR.po
index 1ca89cc..c13a71b 100644
--- a/ui/po/pt_BR.po
+++ b/ui/po/pt_BR.po
@@ -7,18 +7,18 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-22 13:27-0700\n"
+"POT-Creation-Date: 2025-03-05 12:28-0800\n"
"PO-Revision-Date: 2024-08-19 09:39-0700\n"
"Last-Translator: \n"
-"Language-Team: Brazilian Portuguese \n"
+"Language-Team: Brazilian Portuguese \n"
"Language: pt_BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
-#: src/connecteddevice.py:17
+#: src/connecteddevice.py:23
msgid ""
"Switches your glasses into side-by-side mode and doubles the width of the "
"display."
@@ -26,10 +26,56 @@ msgstr ""
"Altera o modo dos óculos para lado a lado (SBS) e dobra a largura da tela "
"(ultrawide)."
-#: src/connecteddevice.py:18
+#: src/connecteddevice.py:24
msgid "This feature is not currently supported for your device."
msgstr "Este recurso não é atualmente suportado para o seu dispositivo."
+#: src/connecteddevice.py:124
+#, fuzzy
+msgid "Set Focused Display Distance"
+msgstr "Distância da tela"
+
+#: src/connecteddevice.py:125
+msgid "Use a closer value so the display zooms in when you look at it."
+msgstr ""
+
+#: src/connecteddevice.py:132
+#, fuzzy
+msgid "Set All Displays Distance"
+msgstr "Distância da tela"
+
+#: src/connecteddevice.py:133
+msgid "Use a farther value so the displays are zoomed out when you look away."
+msgstr ""
+
+#: src/connecteddevice.py:231
+msgid ""
+"Unable to add virtual displays on this machine. xdg-desktop-portal is "
+"required."
+msgstr ""
+
+#: src/connecteddevice.py:265
+#, fuzzy
+msgid "Focused display"
+msgstr "Distância da tela"
+
+#: src/connecteddevice.py:271
+#, fuzzy
+msgid "All displays"
+msgstr "Tela curva"
+
+#: src/displaydistancedialogcontent.py:41
+msgid "closer"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:45
+msgid "fullscreen"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:48
+msgid "farther"
+msgstr ""
+
#: src/licensedialogcontent.py:63
msgid "Paid Tier Status"
msgstr "Status do Nível Pago"
@@ -38,7 +84,7 @@ msgstr "Status do Nível Pago"
msgid "Feature Availability"
msgstr "Disponibilidade de Recursos"
-#: src/licensefeaturerow.py:15 src/shortcutdialog.py:107
+#: src/licensefeaturerow.py:15 src/shortcutdialog.py:104
msgid "Disabled"
msgstr "Desabilitado"
@@ -167,125 +213,183 @@ msgid "Enables the Breezy Desktop XR effect."
msgstr "Habilita o efeito XR do Breezy Desktop."
#: src/gtk/connected-device.ui:55
-msgid "Widescreen mode"
-msgstr "Modo Ultrawide"
+msgid "Zoom on focus"
+msgstr ""
-#: src/gtk/connected-device.ui:66
+#: src/gtk/connected-device.ui:56
+msgid ""
+"Automatically move a display closer when you look at it.\n"
+"Set your preferred focused and unfocused distances in the Adjustments "
+"section.\n"
+" "
+msgstr ""
+
+#: src/gtk/connected-device.ui:69
msgid "Follow mode"
msgstr "Modo de acompanhamento"
-#: src/gtk/connected-device.ui:67
-msgid "Keep the virtual display near the center of your view."
+#: src/gtk/connected-device.ui:70
+#, fuzzy
+msgid "Keep the focused display near the center of your view."
msgstr "Mantenha a tela virtual próxima ao centro da sua visão."
-#: src/gtk/connected-device.ui:77
-msgid "Curved display"
-msgstr "Tela curva"
+#: src/gtk/connected-device.ui:80 src/gtk/no-device.ui:34
+msgid "Disable physical displays"
+msgstr ""
-#: src/gtk/connected-device.ui:78
-msgid "Switch between flat and curved displays."
-msgstr "Alterne entre tela plana e tela curva."
+#: src/gtk/connected-device.ui:81 src/gtk/no-device.ui:35
+msgid ""
+"Automatically disable all physical displays when the XR effect is enabled."
+msgstr ""
#: src/gtk/connected-device.ui:91
+#, fuzzy
+msgid "Virtual displays"
+msgstr "Tela curva"
+
+#: src/gtk/connected-device.ui:103
+#, fuzzy
+msgid "Rearrange displays"
+msgstr "Tela curva"
+
+#: src/gtk/connected-device.ui:115
+msgid "1080p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:116
+msgid "1440p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:140
msgid "Adjustments"
msgstr "Ajustes"
-#: src/gtk/connected-device.ui:94
-msgid "Display distance"
+#: src/gtk/connected-device.ui:143
+#, fuzzy
+msgid "Display distances"
msgstr "Distância da tela"
-#: src/gtk/connected-device.ui:95
-msgid ""
-"Closer appears larger, further appears smaller. Controls depth when in "
-"widescreen mode."
+#: src/gtk/connected-device.ui:144
+msgid "Set how close you want displays to appear."
msgstr ""
-"Mais perto parece maior, mais longe parece menor. Controla a profundidade no "
-"modo ultrawide."
-#: src/gtk/connected-device.ui:123
-msgid "Display size"
-msgstr "Tamanho da tela"
-
-#: src/gtk/connected-device.ui:124
-msgid ""
-"Combine with display distance to achieve a comfortable level of depth and "
-"size."
-msgstr ""
-"Combine com a distância da tela para alcançar um nível confortável de "
-"profundidade e tamanho."
-
-#: src/gtk/connected-device.ui:152
-msgid "Display toggle distances"
-msgstr "Alternância das distâncias de tela"
-
-#: src/gtk/connected-device.ui:153
-msgid ""
-"Use the buttons to capture the current display distance for use with the "
-"keyboard shortcut."
-msgstr ""
-"Use os botões para obter a distância atual da tela para depois usar com o "
-"atalho de teclado."
-
-#: src/gtk/connected-device.ui:178
+#: src/gtk/connected-device.ui:199
msgid "Follow threshold"
msgstr "Sensibilidade do modo de acompanhamento"
-#: src/gtk/connected-device.ui:179
+#: src/gtk/connected-device.ui:200
msgid "How far away you can look before the display follows."
msgstr "Quanto você pode mover a cabeça antes que a tela te acompanhe."
-#: src/gtk/connected-device.ui:209 src/gtk/connected-device.ui:215
+#: src/gtk/connected-device.ui:223
+#, fuzzy
+msgid "Display angling"
+msgstr "Tamanho da tela"
+
+#: src/gtk/connected-device.ui:224
+msgid ""
+"When there are multiple displays, choose how they should angle towards you."
+msgstr ""
+
+#: src/gtk/connected-device.ui:238
+msgid "Automatic"
+msgstr ""
+
+#: src/gtk/connected-device.ui:239
+msgid "Side-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:240
+msgid "Top-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:241
+msgid "Flat"
+msgstr ""
+
+#: src/gtk/connected-device.ui:251
+#, fuzzy
+msgid "Display spacing"
+msgstr "Tamanho da tela"
+
+#: src/gtk/connected-device.ui:252
+msgid "Put empty space between displays, when there are multiple."
+msgstr ""
+
+#: src/gtk/connected-device.ui:282
+msgid "Viewport horizontal offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:283
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport to the left or right."
+msgstr ""
+
+#: src/gtk/connected-device.ui:312
+msgid "Viewport vertical offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:313
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport up or down."
+msgstr ""
+
+#: src/gtk/connected-device.ui:349 src/gtk/connected-device.ui:355
msgid "Keyboard Shortcuts"
msgstr "Atalhos de teclado"
-#: src/gtk/connected-device.ui:218
+#: src/gtk/connected-device.ui:358
msgid "XR Effect on/off shortcut"
msgstr ""
-#: src/gtk/connected-device.ui:219
+#: src/gtk/connected-device.ui:359
msgid ""
"Quickly enable or disable the XR Effect. You may need to enable the effect "
"manually once in order to enable the shortcut."
msgstr ""
-#: src/gtk/connected-device.ui:238 src/gtk/connected-device.ui:267
-#: src/gtk/connected-device.ui:296 src/gtk/connected-device.ui:325
+#: src/gtk/connected-device.ui:378 src/gtk/connected-device.ui:407
+#: src/gtk/connected-device.ui:436 src/gtk/connected-device.ui:465
msgid "Change"
msgstr "Alterar"
-#: src/gtk/connected-device.ui:247
+#: src/gtk/connected-device.ui:387
msgid "Re-center display shortcut"
msgstr "Atalho para recentralizar a tela"
-#: src/gtk/connected-device.ui:248
+#: src/gtk/connected-device.ui:388
msgid "Pin the virtual display to the current position."
msgstr "Traga a tela virtual para onde você está olhando."
-#: src/gtk/connected-device.ui:276
-msgid "Display distance shortcut"
-msgstr "Atalho de distância da tela"
+#: src/gtk/connected-device.ui:416
+#, fuzzy
+msgid "Toggle zoom on focus shortcut"
+msgstr "Atalho para alternar o modo de acompanhamento"
-#: src/gtk/connected-device.ui:277
-msgid "Quickly toggle between two predefined distances."
-msgstr "Alterne rapidamente entre as duas distâncias predefinidas."
+#: src/gtk/connected-device.ui:417
+#, fuzzy
+msgid "Quickly toggle zoom on focus mode."
+msgstr "Alterne rapidamente o modo de acompanhamento."
-#: src/gtk/connected-device.ui:305
+#: src/gtk/connected-device.ui:445
msgid "Toggle follow mode shortcut"
msgstr "Atalho para alternar o modo de acompanhamento"
-#: src/gtk/connected-device.ui:306
+#: src/gtk/connected-device.ui:446
msgid "Quickly toggle follow mode."
msgstr "Alterne rapidamente o modo de acompanhamento."
-#: src/gtk/connected-device.ui:341 src/gtk/connected-device.ui:347
+#: src/gtk/connected-device.ui:481 src/gtk/connected-device.ui:487
msgid "Advanced Settings"
msgstr "Configurações Avançadas"
-#: src/gtk/connected-device.ui:350
+#: src/gtk/connected-device.ui:490
msgid "Find optimal display config"
msgstr "Encontre a configuração de tela ideal"
-#: src/gtk/connected-device.ui:351
+#: src/gtk/connected-device.ui:491
msgid ""
"Automatically modify the glasses display configuration for maximum "
"resolution and best scaling when plugged in."
@@ -293,42 +397,30 @@ msgstr ""
"Modifique automaticamente a configuração de exibição dos óculos para máxima "
"resolução e melhor dimensionamento quando conectado."
-#: src/gtk/connected-device.ui:361
+#: src/gtk/connected-device.ui:501
msgid "Use highest refresh rate"
msgstr "Use a taxa de atualização mais alta"
-#: src/gtk/connected-device.ui:362
+#: src/gtk/connected-device.ui:502
msgid "Refresh rate may affect performance, disable this to set it manually."
msgstr ""
"A taxa de atualização pode afetar o desempenho, desative-a para defini-la "
"manualmente."
-#: src/gtk/connected-device.ui:372
+#: src/gtk/connected-device.ui:512
msgid "Always primary display"
msgstr "Sempre tela principal"
-#: src/gtk/connected-device.ui:373
+#: src/gtk/connected-device.ui:513
msgid "Automatically set the glasses as the primary display when plugged in."
msgstr ""
"Defina automaticamente os óculos como a tela primária quando conectados."
-#: src/gtk/connected-device.ui:383
-msgid "Fast SBS mode switching"
-msgstr "Alternância rápida do modo SBS"
-
-#: src/gtk/connected-device.ui:384
-msgid ""
-"Switches glasses to SBS mode immediately when plugged in, if widescreen mode "
-"is on. May cause instability."
-msgstr ""
-"Altera os óculos para o modo SBS imediatamente quando conectados, se o modo "
-"ultrawide estiver ativado. Pode causar instabilidade."
-
-#: src/gtk/connected-device.ui:394
+#: src/gtk/connected-device.ui:523
msgid "Movement look-ahead"
msgstr "Antecipação de movimento"
-#: src/gtk/connected-device.ui:395
+#: src/gtk/connected-device.ui:524
msgid ""
"Counteracts input lag by predicting head-tracking position ahead of render "
"time. Stick with default unless virtual display drags behind your head "
@@ -339,19 +431,35 @@ msgstr ""
"virtual tenha atrasos, avance ou seja muito instável em relação aos "
"movimentos da cabeça "
-#: src/gtk/connected-device.ui:413
+#: src/gtk/connected-device.ui:542
msgid "Default"
msgstr "Padrão"
-#: src/gtk/connected-device.ui:425
+#: src/gtk/connected-device.ui:554
msgid "Text Scaling"
msgstr "Redimensionamento de Texto"
-#: src/gtk/connected-device.ui:426
+#: src/gtk/connected-device.ui:555
msgid "Scaling text below 1.0 will simulate a higher resolution display"
msgstr ""
"Redimensionar o texto abaixo de 1.0 simulará uma tela de resolução mais alta"
+#: src/gtk/connected-device.ui:585
+msgid "Enable multi-tap detection"
+msgstr ""
+
+#: src/gtk/connected-device.ui:586
+msgid "Enables double-tap to recenter and triple-tap to recalibrate."
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:9
+msgid "Show full range"
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:18
+msgid "Done"
+msgstr ""
+
#: src/gtk/failed-verification.ui:13
msgid "Breezy Desktop GNOME invalid setup"
msgstr "Configuração inválida do Breezy Desktop GNOME"
@@ -401,15 +509,6 @@ msgid ""
"connected."
msgstr ""
-#: src/gtk/no-device.ui:34
-#, fuzzy
-msgid "Start in widescreen mode"
-msgstr "Modo Ultrawide"
-
-#: src/gtk/no-device.ui:35
-msgid "Widescreen mode is not supported for all glasses."
-msgstr ""
-
#: src/gtk/no-driver.ui:13
msgid "No driver running"
msgstr "Nenhum driver em execução"
@@ -516,3 +615,57 @@ msgstr "Forçar redefinição"
#: src/gtk/window.ui:99
msgid "About BreezyDesktop"
msgstr "Sobre o BreezyDesktop"
+
+#~ msgid "Fast SBS mode switching"
+#~ msgstr "Alternância rápida do modo SBS"
+
+#~ msgid ""
+#~ "Switches glasses to SBS mode immediately when plugged in, if widescreen "
+#~ "mode is on. May cause instability."
+#~ msgstr ""
+#~ "Altera os óculos para o modo SBS imediatamente quando conectados, se o "
+#~ "modo ultrawide estiver ativado. Pode causar instabilidade."
+
+#, fuzzy
+#~ msgid "Set Unfocused Displays Distance"
+#~ msgstr "Distância da tela"
+
+#~ msgid "Widescreen mode"
+#~ msgstr "Modo Ultrawide"
+
+#~ msgid "Switch between flat and curved displays."
+#~ msgstr "Alterne entre tela plana e tela curva."
+
+#~ msgid ""
+#~ "Closer appears larger, further appears smaller. Controls depth when in "
+#~ "widescreen mode."
+#~ msgstr ""
+#~ "Mais perto parece maior, mais longe parece menor. Controla a profundidade "
+#~ "no modo ultrawide."
+
+#~ msgid ""
+#~ "Combine with display distance to achieve a comfortable level of depth and "
+#~ "size."
+#~ msgstr ""
+#~ "Combine com a distância da tela para alcançar um nível confortável de "
+#~ "profundidade e tamanho."
+
+#~ msgid "Display toggle distances"
+#~ msgstr "Alternância das distâncias de tela"
+
+#~ msgid ""
+#~ "Use the buttons to capture the current display distance for use with the "
+#~ "keyboard shortcut."
+#~ msgstr ""
+#~ "Use os botões para obter a distância atual da tela para depois usar com o "
+#~ "atalho de teclado."
+
+#~ msgid "Display distance shortcut"
+#~ msgstr "Atalho de distância da tela"
+
+#~ msgid "Quickly toggle between two predefined distances."
+#~ msgstr "Alterne rapidamente entre as duas distâncias predefinidas."
+
+#, fuzzy
+#~ msgid "Start in widescreen mode"
+#~ msgstr "Modo Ultrawide"
diff --git a/ui/po/ru.po b/ui/po/ru.po
index da7815e..2df8733 100644
--- a/ui/po/ru.po
+++ b/ui/po/ru.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-22 13:27-0700\n"
+"POT-Creation-Date: 2025-03-05 12:28-0800\n"
"PO-Revision-Date: 2024-08-17 09:39-0700\n"
"Last-Translator: \n"
"Language-Team: Russian \n"
@@ -18,17 +18,63 @@ msgstr ""
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
-#: src/connecteddevice.py:17
+#: src/connecteddevice.py:23
msgid ""
"Switches your glasses into side-by-side mode and doubles the width of the "
"display."
msgstr ""
"Переключает очки в режим «сторона к стороне» и удваивает ширину дисплея."
-#: src/connecteddevice.py:18
+#: src/connecteddevice.py:24
msgid "This feature is not currently supported for your device."
msgstr "Эта функция в настоящее время не поддерживается для вашего устройства."
+#: src/connecteddevice.py:124
+#, fuzzy
+msgid "Set Focused Display Distance"
+msgstr "Расстояние дисплея"
+
+#: src/connecteddevice.py:125
+msgid "Use a closer value so the display zooms in when you look at it."
+msgstr ""
+
+#: src/connecteddevice.py:132
+#, fuzzy
+msgid "Set All Displays Distance"
+msgstr "Расстояние дисплея"
+
+#: src/connecteddevice.py:133
+msgid "Use a farther value so the displays are zoomed out when you look away."
+msgstr ""
+
+#: src/connecteddevice.py:231
+msgid ""
+"Unable to add virtual displays on this machine. xdg-desktop-portal is "
+"required."
+msgstr ""
+
+#: src/connecteddevice.py:265
+#, fuzzy
+msgid "Focused display"
+msgstr "Расстояние дисплея"
+
+#: src/connecteddevice.py:271
+#, fuzzy
+msgid "All displays"
+msgstr "Изогнутый дисплей"
+
+#: src/displaydistancedialogcontent.py:41
+msgid "closer"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:45
+msgid "fullscreen"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:48
+msgid "farther"
+msgstr ""
+
#: src/licensedialogcontent.py:63
msgid "Paid Tier Status"
msgstr "Статус платного уровня"
@@ -37,7 +83,7 @@ msgstr "Статус платного уровня"
msgid "Feature Availability"
msgstr "Статус функций"
-#: src/licensefeaturerow.py:15 src/shortcutdialog.py:107
+#: src/licensefeaturerow.py:15 src/shortcutdialog.py:104
msgid "Disabled"
msgstr "Отключено"
@@ -166,126 +212,184 @@ msgid "Enables the Breezy Desktop XR effect."
msgstr "Включает эффект Breezy Desktop XR."
#: src/gtk/connected-device.ui:55
-msgid "Widescreen mode"
-msgstr "Режим широкого экрана"
+msgid "Zoom on focus"
+msgstr ""
-#: src/gtk/connected-device.ui:66
+#: src/gtk/connected-device.ui:56
+msgid ""
+"Automatically move a display closer when you look at it.\n"
+"Set your preferred focused and unfocused distances in the Adjustments "
+"section.\n"
+" "
+msgstr ""
+
+#: src/gtk/connected-device.ui:69
msgid "Follow mode"
msgstr "Режим следования"
-#: src/gtk/connected-device.ui:67
-msgid "Keep the virtual display near the center of your view."
+#: src/gtk/connected-device.ui:70
+#, fuzzy
+msgid "Keep the focused display near the center of your view."
msgstr "Поддерживает виртуальный дисплей вблизи центра вашего зрения."
-#: src/gtk/connected-device.ui:77
-msgid "Curved display"
-msgstr "Изогнутый дисплей"
+#: src/gtk/connected-device.ui:80 src/gtk/no-device.ui:34
+msgid "Disable physical displays"
+msgstr ""
-#: src/gtk/connected-device.ui:78
-msgid "Switch between flat and curved displays."
-msgstr "Переключается между плоскими и изогнутыми дисплеями."
+#: src/gtk/connected-device.ui:81 src/gtk/no-device.ui:35
+msgid ""
+"Automatically disable all physical displays when the XR effect is enabled."
+msgstr ""
#: src/gtk/connected-device.ui:91
+#, fuzzy
+msgid "Virtual displays"
+msgstr "Изогнутый дисплей"
+
+#: src/gtk/connected-device.ui:103
+#, fuzzy
+msgid "Rearrange displays"
+msgstr "Изогнутый дисплей"
+
+#: src/gtk/connected-device.ui:115
+msgid "1080p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:116
+msgid "1440p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:140
msgid "Adjustments"
msgstr "Настройки"
-#: src/gtk/connected-device.ui:94
-msgid "Display distance"
+#: src/gtk/connected-device.ui:143
+#, fuzzy
+msgid "Display distances"
msgstr "Расстояние дисплея"
-#: src/gtk/connected-device.ui:95
-msgid ""
-"Closer appears larger, further appears smaller. Controls depth when in "
-"widescreen mode."
+#: src/gtk/connected-device.ui:144
+msgid "Set how close you want displays to appear."
msgstr ""
-"Чем ближе, тем больше. Чем дальше, тем меньше. Управляет глубиной при "
-"использовании режима широкого экрана."
-#: src/gtk/connected-device.ui:123
-msgid "Display size"
-msgstr "Размер дисплея"
-
-#: src/gtk/connected-device.ui:124
-msgid ""
-"Combine with display distance to achieve a comfortable level of depth and "
-"size."
-msgstr ""
-"Комбинируйте с расстоянием дисплея, чтобы достичь комфортного уровня глубины "
-"и размера."
-
-#: src/gtk/connected-device.ui:152
-msgid "Display toggle distances"
-msgstr "Переключения расстояние дисплея"
-
-#: src/gtk/connected-device.ui:153
-msgid ""
-"Use the buttons to capture the current display distance for use with the "
-"keyboard shortcut."
-msgstr ""
-"Используйте кнопки, чтобы зафиксировать текущее расстояние дисплея для "
-"использования с сочетанием клавиш."
-
-#: src/gtk/connected-device.ui:178
+#: src/gtk/connected-device.ui:199
msgid "Follow threshold"
msgstr "Порог следования"
-#: src/gtk/connected-device.ui:179
+#: src/gtk/connected-device.ui:200
msgid "How far away you can look before the display follows."
msgstr ""
"Как далеко вы можете отвернуть взгляд, прежде чем дисплей последует за вами."
-#: src/gtk/connected-device.ui:209 src/gtk/connected-device.ui:215
+#: src/gtk/connected-device.ui:223
+#, fuzzy
+msgid "Display angling"
+msgstr "Размер дисплея"
+
+#: src/gtk/connected-device.ui:224
+msgid ""
+"When there are multiple displays, choose how they should angle towards you."
+msgstr ""
+
+#: src/gtk/connected-device.ui:238
+msgid "Automatic"
+msgstr ""
+
+#: src/gtk/connected-device.ui:239
+msgid "Side-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:240
+msgid "Top-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:241
+msgid "Flat"
+msgstr ""
+
+#: src/gtk/connected-device.ui:251
+#, fuzzy
+msgid "Display spacing"
+msgstr "Размер дисплея"
+
+#: src/gtk/connected-device.ui:252
+msgid "Put empty space between displays, when there are multiple."
+msgstr ""
+
+#: src/gtk/connected-device.ui:282
+msgid "Viewport horizontal offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:283
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport to the left or right."
+msgstr ""
+
+#: src/gtk/connected-device.ui:312
+msgid "Viewport vertical offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:313
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport up or down."
+msgstr ""
+
+#: src/gtk/connected-device.ui:349 src/gtk/connected-device.ui:355
msgid "Keyboard Shortcuts"
msgstr "Сочетания клавиш"
-#: src/gtk/connected-device.ui:218
+#: src/gtk/connected-device.ui:358
msgid "XR Effect on/off shortcut"
msgstr ""
-#: src/gtk/connected-device.ui:219
+#: src/gtk/connected-device.ui:359
msgid ""
"Quickly enable or disable the XR Effect. You may need to enable the effect "
"manually once in order to enable the shortcut."
msgstr ""
-#: src/gtk/connected-device.ui:238 src/gtk/connected-device.ui:267
-#: src/gtk/connected-device.ui:296 src/gtk/connected-device.ui:325
+#: src/gtk/connected-device.ui:378 src/gtk/connected-device.ui:407
+#: src/gtk/connected-device.ui:436 src/gtk/connected-device.ui:465
msgid "Change"
msgstr "Изменить"
-#: src/gtk/connected-device.ui:247
+#: src/gtk/connected-device.ui:387
msgid "Re-center display shortcut"
msgstr "Сочетание клавиш для перецентровки дисплея"
-#: src/gtk/connected-device.ui:248
+#: src/gtk/connected-device.ui:388
msgid "Pin the virtual display to the current position."
msgstr "Фиксировать виртуальный дисплей в текущем положении."
-#: src/gtk/connected-device.ui:276
-msgid "Display distance shortcut"
-msgstr "Сочетание клавиш для расстояния дисплея"
+#: src/gtk/connected-device.ui:416
+#, fuzzy
+msgid "Toggle zoom on focus shortcut"
+msgstr "Сочетание клавиш для переключения режима следования"
-#: src/gtk/connected-device.ui:277
-msgid "Quickly toggle between two predefined distances."
-msgstr "Быстро переключаться между двумя предопределенными расстояниями."
+#: src/gtk/connected-device.ui:417
+#, fuzzy
+msgid "Quickly toggle zoom on focus mode."
+msgstr "Быстро переключать режим следования."
-#: src/gtk/connected-device.ui:305
+#: src/gtk/connected-device.ui:445
msgid "Toggle follow mode shortcut"
msgstr "Сочетание клавиш для переключения режима следования"
-#: src/gtk/connected-device.ui:306
+#: src/gtk/connected-device.ui:446
msgid "Quickly toggle follow mode."
msgstr "Быстро переключать режим следования."
-#: src/gtk/connected-device.ui:341 src/gtk/connected-device.ui:347
+#: src/gtk/connected-device.ui:481 src/gtk/connected-device.ui:487
msgid "Advanced Settings"
msgstr "Дополнительные настройки"
-#: src/gtk/connected-device.ui:350
+#: src/gtk/connected-device.ui:490
msgid "Find optimal display config"
msgstr "Найти оптимальную конфигурацию дисплея"
-#: src/gtk/connected-device.ui:351
+#: src/gtk/connected-device.ui:491
msgid ""
"Automatically modify the glasses display configuration for maximum "
"resolution and best scaling when plugged in."
@@ -293,43 +397,31 @@ msgstr ""
"Автоматически изменять конфигурацию дисплея очков для максимального "
"разрешения и лучшей масштабирования при подключении."
-#: src/gtk/connected-device.ui:361
+#: src/gtk/connected-device.ui:501
msgid "Use highest refresh rate"
msgstr "Использовать высшую частоту обновления"
-#: src/gtk/connected-device.ui:362
+#: src/gtk/connected-device.ui:502
msgid "Refresh rate may affect performance, disable this to set it manually."
msgstr ""
"Частота обновления может повлиять на производительность, отключите это, "
"чтобы установить ее вручную."
-#: src/gtk/connected-device.ui:372
+#: src/gtk/connected-device.ui:512
msgid "Always primary display"
msgstr "Всегда основной дисплей"
-#: src/gtk/connected-device.ui:373
+#: src/gtk/connected-device.ui:513
msgid "Automatically set the glasses as the primary display when plugged in."
msgstr ""
"Автоматически устанавливать очки в качестве основного дисплея при "
"подключении."
-#: src/gtk/connected-device.ui:383
-msgid "Fast SBS mode switching"
-msgstr "Быстрое переключение режима SBS"
-
-#: src/gtk/connected-device.ui:384
-msgid ""
-"Switches glasses to SBS mode immediately when plugged in, if widescreen mode "
-"is on. May cause instability."
-msgstr ""
-"Переключает очки в режим SBS сразу при подключении, если режим широкого "
-"экрана включен. Может вызвать нестабильность."
-
-#: src/gtk/connected-device.ui:394
+#: src/gtk/connected-device.ui:523
msgid "Movement look-ahead"
msgstr "Прогнозирование движения"
-#: src/gtk/connected-device.ui:395
+#: src/gtk/connected-device.ui:524
msgid ""
"Counteracts input lag by predicting head-tracking position ahead of render "
"time. Stick with default unless virtual display drags behind your head "
@@ -340,18 +432,34 @@ msgstr ""
"виртуальный дисплей не отстает от движений вашей головы, не опережает или не "
"очень трясётся."
-#: src/gtk/connected-device.ui:413
+#: src/gtk/connected-device.ui:542
msgid "Default"
msgstr "По умолчанию"
-#: src/gtk/connected-device.ui:425
+#: src/gtk/connected-device.ui:554
msgid "Text Scaling"
msgstr ""
-#: src/gtk/connected-device.ui:426
+#: src/gtk/connected-device.ui:555
msgid "Scaling text below 1.0 will simulate a higher resolution display"
msgstr ""
+#: src/gtk/connected-device.ui:585
+msgid "Enable multi-tap detection"
+msgstr ""
+
+#: src/gtk/connected-device.ui:586
+msgid "Enables double-tap to recenter and triple-tap to recalibrate."
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:9
+msgid "Show full range"
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:18
+msgid "Done"
+msgstr ""
+
#: src/gtk/failed-verification.ui:13
msgid "Breezy Desktop GNOME invalid setup"
msgstr "Недействительная настройка Breezy Desktop GNOME"
@@ -400,15 +508,6 @@ msgid ""
"connected."
msgstr ""
-#: src/gtk/no-device.ui:34
-#, fuzzy
-msgid "Start in widescreen mode"
-msgstr "Режим широкого экрана"
-
-#: src/gtk/no-device.ui:35
-msgid "Widescreen mode is not supported for all glasses."
-msgstr ""
-
#: src/gtk/no-driver.ui:13
msgid "No driver running"
msgstr "Драйвер не запущен"
@@ -517,3 +616,57 @@ msgstr "Сброс"
#: src/gtk/window.ui:99
msgid "About BreezyDesktop"
msgstr "О BreezyDesktop"
+
+#~ msgid "Fast SBS mode switching"
+#~ msgstr "Быстрое переключение режима SBS"
+
+#~ msgid ""
+#~ "Switches glasses to SBS mode immediately when plugged in, if widescreen "
+#~ "mode is on. May cause instability."
+#~ msgstr ""
+#~ "Переключает очки в режим SBS сразу при подключении, если режим широкого "
+#~ "экрана включен. Может вызвать нестабильность."
+
+#, fuzzy
+#~ msgid "Set Unfocused Displays Distance"
+#~ msgstr "Расстояние дисплея"
+
+#~ msgid "Widescreen mode"
+#~ msgstr "Режим широкого экрана"
+
+#~ msgid "Switch between flat and curved displays."
+#~ msgstr "Переключается между плоскими и изогнутыми дисплеями."
+
+#~ msgid ""
+#~ "Closer appears larger, further appears smaller. Controls depth when in "
+#~ "widescreen mode."
+#~ msgstr ""
+#~ "Чем ближе, тем больше. Чем дальше, тем меньше. Управляет глубиной при "
+#~ "использовании режима широкого экрана."
+
+#~ msgid ""
+#~ "Combine with display distance to achieve a comfortable level of depth and "
+#~ "size."
+#~ msgstr ""
+#~ "Комбинируйте с расстоянием дисплея, чтобы достичь комфортного уровня "
+#~ "глубины и размера."
+
+#~ msgid "Display toggle distances"
+#~ msgstr "Переключения расстояние дисплея"
+
+#~ msgid ""
+#~ "Use the buttons to capture the current display distance for use with the "
+#~ "keyboard shortcut."
+#~ msgstr ""
+#~ "Используйте кнопки, чтобы зафиксировать текущее расстояние дисплея для "
+#~ "использования с сочетанием клавиш."
+
+#~ msgid "Display distance shortcut"
+#~ msgstr "Сочетание клавиш для расстояния дисплея"
+
+#~ msgid "Quickly toggle between two predefined distances."
+#~ msgstr "Быстро переключаться между двумя предопределенными расстояниями."
+
+#, fuzzy
+#~ msgid "Start in widescreen mode"
+#~ msgstr "Режим широкого экрана"
diff --git a/ui/po/sv.po b/ui/po/sv.po
index dd5fc39..07fc116 100644
--- a/ui/po/sv.po
+++ b/ui/po/sv.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-22 13:27-0700\n"
+"POT-Creation-Date: 2025-03-05 12:28-0800\n"
"PO-Revision-Date: 2024-08-16 10:31-0700\n"
"Last-Translator: \n"
"Language-Team: Swedish \n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: src/connecteddevice.py:17
+#: src/connecteddevice.py:23
msgid ""
"Switches your glasses into side-by-side mode and doubles the width of the "
"display."
@@ -25,10 +25,56 @@ msgstr ""
"Växlar dina glasögon till side-by-side-läget och dubblerar bredden på "
"visningen."
-#: src/connecteddevice.py:18
+#: src/connecteddevice.py:24
msgid "This feature is not currently supported for your device."
msgstr "Din enhet stöder inte den här funktionen för tillfället."
+#: src/connecteddevice.py:124
+#, fuzzy
+msgid "Set Focused Display Distance"
+msgstr "Avstånd till skärmen"
+
+#: src/connecteddevice.py:125
+msgid "Use a closer value so the display zooms in when you look at it."
+msgstr ""
+
+#: src/connecteddevice.py:132
+#, fuzzy
+msgid "Set All Displays Distance"
+msgstr "Avstånd till skärmen"
+
+#: src/connecteddevice.py:133
+msgid "Use a farther value so the displays are zoomed out when you look away."
+msgstr ""
+
+#: src/connecteddevice.py:231
+msgid ""
+"Unable to add virtual displays on this machine. xdg-desktop-portal is "
+"required."
+msgstr ""
+
+#: src/connecteddevice.py:265
+#, fuzzy
+msgid "Focused display"
+msgstr "Avstånd till skärmen"
+
+#: src/connecteddevice.py:271
+#, fuzzy
+msgid "All displays"
+msgstr "Böjd skärm"
+
+#: src/displaydistancedialogcontent.py:41
+msgid "closer"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:45
+msgid "fullscreen"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:48
+msgid "farther"
+msgstr ""
+
#: src/licensedialogcontent.py:63
msgid "Paid Tier Status"
msgstr "Betalningsstatus"
@@ -37,7 +83,7 @@ msgstr "Betalningsstatus"
msgid "Feature Availability"
msgstr "Funktions tillgänglighet"
-#: src/licensefeaturerow.py:15 src/shortcutdialog.py:107
+#: src/licensefeaturerow.py:15 src/shortcutdialog.py:104
msgid "Disabled"
msgstr "Inaktiverad"
@@ -166,82 +212,138 @@ msgid "Enables the Breezy Desktop XR effect."
msgstr "Aktiverar Breezy Desktop XR-effekten."
#: src/gtk/connected-device.ui:55
-msgid "Widescreen mode"
-msgstr "Bredbildsläge"
+msgid "Zoom on focus"
+msgstr ""
-#: src/gtk/connected-device.ui:66
+#: src/gtk/connected-device.ui:56
+msgid ""
+"Automatically move a display closer when you look at it.\n"
+"Set your preferred focused and unfocused distances in the Adjustments "
+"section.\n"
+" "
+msgstr ""
+
+#: src/gtk/connected-device.ui:69
msgid "Follow mode"
msgstr "Följningsläge"
-#: src/gtk/connected-device.ui:67
-msgid "Keep the virtual display near the center of your view."
+#: src/gtk/connected-device.ui:70
+#, fuzzy
+msgid "Keep the focused display near the center of your view."
msgstr "Håll den virtuella skärmen nära mitten av din syn."
-#: src/gtk/connected-device.ui:77
-msgid "Curved display"
-msgstr "Böjd skärm"
+#: src/gtk/connected-device.ui:80 src/gtk/no-device.ui:34
+msgid "Disable physical displays"
+msgstr ""
-#: src/gtk/connected-device.ui:78
-msgid "Switch between flat and curved displays."
-msgstr "Växla mellan platt eller böjd skärm."
+#: src/gtk/connected-device.ui:81 src/gtk/no-device.ui:35
+msgid ""
+"Automatically disable all physical displays when the XR effect is enabled."
+msgstr ""
#: src/gtk/connected-device.ui:91
+#, fuzzy
+msgid "Virtual displays"
+msgstr "Böjd skärm"
+
+#: src/gtk/connected-device.ui:103
+#, fuzzy
+msgid "Rearrange displays"
+msgstr "Böjd skärm"
+
+#: src/gtk/connected-device.ui:115
+msgid "1080p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:116
+msgid "1440p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:140
msgid "Adjustments"
msgstr "Justeringar"
-#: src/gtk/connected-device.ui:94
-msgid "Display distance"
+#: src/gtk/connected-device.ui:143
+#, fuzzy
+msgid "Display distances"
msgstr "Avstånd till skärmen"
-#: src/gtk/connected-device.ui:95
-msgid ""
-"Closer appears larger, further appears smaller. Controls depth when in "
-"widescreen mode."
+#: src/gtk/connected-device.ui:144
+msgid "Set how close you want displays to appear."
msgstr ""
-"Närmare verkar större, längre verkar mindre. Kontrollerar djupet när du är i "
-"bredbildsläge."
-#: src/gtk/connected-device.ui:123
-msgid "Display size"
-msgstr "Skärmens storlek"
-
-#: src/gtk/connected-device.ui:124
-msgid ""
-"Combine with display distance to achieve a comfortable level of depth and "
-"size."
-msgstr ""
-"Kombinera med skärmens avstånd för att uppnå en bekväm nivå av djup och "
-"storlek."
-
-#: src/gtk/connected-device.ui:152
-msgid "Display toggle distances"
-msgstr "Växla skärm-avstånd"
-
-#: src/gtk/connected-device.ui:153
-msgid ""
-"Use the buttons to capture the current display distance for use with the "
-"keyboard shortcut."
-msgstr ""
-"Använd knapparna för att fanga nuvarande skärmavståndet för användning med "
-"tangentbordsgenvägar."
-
-#: src/gtk/connected-device.ui:178
+#: src/gtk/connected-device.ui:199
msgid "Follow threshold"
msgstr "Automatisk följtröskel"
-#: src/gtk/connected-device.ui:179
+#: src/gtk/connected-device.ui:200
msgid "How far away you can look before the display follows."
msgstr "Huvudrörelsetröskel för automatisk efterföljning."
-#: src/gtk/connected-device.ui:209 src/gtk/connected-device.ui:215
+#: src/gtk/connected-device.ui:223
+#, fuzzy
+msgid "Display angling"
+msgstr "Skärmens storlek"
+
+#: src/gtk/connected-device.ui:224
+msgid ""
+"When there are multiple displays, choose how they should angle towards you."
+msgstr ""
+
+#: src/gtk/connected-device.ui:238
+msgid "Automatic"
+msgstr ""
+
+#: src/gtk/connected-device.ui:239
+msgid "Side-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:240
+msgid "Top-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:241
+msgid "Flat"
+msgstr ""
+
+#: src/gtk/connected-device.ui:251
+#, fuzzy
+msgid "Display spacing"
+msgstr "Skärmens storlek"
+
+#: src/gtk/connected-device.ui:252
+msgid "Put empty space between displays, when there are multiple."
+msgstr ""
+
+#: src/gtk/connected-device.ui:282
+msgid "Viewport horizontal offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:283
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport to the left or right."
+msgstr ""
+
+#: src/gtk/connected-device.ui:312
+msgid "Viewport vertical offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:313
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport up or down."
+msgstr ""
+
+#: src/gtk/connected-device.ui:349 src/gtk/connected-device.ui:355
msgid "Keyboard Shortcuts"
msgstr "Tangentbordsgenvägar"
-#: src/gtk/connected-device.ui:218
+#: src/gtk/connected-device.ui:358
msgid "XR Effect on/off shortcut"
msgstr "XR-Effekt genväg på/av"
-#: src/gtk/connected-device.ui:219
+#: src/gtk/connected-device.ui:359
msgid ""
"Quickly enable or disable the XR Effect. You may need to enable the effect "
"manually once in order to enable the shortcut."
@@ -249,44 +351,46 @@ msgstr ""
"Skifta snabbt mellan att slå av eller på XR-Effekt. Du kan behöva slå på den "
"manuellt en gång för att genvägen ska fungera."
-#: src/gtk/connected-device.ui:238 src/gtk/connected-device.ui:267
-#: src/gtk/connected-device.ui:296 src/gtk/connected-device.ui:325
+#: src/gtk/connected-device.ui:378 src/gtk/connected-device.ui:407
+#: src/gtk/connected-device.ui:436 src/gtk/connected-device.ui:465
msgid "Change"
msgstr "Ändra"
-#: src/gtk/connected-device.ui:247
+#: src/gtk/connected-device.ui:387
msgid "Re-center display shortcut"
msgstr "Om-centrering tangentbordsgenväg"
-#: src/gtk/connected-device.ui:248
+#: src/gtk/connected-device.ui:388
msgid "Pin the virtual display to the current position."
msgstr "Fäst den virtuella skärmen i den nuvarande positionen."
-#: src/gtk/connected-device.ui:276
-msgid "Display distance shortcut"
-msgstr "Skärm avstånd genväg"
+#: src/gtk/connected-device.ui:416
+#, fuzzy
+msgid "Toggle zoom on focus shortcut"
+msgstr "Växla följ läge kort"
-#: src/gtk/connected-device.ui:277
-msgid "Quickly toggle between two predefined distances."
-msgstr "Växla snabbt mellan två fördefinierade avstånd."
+#: src/gtk/connected-device.ui:417
+#, fuzzy
+msgid "Quickly toggle zoom on focus mode."
+msgstr "Växla snabbt följ läge."
-#: src/gtk/connected-device.ui:305
+#: src/gtk/connected-device.ui:445
msgid "Toggle follow mode shortcut"
msgstr "Växla följ läge kort"
-#: src/gtk/connected-device.ui:306
+#: src/gtk/connected-device.ui:446
msgid "Quickly toggle follow mode."
msgstr "Växla snabbt följ läge."
-#: src/gtk/connected-device.ui:341 src/gtk/connected-device.ui:347
+#: src/gtk/connected-device.ui:481 src/gtk/connected-device.ui:487
msgid "Advanced Settings"
msgstr "Avancerade inställningar"
-#: src/gtk/connected-device.ui:350
+#: src/gtk/connected-device.ui:490
msgid "Find optimal display config"
msgstr "Hitta optimal konfiguration till skärmen"
-#: src/gtk/connected-device.ui:351
+#: src/gtk/connected-device.ui:491
msgid ""
"Automatically modify the glasses display configuration for maximum "
"resolution and best scaling when plugged in."
@@ -294,41 +398,29 @@ msgstr ""
"Ändrar automatisk glasögonens skärmkonfiguration för maximal upplösning och "
"bästa skälning när den är ansluten."
-#: src/gtk/connected-device.ui:361
+#: src/gtk/connected-device.ui:501
msgid "Use highest refresh rate"
msgstr "Använd högsta uppdateringsfrekvens"
-#: src/gtk/connected-device.ui:362
+#: src/gtk/connected-device.ui:502
msgid "Refresh rate may affect performance, disable this to set it manually."
msgstr ""
"Uppdateringsfrekvens kan påverka prestanda, inaktivera detta för att ställa "
"in det manuellt."
-#: src/gtk/connected-device.ui:372
+#: src/gtk/connected-device.ui:512
msgid "Always primary display"
msgstr "Alltid primär skärm"
-#: src/gtk/connected-device.ui:373
+#: src/gtk/connected-device.ui:513
msgid "Automatically set the glasses as the primary display when plugged in."
msgstr "Ställer automatisk glasögon som primär skärm när den är ansluten."
-#: src/gtk/connected-device.ui:383
-msgid "Fast SBS mode switching"
-msgstr "Snabb SBS läge växling"
-
-#: src/gtk/connected-device.ui:384
-msgid ""
-"Switches glasses to SBS mode immediately when plugged in, if widescreen mode "
-"is on. May cause instability."
-msgstr ""
-"Växlar glasögon till SBS läge omedelbart när den är ansluten, om bredbildsläge är "
-"på. Kan orsaka instabilitet."
-
-#: src/gtk/connected-device.ui:394
+#: src/gtk/connected-device.ui:523
msgid "Movement look-ahead"
msgstr "Rörs förväntning"
-#: src/gtk/connected-device.ui:395
+#: src/gtk/connected-device.ui:524
msgid ""
"Counteracts input lag by predicting head-tracking position ahead of render "
"time. Stick with default unless virtual display drags behind your head "
@@ -337,18 +429,34 @@ msgstr ""
"Motverkar ingångsfördröjning genom förutsägelse av huvudrörelser.Behåll "
"standardinställningen om inte skärmen skakar mycket eller rörsig konstigt."
-#: src/gtk/connected-device.ui:413
+#: src/gtk/connected-device.ui:542
msgid "Default"
msgstr "Standard"
-#: src/gtk/connected-device.ui:425
+#: src/gtk/connected-device.ui:554
msgid "Text Scaling"
msgstr "Textskalning"
-#: src/gtk/connected-device.ui:426
+#: src/gtk/connected-device.ui:555
msgid "Scaling text below 1.0 will simulate a higher resolution display"
msgstr "Textskalning under 1.0 kommer att simulera en högre skärmupplösning"
+#: src/gtk/connected-device.ui:585
+msgid "Enable multi-tap detection"
+msgstr ""
+
+#: src/gtk/connected-device.ui:586
+msgid "Enables double-tap to recenter and triple-tap to recalibrate."
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:9
+msgid "Show full range"
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:18
+msgid "Done"
+msgstr ""
+
#: src/gtk/failed-verification.ui:13
msgid "Breezy Desktop GNOME invalid setup"
msgstr "Breezy Desktop GNOME ogiltig inställning"
@@ -397,14 +505,6 @@ msgstr ""
"Aktivera Breezy Desktop XR-effekten automatiskt när glasögon som stöds är "
"anslutna."
-#: src/gtk/no-device.ui:34
-msgid "Start in widescreen mode"
-msgstr "Starta i bredbildsläge"
-
-#: src/gtk/no-device.ui:35
-msgid "Widescreen mode is not supported for all glasses."
-msgstr "Bredbildsläge stöds inte av alla glasögon."
-
#: src/gtk/no-driver.ui:13
msgid "No driver running"
msgstr "Inget drivrutin köres"
@@ -513,3 +613,59 @@ msgstr "Tvinga Reset"
#: src/gtk/window.ui:99
msgid "About BreezyDesktop"
msgstr "Om BreezyDesktop"
+
+#~ msgid "Fast SBS mode switching"
+#~ msgstr "Snabb SBS läge växling"
+
+#~ msgid ""
+#~ "Switches glasses to SBS mode immediately when plugged in, if widescreen "
+#~ "mode is on. May cause instability."
+#~ msgstr ""
+#~ "Växlar glasögon till SBS läge omedelbart när den är ansluten, om "
+#~ "bredbildsläge är på. Kan orsaka instabilitet."
+
+#, fuzzy
+#~ msgid "Set Unfocused Displays Distance"
+#~ msgstr "Avstånd till skärmen"
+
+#~ msgid "Widescreen mode"
+#~ msgstr "Bredbildsläge"
+
+#~ msgid "Switch between flat and curved displays."
+#~ msgstr "Växla mellan platt eller böjd skärm."
+
+#~ msgid ""
+#~ "Closer appears larger, further appears smaller. Controls depth when in "
+#~ "widescreen mode."
+#~ msgstr ""
+#~ "Närmare verkar större, längre verkar mindre. Kontrollerar djupet när du "
+#~ "är i bredbildsläge."
+
+#~ msgid ""
+#~ "Combine with display distance to achieve a comfortable level of depth and "
+#~ "size."
+#~ msgstr ""
+#~ "Kombinera med skärmens avstånd för att uppnå en bekväm nivå av djup och "
+#~ "storlek."
+
+#~ msgid "Display toggle distances"
+#~ msgstr "Växla skärm-avstånd"
+
+#~ msgid ""
+#~ "Use the buttons to capture the current display distance for use with the "
+#~ "keyboard shortcut."
+#~ msgstr ""
+#~ "Använd knapparna för att fanga nuvarande skärmavståndet för användning "
+#~ "med tangentbordsgenvägar."
+
+#~ msgid "Display distance shortcut"
+#~ msgstr "Skärm avstånd genväg"
+
+#~ msgid "Quickly toggle between two predefined distances."
+#~ msgstr "Växla snabbt mellan två fördefinierade avstånd."
+
+#~ msgid "Start in widescreen mode"
+#~ msgstr "Starta i bredbildsläge"
+
+#~ msgid "Widescreen mode is not supported for all glasses."
+#~ msgstr "Bredbildsläge stöds inte av alla glasögon."
diff --git a/ui/po/uk_UA.po b/ui/po/uk_UA.po
index 4189b2f..40acb60 100644
--- a/ui/po/uk_UA.po
+++ b/ui/po/uk_UA.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-22 13:27-0700\n"
+"POT-Creation-Date: 2025-03-05 12:28-0800\n"
"PO-Revision-Date: 2024-08-17 10:08-0700\n"
"Last-Translator: \n"
"Language-Team: Ukrainian \n"
@@ -18,16 +18,62 @@ msgstr ""
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
-#: src/connecteddevice.py:17
+#: src/connecteddevice.py:23
msgid ""
"Switches your glasses into side-by-side mode and doubles the width of the "
"display."
msgstr "Переключає окуляри в режим «бок о бок» і подвоює ширину дисплея."
-#: src/connecteddevice.py:18
+#: src/connecteddevice.py:24
msgid "This feature is not currently supported for your device."
msgstr "Ця функція наразі не підтримується на вашому пристрої."
+#: src/connecteddevice.py:124
+#, fuzzy
+msgid "Set Focused Display Distance"
+msgstr "Відстань дисплея"
+
+#: src/connecteddevice.py:125
+msgid "Use a closer value so the display zooms in when you look at it."
+msgstr ""
+
+#: src/connecteddevice.py:132
+#, fuzzy
+msgid "Set All Displays Distance"
+msgstr "Відстань дисплея"
+
+#: src/connecteddevice.py:133
+msgid "Use a farther value so the displays are zoomed out when you look away."
+msgstr ""
+
+#: src/connecteddevice.py:231
+msgid ""
+"Unable to add virtual displays on this machine. xdg-desktop-portal is "
+"required."
+msgstr ""
+
+#: src/connecteddevice.py:265
+#, fuzzy
+msgid "Focused display"
+msgstr "Відстань дисплея"
+
+#: src/connecteddevice.py:271
+#, fuzzy
+msgid "All displays"
+msgstr "Викривлений дисплей"
+
+#: src/displaydistancedialogcontent.py:41
+msgid "closer"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:45
+msgid "fullscreen"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:48
+msgid "farther"
+msgstr ""
+
#: src/licensedialogcontent.py:63
msgid "Paid Tier Status"
msgstr "Статус платного рівня"
@@ -36,7 +82,7 @@ msgstr "Статус платного рівня"
msgid "Feature Availability"
msgstr "Статус функцій"
-#: src/licensefeaturerow.py:15 src/shortcutdialog.py:107
+#: src/licensefeaturerow.py:15 src/shortcutdialog.py:104
msgid "Disabled"
msgstr "Вимкнено"
@@ -165,127 +211,185 @@ msgid "Enables the Breezy Desktop XR effect."
msgstr "Включає ефект Breezy Desktop XR."
#: src/gtk/connected-device.ui:55
-msgid "Widescreen mode"
-msgstr "Режим широкого екрану"
+msgid "Zoom on focus"
+msgstr ""
-#: src/gtk/connected-device.ui:66
+#: src/gtk/connected-device.ui:56
+msgid ""
+"Automatically move a display closer when you look at it.\n"
+"Set your preferred focused and unfocused distances in the Adjustments "
+"section.\n"
+" "
+msgstr ""
+
+#: src/gtk/connected-device.ui:69
msgid "Follow mode"
msgstr "Режим слідування"
-#: src/gtk/connected-device.ui:67
-msgid "Keep the virtual display near the center of your view."
+#: src/gtk/connected-device.ui:70
+#, fuzzy
+msgid "Keep the focused display near the center of your view."
msgstr "Зберігае віртуальний дисплей поблизу центру вашого зору."
-#: src/gtk/connected-device.ui:77
-msgid "Curved display"
-msgstr "Викривлений дисплей"
+#: src/gtk/connected-device.ui:80 src/gtk/no-device.ui:34
+msgid "Disable physical displays"
+msgstr ""
-#: src/gtk/connected-device.ui:78
-msgid "Switch between flat and curved displays."
-msgstr "Переключается між плоскими і викривленими дисплеями."
+#: src/gtk/connected-device.ui:81 src/gtk/no-device.ui:35
+msgid ""
+"Automatically disable all physical displays when the XR effect is enabled."
+msgstr ""
#: src/gtk/connected-device.ui:91
+#, fuzzy
+msgid "Virtual displays"
+msgstr "Викривлений дисплей"
+
+#: src/gtk/connected-device.ui:103
+#, fuzzy
+msgid "Rearrange displays"
+msgstr "Викривлений дисплей"
+
+#: src/gtk/connected-device.ui:115
+msgid "1080p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:116
+msgid "1440p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:140
msgid "Adjustments"
msgstr "Налаштування"
-#: src/gtk/connected-device.ui:94
-msgid "Display distance"
+#: src/gtk/connected-device.ui:143
+#, fuzzy
+msgid "Display distances"
msgstr "Відстань дисплея"
-#: src/gtk/connected-device.ui:95
-msgid ""
-"Closer appears larger, further appears smaller. Controls depth when in "
-"widescreen mode."
+#: src/gtk/connected-device.ui:144
+msgid "Set how close you want displays to appear."
msgstr ""
-"Чим ближче, тим більший. Чим далі, тим менший. Керує глибиною при "
-"використанні режиму широкого екрана."
-#: src/gtk/connected-device.ui:123
-msgid "Display size"
-msgstr "Розмір дисплея"
-
-#: src/gtk/connected-device.ui:124
-msgid ""
-"Combine with display distance to achieve a comfortable level of depth and "
-"size."
-msgstr ""
-"Поєднуйте з відстанню дисплея, щоб досягти комфортного рівня глибини і "
-"розміру."
-
-#: src/gtk/connected-device.ui:152
-msgid "Display toggle distances"
-msgstr "Перемикання відстані дисплея"
-
-#: src/gtk/connected-device.ui:153
-msgid ""
-"Use the buttons to capture the current display distance for use with the "
-"keyboard shortcut."
-msgstr ""
-"Використовуйте кнопки, щоб зафіксувати поточну відстань дисплея для "
-"використання з сполученням клавіш."
-
-#: src/gtk/connected-device.ui:178
+#: src/gtk/connected-device.ui:199
msgid "Follow threshold"
msgstr "Поріг слідування"
-#: src/gtk/connected-device.ui:179
+#: src/gtk/connected-device.ui:200
msgid "How far away you can look before the display follows."
msgstr ""
"Як далеко ви можете відвернути ваш взор, перш ніж дисплей почне слідувати за "
"вами."
-#: src/gtk/connected-device.ui:209 src/gtk/connected-device.ui:215
+#: src/gtk/connected-device.ui:223
+#, fuzzy
+msgid "Display angling"
+msgstr "Розмір дисплея"
+
+#: src/gtk/connected-device.ui:224
+msgid ""
+"When there are multiple displays, choose how they should angle towards you."
+msgstr ""
+
+#: src/gtk/connected-device.ui:238
+msgid "Automatic"
+msgstr ""
+
+#: src/gtk/connected-device.ui:239
+msgid "Side-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:240
+msgid "Top-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:241
+msgid "Flat"
+msgstr ""
+
+#: src/gtk/connected-device.ui:251
+#, fuzzy
+msgid "Display spacing"
+msgstr "Розмір дисплея"
+
+#: src/gtk/connected-device.ui:252
+msgid "Put empty space between displays, when there are multiple."
+msgstr ""
+
+#: src/gtk/connected-device.ui:282
+msgid "Viewport horizontal offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:283
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport to the left or right."
+msgstr ""
+
+#: src/gtk/connected-device.ui:312
+msgid "Viewport vertical offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:313
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport up or down."
+msgstr ""
+
+#: src/gtk/connected-device.ui:349 src/gtk/connected-device.ui:355
msgid "Keyboard Shortcuts"
msgstr "Сполучення клавіш"
-#: src/gtk/connected-device.ui:218
+#: src/gtk/connected-device.ui:358
msgid "XR Effect on/off shortcut"
msgstr ""
-#: src/gtk/connected-device.ui:219
+#: src/gtk/connected-device.ui:359
msgid ""
"Quickly enable or disable the XR Effect. You may need to enable the effect "
"manually once in order to enable the shortcut."
msgstr ""
-#: src/gtk/connected-device.ui:238 src/gtk/connected-device.ui:267
-#: src/gtk/connected-device.ui:296 src/gtk/connected-device.ui:325
+#: src/gtk/connected-device.ui:378 src/gtk/connected-device.ui:407
+#: src/gtk/connected-device.ui:436 src/gtk/connected-device.ui:465
msgid "Change"
msgstr "Змінити"
-#: src/gtk/connected-device.ui:247
+#: src/gtk/connected-device.ui:387
msgid "Re-center display shortcut"
msgstr "Сполучення клавіш для центрування дисплея"
-#: src/gtk/connected-device.ui:248
+#: src/gtk/connected-device.ui:388
msgid "Pin the virtual display to the current position."
msgstr "Фіксувати віртуальний дисплей у поточній позиції."
-#: src/gtk/connected-device.ui:276
-msgid "Display distance shortcut"
-msgstr "Сполучення клавіш для відстані дисплея"
+#: src/gtk/connected-device.ui:416
+#, fuzzy
+msgid "Toggle zoom on focus shortcut"
+msgstr "Сполучення клавіш для перемикання режиму слідування"
-#: src/gtk/connected-device.ui:277
-msgid "Quickly toggle between two predefined distances."
-msgstr "Швидко перемикатися між двома попередньо визначеними відстанями."
+#: src/gtk/connected-device.ui:417
+#, fuzzy
+msgid "Quickly toggle zoom on focus mode."
+msgstr "Швидко перемикати режим слідування."
-#: src/gtk/connected-device.ui:305
+#: src/gtk/connected-device.ui:445
msgid "Toggle follow mode shortcut"
msgstr "Сполучення клавіш для перемикання режиму слідування"
-#: src/gtk/connected-device.ui:306
+#: src/gtk/connected-device.ui:446
msgid "Quickly toggle follow mode."
msgstr "Швидко перемикати режим слідування."
-#: src/gtk/connected-device.ui:341 src/gtk/connected-device.ui:347
+#: src/gtk/connected-device.ui:481 src/gtk/connected-device.ui:487
msgid "Advanced Settings"
msgstr "Додаткові налаштування"
-#: src/gtk/connected-device.ui:350
+#: src/gtk/connected-device.ui:490
msgid "Find optimal display config"
msgstr "Знайти оптимальну конфігурацію дисплея"
-#: src/gtk/connected-device.ui:351
+#: src/gtk/connected-device.ui:491
msgid ""
"Automatically modify the glasses display configuration for maximum "
"resolution and best scaling when plugged in."
@@ -293,41 +397,29 @@ msgstr ""
"Автоматично змінює конфігурацію дисплея окулярів для максимальної роздільної "
"здатності і найкращого масштабування при підключенні."
-#: src/gtk/connected-device.ui:361
+#: src/gtk/connected-device.ui:501
msgid "Use highest refresh rate"
msgstr "Використовувати найвищу частоту оновлення"
-#: src/gtk/connected-device.ui:362
+#: src/gtk/connected-device.ui:502
msgid "Refresh rate may affect performance, disable this to set it manually."
msgstr ""
"Частота оновлення може вплинути на продуктивність, вимкніть це, щоб "
"встановити її вручну."
-#: src/gtk/connected-device.ui:372
+#: src/gtk/connected-device.ui:512
msgid "Always primary display"
msgstr "Завжди основний дисплей"
-#: src/gtk/connected-device.ui:373
+#: src/gtk/connected-device.ui:513
msgid "Automatically set the glasses as the primary display when plugged in."
msgstr "Автоматично встановлює окуляри як основний дисплей при підключенні."
-#: src/gtk/connected-device.ui:383
-msgid "Fast SBS mode switching"
-msgstr "Швидке перемикання режиму SBS"
-
-#: src/gtk/connected-device.ui:384
-msgid ""
-"Switches glasses to SBS mode immediately when plugged in, if widescreen mode "
-"is on. May cause instability."
-msgstr ""
-"Переключає окуляри в режим SBS відразу після підключення, якщо режим "
-"широкого екрана увімкнено. Може викликати нестабільність."
-
-#: src/gtk/connected-device.ui:394
+#: src/gtk/connected-device.ui:523
msgid "Movement look-ahead"
msgstr "Прогнозування руху"
-#: src/gtk/connected-device.ui:395
+#: src/gtk/connected-device.ui:524
msgid ""
"Counteracts input lag by predicting head-tracking position ahead of render "
"time. Stick with default unless virtual display drags behind your head "
@@ -338,18 +430,34 @@ msgstr ""
"віртуальний дисплей відстає від рухів вашої голови, випереджає або дуже "
"тремтить."
-#: src/gtk/connected-device.ui:413
+#: src/gtk/connected-device.ui:542
msgid "Default"
msgstr "За замовчуванням"
-#: src/gtk/connected-device.ui:425
+#: src/gtk/connected-device.ui:554
msgid "Text Scaling"
msgstr ""
-#: src/gtk/connected-device.ui:426
+#: src/gtk/connected-device.ui:555
msgid "Scaling text below 1.0 will simulate a higher resolution display"
msgstr ""
+#: src/gtk/connected-device.ui:585
+msgid "Enable multi-tap detection"
+msgstr ""
+
+#: src/gtk/connected-device.ui:586
+msgid "Enables double-tap to recenter and triple-tap to recalibrate."
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:9
+msgid "Show full range"
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:18
+msgid "Done"
+msgstr ""
+
#: src/gtk/failed-verification.ui:13
msgid "Breezy Desktop GNOME invalid setup"
msgstr "Невірна настройка Breezy Desktop GNOME"
@@ -397,15 +505,6 @@ msgid ""
"connected."
msgstr ""
-#: src/gtk/no-device.ui:34
-#, fuzzy
-msgid "Start in widescreen mode"
-msgstr "Режим широкого екрану"
-
-#: src/gtk/no-device.ui:35
-msgid "Widescreen mode is not supported for all glasses."
-msgstr ""
-
#: src/gtk/no-driver.ui:13
msgid "No driver running"
msgstr "Жоден драйвер не запущений"
@@ -514,3 +613,57 @@ msgstr "Скинути"
#: src/gtk/window.ui:99
msgid "About BreezyDesktop"
msgstr "Про BreezyDesktop"
+
+#~ msgid "Fast SBS mode switching"
+#~ msgstr "Швидке перемикання режиму SBS"
+
+#~ msgid ""
+#~ "Switches glasses to SBS mode immediately when plugged in, if widescreen "
+#~ "mode is on. May cause instability."
+#~ msgstr ""
+#~ "Переключає окуляри в режим SBS відразу після підключення, якщо режим "
+#~ "широкого екрана увімкнено. Може викликати нестабільність."
+
+#, fuzzy
+#~ msgid "Set Unfocused Displays Distance"
+#~ msgstr "Відстань дисплея"
+
+#~ msgid "Widescreen mode"
+#~ msgstr "Режим широкого екрану"
+
+#~ msgid "Switch between flat and curved displays."
+#~ msgstr "Переключается між плоскими і викривленими дисплеями."
+
+#~ msgid ""
+#~ "Closer appears larger, further appears smaller. Controls depth when in "
+#~ "widescreen mode."
+#~ msgstr ""
+#~ "Чим ближче, тим більший. Чим далі, тим менший. Керує глибиною при "
+#~ "використанні режиму широкого екрана."
+
+#~ msgid ""
+#~ "Combine with display distance to achieve a comfortable level of depth and "
+#~ "size."
+#~ msgstr ""
+#~ "Поєднуйте з відстанню дисплея, щоб досягти комфортного рівня глибини і "
+#~ "розміру."
+
+#~ msgid "Display toggle distances"
+#~ msgstr "Перемикання відстані дисплея"
+
+#~ msgid ""
+#~ "Use the buttons to capture the current display distance for use with the "
+#~ "keyboard shortcut."
+#~ msgstr ""
+#~ "Використовуйте кнопки, щоб зафіксувати поточну відстань дисплея для "
+#~ "використання з сполученням клавіш."
+
+#~ msgid "Display distance shortcut"
+#~ msgstr "Сполучення клавіш для відстані дисплея"
+
+#~ msgid "Quickly toggle between two predefined distances."
+#~ msgstr "Швидко перемикатися між двома попередньо визначеними відстанями."
+
+#, fuzzy
+#~ msgid "Start in widescreen mode"
+#~ msgstr "Режим широкого екрану"
diff --git a/ui/po/zh_CN.po b/ui/po/zh_CN.po
index 0f14f64..3faee5c 100644
--- a/ui/po/zh_CN.po
+++ b/ui/po/zh_CN.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-22 13:27-0700\n"
+"POT-Creation-Date: 2025-03-05 12:28-0800\n"
"PO-Revision-Date: 2024-08-02 20:55-0700\n"
"Last-Translator: \n"
"Language-Team: Chinese (simplified) \n"
@@ -16,16 +16,62 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: src/connecteddevice.py:17
+#: src/connecteddevice.py:23
msgid ""
"Switches your glasses into side-by-side mode and doubles the width of the "
"display."
msgstr "切换到并排模式,并将显示宽度翻倍。"
-#: src/connecteddevice.py:18
+#: src/connecteddevice.py:24
msgid "This feature is not currently supported for your device."
msgstr "您的设备目前不支援此功能。"
+#: src/connecteddevice.py:124
+#, fuzzy
+msgid "Set Focused Display Distance"
+msgstr "显示距离"
+
+#: src/connecteddevice.py:125
+msgid "Use a closer value so the display zooms in when you look at it."
+msgstr ""
+
+#: src/connecteddevice.py:132
+#, fuzzy
+msgid "Set All Displays Distance"
+msgstr "显示距离"
+
+#: src/connecteddevice.py:133
+msgid "Use a farther value so the displays are zoomed out when you look away."
+msgstr ""
+
+#: src/connecteddevice.py:231
+msgid ""
+"Unable to add virtual displays on this machine. xdg-desktop-portal is "
+"required."
+msgstr ""
+
+#: src/connecteddevice.py:265
+#, fuzzy
+msgid "Focused display"
+msgstr "显示距离"
+
+#: src/connecteddevice.py:271
+#, fuzzy
+msgid "All displays"
+msgstr "曲面显示"
+
+#: src/displaydistancedialogcontent.py:41
+msgid "closer"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:45
+msgid "fullscreen"
+msgstr ""
+
+#: src/displaydistancedialogcontent.py:48
+msgid "farther"
+msgstr ""
+
#: src/licensedialogcontent.py:63
msgid "Paid Tier Status"
msgstr "订阅状态"
@@ -34,7 +80,7 @@ msgstr "订阅状态"
msgid "Feature Availability"
msgstr "功能状态"
-#: src/licensefeaturerow.py:15 src/shortcutdialog.py:107
+#: src/licensefeaturerow.py:15 src/shortcutdialog.py:104
msgid "Disabled"
msgstr "已禁用"
@@ -163,176 +209,245 @@ msgid "Enables the Breezy Desktop XR effect."
msgstr "启用 Breezy Desktop XR 效果。"
#: src/gtk/connected-device.ui:55
-msgid "Widescreen mode"
-msgstr "宽屏模式"
+msgid "Zoom on focus"
+msgstr ""
-#: src/gtk/connected-device.ui:66
+#: src/gtk/connected-device.ui:56
+msgid ""
+"Automatically move a display closer when you look at it.\n"
+"Set your preferred focused and unfocused distances in the Adjustments "
+"section.\n"
+" "
+msgstr ""
+
+#: src/gtk/connected-device.ui:69
msgid "Follow mode"
msgstr "跟随模式"
-#: src/gtk/connected-device.ui:67
-msgid "Keep the virtual display near the center of your view."
+#: src/gtk/connected-device.ui:70
+#, fuzzy
+msgid "Keep the focused display near the center of your view."
msgstr "虚拟显示保持在视野中心附近。"
-#: src/gtk/connected-device.ui:77
-msgid "Curved display"
-msgstr "曲面显示"
+#: src/gtk/connected-device.ui:80 src/gtk/no-device.ui:34
+msgid "Disable physical displays"
+msgstr ""
-#: src/gtk/connected-device.ui:78
-msgid "Switch between flat and curved displays."
-msgstr "平板和曲面显示模式之间切换。"
+#: src/gtk/connected-device.ui:81 src/gtk/no-device.ui:35
+msgid ""
+"Automatically disable all physical displays when the XR effect is enabled."
+msgstr ""
#: src/gtk/connected-device.ui:91
+#, fuzzy
+msgid "Virtual displays"
+msgstr "曲面显示"
+
+#: src/gtk/connected-device.ui:103
+#, fuzzy
+msgid "Rearrange displays"
+msgstr "曲面显示"
+
+#: src/gtk/connected-device.ui:115
+msgid "1080p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:116
+msgid "1440p"
+msgstr ""
+
+#: src/gtk/connected-device.ui:140
msgid "Adjustments"
msgstr "调整"
-#: src/gtk/connected-device.ui:94
-msgid "Display distance"
+#: src/gtk/connected-device.ui:143
+#, fuzzy
+msgid "Display distances"
msgstr "显示距离"
-#: src/gtk/connected-device.ui:95
-msgid ""
-"Closer appears larger, further appears smaller. Controls depth when in "
-"widescreen mode."
-msgstr "距离近看起来大,距离远看起来小。调整使用宽屏模式时的深度。"
+#: src/gtk/connected-device.ui:144
+msgid "Set how close you want displays to appear."
+msgstr ""
-#: src/gtk/connected-device.ui:123
-msgid "Display size"
-msgstr "显示大小"
-
-#: src/gtk/connected-device.ui:124
-msgid ""
-"Combine with display distance to achieve a comfortable level of depth and "
-"size."
-msgstr "可以跟显示距离一起配合来实现舒适的深度和大小。"
-
-#: src/gtk/connected-device.ui:152
-msgid "Display toggle distances"
-msgstr "显示切换距离"
-
-#: src/gtk/connected-device.ui:153
-msgid ""
-"Use the buttons to capture the current display distance for use with the "
-"keyboard shortcut."
-msgstr "使用按钮记住当时的显示距离,以设成键盘快捷键。"
-
-#: src/gtk/connected-device.ui:178
+#: src/gtk/connected-device.ui:199
msgid "Follow threshold"
msgstr "跟随触发值"
-#: src/gtk/connected-device.ui:179
+#: src/gtk/connected-device.ui:200
msgid "How far away you can look before the display follows."
msgstr "在显示内容跟随前您可以看多远。"
-#: src/gtk/connected-device.ui:209 src/gtk/connected-device.ui:215
+#: src/gtk/connected-device.ui:223
+#, fuzzy
+msgid "Display angling"
+msgstr "显示大小"
+
+#: src/gtk/connected-device.ui:224
+msgid ""
+"When there are multiple displays, choose how they should angle towards you."
+msgstr ""
+
+#: src/gtk/connected-device.ui:238
+msgid "Automatic"
+msgstr ""
+
+#: src/gtk/connected-device.ui:239
+msgid "Side-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:240
+msgid "Top-angled"
+msgstr ""
+
+#: src/gtk/connected-device.ui:241
+msgid "Flat"
+msgstr ""
+
+#: src/gtk/connected-device.ui:251
+#, fuzzy
+msgid "Display spacing"
+msgstr "显示大小"
+
+#: src/gtk/connected-device.ui:252
+msgid "Put empty space between displays, when there are multiple."
+msgstr ""
+
+#: src/gtk/connected-device.ui:282
+msgid "Viewport horizontal offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:283
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport to the left or right."
+msgstr ""
+
+#: src/gtk/connected-device.ui:312
+msgid "Viewport vertical offset"
+msgstr ""
+
+#: src/gtk/connected-device.ui:313
+msgid ""
+"By default, the viewport will center on the primary display. Use this slider "
+"to move the viewport up or down."
+msgstr ""
+
+#: src/gtk/connected-device.ui:349 src/gtk/connected-device.ui:355
msgid "Keyboard Shortcuts"
msgstr "键盘快捷键"
-#: src/gtk/connected-device.ui:218
+#: src/gtk/connected-device.ui:358
msgid "XR Effect on/off shortcut"
msgstr "XR效果 开/关 快捷键"
-#: src/gtk/connected-device.ui:219
+#: src/gtk/connected-device.ui:359
msgid ""
"Quickly enable or disable the XR Effect. You may need to enable the effect "
"manually once in order to enable the shortcut."
msgstr "快速启用或禁用 XR 效果。您可能需要先手动启用一次该效果,才能设此快捷键"
-#: src/gtk/connected-device.ui:238 src/gtk/connected-device.ui:267
-#: src/gtk/connected-device.ui:296 src/gtk/connected-device.ui:325
+#: src/gtk/connected-device.ui:378 src/gtk/connected-device.ui:407
+#: src/gtk/connected-device.ui:436 src/gtk/connected-device.ui:465
msgid "Change"
msgstr "更改"
-#: src/gtk/connected-device.ui:247
+#: src/gtk/connected-device.ui:387
msgid "Re-center display shortcut"
msgstr "重新居中显示快捷键"
-#: src/gtk/connected-device.ui:248
+#: src/gtk/connected-device.ui:388
msgid "Pin the virtual display to the current position."
msgstr "将虚拟显示固定在当前位置。"
-#: src/gtk/connected-device.ui:276
-msgid "Display distance shortcut"
-msgstr "显示距离快捷键"
+#: src/gtk/connected-device.ui:416
+#, fuzzy
+msgid "Toggle zoom on focus shortcut"
+msgstr "切换跟随模式快捷键"
-#: src/gtk/connected-device.ui:277
-msgid "Quickly toggle between two predefined distances."
-msgstr "快速地在两个预定的距离之间切换。"
+#: src/gtk/connected-device.ui:417
+#, fuzzy
+msgid "Quickly toggle zoom on focus mode."
+msgstr "快速切换跟随模式。"
-#: src/gtk/connected-device.ui:305
+#: src/gtk/connected-device.ui:445
msgid "Toggle follow mode shortcut"
msgstr "切换跟随模式快捷键"
-#: src/gtk/connected-device.ui:306
+#: src/gtk/connected-device.ui:446
msgid "Quickly toggle follow mode."
msgstr "快速切换跟随模式。"
-#: src/gtk/connected-device.ui:341 src/gtk/connected-device.ui:347
+#: src/gtk/connected-device.ui:481 src/gtk/connected-device.ui:487
msgid "Advanced Settings"
msgstr "高级设定"
-#: src/gtk/connected-device.ui:350
+#: src/gtk/connected-device.ui:490
msgid "Find optimal display config"
msgstr "寻找最佳显示设定"
-#: src/gtk/connected-device.ui:351
+#: src/gtk/connected-device.ui:491
msgid ""
"Automatically modify the glasses display configuration for maximum "
"resolution and best scaling when plugged in."
msgstr "连接时,可以自动修改眼镜显示设定以表现出最大解析度和最佳的对比。"
-#: src/gtk/connected-device.ui:361
+#: src/gtk/connected-device.ui:501
msgid "Use highest refresh rate"
msgstr "使用最高刷新率"
-#: src/gtk/connected-device.ui:362
+#: src/gtk/connected-device.ui:502
msgid "Refresh rate may affect performance, disable this to set it manually."
msgstr "刷新率可能会影响性能,禁用此功能即可手动设置。"
-#: src/gtk/connected-device.ui:372
+#: src/gtk/connected-device.ui:512
msgid "Always primary display"
msgstr "每次设置为主要显示"
-#: src/gtk/connected-device.ui:373
+#: src/gtk/connected-device.ui:513
msgid "Automatically set the glasses as the primary display when plugged in."
msgstr "连接时,自动将眼镜设置为主要显示。"
-#: src/gtk/connected-device.ui:383
-msgid "Fast SBS mode switching"
-msgstr "快速并排模式切换"
-
-#: src/gtk/connected-device.ui:384
-msgid ""
-"Switches glasses to SBS mode immediately when plugged in, if widescreen mode "
-"is on. May cause instability."
-msgstr ""
-"连接时,如果宽屏模式开启,眼镜会立即切换到 并排模式。这可能会导致不稳定。"
-
-#: src/gtk/connected-device.ui:394
+#: src/gtk/connected-device.ui:523
msgid "Movement look-ahead"
msgstr "移动预测"
-#: src/gtk/connected-device.ui:395
+#: src/gtk/connected-device.ui:524
msgid ""
"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."
msgstr ""
-"透过预测头部追踪位置,提前于渲染时间进行预测来抵消输入延迟。"
-"除非虚拟显示滞后于头部,提前跳动或非常抖动,请尽量使用默认设置。"
+"透过预测头部追踪位置,提前于渲染时间进行预测来抵消输入延迟。除非虚拟显示滞后"
+"于头部,提前跳动或非常抖动,请尽量使用默认设置。"
-#: src/gtk/connected-device.ui:413
+#: src/gtk/connected-device.ui:542
msgid "Default"
msgstr "默认"
-#: src/gtk/connected-device.ui:425
+#: src/gtk/connected-device.ui:554
msgid "Text Scaling"
msgstr "字体大小比例"
-#: src/gtk/connected-device.ui:426
+#: src/gtk/connected-device.ui:555
msgid "Scaling text below 1.0 will simulate a higher resolution display"
msgstr "字体缩放小于1.0,将模拟解析度更高的显示效果"
+#: src/gtk/connected-device.ui:585
+msgid "Enable multi-tap detection"
+msgstr ""
+
+#: src/gtk/connected-device.ui:586
+msgid "Enables double-tap to recenter and triple-tap to recalibrate."
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:9
+msgid "Show full range"
+msgstr ""
+
+#: src/gtk/display-distance-dialog.ui:18
+msgid "Done"
+msgstr ""
+
#: src/gtk/failed-verification.ui:13
msgid "Breezy Desktop GNOME invalid setup"
msgstr "Breezy Desktop GNOME 无效设置"
@@ -379,14 +494,6 @@ msgid ""
"connected."
msgstr "当支援的设备已连接,将BREEZY DESKTOP XR效果自动启动"
-#: src/gtk/no-device.ui:34
-msgid "Start in widescreen mode"
-msgstr "使用宽屏模式以启动软件"
-
-#: src/gtk/no-device.ui:35
-msgid "Widescreen mode is not supported for all glasses."
-msgstr "宽屏模式不支援每一个型号的XR眼睛"
-
#: src/gtk/no-driver.ui:13
msgid "No driver running"
msgstr "未运行驱动程序"
@@ -490,3 +597,52 @@ msgstr "强制重置"
#: src/gtk/window.ui:99
msgid "About BreezyDesktop"
msgstr "关于 Breezy Desktop"
+
+#~ msgid "Fast SBS mode switching"
+#~ msgstr "快速并排模式切换"
+
+#~ msgid ""
+#~ "Switches glasses to SBS mode immediately when plugged in, if widescreen "
+#~ "mode is on. May cause instability."
+#~ msgstr ""
+#~ "连接时,如果宽屏模式开启,眼镜会立即切换到 并排模式。这可能会导致不稳定。"
+
+#, fuzzy
+#~ msgid "Set Unfocused Displays Distance"
+#~ msgstr "显示距离"
+
+#~ msgid "Widescreen mode"
+#~ msgstr "宽屏模式"
+
+#~ msgid "Switch between flat and curved displays."
+#~ msgstr "平板和曲面显示模式之间切换。"
+
+#~ msgid ""
+#~ "Closer appears larger, further appears smaller. Controls depth when in "
+#~ "widescreen mode."
+#~ msgstr "距离近看起来大,距离远看起来小。调整使用宽屏模式时的深度。"
+
+#~ msgid ""
+#~ "Combine with display distance to achieve a comfortable level of depth and "
+#~ "size."
+#~ msgstr "可以跟显示距离一起配合来实现舒适的深度和大小。"
+
+#~ msgid "Display toggle distances"
+#~ msgstr "显示切换距离"
+
+#~ msgid ""
+#~ "Use the buttons to capture the current display distance for use with the "
+#~ "keyboard shortcut."
+#~ msgstr "使用按钮记住当时的显示距离,以设成键盘快捷键。"
+
+#~ msgid "Display distance shortcut"
+#~ msgstr "显示距离快捷键"
+
+#~ msgid "Quickly toggle between two predefined distances."
+#~ msgstr "快速地在两个预定的距离之间切换。"
+
+#~ msgid "Start in widescreen mode"
+#~ msgstr "使用宽屏模式以启动软件"
+
+#~ msgid "Widescreen mode is not supported for all glasses."
+#~ msgstr "宽屏模式不支援每一个型号的XR眼睛"
diff --git a/ui/src/breezydesktop.gresource.xml b/ui/src/breezydesktop.gresource.xml
index 843487c..37c33ba 100644
--- a/ui/src/breezydesktop.gresource.xml
+++ b/ui/src/breezydesktop.gresource.xml
@@ -2,6 +2,8 @@
gtk/connected-device.ui
+ gtk/display-distance-dialog.ui
+ gtk/display-distance-dialog-content.ui
gtk/failed-verification.ui
gtk/license-dialog.ui
gtk/license-dialog-content.ui
@@ -10,6 +12,7 @@
gtk/no-extension.ui
gtk/no-license.ui
gtk/shortcut-dialog.ui
+ gtk/virtual-display-row.ui
gtk/window.ui
diff --git a/ui/src/configmanager.py b/ui/src/configmanager.py
index 37c6b18..983d4bc 100644
--- a/ui/src/configmanager.py
+++ b/ui/src/configmanager.py
@@ -6,6 +6,7 @@ from .xrdriveripc import XRDriverIPC
class ConfigManager(GObject.GObject):
__gproperties__ = {
'breezy-desktop-enabled': (bool, 'Breezy Desktop Enabled', 'Whether Breezy Desktop is enabled', False, GObject.ParamFlags.READWRITE),
+ 'multi-tap-enabled': (bool, 'Multi-Tap Enabled', 'Whether Multi-Tap is enabled', False, GObject.ParamFlags.READWRITE),
}
_instance = None
@@ -27,6 +28,7 @@ class ConfigManager(GObject.GObject):
GObject.GObject.__init__(self)
self.ipc = XRDriverIPC.get_instance()
self.breezy_desktop_enabled = None
+ self.multi_tap_enabled = None
self._running = True
self._refresh_config()
@@ -38,6 +40,9 @@ class ConfigManager(GObject.GObject):
if self._is_breezy_desktop_enabled() != self.breezy_desktop_enabled:
self.set_property('breezy-desktop-enabled', self._is_breezy_desktop_enabled())
+ if self.config['multi_tap_enabled'] != self.multi_tap_enabled:
+ self.set_property('multi-tap-enabled', self.config['multi_tap_enabled'])
+
if self._running: threading.Timer(1.0, self._refresh_config).start()
def _is_breezy_desktop_enabled(self):
@@ -48,17 +53,26 @@ class ConfigManager(GObject.GObject):
self.config['disabled'] = False
self.config['output_mode'] = 'external_only'
self.config['external_mode'] = ['breezy_desktop']
- self.ipc.write_config(self.config)
else:
self.config['external_mode'] = []
- self.ipc.write_config(self.config)
+ self.ipc.write_config(self.config)
self.breezy_desktop_enabled = value
+ def _set_multi_tap_enabled(self, value):
+ if self.multi_tap_enabled != value:
+ self.config['multi_tap_enabled'] = value
+ self.ipc.write_config(self.config)
+ self.multi_tap_enabled = value
+
def do_set_property(self, prop, value):
if prop.name == 'breezy-desktop-enabled':
self._set_breezy_desktop_enabled(value)
+ elif prop.name == 'multi-tap-enabled':
+ self._set_multi_tap_enabled(value)
def do_get_property(self, prop):
if prop.name == 'breezy-desktop-enabled':
- return self.breezy_desktop_enabled
\ No newline at end of file
+ return self.breezy_desktop_enabled
+ elif prop.name == 'multi-tap-enabled':
+ return self.multi_tap_enabled
\ No newline at end of file
diff --git a/ui/src/connecteddevice.py b/ui/src/connecteddevice.py
index efe638e..acb867d 100644
--- a/ui/src/connecteddevice.py
+++ b/ui/src/connecteddevice.py
@@ -1,14 +1,20 @@
-from gi.repository import Gio, Gtk, GObject
+from gi.repository import Gio, GLib, Gtk, GObject
from .configmanager import ConfigManager
from .extensionsmanager import ExtensionsManager
+from .displaydistancedialog import DisplayDistanceDialog
from .license import BREEZY_GNOME_FEATURES
from .settingsmanager import SettingsManager
from .shortcutdialog import bind_shortcut_settings
from .statemanager import StateManager
+from .virtualdisplaymanager import VirtualDisplayManager
+from .virtualdisplay import is_screencast_available
+from .virtualdisplayrow import VirtualDisplayRow
from .xrdriveripc import XRDriverIPC
import gettext
+import logging
_ = gettext.gettext
+logger = logging.getLogger('breezy_ui')
@Gtk.Template(resource_path='/com/xronlinux/BreezyDesktop/gtk/connected-device.ui')
class ConnectedDevice(Gtk.Box):
@@ -19,18 +25,20 @@ class ConnectedDevice(Gtk.Box):
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()
+ disable_physical_displays_switch = Gtk.Template.Child()
+ display_zoom_on_focus_switch = Gtk.Template.Child()
follow_threshold_scale = Gtk.Template.Child()
follow_threshold_adjustment = Gtk.Template.Child()
follow_mode_switch = Gtk.Template.Child()
- widescreen_mode_switch = Gtk.Template.Child()
- widescreen_mode_row = 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()
+ top_features_group = Gtk.Template.Child()
+ virtual_displays_row = Gtk.Template.Child()
+ add_virtual_display_menu = Gtk.Template.Child()
+ add_virtual_display_button = Gtk.Template.Child()
+ launch_display_settings_button = Gtk.Template.Child()
+ all_displays_distance_label = Gtk.Template.Child()
+ change_all_displays_distance_button = Gtk.Template.Child()
+ focused_display_distance_label = Gtk.Template.Child()
+ change_focused_display_distance_button = Gtk.Template.Child()
reassign_toggle_xr_effect_shortcut_button = Gtk.Template.Child()
toggle_xr_effect_shortcut_label = Gtk.Template.Child()
reassign_recenter_display_shortcut_button = Gtk.Template.Child()
@@ -42,44 +50,65 @@ class ConnectedDevice(Gtk.Box):
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()
text_scaling_scale = Gtk.Template.Child()
text_scaling_adjustment = Gtk.Template.Child()
-
+ enable_multi_tap_switch = Gtk.Template.Child()
+ monitor_wrapping_scheme_menu = Gtk.Template.Child()
+ monitor_spacing_scale = Gtk.Template.Child()
+ monitor_spacing_adjustment = Gtk.Template.Child()
+ viewport_offset_x_scale = Gtk.Template.Child()
+ viewport_offset_x_adjustment = Gtk.Template.Child()
+ viewport_offset_y_scale = Gtk.Template.Child()
+ viewport_offset_y_adjustment = Gtk.Template.Child()
def __init__(self):
super(Gtk.Box, self).__init__()
self.init_template()
self.active = True
self.all_enabled_state_inputs = [
- self.display_distance_scale,
- self.display_size_scale,
+ self.display_zoom_on_focus_switch,
+ # self.display_size_scale,
self.follow_mode_switch,
self.follow_threshold_scale,
- self.curved_display_switch,
- self.set_toggle_display_distance_start_button,
- self.set_toggle_display_distance_end_button,
- self.movement_look_ahead_scale
+ # self.curved_display_switch,
+ self.add_virtual_display_menu,
+ self.add_virtual_display_button,
+ self.change_all_displays_distance_button,
+ self.change_focused_display_distance_button,
+ self.movement_look_ahead_scale,
+ self.monitor_wrapping_scheme_menu,
+ self.monitor_spacing_scale,
+ self.viewport_offset_x_scale,
+ self.viewport_offset_y_scale
]
self.settings = SettingsManager.get_instance().settings
self.desktop_settings = SettingsManager.get_instance().desktop_settings
self.ipc = XRDriverIPC.get_instance()
+ self.virtual_display_manager = VirtualDisplayManager.get_instance()
self.extensions_manager = ExtensionsManager.get_instance()
- self.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('disable-physical-displays', self.disable_physical_displays_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
+ self.settings.connect('changed::display-distance', self._handle_display_distance)
+ # 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('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('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)
+ self.settings.bind('monitor-spacing', self.monitor_spacing_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
+ self.settings.bind('viewport-offset-x', self.viewport_offset_x_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
+ self.settings.bind('viewport-offset-y', self.viewport_offset_y_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
+ self.settings.connect('changed::monitor-wrapping-scheme', self._handle_monitor_wrapping_scheme_setting_changed)
self.desktop_settings.bind('text-scaling-factor', self.text_scaling_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
+ self.display_zoom_on_focus_switch.connect('notify::active', self._handle_zoom_on_focus_switch_changed)
+ self.monitor_wrapping_scheme_menu.connect('changed', self._handle_monitor_wrapping_scheme_menu_changed)
+ self._handle_monitor_wrapping_scheme_setting_changed(self.settings, self.settings.get_string('monitor-wrapping-scheme'))
bind_shortcut_settings(self.get_parent(), [
[self.reassign_toggle_xr_effect_shortcut_button, self.toggle_xr_effect_shortcut_label],
@@ -88,10 +117,28 @@ class ConnectedDevice(Gtk.Box):
[self.reassign_toggle_follow_shortcut_button, self.toggle_follow_shortcut_label]
])
- self.bind_set_distance_toggle([
- self.set_toggle_display_distance_start_button,
- self.set_toggle_display_distance_end_button
- ])
+ self.change_focused_display_distance_button.connect('clicked',
+ self._on_display_distance_preset_change_button_clicked,
+ 'toggle-display-distance-start',
+ self._on_set_focused_display_distance,
+ _('Set Focused Display Distance'),
+ _('Use a closer value so the display zooms in when you look at it.'),
+ 0.2, 1.0
+ )
+ self.change_all_displays_distance_button.connect('clicked',
+ self._on_display_distance_preset_change_button_clicked,
+ 'toggle-display-distance-end',
+ self._on_set_all_displays_distance,
+ _('Set All Displays Distance'),
+ _('Use a farther value so the displays are zoomed out when you look away.'),
+ 1.0, 2.5
+ )
+ self._set_all_displays_distance(self.settings.get_double('toggle-display-distance-end'))
+ self._set_focused_display_distance(self.settings.get_double('toggle-display-distance-start'))
+
+ self.add_virtual_display_menu.set_active_id('1080p')
+ self.add_virtual_display_button.connect('clicked', self._on_add_virtual_display)
+ self.launch_display_settings_button.connect('clicked', self._launch_display_settings)
self.state_manager = StateManager.get_instance()
self.state_manager.bind_property('follow-mode', self.follow_mode_switch, 'active', GObject.BindingFlags.DEFAULT)
@@ -104,16 +151,46 @@ class ConnectedDevice(Gtk.Box):
self.config_manager = ConfigManager.get_instance()
self.config_manager.connect('notify::breezy-desktop-enabled', self._handle_enabled_config)
+ self.config_manager.bind_property('multi-tap-enabled', self.enable_multi_tap_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
+ self.enable_multi_tap_switch.connect('notify::active', lambda widget, param: self.config_manager.set_property('multi-tap-enabled', widget.get_active()))
self.use_optimal_monitor_config_switch.connect('notify::active', self._refresh_use_optimal_monitor_config)
+ self._handle_switch_enabled_state(self.effect_enable_switch, None)
+ self._handle_display_distance(self.settings, self.settings.get_double('display-distance'))
self._handle_enabled_features(self.state_manager, None)
self._handle_device_supports_sbs(self.state_manager, None)
self._handle_enabled_config(None, None)
self._refresh_use_optimal_monitor_config(self.use_optimal_monitor_config_switch, None)
self.extensions_manager.connect('notify::breezy-enabled', self._handle_enabled_config)
- self.connect("destroy", self._on_widget_destroy)
+ self.virtual_display_manager.connect('notify::displays', self._on_virtual_displays_update)
+ self._on_virtual_displays_update(self.virtual_display_manager, None)
+
+ self.virtual_displays_by_pid = {}
+
+ self._settings_displays_app_info = None
+
+ for appinfo in Gio.AppInfo.get_all():
+ if appinfo.get_id() == 'gnome-display-panel.desktop':
+ self._settings_displays_app_info = appinfo
+ break
+
+ def _handle_zoom_on_focus_switch_changed(self, widget, param):
+ display_distance = self.settings.get_double('display-distance')
+ toggle_display_distance_end = self.settings.get_double('toggle-display-distance-end')
+ toggle_display_distance_start = self.settings.get_double('toggle-display-distance-start')
+ is_zoom_on_focus_already_enabled = display_distance < toggle_display_distance_end
+ if widget.get_active() and not is_zoom_on_focus_already_enabled:
+ self.settings.set_double('display-distance', toggle_display_distance_start)
+ elif not widget.get_active() and is_zoom_on_focus_already_enabled:
+ self.settings.set_double('display-distance', toggle_display_distance_end)
+
+ def _handle_monitor_wrapping_scheme_setting_changed(self, settings, val):
+ self.monitor_wrapping_scheme_menu.set_active_id(val)
+
+ def _handle_monitor_wrapping_scheme_menu_changed(self, widget):
+ self.settings.set_string('monitor-wrapping-scheme', widget.get_active_id())
def _handle_enabled_features(self, state_manager, val):
enabled_breezy_features = [feature for feature in state_manager.get_property('enabled-features-list') if feature in BREEZY_GNOME_FEATURES]
@@ -125,9 +202,9 @@ class ConnectedDevice(Gtk.Box):
def _handle_device_supports_sbs(self, state_manager, val):
if not state_manager.get_property('device-supports-sbs'):
self.settings.set_boolean('widescreen-mode', False)
- self.widescreen_mode_switch.set_sensitive(state_manager.get_property('device-supports-sbs'))
- subtitle = self.widescreen_mode_subtitle if state_manager.get_property('device-supports-sbs') else self.widescreen_mode_not_supported_subtitle
- self.widescreen_mode_row.set_subtitle(subtitle)
+ # self.widescreen_mode_switch.set_sensitive(state_manager.get_property('device-supports-sbs'))
+ # subtitle = self.widescreen_mode_subtitle if state_manager.get_property('device-supports-sbs') else self.widescreen_mode_not_supported_subtitle
+ # self.widescreen_mode_row.set_subtitle(subtitle)
def _handle_enabled_config(self, object, val):
enabled = self.config_manager.get_property('breezy-desktop-enabled') and self.extensions_manager.get_property('breezy-enabled')
@@ -135,6 +212,9 @@ class ConnectedDevice(Gtk.Box):
self.effect_enable_switch.set_active(enabled)
def _handle_switch_enabled_state(self, switch, param):
+ GLib.idle_add(self._handle_switch_enabled_state_gui, switch, param)
+
+ def _handle_switch_enabled_state_gui(self, switch, param):
requesting_enabled = switch.get_active()
# never turn off the extension, disabling the effect is done via configs only
@@ -145,12 +225,17 @@ class ConnectedDevice(Gtk.Box):
for widget in self.all_enabled_state_inputs:
widget.set_sensitive(requesting_enabled)
+
+ if not is_screencast_available():
+ self.virtual_displays_row.set_subtitle(
+ _("Unable to add virtual displays on this machine. xdg-desktop-portal is required."))
+ self.add_virtual_display_button.set_sensitive(False)
+ self.add_virtual_display_menu.set_sensitive(False)
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())
if (self.state_manager.get_property('follow-mode') == switch.get_active()):
return
@@ -167,18 +252,90 @@ class ConnectedDevice(Gtk.Box):
def set_device_name(self, name):
self.device_label.set_markup(f"{name}")
+
+ def _handle_display_distance(self, *args):
+ display_distance = self.settings.get_double('display-distance')
+ toggle_display_distance_end = self.settings.get_double('toggle-display-distance-end')
+
+ should_zoom_on_focus_be_enabled = display_distance < toggle_display_distance_end
+ if self.display_zoom_on_focus_switch.get_active() != should_zoom_on_focus_be_enabled:
+ self.display_zoom_on_focus_switch.set_active(should_zoom_on_focus_be_enabled)
+
+ def _set_focused_display_distance(self, distance):
+ self.focused_display_distance_label.set_markup(f"{_("Focused display")}: {distance}")
+ self.settings.set_double('toggle-display-distance-start', distance)
+
+ self.display_zoom_on_focus_switch.set_sensitive(distance != self.settings.get_double('toggle-display-distance-end'))
+
+ def _set_all_displays_distance(self, distance):
+ self.all_displays_distance_label.set_markup(f"{_("All displays")}: {distance}")
+ self.settings.set_double('toggle-display-distance-end', distance)
+ self.display_zoom_on_focus_switch.set_active(False)
+ self.display_zoom_on_focus_switch.set_sensitive(distance != self.settings.get_double('toggle-display-distance-start'))
+
+ def _on_display_distance_preset_change_button_clicked(self, widget, settings_key, on_save_callback, title, subtitle, lower_limit, upper_limit):
+ dialog = DisplayDistanceDialog(settings_key, on_save_callback, title, subtitle, lower_limit, upper_limit)
+ dialog.set_transient_for(widget.get_ancestor(Gtk.Window))
+ dialog.present()
- def bind_set_distance_toggle(self, widgets):
- for widget in widgets:
- widget.connect('clicked', lambda *args, widget=widget: on_set_display_distance_toggle(widget))
- reload_display_distance_toggle_button(widget)
-
- 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')
+ def _on_set_all_displays_distance(self, prev_distance, distance):
+ focused_display_distance = self.settings.get_double('toggle-display-distance-start')
+ all_displays_distance = self.settings.get_double('toggle-display-distance-end')
+ if (distance < focused_display_distance):
+ self._set_focused_display_distance(distance)
+
+ self._set_all_displays_distance(distance)
+
+ if prev_distance == focused_display_distance:
+ self.settings.set_double('display-distance', prev_distance)
+
+ def _on_set_focused_display_distance(self, prev_distance, distance):
+ focused_display_distance = self.settings.get_double('toggle-display-distance-start')
+ all_displays_distance = self.settings.get_double('toggle-display-distance-end')
+ if (distance > all_displays_distance):
+ self._set_all_displays_distance(distance)
+
+ self._set_focused_display_distance(distance)
+
+ def _on_add_virtual_display(self, *args):
+ resolution = self.add_virtual_display_menu.get_active_id()
+ logger.info(f"Adding virtual display {resolution}")
+
+ width = 1920
+ height = 1080
+ if resolution == '1440p':
+ width = 2560
+ height = 1440
+
+ self.virtual_display_manager.create_virtual_display(width, height, 60)
+
+ def _on_virtual_displays_update(self, virtual_display_manager, val):
+ GLib.idle_add(self._on_virtual_displays_update_gui, virtual_display_manager)
+
+ def _on_virtual_displays_update_gui(self, virtual_display_manager):
+ effect_enabled = self.effect_enable_switch.get_active()
+ virtual_displays_present = len(virtual_display_manager.displays) > 0
+ self.launch_display_settings_button.set_visible(
+ self._settings_displays_app_info is not None and virtual_displays_present
+ )
+ self.monitor_wrapping_scheme_menu.set_sensitive(effect_enabled and virtual_displays_present)
+ self.monitor_spacing_scale.set_sensitive(effect_enabled and virtual_displays_present)
+
+ for pid, child in self.virtual_displays_by_pid.items():
+ self.top_features_group.remove(child)
+
+ new_displays_by_pid = {}
+ for display in virtual_display_manager.displays:
+ child = self.virtual_displays_by_pid.get(
+ display['pid'],
+ VirtualDisplayRow(display['pid'], display['width'], display['height'], 60))
+ self.top_features_group.add(child)
+ new_displays_by_pid[display['pid']] = child
+
+ self.virtual_displays_by_pid = new_displays_by_pid
+
+ def _launch_display_settings(self, *args):
+ self._settings_displays_app_info.launch()
def reload_display_distance_toggle_button(widget):
distance = SettingsManager.get_instance().settings.get_double(widget.get_name())
diff --git a/ui/src/displaydistancedialog.py b/ui/src/displaydistancedialog.py
new file mode 100644
index 0000000..9f02baa
--- /dev/null
+++ b/ui/src/displaydistancedialog.py
@@ -0,0 +1,28 @@
+from gi.repository import Gtk, Gio
+from .displaydistancedialogcontent import DisplayDistanceDialogContent
+
+@Gtk.Template(resource_path='/com/xronlinux/BreezyDesktop/gtk/display-distance-dialog.ui')
+class DisplayDistanceDialog(Gtk.Dialog):
+ __gtype_name__ = 'DisplayDistanceDialog'
+
+ show_full_scale_button = Gtk.Template.Child()
+ save_button = Gtk.Template.Child()
+
+ def __init__(self, settings_key, on_save_callback, title, subtitle, lower_limit, upper_limit):
+ super(Gtk.Dialog, self).__init__()
+ self.init_template()
+
+ self.on_save_callback = on_save_callback
+ self.set_title(title)
+
+ self.content = DisplayDistanceDialogContent(settings_key, self.show_full_scale_button, self.save_button, self._on_save_callback, subtitle, lower_limit, upper_limit)
+ self.get_content_area().append(self.content)
+
+ self.show_full_scale_button.connect('clicked', self._on_show_full_scale_button_clicked)
+
+ def _on_show_full_scale_button_clicked(self, button):
+ self.show_full_scale_button.set_visible(False)
+
+ def _on_save_callback(self, prev_distance, distance):
+ self.on_save_callback(prev_distance, distance)
+ self.close()
\ No newline at end of file
diff --git a/ui/src/displaydistancedialogcontent.py b/ui/src/displaydistancedialogcontent.py
new file mode 100644
index 0000000..3b81e72
--- /dev/null
+++ b/ui/src/displaydistancedialogcontent.py
@@ -0,0 +1,58 @@
+from gi.repository import Gtk, Gio
+from .settingsmanager import SettingsManager
+
+import gettext
+
+_ = gettext.gettext
+
+
+@Gtk.Template(resource_path='/com/xronlinux/BreezyDesktop/gtk/display-distance-dialog-content.ui')
+class DisplayDistanceDialogContent(Gtk.Box):
+ __gtype_name__ = 'DisplayDistanceDialogContent'
+
+ display_distance_subtitle = Gtk.Template.Child()
+ display_distance_scale = Gtk.Template.Child()
+ display_distance_adjustment = Gtk.Template.Child()
+
+ def __init__(self, settings_key, show_full_scale_button, save_button, on_save_callback, subtitle, lower_limit, upper_limit):
+ super(Gtk.Box, self).__init__()
+ self.init_template()
+
+ self.display_distance_subtitle.set_markup(f"""{subtitle}""")
+
+ self.on_save_callback = on_save_callback
+ self.settings = SettingsManager.get_instance().settings
+ self.prev_distance = self.settings.get_double('display-distance')
+
+ self.lower_limit_orig = self.display_distance_adjustment.get_lower()
+ self.upper_limit_orig = self.display_distance_adjustment.get_upper()
+
+ self._add_marks(lower_limit, upper_limit)
+
+ self.settings.bind('display-distance', self.display_distance_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
+
+ show_full_scale_button.connect('clicked', self._on_show_full_scale_button_clicked)
+ save_button.connect('clicked', self._on_save_button_clicked)
+
+ def _add_marks(self, lower_limit, upper_limit):
+ self.display_distance_scale.clear_marks()
+
+ if self.lower_limit_orig == lower_limit:
+ self.display_distance_scale.add_mark(self.lower_limit_orig, Gtk.PositionType.BOTTOM, _("closer"))
+ else:
+ self.display_distance_adjustment.set_lower(lower_limit)
+
+ self.display_distance_scale.add_mark(1.0, Gtk.PositionType.BOTTOM, _("fullscreen"))
+
+ if self.upper_limit_orig == upper_limit:
+ self.display_distance_scale.add_mark(self.upper_limit_orig, Gtk.PositionType.BOTTOM, _("farther"))
+ else:
+ self.display_distance_adjustment.set_upper(upper_limit)
+
+ def _on_show_full_scale_button_clicked(self, button):
+ self._add_marks(self.lower_limit_orig, self.upper_limit_orig)
+ self.display_distance_adjustment.set_lower(self.lower_limit_orig)
+ self.display_distance_adjustment.set_upper(self.upper_limit_orig)
+
+ def _on_save_button_clicked(self, button):
+ self.on_save_callback(self.prev_distance, self.display_distance_adjustment.get_value())
\ No newline at end of file
diff --git a/ui/src/gtk/connected-device.ui b/ui/src/gtk/connected-device.ui
index 26f7063..2849ef6 100644
--- a/ui/src/gtk/connected-device.ui
+++ b/ui/src/gtk/connected-device.ui
@@ -37,7 +37,7 @@
20
20
-
+
+
+ Enable multi-tap detection
+ Enables double-tap to recenter and triple-tap to recalibrate.
+
+
+ 3
+
+
+
+
diff --git a/ui/src/gtk/display-distance-dialog-content.ui b/ui/src/gtk/display-distance-dialog-content.ui
new file mode 100644
index 0000000..704b9c4
--- /dev/null
+++ b/ui/src/gtk/display-distance-dialog-content.ui
@@ -0,0 +1,37 @@
+
+
+
+
+ vertical
+ 3
+
+
+ true
+ 20
+ 20
+ 20
+
+
+
+
+ 3
+ 20
+ 20
+ 20
+ true
+ 0
+ 2
+ 350
+ false
+
+
+ 0.2
+ 2.5
+ 0.01
+ 1.05
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/gtk/display-distance-dialog.ui b/ui/src/gtk/display-distance-dialog.ui
new file mode 100644
index 0000000..1ac5219
--- /dev/null
+++ b/ui/src/gtk/display-distance-dialog.ui
@@ -0,0 +1,26 @@
+
+
+
+
+ 1
+ 1
+
+
+ Show full range
+ 10
+ 10
+ 10
+ 10
+
+
+
+
+ Done
+ 10
+ 10
+ 10
+ 10
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/gtk/no-device.ui b/ui/src/gtk/no-device.ui
index c7d6cad..4ce401d 100644
--- a/ui/src/gtk/no-device.ui
+++ b/ui/src/gtk/no-device.ui
@@ -30,12 +30,11 @@
-
- Start in widescreen mode
- Widescreen mode is not supported for all glasses.
- 2
+
+ Disable physical displays
+ Automatically disable all physical displays when the XR effect is enabled.
-
+
3
diff --git a/ui/src/gtk/virtual-display-row.ui b/ui/src/gtk/virtual-display-row.ui
new file mode 100644
index 0000000..acb2c2f
--- /dev/null
+++ b/ui/src/gtk/virtual-display-row.ui
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+ remove-virtual-display
+ user-trash-symbolic
+ center
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/meson.build b/ui/src/meson.build
index 0a2b141..447c0d6 100644
--- a/ui/src/meson.build
+++ b/ui/src/meson.build
@@ -26,12 +26,23 @@ configure_file(
install_mode: 'r-xr-xr-x'
)
+configure_file(
+ input: 'virtualdisplay.in',
+ output: 'virtualdisplay',
+ configuration: conf,
+ install: true,
+ install_dir: get_option('bindir'),
+ install_mode: 'r-xr-xr-x'
+)
+
breezydesktop_sources = [
'../modules/PyXRLinuxDriverIPC/xrdriveripc.py',
'__init__.py',
'configmanager.py',
'connecteddevice.py',
'extensionsmanager.py',
+ 'displaydistancedialog.py',
+ 'displaydistancedialogcontent.py',
'failedverification.py',
'license.py',
'licensedialog.py',
@@ -47,8 +58,11 @@ breezydesktop_sources = [
'shortcutdialog.py',
'statemanager.py',
'time.py',
+ 'virtualdisplay.py',
+ 'virtualdisplaymanager.py',
'verify.py',
'window.py'
]
install_data(breezydesktop_sources, install_dir: moduledir)
+install_subdir('../lib', install_dir: moduledir)
\ No newline at end of file
diff --git a/ui/src/nodevice.py b/ui/src/nodevice.py
index b36c656..fa56bfe 100644
--- a/ui/src/nodevice.py
+++ b/ui/src/nodevice.py
@@ -10,7 +10,8 @@ class NoDevice(Gtk.Box):
__gtype_name__ = "NoDevice"
effect_enable_switch = Gtk.Template.Child()
- widescreen_mode_switch = Gtk.Template.Child()
+ disable_physical_displays_switch = Gtk.Template.Child()
+ # widescreen_mode_switch = Gtk.Template.Child()
def __init__(self):
super(Gtk.Box, self).__init__()
@@ -23,7 +24,8 @@ class NoDevice(Gtk.Box):
self.config_manager.connect('notify::breezy-desktop-enabled', self._handle_enabled_config)
self.effect_enable_switch.connect('notify::active', self._handle_switch_enabled_state)
- self.settings.bind('widescreen-mode', self.widescreen_mode_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
+ # self.settings.bind('widescreen-mode', self.widescreen_mode_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
+ self.settings.bind('disable-physical-displays', self.disable_physical_displays_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
self._handle_enabled_config(self.config_manager, None)
@@ -41,5 +43,5 @@ class NoDevice(Gtk.Box):
self.config_manager.set_property('breezy-desktop-enabled', requesting_enabled)
- def _on_widget_destroy(self, widget):
- self.settings.unbind('widescreen-mode', self.widescreen_mode_switch, 'active')
+ # def _on_widget_destroy(self, widget):
+ # self.settings.unbind('widescreen-mode', self.widescreen_mode_switch, 'active')
diff --git a/ui/src/shortcutdialog.py b/ui/src/shortcutdialog.py
index 356c74e..4bb1e3b 100644
--- a/ui/src/shortcutdialog.py
+++ b/ui/src/shortcutdialog.py
@@ -36,9 +36,6 @@ class ShortcutDialog(Gtk.Dialog):
keycode,
state
)
- label = Gtk.accelerator_get_label(keyval, state)
-
- # hacky way to store the label, causes warnings from the WM
SettingsManager.get_instance().settings.set_strv(self.settings_key, [binding])
self.close()
diff --git a/ui/src/virtualdisplay.in b/ui/src/virtualdisplay.in
new file mode 100755
index 0000000..882b148
--- /dev/null
+++ b/ui/src/virtualdisplay.in
@@ -0,0 +1,107 @@
+#!/usr/bin/env python3
+
+# virtualdisplay.in
+#
+# Copyright 2024 Unknown
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+import logging
+import os
+import sys
+
+lib_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lib')
+sys.path.insert(0, lib_dir)
+
+from logging.handlers import TimedRotatingFileHandler
+
+config_home = os.environ.get('XDG_CONFIG_HOME', '~/.config')
+config_dir = os.path.expanduser(config_home)
+state_home = os.environ.get('XDG_STATE_HOME', '~/.local/state')
+state_dir = os.path.expanduser(state_home)
+breezy_state_dir = os.path.join(state_dir, 'breezy_gnome')
+log_dir = os.path.join(breezy_state_dir, 'logs/ui')
+os.makedirs(log_dir, exist_ok=True)
+
+logger = logging.getLogger('breezy_ui')
+logger.setLevel(logging.INFO)
+logname = os.path.join(log_dir, "breezy_desktop.log")
+handler = TimedRotatingFileHandler(logname, when="midnight", backupCount=30)
+handler.suffix = "%Y%m%d"
+formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+handler.setFormatter(formatter)
+logger.addHandler(handler)
+
+import sys
+import argparse
+import signal
+import time
+
+VERSION = '@VERSION@'
+
+xdg_data_home = os.getenv('XDG_DATA_HOME', os.path.join(os.path.expanduser('~'), '.local', 'share'))
+appdir = os.getenv('APPDIR', xdg_data_home)
+locale_dir = os.path.join(appdir, 'locale')
+pkgdatadir = os.path.join(appdir, 'breezydesktop')
+sys.path.insert(1, pkgdatadir)
+
+import gi
+
+gi.require_version('GLib', '2.0')
+
+from gi.repository import GLib
+
+def graceful_shutdown(signum, frame):
+ global virtual_display_instance
+ global loop
+
+ if virtual_display_instance is not None:
+ virtual_display_instance.terminate()
+
+def _on_display_closed():
+ global loop
+ loop.quit()
+
+def create_display(width, height, framerate):
+ global virtual_display_instance
+
+ virtual_display_instance = VirtualDisplay(width, height, framerate, _on_display_closed)
+ virtual_display_instance.create()
+
+if __name__ == "__main__":
+ from breezydesktop import virtualdisplay
+ from breezydesktop.virtualdisplay import VirtualDisplay
+
+ global virtual_display_instance
+ global loop
+
+ parser = argparse.ArgumentParser(description="Virtual display arguments")
+ parser.add_argument("--height", type=int, required=True, help="Height of the display")
+ parser.add_argument("--width", type=int, required=True, help="Width of the display")
+ parser.add_argument("--framerate", type=int, default=60, help="Framerate of the display")
+ args = parser.parse_args()
+
+ signal.signal(signal.SIGTERM, graceful_shutdown)
+ signal.signal(signal.SIGINT, graceful_shutdown)
+
+ loop = GLib.MainLoop()
+
+ try:
+ GLib.idle_add(create_display, args.width, args.height, args.framerate)
+ loop.run()
+ except Exception as e:
+ logger.error(f"Error in main loop: {e}")
+ sys.exit(1)
\ No newline at end of file
diff --git a/ui/src/virtualdisplay.py b/ui/src/virtualdisplay.py
new file mode 100644
index 0000000..64eb478
--- /dev/null
+++ b/ui/src/virtualdisplay.py
@@ -0,0 +1,100 @@
+#!/usr/bin/python3
+
+import argparse
+import logging
+import sys
+import signal
+from .lib import pydbus
+import gi
+import time
+
+gi.require_version('Gst', '1.0')
+from gi.repository import GLib, GObject, Gst
+
+logger = logging.getLogger('breezy_ui')
+
+screen_cast_iface = 'org.gnome.Mutter.ScreenCast'
+screen_cast_session_iface = 'org.gnome.Mutter.ScreenCast.Session'
+screen_cast_stream_iface = 'org.gnome.Mutter.ScreenCast.Session'
+gst_pipeline_format = "pipewiresrc path=%u ! video/x-raw,max-framerate=%d/1,width=%d,height=%d ! fakesink sync=false"
+
+class VirtualDisplay:
+ def __init__(self, width, height, framerate, on_closed_cb):
+ self.width = width
+ self.height = height
+ self.framerate = framerate
+ self.on_closed_cb = on_closed_cb
+
+ Gst.init(None)
+
+ def _screen_cast_session(self):
+ bus = pydbus.SessionBus()
+ screen_cast = bus.get(screen_cast_iface, '/org/gnome/Mutter/ScreenCast')
+ session_path = screen_cast.CreateSession([])
+ screen_cast_session = bus.get(screen_cast_iface, session_path)
+
+ return screen_cast_session
+
+ def _on_session_closed(self):
+ self.stream = None
+ self.terminate()
+
+ def create(self):
+ session = self._screen_cast_session()
+ session.onClosed = self._on_session_closed
+ stream_path = session.RecordVirtual({
+ 'is-platform': GLib.Variant.new_boolean(True),
+ })
+ bus = pydbus.SessionBus()
+ self.stream = bus.get(screen_cast_iface, stream_path)
+
+ self.stream.onPipeWireStreamAdded = self._on_pipewire_stream_added
+
+ session.Start()
+
+ def terminate(self):
+ try:
+ if self.stream is not None:
+ self.stream.Stop()
+ except Exception as e:
+ logger.error("Failed to stop stream: %s" % e)
+
+ try:
+ if self.pipeline is not None:
+ self.pipeline.send_event(Gst.Event.new_eos())
+ self.pipeline.set_state(Gst.State.NULL)
+ except Exception as e:
+ logger.error("Failed to stop pipeline: %s" % e)
+
+ self.on_closed_cb()
+
+ def _on_message(self, bus, message):
+ type = message.type
+ logger.info("message type: %s" % type)
+ if type == Gst.MessageType.EOS:
+ self.pipeline = None
+ self.terminate()
+ elif type == Gst.MessageType.ERROR:
+ err, debug = message.parse_error()
+ logger.error("Error: %s" % err)
+ logger.error("Debug: %s" % debug)
+ self.terminate()
+
+ def _on_pipewire_stream_added(self, node_id):
+ self.pipeline = Gst.parse_launch(gst_pipeline_format % (node_id, self.framerate, self.width, self.height))
+ self.pipeline.set_state(Gst.State.PLAYING)
+ self.pipeline.get_bus().connect('message', self._on_message)
+ self.pipeline.set_state(Gst.State.PAUSED)
+
+def is_screencast_available():
+ try:
+ bus = pydbus.SessionBus()
+ # Try to get the ScreenCast interface
+ screen_cast = bus.get(screen_cast_iface, '/org/gnome/Mutter/ScreenCast')
+ return True
+ except Exception as e:
+ logger.warning(f"ScreenCast portal not available: {e}")
+ return False
+
+
+
diff --git a/ui/src/virtualdisplaymanager.py b/ui/src/virtualdisplaymanager.py
new file mode 100644
index 0000000..dd02bc1
--- /dev/null
+++ b/ui/src/virtualdisplaymanager.py
@@ -0,0 +1,122 @@
+import gi
+import json
+import os
+import signal
+import subprocess
+import time
+from pathlib import Path
+
+import logging
+logger = logging.getLogger('breezy_ui')
+
+gi.require_version('GLib', '2.0')
+from gi.repository import GLib, GObject
+
+xdg_bin_home = os.getenv('XDG_BIN_HOME', os.path.join(os.path.expanduser('~'), '.local', 'bin'))
+bindir = os.getenv('BINDIR', xdg_bin_home)
+
+class VirtualDisplayManager(GObject.GObject):
+ __gproperties__ = {
+ 'displays': (object, 'Displays', 'A list of the displays', GObject.ParamFlags.READWRITE)
+ }
+ _instance = None
+
+ @staticmethod
+ def get_instance():
+ if not VirtualDisplayManager._instance:
+ VirtualDisplayManager._instance = VirtualDisplayManager()
+
+ return VirtualDisplayManager._instance
+
+ def __init__(self):
+ GObject.GObject.__init__(self)
+
+ self.shm_path = Path("/dev/shm/breezy_virtual_displays.json")
+ self._load_displays()
+ self._prune_dead_display_processes()
+
+ GLib.timeout_add_seconds(15, self._prune_dead_display_processes)
+
+ def _process_dead(self, pid):
+ if (not os.path.exists(f"/proc/{pid}")):
+ return True
+
+ try:
+ if (os.waitpid(pid, os.WNOHANG) == (pid, 0)):
+ return True
+ except ChildProcessError:
+ # process isn't tied to the current process, it's not dead if it's still open
+ return False
+
+ return False
+
+ def _prune_dead_display_processes(self):
+ new_displays = [disp for disp in self.displays if not self._process_dead(disp['pid'])]
+ if new_displays != self.displays:
+ self.set_property('displays', new_displays)
+ self._save_processes()
+
+ return GLib.SOURCE_CONTINUE
+
+ def create_virtual_display(self, width, height, framerate):
+ try:
+ process = subprocess.Popen(
+ [f"{bindir}/virtualdisplay", "--width", str(width), "--height", str(height), "--framerate", str(framerate)],
+ start_new_session=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True
+ )
+
+ if process.returncode is not None:
+ logger.error(f"Failed to create virtual display: {process.stderr.read()}")
+ return
+
+ self.displays.append({
+ 'pid': process.pid,
+ 'width': width,
+ 'height': height
+ })
+ self.set_property('displays', self.displays)
+ self._save_processes()
+ except Exception as e:
+ logger.error(f"Failed to create virtual display: {e}")
+
+ def destroy_virtual_display(self, pid: str) -> bool:
+ try:
+ # Send SIGTERM to allow graceful shutdown
+ os.killpg(pid, signal.SIGTERM)
+ self.set_property('displays', [disp for disp in self.displays if disp['pid'] != pid])
+ self._save_processes()
+ return True
+ except ProcessLookupError:
+ # Process already gone, delete pid from list
+ self.set_property('displays', [disp for disp in self.displays if disp['pid'] != pid])
+ self._save_processes()
+ return True
+ except Exception as e:
+ print(f"Failed to kill process {pid}: {e}")
+ return False
+
+ def _save_processes(self):
+ with open(self.shm_path, 'w') as f:
+ json.dump(self.displays, f)
+
+ def _load_displays(self):
+ displays = []
+ if self.shm_path.exists():
+ try:
+ with open(self.shm_path, 'r') as f:
+ displays = json.load(f)
+ except Exception:
+ displays = []
+
+ self.set_property('displays', displays)
+
+ def do_set_property(self, prop, value):
+ if prop.name == 'displays':
+ self.displays = value
+
+ def do_get_property(self, prop):
+ if prop.name == 'displays':
+ return self.displays
\ No newline at end of file
diff --git a/ui/src/virtualdisplayrow.py b/ui/src/virtualdisplayrow.py
new file mode 100644
index 0000000..ef4593a
--- /dev/null
+++ b/ui/src/virtualdisplayrow.py
@@ -0,0 +1,31 @@
+from gi.repository import Adw, Gtk
+from .virtualdisplaymanager import VirtualDisplayManager
+
+import gettext
+
+_ = gettext.gettext
+
+@Gtk.Template(resource_path='/com/xronlinux/BreezyDesktop/gtk/virtual-display-row.ui')
+class VirtualDisplayRow(Adw.ActionRow):
+ __gtype_name__ = "VirtualDisplayRow"
+
+ remove_virtual_display_button = Gtk.Template.Child()
+
+ def __init__(self, pid, width, height, framerate):
+ super(Adw.ActionRow, self).__init__()
+ self.init_template()
+ self.pid = pid
+
+ icon = Gtk.Image.new_from_icon_name("video-display-symbolic")
+
+ # padding around the icon
+ self.add_prefix(Gtk.Label(label=" "))
+ self.add_prefix(icon)
+ self.add_prefix(Gtk.Label(label=" "))
+
+ self.set_subtitle(f"{width} x {height}")
+
+ self.remove_virtual_display_button.connect('clicked', self._remove_virtual_display)
+
+ def _remove_virtual_display(self, widget):
+ VirtualDisplayManager.get_instance().destroy_virtual_display(self.pid)
\ No newline at end of file
diff --git a/ui/src/window.py b/ui/src/window.py
index 502f7dc..330e743 100644
--- a/ui/src/window.py
+++ b/ui/src/window.py
@@ -22,6 +22,7 @@ from .extensionsmanager import ExtensionsManager
from .license import BREEZY_GNOME_FEATURES
from .licensedialog import LicenseDialog
from .statemanager import StateManager
+from .settingsmanager import SettingsManager
from .connecteddevice import ConnectedDevice
from .failedverification import FailedVerification
from .nodevice import NoDevice
@@ -42,14 +43,6 @@ class BreezydesktopWindow(Gtk.ApplicationWindow):
def __init__(self, skip_verification, **kwargs):
super().__init__(**kwargs)
-
- self._skip_verification = skip_verification
-
- self.state_manager = StateManager.get_instance()
- self.state_manager.connect('device-update', self._handle_state_update)
- self.state_manager.connect('notify::license-action-needed', self._handle_state_update)
- self.state_manager.connect('notify::license-present', self._handle_state_update)
- self.state_manager.connect('notify::enabled-features-list', self._handle_state_update)
self.connected_device = ConnectedDevice()
self.failed_verification = FailedVerification()
@@ -57,6 +50,16 @@ class BreezydesktopWindow(Gtk.ApplicationWindow):
self.no_driver = NoDriver()
self.no_extension = NoExtension()
self.no_license = NoLicense()
+
+ self._skip_verification = skip_verification
+
+ self.settings = SettingsManager.get_instance().settings
+ self.state_manager = StateManager.get_instance()
+ self.state_manager.connect('device-update', self._handle_state_update)
+ self.state_manager.connect('notify::license-action-needed', self._handle_state_update)
+ self.state_manager.connect('notify::license-present', self._handle_state_update)
+ self.state_manager.connect('notify::enabled-features-list', self._handle_state_update)
+ self.settings.connect('changed::debug-no-device', self._handle_settings_update)
self.license_action_needed_button.connect('clicked', self._on_license_button_clicked)
self.missing_breezy_features_button.connect('clicked', self._on_license_button_clicked)
@@ -67,6 +70,9 @@ class BreezydesktopWindow(Gtk.ApplicationWindow):
self.connect("destroy", self._on_window_destroy)
+ def _handle_settings_update(self, settings_manager, key):
+ self._handle_state_update(self.state_manager, None)
+
def _handle_state_update(self, state_manager, val):
GLib.idle_add(self._handle_state_update_gui, state_manager)
@@ -79,7 +85,10 @@ class BreezydesktopWindow(Gtk.ApplicationWindow):
for child in self.main_content:
self.main_content.remove(child)
- if not self._skip_verification and not verify_installation():
+ if self.settings.get_boolean('debug-no-device'):
+ self.main_content.append(self.connected_device)
+ self.connected_device.set_device_name('Fake device')
+ elif not self._skip_verification and not verify_installation():
self.main_content.append(self.failed_verification)
elif not ExtensionsManager.get_instance().is_installed():
self.main_content.append(self.no_extension)