diff --git a/.clang-format b/.clang-format
new file mode 100644
index 00000000..27021c78
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,47 @@
+---
+BasedOnStyle: LLVM
+IndentWidth: 2
+TabWidth: 2
+UseTab: Never
+MaxEmptyLinesToKeep: 2
+ColumnLimit: 100
+
+Language: Cpp
+#LambdaBodyIndentation: OuterScope
+Cpp11BracedListStyle: true
+PointerAlignment: Left
+ConstructorInitializerIndentWidth: '2'
+ContinuationIndentWidth: 2
+SortIncludes: 'true'
+EmptyLineBeforeAccessModifier: Leave
+BinPackArguments: 'true'
+BinPackParameters: 'true'
+AlignAfterOpenBracket: Align
+AlignEscapedNewlines: Left
+KeepEmptyLinesAtTheStartOfBlocks: true
+AllowShortIfStatementsOnASingleLine: WithoutElse
+AllowShortLambdasOnASingleLine: All
+AllowShortLoopsOnASingleLine: true
+AllowShortCaseLabelsOnASingleLine: true
+AlwaysBreakTemplateDeclarations: 'Yes'
+AllowShortFunctionsOnASingleLine: Inline
+AllowShortBlocksOnASingleLine: Always
+AllowShortEnumsOnASingleLine: true
+BreakConstructorInitializers: BeforeComma
+BreakBeforeConceptDeclarations: 'true'
+Standard: c++14
+EmptyLineBeforeAccessModifier: Always
+BreakBeforeBinaryOperators: NonAssignment
+AlignConsecutiveAssignments: true
+NamespaceIndentation: Inner
+BreakBeforeBraces: Custom
+BraceWrapping:
+ AfterClass: true
+ AfterControlStatement: MultiLine
+ SplitEmptyFunction: false
+ SplitEmptyRecord: false
+ BeforeElse: true
+ BeforeLambdaBody: false
+
+
+...
diff --git a/.clang-tidy b/.clang-tidy
new file mode 100644
index 00000000..6f069ef0
--- /dev/null
+++ b/.clang-tidy
@@ -0,0 +1,29 @@
+---
+Checks: >
+ *,-fuchsia*,-android-*,
+ -modernize-pass-by-value,-modernize-use-trailing-return-type,
+ -llvmlibc-restrict-system-libc-headers,-llvmlibc-*,
+ -altera-unroll-loops,-altera-struct-pack-align,
+ -cppcoreguidelines-owning-memory,
+ -cppcoreguidelines-pro-bounds-array-to-pointer-decay,-hicpp-no-array-decay,
+ -llvm-qualified-auto,-readability-qualified-auto,
+ -cppcoreguidelines-avoid-magic-numbers,
+ -google-build-using-namespace,
+ -cppcoreguidelines-pro-type-vararg,-hicpp-vararg,
+ -cppcoreguidelines-pro-type-static-cast-downcast,
+ -cppcoreguidelines-pro-bounds-pointer-arithmetic,
+ -readability-implicit-bool-conversion, - readability-container-size-empty,
+ -hicpp-signed-bitwise, -cppcoreguidelines-macro-usage,
+ -cppcoreguidelines-avoid-c-arrays,-hicpp-avoid-c-arrays,-modernize-avoid-c-arrays,
+ -google-default-arguments,-google-readability-todo,
+ -hicpp-uppercase-literal-suffix,-readability-uppercase-literal-suffix,
+ -clang-analyzer-core.CallAndMessage, -readability-static-accessed-through-instance
+
+WarningsAsErrors: false
+CheckOptions:
+ - key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic
+ value: true
+ - key: readability-magic-numbers.IgnorePowersOf2IntegerValues
+ value: true
+ - key: readability-magic-numbers.IgnoredIntegerValues
+ value: '1;2;3;4;5;6;10;24;60;100;1000;'
diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml
index 685f9fc6..6d0706c1 100644
--- a/.github/workflows/ci-build.yml
+++ b/.github/workflows/ci-build.yml
@@ -16,22 +16,28 @@ jobs:
matrix:
docker_tag:
- archlinux
- - fedora-30
- fedora-31
- fedora-32
- fedora-33
- fedora-34
+ - fedora-37
+ - fedora-38
- debian-stretch
- debian-buster
- debian-bullseye
+ - debian-bookworm
- ubuntu-18.04
- ubuntu-20.04
- ubuntu-20.10
- ubuntu-21.04
+ - ubuntu-22.04
+ - ubuntu-23.04
- opensuse-15.0
- opensuse-15.1
- opensuse-15.2
- opensuse-15.3
+ - opensuse-15.4
+ - opensuse-15.5
- centos-8
os:
- ubuntu-latest
@@ -54,7 +60,11 @@ jobs:
# ===================================================================================
# ---------- Checkout and build inside docker container ----------
- - uses: actions/checkout@v1
+ - uses: actions/checkout@v3
+ with:
+ # unfortunately, currently we need all the history for a valid auto generated version
+ fetch-depth: 0
+
- run: |
export BRANCH=${GITHUB_REF/refs\/heads\//}
echo Detected branch: ${BRANCH}
@@ -112,14 +122,14 @@ jobs:
# ---------- Upload artifacts to github ----------
- name: Upload source-pkg artifact to github
if: startsWith(matrix.docker_tag, 'archlinux')
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
with:
name: source-package
path: ${{ env.src_pkg_artifact }}
- name: Upload version-info to github
if: startsWith(matrix.docker_tag, 'archlinux')
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
with:
name: version-info
path: |
@@ -127,7 +137,7 @@ jobs:
./version-branch
- name: Upload binary package artifact to github
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
with:
name: ${{ matrix.docker_tag }}-package
path: ${{ env.dist_pkg_artifact }}
@@ -185,13 +195,16 @@ jobs:
filename=$(basename -- "${{ env.dist_pkg_artifact }}")
export PKG_TYPE="${filename##*.}"
declare -A distromap=( ["debian-stretch"]="debian/stretch" ["debian-buster"]="debian/buster" \
- ["debian-bullseye"]="debian/bullseye" ["ubuntu-18.04"]="ubuntu/bionic" \
+ ["debian-bullseye"]="debian/bullseye" ["debian-bookworm"]="debian/bookworm" \
+ ["ubuntu-18.04"]="ubuntu/bionic" \
["ubuntu-20.04"]="ubuntu/focal" ["ubuntu-21.04"]="ubuntu/hirsute" \
+ ["ubuntu-22.04"]="ubuntu/jammy" ["ubuntu-23.04"]="ubuntu/lunar" \
["opensuse-15.1"]="opensuse/15.1" ["opensuse-15.2"]="opensuse/15.2" \
- ["opensuse-15.3"]="opensuse/15.3" ["centos-8"]="el/8" \
- ["fedora-30"]="fedora/30" ["fedora-31"]="fedora/31" \
+ ["opensuse-15.3"]="opensuse/15.3" ["opensuse-15.4"]="opensuse/15.4" \
+ ["opensuse-15.5"]="opensuse/15.5" ["centos-8"]="el/8" \
+ ["fedora-31"]="fedora/31" \
["fedora-32"]="fedora/32" ["fedora-33"]="fedora/33" \
- ["fedora-34"]="fedora/34" )
+ ["fedora-34"]="fedora/34" ["fedora-37"]="fedora/37" ["fedora-38"]="fedora/38" )
export DISTRO=${distromap[${{ matrix.docker_tag }}]}
echo PKGTYPE=$PKG_TYPE
echo DISTRO=$DISTRO
@@ -220,7 +233,7 @@ jobs:
steps:
- name: Get version-info
- uses: actions/download-artifact@v2
+ uses: actions/download-artifact@v3
with:
name: version-info
@@ -233,7 +246,7 @@ jobs:
DO_UPLOAD=$(( [ "master" = "$BRANCH" ] || [ "develop" = "$BRANCH" ] ) && echo true || echo false)
echo "DO_UPLOAD=${DO_UPLOAD}" >> $GITHUB_ENV
- - uses: actions/download-artifact@v2
+ - uses: actions/download-artifact@v3
if: env.DO_UPLOAD == 'true'
with:
path: artifacts
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 531ff467..388ee417 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -22,7 +22,7 @@ jobs:
qt5-default libqt5x11extras5-dev
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -30,8 +30,8 @@ jobs:
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- - run: git checkout HEAD^2
- if: ${{ github.event_name == 'pull_request' }}
+ # - run: git checkout HEAD^2
+ # if: ${{ github.event_name == 'pull_request' }}
- name: Configure and build Qt moc cpps
run: |
@@ -42,7 +42,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v1
+ uses: github/codeql-action/init@v2
with:
queries: +security-and-quality
@@ -52,4 +52,4 @@ jobs:
make -j2
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v1
+ uses: github/codeql-action/analyze@v2
diff --git a/.gitignore b/.gitignore
index c85722f0..0535854a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,6 @@ CMakeLists.txt.user*
.vscode
.idea
*.code-workspace
-build
+build-*
build/*
icons/icon-font/output/
diff --git a/55-projecteur.rules.in b/55-projecteur.rules.in
index aff67495..af461319 100644
--- a/55-projecteur.rules.in
+++ b/55-projecteur.rules.in
@@ -12,9 +12,11 @@ SUBSYSTEMS=="usb", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c53e", MODE="0660
# Rule fot the Logitech Spotlight when connected via Bluetooth
# Updated rule, thanks to Torsten Maehne (https://github.com/maehne)
SUBSYSTEMS=="input", ENV{LIBINPUT_DEVICE_GROUP}="5/46d/b503*", ATTRS{name}=="SPOTLIGHT*", MODE="0660", TAG+="uaccess"
+# Additional rule for Bluetooth sub-devices (hidraw)
+SUBSYSTEMS=="hid", KERNELS=="0005:046D:B503.*", MODE="0660", TAG+="uaccess"
# Additional supported Bluetooth devices @EXTRA_BLUETOOTH_UDEV_RULES@
-# Rules for uninput: Essential for creating a virtual input device that
-# Projecteur use for forwarding device events to the system after grabbing it
+# Rules for uinput: Essential for creating a virtual input device that
+# Projecteur uses to forward device events to the system after grabbing it
KERNEL=="uinput", SUBSYSTEM=="misc", TAG+="uaccess", OPTIONS+="static_node=uinput"
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0825b6f2..6d1691ff 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -20,22 +20,55 @@ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeR
project(Projecteur LANGUAGES CXX)
add_compile_options(-Wall -Wextra -Werror)
-#set(CMAKE_CXX_CLANG_TIDY "clang-tidy-9;-checks=*,-fuchsia*,-modernize-pass-by-value")
+#set(CMAKE_CXX_CLANG_TIDY clang-tidy-12)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules")
include(GitVersion)
include(Translation)
-set(CMAKE_CXX_STANDARD 14)
+set(QtVersionOptions "Auto" "5" "6")
+set(PROJECTEUR_QT_VERSION "Auto" CACHE STRING "Choose the Qt version")
+set_property(CACHE PROJECTEUR_QT_VERSION PROPERTY STRINGS ${QtVersionOptions})
+
+list(FIND QtVersionOptions ${PROJECTEUR_QT_VERSION} index)
+if(index EQUAL -1)
+ message(FATAL_ERROR "PROJECTEUR_QT_VERSION must be one of ${QtVersionOptions}")
+endif()
+
+if ("${PROJECTEUR_QT_VERSION}" STREQUAL "Auto")
+ find_package(QT NAMES Qt6 Qt5 RCOMPONENTS Core REQUIRED)
+else()
+ set(QT_VERSION_MAJOR ${PROJECTEUR_QT_VERSION})
+endif()
+
+find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core REQUIRED)
+set(QT_PACKAGE_NAME Qt${QT_VERSION_MAJOR})
+
+message(STATUS "Using Qt version: ${Qt${QT_VERSION_MAJOR}_VERSION}")
+
+if(${QT_PACKAGE_NAME}_VERSION VERSION_LESS "6.0")
+ set(CMAKE_CXX_STANDARD 14)
+else()
+ set(CMAKE_CXX_STANDARD 17)
+endif()
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
-set(CMAKE_AUTORCC ON)
-find_package(Qt5 5.7 COMPONENTS Core Gui Quick Widgets REQUIRED)
-find_package(Qt5 QUIET COMPONENTS X11Extras)
-set(HAS_Qt5_X11Extras ${Qt5_FOUND})
-find_package(Qt5 QUIET COMPONENTS DBus)
-set(HAS_Qt5_DBus ${Qt5_FOUND})
+
+find_package(${QT_PACKAGE_NAME} 5.7 REQUIRED COMPONENTS Core Gui Quick Widgets)
+
+if(${QT_PACKAGE_NAME}_VERSION VERSION_LESS "6.0")
+ find_package(${QT_PACKAGE_NAME} QUIET COMPONENTS X11Extras)
+ set(HAS_Qt_X11Extras ${${QT_PACKAGE_NAME}_FOUND})
+else()
+ set(HAS_Qt_X11Extras 0)
+endif()
+
+find_package(${QT_PACKAGE_NAME} QUIET COMPONENTS DBus)
+set(HAS_Qt_DBus ${${QT_PACKAGE_NAME}_FOUND})
+find_package(${QT_PACKAGE_NAME} QUIET COMPONENTS QuickCompiler)
+set(HAS_Qt_QuickCompiler ${${QT_PACKAGE_NAME}_FOUND})
# Qt 5.8 seems to have issues with the way Projecteur shows the full screen overlay window,
# let's warn the user about it.
@@ -45,48 +78,83 @@ if(Qt5_VERSION VERSION_EQUAL "5.8"
"please use a different Qt Version.")
endif()
+if (HAS_Qt_QuickCompiler)
+ # Off by default, since this ties the application strictly to the Qt version
+ # it is built with, see https://doc.qt.io/qt-5.12/qtquick-deployment.html#compiling-qml-ahead-of-time
+ option(USE_QTQUICK_COMPILER "Use the QtQuickCompiler" OFF)
+else()
+ set(USE_QTQUICK_COMPILER OFF)
+endif()
+
+if (USE_QTQUICK_COMPILER)
+ message(STATUS "Using QtQuick Compiler.")
+ qtquick_compiler_add_resources(RESOURCES qml/qml.qrc)
+ # Avoid CMake policy CMP0071 warning
+ foreach(resfile IN LISTS RESOURCES)
+ set_property(SOURCE "${resfile}" PROPERTY SKIP_AUTOMOC ON)
+ endforeach()
+else()
+ if(${QT_PACKAGE_NAME}_VERSION VERSION_LESS "6.0")
+ qt5_add_resources(RESOURCES qml/qml.qrc)
+ else()
+ qt6_add_resources(RESOURCES qml/qml-qt6.qrc)
+ endif()
+endif()
+
+if(${QT_PACKAGE_NAME}_VERSION VERSION_LESS "6.0")
+ qt5_add_resources(RESOURCES resources.qrc)
+else()
+ qt6_add_resources(RESOURCES resources.qrc)
+endif()
+
add_executable(projecteur
- src/main.cc src/enum-helper.h
- src/aboutdlg.cc src/aboutdlg.h
- src/actiondelegate.cc src/actiondelegate.h
- src/colorselector.cc src/colorselector.h
- src/device.cc src/device.h
- src/device-vibration.cc src/device-vibration.h
- src/deviceinput.cc src/deviceinput.h
- src/devicescan.cc src/devicescan.h
- src/deviceswidget.cc src/deviceswidget.h
- src/linuxdesktop.cc src/linuxdesktop.h
- src/iconwidgets.cc src/iconwidgets.h
- src/imageitem.cc src/imageitem.h
- src/inputmapconfig.cc src/inputmapconfig.h
- src/inputseqedit.cc src/inputseqedit.h
- src/logging.cc src/logging.h
- src/nativekeyseqedit.cc src/nativekeyseqedit.h
- src/preferencesdlg.cc src/preferencesdlg.h
- src/projecteurapp.cc src/projecteurapp.h
- src/runguard.cc src/runguard.h
- src/settings.cc src/settings.h
- src/spotlight.cc src/spotlight.h
- src/spotshapes.cc src/spotshapes.h
- src/virtualdevice.h src/virtualdevice.cc
- resources.qrc qml/qml.qrc)
+ src/main.cc src/enum-helper.h
+ src/aboutdlg.cc src/aboutdlg.h
+ src/actiondelegate.cc src/actiondelegate.h
+ src/colorselector.cc src/colorselector.h
+ src/device.cc src/device.h
+ src/device-command-helper.cc src/device-command-helper.h
+ src/device-hidpp.cc src/device-hidpp.h
+ src/device-key-lookup.cc src/device-key-lookup.h
+ src/device-vibration.cc src/device-vibration.h
+ src/deviceinput.cc src/deviceinput.h
+ src/devicescan.cc src/devicescan.h
+ src/deviceswidget.cc src/deviceswidget.h
+ src/hidpp.cc src/hidpp.h
+ src/linuxdesktop.cc src/linuxdesktop.h
+ src/iconwidgets.cc src/iconwidgets.h
+ src/imageitem.cc src/imageitem.h
+ src/inputmapconfig.cc src/inputmapconfig.h
+ src/inputseqedit.cc src/inputseqedit.h
+ src/logging.cc src/logging.h
+ src/nativekeyseqedit.cc src/nativekeyseqedit.h
+ src/preferencesdlg.cc src/preferencesdlg.h
+ src/projecteurapp.cc src/projecteurapp.h
+ src/runguard.cc src/runguard.h
+ src/settings.cc src/settings.h
+ src/spotlight.cc src/spotlight.h
+ src/spotshapes.cc src/spotshapes.h
+ src/virtualdevice.cc src/virtualdevice.h
+ ${RESOURCES})
target_include_directories(projecteur PRIVATE src)
target_link_libraries(projecteur
- PRIVATE Qt5::Core Qt5::Quick Qt5::Widgets
+ PRIVATE ${QT_PACKAGE_NAME}::Core ${QT_PACKAGE_NAME}::Quick ${QT_PACKAGE_NAME}::Widgets
)
-if(HAS_Qt5_X11Extras)
- target_link_libraries(projecteur PRIVATE Qt5::X11Extras)
- target_compile_definitions(projecteur PRIVATE HAS_Qt5_X11Extras=1)
+if(HAS_Qt_X11Extras)
+ if(${QT_PACKAGE_NAME}_VERSION VERSION_LESS "6.0")
+ target_link_libraries(projecteur PRIVATE ${QT_PACKAGE_NAME}::X11Extras)
+ endif()
+ target_compile_definitions(projecteur PRIVATE HAS_Qt_X11Extras=1)
else()
message(STATUS "Compiling without Qt5::X11Extras.")
endif()
-if(HAS_Qt5_DBus)
- target_link_libraries(projecteur PRIVATE Qt5::DBus)
- target_compile_definitions(projecteur PRIVATE HAS_Qt5_DBus=1)
+if(HAS_Qt_DBus)
+ target_link_libraries(projecteur PRIVATE ${QT_PACKAGE_NAME}::DBus)
+ target_compile_definitions(projecteur PRIVATE HAS_Qt_DBus=1)
else()
message(STATUS "Compiling without Qt5::DBus.")
endif()
@@ -106,9 +174,10 @@ target_compile_definitions(projecteur PRIVATE
# VERSION_TYPE must be either 'release' or 'develop'
set_target_properties(projecteur PROPERTIES
VERSION_MAJOR 0
- VERSION_MINOR 9
- VERSION_PATCH 1
+ VERSION_MINOR 10
+ VERSION_PATCH 0
VERSION_TYPE release
+ VERSION_DISTANCE_OFFSET 0
)
add_version_info(projecteur "${CMAKE_CURRENT_SOURCE_DIR}")
@@ -128,7 +197,7 @@ add_translation_update_task("projecteur" "${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_
# Add target with non-source files for convenience when using IDEs like QtCreator and others
add_custom_target(non-sources SOURCES README.md LICENSE.md doc/CHANGELOG.md devices.conf
src/extra-devices.cc.in 55-projecteur.rules.in
- cmake/templates/Projecteur.desktop.in)
+ cmake/templates/projecteur.desktop.in)
# Install
#---------------------------------------------------------------------------------------------------
@@ -218,19 +287,25 @@ get_target_property(VERSION_STRING projecteur VERSION_STRING)
get_target_property(VERSION_DATE_MONTH_YEAR projecteur VERSION_DATE_MONTH_YEAR)
set(HOMEPAGE "https://github.com/jahnf/Projecteur")
-configure_file("${TMPLDIR}/Projecteur.desktop.in" "projecteur.desktop" @ONLY)
+configure_file("${TMPLDIR}/projecteur.desktop.in" "projecteur.desktop" @ONLY)
install(FILES "${OUTDIR}/projecteur.desktop" DESTINATION share/applications/)
# Configure man page and gzip it.
-configure_file("${TMPLDIR}/projecteur.1" "projecteur.1" @ONLY)
-find_program(GZIP_EXECUTABLE gzip)
-add_custom_command(
- OUTPUT ${OUTDIR}/projecteur.1.gz
- COMMAND ${GZIP_EXECUTABLE} -9f -n "${OUTDIR}/projecteur.1"
- WORKING_DIRECTORY ${OUTDIR}
-)
-add_custom_target(gzip-manpage ALL DEPENDS "${OUTDIR}/projecteur.1.gz")
-install(FILES "${OUTDIR}/projecteur.1.gz" DESTINATION share/man/man1/)
+option(COMPRESS_MAN_PAGE "Compress the man page" ON)
+configure_file("${TMPLDIR}/projecteur.1" "${OUTDIR}/projecteur.1" @ONLY)
+
+if(COMPRESS_MAN_PAGE)
+ find_program(GZIP_EXECUTABLE gzip)
+ add_custom_command(
+ OUTPUT ${OUTDIR}/projecteur.1.gz
+ COMMAND ${GZIP_EXECUTABLE} -9f -n "${OUTDIR}/projecteur.1"
+ WORKING_DIRECTORY ${OUTDIR}
+ )
+ add_custom_target(gzip-manpage ALL DEPENDS "${OUTDIR}/projecteur.1.gz")
+ install(FILES "${OUTDIR}/projecteur.1.gz" DESTINATION share/man/man1/)
+else()
+ install(FILES "${OUTDIR}/projecteur.1" DESTINATION share/man/man1/)
+endif()
configure_file("${TMPLDIR}/projecteur.metainfo.xml" "projecteur.metainfo.xml" @ONLY)
install(FILES "${OUTDIR}/projecteur.metainfo.xml" DESTINATION share/metainfo/)
@@ -270,7 +345,10 @@ if(PACKAGE_TARGETS)
# PREINST_SCRIPT "${OUTDIR}/pkg/scripts/preinst"
POSTINST_SCRIPT "${OUTDIR}/pkg/scripts/postinst"
)
- add_dependencies(dist-package gzip-manpage projecteur)
+ add_dependencies(dist-package projecteur)
+ if(TARGET gzip-manpage)
+ add_dependencies(dist-package gzip-manpage)
+ endif()
# Additional files for debian packages, adhering to some debian rules,
# see https://manpages.debian.org/buster/lintian/lintian.1.en.html
diff --git a/README.md b/README.md
index 05eee932..25367c50 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# Projecteur
-develop: [ ![Build Status develop][gh-badge-dev] ][gh-link-dev]
-master: [ ![Build Status master][gh-badge-rel] ][gh-link-rel]
+develop: [![Build Status develop][gh-badge-dev]][gh-link-dev]
+master: [![Build Status master][gh-badge-rel]][gh-link-rel]
Linux/X11 application for the Logitech Spotlight device (and similar devices). \
See **[Download](#download)** section for binary packages.
@@ -22,22 +22,39 @@ So here it is: a Linux application for the Logitech Spotlight.
## Table of Contents
- * [Motivation](#motivation)
- * [Features](#features)
- * [Supported Environments](#supported-environments)
- * [How it works](#how-it-works)
- * [Download](#download)
- * [Building](#building)
- * [Installation/Running](#installationrunning)
- * [Pre-requisites](#pre-requisites)
- * [Application Menu](#application-menu)
- * [Command Line Interface](#command-line-interface)
- * [Scriptability / Keyboard shortcuts](#scriptability)
- * [Using Projecteur without a device](#using-projecteur-without-a-device)
- * [Device Support](#device-support)
- * [Troubleshooting](#troubleshooting)
- * [Changelog](#changelog)
- * [License](#license)
+- [Projecteur](#projecteur)
+ - [Motivation](#motivation)
+ - [Table of Contents](#table-of-contents)
+ - [Features](#features)
+ - [Screenshots](#screenshots)
+ - [Planned features](#planned-features)
+ - [Supported Environments](#supported-environments)
+ - [How it works](#how-it-works)
+ - [Button mapping](#button-mapping)
+ - [Hold Button Mapping for Logitech Spotlight](#hold-button-mapping-for-logitech-spotlight)
+ - [Download](#download)
+ - [Building](#building)
+ - [Requirements](#requirements)
+ - [Build Example](#build-example)
+ - [Installation/Running](#installationrunning)
+ - [Pre-requisites](#pre-requisites)
+ - [When building Projecteur yourself](#when-building-projecteur-yourself)
+ - [Application Menu](#application-menu)
+ - [Command Line Interface](#command-line-interface)
+ - [Scriptability](#scriptability)
+ - [Using Projecteur without a device](#using-projecteur-without-a-device)
+ - [Device Support](#device-support)
+ - [Compile Time](#compile-time)
+ - [Runtime](#runtime)
+ - [Troubleshooting](#troubleshooting)
+ - [Opaque Spotlight / No Transparency](#opaque-spotlight--no-transparency)
+ - [Missing System Tray](#missing-system-tray)
+ - [Zoom is not updated while spotlight is shown](#zoom-is-not-updated-while-spotlight-is-shown)
+ - [Wayland](#wayland)
+ - [Wayland Zoom](#wayland-zoom)
+ - [Device shows as not connected](#device-shows-as-not-connected)
+ - [Changelog](#changelog)
+ - [License](#license)
## Features
@@ -49,7 +66,8 @@ So here it is: a Linux application for the Logitech Spotlight.
* Button mapping:
* Map any button on the device to (almost) any keyboard combination.
* Switch between (cycle through) custom spotlight presets.
-* Vibration (Timer) Support for the Logitech Spotlight (USB)
+ * Audio Volume / Horizontal and Vertical Scrolling (Logitech Spotlight).
+* Vibration (Timer) Support for the Logitech Spotlight
* Usable without a presenter device (e.g. for online presentations)
### Screenshots
@@ -57,7 +75,7 @@ So here it is: a Linux application for the Logitech Spotlight.
[
](./doc/screenshot-settings.png)
[
](./doc/screenshot-spot.png)
[
](./doc/screenshot-button-mapping.png)
-[
](./doc/screenshot-traymenu.png)
+[
](./doc/screenshot-traymenu.png)
### Planned features
@@ -91,15 +109,48 @@ For more details: Have a look at the source code ;)
Button mapping works by **grabbing** all device events of connected
devices and forwarding them to a virtual _'uinput'_ device if not configured
differently by the button mapping configuration. If a mapped configuration for
-a button exists, _Projecteur_ will inject the mapped keyboard events instead.
+a button exists, _Projecteur_ will inject the mapped action instead.
(You can still disable device grabbing with the `--disable-uinput` command
line option - button mapping will be disabled then.)
+Input events from the presenter device can be mapped to different actions.
+The _Key Sequence_ action is particularly powerful as it can emit any user-defined
+keystroke. These keystrokes can invoke shortcut in presentation software
+(or any other software) being used. Similarly, the _Cycle Preset_ action can be
+used for cycling different spotlight presets. However, it should be noted that
+presets are ordered alphabetically on program start. To retain a certain
+order of your presets, you can prepend the preset name with a number.
+
+#### Hold Button Mapping for Logitech Spotlight
+
+Logitech Spotlight can send Hold event for Next and Back buttons as HID++
+messages. Using this device feature, this program provides three different
+usage of the Next or Hold button.
+
+1. Button Tap
+2. Long-Press Event
+3. Button Hold and Move Event
+
+On the Input Mapper tab (Devices tab in Preferences dialog box), the first two
+button usages (_i.e._ tap and long-press) can be mapped directly by tapping or
+long pressing the relevant button. For mapping the third button usage (_i.e._
+Hold Move Event), please ensure that the device is active by pressing any button,
+and then right click in first column (Input Sequence) for any entry and select
+the relevant option. Additional mapped actions (e.g. _Vertical Scrolling_,
+_Horizontal Scrolling_, or _Volume control_) can be selected for these hold
+move events.
+
+Please note that in case when both Long-Press event and Hold Move events are
+mapped for a particular button, both actions will executed if user hold the
+button and move device. To avoid this situation, do not set both Long-Press
+and Hold Move actions for the same button.
+
## Download
-The latest binary packages for some Linux distributions are available for download on bintray.
+The latest binary packages for some Linux distributions are available for download on cloudsmith.
Currently binary packages for _Ubuntu_, _Debian_, _Fedora_, _OpenSuse_, _CentOS_ and
-_Arch_ Linux are automatically built.
+_Arch_ Linux are automatically built. For release version downloads you can also visit
+the project's [github releases page](https://github.com/jahnf/Projecteur/releases).
* **Latest release:**
* on cloudsmith: [![cloudsmith-rel-badge]][cloudsmith-rel-latest]
@@ -108,12 +159,12 @@ _Arch_ Linux are automatically built.
* on cloudsmith: [![cloudsmith-dev-badge]][cloudsmith-dev-latest]
* on secondary server: [![projecteur-dev-badge]][projecteur-dev-dl]
-See also the [list of Linux repositories](./doc/LinuxRepositories.md) where _Projecteur_
+See also the **[list of Linux repositories](./doc/LinuxRepositories.md)** where _Projecteur_
is available.
-[cloudsmith-rel-badge]: https://api-prd.cloudsmith.io/v1/badges/version/jahnf/projecteur-stable/raw/sources/latest/x/?render=true&badge_token=gAAAAABgPebvngKb3w0EsZUr_IHIIzlfYCipDOGxcJdzMRGI3BLdVsLf62Na7Cg6q11ps7yNgv3kR9KXyxJyjFFbPs2eTAGzvL-UXTonyqSY5D1fwva_o_g%3D
+[cloudsmith-rel-badge]: https://img.shields.io/badge/dynamic/json?color=blue&labelColor=12577e&logo=cloudsmith&label=Projecteur&prefix=v&query=%24.version&url=https%3A%2F%2Fprojecteur.de%2Fdownloads%2Fstable-latest.json
[cloudsmith-rel-latest]: https://cloudsmith.io/~jahnf/repos/projecteur-stable/packages/?q=format%3Araw+tag%3Alatest
-[cloudsmith-dev-badge]: https://api-prd.cloudsmith.io/v1/badges/version/jahnf/projecteur-develop/raw/sources/latest/x/?render=true&badge_token=gAAAAABgPd_g3txb3xWrIHsaUrhBB7hOamTwfPVpR7xGUELEaQ0pGnxFnXO1cqTPAMDcTjRsHM2zAjx00OXU_5ARSQDofAUe6lIqKrKNykiMhVT_jlZAy-4%3D
+[cloudsmith-dev-badge]: https://img.shields.io/badge/dynamic/json?color=blue&labelColor=12577e&logo=cloudsmith&label=Projecteur&prefix=v&query=%24.version&url=https%3A%2F%2Fprojecteur.de%2Fdownloads%2Fdevelop-latest.json
[cloudsmith-dev-latest]: https://cloudsmith.io/~jahnf/repos/projecteur-develop/packages/?q=format%3Araw+tag%3Alatest
[projecteur-rel-badge]: https://img.shields.io/badge/dynamic/json?color=blue&label=Projecteur&prefix=v&query=%24.version&url=https%3A%2F%2Fprojecteur.de%2Fdownloads%2Fstable-latest.json
[projecteur-dev-badge]: https://img.shields.io/badge/dynamic/json?color=blue&label=Projecteur&prefix=v&query=%24.version&url=https%3A%2F%2Fprojecteur.de%2Fdownloads%2Fdevelop-latest.json
@@ -130,7 +181,7 @@ is available.
### Build Example
-```
+```sh
git clone https://github.com/jahnf/Projecteur
cd Projecteur
mkdir build && cd build
@@ -178,26 +229,28 @@ see the [Troubleshooting](#missing-system-tray) section.
Additional to the standard `--help` and `--version` options, there is an option to send
commands to a running instance of _Projecteur_ and the ability to set properties.
-```
+```txt
Usage: projecteur [OPTION]...
- -h, --help Show command line usage.
- --help-all Show complete command line usage with all properties.
- -v, --version Print application version.
- -f, --fullversion Print extended version info.
- --cfg FILE Set custom config file.
- -d, --device-scan Print device-scan results.
- -l, --log-level LEVEL Set log level (dbg,inf,wrn,err), default is 'inf'.
- --show-dialog Show preferences dialog on start.
- -m, --minimize-only Only allow minimizing the preferences dialog.
- -D DEVICE Additional accepted device; DEVICE=vendorId:productId
- -c COMMAND|PROPERTY Send command/property to a running instance.
+ -h, --help Show command line usage.
+ --help-all Show complete command line usage with all properties.
+ -v, --version Print application version.
+ -f, --fullversion Print extended version info.
+ --cfg FILE Set custom config file.
+ -d, --device-scan Print device-scan results.
+ -l, --log-level LEVEL Set log level (dbg,inf,wrn,err), default is 'inf'.
+ --show-dialog Show preferences dialog on start.
+ -m, --minimize-only Only allow minimizing the preferences dialog.
+ -D DEVICE Additional accepted device; DEVICE=vendorId:productId
+ -c COMMAND|PROPERTY Send command/property to a running instance.
- spot=[on|off|toggle] Turn spotlight on/off or toggle.
- settings=[show|hide] Show/hide preferences dialog.
- quit Quit the running instance.
+ spot=[on|off|toggle] Turn spotlight on/off or toggle.
+ spot.size.adjust=[+|-]N Increase or decrease spot size by N.
+ settings=[show|hide] Show/hide preferences dialog.
+ preset=NAME Set a preset.
+ quit Quit the running instance.
```
A complete list the properties that can be set via the command line, can be listed with the
@@ -210,11 +263,15 @@ _Projecteur_ allows you to set almost all aspects of the spotlight via the comma
for a running instance.
Example:
+
```bash
# Set showing the border to true
projecteur -c border=true
# Set the border color to red
projecteur -c border.color=#ff0000
+# Send a vibrate command to the device with
+# intensity=128 and length=0 (only Logitech Spotlight)
+projecteur -c vibrate=128,0
```
While _Projecteur_ does not provide global keyboard shortcuts, command line options
@@ -224,6 +281,9 @@ shortcuts in your window manager (e.g. GNOME) to run the commands `projecteur -c
and `projecteur -c spot=off` or `projecteur -c spot=toggle`, and therefore
turning the spot on and off with a keyboard shortcut.
+A complete list the properties that can be set via the command line, can be
+listed with the `--help-all` command line option.
+
### Using Projecteur without a device
You can use _Projecteur_ for your online presentations and video conferences without a presenter
@@ -239,6 +299,8 @@ Besides the _Logitech Spotlight_, the following devices are currently supported
* AVATTO H100 / August WP200 _(0c45:8101)_
* August LP315 _(2312:863d)_
* AVATTO i10 Pro _(2571:4109)_
+* August LP310 _(69a7:9803)_
+* Norwii Wireless Presenter _(3243:0122)_
#### Compile Time
@@ -305,7 +367,7 @@ While not developed with Wayland in mind, users reported _Projecteur_ works with
Wayland. If you experience problems, you can try to set the `QT_QPA_PLATFORM` environment
variable to `wayland`, example:
-```
+```bash
user@ubuntu1904:~/Projecteur/build$ QT_QPA_PLATFORM=wayland ./projecteur
Using Wayland-EGL
```
diff --git a/cmake/modules/GitVersion.cc.in b/cmake/modules/GitVersion.cc.in
index ec137733..d2f4e557 100644
--- a/cmake/modules/GitVersion.cc.in
+++ b/cmake/modules/GitVersion.cc.in
@@ -11,4 +11,5 @@ namespace @TARGET@ {
const char* version_fullhash() { return "@VERSION_FULLHASH@"; }
bool version_isdirty() { return @VERSION_ISDIRTY@; }
const char* version_branch() { return "@VERSION_BRANCH@"; }
-}
+ const char* version_buildtype() { return "@VERSION_BUILDTYPE@"; }
+} // end namespace @TARGET@
diff --git a/cmake/modules/GitVersion.cmake b/cmake/modules/GitVersion.cmake
index 6ff5c89d..d4bc89fe 100644
--- a/cmake/modules/GitVersion.cmake
+++ b/cmake/modules/GitVersion.cmake
@@ -64,7 +64,11 @@ function(get_version_info prefix directory)
set(${prefix}_VERSION_STRING 0.0.0-unknown)
set(${prefix}_VERSION_STRING_FULL 0.0.0-unknown)
set(${prefix}_VERSION_ISDIRTY 0 PARENT_SCOPE)
+ set(${prefix}_VERSION_BUILDTYPE "${CMAKE_BUILD_TYPE}" PARENT_SCOPE)
set(${prefix}_VERSION_DATE_MONTH_YEAR "" PARENT_SCOPE)
+ if("${${prefix}_VERSION_DISTANCE_OFFSET}" STREQUAL "")
+ set(${prefix}_VERSION_DISTANCE_OFFSET 0)
+ endif()
if("${${prefix}_OR_VERSION_MAJOR}" STREQUAL "")
set(${prefix}_OR_VERSION_MAJOR 0)
@@ -76,6 +80,8 @@ function(get_version_info prefix directory)
set(${prefix}_OR_VERSION_PATCH 0)
endif()
+ set(${prefix}_VERSION_BUILDTYPE "${CMAKE_BUILD_TYPE}")
+
find_package(Git)
if(GIT_FOUND)
# Get the version info from the last tag
@@ -245,7 +251,10 @@ function(get_version_info prefix directory)
endif()
set(${prefix}_VERSION_FLAG ${${prefix}_VERSION_FLAG} PARENT_SCOPE)
- set(${prefix}_VERSION_DISTANCE ${${prefix}_VERSION_DISTANCE} PARENT_SCOPE)
+
+ math(EXPR CALCULATED_GIT_DISTANCE "${${prefix}_VERSION_DISTANCE}+${${prefix}_VERSION_DISTANCE_OFFSET}")
+ set(${prefix}_VERSION_DISTANCE ${CALCULATED_GIT_DISTANCE})
+ set(${prefix}_VERSION_DISTANCE ${CALCULATED_GIT_DISTANCE} PARENT_SCOPE)
execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD
RESULT_VARIABLE resultSH
@@ -312,7 +321,7 @@ function(get_version_info prefix directory)
if(NOT ${${prefix}_VERSION_PATCH} EQUAL 0)
set(VERSION_STRING "${VERSION_STRING}.${${prefix}_VERSION_PATCH}")
endif()
- if(NOT ON_MASTER OR NOT ${${prefix}_VERSION_DISTANCE} EQUAL 0)
+ if(NOT ON_MASTER OR NOT ${${prefix}_VERSION_DISTANCE} EQUAL ${${prefix}_VERSION_DISTANCE_OFFSET})
set(VERSION_STRING "${VERSION_STRING}-${${prefix}_VERSION_FLAG}")
set(VERSION_STRING_FULL "${VERSION_STRING_FULL}-${${prefix}_VERSION_FLAG}")
endif()
@@ -320,7 +329,10 @@ function(get_version_info prefix directory)
set(VERSION_STRING "${VERSION_STRING}.")
set(VERSION_STRING_FULL "${VERSION_STRING_FULL}.")
endif()
- if(NOT ON_MASTER OR (NOT ON_MASTER AND NOT ${${prefix}_VERSION_DISTANCE} EQUAL 0))
+ if(NOT ON_MASTER OR (NOT ON_MASTER AND NOT ${${prefix}_VERSION_DISTANCE} EQUAL ${${prefix}_VERSION_DISTANCE_OFFSET}))
+ set(VERSION_STRING "${VERSION_STRING}${${prefix}_VERSION_DISTANCE}")
+ set(VERSION_STRING_FULL "${VERSION_STRING_FULL}${${prefix}_VERSION_DISTANCE}")
+ elseif(ON_MASTER AND NOT ${${prefix}_VERSION_DISTANCE} EQUAL ${${prefix}_VERSION_DISTANCE_OFFSET})
set(VERSION_STRING "${VERSION_STRING}${${prefix}_VERSION_DISTANCE}")
set(VERSION_STRING_FULL "${VERSION_STRING_FULL}${${prefix}_VERSION_DISTANCE}")
endif()
@@ -349,11 +361,13 @@ function(add_version_info_custom_prefix target prefix directory)
set(VERSION_PATCH 0)
set(VERSION_FLAG unknown)
set(VERSION_DISTANCE 0)
+ set(VERSION_DISTANCE_OFFSET 0)
set(VERSION_SHORTHASH unknown)
set(VERSION_FULLHASH unknown)
set(VERSION_STRING "0.0-unknown.0")
set(VERSION_STRING_FULL "0.0.0-unknown.0")
set(VERSION_ISDIRTY 0)
+ set(VERSION_BUILDTYPE "unknown")
set(VERSION_BRANCH unknown)
set(output_dir "${CMAKE_CURRENT_BINARY_DIR}/version/${targetid}")
@@ -374,10 +388,17 @@ function(add_version_info_custom_prefix target prefix directory)
if(TARGET_VTYPE)
set(${prefix}_FALLBACK_VERSION_TYPE ${TARGET_VTYPE})
endif()
+ get_target_property(TARGET_VDIST_OFFSET ${target} VERSION_DISTANCE_OFFSET)
+ if(TARGET_VDIST_OFFSET)
+ set(VERSION_DISTANCE_OFFSET ${TARGET_VDIST_OFFSET})
+ endif()
+ set(${prefix}_VERSION_DISTANCE_OFFSET ${VERSION_DISTANCE_OFFSET})
include(ArchiveVersionInfo_${prefix} OPTIONAL RESULT_VARIABLE ARCHIVE_VERSION_PRESENT)
if(ARCHIVE_VERSION_PRESENT AND ${prefix}_VERSION_SUCCESS)
message(STATUS "Info: Version information from archive file.")
+ set(${prefix}_VERSION_BUILDTYPE "${CMAKE_BUILD_TYPE}")
+ set(${prefix}_VERSION_BUILDTYPE "${CMAKE_BUILD_TYPE}" PARENT_SCOPE)
else()
get_version_info(${prefix} "${directory}")
if("${${prefix}_VERSION_FULLHASH}" STREQUAL "unknown"
@@ -462,6 +483,7 @@ function(add_version_info_custom_prefix target prefix directory)
set(VERSION_STRING ${${prefix}_VERSION_STRING})
set(VERSION_STRING_FULL ${${prefix}_VERSION_STRING_FULL})
set(VERSION_ISDIRTY ${${prefix}_VERSION_ISDIRTY})
+ set(VERSION_BUILDTYPE ${${prefix}_VERSION_BUILDTYPE})
set(VERSION_BRANCH ${${prefix}_VERSION_BRANCH})
set(VERSION_DATE_MONTH_YEAR ${${prefix}_VERSION_DATE_MONTH_YEAR})
@@ -481,6 +503,7 @@ function(add_version_info_custom_prefix target prefix directory)
VERSION_STRING "${VERSION_STRING}"
VERSION_STRING_FULL "${VERSION_STRING_FULL}"
VERSION_ISDIRTY "${VERSION_ISDIRTY}"
+ VERSION_BUILDTYPE "${VERSION_BUILDTYPE}"
VERSION_BRANCH "${VERSION_BRANCH}"
VERSION_DATE_MONTH_YEAR "${VERSION_DATE_MONTH_YEAR}"
)
diff --git a/cmake/modules/GitVersion.h.in b/cmake/modules/GitVersion.h.in
index 16b7431d..400d25c4 100644
--- a/cmake/modules/GitVersion.h.in
+++ b/cmake/modules/GitVersion.h.in
@@ -11,4 +11,5 @@ namespace @TARGET@ {
const char* version_fullhash();
bool version_isdirty();
const char* version_branch();
-}
+ const char* version_buildtype();
+} // end namespace @TARGET@
diff --git a/cmake/modules/LinuxDistributionInfo.cmake b/cmake/modules/LinuxDistributionInfo.cmake
index 4663c370..5b9df22b 100644
--- a/cmake/modules/LinuxDistributionInfo.cmake
+++ b/cmake/modules/LinuxDistributionInfo.cmake
@@ -1,5 +1,5 @@
# This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md
-cmake_minimum_required(VERSION 3.0)
+cmake_minimum_required(VERSION 3.6)
# Try to get the Linux distribution and version as a string (host system)
# When cross compiling this function won't work to get the target distribution.
diff --git a/cmake/modules/LinuxPackaging.cmake b/cmake/modules/LinuxPackaging.cmake
index 75aff50b..29bd9e60 100644
--- a/cmake/modules/LinuxPackaging.cmake
+++ b/cmake/modules/LinuxPackaging.cmake
@@ -1,5 +1,5 @@
# This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md
-cmake_minimum_required(VERSION 3.0)
+cmake_minimum_required(VERSION 3.6)
include(LinuxDistributionInfo)
set(_LinuxPackaging_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}")
diff --git a/cmake/modules/LinuxPkgCPackConfig.cmake.in b/cmake/modules/LinuxPkgCPackConfig.cmake.in
index e38224f2..4f567623 100644
--- a/cmake/modules/LinuxPkgCPackConfig.cmake.in
+++ b/cmake/modules/LinuxPkgCPackConfig.cmake.in
@@ -43,6 +43,7 @@ set(CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION
# Other settings
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "@PKG_HOMEPAGE@")
set(CPACK_DEBIAN_PACKAGE_SECTION "@PKG_DEBIAN_SECTION@")
+set(CPACK_DEBIAN_COMPRESSION_TYPE xz)
# Set requires/depends
set(CPACK_RPM_PACKAGE_REQUIRES "@PKG_DEPENDENCIES@")
diff --git a/cmake/modules/Translation.cmake b/cmake/modules/Translation.cmake
index ca199ca0..5eccc7c6 100644
--- a/cmake/modules/Translation.cmake
+++ b/cmake/modules/Translation.cmake
@@ -98,6 +98,7 @@ function(add_translation_update_task _prefix _input_dirs _output_dir _languages)
ARGS ${_input_dirs}
ARGS -locations relative
ARGS -ts
+ ARGS -noobsolete
ARGS ${_tsfiles_lupdate}
WORKING_DIRECTORY ${_output_dir}
COMMENT "Updating translations (${_prefix})..."
diff --git a/cmake/templates/projecteur.bash-completion b/cmake/templates/projecteur.bash-completion
index 111e87b2..718ced85 100644
--- a/cmake/templates/projecteur.bash-completion
+++ b/cmake/templates/projecteur.bash-completion
@@ -20,7 +20,7 @@ _projecteur()
case "$prev" in
"-c")
# Auto completion for commands and properties
- local commands="quit spot= settings= preset="
+ local commands="quit spot= spot.size.adjust= settings= preset= vibrate="
commands="${commands} spot.size= spot.rotation= spot.shape= spot.shape.square.radius="
commands="${commands} spot.multi-screen= spot.overlay="
commands="${commands} spot.shape.star.points= spot.shape.star.innerradius= spot.shape.ngon.sides="
diff --git a/cmake/templates/Projecteur.desktop.in b/cmake/templates/projecteur.desktop.in
similarity index 100%
rename from cmake/templates/Projecteur.desktop.in
rename to cmake/templates/projecteur.desktop.in
diff --git a/devices.conf b/devices.conf
index bd0d0c3b..fb3a1a08 100644
--- a/devices.conf
+++ b/devices.conf
@@ -10,3 +10,5 @@
0x2571, 0x4109, usb, AVATTO i10 Pro
0x17ef, 0x60d9, usb, Lenovo ThinkPad X1 Presenter Mouse
0x17ef, 0x60db, bt, Lenovo ThinkPad X1 Presenter Mouse
+0x69a7, 0x9803, usb, August LP310
+0x3243, 0x0122, usb, Norwii Wireless Presenter
diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md
index 32a93816..f7f2cff2 100644
--- a/doc/CHANGELOG.md
+++ b/doc/CHANGELOG.md
@@ -1,8 +1,31 @@
-# Changelog
+# Projecteur Changelog
+
+## v0.10.0
+
+### Changes/Updates:
+
+- Logitech Spotlight Bluetooth vibration & hidraw support ([#140][p140]);
+- Logitech Spotlight Scrolling and Audio Volume functionality ([#85][i85]);
+- Add automated builds for Fedora 34, Debian 11 (Bullseye) and OpenSUSE 15.3 ([#148][i148])
+- Add automated builds for Fedora 37 and 38 / OpenSUSE 15.4 and 15.5
+- Add automated builds for Ubuntu 23.04 and Debian Bookworm
+- Bug fix for crash when closing the about dialog.
+- Add adjust spot size command ([#209][i209])
+- Add vibrate for the command line ([#202][i202])
+
+Many thanks to *[@mayanksuman][c-mayanksuman]* for Logitech Bluetooth, Scrolling
+and Audio volume support.
+
+[p140]: https://github.com/jahnf/Projecteur/pull/140
+[i85]: https://github.com/jahnf/Projecteur/issues/85
+[i148]: https://github.com/jahnf/Projecteur/issues/148
+[i209]: https://github.com/jahnf/Projecteur/issues/209
+[i202]: https://github.com/jahnf/Projecteur/issues/202
+[c-mayanksuman]: https://github.com/mayanksuman
## v0.9.2
-### Fixes:
+### Changes/Updates:
- Bug fix for high CPU load in certain situations ([#133][i133])
- Bug fix for wrong button mapping for inputs with same length ([#144][i144])
@@ -12,7 +35,7 @@
## v0.9.1
-### Fixes:
+### Changes/Updates:
- Fixes for automatically generated RPM Packages (especially Fedora)
- Fixes for version numbers in generated packages (DEB and RPM)
@@ -21,7 +44,7 @@
### Changes/Updates:
-- Added man pages and Appstream files - thanks to @llimeht ([#97][p97]);
+- Added man pages and Appstream files - thanks to *[@llimeht][c-llimeht]* ([#97][p97]);
- Command line option to toggle the spotlight ([#104][i104]);
- Bugfix when moving the cursor from one screen to a different screen with higher resolution;
- Multi-screen overlay option ([#80][i80]);
@@ -39,6 +62,7 @@
[p115]: https://github.com/jahnf/Projecteur/pull/115
[p113]: https://github.com/jahnf/Projecteur/pull/113
[i6]: https://github.com/jahnf/Projecteur/issues/6
+[c-llimeht]: https://github.com/llimeht
## v0.8
diff --git a/doc/LinuxRepositories.md b/doc/LinuxRepositories.md
index 4b6cf5ca..def5ca00 100644
--- a/doc/LinuxRepositories.md
+++ b/doc/LinuxRepositories.md
@@ -15,7 +15,7 @@ for all available `projecteur` packages in Debian.
### Ubuntu
-Thanks to debian packages, _Projecteur_ is availabed in the official Ubuntu repositories
+Thanks to debian packages, _Projecteur_ is available in the official Ubuntu repositories
from Ubuntu 20.10 on. See: https://packages.ubuntu.com/search?keywords=projecteur&searchon=names
### Gentoo Linux
@@ -43,12 +43,12 @@ and are accessible as a Linux repository for different distributions.
See also:
* https://cloudsmith.io/~jahnf/repos/projecteur-develop/setup/#formats-deb
* https://cloudsmith.io/~jahnf/repos/projecteur-develop/setup/#formats-rpm
-
+
[](https://cloudsmith.com)
#### Debian Stretch
-```
+```sh
apt-get install -y debian-keyring
apt-get install -y debian-archive-keyring
apt-get install -y apt-transport-https
@@ -59,7 +59,7 @@ apt-get update
#### Debian Buster
-```
+```sh
apt-get install -y debian-keyring
apt-get install -y debian-archive-keyring
apt-get install -y apt-transport-https
@@ -68,9 +68,31 @@ curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/c
apt-get update
```
-#### Ubuntu 18.04
+#### Debian Bullseye
+```sh
+apt-get install -y debian-keyring
+apt-get install -y debian-archive-keyring
+apt-get install -y apt-transport-https
+curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' | apt-key add -
+curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.deb.txt?distro=debian&codename=bullseye' > /etc/apt/sources.list.d/jahnf-projecteur-develop.list
+apt-get update
```
+
+#### Debian Bookworm
+
+```sh
+apt-get install -y debian-keyring
+apt-get install -y debian-archive-keyring
+apt-get install -y apt-transport-https
+curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' | apt-key add -
+curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.deb.txt?distro=debian&codename=bookworm' > /etc/apt/sources.list.d/jahnf-projecteur-develop.list
+apt-get update
+```
+
+#### Ubuntu 18.04
+
+```sh
apt-get install -y apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' | apt-key add -
curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.deb.txt?distro=ubuntu&codename=bionic' > /etc/apt/sources.list.d/jahnf-projecteur-develop.list
@@ -79,16 +101,34 @@ apt-get update
#### Ubuntu 20.04
-```
+```sh
apt-get install -y apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' | apt-key add -
curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.deb.txt?distro=ubuntu&codename=focal' > /etc/apt/sources.list.d/jahnf-projecteur-develop.list
apt-get update
```
-#### OpenSuse 15.1
+#### Ubuntu 22.04
+
+```sh
+apt-get install -y apt-transport-https
+curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' | apt-key add -
+curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.deb.txt?distro=ubuntu&codename=jammy' > /etc/apt/sources.list.d/jahnf-projecteur-develop.list
+apt-get update
+```
+
+#### Ubuntu 23.04
+```sh
+apt-get install -y apt-transport-https
+curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' | apt-key add -
+curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.deb.txt?distro=ubuntu&codename=lunar' > /etc/apt/sources.list.d/jahnf-projecteur-develop.list
+apt-get update
```
+
+#### OpenSuse 15.1
+
+```sh
curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=opensuse&codename=15.1' > /tmp/jahnf-projecteur-develop.repo
zypper ar -f '/tmp/jahnf-projecteur-develop.repo'
zypper --gpg-auto-import-keys refresh jahnf-projecteur-develop jahnf-projecteur-develop-source
@@ -96,15 +136,39 @@ zypper --gpg-auto-import-keys refresh jahnf-projecteur-develop jahnf-projecteur-
#### OpenSuse 15.2
-```
+```sh
curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=opensuse&codename=15.2' > /tmp/jahnf-projecteur-develop.repo
zypper ar -f '/tmp/jahnf-projecteur-develop.repo'
zypper --gpg-auto-import-keys refresh jahnf-projecteur-develop jahnf-projecteur-develop-source
```
+#### OpenSuse 15.3
+
+```sh
+curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=opensuse&codename=15.3' > /tmp/jahnf-projecteur-develop.repo
+zypper ar -f '/tmp/jahnf-projecteur-develop.repo'
+zypper --gpg-auto-import-keys refresh jahnf-projecteur-develop jahnf-projecteur-develop-source
+```
+
+#### OpenSuse 15.4
+
+```sh
+curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=opensuse&codename=15.4' > /tmp/jahnf-projecteur-develop.repo
+zypper ar -f '/tmp/jahnf-projecteur-develop.repo'
+zypper --gpg-auto-import-keys refresh jahnf-projecteur-develop jahnf-projecteur-develop-source
+```
+
+#### OpenSuse 15.5
+
+```sh
+curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=opensuse&codename=15.5' > /tmp/jahnf-projecteur-develop.repo
+zypper ar -f '/tmp/jahnf-projecteur-develop.repo'
+zypper --gpg-auto-import-keys refresh jahnf-projecteur-develop jahnf-projecteur-develop-source
+```
+
#### Fedora 31
- ```
+ ```sh
dnf install yum-utils pygpgme
rpm --import 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key'
curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=fedora&codename=31' > /tmp/jahnf-projecteur-develop.repo
@@ -114,7 +178,7 @@ dnf -q makecache -y --disablerepo='*' --enablerepo='jahnf-projecteur-develop' --
#### Fedora 32
-```
+```sh
dnf install yum-utils pygpgme
rpm --import 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key'
curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=fedora&codename=32' > /tmp/jahnf-projecteur-develop.repo
@@ -124,7 +188,7 @@ dnf -q makecache -y --disablerepo='*' --enablerepo='jahnf-projecteur-develop' --
#### Fedora 33
-```
+```sh
dnf install yum-utils pygpgme
rpm --import 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key'
curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=fedora&codename=33' > /tmp/jahnf-projecteur-develop.repo
@@ -132,9 +196,39 @@ dnf config-manager --add-repo '/tmp/jahnf-projecteur-develop.repo'
dnf -q makecache -y --disablerepo='*' --enablerepo='jahnf-projecteur-develop' --enablerepo='jahnf-projecteur-develop-source'
```
-#### CentOS 8
+#### Fedora 34
+```sh
+dnf install yum-utils pygpgme
+rpm --import 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key'
+curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=fedora&codename=34' > /tmp/jahnf-projecteur-develop.repo
+dnf config-manager --add-repo '/tmp/jahnf-projecteur-develop.repo'
+dnf -q makecache -y --disablerepo='*' --enablerepo='jahnf-projecteur-develop' --enablerepo='jahnf-projecteur-develop-source'
+```
+
+#### Fedora 37
+
+```sh
+dnf install yum-utils pygpgme
+rpm --import 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key'
+curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=fedora&codename=37' > /tmp/jahnf-projecteur-develop.repo
+dnf config-manager --add-repo '/tmp/jahnf-projecteur-develop.repo'
+dnf -q makecache -y --disablerepo='*' --enablerepo='jahnf-projecteur-develop' --enablerepo='jahnf-projecteur-develop-source'
+```
+
+#### Fedora 38
+
+```sh
+dnf install yum-utils pygpgme
+rpm --import 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key'
+curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=fedora&codename=38' > /tmp/jahnf-projecteur-develop.repo
+dnf config-manager --add-repo '/tmp/jahnf-projecteur-develop.repo'
+dnf -q makecache -y --disablerepo='*' --enablerepo='jahnf-projecteur-develop' --enablerepo='jahnf-projecteur-develop-source'
```
+
+#### CentOS 8
+
+```sh
yum install yum-utils pygpgme
rpm --import 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key'
curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.rpm.txt?distro=el&codename=8' > /tmp/jahnf-projecteur-develop.repo
diff --git a/doc/LogitechSpotlightHID++.md b/doc/LogitechSpotlightHID++.md
new file mode 100644
index 00000000..3159cee9
--- /dev/null
+++ b/doc/LogitechSpotlightHID++.md
@@ -0,0 +1,299 @@
+# Logitech Spotlight HID++
+
+## HID++ Basics
+
+There are two major version of Logitech HID++ protocol. The Logitech spotlight
+supports HID++ protocol version 2.0+. This document provide information about
+HID++ version 2.0+ only. In the HID++ protocol two different types of messages
+can be used for communication to the Logitech Spotlight device.
+These two types of message are
+
+ 1. Short Message: 7 bytes long. Default message scheme for USB Dongle. The
+ Spotlight device only supports short messages, when it is connected through
+ the USB dongle.
+
+ * First Byte: `0x10`
+
+ * Second Byte: Device code for which the message is meant (in case it
+ is sent from PC)/originated. `0xff` for USB dongle, `0x01` for
+ Logitech Spotlight device.
+
+ * Third Byte: Feature Index. Some of the featureIndex are `0x00` (for
+ Root feature: used for querying device details),
+ `0x80` (short set), `0x81` (short get).
+
+ * Forth Byte: If third byte is not `0x80` or `0x81` then last 4 bits
+ (`forth_byte & 0xf0`) are function code and first 4 bits
+ (`forth_byte & 0x0f`) are software identification code.
+ Software identification code is random value in range of 0 to 15
+ (used to differentiate traffic for different softwares).
+
+ * Fifth - Seventh Bytes: Parameters/data
+
+ 2. Long Message: 20 bytes long. Logitech Spotlight supports long messages in
+ any connection mode (through USB dongle and Bluetooth). In long messages, the
+ first byte is `0x11`, the next three bytes (second byte to forth byte) are the
+ same as in short messages. However, in long messages, there are additional
+ bytes (Fifth - Twentieth bytes) that can be used as parameters/data.
+
+Please note that in device response the first four bytes will be the same as
+in the request message, if no error is reported. However, in case of an error,
+the third byte in the device response will be `0x8f` and first, second, forth
+and fifth byte in the device response will be same as the first, second, third
+and forth byte respectively as in the request message from the application.
+
+If the Spotlight device is connected through Bluetooth then a short HID++
+message meant for device should be transformed to a long HID++ message before
+sending it to device. For changing a short message to a long message, the first
+byte is replaced as `0x11` and the message is appended with trailing zero to
+achieve the length of 20.
+
+## HID++ Feature Code
+
+HID++ feature codes (of type `uint16_t`; 2^16 possible feature codes) are
+defined for a set of all the possible features supported by any Logitech HID++
+device produced up until today. The feature code is part of the HID++ protocol
+and does not vary for different devices. Some of the well known HID++2 feature
+codes are:
+
+| Feature Code Name | Byte Value |
+| -------------------------- | ------------:|
+| `ROOT` | `0x0000` |
+| `FEATURE_SET` | `0x0001` |
+| `FEATURE_INFO` | `0x0002` |
+| `DEVICE_FW_VERSION` | `0x0003` |
+| `DEVICE_UNIT_ID` | `0x0004` |
+| `DEVICE_NAME` | `0x0005` |
+| `DEVICE_GROUPS` | `0x0006` |
+| `DEVICE_FRIENDLY_NAME` | `0x0007` |
+| `KEEP_ALIVE` | `0x0008` |
+| `RESET` | `0x0020` |
+| `CRYPTO_ID` | `0x0021` |
+| `TARGET_SOFTWARE` | `0x0030` |
+| `WIRELESS_SIGNAL_STRENGTH` | `0x0080` |
+| `DFUCONTROL_LEGACY` | `0x00C0` |
+| `DFUCONTROL_UNSIGNED` | `0x00C1` |
+| `DFUCONTROL_SIGNED` | `0x00C2` |
+| `DFU` | `0x00D0` |
+
+A more extensive list of known feature codes are documented by the
+[Solaar project](https://github.com/pwr-Solaar/Solaar/blob/master/docs/features.md).
+Some of the feature codes relevant for the Logitech Spotlight are defined in
+[hidpp.h](../src/hidpp.h).
+
+```c++
+enum class FeatureCode : uint16_t {
+ Root = 0x0000,
+ FeatureSet = 0x0001,
+ FirmwareVersion = 0x0003,
+ DeviceName = 0x0005,
+ Reset = 0x0020,
+ DFUControlSigned = 0x00c2,
+ BatteryStatus = 0x1000,
+ PresenterControl = 0x1a00,
+ Sensor3D = 0x1a01,
+ ReprogramControlsV4 = 0x1b04,
+ WirelessDeviceStatus = 0x1db4,
+ SwapCancelButton = 0x2005,
+ PointerSpeed = 0x2205,
+};
+```
+
+No single Logitech device supports all feature codes. Rather, a device supports
+a limited range of features and corresponding feature codes. Inside the device,
+the supported feature codes are mapped to an index (or Feature Index). This
+mapping is called FeatureSet table. For any device, the Root Feature Code (`0x0000`)
+has an index of `0x00`. Root Feature Index (`0x00`) is used for getting the
+entire FeatureSet, and pinging the device.
+
+For any device, the feature index corresponding to any feature code can be
+obtained by using Root Feature Index (`0x00`) by sending the request message
+`{0x10, 0x01, 0x00, 0x0d, Feature_Code(2 bytes), 0x00}` (here, the function
+code is `0x00` and the software identification code is `0x0d` in forth byte).
+If the return message is not an error message, the fifth byte in the response
+is the Feature index and the 6th byte is the Feature type (See below).
+
+The application can retrieve the entire FeatureSet table for the device with
+following steps:
+
+ 1. Get the number of features supported by device:
+ * Get the _Feature Index_ corresponding to the FeatureSet code (`0x0001`).
+ * Get the number of features supported by sending the request message
+ `{0x10, 0x01, (FeatureSet Index), 0x0d, 0x00, 0x00, 0x00}`
+ (3rd byte is the Feature Index for FeatureSet Code; function code
+ is `0x00` and software identification code is `0x0d` in forth byte).
+ In the response, the 5th byte will be the number of features
+ supported, except the root feature. As stated above, Root feature
+ always has the Feature Index `0x00`. Hence, total number of features
+ supported is one plus the count obtained in the response.
+
+ 2. Iterate over the Feature Indexes 1 to the number of features supported and
+ send the request (assuming Feature_Index for feature set is `0x01`; third byte)
+ `{0x10, 0x01, 0x01, 0x1d, Feature_Index, 0x00, 0x00}` (function code is `0x10`
+ and software identification code is `0x0d` in forth byte). The response will
+ contain the HID++ Feature Code at byte 5 and 6 as `uint16_t` and the Feature
+ Type at byte 7 for a valid Feature Index. In the Feature Type byte,
+ if 7th bit is set this means _Software Hidden_, if bit 8 is set this means
+ _Obsolete feature_. So, Software_Hidden = (`Feature_Type & (1<<6)`) and
+ Obsolete_Feature = (`Feature_Type & (1<<7)`).
+ Software Hidden or Obsolete features should not be handled by any application.
+ In case the Feature Index is not valid (i.e.,feature index > number of
+ feature supported) then `0x0200` will be in the response at byte 5 and 6.
+
+The FeatureSet table for a device may change with a firmware update. The
+application should cache FeatureSet table along with Firmware version and only
+read FeatureSet table again if the firmware version has changed. This logic for
+getting FeatureSet table from device is implemented in
+`initFromDevice` method in `FeatureSet` class in [hidpp.h](../src/hidpp.h).
+
+## Resetting Logitech Spotlight device
+
+Depending on the connection mode (USB dongle or Bluetooth), the Logitech
+Spotlight device can be reset with following HID++ message from the application:
+
+ 1. Reset the USB dongle by sending following commands in sequence
+
+ ```json
+ {0x10, 0xff, 0x81, 0x00, 0x00, 0x00, 0x00} // get wireless notification and software connection status
+ {0x10, 0xff, 0x80, 0x00, 0x00, 0x01, 0x00} // set sofware bit to false
+ {0x10, 0xff, 0x80, 0x02, 0x02, 0x00, 0x00} // initialize the USB dongle
+ {0x10, 0xff, 0x80, 0x00, 0x00, 0x09, 0x00} // set sofware bit to true
+ ```
+
+ 2. Load the FeatureSet table for the device (from pre-existing cache or from
+ the device if firmware version has changed by calling `initFromDevice`
+ method in `FeatureSet` class in [hidpp.h](../src/hidpp.h)).
+
+ 3. Reset the Spotlight device with the Feature index for Reset Feature Code
+ from the FeatureSet table. If the Feature Index for Reset Feature Code is
+ `0x05`, then HID++ request message for resetting will be
+ `{0x10, 0x01, 0x05, 0x1d, 0x00, 0x00, 0x00}`.
+
+In addition to these steps, the Projecteur also pings the device by sending
+`{0x10, 0x01, 0x00, 0x1d, 0x00, 0x00, 0x5d}` (function code `0x10` and software
+identification code `0x0d` in forth byte; the last byte is a random value that
+will returned back on 7th byte in the response message). The response to this
+ping contains the HID++ version (`fifth_byte + sixth_byte/10.0`) supported by
+the device.
+
+Further, Projecteur configures the Logitech device to send `Next Hold` and
+`Back Hold` events and resets the pointer speed to a default value with
+following HID++ commands:
+
+```json
+// enable next button hold (0x07 - Feature Index for ReprogramControlsV4 Feature Code, 0xda - next button, 0x33 - hold event)
+{0x11, 0x01, 0x07, 0x3d, 0x00, 0xda, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
+// back button hold (0x07 - Feature Index for ReprogramControlsV4 Feature Code, 0xdc - back button, 0x33 - hold event)
+{0x11, 0x01, 0x07, 0x3d, 0x00, 0xdc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
+// Reset pointer Speed (0x0a - Feature Index for Reset Feature Code, 0x14 - 5th level pointer speeed (can be between 0x10-0x19))
+{0x10, 0x01, 0x0a, 0x1d, 0x14, 0x00, 0x00}
+```
+
+These initialization steps are implemented in `initReceiver` and `initPresenter`
+methods of `SubHidppConnection` class in [device-hidpp.h](../src/device-hidpp.h).
+After reprogramming the Next and Back buttons, the spotlight device will send
+mouse movement data when either of these button are long-pressed and device is
+moved. The processing of these events are discussed in the
+[following section](#response-to-next-hold-and-back-hold-keys).
+
+For completeness, it should be noted that the official Logitech Spotlight
+software reprogram the click and double click events too by following HID++
+commands:
+
+```json
+// Send click event as HID++ message (0x07 - Feature Index for ReprogramControlsV4 Feature Code, 0xd8-click button, 0x33- hold event)
+{0x11, 0x01, 0x07, 0x3d, 0x00, 0xd8, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
+// Send double click as HID++ message (0x07 - Feature Index for ReprogramControlsV4 Feature Code, 0xdf - double click)
+{0x11, 0x01, 0x07, 0x3d, 0x00, 0xdf, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
+```
+
+Projecteur does not send these packages. Instead, it grab the events from mouse
+event device associated with Spotlight. The approach taken by Projecteur is
+advantageous as it help in implementing Input Mapping feature that official
+Logitech Software lacks. However, this approach also makes porting Projecteur
+to different platforms more difficult.
+
+## Important HID++ commands for Spotlight device
+
+### Wireless Notification on Activation/Deactivation of Spotlight Device
+
+The Spotlight device sends a wireless notification if it gets activated.
+Wireless notification will be short HID++ message if the Spotlight device is
+connected through USB. Otherwise, it will be a long HID++ message.
+
+For short HID++ wireless notifications, the third byte will be `0x41`. In this
+message, the 6th bit in 5th byte shows the activation status of spotlight
+device. If the 6th bit is 0 then device just got active, otherwise the device
+just got deactivated.
+
+A long HID++ wireless notification is only received for device activation.
+In this message, third byte will be the Feature Index for the Wireless
+Notification Feature Code (`0x1db4`).
+
+### Vibration support
+
+The spotlight device can vibrate if the HID++ message
+`{0x10, 0x01, (Feature Index for Presenter Control Feature Code), 0x1d, length, 0xe8, intensity}`
+is sent to it. In the message, length can range between `0x00` to `0x0a`.
+
+### Battery Status
+
+Battery status can be requested by sending request command
+`{0x10, 0x01, 0x06, 0x0d, 0x00, 0x00, 0x00}` (assuming the Feature Index for
+Battery Status Feature Code (`0x1000`) is `0x06`; function code is `0x00` and
+software identification code is `0x0d`). In the response, the fifth byte shows
+current battery level in percent, sixth byte shows the next reported battery
+level in percent (device do not report continuous battery level) and the seventh
+byte shows the state of battery with following possible values.
+
+```cpp
+enum class BatteryStatus : uint8_t {Discharging = 0x00,
+ Charging = 0x01,
+ AlmostFull = 0x02,
+ Full = 0x03,
+ SlowCharging = 0x04,
+ InvalidBattery = 0x05,
+ ThermalError = 0x06,
+ ChargingError = 0x07
+ };
+```
+
+## Processing of device response
+
+All of the HID++ commands listed above result in response messages from the
+Spotlight device. For most messages, these responses from device are just the
+acknowledgements of the HID++ commands sent by the application. However, some
+responses from the Spotlight device contain useful information. These responses
+are processed in the `onHidppDataAvailable` method in the `SubHidppConnection`
+class in [device-hidpp.h](../src/device-hidpp.h). Description of HID++ messages
+from device to reprogrammed keys (`Next Hold` and `Back Hold`) are provided in
+following sub-section:
+
+### Response to `Next Hold` and `Back Hold` keys
+
+The first HID++ message sent by Spotlight device at the start and end of any
+hold event is `{0x11, 0x01, 0x07, 0x00, (button_code), ...followed by zeroes ....}`
+(assuming `0x07` is the Feature Index for ReprogramControlsV4 Feature Code (`0x1b04`)).
+When the hold event starts, button_code will be `0xda` for start of `Next Hold`
+event and `0xdc` for start of `Back Hold` event. When the button is released
+the HID++ message received have `0x00` as button_code for both cases.
+
+During the Hold event, the Spotlight device sends the relative mouse movement
+data as HID++ messages. These messages are of form
+`{0x11, 0x01, 0x07, 0x10, (mouse data in 4 bytes), ...followed by zeroes ....}`
+(assuming `0x07` is the Feature Index for ReprogramControlsV4 Feature Code (`0x1b04`)).
+In the four bytes (for mouse data), the second and last bytes are relative `x`
+and `y` values. These relative `x` and `y` values are used for Scrolling and
+Volume Control Actions in Projecteur.
+
+The relevant functions for processing `Next Hold` and `Back Hold` are provided
+in `registerForNotifications` method in the `Spotlight` class
+([spotlight.h](../src/spotlight.h)).
+
+## Further information
+
+For more information about HID++ protocol in general please check
+[logitech-hidpp module](https://github.com/torvalds/linux/blob/master/drivers/hid/hid-logitech-hidpp.c)
+code in linux kernel source. Documentation from
+[Solaar project](https://github.com/pwr-Solaar/Solaar/blob/master/docs/)
+might be helpful too.
diff --git a/doc/screenshot-button-mapping.png b/doc/screenshot-button-mapping.png
index ee832ebf..63d600a2 100644
Binary files a/doc/screenshot-button-mapping.png and b/doc/screenshot-button-mapping.png differ
diff --git a/docker/Dockerfile.debian-bookworm b/docker/Dockerfile.debian-bookworm
new file mode 100644
index 00000000..705d8777
--- /dev/null
+++ b/docker/Dockerfile.debian-bookworm
@@ -0,0 +1,23 @@
+# Container for building the Projecteur package
+# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags
+
+FROM debian:bookworm
+
+RUN apt-get update && mkdir /build
+RUN DEBIAN_FRONTEND="noninteractive" \
+ apt-get install -y --no-install-recommends \
+ ca-certificates \
+ g++ \
+ make \
+ cmake \
+ udev \
+ git \
+ pkg-config \
+ qtdeclarative5-dev \
+ qttools5-dev-tools \
+ qttools5-dev \
+ libqt5x11extras5-dev \
+ libusb-1.0-0-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+RUN git config --global --add safe.directory /source
diff --git a/docker/Dockerfile.debian-stretch b/docker/Dockerfile.debian-stretch
index b1e9f873..6a171116 100644
--- a/docker/Dockerfile.debian-stretch
+++ b/docker/Dockerfile.debian-stretch
@@ -23,3 +23,18 @@ RUN apt-get install -y --no-install-recommends \
RUN apt-get install -y --no-install-recommends \
libqt5x11extras5-dev \
libusb-1.0-0-dev
+
+RUN apt-get install -y --no-install-recommends \
+ libqt5x11extras5-dev \
+ libusb-1.0-0-dev
+
+RUN apt-get install -y --no-install-recommends \
+ wget
+
+# Install newer CMake version,
+# otherwise the package version in the debian package
+# created by the dist-package target will not be correct
+RUN wget https://github.com/Kitware/CMake/releases/download/v3.19.6/cmake-3.19.6-Linux-x86_64.sh && \
+ chmod +x cmake-3.19.6-Linux-x86_64.sh && \
+ ./cmake-3.19.6-Linux-x86_64.sh --skip-license --prefix=/usr && \
+ rm ./cmake-3.19.6-Linux-x86_64.sh
diff --git a/docker/Dockerfile.fedora-37 b/docker/Dockerfile.fedora-37
new file mode 100644
index 00000000..a926c031
--- /dev/null
+++ b/docker/Dockerfile.fedora-37
@@ -0,0 +1,21 @@
+# Container for building the Projecteur package
+# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags
+
+FROM fedora:37
+
+RUN mkdir /build
+RUN dnf -y install --setopt=install_weak_deps=False --best \
+ cmake \
+ udev \
+ gcc-c++ \
+ tar \
+ make \
+ git \
+ qt5-qtdeclarative-devel \
+ pkg-config \
+ rpm-build \
+ qt5-linguist \
+ qt5-qtx11extras-devel \
+ libusbx-devel
+
+RUN git config --global --add safe.directory /source
diff --git a/docker/Dockerfile.fedora-38 b/docker/Dockerfile.fedora-38
new file mode 100644
index 00000000..997e5d74
--- /dev/null
+++ b/docker/Dockerfile.fedora-38
@@ -0,0 +1,21 @@
+# Container for building the Projecteur package
+# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags
+
+FROM fedora:38
+
+RUN mkdir /build
+RUN dnf -y install --setopt=install_weak_deps=False --best \
+ cmake \
+ udev \
+ gcc-c++ \
+ tar \
+ make \
+ git \
+ qt5-qtdeclarative-devel \
+ pkg-config \
+ rpm-build \
+ qt5-linguist \
+ qt5-qtx11extras-devel \
+ libusbx-devel
+
+RUN git config --global --add safe.directory /source
diff --git a/docker/Dockerfile.opensuse-15.4 b/docker/Dockerfile.opensuse-15.4
new file mode 100644
index 00000000..7765cdaf
--- /dev/null
+++ b/docker/Dockerfile.opensuse-15.4
@@ -0,0 +1,23 @@
+# Container for building the Projecteur package
+# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags
+
+FROM opensuse/leap:15.4
+
+RUN mkdir /build
+RUN zypper --non-interactive in --no-recommends \
+ pkg-config \
+ udev \
+ gcc-c++ \
+ tar \
+ make \
+ cmake \
+ git \
+ wget \
+ libqt5-qtdeclarative-devel \
+ rpmbuild \
+ libqt5-linguist \
+ libqt5-qtx11extras-devel \
+ libusb-1_0-devel \
+ libQt5DBus-devel
+
+RUN git config --global --add safe.directory /source
diff --git a/docker/Dockerfile.opensuse-15.5 b/docker/Dockerfile.opensuse-15.5
new file mode 100644
index 00000000..a2f3a225
--- /dev/null
+++ b/docker/Dockerfile.opensuse-15.5
@@ -0,0 +1,23 @@
+# Container for building the Projecteur package
+# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags
+
+FROM opensuse/leap:15.5
+
+RUN mkdir /build
+RUN zypper --non-interactive in --no-recommends \
+ pkg-config \
+ udev \
+ gcc-c++ \
+ tar \
+ make \
+ cmake \
+ git \
+ wget \
+ libqt5-qtdeclarative-devel \
+ rpmbuild \
+ libqt5-linguist \
+ libqt5-qtx11extras-devel \
+ libusb-1_0-devel \
+ libQt5DBus-devel
+
+RUN git config --global --add safe.directory /source
diff --git a/docker/Dockerfile.ubuntu-18.10 b/docker/Dockerfile.ubuntu-18.10
deleted file mode 100644
index ea6660fd..00000000
--- a/docker/Dockerfile.ubuntu-18.10
+++ /dev/null
@@ -1,26 +0,0 @@
-# Container for building the Projecteur package
-# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags
-
-FROM ubuntu:18.10
-
-RUN apt-get update
-RUN apt-get install -y --no-install-recommends \
- ca-certificates
-
-RUN apt-get install -y --no-install-recommends \
- g++ \
- make \
- cmake \
- udev \
- git \
- pkg-config
-
-RUN apt-get install -y --no-install-recommends \
- qtdeclarative5-dev \
- qttools5-dev-tools \
- qt5-default
-
-RUN apt-get install -y --no-install-recommends \
- libqt5x11extras5-dev \
- libusb-1.0-0-dev
-
diff --git a/docker/Dockerfile.ubuntu-22.04 b/docker/Dockerfile.ubuntu-22.04
new file mode 100644
index 00000000..638840ad
--- /dev/null
+++ b/docker/Dockerfile.ubuntu-22.04
@@ -0,0 +1,23 @@
+# Container for building the Projecteur package
+# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags
+
+FROM ubuntu:22.04
+
+RUN apt-get update && mkdir /build
+RUN DEBIAN_FRONTEND="noninteractive" \
+ apt-get install -y --no-install-recommends \
+ ca-certificates \
+ g++ \
+ make \
+ cmake \
+ udev \
+ git \
+ pkg-config \
+ qtdeclarative5-dev \
+ qttools5-dev-tools \
+ qttools5-dev \
+ libqt5x11extras5-dev \
+ libusb-1.0-0-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+RUN git config --global --add safe.directory /source
diff --git a/docker/Dockerfile.ubuntu-23.04 b/docker/Dockerfile.ubuntu-23.04
new file mode 100644
index 00000000..5f0c20bf
--- /dev/null
+++ b/docker/Dockerfile.ubuntu-23.04
@@ -0,0 +1,23 @@
+# Container for building the Projecteur package
+# Images available at: https://hub.docker.com/r/jahnf/projecteur/tags
+
+FROM ubuntu:23.04
+
+RUN apt-get update && mkdir /build
+RUN DEBIAN_FRONTEND="noninteractive" \
+ apt-get install -y --no-install-recommends \
+ ca-certificates \
+ g++ \
+ make \
+ cmake \
+ udev \
+ git \
+ pkg-config \
+ qtdeclarative5-dev \
+ qttools5-dev-tools \
+ qttools5-dev \
+ libqt5x11extras5-dev \
+ libusb-1.0-0-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+RUN git config --global --add safe.directory /source
diff --git a/icons/icon-font/.fontcustom-manifest.json b/icons/icon-font/.fontcustom-manifest.json
index 8a1ff0eb..9dfcd559 100644
--- a/icons/icon-font/.fontcustom-manifest.json
+++ b/icons/icon-font/.fontcustom-manifest.json
@@ -1,14 +1,14 @@
{
"checksum": {
- "previous": "a9504014a04ad54718a75911f452551882ad71440ff3fac45f0dad3a5042a9ef",
- "current": "a9504014a04ad54718a75911f452551882ad71440ff3fac45f0dad3a5042a9ef"
+ "previous": "fdceee78baa623d59e66194333bacfa5e2256cd9a60fdbb02e780f8c59cfda1e",
+ "current": "fdceee78baa623d59e66194333bacfa5e2256cd9a60fdbb02e780f8c59cfda1e"
},
"fonts": [
- "output/fonts/projecteur-icons_a9504014a04ad54718a75911f4525518.ttf",
- "output/fonts/projecteur-icons_a9504014a04ad54718a75911f4525518.svg",
- "output/fonts/projecteur-icons_a9504014a04ad54718a75911f4525518.woff",
- "output/fonts/projecteur-icons_a9504014a04ad54718a75911f4525518.eot",
- "output/fonts/projecteur-icons_a9504014a04ad54718a75911f4525518.woff2"
+ "output/fonts/projecteur-icons_fdceee78baa623d59e66194333bacfa5.ttf",
+ "output/fonts/projecteur-icons_fdceee78baa623d59e66194333bacfa5.svg",
+ "output/fonts/projecteur-icons_fdceee78baa623d59e66194333bacfa5.woff",
+ "output/fonts/projecteur-icons_fdceee78baa623d59e66194333bacfa5.eot",
+ "output/fonts/projecteur-icons_fdceee78baa623d59e66194333bacfa5.woff2"
],
"glyphs": {
"iconmonstr-arrow-73": {
@@ -19,6 +19,10 @@
"codepoint": 61708,
"source": "svg/iconmonstr-arrow-74.svg"
},
+ "iconmonstr-audio-6": {
+ "codepoint": 61724,
+ "source": "svg/iconmonstr-audio-6.svg"
+ },
"iconmonstr-battery-3": {
"codepoint": 61696,
"source": "svg/iconmonstr-battery-3.svg"
@@ -47,6 +51,14 @@
"codepoint": 61701,
"source": "svg/iconmonstr-control-panel-9.svg"
},
+ "iconmonstr-cursor-21": {
+ "codepoint": 61721,
+ "source": "svg/iconmonstr-cursor-21.svg"
+ },
+ "iconmonstr-cursor-21-rotated": {
+ "codepoint": 61722,
+ "source": "svg/iconmonstr-cursor-21-rotated.svg"
+ },
"iconmonstr-gear-12": {
"codepoint": 61702,
"source": "svg/iconmonstr-gear-12.svg"
diff --git a/icons/icon-font/svg/iconmonstr-audio-6.svg b/icons/icon-font/svg/iconmonstr-audio-6.svg
new file mode 100644
index 00000000..1aed7b8c
--- /dev/null
+++ b/icons/icon-font/svg/iconmonstr-audio-6.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/icon-font/svg/iconmonstr-cursor-21-rotated.svg b/icons/icon-font/svg/iconmonstr-cursor-21-rotated.svg
new file mode 100644
index 00000000..658c78c5
--- /dev/null
+++ b/icons/icon-font/svg/iconmonstr-cursor-21-rotated.svg
@@ -0,0 +1,54 @@
+
+
diff --git a/icons/icon-font/svg/iconmonstr-cursor-21.svg b/icons/icon-font/svg/iconmonstr-cursor-21.svg
new file mode 100644
index 00000000..5d62fc0c
--- /dev/null
+++ b/icons/icon-font/svg/iconmonstr-cursor-21.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/icon-font/templates/projecteur-icons-def.h b/icons/icon-font/templates/projecteur-icons-def.h
index ed7986c1..abb2f78b 100644
--- a/icons/icon-font/templates/projecteur-icons-def.h
+++ b/icons/icon-font/templates/projecteur-icons-def.h
@@ -1,4 +1,5 @@
-// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
#pragma once
// Auto generated defines for icon-font with `fontcustom`
@@ -6,10 +7,10 @@
namespace Font
{
enum Icon {
-<% @glyphs.each do |key, value|
- name = key.to_s.delete_prefix("iconmonstr-")
+<% @glyphs.each do |key, value|
+ name = key.to_s.delete_prefix("iconmonstr-")
name = name.gsub(/^[0-9]|[^A-Za-z0-9]/, '_')
%><%= " #{name} = 0x#{value[:codepoint].to_s(16)}, // #{value[:source]}" %>
-<% end
+<% end
%> };
}
diff --git a/icons/projecteur-icons.ttf b/icons/projecteur-icons.ttf
index e37116c9..90d45a70 100644
Binary files a/icons/projecteur-icons.ttf and b/icons/projecteur-icons.ttf differ
diff --git a/qml/main-qt6.qml b/qml/main-qt6.qml
new file mode 100644
index 00000000..085d0d1f
--- /dev/null
+++ b/qml/main-qt6.qml
@@ -0,0 +1,213 @@
+// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md
+import QtQuick 2.3
+import QtQuick.Window 2.2
+
+import Qt5Compat.GraphicalEffects
+
+import Projecteur.Utils 1.0 as Utils
+
+Window {
+ id: mainWindow
+ property var screenId: -1
+ readonly property bool spotOnCurrentWindow: ProjecteurApp.currentSpotScreen === screenId
+ property alias desktopPixmap: desktopImage.pixmap
+
+ width: 300; height: 200
+
+ flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.SplashScreen
+
+ color: "transparent"
+
+ readonly property double diagonal: Math.sqrt(Math.pow(Math.max(width, height),2)*2)
+
+ Item {
+ id: rotationItem
+ anchors.centerIn: parent
+ width: rotation === 0 ? mainWindow.width : mainWindow.diagonal;
+ height: rotation === 0 ? mainWindow.height : width
+ rotation: Settings.spotRotationAllowed ? Settings.spotRotation : 0
+
+ opacity: ProjecteurApp.overlayVisible ? 1.0 : 0.0
+ Behavior on opacity { PropertyAnimation { easing.type: Easing.OutQuad } }
+
+ Item {
+ id: desktopItem
+ anchors.centerIn: centerRect
+ visible: false; enabled: false; clip: true
+ scale: Settings.zoomFactor
+ width: centerRect.width / scale; height: centerRect.height / scale
+
+ Utils.Image {
+ id: desktopImage
+ smooth: rotation == 0 ? false : true
+ rotation: -rotationItem.rotation
+ readonly property real xOffset: Math.floor(parent.width/2.0 + ((rotationItem.width-mainWindow.width)/2))
+ readonly property real yOffset: Math.floor(parent.height/2.0 + ((rotationItem.height-mainWindow.height)/2))
+ x: -ma.mouseX + xOffset
+ y: -ma.mouseY + yOffset
+ width: mainWindow.width; height: mainWindow.height
+ }
+ }
+
+ OpacityMask {
+ visible: Settings.zoomEnabled && mainWindow.spotOnCurrentWindow
+ cached: true
+ anchors.fill: centerRect
+ source: desktopItem
+ maskSource: spotShapeLoader.item
+ enabled: false
+ }
+
+ Item {
+ anchors.fill: parent
+ MouseArea {
+ id: ma
+
+ readonly property bool calculateMapping: Settings.multiScreenOverlayEnabled && !mainWindow.spotOnCurrentWindow
+ readonly property point globalPos: calculateMapping ? ProjecteurApp.currentCursorPos : Qt.point(0,0)
+ readonly property point mappedPos: calculateMapping ? mainWindow.contentItem.mapFromGlobal(globalPos.x, globalPos.y) : globalPos
+ readonly property int posX: spotOnCurrentWindow ? mouseX : mappedPos.x
+ readonly property int posY: spotOnCurrentWindow ? mouseY : mappedPos.y
+
+ cursorShape: Settings.cursor
+ anchors.fill: parent
+ hoverEnabled: true
+ onClicked: { ProjecteurApp.spotlightWindowClicked() }
+ onExited: { ProjecteurApp.cursorExitedWindow() }
+ onEntered: { ProjecteurApp.cursorEntered(screenId) }
+ onPositionChanged: (mouse) => {
+
+ if (Settings.multiScreenOverlayEnabled) {
+ ProjecteurApp.cursorPositionChanged(
+ mainWindow.contentItem.mapToGlobal(mouse.x, mouse.y))
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ property int spotSize: (mainWindow.height / 100.0) * Settings.spotSize
+ id: centerRect
+ opacity: Settings.shadeOpacity
+ height: spotSize > 50 ? Math.min(spotSize, mainWindow.height) : 50
+ width: height
+ x: ma.posX - width/2
+ y: ma.posY - height/2
+ color: Settings.shadeColor
+ visible: false
+ enabled: false
+ }
+
+ Loader {
+ id: spotShapeLoader
+ visible: false; enabled: false
+ anchors.centerIn: centerRect
+ width: centerRect.width; height: width
+ sourceComponent: Qt.createComponent(Settings.spotShape)
+ }
+
+ OpacityMask {
+ id: spot
+ visible: Settings.showSpotShade
+ opacity: centerRect.opacity
+ cached: true
+ invert: true
+ anchors.fill: centerRect
+ source: centerRect
+ maskSource: spotShapeLoader.item
+ enabled: false
+ }
+
+ Loader {
+ id: borderShapeLoader
+ anchors.centerIn: centerRect
+ width: centerRect.width; height: width
+ visible: false; enabled: false
+ sourceComponent: spotShapeLoader.sourceComponent
+ onStatusChanged: {
+ if (status == Loader.Ready) {
+ borderShapeLoader.item.color = Qt.binding(function(){ return Settings.borderColor; })
+ }
+ }
+ }
+
+ Item {
+ id: borderShapeMask
+ anchors.centerIn: centerRect
+ width: centerRect.width; height: width
+ enabled: false; visible: false
+ Item {
+ id: borderShapeScaled
+ anchors.centerIn: parent
+ width: parent.width; height: width
+ scale: (100 - Settings.borderSize) * 1.0 / 100.0
+ property Component component: borderShapeLoader.sourceComponent
+ property QtObject innerObject
+ onComponentChanged: {
+ if (innerObject) innerObject.destroy()
+ innerObject = component.createObject(borderShapeScaled, {visible: true})
+ }
+ }
+ }
+
+ OpacityMask {
+ id: spotBorder
+ visible: Settings.showBorder && Settings.borderSize > 0
+ opacity: Settings.borderOpacity
+ cached: true
+ invert: true
+ anchors.fill: centerRect
+ source: borderShapeLoader.item
+ maskSource: borderShapeMask
+ enabled: false
+ }
+
+ Rectangle {
+ id: dotCursor
+ antialiasing: true
+ anchors.centerIn: centerRect
+ width: Settings.dotSize; height: width
+ radius: width*0.5
+ color: Settings.dotColor
+ visible: Settings.showCenterDot
+ opacity: Settings.dotOpacity
+ enabled: false
+ }
+
+ Rectangle {
+ id: topRect
+ visible: spot.visible
+ color: centerRect.color
+ opacity: centerRect.opacity
+ anchors{ top: parent.top; bottom: centerRect.top; left: parent.left; right: parent.right }
+ enabled: false
+ }
+
+ Rectangle {
+ id: bottomRect
+ visible: spot.visible
+ color: centerRect.color
+ opacity: centerRect.opacity
+ anchors{ top: centerRect.bottom; bottom: parent.bottom; left: parent.left; right: parent.right }
+ enabled: false
+ }
+
+ Rectangle {
+ id: leftRect
+ visible: spot.visible
+ color: centerRect.color
+ opacity: centerRect.opacity
+ anchors{ top: topRect.bottom; bottom: bottomRect.top; left: parent.left; right: centerRect.left }
+ enabled: false
+ }
+
+ Rectangle {
+ id: rightRect
+ visible: spot.visible
+ color: centerRect.color
+ opacity: centerRect.opacity
+ anchors{ top: topRect.bottom; bottom: bottomRect.top; left: centerRect.right; right: parent.right }
+ enabled: false
+ }
+ }
+} // Window
diff --git a/qml/main.qml b/qml/main.qml
index 78a2e084..bd970255 100644
--- a/qml/main.qml
+++ b/qml/main.qml
@@ -1,6 +1,7 @@
// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md
import QtQuick 2.3
import QtQuick.Window 2.2
+
import QtGraphicalEffects 1.0
import Projecteur.Utils 1.0 as Utils
@@ -75,6 +76,7 @@ Window {
onExited: { ProjecteurApp.cursorExitedWindow() }
onEntered: { ProjecteurApp.cursorEntered(screenId) }
onPositionChanged: {
+
if (Settings.multiScreenOverlayEnabled) {
ProjecteurApp.cursorPositionChanged(
mainWindow.contentItem.mapToGlobal(mouse.x, mouse.y))
diff --git a/qml/qml-qt6.qrc b/qml/qml-qt6.qrc
new file mode 100644
index 00000000..f90b4c54
--- /dev/null
+++ b/qml/qml-qt6.qrc
@@ -0,0 +1,9 @@
+
+
+ main-qt6.qml
+ spotshapes/Circle.qml
+ spotshapes/Square.qml
+ spotshapes/Star.qml
+ spotshapes/Ngon.qml
+
+
diff --git a/src/aboutdlg.cc b/src/aboutdlg.cc
index 4e315834..167e084f 100644
--- a/src/aboutdlg.cc
+++ b/src/aboutdlg.cc
@@ -1,4 +1,5 @@
-// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
#include "aboutdlg.h"
@@ -17,7 +18,8 @@
#include
namespace {
- // -------------------------------------------------------------------------------------------------
+ // -----------------------------------------------------------------------------------------------
+ /// Contributor (name, github_name, email, url)
struct Contributor
{
explicit Contributor(const QString& name = {}, const QString& github_name = {},
@@ -26,8 +28,10 @@ namespace {
QString toHtml() const
{
- auto html = QString("%1").arg(name.isEmpty() ? QString("%1").arg(github_name)
- : name);
+ auto html = QString("%1").arg(name.isEmpty()
+ ? QString("%1").arg(github_name)
+ : name);
+
if (email.size()) {
html += QString(" <%1>").arg(email);
}
@@ -47,7 +51,7 @@ namespace {
QString url;
};
- // -------------------------------------------------------------------------------------------------
+ // -----------------------------------------------------------------------------------------------
QString getContributorsHtml()
{
static std::vector contributors =
@@ -65,6 +69,9 @@ namespace {
Contributor("Stuart Prescott", "llimeht"),
Contributor("Crista Renouard", "Lumnicence"),
Contributor("freddii", "freddii"),
+ Contributor("Matthias Blümel", "Blaimi"),
+ Contributor("Grzegorz Szymaszek", "gszy"),
+ Contributor("TheAssassin", "TheAssassin"),
};
static std::mt19937 g(std::random_device{}());
@@ -123,8 +130,10 @@ QWidget* AboutDialog::createVersionInfoWidget()
tr("Version %1", "%1=application version number")
.arg(projecteur::version_string())), this);
vbox->addWidget(versionLabel);
- const auto vInfo = QString("git-branch: %1
git-hash: %2")
- .arg(projecteur::version_branch(), projecteur::version_shorthash());
+ const auto vInfo = QString("git-branch: %1
git-hash: %2
build-type: %3")
+ .arg(projecteur::version_branch(),
+ projecteur::version_shorthash(),
+ projecteur::version_buildtype());
versionLabel->setToolTip(vInfo);
if (QString(projecteur::version_flag()).size() ||
@@ -230,18 +239,21 @@ QWidget* AboutDialog::createThirdPartyLicensesWidget()
for (const auto& tpl : thirdPartyProjects)
{
html += "";
- if (tpl.projectUrl.size())
+ if (tpl.projectUrl.size()) {
html += QString("%2").arg(tpl.projectUrl, tpl.projectName);
- else
+ } else {
html += QString("%1").arg(tpl.projectName);
+ }
- if (tpl.copyrightNotice.size())
+ if (tpl.copyrightNotice.size()) {
html += "
" + tpl.copyrightNotice + "";
+ }
- if (tpl.licenseUrl.size())
+ if (tpl.licenseUrl.size()) {
html += QString("
%2").arg(tpl.licenseUrl, tpl.licenseName);
- else
+ } else {
html += QString("
License: %1").arg(tpl.licenseName);
+ }
html += "";
}
diff --git a/src/aboutdlg.h b/src/aboutdlg.h
index 6ccd1a3c..9b295364 100644
--- a/src/aboutdlg.h
+++ b/src/aboutdlg.h
@@ -1,4 +1,5 @@
-// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
#pragma once
#include
diff --git a/src/actiondelegate.cc b/src/actiondelegate.cc
index ea8ecfd4..dea30b69 100644
--- a/src/actiondelegate.cc
+++ b/src/actiondelegate.cc
@@ -1,4 +1,6 @@
-// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
+
#include "actiondelegate.h"
#include "inputmapconfig.h"
@@ -33,9 +35,9 @@ namespace {
const int w = std::max(opt.fontMetrics.width(ActionDelegate::tr("None")) + 2 * horizontalMargin,
opt.fontMetrics.width(action->keySequence.toString()));
#endif
- return QSize(w, h);
+ return { w, h };
}
- }
+ } // end namespace keysequence
namespace cyclepresets {
// ---------------------------------------------------------------------------------------------
@@ -47,11 +49,10 @@ namespace {
}
// ---------------------------------------------------------------------------------------------
- QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const CyclePresetsAction* /*action*/)
- {
- return QSize(100,16);
+ QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const CyclePresetsAction* /*action*/) {
+ return { 100, 16 };
}
- }
+ } // end namespace cyclepresets
namespace togglespotlight {
// ---------------------------------------------------------------------------------------------
@@ -63,11 +64,55 @@ namespace {
}
// ---------------------------------------------------------------------------------------------
- QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const ToggleSpotlightAction* /*action*/)
+ QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const ToggleSpotlightAction* /*action*/) {
+ return { 100, 16 };
+ }
+ } // end namespace togglespotlight
+
+ namespace scrollhorizontal {
+ // ---------------------------------------------------------------------------------------------
+ void paint(QPainter* p, const QStyleOptionViewItem& option, const ScrollHorizontalAction* /*action*/)
{
- return QSize(100,16);
+ const auto& fm = option.fontMetrics;
+ const int xPos = (option.rect.height()-fm.height()) / 2;
+ NativeKeySeqEdit::drawText(xPos, *p, option, ActionDelegate::tr("Scroll Horizontal"));
}
- }
+
+ // ---------------------------------------------------------------------------------------------
+ QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const ScrollHorizontalAction* /*action*/) {
+ return { 100, 16 };
+ }
+ } // end namespace scrollhorizontal
+
+ namespace scrollvertical {
+ // ---------------------------------------------------------------------------------------------
+ void paint(QPainter* p, const QStyleOptionViewItem& option, const ScrollVerticalAction* /*action*/)
+ {
+ const auto& fm = option.fontMetrics;
+ const int xPos = (option.rect.height()-fm.height()) / 2;
+ NativeKeySeqEdit::drawText(xPos, *p, option, ActionDelegate::tr("Scroll Vertical"));
+ }
+
+ // ---------------------------------------------------------------------------------------------
+ QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const ScrollVerticalAction* /*action*/) {
+ return { 100, 16 };
+ }
+ } // end namespace scrollvertical
+
+ namespace volumecontrol {
+ // ---------------------------------------------------------------------------------------------
+ void paint(QPainter* p, const QStyleOptionViewItem& option, const VolumeControlAction* /*action*/)
+ {
+ const auto& fm = option.fontMetrics;
+ const int xPos = (option.rect.height()-fm.height()) / 2;
+ NativeKeySeqEdit::drawText(xPos, *p, option, ActionDelegate::tr("Volume Control"));
+ }
+
+ // ---------------------------------------------------------------------------------------------
+ QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const VolumeControlAction* /*action*/) {
+ return { 100, 16 };
+ }
+ } // end namespace volumecontrol
} // end anonymous namespace
// -------------------------------------------------------------------------------------------------
@@ -94,6 +139,15 @@ void ActionDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option
case Action::Type::ToggleSpotlight:
togglespotlight::paint(painter, option, static_cast(item.action.get()));
break;
+ case Action::Type::ScrollHorizontal:
+ scrollhorizontal::paint(painter, option, static_cast(item.action.get()));
+ break;
+ case Action::Type::ScrollVertical:
+ scrollvertical::paint(painter, option, static_cast(item.action.get()));
+ break;
+ case Action::Type::VolumeControl:
+ volumecontrol::paint(painter, option, static_cast(item.action.get()));
+ break;
}
if (option.state & QStyle::State_HasFocus) {
@@ -105,7 +159,8 @@ void ActionDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option
QSize ActionDelegate::sizeHint(const QStyleOptionViewItem& opt, const QModelIndex& index) const
{
const auto imModel = qobject_cast(index.model());
- if (!imModel) return QStyledItemDelegate::sizeHint(opt, index);
+ if (!imModel) { return QStyledItemDelegate::sizeHint(opt, index); }
+
const auto& item = imModel->configData(index);
if (!item.action) { return QStyledItemDelegate::sizeHint(opt, index); }
@@ -117,6 +172,13 @@ QSize ActionDelegate::sizeHint(const QStyleOptionViewItem& opt, const QModelInde
return cyclepresets::sizeHint(opt, static_cast(item.action.get()));
case Action::Type::ToggleSpotlight:
return togglespotlight::sizeHint(opt, static_cast(item.action.get()));
+ case Action::Type::ScrollHorizontal:
+ return scrollhorizontal::sizeHint(opt, static_cast(item.action.get()));
+ case Action::Type::ScrollVertical:
+ return scrollvertical::sizeHint(opt, static_cast(item.action.get()));
+ case Action::Type::VolumeControl:
+ return volumecontrol::sizeHint(opt, static_cast(item.action.get()));
+
}
return QStyledItemDelegate::sizeHint(opt, index);
@@ -132,10 +194,12 @@ QWidget* ActionDelegate::createEditor(QWidget* parent, const Action* action) con
connect(editor, &NativeKeySeqEdit::editingFinished, this, &ActionDelegate::commitAndCloseEditor);
return editor;
}
- case Action::Type::CyclePresets: // None for now...
- break;
- case Action::Type::ToggleSpotlight: // None for now...
- break;
+ case Action::Type::CyclePresets: // [[fallthrough]];
+ case Action::Type::ToggleSpotlight: // [[fallthrough]];
+ case Action::Type::ScrollHorizontal: // [[fallthrough]];
+ case Action::Type::ScrollVertical: // [[fallthrough]];
+ case Action::Type::VolumeControl:
+ break; // No editor
}
return nullptr;
}
@@ -146,7 +210,7 @@ QWidget* ActionDelegate::createEditor(QWidget* parent, const QStyleOptionViewIte
{
const auto imModel = qobject_cast(index.model());
- if (!imModel) return nullptr;
+ if (!imModel) { return nullptr; }
const auto& item = imModel->configData(index);
if (!item.action) { return nullptr; }
@@ -192,7 +256,7 @@ bool ActionDelegate::eventFilter(QObject* obj, QEvent* ev)
{
// Let all key press events pass through to the editor,
// otherwise some keys cannot be recorded as a key sequence (e.g. [Tab] and [Esc])
- if (qobject_cast(obj)) return false;
+ if (qobject_cast(obj)) { return false; }
}
return QStyledItemDelegate::eventFilter(obj,ev);
}
@@ -214,11 +278,11 @@ void ActionDelegate::commitAndCloseEditor_()
void ActionDelegate::actionContextMenu(QWidget* parent, InputMapConfigModel* model,
const QModelIndex& index, const QPoint& globalPos)
{
- if (!index.isValid() || !model) return;
+ if (!index.isValid() || !model) { return; }
const auto& item = model->configData(index);
- if (!item.action || item.action->type() != Action::Type::KeySequence) return;
+ if (!item.action || item.action->type() != Action::Type::KeySequence) { return; }
- QMenu* menu = new QMenu(parent);
+ auto* const menu = new QMenu(parent);
const std::vector predefinedKeys = {
&NativeKeySequence::predefined::altTab(),
&NativeKeySequence::predefined::altF4(),
@@ -249,17 +313,21 @@ void ActionTypeDelegate::paint(QPainter* painter, const QStyleOptionViewItem& op
const auto& item = imModel->configData(index);
if (!item.action) { return; }
- const auto symbol = [&item]() -> unsigned int {
+ const auto symbol = [&item]() -> QChar {
switch(item.action->type()) {
- case Action::Type::KeySequence: return Font::Icon::keyboard_4;
- case Action::Type::CyclePresets: return Font::Icon::connection_8;
- case Action::Type::ToggleSpotlight: return Font::Icon::power_on_off_11;
+ case Action::Type::KeySequence: return QChar(Font::Icon::keyboard_4);
+ case Action::Type::CyclePresets: return QChar(Font::Icon::connection_8);
+ case Action::Type::ToggleSpotlight: return QChar(Font::Icon::power_on_off_11);
+ case Action::Type::ScrollHorizontal: return QChar(Font::Icon::cursor_21_rotated);
+ case Action::Type::ScrollVertical: return QChar(Font::Icon::cursor_21);
+ case Action::Type::VolumeControl: return QChar(Font::Icon::audio_6);
}
- return 0;
+ return QChar(0);
}();
- if (symbol != 0)
+ if (symbol != 0) {
drawActionTypeSymbol(0, *painter, option, symbol);
+ }
if (option.state & QStyle::State_HasFocus) {
InputSeqDelegate::drawCurrentIndicator(*painter, option);
@@ -270,21 +338,26 @@ void ActionTypeDelegate::paint(QPainter* painter, const QStyleOptionViewItem& op
void ActionTypeDelegate::actionContextMenu(QWidget* parent, InputMapConfigModel* model,
const QModelIndex& index, const QPoint& globalPos)
{
- if (!index.isValid() || !model) return;
+ if (!index.isValid() || !model) { return; }
+
const auto& item = model->configData(index);
- if (!item.action) return;
+ if (!item.action) { return; }
struct actionEntry {
Action::Type type;
QChar symbol;
QString text;
+ bool isMoveAction;
QIcon icon = {};
};
static std::vector items {
- {Action::Type::KeySequence, Font::Icon::keyboard_4, tr("Key Sequence")},
- {Action::Type::CyclePresets, Font::Icon::connection_8, tr("Cycle Presets")},
- {Action::Type::ToggleSpotlight, Font::Icon::power_on_off_11, tr("Toggle Spotlight")},
+ {Action::Type::KeySequence, QChar(Font::Icon::keyboard_4), tr("Key Sequence"), false},
+ {Action::Type::CyclePresets, QChar(Font::Icon::connection_8), tr("Cycle Presets"), false},
+ {Action::Type::ToggleSpotlight, QChar(Font::Icon::power_on_off_11), tr("Toggle Spotlight"), false},
+ {Action::Type::ScrollHorizontal, QChar(Font::Icon::cursor_21_rotated), tr("Scroll Horizontal"), true},
+ {Action::Type::ScrollVertical, QChar(Font::Icon::cursor_21), tr("Scroll Vertical"), true},
+ {Action::Type::VolumeControl, QChar(Font::Icon::audio_6), tr("Volume Control"), true},
};
static bool initIcons = []()
@@ -307,13 +380,19 @@ void ActionTypeDelegate::actionContextMenu(QWidget* parent, InputMapConfigModel*
return true;
}();
- QMenu* menu = new QMenu(parent);
+ auto* const menu = new QMenu(parent);
+
+ // Check if input sequence is a back or next hold move event.
+ const bool isSpecialMoveInput = !SpecialKeys::logitechSpotlightHoldMove(item.deviceSequence).name.isEmpty();
for (const auto& entry : items) {
- const auto qaction = menu->addAction(entry.icon, entry.text);
- connect(qaction, &QAction::triggered, this, [model, index, type=entry.type](){
- model->setItemActionType(index, type);
- });
+ if ((isSpecialMoveInput && entry.isMoveAction)
+ || (!isSpecialMoveInput && !entry.isMoveAction)) {
+ const auto qaction = menu->addAction(entry.icon, entry.text);
+ connect(qaction, &QAction::triggered, this, [model, index, type=entry.type](){
+ model->setItemActionType(index, type);
+ });
+ };
}
menu->exec(globalPos);
@@ -334,10 +413,11 @@ int ActionTypeDelegate::drawActionTypeSymbol(int startX, QPainter& p,
p.setFont(iconFont);
p.setRenderHint(QPainter::Antialiasing, true);
- if (option.state & QStyle::State_Selected)
+ if (option.state & QStyle::State_Selected) {
p.setPen(option.palette.color(QPalette::HighlightedText));
- else
+ } else {
p.setPen(option.palette.color(QPalette::Text));
+ }
QRect br;
p.drawText(r, Qt::AlignHCenter | Qt::AlignVCenter, QString(symbol), &br);
diff --git a/src/actiondelegate.h b/src/actiondelegate.h
index 5f759b21..60b8755b 100644
--- a/src/actiondelegate.h
+++ b/src/actiondelegate.h
@@ -1,4 +1,5 @@
-// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
#pragma once
#include
diff --git a/src/asynchronous.h b/src/asynchronous.h
new file mode 100644
index 00000000..aded391e
--- /dev/null
+++ b/src/asynchronous.h
@@ -0,0 +1,196 @@
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
+#pragma once
+
+#include
+#include
+
+#if (QT_VERSION < QT_VERSION_CHECK(5, 10, 0))
+#include
+#include
+#endif
+
+#include
+#include
+#include
+#include
+
+namespace async {
+
+// capture_call helper method and apply for C++14 taken from here:
+// https://stackoverflow.com/a/49902823
+
+// Implementation detail of a simplified std::apply from C++17
+template
+constexpr decltype(auto)
+apply_impl(F&& f, Tuple&& t, std::index_sequence){
+ return static_cast(f)(std::get(static_cast(t)) ...);
+}
+
+// Implementation of a simplified std::apply from C++17
+template
+constexpr decltype(auto) apply(F&& f, Tuple&& t){
+ return apply_impl(
+ static_cast(f), static_cast(t),
+ std::make_index_sequence>::value>{});
+}
+
+// Capture args and add them as additional arguments
+template
+auto capture_call(Lambda&& lambda, Args&& ... args){
+ return [
+ lambda = std::forward(lambda),
+ capture_args = std::make_tuple(std::forward(args) ...)
+ ](auto&& ... original_args)mutable{
+ return async::apply([&lambda](auto&& ... args){
+ lambda(std::forward(args) ...);
+ },
+ std::tuple_cat(
+ std::forward_as_tuple(original_args ...),
+ async::apply([](auto&& ... args){
+ return std::forward_as_tuple(
+ std::move(args) ...);
+ }, std::move(capture_args))
+ ));
+ };
+}
+
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
+// Invoke a (lambda) function for context QObject with queued connection.
+template
+void invoke(QObject* context, F&& function) {
+ QMetaObject::invokeMethod(context, std::forward(function), Qt::QueuedConnection);
+}
+#else
+// ... older Qt versions < 5.10
+namespace detail {
+template
+struct FEvent : public QEvent {
+ using Fun = typename std::decay::type;
+ Fun fun;
+ FEvent(Fun && fun) : QEvent(QEvent::None), fun(std::move(fun)) {}
+ FEvent(const Fun & fun) : QEvent(QEvent::None), fun(fun) {}
+ ~FEvent() { fun(); }
+}; }
+
+template
+void invoke(QObject* context, F&& function) {
+ QCoreApplication::postEvent(context, new detail::FEvent(std::forward(function)));
+}
+#endif
+
+// --- Helpers to deduce std::function type from a lambda.
+template
+struct remove_member;
+
+template
+struct remove_member {
+ using type = T;
+};
+
+template
+struct remove_member {
+ using type = R(Args...);
+};
+
+/// Create a safe function object, guaranteed to be invoked in the context of
+/// the given QObject context.
+template
+auto makeSafeCallback_impl(QObject* context, F&& f, std::function, bool autoConnection)
+{
+ QPointer ctxPtr(context);
+ return [ctxPtr, autoConnection, f=std::forward(f)](Args&&... args) mutable
+ {
+ // Check if context object is still valid
+ if (ctxPtr.isNull()) {
+ return;
+ }
+
+ #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
+ QMetaObject::invokeMethod(ctxPtr,
+ capture_call(std::forward(f), std::forward(args)...),
+ autoConnection ? Qt::AutoConnection : Qt::QueuedConnection);
+ // Note: if forceQueued is false and current thread is the same as
+ // the context thread -> execute directly
+ #else
+ // For Qt < 5.10 the call is always queued via the event queue
+ async::invoke(ctxPtr, capture_call(std::forward(f), std::forward(args)...));
+ #endif
+ };
+}
+
+/// Create a safe function object, guaranteed to be invoked in the context of
+/// the given QObject context.
+template
+auto makeSafeCallback(QObject* context, F&& f, bool autoConnection) {
+ using sig = decltype(&F::operator());
+ using ft = std::function::type>;
+ return async::makeSafeCallback_impl(context, std::forward(f), ft{}, autoConnection);
+}
+
+/// Deriving from this class will makeSafeCallback and postSelf methods for QObject based
+/// classes available.
+///
+/// Example:
+/// @code
+/// class MyClass : public QObject, public async::Async {
+/// Q_OBJECT
+/// // ... implementation..
+/// }
+/// @endcode
+template
+class Async
+{
+protected:
+ /// Returns a function object that is guaranteed to be invoked in the own thread context.
+ template
+ auto makeSafeCallback(F&& f, bool autoConnection = true) {
+ return async::makeSafeCallback(static_cast(this), std::forward(f), autoConnection);
+ }
+
+ /// Post a function to the own event loop.
+ template
+ void postSelf(F&& function) {
+ async::invoke(static_cast(this), std::forward(function));
+ }
+
+public:
+ /// Post a task to the object's event loop.
+ template
+ void postTask(Task&& task) {
+ postSelf(std::forward(task));
+ }
+
+ template
+ static constexpr bool is_void_return_v = std::is_same, void>::value;
+
+ /// Post a task with no return value and provide a callback.
+ template
+ typename std::enable_if_t>
+ postCallback(Task&& task, Callback&& callback)
+ {
+ postSelf(
+ [task = std::forward(task), callback = std::forward(callback)]() mutable
+ {
+ task();
+ callback();
+ }
+ );
+ }
+
+ /// Post a task with return value and a callback that takes the return value
+ /// as an argument.
+ template
+ typename std::enable_if_t>
+ postCallback(Task&& task, Callback&& callback)
+ {
+ postSelf(
+ [task = std::forward(task), callback = std::forward(callback)]() mutable
+ {
+ callback(task());
+ }
+ );
+ }
+};
+
+} // end namespace async
diff --git a/src/colorselector.cc b/src/colorselector.cc
index 8789ab62..5a0187d2 100644
--- a/src/colorselector.cc
+++ b/src/colorselector.cc
@@ -1,4 +1,6 @@
-// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
+
#include "colorselector.h"
#include
diff --git a/src/colorselector.h b/src/colorselector.h
index 39d61977..be922c87 100644
--- a/src/colorselector.h
+++ b/src/colorselector.h
@@ -1,4 +1,5 @@
-// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
#pragma once
#include
diff --git a/src/device-command-helper.cc b/src/device-command-helper.cc
new file mode 100644
index 00000000..85ad73df
--- /dev/null
+++ b/src/device-command-helper.cc
@@ -0,0 +1,52 @@
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
+
+#include "device-command-helper.h"
+
+#include "device-hidpp.h"
+#include "spotlight.h"
+
+// -------------------------------------------------------------------------------------------------
+DeviceCommandHelper::DeviceCommandHelper(QObject* parent, Spotlight* spotlight)
+ : QObject(parent), m_spotlight(spotlight)
+{
+
+}
+
+// -------------------------------------------------------------------------------------------------
+DeviceCommandHelper::~DeviceCommandHelper() = default;
+
+
+// -------------------------------------------------------------------------------------------------
+bool DeviceCommandHelper::sendVibrateCommand(uint8_t intensity, uint8_t length)
+{
+ if (m_spotlight.isNull()) {
+ return false;
+ }
+
+ for ( auto const& dev : m_spotlight->connectedDevices()) {
+ if (auto connection = m_spotlight->deviceConnection(dev.id)) {
+ if (!connection->hasHidppSupport()) {
+ continue;
+ }
+
+ for (auto const& subInfo : connection->subDevices()) {
+ auto const& subConn = subInfo.second;
+ if (!subConn || !subConn->hasFlags(DeviceFlag::Vibrate)) {
+ continue;
+ }
+
+ if (auto hidppConn = std::dynamic_pointer_cast(subConn))
+ {
+ hidppConn->sendVibrateCommand(intensity, length,
+ [](HidppConnectionInterface::MsgResult, HIDPP::Message&&) {
+ // logDebug(hid) << tr("Vibrate command returned: %1 (%2)")
+ // .arg(toString(result)).arg(msg.hex());
+ });
+ }
+ }
+ }
+ }
+
+ return true;
+}
diff --git a/src/device-command-helper.h b/src/device-command-helper.h
new file mode 100644
index 00000000..00fb34ed
--- /dev/null
+++ b/src/device-command-helper.h
@@ -0,0 +1,24 @@
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
+#pragma once
+
+#include
+#include
+
+class Spotlight;
+
+/// Class that offers easy access to device commands with a given Spotlight
+/// instance.
+class DeviceCommandHelper : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit DeviceCommandHelper(QObject* parent, Spotlight* spotlight);
+ virtual ~DeviceCommandHelper();
+
+ bool sendVibrateCommand(uint8_t intensity, uint8_t length);
+
+private:
+ QPointer m_spotlight;
+};
diff --git a/src/device-defs.h b/src/device-defs.h
new file mode 100644
index 00000000..6539586d
--- /dev/null
+++ b/src/device-defs.h
@@ -0,0 +1,44 @@
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
+
+#pragma once
+
+#include
+
+#include
+#include
+
+// Bus on which device is connected
+enum class BusType : uint8_t { Unknown, Usb, Bluetooth };
+
+enum class ConnectionType : uint8_t { Event, Hidraw };
+
+enum class ConnectionMode : uint8_t { ReadOnly, WriteOnly, ReadWrite };
+
+// -------------------------------------------------------------------------------------------------
+const char* toString(BusType bt, bool withClass = true);
+const char* toString(ConnectionType ct, bool withClass = true);
+const char* toString(ConnectionMode cm, bool withClass = true);
+
+// -------------------------------------------------------------------------------------------------
+struct DeviceId
+{
+ uint16_t vendorId = 0;
+ uint16_t productId = 0;
+ BusType busType = BusType::Unknown;
+ QString phys{}; // should be sufficient to differentiate between two devices of the same type
+ // - not tested, don't have two devices of any type currently.
+
+ inline bool operator==(const DeviceId& rhs) const {
+ return std::tie(vendorId, productId, busType, phys) == std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys);
+ }
+
+ inline bool operator!=(const DeviceId& rhs) const {
+ return std::tie(vendorId, productId, busType, phys) != std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys);
+ }
+
+ inline bool operator<(const DeviceId& rhs) const {
+ return std::tie(vendorId, productId, busType, phys) < std::tie(rhs.vendorId, rhs.productId, rhs.busType, rhs.phys);
+ }
+};
+Q_DECLARE_METATYPE(DeviceId);
diff --git a/src/device-hidpp.cc b/src/device-hidpp.cc
new file mode 100644
index 00000000..a6e93682
--- /dev/null
+++ b/src/device-hidpp.cc
@@ -0,0 +1,1075 @@
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
+
+#include "device-hidpp.h"
+
+#include "deviceinput.h"
+#include "enum-helper.h"
+#include "logging.h"
+
+#include
+
+#include
+#include
+
+DECLARE_LOGGING_CATEGORY(hid)
+
+// -------------------------------------------------------------------------------------------------
+SubHidppConnection::SubHidppConnection(SubHidrawConnection::Token token,
+ const DeviceId& id, const DeviceScan::SubDevice& sd)
+ : SubHidrawConnection(token, id, sd)
+ , m_featureSet(this)
+ , m_requestCleanupTimer(new QTimer(this))
+{
+ constexpr int cleanUpTimerInterval = 500;
+ m_requestCleanupTimer->setInterval(cleanUpTimerInterval);
+ m_requestCleanupTimer->setSingleShot(false);
+ connect(m_requestCleanupTimer, &QTimer::timeout, this, &SubHidppConnection::clearTimedOutRequests);
+}
+
+// -------------------------------------------------------------------------------------------------
+SubHidppConnection::~SubHidppConnection() = default;
+
+// -------------------------------------------------------------------------------------------------
+const char* toString(SubHidppConnection::ReceiverState s, bool withClass)
+{
+ using ReceiverState = SubHidppConnection::ReceiverState;
+ switch (s) {
+ ENUM_CASE_STRINGIFY3(ReceiverState, Uninitialized, withClass);
+ ENUM_CASE_STRINGIFY3(ReceiverState, Initializing, withClass);
+ ENUM_CASE_STRINGIFY3(ReceiverState, Initialized, withClass);
+ ENUM_CASE_STRINGIFY3(ReceiverState, Error, withClass);
+ }
+ return "ReceiverState::(unknown)";
+}
+
+const char* toString(SubHidppConnection::PresenterState s, bool withClass)
+{
+ using PresenterState = SubHidppConnection::PresenterState;
+ switch (s) {
+ ENUM_CASE_STRINGIFY3(PresenterState, Uninitialized, withClass);
+ ENUM_CASE_STRINGIFY3(PresenterState, Uninitialized_Offline, withClass);
+ ENUM_CASE_STRINGIFY3(PresenterState, Initializing, withClass);
+ ENUM_CASE_STRINGIFY3(PresenterState, Initialized_Online, withClass);
+ ENUM_CASE_STRINGIFY3(PresenterState, Initialized_Offline, withClass);
+ ENUM_CASE_STRINGIFY3(PresenterState, Error, withClass);
+ }
+ return "PresenterState::(unknown)";
+}
+
+// -------------------------------------------------------------------------------------------------
+ssize_t SubHidppConnection::sendData(std::vector data) {
+ return sendData(HIDPP::Message(std::move(data)));
+}
+
+// -------------------------------------------------------------------------------------------------
+ssize_t SubHidppConnection::sendData(HIDPP::Message msg)
+{
+ constexpr ssize_t errorResult = -1;
+ if (!msg.isValid()) {
+ return errorResult;
+ }
+
+ // If the message has the device index 0xff it is meant for USB dongle.
+ // We should not be send it, when the device is connected via bluetooth.
+ //
+ // The Logitech Spotlight (USB) can receive data in two different lengths:
+ // 1. Short (7 byte long starting with 0x10)
+ // 2. Long (20 byte long starting with 0x11)
+ // However, the bluetooth connection only accepts data in long (20 byte) messages.
+
+ if (busType() == BusType::Bluetooth)
+ {
+ if (msg.deviceIndex() == HIDPP::DeviceIndex::DefaultDevice) {
+ logWarn(hid) << tr("Invalid message device index in data '%1' for device connected "
+ "via bluetooth.").arg(msg.hex());
+ return errorResult;
+ }
+
+ // For bluetooth always convert to a long message if we have a short message
+ msg.convertToLong();
+ }
+
+ return SubHidrawConnection::sendData(msg.data(), msg.size());
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::sendData(std::vector data, SendResultCallback resultCb) {
+ sendData(HIDPP::Message(std::move(data)), std::move(resultCb));
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::sendData(HIDPP::Message msg, SendResultCallback resultCb)
+{
+ postSelf([this, msg = std::move(msg), cb = std::move(resultCb)]() mutable {
+ // Check for valid message format
+ if (!msg.isValid()) {
+ if (cb) { cb(MsgResult::InvalidFormat); }
+ return;
+ }
+
+ if (busType() == BusType::Bluetooth) {
+ // For bluetooth always convert to a long message if we have a short message
+ msg.convertToLong();
+ }
+
+ const auto result = SubHidrawConnection::sendData(msg.data(), msg.size());
+ if (cb) {
+ const bool success = (result >= 0 && static_cast(result) == msg.size());
+ cb(success ? MsgResult::Ok : MsgResult::WriteError);
+ }
+ });
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::sendRequest(std::vector data, RequestResultCallback responseCb) {
+ sendRequest(HIDPP::Message(std::move(data)), std::move(responseCb));
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::sendRequest(HIDPP::Message msg, RequestResultCallback responseCb)
+{
+ postSelf([this, msg = std::move(msg), cb = std::move(responseCb)]() mutable
+ {
+ // Check for valid message format
+ if (!msg.isValid()) {
+ if (cb) { cb(MsgResult::InvalidFormat, HIDPP::Message()); }
+ return;
+ }
+
+ // Device index sanity check
+ static const std::array validDeviceIndexes {
+ HIDPP::DeviceIndex::CordedDevice,
+ HIDPP::DeviceIndex::DefaultDevice,
+ HIDPP::DeviceIndex::WirelessDevice1,
+ };
+
+ const auto deviceIndexIt
+ = std::find(validDeviceIndexes.cbegin(), validDeviceIndexes.cend(), msg.deviceIndex());
+
+ if (deviceIndexIt == validDeviceIndexes.cend())
+ {
+ logWarn(hid) << tr("Invalid device index (%1) in message for '%2'")
+ .arg(msg.deviceIndex()).arg(path());
+ if (cb) { cb(MsgResult::InvalidFormat, HIDPP::Message()); }
+ return;
+ }
+
+ if (busType() == BusType::Bluetooth) {
+ // For bluetooth always convert to a long message if we have a short message
+ msg.convertToLong();
+ }
+
+ sendData(msg, makeSafeCallback([this, msg](MsgResult result)
+ {
+ // If data was sent successfully the request will be handled when the reply arrives or
+ // the request times out -> return
+ if (result == MsgResult::Ok) { return; }
+
+ // error result, find our message in the request list
+ auto it = std::find_if(m_requests.begin(), m_requests.end(),
+ [&msg](const RequestEntry& entry) { return entry.request == msg; });
+
+ if (it == m_requests.end()) {
+ logDebug(hid) << "Send request write error without matching request queue entry.";
+ return;
+ }
+
+ if (it->callBack) { it->callBack(result, HIDPP::Message()); }
+ m_requests.erase(it);
+ }));
+
+ constexpr uint64_t hidppMsgTimeoutMs = 4000;
+
+ // Place request in request list with a timeout
+ m_requests.emplace_back(RequestEntry{
+ std::move(msg), std::chrono::steady_clock::now() + std::chrono::milliseconds{hidppMsgTimeoutMs},
+ std::move(cb)});
+
+ // Run cleanup timer if not already active
+ if (!m_requestCleanupTimer->isActive()) { m_requestCleanupTimer->start(); }
+ });
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::sendDataBatch(DataBatch dataBatch, DataBatchResultCallback cb,
+ bool continueOnError) {
+ std::vector results;
+ results.reserve(dataBatch.size());
+ sendDataBatch(std::move(dataBatch), std::move(cb), continueOnError, std::move(results));
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::sendDataBatch(DataBatch dataBatch, DataBatchResultCallback cb,
+ bool continueOnError, std::vector results)
+{
+ postSelf([this, batch = std::move(dataBatch), batchCb = std::move(cb),
+ results = std::move(results), coe = continueOnError]() mutable
+ {
+ if (batch.empty()) {
+ if (batchCb) { batchCb(std::move(results)); }
+ return;
+ }
+
+ // Get item from queue and pop
+ DataBatchItem queueItem(std::move(batch.front()));
+ batch.pop();
+
+ // Process queue item
+ sendData(std::move(queueItem.message), makeSafeCallback(
+ [this, batch = std::move(batch), results = std::move(results), coe,
+ batchCb = std::move(batchCb), resultCb = std::move(queueItem.callback)]
+ (MsgResult result) mutable
+ {
+ // Add result to results vector
+ results.push_back(result);
+ // If a result callback is set invoke it
+ if (resultCb) { resultCb(result); }
+
+ // If batch is empty or we got an error result and don't want to continue on
+ // error (coe)
+ if (batch.empty() || (result != MsgResult::Ok && !coe)) {
+ if (batchCb) { batchCb(std::move(results)); }
+ return;
+ }
+
+ // continue processing the rest of the batch
+ sendDataBatch(std::move(batch), std::move(batchCb), coe, std::move(results));
+ }));
+ });
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::sendRequestBatch(RequestBatch requestBatch, RequestBatchResultCallback cb,
+ bool continueOnError) {
+ std::vector results;
+ results.reserve(requestBatch.size());
+ sendRequestBatch(std::move(requestBatch), std::move(cb), continueOnError, std::move(results));
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::sendRequestBatch(RequestBatch requestBatch, RequestBatchResultCallback cb,
+ bool continueOnError, std::vector results)
+{
+ postSelf([this, batch = std::move(requestBatch), batchCb = std::move(cb),
+ results = std::move(results), coe = continueOnError]() mutable
+ {
+ if (batch.empty()) {
+ if (batchCb) { batchCb(std::move(results)); }
+ return;
+ }
+
+ // Get item from queue and pop
+ RequestBatchItem queueItem(std::move(batch.front()));
+ batch.pop();
+
+ // Process queue item
+ sendRequest(std::move(queueItem.message), makeSafeCallback(
+ [this, batch = std::move(batch), results = std::move(results), coe,
+ batchCb = std::move(batchCb), resultCb = std::move(queueItem.callback)]
+ (MsgResult result, HIDPP::Message&& replyMessage) mutable
+ {
+ // Add result to results vector
+ results.push_back(result);
+ // If a result callback is set invoke it
+ if (resultCb) { resultCb(result, std::move(replyMessage)); }
+
+ // If batch is empty or we got an error result and don't want to continue on
+ // error (coe)
+ if (batch.empty() || (result != MsgResult::Ok && !coe)) {
+ if (batchCb) { batchCb(std::move(results)); }
+ return;
+ }
+
+ // continue processing the rest of the batch
+ sendRequestBatch(std::move(batch), std::move(batchCb), coe, std::move(results));
+ }, true));
+ });
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::registerNotificationCallback(QObject* obj, uint8_t featureIndex,
+ NotificationCallback cb, uint8_t function)
+{
+ if (obj == nullptr || !cb) { return; }
+
+ postSelf([this, obj, featureIndex, function, cb=std::move(cb)]() mutable
+ {
+ auto& callbackList = m_notificationSubscribers[featureIndex];
+ callbackList.emplace_back(Subscriber{obj, function, std::move(cb)});
+
+ if (obj != this)
+ {
+ connect(obj, &QObject::destroyed, this, [this, obj, featureIndex, function]()
+ {
+ auto& callbackList = m_notificationSubscribers[featureIndex];
+ callbackList.remove_if([obj, function](const Subscriber& item){
+ return (item.object == obj && item.function == function);
+ });
+ });
+ }
+ });
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::registerNotificationCallback(QObject* obj, HIDPP::Notification n,
+ NotificationCallback cb, uint8_t function)
+{
+ registerNotificationCallback(obj, to_integral(n), std::move(cb), function);
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::unregisterNotificationCallback(QObject* obj,
+ uint8_t featureIndex,
+ uint8_t function)
+{
+ postSelf([this, obj, featureIndex, function](){
+ auto& callbackList = m_notificationSubscribers[featureIndex];
+ callbackList.remove_if([obj, function](const Subscriber& item){
+ if (item.object == obj) {
+ if (function > 15 || item.function == function) { return true; }
+ }
+ return false;
+ });
+ });
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::unregisterNotificationCallback(QObject* obj,
+ HIDPP::Notification n,
+ uint8_t function)
+{
+ unregisterNotificationCallback(obj, to_integral(n), function);
+}
+
+// -------------------------------------------------------------------------------------------------
+std::shared_ptr SubHidppConnection::create(const DeviceScan::SubDevice& sd,
+ const DeviceConnection& dc) {
+ const int devfd = openHidrawSubDevice(sd, dc.deviceId());
+ if (devfd == -1) { return std::shared_ptr(); }
+
+ auto connection = std::make_shared(Token{}, dc.deviceId(), sd);
+ if (dc.hasHidppSupport()) { connection->m_details.deviceFlags |= DeviceFlag::Hidpp; }
+
+ connection->createSocketNotifiers(devfd, sd.deviceFile);
+ connection->m_inputMapper = dc.inputMapper();
+
+ connect(connection->socketReadNotifier(), &QSocketNotifier::activated, &*connection,
+ &SubHidppConnection::onHidppDataAvailable);
+
+ connection->postTask([c = &*connection]() { c->subDeviceInit(); });
+ return connection;
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::sendVibrateCommand(uint8_t intensity, uint8_t length,
+ RequestResultCallback cb)
+{
+ const uint8_t pcIndex = m_featureSet.featureIndex(HIDPP::FeatureCode::PresenterControl);
+
+ if (pcIndex == 0)
+ {
+ if (cb) { cb(MsgResult::FeatureNotSupported, HIDPP::Message()); }
+ return;
+ }
+
+ // Logitech Spotlight:
+ // present
+ // controlID len intensity
+ // unsigned char vibrate[] = {0x10, 0x01, 0x09, 0x1d, 0x00, 0xe8, 0x80};
+
+ length = length > 10 ? 10 : length; // length should be between 0 to 10.
+
+ using namespace HIDPP;
+
+ Message vibrateMsg(Message::Type::Long, DeviceIndex::WirelessDevice1, pcIndex, 1, {
+ length, 0xe8, intensity
+ });
+
+ sendRequest(std::move(vibrateMsg), std::move(cb));
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::getBatteryLevelStatus(
+ std::function cb)
+{
+ using namespace HIDPP;
+
+ const auto batteryIndex = m_featureSet.featureIndex(FeatureCode::BatteryStatus);
+ if (batteryIndex == 0)
+ {
+ if (cb) { cb(MsgResult::FeatureNotSupported, {}); }
+ return;
+ }
+
+ Message batteryReqMsg(Message::Type::Short, DeviceIndex::WirelessDevice1, batteryIndex, 0);
+ sendRequest(std::move(batteryReqMsg), [cb=std::move(cb)](MsgResult res, Message&& msg) mutable
+ {
+ if (!cb) { return; }
+
+ auto batteryInfo = (res != MsgResult::Ok) ? BatteryInfo{}
+ : BatteryInfo{msg[4],
+ msg[5],
+ to_enum(msg[6])};
+ cb(res, std::move(batteryInfo));
+ });
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::setPointerSpeed(uint8_t speed,
+ std::function cb)
+{
+ const uint8_t psIndex = m_featureSet.featureIndex(HIDPP::FeatureCode::PointerSpeed);
+ if (psIndex == 0x00)
+ {
+ if (cb) { cb(MsgResult::FeatureNotSupported, HIDPP::Message()); }
+ return;
+ }
+
+ speed = (speed > 0x09) ? 0x09 : speed; // speed should be in range of 0-9
+ // Pointer speed sent to the device with values 0x10 - 0x19
+ const uint8_t pointerSpeed = 0x10 & speed;
+
+ sendRequest(
+ HIDPP::Message(HIDPP::Message::Type::Long, HIDPP::DeviceIndex::WirelessDevice1,
+ psIndex, 1, HIDPP::Message::Data{pointerSpeed}),
+ std::move(cb)
+ );
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::setReceiverState(ReceiverState rs)
+{
+ if (rs == m_receiverState) { return; }
+
+ logDebug(hid) << tr("Receiver state (%1) changes from %3 to %4")
+ .arg(path()).arg(toString(m_receiverState), toString(rs));
+ m_receiverState = rs;
+ emit receiverStateChanged(m_receiverState);
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::setPresenterState(PresenterState ps)
+{
+ if (ps == m_presenterState) { return; }
+
+ logDebug(hid) << tr("Presenter state (%1) changes from %2 to %3")
+ .arg(path()).arg(toString(m_presenterState), toString(ps));
+ m_presenterState = ps;
+ emit presenterStateChanged(m_presenterState);
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::setBatteryInfo(const HIDPP::BatteryInfo& bi)
+{
+ if (m_batteryInfo == bi) { return; }
+
+ m_batteryInfo = bi;
+ emit batteryInfoChanged(m_batteryInfo);
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::initReceiver(std::function cb)
+{
+ postSelf([this, cb=std::move(cb)]() mutable {
+ if (m_receiverState == ReceiverState::Initializing
+ || m_receiverState == ReceiverState::Initialized)
+ {
+ logDebug(hid) << "Cannot init receiver when initializing or already initialized.";
+ if (cb) { cb(m_receiverState); }
+ return;
+ }
+
+ setReceiverState(ReceiverState::Initializing);
+
+ if (busType() != BusType::Usb)
+ {
+ // If bus type is not USB return immediately with success result and initialized state
+ setReceiverState(ReceiverState::Initialized);
+ if (cb) { cb(m_receiverState); }
+ return;
+ }
+
+ using namespace HIDPP;
+ using Type = HIDPP::Message::Type;
+
+ int index = -1;
+
+ RequestBatch batch{{
+ RequestBatchItem{
+ // Reset device: get rid of any device configuration by other programs
+ Message(Type::Short, DeviceIndex::DefaultDevice, Commands::GetRegister, 0, 0, {}),
+ [index=++index](MsgResult result, HIDPP::Message&& /* msg */) {
+ if (result == MsgResult::Ok) { return; }
+ logWarn(hid) << tr("Usb receiver init error; step %1: %2")
+ .arg(index).arg(toString(result));
+ }
+ },
+ RequestBatchItem{
+ // Turn off software bit and keep the wireless notification bit on
+ Message(Type::Short, DeviceIndex::DefaultDevice, Commands::SetRegister, 0, 0,
+ {0x00, 0x01, 0x00}),
+ [index=++index](MsgResult result, HIDPP::Message&& /* msg */) {
+ if (result == MsgResult::Ok) { return; }
+ logWarn(hid) << tr("Usb receiver init error; step %1: %2")
+ .arg(index).arg(toString(result));
+ }
+ },
+ RequestBatchItem{
+ // Initialize USB dongle
+ Message(Type::Short, DeviceIndex::DefaultDevice, Commands::GetRegister, 0, 2, {}),
+ [index=++index](MsgResult result, HIDPP::Message&& /* msg */) {
+ if (result == MsgResult::Ok) { return; }
+ logWarn(hid) << tr("Usb receiver init error; step %1: %2")
+ .arg(index).arg(toString(result));
+ }
+ },
+ RequestBatchItem{
+ // ---
+ Message(Type::Short, DeviceIndex::DefaultDevice, Commands::SetRegister, 0, 2,
+ {0x02, 0x00, 0x00}),
+ [index=++index](MsgResult result, HIDPP::Message&& /* msg */) {
+ if (result == MsgResult::Ok) { return; }
+ logWarn(hid) << tr("Usb receiver init error; step %1: %2")
+ .arg(index).arg(toString(result));
+ }
+ },
+ RequestBatchItem{
+ // Now enable both software and wireless notification bit
+ Message(Type::Short, DeviceIndex::DefaultDevice, Commands::SetRegister, 0, 0,
+ {0x00, 0x09, 0x00}),
+ [index=++index](MsgResult result, HIDPP::Message&& /* msg */) {
+ if (result == MsgResult::Ok) { return; }
+ logWarn(hid) << tr("Usb receiver init error; step %1: %2")
+ .arg(index).arg(toString(result));
+ }
+ },
+ }};
+
+ sendRequestBatch(std::move(batch),
+ makeSafeCallback([this, cb=std::move(cb)](std::vector&& results)
+ {
+ setReceiverState(results.back() == MsgResult::Ok ? ReceiverState::Initialized
+ : ReceiverState::Error);
+ if (cb) { cb(m_receiverState); }
+ }, false));
+ });
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::initPresenter(std::function cb)
+{
+ postSelf([this, cb=std::move(cb)]() mutable {
+ if (m_presenterState == PresenterState::Initializing
+ || m_presenterState == PresenterState::Initialized_Offline
+ || m_presenterState == PresenterState::Initialized_Online)
+ {
+ logDebug(hid) << "Cannot init presenter when offline, initializing or already initialized.";
+ if (cb) { cb(m_presenterState); }
+ return;
+ }
+
+ setPresenterState(PresenterState::Initializing);
+
+ m_featureSet.initFromDevice(deviceId(), makeSafeCallback(
+ [this, cb=std::move(cb)](HIDPP::FeatureSet::State state) mutable
+ {
+ using FState = HIDPP::FeatureSet::State;
+ switch (state)
+ {
+ case FState::Error: {
+ setPresenterState(PresenterState::Error);
+ break;
+ }
+ case FState::Uninitialized:
+ case FState::Initializing: {
+ logError(hid) << tr("Unexpected state from feature set.");
+ setPresenterState(PresenterState::Error);
+ break;
+ }
+ case FState::Initialized:
+ {
+ logDebug(hid) << tr("Received %1 supported features from device. (%2)")
+ .arg(m_featureSet.featureCount()).arg(path());
+
+ registerForFeatureNotifications();
+ updateDeviceFlags();
+ initFeatures(makeSafeCallback(
+ [this, cb=std::move(cb)](std::map&& resultMap)
+ {
+ if (!resultMap.empty()) {
+ for (const auto& res : resultMap) {
+ logDebug(hid) << tr("InitFeature result %1 => %2").arg(toString(res.first)).arg(toString(res.second));
+ }
+ }
+ emit featureSetInitialized();
+ setPresenterState(PresenterState::Initialized_Online);
+ if (cb) { cb(m_presenterState); }
+ }));
+ return;
+ }
+ }
+ if (cb) { cb(m_presenterState); }
+ }));
+ });
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::initFeatures(
+ std::function&&)> cb)
+{
+ using namespace HIDPP;
+ using ResultMap = std::map;
+
+ RequestBatch batch;
+ auto resultMap = std::make_shared();
+
+ // Reset spotlight device, if supported
+ if (const auto resetFeatureIndex = m_featureSet.featureIndex(FeatureCode::Reset))
+ {
+ batch.emplace(RequestBatchItem {
+ Message(Message::Type::Long, DeviceIndex::WirelessDevice1, resetFeatureIndex, 1),
+ [resultMap](MsgResult res, Message&& /* msg */) {
+ resultMap->emplace(FeatureCode::Reset, res);
+ }
+ });
+ }
+
+ // Enable Next and back button on hold functionality.
+ if (const auto contrFeatureIndex = m_featureSet.featureIndex(FeatureCode::ReprogramControlsV4))
+ {
+ if (hasFlags(DeviceFlags::NextHold))
+ {
+ batch.emplace(RequestBatchItem {
+ Message(Message::Type::Long, DeviceIndex::WirelessDevice1, contrFeatureIndex, 3,
+ Message::Data{0x00, 0xda, 0x33}),
+ [resultMap](MsgResult res, Message&& /* msg */) {
+ resultMap->emplace(FeatureCode::ReprogramControlsV4, res);
+ }
+ });
+ }
+
+ if (hasFlags(DeviceFlags::BackHold))
+ {
+ batch.emplace(RequestBatchItem {
+ Message(Message::Type::Long, DeviceIndex::WirelessDevice1, contrFeatureIndex, 3,
+ Message::Data{0x00, 0xdc, 0x33}),
+ [resultMap](MsgResult res, Message&& /* msg */) {
+ resultMap->emplace(FeatureCode::ReprogramControlsV4, res);
+ }
+ });
+ }
+ }
+
+ if (const auto psFeatureIndex = m_featureSet.featureIndex(FeatureCode::PointerSpeed))
+ {
+ // Reset pointer speed to 0x14 - the device accepts values from 0x10 to 0x19
+ batch.emplace(RequestBatchItem {
+ HIDPP::Message(HIDPP::Message::Type::Long, HIDPP::DeviceIndex::WirelessDevice1,
+ psFeatureIndex, 1, HIDPP::Message::Data{0x14}),
+ [resultMap](MsgResult res, Message&& /* msg */) {
+ resultMap->emplace(FeatureCode::PointerSpeed, res);
+ }
+ });
+ }
+
+ sendRequestBatch(std::move(batch),
+ [resultMap=std::move(resultMap), cb=std::move(cb)](std::vector&& /* msg */) mutable {
+ if (cb) { cb(std::move(*resultMap)); }
+ });
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::updateDeviceFlags()
+{
+ DeviceFlags featureFlagsSet = DeviceFlag::NoFlags;
+ DeviceFlags featureFlagsUnset = DeviceFlag::NoFlags;
+
+ if (m_featureSet.featureCodeSupported(HIDPP::FeatureCode::PresenterControl)) {
+ featureFlagsSet |= DeviceFlag::Vibrate;
+ logDebug(hid) << tr("Subdevice '%1' reported %2 support.")
+ .arg(path()).arg(toString(HIDPP::FeatureCode::PresenterControl));
+ } else {
+ featureFlagsUnset |= DeviceFlag::Vibrate;
+ }
+
+ if (m_featureSet.featureCodeSupported(HIDPP::FeatureCode::BatteryStatus)) {
+ featureFlagsSet |= DeviceFlag::ReportBattery;
+ logDebug(hid) << tr("Subdevice '%1' reported %2 support.")
+ .arg(path()).arg(toString(HIDPP::FeatureCode::BatteryStatus));
+ } else {
+ featureFlagsUnset |= DeviceFlag::ReportBattery;
+ }
+
+ InputMapper::SpecialMoveInputs specialMoveInputs;
+ if (m_featureSet.featureCodeSupported(HIDPP::FeatureCode::ReprogramControlsV4)) {
+ featureFlagsSet |= DeviceFlags::NextHold;
+ featureFlagsSet |= DeviceFlags::BackHold;
+ specialMoveInputs.emplace_back(SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHoldMove));
+ specialMoveInputs.emplace_back(SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHoldMove));
+ logDebug(hid) << tr("Subdevice '%1' reported %2 support.")
+ .arg(path()).arg(toString(HIDPP::FeatureCode::ReprogramControlsV4));
+ }
+ else {
+ featureFlagsUnset |= DeviceFlags::NextHold;
+ featureFlagsUnset |= DeviceFlags::BackHold;
+ }
+ m_inputMapper->setSpecialMoveInputs(std::move(specialMoveInputs));
+
+ if (m_featureSet.featureCodeSupported(HIDPP::FeatureCode::PointerSpeed)) {
+ featureFlagsSet |= DeviceFlags::PointerSpeed;
+ logDebug(hid) << tr("Subdevice '%1' reported %2 support.")
+ .arg(path()).arg(toString(HIDPP::FeatureCode::PointerSpeed));
+ }
+ else {
+ featureFlagsUnset |= DeviceFlags::BackHold;
+ }
+
+ setFlags(featureFlagsUnset, false);
+ setFlags(featureFlagsSet, true);
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::registerForFeatureNotifications()
+{
+ using namespace HIDPP;
+
+ // Logitech button next and back press and hold + movement
+ if (const auto rcIndex = m_featureSet.featureIndex(FeatureCode::ReprogramControlsV4))
+ {
+ registerNotificationCallback(this, rcIndex, makeSafeCallback([](Message&& msg)
+ {
+ // Logitech Spotlight:
+ // * Next Button = 0xda
+ // * Back Button = 0xdc
+ // Byte 5 and 7 indicate pressed buttons
+ // Back and next can be pressed at the same time
+
+ constexpr uint8_t ButtonNext = 0xda;
+ constexpr uint8_t ButtonBack = 0xdc;
+ const auto isNextPressed = msg[5] == ButtonNext || msg[7] == ButtonNext;
+ const auto isBackPressed = msg[5] == ButtonBack || msg[7] == ButtonBack;
+ logDebug(hid) << tr("Buttons pressed: Next = %1, Back = %2")
+ .arg(isNextPressed).arg(isBackPressed);
+
+ }), 0 /* function 0 */);
+
+ // Handling of move events by button hold is done in spotlight.cc
+ // The following commented out code is kept as example
+
+ // registerNotificationCallback(this, rcIndex, makeSafeCallback([this](Message&& msg) {
+ // byte 4 : -1 for left movement, 0 for right movement
+ // byte 5 : horizontal movement speed -128 to 127
+ // byte 6 : -1 for up movement, 0 for down movement
+ // byte 7 : vertical movement speed -128 to 127
+ // }), 1 /* function 1 */);
+ }
+
+ if (const auto batIndex = m_featureSet.featureIndex(FeatureCode::BatteryStatus))
+ {
+ // A device can send a battery status spontaneously to the software.
+ registerNotificationCallback(this, batIndex, makeSafeCallback([this](Message&& msg) {
+ setBatteryInfo(BatteryInfo{msg[4], msg[5], to_enum(msg[6])});
+ }), 0 /* function 0 */); }
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::registerForUsbNotifications()
+{
+ // Register for device connection notifications from the usb receiver
+ registerNotificationCallback(this, HIDPP::Notification::DeviceConnection, makeSafeCallback(
+ [this](HIDPP::Message&& msg)
+ {
+ const bool linkEstablished = !static_cast(msg[4] & (1<<6));
+ logDebug(hid) << tr("%1, link established = %2")
+ .arg(toString(HIDPP::Notification::DeviceConnection)).arg(linkEstablished);
+
+ if (!linkEstablished) {
+ if (m_presenterState == PresenterState::Initialized_Online) {
+ setPresenterState(PresenterState::Initialized_Offline);
+ }
+ logInfo(hid) << tr("HID++ device '%1' went offline.").arg(path());
+ return;
+ }
+
+ if (m_presenterState == PresenterState::Uninitialized_Offline
+ || m_presenterState == PresenterState::Initialized_Offline
+ || m_presenterState == PresenterState::Uninitialized
+ || m_presenterState == PresenterState::Error)
+ {
+ logInfo(hid) << tr("HID++ device '%1' came online.").arg(path());
+ checkAndUpdatePresenterState(makeSafeCallback([](PresenterState /* ps */) {
+ //...
+ }));
+ }
+ }));
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::subDeviceInit()
+{
+ if (!hasFlags(DeviceFlag::Hidpp)) { return; }
+
+ registerForUsbNotifications();
+
+ // Init receiver - will return almost immediately for bluetooth connections
+ initReceiver(makeSafeCallback([this](ReceiverState rs)
+ {
+ Q_UNUSED(rs);
+ // Independent of the receiver init result, try to initialize the
+ // presenter device HID++ features and more
+ checkAndUpdatePresenterState(makeSafeCallback([](PresenterState /* ps */) {
+ //...
+ }));
+ }));
+}
+
+// -------------------------------------------------------------------------------------------------
+SubHidppConnection::ReceiverState SubHidppConnection::receiverState() const {
+ return m_receiverState;
+}
+
+// -------------------------------------------------------------------------------------------------
+SubHidppConnection::PresenterState SubHidppConnection::presenterState() const {
+ return m_presenterState;
+}
+
+// -------------------------------------------------------------------------------------------------
+HIDPP::ProtocolVersion SubHidppConnection::protocolVersion() const {
+ return m_protocolVersion;
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::triggerBattyerInfoUpdate()
+{
+ using namespace HIDPP;
+ getBatteryLevelStatus(makeSafeCallback([this](MsgResult res, BatteryInfo&& bi)
+ {
+ if (res != MsgResult::Ok) {
+ return;
+ }
+
+ setBatteryInfo(bi);
+ }));
+}
+
+// -------------------------------------------------------------------------------------------------
+const HIDPP::BatteryInfo& SubHidppConnection::batteryInfo() const {
+ return m_batteryInfo;
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::sendPing(RequestResultCallback cb)
+{
+ using namespace HIDPP;
+ // Ping wireless device 1 - same as requesting protocol version
+ Message pingMsg(Message::Type::Short, DeviceIndex::WirelessDevice1, 0, 1, getRandomPingPayload());
+ sendRequest(std::move(pingMsg), std::move(cb));
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::getProtocolVersion(std::function cb)
+{
+ sendPing([cb=std::move(cb)](MsgResult res, HIDPP::Message msg) {
+ if (cb) {
+ auto pv = (res == MsgResult::Ok) ? HIDPP::ProtocolVersion{ msg[4], msg[5] }
+ : HIDPP::ProtocolVersion();
+ logDebug(hid) << tr("getProtocolVersion() => %1, version = %2.%3")
+ .arg(toString(res)).arg(pv.major).arg(pv.minor);
+ cb(res, (res == MsgResult::HidppError) ? msg.errorCode()
+ : HIDPP::Error::NoError, pv);
+ }
+ });
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::checkPresenterOnline(std::function cb)
+{
+ getProtocolVersion(
+ [cb=std::move(cb)](MsgResult res, HIDPP::Error err, HIDPP::ProtocolVersion pv) {
+ if (!cb) return;
+ const bool deviceOnline = MsgResult::Ok == res && err == HIDPP::Error::NoError;
+ if (!deviceOnline && err != HIDPP::Error::Unsupported) {
+ // Unsupported is send as error if the device is offline
+ logWarn(hid) << tr("Unexpected error for offline device (%1, %2)")
+ .arg(toString(res)).arg(toString(err));
+ }
+ cb(deviceOnline, std::move(pv));
+ });
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::checkAndUpdatePresenterState(std::function cb)
+{
+ postSelf([this, cb=std::move(cb)]() mutable
+ {
+ if (m_presenterState == PresenterState::Initializing)
+ {
+ if (cb) { cb(m_presenterState); }
+ return;
+ }
+
+ checkPresenterOnline(makeSafeCallback(
+ [this, cb=std::move(cb)](bool isOnline, HIDPP::ProtocolVersion pv) mutable
+ {
+ if (!isOnline)
+ {
+ switch (m_presenterState)
+ {
+ case PresenterState::Initialized_Online: // [[fallthrough]];
+ case PresenterState::Initialized_Offline: {
+ setPresenterState(PresenterState::Initialized_Offline);
+ break;
+ }
+ case PresenterState::Error: // [[fallthrough]];
+ case PresenterState::Initializing: break;
+ case PresenterState::Uninitialized_Offline: // [[fallthrough]];
+ case PresenterState::Uninitialized: {
+ setPresenterState(PresenterState::Uninitialized_Offline);
+ }
+ }
+ if (cb) { cb(m_presenterState); }
+ return;
+ }
+
+ // device is online, set protocol version and init device feature table if necessary.
+ m_protocolVersion = pv;
+
+ if (m_presenterState == PresenterState::Uninitialized
+ || m_presenterState == PresenterState::Uninitialized_Offline
+ || m_presenterState == PresenterState::Error)
+ {
+ if (m_protocolVersion.smallerThan(2, 0))
+ {
+ logWarn(hid) << tr("Hid++ version < 2.0 not supported. (%1)").arg(path());
+ setPresenterState(PresenterState::Error);
+ if (cb) { cb(m_presenterState); }
+ return;
+ }
+
+ initPresenter(std::move(cb));
+ }
+ else if (m_presenterState == PresenterState::Initialized_Offline)
+ {
+ initFeatures(makeSafeCallback(
+ [this, cb=std::move(cb)](std::map&& resultMap)
+ {
+ if (!resultMap.empty()) {
+ for (const auto& res : resultMap) {
+ logDebug(hid) << tr("InitFeature result %1 => %2").arg(toString(res.first)).arg(toString(res.second));
+ }
+ }
+ setPresenterState(PresenterState::Initialized_Online);
+ if (cb) { cb(m_presenterState); }
+ }));
+ }
+ else if (m_presenterState == PresenterState::Initialized_Online)
+ {
+ setPresenterState(PresenterState::Initialized_Online);
+ if (cb) { cb(m_presenterState); }
+ }
+ }));
+ });
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::onHidppDataAvailable(int fd)
+{
+ // size_t{HIDPP::Message } .. to make clang-tidy happy
+ HIDPP::Message msg(std::vector(size_t{HIDPP::Message::LONG_MSG_SIZE}));
+ const auto res = ::read(fd, msg.data(), msg.dataSize());
+ if (res < 0) {
+ if (errno != EAGAIN) {
+ emit socketReadError(errno);
+ }
+ return;
+ }
+
+ if (!msg.isValid())
+ {
+ if (msg[0] == 0x02) {
+ // just ignore regular HID reports from the Logitech Spotlight
+ }
+ else {
+ logDebug(hid) << tr("Received invalid HID++ message '%1' from %2").arg(msg.hex(), path());
+ }
+ return;
+ }
+
+ if (msg.isError()) {
+ // Find first matching request for the incoming error reply
+ const auto it =
+ std::find_if(m_requests.begin(), m_requests.end(), [&msg](const RequestEntry& requestEntry) {
+ return msg.isErrorResponseTo(requestEntry.request);
+ });
+
+ if (it != m_requests.end())
+ {
+ logDebug(hid) << tr("Received hiddpp error with code = %1 on")
+ .arg(to_integral(msg.errorCode())) << path() << "(" << msg.hex() << ")";
+ if (it->callBack) {
+ it->callBack(MsgResult::HidppError, std::move(msg));
+ }
+ m_requests.erase(it);
+ }
+ else {
+ logWarn(hid) << tr("Received error hidpp message '%1' "
+ "without matching request.").arg(qPrintable(msg.hex()));
+ }
+ return;
+ }
+
+ // Find first matching request for the incoming reply
+ const auto it =
+ std::find_if(m_requests.begin(), m_requests.end(), [&msg](const RequestEntry& requestEntry) {
+ return msg.isResponseTo(requestEntry.request);
+ });
+
+ if (it != m_requests.end())
+ {
+ // Found matching request
+ logDebug(hid) << tr("Received %1 bytes on").arg(msg.size()) << path()
+ << "(" << msg.hex() << ")";
+ if (it->callBack) {
+ it->callBack(MsgResult::Ok, std::move(msg));
+ }
+ m_requests.erase(it);
+ }
+ else if (msg.softwareId() == 0 || msg.subId() < 0x80)
+ {
+ // Event/Notification
+ // logDebug(hid) << tr("Received notification (%1) on %2").arg(msg.hex()).arg(path());
+
+ // Notify subscribers
+ const auto& callbackList = m_notificationSubscribers[msg.featureIndex()];
+ for ( const auto& subscriber : callbackList) {
+ if (subscriber.function > 15 || subscriber.function == msg.function()) {
+ subscriber.cb(msg);
+ }
+ }
+ }
+ else
+ {
+ logWarn(hid) << tr("Received hidpp message "
+ "'%1' without matching request.").arg(msg.hex());
+ }
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidppConnection::clearTimedOutRequests() {
+ const auto now = std::chrono::steady_clock::now();
+ m_requests.remove_if([&now](const RequestEntry& entry) {
+ if (now <= entry.validUntil) {
+ return false;
+ }
+ if (entry.callBack) {
+ entry.callBack(MsgResult::Timeout, HIDPP::Message());
+ }
+ return true;
+ });
+
+ if (m_requests.empty()) {
+ m_requestCleanupTimer->stop();
+ }
+}
diff --git a/src/device-hidpp.h b/src/device-hidpp.h
new file mode 100644
index 00000000..bf9791c8
--- /dev/null
+++ b/src/device-hidpp.h
@@ -0,0 +1,137 @@
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
+#pragma once
+
+#include "device.h"
+#include "hidpp.h"
+
+#include
+#include
+#include
+
+class QTimer;
+
+// -------------------------------------------------------------------------------------------------
+/// Hid++ connection class
+class SubHidppConnection : public SubHidrawConnection, public HidppConnectionInterface
+{
+ Q_OBJECT
+
+public:
+ /// Initialization state of the Usb dongle - for bluetooth this will be always initialized.
+ enum class ReceiverState : uint8_t { Uninitialized, Initializing, Initialized, Error };
+ /// Initialization state of the wireless presenter.
+ /// * Uninitialized - no information had been collected and no defaults had been set up
+ /// * Uninitialized_Offline - same as above, but online check detected offline device
+ /// * Initializing - currently fetching feature sets and setting defaults and other information
+ /// * Initialized_Online - device initialized and online
+ /// * Initialized_Offline - device initialized but offline (only relevant when using usb dongle)
+ /// * Error - An error occured during initialization.
+ enum class PresenterState : uint8_t { Uninitialized, Uninitialized_Offline, Initializing,
+ Initialized_Online, Initialized_Offline, Error };
+
+ static std::shared_ptr create(const DeviceScan::SubDevice& sd,
+ const DeviceConnection& dc);
+
+ SubHidppConnection(SubHidrawConnection::Token, const DeviceId&, const DeviceScan::SubDevice&);
+ ~SubHidppConnection();
+
+ using SubHidrawConnection::sendData;
+
+ // --- HidppConnectionInterface implementation:
+
+ BusType busType() const override { return m_details.deviceId.busType; }
+ ssize_t sendData(std::vector msg) override;
+ ssize_t sendData(HIDPP::Message msg) override;
+ void sendData(std::vector msg, SendResultCallback resultCb) override;
+ void sendData(HIDPP::Message msg, SendResultCallback resultCb) override;
+ void sendDataBatch(DataBatch dataBatch, DataBatchResultCallback cb,
+ bool continueOnError = false) override;
+ void sendRequest(std::vector data, RequestResultCallback responseCb) override;
+ void sendRequest(HIDPP::Message msg, RequestResultCallback responseCb) override;
+ void sendRequestBatch(RequestBatch requestBatch, RequestBatchResultCallback cb,
+ bool continueOnError = false) override;
+
+ void registerNotificationCallback(QObject* obj, HIDPP::Notification notification,
+ NotificationCallback cb, uint8_t function = 0xff) override;
+ void registerNotificationCallback(QObject* obj, uint8_t featureIndex,
+ NotificationCallback cb, uint8_t function = 0xff) override;
+ void unregisterNotificationCallback(QObject* obj, uint8_t featureIndex,
+ uint8_t function = 0xff) override;
+ void unregisterNotificationCallback(QObject* obj, HIDPP::Notification notification,
+ uint8_t function = 0xff) override;
+
+ // ---
+
+ PresenterState presenterState() const;
+ ReceiverState receiverState() const;
+ const HIDPP::FeatureSet& featureSet() { return m_featureSet; }
+
+ HIDPP::ProtocolVersion protocolVersion() const;
+ void triggerBattyerInfoUpdate();
+ const HIDPP::BatteryInfo& batteryInfo() const;
+
+ void sendPing(RequestResultCallback cb);
+ void sendVibrateCommand(uint8_t intensity, uint8_t length, RequestResultCallback cb);
+ /// Set device pointer speed - speed needs to be in the range [0-9]
+ void setPointerSpeed(uint8_t speed, RequestResultCallback cb);
+
+signals:
+ void receiverStateChanged(ReceiverState);
+ void presenterStateChanged(PresenterState);
+ void featureSetInitialized();
+
+ void batteryInfoChanged(const HIDPP::BatteryInfo&);
+
+private:
+ void subDeviceInit();
+ void initReceiver(std::function);
+ void initPresenter(std::function);
+ void updateDeviceFlags();
+ void registerForUsbNotifications();
+ void registerForFeatureNotifications();
+ /// Initializes features. Returns a map of initalized features and the result from it.
+ void initFeatures(std::function&&)> cb);
+
+ void getBatteryLevelStatus(std::function cb);
+
+ void setReceiverState(ReceiverState rs);
+ void setPresenterState(PresenterState ps);
+ void setBatteryInfo(const HIDPP::BatteryInfo& bi);
+
+ void onHidppDataAvailable(int fd);
+
+ void getProtocolVersion(std::function cb);
+ void checkPresenterOnline(std::function cb);
+ void checkAndUpdatePresenterState(std::function cb);
+
+ void clearTimedOutRequests();
+
+ void sendDataBatch(DataBatch dataBatch, DataBatchResultCallback cb, bool continueOnError,
+ std::vector results);
+ void sendRequestBatch(RequestBatch requestBatch, RequestBatchResultCallback cb,
+ bool continueOnError, std::vector results);
+
+ HIDPP::FeatureSet m_featureSet;
+ HIDPP::ProtocolVersion m_protocolVersion;
+ HIDPP::BatteryInfo m_batteryInfo;
+
+ ReceiverState m_receiverState = ReceiverState::Uninitialized;
+ PresenterState m_presenterState = PresenterState::Uninitialized;
+
+ /// A request entry for request messages sent to the device.
+ struct RequestEntry {
+ HIDPP::Message request;
+ std::chrono::time_point validUntil;
+ RequestResultCallback callBack;
+ };
+
+ std::list m_requests;
+ QTimer* m_requestCleanupTimer = nullptr;
+
+ struct Subscriber { QObject* object = nullptr; uint8_t function; NotificationCallback cb; };
+ std::unordered_map> m_notificationSubscribers;
+};
+
+const char* toString(SubHidppConnection::ReceiverState rs, bool withClass = true);
+const char* toString(SubHidppConnection::PresenterState ps, bool withClass = true);
diff --git a/src/device-key-lookup.cc b/src/device-key-lookup.cc
new file mode 100644
index 00000000..68eff2c8
--- /dev/null
+++ b/src/device-key-lookup.cc
@@ -0,0 +1,78 @@
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
+
+#include "device-key-lookup.h"
+
+#include "enum-helper.h"
+
+#include
+
+#include
+
+namespace {
+// -------------------------------------------------------------------------------------------------
+inline uint32_t eHash(uint16_t type, uint16_t code)
+{
+ return ( (static_cast(type) << 16)
+ | (static_cast(code)) );
+}
+
+// -------------------------------------------------------------------------------------------------
+inline uint32_t eHash(const DeviceInputEvent& die) {
+ return eHash(die.type, die.code);
+}
+
+// -------------------------------------------------------------------------------------------------
+uint32_t dHash(const DeviceId& dId)
+{
+ return (static_cast(dId.vendorId) << 16) | dId.productId;
+}
+
+} // end anonymous namespace
+
+namespace KeyName
+{
+// -------------------------------------------------------------------------------------------------
+const QString& lookup(const DeviceId& dId, const DeviceInputEvent& die)
+{
+ using KeyNameMap = std::unordered_map;
+
+ static const KeyNameMap logitechSpotlightMapping = {
+ { eHash(EV_KEY, BTN_LEFT), QObject::tr("Click") },
+ { eHash(EV_KEY, KEY_RIGHT), QObject::tr("Next") },
+ { eHash(EV_KEY, KEY_LEFT), QObject::tr("Back") },
+ { eHash(EV_KEY, to_integral(SpecialKeys::Key::NextHold)),
+ SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHold).name },
+ { eHash(EV_KEY, to_integral(SpecialKeys::Key::BackHold)),
+ SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHold).name },
+ };
+
+ static const KeyNameMap avattoH100Mapping = {
+ { eHash(EV_KEY, BTN_LEFT), QObject::tr("Click") },
+ { eHash(EV_KEY, KEY_PAGEDOWN), QObject::tr("Down") },
+ { eHash(EV_KEY, KEY_PAGEUP), QObject::tr("Up") },
+ };
+
+ static const std::unordered_map map =
+ {
+ {dHash({0x046d, 0xc53e}), logitechSpotlightMapping}, // Spotlight USB
+ {dHash({0x046d, 0xb503}), logitechSpotlightMapping}, // Spotlight Bluetooth
+ {dHash({0x0c45, 0x8101}), avattoH100Mapping}, // Avatto H100, August WP200
+ };
+
+ // check for device id
+ const auto dit = map.find(dHash(dId));
+ if (dit != map.cend())
+ {
+ // check for key event sequence
+ const auto& kesMap = dit->second;
+ const auto kit = kesMap.find(eHash(die));
+ if (kit != kesMap.cend()) {
+ return kit->second;
+ }
+ }
+
+ static const QString notFound;
+ return notFound;
+}
+} // end namespace KeyName
\ No newline at end of file
diff --git a/src/device-key-lookup.h b/src/device-key-lookup.h
new file mode 100644
index 00000000..25020975
--- /dev/null
+++ b/src/device-key-lookup.h
@@ -0,0 +1,13 @@
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
+#pragma once
+
+#include "device-defs.h"
+#include "deviceinput.h"
+
+#include
+
+namespace KeyName
+{
+ const QString& lookup(const DeviceId& dId, const DeviceInputEvent& die);
+} // end namespace KeyName
diff --git a/src/device-vibration.cc b/src/device-vibration.cc
index 895d5a59..18b29a94 100644
--- a/src/device-vibration.cc
+++ b/src/device-vibration.cc
@@ -1,7 +1,10 @@
-// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
+
#include "device-vibration.h"
-#include "device.h"
+#include "device-hidpp.h"
+#include "hidpp.h"
#include "iconwidgets.h"
#include "logging.h"
@@ -18,20 +21,19 @@
#include
#include
-#include
-DECLARE_LOGGING_CATEGORY(device)
+DECLARE_LOGGING_CATEGORY(hid)
// -------------------------------------------------------------------------------------------------
namespace {
- constexpr int numTimers = 3;
-}
+ constexpr uint32_t numTimers = 3;
+} // end anonymous namespace
// -------------------------------------------------------------------------------------------------
struct TimerWidget::Impl
{
// -----------------------------------------------------------------------------------------------
- Impl(TimerWidget* parent)
+ explicit Impl(TimerWidget* parent)
: stack(new QStackedWidget(parent))
, editor(new QWidget(parent))
, overlay(new QWidget(parent))
@@ -47,7 +49,7 @@ struct TimerWidget::Impl
const auto layout = new QHBoxLayout(parent);
layout->addWidget(checkbox);
layout->addWidget(stack);
- layout->setMargin(0);
+ layout->setContentsMargins(0, 0, 0, 0);
stack->addWidget(editor);
stack->addWidget(overlay);
@@ -62,9 +64,14 @@ struct TimerWidget::Impl
editLayout->addWidget(new QLabel(TimerWidget::tr("s"), editor));
editLayout->addStretch(1);
- sbHours->setRange(0, 24);
- sbMinutes->setRange(0, 59);
- sbSeconds->setRange(0, 59);
+ constexpr auto day = std::chrono::hours(24);
+ constexpr auto hoursMax = (day - std::chrono::hours(1)).count();
+ constexpr auto minutesMax = std::chrono::minutes(60).count() - 1;
+ constexpr auto secondsMax = std::chrono::seconds(60).count() - 1;
+
+ sbHours->setRange(0, hoursMax);
+ sbMinutes->setRange(0, minutesMax);
+ sbSeconds->setRange(0, secondsMax);
layout->addWidget(btnStartStop);
btnStartStop->setCheckable(true);
@@ -92,7 +99,7 @@ struct TimerWidget::Impl
btnStartStop->setEnabled(checkbox->isChecked());
QObject::connect(checkbox, &QCheckBox::toggled, parent, [this, parent](bool checked) {
editor->setEnabled(checked);
- if (!checked) btnStartStop->setChecked(false);
+ if (!checked) { btnStartStop->setChecked(false); }
btnStartStop->setEnabled(checked);
emit parent->enabledChanged(checked);
});
@@ -188,8 +195,9 @@ bool TimerWidget::timerRunning() const {
// -------------------------------------------------------------------------------------------------
void TimerWidget::start() {
- if (timerEnabled())
+ if (timerEnabled()) {
m_impl->btnStartStop->setChecked(true);
+ }
}
// -------------------------------------------------------------------------------------------------
@@ -204,9 +212,9 @@ void TimerWidget::setValueSeconds(int seconds)
const auto hours = std::chrono::duration_cast(totalSecs);
const auto mins = std::chrono::duration_cast(totalSecs-hours);
const auto secs = std::chrono::duration_cast(totalSecs-hours-mins);
- m_impl->sbHours->setValue(hours.count());
- m_impl->sbMinutes->setValue(mins.count());
- m_impl->sbSeconds->setValue(secs.count());
+ m_impl->sbHours->setValue( static_cast(hours.count()) );
+ m_impl->sbMinutes->setValue( static_cast(mins.count()) );
+ m_impl->sbSeconds->setValue( static_cast(secs.count()) );
}
// -------------------------------------------------------------------------------------------------
@@ -222,7 +230,7 @@ int TimerWidget::valueSeconds() const {
// -------------------------------------------------------------------------------------------------
struct MultiTimerWidget::Impl
{
- Impl(QWidget* parent)
+ explicit Impl(QWidget* parent)
{
for (size_t i = 0; i < numTimers; ++i) {
timers.at(i) = new TimerWidget(parent);
@@ -237,6 +245,8 @@ MultiTimerWidget::MultiTimerWidget(QWidget* parent)
: QWidget(parent)
, m_impl(new Impl(this))
{
+ constexpr int defaultTimeoutIncrMin = 15;
+
const auto layout = new QHBoxLayout(this);
const auto iconLabel = new IconLabel(Font::time_19, this);
layout->addWidget(iconLabel);
@@ -249,15 +259,21 @@ MultiTimerWidget::MultiTimerWidget(QWidget* parent)
layout->setAlignment(groupBox, Qt::AlignTop);
const auto timerLayout = new QVBoxLayout(groupBox);
- for (size_t i = 0; i < numTimers; ++i) {
+ for (uint32_t i = 0; i < numTimers; ++i)
+ {
timerLayout->addWidget(m_impl->timers.at(i));
- m_impl->timers.at(i)->setValueMinutes(15 + i * 15);
+ const auto timerDefaultValueMinutes = defaultTimeoutIncrMin + i * defaultTimeoutIncrMin;
+
+ m_impl->timers.at(i)->setValueMinutes(static_cast(timerDefaultValueMinutes));
+
connect(m_impl->timers.at(i), &TimerWidget::valueSecondsChanged, this, [this, i](int secs) {
emit timerValueChanged(i, secs);
});
+
connect(m_impl->timers.at(i), &TimerWidget::enabledChanged, this, [this, i](bool enabled) {
emit timerEnabledChanged(i, enabled);
});
+
connect(m_impl->timers.at(i), &TimerWidget::timeout, this, [this, i](){
emit timeout(i);
});
@@ -270,35 +286,35 @@ MultiTimerWidget::MultiTimerWidget(QWidget* parent)
MultiTimerWidget::~MultiTimerWidget() = default;
// -------------------------------------------------------------------------------------------------
-int MultiTimerWidget::timerCount() const {
+int MultiTimerWidget::timerCount() {
return numTimers;
}
// -------------------------------------------------------------------------------------------------
-void MultiTimerWidget::setTimerEnabled(int timerId, bool enabled)
+void MultiTimerWidget::setTimerEnabled(uint32_t timerId, bool enabled)
{
- if (timerId < 0 || timerId >= numTimers) return;
+ if (timerId >= numTimers) { return; }
m_impl->timers.at(timerId)->setTimerEnabled(enabled);
}
// -------------------------------------------------------------------------------------------------
-bool MultiTimerWidget::timerEnabled(int timerId) const
+bool MultiTimerWidget::timerEnabled(uint32_t timerId) const
{
- if (timerId < 0 || timerId >= numTimers) return false;
+ if (timerId >= numTimers) { return false; }
return m_impl->timers.at(timerId)->timerEnabled();
}
// -------------------------------------------------------------------------------------------------
-void MultiTimerWidget::startTimer(int timerId)
+void MultiTimerWidget::startTimer(uint32_t timerId)
{
- if (timerId < 0 || timerId >= numTimers) return;
+ if (timerId >= numTimers) { return; }
m_impl->timers.at(timerId)->start();
}
// -------------------------------------------------------------------------------------------------
-void MultiTimerWidget::stopTimer(int timerId)
+void MultiTimerWidget::stopTimer(uint32_t timerId)
{
- if (timerId < 0 || timerId >= numTimers) return;
+ if (timerId >= numTimers) { return; }
m_impl->timers.at(timerId)->stop();
}
@@ -311,23 +327,23 @@ void MultiTimerWidget::stopAllTimers()
}
// -------------------------------------------------------------------------------------------------
-bool MultiTimerWidget::timerRunning(int timerId) const
+bool MultiTimerWidget::timerRunning(uint32_t timerId) const
{
- if (timerId < 0 || timerId >= numTimers) return false;
+ if (timerId >= numTimers) { return false; }
return m_impl->timers.at(timerId)->timerRunning();
}
// -------------------------------------------------------------------------------------------------
-void MultiTimerWidget::setTimerValue(int timerId, int seconds)
+void MultiTimerWidget::setTimerValue(uint32_t timerId, int seconds)
{
- if (timerId < 0 || timerId >= numTimers) return;
+ if (timerId >= numTimers) { return; }
m_impl->timers.at(timerId)->setValueSeconds(seconds);
}
// -------------------------------------------------------------------------------------------------
-int MultiTimerWidget::timerValue(int timerId) const
+int MultiTimerWidget::timerValue(uint32_t timerId) const
{
- if (timerId < 0 || timerId >= numTimers) return -1;
+ if (timerId >= numTimers) { return -1; }
return m_impl->timers.at(timerId)->valueSeconds();
}
@@ -337,8 +353,11 @@ VibrationSettingsWidget::VibrationSettingsWidget(QWidget* parent)
, m_sbLength(new QSpinBox(this))
, m_sbIntensity(new QSpinBox(this))
{
+ constexpr int vibrationIntensityMin = 25;
+ constexpr int vibrationIntensityMax = 255;
+
m_sbLength->setRange(0, 10);
- m_sbIntensity->setRange(25, 255);
+ m_sbIntensity->setRange(vibrationIntensityMin, vibrationIntensityMax);
const auto layout = new QHBoxLayout(this);
const auto iconLabel = new IconLabel(Font::control_panel_9, this);
@@ -393,44 +412,35 @@ uint8_t VibrationSettingsWidget::intensity() const {
// -------------------------------------------------------------------------------------------------
void VibrationSettingsWidget::setLength(uint8_t len)
{
- if (m_sbLength->value() == len) return;
+ if (m_sbLength->value() == len) { return; }
m_sbLength->setValue(len);
}
// -------------------------------------------------------------------------------------------------
void VibrationSettingsWidget::setIntensity(uint8_t intensity)
{
- if (m_sbIntensity->value() == intensity) return;
+ if (m_sbIntensity->value() == intensity) { return; }
m_sbIntensity->setValue(intensity);
}
// -------------------------------------------------------------------------------------------------
void VibrationSettingsWidget::setSubDeviceConnection(SubDeviceConnection *sdc)
{
- if (sdc->type() == ConnectionType::Hidraw &&
- sdc->mode() == ConnectionMode::ReadWrite)
- m_subDeviceConnection = sdc;
+ m_subDeviceConnection = qobject_cast(sdc);
}
// -------------------------------------------------------------------------------------------------
void VibrationSettingsWidget::sendVibrateCommand()
{
- if (!m_subDeviceConnection) return;
- if ((m_subDeviceConnection->flags() & DeviceFlag::Vibrate) != DeviceFlag::Vibrate) return;
- if (!m_subDeviceConnection->isConnected()) return;
-
- // TODO generalize features and protocol for proprietary device features like vibration
- // for not only the Spotlight device.
- //
- // Spotlight:
- // len intensity
- // unsigned char vibrate[] = {0x10, 0x01, 0x09, 0x1a, 0x00, 0xe8, 0x80};
+ if (!m_subDeviceConnection) { return; }
+ if (!m_subDeviceConnection->isConnected()) { return; }
+ if (!m_subDeviceConnection->hasFlags(DeviceFlag::Vibrate)) { return; }
+
const uint8_t vlen = m_sbLength->value();
const uint8_t vint = m_sbIntensity->value();
- const uint8_t vibrateCmd[] = {0x10, 0x01, 0x09, 0x1a, vlen, 0xe8, vint};
-
- const auto res = m_subDeviceConnection->sendData(vibrateCmd, sizeof(vibrateCmd));
- if (res != sizeof(vibrateCmd)) {
- logWarn(device) << "Could not write vibrate command to device socket.";
- }
+ m_subDeviceConnection->sendVibrateCommand(vint, vlen,
+ [](HidppConnectionInterface::MsgResult result, HIDPP::Message&& msg) {
+ logDebug(hid) << tr("Vibrate command returned: %1 (%2)")
+ .arg(toString(result)).arg(msg.hex());
+ });
}
diff --git a/src/device-vibration.h b/src/device-vibration.h
index 99d9bb6f..097a5c20 100644
--- a/src/device-vibration.h
+++ b/src/device-vibration.h
@@ -1,4 +1,5 @@
-// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
#pragma once
#include
@@ -7,6 +8,7 @@
class QSpinBox;
class SubDeviceConnection;
+class SubHidppConnection;
// -------------------------------------------------------------------------------------------------
class TimerWidget : public QWidget
@@ -47,24 +49,24 @@ class MultiTimerWidget : public QWidget
virtual ~MultiTimerWidget() override;
/// Returns the number of timers
- int timerCount() const;
+ static int timerCount();
- void setTimerEnabled(int timerId, bool enabled);
- bool timerEnabled(int timerId) const;
+ void setTimerEnabled(uint32_t timerId, bool enabled);
+ bool timerEnabled(uint32_t timerId) const;
- void startTimer(int timerId);
- void stopTimer(int timerId);
+ void startTimer(uint32_t timerId);
+ void stopTimer(uint32_t timerId);
void stopAllTimers();
- bool timerRunning(int timerId) const;
+ bool timerRunning(uint32_t timerId) const;
- void setTimerValue(int timerId, int seconds);
- int timerValue(int timerId) const;
+ void setTimerValue(uint32_t timerId, int seconds);
+ int timerValue(uint32_t timerId) const;
signals:
/// Emitted when a timer times out.
- void timeout(int timerId);
- void timerEnabledChanged(int timerId, bool enabled);
- void timerValueChanged(int timerId, int seconds);
+ void timeout(uint32_t timerId);
+ void timerEnabledChanged(uint32_t timerId, bool enabled);
+ void timerValueChanged(uint32_t timerId, int seconds);
private:
struct Impl;
@@ -93,7 +95,7 @@ class VibrationSettingsWidget : public QWidget
void lengthChanged(uint8_t length);
private:
- QPointer m_subDeviceConnection;
+ QPointer m_subDeviceConnection;
QSpinBox* m_sbLength = nullptr;
QSpinBox* m_sbIntensity = nullptr;
};
diff --git a/src/device.cc b/src/device.cc
index 60188cdb..32db26a9 100644
--- a/src/device.cc
+++ b/src/device.cc
@@ -1,11 +1,16 @@
-// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
+
#include "device.h"
#include "deviceinput.h"
#include "devicescan.h"
+#include "enum-helper.h"
+#include "hidpp.h"
#include "logging.h"
#include
+#include
#include
#include
@@ -16,15 +21,55 @@ LOGGING_CATEGORY(hid, "HID")
namespace {
// -----------------------------------------------------------------------------------------------
- static const auto registeredComparator_ = QMetaType::registerComparators();
+ #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
+ const auto registeredComparator_ = QMetaType::registerComparators();
+ #endif
const auto hexId = logging::hexId;
+ // class i18n : public QObject {}; // for i18n and logging
+} // end anonymous namespace
+
+// -------------------------------------------------------------------------------------------------
+const char* toString(BusType bt, bool withClass)
+{
+ switch (bt) {
+ ENUM_CASE_STRINGIFY3(BusType, Unknown, withClass);
+ ENUM_CASE_STRINGIFY3(BusType, Usb, withClass);
+ ENUM_CASE_STRINGIFY3(BusType, Bluetooth, withClass);
+ }
+ return withClass ? "BusType::(unknown)" : "(unkown)";
+}
+
+// -------------------------------------------------------------------------------------------------
+const char* toString(ConnectionType ct, bool withClass)
+{
+ switch (ct) {
+ ENUM_CASE_STRINGIFY3(ConnectionType, Event, withClass);
+ ENUM_CASE_STRINGIFY3(ConnectionType, Hidraw, withClass);
+ }
+ return withClass ? "ConnectionType::(unknown)" : "(unkown)";
+}
+
+// -------------------------------------------------------------------------------------------------
+const char* toString(ConnectionMode cm, bool withClass)
+{
+ switch (cm) {
+ ENUM_CASE_STRINGIFY3(ConnectionMode, ReadOnly, withClass);
+ ENUM_CASE_STRINGIFY3(ConnectionMode, WriteOnly, withClass);
+ ENUM_CASE_STRINGIFY3(ConnectionMode, ReadWrite, withClass);
+ }
+ return withClass ? "ConnectionMode::(unknown)" : "(unkown)";
}
// -------------------------------------------------------------------------------------------------
DeviceConnection::DeviceConnection(const DeviceId& id, const QString& name,
- std::shared_ptr vdev)
- : m_deviceId(id), m_deviceName(name), m_inputMapper(std::make_shared(std::move(vdev))){}
+ std::shared_ptr vmouse,
+ std::shared_ptr vkeyboard)
+ : m_deviceId(id)
+ , m_deviceName(name)
+ , m_inputMapper(std::make_shared(std::move(vmouse), std::move(vkeyboard)))
+{
+}
// -------------------------------------------------------------------------------------------------
DeviceConnection::~DeviceConnection() = default;
@@ -39,9 +84,13 @@ bool DeviceConnection::hasSubDevice(const QString& path) const
// -------------------------------------------------------------------------------------------------
void DeviceConnection::addSubDevice(std::shared_ptr sdc)
{
- if (!sdc) return;
+ if (!sdc) { return; }
const auto path = sdc->path();
+ connect(&*sdc, &SubDeviceConnection::flagsChanged, this, [this, path](){
+ emit subDeviceFlagsChanged(m_deviceId, path);
+ });
+
m_subDeviceConnections[path] = std::move(sdc);
emit subDeviceConnected(m_deviceId, path);
}
@@ -64,41 +113,63 @@ bool DeviceConnection::removeSubDevice(const QString& path)
}
// -------------------------------------------------------------------------------------------------
-SubDeviceConnection::SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode)
- : m_details(path, type, mode) {}
+std::shared_ptr DeviceConnection::subDevice(const QString& devicePath) const
+{
+ const auto it = m_subDeviceConnections.find(devicePath);
+ if (it == m_subDeviceConnections.cend()) {
+ return {};
+ }
-// -------------------------------------------------------------------------------------------------
-SubDeviceConnection::~SubDeviceConnection() = default;
+ return it->second;
+}
// -------------------------------------------------------------------------------------------------
-bool SubDeviceConnection::isConnected() const {
- if (type() == ConnectionType::Event)
- return (m_readNotifier && m_readNotifier->isEnabled());
- if (type() == ConnectionType::Hidraw)
- return (m_readNotifier && m_readNotifier->isEnabled()) && (m_writeNotifier);
- return false;
+bool DeviceConnection::hasHidppSupport() const {
+ // HID++ only for Logitech devices
+ return m_deviceId.vendorId == 0x046d;
}
// -------------------------------------------------------------------------------------------------
-void SubDeviceConnection::disconnect() {
- m_readNotifier.reset();
- m_writeNotifier.reset();
-}
+SubDeviceConnectionDetails::SubDeviceConnectionDetails(const DeviceId& dId, const DeviceScan::SubDevice& sd,
+ ConnectionType type, ConnectionMode mode)
+ : deviceId(dId), type(type), mode(mode), devicePath(sd.deviceFile)
+{}
// -------------------------------------------------------------------------------------------------
-void SubDeviceConnection::disable() {
- if (m_readNotifier) m_readNotifier->setEnabled(false);
- if (m_writeNotifier) m_writeNotifier->setEnabled(false);
+SubDeviceConnection::SubDeviceConnection(const DeviceId& dId, const DeviceScan::SubDevice& sd,
+ ConnectionType type, ConnectionMode mode)
+ : m_details(dId, sd, type, mode) {}
+
+// -------------------------------------------------------------------------------------------------
+SubDeviceConnection::~SubDeviceConnection() = default;
+
+// -------------------------------------------------------------------------------------------------
+DeviceFlags SubDeviceConnection::setFlags(DeviceFlags f, bool set)
+{
+ const auto previousFlags = flags();
+ if (set) {
+ m_details.deviceFlags |= f;
+ } else {
+ m_details.deviceFlags &= ~f;
+ }
+
+ if (m_details.deviceFlags != previousFlags) {
+ emit flagsChanged(m_details.deviceFlags);
+ }
+ return m_details.deviceFlags;
}
// -------------------------------------------------------------------------------------------------
-void SubDeviceConnection::disableWrite() {
- if (m_writeNotifier) m_writeNotifier->setEnabled(false);
+bool SubDeviceConnection::isConnected() const {
+ return false;
}
// -------------------------------------------------------------------------------------------------
-void SubDeviceConnection::enableWrite() {
- if (m_writeNotifier) m_writeNotifier->setEnabled(true);
+void SubDeviceConnection::disconnect() {
+ if (m_readNotifier) {
+ m_readNotifier->setEnabled(false);
+ m_readNotifier.reset();
+ }
}
// -------------------------------------------------------------------------------------------------
@@ -112,13 +183,17 @@ QSocketNotifier* SubDeviceConnection::socketReadNotifier() {
}
// -------------------------------------------------------------------------------------------------
-QSocketNotifier* SubDeviceConnection::socketWriteNotifier() {
- return m_writeNotifier.get();
-}
+SubEventConnection::SubEventConnection(Token /* token */,
+ const DeviceId& dId, const DeviceScan::SubDevice& sd)
+ : SubDeviceConnection(dId, sd, ConnectionType::Event, ConnectionMode::ReadOnly) {}
+
+// -------------------------------------------------------------------------------------------------
+SubEventConnection::~SubEventConnection() = default;
// -------------------------------------------------------------------------------------------------
-SubEventConnection::SubEventConnection(Token, const QString& path)
- : SubDeviceConnection(path, ConnectionType::Event, ConnectionMode::ReadOnly) {}
+bool SubEventConnection::isConnected() const {
+ return (m_readNotifier && m_readNotifier->isEnabled());
+}
// -------------------------------------------------------------------------------------------------
std::shared_ptr SubEventConnection::create(const DeviceScan::SubDevice& sd,
@@ -152,11 +227,11 @@ std::shared_ptr SubEventConnection::create(const DeviceScan:
return std::shared_ptr();
}
- auto connection = std::make_shared(Token{}, sd.deviceFile);
+ auto connection = std::make_shared(Token{}, dc.deviceId(), sd);
- if (!!(bitmask & (1 << EV_SYN))) connection->m_details.deviceFlags |= DeviceFlag::SynEvents;
- if (!!(bitmask & (1 << EV_REP))) connection->m_details.deviceFlags |= DeviceFlag::RepEvents;
- if (!!(bitmask & (1 << EV_KEY))) connection->m_details.deviceFlags |= DeviceFlag::KeyEvents;
+ if (!!(bitmask & (1 << EV_SYN))) { connection->m_details.deviceFlags |= DeviceFlag::SynEvents; }
+ if (!!(bitmask & (1 << EV_REP))) { connection->m_details.deviceFlags |= DeviceFlag::RepEvents; }
+ if (!!(bitmask & (1 << EV_KEY))) { connection->m_details.deviceFlags |= DeviceFlag::KeyEvents; }
if (!!(bitmask & (1 << EV_REL)))
{
unsigned long relEvents = 0;
@@ -171,7 +246,7 @@ std::shared_ptr SubEventConnection::create(const DeviceScan:
connection->m_details.grabbed = [&dc, evfd, &sd]()
{
// Grab device inputs if a virtual device exists.
- if (dc.inputMapper()->virtualDevice())
+ if (dc.inputMapper()->hasVirtualDevice())
{
const int res = ioctl(evfd, EVIOCGRAB, 1);
if (res == 0) { return true; }
@@ -192,123 +267,230 @@ std::shared_ptr SubEventConnection::create(const DeviceScan:
connection->m_readNotifier = std::make_unique(evfd, QSocketNotifier::Read);
QSocketNotifier* const notifier = connection->m_readNotifier.get();
// Auto clean up and close descriptor on destruction of notifier
- connect(notifier, &QSocketNotifier::destroyed, [grabbed = connection->m_details.grabbed, notifier]() {
+ connect(notifier, &QSocketNotifier::destroyed,
+ [grabbed = connection->m_details.grabbed, evfd, path=sd.deviceFile]() {
if (grabbed) {
- ioctl(static_cast(notifier->socket()), EVIOCGRAB, 0);
+ ioctl(evfd, EVIOCGRAB, 0);
}
- ::close(static_cast(notifier->socket()));
+ logDebug(device) << tr("Closing file descriptor for '%1'").arg(path);
+ ::close(evfd);
});
connection->m_inputMapper = dc.inputMapper();
- connection->m_details.phys = sd.phys;
-
return connection;
}
// -------------------------------------------------------------------------------------------------
-SubHidrawConnection::SubHidrawConnection(Token, const QString& path)
- : SubDeviceConnection(path, ConnectionType::Hidraw, ConnectionMode::ReadWrite) {}
+SubHidrawConnection::SubHidrawConnection(Token /* token */,
+ const DeviceId& dId, const DeviceScan::SubDevice& sd)
+ : SubDeviceConnection(dId, sd, ConnectionType::Hidraw, ConnectionMode::ReadWrite) {}
+
+// -------------------------------------------------------------------------------------------------
+SubHidrawConnection::~SubHidrawConnection() = default;
+
+// -------------------------------------------------------------------------------------------------
+bool SubHidrawConnection::isConnected() const {
+ return (m_readNotifier && m_readNotifier->isEnabled()) && (m_writeNotifier);
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidrawConnection::disconnect() {
+ SubDeviceConnection::disconnect();
+ if (m_writeNotifier) {
+ m_writeNotifier->setEnabled(false);
+ m_writeNotifier.reset();
+ }
+}
// -------------------------------------------------------------------------------------------------
std::shared_ptr SubHidrawConnection::create(const DeviceScan::SubDevice& sd,
const DeviceConnection& dc)
{
+ const int devfd = openHidrawSubDevice(sd, dc.deviceId());
+ if (devfd == -1) { return std::shared_ptr(); }
+
+ auto connection = std::make_shared(Token{}, dc.deviceId(), sd);
+ connection->createSocketNotifiers(devfd, sd.deviceFile);
+
+ connect(connection->socketReadNotifier(), &QSocketNotifier::activated,
+ &*connection, &SubHidrawConnection::onHidrawDataAvailable);
+
+ return connection;
+}
+
+// -----------------------------------------------------------------------------------------------
+int SubHidrawConnection::openHidrawSubDevice(const DeviceScan::SubDevice& sd, const DeviceId& devId)
+{
+ constexpr int errorResult = -1;
const int devfd = ::open(sd.deviceFile.toLocal8Bit().constData(), O_RDWR|O_NONBLOCK , 0);
- if (devfd == -1) {
+ if (devfd == errorResult) {
logWarn(device) << tr("Cannot open hidraw device '%1' for read/write.").arg(sd.deviceFile);
- return std::shared_ptr();
+ return errorResult;
}
- int descriptorSize = 0;
- // Get Report Descriptor Size
- if (ioctl(devfd, HIDIOCGRDESCSIZE, &descriptorSize) < 0) {
- logWarn(device) << tr("Cannot retrieve report descriptor size of hidraw device '%1'.").arg(sd.deviceFile);
- return std::shared_ptr();
- }
+ { // Get Report Descriptor Size and Descriptor -- currently unused, but if it fails
+ // we don't use the device
+ int descriptorSize = 0;
+ if (ioctl(devfd, HIDIOCGRDESCSIZE, &descriptorSize) < 0)
+ {
+ logWarn(device) << tr("Cannot retrieve report descriptor size of hidraw device '%1'.").arg(sd.deviceFile);
+ ::close(devfd);
+ return errorResult;
+ }
- struct hidraw_report_descriptor reportDescriptor{};
- reportDescriptor.size = descriptorSize;
- if (ioctl(devfd, HIDIOCGRDESC, &reportDescriptor) < 0) {
- logWarn(device) << tr("Cannot retrieve report descriptor of hidraw device '%1'.").arg(sd.deviceFile);
- return std::shared_ptr();
+ struct hidraw_report_descriptor reportDescriptor {};
+ reportDescriptor.size = descriptorSize;
+ if (ioctl(devfd, HIDIOCGRDESC, &reportDescriptor) < 0)
+ {
+ logWarn(device) << tr("Cannot retrieve report descriptor of hidraw device '%1'.").arg(sd.deviceFile);
+ ::close(devfd);
+ return errorResult;
+ }
}
- struct hidraw_devinfo devinfo{};
+ struct hidraw_devinfo devinfo {};
// get the hidraw sub-device id info
- if (ioctl(devfd, HIDIOCGRAWINFO, &devinfo) < 0) {
+ if (ioctl(devfd, HIDIOCGRAWINFO, &devinfo) < 0)
+ {
logWarn(device) << tr("Cannot get info from hidraw device '%1'.").arg(sd.deviceFile);
- return std::shared_ptr();
+ ::close(devfd);
+ return errorResult;
};
// Check against given device id
- if (static_cast(devinfo.vendor) != dc.deviceId().vendorId
- || static_cast(devinfo.product) != dc.deviceId().productId)
+ if (static_cast(devinfo.vendor) != devId.vendorId
+ || static_cast(devinfo.product) != devId.productId)
{
- ::close(devfd);
logDebug(device) << tr("Device id mismatch: %1 (%2:%3)")
- .arg(sd.deviceFile, hexId(devinfo.vendor), hexId(devinfo.product));
- return std::shared_ptr();
+ .arg(sd.deviceFile, hexId(devinfo.vendor), hexId(devinfo.product));
+ ::close(devfd);
+ return errorResult;
}
- auto connection = std::make_shared(Token{}, sd.deviceFile);
+ return devfd;
+}
- fcntl(devfd, F_SETFL, fcntl(devfd, F_GETFL, 0) | O_NONBLOCK);
- if ((fcntl(devfd, F_GETFL, 0) & O_NONBLOCK) == O_NONBLOCK) {
- connection->m_details.deviceFlags |= DeviceFlag::NonBlocking;
+// -------------------------------------------------------------------------------------------------
+ssize_t SubHidrawConnection::sendData(const QByteArray& msg)
+{
+ return sendData(msg.data(), msg.size());
+}
+
+// -------------------------------------------------------------------------------------------------
+ssize_t SubHidrawConnection::sendData(const void* msg, size_t msgLen)
+{
+ constexpr ssize_t errorResult = -1;
+
+ if (mode() != ConnectionMode::ReadWrite || !m_writeNotifier) { return errorResult; }
+ const auto res = ::write(m_writeNotifier->socket(), msg, msgLen);
+
+ if (static_cast(res) == msgLen) {
+ logDebug(hid) << res << "bytes written to" << path() << "("
+ << QByteArray::fromRawData(static_cast(msg), msgLen).toHex() << ")";
+ } else {
+ logWarn(hid) << tr("Writing to '%1' failed. (%2)").arg(path()).arg(res);
}
- // For now vibration is only supported for the Logitech Spotlight (USB)
- // TODO A more generic approach
- if (dc.deviceId().vendorId == 0x46d && dc.deviceId().productId == 0xc53e) {
- connection->m_details.deviceFlags |= DeviceFlag::Vibrate;
+ return res;
+}
+
+// -------------------------------------------------------------------------------------------------
+void SubHidrawConnection::createSocketNotifiers(int fd, const QString& path)
+{
+ fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
+ if ((fcntl(fd, F_GETFL, 0) & O_NONBLOCK) == O_NONBLOCK) {
+ m_details.deviceFlags |= DeviceFlag::NonBlocking;
}
// Create read and write socket notifiers
- connection->m_readNotifier = std::make_unique(devfd, QSocketNotifier::Read);
- QSocketNotifier* const readNotifier = connection->m_readNotifier.get();
+ m_readNotifier = std::make_unique(fd, QSocketNotifier::Read);
+ QSocketNotifier *const readNotifier = m_readNotifier.get();
+ auto fdPtr = std::make_shared(fd);
+
// Auto clean up and close descriptor on destruction of notifier
- connect(readNotifier, &QSocketNotifier::destroyed, [readNotifier]() {
- ::close(static_cast(readNotifier->socket()));
+ connect(readNotifier, &QSocketNotifier::destroyed, [fdPtr, path]()
+ {
+ if (fdPtr && *fdPtr != -1) {
+ logDebug(device) << tr("Closing file descriptor for '%1'").arg(path);
+ ::close(*fdPtr);
+ *fdPtr = -1;
+ }
});
- connection->m_writeNotifier = std::make_unique(devfd, QSocketNotifier::Write);
- QSocketNotifier* const writeNotifier = connection->m_writeNotifier.get();
+ m_writeNotifier = std::make_unique(fd, QSocketNotifier::Write);
+ QSocketNotifier *const writeNotifier = m_writeNotifier.get();
+ writeNotifier->setEnabled(false); // Disable write notifier by default
// Auto clean up and close descriptor on destruction of notifier
- connect(writeNotifier, &QSocketNotifier::destroyed, [writeNotifier]() {
- ::close(static_cast(writeNotifier->socket()));
+ connect(writeNotifier, &QSocketNotifier::destroyed, [fdPtr, path]()
+ {
+ if (fdPtr && *fdPtr != -1) {
+ logDebug(device) << tr("Closing file descriptor for '%1'").arg(path);
+ ::close(*fdPtr);
+ *fdPtr = -1;
+ }
});
-
- connection->m_details.phys = sd.phys;
- connection->disableWrite(); // disable write notifier
- return connection;
}
// -------------------------------------------------------------------------------------------------
-ssize_t SubDeviceConnection::sendData(const QByteArray& hidppMsg)
+void SubHidrawConnection::onHidrawDataAvailable(int fd)
{
- ssize_t res = -1;
- bool isValidMsg = (hidppMsg.length() == 7 && hidppMsg.at(0) == 0x10); // HID++ short message
- isValidMsg = isValidMsg || (hidppMsg.length() == 20 && hidppMsg.at(0) == 0x11); // HID++ long message
-
- if (type() == ConnectionType::Hidraw && mode() == ConnectionMode::ReadWrite
- && m_writeNotifier && isValidMsg)
- {
- enableWrite();
- const auto notifier = socketWriteNotifier();
- res = ::write(notifier->socket(), hidppMsg.data(), hidppMsg.length());
- logDebug(hid) << "Write" << hidppMsg.toHex() << "to" << path();
- disableWrite();
+ QByteArray readVal(20, 0);
+ const auto res = ::read(fd, readVal.data(), readVal.size());
+ if (res < 0) {
+ if (errno != EAGAIN) {
+ emit socketReadError(errno);
+ }
+ return;
}
- return res;
+ // For generic hidraw devices without known protocols, just print out the
+ // received data into the debug log
+ logDebug(hid) << "Received" << readVal.toHex() << "from" << path();
}
+// -------------------------------------------------------------------------------------------------
+const char* toString(DeviceFlag f, bool withClass)
+{
+ switch(f) {
+ ENUM_CASE_STRINGIFY3(DeviceFlag, NoFlags, withClass);
+ ENUM_CASE_STRINGIFY3(DeviceFlag, NonBlocking, withClass);
+ ENUM_CASE_STRINGIFY3(DeviceFlag, SynEvents, withClass);
+ ENUM_CASE_STRINGIFY3(DeviceFlag, RepEvents, withClass);
+ ENUM_CASE_STRINGIFY3(DeviceFlag, RelativeEvents, withClass);
+ ENUM_CASE_STRINGIFY3(DeviceFlag, KeyEvents, withClass);
+ ENUM_CASE_STRINGIFY3(DeviceFlag, Hidpp, withClass);
+ ENUM_CASE_STRINGIFY3(DeviceFlag, Vibrate, withClass);
+ ENUM_CASE_STRINGIFY3(DeviceFlag, ReportBattery, withClass);
+ ENUM_CASE_STRINGIFY3(DeviceFlag, NextHold, withClass);
+ ENUM_CASE_STRINGIFY3(DeviceFlag, BackHold, withClass);
+ ENUM_CASE_STRINGIFY3(DeviceFlag, PointerSpeed, withClass);
+ }
+ return withClass ? "DeviceFlag::(unknown)" : "(unknown)";
+}
+
+// -------------------------------------------------------------------------------------------------
+QString toString(DeviceFlags flags, const QString& separator, bool withClass)
+{
+ return toStringList(flags, withClass).join(separator);
+}
// -------------------------------------------------------------------------------------------------
-ssize_t SubDeviceConnection::sendData(const void* hidppMsg, size_t hidppMsgLen)
+QStringList toStringList(DeviceFlags flags, bool withClass)
{
- const QByteArray hidppMsgArr(reinterpret_cast(hidppMsg), hidppMsgLen);
+ if (flags == DeviceFlags::NoFlags) {
+ return QStringList{ ENUM_STRINGIFY3(DeviceFlag, NoFlags, withClass) };
+ }
+
+ QStringList list;
+
+ for (size_t i = 0; i < sizeof(std::underlying_type_t) * 8; ++i)
+ {
+ const std::underlying_type_t singleFlag = 1 << i;
+ if ((to_integral(flags) & singleFlag) == singleFlag) {
+ list.push_back(toString(to_enum(singleFlag)));
+ }
+ }
- return sendData(hidppMsgArr);
+ return list;
}
diff --git a/src/device.h b/src/device.h
index b4a90c89..b23d0742 100644
--- a/src/device.h
+++ b/src/device.h
@@ -1,36 +1,20 @@
-// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
#pragma once
+#include "asynchronous.h"
#include "enum-helper.h"
+#include "devicescan.h"
+
+#include
#include
+#include
#include
#include
-// -------------------------------------------------------------------------------------------------
-struct DeviceId
-{
- uint16_t vendorId = 0;
- uint16_t productId = 0;
- QString phys; // should be sufficient to differentiate between two devices of the same type
- // - not tested, don't have two devices of any type currently.
-
- inline bool operator==(const DeviceId& rhs) const {
- return std::tie(vendorId, productId, phys) == std::tie(rhs.vendorId, rhs.productId, rhs.phys);
- }
-
- inline bool operator!=(const DeviceId& rhs) const {
- return std::tie(vendorId, productId, phys) != std::tie(rhs.vendorId, rhs.productId, rhs.phys);
- }
-
- inline bool operator<(const DeviceId& rhs) const {
- return std::tie(vendorId, productId, phys) < std::tie(rhs.vendorId, rhs.productId, rhs.phys);
- }
-};
-
-Q_DECLARE_METATYPE(DeviceId);
// -------------------------------------------------------------------------------------------------
class InputMapper;
@@ -38,32 +22,34 @@ class QSocketNotifier;
class SubDeviceConnection;
class VirtualDevice;
-// -----------------------------------------------------------------------------------------------
-enum class ConnectionType : uint8_t { Event, Hidraw };
-enum class ConnectionMode : uint8_t { ReadOnly, WriteOnly, ReadWrite };
-
// -------------------------------------------------------------------------------------------------
+/// The main device connection class, which usually consists of one or multiple sub devices.
class DeviceConnection : public QObject
{
Q_OBJECT
public:
- DeviceConnection(const DeviceId& id, const QString& name, std::shared_ptr vdev);
+ DeviceConnection(const DeviceId& id, const QString& name,
+ std::shared_ptr vmouse, std::shared_ptr vkeyboard);
+
~DeviceConnection();
const auto& deviceName() const { return m_deviceName; }
const auto& deviceId() const { return m_deviceId; }
const auto& inputMapper() const { return m_inputMapper; }
+ bool hasHidppSupport() const;
auto subDeviceCount() const { return m_subDeviceConnections.size(); }
bool hasSubDevice(const QString& path) const;
void addSubDevice(std::shared_ptr);
bool removeSubDevice(const QString& path);
const auto& subDevices() { return m_subDeviceConnections; }
+ std::shared_ptr subDevice(const QString& devicePath) const;
signals:
void subDeviceConnected(const DeviceId& id, const QString& path);
void subDeviceDisconnected(const DeviceId& id, const QString& path);
+ void subDeviceFlagsChanged(const DeviceId& id, const QString& path);
protected:
using DevicePath = QString;
@@ -84,20 +70,30 @@ enum class DeviceFlag : uint32_t {
RelativeEvents = 1 << 3,
KeyEvents = 1 << 4,
- Vibrate = 1 << 16,
+ Hidpp = 1 << 15, ///< Device supports hidpp requests
+ Vibrate = 1 << 16, ///< Device supports vibrate commands
+ ReportBattery = 1 << 17, ///< Device can report battery status
+ NextHold = 1 << 18, ///< Device can be configured to send 'Next Hold' event.
+ BackHold = 1 << 19, ///< Device can be configured to send 'Back Hold' event.
+ PointerSpeed = 1 << 20, ///< Device allows changing pointer speed.
};
ENUM(DeviceFlag, DeviceFlags)
-// -----------------------------------------------------------------------------------------------
+// -------------------------------------------------------------------------------------------------
+const char* toString(DeviceFlag flag, bool withClass = true);
+QString toString(DeviceFlags flags, const QString& separator, bool withClass = true);
+QStringList toStringList(DeviceFlags flags, bool withClass = true);
+
+// -------------------------------------------------------------------------------------------------
struct SubDeviceConnectionDetails {
- SubDeviceConnectionDetails(const QString& path, ConnectionType type, ConnectionMode mode)
- : type(type), mode(mode), devicePath(path) {}
+ SubDeviceConnectionDetails(const DeviceId& dId, const DeviceScan::SubDevice& sd,
+ ConnectionType type, ConnectionMode mode);
+ DeviceId deviceId;
ConnectionType type;
ConnectionMode mode;
bool grabbed = false;
DeviceFlags deviceFlags = DeviceFlags::NoFlags;
- QString phys;
QString devicePath;
};
@@ -118,46 +114,40 @@ struct InputBuffer {
};
// -------------------------------------------------------------------------------------------------
-class SubDeviceConnection : public QObject
+class SubDeviceConnection : public QObject, public async::Async
{
Q_OBJECT
public:
virtual ~SubDeviceConnection() = 0;
- bool isConnected() const;
- void disconnect(); // destroys socket notifier and close file handle
- void disable(); // disable receiving/sending data
- void disableWrite(); // disable sending data
- void enableWrite(); // enable sending data
+ virtual bool isConnected() const;
+ virtual void disconnect(); // destroys socket notifier(s) and close file handle(s)
- ssize_t sendData(const QByteArray& hidppMsg); // Send HID++ Message to HIDraw connection
- ssize_t sendData(const void* hidppMsg, size_t hidppMsgLen); // Send HID++ Message to HIDraw connection
+ auto type() const { return m_details.type; }
+ auto mode() const { return m_details.mode; }
+ auto isGrabbed() const { return m_details.grabbed; }
+ auto flags() const { return m_details.deviceFlags; }
+ const auto& path() const { return m_details.devicePath; }
+ const auto& deviceId() const { return m_details.deviceId; }
- auto type() const { return m_details.type; };
- auto mode() const { return m_details.mode; };
- auto isGrabbed() const { return m_details.grabbed; };
- auto flags() const { return m_details.deviceFlags; };
- const auto& phys() const { return m_details.phys; };
- const auto& path() const { return m_details.devicePath; };
+ inline bool hasFlags(DeviceFlags f) const { return ((flags() & f) == f); }
const std::shared_ptr& inputMapper() const;
QSocketNotifier* socketReadNotifier(); // Read notifier for Hidraw and Event connections for receiving data from device
- QSocketNotifier* socketWriteNotifier(); // Write notifier for Hidraw connection for sending data to device
+
+signals:
+ void flagsChanged(DeviceFlags f);
+ void socketReadError(int err);
protected:
- SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode);
+ SubDeviceConnection(const DeviceId& dId, const DeviceScan::SubDevice& sd, ConnectionType type, ConnectionMode mode);
+ DeviceFlags setFlags(DeviceFlags f, bool set = true);
SubDeviceConnectionDetails m_details;
- std::shared_ptr m_inputMapper; // shared input mapper from parent device.
+ std::shared_ptr m_inputMapper; ///< Shared input mapper from parent device.
std::unique_ptr m_readNotifier;
- std::unique_ptr m_writeNotifier; // only useful for Hidraw connections
};
-// -------------------------------------------------------------------------------------------------
-namespace DeviceScan {
- struct SubDevice;
-}
-
// -------------------------------------------------------------------------------------------------
class SubEventConnection : public SubDeviceConnection
{
@@ -168,7 +158,9 @@ class SubEventConnection : public SubDeviceConnection
static std::shared_ptr create(const DeviceScan::SubDevice& sd,
const DeviceConnection& dc);
- SubEventConnection(Token, const QString& path);
+ SubEventConnection(Token, const DeviceId&, const DeviceScan::SubDevice&);
+ virtual ~SubEventConnection();
+ bool isConnected() const;
auto& inputBuffer() { return m_inputEventBuffer; }
protected:
@@ -176,14 +168,39 @@ class SubEventConnection : public SubDeviceConnection
};
// -------------------------------------------------------------------------------------------------
-class SubHidrawConnection : public SubDeviceConnection
+class HidrawConnectionInterface
+{
+ // Generic plain, synchronous sendData interface
+ virtual ssize_t sendData(const QByteArray& msg) = 0;
+ virtual ssize_t sendData(const void* msg, size_t msgLen) = 0;
+};
+
+// -------------------------------------------------------------------------------------------------
+class SubHidrawConnection : public SubDeviceConnection, public HidrawConnectionInterface
{
Q_OBJECT
+
+protected:
class Token{};
public:
static std::shared_ptr create(const DeviceScan::SubDevice& sd,
const DeviceConnection& dc);
- SubHidrawConnection(Token, const QString& path);
+ SubHidrawConnection(Token, const DeviceId&, const DeviceScan::SubDevice&);
+ virtual ~SubHidrawConnection();
+ virtual bool isConnected() const override;
+ virtual void disconnect() override;
+
+ // Generic plain, synchronous sendData implementation for hidraw devices.
+ ssize_t sendData(const QByteArray& msg) override;
+ ssize_t sendData(const void* msg, size_t msgLen) override;
+
+protected:
+ void createSocketNotifiers(int fd, const QString& path);
+ static int openHidrawSubDevice(const DeviceScan::SubDevice& sd, const DeviceId& devId);
+ std::unique_ptr m_writeNotifier;
+
+private:
+ void onHidrawDataAvailable(int fd);
};
diff --git a/src/deviceinput.cc b/src/deviceinput.cc
index 4ae5b6d5..7fc5a36e 100644
--- a/src/deviceinput.cc
+++ b/src/deviceinput.cc
@@ -1,6 +1,9 @@
-// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md
+// This file is part of Projecteur - https://github.com/jahnf/projecteur
+// - See LICENSE.md and README.md
+
#include "deviceinput.h"
+#include "enum-helper.h"
#include "logging.h"
#include "settings.h"
#include "virtualdevice.h"
@@ -17,8 +20,11 @@ LOGGING_CATEGORY(input, "input")
namespace {
// -----------------------------------------------------------------------------------------------
- static auto registered_ = qRegisterMetaTypeStreamOperators()
- && qRegisterMetaTypeStreamOperators();
+ #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
+ const auto registered_ = qRegisterMetaTypeStreamOperators()
+ && qRegisterMetaTypeStreamOperators();
+ #endif
+
// -----------------------------------------------------------------------------------------------
void addKeyToString(QString& str, const QString& key)
@@ -28,7 +34,8 @@ namespace {
}
// -----------------------------------------------------------------------------------------------
- QKeySequence makeQKeySequence(const std::vector& keys) {
+ QKeySequence makeQKeySequence(const std::vector& keys)
+ {
switch (keys.size()) {
case 4: return QKeySequence(keys[0], keys[1], keys[2], keys[3]);
case 3: return QKeySequence(keys[0], keys[1], keys[2]);
@@ -37,7 +44,44 @@ namespace {
}
return QKeySequence();
}
-}
+
+ // -----------------------------------------------------------------------------------------------
+ KeyEventSequence makeSpecialKeyEventSequence(uint16_t code)
+ {
+ // Special key event with 3 button presses of the same key,
+ // which should not be able with real events
+ KeyEvent pressed {
+ {EV_KEY, code, 1},
+ {EV_KEY, code, 1},
+ {EV_KEY, code, 1},
+ };
+
+ return KeyEventSequence{std::move(pressed)};
+ };
+
+ // -----------------------------------------------------------------------------------------------
+ bool isMouseEvent(const input_event* input_events, size_t num)
+ {
+ if (num < 2) {
+ // no events, or single SYN event
+ return false;
+ }
+
+ auto const& ev = [&]() -> input_event const& {
+ if (input_events[0].type == EV_MSC) {
+ return input_events[1];
+ }
+ return input_events[0];
+ }();
+
+ if (ev.type == EV_KEY && ev.code >= BTN_MISC && ev.code < KEY_OK) {
+ return true;
+ }
+
+ return false;
+ }
+
+} // end anonymous namespace
// -------------------------------------------------------------------------------------------------
DeviceInputEvent::DeviceInputEvent(const struct input_event& ie)
@@ -73,10 +117,10 @@ QDataStream& operator>>(QDataStream& s, DeviceInputEvent& die) {
}
// -------------------------------------------------------------------------------------------------
-QDebug operator<<(QDebug debug, const DeviceInputEvent &d)
+QDebug operator<<(QDebug debug, const DeviceInputEvent &ie)
{
QDebugStateSaver saver(debug);
- debug.nospace() << '{' << d.type << ", " << d.code << ", " << d.value << '}';
+ debug.nospace() << '{' << ie.type << ", " << ie.code << ", " << ie.value << '}';
return debug;
}
@@ -85,17 +129,44 @@ QDebug operator<<(QDebug debug, const KeyEvent &ke)
{
QDebugStateSaver saver(debug);
debug.nospace() << "[";
- for (const auto& e : ke)
+ for (const auto& e : ke) {
debug.nospace() << e << ',';
+ }
debug.nospace() << "]";
return debug;
}
// -------------------------------------------------------------------------------------------------
-QDataStream& operator>>(QDataStream& s, MappedAction& mia) {
- std::underlying_type_t type;
- s >> type;
- switch (static_cast(type))
+std::shared_ptr GlobalActions::scrollHorizontal()
+{
+ static auto scrollHorizontalAction = std::make_shared();
+ return scrollHorizontalAction;
+}
+
+// -------------------------------------------------------------------------------------------------
+std::shared_ptr GlobalActions::scrollVertical()
+{
+ static auto scrollVerticalAction = std::make_shared();
+ return scrollVerticalAction;
+}
+
+// -------------------------------------------------------------------------------------------------
+std::shared_ptr GlobalActions::volumeControl()
+{
+ static auto volumeControlAction = std::make_shared();
+ return volumeControlAction;
+}
+
+// -------------------------------------------------------------------------------------------------
+QDataStream& operator>>(QDataStream& s, MappedAction& mia)
+{
+ const auto type = [&s](){
+ auto type = to_integral(Action::Type::KeySequence);
+ s >> type;
+ return type;
+ }();
+
+ switch (to_enum(type))
{
case Action::Type::KeySequence:
mia.action = std::make_shared();
@@ -106,6 +177,15 @@ QDataStream& operator>>(QDataStream& s, MappedAction& mia) {
case Action::Type::ToggleSpotlight:
mia.action = std::make_shared();
return mia.action->load(s);
+ case Action::Type::ScrollHorizontal:
+ mia.action = std::make_shared();
+ return mia.action->load(s);
+ case Action::Type::ScrollVertical:
+ mia.action = std::make_shared