parent
0cba1b8075
commit
7dc965c684
|
|
@ -1,2 +1,3 @@
|
|||
/vulkan/build/
|
||||
/build/
|
||||
__pycache__
|
||||
|
|
|
|||
|
|
@ -58,6 +58,11 @@ if [ ! -L $extensions_dir/breezydesktop@org.xronlinux ]; then
|
|||
chown -R $user:$group $extensions_dir/breezydesktop@org.xronlinux
|
||||
fi
|
||||
|
||||
glib-compile-schemas --targetdir=$extensions_dir/breezydesktop@org.xronlinux/schemas/ $extensions_dir/breezydesktop@org.xronlinux/schemas/
|
||||
|
||||
sudo cp $extensions_dir/breezydesktop@org.xronlinux/schemas/org.gnome.shell.extensions.breezy-desktop.gschema.xml /usr/share/glib-2.0/schemas/
|
||||
sudo glib-compile-schemas /usr/share/glib-2.0/schemas/
|
||||
|
||||
echo "Breezy Desktop extension is installed. Please log out, log back in, \
|
||||
and then run the following command to enable it:\
|
||||
gnome-extensions enable breezydesktop@org.xronlinux"
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ const SUPPORTED_MONITOR_PRODUCTS = [
|
|||
export default class BreezyDesktopExtension extends Extension {
|
||||
constructor(metadata, uuid) {
|
||||
super(metadata, uuid);
|
||||
|
||||
this.settings = this.getSettings();
|
||||
|
||||
// Set/destroyed by enable/disable
|
||||
this._cursor_manager = null;
|
||||
|
|
@ -129,26 +131,22 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
|
||||
this._xr_effect = new XREffect({
|
||||
target_monitor: this._target_monitor,
|
||||
target_framerate: this._refresh_rate ?? 60
|
||||
target_framerate: this._refresh_rate ?? 60,
|
||||
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'),
|
||||
});
|
||||
|
||||
this.settings.bind('effect-enable', this._xr_effect, 'effect-enable', Gio.SettingsBindFlags.DEFAULT)
|
||||
this.settings.bind('display-distance', this._xr_effect, 'display-distance', Gio.SettingsBindFlags.DEFAULT)
|
||||
this.settings.bind('toggle-display-distance-start', this._xr_effect, 'toggle-display-distance-start', Gio.SettingsBindFlags.DEFAULT)
|
||||
this.settings.bind('toggle-display-distance-end', this._xr_effect, 'toggle-display-distance-end', Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
this._overlay.add_effect_with_name('xr-desktop', this._xr_effect);
|
||||
Meta.disable_unredirect_for_display(global.display);
|
||||
Main.wm.addKeybinding(
|
||||
'shortcut-recenter',
|
||||
this.getSettings(),
|
||||
Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
|
||||
Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW | Shell.ActionMode.POPUP,
|
||||
this._recenter_display.bind(this)
|
||||
);
|
||||
|
||||
Main.wm.addKeybinding(
|
||||
'shortcut-change-distance',
|
||||
this.getSettings(),
|
||||
Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
|
||||
Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW | Shell.ActionMode.POPUP,
|
||||
this._xr_effect._change_distance.bind(this._xr_effect)
|
||||
);
|
||||
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));
|
||||
} catch (e) {
|
||||
console.error('Error enabling XR effect', e);
|
||||
this._effect_disable();
|
||||
|
|
@ -156,6 +154,31 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
}
|
||||
}
|
||||
|
||||
_add_settings_keybinding(settings_key, bind_to_function) {
|
||||
Main.wm.addKeybinding(
|
||||
settings_key,
|
||||
this.settings,
|
||||
Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
|
||||
Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW | Shell.ActionMode.POPUP,
|
||||
bind_to_function
|
||||
);
|
||||
|
||||
// Connect to the 'changed' signal for the keybinding property
|
||||
this.settings.connect(`changed::${settings_key}`, () => {
|
||||
// Remove the old keybinding
|
||||
Main.wm.removeKeybinding(settings_key);
|
||||
|
||||
// Add the updated keybinding
|
||||
Main.wm.addKeybinding(
|
||||
settings_key,
|
||||
this.settings,
|
||||
Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
|
||||
Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW | Shell.ActionMode.POPUP,
|
||||
bind_to_function
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_recenter_display() {
|
||||
const file = Gio.file_new_for_path('/dev/shm/xr_driver_control');
|
||||
const stream = file.replace(null, false, Gio.FileCreateFlags.NONE, null);
|
||||
|
|
@ -168,8 +191,8 @@ export default class BreezyDesktopExtension extends Extension {
|
|||
|
||||
if (this._running_poller_id) GLib.source_remove(this._running_poller_id);
|
||||
|
||||
Main.wm.removeKeybinding('shortcut-recenter');
|
||||
Main.wm.removeKeybinding('shortcut-change-distance');
|
||||
Main.wm.removeKeybinding('recenter-display-shortcut');
|
||||
Main.wm.removeKeybinding('toggle-display-distance-shortcut');
|
||||
Meta.enable_unredirect_for_display(global.display);
|
||||
|
||||
if (this._overlay) {
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,22 +1,58 @@
|
|||
<schemalist gettext-domain="breezydesktop@org.xronlinux">
|
||||
<schema id="org.gnome.shell.extensions.breezy-desktop" path="/org/gnome/shell/extensions/breezy-desktop/">
|
||||
<key name="shortcut-recenter" type="as">
|
||||
<key name="effect-enable" type="b">
|
||||
<default>
|
||||
<![CDATA[['<Control><Super>space']]]>
|
||||
true
|
||||
</default>
|
||||
<summary>Enable XR effect</summary>
|
||||
<description>
|
||||
Enable XR effect
|
||||
</description>
|
||||
</key>
|
||||
<key name="recenter-display-shortcut" type="as">
|
||||
<default>
|
||||
<![CDATA[['<Control><Super>space', 'Ctrl+Super+Space']]]>
|
||||
</default>
|
||||
<summary>Re-center display</summary>
|
||||
<description>
|
||||
Shortcut to re-center the virtual display.
|
||||
</description>
|
||||
</key>
|
||||
<key name="shortcut-change-distance" type="as">
|
||||
<key name="toggle-display-distance-shortcut" type="as">
|
||||
<default>
|
||||
<![CDATA[['<Control><Super>Return']]]>
|
||||
<![CDATA[['<Control><Super>Return', 'Ctrl+Super+Return']]]>
|
||||
</default>
|
||||
<summary>Trigger change to display distance</summary>
|
||||
<description>
|
||||
Shortcut to change the display distance.
|
||||
</description>
|
||||
</key>
|
||||
<key name="display-distance" type="d">
|
||||
<default>
|
||||
1.05
|
||||
</default>
|
||||
<summary>Display distance</summary>
|
||||
<description>
|
||||
How far away the display appears. Farther will look smaller, closer will look larger.
|
||||
</description>
|
||||
</key>
|
||||
<key name="toggle-display-distance-start" type="d">
|
||||
<default>
|
||||
0.85
|
||||
</default>
|
||||
<summary>Display distance start</summary>
|
||||
<description>
|
||||
Start distance when using the "change distance" shortcut.
|
||||
</description>
|
||||
</key>
|
||||
<key name="toggle-display-distance-end" type="d">
|
||||
<default>
|
||||
1.05
|
||||
</default>
|
||||
<summary>Display distance end</summary>
|
||||
<description>
|
||||
End distance when using the "toggle display distance" shortcut.
|
||||
</description>
|
||||
</key>
|
||||
</schema>
|
||||
</schemalist>
|
||||
|
|
@ -25,8 +25,6 @@ import { getShaderSource } from "./shader.js";
|
|||
import { toSec } from "./time.js";
|
||||
|
||||
export const IPC_FILE_PATH = "/dev/shm/breezy_desktop_imu";
|
||||
const display_distance_nearest = 0.85;
|
||||
const display_distance_furthest = 1.05;
|
||||
|
||||
// the driver should be using the same data layout version
|
||||
const DATA_LAYOUT_VERSION = 2;
|
||||
|
|
@ -123,7 +121,7 @@ function setIntermittentUniformVariables() {
|
|||
const validKeepalive = Math.abs(toSec(currentDateMS) - toSec(imuDateMS)) < 5;
|
||||
const imuData = dataViewFloatArray(dataView, IMU_QUAT_DATA);
|
||||
const imuResetState = 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 && !imuResetState;
|
||||
const enabled = this.effect_enable && dataViewUint8(dataView, ENABLED) !== 0 && version === DATA_LAYOUT_VERSION && validKeepalive && !imuResetState;
|
||||
|
||||
if (enabled) {
|
||||
const displayRes = dataViewUintArray(dataView, DISPLAY_RES);
|
||||
|
|
@ -171,6 +169,13 @@ function setIntermittentUniformVariables() {
|
|||
|
||||
export const XREffect = GObject.registerClass({
|
||||
Properties: {
|
||||
'effect-enable': GObject.ParamSpec.boolean(
|
||||
'effect-enable',
|
||||
'Effect enable',
|
||||
'Whether this effect is enabled',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
true
|
||||
),
|
||||
'target-monitor': GObject.ParamSpec.jsobject(
|
||||
'target-monitor',
|
||||
'Target Monitor',
|
||||
|
|
@ -182,6 +187,33 @@ export const XREffect = GObject.registerClass({
|
|||
'Target Framerate',
|
||||
'Target framerate for this effect',
|
||||
GObject.ParamFlags.READWRITE, 60, 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
|
||||
),
|
||||
'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
|
||||
)
|
||||
}
|
||||
}, class XREffect extends Shell.GLSLEffect {
|
||||
|
|
@ -190,28 +222,24 @@ export const XREffect = GObject.registerClass({
|
|||
|
||||
this._frametime = Math.floor(1000 / this.target_framerate);
|
||||
|
||||
// slightly zoomed out by default
|
||||
this._display_distance = display_distance_furthest;
|
||||
this._display_distance_near = false;
|
||||
this._is_display_distance_at_end = false;
|
||||
this._distance_ease_timeline = null;
|
||||
}
|
||||
|
||||
_change_distance() {
|
||||
if (this._distance_ease_timeline?.is_playing()) this._distance_ease_timeline.stop();
|
||||
this._distance_ease_start = this._display_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);
|
||||
|
||||
if (this._display_distance_near) {
|
||||
this._distance_ease_timeline.connect('new-frame', () => {
|
||||
this._display_distance = this._distance_ease_start + this._distance_ease_timeline.get_progress() * (display_distance_furthest - this._distance_ease_start);
|
||||
});
|
||||
this._display_distance_near = false;
|
||||
} else {
|
||||
this._distance_ease_timeline.connect('new-frame', () => {
|
||||
this._display_distance = this._distance_ease_start - this._distance_ease_timeline.get_progress() * (this._distance_ease_start - display_distance_nearest);
|
||||
});
|
||||
this._display_distance_near = true;
|
||||
}
|
||||
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._is_display_distance_at_end = !this._is_display_distance_at_end;
|
||||
|
||||
this._distance_ease_timeline.start();
|
||||
}
|
||||
|
|
@ -252,7 +280,7 @@ export const XREffect = GObject.registerClass({
|
|||
}
|
||||
|
||||
if (this._dataView.byteLength === DATA_VIEW_LENGTH) {
|
||||
setSingleFloat(this, 'display_north_offset', this._display_distance);
|
||||
setSingleFloat(this, 'display_north_offset', this.display_distance);
|
||||
setSingleFloat(this, 'look_ahead_ms', lookAheadMS(this._dataView));
|
||||
setUniformMatrix(this, 'imu_quat_data', 4, this._dataView, IMU_QUAT_DATA);
|
||||
} else if (this._dataView.byteLength !== 0) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
import gi
|
||||
import sys
|
||||
import threading
|
||||
|
||||
gi.require_version("Gtk", "4.0")
|
||||
gi.require_version('Adw', '1')
|
||||
|
||||
from gi.repository import Adw, Gio, Gtk
|
||||
|
||||
from XRDriverIPC import XRDriverIPC
|
||||
from ShortcutDialog import bind_shortcut_settings
|
||||
|
||||
class Logger:
|
||||
def info(self, message):
|
||||
print(message)
|
||||
|
||||
def error(self, message):
|
||||
print(message)
|
||||
|
||||
class MainWindow(Gtk.ApplicationWindow):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_title("Breezy Desktop")
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_file("./breezy-desktop.ui")
|
||||
self.set_child(builder.get_object("main"))
|
||||
self.connected_device_info = builder.get_object("connected-device-info")
|
||||
self.connected_device_label = builder.get_object("connected-device-label")
|
||||
self.connected_device_settings = builder.get_object("connected-device-settings")
|
||||
self.connected_device_shortcuts = builder.get_object("connected-device-shortcuts")
|
||||
self.no_connected_device = builder.get_object("no-connected-device")
|
||||
|
||||
self.settings = Gio.Settings.new_with_path("org.gnome.shell.extensions.breezy-desktop", "/org/gnome/shell/extensions/breezy-desktop/")
|
||||
self.ipc = XRDriverIPC(logger = Logger())
|
||||
self._refresh_state()
|
||||
|
||||
bind_shortcut_settings(self, self.settings, [
|
||||
builder.get_object('reassign-recenter-display-shortcut-button'),
|
||||
builder.get_object('reassign-toggle-display-distance-shortcut-button'),
|
||||
])
|
||||
|
||||
self.bind_set_distance_toggle([
|
||||
builder.get_object('set-toggle-display-distance-start-button'),
|
||||
builder.get_object('set-toggle-display-distance-end-button')
|
||||
])
|
||||
display_distance_slider = builder.get_object('display-distance-slider')
|
||||
self.settings.bind('display-distance', display_distance_slider, 'value', Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
effect_enable_switch = builder.get_object('effect-enable')
|
||||
self.settings.bind('effect-enable', effect_enable_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
def _refresh_state(self):
|
||||
self.state = self.ipc.retrieve_driver_state()
|
||||
if self.state.get('connected_device_brand') and self.state.get('connected_device_model'):
|
||||
self.connected_device_info.set_visible(True)
|
||||
self.connected_device_settings.set_visible(True)
|
||||
self.connected_device_shortcuts.set_visible(True)
|
||||
self.no_connected_device.set_visible(False)
|
||||
self.connected_device_label.set_markup(f"<b>{self.state['connected_device_brand']} {self.state['connected_device_model']}</b>")
|
||||
else:
|
||||
self.connected_device_info.set_visible(False)
|
||||
self.connected_device_settings.set_visible(False)
|
||||
self.connected_device_shortcuts.set_visible(False)
|
||||
self.no_connected_device.set_visible(True)
|
||||
threading.Timer(1.0, self._refresh_state).start()
|
||||
|
||||
|
||||
def bind_set_distance_toggle(self, widgets):
|
||||
for widget in widgets:
|
||||
widget.connect('clicked', lambda *args, widget=widget: on_set_display_distance_toggle(self.settings, widget))
|
||||
reload_display_distance_toggle_button(self.settings, widget)
|
||||
|
||||
def reload_display_distance_toggle_button(settings, widget):
|
||||
distance = settings.get_double(widget.get_name())
|
||||
if distance: widget.set_label(str(distance))
|
||||
|
||||
def on_set_display_distance_toggle(settings, widget):
|
||||
distance = settings.get_double('display-distance')
|
||||
settings.set_double(widget.get_name(), distance)
|
||||
reload_display_distance_toggle_button(settings, widget)
|
||||
|
||||
class BreezyDesktop(Adw.Application):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.connect('activate', self.on_activate)
|
||||
|
||||
def on_activate(self, app):
|
||||
self.win = MainWindow(application=app)
|
||||
self.win.present()
|
||||
|
||||
app = BreezyDesktop(application_id="com.example.GtkApplication")
|
||||
app.run(sys.argv)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
|
||||
[dev-packages]
|
||||
pygobject-stubs = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.11"
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "f363f820b1e601b5678bb4d65939db586a4db4fb008ee873697aff3df08d0877"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.11"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {},
|
||||
"develop": {
|
||||
"pygobject-stubs": {
|
||||
"hashes": [
|
||||
"sha256:e1c7e32658213ae711d8afc5ea083a434231b8b588d1de23f50d5705a9c8eefe"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.11.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
from gi.repository import Gtk, Gdk
|
||||
|
||||
# ported from https://github.com/velitasali/gnome-shell-extension-awesome-tiles
|
||||
class ShortcutDialog:
|
||||
def __init__(self, settings, settings_key):
|
||||
self.settings = settings
|
||||
self.settings_key = settings_key
|
||||
|
||||
self._builder = Gtk.Builder()
|
||||
self._builder.add_from_file('./shortcut-dialog.ui')
|
||||
|
||||
self.widget = self._builder.get_object('dialog')
|
||||
|
||||
self.event_controller = self._builder.get_object('event-controller')
|
||||
self.key_pressed_connect_id = self.event_controller.connect('key-pressed', self._on_key_pressed)
|
||||
|
||||
def _on_key_pressed(self, widget, keyval, keycode, state):
|
||||
mask = state & Gtk.accelerator_get_default_mod_mask()
|
||||
mask &= ~Gdk.ModifierType.LOCK_MASK
|
||||
|
||||
done = True
|
||||
if mask == 0 and keyval == Gdk.KEY_Escape:
|
||||
self.widget.visible = False
|
||||
elif keyval == Gdk.KEY_BackSpace:
|
||||
self.settings.set_strv(self.settings_key, [])
|
||||
self.widget.close()
|
||||
elif is_binding_valid(mask, keycode, keyval) and is_accel_valid(state, keyval):
|
||||
binding = Gtk.accelerator_name_with_keycode(
|
||||
None,
|
||||
keyval,
|
||||
keycode,
|
||||
state
|
||||
)
|
||||
label = Gtk.accelerator_get_label(keyval, state)
|
||||
|
||||
# hacky way to store the label, causes warnings from the WM
|
||||
self.settings.set_strv(self.settings_key, [binding, label])
|
||||
|
||||
self.widget.close()
|
||||
else:
|
||||
done = False
|
||||
|
||||
if done and self.key_pressed_connect_id:
|
||||
self.event_controller.disconnect(self.key_pressed_connect_id)
|
||||
self.key_pressed_connect_id = None
|
||||
|
||||
return Gdk.EVENT_STOP
|
||||
|
||||
def is_binding_valid(mask, keycode, keyval):
|
||||
if mask == 0 or mask == Gdk.ModifierType.SHIFT_MASK and keycode != 0:
|
||||
if keyval >= Gdk.KEY_a and keyval <= Gdk.KEY_z or \
|
||||
keyval >= Gdk.KEY_A and keyval <= Gdk.KEY_Z or \
|
||||
keyval >= Gdk.KEY_0 and keyval <= Gdk.KEY_9 or \
|
||||
keyval >= Gdk.KEY_kana_fullstop and keyval <= Gdk.KEY_semivoicedsound or \
|
||||
keyval >= Gdk.KEY_Arabic_comma and keyval <= Gdk.KEY_Arabic_sukun or \
|
||||
keyval >= Gdk.KEY_Serbian_dje and keyval <= Gdk.KEY_Cyrillic_HARDSIGN or \
|
||||
keyval >= Gdk.KEY_Greek_ALPHAaccent and keyval <= Gdk.KEY_Greek_omega or \
|
||||
keyval >= Gdk.KEY_hebrew_doublelowline and keyval <= Gdk.KEY_hebrew_taf or \
|
||||
keyval >= Gdk.KEY_Thai_kokai and keyval <= Gdk.KEY_Thai_lekkao or \
|
||||
keyval >= Gdk.KEY_Hangul_Kiyeog and keyval <= Gdk.KEY_Hangul_J_YeorinHieuh or \
|
||||
keyval == Gdk.KEY_space and mask == 0 or \
|
||||
is_keyval_forbidden(keyval):
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_keyval_forbidden(keyval):
|
||||
forbidden_keyvals = [
|
||||
Gdk.KEY_Home,
|
||||
Gdk.KEY_Left,
|
||||
Gdk.KEY_Up,
|
||||
Gdk.KEY_Right,
|
||||
Gdk.KEY_Down,
|
||||
Gdk.KEY_Page_Up,
|
||||
Gdk.KEY_Page_Down,
|
||||
Gdk.KEY_End,
|
||||
Gdk.KEY_Tab,
|
||||
Gdk.KEY_KP_Enter,
|
||||
Gdk.KEY_Return,
|
||||
Gdk.KEY_Mode_switch
|
||||
]
|
||||
return keyval in forbidden_keyvals
|
||||
|
||||
def is_accel_valid(mask, keyval):
|
||||
return Gtk.accelerator_valid(keyval, mask) or (keyval == Gdk.KEY_Tab and mask != 0)
|
||||
|
||||
def bind_shortcut_settings(window, settings, widgets):
|
||||
for widget in widgets:
|
||||
settings.connect('changed::' + widget.get_name(), lambda *args, widget=widget: reload_shortcut_widget(settings, widget))
|
||||
widget.connect('clicked', lambda *args, widget=widget: on_assign_shortcut(window, settings, widget))
|
||||
|
||||
reload_shortcut_widgets(settings, widgets)
|
||||
|
||||
def on_assign_shortcut(window, settings, widget):
|
||||
dialog = ShortcutDialog(settings, widget.get_name())
|
||||
dialog.widget.set_transient_for(window)
|
||||
dialog.widget.present()
|
||||
|
||||
def reload_shortcut_widget(settings, widget):
|
||||
shortcut = settings.get_strv(widget.get_name())
|
||||
widget.set_label(shortcut[1] if len(shortcut) > 1 else 'Disabled')
|
||||
|
||||
def reload_shortcut_widgets(settings, widgets):
|
||||
for widget in widgets:
|
||||
reload_shortcut_widget(settings, widget)
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
import json
|
||||
import os
|
||||
import pwd
|
||||
import stat
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
# write-only file that the driver reads (but never writes) to get user-specified control flags
|
||||
CONTROL_FLAGS_FILE_PATH = '/dev/shm/xr_driver_control'
|
||||
|
||||
# read-only file that the driver writes (but never reads) to with its current state
|
||||
DRIVER_STATE_FILE_PATH = '/dev/shm/xr_driver_state'
|
||||
|
||||
CONTROL_FLAGS = ['recenter_screen', 'recalibrate', 'sbs_mode', 'refresh_device_license']
|
||||
SBS_MODE_VALUES = ['unset', 'enable', 'disable']
|
||||
MANAGED_EXTERNAL_MODES = ['virtual_display', 'sideview', 'none']
|
||||
VR_LITE_OUTPUT_MODES = ['mouse', 'joystick']
|
||||
|
||||
def parse_boolean(value, default):
|
||||
if not value:
|
||||
return default
|
||||
|
||||
return value.lower() == 'true'
|
||||
|
||||
|
||||
def parse_int(value, default):
|
||||
return int(value) if value.isdigit() else default
|
||||
|
||||
def parse_float(value, default):
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
def parse_string(value, default):
|
||||
return value if value else default
|
||||
|
||||
def parse_array(value, default):
|
||||
return value.split(",") if value else default
|
||||
|
||||
|
||||
CONFIG_PARSER_INDEX = 0
|
||||
CONFIG_DEFAULT_VALUE_INDEX = 1
|
||||
CONFIG_ENTRIES = {
|
||||
'disabled': [parse_boolean, True],
|
||||
'output_mode': [parse_string, 'mouse'],
|
||||
'external_mode': [parse_array, 'none'],
|
||||
'mouse_sensitivity': [parse_int, 30],
|
||||
'display_zoom': [parse_float, 1.0],
|
||||
'look_ahead': [parse_int, 0],
|
||||
'sbs_display_size': [parse_float, 1.0],
|
||||
'sbs_display_distance': [parse_float, 1.0],
|
||||
'sbs_content': [parse_boolean, False],
|
||||
'sbs_mode_stretched': [parse_boolean, False],
|
||||
'sideview_position': [parse_string, 'center'],
|
||||
'sideview_display_size': [parse_float, 1.0],
|
||||
'virtual_display_smooth_follow_enabled': [parse_boolean, False],
|
||||
'sideview_smooth_follow_enabled': [parse_boolean, False]
|
||||
}
|
||||
|
||||
class XRDriverIPC:
|
||||
def __init__(self, logger, user=None, user_home=None):
|
||||
self.breezy_installed = False
|
||||
self.breezy_installing = False
|
||||
self.user = user if user else pwd.getpwuid( os.getuid() )[0]
|
||||
self.user_home = user_home if user_home else os.path.expanduser("~")
|
||||
self.config_file_path = os.path.join(self.user_home, ".xreal_driver_config")
|
||||
self.config_script_path = os.path.join(self.user_home, "bin/xreal_driver_config")
|
||||
self.logger = logger
|
||||
|
||||
def retrieve_config(self):
|
||||
config = {}
|
||||
for key, value in CONFIG_ENTRIES.items():
|
||||
config[key] = value[CONFIG_DEFAULT_VALUE_INDEX]
|
||||
|
||||
try:
|
||||
with open(self.config_file_path, 'r') as f:
|
||||
for line in f:
|
||||
try:
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
key, value = line.strip().split('=')
|
||||
if key in CONFIG_ENTRIES:
|
||||
parser = CONFIG_ENTRIES[key][CONFIG_PARSER_INDEX]
|
||||
default_val = CONFIG_ENTRIES[key][CONFIG_DEFAULT_VALUE_INDEX]
|
||||
config[key] = parser(value, default_val)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error parsing line {line}: {e}")
|
||||
except FileNotFoundError as e:
|
||||
self.logger.error(f"Config file not found {e}")
|
||||
return config
|
||||
|
||||
config['ui_view'] = self.build_ui_view(config)
|
||||
|
||||
return config
|
||||
|
||||
def write_config(self, config):
|
||||
try:
|
||||
output = ""
|
||||
|
||||
# Since the UI doesn't refresh the config before it updates, the external_mode can get out of sync with
|
||||
# what's on disk. To avoid losing external_mode values, we retrieve the previous configs to preserve
|
||||
# any non-managed external modes.
|
||||
old_config = self._retrieve_config(self)
|
||||
|
||||
# remove the UI's "view" data, translate back to config values, and merge them in
|
||||
view = config.pop('ui_view', None)
|
||||
config.update(self.headset_mode_to_config(view['headset_mode'], view['is_joystick_mode'], old_config['external_mode']))
|
||||
|
||||
for key, value in config.items():
|
||||
if key != "updated":
|
||||
if isinstance(value, bool):
|
||||
output += f'{key}={str(value).lower()}\n'
|
||||
elif isinstance(value, int):
|
||||
output += f'{key}={value}\n'
|
||||
elif isinstance(value, list):
|
||||
output += f'{key}={",".join(value)}\n'
|
||||
else:
|
||||
output += f'{key}={value}\n'
|
||||
|
||||
temp_file = "temp.txt"
|
||||
|
||||
# Write to a temporary file
|
||||
with open(temp_file, 'w') as f:
|
||||
f.write(output)
|
||||
|
||||
# Atomically replace the old config file with the new one
|
||||
os.replace(temp_file, self.config_file_path)
|
||||
os.chmod(self.config_file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH)
|
||||
|
||||
config['ui_view'] = self.build_ui_view(self, config)
|
||||
|
||||
return config
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error writing config {e}")
|
||||
raise e
|
||||
|
||||
# like a SQL "view," these are computed values that are commonly used in the UI
|
||||
def build_ui_view(self, config):
|
||||
view = {}
|
||||
view['headset_mode'] = self.config_to_headset_mode(config)
|
||||
view['is_joystick_mode'] = config['output_mode'] == 'joystick'
|
||||
return view
|
||||
|
||||
def filter_to_other_external_modes(self, external_modes):
|
||||
return [mode for mode in external_modes if mode not in MANAGED_EXTERNAL_MODES]
|
||||
|
||||
def headset_mode_to_config(self, headset_mode, joystick_mode, old_external_modes):
|
||||
new_external_modes = self.filter_to_other_external_modes(old_external_modes)
|
||||
|
||||
config = {}
|
||||
if headset_mode == "virtual_display":
|
||||
# TODO - uncomment this when the driver can support multiple external_mode values
|
||||
# new_external_modes.append("virtual_display")
|
||||
new_external_modes = ["virtual_display"]
|
||||
config['output_mode'] = "external_only"
|
||||
config['disabled'] = False
|
||||
elif headset_mode == "vr_lite":
|
||||
config['output_mode'] = "joystick" if joystick_mode else "mouse"
|
||||
config['disabled'] = False
|
||||
elif headset_mode == "sideview":
|
||||
# TODO - uncomment this when the driver can support multiple external_mode values
|
||||
# new_external_modes.append("sideview")
|
||||
new_external_modes = ["sideview"]
|
||||
config['output_mode'] = "external_only"
|
||||
config['disabled'] = False
|
||||
else:
|
||||
config['output_mode'] = "external_only"
|
||||
|
||||
has_external_mode = len(new_external_modes) > 0
|
||||
if not has_external_mode:
|
||||
new_external_modes.append("none")
|
||||
config['external_mode'] = new_external_modes
|
||||
|
||||
return config
|
||||
|
||||
def config_to_headset_mode(self, config):
|
||||
if not config or config['disabled']:
|
||||
return "disabled"
|
||||
|
||||
if config['output_mode'] in VR_LITE_OUTPUT_MODES:
|
||||
return "vr_lite"
|
||||
|
||||
managed_mode = next((mode for mode in MANAGED_EXTERNAL_MODES if mode in config['external_mode']), None)
|
||||
if managed_mode and managed_mode != "none":
|
||||
return managed_mode
|
||||
|
||||
return "disabled"
|
||||
|
||||
def write_control_flags(self, control_flags):
|
||||
try:
|
||||
output = ""
|
||||
for key, value in control_flags.items():
|
||||
if key in CONTROL_FLAGS:
|
||||
if key == 'sbs_mode':
|
||||
if value not in SBS_MODE_VALUES:
|
||||
self.logger.error(f"Invalid value {value} for sbs_mode flag")
|
||||
continue
|
||||
elif not isinstance(value, bool):
|
||||
self.logger.error(f"Invalid value {value} for {key} flag")
|
||||
continue
|
||||
output += f'{key}={str(value).lower()}\n'
|
||||
|
||||
with open(CONTROL_FLAGS_FILE_PATH, 'w') as f:
|
||||
f.write(output)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error writing control flags {e}")
|
||||
|
||||
def retrieve_driver_state(self):
|
||||
state = {}
|
||||
state['heartbeat'] = 0
|
||||
state['connected_device_brand'] = None
|
||||
state['connected_device_model'] = None
|
||||
state['calibration_setup'] = "AUTOMATIC"
|
||||
state['calibration_state'] = "NOT_CALIBRATED"
|
||||
state['sbs_mode_enabled'] = False
|
||||
state['sbs_mode_supported'] = False
|
||||
state['firmware_update_recommended'] = False
|
||||
state['device_license'] = {}
|
||||
|
||||
try:
|
||||
with open(DRIVER_STATE_FILE_PATH, 'r') as f:
|
||||
output = f.read()
|
||||
for line in output.splitlines():
|
||||
try:
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
key, value = line.strip().split('=')
|
||||
if key == 'heartbeat':
|
||||
state[key] = parse_int(value, 0)
|
||||
elif key in ['calibration_setup', 'calibration_state', 'connected_device_brand', 'connected_device_model']:
|
||||
state[key] = value
|
||||
elif key in ['sbs_mode_enabled', 'sbs_mode_supported', 'firmware_update_recommended']:
|
||||
state[key] = parse_boolean(value, False)
|
||||
elif key == 'device_license':
|
||||
state[key] = json.loads(value)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error parsing key-value pair {key}={value}: {e}")
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
# state is stale, just send the license
|
||||
if state['heartbeat'] == 0 or (time.time() - state['heartbeat']) > 5:
|
||||
return {
|
||||
'heartbeat': state['heartbeat'],
|
||||
'device_license': state['device_license']
|
||||
}
|
||||
|
||||
return state
|
||||
|
||||
async def request_token(self, email):
|
||||
self.logger.info(f"Requesting a new token for {email}")
|
||||
|
||||
# Set the USER environment variable for this command
|
||||
env_copy = os.environ.copy()
|
||||
env_copy["USER"] = self.user
|
||||
|
||||
try:
|
||||
output = subprocess.check_output([self.config_script_path, "--request-token", email], stderr=subprocess.STDOUT, env=env_copy)
|
||||
return output.strip() == b"Token request sent"
|
||||
except subprocess.CalledProcessError as exc:
|
||||
self.logger.error(f"Error running config script {exc.output}")
|
||||
return False
|
||||
|
||||
async def verify_token(self, token):
|
||||
self.logger.info(f"Verifying token {token}")
|
||||
|
||||
# Set the USER environment variable for this command
|
||||
env_copy = os.environ.copy()
|
||||
env_copy["USER"] = self.user
|
||||
|
||||
try:
|
||||
output = subprocess.check_output([self.config_script_path, "--verify-token", token], stderr=subprocess.STDOUT, env=env_copy)
|
||||
return output.strip() == b"Token verified"
|
||||
except subprocess.CalledProcessError as exc:
|
||||
self.logger.error(f"Error running config script {exc.output}")
|
||||
return False
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface domain="breezy-desktop">
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<object class="GtkBox" id="main">
|
||||
<property name="orientation">1</property>
|
||||
<property name="margin-top">20</property>
|
||||
<property name="margin-bottom">20</property>
|
||||
<property name="margin-start">20</property>
|
||||
<property name="margin-end">20</property>
|
||||
<property name="spacing">20</property>
|
||||
<child>
|
||||
<object class="AdwStatusPage" id="no-connected-device">
|
||||
<property name="title" translatable="true">No device connected</property>
|
||||
<property name="description" translatable="true">Breezy Desktop was unable to detect any supported XR devices.</property>
|
||||
<property name="width-request">650</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkGrid" id="connected-device-info">
|
||||
<property name="column-spacing">4</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="connected-device-label">
|
||||
<property name="label">VITURE One</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">connected</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup" id="connected-device-settings">
|
||||
<property name="title" translatable="true">Settings</property>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title" translatable="true">Effect enabled</property>
|
||||
<property name="subtitle" translatable="true">Turn on or off the XR desktop effect</property>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="effect-enable">
|
||||
<property name="valign">3</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title" translatable="true">Display distance</property>
|
||||
<child>
|
||||
<object class="GtkScale" id="display-distance">
|
||||
<property name="valign">3</property>
|
||||
<property name="draw-value">true</property>
|
||||
<property name="value-pos">0</property>
|
||||
<property name="digits">2</property>
|
||||
<property name="width-request">350</property>
|
||||
<property name="has-origin">false</property>
|
||||
<property name="adjustment">
|
||||
<object class="GtkAdjustment" id="display-distance-slider">
|
||||
<property name="lower">0.2</property>
|
||||
<property name="upper">2.5</property>
|
||||
<property name="step-increment">0.01</property>
|
||||
<property name="value">1.05</property>
|
||||
</object>
|
||||
</property>
|
||||
<marks>
|
||||
<mark value="0.2" position="bottom"></mark>
|
||||
<mark value="1.0" position="bottom"></mark>
|
||||
<mark value="2.5" position="bottom"></mark>
|
||||
</marks>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup" id="connected-device-shortcuts">
|
||||
<property name="title" translatable="true">Keyboard Shortcuts</property>
|
||||
<property name="description" translatable="true">Modify keyboard shortcuts and how they work</property>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title" translatable="true">Re-center display shortcut</property>
|
||||
<property name="subtitle" translatable="true">Pin the virtual display to the current position</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="reassign-recenter-display-shortcut-button">
|
||||
<style>
|
||||
<class name="row-button"/>
|
||||
</style>
|
||||
<property name="name">recenter-display-shortcut</property>
|
||||
<property name="valign">3</property>
|
||||
<property name="label">Test</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title" translatable="true">Display distance shortcut</property>
|
||||
<property name="subtitle" translatable="true">Quickly toggle between two predefined distances</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="reassign-toggle-display-distance-shortcut-button">
|
||||
<style>
|
||||
<class name="row-button"/>
|
||||
</style>
|
||||
<property name="name">toggle-display-distance-shortcut</property>
|
||||
<property name="valign">3</property>
|
||||
<property name="label">Test</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwActionRow">
|
||||
<property name="title" translatable="true">Toggle distance start</property>
|
||||
<property name="subtitle" translatable="true">Use the buttons to capture the current display distance as start and end points.</property>
|
||||
<property name="valign">2</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="spacing">30</property>
|
||||
<property name="width-request">150</property>
|
||||
<property name="margin-start">30</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="set-toggle-display-distance-start-button">
|
||||
<property name="name">toggle-display-distance-start</property>
|
||||
<property name="valign">3</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="set-toggle-display-distance-end-button">
|
||||
<property name="name">toggle-display-distance-end</property>
|
||||
<property name="valign">3</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface domain="awesome-tiles@velitasali.com">
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<object class="GtkDialog" id="dialog">
|
||||
<property name="modal">1</property>
|
||||
<property name="default_width">440</property>
|
||||
<property name="default_height">200</property>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<property name="margin-start">16</property>
|
||||
<property name="margin-end">16</property>
|
||||
<property name="margin-top">16</property>
|
||||
<property name="margin-bottom">16</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="vexpand">1</property>
|
||||
<property name="label" translatable="yes">Press your keyboard shortcut or 'Backspace' to disable...</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="titlebar">
|
||||
<object class="GtkHeaderBar">
|
||||
<property name="title-widget">
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes">Keyboard Shortcut</property>
|
||||
<property name="single-line-mode">1</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="width-chars">5</property>
|
||||
<style>
|
||||
<class name="title"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEventControllerKey" id="event-controller"/>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
Loading…
Reference in New Issue