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 - + [![Cloudsmith OSS Hosting](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=for-the-badge)](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 @@ + + + + + + image/svg+xml + + + + + + + + + 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(); + return mia.action->load(s); + case Action::Type::VolumeControl: + mia.action = std::make_shared(); + return mia.action->load(s); } return s; } @@ -113,9 +193,9 @@ QDataStream& operator>>(QDataStream& s, MappedAction& mia) { // ------------------------------------------------------------------------------------------------- bool MappedAction::operator==(const MappedAction& o) const { - if (!action && !o.action) return true; - if (!action || !o.action) return false; - if (action->type() != o.action->type()) return false; + if (!action && !o.action) { return true; } + if (!action || !o.action) { return false; } + if (action->type() != o.action->type()) { return false; } switch(action->type()) { case Action::Type::KeySequence: @@ -127,6 +207,15 @@ bool MappedAction::operator==(const MappedAction& o) const case Action::Type::ToggleSpotlight: return (*static_cast(action.get())) == (*static_cast(o.action.get())); + case Action::Type::ScrollHorizontal: + return (*static_cast(action.get())) + == (*static_cast(o.action.get())); + case Action::Type::ScrollVertical: + return (*static_cast(action.get())) + == (*static_cast(o.action.get())); + case Action::Type::VolumeControl: + return (*static_cast(action.get())) + == (*static_cast(o.action.get())); } return false; @@ -141,7 +230,7 @@ QDataStream& operator<<(QDataStream& s, const MappedAction& mia) { // ------------------------------------------------------------------------------------------------- namespace { struct KeyEventItem { - KeyEventItem(KeyEvent ke = {}) : keyEvent(std::move(ke)) {} + explicit KeyEventItem(KeyEvent ke = {}) : keyEvent(std::move(ke)) {} const KeyEvent keyEvent; std::shared_ptr action; std::vector nextMap; @@ -149,7 +238,7 @@ namespace { struct DeviceKeyMap { - DeviceKeyMap(const InputMapConfig& config = {}) { reconfigure(config); } + explicit DeviceKeyMap(const InputMapConfig& config = {}) { reconfigure(config); } enum Result : uint8_t { Miss, Valid, Hit, PartialHit @@ -160,20 +249,20 @@ namespace { auto state() const { return m_pos; } void resetState(); void reconfigure(const InputMapConfig& config = {}); - bool hasConfig() const { return m_rootItem.nextMap.size(); } + bool hasConfig() const { return !m_rootItem.nextMap.empty(); } private: std::list m_items; KeyEventItem m_rootItem; const KeyEventItem* m_pos = &m_rootItem; }; -} +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- DeviceKeyMap::Result DeviceKeyMap::feed(const struct input_event input_events[], size_t num) { - if (!hasConfig()) return Result::Miss; - if (!m_pos) return Result::Miss; + if (!hasConfig()) { return Result::Miss; } + if (!m_pos) { return Result::Miss; } const auto ke = KeyEvent(KeyEvent(input_events, input_events + num)); const auto& nextMap = m_pos->nextMap; @@ -182,7 +271,7 @@ DeviceKeyMap::Result DeviceKeyMap::feed(const struct input_event input_events[], return next && ke == next->keyEvent; }); - if (find_it == nextMap.cend()) return Result::Miss; + if (find_it == nextMap.cend()) { return Result::Miss; } m_pos = (*find_it); @@ -192,7 +281,7 @@ DeviceKeyMap::Result DeviceKeyMap::feed(const struct input_event input_events[], } // KeyEvent in Sequence has action attached, but there are other possible sequences... - if (m_pos->action && !m_pos->action->empty()) { + if (m_pos->action) { return Result::PartialHit; } @@ -213,9 +302,12 @@ void DeviceKeyMap::reconfigure(const InputMapConfig& config) m_rootItem.nextMap.clear(); m_items.clear(); - // -- fill keymaps + // -- fill keymaps for (const auto& configItem : config) { + // sanity check + if (!configItem.second.action) { continue; } + KeyEventItem* previous = nullptr; KeyEventItem* current = &m_rootItem; const auto& kes = configItem.first; @@ -227,20 +319,20 @@ void DeviceKeyMap::reconfigure(const InputMapConfig& config) return (item && item->keyEvent == keyEvent); }); + previous = current; + if (it != current->nextMap.cend()) { - previous = current; current = *it; } else { // Create new item if not found m_items.emplace_back(KeyEventItem{keyEvent}); - previous = current; current = &m_items.back(); // link previous to current previous->nextMap.push_back(current); } - // if last item in key event set + // if last item in key event sequence if (i == kes.size() - 1) { current->action = configItem.second.action; } @@ -299,10 +391,17 @@ QString NativeKeySequence::toString() const const size_t size = count(); for (size_t i = 0; i < size; ++i) { - if (i > 0) seqString += QLatin1String(", "); - seqString += toString(m_keySequence[i], + if (i > 0) { seqString += QLatin1String(", "); } + + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + const auto key = m_keySequence[i]; + #else + const auto key = m_keySequence[i].key(); + #endif + + seqString += toString(key, (i < m_nativeModifiers.size()) ? m_nativeModifiers[i] - : (uint16_t)Modifier::NoModifier); + : to_integral(Modifier::NoModifier)); } return seqString; } @@ -375,10 +474,10 @@ QString NativeKeySequence::toString(const std::vector& qtKeys, const auto size = qtKeys.size(); for (size_t i = 0; i < size; ++i) { - if (i > 0) seqString += QLatin1String(", "); + if (i > 0) { seqString += QLatin1String(", "); } seqString += toString(qtKeys[i], (i < nativeModifiers.size()) ? nativeModifiers[i] - : (uint16_t)Modifier::NoModifier); + : to_integral(Modifier::NoModifier)); } return seqString; } @@ -451,20 +550,46 @@ const NativeKeySequence& NativeKeySequence::predefined::meta() return ks; } + +// ------------------------------------------------------------------------------------------------- +const char* toString(Action::Type at, bool withClass) +{ + using Type = Action::Type; + switch (at) { + ENUM_CASE_STRINGIFY3(Type, KeySequence, withClass); + ENUM_CASE_STRINGIFY3(Type, CyclePresets, withClass); + ENUM_CASE_STRINGIFY3(Type, ToggleSpotlight, withClass); + ENUM_CASE_STRINGIFY3(Type, ScrollHorizontal, withClass); + ENUM_CASE_STRINGIFY3(Type, ScrollVertical, withClass); + ENUM_CASE_STRINGIFY3(Type, VolumeControl, withClass); + } + return withClass ? "Type::(unknown)" : "(unkown)"; +} + // ------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------- struct InputMapper::Impl { - Impl(InputMapper* parent, std::shared_ptr vdev); + Impl(InputMapper* parent, + std::shared_ptr virtualMouse, + std::shared_ptr virtualKeybaord); void sequenceTimeout(); void resetState(); void record(const struct input_event input_events[], size_t num); void emitNativeKeySequence(const NativeKeySequence& ks); void execAction(const std::shared_ptr& action, DeviceKeyMap::Result r); + bool hasVirtualDevices() const; + + void forwardEvents(const struct input_event input_events[], size_t num); + void forwardEvents(const std::vector& input_events); InputMapper* m_parent = nullptr; - std::shared_ptr m_vdev; // can be a nullptr if application is started without uinput + + // virtual devices can be empty shared_ptr's if app is started without uinput + std::shared_ptr m_vmouse; + std::shared_ptr m_vkeyboard; + QTimer* m_seqTimer = nullptr; DeviceKeyMap m_keymap; @@ -472,26 +597,38 @@ struct InputMapper::Impl std::vector m_events; InputMapConfig m_config; bool m_recordingMode = false; + + SpecialMoveInputs m_specialMoveInputs; }; // ------------------------------------------------------------------------------------------------- -InputMapper::Impl::Impl(InputMapper* parent, std::shared_ptr vdev) +InputMapper::Impl::Impl(InputMapper* parent + , std::shared_ptr virtualMouse + , std::shared_ptr virtualKeyboard) : m_parent(parent) - , m_vdev(std::move(vdev)) + , m_vmouse(std::move(virtualMouse)) + , m_vkeyboard(std::move(virtualKeyboard)) , m_seqTimer(new QTimer(parent)) { + constexpr int defaultSequenceIntervalMs = 250; m_seqTimer->setSingleShot(true); - m_seqTimer->setInterval(250); + m_seqTimer->setInterval(defaultSequenceIntervalMs); connect(m_seqTimer, &QTimer::timeout, parent, [this](){ sequenceTimeout(); }); } +// ------------------------------------------------------------------------------------------------- +bool InputMapper::Impl::hasVirtualDevices() const +{ + return (m_vmouse && m_vkeyboard); +} + // ------------------------------------------------------------------------------------------------- void InputMapper::Impl::execAction(const std::shared_ptr& action, DeviceKeyMap::Result r) { - if (!action) return; + if (!action || action->empty()) { return; } - logDebug(input) << "Input map action, type = " << int(action->type()) - << ", partial_hit = " << (r == DeviceKeyMap::Result::PartialHit); + logDebug(input) << "Input map execAction, type =" << toString(action->type()) + << ", partial_hit =" << (r == DeviceKeyMap::Result::PartialHit); if (action->type() == Action::Type::KeySequence) { @@ -517,9 +654,9 @@ void InputMapper::Impl::sequenceTimeout() if (m_lastState.first == DeviceKeyMap::Result::Valid) { // Last input event was part of a valid key sequence, but timeout hit // So we emit our stored event so far to the virtual device - if (m_vdev && m_events.size()) + if (hasVirtualDevices() && !m_events.empty()) { - m_vdev->emitEvents(m_events); + forwardEvents(m_events); } resetState(); } @@ -530,9 +667,10 @@ void InputMapper::Impl::sequenceTimeout() { execAction(m_lastState.second->action, DeviceKeyMap::Result::PartialHit); } - else if (m_vdev && m_events.size()) + else if (hasVirtualDevices() && !m_events.empty()) { - m_vdev->emitEvents(m_events); + // TODO differentiate between mouse and keyboard events + forwardEvents(m_events); m_events.resize(0); } resetState(); @@ -549,16 +687,16 @@ void InputMapper::Impl::resetState() // ------------------------------------------------------------------------------------------------- void InputMapper::Impl::emitNativeKeySequence(const NativeKeySequence& ks) { - if (!m_vdev) return; + if (!m_vkeyboard) { return; } std::vector events; events.reserve(5); // up to 3 modifier keys + 1 key + 1 syn event for (const auto& ke : ks.nativeSequence()) { - for (const auto& ie : ke) + for (const auto& ie : ke) { events.emplace_back(input_event{{}, ie.type, ie.code, ie.value}); - - m_vdev->emitEvents(events); + } + m_vkeyboard->emitEvents(events); events.resize(0); } } @@ -576,28 +714,67 @@ void InputMapper::Impl::record(const struct input_event input_events[], size_t n } // ------------------------------------------------------------------------------------------------- +void InputMapper::Impl::forwardEvents(const std::vector& input_events) +{ + forwardEvents(input_events.data(), input_events.size()); +} + // ------------------------------------------------------------------------------------------------- -InputMapper::InputMapper(std::shared_ptr virtualDevice, QObject* parent) - : QObject(parent) - , impl(std::make_unique(this, std::move(virtualDevice))) +void InputMapper::Impl::forwardEvents(const struct input_event input_events[], size_t num) { + input_event const* beg = input_events; + input_event const* end = input_events + num; + + auto predicate = [](input_event const& e){ + return e.type == EV_SYN; + }; + + // handle each part separated by a SYN event + input_event const* syn = std::find_if(beg, end, predicate); + + while (syn != end) { + auto const len = std::distance(beg, syn) + 1; + + if (isMouseEvent(beg, len)) { + m_vmouse->emitEvents(beg, len); + } else { + m_vkeyboard->emitEvents(beg, len); + } + + beg = syn + 1; + syn = std::find_if(beg, end, predicate); + } } // ------------------------------------------------------------------------------------------------- -InputMapper::~InputMapper() +// ------------------------------------------------------------------------------------------------- +InputMapper::InputMapper( + std::shared_ptr virtualMouse + , std::shared_ptr virtualKeyboard + , QObject* parent) + : QObject(parent) + , impl(std::make_unique(this, std::move(virtualMouse), std::move(virtualKeyboard))) +{} + +// ------------------------------------------------------------------------------------------------- +InputMapper::~InputMapper() = default; + +// ------------------------------------------------------------------------------------------------- +std::shared_ptr InputMapper::virtualMouse() const { + return impl->m_vmouse; } // ------------------------------------------------------------------------------------------------- -std::shared_ptr InputMapper::virtualDevice() const +std::shared_ptr InputMapper::virtualKeyboard() const { - return impl->m_vdev; + return impl->m_vkeyboard; } // ------------------------------------------------------------------------------------------------- bool InputMapper::hasVirtualDevice() const { - return !!(impl->m_vdev); + return impl->hasVirtualDevices(); } // ------------------------------------------------------------------------------------------------- @@ -609,13 +786,12 @@ bool InputMapper::recordingMode() const // ------------------------------------------------------------------------------------------------- void InputMapper::setRecordingMode(bool recording) { - if (impl->m_recordingMode == recording) - return; + if (impl->m_recordingMode == recording) { return; } const auto wasRecording = (impl->m_recordingMode && impl->m_seqTimer->isActive()); impl->m_recordingMode = recording; - if (wasRecording) emit recordingFinished(true); + if (wasRecording) { emit recordingFinished(true); } impl->m_seqTimer->stop(); resetState(); emit recordingModeChanged(impl->m_recordingMode); @@ -637,20 +813,21 @@ void InputMapper::setKeyEventInterval(int interval) // ------------------------------------------------------------------------------------------------- void InputMapper::addEvents(const input_event* input_events, size_t num) { - if (num == 0 || (!impl->m_vdev)) return; + if (num == 0 || (!hasVirtualDevice())) { return; } // If no key mapping is configured ... if (!impl->m_recordingMode && !impl->m_keymap.hasConfig()) { - if (impl->m_vdev) { // ... forward events to virtual device if it exists... - impl->m_vdev->emitEvents(input_events, num); - } // ... end return + // ... forward events to virtual device + impl->forwardEvents(input_events, num); return; } if (input_events[num-1].type != EV_SYN) { logWarning(input) << tr("Input mapper expects events separated by SYN event."); return; - } else if (num == 1) { + } + + if (num == 1) { logWarning(input) << tr("Ignoring single SYN event received."); return; } @@ -675,55 +852,62 @@ void InputMapper::addEvents(const input_event* input_events, size_t num) const auto res = impl->m_keymap.feed(input_events, num-1); // exclude syn event for keymap feed + // Add current events to the buffered events + impl->m_events.reserve(impl->m_events.size() + num); + std::copy(input_events, input_events + num, std::back_inserter(impl->m_events)); + if (res == DeviceKeyMap::Result::Miss) - { // key sequence miss, send all buffered events so far + current event + { // key sequence miss, send all buffered events so far impl->m_seqTimer->stop(); - if (impl->m_vdev) - { - if (impl->m_events.size()) { - impl->m_vdev->emitEvents(impl->m_events); - impl->m_events.resize(0); - } - impl->m_vdev->emitEvents(input_events, num); - } - impl->m_keymap.resetState(); - } - else if (res == DeviceKeyMap::Result::Valid) - { // KeyEvent is part of valid key sequence. - impl->m_lastState = std::make_pair(res, impl->m_keymap.state()); - impl->m_seqTimer->start(); - if (impl->m_vdev) { - impl->m_events.reserve(impl->m_events.size() + num); - std::copy(input_events, input_events + num, std::back_inserter(impl->m_events)); - } + + impl->forwardEvents(impl->m_events); + + impl->resetState(); } else if (res == DeviceKeyMap::Result::Hit) { // Found a valid key sequence impl->m_seqTimer->stop(); - if (impl->m_vdev) - { - if (const auto pos = impl->m_keymap.state()) { - impl->execAction(pos->action, DeviceKeyMap::Result::Hit); - } - else - { - if (impl->m_events.size()) impl->m_vdev->emitEvents(impl->m_events); - impl->m_vdev->emitEvents(input_events, num); - } + if (const auto pos = impl->m_keymap.state()) { + impl->execAction(pos->action, res); } + else { + impl->forwardEvents(impl->m_events); + } + impl->resetState(); } - else if (res == DeviceKeyMap::Result::PartialHit) - { // Found a valid key sequence, but are still more valid sequences possible -> start timer + else if (res == DeviceKeyMap::Result::Valid || res == DeviceKeyMap::Result::PartialHit) + { // KeyEvent is either a part of valid key sequence or Partial Hit. + // In both case, save the current state and start timer impl->m_lastState = std::make_pair(res, impl->m_keymap.state()); impl->m_seqTimer->start(); - if (impl->m_vdev) { - impl->m_events.reserve(impl->m_events.size() + num); - std::copy(input_events, input_events + num, std::back_inserter(impl->m_events)); - } } } +// ------------------------------------------------------------------------------------------------- +void InputMapper::addEvents(const KeyEvent& key_event) +{ + if (key_event.empty()) { addEvents({}, 0); } + + static const auto to_input_event = [](const DeviceInputEvent& de){ + struct input_event ie = {{}, de.type, de.code, de.value}; + return ie; + }; + + // Check if key_event does have SYN event at end + const bool hasLastSYN = (key_event.back().type == EV_SYN); + + std::vector events; + events.reserve(key_event.size() + ((!hasLastSYN) ? 1 : 0)); + for (const auto& dev_input_event : key_event) { + events.emplace_back(to_input_event(dev_input_event)); + } + + if (!hasLastSYN) { events.emplace_back(input_event{{}, EV_SYN, SYN_REPORT, 0}); } + + addEvents(events.data(), events.size()); +} + // ------------------------------------------------------------------------------------------------- void InputMapper::resetState() { @@ -733,7 +917,7 @@ void InputMapper::resetState() // ------------------------------------------------------------------------------------------------- void InputMapper::setConfiguration(const InputMapConfig& config) { - if (config == impl->m_config) return; + if (config == impl->m_config) { return; } impl->m_config = config; impl->resetState(); @@ -744,7 +928,7 @@ void InputMapper::setConfiguration(const InputMapConfig& config) // ------------------------------------------------------------------------------------------------- void InputMapper::setConfiguration(InputMapConfig&& config) { - if (config == impl->m_config) return; + if (config == impl->m_config) { return; } impl->m_config.swap(config); impl->resetState(); @@ -758,4 +942,67 @@ const InputMapConfig& InputMapper::configuration() const return impl->m_config; } +// ------------------------------------------------------------------------------------------------- +const InputMapper::SpecialMoveInputs& InputMapper::specialMoveInputs() +{ + return impl->m_specialMoveInputs; +} + +// ------------------------------------------------------------------------------------------------- +void InputMapper::setSpecialMoveInputs(SpecialMoveInputs moveInputs) +{ + impl->m_specialMoveInputs = std::move(moveInputs); +} + +// ------------------------------------------------------------------------------------------------- +namespace SpecialKeys +{ +// ------------------------------------------------------------------------------------------------- +// Functions that provide all special event sequences for a device. +// Currently, special event seqences are only defined for the Logitech Spotlight device. +// Move type Key Sequences for the device are stored in +// InputMapper::Impl::m_specialMoveInputs by SubHidppConnection::updateDeviceFlags. +const std::map& keyEventSequenceMap() +{ + static const std::map keyMap { + {Key::NextHold, {InputMapper::tr("Next Hold"), + KeyEventSequence{{{EV_KEY, to_integral(Key::NextHold), 1}}}}}, + {Key::BackHold, {InputMapper::tr("Back Hold"), + KeyEventSequence{{{EV_KEY, to_integral(Key::BackHold), 1}}}}}, + {Key::NextHoldMove, {InputMapper::tr("Next Hold Move"), + makeSpecialKeyEventSequence(to_integral(Key::NextHoldMove)) }}, + {Key::BackHoldMove, {InputMapper::tr("Back Hold Move"), + makeSpecialKeyEventSequence(to_integral(Key::BackHoldMove))}}, + }; + return keyMap; +} + +// ------------------------------------------------------------------------------------------------- +const SpecialKeyEventSeqInfo& eventSequenceInfo(SpecialKeys::Key key) +{ + const auto it = keyEventSequenceMap().find(key); + if (it != keyEventSequenceMap().cend()) { + return it->second; + } + + static const SpecialKeyEventSeqInfo notFound; + return notFound; +} + +// ------------------------------------------------------------------------------------------------- +const SpecialKeyEventSeqInfo& logitechSpotlightHoldMove(const KeyEventSequence& inputSequence) +{ + const auto& specialKeysMap = SpecialKeys::keyEventSequenceMap(); + for (const auto& key : {SpecialKeys::Key::BackHoldMove, SpecialKeys::Key::NextHoldMove}) + { + const auto it = specialKeysMap.find(key); + if (it != specialKeysMap.cend() && it->second.keyEventSeq == inputSequence) { + return it->second; + } + } + + static const SpecialKeyEventSeqInfo notFound; + return notFound; +} +} // end namespace SpecialKeys diff --git a/src/deviceinput.h b/src/deviceinput.h index dbdc8881..71c4547a 100644 --- a/src/deviceinput.h +++ b/src/deviceinput.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 @@ -9,6 +10,7 @@ #include class VirtualDevice; +class QTimer; // ------------------------------------------------------------------------------------------------- /// This is basically the input_event struct from linux/input.h without the time member @@ -38,14 +40,6 @@ struct DeviceInputEvent QDataStream& operator<<(QDataStream& s, const DeviceInputEvent& die); QDataStream& operator>>(QDataStream& s, DeviceInputEvent& die); -// ------------------------------------------------------------------------------------------------- -/// KeyEvent is a sequence of DeviceInputEvent. -using KeyEvent = std::vector; - -/// KeyEventSequence is a sequence of KeyEvents. -using KeyEventSequence = std::vector; -Q_DECLARE_METATYPE(KeyEventSequence); - // ------------------------------------------------------------------------------------------------- template QDataStream& operator<<(QDataStream& s, const std::vector& container) @@ -69,10 +63,43 @@ QDataStream& operator>>(QDataStream& s, std::vector& container) return s; } +// ------------------------------------------------------------------------------------------------- +/// KeyEvent is a sequence of DeviceInputEvent. +using KeyEvent = std::vector; + +/// KeyEventSequence is a sequence of KeyEvents. +using KeyEventSequence = std::vector; +Q_DECLARE_METATYPE(KeyEventSequence); + // ------------------------------------------------------------------------------------------------- QDebug operator<<(QDebug debug, const DeviceInputEvent &ie); QDebug operator<<(QDebug debug, const KeyEvent &ke); +// ------------------------------------------------------------------------------------------------- +// Some inputs from Logitech Spotlight device (like Next Hold and Back Hold events) are not a valid +// input event (input_event in linux/input.h) in a conventional sense. They are communicated +// via HID++ messages from the device. Using the input mapper we need to +// reserve some KeyEventSequence for these events. These KeyEventSequence should be designed in +// such a way that they cannot interfere with other valid input events from the device. +namespace SpecialKeys +{ + enum class Key : uint16_t { + NextHold = 0x0e10, + BackHold = 0x0e11, + NextHoldMove = 0x0ff0, + BackHoldMove = 0x0ff1, + }; + + struct SpecialKeyEventSeqInfo { + QString name; + KeyEventSequence keyEventSeq; + }; + + const SpecialKeyEventSeqInfo& logitechSpotlightHoldMove(const KeyEventSequence& inputSequence); + const SpecialKeyEventSeqInfo& eventSequenceInfo(SpecialKeys::Key key); + const std::map& keyEventSequenceMap(); +} + // ------------------------------------------------------------------------------------------------- class NativeKeySequence { @@ -93,7 +120,7 @@ class NativeKeySequence NativeKeySequence(NativeKeySequence&&) = default; NativeKeySequence(const NativeKeySequence&) = default; - NativeKeySequence(const std::vector& qtKeys, + NativeKeySequence(const std::vector& qtKeys, std::vector&& nativeModifiers, KeyEventSequence&& kes); @@ -143,14 +170,22 @@ struct Action KeySequence = 1, CyclePresets = 2, ToggleSpotlight = 3, + ScrollHorizontal = 11, + ScrollVertical = 12, + VolumeControl = 13, }; + virtual ~Action() = default; + virtual Type type() const = 0; virtual QDataStream& save(QDataStream&) const = 0; virtual QDataStream& load(QDataStream&) = 0; virtual bool empty() const = 0; }; +// ------------------------------------------------------------------------------------------------- +const char* toString(Action::Type at, bool withClass = true); + // ------------------------------------------------------------------------------------------------- struct KeySequenceAction : public Action { @@ -187,6 +222,52 @@ struct ToggleSpotlightAction : public Action bool placeholder = false; }; +// ------------------------------------------------------------------------------------------------- +struct ScrollHorizontalAction : public Action +{ + Type type() const override { return Type::ScrollHorizontal; } + QDataStream& save(QDataStream& s) const override { return s << placeholder; } + QDataStream& load(QDataStream& s) override { return s >> placeholder; } + bool empty() const override { return false; } + bool operator==(const ScrollHorizontalAction&) const { return true; } + bool placeholder = false; + + int param = 0; +}; + +// ------------------------------------------------------------------------------------------------- +struct ScrollVerticalAction : public Action +{ + Type type() const override { return Type::ScrollVertical; } + QDataStream& save(QDataStream& s) const override { return s << placeholder; } + QDataStream& load(QDataStream& s) override { return s >> placeholder; } + bool empty() const override { return false; } + bool operator==(const ScrollVerticalAction&) const { return true; } + bool placeholder = false; + + int param = 0; +}; + +// ------------------------------------------------------------------------------------------------- +struct VolumeControlAction : public Action +{ + Type type() const override { return Type::VolumeControl; } + QDataStream& save(QDataStream& s) const override { return s << placeholder; } + QDataStream& load(QDataStream& s) override { return s >> placeholder; } + bool empty() const override { return false; } + bool operator==(const VolumeControlAction&) const { return true; } + bool placeholder = false; + + int param = 0; +}; + +// ------------------------------------------------------------------------------------------------- +namespace GlobalActions { + std::shared_ptr scrollHorizontal(); + std::shared_ptr scrollVertical(); + std::shared_ptr volumeControl(); +} + // ------------------------------------------------------------------------------------------------- struct MappedAction { @@ -207,13 +288,18 @@ class InputMapper : public QObject Q_OBJECT public: - InputMapper(std::shared_ptr virtualDevice, QObject* parent = nullptr); + InputMapper( + std::shared_ptr virtualMouse, + std::shared_ptr virtualKeyboard, + QObject* parent = nullptr); + ~InputMapper(); void resetState(); // Reset any stored sequence state. // input_events = complete sequence including SYN event void addEvents(const struct input_event input_events[], size_t num); + void addEvents(const KeyEvent& key_events); bool recordingMode() const; void setRecordingMode(bool recording); @@ -221,7 +307,12 @@ class InputMapper : public QObject int keyEventInterval() const; void setKeyEventInterval(int interval); - std::shared_ptr virtualDevice() const; + using SpecialMoveInputs = std::vector; + const SpecialMoveInputs& specialMoveInputs(); + void setSpecialMoveInputs(SpecialMoveInputs moveInputs); + + std::shared_ptr virtualMouse() const; + std::shared_ptr virtualKeyboard() const; bool hasVirtualDevice() const; void setConfiguration(const InputMapConfig& config); diff --git a/src/devicescan.cc b/src/devicescan.cc index 84374bee..32f1cc2d 100644 --- a/src/devicescan.cc +++ b/src/devicescan.cc @@ -1,6 +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 "devicescan.h" +#include + #include #include #include @@ -52,16 +56,16 @@ namespace { [vendorId, productId](const SupportedDevice& d) { return (vendorId == d.vendorId) && (productId == d.productId); }); - if (it != supportedDefaultDevices.cend() && it->name.size()) return it->name; + if (it != supportedDefaultDevices.cend() && it->name.size()) { return it->name; } auto extraName = getExtraDeviceName(vendorId, productId); - if (!extraName.isEmpty()) return extraName; + if (!extraName.isEmpty()) { return extraName; } const auto ait = std::find_if(additionalDevices.cbegin(), additionalDevices.cend(), [vendorId, productId](const SupportedDevice& d) { return (vendorId == d.vendorId) && (productId == d.productId); }); - if (ait != additionalDevices.cend() && ait->name.size()) return ait->name; + if (ait != additionalDevices.cend() && ait->name.size()) { return ait->name; } return QString(); } @@ -125,27 +129,28 @@ namespace { const auto line = in.readLine(); for (const auto property : properties) { - if (line.startsWith(property) && line.size() > property->size() && line[property->size()] == '=') + if (line.startsWith(*property) && line.size() > property->size() && line[property->size()] == '=') { const QString value = line.mid(property->size() + 1); - if (property == hid_id) + if (*property == hid_id) { const auto ids = value.split(':'); - const auto busType = ids.size() ? ids[0].toUShort(nullptr, 16) : 0; + const auto busType = ids.empty() ? 0: ids[0].toUShort(nullptr, 16); switch (busType) { - case BUS_USB: spotlightDevice.busType = DeviceScan::Device::BusType::Usb; break; - case BUS_BLUETOOTH: spotlightDevice.busType = DeviceScan::Device::BusType::Bluetooth; break; + case BUS_USB: spotlightDevice.id.busType = BusType::Usb; break; + case BUS_BLUETOOTH: spotlightDevice.id.busType = BusType::Bluetooth; break; + default: spotlightDevice.id.busType = BusType::Unknown; } spotlightDevice.id.vendorId = ids.size() > 1 ? ids[1].toUShort(nullptr, 16) : 0; spotlightDevice.id.productId = ids.size() > 2 ? ids[2].toUShort(nullptr, 16) : 0; } - else if (property == hid_name) + else if (*property == hid_name) { spotlightDevice.name = value; } - else if (property == hid_phys) + else if (*property == hid_phys) { spotlightDevice.id.phys = value.split('/').first(); } @@ -154,8 +159,7 @@ namespace { } return spotlightDevice; } - -} +} // end anonymous namespace namespace DeviceScan { // ----------------------------------------------------------------------------------------------- @@ -182,15 +186,15 @@ namespace DeviceScan { hidIt.next(); const QFileInfo uEventFile(QDir(hidIt.filePath()).filePath("uevent")); - if (!uEventFile.exists()) continue; + if (!uEventFile.exists()) { continue; } // Get basic information from uevent file Device newDevice = deviceFromUEventFile(uEventFile.filePath()); const auto& deviceId = newDevice.id; // Skip unsupported devices - if (deviceId.vendorId == 0 || deviceId.productId == 0) continue; + if (deviceId.vendorId == 0 || deviceId.productId == 0) { continue; } if (!isDeviceSupported(deviceId.vendorId, deviceId.productId) - && !(isAdditionallySupported(deviceId.vendorId, deviceId.productId, additionalDevices))) continue; + && !(isAdditionallySupported(deviceId.vendorId, deviceId.productId, additionalDevices))) { continue; } // Check if device is already in list (and we have another sub-device for it) const auto find_it = std::find_if(result.devices.begin(), result.devices.end(), @@ -225,7 +229,7 @@ namespace DeviceScan { while (dirIt.hasNext()) { dirIt.next(); - if (!dirIt.fileName().startsWith("event")) continue; + if (!dirIt.fileName().startsWith("event")) { continue; } subDevice.type = SubDevice::Type::Event; subDevice.deviceFile = readPropertyFromDeviceFile(QDir(dirIt.filePath()).filePath("uevent"), "DEVNAME"); if (!subDevice.deviceFile.isEmpty()) { @@ -234,7 +238,7 @@ namespace DeviceScan { } } - if (subDevice.deviceFile.isEmpty()) continue; + if (subDevice.deviceFile.isEmpty()) { continue; } subDevice.phys = readStringFromDeviceFile(QDir(inputIt.filePath()).filePath("phys")); ++eventSubDeviceCount; @@ -257,10 +261,9 @@ namespace DeviceScan { } } - // For now: only check for hidraw sub-devices that have support for custom "proprietary" - // functionality/protocol with Projecteur built in. - // TODO check if _Projecteur_ supports additional "proprietary" device protocol features.. - if (eventSubDeviceCount > 0) continue; + // Spotlight (Bluetooth) have hidraw interface in the same folder. However + // for other connection, it has separate folder for hidraw device and input device. + if (!(rootDevice.id.busType == BusType::Bluetooth) && eventSubDeviceCount > 0) { continue; } // Iterate over 'hidraw' sub-dircectory, check for hidraw device node const QFileInfo hidrawSubdir(QDir(hidIt.filePath()).filePath("hidraw")); @@ -270,13 +273,13 @@ namespace DeviceScan { while (hidrawIt.hasNext()) { hidrawIt.next(); - if (!hidrawIt.fileName().startsWith("hidraw")) continue; + if (!hidrawIt.fileName().startsWith("hidraw")) { continue; } SubDevice subDevice; subDevice.deviceFile = readPropertyFromDeviceFile(QDir(hidrawIt.filePath()).filePath("uevent"), "DEVNAME"); if (!subDevice.deviceFile.isEmpty()) { subDevice.type = SubDevice::Type::Hidraw; subDevice.deviceFile = QDir("/dev").filePath(subDevice.deviceFile); - if (subDevice.deviceFile.isEmpty()) continue; + if (subDevice.deviceFile.isEmpty()) { continue; } const QFileInfo fi(subDevice.deviceFile); subDevice.deviceReadable = fi.isReadable(); subDevice.deviceWritable = fi.isWritable(); @@ -305,4 +308,4 @@ namespace DeviceScan { return result; } -} +} // end namespace DeviceScan diff --git a/src/devicescan.h b/src/devicescan.h index ea1b0c6c..90cb7548 100644 --- a/src/devicescan.h +++ b/src/devicescan.h @@ -1,12 +1,14 @@ -// 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 "device.h" +#include "device-defs.h" -#include #include +#include #include +#include // ------------------------------------------------------------------------------------------------- struct SupportedDevice @@ -31,12 +33,10 @@ namespace DeviceScan }; struct Device { // Structure for device scan results - enum class BusType : uint16_t { Unknown, Usb, Bluetooth }; const QString& getName() const { return userName.size() ? userName : name; } QString name; QString userName; DeviceId id; - BusType busType = BusType::Unknown; std::vector subDevices; }; diff --git a/src/deviceswidget.cc b/src/deviceswidget.cc index 080a5747..6cc57b54 100644 --- a/src/deviceswidget.cc +++ b/src/deviceswidget.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 "deviceswidget.h" +#include "device-hidpp.h" #include "device-vibration.h" #include "deviceinput.h" #include "iconwidgets.h" @@ -17,6 +20,9 @@ #include #include #include +#include +#include +#include DECLARE_LOGGING_CATEGORY(preferences) @@ -29,7 +35,17 @@ namespace { } const auto invalidDeviceId = DeviceId(); // vendorId = 0, productId = 0 -} + + bool removeTab(QTabWidget* tabWidget, QWidget* widget) + { + const auto idx = tabWidget->indexOf(widget); + if (idx >= 0) { + tabWidget->removeTab(idx); + return true; + } + return false; + } +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- DevicesWidget::DevicesWidget(Settings* settings, Spotlight* spotlight, QWidget* parent) @@ -53,74 +69,26 @@ DevicesWidget::DevicesWidget(Settings* settings, Spotlight* spotlight, QWidget* } // ------------------------------------------------------------------------------------------------- -const DeviceId DevicesWidget::currentDeviceId() const +DeviceId DevicesWidget::currentDeviceId() const { - if (m_devicesCombo->currentIndex() < 0) + if (m_devicesCombo->currentIndex() < 0) { return invalidDeviceId; + } return qvariant_cast(m_devicesCombo->currentData()); } // ------------------------------------------------------------------------------------------------- -QWidget* DevicesWidget::createTimerTabWidget(Settings* settings, Spotlight* spotlight) +TimerTabWidget* DevicesWidget::createTimerTabWidget(Settings* settings, Spotlight* spotlight) { - Q_UNUSED(settings); Q_UNUSED(spotlight); + const auto w = new TimerTabWidget(settings, this); + w->loadSettings(currentDeviceId()); - const auto w = new QWidget(this); - const auto layout = new QVBoxLayout(w); - const auto timerWidget = new MultiTimerWidget(this); - m_vibrationSettingsWidget = new VibrationSettingsWidget(this); - - layout->addWidget(timerWidget); - layout->addWidget(m_vibrationSettingsWidget); - - auto loadSettings = [this, settings, timerWidget](const DeviceId& dId) { - for (int i = 0; i < timerWidget->timerCount(); ++i) { - const auto ts = settings->timerSettings(dId, i); - timerWidget->setTimerEnabled(i, ts.first); - timerWidget->setTimerValue(i, ts.second); - } - const auto vs = settings->vibrationSettings(dId); - m_vibrationSettingsWidget->setLength(vs.first); - m_vibrationSettingsWidget->setIntensity(vs.second); - }; - - loadSettings(currentDeviceId()); - - connect(this, &DevicesWidget::currentDeviceChanged, this, - [loadSettings=std::move(loadSettings), timerWidget, this](const DeviceId& dId) { - timerWidget->stopAllTimers(); - timerWidget->blockSignals(true); - m_vibrationSettingsWidget->blockSignals(true); - loadSettings(dId); - m_vibrationSettingsWidget->blockSignals(false); - timerWidget->blockSignals(false); - }); - - connect(timerWidget, &MultiTimerWidget::timerValueChanged, this, - [timerWidget, settings, this](int id, int secs) { - settings->setTimerSettings(currentDeviceId(), id, timerWidget->timerEnabled(id), secs); - }); - - connect(timerWidget, &MultiTimerWidget::timerEnabledChanged, this, - [timerWidget, settings, this](int id, bool enabled) { - settings->setTimerSettings(currentDeviceId(), id, enabled, timerWidget->timerValue(id)); - }); - - connect(m_vibrationSettingsWidget, &VibrationSettingsWidget::intensityChanged, this, - [settings, this](uint8_t intensity) { - settings->setVibrationSettings(currentDeviceId(), m_vibrationSettingsWidget->length(), intensity); - }); - - connect(m_vibrationSettingsWidget, &VibrationSettingsWidget::lengthChanged, this, - [settings, this](uint8_t len) { - settings->setVibrationSettings(currentDeviceId(), len, m_vibrationSettingsWidget->intensity()); + connect(this, &DevicesWidget::currentDeviceChanged, this, [this](const DeviceId& dId) { + if (m_timerTabWidget) { m_timerTabWidget->loadSettings(dId); } }); - connect(timerWidget, &MultiTimerWidget::timeout, - m_vibrationSettingsWidget, &VibrationSettingsWidget::sendVibrateCommand); - return w; } @@ -138,65 +106,42 @@ QWidget* DevicesWidget::createDevicesWidget(Settings* settings, Spotlight* spotl vLayout->addSpacing(10); - const auto tabWidget = new QTabWidget(dw); - vLayout->addWidget(tabWidget); + m_tabWidget = new QTabWidget(dw); + vLayout->addWidget(m_tabWidget); - tabWidget->addTab(createInputMapperWidget(settings, spotlight), tr("Input Mapping")); + m_tabWidget->addTab(createInputMapperWidget(settings, spotlight), tr("Input Mapping")); + m_timerTabWidget = createTimerTabWidget(settings, spotlight); - auto vibrateConn = [spotlight](const DeviceId& devId) { - const auto currentConn = spotlight->deviceConnection(devId); - if (currentConn) { - for (const auto& item : currentConn->subDevices()) { - if ((item.second->flags() & DeviceFlag::Vibrate) == DeviceFlag::Vibrate) return item.second; - } - } - return std::shared_ptr{}; - }; + updateTimerTab(spotlight); - if (const auto conn = vibrateConn(currentDeviceId())) { - m_timerTabWidget = createTimerTabWidget(settings, spotlight); - tabWidget->addTab(m_timerTabWidget, tr("Vibration Timer")); - m_vibrationSettingsWidget->setSubDeviceConnection(conn.get()); - } + m_deviceDetailsTabWidget = createDeviceInfoWidget(spotlight); + m_tabWidget->addTab(m_deviceDetailsTabWidget, tr("Details")); + // Update the timer tab when the current device has changed connect(this, &DevicesWidget::currentDeviceChanged, this, - [vibrateConn=std::move(vibrateConn), tabWidget, settings, spotlight, this] - (const DeviceId& devId) { - if (const auto conn = vibrateConn(devId)) { - if (m_timerTabWidget == nullptr) { - m_timerTabWidget = createTimerTabWidget(settings, spotlight); - } - if (tabWidget->indexOf(m_timerTabWidget) < 0) { - tabWidget->addTab(m_timerTabWidget, tr("Vibration Timer")); - } - m_vibrationSettingsWidget->setSubDeviceConnection(conn.get()); - } - else if (m_timerTabWidget) { - const auto idx = tabWidget->indexOf(m_timerTabWidget); - if (idx >= 0) tabWidget->removeTab(idx); - m_vibrationSettingsWidget->setSubDeviceConnection(nullptr); - } - }); + [spotlight, this]() { updateTimerTab(spotlight); }); return dw; } // ------------------------------------------------------------------------------------------------- -QWidget* DevicesWidget::createDeviceInfoWidget(Spotlight* /*spotlight*/) +QWidget* DevicesWidget::createDeviceInfoWidget(Spotlight* spotlight) { - const auto diWidget = new QWidget(this); - const auto layout = new QHBoxLayout(diWidget); - layout->addStretch(1); - layout->addWidget(new QLabel(tr("Not yet implemented"), this)); - layout->addStretch(1); - diWidget->setDisabled(true); + const auto diWidget = new DeviceInfoWidget(this); + + connect(this, &DevicesWidget::currentDeviceChanged, this, + [diWidget, spotlight](const DeviceId& dId) { + diWidget->setDeviceConnection(spotlight->deviceConnection(dId).get()); + }); + + diWidget->setDeviceConnection(spotlight->deviceConnection(currentDeviceId()).get()); return diWidget; } // ------------------------------------------------------------------------------------------------- QWidget* DevicesWidget::createInputMapperWidget(Settings* settings, Spotlight* /*spotlight*/) { - const auto delShortcut = new QShortcut( QKeySequence(Qt::ShiftModifier + Qt::Key_Delete), this); + const auto delShortcut = new QShortcut( QKeySequence(Qt::ShiftModifier | Qt::Key_Delete), this); const auto imWidget = new QWidget(this); const auto layout = new QVBoxLayout(imWidget); @@ -226,8 +171,8 @@ QWidget* DevicesWidget::createInputMapperWidget(Settings* settings, Spotlight* / intervalLayout->addWidget(intervalUnitLbl); const auto tblView = new InputMapConfigView(imWidget); - const auto imModel = new InputMapConfigModel(m_inputMapper, imWidget); - if (m_inputMapper) imModel->setConfiguration(m_inputMapper->configuration()); + const auto imModel = new InputMapConfigModel(m_inputMapper, currentDeviceId(), imWidget); + if (m_inputMapper) { imModel->setConfiguration(m_inputMapper->configuration()); } tblView->setModel(imModel); const auto selectionModel = tblView->selectionModel(); @@ -238,11 +183,13 @@ QWidget* DevicesWidget::createInputMapperWidget(Settings* settings, Spotlight* / updateImWidget(); connect(this, &DevicesWidget::currentDeviceChanged, this, - [this, imModel, intervalSb, updateImWidget=std::move(updateImWidget)](){ + [this, imModel, intervalSb, updateImWidget=std::move(updateImWidget)](const DeviceId& dId) + { imModel->setInputMapper(m_inputMapper); if (m_inputMapper) { intervalSb->setValue(m_inputMapper->keyEventInterval()); imModel->setConfiguration(m_inputMapper->configuration()); + imModel->setDeviceId(dId); } updateImWidget(); }); @@ -352,3 +299,443 @@ QWidget* DevicesWidget::createDisconnectedStateWidget() return stateWidget; } +// ------------------------------------------------------------------------------------------------- +TimerTabWidget::TimerTabWidget(Settings* settings, QWidget* parent) + : QWidget(parent) + , m_settings(settings) + , m_multiTimerWidget(new MultiTimerWidget(this)) + , m_vibrationSettingsWidget(new VibrationSettingsWidget(this)) +{ + const auto layout = new QVBoxLayout(this); + + layout->addWidget(m_multiTimerWidget); + layout->addWidget(m_vibrationSettingsWidget); + + connect(m_multiTimerWidget, &MultiTimerWidget::timerValueChanged, this, + [this](int id, int secs) { + m_settings->setTimerSettings(m_deviceId, id, m_multiTimerWidget->timerEnabled(id), secs); + }); + + connect(m_multiTimerWidget, &MultiTimerWidget::timerEnabledChanged, this, + [this](int id, bool enabled) { + m_settings->setTimerSettings(m_deviceId, id, enabled, m_multiTimerWidget->timerValue(id)); + }); + + connect(m_vibrationSettingsWidget, &VibrationSettingsWidget::intensityChanged, this, + [this](uint8_t intensity) { + m_settings->setVibrationSettings(m_deviceId, m_vibrationSettingsWidget->length(), intensity); + }); + + connect(m_vibrationSettingsWidget, &VibrationSettingsWidget::lengthChanged, this, + [this](uint8_t len) { + m_settings->setVibrationSettings(m_deviceId, len, m_vibrationSettingsWidget->intensity()); + }); + + connect(m_multiTimerWidget, &MultiTimerWidget::timeout, + m_vibrationSettingsWidget, &VibrationSettingsWidget::sendVibrateCommand); +} + +// ------------------------------------------------------------------------------------------------- +void DevicesWidget::updateTimerTab(Spotlight* spotlight) +{ + // Helper method to return the first subconnection that supports vibrate. + auto getVibrateConnection = [](const std::shared_ptr& conn) { + if (conn) { + for (const auto& item : conn->subDevices()) { + if (item.second->hasFlags(DeviceFlag::Vibrate)) { return item.second; } + } + } + return std::shared_ptr{}; + }; + + const auto currentConn = spotlight->deviceConnection(currentDeviceId()); + const auto vibrateConn = getVibrateConnection(currentConn); + + if (m_timerTabContext) { m_timerTabContext->deleteLater(); } + + if (vibrateConn) + { + if (m_tabWidget->indexOf(m_timerTabWidget) < 0) { + m_tabWidget->insertTab(1, m_timerTabWidget, tr("Vibration Timer")); + } + m_timerTabWidget->setSubDeviceConnection(vibrateConn.get()); + } + else if (m_timerTabWidget) { + removeTab(m_tabWidget, m_timerTabWidget); + m_timerTabWidget->setSubDeviceConnection(nullptr); + } + + if (currentConn) { + m_timerTabContext = QPointer(new QObject(this)); + connect(&*currentConn, &DeviceConnection::subDeviceFlagsChanged, m_timerTabContext, + [currId=currentDeviceId(), spotlight, this](const DeviceId& id, const QString& /* path */) { + if (currId != id) { return; } + updateTimerTab(spotlight); + }); + } + +} + +// ------------------------------------------------------------------------------------------------- +void TimerTabWidget::loadSettings(const DeviceId& deviceId) +{ + m_multiTimerWidget->stopAllTimers(); + m_multiTimerWidget->blockSignals(true); + m_vibrationSettingsWidget->blockSignals(true); + + m_deviceId = deviceId; + + for (int i = 0; i < m_multiTimerWidget->timerCount(); ++i) { + const auto ts = m_settings->timerSettings(deviceId, i); + m_multiTimerWidget->setTimerEnabled(i, ts.first); + m_multiTimerWidget->setTimerValue(i, ts.second); + } + + const auto vs = m_settings->vibrationSettings(deviceId); + m_vibrationSettingsWidget->setLength(vs.first); + m_vibrationSettingsWidget->setIntensity(vs.second); + + m_vibrationSettingsWidget->blockSignals(false); + m_multiTimerWidget->blockSignals(false); +} + +// ------------------------------------------------------------------------------------------------- +void TimerTabWidget::setSubDeviceConnection(SubDeviceConnection* sdc) { + m_vibrationSettingsWidget->setSubDeviceConnection(sdc); +} + +// ------------------------------------------------------------------------------------------------- +DeviceInfoWidget::DeviceInfoWidget(QWidget* parent) + : QWidget(parent) + , m_textEdit(new QTextEdit(this)) + , m_delayedUpdateTimer(new QTimer(this)) + , m_batteryInfoTimer(new QTimer(this)) +{ + m_textEdit->setReadOnly(true); + + const auto layout = new QVBoxLayout(this); + layout->addWidget(m_textEdit); + + constexpr int delayedUpdateTimerInterval = 150; + m_delayedUpdateTimer->setSingleShot(true); + m_delayedUpdateTimer->setInterval(delayedUpdateTimerInterval); + connect(m_delayedUpdateTimer, &QTimer::timeout, this, &DeviceInfoWidget::updateTextEdit); + + m_batteryInfoTimer->setSingleShot(false); + m_batteryInfoTimer->setTimerType(Qt::VeryCoarseTimer); + m_batteryInfoTimer->setInterval(5 * 60 * 1000); // 5 minutes +} + +// ------------------------------------------------------------------------------------------------- +void DeviceInfoWidget::delayedTextEditUpdate() { + m_delayedUpdateTimer->start(); +} + +// ------------------------------------------------------------------------------------------------- +void DeviceInfoWidget::setDeviceConnection(DeviceConnection* connection) +{ + if (m_connection == connection) { return; } + if (m_connectionContext) { m_connectionContext->deleteLater(); } + + m_connection = connection; + + if (m_connection.isNull()) + { + m_delayedUpdateTimer->stop(); + m_batteryInfoTimer->stop(); + m_textEdit->clear(); + return; + } + + m_connectionContext = new QObject(this); + + m_deviceBaseInfo.clear(); + m_deviceBaseInfo.emplace_back("Name", m_connection->deviceName()); + m_deviceBaseInfo.emplace_back("VendorId", hexId(m_connection->deviceId().vendorId)); + m_deviceBaseInfo.emplace_back("ProductId", hexId(m_connection->deviceId().productId)); + m_deviceBaseInfo.emplace_back("Phys", m_connection->deviceId().phys); + m_deviceBaseInfo.emplace_back("Bus Type", toString(m_connection->deviceId().busType, false)); + + connect(m_connection, &DeviceConnection::subDeviceConnected, m_connectionContext, + [this](const DeviceId& /* deviceId */, const QString& path) + { + if (const auto sdc = m_connection->subDevice(path)) + { + updateSubdeviceInfo(sdc.get()); + connectToSubdeviceUpdates(sdc.get()); + delayedTextEditUpdate(); + } + }); + + connect(m_connection, &DeviceConnection::subDeviceConnected, m_connectionContext, + [this](const DeviceId& /* deviceId */, const QString& path) + { + const auto it = m_subDevices.find(path); + if (it == m_subDevices.cend()) { + return; + } + + if (it->second.isHidpp) { + m_hidppInfo.clear(); + } + + if (it->second.hasBatteryInfo) { + m_batteryInfo.clear(); + m_batteryInfoTimer->stop(); + } + + m_subDevices.erase(it); + delayedTextEditUpdate(); + }); + + initSubdeviceInfo(); + updateTextEdit(); +} + +// ------------------------------------------------------------------------------------------------- +void DeviceInfoWidget::connectToBatteryUpdates(SubHidppConnection* hdc) +{ + if (hdc->hasFlags(DeviceFlag::ReportBattery)) + { + connect(hdc, &SubHidppConnection::batteryInfoChanged, m_connectionContext, [this, hdc]() { + updateBatteryInfo(hdc); + m_batteryInfoTimer->start(); + delayedTextEditUpdate(); + }); + + connect(m_batteryInfoTimer, &QTimer::timeout, m_connectionContext, [hdc]() { + hdc->triggerBattyerInfoUpdate(); + }); + } +} + +// ------------------------------------------------------------------------------------------------- +void DeviceInfoWidget::connectToSubdeviceUpdates(SubDeviceConnection* sdc) +{ + connect(sdc, &SubDeviceConnection::flagsChanged, m_connectionContext, [this, sdc]() + { + if (!m_subDevices[sdc->path()].hasBatteryInfo + && sdc->hasFlags(DeviceFlag::ReportBattery)) + { + if (const auto hdc = qobject_cast(sdc)) { + connectToBatteryUpdates(hdc); + hdc->triggerBattyerInfoUpdate(); + } + } + + updateSubdeviceInfo(sdc); + if (const auto hdc = qobject_cast(sdc)) { + updateHidppInfo(hdc); + delayedTextEditUpdate(); + } + }); + + // HID++ device only updates + if (const auto hdc = qobject_cast(sdc)) + { + connectToBatteryUpdates(hdc); + + if (hdc->busType() == BusType::Usb) + { + connect(hdc, &SubHidppConnection::receiverStateChanged, m_connectionContext, + [this](SubHidppConnection::ReceiverState s) { + m_hidppInfo.receiverState = toString(s, false); + delayedTextEditUpdate(); + }); + } + + connect(hdc, &SubHidppConnection::presenterStateChanged, m_connectionContext, + [this, hdc](SubHidppConnection::PresenterState s) { + m_hidppInfo.presenterState = toString(s, false); + const auto pv = hdc->protocolVersion(); + m_hidppInfo.protocolVersion = QString("%1.%2").arg(pv.major).arg(pv.minor); + delayedTextEditUpdate(); + }); + } +} + +// ------------------------------------------------------------------------------------------------- +void DeviceInfoWidget::updateTextEdit() +{ + m_textEdit->clear(); + + QTextCharFormat normalFormat; + normalFormat.setFontUnderline(false); + QTextCharFormat underlineFormat; + underlineFormat.setFontUnderline(true); + QTextCharFormat italicFormat; + italicFormat.setFontItalic(true); + + auto cursor = m_textEdit->textCursor(); + + { // Insert table with basic device information + QTextTableFormat tableFormat; + tableFormat.setBorder(1); + tableFormat.setCellSpacing(0); + tableFormat.setBorderBrush(QBrush(Qt::lightGray)); + tableFormat.setCellPadding(2); + tableFormat.setBorderStyle(QTextFrameFormat::BorderStyle_Solid); + cursor.insertTable(m_deviceBaseInfo.size(), 2, tableFormat); + + for (const auto& info : m_deviceBaseInfo) + { + cursor.insertText(info.first, italicFormat); + cursor.movePosition(QTextCursor::NextCell); + cursor.insertText(info.second, normalFormat); + cursor.movePosition(QTextCursor::NextCell); + } + cursor.movePosition(QTextCursor::End); + } + + { // Insert list of sub devices + cursor.insertBlock(); + cursor.insertBlock(); + cursor.insertText(tr("Sub devices:"), underlineFormat); + cursor.insertText(" ", normalFormat); + cursor.insertBlock(); + cursor.movePosition(QTextCursor::PreviousBlock); + cursor.movePosition(QTextCursor::EndOfBlock); + cursor.setBlockCharFormat(normalFormat); + QTextListFormat listFormat; + listFormat.setStyle(QTextListFormat::ListDisc); + listFormat.setIndent(1); + cursor.insertList(listFormat); + + for (const auto& subDeviceInfo : m_subDevices) { + cursor.insertText(subDeviceInfo.first); + cursor.insertText(": "); + cursor.insertText(subDeviceInfo.second.info); + if (cursor.currentList()->itemNumber(cursor.block()) + < static_cast(m_subDevices.size() - 1)) { + cursor.insertBlock(); + } + } + cursor.movePosition(QTextCursor::MoveOperation::NextBlock); + } + + if (!m_batteryInfo.isEmpty()) { + cursor.insertBlock(); + cursor.insertText(tr("Battery Info:"), underlineFormat); + cursor.insertText(" ", normalFormat); + cursor.insertText(m_batteryInfo); + cursor.insertBlock(); + } + + if (!m_hidppInfo.presenterState.isEmpty()) + { + cursor.insertBlock(); + cursor.insertText(tr("HID++ Info:"), underlineFormat); + cursor.insertText(" ", normalFormat); + cursor.insertBlock(); + cursor.movePosition(QTextCursor::PreviousBlock); + cursor.movePosition(QTextCursor::EndOfBlock); + cursor.setBlockCharFormat(normalFormat); + QTextListFormat listFormat; + listFormat.setStyle(QTextListFormat::ListDisc); + listFormat.setIndent(1); + cursor.insertList(listFormat); + + if (!m_hidppInfo.receiverState.isEmpty()) { + cursor.insertText(tr("Receiver state:"), italicFormat); + cursor.insertText(" ", normalFormat); + cursor.insertText(m_hidppInfo.receiverState); + } + + cursor.insertBlock(); + cursor.insertText(tr("Presenter state:"), italicFormat); + cursor.insertText(" ", normalFormat); + cursor.insertText(m_hidppInfo.presenterState); + + cursor.insertBlock(); + cursor.insertText(tr("Protocol version:"), italicFormat); + cursor.insertText(" ", normalFormat); + cursor.insertText(m_hidppInfo.protocolVersion); + + cursor.insertBlock(); + cursor.insertText(tr("Supported features:"), italicFormat); + cursor.insertText(" ", normalFormat); + cursor.insertText(m_hidppInfo.hidppFlags.join(", ")); + + cursor.movePosition(QTextCursor::MoveOperation::NextBlock); + } +} + +// ------------------------------------------------------------------------------------------------- +void DeviceInfoWidget::updateSubdeviceInfo(SubDeviceConnection* sdc) +{ + const auto hdc = qobject_cast(sdc); + m_subDevices[sdc->path()] = SubDeviceInfo{ + QString("[%2%3%4]").arg( + toString(sdc->mode(), false), + sdc->isGrabbed() ? ", Grabbed" : "", + sdc->hasFlags(DeviceFlag::Hidpp) ? ", HID++" : ""), + hdc != nullptr, + (hdc != nullptr) ? hdc->hasFlags(DeviceFlag::ReportBattery) : false + }; +} + +// ------------------------------------------------------------------------------------------------- +void DeviceInfoWidget::initSubdeviceInfo() +{ + m_subDevices.clear(); + m_batteryInfo.clear(); + m_batteryInfoTimer->stop(); + m_hidppInfo.clear(); + + for (const auto& sd : m_connection->subDevices()) + { + const auto& sdc = sd.second; + if (sdc->path().isEmpty()) { continue; } + updateSubdeviceInfo(sdc.get()); + connectToSubdeviceUpdates(sdc.get()); + + if (const auto hdc = qobject_cast(sdc.get())) + { + updateHidppInfo(hdc); + + if (hdc->hasFlags(DeviceFlag::ReportBattery)) { + updateBatteryInfo(hdc); + hdc->triggerBattyerInfoUpdate(); + } + } + } +} + +// ------------------------------------------------------------------------------------------------- +void DeviceInfoWidget::updateHidppInfo(SubHidppConnection* hdc) +{ + m_hidppInfo.clear(); + + if (hdc->busType() == BusType::Usb) { + m_hidppInfo.receiverState = toString(hdc->receiverState(), false); + } + + m_hidppInfo.presenterState = toString(hdc->presenterState(), false); + + const auto pv = hdc->protocolVersion(); + m_hidppInfo.protocolVersion = QString("%1.%2").arg(pv.major).arg(pv.minor); + + for (const auto flag : { DeviceFlag::Vibrate + , DeviceFlag::ReportBattery + , DeviceFlag::NextHold + , DeviceFlag::BackHold + , DeviceFlag::PointerSpeed }) + { + if (hdc->hasFlags(flag)) { m_hidppInfo.hidppFlags.push_back(toString(flag, false)); } + } +} + +// ------------------------------------------------------------------------------------------------- +void DeviceInfoWidget::updateBatteryInfo(SubHidppConnection* hdc) +{ + const auto batteryInfo = hdc->batteryInfo(); + if (batteryInfo.status == HIDPP::BatteryStatus::Discharging) + { + m_batteryInfo = QString("%1% - %2% (%3)").arg( + QString::number(batteryInfo.currentLevel), + QString::number(batteryInfo.nextReportedLevel), + toString(batteryInfo.status)); + } else { + m_batteryInfo = toString(batteryInfo.status); + } +} diff --git a/src/deviceswidget.h b/src/deviceswidget.h index 6e3c865c..cde9dc1d 100644 --- a/src/deviceswidget.h +++ b/src/deviceswidget.h @@ -1,15 +1,28 @@ -// 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 "device-defs.h" + #include #include -struct DeviceId; +#include +#include +#include + +class DeviceConnection; class InputMapper; +class MultiTimerWidget; class QComboBox; +class QTabWidget; +class QTextEdit; class Settings; class Spotlight; class VibrationSettingsWidget; +class SubDeviceConnection; +class SubHidppConnection; +class TimerTabWidget; // ------------------------------------------------------------------------------------------------- class DevicesWidget : public QWidget @@ -18,7 +31,7 @@ class DevicesWidget : public QWidget public: explicit DevicesWidget(Settings* settings, Spotlight* spotlight, QWidget* parent = nullptr); - const DeviceId currentDeviceId() const; + DeviceId currentDeviceId() const; signals: void currentDeviceChanged(const DeviceId&); @@ -29,10 +42,89 @@ class DevicesWidget : public QWidget QWidget* createDevicesWidget(Settings* settings, Spotlight* spotlight); QWidget* createInputMapperWidget(Settings* settings, Spotlight* spotlight); QWidget* createDeviceInfoWidget(Spotlight* spotlight); - QWidget* createTimerTabWidget(Settings* settings, Spotlight* spotlight); + TimerTabWidget* createTimerTabWidget(Settings* settings, Spotlight* spotlight); + void updateTimerTab(Spotlight* spotlight); QComboBox* m_devicesCombo = nullptr; - QWidget* m_timerTabWidget = nullptr; - VibrationSettingsWidget* m_vibrationSettingsWidget = nullptr; + QTabWidget* m_tabWidget = nullptr; + TimerTabWidget* m_timerTabWidget = nullptr; + QPointer m_timerTabContext; + QWidget* m_deviceDetailsTabWidget = nullptr; + QPointer m_inputMapper; }; + +// ------------------------------------------------------------------------------------------------- +class TimerTabWidget : public QWidget +{ + Q_OBJECT + +public: + TimerTabWidget(Settings* settings, QWidget* parent = nullptr); + VibrationSettingsWidget* vibrationSettingsWidget(); + + void loadSettings(const DeviceId& deviceId); + void setSubDeviceConnection(SubDeviceConnection* sdc); + +private: + DeviceId m_deviceId; + Settings* const m_settings = nullptr; + MultiTimerWidget* m_multiTimerWidget = nullptr; + VibrationSettingsWidget* m_vibrationSettingsWidget = nullptr; +}; + +// ------------------------------------------------------------------------------------------------- +class DeviceInfoWidget : public QWidget +{ + Q_OBJECT + +public: + DeviceInfoWidget(QWidget* parent = nullptr); + void setDeviceConnection(DeviceConnection* connection); + +private: + void initSubdeviceInfo(); + void updateSubdeviceInfo(SubDeviceConnection* sdc); + void connectToSubdeviceUpdates(SubDeviceConnection* sdc); + void connectToBatteryUpdates(SubHidppConnection* hdc); + void updateHidppInfo(SubHidppConnection* hdc); + void updateBatteryInfo(SubHidppConnection* hdc); + + void delayedTextEditUpdate(); + void updateTextEdit(); + + QTextEdit* m_textEdit = nullptr; + QTimer* m_delayedUpdateTimer = nullptr; + QTimer* m_batteryInfoTimer = nullptr; + + std::vector> m_deviceBaseInfo; + + struct SubDeviceInfo { + QString info; + bool isHidpp = false; + bool hasBatteryInfo = false; + }; + + std::map m_subDevices; + QString m_batteryInfo; + + struct HidppInfo { + QString receiverState; + QString presenterState; + QString protocolVersion; + QStringList hidppFlags; + + void clear() + { + receiverState.clear(); + presenterState.clear(); + protocolVersion.clear(); + hidppFlags.clear(); + } + }; + + HidppInfo m_hidppInfo; + + QPointer m_connectionContext; + QPointer m_connection; +}; \ No newline at end of file diff --git a/src/enum-helper.h b/src/enum-helper.h index 57a7bdea..b84881f7 100644 --- a/src/enum-helper.h +++ b/src/enum-helper.h @@ -1,26 +1,45 @@ -// 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 + +/// Cast enum type to underlying integral type. +template +constexpr auto to_integral(T e) { + return static_cast>(e); +} + +/// Cast integral type to a given enum type. +template +constexpr auto to_enum(T v) { + return static_cast(v); +} + // ------------------------------------------------------------------------------------------------- + #define EXPAND_( x ) x // MSVC workaround #define GET_ENUM_MACRO(_1,_2,NAME,...) NAME #define ENUM(...) EXPAND_(GET_ENUM_MACRO(__VA_ARGS__, ENUM2, ENUM1)(__VA_ARGS__)) // enum flags macro (cannot be used inside class declaration) #define ENUM1(ENUMCLASS) \ inline ENUMCLASS operator|(ENUMCLASS lhs, ENUMCLASS rhs) { \ - using T = std::underlying_type_t; \ - return static_cast(static_cast(lhs) | static_cast(rhs)); } \ + return to_enum(to_integral(lhs) | to_integral(rhs)); } \ inline ENUMCLASS operator&(ENUMCLASS lhs, ENUMCLASS rhs) { \ - using T = std::underlying_type_t; \ - return static_cast(static_cast(lhs) & static_cast(rhs)); } \ + return to_enum(to_integral(lhs) & to_integral(rhs)); } \ inline ENUMCLASS operator~(ENUMCLASS lhs) { \ - using T = std::underlying_type_t; \ - return static_cast(~static_cast(lhs)); } \ + return to_enum(~to_integral(lhs)); } \ inline ENUMCLASS& operator |= (ENUMCLASS& lhs, ENUMCLASS rhs) {lhs = lhs | rhs; return lhs; } \ inline ENUMCLASS& operator &= (ENUMCLASS& lhs, ENUMCLASS rhs) {lhs = lhs & rhs; return lhs; } \ - inline bool operator!(ENUMCLASS e) { return e == static_cast(0); } + inline bool operator!(ENUMCLASS e) { return e == to_enum(0); } // enum flags macro (cannot be used inside class declaration) #define ENUM2(ENUMCLASS, PLURALNAME) \ ENUM1(ENUMCLASS); \ using PLURALNAME = ENUMCLASS; + +#define ENUM_CASE_STRINGIFY(x) case x: return #x +#define ENUM_CASE_STRINGIFY2(c, n) case c::n: return #n +#define ENUM_CASE_STRINGIFY3(c, n, b) case c::n: return b ? #c"::"#n : #n + +#define ENUM_STRINGIFY3(c, n, b) (b ? #c"::"#n : #n) diff --git a/src/extra-devices.cc.in b/src/extra-devices.cc.in index f775a54e..6f961ac9 100644 --- a/src/extra-devices.cc.in +++ b/src/extra-devices.cc.in @@ -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 "devicescan.h" #include @@ -11,7 +13,7 @@ namespace { // List of supported extra-devices const std::vector supportedExtraDevices { // @SUPPORTED_EXTRA_DEVICES@ }; -} +} // end anonymous namespace // Function declaration to check for extra devices, definition in generated source bool isExtraDeviceSupported(quint16 vendorId, quint16 productId) diff --git a/src/hidpp.cc b/src/hidpp.cc new file mode 100644 index 00000000..e23403f3 --- /dev/null +++ b/src/hidpp.cc @@ -0,0 +1,817 @@ +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + +#include "hidpp.h" + +#include "enum-helper.h" +#include "logging.h" + +#include + +#include +#include + +#include +#include +#include +#include + +DECLARE_LOGGING_CATEGORY(hid) + +namespace { + // ----------------------------------------------------------------------------------------------- + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + const auto registered_ = qRegisterMetaTypeStreamOperators() + && qRegisterMetaTypeStreamOperators(); + #endif + + // ----------------------------------------------------------------------------------------------- + constexpr char featureSetFilename[] = "DeviceFeatureSet.conf"; + constexpr char firmwareKey[] = "firmwareVersion"; + constexpr char featureTableKey[] = "featureTable"; + + // ----------------------------------------------------------------------------------------------- + namespace Defaults { + constexpr uint8_t HidppSoftwareId = 7; + } // end namespace Defaults + + // ----------------------------------------------------------------------------------------------- + // -- HID++ message offsets + namespace Offset { + constexpr uint32_t Type = 0; + constexpr uint32_t DeviceIndex = 1; + constexpr uint32_t SubId = 2; + constexpr uint32_t FeatureIndex = SubId; + constexpr uint32_t Address = 3; + + constexpr uint32_t ErrorSubId = 3; + constexpr uint32_t ErrorFeatureIndex = ErrorSubId; + constexpr uint32_t ErrorAddress = 4; + constexpr uint32_t ErrorCode = 5; + + constexpr uint32_t Payload = 4; + + constexpr uint32_t FwType = Payload; + constexpr uint32_t FwPrefix = FwType + 1; + constexpr uint32_t FwVersion = FwPrefix + 3; + constexpr uint32_t FwBuild = FwVersion + 2; + } // end namespace Offset + + // ----------------------------------------------------------------------------------------------- + namespace Defines { + constexpr uint8_t ErrorShort = 0x8f; + constexpr uint8_t ErrorLong = 0xff; + } // end namespace Defines + + // ----------------------------------------------------------------------------------------------- + uint8_t funcSwIdToByte(uint8_t function, uint8_t swId) { + return (swId & 0x0f)|((function & 0x0f) << 4); + } + + // ----------------------------------------------------------------------------------------------- + uint8_t getRandomByte() + { + static std::mt19937 gen(std::random_device{}()); + std::uniform_int_distribution distribution; + return distribution(gen); + } + + // ----------------------------------------------------------------------------------------------- + QString settingsKey(const DeviceId& dId, const QString& key) { + return QString("Device_%1_%2/%3") + .arg(logging::hexId(dId.vendorId), logging::hexId(dId.productId), key); + } +} // end anonymous namespace + +// ------------------------------------------------------------------------------------------------- +const char* toString(HidppConnectionInterface::MsgResult res) +{ + using MsgResult = HidppConnectionInterface::MsgResult; + switch(res) { + ENUM_CASE_STRINGIFY(MsgResult::Ok); + ENUM_CASE_STRINGIFY(MsgResult::InvalidFormat); + ENUM_CASE_STRINGIFY(MsgResult::WriteError); + ENUM_CASE_STRINGIFY(MsgResult::Timeout); + ENUM_CASE_STRINGIFY(MsgResult::HidppError); + ENUM_CASE_STRINGIFY(MsgResult::FeatureNotSupported); + } + return "MsgResult::(unknown)"; +} + +// ------------------------------------------------------------------------------------------------- +const char* toString(HIDPP::Error e) +{ + using Error = HIDPP::Error; + switch(e) { + ENUM_CASE_STRINGIFY(Error::NoError); + ENUM_CASE_STRINGIFY(Error::Unknown); + ENUM_CASE_STRINGIFY(Error::InvalidArgument); + ENUM_CASE_STRINGIFY(Error::OutOfRange); + ENUM_CASE_STRINGIFY(Error::HWError); + ENUM_CASE_STRINGIFY(Error::LogitechInternal); + ENUM_CASE_STRINGIFY(Error::InvalidFeatureIndex); + ENUM_CASE_STRINGIFY(Error::InvalidFunctionId); + ENUM_CASE_STRINGIFY(Error::Busy); + ENUM_CASE_STRINGIFY(Error::Unsupported); + } + return "Error::(unknown)"; +} + +namespace HIDPP { +// ------------------------------------------------------------------------------------------------- +Message::Data getRandomPingPayload() { + return {0, 0, getRandomByte()}; +} + +// ------------------------------------------------------------------------------------------------- +Message::Message() = default; + +// ------------------------------------------------------------------------------------------------- +Message::Message(Type type) + : Message(type, DeviceIndex::DefaultDevice, 0, 0, Defaults::HidppSoftwareId, {}) +{} + +// ------------------------------------------------------------------------------------------------- +Message::Message(std::vector&& data) : m_data(std::move(data)) {} + +// ------------------------------------------------------------------------------------------------- +Message::Message(Type type, uint8_t deviceIndex, uint8_t featureIndex, uint8_t function, + uint8_t swId, Data payload) + : Message(Data{to_integral(type), deviceIndex, featureIndex, funcSwIdToByte(function, swId)}) +{ + if (type == Type::Invalid) { return; } + + m_data.reserve(m_data.size() + payload.size()); + std::move(payload.begin(), payload.end(), std::back_inserter(m_data)); + + if (type == Type::Long) { m_data.resize(LONG_MSG_SIZE, 0x0); } + else if (type == Type::Short) { m_data.resize(SHORT_MSG_SIZE, 0x0); } +} + +// ------------------------------------------------------------------------------------------------- +Message::Message(Type type, uint8_t deviceIndex, uint8_t featureIndex, uint8_t function, + Data payload) + : Message(type, deviceIndex, featureIndex, function, Defaults::HidppSoftwareId, std::move(payload)) +{} + +// ------------------------------------------------------------------------------------------------- +Message::Message(Type type, uint8_t deviceIndex, uint8_t featureIndex, Data payload) + : Message(type, deviceIndex, featureIndex, 0, Defaults::HidppSoftwareId, std::move(payload)) +{} + +// ------------------------------------------------------------------------------------------------- +Message::Message(Type type, uint8_t deviceIndex, Data payload) + : Message(type, deviceIndex, 0, 0, Defaults::HidppSoftwareId, std::move(payload)) +{} + +// ------------------------------------------------------------------------------------------------- +size_t Message::size() const +{ + if (isLong()) { return LONG_MSG_SIZE; } + if (isShort()) { return SHORT_MSG_SIZE; } + return 0; +} + +// ------------------------------------------------------------------------------------------------- +Message::Type Message::type() const +{ + if (isLong()) { return Type::Long; } + if (isShort()) { return Type::Short; } + return Type::Invalid; +} + +// ------------------------------------------------------------------------------------------------- +bool Message::isValid() const { return isLong() || isShort(); } + +// ------------------------------------------------------------------------------------------------- +bool Message::isShort() const { + return (m_data.size() >= SHORT_MSG_SIZE && m_data[Offset::Type] == to_integral(Message::Type::Short)); +} + +// ------------------------------------------------------------------------------------------------- +bool Message::isLong() const { + return (m_data.size() >= LONG_MSG_SIZE && m_data[Offset::Type] == to_integral(Message::Type::Long)); +} + +// ------------------------------------------------------------------------------------------------- +bool Message::isError() const +{ + if (isShort() && m_data[Offset::SubId] == Defines::ErrorShort) { + return true; + } + + if (isLong() && m_data[Offset::SubId] == Defines::ErrorLong) { + return true; + } + + return false; +} + +// ------------------------------------------------------------------------------------------------- +uint8_t Message::errorSubId() const { + return m_data[Offset::ErrorSubId]; +} + +// ------------------------------------------------------------------------------------------------- +uint8_t Message::errorAddress() const { + return m_data[Offset::ErrorAddress]; +} + +// ------------------------------------------------------------------------------------------------- +uint8_t Message::errorFeatureIndex() const { + return m_data[Offset::ErrorFeatureIndex]; +} + +// ------------------------------------------------------------------------------------------------- +uint8_t Message::errorFunction() const { + return ((m_data[Offset::ErrorAddress] & 0xf0) >> 4); +} + +// ------------------------------------------------------------------------------------------------- +uint8_t Message::errorSoftwareId() const { + return (m_data[Offset::ErrorAddress] & 0x0f); +} + +// ------------------------------------------------------------------------------------------------- +HIDPP::Error Message::errorCode() const { + return to_enum(m_data[Offset::ErrorCode]); +} + +// ------------------------------------------------------------------------------------------------- +uint8_t Message::deviceIndex() const { + return m_data[Offset::DeviceIndex]; +} + +// ------------------------------------------------------------------------------------------------- +uint8_t Message::subId() const { + return m_data[Offset::SubId]; +} + +// ------------------------------------------------------------------------------------------------- +uint8_t Message::address() const { + return m_data[Offset::Address]; +} + +// ------------------------------------------------------------------------------------------------- +uint8_t Message::featureIndex() const { + return m_data[Offset::FeatureIndex]; +} + +// ------------------------------------------------------------------------------------------------- +uint8_t Message::function() const { + return ((m_data[Offset::Address] & 0xf0) >> 4); +} + +// ------------------------------------------------------------------------------------------------- +uint8_t Message::softwareId() const { + return (m_data[Offset::Address] & 0x0f); +} + +// ------------------------------------------------------------------------------------------------- +void Message::setSubId(uint8_t subId) { + m_data[Offset::SubId] = subId; +} + +// ------------------------------------------------------------------------------------------------- +void Message::setAddress(uint8_t address) { + m_data[Offset::Address] = address; +} + +// ------------------------------------------------------------------------------------------------- +void Message::setFeatureIndex(uint8_t featureIndex) { + m_data[Offset::FeatureIndex] = featureIndex; +} + +// ------------------------------------------------------------------------------------------------- +void Message::setFunction(uint8_t function) { + m_data[Offset::Address] = ((function & 0x0f) << 4) | (m_data[Offset::Address] & 0x0f); +} + +// ------------------------------------------------------------------------------------------------- +void Message::setSoftwareId(uint8_t softwareId) { + m_data[Offset::Address] = (softwareId & 0x0f) | (m_data[Offset::Address] & 0xf0); +} + +// ------------------------------------------------------------------------------------------------- +bool Message::isResponseTo(const Message& other) const +{ + if (!isValid() || !other.isValid()) { return false; } + + return deviceIndex() == other.deviceIndex() + && subId() == other.subId() + && address() == other.address(); +} + +// ------------------------------------------------------------------------------------------------- +bool Message::isErrorResponseTo(const Message& other) const +{ + if (!isValid() || !other.isValid()) { return false; } + + return deviceIndex() == other.deviceIndex() + && errorSubId() == other.subId() + && errorAddress() == other.address(); +} + +// ------------------------------------------------------------------------------------------------- +Message& Message::convertToLong() +{ + if (!isShort()) { return *this; } + + // Resize data vector, pad with zeroes. + m_data.resize(LONG_MSG_SIZE, 0); + m_data[Offset::Type] = to_integral(Type::Long); + return *this; +} + +// ------------------------------------------------------------------------------------------------- +Message Message::toLong() const { + return Message(*this).convertToLong(); +} + +// ------------------------------------------------------------------------------------------------- +QString Message::hex() const +{ + return qPrintable(QByteArray::fromRawData( + reinterpret_cast(m_data.data()), isValid() ? size() : m_data.size()).toHex() + ); +} + +// ================================================================================================= +FeatureSet::FeatureSet(HidppConnectionInterface* connection, QObject* parent) + : QObject(parent) + , m_connection(connection) +{} + +// ------------------------------------------------------------------------------------------------- +FeatureSet::State FeatureSet::state() const { + return m_state; +} + +// ------------------------------------------------------------------------------------------------- +void FeatureSet::setState(State s) +{ + if (s == m_state) { return; } + + m_state = s; + emit stateChanged(m_state); +} + +// ------------------------------------------------------------------------------------------------- +void FeatureSet::getFeatureIndex(FeatureCode fc, std::function cb) +{ + postSelf([this, fc, cb=std::move(cb)]() mutable + { + if (m_connection == nullptr) + { + if (cb) { cb(MsgResult::WriteError, 0); } + return; + } + + const auto fcLSB = static_cast(to_integral(fc) >> 8); + const auto fcMSB = static_cast(to_integral(fc) & 0x00ff); + + Message featureIndexReqMsg(Message::Type::Long, DeviceIndex::WirelessDevice1, + Message::Data{fcLSB, fcMSB}); + + m_connection->sendRequest(std::move(featureIndexReqMsg), + [cb=std::move(cb), fc](MsgResult result, Message&& msg) + { + logDebug(hid) << tr("getFeatureIndex(%1) => %2, %3") + .arg(to_integral(fc)).arg(toString(result)).arg(msg[4]); + if (cb) { cb(result, (result != MsgResult::Ok) ? 0 : msg[4]); } + }); + }); +} + +// ------------------------------------------------------------------------------------------------- +void FeatureSet::getFeatureCount(std::function cb) +{ + getFeatureIndex(FeatureCode::FeatureSet, makeSafeCallback( + [this, cb=std::move(cb)](MsgResult res, uint8_t featureIndex) mutable + { + if (res != MsgResult::Ok) + { + if (cb) { cb(res, 0, 0); } + return; + } + + Message featureCountReqMsg(Message::Type::Long, DeviceIndex::WirelessDevice1, featureIndex); + + m_connection->sendRequest(std::move(featureCountReqMsg), + [featureIndex, cb=std::move(cb)](MsgResult result, Message&& msg) { + if (cb) { cb(result, featureIndex, (result != MsgResult::Ok) ? 0 : msg[4]); } + }); + })); +} + +// ------------------------------------------------------------------------------------------------- +void FeatureSet::getFirmwareCount(std::function cb) +{ + getFeatureIndex(FeatureCode::FirmwareVersion, makeSafeCallback( + [this, cb=std::move(cb)](MsgResult res, uint8_t featureIndex) mutable + { + if (res != MsgResult::Ok) + { + if (cb) { cb(res, 0, 0); } + return; + } + + Message fwCountReqMsg(Message::Type::Long, DeviceIndex::WirelessDevice1, featureIndex); + + m_connection->sendRequest(std::move(fwCountReqMsg), + [featureIndex, cb=std::move(cb)](MsgResult result, Message&& msg) + { + logDebug(hid) << tr("getFirmwareCount() => %1, featureIndex = %2, count = %3") + .arg(toString(result)).arg(featureIndex).arg(msg[4]); + if (cb) { cb(result, featureIndex, (result != MsgResult::Ok) ? 0 : msg[4]); } + }); + })); +} + +// ------------------------------------------------------------------------------------------------- +void FeatureSet::getFirmwareInfo(uint8_t fwIndex, uint8_t entity, + std::function cb) +{ + if (m_connection == nullptr) + { + if (cb) { cb(MsgResult::WriteError, FirmwareInfo()); } + return; + } + + Message fwVerReqMessage(Message::Type::Long, DeviceIndex::WirelessDevice1, fwIndex, 1, + Message::Data{entity}); + + m_connection->sendRequest(std::move(fwVerReqMessage), + [cb=std::move(cb)](MsgResult res, Message&& msg) { + if (cb) { cb(res, FirmwareInfo(std::move(msg))); } + }); +} + +// ------------------------------------------------------------------------------------------------- +void FeatureSet::getMainFirmwareInfo(std::function cb) +{ + getFirmwareCount(makeSafeCallback( + [this, cb=std::move(cb)](MsgResult res, uint8_t featureIndex, uint8_t count) mutable + { + if (res != MsgResult::Ok) + { + if (cb) { cb(res, FirmwareInfo()); } + return; + } + getMainFirmwareInfo(featureIndex, count, 0, std::move(cb)); + })); +} + +// ------------------------------------------------------------------------------------------------- +void FeatureSet::getMainFirmwareInfo(uint8_t fwIndex, uint8_t max, uint8_t current, + std::function cb) +{ + getFirmwareInfo(fwIndex, current, makeSafeCallback( + [this, current, max, fwIndex, cb=std::move(cb)](MsgResult res, FirmwareInfo&& fi) mutable + { + logDebug(hid) << tr("getFirmwareInfo(%1, %2, %3) => %4, fi.type = %5, fi.ver = %6, fi.pref = %7") + .arg(fwIndex).arg(max).arg(current).arg(toString(res)) + .arg(to_integral(fi.firmwareType())).arg(fi.firmwareVersion()).arg(fi.firmwarePrefix()); + + if (res == MsgResult::Ok && fi.firmwareType() == FirmwareInfo::FirmwareType::MainApp) + { + if (cb) { cb(res, std::move(fi)); } + return; + } + + if (max == current + 1) { + if (cb) { cb(res, FirmwareInfo()); } + return; + } + + getMainFirmwareInfo(fwIndex, max, current + 1, std::move(cb)); + })); +} + +// ------------------------------------------------------------------------------------------------- +void FeatureSet::initFromDevice(DeviceId dId, std::function cb) +{ + postSelf([this, dId, cb=std::move(cb)]() mutable + { + if (m_connection == nullptr || m_state == State::Initialized || m_state == State::Initializing) + { + if (cb) { cb(m_state); } + return; + } + + setState(State::Initializing); + + getMainFirmwareInfo(makeSafeCallback( + [this, dId, cb=std::move(cb)](MsgResult res, FirmwareInfo&& fi) mutable + { + logDebug(hid) << tr("getMainFirmwareInfo() => %1, fi.type = %2").arg(toString(res)) + .arg(to_integral(fi.firmwareType())); + + if (fi.firmwareType() == FirmwareInfo::FirmwareType::MainApp) { + m_mainFirmwareInfo = std::move(fi); + } + + // --- Try to load feature set from cache file + const auto cacheFile = QStandardPaths::locate( + QStandardPaths::StandardLocation::AppLocalDataLocation, featureSetFilename); + + if (!cacheFile.isEmpty() && res == MsgResult::Ok && m_mainFirmwareInfo.isValid()) + { + // load feature set and return + QSettings settings(cacheFile, QSettings::NativeFormat); + const auto fw = settings.value(settingsKey(dId, firmwareKey)); + if (fw.canConvert()) + { + auto cacheFirmwareInfo = fw.value(); + if (cacheFirmwareInfo == m_mainFirmwareInfo) + { + const auto table = settings.value(settingsKey(dId, featureTableKey)); + if (table.canConvert()) + { + m_featureTable = table.value(); + logDebug(hid) << tr("Loaded feature set with %1 entries from local cache").arg(m_featureTable.size()); + setState(State::Initialized); + if (cb) { cb(m_state); } + return; + } + } + } + } + + getFeatureCount(makeSafeCallback( + [this, dId, cb=std::move(cb)](MsgResult res, uint8_t featureIndex, uint8_t count) mutable + { + logDebug(hid) << tr("getFeatureCount() => %1, featureIndex = %2, count = %3") + .arg(toString(res)).arg(featureIndex).arg(count); + + if (res != MsgResult::Ok) + { + setState(State::Error); + if (cb) { cb(m_state); } + return; + } + + getFeatureIds(featureIndex, count, makeSafeCallback( + [this, dId, cb=std::move(cb)](MsgResult res, FeatureTable&& ft) + { + if (res != MsgResult::Ok) { + setState(State::Error); + } + else + { + m_featureTable = std::move(ft); + setState(State::Initialized); + + // Store feature table in cache file + const auto dataPath = QStandardPaths::writableLocation( + QStandardPaths::StandardLocation::AppLocalDataLocation); + + if (!dataPath.isEmpty() && m_mainFirmwareInfo.isValid()) + { + const auto cacheFile = QDir(dataPath).filePath(featureSetFilename); + QSettings settings(cacheFile, QSettings::NativeFormat); + settings.setValue(settingsKey(dId, firmwareKey), QVariant::fromValue(m_mainFirmwareInfo)); + settings.setValue(settingsKey(dId, featureTableKey), QVariant::fromValue(m_featureTable)); + } + } + + if (cb) { cb(m_state); } + })); // getFeatureIds (table) + })); // getFeatureCount + })); // getMainFwInfo + }); // postSelf +} + +// ------------------------------------------------------------------------------------------------- +void FeatureSet::getFeatureIds(uint8_t featureSetIndex, uint8_t count, + std::function cb) +{ + if (m_connection == nullptr) + { + if (cb) { cb(MsgResult::WriteError, FeatureTable{}); } // empty featuretable + return; + } + + if (count == 0) + { + if (cb) { cb(MsgResult::Ok, FeatureTable{}); }// no count, empty featuretable + return; + } + + auto featureTable = std::make_shared(); + + HidppConnectionInterface::RequestBatch batch; + for (uint8_t featureIndex = 1; featureIndex <= count; ++featureIndex) + { + batch.emplace(HidppConnectionInterface::RequestBatchItem { + Message(Message::Type::Long, DeviceIndex::WirelessDevice1, featureSetIndex, 1, + Message::Data{featureIndex}), + [featureTable, featureIndex](MsgResult res, Message&& msg) + { + if (res != MsgResult::Ok) { return; } + const uint16_t featureCode = (static_cast(msg[4]) << 8) + | static_cast(msg[5]); + const uint8_t featureType = msg[6]; + const bool softwareHidden = (featureType & (1<<6)); + const bool obsoleteFeature = (featureType & (1<<7)); + if (!softwareHidden && !obsoleteFeature) { + featureTable->emplace(featureCode, featureIndex); + } + } + }); + } + + m_connection->sendRequestBatch(std::move(batch), + [featureTable, cb=std::move(cb)](std::vector&& results) { + if (cb) { cb(results.back(), std::move(*featureTable)); } + }); +} + +// ------------------------------------------------------------------------------------------------- +bool FeatureSet::featureCodeSupported(FeatureCode fc) const +{ + const auto featurePair = m_featureTable.find(to_integral(fc)); + return (featurePair != m_featureTable.end()); +} + +// ------------------------------------------------------------------------------------------------- +uint8_t FeatureSet::featureIndex(FeatureCode fc) const +{ + const auto it = m_featureTable.find(to_integral(fc)); + if (it == m_featureTable.cend()) { + return 0x00; + } + return it->second; +} + +// ================================================================================================= +FirmwareInfo::FirmwareInfo(Message&& msg) + : m_rawMsg(std::move(msg)) +{} + +// ------------------------------------------------------------------------------------------------- +FirmwareInfo::FirmwareType FirmwareInfo::firmwareType() const +{ + if (!m_rawMsg.isLong()) { return FirmwareType::Invalid; } + + switch(m_rawMsg[Offset::Payload] & 0xf) + { + case 0: return FirmwareType::MainApp; + case 1: return FirmwareType::Bootloader; + case 2: return FirmwareType::Hardware; + default: return FirmwareType::Other; + } +} + +// ------------------------------------------------------------------------------------------------- +QString FirmwareInfo::firmwarePrefix() const +{ + if (!m_rawMsg.isLong()) { return QString(); } + + return QString( + QByteArray::fromRawData(reinterpret_cast(&m_rawMsg[Offset::FwPrefix]), 3) + ); +} + +// ------------------------------------------------------------------------------------------------- +uint16_t FirmwareInfo::firmwareVersion() const +{ + if (!m_rawMsg.isLong()) { return 0; } + + const auto& fwVersionMsb = m_rawMsg[Offset::FwVersion]; + const auto& fwVersionLsb = m_rawMsg[Offset::FwVersion+1]; + + // Firmware version is BCD encoded + return ( fwVersionLsb & 0xF) + + (((fwVersionLsb >> 4 ) & 0xF) * 10) + + (( fwVersionMsb & 0xF) * 100) + + (((fwVersionMsb >> 4) & 0xF) * 1000); +} + +// ------------------------------------------------------------------------------------------------- +uint16_t FirmwareInfo::firmwareBuild() const +{ + if (!m_rawMsg.isLong()) { return 0; } + + const auto& fwBuildMsb = m_rawMsg[Offset::FwBuild]; + const auto& fwBuildLsb = m_rawMsg[Offset::FwBuild+1]; + + // Firmware build is BCD encoded ?? + return ( fwBuildLsb & 0xF) + + (((fwBuildLsb >> 4 ) & 0xF) * 10) + + (( fwBuildMsb & 0xF) * 100) + + (((fwBuildMsb >> 4) & 0xF) * 1000); +} + +} // end namespace HIDPP + +// ------------------------------------------------------------------------------------------------- +const char* toString(HIDPP::FeatureSet::State s) +{ + using State = HIDPP::FeatureSet::State; + switch (s) + { + ENUM_CASE_STRINGIFY(State::Uninitialized); + ENUM_CASE_STRINGIFY(State::Initialized); + ENUM_CASE_STRINGIFY(State::Initializing); + ENUM_CASE_STRINGIFY(State::Error); + }; + return "State::(unknown)"; +} + +// ------------------------------------------------------------------------------------------------- +const char* toString(HIDPP::FeatureCode fc) +{ + using FeatureCode = HIDPP::FeatureCode; + switch (fc) + { + ENUM_CASE_STRINGIFY(FeatureCode::Root); + ENUM_CASE_STRINGIFY(FeatureCode::FeatureSet); + ENUM_CASE_STRINGIFY(FeatureCode::FirmwareVersion); + ENUM_CASE_STRINGIFY(FeatureCode::DeviceName); + ENUM_CASE_STRINGIFY(FeatureCode::Reset); + ENUM_CASE_STRINGIFY(FeatureCode::DFUControlSigned); + ENUM_CASE_STRINGIFY(FeatureCode::BatteryStatus); + ENUM_CASE_STRINGIFY(FeatureCode::PresenterControl); + ENUM_CASE_STRINGIFY(FeatureCode::Sensor3D); + ENUM_CASE_STRINGIFY(FeatureCode::ReprogramControlsV4); + ENUM_CASE_STRINGIFY(FeatureCode::WirelessDeviceStatus); + ENUM_CASE_STRINGIFY(FeatureCode::SwapCancelButton); + ENUM_CASE_STRINGIFY(FeatureCode::PointerSpeed); + }; + return "FeatureCode::(unknown)"; +} + +// ------------------------------------------------------------------------------------------------- +const char* toString(HIDPP::BatteryStatus bs) +{ + using BatteryStatus = HIDPP::BatteryStatus; + switch (bs) + { + ENUM_CASE_STRINGIFY(BatteryStatus::AlmostFull); + ENUM_CASE_STRINGIFY(BatteryStatus::Charging); + ENUM_CASE_STRINGIFY(BatteryStatus::ChargingError); + ENUM_CASE_STRINGIFY(BatteryStatus::Discharging); + ENUM_CASE_STRINGIFY(BatteryStatus::Full); + ENUM_CASE_STRINGIFY(BatteryStatus::InvalidBattery); + ENUM_CASE_STRINGIFY(BatteryStatus::SlowCharging); + ENUM_CASE_STRINGIFY(BatteryStatus::ThermalError); + ENUM_CASE_STRINGIFY(BatteryStatus::Uninitialized); + }; + return "BatteryStatus::(unknown)"; +} + +// ------------------------------------------------------------------------------------------------- +const char* toString(HIDPP::Notification n) +{ + using Notification = HIDPP::Notification; + switch (n) + { + ENUM_CASE_STRINGIFY(Notification::DeviceDisconnection); + ENUM_CASE_STRINGIFY(Notification::DeviceConnection); + }; + return "Notification::(unknown)"; +} + +// ------------------------------------------------------------------------------------------------- +QDataStream& operator<<(QDataStream& s, const HIDPP::FeatureSet::FeatureTable& ft) +{ + s << static_cast(ft.size()); + for (const auto& entry : ft) { + s << entry.first << entry.second; + } + return s; +} + +// ------------------------------------------------------------------------------------------------- +QDataStream& operator>>(QDataStream& s, HIDPP::FeatureSet::FeatureTable& ft) +{ + quint64 size{}; + s >> size; + for (quint64 i = 0; i < size; ++i) { + HIDPP::FeatureSet::FeatureTable::key_type key; + HIDPP::FeatureSet::FeatureTable::mapped_type value; + s >> key; + s >> value; + ft.emplace(key, value); + } + return s; +} + +// ------------------------------------------------------------------------------------------------- +QDataStream& operator<<(QDataStream& s, const HIDPP::FirmwareInfo& fi) +{ + const auto& msg = fi.msg(); + const auto data = QByteArray::fromRawData(reinterpret_cast(msg.data()), msg.dataSize()); + s << data; + return s; +} + +// ------------------------------------------------------------------------------------------------- +QDataStream& operator>>(QDataStream& s, HIDPP::FirmwareInfo& fi) +{ + QByteArray data; + s >> data; + fi = HIDPP::FirmwareInfo(std::vector(data.begin(), data.end())); + return s; +} diff --git a/src/hidpp.h b/src/hidpp.h new file mode 100644 index 00000000..9cd861ca --- /dev/null +++ b/src/hidpp.h @@ -0,0 +1,394 @@ +// This file is part of Projecteur - https://github.com/jahnf/projecteur +// - See LICENSE.md and README.md + +#pragma once + +#include "device-defs.h" +#include "asynchronous.h" + +#include +#include +#include +#include +#include + +#include + +// Hidpp specific functionality +// - code is heavily inspired by this library: https://github.com/cvuchener/hidpp +// - also see https://6xq.net/git/lars/lshidpp.git +// - also see https://github.com/cvuchener/g500/blob/master/doc/hidpp10.md + +namespace HIDPP { + // ----------------------------------------------------------------------------------------------- + namespace DeviceIndex { + constexpr uint8_t DefaultDevice = 0xff; + constexpr uint8_t CordedDevice = 0x00; + constexpr uint8_t WirelessDevice1 = 1; + constexpr uint8_t WirelessDevice2 = 2; + constexpr uint8_t WirelessDevice3 = 3; + constexpr uint8_t WirelessDevice4 = 4; + constexpr uint8_t WirelessDevice5 = 5; + constexpr uint8_t WirelessDevice6 = 6; + } // end namespace DeviceIndex + + // ----------------------------------------------------------------------------------------------- + // see also: https://github.com/cvuchener/hidpp/blob/master/src/tools/hidpp-list-features.cpp + // Feature Codes important for Logitech Spotlight + 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, + }; + + // ----------------------------------------------------------------------------------------------- + /// Hid++ 2.0 error codes + enum class Error : uint8_t { + NoError = 0, + Unknown = 1, + InvalidArgument = 2, + OutOfRange = 3, + HWError = 4, + LogitechInternal = 5, + InvalidFeatureIndex = 6, + InvalidFunctionId = 7, + Busy = 8, // Device (or receiver) busy + Unsupported = 9, + }; + + // ----------------------------------------------------------------------------------------------- + enum class Notification : uint8_t { + DeviceDisconnection = 0x40, + DeviceConnection = 0x41, + }; + + // ----------------------------------------------------------------------------------------------- + namespace Commands { + constexpr uint8_t SetRegister = 0x80; + constexpr uint8_t GetRegister = 0x81; + constexpr uint8_t SetLongRegister = 0x82; + constexpr uint8_t GetLongRegister = 0x83; + } + + // ------------------------------------------------------------------------------------------------- + // Battery Status as returned on HID++ BatteryStatus feature code (0x1000) + enum class BatteryStatus : uint8_t { + Discharging = 0x00, + Charging = 0x01, + AlmostFull = 0x02, + Full = 0x03, + SlowCharging = 0x04, + InvalidBattery = 0x05, + ThermalError = 0x06, + ChargingError = 0x07, + + Uninitialized = 0xff // Custom value of Projecteur + }; + + // ------------------------------------------------------------------------------------------------- + struct BatteryInfo + { + uint8_t currentLevel = 0; + uint8_t nextReportedLevel = 0; + BatteryStatus status = BatteryStatus::Uninitialized; + + inline bool operator==(const BatteryInfo& rhs) const { + return std::tie(currentLevel, nextReportedLevel, status) + == std::tie(rhs.currentLevel, rhs.nextReportedLevel, rhs.status); + } + }; + + // ----------------------------------------------------------------------------------------------- + struct ProtocolVersion { + uint8_t major = 0; + uint8_t minor = 0; + + inline bool smallerThan(uint8_t otherMajor, uint8_t otherMinor) const { + return (major < otherMajor) ? true : (minor < otherMinor) ? true : false; + } + + inline bool operator<(const ProtocolVersion& other) const { + return smallerThan(other.major, other.minor); + } + + inline bool operator==(const ProtocolVersion& rhs) const { + return std::tie(major, minor) == std::tie(rhs.major, rhs.minor); + } + }; + + // ----------------------------------------------------------------------------------------------- + /// Hidpp message class, heavily inspired by this library: https://github.com/cvuchener/hidpp + class Message final + { + public: + static constexpr int SHORT_MSG_SIZE = 7; + static constexpr int LONG_MSG_SIZE = 20; + + using Data = std::vector; + + /// HID++ message type. + enum class Type : uint8_t { + Invalid = 0x0, + Short = 0x10, + Long = 0x11, + }; + + /// Creates an invalid HID++ message object. + Message(); + /// Creates an empty default HID++ message of the given type. + /// An internal default is used as software id for the message. + Message(Type type); + /// Create a message with the given properties and payload. + Message(Type type, uint8_t deviceIndex, uint8_t featureIndex, uint8_t function, uint8_t swId, + Data payload = {}); + /// Create a message with the given properties and payload. + /// An internal default is used as software id for the message. + Message(Type type, uint8_t deviceIndex, uint8_t featureIndex, uint8_t function, + Data payload = {}); + + Message(Type type, uint8_t deviceIndex, uint8_t featureIndex, Data payload = {}); + Message(Type type, uint8_t deviceIndex, Data payload = {}); + + /// Create a message from raw data. + /// If the data is not a valid Hidpp message, this will result in an invalid HID++ message. + Message(std::vector&& data); + + Message(Message&& msg) = default; + Message(const Message& msg) = default; + Message& operator=(Message&&) = default; + + inline bool operator==(const Message& other) const { return m_data == other.m_data; } + + bool isValid() const; + bool isLong() const; + bool isShort() const; + size_t size() const; + + bool isError() const; + // --- For short error messages (isShort() && isError()) + uint8_t errorSubId() const; + uint8_t errorAddress() const; + // -- For long error messages (isLong() && isError()) + uint8_t errorFeatureIndex() const; + uint8_t errorFunction() const; + uint8_t errorSoftwareId() const; + // --- for both long & short error messages + Error errorCode() const; + + /// Converts the message to a long message, if it is a valid short message + Message& convertToLong(); + /// Converts the message to a long message and returns it as a new object, + /// if it is a valid short message. + Message toLong() const; + + Type type() const; + uint8_t deviceIndex() const; + void setDeviceIndex(uint8_t); + + // --- HIDPP 1.0 + uint8_t subId() const; + void setSubId(uint8_t subId); + uint8_t address() const; + void setAddress(uint8_t address); + + // --- HIDPP 2.0 + uint8_t featureIndex () const; + void setFeatureIndex(uint8_t featureIndex); + uint8_t function() const; + void setFunction(uint8_t function); + uint8_t softwareId() const; + void setSoftwareId(uint8_t softwareId); + + /// Returns true if the message is a possible response to a given Hidpp message. + bool isResponseTo(const Message& other) const; + /// Returns true if the message is a possible error response to a given Hidpp message. + bool isErrorResponseTo(const Message& other) const; + + auto data() { return m_data.data(); } + const auto data() const { return m_data.data(); } + auto dataSize() const { return m_data.size(); } + auto& operator[](size_t i) { return m_data.operator[](i); } + const auto& operator[](size_t i) const { return m_data.operator[](i); } + QString hex() const; + + private: + Data m_data; + }; + + Message::Data getRandomPingPayload(); +} //end of HIDPP namespace + +// ------------------------------------------------------------------------------------------------- +/// Hidpp interface to be implemented by classes that allow communicating with a HID++ device. +class HidppConnectionInterface +{ +public: + enum class MsgResult : uint8_t { + Ok = 0, + InvalidFormat, + WriteError, + Timeout, + HidppError, + FeatureNotSupported, + }; + + using SendResultCallback = std::function; + using RequestResultCallback = std::function; + + virtual BusType busType() const = 0; + + // --- synchronous versions + virtual ssize_t sendData(std::vector msg) = 0; + virtual ssize_t sendData(HIDPP::Message msg) = 0; + + // --- asynchronous versions, implementations must return immediately + virtual void sendData(std::vector msg, SendResultCallback resultCb) = 0; + virtual void sendData(HIDPP::Message msg, SendResultCallback resultCb) = 0; + virtual void sendRequest(std::vector msg, RequestResultCallback responseCb) = 0; + virtual void sendRequest(HIDPP::Message msg, RequestResultCallback responseCb) = 0; + + struct RequestBatchItem { + HIDPP::Message message; + RequestResultCallback callback; + }; + + using RequestBatch = std::queue; + using RequestBatchResultCallback = std::function&&)>; + virtual void sendRequestBatch(RequestBatch requestBatch, RequestBatchResultCallback cb, + bool continueOnError = false) = 0; + + struct DataBatchItem { + HIDPP::Message message; + SendResultCallback callback; + }; + + using DataBatch = std::queue; + using DataBatchResultCallback = std::function&&)>; + virtual void sendDataBatch(DataBatch dataBatch, DataBatchResultCallback cb, + bool continueOnError = false) = 0; + + // --- + + using NotificationCallback = std::function; + // The registered notification callback will be automatically unregistered if obj is destroyed. + virtual void registerNotificationCallback(QObject* obj, + uint8_t featureIndex, + NotificationCallback cb, + uint8_t function = 0xff) = 0; + virtual void registerNotificationCallback(QObject* obj, + HIDPP::Notification n, + NotificationCallback cb, + uint8_t function = 0xff) = 0; + + + virtual void unregisterNotificationCallback(QObject* obj, uint8_t featureIndex, + uint8_t function = 0xff) = 0; + virtual void unregisterNotificationCallback(QObject* obj, HIDPP::Notification n, + uint8_t function = 0xff) = 0; +}; + +namespace HIDPP { + // ----------------------------------------------------------------------------------------------- + class FirmwareInfo + { + public: + enum class FirmwareType : uint8_t { + MainApp = 0, + Bootloader = 1, + Hardware = 2, + Other = 3, + Invalid = 0xff + }; + + FirmwareInfo() = default; + FirmwareInfo(Message&& msg); + FirmwareInfo(const FirmwareInfo&) = default; + FirmwareInfo(FirmwareInfo&&) = default; + FirmwareInfo& operator=(FirmwareInfo&&) = default; + bool operator==(const FirmwareInfo& other) const { return m_rawMsg == other.m_rawMsg; } + + FirmwareType firmwareType() const; + QString firmwarePrefix() const; + uint16_t firmwareVersion() const; + uint16_t firmwareBuild() const; + bool isValid() const { return firmwareType() != FirmwareType::Invalid; } + const HIDPP::Message& msg() const { return m_rawMsg; } + + private: + HIDPP::Message m_rawMsg; + }; + + // ----------------------------------------------------------------------------------------------- + /// Class to get and store set of supported features and additional information + /// for a HID++ 2.0 device (although very much specialized for the Logitech Spotlight). + class FeatureSet : public QObject, public async::Async + { + Q_OBJECT + + public: + using FeatureTable = std::map; + enum class State : uint8_t { Uninitialized, Initializing, Initialized, Error }; + + FeatureSet(HidppConnectionInterface* connection, QObject* parent = nullptr); + + void initFromDevice(DeviceId dId, std::function cb); + State state() const; + + uint8_t featureIndex(FeatureCode fc) const; + bool featureCodeSupported(FeatureCode fc) const; + auto featureCount() const { return m_featureTable.size(); } + + signals: + void stateChanged(State s); + + private: + using MsgResult = HidppConnectionInterface::MsgResult; + + void getFeatureIndex(FeatureCode fc, std::function cb); + void getFeatureCount(std::function cb); + void getFirmwareCount(std::function cb); + void getFeatureIds(uint8_t featureSetIndex, uint8_t count, + std::function cb); + void getMainFirmwareInfo(std::function cb); + void getMainFirmwareInfo(uint8_t fwIndex, uint8_t max, uint8_t current, + std::function cb); + void getFirmwareInfo(uint8_t fwIndex, uint8_t entity, + std::function cb); + + void setState(State s); + + HidppConnectionInterface* m_connection = nullptr; + FeatureTable m_featureTable; + FirmwareInfo m_mainFirmwareInfo; + + State m_state = State::Uninitialized; + }; +} //end namespace HIDPP + +// ------------------------------------------------------------------------------------------------- +const char* toString(HidppConnectionInterface::MsgResult r); +const char* toString(HIDPP::Error e); +const char* toString(HIDPP::FeatureSet::State s); +const char* toString(HIDPP::FeatureCode fc); +const char* toString(HIDPP::BatteryStatus bs); +const char* toString(HIDPP::Notification n); + +// ------------------------------------------------------------------------------------------------- +Q_DECLARE_METATYPE(HIDPP::FeatureSet::FeatureTable); +QDataStream& operator<<(QDataStream& s, const HIDPP::FeatureSet::FeatureTable& ft); +QDataStream& operator>>(QDataStream& s, HIDPP::FeatureSet::FeatureTable& ft); + +// ------------------------------------------------------------------------------------------------- +Q_DECLARE_METATYPE(HIDPP::FirmwareInfo); +QDataStream& operator<<(QDataStream& s, const HIDPP::FirmwareInfo& fi); +QDataStream& operator>>(QDataStream& s, HIDPP::FirmwareInfo& fi); diff --git a/src/iconwidgets.cc b/src/iconwidgets.cc index 1c034b23..d789bd94 100644 --- a/src/iconwidgets.cc +++ b/src/iconwidgets.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 "iconwidgets.h" namespace { @@ -10,7 +12,7 @@ namespace { bool isDark(const QColor& c) { return !isLight(c); } constexpr int defaultIconLabelSize = 32; -} +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- IconButton::IconButton(Font::Icon symbol, QWidget* parent) diff --git a/src/iconwidgets.h b/src/iconwidgets.h index e179627a..84a703e2 100644 --- a/src/iconwidgets.h +++ b/src/iconwidgets.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 "projecteur-icons-def.h" @@ -7,6 +8,7 @@ #include // ------------------------------------------------------------------------------------------------- +/// Icon button class used throughout the application's widget based dialogs. class IconButton : public QToolButton { Q_OBJECT @@ -16,6 +18,7 @@ class IconButton : public QToolButton }; // ------------------------------------------------------------------------------------------------- +/// Icon label class used throughout the application's widget based dialogs. class IconLabel : public QLabel { Q_OBJECT diff --git a/src/imageitem.cc b/src/imageitem.cc index 3c185a72..ee124d6a 100644 --- a/src/imageitem.cc +++ b/src/imageitem.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 "imageitem.h" #include diff --git a/src/imageitem.h b/src/imageitem.h index 104cf951..aa164473 100644 --- a/src/imageitem.h +++ b/src/imageitem.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/inputmapconfig.cc b/src/inputmapconfig.cc index 29effdb1..f23a494a 100644 --- a/src/inputmapconfig.cc +++ b/src/inputmapconfig.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 "inputmapconfig.h" #include "actiondelegate.h" @@ -11,16 +13,12 @@ // ------------------------------------------------------------------------------------------------- namespace { const InputMapModelItem invalidItem_; -} - -// ------------------------------------------------------------------------------------------------- -InputMapConfigModel::InputMapConfigModel(QObject* parent) - : InputMapConfigModel(nullptr, parent) -{} +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- -InputMapConfigModel::InputMapConfigModel(InputMapper* im, QObject* parent) +InputMapConfigModel::InputMapConfigModel(InputMapper* im, const DeviceId& dId, QObject* parent) : QAbstractTableModel(parent) + , m_currentDeviceId(dId) , m_inputMapper(im) {} @@ -39,10 +37,9 @@ int InputMapConfigModel::columnCount(const QModelIndex& /*parent*/) const // ------------------------------------------------------------------------------------------------- Qt::ItemFlags InputMapConfigModel::flags(const QModelIndex &index) const { - if (index.column() == InputSeqCol) - return (QAbstractTableModel::flags(index) | Qt::ItemIsEditable); - else if (index.column() == ActionCol) + if (index.column() == InputSeqCol || index.column() == ActionCol) { return (QAbstractTableModel::flags(index) | Qt::ItemIsEditable); + } return QAbstractTableModel::flags(index) & ~Qt::ItemIsEditable; } @@ -66,6 +63,7 @@ QVariant InputMapConfigModel::headerData(int section, Qt::Orientation orientatio case InputSeqCol: return tr("Input Sequence"); case ActionTypeCol: return "Type"; case ActionCol: return tr("Mapped Action"); + default: return "Invalid"; } } else if (orientation == Qt::Vertical) @@ -83,8 +81,9 @@ QVariant InputMapConfigModel::headerData(int section, Qt::Orientation orientatio // ------------------------------------------------------------------------------------------------- const InputMapModelItem& InputMapConfigModel::configData(const QModelIndex& index) const { - if (index.row() >= static_cast(m_configItems.size())) + if (index.row() >= static_cast(m_configItems.size())) { return invalidItem_; + } return m_configItems[index.row()]; } @@ -92,7 +91,7 @@ const InputMapModelItem& InputMapConfigModel::configData(const QModelIndex& inde // ------------------------------------------------------------------------------------------------- void InputMapConfigModel::removeConfigItemRows(int fromRow, int toRow) { - if (fromRow > toRow) return; + if (fromRow > toRow) { return; } beginRemoveRows(QModelIndex(), fromRow, toRow); for (int i = toRow; i >= fromRow && i < m_configItems.size(); --i) { @@ -105,7 +104,7 @@ void InputMapConfigModel::removeConfigItemRows(int fromRow, int toRow) // ------------------------------------------------------------------------------------------------- int InputMapConfigModel::addNewItem(std::shared_ptr action) { - if (!action) return -1; + if (!action) { return -1; } const auto row = m_configItems.size(); beginInsertRows(QModelIndex(), row, row); @@ -126,7 +125,7 @@ void InputMapConfigModel::configureInputMapper() // ------------------------------------------------------------------------------------------------- void InputMapConfigModel::removeConfigItemRows(std::vector rows) { - if (rows.empty()) return; + if (rows.empty()) { return; } std::sort(rows.rbegin(), rows.rend()); int seq_last = rows.front(); @@ -161,6 +160,21 @@ void InputMapConfigModel::setInputSequence(const QModelIndex& index, const KeyEv --m_duplicates[c.deviceSequence]; ++m_duplicates[kes]; c.deviceSequence = kes; + + const bool isSpecialMoveInput = !SpecialKeys::logitechSpotlightHoldMove(c.deviceSequence).name.isEmpty(); + + const bool isMoveAction = + (c.action->type() == Action::Type::ScrollHorizontal + || c.action->type() == Action::Type::ScrollVertical + || c.action->type() == Action::Type::VolumeControl); + + if (!isSpecialMoveInput && isMoveAction) { + setItemActionType(index, Action::Type::KeySequence); + } + else if (isSpecialMoveInput && !isMoveAction) { + setItemActionType(index, Action::Type::ScrollVertical); + } + configureInputMapper(); updateDuplicates(); emit dataChanged(index, index, {Qt::DisplayRole, Roles::InputSeqRole}); @@ -174,7 +188,8 @@ void InputMapConfigModel::setKeySequence(const QModelIndex& index, const NativeK if (index.row() < static_cast(m_configItems.size())) { auto& c = m_configItems[index.row()]; - // TODO if action is currently not a keysequence action.. -> just change action type? + // If the current action is not a keysequence action + // -> setting the key sequence is currently ignored. if (auto action = std::dynamic_pointer_cast(c.action)) { if (action->keySequence != ks) { @@ -189,9 +204,9 @@ void InputMapConfigModel::setKeySequence(const QModelIndex& index, const NativeK // ------------------------------------------------------------------------------------------------- void InputMapConfigModel::setItemActionType(const QModelIndex& idx, Action::Type type) { - if (idx.row() >= m_configItems.size()) return; + if (idx.row() >= m_configItems.size()) { return; } auto& item = m_configItems[idx.row()]; - if (item.action->type() == type) return; + if (item.action->type() == type) { return; } switch(type) { @@ -204,6 +219,15 @@ void InputMapConfigModel::setItemActionType(const QModelIndex& idx, Action::Type case Action::Type::ToggleSpotlight: item.action = std::make_shared(); break; + case Action::Type::ScrollHorizontal: + item.action = GlobalActions::scrollHorizontal(); + break; + case Action::Type::ScrollVertical: + item.action = GlobalActions::scrollVertical(); + break; + case Action::Type::VolumeControl: + item.action = GlobalActions::volumeControl(); + break; } configureInputMapper(); @@ -233,7 +257,7 @@ InputMapConfig InputMapConfigModel::configuration() const for (const auto& item : m_configItems) { - if (item.deviceSequence.size() == 0) continue; + if (item.deviceSequence.empty()) { continue; } config.emplace(item.deviceSequence, MappedAction{item.action}); } @@ -256,6 +280,18 @@ void InputMapConfigModel::setConfiguration(const InputMapConfig& config) endResetModel(); } +// ------------------------------------------------------------------------------------------------- +const DeviceId& InputMapConfigModel::deviceId() const +{ + return m_currentDeviceId; +} + +// ------------------------------------------------------------------------------------------------- +void InputMapConfigModel::setDeviceId(const DeviceId& dId) +{ + m_currentDeviceId = dId; +} + // ------------------------------------------------------------------------------------------------- void InputMapConfigModel::updateDuplicates() { @@ -271,12 +307,11 @@ void InputMapConfigModel::updateDuplicates() } } -#include // ------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------- InputMapConfigView::InputMapConfigView(QWidget* parent) - : QTableView(parent) - , m_actionTypeDelegate(new ActionTypeDelegate(this)) + : QTableView(parent), + m_actionTypeDelegate(new ActionTypeDelegate(this)) { verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); @@ -296,26 +331,30 @@ InputMapConfigView::InputMapConfigView(QWidget* parent) setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); connect(this, &QWidget::customContextMenuRequested, this, - [this, actionDelegate](const QPoint& pos) + [this, imSeqDelegate, actionDelegate](const QPoint& pos) { const auto idx = indexAt(pos); - if (!idx.isValid()) return; + if (!idx.isValid()) { return; } - if (idx.column() == InputMapConfigModel::ActionCol) + switch(idx.column()) { - actionDelegate->actionContextMenu(this, qobject_cast(model()), - idx, this->viewport()->mapToGlobal(pos)); - } - else if (idx.column() == InputMapConfigModel::ActionTypeCol) - { - m_actionTypeDelegate->actionContextMenu(this, qobject_cast(model()), - idx, this->viewport()->mapToGlobal(pos)); - } + case InputMapConfigModel::InputSeqCol: + imSeqDelegate->inputSeqContextMenu(this, qobject_cast(model()), + idx, this->viewport()->mapToGlobal(pos)); + break; + case InputMapConfigModel::ActionTypeCol: + m_actionTypeDelegate->actionContextMenu(this, qobject_cast(model()), + idx, this->viewport()->mapToGlobal(pos)); + break; + case InputMapConfigModel::ActionCol: + actionDelegate->actionContextMenu(this, qobject_cast(model()), + idx, this->viewport()->mapToGlobal(pos)); + }; }); connect(this, &QTableView::doubleClicked, this, [this](const QModelIndex& idx) { - if (!idx.isValid()) return; + if (!idx.isValid()) { return; } if (idx.column() == InputMapConfigModel::ActionTypeCol) { const auto pos = viewport()->mapToGlobal(visualRect(currentIndex()).bottomLeft()); m_actionTypeDelegate->actionContextMenu(this, qobject_cast(model()), @@ -354,15 +393,16 @@ void InputMapConfigView::keyPressEvent(QKeyEvent* e) } break; case Qt::Key_Delete: - if (const auto imModel = qobject_cast(model())) - switch (currentIndex().column()) - { - case InputMapConfigModel::InputSeqCol: - imModel->setInputSequence(currentIndex(), KeyEventSequence{}); - return; - case InputMapConfigModel::ActionCol: - imModel->setKeySequence(currentIndex(), NativeKeySequence()); - return; + if (const auto imModel = qobject_cast(model())) { + switch (currentIndex().column()) + { + case InputMapConfigModel::InputSeqCol: + imModel->setInputSequence(currentIndex(), KeyEventSequence{}); + return; + case InputMapConfigModel::ActionCol: + imModel->setKeySequence(currentIndex(), NativeKeySequence()); + return; + } } break; case Qt::Key_Tab: diff --git a/src/inputmapconfig.h b/src/inputmapconfig.h index c2639337..4f375dd9 100644 --- a/src/inputmapconfig.h +++ b/src/inputmapconfig.h @@ -1,6 +1,8 @@ -// 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/ +// - See LICENSE.md and README.md #pragma once +#include "device-defs.h" #include "deviceinput.h" #include @@ -8,9 +10,12 @@ #include // ------------------------------------------------------------------------------------------------- + class ActionTypeDelegate; +class InputSeqDelegate; // ------------------------------------------------------------------------------------------------- +/// Item for the input map model. struct InputMapModelItem { KeyEventSequence deviceSequence; std::shared_ptr action; @@ -18,6 +23,7 @@ struct InputMapModelItem { }; // ------------------------------------------------------------------------------------------------- +/// Input map configuration table model. class InputMapConfigModel : public QAbstractTableModel { Q_OBJECT @@ -26,8 +32,7 @@ class InputMapConfigModel : public QAbstractTableModel enum Roles { InputSeqRole = Qt::UserRole + 1, ActionTypeRole, NativeSeqRole }; enum Columns { InputSeqCol = 0, ActionTypeCol, ActionCol, ColumnsCount}; - InputMapConfigModel(QObject* parent = nullptr); - InputMapConfigModel(InputMapper* im, QObject* parent = nullptr); + InputMapConfigModel(InputMapper* im, const DeviceId& dId, QObject* parent = nullptr); int rowCount(const QModelIndex& parent = QModelIndex()) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override; @@ -49,16 +54,22 @@ class InputMapConfigModel : public QAbstractTableModel InputMapConfig configuration() const; void setConfiguration(const InputMapConfig& config); + const DeviceId& deviceId() const; + void setDeviceId(const DeviceId& dId); + private: void configureInputMapper(); void removeConfigItemRows(int fromRow, int toRow); void updateDuplicates(); + + DeviceId m_currentDeviceId; QPointer m_inputMapper; QVector m_configItems; std::map m_duplicates; }; // ------------------------------------------------------------------------------------------------- +/// Input map configuration view. struct InputMapConfigView : public QTableView { Q_OBJECT diff --git a/src/inputseqedit.cc b/src/inputseqedit.cc index 1fa8c651..0513c05c 100644 --- a/src/inputseqedit.cc +++ b/src/inputseqedit.cc @@ -1,11 +1,15 @@ -// 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 "inputseqedit.h" +#include "device-key-lookup.h" #include "deviceinput.h" #include "inputmapconfig.h" #include "logging.h" #include +#include #include #include #include @@ -26,7 +30,7 @@ namespace { return std::equal(first.cbegin(), first.cend(), second.cbegin(), second.cend(), [](const DeviceInputEvent& e1, const DeviceInputEvent& e2) { - if (e1.type != EV_KEY) return e1 == e2; // just compare for non key events + if (e1.type != EV_KEY) { return e1 == e2; } // just compare for non key events return (e2.type == EV_KEY // special handling for key events... && e1.code == e2.code @@ -37,17 +41,20 @@ namespace { // ----------------------------------------------------------------------------------------------- int drawKeyEvent(int startX, QPainter& p, const QStyleOption& option, const KeyEvent& ke, - bool buttonTap = false) + const DeviceId& dId, bool buttonTap = false) { - if (ke.empty()) return 0; + if (ke.empty()) { return 0; } static auto const pressChar = QChar(0x2193); // ↓ static auto const releaseChar = QChar(0x2191); // ↑ - // TODO some devices (e.g. August WP 200) have buttons that send a key combination + const auto& die = (ke.back().code != SYN_REPORT) ? ke.back() : ke.front(); + const auto& lookupName = KeyName::lookup(dId, die); + + // TODO Some devices (e.g. August WP 200) have buttons that send a key combination // (modifiers + key) - this is ignored completely right now. const auto text = QString("[%1%2%3") - .arg(ke.back().code, 0, 16) + .arg(lookupName.isEmpty() ? QString("%1").arg(die.code, 0, 16) : lookupName) .arg(buttonTap ? pressChar : ke.back().value ? pressChar : releaseChar) .arg(buttonTap ? "" : "]"); @@ -57,10 +64,11 @@ namespace { p.save(); - 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::AlignLeft | Qt::AlignVCenter, text, &br); @@ -82,7 +90,8 @@ namespace { // ----------------------------------------------------------------------------------------------- int drawKeyEventSequence(int startX, QPainter& p, const QStyleOption& option, - const KeyEventSequence& kes, bool drawEmptyPlaceholder = true) + const KeyEventSequence& kes, const DeviceId& dId, + bool drawEmptyPlaceholder = true) { if (kes.empty()) { @@ -94,8 +103,8 @@ namespace { const int paddingX = static_cast(QStaticText(" ").size().width()); for (auto it = kes.cbegin(); it!=kes.cend(); ++it) { - if (it != kes.cbegin()) sequenceWidth += paddingX; - if (startX + sequenceWidth >= option.rect.width()) break; + if (it != kes.cbegin()) { sequenceWidth += paddingX; } + if (startX + sequenceWidth >= option.rect.width()) { break; } const bool isTap = [&]() { // Check if this event and the next event represent a button press & release @@ -107,21 +116,39 @@ namespace { return false; }(); - sequenceWidth += drawKeyEvent(startX + sequenceWidth, p, option, *it, isTap); + sequenceWidth += drawKeyEvent(startX + sequenceWidth, p, option, *it, dId, isTap); } return sequenceWidth; } -} -// ------------------------------------------------------------------------------------------------- -InputSeqEdit::InputSeqEdit(QWidget* parent) - : InputSeqEdit(nullptr, parent) -{} + // ----------------------------------------------------------------------------------------------- + int drawPlaceHolderText(int startX, QPainter& p, const QStyleOption& option, const QString& text, bool textDisabled) + { + const auto r = QRect(QPoint(startX + option.rect.left(), option.rect.top()), + option.rect.bottomRight()); + + p.save(); + if (textDisabled) + { + p.setPen(option.palette.color(QPalette::Disabled, QPalette::Text)); + } + else + { + p.setPen(option.palette.color(QPalette::Text)); + } + QRect br; + p.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, text, &br); + p.restore(); + + return br.width(); + } +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- -InputSeqEdit::InputSeqEdit(InputMapper* im, QWidget* parent) +InputSeqEdit::InputSeqEdit(InputMapper* im, const DeviceId& dId, QWidget* parent) : QWidget(parent) + , m_deviceId(dId) { setInputMapper(im); @@ -132,9 +159,7 @@ InputSeqEdit::InputSeqEdit(InputMapper* im, QWidget* parent) } // ------------------------------------------------------------------------------------------------- -InputSeqEdit::~InputSeqEdit() -{ -} +InputSeqEdit::~InputSeqEdit() = default; // ------------------------------------------------------------------------------------------------- QStyleOptionFrame InputSeqEdit::styleOption() const @@ -165,12 +190,17 @@ QSize InputSeqEdit::sizeHint() const #endif const QStyleOptionFrame option = styleOption(); + + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) return (style()->sizeFromContents(QStyle::CT_LineEdit, &option, QSize(w, h). expandedTo(QApplication::globalStrut()), this)); + #else + return style()->sizeFromContents(QStyle::CT_LineEdit, &option, QSize(w, h), this); + #endif } // ------------------------------------------------------------------------------------------------- -void InputSeqEdit::paintEvent(QPaintEvent*) +void InputSeqEdit::paintEvent(QPaintEvent* /* paintEvent */) { const QStyleOptionFrame option = styleOption(); @@ -187,13 +217,13 @@ void InputSeqEdit::paintEvent(QPaintEvent*) const auto spacingX = QStaticText(" ").size().width(); xPos += drawRecordingSymbol(xPos, p, option) + spacingX; if (m_recordedSequence.empty()) { - xPos += drawPlaceHolderText(xPos, p, option, tr("Press device button(s)...")) + spacingX; + drawPlaceHolderText(xPos, p, option, tr("Press device button(s)...")); } else { - xPos += drawKeyEventSequence(xPos, p, option, m_recordedSequence, false); + drawKeyEventSequence(xPos, p, option, m_recordedSequence, m_deviceId, false); } } else { - xPos += drawKeyEventSequence(xPos, p, option, m_inputSequence); + drawKeyEventSequence(xPos, p, option, m_inputSequence, m_deviceId); } } @@ -206,7 +236,7 @@ const KeyEventSequence& InputSeqEdit::inputSequence() const // ------------------------------------------------------------------------------------------------- void InputSeqEdit::setInputSequence(const KeyEventSequence& is) { - if (is == m_inputSequence) return; + if (is == m_inputSequence) { return; } m_inputSequence = is; update(); @@ -216,7 +246,7 @@ void InputSeqEdit::setInputSequence(const KeyEventSequence& is) // ------------------------------------------------------------------------------------------------- void InputSeqEdit::clear() { - if (m_inputSequence.size() == 0) return; + if (m_inputSequence.empty()) { return; } m_inputSequence.clear(); update(); @@ -227,7 +257,7 @@ void InputSeqEdit::clear() void InputSeqEdit::mouseDoubleClickEvent(QMouseEvent* e) { QWidget::mouseDoubleClickEvent(e); - if (!m_inputMapper) return; + if (!m_inputMapper) { return; } e->accept(); m_inputMapper->setRecordingMode(!m_inputMapper->recordingMode()); } @@ -240,7 +270,8 @@ void InputSeqEdit::keyPressEvent(QKeyEvent* e) m_inputMapper->setRecordingMode(!m_inputMapper->recordingMode()); return; } - else if (e->key() == Qt::Key_Escape) + + if (e->key() == Qt::Key_Escape) { if (m_inputMapper && m_inputMapper->recordingMode()) { m_inputMapper->setRecordingMode(false); @@ -249,10 +280,11 @@ void InputSeqEdit::keyPressEvent(QKeyEvent* e) } else if (e->key() == Qt::Key_Delete) { - if (m_inputMapper && m_inputMapper->recordingMode()) + if (m_inputMapper && m_inputMapper->recordingMode()) { m_inputMapper->setRecordingMode(false); - else + } else { setInputSequence(KeyEventSequence{}); + } return; } @@ -268,8 +300,9 @@ void InputSeqEdit::keyReleaseEvent(QKeyEvent* e) // ------------------------------------------------------------------------------------------------- void InputSeqEdit::focusOutEvent(QFocusEvent* e) { - if (m_inputMapper) + if (m_inputMapper) { m_inputMapper->setRecordingMode(false); + } QWidget::focusOutEvent(e); } @@ -277,9 +310,9 @@ void InputSeqEdit::focusOutEvent(QFocusEvent* e) // ------------------------------------------------------------------------------------------------- void InputSeqEdit::setInputMapper(InputMapper* im) { - if (m_inputMapper == im) return; + if (m_inputMapper == im) { return; } - auto removeIm = [this](){ + const auto removeIm = [this](){ if (m_inputMapper) { m_inputMapper->disconnect(this); this->disconnect(m_inputMapper); @@ -289,7 +322,7 @@ void InputSeqEdit::setInputMapper(InputMapper* im) removeIm(); m_inputMapper = im; - if (m_inputMapper == nullptr) return; + if (m_inputMapper == nullptr) { return; } connect(m_inputMapper, &InputMapper::destroyed, this, [removeIm=std::move(removeIm)](){ @@ -301,14 +334,14 @@ void InputSeqEdit::setInputMapper(InputMapper* im) }); connect(m_inputMapper, &InputMapper::recordingFinished, this, [this](bool canceled){ - if (!canceled) setInputSequence(m_recordedSequence); + if (!canceled) { setInputSequence(m_recordedSequence); } m_inputMapper->setRecordingMode(false); m_recordedSequence.clear(); }); connect(m_inputMapper, &InputMapper::recordingModeChanged, this, [this](bool recording){ update(); - if (!recording) emit editingFinished(this); + if (!recording) { emit editingFinished(this); } }); connect(m_inputMapper, &InputMapper::keyEventRecorded, this, [this](const KeyEvent& ke){ @@ -342,16 +375,7 @@ int InputSeqEdit::drawRecordingSymbol(int startX, QPainter& p, const QStyleOptio // ------------------------------------------------------------------------------------------------- int InputSeqEdit::drawPlaceHolderText(int startX, QPainter& p, const QStyleOption& option, const QString& text) { - const auto r = QRect(QPoint(startX + option.rect.left(), option.rect.top()), - option.rect.bottomRight()); - - p.save(); - p.setPen(option.palette.color(QPalette::Disabled, QPalette::Text)); - QRect br; - p.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, text, &br); - p.restore(); - - return br.width(); + return ::drawPlaceHolderText(startX, p, option, text, true); } // ------------------------------------------------------------------------------------------------- @@ -359,13 +383,14 @@ int InputSeqEdit::drawEmptyIndicator(int startX, QPainter& p, const QStyleOption { p.save(); p.setFont([&p](){ auto f = p.font(); f.setItalic(true); return f; }()); - if (option.state & QStyle::State_Selected) + if (option.state & QStyle::State_Selected) { p.setPen(option.palette.color(QPalette::Disabled, QPalette::HighlightedText)); - else + } else { p.setPen(option.palette.color(QPalette::Disabled, QPalette::Text)); + } static const QStaticText textNone(InputSeqEdit::tr("None")); - const auto top = (option.rect.height() - textNone.size().height()) / 2; + const auto top = static_cast((option.rect.height() - textNone.size().height()) / 2); p.drawStaticText(startX + option.rect.left(), option.rect.top() + top, textNone); p.restore(); return static_cast(textNone.size().width()); @@ -384,7 +409,15 @@ void InputSeqDelegate::paint(QPainter* painter, const QStyleOptionViewItem& opti // Our custom drawing of the KeyEventSequence... const auto& fm = option.fontMetrics; const int xPos = (option.rect.height()-fm.height()) / 2; - drawKeyEventSequence(xPos, *painter, option, imModel->configData(index).deviceSequence); + const auto& keySeq = imModel->configData(index).deviceSequence; + const auto& holdMoveEvent = SpecialKeys::logitechSpotlightHoldMove(keySeq); + + if (!holdMoveEvent.name.isEmpty()) { + drawPlaceHolderText(xPos, *painter, option, holdMoveEvent.name, false); + } + else { + drawKeyEventSequence(xPos, *painter, option, keySeq, imModel->deviceId()); + } if (option.state & QStyle::State_HasFocus) { drawCurrentIndicator(*painter, option); @@ -420,10 +453,10 @@ QWidget* InputSeqDelegate::createEditor(QWidget* parent, { if (const auto imModel = qobject_cast(index.model())) { - if (imModel->inputMapper()) imModel->inputMapper()->setRecordingMode(false); - auto *editor = new InputSeqEdit(imModel->inputMapper(), parent); + if (imModel->inputMapper()) { imModel->inputMapper()->setRecordingMode(false); } + auto *editor = new InputSeqEdit(imModel->inputMapper(), imModel->deviceId(), parent); connect(editor, &InputSeqEdit::editingFinished, this, &InputSeqDelegate::commitAndCloseEditor); - if (imModel->inputMapper()) imModel->inputMapper()->setRecordingMode(true); + if (imModel->inputMapper()) { imModel->inputMapper()->setRecordingMode(true); } return editor; } @@ -474,8 +507,52 @@ QSize InputSeqDelegate::sizeHint(const QStyleOptionViewItem& option, { if (const auto imModel = qobject_cast(index.model())) { - // TODO calc size hint from KeyEventSequence..... + // TODO Calculate size hint from KeyEventSequence..... return QStyledItemDelegate::sizeHint(option, index); } return QStyledItemDelegate::sizeHint(option, index); } + +// ------------------------------------------------------------------------------------------------- +void InputSeqDelegate::inputSeqContextMenu(QWidget* parent, InputMapConfigModel* model, + const QModelIndex& index, const QPoint& globalPos) +{ + if (!index.isValid() || !model) { return; } + + const auto& specialMoveInputs = model->inputMapper()->specialMoveInputs(); + if (!specialMoveInputs.empty()) + { + auto* const menu = new QMenu(parent); + + for (const auto& input : specialMoveInputs) + { + const auto qaction = menu->addAction(input.name); + connect(qaction, &QAction::triggered, this, [model, index, inputSeq=input.keyEventSeq](){ + model->setInputSequence(index, inputSeq); + const auto& currentItem = model->configData(index); + if (!currentItem.action) { + model->setItemActionType(index, Action::Type::ScrollVertical); + } + else + { + switch (currentItem.action->type()) + { + case Action::Type::ScrollHorizontal: // [[fallthrough]]; + case Action::Type::ScrollVertical: // [[fallthrough]]; + case Action::Type::VolumeControl: { + // scrolling and volume control allowed for special input + break; + } + default: { + model->setItemActionType(index, Action::Type::ScrollVertical); + break; + } + } + } + }); + } + + menu->exec(globalPos); + menu->deleteLater(); + } +} diff --git a/src/inputseqedit.h b/src/inputseqedit.h index 3a7080b8..c19635e8 100644 --- a/src/inputseqedit.h +++ b/src/inputseqedit.h @@ -1,6 +1,8 @@ -// 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 "device-defs.h" #include "deviceinput.h" #include @@ -8,6 +10,7 @@ // ------------------------------------------------------------------------------------------------- class QStyleOptionFrame; +class InputMapConfigModel; // ------------------------------------------------------------------------------------------------- class InputSeqEdit : public QWidget @@ -15,14 +18,11 @@ class InputSeqEdit : public QWidget Q_OBJECT public: - InputSeqEdit(QWidget* parent = nullptr); - InputSeqEdit(InputMapper* im, QWidget* parent = nullptr); + InputSeqEdit(InputMapper* im, const DeviceId& dId, QWidget* parent = nullptr); ~InputSeqEdit(); QSize sizeHint() const override; - void setInputMapper(InputMapper* im); - const KeyEventSequence& inputSequence() const; void setInputSequence(const KeyEventSequence& is); @@ -39,6 +39,8 @@ class InputSeqEdit : public QWidget static int drawEmptyIndicator(int startX, QPainter& p, const QStyleOption& option); protected: + void setInputMapper(InputMapper* im); + void paintEvent(QPaintEvent* e) override; void mouseDoubleClickEvent(QMouseEvent* e) override; void keyPressEvent(QKeyEvent* e) override; @@ -47,6 +49,7 @@ class InputSeqEdit : public QWidget QStyleOptionFrame styleOption() const; private: + DeviceId m_deviceId; InputMapper* m_inputMapper = nullptr; KeyEventSequence m_inputSequence; KeyEventSequence m_recordedSequence; @@ -68,6 +71,8 @@ class InputSeqDelegate : public QStyledItemDelegate QWidget *createEditor(QWidget*, const QStyleOptionViewItem&, const QModelIndex&) const override; void setEditorData(QWidget* editor, const QModelIndex& index) const override; void setModelData(QWidget* editor, QAbstractItemModel*, const QModelIndex&) const override; + void inputSeqContextMenu(QWidget* parent, InputMapConfigModel* model, const QModelIndex& index, + const QPoint& globalPos); static void drawCurrentIndicator(QPainter &p, const QStyleOption& option); diff --git a/src/linuxdesktop.cc b/src/linuxdesktop.cc index f3cbf133..72428b5c 100644 --- a/src/linuxdesktop.cc +++ b/src/linuxdesktop.cc @@ -1,16 +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 + #include "linuxdesktop.h" #include "logging.h" #include -#include +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + #include +#endif #include #include #include #include -#if HAS_Qt5_DBus +#if HAS_Qt_DBus #include #include #endif @@ -18,7 +22,7 @@ LOGGING_CATEGORY(desktop, "desktop") namespace { -#if HAS_Qt5_DBus +#if HAS_Qt_DBus // ----------------------------------------------------------------------------------------------- QPixmap grabScreenDBusGnome() { @@ -27,8 +31,8 @@ namespace { QStringLiteral("/org/gnome/Shell/Screenshot"), QStringLiteral("org.gnome.Shell.Screenshot")); QDBusReply reply = interface.call(QStringLiteral("Screenshot"), false, false, filepath); - - if (reply.value()) + + if (reply.value()) { QPixmap pm(filepath); QFile::remove(filepath); @@ -53,7 +57,7 @@ namespace { } return pm; } -#endif // HAS_Qt5_DBus +#endif // HAS_Qt_DBus // ----------------------------------------------------------------------------------------------- QPixmap grabScreenVirtualDesktop(QScreen* screen) @@ -63,10 +67,14 @@ namespace { g = g.united(s->geometry()); } + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QPixmap pm(QApplication::primaryScreen()->grabWindow( QApplication::desktop()->winId(), g.x(), g.y(), g.width(), g.height())); + #else + QPixmap pm(QApplication::primaryScreen()->grabWindow(0, g.x(), g.y(), g.width(), g.height())); + #endif - if (!pm.isNull()) + if (!pm.isNull()) { pm.setDevicePixelRatio(screen->devicePixelRatio()); return pm.copy(screen->geometry()); @@ -87,7 +95,7 @@ LinuxDesktop::LinuxDesktop(QObject* parent) const auto xdgCurrentDesktop = env.value(QStringLiteral("XDG_CURRENT_DESKTOP")); if (gnomeSessionId.size() || xdgCurrentDesktop.contains("Gnome", Qt::CaseInsensitive)) { m_type = LinuxDesktop::Type::Gnome; - } + } else if (kdeFullSession.size() || desktopSession == "kde-plasma") { m_type = LinuxDesktop::Type::KDE; } @@ -96,18 +104,20 @@ LinuxDesktop::LinuxDesktop(QObject* parent) { // check for wayland session const auto waylandDisplay = env.value(QStringLiteral("WAYLAND_DISPLAY")); const auto xdgSessionType = env.value(QStringLiteral("XDG_SESSION_TYPE")); - m_wayland = (xdgSessionType == "wayland") + m_wayland = (xdgSessionType == "wayland") || waylandDisplay.contains("wayland", Qt::CaseInsensitive); } } QPixmap LinuxDesktop::grabScreen(QScreen* screen) const { - if (screen == nullptr) + if (screen == nullptr) { return QPixmap(); - - if (isWayland()) + } + + if (isWayland()) { return grabScreenWayland(screen); + } #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) const bool isVirtualDesktop = QApplication::primaryScreen()->virtualSiblings().size() > 1; @@ -115,24 +125,25 @@ QPixmap LinuxDesktop::grabScreen(QScreen* screen) const const bool isVirtualDesktop = QApplication::desktop()->isVirtualDesktop(); #endif - if (isVirtualDesktop) + if (isVirtualDesktop) { return grabScreenVirtualDesktop(screen); - + } + // everything else.. usually X11 return screen->grabWindow(0); } QPixmap LinuxDesktop::grabScreenWayland(QScreen* screen) const { -#if HAS_Qt5_DBus +#if HAS_Qt_DBus QPixmap pm; - switch (type()) + switch (type()) { - case LinuxDesktop::Type::Gnome: - pm = grabScreenDBusGnome(); + case LinuxDesktop::Type::Gnome: + pm = grabScreenDBusGnome(); break; - case LinuxDesktop::Type::KDE: - pm = grabScreenDBusKde(); + case LinuxDesktop::Type::KDE: + pm = grabScreenDBusKde(); break; default: logWarning(desktop) << tr("Currently zoom on Wayland is only supported via DBus on KDE and GNOME."); diff --git a/src/linuxdesktop.h b/src/linuxdesktop.h index c29dca9b..1c9048a1 100644 --- a/src/linuxdesktop.h +++ b/src/linuxdesktop.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 @@ -6,7 +7,7 @@ class QScreen; -class LinuxDesktop : public QObject +class LinuxDesktop : public QObject { Q_OBJECT diff --git a/src/logging.cc b/src/logging.cc index 46e4ee93..9a77b08e 100644 --- a/src/logging.cc +++ b/src/logging.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 "logging.h" #include @@ -119,10 +121,11 @@ namespace { const auto logMsg = QString("[%1][%2][%3] %4").arg(QDateTime::currentDateTime().toString(dateFormat), typeToShortString(type), category, msgQString); - if (type == QtDebugMsg || type == QtInfoMsg) + if (type == QtDebugMsg || type == QtInfoMsg) { std::cout << qUtf8Printable(logMsg) << std::endl; - else + } else { std::cerr << qUtf8Printable(logMsg) << std::endl; + } logToTextEdit(logMsg); } @@ -132,7 +135,7 @@ namespace logging { void registerTextEdit(QPlainTextEdit* textEdit) { logPlainTextEdit = textEdit; - if (!logPlainTextEdit) return; + if (!logPlainTextEdit) { return; } const auto index = logPlainTextEdit->metaObject()->indexOfMethod("appendPlainText(QString)"); logAppendMetaMethod = logPlainTextEdit->metaObject()->method(index); @@ -160,20 +163,20 @@ namespace logging { level levelFromName(const QString& name) { const auto lvlName = name.toLower(); - if (lvlName == "dbg" || lvlName == "debug") return level::debug; - if (lvlName == "inf" || lvlName == "info") return level::info; - if (lvlName == "wrn" || lvlName == "warning") return level::warning; - if (lvlName == "err" || lvlName == "error") return level::error; + if (lvlName == "dbg" || lvlName == "debug") { return level::debug; } + if (lvlName == "inf" || lvlName == "info") { return level::info; } + if (lvlName == "wrn" || lvlName == "warning") { return level::warning; } + if (lvlName == "err" || lvlName == "error") { return level::error; } return level::unknown; } level currentLevel() { - if (currentCategoryFilter == defaultCategoryFilter) return level::custom; - if (currentCategoryFilter == categoryFilterDebug) return level::debug; - if (currentCategoryFilter == categoryFilterInfo) return level::info; - if (currentCategoryFilter == categoryFilterWarning) return level::warning; - if (currentCategoryFilter == categoryFilterError) return level::error; + if (currentCategoryFilter == defaultCategoryFilter) { return level::custom; } + if (currentCategoryFilter == categoryFilterDebug) { return level::debug; } + if (currentCategoryFilter == categoryFilterInfo) { return level::info; } + if (currentCategoryFilter == categoryFilterWarning) { return level::warning; } + if (currentCategoryFilter == categoryFilterError) { return level::error; } return level::unknown; } @@ -181,16 +184,17 @@ namespace logging { { QLoggingCategory::CategoryFilter newFilter = currentCategoryFilter; - if (lvl == level::debug) + if (lvl == level::debug) { newFilter = categoryFilterDebug; - else if (lvl == level::info) + } else if (lvl == level::info) { newFilter = categoryFilterInfo; - else if (lvl == level::warning) + } else if (lvl == level::warning) { newFilter = categoryFilterWarning; - else if (lvl == level::error) + } else if (lvl == level::error) { newFilter = categoryFilterError; - else if (lvl == level::custom) + } else if (lvl == level::custom) { newFilter = defaultCategoryFilter; + } if (newFilter != currentCategoryFilter) { QLoggingCategory::installFilter(newFilter); @@ -198,7 +202,7 @@ namespace logging { } } - QString hexId(unsigned short id) { + QString hexId(uint16_t id) { return QString("%1").arg(id, 4, 16, QChar('0')); } -} +} // end namespace logging diff --git a/src/logging.h b/src/logging.h index 7feed3f9..b849c734 100644 --- a/src/logging.h +++ b/src/logging.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 @@ -69,7 +70,7 @@ namespace logging { void registerTextEdit(QPlainTextEdit* textEdit); - QString hexId(unsigned short id); + QString hexId(uint16_t id); } diff --git a/src/main.cc b/src/main.cc index 850b93e2..26b452fa 100644 --- a/src/main.cc +++ b/src/main.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 "projecteurapp.h" #include "projecteur-GitVersion.h" @@ -12,8 +14,9 @@ #include #endif -#include +#include #include +#include #define XSTRINGIFY(s) STRINGIFY(s) #define STRINGIFY(x) #x @@ -21,6 +24,12 @@ LOGGING_CATEGORY(appMain, "main") namespace { + // ----------------------------------------------------------------------------------------------- + constexpr int PROJECTEUR_ERROR_ANOTHER_INST_RUNNING = 42; + constexpr int PROJECTEUR_ERROR_NO_INSTANCE_FOUND = 43; + constexpr int PROJECTEUR_ERROR_EMPTY_COMMAND_PROPS = 44; + + // ----------------------------------------------------------------------------------------------- class Main : public QObject {}; std::ostream& operator<<(std::ostream& os, const QString& s) { @@ -39,115 +48,264 @@ namespace { auto& operator<<(const T& a) const { return std::cerr << a; } ~error() { std::cerr << std::endl; } }; -} -int main(int argc, char *argv[]) -{ - QCoreApplication::setApplicationName("Projecteur"); - QCoreApplication::setApplicationVersion(projecteur::version_string()); - ProjecteurApplication::Options options; - QStringList ipcCommands; + void ctrl_c_signal_handler(int sig) + { + if (sig == SIGINT) { + print() << "..."; + if (qApp) { QCoreApplication::quit(); } + } + } + + // ----------------------------------------------------------------------------------------------- + // Helper function to get the range of valid values for a string property + QString getValuesDescription(const Settings::StringProperty& sp) + { + if (sp.type == Settings::StringProperty::Type::Integer + || sp.type == Settings::StringProperty::Type::Double) { + return QString("(%1 ... %2)").arg(sp.range[0].toString(), sp.range[1].toString()); + } + + if (sp.type == Settings::StringProperty::Type::Bool) { + return "(false, true)"; + } + + if (sp.type == Settings::StringProperty::Type::Color) { + return "(HTML-color; #RRGGBB)"; + } + + if (sp.type == Settings::StringProperty::Type::StringEnum) { + QStringList values; + for (const auto& v : sp.range) { + values.push_back(v.toString()); + } + return QString("(%1)").arg(values.join(", ")); + } + return QString(); + } + + // ----------------------------------------------------------------------------------------------- + void printVersionInfo(const ProjecteurApplication::Options& options, bool fullVersionOption) + { + print() << QCoreApplication::applicationName().toStdString() << " " + << projecteur::version_string(); + + if (fullVersionOption || + (std::string(projecteur::version_branch()) != "master" && + std::string(projecteur::version_branch()) != "not-within-git-repo")) + { // Not a build from master branch, print out additional information: + print() << " - git-branch: " << projecteur::version_branch(); + print() << " - git-hash: " << projecteur::version_fullhash(); + } + + // Show if we have a build from modified sources + if (projecteur::version_isdirty()) { + print() << " - dirty-flag: " << projecteur::version_isdirty(); + } + + // Additional useful information + if (fullVersionOption) + { + print() << " - compiler: " << XSTRINGIFY(CXX_COMPILER_ID) << " " + << XSTRINGIFY(CXX_COMPILER_VERSION); + print() << " - build-type: " << projecteur::version_buildtype(); + print() << " - qt-version: (build: " << QT_VERSION_STR << ", runtime: " << qVersion() << ")"; + + const auto result = DeviceScan::getDevices(options.additionalDevices); + print() << " - device-scan: " + << QString("(errors: %1, devices: %2 [readable: %3, writable: %4])") + .arg(result.errorMessages.size()).arg(result.devices.size()) + .arg(result.numDevicesReadable).arg(result.numDevicesWritable); + } + } + + // ----------------------------------------------------------------------------------------------- + void printDeviceInfo(const ProjecteurApplication::Options& options) + { + const auto result = DeviceScan::getDevices(options.additionalDevices); + print() << QCoreApplication::applicationName() << " " + << projecteur::version_string() << "; " << Main::tr("device scan") << std::endl; + + for (const auto& errmsg : result.errorMessages) { + print() << "** " << Main::tr("Error: ") << errmsg; + } + + print() << (!result.errorMessages.empty() ? "\n" : "") + << Main::tr(" * Found %1 supported devices. (%2 readable, %3 writable)") + .arg(result.devices.size()).arg(result.numDevicesReadable).arg(result.numDevicesWritable); + + for (const auto& device : result.devices) + { + print() << "\n" + << " +++ " << "name: '" << device.name << "'"; + if (!device.userName.isEmpty()) { + print() << " " << "userName: '" << device.userName << "'"; + } + + const QStringList subDeviceList = [&device](){ + QStringList subDeviceList; + for (const auto& sd: device.subDevices) { + if (sd.deviceFile.size()) { subDeviceList.push_back(sd.deviceFile); } + } + return subDeviceList; + }(); + + const bool allReadable = std::all_of(device.subDevices.cbegin(), device.subDevices.cend(), + [](const auto& subDevice){ + return subDevice.deviceReadable; + }); + + const bool allWriteable = std::all_of(device.subDevices.cbegin(), device.subDevices.cend(), + [](const auto& subDevice){ + return subDevice.deviceWritable; + }); + + print() << " " << "vendorId: " << logging::hexId(device.id.vendorId); + print() << " " << "productId: " << logging::hexId(device.id.productId); + print() << " " << "phys: " << device.id.phys; + print() << " " << "busType: " << toString(device.id.busType); + print() << " " << "devices: " << subDeviceList.join(", "); + print() << " " << "readable: " << (allReadable ? "true" : "false"); + print() << " " << "writable: " << (allWriteable ? "true" : "false"); + } + } + + // ----------------------------------------------------------------------------------------------- + void addDevices(ProjecteurApplication::Options& options, const QStringList& devices) + { + for (auto& deviceValue : devices) { + const auto devAttribs = deviceValue.split(":"); + const uint16_t vendorId = devAttribs.size() > 0 ? devAttribs[0].toUShort(nullptr, 16) : 0; + const uint16_t productId = devAttribs.size() > 1 ? devAttribs[1].toUShort(nullptr, 16) : 0; + if (vendorId == 0 || productId == 0) { + error() << Main::tr("Invalid vendor/productId pair: ") << deviceValue; + } else { + const QString name = (devAttribs.size() >= 3) ? devAttribs[2] : ""; + options.additionalDevices.push_back({vendorId, productId, false, name}); + } + } + } + + // ----------------------------------------------------------------------------------------------- + struct ProjecteurCmdLineParser { QCommandLineParser parser; - parser.setApplicationDescription(Main::tr("Linux/X11 application for the Logitech Spotlight device.")); - const QCommandLineOption versionOption(QStringList{ "v", "version"}, Main::tr("Print application version.")); - const QCommandLineOption fullVersionOption(QStringList{ "f", "fullversion" }); - const QCommandLineOption helpOption(QStringList{ "h", "help"}, Main::tr("Show command line usage.")); - const QCommandLineOption fullHelpOption(QStringList{ "help-all"}, Main::tr("Show complete command line usage with all properties.")); - const QCommandLineOption cfgFileOption(QStringList{ "cfg" }, Main::tr("Set custom config file."), "file"); - const QCommandLineOption commandOption(QStringList{ "c", "command"}, Main::tr("Send command/property to a running instance."), "cmd"); - const QCommandLineOption deviceInfoOption(QStringList{ "d", "device-scan"}, Main::tr("Print device-scan results.")); - const QCommandLineOption logLvlOption(QStringList{ "l", "log-level" }, Main::tr("Set log level (dbg,inf,wrn,err)."), "lvl"); - const QCommandLineOption disableUInputOption(QStringList{ "disable-uinput" }, Main::tr("Disable uinput support.")); - const QCommandLineOption showDlgOnStartOption(QStringList{ "show-dialog" }, Main::tr("Show preferences dialog on start.")); - const QCommandLineOption dialogMinOnlyOption(QStringList{ "m", "minimize-only" }, Main::tr("Only allow minimizing the dialog.")); - const QCommandLineOption disableOverlayOption(QStringList{ "disable-overlay" }, Main::tr("Disable spotlight overlay completely.")); - const QCommandLineOption additionalDeviceOption(QStringList{ "D", "additional-device"}, + + const QCommandLineOption versionOption_ = {QStringList{ "v", "version"}, Main::tr("Print application version.")}; + const QCommandLineOption fullVersionOption_ = QCommandLineOption{QStringList{ "f", "fullversion" }}; + const QCommandLineOption helpOption_ = {QStringList{ "h", "help"}, Main::tr("Show command line usage.")}; + const QCommandLineOption fullHelpOption_ = {QStringList{ "help-all"}, Main::tr("Show complete command line usage with all properties.")}; + const QCommandLineOption cfgFileOption_ = {QStringList{ "cfg" }, Main::tr("Set custom config file."), "file"}; + const QCommandLineOption commandOption_ = {QStringList{ "c", "command"}, Main::tr("Send command/property to a running instance."), "cmd"}; + const QCommandLineOption deviceInfoOption_ = {QStringList{ "d", "device-scan"}, Main::tr("Print device-scan results.")}; + const QCommandLineOption logLvlOption_ = {QStringList{ "l", "log-level" }, Main::tr("Set log level (dbg,inf,wrn,err)."), "lvl"}; + const QCommandLineOption disableUInputOption_ = {QStringList{ "disable-uinput" }, Main::tr("Disable uinput support.")}; + const QCommandLineOption showDlgOnStartOption_ = {QStringList{ "show-dialog" }, Main::tr("Show preferences dialog on start.")}; + const QCommandLineOption dialogMinOnlyOption_ = {QStringList{ "m", "minimize-only" }, Main::tr("Only allow minimizing the dialog.")}; + const QCommandLineOption disableOverlayOption_ = {QStringList{ "disable-overlay" }, Main::tr("Disable spotlight overlay completely.")}; + const QCommandLineOption additionalDeviceOption_ = {QStringList{ "D", "additional-device"}, Main::tr("Additional accepted device; DEVICE = vendorId:productId\n" " " - "e.g., -D 04b3:310c; e.g. -D 0x0c45:0x8101"), "device"); + "e.g., -D 04b3:310c; e.g. -D 0x0c45:0x8101"), "device"}; - parser.addOptions({versionOption, helpOption, fullHelpOption, commandOption, - cfgFileOption, fullVersionOption, deviceInfoOption, logLvlOption, - disableUInputOption, showDlgOnStartOption, dialogMinOnlyOption, - disableOverlayOption, additionalDeviceOption}); + // --------------------------------------------------------------------------------------------- + ProjecteurCmdLineParser() + { + parser.setApplicationDescription(Main::tr("Linux/X11 application for the Logitech Spotlight device.")); + parser.addOptions({versionOption_, helpOption_, fullHelpOption_, commandOption_, + cfgFileOption_, fullVersionOption_, deviceInfoOption_, logLvlOption_, + disableUInputOption_, showDlgOnStartOption_, dialogMinOnlyOption_, + disableOverlayOption_, additionalDeviceOption_}); + } - const QStringList args = [argc, &argv]() + // --------------------------------------------------------------------------------------------- + bool versionOptionSet() const { return parser.isSet(versionOption_); } + bool fullVersionOptionSet() const { return parser.isSet(fullVersionOption_); } + bool helpOptionSet() const { return parser.isSet(helpOption_); } + bool fullHelpOptionSet() const { return parser.isSet(fullHelpOption_); } + bool additionalDeviceOptionSet() const { return parser.isSet(additionalDeviceOption_); } + auto additionalDeviceOptionValues() const { return parser.values(additionalDeviceOption_); } + bool deviceInfoOptionSet() const { return parser.isSet(deviceInfoOption_); } + bool commandOptionSet() const { return parser.isSet(commandOption_); } + bool disableUInputOptionSet() const { return parser.isSet(disableUInputOption_); } + bool showDlgOnStartOptionSet() const { return parser.isSet(showDlgOnStartOption_); } + bool dialogMinOnlyOptionSet() const { return parser.isSet(dialogMinOnlyOption_); } + bool disableOverlayOptionSet() const { return parser.isSet(disableOverlayOption_); } + auto commandOptionValues() const { return parser.values(commandOption_); } + bool cfgFileOptionSet() const { return parser.isSet(cfgFileOption_); } + auto cfgFileOptionValue() const { return parser.value(cfgFileOption_); } + bool logLvlOptionSet() const { return parser.isSet(logLvlOption_); } + auto logLvlOptionValue() const { return parser.value(logLvlOption_); } + + // --------------------------------------------------------------------------------------------- + void processArgs(int argc, char** argv) { - const QStringList qtAppKeyValueOptions = { - "-platform", "-platformpluginpath", "-platformtheme", "-plugin", "-display" - }; - const QStringList qtAppSingleOptions = {"-reverse"}; - QStringList args; - for (int i = 0; i < argc; ++i) - { // Skip some default arguments supported by QtGuiApplication, we don't want to parse them - // but they will get passed through to the ProjecteurApp. - if (qtAppKeyValueOptions.contains(argv[i])) { ++i; } - else if (qtAppSingleOptions.contains(argv[i])) { continue; } - else { args.push_back(argv[i]); } - } - return args; - }(); + const QStringList args = [argc, &argv]() + { + const QStringList qtAppKeyValueOptions = { + "-platform", "-platformpluginpath", "-platformtheme", "-plugin", "-display" + }; + const QStringList qtAppSingleOptions = {"-reverse"}; + QStringList args; + for (int i = 0; i < argc; ++i) + { // Skip some default arguments supported by QtGuiApplication, we don't want to parse them + // but they will get passed through to the ProjecteurApp. + if (qtAppKeyValueOptions.contains(argv[i])) { ++i; } + else if (qtAppSingleOptions.contains(argv[i])) { continue; } + else { args.push_back(argv[i]); } + } + return args; + }(); - parser.process(args); - if (parser.isSet(helpOption) || parser.isSet(fullHelpOption)) + parser.process(args); + } + + // --------------------------------------------------------------------------------------------- + auto value(const QCommandLineOption& option) const { return parser.value(option); } + auto isSet(const QCommandLineOption& option) const { return parser.isSet(option); } + auto values(const QCommandLineOption& option) const { return parser.values(option); } + + // --------------------------------------------------------------------------------------------- + void printHelp(bool fullHelp) { print() << QCoreApplication::applicationName() << " " << projecteur::version_string() << std::endl; print() << "Usage: projecteur [OPTION]..." << std::endl; print() << ""; - print() << " -h, --help " << helpOption.description(); - print() << " --help-all " << fullHelpOption.description(); - print() << " -v, --version " << versionOption.description(); - print() << " --cfg FILE " << cfgFileOption.description(); - print() << " -d, --device-scan " << deviceInfoOption.description(); - print() << " -l, --log-level LEVEL " << logLvlOption.description(); - print() << " -D DEVICE " << additionalDeviceOption.description(); - if (parser.isSet(fullHelpOption)) { - print() << " --disable-uinput " << disableUInputOption.description(); - print() << " --show-dialog " << showDlgOnStartOption.description(); - print() << " -m, --minimize-only " << dialogMinOnlyOption.description(); + print() << " -h, --help " << helpOption_.description(); + print() << " --help-all " << fullHelpOption_.description(); + print() << " -v, --version " << versionOption_.description(); + print() << " --cfg FILE " << cfgFileOption_.description(); + print() << " -d, --device-scan " << deviceInfoOption_.description(); + print() << " -l, --log-level LEVEL " << logLvlOption_.description(); + print() << " -D DEVICE " << additionalDeviceOption_.description(); + if (fullHelp) { + print() << " --disable-uinput " << disableUInputOption_.description(); + print() << " --show-dialog " << showDlgOnStartOption_.description(); + print() << " -m, --minimize-only " << dialogMinOnlyOption_.description(); } - print() << " -c COMMAND|PROPERTY " << commandOption.description() << std::endl; + print() << " -c COMMAND|PROPERTY " << commandOption_.description() << std::endl; print() << ""; - print() << " spot=[on|off|toggle] " << Main::tr("Turn spotlight on/off or toggle."); - print() << " settings=[show|hide] " << Main::tr("Show/hide preferences dialog."); - if (parser.isSet(fullHelpOption)) { - print() << " preset=NAME " << Main::tr("Set a preset."); + print() << " spot=[on|off|toggle] " << Main::tr("Turn spotlight on/off or toggle."); + if (fullHelp) { + print() << " preset=NAME " << Main::tr("Set a preset."); + print() << " vibrate[=I[,L]] " << Main::tr("Send vibrate command to device with intensity,length."); + print() << " spot.size.adjust=[+|-]N " << Main::tr("Increase or decrease spot size by N."); + } + print() << " settings=[show|hide] " << Main::tr("Show/hide preferences dialog."); + if (fullHelp) { + print() << " preset=NAME " << Main::tr("Set a preset."); } - print() << " quit " << Main::tr("Quit the running instance."); + print() << " quit " << Main::tr("Quit the running instance."); // Early return if the user not explicitly requested the full help - if (!parser.isSet(fullHelpOption)) return 0; + if (!fullHelp) { return; } print() << "\n" << ""; - // Helper function to get the range of valid values for a string property - const auto getValues = [](const Settings::StringProperty& sp) -> QString - { - if (sp.type == Settings::StringProperty::Type::Integer - || sp.type == Settings::StringProperty::Type::Double) { - return QString("(%1 ... %2)").arg(sp.range[0].toString(), sp.range[1].toString()); - } - else if (sp.type == Settings::StringProperty::Type::Bool) { - return "(false, true)"; - } - else if (sp.type == Settings::StringProperty::Type::Color) { - return "(HTML-color; #RRGGBB)"; - } - else if (sp.type == Settings::StringProperty::Type::StringEnum) { - QStringList values; - for (const auto& v : sp.range) { - values.push_back(v.toString()); - } - return QString("(%1)").arg(values.join(", ")); - } - return QString(); - }; - int maxPropertyStringLength = 0; const std::vector> propertiesList = - [getValues=std::move(getValues), &maxPropertyStringLength]() + [&maxPropertyStringLength]() { std::vector> list; // Fill temporary list with properties to be able to format our output better @@ -156,7 +314,7 @@ int main(int argc, char *argv[]) { list.emplace_back( QString("%1=[%2]").arg(sp.first, sp.second.typeToString(sp.second.type)), - getValues(sp.second)); + getValuesDescription(sp.second)); maxPropertyStringLength = qMax(maxPropertyStringLength, list.back().first.size()); } @@ -166,116 +324,51 @@ int main(int argc, char *argv[]) for (const auto& sp : propertiesList) { print() << " " << std::left << std::setw(maxPropertyStringLength + 3) << sp.first << sp.second; } + } + }; + +} // end anonymous namespace + + +// ------------------------------------------------------------------------------------------------- +int main(int argc, char *argv[]) +{ + QCoreApplication::setApplicationName("Projecteur"); + QCoreApplication::setApplicationVersion(projecteur::version_string()); + ProjecteurApplication::Options options; + QStringList ipcCommands; + { + ProjecteurCmdLineParser parser; + parser.processArgs(argc, argv); + if (parser.helpOptionSet() || parser.fullHelpOptionSet()) + { + parser.printHelp(parser.fullHelpOptionSet()); return 0; } - if (parser.isSet(additionalDeviceOption)) { - for (auto& deviceValue : parser.values(additionalDeviceOption)) { - const auto devAttribs = deviceValue.split(":"); - const auto vendorId = devAttribs[0].toUShort(nullptr, 16); - const auto productId = devAttribs[1].toUShort(nullptr, 16); - if (vendorId == 0 || productId == 0) { - error() << Main::tr("Invalid vendor/productId pair: ") << deviceValue; - } else { - const QString name = (devAttribs.size() >= 3) ? devAttribs[2] : ""; - options.additionalDevices.push_back({vendorId, productId, false, name}); - } - } + if (parser.additionalDeviceOptionSet()) { + addDevices(options, parser.additionalDeviceOptionValues()); } - if (parser.isSet(versionOption) || parser.isSet(fullVersionOption)) + // Print version information, if option is set + if (parser.versionOptionSet() || parser.fullVersionOptionSet()) { - print() << QCoreApplication::applicationName().toStdString() << " " - << projecteur::version_string(); - - if (parser.isSet(fullVersionOption) || - (std::string(projecteur::version_branch()) != "master" && - std::string(projecteur::version_branch()) != "not-within-git-repo")) - { // Not a build from master branch, print out additional information: - print() << " - git-branch: " << projecteur::version_branch(); - print() << " - git-hash: " << projecteur::version_fullhash(); - } - - // Show if we have a build from modified sources - if (projecteur::version_isdirty()) - print() << " - dirty-flag: " << projecteur::version_isdirty(); - - // Additional useful information - if (parser.isSet(fullVersionOption)) - { - print() << " - compiler: " << XSTRINGIFY(CXX_COMPILER_ID) << " " - << XSTRINGIFY(CXX_COMPILER_VERSION); - print() << " - qt-version: (build: " << QT_VERSION_STR << ", runtime: " << qVersion() << ")"; - - const auto result = DeviceScan::getDevices(options.additionalDevices); - print() << " - device-scan: " - << QString("(errors: %1, devices: %2 [readable: %3, writable: %4])") - .arg(result.errorMessages.size()).arg(result.devices.size()) - .arg(result.numDevicesReadable).arg(result.numDevicesWritable); - } + printVersionInfo(options, parser.fullVersionOptionSet()); return 0; } - if (parser.isSet(deviceInfoOption)) + // Print device information if option is set + if (parser.deviceInfoOptionSet()) { - const auto result = DeviceScan::getDevices(options.additionalDevices); - print() << QCoreApplication::applicationName() << " " - << projecteur::version_string() << "; " << Main::tr("device scan") << std::endl; - - for (const auto& errmsg : result.errorMessages) { - print() << "** " << Main::tr("Error: ") << errmsg; - } - - print() << (result.errorMessages.size() ? "\n" : "") - << Main::tr(" * Found %1 supported devices. (%2 readable, %3 writable)") - .arg(result.devices.size()).arg(result.numDevicesReadable).arg(result.numDevicesWritable); - - const auto busTypeToString = [](DeviceScan::Device::BusType type) -> QString { - if (type == DeviceScan::Device::BusType::Usb) return "USB"; - if (type == DeviceScan::Device::BusType::Bluetooth) return "Bluetooth"; - return "unknown"; - }; - - for (const auto& device : result.devices) - { - print() << "\n" - << " +++ " << "name: '" << device.name << "'"; - if (!device.userName.isEmpty()) { - print() << " " << "userName: '" << device.userName << "'"; - } - - const QStringList subDeviceList = [&device](){ - QStringList subDeviceList; - for (const auto& sd: device.subDevices) { - if (sd.deviceFile.size()) subDeviceList.push_back(sd.deviceFile); - } - return subDeviceList; - }(); - - const bool allReadable = std::all_of(device.subDevices.cbegin(), device.subDevices.cend(), - [](const auto& subDevice){ - return subDevice.deviceReadable; - }); - - const bool allWriteable = std::all_of(device.subDevices.cbegin(), device.subDevices.cend(), - [](const auto& subDevice){ - return subDevice.deviceWritable; - }); - - print() << " " << "vendorId: " << logging::hexId(device.id.vendorId); - print() << " " << "productId: " << logging::hexId(device.id.productId); - print() << " " << "phys: " << device.id.phys; - print() << " " << "busType: " << busTypeToString(device.busType); - print() << " " << "devices: " << subDeviceList.join(", "); - print() << " " << "readable: " << (allReadable ? "true" : "false"); - print() << " " << "writable: " << (allWriteable ? "true" : "false"); - } + printDeviceInfo(options); return 0; } - else if (parser.isSet(commandOption)) + + // Check and trim ipc commands if set + if (parser.commandOptionSet()) { - ipcCommands = parser.values(commandOption); + ipcCommands = parser.commandOptionValues(); for (auto& value : ipcCommands) { value = value.trimmed(); } @@ -283,25 +376,25 @@ int main(int argc, char *argv[]) if (ipcCommands.isEmpty()) { error() << Main::tr("Command/Properties cannot be an empty string."); - return 44; + return PROJECTEUR_ERROR_EMPTY_COMMAND_PROPS; } } - if (parser.isSet(cfgFileOption)) { - options.configFile = parser.value(cfgFileOption); + if (parser.cfgFileOptionSet()) { + options.configFile = parser.cfgFileOptionValue(); } - options.enableUInput = !parser.isSet(disableUInputOption); - options.showPreferencesOnStart = parser.isSet(showDlgOnStartOption); - options.dialogMinimizeOnly = parser.isSet(dialogMinOnlyOption); - options.disableOverlay = parser.isSet(disableOverlayOption); + options.enableUInput = !parser.disableUInputOptionSet(); + options.showPreferencesOnStart = parser.showDlgOnStartOptionSet(); + options.dialogMinimizeOnly = parser.dialogMinOnlyOptionSet(); + options.disableOverlay = parser.disableOverlayOptionSet(); - if (parser.isSet(logLvlOption)) { - const auto lvl = logging::levelFromName(parser.value(logLvlOption)); + if (parser.logLvlOptionSet()) { + const auto lvl = logging::levelFromName(parser.logLvlOptionValue()); if (lvl != logging::level::unknown) { logging::setCurrentLevel(lvl); } else { - error() << Main::tr("Cannot set log level, unknown level: '%1'").arg(parser.value(logLvlOption)); + error() << Main::tr("Cannot set log level, unknown level: '%1'").arg(parser.logLvlOptionValue()); } } } @@ -309,21 +402,23 @@ int main(int argc, char *argv[]) RunGuard guard(QCoreApplication::applicationName()); if (!guard.tryToRun()) { - if (ipcCommands.size()) { + if (ipcCommands.size() > 0) { return ProjecteurCommandClientApp(ipcCommands, argc, argv).exec(); } error() << Main::tr("Another application instance is already running. Exiting."); - return 42; + return PROJECTEUR_ERROR_ANOTHER_INST_RUNNING; } - else if (ipcCommands.size()) + + if (ipcCommands.size() > 0) { // No other application instance running - but command option was used. logInfo(appMain) << Main::tr("Cannot send commands '%1' - no running application instance found.").arg(ipcCommands.join("; ")); logWarning(appMain) << Main::tr("Cannot send commands '%1' - no running application instance found.").arg(ipcCommands.join("; ")); error() << Main::tr("Cannot send commands '%1' - no running application instance found.").arg(ipcCommands.join("; ")); - return 43; + return PROJECTEUR_ERROR_NO_INSTANCE_FOUND; } ProjecteurApplication app(argc, argv, options); + signal(SIGINT, ctrl_c_signal_handler); return app.exec(); } diff --git a/src/nativekeyseqedit.cc b/src/nativekeyseqedit.cc index 898fbe6c..1246fd7a 100644 --- a/src/nativekeyseqedit.cc +++ b/src/nativekeyseqedit.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 "nativekeyseqedit.h" #include "inputmapconfig.h" @@ -17,7 +19,8 @@ namespace { // ----------------------------------------------------------------------------------------------- constexpr int maxKeyCount = 4; // Same as QKeySequence -} + constexpr int keySeqInterval = 950; +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------- @@ -31,14 +34,12 @@ NativeKeySeqEdit::NativeKeySeqEdit(QWidget* parent) setAttribute(Qt::WA_MacShowFocusRect, true); m_timer->setSingleShot(true); - m_timer->setInterval(950); + m_timer->setInterval(keySeqInterval); connect(m_timer, &QTimer::timeout, this, [this](){ setRecording(false); }); } // ------------------------------------------------------------------------------------------------- -NativeKeySeqEdit::~NativeKeySeqEdit() -{ -} +NativeKeySeqEdit::~NativeKeySeqEdit() = default; // ------------------------------------------------------------------------------------------------- const NativeKeySequence& NativeKeySeqEdit::keySequence() const @@ -49,7 +50,7 @@ const NativeKeySequence& NativeKeySeqEdit::keySequence() const // ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::setKeySequence(const NativeKeySequence& nks) { - if (nks == m_nativeSequence) return; + if (nks == m_nativeSequence) { return; } m_nativeSequence = nks; update(); @@ -59,7 +60,7 @@ void NativeKeySeqEdit::setKeySequence(const NativeKeySequence& nks) // ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::clear() { - if (m_nativeSequence.count() == 0) return; + if (m_nativeSequence.count() == 0) { return; } m_nativeSequence.clear(); update(); @@ -99,12 +100,16 @@ QSize NativeKeySeqEdit::sizeHint() const opt.fontMetrics.width(m_nativeSequence.toString())); #endif + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) return (style()->sizeFromContents(QStyle::CT_LineEdit, &opt, QSize(w, h). expandedTo(QApplication::globalStrut()), this)); + #else + return style()->sizeFromContents(QStyle::CT_LineEdit, &opt, QSize(w, h), this); + #endif } // ------------------------------------------------------------------------------------------------- -void NativeKeySeqEdit::paintEvent(QPaintEvent*) +void NativeKeySeqEdit::paintEvent(QPaintEvent* /* event */) { const QStyleOptionFrame option = styleOption(); @@ -151,7 +156,7 @@ void NativeKeySeqEdit::reset() // ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::setRecording(bool doRecord) { - if (m_recording == doRecord) return; + if (m_recording == doRecord) { return; } m_recording = doRecord; @@ -203,7 +208,7 @@ bool NativeKeySeqEdit::event(QEvent* e) return QWidget::event(e); } -//------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::recordKeyPressEvent(QKeyEvent* e) { int key = m_lastKey = e->key(); @@ -256,7 +261,7 @@ void NativeKeySeqEdit::recordKeyPressEvent(QKeyEvent* e) } } -//------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::keyPressEvent(QKeyEvent* e) { if (!recording()) @@ -266,11 +271,13 @@ void NativeKeySeqEdit::keyPressEvent(QKeyEvent* e) setRecording(true); return; } - else if (e->key() == Qt::Key_Delete) + + if (e->key() == Qt::Key_Delete) { clear(); return; } + QWidget::keyPressEvent(e); return; } @@ -278,7 +285,7 @@ void NativeKeySeqEdit::keyPressEvent(QKeyEvent* e) recordKeyPressEvent(e); } -//------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::keyReleaseEvent(QKeyEvent* e) { if (recording()) @@ -305,26 +312,26 @@ void NativeKeySeqEdit::keyReleaseEvent(QKeyEvent* e) QWidget::keyReleaseEvent(e); } -//------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::focusOutEvent(QFocusEvent* e) { setRecording(false); QWidget::focusOutEvent(e); } -//------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- int NativeKeySeqEdit::getQtModifiers(Qt::KeyboardModifiers state) { int result = 0; - if (state & Qt::ControlModifier) result |= Qt::ControlModifier; - if (state & Qt::MetaModifier) result |= Qt::MetaModifier; - if (state & Qt::AltModifier) result |= Qt::AltModifier; - if (state & Qt::ShiftModifier) result |= Qt::ShiftModifier; - if (state & Qt::GroupSwitchModifier) result |= Qt::GroupSwitchModifier; + if (state & Qt::ControlModifier) { result |= Qt::ControlModifier; } + if (state & Qt::MetaModifier) { result |= Qt::MetaModifier; } + if (state & Qt::AltModifier) { result |= Qt::AltModifier; } + if (state & Qt::ShiftModifier) { result |= Qt::ShiftModifier; } + if (state & Qt::GroupSwitchModifier) { result |= Qt::GroupSwitchModifier; } return result; } -//------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- uint16_t NativeKeySeqEdit::getNativeModifiers(const std::set& modifiersPressed) { using Modifier = NativeKeySequence::Modifier; @@ -366,10 +373,11 @@ int NativeKeySeqEdit::drawText(int startX, QPainter& p, const QStyleOption& opti option.rect.bottomRight()); p.save(); - 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::AlignLeft | Qt::AlignVCenter, text, &br); diff --git a/src/nativekeyseqedit.h b/src/nativekeyseqedit.h index 0b035bb8..f0b04305 100644 --- a/src/nativekeyseqedit.h +++ b/src/nativekeyseqedit.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 // _Note_: This is custom implementation similar to QKeySequenceEdit. Unfortunately QKeySequence diff --git a/src/preferencesdlg.cc b/src/preferencesdlg.cc index 792c49e1..47020fce 100644 --- a/src/preferencesdlg.cc +++ b/src/preferencesdlg.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 "preferencesdlg.h" #include "projecteur-GitVersion.h" // auto generated version information @@ -9,9 +11,9 @@ #include "logging.h" #include "settings.h" +#include #include #include -#include #include #include #include @@ -29,8 +31,10 @@ #include #include -#if HAS_Qt5_X11Extras -#include +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + #if HAS_Qt_X11Extras + #include + #endif #endif #include @@ -51,7 +55,7 @@ namespace { { CURSOR_PATH "cursor-uparrow.png", {"Up Arrow Cursor", Qt::UpArrowCursor}}, { CURSOR_PATH "cursor-whatsthis.png", {"What't This Cursor", Qt::WhatsThisCursor}}, }; -} +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- PreferencesDialog::PreferencesDialog(Settings* settings, Spotlight* spotlight, @@ -79,7 +83,8 @@ PreferencesDialog::PreferencesDialog(Settings* settings, Spotlight* spotlight, const auto tabWidget = new QTabWidget(this); tabWidget->addTab(settingsWidget, tr("Spotlight")); - tabWidget->addTab(new DevicesWidget(settings, spotlight, this), tr("Devices")); + m_deviceswidget = new DevicesWidget(settings, spotlight, this); + tabWidget->addTab(m_deviceswidget, tr("Devices")); tabWidget->addTab(createLogTabWidget(), tr("Log")); const auto overlayCheckBox = new QCheckBox(this); @@ -97,7 +102,6 @@ PreferencesDialog::PreferencesDialog(Settings* settings, Spotlight* spotlight, connect(overlayCheckBox, &QCheckBox::toggled, this, [settings](bool checked){ settings->setOverlayDisabled(!checked); - }); connect(settings, &Settings::overlayDisabledChanged, this, @@ -145,7 +149,7 @@ QWidget* PreferencesDialog::createSettingsTabWidget(Settings* settings) const auto mainVBox = new QVBoxLayout(widget); mainVBox->addLayout(mainHBox); mainVBox->addWidget(presetSelector); -#if HAS_Qt5_X11Extras +#if HAS_Qt_X11Extras mainVBox->addWidget(createCompositorWarningWidget()); #endif mainVBox->addLayout(hbox); @@ -188,8 +192,9 @@ QWidget* PreferencesDialog::createPresetSelector(Settings* settings) deleteBtn->setEnabled(index > 0); m_presetCombo->setStyle(index == 0 ? &*m_presetComboStyle : normalComboStyle); - if (index > 0 && !m_presetCombo->currentText().isEmpty()) + if (index > 0 && !m_presetCombo->currentText().isEmpty()) { settings->loadPreset(m_presetCombo->currentText()); + } }); connect(newBtn, &QPushButton::clicked, this, [newBtn, settings, this]() @@ -230,7 +235,7 @@ QWidget* PreferencesDialog::createPresetSelector(Settings* settings) connect(deleteBtn, &QPushButton::clicked, this, [this, settings]() { - if (m_presetCombo->currentIndex() < 0) return; + if (m_presetCombo->currentIndex() < 0) { return; } settings->removePreset(m_presetCombo->currentText()); }); @@ -252,7 +257,7 @@ QWidget* PreferencesDialog::createPresetSelector(Settings* settings) } // ------------------------------------------------------------------------------------------------- -#if HAS_Qt5_X11Extras +#if HAS_Qt_X11Extras QWidget* PreferencesDialog::createCompositorWarningWidget() { if (!QX11Info::isPlatformX11()) @@ -365,7 +370,7 @@ QGroupBox* PreferencesDialog::createShapeGroupBox(Settings* settings) // Function for updating all spotlight shape related widgets auto updateShapeSettingsWidgets = [settings, shapeCombo, shapeRotationSb, shapeRotationLabel, spotGrid, this]() { - if (shapeCombo->currentIndex() == -1) return; + if (shapeCombo->currentIndex() == -1) { return; } const QString shapeQml = shapeCombo->itemData(shapeCombo->currentIndex()).toString(); const auto& shapes = settings->spotShapes(); auto it = std::find_if(shapes.cbegin(), shapes.cend(), [&shapeQml](const Settings::SpotShape& s) { @@ -398,9 +403,13 @@ QGroupBox* PreferencesDialog::createShapeGroupBox(Settings* settings) int row = startRow; for (const auto& s : it->shapeSettings()) { - if (row >= startRow + maxRows) break; + if (row >= startRow + maxRows) { break; } spotGrid->addWidget(new QLabel(s.displayName(), this),row, 0); + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) if (s.defaultValue().type() == QVariant::Int) + #else + if (s.defaultValue().metaType().id() == QMetaType::Int) + #endif { const auto spinbox = new QSpinBox(this); spinbox->setMaximum(s.maxValue().toInt()); @@ -419,7 +428,7 @@ QGroupBox* PreferencesDialog::createShapeGroupBox(Settings* settings) connect(pm, &QQmlPropertyMap::valueChanged, spinbox, [s, spinbox, this](const QString& key, const QVariant& value) { - if (key != s.settingsKey() || !value.isValid()) return; + if (key != s.settingsKey() || !value.isValid()) { return; } spinbox->setValue(value.toInt()); resetPresetCombo(); }); @@ -723,7 +732,7 @@ QWidget* PreferencesDialog::createLogTabWidget() QString logFilter(tr("Log files (*.log *.txt)")); const auto logFile = QFileDialog::getSaveFileName(this, tr("Save log file"), defaultFile, logFilter, &logFilter); - if (logFile.isEmpty()) return; + if (logFile.isEmpty()) { return; } saveDir = QFileInfo(logFile).path(); QFile f(logFile); @@ -761,8 +770,9 @@ QWidget* PreferencesDialog::createLogTabWidget() // ------------------------------------------------------------------------------------------------- void PreferencesDialog::setMode(Mode dialogMode) { - if (m_dialogMode == dialogMode) + if (m_dialogMode == dialogMode) { return; + } setDialogMode(dialogMode); } @@ -792,14 +802,15 @@ void PreferencesDialog::setDialogMode(Mode dialogMode) // ------------------------------------------------------------------------------------------------- void PreferencesDialog::resetPresetCombo() { - if (m_presetCombo) m_presetCombo->setCurrentIndex(0); + if (m_presetCombo) { m_presetCombo->setCurrentIndex(0); } } // ------------------------------------------------------------------------------------------------- void PreferencesDialog::setDialogActive(bool active) { - if (active == m_active) + if (active == m_active) { return; + } m_active = active; emit dialogActiveChanged(active); @@ -818,7 +829,7 @@ bool PreferencesDialog::event(QEvent* e) } // ------------------------------------------------------------------------------------------------- -void PreferencesDialog::closeEvent(QCloseEvent*) +void PreferencesDialog::closeEvent(QCloseEvent* /* ev */) { if (m_dialogMode == Mode::MinimizeOnlyDialog) { emit exitApplicationRequested(); diff --git a/src/preferencesdlg.h b/src/preferencesdlg.h index 9e4cec8f..21a9b8b3 100644 --- a/src/preferencesdlg.h +++ b/src/preferencesdlg.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 @@ -11,6 +12,7 @@ class QComboBox; class QGroupBox; class Settings; class Spotlight; +class DevicesWidget; // ------------------------------------------------------------------------------------------------- class PresetComboCustomStyle : public QProxyStyle @@ -63,7 +65,7 @@ class PreferencesDialog : public QDialog QWidget* createMultiScreenWidget(Settings* settings); QGroupBox* createZoomGroupBox(Settings* settings); QWidget* createPresetSelector(Settings* settings); -#if HAS_Qt5_X11Extras +#if HAS_Qt_X11Extras QWidget* createCompositorWarningWidget(); #endif QWidget* createLogTabWidget(); @@ -73,6 +75,7 @@ class PreferencesDialog : public QDialog QComboBox* m_presetCombo = nullptr; QPushButton* m_closeMinimizeBtn = nullptr; QPushButton* m_exitBtn = nullptr; + DevicesWidget* m_deviceswidget = nullptr; bool m_active = false; Mode m_dialogMode = Mode::ClosableDialog; quint32 m_discardedLogCount = 0; diff --git a/src/projecteur-icons-def.h b/src/projecteur-icons-def.h index d0337e14..44fe3241 100644 --- a/src/projecteur-icons-def.h +++ b/src/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` @@ -8,6 +9,7 @@ namespace Font enum Icon { arrow_73 = 0xf10b, // svg/iconmonstr-arrow-73.svg arrow_74 = 0xf10c, // svg/iconmonstr-arrow-74.svg + audio_6 = 0xf11c, // svg/iconmonstr-audio-6.svg battery_3 = 0xf100, // svg/iconmonstr-battery-3.svg battery_4 = 0xf101, // svg/iconmonstr-battery-4.svg battery_5 = 0xf102, // svg/iconmonstr-battery-5.svg @@ -15,6 +17,8 @@ namespace Font battery_7 = 0xf104, // svg/iconmonstr-battery-7.svg connection_8 = 0xf114, // svg/iconmonstr-connection-8.svg control_panel_9 = 0xf105, // svg/iconmonstr-control-panel-9.svg + cursor_21 = 0xf119, // svg/iconmonstr-cursor-21.svg + cursor_21_rotated = 0xf11a, // svg/iconmonstr-cursor-21-rotated.svg gear_12 = 0xf106, // svg/iconmonstr-gear-12.svg keyboard_14 = 0xf10e, // svg/iconmonstr-keyboard-14.svg keyboard_4 = 0xf10f, // svg/iconmonstr-keyboard-4.svg diff --git a/src/projecteurapp.cc b/src/projecteurapp.cc index dcf86a5a..378b3c6e 100644 --- a/src/projecteurapp.cc +++ b/src/projecteurapp.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 "projecteurapp.h" #include "aboutdlg.h" +#include "device-command-helper.h" #include "imageitem.h" #include "linuxdesktop.h" #include "logging.h" @@ -9,8 +12,10 @@ #include "settings.h" #include "spotlight.h" +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) #include -#include +#endif + #include #include #include @@ -34,7 +39,7 @@ namespace { QString localServerName() { return QCoreApplication::applicationName() + "_local_socket"; } -} +} // end anonymous namespace // ------------------------------------------------------------------------------------------------- ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Options& options) @@ -45,7 +50,7 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio , m_linuxDesktop(new LinuxDesktop(this)) , m_xcbOnWayland(QGuiApplication::platformName() == "xcb" && m_linuxDesktop->isWayland()) { - if (screens().size() < 1) + if (screens().empty()) { const auto title = tr("No Screens detected"); const auto text = tr("screens().size() returned a size < 1. Exiting."); @@ -55,6 +60,7 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio return; } + // don't quit application when last windows (usually preferences dialog) is closed setQuitOnLastWindowClosed(false); QFontDatabase::addApplicationFont(":/icons/projecteur-icons.ttf"); @@ -63,19 +69,21 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio m_spotlight = new Spotlight(this, Spotlight::Options{options.enableUInput, options.additionalDevices}, m_settings); + m_deviceCommandHelper = new DeviceCommandHelper(this, m_spotlight); + m_settings->setOverlayDisabled(options.disableOverlay); - m_dialog.reset(new PreferencesDialog(m_settings, m_spotlight, - options.dialogMinimizeOnly - ? PreferencesDialog::Mode::MinimizeOnlyDialog - : PreferencesDialog::Mode::ClosableDialog)); + m_dialog = std::make_unique(m_settings, m_spotlight, + options.dialogMinimizeOnly + ? PreferencesDialog::Mode::MinimizeOnlyDialog + : PreferencesDialog::Mode::ClosableDialog); connect(&*m_dialog, &PreferencesDialog::testButtonClicked, this, [this](){ m_spotlight->setSpotActive(true); }); const QString desktopEnv = m_linuxDesktop->type() == LinuxDesktop::Type::KDE ? "KDE" : - m_linuxDesktop->type() == LinuxDesktop::Type::Gnome ? "Gnome" - : tr("Unknown"); + m_linuxDesktop->type() == LinuxDesktop::Type::Gnome ? "Gnome" + : tr("Unknown"); logDebug(mainapp) << tr("Qt platform plugin: %1;").arg(QGuiApplication::platformName()) << tr("Desktop Environment: %1;").arg(desktopEnv) @@ -103,7 +111,12 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio if (m_windowQmlComponent->status() != QQmlComponent::Status::Ready) { const auto title = tr("Overlay window error."); const auto text = tr("Qml component has status '%1'. Exiting.").arg(m_windowQmlComponent->status()); + logError(mainapp) << title << ";" << text; + for (const auto& error : m_windowQmlComponent->errors()) { + logError(mainapp) << error.toString(); + } + QMessageBox::critical(nullptr, title, text); QTimer::singleShot(0, this, [this](){ this->exit(2); }); return; @@ -116,15 +129,16 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio connect(m_settings, &Settings::multiScreenOverlayEnabledChanged, this, [this](){ setupScreenOverlays(); }); connect(m_settings, &Settings::overlayDisabledChanged, this, [this](bool disabled){ if (disabled) { - if (m_spotlight->spotActive()) m_spotlight->setSpotActive(false); - else emit m_spotlight->spotActiveChanged(false); + if (m_spotlight->spotActive()) { m_spotlight->setSpotActive(false); } + else { emit m_spotlight->spotActiveChanged(false); } } else { QTimer::singleShot(0, this, [this](){ - if (m_spotlight->spotActive()) + if (m_spotlight->spotActive()) { emit m_spotlight->spotActiveChanged(true); - else + } else { m_spotlight->setSpotActive(true); + } }); } }); @@ -133,83 +147,77 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio connect(this, &ProjecteurApplication::screenAdded, this, [this](){ setupScreenOverlays(); }); connect(this, &ProjecteurApplication::screenRemoved, this, [this](){ setupScreenOverlays(); }); - // add and connect 'Preferences' tray menu action - const auto actionPref = m_trayMenu->addAction(tr("&Preferences...")); - connect(actionPref, &QAction::triggered, this, [this](){ - this->showPreferences(true); - }); + // Setup the tray icon and menu + setupTrayIcon(); - // add and and connect 'About' tray menu action - const auto actionAbout = m_trayMenu->addAction(tr("&About")); - connect(actionAbout, &QAction::triggered, this, [this]() - { - if (!m_aboutDialog) { - m_aboutDialog = std::make_unique(); - connect(m_aboutDialog.get(), &QDialog::finished, this, [this](int){ - m_aboutDialog.reset(); // No need to keep about dialog in memory, not that important - }); - } - - if (m_aboutDialog->isVisible()) { - m_aboutDialog->show(); - m_aboutDialog->raise(); - m_aboutDialog->activateWindow(); - } else { - m_aboutDialog->exec(); - } - }); - - m_trayMenu->addSeparator(); - const auto actionQuit = m_trayMenu->addAction(tr("&Quit")); - connect(actionQuit, &QAction::triggered, this, [this](){ - m_qmlEngine->deleteLater(); // see: https://bugreports.qt.io/browse/QTBUG-81247 - this->quit(); + connect(this, &ProjecteurApplication::aboutToQuit, this, [this](){ + for (const auto window : m_overlayWindows) { window->close(); } + m_overlayWindows.clear(); }); - m_trayIcon->setContextMenu(&*m_trayMenu); - m_trayIcon->setIcon(QIcon(":/icons/projecteur-tray-64.png")); - m_trayIcon->show(); + // Setup the spotlight connections. + setupSpotlight(); - connect(&*m_trayIcon, &QSystemTrayIcon::activated, this, - [this](QSystemTrayIcon::ActivationReason reason) { - if (reason == QSystemTrayIcon::Trigger) + // Open local server for local IPC commands, e.g. from other command line instances + QLocalServer::removeServer(localServerName()); + if (m_localServer->listen(localServerName())) + { + connect(m_localServer, &QLocalServer::newConnection, this, [this]() { - const auto trayGeometry = m_trayIcon->geometry(); - // This usually won't give us a valid geometry, since Qt isn't drawing the tray icon itself - if (trayGeometry.isValid()) { - m_trayIcon->contextMenu()->popup(m_trayIcon->geometry().center()); - } else { - // It's tricky to get the same behavior on all desktop environments. While on GNOME3 - // it behaves as one (or most) would expect, it behaves differently on other Desktop - // environments. - // QSystemTrayIcon is a wrapper around the StatusNotfierItem on modern (Linux) Desktops - // see: https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/ - // Via the Qt API there is not much control over how e.g. KDE or GNOME show the icon - // and how it behaves.. e.g. setting something like - // org.freedesktop.StatusNotifierItem.ItemIsMenu to True would be good for KDE Plasma - // see: https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierItem/ - this->showPreferences(true); - } - } - }); + while(QLocalSocket *clientConnection = m_localServer->nextPendingConnection()) + { + connect(clientConnection, &QLocalSocket::readyRead, this, [this, clientConnection]() { + this->readCommand(clientConnection); + }); + connect(clientConnection, &QLocalSocket::disconnected, this, [this, clientConnection]() { + const auto it = m_commandConnections.find(clientConnection); + if (it != m_commandConnections.end()) + { + quint32& commandSize = it->second; + while (clientConnection->bytesAvailable() && commandSize <= clientConnection->bytesAvailable()) { + this->readCommand(clientConnection); + } + m_commandConnections.erase(it); + } + clientConnection->close(); + clientConnection->deleteLater(); + }); - connect(&*m_dialog, &PreferencesDialog::exitApplicationRequested, actionQuit, [actionQuit]() { - logDebug(mainapp) << tr("Exit request from preferences dialog."); - actionQuit->trigger(); - }); + // Timeout timer - if after 5 seconds the connection is still open just disconnect... + const auto clientConnPtr = QPointer(clientConnection); + QTimer::singleShot(5000, clientConnection, [clientConnPtr](){ + if (clientConnPtr) { + // time out + clientConnPtr->disconnectFromServer(); + } + }); - connect(this, &ProjecteurApplication::aboutToQuit, this, [this](){ - for (const auto window : m_overlayWindows) { window->close(); } - m_overlayWindows.clear(); - }); + m_commandConnections.emplace(clientConnection, 0); + } + }); + } + else + { + logError(cmdserver) << tr("Error starting local socket for inter-process communication."); + } +} + +// ------------------------------------------------------------------------------------------------- +ProjecteurApplication::~ProjecteurApplication() +{ + if (m_localServer) { m_localServer->close(); } +} +// ------------------------------------------------------------------------------------------------- +void ProjecteurApplication::setupSpotlight() +{ // Handling of spotlight window when mouse move events from spotlight device are detected connect(m_spotlight, &Spotlight::spotActiveChanged, this, [this](bool active) { if (active && !m_settings->overlayDisabled()) { - if (!m_settings->multiScreenOverlayEnabled()) setScreenForCursorPos(); + if (!m_settings->multiScreenOverlayEnabled()) { setScreenForCursorPos(); } for (const auto window : m_overlayWindows) { @@ -247,7 +255,7 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio // Workaround for 'xcb' on Wayland session (default on Ubuntu) // .. the window in that case is not transparent for inputs and cannot be clicked through. // --> hide the window, although animations will not be visible - if (m_xcbOnWayland) window->hide(); + if (m_xcbOnWayland) { window->hide(); } } if (m_xcbOnWayland && m_dialog->mode() == PreferencesDialog::Mode::MinimizeOnlyDialog && m_dialog->isMinimized()) { // keep Window minimized... @@ -264,55 +272,75 @@ ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Optio m_dialog->activateWindow(); } }); +} - // Open local server for local IPC commands, e.g. from other command line instances - QLocalServer::removeServer(localServerName()); - if (m_localServer->listen(localServerName())) +// ------------------------------------------------------------------------------------------------- +void ProjecteurApplication::setupTrayIcon() +{ + // add and connect 'Preferences' tray menu action + const auto actionPref = m_trayMenu->addAction(tr("&Preferences...")); + connect(actionPref, &QAction::triggered, this, [this](){ + this->showPreferences(true); + }); + + // add and and connect 'About' tray menu action + const auto actionAbout = m_trayMenu->addAction(tr("&About")); + connect(actionAbout, &QAction::triggered, this, [this]() { - connect(m_localServer, &QLocalServer::newConnection, this, [this]() - { - while(QLocalSocket *clientConnection = m_localServer->nextPendingConnection()) - { - connect(clientConnection, &QLocalSocket::readyRead, this, [this, clientConnection]() { - this->readCommand(clientConnection); - }); - connect(clientConnection, &QLocalSocket::disconnected, this, [this, clientConnection]() { - const auto it = m_commandConnections.find(clientConnection); - if (it != m_commandConnections.end()) - { - quint32& commandSize = it->second; - while (clientConnection->bytesAvailable() && commandSize <= clientConnection->bytesAvailable()) { - this->readCommand(clientConnection); - } - m_commandConnections.erase(it); - } - clientConnection->close(); - clientConnection->deleteLater(); - }); + if (!m_aboutDialog) { + m_aboutDialog = new AboutDialog(); + connect(m_aboutDialog, &QDialog::finished, this, [this](int /* result */) { + m_aboutDialog->deleteLater(); // No need to keep about dialog in memory, not that important + }); + } - // Timeout timer - if after 5 seconds the connection is still open just disconnect... - const auto clientConnPtr = QPointer(clientConnection); - QTimer::singleShot(5000, clientConnection, [clientConnPtr](){ - if (clientConnPtr) { - // time out - clientConnPtr->disconnectFromServer(); - } - }); + if (m_aboutDialog->isVisible()) { + m_aboutDialog->show(); + m_aboutDialog->raise(); + m_aboutDialog->activateWindow(); + } else { + m_aboutDialog->open(); + } + }); - m_commandConnections.emplace(clientConnection, 0); + m_trayMenu->addSeparator(); + const auto actionQuit = m_trayMenu->addAction(tr("&Quit")); + connect(actionQuit, &QAction::triggered, this, [this](){ + m_qmlEngine->deleteLater(); // see: https://bugreports.qt.io/browse/QTBUG-81247 + this->quit(); + }); + m_trayIcon->setContextMenu(&*m_trayMenu); + + m_trayIcon->setIcon(QIcon(":/icons/projecteur-tray-64.png")); + m_trayIcon->show(); + + connect(&*m_trayIcon, &QSystemTrayIcon::activated, this, + [this](QSystemTrayIcon::ActivationReason reason) { + if (reason == QSystemTrayIcon::Trigger) + { + const auto trayGeometry = m_trayIcon->geometry(); + // This usually won't give us a valid geometry, since Qt isn't drawing the tray icon itself + if (trayGeometry.isValid()) { + m_trayIcon->contextMenu()->popup(m_trayIcon->geometry().center()); + } else { + // It's tricky to get the same behavior on all desktop environments. While on GNOME3 + // it behaves as one (or most) would expect, it behaves differently on other Desktop + // environments. + // QSystemTrayIcon is a wrapper around the StatusNotfierItem on modern (Linux) Desktops + // see: https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/ + // Via the Qt API there is not much control over how e.g. KDE or GNOME show the icon + // and how it behaves.. e.g. setting something like + // org.freedesktop.StatusNotifierItem.ItemIsMenu to True would be good for KDE Plasma + // see: https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierItem/ + this->showPreferences(true); } - }); - } - else - { - logError(cmdserver) << tr("Error starting local socket for inter-process communication."); - } -} + } + }); -// ------------------------------------------------------------------------------------------------- -ProjecteurApplication::~ProjecteurApplication() -{ - if (m_localServer) m_localServer->close(); + connect(&*m_dialog, &PreferencesDialog::exitApplicationRequested, actionQuit, [actionQuit]() { + logDebug(mainapp) << tr("Exit request from preferences dialog."); + actionQuit->trigger(); + }); } // ------------------------------------------------------------------------------------------------- @@ -352,8 +380,9 @@ void ProjecteurApplication::cursorPositionChanged(const QPoint& pos) // ------------------------------------------------------------------------------------------------- void ProjecteurApplication::updateOverlayWindow(QWindow* window, QScreen* screen) { - if (screen == nullptr) + if (screen == nullptr) { return; + } if (window->screen() == screen && screen->geometry() == window->geometry()) { return; @@ -387,10 +416,11 @@ void ProjecteurApplication::updateOverlayWindow(QWindow* window, QScreen* screen if (wasVisible && wasSpotActive) { QTimer::singleShot(0, this, [this](){ - if (m_spotlight->spotActive()) + if (m_spotlight->spotActive()) { emit m_spotlight->spotActiveChanged(true); - else + } else { m_spotlight->setSpotActive(true); + } }); } } @@ -422,7 +452,7 @@ void ProjecteurApplication::setupScreenOverlays() m_screenWindowMap.clear(); const auto currentScreens = screens(); - if (currentScreens.size() == 0) + if (currentScreens.empty()) { for (const auto window : m_overlayWindows) { window->deleteLater(); } m_overlayWindows.clear(); @@ -438,7 +468,7 @@ void ProjecteurApplication::setupScreenOverlays() if (m_settings->multiScreenOverlayEnabled()) { const auto it = m_screenWindowMap.find(screen); - if (it == m_screenWindowMap.cend()) return; + if (it == m_screenWindowMap.cend()) { return; } updateOverlayWindow(it->second, it->first); } else { @@ -482,10 +512,11 @@ void ProjecteurApplication::setupScreenOverlays() // make sure it will be activated again. if (wasSpotActive) { QTimer::singleShot(0, this, [this](){ - if (m_spotlight->spotActive()) + if (m_spotlight->spotActive()) { emit m_spotlight->spotActiveChanged(true); - else + } else { m_spotlight->setSpotActive(true); + } }); } } @@ -499,7 +530,7 @@ quint64 ProjecteurApplication::currentSpotScreen() const // ------------------------------------------------------------------------------------------------- void ProjecteurApplication::setCurrentSpotScreen(quint64 screen) { - if (m_currentSpotScreen == screen) return; + if (m_currentSpotScreen == screen) { return; } m_currentSpotScreen = screen; emit currentSpotScreenChanged(m_currentSpotScreen); } @@ -513,7 +544,7 @@ QPoint ProjecteurApplication::currentCursorPos() const // ------------------------------------------------------------------------------------------------- void ProjecteurApplication::setCurrentCursorPos(const QPoint& pos) { - if (pos == m_currentCursorPos) return; + if (pos == m_currentCursorPos) { return; } m_currentCursorPos = pos; emit currentCursorPosChanged(m_currentCursorPos); } @@ -530,8 +561,9 @@ void ProjecteurApplication::readCommand(QLocalSocket* clientConnection) // Read size of command (always quint32) if not already done. if (commandSize == 0) { - if (clientConnection->bytesAvailable() < static_cast(sizeof(quint32))) + if (clientConnection->bytesAvailable() < static_cast(sizeof(quint32))) { return; + } QDataStream in(clientConnection); in >> commandSize; @@ -557,13 +589,66 @@ void ProjecteurApplication::readCommand(QLocalSocket* clientConnection) logDebug(cmdserver) << tr("Received quit command."); this->quit(); } + else if (cmdKey == "vibrate") // with args intensity (0-255), length (0-10) + { + #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + auto const args = cmdValue.split(QLatin1Char(','), Qt::SkipEmptyParts); + #else + auto const args = cmdValue.split(QLatin1Char(','), QString::SkipEmptyParts); + #endif + + std::uint8_t const intensity = [&args]{ + if (args.size() >= 1) { + bool ok = false; + auto intensity = args[0].toInt(&ok); + if (ok) { + return static_cast(qMin(255, qMax(0, intensity))); + } + } + return std::uint8_t{128}; + }(); + + std::uint8_t const length = [&args]{ + if (args.size() >= 2) { + bool ok = false; + auto intensity = args[1].toInt(&ok); + if (ok) { + return static_cast(qMin(10, qMax(0, intensity))); + } + } + return std::uint8_t{0}; + }(); + + logDebug(cmdserver) << tr("Received command vibrate = intensity:%1, length:%2") + .arg(intensity) + .arg(length); + + m_deviceCommandHelper->sendVibrateCommand(intensity, length); + } + else if (cmdKey == "spot.size.adjust") + { + bool ok = false; + int const sizeAdjust = cmdValue.toInt(&ok); + if (ok) { + logDebug(cmdserver) << tr("Received command spot.size.adjust = %1%2") + .arg(sizeAdjust > 0 ? "+" : "") + .arg(sizeAdjust); + m_settings->setSpotSize(m_settings->spotSize() + sizeAdjust); + } else { + logDebug(cmdserver) << tr("Received invalid value for command spot.size.adjust"); + } + } else if (cmdKey == "spot") { - if (cmdValue.toLower() == "toggle") { + if (cmdValue.isEmpty()) { + logDebug(cmdserver) << tr("Received empty command value for command spot"); + } else if (cmdValue.toLower() == "toggle") { m_spotlight->setSpotActive(!m_spotlight->spotActive()); } else { - const bool active = (cmdValue.toLower() == "on" || cmdValue == "1" || cmdValue.toLower() == "true"); + const bool active = (cmdValue.toLower() == "on" + || cmdValue == "1" + || cmdValue.toLower() == "true"); logDebug(cmdserver) << tr("Received command spot = %1").arg(active); m_spotlight->setSpotActive(active); } @@ -577,7 +662,7 @@ void ProjecteurApplication::readCommand(QLocalSocket* clientConnection) else if (cmdKey == "preset") { logDebug(cmdserver) << tr("Received command preset = %1").arg(cmdValue); - if (!cmdValue.isEmpty()) m_settings->loadPreset(cmdValue); + if (!cmdValue.isEmpty()) { m_settings->loadPreset(cmdValue); } } else if (cmdValue.size()) { @@ -607,13 +692,14 @@ void ProjecteurApplication::showPreferences(bool show) m_dialog->show(); m_dialog->raise(); static const bool qtPlatformIsWayland = QGuiApplication::platformName().toLower().startsWith("wayland"); - if (!qtPlatformIsWayland) m_dialog->activateWindow(); + if (!qtPlatformIsWayland) { m_dialog->activateWindow(); } } else { - if (m_dialog->mode() == PreferencesDialog::Mode::MinimizeOnlyDialog) + if (m_dialog->mode() == PreferencesDialog::Mode::MinimizeOnlyDialog) { m_dialog->showMinimized(); - else + } else { m_dialog->hide(); + } } } @@ -629,24 +715,26 @@ ProjecteurCommandClientApp::ProjecteurCommandClientApp(const QStringList& ipcCom QLocalSocket* const localSocket = new QLocalSocket(this); + auto socketErrorFunc = [this, localSocket](QLocalSocket::LocalSocketError /*socketError*/) { + logError(cmdclient) << tr("Error sending commands: %1", "%1=error message") + .arg(localSocket->errorString()); + localSocket->close(); + QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); + }; + #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) - connect(localSocket, &QLocalSocket::errorOccurred, + connect(localSocket, &QLocalSocket::errorOccurred, this, std::move(socketErrorFunc)); #else connect(localSocket, - static_cast(&QLocalSocket::error), + static_cast(&QLocalSocket::error), + this, std::move(socketErrorFunc)); #endif - this, - [this, localSocket](QLocalSocket::LocalSocketError /*socketError*/) { - logError(cmdclient) << tr("Error sending commands: %1", "%1=error message").arg(localSocket->errorString()); - localSocket->close(); - QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); - }); connect(localSocket, &QLocalSocket::connected, [localSocket, &ipcCommands]() { for (const auto& ipcCommand : ipcCommands) { - if (ipcCommand.isEmpty()) continue; + if (ipcCommand.isEmpty()) { continue; } const QByteArray commandBlock = [&ipcCommand]() { diff --git a/src/projecteurapp.h b/src/projecteurapp.h index 8f2dc8dd..e3ab04ae 100644 --- a/src/projecteurapp.h +++ b/src/projecteurapp.h @@ -1,13 +1,17 @@ -// 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 "spotlight.h" + +#include "devicescan.h" #include +#include #include #include class AboutDialog; +class DeviceCommandHelper; class LinuxDesktop; class PreferencesDialog; class QLocalServer; @@ -17,7 +21,7 @@ class QQmlApplicationEngine; class QQmlComponent; class QSystemTrayIcon; class Settings; -class Settings; +class Spotlight; class ProjecteurApplication : public QApplication { @@ -67,14 +71,18 @@ private slots: QPoint currentCursorPos() const; void setCurrentCursorPos(const QPoint& pos); + void setupTrayIcon(); + void setupSpotlight(); + private: std::unique_ptr m_trayIcon; std::unique_ptr m_trayMenu; std::unique_ptr m_dialog; - std::unique_ptr m_aboutDialog; + QPointer m_aboutDialog; QLocalServer* const m_localServer = nullptr; - Spotlight* m_spotlight = nullptr; Settings* m_settings = nullptr; + Spotlight* m_spotlight = nullptr; + DeviceCommandHelper* m_deviceCommandHelper = nullptr; LinuxDesktop* m_linuxDesktop = nullptr; QQmlApplicationEngine* m_qmlEngine = nullptr; QQmlComponent* m_windowQmlComponent = nullptr; diff --git a/src/runguard.h b/src/runguard.h index 3ea97932..03cac1bf 100644 --- a/src/runguard.h +++ b/src/runguard.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include diff --git a/src/settings.cc b/src/settings.cc index 5e8e3607..efa41dbe 100644 --- a/src/settings.cc +++ b/src/settings.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 "settings.h" #include "device.h" @@ -8,9 +10,9 @@ #include #include -#include #include #include +#include #include #include #include @@ -71,7 +73,7 @@ namespace { constexpr int inputSequenceInterval = 250; constexpr uint8_t vibrationLength = 0; constexpr uint8_t vibrationIntensity = 128; - } + } // end namespace defaultValue namespace ranges { constexpr Settings::SettingRange spotSize{ 5, 100 }; @@ -84,8 +86,8 @@ namespace { constexpr Settings::SettingRange zoomFactor{ 1.5, 20.0 }; constexpr Settings::SettingRange inputSequenceInterval{ 100, 950 }; - } - } + } // end namespace ranges + } // end namespace settings // ----------------------------------------------------------------------------------------------- bool toBool(const QString& value) { @@ -141,9 +143,7 @@ Settings::Settings(const QString& configFile, QObject* parent) } // ------------------------------------------------------------------------------------------------- -Settings::~Settings() -{ -} +Settings::~Settings() = default; // ------------------------------------------------------------------------------------------------- void Settings::init() @@ -196,8 +196,13 @@ void Settings::initializeStringProperties() for (const auto& shapeSetting : shape.shapeSettings()) { const auto pm = shapeSettings(shape.name()); - if (!pm || !pm->property(shapeSetting.settingsKey().toLocal8Bit()).isValid()) continue; - if (shapeSetting.defaultValue().type() != QVariant::Int) continue; + if (!pm || !pm->property(shapeSetting.settingsKey().toLocal8Bit()).isValid()) { continue; } + + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + if (shapeSetting.defaultValue().type() != QVariant::Int) { continue; } + #else + if (shapeSetting.defaultValue().metaType().id() != QMetaType::Int) { continue; } + #endif const auto stringProperty = QString("spot.shape.%1.%2").arg(shape.name().toLower()) .arg(shapeSetting.settingsKey().toLower()); @@ -314,7 +319,7 @@ void Settings::shapeSettingsSetDefaults() { if (auto propertyMap = shapeSettings(shape.name())) { - const QString key = settingDefinition.settingsKey(); + const QString& key = settingDefinition.settingsKey(); if (propertyMap->property(key.toLocal8Bit()).isValid()) { propertyMap->setProperty(key.toLocal8Bit(), settingDefinition.defaultValue()); } else { @@ -337,14 +342,21 @@ void Settings::shapeSettingsLoad(const QString& preset) { if (auto propertyMap = shapeSettings(shape.name())) { - const QString key = settingDefinition.settingsKey(); + const QString& key = settingDefinition.settingsKey(); const QString settingsKey = section + QString("Shape.%1/%2").arg(shape.name()).arg(key); const QVariant loadedValue = m_settings->value(settingsKey, settingDefinition.defaultValue()); + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) if (settingDefinition.defaultValue().type() == QVariant::Int // Currently only int shape settings supported && settingDefinition.defaultValue() != loadedValue) { logDebug(lcSettings) << QString("spot.shape.%1.%2 = ").arg(shape.name().toLower(), key) << loadedValue.toInt(); } + #else + if (settingDefinition.defaultValue().metaType().id() == QMetaType::Int // Currently only int shape settings supported + && settingDefinition.defaultValue() != loadedValue) { + logDebug(lcSettings) << QString("spot.shape.%1.%2 = ").arg(shape.name().toLower(), key) << loadedValue.toInt(); + } + #endif if (propertyMap->property(key.toLocal8Bit()).isValid()) { propertyMap->setProperty(key.toLocal8Bit(), loadedValue); @@ -368,7 +380,7 @@ void Settings::shapeSettingsSavePreset(const QString& preset) { if (auto propertyMap = shapeSettings(shape.name())) { - const QString key = settingDefinition.settingsKey(); + const QString& key = settingDefinition.settingsKey(); const QString settingsKey = section + QString("Shape.%1/%2").arg(shape.name()).arg(key); m_settings->setValue(settingsKey, propertyMap->property(key.toLocal8Bit())); } @@ -394,7 +406,11 @@ void Settings::shapeSettingsInitialize() if (it != s.cend()) { + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) if (it->defaultValue().type() == QVariant::Int) // Currently only int shape settings supported + #else + if (it->defaultValue().metaType().id() == QMetaType::Int) + #endif { const auto setValue = value.toInt(); const auto min = it->minValue().toInt(); @@ -504,8 +520,7 @@ void Settings::savePreset(const QString& preset) // ------------------------------------------------------------------------------------------------- void Settings::setShowSpotShade(bool show) { - if (show == m_showSpotShade) - return; + if (show == m_showSpotShade) { return; } m_showSpotShade = show; m_settings->setValue(::settings::showSpotShade, m_showSpotShade); @@ -516,8 +531,7 @@ void Settings::setShowSpotShade(bool show) // ------------------------------------------------------------------------------------------------- void Settings::setSpotSize(int size) { - if (size == m_spotSize) - return; + if (size == m_spotSize) { return; } m_spotSize = qMin(qMax(::settings::ranges::spotSize.min, size), ::settings::ranges::spotSize.max); m_settings->setValue(::settings::spotSize, m_spotSize); @@ -528,8 +542,7 @@ void Settings::setSpotSize(int size) // ------------------------------------------------------------------------------------------------- void Settings::setShowCenterDot(bool show) { - if (show == m_showCenterDot) - return; + if (show == m_showCenterDot) { return; } m_showCenterDot = show; m_settings->setValue(::settings::showCenterDot, m_showCenterDot); @@ -540,8 +553,7 @@ void Settings::setShowCenterDot(bool show) // ------------------------------------------------------------------------------------------------- void Settings::setDotSize(int size) { - if (size == m_dotSize) - return; + if (size == m_dotSize) { return; } m_dotSize = qMin(qMax(::settings::ranges::dotSize.min, size), ::settings::ranges::dotSize.max); m_settings->setValue(::settings::dotSize, m_dotSize); @@ -552,8 +564,7 @@ void Settings::setDotSize(int size) // ------------------------------------------------------------------------------------------------- void Settings::setDotColor(const QColor& color) { - if (color == m_dotColor) - return; + if (color == m_dotColor) { return; } m_dotColor = color; m_settings->setValue(::settings::dotColor, m_dotColor); @@ -576,8 +587,7 @@ void Settings::setDotOpacity(double opacity) // ------------------------------------------------------------------------------------------------- void Settings::setShadeColor(const QColor& color) { - if (color == m_shadeColor) - return; + if (color == m_shadeColor) { return; } m_shadeColor = color; m_settings->setValue(::settings::shadeColor, m_shadeColor); @@ -600,8 +610,7 @@ void Settings::setShadeOpacity(double opacity) // ------------------------------------------------------------------------------------------------- void Settings::setCursor(Qt::CursorShape cursor) { - if (cursor == m_cursor) - return; + if (cursor == m_cursor) { return; } m_cursor = qMin(qMax(static_cast(0), cursor), Qt::LastCursor); m_settings->setValue(::settings::cursor, static_cast(m_cursor)); @@ -612,8 +621,7 @@ void Settings::setCursor(Qt::CursorShape cursor) // ------------------------------------------------------------------------------------------------- void Settings::setSpotShape(const QString& spotShapeQmlComponent) { - if (m_spotShape == spotShapeQmlComponent) - return; + if (m_spotShape == spotShapeQmlComponent) { return; } const auto it = std::find_if(spotShapes().cbegin(), spotShapes().cend(), [&spotShapeQmlComponent](const SpotShape& s) { @@ -679,8 +687,7 @@ bool Settings::spotRotationAllowed() const // ------------------------------------------------------------------------------------------------- void Settings::setSpotRotationAllowed(bool allowed) { - if (allowed == m_spotRotationAllowed) - return; + if (allowed == m_spotRotationAllowed) { return; } m_spotRotationAllowed = allowed; emit spotRotationAllowedChanged(allowed); @@ -689,8 +696,7 @@ void Settings::setSpotRotationAllowed(bool allowed) // ------------------------------------------------------------------------------------------------- void Settings::setShowBorder(bool show) { - if (show == m_showBorder) - return; + if (show == m_showBorder) { return; } m_showBorder = show; m_settings->setValue(::settings::showBorder, m_showBorder); @@ -701,8 +707,7 @@ void Settings::setShowBorder(bool show) // ------------------------------------------------------------------------------------------------- void Settings::setBorderColor(const QColor& color) { - if (color == m_borderColor) - return; + if (color == m_borderColor) { return; } m_borderColor = color; m_settings->setValue(::settings::borderColor, m_borderColor); @@ -713,8 +718,7 @@ void Settings::setBorderColor(const QColor& color) // ------------------------------------------------------------------------------------------------- void Settings::setBorderSize(int size) { - if (size == m_borderSize) - return; + if (size == m_borderSize) { return; } m_borderSize = qMin(qMax(::settings::ranges::borderSize.min, size), ::settings::ranges::borderSize.max); m_settings->setValue(::settings::borderSize, m_borderSize); @@ -737,8 +741,7 @@ void Settings::setBorderOpacity(double opacity) // ------------------------------------------------------------------------------------------------- void Settings::setZoomEnabled(bool enabled) { - if (enabled == m_zoomEnabled) - return; + if (enabled == m_zoomEnabled) { return; } m_zoomEnabled = enabled; m_settings->setValue(::settings::zoomEnabled, m_zoomEnabled); @@ -761,7 +764,7 @@ void Settings::setZoomFactor(double factor) // ------------------------------------------------------------------------------------------------- void Settings::setMultiScreenOverlayEnabled(bool enabled) { - if (m_multiScreenOverlayEnabled == enabled) return; + if (m_multiScreenOverlayEnabled == enabled) { return; } m_multiScreenOverlayEnabled = enabled; m_settings->setValue(::settings::multiScreenOverlay, m_multiScreenOverlayEnabled); logDebug(lcSettings) << "multi-screen-overlay = " << m_multiScreenOverlayEnabled; @@ -771,7 +774,7 @@ void Settings::setMultiScreenOverlayEnabled(bool enabled) // ------------------------------------------------------------------------------------------------- void Settings::setOverlayDisabled(bool disabled) { - if (m_overlayDisabled == disabled) return; + if (m_overlayDisabled == disabled) { return; } m_overlayDisabled = disabled; emit overlayDisabledChanged(m_overlayDisabled); } @@ -839,10 +842,18 @@ InputMapConfig Settings::getDeviceInputMapConfig(const DeviceId& dId) { m_settings->setArrayIndex(i); const auto seq = m_settings->value("deviceSequence"); - if (!seq.canConvert()) continue; + if (!seq.canConvert()) { continue; } const auto conf = m_settings->value("mappedAction"); - if (!conf.canConvert()) continue; - cfg.emplace(qvariant_cast(seq), qvariant_cast(conf)); + if (!conf.canConvert()) { continue; } + auto mappedAction = qvariant_cast(conf); + if (mappedAction.action->type() == Action::Type::ScrollHorizontal) { + mappedAction.action = GlobalActions::scrollHorizontal(); + } else if (mappedAction.action->type() == Action::Type::ScrollVertical) { + mappedAction.action = GlobalActions::scrollVertical(); + } else if (mappedAction.action->type() == Action::Type::VolumeControl) { + mappedAction.action = GlobalActions::volumeControl(); + } + cfg.emplace(qvariant_cast(seq), std::move(mappedAction)); } m_settings->endArray(); @@ -908,25 +919,27 @@ int PresetModel::rowCount(const QModelIndex& parent) const // ------------------------------------------------------------------------------------------------- QVariant PresetModel::data(const QModelIndex& index, int role) const { - if (index.row() > static_cast(m_presets.size())) + if (index.row() > static_cast(m_presets.size())) { return QVariant(); + } if (role == Qt::DisplayRole) { if (index.row() == 0) { return tr("Current Settings"); } - else { - return m_presets[index.row()-1]; - } + + return m_presets[index.row()-1]; } - else if (role == Qt::FontRole && index.row() == 0) + + if (role == Qt::FontRole && index.row() == 0) { QFont f; f.setItalic(true); return f; } - else if (role == Qt::ForegroundRole && index.row() == 0) { + + if (role == Qt::ForegroundRole && index.row() == 0) { return QColor(QGuiApplication::palette().color(QPalette::Disabled, QPalette::Text)); } @@ -937,7 +950,7 @@ QVariant PresetModel::data(const QModelIndex& index, int role) const void PresetModel::addPreset(const QString& preset) { const auto lb = std::lower_bound(m_presets.begin(), m_presets.end(), preset); - if (lb != m_presets.end() && *lb == preset) return; // Already exists + if (lb != m_presets.end() && *lb == preset) { return; } // Already exists const auto insertRow = std::distance(m_presets.begin(), lb) + 1; beginInsertRows(QModelIndex(), insertRow, insertRow); @@ -956,7 +969,7 @@ void PresetModel::removePreset(const QString& preset) { const auto r = std::equal_range(m_presets.begin(), m_presets.end(), preset); const auto count = std::distance(r.first, r.second); - if (count == 0) return; + if (count == 0) { return; } const auto startRow = std::distance(m_presets.begin(), r.first) + 1; diff --git a/src/settings.h b/src/settings.h index c9c4b83e..f5cc11e4 100644 --- a/src/settings.h +++ b/src/settings.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/spotlight.cc b/src/spotlight.cc index 23d75531..20bd96fc 100644 --- a/src/spotlight.cc +++ b/src/spotlight.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 "spotlight.h" +#include "device-hidpp.h" #include "deviceinput.h" #include "logging.h" #include "settings.h" @@ -10,6 +13,7 @@ #include #include +#include #include #include #include @@ -17,10 +21,54 @@ DECLARE_LOGGING_CATEGORY(device) DECLARE_LOGGING_CATEGORY(hid) +DECLARE_LOGGING_CATEGORY(input) namespace { const auto hexId = logging::hexId; -} // --- end anonymous namespace + + // See details on workaround in onEventDataAvailable + bool workaroundLogitechFirstMoveEvent = true; + +} // end anonymous namespace + + +// ------------------------------------------------------------------------------------------------- +// Hold button state. Very much Logitech Spotlight specific. +struct HoldButtonStatus +{ + void setButtonsPressed(bool nextPressed, bool backPressed) + { + if (!m_nextPressed && nextPressed) { + m_moveKeyEvSeq = SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHoldMove).keyEventSeq; + } else if (!m_backPressed && backPressed) { + m_moveKeyEvSeq = SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHoldMove).keyEventSeq; + } else if (m_nextPressed && !nextPressed && backPressed) { + m_moveKeyEvSeq = SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHoldMove).keyEventSeq; + } else if (m_backPressed && !backPressed && nextPressed) { + m_moveKeyEvSeq = SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHoldMove).keyEventSeq; + } + + m_nextPressed = nextPressed; + m_backPressed = backPressed; + + if (!nextPressed && !backPressed) { m_moveKeyEvSeq.clear(); } + } + + bool nextPressed() const { return m_nextPressed; } + bool backPressed() const { return m_backPressed; } + + void reset() { m_nextPressed = m_backPressed = false; m_moveKeyEvSeq.clear(); }; + + const KeyEventSequence& moveKeyEventSeq() const { + return m_moveKeyEvSeq; + }; + +private: + bool m_nextPressed = false; + bool m_backPressed = false; + + KeyEventSequence m_moveKeyEvSeq; +}; // ------------------------------------------------------------------------------------------------- Spotlight::Spotlight(QObject* parent, Options options, Settings* settings) @@ -28,32 +76,44 @@ Spotlight::Spotlight(QObject* parent, Options options, Settings* settings) , m_options(std::move(options)) , m_activeTimer(new QTimer(this)) , m_connectionTimer(new QTimer(this)) + , m_holdMoveEventTimer(new QTimer(this)) , m_settings(settings) + , m_holdButtonStatus(std::make_unique()) { + constexpr int spotlightActiveTimoutMs = 600; m_activeTimer->setSingleShot(true); - m_activeTimer->setInterval(600); + m_activeTimer->setInterval(spotlightActiveTimoutMs); connect(m_activeTimer, &QTimer::timeout, this, [this](){ setSpotActive(false); + workaroundLogitechFirstMoveEvent = true; }); if (m_options.enableUInput) { - m_virtualDevice = VirtualDevice::create(); + m_virtualMouseDevice = VirtualDevice::create( + VirtualDevice::Type::Mouse, "Projecteur_virtual_mouse"); + m_virtualKeyDevice = VirtualDevice::create( + VirtualDevice::Type::Keyboard, "Projecteur_virtual_keyboard"); } else { logInfo(device) << tr("Virtual device initialization was skipped."); } m_connectionTimer->setSingleShot(true); - // From detecting a change from inotify, the device needs some time to be ready for open + // From detecting a change with inotify, the device needs some time to be ready for open, + // otherwise opening the device will fail. // TODO: This interval seems to work, but it is arbitrary - there should be a better way. - m_connectionTimer->setInterval(800); + constexpr int delayedConnectionTimerIntervalMs = 800; + m_connectionTimer->setInterval(delayedConnectionTimerIntervalMs); connect(m_connectionTimer, &QTimer::timeout, this, [this]() { logDebug(device) << tr("New connection check triggered"); connectDevices(); }); + m_holdMoveEventTimer->setSingleShot(true); + m_holdMoveEventTimer->setInterval(30); + // Try to find already attached device(s) and connect to it. connectDevices(); setupDevEventInotify(); @@ -66,7 +126,7 @@ Spotlight::~Spotlight() = default; bool Spotlight::anySpotlightDeviceConnected() const { for (const auto& dc : m_deviceConnections) { - if (dc.second->subDeviceCount()) return true; + if (dc.second->subDeviceCount()) { return true; } } return false; } @@ -76,7 +136,7 @@ uint32_t Spotlight::connectedDeviceCount() const { uint32_t count = 0; for (const auto& dc : m_deviceConnections) { - if (dc.second->subDeviceCount()) ++count; + if (dc.second->subDeviceCount()) { ++count; } } return count; } @@ -84,9 +144,9 @@ uint32_t Spotlight::connectedDeviceCount() const // ------------------------------------------------------------------------------------------------- void Spotlight::setSpotActive(bool active) { - if (m_spotActive == active) return; + if (m_spotActive == active) { return; } m_spotActive = active; - if (!m_spotActive) m_activeTimer->stop(); + if (!m_spotActive) { m_activeTimer->stop(); } emit spotActiveChanged(m_spotActive); } @@ -117,28 +177,81 @@ int Spotlight::connectDevices() { auto& dc = m_deviceConnections[dev.id]; if (!dc) { - dc = std::make_shared(dev.id, dev.getName(), m_virtualDevice); + dc = std::make_shared( + dev.id, dev.getName(), m_virtualMouseDevice, m_virtualKeyDevice); } const bool anyConnectedBefore = anySpotlightDeviceConnected(); for (const auto& scanSubDevice : dev.subDevices) { - if (!scanSubDevice.deviceReadable) continue; - if (dc->hasSubDevice(scanSubDevice.deviceFile)) continue; + if (!scanSubDevice.deviceReadable) + { + logWarn(device) << tr("Sub-device not readable: %1 (%2:%3) %4") + .arg(dc->deviceName(), hexId(dev.id.vendorId), hexId(dev.id.productId), scanSubDevice.deviceFile); + continue; + } + if (dc->hasSubDevice(scanSubDevice.deviceFile)) { continue; } std::shared_ptr subDeviceConnection = - [&scanSubDevice, &dc, this]() -> std::shared_ptr { + [&scanSubDevice, &dc, this]() -> std::shared_ptr + { // Input event sub devices if (scanSubDevice.type == DeviceScan::SubDevice::Type::Event) { auto devCon = SubEventConnection::create(scanSubDevice, *dc); - if (addInputEventHandler(devCon)) return devCon; - } else if (scanSubDevice.type == DeviceScan::SubDevice::Type::Hidraw) { - auto hidCon = SubHidrawConnection::create(scanSubDevice, *dc); - if(addHIDInputHandler(hidCon)) return hidCon; + if (addInputEventHandler(devCon)) { return devCon; } + } // Hidraw sub devices + else if (scanSubDevice.type == DeviceScan::SubDevice::Type::Hidraw) + { + if (dc->hasHidppSupport()) + { + if (auto hidppCon = SubHidppConnection::create(scanSubDevice, *dc)) + { + QPointer connPtr(hidppCon.get()); + + connect(&*hidppCon, &SubHidppConnection::featureSetInitialized, this, + [this, connPtr](){ + if (!connPtr) { return; } + this->registerForNotifications(connPtr.data()); + }); + + // Remove device on socketReadError + connect(&*hidppCon, &SubHidppConnection::socketReadError, this, [this, connPtr](){ + if (!connPtr) { return; } + const bool anyConnectedBefore = anySpotlightDeviceConnected(); + connPtr->disconnect(); + QTimer::singleShot(0, this, [this, devicePath=connPtr->path(), anyConnectedBefore](){ + removeDeviceConnection(devicePath); + if (!anySpotlightDeviceConnected() && anyConnectedBefore) { + emit anySpotlightDeviceConnectedChanged(false); + } + }); + }); + + return hidppCon; + } + } + else if (auto hidrawConn = SubHidrawConnection::create(scanSubDevice, *dc)) + { + QPointer connPtr(hidrawConn.get()); + // Remove device on socketReadError + connect(&*hidrawConn, &SubHidrawConnection::socketReadError, this, [this, connPtr](){ + if (!connPtr) { return; } + const bool anyConnectedBefore = anySpotlightDeviceConnected(); + connPtr->disconnect(); + QTimer::singleShot(0, this, [this, devicePath=connPtr->path(), anyConnectedBefore](){ + removeDeviceConnection(devicePath); + if (!anySpotlightDeviceConnected() && anyConnectedBefore) { + emit anySpotlightDeviceConnectedChanged(false); + } + }); + }); + + return hidrawConn; + } } return std::shared_ptr(); }(); - if (!subDeviceConnection) continue; + if (!subDeviceConnection) { continue; } if (dc->subDeviceCount() == 0) { // Load Input mapping settings when first sub-device gets added. @@ -153,7 +266,7 @@ int Spotlight::connectDevices() static QString lastPreset; - connect(im, &InputMapper::actionMapped, this, [this](std::shared_ptr action) + connect(im, &InputMapper::actionMapped, this, [this](const std::shared_ptr& action) { if (action->type() == Action::Type::CyclePresets) { @@ -172,6 +285,31 @@ int Spotlight::connectDevices() { m_settings->setOverlayDisabled(!m_settings->overlayDisabled()); } + else if (action->type() == Action::Type::ScrollHorizontal || action->type() == Action::Type::ScrollVertical) + { + if (!m_virtualMouseDevice) { return; } + + const int param = (action->type() == Action::Type::ScrollHorizontal) + ? static_cast(action.get())->param + : static_cast(action.get())->param; + + if (param) + { + const uint16_t wheelCode = (action->type() == Action::Type::ScrollHorizontal) ? REL_HWHEEL : REL_WHEEL; + const std::vector scrollInputEvents = {{{}, EV_REL, wheelCode, param}, {{}, EV_SYN, SYN_REPORT, 0},}; + m_virtualMouseDevice->emitEvents(scrollInputEvents); + } + } + else if (action->type() == Action::Type::VolumeControl) + { + if (!m_virtualMouseDevice) { return; } + + auto param = static_cast(action.get())->param; + uint16_t keyCode = (param > 0)? KEY_VOLUMEUP: KEY_VOLUMEDOWN; + const std::vector curVolInputEvents = {{{}, EV_KEY, keyCode, 1}, {{}, EV_SYN, SYN_REPORT, 0}, + {{}, EV_KEY, keyCode, 0}, {{}, EV_SYN, SYN_REPORT, 0},}; + if (param) { m_virtualMouseDevice->emitEvents(curVolInputEvents); } + } }); connect(m_settings, &Settings::presetLoaded, this, [](const QString& preset){ @@ -187,7 +325,7 @@ int Spotlight::connectDevices() logInfo(device) << tr("Connected device: %1 (%2:%3)") .arg(devName, hexId(id.vendorId), hexId(id.productId)); emit deviceConnected(id, devName); - if (!anyConnectedBefore) emit anySpotlightDeviceConnectedChanged(true); + if (!anyConnectedBefore) { emit anySpotlightDeviceConnectedChanged(true); } }); } @@ -236,7 +374,7 @@ void Spotlight::removeDeviceConnection(const QString &devicePath) // ------------------------------------------------------------------------------------------------- void Spotlight::onEventDataAvailable(int fd, SubEventConnection& connection) { - const bool isNonBlocking = !!(connection.flags() & DeviceFlag::NonBlocking); + const bool isNonBlocking = connection.hasFlags(DeviceFlag::NonBlocking); while (true) { auto& buf = connection.inputBuffer(); @@ -246,7 +384,7 @@ void Spotlight::onEventDataAvailable(int fd, SubEventConnection& connection) if (errno != EAGAIN) { const bool anyConnectedBefore = anySpotlightDeviceConnected(); - connection.disable(); + connection.disconnect(); QTimer::singleShot(0, this, [this, devicePath=connection.path(), anyConnectedBefore](){ removeDeviceConnection(devicePath); if (!anySpotlightDeviceConnected() && anyConnectedBefore) { @@ -264,13 +402,35 @@ void Spotlight::onEventDataAvailable(int fd, SubEventConnection& connection) const auto &first_ev = buf[0]; const bool isMouseMoveEvent = first_ev.type == EV_REL && (first_ev.code == REL_X || first_ev.code == REL_Y); + if (isMouseMoveEvent) { // Skip input mapping for mouse move events completely - if (!m_activeTimer->isActive()) { + + // Note: During a Next or Back button press the Logitech Spotlight device can send + // move events via hid++ notifications. It seems that just when releasing the + // next or back button sometimes a mouse move event 'leaks' through here as + // relative input event causing the spotlight to be activated. + // The workaround skips a first input move event from the logitech spotlight device. + const bool isLogitechSpotlight = connection.deviceId().vendorId == 0x46d + && (connection.deviceId().productId == 0xc53e || connection.deviceId().productId == 0xb503); + const bool logitechIsFirst = isLogitechSpotlight && workaroundLogitechFirstMoveEvent; + + if (isLogitechSpotlight) + { + workaroundLogitechFirstMoveEvent = false; + if(!logitechIsFirst) { + if (!spotActive()) { setSpotActive(true); } + } + } + else if (!m_activeTimer->isActive()) { setSpotActive(true); } + m_activeTimer->start(); - if (m_virtualDevice) m_virtualDevice->emitEvents(buf.data(), buf.pos()); + if (m_virtualMouseDevice) { + // forward events to virtual mouse device + m_virtualMouseDevice->emitEvents(buf.data(), buf.pos()); + } } else { // Forward events to input mapper for the device @@ -285,60 +445,115 @@ void Spotlight::onEventDataAvailable(int fd, SubEventConnection& connection) buf.reset(); } - if (!isNonBlocking) break; + if (!isNonBlocking) { break; } } // end while loop } // ------------------------------------------------------------------------------------------------- -void Spotlight::onHIDDataAvailable(int fd, SubHidrawConnection& connection) +void Spotlight::registerForNotifications(SubHidppConnection* connection) { - QByteArray readVal(20, 0); - if (::read(fd, static_cast(readVal.data()), readVal.length()) < 0) + using namespace HIDPP; + + // Logitech button next and back press and hold + movement + if (const auto rcIndex = connection->featureSet().featureIndex(FeatureCode::ReprogramControlsV4)) { - if (errno != EAGAIN) + connection->registerNotificationCallback(this, rcIndex, makeSafeCallback( + [this, connection](Message&& msg) { - const bool anyConnectedBefore = anySpotlightDeviceConnected(); - connection.disable(); - QTimer::singleShot(0, this, [this, devicePath=connection.path(), anyConnectedBefore](){ - removeDeviceConnection(devicePath); - if (!anySpotlightDeviceConnected() && anyConnectedBefore) { - emit anySpotlightDeviceConnectedChanged(false); + // 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; + + if (!m_holdButtonStatus->nextPressed() && isNextPressed) + { + const auto& nextHold = SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHold); + for (const auto& ke: nextHold.keyEventSeq) { + connection->inputMapper()->addEvents(ke); } - }); - } - return; - } - logDebug(hid) << "Received" << readVal.toHex() << "from" << connection.path(); - // TODO: Process Logitech HIDPP message -} + } -// ------------------------------------------------------------------------------------------------- -bool Spotlight::addInputEventHandler(std::shared_ptr connection) -{ - if (!connection || connection->type() != ConnectionType::Event || !connection->isConnected()) { - return false; - } + if (!m_holdButtonStatus->backPressed() && isBackPressed) + { + const auto& backHold = SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHold); + for (const auto& ke: backHold.keyEventSeq) { + connection->inputMapper()->addEvents(ke); + } + } - QSocketNotifier* const readNotifier = connection->socketReadNotifier(); - connect(readNotifier, &QSocketNotifier::activated, this, - [this, connection=std::move(connection)](int fd) { - onEventDataAvailable(fd, *connection.get()); - }); + m_holdButtonStatus->setButtonsPressed(isNextPressed, isBackPressed); + }), 0 /* function 0 */); - return true; + connection->registerNotificationCallback(this, rcIndex, + makeSafeCallback([this, connection](Message&& msg) + { + // Block some of the move events + // TODO This works quiet okay in combination with adjusting x and y values, + // but needs to be a more solid option to accumulate the mass of move events + // and consolidate them to a number of meaningful action special key events. + if (m_holdMoveEventTimer->isActive()) { return; } + m_holdMoveEventTimer->start(); + + // 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 + + static const auto intcast = [](uint8_t v) -> int{ return static_cast(v); }; + + const int x = intcast(msg[5]); + const int y = intcast(msg[7]); + + static const auto getReducedParam = [](int param) -> int { + constexpr int divider = 5; + constexpr int minimum = 5; + constexpr int maximum = 10; + if (std::abs(param) < minimum) { return 0; } + const auto sign = (param == 0) ? 0 : ((param > 0) ? 1 : -1); + return std::floor(1.0 * ((abs(param) > maximum)? sign * maximum : param) / divider); + }; + + const int adjustedX = getReducedParam(x); + const int adjustedY = getReducedParam(y); + + if (adjustedX == 0 && adjustedY == 0) { return; } + + static const auto scrollHAction = GlobalActions::scrollHorizontal(); + scrollHAction->param = -adjustedX; + + static const auto scrollVAction = GlobalActions::scrollVertical(); + scrollVAction->param = adjustedY; + + static const auto volumeControlAction = GlobalActions::volumeControl(); + volumeControlAction->param = -adjustedY; + + if (!connection->inputMapper()->recordingMode()) + { + for (const auto& key_event : m_holdButtonStatus->moveKeyEventSeq()) { + connection->inputMapper()->addEvents(key_event); + } + } + }), 1 /* function 1 */); + } } // ------------------------------------------------------------------------------------------------- -bool Spotlight::addHIDInputHandler(std::shared_ptr connection) +bool Spotlight::addInputEventHandler(std::shared_ptr connection) { - if (!connection || connection->type() != ConnectionType::Hidraw || !connection->isConnected()) { + if (!connection || connection->type() != ConnectionType::Event || !connection->isConnected()) { return false; } QSocketNotifier* const readNotifier = connection->socketReadNotifier(); connect(readNotifier, &QSocketNotifier::activated, this, [this, connection=std::move(connection)](int fd) { - onHIDDataAvailable(fd, *connection.get()); + onEventDataAvailable(fd, *connection); }); return true; diff --git a/src/spotlight.h b/src/spotlight.h index 235d1fe2..d77c9eef 100644 --- a/src/spotlight.h +++ b/src/spotlight.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,15 +8,21 @@ #include #include +#include "asynchronous.h" #include "devicescan.h" class QTimer; class Settings; class VirtualDevice; +class DeviceConnection; +class SubEventConnection; +class SubHidppConnection; + +struct HoldButtonStatus; /// Class handling spotlight device connections and indicating if a device is sending /// sending mouse move events. -class Spotlight : public QObject +class Spotlight : public QObject, public async::Async { Q_OBJECT @@ -54,20 +61,23 @@ class Spotlight : public QObject ConnectionResult connectSpotlightDevice(const QString& devicePath, bool verbose = false); bool addInputEventHandler(std::shared_ptr connection); - bool addHIDInputHandler(std::shared_ptr connection); + void registerForNotifications(SubHidppConnection* connection); bool setupDevEventInotify(); int connectDevices(); void removeDeviceConnection(const QString& devicePath); void onEventDataAvailable(int fd, SubEventConnection& connection); - void onHIDDataAvailable(int fd, SubHidrawConnection& connection); const Options m_options; std::map> m_deviceConnections; + std::vector m_activeDeviceIds; QTimer* m_activeTimer = nullptr; QTimer* m_connectionTimer = nullptr; + QTimer* m_holdMoveEventTimer = nullptr; bool m_spotActive = false; - std::shared_ptr m_virtualDevice; + std::shared_ptr m_virtualMouseDevice; + std::shared_ptr m_virtualKeyDevice; Settings* m_settings = nullptr; + std::unique_ptr m_holdButtonStatus; }; diff --git a/src/spotshapes.cc b/src/spotshapes.cc index 4c1c2168..6cda9eb6 100644 --- a/src/spotshapes.cc +++ b/src/spotshapes.cc @@ -1,8 +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 "spotshapes.h" -#include #include +#include #include @@ -12,7 +14,7 @@ namespace { SpotShapeNGon::qmlRegister(); return true; }(); -} +} // end anonymous namespace SpotShapeStar::SpotShapeStar(QQuickItem* parent) : QQuickItem (parent) { @@ -54,7 +56,7 @@ QSGNode* SpotShapeStar::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* u geometryNode->setGeometry(geometry); geometryNode->setFlag(QSGNode::OwnsGeometry, true); - QSGFlatColorMaterial* const material = new QSGFlatColorMaterial(); + auto* const material = new QSGFlatColorMaterial(); material->setColor(m_color); geometryNode->setMaterial(material); geometryNode->setFlag(QSGNode::OwnsMaterial); @@ -71,9 +73,9 @@ QSGNode* SpotShapeStar::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* u QSGGeometry::Point2D* const vertices = geometryNode->geometry()->vertexDataAsPoint2D(); const int numSegments = m_points * 2; - const float cx = static_cast(width()/2); // center X - const float cy = static_cast(height()/2); // center Y - const float deltaRad = static_cast((360.0 / m_points) * (M_PI/180.0)); + const auto cx = static_cast(width()/2); // center X + const auto cy = static_cast(height()/2); // center Y + const auto deltaRad = static_cast((360.0 / m_points) * (M_PI/180.0)); float theta = -static_cast(90.0 * M_PI/180.0); vertices[0].set(cx, cy); @@ -239,9 +241,9 @@ QSGNode* SpotShapeNGon::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* u } QSGGeometry::Point2D* const vertices = geometryNode->geometry()->vertexDataAsPoint2D(); - const float cx = static_cast(width()/2); // center X - const float cy = static_cast(height()/2); // center Y - const float deltaRad = static_cast((360.0 / m_sides) * (M_PI/180.0)); + const auto cx = static_cast(width()/2); // center X + const auto cy = static_cast(height()/2); // center Y + const auto deltaRad = static_cast((360.0 / m_sides) * (M_PI/180.0)); float theta = -static_cast(90.0 * M_PI/180.0); vertices[0].set(cx, cy); diff --git a/src/spotshapes.h b/src/spotshapes.h index 70e8087a..54345a48 100644 --- a/src/spotshapes.h +++ b/src/spotshapes.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/virtualdevice.cc b/src/virtualdevice.cc index 2ca2ad96..be163757 100644 --- a/src/virtualdevice.cc +++ b/src/virtualdevice.cc @@ -1,38 +1,53 @@ -// 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 "virtualdevice.h" #include "logging.h" #include #include +#include #include #include LOGGING_CATEGORY(virtualdevice, "virtualdevice") +// KEY_MACRO1 is only defined in newer linux versions +#ifndef KEY_MACRO1 +#define KEY_MACRO1 0x290 +#endif + namespace { class VirtualDevice_ : public QObject {}; // for i18n and logging -} +} // end anonymous namespace struct VirtualDevice::Token {}; -VirtualDevice::VirtualDevice(Token, int fd) +// ------------------------------------------------------------------------------------------------- +VirtualDevice::VirtualDevice(Token /* token */, int fd, const char* name, const char* sysfs_name) : m_uinpFd(fd) + , m_userName(name) + , m_deviceName(sysfs_name) {} +// ------------------------------------------------------------------------------------------------- VirtualDevice::~VirtualDevice() { if (m_uinpFd >= 0) { ioctl(m_uinpFd, UI_DEV_DESTROY); ::close(m_uinpFd); - logDebug(virtualdevice) << VirtualDevice_::tr("uinput Device Closed"); + logDebug(virtualdevice) + << VirtualDevice_::tr("uinput Device Closed (%1; %2)").arg(m_userName, m_deviceName); } } -// Setup uinput device that can send mouse and keyboard events. -std::shared_ptr VirtualDevice::create(const char* name, +// ------------------------------------------------------------------------------------------------- +// Setup a uinput device that can send mouse or keyboard events. +std::shared_ptr VirtualDevice::create(Type deviceType, + const char* name, uint16_t virtualVendorId, uint16_t virtualProductId, uint16_t virtualVersionId, @@ -42,14 +57,14 @@ std::shared_ptr VirtualDevice::create(const char* name, if (!fi.exists()) { logWarn(virtualdevice) << VirtualDevice_::tr("File not found: %1").arg(location); logWarn(virtualdevice) << VirtualDevice_::tr("Please check if uinput kernel module is loaded"); - return std::unique_ptr(); + return std::shared_ptr(); } const int fd = ::open(location, O_WRONLY | O_NDELAY); if (fd < 0) { logWarn(virtualdevice) << VirtualDevice_::tr("Unable to open: %1").arg(location); logWarn(virtualdevice) << VirtualDevice_::tr("Please check if current user has write access"); - return std::unique_ptr(); + return std::shared_ptr(); } struct uinput_user_dev uinp {}; @@ -60,19 +75,35 @@ std::shared_ptr VirtualDevice::create(const char* name, uinp.id.version = virtualVersionId; // Setup the uinput device - // TODO Are the following Key and Event bits sufficient? Do we need more? (see all in Linux's input-event-codes.h) + // (see all in Linux's input-event-codes.h) ioctl(fd, UI_SET_EVBIT, EV_SYN); ioctl(fd, UI_SET_EVBIT, EV_KEY); ioctl(fd, UI_SET_EVBIT, EV_REL); - // Set all rel event code bits on virtual device + // Set all relative event code bits on virtual device for (int i = 0; i < REL_CNT; ++i) { ioctl(fd, UI_SET_RELBIT, i); } - // Set all key code bits on virtual device - for (int i = 1; i < KEY_CNT; ++i) { - ioctl(fd, UI_SET_KEYBIT, i); + // Thank's to Matthias Blümel / https://github.com/Blaimi + // for the detailed investigation on the uinput issue on newer + // Linux distributions. + // See https://github.com/jahnf/Projecteur/issues/175#issuecomment-1432112896 + + if (deviceType == Type::Mouse) { + // Set key code bits for a virtual mouse + for (int i = BTN_MISC; i < KEY_OK; ++i) { + ioctl(fd, UI_SET_KEYBIT, i); + } + } else if (deviceType == Type::Keyboard) { + // Set key code bits for a virtual keyboard + for (int i = 1; i < BTN_MISC; ++i) { + ioctl(fd, UI_SET_KEYBIT, i); + } + for (int i = KEY_OK; i < KEY_MACRO1; ++i) { + ioctl(fd, UI_SET_KEYBIT, i); + } + // will set key bits from i = KEY_MACRO1 to i < KEY_CNT also work? } // Create input device into input sub-system @@ -88,13 +119,17 @@ std::shared_ptr VirtualDevice::create(const char* name, char sysfs_device_name[16]{}; ioctl(fd, UI_GET_SYSNAME(sizeof(sysfs_device_name)), sysfs_device_name); logInfo(virtualdevice) << VirtualDevice_::tr("Created uinput device: %1") - .arg(QString("/sys/devices/virtual/input/%1").arg(sysfs_device_name)); + .arg(QString("%1; /sys/devices/virtual/input/%2") + .arg(name, sysfs_device_name)); - return std::make_shared(Token{}, fd); + return std::make_shared(Token{}, fd, name, sysfs_device_name); } +// ------------------------------------------------------------------------------------------------- void VirtualDevice::emitEvents(const struct input_event input_events[], size_t num) { + if (!num) { return; } + if (const ssize_t sz = sizeof(input_event) * num) { const auto bytesWritten = write(m_uinpFd, input_events, sz); if (bytesWritten != sz) { @@ -103,13 +138,8 @@ void VirtualDevice::emitEvents(const struct input_event input_events[], size_t n } } +// ------------------------------------------------------------------------------------------------- void VirtualDevice::emitEvents(const std::vector& events) { - if (const ssize_t sz = sizeof(input_event) * events.size()) { - const auto bytesWritten = write(m_uinpFd, events.data(), sz); - if (bytesWritten != sz) { - logError(virtualdevice) << VirtualDevice_::tr("Error while writing to virtual device."); - } - } + emitEvents(events.data(), events.size()); } - diff --git a/src/virtualdevice.h b/src/virtualdevice.h index 482ad543..f70d2761 100644 --- a/src/virtualdevice.h +++ b/src/virtualdevice.h @@ -1,31 +1,42 @@ -// 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 -// Virtual Device to emit customized events from Projecteur device +// Virtual Device to emit custom events from Projecteur. // The spotlight.cc grabs mouse inputs from Logitech Spotlight device. // This module is used when the input events are supposed to be forwarded to the system. # pragma once +#include + #include #include #include -// Device that can act as virtual keyboard and mouse +/// Device that can act as virtual keyboard or mouse class VirtualDevice { private: struct Token; int m_uinpFd = -1; + QString m_userName; + QString m_deviceName; public: - // Return a VirtualDevice shared_ptr or an empty shared_ptr if the creation fails. - static std::shared_ptr create(const char* name = "Projecteur_input_device", + enum class Type { + Mouse, + Keyboard + }; + + /// Return a VirtualDevice shared_ptr or an empty shared_ptr if the creation fails. + static std::shared_ptr create(Type deviceType, + const char* name = "Projecteur_input_device", uint16_t virtualVendorId = 0xfeed, uint16_t virtualProductId = 0xc0de, uint16_t virtualVersionId = 1, const char* location = "/dev/uinput"); - explicit VirtualDevice(Token, int fd); + VirtualDevice(Token, int fd, const char* name, const char* sysfs_name); ~VirtualDevice(); void emitEvents(const struct input_event[], size_t num);