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