From dbd1b88e18f6d36ec30f6ac9bfab7d000aa48973 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Thu, 8 May 2025 11:43:17 -0700 Subject: [PATCH 01/41] Pull in latest backports --- gnome/backports/gnome-44-max | 2 +- gnome/backports/gnome-45 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gnome/backports/gnome-44-max b/gnome/backports/gnome-44-max index 79a5d63..0f4c9b3 160000 --- a/gnome/backports/gnome-44-max +++ b/gnome/backports/gnome-44-max @@ -1 +1 @@ -Subproject commit 79a5d639b2ac6c7013a6c3364ef0c2ff40d881bd +Subproject commit 0f4c9b3f5fb0aefdf864e1935515af94f7704a93 diff --git a/gnome/backports/gnome-45 b/gnome/backports/gnome-45 index 3aebbbb..558d755 160000 --- a/gnome/backports/gnome-45 +++ b/gnome/backports/gnome-45 @@ -1 +1 @@ -Subproject commit 3aebbbbf2cea422244d3687877bb99b17a8d8133 +Subproject commit 558d75545ac1f76eecfde639b92f49c5760b223e From 6fb672288c1836a806e6e5667eb2abd84e725933 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Thu, 15 May 2025 13:37:37 -0700 Subject: [PATCH 02/41] Pull in the latest driver, v2.1.7 --- VERSION | 2 +- modules/XRLinuxDriver | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index b6da512..9671f9a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.6 \ No newline at end of file +2.1.7 \ No newline at end of file diff --git a/modules/XRLinuxDriver b/modules/XRLinuxDriver index caca100..2d2e36d 160000 --- a/modules/XRLinuxDriver +++ b/modules/XRLinuxDriver @@ -1 +1 @@ -Subproject commit caca100752be8d5dec5e9d742bf5d2ae3e56b0fb +Subproject commit 2d2e36deb84f3d532ce2f7c2246ce240b7f99eae From 1995d9a71ec6bc593ba67c45f7454183b9981472 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:37:46 -0700 Subject: [PATCH 03/41] Update gstreamer-pipewire check so it doesn't require gst-inspect https://github.com/wheaney/breezy-desktop/issues/125 --- gnome/bin/setup | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/gnome/bin/setup b/gnome/bin/setup index db438da..57197e4 100755 --- a/gnome/bin/setup +++ b/gnome/bin/setup @@ -37,7 +37,19 @@ if ! { python3 -c "$PYTHON_GI_CHECK" 2>/dev/null || python -c "$PYTHON_GI_CHECK" fi fi -if ! gst-inspect-1.0 pipewiresrc &>/dev/null; then +gst_pipewire_found=0 +if command -v dpkg >/dev/null 2>&1; then + # Debian/Ubuntu + gst_pipewire_found=$(dpkg -l | grep -c "gstreamer1.0-pipewire") +elif command -v pacman >/dev/null 2>&1; then + # Arch Linux + gst_pipewire_found=$(pacman -Qq gst-plugin-pipewire 2>/dev/null | wc -l) +elif command -v rpm >/dev/null 2>&1; then + # RPM-based (Fedora, RHEL, openSUSE, etc.) + gst_pipewire_found=$((rpm -q gstreamer1-pipewire &>/dev/null && echo "found") | wc -l) +fi + +if [ "$gst_pipewire_found" -eq 0 ]; then if [ -z "$BREEZY_IGNORE_GST_ERRORS" ]; then printf "\033[1;31mERROR:\033[0m Pipewire GStreamer plugin not found\n" printf "Please install the required Pipewire GStreamer plugin:\n" From d9ae97a3b290c3346ec482683d6095f0869adb77 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:39:15 -0700 Subject: [PATCH 04/41] Fix accidentally reverted change in last commit --- gnome/bin/setup | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnome/bin/setup b/gnome/bin/setup index 57197e4..e61c132 100755 --- a/gnome/bin/setup +++ b/gnome/bin/setup @@ -46,7 +46,7 @@ elif command -v pacman >/dev/null 2>&1; then gst_pipewire_found=$(pacman -Qq gst-plugin-pipewire 2>/dev/null | wc -l) elif command -v rpm >/dev/null 2>&1; then # RPM-based (Fedora, RHEL, openSUSE, etc.) - gst_pipewire_found=$((rpm -q gstreamer1-pipewire &>/dev/null && echo "found") | wc -l) + gst_pipewire_found=$((rpm -q pipewire-gstreamer &>/dev/null && echo "found") | wc -l) fi if [ "$gst_pipewire_found" -eq 0 ]; then From aebbf2aaead71c8746e66a6e564fffc9c96a923d Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Sat, 12 Jul 2025 13:30:47 -0700 Subject: [PATCH 05/41] Pull in XR driver with VITURE Luma support, bump to v2.2.0 --- VERSION | 2 +- modules/XRLinuxDriver | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 9671f9a..e3a4f19 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.7 \ No newline at end of file +2.2.0 \ No newline at end of file diff --git a/modules/XRLinuxDriver b/modules/XRLinuxDriver index 2d2e36d..6875b49 160000 --- a/modules/XRLinuxDriver +++ b/modules/XRLinuxDriver @@ -1 +1 @@ -Subproject commit 2d2e36deb84f3d532ce2f7c2246ce240b7f99eae +Subproject commit 6875b49c6c9ebb89e028684daf85777401fd77d7 From dde072f042d9e69e508c0ccebb83f679ee0d2f46 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Wed, 16 Jul 2025 15:37:10 -0700 Subject: [PATCH 06/41] Pull in latest driver with VITURE Luma fix, bump to v2.2.1 --- VERSION | 2 +- modules/XRLinuxDriver | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index e3a4f19..fae692e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.0 \ No newline at end of file +2.2.1 \ No newline at end of file diff --git a/modules/XRLinuxDriver b/modules/XRLinuxDriver index 6875b49..985e8df 160000 --- a/modules/XRLinuxDriver +++ b/modules/XRLinuxDriver @@ -1 +1 @@ -Subproject commit 6875b49c6c9ebb89e028684daf85777401fd77d7 +Subproject commit 985e8df16fc3b39a979273770b5be68634986761 From 6465007e3740ccd2e6750b584424718f6fa7431e Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:10:41 -0700 Subject: [PATCH 07/41] Pull in latest driver with VITURE Luma pitch fixes --- modules/XRLinuxDriver | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/XRLinuxDriver b/modules/XRLinuxDriver index 985e8df..bfd3b59 160000 --- a/modules/XRLinuxDriver +++ b/modules/XRLinuxDriver @@ -1 +1 @@ -Subproject commit 985e8df16fc3b39a979273770b5be68634986761 +Subproject commit bfd3b5908d6a7a1c5c52878a544648f3d30fa15e From ecd75cb7990eff718c40641a960efee5fcdcf19c Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:11:48 -0700 Subject: [PATCH 08/41] Bump to v2.2.2 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index fae692e..7e541ae 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.1 \ No newline at end of file +2.2.2 \ No newline at end of file From 449fda2e9e70c7d5f4a6671df915f85646b12176 Mon Sep 17 00:00:00 2001 From: Wayne Heaney <42350981+wheaney@users.noreply.github.com> Date: Sat, 30 Aug 2025 21:25:03 -0700 Subject: [PATCH 09/41] Add KDE port (#129) Working KWIn plugin for SteamOS and non-Debian distros (x86_64 only for now) --- .gitignore | 1 + bin/breezy_gnome_setup | 2 +- bin/breezy_kwin_setup | 79 +++ bin/breezy_vulkan_setup | 7 +- bin/package_kwin | 82 ++++ gnome/src/math.js | 4 +- kwin/CMakeLists.txt | 55 +++ kwin/bin/breezy_kwin_uninstall | 56 +++ kwin/bin/package_kwin_plugin | 23 + kwin/bin/setup | 96 ++++ kwin/cmake/default-vars.cmake | 15 + kwin/cmake/info.cmake | 24 + kwin/cmake/pack.cmake | 18 + kwin/cmake/qtversion.cmake | 7 + kwin/cmake/test.cmake | 2 + kwin/docker-build/Dockerfile | 36 ++ kwin/docker-build/Dockerfile.steamos | 37 ++ kwin/docker-build/init.sh | 23 + kwin/docker-build/run-build.sh | 28 ++ kwin/src/CMakeLists.txt | 61 +++ kwin/src/breezydesktopconfig.kcfg | 33 ++ kwin/src/breezydesktopconfig.kcfgc | 5 + kwin/src/breezydesktopeffect.cpp | 533 +++++++++++++++++++++ kwin/src/breezydesktopeffect.h | 124 +++++ kwin/src/kcm/CMakeLists.txt | 18 + kwin/src/kcm/breezydesktopeffectkcm.cpp | 151 ++++++ kwin/src/kcm/breezydesktopeffectkcm.h | 42 ++ kwin/src/kcm/breezydesktopeffectkcm.ui | 159 ++++++ kwin/src/kcm/labeledslider.cpp | 1 + kwin/src/kcm/labeledslider.h | 157 ++++++ kwin/src/kcm/shortcuts.h | 31 ++ kwin/src/main.cpp | 10 + kwin/src/metadata.json | 17 + kwin/src/qml/BreezyDesktop.qml | 147 ++++++ kwin/src/qml/BreezyDesktopDisplay.qml | 26 + kwin/src/qml/CameraController.qml | 84 ++++ kwin/src/qml/DesktopView.qml | 57 +++ kwin/src/qml/Displays.qml | 400 ++++++++++++++++ kwin/src/qml/main.qml | 122 +++++ kwin/src/xrdriveripc/CMakeLists.txt | 26 + kwin/src/xrdriveripc/xrdriveripc.cpp | 142 ++++++ kwin/src/xrdriveripc/xrdriveripc.h | 111 +++++ kwin/src/xrdriveripc/xrdriveripc_runner.py | 63 +++ 43 files changed, 3109 insertions(+), 6 deletions(-) create mode 100755 bin/breezy_kwin_setup create mode 100755 bin/package_kwin create mode 100644 kwin/CMakeLists.txt create mode 100755 kwin/bin/breezy_kwin_uninstall create mode 100755 kwin/bin/package_kwin_plugin create mode 100755 kwin/bin/setup create mode 100644 kwin/cmake/default-vars.cmake create mode 100644 kwin/cmake/info.cmake create mode 100644 kwin/cmake/pack.cmake create mode 100644 kwin/cmake/qtversion.cmake create mode 100644 kwin/cmake/test.cmake create mode 100644 kwin/docker-build/Dockerfile create mode 100644 kwin/docker-build/Dockerfile.steamos create mode 100755 kwin/docker-build/init.sh create mode 100755 kwin/docker-build/run-build.sh create mode 100644 kwin/src/CMakeLists.txt create mode 100644 kwin/src/breezydesktopconfig.kcfg create mode 100644 kwin/src/breezydesktopconfig.kcfgc create mode 100644 kwin/src/breezydesktopeffect.cpp create mode 100644 kwin/src/breezydesktopeffect.h create mode 100644 kwin/src/kcm/CMakeLists.txt create mode 100644 kwin/src/kcm/breezydesktopeffectkcm.cpp create mode 100644 kwin/src/kcm/breezydesktopeffectkcm.h create mode 100644 kwin/src/kcm/breezydesktopeffectkcm.ui create mode 100644 kwin/src/kcm/labeledslider.cpp create mode 100644 kwin/src/kcm/labeledslider.h create mode 100644 kwin/src/kcm/shortcuts.h create mode 100644 kwin/src/main.cpp create mode 100644 kwin/src/metadata.json create mode 100644 kwin/src/qml/BreezyDesktop.qml create mode 100644 kwin/src/qml/BreezyDesktopDisplay.qml create mode 100644 kwin/src/qml/CameraController.qml create mode 100644 kwin/src/qml/DesktopView.qml create mode 100644 kwin/src/qml/Displays.qml create mode 100644 kwin/src/qml/main.qml create mode 100644 kwin/src/xrdriveripc/CMakeLists.txt create mode 100644 kwin/src/xrdriveripc/xrdriveripc.cpp create mode 100644 kwin/src/xrdriveripc/xrdriveripc.h create mode 100644 kwin/src/xrdriveripc/xrdriveripc_runner.py diff --git a/.gitignore b/.gitignore index 51c61dc..ae97f4c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __pycache__ gschemas.compiled out/ *.po~ +kwin/src/xrdriveripc/xrdriveripc.py diff --git a/bin/breezy_gnome_setup b/bin/breezy_gnome_setup index f93c496..274bad5 100755 --- a/bin/breezy_gnome_setup +++ b/bin/breezy_gnome_setup @@ -59,7 +59,7 @@ if [ -z "$binary_path_arg" ] then # download and unzip the binary echo "Downloading to: ${tmp_dir}/$FILE_NAME" - curl -L -O $binary_download_url + curl -L "$binary_download_url" > "$FILE_NAME" else FILE_NAME=$(basename $binary_path_arg) if [[ "$binary_path_arg" = /* ]]; then diff --git a/bin/breezy_kwin_setup b/bin/breezy_kwin_setup new file mode 100755 index 0000000..ac35d7e --- /dev/null +++ b/bin/breezy_kwin_setup @@ -0,0 +1,79 @@ +#!/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 + +check_command() { + if ! command -v "$1" &>/dev/null; then + echo "Please install \"$1\" and make sure it's available in your \$PATH, then rerun the setup." + exit 1 + fi +} + +check_command "curl" + +ARCH=$(uname -m) +if [ -f /etc/os-release ]; then + . /etc/os-release + if [ "$ID" == "steamos" ]; then + ARCH="steamos" + fi +fi +FILE_NAME="breezyKWin-$ARCH.tar.gz" +LATEST_RELEASE=$(curl -s "https://api.github.com/repos/wheaney/breezy-desktop/releases/latest") +echo "Performing setup for KWin ($ARCH)" + +start_dir=$(pwd) + +# create temp directory +tmp_dir=$(mktemp -d -t breezy-kwin-XXXXXXXXXX) +pushd $tmp_dir > /dev/null +echo "Created temp directory: ${tmp_dir}" + +binary_download_url="https://github.com/wheaney/breezy-desktop/releases/latest/download/$FILE_NAME" +if [ "$1" = "-v" ] +then + metrics_version="$2" + binary_path_arg="$3" +elif [ "$1" = "--tag" ] && [ -n "$2" ] +then + binary_download_url="https://github.com/wheaney/breezy-desktop/releases/download/$2/$FILE_NAME" +else + binary_path_arg="$1" +fi + +if [ -z "$binary_path_arg" ] +then + # download and unzip the binary + echo "Downloading to: ${tmp_dir}/$FILE_NAME" + curl -L "$binary_download_url" > "$FILE_NAME" +else + FILE_NAME=$(basename $binary_path_arg) + 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_kwin" +tar -xf $FILE_NAME + +pushd breezy_kwin > /dev/null + +# run the setup script that comes with this release +bin/setup $metrics_version + +echo "Deleting temp directory: ${tmp_dir}" +rm -rf $tmp_dir +cd "$(dirs -l -0)" && dirs -c diff --git a/bin/breezy_vulkan_setup b/bin/breezy_vulkan_setup index 464e785..bac0377 100755 --- a/bin/breezy_vulkan_setup +++ b/bin/breezy_vulkan_setup @@ -39,10 +39,11 @@ fi if [ -z "$binary_path_arg" ] then - # download and unzip the latest driver - echo "Downloading to: ${tmp_dir}/breezyVulkan-$ARCH.tar.gz" - curl -L -O $binary_download_url + # download and unzip the binary binary_path_arg="breezyVulkan-$ARCH.tar.gz" + echo "Downloading to: ${tmp_dir}/$binary_path_arg" + + curl -L "$binary_download_url" > "$binary_path_arg" else if [[ "$binary_path_arg" = /* ]]; then abs_path="$binary_path_arg" diff --git a/bin/package_kwin b/bin/package_kwin new file mode 100755 index 0000000..b30f076 --- /dev/null +++ b/bin/package_kwin @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +# exit when any command fails +set -e + +ARCH=${ARCH:-$(uname -m)} +BUILD_ARCH=$ARCH +if [ -n "${STEAMOS+x}" ]; then + ARCH="x86_64" + BUILD_ARCH="steamos" +fi + +# https://stackoverflow.com/a/246128 +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) +ROOT_DIR=$(realpath $SCRIPT_DIR/..) +XR_DRIVER_DIR=$ROOT_DIR/modules/XRLinuxDriver + +echo "Building Breezy KWin for $BUILD_ARCH" +KWIN_DIR=$ROOT_DIR/kwin +BUILD_FILE_NAME=breezyKWin-$BUILD_ARCH.tar.gz + +TMP_DIR=$(mktemp -d -t breezy-kwin-XXXXXXXXXX) +PACKAGE_DIR=$TMP_DIR/breezy_kwin +mkdir -p $PACKAGE_DIR + +source $XR_DRIVER_DIR/bin/inject_ua + +# copy vulkan setup scripts and configs +mkdir -p $PACKAGE_DIR/bin +copy_and_inject_ua "$XR_DRIVER_DIR/bin/ua.sh" "$PACKAGE_DIR/bin" "$KWIN_DIR/bin/setup" "$KWIN_DIR/bin/breezy_kwin_uninstall" + +XR_DRIVER_BINARY=$XR_DRIVER_DIR/out/xrDriver-$ARCH.tar.gz + +if [ ! -e "$XR_DRIVER_BINARY" ] || [ "$1" == "--rebuild-driver" ] || [ "$1" == "--rebuild-all" ]; 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 $XR_DRIVER_DIR + fi + + pushd $XR_DRIVER_DIR + + # strange issue where the base library produces a .so file if the build is not cleaned + rm -rf build/ + + docker-build/init.sh + docker-build/run-build.sh $ARCH + popd +fi + +XR_DRIVER_TMP_DIR=$(mktemp -d -t xr-driver-XXXXXXXXXX) +pushd $XR_DRIVER_TMP_DIR +cp $XR_DRIVER_BINARY $XR_DRIVER_TMP_DIR/xrDriver.tar.gz +tar -xf $XR_DRIVER_TMP_DIR/xrDriver.tar.gz + +XR_DRIVER_MANIFEST_LINE=$(sha256sum xr_driver/manifest) +popd +rm -rf $XR_DRIVER_TMP_DIR + +cp $XR_DRIVER_BINARY $PACKAGE_DIR/xrDriver.tar.gz +cp $XR_DRIVER_DIR/bin/xr_driver_setup $PACKAGE_DIR/bin + +# alternative to symlinking, since the Docker build can't resolve to the parent directory +# this file is in .gitignore so it doesn't get duplicated +cp ui/modules/PyXRLinuxDriverIPC/xrdriveripc.py $KWIN_DIR/src/xrdriveripc/xrdriveripc.py + +pushd $KWIN_DIR +docker-build/init.sh +docker-build/run-build.sh $BUILD_ARCH +popd +cp $KWIN_DIR/out/breezyKWinPlugin-$BUILD_ARCH.tar.gz $PACKAGE_DIR/breezyKWinPlugin.tar.gz + +pushd $TMP_DIR +tar -zcvf $BUILD_FILE_NAME breezy_kwin +popd + +mkdir -p out +if [ -e "out/$BUILD_FILE_NAME" ]; then + rm out/$BUILD_FILE_NAME +fi +cp $TMP_DIR/$BUILD_FILE_NAME out + +rm -rf $TMP_DIR \ No newline at end of file diff --git a/gnome/src/math.js b/gnome/src/math.js index 0eebd6c..2a1c2eb 100644 --- a/gnome/src/math.js +++ b/gnome/src/math.js @@ -14,8 +14,8 @@ export function diagonalToCrossFOVs(diagonalFOVRadians, aspectRatio) { // then convert back to spherical FOV return { diagonal: diagonalFOVRadians, - horizontal: 2 * Math.atan(Math.tan(flatHorizontalFOV / 2)), - vertical: 2 * Math.atan(Math.tan(flatVerticalFOV / 2)) + horizontal: 2 * Math.atan(flatHorizontalFOV / 2), + vertical: 2 * Math.atan(flatVerticalFOV / 2) } } diff --git a/kwin/CMakeLists.txt b/kwin/CMakeLists.txt new file mode 100644 index 0000000..c3e8411 --- /dev/null +++ b/kwin/CMakeLists.txt @@ -0,0 +1,55 @@ +cmake_minimum_required(VERSION 3.20) + +project(breezy_desktop VERSION 0.0.1 LANGUAGES CXX) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +include(cmake/default-vars.cmake) + +find_package(ECM "5.100" REQUIRED NO_MODULE) +set(CMAKE_MODULE_PATH + ${CMAKE_MODULE_PATH} + ${ECM_MODULE_PATH} + ${ECM_KDE_MODULE_DIR} +) +include(cmake/qtversion.cmake) +include(FeatureSummary) +include(KDEInstallDirs) +include(KDECMakeSettings) +include(KDECompilerSettings NO_POLICY_SCOPE) + +include(cmake/qtversion.cmake) + +# required frameworks by Core +find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} REQUIRED COMPONENTS + Config + ConfigWidgets + CoreAddons + GlobalAccel + I18n + KCMUtils + WindowSystem + XmlGui +) + +if(${QT_MAJOR_VERSION} EQUAL 6) + find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} REQUIRED COMPONENTS KCMUtils) + find_package(KWin REQUIRED COMPONENTS kwineffects) + message(STATUS "Found KWin Version: ${KWin_VERSION}") +endif () +include(cmake/info.cmake) +find_package(epoxy REQUIRED) +find_package(XCB REQUIRED COMPONENTS XCB) +find_package(KWinDBusInterface CONFIG REQUIRED) + +add_subdirectory(src) +ki18n_install(po) + +feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) + +include(cmake/test.cmake) +include(cmake/pack.cmake) diff --git a/kwin/bin/breezy_kwin_uninstall b/kwin/bin/breezy_kwin_uninstall new file mode 100755 index 0000000..b14d6de --- /dev/null +++ b/kwin/bin/breezy_kwin_uninstall @@ -0,0 +1,56 @@ +#!/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 + +USER_HOME=$(realpath ~) +if [ "$for_install" -eq 0 ]; then + UA_EVENT_NAME="breezy_kwin_uninstall" + UA_CLIENT_ID="BreezyKWin" + #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 + +if [ -z "$LIB_HOME" ]; then + LIB_HOME="$USER_HOME/.local/lib" +fi + +EFFECT_ID="breezy_desktop" +EFFECT_DIR="$XDG_DATA_HOME/kwin/effects/$EFFECT_ID" +PLUGIN_SO="$LIB_HOME/qt6/plugins/kwin/effects/plugins/${EFFECT_ID}.so" +CONFIG_SO="$LIB_HOME/qt6/plugins/kwin/effects/configs/${EFFECT_ID}_config.so" + +if [[ -d "$EFFECT_DIR" ]]; then + [ "$for_install" -eq 0 ] && echo "Removing $EFFECT_DIR and its contents" + $SUDO rm -rf "$EFFECT_DIR" +fi + +if [[ -f "$PLUGIN_SO" ]]; then + [ "$for_install" -eq 0 ] && echo "Removing $PLUGIN_SO" + $SUDO rm -f "$PLUGIN_SO" +fi + +if [[ -f "$CONFIG_SO" ]]; then + [ "$for_install" -eq 0 ] && echo "Removing $CONFIG_SO" + $SUDO rm -f "$CONFIG_SO" +fi + +if [[ -e "$XDG_BIN_HOME/xr_driver_uninstall" && "$for_install" -eq 0 ]]; then + echo "Uninstalling XRLinuxDriver" + sudo "$XDG_BIN_HOME/xr_driver_uninstall" +fi + +# this script is self-deleting, leave this as the last command +rm -f $XDG_BIN_HOME/breezy_kwin_uninstall \ No newline at end of file diff --git a/kwin/bin/package_kwin_plugin b/kwin/bin/package_kwin_plugin new file mode 100755 index 0000000..a5f4346 --- /dev/null +++ b/kwin/bin/package_kwin_plugin @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +# exit when any command fails +set -e + +ARCH=${ARCH:-$(uname -m)} +if [ -n "${STEAMOS+x}" ]; then + ARCH="steamos" +fi +echo "Building Breezy KWin plugin for $ARCH" + +BUILD_PATH=build +rm -rf $BUILD_PATH +mkdir $BUILD_PATH + +pushd $BUILD_PATH +cmake .. +make +cpack -G TGZ +popd + +mkdir -p out +cp $BUILD_PATH/breezy_desktop.tar.gz out/breezyKWinPlugin-$ARCH.tar.gz \ No newline at end of file diff --git a/kwin/bin/setup b/kwin/bin/setup new file mode 100755 index 0000000..23a532d --- /dev/null +++ b/kwin/bin/setup @@ -0,0 +1,96 @@ +#!/usr/bin/env bash + +set -e + +if [ "$XDG_SESSION_TYPE" != "wayland" ]; then + printf "\033[1;33mWARNING:\033[0m Windowing system is %s\n" "$XDG_SESSION_TYPE" + printf "\033[1;33mWARNING:\033[0m Virtual display functionality requires Wayland\n" +fi + +USER_HOME=$(realpath ~) + +if [ -z "$XDG_BIN_HOME" ]; then + XDG_BIN_HOME="$USER_HOME/.local/bin" +fi + +if [ -d "$XDG_BIN_HOME" ]; then + # check ownership and permissions before doing chown and chmod + XDG_BIN_USER=$(stat -c %U $XDG_BIN_HOME) + XDG_BIN_GROUP=$(stat -c %G $XDG_BIN_HOME) + + USER=$(whoami) + GROUP=$(id -gn) + + if [ "$XDG_BIN_USER" != "$USER" ] || [ "$XDG_BIN_GROUP" != "$GROUP" ]; then + echo "Fixing ownership and permissions of $XDG_BIN_HOME" + sudo chown -R $USER:$GROUP $XDG_BIN_HOME + sudo chmod -R 700 $XDG_BIN_HOME + fi +fi + +UA_EVENT_NAME="breezy_kwin_install" +if [ -e "$XDG_BIN_HOME/breezy_kwin_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_kwin_uninstall --for-install || true + + UA_EVENT_NAME="breezy_kwin_update" +fi + +UA_CLIENT_ID="BreezyKWin" +UA_EVENT_VERSION="$1" +#INJECT_UA_CALL + +tar -xf $(pwd)/breezyKWinPlugin.tar.gz +pushd breezy_desktop/usr > /dev/null + +echo "Copying KWin plugin files to $USER_HOME/.local/{lib,share}" + +# locate the lib path that ends with qt6/plugins (handles multiarch dirs) +QT_PLUGIN_DIR_RELATIVE=$(find lib -type d -path '*/qt6/plugins' -print -quit 2>/dev/null || true) +if [ -z "$QT_PLUGIN_DIR_RELATIVE" ]; then + QT_PLUGIN_DIR_RELATIVE="lib/qt6/plugins" +fi + +# directory structure matches XDG, so just recursive copy +chmod -R 755 . +cp -r . "$USER_HOME/.local/" + +popd > /dev/null + +mkdir -p $XDG_BIN_HOME +cp bin/breezy_kwin_uninstall $XDG_BIN_HOME + +# Install QT_PLUGIN_PATH snippet into ~/.bash_profile if not present +BASH_PROFILE="$HOME/.bash_profile" +QT_PLUGIN_DIR="$HOME/.local/$QT_PLUGIN_DIR_RELATIVE" +QT_PLUGIN_EXPORT="export QT_PLUGIN_PATH=\"$QT_PLUGIN_DIR\"" +if [[ ! -f "$BASH_PROFILE" ]] || ! grep -Fq "$QT_PLUGIN_EXPORT" "$BASH_PROFILE" 2>/dev/null; then + echo "Adding QT_PLUGIN_PATH to $BASH_PROFILE" + mkdir -p "$(dirname "$BASH_PROFILE")" + cat >> "$BASH_PROFILE" < files + +if [[ "$1" == "--init" || ! $(docker buildx inspect breezykwinbuilder &>/dev/null; echo $?) -eq 0 ]]; then + # start fresh + echo "Creating new docker builder instance" + docker buildx rm breezykwinbuilder 2>/dev/null || true + docker buildx create --use --name breezykwinbuilder --driver docker-container --driver-opt image=moby/buildkit:latest +else + echo "Using existing docker builder instance" + docker buildx use breezykwinbuilder +fi + +echo "Building docker image" +docker buildx build --platform linux/amd64 -f ./docker-build/Dockerfile -t "breezy-kwin:amd64" --load . +# docker buildx build --platform linux/arm64 -f ./docker-build/Dockerfile -t "breezy-kwin:arm64" --load . +docker buildx build --platform linux/amd64 -f ./docker-build/Dockerfile.steamos -t "breezy-kwin-steamos:amd64" --load . \ No newline at end of file diff --git a/kwin/docker-build/run-build.sh b/kwin/docker-build/run-build.sh new file mode 100755 index 0000000..ff19c84 --- /dev/null +++ b/kwin/docker-build/run-build.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +set -e + +USER=${SUDO_USER:-$USER} +GROUP=$(id -gn $USER) + +# Run containers for each architecture +if [[ "$1" == "x86_64" || -z "$1" ]]; then + sudo rm -rf build/ + docker run --rm -t -v ./:/source --platform linux/amd64 "breezy-kwin:amd64" + sudo chown -R $USER:$GROUP out/ +fi + +if [[ "$1" == "aarch64" || -z "$1" ]]; then + sudo rm -rf build/ + docker run --rm -t -v ./:/source --platform linux/arm64 "breezy-kwin:arm64" + sudo chown -R $USER:$GROUP out/ +fi + +if [[ "$1" == "steamos" || -z "$1" ]]; then + sudo rm -rf build/ + docker run --rm -t -v ./:/source --platform linux/amd64 "breezy-kwin-steamos:amd64" + sudo chown -R $USER:$GROUP out/ +fi + +# build directory structure is all owned by root because of docker, delete it all now +sudo chown -R $USER:$GROUP build/ \ No newline at end of file diff --git a/kwin/src/CMakeLists.txt b/kwin/src/CMakeLists.txt new file mode 100644 index 0000000..619ee0b --- /dev/null +++ b/kwin/src/CMakeLists.txt @@ -0,0 +1,61 @@ +add_subdirectory(xrdriveripc) +add_subdirectory(kcm) + +kcoreaddons_add_plugin(breezy_desktop INSTALL_NAMESPACE "kwin/effects/plugins/") +target_sources(breezy_desktop PRIVATE + breezydesktopeffect.cpp + main.cpp +) +kconfig_add_kcfg_files(breezy_desktop breezydesktopconfig.kcfgc) + +# Split KWin version into numeric components (major, minor, patch) +string(REGEX MATCHALL "[0-9]+" KWIN_VERSION_COMPONENTS "${KWin_VERSION}") + +# defaults +set(KWIN_VERSION_MAJOR 0) +set(KWIN_VERSION_MINOR 0) +set(KWIN_VERSION_PATCH 0) + +list(LENGTH KWIN_VERSION_COMPONENTS _kwin_version_len) +if(_kwin_version_len GREATER 0) + list(GET KWIN_VERSION_COMPONENTS 0 KWIN_VERSION_MAJOR) +endif() +if(_kwin_version_len GREATER 1) + list(GET KWIN_VERSION_COMPONENTS 1 KWIN_VERSION_MINOR) +endif() +if(_kwin_version_len GREATER 2) + list(GET KWIN_VERSION_COMPONENTS 2 KWIN_VERSION_PATCH) +endif() + +# optional: a single encoded integer (major*10000 + minor*100 + patch) +math(EXPR KWIN_VERSION_ENCODED "${KWIN_VERSION_MAJOR} * 10000 + ${KWIN_VERSION_MINOR} * 100 + ${KWIN_VERSION_PATCH}") + +# Export as compile definitions. Keep the original string macro as well. +target_compile_definitions(breezy_desktop PRIVATE + KWIN_VERSION_STR=\"${KWin_VERSION}\" + KWIN_VERSION_MAJOR=${KWIN_VERSION_MAJOR} + KWIN_VERSION_MINOR=${KWIN_VERSION_MINOR} + KWIN_VERSION_PATCH=${KWIN_VERSION_PATCH} + KWIN_VERSION_ENCODED=${KWIN_VERSION_ENCODED} +) +target_include_directories(breezy_desktop PRIVATE /usr/include/kwin) +target_include_directories(breezy_desktop PRIVATE xrdriveripc) +target_link_libraries(breezy_desktop + Qt6::Core + Qt6::Gui + Qt6::Quick + + KF6::ConfigCore + KF6::ConfigGui + KF6::CoreAddons + KF6::GlobalAccel + KF6::I18n + KF6::WindowSystem + + KWin::kwin + + xr_driver_ipc +) + + +install(DIRECTORY qml DESTINATION ${KDE_INSTALL_DATADIR}/kwin/effects/breezy_desktop) \ No newline at end of file diff --git a/kwin/src/breezydesktopconfig.kcfg b/kwin/src/breezydesktopconfig.kcfg new file mode 100644 index 0000000..8d0824d --- /dev/null +++ b/kwin/src/breezydesktopconfig.kcfg @@ -0,0 +1,33 @@ + + + + + + 85 + 25 + 225 + + + + 105 + 25 + 225 + + + + false + + Enable zooming in on the focused display. + + + 0 + 0 + 100 + + How far apart the displays are visually (not logically) + + + diff --git a/kwin/src/breezydesktopconfig.kcfgc b/kwin/src/breezydesktopconfig.kcfgc new file mode 100644 index 0000000..f1666fe --- /dev/null +++ b/kwin/src/breezydesktopconfig.kcfgc @@ -0,0 +1,5 @@ +File=breezydesktopconfig.kcfg +ClassName=BreezyDesktopConfig +Singleton=true +Mutators=true +Notifiers=true \ No newline at end of file diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp new file mode 100644 index 0000000..6288baa --- /dev/null +++ b/kwin/src/breezydesktopeffect.cpp @@ -0,0 +1,533 @@ + +#include "core/rendertarget.h" +#include "core/renderviewport.h" +#include "kcm/shortcuts.h" +#include "breezydesktopeffect.h" +#include "breezydesktopconfig.h" +#include "effect/effect.h" +#include "effect/effecthandler.h" +#include "opengl/glutils.h" +#include "xrdriveripc.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +Q_LOGGING_CATEGORY(KWIN_XR, "kwin.xr") + +namespace DataView +{ + const QString SHM_DIR = QStringLiteral("/dev/shm"); + const QString SHM_PATH = SHM_DIR + QStringLiteral("/breezy_desktop_imu"); + + // Helper constants and functions for shared memory buffer offsets + constexpr int UINT8_SIZE = sizeof(uint8_t); + constexpr int BOOL_SIZE = UINT8_SIZE; + constexpr int UINT_SIZE = sizeof(uint32_t); + constexpr int FLOAT_SIZE = sizeof(float); + + // DataView info: [offset, size, count] + constexpr int OFFSET_INDEX = 0; + constexpr int SIZE_INDEX = 1; + constexpr int COUNT_INDEX = 2; + + // Computes the end offset, exclusive + constexpr int dataViewEnd(const int info[3]) { + return info[OFFSET_INDEX] + info[SIZE_INDEX] * info[COUNT_INDEX]; + } + + constexpr int VERSION[3] = {0, UINT8_SIZE, 1}; + constexpr int ENABLED[3] = {dataViewEnd(VERSION), BOOL_SIZE, 1}; + constexpr int LOOK_AHEAD_CFG[3] = {dataViewEnd(ENABLED), FLOAT_SIZE, 4}; + constexpr int DISPLAY_RES[3] = {dataViewEnd(LOOK_AHEAD_CFG), UINT_SIZE, 2}; + constexpr int DISPLAY_FOV[3] = {dataViewEnd(DISPLAY_RES), FLOAT_SIZE, 1}; + constexpr int LENS_DISTANCE_RATIO[3] = {dataViewEnd(DISPLAY_FOV), FLOAT_SIZE, 1}; + constexpr int SBS_ENABLED[3] = {dataViewEnd(LENS_DISTANCE_RATIO), BOOL_SIZE, 1}; + constexpr int CUSTOM_BANNER_ENABLED[3] = {dataViewEnd(SBS_ENABLED), BOOL_SIZE, 1}; + constexpr int SMOOTH_FOLLOW_ENABLED[3] = {dataViewEnd(CUSTOM_BANNER_ENABLED), BOOL_SIZE, 1}; + constexpr int SMOOTH_FOLLOW_ORIGIN_DATA[3] = {dataViewEnd(SMOOTH_FOLLOW_ENABLED), FLOAT_SIZE, 16}; + constexpr int IMU_DATE_MS[3] = {dataViewEnd(SMOOTH_FOLLOW_ORIGIN_DATA), UINT_SIZE, 2}; + constexpr int IMU_QUAT_ENTRIES = 4; + constexpr int IMU_QUAT_DATA[3] = {dataViewEnd(IMU_DATE_MS), FLOAT_SIZE, 4 * IMU_QUAT_ENTRIES}; + constexpr int IMU_PARITY_BYTE[3] = {dataViewEnd(IMU_QUAT_DATA), UINT8_SIZE, 1}; + constexpr int LENGTH = dataViewEnd(IMU_PARITY_BYTE); +} + +namespace KWin +{ + +BreezyDesktopEffect::BreezyDesktopEffect() + : m_shutdownTimer(new QTimer(this)) +{ + qCCritical(KWIN_XR) << "\t\t\tBreezy - constructor"; + qmlRegisterUncreatableType("org.kde.kwin.effect.breezy_desktop", 1, 0, "BreezyDesktopEffect", QStringLiteral("BreezyDesktop cannot be created in QML")); + + m_shutdownTimer->setSingleShot(true); + connect(m_shutdownTimer, &QTimer::timeout, this, &BreezyDesktopEffect::realDeactivate); + + setupGlobalShortcut( + BreezyShortcuts::TOGGLE, + [this]() { this->toggle(); } + ); + setupGlobalShortcut( + BreezyShortcuts::RECENTER, + [this]() { this->recenter(); } + ); + setupGlobalShortcut( + BreezyShortcuts::TOGGLE_ZOOM_ON_FOCUS, + [this]() { + this->setZoomOnFocusEnabled(!m_zoomOnFocusEnabled); + } + ); + + connect(effects, &EffectsHandler::cursorShapeChanged, this, &BreezyDesktopEffect::updateCursorImage); + updateCursorImage(); + reconfigure(ReconfigureAll); + + setSource(QUrl::fromLocalFile(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kwin/effects/breezy_desktop/qml/main.qml")))); + + // Monitor the IMU file for changes, even if it doesn't exist at startup + m_shmDirectoryWatcher = new QFileSystemWatcher(this); + m_shmDirectoryWatcher->addPath(DataView::SHM_DIR); + + m_shmFileWatcher = new QFileSystemWatcher(this); + + // Setup file watcher with recreation detection + auto setupFileWatcher = [this]() { + if (QFile::exists(DataView::SHM_PATH) && ( + m_imuTimestamp == 0 || + QDateTime::currentMSecsSinceEpoch() - m_imuTimestamp > 50 || // file may have been deleted and recreated + !m_shmFileWatcher->files().contains(DataView::SHM_PATH) + )) { + m_shmFileWatcher->removePath(DataView::SHM_PATH); + disconnect(m_shmFileWatcher, &QFileSystemWatcher::fileChanged, this, &BreezyDesktopEffect::updateImuRotation); + m_shmFileWatcher->addPath(DataView::SHM_PATH); + connect(m_shmFileWatcher, &QFileSystemWatcher::fileChanged, this, &BreezyDesktopEffect::updateImuRotation); + } + }; + + // Handle directory changes (file creation/recreation) + connect(m_shmDirectoryWatcher, &QFileSystemWatcher::directoryChanged, this, setupFileWatcher); + + // Initial setup + setupFileWatcher(); + + m_cursorUpdateTimer = new QTimer(this); + connect(m_cursorUpdateTimer, &QTimer::timeout, this, &BreezyDesktopEffect::updateCursorPos); + m_cursorUpdateTimer->setInterval(16); // ~60Hz + m_cursorUpdateTimer->start(); + + enableDriver(); +} + +void BreezyDesktopEffect::setupGlobalShortcut(const BreezyShortcuts::Shortcut &shortcut, std::function triggeredFunc) { + QAction *action = new QAction(this); + action->setObjectName(shortcut.actionName); + action->setText(shortcut.actionText); + KGlobalAccel::self()->setDefaultShortcut(action, {shortcut.shortcut}); + KGlobalAccel::self()->setShortcut(action, {shortcut.shortcut}); + connect(action, &QAction::triggered, this, triggeredFunc); +} + +void BreezyDesktopEffect::recenter() { + XRDriverIPC::instance().writeControlFlags({ + {"recenter_screen", true} + }); +} + +void BreezyDesktopEffect::reconfigure(ReconfigureFlags) +{ + BreezyDesktopConfig::self()->read(); + setFocusedDisplayDistance(BreezyDesktopConfig::focusedDisplayDistance() / 100.0f); + setAllDisplaysDistance(BreezyDesktopConfig::allDisplaysDistance() / 100.0f); + setDisplaySpacing(BreezyDesktopConfig::displaySpacing() / 1000.0f); + setZoomOnFocusEnabled(BreezyDesktopConfig::zoomOnFocusEnabled()); +} + +QVariantMap BreezyDesktopEffect::initialProperties(Output *screen) +{ + return QVariantMap{ + {QStringLiteral("effect"), QVariant::fromValue(this)}, + {QStringLiteral("targetScreen"), QVariant::fromValue(screen)} + }; +} + +int BreezyDesktopEffect::requestedEffectChainPosition() const +{ + return 70; +} + +void BreezyDesktopEffect::toggle() +{ + if (isRunning()) { + qCCritical(KWIN_XR) << "\t\t\tBreezy - toggle - deactivating"; + deactivate(); + } else { + qCCritical(KWIN_XR) << "\t\t\tBreezy - toggle - activating"; + activate(); + } +} + +void BreezyDesktopEffect::activate() +{ + qCCritical(KWIN_XR) << "\t\t\tBreezy - activate"; + + if (!isRunning()) setRunning(true); + + connect(effects, &EffectsHandler::cursorShapeChanged, this, &BreezyDesktopEffect::updateCursorImage); + m_cursorUpdateTimer->start(); + + // QuickSceneEffect grabs the keyboard and mouse input, which pulls focus away from the active window + // and doesn't allow for interaction with anything on the desktop. These two calls fix that. + effects->ungrabKeyboard(); + effects->stopMouseInterception(this); + + hideCursor(); + recenter(); +} + +void BreezyDesktopEffect::deactivate() +{ + if (m_shutdownTimer->isActive()) { + return; + } + + qCCritical(KWIN_XR) << "\t\t\tBreezy - deactivate"; + disconnect(effects, &EffectsHandler::cursorShapeChanged, this, &BreezyDesktopEffect::updateCursorImage); + m_cursorUpdateTimer->stop(); + showCursor(); + + for (auto output : m_virtualOutputs) { + KWin::kwinApp()->outputBackend()->removeVirtualOutput(output); + } + m_virtualOutputs.clear(); + + // this triggers realDeactivate with a delay so if it's triggered from QML it gives the QML function time to + // exit, avoiding a crash + m_shutdownTimer->start(250); +} + +void BreezyDesktopEffect::enableDriver() +{ + qCCritical(KWIN_XR) << "\t\t\tBreezy - enableDriver"; + XRDriverIPC::instance().writeConfig({ + {"disabled", false}, + {"output_mode", "external_only"}, + {"external_mode", "breezy_desktop"} + }); +} + +void BreezyDesktopEffect::realDeactivate() +{ + qCCritical(KWIN_XR) << "\t\t\tBreezy - realDeactivate"; + setRunning(false); +} + +void BreezyDesktopEffect::addVirtualDisplay(QSize size) +{ + // QSize size(2560, 1440); + // addVirtualDisplay(size); + + static int virtualDisplayCount = 0; + ++virtualDisplayCount; + QString name = QStringLiteral("BreezyDesktop_VirtualDisplay_%1x%2_%3").arg(size.width()).arg(size.height()).arg(virtualDisplayCount); + #if defined(KWIN_VERSION_ENCODED) && KWIN_VERSION_ENCODED >= 60290 + QString description = QStringLiteral("Breezy Display %1x%2 (%3)").arg(size.width()).arg(size.height()).arg(virtualDisplayCount); + auto output = KWin::kwinApp()->outputBackend()->createVirtualOutput(name, description, size, 1.0); + #else + auto output = KWin::kwinApp()->outputBackend()->createVirtualOutput(name, size, 1.0); + #endif + if (output) { + m_virtualOutputs.append(output); + } +} + +bool BreezyDesktopEffect::isEnabled() const { + return m_enabled; +} + +bool BreezyDesktopEffect::isZoomOnFocusEnabled() const { + return m_zoomOnFocusEnabled; +} + +void BreezyDesktopEffect::setZoomOnFocusEnabled(bool enabled) { + if (m_zoomOnFocusEnabled != enabled) { + m_zoomOnFocusEnabled = enabled; + BreezyDesktopConfig::setZoomOnFocusEnabled(enabled); + BreezyDesktopConfig::self()->save(); + Q_EMIT zoomOnFocusChanged(); + } +} + +bool BreezyDesktopEffect::imuResetState() const { + return m_imuResetState; +} + +QList BreezyDesktopEffect::imuRotations() const { + return m_imuRotations; +} + +quint32 BreezyDesktopEffect::imuTimeElapsedMs() const { + return m_imuTimeElapsedMs; +} + +quint64 BreezyDesktopEffect::imuTimestamp() const { + return m_imuTimestamp; +} + +QList BreezyDesktopEffect::lookAheadConfig() const { + return m_lookAheadConfig; +} + +QList BreezyDesktopEffect::displayResolution() const { + return m_displayResolution; +} + +qreal BreezyDesktopEffect::focusedDisplayDistance() const { + return m_focusedDisplayDistance; +} + +void BreezyDesktopEffect::setFocusedDisplayDistance(qreal distance) { + if (distance != m_focusedDisplayDistance) { + m_focusedDisplayDistance = std::clamp(distance, 0.2, m_allDisplaysDistance); + Q_EMIT displayDistanceChanged(); + } +} + +qreal BreezyDesktopEffect::allDisplaysDistance() const { + return m_allDisplaysDistance; +} + +void BreezyDesktopEffect::setAllDisplaysDistance(qreal distance) { + if (distance != m_allDisplaysDistance) { + m_allDisplaysDistance = std::clamp(distance, m_focusedDisplayDistance, 2.5); + Q_EMIT displayDistanceChanged(); + } +} + +qreal BreezyDesktopEffect::displaySpacing() const { + return m_displaySpacing; +} + +void BreezyDesktopEffect::setDisplaySpacing(qreal spacing) { + if (spacing != m_displaySpacing) { + m_displaySpacing = spacing; + Q_EMIT displaySpacingChanged(); + } +} + +qreal BreezyDesktopEffect::diagonalFOV() const { + return m_diagonalFOV; +} + +qreal BreezyDesktopEffect::lensDistanceRatio() const { + return m_lensDistanceRatio; +} + +bool BreezyDesktopEffect::sbsEnabled() const { + return m_sbsEnabled; +} + +bool BreezyDesktopEffect::customBannerEnabled() const { + return m_customBannerEnabled; +} + +bool BreezyDesktopEffect::checkParityByte(const char* data) { + const uint8_t parityByte = static_cast(data[DataView::IMU_PARITY_BYTE[DataView::OFFSET_INDEX]]); + uint8_t parity = 0; + + const int dateBytes = DataView::IMU_DATE_MS[DataView::COUNT_INDEX] * DataView::IMU_DATE_MS[DataView::SIZE_INDEX]; + for (int i = 0; i < dateBytes; ++i) { + parity ^= static_cast(data[DataView::IMU_DATE_MS[DataView::OFFSET_INDEX] + i]); + } + + const int quatBytes = DataView::IMU_QUAT_DATA[DataView::COUNT_INDEX] * DataView::IMU_QUAT_DATA[DataView::SIZE_INDEX]; + for (int i = 0; i < quatBytes; ++i) { + parity ^= static_cast(data[DataView::IMU_QUAT_DATA[DataView::OFFSET_INDEX] + i]); + } + + return parityByte == parity; +} + +// TODO - can this be something callable from the camera qml code, so it's pulled only when needed? +static qint64 lastConfigUpdate = 0; +static qint64 activatedAt = 0; +void BreezyDesktopEffect::updateImuRotation() { + const QString shmPath = QStringLiteral("/dev/shm/breezy_desktop_imu"); + QFile shmFile(shmPath); + if (!shmFile.open(QIODevice::ReadOnly)) { + return; + } + QByteArray buffer = shmFile.readAll(); + shmFile.close(); + if (buffer.size() != DataView::LENGTH) return; + + const char* data = buffer.constData(); + if (!checkParityByte(data)) return; + + uint8_t version = static_cast(data[DataView::VERSION[DataView::OFFSET_INDEX]]); + uint8_t enabledFlag = static_cast(data[DataView::ENABLED[DataView::OFFSET_INDEX]]); + uint64_t imuDateMs; + memcpy(&imuDateMs, data + DataView::IMU_DATE_MS[DataView::OFFSET_INDEX], sizeof(imuDateMs)); + imuDateMs = qFromLittleEndian(imuDateMs); + + const qint64 currentTimeMs = QDateTime::currentMSecsSinceEpoch(); + const bool updateConfig = lastConfigUpdate == 0 || currentTimeMs - lastConfigUpdate > 1000; + + if (updateConfig) { + float lookAheadConfig[4]; + memcpy(&lookAheadConfig[0], data + DataView::LOOK_AHEAD_CFG[DataView::OFFSET_INDEX], sizeof(lookAheadConfig)); + m_lookAheadConfig.clear(); + m_lookAheadConfig.append(lookAheadConfig[0]); + m_lookAheadConfig.append(lookAheadConfig[1]); + m_lookAheadConfig.append(lookAheadConfig[2]); + m_lookAheadConfig.append(lookAheadConfig[3]); + + uint32_t displayResolution[2]; + memcpy(&displayResolution[0], data + DataView::DISPLAY_RES[DataView::OFFSET_INDEX], sizeof(displayResolution)); + m_displayResolution.clear(); + m_displayResolution.append(displayResolution[0]); + m_displayResolution.append(displayResolution[1]); + + float displayFov = 0.0f; + memcpy(&displayFov, data + DataView::DISPLAY_FOV[DataView::OFFSET_INDEX], sizeof(displayFov)); + m_diagonalFOV = displayFov; + + float lensDistanceRatio = 0.0f; + memcpy(&lensDistanceRatio, data + DataView::LENS_DISTANCE_RATIO[DataView::OFFSET_INDEX], sizeof(lensDistanceRatio)); + m_lensDistanceRatio = lensDistanceRatio; + + uint8_t sbsEnabled = false; + memcpy(&sbsEnabled, data + DataView::SBS_ENABLED[DataView::OFFSET_INDEX], sizeof(sbsEnabled)); + m_sbsEnabled = (sbsEnabled != 0); + + uint8_t customBannerEnabled = false; + memcpy(&customBannerEnabled, data + DataView::CUSTOM_BANNER_ENABLED[DataView::OFFSET_INDEX], sizeof(customBannerEnabled)); + m_customBannerEnabled = (customBannerEnabled != 0); + + lastConfigUpdate = currentTimeMs; + } + + const bool validKeepAlive = (currentTimeMs - imuDateMs) < 5000; + const bool validData = validKeepAlive && m_diagonalFOV != 0.0f; + const uint8_t expectedVersion = 4; + bool enabledFlagSet = (enabledFlag != 0); + bool validVersion = (version == expectedVersion); + const bool wasEnabled = m_enabled; + const bool enabled = enabledFlagSet && validVersion && validData; + if (!enabled) { + // give a grace period after enabling the effect + if (wasEnabled && (currentTimeMs - activatedAt > 1000)) { + qCCritical(KWIN_XR) << "\t\t\tBreezy - disabling effect; currentTimeMs:" << currentTimeMs + << "imuDateMs:" << imuDateMs + << "enabledFlag:" << enabledFlag + << "version:" << version + << "diagonalFOV:" << m_diagonalFOV; + deactivate(); + m_enabled = false; + Q_EMIT enabledStateChanged(); + return; + } + } else if (!wasEnabled) { + qCCritical(KWIN_XR) << "\t\t\tBreezy - enabling effect; currentTimeMs:" << currentTimeMs + << "imuDateMs:" << imuDateMs + << "enabledFlag:" << enabledFlag + << "version:" << version + << "diagonalFOV:" << m_diagonalFOV; + activate(); + m_enabled = true; + Q_EMIT enabledStateChanged(); + activatedAt = currentTimeMs; + } + + if (updateConfig) Q_EMIT devicePropertiesChanged(); + + float imuData[4 * DataView::IMU_QUAT_ENTRIES]; // 4 quaternion-sized rows + memcpy(imuData, data + DataView::IMU_QUAT_DATA[DataView::OFFSET_INDEX], sizeof(imuData)); + m_imuResetState = (imuData[0] == 0.0f && imuData[1] == 0.0f && imuData[2] == 0.0f && imuData[3] == 1.0f); + + // convert NWU to EUS by passing root.rotation values: -y, z, -x + QQuaternion quatT0(imuData[3], -imuData[1], imuData[2], -imuData[0]); + + int imuDataOffset = DataView::IMU_QUAT_ENTRIES; + QQuaternion quatT1(imuData[imuDataOffset + 3], -imuData[imuDataOffset + 1], imuData[imuDataOffset + 2], -imuData[imuDataOffset + 0]); + + imuDataOffset += DataView::IMU_QUAT_ENTRIES; + + // skip the 3rd quaternion + imuDataOffset += DataView::IMU_QUAT_ENTRIES; + + // set imuRotations to the last two rotations, leave out the elapsed time + m_imuRotations.clear(); + m_imuRotations.append(quatT0); + m_imuRotations.append(quatT1); + + // 4th row isn't actually a quaternion, it contains the timestamps for each of the 3 quaternions + // elapsed time between T0 and T1 is: imuData[0] - imuData[1] + m_imuTimeElapsedMs = static_cast(imuData[imuDataOffset + 0] - imuData[imuDataOffset + 1]); + + m_imuTimestamp = imuDateMs; + Q_EMIT imuRotationsChanged(); +} + +QString BreezyDesktopEffect::cursorImageSource() const +{ + return m_cursorImageSource; +} + +QPointF BreezyDesktopEffect::cursorPos() const +{ + return m_cursorPos; +} + +void BreezyDesktopEffect::showCursor() +{ + effects->showCursor(); +} + +void BreezyDesktopEffect::hideCursor() +{ + updateCursorImage(); + effects->hideCursor(); +} + +void BreezyDesktopEffect::updateCursorImage() +{ + const auto cursor = effects->cursorImage(); + if (!cursor.image().isNull()) { + QByteArray data; + QBuffer buffer(&data); + buffer.open(QIODevice::WriteOnly); + cursor.image().save(&buffer, "PNG"); + + m_cursorImageSource = QStringLiteral("data:image/png;base64,%1").arg(QString::fromLatin1(data.toBase64())); + } else { + m_cursorImageSource = QString(); + } + Q_EMIT cursorImageChanged(); +} + +void BreezyDesktopEffect::updateCursorPos() +{ + // Update cursor position from effects + const auto cursor = effects->cursorImage(); + QPointF newPos = effects->cursorPos() - cursor.hotSpot(); + if (m_cursorPos != newPos) { + m_cursorPos = newPos; + Q_EMIT cursorPosChanged(); + } +} +} + +#include "moc_breezydesktopeffect.cpp" \ No newline at end of file diff --git a/kwin/src/breezydesktopeffect.h b/kwin/src/breezydesktopeffect.h new file mode 100644 index 0000000..ba3dee7 --- /dev/null +++ b/kwin/src/breezydesktopeffect.h @@ -0,0 +1,124 @@ +#pragma once + +#include "kcm/shortcuts.h" +#include + +#include +#include +#include +#include +#include + +namespace KWin +{ + class BreezyDesktopEffect : public QuickSceneEffect + { + Q_OBJECT + Q_PROPERTY(bool isEnabled READ isEnabled NOTIFY enabledStateChanged) + Q_PROPERTY(bool zoomOnFocusEnabled READ isZoomOnFocusEnabled WRITE setZoomOnFocusEnabled NOTIFY zoomOnFocusChanged) + Q_PROPERTY(bool imuResetState READ imuResetState NOTIFY imuRotationsChanged) + Q_PROPERTY(QList imuRotations READ imuRotations NOTIFY imuRotationsChanged) + Q_PROPERTY(quint32 imuTimeElapsedMs READ imuTimeElapsedMs NOTIFY imuRotationsChanged) + Q_PROPERTY(quint64 imuTimestamp READ imuTimestamp NOTIFY imuRotationsChanged) + Q_PROPERTY(QString cursorImageSource READ cursorImageSource NOTIFY cursorImageChanged) + Q_PROPERTY(QPointF cursorPos READ cursorPos NOTIFY cursorPosChanged) + Q_PROPERTY(QList lookAheadConfig READ lookAheadConfig NOTIFY devicePropertiesChanged) + Q_PROPERTY(QList displayResolution READ displayResolution NOTIFY devicePropertiesChanged) + Q_PROPERTY(qreal focusedDisplayDistance READ focusedDisplayDistance NOTIFY displayDistanceChanged) + Q_PROPERTY(qreal allDisplaysDistance READ allDisplaysDistance NOTIFY displayDistanceChanged) + Q_PROPERTY(qreal displaySpacing READ displaySpacing NOTIFY displaySpacingChanged) + Q_PROPERTY(qreal diagonalFOV READ diagonalFOV NOTIFY devicePropertiesChanged) + Q_PROPERTY(qreal lensDistanceRatio READ lensDistanceRatio NOTIFY devicePropertiesChanged) + Q_PROPERTY(bool sbsEnabled READ sbsEnabled NOTIFY devicePropertiesChanged) + Q_PROPERTY(bool customBannerEnabled READ customBannerEnabled NOTIFY devicePropertiesChanged) + + public: + + BreezyDesktopEffect(); + + void reconfigure(ReconfigureFlags) override; + + int requestedEffectChainPosition() const override; + + QString cursorImageSource() const; + QPointF cursorPos() const; + + bool isEnabled() const; + bool isZoomOnFocusEnabled() const; + void setZoomOnFocusEnabled(bool enabled); + QList imuRotations() const; + quint32 imuTimeElapsedMs() const; + quint64 imuTimestamp() const; + bool imuResetState() const; + QList lookAheadConfig() const; + QList displayResolution() const; + qreal focusedDisplayDistance() const; + void setFocusedDisplayDistance(qreal distance); + qreal allDisplaysDistance() const; + void setAllDisplaysDistance(qreal distance); + qreal displaySpacing() const; + void setDisplaySpacing(qreal spacing); + qreal diagonalFOV() const; + qreal lensDistanceRatio() const; + bool sbsEnabled() const; + bool customBannerEnabled() const; + + void showCursor(); + void hideCursor(); + + public Q_SLOTS: + void activate(); + void deactivate(); + void enableDriver(); + void toggle(); + void addVirtualDisplay(QSize size); + void updateImuRotation(); + void updateCursorImage(); + void updateCursorPos(); + + Q_SIGNALS: + void displayDistanceChanged(); + void displaySpacingChanged(); + void enabledStateChanged(); + void zoomOnFocusChanged(); + void imuRotationsChanged(); + void cursorImageChanged(); + void cursorPosChanged(); + void devicePropertiesChanged(); + + protected: + QVariantMap initialProperties(Output *screen) override; + + private: + void realDeactivate(); + bool checkParityByte(const char* data); + void setupGlobalShortcut(const BreezyShortcuts::Shortcut &shortcut, + std::function triggeredFunc); + void recenter(); + + QTimer *m_shutdownTimer; + QString m_cursorImageSource; + + bool m_enabled = false; + bool m_zoomOnFocusEnabled = false; + bool m_imuResetState; + QList m_imuRotations; + quint32 m_imuTimeElapsedMs; + quint64 m_imuTimestamp = 0; + QList m_lookAheadConfig; + QList m_displayResolution; + qreal m_diagonalFOV; + qreal m_lensDistanceRatio; + bool m_sbsEnabled; + bool m_customBannerEnabled; + QFileSystemWatcher *m_shmFileWatcher = nullptr; + QFileSystemWatcher *m_shmDirectoryWatcher = nullptr; + QPointF m_cursorPos; + QTimer *m_cursorUpdateTimer = nullptr; + qreal m_focusedDisplayDistance = 0.85; + qreal m_allDisplaysDistance = 1.05; + qreal m_displaySpacing = 0.0; + QList m_virtualOutputs; + }; + +} // namespace KWin diff --git a/kwin/src/kcm/CMakeLists.txt b/kwin/src/kcm/CMakeLists.txt new file mode 100644 index 0000000..34e048e --- /dev/null +++ b/kwin/src/kcm/CMakeLists.txt @@ -0,0 +1,18 @@ +set(breezy_desktop_config_SOURCES breezydesktopeffectkcm.cpp labeledslider.cpp) +ki18n_wrap_ui(breezy_desktop_config_SOURCES breezydesktopeffectkcm.ui) +qt_add_dbus_interface(breezy_desktop_config_SOURCES ${KWIN_EFFECTS_INTERFACE} kwineffects_interface) + +kcoreaddons_add_plugin(breezy_desktop_config INSTALL_NAMESPACE "kwin/effects/configs" SOURCES ${breezy_desktop_config_SOURCES}) +kconfig_add_kcfg_files(breezy_desktop_config ../breezydesktopconfig.kcfgc) +target_link_libraries(breezy_desktop_config + KF6::ConfigCore + KF6::ConfigGui + KF6::ConfigWidgets + KF6::CoreAddons + KF6::GlobalAccel + KF6::I18n + KF6::KCMUtils + KF6::XmlGui + + xr_driver_ipc +) diff --git a/kwin/src/kcm/breezydesktopeffectkcm.cpp b/kwin/src/kcm/breezydesktopeffectkcm.cpp new file mode 100644 index 0000000..fc0c10d --- /dev/null +++ b/kwin/src/kcm/breezydesktopeffectkcm.cpp @@ -0,0 +1,151 @@ +#include "shortcuts.h" +#include "breezydesktopeffectkcm.h" +#include "breezydesktopconfig.h" +#include "labeledslider.h" +#include "xrdriveripc.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include + +Q_LOGGING_CATEGORY(KWIN_XR, "kwin.xr") + +static const char EFFECT_GROUP[] = "Effect-breezy_desktop"; + +void addShortcutAction(KActionCollection *collection, const BreezyShortcuts::Shortcut &shortcut) +{ + QAction *action = collection->addAction(shortcut.actionName); + action->setText(shortcut.actionText); + action->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(action, {shortcut.shortcut}); + KGlobalAccel::self()->setShortcut(action, {shortcut.shortcut}); +} + +K_PLUGIN_CLASS(BreezyDesktopEffectConfig) + +BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) +{ + ui.setupUi(widget()); + addConfig(BreezyDesktopConfig::self(), widget()); + + m_statePollTimer.setInterval(2000); + m_statePollTimer.setTimerType(Qt::CoarseTimer); + connect(&m_statePollTimer, &QTimer::timeout, this, &BreezyDesktopEffectConfig::pollDriverState); + m_statePollTimer.start(); + + m_configWatcher = KConfigWatcher::create(BreezyDesktopConfig::self()->sharedConfig()); + if (m_configWatcher) { + connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, + [this](const KConfigGroup &group) { + if (m_updatingFromConfig) { + return; + } + if (group.name() != QLatin1String(EFFECT_GROUP)) { + return; + } + BreezyDesktopConfig::self()->read(); + updateUiFromConfig(); + updateUnmanagedState(); + }); + } + + auto actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + actionCollection->setComponentDisplayName(i18n("KWin")); + actionCollection->setConfigGroup(QStringLiteral("breezy_desktop")); + actionCollection->setConfigGlobal(true); + + addShortcutAction(actionCollection, BreezyShortcuts::TOGGLE); + addShortcutAction(actionCollection, BreezyShortcuts::RECENTER); + addShortcutAction(actionCollection, BreezyShortcuts::TOGGLE_ZOOM_ON_FOCUS); + ui.shortcutsEditor->addCollection(actionCollection); + connect(ui.shortcutsEditor, &KShortcutsEditor::keyChange, this, &BreezyDesktopEffectConfig::markAsChanged); + connect(ui.kcfg_FocusedDisplayDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); + connect(ui.kcfg_AllDisplaysDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); + connect(ui.kcfg_DisplaySpacing, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); +} + +BreezyDesktopEffectConfig::~BreezyDesktopEffectConfig() +{ +} + +void BreezyDesktopEffectConfig::load() +{ + KCModule::load(); + updateUiFromConfig(); + updateUnmanagedState(); +} + +void BreezyDesktopEffectConfig::save() +{ + // Prevent reacting to the file change we ourselves are about to write. + m_updatingFromConfig = true; + updateConfigFromUi(); + BreezyDesktopConfig::self()->save(); + KCModule::save(); + ui.kcfg_FocusedDisplayDistance->setEnabled(ui.kcfg_ZoomOnFocusEnabled->isChecked()); + m_updatingFromConfig = false; + updateUnmanagedState(); + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Effects"), QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("breezy_desktop")); +} + +void BreezyDesktopEffectConfig::defaults() +{ + KCModule::defaults(); + updateUiFromDefaultConfig(); + updateUnmanagedState(); +} + +void BreezyDesktopEffectConfig::updateConfigFromUi() +{ + ui.shortcutsEditor->save(); +} + +void BreezyDesktopEffectConfig::updateUiFromConfig() +{ + ui.kcfg_FocusedDisplayDistance->setValue(BreezyDesktopConfig::self()->focusedDisplayDistance()); + ui.kcfg_AllDisplaysDistance->setValue(BreezyDesktopConfig::self()->allDisplaysDistance()); + ui.kcfg_DisplaySpacing->setValue(BreezyDesktopConfig::self()->displaySpacing()); + ui.kcfg_ZoomOnFocusEnabled->setChecked(BreezyDesktopConfig::self()->zoomOnFocusEnabled()); + ui.kcfg_FocusedDisplayDistance->setEnabled(ui.kcfg_ZoomOnFocusEnabled->isChecked()); +} + +void BreezyDesktopEffectConfig::updateUiFromDefaultConfig() +{ + ui.shortcutsEditor->allDefault(); +} + +void BreezyDesktopEffectConfig::updateUnmanagedState() +{ +} + +void BreezyDesktopEffectConfig::pollDriverState() +{ + auto &bridge = XRDriverIPC::instance(); + auto stateOpt = bridge.retrieveDriverState(); + const XRDict &state = stateOpt.value(); + + m_connectedDeviceBrand = QString::fromStdString(XRDriverIPC::string(state, XRStateEntry::ConnectedDeviceBrand)); + m_connectedDeviceModel = QString::fromStdString(XRDriverIPC::string(state, XRStateEntry::ConnectedDeviceModel)); + + const bool wasDeviceConnected = m_deviceConnected; + m_deviceConnected = !m_connectedDeviceBrand.isEmpty() && !m_connectedDeviceModel.isEmpty(); + if (m_deviceConnected != wasDeviceConnected) { + ui.labelDeviceConnectionStatus->setText(m_deviceConnected ? + QStringLiteral("%1 %2 connected").arg(m_connectedDeviceBrand, m_connectedDeviceModel) : + QStringLiteral("No device connected")); + } +} + +#include "breezydesktopeffectkcm.moc" \ No newline at end of file diff --git a/kwin/src/kcm/breezydesktopeffectkcm.h b/kwin/src/kcm/breezydesktopeffectkcm.h new file mode 100644 index 0000000..13d354c --- /dev/null +++ b/kwin/src/kcm/breezydesktopeffectkcm.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include + +#include + +#include "ui_breezydesktopeffectkcm.h" + +class KConfigWatcher; +class KConfigGroup; + +class BreezyDesktopEffectConfig : public KCModule +{ + Q_OBJECT + +public: + BreezyDesktopEffectConfig(QObject *parent, const KPluginMetaData &data); + ~BreezyDesktopEffectConfig() override; + +public Q_SLOTS: + void load() override; + void save() override; + void defaults() override; + +private: + void updateUiFromConfig(); + void updateUiFromDefaultConfig(); + void updateConfigFromUi(); + void updateUnmanagedState(); + void pollDriverState(); + + ::Ui::BreezyDesktopEffectConfig ui; + + KConfigWatcher::Ptr m_configWatcher; + bool m_updatingFromConfig = false; + bool m_deviceConnected = false; + QString m_connectedDeviceBrand; + QString m_connectedDeviceModel; + QTimer m_statePollTimer; // periodic driver state polling +}; diff --git a/kwin/src/kcm/breezydesktopeffectkcm.ui b/kwin/src/kcm/breezydesktopeffectkcm.ui new file mode 100644 index 0000000..c431a53 --- /dev/null +++ b/kwin/src/kcm/breezydesktopeffectkcm.ui @@ -0,0 +1,159 @@ + + + BreezyDesktopEffectConfig + + + + + + No device connected + + + Qt::AlignHCenter|Qt::AlignVCenter + + + + 14 + 75 + true + + + + + + + + QTabWidget::North + + + QTabWidget::Rounded + + + + &General + + + + + + Zoom on Focus + + + false + + + + + + + Focused Display Distance: + + + + + + + 2 + + + QSlider::TicksBelow + + + 25 + + + Qt::Horizontal + + + true + + + + + + + All Displays Distance: + + + + + + + 2 + + + QSlider::TicksBelow + + + 25 + + + Qt::Horizontal + + + true + + + + + + + Display Spacing: + + + + + + + Qt::Horizontal + + + true + + + + + + + + 0 + 0 + + + + + + + + + &Advanced + + + + + + Advanced settings will appear here. + + + Qt::AlignCenter + + + + + + + + + + + + KShortcutsEditor + QWidget +
kshortcutseditor.h
+ 1 +
+
+ + + +
diff --git a/kwin/src/kcm/labeledslider.cpp b/kwin/src/kcm/labeledslider.cpp new file mode 100644 index 0000000..23cdc32 --- /dev/null +++ b/kwin/src/kcm/labeledslider.cpp @@ -0,0 +1 @@ +#include "labeledslider.h" \ No newline at end of file diff --git a/kwin/src/kcm/labeledslider.h b/kwin/src/kcm/labeledslider.h new file mode 100644 index 0000000..6a8a4ec --- /dev/null +++ b/kwin/src/kcm/labeledslider.h @@ -0,0 +1,157 @@ +#pragma once + +#include +#include +#include +#include +#include // for std::max + +/* + * LabeledSlider + * Horizontal QSlider that draws numeric labels at tick intervals and (optionally) a value bubble. + * Usage: + * auto *s = new LabeledSlider(parent); + * s->setMinimum(20); + * s->setMaximum(250); + * s->setTickInterval(20); + * s->setTickPosition(QSlider::TicksBelow); + * s->setShowValueBubble(true); + */ + +class LabeledSlider : public QSlider { + Q_OBJECT + Q_PROPERTY(bool showValueBubble READ showValueBubble WRITE setShowValueBubble) + // decimalShift: number of places to shift the decimal point left for display ONLY. + // Example: raw value 250 with decimalShift=2 displays as 2.50. Underlying slider value + // (signals, stored config) remains 250. + Q_PROPERTY(int decimalShift READ decimalShift WRITE setDecimalShift) +public: + explicit LabeledSlider(QWidget *parent = nullptr) + : QSlider(Qt::Horizontal, parent) + { + setTickPosition(QSlider::TicksBelow); + } + + bool showValueBubble() const { return m_showValueBubble; } + void setShowValueBubble(bool on) { + if (m_showValueBubble == on) return; + m_showValueBubble = on; + update(); + } + + int decimalShift() const { return m_decimalShift; } + void setDecimalShift(int shift) { + // clamp to sensible range + if (shift < 0) shift = 0; + if (shift > 6) shift = 6; // avoid large power-of-10 overflow + if (m_decimalShift == shift) return; + m_decimalShift = shift; + updateGeometry(); + update(); + } + + QSize sizeHint() const override { + QSize sz = QSlider::sizeHint(); + int extraH = 0; + if (labelInterval() > 0) { + // Reserve space for bottom labels + QFontMetrics fm(font()); + extraH += fm.height() + 4; + } + if (m_showValueBubble) { + QFontMetrics fm(font()); + extraH = std::max(extraH, fm.height() + 8); // bubble might be above + } + sz.setHeight(sz.height() + extraH); + return sz; + } + +protected: + void paintEvent(QPaintEvent *e) override { + QSlider::paintEvent(e); + + QStyleOptionSlider opt; + initStyleOption(&opt); + + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing, true); + + const int minV = minimum(); + const int maxV = maximum(); + + // Draw labels below ticks + if (labelInterval() > 0) { + QFontMetrics fm(font()); + const int baselineY = height() - fm.descent() - 1; + int interval = labelInterval(); + for (int v = minV; v <= maxV; v += interval) { + // Use style geometry for handle at this position to match tick placement. + QStyleOptionSlider optPos = opt; + optPos.sliderPosition = v; + optPos.sliderValue = v; + QRect handleAtVal = style()->subControlRect(QStyle::CC_Slider, &optPos, QStyle::SC_SliderHandle, this); + int x = handleAtVal.center().x(); + QString text = valueToDisplayString(v); + int halfW = fm.horizontalAdvance(text) / 2; + QRect r(x - halfW, baselineY - fm.ascent(), fm.horizontalAdvance(text), fm.height()); + p.drawText(r, Qt::AlignCenter, text); + } + } + + // Draw floating value bubble over handle + if (m_showValueBubble) { + // Handle rect + const QRect handle = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); + QString valText = valueToDisplayString(value()); + QFontMetrics fm(font()); + QRect textRect = fm.boundingRect(valText); + textRect.adjust(-6, -4, 6, 4); + + // Position bubble above the handle; add extra lift + const int extraLift = 10; // requested additional pixels + const int gap = 4; // minimal gap between handle top and bubble + int topY = handle.top() - gap - extraLift - textRect.height(); + if (topY < 0) topY = 0; // clamp to widget + textRect.moveTop(topY); + textRect.moveLeft(handle.center().x() - textRect.width()/2); + + // Bubble shape + QPainterPath path; + path.addRoundedRect(textRect, 6, 6); + + p.setPen(Qt::NoPen); + p.setBrush(palette().toolTipBase()); + p.drawPath(path); + + p.setPen(palette().toolTipText().color()); + p.drawText(textRect, Qt::AlignCenter, valText); + } + } + +private: + QString valueToDisplayString(int raw) const { + if (m_decimalShift == 0) { + return QString::number(raw); + } + int divisor = 1; + for (int i = 0; i < m_decimalShift; ++i) divisor *= 10; // small loop, m_decimalShift capped + int whole = raw / divisor; + int frac = std::abs(raw % divisor); + QString fracStr = QString::number(frac).rightJustified(m_decimalShift, QLatin1Char('0')); + return QString::number(whole) + QLatin1Char('.') + fracStr; + } + + bool m_showValueBubble = true; + int m_decimalShift = 0; // display-only decimal shift +private: + int labelInterval() const { + int ti = tickInterval(); + if (ti > 0) return ti; + // Heuristic fallback: divide range into ~10 segments. + int range = maximum() - minimum(); + if (range <= 0) return 0; + int approx = range / 10; + if (approx <= 0) approx = range; // single label at ends + return approx; + } +}; \ No newline at end of file diff --git a/kwin/src/kcm/shortcuts.h b/kwin/src/kcm/shortcuts.h new file mode 100644 index 0000000..f5792a6 --- /dev/null +++ b/kwin/src/kcm/shortcuts.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include + +namespace BreezyShortcuts { + struct Shortcut { + QKeySequence shortcut; + QString actionName; + QString actionText; + }; + + const Shortcut TOGGLE = { + Qt::CTRL | Qt::META | Qt::Key_Backslash, + QStringLiteral("Toggle XR Effect"), + QStringLiteral("Toggle XR Effect") + }; + + const Shortcut RECENTER = { + Qt::CTRL | Qt::META | Qt::Key_Space, + QStringLiteral("Recenter"), + QStringLiteral("Recenter") + }; + + const Shortcut TOGGLE_ZOOM_ON_FOCUS = { + Qt::CTRL | Qt::META | Qt::Key_0, + QStringLiteral("Toggle Zoom on Focus"), + QStringLiteral("Toggle Zoom on Focus") + }; +} diff --git a/kwin/src/main.cpp b/kwin/src/main.cpp new file mode 100644 index 0000000..fd243a0 --- /dev/null +++ b/kwin/src/main.cpp @@ -0,0 +1,10 @@ +#include "breezydesktopeffect.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY(BreezyDesktopEffect, "metadata.json") + +} // namespace KWin + +#include "main.moc" diff --git a/kwin/src/metadata.json b/kwin/src/metadata.json new file mode 100644 index 0000000..8d582c4 --- /dev/null +++ b/kwin/src/metadata.json @@ -0,0 +1,17 @@ +{ + "KPackageStructure": "KWin/Effect", + "KPlugin": { + "Authors": [ + { + "Email": "wayne@xronlinux.com", + "Name": "Wayne Heaney" + } + ], + "Category": "Tools", + "Description": "Breezy Desktop XR Effect", + "EnabledByDefault": true, + "License": "GPL", + "Name": "Breezy Desktop XR" + }, + "X-KDE-ConfigModule": "breezy_desktop_config" +} diff --git a/kwin/src/qml/BreezyDesktop.qml b/kwin/src/qml/BreezyDesktop.qml new file mode 100644 index 0000000..8ffa052 --- /dev/null +++ b/kwin/src/qml/BreezyDesktop.qml @@ -0,0 +1,147 @@ +import QtQuick +import QtQuick3D + + +Node { + id: breezyDesktop + + property var viewportResolution: effect.displayResolution + property var screens: root.screens + property var fovDetails: root.fovDetails + property var monitorPlacements: root.monitorPlacements + property var imuRotations: effect.imuRotations + property int focusedMonitorIndex: -1 + + Displays { + id: displays + } + + function displayAtIndex(index) { + if (index < 0 || index >= screens.length) { + return null; + } + return breezyDesktopDisplays.objectAt(index); + } + + Repeater3D { + id: breezyDesktopDisplays + model: screens.length + delegate: BreezyDesktopDisplay { + screen: screens[index] + monitorPlacement: monitorPlacements[index] + property real monitorDistance: effect.allDisplaysDistance + property real targetDistance: effect.allDisplaysDistance + property real screenRotationY: displays.radianToDegree(monitorPlacement.rotationAngleRadians.y) + property real screenRotationX: displays.radianToDegree(monitorPlacement.rotationAngleRadians.x) + + property vector3d screenScale: { + const geometry = screen.geometry; + + // apparently the default model unit size is 100x100, so we scale it up to the screen size + return Qt.vector3d(geometry.width / 100, geometry.height / 100, 1); + } + + scale: screenScale + eulerRotation.y: screenRotationY + eulerRotation.x: screenRotationX + position: { + // camera looks along the negative Z axis + const positionVector = + displays.nwuToEusVector(monitorPlacement.centerNoRotate) + .times(monitorDistance / effect.allDisplaysDistance); + + // position vector is only translated in flat directions, without rotations applied, so apply them here + const rotationMatrix = Qt.matrix4x4(); + + // only one of these should ever be non-zero, since we only rotate in the direction of the "wrap" preference + rotationMatrix.rotate(screenRotationY, Qt.vector3d(0, 1, 0)); + rotationMatrix.rotate(screenRotationX, Qt.vector3d(1, 0, 0)); + + return rotationMatrix.times(positionVector); + } + } + } + + Timer { + interval: 500 // 500ms - 2x per second to avoid running this check too frequently + repeat: true + running: true + onTriggered: { + if (breezyDesktop.imuRotations && breezyDesktop.imuRotations.length > 0) { + let focusedIndex = -1; + + if (effect.zoomOnFocusEnabled) { + focusedIndex = displays.findFocusedMonitor( + displays.eusToNwuQuat(breezyDesktop.imuRotations[0]), + breezyDesktop.monitorPlacements.map(monitorVectors => monitorVectors.centerLook), + breezyDesktop.focusedMonitorIndex, + false, // TODO smooth follow + breezyDesktop.fovDetails, + breezyDesktop.screens.map(screen => screen.geometry) + ); + } + + const focusedDisplay = focusedIndex !== -1 ? breezyDesktop.displayAtIndex(focusedIndex) : null; + if (focusedIndex !== breezyDesktop.focusedMonitorIndex) { + if (focusedDisplay === null) { + zoomOutAnimation.target = breezyDesktop.displayAtIndex(breezyDesktop.focusedMonitorIndex); + zoomOutAnimation.target.targetDistance = zoomOutAnimation.to; + zoomOutAnimation.start(); + } else { + if (breezyDesktop.focusedMonitorIndex === -1) { + zoomInAnimation.target = focusedDisplay; + focusedDisplay.targetDistance = zoomInAnimation.to; + zoomInAnimation.start(); + } else { + zoomInSeqAnimation.target = focusedDisplay; + focusedDisplay.targetDistance = zoomInSeqAnimation.to; + zoomOutSeqAnimation.target = breezyDesktop.displayAtIndex(breezyDesktop.focusedMonitorIndex); + zoomOutSeqAnimation.target.targetDistance = zoomOutSeqAnimation.to; + zoomOnFocusSequence.start(); + } + } + breezyDesktop.focusedMonitorIndex = focusedIndex; + } else if (focusedDisplay !== null && focusedDisplay.targetDistance !== effect.focusedDisplayDistance) { + // user is changing the focused display distance setting, so just move it to match + focusedDisplay.monitorDistance = effect.focusedDisplayDistance; + focusedDisplay.targetDistance = effect.focusedDisplayDistance; + } + } + } + } + + NumberAnimation { + id: zoomOutAnimation + property: "monitorDistance" + to: effect.allDisplaysDistance + duration: 150 + running: false + } + + NumberAnimation { + id: zoomInAnimation + property: "monitorDistance" + to: effect.focusedDisplayDistance + duration: 300 + running: false + } + + SequentialAnimation { + id: zoomOnFocusSequence + running: false + + NumberAnimation { + id: zoomOutSeqAnimation + property: "monitorDistance" + to: effect.allDisplaysDistance + duration: 150 + } + PauseAnimation { duration: 50 } + NumberAnimation { + id: zoomInSeqAnimation + property: "monitorDistance" + to: effect.focusedDisplayDistance + duration: 300 + } + } +} diff --git a/kwin/src/qml/BreezyDesktopDisplay.qml b/kwin/src/qml/BreezyDesktopDisplay.qml new file mode 100644 index 0000000..d7ea78a --- /dev/null +++ b/kwin/src/qml/BreezyDesktopDisplay.qml @@ -0,0 +1,26 @@ +import QtQuick +import QtQuick3D + +Model { + id: display + + required property QtObject screen + required property var monitorPlacement + required property int index + + source: "#Rectangle" + materials: [ + DefaultMaterial { + cullMode: Material.NoCulling + lighting: DefaultMaterial.NoLighting + depthDrawMode: Material.AlwaysDepthDraw + diffuseMap: Texture { + sourceItem: DesktopView { + screen: display.screen + width: display.screen.geometry.width + height: display.screen.geometry.height + } + } + } + ] +} diff --git a/kwin/src/qml/CameraController.qml b/kwin/src/qml/CameraController.qml new file mode 100644 index 0000000..15b5276 --- /dev/null +++ b/kwin/src/qml/CameraController.qml @@ -0,0 +1,84 @@ +import QtQuick +import QtQuick3D + +Item { + id: root + + required property Camera camera + + property var imuRotations: effect.imuRotations + property int imuTimeElapsedMs: effect.imuTimeElapsedMs + property double imuTimestamp: effect.imuTimestamp + property var lookAheadConfig: effect.lookAheadConfig + property var displayResolution: effect.displayResolution + property real diagonalFOV: effect.diagonalFOV + property real lensDistanceRatio: effect.lensDistanceRatio + property bool sbsEnabled: effect.sbsEnabled + property bool customBannerEnabled: effect.customBannerEnabled + + implicitWidth: parent.width + implicitHeight: parent.height + + Displays { + id: displays + } + + function updateCamera(rotation) { + camera.eulerRotation = rotation; + } + + // how far to look ahead is how old the IMU data is plus a constant that is either the default for this device or an override + function lookAheadMS(imuDateMs, lookAheadConfig, override) { + // how stale the imu data is + const dataAge = Date.now() - imuDateMs; + + const lookAheadConstant = lookAheadConfig[0]; + const lookAheadMultiplier = lookAheadConfig[1]; + return (override === -1 ? lookAheadConstant : override) + dataAge; + } + + function applyLookAhead(quatT0, quatT1, elapsedTimeMs, lookAheadMs) { + // convert both quats to euler angles + const eulerT0 = quatT0.toEulerAngles(); + const eulerT1 = quatT1.toEulerAngles(); + + // compute the rate of change of the angles based on the elapsed time + const deltaX = (eulerT0.x - eulerT1.x); + const deltaY = (eulerT0.y - eulerT1.y); + const deltaZ = (eulerT0.z - eulerT1.z); + + // how much of the delta to apply based on the look-ahead time + const timeConstant = lookAheadMs / elapsedTimeMs; + + return Qt.vector3d( + eulerT0.x + deltaX * timeConstant, + eulerT0.y + deltaY * timeConstant, + eulerT0.z + deltaZ * timeConstant, + ); + } + + function updateFOV() { + const aspectRatio = displayResolution[0] / displayResolution[1]; + camera.fieldOfView = displays.radianToDegree(displays.diagonalToCrossFOVs( + displays.degreeToRadian(root.diagonalFOV), + aspectRatio + ).vertical); + } + + onDisplayResolutionChanged: updateFOV(); + onDiagonalFOVChanged: updateFOV(); + + FrameAnimation { + running: true + onTriggered: { + if (root.imuRotations && root.imuRotations.length > 0) { + updateCamera(applyLookAhead( + root.imuRotations[0], + root.imuRotations[1], + root.imuTimeElapsedMs, + lookAheadMS(root.imuTimestamp, root.lookAheadConfig, -1) + )); + } + } + } +} diff --git a/kwin/src/qml/DesktopView.qml b/kwin/src/qml/DesktopView.qml new file mode 100644 index 0000000..03cacd6 --- /dev/null +++ b/kwin/src/qml/DesktopView.qml @@ -0,0 +1,57 @@ +import QtQuick +import org.kde.kwin as KWinComponents + +Item { + id: desktopView + + required property QtObject screen + + function overlapsScreen(win, screenGeom) { + if (!win) return false + const winLeft = win.x + const winTop = win.y + const winRight = winLeft + win.width + const winBottom = winTop + win.height + + const scrLeft = screenGeom.x + const scrTop = screenGeom.y + const scrRight = scrLeft + screenGeom.width + const scrBottom = scrTop + screenGeom.height + + return winLeft < scrRight && + winRight > scrLeft && + winTop < scrBottom && + winBottom > scrTop + } + + Repeater { + model: KWinComponents.WindowModel {} + + KWinComponents.WindowThumbnail { + // Only show if window overlaps this screen (any amount) and not minimized. + readonly property bool onThisActivity: model.window.activities.length === 0 || model.window.activities.includes(KWinComponents.Workspace.currentActivity) + readonly property bool onThisDesktop: onThisActivity && (model.window.onAllDesktops || model.window.desktops.includes(KWinComponents.Workspace.currentDesktop)) + readonly property bool onThisScreen: onThisDesktop && desktopView.overlapsScreen(model.window, desktopView.screen.geometry) + + wId: model.window.internalId + x: model.window.x - desktopView.screen.geometry.x + y: model.window.y - desktopView.screen.geometry.y + z: model.window.stackingOrder + visible: onThisScreen && !model.window.minimized + } + } + + Image { + id: cursorImg + source: effect.cursorImageSource + cache: false + visible: true // TODO - cursor position bounds check? + x: effect.cursorPos.x - desktopView.screen.geometry.x + y: effect.cursorPos.y - desktopView.screen.geometry.y + z: 9999 // ensure on top + anchors.centerIn: undefined + + layer.enabled: true + layer.smooth: true + } +} diff --git a/kwin/src/qml/Displays.qml b/kwin/src/qml/Displays.qml new file mode 100644 index 0000000..69e79af --- /dev/null +++ b/kwin/src/qml/Displays.qml @@ -0,0 +1,400 @@ +import QtQuick + +QtObject { + readonly property real focusThreshold: 0.95 / 2.0 + readonly property real unfocusThreshold: 1.1 / 2.0 + + // Converts degrees to radians + function degreeToRadian(degree) { + return degree * Math.PI / 180; + } + + function radianToDegree(radian) { + return radian * 180 / Math.PI; + } + + function nwuToEusVector(vector) { + // Converts NWU vector to EUS vector + return Qt.vector3d(-vector.y, vector.z, -vector.x); + } + + function eusToNwuQuat(quaternion) { + // Converts EUS quaternion to NWU quaternion + return Qt.quaternion(quaternion.scalar, -quaternion.z, -quaternion.x, quaternion.y); + } + + // Converts diagonal FOV in radians and aspect ratio to horizontal and vertical FOVs + function diagonalToCrossFOVs(diagonalFOVRadians, aspectRatio) { + var flatDiagonalFOV = 2 * Math.tan(diagonalFOVRadians / 2); + var flatVerticalFOV = flatDiagonalFOV / Math.sqrt(1 + aspectRatio * aspectRatio); + var flatHorizontalFOV = flatVerticalFOV * aspectRatio; + return { + diagonal: diagonalFOVRadians, + horizontal: 2 * Math.atan(flatHorizontalFOV / 2), + vertical: 2 * Math.atan(flatVerticalFOV / 2) + } + } + + function actualWrapScheme(screens, viewportWidth, viewportHeight) { + const minX = Math.min(...screens.map(screen => screen.geometry.x)); + const maxX = Math.max(...screens.map(screen => screen.geometry.x + screen.geometry.width)); + const minY = Math.min(...screens.map(screen => screen.geometry.y)); + const maxY = Math.max(...screens.map(screen => screen.geometry.y + screen.geometry.height)); + + if ((maxX - minX) / viewportWidth >= (maxY - minY) / viewportHeight) { + return 'horizontal'; + } else { + return 'vertical'; + } + } + + function fovDetails(screens, viewportWidth, viewportHeight, viewportDiagonalFOV, lensDistanceRatio) { + const aspect = viewportWidth / viewportHeight; + const fovRadians = diagonalToCrossFOVs(degreeToRadian(viewportDiagonalFOV), aspect); + const defaultDistanceVerticalRadians = 2 * Math.atan(Math.tan(fovRadians.vertical / 2) / effect.allDisplaysDistance); + const defaultDistanceHorizontalRadians = 2 * Math.atan(Math.tan(fovRadians.horizontal / 2) / effect.allDisplaysDistance); + + // distance needed for the FOV-sized monitor to fill up the screen + const fullScreenDistance = viewportHeight / 2 / Math.tan(fovRadians.vertical / 2); + const lensDistancePixels = fullScreenDistance / (1.0 - lensDistanceRatio) - fullScreenDistance; + + // distance of a display at the default (most zoomed out) distance, plus the lens distance constant + const lensToScreenDistance = viewportHeight / 2 / Math.tan(defaultDistanceVerticalRadians / 2); + const completeScreenDistancePixels = lensToScreenDistance + lensDistancePixels; + + return { + widthPixels: viewportWidth, + heightPixels: viewportHeight, + defaultDistanceVerticalRadians, + defaultDistanceHorizontalRadians, + lensDistancePixels, + completeScreenDistancePixels, + monitorWrappingScheme: actualWrapScheme(screens, viewportWidth, viewportHeight), + curvedDisplay: false // or true + }; + } + + // Utility constant + readonly property real segmentsPerRadian: 20.0 / degreeToRadian(90.0) + + // FOV conversion functions for flat and curved displays + property var fovConversionFns: ({ + flat: { + centerToFovEdgeDistance: function(centerDistance, fovLength) { + return Math.sqrt(Math.pow(fovLength / 2, 2) + Math.pow(centerDistance, 2)); + }, + fovEdgeToScreenCenterDistance: function(edgeDistance, screenLength) { + return Math.sqrt(Math.pow(edgeDistance, 2) - Math.pow(screenLength / 2, 2)); + }, + lengthToRadians: function(fovRadians, fovLength, screenEdgeDistance, toLength) { + return Math.asin(toLength / 2 / screenEdgeDistance) * 2; + }, + angleToLength: function(fovRadians, fovLength, screenDistance, toAngleOpposite, toAngleAdjacent) { + return toAngleOpposite / toAngleAdjacent * screenDistance; + }, + radiansToSegments: function(screenRadians) { return 1; } + }, + curved: { + centerToFovEdgeDistance: function(centerDistance, fovLength) { + return centerDistance; + }, + fovEdgeToScreenCenterDistance: function(edgeDistance, screenLength) { + return edgeDistance; + }, + lengthToRadians: function(fovRadians, fovLength, screenEdgeDistance, toLength) { + return fovRadians / fovLength * toLength; + }, + angleToLength: function(fovRadians, fovLength, screenDistance, toAngleOpposite, toAngleAdjacent) { + return fovLength / fovRadians * Math.atan2(toAngleOpposite, toAngleAdjacent); + }, + radiansToSegments: function(screenRadians) { + return Math.ceil(screenRadians * segmentsPerRadian); + } + } + }) + + function monitorWrap(cachedMonitorRadians, monitorSpacingPixels, monitorBeginPixel, monitorLengthPixels, lengthToRadianFn) { + var closestWrapPixel = monitorBeginPixel; + var closestWrap = cachedMonitorRadians[monitorBeginPixel]; + if (closestWrap === undefined) { + var keys = Object.keys(cachedMonitorRadians); + closestWrapPixel = keys.reduce(function(previousPixel, currentPixel) { + if (previousPixel === undefined) return currentPixel; + + var currentDelta = currentPixel - monitorBeginPixel; + var previousDelta = previousPixel - monitorBeginPixel; + + if (previousDelta % monitorLengthPixels !== 0) { + if (currentDelta % monitorLengthPixels === 0) return currentPixel; + if (previousDelta < 0 && currentDelta > 0) return currentPixel; + if (Math.abs(currentDelta) < Math.abs(previousDelta)) return currentPixel; + } + return previousPixel; + }, undefined); + closestWrap = cachedMonitorRadians[closestWrapPixel]; + } + + var spacingRadians = lengthToRadianFn(monitorSpacingPixels); + if (closestWrapPixel !== monitorBeginPixel) { + var gapPixels = monitorBeginPixel - closestWrapPixel; + var gapRadians = lengthToRadianFn(gapPixels); + var appliedSpacingRadians = Math.floor(gapPixels / monitorLengthPixels) * spacingRadians; + closestWrap = closestWrap + gapRadians + appliedSpacingRadians; + closestWrapPixel = monitorBeginPixel; + cachedMonitorRadians[closestWrapPixel] = closestWrap; + } + + var monitorRadians = lengthToRadianFn(monitorLengthPixels); + var centerRadians = closestWrap + monitorRadians / 2; + var endRadians = closestWrap + monitorRadians; + + var nextMonitorPixel = monitorBeginPixel + monitorLengthPixels; + if (cachedMonitorRadians[nextMonitorPixel] === undefined) + cachedMonitorRadians[nextMonitorPixel] = endRadians + spacingRadians; + + return { + begin: closestWrap, + center: centerRadians, + end: endRadians + } + } + + function horizontalMonitorSort(monitors) { + return monitors.map(function(monitor, index) { + return { originalIndex: index, monitorDetails: monitor }; + }).sort(function(a, b) { + var aMon = a.monitorDetails; + var bMon = b.monitorDetails; + if (aMon.y !== bMon.y) return aMon.y - bMon.y; + return aMon.x - bMon.x; + }); + } + + function verticalMonitorSort(monitors) { + return monitors.map(function(monitor, index) { + return { originalIndex: index, monitorDetails: monitor }; + }).sort(function(a, b) { + var aMon = a.monitorDetails; + var bMon = b.monitorDetails; + if (aMon.x !== bMon.x) return aMon.x - bMon.x; + return aMon.y - bMon.y; + }); + } + + // fovDetails: { widthPixels, heightPixels, defaultDistanceHorizontalRadians, defaultDistanceVerticalRadians, completeScreenDistancePixels, monitorWrappingScheme, curvedDisplay } + // monitorDetailsList: [{x, y, width, height}, ...] + // monitorSpacing: number (percentage, e.g. 0.05 for 5%) + function monitorsToPlacements(fovDetails, monitorDetailsList, monitorSpacing) { + var monitorPlacements = []; + var cachedMonitorRadians = {}; + + var conversionFns = fovDetails.curvedDisplay ? fovConversionFns.curved : fovConversionFns.flat; + + if (fovDetails.monitorWrappingScheme === 'horizontal') { + var sideEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.widthPixels); + var monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels; + var lengthToRadianFn = function(targetWidth) { + return conversionFns.lengthToRadians( + fovDetails.defaultDistanceHorizontalRadians, + fovDetails.widthPixels, + sideEdgeRadius, + targetWidth + ); + }; + + cachedMonitorRadians[0] = -fovDetails.defaultDistanceHorizontalRadians / 2; + horizontalMonitorSort(monitorDetailsList).forEach(function(obj) { + var monitorDetails = obj.monitorDetails; + var originalIndex = obj.originalIndex; + var monitorWrapDetails = monitorWrap(cachedMonitorRadians, monitorSpacingPixels, monitorDetails.x, monitorDetails.width, lengthToRadianFn); + var monitorCenterRadius = conversionFns.fovEdgeToScreenCenterDistance(sideEdgeRadius, monitorDetails.width); + var upTopPixels = -monitorDetails.y - (monitorDetails.y / fovDetails.heightPixels) * monitorSpacingPixels; + var upCenterOffsetPixels = (monitorDetails.height - fovDetails.heightPixels) / 2; + var upCenterPixels = upTopPixels - upCenterOffsetPixels; + + monitorPlacements.push({ + originalIndex: originalIndex, + centerNoRotate: Qt.vector3d( + monitorCenterRadius, + 0, + upCenterPixels + ), + centerLook: Qt.vector3d( + monitorCenterRadius * Math.cos(monitorWrapDetails.center), + -monitorCenterRadius * Math.sin(monitorWrapDetails.center), + upCenterPixels + ).normalized(), + rotationAngleRadians: { + x: 0, + y: -monitorWrapDetails.center + } + }); + }); + } else if (fovDetails.monitorWrappingScheme === 'vertical') { + var topEdgeRadius = conversionFns.centerToFovEdgeDistance(fovDetails.completeScreenDistancePixels, fovDetails.heightPixels); + var monitorSpacingPixels = monitorSpacing * fovDetails.heightPixels; + var lengthToRadianFn = function(targetHeight) { + return conversionFns.lengthToRadians( + fovDetails.defaultDistanceVerticalRadians, + fovDetails.heightPixels, + topEdgeRadius, + targetHeight + ); + }; + + cachedMonitorRadians[0] = -fovDetails.defaultDistanceVerticalRadians / 2; + verticalMonitorSort(monitorDetailsList).forEach(function(obj) { + var monitorDetails = obj.monitorDetails; + var originalIndex = obj.originalIndex; + var monitorWrapDetails = monitorWrap(cachedMonitorRadians, monitorSpacingPixels, monitorDetails.y, monitorDetails.height, lengthToRadianFn); + var monitorCenterRadius = conversionFns.fovEdgeToScreenCenterDistance(topEdgeRadius, monitorDetails.height); + var westLeftPixels = -monitorDetails.x - (monitorDetails.x / fovDetails.widthPixels) * monitorSpacingPixels; + var westCenterOffsetPixels = (monitorDetails.width - fovDetails.widthPixels) / 2; + var westCenterPixels = westLeftPixels - westCenterOffsetPixels; + + monitorPlacements.push({ + originalIndex: originalIndex, + centerNoRotate: Qt.vector3d( + monitorCenterRadius, + westCenterPixels, + 0 + ), + centerLook: Qt.vector3d( + monitorCenterRadius * Math.cos(monitorWrapDetails.center), + westCenterPixels, + -monitorCenterRadius * Math.sin(monitorWrapDetails.center) + ).normalized(), + rotationAngleRadians: { + x: -monitorWrapDetails.center, + y: 0 + } + }); + }); + } else { + var monitorSpacingPixels = monitorSpacing * fovDetails.widthPixels; + monitorDetailsList.forEach(function(monitorDetails, index) { + var upTopPixels = -monitorDetails.y - (monitorDetails.y / fovDetails.heightPixels) * monitorSpacingPixels; + var westLeftPixels = -monitorDetails.x - (monitorDetails.x / fovDetails.widthPixels) * monitorSpacingPixels; + var westCenterOffsetPixels = (monitorDetails.width - fovDetails.widthPixels) / 2; + var upCenterOffsetPixels = (monitorDetails.height - fovDetails.heightPixels) / 2; + var westCenterPixels = westLeftPixels - westCenterOffsetPixels; + var upCenterPixels = upTopPixels - upCenterOffsetPixels; + + monitorPlacements.push({ + originalIndex: index, + centerNoRotate: Qt.vector3d( + fovDetails.completeScreenDistancePixels, + westCenterPixels, + upCenterPixels + ), + centerLook: Qt.vector3d( + fovDetails.completeScreenDistancePixels, + westCenterPixels, + upCenterPixels + ).normalized(), + rotationAngleRadians: { + x: 0, + y: 0 + } + }); + }); + } + + // put them back in the original monitor order before returning + monitorPlacements.sort(function(a, b) { return a.originalIndex - b.originalIndex; }); + + return monitorPlacements; + } + + // returns how far the look vector is from the center of the monitor, as a percentage of the monitor's dimensions + function getMonitorDistance(fovDetails, lookUpPixels, lookWestPixels, monitorVector, monitorDetails, upAngleToLength, westAngleToLength) { + var vectorUpPixels = upAngleToLength( + fovDetails.defaultDistanceVerticalRadians, + fovDetails.heightPixels, + fovDetails.completeScreenDistancePixels, + monitorVector.z, + monitorVector.x + ); + var upPercentage = Math.abs(lookUpPixels - vectorUpPixels) / monitorDetails.height; + + var vectorWestPixels = westAngleToLength( + fovDetails.defaultDistanceHorizontalRadians, + fovDetails.widthPixels, + fovDetails.completeScreenDistancePixels, + monitorVector.y, + monitorVector.x + ); + var westPercentage = Math.abs(lookWestPixels - vectorWestPixels) / monitorDetails.width; + + // how close we are to any edge is the largest of the two percentages + return Math.max(upPercentage, westPercentage); + } + + function findFocusedMonitor(quaternion, monitorVectors, currentFocusedIndex, smoothFollowEnabled, fovDetails, monitorsDetails) { + var lookVector = Qt.vector3d(1.0, 0.0, 0.0); // NWU vector pointing to the center of the screen + var rotatedLookVector = quaternion.times(lookVector); + + // Use curved or flat conversion functions depending on wrapping scheme + var upConversionFns = fovDetails.monitorWrappingScheme === "vertical" ? fovConversionFns.curved : fovConversionFns.flat; + var lookUpPixels = upConversionFns.angleToLength( + fovDetails.defaultDistanceVerticalRadians, + fovDetails.heightPixels, + fovDetails.completeScreenDistancePixels, + rotatedLookVector.z, + rotatedLookVector.x + ); + var westConversionFns = fovDetails.monitorWrappingScheme === "horizontal" ? fovConversionFns.curved : fovConversionFns.flat; + var lookWestPixels = westConversionFns.angleToLength( + fovDetails.defaultDistanceHorizontalRadians, + fovDetails.widthPixels, + fovDetails.completeScreenDistancePixels, + rotatedLookVector.y, + rotatedLookVector.x + ); + + // Check current focused monitor first + if (currentFocusedIndex !== -1) { + var focusedDistance = getMonitorDistance( + fovDetails, + lookUpPixels, + lookWestPixels, + monitorVectors[currentFocusedIndex], + monitorsDetails[currentFocusedIndex], + upConversionFns.angleToLength, + westConversionFns.angleToLength + ) * effect.focusedDisplayDistance / effect.allDisplaysDistance; + + if (smoothFollowEnabled || focusedDistance < unfocusThreshold) + return currentFocusedIndex; + } + + var closestIndex = -1; + var closestDistance = Number.POSITIVE_INFINITY; + + // Find the closest monitor + for (var i = 0; i < monitorVectors.length; ++i) { + if (i === currentFocusedIndex) + continue; + var distance = getMonitorDistance( + fovDetails, + lookUpPixels, + lookWestPixels, + monitorVectors[i], + monitorsDetails[i], + upConversionFns.angleToLength, + westConversionFns.angleToLength + ); + + if (distance < closestDistance) { + closestIndex = i; + closestDistance = distance; + } + } + + if (smoothFollowEnabled || closestDistance < focusThreshold) + return closestIndex; + + // Unfocus all displays + return -1; + } +} \ No newline at end of file diff --git a/kwin/src/qml/main.qml b/kwin/src/qml/main.qml new file mode 100644 index 0000000..d2ad25a --- /dev/null +++ b/kwin/src/qml/main.qml @@ -0,0 +1,122 @@ +import QtQuick +import QtQuick3D +import org.kde.kwin as KWinComponents +import org.kde.kwin.effect.breezy_desktop + +Item { + id: root + antialiasing: true + focus: false + + readonly property var supportedModels: [ + "VITURE", + "nreal air", + "Air", + "Air 2", + "Air 2 Pro", + "Air 2 Ultra", + "SmartGlasses", // TCL/RayNeo + "Rokid Max", + "Rokid Air" + ] + required property QtObject effect + required property QtObject targetScreen + + property real viewportDiagonalFOVDegrees: effect.diagonalFOV + property var viewportResolution: effect.displayResolution + property var screens: KWinComponents.Workspace.screens + // .filter(function(screen) { + // return supportedModels.includes(screen.model); + // }) + + // x value for placing the viewport in the middle of all screens + property real screensXMid: { + let xMin = Number.MAX_VALUE; + let xMax = Number.MIN_VALUE; + + for (let i = 0; i < screens.length; i++) { + const geometry = screens[i].geometry; + xMin = Math.min(xMin, geometry.x); + xMax = Math.max(xMax, geometry.x + geometry.width); + } + + return (xMin + xMax) / 2 - (viewportResolution[0] / 2); + } + + // y value for placing the viewport in the middle of all screens + property real screensYMid: { + let yMin = Number.MAX_VALUE; + let yMax = Number.MIN_VALUE; + + for (let i = 0; i < screens.length; i++) { + const geometry = screens[i].geometry; + yMin = Math.min(yMin, geometry.y); + yMax = Math.max(yMax, geometry.y + geometry.height); + } + + return (yMin + yMax) / 2 - (viewportResolution[1] / 2); + } + + Displays { + id: displays + } + + property var fovDetails: displays.fovDetails(screens, viewportResolution[0], viewportResolution[1], viewportDiagonalFOVDegrees, effect.lensDistanceRatio) + + property var monitorPlacements: { + const adjustedGeometries = screens.map(screen => { + const g = screen.geometry; + return { + x: g.x - screensXMid, + y: g.y - screensYMid, + width: g.width, + height: g.height + }; + }); + return displays.monitorsToPlacements(fovDetails, adjustedGeometries, effect.displaySpacing); + } + + Component { + id: desktopViewComponent + DesktopView { + screen: root.targetScreen + width: root.targetScreen.geometry.width + height: root.targetScreen.geometry.height + } + } + + Component { + id: view3DComponent + View3D { + anchors.fill: parent + environment: SceneEnvironment { + antialiasingMode: SceneEnvironment.SSAA + antialiasingQuality: SceneEnvironment.VeryHigh + } + + PerspectiveCamera { + id: camera + } + + BreezyDesktop { + id: breezyDesktop + } + + CameraController { + id: cameraController + anchors.fill: parent + camera: camera + } + } + } + + Loader { + id: viewLoader + anchors.fill: parent + } + + Component.onCompleted: { + const targetScreenSupported = supportedModels.some(model => root.targetScreen.model.endsWith(model)); + viewLoader.sourceComponent = targetScreenSupported ? view3DComponent : desktopViewComponent; + } +} diff --git a/kwin/src/xrdriveripc/CMakeLists.txt b/kwin/src/xrdriveripc/CMakeLists.txt new file mode 100644 index 0000000..81887b6 --- /dev/null +++ b/kwin/src/xrdriveripc/CMakeLists.txt @@ -0,0 +1,26 @@ +add_library(xr_driver_ipc STATIC + xrdriveripc.cpp +) + +# Ensure position independent code so the static archive can link into the KWin effect plugin (a shared module) +set_target_properties(xr_driver_ipc PROPERTIES POSITION_INDEPENDENT_CODE ON) + +# Generate an export header so symbols can be visible outside the shared lib +include(GenerateExportHeader) +generate_export_header(xr_driver_ipc EXPORT_FILE_NAME xr_driver_ipc_export.h) + +target_include_directories(xr_driver_ipc + PUBLIC + ${CMAKE_CURRENT_BINARY_DIR} # for generated export header +) + +target_compile_options(xr_driver_ipc PRIVATE + $<$:/EHsc> + $<$>:-fexceptions> +) + +target_link_libraries(xr_driver_ipc + PRIVATE Qt6::Core +) + +install(FILES xrdriveripc.py xrdriveripc_runner.py DESTINATION ${KDE_INSTALL_DATADIR}/kwin/effects/breezy_desktop) \ No newline at end of file diff --git a/kwin/src/xrdriveripc/xrdriveripc.cpp b/kwin/src/xrdriveripc/xrdriveripc.cpp new file mode 100644 index 0000000..bad7835 --- /dev/null +++ b/kwin/src/xrdriveripc/xrdriveripc.cpp @@ -0,0 +1,142 @@ +// New implementation using QProcess to call python +#include "xrdriveripc.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +XRDriverIPC &XRDriverIPC::instance() { + static XRDriverIPC inst; + if (!inst.m_initialized) { + QString installedFile = QStandardPaths::locate( + QStandardPaths::GenericDataLocation, + QStringLiteral("kwin/effects/breezy_desktop/xrdriveripc.py"), + QStandardPaths::LocateFile); + if (installedFile.isEmpty()) { + throw std::runtime_error("Cannot locate kwin/effects/breezy_desktop/xrdriveripc.py"); + } + inst.m_pythonDir = QFileInfo(installedFile).path(); + inst.m_initialized = true; + } + return inst; +} + +std::string XRDriverIPC::string(const XRDict &dict, const std::string &key) { + auto it = dict.find(key); + if (it != dict.end() && std::holds_alternative(it->second)) { + return std::get(it->second); + } + return {}; +} + +std::string XRDriverIPC::configHome() const { + QString configHome = QString::fromUtf8(qgetenv("XDG_CONFIG_HOME")); + if (configHome.isEmpty()) { + QString homeDir = QString::fromUtf8(qgetenv("HOME")); + configHome = homeDir + QStringLiteral("/.config"); + } + return configHome.toStdString(); +} + +QByteArray XRDriverIPC::invokePython(const QString &method, + const QByteArray &payloadJson, + const QString &singleArg) const { + QProcess proc; + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("BREEZY_METHOD"), method); + env.insert(QStringLiteral("BREEZY_CONFIG_HOME"), QString::fromStdString(configHome())); + if (!singleArg.isEmpty()) env.insert(QStringLiteral("BREEZY_ARG"), singleArg); + if (!payloadJson.isEmpty()) env.insert(QStringLiteral("BREEZY_PAYLOAD"), QString::fromUtf8(payloadJson)); + proc.setProcessEnvironment(env); + // Expect xrdriveripc_runner.py to reside in the same directory as xrdriveripc.py (m_pythonDir) + QString wrapperPath = m_pythonDir + QStringLiteral("/xrdriveripc_runner.py"); + proc.start(QStringLiteral("python3"), QStringList() << wrapperPath); + if (!proc.waitForStarted(5000)) { + std::cerr << "Failed to start python process" << std::endl; + return {}; + } + proc.closeWriteChannel(); + if (!proc.waitForFinished(15000)) { + proc.kill(); + std::cerr << "Python process timeout" << std::endl; + return {}; + } + if (proc.exitStatus() != QProcess::NormalExit || proc.exitCode() != 0) { + std::cerr << "Python process failed (" << proc.exitCode() << "):\n" + << proc.readAllStandardError().toStdString() << std::endl; + return {}; + } + return proc.readAllStandardOutput().trimmed(); +} + +static XRDict jsonToXRDict(const QJsonObject &obj) { + XRDict out; + for (auto it = obj.begin(); it != obj.end(); ++it) { + const QString &k = it.key(); + const QJsonValue &v = it.value(); + if (v.isBool()) out[k.toStdString()] = v.toBool(); + else if (v.isDouble() && std::floor(v.toDouble()) == v.toDouble()) + out[k.toStdString()] = (int)v.toDouble(); + else if (v.isDouble()) out[k.toStdString()] = v.toDouble(); + else if (v.isString()) out[k.toStdString()] = v.toString().toStdString(); + else out[k.toStdString()] = std::monostate{}; + } + return out; +} + +std::optional XRDriverIPC::retrieveConfig() { + QByteArray out = invokePython(QStringLiteral("retrieve_config"), {}, QStringLiteral("1")); + if (out.isEmpty()) return std::nullopt; + QJsonParseError err; auto doc = QJsonDocument::fromJson(out, &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) return std::nullopt; + return jsonToXRDict(doc.object()); +} + +std::optional XRDriverIPC::retrieveDriverState() { + QByteArray out = invokePython(QStringLiteral("retrieve_driver_state"), {}, {}); + if (out.isEmpty()) return std::nullopt; + QJsonParseError err; auto doc = QJsonDocument::fromJson(out, &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) return std::nullopt; + return jsonToXRDict(doc.object()); +} + +bool XRDriverIPC::writeConfig(const XRDict &configUpdate) { + QJsonObject obj; + for (const auto &kv : configUpdate) { + const std::string &k = kv.first; const XRValue &v = kv.second; + if (std::holds_alternative(v)) obj.insert(QString::fromStdString(k), std::get(v)); + else if (std::holds_alternative(v)) obj.insert(QString::fromStdString(k), std::get(v)); + else if (std::holds_alternative(v)) obj.insert(QString::fromStdString(k), std::get(v)); + else if (std::holds_alternative(v)) obj.insert(QString::fromStdString(k), QString::fromStdString(std::get(v))); + } + QByteArray payload = QJsonDocument(obj).toJson(QJsonDocument::Compact); + QByteArray out = invokePython(QStringLiteral("write_config"), payload, {}); + return !out.isEmpty(); +} + +bool XRDriverIPC::writeControlFlags(const std::map &flags) { + QJsonObject obj; for (const auto &kv : flags) obj.insert(QString::fromStdString(kv.first), kv.second); + QByteArray payload = QJsonDocument(obj).toJson(QJsonDocument::Compact); + QByteArray out = invokePython(QStringLiteral("write_control_flags"), payload, {}); + return !out.isEmpty(); +} + +bool XRDriverIPC::requestToken(const std::string &email) { + QByteArray out = invokePython(QStringLiteral("request_token"), {}, QString::fromStdString(email)); + if (out.isEmpty()) return false; + auto value = QJsonValue(QString::fromStdString(out.toStdString())); + return value.isBool() ? value.toBool() : false; +} + +bool XRDriverIPC::verifyToken(const std::string &token) { + QByteArray out = invokePython(QStringLiteral("verify_token"), {}, QString::fromStdString(token)); + if (out.isEmpty()) return false; + auto value = QJsonValue(QString::fromStdString(out.toStdString())); + return value.isBool() ? value.toBool() : false; +} diff --git a/kwin/src/xrdriveripc/xrdriveripc.h b/kwin/src/xrdriveripc/xrdriveripc.h new file mode 100644 index 0000000..ff6d654 --- /dev/null +++ b/kwin/src/xrdriveripc/xrdriveripc.h @@ -0,0 +1,111 @@ +// C++ bridge now invoking xrdriveripc via external python process +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +// Export header generated by CMake (GenerateExportHeader) +#ifdef __has_include +# if __has_include("xr_driver_ipc_export.h") +# include "xr_driver_ipc_export.h" +# endif +#endif + +#ifndef XR_DRIVER_IPC_EXPORT +# define XR_DRIVER_IPC_EXPORT __attribute__((visibility("default"))) +#endif + +namespace XRStateEntry { + inline constexpr const char *Heartbeat = "heartbeat"; + inline constexpr const char *HardwareId = "hardware_id"; + inline constexpr const char *ConnectedDeviceBrand = "connected_device_brand"; + inline constexpr const char *ConnectedDeviceModel = "connected_device_model"; + inline constexpr const char *MagnetSupported = "magnet_supported"; + inline constexpr const char *MagnetCalibrationType = "magnet_calibration_type"; + inline constexpr const char *UsingMagnet = "using_magnet"; + inline constexpr const char *MagnetStale = "magnet_stale"; + inline constexpr const char *MagnetCalibrating = "magnet_calibrating"; + inline constexpr const char *GyroCalibrating = "gyro_calibrating"; + inline constexpr const char *AccelCalibrating = "accel_calibrating"; + inline constexpr const char *SbsModeEnabled = "sbs_mode_enabled"; + inline constexpr const char *SbsModeSupported = "sbs_mode_supported"; + inline constexpr const char *FirmwareUpdateRecommended = "firmware_update_recommended"; + inline constexpr const char *BreezyDesktopSmoothFollowEnabled = "breezy_desktop_smooth_follow_enabled"; + inline constexpr const char *IsGamescopeReshadeIPCConnected = "is_gamescope_reshade_ipc_connected"; + inline constexpr const char *UiView = "ui_view_enabled"; + + namespace UIView { + inline constexpr const char *DriverRunning = "driver_running"; + + namespace License { + inline constexpr const char *Tiers = "tiers"; + inline constexpr const char *Features = "features"; + inline constexpr const char *HardwareId = "hardware_id"; + inline constexpr const char *ConfirmedToken = "confirmed_token"; + inline constexpr const char *ActionNeeded = "action_needed"; + inline constexpr const char *EnabledFeatures = "enabled_features"; + } + } +} + +namespace XRConfigEntry { + inline constexpr const char *Disabled = "disabled"; + inline constexpr const char *GamescopeReshadeWaylandDisabled = "gamescope_reshade_wayland_disabled"; + inline constexpr const char *OutputMode = "output_mode"; + inline constexpr const char *ExternalMode = "external_mode"; + inline constexpr const char *MouseSensitivity = "mouse_sensitivity"; + inline constexpr const char *DisplayZoom = "display_zoom"; + inline constexpr const char *LookAhead = "look_ahead"; + inline constexpr const char *SbsDisplaySize = "sbs_display_size"; + inline constexpr const char *SbsDisplayDistance = "sbs_display_distance"; + inline constexpr const char *SbsContent = "sbs_content"; + inline constexpr const char *SbsModeStretched = "sbs_mode_stretched"; + inline constexpr const char *SideviewPosition = "sideview_position"; + inline constexpr const char *SideviewDisplaySize = "sideview_display_size"; + inline constexpr const char *VirtualDisplaySmoothFollowEnabled = "virtual_display_smooth_follow_enabled"; + inline constexpr const char *SideviewSmoothFollowEnabled = "sideview_smooth_follow_enabled"; + inline constexpr const char *SideviewFollowThreshold = "sideview_follow_threshold"; + inline constexpr const char *CurvedDisplay = "curved_display"; + inline constexpr const char *MultiTapEnabled = "multi_tap_enabled"; + inline constexpr const char *SmoothFollowTrackRoll = "smooth_follow_track_roll"; + inline constexpr const char *SmoothFollowTrackPitch = "smooth_follow_track_pitch"; + inline constexpr const char *SmoothFollowTrackYaw = "smooth_follow_track_yaw"; + inline constexpr const char *Debug = "debug"; +} + +// Simple variant type for config/state key values we care about +using XRValue = std::variant; +using XRDict = std::map; + +class XR_DRIVER_IPC_EXPORT XRDriverIPC { +public: + static XRDriverIPC &instance(); + static std::string string(const XRDict &dict, const std::string &key); + + std::optional retrieveConfig(); + std::optional retrieveDriverState(); + bool writeConfig(const XRDict &configUpdate); + bool writeControlFlags(const std::map &flags); + bool requestToken(const std::string &email); + bool verifyToken(const std::string &token); + + +private: + XRDriverIPC() = default; + ~XRDriverIPC() = default; + XRDriverIPC(const XRDriverIPC&) = delete; + XRDriverIPC& operator=(const XRDriverIPC&) = delete; + + std::string configHome() const; + QByteArray invokePython(const QString &method, + const QByteArray &payloadJson, + const QString &singleArg) const; + + bool m_initialized = false; + QString m_pythonDir; // directory containing xrdriveripc.py +}; diff --git a/kwin/src/xrdriveripc/xrdriveripc_runner.py b/kwin/src/xrdriveripc/xrdriveripc_runner.py new file mode 100644 index 0000000..748bc70 --- /dev/null +++ b/kwin/src/xrdriveripc/xrdriveripc_runner.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Wrapper script invoked by xrdriveripc.cpp via QProcess. + +It reads environment variables to determine which XRDriverIPC method to call +and prints the JSON-serialized result to stdout, mirroring the prior inline +python one-liner implementation. +""" + +from __future__ import annotations + +import json +import os +import sys +import traceback + + +def main() -> int: + # Ensure the current directory (where xrdriveripc.py lives) is in sys.path + script_dir = os.path.dirname(os.path.abspath(__file__)) + if script_dir not in sys.path: + sys.path.insert(0, script_dir) + + try: + import xrdriveripc # type: ignore + except Exception as e: # pragma: no cover - import failure path + print("Failed to import xrdriveripc: %s" % e, file=sys.stderr) + return 2 + + method = os.environ.get("BREEZY_METHOD") + if not method: + print("BREEZY_METHOD not set", file=sys.stderr) + return 2 + + config_home = os.environ.get("BREEZY_CONFIG_HOME") + inst = xrdriveripc.XRDriverIPC(config_home=config_home) + + arg = os.environ.get("BREEZY_ARG") + payload_raw = os.environ.get("BREEZY_PAYLOAD") + + # Dispatch replicating previous inline logic + try: + if method == "retrieve_config": + res = getattr(inst, method)(int(arg) if arg else 1) + elif method in ("write_config", "write_control_flags") and payload_raw: + res = getattr(inst, method)(json.loads(payload_raw)) + elif method in ("request_token", "verify_token") and arg: + res = getattr(inst, method)(arg) + else: + res = getattr(inst, method)() + except Exception: # pragma: no cover - runtime failure path + traceback.print_exc() + return 3 + + try: + print(json.dumps(res)) + except Exception: # pragma: no cover + traceback.print_exc() + return 3 + return 0 + + +if __name__ == "__main__": # pragma: no cover + sys.exit(main()) From b266a20949d0791d2cfcc9ad22af41ac8fde78a8 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:29:19 -0700 Subject: [PATCH 10/41] Add KWin UI tab for license details --- .gitignore | 2 + bin/package_kwin | 1 + kwin/src/CMakeLists.txt | 18 +- kwin/src/breezydesktopeffect.cpp | 10 +- kwin/src/kcm/CMakeLists.txt | 5 + kwin/src/kcm/breezydesktopeffectkcm.cpp | 187 ++++++++++++++++++++- kwin/src/kcm/breezydesktopeffectkcm.h | 5 + kwin/src/kcm/breezydesktopeffectkcm.ui | 161 ++++++++++++++++++ kwin/src/xrdriveripc/xrdriveripc.cpp | 51 ++---- kwin/src/xrdriveripc/xrdriveripc.h | 18 +- kwin/src/xrdriveripc/xrdriveripc_runner.py | 9 +- ui/src/licensefeaturerow.py | 5 +- 12 files changed, 399 insertions(+), 73 deletions(-) diff --git a/.gitignore b/.gitignore index ae97f4c..4eb9fcf 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ gschemas.compiled out/ *.po~ kwin/src/xrdriveripc/xrdriveripc.py +kwin/VERSION +kwin/build-test/ \ No newline at end of file diff --git a/bin/package_kwin b/bin/package_kwin index b30f076..9d71536 100755 --- a/bin/package_kwin +++ b/bin/package_kwin @@ -62,6 +62,7 @@ cp $XR_DRIVER_DIR/bin/xr_driver_setup $PACKAGE_DIR/bin # alternative to symlinking, since the Docker build can't resolve to the parent directory # this file is in .gitignore so it doesn't get duplicated cp ui/modules/PyXRLinuxDriverIPC/xrdriveripc.py $KWIN_DIR/src/xrdriveripc/xrdriveripc.py +cp VERSION $KWIN_DIR pushd $KWIN_DIR docker-build/init.sh diff --git a/kwin/src/CMakeLists.txt b/kwin/src/CMakeLists.txt index 619ee0b..399840d 100644 --- a/kwin/src/CMakeLists.txt +++ b/kwin/src/CMakeLists.txt @@ -1,4 +1,11 @@ add_subdirectory(xrdriveripc) + +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/../VERSION" BREEZY_DESKTOP_VERSION_RAW) +if(NOT BREEZY_DESKTOP_VERSION_RAW) + set(BREEZY_DESKTOP_VERSION_RAW "dev") +endif() +string(STRIP "${BREEZY_DESKTOP_VERSION_RAW}" BREEZY_DESKTOP_VERSION) + add_subdirectory(kcm) kcoreaddons_add_plugin(breezy_desktop INSTALL_NAMESPACE "kwin/effects/plugins/") @@ -32,11 +39,12 @@ math(EXPR KWIN_VERSION_ENCODED "${KWIN_VERSION_MAJOR} * 10000 + ${KWIN_VERSION_M # Export as compile definitions. Keep the original string macro as well. target_compile_definitions(breezy_desktop PRIVATE - KWIN_VERSION_STR=\"${KWin_VERSION}\" - KWIN_VERSION_MAJOR=${KWIN_VERSION_MAJOR} - KWIN_VERSION_MINOR=${KWIN_VERSION_MINOR} - KWIN_VERSION_PATCH=${KWIN_VERSION_PATCH} - KWIN_VERSION_ENCODED=${KWIN_VERSION_ENCODED} + KWIN_VERSION_STR=\"${KWin_VERSION}\" + KWIN_VERSION_MAJOR=${KWIN_VERSION_MAJOR} + KWIN_VERSION_MINOR=${KWIN_VERSION_MINOR} + KWIN_VERSION_PATCH=${KWIN_VERSION_PATCH} + KWIN_VERSION_ENCODED=${KWIN_VERSION_ENCODED} + BREEZY_DESKTOP_VERSION_STR=\"${BREEZY_DESKTOP_VERSION}\" ) target_include_directories(breezy_desktop PRIVATE /usr/include/kwin) target_include_directories(breezy_desktop PRIVATE xrdriveripc) diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index 6288baa..aa1c7a5 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -221,11 +221,11 @@ void BreezyDesktopEffect::deactivate() void BreezyDesktopEffect::enableDriver() { qCCritical(KWIN_XR) << "\t\t\tBreezy - enableDriver"; - XRDriverIPC::instance().writeConfig({ - {"disabled", false}, - {"output_mode", "external_only"}, - {"external_mode", "breezy_desktop"} - }); + QJsonObject obj; + obj.insert(QStringLiteral("disabled"), false); + obj.insert(QStringLiteral("output_mode"), QStringLiteral("external_only")); + obj.insert(QStringLiteral("external_mode"), QStringLiteral("breezy_desktop")); + XRDriverIPC::instance().writeConfig(obj); } void BreezyDesktopEffect::realDeactivate() diff --git a/kwin/src/kcm/CMakeLists.txt b/kwin/src/kcm/CMakeLists.txt index 34e048e..87ceacc 100644 --- a/kwin/src/kcm/CMakeLists.txt +++ b/kwin/src/kcm/CMakeLists.txt @@ -16,3 +16,8 @@ target_link_libraries(breezy_desktop_config xr_driver_ipc ) + +# Ensure the version macro is available to the KCM as well (defined in parent CMakeLists) +if(BREEZY_DESKTOP_VERSION) + target_compile_definitions(breezy_desktop_config PRIVATE BREEZY_DESKTOP_VERSION_STR=\"${BREEZY_DESKTOP_VERSION}\") +endif() diff --git a/kwin/src/kcm/breezydesktopeffectkcm.cpp b/kwin/src/kcm/breezydesktopeffectkcm.cpp index fc0c10d..105163f 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.cpp +++ b/kwin/src/kcm/breezydesktopeffectkcm.cpp @@ -14,8 +14,17 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include Q_LOGGING_CATEGORY(KWIN_XR, "kwin.xr") @@ -72,6 +81,44 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu connect(ui.kcfg_FocusedDisplayDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_AllDisplaysDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_DisplaySpacing, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); + + if (auto label = widget()->findChild("labelAppNameVersion")) { + label->setText(QStringLiteral("Breezy Desktop - v%1").arg(QLatin1String(BREEZY_DESKTOP_VERSION_STR))); + } + + if (auto btnEmail = widget()->findChild("buttonSubmitEmail")) { + connect(btnEmail, &QPushButton::clicked, this, [this]() { + auto edit = widget()->findChild("lineEditLicenseEmail"); + auto labelStatus = widget()->findChild("labelEmailStatus"); + if (!edit || edit->text().trimmed().isEmpty() || !labelStatus) return; + setRequestInProgress({edit, sender()}, true); + labelStatus->setVisible(false); + bool success = XRDriverIPC::instance().requestToken(edit->text().trimmed().toStdString()); + showStatus(labelStatus, success, success ? tr("Request sent. Check your email for instructions.") : tr("Failed to send request.")); + setRequestInProgress({edit, sender()}, false); + }); + if (auto emailEdit = widget()->findChild("lineEditLicenseEmail")) { + emailEdit->installEventFilter(this); + } + } + if (auto btnToken = widget()->findChild("buttonSubmitToken")) { + connect(btnToken, &QPushButton::clicked, this, [this]() { + auto edit = widget()->findChild("lineEditLicenseToken"); + auto labelStatus = widget()->findChild("labelTokenStatus"); + if (!edit || edit->text().trimmed().isEmpty() || !labelStatus) return; + setRequestInProgress({edit, sender()}, true); + labelStatus->setVisible(false); + bool success = XRDriverIPC::instance().verifyToken(edit->text().trimmed().toStdString()); + if (success) { + XRDriverIPC::instance().writeControlFlags({{"refresh_device_license", true}}); + } + showStatus(labelStatus, success, success ? tr("Your license has been refreshed.") : tr("Invalid or expired token.")); + setRequestInProgress({edit, sender()}, false); + }); + if (auto tokenEdit = widget()->findChild("lineEditLicenseToken")) { + tokenEdit->installEventFilter(this); + } + } } BreezyDesktopEffectConfig::~BreezyDesktopEffectConfig() @@ -133,11 +180,12 @@ void BreezyDesktopEffectConfig::updateUnmanagedState() void BreezyDesktopEffectConfig::pollDriverState() { auto &bridge = XRDriverIPC::instance(); - auto stateOpt = bridge.retrieveDriverState(); - const XRDict &state = stateOpt.value(); + auto stateJsonOpt = bridge.retrieveDriverState(); + if (!stateJsonOpt) return; + auto stateJson = stateJsonOpt.value(); - m_connectedDeviceBrand = QString::fromStdString(XRDriverIPC::string(state, XRStateEntry::ConnectedDeviceBrand)); - m_connectedDeviceModel = QString::fromStdString(XRDriverIPC::string(state, XRStateEntry::ConnectedDeviceModel)); + m_connectedDeviceBrand = stateJson.value(QStringLiteral("connected_device_brand")).toString(); + m_connectedDeviceModel = stateJson.value(QStringLiteral("connected_device_model")).toString(); const bool wasDeviceConnected = m_deviceConnected; m_deviceConnected = !m_connectedDeviceBrand.isEmpty() && !m_connectedDeviceModel.isEmpty(); @@ -146,6 +194,137 @@ void BreezyDesktopEffectConfig::pollDriverState() QStringLiteral("%1 %2 connected").arg(m_connectedDeviceBrand, m_connectedDeviceModel) : QStringLiteral("No device connected")); } + + refreshLicenseUi(stateJson); +} + +void BreezyDesktopEffectConfig::showStatus(QLabel *label, bool success, const QString &message) { + if (!label) return; + QPalette pal = label->palette(); + pal.setColor(QPalette::WindowText, success ? QColor(Qt::darkGreen) : QColor(Qt::red)); + label->setPalette(pal); + label->setText(message); + label->setVisible(true); +} + +void BreezyDesktopEffectConfig::setRequestInProgress(std::initializer_list widgets, bool inProgress) { + for (auto *obj : widgets) { + if (auto *w = qobject_cast(obj)) { + w->setEnabled(!inProgress); + } + } +} + +bool BreezyDesktopEffectConfig::eventFilter(QObject *watched, QEvent *event) { + if (event->type() == QEvent::KeyPress) { + auto *ke = static_cast(event); + if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) { + if (auto *edit = qobject_cast(watched)) { + // Determine which button to invoke + QString objName = edit->objectName(); + QString buttonName; + if (objName == QLatin1String("lineEditLicenseEmail")) buttonName = QStringLiteral("buttonSubmitEmail"); + else if (objName == QLatin1String("lineEditLicenseToken")) buttonName = QStringLiteral("buttonSubmitToken"); + if (!buttonName.isEmpty()) { + if (auto btn = widget()->findChild(buttonName)) { + // Trigger click but stop further propagation so dialog doesn't accept/close + QMetaObject::invokeMethod(btn, "click", Qt::QueuedConnection); + event->accept(); + return true; // eat event + } + } + } + } + } + return KCModule::eventFilter(watched, event); +} + +static QString secondsToRemainingString(qint64 secs) { + if (secs <= 0) return {}; + + if (secs / 60 < 60) { + return QObject::tr("less than an hour"); + } + if (secs / 3600 < 24) { + qint64 hours = secs / 3600; + if (hours == 1) return QObject::tr("1 hour"); + return QObject::tr("%1 hours").arg(hours); + } + if ((secs / 86400) < 30 ) { + qint64 days = secs / 86400; + if (days == 1) return QObject::tr("1 day"); + return QObject::tr("%1 days").arg(days); + } + return {}; +} + +void BreezyDesktopEffectConfig::refreshLicenseUi(const QJsonObject &rootObj) { + auto tab = widget()->findChild("tabLicenseDetails"); + if (!tab) return; + auto labelSummary = tab->findChild("labelLicenseSummary"); + if (!labelSummary) return; + + QString status = tr("disabled"); + QString renewalDescriptor = QStringLiteral(""); + auto uiView = rootObj.value(QStringLiteral("ui_view")).toObject(); + auto license = uiView.value(QStringLiteral("license")).toObject(); + bool warningState = true; + if (!license.isEmpty()) { + auto tiers = license.value(QStringLiteral("tiers")).toObject(); + QJsonValue prodTier = tiers.value(QStringLiteral("subscriber")); + QJsonObject prodTierObj = prodTier.isUndefined() ? QJsonObject() : prodTier.toObject(); + + auto features = license.value(QStringLiteral("features")).toObject(); + QJsonValue prodFeature = features.value(QStringLiteral("productivity_basic")); + QJsonObject prodFeatureObj = prodFeature.isUndefined() ? QJsonObject() : prodFeature.toObject(); + if (!prodTierObj.isEmpty() && !prodFeatureObj.isEmpty()) { + const QString activePeriod = prodTierObj.value(QStringLiteral("active_period")).toString(); + const bool isActive = !activePeriod.isEmpty(); + if (isActive) { + status = tr("active"); + + QString periodDescriptor = activePeriod.contains(QStringLiteral("lifetime"), Qt::CaseInsensitive) ? + tr("lifetime") : + tr("%1 license").arg(activePeriod); + + QString timeDescriptor; + auto secsVal = prodTierObj.value(QStringLiteral("funds_needed_in_seconds")); + if (secsVal.isDouble()) { + qint64 secs = static_cast(secsVal.toDouble()); + QString remaining = secondsToRemainingString(secs); + if (!remaining.isEmpty()) { + timeDescriptor = tr("%1 remaining").arg(remaining); + } + } + renewalDescriptor = tr(" (%1)").arg(periodDescriptor); + warningState = !timeDescriptor.isEmpty(); + if (warningState) { + auto fundsNeeded = prodTierObj.value(QStringLiteral("funds_needed_by_period")).toObject().value(activePeriod).toDouble(); + if (fundsNeeded > 0.0) { + QString fundsNeededDescriptor = tr("$%1 USD to renew").arg(fundsNeeded); + renewalDescriptor = tr(" (%1, %2, %3)").arg(periodDescriptor, fundsNeededDescriptor, timeDescriptor); + } + } + } else { + QJsonValue isEnabled = prodFeatureObj.value(QStringLiteral("is_enabled")); + QJsonValue isTrial = prodFeatureObj.value(QStringLiteral("is_trial")); + if (isEnabled.toBool() && isTrial.toBool()) { + status = tr("in trial"); + auto secsVal = prodFeatureObj.value(QStringLiteral("funds_needed_in_seconds")); + if (secsVal.isDouble()) { + qint64 secs = static_cast(secsVal.toDouble()); + QString remaining = secondsToRemainingString(secs); + warningState = !remaining.isEmpty(); + if (warningState) { + QString timeDescriptor = tr("%1 remaining").arg(remaining); + renewalDescriptor = tr(" (%1)").arg(timeDescriptor); + } + } + } + } + } + } + labelSummary->setText(tr("Productivity Tier features are %1%2").arg(status, renewalDescriptor)); } #include "breezydesktopeffectkcm.moc" \ No newline at end of file diff --git a/kwin/src/kcm/breezydesktopeffectkcm.h b/kwin/src/kcm/breezydesktopeffectkcm.h index 13d354c..d717916 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.h +++ b/kwin/src/kcm/breezydesktopeffectkcm.h @@ -30,6 +30,10 @@ private: void updateConfigFromUi(); void updateUnmanagedState(); void pollDriverState(); + void refreshLicenseUi(const QJsonObject &rootObj); + void showStatus(QLabel *label, bool success, const QString &message); + void setRequestInProgress(std::initializer_list widgets, bool inProgress); + bool eventFilter(QObject *watched, QEvent *event) override; ::Ui::BreezyDesktopEffectConfig ui; @@ -39,4 +43,5 @@ private: QString m_connectedDeviceBrand; QString m_connectedDeviceModel; QTimer m_statePollTimer; // periodic driver state polling + bool m_licenseLoading = false; }; diff --git a/kwin/src/kcm/breezydesktopeffectkcm.ui b/kwin/src/kcm/breezydesktopeffectkcm.ui index c431a53..0930c71 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.ui +++ b/kwin/src/kcm/breezydesktopeffectkcm.ui @@ -141,6 +141,167 @@ + + + &License Details + + + + + + + + + true + + + true + + + + + + + Request a token + + + + + + you@example.com + + + + + + + Submit + + + + + + + + + + false + + + true + + + + + + + + + + Verify token + + + + + + + + + Verify + + + + + + + + + + false + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + &About + + + + + + Breezy Desktop Effect - v0.0.0 + + + Qt::AlignHCenter|Qt::AlignVCenter + + + + 14 + 75 + true + + + + + + + + Author: Wayne Heaney <wayne@xronlinux.com> + + + Qt::AlignHCenter|Qt::AlignVCenter + + + + + + + License: GPL-3.0 + + + Qt::AlignHCenter|Qt::AlignVCenter + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + diff --git a/kwin/src/xrdriveripc/xrdriveripc.cpp b/kwin/src/xrdriveripc/xrdriveripc.cpp index bad7835..168043d 100644 --- a/kwin/src/xrdriveripc/xrdriveripc.cpp +++ b/kwin/src/xrdriveripc/xrdriveripc.cpp @@ -27,14 +27,6 @@ XRDriverIPC &XRDriverIPC::instance() { return inst; } -std::string XRDriverIPC::string(const XRDict &dict, const std::string &key) { - auto it = dict.find(key); - if (it != dict.end() && std::holds_alternative(it->second)) { - return std::get(it->second); - } - return {}; -} - std::string XRDriverIPC::configHome() const { QString configHome = QString::fromUtf8(qgetenv("XDG_CONFIG_HOME")); if (configHome.isEmpty()) { @@ -75,47 +67,24 @@ QByteArray XRDriverIPC::invokePython(const QString &method, return proc.readAllStandardOutput().trimmed(); } -static XRDict jsonToXRDict(const QJsonObject &obj) { - XRDict out; - for (auto it = obj.begin(); it != obj.end(); ++it) { - const QString &k = it.key(); - const QJsonValue &v = it.value(); - if (v.isBool()) out[k.toStdString()] = v.toBool(); - else if (v.isDouble() && std::floor(v.toDouble()) == v.toDouble()) - out[k.toStdString()] = (int)v.toDouble(); - else if (v.isDouble()) out[k.toStdString()] = v.toDouble(); - else if (v.isString()) out[k.toStdString()] = v.toString().toStdString(); - else out[k.toStdString()] = std::monostate{}; - } - return out; -} - -std::optional XRDriverIPC::retrieveConfig() { +std::optional XRDriverIPC::retrieveConfig() { QByteArray out = invokePython(QStringLiteral("retrieve_config"), {}, QStringLiteral("1")); if (out.isEmpty()) return std::nullopt; QJsonParseError err; auto doc = QJsonDocument::fromJson(out, &err); if (err.error != QJsonParseError::NoError || !doc.isObject()) return std::nullopt; - return jsonToXRDict(doc.object()); + return doc.object(); } -std::optional XRDriverIPC::retrieveDriverState() { +std::optional XRDriverIPC::retrieveDriverState() { QByteArray out = invokePython(QStringLiteral("retrieve_driver_state"), {}, {}); if (out.isEmpty()) return std::nullopt; QJsonParseError err; auto doc = QJsonDocument::fromJson(out, &err); if (err.error != QJsonParseError::NoError || !doc.isObject()) return std::nullopt; - return jsonToXRDict(doc.object()); + return doc.object(); } -bool XRDriverIPC::writeConfig(const XRDict &configUpdate) { - QJsonObject obj; - for (const auto &kv : configUpdate) { - const std::string &k = kv.first; const XRValue &v = kv.second; - if (std::holds_alternative(v)) obj.insert(QString::fromStdString(k), std::get(v)); - else if (std::holds_alternative(v)) obj.insert(QString::fromStdString(k), std::get(v)); - else if (std::holds_alternative(v)) obj.insert(QString::fromStdString(k), std::get(v)); - else if (std::holds_alternative(v)) obj.insert(QString::fromStdString(k), QString::fromStdString(std::get(v))); - } - QByteArray payload = QJsonDocument(obj).toJson(QJsonDocument::Compact); +bool XRDriverIPC::writeConfig(const QJsonObject &configUpdate) { + QByteArray payload = QJsonDocument(configUpdate).toJson(QJsonDocument::Compact); QByteArray out = invokePython(QStringLiteral("write_config"), payload, {}); return !out.isEmpty(); } @@ -130,13 +99,13 @@ bool XRDriverIPC::writeControlFlags(const std::map &flags) { bool XRDriverIPC::requestToken(const std::string &email) { QByteArray out = invokePython(QStringLiteral("request_token"), {}, QString::fromStdString(email)); if (out.isEmpty()) return false; - auto value = QJsonValue(QString::fromStdString(out.toStdString())); - return value.isBool() ? value.toBool() : false; + QString result = QString::fromUtf8(out).trimmed().toLower(); + return result == QStringLiteral("true"); } bool XRDriverIPC::verifyToken(const std::string &token) { QByteArray out = invokePython(QStringLiteral("verify_token"), {}, QString::fromStdString(token)); if (out.isEmpty()) return false; - auto value = QJsonValue(QString::fromStdString(out.toStdString())); - return value.isBool() ? value.toBool() : false; + QString result = QString::fromUtf8(out).trimmed().toLower(); + return result == QStringLiteral("true"); } diff --git a/kwin/src/xrdriveripc/xrdriveripc.h b/kwin/src/xrdriveripc/xrdriveripc.h index ff6d654..ee6ca81 100644 --- a/kwin/src/xrdriveripc/xrdriveripc.h +++ b/kwin/src/xrdriveripc/xrdriveripc.h @@ -1,13 +1,10 @@ // C++ bridge now invoking xrdriveripc via external python process #pragma once -#include -#include -#include -#include -#include #include #include +#include +#include // Export header generated by CMake (GenerateExportHeader) #ifdef __has_include @@ -78,18 +75,13 @@ namespace XRConfigEntry { inline constexpr const char *Debug = "debug"; } -// Simple variant type for config/state key values we care about -using XRValue = std::variant; -using XRDict = std::map; - class XR_DRIVER_IPC_EXPORT XRDriverIPC { public: static XRDriverIPC &instance(); - static std::string string(const XRDict &dict, const std::string &key); - std::optional retrieveConfig(); - std::optional retrieveDriverState(); - bool writeConfig(const XRDict &configUpdate); + std::optional retrieveConfig(); + std::optional retrieveDriverState(); + bool writeConfig(const QJsonObject &configUpdate); bool writeControlFlags(const std::map &flags); bool requestToken(const std::string &email); bool verifyToken(const std::string &token); diff --git a/kwin/src/xrdriveripc/xrdriveripc_runner.py b/kwin/src/xrdriveripc/xrdriveripc_runner.py index 748bc70..3a47250 100644 --- a/kwin/src/xrdriveripc/xrdriveripc_runner.py +++ b/kwin/src/xrdriveripc/xrdriveripc_runner.py @@ -13,6 +13,13 @@ import os import sys import traceback +class Logger: + def info(self, *args, **kwargs): + pass + + def error(self, *args, **kwargs): + pass + def main() -> int: # Ensure the current directory (where xrdriveripc.py lives) is in sys.path @@ -32,7 +39,7 @@ def main() -> int: return 2 config_home = os.environ.get("BREEZY_CONFIG_HOME") - inst = xrdriveripc.XRDriverIPC(config_home=config_home) + inst = xrdriveripc.XRDriverIPC(logger=Logger(), config_home=config_home) arg = os.environ.get("BREEZY_ARG") payload_raw = os.environ.get("BREEZY_PAYLOAD") diff --git a/ui/src/licensefeaturerow.py b/ui/src/licensefeaturerow.py index 68ed4ac..02f2c52 100644 --- a/ui/src/licensefeaturerow.py +++ b/ui/src/licensefeaturerow.py @@ -26,12 +26,9 @@ class LicenseFeatureRow(Adw.ActionRow): self.set_subtitle(f"{status}{details}") def _feature_name(self, feature): - print(f"Translating feature: {feature}") - print(f"_ is: {_}") feature_names = { - 'sbs': lambda: gettext.gettext('Side-by-side mode (gaming)'), + 'sbs': lambda: _('Side-by-side mode (gaming)'), 'smooth_follow': lambda: _('Smooth Follow (gaming)'), 'productivity_basic': lambda: _('Breezy Desktop (productivity)') } - print(f"Translated string: {feature_names[feature]()}") return feature_names[feature]() \ No newline at end of file From dbb2196819186a70af715ce9f373eae90643d322 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Tue, 2 Sep 2025 12:05:04 -0700 Subject: [PATCH 11/41] Update setup script to perform local build if a binary isn't available for download --- bin/breezy_kwin_setup | 49 +++++++++++++++++-- bin/package_kwin | 65 ++++++++++++++++--------- kwin/bin/breezy_kwin_uninstall | 6 +++ kwin/bin/package_kwin_plugin | 9 ++++ kwin/src/kcm/breezydesktopeffectkcm.cpp | 1 - 5 files changed, 102 insertions(+), 28 deletions(-) diff --git a/bin/breezy_kwin_setup b/bin/breezy_kwin_setup index ac35d7e..41b2cfb 100755 --- a/bin/breezy_kwin_setup +++ b/bin/breezy_kwin_setup @@ -52,9 +52,52 @@ fi if [ -z "$binary_path_arg" ] then - # download and unzip the binary - echo "Downloading to: ${tmp_dir}/$FILE_NAME" - curl -L "$binary_download_url" > "$FILE_NAME" + http_code=$(curl -s -o /dev/null -w "%{http_code}" -L "$binary_download_url" || echo "000") + + if [[ "$http_code" =~ ^(2|3) ]]; then + echo "Downloading to: ${tmp_dir}/$FILE_NAME" + curl -L "$binary_download_url" -o "$FILE_NAME" + else + check_command "git" + + # handle -v / --tag like the rest of the script + if [ "$1" = "--tag" ] && [ -n "$2" ]; then + requested_tag="$2" + fi + + # resolve tag: prefer requested_tag, then LATEST_RELEASE if already fetched, else query GitHub API + if [ -n "$requested_tag" ]; then + tag="$requested_tag" + else + tag=$(curl -s "https://api.github.com/repos/wheaney/breezy-desktop/releases/latest" \ + | grep -m1 '"tag_name":' \ + | sed -E 's/.*"tag_name":[[:space:]]*"([^"]+)".*/\1/') + fi + + if [ -z "$tag" ]; then + echo "Failed to determine latest tag for wheaney/breezy-desktop" 1>&2 + exit 1 + fi + + echo "Cloning wheaney/breezy-desktop (tag: ${tag})" + if git clone --depth 1 --branch "$tag" "https://github.com/wheaney/breezy-desktop.git" breezy-desktop 2>/dev/null; then + pushd breezy-desktop > /dev/null + else + git clone "https://github.com/wheaney/breezy-desktop.git" breezy-desktop + pushd breezy-desktop > /dev/null + git checkout "$tag" + fi + + echo "Downloading git submodules" + git submodule sync --recursive || true + git submodule update --init --recursive modules/XRLinuxDriver + git submodule update --init --recursive ui/modules/PyXRLinuxDriverIPC + + LOCAL_BUILD_SYSTEM=1 bin/package_kwin --download-driver + FILE_NAME="breezyKWin-$ARCH.tar.gz" + cp "out/$FILE_NAME" "$tmp_dir" + popd > /dev/null + fi else FILE_NAME=$(basename $binary_path_arg) if [[ "$binary_path_arg" = /* ]]; then diff --git a/bin/package_kwin b/bin/package_kwin index 9d71536..4153a99 100755 --- a/bin/package_kwin +++ b/bin/package_kwin @@ -30,43 +30,60 @@ mkdir -p $PACKAGE_DIR/bin copy_and_inject_ua "$XR_DRIVER_DIR/bin/ua.sh" "$PACKAGE_DIR/bin" "$KWIN_DIR/bin/setup" "$KWIN_DIR/bin/breezy_kwin_uninstall" XR_DRIVER_BINARY=$XR_DRIVER_DIR/out/xrDriver-$ARCH.tar.gz +if [ "$1" == "--download-driver" ]; then + driver_download_url="https://github.com/wheaney/XRLinuxDriver/releases/latest/download/xrDriver-$ARCH.tar.gz" + setup_download_url="https://github.com/wheaney/XRLinuxDriver/releases/latest/download/xr_driver_setup" -if [ ! -e "$XR_DRIVER_BINARY" ] || [ "$1" == "--rebuild-driver" ] || [ "$1" == "--rebuild-all" ]; 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 $XR_DRIVER_DIR + curl -L "$driver_download_url" > "$PACKAGE_DIR/xrDriver.tar.gz" + curl -L "$setup_download_url" > "$PACKAGE_DIR/bin/xr_driver_setup" + chmod +x "$PACKAGE_DIR/bin/xr_driver_setup" + + echo "Downloaded XRLinuxDriver binary and setup script, with hashes:" + echo "xrDriver-$ARCH.tar.gz: $(sha256sum "$PACKAGE_DIR/xrDriver.tar.gz")" + echo "xr_driver_setup: $(sha256sum "$PACKAGE_DIR/bin/xr_driver_setup")" +else + if [ ! -e "$XR_DRIVER_BINARY" ] || [ "$1" == "--rebuild-driver" ] || [ "$1" == "--rebuild-all" ]; 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 $XR_DRIVER_DIR + fi + + pushd $XR_DRIVER_DIR + + # strange issue where the base library produces a .so file if the build is not cleaned + rm -rf build/ + + docker-build/init.sh + docker-build/run-build.sh $ARCH + popd fi - pushd $XR_DRIVER_DIR + XR_DRIVER_TMP_DIR=$(mktemp -d -t xr-driver-XXXXXXXXXX) + pushd $XR_DRIVER_TMP_DIR + cp $XR_DRIVER_BINARY $XR_DRIVER_TMP_DIR/xrDriver.tar.gz + tar -xf $XR_DRIVER_TMP_DIR/xrDriver.tar.gz - # strange issue where the base library produces a .so file if the build is not cleaned - rm -rf build/ - - docker-build/init.sh - docker-build/run-build.sh $ARCH + XR_DRIVER_MANIFEST_LINE=$(sha256sum xr_driver/manifest) popd + rm -rf $XR_DRIVER_TMP_DIR + + cp $XR_DRIVER_BINARY $PACKAGE_DIR/xrDriver.tar.gz + cp $XR_DRIVER_DIR/bin/xr_driver_setup $PACKAGE_DIR/bin/xr_driver_setup fi -XR_DRIVER_TMP_DIR=$(mktemp -d -t xr-driver-XXXXXXXXXX) -pushd $XR_DRIVER_TMP_DIR -cp $XR_DRIVER_BINARY $XR_DRIVER_TMP_DIR/xrDriver.tar.gz -tar -xf $XR_DRIVER_TMP_DIR/xrDriver.tar.gz - -XR_DRIVER_MANIFEST_LINE=$(sha256sum xr_driver/manifest) -popd -rm -rf $XR_DRIVER_TMP_DIR - -cp $XR_DRIVER_BINARY $PACKAGE_DIR/xrDriver.tar.gz -cp $XR_DRIVER_DIR/bin/xr_driver_setup $PACKAGE_DIR/bin - # alternative to symlinking, since the Docker build can't resolve to the parent directory # this file is in .gitignore so it doesn't get duplicated cp ui/modules/PyXRLinuxDriverIPC/xrdriveripc.py $KWIN_DIR/src/xrdriveripc/xrdriveripc.py cp VERSION $KWIN_DIR pushd $KWIN_DIR -docker-build/init.sh -docker-build/run-build.sh $BUILD_ARCH +if [ -z "${LOCAL_BUILD_SYSTEM+x}" ]; then + docker-build/init.sh + docker-build/run-build.sh "$BUILD_ARCH" +else + echo "LOCAL_BUILD_SYSTEM set; skipping Docker build" + bin/package_kwin_plugin +fi popd cp $KWIN_DIR/out/breezyKWinPlugin-$BUILD_ARCH.tar.gz $PACKAGE_DIR/breezyKWinPlugin.tar.gz diff --git a/kwin/bin/breezy_kwin_uninstall b/kwin/bin/breezy_kwin_uninstall index b14d6de..82f9c49 100755 --- a/kwin/bin/breezy_kwin_uninstall +++ b/kwin/bin/breezy_kwin_uninstall @@ -31,6 +31,7 @@ EFFECT_ID="breezy_desktop" EFFECT_DIR="$XDG_DATA_HOME/kwin/effects/$EFFECT_ID" PLUGIN_SO="$LIB_HOME/qt6/plugins/kwin/effects/plugins/${EFFECT_ID}.so" CONFIG_SO="$LIB_HOME/qt6/plugins/kwin/effects/configs/${EFFECT_ID}_config.so" +BREEZY_LIBRARY_DIR="$LIB_HOME/breezy_kwin" if [[ -d "$EFFECT_DIR" ]]; then [ "$for_install" -eq 0 ] && echo "Removing $EFFECT_DIR and its contents" @@ -47,6 +48,11 @@ if [[ -f "$CONFIG_SO" ]]; then $SUDO rm -f "$CONFIG_SO" fi +if [[ -d "$BREEZY_LIBRARY_DIR" ]]; then + [ "$for_install" -eq 0 ] && echo "Removing $BREEZY_LIBRARY_DIR and its contents" + $SUDO rm -rf "$BREEZY_LIBRARY_DIR" +fi + if [[ -e "$XDG_BIN_HOME/xr_driver_uninstall" && "$for_install" -eq 0 ]]; then echo "Uninstalling XRLinuxDriver" sudo "$XDG_BIN_HOME/xr_driver_uninstall" diff --git a/kwin/bin/package_kwin_plugin b/kwin/bin/package_kwin_plugin index a5f4346..4c88db5 100755 --- a/kwin/bin/package_kwin_plugin +++ b/kwin/bin/package_kwin_plugin @@ -3,6 +3,15 @@ # 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, then rerun the setup." + exit 1 + fi +} +check_command "cmake" +check_command "make" + ARCH=${ARCH:-$(uname -m)} if [ -n "${STEAMOS+x}" ]; then ARCH="steamos" diff --git a/kwin/src/kcm/breezydesktopeffectkcm.cpp b/kwin/src/kcm/breezydesktopeffectkcm.cpp index 105163f..03cc41f 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.cpp +++ b/kwin/src/kcm/breezydesktopeffectkcm.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include #include From 0a0ac74bc87b8031884ad7361e2607b37d9c7429 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:28:40 -0700 Subject: [PATCH 12/41] Attempt to improve dependency messaging --- bin/breezy_kwin_setup | 60 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/bin/breezy_kwin_setup b/bin/breezy_kwin_setup index 41b2cfb..2bc92cf 100755 --- a/bin/breezy_kwin_setup +++ b/bin/breezy_kwin_setup @@ -20,6 +20,19 @@ check_command() { check_command "curl" +print_missing_dependencies() { + echo "" + printf "\n\033[1;31mMissing required components\033[0m\n" + echo "" + echo "Install the corresponding packages with your package manager, then rerun this setup:" + echo " Debian/Ubuntu: sudo apt-get update && sudo apt-get install -y extra-cmake-modules kf6-kconfig-dev kf6-kconfigwidgets-dev kf6-kcoreaddons-dev kf6-kcmutils-dev" + echo " Fedora/RHEL: sudo dnf install -y extra-cmake-modules kf6-kconfig kf6-kconfigwidgets kf6-kcoreaddons kf6-kcmutils" + echo " Arch: sudo pacman -S --needed extra-cmake-modules kf6" + echo "" + printf "\n\033[1;33mIf you continue to see this issue\033[0m run with PRINT_FULL_STDERR=1 and attach the full output to a bug report.\n" + exit 1 +} + ARCH=$(uname -m) if [ -f /etc/os-release ]; then . /etc/os-release @@ -59,6 +72,8 @@ then curl -L "$binary_download_url" -o "$FILE_NAME" else check_command "git" + check_command "cmake" + check_command "make" # handle -v / --tag like the rest of the script if [ "$1" = "--tag" ] && [ -n "$2" ]; then @@ -79,21 +94,42 @@ then exit 1 fi - echo "Cloning wheaney/breezy-desktop (tag: ${tag})" - if git clone --depth 1 --branch "$tag" "https://github.com/wheaney/breezy-desktop.git" breezy-desktop 2>/dev/null; then - pushd breezy-desktop > /dev/null + if [ -z "${DEV_BUILD+x}" ]; then + echo "Cloning wheaney/breezy-desktop (tag: ${tag})" + if git clone --depth 1 --branch "$tag" "https://github.com/wheaney/breezy-desktop.git" breezy-desktop 2>/dev/null; then + pushd breezy-desktop > /dev/null + else + git clone "https://github.com/wheaney/breezy-desktop.git" breezy-desktop + pushd breezy-desktop > /dev/null + git checkout "$tag" + fi + + echo "Downloading git submodules" + git submodule sync --recursive || true + git submodule update --init modules/XRLinuxDriver + git submodule update --init ui/modules/PyXRLinuxDriverIPC else - git clone "https://github.com/wheaney/breezy-desktop.git" breezy-desktop - pushd breezy-desktop > /dev/null - git checkout "$tag" + pushd $start_dir > /dev/null fi - echo "Downloading git submodules" - git submodule sync --recursive || true - git submodule update --init --recursive modules/XRLinuxDriver - git submodule update --init --recursive ui/modules/PyXRLinuxDriverIPC - - LOCAL_BUILD_SYSTEM=1 bin/package_kwin --download-driver + echo "Building Breezy Desktop from source, this may take a while..." + set +e + pkgkwin_stderr=$(LOCAL_BUILD_SYSTEM=1 bin/package_kwin --download-driver 2>&1) + pkgkwin_rc=$? + set -e + if echo "$pkgkwin_stderr" | grep -q "Could NOT find"; then + if [ -z "${PRINT_FULL_STDERR+x}" ]; then + print_missing_dependencies + exit 1 + else + echo "$pkgkwin_stderr" + fi + fi + if [ "$pkgkwin_rc" -ne 0 ]; then + echo "$pkgkwin_stderr" + echo "Error: build failed with exit code $pkgkwin_rc" + exit $pkgkwin_rc + fi FILE_NAME="breezyKWin-$ARCH.tar.gz" cp "out/$FILE_NAME" "$tmp_dir" popd > /dev/null From 7a810bb1bc668395a09c91750d33c7db50271d7a Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Tue, 2 Sep 2025 19:30:14 -0700 Subject: [PATCH 13/41] Fix a few issues in the new setup --- bin/breezy_kwin_setup | 4 ++-- bin/package_kwin | 21 ++++++++++----------- kwin/bin/package_kwin_plugin | 4 ++-- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/bin/breezy_kwin_setup b/bin/breezy_kwin_setup index 2bc92cf..bac933e 100755 --- a/bin/breezy_kwin_setup +++ b/bin/breezy_kwin_setup @@ -27,7 +27,7 @@ print_missing_dependencies() { echo "Install the corresponding packages with your package manager, then rerun this setup:" echo " Debian/Ubuntu: sudo apt-get update && sudo apt-get install -y extra-cmake-modules kf6-kconfig-dev kf6-kconfigwidgets-dev kf6-kcoreaddons-dev kf6-kcmutils-dev" echo " Fedora/RHEL: sudo dnf install -y extra-cmake-modules kf6-kconfig kf6-kconfigwidgets kf6-kcoreaddons kf6-kcmutils" - echo " Arch: sudo pacman -S --needed extra-cmake-modules kf6" + echo " Arch: sudo pacman -S --needed extra-cmake-modules kf6" echo "" printf "\n\033[1;33mIf you continue to see this issue\033[0m run with PRINT_FULL_STDERR=1 and attach the full output to a bug report.\n" exit 1 @@ -117,7 +117,7 @@ then pkgkwin_stderr=$(LOCAL_BUILD_SYSTEM=1 bin/package_kwin --download-driver 2>&1) pkgkwin_rc=$? set -e - if echo "$pkgkwin_stderr" | grep -q "Could NOT find"; then + if echo "$pkgkwin_stderr" | grep -qi "could not find"; then if [ -z "${PRINT_FULL_STDERR+x}" ]; then print_missing_dependencies exit 1 diff --git a/bin/package_kwin b/bin/package_kwin index 4153a99..af1dd34 100755 --- a/bin/package_kwin +++ b/bin/package_kwin @@ -39,8 +39,8 @@ if [ "$1" == "--download-driver" ]; then chmod +x "$PACKAGE_DIR/bin/xr_driver_setup" echo "Downloaded XRLinuxDriver binary and setup script, with hashes:" - echo "xrDriver-$ARCH.tar.gz: $(sha256sum "$PACKAGE_DIR/xrDriver.tar.gz")" - echo "xr_driver_setup: $(sha256sum "$PACKAGE_DIR/bin/xr_driver_setup")" + printf '\txrDriver-%s.tar.gz: %s\n' "$ARCH" "$(sha256sum "$PACKAGE_DIR/xrDriver.tar.gz" | sort | sha256sum | sed 's/ .*//')" + printf '\txr_driver_setup: %s\n' "$(sha256sum "$PACKAGE_DIR/bin/xr_driver_setup" | sort | sha256sum | sed 's/ .*//')" else if [ ! -e "$XR_DRIVER_BINARY" ] || [ "$1" == "--rebuild-driver" ] || [ "$1" == "--rebuild-all" ]; then # if a file exists at custom_banner_config.yml, copy it to the xrealAirLinuxDriver directory @@ -48,23 +48,23 @@ else cp $VULKAN_DIR/custom_banner_config.yml $XR_DRIVER_DIR fi - pushd $XR_DRIVER_DIR + pushd $XR_DRIVER_DIR > /dev/null # strange issue where the base library produces a .so file if the build is not cleaned rm -rf build/ docker-build/init.sh docker-build/run-build.sh $ARCH - popd + popd > /dev/null fi XR_DRIVER_TMP_DIR=$(mktemp -d -t xr-driver-XXXXXXXXXX) - pushd $XR_DRIVER_TMP_DIR + pushd $XR_DRIVER_TMP_DIR > /dev/null cp $XR_DRIVER_BINARY $XR_DRIVER_TMP_DIR/xrDriver.tar.gz tar -xf $XR_DRIVER_TMP_DIR/xrDriver.tar.gz XR_DRIVER_MANIFEST_LINE=$(sha256sum xr_driver/manifest) - popd + popd > /dev/null rm -rf $XR_DRIVER_TMP_DIR cp $XR_DRIVER_BINARY $PACKAGE_DIR/xrDriver.tar.gz @@ -76,20 +76,19 @@ fi cp ui/modules/PyXRLinuxDriverIPC/xrdriveripc.py $KWIN_DIR/src/xrdriveripc/xrdriveripc.py cp VERSION $KWIN_DIR -pushd $KWIN_DIR +pushd $KWIN_DIR > /dev/null if [ -z "${LOCAL_BUILD_SYSTEM+x}" ]; then docker-build/init.sh docker-build/run-build.sh "$BUILD_ARCH" else - echo "LOCAL_BUILD_SYSTEM set; skipping Docker build" bin/package_kwin_plugin fi -popd +popd > /dev/null cp $KWIN_DIR/out/breezyKWinPlugin-$BUILD_ARCH.tar.gz $PACKAGE_DIR/breezyKWinPlugin.tar.gz -pushd $TMP_DIR +pushd $TMP_DIR > /dev/null tar -zcvf $BUILD_FILE_NAME breezy_kwin -popd +popd > /dev/null mkdir -p out if [ -e "out/$BUILD_FILE_NAME" ]; then diff --git a/kwin/bin/package_kwin_plugin b/kwin/bin/package_kwin_plugin index 4c88db5..a60efe5 100755 --- a/kwin/bin/package_kwin_plugin +++ b/kwin/bin/package_kwin_plugin @@ -22,11 +22,11 @@ BUILD_PATH=build rm -rf $BUILD_PATH mkdir $BUILD_PATH -pushd $BUILD_PATH +pushd $BUILD_PATH > /dev/null cmake .. make cpack -G TGZ -popd +popd > /dev/null mkdir -p out cp $BUILD_PATH/breezy_desktop.tar.gz out/breezyKWinPlugin-$ARCH.tar.gz \ No newline at end of file From 6c5f08611e36bfd4a35cf57d1722f9ccedb020a1 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Tue, 2 Sep 2025 21:24:49 -0700 Subject: [PATCH 14/41] Fix how display distances are bound so it tracks the UI smoothly --- kwin/src/breezydesktopeffect.cpp | 4 +-- kwin/src/breezydesktopeffect.h | 7 ++--- kwin/src/qml/BreezyDesktop.qml | 45 +++++++++++++++++++++----------- kwin/src/qml/Displays.qml | 6 ++--- kwin/src/qml/main.qml | 2 +- 5 files changed, 40 insertions(+), 24 deletions(-) diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index aa1c7a5..0ae5596 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -301,7 +301,7 @@ qreal BreezyDesktopEffect::focusedDisplayDistance() const { void BreezyDesktopEffect::setFocusedDisplayDistance(qreal distance) { if (distance != m_focusedDisplayDistance) { m_focusedDisplayDistance = std::clamp(distance, 0.2, m_allDisplaysDistance); - Q_EMIT displayDistanceChanged(); + Q_EMIT focusedDisplayDistanceChanged(); } } @@ -312,7 +312,7 @@ qreal BreezyDesktopEffect::allDisplaysDistance() const { void BreezyDesktopEffect::setAllDisplaysDistance(qreal distance) { if (distance != m_allDisplaysDistance) { m_allDisplaysDistance = std::clamp(distance, m_focusedDisplayDistance, 2.5); - Q_EMIT displayDistanceChanged(); + Q_EMIT allDisplaysDistanceChanged(); } } diff --git a/kwin/src/breezydesktopeffect.h b/kwin/src/breezydesktopeffect.h index ba3dee7..4f232b5 100644 --- a/kwin/src/breezydesktopeffect.h +++ b/kwin/src/breezydesktopeffect.h @@ -24,8 +24,8 @@ namespace KWin Q_PROPERTY(QPointF cursorPos READ cursorPos NOTIFY cursorPosChanged) Q_PROPERTY(QList lookAheadConfig READ lookAheadConfig NOTIFY devicePropertiesChanged) Q_PROPERTY(QList displayResolution READ displayResolution NOTIFY devicePropertiesChanged) - Q_PROPERTY(qreal focusedDisplayDistance READ focusedDisplayDistance NOTIFY displayDistanceChanged) - Q_PROPERTY(qreal allDisplaysDistance READ allDisplaysDistance NOTIFY displayDistanceChanged) + Q_PROPERTY(qreal focusedDisplayDistance READ focusedDisplayDistance NOTIFY focusedDisplayDistanceChanged) + Q_PROPERTY(qreal allDisplaysDistance READ allDisplaysDistance NOTIFY allDisplaysDistanceChanged) Q_PROPERTY(qreal displaySpacing READ displaySpacing NOTIFY displaySpacingChanged) Q_PROPERTY(qreal diagonalFOV READ diagonalFOV NOTIFY devicePropertiesChanged) Q_PROPERTY(qreal lensDistanceRatio READ lensDistanceRatio NOTIFY devicePropertiesChanged) @@ -77,7 +77,8 @@ namespace KWin void updateCursorPos(); Q_SIGNALS: - void displayDistanceChanged(); + void focusedDisplayDistanceChanged(); + void allDisplaysDistanceChanged(); void displaySpacingChanged(); void enabledStateChanged(); void zoomOnFocusChanged(); diff --git a/kwin/src/qml/BreezyDesktop.qml b/kwin/src/qml/BreezyDesktop.qml index 8ffa052..6fbb3be 100644 --- a/kwin/src/qml/BreezyDesktop.qml +++ b/kwin/src/qml/BreezyDesktop.qml @@ -25,10 +25,10 @@ Node { Repeater3D { id: breezyDesktopDisplays - model: screens.length + model: breezyDesktop.screens.length delegate: BreezyDesktopDisplay { - screen: screens[index] - monitorPlacement: monitorPlacements[index] + screen: breezyDesktop.screens[index] + monitorPlacement: breezyDesktop.monitorPlacements[index] property real monitorDistance: effect.allDisplaysDistance property real targetDistance: effect.allDisplaysDistance property real screenRotationY: displays.radianToDegree(monitorPlacement.rotationAngleRadians.y) @@ -81,30 +81,45 @@ Node { ); } - const focusedDisplay = focusedIndex !== -1 ? breezyDesktop.displayAtIndex(focusedIndex) : null; if (focusedIndex !== breezyDesktop.focusedMonitorIndex) { + const unfocusedIndex = breezyDesktop.focusedMonitorIndex; + const focusedDisplay = focusedIndex !== -1 ? breezyDesktop.displayAtIndex(focusedIndex) : null; + const allDisplaysDistanceBinding = Qt.binding(function() { return effect.allDisplaysDistance; }); + const focusedDisplayDistanceBinding = Qt.binding(function() { return effect.focusedDisplayDistance; }); if (focusedDisplay === null) { - zoomOutAnimation.target = breezyDesktop.displayAtIndex(breezyDesktop.focusedMonitorIndex); - zoomOutAnimation.target.targetDistance = zoomOutAnimation.to; + const unfocusedDisplay = breezyDesktop.displayAtIndex(unfocusedIndex); + zoomOutAnimation.target = unfocusedDisplay; + zoomOutAnimation.target.targetDistance = effect.allDisplaysDistance; + zoomOutAnimation.onFinished.connect(function() { + unfocusedDisplay.monitorDistance = allDisplaysDistanceBinding; + }); zoomOutAnimation.start(); } else { - if (breezyDesktop.focusedMonitorIndex === -1) { + if (unfocusedIndex === -1) { zoomInAnimation.target = focusedDisplay; - focusedDisplay.targetDistance = zoomInAnimation.to; + focusedDisplay.targetDistance = effect.focusedDisplayDistance; + zoomInAnimation.onFinished.connect(function() { + focusedDisplay.monitorDistance = focusedDisplayDistanceBinding; + }); zoomInAnimation.start(); } else { + const focusedDisplay = breezyDesktop.displayAtIndex(focusedIndex); zoomInSeqAnimation.target = focusedDisplay; - focusedDisplay.targetDistance = zoomInSeqAnimation.to; - zoomOutSeqAnimation.target = breezyDesktop.displayAtIndex(breezyDesktop.focusedMonitorIndex); - zoomOutSeqAnimation.target.targetDistance = zoomOutSeqAnimation.to; + focusedDisplay.targetDistance = effect.focusedDisplayDistance; + + const unfocusedDisplay = breezyDesktop.displayAtIndex(unfocusedIndex); + zoomOutSeqAnimation.target = unfocusedDisplay; + zoomOutSeqAnimation.target.targetDistance = effect.allDisplaysDistance; + + zoomOnFocusSequence.onFinished.connect(function() { + focusedDisplay.monitorDistance = focusedDisplayDistanceBinding; + unfocusedDisplay.monitorDistance = allDisplaysDistanceBinding; + }); + zoomOnFocusSequence.start(); } } breezyDesktop.focusedMonitorIndex = focusedIndex; - } else if (focusedDisplay !== null && focusedDisplay.targetDistance !== effect.focusedDisplayDistance) { - // user is changing the focused display distance setting, so just move it to match - focusedDisplay.monitorDistance = effect.focusedDisplayDistance; - focusedDisplay.targetDistance = effect.focusedDisplayDistance; } } } diff --git a/kwin/src/qml/Displays.qml b/kwin/src/qml/Displays.qml index 69e79af..53d1dbf 100644 --- a/kwin/src/qml/Displays.qml +++ b/kwin/src/qml/Displays.qml @@ -48,11 +48,11 @@ QtObject { } } - function fovDetails(screens, viewportWidth, viewportHeight, viewportDiagonalFOV, lensDistanceRatio) { + function fovDetails(screens, viewportWidth, viewportHeight, viewportDiagonalFOV, lensDistanceRatio, defaultDisplayDistanace) { const aspect = viewportWidth / viewportHeight; const fovRadians = diagonalToCrossFOVs(degreeToRadian(viewportDiagonalFOV), aspect); - const defaultDistanceVerticalRadians = 2 * Math.atan(Math.tan(fovRadians.vertical / 2) / effect.allDisplaysDistance); - const defaultDistanceHorizontalRadians = 2 * Math.atan(Math.tan(fovRadians.horizontal / 2) / effect.allDisplaysDistance); + const defaultDistanceVerticalRadians = 2 * Math.atan(Math.tan(fovRadians.vertical / 2) / defaultDisplayDistanace); + const defaultDistanceHorizontalRadians = 2 * Math.atan(Math.tan(fovRadians.horizontal / 2) / defaultDisplayDistanace); // distance needed for the FOV-sized monitor to fill up the screen const fullScreenDistance = viewportHeight / 2 / Math.tan(fovRadians.vertical / 2); diff --git a/kwin/src/qml/main.qml b/kwin/src/qml/main.qml index d2ad25a..983cc67 100644 --- a/kwin/src/qml/main.qml +++ b/kwin/src/qml/main.qml @@ -61,7 +61,7 @@ Item { id: displays } - property var fovDetails: displays.fovDetails(screens, viewportResolution[0], viewportResolution[1], viewportDiagonalFOVDegrees, effect.lensDistanceRatio) + property var fovDetails: displays.fovDetails(screens, viewportResolution[0], viewportResolution[1], viewportDiagonalFOVDegrees, effect.lensDistanceRatio, effect.allDisplaysDistance) property var monitorPlacements: { const adjustedGeometries = screens.map(screen => { From bc959c2f4c2a8fdc2e974e4a0c3441e3f7bad2bf Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Tue, 2 Sep 2025 22:01:59 -0700 Subject: [PATCH 15/41] Add display offsets and wrapping scheme options --- kwin/src/breezydesktopconfig.kcfg | 21 +++++ kwin/src/breezydesktopeffect.cpp | 20 +++++ kwin/src/breezydesktopeffect.h | 11 +++ kwin/src/kcm/breezydesktopeffectkcm.cpp | 15 ++-- kwin/src/kcm/breezydesktopeffectkcm.ui | 102 +++++++++++++++++++++--- kwin/src/qml/Displays.qml | 13 ++- kwin/src/qml/main.qml | 16 +++- 7 files changed, 172 insertions(+), 26 deletions(-) diff --git a/kwin/src/breezydesktopconfig.kcfg b/kwin/src/breezydesktopconfig.kcfg index 8d0824d..3e47677 100644 --- a/kwin/src/breezydesktopconfig.kcfg +++ b/kwin/src/breezydesktopconfig.kcfg @@ -29,5 +29,26 @@ How far apart the displays are visually (not logically) + + 0 + -250 + 250 + + Horizontal offset as a percent of the viewport width (-2.50 to 2.50) + + + 0 + -250 + 250 + + Vertical offset as a percent of the viewport height (-2.50 to 2.50) + + + 0 + 0 + 3 + + How to arrange monitors: 0=Auto, 1=Horizontal, 2=Vertical, 3=Flat + diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index 0ae5596..330ece4 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -153,6 +153,14 @@ void BreezyDesktopEffect::reconfigure(ReconfigureFlags) setAllDisplaysDistance(BreezyDesktopConfig::allDisplaysDistance() / 100.0f); setDisplaySpacing(BreezyDesktopConfig::displaySpacing() / 1000.0f); setZoomOnFocusEnabled(BreezyDesktopConfig::zoomOnFocusEnabled()); + qreal horiz = BreezyDesktopConfig::displayHorizontalOffset() / 100.0f; + qreal vert = BreezyDesktopConfig::displayVerticalOffset() / 100.0f; + int wrap = BreezyDesktopConfig::displayWrappingScheme(); + bool changed = false; + if (!qFuzzyCompare(m_displayHorizontalOffset, horiz)) { m_displayHorizontalOffset = horiz; changed = true; } + if (!qFuzzyCompare(m_displayVerticalOffset, vert)) { m_displayVerticalOffset = vert; changed = true; } + if (m_displayWrappingScheme != wrap) { m_displayWrappingScheme = wrap; Q_EMIT displayWrappingSchemeChanged(); } + if (changed) Q_EMIT displayOffsetChanged(); } QVariantMap BreezyDesktopEffect::initialProperties(Output *screen) @@ -327,6 +335,18 @@ void BreezyDesktopEffect::setDisplaySpacing(qreal spacing) { } } +qreal BreezyDesktopEffect::displayHorizontalOffset() const { + return m_displayHorizontalOffset; +} + +qreal BreezyDesktopEffect::displayVerticalOffset() const { + return m_displayVerticalOffset; +} + +int BreezyDesktopEffect::displayWrappingScheme() const { + return m_displayWrappingScheme; +} + qreal BreezyDesktopEffect::diagonalFOV() const { return m_diagonalFOV; } diff --git a/kwin/src/breezydesktopeffect.h b/kwin/src/breezydesktopeffect.h index 4f232b5..6011c42 100644 --- a/kwin/src/breezydesktopeffect.h +++ b/kwin/src/breezydesktopeffect.h @@ -27,6 +27,9 @@ namespace KWin Q_PROPERTY(qreal focusedDisplayDistance READ focusedDisplayDistance NOTIFY focusedDisplayDistanceChanged) Q_PROPERTY(qreal allDisplaysDistance READ allDisplaysDistance NOTIFY allDisplaysDistanceChanged) Q_PROPERTY(qreal displaySpacing READ displaySpacing NOTIFY displaySpacingChanged) + Q_PROPERTY(qreal displayHorizontalOffset READ displayHorizontalOffset NOTIFY displayOffsetChanged) + Q_PROPERTY(qreal displayVerticalOffset READ displayVerticalOffset NOTIFY displayOffsetChanged) + Q_PROPERTY(int displayWrappingScheme READ displayWrappingScheme NOTIFY displayWrappingSchemeChanged) Q_PROPERTY(qreal diagonalFOV READ diagonalFOV NOTIFY devicePropertiesChanged) Q_PROPERTY(qreal lensDistanceRatio READ lensDistanceRatio NOTIFY devicePropertiesChanged) Q_PROPERTY(bool sbsEnabled READ sbsEnabled NOTIFY devicePropertiesChanged) @@ -58,6 +61,9 @@ namespace KWin void setAllDisplaysDistance(qreal distance); qreal displaySpacing() const; void setDisplaySpacing(qreal spacing); + qreal displayHorizontalOffset() const; + qreal displayVerticalOffset() const; + int displayWrappingScheme() const; qreal diagonalFOV() const; qreal lensDistanceRatio() const; bool sbsEnabled() const; @@ -80,6 +86,8 @@ namespace KWin void focusedDisplayDistanceChanged(); void allDisplaysDistanceChanged(); void displaySpacingChanged(); + void displayOffsetChanged(); + void displayWrappingSchemeChanged(); void enabledStateChanged(); void zoomOnFocusChanged(); void imuRotationsChanged(); @@ -119,6 +127,9 @@ namespace KWin qreal m_focusedDisplayDistance = 0.85; qreal m_allDisplaysDistance = 1.05; qreal m_displaySpacing = 0.0; + qreal m_displayHorizontalOffset = 0.0; + qreal m_displayVerticalOffset = 0.0; + int m_displayWrappingScheme = 0; // 0=auto,1=horizontal,2=vertical,3=flat QList m_virtualOutputs; }; diff --git a/kwin/src/kcm/breezydesktopeffectkcm.cpp b/kwin/src/kcm/breezydesktopeffectkcm.cpp index 03cc41f..bc81e1e 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.cpp +++ b/kwin/src/kcm/breezydesktopeffectkcm.cpp @@ -14,16 +14,13 @@ #include #include -#include -#include -#include +#include #include #include #include #include - -#include -#include +#include +#include Q_LOGGING_CATEGORY(KWIN_XR, "kwin.xr") @@ -80,6 +77,9 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu connect(ui.kcfg_FocusedDisplayDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_AllDisplaysDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_DisplaySpacing, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); + connect(ui.kcfg_DisplayHorizontalOffset, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); + connect(ui.kcfg_DisplayVerticalOffset, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); + connect(ui.kcfg_DisplayWrappingScheme, qOverload(&QComboBox::currentIndexChanged), this, &BreezyDesktopEffectConfig::save); if (auto label = widget()->findChild("labelAppNameVersion")) { label->setText(QStringLiteral("Breezy Desktop - v%1").arg(QLatin1String(BREEZY_DESKTOP_VERSION_STR))); @@ -163,6 +163,9 @@ void BreezyDesktopEffectConfig::updateUiFromConfig() ui.kcfg_FocusedDisplayDistance->setValue(BreezyDesktopConfig::self()->focusedDisplayDistance()); ui.kcfg_AllDisplaysDistance->setValue(BreezyDesktopConfig::self()->allDisplaysDistance()); ui.kcfg_DisplaySpacing->setValue(BreezyDesktopConfig::self()->displaySpacing()); + ui.kcfg_DisplayHorizontalOffset->setValue(BreezyDesktopConfig::self()->displayHorizontalOffset()); + ui.kcfg_DisplayVerticalOffset->setValue(BreezyDesktopConfig::self()->displayVerticalOffset()); + ui.kcfg_DisplayWrappingScheme->setCurrentIndex(BreezyDesktopConfig::self()->displayWrappingScheme()); ui.kcfg_ZoomOnFocusEnabled->setChecked(BreezyDesktopConfig::self()->zoomOnFocusEnabled()); ui.kcfg_FocusedDisplayDistance->setEnabled(ui.kcfg_ZoomOnFocusEnabled->isChecked()); } diff --git a/kwin/src/kcm/breezydesktopeffectkcm.ui b/kwin/src/kcm/breezydesktopeffectkcm.ui index 0930c71..a88600d 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.ui +++ b/kwin/src/kcm/breezydesktopeffectkcm.ui @@ -2,6 +2,9 @@ BreezyDesktopEffectConfig + + 800 + @@ -32,7 +35,7 @@ &General - + @@ -112,7 +115,59 @@ - + + + + Display Horizontal Offset: + + + + + + + 2 + + + QSlider::TicksBelow + + + 50 + + + Qt::Horizontal + + + true + + + + + + + Display Vertical Offset: + + + + + + + 2 + + + QSlider::TicksBelow + + + 50 + + + Qt::Horizontal + + + true + + + + @@ -128,17 +183,38 @@ &Advanced - - - - - Advanced settings will appear here. - - - Qt::AlignCenter - - - + + + + + Display Wrapping Scheme: + + + + + + + + Auto + + + + + Horizontal + + + + + Vertical + + + + + Flat + + + + diff --git a/kwin/src/qml/Displays.qml b/kwin/src/qml/Displays.qml index 53d1dbf..dee70b6 100644 --- a/kwin/src/qml/Displays.qml +++ b/kwin/src/qml/Displays.qml @@ -48,11 +48,11 @@ QtObject { } } - function fovDetails(screens, viewportWidth, viewportHeight, viewportDiagonalFOV, lensDistanceRatio, defaultDisplayDistanace) { + function fovDetails(screens, viewportWidth, viewportHeight, viewportDiagonalFOV, lensDistanceRatio, defaultDisplayDistance, wrappingChoice) { const aspect = viewportWidth / viewportHeight; const fovRadians = diagonalToCrossFOVs(degreeToRadian(viewportDiagonalFOV), aspect); - const defaultDistanceVerticalRadians = 2 * Math.atan(Math.tan(fovRadians.vertical / 2) / defaultDisplayDistanace); - const defaultDistanceHorizontalRadians = 2 * Math.atan(Math.tan(fovRadians.horizontal / 2) / defaultDisplayDistanace); + const defaultDistanceVerticalRadians = 2 * Math.atan(Math.tan(fovRadians.vertical / 2) / defaultDisplayDistance); + const defaultDistanceHorizontalRadians = 2 * Math.atan(Math.tan(fovRadians.horizontal / 2) / defaultDisplayDistance); // distance needed for the FOV-sized monitor to fill up the screen const fullScreenDistance = viewportHeight / 2 / Math.tan(fovRadians.vertical / 2); @@ -62,6 +62,11 @@ QtObject { const lensToScreenDistance = viewportHeight / 2 / Math.tan(defaultDistanceVerticalRadians / 2); const completeScreenDistancePixels = lensToScreenDistance + lensDistancePixels; + let monitorWrappingScheme = actualWrapScheme(screens, viewportWidth, viewportHeight); + if (wrappingChoice === 1) monitorWrappingScheme = 'horizontal'; + else if (wrappingChoice === 2) monitorWrappingScheme = 'vertical'; + else if (wrappingChoice === 3) monitorWrappingScheme = 'flat'; + return { widthPixels: viewportWidth, heightPixels: viewportHeight, @@ -69,7 +74,7 @@ QtObject { defaultDistanceHorizontalRadians, lensDistancePixels, completeScreenDistancePixels, - monitorWrappingScheme: actualWrapScheme(screens, viewportWidth, viewportHeight), + monitorWrappingScheme: monitorWrappingScheme, curvedDisplay: false // or true }; } diff --git a/kwin/src/qml/main.qml b/kwin/src/qml/main.qml index 983cc67..6b81a7e 100644 --- a/kwin/src/qml/main.qml +++ b/kwin/src/qml/main.qml @@ -61,14 +61,24 @@ Item { id: displays } - property var fovDetails: displays.fovDetails(screens, viewportResolution[0], viewportResolution[1], viewportDiagonalFOVDegrees, effect.lensDistanceRatio, effect.allDisplaysDistance) + property var fovDetails: displays.fovDetails( + screens, + viewportResolution[0], + viewportResolution[1], + viewportDiagonalFOVDegrees, + effect.lensDistanceRatio, + effect.allDisplaysDistance, + effect.displayWrappingScheme + ) property var monitorPlacements: { + const dx = effect.displayHorizontalOffset * viewportResolution[0]; + const dy = effect.displayVerticalOffset * viewportResolution[1]; const adjustedGeometries = screens.map(screen => { const g = screen.geometry; return { - x: g.x - screensXMid, - y: g.y - screensYMid, + x: g.x - screensXMid + dx, + y: g.y - screensYMid + dy, width: g.width, height: g.height }; From d3af88ec3523edea7336efdb3691912980709c66 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Wed, 3 Sep 2025 08:02:00 -0700 Subject: [PATCH 16/41] Improve build failure messaging --- bin/breezy_kwin_setup | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bin/breezy_kwin_setup b/bin/breezy_kwin_setup index bac933e..8b77672 100755 --- a/bin/breezy_kwin_setup +++ b/bin/breezy_kwin_setup @@ -25,11 +25,12 @@ print_missing_dependencies() { printf "\n\033[1;31mMissing required components\033[0m\n" echo "" echo "Install the corresponding packages with your package manager, then rerun this setup:" - echo " Debian/Ubuntu: sudo apt-get update && sudo apt-get install -y extra-cmake-modules kf6-kconfig-dev kf6-kconfigwidgets-dev kf6-kcoreaddons-dev kf6-kcmutils-dev" + echo " Debian/Ubuntu: sudo apt-get update && sudo apt-get install -y extra-cmake-modules libkwin-dev qt6-base-dev qt6-declarative-dev libkf6config-dev libkf6configwidgets-dev libkf6coreaddons-dev libkf6kcmutils-dev" echo " Fedora/RHEL: sudo dnf install -y extra-cmake-modules kf6-kconfig kf6-kconfigwidgets kf6-kcoreaddons kf6-kcmutils" - echo " Arch: sudo pacman -S --needed extra-cmake-modules kf6" + echo " Arch: sudo pacman -S --needed extra-cmake-modules kf6 qt6-base qt6-declarative" echo "" - printf "\n\033[1;33mIf you continue to see this issue\033[0m run with PRINT_FULL_STDERR=1 and attach the full output to a bug report.\n" + printf "\n\033[1;33mIf you continue to see this issue\033[0m rerun as follows and attach the full output to a bug report:\n" + printf "\n\tPRINT_FULL_STDERR=1 %s\n" "$0" exit 1 } @@ -119,7 +120,7 @@ then set -e if echo "$pkgkwin_stderr" | grep -qi "could not find"; then if [ -z "${PRINT_FULL_STDERR+x}" ]; then - print_missing_dependencies + print_missing_dependencies "$0" exit 1 else echo "$pkgkwin_stderr" From 78a6487a5c54cdeb35748a889b6a3d834a570500 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Wed, 3 Sep 2025 09:45:15 -0700 Subject: [PATCH 17/41] Fix an issue with non-zero exit code states in the setup, add Rokid Max 2 to the supported models list, fix the connection status in the UI, fix an issue with the negative values on the labeled sliders --- bin/breezy_kwin_setup | 19 +++++++++---------- gnome/src/monitormanager.js | 1 + kwin/src/kcm/breezydesktopeffectkcm.cpp | 6 +++--- kwin/src/kcm/breezydesktopeffectkcm.ui | 13 +++++-------- kwin/src/kcm/labeledslider.h | 6 ++++-- kwin/src/qml/main.qml | 1 + 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/bin/breezy_kwin_setup b/bin/breezy_kwin_setup index 8b77672..67a383f 100755 --- a/bin/breezy_kwin_setup +++ b/bin/breezy_kwin_setup @@ -118,17 +118,16 @@ then pkgkwin_stderr=$(LOCAL_BUILD_SYSTEM=1 bin/package_kwin --download-driver 2>&1) pkgkwin_rc=$? set -e - if echo "$pkgkwin_stderr" | grep -qi "could not find"; then - if [ -z "${PRINT_FULL_STDERR+x}" ]; then - print_missing_dependencies "$0" - exit 1 - else - echo "$pkgkwin_stderr" - fi - fi if [ "$pkgkwin_rc" -ne 0 ]; then - echo "$pkgkwin_stderr" - echo "Error: build failed with exit code $pkgkwin_rc" + if echo "$pkgkwin_stderr" | grep -qi "could not find"; then + if [ -z "${PRINT_FULL_STDERR+x}" ]; then + print_missing_dependencies "$0" + else + echo "$pkgkwin_stderr" + fi + fi + echo "" + echo "Error: Breezy Desktop build failed with exit code $pkgkwin_rc" exit $pkgkwin_rc fi FILE_NAME="breezyKWin-$ARCH.tar.gz" diff --git a/gnome/src/monitormanager.js b/gnome/src/monitormanager.js index 4c57171..4c02ce6 100644 --- a/gnome/src/monitormanager.js +++ b/gnome/src/monitormanager.js @@ -34,6 +34,7 @@ export const SUPPORTED_MONITOR_PRODUCTS = [ 'Air 2 Ultra', 'SmartGlasses', // TCL/RayNeo 'Rokid Max', + 'Rokid Max 2', 'Rokid Air', NESTED_MONITOR_PRODUCT ]; diff --git a/kwin/src/kcm/breezydesktopeffectkcm.cpp b/kwin/src/kcm/breezydesktopeffectkcm.cpp index bc81e1e..5135087 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.cpp +++ b/kwin/src/kcm/breezydesktopeffectkcm.cpp @@ -191,9 +191,9 @@ void BreezyDesktopEffectConfig::pollDriverState() const bool wasDeviceConnected = m_deviceConnected; m_deviceConnected = !m_connectedDeviceBrand.isEmpty() && !m_connectedDeviceModel.isEmpty(); - if (m_deviceConnected != wasDeviceConnected) { - ui.labelDeviceConnectionStatus->setText(m_deviceConnected ? - QStringLiteral("%1 %2 connected").arg(m_connectedDeviceBrand, m_connectedDeviceModel) : + if (ui.labelDeviceConnectionStatus->text().isEmpty() || m_deviceConnected != wasDeviceConnected) { + ui.labelDeviceConnectionStatus->setText(m_deviceConnected ? + QStringLiteral("%1 %2 connected").arg(m_connectedDeviceBrand, m_connectedDeviceModel) : QStringLiteral("No device connected")); } diff --git a/kwin/src/kcm/breezydesktopeffectkcm.ui b/kwin/src/kcm/breezydesktopeffectkcm.ui index a88600d..fbca9ed 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.ui +++ b/kwin/src/kcm/breezydesktopeffectkcm.ui @@ -2,14 +2,11 @@ BreezyDesktopEffectConfig - - 800 - - - - - - No device connected + + + + + Qt::AlignHCenter|Qt::AlignVCenter diff --git a/kwin/src/kcm/labeledslider.h b/kwin/src/kcm/labeledslider.h index 6a8a4ec..77a62e0 100644 --- a/kwin/src/kcm/labeledslider.h +++ b/kwin/src/kcm/labeledslider.h @@ -134,11 +134,13 @@ private: return QString::number(raw); } int divisor = 1; - for (int i = 0; i < m_decimalShift; ++i) divisor *= 10; // small loop, m_decimalShift capped + for (int i = 0; i < m_decimalShift; ++i) divisor *= 10; int whole = raw / divisor; int frac = std::abs(raw % divisor); QString fracStr = QString::number(frac).rightJustified(m_decimalShift, QLatin1Char('0')); - return QString::number(whole) + QLatin1Char('.') + fracStr; + QString result = QString::number(std::abs(whole)) + QLatin1Char('.') + fracStr; + if (raw < 0) result.prepend(QLatin1Char('-')); + return result; } bool m_showValueBubble = true; diff --git a/kwin/src/qml/main.qml b/kwin/src/qml/main.qml index 6b81a7e..6b9a615 100644 --- a/kwin/src/qml/main.qml +++ b/kwin/src/qml/main.qml @@ -17,6 +17,7 @@ Item { "Air 2 Ultra", "SmartGlasses", // TCL/RayNeo "Rokid Max", + "Rokid Max 2", "Rokid Air" ] required property QtObject effect From 2b85ba5e3aeadfaddd3ab77025d6f20aabd2e414 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Wed, 3 Sep 2025 10:14:40 -0700 Subject: [PATCH 18/41] Move away from FrameAnimation, update camera orientation as soon as new IMU data is received --- kwin/src/qml/CameraController.qml | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/kwin/src/qml/CameraController.qml b/kwin/src/qml/CameraController.qml index 15b5276..5cf3853 100644 --- a/kwin/src/qml/CameraController.qml +++ b/kwin/src/qml/CameraController.qml @@ -67,18 +67,14 @@ Item { onDisplayResolutionChanged: updateFOV(); onDiagonalFOVChanged: updateFOV(); - - FrameAnimation { - running: true - onTriggered: { - if (root.imuRotations && root.imuRotations.length > 0) { - updateCamera(applyLookAhead( - root.imuRotations[0], - root.imuRotations[1], - root.imuTimeElapsedMs, - lookAheadMS(root.imuTimestamp, root.lookAheadConfig, -1) - )); - } + onImuRotationsChanged: { + if (root.imuRotations && root.imuRotations.length > 0) { + updateCamera(applyLookAhead( + root.imuRotations[0], + root.imuRotations[1], + root.imuTimeElapsedMs, + lookAheadMS(root.imuTimestamp, root.lookAheadConfig, -1) + )); } } } From 7dbb2357565166f7e00116d3f0772cbb9b5760b0 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Wed, 3 Sep 2025 10:29:31 -0700 Subject: [PATCH 19/41] Fix focused display zooming issue --- kwin/src/qml/BreezyDesktop.qml | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/kwin/src/qml/BreezyDesktop.qml b/kwin/src/qml/BreezyDesktop.qml index 6fbb3be..621ee0c 100644 --- a/kwin/src/qml/BreezyDesktop.qml +++ b/kwin/src/qml/BreezyDesktop.qml @@ -90,31 +90,19 @@ Node { const unfocusedDisplay = breezyDesktop.displayAtIndex(unfocusedIndex); zoomOutAnimation.target = unfocusedDisplay; zoomOutAnimation.target.targetDistance = effect.allDisplaysDistance; - zoomOutAnimation.onFinished.connect(function() { - unfocusedDisplay.monitorDistance = allDisplaysDistanceBinding; - }); zoomOutAnimation.start(); } else { if (unfocusedIndex === -1) { zoomInAnimation.target = focusedDisplay; focusedDisplay.targetDistance = effect.focusedDisplayDistance; - zoomInAnimation.onFinished.connect(function() { - focusedDisplay.monitorDistance = focusedDisplayDistanceBinding; - }); zoomInAnimation.start(); } else { - const focusedDisplay = breezyDesktop.displayAtIndex(focusedIndex); zoomInSeqAnimation.target = focusedDisplay; focusedDisplay.targetDistance = effect.focusedDisplayDistance; const unfocusedDisplay = breezyDesktop.displayAtIndex(unfocusedIndex); zoomOutSeqAnimation.target = unfocusedDisplay; zoomOutSeqAnimation.target.targetDistance = effect.allDisplaysDistance; - - zoomOnFocusSequence.onFinished.connect(function() { - focusedDisplay.monitorDistance = focusedDisplayDistanceBinding; - unfocusedDisplay.monitorDistance = allDisplaysDistanceBinding; - }); zoomOnFocusSequence.start(); } @@ -131,6 +119,12 @@ Node { to: effect.allDisplaysDistance duration: 150 running: false + onFinished: { + const unfocusedDisplay = zoomInAnimation.target; + if (unfocusedDisplay) { + unfocusedDisplay.monitorDistance = Qt.binding(function() { return effect.allDisplaysDistance; }); + } + } } NumberAnimation { @@ -139,11 +133,27 @@ Node { to: effect.focusedDisplayDistance duration: 300 running: false + onFinished: { + const focusedDisplay = zoomInAnimation.target; + if (focusedDisplay) { + focusedDisplay.monitorDistance = Qt.binding(function() { return effect.focusedDisplayDistance; }); + } + } } SequentialAnimation { id: zoomOnFocusSequence running: false + onFinished: { + const focusedDisplay = zoomInSeqAnimation.target; + if (focusedDisplay) { + focusedDisplay.monitorDistance = Qt.binding(function() { return effect.focusedDisplayDistance; }); + } + const unfocusedDisplay = zoomOutSeqAnimation.target; + if (unfocusedDisplay) { + unfocusedDisplay.monitorDistance = Qt.binding(function() { return effect.allDisplaysDistance; }); + } + } NumberAnimation { id: zoomOutSeqAnimation From 259d2f97f6f7e595f1e01abffacd217167190fae Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:27:27 -0700 Subject: [PATCH 20/41] Add plasma environment script back since it works for some environments --- kwin/bin/setup | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/kwin/bin/setup b/kwin/bin/setup index 23a532d..88f57af 100755 --- a/kwin/bin/setup +++ b/kwin/bin/setup @@ -65,18 +65,26 @@ cp bin/breezy_kwin_uninstall $XDG_BIN_HOME # Install QT_PLUGIN_PATH snippet into ~/.bash_profile if not present BASH_PROFILE="$HOME/.bash_profile" QT_PLUGIN_DIR="$HOME/.local/$QT_PLUGIN_DIR_RELATIVE" -QT_PLUGIN_EXPORT="export QT_PLUGIN_PATH=\"$QT_PLUGIN_DIR\"" +QT_PLUGIN_EXPORT="export QT_PLUGIN_PATH=\"$QT_PLUGIN_DIR:\$QT_PLUGIN_PATH\"" if [[ ! -f "$BASH_PROFILE" ]] || ! grep -Fq "$QT_PLUGIN_EXPORT" "$BASH_PROFILE" 2>/dev/null; then echo "Adding QT_PLUGIN_PATH to $BASH_PROFILE" mkdir -p "$(dirname "$BASH_PROFILE")" cat >> "$BASH_PROFILE" <> "$PLASMA_ENV_SCRIPT" < Date: Wed, 3 Sep 2025 16:40:43 -0700 Subject: [PATCH 21/41] Add buttons to create virtual displays --- bin/breezy_kwin_setup | 11 +- kwin/src/CMakeLists.txt | 1 + kwin/src/breezydesktopeffect.cpp | 36 +++++- kwin/src/kcm/CMakeLists.txt | 1 + kwin/src/kcm/breezydesktopeffectkcm.cpp | 39 +++++++ kwin/src/kcm/breezydesktopeffectkcm.ui | 145 +++++++++++++++--------- kwin/src/qml/BreezyDesktop.qml | 2 +- 7 files changed, 173 insertions(+), 62 deletions(-) diff --git a/bin/breezy_kwin_setup b/bin/breezy_kwin_setup index 67a383f..0bfdfe9 100755 --- a/bin/breezy_kwin_setup +++ b/bin/breezy_kwin_setup @@ -119,14 +119,11 @@ then pkgkwin_rc=$? set -e if [ "$pkgkwin_rc" -ne 0 ]; then - if echo "$pkgkwin_stderr" | grep -qi "could not find"; then - if [ -z "${PRINT_FULL_STDERR+x}" ]; then - print_missing_dependencies "$0" - else - echo "$pkgkwin_stderr" - fi + if echo "$pkgkwin_stderr" | grep -qi "could not find" && [ -z "${PRINT_FULL_STDERR+x}" ]; then + print_missing_dependencies "$0" + else + echo "$pkgkwin_stderr" fi - echo "" echo "Error: Breezy Desktop build failed with exit code $pkgkwin_rc" exit $pkgkwin_rc fi diff --git a/kwin/src/CMakeLists.txt b/kwin/src/CMakeLists.txt index 399840d..5b88fdd 100644 --- a/kwin/src/CMakeLists.txt +++ b/kwin/src/CMakeLists.txt @@ -52,6 +52,7 @@ target_link_libraries(breezy_desktop Qt6::Core Qt6::Gui Qt6::Quick + Qt6::DBus KF6::ConfigCore KF6::ConfigGui diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index 330ece4..fbe5221 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -20,12 +20,36 @@ #include #include #include +#include #include #include Q_LOGGING_CATEGORY(KWIN_XR, "kwin.xr") +// A small DBus adaptor to expose effect controls to the KCM. +// Service is provided by KWin (org.kde.KWin). We only register an object path. +// Interface: com.xronlinux.BreezyDesktop, Path: /com/xronlinux/BreezyDesktop +namespace { +class BreezyDesktopDBusAdaptor : public QObject { + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "com.xronlinux.BreezyDesktop") +public: + explicit BreezyDesktopDBusAdaptor(KWin::BreezyDesktopEffect *effect) + : QObject(effect), m_effect(effect) {} + +public Q_SLOTS: + void AddVirtualDisplay(int width, int height) { + QMetaObject::invokeMethod(m_effect, [this, width, height]() { + m_effect->addVirtualDisplay(QSize(width, height)); + }, Qt::QueuedConnection); + } + +private: + KWin::BreezyDesktopEffect *m_effect; +}; +} // namespace + namespace DataView { const QString SHM_DIR = QStringLiteral("/dev/shm"); @@ -129,6 +153,16 @@ BreezyDesktopEffect::BreezyDesktopEffect() m_cursorUpdateTimer->start(); enableDriver(); + + // Register DBus object under KWin's session bus name + auto *adaptor = new BreezyDesktopDBusAdaptor(this); + const bool dbusOk = QDBusConnection::sessionBus().registerObject( + QStringLiteral("/com/xronlinux/BreezyDesktop"), + adaptor, + QDBusConnection::ExportAllSlots); + if (!dbusOk) { + qCWarning(KWIN_XR) << "Failed to register DBus object /com/xronlinux/BreezyDesktop"; + } } void BreezyDesktopEffect::setupGlobalShortcut(const BreezyShortcuts::Shortcut &shortcut, std::function triggeredFunc) { @@ -550,4 +584,4 @@ void BreezyDesktopEffect::updateCursorPos() } } -#include "moc_breezydesktopeffect.cpp" \ No newline at end of file +#include "breezydesktopeffect.moc" \ No newline at end of file diff --git a/kwin/src/kcm/CMakeLists.txt b/kwin/src/kcm/CMakeLists.txt index 87ceacc..1df5bc2 100644 --- a/kwin/src/kcm/CMakeLists.txt +++ b/kwin/src/kcm/CMakeLists.txt @@ -5,6 +5,7 @@ qt_add_dbus_interface(breezy_desktop_config_SOURCES ${KWIN_EFFECTS_INTERFACE} kw kcoreaddons_add_plugin(breezy_desktop_config INSTALL_NAMESPACE "kwin/effects/configs" SOURCES ${breezy_desktop_config_SOURCES}) kconfig_add_kcfg_files(breezy_desktop_config ../breezydesktopconfig.kcfgc) target_link_libraries(breezy_desktop_config + Qt6::DBus KF6::ConfigCore KF6::ConfigGui KF6::ConfigWidgets diff --git a/kwin/src/kcm/breezydesktopeffectkcm.cpp b/kwin/src/kcm/breezydesktopeffectkcm.cpp index 5135087..51c6c2f 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.cpp +++ b/kwin/src/kcm/breezydesktopeffectkcm.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -21,6 +22,8 @@ #include #include #include +#include +#include Q_LOGGING_CATEGORY(KWIN_XR, "kwin.xr") @@ -43,6 +46,20 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu ui.setupUi(widget()); addConfig(BreezyDesktopConfig::self(), widget()); + // Show/enable Virtual Display controls only when we're on Wayland + const bool isWaylandSession = QGuiApplication::platformName().contains(QStringLiteral("wayland"), Qt::CaseInsensitive) + || qEnvironmentVariable("XDG_SESSION_TYPE").compare(QStringLiteral("wayland"), Qt::CaseInsensitive) == 0; + if (isWaylandSession) { + if (auto lbl = widget()->findChild(QStringLiteral("labelVirtualDisplays"))) { + lbl->setVisible(true); + lbl->setEnabled(true); + } + if (auto row = widget()->findChild(QStringLiteral("widgetVirtualDisplayButtons"))) { + row->setVisible(true); + row->setEnabled(true); + } + } + m_statePollTimer.setInterval(2000); m_statePollTimer.setTimerType(Qt::CoarseTimer); connect(&m_statePollTimer, &QTimer::timeout, this, &BreezyDesktopEffectConfig::pollDriverState); @@ -118,6 +135,28 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu tokenEdit->installEventFilter(this); } } + + // Wire Add Virtual Display buttons via DBus to the effect + auto callAddVirtualDisplay = [](int w, int h) { + QDBusInterface iface( + QStringLiteral("org.kde.KWin"), + QStringLiteral("/com/xronlinux/BreezyDesktop"), + QStringLiteral("com.xronlinux.BreezyDesktop"), + QDBusConnection::sessionBus()); + if (iface.isValid()) { + iface.call(QDBus::NoBlock, QStringLiteral("AddVirtualDisplay"), w, h); + } + }; + if (auto btn1080p = widget()->findChild("buttonAdd1080p")) { + connect(btn1080p, &QPushButton::clicked, this, [callAddVirtualDisplay]() { + callAddVirtualDisplay(1920, 1080); + }); + } + if (auto btn1440p = widget()->findChild("buttonAdd1440p")) { + connect(btn1440p, &QPushButton::clicked, this, [callAddVirtualDisplay]() { + callAddVirtualDisplay(2560, 1440); + }); + } } BreezyDesktopEffectConfig::~BreezyDesktopEffectConfig() diff --git a/kwin/src/kcm/breezydesktopeffectkcm.ui b/kwin/src/kcm/breezydesktopeffectkcm.ui index fbca9ed..5bcb098 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.ui +++ b/kwin/src/kcm/breezydesktopeffectkcm.ui @@ -112,59 +112,46 @@ - - - - Display Horizontal Offset: - - - - - - - 2 - - - QSlider::TicksBelow - - - 50 - - - Qt::Horizontal - - - true - - - - - - - Display Vertical Offset: - - - - - - - 2 - - - QSlider::TicksBelow - - - 50 - - - Qt::Horizontal - - - true - - - - + + + + Add Virtual Display: + + + false + + + false + + + + + + + false + + + false + + + + + + + 1080p + + + + + + + + 1440p + + + + + + + @@ -212,6 +199,58 @@ + + + + Display Horizontal Offset: + + + + + + + 2 + + + QSlider::TicksBelow + + + 50 + + + Qt::Horizontal + + + true + + + + + + + Display Vertical Offset: + + + + + + + 2 + + + QSlider::TicksBelow + + + 50 + + + Qt::Horizontal + + + true + + + diff --git a/kwin/src/qml/BreezyDesktop.qml b/kwin/src/qml/BreezyDesktop.qml index 621ee0c..68fa21c 100644 --- a/kwin/src/qml/BreezyDesktop.qml +++ b/kwin/src/qml/BreezyDesktop.qml @@ -120,7 +120,7 @@ Node { duration: 150 running: false onFinished: { - const unfocusedDisplay = zoomInAnimation.target; + const unfocusedDisplay = zoomOutAnimation.target; if (unfocusedDisplay) { unfocusedDisplay.monitorDistance = Qt.binding(function() { return effect.allDisplaysDistance; }); } From 4079d46de494138722942e89f875511eb4dbf468 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Thu, 4 Sep 2025 13:13:39 -0700 Subject: [PATCH 22/41] Fix package manager suggestions in kwin setup script --- bin/breezy_kwin_setup | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/breezy_kwin_setup b/bin/breezy_kwin_setup index 0bfdfe9..d7aebed 100755 --- a/bin/breezy_kwin_setup +++ b/bin/breezy_kwin_setup @@ -25,8 +25,8 @@ print_missing_dependencies() { printf "\n\033[1;31mMissing required components\033[0m\n" echo "" echo "Install the corresponding packages with your package manager, then rerun this setup:" - echo " Debian/Ubuntu: sudo apt-get update && sudo apt-get install -y extra-cmake-modules libkwin-dev qt6-base-dev qt6-declarative-dev libkf6config-dev libkf6configwidgets-dev libkf6coreaddons-dev libkf6kcmutils-dev" - echo " Fedora/RHEL: sudo dnf install -y extra-cmake-modules kf6-kconfig kf6-kconfigwidgets kf6-kcoreaddons kf6-kcmutils" + echo " Debian/Ubuntu: sudo apt-get update && sudo apt-get install -y extra-cmake-modules kwin-dev libkf6config-dev libkf6configwidgets-dev libkf6coreaddons-dev libkf6kcmutils-dev libkf6globalaccel-dev libkf6i18n-dev libkf6windowsystem-dev libkf6xmlgui-dev qt6-base-dev qt6-declarative-dev" + echo " Fedora/RHEL: sudo dnf install -y extra-cmake-modules kwin-devel kf6-kconfig-devel kf6-kconfigwidgets-devel kf6-kcoreaddons-devel kf6-kcmutils-devel kf6-kglobalaccel-devel kf6-ki18n-devel kf6-kwindowsystem-devel kf6-kxmlgui-devel qt6-qtbase-devel qt6-qttools-devel wayland-devel libepoxy-devel" echo " Arch: sudo pacman -S --needed extra-cmake-modules kf6 qt6-base qt6-declarative" echo "" printf "\n\033[1;33mIf you continue to see this issue\033[0m rerun as follows and attach the full output to a bug report:\n" From 07e45427954114412c0fcd97d32d0e36c1865429 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Thu, 4 Sep 2025 13:21:30 -0700 Subject: [PATCH 23/41] Fix setup script re-run suggestion so it provides all params it was given --- bin/breezy_kwin_setup | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/breezy_kwin_setup b/bin/breezy_kwin_setup index d7aebed..f3fca16 100755 --- a/bin/breezy_kwin_setup +++ b/bin/breezy_kwin_setup @@ -30,7 +30,7 @@ print_missing_dependencies() { echo " Arch: sudo pacman -S --needed extra-cmake-modules kf6 qt6-base qt6-declarative" echo "" printf "\n\033[1;33mIf you continue to see this issue\033[0m rerun as follows and attach the full output to a bug report:\n" - printf "\n\tPRINT_FULL_STDERR=1 %s\n" "$0" + printf "\n\tPRINT_FULL_STDERR=1 %s\n" "$0 $*" exit 1 } @@ -120,7 +120,7 @@ then set -e if [ "$pkgkwin_rc" -ne 0 ]; then if echo "$pkgkwin_stderr" | grep -qi "could not find" && [ -z "${PRINT_FULL_STDERR+x}" ]; then - print_missing_dependencies "$0" + print_missing_dependencies "$*" else echo "$pkgkwin_stderr" fi From ebcd10d548d4a26ffef31f651a4b960905270aeb Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:09:16 -0700 Subject: [PATCH 24/41] Fix arch setup instructions for kwin --- bin/breezy_kwin_setup | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/breezy_kwin_setup b/bin/breezy_kwin_setup index f3fca16..952ecdf 100755 --- a/bin/breezy_kwin_setup +++ b/bin/breezy_kwin_setup @@ -27,7 +27,7 @@ print_missing_dependencies() { echo "Install the corresponding packages with your package manager, then rerun this setup:" echo " Debian/Ubuntu: sudo apt-get update && sudo apt-get install -y extra-cmake-modules kwin-dev libkf6config-dev libkf6configwidgets-dev libkf6coreaddons-dev libkf6kcmutils-dev libkf6globalaccel-dev libkf6i18n-dev libkf6windowsystem-dev libkf6xmlgui-dev qt6-base-dev qt6-declarative-dev" echo " Fedora/RHEL: sudo dnf install -y extra-cmake-modules kwin-devel kf6-kconfig-devel kf6-kconfigwidgets-devel kf6-kcoreaddons-devel kf6-kcmutils-devel kf6-kglobalaccel-devel kf6-ki18n-devel kf6-kwindowsystem-devel kf6-kxmlgui-devel qt6-qtbase-devel qt6-qttools-devel wayland-devel libepoxy-devel" - echo " Arch: sudo pacman -S --needed extra-cmake-modules kf6 qt6-base qt6-declarative" + echo " Arch: sudo pacman -S --needed extra-cmake-modules qt6-base qt6-declarative qt6-tools kconfig kconfigwidgets kcoreaddons kglobalaccel ki18n kcmutils kxmlgui kwindowsystem kwin" echo "" printf "\n\033[1;33mIf you continue to see this issue\033[0m rerun as follows and attach the full output to a bug report:\n" printf "\n\tPRINT_FULL_STDERR=1 %s\n" "$0 $*" From 638105667ebd9fcfe1e14e414e771a6e7b945efa Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:28:29 -0700 Subject: [PATCH 25/41] Fix setup issue where the lib directory wasn't being properly identified --- kwin/bin/setup | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kwin/bin/setup b/kwin/bin/setup index 88f57af..28a63f1 100755 --- a/kwin/bin/setup +++ b/kwin/bin/setup @@ -48,7 +48,7 @@ pushd breezy_desktop/usr > /dev/null echo "Copying KWin plugin files to $USER_HOME/.local/{lib,share}" # locate the lib path that ends with qt6/plugins (handles multiarch dirs) -QT_PLUGIN_DIR_RELATIVE=$(find lib -type d -path '*/qt6/plugins' -print -quit 2>/dev/null || true) +QT_PLUGIN_DIR_RELATIVE=$(find lib* -type d -path '*/qt6/plugins' -print -quit 2>/dev/null || true) if [ -z "$QT_PLUGIN_DIR_RELATIVE" ]; then QT_PLUGIN_DIR_RELATIVE="lib/qt6/plugins" fi From 205c80445a55cbaf1db0d5815e9474474459d21b Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Fri, 5 Sep 2025 22:19:04 -0700 Subject: [PATCH 26/41] Fix rendering so that moving the cursor doesn't cause the whole screen to rerender --- kwin/src/CMakeLists.txt | 4 ++- kwin/src/breezydesktopeffect.cpp | 9 +++++- kwin/src/breezydesktopeffect.h | 15 +++++---- kwin/src/qml/BreezyDesktop.qml | 27 ++++++++-------- kwin/src/qml/BreezyDesktopDisplay.qml | 45 +++++++++++++++++++++------ kwin/src/qml/CameraController.qml | 24 +++++++------- kwin/src/qml/DesktopView.qml | 14 --------- kwin/src/qml/SingleDesktopView.qml | 43 +++++++++++++++++++++++++ kwin/src/qml/cursorOverlay.frag | 18 +++++++++++ kwin/src/qml/cursorOverlay.vert | 10 ++++++ kwin/src/qml/main.qml | 6 +--- 11 files changed, 153 insertions(+), 62 deletions(-) create mode 100644 kwin/src/qml/SingleDesktopView.qml create mode 100644 kwin/src/qml/cursorOverlay.frag create mode 100644 kwin/src/qml/cursorOverlay.vert diff --git a/kwin/src/CMakeLists.txt b/kwin/src/CMakeLists.txt index 5b88fdd..8cbb15a 100644 --- a/kwin/src/CMakeLists.txt +++ b/kwin/src/CMakeLists.txt @@ -67,4 +67,6 @@ target_link_libraries(breezy_desktop ) -install(DIRECTORY qml DESTINATION ${KDE_INSTALL_DATADIR}/kwin/effects/breezy_desktop) \ No newline at end of file +install(DIRECTORY qml DESTINATION ${KDE_INSTALL_DATADIR}/kwin/effects/breezy_desktop) +install(FILES qml/cursorOverlay.frag DESTINATION ${KDE_INSTALL_DATADIR}/kwin/effects/breezy_desktop/qml) +install(FILES qml/cursorOverlay.vert DESTINATION ${KDE_INSTALL_DATADIR}/kwin/effects/breezy_desktop/qml) \ No newline at end of file diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index fbe5221..55ecdc4 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -540,6 +540,11 @@ QString BreezyDesktopEffect::cursorImageSource() const return m_cursorImageSource; } +QSize BreezyDesktopEffect::cursorImageSize() const +{ + return m_cursorImageSize; +} + QPointF BreezyDesktopEffect::cursorPos() const { return m_cursorPos; @@ -566,10 +571,12 @@ void BreezyDesktopEffect::updateCursorImage() cursor.image().save(&buffer, "PNG"); m_cursorImageSource = QStringLiteral("data:image/png;base64,%1").arg(QString::fromLatin1(data.toBase64())); + m_cursorImageSize = cursor.image().size(); } else { m_cursorImageSource = QString(); + m_cursorImageSize = QSize(); } - Q_EMIT cursorImageChanged(); + Q_EMIT cursorImageSourceChanged(); } void BreezyDesktopEffect::updateCursorPos() diff --git a/kwin/src/breezydesktopeffect.h b/kwin/src/breezydesktopeffect.h index 6011c42..4d21cb6 100644 --- a/kwin/src/breezydesktopeffect.h +++ b/kwin/src/breezydesktopeffect.h @@ -16,11 +16,12 @@ namespace KWin Q_OBJECT Q_PROPERTY(bool isEnabled READ isEnabled NOTIFY enabledStateChanged) Q_PROPERTY(bool zoomOnFocusEnabled READ isZoomOnFocusEnabled WRITE setZoomOnFocusEnabled NOTIFY zoomOnFocusChanged) - Q_PROPERTY(bool imuResetState READ imuResetState NOTIFY imuRotationsChanged) - Q_PROPERTY(QList imuRotations READ imuRotations NOTIFY imuRotationsChanged) - Q_PROPERTY(quint32 imuTimeElapsedMs READ imuTimeElapsedMs NOTIFY imuRotationsChanged) - Q_PROPERTY(quint64 imuTimestamp READ imuTimestamp NOTIFY imuRotationsChanged) - Q_PROPERTY(QString cursorImageSource READ cursorImageSource NOTIFY cursorImageChanged) + Q_PROPERTY(bool imuResetState READ imuResetState) + Q_PROPERTY(QList imuRotations READ imuRotations) + Q_PROPERTY(quint32 imuTimeElapsedMs READ imuTimeElapsedMs) + Q_PROPERTY(quint64 imuTimestamp READ imuTimestamp) + Q_PROPERTY(QString cursorImageSource READ cursorImageSource NOTIFY cursorImageSourceChanged) + Q_PROPERTY(QSize cursorImageSize READ cursorImageSize NOTIFY cursorImageSourceChanged) Q_PROPERTY(QPointF cursorPos READ cursorPos NOTIFY cursorPosChanged) Q_PROPERTY(QList lookAheadConfig READ lookAheadConfig NOTIFY devicePropertiesChanged) Q_PROPERTY(QList displayResolution READ displayResolution NOTIFY devicePropertiesChanged) @@ -44,6 +45,7 @@ namespace KWin int requestedEffectChainPosition() const override; QString cursorImageSource() const; + QSize cursorImageSize() const; QPointF cursorPos() const; bool isEnabled() const; @@ -91,7 +93,7 @@ namespace KWin void enabledStateChanged(); void zoomOnFocusChanged(); void imuRotationsChanged(); - void cursorImageChanged(); + void cursorImageSourceChanged(); void cursorPosChanged(); void devicePropertiesChanged(); @@ -107,6 +109,7 @@ namespace KWin QTimer *m_shutdownTimer; QString m_cursorImageSource; + QSize m_cursorImageSize; bool m_enabled = false; bool m_zoomOnFocusEnabled = false; diff --git a/kwin/src/qml/BreezyDesktop.qml b/kwin/src/qml/BreezyDesktop.qml index 68fa21c..4520c1e 100644 --- a/kwin/src/qml/BreezyDesktop.qml +++ b/kwin/src/qml/BreezyDesktop.qml @@ -9,7 +9,6 @@ Node { property var screens: root.screens property var fovDetails: root.fovDetails property var monitorPlacements: root.monitorPlacements - property var imuRotations: effect.imuRotations property int focusedMonitorIndex: -1 Displays { @@ -29,10 +28,17 @@ Node { delegate: BreezyDesktopDisplay { screen: breezyDesktop.screens[index] monitorPlacement: breezyDesktop.monitorPlacements[index] + property real monitorDistance: effect.allDisplaysDistance property real targetDistance: effect.allDisplaysDistance property real screenRotationY: displays.radianToDegree(monitorPlacement.rotationAngleRadians.y) property real screenRotationX: displays.radianToDegree(monitorPlacement.rotationAngleRadians.x) + property matrix4x4 rotationMatrix: { + const matrix = Qt.matrix4x4(); + matrix.rotate(screenRotationY, Qt.vector3d(0, 1, 0)); + matrix.rotate(screenRotationX, Qt.vector3d(1, 0, 0)); + return matrix; + } property vector3d screenScale: { const geometry = screen.geometry; @@ -45,19 +51,12 @@ Node { eulerRotation.y: screenRotationY eulerRotation.x: screenRotationX position: { - // camera looks along the negative Z axis - const positionVector = - displays.nwuToEusVector(monitorPlacement.centerNoRotate) - .times(monitorDistance / effect.allDisplaysDistance); + const displayNwu = + monitorPlacement.centerNoRotate + .times(monitorDistance / effect.allDisplaysDistance); - // position vector is only translated in flat directions, without rotations applied, so apply them here - const rotationMatrix = Qt.matrix4x4(); - // only one of these should ever be non-zero, since we only rotate in the direction of the "wrap" preference - rotationMatrix.rotate(screenRotationY, Qt.vector3d(0, 1, 0)); - rotationMatrix.rotate(screenRotationX, Qt.vector3d(1, 0, 0)); - - return rotationMatrix.times(positionVector); + return rotationMatrix.times(displays.nwuToEusVector(displayNwu)); } } } @@ -67,12 +66,12 @@ Node { repeat: true running: true onTriggered: { - if (breezyDesktop.imuRotations && breezyDesktop.imuRotations.length > 0) { + if (effect.imuRotations && effect.imuRotations.length > 0) { let focusedIndex = -1; if (effect.zoomOnFocusEnabled) { focusedIndex = displays.findFocusedMonitor( - displays.eusToNwuQuat(breezyDesktop.imuRotations[0]), + displays.eusToNwuQuat(effect.imuRotations[0]), breezyDesktop.monitorPlacements.map(monitorVectors => monitorVectors.centerLook), breezyDesktop.focusedMonitorIndex, false, // TODO smooth follow diff --git a/kwin/src/qml/BreezyDesktopDisplay.qml b/kwin/src/qml/BreezyDesktopDisplay.qml index d7ea78a..e192c11 100644 --- a/kwin/src/qml/BreezyDesktopDisplay.qml +++ b/kwin/src/qml/BreezyDesktopDisplay.qml @@ -8,19 +8,46 @@ Model { required property var monitorPlacement required property int index + property string cursorImageSource: effect.cursorImageSource + property size cursorImageSize: effect.cursorImageSize + property point cursorPos: effect.cursorPos + source: "#Rectangle" materials: [ - DefaultMaterial { - cullMode: Material.NoCulling - lighting: DefaultMaterial.NoLighting - depthDrawMode: Material.AlwaysDepthDraw - diffuseMap: Texture { - sourceItem: DesktopView { - screen: display.screen - width: display.screen.geometry.width - height: display.screen.geometry.height + CustomMaterial { + id: customMat + depthDrawMode: CustomMaterial.AlwaysDepthDraw + shadingMode: CustomMaterial.Unshaded + + property real screenWidth: display.screen.geometry.width + property real screenHeight: display.screen.geometry.height + property real cursorX: display.cursorPos.x - display.screen.geometry.x + property real cursorY: display.cursorPos.y - display.screen.geometry.y + property real cursorW: display.cursorImageSize.width + property real cursorH: display.cursorImageSize.height + property bool showCursor: cursorX >= 0 && cursorX < screenWidth && cursorY >= 0 && cursorY < screenHeight + + property TextureInput desktopTex: TextureInput { + texture: Texture { + sourceItem: DesktopView { + screen: display.screen + width: display.screen.geometry.width + height: display.screen.geometry.height + } } } + property TextureInput cursorTex: TextureInput { + texture: Texture { + sourceItem: Image { + source: effect.cursorImageSource + width: effect.cursorImageSize.width + height: effect.cursorImageSize.height + } + } + } + + fragmentShader: "cursorOverlay.frag" + vertexShader: "cursorOverlay.vert" } ] } diff --git a/kwin/src/qml/CameraController.qml b/kwin/src/qml/CameraController.qml index 5cf3853..ce69a9c 100644 --- a/kwin/src/qml/CameraController.qml +++ b/kwin/src/qml/CameraController.qml @@ -6,10 +6,6 @@ Item { required property Camera camera - property var imuRotations: effect.imuRotations - property int imuTimeElapsedMs: effect.imuTimeElapsedMs - property double imuTimestamp: effect.imuTimestamp - property var lookAheadConfig: effect.lookAheadConfig property var displayResolution: effect.displayResolution property real diagonalFOV: effect.diagonalFOV property real lensDistanceRatio: effect.lensDistanceRatio @@ -67,14 +63,18 @@ Item { onDisplayResolutionChanged: updateFOV(); onDiagonalFOVChanged: updateFOV(); - onImuRotationsChanged: { - if (root.imuRotations && root.imuRotations.length > 0) { - updateCamera(applyLookAhead( - root.imuRotations[0], - root.imuRotations[1], - root.imuTimeElapsedMs, - lookAheadMS(root.imuTimestamp, root.lookAheadConfig, -1) - )); + + FrameAnimation { + running: true + onTriggered: { + if (effect.imuRotations && effect.imuRotations.length > 0) { + updateCamera(applyLookAhead( + effect.imuRotations[0], + effect.imuRotations[1], + effect.imuTimeElapsedMs, + lookAheadMS(effect.imuTimestamp, effect.lookAheadConfig, -1) + )); + } } } } diff --git a/kwin/src/qml/DesktopView.qml b/kwin/src/qml/DesktopView.qml index 03cacd6..10d9940 100644 --- a/kwin/src/qml/DesktopView.qml +++ b/kwin/src/qml/DesktopView.qml @@ -40,18 +40,4 @@ Item { visible: onThisScreen && !model.window.minimized } } - - Image { - id: cursorImg - source: effect.cursorImageSource - cache: false - visible: true // TODO - cursor position bounds check? - x: effect.cursorPos.x - desktopView.screen.geometry.x - y: effect.cursorPos.y - desktopView.screen.geometry.y - z: 9999 // ensure on top - anchors.centerIn: undefined - - layer.enabled: true - layer.smooth: true - } } diff --git a/kwin/src/qml/SingleDesktopView.qml b/kwin/src/qml/SingleDesktopView.qml new file mode 100644 index 0000000..3d0151f --- /dev/null +++ b/kwin/src/qml/SingleDesktopView.qml @@ -0,0 +1,43 @@ +import QtQuick + +Item { + id: singleDesktopView + property point cursorPos: effect.cursorPos + + function cursorInBounds() { + const x = cursorPos.x + const y = cursorPos.y + const screenGeom = targetScreen.geometry + return x >= screenGeom.x && + x < screenGeom.x + screenGeom.width && + y >= screenGeom.y && + y < screenGeom.y + screenGeom.height + } + + DesktopView { + screen: targetScreen + width: targetScreen.geometry.width + height: targetScreen.geometry.height + } + + Image { + id: cursorImg + x: 0 + y: 0 + z: 9999 // ensure on top + } + + onCursorPosChanged: { + if (singleDesktopView.cursorInBounds()) { + const newX = effect.cursorPos.x - targetScreen.geometry.x + const newY = effect.cursorPos.y - targetScreen.geometry.y + const newSrc = effect.cursorImageSource + if (cursorImg.x !== newX) cursorImg.x = newX + if (cursorImg.y !== newY) cursorImg.y = newY + if (cursorImg.source !== newSrc) cursorImg.source = newSrc + if (!cursorImg.visible) cursorImg.visible = true + } else if (cursorImg.visible) { + cursorImg.visible = false + } + } +} \ No newline at end of file diff --git a/kwin/src/qml/cursorOverlay.frag b/kwin/src/qml/cursorOverlay.frag new file mode 100644 index 0000000..4660e60 --- /dev/null +++ b/kwin/src/qml/cursorOverlay.frag @@ -0,0 +1,18 @@ +VARYING vec3 pos; +VARYING vec2 texcoord; + +void MAIN() { + vec2 tex = vec2(texcoord.x, 1.0 - texcoord.y); + vec4 color = texture(desktopTex, tex); + if (showCursor) { + vec2 fragCoord = tex * vec2(screenWidth, screenHeight); + vec2 cursorTopLeft = vec2(cursorX, cursorY); + vec2 cursorBottomRight = cursorTopLeft + vec2(cursorW, cursorH); + if (fragCoord.x >= cursorTopLeft.x && fragCoord.x < cursorBottomRight.x && fragCoord.y >= cursorTopLeft.y && fragCoord.y < cursorBottomRight.y) { + vec2 rel = (fragCoord - cursorTopLeft) / vec2(cursorW, cursorH); + vec4 cursorCol = texture(cursorTex, rel); + color = mix(color, cursorCol, cursorCol.a); + } + } + FRAGCOLOR = color; +} \ No newline at end of file diff --git a/kwin/src/qml/cursorOverlay.vert b/kwin/src/qml/cursorOverlay.vert new file mode 100644 index 0000000..4dc6bf3 --- /dev/null +++ b/kwin/src/qml/cursorOverlay.vert @@ -0,0 +1,10 @@ +VARYING vec3 pos; +VARYING vec2 texcoord; + +// this is a no-op vertex shader, CustomMaterial required one +void MAIN() +{ + pos = VERTEX; + texcoord = UV0; + POSITION = MODELVIEWPROJECTION_MATRIX * vec4(pos, 1.0); +} \ No newline at end of file diff --git a/kwin/src/qml/main.qml b/kwin/src/qml/main.qml index 6b9a615..01cd949 100644 --- a/kwin/src/qml/main.qml +++ b/kwin/src/qml/main.qml @@ -89,11 +89,7 @@ Item { Component { id: desktopViewComponent - DesktopView { - screen: root.targetScreen - width: root.targetScreen.geometry.width - height: root.targetScreen.geometry.height - } + SingleDesktopView {} } Component { From 4682153ed37279271e60ca88d8dff1667abe9cb3 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Sat, 6 Sep 2025 11:46:08 -0700 Subject: [PATCH 27/41] Add libdrm for ubuntu dependencies message --- bin/breezy_kwin_setup | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/breezy_kwin_setup b/bin/breezy_kwin_setup index 952ecdf..b66709a 100755 --- a/bin/breezy_kwin_setup +++ b/bin/breezy_kwin_setup @@ -25,7 +25,7 @@ print_missing_dependencies() { printf "\n\033[1;31mMissing required components\033[0m\n" echo "" echo "Install the corresponding packages with your package manager, then rerun this setup:" - echo " Debian/Ubuntu: sudo apt-get update && sudo apt-get install -y extra-cmake-modules kwin-dev libkf6config-dev libkf6configwidgets-dev libkf6coreaddons-dev libkf6kcmutils-dev libkf6globalaccel-dev libkf6i18n-dev libkf6windowsystem-dev libkf6xmlgui-dev qt6-base-dev qt6-declarative-dev" + echo " Debian/Ubuntu: sudo apt-get update && sudo apt-get install -y extra-cmake-modules kwin-dev libkf6config-dev libkf6configwidgets-dev libkf6coreaddons-dev libkf6kcmutils-dev libkf6globalaccel-dev libkf6i18n-dev libkf6windowsystem-dev libkf6xmlgui-dev qt6-base-dev qt6-declarative-dev libdrm-dev" echo " Fedora/RHEL: sudo dnf install -y extra-cmake-modules kwin-devel kf6-kconfig-devel kf6-kconfigwidgets-devel kf6-kcoreaddons-devel kf6-kcmutils-devel kf6-kglobalaccel-devel kf6-ki18n-devel kf6-kwindowsystem-devel kf6-kxmlgui-devel qt6-qtbase-devel qt6-qttools-devel wayland-devel libepoxy-devel" echo " Arch: sudo pacman -S --needed extra-cmake-modules qt6-base qt6-declarative qt6-tools kconfig kconfigwidgets kcoreaddons kglobalaccel ki18n kcmutils kxmlgui kwindowsystem kwin" echo "" From f01c6351380d243d0471dc1c9f7530e949e180d6 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Sat, 6 Sep 2025 12:42:31 -0700 Subject: [PATCH 28/41] Add calibrating banner --- .gitignore | 4 +++- bin/package_kwin | 1 + kwin/src/CMakeLists.txt | 4 +--- kwin/src/breezydesktopeffect.cpp | 5 ++++- kwin/src/breezydesktopeffect.h | 4 ++-- kwin/src/qml/SingleDesktopView.qml | 10 ++++++++++ kwin/src/qml/main.qml | 25 ++++++++++++++++++++++--- 7 files changed, 43 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 4eb9fcf..40559c0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ out/ *.po~ kwin/src/xrdriveripc/xrdriveripc.py kwin/VERSION -kwin/build-test/ \ No newline at end of file +kwin/build-test/ +kwin/src/qml/calibrating.png +kwin/src/qml/custom_banner.png diff --git a/bin/package_kwin b/bin/package_kwin index af1dd34..6ba691f 100755 --- a/bin/package_kwin +++ b/bin/package_kwin @@ -75,6 +75,7 @@ fi # this file is in .gitignore so it doesn't get duplicated cp ui/modules/PyXRLinuxDriverIPC/xrdriveripc.py $KWIN_DIR/src/xrdriveripc/xrdriveripc.py cp VERSION $KWIN_DIR +cp modules/sombrero/*.png $KWIN_DIR/src/qml pushd $KWIN_DIR > /dev/null if [ -z "${LOCAL_BUILD_SYSTEM+x}" ]; then diff --git a/kwin/src/CMakeLists.txt b/kwin/src/CMakeLists.txt index 8cbb15a..5b88fdd 100644 --- a/kwin/src/CMakeLists.txt +++ b/kwin/src/CMakeLists.txt @@ -67,6 +67,4 @@ target_link_libraries(breezy_desktop ) -install(DIRECTORY qml DESTINATION ${KDE_INSTALL_DATADIR}/kwin/effects/breezy_desktop) -install(FILES qml/cursorOverlay.frag DESTINATION ${KDE_INSTALL_DATADIR}/kwin/effects/breezy_desktop/qml) -install(FILES qml/cursorOverlay.vert DESTINATION ${KDE_INSTALL_DATADIR}/kwin/effects/breezy_desktop/qml) \ No newline at end of file +install(DIRECTORY qml DESTINATION ${KDE_INSTALL_DATADIR}/kwin/effects/breezy_desktop) \ No newline at end of file diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index 55ecdc4..8b5f91d 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -509,7 +509,11 @@ void BreezyDesktopEffect::updateImuRotation() { float imuData[4 * DataView::IMU_QUAT_ENTRIES]; // 4 quaternion-sized rows memcpy(imuData, data + DataView::IMU_QUAT_DATA[DataView::OFFSET_INDEX], sizeof(imuData)); + bool wasImuResetState = m_imuResetState; m_imuResetState = (imuData[0] == 0.0f && imuData[1] == 0.0f && imuData[2] == 0.0f && imuData[3] == 1.0f); + if (m_imuResetState != wasImuResetState) { + Q_EMIT imuResetStateChanged(); + } // convert NWU to EUS by passing root.rotation values: -y, z, -x QQuaternion quatT0(imuData[3], -imuData[1], imuData[2], -imuData[0]); @@ -532,7 +536,6 @@ void BreezyDesktopEffect::updateImuRotation() { m_imuTimeElapsedMs = static_cast(imuData[imuDataOffset + 0] - imuData[imuDataOffset + 1]); m_imuTimestamp = imuDateMs; - Q_EMIT imuRotationsChanged(); } QString BreezyDesktopEffect::cursorImageSource() const diff --git a/kwin/src/breezydesktopeffect.h b/kwin/src/breezydesktopeffect.h index 4d21cb6..9ef137e 100644 --- a/kwin/src/breezydesktopeffect.h +++ b/kwin/src/breezydesktopeffect.h @@ -16,7 +16,7 @@ namespace KWin Q_OBJECT Q_PROPERTY(bool isEnabled READ isEnabled NOTIFY enabledStateChanged) Q_PROPERTY(bool zoomOnFocusEnabled READ isZoomOnFocusEnabled WRITE setZoomOnFocusEnabled NOTIFY zoomOnFocusChanged) - Q_PROPERTY(bool imuResetState READ imuResetState) + Q_PROPERTY(bool imuResetState READ imuResetState NOTIFY imuResetStateChanged) Q_PROPERTY(QList imuRotations READ imuRotations) Q_PROPERTY(quint32 imuTimeElapsedMs READ imuTimeElapsedMs) Q_PROPERTY(quint64 imuTimestamp READ imuTimestamp) @@ -92,7 +92,7 @@ namespace KWin void displayWrappingSchemeChanged(); void enabledStateChanged(); void zoomOnFocusChanged(); - void imuRotationsChanged(); + void imuResetStateChanged(); void cursorImageSourceChanged(); void cursorPosChanged(); void devicePropertiesChanged(); diff --git a/kwin/src/qml/SingleDesktopView.qml b/kwin/src/qml/SingleDesktopView.qml index 3d0151f..88eeccd 100644 --- a/kwin/src/qml/SingleDesktopView.qml +++ b/kwin/src/qml/SingleDesktopView.qml @@ -3,6 +3,8 @@ import QtQuick Item { id: singleDesktopView property point cursorPos: effect.cursorPos + property bool supportsXR: false + property bool showCalibratingBanner: false function cursorInBounds() { const x = cursorPos.x @@ -15,6 +17,7 @@ Item { } DesktopView { + id: desktopViewComponent screen: targetScreen width: targetScreen.geometry.width height: targetScreen.geometry.height @@ -27,6 +30,13 @@ Item { z: 9999 // ensure on top } + Image { + source: effect.customBannerEnabled ? "custom_banner.png" : "calibrating.png" + visible: supportsXR && showCalibratingBanner + anchors.horizontalCenter: desktopViewComponent.horizontalCenter + anchors.bottom: desktopViewComponent.bottom + } + onCursorPosChanged: { if (singleDesktopView.cursorInBounds()) { const newX = effect.cursorPos.x - targetScreen.geometry.x diff --git a/kwin/src/qml/main.qml b/kwin/src/qml/main.qml index 01cd949..2794a05 100644 --- a/kwin/src/qml/main.qml +++ b/kwin/src/qml/main.qml @@ -87,9 +87,16 @@ Item { return displays.monitorsToPlacements(fovDetails, adjustedGeometries, effect.displaySpacing); } + property bool targetScreenSupported: supportedModels.some(model => root.targetScreen.model.endsWith(model)) + property bool imuResetState: effect.imuResetState + property bool isEnabled: effect.isEnabled + Component { id: desktopViewComponent - SingleDesktopView {} + SingleDesktopView { + supportsXR: targetScreenSupported + showCalibratingBanner: isEnabled && imuResetState + } } Component { @@ -121,9 +128,21 @@ Item { id: viewLoader anchors.fill: parent } + + function checkLoadedComponent() { + const show3DView = targetScreenSupported && isEnabled && !imuResetState; + viewLoader.sourceComponent = show3DView ? view3DComponent : desktopViewComponent; + } + + onImuResetStateChanged: { + checkLoadedComponent(); + } + + onIsEnabledChanged: { + checkLoadedComponent(); + } Component.onCompleted: { - const targetScreenSupported = supportedModels.some(model => root.targetScreen.model.endsWith(model)); - viewLoader.sourceComponent = targetScreenSupported ? view3DComponent : desktopViewComponent; + checkLoadedComponent(); } } From 229ee9749ab623419316ad918ef076816a7842fd Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Sat, 6 Sep 2025 21:59:00 -0700 Subject: [PATCH 29/41] Update KWin setup to check out the sombrero submodule --- bin/breezy_kwin_setup | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/breezy_kwin_setup b/bin/breezy_kwin_setup index b66709a..e4c9c1b 100755 --- a/bin/breezy_kwin_setup +++ b/bin/breezy_kwin_setup @@ -108,6 +108,7 @@ then echo "Downloading git submodules" git submodule sync --recursive || true git submodule update --init modules/XRLinuxDriver + git submodule update --init modules/sombrero git submodule update --init ui/modules/PyXRLinuxDriverIPC else pushd $start_dir > /dev/null From 8967c006668028df14b13c8c5c3e6f343bbb4df9 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Mon, 8 Sep 2025 10:38:49 -0700 Subject: [PATCH 30/41] Fix issue where cursor doesn't reappear when disabling the KWin plugin deactivate() wasn't getting triggered, add a deconstructor --- kwin/src/breezydesktopeffect.cpp | 35 ++++++++++++++++---------------- kwin/src/breezydesktopeffect.h | 4 ++-- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index 8b5f91d..7ae4f49 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -92,14 +92,10 @@ namespace KWin { BreezyDesktopEffect::BreezyDesktopEffect() - : m_shutdownTimer(new QTimer(this)) { qCCritical(KWIN_XR) << "\t\t\tBreezy - constructor"; qmlRegisterUncreatableType("org.kde.kwin.effect.breezy_desktop", 1, 0, "BreezyDesktopEffect", QStringLiteral("BreezyDesktop cannot be created in QML")); - m_shutdownTimer->setSingleShot(true); - connect(m_shutdownTimer, &QTimer::timeout, this, &BreezyDesktopEffect::realDeactivate); - setupGlobalShortcut( BreezyShortcuts::TOGGLE, [this]() { this->toggle(); } @@ -165,6 +161,23 @@ BreezyDesktopEffect::BreezyDesktopEffect() } } +BreezyDesktopEffect::~BreezyDesktopEffect() +{ + qCCritical(KWIN_XR) << "\t\t\tBreezy - destructor"; + if (m_shmFileWatcher) { + if (!DataView::SHM_PATH.isEmpty()) { + m_shmFileWatcher->removePath(DataView::SHM_PATH); + } + m_shmFileWatcher->deleteLater(); + m_shmFileWatcher = nullptr; + } + if (m_shmDirectoryWatcher) { + m_shmDirectoryWatcher->deleteLater(); + m_shmDirectoryWatcher = nullptr; + } + deactivate(); +} + void BreezyDesktopEffect::setupGlobalShortcut(const BreezyShortcuts::Shortcut &shortcut, std::function triggeredFunc) { QAction *action = new QAction(this); action->setObjectName(shortcut.actionName); @@ -241,10 +254,6 @@ void BreezyDesktopEffect::activate() void BreezyDesktopEffect::deactivate() { - if (m_shutdownTimer->isActive()) { - return; - } - qCCritical(KWIN_XR) << "\t\t\tBreezy - deactivate"; disconnect(effects, &EffectsHandler::cursorShapeChanged, this, &BreezyDesktopEffect::updateCursorImage); m_cursorUpdateTimer->stop(); @@ -255,9 +264,7 @@ void BreezyDesktopEffect::deactivate() } m_virtualOutputs.clear(); - // this triggers realDeactivate with a delay so if it's triggered from QML it gives the QML function time to - // exit, avoiding a crash - m_shutdownTimer->start(250); + setRunning(false); } void BreezyDesktopEffect::enableDriver() @@ -270,12 +277,6 @@ void BreezyDesktopEffect::enableDriver() XRDriverIPC::instance().writeConfig(obj); } -void BreezyDesktopEffect::realDeactivate() -{ - qCCritical(KWIN_XR) << "\t\t\tBreezy - realDeactivate"; - setRunning(false); -} - void BreezyDesktopEffect::addVirtualDisplay(QSize size) { // QSize size(2560, 1440); diff --git a/kwin/src/breezydesktopeffect.h b/kwin/src/breezydesktopeffect.h index 9ef137e..cc8daa4 100644 --- a/kwin/src/breezydesktopeffect.h +++ b/kwin/src/breezydesktopeffect.h @@ -39,6 +39,7 @@ namespace KWin public: BreezyDesktopEffect(); + ~BreezyDesktopEffect() override; void reconfigure(ReconfigureFlags) override; @@ -101,13 +102,12 @@ namespace KWin QVariantMap initialProperties(Output *screen) override; private: - void realDeactivate(); + void teardown(); bool checkParityByte(const char* data); void setupGlobalShortcut(const BreezyShortcuts::Shortcut &shortcut, std::function triggeredFunc); void recenter(); - QTimer *m_shutdownTimer; QString m_cursorImageSource; QSize m_cursorImageSize; From 713b9c7fc1e934c76334e5aaead21e6cbe71cb7c Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Mon, 8 Sep 2025 11:35:22 -0700 Subject: [PATCH 31/41] Pull in latest XR driver --- modules/XRLinuxDriver | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/XRLinuxDriver b/modules/XRLinuxDriver index bfd3b59..69d3603 160000 --- a/modules/XRLinuxDriver +++ b/modules/XRLinuxDriver @@ -1 +1 @@ -Subproject commit bfd3b5908d6a7a1c5c52878a544648f3d30fa15e +Subproject commit 69d3603ef9c931a0f66777279501d4c6dfe20f09 From 343205f598a913ac5e6bf1215aec244d2b15613f Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Mon, 8 Sep 2025 12:44:26 -0700 Subject: [PATCH 32/41] Improve license messaging, effect toggling, and add button for rearranging displays --- kwin/src/breezydesktopeffect.cpp | 20 ++++-- kwin/src/breezydesktopeffect.h | 1 + kwin/src/kcm/breezydesktopeffectkcm.cpp | 85 +++++++++++++++++++++---- kwin/src/kcm/breezydesktopeffectkcm.h | 2 + kwin/src/kcm/breezydesktopeffectkcm.ui | 44 ++++++++++++- 5 files changed, 131 insertions(+), 21 deletions(-) diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index 7ae4f49..4e404a4 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -226,11 +226,11 @@ int BreezyDesktopEffect::requestedEffectChainPosition() const void BreezyDesktopEffect::toggle() { if (isRunning()) { - qCCritical(KWIN_XR) << "\t\t\tBreezy - toggle - deactivating"; - deactivate(); + qCCritical(KWIN_XR) << "\t\t\tBreezy - toggle - disabling"; + disableDriver(); } else { - qCCritical(KWIN_XR) << "\t\t\tBreezy - toggle - activating"; - activate(); + qCCritical(KWIN_XR) << "\t\t\tBreezy - toggle - enabling"; + enableDriver(); } } @@ -277,11 +277,17 @@ void BreezyDesktopEffect::enableDriver() XRDriverIPC::instance().writeConfig(obj); } +void BreezyDesktopEffect::disableDriver() +{ + qCCritical(KWIN_XR) << "\t\t\tBreezy - disableDriver"; + QJsonObject obj; + obj.insert(QStringLiteral("disabled"), true); + obj.insert(QStringLiteral("external_mode"), QStringLiteral("none")); + XRDriverIPC::instance().writeConfig(obj); +} + void BreezyDesktopEffect::addVirtualDisplay(QSize size) { - // QSize size(2560, 1440); - // addVirtualDisplay(size); - static int virtualDisplayCount = 0; ++virtualDisplayCount; QString name = QStringLiteral("BreezyDesktop_VirtualDisplay_%1x%2_%3").arg(size.width()).arg(size.height()).arg(virtualDisplayCount); diff --git a/kwin/src/breezydesktopeffect.h b/kwin/src/breezydesktopeffect.h index cc8daa4..674605f 100644 --- a/kwin/src/breezydesktopeffect.h +++ b/kwin/src/breezydesktopeffect.h @@ -79,6 +79,7 @@ namespace KWin void activate(); void deactivate(); void enableDriver(); + void disableDriver(); void toggle(); void addVirtualDisplay(QSize size); void updateImuRotation(); diff --git a/kwin/src/kcm/breezydesktopeffectkcm.cpp b/kwin/src/kcm/breezydesktopeffectkcm.cpp index 51c6c2f..318361d 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.cpp +++ b/kwin/src/kcm/breezydesktopeffectkcm.cpp @@ -20,7 +20,9 @@ #include #include #include -#include +#include +#include +#include #include #include #include @@ -157,6 +159,16 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu callAddVirtualDisplay(2560, 1440); }); } + + // General tab: Open KDE Displays Settings + if (auto btnDisplays = widget()->findChild(QStringLiteral("buttonOpenDisplaysSettings"))) { + connect(btnDisplays, &QPushButton::clicked, this, [this]() { + // Try launching the KScreen KCM + if (!QProcess::startDetached(QStringLiteral("kcmshell6"), {QStringLiteral("kcm_kscreen")})) { + QDesktopServices::openUrl(QUrl(QStringLiteral("systemsettings://kcm_kscreen"))); + } + }); + } } BreezyDesktopEffectConfig::~BreezyDesktopEffectConfig() @@ -218,6 +230,25 @@ void BreezyDesktopEffectConfig::updateUnmanagedState() { } +void BreezyDesktopEffectConfig::enableDriver() +{ + qCCritical(KWIN_XR) << "\t\t\tBreezy config - enableDriver"; + QJsonObject obj; + obj.insert(QStringLiteral("disabled"), false); + obj.insert(QStringLiteral("output_mode"), QStringLiteral("external_only")); + obj.insert(QStringLiteral("external_mode"), QStringLiteral("breezy_desktop")); + XRDriverIPC::instance().writeConfig(obj); +} + +void BreezyDesktopEffectConfig::disableDriver() +{ + qCCritical(KWIN_XR) << "\t\t\tBreezy config - disableDriver"; + QJsonObject obj; + obj.insert(QStringLiteral("disabled"), true); + obj.insert(QStringLiteral("external_mode"), QStringLiteral("none")); + XRDriverIPC::instance().writeConfig(obj); +} + void BreezyDesktopEffectConfig::pollDriverState() { auto &bridge = XRDriverIPC::instance(); @@ -304,12 +335,15 @@ void BreezyDesktopEffectConfig::refreshLicenseUi(const QJsonObject &rootObj) { if (!tab) return; auto labelSummary = tab->findChild("labelLicenseSummary"); if (!labelSummary) return; + auto donate = tab->findChild("labelDonateLink"); + auto globalWarn = widget()->findChild("labelGlobalLicenseWarning"); QString status = tr("disabled"); QString renewalDescriptor = QStringLiteral(""); auto uiView = rootObj.value(QStringLiteral("ui_view")).toObject(); auto license = uiView.value(QStringLiteral("license")).toObject(); - bool warningState = true; + bool warningState = false; + bool expired = false; if (!license.isEmpty()) { auto tiers = license.value(QStringLiteral("tiers")).toObject(); QJsonValue prodTier = tiers.value(QStringLiteral("subscriber")); @@ -349,23 +383,48 @@ void BreezyDesktopEffectConfig::refreshLicenseUi(const QJsonObject &rootObj) { } else { QJsonValue isEnabled = prodFeatureObj.value(QStringLiteral("is_enabled")); QJsonValue isTrial = prodFeatureObj.value(QStringLiteral("is_trial")); - if (isEnabled.toBool() && isTrial.toBool()) { - status = tr("in trial"); - auto secsVal = prodFeatureObj.value(QStringLiteral("funds_needed_in_seconds")); - if (secsVal.isDouble()) { - qint64 secs = static_cast(secsVal.toDouble()); - QString remaining = secondsToRemainingString(secs); - warningState = !remaining.isEmpty(); - if (warningState) { - QString timeDescriptor = tr("%1 remaining").arg(remaining); - renewalDescriptor = tr(" (%1)").arg(timeDescriptor); + if (isEnabled.toBool()) { + if (isTrial.toBool()) { + status = tr("in trial"); + auto secsVal = prodFeatureObj.value(QStringLiteral("funds_needed_in_seconds")); + if (secsVal.isDouble()) { + qint64 secs = static_cast(secsVal.toDouble()); + QString remaining = secondsToRemainingString(secs); + warningState = !remaining.isEmpty(); + if (warningState) { + QString timeDescriptor = tr("%1 remaining").arg(remaining); + renewalDescriptor = tr(" (%1)").arg(timeDescriptor); + } } } + } else { + expired = true; } } } } - labelSummary->setText(tr("Productivity Tier features are %1%2").arg(status, renewalDescriptor)); + const QString message = tr("Productivity Tier features are %1%2").arg(status, renewalDescriptor); + labelSummary->setText(message); + + if (donate) donate->setVisible(warningState || expired); + + if (globalWarn) { + if (warningState || expired) { + globalWarn->setText(message + (expired ? tr(" — effect disabled") : QString())); + globalWarn->setVisible(true); + } else { + globalWarn->clear(); + globalWarn->setVisible(false); + } + } + + if (expired) { + if (ui.tabWidget) ui.tabWidget->setEnabled(false); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Effects"), QDBusConnection::sessionBus()); + interface.unloadEffect(QStringLiteral("breezy_desktop")); + } else { + if (ui.tabWidget) ui.tabWidget->setEnabled(true); + } } #include "breezydesktopeffectkcm.moc" \ No newline at end of file diff --git a/kwin/src/kcm/breezydesktopeffectkcm.h b/kwin/src/kcm/breezydesktopeffectkcm.h index d717916..3ffc66a 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.h +++ b/kwin/src/kcm/breezydesktopeffectkcm.h @@ -25,6 +25,8 @@ public Q_SLOTS: void defaults() override; private: + void enableDriver(); + void disableDriver(); void updateUiFromConfig(); void updateUiFromDefaultConfig(); void updateConfigFromUi(); diff --git a/kwin/src/kcm/breezydesktopeffectkcm.ui b/kwin/src/kcm/breezydesktopeffectkcm.ui index 5bcb098..22177f6 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.ui +++ b/kwin/src/kcm/breezydesktopeffectkcm.ui @@ -20,6 +20,25 @@ + + + + + + + false + + + true + + + Qt::AlignHCenter|Qt::AlignVCenter + + + color: rgb(200,0,0); font-weight: bold; + + + @@ -148,10 +167,17 @@ + + + + Rearrange displays + + + - + @@ -271,6 +297,22 @@ + + + + <a href="https://ko-fi.com/wheaney">Renew or support on Ko‑fi</a> + + + true + + + Qt::AlignHCenter|Qt::AlignVCenter + + + false + + + From 2d4c6a96e3382b5222e7e6c05f7ad8e8c3b2848d Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Mon, 8 Sep 2025 13:03:38 -0700 Subject: [PATCH 33/41] Fix minimum display distance when zoom on focus is disabled, fix recentering after initial calibration --- kwin/src/breezydesktopeffect.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index 4e404a4..25e79a7 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -249,7 +249,6 @@ void BreezyDesktopEffect::activate() effects->stopMouseInterception(this); hideCursor(); - recenter(); } void BreezyDesktopEffect::deactivate() @@ -313,6 +312,10 @@ bool BreezyDesktopEffect::isZoomOnFocusEnabled() const { void BreezyDesktopEffect::setZoomOnFocusEnabled(bool enabled) { if (m_zoomOnFocusEnabled != enabled) { m_zoomOnFocusEnabled = enabled; + if (m_zoomOnFocusEnabled && m_focusedDisplayDistance > m_allDisplaysDistance) { + setFocusedDisplayDistance(m_allDisplaysDistance); + BreezyDesktopConfig::setFocusedDisplayDistance(static_cast(m_focusedDisplayDistance * 100.0f)); + } BreezyDesktopConfig::setZoomOnFocusEnabled(enabled); BreezyDesktopConfig::self()->save(); Q_EMIT zoomOnFocusChanged(); @@ -360,7 +363,8 @@ qreal BreezyDesktopEffect::allDisplaysDistance() const { void BreezyDesktopEffect::setAllDisplaysDistance(qreal distance) { if (distance != m_allDisplaysDistance) { - m_allDisplaysDistance = std::clamp(distance, m_focusedDisplayDistance, 2.5); + qreal min = m_zoomOnFocusEnabled ? m_focusedDisplayDistance : 0.2; + m_allDisplaysDistance = std::clamp(distance, min, 2.5); Q_EMIT allDisplaysDistanceChanged(); } } @@ -519,6 +523,7 @@ void BreezyDesktopEffect::updateImuRotation() { bool wasImuResetState = m_imuResetState; m_imuResetState = (imuData[0] == 0.0f && imuData[1] == 0.0f && imuData[2] == 0.0f && imuData[3] == 1.0f); if (m_imuResetState != wasImuResetState) { + if (m_imuResetState) recenter(); Q_EMIT imuResetStateChanged(); } From 35e2b2d71d40ccab5daa72e003a51dc814634d67 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Mon, 8 Sep 2025 13:37:12 -0700 Subject: [PATCH 34/41] Add anti-aliasing quality menu --- kwin/src/breezydesktopconfig.kcfg | 7 +++++ kwin/src/breezydesktopeffect.cpp | 6 ++++ kwin/src/breezydesktopeffect.h | 4 +++ kwin/src/kcm/breezydesktopeffectkcm.cpp | 2 ++ kwin/src/kcm/breezydesktopeffectkcm.ui | 37 +++++++++++++++++++++++-- kwin/src/qml/main.qml | 6 ++-- 6 files changed, 57 insertions(+), 5 deletions(-) diff --git a/kwin/src/breezydesktopconfig.kcfg b/kwin/src/breezydesktopconfig.kcfg index 3e47677..7ba2b62 100644 --- a/kwin/src/breezydesktopconfig.kcfg +++ b/kwin/src/breezydesktopconfig.kcfg @@ -50,5 +50,12 @@ How to arrange monitors: 0=Auto, 1=Horizontal, 2=Vertical, 3=Flat + + 3 + 0 + 3 + + 0=None, 1=Medium, 2=High, 3=Very High + diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index 25e79a7..e1f83e8 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -203,10 +203,12 @@ void BreezyDesktopEffect::reconfigure(ReconfigureFlags) qreal horiz = BreezyDesktopConfig::displayHorizontalOffset() / 100.0f; qreal vert = BreezyDesktopConfig::displayVerticalOffset() / 100.0f; int wrap = BreezyDesktopConfig::displayWrappingScheme(); + int aaQuality = BreezyDesktopConfig::antialiasingQuality(); bool changed = false; if (!qFuzzyCompare(m_displayHorizontalOffset, horiz)) { m_displayHorizontalOffset = horiz; changed = true; } if (!qFuzzyCompare(m_displayVerticalOffset, vert)) { m_displayVerticalOffset = vert; changed = true; } if (m_displayWrappingScheme != wrap) { m_displayWrappingScheme = wrap; Q_EMIT displayWrappingSchemeChanged(); } + if (m_antialiasingQuality != aaQuality) { m_antialiasingQuality = aaQuality; Q_EMIT antialiasingQualityChanged(); } if (changed) Q_EMIT displayOffsetChanged(); } @@ -408,6 +410,10 @@ bool BreezyDesktopEffect::customBannerEnabled() const { return m_customBannerEnabled; } +int BreezyDesktopEffect::antialiasingQuality() const { + return m_antialiasingQuality; +} + bool BreezyDesktopEffect::checkParityByte(const char* data) { const uint8_t parityByte = static_cast(data[DataView::IMU_PARITY_BYTE[DataView::OFFSET_INDEX]]); uint8_t parity = 0; diff --git a/kwin/src/breezydesktopeffect.h b/kwin/src/breezydesktopeffect.h index 674605f..e0ad83b 100644 --- a/kwin/src/breezydesktopeffect.h +++ b/kwin/src/breezydesktopeffect.h @@ -35,6 +35,7 @@ namespace KWin Q_PROPERTY(qreal lensDistanceRatio READ lensDistanceRatio NOTIFY devicePropertiesChanged) Q_PROPERTY(bool sbsEnabled READ sbsEnabled NOTIFY devicePropertiesChanged) Q_PROPERTY(bool customBannerEnabled READ customBannerEnabled NOTIFY devicePropertiesChanged) + Q_PROPERTY(int antialiasingQuality READ antialiasingQuality NOTIFY antialiasingQualityChanged) public: @@ -71,6 +72,7 @@ namespace KWin qreal lensDistanceRatio() const; bool sbsEnabled() const; bool customBannerEnabled() const; + int antialiasingQuality() const; void showCursor(); void hideCursor(); @@ -98,6 +100,7 @@ namespace KWin void cursorImageSourceChanged(); void cursorPosChanged(); void devicePropertiesChanged(); + void antialiasingQualityChanged(); protected: QVariantMap initialProperties(Output *screen) override; @@ -134,6 +137,7 @@ namespace KWin qreal m_displayHorizontalOffset = 0.0; qreal m_displayVerticalOffset = 0.0; int m_displayWrappingScheme = 0; // 0=auto,1=horizontal,2=vertical,3=flat + int m_antialiasingQuality = 3; // 0=None, 1=Medium, 2=High, 3=VeryHigh QList m_virtualOutputs; }; diff --git a/kwin/src/kcm/breezydesktopeffectkcm.cpp b/kwin/src/kcm/breezydesktopeffectkcm.cpp index 318361d..1818e2a 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.cpp +++ b/kwin/src/kcm/breezydesktopeffectkcm.cpp @@ -99,6 +99,7 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu connect(ui.kcfg_DisplayHorizontalOffset, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_DisplayVerticalOffset, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_DisplayWrappingScheme, qOverload(&QComboBox::currentIndexChanged), this, &BreezyDesktopEffectConfig::save); + connect(ui.kcfg_AntialiasingQuality, qOverload(&QComboBox::currentIndexChanged), this, &BreezyDesktopEffectConfig::save); if (auto label = widget()->findChild("labelAppNameVersion")) { label->setText(QStringLiteral("Breezy Desktop - v%1").arg(QLatin1String(BREEZY_DESKTOP_VERSION_STR))); @@ -217,6 +218,7 @@ void BreezyDesktopEffectConfig::updateUiFromConfig() ui.kcfg_DisplayHorizontalOffset->setValue(BreezyDesktopConfig::self()->displayHorizontalOffset()); ui.kcfg_DisplayVerticalOffset->setValue(BreezyDesktopConfig::self()->displayVerticalOffset()); ui.kcfg_DisplayWrappingScheme->setCurrentIndex(BreezyDesktopConfig::self()->displayWrappingScheme()); + ui.kcfg_AntialiasingQuality->setCurrentIndex(BreezyDesktopConfig::self()->antialiasingQuality()); ui.kcfg_ZoomOnFocusEnabled->setChecked(BreezyDesktopConfig::self()->zoomOnFocusEnabled()); ui.kcfg_FocusedDisplayDistance->setEnabled(ui.kcfg_ZoomOnFocusEnabled->isChecked()); } diff --git a/kwin/src/kcm/breezydesktopeffectkcm.ui b/kwin/src/kcm/breezydesktopeffectkcm.ui index 22177f6..d01f119 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.ui +++ b/kwin/src/kcm/breezydesktopeffectkcm.ui @@ -226,13 +226,44 @@ + + + Anti-aliasing quality: + + + + + + + + None + + + + + Medium + + + + + High + + + + + Very High + + + + + Display Horizontal Offset: - + 2 @@ -251,14 +282,14 @@ - + Display Vertical Offset: - + 2 diff --git a/kwin/src/qml/main.qml b/kwin/src/qml/main.qml index 2794a05..d7f740d 100644 --- a/kwin/src/qml/main.qml +++ b/kwin/src/qml/main.qml @@ -104,8 +104,10 @@ Item { View3D { anchors.fill: parent environment: SceneEnvironment { - antialiasingMode: SceneEnvironment.SSAA - antialiasingQuality: SceneEnvironment.VeryHigh + antialiasingMode: root.effect.antialiasingQuality === 0 ? SceneEnvironment.NoAA : SceneEnvironment.SSAA + antialiasingQuality: root.effect.antialiasingQuality === 0 ? SceneEnvironment.Medium : ( + root.effect.antialiasingQuality === 1 ? SceneEnvironment.Medium : ( + root.effect.antialiasingQuality === 2 ? SceneEnvironment.High : SceneEnvironment.VeryHigh)) } PerspectiveCamera { From c2d36383b582d71b538ac01f6450f5a47ea1ee41 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:42:25 -0700 Subject: [PATCH 35/41] Add "remove virtual displays on disable" option --- kwin/src/breezydesktopconfig.kcfg | 5 +++++ kwin/src/breezydesktopeffect.cpp | 14 +++++++++++--- kwin/src/breezydesktopeffect.h | 4 ++++ kwin/src/kcm/breezydesktopeffectkcm.cpp | 7 +++++++ kwin/src/kcm/breezydesktopeffectkcm.ui | 18 ++++++++++++++++-- 5 files changed, 43 insertions(+), 5 deletions(-) diff --git a/kwin/src/breezydesktopconfig.kcfg b/kwin/src/breezydesktopconfig.kcfg index 7ba2b62..2a8ec8b 100644 --- a/kwin/src/breezydesktopconfig.kcfg +++ b/kwin/src/breezydesktopconfig.kcfg @@ -57,5 +57,10 @@ 0=None, 1=Medium, 2=High, 3=Very High + + true + + Whether to remove any virtual displays when the effect is disabled + diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index e1f83e8..d20a1d2 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -204,11 +204,13 @@ void BreezyDesktopEffect::reconfigure(ReconfigureFlags) qreal vert = BreezyDesktopConfig::displayVerticalOffset() / 100.0f; int wrap = BreezyDesktopConfig::displayWrappingScheme(); int aaQuality = BreezyDesktopConfig::antialiasingQuality(); + bool removeVD = BreezyDesktopConfig::removeVirtualDisplaysOnDisable(); bool changed = false; if (!qFuzzyCompare(m_displayHorizontalOffset, horiz)) { m_displayHorizontalOffset = horiz; changed = true; } if (!qFuzzyCompare(m_displayVerticalOffset, vert)) { m_displayVerticalOffset = vert; changed = true; } if (m_displayWrappingScheme != wrap) { m_displayWrappingScheme = wrap; Q_EMIT displayWrappingSchemeChanged(); } if (m_antialiasingQuality != aaQuality) { m_antialiasingQuality = aaQuality; Q_EMIT antialiasingQualityChanged(); } + if (m_removeVirtualDisplaysOnDisable != removeVD) { m_removeVirtualDisplaysOnDisable = removeVD; Q_EMIT removeVirtualDisplaysOnDisableChanged(); } if (changed) Q_EMIT displayOffsetChanged(); } @@ -260,10 +262,12 @@ void BreezyDesktopEffect::deactivate() m_cursorUpdateTimer->stop(); showCursor(); - for (auto output : m_virtualOutputs) { - KWin::kwinApp()->outputBackend()->removeVirtualOutput(output); + if (m_removeVirtualDisplaysOnDisable) { + for (auto output : m_virtualOutputs) { + KWin::kwinApp()->outputBackend()->removeVirtualOutput(output); + } + m_virtualOutputs.clear(); } - m_virtualOutputs.clear(); setRunning(false); } @@ -414,6 +418,10 @@ int BreezyDesktopEffect::antialiasingQuality() const { return m_antialiasingQuality; } +bool BreezyDesktopEffect::removeVirtualDisplaysOnDisable() const { + return m_removeVirtualDisplaysOnDisable; +} + bool BreezyDesktopEffect::checkParityByte(const char* data) { const uint8_t parityByte = static_cast(data[DataView::IMU_PARITY_BYTE[DataView::OFFSET_INDEX]]); uint8_t parity = 0; diff --git a/kwin/src/breezydesktopeffect.h b/kwin/src/breezydesktopeffect.h index e0ad83b..f49dba8 100644 --- a/kwin/src/breezydesktopeffect.h +++ b/kwin/src/breezydesktopeffect.h @@ -36,6 +36,7 @@ namespace KWin Q_PROPERTY(bool sbsEnabled READ sbsEnabled NOTIFY devicePropertiesChanged) Q_PROPERTY(bool customBannerEnabled READ customBannerEnabled NOTIFY devicePropertiesChanged) Q_PROPERTY(int antialiasingQuality READ antialiasingQuality NOTIFY antialiasingQualityChanged) + Q_PROPERTY(bool removeVirtualDisplaysOnDisable READ removeVirtualDisplaysOnDisable NOTIFY removeVirtualDisplaysOnDisableChanged) public: @@ -73,6 +74,7 @@ namespace KWin bool sbsEnabled() const; bool customBannerEnabled() const; int antialiasingQuality() const; + bool removeVirtualDisplaysOnDisable() const; void showCursor(); void hideCursor(); @@ -101,6 +103,7 @@ namespace KWin void cursorPosChanged(); void devicePropertiesChanged(); void antialiasingQualityChanged(); + void removeVirtualDisplaysOnDisableChanged(); protected: QVariantMap initialProperties(Output *screen) override; @@ -138,6 +141,7 @@ namespace KWin qreal m_displayVerticalOffset = 0.0; int m_displayWrappingScheme = 0; // 0=auto,1=horizontal,2=vertical,3=flat int m_antialiasingQuality = 3; // 0=None, 1=Medium, 2=High, 3=VeryHigh + bool m_removeVirtualDisplaysOnDisable = true; QList m_virtualOutputs; }; diff --git a/kwin/src/kcm/breezydesktopeffectkcm.cpp b/kwin/src/kcm/breezydesktopeffectkcm.cpp index 1818e2a..5861500 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.cpp +++ b/kwin/src/kcm/breezydesktopeffectkcm.cpp @@ -60,6 +60,10 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu row->setVisible(true); row->setEnabled(true); } + if (auto chk = widget()->findChild(QStringLiteral("kcfg_RemoveVirtualDisplaysOnDisable"))) { + chk->setVisible(true); + chk->setEnabled(true); + } } m_statePollTimer.setInterval(2000); @@ -93,6 +97,7 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu addShortcutAction(actionCollection, BreezyShortcuts::TOGGLE_ZOOM_ON_FOCUS); ui.shortcutsEditor->addCollection(actionCollection); connect(ui.shortcutsEditor, &KShortcutsEditor::keyChange, this, &BreezyDesktopEffectConfig::markAsChanged); + connect(ui.kcfg_ZoomOnFocusEnabled, &QCheckBox::toggled, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_FocusedDisplayDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_AllDisplaysDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_DisplaySpacing, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); @@ -100,6 +105,7 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu connect(ui.kcfg_DisplayVerticalOffset, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_DisplayWrappingScheme, qOverload(&QComboBox::currentIndexChanged), this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_AntialiasingQuality, qOverload(&QComboBox::currentIndexChanged), this, &BreezyDesktopEffectConfig::save); + connect(ui.kcfg_RemoveVirtualDisplaysOnDisable, &QCheckBox::toggled, this, &BreezyDesktopEffectConfig::save); if (auto label = widget()->findChild("labelAppNameVersion")) { label->setText(QStringLiteral("Breezy Desktop - v%1").arg(QLatin1String(BREEZY_DESKTOP_VERSION_STR))); @@ -219,6 +225,7 @@ void BreezyDesktopEffectConfig::updateUiFromConfig() ui.kcfg_DisplayVerticalOffset->setValue(BreezyDesktopConfig::self()->displayVerticalOffset()); ui.kcfg_DisplayWrappingScheme->setCurrentIndex(BreezyDesktopConfig::self()->displayWrappingScheme()); ui.kcfg_AntialiasingQuality->setCurrentIndex(BreezyDesktopConfig::self()->antialiasingQuality()); + ui.kcfg_RemoveVirtualDisplaysOnDisable->setChecked(BreezyDesktopConfig::self()->removeVirtualDisplaysOnDisable()); ui.kcfg_ZoomOnFocusEnabled->setChecked(BreezyDesktopConfig::self()->zoomOnFocusEnabled()); ui.kcfg_FocusedDisplayDistance->setEnabled(ui.kcfg_ZoomOnFocusEnabled->isChecked()); } diff --git a/kwin/src/kcm/breezydesktopeffectkcm.ui b/kwin/src/kcm/breezydesktopeffectkcm.ui index d01f119..061b322 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.ui +++ b/kwin/src/kcm/breezydesktopeffectkcm.ui @@ -308,6 +308,20 @@ + + + + false + + + false + + + Remove virtual displays on disable + + true + + @@ -321,10 +335,10 @@ - true + true - true + true From f7daccb2f83c2e21518a51eddfff5f938003335d Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Mon, 8 Sep 2025 15:18:12 -0700 Subject: [PATCH 36/41] Add "effect enabled" checkbox --- kwin/src/kcm/breezydesktopeffectkcm.cpp | 37 ++++++++++++--------- kwin/src/kcm/breezydesktopeffectkcm.h | 3 +- kwin/src/kcm/breezydesktopeffectkcm.ui | 44 +++++++++++++++---------- 3 files changed, 49 insertions(+), 35 deletions(-) diff --git a/kwin/src/kcm/breezydesktopeffectkcm.cpp b/kwin/src/kcm/breezydesktopeffectkcm.cpp index 5861500..038779d 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.cpp +++ b/kwin/src/kcm/breezydesktopeffectkcm.cpp @@ -97,6 +97,7 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu addShortcutAction(actionCollection, BreezyShortcuts::TOGGLE_ZOOM_ON_FOCUS); ui.shortcutsEditor->addCollection(actionCollection); connect(ui.shortcutsEditor, &KShortcutsEditor::keyChange, this, &BreezyDesktopEffectConfig::markAsChanged); + connect(ui.kcfg_EffectEnabled, &QCheckBox::toggled, this, &BreezyDesktopEffectConfig::updateDriverEnabled); connect(ui.kcfg_ZoomOnFocusEnabled, &QCheckBox::toggled, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_FocusedDisplayDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_AllDisplaysDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); @@ -239,22 +240,17 @@ void BreezyDesktopEffectConfig::updateUnmanagedState() { } -void BreezyDesktopEffectConfig::enableDriver() +void BreezyDesktopEffectConfig::updateDriverEnabled() { - qCCritical(KWIN_XR) << "\t\t\tBreezy config - enableDriver"; QJsonObject obj; - obj.insert(QStringLiteral("disabled"), false); - obj.insert(QStringLiteral("output_mode"), QStringLiteral("external_only")); - obj.insert(QStringLiteral("external_mode"), QStringLiteral("breezy_desktop")); - XRDriverIPC::instance().writeConfig(obj); -} - -void BreezyDesktopEffectConfig::disableDriver() -{ - qCCritical(KWIN_XR) << "\t\t\tBreezy config - disableDriver"; - QJsonObject obj; - obj.insert(QStringLiteral("disabled"), true); - obj.insert(QStringLiteral("external_mode"), QStringLiteral("none")); + if (ui.kcfg_EffectEnabled->isChecked()) { + obj.insert(QStringLiteral("disabled"), false); + obj.insert(QStringLiteral("output_mode"), QStringLiteral("external_only")); + obj.insert(QStringLiteral("external_mode"), QStringLiteral("breezy_desktop")); + } else { + obj.insert(QStringLiteral("disabled"), true); + obj.insert(QStringLiteral("external_mode"), QStringLiteral("none")); + } XRDriverIPC::instance().writeConfig(obj); } @@ -262,9 +258,9 @@ void BreezyDesktopEffectConfig::pollDriverState() { auto &bridge = XRDriverIPC::instance(); auto stateJsonOpt = bridge.retrieveDriverState(); - if (!stateJsonOpt) return; + auto configJsonOpt = bridge.retrieveConfig(); + if (!stateJsonOpt || !configJsonOpt) return; auto stateJson = stateJsonOpt.value(); - m_connectedDeviceBrand = stateJson.value(QStringLiteral("connected_device_brand")).toString(); m_connectedDeviceModel = stateJson.value(QStringLiteral("connected_device_model")).toString(); @@ -276,6 +272,15 @@ void BreezyDesktopEffectConfig::pollDriverState() QStringLiteral("No device connected")); } + auto configJson = configJsonOpt.value(); + bool driverDisabled = configJson.value(QStringLiteral("disabled")).toBool(); + QString driverOutputMode = configJson.value(QStringLiteral("output_mode")).toString(); + QJsonArray driverExternalMode = configJson.value(QStringLiteral("external_mode")).toArray(); + bool effectEnabled = !driverDisabled && + driverOutputMode == QStringLiteral("external_only") && + driverExternalMode.contains(QJsonValue(QStringLiteral("breezy_desktop"))); + if (ui.kcfg_EffectEnabled->isChecked() != effectEnabled) ui.kcfg_EffectEnabled->setChecked(effectEnabled); + refreshLicenseUi(stateJson); } diff --git a/kwin/src/kcm/breezydesktopeffectkcm.h b/kwin/src/kcm/breezydesktopeffectkcm.h index 3ffc66a..c42eb51 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.h +++ b/kwin/src/kcm/breezydesktopeffectkcm.h @@ -25,8 +25,7 @@ public Q_SLOTS: void defaults() override; private: - void enableDriver(); - void disableDriver(); + void updateDriverEnabled(); void updateUiFromConfig(); void updateUiFromDefaultConfig(); void updateConfigFromUi(); diff --git a/kwin/src/kcm/breezydesktopeffectkcm.ui b/kwin/src/kcm/breezydesktopeffectkcm.ui index 061b322..1b57da3 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.ui +++ b/kwin/src/kcm/breezydesktopeffectkcm.ui @@ -52,7 +52,17 @@ &General - + + + + XR Effect enabled + + + true + + + + Zoom on Focus @@ -61,15 +71,15 @@ false - - - - - Focused Display Distance: - - - - + + + + + Focused Display Distance: + + + + 2 @@ -88,14 +98,14 @@ - + All Displays Distance: - + 2 @@ -114,14 +124,14 @@ - + Display Spacing: - + Qt::Horizontal @@ -131,7 +141,7 @@ - + Add Virtual Display: @@ -144,7 +154,7 @@ - + false @@ -177,7 +187,7 @@ - + From f2b0913a4efdb0415d9fcc25b5d5cd69c818f30f Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Mon, 8 Sep 2025 21:39:08 -0700 Subject: [PATCH 37/41] Fix supported display check for Plasma Wayland on Steam Deck, add logging of display names --- kwin/src/qml/main.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kwin/src/qml/main.qml b/kwin/src/qml/main.qml index d7f740d..d99b850 100644 --- a/kwin/src/qml/main.qml +++ b/kwin/src/qml/main.qml @@ -87,7 +87,7 @@ Item { return displays.monitorsToPlacements(fovDetails, adjustedGeometries, effect.displaySpacing); } - property bool targetScreenSupported: supportedModels.some(model => root.targetScreen.model.endsWith(model)) + property bool targetScreenSupported: supportedModels.some(model => root.targetScreen.model.includes(model)) property bool imuResetState: effect.imuResetState property bool isEnabled: effect.isEnabled @@ -132,6 +132,7 @@ Item { } function checkLoadedComponent() { + console.log(`Breezy - checking screen ${targetScreen.model}: ${targetScreenSupported} ${isEnabled} ${imuResetState}`); const show3DView = targetScreenSupported && isEnabled && !imuResetState; viewLoader.sourceComponent = show3DView ? view3DComponent : desktopViewComponent; } From 4e4281cf6f6f002b8e75710bd14505b2de81468b Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Tue, 9 Sep 2025 10:24:59 -0700 Subject: [PATCH 38/41] Remove explicit enableDriver call when the plugin is initialized --- kwin/src/breezydesktopeffect.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index d20a1d2..278f15e 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -148,8 +148,6 @@ BreezyDesktopEffect::BreezyDesktopEffect() m_cursorUpdateTimer->setInterval(16); // ~60Hz m_cursorUpdateTimer->start(); - enableDriver(); - // Register DBus object under KWin's session bus name auto *adaptor = new BreezyDesktopDBusAdaptor(this); const bool dbusOk = QDBusConnection::sessionBus().registerObject( @@ -275,11 +273,11 @@ void BreezyDesktopEffect::deactivate() void BreezyDesktopEffect::enableDriver() { qCCritical(KWIN_XR) << "\t\t\tBreezy - enableDriver"; - QJsonObject obj; - obj.insert(QStringLiteral("disabled"), false); - obj.insert(QStringLiteral("output_mode"), QStringLiteral("external_only")); - obj.insert(QStringLiteral("external_mode"), QStringLiteral("breezy_desktop")); - XRDriverIPC::instance().writeConfig(obj); + QJsonObject obj; + obj.insert(QStringLiteral("disabled"), false); + obj.insert(QStringLiteral("output_mode"), QStringLiteral("external_only")); + obj.insert(QStringLiteral("external_mode"), QStringLiteral("breezy_desktop")); + XRDriverIPC::instance().writeConfig(obj); } void BreezyDesktopEffect::disableDriver() From 6be66ed8f95d6ab7b0de73e8e5fd948d91142744 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Tue, 9 Sep 2025 21:40:19 -0700 Subject: [PATCH 39/41] Attempt to unload the KWin plugin at the start of uninstall to prevent crashes --- kwin/bin/breezy_kwin_uninstall | 12 ++++++++++++ kwin/src/breezydesktopeffect.cpp | 10 +++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/kwin/bin/breezy_kwin_uninstall b/kwin/bin/breezy_kwin_uninstall index 82f9c49..8f620b2 100755 --- a/kwin/bin/breezy_kwin_uninstall +++ b/kwin/bin/breezy_kwin_uninstall @@ -8,6 +8,18 @@ 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 command -v qdbus >/dev/null 2>&1; then + QDBUS_CMD=$(command -v qdbus) +elif command -v qdbus6 >/dev/null 2>&1; then + QDBUS_CMD=$(command -v qdbus6) +fi + +if [ -n "$QDBUS_CMD" ]; then + if $QDBUS_CMD org.kde.KWin /Effects org.kde.kwin.Effects.unloadEffect breezy_desktop; then + [ "$for_install" -eq 0 ] && echo "Unloaded breezy_desktop effect" + fi +fi + USER_HOME=$(realpath ~) if [ "$for_install" -eq 0 ]; then UA_EVENT_NAME="breezy_kwin_uninstall" diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index 278f15e..0807139 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -273,11 +273,11 @@ void BreezyDesktopEffect::deactivate() void BreezyDesktopEffect::enableDriver() { qCCritical(KWIN_XR) << "\t\t\tBreezy - enableDriver"; - QJsonObject obj; - obj.insert(QStringLiteral("disabled"), false); - obj.insert(QStringLiteral("output_mode"), QStringLiteral("external_only")); - obj.insert(QStringLiteral("external_mode"), QStringLiteral("breezy_desktop")); - XRDriverIPC::instance().writeConfig(obj); + QJsonObject obj; + obj.insert(QStringLiteral("disabled"), false); + obj.insert(QStringLiteral("output_mode"), QStringLiteral("external_only")); + obj.insert(QStringLiteral("external_mode"), QStringLiteral("breezy_desktop")); + XRDriverIPC::instance().writeConfig(obj); } void BreezyDesktopEffect::disableDriver() From 7b70aac358de35241325b3cacc549d1206e623b2 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Wed, 10 Sep 2025 12:19:14 -0700 Subject: [PATCH 40/41] Pull in latest driver IPC integration --- ui/modules/PyXRLinuxDriverIPC | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/modules/PyXRLinuxDriverIPC b/ui/modules/PyXRLinuxDriverIPC index cfe4c91..7fecfc6 160000 --- a/ui/modules/PyXRLinuxDriverIPC +++ b/ui/modules/PyXRLinuxDriverIPC @@ -1 +1 @@ -Subproject commit cfe4c918e2d23b56bf77de62d0d469cc4e5a7c2c +Subproject commit 7fecfc604b553b9155837c58aaae423ba00afc63 From a54782c4e30452a18f2bcfdff4da4c9301e51158 Mon Sep 17 00:00:00 2001 From: wheaney <42350981+wheaney@users.noreply.github.com> Date: Wed, 10 Sep 2025 13:28:59 -0700 Subject: [PATCH 41/41] Fix driver IPC usage --- kwin/src/breezydesktopeffect.cpp | 25 ++++++++----- kwin/src/kcm/breezydesktopeffectkcm.cpp | 49 ++++++++++++++++--------- kwin/src/kcm/breezydesktopeffectkcm.h | 1 + kwin/src/xrdriveripc/xrdriveripc.cpp | 2 +- modules/XRLinuxDriver | 2 +- 5 files changed, 50 insertions(+), 29 deletions(-) diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index 0807139..e27830d 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -273,20 +273,27 @@ void BreezyDesktopEffect::deactivate() void BreezyDesktopEffect::enableDriver() { qCCritical(KWIN_XR) << "\t\t\tBreezy - enableDriver"; - QJsonObject obj; - obj.insert(QStringLiteral("disabled"), false); - obj.insert(QStringLiteral("output_mode"), QStringLiteral("external_only")); - obj.insert(QStringLiteral("external_mode"), QStringLiteral("breezy_desktop")); - XRDriverIPC::instance().writeConfig(obj); + QJsonObject newConfig = QJsonObject(); + auto configJsonOpt = XRDriverIPC::instance().retrieveConfig(); + if (configJsonOpt) { + newConfig = configJsonOpt.value(); + } + newConfig.insert(QStringLiteral("disabled"), false); + newConfig.insert(QStringLiteral("output_mode"), QStringLiteral("external_only")); + newConfig.insert(QStringLiteral("external_mode"), QStringLiteral("breezy_desktop")); + XRDriverIPC::instance().writeConfig(newConfig); } void BreezyDesktopEffect::disableDriver() { qCCritical(KWIN_XR) << "\t\t\tBreezy - disableDriver"; - QJsonObject obj; - obj.insert(QStringLiteral("disabled"), true); - obj.insert(QStringLiteral("external_mode"), QStringLiteral("none")); - XRDriverIPC::instance().writeConfig(obj); + QJsonObject newConfig = QJsonObject(); + auto configJsonOpt = XRDriverIPC::instance().retrieveConfig(); + if (configJsonOpt) { + newConfig = configJsonOpt.value(); + } + newConfig.insert(QStringLiteral("external_mode"), QStringLiteral("none")); + XRDriverIPC::instance().writeConfig(newConfig); } void BreezyDesktopEffect::addVirtualDisplay(QSize size) diff --git a/kwin/src/kcm/breezydesktopeffectkcm.cpp b/kwin/src/kcm/breezydesktopeffectkcm.cpp index 038779d..a3b62e4 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.cpp +++ b/kwin/src/kcm/breezydesktopeffectkcm.cpp @@ -242,24 +242,43 @@ void BreezyDesktopEffectConfig::updateUnmanagedState() void BreezyDesktopEffectConfig::updateDriverEnabled() { - QJsonObject obj; - if (ui.kcfg_EffectEnabled->isChecked()) { - obj.insert(QStringLiteral("disabled"), false); - obj.insert(QStringLiteral("output_mode"), QStringLiteral("external_only")); - obj.insert(QStringLiteral("external_mode"), QStringLiteral("breezy_desktop")); - } else { - obj.insert(QStringLiteral("disabled"), true); - obj.insert(QStringLiteral("external_mode"), QStringLiteral("none")); + if (driverEnabled() == ui.kcfg_EffectEnabled->isChecked()) { + return; } - XRDriverIPC::instance().writeConfig(obj); + + QJsonObject newConfig = QJsonObject(); + auto configJsonOpt = XRDriverIPC::instance().retrieveConfig(); + if (configJsonOpt) { + newConfig = configJsonOpt.value(); + } + if (ui.kcfg_EffectEnabled->isChecked()) { + newConfig.insert(QStringLiteral("disabled"), false); + newConfig.insert(QStringLiteral("output_mode"), QStringLiteral("external_only")); + newConfig.insert(QStringLiteral("external_mode"), QStringLiteral("breezy_desktop")); + } else { + newConfig.insert(QStringLiteral("external_mode"), QStringLiteral("none")); + } + XRDriverIPC::instance().writeConfig(newConfig); +} + +bool BreezyDesktopEffectConfig::driverEnabled() +{ + auto configJsonOpt = XRDriverIPC::instance().retrieveConfig(); + if (!configJsonOpt) return false; + auto configJson = configJsonOpt.value(); + bool driverDisabled = configJson.value(QStringLiteral("disabled")).toBool(); + QString driverOutputMode = configJson.value(QStringLiteral("output_mode")).toString(); + QJsonArray driverExternalMode = configJson.value(QStringLiteral("external_mode")).toArray(); + return !driverDisabled && + driverOutputMode == QStringLiteral("external_only") && + driverExternalMode.contains(QJsonValue(QStringLiteral("breezy_desktop"))); } void BreezyDesktopEffectConfig::pollDriverState() { auto &bridge = XRDriverIPC::instance(); auto stateJsonOpt = bridge.retrieveDriverState(); - auto configJsonOpt = bridge.retrieveConfig(); - if (!stateJsonOpt || !configJsonOpt) return; + if (!stateJsonOpt) return; auto stateJson = stateJsonOpt.value(); m_connectedDeviceBrand = stateJson.value(QStringLiteral("connected_device_brand")).toString(); m_connectedDeviceModel = stateJson.value(QStringLiteral("connected_device_model")).toString(); @@ -272,13 +291,7 @@ void BreezyDesktopEffectConfig::pollDriverState() QStringLiteral("No device connected")); } - auto configJson = configJsonOpt.value(); - bool driverDisabled = configJson.value(QStringLiteral("disabled")).toBool(); - QString driverOutputMode = configJson.value(QStringLiteral("output_mode")).toString(); - QJsonArray driverExternalMode = configJson.value(QStringLiteral("external_mode")).toArray(); - bool effectEnabled = !driverDisabled && - driverOutputMode == QStringLiteral("external_only") && - driverExternalMode.contains(QJsonValue(QStringLiteral("breezy_desktop"))); + bool effectEnabled = driverEnabled(); if (ui.kcfg_EffectEnabled->isChecked() != effectEnabled) ui.kcfg_EffectEnabled->setChecked(effectEnabled); refreshLicenseUi(stateJson); diff --git a/kwin/src/kcm/breezydesktopeffectkcm.h b/kwin/src/kcm/breezydesktopeffectkcm.h index c42eb51..1a54f4b 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.h +++ b/kwin/src/kcm/breezydesktopeffectkcm.h @@ -30,6 +30,7 @@ private: void updateUiFromDefaultConfig(); void updateConfigFromUi(); void updateUnmanagedState(); + bool driverEnabled(); void pollDriverState(); void refreshLicenseUi(const QJsonObject &rootObj); void showStatus(QLabel *label, bool success, const QString &message); diff --git a/kwin/src/xrdriveripc/xrdriveripc.cpp b/kwin/src/xrdriveripc/xrdriveripc.cpp index 168043d..4508f3d 100644 --- a/kwin/src/xrdriveripc/xrdriveripc.cpp +++ b/kwin/src/xrdriveripc/xrdriveripc.cpp @@ -68,7 +68,7 @@ QByteArray XRDriverIPC::invokePython(const QString &method, } std::optional XRDriverIPC::retrieveConfig() { - QByteArray out = invokePython(QStringLiteral("retrieve_config"), {}, QStringLiteral("1")); + QByteArray out = invokePython(QStringLiteral("retrieve_config"), {}, QStringLiteral("0")); if (out.isEmpty()) return std::nullopt; QJsonParseError err; auto doc = QJsonDocument::fromJson(out, &err); if (err.error != QJsonParseError::NoError || !doc.isObject()) return std::nullopt; diff --git a/modules/XRLinuxDriver b/modules/XRLinuxDriver index 69d3603..09c6627 160000 --- a/modules/XRLinuxDriver +++ b/modules/XRLinuxDriver @@ -1 +1 @@ -Subproject commit 69d3603ef9c931a0f66777279501d4c6dfe20f09 +Subproject commit 09c6627d2a60985a4f8dde86be70d0e797d62f45