From e9b10838bad7c3511d03d76c09efc02999bf872f Mon Sep 17 00:00:00 2001
From: Wayne Heaney <42350981+wheaney@users.noreply.github.com>
Date: Thu, 16 May 2024 12:26:59 -0700
Subject: [PATCH] Breezy GNOME support (#22)
* Adds a gnome subdirectory with the Breezy GNOME extension source
* Adds a 'ui' subdirectory with the Gtk UI app source, currently with the Breezy GNOME view implemented, eventually will support Breezy Vulkan
* Adds all the necessary package and setup scripts for the full GNOME build
* Pulls in the latest driver with the BREEZY_DESKTOP build support
---
.gitignore | 8 +-
.gitmodules | 11 +-
bin/breezy_gnome_setup | 56 +++
bin/package_gnome | 74 +++
bin/package_vulkan | 8 +-
gnome/bin/breezy_gnome_uninstall | 55 +++
gnome/bin/breezy_gnome_verify | 49 ++
gnome/bin/dev/use_local_extension.sh | 20 +
gnome/bin/package_extension | 79 +++
gnome/bin/setup | 64 +++
gnome/src/IMUAdjust.frag | 1 +
gnome/src/cursor.js | 67 +++
gnome/src/cursormanager.js | 211 ++++++++
.../org.gnome.Mutter.DisplayConfig.xml | 453 ++++++++++++++++++
gnome/src/extension.js | 243 ++++++++++
gnome/src/globals.js | 5 +
gnome/src/ipc.js | 49 ++
gnome/src/math.js | 3 +
gnome/src/metadata.json | 10 +
gnome/src/monitormanager.js | 167 +++++++
.../com.xronlinux.BreezyDesktop.gschema.xml | 1 +
gnome/src/shader.js | 14 +
gnome/src/textures/calibrating.png | 1 +
gnome/src/textures/custom_banner.png | 1 +
gnome/src/time.js | 7 +
gnome/src/xrEffect.js | 320 +++++++++++++
modules/sombrero | 1 +
modules/xrealAirLinuxDriver | 2 +-
ui/bin/package | 28 ++
ui/build-aux/dconf-override.patch | 23 +
ui/build-aux/start-breezy-desktop.sh | 37 ++
ui/com.xronlinux.BreezyDesktop.json | 86 ++++
.../com.xronlinux.BreezyDesktop.desktop.in | 8 +
.../com.xronlinux.BreezyDesktop.gschema.xml | 68 +++
...om.xronlinux.BreezyDesktop.metainfo.xml.in | 9 +
.../apps/com.xronlinux.BreezyDesktop.svg | 1 +
ui/data/icons/meson.build | 7 +
ui/data/meson.build | 36 ++
ui/meson.build | 20 +
ui/modules/PyXRLinuxDriverIPC | 1 +
ui/po/LINGUAS | 0
ui/po/POTFILES | 6 +
ui/po/meson.build | 1 +
ui/python3-pydbus.json | 14 +
ui/src/__init__.py | 0
ui/src/breezydesktop.gresource.xml | 10 +
ui/src/breezydesktop.in | 46 ++
ui/src/connecteddevice.py | 116 +++++
ui/src/extensionsmanager.py | 70 +++
ui/src/gtk/connected-device.ui | 201 ++++++++
ui/src/gtk/no-device.ui | 19 +
ui/src/gtk/no-extension.ui | 19 +
ui/src/gtk/shortcut-dialog.ui | 43 ++
ui/src/gtk/window.ui | 38 ++
ui/src/main.py | 91 ++++
ui/src/meson.build | 43 ++
ui/src/nodevice.py | 5 +
ui/src/noextension.py | 5 +
ui/src/settingsmanager.py | 15 +
ui/src/shortcutdialog.py | 109 +++++
ui/src/statemanager.py | 74 +++
ui/src/window.py | 55 +++
vulkan/custom_banner.png | Bin 153232 -> 153462 bytes
vulkan/modules/sombrero | 1 -
64 files changed, 3274 insertions(+), 11 deletions(-)
create mode 100755 bin/breezy_gnome_setup
create mode 100755 bin/package_gnome
create mode 100755 gnome/bin/breezy_gnome_uninstall
create mode 100755 gnome/bin/breezy_gnome_verify
create mode 100755 gnome/bin/dev/use_local_extension.sh
create mode 100755 gnome/bin/package_extension
create mode 100755 gnome/bin/setup
create mode 120000 gnome/src/IMUAdjust.frag
create mode 100644 gnome/src/cursor.js
create mode 100644 gnome/src/cursormanager.js
create mode 100644 gnome/src/dbus-interfaces/org.gnome.Mutter.DisplayConfig.xml
create mode 100644 gnome/src/extension.js
create mode 100644 gnome/src/globals.js
create mode 100644 gnome/src/ipc.js
create mode 100644 gnome/src/math.js
create mode 100644 gnome/src/metadata.json
create mode 100644 gnome/src/monitormanager.js
create mode 120000 gnome/src/schemas/com.xronlinux.BreezyDesktop.gschema.xml
create mode 100644 gnome/src/shader.js
create mode 120000 gnome/src/textures/calibrating.png
create mode 120000 gnome/src/textures/custom_banner.png
create mode 100644 gnome/src/time.js
create mode 100644 gnome/src/xrEffect.js
create mode 160000 modules/sombrero
create mode 100755 ui/bin/package
create mode 100644 ui/build-aux/dconf-override.patch
create mode 100644 ui/build-aux/start-breezy-desktop.sh
create mode 100644 ui/com.xronlinux.BreezyDesktop.json
create mode 100644 ui/data/com.xronlinux.BreezyDesktop.desktop.in
create mode 100644 ui/data/com.xronlinux.BreezyDesktop.gschema.xml
create mode 100644 ui/data/com.xronlinux.BreezyDesktop.metainfo.xml.in
create mode 100644 ui/data/icons/hicolor/scalable/apps/com.xronlinux.BreezyDesktop.svg
create mode 100644 ui/data/icons/meson.build
create mode 100644 ui/data/meson.build
create mode 100644 ui/meson.build
create mode 160000 ui/modules/PyXRLinuxDriverIPC
create mode 100644 ui/po/LINGUAS
create mode 100644 ui/po/POTFILES
create mode 100644 ui/po/meson.build
create mode 100644 ui/python3-pydbus.json
create mode 100644 ui/src/__init__.py
create mode 100644 ui/src/breezydesktop.gresource.xml
create mode 100755 ui/src/breezydesktop.in
create mode 100644 ui/src/connecteddevice.py
create mode 100644 ui/src/extensionsmanager.py
create mode 100644 ui/src/gtk/connected-device.ui
create mode 100644 ui/src/gtk/no-device.ui
create mode 100644 ui/src/gtk/no-extension.ui
create mode 100644 ui/src/gtk/shortcut-dialog.ui
create mode 100644 ui/src/gtk/window.ui
create mode 100644 ui/src/main.py
create mode 100644 ui/src/meson.build
create mode 100644 ui/src/nodevice.py
create mode 100644 ui/src/noextension.py
create mode 100644 ui/src/settingsmanager.py
create mode 100644 ui/src/shortcutdialog.py
create mode 100644 ui/src/statemanager.py
create mode 100644 ui/src/window.py
delete mode 160000 vulkan/modules/sombrero
diff --git a/.gitignore b/.gitignore
index 6f4d703..cc9412a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,6 @@
-/vulkan/build/
-/build/
\ No newline at end of file
+build/
+__pycache__
+*.zip
+gschemas.compiled
+.flatpak-builder/
+out/
diff --git a/.gitmodules b/.gitmodules
index a6a6cb3..935efb3 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -2,9 +2,12 @@
path = vulkan/modules/vkBasalt
url = https://github.com/wheaney/vkBasalt.git
branch = runtime_uniforms
-[submodule "vulkan/modules/sombrero"]
- path = vulkan/modules/sombrero
- url = https://github.com/wheaney/sombrero.git
[submodule "modules/xrealAirLinuxDriver"]
path = modules/xrealAirLinuxDriver
- url = https://github.com/wheaney/XRLinuxDriver.git
\ No newline at end of file
+ url = https://github.com/wheaney/XRLinuxDriver.git
+[submodule "modules/sombrero"]
+ path = modules/sombrero
+ url = https://github.com/wheaney/sombrero.git
+[submodule "ui/modules/PyXRLinuxDriverIPC"]
+ path = ui/modules/PyXRLinuxDriverIPC
+ url = https://github.com/wheaney/PyXRLinuxDriverIPC.git
diff --git a/bin/breezy_gnome_setup b/bin/breezy_gnome_setup
new file mode 100755
index 0000000..c5d9ce6
--- /dev/null
+++ b/bin/breezy_gnome_setup
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+
+# This setup script should do the minimum work required to download the release package, unzip it, and kick off the
+# setup script contained within.
+
+# exit when any command fails
+set -e
+
+if [ "$(id -u)" == "0" ]; then
+ echo "This script must not be run as root" 1>&2
+ exit 1
+fi
+
+start_dir=$(pwd)
+
+# create temp directory
+tmp_dir=$(mktemp -d -t breezy-gnome-XXXXXXXXXX)
+pushd $tmp_dir > /dev/null
+echo "Created temp directory: ${tmp_dir}"
+
+# if the first argument is "-v" then the second argument is metrics version, and the third argument is binary path
+# otherwise, if the first argument is present, it's the binary path
+if [ "$1" = "-v" ]
+then
+ metrics_version="$2"
+ binary_path_arg="$3"
+else
+ binary_path_arg="$1"
+fi
+
+if [ -z "$binary_path_arg" ]
+then
+ # download and unzip the latest driver
+ echo "Downloading latest release to: ${tmp_dir}/breezyGNOME.tar.gz"
+ curl -L -O https://github.com/wheaney/breezy-desktop/releases/latest/download/breezyGNOME.tar.gz
+else
+ if [[ "$binary_path_arg" = /* ]]; then
+ abs_path="$binary_path_arg"
+ else
+ # Convert relative path to absolute path
+ abs_path=$(realpath "$start_dir/$binary_path_arg")
+ fi
+ cp $abs_path $tmp_dir
+fi
+
+echo "Extracting to: ${tmp_dir}/breezy_gnome"
+tar -xf breezyGNOME.tar.gz
+
+pushd breezy_gnome > /dev/null
+
+# run the setup script that comes with this release
+bin/setup $metrics_version
+
+echo "Deleting temp directory: ${tmp_dir}"
+rm -rf $tmp_dir
+cd "$(dirs -l -0)" && dirs -c
diff --git a/bin/package_gnome b/bin/package_gnome
new file mode 100755
index 0000000..65c8bfe
--- /dev/null
+++ b/bin/package_gnome
@@ -0,0 +1,74 @@
+#!/usr/bin/env bash
+
+# exit when any command fails
+set -e
+
+# https://stackoverflow.com/a/246128
+SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
+ROOT_DIR=$(realpath $SCRIPT_DIR/..)
+VULKAN_DIR=$ROOT_DIR/vulkan
+GNOME_DIR=$ROOT_DIR/gnome
+GNOME_BUILD_DIR=$GNOME_DIR/build
+
+rm -rf $GNOME_BUILD_DIR
+mkdir -p $GNOME_BUILD_DIR
+
+PACKAGE_DIR=$GNOME_BUILD_DIR/breezy_gnome
+rm -rf $PACKAGE_DIR
+mkdir -p $PACKAGE_DIR
+
+XREAL_DRIVER_DIR=$ROOT_DIR/modules/xrealAirLinuxDriver
+source $XREAL_DRIVER_DIR/bin/inject_ua
+
+# check out submodules, recursively for nested ones
+git submodule update --init --recursive
+
+# if a custom_banner image exists, copy it over the sombrero one
+if [ -e "$VULKAN_DIR/custom_banner.png" ]; then
+ cp $VULKAN_DIR/custom_banner.png $PACKAGE_DIR
+fi
+
+# copy vulkan setup scripts and configs
+mkdir -p $PACKAGE_DIR/bin
+copy_and_inject_ua "$XREAL_DRIVER_DIR/bin/ua.sh" "$PACKAGE_DIR/bin" "$GNOME_DIR/bin/setup" "$GNOME_DIR/bin/breezy_gnome_verify" "$GNOME_DIR/bin/breezy_gnome_uninstall"
+
+XREAL_BINARY=$XREAL_DRIVER_DIR/build/xrealAirLinuxDriver.tar.gz
+pushd $XREAL_DRIVER_DIR
+
+if [ ! -e "$XREAL_BINARY" ] || [ "$1" != "--skip-module-builds" ]; then
+ # if a file exists at custom_banner_config.yml, copy it to the xrealAirLinuxDriver directory
+ if [ -e "$VULKAN_DIR/custom_banner_config.yml" ]; then
+ cp $VULKAN_DIR/custom_banner_config.yml $XREAL_DRIVER_DIR
+ fi
+
+ BREEZY_DESKTOP=1 bin/package
+fi
+
+XREAL_MANIFEST_LINE=$(sha256sum build/driver_air_glasses/manifest)
+popd
+
+cp $XREAL_BINARY $PACKAGE_DIR
+cp $XREAL_DRIVER_DIR/bin/xreal_driver_setup $PACKAGE_DIR/bin
+
+gnome/bin/package_extension
+cp gnome/out/* $PACKAGE_DIR
+
+# create a checksum that combines the checksums of all files in the directory
+pushd gnome/src
+GNOME_MANIFEST_LINE=$(find -L . -type f ! -name "*.compiled" -exec sha256sum {} \; | sort | sha256sum | sed 's/ .*//')
+popd
+
+ui/bin/package
+cp ui/out/* $PACKAGE_DIR
+
+# create manifest file for verifying installed file checksums against the originally packaged versions
+# include any file that doesn't get modified during setup (e.g. vkBasalt.json files)
+pushd $PACKAGE_DIR
+echo $XREAL_MANIFEST_LINE > manifest
+echo -e "$GNOME_MANIFEST_LINE breezydesktop@xronlinux.com" >> manifest
+popd
+
+# bundle everything up
+pushd $GNOME_BUILD_DIR
+tar -zcvf breezyGNOME.tar.gz breezy_gnome
+popd
\ No newline at end of file
diff --git a/bin/package_vulkan b/bin/package_vulkan
index 59cb32f..0b12c32 100755
--- a/bin/package_vulkan
+++ b/bin/package_vulkan
@@ -41,8 +41,8 @@ cp $VKBASALT_BUILD_DIR/builddir/config/vkBasalt.json $PACKAGE_DIR/vkBasalt.64/
cp $VKBASALT_BUILD_DIR/builddir.32/src/libvkbasalt.so $PACKAGE_DIR/vkBasalt.32/
# copy Sombrero FX, get ReShade headers
-cp $VULKAN_MODULES/sombrero/*.fx $PACKAGE_DIR
-cp $VULKAN_MODULES/sombrero/*.png $PACKAGE_DIR
+cp modules/sombrero/*.fx $PACKAGE_DIR
+cp modules/sombrero/*.png $PACKAGE_DIR
wget -P $PACKAGE_DIR https://raw.githubusercontent.com/crosire/reshade-shaders/384465d0287999caa6190b5ebea506200b4f4a0a/Shaders/ReShade.fxh
wget -P $PACKAGE_DIR https://raw.githubusercontent.com/crosire/reshade-shaders/384465d0287999caa6190b5ebea506200b4f4a0a/Shaders/ReShadeUI.fxh
@@ -58,7 +58,6 @@ cp -r $VULKAN_DIR/config $PACKAGE_DIR
# build xreal driver
XREAL_BINARY=$XREAL_DRIVER_DIR/build/xrealAirLinuxDriver.tar.gz
-pushd $XREAL_DRIVER_DIR
if [ ! -e "$XREAL_BINARY" ] || [ "$1" != "--skip-module-builds" ]; then
# if a file exists at custom_banner_config.yml, copy it to the xrealAirLinuxDriver directory
@@ -66,7 +65,10 @@ if [ ! -e "$XREAL_BINARY" ] || [ "$1" != "--skip-module-builds" ]; then
cp $VULKAN_DIR/custom_banner_config.yml $XREAL_DRIVER_DIR
fi
+ pushd $XREAL_DRIVER_DIR
bin/package
+else
+ pushd $XREAL_DRIVER_DIR
fi
XREAL_MANIFEST_LINE=$(sha256sum build/driver_air_glasses/manifest)
diff --git a/gnome/bin/breezy_gnome_uninstall b/gnome/bin/breezy_gnome_uninstall
new file mode 100755
index 0000000..fbffccc
--- /dev/null
+++ b/gnome/bin/breezy_gnome_uninstall
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+
+for_install=0
+if [[ -n "$1" ]] && [[ "$1" == "--for-install" ]]; then
+ for_install=1
+fi
+
+# we don't want the uninstall script to be able to cause a failure if being triggered by the setup script
+[ "$for_install" -eq 0 ] && set -e
+
+if [ "$(id -u)" == "0" ]; then
+ echo "This script must not be run as root" 1>&2
+ exit 1
+fi
+
+USER_HOME=$(realpath ~)
+if [ "$for_install" -eq 0 ]; then
+ UA_EVENT_NAME="breezy_gnome_uninstall"
+ UA_CLIENT_ID="BreezyGNOME"
+ #INJECT_UA_CALL
+fi
+
+if [ -z "$XDG_BIN_HOME" ]; then
+ XDG_BIN_HOME="$USER_HOME/.local/bin"
+fi
+
+if [ -z "$XDG_DATA_HOME" ]; then
+ XDG_DATA_HOME="$USER_HOME/.local/share"
+fi
+DATA_DIR="$XDG_DATA_HOME/breezy_gnome"
+
+
+[ "$for_install" -eq 0 ] && echo "Removing ${DATA_DIR} and its contents"
+rm -rf $DATA_DIR
+
+[ "$for_install" -eq 0 ] && echo "Uninstalling the breezydesktop@xronlinux.com GNOME extension"
+gnome-extensions uninstall breezydesktop@xronlinux.com
+
+[ "$for_install" -eq 0 ] && echo "Uninstalling the Breezy Desktop UI Flatpak"
+flatpak uninstall --user --noninteractive --force-remove com.xronlinux.BreezyDesktop
+
+[ "$for_install" -eq 0 ] && echo "Uninstalling XRLinuxDriver"
+# if for-install
+if [ "$for_install" -eq 1 ]; then
+ sudo ~/bin/xreal_driver_uninstall --for-install
+else
+ sudo ~/bin/xreal_driver_uninstall
+fi
+
+
+[ "$for_install" -eq 0 ] && echo "Removing the breezy_gnome scripts from ${XDG_BIN_HOME}"
+rm -f $XDG_BIN_HOME/breezy_gnome_verify
+
+# this script is self-deleting, leave this as the last command
+rm -f $XDG_BIN_HOME/breezy_gnome_uninstall
\ No newline at end of file
diff --git a/gnome/bin/breezy_gnome_verify b/gnome/bin/breezy_gnome_verify
new file mode 100755
index 0000000..c91b47b
--- /dev/null
+++ b/gnome/bin/breezy_gnome_verify
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+
+set -e
+
+USER_HOME=$(realpath ~)
+
+if [ -z "$XDG_DATA_HOME" ]; then
+ XDG_DATA_HOME="$USER_HOME/.local/share"
+fi
+DATA_DIR="$XDG_DATA_HOME/breezy_gnome"
+
+# create a string to string mapping, file name to expected file location
+declare -A file_paths
+file_paths=(
+ ["build/driver_air_glasses/manifest"]="$USER_HOME/.local/bin/xr_driver/manifest"
+ ["breezydesktop@xronlinux.com"]="$XDG_DATA_HOME/gnome-shell/extensions/breezydesktop@xronlinux.com"
+)
+
+# verify the file hashes in ./manifest
+while IFS= read -r line
+do
+ # split the line into hash and filename
+ manifest_hash=$(echo $line | awk '{print $1}')
+ file=$(echo $line | awk '{print $2}')
+
+ actual_file_path=${file_paths[$file]}
+
+ # check if the file path is a directory
+ if [ -d "$actual_file_path" ]; then
+ # compute the SHA256 hash of the directory contents
+ pushd $actual_file_path > /dev/null
+ actual_hash=$(find -L . -type f ! -name "*.compiled" -exec sha256sum {} \; | sort | sha256sum | sed 's/ .*//')
+ popd > /dev/null
+ else
+ # compute the SHA256 hash of the actual file
+ actual_hash=$(sha256sum $actual_file_path | awk '{print $1}')
+ fi
+
+ # compare the hashes
+ if ! [ "$manifest_hash" = "$actual_hash" ]; then
+ echo "Verification failed" >&2
+ exit 1
+ fi
+done < "$XDG_DATA_HOME/breezy_gnome/manifest"
+
+# if our checks succeeded, run the xr_driver verify script
+$USER_HOME/.local/bin/xr_driver/verify_installation > /dev/null
+
+echo "Verification succeeded"
\ No newline at end of file
diff --git a/gnome/bin/dev/use_local_extension.sh b/gnome/bin/dev/use_local_extension.sh
new file mode 100755
index 0000000..54e9577
--- /dev/null
+++ b/gnome/bin/dev/use_local_extension.sh
@@ -0,0 +1,20 @@
+USER_HOME=$(realpath ~)
+
+# https://stackoverflow.com/a/246128
+SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
+
+if [ -z "$XDG_DATA_HOME" ]; then
+ XDG_DATA_HOME="$USER_HOME/.local/share"
+fi
+
+# if $XDG_DATA_HOME/gnome-shell/extensions/breezydesktop@xronlinux.com exists
+extension_path="$XDG_DATA_HOME/gnome-shell/extensions/breezydesktop@xronlinux.com"
+if [ -d $extension_path ]; then
+ # remove it
+ rm -rf $extension_path
+fi
+
+# recursively copy the $SCRIPT_DIR/../../src to extension_path, don't preserve symlinks
+cp -rL $SCRIPT_DIR/../../src $extension_path
+
+glib-compile-schemas $extension_path/schemas
\ No newline at end of file
diff --git a/gnome/bin/package_extension b/gnome/bin/package_extension
new file mode 100755
index 0000000..69af779
--- /dev/null
+++ b/gnome/bin/package_extension
@@ -0,0 +1,79 @@
+#!/usr/bin/env bash
+
+UUID="breezydesktop@xronlinux.com"
+
+# fail on error
+set -e
+
+if [[ $EUID -eq 0 ]]; then
+ echo "This script must NOT be run as root" 1>&2
+ exit 1
+fi
+
+# https://stackoverflow.com/a/246128
+SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
+
+SRC_DIR="$SCRIPT_DIR/../src"
+TMP_DIR=$(mktemp -d -t breezy-gnome-XXXXXXXXXX)
+DEST_DIR="$TMP_DIR/$UUID"
+OUT_DIR="$SCRIPT_DIR/../out"
+rm -rf "$OUT_DIR"
+
+cd "$SCRIPT_DIR" || exit 1
+
+check_command() {
+ if ! command -v "$1" &>/dev/null; then
+ echo "Please install \"$1\" and make sure it's available in your \$PATH"
+ exit 1
+ fi
+}
+
+compile_schemas() {
+ check_command "glib-compile-schemas"
+ mkdir -p "$DEST_DIR/schemas/"
+
+ # the pack command also compiles the schemas but only into the zip file
+ glib-compile-schemas --targetdir="$DEST_DIR/schemas" "$SRC_DIR/schemas/"
+}
+
+copy_static_files() {
+ # Copy non generated files to destdir
+ cp $SRC_DIR/*.js $DEST_DIR/
+ cp $SRC_DIR/*.frag $DEST_DIR/
+ mkdir -p "$DEST_DIR/schemas/"
+ cp $SRC_DIR/schemas/*.xml $DEST_DIR/schemas/
+ mkdir -p $DEST_DIR/dbus-interfaces/
+ cp $SRC_DIR/dbus-interfaces/*.xml $DEST_DIR/dbus-interfaces/
+ mkdir -p $DEST_DIR/textures/
+ cp -rL $SRC_DIR/textures/* $DEST_DIR/textures/
+ cp $SRC_DIR/metadata.json $DEST_DIR/
+}
+
+pack() {
+ check_command "gnome-extensions"
+
+ # pack everything into a sharable zip file
+ extra_source=()
+ for file in "$DEST_DIR"/*; do
+ extra_source+=("--extra-source=$file")
+ done
+
+ mkdir -p "$OUT_DIR"
+ gnome-extensions pack --force "${extra_source[@]}" "$DEST_DIR" -o "$OUT_DIR"
+}
+
+if [ $# -eq 0 ]; then
+ # No arguments, do everything
+ compile_schemas
+ copy_static_files
+ pack
+elif [ "$1" == "build_local" ]; then
+ compile_schemas
+ copy_static_files
+elif [ "$1" == "pack" ]; then
+ pack
+elif [ "$1" == "copy_static" ]; then
+ copy_static_files
+fi
+
+rm -rf "$TMP_DIR"
\ No newline at end of file
diff --git a/gnome/bin/setup b/gnome/bin/setup
new file mode 100755
index 0000000..18f6ee6
--- /dev/null
+++ b/gnome/bin/setup
@@ -0,0 +1,64 @@
+#!/usr/bin/env bash
+
+set -e
+
+# This script gets packaged with the release and should do the bulk of the setup work. This allows this setup to be tied
+# to a specific release of the code, and guarantees it will never run along-side newer or older binaries.
+
+USER_HOME=$(realpath ~)
+
+if [ -z "$XDG_BIN_HOME" ]; then
+ XDG_BIN_HOME="$USER_HOME/.local/bin"
+fi
+
+if [ -z "$XDG_DATA_HOME" ]; then
+ XDG_DATA_HOME="$USER_HOME/.local/share"
+fi
+DATA_DIR="$XDG_DATA_HOME/breezy_gnome"
+
+UA_EVENT_NAME="breezy_gnome_install"
+if [ -e "$XDG_BIN_HOME/breezy_gnome_uninstall" ]; then
+ echo "Cleaning up the previous installation"
+
+ # ` || true` will ensure that this can't cause a failure, even with `set -e`
+ $XDG_BIN_HOME/breezy_gnome_uninstall --for-install || true
+
+ UA_EVENT_NAME="breezy_gnome_update"
+fi
+
+UA_CLIENT_ID="BreezyGNOME"
+UA_EVENT_VERSION="$1"
+#INJECT_UA_CALL
+
+echo "Copying the breezy_gnome scripts to ${XDG_BIN_HOME}"
+mkdir -p $XDG_BIN_HOME
+cp bin/breezy_gnome_uninstall $XDG_BIN_HOME
+cp bin/breezy_gnome_verify $XDG_BIN_HOME
+
+echo "Copying the manifest file to ${DATA_DIR}"
+mkdir -p $DATA_DIR
+cp manifest $DATA_DIR
+
+echo "Installing the breezydesktop@xronlinux.com GNOME extension"
+gnome-extensions install --force breezydesktop@xronlinux.com.shell-extension.zip
+
+echo "Installing the Breezy Desktop UI Flatpak (this may take a couple minutes the first time)"
+flatpak install --user --noninteractive --reinstall com.xronlinux.BreezyDesktop.flatpak
+
+# set up the XREAL driver using the local binary
+echo "Installing xrealAirLinuxDriver"
+echo "BEGIN - xreal_driver_setup"
+if [ -z "$1" ]
+then
+ sudo bin/xreal_driver_setup $(pwd)/xrealAirLinuxDriver.tar.gz
+else
+ sudo bin/xreal_driver_setup -v $1 $(pwd)/xrealAirLinuxDriver.tar.gz
+fi
+
+echo "END - xreal_driver_setup"
+
+echo "Enabling the driver and setting it to Breezy Desktop mode"
+$USER_HOME/bin/xreal_driver_config -e
+$USER_HOME/bin/xreal_driver_config -vd
+
+sed -i 's/virtual_display/breezy_desktop/g' $USER_HOME/.xreal_driver_config
\ No newline at end of file
diff --git a/gnome/src/IMUAdjust.frag b/gnome/src/IMUAdjust.frag
new file mode 120000
index 0000000..ffacb50
--- /dev/null
+++ b/gnome/src/IMUAdjust.frag
@@ -0,0 +1 @@
+../../modules/sombrero/IMUAdjust.frag
\ No newline at end of file
diff --git a/gnome/src/cursor.js b/gnome/src/cursor.js
new file mode 100644
index 0000000..36ad7ee
--- /dev/null
+++ b/gnome/src/cursor.js
@@ -0,0 +1,67 @@
+// Taken from https://github.com/jkitching/soft-brightness-plus
+//
+// Copyright (C) 2023 Joel Kitching (jkitching on Github)
+//
+// 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 .
+
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+
+// Copied almost verbatim from ui/magnifier.js.
+export const MouseSpriteContent = GObject.registerClass({
+ Implements: [Clutter.Content],
+}, class MouseSpriteContent extends GObject.Object {
+ _init() {
+ super._init();
+ this._texture = null;
+ }
+
+ vfunc_get_preferred_size() {
+ if (!this._texture)
+ return [false, 0, 0];
+
+ return [true, this._texture.get_width(), this._texture.get_height()];
+ }
+
+ vfunc_paint_content(actor, node, _paintContext) {
+ if (!this._texture)
+ return;
+
+ let [minFilter, magFilter] = actor.get_content_scaling_filters();
+ let textureNode = new Clutter.TextureNode(this._texture,
+ null, minFilter, magFilter);
+ textureNode.set_name('BreezyDesktopSpriteContent');
+ node.add_child(textureNode);
+
+ textureNode.add_rectangle(actor.get_content_box());
+ }
+
+ get texture() {
+ return this._texture;
+ }
+
+ set texture(coglTexture) {
+ if (this._texture === coglTexture)
+ return;
+
+ let oldTexture = this._texture;
+ this._texture = coglTexture;
+ this.invalidate();
+
+ if (!oldTexture || !coglTexture ||
+ oldTexture.get_width() !== coglTexture.get_width() ||
+ oldTexture.get_height() !== coglTexture.get_height())
+ this.invalidate_size();
+ }
+});
\ No newline at end of file
diff --git a/gnome/src/cursormanager.js b/gnome/src/cursormanager.js
new file mode 100644
index 0000000..9f80ad0
--- /dev/null
+++ b/gnome/src/cursormanager.js
@@ -0,0 +1,211 @@
+import Clutter from 'gi://Clutter';
+import Meta from 'gi://Meta';
+import * as PointerWatcher from 'resource:///org/gnome/shell/ui/pointerWatcher.js';
+import { MouseSpriteContent } from './cursor.js';
+
+// Taken from https://github.com/jkitching/soft-brightness-plus
+export class CursorManager {
+ constructor(mainActor) {
+ this._mainActor = mainActor;
+
+ this._changeHookFn = null;
+
+ // Set/destroyed by _enableCloningMouse/_disableCloningMouse
+ this._cursorWantedVisible = null;
+ this._cursorTracker = null;
+ this._cursorTrackerSetPointerVisible = null;
+ this._cursorTrackerSetPointerVisibleBound = null;
+ this._cursorSprite = null;
+ this._cursorActor = null;
+ this._cursorWatcher = null;
+ this._cursorSeat = null;
+ this._cursorUnfocusInhibited = false;
+
+ // Set/destroyed by _startCloningMouse / _stopCloningMouse
+ this._cursorWatch = null;
+ this._cursorChangedConnection = null;
+ this._cursorVisibilityChangedConnection = null;
+ this._cursorPositionInvalidatedConnection = null;
+ }
+
+ enable() {
+ this._enableCloningMouse();
+ this.startCloning();
+ }
+
+ disable() {
+ this._disableCloningMouse();
+ }
+
+ startCloning() {
+ this._startCloningMouse();
+ }
+
+ stopCloning() {
+ this._stopCloningMouse();
+ }
+
+ // After this:
+ // * real cursor is disabled
+ // * cloning is "on"
+ // * cloned cursor not visible, but ready for _startCloningMouse to make it visible
+ //
+ // okay if _startCloningMouse is not immediately called since set_pointer_visible is bound to our replacement function
+ // and will trigger _startCloningMouse when the cursor should be shown
+ _enableCloningMouse() {
+ this._cursorTracker = Meta.CursorTracker.get_for_display(global.display);
+ this._cursorWantedVisible = this._cursorTracker.get_pointer_visible();
+ this._cursorTrackerSetPointerVisible = Meta.CursorTracker.prototype.set_pointer_visible;
+ this._cursorTrackerSetPointerVisibleBound = this._cursorTrackerSetPointerVisible.bind(this._cursorTracker);
+ Meta.CursorTracker.prototype.set_pointer_visible = this._cursorTrackerSetPointerVisibleReplacement.bind(this);
+
+ this._cursorTrackerSetPointerVisibleBound(false);
+
+ this._cursorSprite = new Clutter.Actor({ request_mode: Clutter.RequestMode.CONTENT_SIZE });
+ this._cursorSprite.content = new MouseSpriteContent();
+
+ this._cursorActor = new Clutter.Actor();
+ if (Clutter.Container === undefined) {
+ this._cursorActor.add_child(this._cursorSprite);
+ } else {
+ this._cursorActor.add_actor(this._cursorSprite);
+ }
+ this._cursorWatcher = PointerWatcher.getPointerWatcher();
+ this._cursorSeat = Clutter.get_default_backend().get_default_seat();
+ }
+
+ // After this:
+ // * real cursor enabled, manages its own visibility
+ // * cloning is "off"
+ // * no cloned cursor
+ //
+ // completely reverts _enableCloningMouse
+ _disableCloningMouse() {
+ this._stopCloningMouse();
+ Meta.CursorTracker.prototype.set_pointer_visible = this._cursorTrackerSetPointerVisible;
+ this._cursorTracker.set_pointer_visible(this._cursorWantedVisible);
+
+ this._cursorWantedVisible = null;
+ this._cursorTracker = null;
+ this._cursorTrackerSetPointerVisible = null;
+ this._cursorTrackerSetPointerVisibleBound = null;
+ this._cursorSprite = null;
+ this._cursorActor = null;
+ this._cursorWatcher = null;
+ this._cursorSeat = null;
+ }
+
+ // bound to Meta.CursorTracker.prototype.set_pointer_visible when cloning is "on"
+ // original function available in this._cursorTrackerSetPointerVisibleBound
+ _cursorTrackerSetPointerVisibleReplacement(visible) {
+ this._cursorWantedVisible = visible;
+ if (visible) {
+ this._startCloningMouse();
+ } else {
+ this._stopCloningMouse();
+ }
+ }
+
+ // After this:
+ // * real cursor is hidden
+ // * cloning is "on"
+ // * clone cursor is visible
+ //
+ // add the clone cursor actor, watch for pointer movement and cursor changes, reflect them in the cloned cursor
+ // prereqs: setup in _enableCloningMouse, _cursorWantedVisible is true
+ _startCloningMouse() {
+ if (this._cursorWatch == null) {
+ if (Clutter.Container === undefined) {
+ this._mainActor.add_child(this._cursorActor);
+ } else {
+ this._mainActor.add_actor(this._cursorActor);
+ }
+ this._cursorChangedConnection = this._cursorTracker.connect('cursor-changed', this._updateMouseSprite.bind(this));
+ this._cursorVisibilityChangedConnection = this._cursorTracker.connect('visibility-changed', this._updateMouseSprite.bind(this));
+ this._cursorPositionInvalidatedConnection = this._cursorTracker.connect('position-invalidated', this._updateMouseSprite.bind(this));
+
+ const interval = 1000 / 250;
+ this._cursorWatch = this._cursorWatcher.addWatch(interval, this._updateMousePosition.bind(this));
+
+ const [x, y] = global.get_pointer();
+ this._updateMousePosition(x, y);
+ this._updateMouseSprite();
+ }
+
+ if (this._cursorTracker.set_keep_focus_while_hidden) {
+ this._cursorTracker.set_keep_focus_while_hidden(true);
+ }
+
+ if (!this._cursorUnfocusInhibited) {
+ console.log('Breezy debug - inhibit_unfocus\n');
+ this._cursorSeat.inhibit_unfocus();
+ this._cursorUnfocusInhibited = true;
+ }
+ }
+
+ // After this:
+ // * real cursor is hidden
+ // * cloning is "on"
+ // * cloned cursor not visible, but ready for _startCloningMouse to make it visible
+ //
+ // completely reverts _startCloningMouse
+ _stopCloningMouse() {
+ if (this._cursorWatch != null) {
+ this._cursorWatch.remove();
+ this._cursorWatch = null;
+
+ this._cursorTracker.disconnect(this._cursorChangedConnection);
+ this._cursorChangedConnection = null;
+
+ this._cursorTracker.disconnect(this._cursorVisibilityChangedConnection);
+ this._cursorVisibilityChangedConnection = null;
+
+ this._cursorTracker.disconnect(this._cursorPositionInvalidatedConnection);
+ this._cursorPositionInvalidatedConnection = null;
+
+ if (Clutter.Container === undefined) {
+ this._mainActor.remove_child(this._cursorActor);
+ } else {
+ this._mainActor.remove_actor(this._cursorActor);
+ }
+ }
+
+ if (this._cursorTracker.set_keep_focus_while_hidden) {
+ this._cursorTracker.set_keep_focus_while_hidden(false);
+ }
+
+ if (this._cursorUnfocusInhibited) {
+ console.log('Breezy debug - uninhibit_unfocus\n');
+ this._cursorSeat.uninhibit_unfocus();
+ this._cursorUnfocusInhibited = false;
+ }
+ }
+
+ _updateMousePosition(x, y) {
+ this._cursorActor.set_position(x, y);
+ }
+
+ _updateMouseSprite() {
+ const sprite = this._cursorTracker.get_sprite();
+ if (sprite) {
+ this._cursorSprite.content.texture = sprite;
+ this._cursorSprite.show();
+ } else {
+ this._cursorSprite.hide();
+ }
+
+ const [xHot, yHot] = this._cursorTracker.get_hot();
+ this._cursorSprite.set({
+ translation_x: -xHot,
+ translation_y: -yHot,
+ });
+ this._mainActor.set_child_above_sibling(this._cursorActor, null);
+ this._cursorTrackerSetPointerVisibleBound(false);
+
+ // some other processes are uninhibiting when they shouldn't, so we need to re-inhibit here
+ if (!this._cursorSeat.is_unfocus_inhibited() && this._cursorUnfocusInhibited) {
+ console.log('Breezy debug - reinhibiting\n');
+ this._cursorSeat.inhibit_unfocus();
+ }
+ }
+}
\ No newline at end of file
diff --git a/gnome/src/dbus-interfaces/org.gnome.Mutter.DisplayConfig.xml b/gnome/src/dbus-interfaces/org.gnome.Mutter.DisplayConfig.xml
new file mode 100644
index 0000000..50b07fa
--- /dev/null
+++ b/gnome/src/dbus-interfaces/org.gnome.Mutter.DisplayConfig.xml
@@ -0,0 +1,453 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gnome/src/extension.js b/gnome/src/extension.js
new file mode 100644
index 0000000..d5d5384
--- /dev/null
+++ b/gnome/src/extension.js
@@ -0,0 +1,243 @@
+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 Globals from './globals.js';
+import MonitorManager from './monitormanager.js';
+import { IPC_FILE_PATH, XREffect } from './xrEffect.js';
+
+import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+
+const SUPPORTED_MONITOR_PRODUCTS = [
+ 'VITURE',
+ 'nreal air',
+ 'Air',
+ 'MetaMonitor' // nested mode dummy monitor
+];
+
+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;
+ this._monitor_manager = null;
+ this._xr_effect = null;
+ this._overlay = null;
+ this._target_monitor = null;
+ this._is_effect_running = false;
+ }
+
+ enable() {
+ Globals.extension_dir = this.path;
+ this._monitor_manager = new MonitorManager(this.path);
+ this._monitor_manager.setChangeHook(this._setup.bind(this));
+ this._monitor_manager.enable();
+
+ this._setup();
+ }
+
+ _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, (() => {
+ if (is_effect_running) return GLib.SOURCE_REMOVE;
+
+ const is_driver_running = this._check_driver_running();
+ if (is_driver_running && target_monitor) {
+ console.log('Driver is running, supported monitor connected. Enabling XR effect.');
+ this._effect_enable();
+ return GLib.SOURCE_REMOVE;
+ } else {
+ return GLib.SOURCE_CONTINUE;
+ }
+ }).bind(this));
+ }
+
+ _find_supported_monitor() {
+ const target_monitor = this._monitor_manager.getMonitorPropertiesList()?.find(
+ monitor => SUPPORTED_MONITOR_PRODUCTS.includes(monitor.product));
+ if (target_monitor !== undefined) {
+ return {
+ monitor: this._monitor_manager.getMonitors()[target_monitor.index],
+ refreshRate: target_monitor.refreshRate,
+ };
+ }
+
+ if (this.settings.get_boolean('developer-mode')) {
+ // allow testing XR devices with just USB, no video needed
+ return {
+ monitor: this._monitor_manager.getMonitors()[0],
+ refreshRate: 60,
+ };
+ }
+
+ return null;
+ }
+
+ _setup() {
+ if (this._is_effect_running) {
+ console.log('Monitors changed, disabling XR effect');
+ this._effect_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.monitor;
+ this._refresh_rate = target_monitor.refreshRate;
+
+ if (this._check_driver_running()) {
+ console.log('Ready, enabling XR effect');
+ this._effect_enable();
+ } else {
+ this._poll_for_ready();
+ }
+ }
+ }
+
+ _check_driver_running() {
+ if (!Globals.ipc_file) Globals.ipc_file = Gio.file_new_for_path(IPC_FILE_PATH);
+ return Globals.ipc_file.query_exists(null);
+ }
+
+ _effect_enable() {
+ this._running_poller_id = undefined;
+ if (!this._is_effect_running) {
+ this._is_effect_running = true;
+
+ try {
+ this._cursor_manager = new CursorManager(Main.layoutManager.uiGroup);
+ this._cursor_manager.enable();
+
+ this._overlay = new St.Bin({ style: 'background-color: rgba(0, 0, 0, 1);'});
+ this._overlay.opacity = 255;
+ this._overlay.set_position(this._target_monitor.x, this._target_monitor.y);
+ this._overlay.set_size(this._target_monitor.width, this._target_monitor.height);
+
+ const overlayContent = new Clutter.Actor({clip_to_allocation: true});
+ const uiClone = new Clutter.Clone({ source: Main.layoutManager.uiGroup, clip_to_allocation: true });
+ uiClone.x = -this._target_monitor.x;
+ uiClone.y = -this._target_monitor.y;
+ if (Clutter.Container === undefined) {
+ overlayContent.add_child(uiClone);
+ } else {
+ overlayContent.add_actor(uiClone);
+ }
+
+ this._overlay.set_child(overlayContent);
+
+ global.stage.insert_child_above(this._overlay, null);
+ Shell.util_set_hidden_from_pick(this._overlay, true);
+
+ this._xr_effect = new XREffect({
+ target_monitor: this._target_monitor,
+ target_framerate: this._refresh_rate ?? 60,
+ 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('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);
+
+ 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-follow-shortcut', this._toggle_follow_mode.bind(this));
+ } catch (e) {
+ console.error(`Error enabling XR effect ${e.message}`, e.stack);
+ this._effect_disable();
+ }
+ }
+ }
+
+ _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
+ );
+ });
+ }
+
+ _write_control(key, value) {
+ 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);
+ }
+
+ _recenter_display() {
+ this._write_control('recenter_screen', 'true');
+ }
+
+ _toggle_follow_mode() {
+ this._write_control('toggle_breezy_desktop_smooth_follow', 'true');
+ }
+
+ _effect_disable() {
+ this._is_effect_running = false;
+
+ if (this._running_poller_id) GLib.source_remove(this._running_poller_id);
+
+ Main.wm.removeKeybinding('recenter-display-shortcut');
+ Main.wm.removeKeybinding('toggle-display-distance-shortcut');
+ Meta.enable_unredirect_for_display(global.display);
+
+ if (this._overlay) {
+ global.stage.remove_child(this._overlay);
+ this._overlay.remove_effect_by_name('xr-desktop');
+ this._overlay.destroy();
+ this._overlay = null;
+ }
+
+ if (this._xr_effect) {
+ this._xr_effect = null;
+ }
+
+ if (this._cursor_manager) {
+ this._cursor_manager.disable();
+ this._cursor_manager = null;
+ }
+ }
+
+ disable() {
+ this._effect_disable();
+ this._target_monitor = null;
+ if (this._monitor_manager) {
+ this._monitor_manager.disable();
+ this._monitor_manager = null;
+ }
+ }
+}
+
+function init() {
+ return new Extension();
+}
diff --git a/gnome/src/globals.js b/gnome/src/globals.js
new file mode 100644
index 0000000..b30b510
--- /dev/null
+++ b/gnome/src/globals.js
@@ -0,0 +1,5 @@
+const Globals = {
+ ipc_file: null, // Gio.File instance, file exists
+ extension_dir: null // string path
+}
+export default Globals;
\ No newline at end of file
diff --git a/gnome/src/ipc.js b/gnome/src/ipc.js
new file mode 100644
index 0000000..77d8fa1
--- /dev/null
+++ b/gnome/src/ipc.js
@@ -0,0 +1,49 @@
+export const UINT8_SIZE = 1;
+export const BOOL_SIZE = UINT8_SIZE;
+export const UINT_SIZE = 4;
+export const FLOAT_SIZE = 4;
+
+export const DATA_VIEW_INFO_OFFSET_INDEX = 0;
+export const DATA_VIEW_INFO_SIZE_INDEX = 1;
+export const DATA_VIEW_INFO_COUNT_INDEX = 2;
+
+// computes the end offset, exclusive
+export function dataViewEnd(dataViewInfo) {
+ return dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX] + dataViewInfo[DATA_VIEW_INFO_SIZE_INDEX] * dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX];
+}
+
+export function dataViewUint8(dataView, dataViewInfo) {
+ return dataView.getUint8(dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX]);
+}
+
+export function dataViewUint(dataView, dataViewInfo) {
+ return dataView.getUint32(dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX], true);
+}
+
+export function dataViewBigUint(dataView, dataViewInfo) {
+ return Number(dataView.getBigUint64(dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX], true));
+}
+
+export function dataViewUintArray(dataView, dataViewInfo) {
+ const uintArray = []
+ let offset = dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX];
+ for (let i = 0; i < dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX]; i++) {
+ uintArray.push(dataView.getUint32(offset, true));
+ offset += UINT_SIZE;
+ }
+ return uintArray;
+}
+
+export function dataViewFloat(dataView, dataViewInfo) {
+ return dataView.getFloat32(dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX], true);
+}
+
+export function dataViewFloatArray(dataView, dataViewInfo) {
+ const floatArray = []
+ let offset = dataViewInfo[DATA_VIEW_INFO_OFFSET_INDEX];
+ for (let i = 0; i < dataViewInfo[DATA_VIEW_INFO_COUNT_INDEX]; i++) {
+ floatArray.push(dataView.getFloat32(offset, true));
+ offset += FLOAT_SIZE;
+ }
+ return floatArray;
+}
\ No newline at end of file
diff --git a/gnome/src/math.js b/gnome/src/math.js
new file mode 100644
index 0000000..497274e
--- /dev/null
+++ b/gnome/src/math.js
@@ -0,0 +1,3 @@
+export function degreeToRadian(degree) {
+ return degree * Math.PI / 180;
+}
\ No newline at end of file
diff --git a/gnome/src/metadata.json b/gnome/src/metadata.json
new file mode 100644
index 0000000..894c670
--- /dev/null
+++ b/gnome/src/metadata.json
@@ -0,0 +1,10 @@
+{
+ "uuid": "breezydesktop@xronlinux.com",
+ "name": "Breezy GNOME XR Desktop",
+ "description": "XR virtual desktop for GNOME.",
+ "settings-schema": "com.xronlinux.BreezyDesktop",
+ "shell-version": [
+ "45", "46"
+ ],
+ "url": "https://github.com/wheaney/breezy-desktop"
+}
\ No newline at end of file
diff --git a/gnome/src/monitormanager.js b/gnome/src/monitormanager.js
new file mode 100644
index 0000000..09e4e2b
--- /dev/null
+++ b/gnome/src/monitormanager.js
@@ -0,0 +1,167 @@
+// Taken from https://github.com/jkitching/soft-brightness-plus
+//
+// Copyright (C) 2019, 2021 Philippe Troin (F-i-f on Github)
+// Copyright (C) 2023 Joel Kitching (jkitching on Github)
+//
+// 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 .
+
+import Gio from 'gi://Gio';
+
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+
+let cachedDisplayConfigProxy = null;
+
+function getDisplayConfigProxy(extPath) {
+ if (cachedDisplayConfigProxy == null) {
+ let xml = null;
+ const file = Gio.File.new_for_path(extPath + '/dbus-interfaces/org.gnome.Mutter.DisplayConfig.xml');
+ try {
+ const [ok, bytes] = file.load_contents(null);
+ if (ok) {
+ xml = new TextDecoder().decode(bytes);
+ }
+ } catch (e) {
+ console.error('failed to load DisplayConfig interface XML');
+ throw e;
+ }
+ cachedDisplayConfigProxy = Gio.DBusProxy.makeProxyWrapper(xml);
+ }
+ return cachedDisplayConfigProxy;
+}
+
+export function newDisplayConfig(extPath, callback) {
+ const DisplayConfigProxy = getDisplayConfigProxy(extPath);
+ new DisplayConfigProxy(
+ Gio.DBus.session,
+ 'org.gnome.Mutter.DisplayConfig',
+ '/org/gnome/Mutter/DisplayConfig',
+ callback
+ );
+}
+
+export function getMonitorConfig(displayConfigProxy, callback) {
+ displayConfigProxy.GetResourcesRemote((result) => {
+ if (result.length <= 2) {
+ callback(null, 'Cannot get DisplayConfig: No outputs in GetResources()');
+ } 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];
+
+ // grab refresh rate from the modes array
+ const refreshRate = result[3][i][4];
+
+ monitors.push([displayName, connectorName, vendor, product, serial, refreshRate]);
+ }
+ callback(monitors, null);
+ }
+ });
+}
+
+// Monitor change handling
+export default class MonitorManager {
+ constructor(extPath) {
+ this._extPath = extPath;
+
+ this._monitorsChangedConnection = null;
+ this._displayConfigProxy = null;
+ this._backendManager = null;
+ this._monitorProperties = null;
+ this._changeHookFn = null;
+ }
+
+ enable() {
+ this._backendManager = global.backend.get_monitor_manager();
+ newDisplayConfig(this._extPath, (proxy, error) => {
+ if (error) {
+ return;
+ }
+ this._displayConfigProxy = proxy;
+ this._on_monitors_change();
+ });
+
+ this._monitorsChangedConnection = Main.layoutManager.connect('monitors-changed', this._on_monitors_change.bind(this));
+ }
+
+ disable() {
+ Main.layoutManager.disconnect(this._monitorsChangedConnection);
+
+ this._monitorsChangedConnection = null;
+ this._displayConfigProxy = null;
+ this._backendManager = null;
+ this._monitorProperties = null;
+ this._changeHookFn = null;
+ }
+
+ setChangeHook(fn) {
+ this._changeHookFn = fn;
+ }
+
+ setPostCallback(callback) {
+ this._postCallback = callback;
+ }
+
+ getMonitors() {
+ return Main.layoutManager.monitors;
+ }
+
+ getMonitorPropertiesList() {
+ return this._monitorProperties;
+ }
+
+ _on_monitors_change() {
+ if (this._displayConfigProxy == null) {
+ return;
+ }
+ getMonitorConfig(this._displayConfigProxy, (result, error) => {
+ if (error) {
+ return;
+ }
+ const monitorProperties = [];
+ for (let i = 0; i < result.length; i++) {
+ const [monitorName, connectorName, vendor, product, serial, refreshRate] = result[i];
+ const monitorIndex = this._backendManager.get_monitor_for_connector(connectorName);
+ console.log(`\n\nFound monitor ${monitorName}, vendor ${vendor}, product ${product}, serial ${serial}, connector ${connectorName}, index ${monitorIndex}\n\n`);
+ if (monitorIndex >= 0) {
+ monitorProperties[monitorIndex] = {
+ index: monitorIndex,
+ name: monitorName,
+ vendor: vendor,
+ product: product,
+ serial: serial,
+ connector: connectorName,
+ refreshRate: refreshRate
+ };
+ }
+ }
+ this._monitorProperties = monitorProperties;
+ if (this._changeHookFn !== null) {
+ this._changeHookFn();
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/gnome/src/schemas/com.xronlinux.BreezyDesktop.gschema.xml b/gnome/src/schemas/com.xronlinux.BreezyDesktop.gschema.xml
new file mode 120000
index 0000000..399c9da
--- /dev/null
+++ b/gnome/src/schemas/com.xronlinux.BreezyDesktop.gschema.xml
@@ -0,0 +1 @@
+../../../ui/data/com.xronlinux.BreezyDesktop.gschema.xml
\ No newline at end of file
diff --git a/gnome/src/shader.js b/gnome/src/shader.js
new file mode 100644
index 0000000..b906762
--- /dev/null
+++ b/gnome/src/shader.js
@@ -0,0 +1,14 @@
+import Gio from 'gi://Gio';
+
+export function getShaderSource(path) {
+ const file = Gio.file_new_for_path(path);
+ const data = file.load_contents(null);
+
+ // version string helps with linting, but GNOME extension doesn't like it, so remove it if it's there
+ //
+ // TODO - Gjs on GNOME 45.5 WARNING: Some code called array.toString() on a Uint8Array instance. Previously this
+ // would have interpreted the bytes of the array as a string, but that is nonstandard. In the future this
+ // will return the bytes as comma-separated digits. For the time being, the old behavior has been preserved,
+ // but please fix your code anyway to use TextDecoder.
+ return data[1].toString().replace(/^#version .*$/gm, '') + '\n';
+}
\ No newline at end of file
diff --git a/gnome/src/textures/calibrating.png b/gnome/src/textures/calibrating.png
new file mode 120000
index 0000000..9611d2d
--- /dev/null
+++ b/gnome/src/textures/calibrating.png
@@ -0,0 +1 @@
+../../../modules/sombrero/calibrating.png
\ No newline at end of file
diff --git a/gnome/src/textures/custom_banner.png b/gnome/src/textures/custom_banner.png
new file mode 120000
index 0000000..e48019a
--- /dev/null
+++ b/gnome/src/textures/custom_banner.png
@@ -0,0 +1 @@
+../../../vulkan/custom_banner.png
\ No newline at end of file
diff --git a/gnome/src/time.js b/gnome/src/time.js
new file mode 100644
index 0000000..6c3259c
--- /dev/null
+++ b/gnome/src/time.js
@@ -0,0 +1,7 @@
+export function getEpochSec() {
+ return toSec(Date.now());
+}
+
+export function toSec(milliseconds) {
+ return Math.floor(milliseconds / 1000);
+}
\ No newline at end of file
diff --git a/gnome/src/xrEffect.js b/gnome/src/xrEffect.js
new file mode 100644
index 0000000..ad3a21f
--- /dev/null
+++ b/gnome/src/xrEffect.js
@@ -0,0 +1,320 @@
+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,
+ dataViewUintArray,
+ 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 { toSec } from "./time.js";
+
+export const IPC_FILE_PATH = "/dev/shm/breezy_desktop_imu";
+
+// the driver should be using the same data layout version
+const DATA_LAYOUT_VERSION = 2;
+
+// 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 DATA_VIEW_LENGTH = dataViewEnd(IMU_QUAT_DATA);
+
+// cached after first retrieval
+const shaderUniformLocations = {
+ 'enabled': null,
+ 'show_banner': null,
+ 'imu_quat_data': null,
+ 'look_ahead_cfg': null,
+ 'look_ahead_ms': null,
+ 'stage_aspect_ratio': null,
+ 'display_aspect_ratio': null,
+ 'trim_width_percent': null,
+ 'trim_height_percent': null,
+ 'display_zoom': null,
+ 'display_north_offset': null,
+ 'lens_distance_ratio': null,
+ 'sbs_enabled': null,
+ 'sbs_content': null,
+ 'custom_banner_enabled': null,
+ 'half_fov_z_rads': null,
+ 'half_fov_y_rads': null,
+ 'screen_distance': null,
+ 'display_res': null
+};
+
+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() {
+ const dataView = this._dataView;
+
+ if (dataView.byteLength === DATA_VIEW_LENGTH) {
+ const version = dataViewUint8(dataView, VERSION);
+ const imuDateMS = dataViewBigUint(dataView, EPOCH_MS);
+ const currentDateMS = Date.now();
+ const validKeepalive = Math.abs(toSec(currentDateMS) - toSec(imuDateMS)) < 5;
+ const imuData = dataViewFloatArray(dataView, IMU_QUAT_DATA);
+ const imuResetState = validKeepalive && imuData[0] === 0.0 && imuData[1] === 0.0 && imuData[2] === 0.0 && imuData[3] === 1.0;
+ const enabled = dataViewUint8(dataView, ENABLED) !== 0 && version === DATA_LAYOUT_VERSION && validKeepalive;
+
+ if (enabled) {
+ const displayRes = dataViewUintArray(dataView, DISPLAY_RES);
+ const displayFov = dataViewFloat(dataView, DISPLAY_FOV);
+ const lensDistanceRatio = dataViewFloat(dataView, LENS_DISTANCE_RATIO);
+
+ // compute these values once, they only change when the XR device changes
+ const displayAspectRatio = displayRes[0] / displayRes[1];
+ const stageAspectRatio = this.target_monitor.width / this.target_monitor.height;
+ const diagToVertRatio = Math.sqrt(Math.pow(stageAspectRatio, 2) + 1);
+ const halfFovZRads = degreeToRadian(displayFov / diagToVertRatio) / 2;
+ const halfFovYRads = halfFovZRads * stageAspectRatio;
+ const screenDistance = 1.0 - lensDistanceRatio;
+
+ // our overlay doesn't quite cover the full screen texture, which allows us to see some of the real desktop
+ // underneath, so we trim two pixels around the entire edge of the texture
+ const trimWidthPercent = 2.0 / this.target_monitor.width;
+ const trimHeightPercent = 2.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
+ setSingleFloat(this, 'stage_aspect_ratio', stageAspectRatio);
+ setSingleFloat(this, 'display_aspect_ratio', displayAspectRatio);
+ setSingleFloat(this, 'trim_width_percent', trimWidthPercent);
+ setSingleFloat(this, 'trim_height_percent', trimHeightPercent);
+ setSingleFloat(this, 'half_fov_z_rads', halfFovZRads);
+ setSingleFloat(this, 'half_fov_y_rads', halfFovYRads);
+ setSingleFloat(this, 'screen_distance', screenDistance);
+
+ // TOOD - drive from settings
+ setSingleFloat(this, 'display_zoom', 1.0);
+ }
+
+ // these variables are always in play, even if enabled is false
+ setSingleFloat(this, 'enabled', enabled ? 1.0 : 0.0);
+ setSingleFloat(this, 'show_banner', imuResetState ? 1.0 : 0.0);
+ setSingleFloat(this, 'sbs_content', 0.0); // TOOD - drive from settings
+ setSingleFloat(this, 'sbs_enabled', dataViewUint8(dataView, SBS_ENABLED) !== 0 ? 1.0 : 0.0);
+ setSingleFloat(this, 'custom_banner_enabled', dataViewUint8(dataView, CUSTOM_BANNER_ENABLED) !== 0 ? 1.0 : 0.0);
+
+ this.set_uniform_float(shaderUniformLocations['display_res'], 2, [this.target_monitor.width, this.target_monitor.height]);
+ } else if (dataView.byteLength !== 0) {
+ console.error(`Invalid dataView.byteLength: ${dataView.byteLength} !== ${DATA_VIEW_LENGTH}`)
+ }
+}
+
+export const XREffect = GObject.registerClass({
+ Properties: {
+ '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
+ ),
+ '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 {
+ constructor(params = {}) {
+ super(params);
+
+ this._frametime = Math.floor(1000 / this.target_framerate);
+
+ this._is_display_distance_at_end = false;
+ this._distance_ease_timeline = null;
+
+ 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);
+ }
+
+ _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._is_display_distance_at_end = !this._is_display_distance_at_end;
+
+ this._distance_ease_timeline.start();
+ }
+
+ vfunc_build_pipeline() {
+ const code = getShaderSource(`${Globals.extension_dir}/IMUAdjust.frag`);
+ const main = 'PS_IMU_Transform(vec4(0, 0, 0, 0), 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 now = Date.now();
+ var lastPaint = this._last_paint || 0;
+ var frametime = this._frametime;
+ var calibratingImage = this.calibratingImage;
+ var customBannerImage = this.customBannerImage;
+ const data = Globals.ipc_file.load_contents(null);
+ if (data[0]) {
+ const buffer = new Uint8Array(data[1]).buffer;
+ this._dataView = new DataView(buffer);
+ if (!this._initialized) {
+ this.set_uniform_float(this.get_uniform_location('uDesktopTexture'), 1, [0]);
+
+ 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('uCalibratingTexture'), 1);
+ this.get_pipeline().set_uniform_1i(this.get_uniform_location('uCustomBannerTexture'), 2);
+
+ for (let key in shaderUniformLocations) {
+ shaderUniformLocations[key] = this.get_uniform_location(key);
+ }
+ this.setIntermittentUniformVariables = setIntermittentUniformVariables.bind(this);
+ this.setIntermittentUniformVariables();
+
+ this._redraw_timeout_id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, this._frametime, () => {
+ if ((now - lastPaint) > frametime) global.stage.queue_redraw();
+ return GLib.SOURCE_CONTINUE;
+ });
+
+ this._uniforms_timeout_id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, (() => {
+ this.setIntermittentUniformVariables();
+ return GLib.SOURCE_CONTINUE;
+ }).bind(this));
+
+ this._initialized = true;
+ }
+
+ if (this._dataView.byteLength === DATA_VIEW_LENGTH) {
+ setSingleFloat(this, 'display_north_offset', this.display_distance);
+ setSingleFloat(this, 'look_ahead_ms', lookAheadMS(this._dataView));
+ setUniformMatrix(this, 'imu_quat_data', 4, this._dataView, IMU_QUAT_DATA);
+ } else if (this._dataView.byteLength !== 0) {
+ console.error(`Invalid dataView.byteLength: ${this._dataView.byteLength} !== ${DATA_VIEW_LENGTH}`)
+ }
+
+ // improves sampling quality for smooth text and edges
+ this.get_pipeline().set_layer_filters (
+ 0,
+ Cogl.PipelineFilter.LINEAR_MIPMAP_LINEAR,
+ Cogl.PipelineFilter.LINEAR
+ );
+
+ super.vfunc_paint_target(node, paintContext);
+ } else {
+ super.vfunc_paint_target(node, paintContext);
+ }
+ this._last_paint = now;
+ }
+
+ vfunc_dispose() {
+ if (this._redraw_timeout_id) GLib.source_remove(this._redraw_timeout_id);
+ if (this._uniforms_timeout_id) GLib.source_remove(this._uniforms_timeout_id);
+ }
+});
\ No newline at end of file
diff --git a/modules/sombrero b/modules/sombrero
new file mode 160000
index 0000000..84c2caa
--- /dev/null
+++ b/modules/sombrero
@@ -0,0 +1 @@
+Subproject commit 84c2caa96271eb37b02f78c3be1fcb5d641dc247
diff --git a/modules/xrealAirLinuxDriver b/modules/xrealAirLinuxDriver
index 9132174..f379c97 160000
--- a/modules/xrealAirLinuxDriver
+++ b/modules/xrealAirLinuxDriver
@@ -1 +1 @@
-Subproject commit 91321744c438914154279abb01265d0541ea0972
+Subproject commit f379c976e003823ed6a61fe2d054c802b3bca80d
diff --git a/ui/bin/package b/ui/bin/package
new file mode 100755
index 0000000..a3c6764
--- /dev/null
+++ b/ui/bin/package
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+
+# exit when any command fails
+set -e
+
+check_command() {
+ if ! command -v "$1" &>/dev/null; then
+ echo "Please install \"$1\" and make sure it's available in your \$PATH"
+ exit 1
+ fi
+}
+
+check_command "flatpak"
+check_command "flatpak-builder"
+
+# https://stackoverflow.com/a/246128
+SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
+
+TMP_DIR=$(mktemp -d -t breezy-ui-flatpak-XXXXXXXXXX)
+OUT_DIR=$SCRIPT_DIR/../out
+rm -rf $OUT_DIR
+mkdir -p $OUT_DIR
+
+flatpak-builder --force-clean $TMP_DIR/build $SCRIPT_DIR/../com.xronlinux.BreezyDesktop.json
+flatpak build-export $TMP_DIR/export $TMP_DIR/build
+flatpak build-bundle $TMP_DIR/export $OUT_DIR/com.xronlinux.BreezyDesktop.flatpak com.xronlinux.BreezyDesktop --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
+
+rm -rf "$TMP_DIR"
\ No newline at end of file
diff --git a/ui/build-aux/dconf-override.patch b/ui/build-aux/dconf-override.patch
new file mode 100644
index 0000000..2f6e38e
--- /dev/null
+++ b/ui/build-aux/dconf-override.patch
@@ -0,0 +1,23 @@
+diff --git a/engine/dconf-engine-source-user.c b/engine/dconf-engine-source-user.c
+index 1657875..e4f8786 100644
+--- a/engine/dconf-engine-source-user.c
++++ b/engine/dconf-engine-source-user.c
+@@ -39,11 +39,17 @@ dconf_engine_source_user_open_gvdb (const gchar *name)
+ {
+ GvdbTable *table;
+ gchar *filename;
++ const gchar *override;
++
++ override = g_getenv ("DCONF_USER_CONFIG_DIR");
++ if (override == NULL)
++ filename = g_build_filename (g_get_user_config_dir (), "dconf", name, NULL);
++ else
++ filename = g_build_filename (g_get_home_dir (), override, name, NULL);
+
+ /* This can fail in the normal case of the user not having any
+ * settings. That's OK and it shouldn't be considered as an error.
+ */
+- filename = g_build_filename (g_get_user_config_dir (), "dconf", name, NULL);
+ table = gvdb_table_new (filename, FALSE, NULL);
+ g_free (filename);
+
\ No newline at end of file
diff --git a/ui/build-aux/start-breezy-desktop.sh b/ui/build-aux/start-breezy-desktop.sh
new file mode 100644
index 0000000..988ec3f
--- /dev/null
+++ b/ui/build-aux/start-breezy-desktop.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+
+# stolen from dconf-editor:
+# https://gitlab.gnome.org/GNOME/dconf-editor/-/blob/master/build-aux/start-dconf-editor.sh
+
+IFS=: read -ra host_data_dirs < <(flatpak-spawn --host sh -c 'echo "$XDG_DATA_DIRS"')
+
+# To avoid potentially muddying up $XDG_DATA_DIRS too much, we link the schema paths
+# into a temporary directory.
+bridge_dir=$XDG_RUNTIME_DIR/dconf-bridge
+mkdir -p "$bridge_dir"
+
+HOST_XDG_DATA_DIRS=""
+
+for dir in "${host_data_dirs[@]}"; do
+ if [[ "$dir" == /usr/* ]]; then
+ dir=/run/host/"$dir"
+ fi
+
+ schemas="$dir/glib-2.0/schemas"
+ if [[ -d "$schemas" ]]; then
+ bridged=$(mktemp -d XXXXXXXXXX -p "$bridge_dir")
+ mkdir -p "$bridged"/glib-2.0
+ ln -s "$schemas" "$bridged"/glib-2.0
+ HOST_XDG_DATA_DIRS="${HOST_XDG_DATA_DIRS}:${bridged}"
+ fi
+done
+
+# We MUST prepend the host's data dirs BEFORE the Flatpak environment's own dirs,
+# otherwise data (such as default values) load in the wrong order and would then
+# incorrectly prefer the Flatpak's internal defaults instead of the host's defaults!
+if [[ ! -z "${HOST_XDG_DATA_DIRS}" ]]; then
+ XDG_DATA_DIRS="${HOST_XDG_DATA_DIRS:1}:${XDG_DATA_DIRS}"
+fi
+
+export XDG_DATA_DIRS
+exec breezydesktop "$@"
\ No newline at end of file
diff --git a/ui/com.xronlinux.BreezyDesktop.json b/ui/com.xronlinux.BreezyDesktop.json
new file mode 100644
index 0000000..5c87a57
--- /dev/null
+++ b/ui/com.xronlinux.BreezyDesktop.json
@@ -0,0 +1,86 @@
+{
+ "id" : "com.xronlinux.BreezyDesktop",
+ "runtime" : "org.gnome.Platform",
+ "runtime-version" : "46",
+ "sdk" : "org.gnome.Sdk",
+ "command" : "breezydesktop",
+ "finish-args" : [
+ "--share=network",
+ "--share=ipc",
+ "--socket=fallback-x11",
+ "--device=dri",
+ "--socket=wayland",
+ "--device=shm",
+ "--talk-name=ca.desrt.dconf",
+ "--filesystem=xdg-run/dconf",
+ "--filesystem=host",
+ "--env=DCONF_USER_CONFIG_DIR=.config/dconf",
+ "--env=GIO_EXTRA_MODULES=/app/lib/gio/modules/",
+ "--talk-name=org.freedesktop.Flatpak",
+ "--talk-name=org.gnome.Shell.Extensions"
+ ],
+ "cleanup" : [
+ "/include",
+ "/lib/pkgconfig",
+ "/man",
+ "/share/doc",
+ "/share/gtk-doc",
+ "/share/man",
+ "/share/pkgconfig",
+ "*.la",
+ "*.a"
+ ],
+ "modules" : [
+ "python3-pydbus.json",
+ {
+ "name": "dconf",
+ "buildsystem": "meson",
+ "config-opts": [
+ "-Dbash_completion=false",
+ "-Dman=false",
+ "-Dsystemduserunitdir=' '"
+ ],
+ "cleanup": [
+ "/include",
+ "/lib/pkgconfig",
+ "/libexec",
+ "/share/dbus-1"
+ ],
+ "sources": [
+ {
+ "type": "archive",
+ "url": "https://download.gnome.org/sources/dconf/0.40/dconf-0.40.0.tar.xz",
+ "sha256": "cf7f22a4c9200421d8d3325c5c1b8b93a36843650c9f95d6451e20f0bcb24533"
+ },
+ {
+ "type": "patch",
+ "path": "build-aux/dconf-override.patch"
+ }
+ ]
+ },
+ {
+ "name": "scripts",
+ "buildsystem": "simple",
+ "build-commands": [
+ "install -Dm 755 start-breezy-desktop.sh /app/bin/start-breezy-desktop"
+ ],
+ "sources": [
+ {
+ "type": "file",
+ "path": "build-aux/start-breezy-desktop.sh"
+ }
+ ]
+ },
+ {
+ "name" : "breezydesktop",
+ "builddir" : true,
+ "buildsystem" : "meson",
+ "sources" : [
+ {
+ "type" : "dir",
+ "path" : "."
+ }
+ ]
+ }
+ ]
+}
diff --git a/ui/data/com.xronlinux.BreezyDesktop.desktop.in b/ui/data/com.xronlinux.BreezyDesktop.desktop.in
new file mode 100644
index 0000000..3955dac
--- /dev/null
+++ b/ui/data/com.xronlinux.BreezyDesktop.desktop.in
@@ -0,0 +1,8 @@
+[Desktop Entry]
+Name=Breezy Desktop
+Exec=start-breezy-desktop
+Icon=com.xronlinux.BreezyDesktop
+Terminal=false
+Type=Application
+Categories=GTK;
+StartupNotify=true
diff --git a/ui/data/com.xronlinux.BreezyDesktop.gschema.xml b/ui/data/com.xronlinux.BreezyDesktop.gschema.xml
new file mode 100644
index 0000000..0dc1db1
--- /dev/null
+++ b/ui/data/com.xronlinux.BreezyDesktop.gschema.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+ space']]]>
+
+ Re-center display
+
+ Shortcut to re-center the virtual display.
+
+
+
+
+ Return']]]>
+
+ Trigger change to display distance
+
+ Shortcut to change the display distance.
+
+
+
+
+ 0']]]>
+
+ Toggle follow mode
+
+ Shortcut to toggle follow mode.
+
+
+
+
+ 1.05
+
+ Display distance
+
+ How far away the display appears. Farther will look smaller, closer will look larger.
+
+
+
+
+ 0.85
+
+ Display distance start
+
+ Start distance when using the "change distance" shortcut.
+
+
+
+
+ 1.05
+
+ Display distance end
+
+ End distance when using the "toggle display distance" shortcut.
+
+
+
+
+ false
+
+ Developer mode
+
+ Enable developer mode
+
+
+
+
diff --git a/ui/data/com.xronlinux.BreezyDesktop.metainfo.xml.in b/ui/data/com.xronlinux.BreezyDesktop.metainfo.xml.in
new file mode 100644
index 0000000..eab0852
--- /dev/null
+++ b/ui/data/com.xronlinux.BreezyDesktop.metainfo.xml.in
@@ -0,0 +1,9 @@
+
+
+ com.xronlinux.BreezyDesktop.desktop
+ CC0-1.0
+ GPL-3.0-or-later
+
+ No description
+
+
diff --git a/ui/data/icons/hicolor/scalable/apps/com.xronlinux.BreezyDesktop.svg b/ui/data/icons/hicolor/scalable/apps/com.xronlinux.BreezyDesktop.svg
new file mode 100644
index 0000000..45aed09
--- /dev/null
+++ b/ui/data/icons/hicolor/scalable/apps/com.xronlinux.BreezyDesktop.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/data/icons/meson.build b/ui/data/icons/meson.build
new file mode 100644
index 0000000..919bfad
--- /dev/null
+++ b/ui/data/icons/meson.build
@@ -0,0 +1,7 @@
+application_id = 'com.xronlinux.BreezyDesktop'
+
+scalable_dir = 'hicolor' / 'scalable' / 'apps'
+install_data(
+ scalable_dir / ('@0@.svg').format(application_id),
+ install_dir: get_option('datadir') / 'icons' / scalable_dir
+)
diff --git a/ui/data/meson.build b/ui/data/meson.build
new file mode 100644
index 0000000..71c25bd
--- /dev/null
+++ b/ui/data/meson.build
@@ -0,0 +1,36 @@
+desktop_file = i18n.merge_file(
+ input: 'com.xronlinux.BreezyDesktop.desktop.in',
+ output: 'com.xronlinux.BreezyDesktop.desktop',
+ type: 'desktop',
+ po_dir: '../po',
+ install: true,
+ install_dir: get_option('datadir') / 'applications'
+)
+
+desktop_utils = find_program('desktop-file-validate', required: false)
+if desktop_utils.found()
+ test('Validate desktop file', desktop_utils, args: [desktop_file])
+endif
+
+appstream_file = i18n.merge_file(
+ input: 'com.xronlinux.BreezyDesktop.metainfo.xml.in',
+ output: 'com.xronlinux.BreezyDesktop.metainfo.xml',
+ po_dir: '../po',
+ install: true,
+ install_dir: get_option('datadir') / 'metainfo'
+)
+
+appstreamcli = find_program('appstreamcli', required: false, disabler: true)
+test('Validate appstream file', appstreamcli,
+ args: ['validate', '--no-net', '--explain', appstream_file])
+
+install_data('com.xronlinux.BreezyDesktop.gschema.xml',
+ install_dir: get_option('datadir') / 'glib-2.0' / 'schemas'
+)
+
+compile_schemas = find_program('glib-compile-schemas', required: false, disabler: true)
+test('Validate schema file',
+ compile_schemas,
+ args: ['--strict', '--dry-run', meson.current_source_dir()])
+
+subdir('icons')
diff --git a/ui/meson.build b/ui/meson.build
new file mode 100644
index 0000000..9dffea0
--- /dev/null
+++ b/ui/meson.build
@@ -0,0 +1,20 @@
+project('breezydesktop',
+ version: '0.1.0',
+ meson_version: '>= 0.62.0',
+ default_options: [ 'warning_level=2', 'werror=false', ],
+)
+
+i18n = import('i18n')
+gnome = import('gnome')
+
+
+
+subdir('data')
+subdir('src')
+subdir('po')
+
+gnome.post_install(
+ glib_compile_schemas: true,
+ gtk_update_icon_cache: true,
+ update_desktop_database: true,
+)
diff --git a/ui/modules/PyXRLinuxDriverIPC b/ui/modules/PyXRLinuxDriverIPC
new file mode 160000
index 0000000..b2cdc8c
--- /dev/null
+++ b/ui/modules/PyXRLinuxDriverIPC
@@ -0,0 +1 @@
+Subproject commit b2cdc8cc5fc593ab04015cec08a8e97b103ffa63
diff --git a/ui/po/LINGUAS b/ui/po/LINGUAS
new file mode 100644
index 0000000..e69de29
diff --git a/ui/po/POTFILES b/ui/po/POTFILES
new file mode 100644
index 0000000..2fbee68
--- /dev/null
+++ b/ui/po/POTFILES
@@ -0,0 +1,6 @@
+data/com.xronlinux.BreezyDesktop.desktop.in
+data/com.xronlinux.BreezyDesktop.metainfo.xml.in
+data/com.xronlinux.BreezyDesktop.gschema.xml
+src/main.py
+src/window.py
+src/window.ui
diff --git a/ui/po/meson.build b/ui/po/meson.build
new file mode 100644
index 0000000..25704f5
--- /dev/null
+++ b/ui/po/meson.build
@@ -0,0 +1 @@
+i18n.gettext('breezydesktop', preset: 'glib')
diff --git a/ui/python3-pydbus.json b/ui/python3-pydbus.json
new file mode 100644
index 0000000..9fd19cd
--- /dev/null
+++ b/ui/python3-pydbus.json
@@ -0,0 +1,14 @@
+{
+ "name": "python3-pydbus",
+ "buildsystem": "simple",
+ "build-commands": [
+ "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"pydbus\" --no-build-isolation"
+ ],
+ "sources": [
+ {
+ "type": "file",
+ "url": "https://files.pythonhosted.org/packages/92/56/27148014c2f85ce70332f18612f921f682395c7d4e91ec103783be4fce00/pydbus-0.6.0-py2.py3-none-any.whl",
+ "sha256": "66b80106352a718d80d6c681dc2a82588048e30b75aab933e4020eb0660bf85e"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ui/src/__init__.py b/ui/src/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/ui/src/breezydesktop.gresource.xml b/ui/src/breezydesktop.gresource.xml
new file mode 100644
index 0000000..4df66f8
--- /dev/null
+++ b/ui/src/breezydesktop.gresource.xml
@@ -0,0 +1,10 @@
+
+
+
+ gtk/connected-device.ui
+ gtk/no-device.ui
+ gtk/no-extension.ui
+ gtk/shortcut-dialog.ui
+ gtk/window.ui
+
+
diff --git a/ui/src/breezydesktop.in b/ui/src/breezydesktop.in
new file mode 100755
index 0000000..f25bbe3
--- /dev/null
+++ b/ui/src/breezydesktop.in
@@ -0,0 +1,46 @@
+#!@PYTHON@
+
+# breezydesktop.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 os
+import sys
+import signal
+import locale
+import gettext
+
+VERSION = '@VERSION@'
+pkgdatadir = '@pkgdatadir@'
+localedir = '@localedir@'
+
+sys.path.insert(1, pkgdatadir)
+signal.signal(signal.SIGINT, signal.SIG_DFL)
+locale.bindtextdomain('breezydesktop', localedir)
+locale.textdomain('breezydesktop')
+gettext.install('breezydesktop', localedir)
+
+if __name__ == '__main__':
+ import gi
+
+ from gi.repository import Gio
+ resource = Gio.Resource.load(os.path.join(pkgdatadir, 'breezydesktop.gresource'))
+ resource._register()
+
+ from breezydesktop import main
+ sys.exit(main.main(VERSION))
diff --git a/ui/src/connecteddevice.py b/ui/src/connecteddevice.py
new file mode 100644
index 0000000..175354c
--- /dev/null
+++ b/ui/src/connecteddevice.py
@@ -0,0 +1,116 @@
+from gi.repository import Gio, Gtk, GObject
+from .extensionsmanager import ExtensionsManager
+from .settingsmanager import SettingsManager
+from .shortcutdialog import bind_shortcut_settings
+from .statemanager import StateManager
+from .xrdriveripc import XRDriverIPC
+
+@Gtk.Template(resource_path='/com/xronlinux/BreezyDesktop/gtk/connected-device.ui')
+class ConnectedDevice(Gtk.Box):
+ __gtype_name__ = "ConnectedDevice"
+
+ effect_enable_switch = Gtk.Template.Child()
+ display_distance_scale = Gtk.Template.Child()
+ display_distance_adjustment = Gtk.Template.Child()
+ follow_mode_switch = Gtk.Template.Child()
+ device_label = Gtk.Template.Child()
+ set_toggle_display_distance_start_button = Gtk.Template.Child()
+ set_toggle_display_distance_end_button = Gtk.Template.Child()
+ reassign_recenter_display_shortcut_button = Gtk.Template.Child()
+ recenter_display_shortcut_label = Gtk.Template.Child()
+ reassign_toggle_display_distance_shortcut_button = Gtk.Template.Child()
+ toggle_display_distance_shortcut_label = Gtk.Template.Child()
+ reassign_toggle_follow_shortcut_button = Gtk.Template.Child()
+ toggle_follow_shortcut_label = Gtk.Template.Child()
+
+ def __init__(self):
+ super(Gtk.Box, self).__init__()
+ self.init_template()
+ self.all_enabled_state_inputs = [
+ self.display_distance_scale,
+ self.follow_mode_switch,
+ self.set_toggle_display_distance_start_button,
+ self.set_toggle_display_distance_end_button,
+ self.reassign_recenter_display_shortcut_button,
+ self.reassign_toggle_display_distance_shortcut_button,
+ self.reassign_toggle_follow_shortcut_button
+ ]
+
+ self.settings = SettingsManager.get_instance().settings
+ self.ipc = XRDriverIPC.get_instance()
+ self.extensions_manager = ExtensionsManager.get_instance()
+
+ self.settings.bind('display-distance', self.display_distance_adjustment, 'value', Gio.SettingsBindFlags.DEFAULT)
+
+ bind_shortcut_settings(self.get_parent(), [
+ [self.reassign_recenter_display_shortcut_button, self.recenter_display_shortcut_label],
+ [self.reassign_toggle_display_distance_shortcut_button, self.toggle_display_distance_shortcut_label],
+ [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.state_manager = StateManager.get_instance()
+ self.state_manager.bind_property('follow-mode', self.follow_mode_switch, 'active', GObject.BindingFlags.DEFAULT)
+
+ self.follow_mode_switch.set_active(self.state_manager.follow_mode)
+ self.follow_mode_switch.connect('notify::active', self._request_follow_mode)
+
+ self.effect_enable_switch.set_active(self._is_config_enabled(self.ipc.retrieve_config()) and self.extensions_manager.is_enabled())
+ self.effect_enable_switch.connect('notify::active', self._refresh_inputs_for_enabled_state)
+
+ self._refresh_inputs_for_enabled_state(self.effect_enable_switch, None)
+ self.extensions_manager.bind_property('breezy-enabled', self.effect_enable_switch, 'active', GObject.BindingFlags.BIDIRECTIONAL)
+
+ self.connect("destroy", self._on_widget_destroy)
+
+ def _is_config_enabled(self, config):
+ return config.get('disabled') == False and 'breezy_desktop' in config.get('external_mode', [])
+
+ def _refresh_inputs_for_enabled_state(self, switch, param):
+ requesting_enabled = switch.get_active()
+ self.extensions_manager.set_property('breezy-enabled', requesting_enabled)
+ if requesting_enabled:
+ config = self.ipc.retrieve_config(False)
+ if not self._is_config_enabled(config):
+ config['disabled'] = False
+ config['output_mode'] = 'external_only'
+ config['external_mode'] = ['breezy_desktop']
+ self.ipc.write_config(config)
+
+ for widget in self.all_enabled_state_inputs:
+ widget.set_sensitive(requesting_enabled)
+
+ def _request_follow_mode(self, switch, param):
+ if (self.state_manager.follow_mode == switch.get_active()):
+ return
+
+ self.ipc.write_control_flags({
+ 'enable_breezy_desktop_smooth_follow': switch.get_active()
+ })
+
+ def set_device_name(self, name):
+ self.device_label.set_markup(f"{name}")
+
+ 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.extensions_manager.unbind_property('breezy-enabled', self.effect_enable_switch, 'active')
+
+def reload_display_distance_toggle_button(widget):
+ distance = SettingsManager.get_instance().settings.get_double(widget.get_name())
+ if distance: widget.set_label(str(distance))
+
+def on_set_display_distance_toggle(widget):
+ settings = SettingsManager.get_instance().settings
+ distance = settings.get_double('display-distance')
+ settings.set_double(widget.get_name(), distance)
+ reload_display_distance_toggle_button(widget)
\ No newline at end of file
diff --git a/ui/src/extensionsmanager.py b/ui/src/extensionsmanager.py
new file mode 100644
index 0000000..5a3e521
--- /dev/null
+++ b/ui/src/extensionsmanager.py
@@ -0,0 +1,70 @@
+import pydbus
+from gi.repository import GObject
+
+BREEZY_DESKTOP_UUID = "breezydesktop@xronlinux.com"
+EXTENSION_STATE_ENABLED = 1
+
+class ExtensionsManager(GObject.GObject):
+ __gproperties__ = {
+ 'breezy-enabled': (bool, 'Breezy Enabled', 'Whether the Breezy Desktop GNOME extension is enabled', False, GObject.ParamFlags.READWRITE)
+ }
+
+ _instance = None
+
+ @staticmethod
+ def get_instance():
+ if ExtensionsManager._instance is None:
+ ExtensionsManager._instance = ExtensionsManager()
+ return ExtensionsManager._instance
+
+ def __init__(self):
+ GObject.GObject.__init__(self)
+
+ self.bus = pydbus.SessionBus()
+ self.gnome_shell_extensions = self.bus.get("org.gnome.Shell.Extensions")
+ self.gnome_shell_extensions.ExtensionStateChanged.connect(self._handle_extension_state_change)
+
+ self.remote_extension_state = None
+
+ def _handle_extension_state_change(self, extension_uuid, state):
+ if extension_uuid == BREEZY_DESKTOP_UUID:
+ self.remote_extension_state = state.get('state') == EXTENSION_STATE_ENABLED
+ self.set_property('breezy-enabled', self.remote_extension_state)
+
+ def is_installed(self):
+ # TODO check specific version
+ return self._is_installed(BREEZY_DESKTOP_UUID)
+
+ def enable(self):
+ self._enable_extension(BREEZY_DESKTOP_UUID)
+
+ def disable(self):
+ self._disable_extension(BREEZY_DESKTOP_UUID)
+
+ def is_enabled(self):
+ return self._is_enabled(BREEZY_DESKTOP_UUID)
+
+ def _is_installed(self, extension_uuid):
+ extensions_result = self.gnome_shell_extensions.ListExtensions()
+ for extension in extensions_result:
+ if extension == extension_uuid:
+ return True
+
+ return False
+
+ def _enable_extension(self, extension_uuid):
+ self.gnome_shell_extensions.EnableExtension(extension_uuid)
+
+ def _disable_extension(self, extension_uuid):
+ self.gnome_shell_extensions.DisableExtension(extension_uuid)
+
+ def _is_enabled(self, extension_uuid):
+ return self.gnome_shell_extensions.GetExtensionInfo(extension_uuid).get('state') == EXTENSION_STATE_ENABLED
+
+ def do_set_property(self, prop, value):
+ if prop.name == 'breezy-enabled' and value != self.remote_extension_state:
+ self.enable() if value == True else self.disable()
+
+ def do_get_property(self, prop):
+ if prop.name == 'breezy-enabled':
+ return self.remote_extension_state
\ No newline at end of file
diff --git a/ui/src/gtk/connected-device.ui b/ui/src/gtk/connected-device.ui
new file mode 100644
index 0000000..4314de3
--- /dev/null
+++ b/ui/src/gtk/connected-device.ui
@@ -0,0 +1,201 @@
+
+
+
+
+ 1
+ 20
+ 20
+ 20
+ 20
+ 20
+
+
+
+
+
+ Settings
+
+
+ Effect enabled
+ Turn on or off the XR desktop effect
+
+
+ 3
+
+
+
+
+
+
+ Display distance
+
+
+ 3
+ true
+ 0
+ 2
+ 350
+ false
+
+
+ 0.2
+ 2.5
+ 0.01
+ 1.05
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Follow enabled
+ Keep the virtual display near the center of your view
+
+
+ 3
+
+
+
+
+
+
+
+
+ Shortcuts
+ Modify keyboard shortcuts and how they work
+
+
+ Re-center display shortcut
+ Pin the virtual display to the current position
+ 2
+
+
+ 30
+ 30
+
+
+ 3
+
+
+
+
+
+
+ recenter-display-shortcut
+ 3
+ Change
+
+
+
+
+
+
+
+
+ Display distance shortcut
+ Quickly toggle between two predefined distances
+ 2
+
+
+ 30
+ 30
+
+
+ 3
+
+
+
+
+
+
+ toggle-display-distance-shortcut
+ 3
+ Change
+
+
+
+
+
+
+
+
+ Display distance start and end
+ Use the buttons to capture the current display distance as start and end points.
+ 2
+
+
+ 30
+ 150
+ 30
+
+
+ toggle-display-distance-start
+ 3
+
+
+
+
+ toggle-display-distance-end
+ 3
+
+
+
+
+
+
+
+
+ Toggle follow mode shortcut
+ Quickly toggle follow mode
+ 2
+
+
+ 30
+ 30
+
+
+ 3
+
+
+
+
+
+
+ toggle-follow-shortcut
+ 3
+ Change
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/gtk/no-device.ui b/ui/src/gtk/no-device.ui
new file mode 100644
index 0000000..54300e4
--- /dev/null
+++ b/ui/src/gtk/no-device.ui
@@ -0,0 +1,19 @@
+
+
+
+
+ 1
+ 20
+ 20
+ 20
+ 20
+ 20
+
+
+ No device connected
+ Breezy Desktop was unable to detect any supported XR devices.
+ 650
+
+
+
+
diff --git a/ui/src/gtk/no-extension.ui b/ui/src/gtk/no-extension.ui
new file mode 100644
index 0000000..e56a9dc
--- /dev/null
+++ b/ui/src/gtk/no-extension.ui
@@ -0,0 +1,19 @@
+
+
+
+
+ 1
+ 20
+ 20
+ 20
+ 20
+ 20
+
+
+ Breezy Desktop GNOME extension not ready
+ 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.
+ 650
+
+
+
+
diff --git a/ui/src/gtk/shortcut-dialog.ui b/ui/src/gtk/shortcut-dialog.ui
new file mode 100644
index 0000000..8435a9b
--- /dev/null
+++ b/ui/src/gtk/shortcut-dialog.ui
@@ -0,0 +1,43 @@
+
+
+
+
+ 1
+ 440
+ 200
+
+
+ vertical
+ 2
+ 16
+ 16
+ 16
+ 16
+
+
+ 1
+ Press your keyboard shortcut or 'Backspace' to disable...
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/gtk/window.ui b/ui/src/gtk/window.ui
new file mode 100644
index 0000000..59c560f
--- /dev/null
+++ b/ui/src/gtk/window.ui
@@ -0,0 +1,38 @@
+
+
+
+
+ 800
+
+
+
+
+
+
diff --git a/ui/src/main.py b/ui/src/main.py
new file mode 100644
index 0000000..78789b4
--- /dev/null
+++ b/ui/src/main.py
@@ -0,0 +1,91 @@
+# main.py
+#
+# 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 sys
+import gi
+
+gi.require_version('Gtk', '4.0')
+gi.require_version('Adw', '1')
+gi.require_version('Gio', '2.0')
+
+from gi.repository import Adw, Gtk, Gio
+from .statemanager import StateManager
+from .window import BreezydesktopWindow
+
+class BreezydesktopApplication(Adw.Application):
+ """The main application singleton class."""
+
+ def __init__(self):
+ super().__init__(application_id='com.xronlinux.BreezyDesktop',
+ flags=Gio.ApplicationFlags.DEFAULT_FLAGS)
+ self.create_action('quit', self.on_quit_action, ['q'])
+ self.create_action('about', self.on_about_action)
+
+ def do_activate(self):
+ """Called when the application is activated.
+
+ We raise the application's main window, creating it if
+ necessary.
+ """
+ win = self.props.active_window
+ if not win:
+ win = BreezydesktopWindow(application=self)
+ win.connect('close-request', lambda *_: self.on_quit_action())
+ win.connect('destroy', lambda *_: self.on_quit_action())
+ win.present()
+
+ def on_about_action(self, widget, _):
+ """Callback for the app.about action."""
+ about = Gtk.AboutDialog(transient_for=self.props.active_window,
+ modal=True,
+ program_name='Breezy Desktop',
+ logo_icon_name='com.xronlinux.BreezyDesktop',
+ version='0.1.0',
+ authors=['Wayne Heaney'],
+ copyright='© 2024 Wayne Heaney')
+ about.present()
+
+ def create_action(self, name, callback, shortcuts=None):
+ """Add an application action.
+
+ Args:
+ name: the name of the action
+ callback: the function to be called when the action is
+ activated
+ shortcuts: an optional list of accelerators
+ """
+ action = Gio.SimpleAction.new(name, None)
+ action.connect("activate", callback)
+ self.add_action(action)
+ if shortcuts:
+ self.set_accels_for_action(f"app.{name}", shortcuts)
+
+ def on_quit_action(self, _action = None, _pspec = None):
+ win = self.props.active_window
+ if win:
+ win.close()
+
+ StateManager.destroy_instance()
+ self.quit()
+
+
+def main(version):
+ """The application's entry point."""
+ app = BreezydesktopApplication()
+ return app.run(sys.argv)
diff --git a/ui/src/meson.build b/ui/src/meson.build
new file mode 100644
index 0000000..674d1f1
--- /dev/null
+++ b/ui/src/meson.build
@@ -0,0 +1,43 @@
+pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name()
+moduledir = pkgdatadir / 'breezydesktop'
+gnome = import('gnome')
+
+gnome.compile_resources('breezydesktop',
+ 'breezydesktop.gresource.xml',
+ gresource_bundle: true,
+ install: true,
+ install_dir: pkgdatadir,
+)
+
+python = import('python')
+
+conf = configuration_data()
+conf.set('PYTHON', python.find_installation('python3').full_path())
+conf.set('VERSION', meson.project_version())
+conf.set('localedir', get_option('prefix') / get_option('localedir'))
+conf.set('pkgdatadir', pkgdatadir)
+
+configure_file(
+ input: 'breezydesktop.in',
+ output: 'breezydesktop',
+ configuration: conf,
+ install: true,
+ install_dir: get_option('bindir'),
+ install_mode: 'r-xr-xr-x'
+)
+
+breezydesktop_sources = [
+ '../modules/PyXRLinuxDriverIPC/xrdriveripc.py',
+ '__init__.py',
+ 'connecteddevice.py',
+ 'extensionsmanager.py',
+ 'main.py',
+ 'nodevice.py',
+ 'noextension.py',
+ 'settingsmanager.py',
+ 'shortcutdialog.py',
+ 'statemanager.py',
+ 'window.py'
+]
+
+install_data(breezydesktop_sources, install_dir: moduledir)
diff --git a/ui/src/nodevice.py b/ui/src/nodevice.py
new file mode 100644
index 0000000..7006584
--- /dev/null
+++ b/ui/src/nodevice.py
@@ -0,0 +1,5 @@
+from gi.repository import Adw, Gtk
+
+@Gtk.Template(resource_path='/com/xronlinux/BreezyDesktop/gtk/no-device.ui')
+class NoDevice(Gtk.Box):
+ __gtype_name__ = "NoDevice"
diff --git a/ui/src/noextension.py b/ui/src/noextension.py
new file mode 100644
index 0000000..2c8e444
--- /dev/null
+++ b/ui/src/noextension.py
@@ -0,0 +1,5 @@
+from gi.repository import Adw, Gtk
+
+@Gtk.Template(resource_path='/com/xronlinux/BreezyDesktop/gtk/no-extension.ui')
+class NoExtension(Gtk.Box):
+ __gtype_name__ = "NoExtension"
diff --git a/ui/src/settingsmanager.py b/ui/src/settingsmanager.py
new file mode 100644
index 0000000..c88ce68
--- /dev/null
+++ b/ui/src/settingsmanager.py
@@ -0,0 +1,15 @@
+
+from gi.repository import Gio
+
+class SettingsManager:
+ _instance = None
+
+ @staticmethod
+ def get_instance():
+ if not SettingsManager._instance:
+ SettingsManager._instance = SettingsManager()
+
+ return SettingsManager._instance
+
+ def __init__(self):
+ self.settings = Gio.Settings.new_with_path("com.xronlinux.BreezyDesktop", "/com/xronlinux/BreezyDesktop/")
diff --git a/ui/src/shortcutdialog.py b/ui/src/shortcutdialog.py
new file mode 100644
index 0000000..66f1140
--- /dev/null
+++ b/ui/src/shortcutdialog.py
@@ -0,0 +1,109 @@
+from gi.repository import Gtk, Gdk
+
+from .settingsmanager import SettingsManager
+
+# ported from https://github.com/velitasali/gnome-shell-extension-awesome-tiles
+@Gtk.Template(resource_path='/com/xronlinux/BreezyDesktop/gtk/shortcut-dialog.ui')
+class ShortcutDialog(Gtk.Dialog):
+ __gtype_name__ = 'ShortcutDialog'
+
+ event_controller = Gtk.Template.Child()
+
+ def __init__(self, settings_key):
+ super(Gtk.Dialog, self).__init__()
+ self.init_template()
+
+ self.settings_key = settings_key
+ 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.visible = False
+ elif keyval == Gdk.KEY_BackSpace:
+ SettingsManager.get_instance().settings.set_strv(self.settings_key, [])
+ self.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
+ SettingsManager.get_instance().settings.set_strv(self.settings_key, [binding])
+
+ self.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, widget_tuples):
+ for widget_tuple in widget_tuples:
+ widget, label = widget_tuple
+ SettingsManager.get_instance().settings.connect('changed::' + widget.get_name(),
+ lambda *args, widget=widget, label=label: reload_shortcut_widget(widget, label))
+ widget.connect('clicked', lambda *args, widget=widget: on_assign_shortcut(window, widget))
+
+ reload_shortcut_widgets(widget_tuples)
+
+def on_assign_shortcut(window, widget):
+ dialog = ShortcutDialog(widget.get_name())
+ dialog.set_transient_for(widget.get_ancestor(Gtk.Window))
+ dialog.present()
+
+def reload_shortcut_widget(widget, label):
+ shortcut = SettingsManager.get_instance().settings.get_strv(widget.get_name())
+ label.set_accelerator(shortcut[0] if len(shortcut) > 0 else 'Disabled')
+
+def reload_shortcut_widgets(widget_tuples):
+ for widget_tuple in widget_tuples:
+ widget, label = widget_tuple
+ reload_shortcut_widget(widget, label)
\ No newline at end of file
diff --git a/ui/src/statemanager.py b/ui/src/statemanager.py
new file mode 100644
index 0000000..78c54e1
--- /dev/null
+++ b/ui/src/statemanager.py
@@ -0,0 +1,74 @@
+import threading
+from gi.repository import GObject
+from .xrdriveripc import XRDriverIPC
+
+class Logger:
+ def info(self, message):
+ print(message)
+
+ def error(self, message):
+ print(message)
+
+class StateManager(GObject.GObject):
+ __gsignals__ = {
+ 'device-update': (GObject.SIGNAL_RUN_FIRST, None, (str,))
+ }
+
+ __gproperties__ = {
+ 'follow-mode': (bool, 'Follow Mode', 'Whether the follow mode is enabled', False, GObject.ParamFlags.READWRITE)
+ }
+
+ _instance = None
+
+ @staticmethod
+ def get_instance():
+ if not StateManager._instance:
+ StateManager._instance = StateManager()
+
+ return StateManager._instance
+
+ @staticmethod
+ def destroy_instance():
+ if StateManager._instance:
+ StateManager._instance.stop()
+ StateManager._instance = None
+
+ @staticmethod
+ def device_name(state):
+ if state.get('connected_device_brand') and state.get('connected_device_model'):
+ return f"{state['connected_device_brand']} {state['connected_device_model']}"
+
+ return None
+
+ def __init__(self):
+ GObject.GObject.__init__(self)
+ self.ipc = XRDriverIPC.get_instance()
+ self.connected_device_name = None
+
+ self.start()
+
+ def start(self):
+ self.running = True
+ self._refresh_state()
+
+ def stop(self):
+ self.running = False
+
+ def _refresh_state(self):
+ self.state = self.ipc.retrieve_driver_state()
+ new_device_name = StateManager.device_name(self.state)
+ if self.connected_device_name != new_device_name:
+ self.connected_device_name = new_device_name
+ self.emit('device-update', self.connected_device_name)
+
+ self.set_property('follow-mode', self.state.get('breezy_desktop_smooth_follow_enabled'))
+
+ if self.running: threading.Timer(1.0, self._refresh_state).start()
+
+ def do_set_property(self, prop, value):
+ if prop.name == 'follow-mode':
+ self.follow_mode = value
+
+ def do_get_property(self, prop):
+ if prop.name == 'follow-mode':
+ return self.follow_mode
\ No newline at end of file
diff --git a/ui/src/window.py b/ui/src/window.py
new file mode 100644
index 0000000..3f719d9
--- /dev/null
+++ b/ui/src/window.py
@@ -0,0 +1,55 @@
+# window.py
+#
+# 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
+
+from gi.repository import Gtk
+from .extensionsmanager import ExtensionsManager
+from .statemanager import StateManager
+from .connecteddevice import ConnectedDevice
+from .nodevice import NoDevice
+from .noextension import NoExtension
+
+@Gtk.Template(resource_path='/com/xronlinux/BreezyDesktop/gtk/window.ui')
+class BreezydesktopWindow(Gtk.ApplicationWindow):
+ __gtype_name__ = 'BreezydesktopWindow'
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+
+ self.state_manager = StateManager.get_instance()
+ self.state_manager.connect('device-update', self._handle_device_update)
+
+ self.connected_device = ConnectedDevice()
+ self.no_device = NoDevice()
+ self.no_extension = NoExtension()
+
+ self._handle_device_update(self.state_manager, StateManager.device_name(self.state_manager.state))
+
+ self.connect("destroy", self._on_window_destroy)
+
+ def _handle_device_update(self, state_manager, connected_device_name):
+ if not ExtensionsManager.get_instance().is_installed():
+ self.set_child(self.no_extension)
+ elif connected_device_name:
+ self.set_child(self.connected_device)
+ self.connected_device.set_device_name(connected_device_name)
+ else:
+ self.set_child(self.no_device)
+
+ def _on_window_destroy(self, widget):
+ self.state_manager.disconnect_by_func(self._handle_device_update)
\ No newline at end of file
diff --git a/vulkan/custom_banner.png b/vulkan/custom_banner.png
index d6c916525301f72bcd5ba2bf2a1783aab8076ee1..c31648b514fb89fcd6cb9437c75d5a5cb7ab7ef2 100644
GIT binary patch
delta 765
zcmVUuPl{%dpYS+r@Gr9Mf7yG@v
z*gJ%HP$n=YVsC)@;zZ4YVw|a1p%o2>%Qhp?0#1ia6FP6rn<>6nNgNw7S4z7YA_yOYV;H2mx
zCH^ldw21NGxF7HCJ?`ECLaWL&vo8T?x@BZiNimmS6@#w`AdDgS5tEr&pOd6CJjd5P
ze0;r&^DOUke~v-5U^2ib5+_)H-LQx^h-WsfI_G`j2rJ7f@j3B?K^G)`G`%*^&G-
zg<=tSKcjET1H-q#z?$1zXCJ2zK$f~n-v9@PK&(XB>mKj!@9pj1GoAi^0AP)BpP_zK
zQ2+oATWM5SbXZMHI%99Mkph4MBw{l(HZ(IZI4v+YHD)a|WHd4@VL3K2EjczaVq-Tp
zGc{smW0N%nsS7zWHZ(9aH83_dGn45BaSAn5F)%tXGdeXjlVk=Y3N=(QFgh?ZIyE)3
zk_Hh0lfVXCBxPbYIXPrCV=ZB1Wo0ciHe_KfVqs!qEjBY{H!(FiVq`I8IFmpJOA0kq
vF)%tXG&(Ravy}&K2n5(P-29UV5FiK-5d;9=$e=2NFA#?>5COL@5CUOFJ(yCV
delta 706
zcmV;z0zLiqt_hH>36LZK#*rl-f7=biAPoFxijLqH#Ee4{Ds_X7UxSU5L_LYKb2b;_
z)W4s1dJPatlDuiDrGP=IRBDzeIND?cnLI%N9O_L^$GSu&jTUbMEgo^{abZPlmgS3)
z9#Poug(~5%4UMp?-r)}QlLpZdZEFvYFmKsxduO))
zFi~e@FlH7XfX9)<&5~@4EY@gEGariZn#30JZ%+&92Ujs!nTcjOaFO;Qe~Tj3A{G&G
z$WWc^q9Ts93Pq?8YK2xEOfLO`CJjl7i=*ILaPVWX>fqw6tAnc`2!4P#J2)x2NQwVT
z3N2ziIPS;0dyl(!fKV?p&FYE)nr@q^L|n{dSH}9e&o91@f+uY%L2~~
zo9Wa%ahO;vw6W60tY~V))5H-~(`&;Q6#X4B}fpV
zpo$X8uo0zIC&faF_TwG=Bd%W}mqM-*7VDfd<+2ga5(rZms;(f25Zbj01re$N3lo
zx_5y_&2heu9j9>u_@99*z2&def!R;eYb`Bu1cbJMi|dxA>;acM!0?kHo3bnUX$tu~
z@P0<$lm!NFf!;N*x8^=hAAmGAR536*F*Q0gG?QlrBnmWCF)%tYH99mjvy}!B
o0keq+U