Remove kwin directory from gnome backport

This commit is contained in:
wheaney 2025-09-19 10:39:23 -07:00
parent 2f2549fa7e
commit 719a65aad3
41 changed files with 1 additions and 3948 deletions

6
.gitignore vendored
View File

@ -6,8 +6,4 @@ out/
*.po~
gnome-44-max/
gnome-45/
kwin/src/xrdriveripc/xrdriveripc.py
kwin/VERSION
kwin/build-test/
kwin/src/qml/calibrating.png
kwin/src/qml/custom_banner.png
kwin/

View File

@ -1,55 +0,0 @@
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)

View File

@ -1,74 +0,0 @@
#!/usr/bin/env bash
for_install=0
if [[ -n "$1" ]] && [[ "$1" == "--for-install" ]]; then
for_install=1
fi
# we don't want the uninstall script to be able to cause a failure if being triggered by the setup script
[ "$for_install" -eq 0 ] && set -e
if 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"
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"
BREEZY_LIBRARY_DIR="$LIB_HOME/breezy_kwin"
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 [[ -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"
fi
# this script is self-deleting, leave this as the last command
rm -f $XDG_BIN_HOME/breezy_kwin_uninstall

View File

@ -1,32 +0,0 @@
#!/usr/bin/env bash
# exit when any command fails
set -e
check_command() {
if ! command -v "$1" &>/dev/null; then
echo "Please install \"$1\" and make sure it's available in your \$PATH, then rerun the setup."
exit 1
fi
}
check_command "cmake"
check_command "make"
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 > /dev/null
cmake ..
make
cpack -G TGZ
popd > /dev/null
mkdir -p out
cp $BUILD_PATH/breezy_desktop.tar.gz out/breezyKWinPlugin-$ARCH.tar.gz

View File

@ -1,104 +0,0 @@
#!/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:\$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" <<EOF
# Added by Breezy Desktop installer: QT plugin path setup
$QT_PLUGIN_EXPORT
export QT_DEBUG_PLUGINS=1
EOF
fi
PLASMA_ENV_SCRIPT="$HOME/.config/plasma-workspace/env/breezy_desktop.sh"
if [[ ! -f "$PLASMA_ENV_SCRIPT" ]]; then
echo "Adding QT_PLUGIN_PATH to $PLASMA_ENV_SCRIPT"
mkdir -p "$(dirname "$PLASMA_ENV_SCRIPT")"
cat >> "$PLASMA_ENV_SCRIPT" <<EOF
# Added by Breezy Desktop installer: QT plugin path setup
$QT_PLUGIN_EXPORT
export QT_DEBUG_PLUGINS=1
EOF
fi
# set up the XR driver using the local binary
echo "Installing xrDriver (requires sudo)"
echo "BEGIN - xr_driver_setup"
if [ -z "$1" ]
then
sudo bin/xr_driver_setup $(pwd)/xrDriver.tar.gz
else
sudo bin/xr_driver_setup -v $1 $(pwd)/xrDriver.tar.gz
fi
echo "END - xr_driver_setup"
printf "\n\033[1;33m!!! IMPORTANT !!!\033[0m You must log out and back in, then enable Breezy Desktop from the Desktop Effects in System Settings\n\n"

View File

@ -1,15 +0,0 @@
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "/usr" CACHE PATH "Force path to set CMAKE_INSTALL_PREFIX" FORCE)
endif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose Release or Debug" FORCE)
endif()
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -DQT_NO_DEBUG_OUTPUT")
set(QT_MIN_VERSION "6.4.0")
set(QT_MAJOR_VERSION 6)
set(KF_MIN_VERSION 6)

View File

@ -1,24 +0,0 @@
if(${KF_MIN_VERSION} EQUAL 6)
set(KWIN_EFFECT_INCLUDE_FILE "/usr/include/kwin/effect/effect.h")
else ()
set(KWIN_EFFECT_INCLUDE_FILE "/usr/include/kwineffects.h")
endif ()
execute_process(
COMMAND sh -c "grep '#define KWIN_EFFECT_API_VERSION_MINOR' ${KWIN_EFFECT_INCLUDE_FILE} | awk '{print \$NF}'"
OUTPUT_VARIABLE KWIN_EFFECT_API_VERSION_MINOR OUTPUT_STRIP_TRAILING_WHITESPACE
)
message(STATUS "Found KWinEffect API Version: ${KWIN_EFFECT_API_VERSION_MINOR}")
#below is a very useful way of finding variables and contains:
#get_cmake_property(_variableNames VARIABLES)
#list (SORT _variableNames)
#foreach (_variableName ${_variableNames})
# string(TOLOWER "${_variableName}" KEY)
# string(TOLOWER "${${_variableName}}" VALUE)
# string(FIND "${KEY}" "kwin" INDEX1)
# string(FIND "${VALUE}" "kwin" INDEX2)
# if (${INDEX1} GREATER -1 OR ${INDEX2} GREATER -1)
# message(STATUS "VARIABLE ${_variableName}=${${_variableName}}")
# endif ()
#endforeach()

View File

@ -1,18 +0,0 @@
# these are cache variables, so they could be overwritten with -D,
set(CPACK_PACKAGE_NAME ${CMAKE_PROJECT_NAME} CACHE STRING ${CMAKE_PROJECT_NAME})
set(CPACK_PACKAGING_INSTALL_PREFIX "/usr")
set(CPACK_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME}")
set(CPACK_PACKAGE_VERSION "${CMAKE_PROJECT_VERSION}")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Breezy Desktop - KWin Plugin")
set(CPACK_PACKAGE_CONTACT "wayne@xronlinux.com")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Wayne Heaney")
set(CPACK_DEBIAN_PACKAGE_SECTION "kde")
# autogenerate dependency information
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
set(CPACK_DEBIAN_PACKAGE_GENERATE_SHLIBS ON)
set(CPACK_DEBIAN_PACKAGE_GENERATE_SHLIBS_POLICY "=")
include(CPack)
# To generate deb files, install 'dpkg-dev' package and then run 'cpack -G DEB'

View File

@ -1,7 +0,0 @@
find_package(KF6 QUIET COMPONENTS ConfigWidgets)
if(${KF6_FOUND} EQUAL 0)
set(QT_MIN_VERSION "5.15")
set(QT_MAJOR_VERSION 5)
set(KF_MIN_VERSION "5.78")
endif ()

View File

@ -1,2 +0,0 @@
add_test (NAME KWinEffectSupport COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/tools/isSupported.sh)
set_property (TEST KWinEffectSupport PROPERTY PASS_REGULAR_EXPRESSION "true")

View File

@ -1,36 +0,0 @@
# To run the build from the package root:
# docker buildx build --platform linux/amd64,linux/arm64 -f ./docker-build/Dockerfile -t "breezy-kwin" .
# docker run --rm -t -v ./:/source -v --platform linux/amd64 "breezy-kwin:amd64"
# docker run --rm -t -v ./:/source -v --platform linux/arm64 "breezy-kwin:arm64"
FROM --platform=$TARGETPLATFORM archlinux:base-20250817.0.405639@sha256:31f0749bdb81517dc8f379feac0a3860b097f1da1f53c8315c1bae0817d6c0a1
ARG TARGETPLATFORM
RUN echo "Target platform: $TARGETPLATFORM"
RUN pacman -Sy --noconfirm --needed \
ca-certificates \
base-devel \
cmake \
pkgconf \
git \
curl \
wget \
extra-cmake-modules \
qt6-base \
qt6-declarative \
qt6-tools \
kconfig \
kconfigwidgets \
kcoreaddons \
kglobalaccel \
ki18n \
kcmutils \
kxmlgui \
kwindowsystem \
kwin \
&& pacman -Scc --noconfirm
WORKDIR /source
CMD bin/package_kwin_plugin

View File

@ -1,37 +0,0 @@
# To run the build from the package root:
# docker buildx build --platform linux/amd64,linux/arm64 -f ./docker-build/Dockerfile.steamos -t "breezy-kwin-steamos" .
# docker run --rm -t -v ./:/source -v --platform linux/amd64 "breezy-kwin-steamos:amd64"
# docker run --rm -t -v ./:/source -v --platform linux/arm64 "breezy-kwin-steamos:arm64"
FROM --platform=$TARGETPLATFORM ghcr.io/steamdeckhomebrew/holo-base:3.7@sha256:8da120a3e89c750abd0090c0aab86d543a55d667c3002c8d64960f7fd82ccdd6
ARG TARGETPLATFORM
ENV STEAMOS=1
RUN echo "SteamOS build - target platform: $TARGETPLATFORM"
RUN pacman -Sy --noconfirm --needed \
ca-certificates \
base-devel \
cmake \
pkgconf \
git \
curl \
wget \
extra-cmake-modules \
qt6-base \
qt6-declarative \
qt6-tools \
kconfig \
kconfigwidgets \
kcoreaddons \
kglobalaccel \
ki18n \
kcmutils \
kxmlgui \
kwindowsystem \
kwin \
&& pacman -Scc --noconfirm
WORKDIR /source
CMD bin/package_kwin_plugin

View File

@ -1,23 +0,0 @@
#!/bin/bash
# might be needed on a fresh docker setup:
# install qemu and qemu-user-static packages
# sudo docker context rm default
# docker run --privileged --rm tonistiigi/binfmt --install all
# sudo docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
# ls -l /proc/sys/fs/binfmt_misc/ # should contain qemu-<arch> 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 .

View File

@ -1,28 +0,0 @@
#!/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/

View File

@ -1,70 +0,0 @@
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/")
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}
BREEZY_DESKTOP_VERSION_STR=\"${BREEZY_DESKTOP_VERSION}\"
)
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
Qt6::DBus
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)

View File

@ -1,66 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
<kcfgfile name="kwinrc"/>
<group name="Effect-breezy_desktop">
<entry name="FocusedDisplayDistance" type="Int">
<default>85</default>
<min>25</min>
<max>225</max>
<label>Focused Display Distance</label>
</entry>
<entry name="AllDisplaysDistance" type="Int">
<default>105</default>
<min>25</min>
<max>225</max>
<label>All Displays Distance</label>
</entry>
<entry name="ZoomOnFocusEnabled" type="Bool">
<default>false</default>
<label>Zoom on Focus Enabled</label>
<description>Enable zooming in on the focused display.</description>
</entry>
<entry name="DisplaySpacing" type="Int">
<default>0</default>
<min>0</min>
<max>100</max>
<label>Display Spacing</label>
<description>How far apart the displays are visually (not logically)</description>
</entry>
<entry name="DisplayHorizontalOffset" type="Int">
<default>0</default>
<min>-250</min>
<max>250</max>
<label>Display Horizontal Offset</label>
<description>Horizontal offset as a percent of the viewport width (-2.50 to 2.50)</description>
</entry>
<entry name="DisplayVerticalOffset" type="Int">
<default>0</default>
<min>-250</min>
<max>250</max>
<label>Display Vertical Offset</label>
<description>Vertical offset as a percent of the viewport height (-2.50 to 2.50)</description>
</entry>
<entry name="DisplayWrappingScheme" type="Int">
<default>0</default>
<min>0</min>
<max>3</max>
<label>Display Wrapping Scheme</label>
<description>How to arrange monitors: 0=Auto, 1=Horizontal, 2=Vertical, 3=Flat</description>
</entry>
<entry name="AntialiasingQuality" type="Int">
<default>3</default>
<min>0</min>
<max>3</max>
<label>Antialiasing Quality</label>
<description>0=None, 1=Medium, 2=High, 3=Very High</description>
</entry>
<entry name="RemoveVirtualDisplaysOnDisable" type="Bool">
<default>true</default>
<label>Remove virtual displays on disable</label>
<description>Whether to remove any virtual displays when the effect is disabled</description>
</entry>
</group>
</kcfg>

View File

@ -1,5 +0,0 @@
File=breezydesktopconfig.kcfg
ClassName=BreezyDesktopConfig
Singleton=true
Mutators=true
Notifiers=true

View File

@ -1,628 +0,0 @@
#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 <kwin/main.h>
#include <core/outputbackend.h>
#include <functional>
#include <QAction>
#include <QBuffer>
#include <QFile>
#include <QFileSystemWatcher>
#include <QLoggingCategory>
#include <QQuickItem>
#include <QTimer>
#include <QDBusConnection>
#include <KGlobalAccel>
#include <KLocalizedString>
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");
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()
{
qCCritical(KWIN_XR) << "\t\t\tBreezy - constructor";
qmlRegisterUncreatableType<BreezyDesktopEffect>("org.kde.kwin.effect.breezy_desktop", 1, 0, "BreezyDesktopEffect", QStringLiteral("BreezyDesktop cannot be created in QML"));
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();
// 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";
}
}
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<void()> 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());
qreal horiz = BreezyDesktopConfig::displayHorizontalOffset() / 100.0f;
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();
}
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 - disabling";
disableDriver();
} else {
qCCritical(KWIN_XR) << "\t\t\tBreezy - toggle - enabling";
enableDriver();
}
}
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();
}
void BreezyDesktopEffect::deactivate()
{
qCCritical(KWIN_XR) << "\t\t\tBreezy - deactivate";
disconnect(effects, &EffectsHandler::cursorShapeChanged, this, &BreezyDesktopEffect::updateCursorImage);
m_cursorUpdateTimer->stop();
showCursor();
if (m_removeVirtualDisplaysOnDisable) {
for (auto output : m_virtualOutputs) {
KWin::kwinApp()->outputBackend()->removeVirtualOutput(output);
}
m_virtualOutputs.clear();
}
setRunning(false);
}
void BreezyDesktopEffect::enableDriver()
{
qCCritical(KWIN_XR) << "\t\t\tBreezy - enableDriver";
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 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)
{
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;
if (m_zoomOnFocusEnabled && m_focusedDisplayDistance > m_allDisplaysDistance) {
setFocusedDisplayDistance(m_allDisplaysDistance);
BreezyDesktopConfig::setFocusedDisplayDistance(static_cast<int>(m_focusedDisplayDistance * 100.0f));
}
BreezyDesktopConfig::setZoomOnFocusEnabled(enabled);
BreezyDesktopConfig::self()->save();
Q_EMIT zoomOnFocusChanged();
}
}
bool BreezyDesktopEffect::imuResetState() const {
return m_imuResetState;
}
QList<QQuaternion> BreezyDesktopEffect::imuRotations() const {
return m_imuRotations;
}
quint32 BreezyDesktopEffect::imuTimeElapsedMs() const {
return m_imuTimeElapsedMs;
}
quint64 BreezyDesktopEffect::imuTimestamp() const {
return m_imuTimestamp;
}
QList<qreal> BreezyDesktopEffect::lookAheadConfig() const {
return m_lookAheadConfig;
}
QList<quint32> 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 focusedDisplayDistanceChanged();
}
}
qreal BreezyDesktopEffect::allDisplaysDistance() const {
return m_allDisplaysDistance;
}
void BreezyDesktopEffect::setAllDisplaysDistance(qreal distance) {
if (distance != m_allDisplaysDistance) {
qreal min = m_zoomOnFocusEnabled ? m_focusedDisplayDistance : 0.2;
m_allDisplaysDistance = std::clamp(distance, min, 2.5);
Q_EMIT allDisplaysDistanceChanged();
}
}
qreal BreezyDesktopEffect::displaySpacing() const {
return m_displaySpacing;
}
void BreezyDesktopEffect::setDisplaySpacing(qreal spacing) {
if (spacing != m_displaySpacing) {
m_displaySpacing = spacing;
Q_EMIT displaySpacingChanged();
}
}
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;
}
qreal BreezyDesktopEffect::lensDistanceRatio() const {
return m_lensDistanceRatio;
}
bool BreezyDesktopEffect::sbsEnabled() const {
return m_sbsEnabled;
}
bool BreezyDesktopEffect::customBannerEnabled() const {
return m_customBannerEnabled;
}
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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(data[DataView::VERSION[DataView::OFFSET_INDEX]]);
uint8_t enabledFlag = static_cast<uint8_t>(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));
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();
}
// 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<quint32>(imuData[imuDataOffset + 0] - imuData[imuDataOffset + 1]);
m_imuTimestamp = imuDateMs;
}
QString BreezyDesktopEffect::cursorImageSource() const
{
return m_cursorImageSource;
}
QSize BreezyDesktopEffect::cursorImageSize() const
{
return m_cursorImageSize;
}
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()));
m_cursorImageSize = cursor.image().size();
} else {
m_cursorImageSource = QString();
m_cursorImageSize = QSize();
}
Q_EMIT cursorImageSourceChanged();
}
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 "breezydesktopeffect.moc"

View File

@ -1,148 +0,0 @@
#pragma once
#include "kcm/shortcuts.h"
#include <effect/quickeffect.h>
#include <QAction>
#include <QFileSystemWatcher>
#include <QImage>
#include <QKeySequence>
#include <QQuaternion>
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 imuResetStateChanged)
Q_PROPERTY(QList<QQuaternion> 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<qreal> lookAheadConfig READ lookAheadConfig NOTIFY devicePropertiesChanged)
Q_PROPERTY(QList<quint32> displayResolution READ displayResolution NOTIFY devicePropertiesChanged)
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)
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:
BreezyDesktopEffect();
~BreezyDesktopEffect() override;
void reconfigure(ReconfigureFlags) override;
int requestedEffectChainPosition() const override;
QString cursorImageSource() const;
QSize cursorImageSize() const;
QPointF cursorPos() const;
bool isEnabled() const;
bool isZoomOnFocusEnabled() const;
void setZoomOnFocusEnabled(bool enabled);
QList<QQuaternion> imuRotations() const;
quint32 imuTimeElapsedMs() const;
quint64 imuTimestamp() const;
bool imuResetState() const;
QList<qreal> lookAheadConfig() const;
QList<quint32> displayResolution() const;
qreal focusedDisplayDistance() const;
void setFocusedDisplayDistance(qreal distance);
qreal allDisplaysDistance() const;
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;
bool customBannerEnabled() const;
int antialiasingQuality() const;
bool removeVirtualDisplaysOnDisable() const;
void showCursor();
void hideCursor();
public Q_SLOTS:
void activate();
void deactivate();
void enableDriver();
void disableDriver();
void toggle();
void addVirtualDisplay(QSize size);
void updateImuRotation();
void updateCursorImage();
void updateCursorPos();
Q_SIGNALS:
void focusedDisplayDistanceChanged();
void allDisplaysDistanceChanged();
void displaySpacingChanged();
void displayOffsetChanged();
void displayWrappingSchemeChanged();
void enabledStateChanged();
void zoomOnFocusChanged();
void imuResetStateChanged();
void cursorImageSourceChanged();
void cursorPosChanged();
void devicePropertiesChanged();
void antialiasingQualityChanged();
void removeVirtualDisplaysOnDisableChanged();
protected:
QVariantMap initialProperties(Output *screen) override;
private:
void teardown();
bool checkParityByte(const char* data);
void setupGlobalShortcut(const BreezyShortcuts::Shortcut &shortcut,
std::function<void()> triggeredFunc);
void recenter();
QString m_cursorImageSource;
QSize m_cursorImageSize;
bool m_enabled = false;
bool m_zoomOnFocusEnabled = false;
bool m_imuResetState;
QList<QQuaternion> m_imuRotations;
quint32 m_imuTimeElapsedMs;
quint64 m_imuTimestamp = 0;
QList<qreal> m_lookAheadConfig;
QList<quint32> 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;
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
bool m_removeVirtualDisplaysOnDisable = true;
QList<Output *> m_virtualOutputs;
};
} // namespace KWin

View File

@ -1,24 +0,0 @@
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
Qt6::DBus
KF6::ConfigCore
KF6::ConfigGui
KF6::ConfigWidgets
KF6::CoreAddons
KF6::GlobalAccel
KF6::I18n
KF6::KCMUtils
KF6::XmlGui
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()

View File

@ -1,457 +0,0 @@
#include "shortcuts.h"
#include "breezydesktopeffectkcm.h"
#include "breezydesktopconfig.h"
#include "labeledslider.h"
#include "xrdriveripc.h"
#include <kwineffects_interface.h>
#include <KActionCollection>
#include <KGlobalAccel>
#include <KLocalizedString>
#include <KConfigWatcher>
#include <KSharedConfig>
#include <KPluginFactory>
#include <QAction>
#include <QGuiApplication>
#include <QKeyEvent>
#include <QLineEdit>
#include <QLabel>
#include <QJsonValue>
#include <QJsonArray>
#include <QDesktopServices>
#include <QUrl>
#include <QProcess>
#include <QComboBox>
#include <QDBusInterface>
#include <QDBusConnection>
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());
// 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<QLabel*>(QStringLiteral("labelVirtualDisplays"))) {
lbl->setVisible(true);
lbl->setEnabled(true);
}
if (auto row = widget()->findChild<QWidget*>(QStringLiteral("widgetVirtualDisplayButtons"))) {
row->setVisible(true);
row->setEnabled(true);
}
if (auto chk = widget()->findChild<QWidget*>(QStringLiteral("kcfg_RemoveVirtualDisplaysOnDisable"))) {
chk->setVisible(true);
chk->setEnabled(true);
}
}
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_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);
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<int>(&QComboBox::currentIndexChanged), this, &BreezyDesktopEffectConfig::save);
connect(ui.kcfg_AntialiasingQuality, qOverload<int>(&QComboBox::currentIndexChanged), this, &BreezyDesktopEffectConfig::save);
connect(ui.kcfg_RemoveVirtualDisplaysOnDisable, &QCheckBox::toggled, this, &BreezyDesktopEffectConfig::save);
if (auto label = widget()->findChild<QLabel*>("labelAppNameVersion")) {
label->setText(QStringLiteral("Breezy Desktop - v%1").arg(QLatin1String(BREEZY_DESKTOP_VERSION_STR)));
}
if (auto btnEmail = widget()->findChild<QPushButton*>("buttonSubmitEmail")) {
connect(btnEmail, &QPushButton::clicked, this, [this]() {
auto edit = widget()->findChild<QLineEdit*>("lineEditLicenseEmail");
auto labelStatus = widget()->findChild<QLabel*>("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<QLineEdit*>("lineEditLicenseEmail")) {
emailEdit->installEventFilter(this);
}
}
if (auto btnToken = widget()->findChild<QPushButton*>("buttonSubmitToken")) {
connect(btnToken, &QPushButton::clicked, this, [this]() {
auto edit = widget()->findChild<QLineEdit*>("lineEditLicenseToken");
auto labelStatus = widget()->findChild<QLabel*>("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<QLineEdit*>("lineEditLicenseToken")) {
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<QPushButton*>("buttonAdd1080p")) {
connect(btn1080p, &QPushButton::clicked, this, [callAddVirtualDisplay]() {
callAddVirtualDisplay(1920, 1080);
});
}
if (auto btn1440p = widget()->findChild<QPushButton*>("buttonAdd1440p")) {
connect(btn1440p, &QPushButton::clicked, this, [callAddVirtualDisplay]() {
callAddVirtualDisplay(2560, 1440);
});
}
// General tab: Open KDE Displays Settings
if (auto btnDisplays = widget()->findChild<QPushButton*>(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()
{
}
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_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_RemoveVirtualDisplaysOnDisable->setChecked(BreezyDesktopConfig::self()->removeVirtualDisplaysOnDisable());
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::updateDriverEnabled()
{
if (driverEnabled() == ui.kcfg_EffectEnabled->isChecked()) {
return;
}
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();
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();
const bool wasDeviceConnected = m_deviceConnected;
m_deviceConnected = !m_connectedDeviceBrand.isEmpty() && !m_connectedDeviceModel.isEmpty();
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"));
}
bool effectEnabled = driverEnabled();
if (ui.kcfg_EffectEnabled->isChecked() != effectEnabled) ui.kcfg_EffectEnabled->setChecked(effectEnabled);
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<QObject*> widgets, bool inProgress) {
for (auto *obj : widgets) {
if (auto *w = qobject_cast<QWidget*>(obj)) {
w->setEnabled(!inProgress);
}
}
}
bool BreezyDesktopEffectConfig::eventFilter(QObject *watched, QEvent *event) {
if (event->type() == QEvent::KeyPress) {
auto *ke = static_cast<QKeyEvent*>(event);
if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) {
if (auto *edit = qobject_cast<QLineEdit*>(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<QPushButton*>(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<QWidget*>("tabLicenseDetails");
if (!tab) return;
auto labelSummary = tab->findChild<QLabel*>("labelLicenseSummary");
if (!labelSummary) return;
auto donate = tab->findChild<QLabel*>("labelDonateLink");
auto globalWarn = widget()->findChild<QLabel*>("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 = false;
bool expired = false;
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<qint64>(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()) {
if (isTrial.toBool()) {
status = tr("in trial");
auto secsVal = prodFeatureObj.value(QStringLiteral("funds_needed_in_seconds"));
if (secsVal.isDouble()) {
qint64 secs = static_cast<qint64>(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;
}
}
}
}
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"

View File

@ -1,49 +0,0 @@
#pragma once
#include <KCModule>
#include <KConfigWatcher>
#include <memory>
#include <QTimer>
#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 updateDriverEnabled();
void updateUiFromConfig();
void updateUiFromDefaultConfig();
void updateConfigFromUi();
void updateUnmanagedState();
bool driverEnabled();
void pollDriverState();
void refreshLicenseUi(const QJsonObject &rootObj);
void showStatus(QLabel *label, bool success, const QString &message);
void setRequestInProgress(std::initializer_list<QObject*> widgets, bool inProgress);
bool eventFilter(QObject *watched, QEvent *event) override;
::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
bool m_licenseLoading = false;
};

View File

@ -1,529 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>BreezyDesktopEffectConfig</class>
<widget class="QWidget" name="BreezyDesktopEffectConfig">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="labelDeviceConnectionStatus">
<property name="text">
<string></string>
</property>
<property name="alignment">
<set>Qt::AlignHCenter|Qt::AlignVCenter</set>
</property>
<property name="font">
<font>
<pointsize>14</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelGlobalLicenseWarning">
<property name="text">
<string/>
</property>
<property name="visible">
<bool>false</bool>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignHCenter|Qt::AlignVCenter</set>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(200,0,0); font-weight: bold;</string>
</property>
</widget>
</item>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="tabPosition">
<enum>QTabWidget::North</enum>
</property>
<property name="tabShape">
<enum>QTabWidget::Rounded</enum>
</property>
<widget class="QWidget" name="tabGeneral">
<attribute name="title">
<string>&amp;General</string>
</attribute>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="kcfg_EffectEnabled">
<property name="text">
<string>XR Effect enabled</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="kcfg_ZoomOnFocusEnabled">
<property name="text">
<string>Zoom on Focus</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelFocusedDisplayDistance">
<property name="text">
<string>Focused Display Distance:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="LabeledSlider" name="kcfg_FocusedDisplayDistance">
<property name="decimalShift">
<double>2</double>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
<property name="tickInterval">
<double>25</double>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tracking">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="labelAllDisplaysDistance">
<property name="text">
<string>All Displays Distance:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="LabeledSlider" name="kcfg_AllDisplaysDistance">
<property name="decimalShift">
<double>2</double>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
<property name="tickInterval">
<double>25</double>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tracking">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="labelDisplaySpacing">
<property name="text">
<string>Display Spacing:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSlider" name="kcfg_DisplaySpacing">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tracking">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="labelVirtualDisplays">
<property name="text">
<string>Add Virtual Display:</string>
</property>
<property name="visible">
<bool>false</bool>
</property>
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QWidget" name="widgetVirtualDisplayButtons">
<property name="visible">
<bool>false</bool>
</property>
<property name="enabled">
<bool>false</bool>
</property>
<layout class="QHBoxLayout" name="layoutVirtualDisplayButtons">
<item>
<widget class="QPushButton" name="buttonAdd1080p">
<property name="text">
<string>+ 1080p</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonAdd1440p">
<property name="text">
<string>+ 1440p</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonOpenDisplaysSettings">
<property name="text">
<string>Rearrange displays</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="6" column="0" colspan="2">
<widget class="KShortcutsEditor" name="shortcutsEditor" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabAdvanced">
<attribute name="title">
<string>&amp;Advanced</string>
</attribute>
<layout class="QFormLayout" name="formAdvanced">
<item row="0" column="0">
<widget class="QLabel" name="labelDisplayWrappingScheme">
<property name="text">
<string>Display Wrapping Scheme:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="kcfg_DisplayWrappingScheme">
<item>
<property name="text">
<string>Auto</string>
</property>
</item>
<item>
<property name="text">
<string>Horizontal</string>
</property>
</item>
<item>
<property name="text">
<string>Vertical</string>
</property>
</item>
<item>
<property name="text">
<string>Flat</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelAntialiasingQuality">
<property name="text">
<string>Anti-aliasing quality:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="kcfg_AntialiasingQuality">
<item>
<property name="text">
<string>None</string>
</property>
</item>
<item>
<property name="text">
<string>Medium</string>
</property>
</item>
<item>
<property name="text">
<string>High</string>
</property>
</item>
<item>
<property name="text">
<string>Very High</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelDisplayHorizontalOffset">
<property name="text">
<string>Display Horizontal Offset:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="LabeledSlider" name="kcfg_DisplayHorizontalOffset">
<property name="decimalShift">
<double>2</double>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
<property name="tickInterval">
<double>50</double>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tracking">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="labelDisplayVerticalOffset">
<property name="text">
<string>Display Vertical Offset:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="LabeledSlider" name="kcfg_DisplayVerticalOffset">
<property name="decimalShift">
<double>2</double>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
<property name="tickInterval">
<double>50</double>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tracking">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="kcfg_RemoveVirtualDisplaysOnDisable">
<property name="visible">
<bool>false</bool>
</property>
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Remove virtual displays on disable</string>
</property>
<property name="checked"><bool>true</bool></property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabLicenseDetails">
<attribute name="title">
<string>&amp;License Details</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayoutLicense">
<item>
<widget class="QLabel" name="labelLicenseSummary">
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="visible">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelDonateLink">
<property name="text">
<string>&lt;a href=&quot;https://ko-fi.com/wheaney&quot;&gt;Renew or support on Kofi&lt;/a&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignHCenter|Qt::AlignVCenter</set>
</property>
<property name="visible">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBoxEmail">
<property name="title">
<string>Request a token</string>
</property>
<layout class="QGridLayout" name="gridLayoutEmail">
<item row="0" column="0">
<widget class="QLineEdit" name="lineEditLicenseEmail">
<property name="placeholderText">
<string>you@example.com</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="buttonSubmitEmail">
<property name="text">
<string>Submit</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QLabel" name="labelEmailStatus">
<property name="text">
<string/>
</property>
<property name="visible">
<bool>false</bool>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBoxToken">
<property name="title">
<string>Verify token</string>
</property>
<layout class="QGridLayout" name="gridLayoutToken">
<item row="0" column="0">
<widget class="QLineEdit" name="lineEditLicenseToken"/>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="buttonSubmitToken">
<property name="text">
<string>Verify</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QLabel" name="labelTokenStatus">
<property name="text">
<string/>
</property>
<property name="visible">
<bool>false</bool>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacerLicense">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabAbout">
<attribute name="title">
<string>&amp;About</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayoutAbout">
<item>
<widget class="QLabel" name="labelAppNameVersion">
<property name="text">
<string>Breezy Desktop Effect - v0.0.0</string>
</property>
<property name="alignment">
<set>Qt::AlignHCenter|Qt::AlignVCenter</set>
</property>
<property name="font">
<font>
<pointsize>14</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
</widget>
</item>
<item>
<widget class="QLabel">
<property name="text">
<string>Author: Wayne Heaney &lt;wayne@xronlinux.com&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignHCenter|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel">
<property name="text">
<string>License: GPL-3.0</string>
</property>
<property name="alignment">
<set>Qt::AlignHCenter|Qt::AlignVCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacerAbout">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>KShortcutsEditor</class>
<extends>QWidget</extends>
<header>kshortcutseditor.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections>
</connections>
</ui>

View File

@ -1 +0,0 @@
#include "labeledslider.h"

View File

@ -1,159 +0,0 @@
#pragma once
#include <QSlider>
#include <QPainter>
#include <QPainterPath>
#include <QStyleOptionSlider>
#include <algorithm> // 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;
int whole = raw / divisor;
int frac = std::abs(raw % divisor);
QString fracStr = QString::number(frac).rightJustified(m_decimalShift, QLatin1Char('0'));
QString result = QString::number(std::abs(whole)) + QLatin1Char('.') + fracStr;
if (raw < 0) result.prepend(QLatin1Char('-'));
return result;
}
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;
}
};

View File

@ -1,31 +0,0 @@
#pragma once
#include <QKeySequence>
#include <Qt>
#include <QString>
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")
};
}

View File

@ -1,10 +0,0 @@
#include "breezydesktopeffect.h"
namespace KWin
{
KWIN_EFFECT_FACTORY(BreezyDesktopEffect, "metadata.json")
} // namespace KWin
#include "main.moc"

View File

@ -1,17 +0,0 @@
{
"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"
}

View File

@ -1,171 +0,0 @@
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 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: breezyDesktop.screens.length
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;
// 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: {
const displayNwu =
monitorPlacement.centerNoRotate
.times(monitorDistance / effect.allDisplaysDistance);
return rotationMatrix.times(displays.nwuToEusVector(displayNwu));
}
}
}
Timer {
interval: 500 // 500ms - 2x per second to avoid running this check too frequently
repeat: true
running: true
onTriggered: {
if (effect.imuRotations && effect.imuRotations.length > 0) {
let focusedIndex = -1;
if (effect.zoomOnFocusEnabled) {
focusedIndex = displays.findFocusedMonitor(
displays.eusToNwuQuat(effect.imuRotations[0]),
breezyDesktop.monitorPlacements.map(monitorVectors => monitorVectors.centerLook),
breezyDesktop.focusedMonitorIndex,
false, // TODO smooth follow
breezyDesktop.fovDetails,
breezyDesktop.screens.map(screen => screen.geometry)
);
}
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) {
const unfocusedDisplay = breezyDesktop.displayAtIndex(unfocusedIndex);
zoomOutAnimation.target = unfocusedDisplay;
zoomOutAnimation.target.targetDistance = effect.allDisplaysDistance;
zoomOutAnimation.start();
} else {
if (unfocusedIndex === -1) {
zoomInAnimation.target = focusedDisplay;
focusedDisplay.targetDistance = effect.focusedDisplayDistance;
zoomInAnimation.start();
} else {
zoomInSeqAnimation.target = focusedDisplay;
focusedDisplay.targetDistance = effect.focusedDisplayDistance;
const unfocusedDisplay = breezyDesktop.displayAtIndex(unfocusedIndex);
zoomOutSeqAnimation.target = unfocusedDisplay;
zoomOutSeqAnimation.target.targetDistance = effect.allDisplaysDistance;
zoomOnFocusSequence.start();
}
}
breezyDesktop.focusedMonitorIndex = focusedIndex;
}
}
}
}
NumberAnimation {
id: zoomOutAnimation
property: "monitorDistance"
to: effect.allDisplaysDistance
duration: 150
running: false
onFinished: {
const unfocusedDisplay = zoomOutAnimation.target;
if (unfocusedDisplay) {
unfocusedDisplay.monitorDistance = Qt.binding(function() { return effect.allDisplaysDistance; });
}
}
}
NumberAnimation {
id: zoomInAnimation
property: "monitorDistance"
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
property: "monitorDistance"
to: effect.allDisplaysDistance
duration: 150
}
PauseAnimation { duration: 50 }
NumberAnimation {
id: zoomInSeqAnimation
property: "monitorDistance"
to: effect.focusedDisplayDistance
duration: 300
}
}
}

View File

@ -1,53 +0,0 @@
import QtQuick
import QtQuick3D
Model {
id: display
required property QtObject screen
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: [
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"
}
]
}

View File

@ -1,80 +0,0 @@
import QtQuick
import QtQuick3D
Item {
id: root
required property Camera camera
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 (effect.imuRotations && effect.imuRotations.length > 0) {
updateCamera(applyLookAhead(
effect.imuRotations[0],
effect.imuRotations[1],
effect.imuTimeElapsedMs,
lookAheadMS(effect.imuTimestamp, effect.lookAheadConfig, -1)
));
}
}
}
}

View File

@ -1,43 +0,0 @@
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
}
}
}

View File

@ -1,405 +0,0 @@
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, defaultDisplayDistance, wrappingChoice) {
const aspect = viewportWidth / viewportHeight;
const fovRadians = diagonalToCrossFOVs(degreeToRadian(viewportDiagonalFOV), aspect);
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);
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;
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,
defaultDistanceVerticalRadians,
defaultDistanceHorizontalRadians,
lensDistancePixels,
completeScreenDistancePixels,
monitorWrappingScheme: monitorWrappingScheme,
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;
}
}

View File

@ -1,53 +0,0 @@
import QtQuick
Item {
id: singleDesktopView
property point cursorPos: effect.cursorPos
property bool supportsXR: false
property bool showCalibratingBanner: false
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 {
id: desktopViewComponent
screen: targetScreen
width: targetScreen.geometry.width
height: targetScreen.geometry.height
}
Image {
id: cursorImg
x: 0
y: 0
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
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
}
}
}

View File

@ -1,18 +0,0 @@
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;
}

View File

@ -1,10 +0,0 @@
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);
}

View File

@ -1,151 +0,0 @@
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 Max 2",
"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,
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 + dx,
y: g.y - screensYMid + dy,
width: g.width,
height: g.height
};
});
return displays.monitorsToPlacements(fovDetails, adjustedGeometries, effect.displaySpacing);
}
property bool targetScreenSupported: supportedModels.some(model => root.targetScreen.model.includes(model))
property bool imuResetState: effect.imuResetState
property bool isEnabled: effect.isEnabled
Component {
id: desktopViewComponent
SingleDesktopView {
supportsXR: targetScreenSupported
showCalibratingBanner: isEnabled && imuResetState
}
}
Component {
id: view3DComponent
View3D {
anchors.fill: parent
environment: SceneEnvironment {
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 {
id: camera
}
BreezyDesktop {
id: breezyDesktop
}
CameraController {
id: cameraController
anchors.fill: parent
camera: camera
}
}
}
Loader {
id: viewLoader
anchors.fill: parent
}
function checkLoadedComponent() {
console.log(`Breezy - checking screen ${targetScreen.model}: ${targetScreenSupported} ${isEnabled} ${imuResetState}`);
const show3DView = targetScreenSupported && isEnabled && !imuResetState;
viewLoader.sourceComponent = show3DView ? view3DComponent : desktopViewComponent;
}
onImuResetStateChanged: {
checkLoadedComponent();
}
onIsEnabledChanged: {
checkLoadedComponent();
}
Component.onCompleted: {
checkLoadedComponent();
}
}

View File

@ -1,26 +0,0 @@
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
$<$<CXX_COMPILER_ID:MSVC>:/EHsc>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-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)

View File

@ -1,111 +0,0 @@
// New implementation using QProcess to call python
#include "xrdriveripc.h"
#include <iostream>
#include <cmath>
#include <QFileInfo>
#include <QProcess>
#include <QProcessEnvironment>
#include <QStandardPaths>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
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::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();
}
std::optional<QJsonObject> XRDriverIPC::retrieveConfig() {
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;
return doc.object();
}
std::optional<QJsonObject> 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 doc.object();
}
bool XRDriverIPC::writeConfig(const QJsonObject &configUpdate) {
QByteArray payload = QJsonDocument(configUpdate).toJson(QJsonDocument::Compact);
QByteArray out = invokePython(QStringLiteral("write_config"), payload, {});
return !out.isEmpty();
}
bool XRDriverIPC::writeControlFlags(const std::map<std::string, bool> &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;
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;
QString result = QString::fromUtf8(out).trimmed().toLower();
return result == QStringLiteral("true");
}

View File

@ -1,103 +0,0 @@
// C++ bridge now invoking xrdriveripc via external python process
#pragma once
#include <QString>
#include <QByteArray>
#include <QJsonObject>
#include <optional>
// 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";
}
class XR_DRIVER_IPC_EXPORT XRDriverIPC {
public:
static XRDriverIPC &instance();
std::optional<QJsonObject> retrieveConfig();
std::optional<QJsonObject> retrieveDriverState();
bool writeConfig(const QJsonObject &configUpdate);
bool writeControlFlags(const std::map<std::string, bool> &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
};

View File

@ -1,70 +0,0 @@
#!/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
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
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(logger=Logger(), 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())