From 449620a1b11ccf5b3c4adc1fbe8c81960268d346 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Thu, 6 Feb 2025 14:03:24 -0800 Subject: [PATCH] Add more controls, fix vertical wrapping, add automatic wrap support --- gnome/src/extension.js | 6 ++ gnome/src/virtualmonitorsactor.js | 76 +++++++++------ .../com.xronlinux.BreezyDesktop.gschema.xml | 9 ++ ui/src/connecteddevice.py | 21 ++++- ui/src/gtk/connected-device.ui | 93 ++++++++++++++++++- 5 files changed, 173 insertions(+), 32 deletions(-) diff --git a/gnome/src/extension.js b/gnome/src/extension.js index 6a86e20..4d4daa5 100644 --- a/gnome/src/extension.js +++ b/gnome/src/extension.js @@ -51,6 +51,7 @@ export default class BreezyDesktopExtension extends Extension { this._monitor_wrapping_scheme_binding = null; this._viewport_offset_x_binding = null; this._viewport_offset_y_binding = null; + this._monitor_spacing_binding = null; this._distance_connection = null; this._follow_threshold_connection = null; this._widescreen_mode_settings_connection = null; @@ -333,6 +334,7 @@ export default class BreezyDesktopExtension extends Extension { this._monitor_wrapping_scheme_binding = this.settings.bind('monitor-wrapping-scheme', this._overlay_content, 'monitor-wrapping-scheme', Gio.SettingsBindFlags.DEFAULT); this._viewport_offset_x_binding = this.settings.bind('viewport-offset-x', this._overlay_content, 'viewport-offset-x', Gio.SettingsBindFlags.DEFAULT); this._viewport_offset_y_binding = this.settings.bind('viewport-offset-y', this._overlay_content, 'viewport-offset-y', Gio.SettingsBindFlags.DEFAULT); + this._monitor_spacing_binding = this.settings.bind('monitor-spacing', this._overlay_content, 'monitor-spacing', Gio.SettingsBindFlags.DEFAULT); this._distance_binding = this.settings.bind('display-distance', this._overlay_content, '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)); @@ -598,6 +600,10 @@ export default class BreezyDesktopExtension extends Extension { this.settings.unbind(this._distance_binding); this._distance_binding = null; } + if (this._monitor_spacing_binding) { + this.settings.unbind(this._monitor_spacing_binding); + this._monitor_spacing_binding = null; + } if (this._viewport_offset_x_binding) { this.settings.unbind(this._viewport_offset_x_binding); this._viewport_offset_x_binding = null; diff --git a/gnome/src/virtualmonitorsactor.js b/gnome/src/virtualmonitorsactor.js index f005379..199d024 100644 --- a/gnome/src/virtualmonitorsactor.js +++ b/gnome/src/virtualmonitorsactor.js @@ -68,7 +68,7 @@ function degreesToRadians(degrees) { /*** * @returns {Object} - containing `start`, `center`, and `end` radians for rotating the given monitor */ -function monitorWrap(cachedMonitorWrap, radiusPixels, monitorBeginPixel, monitorLengthPixels) { +function monitorWrap(cachedMonitorWrap, radiusPixels, monitorSpacingPixels, monitorBeginPixel, monitorLengthPixels) { let closestWrap = cachedMonitorWrap.reduce((previous, current) => { return (!previous || Math.abs(current.pixel - monitorBeginPixel) < Math.abs(previous.pixel - monitorBeginPixel)) ? current : previous; }, undefined); @@ -84,8 +84,9 @@ function monitorWrap(cachedMonitorWrap, radiusPixels, monitorBeginPixel, monitor cachedMonitorWrap.push(closestWrap); } + const spacingRadians = monitorBeginPixel === 0 ? 0 : Math.asin(monitorSpacingPixels / 2 / radiusPixels); const monitorHalfRadians = Math.asin(monitorLengthPixels / 2 / radiusPixels); - const centerRadians = closestWrap.radians + monitorHalfRadians; + const centerRadians = closestWrap.radians + spacingRadians + 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 @@ -107,7 +108,7 @@ function monitorWrap(cachedMonitorWrap, radiusPixels, monitorBeginPixel, monitor * @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) { +function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingScheme, monitorSpacing) { const aspect = fovDetails.widthPixels / fovDetails.heightPixels; const fovVerticalRadians = degreesToRadians(fovDetails.fovDegrees / Math.sqrt(1 + aspect * aspect)); @@ -126,7 +127,7 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch cachedMonitorWrap.push({ pixel: 0, radians: -fovHorizontalRadians / 2 }); monitorDetailsList.forEach(monitorDetails => { - const monitorWrapDetails = monitorWrap(cachedMonitorWrap, edgeRadius, monitorDetails.x, monitorDetails.width); + const monitorWrapDetails = monitorWrap(cachedMonitorWrap, edgeRadius, monitorSpacing * fovDetails.widthPixels, monitorDetails.x, monitorDetails.width); const monitorCenterRadius = Math.sqrt(Math.pow(edgeRadius, 2) - Math.pow(monitorDetails.width / 2, 2)) monitorPlacements.push({ @@ -145,7 +146,10 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch // up is flat when wrapping horizontally -(monitorDetails.y + monitorDetails.height / 2 - fovDetails.heightPixels / 2) ], - rotationAngleRadians: -monitorWrapDetails.center + rotationAngleRadians: { + x: 0, + y: -monitorWrapDetails.center + } }); }); } else if (monitorWrappingScheme === 'vertical') { @@ -156,14 +160,14 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch cachedMonitorWrap.push({ pixel: 0, radians: -fovVerticalRadians / 2 }); monitorDetailsList.forEach(monitorDetails => { - const monitorWrapDetails = monitorWrap(cachedMonitorWrap, edgeRadius, monitorDetails.y, monitorDetails.height); + const monitorWrapDetails = monitorWrap(cachedMonitorWrap, edgeRadius, monitorSpacing * fovDetails.heightPixels, monitorDetails.y, monitorDetails.height); const monitorCenterRadius = Math.sqrt(Math.pow(edgeRadius, 2) - Math.pow(monitorDetails.height / 2, 2)) ; monitorPlacements.push({ topLeftNoRotate: [ monitorCenterRadius, - -monitorDetails.x, - -(monitorDetails.height - fovDetails.heightPixels) / 2 + monitorDetails.x, + (monitorDetails.height - fovDetails.heightPixels) / 2 ], center: [ // north is adjacent where radius is the hypotenuse, using monitorWrapDetails.center as the radians @@ -175,7 +179,10 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch // up is opposite where radius is the hypotenuse, using monitorWrapDetails.center as the radians -monitorCenterRadius * Math.sin(monitorWrapDetails.center) ], - rotationAngleRadians: -monitorWrapDetails.center + rotationAngleRadians: { + x: -monitorWrapDetails.center, + y: 0 + } }); }); } else { @@ -184,7 +191,7 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch monitorPlacements.push({ topLeftNoRotate: [ centerRadius, - -monitorDetails.x, + monitorDetails.x, -monitorDetails.y ], center: [ @@ -192,7 +199,10 @@ function monitorsToPlacements(fovDetails, monitorDetailsList, monitorWrappingSch -(monitorDetails.x + monitorDetails.width / 2 - fovDetails.widthPixels / 2), -(monitorDetails.y + monitorDetails.height / 2 - fovDetails.heightPixels / 2) ], - rotationAngleRadians: 0 + rotationAngleRadians: { + x: 0, + y: 0 + } }); }); } @@ -266,13 +276,6 @@ export const VirtualMonitorEffect = GObject.registerClass({ GObject.ParamFlags.READWRITE, 1, 10000, 1080 ), - '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'] - ), 'focused-monitor-index': GObject.ParamSpec.int( 'focused-monitor-index', 'Focused Monitor Index', @@ -391,10 +394,8 @@ export const VirtualMonitorEffect = GObject.registerClass({ [-noRotationVector[1], -noRotationVector[2], this._current_display_distance * -noRotationVector[0]]); const rotation_radians = this.monitor_placements[this.monitor_index].rotationAngleRadians; - this.set_uniform_float(this.get_uniform_location("u_rotation_x_radians"), 1, - [this.monitor_wrapping_scheme === 'vertical' ? rotation_radians : 0.0]); - this.set_uniform_float(this.get_uniform_location("u_rotation_y_radians"), 1, - [this.monitor_wrapping_scheme === 'horizontal' ? rotation_radians : 0.0]); + this.set_uniform_float(this.get_uniform_location("u_rotation_x_radians"), 1, [rotation_radians.x]); + this.set_uniform_float(this.get_uniform_location("u_rotation_y_radians"), 1, [rotation_radians.y]); } perspective(fovDiagonalRadians, aspect, near, far) { @@ -559,6 +560,13 @@ export const VirtualMonitorsActor = GObject.registerClass({ GObject.ParamFlags.READWRITE, 'horizontal', ['horizontal', 'vertical', 'none'] ), + 'monitor-spacing': GObject.ParamSpec.double( + 'monitor-spacing', + 'Monitor Spacing', + 'Visual spacing between monitors', + GObject.ParamFlags.READWRITE, + 0.0, 1.0, 0.0 + ), 'target-monitor': GObject.ParamSpec.jsobject( 'target-monitor', 'Target Monitor', @@ -700,14 +708,12 @@ export const VirtualMonitorsActor = GObject.registerClass({ monitor_placements: this.monitor_placements, display_distance: this.display_distance, display_distance_default: Math.max(this.toggle_display_distance_start, this.toggle_display_distance_end), - monitor_wrapping_scheme: this.monitor_wrapping_scheme, actor_to_display_ratios: actorToDisplayRatios, actor_to_display_offsets: actorToDisplayOffsets }); containerActor.add_effect_with_name('viewport-effect', effect); this.add_child(containerActor); this.bind_property('monitor-placements', effect, 'monitor-placements', GObject.BindingFlags.DEFAULT); - this.bind_property('monitor-wrapping-scheme', effect, 'monitor-wrapping-scheme', GObject.BindingFlags.DEFAULT); this.bind_property('imu-snapshots', effect, 'imu-snapshots', GObject.BindingFlags.DEFAULT); this.bind_property('focused-monitor-index', effect, 'focused-monitor-index', GObject.BindingFlags.DEFAULT); this.bind_property('display-distance', effect, 'display-distance', GObject.BindingFlags.DEFAULT); @@ -751,13 +757,30 @@ export const VirtualMonitorsActor = GObject.registerClass({ 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.connect('notify::monitor-wrapping-scheme', this._update_monitor_placements.bind(this)); + this.connect('notify::monitor-spacing', this._update_monitor_placements.bind(this)); this.connect('notify::viewport-offset-x', this._update_monitor_placements.bind(this)); this.connect('notify::viewport-offset-y', this._update_monitor_placements.bind(this)); this._handle_display_distance_properties_change(); } _update_monitor_placements() { - Globals.logger.log_debug(`\t\t\tUpdating monitor placements ${this.viewport_offset_x}, ${this.viewport_offset_y} ${Globals.data_stream.device_data.displayFov}`); + // 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.width >= (maxY - minY) / this.height) { + actualWrapScheme = 'horizontal'; + } else { + actualWrapScheme = 'vertical'; + } + } + this.monitor_placements = monitorsToPlacements( { fovDegrees: Globals.data_stream.device_data.displayFov, @@ -772,7 +795,8 @@ export const VirtualMonitorsActor = GObject.registerClass({ width: monitor.width, height: monitor.height })), - this.monitor_wrapping_scheme + actualWrapScheme, + this.monitor_spacing ); // normalize the center vectors diff --git a/ui/data/com.xronlinux.BreezyDesktop.gschema.xml b/ui/data/com.xronlinux.BreezyDesktop.gschema.xml index 71cabf2..429d0e9 100644 --- a/ui/data/com.xronlinux.BreezyDesktop.gschema.xml +++ b/ui/data/com.xronlinux.BreezyDesktop.gschema.xml @@ -118,6 +118,15 @@ How the monitors are wrapped around the viewport + + + 0.0 + + Monitor spacing + + How far apart the monitors are visually (not logically) + + false diff --git a/ui/src/connecteddevice.py b/ui/src/connecteddevice.py index 9ab3523..682d8eb 100644 --- a/ui/src/connecteddevice.py +++ b/ui/src/connecteddevice.py @@ -52,6 +52,12 @@ class ConnectedDevice(Gtk.Box): text_scaling_scale = Gtk.Template.Child() text_scaling_adjustment = 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): @@ -68,7 +74,10 @@ class ConnectedDevice(Gtk.Box): self.set_toggle_display_distance_start_button, self.set_toggle_display_distance_end_button, self.movement_look_ahead_scale, - self.monitor_wrapping_scheme_menu + 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 @@ -86,10 +95,13 @@ class ConnectedDevice(Gtk.Box): self.settings.bind('use-highest-refresh-rate', self.use_highest_refresh_rate_switch, 'active', Gio.SettingsBindFlags.DEFAULT) self.settings.bind('fast-sbs-mode-switching', self.fast_sbs_mode_switch, 'active', Gio.SettingsBindFlags.DEFAULT) self.settings.bind('look-ahead-override', self.movement_look_ahead_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT) + 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.monitor_wrapping_scheme_menu.connect('changed', self._handle_monitor_wrapping_scheme_menu_changed) - self._handle_monitor_wrapping_scheme_setting_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], @@ -126,9 +138,8 @@ class ConnectedDevice(Gtk.Box): self.connect("destroy", self._on_widget_destroy) - def _handle_monitor_wrapping_scheme_setting_changed(self): - current_scheme = self.settings.get_string('monitor-wrapping-scheme') - self.monitor_wrapping_scheme_menu.set_active_id(current_scheme) + 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()) diff --git a/ui/src/gtk/connected-device.ui b/ui/src/gtk/connected-device.ui index 3b73751..7596454 100644 --- a/ui/src/gtk/connected-device.ui +++ b/ui/src/gtk/connected-device.ui @@ -155,7 +155,7 @@ Multi-monitor wrapping - When displaying multiple monitors, choose how they should wrap around you + When there are multiple monitors, choose how they should wrap around you. 2 @@ -176,6 +176,97 @@ + + + Multi-monitor spacing + Put empty space between monitors, when there are multiple. + + + 3 + true + 0 + 2 + 350 + false + + + 0.0 + 0.5 + 0.01 + 0.0 + + + + + + + + + + + + + + Viewport horizontal offset + By default, the viewport will center on the primary display. Use this slider to move the viewport to the left or right. + + + 3 + true + 0 + 1 + 350 + false + + + -2.5 + 2.5 + 0.1 + 0.0 + + + + + + + + + + + + + + + + Viewport vertical offset + By default, the viewport will center on the primary display. Use this slider to move the viewport up or down. + + + 3 + true + 0 + 1 + 350 + false + + + -2.5 + 2.5 + 0.1 + 0.0 + + + + + + + + + + + + +