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
This commit is contained in:
Wayne Heaney 2024-05-16 12:26:59 -07:00 committed by GitHub
parent 02b9bbfb83
commit e9b10838ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
64 changed files with 3274 additions and 11 deletions

8
.gitignore vendored
View File

@ -1,2 +1,6 @@
/vulkan/build/
/build/
build/
__pycache__
*.zip
gschemas.compiled
.flatpak-builder/
out/

11
.gitmodules vendored
View File

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

56
bin/breezy_gnome_setup Executable file
View File

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

74
bin/package_gnome Executable file
View File

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

View File

@ -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)

View File

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

49
gnome/bin/breezy_gnome_verify Executable file
View File

@ -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"

View File

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

79
gnome/bin/package_extension Executable file
View File

@ -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"

64
gnome/bin/setup Executable file
View File

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

1
gnome/src/IMUAdjust.frag Symbolic link
View File

@ -0,0 +1 @@
../../modules/sombrero/IMUAdjust.frag

67
gnome/src/cursor.js Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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();
}
});

211
gnome/src/cursormanager.js Normal file
View File

@ -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();
}
}
}

View File

@ -0,0 +1,453 @@
<!DOCTYPE node PUBLIC
'-//freedesktop//DTD D-BUS Object Introspection 1.0//EN'
'http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd'>
<node>
<!--
org.gnome.Mutter.DisplayConfig:
@short_description: display configuration interface
This interface is used by mutter and gnome-settings-daemon
to apply multiple monitor configuration.
-->
<interface name="org.gnome.Mutter.DisplayConfig">
<!--
GetResources:
@serial: configuration serial
@crtcs: available CRTCs
@outputs: available outputs
@modes: available modes
@max_screen_width:
@max_screen_height:
Retrieves the current layout of the hardware.
@serial is an unique identifier representing the current state
of the screen. It must be passed back to ApplyConfiguration()
and will be increased for every configuration change (so that
mutter can detect that the new configuration is based on old
state).
A CRTC (CRT controller) is a logical monitor, ie a portion
of the compositor coordinate space. It might correspond
to multiple monitors, when in clone mode, but not that
it is possible to implement clone mode also by setting different
CRTCs to the same coordinates.
The number of CRTCs represent the maximum number of monitors
that can be set to expand and it is a HW constraint; if more
monitors are connected, then necessarily some will clone. This
is complementary to the concept of the encoder (not exposed in
the API), which groups outputs that necessarily will show the
same image (again a HW constraint).
A CRTC is represented by a DBus structure with the following
layout:
* u ID: the ID in the API of this CRTC
* x winsys_id: the low-level ID of this CRTC (which might
be a XID, a KMS handle or something entirely
different)
* i x, y, width, height: the geometry of this CRTC
(might be invalid if the CRTC is not in
use)
* i current_mode: the current mode of the CRTC, or -1 if this
CRTC is not used
Note: the size of the mode will always correspond
to the width and height of the CRTC
* u current_transform: the current transform (espressed according
to the wayland protocol)
* au transforms: all possible transforms
* a{sv} properties: other high-level properties that affect this
CRTC; they are not necessarily reflected in
the hardware.
No property is specified in this version of the API.
Note: all geometry information refers to the untransformed
display.
An output represents a physical screen, connected somewhere to
the computer. Floating connectors are not exposed in the API.
An output is a DBus struct with the following fields:
* u ID: the ID in the API
* x winsys_id: the low-level ID of this output (XID or KMS handle)
* i current_crtc: the CRTC that is currently driving this output,
or -1 if the output is disabled
* au possible_crtcs: all CRTCs that can control this output
* s name: the name of the connector to which the output is attached
(like VGA1 or HDMI)
* au modes: valid modes for this output
* au clones: valid clones for this output, ie other outputs that
can be assigned the same CRTC as this one; if you
want to mirror two outputs that don't have each other
in the clone list, you must configure two different
CRTCs for the same geometry
* a{sv} properties: other high-level properties that affect this
output; they are not necessarily reflected in
the hardware.
Known properties:
- "vendor" (s): (readonly) the human readable name
of the manufacturer
- "product" (s): (readonly) the human readable name
of the display model
- "serial" (s): (readonly) the serial number of this
particular hardware part
- "display-name" (s): (readonly) a human readable name
of this output, to be shown in the UI
- "backlight" (i): (readonly, use the specific interface)
the backlight value as a percentage
(-1 if not supported)
- "primary" (b): whether this output is primary
or not
- "presentation" (b): whether this output is
for presentation only
Note: properties might be ignored if not consistenly
applied to all outputs in the same clone group. In
general, it's expected that presentation or primary
outputs will not be cloned.
A mode represents a set of parameters that are applied to
each output, such as resolution and refresh rate. It is a separate
object so that it can be referenced by CRTCs and outputs.
Multiple outputs in the same CRTCs must all have the same mode.
A mode is exposed as:
* u ID: the ID in the API
* x winsys_id: the low-level ID of this mode
* u width, height: the resolution
* d frequency: refresh rate
* u flags: mode flags as defined in xf86drmMode.h and randr.h
Output and modes are read-only objects (except for output properties),
they can change only in accordance to HW changes (such as hotplugging
a monitor), while CRTCs can be changed with ApplyConfiguration().
XXX: actually, if you insist enough, you can add new modes
through xrandr command line or the KMS API, overriding what the
kernel driver and the EDID say.
Usually, it only matters with old cards with broken drivers, or
old monitors with broken EDIDs, but it happens more often with
projectors (if for example the kernel driver doesn't add the
640x480 - 800x600 - 1024x768 default modes). Probably something
that we need to handle in mutter anyway.
-->
<method name="GetResources">
<arg name="serial" direction="out" type="u" />
<arg name="crtcs" direction="out" type="a(uxiiiiiuaua{sv})" />
<arg name="outputs" direction="out" type="a(uxiausauaua{sv})" />
<arg name="modes" direction="out" type="a(uxuudu)" />
<arg name="max_screen_width" direction="out" type="i" />
<arg name="max_screen_height" direction="out" type="i" />
</method>
<!--
ApplyConfiguration:
@serial: configuration serial
@persistent: whether this configuration should be saved on disk
@crtcs: new data for CRTCs
@outputs: new data for outputs
Applies the requested configuration changes.
@serial must match the serial from the last GetResources() call,
or org.freedesktop.DBus.AccessDenied will be generated.
If @persistent is true, mutter will attempt to replicate this
configuration the next time this HW layout appears.
@crtcs represents the new logical configuration, as a list
of structures containing:
- u ID: the API ID from the corresponding GetResources() call
- i new_mode: the API ID of the new mode to configure the CRTC
with, or -1 if the CRTC should be disabled
- i x, y: the new coordinates of the top left corner
the geometry will be completed with the size information
from @new_mode
- u transform: the desired transform
- au outputs: the API ID of outputs that should be assigned to
this CRTC
- a{sv} properties: properties whose value should be changed
Note: CRTCs not referenced in the array will be disabled.
@outputs represent the output property changes as:
- u ID: the API ID of the output to change
- a{sv} properties: properties whose value should be changed
Note: both for CRTCs and outputs, properties not included in
the dictionary will not be changed.
Note: unrecognized properties will have no effect, but if the
configuration change succeeds the property will be reported
by the next GetResources() call, and if @persistent is true,
it will also be saved to disk.
If the configuration is invalid according to the previous
GetResources() call, for example because a CRTC references
an output it cannot drive, or not all outputs support the
chosen mode, the error org.freedesktop.DBus.InvalidArgs will
be generated.
If the configuration cannot be applied for any other reason
(eg. the screen size would exceed texture limits), the error
org.freedesktop.DBus.Error.LimitsExceeded will be generated.
-->
<method name="ApplyConfiguration">
<arg name="serial" direction="in" type="u" />
<arg name="persistent" direction="in" type="b" />
<arg name="crtcs" direction="in" type="a(uiiiuaua{sv})" />
<arg name="outputs" direction="in" type="a(ua{sv})" />
</method>
<!--
ChangeBacklight:
@serial: configuration serial
@output: the API id of the output
@value: the new backlight value
Changes the backlight of @output to @value, which is
expressed as a percentage and rounded to the HW limits.
Returns the new value after rounding.
-->
<method name="ChangeBacklight">
<arg name="serial" direction="in" type="u" />
<arg name="output" direction="in" type="u" />
<arg name="value" direction="in" type="i" />
<arg name="new_value" direction="out" type="i" />
</method>
<!--
GetCrtcGamma:
@serial: configuration serial
@crtc: API id of the crtc
@red: red gamma ramp
@green: green gamma ramp
@blue: blue gamma ramp
Requests the current gamma ramps of @crtc.
-->
<method name="GetCrtcGamma">
<arg name="serial" direction="in" type="u" />
<arg name="crtc" direction="in" type="u" />
<arg name="red" direction="out" type="aq" />
<arg name="green" direction="out" type="aq" />
<arg name="blue" direction="out" type="aq" />
</method>
<!--
SetCrtcGamma:
@serial: configuration serial
@crtc: API id of the crtc
@red: red gamma ramp
@green: green gamma ramp
@blue: blue gamma ramp
Changes the gamma ramps of @crtc.
-->
<method name="SetCrtcGamma">
<arg name="serial" direction="in" type="u" />
<arg name="crtc" direction="in" type="u" />
<arg name="red" direction="in" type="aq" />
<arg name="green" direction="in" type="aq" />
<arg name="blue" direction="in" type="aq" />
</method>
<!--
PowerSaveMode:
Contains the current power saving mode for the screen, and
allows changing it.
Possible values:
- 0: on
- 1: standby
- 2: suspend
- 3: off
- -1: unknown (unsupported)
A client should not attempt to change the powersave mode
from -1 (unknown) to any other value, and viceversa.
Note that the actual effects of the different values
depend on the hardware and the kernel driver in use, and
it's perfectly possible that all values different than on
have the same effect.
Also, setting the PowerSaveMode to 3 (off) may or may
not have the same effect as disabling all outputs by
setting no CRTC on them with ApplyConfiguration(), and
may or may not cause a configuration change.
Also note that this property might become out of date
if changed through different means (for example using the
XRandR interface directly).
-->
<property name="PowerSaveMode" type="i" access="readwrite" />
<!--
MonitorsChanged:
The signal is emitted every time the screen configuration
changes.
The client should then call GetResources() to read the new layout.
-->
<signal name="MonitorsChanged" />
<!--
GetCurrentState:
@serial: configuration serial
@monitors: available monitors
@logical_monitors: current logical monitor configuration
@properties: display configuration properties
@monitors represent connected physical monitors
* s connector: connector name (e.g. HDMI-1, DP-1, etc)
* s vendor: vendor name
* s product: product name
* s serial: product serial
* a(siiddada{sv}) modes: available modes
* s id: mode ID
* i width: width in physical pixels
* i height: height in physical pixels
* d refresh rate: refresh rate
* d preferred scale: scale preferred as per calculations
* ad supported scales: scales supported by this mode
* a{sv} properties: optional properties, including:
- "is-current" (b): the mode is currently active mode
- "is-preferred" (b): the mode is the preferred mode
- "is-interlaced" (b): the mode is an interlaced mode
* a{sv} properties: optional properties, including:
- "width-mm" (i): physical width of monitor in millimeters
- "height-mm" (i): physical height of monitor in millimeters
- "is-underscanning" (b): whether underscanning is enabled
(absence of this means underscanning
not being supported)
- "max-screen-size" (ii): the maximum size a screen may have
(absence of this means unlimited screen
size)
- "is-builtin" (b): whether the monitor is built in, e.g. a
laptop panel (absence of this means it is
not built in)
- "display-name" (s): a human readable display name of the monitor
Possible mode flags:
1 : preferred mode
2 : current mode
@logical_monitors represent current logical monitor configuration
* i x: x position
* i y: y position
* d scale: scale
* u transform: transform (see below)
* b primary: true if this is the primary logical monitor
* a(sss) monitors: monitors displaying this logical monitor
* connector: name of the connector (e.g. DP-1, eDP-1 etc)
* vendor: vendor name
* product: product name
* serial: product serial
* a{sv} properties: possibly other properties
Posisble transform values:
0: normal
1: 90°
2: 180°
3: 270°
4: flipped
5: 90° flipped
6: 180° flipped
7: 270° flipped
@layout_mode current layout mode represents the way logical monitors
are layed out on the screen. Possible modes include:
1 : physical
2 : logical
With physical layout mode, each logical monitor has the same dimensions
as the monitor modes of the associated monitors assigned to it, no
matter what scale is in use.
With logical mode, the dimension of a logical monitor is the dimension
of the monitor mode, divided by the logical monitor scale.
Possible @properties are:
* "supports-mirroring" (b): FALSE if mirroring not supported; TRUE or not
present if mirroring is supported.
* "layout-mode" (u): Represents in what way logical monitors are laid
out on the screen. The layout mode can be either
of the ones listed below. Absence of this property
means the layout mode cannot be changed, and that
"logical" mode is assumed to be used.
* 1 : logical - the dimension of a logical monitor is derived from
the monitor modes associated with it, then scaled
using the logical monitor scale.
* 2 : physical - the dimension of a logical monitor is derived from
the monitor modes associated with it.
* "supports-changing-layout-mode" (b): True if the layout mode can be
changed. Absence of this means the
layout mode cannot be changed.
* "global-scale-required" (b): True if all the logical monitors must
always use the same scale. Absence of
this means logical monitor scales can
differ.
* "legacy-ui-scaling-factor" (i): The legacy scaling factor traditionally
used to scale X11 clients (commonly
communicated via the
Gdk/WindowScalingFactor XSetting entry).
-->
<method name="GetCurrentState">
<arg name="serial" direction="out" type="u" />
<arg name="monitors" direction="out" type="a((ssss)a(siiddada{sv})a{sv})" />
<arg name="logical_monitors" direction="out" type="a(iiduba(ssss)a{sv})" />
<arg name="properties" direction="out" type="a{sv}" />
</method>
<!--
ApplyMonitorsConfig:
@serial: configuration serial
@method: configuration method
@logical_monitors: monitors configuration
@properties: properties
@method represents the way the configuration should be handled.
Possible methods:
0: verify
1: temporary
2: persistent
@logical_monitors consists of a list of logical monitor configurations.
Each logical monitor configuration consists of:
* i: layout x position
* i: layout y position
* d: scale
* u: transform (see GetCurrentState)
* b primary: true if this is the primary logical monitor
* a(ssa{sv}): a list of monitors, each consisting of:
* s: connector
* s: monitor mode ID
* a{sv}: monitor properties, including:
- "enable_underscanning" (b): enable monitor underscanning;
may only be set when underscanning
is supported (see GetCurrentState).
@properties may effect the global monitor configuration state. Possible
properties are:
* "layout-mode" (u): layout mode the passed configuration is in; may
only be set when changing the layout mode is
supported (see GetCurrentState).
-->
<method name="ApplyMonitorsConfig">
<arg name="serial" direction="in" type="u" />
<arg name="method" direction="in" type="u" />
<arg name="logical_monitors" direction="in" type="a(iiduba(ssa{sv}))" />
<arg name="properties" direction="in" type="a{sv}" />
</method>
</interface>
</node>

243
gnome/src/extension.js Normal file
View File

@ -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();
}

5
gnome/src/globals.js Normal file
View File

@ -0,0 +1,5 @@
const Globals = {
ipc_file: null, // Gio.File instance, file exists
extension_dir: null // string path
}
export default Globals;

49
gnome/src/ipc.js Normal file
View File

@ -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;
}

3
gnome/src/math.js Normal file
View File

@ -0,0 +1,3 @@
export function degreeToRadian(degree) {
return degree * Math.PI / 180;
}

10
gnome/src/metadata.json Normal file
View File

@ -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"
}

167
gnome/src/monitormanager.js Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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();
}
});
}
}

View File

@ -0,0 +1 @@
../../../ui/data/com.xronlinux.BreezyDesktop.gschema.xml

14
gnome/src/shader.js Normal file
View File

@ -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';
}

View File

@ -0,0 +1 @@
../../../modules/sombrero/calibrating.png

View File

@ -0,0 +1 @@
../../../vulkan/custom_banner.png

7
gnome/src/time.js Normal file
View File

@ -0,0 +1,7 @@
export function getEpochSec() {
return toSec(Date.now());
}
export function toSec(milliseconds) {
return Math.floor(milliseconds / 1000);
}

320
gnome/src/xrEffect.js Normal file
View File

@ -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);
}
});

1
modules/sombrero Submodule

@ -0,0 +1 @@
Subproject commit 84c2caa96271eb37b02f78c3be1fcb5d641dc247

@ -1 +1 @@
Subproject commit 91321744c438914154279abb01265d0541ea0972
Subproject commit f379c976e003823ed6a61fe2d054c802b3bca80d

28
ui/bin/package Executable file
View File

@ -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"

View File

@ -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);

View File

@ -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 "$@"

View File

@ -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" : "."
}
]
}
]
}

View File

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

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist gettext-domain="breezydesktop">
<schema id="com.xronlinux.BreezyDesktop" path="/com/xronlinux/BreezyDesktop/">
<key name="recenter-display-shortcut" type="as">
<default>
<![CDATA[['<Control><Super>space']]]>
</default>
<summary>Re-center display</summary>
<description>
Shortcut to re-center the virtual display.
</description>
</key>
<key name="toggle-display-distance-shortcut" type="as">
<default>
<![CDATA[['<Control><Super>Return']]]>
</default>
<summary>Trigger change to display distance</summary>
<description>
Shortcut to change the display distance.
</description>
</key>
<key name="toggle-follow-shortcut" type="as">
<default>
<![CDATA[['<Control><Super>0']]]>
</default>
<summary>Toggle follow mode</summary>
<description>
Shortcut to toggle follow mode.
</description>
</key>
<key name="display-distance" type="d">
<default>
1.05
</default>
<summary>Display distance</summary>
<description>
How far away the display appears. Farther will look smaller, closer will look larger.
</description>
</key>
<key name="toggle-display-distance-start" type="d">
<default>
0.85
</default>
<summary>Display distance start</summary>
<description>
Start distance when using the "change distance" shortcut.
</description>
</key>
<key name="toggle-display-distance-end" type="d">
<default>
1.05
</default>
<summary>Display distance end</summary>
<description>
End distance when using the "toggle display distance" shortcut.
</description>
</key>
<key name="developer-mode" type="b">
<default>
false
</default>
<summary>Developer mode</summary>
<description>
Enable developer mode
</description>
</key>
</schema>
</schemalist>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>com.xronlinux.BreezyDesktop.desktop</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-or-later</project_license>
<description>
<p>No description</p>
</description>
</component>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 611 KiB

View File

@ -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
)

36
ui/data/meson.build Normal file
View File

@ -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')

20
ui/meson.build Normal file
View File

@ -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,
)

@ -0,0 +1 @@
Subproject commit b2cdc8cc5fc593ab04015cec08a8e97b103ffa63

0
ui/po/LINGUAS Normal file
View File

6
ui/po/POTFILES Normal file
View File

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

1
ui/po/meson.build Normal file
View File

@ -0,0 +1 @@
i18n.gettext('breezydesktop', preset: 'glib')

14
ui/python3-pydbus.json Normal file
View File

@ -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"
}
]
}

0
ui/src/__init__.py Normal file
View File

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/com/xronlinux/BreezyDesktop">
<file preprocess="xml-stripblanks">gtk/connected-device.ui</file>
<file preprocess="xml-stripblanks">gtk/no-device.ui</file>
<file preprocess="xml-stripblanks">gtk/no-extension.ui</file>
<file preprocess="xml-stripblanks">gtk/shortcut-dialog.ui</file>
<file preprocess="xml-stripblanks">gtk/window.ui</file>
</gresource>
</gresources>

46
ui/src/breezydesktop.in Executable file
View File

@ -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 <https://www.gnu.org/licenses/>.
#
# 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))

116
ui/src/connecteddevice.py Normal file
View File

@ -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"<b>{name}</b>")
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)

View File

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

View File

@ -0,0 +1,201 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<template class="ConnectedDevice" parent="GtkBox">
<property name="orientation">1</property>
<property name="margin-top">20</property>
<property name="margin-bottom">20</property>
<property name="margin-start">20</property>
<property name="margin-end">20</property>
<property name="spacing">20</property>
<child>
<object class="GtkGrid">
<property name="column-spacing">4</property>
<child>
<object class="GtkLabel" id="device_label">
<property name="label">VITURE One</property>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">connected</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="true">Settings</property>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Effect enabled</property>
<property name="subtitle" translatable="true">Turn on or off the XR desktop effect</property>
<child>
<object class="GtkSwitch" id="effect_enable_switch">
<property name="valign">3</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Display distance</property>
<child>
<object class="GtkScale" id="display_distance_scale">
<property name="valign">3</property>
<property name="draw-value">true</property>
<property name="value-pos">0</property>
<property name="digits">2</property>
<property name="width-request">350</property>
<property name="has-origin">false</property>
<property name="adjustment">
<object class="GtkAdjustment" id="display_distance_adjustment">
<property name="lower">0.2</property>
<property name="upper">2.5</property>
<property name="step-increment">0.01</property>
<property name="value">1.05</property>
</object>
</property>
<marks>
<mark value="0.2" position="bottom"></mark>
<mark value="1.0" position="bottom"></mark>
<mark value="2.5" position="bottom"></mark>
</marks>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Follow enabled</property>
<property name="subtitle" translatable="true">Keep the virtual display near the center of your view</property>
<child>
<object class="GtkSwitch" id="follow_mode_switch">
<property name="valign">3</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="true">Shortcuts</property>
<property name="description" translatable="true">Modify keyboard shortcuts and how they work</property>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Re-center display shortcut</property>
<property name="subtitle" translatable="true">Pin the virtual display to the current position</property>
<property name="valign">2</property>
<child>
<object class="GtkBox">
<property name="spacing">30</property>
<property name="margin-start">30</property>
<child>
<object class="GtkShortcutLabel" id="recenter_display_shortcut_label">
<property name="valign">3</property>
<property name="accelerator"></property>
</object>
</child>
<child>
<object class="GtkButton" id="reassign_recenter_display_shortcut_button">
<style>
<class name="row-button"/>
</style>
<property name="name">recenter-display-shortcut</property>
<property name="valign">3</property>
<property name="label" translatable="true">Change</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Display distance shortcut</property>
<property name="subtitle" translatable="true">Quickly toggle between two predefined distances</property>
<property name="valign">2</property>
<child>
<object class="GtkBox">
<property name="spacing">30</property>
<property name="margin-start">30</property>
<child>
<object class="GtkShortcutLabel" id="toggle_display_distance_shortcut_label">
<property name="valign">3</property>
<property name="accelerator"></property>
</object>
</child>
<child>
<object class="GtkButton" id="reassign_toggle_display_distance_shortcut_button">
<style>
<class name="row-button"/>
</style>
<property name="name">toggle-display-distance-shortcut</property>
<property name="valign">3</property>
<property name="label" translatable="true">Change</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Display distance start and end</property>
<property name="subtitle" translatable="true">Use the buttons to capture the current display distance as start and end points.</property>
<property name="valign">2</property>
<child>
<object class="GtkBox">
<property name="spacing">30</property>
<property name="width-request">150</property>
<property name="margin-start">30</property>
<child>
<object class="GtkButton" id="set_toggle_display_distance_start_button">
<property name="name">toggle-display-distance-start</property>
<property name="valign">3</property>
</object>
</child>
<child>
<object class="GtkButton" id="set_toggle_display_distance_end_button">
<property name="name">toggle-display-distance-end</property>
<property name="valign">3</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="true">Toggle follow mode shortcut</property>
<property name="subtitle" translatable="true">Quickly toggle follow mode</property>
<property name="valign">2</property>
<child>
<object class="GtkBox">
<property name="spacing">30</property>
<property name="margin-start">30</property>
<child>
<object class="GtkShortcutLabel" id="toggle_follow_shortcut_label">
<property name="valign">3</property>
<property name="accelerator"></property>
</object>
</child>
<child>
<object class="GtkButton" id="reassign_toggle_follow_shortcut_button">
<style>
<class name="row-button"/>
</style>
<property name="name">toggle-follow-shortcut</property>
<property name="valign">3</property>
<property name="label" translatable="true">Change</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

19
ui/src/gtk/no-device.ui Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<template class="NoDevice" parent="GtkBox">
<property name="orientation">1</property>
<property name="margin-top">20</property>
<property name="margin-bottom">20</property>
<property name="margin-start">20</property>
<property name="margin-end">20</property>
<property name="spacing">20</property>
<child>
<object class="AdwStatusPage">
<property name="title" translatable="true">No device connected</property>
<property name="description" translatable="true">Breezy Desktop was unable to detect any supported XR devices.</property>
<property name="width-request">650</property>
</object>
</child>
</template>
</interface>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<template class="NoExtension" parent="GtkBox">
<property name="orientation">1</property>
<property name="margin-top">20</property>
<property name="margin-bottom">20</property>
<property name="margin-start">20</property>
<property name="margin-end">20</property>
<property name="spacing">20</property>
<child>
<object class="AdwStatusPage">
<property name="title" translatable="true">Breezy Desktop GNOME extension not ready</property>
<property name="description" translatable="true">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.</property>
<property name="width-request">650</property>
</object>
</child>
</template>
</interface>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<template class="ShortcutDialog" parent="GtkDialog">
<property name="modal">1</property>
<property name="default_width">440</property>
<property name="default_height">200</property>
<child internal-child="content_area">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<property name="margin-start">16</property>
<property name="margin-end">16</property>
<property name="margin-top">16</property>
<property name="margin-bottom">16</property>
<child>
<object class="GtkLabel">
<property name="vexpand">1</property>
<property name="label" translatable="yes">Press your keyboard shortcut or 'Backspace' to disable...</property>
</object>
</child>
</object>
</child>
<child type="titlebar">
<object class="GtkHeaderBar">
<property name="title-widget">
<object class="GtkLabel">
<property name="label" translatable="yes">Keyboard Shortcut</property>
<property name="single-line-mode">1</property>
<property name="ellipsize">end</property>
<property name="width-chars">5</property>
<style>
<class name="title"/>
</style>
</object>
</property>
</object>
</child>
<child>
<object class="GtkEventControllerKey" id="event_controller" />
</child>
</template>
</interface>

38
ui/src/gtk/window.ui Normal file
View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<template class="BreezydesktopWindow" parent="GtkApplicationWindow">
<property name="default-width">800</property>
<property name="titlebar">
<object class="GtkHeaderBar" id="header_bar">
<property name="title-widget">
<object class="GtkLabel">
<property name="label" translatable="yes">Breezy Desktop</property>
<property name="single-line-mode">1</property>
<property name="ellipsize">end</property>
<property name="width-chars">5</property>
<style>
<class name="title"/>
</style>
</object>
</property>
<child type="end">
<object class="GtkMenuButton">
<property name="primary">True</property>
<property name="icon-name">open-menu-symbolic</property>
<property name="tooltip-text" translatable="yes">Menu</property>
<property name="menu-model">primary_menu</property>
</object>
</child>
</object>
</property>
</template>
<menu id="primary_menu">
<section>
<item>
<attribute name="label" translatable="yes">_About BreezyDesktop</attribute>
<attribute name="action">app.about</attribute>
</item>
</section>
</menu>
</interface>

91
ui/src/main.py Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#
# 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, ['<primary>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)

43
ui/src/meson.build Normal file
View File

@ -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)

5
ui/src/nodevice.py Normal file
View File

@ -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"

5
ui/src/noextension.py Normal file
View File

@ -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"

15
ui/src/settingsmanager.py Normal file
View File

@ -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/")

109
ui/src/shortcutdialog.py Normal file
View File

@ -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)

74
ui/src/statemanager.py Normal file
View File

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

55
ui/src/window.py Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
#
# 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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 150 KiB

@ -1 +0,0 @@
Subproject commit 9edf687dc71f618d01a42038d7381e49370a01d7