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:
parent
02b9bbfb83
commit
e9b10838ba
|
|
@ -1,2 +1,6 @@
|
|||
/vulkan/build/
|
||||
/build/
|
||||
build/
|
||||
__pycache__
|
||||
*.zip
|
||||
gschemas.compiled
|
||||
.flatpak-builder/
|
||||
out/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../modules/sombrero/IMUAdjust.frag
|
||||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
const Globals = {
|
||||
ipc_file: null, // Gio.File instance, file exists
|
||||
extension_dir: null // string path
|
||||
}
|
||||
export default Globals;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export function degreeToRadian(degree) {
|
||||
return degree * Math.PI / 180;
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../../ui/data/com.xronlinux.BreezyDesktop.gschema.xml
|
||||
|
|
@ -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';
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../../modules/sombrero/calibrating.png
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../../vulkan/custom_banner.png
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export function getEpochSec() {
|
||||
return toSec(Date.now());
|
||||
}
|
||||
|
||||
export function toSec(milliseconds) {
|
||||
return Math.floor(milliseconds / 1000);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 84c2caa96271eb37b02f78c3be1fcb5d641dc247
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 91321744c438914154279abb01265d0541ea0972
|
||||
Subproject commit f379c976e003823ed6a61fe2d054c802b3bca80d
|
||||
|
|
@ -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"
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
@ -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 "$@"
|
||||
|
|
@ -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" : "."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 |
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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')
|
||||
|
|
@ -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,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
|
||||
|
|
@ -0,0 +1 @@
|
|||
i18n.gettext('breezydesktop', preset: 'glib')
|
||||
|
|
@ -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,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>
|
||||
|
|
@ -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))
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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/")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue