pax_global_header00006660000000000000000000000064145134407060014516gustar00rootroot0000000000000052 comment=a7b2649296f5f8b6793e0c38cdee93d36831404b Projecteur-0.10/000077500000000000000000000000001451344070600135605ustar00rootroot00000000000000Projecteur-0.10/.clang-format000066400000000000000000000023151451344070600161340ustar00rootroot00000000000000--- 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 ... Projecteur-0.10/.clang-tidy000066400000000000000000000025121451344070600156140ustar00rootroot00000000000000--- 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;' Projecteur-0.10/55-projecteur.rules.in000066400000000000000000000022271451344070600176550ustar00rootroot00000000000000# Set up permissions for non root users to open the Logitech Spotlight USB Receiver and other # supported devices. Enables the Projecteur application to access the device. # Copy the generated file `55-projecteur.rules` from the build directory # to /lib/udev/rules.d/55-projecteur.rules # Rule for the Logitech Spotlight USB Receiver SUBSYSTEMS=="usb", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c53e", MODE="0660", TAG+="uaccess" # Additional supported USB devices @EXTRA_USB_UDEV_RULES@ # 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 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" Projecteur-0.10/CMakeLists.txt000066400000000000000000000367541451344070600163370ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.6) # Use QTDIR environment variable with find_package, # e.g. set QTDIR=/home/user/Qt/5.9.6/gcc_64/ if(NOT "$ENV{QTDIR}" STREQUAL "") set(QTDIR $ENV{QTDIR}) list(APPEND CMAKE_PREFIX_PATH $ENV{QTDIR}) elseif(QTDIR) list(APPEND CMAKE_PREFIX_PATH ${QTDIR}) endif() # Set the default build type to release if( NOT CMAKE_BUILD_TYPE ) message(STATUS "Setting build type to 'Release' as none was specified.") set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) endif() set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") project(Projecteur LANGUAGES CXX) add_compile_options(-Wall -Wextra -Werror) #set(CMAKE_CXX_CLANG_TIDY clang-tidy-12) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules") include(GitVersion) include(Translation) 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) 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. if(Qt5_VERSION VERSION_EQUAL "5.8" OR (Qt5_VERSION VERSION_GREATER "5.8" AND Qt5_VERSION VERSION_LESS "5.9")) message(WARNING "There are known issues when using Projecteur with Qt Version 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-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 ${QT_PACKAGE_NAME}::Core ${QT_PACKAGE_NAME}::Quick ${QT_PACKAGE_NAME}::Widgets ) 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_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() target_compile_options(projecteur PRIVATE $<$,$>:-Wall -Wextra> ) target_compile_definitions(projecteur PRIVATE CXX_COMPILER_ID=${CMAKE_CXX_COMPILER_ID} CXX_COMPILER_VERSION=${CMAKE_CXX_COMPILER_VERSION}) # Set version project properties for builds not from a git repository (e.g. created with git archive) # If creating the version number via git information fails, the following target properties # will be used. IMPORTANT - when creating a release tag with git flow: # Update this information - the version numbers and the version type. # VERSION_TYPE must be either 'release' or 'develop' set_target_properties(projecteur PROPERTIES VERSION_MAJOR 0 VERSION_MINOR 10 VERSION_PATCH 0 VERSION_TYPE release VERSION_DISTANCE_OFFSET 0 ) add_version_info(projecteur "${CMAKE_CURRENT_SOURCE_DIR}") # Create files containing generated version strings, helping package maintainers get_target_property(PROJECTEUR_VERSION_STRING projecteur VERSION_STRING) # Arch Linux = PKGBUILD/makepkg: '-' is not allowed in version number string(REPLACE "-" "" PROJECTEUR_VERSION_STRING_ARCHLINUX "${PROJECTEUR_VERSION_STRING}") file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/version-string" "${PROJECTEUR_VERSION_STRING}") file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/version-string.archlinux" "${PROJECTEUR_VERSION_STRING_ARCHLINUX}") # Translation list(APPEND languages de fr es) set(ts_directories "${CMAKE_CURRENT_SOURCE_DIR}/i18n") add_translations_target("projecteur" "${CMAKE_CURRENT_BINARY_DIR}" "${ts_directories}" "${languages}") add_translation_update_task("projecteur" "${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/i18n" "${languages}") # 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) # Install #--------------------------------------------------------------------------------------------------- # Set default directory permissions with CMake >= 3.11, avoids some # lintian 'non-standard-dir-perm' errors for deb packages. set(CMAKE_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE ) install(TARGETS projecteur DESTINATION bin) set(PROJECTEUR_INSTALL_PATH "${CMAKE_INSTALL_PREFIX}/bin/projecteur") #used in desktop file template # Use udev.pc pkg-config file to set the dir path if (NOT CMAKE_INSTALL_UDEVRULESDIR) set (UDEVDIR /lib/udev) find_package(PkgConfig) if(PKG_CONFIG_FOUND) pkg_check_modules(PKGCONFIG_UDEV udev) if(PKGCONFIG_UDEV_FOUND) execute_process( COMMAND ${PKG_CONFIG_EXECUTABLE} --variable=udevdir udev OUTPUT_VARIABLE PKGCONFIG_UDEVDIR OUTPUT_STRIP_TRAILING_WHITESPACE ) if(PKGCONFIG_UDEVDIR) file(TO_CMAKE_PATH "${PKGCONFIG_UDEVDIR}" UDEVDIR) endif(PKGCONFIG_UDEVDIR) endif(PKGCONFIG_UDEV_FOUND) endif(PKG_CONFIG_FOUND) endif(NOT CMAKE_INSTALL_UDEVRULESDIR) set (CMAKE_INSTALL_UDEVDIR ${UDEVDIR} CACHE PATH "Udev base dir.") mark_as_advanced(CMAKE_INSTALL_UDEVDIR) set (CMAKE_INSTALL_UDEVRULESDIR ${UDEVDIR}/rules.d CACHE PATH "Where to install udev rules") mark_as_advanced(CMAKE_INSTALL_UDEVRULESDIR) # Configure and install files set(OUTDIR "${CMAKE_CURRENT_BINARY_DIR}") set(TMPLDIR "${CMAKE_CURRENT_SOURCE_DIR}/cmake/templates") # Read devices.conf file set(idRegex "0x([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])") set(lineRegex "^[ \t]*${idRegex}[ \t]*,[ \t]*${idRegex}[ \t]*,[ \t]*(usb|bt)[ \t]*,[ \t]*(.*)[ \t]*") file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/devices.conf" CONFLINES REGEX "${lineRegex}") foreach(line ${CONFLINES}) #message(STATUS "## ${line}") if(line MATCHES "${lineRegex}") # message(STATUS "vendorId: ${CMAKE_MATCH_1}, productId: ${CMAKE_MATCH_2}, ${CMAKE_MATCH_3}, '${CMAKE_MATCH_4}'") set(vendorId "${CMAKE_MATCH_1}") set(productId "${CMAKE_MATCH_2}") if("${CMAKE_MATCH_3}" STREQUAL "usb") string(APPEND EXTRA_USB_UDEV_RULES "\n## Extra-Device: ${CMAKE_MATCH_4}") string(APPEND EXTRA_USB_UDEV_RULES "\nSUBSYSTEMS==\"usb\", ATTRS{idVendor}==\"${vendorId}\"") string(APPEND EXTRA_USB_UDEV_RULES ", ATTRS{idProduct}==\"${productId}\", MODE=\"0660\", TAG+=\"uaccess\"") string(APPEND SUPPORTED_EXTRA_DEVICES "\n {0x${vendorId}, 0x${productId}, false, \"${CMAKE_MATCH_4}\"}, // ${CMAKE_MATCH_4}") elseif("${CMAKE_MATCH_3}" STREQUAL "bt") string(APPEND SUPPORTED_EXTRA_DEVICES "\n {0x${vendorId}, 0x${productId}, true, \"${CMAKE_MATCH_4}\"}, // ${CMAKE_MATCH_4}") if("${vendorId}" MATCHES "0*([0-9a-fA-F]+)") set(vendorId "${CMAKE_MATCH_1}") endif() if("${productId}" MATCHES "0*([0-9a-fA-F]+)") set(productId "${CMAKE_MATCH_1}") endif() string(APPEND EXTRA_BLUETOOTH_UDEV_RULES "\n## Extra-Device: ${CMAKE_MATCH_4}") string(APPEND EXTRA_BLUETOOTH_UDEV_RULES "\nSUBSYSTEMS==\"input\", ") string(APPEND EXTRA_BLUETOOTH_UDEV_RULES "ENV{LIBINPUT_DEVICE_GROUP}=\"5/${vendorId}/${productId}*\", ") string(APPEND EXTRA_BLUETOOTH_UDEV_RULES "MODE=\"0660\", TAG+=\"uaccess\"") endif() endif() endforeach() configure_file("src/extra-devices.cc.in" "src/extra-devices.cc" @ONLY) set_property(TARGET projecteur APPEND PROPERTY SOURCES "${CMAKE_CURRENT_BINARY_DIR}/src/extra-devices.cc") configure_file("55-projecteur.rules.in" "55-projecteur.rules" @ONLY) install(FILES "${OUTDIR}/55-projecteur.rules" DESTINATION ${CMAKE_INSTALL_UDEVRULESDIR}/) install(FILES icons/projecteur-tray.svg DESTINATION share/icons/hicolor/48x48/apps/ RENAME projecteur.svg) install(FILES icons/projecteur-tray.svg DESTINATION share/icons/hicolor/64x64/apps/ RENAME projecteur.svg) install(FILES icons/projecteur-tray.svg DESTINATION share/icons/hicolor/128x128/apps/ RENAME projecteur.svg) install(FILES icons/projecteur-tray.svg DESTINATION share/icons/hicolor/256x256/apps/ RENAME projecteur.svg) # Set variables for file configurations 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) install(FILES "${OUTDIR}/projecteur.desktop" DESTINATION share/applications/) # Configure man page and gzip it. 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/) configure_file("${TMPLDIR}/projecteur.bash-completion" "projecteur.bash-completion" @ONLY) install(FILES "${OUTDIR}/projecteur.bash-completion" DESTINATION share/bash-completion/completions/ RENAME projecteur) configure_file("${TMPLDIR}/preinst.in" "pkg/scripts/preinst" @ONLY) configure_file("${TMPLDIR}/postinst.in" "pkg/scripts/postinst" @ONLY) # --- Linux packaging --- include(LinuxPackaging) option(PACKAGE_TARGETS "Create packaging build targets" ON) if(PACKAGE_TARGETS) # Add 'source-archive' target add_source_archive_target(projecteur) # Add 'dist-package' target: Creates a deb/rpm/tgz package depending on the current Linux distribution add_dist_package_target( PROJECT "${CMAKE_PROJECT_NAME}" TARGET projecteur DESCRIPTION_BRIEF "Linux/X11 application for the Logitech Spotlight device." DESCRIPTION_FULL "Projecteur is a virtual laser pointer for use with inertial pointers such as the Logitech Spotlight. Projecteur can show a colored dot, a highlighted circle or a zoom effect to act as a pointer. The location of the pointer moves in response to moving the handheld pointer device. The effect is much like that of a traditional laser pointer, except that it is captured by recording software and works across multiple screens." CONTACT "Jahn Fuchs " HOMEPAGE "${HOMEPAGE}" DEBIAN_SECTION "utils" # PREINST_SCRIPT "${OUTDIR}/pkg/scripts/preinst" POSTINST_SCRIPT "${OUTDIR}/pkg/scripts/postinst" ) 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 if ("${PKG_TYPE}" STREQUAL "DEB") # TODO Lintian expects the the copyright file in /usr/share/doc/projecteur # This clashes with the default CMAKE_INSTALL_PREFIX /usr/local # Need to check if this would clash with packages from the debian/ubuntu repos. #configure_file("${TMPLDIR}/copyright.in" "pkg/copyright" @ONLY) #install(FILES "${OUTDIR}/pkg/copyright" DESTINATION /usr/share/doc/projecteur/) ## -- prevent additional lintian warnings 'non-standard-dir-perm' for the cmake install prefix set(DIR_TO_INSTALL "${CMAKE_INSTALL_PREFIX}") while(NOT "${DIR_TO_INSTALL}" STREQUAL "/" AND NOT "${DIR_TO_INSTALL}" STREQUAL "") install(DIRECTORY DESTINATION "${DIR_TO_INSTALL}") get_filename_component(DIR_TO_INSTALL "${DIR_TO_INSTALL}" PATH) endwhile() endif() endif() option(ENABLE_IWYU "Enable Include-What-You-Use" OFF) find_program(iwyu_path NAMES include-what-you-use iwyu) if(ENABLE_IWYU AND iwyu_path) set_property(TARGET projecteur PROPERTY CXX_INCLUDE_WHAT_YOU_USE ${iwyu_path}) endif() Projecteur-0.10/CONTRIBUTING.md000066400000000000000000000003661451344070600160160ustar00rootroot00000000000000# Contributing * Contributions are very welcome. * When contributing to this repository, please first discuss the change(s) you wish to implement via issue, email, or any other method with the owners of this repository before making a change. Projecteur-0.10/LICENSE.md000066400000000000000000000020401451344070600151600ustar00rootroot00000000000000Copyright 2018-2021, Jahn Fuchs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Projecteur-0.10/README.md000066400000000000000000000450231451344070600150430ustar00rootroot00000000000000# Projecteur 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. [gh-badge-dev]: https://github.com/jahnf/Projecteur/workflows/ci-build/badge.svg?branch=develop [gh-badge-rel]: https://github.com/jahnf/Projecteur/workflows/ci-build/badge.svg?branch=master [gh-link-dev]: https://github.com/jahnf/Projecteur/actions?query=workflow%3Aci-build+branch%3Adevelop [gh-link-rel]: https://github.com/jahnf/Projecteur/actions?query=workflow%3Aci-build+branch%3Amaster ## Motivation I saw the Logitech Spotlight device in action at a conference and liked it immediately. Unfortunately as in a lot of cases, software is only provided for Windows and Mac. The device itself works just fine on Linux, but the cool spotlight feature is only available using additional software. So here it is: a Linux application for the Logitech Spotlight. ## Table of Contents - [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 * Configurable desktop spotlight * _shade color_, _opacity_, _cursor_, _border_, _center dot_ and different _shapes_ * Zoom (magnifier) functionality * Multiple screen support * Support of devices beyond the Logitech Spotlight (see [Device Support](#device-support)) * Button mapping: * Map any button on the device to (almost) any keyboard combination. * Switch between (cycle through) custom spotlight presets. * 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 [](./doc/screenshot-settings.png) [](./doc/screenshot-spot.png) [](./doc/screenshot-button-mapping.png) [](./doc/screenshot-traymenu.png) ### Planned features * Support for more customizable button mapping actions. * Support of more proprietary features of the Logitech Spotlight and other devices. ## Supported Environments The application was mostly tested on Ubuntu 18.04, Ubuntu 20.04 (GNOME) and OpenSuse 15 (GNOME) but should work on almost any Linux/X11 Desktop. In case you are building the application yourself, make sure you have the correct udev rules installed (see [pre-requisites section](#pre-requisites)). ## How it works With a connection via the USB Dongle Receiver or via Bluetooth, the Logitech Spotlight device will be detected by Linux as a HID device with mouse and keyboard events. As mouse events, the device sends relative cursor movements and left button presses. Acting as a keyboard, the device basically just sends left and right arrow key press events when forward or back is pressed on the device. The mouse move events of the device are what we are mainly interested in. Since the device is already detected as a mouse input device and able to move the cursor, we simply detect if the Spotlight device is sending mouse move events. If it is sending mouse events, we will 'turn on' the desktop spot (virtual laser). For more details: Have a look at the source code ;) ### Button mapping 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 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 cloudsmith. Currently binary packages for _Ubuntu_, _Debian_, _Fedora_, _OpenSuse_, _CentOS_ and _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] * on secondery server: [![projecteur-rel-badge]][projecteur-rel-dl] * Latest development version: * 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_ is available. [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://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 [projecteur-dev-dl]: https://projecteur.de/downloads/develop/latest [projecteur-rel-dl]: https://projecteur.de/downloads/stable/latest ## Building ### Requirements * C++14 compiler * CMake 3.6 or later * Qt 5.7 and later ### Build Example ```sh git clone https://github.com/jahnf/Projecteur cd Projecteur mkdir build && cd build cmake .. make ``` Building against other Qt versions, than the default one from your Linux distribution can be done by setting the `QTDIR` variable during CMake configuration. Example: `QTDIR=/opt/Qt/5.9.6/gcc_64 cmake ..` ## Installation/Running ### Pre-requisites #### When building Projecteur yourself The input devices detected from the Spotlight device must be readable to the user running the application. To make this easier there is a udev rule template file in this repository: `55-projecteur.rules.in` * During the CMake run, the file `55-projecteur.rules` will be created from this template in your **build directory**. Copy that generated file to `/lib/udev/rules.d/55-projecteur.rules` * Most recent systems (using systemd) will automatically pick up the rule. If not, run `sudo udevadm control --reload-rules` and `sudo udevadm trigger` to load the rules without a reboot. * After that, the input devices from the Logitech USB Receiver (but also the Bluetooth device) in /dev/input should be readable/writable by you. (See also about [device detection](#device-shows-as-not-connected)) * When building against the Qt version that comes with your distribution's packages, you might need to install some additional QML module packages. For example this is the case for Ubuntu, where you need to install the packages `qml-module-qtgraphicaleffects`, `qml-module-qtquick-window2`, `qml-modules-qtquick2` and `qtdeclarative5-dev` to satisfy the application's run time dependencies. ### Application Menu The application menu is accessible via the system tray icon. There you will find the preferences and the menu entry to exit the application. If the system tray icon is missing, see the [Troubleshooting](#missing-system-tray) section. ### Command Line Interface 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. 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 `--help-all` option or can also be found on the man pagers with newer versions of _Projecteur_ (`man projecteur`). ### Scriptability _Projecteur_ allows you to set almost all aspects of the spotlight via the command line 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 can but utilized for that. For instance, if you like to use _Projecteur_ as a tool while sharing your screen in a video call without additional presenter hardware, you can assign global shortcuts in your window manager (e.g. GNOME) to run the commands `projecteur -c spot=on` 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 device. For this you can assign a global keyboard shortcut in your window manager (e.g. KDE, GNOME...) to run the command `projecteur -c spot=toggle`. You will then be able to turn the digital spot on and off with the assigned keyboard shortcut while sharing your screen in an online presentation or call. ### Device Support Besides the _Logitech Spotlight_, the following devices are currently supported out of the box: * 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 Besides the Logitech Spotlight, similar devices can be used and are supported. Additional devices can be added to `devices.conf`. At CMake configuration time, the project will be configured to support these devices and also create entries for them in the generated udev-rule file. #### Runtime _Projecteur_ will also accept devices as supported when added via the `-D` command line option. Example: `projecteur -D 04b3:310c` This will enable devices within _Projecteur_ and the application will try to connect to that device if it is detected. It is, however, up to the user to make sure the device is accessible (via udev rules). ### Troubleshooting #### Opaque Spotlight / No Transparency To be able to show transparent windows, a **compositing manager** is necessary. If there is no compositing manager running you will see the spotlight overlay as an opaque window. * On **KDE** it might be necessary to turn on Desktop effects to allow transparent windows. * Depending on your Linux Desktop and configuration there might not be a compositing manager running by default. You can run `xcompmgr`, `compton` or others manually. * Examples: `xcompmgr -c -t-6 -l-6 -o.1` or `xcompmgr -c` #### Missing System Tray _Projecteur_ was developed and tested on GNOME and KDE Desktop environments, but should work on most other desktop environments. If the system tray with the _Application Menu_ is not showing, commands can be send to the application to bring up the preferences dialog, test the spotlight, quit the application or set spotlight properties. See [Command Line Interface](#command-line-interface). There is also a command line option (`-m`) to prevent the preferences dialog from hiding, allowing it only to minimize - behaving more like a regular application window. On some distributions that have a **GNOME Desktop** by default there is **no system tray extensions** installed (_Fedora_ for example). You can install the [KStatusNotifierItem/AppIndicator Support][appind-ext] or the [TopIcons Plus][topicon-ext] GNOME extension to have a system tray that can show the _Projecteur_ tray icon (and also from other applications like Dropbox or Skype). [appind-ext]: https://extensions.gnome.org/extension/615/appindicator-support/ [topicon-ext]: https://extensions.gnome.org/extension/1031/topicons/ #### Zoom is not updated while spotlight is shown Zoom does not update while spotlight is shown due to how the zoom currently works. A screenshot is taken shortly before the overlay window is shown, and then a magnified section is shown wherever the mouse/spotlight is. If the zoom would be updated while the overlay window is shown, the overlay window it self would show up in the magnified section. That is a general problem that other magnifier tools also face, although they get around the problem by showing the magnified content rectangle always in the same position on the screen. #### Wayland 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 ``` #### Wayland Zoom On Wayland the Zoom feature is currently only implemented on KDE and GNOME. This is done with the help of their respective DBus interfaces for screen capturing. On other environments with Wayland, the zoom feature is not currently supported. #### Device shows as not connected If the device shows as not connected, there are some things you can do: * Check for devices with _Projecteur_'s command line option `-d` or `--device-scan` option. This will show you a list of all supported and detected devices and also if they are readable/writable. If a detected device is not readable/writable, it is an indicator that there is something wrong with the installed _udev_ rules. * Manually on the shell: Check if the device is detected by the Linux system: Run `cat /proc/bus/input/devices | grep -A 5 "Vendor=046d"` \ This should show one or multiple spotlight devices (among other Logitech devices) * Check that the corresponding `/dev/input/event??` device file is readable by you. \ Example: `test -r /dev/input/event19 && echo "SUCCESS" || echo "NOT readable"` * Make sure you don't have conflicting udev rules installed, e.g. first you installed the udev rule yourself and later you used the automatically built Linux packages to install _Projecteur_. ## Changelog See [CHANGELOG.md](./doc/CHANGELOG.md) for a detailed changelog. ## License Copyright 2018-2021 Jahn Fuchs This project is distributed under the [MIT License](https://opensource.org/licenses/MIT), see [LICENSE.md](./LICENSE.md) for more information. Projecteur-0.10/cmake/000077500000000000000000000000001451344070600146405ustar00rootroot00000000000000Projecteur-0.10/cmake/modules/000077500000000000000000000000001451344070600163105ustar00rootroot00000000000000Projecteur-0.10/cmake/modules/ArchiveExportInfo.cmake000066400000000000000000000005311451344070600227100ustar00rootroot00000000000000# Fallback for version generation from pure git archive exports set(GIT_EXPORT_VERSION_SHORTHASH "a7b2649") set(GIT_EXPORT_VERSION_FULLHASH "a7b2649296f5f8b6793e0c38cdee93d36831404b") set(GIT_EXPORT_VERSION_BRANCH "tag: v0.10, master") # needs parsing in cmake... set(GIT_EXPORT_VERSION_DATE_MONTH_YEAR "2023-10-17") set(HAS_GIT_EXPORT_INFO 1) Projecteur-0.10/cmake/modules/ArchiveVersionInfo.cmake.in000066400000000000000000000012341451344070600234620ustar00rootroot00000000000000# Auto generated archive version information # Included in created source archives set(@prefix@_VERSION_MAJOR "@VERSION_MAJOR@") set(@prefix@_VERSION_MINOR "@VERSION_MINOR@") set(@prefix@_VERSION_PATCH "@VERSION_PATCH@") set(@prefix@_VERSION_FLAG "@VERSION_FLAG@") set(@prefix@_VERSION_DISTANCE "@VERSION_DISTANCE@") set(@prefix@_VERSION_SHORTHASH "@VERSION_SHORTHASH@") set(@prefix@_VERSION_FULLHASH "@VERSION_FULLHASH@") set(@prefix@_VERSION_STRING "@VERSION_STRING@") set(@prefix@_VERSION_ISDIRTY "@VERSION_ISDIRTY@") set(@prefix@_VERSION_BRANCH "@VERSION_BRANCH@") set(@prefix@_VERSION_DATE_MONTH_YEAR "@VERSION_DATE_MONTH_YEAR@") set(@prefix@_VERSION_SUCCESS 1) Projecteur-0.10/cmake/modules/GitVersion.cc.in000066400000000000000000000013741451344070600213220ustar00rootroot00000000000000#include "@TARGET@-GitVersion.h" namespace @TARGET@ { const char* version_string() { return "@VERSION_STRING@"; } unsigned int version_major() { return @VERSION_MAJOR@; } unsigned int version_minor() { return @VERSION_MINOR@; } unsigned int version_patch() { return @VERSION_PATCH@; } const char* version_flag() { return "@VERSION_FLAG@"; } unsigned int version_distance() { return @VERSION_DISTANCE@; } const char* version_shorthash() { return "@VERSION_SHORTHASH@"; } 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@ Projecteur-0.10/cmake/modules/GitVersion.cmake000066400000000000000000000564641451344070600214220ustar00rootroot00000000000000# GitVersion.cmake - Generate version information using information from git. # Best suited with git-flow workflows. # Written for building dcled-hidapi - userland driver for the Dream Cheeky LED Message Board # Copyright 2018 Jahn Fuchs # Distributed under the MIT License. See accompanying LICENSE file. # Definition: # LAST_TAG_VERSION = latest tagged version (e.g. 1.2.0 for v1.2.0) or 0.0.0 if it does not exist. # # Version Number rules: # - on 'master': X.Y.Z[-DIST] (using LAST_TAG_VERSION),while DIST should always be 0 on the master branch # - on 'develop' and other branches : X.Y.Z-ALPHA_FLAG.DIST (using LAST_TAG_VERSION, incrementing Y by 1) # - on release branches: X.Y.Z-RC_FLAG.DIST (using semver from release branch name # or XYZ like on develop if not possible) # DIST is either calculated to last tag or to the closest rc-X.Y.Z tag # * DISTANCE is only added on versions from master branch when != 0 # * On all other branches DISTANCE is always added. # * All version numbers besides the ones from master have a pre-release identifier set. # * When printing the version string and the PATCH number is 0 - the patch number is omitted. ### Configuration set(VERSION_TAG_PREFIX v) # Should be the same as configured in git-flow set(VERSION_ALPHA_FLAG alpha) # Pre-release identifier for all builds besides release and hotfix branches set(VERSION_RC_FLAG rc) # Pre-release identifier for all builds from release and hotfix branches set(VERSION_RC_START_TAG_PREFIX "rc-") # If available tags with the given prefix are used for distance calculation on release branches. set(RC_BRANCH_PREFIX release) # e.g. release/0.2 set(HOTFIX_BRANCH_PREFIX hotfix) # e.g. hotfix/2.0.3 set(MAIN_BRANCH master) set(_GitVersion_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}") # Get the version information for a directory, sets the following variables # ${prefix}_VERSION_SUCCESS // 0 on error (e.g. git not found), 1 on success # ${prefix}_VERSION_MAJOR # ${prefix}_VERSION_MINOR # ${prefix}_VERSION_PATCH # ${prefix}_VERSION_FLAG # ${prefix}_VERSION_DISTANCE # ${prefix}_VERSION_SHORTHASH # ${prefix}_VERSION_FULLHASH # ${prefix}_VERSION_ISDIRTY // 0 or 1 if tree has local modifications # ${prefix}_VERSION_STRING // Full version string, e.g. 1.2.3-rc.239 # # A created version number can be overruled if the following variables are set and the version number is GREATER # than the dynamically created one. # - ${prefix}_OR_VERSION_MAJOR # - ${prefix}_OR_VERSION_MINOR # - ${prefix}_OR_VERSION_PATCH # # A version 'type' (release or develop) in case the branch cannot be determined via git # - #{prefix}_FALLBACK_VERSION_TYPE function(get_version_info prefix directory) set(${prefix}_VERSION_SUCCESS 0 PARENT_SCOPE) set(${prefix}_VERSION_MAJOR 0) set(${prefix}_VERSION_MINOR 0) set(${prefix}_VERSION_PATCH 0) set(${prefix}_VERSION_BRANCH unknown) set(${prefix}_VERSION_FLAG unknown) set(${prefix}_VERSION_DISTANCE 0) set(${prefix}_VERSION_DISTANCE 0 PARENT_SCOPE) 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) endif() if("${${prefix}_OR_VERSION_MINOR}" STREQUAL "") set(${prefix}_OR_VERSION_MINOR 0) endif() if("${${prefix}_OR_VERSION_PATCH}" STREQUAL "") 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 execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --match "${VERSION_TAG_PREFIX}[0-9].[0-9]*" RESULT_VARIABLE result OUTPUT_VARIABLE GIT_TAG_VERSION ERROR_VARIABLE error_out OUTPUT_STRIP_TRAILING_WHITESPACE WORKING_DIRECTORY ${directory} ) if(result EQUAL 0) if(GIT_TAG_VERSION MATCHES "^${VERSION_TAG_PREFIX}?([0-9]+)\\.([0-9]+)(\\.([0-9]+))?(-([0-9]+))?.*$") set(${prefix}_VERSION_MAJOR ${CMAKE_MATCH_1}) set(${prefix}_VERSION_MINOR ${CMAKE_MATCH_2}) if(NOT ${CMAKE_MATCH_4} STREQUAL "") set(${prefix}_VERSION_PATCH ${CMAKE_MATCH_4}) endif() if(NOT ${CMAKE_MATCH_6} STREQUAL "") set(${prefix}_VERSION_DISTANCE ${CMAKE_MATCH_6}) endif() endif() else() # Count distance execute_process(COMMAND ${GIT_EXECUTABLE} rev-list --count HEAD RESULT_VARIABLE result OUTPUT_VARIABLE GIT_DISTANCE OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_VARIABLE error_out WORKING_DIRECTORY ${directory} ) if(result EQUAL 0) set(${prefix}_VERSION_DISTANCE ${GIT_DISTANCE}) endif() endif() execute_process(COMMAND ${GIT_EXECUTABLE} describe --always --dirty RESULT_VARIABLE result OUTPUT_VARIABLE GIT_ALWAYS_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_VARIABLE error_out WORKING_DIRECTORY ${directory} ) if(result EQUAL 0) if(GIT_ALWAYS_VERSION MATCHES "^.*-dirty$") set(${prefix}_VERSION_ISDIRTY 1 PARENT_SCOPE) endif() endif() # Get committer date set(ENV_LC_TIME ENV{LC_TIME}) set(ENV{LC_TIME} C) # we want to enforce C locale for date formatting execute_process(COMMAND ${GIT_EXECUTABLE} show -s --format=%cd "--date=format:%B %Y" RESULT_VARIABLE result OUTPUT_VARIABLE GIT_DATE_MONTH_YEAR ERROR_VARIABLE error_out OUTPUT_STRIP_TRAILING_WHITESPACE WORKING_DIRECTORY ${directory} ) set(ENV{LC_TIME} ENV_LC_TIME) # Reset environment variable to previous value if(result EQUAL 0) set(${prefix}_VERSION_DATE_MONTH_YEAR "${GIT_DATE_MONTH_YEAR}" PARENT_SCOPE) endif() # Check the branch we are on execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD RESULT_VARIABLE result OUTPUT_VARIABLE GIT_BRANCH OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_VARIABLE error_out WORKING_DIRECTORY ${directory} ) if(result EQUAL 0) if("${GIT_BRANCH}" STREQUAL "HEAD" AND NOT "$ENV{TRAVIS_BRANCH}" STREQUAL "") set(GIT_BRANCH "$ENV{TRAVIS_BRANCH}") endif() set(${prefix}_VERSION_BRANCH "${GIT_BRANCH}") set(${prefix}_VERSION_BRANCH "${GIT_BRANCH}" PARENT_SCOPE) # Check for release branch string(LENGTH ${RC_BRANCH_PREFIX} PREFIX_LEN) string(SUBSTRING ${GIT_BRANCH} 0 ${PREFIX_LEN} COMPARE_PREFIX) string(COMPARE EQUAL ${RC_BRANCH_PREFIX} ${COMPARE_PREFIX} ON_RELEASE_BRANCH) # Check for hotfix branch string(LENGTH ${HOTFIX_BRANCH_PREFIX} PREFIX_LEN) string(SUBSTRING ${GIT_BRANCH} 0 ${PREFIX_LEN} COMPARE_PREFIX) string(COMPARE EQUAL ${HOTFIX_BRANCH_PREFIX} ${COMPARE_PREFIX} ON_HOTFIX_BRANCH) # Check for master branch string(COMPARE EQUAL "${MAIN_BRANCH}" "${GIT_BRANCH}" ON_MASTER) if(ON_RELEASE_BRANCH) set(${prefix}_VERSION_FLAG ${VERSION_RC_FLAG}) set(RC_VERSION_MAJOR 0) set(RC_VERSION_MINOR 0) set(RC_VERSION_PATCH 0) if(GIT_BRANCH MATCHES "^${RC_BRANCH_PREFIX}.*([0-9]+)\\.([0-9]+)(\\.([0-9]+))?.*$") set(RC_VERSION_MAJOR ${CMAKE_MATCH_1}) set(RC_VERSION_MINOR ${CMAKE_MATCH_2}) if(NOT ${CMAKE_MATCH_4} STREQUAL "") set(RC_VERSION_PATCH ${CMAKE_MATCH_4}) endif() endif() if("${RC_VERSION_MAJOR}.${RC_VERSION_MINOR}.${RC_VERSION_PATCH}" VERSION_GREATER "${${prefix}_VERSION_MAJOR}.${${prefix}_VERSION_MINOR}.${${prefix}_VERSION_PATCH}") set(${prefix}_VERSION_MAJOR ${RC_VERSION_MAJOR}) set(${prefix}_VERSION_MINOR ${RC_VERSION_MINOR}) set(${prefix}_VERSION_PATCH ${RC_VERSION_PATCH}) else() # Auto increment minor number, patch = 0 MATH(EXPR ${prefix}_VERSION_MINOR "${${prefix}_VERSION_MINOR}+1") set(${prefix}_VERSION_PATCH 0) endif() # Try to get distance from last rc start tag execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --match "${VERSION_RC_START_TAG_PREFIX}[0-9].[0-9]*" RESULT_VARIABLE result OUTPUT_VARIABLE GIT_RC_TAG_VERSION ERROR_VARIABLE error_out OUTPUT_STRIP_TRAILING_WHITESPACE WORKING_DIRECTORY ${directory} ) if(result EQUAL 0) if(GIT_RC_TAG_VERSION MATCHES "^${VERSION_RC_START_TAG_PREFIX}?([0-9]+)\\.([0-9]+)(\\.([0-9]+))?(-([0-9]+))?.*$") if(NOT ${CMAKE_MATCH_6} STREQUAL "") set(${prefix}_VERSION_DISTANCE ${CMAKE_MATCH_6}) else() set(${prefix}_VERSION_DISTANCE 0) endif() endif() endif() elseif(ON_HOTFIX_BRANCH) set(${prefix}_VERSION_FLAG ${VERSION_RC_FLAG}) set(RC_VERSION_MAJOR 0) set(RC_VERSION_MINOR 0) set(RC_VERSION_PATCH 0) if(GIT_BRANCH MATCHES "^${RC_BRANCH_PREFIX}.*([0-9]+)\\.([0-9]+)(\\.([0-9]+))?.*$") set(RC_VERSION_MAJOR ${CMAKE_MATCH_1}) set(RC_VERSION_MINOR ${CMAKE_MATCH_2}) if(NOT ${CMAKE_MATCH_4} STREQUAL "") set(RC_VERSION_PATCH ${CMAKE_MATCH_4}) endif() endif() if("${RC_VERSION_MAJOR}.${RC_VERSION_MINOR}.${RC_VERSION_PATCH}" VERSION_GREATER "${${prefix}_VERSION_MAJOR}.${${prefix}_VERSION_MINOR}.${${prefix}_VERSION_PATCH}") set(${prefix}_VERSION_MAJOR ${RC_VERSION_MAJOR}) set(${prefix}_VERSION_MINOR ${RC_VERSION_MINOR}) set(${prefix}_VERSION_PATCH ${RC_VERSION_PATCH}) else() # Auto increment patch number MATH(EXPR ${prefix}_VERSION_PATCH "${${prefix}_VERSION_PATCH}+1") endif() elseif(ON_MASTER) set(${prefix}_VERSION_FLAG "") endif() endif() if(NOT ON_MASTER AND NOT ON_RELEASE_BRANCH AND NOT ON_HOTFIX_BRANCH) # Auto increment version number, set alpha flag MATH(EXPR ${prefix}_VERSION_MINOR "${${prefix}_VERSION_MINOR}+1") set(${prefix}_VERSION_PATCH 0) set(${prefix}_VERSION_FLAG ${VERSION_ALPHA_FLAG}) endif() set(${prefix}_VERSION_FLAG ${${prefix}_VERSION_FLAG} 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 OUTPUT_VARIABLE GIT_SHORT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_VARIABLE error_out WORKING_DIRECTORY ${directory} ) if(resultSH EQUAL 0) set(${prefix}_VERSION_SHORTHASH ${GIT_SHORT_HASH} PARENT_SCOPE) else() message(STATUS "Version-Info: Could not fetch short version hash.") set(${prefix}_VERSION_SHORTHASH "unknown" PARENT_SCOPE) endif() execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse HEAD RESULT_VARIABLE resultFH OUTPUT_VARIABLE GIT_FULL_HASH OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_VARIABLE error_out WORKING_DIRECTORY ${directory} ) if(resultFH EQUAL 0) set(${prefix}_VERSION_FULLHASH ${GIT_FULL_HASH} PARENT_SCOPE) else() message(STATUS "Version-Info: Could not fetch full version hash.") set(${prefix}_VERSION_FULLHASH "unknown" PARENT_SCOPE) endif() if(resultSH EQUAL 0 AND resultFH EQUAL 0) set(${prefix}_VERSION_SUCCESS 1 PARENT_SCOPE) endif() else() message(STATUS "Version-Info: Git not found. Possible incomplete version information.") endif() if("${${prefix}_VERSION_BRANCH}" STREQUAL "unknown" OR "${${prefix}_VERSION_BRANCH}" STREQUAL "") if("${${prefix}_FALLBACK_VERSION_TYPE}" STREQUAL "release") set(ON_MASTER ON) set(${prefix}_VERSION_FLAG "") set(${prefix}_VERSION_FLAG "" PARENT_SCOPE) set(${prefix}_VERSION_DISTANCE 0) set(${prefix}_VERSION_DISTANCE 0 PARENT_SCOPE) endif() set(${prefix}_VERSION_BRANCH "not-within-git-repo" PARENT_SCOPE) endif() # Check if overrule version is greater than dynamically created one if("${${prefix}_OR_VERSION_MAJOR}.${${prefix}_OR_VERSION_MINOR}.${${prefix}_OR_VERSION_PATCH}" VERSION_GREATER "${${prefix}_VERSION_MAJOR}.${${prefix}_VERSION_MINOR}.${${prefix}_VERSION_PATCH}") set(${prefix}_VERSION_MAJOR ${${prefix}_OR_VERSION_MAJOR}) set(${prefix}_VERSION_MINOR ${${prefix}_OR_VERSION_MINOR}) set(${prefix}_VERSION_PATCH ${${prefix}_OR_VERSION_PATCH}) endif() set(${prefix}_VERSION_MAJOR ${${prefix}_VERSION_MAJOR} PARENT_SCOPE) set(${prefix}_VERSION_MINOR ${${prefix}_VERSION_MINOR} PARENT_SCOPE) set(${prefix}_VERSION_PATCH ${${prefix}_VERSION_PATCH} PARENT_SCOPE) set(${prefix}_VERSION_DISTANCE ${${prefix}_VERSION_DISTANCE} PARENT_SCOPE) # Build version string... set(VERSION_STRING "${${prefix}_VERSION_MAJOR}.${${prefix}_VERSION_MINOR}") set(VERSION_STRING_FULL "${VERSION_STRING}.${${prefix}_VERSION_PATCH}") 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 ${${prefix}_VERSION_DISTANCE_OFFSET}) set(VERSION_STRING "${VERSION_STRING}-${${prefix}_VERSION_FLAG}") set(VERSION_STRING_FULL "${VERSION_STRING_FULL}-${${prefix}_VERSION_FLAG}") endif() if(NOT ${${prefix}_VERSION_FLAG} STREQUAL "") 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 ${${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() set(${prefix}_VERSION_STRING "${VERSION_STRING}" PARENT_SCOPE) set(${prefix}_VERSION_STRING_FULL "${VERSION_STRING_FULL}" PARENT_SCOPE) endfunction() # Add version information to a target, header and source file are configured from templates. # (GitVersion.h.in and GitVersion.cc.in if no other templates are defined) # Variables available to input templates # @TARGET@ = target name given in the function call, converted to C Identifier (e.g. replace '-')- # @VERSION_MAJOR@, @VERSION_MINOR@, @VERSION_PATCH@, @VERSION_FLAG@, @VERSION_DISTANCE@ # @VERSION_SHORTHASH@, @VERSION_FULLHASH@, @VERSION_STRING@, @VERSION_ISDIRTY, @VERSION_BRANCH@ function(add_version_info_custom_prefix target prefix directory) list(LENGTH ARGN NUM_TEMPLATE_ARGS) if(NUM_TEMPLATE_ARGS EQUAL 0) # Use default templates list(APPEND ARGN "${_GitVersion_DIRECTORY}/GitVersion.h.in") list(APPEND ARGN "${_GitVersion_DIRECTORY}/GitVersion.cc.in") endif() string(MAKE_C_IDENTIFIER "${target}" targetid) # Set default values, in case sth goes wrong badly set(VERSION_MAJOR 0) set(VERSION_MINOR 0) 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}") get_target_property(TARGET_VMAJOR ${target} VERSION_MAJOR) if(TARGET_VMAJOR) set(${prefix}_OR_VERSION_MAJOR ${TARGET_VMAJOR}) endif() get_target_property(TARGET_VMINOR ${target} VERSION_MINOR) if(TARGET_VMINOR) set(${prefix}_OR_VERSION_MINOR ${TARGET_VMINOR}) set(VERSION_MINOR ${TARGET_VMINOR}) endif() get_target_property(TARGET_VPATCH ${target} VERSION_PATCH) if(TARGET_VPATCH) set(${prefix}_OR_VERSION_PATCH ${TARGET_VPATCH}) endif() get_target_property(TARGET_VTYPE ${target} VERSION_TYPE) 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" OR "${${prefix}_VERSION_SHORTHASH}" STREQUAL "unknown" OR "${${prefix}_VERSION_FULLHASH}" STREQUAL "" OR "${${prefix}_VERSION_SHORTHASH}" STREQUAL "") include(ArchiveExportInfo OPTIONAL RESULT_VARIABLE GIT_EXPORT_INFO_FILE_PRESENT) if("${GIT_EXPORT_VERSION_SHORTHASH}" MATCHES "(.?Format:).*") set(HAS_GIT_EXPORT_INFO OFF) endif() if(GIT_EXPORT_INFO_FILE_PRESENT AND HAS_GIT_EXPORT_INFO) message(STATUS "Using ArchiveExportInfo as fallback for version info.") set(${prefix}_VERSION_SHORTHASH "${GIT_EXPORT_VERSION_SHORTHASH}") set(${prefix}_VERSION_FULLHASH "${GIT_EXPORT_VERSION_FULLHASH}") set(${prefix}_VERSION_BRANCH "${GIT_EXPORT_VERSION_BRANCH}") set(${prefix}_VERSION_DATE_MONTH_YEAR "${GIT_EXPORT_VERSION_DATE_MONTH_YEAR}") if("${${prefix}_VERSION_BRANCH}" MATCHES ".*[ \t]+[->]+[\t ]+(.*)([,]?.*)") set(${prefix}_VERSION_BRANCH "${CMAKE_MATCH_1}") elseif("${${prefix}_VERSION_BRANCH}" MATCHES ".*,[ \t](.*)") if("${CMAKE_MATCH_1}" STREQUAL "${MAIN_BRANCH}") set(ON_MASTER ON) endif() endif() # Check for release branch string(LENGTH ${RC_BRANCH_PREFIX} PREFIX_LEN) string(SUBSTRING ${${prefix}_VERSION_BRANCH} 0 ${PREFIX_LEN} COMPARE_PREFIX) string(COMPARE EQUAL ${RC_BRANCH_PREFIX} ${COMPARE_PREFIX} ON_RELEASE_BRANCH) # Check for hotfix branch string(LENGTH ${HOTFIX_BRANCH_PREFIX} PREFIX_LEN) string(SUBSTRING ${${prefix}_VERSION_BRANCH} 0 ${PREFIX_LEN} COMPARE_PREFIX) string(COMPARE EQUAL ${HOTFIX_BRANCH_PREFIX} ${COMPARE_PREFIX} ON_HOTFIX_BRANCH) # Check for master branch if(NOT ON_MASTER) string(COMPARE EQUAL "${MAIN_BRANCH}" "${${prefix}_VERSION_BRANCH}" ON_MASTER) endif() if(ON_MASTER) set(${prefix}_VERSION_FLAG "") elseif(ON_RELEASE_BRANCH) set(${prefix}_VERSION_FLAG "${VERSION_RC_FLAG}") elseiF(ON_HOTFIX_BRANCH) set(${prefix}_VERSION_FLAG "hotfix") else() set(${prefix}_VERSION_FLAG "${VERSION_ALPHA_FLAG}") endif() # Build version string... set(VERSION_STRING "${${prefix}_VERSION_MAJOR}.${${prefix}_VERSION_MINOR}") set(VERSION_STRING_FULL "${VERSION_STRING}.${${prefix}_VERSION_PATCH}") 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) set(VERSION_STRING "${VERSION_STRING}-${${prefix}_VERSION_FLAG}") set(VERSION_STRING_FULL "${VERSION_STRING_FULL}-${${prefix}_VERSION_FLAG}") endif() if(NOT ${${prefix}_VERSION_FLAG} STREQUAL "") 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)) set(VERSION_STRING "${VERSION_STRING}${${prefix}_VERSION_DISTANCE}") set(VERSION_STRING_FULL "${VERSION_STRING_FULL}${${prefix}_VERSION_DISTANCE}") endif() set(${prefix}_VERSION_STRING "${VERSION_STRING}") set(${prefix}_VERSION_STRING_FULL "${VERSION_STRING_FULL}") endif() endif() endif() if(${${prefix}_VERSION_SUCCESS}) # All information gathered via git else() message(STATUS "Version-Info: Failure during version retrieval. Possible incomplete version information!") endif() # Test if we are building from an archive that has generated version information set(VERSION_MAJOR ${${prefix}_VERSION_MAJOR}) set(VERSION_MINOR ${${prefix}_VERSION_MINOR}) set(VERSION_PATCH ${${prefix}_VERSION_PATCH}) set(VERSION_FLAG ${${prefix}_VERSION_FLAG}) set(VERSION_DISTANCE ${${prefix}_VERSION_DISTANCE}) set(VERSION_SHORTHASH ${${prefix}_VERSION_SHORTHASH}) set(VERSION_FULLHASH ${${prefix}_VERSION_FULLHASH}) 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}) # Fallback if("${VERSION_DATE_MONTH_YEAR}" STREQUAL "") string(TIMESTAMP VERSION_DATE_MONTH_YEAR "%b %Y") endif() set_target_properties(${target} PROPERTIES VERSION_MAJOR "${VERSION_MAJOR}" VERSION_MINOR "${VERSION_MINOR}" VERSION_PATCH "${VERSION_PATCH}" VERSION_FLAG "${VERSION_FLAG}" VERSION_DISTANCE "${VERSION_DISTANCE}" VERSION_SHORTHASH "${VERSION_SHORTHASH}" VERSION_FULLHASH "${VERSION_FULLHASH}" 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}" ) set(TARGET ${prefix}) foreach(template_file ${ARGN}) if(template_file MATCHES "(.*)(\.in)$") get_filename_component(output_basename "${CMAKE_MATCH_1}" NAME) else() get_filename_component(output_basename "${template_file}" NAME) endif() set(output_file "${output_dir}/${prefix}-${output_basename}") configure_file("${template_file}" "${output_file}") list(APPEND output_files "${output_file}") endforeach() configure_file("${_GitVersion_DIRECTORY}/ArchiveVersionInfo.cmake.in" "archive_append/cmake/modules/ArchiveVersionInfo_${prefix}.cmake" @ONLY) get_target_property(type ${target} TYPE) if(type STREQUAL "SHARED_LIBRARY") set_target_properties(${target} PROPERTIES SOVERSION "${VERSION_MAJOR}.${VERSION_MINOR}") set_property(TARGET ${target} PROPERTY VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}") endif() set_property(TARGET ${target} APPEND PROPERTY SOURCES ${output_files}) target_include_directories(${target} PUBLIC $) message(STATUS "Version info for '${target}': ${VERSION_STRING}") endfunction() function(add_version_info target directory) string(MAKE_C_IDENTIFIER "${target}" prefix) add_version_info_custom_prefix(${target} ${prefix} ${directory} ${ARGN}) endfunction() Projecteur-0.10/cmake/modules/GitVersion.h.in000066400000000000000000000006421451344070600211610ustar00rootroot00000000000000#pragma once namespace @TARGET@ { const char* version_string(); unsigned int version_major(); unsigned int version_minor(); unsigned int version_patch(); const char* version_flag(); unsigned int version_distance(); const char* version_shorthash(); const char* version_fullhash(); bool version_isdirty(); const char* version_branch(); const char* version_buildtype(); } // end namespace @TARGET@ Projecteur-0.10/cmake/modules/LinuxDistributionInfo.cmake000066400000000000000000000042361451344070600236320ustar00rootroot00000000000000# This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md 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. function(get_linux_distribution VAR_DIST_NAME VAR_DIST_VERSION) # Set fallback defaults set(DIST_NAME "linux") set(DIST_NAME_SET 0) set(DIST_VERSION "unknown") set(DIST_VERSION_SET 0) # Read os- lsb-... release files file(GLOB rel_info_files "/etc/*-release") foreach(info_file IN LISTS rel_info_files) file(STRINGS "${info_file}" file_lines LIMIT_COUNT 128) list(APPEND rel_info_all "${file_lines}") endforeach() # Get distribution id/name - try different keys foreach(var ID DISTRIB_ID NAME) foreach(line IN LISTS rel_info_all) if( "${line}" MATCHES "^${var}=[\"]?([^ \"]*)") string(STRIP "${CMAKE_MATCH_1}" DIST_NAME) string(TOLOWER "${DIST_NAME}" DIST_NAME) string(REPLACE "\\" "_" DIST_NAME "${DIST_NAME}") string(REPLACE "/" "_" DIST_NAME "${DIST_NAME}") set(DIST_NAME_SET 1) break() endif() endforeach() if(DIST_NAME_SET) break() endif() endforeach() # Get distribution version/release - try different keys foreach(var VERSION_ID DISTRIB_RELEASE VERSION) foreach(line IN LISTS rel_info_all) if( "${line}" MATCHES "^${var}=[\"]?([^ \"]*)") string(STRIP "${CMAKE_MATCH_1}" DIST_VERSION) string(TOLOWER "${DIST_VERSION}" DIST_VERSION) string(REPLACE "\\" "_" DIST_VERSION "${DIST_VERSION}") string(REPLACE "/" "_" DIST_VERSION "${DIST_VERSION}") set(DIST_VERSION_SET 1) break() endif() endforeach() if(DIST_VERSION_SET) break() endif() endforeach() if(NOT DIST_NAME_SET) message(STATUS "Could not get linux distribution id, defaulting to 'linux'") endif() if(NOT DIST_VERSION_SET) message(STATUS "Could not get linux version, defaulting to 'unknown'") endif() set(${VAR_DIST_NAME} "${DIST_NAME}" PARENT_SCOPE) set(${VAR_DIST_VERSION} "${DIST_VERSION}" PARENT_SCOPE) endfunction() Projecteur-0.10/cmake/modules/LinuxPackaging.cmake000066400000000000000000000320431451344070600222200ustar00rootroot00000000000000# This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md cmake_minimum_required(VERSION 3.6) include(LinuxDistributionInfo) set(_LinuxPackaging_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}") set(_LinuxPackaging_cpack_template "LinuxPkgCPackConfig.cmake.in") list(APPEND _LinuxPackaging_MAP_dist_pkgtype "debian::DEB" "ubuntu::DEB" "opensuse::RPM" "opensuse-leap::RPM" "fedora::RPM" "centos::RPM" "rhel::RPM" "arch::PKGBUILD" "archlinux::PKGBUILD" ) set(_LinuxPackaging_default_pkgtype "TGZ") # Function that adds 'dist-package' target # Arguments: # PROJECT : Project name to package # TARGET : Main executable target with version information # DESCRIPTION_BRIEF : Brief package description. # DESCRIPTION_FULL : Full package description. # CONTACT : Package maintainer/contact. # HOMEAPGE : The project homepage # DEBIAN_SECTION.....: A valid debian package section (default=devel) function(add_dist_package_target) set(oneValueArgs PROJECT # project name to package TARGET # main executable build target that has version information attached to it DESCRIPTION_BRIEF DESCRIPTION_FULL CONTACT # Maintainer / contact person HOMEPAGE DEBIAN_SECTION PREINST_SCRIPT POSTINST_SCRIPT PRERM_SCRIPT POSTRM_SCRIPT ) set(requiredArgs PROJECT TARGET) cmake_parse_arguments(PKG "" "${oneValueArgs}" "" ${ARGN}) foreach(arg IN LISTS requiredArgs) if("${PKG_${arg}}" STREQUAL "") message(FATAL_ERROR "Required argument '${arg}' is not set.") endif() endforeach() if(NOT TARGET ${PKG_TARGET}) message(FATAL_ERROR "Argument 'TARGET' needs to be a valid target.") endif() get_target_property(PKG_VERSION_STRING_FULL ${PKG_TARGET} VERSION_STRING_FULL) get_target_property(PKG_VERSION_STRING ${PKG_TARGET} VERSION_STRING) get_target_property(PKG_VERSION_MAJOR ${PKG_TARGET} VERSION_MAJOR) get_target_property(PKG_VERSION_MINOR ${PKG_TARGET} VERSION_MINOR) get_target_property(PKG_VERSION_PATCH ${PKG_TARGET} VERSION_PATCH) get_target_property(PKG_VERSION_FLAG ${PKG_TARGET} VERSION_FLAG) get_target_property(PKG_VERSION_DISTANCE ${PKG_TARGET} VERSION_DISTANCE) get_target_property(PKG_VERSION_BRANCH ${PKG_TARGET} VERSION_BRANCH) if("${PKG_VERSION_MAJOR}" STREQUAL "") set(PKG_VERSION_MAJOR 0) endif() if("${PKG_VERSION_MINOR}" STREQUAL "") set(PKG_VERSION_MINOR 0) endif() if("${PKG_VERSION_PATCH}" STREQUAL "") set(PKG_VERSION_PATCH 0) endif() set(PKG_VERSION_STRING_BASE "${PKG_VERSION_MAJOR}.${PKG_VERSION_MINOR}.${PKG_VERSION_PATCH}") if("${PKG_VERSION_FLAG}" STREQUAL "") set(PKG_VERSION_IDENTIFIERS "1") else() set(PKG_VERSION_IDENTIFIERS "0${PKG_VERSION_FLAG}.${PKG_VERSION_DISTANCE}") endif() # Set defaults if not set if("${PKG_CONTACT}" STREQUAL "") set(PKG_CONTACT "Generic Maintainer ") endif() if("${PKG_DEBIAN_SECTION}" STREQUAL "") set(PKG_DEBIAN_SECTION "devel") endif() find_program(CPACK_COMMAND cpack) if(NOT CPACK_COMMAND) message(FATAL_ERROR "CPack command was not found.") endif() get_linux_distribution(LINUX_DIST_NAME LINUX_DIST_VERSION) # Get the package type to be generated by the target from our map variable set(PKG_TYPE "${_LinuxPackaging_default_pkgtype}") set(PKG_TYPE_FOUND 0) foreach(v "${LINUX_DIST_NAME}-${LINUX_DIST_VERSION}" "${LINUX_DIST_NAME}") foreach(pair ${_LinuxPackaging_MAP_dist_pkgtype}) if( "${pair}" MATCHES "${v}::(.*)") set(PKG_TYPE "${CMAKE_MATCH_1}") set(PKG_TYPE_FOUND 1) break() endif() endforeach() if(PKG_TYPE_FOUND) break() endif() endforeach() # Check if project package dependencies exist include(PkgDependencies${PKG_PROJECT} OPTIONAL RESULT_VARIABLE INCLUDED_PROJECT_DEPENDENCIES) if(INCLUDED_PROJECT_DEPENDENCIES AND PkgDependencies_MAP_${PKG_PROJECT}) set(PKG_DEPENDENCY_FOUND 0) # Find dependencies for Linux distribution (and version) foreach(v "${LINUX_DIST_NAME}-${LINUX_DIST_VERSION}" "${LINUX_DIST_NAME}") foreach(pair ${PkgDependencies_MAP_${PKG_PROJECT}}) if( "${pair}" MATCHES "${v}::(.*)") if("${PKG_TYPE}" STREQUAL "PKGBUILD") unset(_install_deps) foreach(_dep ${${CMAKE_MATCH_1}}) list(APPEND _install_deps "'${_dep}'") endforeach() string(REPLACE ";" " " PKG_DEPENDENCIES "${_install_deps}") else() string(REPLACE ";" ", " PKG_DEPENDENCIES "${${CMAKE_MATCH_1}}") endif() set(PKG_DEPENDENCY_FOUND 1) break() endif() endforeach() if(PKG_DEPENDENCY_FOUND) break() endif() endforeach() endif() if(INCLUDED_PROJECT_DEPENDENCIES AND PkgDependenciesMake_MAP_${PKG_PROJECT}) set(PKG_BUILD_DEPENDENCY_FOUND 0) # Find dependencies for Linux distribution (and version) foreach(v "${LINUX_DIST_NAME}-${LINUX_DIST_VERSION}" "${LINUX_DIST_NAME}") foreach(pair ${PkgDependenciesMake_MAP_${PKG_PROJECT}}) if( "${pair}" MATCHES "${v}::(.*)") if("${PKG_TYPE}" STREQUAL "PKGBUILD") unset(_install_deps) foreach(_dep ${${CMAKE_MATCH_1}}) list(APPEND _install_deps "'${_dep}'") endforeach() string(REPLACE ";" " " PKG_BUILD_DEPENDENCIES "${_install_deps}") else() string(REPLACE ";" ", " PKG_BUILD_DEPENDENCIES "${${CMAKE_MATCH_1}}") endif() set(PKG_BUILD_DEPENDENCY_FOUND 1) break() endif() endforeach() if(PKG_BUILD_DEPENDENCY_FOUND) break() endif() endforeach() endif() string(TOLOWER "${PKG_PROJECT}" PKG_NAME) set(PKG_LICENSE "MIT") set(PKG_DIST "${LINUX_DIST_NAME}-${LINUX_DIST_VERSION}") string(TIMESTAMP PKG_DATE "%Y-%m-%d") if("${PKG_TYPE}" STREQUAL "PKGBUILD") _makepkg_packaging() else() _cpack_default_packaging() endif() configure_file( "${_LinuxPackaging_DIRECTORY}/travis-ci-bintray-deploy.json.in" "${CMAKE_CURRENT_BINARY_DIR}/travis-ci-bintray-deploy.json" @ONLY) message(STATUS "Configured target 'dist-package' with Linux '${PKG_DIST}' and package type '${PKG_TYPE}'") # Make some information available to parent scope set(PKG_DIST "${PKG_DIST}" PARENT_SCOPE) set(PKG_TYPE "${PKG_TYPE}" PARENT_SCOPE) endfunction() # makepg packaging (arch linux/pacman) function(_makepkg_packaging) get_target_property(PKG_SOURCE_ARCHIVE_PATH source-archive OUTPUT_ARCHIVE) get_filename_component(PKG_SOURCE_ARCHIVE_DIR "${PKG_SOURCE_ARCHIVE_PATH}" DIRECTORY) get_filename_component(PKG_SOURCE_ARCHIVE_FILE "${PKG_SOURCE_ARCHIVE_PATH}" NAME) set(PKG_PKGBUILD_PKGREL 1) set(PKG_PKGBUILD_ARCH "${CMAKE_SYSTEM_PROCESSOR}") set(PKG_PKGBUILD_INSTALL_FILE "${PKG_NAME}.install") set(PKG_PKGBUILD_INSTALL_FILE_PATH "${PKG_SOURCE_ARCHIVE_DIR}/${PKG_PKGBUILD_INSTALL_FILE}") file(MAKE_DIRECTORY "${PKG_SOURCE_ARCHIVE_DIR}") file(WRITE "${PKG_PKGBUILD_INSTALL_FILE_PATH}" "# generated install file\n\n") if(PKG_PREINST_SCRIPT) file(READ "${PKG_PREINST_SCRIPT}" _pkg_preinst_script_content) file(APPEND "${PKG_PKGBUILD_INSTALL_FILE_PATH}" "pre_install() {\n" " : # null operation in case the preinst file is empty\n" " ${_pkg_preinst_script_content}" "}\n\n" "pre_upgrade() {\n" " : # null operation in case the preinst file is empty\n" " ${_pkg_preinst_script_content}" "}\n\n" ) endif() if(PKG_POSTINST_SCRIPT) file(READ "${PKG_POSTINST_SCRIPT}" _pkg_postinst_script_content) file(APPEND "${PKG_PKGBUILD_INSTALL_FILE_PATH}" "post_install() {\n" " : # null operation in case the preinst file is empty\n" " ${_pkg_postinst_script_content}" "}\n\n" "post_upgrade() {\n" " : # null operation in case the preinst file is empty\n" " ${_pkg_postinst_script_content}" "}\n\n" ) endif() if(PKG_PRERM_SCRIPT) file(READ "${PKG_PRERM_SCRIPT}" _pkg_prerm_script_content) file(APPEND "${PKG_PKGBUILD_INSTALL_FILE_PATH}" "pre_remove() {\n" " : # null operation in case the preinst file is empty\n" " ${_pkg_prerm_script_content}" "}\n\n" ) endif() if(PKG_POSTRM_SCRIPT) file(READ "${PKG_POSTRM_SCRIPT}" _pkg_postrm_script_content) file(APPEND "${PKG_PKGBUILD_INSTALL_FILE_PATH}" "post_remove() {\n" " : # null operation in case the preinst file is empty\n" " ${_pkg_postrm_script_content}" "}\n\n" ) endif() # makepkg: '-' is not allowed in version number string(REPLACE "-" "" PKG_PKGBUILD_VER "${PKG_VERSION_STRING_FULL}") set(PKG_CONFIG_TEMPLATE "${_LinuxPackaging_DIRECTORY}/PKGBUILD.in") set(PKG_CONFIG_FILE "${PKG_SOURCE_ARCHIVE_DIR}/PKGBUILD") configure_file("${PKG_CONFIG_TEMPLATE}" "${PKG_CONFIG_FILE}" @ONLY) find_program(MAKEPKG_EXECUTABLE makepkg) set(PKGBUILD_OUTPUT_FILE "${PKG_NAME}-${PKG_PKGBUILD_VER}-${PKG_PKGBUILD_PKGREL}-${PKG_PKGBUILD_ARCH}.pkg.tar.xz") set(PKGBUILD_OUTPUT_PATH "${PKG_SOURCE_ARCHIVE_DIR}/${PKGBUILD_OUTPUT_FILE}") set(PKGBUILD_FINAL_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/dist-pkg") set(PKGBUILD_FINAL_FILE "${PKG_NAME}-${PKG_PKGBUILD_VER}-${PKG_PKGBUILD_PKGREL}_${LINUX_DIST_NAME}-${PKG_PKGBUILD_ARCH}.pkg.tar.xz") add_custom_target(dist-package COMMAND ${MAKEPKG_EXECUTABLE} -f --log --skipinteg WORKING_DIRECTORY "${PKG_SOURCE_ARCHIVE_DIR}" VERBATIM ) add_custom_command(TARGET dist-package PRE_BUILD COMMAND ${CMAKE_COMMAND} ARGS -E make_directory "${PKGBUILD_FINAL_OUTPUT_DIR}" ) add_custom_command(TARGET dist-package POST_BUILD COMMAND ${CMAKE_COMMAND} ARGS -E copy_if_different "${PKGBUILD_OUTPUT_PATH}" "${PKGBUILD_FINAL_OUTPUT_DIR}/${PKGBUILD_FINAL_FILE}" ) add_dependencies(dist-package source-archive) endfunction() # Default cpack packaging (DEB, RPM, TGZ) function(_cpack_default_packaging) set(PKG_CPACK_PKG_FILENAME "${PKG_NAME}-${PKG_VERSION_STRING}_${PKG_DIST}-${CMAKE_SYSTEM_PROCESSOR}") set(PKG_CPACK_PKG_FILE_PREFIX "dist-pkg") set(PKG_CONFIG_TEMPLATE "${_LinuxPackaging_DIRECTORY}/LinuxPkgCPackConfig.cmake.in") set(PKG_CONFIG_FILE "${CMAKE_CURRENT_BINARY_DIR}/CPackConfig-${PKG_TYPE}.cmake") configure_file("${PKG_CONFIG_TEMPLATE}" "${PKG_CONFIG_FILE}" @ONLY) add_custom_target(dist-package COMMAND ${CPACK_COMMAND} --config "${PKG_CONFIG_FILE}" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} VERBATIM ) endfunction() ## Add 'source-archive' target function(add_source_archive_target target) find_package(Git) find_program(TAR_EXECUTABLE tar) find_program(GZIP_EXECUTABLE gzip) if(GIT_FOUND) get_target_property(VERSION_STRING ${target} VERSION_STRING) execute_process(COMMAND ${GIT_EXECUTABLE} describe --always RESULT_VARIABLE result OUTPUT_VARIABLE GIT_TREEISH ERROR_VARIABLE error_out OUTPUT_STRIP_TRAILING_WHITESPACE WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} ) if(NOT result EQUAL 0) set(GIT_TREEISH "HEAD") endif() # Write set(ARCHIVE_STAGE_DIR "${PROJECT_BINARY_DIR}/archive_stage") set(FILE_BASENAME "${target}-${VERSION_STRING}_source") set(GIT_TAR_FILE_PATH "${ARCHIVE_STAGE_DIR}/${FILE_BASENAME}.git-stage.tar") add_custom_command(OUTPUT "${GIT_TAR_FILE_PATH}" COMMAND ${CMAKE_COMMAND} ARGS -E make_directory "${ARCHIVE_STAGE_DIR}" COMMAND ${GIT_EXECUTABLE} ARGS archive --format=tar --prefix=${target}-${VERSION_STRING}/ --output="${GIT_TAR_FILE_PATH}" ${GIT_TREEISH} WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} COMMENT "Running git archive (${target})..." ) set(ARCHIVE_OUTPUT_DIR "${PROJECT_BINARY_DIR}/archive_output") set(TAR_FILE_PATH "${ARCHIVE_OUTPUT_DIR}/${FILE_BASENAME}.tar") set(TARGZ_FILE_PATH "${TAR_FILE_PATH}.gz") set(TARGZ_FILE_NAME "${FILE_BASENAME}.tar.gz") set(TAR_APPEND_DIR "${PROJECT_BINARY_DIR}/archive_append") add_custom_command(OUTPUT "${TARGZ_FILE_PATH}" DEPENDS "${GIT_TAR_FILE_PATH}" COMMAND ${CMAKE_COMMAND} ARGS -E copy "${GIT_TAR_FILE_PATH}" "${TAR_FILE_PATH}" COMMAND ${CMAKE_COMMAND} ARGS -E create_symlink "${PROJECT_BINARY_DIR}/archive_append" "${ARCHIVE_STAGE_DIR}/${target}-${VERSION_STRING}" COMMAND ${TAR_EXECUTABLE} ARGS -rf "${TAR_FILE_PATH}" "${target}-${VERSION_STRING}/*" COMMAND ${GZIP_EXECUTABLE} ARGS -9f "${TAR_FILE_PATH}" WORKING_DIRECTORY ${ARCHIVE_STAGE_DIR} COMMENT "Add version information to git archive (${target})..." ) set(ARCHIVE_FINAL_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/dist-pkg") add_custom_target(source-archive DEPENDS "${TARGZ_FILE_PATH}") set_target_properties(source-archive PROPERTIES OUTPUT_ARCHIVE "${TARGZ_FILE_PATH}") add_custom_command(TARGET source-archive PRE_BUILD COMMAND ${CMAKE_COMMAND} ARGS -E make_directory "${ARCHIVE_FINAL_OUTPUT_DIR}") add_custom_command(TARGET source-archive POST_BUILD COMMAND ${CMAKE_COMMAND} ARGS -E copy_if_different "${TARGZ_FILE_PATH}" "${ARCHIVE_FINAL_OUTPUT_DIR}/${TARGZ_FILE_NAME}") else() message(STATUS "Cannot add 'source-archive' target, git not found.") endif() endfunction() Projecteur-0.10/cmake/modules/LinuxPkgCPackConfig.cmake.in000066400000000000000000000054231451344070600235140ustar00rootroot00000000000000# CPackConfig for Linux packages set(CPACK_GENERATOR "@PKG_TYPE@") set(CPACK_PACKAGING_INSTALL_PREFIX "@CMAKE_INSTALL_PREFIX@") set(CPACK_CMAKE_GENERATOR "@CMAKE_GENERATOR@") set(CPACK_PACKAGE_NAME "@PKG_NAME@") set(CPACK_STRIP_FILES ON) set(CPACK_PACKAGE_CONTACT "@PKG_CONTACT@") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "@PKG_DESCRIPTION_BRIEF@") set(CPACK_PACKAGE_DESCRIPTION "@PKG_DESCRIPTION_FULL@") set(CPACK_PACKAGE_VERSION @PKG_VERSION_STRING_FULL@) set(CPACK_PACKAGE_VERSION_MAJOR @PKG_VERSION_MAJOR@) set(CPACK_PACKAGE_VERSION_MINOR @PKG_VERSION_MINOR@) set(CPACK_PACKAGE_VERSION_PATCH @PKG_VERSION_PATCH@) set(CPACK_INSTALL_CMAKE_PROJECTS "@CMAKE_CURRENT_BINARY_DIR@;@PKG_PROJECT@;ALL;.") set(CPACK_PACKAGE_FILE_NAME "@PKG_CPACK_PKG_FILENAME@") set(CPACK_OUTPUT_FILE_PREFIX "@PKG_CPACK_PKG_FILE_PREFIX@") set(CPACK_DEBIAN_PACKAGE_NAME "${CPACK_PACKAGE_NAME}") set(CPACK_RPM_PACKAGE_NAME "${CPACK_PACKAGE_NAME}") set(CPACK_RPM_COMPRESSION_TYPE gzip) set(CPACK_DEBIAN_PACKAGE_VERSION "@PKG_VERSION_STRING_BASE@") set(CPACK_DEBIAN_PACKAGE_RELEASE "@PKG_VERSION_IDENTIFIERS@") set(CPACK_RPM_PACKAGE_VERSION "@PKG_VERSION_STRING_BASE@") set(CPACK_RPM_PACKAGE_RELEASE "@PKG_VERSION_IDENTIFIERS@") set(CPACK_RPM_PACKAGE_LICENSE "@PKG_LICENSE@") set(CPACK_RPM_PACKAGE_DESCRIPTION "@PKG_DESCRIPTION_FULL@") set(CPACK_RPM_PACKAGE_AUTOPROV 1) set(CPACK_RPM_PACKAGE_AUTOREQ 1) set(CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION "@CMAKE_INSTALL_PREFIX@" "@CMAKE_INSTALL_PREFIX@/bin" "@CMAKE_INSTALL_PREFIX@/share" "@CMAKE_INSTALL_PREFIX@/share/applications" "@CMAKE_INSTALL_PREFIX@/share/man" "@CMAKE_INSTALL_PREFIX@/share/man/man1" "@CMAKE_INSTALL_UDEVDIR@" "@CMAKE_INSTALL_UDEVRULESDIR@" ) # 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@") set(CPACK_DEBIAN_PACKAGE_DEPENDS "@PKG_DEPENDENCIES@") # Post and Pre-install actions if necessary set(CPACK_RPM_PRE_INSTALL_SCRIPT_FILE "@PKG_PREINST_SCRIPT@") set(CPACK_RPM_POST_INSTALL_SCRIPT_FILE "@PKG_POSTINST_SCRIPT@") set(CPACK_RPM_PRE_UNINSTALL_SCRIPT_FILE "@PKG_PRERM_SCRIPT@") set(CPACK_RPM_POST_UNINSTALL_SCRIPT_FILE "@PKG_POSTRM_SCRIPT@") foreach(script CPACK_RPM_PRE_INSTALL_SCRIPT_FILE CPACK_RPM_POST_INSTALL_SCRIPT_FILE CPACK_RPM_PRE_UNINSTALL_SCRIPT_FILE CPACK_RPM_POST_UNINSTALL_SCRIPT_FILE) if(NOT "${${script}}" STREQUAL "") list(APPEND PKG_DEBIAN_CTRL_EXTRA "${${script}}") endif() endforeach() list(LENGTH PKG_DEBIAN_CTRL_EXTRA NUM_PKG_CTRL_SCRIPTS) set(CPACK_DEBIAN_PACKAGE_CONTROL_STRICT_PERMISSION TRUE) if(NUM_PKG_CTRL_SCRIPTS) set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${PKG_DEBIAN_CTRL_EXTRA}") endif() Projecteur-0.10/cmake/modules/PKGBUILD.in000066400000000000000000000010401451344070600200340ustar00rootroot00000000000000# PKGBUILD template file pkgname=@PKG_NAME@ pkgver=@PKG_PKGBUILD_VER@ pkgrel=@PKG_PKGBUILD_PKGREL@ pkgdesc="@PKG_DESCRIPTION_BRIEF@" arch=('@PKG_PKGBUILD_ARCH@') url="@PKG_HOMEPAGE@" license=('@PKG_LICENSE@') replaces=('') depends=(@PKG_DEPENDENCIES@) makedepends=(@PKG_BUILD_DEPENDENCIES@) install=@PKG_PKGBUILD_INSTALL_FILE@ source=("@PKG_SOURCE_ARCHIVE_FILE@") build() { cd $srcdir mkdir -p builddir cd builddir cmake $srcdir/@PKG_NAME@-@PKG_VERSION_STRING@ make -j2 } package() { cd builddir make DESTDIR=$pkgdir/ install } Projecteur-0.10/cmake/modules/PkgDependenciesProjecteur.cmake000066400000000000000000000027761451344070600244210ustar00rootroot00000000000000list(APPEND _PkgDeps_Projecteur_opensuse "libqt5-qtgraphicaleffects >= 5.7" "libQt5Widgets5 >= 5.7" "libQt5X11Extras5 >= 5.7" "libQt5DBus5 >= 5.7" "shadow" "udev" ) list(APPEND _PkgDeps_Projecteur_fedora "qt5-qtbase >= 5.7" "qt5-qtdeclarative >= 5.7" "qt5-qtgraphicaleffects >= 5.7" "qt5-qtx11extras >= 5.7" "passwd" "udev" ) list(APPEND _PkgDeps_Projecteur_centos "qt5-qtbase >= 5.7" "qt5-qtdeclarative >= 5.7" "qt5-qtgraphicaleffects >= 5.7" "qt5-qtx11extras >= 5.7" "passwd" "udev" ) list(APPEND _PkgDeps_Projecteur_debian "qml-module-qtgraphicaleffects (>= 5.7)" "libqt5widgets5 (>= 5.7)" "libqt5x11extras5 (>= 5.7)" "passwd" "udev" "libc6" ) list(APPEND _PkgDeps_Projecteur_archlinux "qt5-base>=5.7" "qt5-declarative>=5.7" "qt5-graphicaleffects>=5.7" "qt5-x11extras>=5.7" "udev" ) list(APPEND _PkgDepsMake_Projecteur_archlinux "fakeroot" "awk" "cmake" "make" "lsb-release" "tar" "pkg-config" "qt5-tools" ) list(APPEND PkgDependencies_MAP_Projecteur "debian::_PkgDeps_Projecteur_debian" "ubuntu::_PkgDeps_Projecteur_debian" "fedora::_PkgDeps_Projecteur_fedora" "centos::_PkgDeps_Projecteur_centos" "rhel::_PkgDeps_Projecteur_centos" "opensuse::_PkgDeps_Projecteur_opensuse" "opensuse-leap::_PkgDeps_Projecteur_opensuse" "archlinux::_PkgDeps_Projecteur_archlinux" "arch::_PkgDeps_Projecteur_archlinux" ) list(APPEND PkgDependenciesMake_MAP_Projecteur "archlinux::_PkgDepsMake_Projecteur_archlinux" "arch::_PkgDepsMake_Projecteur_archlinux" ) Projecteur-0.10/cmake/modules/Translation.cmake000066400000000000000000000144211451344070600216120ustar00rootroot00000000000000find_package(Qt5 REQUIRED COMPONENTS Core) # Extract the qmake executable location get_target_property(Qt5_QMAKE_EXECUTABLE Qt5::qmake IMPORTED_LOCATION) # Find Qts own translations dir (containing qt_*.qm, qtbase_*.qm ...) if(NOT QT_TRANSLATIONS_DIR) # Ask Qt5 where to put the translations execute_process(COMMAND ${Qt5_QMAKE_EXECUTABLE} -query QT_INSTALL_TRANSLATIONS OUTPUT_VARIABLE qt_translations_dir OUTPUT_STRIP_TRAILING_WHITESPACE) # For windows systems: replace \ with / in directory path file(TO_CMAKE_PATH "${qt_translations_dir}" qt_translations_dir) set(QT_TRANSLATIONS_DIR ${qt_translations_dir} CACHE PATH "The location of the Qt translations" FORCE) endif() find_package(Qt5LinguistTools QUIET) if(NOT Qt5_LRELEASE_EXECUTABLE) execute_process(COMMAND ${Qt5_QMAKE_EXECUTABLE} -query QT_INSTALL_BINS OUTPUT_VARIABLE _qt_bin_dir OUTPUT_STRIP_TRAILING_WHITESPACE) # For windows systems: replace \ with / in directory path file(TO_CMAKE_PATH "${_qt_bin_dir}" _qt_bin_dir) set(Qt5_LRELEASE_EXECUTABLE ${_qt_bin_dir}/lrelease) set(Qt5_LCONVERT_EXECUTABLE ${_qt_bin_dir}/lconvert) set(Qt5_LUPDATE_EXECUTABLE ${_qt_bin_dir}/lupdate) else() get_target_property(Qt5_LCONVERT_EXECUTABLE Qt5::lconvert IMPORTED_LOCATION) endif() # Helper function, takes the .qm file to be generated and a variable list of .ts files # to create a custom command that then can be used for a custom target. function(add_qm_translation_file _qm_file) foreach(_current_FILE ${ARGN}) get_filename_component(_abs_FILE ${_current_FILE} ABSOLUTE) list(APPEND _ts_files ${_abs_FILE}) endforeach() foreach(tsfile ${_ts_files}) SET(tsfiles_blank_sep "${tsfiles_blank_sep} ${tsfile}") endforeach() add_custom_command(OUTPUT ${_qm_file} COMMAND ${Qt5_LRELEASE_EXECUTABLE} ARGS ${_ts_files} -qm ${_qm_file} DEPENDS ${_ts_files} VERBATIM COMMENT "Executing: lrelease -silent ${tsfiles_blank_sep} -qm ${_qm_file}" ) endfunction() # Helper function, takes the qrc filename to generate and a variable list .qm files to be included. function(mk_translation_qrc_file _qrc_file) if(NOT EXISTS ${_qrc_file}) file(WRITE ${_qrc_file} "\n") file(APPEND ${_qrc_file} " \n") foreach(_qm_file ${ARGN}) get_filename_component(filename "${_qm_file}" NAME) file(APPEND ${_qrc_file} " ${_qm_file}\n") endforeach() file(APPEND ${_qrc_file} " \n") file(APPEND ${_qrc_file} "\n") endif() endfunction() # Helper function, takes the .qm file to be generated and a variable list .qm files # to be combined to one. Creates a custom command for the .qm file to be created. function(add_combined_qm_translation_file _combined_qm_file) foreach(_current_FILE ${ARGN}) get_filename_component(_abs_FILE ${_current_FILE} ABSOLUTE) list(APPEND _single_qm_files ${_abs_FILE}) endforeach() list(REMOVE_DUPLICATES _single_qm_files) add_custom_command(OUTPUT ${_combined_qm_file} COMMAND ${Qt5_LCONVERT_EXECUTABLE} ARGS -o ${_combined_qm_file} ${_single_qm_files} DEPENDS ${_single_qm_files} VERBATIM COMMENT "Executing: ${Qt5_LCONVERT_EXECUTABLE} -o ${_combined_qm_file} ${_single_qm_files}" ) endfunction() if(NOT TARGET ts_files) add_custom_target(ts_files) set_target_properties(ts_files PROPERTIES FOLDER "translation") set_target_properties(ts_files PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD 1) endif() # Function to add an updating 'task' to the custom translations_update target. # _prefix : prefix for the *.ts files, i.e. myprefix_de.ts # _input_dirs : list of directories to scan for translations with lupdate # _ourput_dir : where to produce the *.ts files function(add_translation_update_task _prefix _input_dirs _output_dir _languages) foreach(_lang ${_languages}) list(APPEND _tsfiles_lupdate "${_prefix}_${_lang}.ts") endforeach() set(_ts_files_tgt ts_files_${_prefix}) add_custom_target(${_ts_files_tgt}) set_target_properties(${_ts_files_tgt} PROPERTIES FOLDER "translation") set_target_properties(${_ts_files_tgt} PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD 1) add_custom_command(TARGET ${_ts_files_tgt} PRE_BUILD COMMAND ${Qt5_LUPDATE_EXECUTABLE} ARGS ${_input_dirs} ARGS -locations relative ARGS -ts ARGS -noobsolete ARGS ${_tsfiles_lupdate} WORKING_DIRECTORY ${_output_dir} COMMENT "Updating translations (${_prefix})..." ) add_dependencies(ts_files ${_ts_files_tgt}) endfunction() if(NOT TARGET qm_files) add_custom_target(qm_files) set_target_properties(qm_files PROPERTIES FOLDER "translation") endif() # Main function to be used in the main build configuration scripts. # Will add a target 'translations' that will create/copy all the necessary # .qm files to the given _target_dir for the given _languages. # This includes also the translations from qt itself. function(add_translations_target _prefix _target_dir _ts_dirs _languages) file(MAKE_DIRECTORY "${_target_dir}") # for each language foreach(_lang ${_languages}) # find all .ts files in the given _ts_dirs for our translations foreach(_ts_dir ${_ts_dirs}) file(GLOB _ts_files_glob LIST_DIRECTORIES false ${_ts_dir}/*_${_lang}.ts) list(APPEND _ts_files_all${_lang} ${_ts_files_glob}) endforeach() list(LENGTH _ts_files_all${_lang} _num_ts_files) if(_num_ts_files) set(_qm_file ${_target_dir}/${_prefix}_${_lang}.qm) add_qm_translation_file(${_qm_file} ${_ts_files_all${_lang}}) list(APPEND _qm_files ${_qm_file}) endif() endforeach() list(LENGTH _qm_files _num_qm_files) if(_num_qm_files) set(_qm_files_tgt qm_files_${_prefix}) add_custom_target(${_qm_files_tgt} ALL DEPENDS ${_qm_files}) if(TARGET ${_prefix}) set(_qrc_file translations.qrc) mk_translation_qrc_file(${_target_dir}/${_qrc_file} ${_qm_files}) set_property(TARGET ${_prefix} APPEND PROPERTY SOURCES "${_target_dir}/${_qrc_file}" ) add_dependencies(${_prefix} ${_qm_files_tgt}) else() message(FATAL_ERROR "'${_prefix}' is not a valid target.") endif() set_target_properties(${_qm_files_tgt} PROPERTIES FOLDER "translation") set_target_properties(${_qm_files_tgt} PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD 1) add_dependencies(qm_files ${_qm_files_tgt}) endif() endfunction() Projecteur-0.10/cmake/modules/travis-ci-bintray-deploy.json.in000066400000000000000000000020061451344070600244470ustar00rootroot00000000000000{ "package": { "name": "projecteur-@PKG_VERSION_BRANCH@", "repo": "Projecteur", "subject": "jahnf", "desc": "Automated build of Projecteur.", "website_url": "https://github.com/jahnf/Projecteur", "issue_tracker_url": "https://github.com/jahnf/Projecteur/issues", "vcs_url": "https://github.com/jahnf/Projecteur.git", "github_use_tag_release_notes": false, "licenses": ["MIT"], "labels": ["linux", "x11", "logitech", "spotlight", "desktop", "presentation"], "public_download_numbers": false, "public_stats": false }, "version": { "name": "@PKG_VERSION_STRING_FULL@", "desc": "Automated package build of Projecteur (@PKG_VERSION_STRING_FULL@)", "released": "@PKG_DATE@", "gpgSign": false }, "files": [ {"includePattern": "dist-pkg/(.*)", "uploadPattern": "packages/branches/@PKG_VERSION_BRANCH@/@PKG_VERSION_STRING_FULL@/$1" } ], "publish": true } Projecteur-0.10/cmake/templates/000077500000000000000000000000001451344070600166365ustar00rootroot00000000000000Projecteur-0.10/cmake/templates/copyright.in000066400000000000000000000024011451344070600211730ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: projecteur Source: @HOMEPAGE@ License: Expat Files: * Copyright: 2018-2021, Jahn Fuchs License: Expat License: Expat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: . The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. . THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.Projecteur-0.10/cmake/templates/postinst.in000077500000000000000000000001701451344070600210520ustar00rootroot00000000000000# Make sure uinput module is loaded modprobe uinput # Reload udev rules. udevadm control --reload-rules udevadm trigger Projecteur-0.10/cmake/templates/preinst.in000077500000000000000000000000021451344070600206450ustar00rootroot00000000000000# Projecteur-0.10/cmake/templates/projecteur.1000066400000000000000000000063671451344070600211160ustar00rootroot00000000000000.TH PROJECTEUR "1" "@VERSION_DATE_MONTH_YEAR@" "Projecteur @VERSION_STRING@" "User Commands" .SH NAME Projecteur \- virtual laser pointer for presentations .SH SYNOPSIS .B projecteur [\fI\,OPTION\/\fR]... .SH DESCRIPTION Projecteur provides a virtual laser pointer on the screen for use when giving presentations. The laser pointer can be controlled using a Logitech Spotlight or similar device. Projecteur supports a "laser pointer" like effect that is a colored dot on the screen, a "highlight" effect that dims the image except in the highlighted region, and a "zoom" effect that enlarges part of the display. .PP Projecteur can be configured with a dialog box activated from the system tray in a supported desktop environment. .PP .SH Options .TP \fB\-h\fR, \fB\-\-help\fR Show command line usage. .TP \fB\-\-help\-all\fR Show complete command line usage with all properties. .TP \fB\-v\fR, \fB\-\-version\fR Print application version. .TP \fB\-f\fR Print detailed application version information. .TP \fB\-\-cfg\fR \fIFILE\fR Set custom config file. .TP \fB\-d\fR, \fB\-\-device\-scan\fR Print device\-scan results. .TP \fB\-l\fR, \fB\-\-log\-level\fR \fILEVEL\fR Set log level, where LEVEL is one of \fBdbg\fR, \fBinf\fR, \fBwrn\fR, \fBerr\fR .TP \fB\-D\fR \fIDEVICE\fR Additional accepted device; DEVICE = vendorId:productId e.g., \fB\-D\fR 04b3:310c; e.g. \fB\-D\fR 0x0c45:0x8101; This option can be used multiple times and works in connection with the \fB\-\-device\-scan\fP option. .TP \fB\-c\fR \fICOMMAND\fR|\fIPROPERTY\fR Send command/property to a running instance. See \fBCommands\fP and \fBProperties\fP for details. This option can be use multiple times. .TP \fB\-\-disable-uinput\fR Disable uinput support. .TP \fB\-\-show-dialog\fR Show preferences dialog on application start. .TP \fB\-m\fR, \fB\-\-minimize-only\fR Only allow minimizing the dialog. Useful for desktop environments that do not have a system tray. .PP .SH Commands .TP spot=[on|off|toggle] Turn spotlight on/off or toggle. .TP settings=[show|hide] Show/hide preferences dialog. .TP preset=NAME Set a preset. .TP quit Quit the running instance. .PP .SH Properties .TP spot.size=[Integer] (5 ... 100) .TP spot.rotation=[Double] (0 ... 360) .TP spot.shape=[Value] (Circle, Square, Star, Ngon) .TP spot.shape.square.radius=[Integer] (0 ... 100) .TP spot.shape.star.points=[Integer] (3 ... 100) .TP spot.shape.star.innerradius=[Integer] (5 ... 100) .TP spot.shape.ngon.sides=[Integer] (3 ... 100) .TP shade=[Bool] (false, true) .TP shade.opacity=[Double] (0 ... 1) .TP shade.color=[Color] (HTML-color; #RRGGBB) .TP dot=[Bool] (false, true) .TP dot.size=[Integer] (3 ... 100) .TP dot.color=[Color] (HTML-color; #RRGGBB) .TP dot.opacity=[Double] (0 ... 1) .TP border=[Bool] (false, true) .TP border.size=[Integer] (0 ... 100) .TP border.color=[Color] (HTML-color; #RRGGBB) .TP border.opacity=[Double] (0 ... 1) .TP zoom=[Bool] (false, true) .TP zoom.factor=[Double] (1.5 ... 20) Projecteur-0.10/cmake/templates/projecteur.bash-completion000066400000000000000000000075771451344070600240460ustar00rootroot00000000000000# projecteur(1) completion -*- shell-script -*- _projecteur() { COMPREPLY=() local cur=${COMP_WORDS[COMP_CWORD]} local prev=${COMP_WORDS[COMP_CWORD-1]} local prev_prev=${COMP_WORDS[COMP_CWORD-2]} local first_level=0 # Handling of '=' if [ "${prev}" = "=" ]; then prev="${prev_prev}" prev_prev="=" fi local options="-h --help --help-all --version -v --cfg --device-scan -m --minimize-only" options="${options} --log-level -l --show-dialog --disable-uinput -D -c" case "$prev" in "-c") # Auto completion for commands and properties 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=" commands="${commands} shade= shade.opacity= shade.color= dot= dot.size= dot.color= dot.opacity=" commands="${commands} border= border.size= border.color= border.opacity= zoom= zoom.factor=" local fl=$(printf '%.1s' "$cur") [ ! "$fl" = "q" ] && compopt -o nospace COMPREPLY=( $(compgen -W "${commands}" -- $cur) ) ;; "spot") if [ "${prev_prev}" = "=" ] || [ "${cur}" = "=" ]; then [ "${cur}" = "=" ] && cur="" COMPREPLY=( $(compgen -W "on off toggle" -- $cur) ) fi ;; "spot.overlay") if [ "${prev_prev}" = "=" ] || [ "${cur}" = "=" ]; then [ "${cur}" = "=" ] && cur="" COMPREPLY=( $(compgen -W "false true" -- $cur) ) fi ;; "spot.multi-screen") if [ "${prev_prev}" = "=" ] || [ "${cur}" = "=" ]; then [ "${cur}" = "=" ] && cur="" COMPREPLY=( $(compgen -W "false true" -- $cur) ) fi ;; "settings") if [ "${prev_prev}" = "=" ] || [ "${cur}" = "=" ]; then [ "${cur}" = "=" ] && cur="" COMPREPLY=( $(compgen -W "show hide" -- $cur) ) fi ;; "spot.shape") if [ "${prev_prev}" = "=" ] || [ "${cur}" = "=" ]; then [ "${cur}" = "=" ] && cur="" COMPREPLY=( $(compgen -W "circle square star ngon" -- $cur) ) fi ;; "shade") if [ "${prev_prev}" = "=" ] || [ "${cur}" = "=" ]; then [ "${cur}" = "=" ] && cur="" COMPREPLY=( $(compgen -W "false true" -- $cur) ) fi ;; "dot") if [ "${prev_prev}" = "=" ] || [ "${cur}" = "=" ]; then [ "${cur}" = "=" ] && cur="" COMPREPLY=( $(compgen -W "false true" -- $cur) ) fi ;; "border") if [ "${prev_prev}" = "=" ] || [ "${cur}" = "=" ]; then [ "${cur}" = "=" ] && cur="" COMPREPLY=( $(compgen -W "false true" -- $cur) ) fi ;; "zoom") if [ "${prev_prev}" = "=" ] || [ "${cur}" = "=" ]; then [ "${cur}" = "=" ] && cur="" COMPREPLY=( $(compgen -W "false true" -- $cur) ) fi ;; "-D") # TODO: Auto completion for devices (vendorId:productId) COMPREPLY=( $(compgen -W "0123:4567" -- $cur) ) ;; "-l") COMPREPLY=( $(compgen -W "dbg inf wrn err" -- $cur) ) ;; "--log-level") COMPREPLY=( $(compgen -W "dbg inf wrn err" -- $cur) ) ;; "--cfg") # Auto completion for files local IFS=$'\n' local LASTCHAR=' ' compopt -o nospace COMPREPLY=( $(compgen -f -- ${cur}) ) if [ ${#COMPREPLY[@]} = 1 ]; then [ -d "$COMPREPLY" ] && LASTCHAR=/ COMPREPLY=$(printf %q%s "$COMPREPLY" "$LASTCHAR") else for ((i=0; i < ${#COMPREPLY[@]}; i++)); do [ -d "${COMPREPLY[$i]}" ] && COMPREPLY[$i]=${COMPREPLY[$i]}/ done fi ;; *) first_level=1 ;; esac if [ $first_level -eq 1 ]; then COMPREPLY=( $(compgen -W "${options}" -- $cur) ) fi } complete -F _projecteur projecteur Projecteur-0.10/cmake/templates/projecteur.desktop.in000066400000000000000000000003241451344070600230170ustar00rootroot00000000000000[Desktop Entry] Type=Application Exec=@PROJECTEUR_INSTALL_PATH@ Name=Projecteur GenericName=Linux/X11 application for the Logitech Spotlight device. Icon=projecteur Terminal=false Categories=Office;Presentation; Projecteur-0.10/cmake/templates/projecteur.metainfo.xml000066400000000000000000000025601451344070600233460ustar00rootroot00000000000000 projecteur Expat Expat Projecteur Virtual pointer for the Logitech Spotlight device @HOMEPAGE@

Projecteur is a virtual laser pointer for use with inertial pointers such as the Logitech Spotlight. Projecteur can show a colored dot, a highlighted circle or a zoom effect to act as a pointer. The location of the pointer moves in response to moving the handheld pointer device. The effect is much like that of a traditional laser pointer, except that it is captured by recording software and works across multiple screens.

usb:v046DpC53Ed* Highlight virtual pointer effect https://raw.githubusercontent.com/jahnf/Projecteur/develop/doc/screenshot-spot.png Office Presentation
Projecteur-0.10/devices.conf000066400000000000000000000010611451344070600160470ustar00rootroot00000000000000# List of supported devices, besides the Logitech Spotlight # From this config additional entries in the rules file and compiled sources are generated # Format: # vendorId, productId, [usb|bt], name # Example: # 0x0abc, 0x1234, usb, MyExample Device 0x0c45, 0x8101, usb, AVATTO H100 / August WP200 0x2312, 0x863d, usb, August LP315 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 Projecteur-0.10/doc/000077500000000000000000000000001451344070600143255ustar00rootroot00000000000000Projecteur-0.10/doc/CHANGELOG.md000066400000000000000000000100521451344070600161340ustar00rootroot00000000000000# 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 ### 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]) [i133]: https://github.com/jahnf/Projecteur/issues/133 [i144]: https://github.com/jahnf/Projecteur/issues/144 ## v0.9.1 ### Changes/Updates: - Fixes for automatically generated RPM Packages (especially Fedora) - Fixes for version numbers in generated packages (DEB and RPM) ## v0.9 ### Changes/Updates: - 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]); - Added bash-completion ([#110][p110]); - Added automated Fedora-33 build ([#111)][p111]; - Added automated OpenSUSE 15.2 build ([#115][p115]); - Automated build: Added automated CodeQL security analysis ([#113][p113]); - Added vibration support for the Logitech Spotlight (USB) ([#6][i6]); [p97]: https://github.com/jahnf/Projecteur/pull/97 [i104]: https://github.com/jahnf/Projecteur/issues/104 [i80]: https://github.com/jahnf/Projecteur/issues/80 [p110]: https://github.com/jahnf/Projecteur/pull/110 [p111]: https://github.com/jahnf/Projecteur/pull/111 [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 ### Changes/Updates: - Device button mapping: Map any button on your device to (almost) any button combination. - Store and load different setting presets. - Spotlight fade in/out effect. - Additional command line options: - `-m, --minimize-only`: Preferences dialog can only be minimized, particular useful on desktops without system tray. - `--show-dialog` : Preferences dialog will be shown at application start. - Show third-party licenses in about dialog. - Spotlight center dot opacity configurable. - Under the hood: Restructure device connection; Preparation for additional _hidraw_ communication with the device (vibration and other features). - Under the hood: switched CI builds to Github actions. - Automated Fedora 32 and Ubuntu 20.04 builds. - Additional automated package/build artifact upload to [cloudsmith.io](https://cloudsmith.io/~jahnf/repos/projecteur-develop/). ## v0.7 ### Changes/Updates: - Added the support to use with other devices (compile and run time). - Added logging output (UI and console) with different log levels. - Under the hood: Integration of a virtual device via uinput (preparation for button mapping feature in v0.8) - Rename `55-spotlight.rules` to `55-projecteur.rules` - CentOS-8 package build. ## v0.6 ### Changes/Updates: - Spotlight zoom Feature. - Updated udev rules, no need to add the user to a special group anymore. - Automated build of Fedora packages and Arch Linux packages. - Configurable spotlight borders. - Scriptability: Properties can be set via command line. - New Command line option for device scan. Projecteur-0.10/doc/LinuxRepositories.md000066400000000000000000000240241451344070600203600ustar00rootroot00000000000000# Projecteur Linux Repositories This document aims to list all Linux repositories where _Projecteur_ is available. Is something missing? Please let me know or create a pull request. ## Official Repositories ### Debian (and Debian based distributions) The stable version of _Projecteur_ is available in Debian starting with _Debian bullseye_. See [this listing](https://packages.debian.org/search?keywords=projecteur&searchon=names&suite=all§ion=all) for all available `projecteur` packages in Debian. ### Ubuntu 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 See: https://packages.gentoo.org/packages/x11-misc/projecteur ## User Repositories ### Arch Linux * https://aur.archlinux.org/packages/projecteur/ * https://aur.archlinux.org/packages/projecteur-git/ ### OpenSUSE User/community repositories: * https://software.opensuse.org/package/projecteur?search_term=projecteur ### Projecteur's Development Repositories Automated project builds from the development branch of _Projecteur_ are also uploaded to [cloudsmith.io](https://cloudsmith.io/~jahnf/repos/projecteur-develop/packages/) 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 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=stretch' > /etc/apt/sources.list.d/jahnf-projecteur-develop.list 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 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=buster' > /etc/apt/sources.list.d/jahnf-projecteur-develop.list apt-get update ``` #### 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 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 ``` #### 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 ``` #### 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 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 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 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 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 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 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 yum-config-manager --add-repo '/tmp/jahnf-projecteur-develop.repo' yum -q makecache -y --disablerepo='*' --enablerepo='jahnf-projecteur-develop' ``` Projecteur-0.10/doc/LogitechSpotlightHID++.md000066400000000000000000000361571451344070600207720ustar00rootroot00000000000000# 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. Projecteur-0.10/doc/screenshot-button-mapping.png000066400000000000000000002052471451344070600221640ustar00rootroot00000000000000PNG  IHDRcc@}sBIT|d IDATxgxUgf !t%w*(E;] D'vET@"Ҕz' $!@9ߏ݄@ }93`JA)Y 蚆aX@Shh-tMC4 C CYm4Mm=0 Sk躎l(Ca:k:2lk`M]@P@r0ueN440 @C5, a4 ,fL tM0nPtMP(Ω [^ֶ4Z.PJYX)V-0 Iɚ˺f{]dfJ)ݧ[a2Yi&aƄM&1uh0nl}#]Ӱ(h`1i~ʨa[oQ nPZlAQ,Að] ,L& [ٚkcBGvt2 VÖikgd; G}Z%ðg1 t]+joMɡh\JnVYװ1;) b}o6Q8lٱlQ6ǰ!vp;c( ن*Vla;6~VX6z<2 1K׭cN:dr@E}nu, bqY4²X_|lÛbAtm4&Jcm_0XPֺ)kYTDv\V`P XYV#Zg[B&fFqVm㨦[ou1z -4Lcb`2Ƴ2`2YǴ2IQt+<(0[ L&#mgM-O ふ}Ǝ-݄pVm mZ; mt e_ ϣ&]bX0&,ۡm0b`Vn;aך^xTt,sqפEQp&qVaX(z~EA1@㦭i=Wǽsa;&]Ö+XD)8/YLzgv(}90(?~jEVn]l >fҬ1EY(v@(:>`-fyMu[c%㢗8!9ٻC/«nq@}^6%?MsmVZޚm)>}R\(UFzPS}uooxlX{ţ:%1.2R"TR>/e91bB!W ]1!Bp2 ΙB!WcB!v$B!I0&BaG !BؑcB!v$B!I0&BaG !BؑcB!v$B!I0&BaG !Bؑ \wKyz(W; 9\`]{L;7Me'f{L W S$}ǎK'N:d8kuğVH A:i.t51㒓*4>6qH [Od'ryK/I0&Bsg4fxIĔkHxˆ[̴w؜KĬE%$^)W~$nwd{e/:'W# g4!Ͼ؇DytڗjP=?<SU&az'hmg0ebڋkJ]އBqΕ47gY=ӵiM| ybe2,^_mΡӥ߁QvMCso#9碕׎Υשph⏷ Z)eE]4)*z+(98`P @s ~(Bc+;ǑKQq 5qN?Ⱦ$ޤ'=q;Q.$JQ*# >C:ԥJQv>Bzf&fTn<׮eMTNB,^'NW<~eWy\kױ;Ō@/%/яuwmKw"-7Q~Yn %Il]RH=)W j'x6'Gm,:=W"1k N aswݑN ?HՂ47GZvK *i9Ołoo*3/g!4غWRSc42N,%+k o;߾=[ չhK+g>;]3i 5#bJe@eplJN"W"1! q\V.~Ӝ:qMVPp$ !D3@&qh_}8we:6$kpO_IyxeYl<K)ZޭN;',@|If7gГδ&d*BQz0vi]j^Eۗ7v?BFbgܭ, C*!:sK?tjIm=䝿x/:мq20`>/{Ѯ~iJ~̀[! *rRwA;o$VN!o7 iijo| k4:CNzq^KR*TV8)'] gxDШM}Ӥ m/Ur|]ֵkWe@"͝]:4Ycw "3cBWiD4jBE,abBq1!B;}B!# ƄB!H1!B;`L!Ž$B!# ƄB!H1!B;`L!Ž$B!# ƄB!H1!B;`L!Ž$B!#-##CٻB!+B!# ƄB!H1!B;`L!Ž$B!# ƄB!H1!B;`L!Ž$B!# ƄB!wAHKKwQ///j֬i!D65Nf]͛sIBp0Gnn.ٕU&Q <<ӐgSBؓ]ϡseϞ= :ggWcc(>_uajR؝C2sX|N~~ѐNC&iE|m U&>zсTq',@:F Ee]8Б,?Uh8~ ֹlbf>J׺zP޽IRe I˙pk};R56\xR!XDzX,L<WWWƌSv9C;mT8aOGteRyzoIEW gpDv ~>b05ބ֖ ryDM:pf/2ma!DmF;gKf$KGnx5v֑^T|6kA`%J.(@17YXh~:|C>{M)ݻxy_d*RyTnq.L[Aƒ'ԛYh:6 'x/w8Kvh _쿻v4[%,6-ҍL8)rp>{bc=7Sn(6tE:Ÿ[7km9y7I'b͍}f[퉟8o/i5qGcc=!*teJ@TNZO.#cW֟vmZȪ,`+gqc(:ɒO!-JThp9a71qn'}Aq>1/ r?S?ǒtaʈdֿƶqw]!( <<<ظq#/2}^׋-{'I@Ѡ3L{ {gW=M mxL~/N۶tGqvr¡Qz^bM@ho'.Mh`uoɝMؼ9kQ5t'[^v*|J*gpssjOH =KNnu̢`|etnMF2̈́ ;=֩7=kQ}L_o{T& s五KQtw Z0a@ !]xݝSr)&LPYBGz?)-9PM-TgJI$ 2gB8h}Z"T̄ԮŬ{㥑sI|[>_@[;LKnn.gZ}v஁UX"~3[ӭ䃯qc?ҮCsth 3Zb/Hxг?a !"X%+SԪUE0!>|8͚5cƌ p>"C¨ݬ;MFٸ`1'fs=hJݶ{('f=2s\\YXw S-ŗ϶ x0<Ÿ OcZ,Ƶu3]2WoiK5q/wy_j?u.VAQslO>p"<Sk.xʙDχ^f`7q >ahDe\#^ũ/Vʇ?-s==3\-?evzTMLIMx֌'2nr?$* &qT˟1 LІdS4D8(-B\-##㢯m޼&M`\<@LL ]tPVXAÆ /~?yh7V<*8Mxc:]Ulc֘Yg5iw2@L!(RLFގQظuDz-X~j@~bc.BkO !BTxfl6_rd)})P1ł|O ao26B\*eddnݺ*.WKSZkrl.BJеk׋^3c&BTvٻB!ͫ2"BaG !BؑcB!v$B!I0&BaG !BؑcB!v$B!I0&BaGΝ;9y$.B!իWiyH0VΝ;1tEB!U`˖-ڵWJr'OҴiS{C!Wf͚QiX (] !B\E*% J!B! B!4M垱 Kn SOۻ%#T85gN^吒S+H&5!393\s&E3=_ -ehzd_R&*gpxzK.Sq]&1#jr/'+䪔K[@H7iWp36,~E{("(c)Y#FuaX̟Oڏ/,H}?l6įOѡKɭ:izLZGN9Y}ygg#S׽f e!˻iy/ -0KN2v/%yak&Ϸc]ő31ub"˿_*Sc]GGܨZ{?̛Kol4zv2&넂M؛_b\ i7%PS҆/ Z Ҫ?UhJdKKQ㢔¡fF7ʫr}a1 {.Y|7Л'W_lˑݚ;(GskP|<< UBw1G8~Upqu+&z<{kRͭ/dYAI<^bd rt凗o 9۳̮IWwc>GӦիƬ4|ŗSgŋA?䠔A)U=Z] rxx$/q87ů_^ǫu>܃ygM<1AURa&,K(x\\^0^x`;H+g52D^{?;RYX#g(ͳNYҥ2U^0pdފ!a1:2{ t{K`1*9'ڱa?o( @a\Lt0LzN[¦ͬzdVޝ|AFFI|5JD[|y.vaPeS<]҅87cFp{ͥSN_Wd@]R±nv2^ _!@, ufΦ_/Y.9e(7371fV6~՗oi緵%?[ -'|θQ 3qJSn+ge;e>Ke˔N݆g{q>co{'üō~Ǿ<{<[ʮK Xl~GFo m7b }i\wO}ӾTۿ|n]5.̢oF*֎Zռ@DyGa[ټ7zWϏI0 v1:>GOUxa~w-}Sz?a±Mjh'l;/pt~ ^ͩIv}gPX,]$л U|hY~3ilP?vfuq6RRybn΅ɪ f="Qջ:7?yW.-מh|q]j>Cy_7EFPэ ڙE/oudĤܐ-̠UXH[ jOφq1LW޼4;'|cuύb_>kM,iM@U׳Otֿw7"Oc~d9i,c'Orw\\\pqq6l__OܜLPZgg:A|B.ޱB+F( wur2ȿ\v|NKp{>*/Hj^xWo@?ԛS|m{u#3x 1(j֠C/|-~>t?M~|v(?.~v;f=JjFϓ %yu>߅jرcy[ƎKZZ]u}Vaoq DLX*zB`dZtDs3lܟ{~汹[9gsJ Si_PVD|Kh2Xm0'Yp@w̾|z7v,A9iY"?LQ<2tGuNz/??Oݖw/olGlr;/#v- Mnv,Lg;ϽL4ޝ_g؜bj la]ٲ]fX< 0P*u`}BߓtQM׎lyE)o=[XՅ<,˶`34lo`shy 7 C'2gsg l=-hUۄR M-I%'fS RCW(Kve l[t#"sv3l_ݷ3/5̜}iOHSMk%(wˍV'3Gf+A֭i #ˉdlg˯2?9u%PW;Oo2ƭ4Mr/j_+9Sf[#g&]K6|Ms_juzkc.{}^_g8sd۷> ص${cK]]4ľ=i݄pO((/ܽO47թNݫZm[ 1N`^ìIt{ 5'vgzk⧃RR| h'j8ѵoŽ#o}zn%TOۡJєB ȧ|ȰMrk=궽ח+v1?F7FD(txq3Q>EO[S:H&!d0vSUf=d倻D76Pxt SJaS5=;6I8~}9Hc:x"/GHP$mz#Z?1xqGg+iĪЪX =z,kؾE͐bVqJ'Rn>~ m?Hŝ5+ 5wtAjמnGظm:x鉧_?f&gr7< Tlr=p3-Y_o=<ìr"p]+;d"pJk-~Oݞdr?+XgS4DOO<=u%+Yf>bLJѣG^%_T+HH?o23yXuqwBL'p]?%f2=-oOEV;U_nN(%a8g F"1>XjQ'MORgÜx3k||1fmiOOxkZgw5|!7nu >gw< ~I7JMjR' 1> sqw+\AD|ԺWw(Bм|*!v;w1PJl>[ŌMv[IL>ܛksmc/-gټy#+y(44 b(tV“\VNQPbdg߾O5ɶng0iUU lsyTS c*>{ qЂn`xw~2?rS7Ցu|>,F)x?54T*xqZnpLi+T D"'Ś_ǽ>ЬON6مOڂ,7w4w) ;+Gw73ꁻCvg9{yMpW:s/ n;F{(U0a֯'m.-vE;/>>><3ԩSgy悁R`LslHϛC1yTC禒c]rO9`Y?bt֟Gn)>].u缧sѳ&8 |>HbQ3m.~@tL䓗v]?|/'Np 8Uk*GeR^J$lc`Zeb/-K_t@S9|4$i=1@pVj]JֽBU ƶmYy#3IM-|ҁ _NKrݷ#Pܧ;q SV<+W"ժZ桛4ewB[IQg|IO9a=AkՈMd];8E,V-YCܢ遇G.a4}q#jT'# U3f\4?<\\)W)(w["iXǙɩ%.w(0.0nBYN~~>EFnp9A|_m o%fC8̹9C&ŇhC9}DzQ8a-3Rw3:=q]j/1{m}`>H|4 >wT^:u_$w%ڂ2ؾU:?f KξxxxPÛ=5!AMz}TNThZn;e*X|||x/*S2SIJ<Α]katc1*kƏᛍILIMMUT!c[Oou]wd|f?{~\.%8mK)Y/ L\=,2bXO?OZy~SYͦ2èĪ89v7|Κ ijwn =nfٔ| v􋮞Ë>}$؃Yя0ER7zeoNۓ?K{ oS2{s, 佱38|0DJKθB$9ih;ԡQ={z#-IMo0ٸ{V|/C51sylwlRNq|~v:ii)$&֯k7 o4aGqrg ̝h=6//Cܾ_2E7ox,ے緺dLiwܔǼ19ә̝^}ycD_9/08d Lz/o-gfϊKClq2,7ѮSp,tXvθPWwjI9P9^F .ؽQmh麚OY$b=rMSPC+N>dbdbɩ ]{sW:>ya'j(8fhcSX[_8wMtU~:8;>m^-װjb$w!vS~2h#CS^1/oG>ۘZ4i8teu,<|>x7fX-ZO s$xynUJȓ^'=RxԪՄbW.߹1@>k@tTQQQDE׷MҘmzФIWƬ˄9K^Oc`z2NN֙LMx`p\[2қ0zg;5Nt+>9lRtzߜjK7+ެ|3d/[F4jӗW0ˏ7\,ǻѬQGYèY0R=Ӱ̫~20ݸޑDnc$=%X;{js~2%e2vwjsfo/пc[Il،F3=[PFͣQ*KVZ:fإ wxe'lgv|uS5e|+;ۧ i0+]=P {ŏ';hU}>#~"4,yzv`5Nw<|K 4ۧls@!aLґ15O5|97n W,nk*gWIҡa o|#uR ,)4 Nh, z)Cn?@Tt~l>~÷O68m)Wz_mL:r|8J ^Klق3N3 .Avv6̑L3z%zfgz'izmOy?\r/XG[rgJ,\{Ke222.:Wfyn޼yøșrz4|w?8[H36jۋ0ы6 ~ҡTr;e:woCq$~֖#cդTbYJLٍd56Xv,-5Si#1>s tK=|j> Q>]YtE@ͣk׮R\HCiќ[8]HҩoLN72zlLx;NUeE@$^yFeݟ~/>gE r2+{F~qX3pt/lMfNiE(%kw˨/6ne֭l}?5]2-lݺK[m~[k:^̩z~;g\޺>q39p1VAc更%Y!i E/BA-lG8}>VÏ}IEaIƘPFkrwwmiXRL^yЅDC v;khz撜bK) 6s8!Zs(^Ro!v,D]sg(%i??Avٙ;ʒ®4}czWAlԗ߻2r656m>銶%ZyuͮX,Ix4r4/5}z6-[Q'4Z{W;l2"=O' >E # ^X^a\  syDM:.E<ܣ)aDȸp<ֽ9NͣLHdS?:9%*]= 9S5LF^U(Җw& !8,wOڴaQ&k rum_=EvEAS-zq|@Z֥[إWݹ YF IDAT>~Evha<6MeDkۗSnu ڍv@ݼ2uuj>u+?X3.NΔփ"nLlpvélDkOW\ʶo_B.+>MJ`R!;`zlҳ;wJ9^,m\ BčC1FHgE<v^ NDǺk\Hкe߂~y zc1 B8;Q;:c%I!FgΣDI&O9I'Ș"aBmH&oɈ^(053!)! Y$'z|zqmq1]&!,d{g y0ݔ3!nzsJUл%VDqUѨo ut@F=%i\[3^MSˋJ]~FG%2"=pPbLRN߽G¥)-N%e7JYAZ:D>x(A%({ 2Oַ2ӈL@ic%:ѪfEʔ@iQk~ʟkSS:'XF%c\g\uјazp:֝ݻܹσ 5RxGo>nD!܍G9T05˺3NYv_ ߱6 LER|":YGV%LfzjQGԩx_T7]++p {γE4gc6M‰uC"_9(5>w׭`ώll_{B`r+$ Vy̠X/?gsqH|$V 2O9A+qvuBI؃ [P9;>$^$`Y葑%;`!FwЯͭ,;l@61Wef&\_cҎZeeǹrSUW }z/?Լ̡p' dLN-Wfk}VSq,ᑍ)Wʓ꟟7 &ȝRHNd ɞ?lcg0@ubiUGg+\|'<^9U[2;NeTɂ~,]6jtDFe[کy(Ɇ?ߪ<^ F玃iϘ۰9fLK$N$pteK~f:YN hd Ev)l)YҌ{p6,{DD~47at(N z$y4@h}"op+ktiqDE#VӸ\KtAQ@JwOu5F\XdL2螖K*LU\ ENG̥œ*[YI$ e/A- Lz$57\L[`X1;,eIv4jkE ;82\b~R<$6\tj@k9=S:[bz9Gߨ5r7f@YYkbVt-ہu:0j`էz(Pz׶4y5em` <~#(\{2i ܆2PZa_`LO&ntzCwG,[_)1K0Y{$:ȿqS: B,Kd[Ȣ2ASjm00lM5Ͱตz3T:-c2gإ:_%[cxNfX~aOh#My繗*aV?&D_ $"|tڵPӭ[,?3 #dVtsc>敌ebH䑌= goKߟN}9fsT_͛վua*vJ%ϟZjv|͚5+PA(FiժU5cܪP֘N@<1Ϻx@m؊yad~PRc.ys}nAAAAx dLAO~Aoh4bm]*!2dLxVEU›HOAʒ1B9zhQ!Dbf$ Dg%jƄG4lذCA*/^Yp/AA(D[  Bɘ  Bɘ  Bɘ  Bɘ  Bɘ  Bɘ  Bɘ  Bɘ  BCAL`` DGGu([[[:WNoyvcL @Z-+W.Pw^wL9s *Uzhhܸk[ٳg  $>>MuyeQzE3OjՐe իW'..IA &7Mm۞EXtt3g/kӷAMV7^$cE[w‘X)DDvL*AAO䕌mܸΝ;pFQ/(L,rxBl:ɝd YsH(ݨޏaZp$I˒ICrl<:ԫ˙V´߉SW @YՌztE9WT&XWwN臵F?3uw0EJښ2svq?I9yb:Ta9  ^k3S,ݪ#ľAkϬ?3^ވθF_F|ւuq$hBRRcK o=oPʻ3Ʒٟq-@ooĬMAnTc6ƠNΩ˅qHWK|#_yqbu8M6Mƕ|!#W)pA%s*7^iWltNLʻl/e7TrhR(Ǵ׸o2%w}_'s.K'é)\ܦܜ9o=;."29k0uډQr֦YFt"]KZW2@֞o6&86Mд̽&NUoMc[woe>GHc)YM;f>(;Ϟɼd,{_7\e 6\4t)Ǯ\b(|6e2` '%v O=xf$c 2tU0Bw2dC6; aȐ!v _K؃"YIٹg㧈)*V.[)oZ}'3|Yh.3gJZcHr ?&e/, @N_aym/ ?7JnUr,ވTƵޭ˩QX\ i=zOq2 5ս,+c~%ˣh;a:9T?_.5ڵ$y4kDsI0̪A26@*LָvMnՙ&I;z:HDm#ĝY;u(oqZ I/-A],ݓoBuՃ*:Tr2I)`fa a-0#g?gd ,]qt,Ihd eZz&` sKLSnxjSGʖsE[jM|)lV@j%91kv'=$&AxQ llRSHɮqElygR/ۚdMI>.ovKav{q7Yy:t2c&Lr%`OHMXD:a;|h-Mc-fiȉa&:RLrR sl`֊(lHII;gѫgLpߩ?_.bh2IKs˸XWt mgMdpoH#>_7Fmf99̱0^ƖXIdla:ii`t*Ƅ[ W = Ì%c'i<UT檹z1=MnY Zw1vtJH23C&F_*($#e’]1u :>Htga ,}Bӡ200-O~8;5? h_ ;dEȭd0Mq,5-x[@D C&5c$;2^7m s}Ņ89I~>'}_[߰ IQ9QhU *Hx`JT.m|9͛7e˖<СC'!Iۥr/YRel0K+?װ~XczOՔFTmc.gw&[Ѓkz ́3 gNM~QxD)K'z &2m16%tv?ws7 =r`}܈C$.e9w[wBڷ;P<з!t>u/K'Lз9c;ܽu-Zo5Je/s8p-aϩh7-ITs܊""炓 3i€^l:qKjʀO7ɭ\yVki6Btbl.=U b+%QuLfzja$cj w۠ d狰_0&#= :4vs(a9r+iҲ&F?f2?;73u6lɳ{9:СC'|*΍ '>M7I$$ǹWoEΌCZQY}mJR2btRpS>c$'!YdcHՁ.9Idf-L95wWf-[#ƍ&p|jLyε* jѷC1]݋^Uܟ\}uq\;}Pl%c݌`ܺ%H]e)Yv `I}skQ=},aBpg/5y>ݪƣB&VNaJ9kKeTh؇i;(?n F1W-ʔ;,!(ӂ60I֠t2nHgx Z|=ĮjKr?zAe>i ;RkW_I/0O(sS>'򙢖LL8Y即Ҽa*NȟU)gFգ\9k}ZU___|}}='$޸Q(ZBoݻڵkiyt~rr7!"ɰ%#'ad2F$B Ssޝs -XA1 МDWsV|M2iB) q{ӣG k׮M6O|\WMB`ǎzq^|ּUV^v7f+\_}ӗo܁jФUs8$X^FLJ*4knAYx(9cAI^IXqɘ:I  iǛy+LBP(tٽpL;QWY`lI!Au3nE/ҡTRE1[[[.] yՃNҥKھQ1 BA:zC%^ԌpEΟ?OLL+_Q1 BAlmm9s 5k,P IN#((H$cSʕ:'cL ooo8pE#vUis*)*U #z rm}AA+  !  !  !  !  !xݻ:AAgy@˖yiRիW)_/ucovBqgϞ͔  EJojƊ˛$dovPD͘  B5c voK۲BLFFs:zslM%y[Cxoǂ{mYFz ~M_{AQ8_ IDAT=ҫ^D3[K_[b2胨_wMU4r8(ey4+k^qtKVT< @͉*a2ťRrJf0.OhRM0Qa=#i[te׉IMЍxt-@xï4}2a9u) 6ڧK&]G澡#k&3'nn)|[Z=pG4 aJ v$:tdZM#@ǝe?OhX lU>$yL_[Kq,{T2Ӛ9"ҵP-^freZԬ@);ՍI[C-u=pʱ4u}ɪ)Ys_F&X6ZL V#Tpk s\ 'Vフ߷Gu_:e[7Г]/En|+3{~S]UJYaeWFt0de.-Hc]?LvغUe\JyJ(EqO Ț+,88ͼ,{hC_ӬR ,rvl ˪R=TVUj^vUA㑓X2puī~?w/J&_|ؼr! =þ3׈RUU8']`AL/ Yif [iE\X_/YzVOTS\`=)]QtxV`N2$Qw5@EEtYыi/c\ ʦptzT=ַj'*FWӄͿ즆 #jկ#6*,43ǾdvAKL.7/hCOU}x]Ob|7zξHfuDNs(ރ6]c}gm-rNb_-M=M<,L.5.G'N`p}ٌi84#=b'M|[SG)xΫ|3~?m禪2Mj}pwmr;hm|i`ЛfJZ thYӢVdZ-`I4a>Jjt2`PqhV-Y?VuY EVd_ iz tjԹ:=*Ž9}J4hn˂t<6`̡l;p(A#_R4q"c<,_^$ck&Yj$Ձ@e1|axP랫3S8(]+m#Ləa60҆\pレS&tuoُUꌒ "kùA6(Gڶh  GZ%EESw~{I6əMb*KǰJẈh{䘎$AFN&8jp2(k1hKl)N_Y˜mj&G.KH$ 2 % H y꣪T ¸NY{E=˲TDߋ&0dMi/ sbZnhcӐ%rrvMooeekSEY'"CFY[ 0Re/="\pW N!%UKq/Bo1}.|;<ٴ$Yrvu|:!ahQ`ko S taUxL=!$FfRꌥB&I3Pr,N`h|d= D09ʻ|V8j,qNu8%pZ9S;A/7$cPá]wY4k[ Ҡ!G..2NX&HS*6c蚰]%F.h Yԏ߅Q4i @Dy3 Veg9QUz,fDco~= rj$>Cyhh4x{Eȇ`>i=ŃmWws{3Si?]ï1fEjyZĺR_T[v%'IeijZӷ &I$J @FcfBx>!z~2ܕ:FW˯?3>#^% RN P9 _9=Z^NgOnS޵숖Q5)tD,F6zٙ]+wyf 2f/Q>ѓ( ^Jٲe_pIiC%07Uv>v_1Cx9xVK (~A!'OҪUBM,y R76ǏH"R-;gz9E B^&ӧbIQ1#385#YO~&.i}l(:,$~?y /Hô'፧T*QoomA$c.Ax"##:mA(,,, 5ЛdK.vÆ F#«tqիWae;8:~8 EGAA(fL)>M;qL7BIHU4¡5мm'|xb..AA3z;Axfr"g1il6bJm_aY=MUZO=7ɘ ³Edֻ~|̝Z-}mU %q5Ȱڳ-Ozu vlg-͂ /HAxd/CYz-L=y㯙9?mndqLSi2͠yxP5қo⻤Axtݻ,A?=5ˡBߙ[8{p]< ^cM5E.7IA u5Fe?k[W-'u >h&d9Bxn EHoV3&vfPWg9MgDqxtUs OZY.gZPaG½nP踽xĂU]K9}dF|s ^_V?6łD|=_U/g9_͟ o2{BML Na\~pcEڽgH+O^=VH>!ORqۖ_MϹ[)i"VҢt˄a> dp66!aЉ ,DE;ɑcYQS_q ^դg +7wS>bϤnރcY:h¨|>}?vAsҰ6AC2Ϲ’3C7W*ԙ)}'T6}I+)-28m71:%%+D>x ޣۂfXwGRδ1{fޏv^xLܙLkaw=:}ʼhդ%ޒ~_Lvtak[7ʣBM2vlL,K&fD&SS0TnܒYՅ>8?$s/t*I̳ІapDQ> '9/cڿ_K+v o-MΜN2~d5PRD>md ֡k0.jbuϝ溦U_Z( u\a߯L#!n8ZDN'!NMYgKF|(gVNfx7_cfבwͣ !9 >!ؿňyE:G6"`U0,xG#"~ۧt$ ( K3LJJ<=Vzd!@P(kYEuG.@u6[aZP\d׀1OeBS`@Qkػ?v={ܳK?P66a/&_FN|%`zSbMZao0-nrqJdUG*;2ū\}"6Qgѷf8W[9LzpX:m קF85-oofB1½8K ŅC_ c#3`YDV<}L}RLz6C&lzUws'Fn{JM2ރ끵qx:|91i`>?ӵjU)N1{ԜϦͰѧW^r*))2(̱xRzNAmNj4eA]I\ҒI}]7 ݆s=d'8ĪYKs|jgmaB`ͷ|<^ȠsH꽌ہ,Ʊ|FN-ooGpNA̱?⩻|$̯u3Nc2Cr\4 P?}썦WIIK#-~ CXdnYNat] Fo1F䠓e>/4{;i{ ^T}*4!Y}Bl-jRRMhSSx[qn%al,Azz^]a/gLGDsb˙Xw pu/K ~:n%1sD=\=hh$bټdpO,L02sīn53Z3Į'.xy8 [ƒ|>ˏ*N#LWDBq>O}wøQ^Y6dA51,; S)C{qznd0O_Ca*zU(A3.&Ozνfl3|ٮ3!z0Wd݂O  HdGAP/NAHyBT5i.VGb||\e.[v o-/T2CrլcV)QD>CIٟ}a!76l obr$**{ɘҫ9Ja&ސ l; HN8Fr+FΎ)餧A:]yҥp5).N3fmYt2231(ph ^#x'.c!|_`\Jdɲ0\Rvdʡ7 ;Ɇ*+ջ\;͋hս+,ݍ%-?+?:n2RupEFȽfΘB&_cZ2 {77 V_s(y 9 |3%E "1]}ɦ<,AsW icKxOi>(&S.m&ȽޜĆx@#y| /^@%Хf=m$Q_7yGBd+ Ƅ"}tj;~,xpnt.n%{r;"&"os>|?nuJS,;έ>l6_˺9fΘB: 7n8|| N2IٙtjtOa̦6G!DF霫3`AKv.̝FLF#AgD թ۸ K !̊cFCrl=ktP DݻOd# 8`%їŒM0 !DZ4V88Bt2`lJE9B 9)Bd |}}ӝvT^= s#J9)"72,m!B$B!Ff3L)K[!"71!D.8yBgI0&ȱb.mv)]}bBs$ÔBp'd'-iRa2V++ ! !rkl UJנ]%Ŗ,Ҩw1[i1kWU!xl1T7!x)FѸ\9\0[3_yS?5]b(I2o1`A.mgKօl1MZa!:yEv0JTXV5#mF._j,5y~. `c [:Crj}a.ְΎp2O!!snI`,T"qYxP>˯UV L3X"Qs'Sl.pA]_9~~T9eW[|aǀܰ|uI#;?ϡ7)>_?Pxl-M&li__fQ~ KʪKCi~ڞry^4ߚ*csvwH=͇c k-`_B\8)Ž-+S$+ܔ!>[dD!+6@WlT):Ufou*vzMfO8}'f#su 2!41=_|oa)sɶ)nέiB=m>JzO}ʀK[ּOi4.8fӓ4˝+t+h$`*nKjVYwL9uFǥ;m8!\OTDBw/n~+jlÝ ~L؟FOP9 m`׋q!=*24)09I`0/t ~ *w~ly՛9" ooy7p؛7Rou5saxxOL]D7p֝r(NjEȣ; ]vX~lC!~؍gj&9[['J5oA2M<'8q"v mԉwmrc2kWXTLLE̯q(Ǹcw:38$ݓ(lF2䷵ƙr#샖q։RM'ם5ۻѩ7Nvo>[Nل_yiV,%TNWMut(Vg .<04֠XRq) 3\X[L.*R^[yZ~2,)r R{ P7 d,݆̍kŀ%lgWwOȟ%6U=w0-v}2U%2ge[__ &6_߆rq*NɅYƶxuw njgӥ|;-XyvhM4&~ w7Ie,Qon,Gil:Cϳu|1voɘpny6Ww׹R4jA؆HYziu>h](D* gвJ%*UJU-FB/ NƌVt9:U@= )bC/s;>Q[!6",Ѳ=?hę~ٳk(}Y"‰0 Oo<=qh1wb U\XKl^myM{86QGד\B¸%]câhv\ڵۅ94XUNry hY{vS{0)Y>U"ÉT: z]&ypɫR]7j^ZʝkuYt*N넛5vwK 1f3]U2Pq &TD^͌g8e H~֮?KpyORz?ƗCf7 2t>JuFUKax@flԽ_')憛nnE45]4xwꗴsX\|j_,OY}ɀѼa!)LwsWi0r3y>m?c[pF7M_p|dJx0ul8k{LC(M,SMR(TƏ5PS=&bb`akkSks~\_bzQb1yd1vDRuRmZƇVГi_ދr~ okGUx͏ci/QϘ(\4𝰖w}"8b,^+CI;|C9wd /cvd^Bf !ĿeUc\Tghp,ߙ#`n^/B"fIϘ"shzG>4gn>=obƎDBl1!\z<|χ/pxp.wW !īd$B+{߰M%0Cf"K,XLa&3B!}&K B!r ߟY!īSR!D1`7iׯ_/^ SV)"72,m!B$B8* <@<-0aJYB) Xګ }֙ب˪S !D*gLsHLlG570-c#M=!9`LCKOqK=8Y K!̗cp!SD@k 6>YGnndp۱Ca&Beq<&Kt: B#*=^B˅#>8v:kA)-gL.oyBgM0fT ׏#HM^wۜ(kjR ͉6k??܏eX{Jɝ3sYp2oPk|4хR*&0ez$gr:cǦѢ$y(ZKNBOK:vjIw]vg& ߙGAbL>jcMqԽt)I /+οlG.⃖Tr"Ogh;xo$|yA/sStuƔG &vUrΘW3@^ə .qn}$!3 Y_YW>C_>ܰ&*5ިWߟ4?݊{>d73=%BbCؙR8F޽iV"&{rdζ\8o dOoOnHտȽ;߷Eood/1jI|y 6~4۷˶ (}?W7N^kXľ8cWݫϋE/s5+< ߢ۱nTb< OpJa')a/s,Db6أ1)[*s  bqddi țru5eoŝE]tc+Ʊ080#kRv~*R^[yZ~i* +}bԝzTnzׅ5-XqYac8ۢwjimxal}|a7^"F _e)H 8x 8N(noO25xᛔͺЬOY.ׯ1{Qb}:v{ڤ|?8oyF{E>J-G!G=?D"ӹZmp*C'EHIW*vGб39[['J5oAI@u{>5h4Tt CŸu/Hm!_|F7u7;n?._ M0x6J>tZi]~٩e 'ߠF) @CZ#Yy27oaI;|o2;yfth|2'~g_O9$p駮KtMD~=qSk_Gi'S > 8aSpx uì#ކГOۻ5IJ_zp"ZGn ]dI";wޢj5W~rlV5N#2M yKE\Rv7[V෬ޜ1,?JSzgN~09#n {Y=< j7l_[2fwLrdTGmfx+ wڳq&}2BиdSD,SO?%mǐ5r4K:X\l1b@a~LjM|ųmhiݜɛǕ7F̰4sM ct(_ԙѰo^ & 'ʌ5K{PЫ<쓃#7V'Q ]%(q&Tv>yCq{ “>9=~?3%<aHM.URXU|.#wT #ރ36rp3S~ 'O؝`,n7 d zGn(khѼGmƫrUZ6uRʇӺRՙ-2@"2NKcK!+6M~< e%{'X #LjX ʝ9L\_m2c|s^/Rϲ(Am^< I17kƔrLnҟv #)Ҏ#Xd;Q_iխ!y}X$5(S? v7hz,++~(&[(kXv,Dne63+c{y|6wcֳbOl)NNq`0Kȓ:_Ơ@Bl=Jm 5R E9;Ή'l^Qe M[/=ZBCb^uOE4*&,E,LaDH>G?g2w npX816xPQGPu$(DKvm(r`-2pu:ξюoq3nCmm'{eA =oO^Wǧ]0Ian0bVuC&z)F..d$$(<|VOb h\'65gr IDATG[0!($Mpi40<čH$v,Dne6a4]pusg͠w4s2p+'"0<{FS1ӺWnR+hzLv[N;8Q(MmNG-s]A\%k(®\%Ճ8-.D*2H ;A[ď!-DcK$Q* \UxkЙ.b.VHn)M}\3/(8O'Zt]a.4d;#jdilh4sz( : z]N6k7)X,ctʿ`"I+@넛5v|o%ޮi\Dg.!4*'+r-BVf=2aݜLì?,ޡYu )q-fϒ|[?gHyT]x5rR6.oR ϩ*'jgR*.@BwW:Ke[SU[.Rvʾ07A.r+FJLڏV|H*WVUԮ[GMakUgKUꣃ*>inPc[VPUUg?9x`ZV@Zy^9LΎ)C2J,lSWTQ)ic*V~⏎Ue[Okt `WK4PCS?5^_UvS.VhDy>=ex&WJ)e Yk ئL:C/UxecalJ?UTbj2U;&9Z) [WUi ɺRŜUˆ5Tmrv>~q,T[lɒfժU鎭”&,,,;۷oUVJnݺtݾ};իWOo6ضmm۶Mwz!y=1l/YCr!?4h ]3׬[.3L&rz8YY`aa~kx6/k1XFBWl1!x96Tp)<њF*i1eL7i{'\l85*~~^3+OU*lmLi{N=xhϞɓiG> &Tt0gO%9Lt=I?sl' 'VlMCU4vGHP>emKG1!O_y1jLH3 M0zf3E3y-8;&Һ0fn _"K*lfuEQ4m7$ "4.熽6PTE֙г: aT~IǵzMCr6'O3XjhuV9r!;r?k5PO2TV΢`k4y486MC:5~O'Q!#flu#kf)4+p}9[\gq䧦ĭ3h 1p~g,i-q}w&[Xq'V/"|i,&<ŒYFk %Ƴjql]Mph(R [S 4v)bϧ+1}Q*fa h<=νPE-]UR{7S*`?p/]*L>-is(4JHOk|޽ʡ%uMyeBff^1#0n?f_S X6;3~MSŽ wڳq&}2BSf\$x|߀_5)yүuE܉%AIG h1plGJX%DP>cRFR"w-m›e(&V9/zD{SQ z Bl\, rIW ~X܋ӱ~ẍ́݌芽ҍ[I,kk |NFcҤ+AUO]TFqvvdz̛8{}O3ׄer ThL06+M[k+MQ57!D&K ~ϠP.~ʢޣ3,E͡&1AQ m۽qٝИCi0+9=~JDף-!^%URJTҎ `aJkAӱjG|ޙW0q߲zsh(O'(Kp$+s$SolZ֚ŘXbcYxF#0ήY0}-xwn}s791OpޫƎ7ͭO|q Uބȅ&t0 I[~4[]>'M e2>`WL+#~Êt86QGד\B¸ ov ޠqIxK)!Di;ZyaRbxVĔ́:S7 ~ԍ?́oz=z5ɵ@G'T cwk :T?N~;J\ /vx"FOr@Ǽ$t7]R͝OM½k-Af^3NBd& gZ[ x8*o)z퇯eI~I넛5vG\\qq $lش:QUv빝 O3R[ӛ>`-NnL6 .DGGGt;hoVg}`{5aĜ :ϭ#_놌ٛ#o/0`A~2N:|'0Srъ7'S'ctUVӣ[0Y6鹴6i~ Kl[^6aτO~̘^'UR4ڄG2 ULxSہ#~:jU'ޡ}(av>VP h(_miDeW(b6!qR|`$<AQ0c'S$V0{W4VNxt#;^=Ѩ齲:3)Р̤l'rʋΖx<!Lpsǣ3a& -G|8РŃ#[fބg6aARbCq'Bptƒؾ1贮uOA3f@5gMy0XcžM:3ƴ鿙 * lPfnJVʣwSP>odM14XK^ZǩX~;6+OLv2 uQ%[{v/h>͢ S>5 Wޢcnk#Exp_GoRЦ+pބeڋ5G9[Iz7ΆE%y'Pg{ߙ}/PB>H=p64Du9=0-B,f ! %fBBLȊs˂ˏؽlkM0i=cB!!f !D[,IJ·Bd B̦gL)BM0f4_H!"1`߽\!lg(D*{8C}&MwSz,̍"+{8CȜR4B!D6`L!"0,m!BHzƄB! !D7ӏY؃ohkuBԘM0&ÔB!&& c<l1!x!*s)=2os (c6X[,hZ;h-0j&U2b)[Uá*S9g%t/Rn.4zٓ4IJSt~U}]!ȡؓǕ|5p? >[y~8<NMj &/KdIcb,Hq}9>fO%Bd?fO5OZꋝvK"c)cH|KN #,,ۿ_Mظ7ħDEptNov։RG[T8@\4c#wqhװnݺmbU-NoW'U-(כykҬۓ.teEߛR)ȁ1a|8Xcmm=g/C.3 &Ό &Ž wڳq&}2BSf\$x|߀_5)yүuE܉%AձJW~48#%NqD])=Wn.ȡ4xaK|}fY/z cFU΋5vgc Ҽ< JϴRk?w>TnCPHAsh4VzqD)w.Oz kU{9"v, }ze^@nj)+{4hڵ cd|{B񦒷 ǧ;F!v9Wu#6g_' yE7w#NCsP|xP ҈]#;=4廭Q$C\vJ;| E%ŒO$Z]MX9UҍWI;g.`breQupOu!x GYV\l4>3\e=BՓ1Kܫ@7 3zRLip0Jr`TC.D홴5)y#foXGgf)Px>qkxQF7f{HEaZʏ}Xmp_{g0x]?" l ]{i]ޢ0C3+gc_Ml_n.MG!+Y!c'8l=|h1q_B TGfQ{&N]˷͔Q!cz|$m@al5XYf+o@o沋K;ly8չQoR|N˪ܾv,WB3c6X(\J߭q͜e3ؗrsEJͳ=>}^(ON$ I !D0`L!^(0bBV;<3O,u&˴1!B Ƅ"M֭XebYvC!,m!BMϘ S !"72`h̔ !BM0^.Bd63?r"xr=S!>f;^zFs=S!DndN )yR!"I0&BfRB!Dn$=cB!lzƄ"M,A׷`eͺK!Dj&aJ!D 1TCBl`6Be7B^Nz9igB1`,z-4-Fe*ev1I,CydLR%Tʩ=#(˂ _Y^:c7BSo2x{ b΂@ G4+YZΥPb7+'Z쎊XM)1+VL|1iYu_6IpK$l:\EI>olʿJ Fl¬`I烋:+;'|ecQ IDATGEf_9ʫ7U>&ۘ^#:Ӱz"GP=ɓ'\|@S)M`ΞJNs6{Q1[e96MW؝2#@bfu:/U&ŀW׸3 U֖ظ=xuBd ƔROn1l}ZV/EA8d!R 7 HZ(*>*φTȣ>E Mz{϶ $@.}sNr2{Y}5oL#:=?rk\Ԫx,tiGcc+ZΠpP׈"-ĝa0[wMgK^%|ߣ<'=`2>eIJ&ק[r6źZo~Zz> i2FvƄ)3g\Zvz_Ÿ`ꇿyP|.oR __\||;u~׉bN~ΰ;^f]B.O34^쁯Ngyuz"s]mV-֒nFmcЊuqW 0n/0XԚG\JhРscfLl;;_,E{cZzƏyJNZqK {c n{~wlO:>PI=xV=::+I&*-oKM4y;bg2P٢ !jü+VĎw&]m$ؘ@o#sz.-@Ї `{NgG9>:` \Y@"ιŰLj.LxiM %{(yiazqCA ܏ZFޠJr ]{K:zzG7ZF JlwϠ xF!R߅QI4iכFؙc71]F7ɔKh'_كZFzEqe%]k+F((P/̱J']Fvku eƦҰej.(s˗mGEQpm. 2M0aviLN׺u`kHqРn^~[%/Ңe(~>B8zM| ~>mB֡J,>%~ߌ_lp0Nըδoߞ峣۷Eğ۟NxY;ӅZ2v6L|DVsl~ H+*bv1y9 1np*Le͙1P%qˡ}o з`<]8!r8̟jjb=z{/߱LXL)x1;-^רī;?[\!̏Zng\_vle S\T.B"hshWrns{7~ʮ\cWh8D[*˛&'%:F0{՘/ڪomFC%eͳh3% Qst=^x{ӻu6/Wкbƈ~ڗ[ʹҥnE'>{cϚt';o. `$Lޢ{I2bY~62(ĕpuA!+ DYQ\lj璑8͸tzݯNpWcMm\X_n~c 3jī]zx`,s^IZ ZB}3ېsȎ9N|>@sk0wc??MbGO*n1yؕ:L >)IG30iQzIq"T~耂tϾ^(tyg1c2K!jӋLx'瑲c>ݍoz<QVOh`!s.[0PC=5chgP?7䶙UT>֞:rox)[]h6nx'nጜyCB}!jŲ.Ι콂/Wh1غ3Vti˼ i~//JZ/VqV_ 4:.05K!5ǖpQh"뗮db,$WrzPr41m!߻9&[} gjٔաPȳϦ4Y:X Zltz7+3%ly;s5勤3 s>?g.߽#,;…/ʩӏX~Sʾp^:#QU.Ä{PckyfjgSNw3p,ܴd]~}aNVF2VY*/(5a˜BTʲ׺Zm- . !aX !B\E& !E aQ"{C!j,m!Bq=r1B!a˜fwB!8 cp!BÄ[Vk%B+ZVCa?\+V`Vc& Ol1aE]}jh8{1\J}q=Ƅk~r~xU0g$.n K[!BQˏ8W9-h_WB0aLq4& aLq5 ݹaY'!0 c2L)uufSh%& !D/;/5W? m(0azƜrO^ U-b|Zu=|Њ1> :578Ư[rI34bbskŊ$hB0a&^:h `\aoRO{}gѸ.< ^wv;$ܗ|>^O=C>~}JΝwGpVo!mC66m<疺|Ǐl鼱̰Ajq&3'r68frrṞejj&RcMBԀk2O] JjV1`yyhlE{ Xz~u兗y .ޘd%aR4~4,Ь14@0hF=T7JM^qd0bV+*}Zq_A(}`#{x+Z ǹwSV4h륣(61˒IT!jÄaJyi9i ^7=ӹ&П>d]h85Xi%a?.Oyˤ n$ȣ'JZF>vG1; %|RS޼>/t}'f Q_{*iVOaE OOGpĄI#;4+ie3C+XO׹=qR9;0 qp0VBioQ5\ʶ=qlц>.NO O=:S9;}S8[{Sߠĕ /xq>Ka'#}{D0NVNNfvkDNбMif{̺W9eDL#)⍑D|bSx\f-T` L$WCcbL:_hCCI\4 ڗř :`G֓=YDH HPVl=^4v#8+8_th]]T* J$GGwשӛSyD1!J9L;3ߧ |ѫd7.[)Z8np0S`~/bZ_x}fِQQ,MᆛF>e?[rE%A^?~yѮ{s^g}l7Ы12zY kސe!R/"7zUR;4+YD÷'͙y thl1;Bpg(SU 3g>(u;zy#~*pdP\7+KlwJPLmI2ysrW\<ŝYėf:Xp fĭF\oPySSx :xg0’9:];㜞<S4V| LN qs0VMzulxbgƬpriP8kg`V3Q%͉Lec?"(Vn8IJiyz@/-}r'lu&Šl]d+`bdz O*ߜwט[`:z' )sI5~6Bǥ&sQ ߚP bQ$Ez%]8hn0 )rڪӞ||YŸf be(att 3&{avQ\"kb̍xv`}t%>rM?Ǚi5(74}_NeL^@U| Sy(j%7/Yp$go&c&zO6vf % qÄ1)Z*-:=ojI!ǓA=#r_( nKYa@]9,P?BgZ*h3K`_Iol s֥PsI@pnSwWH3Wz(`tazo̤T\Nwϙ M$Zi7vF􊂻B}{^8ˋ9,˷Q&JmgAA1 ]tKX*ҋsi,6s9iZ#ga1 s[r$|QNbKe8L;_[l@cDz=ޮΨNz} sr446Et8\vG73  )iVzW[30N,ao0/Κ/G,|rS8sn22W]B=KJՉ@#n El9ɓ™xzpJ57}_(u͝&V= b‘\ YQ2dP`aƞS C6BB2:grjv¸'PLPQ0Tsd!:59fR*,&ey0V]MڭV3s5T0J+fYqs0v^ϗ[w;/W fWġX&J ~ؘ@^M3Ϊb~-s1u2٠̔NyInɫ7e1zc.[:LecLS"?/;Y9y:O!Alyؓ;]3:_bK3~f%%=胑 re#y9mBϥ*?/ݧ G\A5'f_97ݖtnЄ8L9g7 QR weள{sHD]J>xGl6Mgt)PsceV˦GХo[Q)`BVÖnʲϤSʩ+ryj|+5/_B֡<ˈFnn[gq+ZK[TNN]uxYCsJK]"{co&gc*qr1 cJ݇Y@>ޫMy9sMD6^f Qr)hY.^jP)=_]YyA4Rby-&WSNF3dyێATNL,㾈hq LYr8lټ9s7cRbgkneD٧ c5p K`i vM*}6nRtx9_Pu{X٦;g{{e:& !DtݙQwΑ & jgL!j1!Dmq0&hPYә_L]ܾz(52Bk[AL!j0 MfSlTK1TnԝWT-&QrHQPt%*Z9L*{ڢZi*&٨ Vc-{#}ݒLR s֥/.UX.h9(J5 .|PL33P]97nImbbiM AdÄ[VgݯOGB\NJURRJ=a;F;w..&(HhÄ>}TO-DQ aÆ nM64h777{7庳vZ{7)B!J˜B!IB!#3&g(ޒ[(HB8~nݺUk9sRv0!u֍Nzz:(Bzz:mڴ!==___|}}IOOcǎ7n^jNiKjl{n~.XPMQ]uIf=ySȒBHϘaedd0qDy6n/8q"Ç~`ժ;:k^˂11ԲY1^ފμ߾k];H ,) !V:u1c3f`<3̘1o3f0j(FŌ3h۶mk(~U,y%FTR7nwnؔF>-9=oj"G  m_vypk xy駙2e -ZB|O]OOe6՘ X M(+{XL~xyMyVP5q K( EVI˴_B45x+=`8VA[5p'4YEHB8z?0iҤ>%KT. = ?ο[== K3 sߐ?.r¿Ã~,zh-;,Bg̮8'–Xms7422P `8CWqeξ_ t T;F(~ !j S ! wwFc;G˔,{ >G4:.05K!5Ǟ3퇘v5?Os2&SV3g_X#yuSI<=h:ELL5L.0ԡKD-~ov&1) SiGv_gG݋<N'&r>NVV!Dm1!CחLs{.ܧB=9-gҔ'Z~465?=[F6-\M̞9c9ҢHq d>QtO}dل>>*/p DGGqF&LPnzBs?>{/3|ϧ}#a;nb׳`dֵkܹz뭄VF]>}Tlur˫\/0ivY_5zS'?ǙĄB8]|%W]# !HzƄB!H˜B!IB!# cB!v$vQ+M\V{A؛1!ݸ6wcnp2L)BaGƄB!H)(1+?TGukp{7Q!.1!lV|Hժ'.=`B=ws$ !>;RY8sWp\ܳe0h srṞ5DLqgB8Rw&>÷㝠(':NL$ Љ ; mB !PQb$W`ī4ESm$kAc(S5)C_ v[! nuꊿ^%-#Mu6O ō#Zda6YKa8erE``=mw1]B\1!WِLéӡ/|yp iJږʔw o;H]+ |zՐ☱4@/^aFEQa}L9b`tE03'JDYu]dRHB8p+( 1)>)5eØWx5WR~:wF FENv<4we~3*PUs~ǿbMoM}JjL,Wf9oX/ kN6<7rl%ȂVd!yg<[m]!@7b4:<+axY ٰ43P&7DJ7?+f2HV僓ѡWX3x XF٪BT1!ñay7LSp09;OybN-N=XA,-QFx?,tA~\z6|~䂿Jέ+7ﮋm ]na&߀ߜbkc3͛ʮ Ε] ꬑ&ܑ_gceV&K9~B8g SZ+N!k&D`k1x[Zl!g݀첯[XӚ6ɇ| 5X8S4wOP{&6Q2>,,dN{ŋtl8EQHIsBgd z4{޶LR*ڇrz43+~?WPf>BVTnLd{gIB8&9z'Lk~I?1lz#G£qrTs؝m-<12dj!1}=v2fМ,dX Cm,`NNfDb_ MXtZh&ӒW_㗢g%&)|īz~'UboU|C1B( cB+NQ*ϯ ^N  yL^8N؀PCʯT-> q|y$x; ~5z,]1P`V>)altWHOPdwL`\ȜdjzjH*)nŸA$bv"7t)$(/ ztYIȱЏatƖYK+֬"-&"88R^ǞQ=k1!q n_A-1(אep ^MEgp(fupkGХo[Q)`BVǚQ-,>PĠvv ;7cZp- LY+ryj SWy|}WJh\k"rÌVFۣ#ӫd2h2 =>>N8̜I6.OYeǮ6Հy°e]D}&|͎nylⓕY|r 4^&˩֬4^oq^Wz!D-0&:$iįM7Hs!ޭz]œ@-Fu0!Rx:ElɛǪ6K"C7 cB E !cJnĒq٤ F^dU))1%?΋/ȑ#`˪C%7f{N;.'ٹ?Suwd%V^TqqƄb0fZjŲe.ZE]˗_\Voű8L^,mS_W3c0vevҺg[NduQU%(1!駟8|0~-...޽;/Ň+|윳 x됣 sql$lUWk:[.Z5ywrǼCų5yZ%$ !… 뮻R'|֯__~][~8NϨO|u5<\_|C3zRcבXu!rkV1x3WsKHB8CqM7>|p^浐ׯÇ/RBCC ]w@"+/XׯĵO˜^iEuVzApppejR kK{u[_LM,}^'z4^D=3J%/X56au iTrG=U.`s̘ؕ`pLD?Xr^I],ܶbZ߆48W`>[ ߰DWz!̕ :'FsoдM#B{jɶ#gq,!c!p8:ubÆ KLLW^|̞=|SSK(ÙmN hT_#1>[nq9A4kS΄v1иO[tv7Q,&#l/1[|;4φen[Դ ^q$Z*>N*?#7x#G: 7m.=@\a~yE)½]X9m2?O 96M.pñ0{#Q)ݴHSiP\psă{I(häp(.Ԅ{c żjv"&[Ɗn4m@59OԎM6e„ Uw1.߁{-4iҝgj_rOJoM8y AVϱh}XMf>y5I Fii[x-{eOгI(or\(v}wiN&aL['4kD;"Y6#}^|sq !e<4W T|+XAʪru2dH._e׭[G.] |||U^ aÆ L0² 1[r7ӭ[7<==9u<vo%""ͷ9)DO]|h~볞_j^$D@^ed+/k)%KO4xea4nek׾sҫW/4hsYv-}R˗W^1! a˖-o4lؐw7:ub̜9"f͚U rtپuh|˳lTFLޝeا&} vU|Bd!DeO/z:u:t(v;q򟜜\J0?+dLC_HJB܅5C˜O?ٻ BqIdR!Ž$ !8N.=B!B)W^?{ܹs! ^nIB\Qz///Haaᥭ_4\&Z}K憗뜄1!qwwh4b"rB@ן/ cB+JQprˏBLB!+ cB!v$aL!Ž$ !Bؑ1!B;0&BaGƄB!H˜B!IB!# cB!v$aL!Ž$ !Bؑ1!B;0&BaGƄB!H˜B!IB!# cB!v$aL!Ž$ !Bؑ1!B;0&BaGƄB!H˜B!IB!# cB!v$aL!Ž$ !Bؑ1!B;0&BaGƄB!H˜B!IB!# cB!v$aL!Ž$ !Bؑ1!B;0&BaGƄB!H˜B!IB!# cB!v$aL!Ž$ !BؑpƎ;!B+!X>}!BaJ!B;0&BaGƄB!H˜B!IB!# cB!v$aL!Ž$ !BQ.|ڬ^!Wkaw޵UB!5˜wmU-Bq͐9cB!v$aL!Ž$ !Bؑ1!B;0&BaGƄB!H˜B!IB!j/*8B!9JVVV¹!BkWVg: !Bɜ1!B;0&BaGƄB!H˜B!IB!j3vIȨJMÆ !8yϯK}:c'ODQڷo_gEQFnBk_Ky0e^^R@d^!J/ͥGWk2##MrGEe,6{{t/F!>{Q0˱7EQ! yϯKy-B!aLӴRӗ`D#% UT3EŶZ9VI_6LWB!Dm"l0_Uoanܻ8MpjxJ:J%>q#ork=&{ 5i~tn H{3^~XsSԒU&ߓ*-?unTI/d4u1=% ɄMmB!6.$-l5>gB@.ޙ6nLMǦy _Ш0=j=e׾˜3VSX1͘[дi[z=.=Ӓ)V X7#@~?""I֙: ?g^e qwuŵh9 懘D7 oøE(~^^Eϒ4 #Y:nڅЈ4p?m !1~N)7Mf@%mx5Spͭ_~p SatԮbH~۔EGغ"#K-:y};ສR/S+u*nw&dZ,X?gK--œɺx;N[Dw{Ⱦo)؎<`,`<݋)qX^BV qod$vJ]yaY"ŪmӉvNmPv-Bp,X,XN3g\R§g#7jmMUi T[BѐEgĢeɌ]ΜںwI Cg2'Y2J"ٍcHTOg~\a]'UUe'r(!{"nܱypNܠxވZ{,, ocGh=c٠ƣA˻EMY0f"# [y:MK| +# ngw<:ꊫJ ,c 1lpCl3(*Ԇ&#>Z/cf.QcS{vWlq Xg3Sp$SGrt2l]"1ox(ޭRE㌋+..h+6}1x{1`_ ͎EhkR*kdBU R`ɢ'ȳun'K6x0v@z 33Rާ5m2L8_z *M O# 󺞀ж aН:ƩCY0a!yO#>XeMB7&9;MF?b1}M g6!bdj5WjkB![Å9S;&ѼOXkˁ ,rF9jN6V]Y kŀW򯔼 >ǁҴ9M S`+ִMR)1cm [_MSTb.czp nSiu6/936|ɉ^&ĵ~׳~;UM玚AUE);DW6BN*E׊*5WUz|soRyy )9[w= `M]J])MW8&H=wkjM*.hQQhSqWZx39v*Ous6YŨ"UA0&QrsPu_ rCL^a /\o `VR m! SE2=yi73wM"ĥ4̙03W6VIPt?}S2j{p28oDE}Lΐ9Lů=<6#l9P_yߔ37No4CԨ|+/vvcH "jJrKd-oڃI3.rg,*( zA{W'9=H7l:J?8,N.\"#,B]u>!lT2 $v !\7ƄK7OIi4m[&lKg!Bn ݴ|~)1Զ3{sAsߎ3f0l("{hl GOE!mLj~յF*zԩr-fԩSzGOE!mLj~յFݔ֪)8^O=!9S]0&B!8 疄B!o0&B@ƄB!H˜B!IB!p cB!$aL!$ !B81!B0&B@{22IENDB`Projecteur-0.10/doc/screenshot-settings.png000066400000000000000000001714721451344070600210620ustar00rootroot00000000000000PNG  IHDR2bB(sBIT|d IDATxwTTGR"RDQ{7j,`515Qc]c]cԘ/K4JƆbM(RP>pw{﮳Ν;7Fu #BQi^!j!H%I$B!DyZi!BQT!ܒDF!$2B!($B!D%B!-IdBQnI"#BrK!B[!ܒDF!$2B!($B!DQ244dҤI$%%`222J;rؘ={RZ5:įZQ !^IdxiӦamm$$$yY}}}455III)r]FFF9555̙SzJJICFxx8qqqܿB &ʕ+IJJ25ϟhѢbiG!^ 555Q*899퍳3+V(rQQQ̞=WJb'&&))%uթV)))̟?_F(C455gر,]1c`mmMxxx#wlmm6m萖F\2&&&i&OOOչ|2۶mÇ2c W͛7՟UΝ;zLZZZJLL 3g$---GIIIl޼Y-: "oooVze˖<4"wQ{ׯԔy S^_MMMU/^ի(\ϣQ@$''9tPMLL{jFDD...XXXHdٱlٲlLMM <<<$&3BQrcmm-γlQa^?zTKHH@__ euo>yFELL _5!!!*DYg`` d1K.-QId(iӦ*kooJ\/d=Ö-[M\ TWW'==]=]]] 000P])\RsXKk<|5zY;vjՊvѷo_fϞkBuGV%1Y}ѣG3wbiG!JHHH!!!8991m4RSSqpp\r%LJ͛7svvvӇ(ttt}67nƍDFFbmmٳD&>>SSSƏǏƍ֙JDDL0A5PV䕐ntBTTzzzr(מ?NDDVR,]ѣG/}(!J5kp)133… ,_T՜T CkŊ8q===jԨ[}FF˗/N GTqF"##qtt}} aݝ8^ EA/ ^PzzzԫWjժq 6mT%Dh"ΝmzRRse֎wޑdBf޸ĕ+WVD!xȥ%!JAZX"7o䧟~*pܒ!B[2GF!$2B!($B!D%B!-IdBQnI"#BrK!B[!ܒDF!$2B!($B!D%B!-IdBQn)bׯBQ.ɈB!-IdBQnI"#BrK!B[!ܒDF!$2B!($B!D%B!-IdBQniv[!!$%%W瘙ad`ci"B귖nGz%A$3B!D*T"s!0ڔd}CGObUdddpf[`dbJ{wٽ5 M449|H}0mt]B577ttut dc+m;n%6.mZtM}Xz } =nWKSB!ʷB'2ުG˖-9jMZzzG9sY=g_%G6miܤ1ѳTwؑ'x8&M٫7'L,[Gd;1ܸU+dxΞ?ڶpU[sTV^=z9y?oߡ*Ï?ѠQTsP^6EUnTY  ¼ Uv cHϷw@\\Օe2ՎhҼ%U]ڽ'7nPmKOOgTY NxxcJ1Z!#2ec̚5kP*3xގ.]t$Xd۶m2g,0>>ذaj_55Zhkk)5kн{7  +,V??_xzr16 uU[yRgg7j8r(Legs8~0)))L>w۶c}9F6QS<#Gĸc8s_Ziu]$N?ޭkgQ?~ǎcǣ?77Wœ'OXj5—__'in[.\ ,娅B쫫ԩS}&˖.eiZ:ײի @Xx8NN(^XŅ"[[V$>!Hm4kkkbcذkuBͱXQP%9۵GHMM`߁o.G}lL.~tkkk<==^Z{{qڼe ]{fNhҸ1X4BQ q |}ֵ+7plA ܾ2C~M*;8HIyGP5wm&XTmkik0᷈޽{p?>}NUvW~D^hiiq.^ Ç4m8G}8::x>o(;ӵ{O:vK{EqUձ']>}Jll,\VxB!J@. p!0:CF֧}rcǏp" (ֵ qL>аP6o/f*U8w._"(8H  #66Xu·[w^9|Ͽ]T <>|6E?ل=wgo Vё~OOO||:c.6k_^6}˘qoҫWO#"6|8I8;9addT?~}zXW{3G^OA q\`4_]ճ;?EOOS NۇwڶCGG[͵{1aԫEƍOHr|zFw[k9/>$2*JX_Oҧ6iGmkUݑ%˖R z!( 1Q.8x.])Slq+Z,򓘘HLl,f$%=f܋d׎KmPcϴhѲPM>o>_|TtuuR.~~ 8Mru OƎaz"1)}zn!Vc g֬]KHH(fln nnyֽxRmӧOgqt)YQWWj(yװQR?رsV$22ZfgsZ5rRRRS`hjjbldDժ.|ՆBQ}B%2Ŵ0EEzÇ0x{DEGcllDXd1*Yx(5)hJ%zܿ͘q؁aW:$BЗsj\b3/͚79ma]b-XZZÐJ;,!sdJ6!^?`J; !ŨD'$0/KBf(F"}ye(BA.-Five!EUD߿# H b$?N}eg^Zך_ҧ߀BUPܫs"~i\x3h"s/xx [{G:xӒl[zz:&\z-۶g݆=׸aCzQq _NϞ=qww'%%%߲e_Q*|3>HOիT%R9Oy[׋u-f!˶mFPPƍCKK+re_vw,]o ww7޽O^޼YUS^K[~]S^]4`[(q&1*xyaÛ5@#޾JPӋ#Gu늭#z&U6+W!##C34jc3K5l̹s [ -]Znzn'ECKVӴE+é۠V6v8tΛz&8UƔOǷ fWdgs "QF <==ٳgUTMsU*3x$$<JU7֭gTYwXdqv3l\;R͚ٙѱC{ s>KFMd 4%KnLKhh. %$~ (k߿/K53b*Vү|>?}Cвe N< d~Ȟ;wS2琜>sV-ZdkCv9s1c?ݻٶ=Lxȹs9s샷^Kݗwqa/Ę7np>.]<&F\h٢Fa_IOOzV\ TUŊs;!=kcǍ[L:m.5U)Jz/dB f*D̔Nс6U!y|JVV8;9QҒ>{MjΎ.~L4kﱪh'c`ia)zn~ *M Tihhv礲>mJ]f-3OeX[cUJ!QQbiacʅy(S9}4s!&&P1j _1s46lW-gϯM6?qjժr 2228s,9.7iҘ# ս~]sleqciۦ5ꥱdɭMPp0ٰ~-ի`oa3Tuq3'mϯ];ѷooƌgW֬2ۈkX[OOȼL[[5܌}zckcÕ+Wؼe 3gLATZ6[p"'Ob%XWwԫW؛yokog5J'tL@tuu100 >!s4c۩].f4kyyyjq @Td7nD=CL+P(u/0uՋǛ̃r-;zѝ6Ӥy< Q矓Ȍ3 5(ӧOyCggv*U Mw'0`p9ػw۶EC# kܺ:i˓'ϓNHHKc)pprt,P$&&:]]]~:[7XdSvu'30iV6v<$''>} (^y-_0mʧ8۳QST*y} ICZ{ގ5IbٓDuĖ_hhh丫B[KwZuRBf͜NxԪE~jGxxxvZzUј+LyЦ;gmKmt7JVV,[8,aX4X[BLLlʫevJPm) |;ӭkc_ug o׬0"Bockcd&Ff_kvvq7 / IDAT\01EW:Y?S\ b䨏ء=֕*Чw/|mm8;ϗ 7r]ڽs]{ϯ !u/_6Nl߱C_K^W^˱fnnx w#0'%$''`b.\HXx89xsA~QV?Y iii( նanj9p Q܋TmQ-jz}ի׸ɉ] E%$\s;y=nk21ef 2Q(b CA).^dfiO?|OU EFT*17 2*?A?_>=CX>'}77N-^^ g<{ܚv-իtًvGo[bPsiUx2bNsRJ;QhjhPB6GO'IdJѵ >zH샘--056խCB~˾  ..eCn &-pT$)%ׂȠ۞J6ҥ{Bsiqss-%8ȁJ;Qű@AEKK<=jvH$2ᣇ4oѺȕR,TyϺ;|BNPar" gg'IdterR!2|~߫o.\kw,11HMybNQPe*)+==\v-2\]NXXYS-M+tGf^)?===8zjݻDGGD (F"slgrRdćLmϺ\ Tm{7|Q(SRR4x(Jl\e)ϊ8"zu!!7^={p_l#]|)8zZ!?81ajbRڡ F*3}:9fɛ^ʝRgWwޓzurCp[[[zJ:ubԨQ0oKbR.Ul"6mݻw`(;UEWg2~D^FXb)uQF 臉1̘5ڞu5I2)))L2'l0"_j۶-鑑AժtׯgϞ8991~vFOz_uuuIOOysIMMeOY0s4557&&~Qƞnՙ`<}9Zgdj0۾ v醙+ymwĽVMb8tȟaae`Qf-,ҭG ϜiYZѰq3Ν?' Ͱ̪k<+Wʶi!ۄ֨(z kT^|+kB['#Gadjk<{~M>zX4///փym ogzϳgh԰Gs54i!IRk^hs ~-FFbbbKg^uV/Ǘ!O4jԐsb۟ r-߫O?>|a|Km5_24=vta -܉ Qf:9w<s_tǯjG6C&OٿNV؉Ǐ8~0cm/ӥE3=SyCԽ\]1b2w|~c/{}NisEϞ=`|E]8>+r;y)D~4kY3X~t>}Q| WHLLJ\o=vCgI] 䛯7bnfƵ J%ϟ%խǡC|yyK- uN?BJ7!gΠwTvʇ#GmvZZF+FFDFE+;~Wك>{akkK^}CCV>zѦuk*UcT輸|YW5ݺvӣ6'NRgg'F.\Sl"*UDW׋?Q/ѭk臍 +WQZժX[['GL$m[e9C[[KK\ۋ[chbyEk 335^ddB*U pQ=LDĝN_L"{Hxx8NΪm8Vv <`M’E 6cfoY/MCCC'OILLbU%x +* />m.]y̚5={|eR]Bl<:vl{# ׯ߲{Վ~ѽ{Ӽ;ч0_whfۅgRHLL*_KKK^ڦ%IyܮU>s9? ͵wUxJS>ZFMRG0ICn=Ϟ=s {;[j ' thRlvKЭ[loKp=s7o."ȡL}QV9xПms&Qxx6֕vmնtBqpG]] R B}lcbcV,_>FF|a]JPvC>}:|AN_yn4p߁?aitƝTdŭ۷ T-7b9e}DRSS+Єڬ[TbggᡷTbm]PѼv.p롥ű's.zFFjgε+5d{Xhǣd5},~>޽zF\|Ѧ"oefD&%foyع#Jޙs2?ď?m#44LTQ(8;;ܻw/d-^ʅ Ow攩ӨQ}xϞevcڌ;~(._sȦM ڵk1ccc5ʜ0|j׮СCU02xvv9;;;_ud&NF g'GΞ;R8;;e+bGM=ƂyoHnt2gP[$%er 4?{\(n@~oT7oqVfܵnb #"" /\Dޢo3\vHN۷zL)CC173vs |HŊWLOOgߡT*12P=r ͞CFTT4*YoT *>>5k0|ptttbpQΝ;>}eKEbbIL4G@7O'OVFQD?sߒ9z?p;'Zlo ƌO70(TG{\O\ف_~:۽˚U+Xl#G{~}1ڳ|_ :e2U8>&:+WPۻ#_z6 b'x7?3Gʱ4A݉GFw^wGvZ˳N͚5ſ\]&)Iٞ{ϥ"s3|_^u8t`_bɁσ]oaa񣇳=޲\vB 1Q:}?~~iIxu7Ib+UV޽=zv9inG1+W,ϱMMMNNJ<>$bcc153qR -^d$۷XߗzsS\}(.ai)BE`Ӫec^EKrϝ&((̬&L)P]NtmA,wSCh]aRTعu9vP ESxȔ+yr7Ĕ$2B!=+B-˜;%͂x ( 55K]BgJ2ߘ1cHMM%551c^KQ%\$2.Z4YbQP [cCB+ξ`|:uz1Dz,_6:?}%/&)fѣGɊRɣG5{vd )S̮_vU!f;VOAKC?AeB;\K *! ^3>w!WSڡ*z k`G;C B{K K.(Ve/8z:uWyܵ .Yv )vzաWRS(?Cv;vϏ;w`bb\ʕ;w....C:u8p ҫW/v۷/'O,E(^ja 4Jt"(3#27n ##ÇWO藭u006㭷ksQwCs>k@dddaN?ܹ ȸq㰶f9ӢE @]]'''~7?~̼y󰷷gٲe:uB$Jο/,)3fffu\lkه}35lܺ~&M3p{}?3z3O㳹X~9tǗ>>_L֭ٻ_4MjȟMyDXSr:F֥X{HmNT?Gxa 84?*߫Qɀͽ|׷խ_1 o%(Jv6gN`&N IDATwbld}AXX8AA <(GLC $CN ȿXr97s[ d^6<}LDЕ@z#ff9324Q| cYn޼IFF<== ʵ|pp04lؐӴiS]6XYYY(3SSN?{SaczfMlmm:DXXnnnÝe5|Tz~\ UVʹ=f%,, `eiI}k042"9?ֆ ~];{׎NQ*??<<|͛~u`tu_u3j&&)_{'ݏM7b_Ŕ.W6 K#S'ObTRkJkNzKbhh7| [l x+3gر#%KdĈ :#FP`AmƮ](^8gݺuy2>hf+Hf+*Df9}غuH3uQn_ϟ?'4,ǏsL\2V˝2Q*~\2?䗋9ieFmx,G,= k)[B{HbXu*_.=}3Ep&zжȿ߿PjԨ&&&ʣGM2}{IMMeQXYZբ/mYORR>}oןe4ʰXբ eJ];EYGVZ ۶n~YҒըje5yo724,>Ms~M{[ȧٔ>yCÀ& K͙=kF'))yId28xWۈLOd2] >Ӥ2 ߡ1( Ȇ}y_7-XXX0'#!:2BҤ^(}&ihrw)za[?tYCFPVdF]DF}_;MJ;Х %‚gN: C 2I̕WA$D^ز9;I:[$``b.D!8:%6ZVݻ;uҫ$BOѶu [ &_C$3"]wI"c_uBϨSzT*BCCѭ+~HO{y6 MHB|$ѡS:^>&%%Jŭ[ӻk׮[XYbfV8rچro!I2F-JKKCRRy& «xIb=ӧzw B$2B豗2)))\z#GR»cb%B$BLb^\p] 1x$3B$2B蹗I˄ܹs̘1>xP!M] Mb^ԩS̝;Re&111 !k)Vi)bNm˅ZJBVk.<<<ҕ.W7(X  MB#2">BT*]Ncwj{ jԪKLllGӫW/x e͏+ٰG+q!ćG=V4P`A7k36.H^}rNb{/z{n̚3/zqq 2>eqrG˖o߼ӓcǎ1sLLMM駟pvvˋJU*SCFc$Id={Ҿm>fڔI_quqE`[u:6'O2M|2/_eU>I(^ԍ%Jp1N۷n2x`>}ʕ+q$5-066B_əOܵMK̬066x{d<ȉ^$mݓZ8xq<|0]6nRysDdm)\9p`~ ߿ŋyѤq#ʗ+-Nѵ(JAf|Hhբ9?̤QY?f̙8BrrA&e-@J:}&CLlJX]ǎӫot_Ӻ՜9sgϞXظ~ W/gb8&Lݻ\q:vGRk/h4:yj|}+`oǣ G8y$iJBH"2gx+'~6zuPZz-ǍJeJ,ٹkwѓWޞkӚv\]];z$ԫ[']\/_ImW )vR[;[[f<6j0̝ͦL&L&4?ZM6 w77<''G\]133{ |FT,_K LLLФ2d[lϏB!H"j5Je/1FpaF 2KO(PIIIYC[ӳ(aa/nkk˓'Ox WW֍b 88N]HNN‹/4uZeڷeW>O8pJ(> +򕫴lWI,MlL,*U[_n(TfM(U "(JJ%MMu1׭x,NaSSIf4Id =J¢5R7-kkֽ{7WW (@…<:T* w}..ӋP+6>fffol 7SlF }hptt~D+[XXX Nҥy._~RI…3L544ͅreKSZxyyQP\A! IdXr8be,;hwэYsqܸqc(!TV~͛"111KqjՂx&L"=$.:f;Dr]mLoj p*V(aVs%~3%K@Pб}{6n~;L׶zV:7>?Kii/dR8 B BhÔۿ豀ٽg+?*Ѡ~]&M: i@\]]vbmsfT*۰1>縻1kk7oq݆TY j/5n3`~UӴy+>aܺ}ۧF!8:?m 3¬^fMqL2j5k_ҵק եk^ž(S(U}HKKY&(FCZZrILd. uk H00'!sBC8~8;vxmݔ*Uf4n"|OÇѬ\;c,sLކQnnjj*=uBBa^c##JeIBFZyb" yPmV?n_G2 =VPAN _n[[۰v:l(P@!ޓ4ҰϽgI"lzZ8r(nEr,&!r$2yTRRC"l޼`t邡+ݹ{N]ѹSGΟm[6ӶMk̞;(!e4o3sw79\vT*bbb9ywn]gUQZ 8>RUK ۾Oj֮kQJ.OIJJxkͿLKKcڴi3z׶yjOճG:ebbfA29tvJIS(UG7zϓ'q+n}-R͘>cfk~1pЧ11t;Gga9i'hc6}ƫ7 ^I"BݣwkۚGa폴j###T*ju=XÇ`1+ccmM8{\u֭Wh؊jG/g̜M?~jMhhn:?qx1yYN TN7I{VӻgqDдEK[4h@IIIɓ̜1[73.1͙=y}ٰ,Z+Γ̈/G8r(` r׮СS8r+W|/h)bi|,C&}jY(~ڹOWI\<ѪM{ߏHlMqpvg:9&! .̩S>}:111⯓(Zԝ_zK#˗ŅAc}7X|S&N(Ò}DFEа,YcǏVrety 4u7`ñe "mq?}ڴwڵHKKc2ݷ+e˖Y+Y͚6yEJ\#C woɠK~?f7}OQJ͕dGDhoݺ]EL)o/!r$2zř qE4nD]pRu5 ̣?^L6JN:B(J-ZwvFc.^~$uzZ)]ۗӾĉ\ !hִ Nt܉|żh:+W~ƍ2g~'C2o?ٸgnܼIm}gnHaa?!KYEhX}zU?-܏[d,bc #tփͿloFPPLJ>ApM99sׯE=/[r/4׳o!r$2ycFŅad{lJ~ٸn3eDE+ěլYA흭ј'qOXvժM ~_ҼY78!ѹkwoV;Ef( :o/Vt ͟7]u^cըŒo%&:GRr58:F %%2*ҭG/DFҼYS/fSu6`Y`Rv],EQ*:qiq|'89:2md o*]>K"l-=uU.\H ɿwIU;=1h,B^=8~[ѼU077Fj:> !^`?\B7DFO;w>cm[0w6 Pl<Ȫ*:!D>%sdBgI" kTH"#BP "W/)K"#x/$Ôۿ!"ϒDF!y$2B!ȳ$B!D%B!,IdBgI",>׿a{9_~޼%"~NBde_=(^޽z:!L\|1E/beaC9H=׳{W&MNeTX,[lliۦ5'MWqiҸcƢT*i؊9(WQV)W]i)[mΞ9Epu||s)*I"+QLzϱC( Uװo(Q8_}/ZiG=vb8I*/ǕרWcG 4,^}|*|2}QN]i4lCs) 2G&ݫ~~ 1*u~`ǵj考Ǐ͍;дqcVYcei=iצ5':~RN>IM_11{OɈLp<լno&MX#c#ŻDL {^xz ַߧs("gewDF?EXx!FVkKMEs*: wF&3w>J$z-sԀ26;ri)W6 '..uZh_r-8p_oY<~/f!4,6;wҽ[B8F5|pT**Çg(G8{ /]u$2yӦ`eeS'S|ylM߸VVlgN}Ƅ\8sn’Uh+/qmޯ`UL?NAsxb\ƅAܵ;=FQ犽o_:v-FILLؘ{]]C/ܺu)Q+VuH9LhXJBC3l2mWzu:u⽑92/[$/;;;޺Klbάt؁pvM.3j$ƾ}JKwCҭӧNe<~Wall sXtM64lؐ7r)ZjEŊh4/?Փ޽zbueJTN}%#2̐OqFqL")lf-~LE b, @R1iTJV$.N>>!C3 M̘>cfo޺Zfxyy_ɏ=i9p ixDFFұsW.]ֿw%{RRR|2:uҒF7tZzmV6ǒh㯗,N l6٥[@rr2A&YwOƌOjjj. 6IdyB144d=̜=M?ofUݽа0%Pܜ1?!ht}cmmMv8{+icmc0z4 vGTrϝàOp~% A=P֭[ӧOiҤC1;viS&syL/fe3ԛ5s8 `Ƭsή?e6V^/Ғ"P(899bfW3^*_/ZHyJDEEiۼVVӺ&`jbӦ"Y*]sK-m8iF演Yx1aaaڵٳgm6]sbe*Uʇ{U^ kkkRU*scųgIH~>/XHr.P*$O>׬#%% 166Ɔ;wP?o͕!HNIRL?u6o|Y-ÇfzNuB;RIhm,xt󄄄ЧOhڴ)'00P北#2+VәD^jUz'2Bprr̴lƬٸPDI ɏ?̬0Eb4onj7D=+P&&&lظIj&&&(P@g#22ܵ/~qc, @߾ <<<#hڤ1/pRRRزuHLLLwK;wjU`hhc OR>bPT 6Q#LRR eP|y I ^(5W{oPxzzbhhȒ%K֭7oL>]ס嘗L޽"<<<]+={tTGdv݇"#Ku풡ΕWzRNucc#*knн{7>< yy0:WWW0/bNhh(acmnoD}300܌Fx9ꗷb\wk%ؽ"rk'JtvǛ'N"hXZXiIơVڽ'II4n܈ysgɓ:l)חӧi=cuDrJ ̟;;]'NƍLbӾ!omό5FMSyyq~-֖_#G2rH۷/{y#3>hcysKWGD^C##ڷkѣ29z,# hQ&OȨ1[_LB?(XGhݪ%k׮cC~[S<7ľl:X[[hPդVYFB( J%(tLjj*=~;ȹ}iۡx~ٴ2ll0(CRd?iѢ+spfuNٗsB[_n{P((PN"ŋL$r$2B!jg~tTۆ#t@!lluNd'Q*jem_W !x+V=sJaw 2/j+W`eeE!}Ρ{^ۻ7OE 7Wy$2BO\_\ZB!D%B!,IdBgI"#B֬7 \vtoE]!7Ν uCK!' ruNJLuZ!D>!{O5Id"?yKtIdTbbbr*MȵY'BԭE0.\gWZeYn1zxcsӺm{lje뻄.x$BȨ(̚AXm~m+ԥ3gRT Z:G:177MQF eN=DF akk/'xB9wCPTL2>o<ӶOH C3ϘGҢy3~a\%Kо][֭@LL zٍ>=giii4h6LENUB|dAS>ҿo>d0[ʊQǾ"w)DjZyn 5y kk+&D>ʣGqrr|e9.`)C|NBИYsB[_nc!BY!D>C>k!D>ldD\3*cc]%B5q^=]!ǷF5]%3"(KjfmxW]>SַђDF!fMȎ2e(D;26ƷF5Jb@!wVԻ/!ȓB!D%B!,IdBgI"#Bo!IdBXf̀26P(r;lۯBFI>|8* J3ͥ$"_JLLuZb6nwjj*:H|L6ɊFC||DF'tCCCͬ]wF~81{B N޽2O<ɿ8w<]'N m/l ۷ ?/.o۟ ArXuv!hۦ5@Rp!ʗ+S߳[nVn#888Cƍw^֬YÇYh 88+b`FW'''2G=y=ۯyBɑZΞIjUzBV"**J(Ӿ^육 BALlv-...wKVQlullhWX|;vK˿ *N(P( ҨQìIOll,&&&(jnnbf5k֤]v5`oo|K…U?"~b"_ !Dj""`gkKbb"111xyyiˋ{а7egk Lf})22FMmKxXݐ{ޑ;@ϥ{*ڶmÚIIIaϞ4m$t~ŋ\~PjU7nӧ꿪:;/="#2BQ=]?ƆUf6DɑYƅgzܾsT@ HNNQϟ7`go=*^8JaΝcn޼vaeeԩS&66ΝKjj*DFF<Y#2+Vӧ3SjUzgB!x8"## ]rW7OϢۛi_K >>M@JJ [nx{Lw.v*D.14,oogXz&ں>͕W1(`1cU#\]]IKKCPLJJJO{I.ݨ^:ujxyQ(xyy˖8;9!~xxӪe FÄqoرcYb۶m#""nݺШQ#Yheʔa˖-m{{{;\]]iӦ2a>ƎKj(_[&DO^DEEՕ={ JFdҒB8;91~$+IV֯] A:Ѿ][vIpwsce8::2u$A*UY>ԕ8oahH>|͛~u`tuhѪ jȈW0fML(S"6Nٻ4nүbW)]"mqsg.Wk4JE.\t)x{{7߰eK||<Ŋ`TZի P(,Xm۶k./Yn;%' 9ʾ'1Yhnx kk+Lw!DʣGqrr|e|.h۾#KbIeq񤦦2|(,-jїYjӶC,')) [[[N>ۆ+_6ma@ ?: _-PT*ٵOZh}dPmV׫A.- !^iЄ!wiѼ9gȵ$%%1oCpuKl+nެ?YK++ZCH!'||dȥ%!BY!"ϒDF!y$2B!ȳ$B!D%B!,IdBgI"#B<+}{"gBr5IKKF!--Ma!>PT'&hP*_J@J瓦Pԫ.n۟'Ocbҭv+s+"neUK{:uƘAxxܒVB!D^Fd^K4` G.9||GGGBBBhؚ*+ӧwO jXl6`,,퉎i;m[6d7ؙ.SPL=f|B3gqϬ_ K |6}mf:tꂉ1GÇt}6!!s%bcb lӎȨ( RI`v|t)»,ۓ;%O%2Fd̸';rooqtpN;pqm"S\Ylmmh˗}Qvmf~bt%ݾ׭߀F ` ̛W]YvNH}̛;իػ︨?@8Ʊ7K!;ͭY iL̕3-R3ML-ϙ8RAQAY=E9~w|{w2BjHZZ(< ^iS7LNNكϟ'$Mn=FI*ʳBQ Yff&*؏+A UǩuSU??圜HIM 55~_|Rfe 7 -H$MKKg,^usrrHR*]jUb)W#&6VovdeeڐqGoYAh֏bbbh4eu9R!ӴKcGE!R%2vx{y&匌t#/XHLl,9^ڵwe OBBjr2ҹSӦOǯÇ )2_&Wu\ k O򈋏BVoD^^#nB7_yHg%ISs+dxVcȰ$)Ou?h*ɸA[4' ZD"!/?bwF\1j fft(yErssճW\e\&&6ӧߩS[fLƭ$]>t(|ѣǸ[cӥs'\] Y5K/Yh1Jye"""ӧ%za};MN[гGwdg?1??D=LlU[ϟ{ןy,"TwMժ=vw}s9A5ja{[KvV6!iҬJݻ~#G?'Xa#..w?@[!W*7Z]EbUx6'?}oދ}ӺM;|fn7oB.Ӽe+C֘ xAV3gd2'O~z/Ghxk(Q~= PLHP̭lQ?Gȗ_l݆~{޾cΛ7&[h֬)ǿ]d4OqqBnˬs8|5ڎz P7\;GTy'IU&rtJY_x]Gٷw^zU;ܺu#GҴiSy6oLXX&Mbɒ%u nN!Vj֬ɼt?{u489;_zZ_̌Yq|݈A܊[0|hrssYn-;Ҿ][#* g7~]'}(L}qr^^g0x0RRS9w7#y/NT=L@>BXpc}&?nDHH -7|w{..O)I"amyuW)QK66{diiٿ? ήԵ1v}0 .]NpIV޴),\]#G oז&pW#h4R47FM!HHMMؘ?o-{>燱qxUx)|t|}|pvv֕]b}Ѿ][ޝ<׺*σAj) PXXX0| ̙3iѢL4(<=| >^K>ίCC*)gnn))1خmnp9vàX:N8C%&3uT>뉏G,LH`=vnJ"I##B׿BCC +umd23B{12j;bʊvm˯?ء=۷緝ѽ[q88:L`@'MZαO}^ h/d̨EFOeA.?瘨222bٲehѣG6s qFrabo 8W7bO06TGnt*tоbnnN2Mo&--&=vʤ.{s8i2cǎؘOhԨ!7";;DHdAx7oΨQ8p@jcRRvwL2||Yd1tٳӧhݷ?ytؑ--Jƾ=7^/sRn(vjղչR .4hD#k*>ݻ4y 6AWte3oB^ʊOB+QKYYY(Hbׂ T~jZ%#A(CtVrk92FFF.F'Bjh4撔ǧDF`k"cbbywH$d?p/}FAKZJFsD4 Pa}Hd&> T$qAA*-1Zr'F0 ‹#HdA(wb x2IDFr'F0 ‹4###B#U#E"#3!Ax1g_$2  TZL%tyHRѡ;{;mm PAJE$2 /G0pzX9y/TNQ <="d]*m)VOh<AxD"SɈ$ʘ$''C OHd9WAD$2ӗN)ٳ+&-B%"işExzNdRSSuRRR-\1ňHNRVt(#v66Vt(B9ߘ܅ PȃLzFF\Ւ\^ bDVt(EI``@9E%T4IdnyJffԭ]"emQ?V,n)oJFff32eSRӹyKZ OwW6'dHn^2nV1y/Ex ̬,*Uj;w P(-=Z0/ $266ԯ{Xq~^>#.\QUj\<016-%c Zlnn xy!33#:8j5LH"F<~E OV[Xca.+1}h\ 9?t^W* O`## U k4Z"p͵0)((X3J89檻CfV6c;v6q=XcZ )TX[_շL7hšTF 8: ??Z+ X ʝL ;' ?/,-R-QMFFEBօM .NLʼn;h4\H$:s>2\dffOPqĨ%y"gay1hZ|t"VH5?n)IHL@Rq!r%kL+dfeR##fp Y"(j[eF|<)UF`unNVR2X+-\-ukƚȨ+Oq' sY$ ߋPT#33%&&ƘT-V[I u 1B\ ԴtT"!#U}̰HU$NXf89:`og˭[011\R sseےU8Pxa.an.с4lusƆۘ悉 7 ~oNICwZ.I TjT*҇#*u~BQD#~6 HJJ02zx0a0a„Ir@ *Q\Y 7/J<333tj033#7OM1s1jZoY}j5jWq쟓$q7(((l!==J۩XK/ ''W'N蚼TJZ:7̴2Ruccc)ƨXvo`Z ɼ, `i序cϟrϋ$e9tfϙc05u"u+>}䔪tlT"{333\75F%?~biS"e^/c6JRWqz4nP& Q+&{jzSRQ޾.N s:ֻחIdfew3?oGv433%;T1fU} T* T*JxJ<3+ SVvUȴm+{dָ{zj툽O̍8ȸTjx AMT1qKQPPPbv.N܈F~q܈=$TXZ #p Gd234wO$H$pz,x:`kCh3]a"%HB-%LL\&:VrefdfefW/vgтyI||)ULL -=3.p$<ݕظGѐK|bgG{tj wLQp+јpqv@"AƝL.^JA /O+TV[@"߂$`a.3>VP#8rN&/G[>5?@<> >Դ4ݺ+{u @\\]vCn`0(6\6.nL||^P_--, _ ꂻ+nstѡ_ zIhX/|+K B_aYuBvVKNnxZcgkLF@52 O22u^nh Xa#r9AA-llml7a"C`],^Ν:Vaxzzϱ#deg1~/}3~D6o5:b&Oa÷ߐ|6SX[Ա#>]weqxx?'jWS  l~3ӧ`ؐhZz폫+gOTiݗPiO$113fpQ~ i6e֨ _9_~5nnntLf_r- qcҕIQB*5*vO?[NFFϞؘk׮QŤ 'MNN.}. ?c}kWźur`IPuTc`|h5]J/$<-` ̤ɓ>222eάX&_^iҸ˗}Bz$&&R#E遣#{<Ϝa[qr*J&>OX^v tzx|y߻,%(6ڶaтA`dd[-~~>c}gggtvae0uTz-+,'2E,hPXS`b#豿ܱ?[Vdgd%>µHݩ^ݟS&|"LIϳ[s3!er25CBptﻗYVA=y`S\NvNW\%0 xe?6 iI(R;[:23gG9s7|˲O?c_R~ mժV||DG_O~zub r52\'TneILh4҉ar5woQm9tGWҰA}\)JRxݖB 77XM؞4k֔%>Q...|d1"X# 7wv}}['X^~xaz8y3zZN8LF4p@I˗ش{΄`GKiڤ1UTqFDF^ծ*<Kf^EQ1*U0LӦqIAq"y_N^~ˎF7Ï :ڵ?2F%"2*ĨF 5Z;$߾Mtu}{мYS̜qDDD2v:wꈫkn4kڄ!Fp9HII-п߬[O[OT~m'׮Ese}333|}zK~²jͥ5]#Fŋܼy#G^~g!Y[m'&&KdgT*%-=H,52L`P0/\$Rn<~ᅷgϮRΎr<ݙMWm(|EGcl,%F ~޺'''跰Ս{ ֭]?2w233!|MՃ{ӦidFn-iS29p/{M߭`޳O/_ZnE׊_n0i2-[Ç1K;~+ۋX_AAޞuϺG̞oH^~Ç y 9d0~6ܿbǦ$UPtut޽m7o&`oo χ+Xg=\Pرo0MkߡqoeŖӵkriXb%۶́}{*:4۶֥޾8 ҟ-e2 O(? PiTQx51Um̳ jdA"_Qxv[ 52 Bv66:y뿌3 G E_p((<{{{l | 7 HdAbN hZA  V'2m e#> TrMdj8 sN#kg_JEvNZ#p <Z- *:A^P899T& ZZ٩CU A xhIOM/*kW)U6"VhES ׾sn2CIp t9riI_.E$2 B1g9ڿVt(P$^g'N3CTtH e[*V;wE#AqhNR&Ut:"A{G1C$2 HJX]0 /STt~m~qKE!HdA08mw5޼ڭ''OzxڷOi6l#.ntҕϟ8`pY`7bNlЩ ɷoWthz-h) }v'w+;!Wth)6ORR֣s{Va s [j`\(f‡G[xcecOKnm fE~Fkōm{z#u`yY"ޟ>zuR3$ѣF3ܶ=ήkИ_YV*0Ww,\A8až{]ʯӼֽ'v.mߨ [~Z޻-4r[j֮xzBWdZFn ճ^jTEAA7lZ.'(( Ï[71 6BΟb3ɗVjz2;7`cm+o~O'K?S1io߻1lhݏFaz㿝BNдiNqcߢw>|X~ z5+W}ňZ-gN ?4l𿿱(+A:vJp9lseλS8s,~[;[.]c+Phj+;ͧ (&MAnn.sf%_^iҸ˗}Bz$&&GGGyN9ÎTxPXsѮm[&O@^c̤$011:~ں ;{;fDvf͚o]Qʨ8tע"qwwzuN+7-,--pwwO <ʵFfx+_Z$zux/n;wrѕUTs9s+ڴw7Qz2KkT&Tҧ\ݩtU{nF9`c=Eb/\HhԨ!Jԩ[ хn=zqclj^nFӺm{|fvRf\t-&6Vnjj {'Cjm:NM`=O5s<~3Nq÷,3V}999(J׈VZzLmcffKbu"u@n@n=##c㴳CTkR'&&__$Ix5&6SS"J5??bco<6AxQ["s=&#1| ǟ2{\W_"''a#F1\ؾu և45/fU\XX{)΄QEJάX9+V}Ν;EOZ{c,d)ϝ&77&[[LO!3=M?v )=JEGc&3ܙpOȈ떿ٻ/qq9gOֆK/0ɴh֌h>Hu{+\n3>>ѝ^jɡGd8:8pU]+W {)!ڷ3ppa-̈́֬] -]v#77NNdddV~ZGo/X-kЧ߀"\NH`tIJ`bk֡u{hTӣ|h\w7ѼekhɪϿ Iޞw#mАO?[S74#ωlظ ͛C.e2|$Ç1[)Ͻr[/u7ТEbmml ++ RmANxxxмyS򈋏%3QQWSm8kbʻ0|hZ- OOZ4k%535#/h^T1yyQa.jqws#//2Yl 7WW֭]֧͂&æ֗U ۳G.711aά̙5SbY%n_>ۧZ5k2,ch:߻[͚r9#߭_ط01E^$wl=#GСD]®{ػoqpg\zL#R###W7WIIM!(0f͚2r.]d 6T1X9zL Ya#H$Ϛoֱ姭s9*+W!??nzL H$ʟ:^+((@ƍ@\|CjӨI3j׮m%)3'WBjZ5F(nď7!i޲ g+ůV˅  T$1ZA(bDii$')+:889bgcC@@`E#Ah4j֪[ѡl?zX9u8STe#A4^jնmoZWNєHdA"Ds Ri8LjHdAVC#AAxKdF _|Q|('$z CP^ HMMɩC p"3qD 0aq冕ƈD\@VӢ <5'1 eidff+=8+e~b;A&UpTBe`"3g\uә=g̘QHKKNhŽRbs2֭OjZJ~ڶ=2Kk=y[ON:T'6 k;G\< ӏKOukҤ aoC/)JtL&ߟݻwXvɒ%x{{ccckF||nى']6fffhтk׮kh-k֮!6&ؘ+]p'L~'CA0 \|<#,MY2:tB8Whl,Zy 2}TV,_FZj+V}N/ *6_*(@є~3f \xm۶ѽ{wnܸmr۶mcҥ2bFŎ;((([nc&OL߾}9zh.L-/۶R +{k$mXI_%=#Ss+Lͭ;o>#FOׯ`jn͛7HNNf,^&Mׯ/VɓԫWڵk̛oIdd$;w$33 P(O8vgϞ-sLӡj<Q# *O$PPP  *<"~q [Ma#غs-sdffjzsYY0I}3~D6o5:b&Oa÷ߐ|6SX[ˋŷg~$ .l 9Zw#,^[ĉ=f,RQ#G?ʮ]2l۵jU?:Y3ZhN5Sr9ׯ`lĺ_A`bbV͚F!44T7nݺDDD+۱cG>c֯_OYlÆ  "":u`l\憋 ԪUq ODI[ S5id rss3k&6|z-ΧIF,_ B둘 Oqtt9} ;~ުJVV+V}N0ڵ-ݘ5.P_}oAޅ(}333=nKHROٿ:{[Wrr2ݯȗz/ּysz;È#pvvo߾XZZ)_vg쉌a2j6Ax̜>z ;;S7++K ROWjcbũ133+quBB" T*22<6NGGGnݺZF*YT*FAADG_sL23`bbLN^}l,366g֭߰Z{~s@vv6FdddXڵk9{,H$FEǎ9s )_vgagp}dO{qtqFMmV3qҤbWgӦM=Tx8{[;{5j̖~O7ggg||٣;/ԒC ptpիW>RxݖB 77X===5rɺ)Nrclۦ5R6[mӦ!cbqss`1 !U^~󏫫 W^ydffM|4qǟlظ 'g'6פOj022ӺyЖ-[իvvv2{lΞ=Krr29sJ@BBz#<>_^#NkYwa`AZ#)))0? L hܨ!9H$]MϣY?RI6IOKg_pNH]{̈́,~}Vˣ :+ 4>hҸ ^j|M~~>[m^ݺPoo/^{+yӦnݺ1eV^ۉ_~tЁ-[F5غu+={ٙ/OOOܹ3L>1c0e7n,T{#C"117nYAȼ=a]_ºiٲիzn݊+R05sQ 7}OXZQ- erRRIpuWEN"A8až{Vϵ82d`O?Ǐ?ԏqA~>3g΢u<^ 6Tۛ0a҅o֬eTF=i%ߏΞO?!! =-M6چ$+բLJ{ĢEPTGT`ckǜ9s+z'bz_3lpڵkK^y3y4=۠!+W}Mc˦O6{oi/5*q{6oˋ.]Q=(c'F0]:wbKYp15кm{=4Q3U+>㧭h޲5Cpx{{){4kA~xkWL~e,;/^jԱ+ʲO?#fxآpuuex{D5lĆuFIAA}5rJLMM `ժUlݺ;;;iUV`̙4jԈ&M͙3g_H$}vOUF\\}ݣv+kcߛ6+\w*yo"e  bD>^xBNN#Faմo׎ѺBX~d/nbࡘ7-.FDj9s*FF 4f?ӵ+`B~#߮&Lً5 IDATzߗKϽ?_rZh3HIMĄFvjڵkhj9/ E&1HNNСÜ?\^d_:dPx2h@#0gL̚wyRb|yo}t#U&yُ+;MzrsfͤuV7mҘKJ>ۧȨ}k'QT,_XcUZܹ\Ο?O6m/ }ׯ/[gu^nh u 8F)xcllLň  2U˩^ݟ-3udZFKK R<09׿@!ChP>c{w?k2c4jؐY; Eu+`f/G‘Jd2.^@jdD5jDFFrV. jhެN~իٮ8;;GL7M=r+Ubr+#Ak߉prOnn.}m-[K>[ -`kmd0.$HĄ~G[fFR)ZZ.(vg-~f]lJ%>~>r~\)%;7I")yrfbx8Jcc~e;G?fϦ@UMWϏ7\$M ŋ016؄ŋ=P>Ɣ>B֮\dccȻwK*ɐLӒ/ EfŖڐql,,K yӦKf^EQhRn͋5V`Ȑah4h133 //~JO"AɆ ѴibΝ;deeann[WwZzpeo~4z;M6!//xh+WY5ăX(?JQ-, ")9F*`%֧ULL {WE_Cj$I83{5R7t$*#GRN\]qrt$5% ƌc$&&h;+QIloӺ5ڷcVhp#G }y bQT=v:k삣TN&7n '-,Yʘ1[|[sJ}_xv66:yVt(3 qFF NކW1m ZG[q÷L+i#("$02 7_s;sr;6L|9^}}#чعPԷviݛր ŬgȰ'4hPI'D[OKKKZP.-C &6^Wx (q*NN^}a„gyIrD!פISN>#w}4#?h{` j_lQQ8::xM8+Dyjؾ gffo[A@q 7lgۗ^xYaN$)-fD-0.&ӓD)%!ăΈ>>>^={a#jqpp(y:6m̭[ [YYѿcnjFH"sGff&@т(e#2}m۷kꧥaiiix 2lقY3grZjjj955oXOy  ,Xhxlo_U_.1ݼy$"""ߴ֯#ayۗ:P)n=|9x:ggZaٳ\E RU?μy8>;;;ZuJv5k,G~߫8}˖Ylmmpg̘16y2UT)vl9 >UWo[ܹАSZƟɈ#2ʜQWңW\cmg9p_tڝZ::t‘#Grw 7,~hӾ#U-ѫ?Y#Qæ&4!6F+sN9wz^gÆmӆg= Jy ]Jn89ڭ;y1\\\qe_УgOP+/өs=Y -سgO󤊷N55*UD:uۻ7M u~G}|pqqe\JѤi3C-[Q>,F}.δ['pq9J1q$7PoΜ۷\?PLJ2l> JZ`P?&M䌳N/BjJpzR=ԥS.;w. E^X`?o1nI&X[[жMlllhܸqNcFW?_JS94Ln x{w#{@"ΝfҤ Ĺ3aՋ"##ݻn*v+,d@8o/܌gͤ¤7Cb٩s2Үm[yr=gZBBG%0`d?Cɓ(=*8zo>۷G?~?vs ϘOR߈[N+l޲[R17db;JZZo='8xaޠ`Cdxa?a!!1cIOO{nS^^|wښ:uᎭa=;xgqqq9s0JXXX`aa ڵkyIi愅Y(5___\]]ycl۶  0tRl۾  lkŅ_ӧM#v٣ќ;wn"ikԩSs~z;>.Zj֛oW`_2p7o11dee|Jyg:M7Yf3w֡9ۮJ*ww\:oψٿJ)~]kk+̟Gh陝U];cvv;?vv;cF!/ݠݸ1Qr:۷M;ܸ܄ gVCF`9Glmm ͹籶{EF!5_+bƻxi̙OU<][Z5ǒ/yi$ofjQrss…l :A jsӴiN:uK Rrae 'wK))ݧ}OtGM@` ?aLx챎XZZYG||ݺwUԵh"""pprλݯGӪuKڵm]hа]:w[]G͚h'_ ԪU _ߞW߯Zm[Q(jժEDn"99&NbR /E_ȿȷ:3F}tGEՐ${E꫓ 8aľ|8lL[e| Moŗ_ɓ/v*&qvr~zFupۋ*p1O1Ox-[:RUf_KܵG?x(nnnL|BnHdiiiSYgիa9\vn'cVkHp~VN|MNn]Y&nnntԉwNkzt:gb>gWRJ憗Ws3bAlٺOOOzEuXp1QWoB)ū^:R +++n޼55kdyi ##mgk$zhZôujc|$(˖-5F& Eᩂ/AL-[0$2y<-[(<.#b*Rq rp<.^ĕH?^%$$7J*LY<FU=Fff&CRRYL|B|B:uV3g|k~Z;DFF/eV=*bix.aK4 =6dΝDFFrɸ)%õҴIޙ.(cưjlذWq<"" ?-[_01yr`LmWWvl#(x7m?FvXWү@]ϛS^cNj}f6iӉW&wV<#4iRiZz]|t)ͥ_d|-wd,$7ϯtε#2w~&A۶DU;W̙͚5k9q$哏hǴjՆ>>B_[.\\tl;wA.]q/_iBOТTZAi׮mؔR?XըRիӭkWC+k+oD)͢o>Lze[dq\}[a¬ƛoӱS֬]o^{_zYg|Z2O[')RT2.i(;}BBB"IIIz]^')9(ԮgWWpp྽',Dgi5mҘMZeQL9yjȒ/8LL2&GW¼яb1Idʐ#zFcu2B!*[X8::޷NySyD 988~:MjME$h s:4 כoLE2EHH(''..BQL88:ռ}w_o1IdʘWE *c__c*fyC۴=*&!aV\Z&KdbccQ0:"bB!fͥekdTˇB0+zl. &2<rmU][)INNWWW47qUI!ex6QQmۆ l޼IIۓ-6l!)KII[9;Wt(BiO-~ٳP7n;L V^ԩ_=?ֶ\bBqмy3̞]nLKKҲRF2HQQLzC~%))7tTROOO4 111h4Zŋѹl1I"%OH_{GZj֡TrO~Ky7 ߶R< Ŕ*􈌍 }tΛ[o ;v7˯LLylT!4SNɓ8{,nnUמC0W(UhٺuW^ȼ}y%,\4UʰCPdI)KxiCyF ߟ-ZdO~G\\\qxWHMI1{q|{Bs}l޼⧟~~zwY#+388:7oOF +kjXYŕGs:(EB|<Æ=NNܹsl1w R<ń*ҚkIIIA)EݤѭkW~`׮R4o֌ l1I"%jrehIDATNGD/mJ@` +VD)E\\sN>tW_}Oرct֍ׯ}? }~ؑիY Ⱦ7T`PSxO/SQJϹy&''4O~*~~ tԩ·)j1%ZHMMŪJ)bbb[t:gR$$&ʊ"6)"&!D~Ӧ;Wg3xo,lll@u֥nݺ I?k'ۿ͚F^صk~=>_JӦMٽ{7 4 8x7>>>g/zVR {5___ޘ:GD)퍓S4h %(kf|u~km6ab(ccpp/=]Q1 ! 6jHFɓx/?ԻN̡ÇQ(\A0ӳ!GC׏]6l(/]b'jj}8K>η{}-@~'s$%'K/D4jԈŋҺu<- !Lɜq`V-ٿo?-[@0fhRRRǟ3)jhPJqA||| Icd"#u Qb>>޼^}ur' 9F5NvwwM[F\J) W}qx{wz/̆ iOO|Bnnntԉ ̙o2k{<9|"aa! 16F^;  0̟˨0F_oAHX}#?5?Ӥq' 2x>C\̚5kٲe+Jww7{6ccmMΝxw,?dx6lȎ;cưjlذWq<""@)BBBDW34 왳DGEs1FMʭ[$'%1b(N< J[of -H1bB&oa8;;s\f<=hִa+|\?9zڞƘUDLBR/vtԉ-ޗzw~wpp`60m XIY;Zli?Yh1=QJ1rp=zTM瞧m֮7ү__>x}ŋei4lؐ͛WIMM^= Z-شi3zʪ.LfV&իx{sa,--K~BlK fСFeW7no??zQ~wtЁvm`_˞ě79vϘѣXd;Z۷1dȐR$B+jlCa 4q={2/q7n"QG''jҰaE!D!d,oźLŋh4ZZi_E9v/4(<%BJI˒)VSKIɴjRuvrsB!d,XPJk4*:!!ew|+52*ȗv !De'eI|+_+2+|B/$[sYB! /9r(;w嗧bGF)U%%%F`x.55$2v%mbBQxPP9~zUh 5I"͋/Gդe|lYYYFmiisMq(ӷ߼y NXX o̜97ohݻ1n3BQc,Xoty+5kZ~~9u*p$"ߊ= ][.+V,nݺ;oͧK>Z۷[[RRnbx+ T|ϟ?jժkvwL|y֮~ÇR̞=#F յwW"oOF +kjXY9*UDmWWލ}R,]/ooqsfb"32xӦO'mC ހ75k+ 6oby޽>LOѹ=r"mjXYsIC=ze_Rѡcp6BQ;va⣹sYhx6[/H=ĝJgwpsfRL>m<ժUݻSw츓n/c̛7b{CO9ϝ?/J)O׮]9F1~]+W}ۉ \پ};[lfۖlذUVb /鄞:w|gsN?x oOFk8zsDaF(8zo>۷ϷB!*/S~  B3q7ݻYz5vlʥ,ZTcXe9TD&[qws'&&'!{6 ysWR ,,,򯤂hu.֬EquqV;;9LGE)7~ǬwgұC4ng.amDGGY~VVWwӤqc5k1cغmJ)FQ[.ڵ5V5ĬhXg\\\x8991}4 -8ɳ}ښ:uᎭM7B,KLut:UV-yǘ@[[[RSS9a,j 3M)v.~"sϏ=QW(FE`B`ظw71y~{ܦ%t:ts=^EW@ˊbcci\Cφ\gRn '䌃3K>K/гax( kDDDqSFCh=T![A!DV(V{{bccyq˜1M|2f>^>"0S[q,ɕ=Ӈu~y?p .]?RԬUk׮p"cKK]REKqvvF曮g.\0<{[[[SfM~X׹{1c7J)\]]EjnhFuoЯo_Z-666D_λ rޅdB!*{˲,],֭96G#1sֻ0qO.t 3Hd@wǰa9tW.__3fӌڶ1?ί'jyۻ cN"##9dRp,yں ώg:x30eʛחڮ.1̞3ɓ'?n+k~Z_N_ZeĈQ\r~~t֕^7`[6ЩKW:u~WhZ=уmq+y7BQse܏Xt)k^GDV\ILl,<҈#3E! 2:n :*ڵ-r 3E)I6z`C߷N^4lА%,ήd>̈ =Eڵ+:"iZvl߆oB3^J2xq*ޚ:G4.u]D:l8/]fSյu,!F!x5^JJ + ZN${1e׎mw17)3] P["G";Lc<BTGd{y0{{{z=FUV^޾CBq2^_IǷb%25?FӦh5&ʉB?M͚5+:!!eq||+֧G\ ))b-H {{{j֬I;weBQ9xY<%ߊ!BTſ!B!D%!B!̖$2B!0[!lI"#B%B!̖$2B!0[!lI"#B%B!C?.IENDB`Projecteur-0.10/doc/screenshot-spot.png000066400000000000000000004771211451344070600202070ustar00rootroot00000000000000PNG  IHDRcTEsBIT|d IDATxw|T̙{^ bcq\o'N6[r$nM8.8q `0`4a0UB^P>1Ҡ2f9): .˲eHIIaVZ5-B!Kڈ7S\x^D+s'ojj=Wa'&v;͛imm%>>mz EQ;7n;w vE\\gfܹg1i$ƍd20nܸK?xqUPU5t޽{svf1B!Qm|VhnnFZZZiimglҤI$%%ey 7oF7hd2QPPӧ9sfV'@E\l,f9UJN P"]M]FG@֛ai4⨯5 5CB!45;b4C)>:Zc-'K օl&QV^\M13y<֭[IIIAq<>9L&ǏgƍzPSS%33_u2TWWrzB!py1(F#V2x}~fs3.F _jD5ʈE`3a߿? KHH`92͢Eغu+o&Nq!{9222Xr(B ' a=[[ H9?kˌE 8ٳ={vt:.\… îwqFF=XࡇpBJ11\ގhquE{'3# +3=UB!H &Qt9-2< o=UB!B+BjXL!B\GAFB!B\$띜ińBhw:ǽ+b۩HWQU><'NtUbXi4`<&nB 3=_B7zfp׌8LXїٺtΈh=.;Gl6/?D \*0&'J!@Uw6PRᇟKr!)hʴ-!%982Q#Xv{ܮ݊ۗ%O俟_fLC|hqm5eKi  VS{/,aՊPxv6mO-.2f'OdӇQRRp0&/K?$`SN !WoLd8;-k' )q~-W\w&9Hyr˴ls9O[7%sh+ oo`fL 7qό8m8Uut&Œ\ Ίbr_vu+u>˾wEtF&wޡ˒z=Ζ'';^TWo?&3.Xi={atR?!zg^Wp{vsc|ﻏl6\\~Q5ϘBqFlE1@Q;z^Yw?p;|f}ײ1}}xw4kAa#ŵ~z{ZПSZ^T~o4 I,Xp=O|>tNBPSSî={qz22ҹU :^~_~^`1ٽS03g=jԥz9Y-@eĉr-$QS[SYQIsLѣGyzt߁jt'#3eKob8N ;+SKK Wch5ƌcʕ8xs8{~';Va4; CO2cB!?|T>|hz%~eu-~~qO={/0'ƃfCg*ݼB`gӑ`29DV4s8ܾY6)/̊Lxgp{ff⥯Z<>];%dSڷֱuzӧnWE]nLfϚڷ|i)0bZl\ ЍF=&Ndݬh޽Xl){8կ F{}w(N'_gﻗD~|a>ZZK,Z|#7-^LEyϽZm- B.#OvvzEq_CO:S{gK54GUKs37-Y 7,7Ws1ous,Vo~XLFOQUUMYY9TUW׿<^/2oC8 CM2cB!}!3S4@M-x_jZ::9mtfnoF`1"?ezJ!#lyƎLǾV /[nn]8EŔWT2{,bbc{}#([n,^_}mTWWS]SKa~<;TFAvVpcFST\/}~?,]Nɓ8tp@'#CgϞ=46^l:m( Kބ(L>t ??Ԕ`оNhUUդekq"BtrM?>JO9U5:l=Sd3i5Fvۄק`5(Q3_Vq;EBrQU]˲% U2i")ɬ{];Q~~:-/>[wc]]-&wC ĩS8~S]Z&#@ `^ltk܄bܱݳꕔ@I9<^}{ٸqcf,Km0haXCb%̢nz =B1Xi&L-.O?n+ hvxc_QnkV^&zmowL>YnNקʞ 7;:7| ?w0%h%5^~QI} `3)4?H^R'GJ:%1$'%j|[xYd1iiv{_WO>9'?_٬6ZN@X݅ cQj0;<K;zcGvZo6[HKM98`?Nj'cN#̤y/CRRRFQsl1?ioђc}C"DzJNT{ ߿?%%%\28ȉ*۷SWWX!ew$w had{fƑjqOaVY3->-3|qL}㉵xP}XtL6qAAVNCmNӯGϱ2Fӂ,wW%%=s`N_v-R\|g= 1zh/3M L]7g6Xla#++|iii'3`Bu~ΣO3vE֬{uψn=m6ĕ0&Ϸ 6nbnvQFqӒ$%%ǩS9q$vo>\<̛[ώUFʕ+еs~Yz-EEE~233|%  #B4qnTik!Ձ\N!ػw/EEE}CVHfl۶mֲbŊ+P3QtmO=}~4߀B#:G}I%E%dg .~?%TO>ɬY8z(---deeq׳w^;w.&M m?ydihh 55Kv ;ڲe 'Nμy;v,W7jժ>{1vMCCV)S0B!օV?z4%Z"3DNMﮪ0-KHHp9F|ի1̜$j,Zncǎ2zh,HH.%K`ضmo&>`yyyL6 ʑ#GX~=#F`ժUa3cշ{KuD!>C3{"^1H4;978f͚^ob,YdpPPPH7nǓMVVf&L@ BIOOpxb)-- [tv;Zq:1ֳ08RS7BKS[ fĒ+BAtPltjh:O6S_6ш^qٻw/x<9_z*1j(^~e&NȴiӰlaB1Ƨx}o(TIJNot6 x5kְroh4j}Ymx̟?XN_h4X3gΰ~^x.\HAAB!biUFtM¸1@ee%֮gN&Oy94iqqq g\o9;3m }+B!DL&ƏƍSYY/b<N7껥멬DUU, 555TVVʖ-[:c6rg}'Ovrl6_ұ!BƄF4ӻrXh[n7tbۙ>}z]U__ϋ/!++S=Ν֭[q:,X)So`48q"vŹ8t=\rA6n܈%))K^B!B j&js"E36@&uK&}B9_VEb| F"RTUPS@fzru'"-rՒ7"7UZZ̎ !bhmfx!Îx^ۺ\Ɔ\.gj\.'8d^M=)*0B!Dd8kCOcCR o@jDھBbB\ RH0>GK$vzG#]!B!UF{1%&B!B\)18%B!B+J*C !B!ĕEӞL!B!hB!B\ctDBq޶EUl+~4IS!.N,B!@E4!#h+u  벟dĄuL!D8:4TU R\úc TB3%00T ̄B  ^@FׅoEyyEqZZ*y{k$\wz ԋ~3%M}m3IP&BױX/_}-+*{\_V^|&8-5[p?˸ʶyꩧX|9YYYCVaye!el+AB]#~XVs ]|=8psq68)}2c@UUC]wE!!\YUNwͦlu_!ĵ#LQ"}o><}h4t:Zme }|u\z\/gzw z $K&צ`3Ŷ!ȊS{G{Bgy˗uVxٽ{7'Otbۙ3gyyy8q={؈baҤIL6-Tާ~ʖ-[hhh ..n6]V{5* !=CW2bÕߠL2!v@E>c0oOY+͛Y`qqqt:FɓX,=z 6c,ZѣGvimmTNc…86l-[Xj@[D2c5O/@m ¢3%ĵPS[%Џbo1Mmt:nо}ZLzm 8~G RD ڵ=d'TUe޼y֧֏3͛7؈VEUUtt:VkLUU5jcǎ8bzI YXO=!ĕSU]VђN xAW۾MQI)I)}o(ÜN @pᨪCp](WVVrx<%))\^}UƍɓZj 08bcguu|?@qݤ$jY 7G~d}5]쫼4B1.%jv8/Yf cX,}6˖-ٳ>|W^yy摟߭ޞS\]z+-`LP.C n}pT_}ʺL!<@HH0W}/n~1 IDATܹs2vN6dffrQvܣ/У\O3Ůxm wI_%a}Y}\Bqm]>J9C뭙bL`Jl6 8{,騪ʅ 0 umz4-zlر`Mt.aÕ1 <$XBMG ?0`R2cYoc43f Oss3G?CvͲ#Qp. 6xGA<~?dƄpg Y@lX qB讙1 ڣ;Km}%Ձ_k}{/cMĵݲa恇&6v݊"2ΗU<}zjib LՆ]q]e}eլHWC!"NhXrRU}nGlXpYp'JfLH} 5,}OWAY @Նn{'&1!4|Hׇ/P_1|nR}f{T}~jWFઽ\l{i(&͔)S0ͲB!zc40aBAZI3E!B!vunt\2͋B.B!N͚ۛ'v z\z8eBL2cB!eu~ž&Bq`L! Caծg4FOB\$B!.pć[.K! g,fΜɬYBn7MMM?~Çw=Pz;3R>Kfl6S__.y>ۍ?p|]rB;YIۗo+!XN\\p\R( v!I:u*k׮CRr9'@NN .ihhZ !XG3`]Njzysr߽wyaſu^ӦT`ˍfngqkjq=df1rd.1BH0 OQQV/~;ŋ/j^5.HOO`֭={)^Qut:X,vm|>YFfN~@4:g -dυ-/ŗ^*uMҚ[Z,jw|)6}DCcSK9_Z=edefZ_+.ԇ:b&O䂂!g^_Ą!//~yY{)))WI0eZZZhnn&&&&ԌGaݤFQQ6l@3grrrX,԰k.P|+TTTo#77V˹sغu+. Z;m4p8\.?Nbb"Xby^˴X,>~h4A;hдߛ0EQx> J~NVVpc|i T465u,]|Ə뵼f3cT|^/E%yMXLFҺF#CZ"I0%ڛfeeKuu5@ +ٳ9{,nnf9}4uuuL8+WSWW9n69tF#o6K,aєqq NBZ-rUf;K5۱;Y>_ף|fϞ͹s稬T.\Hss3{Eӑ Ԅn:V\I||<+W >>~u# zɏPVHբ*eeW!;; : >oC oNee>=,\>)>[ s &WYbmm} y2y2_. @NutQ@K_SsĘٙ7hljM[0Mѯuz=yFIbCE(ĕ"X[;=zlݺӲNIIIdggSRRYjӧO]JOO'==Bv/,rrr0l6FMiiiWgffr̙PS쯮U\\&Ll޽=n9}Kqq΁WYYڵkp:̛7|>_W_e#ΖY3zϳlBɬӘ:yulپ=27GkHOKaBnٺʪjVvsffuo"m(/*9&ƍe왡 l15pM Imkx|)v즶Moh4zߧcc^jꈉqlKxNHcĈ(/cع{/;NKK+vɓ >ero!cQFn79s{qAqqqhYYY^78&Nĉ;s8 ӧO}~ ]+ɝh:c06ױTWW}kPe6ۗL&W_e;w˔)S1bǎ}B bJ *^:ɏ,V+ymv~s?~Dccc}O?ç~w(yyvUUdff{ó=q+a͸Iggzϝ ೓X0wvܜlK餢EN)&;+[ X]L,`#618 >wMf`vUAUٰ#Nss+z=3uhC|%wݼ̘>S&SV^ɦ[жo: >9pX*ydfFC!MA .:uӺP7a*Rj.))᷿mXev4ܹl+¬Y=z4saرk:jCH||<+V`͚5ʎu~/j7|h4̽nNRQY [\.3/UO6NJ>c=b\ .Ca2;g&mH|u%ʠ~1y.%J{-gĈCPUN)"!!b8UtS&vkR<fLCiI0jD.{ф2olE}}瑅'O{T5=9ٙKQɹ~d"P9yQ#IKM^_!cWZ),, %%^ZQńj ;/V{'//Çw[u+Wv^?ZX_O# vv^9f>Cl{ƹsz}^!˗/bMۃ˗kYF>cmz=?hnz(ei@mMihl`lْ,X2F[ƏXF 8G;% /鹭Vs$Pywvۮؘdgeg76Eu *';3GYEj= u1"۾-MFZmc6glwa ?f|>/.ct_ tb1:=wLJybc,7VCP0>9|j~gc͚5,_w1Hq>¶;]Ҳ2z1?RھF_}~e^n11.^ u3e~ z`K/Wt v;zo}a!.fQTt6)'+3'OSr3cXؼe;G?#5%m͊ukؗl[gG[iiiF)8X9TRS`yFG"ZI0v{%''j{=jkkfV6lٳ%++N}6mDmm-cǎe֬YnN< 'HOO';; &Pք27o̢EɡÇ3uj="czV` ##1cƠhhjj⣏>-pn1ٯXG])z~~mt 5B1!8`zv-c8yGNQqf-)II#*7-446翾sIjk_]ھ}oHl5=nϾ446֛HOKj$%%ԟrFfyi)ɠÁC̚omvf:&;0{t[x{{ky0EѶ<_n?K=DzmnnZ`߼ܼP&iDN6fTnn宺V6n?FqcF3yb{@5zN<͟z)'ycm:V}V+˖Ȏ]{ٵ{99L<#ucnjW\b0vhyf)SDA'\.++cՑB\s>t:( :VGgŠ5VQht~4(?փAo7>Ս_BUUt:E5`@vk@סh h4m@&CWhToU)JV>#:}OCFzFR0.@Sgמ}-9=wtUɌ Cf9sO!љU`nDksz܊ħ4V;ntԮzh4޶-xi`ɬxLz_ : m ՄKȄ0^E U _cQLR@kǎ?6/U"bT JZZyyyTTT'D:B1h` XC?xsQ * iBVk:ޏ&-j-j[?V]ۿAuS0X#ώⱢqYi$bUFi4Z8-vALF#F2kƴHWMfB!DZ=3TliI 1~ ??z[߃ϊ4`4k1kc1qX X-m^fBq5̘BT`McNaIaI[3ySRlZM%ÚcB!9b˒ǔÜE1Hc˩)PI8\5a'/;ms0{IG!ĵA?$yB kZ mL >k=.c JZs9]>(f}q:]LG3i1y;۲g+\C!2$3&bxѨ( ͨF:j4eDfU4k@kġM fϚb 9P !M !iI xUKGJbT7u)Sgi14`~!" !*i->4 x4Ѥ)tR8Eā#!O.44HWO!M1!W ux4Ki*U8G8J73m](z !jJ\+: %L@J'?3> 5Bn$Bufq5+g*LkVMX>`> +]}2:G'cB!Ҁ? JU&\GkM4&t#mpwoWuS]x.rB ~9GA !! ƄB =^'SWpZ$貉dddcʐAK>8}cl pSD 53T6pZ),$cn%Pj !D_$B1dv7jrj㑮NJqJJWZ5lId'{S.}@[Z& 9vvH42I0&)xʨ7HWltjI吠A3bQ[v}Zo3ԸD@!UT>őJwƆ,U*%vΗrd"3#2HIT(Vw@57s()ǑKn,dAc!< вzyJ]R:BzoYkxQcF%^Or|ΧQe!DI0&1ϩ.ݷvQT\LzZGb9e6j5UVn$7' 3gLǐ=O@U@hxgK8_̑Or1h5}xq8{]a׫ }ktI~駞$6Zɪ5RHI.'|I5â.֐I! e(uR,ujHW?^9O֘Zj !aj)(( w}7sӒ,\x#.CQ^QAzze|ʜ9sHJJ[w0e$*kii=?' &M 5( z񋋔:g-33'ƐKa|iz8Yxn`?n7/N91;=疭ظi3zYa#w!>!ѣFc./0~\eW9Q&r ӧMaó75W9q4nSr-7cjljPa!c0;g]|>m={3g67.Xg8PR$iD'i6jmǑGϮh4 YdY&3pu8Ԝ=~r In+^!DtY+ŰQ;dN>֭[뮻zmΨ)|K/ֺwBѰjܱj/g:@}C? ?x?[ֱ3k&;v$PV^NI9؉wQ]ovF J)[/mWom J/ JHC s>as93g\N'e:ёÆ_qJKKoj ӧ7?ײo߁;vz͟ ɂysQ*kK_`y|;tPߖvF}qcүOoILJsNaavvv<4>ʕ,:أ xeK|;X4Fr w7wfx撘ŗ_ɔIRPPw?m "2 1er8OGu&ݎԺ_,;ɐ#Jd"P94{`4Jpaɫ_`ڢc&~6b@gp82!ZM~?_EŧPU]cP[b.]$l/)6@bUhPH̻ms9--[5ޞOO7_y>_g腏VzӲ_*w;f[2228p #FʊѣG7)wX(l۾'=È:dPss3F^w``IIt#ckc)Hd{ۇڳ︜ wԴxq.>?zBbOko/.g„ZccjѰ?+rVA f\ȁC0n Z[fmXPXȺ xeY0/v[y~ ''GXIzF:AAZ8::4f%o7wڲݺevxc[355} /u7;o@5r8R[PcX)#l8˃pc%jd{nΎt:̟;6]yl_p- 2\+ɨy&CRP\D"DB[D *l,縡w-lMkCz1RQ)P)Ufؿ#96nRe+we-9HIK붆ۭ1@3'))  `ڴi{99:2o,F ơÇy?Ӌ9RU'11ZG {{C|B.N?yÇaiiI… r zYE"|}Ё /snm΋?͓8:ړMaa!ETUUr$ O|…f]vͮV靧7FZ'NpL4I)\NOH5#S!g0j;_Xze%TEE5iQFm/)q"EBd|m04eu(6X+?$S<7~VK7N?pbj?'׷/ee9vYF@GNW_'++c7_ި R.\Ζ?K}bהU4R/+Ѱm:ILJ7!*:++ }x>Ǎmf&F/F+؍ңiARYHjR2s)B.hk>L\J+J$ۘceV #;7I<+8GEe$5e߸5\HĈ=ۏRQYB!eh4Z>]<k Ү$eaifcӆ@klK˘>kv 1v@g3rr.2r43f渾tYێQ l?IeU SdD CjfXИ>t,XEr bˮ9|s2\ADӉs>+ƌҒWק7"77W> :u:LML/Y }ĥK,xqBBB8yq&Fu,Y83R|\RIebc1r~Ĉ>AȤVm]|@BZ.q.sk  @*ɯ[ d8s>ϖo%s0Q+v|-Ke]z]_6dXN o[|>6]}Q3rpsFqp\LLwaƸq,"ޮE,w!O 1-w]GX7&F}tȐo:L^aqR*ό)娍􃏁QTDEw?R?z6kcQ֩Us4ڹ\fnubXX>IYe%W5v}fzy9jL_c|c2 l k[yHII!z3fԙjʫԪZǮ)܈]\.V?z38x173ˋh<7 PiڵeYF.ꂙVV,_i=w/O:nE&dkk\.Jwuچk~OyzbeucoogGjjZubװD.#KpvrsHo|;y%ﱟ4sx6Mz}MDFa?:evo^>(&ꂩ)BCi΃B IJNf̢'Z4Jɓx!Al߲o6&//^_ JDBrr iX8>j59zhn. lR=?iS\RΣ L\.E,SHfN> R.{`;l,LxѲkLSRr NJqH#wf茛/!JS@=ݠ4t]@[\&{WIHYOJ戃* 7\-).y+.8[s>)Q}edf3o0*|yߗޝ;=D1qX_NLs8yYv^u'>ח SP̨(r& V[}]@VnwH)5盒igS cȐ!ܹwwwz٬,y5 # Tc-jC֓۶utBqI)+Wf6''G<==Xll?1{C i;k۷w횲hNGD/\oKA!G,~c-fLG"`nfƃL+6u :},\0\\.LTrcccWGΝC۩355%|V\S'cfjf]k(vOEDbog &jCE"\]9-lܼLJYrC B"yz;ML4YXZZGVVC nQ{o'l-ϓ'zb+\(Bp5kE `m5˾{3_zޤCf҇"JVKaQFFFȮ 166FŲYؓ y'ĸ8cbRY/(ĴV!+,DeT;saoBW/&&{cwG[kɛR 55j$qyEP#%\L"p6܄+Jv`yQ:GD`geڨUR^F?ŨUˮan|7J$h~͝ЏێwC{0gFV)Qaj払nUolLC`(6;E~Q)U55X\^ޫO3ť8Ͱ^׷"2}Zk}`nz}ZIiYEJ*Q嵿J)GՌNKDEEKyE9>>5o,psټ?nuB6K*0R\oB.E~u vAqYQ^YsA|Qiy2 l Θ}h4d2FTF*Ξə3Q9sҲ2{{sӌll߱{{ƎEGy"a=t)o/}8HDn݈;{_廹1d Ν;={QW_z-#To،5fL[.#G ܜ={C@?K_o(xtì]lM!,\0-۶#O>bPT*6n6Gsqqqfl۶v^5|x=goY3vFZ5 :~n)O>Xo!AuSG {#5-www: y9UIdEPCE Y.`,$w~ {;;{z= ϊ;{G.hLccc***h4H$J(k ?177cX8cO,wXWY~ښ"ovhbv(-FT*jDhՇc'VyIJ㜫IG& Jht~O4-yEt6F18[R\VAbj&j:24U7Yh 9jRh=[ K,k y1ob?bxǍamn`s5%eׯ'@Aa)Ml+r|E6^f%'K U[6Rҁŕ|Bصn H Kq9+.)X*jZbʪƈatepuiٻHEuJ+~ *4-JF4vY)GYsA|S2֟MQ*,Z\.#8SB}{o^umuaC6ka>ο 2!C5ZĘ}zѷOFS(L<)'ݴ\,ӻWhGu6׷.\ DR} 6#G̛SGD"aՊ_4ix&MߠMiS%̪׮kXYYǨ}H$z wmxfPhvQmѧq*Um+8L`*ӛ1,-a՚?7f4|foO2}!A] Elo䈇;_-ysfq|X#3S5G1kvTJ9h.$gyp, &l<3&pߡ2Rq&d-6f63;ʵCGN6%T^ Mjkxz Atj6l߃D,A" toX;m.^b4W2a8:vl{//7g&&&(M;y ^{-R/aee# coL}/̏? =v@$,zgX1HOo>coȗ+lOF?` kME&hkR!FC~a 7s 53ʪdR dzGɖlrjeVI`-8[}Sf`٪]l{ 3Su+ki#nr 13U>;VpD"Mջز4fg:kwݚ=:hi='kB=x_4HoS}8T,aXX`n3|hc/kwgX l,jgʚƂ'F 0Q7o|^Vn9Q)m[@;@kК"Cɋ+DwkjF DmVk>{f(>_ԘβQ}=ߐ̦h)Z;bCv}|Mۊ cEq y~1nq׽JrIKs/jt- j fbGBޡBUu _KUU qio):4>u߿> }^|:^UUomUz3P]R 7CP` ۀ>r dI\1S*{mg_|~_3vhxGD;y>&'''y# a/h< *q 8mVp0juZ wg;3N&L׍((*ŒǦ iiVFuO1|PF;rGv΢ߊ0Sq IDAT3& p VWuN!WI+!G3cwėBɡ6ץY`WY'ڲg{Vs%Vɕos]`<=D,>#z %TztIŽқ=V@@C HʩvL"(T,vx*B޷F@߆X$d^^ė|>R.yN.|DP\B[$$1{G:<]wE)BQ3H@ߎLt8_ %H+j]պ2%rNâ$R%m@@hZZ>}r#WVVRT\X_! p!.CDq Դݲ_ ;tIe)emnEѠhDfRlB'1x"xKYqMtj/K} 'RccA@It::j i+,,B^gѻ%R۳6 WeqDNQiTTT Ē` xC/G|>+/&B,%J{"*E"P(:Wf)" "WZip2"]'pc&T\U!n%x:zr`(N ܭk-D4z(i~CbVNEҕ'dh/O(HaSζ=J?b^cpe<.)QC@.Ep)r(& /H (#W&"VL&:ZN#\<Kug:#_bvS muc\y~ ah K{kqRDɺ|8M/.s:;ab,|~X\e5{/H,;npYX-rwCnpGeи^"]uNkp&kVC%27| TX, TNk5B>^0m'hW *XIj .޽Qw@W*( p7 8cwBϓ-katPAox{.{;k!=@%tҗ­ė7|NCd.Ͷ0|#8cw b ,(e\ę~x# #NGdteN#pbG#?b SdPDRn D IgL@@@#Rhк_"5TV6Z'F*$h4Z>}C L$l< ?bS [.ЦRjS0diU ]B; A$VT:,mi苷e 4c"$ЯÝ6E^A(|)NFEdWIɣuVGy&- <gL@@d$gP_SqYnA(H#3=%NʿױVfͫm2XRZF,dr.DBЮo'R ӡW| xå TK7\FE~bbjJ pYFDI>Jitm-h7|Q( '8C*2#"H _b. F{*cI!JdTz0-rWz[JFD:+/Lv&8xDNHB &,*4΅ \`v$gI>T"!FwZh)TUNviSEv|އX[S(;m@3̞\(9d0Bqky/*a@@`/D )JQ)diS RyTV!~gӈm@vN.NN\iSZQ 2G̤DC1d_zTL툶LxNڒ6h8EEE7C@@>)LΈ+..X[#\.c5c&'`i":K@Exm1 v)7ZKa=-"pAFc*Շ-&0ˢ88Uc* Fkl01̘ b#ou.S<܁{n2 ]*D!r, &W@@@mxGW  NFI6{<_Gn{Dh5+ p`.s#(XKqa ebTa%6 2?=55E=sΫS.0Nc$%%W;Oz쎣b |pWY\JLHbcxzӥKg|:onuM 3gEOѥspmh[tS]cjGy{!{]k(p"I 1[,XOZa6%F]Ad aW;P:V\IiiYmIiiVsNS2V_z6QuCR^Xco->;D_/<Ģku;wsf[]SôfqTDt:ڻuV^g͠_撑yN 7A&1vHFB||6M%%p 7͈%:NL}q/<,Хi (D@ߎ1t:vEVvvlo^{ ol0큩\LLk6mފ]tni|WḺ8ZH^~Eņۑv={wT԰evN gat {<<.:inʹZ^Y¸1sH0&0z;mḻ|f=PJL 4p\U1!b%Pg,K8nh3>$~!- v;uG9fA]G~~6˼އ,9X+zZD)6VFLMsK2<;mfL1xР&k*LӄO@`?EEf@ĝ=yŭ)scԳGw Eܯ<0e6\+jTTVe6"DQVVJ^|{|f͜bnnV;wxyyytD>,?e _37)8ɶj>K3{&GLl\̓N`Sޭ+~%ii7:Ǝy ]yxzzWOΕ,~=l>m*N$%%;/?͝Je7@uI3vh<=yݷy٧nBCw橧ede 573cat[v$)9k++zӃv<֭+{>>f>4tB߾}jnF'MħC{{{ѷO/6mB77RXX6(666L< v1stsy?'=k~>thϬٳgk>lƍNZDpLj %#v#5?Nv QU]B.H.77‰';!øva#ǶdRF կKW^cc5ډN//}\]ݴe+/Þ8ppe%_5ٹÆ蓋!''7z}%1\>d NNulT_c335_4NqfJjj*3~OFmxa{đ,7>ڄ[P:oަjH+7K@@i>c:Q1|qgqr:/B8@bvuqaæ͜={^au;~0~,"?_ˉ)g }Z=;[[#TxS[N̝=ÆpQ=\mmlm<~$gINI%==R @f\̚wzze%^M/))m=It⬟ĤځL{{>'I`@GJ'Y-Żu\<#v3LJvFV7;2?ƍ!(/~~ryx@˚Cuu l};wj]f`~8+4>$\]\,jc 07c|Olٶ kkk F܌FySۚѣ7kC O,s?su28xQ[j45űn&zpܫ=nkd_UL눮LX! n>c!N1&&Ɣ|o̘>+W/(+/[.<`FW;hn1b1ӦN?JNN]tu***z5L.(6μr"fͭ!##%TT7ٖkH$'p)9y$'ծ]9Mh3G'G2bo+ n:Ǧ-[e 8gٲU1h4h4d%/iݰ%kw#&x{{q)1 tzOֵ濟Y&p#K&I5\]UDebN!l ;=!0o0311w6Vʚ~;{;̓͝S_gxÏؼeEEhFhߓo̢|lo˛o[{nt$R}7j[ߴ1}OپWرn1[^ex g}ʞP9DFDmM$U|G*i!C& `X i{iӹs/.^‚se_yFf ׷~~<}}w;uy-pN|+ \yu$6R1l`pc5t:v,U*++%oQTJ ط')0+Wķ#5-›ΎYYY"E͚-DL4O?t:D"nnD0jv\N@.c[gx jg p"ǍiҮ256#٭:b܅b츔Jƕl;cO=v`o+ضm;LBNNN^~&Κ?f |`*>/^R89߃/}I)ۼU?s-;+;t5Z>UYprmzsl:xCDmȏ?ۙ4a<*_!ڤO,G ]b  ~͘Nײr.cn&O+YY8;;aVL^^u07v0qز09kc_9w.#l9|xȤlmIԤN}m...dPGECv>a-7$6mmZ-k}&HRVC_nٖL~w7WB{juN&Vj50YD CO,~fwMBӧvS̢9Xyy+Vbiؠ iޛ3gЃ>l(b;w{^066Mq-~{Q|aooOaa!eeen|Oyydeg1t {{џ_~YΠAP(g`lS``mn.=zy6\ Z-.\$8SlZ=zmN6lDPNTTT"}FJ%IDEǢ)0T*fϜITTg)++ۋgOƘ1AoW˾ߏ!Cj~QL&aox!Æ ̌ D 6/v>hA6,}عo{{;ƌEY7b e?bss3B{e^ |'O}Iq&* ۯ( {f_=!073c֍ģ 1s|6'N6TΝ/G~9fؐtx؈y+)n)ɒ!v8/6mdӹ ׬!?[0%MxhصgqJ~} ='*:+CʾزmV#ɓu:ג{DP^V+}zׇ+++'^GXؓLp*B&}ݵ6I!~F4b+YYعsT*<=4h vRۧgd}NN/L0^EchZJJJh !@~A!j$wQ]]& G#M-8Qj%YUGA&b\y#/i5 6[Gd2RLV{H:D"A" "- h 9[%Iq"C& wBB:~RJcXĩ'LCc˶mJS/;cntƼI (AuYi:@[%:gL@/ljTߺj\J"JveY+mج3J%Hf͜Q1!Y& AãQqr.K9~%q?B-࠼*m)CfOn>c oL [7ppp ??h?{ RGKP`Of͐u^x ܽx#)9$L#;EntgΕ%wjut@TT4oڊY36M@Mp$[ KЇ&% o=U'dbCv[UDb7h3 dǍbb]tkM, pYG()nJ:#n!ִr]YA.6\[UKAbq˚r QT@^f8:LLwfj+m~?JM{8bGnښlw+V1p˲ru ڜFbUb vs;:T6;7IیRVCS*"@@u(S-ي.-t{&mB&m >y{Ϲsrs#ڿ pw\cx7;.)H:M%íj)'LZz:F=iYVM ק0JXDcLGDiD_ ɒȩu=K!*a7p98fDYën -ݞK$B:DYeEDL_##wŹȱ? UJ!"aG\bF&Y^z[wxPjZ>E)IkJMyhRL*>h8Tx{{w(BKVv6iN sJGJSo.VϷLW 5( ]3M Mu$9f=7(G񭦁j Tl;w #,4*1T 21HRjzr*%Gj;uHbRǞ TҜ<7a >A( 2̜vc"^s/O5O" Yg; Φaҟ^'52A^Qڵ5m:ERN(se͚eL!l\B@)LG !W۝O)G ԙ0g(K1BAx< BKK' D}+DQsZ lǍ2Dm:E%Z$ǔ͵ o +ܰG^ܻ Ef0"K4[Y#pw(g3s TpAFn1v)?LI_&\r%)TPRc6C%e >*1B$NePe 㖐mol$8>>LQ T rqʓD\A eL$ ie0u4J)*tc fAPh_+#\2`Ju*bN/*A(3fJ5](Q>=h2ʂ_rrraS.~|h2z%#tJ0K g/^VeQ0$Qb7F#'Nc5hӦ}|8t6nO?*] όqqٝυ8DTC5i߮-RӖ]iiݪ%&$ep5Ǖ+WhѢ}A2wHzz:xxxܷff& dddD.ϛm!Mtp:i\6wюǐTfA C '7߷dاiղV-[`2Ű.k^}k&*y[oժ_~& mS#iٺu+{3c h޼e?[: .12UٙdRSSqv銍'''\BZZ'PCRǝq?nAxjۓ͡o#ӔHiܳcLTbtP~dC }董W6nDLL :@RҠaKӧNf:֯_O|B<իUG p l`QUPl=H0\d2c.j$?dӇGDp9$/7sn JDEGy6~۸]o(i6֮7n+Wۧ7*\8Ⱥ_~CUL&+,{4nTJ*Uaԭ[ww-kÈ9s&Z'ףc.6n†ǧL& 6š,[=7nP(2m:巍eDGR0u u늃CR/{)))|ᇌ;777r9IIIвeK˾| gggrss~:UT,+H%嬎q H S.^vNJF\r?"o2F.pDd2RLfyI_ l~j3 \q?xJ=V^q\\\?1j8^ 5k -wS$0 772PZdRmNw\tσT,BPPjUb. ӦϤq(2MZ6oc?R%4,{Y(> +8xO>97WWnܼ ̷мg(ى'Ybcǎ͕Çż4r۵{7W c1\QOxɷddfѦUK<=F  #33ϾOO7kBϷ:mZ6n;xr0F#_/''G`oڴ T*e-dfe2őNH5ƌ.9NWWjrVktԁR)qvrb(JK~wA!cn'C Eǭ[Q5+0g;̞3O5pi1sa iΞ [Q;FeӧQ=Hɱ|ܙ!11Nc.''PD]g#HII4V6 6Z̮={9"S_d5MFf&-ZussS ʲѺeï1;1/*kyZ#i2h2;H1jTlάv翠/*d3NJWj4_]X}ek֮Y3_>vׯ_!Chެ*I_͛V- {t3Ktڍ}۶sn&>>zң{7<-twÆ yxӾ][˶-[c.>+W :>dZ[X8N*:?Чwnrd1I|ϠQuG G&ѭk˾&mo'%9_S[}**A*S1RSS Fu?p/Xhy 'Rɵk0tY$_CLuט3L@-UQmfm훋l. IQQ t/w }4xV]_׷:O_GQ;s+*)Uyn8BB1{\rJ޼15:w`f!x{{1s4/¶y >`u,XLf3/t~jZkqnߙ+Ww}ӮSWV??_ZȄÊxƌ `ujR2Q9*Y*rsغJt҃x @ǎYVfxܽw+W%[ͫ# u}o.^ۋEҤq#eEQƺDs}u+ tVz[QQdeeP(R2NNʟ?g1驖}Wܜ\>=5֭[Ye_VOy7TV^KDd$...<(}7o⧕aCѰ~}8pf=xwj0+W![U(xRSS`! /X:saɡ_>E*Yf7nŅvBBrRRRիeėZg ־g֋`0PrB*=l&##/*>))` >L ÇP1.tTqcݫG;ү믿,Wa?YoϦsH$ޙ&z  n,=Mqe^8^ݻ8bAp58Z5kba,vZT?'Nmo\|_y>={^6ww7[}a֍h5Kiש++_F"ϴSl.\IS1tLw__ǘ8u݉Ba snAjn4`2r+* W7W\]0qZogdIZ5qG{ظx<=5ri^|Y>'?4mڈ /> 2Q#d5Nvؿ֭Z츱 Y шXxw]=k.Z嬾\VϷ:旚ի*hBzt5=gǍYh֬&##gҥs'vuم|r M/HP*xzz`i5 4lذм Ȟ ''m`ټc/WCxGO>"6&YF Ӧ}'7n㞣E<5|QQ\ c/D['Oy7N8Ih56i7[.ڬmeӣquѶM+8w FΊW}.Wck}P)4n/OOBBB &66^T!fxX+ٿEE[a`6a6Q(4nܘl=UdE"" /FlRL&3b!4=MLiw+U& S sww˛lD* F"0l`V[l4ٽw4rEϚ{[2flX쑚sVѐFFfJ6JRYuYէR,{vǝz>k2ljqpVhZf@G~~dgt|OOOjusz*U$''\.'''h,^^^-vZWNNJ` |OnݚgkI I$y"##ݳGh-NLFNn.99uHLL$!It:l'g.ʑz~,Nrjf̵5+ TJ{fܼVZ9| #nHZ2ͽo8NBO>(GٙÆ2bPeԭS7W۳Nf]2=#gsAhD&EnnNlbVCFfF69%*L&Rl>M{X+CBBb߳П۳yV+Ξ;kyGn8;v2z=op>S6o݂AC!!srrbر$%%r5nE1mڶi]<ƌJbo9w.=[)r^9_Ճ={ǁ|:|ECR)C ㉉cus$L:_;v/ZLxxo52Fh߮-X}ЦM+ kۆm`o9BK/>رcQQwgjr{ N:h .,[ Fk"9oJ^O\\e'''T*;UwQ{2l.bɸlٲ2X(Wv8Ujժ1ĂzvAl\&xp/˒O%X2صg/Ѵh!#FrFoIs=bm-?ZCzz:p6h`;0nhvǠ,[Cbo}i*RR\~MXf>mqrr$((HT텏rI`?_/,Ξ $',Lʝ8_LL&qL@%Q*d2EH|/i.I*"qwwˋw$;;P9輥||ٶczaay圲dUCp(zCdggsj0\t:4-!;;w3eY>RIXx8&$۽[b+*ycn Μ!9)Z5jѨqj9t(z=[lb,]SP$]SĄGD*4lPو&Z6Z팬 |Ee˿G2bz{%&&2q|p>U \sAivϔHF_"yŋ[.E|60:x{,wq\HZ5}{u\t_o >!FK]ܱ w}G"77~OCvvV_ȚI9|OO{`Clݶ\V7M>ӦuGv̷6e2^^^VcfϫxgGݸUky+ 7LϏ;5k!;[O]f͘>;j}Z+2X[/ukWƘPn)'O>PcɁCxkƛHE,jcwP6 ( ڂݽPH uѳ}O_|}ۤR);wl/ƘB@.[wEi=,z=';+ߍn h`Oʾ`wO {l6sj0\ /TyENxԈ/d2s!:k[XQPķKsweRLN__׋z{SĹ!8{zcGصMlsRLи]F#Y&4kZI. \H<̞v|ϗ?XJ|opCz~Qܩr,P1&*"0ߐoeUK70A(#| "+|Iu$:-t{Q੒b,,H1̸W 6%Y|PŵP^$ f9TR|5o0e{` ;ZN/^x$E،d օ⨯MTE+})g84*+ƒk£Ȍ3899w0BEtL,Ξ&-5sT0r!GCE6VBgϮ"VWL"BٗH͍+(qxcY%?sty ǜE<.I3 "yQwA(Wo( ;y5E#j*;^< BETEgsx"^##&“djwzԉ8I\J1*AL06mڒ;^ǚ:}&!׊KY:1^/V)Sȱ$g[j!>n99 o/y qK-,aQQѥvigkβPX_˯N3- cs"Aw =o*MZ.:(ňh&N%*6ʑP3пL~FPٵg//\dk9f}zdSOZ;u /:PeïesABKvv6ޝү@Yj}6iܘU?m=8yѭkf,Yn:d ^yLG u{O)~ZDhFN"Mayϫj Я#ά]z 8~۷"!1gg'ڵmC}Joa޵ ݻvϿ,4mauVڱ99ӸQ h P\H^;65K1"A(KWBxG s~ՑK;xV]_׷:Oп/71mNz聃Bf:}K}.VK/>#jR2Q9*Y*rsغJ}7o[ ͡~t҃xw, ^c| ycktEێL{},#3+^xό-RW5h@:izsM[5?S#08W]'8 :wDVf&},Az^λKx{{h\4nd>tZ #e`ݑder,TR}H2m۫\֭( 'a ݀xg֛\r1Hug~wR}m)8U?}Zj}gLM4C1qnVwуc2lƘh7 ծŞ,?kdR)8ɶMѢ|g'Xf3VCF=gS^"7Rj5naplf+Y#E 1|Ogdj:ڵmsILL")9;s?9|(zݗPռ3k&<_'0П-`,],v95`\}f,mDc!!5sώˆ- c0̸ ˩Z {J$\ a3lܴ4b|X3kumEY&?sYK4An$9_;}-NTv*ňL&z7ذuͅ+!ez#G'Sglj5:gu8ZF xcmZǹ8ILLbDR!FR{.܊wDEEs=,/ݺ>ouN>>\ Z/e(u]ܲ߫zr3a TXJ7$jR=^ BFf3r}NP Je>i ǧ ύg߷i=0tuq%5%F"RZU4}iVZIlL*UJsNC{IF|ww+ LFcN@V({9H%TJd2&T4n*1yϾX{ҠX^|YƌNg0[Jum>uZ)VZ>D/n3FɈTn" i89:#mj4RB`٦RP*[f,L&#L~Rr2JgG|N{2"7N=ǜڤW^-QZurt"33jԵ=eё,p-DcL IFEsAC-== z^t؞NTnܼzu"ȹ=+''DomKJNFYVϨ(V֢"7ƤRYaqT(|wVCFf:SRqqRad2!J볋Ga\tZ %}$߶D2RZ_.5:1voc>=#g''Rʢ-;+rsa TH !Ц#VŇ KBb񉸻d2}O>T^;~^t>aa4l`}xcRr2rijS,Ϟ899h$*:f >>c7bמDGGӢY"0\ }â$0П~CffgwP1*8$'G'<=mw~}ky/\[+*ycn Μ!9)Z5jѨqj9t(z=[lI/J%aL&nbt:||ٶczaay!;vMpH(ޜU9s/hF#GDyw+`^^eQAb1A*w qN Fp:vщ ZFwKb 2R^jpf 9b4[6à70sT\ ޢcܩJ*̝WCBxaDVcnjfG ]^i3?X;OZ2}a /ֵ3{}zikq(,sǕ{ʄk:uPhڽ<މ.܄ILLEB9b8_z=)Ub)00kӅm+gŃζZr iek BY1TQVU'\;CcZrDGBSS݉gzr9krDP|1&~DX vʢ>ɷ-uFOmw^w|=- RazƮ̳/Ҵe;~6<^Ql߹ѶSW~^6kCY~=Eێ|BcL ` ,\ϒOa+غ7>^?qq6X fsIwG ܜZkդj*DZݸqӲۋ7osG4Rg5M?-[KjX4nԈaÆYԯ_'eS^Zfڶf$&&LQyD݊w෍ܩ~:uc=EVygL9Lx8f3\A,Z0"zJN4A(Lu]igijbͨT.9r>ؘXz=<ܙ4%F^?qόEG֭șsAVEEEs=,/ݺ>ouN<YMbb羇'* Y5^"ظ8n݊b8;91,""o/HS'NՅ[yV8pFMͬ/<<7o0Q;;ӰA}9ojVWcmZǹiρwQ?3 UZ"D)*,Xrv{z;) 6E"* HGt6&lXIv01n lc`Oc '\+.g˯fɷL~&oe 3?2GmmMmęP~$%&RQQ$N1mnڣ-5LFcN^FݷÚ)- >N;4׋kYB~jU[ę-Map  w}VXS2[?xO/eKz<EiMRxH3ip >} + T$ n]s ..$5n4j5{+HKM $ŌށARQWWLMMAӳj-9qxa+~;{djj>΁cV[ND-CzXFw>~Y@RzMy<ԪvqwN!2 N'NgCpxKNjQߤFsqõеk.;wwXkjiBi vJ&;ս B)[Njr"r O鏴Kr}N<lܴΝ;Ȳŗ_7)?ֱbڵABnHkn/Ŕ2dp z6l؀륤PҒTv^|?Ԕu兗_g :lc4 't=4SvveeeË/fc58z<;v {pޚ6=>!]Bu|>ӉZFP^^磶ۃh %f6X48,\Ho-f37nt/kݎ($$&R[[Ƿ-fа?jލ磪:|:aBBi|׸\.֬]ݻGsBD0&aM.r;m)p4ZH3:L?qcFq5ג7lΜŐsyp_9s3~"~NYY^.]0pm}$m$ӊ9v"/ |IL&S{̋xg6oPZwo6&'!!/˯k5n"y7W^~i3Ν;1{w Τ?rO<}!{)~e#N{[o H;^k>}z3;o%)13;3ji[{|jIY#naEŤz:u{+蔝=XS"X48CM3wp#o Byygp=c1ԁ5N:~ǝwߋ?)Idgg6/5y::쬬CmK˰J5ILM) ۰ejJZT*U׾dYn$ hq̑0|8}MrrDMJW>C{1I3*eMm`11 $&$4fP@5iAMIrX_Ye+}ILY}p'֒̋/  hI=}K9yI?֭[GIi)19ơjp:yNx5kRV^`o/rwr]wdff`0raZZkҹs˝̼ 0o)Bh+.mpD؍0`lp 牱BD0&x<| .|!GOvҥ(*.޻$.|GU;T)uj5{_cj M@|\<ѩS`{' gsA?Pr C%&B&(/3n r1Ȟf=Ϣ/8z}ao@?8 `J|w}v{xY**+ճG1}̾^**w|`M"wރVm6j3_ ZZj'okXB*+Brpedff7{lܴt/)`G 2yxٵ+Ԕ/*~Uk朷QTtF3&޽$&$j˲FxT*} Sس'z~k~#bK2;mv[=O< ncFG-[Q o\XkcZ 1cpplGpc$#WE?ž MQZR)\t[ $Ibi9c:sޞo`=~K0~, ?O&O+Vk\8={ X >ӦN ~g<Ԕd?djG{qcгg^z?);[ѷOo6mBAa9]}VUUh4 fcf1y4?V-Ϧt 9A*E˯̌tiMfqO8|8O@N|S6oѣF0u$Μ1k16_{Ytx<:ix9} kTVU2qxzl&55= ±'NNa8=R -D0&adefrŬK:e2?._΍7m ,H`׮|dV}8;}zbmtU:e2l۾._9[LMOrQ__v;wKK6V[[ o}{+?_WfҥMޯYߟNn~*zYV& C?ZӦsAcOi(k?嫏lq'&D0& Befsga4yk`/R(~Z!79aj5gu\s88?7mn5.=-H)( ;;Z+fѣqM%'= LI5yߠo*i5TDTCC 5$SO8}e1FAhY?a[q2F,)GRBc!yhNNN.lڴ98& FCJj zbu|rF F|\ݻwg |Ïwqdd<~bb/:vߏFf48dge5y-6{Ν(/ +3Iay*k~k|>$&&0 }F1GuM[a#Gb?AШ6Ÿ5VXkjx:eeSPqŬKy"/`ȐR{)&NOjJ z֮[aCxN?TytᰳafΚqFNgWnm؈amw7h` !ohh@eq\X5x<vMg`n&λbRR߯>$NDV+F$ JiYmQd4ҿoozv%ǜ\p2t)iqY ڡU zU/ku5-9oϥ;n#NlS <}8 ۥLv'G$F6w\M$I $z @qqƴٽ7%ߒLrr2}naӖ-sϳEh4j\5h@ynF⨯Ƽwߣ$ʮmvvʧDJV _=j$?X9q`=W^1 Ï>V=f-{YMb65l߱Nu{7=H)|q9)qw 6lVүo/r:gcwUud z=* Jr1Θ ^6>)*꜊{;J8։4E1 mUe`ltJ7$??FC4N#ᕪ=lC.]!&D0&ĞƄ'3.OIp`U"~t|>+WcPtfӪg _ 1U0V,CYopLcAh7*8"."lv<3PD,fVT 1+KboW-u"8QKS$AezRUe%"zHI䐩;>FD D6;Slw[#Zb زu;od42_tᘖNʈY]Lt*X%1AGRX#.HjU":d3Q GV?*IW/l2X$Ah(SSU=IQeAIF "UQ13&t8Nں:d9np)vv{".kdQڰZ%zx^12dQP[jT1Ջ^n!6D0&t855z4ZMRG\UcEAla j1F8:~> N+*YcdEUWJzbŸE±JcBThZT!6|r$"SUǑ$N8HH7CO% #.4AcBl.Ñe(!F$0i"N t帱u> * HM,Ya#}5{bRkx٧bRבp>X#^UY" 3nCt7i$б~Vn]8c)эV~Mko<멭#=-MZa&Ì5˖0ҋdg,da0 y9,so}Ns€{JBm@3s?x(c'N? np,&:}/o(,>(--?ig,i.r)m!#0ᔩm]Dҙy뚫ddzs/ACFpMx0`cOg5-3ڹfCpWqʩ8x/r°|hQkpÏ?`ڙ0?e ovD׻Ĩ{ǜF8`LiD7yGJ̢(t< x}^r;gRr;gVhޱ}M IDAT?^Hy*yɧN-=ȒAg֥wY!i[=={oϝO+~fq5גK0tܙYrƌIK /`5,(h۹p{,zS'OڿDyy׷/7nhhEwKzOsxWϏ.=֢7P. B8"!&ds69)U4Uvh Yz"|>e{9w~3ΘƢ 8pׯ[o? -cңGޘM[ǟVMƏSv6g͘˗˱٘vY̘~:_}pEWhj4@pZgʕ`Z>4쒙].> V >NLB|38Ɯ)@8i8:wfԈ3f( F#.]7KഩSsuB ;;}p|߾dfdPc yyf,#GOߓ%> t}QI Mb1xШtA8Pf͆;2>-18Yn+59%$$g65H7Eh',3l6lʺ[)8 }^qcz-16mkgoe,a9K[׋sm77hPI P]m "o giUU'Q"|^ +Q@5PGG_XTR5iC||Bs:Х3/nn; ԩVp\hjƏ÷Krt2@ffFcvYkmurFA$|>_k>ǟzz#Ng`8kIvyNԁȟ%1AZU\Z>`lI%J| fW~>wuGD>^HfhL:y"NȎXB^ߛ,f8RR*RۡE±B) Ъz'٘Lư/)ކJQ"~e{^~BrYq fx%y٧8`}.]ؼ~ =;Pg<G=;vrѫg/232PT*~\j+KY-~ Likmۨ#X,,l5u?CN29[,f]vt:Zk| HJj |J>SN6V̰!C}EeU5~;j#Gl1EQ:9r(׬5Y8}%% oy.1|8.K^Aб`LVKMGRpȜ8B(;pf5׻'[NjSY]È'q<E%taok۷O2n}!V ' 7ʌӘ}-qõװuV.u%S& x.{}.BFrh'yG+?ZoM7l$N?G}=:k)ڮQH*_Ҽˇ ^NBczK5LFY?5&pHv])qKcy%{qU*5 &E @˅ӅP = Y-戏8TYkRqn;J.tZ-ܶ5I$񀞱Z|}>?5b; ZEjr!ճ}TBNL%E/;_HMN-:23&V!%)LFn lvGDTVY), vS׋l"!ނVf,׋sQUdӀJR1$Ug,JNLlxdEir}Q+ )ɉt/nKUu N'`1M$M>̩nD(B4X|l};ɉ=lztZ i)tQILcBS)VJҵK6&AYy% NZ MB R4ѭ7֠-HF84EAhh4j\/e借,$'cedR[d4 2%e|oHMq秴?>RmRg,2R~Mi^Jwt5 0f綨j4dS;w6[d4OYy N\.7%e$%a2oh`B:-30uSD gbg!Z"Y ǩCeҒ0tX̦`AѠRdIBӢӶ)XTm_}:orZ$tt"MQBS .:I:NiFn,KǙMvud:>f$〱fn&Hliiprv15JQoxp՝vmXSdFCCC-Y错Ǝ݅tL'mhptXyk2nY=BXa36 9H7KVhɺ"_kBElg:> B+|>nq7Ѯ+ ^Oaq)6xT[kLMz$YBeҒ.(,ń3mF+e[Ɋ,hȘfJhO^GNYՃj+YMfFpKV粓viBD Z @Ut5 8m4upZJGTd IZ-YYmE$>qӨ|>*ʨiѶ"+=@{aI`=qbћrck[<~AQzFMAQYjq&z:k =938IHrg6-ќ XP_HJJś[QdQel2n|-2^:ߏllVYmrcPU|Yt6{=-d"Č=:Wg1!2.Y' 2tz-/^/$ږ{n/r8댵@V%ڢZ$Aye5,l8].ˎ+HK!%9.(&!ҸĄ8 z*E颦 F84  z\n/Z̄1#MGi nx B{ Nrg`CѐV@VL6pC(ęMY߸LF֠ը7(s`FG}'^Wl_Z[`@k3mJMZC?~$ -X8xEFRi^N[C^h6ä,ˁF0Ƭto9AO74`*kJRX&=r:QR$Yn򚞚̞R K1uEiqǂ}NV),.f Q: * '[/yOD0&3>kVX5jBfz )S2RZsKE!%)1>f401yrd!}` Ѡߘ&(F>R8uدO&2R&B Hڥ#t:-=v#dp7 klvt[%B82UjXgӷS˄c $+N-0*v 3HR7E30 lٺ"l6[6 y( n"#zA_kQNqK"-W&m) NCv&ǨCZLGbRJiOcֈ`Lu Q3+7ptvhfS\,&FZA8YQcZH;""1AVG7QܟAieAڟYU9]!Hc dj}sb]eY ‘c2M%׊`LIUTֈS`;$%ظi#8KAʤJE%%NZ#tdgLVQZZ;)znӺXX77_y߄x,qqti Q1(qUT[~  IBۈo !7ϛW챬12=}lZ& ǤJ8s D ѭb:x1ACDX & 1.5LxgAc1 Ƽ&M"!b^#rƘ б\ḳv"!b(3u5h6|8wfs?>-3s1;w;[ut]϶|<^,D֘[UͷͦY;b޽1IIIiزݑMlbARE(1v)+d钝8>4oϝA2wr0뒙̺d&3g]> ? ܱ3|ѵv=x3ڵƜrt,c''=d𽧟}Q#3|v;nIil1m['r|߾{ඍ7`B+pē3jd:y{;nj31&TUUO1~N:Mmu\̙;kZ|Ǟ=FFɓip:;8mN,zASi^6cTDϘpp7ص]Oٷy6_Ngl^I %p CGy߱sΜNdҩm6_~g3L;6n6d^7Q&2@1ԶP{vsW7t$'O9Ҧvp [ncڌ?k[}N.9Fr3nM{$o8|~uu0x'9lݶϿ' S􀶴fq-su߹;:r 6n {~zrW5yg>l^F ɮ|6omwpvmleΦe(%%6oygFo7q/ӊ\-,]%䉧 3S7_ ܄)̯V߹:iI!rݳ-_mLt~YvUݺv&Nv[vCaQ!\y%jgߵ*))eW~>ߝZbIjgV^ē-_ΝZss]r> Vf ȗҬ ן.;a1[5b~?=^k4j5\p.o5GzE_|ENճGu)( LTʜp\\.RSS$ LÁNv$I$'nÌՔqotɘMFZj%%f&0~&޽zJZme_/ޜR3`4ƪ$$c۸IIIٸy 559c:??ٴWٌNm'O˯Wc Ky~YƮ| ^ouZcDA@ׂ"6&[K-$'yR^lv;^^e {}ޘ6{qdeez8K5mj8[pTEe%q&KJL2u(]ssY;z>t(wknns;K/su ՜{֙aCx*:EQ|a/r139sڐHA y>~?dƫR!J7ReصuFT*%l.89go:&͐m`sΞ(,,NN.!C?~x_UDMM _} _{MTmw:tMޓ$?+WLǞ>tƎ @Zj*)"uVGn/Iŧ ?b 5ZkjAmpף7mIv&#"i|pxV,-+ɰ!C5tvl2KNL ~K߸;wpm7)S]]MzCi >Çכ= 7} ^y]%^G%Bn+8[SjEȠ9oϣZsޞKMm+V̮]4M"InN/ 6UpعM4lذKIitWC}}=_}R4;F*++Y~#>8o`t-9uj^/i))t:B ; wH>ͼ/aÎWl߱׋ZFRQ^QJCHHH ==/ŚkVWӣ{60 n7[Ayyy%.غ{%ƍp:],K"WRՒ{7>Y0RgKqj5n޳zkjjٸqD7II$''W_/VQQQI޽=r$/kL2M@Fz:EMKMMkN|u6;6l׽!n\/YG=c0c.O\"+(Jmnwab:RO?>{m>ݫ'Ǝf璕C۹ҥ SNn':[u\8>Vf 836;g8};マߜCjJ zݺöSg?mxO?=o̙Kvv&|IL&SԟN3fh14֭+>9| y$1ҙ=]ɉ\yeR"!2\|!?\?b2!11Sv=8sΞlO:wޛ_~͸qcHIJ j3a8>h f~ku٘}=Mnwp ̺bu)/C\Nsκd&o}K#))+f]꘮H Џ֮c 9osի'y/o"{ xP|l&Vmd2Dr#^;)5vF? [b*.o$j+imOE۰e[sj:RTM^5Y$IjBΒw>j&99]"&N'< Y"̎n; wVrxࡇMogL&=ZDϘpEA8*x^C޽y>iF-GKW<MXh4\|̾& ?Ƅc^a^m*Zń6ĺ0C|Xkhpp=NKdfT[kѥS;OAk|UMGd%Ah.dpQu>hZ./`Xҧw/UY j4&xq44PRVArb<&!L*8zCފJ$I¨c6x<̍8pS 3&E Z~Uk'-:* V&AWDE:g~j*IggIZ11 mLSD0&$QON|\`CMUM-N E1Ǚ$ҽ\t]{%BMvJZ@ 7~jlx^5 񖐩vG5uu* 2 TV[IZSSfzMѧD F pEA 1c>"Ѩq5's{iHNLP]S!59x51`iɉЋ ̪ WY]QO|JfT4Cmz+M[48WV`VU`uP02M1Θ  wQ6> !$EVa֡2iIL1-SQUAѠRdIBk}o`Af')1>HtZ Kʨ#%9YkM-F׋Z %9MQ ![xG] Q)z=cBۈ`L 0nnzo!t$ޥ(EP( (bQA*vA(f顧ZzowB- %}ff9=s F:*?*cz 92Zo̮~bRVw/xɄ`jJr7kqJD W'0ID˘* d2q^Dݮ9 @|\\]ތ2 ,c&ebuFR9N4"|tbZ([nynIII5u|t-)ϊR /ejDrssmy VY _};D˘ !;7$J&pS*hfKV-9v.s6 /O #8i)5+ j4:^7S^JIR\F꬐LL&\|KwW L `u*c;u"/OO5iD:oJHH =1G1j[Jp+MAALxyڴhQk*Kڵ6Nzҡk[eN-Uޜ9ǀ(^PYgjG~s.[A{{Ӧc[ʲ?>!вmG ĎkيӖWtڃy u(?¾Xh44m`3Ώ?]wO>UN4kվ7kxxl׺CVYCѺCvC׸w81`?a7͊WҫѾswx_D]=vn W3[P}Y'F?U> vkIX&HIM?q򆖱N3q9b9L8j|]/i,Z_&1%ͥd.XċLd欷8zXy'&e`$''֜d293MvS^R8gZz:?^zu 8edfMAa2 Y{Ll"#+B|,fxyytSGZJ77"CpW))VɥX1ȿdBˬB-?oOO=R3R@r9fk.qh,Eȴf-aN=Ȓ?t>v3_>dO?y,z}t:f ЪesvW^z5̜ MtsnZ59վI爨V Lf3Κ5ki:'!!Vy),^0[T`ݚ,^8>])㇯7bF ʾvұC{.~nLƑX_6Ѧm+?ۍ3sfʺ/ $ĕt?2ԡ2we07j H$ Md]:`{)o 0urC_CpjDFZ=8?gϞy6F|DBP` -Z>s3iC$R@e_~Mtt aЃXŗkFڶi{ c#7L˗,''(0y%ʵloRNmy(o^*p&.7fLe㏱n8J$9˴yrh6󯈮9EeLU#²EV #$(nnQz8C,k]˨Bᄅ!,W&{jT!4?hR*H%8J@V fpqu(wQ5"P2--fj32b6&jَ#,4r ac#CDE*-tE`$$$Ңy3~g߇O?5?:wR$5-T&%sAݩYt9 S5r${)EE$'|UI+٤$jEմ93MBQTj4>4kf3 ^ǒϿCZZETҬi !_.g{ATz˓ pS3ʢOPTb̘ v[ 2ӭ IQl6s8-;Բ~!'?|-NL2?]¸ǃ>|eЮ- 2яd\=յs'Fe-MQqUWܼ|)FaQ>>Ֆk1Al3Y2n!&{04Sn* ٻ?Ӥ Aܵb5,lxxhtÆFB/wճ#-<Juu|HHo~!9sOmScXl9؎dwޜP&EyNNn^^Ȥ᷼.^DAae_Ƹ4 [e. }>~HNN.aw=B@.P(,\.ڮTҤR D"ڀrO wVAkAhO$Г(n⤩LJcnWeL2 ՂJˌ&$Տp;bc4iӡ 6;?W)ʸqFfA( P&REzhB  ke`ͅfPj5I[v /LƑ{+$/Ilݨbh[DeL2/!/O٫3>x?nա"0֭Eۜ+b Zljʘ 6\7 _睌At>Q!x>܂ `@TƄ%f8)2\؀s~ ­j˘(;Ljʘ Nq,*c n2&8FTAp wW+c>2&sL͈hA;͢Q)]&­e4iAlٺs * X.N`s8y4~ѼuVYs 3nIٕ <&W_k⤩;7KIIq-Λ(Qm)k-cbnSq5x{Шa]b5a9lQ=QcƖyΆ?Vh eW*;ai԰/TdV].vz*+}-c 8 <Ę1nc2w(ѹ]+;I~Qqw81`?a9ֺC[:uIn=YbClyk|Kk-UgxghѦ#Uy $ IDATg˽9s& QL ƽ0ޡ1j -v߀AعVYCѺCvk['F?վYo-]n$%ܱkgjG~t g1j Z~9t88I[3@6h֪{,+_KFݙ:M^~um:v!qwCе}\tֽ%,|}>SZDV-Ģ7!9cii$j9SO t[&MpKG2}&Nbu1Dޚ3s/]_*J]he"zij1eLyk\{%stt$f9wJJ*&19;f9M]3l+'1dg=f+N[ػ/e1b6ouw=1 8F*пWw"CA"Aףtsݭr_ÇﯝtО߷Ia̲{̖&sAttñ^H˖]0޳|Y[gú58z%h<̄IShղ9+/ͳ\`ݚ,^8>Ng8I@׳_PG0~n= ޵]k4oޔwmW_慗^E̯q8z^={0m$N=ȉZg(LƑX_6Ѧm+??+RRxu/Gl3[j֬[g/wϟ;˕1???Zm2Lnn.\x ׫KHhkFaQ8퉏O`)??_ڷkKhu蘳Ν;K7v,^4fHtVJJ*gxggԬYZy1ѹc*%i[% jiMظi35"#iPCÃHH$p3pgDD<+C&1Ԭ:t ::˕M{V/³вEsr]_YoO/sxgRdNǢ7H$ @^^>nnnT*z=Z`R)>`7N{ o/O7lMANn.&FΝP(4oCvvI\^4_pX,ԭ[L222x__J75!0 J3ʢOPTԪUE DNNiiiRI1[JEAa!/nnnHRli+"7/l6c6iux"Tѵw8z7тPU1d`2s~?_ /gvr2DDT:ho__:欜\|}ʬtڋYYYzZ@fֽkO!W`2;aArrXydeI|=?$0Ac4hֲif;JL&}l9ֵ+uHDFVjWnjBNN%bLfUIt{_P`7N{|}/0_vö'gmlu#?MV1eUW*xZu¢"<ݑo|_1ooo˽ %((wwҖu}Q*eRQ䅅oBd2a2/1$z2&ΒJDx)i^KIIeE|4o֔G2YVdgg]; \au蘳+G㦰QΑ8Hn~Uy+_ooosqۡ2/ zr??_fĽ~e:wHHH0*;my;r>l(MÔi3Y*Je{j^Ts651L/))hu:|}JrF#fR!.idɿ#L Hqک{p~\GQ$MbU͚ưbjN<ͩS6Bw;ԺX^CZ`@.~MQqձܼ|KwC7g,혭8ˌ<}&~.ljS DѲi.:kExXY9de" /+'7LNxx8Y,[< `ԫW'NpYZ4onIo똳""U|OySXkZ8=<܉%VM32  Y^xxj/nkQ#oFT]V/j|Æ3yZhFs |Ɠ?N\|!fyev W.˫U\&aC,$^xf3%}aiy;\t@7В/teg9L<5?pg[!-!d 2ڵfᄑU1fUl۶/PE {u\#ի3 \Oalc; zh3Q,_=۶ ((O>a׮Xr3!~21=P\Vy{v9-QscJܥ YcUZ9i?Dy g0h VN=X|=1G1jX7 kt:OgӺ]'tҙקOep~Lժ DVK',T,8F E-LM,w*`G6iBc##hڪ ϝ,2bj}rٲa폜8zBuk*b;ta5tq;to:g{{sLL{a}o`jǀAC8v7 [s[ZۮoOHdĨ1lۑ~u_gN+r&.VOOiծZ'%%pu5Whӑޣ߀A\ @NHJ:g9cmO[ϋt>+gOXm9jIL<ˉ*_LFȦyCR@TApTRBcvG"'Q PۺOz| f۳]f3_y͛˼ҫz\%J[ Gƹk-%hsŌ>}c,\~bXfN̉ҥU~f ЪesvW^zg8h0QJ&Y6o!..= ٺG-2&%a2Wϋt>+g … /OO=k7:>/e|Q)(*.@*R\TVÃuo#Ļ&ZR{Ę1ʹxƩ4*Qd.vs)XY[h:N%еSVfaQ^^7.8;}$>t xg 1 4kԴT||=j_1׻͛5fL=M1;0/Tqidd-d2˦iԮuEaa |1u23Nfyŗܱ#!!Tw7R2U  -3MJJ*s/O?y>ro*WgP` yz 32 ܉T'CIeք+晚FQq1zBN^~ձXR /pl W[\Qd! #<,b_'˟SZG+A|$OOOÇ1 ˲qZu0ֵ+YYpE9Q|}SL^z_K9,Eiu:ڔCPjՈ`3d`=ԏzQ4nHV+ڵ9w g0^Z&ُK>X͑8<*KYq^Nll,FԒ:9erbQXTrYZ,Q;K>ZHMMu.lmBpP+V}k*lAܵb5,V[J\Ç.@Z&>!ʄijc6Q(HօINNCc^^Ͻǎrj1ᮓ… ;v,G-<7aۇf1Mh׶5<͛7Ih=8֭Zr&.~rr36!A|G"@غm _yEf۴牧츱,?}SL2O>c 4F1-a+NѣF2wỴhۑ^@ֵ3 3Q8teFI4ilX-yڵiIxc6E?{ص'3wAJoW?[WH$zc߭{냇q/< .[A=1DFD7lO(yJ\C칽>K D&BJJ 'O&9-bJ{kpw|bERhڼU,Xݻ䫯ӓ#S'z `_b٣{yܽ+>|btD^=K~ygfeyV;A`` [Sd2/\Lxp@L&SϤyf<:dhd<Я/]v*JS&Ѵ=@Ɍ]s/h׶ܸܱ3g];]:ӸQIwγIXrC [HHLdls݋?uYn4$[AMPP X֭CNN.ԬYww?HKa|plj=B@.P(,\.ڮt_J7ljl~h4 K>vҪ}gou(7;r:]XqfXϫ/ -cBf4;$$${n b\.c|7l73J_K Hxh=4eWpH*yy90ÓO]/WڵiT2orJ _௿pK\t n!튋/rLGƲ5z5l`9~f1Qxv4_ޙᣱs?}* Ɩ۸tҙbn?S1\p'%]EEHMeL*Jo2eLZGH.ۺUT <*+%%EѣG ҥ%hͣsԎŦ߷0 s?i5ϗ}X~iB$Zl_CHp bP(xg|uB/BdΜq?q /U&Μq6y|ϑINIǧ70tQPt( iz4{jT* L rڬi +w{vˑJ:uVҭQ ]_љp]5:S: Ai?ou Ը6^1eUzB"*cB_rhܸ1yyy:tÇժԘ~/2L2]ӗ,yl1MX*bPRQ̤^:4j؀3qqDV`s?Sf tlF]\?3;xxzpQ*MQ|-nWgdf +̢bJ$ba KeL :7  T ]"w4S'D7EzgBӱ` """ѣU cylp[ҵLYS;I/i߾-?@vv6!:6͛ưA2239v mtQ>OOOOuhsp 92MW Lȶ4_jp?oQtUAn/Y:׺(zQ|' Jf:|Q`*|Au:zvHHP iE~+8!98E $Ht: :<"(&өt S؟DD:|An/b4D,,3&Y$8Ɨp R< ˝dDeLz=nގL&ۋ iTέK2"f^"NTƄ;E%$A̚__{HxX Vn'Yz׺(C*8n# Ty (YoL:DBHP ApA-c u@G"mD˘PhZ JeKM$71K0w*]>u,JLh4b4 e04ZCIfV[4hApV".-= &*cB;nJ7RJfOIMhӨVR0͘M&4-YԬxeLt#枆8r ~=>^i_JXg]`v~r0_I$B1P>2&T92R\.աuu!䩜vi{%D$C"PV;FD8kFZS 'Ys̥tѣC(?чKrR)ư[Bw)]4 F*DL&^BUSd:֥JmPG#܍DeLD[CRZ|ciS T;EKXr.6q \KTA0L/5]Jf1ArU,PZS)hkdpw',L2<z8;ΠG1ZA|:sżt5,Z̅׉1UD|B:uq.MR &V`p|9lyRT4A:Ա./, Tw'筎AթS#nuB2fN$iӢ2& ]+ĜڄUp=1fL`f[P)z^.M3l6VpD `l6r.ZbvUbʘPfӛP( \n#pAkHќp)ZG#DeLX7 U9O]J.fU^k71&M|of⤩>s \ipR={[e w.NikIh XQQVXTuL"ƪ,Ea(Oa8M!`ym˾Ĥ$d29M+GJs`^]VJJUNN@ѿ5:;tZ5+ᣱZM~AUe1W=7gs6) >(x-_ov{1rpMPbH$e^Cf1ԨQCK|Ԯuÿ=ǟ~΋Ϗ{ג]&OAvX ~Z3n;%Q f<HsĔB9fJF/B,FCjz&J771$eX} "f3ͨF ÌF#jrYUY%:ZzҮ7vuM4;L&RXf+ h֬)' msķ˾QL &Njٷk_2~xؼu+Q5kKhӂ-[qEj2|Obu ~~>( M͒wZZ:MFvmX~'X:w%(0*8ng2f2S)pW)-վ9x{za!AF +7rtJ˫MTYs鯫pP.p\㍭w 1%/f3r}{ѥ}kN?;;ڵHIIeQ(s.&]Vϕ'bu JznuRT\D$t:e7fj5zy=Oq؅ʑK".P lvS4KZlzVGRw%z2aP9RtS$9(T)dWvˉ( 2QՑɤ@^A!a!ZKmb*6mGFffffNRy!%3< FIB'tUtDKA 4AC Hi*Hґ* $!gl~,$dМg39gfvs$g#{xSߟX;ٵw<<ݩSZ%],$vy]~}F izF @vѡkܽŽ]Yf-{>zE ""EH݅E0Ŭ'$ "OH2Nz\ eRG[X%bHTh.<ߡ/y|5F=3(5zsV:Dpuv,X,sԮﻶs ʗ矫9}cҬiSSRٺWtH?4_C+dܹL e=4 II.% -7Aj DFFѢu{~Z3~ѤQ#lq>Vw$> <ɤD`=)ijF왳"(k"^C_~lVEno)ampt:: Nc@BVcu25J(;?7o_yխC:ueGGZSH\ӈ߁T>Qcy_mYv-"$$VZ1qD||϶-޳9s~6˕Of?D"vnFV-399Gޭ+cF&kybRza tjwUk~B.31l꾬!{P\Ukpw{Yz$< ^x杬PELfޤRiM" HŹ6Hc(g6p:am$")`Lp(@)?#Lptz} <'[y&FF{H$iYV ,\ʕ+װ%*:cOP^sBb"y3TΟxqѳE.S&eZͨQeر(J^]^Ɵ"١ܫV߿#F:߯\Ett4?ֲuv rN-jՠK…V͛>]}n68Z]ԍad$gL xdS M?g p-b٣:b uz*e(h"=#Tb9uwjДժb0ybÆզ&ڶ5fgbV}ѝ}0K߻w/qqqY,+kGk4%߃w)]AsӦLuq ǩNqۃ#"ٽ*fL*@y*YLʴif$\]QegRVCrjn.\C"INMC*}Hedj_:<=r#ȱcݹK-7#z5BBCٴy }zfܽ‚ 8r(gΝ);p;T@)W Z!F2gLNu׮!"#3FTEYVAnsq~qu.=X/Otz=ǎ z5lHs(*d7D"w2wNU_R%ӤIcnQll~6__!ij5~Gji݊w ё]ΰ(P k 4n*c&Ngg'?+yf\'NCNӻ'Rcgy|r\<7cnSxaF Fʕ0qʕcaf>3'Cղq~c/:ݺ7Q8!F)dQ(5Q1/F%#SCFF BARARJL+JsJ*z>X Ì{A̙7 mքg 1eDjת 71ѩC{Zj ;wƩgxwOJ%AAwr7&6s[}iӺ%0b>;Gv_,[N>_^vծUCWХsGUj!΋ujx| z=~ʕ+<ܹгG7b1;v&=#]_qBtWpTگh8x իWg߾}\xz1bڴic%AI23ayVƺ5XTZw(k07C4 D"V2|l\͛p9oه&Q(>1!&koгϛuj&66.{ҽ[W>_8xf}:>pYmRRRع7&NOrin_4jԐ-W,~7o b ;;;N Xr={tN:p'qtt|4ݻ#OH`Ǯ,l.e6lьWsZ J.EwtLҔ*f 7d̘1\~mrq7 (8תY_yns?oIJN&119CxyvW/Z0?Ԩ]SuxyygN $s/4n*uѦM {g͚6ݻlٺ:kmNʗ+SͥP M~oW<Rr%s!ԪH-Dz$g &m;IL0HÇ,r7=V_&L̘ѣ兽 /(i+mxNcx^rY鸻(F8 ֲ(w zN>}=dB|˖3*Uիt!L@@n#Mn'>ţj=ːx1#d[QE\N&hРׯpq.|JYq3C }<3W{ub6FaE*QEMU%YN{Ѻu3۬Y3_0g3..Τ2̟;1t(RRSzg/2Wx{4|i߽o'H$̞1S ,,.; V͚ZW\u9jRVZӫ'F˸ yw/4n哄GD0s-gv0õl>W\~^.uQJQ\b=KX#WEH!P\x-4ƛD" x+ Rż fR^6H-+mtЎ6lT*e>QԯWJ+GSv _n&~ݾi왕ӧ3sm13r+*3)O 4T*6lÇsɓҵؔc+@hѺ˾ZJfM=}>]rr9rK_ ѧWaWWr9b(>e|} y,*x/X>yywطo_o޼I:u-TɮU譐߃.rOLFF&!TR'GGVB\2~9{al\/MJdWٲe5b8nĺ9}lm|\f?y AwQ*/WμO7zbkQ*<ϷoA~}ӦNʑYRX,JJ9֟ <0YC&RH = >B10Y$}vɄF\xngޭ/_3u|tvr{lڲxBBCsiҨ_wn:FRnB;d޽{ԫ[WWW2228q^TISRXa#[`002{{Փu7F^8HLl ڽVi'k IDAT}=._/lH9BzռW^ZKҺuk.__]l]CsjjΟ?gF1d`DA9̟)˕h4ҦuKlƫ _С#̟i.ƍ^cLx&G@"ռֶ5M X..ܺ`N3;~Wg:s?Y~E/s$&&1gg̘>&hggǵ78r8zmZsaL&(SƇBCйSzjqqd6G|w\]]:i"2i'ÇQpC/; ~j℈1Gp" BTRY3?6(V9zG1m  a̦G`2_[Ɨ8992kt?WR0kya$b1W\^H^ԮU&BvUbbcc^س׼IԪHǎqptѣWZ5N` 6)e@lgÃ͛7?0tP4hXİEXXXe˖eA/ 1r|||#+ Q,,| /a!4m8T*ۯưDBv1x`B&thZa%7I@6)GB gg'v3xWصcɞv|0a"|{t,tڅsҴIc^iP]|*3d oKCpY߬μiݪFݝu7pmj MV8h:vyXDxkߺ9eX.G P|B9xmްNxJWRNB6&UWz m[6ҡc&OFJr ,^ qX=*ȄpuueZRCo0T*k7 ɐJd24Z'^kǶ(HRRIŹ6HcU'^·-".wc n >W>k^"._TvvTEzyQǘtlS˓Pxg,T},!ϵ3,Z0q7% _̅ԫh0+ PzZ8dT1VZ8/d,~Nݐ8TùW_{k-^<""iJ)b* D_7(8. DEMDƵbx,YEJnߡzy>KD5d] U1&=9Xg" x]J,ZB%HQ_0/~UxIUNS(0- 4W;e0+&Tf N*Ts(T}+V`*&~UYi+PDox#< V:c@Q%egL@@`J-[ts1۩GKI# 0\qGGˊZJqxzLcU`M.B#;'wҰJ.WjHMMAp*~+ae=EƁ GGʗ+E%kTC&=k5^z/>f I(1g1UCFEҔ9khfb(YsUØfS ( L&x#r\(D{0%=nJV ErJxpUJVoqOW k$ q;z*Yj`q*a P)FbzBw1#(8A֭S6ECp^,L`4 k_6pZ%kD Fx1de oQ]^%@[ɄdBb //"e߰J!̊ <[NBQe|[=M`4`gb'H9F)k)t';'~֪S[fX B]1gPgL@@AႻk dsԦ#jxy&! vGGQ+` QֳͯV煠[7:h =zt+v_׮cүjbڷ{PNJxw*VșQVl^?%ruJ1۩G6WѮMk5z+a#XcbD݃M(jZ:w쀷wΙ&OcG(-KyZu1 yR[43 5cf$fDSTEEz$-S犊;wH`gkKjT\ϣNOFYk& :Ǝ)woD:xCbE:V޻W;=`Ν2?h(oAmv%H=7vI9777V̄ާZUʅGODƍ3k*2_۩GxqɪsD*TI9u²9z8ʖaB'1*e 888г{{yY܅ ٳlmU4mҘ]:x0Ĉ-:b٤pP:#cyI2*\Ɉ^Lu+K:#Rr'ag 2vmRjΞ;ϜNre?SjV Ϝ͝xzz0mGniϙcf,$4O.븺:3fzeYÖmg>֮/"559fXmѦ=qqqt'sf|U3gÊW.i޴)+W#ddf~&>=ىu60z<\,wҎj$##DNNY4 d4,d3Ϻ9vs/0zmؼNf< ɉ!{h)d>|l(6lBxDNNٍ5j ,۝99f(j԰]LRRR;=|+=vNG.wKYK~߽ HNNc?qqkn@{T}la_ ;vw]#SyT@Tt4߯\E`jᤩtЎwЎ6Z.Bj{yl9";dՙ$%`2!l–F;bJ"넀H$B*"8dBաPquu`ђ/߯/Ni|W˖[.?}hRk:Μ>^EܙJ#ׯ#Yx!|azvFȽP@ӱso{oٙ;K91 /׭,]%KB*g'7mFATt yqARZzsuaqnn{]0Dzz:bx_Q^DFF{cV⵶)_/>!/ۓ A`ggc*Dh\qYf- ]oIFF<,[Ņvjj!Q*TR\n>X,\:=]m1Ny`ogK*U8{W_CRRREb∍k(rԮ w!5-\.G,Htt4]:wFNZxҢCvX2ĤKjcbhݪ666Y!SdCLL,qtjʳd<,^K88,Z%˿k6^hh") y {;[?xi3H$M6nOV,|w;D0Qypw@,YKޤ=HLJfmL*h0VcS?皢S5|8t{ӏ(/W 0q8;G..%ě?e{rJ J,gggֶ󦤤뚝>ykׯ3y|Ri}iNAD"s`0b2Mr0XՆ=tҙgѰa30 01GvbP*_oRRRQ)mJ9'%5}XaRHFƢ}`>uz |s{P(U*so.ѱ}VaPP0_ʧ3~Xʕr[RHOOu]gL@@Dk{ Vɧ'R8`#Sf/+D"G{bJѥS?I-Yh +ujsUfɑӦ0cΞӼiSOFrj Z.L_ddY=]:wb S3?<<ܱQr̉g7VFd2T쟸J-Q]1}&+I)9QLld#33D"AN^'i֤1e9"Wњ |A:Zg#IŹە׷lWv-6l͛u6oprt@&SEEvpG;!f'%Xƾb> ZшX,&SA,GX!>>uFF]J \N!Ϝ5;ci4MVu]0E<_~~p!tň:~p?%wFn|4y˿V('N2I%WqD-o Rc0M&lR*1 EDb4OH"!! '-\o2xȟRJeJx{{ϺILJ6fL` zϏ~OZZ.^qܽR;!zvZpOJɵk0 DFEݝ IOktއG GFéUCG-CPQhg6%>>/-'1)Ν:E} 8HTTԯgWg<6|kDpf 5 LB!'66Hzz:nhJ(rba N[.JS6?e9::{h[ϵ3^^s?ZK/H3BAF zhؽw->K6{$33;whB+W`1Q O>zuzn&:: r]afL@hݪ͛5h402?؋D"MVZBkPҠ>M7&11NQٿ5;=MUI (dv8\ n,i,ɤ4jPA\z;[[ԬsUJZY۴n#jٜ˜Y5L}>aḺxyz|9!'lzO?a3uLV];&B?&)M81yBVѩC|M6J%R,ZAXRf[@ٺq}2K-`Ƭ9i=n.= DpR/W>1 o<~G!!!'G':oG7yt"W7kL.~ݺf lYsiʔ᫥͉1,1̞_.C`oƆ/_xff@4n˾f>w_RpC" *[ B*a/#O@*hM'Q,|7n`݆4nԘQW8{,ھCC~&<]:u¶!!:d7l᣸0b;/\MV}BoLXH;p 4ޭ+ڽfѾ7æ[ٵwڷkJD*g%Fu9x0b@AԮUߒ-֥e <<<=gzo.O^ *fDbR2vvH &WG878MNg1L:Xih4|wзOodO&…?q;v^%9 _{F`(DjP <؊0mNGi Bo0g ^yLT*E&7TcH$H$bqM$؀ !zh4̚|gs8y'|f^.1_0Ub<v˜d$PzL&꧵,?+l!iafL@ tZ]9v;w EfVʦ[ӻ7|6TƑG9s1V1@"pgNڗj>=V;!+g1~ԯWYQj,YFΘc9WHĮݿӧwO^kRSپs1`|ҩsoL02뎝'OEF ire˲߹y _J;h@?0_ݣcqs&*%5[anH'OE]sXcsi!Ku;1@:-'EpoԆn$%XDTyYCJ q`l\RuwwsNp6qhYb15h4ɈP I/qu)(2qR~͏K( s#OBY-.$S(d! `=3& "BA`jnQbu^m7 6Eicvi:~xe{HIM>Ftk.@$"D'\ͥN-D잉 1Ejf i^nL´sMXHkL)&tE j% Pud2666FHHH6^Z[V蠥ʖ%6._o7gBX:*V}sFg;ٺ&n޼E km.ML&ň2wt)qYB <茙\KCP"O[. fxd2hv/^]ޭ/_3uru;ޭ+lm4j;[Fԭ[T*a*;ԯ#III8uÇLdMV_t2> ʖxns+Xesb2a2Az 6ęVu֦c/w+a^$L"x^c7Wql"+YC! t1r&J̚9D1z5Nc'c>*WiSOc6=we24_U;992kt?WR<=4q+WrN0ʖJ;uɑCGG55csZ5ֵ k~Zd2E(\sR5Ķ($¢*ĘITm'[ࢮ1YUB .Bg}~tzd2aZ^. d>gز;b[ IQoR}nԬׯϯ|u|n`蠁*ykiи9mo Ɏ8yA'«KIY,s(+#}c#lg ;!ڴQWKB=Z;9fNK+Ӵ$(IK?oǛvm۔XO#V|L\@Y^A4m2;/t08d\|"o T]TvoYP w{!)bTqUs'Μ>Ȩv4m Q@@њl"*pM_ruIJdb!h2qԩYrDq9t\j>6nފdQ#- sM<==X֩ شy 13J|8V| 9Cp̜5Fos1St{=絰g6퉋[>Y[4㻯W\Hh(]kquuf̨ѽX}Z |pʗ+R#0eН;L9;w`hݲE}߽ǬOqm<=4q<Yˏ pΝ@-HO篳gXԬhQ6mٖ kWW;wdD` {dMddd"?qVz=ql޺'N0CNJ]ٳgNѮm[NqǏfПLx&)))̝U700GxnaMdj4h6Qlظaa8991d`9r8_`G>qow7 OGt,]kK["22 zvfZ޽V^ o'NŅ0=Ȥop%?)dSx1^3*EʪkŦ5&a4"lQR%##р2H%˕A&Xjr% d0n$֭_3huY3O៫hѢY$b1eǶ-|B-_VOѻWw(sȋ孾}87r9j%=ޏ3wl㟫NSAr},]EKbiK,Zܩc4mҘϿ|ltڅOp-^Zࠁdb4_G0ht,?KD"ʵklް={޹3.PK,[ڐh">@s9Z>;zOfL1,,gjr2"Lk1Ԇ$Ժgf]؞V(mhat idg9#MW_AX801TJ^k__o.\T>G͛6Aa *:dBgЀ1q{1<|ɠU=rzZ1;[[jլGͣiɆؾuB]6<4ST\pwQncW"$4ƼJkm[S|9.^||y&ԬEreqwY&TORRrte8ŧh4T*s,VVX=2 uzzytϕM%Z5drIIc[[[V7 HNN&!!֭ZaPЩcظ8bccڥ3 :kev\VAׯT)TOϯ|w;D0Qbq8;Ņx_Zm3|"(Q H0%/3YHygg' #&n!  .$濥I{*9%ZR@7A.#HOh40nBr2N _a{ڥ3ϜaÂjlJY؉<|dzO;[5kբ9#MSh4G;GǬLp{*#zC& -CpP)m0ɭq3I' Ȑŗ ^Ppbᆯ2 D?aoWzN/II89;ᎍ gN .,vvvdffb0H$ g`OzF:zTJjj*Zj=-ED<$95N\V^aKDFF`V~5u̘#3Maƴ)=yӦx{_Օa9gi<"H= aJnnM :aae9bjU{^Q Ȁ) {Z*;+~7zŜVh(##>trt@&SEyΐ֩] pmnݺ͛},;32tHHHmyh d%Qsؐb.ӓڗM+_g<ã*nM^ z)*ҤA"U vФWJ{IvHvfC\d̙3s'{s+C3޷BaeL@@wDP&x/Ow Z&bv;yҽq1QLFѕ:AUK:w Ow7ڴjVemiu:>PL%z̛çSg˯+pusoHhC$A2i݋Y?3ޔO&2m|>r8 VFD"Oi3g72|FaCŗL5Z5k:|zopwgG|W5sdCyԮEXvԧ?̘ Wxc c>x3?'V<5z^寊0e ~]??~9{{{*2)]}Mf|>~dWj͕ȭxzxT;5?__n%$a8'hO Q .\jي싇fBп_K\p_эUkپcaaqwu1tƚ?j ~>LQ 5f'"r57oS0ѕD9b8+VF{^y{wrnٲGaHW*7^oW=8::ҧKWS핅/2k\\]]Xd)~hvߍ9b+VF{^\ګX12|. ףvZ&eʭH$ڷ+ Iˌ I{[ײji W77 Us*krvUJ8tzx|1Ǟb1A5̪/"M%)fu#5;-Cj^wqbb_EZ \]}b N 0YcL@@౧ / Gs[mכKK!Ѻ𷭅uuI{w)Ѳm+~{W{aCիgǓxQ2[FV*m /r&{\eD<%>3nS&Oj>60Wi԰aoֵ9s\tH˖-,@Q1ǞuM, Qlϴd2 r,sm\G*zjGw|4mTj~ogX~6d-ޓȦ?6 /gШa\ѣ9z8a:P?GӲW7L>W>$C{U6: kטsgϝՙE^/Uj^Ɣy( >w"{R1X{r,?޽^d۶jwBΝ/B4E Ez=ĕ't%oeE5?ћqT1 LR?/ ߻X8Ɇur\|uInjQ~ȻE]TdTD"̹sD\ζiޢ)/Aiڤ1Ǹ 2hz<=Y37KmS&Oٓo~^֯]W_e%q;~-YBM.ٴ~-Ϟ!0eL˟7c%TΕwތH]ҧ׋&hҤ!ŗ2o9t=VLJdù[RVM<==3\[HDNn.ArL6!V}6̜2 sӖmhތˈYnC^xxs)6̛#3|1w6}zd?hL\~a9wFޞ)'d"!0_,^Dhhh)yM4!>>|F˩hݲy\Od6b8`h1+:z~73y rJ1X{,E"Μ9Ixmp6mfFwsMvZ@*P88`kmǣVE)BꌛuSi ̖s>{;2 ]^E&P؛Gl+<4ڠ.Ұan:oe2H\hצ5VV$%7no#xsG'?IV>4WqP8жukz=W\#1111 G&lT'Nʴ4k҄ƍcӜq_J899Ҫe js9V"%5D L&eOޞW IDATe 6n͞{Q(iִbco+ѣF!I_.P{QSaS&I(UnL`j5jwD" i4T"YNL"l222T*{^O+++xF > 5yy <ii=J4lPZuBj2*s5ϙ3 寰vZ9z簱gjgJJ*it+R,?B7fPu 99!Jhؠ>wu iNW,]ASt} {;BCBedfeVjH[m_a$%%d8|#\SY YU )&`.;v,E%onjW_}UXx>3d2Nez'L*CՒThhؤCvR-6%.f"kk+$ :tdw\1]]\H0ďwK wz/!''l=MQָuY 0f666/yL/hmoD7^fV R6p[*IIV6VVH!r9RPg@>lٶ%-<ߣm[6b3 鴼?RgfeAvl4"\;Ңy36mƋGi޼iS*1PޘA!''[R) rrsM43WSs RN/҇! P<ù3߱_/\̱' Ѿ]: 2Qfďjو|yJ%õQR753慃PRRSٶ}}zA(qQj<믿r ͚5G(r˧i&^>AQɩT|Q*8::^4s~殕3f}9;;ggSJv6%M:g9y>F=<ܱŖٹJVN6"⇖44O'MȱއkӦJdA FT*%77ZFvggq>)uncXˌhf$o//P*})M. Y|M66*Z-4;e@\v~NH\_ j'Gd2+~9gddfhÊ / 2b.]+ ۧRz::8,(0̳{IhJ^Fh4ZY/k̠@,^VvN)Wdc홫9h^_|xTGn%obcX{>tNHض}'|jUD4n܈>{aggǂs ߭RIJN6u!_%D͛L>^{5Xh*̛7l^:9wEk,(( ** h4Ɩz.P=('OrX}acm-^Gw<|Owwv2 8sϿԷR^t$%%ѬIc\]ط?,YԵxy ;VkkkDѳ",_\=|7nиQ#}ܹshZ] O\]] _<%~0MVf&5kXg.9;_<㎻kRV˩g1*V4-Q$F ӠA}ϘE``@2q*m^6oL^ҥ L8sѴi߼y˗/3n8<==D;KnZ6eRRR4lmmr R/hF25nUW~꫌||чuY>wXiۺY.BUɂyst ~un.3 m/HĂys2u._ }qTwze2}} Lc>x3?'V<5z^fu=`Ll}zbLzt%EEL4`!K͚C5XM foe!8;;i+^/MyWL}W_0q~m9C*`EBfDzzxT;7?__n%$a8w~U'6%]AiHĠ\[9 {{{tc/|դP8У{WLcX{qpݱ#: ^xg|F{Qn(fՕ !!K2~tƚ?j ~>LI.\`pZlhٲGaHi7^oW=8::ҧKx{Уֲ}.bX)30r0Vd0W6)>k]\-5_D֯1GHRPZ۲u;YYY |/*:k } CGD/kן\g {^=5Ybe)7[s7W^Fp]>G5?j5#^{Sp9{<3R9{sgB[Gc>HaګVG}{.^C\͛өcM355]KxyRVM:wD\|<>·,3/wXͷ, <<7[]\5:{ ##wy>9ɼ,^q?>yF̙3`kkkZz(Բ3ZN]΋={=PHĦta@Ւ@[]L&C*" T*-uH$$ bC$:n.Ps%o/BvQbe!QtA {<9C8qGMZZ*WEqevuV\3 7_V|>ggΞ39֬e6rlٺ]е˳th>T͘E@j~u=w>2_-ZLxu0}prrýx֡KD\|zuɓoNVVVDGG\A '<<8^u<|V \X,&??Bznp,3[IR,YvwXfiUMuW@a"x<&WjGh񷸸8358s,=w-U'((k +JP6ij|74bcnrĉ 7ZFuwws#&&󷍖Vr9k$1 g~~>/ĭ[x܈vfGH_+#ILJˋCmf]/'J"((Lׯ_g̙35krq~grrr2dO_N+h‹ G&x0c# Tx$^,_}sS[6jT%leV$aeeEhZ4oƕW I+* v=c6v6tڅNK'Bx _oI~~~ÍXԯu wjEt:~OO/u{kk+nq^"JPV-.\H$1jYnyjj*e9{,-Zn=[ɉ &ޝ 4,"8C=Z@yP Nq436#qW <(Wtu,G3É+8UiYޢF%GoeK! FkkkꆆpFiXꄄk8'|4ָ?ixc8\Kݺx{{^tOIIlܼhB\)pn]R߉TZ5 `E{oX,}68xӺUK\2gggt¹swT*=JeuZYY.+pvvEhgn! ꬬrUhˌ Gc:@;d}z09VZ(Fr*m _FGϥt~Glmlye ̙Á8}cnjٙm۷s lllS'={B^zvի1,%nDGye@mNTT_.XMB[ xLoT*Ν'BRϞ|888@||8u4PZ3QףcyٓSݝz%&&qF틛M6eݺu\vڑΎ;֭[I;2ƙ[gr9"U!~cǎYe˖1PC@@4XeF*SM7T@࿈@5k -BPk!ɈUse>27xNƧ~B ܹst:;wںvq2ծ]g1JVLE1N?uNjIQ mz",ҴIcC>3(t1udv~[[[x,~L1GHKKN:|;ԾϒT"a-Ұa}~Vr9Vr9~8v$͛6GnGbtsueرswj׮MQL^݋mwRF0ݻuAݍ-sijլ\( >3/nJf0`}vlݺz憇3gdǎ,*Nkժ;w6-3ڌtû J(˹{P hތ4.ܡz5Rb^Ӌܟr<jZ5x;"}||9>e|I#WT)=R+ !bpIx$rsppt`򤉥ʈcL@(qg,GL!=$W h& B/'㓕\n"2d0?#Wq#\SOS3WSd2r𻍭 /mCLTj5IUٳg8q"O{/OLaX[?Y|Du j:})*JF_He|yЕ2_P߇ᎋC5y8&O@mX6vŊH U[  ףoٴ\x OO~>F ̕3|8cV3~\v۶䷟7⫅1cl۱zzsC W[N+WdG֡=GcOj̞9uwK)/ƊߖDѲ}z8rN2/W^`Zyg5qQ3k3g#"rgLWr7bb>sgϝՙE^/m";֍_thF YQ$&%V`StP"V*2/O/ybRa5ǏF=BDn`On]iղW^#rZpwcؐT_q/'ذB2hk׳w^Kd>-> |X"$roꗤQrrr9{3 0&31)܌ɉCTe"۶DVӽ[t  U*Z4kZ̥<=o1wCGW~M֭cK掋1lێZFf%cϞh:Mg"We <%V ;;񿇏woq2Jvsbcnr$Ϗ͚سg/7nİ"W~/caL8#0IkGN}YA]EYm.JP/`yO;!W.m8-'ٰn5_Ο˂/Vyx5j?0~쇼X$S7FbR2c?WkTfzr#:׊=ذq37;s7c,kRS|D6id1nd2+ChӺ_| zjl߹kעضy=;Sd`}3Vr9ʊ|W^ߟޝK/{8LQOeNų'i߾}84iw[ANecY=E$%LHZۑr'=GJJ NԮIwy{yZ'=Vqk-'k2o ^1ul dЎ!$6lX!ˣU$'%s+!F#GPKY+/ IDATxS&Oz67mF:u6g~>{{{LĒŋ*dT?sgӧWO!mX$̙sL4Fgms+""i߶ 3}L*E*P{RYx 4_YxK/n:3csq1;'N"1!iS>a8990_,^Dhh}VFM|ӧNӰQÒ\;dVH͖ۘ;>s TXnh(N̥Kfp"SN!$v:"4#cQ8Tƒ ^ڎ V|m&~XfE=cX[[#HHIM%!!aC`gk7^P3wg'Zl͛سw/ =͚61^lMo3z(_.z5J_c-3=/@\D8(e܉bT*lmmt9=I);cJZz:ݟ늵5Y0nL0"hԠ>ҿ~nUQQ+AAL!JeʋJNquqZuCݮuAaoGhH2̬,ȤcXVVtֵJP=-dθښܼ<#rdeRIrJJq{HGYli+IְD,G^:Lrڵ}O) @Ű&j<‹b7C&N|t 8+EB9B&O"QESt\xGG*qN$;TN%ODаIRe+W+yWҜ2@zͪk8_r%Ndg±T0$W2__ %9ZH$BQl 2mllMg=_|E;o~&z2pT8T{pvs_8FV #3t4z2)9ww um1TDOKcj.3.!ͭd[VP`%ړ>M?DB ARr gϜ"7'QTjۙ-'=Rmſϝ.₃#իUFOX[pr Пz2qXI {tgѽsg3ndd.*2)ixzxĜy ѨaN9˧Sim}pP_FA*Z2gcLNl,s='?6ٞٹ9~\?y+I@ H)**"))%??;+o//P*2 ]3?Q-G1L֬MPP 7bX^yo6iʪkIIKIʻ$b4%F%?pΎ2__\e?P'.wJy2ɬXZAVjH$Yt\̝Kry 7C{֨tny*kR%w0;|t(&O~2{0;m.j!NP!sdؐA v-.\DLL,ى.QHJJY܌G*MZZ:WMR]{zxJKPFcL].ӥsg_@z3eׇ|G򕫼Ggͺ~EIxep*||(((@RcccFStD 4Zz <=A&"_7|]]]`t,qXO|44EEH$]\e})),,tg;t># Ο+;wY fμ/xOojL&deeELl,deT)O#ޞlپ:?KBbWD0nN(;p-[uNnzALT5LQ(qrp`Cl\XxOIF# 'XFͿyp/ֽpTG.b㎿%H$ǵXjСM  ө37+d͔Iٻ;Ѫe 2mlJ2VNH-^Jͤls|%~~>,r>ԮEXvԧ?̘ WxcK:D"Oi3g72|Fjz6n>M} ɟMO^̚opwgG\9A_:ƕx{y"JJ%+D]F.J"6&%J%SCG\ٝ .|e8ZbH~[ё>_^O̚=WWW2/Y˽^bwDEE!{ȭH$ڷ3^nwBnDZ ka쥞/4 ztS0UkfҮ{eaLY2ݻ`ݻ4pD"'"r57oS0uCYDG0z6:=ؘ;Əq1$C$HaDDf-"W X̩'_bS:yyJh2CbˍYP3D?UjԳ~3 T9̬Z=w2 TL&3RqP"<wDR1uZ‹\E2/")Ջ:!k^Jͨ_PB:=2 UŚ YEZϙ! 114g1*E∽!b_ {p7B'o`1O&1c@L15!t{(F0M-6Eu&%Y~brΒK!OJmg-&W@@lfNe'(ޢr}ͱK6G% PUluV5lmGX~ xt AӖ4j֚Uk>jLҴU;I-ۆkQ>Ͽ-Ӥq }W! Ju痯\EV+-vqU3nf׿rz{(aeL@@@!Sʐ\ 'XFrpk>XL@HM( a:ԪTeNHm"WX,FUY[@TAT 1STtD~3yL2y=L@@@)@֐pI >FnI>V-&W|>GǨ޲ /o`6n}Cm鳸| ^C:ϑ}BmmJ4-7>d4o m:p!N<Ō+=s:uCőtt|GhNLݶc'ᑫ(ߍϜsquu7GѻK>u kֺ=ƾϒ~ ![4Tu2\F!z4 {{U7!=#^;x"=+W|͸81t0AA&kPE^Iɬ Osq&k(2r5" Ç"rZ8hٲ\rJ k==uU3n%$|E8 I899ԯk:*޵ 7ݺvSXlێZFf%cϞh:Mwq{N;buMnrýpƔ/@ ! 0ŔPcz fL+lܻ dutҞjüv3e四Y2(ŢtL)MX($Y iVN!Lڇ8WŹ˩wi©sTRRD ]AسsΞ=سS&!2y7G`/Xx=̚ג?໯MZj G H~ }hxsL0\JKŚt8hc|}~|1O?,6[vJ񖅌5M̓RpEevǃĂORWϊgIM+/hFoF:e22ILH jkf>ҟW^y4z|1/^7>/h~K`ʤ<_j4ݮcV=jEEETw;0n __xKFF4X0]s5\՜N[Gf옳xǹ+yUȲ4M$-kUR_E\LL4w줴ᅲXLX5hbQ:R G Ƅ3J{7 !;Ks}7uoҴoCFa2feD3;0R]]~h jQRRʒ+Դ/V+.^93gPRZڜg[iVl_?^n-C{\}կ 2aXRٳob RT\{BCB6t}:n?n5rD0LwFM6Fۮ)BV3 t:?1Qπ5Ȉrgꨮaiz̞|L)Msh `֭ٷH~}󫨰QYUŜsga0HMM'#&:eFדGiY9<}HԮs>{!!Mep~>FF V+:J JuL)MX(Yd@ń3*mc}RTnG}Y~Dk^=Y،,{I8*h?Aeu"))4j6o橧eܘ1ִi7PQnqЧܶҬ#4$9шN2GttTa)"ϛK߯_bP*_+9dGj:.Ѿ@0zu(hX r &s󭷷xoMmbZv^F4th4ZbcǜGys|\N؜߉Q-'^ ͤ͝ ulid4p8Ph<2(14K't/_+dYFeo5lgX;}D-C0!!!\v.dٹ DDxXs .gfuQ2(14kjkl?{iA  cZgohRdmP,&j9]t%TzޤPG4hti?g~blETd^}ǑK/-25RT(**.ZC||VKc3xh4 WBTTk-22ؘ>]9¢b^~5nb;T`{).}3 Zjuwn$$PXxr22W*3X:RWWWǢ C @ фIP~.z=AӖˊT9)2Vdk7{`U 3oI 1v%۪8{ZZfXW$xx><̓*g0dLLTT$Sf̜Fhh(9Ю4Ǎ;-[2YX߿7.c\u\ky%|:V+/f:R`Y**1-̝3 q%8N J(SJS#XF4$Q ;ݒ~v HՏEբhj/Fu|RNz_j}k)knI߬#> [ڶEܹVbbbYx ÉD%`>@*/Ÿ͟G4>OKx _9W7vK1 'P- qb0&p8]R];`gcʽOaZ!B)q<1dG*?zǂI1AӜΈ~If*-;pҹ[S=D//Dm۔sW} w}ǙT9!{~OAG AFK%&1}H<Y7m#C? xBUA*26ݸJn'J՟%ݖ ]C A (1);p^;6Q <Ѝ'M?*_ ƍd7nusHa*_ݹ&Ab0&pUPWeM}!jnͣT82K +@vzr7QӭyE8&= Ab0&p O<]pvK>ރzRFiL+%/A96n$aG;5/do bA8 &y( IDAT_gDUIBb4T~ J{)=iƣ?#p\@ݸƍobX "+g& π(+8LpJuݖQNzvRFn[(pRcmr{ $F_JٲS'Ei#-o!߾Wga "YbzAV,}S|" sf܉W$I;$I=oo{0VFAVSY]w ¬4M1ZJ:%IR_Uy9bV{扴x ԉL|=' 5$!)l9=o eރp%Y7$HÑ *T* .(Fg%#4ogAA.t/ @9zG8/IҎ Q?$pɿ\n|IFCUu UU56CZQ-#߱|V8xad"Q'#;= C A~d]LPSޝzwrH@n$k2{,`0>l;ǵm'!&Ædۡ '(r!q+GQ9[U}pAW(6_1A6x,1EԄAe_/YA$FNn$z)bN $j_Bs8bypx CU t/ kLH5/X^avK$FI w.G[wluWb0"f+OAN AfPTN}*~<6UrrqAiډQ:U|{)vȱWKfb}1V㯴刂 b0& g3 &>6Ot6~|R.\a8zUi3dX3K+b{)qCgnV2*-\,G1A/.FB$ܑtsԳ%-1k3 $^;IRxLC9QKs/}zH#iGbK,R,GUb0& / GSNbb ش{{i_RGw1i$N;-^KYѴбb>J]z;$$$Ih]%b&"1A_(ᇩ퐨Pac"4)ǖ3$NI .dW7 -?vH-1sCfAM AS/F_AJJ Q4eaվ}tŢ%N48 QOkνzKz;hatEqȲX2+B !Ɵ8b(ޘG#r5:"Dj S%I!R"4f<lTy`s}NbQD_H H g/zAl݀T1&VSUa5TS;Bwv>j}ŸFԒeyVTVVQTJj%IRS1n4Ξ1[9zdΟ79osv nXκl~+w|O;F_%v?XTT^GefV=# &5K(Te Ȏ@k$&&v{^ r»3z4Fc'ޓ-sórvE:FYgbfLA24$a 3Ox!}t{5m4me*DjЦN&\L:?5_1$s# tz?YtI1|8K|0U#T'gW -ou{]NkL"1֧"E"uOcyX^ѓe:tk֑6:u[ /X HA.wX ގJo6%ߘ,!Oe }BaQ!jwYf]Y-4ܵ{Z1yg[Ho_~Œe+p]?qafyn==v ΛXx{);D|?3.@\4>` %9nRNZ>}0fGǎijgsr|pҴ픔 THU_,ξhk %lش\rr:d9HMIߦ0_tC f3\x+111ܹ^z 111ef咋/bرJYlG 㚫$==-hx=6m oq6펻Yssf93g63j=gÌiS{茶G?gvN.K-v3-U7mܵ.]>B,_.G XB;{Vj۶<ؓ\`>m;3Ocn6l܄ys>uJs}x=*7cCG(X,ՙR^Aײn/=~=(ՙR,-&4 B\61lr>'rãh~t C$Gvv)ic %%9F X$'%v)8ZdBu8͒R#‰VYI @>r!4(AjẐT*pxo0[ڽ$^ <G#=@hd}cbʤ=ʛo-cH+~aCsӟCx.ɼ\~+'SIwEwq(/oOFA%I}(,*z#Gb(/}op{曈S|,_I;v_/#Gf믣Kddd-uԶmfW؈h^F.&OFV1md&ˇ|Ur7k`tΔ⌉?!%%؜^O_JuK{ˠQ  t_\0Ę"˨7@IBL@jH Ԁ/Ĥġԛ.Ye]kc zʛ6Z9ZxoV3kmƎIz_9 ^ }TًHb/ &'o=2$$Ib6Sp(H<ix<$ Y)M^%2"IȈb;_3Cw&_SSRd41(s ,S\\BMM }B45óh~`x0Xz=yyyۗtz>ll6Ο7Fa| r~ sudֹ` apf&ZZ80p9aC;_kꨮaih4j̞_| /زu+ϛ˞}{1;aК UU9w~vQmlTWW{>OByy9q1i{:{X:SgJq +Z;[Ck2{A0CkH ᱕8GReڃuR5.:'~!lHv'jW2!hL+Zʀ^cB2SЩ?WmlڈVjY6EFpxN 1:v] u^G!t2*]Pr=q[uѫ? @e<W0or(hPlO[,yn; :$ZT*L}}&c͏fzق|z /e`L0I [oo߇i=aKV#~\Vk2p8ί5v^\g:iV6Xǎ9>Yw[34-VI\l,we\!VA\ll{ju멫٩bL))"**M\zzhMX[1Azl?=}HoRJ~}蝮>/>ںz@B-y4 )@Cb[;&jIVe@#JfێFY*hasu%|z]I " y^GQ67.~mi|4 t$I#MiVJ:ozyʀ4ՒHre8エOFVq:|Lb1cw|ˑjG*|~[,] Kp% 套_eP ¬Z==DvƆkhb2-CGMF7~O!Xهg c9x0ClY-Ng6:꺭o{wӸXlsii|n=,ӯob^5JnᏤqH>K-TJtNwR޺ZӞXSFQA8-˭a$\Dbtgu:ìY1 :WoWI w wrg'˚xرk55۴tb6o~n|b'"'7@ @ՊVCj'kv{8C>nT۰ɡl8!RiI^~A@aV+fvVaYJRSSRXRv\dd11|3\.y?{(-+ﶺnm@~~59Oiiqyvj57nF\?~rmKG=qꬣ<13& vUPóëpѠ+)]RbB%TUUѨV|q}))-Fddy4###(+/ AOzZZ)..*FC\lL:~搌Q҈0LW6bZ,P͙l+.+?ei;i$I\{oXt_@TT M͝cgk6m ёͳL|xʪJf s"n^s5K./7T5~\sՕ-KLL >(>K/|Rf̘ƻ+?`' N3vVqb-zZxs _~~EYkmNC 4qcÏ FbbC ŏɕw%%%Kq_So`tg[zz1:H,?=OZO Вe4英c|ed>?Zmt܊nnAY(ͬKL@UE6L9tzAA=4ġ&"ji4 vxB/zS; UGp6ݣq&=Y_.13& $ut=^ ZK3cHU_B=㐫‘U ̘ pƓj|%H%HD5"jpl4JɞS*%aǢwEj #PmY_ `LAy%63̨I 0]v|! U848HVb A대z3w0A~N`LA 44Hd`w [qlUexewд XDBqfjC}MO/A~`LAő}*|Pϱ3w왩\4-XB}0Ѻ"z5x(gcAN M;5@:2kD;kШ=TupOkɊp YyQŬ O2Թ]rrxGy }Ͻ۷a+* 'zvrC1%p OI[..V $YMA 4c@ޏ䦸(jUz'>M=nU]\YUEyyI X,瘤R$ϊ .Ӏ>yqٮ~{»dI:kygu-"G?933nnt=YeWz`_U~ˈ;ω:2a Bn5~ W9y8NT**6уZjJ'#HZ?#|HC@"}$/~BJuԗ;mwa̝=SMeW[p=He2ib׮\t\vvŚ?_K/YA6'p(FNn^'d[nl6^;yyyƍ7h&^/ϿO<jOʫ}dAA(ߏV푼`ӦNO>Ff@V[LLL4w줴ᅲX|+~4z|1/^7>/h~K`ʤ<_j4.ƏCyY9%%|>۲S&4!44Yċ/<y/}xb\wռrNT%Ju';,}qJ,w*ͶT*pض};cڽYyoЯo:/~_]pM*A>\~^|y|r9$ݻrϢګi$<o^s|g& IDAT) %ףG?|~vɄqc]_wR]wp$޷k +?.um\RHL`bЇÏd П2u$>S>9ݷ5ÆaL=a9cbA֭ȃ?V9{ KJrǍ|NW]SChh(G -5Y<(Q\\%_DzZó1~~-h% BG|~߱~wvQ\ZmysL< VˈaTWW<@Cc#W]yVNJVYfysu FdDy?(WWWGuu ӧMà3g4 SNfl۾hcTJ3XmQ:FeUsΝ` 55|T`VדGiY9<}ZJLt&AnwPSSCyy9Š3[} BBB8`t~h66ŶeVۋd_meد;SRku] !sbcc/(PL;٩\UU5nѽthԔd]U.d^p^Dzsrٶ}?~?y:h@x0cT6/,+/kk-Ef BGM3JZpr03w>U7͗SW[%22]Ijq$s󭷷mg;0h4M)t:4gM?J毿aʔxF)`qE<^O؜_{)iXER^`>^Ëz$7w6<Jtz tZ-*@@1Ef*Um{X-5j5r;"-71MAWxuTg1g'9\n+c_hٯ;SRkuD~!Aۯ;٩\K+v^ w{_/ogg۸.,ʢ|e+oj^'5%ժn]A$09僱}o%7G8|$%˖(jA3O4ӢdqQ8N|>oFѣF;QQicԈQJS)N%JUUUcw:|ͷ=v]10_rfgϘ]?vL;Ji*y^'@ @uMm"##5rwCiYylO&X>SZju<%<&&6޷?DEEPU]CBx <Gg^f9|aaa׳}Ns7wJS<#-5|V8IJ߷ߑL5ST\|llEEuM-{SYYŎ;`DsH>bȁ!.6]- rɲh0F;b Khhl_z*a-vNN1Qu\kZ,fKJx /8{^ccϨf]|3eRQa#'/IO^ֿ6l{+WqA̚EzZb~''66>>_K~3a8Lf7l7wJ$IRL8WI||srs˪tXn\ɡ.8< 4-u=~{\0{9dy\P4RpZg uo#11ʿ_'.6<~=6u __OG@sjmt8|W޻ArJ2S&MboLAh'\/,AѠRh4[Ou0v&z,[ `AҲu'z}h3AKXr$'%qmq `Li]6A+1ANPAA   B/1AAA^ c   @ AAAz   1A_G?wm钴j7t 7t ;w4օwQ]]Ӯ7(--;f[u]XṬ=M.`7t:ڊeᝋ|S[ehO? V/]ѯO: KG/uu;әzOiAnQtKKO|ls=wwYZ̜93g_4;kbMްqIy_PTNqvTwC[n՟SU]MHsN2ϟ]]qv&?1AnRX\x|'66Zyef咋/bؐ!x=6m oqafrrrc!xndso 5%9ዯ0{9̘6U1mwp76nq:]\)˒eq݌=ktce,]qUWO}}=,~ ?^ x#X,̝=F)M%mB}[ ԡ4#NPKJŲcn6l܄ys>u Fmqݼd7ddSV^˯LniiKMQq1aa̿+_wgYt9uuu̞}.67ؘzh,oqۚ߻t;> )]GiG{ĶͿx<~"{I:hJi*H,SAnRQYIZJ":0qVu?}c٧皫W!2__x8GRq;ƶ?cFfM%<o^s8ƏCyY9%%|>۲S&-Ò+2i""^|X{kUMSI[uċ/<,.b^|y^|14#NPKJŒ.~{5+?ڨ-@&<< FCbB<m]YyoЯo:/~_]pMnϥfq<X:r5ٹc'ƍ Z>#mfk _dUgS)M%]q  tYx}تj|v[~z{!!! 0x+h:u2o`DGEx^]]5L6 ^Ϝٳ*+l?o.zYÈ 74ΫQYUŜsga0HMM'#&:*Hu/3s0083NKMmmb1sysygy⋯QWW=ݚ˙7w.YÈ&/H~e)h|[.Mm^:ב9l }{1;av5JQG666[e%լߴ rbbsu֞+e D{f~^ ZŸ#p?KD.d^ 4aBtz tZ~,lid4p8NJ㧦M?J毿aʔAϱz4ou:I [ooqNKJzBu/c֨ȁNCm 0ܦ99yܽ]_n3II@}1Efg|;p3 i^:_'cOVs|Vƌiı5v{QG۸XvXCOg($[nzBKK&x 'X;RSQv;r$"bJMI( IM#ɒl&!@o_cSfwgx+9+iu$BኍZMRP)T*0b0QPXDLJwLaS8{|Ν:9𻜗ae.׻GѧwV .һgOu-fJKKQTRRR hKoZgedd_XHII j|RM<=ڵ}?c5]*=;'XO5Jb).jT*)2 p;vO0ud ב6,4mY;=J֑l-6vm۸FmPܽV+","oRKJ0͔q@uA>{Rr.`_}DV+F#^^2mN~A!O ==6m18xd4g`Ǯ(**bA5FPoڄl!)9_^L~}>P6$(9%JfV aæx%|4E]κږŝs[~ůvR.]D۷ʦ_cX YYo{e8^}O??_L;"u֗uCeuV'%22 Cv]GHNNgLZy\0Pܽԗ/;? B!DSfZZK1 7uLN}j4DEǧJ M@l.e\7HK;o ȑ#|r $"tCBBxᥗ)));&éSImÝS&eU*Æ@Pp]w1b-be,[n`eرT:#FYZ4 gޜg\ClfFɓn#<,e8;L˭I]NGm- >5ލӧwzdxVS-^Vc^:XS:=SN!6n _[-FP{y>L]{n5Wuɹfѥs ?_#,y0Vrz#Eʏk?^!N%IEVT*Qը겡RX gB}Ls0wӄ޸bVŭbC7PpӶېl6 |强z-k B!DZ̚3V-[C4vqhtϿ$77gbNzƄBp# !hX1!B!h !B!D#`L!B!_ B!5`G#S !5Q2 B\N&ēv>NϤB!SL(B8Ӧ];~=t!߿؅B!u'j5T1;7tg7gΞ'k?_oׇVw%hhѢE?ruyyzz>>IMzƄBVRRP**a*zvLpP`pҪ읥o߱V-[0P*XV~5vguF.mzQS;5rFoKߩ:WQT߬9ӐuW$B!7JV1_oCKJ8z$F xO=ó^m3gX~ڽ]vvfܚOٹkNyrssYh1]t?>Ⲝ<+VvO{׮.t_tV3L&f3̚3Iƍl0v(F Vl>nY'Obe,999s ;w}Аu۾s '$ҭkW,f3Oģ3hEZyVƮsLL4nu=wsg?MHys<짟$$$c挣ݱ=?eYmWƭs zږyyk9e2۵fXxf~5.\Tc*O>a;kHjX;kg7nbb۶픖ZywznqI9nCl߱Kq1Əeq{)m5&,",ԼT.N$$&*}L-YRzDGs읥,{g@ ;읥t7떼___vYV *u7ҿ__z} 3g};_;_|D6y}"&n"%%%( gcWV1l`j",͕k?ҞmBdEYd_lIFf6ߣi4I;N(^ݺG~/]ŋL?ƛ  !}ѷׇ.:EVv6999dffqSL ZcFת5O eijLm*IHpzΝ:J~~YYY3~8 =Lb51|}ءÿh׮y*44.ɷ;wNhXYi AVKZ:˗gFQVB֝6*j&1Q+Vvw}ɓ'iѼ9۷ghޜdfz]FhhLm :6dzB돭stolɬ߸e0~ ~X<3Uh[RaZ/@ѠVLj/QchijZ4[ykP(˫?yu>4sf]YM\׿__֭Ȅ=ǭi<ʡC둣]%۵ z|̜yP$((7ܺ=i/uQS~EfۄZb1Q֭c˖-2z47IQQ'55͚Sl4Zq&~G^zk2=!GQB"3Csc>>>Lc S‰tԙ9d47_UNl1SZZJ:Mb).jT*)2 }G~A>%%%osr*%%@B}\#;+cWs ?)S%GBCٖѣnmbh׶upY;v W\kEm.k%%|nӧO綉ٳ'љҧw.B\s1nw IDATuFCrJ V,C\Y'$bZ7vkGFCYifd^|y1.2ر;ذ.|1 {f֭[_UiX,fSAPP[ jK_^}d!呜C ))󄇅y\gi^kgj uij[2q#x7F;͎;IJ:ř3gݻ'O`(AlӳGFo?쳗\{oGgV)ҷg|턄8{ 6nk׎Q6l(;+6o>t:=;wbĉ|B4!uh6є3kHvޞ#Gx*=`Ĉ>Y9VBf̛L͉m'K30nhM{2X2[c02~XR(}םƭ᫯60bd XmV#SN!6n _[-FP{y6 ̸+غm;AAϙ#jnRBb"ƌv'F]08YK=4[o%&&3<>z8\wInՑ#j>Y}sʈ{ۄO?hLm],߹g4LhHpI)̞= &еK4 qqqtܙj[oc)0qJ__OQQ! ,3̞3e?BCC)..'##oE-0[,Krss {K ㉉&&0EQk͛7G[dzsw|1͛C>}b+ m? /п?"#"'hZu_H޽zһW.Fw,;mTf5|://{֜{nblhN];waU̸^~Gk*&w},~7үj>>zƏǨ>]lqݻueϞwh߾=3N[czB&s !5Pi)]S5 wXVݫ7aoV7 ׏g`2Vsz/`9 bV-^{M #<!>BE6=? Ӿ}{ϛKmmj}laUl*^z1摨Wҵ[>3uT6Fճ +VХsg:o^|M7?^C(QRZBI6Bdd!uky{1ڧPJ___ !gͥBX+aB'JB+MQ!pA!B4gL!pIX !3&v9]*D!.ORfer:5VCdl^eBqR#S 7ŭZ|iKBN$}(B),2깄B!GJכ!M_6iWS(@RRɟ^!eҙ!B8P(lzvm"9pɨjtlZ-OBQF-S !ذa3iqdQ ㉧0.-!7& B!5^OZWXX@JFz\ ƄB,Bʥ,Bͻ||$BQF ! @RѡmΤtVCV4vфB\#$B!@` ZZFR5vB\Cd~]!B!h !B!D#`L!B!cB!B$B!BF B!B4 ƄB!H0&B!@1!BP=,IK;Q> -᧟vWym7ۘ|I!n]b19'IdD+K;Uq>sČ{eO'bU,Ef3Zw'iyrssYh1]t?>bO3nͧ\ $8{O#UK|]RSx*Rc2It/?.999,_?еkgtQݱ=?eYmWƭN1󔳶l]eN%G晧$(0:8k/θoCl߱Kq1ƏypΜs0wӄs<짟$$$u~q˸1X~MxZi:86}:}͌{^e2LQ!hfK1V@TJ%a!A*r._rl6>rڵbɢ6V>\ $$} ?.NTٳػo;_>Ckԩ#kҾ;읥tͳPfח^_Œ{g_`jFm8C޽9{,.^0h@?yY[r*dfe3\bଽ8jē<`>k?~==ڙǬ&ή!mڶE/liU=9|G׳cB!Dx{@ɅKl62)2[- ޛ?ɩSIdeeqj4NHp0'i]=h4RATD$!Auz:wHXX(w%.^Ȅhݝs$'',nA0vh:Oxv=rW4 OMT/( zȲwֺWlμs}Guc޳_N]1s֖\3b }n7mʳ!λ+o]:uۋ:G̝c戣…\`-jDFFk tVz/fp3Y9nW SB&&y m\ |t:BkBC$9GRѣP55 SLNNAA28Ϗܼr$((NW5e+ Rj#77ϣ ZKyg4{~ި^uJ`Ǯغ[rs())&Ѝ^WNX?AǍaȠAuN=cա[ O{ׯlH׵t̜%W `4k<⼻b4VY=>ڙ;G||t*iV-9KY]ZB7j? Ƅ I=BI=(ו O'dW ZRdC赚*e ? )))A.[e(Wg2V-rEa)--ERQXXHIIqO?՚SIɬX0)Lc 'xӹSgj#;+cWs ?)k9kKCW9xy9o u=y1SeCW [uk4/,f],Mguh(*,DݗZ I),(MvMZ,mڵБ\III I)giuN. e毱X,?x,ڷm{Mg4 eMy仸d4g`Ǯ(**b h4$`Z*$S>j|Z"## $cSxk0k?[ !DGpCsc(Ou-s* R`2Ա]9B3e8nNPP?x?Zbe,[n`eر$6n _}#cj1|sV™7ZhN]xyβ9rWb ؟7z۾.1119eM@l.e\7sf=}ٽg7GʾwɩSINn;YwcuÈm/d?Oഝ9;fCOٴkbb`)s摇`+6F#'fEiuy/p&>!ָ~tţ^7EʏϲB4ACץ>L}xZFVRRX gBC BQ՟ob-~.¬ !pHX-I$Mٳ[k\r*F4ܣ@ $B&ֽ 1!hB!`L!bŊ<ߦP\+f?O?zΝ0~X(..foϧc?-Zp`~ɹgջǍhC7]qB! 3VKjZ֑NRMeСD0\xl6kmƔosN*^> 2?K1Xz?)!HϘB45e]c*##ooo|zҶAnn.+Vd6QIEsD J0&H0&MP}L&, zy~V(JwƉxl:to-] JQ瓐H~NNnOg 9|"VG6kلB\$B3,, ӧOӱc Q>< 7R|B!KrϘB416ll3}4֮]K^nol6Ae=DgNog-rڶmkM1t`ϝFîRӣG 4oѼʢ{))|a^ބ@rJ aa4kV/Baa!| >NeQl)칳|у֑`2@ l*۶}KǎQ*FsKj.O?`𣤤JɀB@TP(ZEfvBcBфOOYf 2ZM^0 $$&𿯷FdD+:tha?Ϸ۷3|P:v숏^]رsAAua;K4!HϘB,b>ɄPsoY}:Ծ}X,o2ΙQ7YUR̝Mf$'"6t|}}?n C }2v-4?;XSO<?nu3'N$FNxQTN(?G_AP` =QQ>b:=kyDaIϘBZ C{1wO?fOˋ3s/t}ǰCu;]Դ`2N~A&PrrrF{0kv^9x}aØ{-* B$B!@EХT*A \?ӞGz6L䅿{c󣨨鑇ط?i{T7.:9/##VK8@BvP(Uw?ݺtaϞA!zS ggʯl6jsƍL$ֲ}|tWB2jhQT( l6HLJ!;'oo <gWM߽lVGDVhZuŽ l6;̧`:ǟCOHVt)Bb&El-b{*22Ge WT4Ó}^tVApӐ1ۿ 7RZe RgaB9X}T ȪkJSa5qUBb)KJEdDE 'V{ńBF,NzDW}jXSW*) l gLI0nt%+{PU6|MLFN_P@ !S/rW{€ J0&tU9yhcB\ =lB!uc@8Qc5-PU&ה.շkB!-jM -aĚ>Ū_!ĵObB!DH%ʿє4G5MQy*a }\SB4.B\˔/&B!WP!B!:ec@! k\p^6f>$3jZOt,\~]]ww%[p˙xyke];n og֜L6nfcF1jHu_q/۶m ?Ǒcػo?O?=_|Iaa~?ig͙ѣͷ=1]%b>!)9qcF3R+:g~@i&<Ŋ0f-=O<'BCB<>>s%ae=3vLZyVƮsLL4ni::NtVNGm߹ֵ+c'xљDFjӶ;{~.xO?Vcѫyf]KxOeNr">b)2߷zwvn eq_,{DB!Mּd7S}1ÇөSG~}ݒW.`;KMʲnF[/a=/3:KӕJC~xXF{9=$$} ?.d22?ϧs.5~ڽÆ,R >!y̸Ͽrڵbɢ6V>\wz\qTNg)JRR;{{}vӇ<.:b0qǔɬXfc'3|@ `ElÆ f Kl.r^έ{3(E)B!ʢhHLL$|:mxG)_:u@hh))nWui]=h4RDV3|P}L?||||ءJNvZ .r)#F-&$8Ȟfq>C6QO V->g+2xmVV7FCXr/*" :=;u$,,\qvݳ'AAAGdff1z.!33Mh432z&<={AmUo/rϘBR+6l*n>˗ٳL˿ `1\_0_ozͧ\t={< RiY8>t,xΟ4uZ-bV+J">$j[*ɭ2ypQ9嗝0BRRRZ @II)uer˨ٰi=:RIa)--ERQXXHII[eK܂gۋJ &BQ[C?DII 9deb4Qdq)3 FvN.9y~}3~&f Iɼb]@~~>* rsض}/STt^FCrJ V̬lʲ׉OHjo4]Q5ow줨 \p ZwgnÁؿ_n۾B8Azz:mD9-K``!!lش"O2{޳Oލ-[ s/&}O̺u˺-Bٴk, $;+mzrz_C\ua#6n\%2غ[|}} ك5Ld4g`Ǯ(**b\ u;q,ۋ SB!sv:r-;˛0Хc{7۶Ti(̵be,[n`eر8OJ⍷޶@wtڅ-!00iJjj*,{9`Ĉ>Y9VBf̛˲6qqkq ??Ǝt[rGi:ch٢9/b2 ??_gG8r\g]kѢ˺;FR1l;* ᅗ^;>zӲ<,x[نhd/];ng+Kލ;Ms)ƭueH:j!zu9P(x`ƽXm wٓ㊳rz'_-[e*޵Gmqc])6ML0()-|m0~,~CJtnS(}םƭ᫯60bd XmVX}_s^8+*wˮϖ-qZ!ndgn!bZ?kZ-[SfSZ>:WSVB --kc8*n O}-l'ꢗ!xB\*3&B!fZ5g>ޝ7]Agz([A(Z`AADuQD[YÛ/z()˱r*ʵr(Mz;mҦ@MK|<I33O g3OOhp#lܼ7v={bB! GTw|P=|w':]G\Y)2!B!WW&2!B!7%B!B[L !B!D}1!B!hWcB!B`LC!1ү{緉L4&s[R״t{̼oI}˿\o:ZIIMc·4eNg߁ RNENn-/w?frW_ϡG)nB_rN&=# FM"B+cXh8ؘ&%XsӨם? xGtה~@ _x%K5sfk}޳Hf"J q|޽ӧ{il^|yy{wK|9~y} nՊGħ|/Ԫ^[k}ZK2mܗc֌iDGE^_sfe!F%Kj-wn%=~C0j 8>SGGS)*24kC֭6c63M`00cL27[Qj-E!1q7v^2nL\K))1~bbvn݁ѯOf<-pެm۶<3n۶aM_m˜GG7?:q`Ѐ56w>ƍynݺk3gYnٗ/cbchrU]8um:}c緻пqj%#3ks!":/5m |?xidݖt<+V&??d_1~&ҌFX )??=NvܶRJ zgڵkɔkIMKϏÇrWE>uuR*QQٔ' OO'N`03iz8Sy 'O3Ow~ΟOFTrLzn"|,b͓&0mbb-]BVx/&=#/Wb͌{1;e}XJJh׮O<>ZEjZZ}fxatpt,zsro̡#G(ѥ݌HE?TߟRJ {ץ]/BqRMiON@X`40M<4|>>9}*mZ1̡G0M6eY??}i}͚5E ǡ|'K ǎĜ3yj|ϭEoחsflbݻKFziF#6?HԪ.$_䯯fشy >>jْ0''%33˗`hҤ1UCp̲R>c0h͛ŸޯLLFf&b!p^2Xgӓ$L4W_Khsss`AxyzұC{ $Us]W4G7vBds05H޴ۛH,V9+y<7h4үo<-ccBŧQU~FܕYRRS]3 FMyy9 b# 1h2_+҈|w|\/&?|||0V]}VS֧T8Z;qd=v]8/StB!D-q@VѷqѨ7I.ro{  }ҊKyaK޿vi]*\׳1"}гΫ\}}|)..]jڰg~w͞k$IDATVF`ŌϛiN{ P],Ef3>ިժ*HH @x3Ui..U_=rlƲ0xZ iS}ZB猡PYٔf=Kt6M <ȕKldJ'2jӉ w&#>2j)k5kdAtT뽳Iiڔ4bzJy1pgWMtء{{g}RVVJ>X^JI/kd &B(FJ"*< J^"s1EbrؑR]mZ8 EE\6MRb+/_K*{xh-4?Ɠ]\ox{{ӥs֮@fv;uu^sl&~&&M|̊U+S]L !4j5r^\Rfv^=I,bsfMGѸ-ˋҲ2v;JՊjjL0zHFgӦu:oxi1+//ҥ>7ʔNpPKm6ytHl6Y/Z#?ru 襔 Ŋ!8"J,BC繰Ȍj%" we7ɈkMb<=I dsgO32ۉSؾi#A~ @Yl6YTӽ/Z !7*MFF͚Ÿ%00mێbl9͜)=UfF.""]ܕӱw,V+_ (_%kL,\6g&ahK9l<>4j6g/Vm"zÇz,#bޛ.ZhѣGz:| C#oo%b>Æaud_F1h` B`qX𧧞ӺhӺMG*Z' ӥ3;w&mθ_DFF07Ud_ @10WT"¯ A9h}}5r;ur[fmMJbЀdV;%%3gדqϒaMV<2y >'Oro-⮶m} zr}شy v\cV˥K_?+#poor!~~qnl2^:uzmUU !d",;[YY֬Zʟ~éᗷ&8Μ9˧/gwrrryƫw[C5B!rL;[yv~I8i2GuZ]M1̪-UU8'B!nBO'OZul a8xױ*}&V^sm7UVVv6*[,r%:ÆT{9nY}"_S}O>d 5^S}DBq%cһG7?I!JEYuAm[-ww1Xi0”i̜6ks%:uȄcvnvKAAM4'ǡjIIM%2"_3ݖ׬}71uJ:}8[w`--e~pf̘6/3mdY-&="),2Eڵc}9s3g];JVN9 MIhd\HNˠ^w?x0S^xަPRbạk޻ O^=k<rg!&iӲ9J@YY ;ؾ=͚ǐf4w8ٰy C wc/;v&㏇y~,zu8w))gfЭ۽,^0Rɉ'o[<Yh#8?8kϝEoחsfly/'y c?_Pd6T*p"3MCh2{tc5R]}}ѧ4kޔ OqWdfeT۫ӽdgf4`@_H__̈́'Ʊi?oҡp8B! XMHGžKNqvsKrJZęˁ~Otu0;Æ Elsj5lؙ=B7S^xAƃ}26///Z_n~h}}hۺ5 yy5ܩAAA|gҿ_Z5gtzxFC+򢰨cCR$+;, ;'0 In%##I1TKL&h݈6[\|[b"e>///4i;o-t[SՃ]{y;_ZjIHo*! 8 B!nPyTj}{OaQ?~z3yp':-z]ƹyx>|}|0pP(0Ѥqt<.^eF##zm2^Az)Za*_,z=8 Ev3۳o? f+#00Е@IPPޕ`”*_AꢠF>*{ZB<<=mQFRnwܖXf|+r[]@\|f?@Ϟ:]__o:HQ! FJ"*< J^"s1( (*2`,9"`0 4._~A:]qf(,,b!4$6(TJllҜ'n~}׶mtp^^^neML,11M9!VW:Cs]>?Ɠ]\A_ms׾tZfl6js9tKJ*r;K.],:wT"yٌP \Y3&B!$G=a2G;VH祵T*zvxs6[FG[^ypw1ΪS(4Q sjl7tQ(*E!B!gʆ@!B!~VL!B!BQGv;?+o~~>_+gڵkSc]B/Y3&B܄TS:mZ6Ã==V;SmVeqW{o-uVlj+mћe,[ҠO>Yh#8?lۋ?v 7~v9tg5|~W4j5VXtLjj*Yl9|[cp86aтy > f g&+0~,BAAa(R*P4t B!įnj-h`G>\ruedf $0gtzxFCɩ1OVv6YYY <OO:vhO`@gIRR P;E~~>99ӓ*ZnMllP\222 BQ^nǁZE^SXT#O5#pj4vٳo? f+#0Aaqq v{9/Lyyy9onc XZ-WgtZy#Fh5 *a2C&BFJ"*< J^"sk0f..@1''61i4dVZ]c>?Ɠ]\北Gd>{1mZ!߯Nqz7ZjJEII 6[Ybء=+WԩiFQ|鴘l6j *+*,sFn!!3bB!D8s6r 봮9oa6QTPPPH=X$Ev;9Y#^OXXlۆZʅd^sź$v;z=62juٷ¿/kEVӱCoD& U¶GK]$ލO&##fb\iL4On!O ʄBզE,Nbxhծ-,"001h4tهL"qߴUk&+WΗ*{ud_F1h`ΊUWfu =0׬} @nyϣʭ.x\%%%+GgqX𧧞y4 |l65!Dm)m 7~  .`׾nl2^:rC^[UUBz`2 k0oд1m CB)8S]B!IENDB`Projecteur-0.10/doc/screenshot-traymenu.png000066400000000000000000000314411451344070600210550ustar00rootroot00000000000000PNG  IHDRi? sBIT|d IDATxwTNٝK&.ĒXK4*MO 1f^5*F!6$JKg) wvyݻ ,8~>̝sOy9wfw3  `5C1X9w>QNr}͹TC{ sq96Dq.r`6:$GXgrxK\bN3挓O:gY,g-[/ :v!m`FD'8,JƎ|Ũ9g;K0L>}a܆9brk̿)P^0 Mٶ3.&Bə< 13ĠŤuyq1r, PB&j9,pe[91BeȓYB8ܱB5NǔŢb*Chpq9Й 6͘3jp\I. 1xi:-xC:qd O=ȸOhqƝ׌֙9'Irr8brAErR5 ඖӾiQvd#cEO3YolʣF/r\ըwc(dKe,% e3/zSi-M3K?74N[qI]f宕F.uҟ5RTs9A0'c-mVוٻt2U+u/g r3]0]9<3_k`6k/d2%ۜf{?/#ma28hAU&D FmBsM%14Td2YRݾ MkV28^VRAcebK*![gMK"J Ys'R: ^Q^@k-tKʽq2)rp^qQn)0W)5ԁ5|9DNaݸ݆wǠp2058{(KKVcmILGktg D r<~ZLob%淈rִesM.%q@iJNb J=h)x5źM[x2,\)6ĹǎA{ g*,J\.QK aALMmc1i=3T)JĚ0v 7a&~`a`<-/~`ÇqԘQݳ'8Я .ÊhK@jvӒafp K` c'zQ9AvssL*V(d슛1!69w%N, \ 0Nu`֥53aΞ2 ~P0 YϾJv&)ʹ zbʅj; I碚Le{)A uJ%67,Lr];8$qij%e;3Əoi~knÍgOÃ/ +R4`Π, L)a&W{S @]v1~*&4}>zY\ruaHDTAQ<4?7l@ZGu2<R::Q+tHpcB^9x@Gq%?@L8_UYӰupv\t\1h6B.aL銜Iݶq=k{, 9"Ig[˫>rM!.^i9kw8)2kƌ{)< lH R)Lrg1ҝ cd {UH Smp٥'ޤ Z"b]oM3[ F0ZT"mWDQWU%%#*F(mGuM-jj '! 3?΍WEok3,3;r^pUZ{T1pQif\/2=xuE%n*& #1^$`pw5Np(OW%kV؂j5* 83=wadFaULDr2R(' ɉh(svQq ô0g6TNyv)PleTwo:pD0=AGɍ>J.jRC͍inz湯]Y썃|ӛ2@/ll\j Ę#=Y&¤ ZʳDt c|U|v-.~RR"V!g! PUUz_]9MMxW|M1{A>`y92nǹ.O)H17馍R(Â֘RQ12k'"S|K%e,#rq} ,YZu-^f\%M@YF_N!uΟ~:8Ix7T 0 k%眅sw* {x9OCN9ơ16^ ̌1^"ϾL>{ Gg:X|s4v؞ǠX} зH d:b:9(`pqpu1.- wdVdFcbpX+ˊV .w? w9OL̉RЈe+Vy9=9؉g5̝{QW_\s[bS_AESi %4t)o1L]7Loةw l\~L$LEڻlιw{,-xwe1/tؐPX:3RNSuHGydfm97Yri w/pl-F{zO)8Ajr,fs)akB$)?sCE]xXΈN{y^oj ӏ:[iIhstokU麫o1fͥ0a8cq"*g6:\V~zԦ'! q7߼ƎB(§WnnL=L?يUXq3Əh4%Wa}I)l &G6*>)0su%Ŀ8Ց*aa1\}cX$PeLE)L$ ]{oVs)(M0CCGkXFYscGD-MjZfJ^Vd;# yJڊVwu?BFZ*7>9mh#fzh*cj&Q0 b6lپٙۻ Ǝ:yз|o5un!TXT)&&U=`"V69gj. 4Wv=n{"`ƽK,!_rw?m'.WBgAsMsF GsTK@ˆoZXGx͊䲧{lT~▋즣qD XդW>ҧOuZ0UP، +-AMu%z}olEM8=~ F jW?cLJBK dhh]!dw1`.~:m7 9ZG ex 8` ӳdPVU^=pIcq\A~N>xdaοGiU-"9HNTʦr3z\#RN2ܽW z_a)R1isZV8]á *" Wg~E9 cGL O6-?\qfģ3=:h8㰤>glsffV뼲fֿ9PSVv'~v,kH7lC,5HDx@qc`\4ڸW; Us&N9rG۔I&T*$]!xa %GFkc(ް 1X 6ahqdf#q^ofi20qqBsU1Q&,A= 0Zom|41tƢs# 9ԲSKMt~90̰ʖj5=92A"g"\ ]3wäWf-gS^?%l<'5OH `)7ZzFlp47;_d B8bP,+fI6(gLmW7w"0TW#g|nyú](*mڂSN8 sxhdlFJɕ2.3_f< HʙRFOFP8:"jqX# _*Q3peN4\Hi6 k+!t}>򟏌qyg4f}xp w,cيrʝc[ȗ\c62Ktlu_>T5)Nas K_57⇧ 8IV]KabTFc[;c6RA%0l@97Ia@k[Ͼs^{TcBa땽C{^u(GFnԀ dG8ixmd!f\e Qk4%W>CLe̔Qke7[g<`V;CqBN#Zy}L?b5E9y#$dwӛs$38%G9w]ƾ綵2hæ^w˥uQni#P)D.CƩL1zpd믺ƣ+\_ś}Hy1pb;L9ڣh]BD2zYJ-) ̒lyP+vaI62ǀtL1WyYjox]:,/w]6zٮBG4t}Bm k}J"@cіRQeu\wW?t_T'\Ή1.AY8r9`Xt({߬KشnUFXetZ31SMd}wr 4!£JOEFz2ukhD]}BRZkbbT Iwjd؛y͔p6p'ag\mnQl]Xm\\["KC4wv{OF|#+3 2lJbL~< 09VݙǸrdng'ӣ!)Jus;~g.:[ZMzb1L҈j}e\528*}NXJ g5 6PgHғ ό|4&90pإpLYuk^+Gq0~*^Ê1d fE  UO7 0qR #cfFYV2hϤdp!7#'*3sig~@N/︜3yM>otqu3:Yk !M;o(F1t~L_ X GN^Y).)pp񐕱;(0F/̝=C\d̵,tY0ǮfTbCx~cOM-6c "9 7nDcSSww Cwcgi)2zR쮯.D{Om]*WPP($T!!;+G GGT _=B!ZWVvs "6眣u Aa` ^Ԅq6J-9RWw wò,쉶vص :.7|۶}N6r{ )n0(ٱY ]jX %;vIN^  9]fvl-)A(9=rsγY99HDh .1ֶ6lݶ ) |>A2|!dC ‡!'BO>Sݙ-[C ‡!'BO> |>A2|!dC:ɽأګTص_~VzRSq5Wc"W`欻FHwu7$L87\7[QQY_u1ŗ1B?5v ifsz b#ƾ޳?|l96mނX[XHO1S&ObΝhii?|u1܌ʪ*lڼ$N|I/7 C;fyy`E]ﱍ}@۩E@,QzG,C߸?J˰lJ۰୷QS[v 0/7fg-?}#]u,pC{sxcGӦOk7X$LM{LJ PPs^r_/ݯ~o |q֫ Cv=&uuutr\~B8#CzZ^XƏHiii(虯"~q=X$L?<<\p/~TT7n']q¡v*ŜB}C֝t2$V>> 1bpl+َ/j; n۸+O kaӦM 8]GU L,ÂW^?Οp8L YbPMAnBt 8mkۯMO 'ӧcYg;'h4JzOt۶my瞋_z 6xwBB!JDFuV\~e0yS2(B[[(֯_s΅me2n!!"7p~ȑ#ko%mHʕ;"dD"^:˗{]wމd 2|[1^tR ԯ G׈_!=5b۶Fa6x 8W_wvN'ByDҿ|{üy0{l|q͌y,B8) ɑ~'B>m 4-]w݅T#GⰑ#lgf"9ev&$M[:t(-Z<8zm̘1 3g paYA$Jt >vxj<̘1ɘ;w.?8dge+"֭ۺψby騨‡㈑#' $jhh|+} 茪ƶm;e "g~>f? 1]pДlقH8 '9l\ܶm"=- _YЦ çZ rrzheN9y¼1ΙSB!\uexSO߆޽YYYxSO<0[p1ixsy-y$O0~55شyst?~_`ݸ6m_s5ƌ;gݍ]vw߅̼k^xe|Oqyc]:ulљ$쉁.[)q v:ُwo :v҉xV㈵p1tp⠒0?y*-E}}=RSRfZ),D=SЀ*Fa^m֯Wb1-.:HDM8 ܿ\_|iscsp$gef˛nv)&𳳲[@ †8NaXQ~(c_,[@BW}EC '6vhD"def^ذq#jŸaƵݫ))\ue> ‰ ud=Z iii钱?y|ɒ~i_'9=zpp{T~kq?F= Ǝ9.7lt䓥1<1a\5wqз[Fg|;/!">A 2|!dC ‡!'BO> |>A2|!dC ‡!'BO> |>A2|!֖Ω~] [oO#!~^ ;XJ ‡!'BO> |>A:KnBPwwG28B>A2|!dC ‡!'BO> |>A2|!dC ‡!'BO> |>A2|!dC ‡!'BO> |HBA q:&O`箝Xj5=,0pYgEQ]Ss{LCD|˲pN:/~_y`A=@zZ>Փ3]p_`v 'O>~fRX 3o}9{W\}z$@E{62zI,ÿyv*"K󑟗L=~~ Æa歷༳c҄x0[l<q0IPINFAϞ5q?_W\ [X}w]pť?@BuϽW8H 9553+E, aUUgefLzZ ! >lza7`hQ0JġEB>+8sյں: wͼ ݻwcDw0 PSS?κ ǍEnN))D(BCC*pq!##G)&ޚZ iii#a =íw.Y y~7 W͸I$s֙xcnjƜُ^6%,E̎kA׏wFE6c;`@gTuPðm[mdX6!A2$LOľCO> |>A2|!dC ‡!'BO> |>A2|!dC ‡!'BO> |>A2|!d-jK:chmn{A Xy߶QYQAOTVTw?X cΪ eY:Ov_aNq0$hs | >A2|!dC ‡!'BO> |>A2|!dC*H+IENDB`Projecteur-0.10/docker/000077500000000000000000000000001451344070600150275ustar00rootroot00000000000000Projecteur-0.10/docker/Dockerfile.archlinux000066400000000000000000000013111451344070600210110ustar00rootroot00000000000000# Container for building the Projecteur package # Images available at: https://hub.docker.com/r/jahnf/projecteur/tags FROM archlinux RUN pacman --noconfirm -Sy && pacman --noconfirm -S \ lsb-release \ file \ awk \ fakeroot \ sudo \ tar \ pkg-config \ gcc \ make \ cmake \ git \ qt5-tools \ qt5-base \ qt5-declarative \ qt5-x11extras \ qt5-graphicaleffects RUN pacman --noconfirm -Sy && pacman --noconfirm -S \ libusb # makepkg cannot run as root RUN useradd builduser RUN mkdir /build && chown builduser /build # Allow builduser to run stuff as root: RUN echo "builduser ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/builduser # Continue execution as builduser: USER builduser Projecteur-0.10/docker/Dockerfile.centos-8000066400000000000000000000005561451344070600204660ustar00rootroot00000000000000# Container for building the Projecteur package # Images available at: https://hub.docker.com/r/jahnf/projecteur/tags FROM centos:centos8 RUN dnf -y install --setopt=install_weak_deps=False \ cmake \ udev \ gcc-c++ \ tar \ make \ git \ qt5-qtdeclarative-devel \ pkg-config \ rpm-build \ qt5-linguist \ qt5-qtx11extras-devel \ libusbx-devel Projecteur-0.10/docker/Dockerfile.debian-bookworm000066400000000000000000000010301451344070600220710ustar00rootroot00000000000000# 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 Projecteur-0.10/docker/Dockerfile.debian-bullseye000066400000000000000000000007011451344070600220620ustar00rootroot00000000000000# Container for building the Projecteur package # Images available at: https://hub.docker.com/r/jahnf/projecteur/tags FROM debian:bullseye RUN apt-get update 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 \ libqt5x11extras5-dev \ libusb-1.0-0-dev \ && rm -rf /var/lib/apt/lists/* Projecteur-0.10/docker/Dockerfile.debian-buster000066400000000000000000000010301451344070600215360ustar00rootroot00000000000000# Container for building the Projecteur package # Images available at: https://hub.docker.com/r/jahnf/projecteur/tags FROM debian:buster 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 Projecteur-0.10/docker/Dockerfile.debian-stretch000066400000000000000000000020711451344070600217140ustar00rootroot00000000000000# Container for building the Projecteur package # Images available at: https://hub.docker.com/r/jahnf/projecteur/tags FROM debian:stretch 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 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 Projecteur-0.10/docker/Dockerfile.fedora-30000066400000000000000000000006541451344070600205050ustar00rootroot00000000000000# Container for building the Projecteur package # Images available at: https://hub.docker.com/r/jahnf/projecteur/tags FROM fedora:30 RUN dnf -y install --setopt=install_weak_deps=False --best \ cmake \ udev \ gcc-c++ \ tar \ make \ git \ qt5-qtdeclarative-devel \ pkg-config \ rpm-build RUN dnf -y install --setopt=install_weak_deps=False --best \ qt5-linguist \ qt5-qtx11extras-devel \ libusbx-devel Projecteur-0.10/docker/Dockerfile.fedora-31000066400000000000000000000006551451344070600205070ustar00rootroot00000000000000# Container for building the Projecteur package # Images available at: https://hub.docker.com/r/jahnf/projecteur/tags FROM fedora:31 RUN dnf -y install --setopt=install_weak_deps=False --best \ cmake \ udev \ gcc-c++ \ tar \ make \ git \ qt5-qtdeclarative-devel \ pkg-config \ rpm-build RUN dnf -y install --setopt=install_weak_deps=False --best \ qt5-linguist \ qt5-qtx11extras-devel \ libusbx-devel Projecteur-0.10/docker/Dockerfile.fedora-32000066400000000000000000000006021451344070600205000ustar00rootroot00000000000000# Container for building the Projecteur package # Images available at: https://hub.docker.com/r/jahnf/projecteur/tags FROM fedora:32 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 Projecteur-0.10/docker/Dockerfile.fedora-33000066400000000000000000000006021451344070600205010ustar00rootroot00000000000000# Container for building the Projecteur package # Images available at: https://hub.docker.com/r/jahnf/projecteur/tags FROM fedora:33 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 Projecteur-0.10/docker/Dockerfile.fedora-34000066400000000000000000000006021451344070600205020ustar00rootroot00000000000000# Container for building the Projecteur package # Images available at: https://hub.docker.com/r/jahnf/projecteur/tags FROM fedora:34 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 Projecteur-0.10/docker/Dockerfile.fedora-37000066400000000000000000000006671451344070600205200ustar00rootroot00000000000000# 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 Projecteur-0.10/docker/Dockerfile.fedora-38000066400000000000000000000006671451344070600205210ustar00rootroot00000000000000# 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 Projecteur-0.10/docker/Dockerfile.opensuse-15.0000066400000000000000000000010521451344070600212400ustar00rootroot00000000000000# Container for building the Projecteur package # Images available at: https://hub.docker.com/r/jahnf/projecteur/tags FROM opensuse/leap:15.0 RUN zypper --non-interactive in --no-recommends \ pkg-config \ udev \ gcc-c++ \ tar \ make \ cmake \ git \ wget \ libqt5-qtdeclarative-devel \ rpmbuild RUN zypper --non-interactive in --no-recommends \ libqt5-linguist RUN zypper --non-interactive in --no-recommends \ libqt5-qtx11extras-devel \ libusb-1_0-devel RUN zypper --non-interactive in --no-recommends \ libQt5DBus-devel Projecteur-0.10/docker/Dockerfile.opensuse-15.1000066400000000000000000000010511451344070600212400ustar00rootroot00000000000000# Container for building the Projecteur package # Images available at: https://hub.docker.com/r/jahnf/projecteur/tags FROM opensuse/leap:15.1 RUN zypper --non-interactive in --no-recommends \ pkg-config \ udev \ gcc-c++ \ tar \ make \ cmake \ git \ wget \ libqt5-qtdeclarative-devel \ rpmbuild RUN zypper --non-interactive in --no-recommends \ libqt5-linguist RUN zypper --non-interactive in --no-recommends \ libqt5-qtx11extras-devel \ libusb-1_0-devel RUN zypper --non-interactive in --no-recommends \ libQt5DBus-develProjecteur-0.10/docker/Dockerfile.opensuse-15.2000066400000000000000000000006271451344070600212510ustar00rootroot00000000000000# Container for building the Projecteur package # Images available at: https://hub.docker.com/r/jahnf/projecteur/tags FROM opensuse/leap:15.2 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 Projecteur-0.10/docker/Dockerfile.opensuse-15.3000066400000000000000000000006271451344070600212520ustar00rootroot00000000000000# Container for building the Projecteur package # Images available at: https://hub.docker.com/r/jahnf/projecteur/tags FROM opensuse/leap:15.3 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 Projecteur-0.10/docker/Dockerfile.opensuse-15.4000066400000000000000000000007361451344070600212540ustar00rootroot00000000000000# 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 Projecteur-0.10/docker/Dockerfile.opensuse-15.5000066400000000000000000000007361451344070600212550ustar00rootroot00000000000000# 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 Projecteur-0.10/docker/Dockerfile.ubuntu-18.04000066400000000000000000000007531451344070600210170ustar00rootroot00000000000000# Container for building the Projecteur package # Images available at: https://hub.docker.com/r/jahnf/projecteur/tags FROM ubuntu:18.04 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 \ qtdeclarative5-dev \ qttools5-dev-tools \ qttools5-dev \ qt5-default \ libqt5x11extras5-dev \ libusb-1.0-0-dev \ && rm -rf /var/lib/apt/lists/* Projecteur-0.10/docker/Dockerfile.ubuntu-20.04000066400000000000000000000007571451344070600210140ustar00rootroot00000000000000# Container for building the Projecteur package # Images available at: https://hub.docker.com/r/jahnf/projecteur/tags FROM ubuntu:20.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 \ qt5-default \ libqt5x11extras5-dev \ libusb-1.0-0-dev \ && rm -rf /var/lib/apt/lists/* Projecteur-0.10/docker/Dockerfile.ubuntu-20.10000066400000000000000000000007571451344070600210110ustar00rootroot00000000000000# Container for building the Projecteur package # Images available at: https://hub.docker.com/r/jahnf/projecteur/tags FROM ubuntu:20.10 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 \ qt5-default \ libqt5x11extras5-dev \ libusb-1.0-0-dev \ && rm -rf /var/lib/apt/lists/* Projecteur-0.10/docker/Dockerfile.ubuntu-21.04000066400000000000000000000007371451344070600210130ustar00rootroot00000000000000# Container for building the Projecteur package # Images available at: https://hub.docker.com/r/jahnf/projecteur/tags FROM ubuntu:21.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/* Projecteur-0.10/docker/Dockerfile.ubuntu-22.04000066400000000000000000000010251451344070600210030ustar00rootroot00000000000000# 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 Projecteur-0.10/docker/Dockerfile.ubuntu-23.04000066400000000000000000000010251451344070600210040ustar00rootroot00000000000000# 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 Projecteur-0.10/docker/README.md000066400000000000000000000005041451344070600163050ustar00rootroot00000000000000# Projecteur Dockerfiles Docker configuration files for build containers used in _Projecteur_ CI builds. Example for creating an image: ``` docker build -f Dockerfile.ubuntu-20.10 --tag jahnf/projecteur:ubuntu-20.10 . ``` Images used in the CI build can be found on docker hub: https://hub.docker.com/r/jahnf/projecteur Projecteur-0.10/icons/000077500000000000000000000000001451344070600146735ustar00rootroot00000000000000Projecteur-0.10/icons/cursors/000077500000000000000000000000001451344070600163735ustar00rootroot00000000000000Projecteur-0.10/icons/cursors/cursor-arrow.png000066400000000000000000000002531451344070600215460ustar00rootroot00000000000000PNG  IHDRm PLTEtRNS@fPIDATx^=α 0 ѤA"9Oh؁-k^cK?m :V-@Gêeΰqo_NpoYք^]IuZIENDB`Projecteur-0.10/icons/cursors/cursor-busy.png000066400000000000000000000003111451344070600213710ustar00rootroot00000000000000PNG  IHDR g PLTE~OtRNS@fnIDATx^ͱ 0P ~)~6idJWO.l(@ X`@ tJ!9l&aqlQ] ~#-搫I 8{4u4IENDB`Projecteur-0.10/icons/cursors/cursor-cross.png000066400000000000000000000002021451344070600215370ustar00rootroot00000000000000PNG  IHDRm PLTEtRNS@f'IDATc`  P Vpm=XK "IENDB`Projecteur-0.10/icons/cursors/cursor-hand.png000066400000000000000000000002371451344070600213300ustar00rootroot00000000000000PNG  IHDRm PLTEtRNS@fDIDAT[c``aL(=XzjБ JcZd N Ѫf @&ЫV-J7U%IENDB`Projecteur-0.10/icons/cursors/cursor-openhand.png000066400000000000000000000002401451344070600222040ustar00rootroot00000000000000PNG  IHDR7 pHYs 7˭RIDAT(υ_tW&DNt^a'`O#9#V*~Wa`'-$2d_=J;Ҁ>x0tbIENDB`Projecteur-0.10/icons/cursors/cursor-sizeall.png000066400000000000000000000002561451344070600220620ustar00rootroot00000000000000PNG  IHDRm PLTE~OtRNS@fSIDAT[c`\ f^5P00:H0:%BD&pÁ f/0watêNXIENDB`Projecteur-0.10/icons/cursors/cursor-uparrow.png000066400000000000000000000002041451344070600221070ustar00rootroot00000000000000PNG  IHDRm PLTE~OtRNS@f)IDAT[c`L+ 4W kKC驡 0` B7~2IENDB`Projecteur-0.10/icons/cursors/cursor-whatsthis.png000066400000000000000000000002771451344070600224400ustar00rootroot00000000000000PNG  IHDR g PLTEtRNS@fdIDATWch` ! U+Ai lS R #1B[&-3VHMa̘e,1TD֫6@D8 SÀ$W/qIENDB`Projecteur-0.10/icons/icon-font/000077500000000000000000000000001451344070600165675ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/.fontcustom-manifest.json000066400000000000000000000075151451344070600235550ustar00rootroot00000000000000{ "checksum": { "previous": "fdceee78baa623d59e66194333bacfa5e2256cd9a60fdbb02e780f8c59cfda1e", "current": "fdceee78baa623d59e66194333bacfa5e2256cd9a60fdbb02e780f8c59cfda1e" }, "fonts": [ "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": { "codepoint": 61707, "source": "svg/iconmonstr-arrow-73.svg" }, "iconmonstr-arrow-74": { "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" }, "iconmonstr-battery-4": { "codepoint": 61697, "source": "svg/iconmonstr-battery-4.svg" }, "iconmonstr-battery-5": { "codepoint": 61698, "source": "svg/iconmonstr-battery-5.svg" }, "iconmonstr-battery-6": { "codepoint": 61699, "source": "svg/iconmonstr-battery-6.svg" }, "iconmonstr-battery-7": { "codepoint": 61700, "source": "svg/iconmonstr-battery-7.svg" }, "iconmonstr-connection-8": { "codepoint": 61716, "source": "svg/iconmonstr-connection-8.svg" }, "iconmonstr-control-panel-9": { "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" }, "iconmonstr-keyboard-14": { "codepoint": 61710, "source": "svg/iconmonstr-keyboard-14.svg" }, "iconmonstr-keyboard-4": { "codepoint": 61711, "source": "svg/iconmonstr-keyboard-4.svg" }, "iconmonstr-media-control-48": { "codepoint": 61719, "source": "svg/iconmonstr-media-control-48.svg" }, "iconmonstr-media-control-50": { "codepoint": 61720, "source": "svg/iconmonstr-media-control-50.svg" }, "iconmonstr-plus-5": { "codepoint": 61703, "source": "svg/iconmonstr-plus-5.svg" }, "iconmonstr-power-on-off-11": { "codepoint": 61717, "source": "svg/iconmonstr-power-on-off-11.svg" }, "iconmonstr-share-8": { "codepoint": 61704, "source": "svg/iconmonstr-share-8.svg" }, "iconmonstr-target-8": { "codepoint": 61712, "source": "svg/iconmonstr-target-8.svg" }, "iconmonstr-time-19": { "codepoint": 61705, "source": "svg/iconmonstr-time-19.svg" }, "iconmonstr-trash-can-1": { "codepoint": 61706, "source": "svg/iconmonstr-trash-can-1.svg" } }, "options": { "autowidth": false, "base64": false, "config": "fontcustom.yml", "copyright": "", "css3": false, "css_selector": ".icon-{{glyph}}", "debug": false, "font_ascent": 448, "font_descent": 64, "font_design_size": 16, "font_em": 512, "font_name": "projecteur-icons", "force": false, "input": { "templates": "templates", "vectors": "svg" }, "no_hash": false, "output": { "css": "output/fonts", "fonts": "output/fonts", "preview": "output/fonts" }, "preprocessor_path": null, "quiet": false, "templates": [ "projecteur-icons-def.h" ] }, "templates": [ "output/fonts/projecteur-icons-def.h" ] }Projecteur-0.10/icons/icon-font/README.md000066400000000000000000000022531451344070600200500ustar00rootroot00000000000000# Projecteur Icon Font All configurations for the `fontcustom` tool to build the projecteur-icons font from the command line. See: https://github.com/FontCustom/fontcustom The following files are necessary * `fontcustom.yml` - basic configuration * `svg` - directory with svg icons (currently all from https://www.iconmonstr.com) * `templates` - C++ header template ## Install `fontcustom` For details see the github project of `fontcustom`: https://github.com/FontCustom/fontcustom` ``` $ sudo apt-get install zlib1g-dev fontforge $ git clone https://github.com/bramstein/sfnt2woff-zopfli.git sfnt2woff-zopfli && cd sfnt2woff-zopfli && make && mv sfnt2woff-zopfli /usr/local/bin/sfnt2woff $ git clone --recursive https://github.com/google/woff2.git && cd woff2 && $ make clean all && sudo mv woff2_compress /usr/local/bin/ && sudo mv woff2_decompress /usr/local/bin/ $ gem install fontcustom ``` ## Run font generation Change to the directory containing the `fontcustom.yml` and the `svg` and `templates` directory. ``` $ fontcustom compile ``` ## Result output The results will be in `./output/fonts`, including a C++ header with definition for each glyph/icon and it's code point. Projecteur-0.10/icons/icon-font/fontcustom.yml000066400000000000000000000002351451344070600215130ustar00rootroot00000000000000font_name: projecteur-icons base64: false input: vectors: svg templates: templates output: fonts: output/fonts templates: - projecteur-icons-def.h Projecteur-0.10/icons/icon-font/svg/000077500000000000000000000000001451344070600173665ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/svg/iconmonstr-arrow-73.svg000066400000000000000000000001761451344070600236650ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/svg/iconmonstr-arrow-74.svg000066400000000000000000000001771451344070600236670ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/svg/iconmonstr-audio-6.svg000066400000000000000000000004671451344070600235530ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/svg/iconmonstr-battery-3.svg000066400000000000000000000003211451344070600241060ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/svg/iconmonstr-battery-4.svg000066400000000000000000000003421451344070600241120ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/svg/iconmonstr-battery-5.svg000066400000000000000000000003611451344070600241140ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/svg/iconmonstr-battery-6.svg000066400000000000000000000004001451344070600241070ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/svg/iconmonstr-battery-7.svg000066400000000000000000000004171451344070600241200ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/svg/iconmonstr-connection-8.svg000066400000000000000000000011641451344070600246060ustar00rootroot00000000000000 Projecteur-0.10/icons/icon-font/svg/iconmonstr-control-panel-9.svg000066400000000000000000000003531451344070600252240ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/svg/iconmonstr-cursor-21-rotated.svg000066400000000000000000000034251451344070600255010ustar00rootroot00000000000000 image/svg+xml Projecteur-0.10/icons/icon-font/svg/iconmonstr-cursor-21.svg000066400000000000000000000004771451344070600240450ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/svg/iconmonstr-gear-12.svg000066400000000000000000000041701451344070600234400ustar00rootroot00000000000000 Projecteur-0.10/icons/icon-font/svg/iconmonstr-keyboard-14.svg000066400000000000000000000004251451344070600243230ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/svg/iconmonstr-keyboard-4.svg000066400000000000000000000004111451344070600242350ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/svg/iconmonstr-media-control-48.svg000066400000000000000000000001731451344070600252670ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/svg/iconmonstr-media-control-50.svg000066400000000000000000000001641451344070600252600ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/svg/iconmonstr-plus-5.svg000066400000000000000000000003241451344070600234240ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/svg/iconmonstr-power-on-off-11.svg000066400000000000000000000010341451344070600250330ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/svg/iconmonstr-share-8.svg000066400000000000000000000004261451344070600235510ustar00rootroot00000000000000 Projecteur-0.10/icons/icon-font/svg/iconmonstr-target-8.svg000066400000000000000000000013631451344070600237360ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/svg/iconmonstr-time-19.svg000066400000000000000000000012231451344070600234630ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/svg/iconmonstr-trash-can-1.svg000066400000000000000000000006311451344070600243160ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/templates/000077500000000000000000000000001451344070600205655ustar00rootroot00000000000000Projecteur-0.10/icons/icon-font/templates/projecteur-icons-def.h000066400000000000000000000006711451344070600247710ustar00rootroot00000000000000// 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` namespace Font { enum Icon { <% @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 %> }; } Projecteur-0.10/icons/projecteur-icons.ttf000066400000000000000000000111201451344070600207000ustar00rootroot00000000000000 PFFTM4OS/2O]/X`cmapnjcvt Ddgasp,glyf2+ head6hhea$hmtxe@loca"h8maxph8 name&1 postwWϿ^_< 55. @.LfGLfPfEd .@ *UU@*dH  D****Lt F&T^,nzU./<2<2/<2<233'3#wffU3@@!!7!532#j+@   J @@!!7!532#%#5j+@  @ J @@!!7!532#%#53#5j+@  @@ J @@!!7!532#%#53#53#5j+@  @@@ J @@!!!7!532#%#53#53#53#5j+@  @@@@ J  #7##5#53####57#5##5#5#+*++*+*+U*+*+U*@kk@+@@kk@kk@+632"&5473264&#"#;>7.#.*'><.'5><.'7:>"264'H$CQjԖ, }}}XC7%"     "    %$$,//Ԗj !Y}}}&/  "    "  S$$2"&45#5##335ԖԖk*kk*Ԗ*kk*kk7>?55"7!3#! $+/+# 86G+UUU+J2(GG -")U !5%.767'53&7&3#3#3#$#"'33264&#"#632S # N U*]+@}XY?F&,FddF,&F?YX #B 994N+*++*ñ}@dd@*'3!!754&"26754&"26754&"26!5326533@k   j   k   UVz r @U      **U'333V@@U###V@  #'+!!35#3'35#3'35#35355!%5#75#@@+@@j@+@@k@UUUjj@+@@V@@@V@@@@@V@@@@U@@V@@  #'!!!#53#53#53#5#53#53#5#5V+V@@@@@U*UUU@@@@@@@@V@@@@@@U@@&>%'"&462&2'654&"327#"&4#"&462'654&"32%JC!D.<##FjK'2G22$ 5KY}}}& dddGbVXA%##VK5 #22G2*Kj*}}}X*' Fddd2"&4$"2643#'755#5ԖԖX}}}jjVjjԖk}}}*@UU@UU@*(%'74&'7'6'#5#"&546732UJ/,.6V-'/6&Uk b7.(-pP ]_11S" f;a- a;f !T1Oq@@*!!+VVk$"&462'5#7#&"3'352+$$'VkkV VkkV $$'....U+$"&4627355#64#'73$$'....$$'VkkV VkkV  #3'55%'%#5#5'@I]kk]\Vj1SS1/%5%V**U5%5%B+!T     6 VH      2projecteur-iconsprojecteur-iconsiconsiconsFontForge 2.0 : projecteur-icons : 8-8-2021FontForge 2.0 : projecteur-icons : 8-8-2021projecteur-iconsprojecteur-iconsVersion 001.000 Version 001.000 projecteur-iconsprojecteur-icons     iconmonstr-battery-3iconmonstr-battery-4iconmonstr-battery-5iconmonstr-battery-6iconmonstr-battery-7iconmonstr-control-panel-9iconmonstr-gear-12iconmonstr-plus-5iconmonstr-share-8iconmonstr-time-19iconmonstr-trash-can-1iconmonstr-arrow-73iconmonstr-arrow-74iconmonstr-keyboard-14iconmonstr-keyboard-4iconmonstr-target-8iconmonstr-connection-8iconmonstr-power-on-off-11iconmonstr-media-control-48iconmonstr-media-control-50iconmonstr-cursor-21iconmonstr-cursor-21-rotatediconmonstr-audio-6'k55Projecteur-0.10/icons/projecteur-tray-64.png000066400000000000000000000104641451344070600207740ustar00rootroot00000000000000PNG  IHDR@@iqsBIT|d pHYs+tEXtSoftwarewww.inkscape.org<IDATx՛}Tվ?300/ /&M 8yzQLQRʎuQsUz*ת֪n xW3,W% rME`f38ߵb?o=zPJ~]@ǘ<{'3322Jޮ:yxxX+**6lؐ yH 2 a} ({k @Z_}' Dz( O ݋/ \. J`™3g&@355@,upKPQAFc۸qB$$edHNNjoZ8x`}lz nw>&-@"z}?PSPP7|8 oV;H$>5ݻkڵ4p 8 \ Zg^t^VRUUu?>0bĈπQӳg޽vڃRt Žev_By߾} tuuWfee H⁁@ ]TT ݻJSyI ŢI&}R x988m\>!Ç_ZTTԗF±)a@LWέ[R^z}C>۷_ZVˡ/,O8qB) o?00pPLL@iӦ?Q[l#&LgϞR/ڍwU+5j `̂ ZV X?СCM˖-K333ĉU)))o߾IpWoȑ#ѷ[t:ܓ?|xdd|xƌymmm-cǎgϞwaс qyX5~˖-y欬pɒ% `HII{''|VoĘh2]HKK8 ٸqcW_}u>иqJHpѣGdBV(\X~*{ҥi^xasee9&M8w^ 92pf%`H$|v**0Ϙ1chls=&ʀ#Fꚰ0bT|WĚ~-oɒ%{BZ{wwٞ eV:$0wܭd766^py&o۶sns29`Oa\>qQRbt-N珨j2V⋳8ss/C @'[jペ>_VUx嗓s9`2Κ5+E'O+((p4wt؉5F*\VV|TTTмyfg=cٲeAN0eʔo"] /@СC &`h42Doaܼyo/~|+:pԩ#mh7iA_\\ێgCW\/[;ǵ&L&{YAO #N]T ÚЗ>>±Q[֧h p |? `=z1cLફʶ/]{\ N6w!Ʀ&3V 49rcǎH$D!n9e6mZ$7r|Zܟ݈T*_ 6m:0~1q" 3gNٳg/dڴi ;v#$F㽼dTln) аnݺ=3++k 1t] 3};vm&'$"11q [&h*H$gL3ӧO2))iDDDt:J%WR???/___s|ɳWcZ-mmmfliooL?ԩS_Mf 233s5 Wl+\u%BYLv𖟟^'V{X`hAV.\m}L'Ug^ϤI2N:'"hlmiiimhhhojj4Ln j/TjU^:Nh*_*ʅ ׉xz}DDDsgAĥkllTRRrO?Tȴvx13![??11qp||8MRR::::ڽa?.ZOЯ_W-Kw'A?πS&B r \k֮M^URZ oqqqb1-U[n?07CxFFFn_rDr;25 v! L~dggR"Yfu(,,,U*t6!FQRSSvvvKBnnn1λ!/))J xg <S'ΝM$zX 7o^*DyҥZw mD܂8Zt`???z@hh/=w^ BT*ݎ;u?=]F'/_4jԨUr bR^^^__͵ը+⋒tRcFF ar&޽{A>bD2В2쥣Gm jݿ9s 2+]*wY?b9rR./D=&.)***ܰϟߴiSiFF?}||f @ĺ_?ۗSR!!{F!fbV8)))i{?|l6Pd^;w|xhxxxH Rk4r_8|͊+l۶j-Wx ~´%%%7BGGGr image/svg+xml Projecteur-0.10/qml/000077500000000000000000000000001451344070600143515ustar00rootroot00000000000000Projecteur-0.10/qml/main-qt6.qml000066400000000000000000000170301451344070600165210ustar00rootroot00000000000000// 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 Projecteur-0.10/qml/main.qml000066400000000000000000000170111451344070600160100ustar00rootroot00000000000000// 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 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: { 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 Projecteur-0.10/qml/qml-qt6.qrc000066400000000000000000000004241451344070600163610ustar00rootroot00000000000000 main-qt6.qml spotshapes/Circle.qml spotshapes/Square.qml spotshapes/Star.qml spotshapes/Ngon.qml Projecteur-0.10/qml/qml.qrc000066400000000000000000000003771451344070600156600ustar00rootroot00000000000000 main.qml spotshapes/Circle.qml spotshapes/Square.qml spotshapes/Star.qml spotshapes/Ngon.qml Projecteur-0.10/qml/spotshapes/000077500000000000000000000000001451344070600165425ustar00rootroot00000000000000Projecteur-0.10/qml/spotshapes/Circle.qml000066400000000000000000000003731451344070600204610ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md import QtQuick 2.3 // Circle spotlight shape Rectangle { anchors.fill: parent radius: width * 0.5 visible: false enabled: false } Projecteur-0.10/qml/spotshapes/Ngon.qml000066400000000000000000000004611451344070600201570ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md import QtQuick 2.3 import Projecteur.Shapes 1.0 as Shapes // N-gon spotlight shape Shapes.NGon { anchors.fill: parent sides: Settings.shapes.Ngon.sides visible: false enabled: false } Projecteur-0.10/qml/spotshapes/Square.qml000066400000000000000000000004571451344070600205230ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md import QtQuick 2.3 // (Rounded) Square spotlight shape Rectangle { anchors.fill: parent radius: width * 0.5 * (Settings.shapes.Square.radius / 100.0) visible: false enabled: false } Projecteur-0.10/qml/spotshapes/Star.qml000066400000000000000000000005741451344070600201740ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md import QtQuick 2.3 import Projecteur.Shapes 1.0 as Shapes // Star spotlight shape Shapes.Star { anchors.fill: parent points: Settings.shapes.Star.points innerRadius: Settings.shapes.Star.innerRadius visible: false enabled: false antialiasing: true } Projecteur-0.10/resources.qrc000066400000000000000000000011631451344070600163020ustar00rootroot00000000000000 icons/projecteur-tray.svg icons/projecteur-tray-64.png icons/cursors/cursor-arrow.png icons/cursors/cursor-busy.png icons/cursors/cursor-cross.png icons/cursors/cursor-hand.png icons/cursors/cursor-openhand.png icons/cursors/cursor-sizeall.png icons/cursors/cursor-uparrow.png icons/cursors/cursor-whatsthis.png icons/projecteur-icons.ttf Projecteur-0.10/src/000077500000000000000000000000001451344070600143475ustar00rootroot00000000000000Projecteur-0.10/src/aboutdlg.cc000066400000000000000000000221131451344070600164560ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #include "aboutdlg.h" #include "projecteur-GitVersion.h" #include #include #include #include #include #include #include #include #include #include namespace { // ----------------------------------------------------------------------------------------------- /// Contributor (name, github_name, email, url) struct Contributor { explicit Contributor(const QString& name = {}, const QString& github_name = {}, const QString& email = {}, const QString& url = {}) : name(name), github_name(github_name), email(email), url(url) {} QString toHtml() const { auto html = QString("%1").arg(name.isEmpty() ? QString("%1").arg(github_name) : name); if (email.size()) { html += QString(" <%1>").arg(email); } if (url.size()) { html += QString(" %1").arg(url); } else if (!name.isEmpty()) { html += QString(" - github: %1").arg(github_name); } return html; } QString name; QString github_name; QString email; QString url; }; // ----------------------------------------------------------------------------------------------- QString getContributorsHtml() { static std::vector contributors = { Contributor("Ricardo Jesus", "rj-jesus"), Contributor("Mayank Suman", "mayanksuman"), Contributor("Tiziano Müller", "dev-zero"), Contributor("Torsten Maehne", "maehne"), Contributor("TBK", "TBK"), Contributor("Louie Lu", "mlouielu"), Contributor("fmuelle4711", "fmuelle4711"), Contributor("Deniz Bahadir", "Bagira80"), Contributor("Tomáš Chvátal", "scarabeusiv"), Contributor("Brandon Johnson", "dbrandonjohnson"), 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{}()); std::shuffle(contributors.begin(), contributors.end(), g); QStringList contributorsHtml; for (const auto& contributor : contributors) { contributorsHtml.append(contributor.toHtml()); } return contributorsHtml.join("
"); } } // end anonymous namespace // ------------------------------------------------------------------------------------------------- AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent) , m_tabWidget(new QTabWidget(this)) { setWindowTitle(tr("About %1", "%1=application name").arg(QCoreApplication::applicationName())); setWindowIcon(QIcon(":/icons/projecteur-tray.svg")); const auto hbox = new QHBoxLayout(); const auto iconLabel = new QLabel(this); iconLabel->setPixmap(QIcon(":/icons/projecteur-tray.svg").pixmap(QSize(128,128))); hbox->addWidget(iconLabel); hbox->addWidget(m_tabWidget, 1); m_tabWidget->addTab(createVersionInfoWidget(), tr("Version")); m_tabWidget->addTab(createContributorInfoWidget(), tr("Contributors")); m_tabWidget->addTab(createThirdPartyLicensesWidget(), tr("Licenses")); const auto bbox = new QDialogButtonBox(QDialogButtonBox::Ok, this); connect(bbox, &QDialogButtonBox::clicked, this, &QDialog::accept); const auto mainVbox = new QVBoxLayout(this); mainVbox->addLayout(hbox); mainVbox->addSpacing(10); mainVbox->addWidget(bbox); } // ------------------------------------------------------------------------------------------------- void AboutDialog::showEvent(QShowEvent* e) { QDialog::showEvent(e); m_tabWidget->setCurrentIndex(0); } // ------------------------------------------------------------------------------------------------- QWidget* AboutDialog::createVersionInfoWidget() { const auto versionInfoWidget = new QWidget(this); const auto vbox = new QVBoxLayout(versionInfoWidget); const auto versionLabel = new QLabel(QString("%1
%2") .arg(QCoreApplication::applicationName(), 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
build-type: %3") .arg(projecteur::version_branch(), projecteur::version_shorthash(), projecteur::version_buildtype()); versionLabel->setToolTip(vInfo); if (QString(projecteur::version_flag()).size() || (QString(projecteur::version_branch()) != "master" && QString(projecteur::version_branch()) != "not-within-git-repo")) { vbox->addSpacing(4); vbox->addWidget(new QLabel(vInfo, this)); } vbox->addSpacing(4); const auto weblinkLabel = new QLabel(QString("" "https://github.com/jahnf/Projecteur"), this); weblinkLabel->setOpenExternalLinks(true); vbox->addWidget(weblinkLabel); vbox->addSpacing(8); auto qtVerText = tr("Qt Version: %1", "%1=qt version number").arg(QT_VERSION_STR); if (QString(QT_VERSION_STR) != qVersion()) { qtVerText += QString(" (runtime: %1)").arg(qVersion()); } vbox->addWidget(new QLabel(qtVerText, this)); vbox->addSpacing(15); vbox->addWidget(new QLabel("Copyright 2018-2021 Jahn Fuchs", this)); auto licenseText = new QLabel(tr("This project is distributed under the
" "" "MIT License"), this); licenseText->setWordWrap(true); licenseText->setTextFormat(Qt::TextFormat::RichText); licenseText->setOpenExternalLinks(true); vbox->addWidget(licenseText); vbox->addStretch(1); return versionInfoWidget; } // ------------------------------------------------------------------------------------------------- QWidget* AboutDialog::createContributorInfoWidget() { const auto contributorWidget = new QWidget(this); const auto vbox = new QVBoxLayout(contributorWidget); const auto label = new QLabel(tr("Contributors, in random order:"), contributorWidget); vbox->addWidget(label); const auto textBrowser = new QTextBrowser(contributorWidget); textBrowser->setWordWrapMode(QTextOption::NoWrap); textBrowser->setOpenLinks(true); textBrowser->setOpenExternalLinks(true); textBrowser->setFont([textBrowser]() { auto font = textBrowser->font(); font.setPointSizeF(font.pointSizeF() - 2.0); return font; }()); // randomize contributors list on every contributors tab selection connect(m_tabWidget, &QTabWidget::currentChanged, this, [contributorWidget, textBrowser, this](int){ if (contributorWidget == m_tabWidget->currentWidget()) { textBrowser->setHtml(getContributorsHtml()); } }); vbox->addWidget(textBrowser); return contributorWidget; } // ------------------------------------------------------------------------------------------------- QWidget* AboutDialog::createThirdPartyLicensesWidget() { const auto tpLicenceWidget = new QWidget(this); const auto layout = new QVBoxLayout(tpLicenceWidget); struct ThirdPartyProject { const QString projectName; const QString projectUrl; const QString copyrightNotice; const QString licenseName; const QString licenseUrl; }; static const std::vector thirdPartyProjects = { ThirdPartyProject{ "Qt Toolkit", "https://www.qt.io", "Copyright (C) The Qt Company Ltd.", "GPL/LGPLv3", "" }, }; const auto textBrowser = new QTextBrowser(tpLicenceWidget); layout->addWidget(textBrowser); textBrowser->setOpenLinks(true); textBrowser->setOpenExternalLinks(true); textBrowser->setWordWrapMode(QTextOption::NoWrap); textBrowser->setFont([textBrowser]() { auto font = textBrowser->font(); font.setPointSizeF(font.pointSizeF() - 2.5); return font; }()); QString html = ""; html += "
    "; for (const auto& tpl : thirdPartyProjects) { html += "
  • "; if (tpl.projectUrl.size()) { html += QString("%2").arg(tpl.projectUrl, tpl.projectName); } else { html += QString("%1").arg(tpl.projectName); } if (tpl.copyrightNotice.size()) { html += "
    " + tpl.copyrightNotice + ""; } if (tpl.licenseUrl.size()) { html += QString("
    %2").arg(tpl.licenseUrl, tpl.licenseName); } else { html += QString("
    License: %1").arg(tpl.licenseName); } html += "
  • "; } html += "
"; textBrowser->setHtml(html); return tpLicenceWidget; } Projecteur-0.10/src/aboutdlg.h000066400000000000000000000007601451344070600163240ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #pragma once #include class QTabWidget; class AboutDialog : public QDialog { Q_OBJECT public: explicit AboutDialog(QWidget* parent = nullptr); protected: void showEvent(QShowEvent*) override; private: QTabWidget* m_tabWidget = nullptr; QWidget* createVersionInfoWidget(); QWidget* createContributorInfoWidget(); QWidget* createThirdPartyLicensesWidget(); }; Projecteur-0.10/src/actiondelegate.cc000066400000000000000000000421021451344070600176250ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #include "actiondelegate.h" #include "inputmapconfig.h" #include "inputseqedit.h" #include "nativekeyseqedit.h" #include "projecteur-icons-def.h" #include #include #include namespace { namespace keysequence { // --------------------------------------------------------------------------------------------- void paint(QPainter* p, const QStyleOptionViewItem& option, const KeySequenceAction* action) { const auto& fm = option.fontMetrics; const int xPos = (option.rect.height()-fm.height()) / 2; NativeKeySeqEdit::drawSequence(xPos, *p, option, action->keySequence); } // --------------------------------------------------------------------------------------------- QSize sizeHint(const QStyleOptionViewItem& opt, const KeySequenceAction* action) { constexpr int verticalMargin = 3; constexpr int horizontalMargin = 3; const int h = opt.fontMetrics.height() + 2 * verticalMargin; #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) const int w = std::max(opt.fontMetrics.horizontalAdvance(ActionDelegate::tr("None")) + 2 * horizontalMargin, opt.fontMetrics.horizontalAdvance(action->keySequence.toString())); #else const int w = std::max(opt.fontMetrics.width(ActionDelegate::tr("None")) + 2 * horizontalMargin, opt.fontMetrics.width(action->keySequence.toString())); #endif return { w, h }; } } // end namespace keysequence namespace cyclepresets { // --------------------------------------------------------------------------------------------- void paint(QPainter* p, const QStyleOptionViewItem& option, const CyclePresetsAction* /*action*/) { const auto& fm = option.fontMetrics; const int xPos = (option.rect.height()-fm.height()) / 2; NativeKeySeqEdit::drawText(xPos, *p, option, ActionDelegate::tr("Cycle Presets")); } // --------------------------------------------------------------------------------------------- QSize sizeHint(const QStyleOptionViewItem& /*opt*/, const CyclePresetsAction* /*action*/) { return { 100, 16 }; } } // end namespace cyclepresets namespace togglespotlight { // --------------------------------------------------------------------------------------------- void paint(QPainter* p, const QStyleOptionViewItem& option, const ToggleSpotlightAction* /*action*/) { const auto& fm = option.fontMetrics; const int xPos = (option.rect.height()-fm.height()) / 2; NativeKeySeqEdit::drawText(xPos, *p, option, ActionDelegate::tr("Toggle Spotlight")); } // --------------------------------------------------------------------------------------------- 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*/) { 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 // ------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------- void ActionDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { // Let QStyledItemDelegate handle drawing current focus inidicator and other basic stuff.. QStyledItemDelegate::paint(painter, option, index); const auto imModel = qobject_cast(index.model()); if (!imModel) { return; } const auto& item = imModel->configData(index); if (!item.action) { return; } switch (item.action->type()) { case Action::Type::KeySequence: keysequence::paint(painter, option, static_cast(item.action.get())); break; case Action::Type::CyclePresets: cyclepresets::paint(painter, option, static_cast(item.action.get())); break; 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) { InputSeqDelegate::drawCurrentIndicator(*painter, 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); } const auto& item = imModel->configData(index); if (!item.action) { return QStyledItemDelegate::sizeHint(opt, index); } switch (item.action->type()) { case Action::Type::KeySequence: return keysequence::sizeHint(opt, static_cast(item.action.get())); case Action::Type::CyclePresets: 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); } // ------------------------------------------------------------------------------------------------- QWidget* ActionDelegate::createEditor(QWidget* parent, const Action* action) const { switch (action->type()) { case Action::Type::KeySequence: { const auto editor = new NativeKeySeqEdit(parent); connect(editor, &NativeKeySeqEdit::editingFinished, this, &ActionDelegate::commitAndCloseEditor); return editor; } 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; } // ------------------------------------------------------------------------------------------------- QWidget* ActionDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& /*option*/, const QModelIndex& index) const { const auto imModel = qobject_cast(index.model()); if (!imModel) { return nullptr; } const auto& item = imModel->configData(index); if (!item.action) { return nullptr; } return createEditor(parent, item.action.get()); } // ------------------------------------------------------------------------------------------------- void ActionDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const { if (const auto seqEditor = qobject_cast(editor)) { if (const auto imModel = qobject_cast(index.model())) { const auto& item = imModel->configData(index); const auto action = static_cast(item.action.get()); seqEditor->setKeySequence(action->keySequence); seqEditor->setRecording(true); return; } } QStyledItemDelegate::setEditorData(editor, index); } // ------------------------------------------------------------------------------------------------- void ActionDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const { if (const auto seqEditor = qobject_cast(editor)) { if (const auto imModel = qobject_cast(model)) { imModel->setKeySequence(index, seqEditor->keySequence()); return; } } QStyledItemDelegate::setModelData(editor, model, index); } // ------------------------------------------------------------------------------------------------- bool ActionDelegate::eventFilter(QObject* obj, QEvent* ev) { if (ev->type() == QEvent::KeyPress) { // 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; } } return QStyledItemDelegate::eventFilter(obj,ev); } // ------------------------------------------------------------------------------------------------- void ActionDelegate::commitAndCloseEditor(QWidget* editor) { emit commitData(editor); emit closeEditor(editor); } // ------------------------------------------------------------------------------------------------- void ActionDelegate::commitAndCloseEditor_() { commitAndCloseEditor(qobject_cast(sender())); } // ------------------------------------------------------------------------------------------------- void ActionDelegate::actionContextMenu(QWidget* parent, InputMapConfigModel* model, const QModelIndex& index, const QPoint& globalPos) { if (!index.isValid() || !model) { return; } const auto& item = model->configData(index); if (!item.action || item.action->type() != Action::Type::KeySequence) { return; } auto* const menu = new QMenu(parent); const std::vector predefinedKeys = { &NativeKeySequence::predefined::altTab(), &NativeKeySequence::predefined::altF4(), &NativeKeySequence::predefined::meta(), }; for (const auto ks : predefinedKeys) { const auto qaction = menu->addAction(ks->toString()); connect(qaction, &QAction::triggered, this, [model, index, ks](){ model->setKeySequence(index, *ks); }); } menu->exec(globalPos); menu->deleteLater(); } //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- void ActionTypeDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { // Let QStyledItemDelegate handle drawing current focus inidicator and other basic stuff.. QStyledItemDelegate::paint(painter, option, index); const auto imModel = qobject_cast(index.model()); if (!imModel) { return; } const auto& item = imModel->configData(index); if (!item.action) { return; } const auto symbol = [&item]() -> QChar { switch(item.action->type()) { 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 QChar(0); }(); if (symbol != 0) { drawActionTypeSymbol(0, *painter, option, symbol); } if (option.state & QStyle::State_HasFocus) { InputSeqDelegate::drawCurrentIndicator(*painter, option); } } // ------------------------------------------------------------------------------------------------- void ActionTypeDelegate::actionContextMenu(QWidget* parent, InputMapConfigModel* model, const QModelIndex& index, const QPoint& globalPos) { if (!index.isValid() || !model) { return; } const auto& item = model->configData(index); if (!item.action) { return; } struct actionEntry { Action::Type type; QChar symbol; QString text; bool isMoveAction; QIcon icon = {}; }; static std::vector items { {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 = []() { Q_UNUSED(initIcons) QFont iconFont("projecteur-icons"); constexpr int iconSize = 16; iconFont.setPixelSize(iconSize); for (auto& item : items) { QImage img(QSize(iconSize, iconSize), QImage::Format::Format_ARGB32_Premultiplied); img.fill(Qt::transparent); QPainter p(&img); p.setFont(iconFont); QRect(0, 0, img.width(), img.height()); p.drawText(QRect(0, 0, img.width(), img.height()), Qt::AlignHCenter | Qt::AlignVCenter, QString(item.symbol)); item.icon = QIcon(QPixmap::fromImage(img)); } return true; }(); 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) { 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); menu->deleteLater(); } // ------------------------------------------------------------------------------------------------- int ActionTypeDelegate::drawActionTypeSymbol(int startX, QPainter& p, const QStyleOptionViewItem& option, const QChar& symbol) { const auto r = QRect(QPoint(startX + option.rect.left(), option.rect.top()), option.rect.bottomRight()); QFont iconFont("projecteur-icons"); iconFont.setPixelSize(qMin(option.rect.height(), option.rect.width()) - 4); p.save(); p.setFont(iconFont); p.setRenderHint(QPainter::Antialiasing, true); if (option.state & QStyle::State_Selected) { p.setPen(option.palette.color(QPalette::HighlightedText)); } else { p.setPen(option.palette.color(QPalette::Text)); } QRect br; p.drawText(r, Qt::AlignHCenter | Qt::AlignVCenter, QString(symbol), &br); p.restore(); return br.width(); } Projecteur-0.10/src/actiondelegate.h000066400000000000000000000036611451344070600174760ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #pragma once #include // ------------------------------------------------------------------------------------------------- struct Action; class InputMapConfigModel; // ------------------------------------------------------------------------------------------------- class ActionDelegate : public QStyledItemDelegate { Q_OBJECT public: using QStyledItemDelegate::QStyledItemDelegate; void paint(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const override; QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const override; 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 actionContextMenu(QWidget* parent, InputMapConfigModel* model, const QModelIndex& index, const QPoint& globalPos); protected: bool eventFilter(QObject* obj, QEvent* ev) override; private: QWidget* createEditor(QWidget* parent, const Action* action) const; void commitAndCloseEditor(QWidget* editor); void commitAndCloseEditor_(); }; // ------------------------------------------------------------------------------------------------- class ActionTypeDelegate : public QStyledItemDelegate { Q_OBJECT public: using QStyledItemDelegate::QStyledItemDelegate; void paint(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const override; void actionContextMenu(QWidget* parent, InputMapConfigModel* model, const QModelIndex& index, const QPoint& globalPos); private: static int drawActionTypeSymbol(int startX, QPainter& p, const QStyleOptionViewItem& option, const QChar& symbol); }; Projecteur-0.10/src/asynchronous.h000066400000000000000000000137401451344070600172600ustar00rootroot00000000000000// 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 Projecteur-0.10/src/colorselector.cc000066400000000000000000000057171451344070600175470ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #include "colorselector.h" #include #include #include #include #include namespace { std::unique_ptr colorButtonStyle = std::make_unique(); QColor mixColors(const QColor& a, const QColor& b, double ratio = 0.5) { return QColor( a.red() *(1.0-ratio) + b.red() *ratio, a.green()*(1.0-ratio) + b.green()*ratio, a.blue() *(1.0-ratio) + b.blue() *ratio, 255 ); } } // end anonymous namespace ColorSelectorButtonStyle::ColorSelectorButtonStyle() { setObjectName("ColorSelectorButtontyle"); } void ColorSelectorButtonStyle::drawPrimitive(PrimitiveElement element, const QStyleOption *option, QPainter *p, const QWidget *widget) const { if (element != PE_PanelButtonCommand) QProxyStyle::drawPrimitive(element, option, p, widget); p->save(); p->setRenderHint(QPainter::Antialiasing); p->translate(0.5, -0.5); QPainterPath path; const auto rect = option->rect.adjusted(1,1,-1,0); path.addRoundedRect(rect, 4, 4); const auto borderColor = [option]() { // Set border color based on window color const auto w = option->palette.color(QPalette::Window); const auto c = (w.redF() * 0.299 + w.greenF() * 0.587 + w.blueF() * 0.114 ) > 0.6 ? Qt::darkGray : Qt::lightGray; if (option->state & State_Enabled) return QColor(c); return mixColors(c, option->palette.color(QPalette::Disabled, QPalette::Button)); }(); const auto buttonBrush = [option]() { if (option->state & State_Enabled) return option->palette.button(); return QBrush(mixColors(option->palette.color(QPalette::Normal, QPalette::Button), option->palette.color(QPalette::Disabled, QPalette::Button))); }(); p->setPen(QPen(borderColor, 1)); p->fillPath(path, buttonBrush); p->drawPath(path); p->restore(); } ColorSelector::ColorSelector(QWidget* parent) : ColorSelector(tr("Select Color"), Qt::black, parent) { } ColorSelector::ColorSelector(const QString& selectionDialogTitle, const QColor& color, QWidget* parent) : QPushButton(parent) , m_color(color) { setStyle(colorButtonStyle.get()); setMinimumWidth(30); updateButton(); connect(this, &QPushButton::clicked, [this, selectionDialogTitle](){ const QColor c = QColorDialog::getColor(m_color, this, selectionDialogTitle); if (c.isValid()) setColor(c); }); } void ColorSelector::setColor(const QColor& color) { if (m_color == color) return; m_color = color; updateButton(); emit colorChanged(color); } void ColorSelector::updateButton() { QPalette p(palette()); p.setColor(QPalette::Button, m_color); p.setColor(QPalette::ButtonText, m_color); setPalette(p); setToolTip(m_color.name()); } Projecteur-0.10/src/colorselector.h000066400000000000000000000016341451344070600174030ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #pragma once #include #include class ColorSelectorButtonStyle : public QProxyStyle { Q_OBJECT public: ColorSelectorButtonStyle(); void drawPrimitive(PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const override; }; class ColorSelector : public QPushButton { Q_OBJECT Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) public: explicit ColorSelector(QWidget* parent = nullptr); explicit ColorSelector(const QString& selectionDialogTitle, const QColor& color, QWidget* parent = nullptr); void setColor(const QColor& color); QColor color() const { return m_color; } signals: void colorChanged(QColor); private: void updateButton(); private: QColor m_color; }; Projecteur-0.10/src/device-command-helper.cc000066400000000000000000000031571451344070600210140ustar00rootroot00000000000000// 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; } Projecteur-0.10/src/device-command-helper.h000066400000000000000000000010511451344070600206450ustar00rootroot00000000000000// 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; }; Projecteur-0.10/src/device-defs.h000066400000000000000000000031621451344070600167000ustar00rootroot00000000000000// 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); Projecteur-0.10/src/device-hidpp.cc000066400000000000000000001202061451344070600172200ustar00rootroot00000000000000// 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(); } } Projecteur-0.10/src/device-hidpp.h000066400000000000000000000140141451344070600170610ustar00rootroot00000000000000// 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); Projecteur-0.10/src/device-key-lookup.cc000066400000000000000000000050151451344070600202130ustar00rootroot00000000000000// 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 KeyNameProjecteur-0.10/src/device-key-lookup.h000066400000000000000000000004721451344070600200570ustar00rootroot00000000000000// 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 Projecteur-0.10/src/device-vibration.cc000066400000000000000000000377661451344070600201330ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #include "device-vibration.h" #include "device-hidpp.h" #include "hidpp.h" #include "iconwidgets.h" #include "logging.h" #include #include #include #include #include #include #include #include #include #include #include #include DECLARE_LOGGING_CATEGORY(hid) // ------------------------------------------------------------------------------------------------- namespace { constexpr uint32_t numTimers = 3; } // end anonymous namespace // ------------------------------------------------------------------------------------------------- struct TimerWidget::Impl { // ----------------------------------------------------------------------------------------------- explicit Impl(TimerWidget* parent) : stack(new QStackedWidget(parent)) , editor(new QWidget(parent)) , overlay(new QWidget(parent)) , checkbox(new QCheckBox(parent)) , sbHours(new QSpinBox(parent)) , sbMinutes(new QSpinBox(parent)) , sbSeconds(new QSpinBox(parent)) , btnStartStop(new IconButton(Font::Icon::media_control_48, parent)) , timer(new QTimer(parent)) , countdownTimer(new QTimer(parent)) , overlayLabel(new QLabel(parent)) { const auto layout = new QHBoxLayout(parent); layout->addWidget(checkbox); layout->addWidget(stack); layout->setContentsMargins(0, 0, 0, 0); stack->addWidget(editor); stack->addWidget(overlay); const auto editLayout = new QHBoxLayout(editor); const auto m = editLayout->contentsMargins(); editLayout->setContentsMargins(m.left(), 0, m.right(), 0); editLayout->addWidget(sbHours); editLayout->addWidget(new QLabel(TimerWidget::tr("h"), editor)); editLayout->addWidget(sbMinutes); editLayout->addWidget(new QLabel(TimerWidget::tr("m"), editor)); editLayout->addWidget(sbSeconds); editLayout->addWidget(new QLabel(TimerWidget::tr("s"), editor)); editLayout->addStretch(1); 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); QObject::connect(btnStartStop, &IconButton::toggled, parent, [this](bool checked) { stack->setCurrentWidget(checked ? overlay : editor); btnStartStop->setText(checked ? QChar(Font::Icon::media_control_50) : QChar(Font::Icon::media_control_48)); if (checked) { secondsLeft = valueSeconds(); updateOverlayLabel(secondsLeft); countdownTimer->start(); timer->start(); } else { timer->stop(); countdownTimer->stop(); } }); const auto overlayLayout = new QHBoxLayout(overlay); overlayLayout->addWidget(overlayLabel); overlayLayout->setContentsMargins(m.left(), 0, m.right(), 0); overlayLabel->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); editor->setEnabled(checkbox->isChecked()); btnStartStop->setEnabled(checkbox->isChecked()); QObject::connect(checkbox, &QCheckBox::toggled, parent, [this, parent](bool checked) { editor->setEnabled(checked); if (!checked) { btnStartStop->setChecked(false); } btnStartStop->setEnabled(checked); emit parent->enabledChanged(checked); }); QObject::connect(timer, &QTimer::timeout, parent, [this](){ btnStartStop->setChecked(false); }); QObject::connect(sbHours, static_cast(&QSpinBox::valueChanged), parent, [this, parent]() { updateTimerInterval(); emit parent->valueSecondsChanged(valueSeconds()); }); QObject::connect(sbMinutes, static_cast(&QSpinBox::valueChanged), parent, [this, parent]() { updateTimerInterval(); emit parent->valueSecondsChanged(valueSeconds()); }); QObject::connect(sbSeconds, static_cast(&QSpinBox::valueChanged), parent, [this, parent]() { updateTimerInterval(); emit parent->valueSecondsChanged(valueSeconds()); }); timer->setSingleShot(true); countdownTimer->setInterval(1000); QObject::connect(countdownTimer, &QTimer::timeout, parent, [this](){ updateOverlayLabel(--secondsLeft); }); } int valueSeconds() const { return sbSeconds->value() + sbMinutes->value() * 60 + sbHours->value() * 60 * 60; } // ----------------------------------------------------------------------------------------------- void updateTimerInterval() { timer->setInterval(valueSeconds() * 1000); } // ----------------------------------------------------------------------------------------------- void updateOverlayLabel(int remainingSeconds) { const std::chrono::seconds remainingTime(remainingSeconds); const auto hours = std::chrono::duration_cast(remainingTime); const auto mins = std::chrono::duration_cast(remainingTime-hours); const auto secs = std::chrono::duration_cast(remainingTime-hours-mins); overlayLabel->setText(QString("%1:%2:%3") .arg(hours.count(), 2, 10, QChar('0')) .arg(mins.count(), 2, 10, QChar('0')) .arg(secs.count(), 2, 10, QChar('0'))); } // ----------------------------------------------------------------------------------------------- QStackedWidget* stack = nullptr; QWidget* editor = nullptr; QWidget* overlay = nullptr; QCheckBox* checkbox = nullptr; QSpinBox* sbHours = nullptr; QSpinBox* sbMinutes = nullptr; QSpinBox* sbSeconds = nullptr; IconButton* btnStartStop = nullptr; QTimer* timer = nullptr; QTimer* countdownTimer = nullptr; QLabel* overlayLabel = nullptr; int secondsLeft = 0; }; // ------------------------------------------------------------------------------------------------- TimerWidget::TimerWidget(QWidget* parent) : QWidget(parent) , m_impl(new Impl(this)) { connect(m_impl->timer, &QTimer::timeout, this, &TimerWidget::timeout); } // ------------------------------------------------------------------------------------------------- TimerWidget::~TimerWidget() = default; // ------------------------------------------------------------------------------------------------- bool TimerWidget::timerEnabled() const { return m_impl->checkbox->isChecked(); } // ------------------------------------------------------------------------------------------------- void TimerWidget::setTimerEnabled(bool enabled) { m_impl->checkbox->setChecked(enabled); } // ------------------------------------------------------------------------------------------------- bool TimerWidget::timerRunning() const { return m_impl->timer->isActive(); } // ------------------------------------------------------------------------------------------------- void TimerWidget::start() { if (timerEnabled()) { m_impl->btnStartStop->setChecked(true); } } // ------------------------------------------------------------------------------------------------- void TimerWidget::stop() { m_impl->btnStartStop->setChecked(false); } // ------------------------------------------------------------------------------------------------- void TimerWidget::setValueSeconds(int seconds) { const std::chrono::seconds totalSecs(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( static_cast(hours.count()) ); m_impl->sbMinutes->setValue( static_cast(mins.count()) ); m_impl->sbSeconds->setValue( static_cast(secs.count()) ); } // ------------------------------------------------------------------------------------------------- void TimerWidget::setValueMinutes(int minutes) { setValueSeconds(minutes * 60); } // ------------------------------------------------------------------------------------------------- int TimerWidget::valueSeconds() const { return m_impl->valueSeconds(); } // ------------------------------------------------------------------------------------------------- struct MultiTimerWidget::Impl { explicit Impl(QWidget* parent) { for (size_t i = 0; i < numTimers; ++i) { timers.at(i) = new TimerWidget(parent); } } std::array timers = {}; }; // ------------------------------------------------------------------------------------------------- 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); layout->setAlignment(iconLabel, Qt::AlignTop); const auto groupBox = new QGroupBox(tr("Timers"), this); groupBox->setSizePolicy(groupBox->sizePolicy().horizontalPolicy(), QSizePolicy::Maximum); layout->addWidget(groupBox); layout->setAlignment(groupBox, Qt::AlignTop); const auto timerLayout = new QVBoxLayout(groupBox); for (uint32_t i = 0; i < numTimers; ++i) { timerLayout->addWidget(m_impl->timers.at(i)); 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); }); } layout->setStretch(1, 1); } // ------------------------------------------------------------------------------------------------- MultiTimerWidget::~MultiTimerWidget() = default; // ------------------------------------------------------------------------------------------------- int MultiTimerWidget::timerCount() { return numTimers; } // ------------------------------------------------------------------------------------------------- void MultiTimerWidget::setTimerEnabled(uint32_t timerId, bool enabled) { if (timerId >= numTimers) { return; } m_impl->timers.at(timerId)->setTimerEnabled(enabled); } // ------------------------------------------------------------------------------------------------- bool MultiTimerWidget::timerEnabled(uint32_t timerId) const { if (timerId >= numTimers) { return false; } return m_impl->timers.at(timerId)->timerEnabled(); } // ------------------------------------------------------------------------------------------------- void MultiTimerWidget::startTimer(uint32_t timerId) { if (timerId >= numTimers) { return; } m_impl->timers.at(timerId)->start(); } // ------------------------------------------------------------------------------------------------- void MultiTimerWidget::stopTimer(uint32_t timerId) { if (timerId >= numTimers) { return; } m_impl->timers.at(timerId)->stop(); } // ------------------------------------------------------------------------------------------------- void MultiTimerWidget::stopAllTimers() { for (size_t i = 0; i < numTimers; ++i) { m_impl->timers.at(i)->stop(); } } // ------------------------------------------------------------------------------------------------- bool MultiTimerWidget::timerRunning(uint32_t timerId) const { if (timerId >= numTimers) { return false; } return m_impl->timers.at(timerId)->timerRunning(); } // ------------------------------------------------------------------------------------------------- void MultiTimerWidget::setTimerValue(uint32_t timerId, int seconds) { if (timerId >= numTimers) { return; } m_impl->timers.at(timerId)->setValueSeconds(seconds); } // ------------------------------------------------------------------------------------------------- int MultiTimerWidget::timerValue(uint32_t timerId) const { if (timerId >= numTimers) { return -1; } return m_impl->timers.at(timerId)->valueSeconds(); } // ------------------------------------------------------------------------------------------------- VibrationSettingsWidget::VibrationSettingsWidget(QWidget* parent) : 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(vibrationIntensityMin, vibrationIntensityMax); const auto layout = new QHBoxLayout(this); const auto iconLabel = new IconLabel(Font::control_panel_9, this); layout->addWidget(iconLabel); layout->setAlignment(iconLabel, Qt::AlignTop); const auto groupBox = new QGroupBox(tr("Vibration Settings"), this); groupBox->setSizePolicy(groupBox->sizePolicy().horizontalPolicy(), QSizePolicy::Maximum); layout->addWidget(groupBox); layout->setAlignment(groupBox, Qt::AlignTop); const auto grid = new QGridLayout(groupBox); grid->addWidget(new QLabel(tr("Length"), this), 0, 0); grid->addWidget(new QLabel(tr("Intensity"), this), 1, 0); grid->addWidget(m_sbLength, 0, 1); grid->addWidget(m_sbIntensity, 1, 1); grid->setColumnStretch(0, 1); grid->setColumnStretch(1, 2); const auto testBtn = new QPushButton(tr("Test"), this); grid->addWidget(testBtn, 2, 0, 1, 2); m_sbLength->setValue(0x00); m_sbIntensity->setValue(0x80); connect(m_sbLength, static_cast(&QSpinBox::valueChanged), this, [this](int value){ emit lengthChanged(value); }); connect(m_sbIntensity, static_cast(&QSpinBox::valueChanged), this, [this](int value){ emit intensityChanged(value); }); connect(testBtn, &QPushButton::clicked, this, &VibrationSettingsWidget::sendVibrateCommand); layout->setStretch(1, 1); } // ------------------------------------------------------------------------------------------------- uint8_t VibrationSettingsWidget::length() const { return m_sbLength->value(); } // ------------------------------------------------------------------------------------------------- uint8_t VibrationSettingsWidget::intensity() const { return m_sbIntensity->value(); } // ------------------------------------------------------------------------------------------------- void VibrationSettingsWidget::setLength(uint8_t len) { if (m_sbLength->value() == len) { return; } m_sbLength->setValue(len); } // ------------------------------------------------------------------------------------------------- void VibrationSettingsWidget::setIntensity(uint8_t intensity) { if (m_sbIntensity->value() == intensity) { return; } m_sbIntensity->setValue(intensity); } // ------------------------------------------------------------------------------------------------- void VibrationSettingsWidget::setSubDeviceConnection(SubDeviceConnection *sdc) { m_subDeviceConnection = qobject_cast(sdc); } // ------------------------------------------------------------------------------------------------- void VibrationSettingsWidget::sendVibrateCommand() { 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(); 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()); }); } Projecteur-0.10/src/device-vibration.h000066400000000000000000000046211451344070600177550ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #pragma once #include #include #include class QSpinBox; class SubDeviceConnection; class SubHidppConnection; // ------------------------------------------------------------------------------------------------- class TimerWidget : public QWidget { Q_OBJECT public: TimerWidget(QWidget* parent); ~TimerWidget() override; bool timerEnabled() const; void setTimerEnabled(bool enabled); void start(); void stop(); bool timerRunning() const; void setValueSeconds(int seconds); void setValueMinutes(int minutes); int valueSeconds() const; signals: void timeout(); void valueSecondsChanged(int); void enabledChanged(bool); private: struct Impl; std::unique_ptr m_impl; }; // ------------------------------------------------------------------------------------------------- class MultiTimerWidget : public QWidget { Q_OBJECT public: explicit MultiTimerWidget(QWidget* parent = nullptr); virtual ~MultiTimerWidget() override; /// Returns the number of timers static int timerCount(); void setTimerEnabled(uint32_t timerId, bool enabled); bool timerEnabled(uint32_t timerId) const; void startTimer(uint32_t timerId); void stopTimer(uint32_t timerId); void stopAllTimers(); bool timerRunning(uint32_t timerId) const; void setTimerValue(uint32_t timerId, int seconds); int timerValue(uint32_t timerId) const; signals: /// Emitted when a timer times out. void timeout(uint32_t timerId); void timerEnabledChanged(uint32_t timerId, bool enabled); void timerValueChanged(uint32_t timerId, int seconds); private: struct Impl; std::unique_ptr m_impl; }; // ------------------------------------------------------------------------------------------------- class VibrationSettingsWidget : public QWidget { Q_OBJECT public: explicit VibrationSettingsWidget(QWidget* parent = nullptr); uint8_t length() const; void setLength(uint8_t len); uint8_t intensity() const; void setIntensity(uint8_t intensity); void setSubDeviceConnection(SubDeviceConnection* sdc); void sendVibrateCommand(); signals: void intensityChanged(uint8_t intensity); void lengthChanged(uint8_t length); private: QPointer m_subDeviceConnection; QSpinBox* m_sbLength = nullptr; QSpinBox* m_sbIntensity = nullptr; }; Projecteur-0.10/src/device.cc000066400000000000000000000451531451344070600161250ustar00rootroot00000000000000// 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 #include LOGGING_CATEGORY(device, "device") LOGGING_CATEGORY(hid, "HID") namespace { // ----------------------------------------------------------------------------------------------- #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 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; // ------------------------------------------------------------------------------------------------- bool DeviceConnection::hasSubDevice(const QString& path) const { const auto find_it = m_subDeviceConnections.find(path); return (find_it != m_subDeviceConnections.end() && find_it->second && find_it->second->isConnected()); } // ------------------------------------------------------------------------------------------------- void DeviceConnection::addSubDevice(std::shared_ptr sdc) { 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); } // ------------------------------------------------------------------------------------------------- bool DeviceConnection::removeSubDevice(const QString& path) { auto find_it = m_subDeviceConnections.find(path); if (find_it != m_subDeviceConnections.end()) { if (find_it->second) { find_it->second->disconnect(); } // Important logDebug(device) << tr("Disconnected sub-device: %1 (%2:%3) %4") .arg(m_deviceName, hexId(m_deviceId.vendorId), hexId(m_deviceId.productId), path); emit subDeviceDisconnected(m_deviceId, path); m_subDeviceConnections.erase(find_it); return true; } return false; } // ------------------------------------------------------------------------------------------------- std::shared_ptr DeviceConnection::subDevice(const QString& devicePath) const { const auto it = m_subDeviceConnections.find(devicePath); if (it == m_subDeviceConnections.cend()) { return {}; } return it->second; } // ------------------------------------------------------------------------------------------------- bool DeviceConnection::hasHidppSupport() const { // HID++ only for Logitech devices return m_deviceId.vendorId == 0x046d; } // ------------------------------------------------------------------------------------------------- SubDeviceConnectionDetails::SubDeviceConnectionDetails(const DeviceId& dId, const DeviceScan::SubDevice& sd, ConnectionType type, ConnectionMode mode) : deviceId(dId), type(type), mode(mode), devicePath(sd.deviceFile) {} // ------------------------------------------------------------------------------------------------- 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; } // ------------------------------------------------------------------------------------------------- bool SubDeviceConnection::isConnected() const { return false; } // ------------------------------------------------------------------------------------------------- void SubDeviceConnection::disconnect() { if (m_readNotifier) { m_readNotifier->setEnabled(false); m_readNotifier.reset(); } } // ------------------------------------------------------------------------------------------------- const std::shared_ptr& SubDeviceConnection::inputMapper() const { return m_inputMapper; } // ------------------------------------------------------------------------------------------------- QSocketNotifier* SubDeviceConnection::socketReadNotifier() { return m_readNotifier.get(); } // ------------------------------------------------------------------------------------------------- SubEventConnection::SubEventConnection(Token /* token */, const DeviceId& dId, const DeviceScan::SubDevice& sd) : SubDeviceConnection(dId, sd, ConnectionType::Event, ConnectionMode::ReadOnly) {} // ------------------------------------------------------------------------------------------------- SubEventConnection::~SubEventConnection() = default; // ------------------------------------------------------------------------------------------------- bool SubEventConnection::isConnected() const { return (m_readNotifier && m_readNotifier->isEnabled()); } // ------------------------------------------------------------------------------------------------- std::shared_ptr SubEventConnection::create(const DeviceScan::SubDevice& sd, const DeviceConnection& dc) { const int evfd = ::open(sd.deviceFile.toLocal8Bit().constData(), O_RDONLY, 0); if (evfd == -1) { logWarn(device) << tr("Cannot open event device '%1' for read.").arg(sd.deviceFile); return std::shared_ptr(); } struct input_id id{}; ioctl(evfd, EVIOCGID, &id); // get the event sub-device id // Check against given device id if (id.vendor != dc.deviceId().vendorId || id.product != dc.deviceId().productId) { ::close(evfd); logDebug(device) << tr("Device id mismatch: %1 (%2:%3)") .arg(sd.deviceFile, hexId(id.vendor), hexId(id.product)); return std::shared_ptr(); } unsigned long bitmask = 0; if (ioctl(evfd, EVIOCGBIT(0, sizeof(bitmask)), &bitmask) < 0) { ::close(evfd); logWarn(device) << tr("Cannot get device properties: %1 (%2:%3)") .arg(sd.deviceFile, hexId(id.vendor), hexId(id.product)); return std::shared_ptr(); } 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_REL))) { unsigned long relEvents = 0; ioctl(evfd, EVIOCGBIT(EV_REL, sizeof(relEvents)), &relEvents); const bool hasRelXEvents = !!(relEvents & (1 << REL_X)); const bool hasRelYEvents = !!(relEvents & (1 << REL_Y)); if (hasRelXEvents && hasRelYEvents) { connection->m_details.deviceFlags |= DeviceFlag::RelativeEvents; } } connection->m_details.grabbed = [&dc, evfd, &sd]() { // Grab device inputs if a virtual device exists. if (dc.inputMapper()->hasVirtualDevice()) { const int res = ioctl(evfd, EVIOCGRAB, 1); if (res == 0) { return true; } // Grab not successful logError(device) << tr("Error grabbing device: %1 (return value: %2)").arg(sd.deviceFile).arg(res); ioctl(evfd, EVIOCGRAB, 0); } return false; }(); fcntl(evfd, F_SETFL, fcntl(evfd, F_GETFL, 0) | O_NONBLOCK); if ((fcntl(evfd, F_GETFL, 0) & O_NONBLOCK) == O_NONBLOCK) { connection->m_details.deviceFlags |= DeviceFlag::NonBlocking; } // Create socket notifier 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, evfd, path=sd.deviceFile]() { if (grabbed) { ioctl(evfd, EVIOCGRAB, 0); } logDebug(device) << tr("Closing file descriptor for '%1'").arg(path); ::close(evfd); }); connection->m_inputMapper = dc.inputMapper(); return connection; } // ------------------------------------------------------------------------------------------------- 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 == errorResult) { logWarn(device) << tr("Cannot open hidraw device '%1' for read/write.").arg(sd.deviceFile); return errorResult; } { // 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); ::close(devfd); return errorResult; } } struct hidraw_devinfo devinfo {}; // get the hidraw sub-device id info if (ioctl(devfd, HIDIOCGRAWINFO, &devinfo) < 0) { logWarn(device) << tr("Cannot get info from hidraw device '%1'.").arg(sd.deviceFile); ::close(devfd); return errorResult; }; // Check against given device id if (static_cast(devinfo.vendor) != devId.vendorId || static_cast(devinfo.product) != devId.productId) { logDebug(device) << tr("Device id mismatch: %1 (%2:%3)") .arg(sd.deviceFile, hexId(devinfo.vendor), hexId(devinfo.product)); ::close(devfd); return errorResult; } return devfd; } // ------------------------------------------------------------------------------------------------- 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); } 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 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, [fdPtr, path]() { if (fdPtr && *fdPtr != -1) { logDebug(device) << tr("Closing file descriptor for '%1'").arg(path); ::close(*fdPtr); *fdPtr = -1; } }); 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, [fdPtr, path]() { if (fdPtr && *fdPtr != -1) { logDebug(device) << tr("Closing file descriptor for '%1'").arg(path); ::close(*fdPtr); *fdPtr = -1; } }); } // ------------------------------------------------------------------------------------------------- void SubHidrawConnection::onHidrawDataAvailable(int fd) { QByteArray readVal(20, 0); const auto res = ::read(fd, readVal.data(), readVal.size()); if (res < 0) { if (errno != EAGAIN) { emit socketReadError(errno); } return; } // 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); } // ------------------------------------------------------------------------------------------------- QStringList toStringList(DeviceFlags flags, bool withClass) { 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 list; } Projecteur-0.10/src/device.h000066400000000000000000000164421451344070600157660ustar00rootroot00000000000000// 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 // ------------------------------------------------------------------------------------------------- class InputMapper; class QSocketNotifier; class SubDeviceConnection; class VirtualDevice; // ------------------------------------------------------------------------------------------------- /// 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 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; using ConnectionMap = std::map>; DeviceId m_deviceId; QString m_deviceName; std::shared_ptr m_inputMapper; ConnectionMap m_subDeviceConnections; }; // ------------------------------------------------------------------------------------------------- enum class DeviceFlag : uint32_t { NoFlags = 0, NonBlocking = 1 << 0, SynEvents = 1 << 1, RepEvents = 1 << 2, RelativeEvents = 1 << 3, KeyEvents = 1 << 4, 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 DeviceId& dId, const DeviceScan::SubDevice& sd, ConnectionType type, ConnectionMode mode); DeviceId deviceId; ConnectionType type; ConnectionMode mode; bool grabbed = false; DeviceFlags deviceFlags = DeviceFlags::NoFlags; QString devicePath; }; // ------------------------------------------------------------------------------------------------- template struct InputBuffer { auto pos() const { return pos_; } void reset() { pos_ = 0; } auto data() { return data_.data(); } auto size() const { return data_.size(); } T& current() { return data_.at(pos_); } InputBuffer& operator++() { ++pos_; return *this; } T& operator[](size_t pos) { return data_[pos]; } T& first() { return data_[0]; } private: std::array data_; size_t pos_ = 0; }; // ------------------------------------------------------------------------------------------------- class SubDeviceConnection : public QObject, public async::Async { Q_OBJECT public: virtual ~SubDeviceConnection() = 0; virtual bool isConnected() const; virtual void disconnect(); // destroys socket notifier(s) and close file handle(s) 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; } 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 signals: void flagsChanged(DeviceFlags f); void socketReadError(int err); protected: 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::unique_ptr m_readNotifier; }; // ------------------------------------------------------------------------------------------------- class SubEventConnection : public SubDeviceConnection { Q_OBJECT class Token{}; public: static std::shared_ptr create(const DeviceScan::SubDevice& sd, const DeviceConnection& dc); SubEventConnection(Token, const DeviceId&, const DeviceScan::SubDevice&); virtual ~SubEventConnection(); bool isConnected() const; auto& inputBuffer() { return m_inputEventBuffer; } protected: InputBuffer<12> m_inputEventBuffer; }; // ------------------------------------------------------------------------------------------------- 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 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); }; Projecteur-0.10/src/deviceinput.cc000066400000000000000000001041461451344070600172030ustar00rootroot00000000000000// 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" #include #include #include #include #include LOGGING_CATEGORY(input, "input") namespace { // ----------------------------------------------------------------------------------------------- #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) const auto registered_ = qRegisterMetaTypeStreamOperators() && qRegisterMetaTypeStreamOperators(); #endif // ----------------------------------------------------------------------------------------------- void addKeyToString(QString& str, const QString& key) { if (!str.isEmpty()) { str += QLatin1Char('+'); } str += key; } // ----------------------------------------------------------------------------------------------- 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]); case 2: return QKeySequence(keys[0], keys[1]); case 1: return QKeySequence(keys[0]); } 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) : type(ie.type), code(ie.code), value(ie.value) {} bool DeviceInputEvent::operator==(const DeviceInputEvent& o) const { return std::tie(type,code,value) == std::tie(o.type,o.code,o.value); } bool DeviceInputEvent::operator!=(const DeviceInputEvent& o) const { return std::tie(type,code,value) != std::tie(o.type,o.code,o.value); } bool DeviceInputEvent::operator==(const input_event& o) const { return std::tie(type,code,value) == std::tie(o.type,o.code,o.value); } bool DeviceInputEvent::operator<(const DeviceInputEvent& o) const { return std::tie(type,code,value) < std::tie(o.type,o.code,o.value); } bool DeviceInputEvent::operator<(const input_event& o) const { return std::tie(type,code,value) < std::tie(o.type,o.code,o.value); } // ------------------------------------------------------------------------------------------------- QDataStream& operator<<(QDataStream& s, const DeviceInputEvent& die) { return s << die.type << die.code <>(QDataStream& s, DeviceInputEvent& die) { return s >> die.type >> die.code >> die.value; } // ------------------------------------------------------------------------------------------------- QDebug operator<<(QDebug debug, const DeviceInputEvent &ie) { QDebugStateSaver saver(debug); debug.nospace() << '{' << ie.type << ", " << ie.code << ", " << ie.value << '}'; return debug; } // ------------------------------------------------------------------------------------------------- QDebug operator<<(QDebug debug, const KeyEvent &ke) { QDebugStateSaver saver(debug); debug.nospace() << "["; for (const auto& e : ke) { debug.nospace() << e << ','; } debug.nospace() << "]"; return debug; } // ------------------------------------------------------------------------------------------------- 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(); return mia.action->load(s); case Action::Type::CyclePresets: mia.action = std::make_shared(); return mia.action->load(s); 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; } // ------------------------------------------------------------------------------------------------- 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; } switch(action->type()) { case Action::Type::KeySequence: return (*static_cast(action.get())) == (*static_cast(o.action.get())); case Action::Type::CyclePresets: return (*static_cast(action.get())) == (*static_cast(o.action.get())); 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; } // ------------------------------------------------------------------------------------------------- QDataStream& operator<<(QDataStream& s, const MappedAction& mia) { s << static_cast>(mia.action->type()); return mia.action->save(s); } // ------------------------------------------------------------------------------------------------- namespace { struct KeyEventItem { explicit KeyEventItem(KeyEvent ke = {}) : keyEvent(std::move(ke)) {} const KeyEvent keyEvent; std::shared_ptr action; std::vector nextMap; }; struct DeviceKeyMap { explicit DeviceKeyMap(const InputMapConfig& config = {}) { reconfigure(config); } enum Result : uint8_t { Miss, Valid, Hit, PartialHit }; Result feed(const struct input_event input_events[], size_t num); auto state() const { return m_pos; } void resetState(); void reconfigure(const InputMapConfig& config = {}); 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; } const auto ke = KeyEvent(KeyEvent(input_events, input_events + num)); const auto& nextMap = m_pos->nextMap; const auto find_it = std::find_if(nextMap.cbegin(), nextMap.cend(), [&ke](KeyEventItem const* next) { return next && ke == next->keyEvent; }); if (find_it == nextMap.cend()) { return Result::Miss; } m_pos = (*find_it); // Last KeyEvent in possible sequence... if (m_pos->nextMap.empty()) { return Result::Hit; } // KeyEvent in Sequence has action attached, but there are other possible sequences... if (m_pos->action) { return Result::PartialHit; } return Result::Valid; } // ------------------------------------------------------------------------------------------------- void DeviceKeyMap::resetState() { m_pos = &m_rootItem; } // ------------------------------------------------------------------------------------------------- void DeviceKeyMap::reconfigure(const InputMapConfig& config) { // -- clear maps + state resetState(); m_rootItem.nextMap.clear(); m_items.clear(); // -- 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; for (size_t i = 0; i < kes.size(); ++i) { const auto& keyEvent = kes[i]; const auto it = std::find_if(current->nextMap.cbegin(), current->nextMap.cend(), [&keyEvent](const KeyEventItem* item) { return (item && item->keyEvent == keyEvent); }); previous = current; if (it != current->nextMap.cend()) { current = *it; } else { // Create new item if not found m_items.emplace_back(KeyEventItem{keyEvent}); current = &m_items.back(); // link previous to current previous->nextMap.push_back(current); } // if last item in key event sequence if (i == kes.size() - 1) { current->action = configItem.second.action; } } } } // ------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------- NativeKeySequence::NativeKeySequence() = default; // ------------------------------------------------------------------------------------------------- NativeKeySequence::NativeKeySequence(const std::vector& qtKeys, std::vector&& nativeModifiers, KeyEventSequence&& kes) : m_keySequence(makeQKeySequence(qtKeys)) , m_nativeSequence(std::move(kes)) , m_nativeModifiers(std::move(nativeModifiers)) { } // ------------------------------------------------------------------------------------------------- bool NativeKeySequence::operator==(const NativeKeySequence &other) const { return m_keySequence == other.m_keySequence && m_nativeSequence == other.m_nativeSequence && m_nativeModifiers == other.m_nativeModifiers; } // ------------------------------------------------------------------------------------------------- bool NativeKeySequence::operator!=(const NativeKeySequence &other) const { return m_keySequence != other.m_keySequence || m_nativeSequence != other.m_nativeSequence || m_nativeModifiers != other.m_nativeModifiers; } // ------------------------------------------------------------------------------------------------- void NativeKeySequence::clear() { m_keySequence = QKeySequence{}; m_nativeModifiers.clear(); m_nativeSequence.clear(); } // ------------------------------------------------------------------------------------------------- int NativeKeySequence::count() const { return qMax(m_keySequence.count(), static_cast(m_nativeModifiers.size())); } // ------------------------------------------------------------------------------------------------- QString NativeKeySequence::toString() const { QString seqString; const size_t size = count(); for (size_t i = 0; i < size; ++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] : to_integral(Modifier::NoModifier)); } return seqString; } // ------------------------------------------------------------------------------------------------- QString NativeKeySequence::toString(int qtKey, uint16_t nativeModifiers) { QString keyStr; if (qtKey == 0) // Special case for manually created Key Sequences { if ((nativeModifiers & Modifier::LeftMeta) == Modifier::LeftMeta || (nativeModifiers & Modifier::RightMeta) == Modifier::RightMeta) { addKeyToString(keyStr, QLatin1String("Meta")); } if ((nativeModifiers & Modifier::LeftCtrl) == Modifier::LeftCtrl || (nativeModifiers & Modifier::RightCtrl) == Modifier::RightCtrl) { addKeyToString(keyStr, QLatin1String("Ctrl")); } if ((nativeModifiers & Modifier::LeftAlt) == Modifier::LeftAlt) { addKeyToString(keyStr, QLatin1String("Alt")); } if ((nativeModifiers & Modifier::RightAlt) == Modifier::RightAlt) { addKeyToString(keyStr, QLatin1String("AltGr")); } if ((nativeModifiers & Modifier::LeftShift) == Modifier::LeftShift || (nativeModifiers & Modifier::RightShift) == Modifier::RightShift) { addKeyToString(keyStr, QLatin1String("Shift")); } return keyStr; } if((qtKey & Qt::MetaModifier) == Qt::MetaModifier) { addKeyToString(keyStr, QLatin1String("Meta")); } if((qtKey & Qt::ControlModifier) == Qt::ControlModifier) { addKeyToString(keyStr, QLatin1String("Ctrl")); } if((qtKey & Qt::AltModifier) == Qt::AltModifier) { addKeyToString(keyStr, QLatin1String("Alt")); } if((qtKey & Qt::GroupSwitchModifier) == Qt::GroupSwitchModifier) { addKeyToString(keyStr, QLatin1String("AltGr")); } if((qtKey & Qt::ShiftModifier) == Qt::ShiftModifier) { addKeyToString(keyStr, QLatin1String("Shift")); } qtKey &= ~(Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier | Qt::KeypadModifier | Qt::GroupSwitchModifier); addKeyToString(keyStr, QKeySequence(qtKey).toString()); return keyStr; } // ------------------------------------------------------------------------------------------------- QString NativeKeySequence::toString(const std::vector& qtKeys, const std::vector& nativeModifiers) { QString seqString; const auto size = qtKeys.size(); for (size_t i = 0; i < size; ++i) { if (i > 0) { seqString += QLatin1String(", "); } seqString += toString(qtKeys[i], (i < nativeModifiers.size()) ? nativeModifiers[i] : to_integral(Modifier::NoModifier)); } return seqString; } // ------------------------------------------------------------------------------------------------- void NativeKeySequence::swap(NativeKeySequence& other) { m_keySequence.swap(other.m_keySequence); m_nativeSequence.swap(other.m_nativeSequence); m_nativeModifiers.swap(other.m_nativeModifiers); } // ------------------------------------------------------------------------------------------------- const NativeKeySequence& NativeKeySequence::predefined::altTab() { static const NativeKeySequence ks = [](){ NativeKeySequence ks; ks.m_keySequence = QKeySequence::fromString("Alt+Tab"); ks.m_nativeModifiers.push_back(NativeKeySequence::LeftAlt); KeyEvent pressed; KeyEvent released; pressed.emplace_back(EV_KEY, KEY_LEFTALT, 1); released.emplace_back(EV_KEY, KEY_LEFTALT, 0); pressed.emplace_back(EV_KEY, KEY_TAB, 1); released.emplace_back(EV_KEY, KEY_TAB, 0); pressed.emplace_back(EV_SYN, SYN_REPORT, 0); released.emplace_back(EV_SYN, SYN_REPORT, 0); ks.m_nativeSequence.emplace_back(std::move(pressed)); ks.m_nativeSequence.emplace_back(std::move(released)); return ks; }(); return ks; } // ------------------------------------------------------------------------------------------------- const NativeKeySequence& NativeKeySequence::predefined::altF4() { static const NativeKeySequence ks = [](){ NativeKeySequence ks; ks.m_keySequence = QKeySequence::fromString("Alt+F4"); ks.m_nativeModifiers.push_back(NativeKeySequence::LeftAlt); KeyEvent pressed; KeyEvent released; pressed.emplace_back(EV_KEY, KEY_LEFTALT, 1); released.emplace_back(EV_KEY, KEY_LEFTALT, 0); pressed.emplace_back(EV_KEY, KEY_F4, 1); released.emplace_back(EV_KEY, KEY_F4, 0); pressed.emplace_back(EV_SYN, SYN_REPORT, 0); released.emplace_back(EV_SYN, SYN_REPORT, 0); ks.m_nativeSequence.emplace_back(std::move(pressed)); ks.m_nativeSequence.emplace_back(std::move(released)); return ks; }(); return ks; } // ------------------------------------------------------------------------------------------------- const NativeKeySequence& NativeKeySequence::predefined::meta() { static const NativeKeySequence ks = [](){ NativeKeySequence ks; ks.m_nativeModifiers.push_back(NativeKeySequence::LeftMeta); KeyEvent pressed; KeyEvent released; pressed.emplace_back(EV_KEY, KEY_LEFTMETA, 1); released.emplace_back(EV_KEY, KEY_LEFTMETA, 0); pressed.emplace_back(EV_SYN, SYN_REPORT, 0); released.emplace_back(EV_SYN, SYN_REPORT, 0); ks.m_nativeSequence.emplace_back(std::move(pressed)); ks.m_nativeSequence.emplace_back(std::move(released)); return ks; }(); 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 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; // 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; std::pair m_lastState; std::vector m_events; InputMapConfig m_config; bool m_recordingMode = false; SpecialMoveInputs m_specialMoveInputs; }; // ------------------------------------------------------------------------------------------------- InputMapper::Impl::Impl(InputMapper* parent , std::shared_ptr virtualMouse , std::shared_ptr virtualKeyboard) : m_parent(parent) , 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(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 || action->empty()) { return; } logDebug(input) << "Input map execAction, type =" << toString(action->type()) << ", partial_hit =" << (r == DeviceKeyMap::Result::PartialHit); if (action->type() == Action::Type::KeySequence) { const auto keySequenceAction = static_cast(action.get()); logDebug(input) << "Emitting Key Sequence:" << keySequenceAction->keySequence.toString(); emitNativeKeySequence(keySequenceAction->keySequence); } else { emit m_parent->actionMapped(action); } } // ------------------------------------------------------------------------------------------------- void InputMapper::Impl::sequenceTimeout() { if(m_recordingMode) { emit m_parent->recordingFinished(false); return; } 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 (hasVirtualDevices() && !m_events.empty()) { forwardEvents(m_events); } resetState(); } else if (m_lastState.first == DeviceKeyMap::Result::PartialHit) { // Last input could have triggered an action, but we needed to wait for the timeout, since // other sequences could have been possible. if (m_lastState.second) { execAction(m_lastState.second->action, DeviceKeyMap::Result::PartialHit); } else if (hasVirtualDevices() && !m_events.empty()) { // TODO differentiate between mouse and keyboard events forwardEvents(m_events); m_events.resize(0); } resetState(); } } // ------------------------------------------------------------------------------------------------- void InputMapper::Impl::resetState() { m_keymap.resetState(); m_events.resize(0); } // ------------------------------------------------------------------------------------------------- void InputMapper::Impl::emitNativeKeySequence(const NativeKeySequence& ks) { 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) { events.emplace_back(input_event{{}, ie.type, ie.code, ie.value}); } m_vkeyboard->emitEvents(events); events.resize(0); } } // ------------------------------------------------------------------------------------------------- void InputMapper::Impl::record(const struct input_event input_events[], size_t num) { const auto ev = KeyEvent(input_events, input_events + num); if (!m_seqTimer->isActive()) { emit m_parent->recordingStarted(); } m_seqTimer->start(); emit m_parent->keyEventRecorded(ev); } // ------------------------------------------------------------------------------------------------- void InputMapper::Impl::forwardEvents(const std::vector& input_events) { forwardEvents(input_events.data(), input_events.size()); } // ------------------------------------------------------------------------------------------------- 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( 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::virtualKeyboard() const { return impl->m_vkeyboard; } // ------------------------------------------------------------------------------------------------- bool InputMapper::hasVirtualDevice() const { return impl->hasVirtualDevices(); } // ------------------------------------------------------------------------------------------------- bool InputMapper::recordingMode() const { return impl->m_recordingMode; } // ------------------------------------------------------------------------------------------------- void InputMapper::setRecordingMode(bool recording) { 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); } impl->m_seqTimer->stop(); resetState(); emit recordingModeChanged(impl->m_recordingMode); } // ------------------------------------------------------------------------------------------------- int InputMapper::keyEventInterval() const { return impl->m_seqTimer->interval(); } // ------------------------------------------------------------------------------------------------- void InputMapper::setKeyEventInterval(int interval) { impl->m_seqTimer->setInterval(std::min(Settings::inputSequenceIntervalRange().max, std::max(Settings::inputSequenceIntervalRange().min, interval))); } // ------------------------------------------------------------------------------------------------- void InputMapper::addEvents(const input_event* input_events, size_t num) { if (num == 0 || (!hasVirtualDevice())) { return; } // If no key mapping is configured ... if (!impl->m_recordingMode && !impl->m_keymap.hasConfig()) { // ... 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; } if (num == 1) { logWarning(input) << tr("Ignoring single SYN event received."); return; } // For mouse button press ignore MSC_SCAN events if (num == 3 && input_events[1].type == EV_KEY && (input_events[1].code == BTN_LEFT || input_events[1].code == BTN_RIGHT || input_events[1].code == BTN_MIDDLE) && input_events[0].type == EV_MSC && input_events[0].code == MSC_SCAN) { ++input_events; --num; } if (impl->m_recordingMode) { logDebug(input) << "Recorded device event:" << KeyEvent{input_events, input_events + num - 1}; impl->record(input_events, num-1); // exclude closing syn event for recording return; } 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 impl->m_seqTimer->stop(); impl->forwardEvents(impl->m_events); impl->resetState(); } else if (res == DeviceKeyMap::Result::Hit) { // Found a valid key sequence impl->m_seqTimer->stop(); 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::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(); } } // ------------------------------------------------------------------------------------------------- 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() { impl->resetState(); } // ------------------------------------------------------------------------------------------------- void InputMapper::setConfiguration(const InputMapConfig& config) { if (config == impl->m_config) { return; } impl->m_config = config; impl->resetState(); impl->m_keymap.reconfigure(impl->m_config); emit configurationChanged(); } // ------------------------------------------------------------------------------------------------- void InputMapper::setConfiguration(InputMapConfig&& config) { if (config == impl->m_config) { return; } impl->m_config.swap(config); impl->resetState(); impl->m_keymap.reconfigure(impl->m_config); emit configurationChanged(); } // ------------------------------------------------------------------------------------------------- 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 Projecteur-0.10/src/deviceinput.h000066400000000000000000000275251451344070600170520ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #pragma once #include #include #include #include #include class VirtualDevice; class QTimer; // ------------------------------------------------------------------------------------------------- /// This is basically the input_event struct from linux/input.h without the time member struct DeviceInputEvent { DeviceInputEvent() = default; DeviceInputEvent(uint16_t type, uint16_t code, int32_t value) : type(type), code(code), value(value) {} DeviceInputEvent(const struct input_event& ie); DeviceInputEvent(const DeviceInputEvent&) = default; DeviceInputEvent(DeviceInputEvent&&) = default; DeviceInputEvent& operator=(const DeviceInputEvent&) = default; DeviceInputEvent& operator=(DeviceInputEvent&&) = default; uint16_t type; uint16_t code; int32_t value; bool operator==(const DeviceInputEvent& o) const; bool operator!=(const DeviceInputEvent& o) const; bool operator==(const struct input_event& o) const; bool operator<(const DeviceInputEvent& o) const; bool operator<(const struct input_event& o) const; }; // ------------------------------------------------------------------------------------------------- QDataStream& operator<<(QDataStream& s, const DeviceInputEvent& die); QDataStream& operator>>(QDataStream& s, DeviceInputEvent& die); // ------------------------------------------------------------------------------------------------- template QDataStream& operator<<(QDataStream& s, const std::vector& container) { s << quint32(container.size()); for (const auto& item : container) { s << item; } return s; } template QDataStream& operator>>(QDataStream& s, std::vector& container) { quint32 size{}; s >> size; container.resize(size); for (quint64 i = 0; i < size; ++i) { s >> container[i]; } 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 { public: enum Modifier : uint16_t { NoModifier = 0, LeftCtrl = 1 << 0, RightCtrl = 1 << 1, LeftAlt = 1 << 2, RightAlt = 1 << 3, LeftShift = 1 << 4, RightShift = 1 << 5, LeftMeta = 1 << 6, RightMeta = 1 << 7, }; NativeKeySequence(); NativeKeySequence(NativeKeySequence&&) = default; NativeKeySequence(const NativeKeySequence&) = default; NativeKeySequence(const std::vector& qtKeys, std::vector&& nativeModifiers, KeyEventSequence&& kes); NativeKeySequence& operator=(NativeKeySequence&&) = default; NativeKeySequence& operator=(const NativeKeySequence&) = default; bool operator==(const NativeKeySequence& other) const; bool operator!=(const NativeKeySequence& other) const; void swap(NativeKeySequence& other); int count() const; bool empty() const { return count() == 0; } const auto& keySequence() const { return m_keySequence; } const auto& nativeSequence() const { return m_nativeSequence; } QString toString() const; void clear(); friend QDataStream& operator>>(QDataStream& s, NativeKeySequence& ks) { return s >> ks.m_keySequence >> ks.m_nativeSequence >> ks.m_nativeModifiers; } friend QDataStream& operator<<(QDataStream& s, const NativeKeySequence& ks) { return s << ks.m_keySequence << ks.m_nativeSequence << ks.m_nativeModifiers; } static QString toString(int qtKey, uint16_t nativeModifiers); static QString toString(const std::vector& qtKey, const std::vector& nativeModifiers); struct predefined { static const NativeKeySequence& altTab(); static const NativeKeySequence& altF4(); static const NativeKeySequence& meta(); }; private: QKeySequence m_keySequence; KeyEventSequence m_nativeSequence; std::vector m_nativeModifiers; }; Q_DECLARE_METATYPE(NativeKeySequence) // ------------------------------------------------------------------------------------------------- struct Action { enum class Type { 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 { KeySequenceAction() = default; KeySequenceAction(const NativeKeySequence& ks) : keySequence(ks) {} Type type() const override { return Type::KeySequence; } QDataStream& save(QDataStream& s) const override { return s << keySequence; } QDataStream& load(QDataStream& s) override { return s >> keySequence; } bool empty() const override { return keySequence.empty(); } bool operator==(const KeySequenceAction& o) const { return keySequence == o.keySequence; } NativeKeySequence keySequence; }; // ------------------------------------------------------------------------------------------------- struct CyclePresetsAction : public Action { Type type() const override { return Type::CyclePresets; } 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 CyclePresetsAction&) const { return true; } bool placeholder = false; }; // ------------------------------------------------------------------------------------------------- struct ToggleSpotlightAction : public Action { Type type() const override { return Type::ToggleSpotlight; } 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 ToggleSpotlightAction&) const { return true; } 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 { bool operator==(const MappedAction& o) const; std::shared_ptr action; }; Q_DECLARE_METATYPE(MappedAction); QDataStream& operator>>(QDataStream& s, MappedAction& mia); QDataStream& operator<<(QDataStream& s, const MappedAction& mia); // ------------------------------------------------------------------------------------------------- class InputMapConfig : public std::map{}; // ------------------------------------------------------------------------------------------------- class InputMapper : public QObject { Q_OBJECT public: 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); int keyEventInterval() const; void setKeyEventInterval(int interval); 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); void setConfiguration(InputMapConfig&& config); const InputMapConfig& configuration() const; signals: void configurationChanged(); void recordingModeChanged(bool recording); void keyEventRecorded(const KeyEvent&); // Right before first key event recorded: void recordingStarted(); // After key sequence interval timer timeout or max sequence length reached void recordingFinished(bool canceled); // canceled if recordingMode was set to false instead of interval time out void actionMapped(std::shared_ptr action); private: struct Impl; std::unique_ptr impl; }; Projecteur-0.10/src/devicescan.cc000066400000000000000000000303501451344070600167630ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #include "devicescan.h" #include #include #include #include #include // Function declaration to check for extra devices, definition in generated source bool isExtraDeviceSupported(quint16 vendorId, quint16 productId); QString getExtraDeviceName(quint16 vendorId, quint16 productId); namespace { class DeviceScan_ : public QObject {}; // for i18n and logging // ----------------------------------------------------------------------------------------------- // List of supported devices const std::array supportedDefaultDevices {{ {0x46d, 0xc53e, false, "Logitech Spotlight (USB)"}, {0x46d, 0xb503, true, "Logitech Spotlight (Bluetooth)"}, }}; // ----------------------------------------------------------------------------------------------- bool isDeviceSupported(quint16 vendorId, quint16 productId) { const auto it = std::find_if(supportedDefaultDevices.cbegin(), supportedDefaultDevices.cend(), [vendorId, productId](const SupportedDevice& d) { return (vendorId == d.vendorId) && (productId == d.productId); }); return (it != supportedDefaultDevices.cend()) || isExtraDeviceSupported(vendorId, productId); } // ----------------------------------------------------------------------------------------------- bool isAdditionallySupported(quint16 vendorId, quint16 productId, const std::vector& devices) { const auto it = std::find_if(devices.cbegin(), devices.cend(), [vendorId, productId](const SupportedDevice& d) { return (vendorId == d.vendorId) && (productId == d.productId); }); return (it != devices.cend()); } // ----------------------------------------------------------------------------------------------- // Return the defined device name for vendor/productId if defined in // any of the supported device lists (default, extra, additional) QString getUserDeviceName(quint16 vendorId, quint16 productId, const std::vector& additionalDevices) { const auto it = std::find_if(supportedDefaultDevices.cbegin(), supportedDefaultDevices.cend(), [vendorId, productId](const SupportedDevice& d) { return (vendorId == d.vendorId) && (productId == d.productId); }); if (it != supportedDefaultDevices.cend() && it->name.size()) { return it->name; } auto extraName = getExtraDeviceName(vendorId, productId); 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; } return QString(); } // ----------------------------------------------------------------------------------------------- quint64 readULongLongFromDeviceFile(const QString& filename) { QFile f(filename); if (f.open(QIODevice::ReadOnly)) { return f.readAll().trimmed().toULongLong(nullptr, 16); } return 0; } // ----------------------------------------------------------------------------------------------- QString readStringFromDeviceFile(const QString& filename) { QFile f(filename); if (f.open(QIODevice::ReadOnly)) { return f.readAll().trimmed(); } return QString(); } // ----------------------------------------------------------------------------------------------- QString readPropertyFromDeviceFile(const QString& filename, const QString& property) { QFile f(filename); if (f.open(QIODevice::ReadOnly)) { auto contents = f.readAll(); QTextStream in(&contents, QIODevice::ReadOnly); while (!in.atEnd()) { const auto line = in.readLine(); if (line.startsWith(property) && line.size() > property.size() && line[property.size()] == '=') { return line.mid(property.size() + 1); } } } return QString(); } // ----------------------------------------------------------------------------------------------- DeviceScan::Device deviceFromUEventFile(const QString& filename) { QFile f(filename); DeviceScan::Device spotlightDevice; static const QString hid_id("HID_ID"); static const QString hid_name("HID_NAME"); static const QString hid_phys("HID_PHYS"); static const std::array properties = {{ &hid_id, &hid_name, &hid_phys }}; if (!f.open(QIODevice::ReadOnly)) { return spotlightDevice; } auto contents = f.readAll(); QTextStream in(&contents, QIODevice::ReadOnly); while (!in.atEnd()) { const auto line = in.readLine(); for (const auto property : properties) { if (line.startsWith(*property) && line.size() > property->size() && line[property->size()] == '=') { const QString value = line.mid(property->size() + 1); if (*property == hid_id) { const auto ids = value.split(':'); const auto busType = ids.empty() ? 0: ids[0].toUShort(nullptr, 16); switch (busType) { 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) { spotlightDevice.name = value; } else if (*property == hid_phys) { spotlightDevice.id.phys = value.split('/').first(); } } } } return spotlightDevice; } } // end anonymous namespace namespace DeviceScan { // ----------------------------------------------------------------------------------------------- ScanResult getDevices(const std::vector& additionalDevices) { constexpr char hidDevicePath[] = "/sys/bus/hid/devices"; ScanResult result; const QFileInfo dpInfo(hidDevicePath); if (!dpInfo.exists()) { result.errorMessages.push_back(DeviceScan_::tr("HID device path '%1' does not exist.").arg(hidDevicePath)); return result; } if (!dpInfo.isExecutable()) { result.errorMessages.push_back(DeviceScan_::tr("HID device path '%1': Cannot list files.").arg(hidDevicePath)); return result; } QDirIterator hidIt(hidDevicePath, QDir::System | QDir::Dirs | QDir::Executable | QDir::NoDotAndDotDot); while (hidIt.hasNext()) { hidIt.next(); const QFileInfo uEventFile(QDir(hidIt.filePath()).filePath("uevent")); 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 (!isDeviceSupported(deviceId.vendorId, deviceId.productId) && !(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(), [&newDevice](const Device& existingDevice){ return existingDevice.id == newDevice.id; }); Device& rootDevice = [&find_it, &result, &newDevice, &additionalDevices]() -> Device& { if (find_it == result.devices.end()) { newDevice.userName = getUserDeviceName(newDevice.id.vendorId, newDevice.id.productId, additionalDevices); result.devices.emplace_back(std::move(newDevice)); return result.devices.back(); } return *find_it; }(); int eventSubDeviceCount = 0; // Iterate over 'input' sub-dircectory, check for input-hid device nodes const QFileInfo inputSubdir(QDir(hidIt.filePath()).filePath("input")); if (inputSubdir.exists() || inputSubdir.isExecutable()) { QDirIterator inputIt(inputSubdir.filePath(), QDir::System | QDir::Dirs | QDir::Executable | QDir::NoDotAndDotDot); while (inputIt.hasNext()) { inputIt.next(); SubDevice subDevice; QDirIterator dirIt(inputIt.filePath(), QDir::System | QDir::Dirs | QDir::Executable | QDir::NoDotAndDotDot); while (dirIt.hasNext()) { dirIt.next(); if (!dirIt.fileName().startsWith("event")) { continue; } subDevice.type = SubDevice::Type::Event; subDevice.deviceFile = readPropertyFromDeviceFile(QDir(dirIt.filePath()).filePath("uevent"), "DEVNAME"); if (!subDevice.deviceFile.isEmpty()) { subDevice.deviceFile = QDir("/dev").filePath(subDevice.deviceFile); break; } } if (subDevice.deviceFile.isEmpty()) { continue; } subDevice.phys = readStringFromDeviceFile(QDir(inputIt.filePath()).filePath("phys")); ++eventSubDeviceCount; // Check if device supports relative events const auto supportedEvents = readULongLongFromDeviceFile(QDir(inputIt.filePath()).filePath("capabilities/ev")); const bool hasRelativeEvents = !!(supportedEvents & (1 << EV_REL)); // Check if device supports relative x and y event types const auto supportedRelEv = readULongLongFromDeviceFile(QDir(inputIt.filePath()).filePath("capabilities/rel")); const bool hasRelXEvents = !!(supportedRelEv & (1 << REL_X)); const bool hasRelYEvents = !!(supportedRelEv & (1 << REL_Y)); subDevice.hasRelativeEvents = hasRelativeEvents && hasRelXEvents && hasRelYEvents; const QFileInfo fi(subDevice.deviceFile); subDevice.deviceReadable = fi.isReadable(); subDevice.deviceWritable = fi.isWritable(); rootDevice.subDevices.emplace_back(std::move(subDevice)); } } // 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")); if (hidrawSubdir.exists() || hidrawSubdir.isExecutable()) { QDirIterator hidrawIt(hidrawSubdir.filePath(), QDir::System | QDir::Dirs | QDir::Executable | QDir::NoDotAndDotDot); while (hidrawIt.hasNext()) { hidrawIt.next(); 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; } const QFileInfo fi(subDevice.deviceFile); subDevice.deviceReadable = fi.isReadable(); subDevice.deviceWritable = fi.isWritable(); rootDevice.subDevices.emplace_back(std::move(subDevice)); } } } } for (const auto& dev : result.devices) { const bool allReadable = std::all_of(dev.subDevices.cbegin(), dev.subDevices.cend(), [](const SubDevice& subDevice){ return subDevice.deviceReadable; }); const bool allWriteable = std::all_of(dev.subDevices.cbegin(), dev.subDevices.cend(), [](const SubDevice& subDevice){ return subDevice.deviceWritable; }); result.numDevicesReadable += allReadable; result.numDevicesWritable += allWriteable; } return result; } } // end namespace DeviceScan Projecteur-0.10/src/devicescan.h000066400000000000000000000026201451344070600166240ustar00rootroot00000000000000// 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 #include #include // ------------------------------------------------------------------------------------------------- struct SupportedDevice { quint16 vendorId; quint16 productId; bool isBluetooth = false; QString name = {}; }; // ------------------------------------------------------------------------------------------------- namespace DeviceScan { struct SubDevice { // Structure for device scan results enum class Type : uint8_t { Unknown, Event, Hidraw }; QString deviceFile; QString phys; Type type = Type::Unknown; bool hasRelativeEvents = false; bool deviceReadable = false; bool deviceWritable = false; }; struct Device { // Structure for device scan results const QString& getName() const { return userName.size() ? userName : name; } QString name; QString userName; DeviceId id; std::vector subDevices; }; struct ScanResult { std::vector devices; quint16 numDevicesReadable = 0; quint16 numDevicesWritable = 0; QStringList errorMessages; }; /// Scan for supported devices and check if they are accessible ScanResult getDevices(const std::vector& additionalDevices = {}); } Projecteur-0.10/src/deviceswidget.cc000066400000000000000000000624271451344070600175170ustar00rootroot00000000000000// 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" #include "inputmapconfig.h" #include "logging.h" #include "settings.h" #include "spotlight.h" #include #include #include #include #include #include #include #include #include #include #include DECLARE_LOGGING_CATEGORY(preferences) // ------------------------------------------------------------------------------------------------- namespace { const auto hexId = logging::hexId; QString descriptionString(const QString& name, const DeviceId& id) { return QString("%1 (%2:%3) [%4]").arg(name, hexId(id.vendorId), hexId(id.productId), id.phys); } 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) : QWidget(parent) { createDeviceComboBox(spotlight); const auto stackLayout = new QStackedLayout(this); const auto disconnectedWidget = createDisconnectedStateWidget(); stackLayout->addWidget(disconnectedWidget); const auto deviceWidget = createDevicesWidget(settings, spotlight); stackLayout->addWidget(deviceWidget); const bool anyDeviceConnected = spotlight->anySpotlightDeviceConnected(); stackLayout->setCurrentWidget(anyDeviceConnected ? deviceWidget : disconnectedWidget); connect(spotlight, &Spotlight::anySpotlightDeviceConnectedChanged, this, [stackLayout, deviceWidget, disconnectedWidget](bool anyConnected){ stackLayout->setCurrentWidget(anyConnected ? deviceWidget : disconnectedWidget); }); } // ------------------------------------------------------------------------------------------------- DeviceId DevicesWidget::currentDeviceId() const { if (m_devicesCombo->currentIndex() < 0) { return invalidDeviceId; } return qvariant_cast(m_devicesCombo->currentData()); } // ------------------------------------------------------------------------------------------------- TimerTabWidget* DevicesWidget::createTimerTabWidget(Settings* settings, Spotlight* spotlight) { Q_UNUSED(spotlight); const auto w = new TimerTabWidget(settings, this); w->loadSettings(currentDeviceId()); connect(this, &DevicesWidget::currentDeviceChanged, this, [this](const DeviceId& dId) { if (m_timerTabWidget) { m_timerTabWidget->loadSettings(dId); } }); return w; } // ------------------------------------------------------------------------------------------------- QWidget* DevicesWidget::createDevicesWidget(Settings* settings, Spotlight* spotlight) { const auto dw = new QWidget(this); const auto vLayout = new QVBoxLayout(dw); const auto devHLayout = new QHBoxLayout(); vLayout->addLayout(devHLayout); devHLayout->addWidget(new QLabel(tr("Device"), dw)); devHLayout->addWidget(m_devicesCombo); devHLayout->setStretch(1, 1); vLayout->addSpacing(10); m_tabWidget = new QTabWidget(dw); vLayout->addWidget(m_tabWidget); m_tabWidget->addTab(createInputMapperWidget(settings, spotlight), tr("Input Mapping")); m_timerTabWidget = createTimerTabWidget(settings, spotlight); updateTimerTab(spotlight); 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, [spotlight, this]() { updateTimerTab(spotlight); }); return dw; } // ------------------------------------------------------------------------------------------------- QWidget* DevicesWidget::createDeviceInfoWidget(Spotlight* spotlight) { 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 imWidget = new QWidget(this); const auto layout = new QVBoxLayout(imWidget); const auto intervalLayout = new QHBoxLayout(); const auto addBtn = new IconButton(Font::Icon::plus_5, imWidget); addBtn->setToolTip(tr("Add a new input mapping entry.")); const auto delBtn = new IconButton(Font::Icon::trash_can_1, imWidget); delBtn->setToolTip(tr("Delete the selected input mapping entries (%1).", "%1=shortcut") .arg(delShortcut->key().toString())); delBtn->setEnabled(false); const auto intervalLbl = new QLabel(tr("Input Sequence Interval"), imWidget); const auto intervalSb = new QSpinBox(this); const auto intervalUnitLbl = new QLabel(tr("ms"), imWidget); intervalSb->setMaximum(settings->inputSequenceIntervalRange().max); intervalSb->setMinimum(settings->inputSequenceIntervalRange().min); intervalSb->setValue(m_inputMapper ? m_inputMapper->keyEventInterval() : settings->deviceInputSeqInterval(currentDeviceId())); intervalSb->setSingleStep(50); intervalLayout->addWidget(addBtn); intervalLayout->addWidget(delBtn); intervalLayout->addStretch(1); intervalLayout->addWidget(intervalLbl); intervalLayout->addWidget(intervalSb); intervalLayout->addWidget(intervalUnitLbl); const auto tblView = new InputMapConfigView(imWidget); 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(); auto updateImWidget = [this, imWidget]() { imWidget->setDisabled(!m_inputMapper || !m_inputMapper->hasVirtualDevice()); }; updateImWidget(); connect(this, &DevicesWidget::currentDeviceChanged, this, [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(); }); connect(intervalSb, static_cast(&QSpinBox::valueChanged), this, [this, settings](int valueMs) { if (m_inputMapper) { m_inputMapper->setKeyEventInterval(valueMs); settings->setDeviceInputSeqInterval(currentDeviceId(), valueMs); } }); connect(selectionModel, &QItemSelectionModel::selectionChanged, this, [delBtn, selectionModel](){ delBtn->setEnabled(selectionModel->hasSelection()); }); auto removeCurrentSelection = [imModel, selectionModel](){ const auto selectedRows = selectionModel->selectedRows(); std::vector rows; rows.reserve(selectedRows.size()); for (const auto& selectedRow : selectedRows) { rows.emplace_back(selectedRow.row()); } imModel->removeConfigItemRows(std::move(rows)); }; connect(delBtn, &QToolButton::clicked, this, removeCurrentSelection); // --- Delete selected items on Shift + Delete connect(delShortcut, &QShortcut::activated, this, std::move(removeCurrentSelection)); connect(addBtn, &QToolButton::clicked, this, [imModel, tblView](){ tblView->selectRow(imModel->addNewItem(std::make_shared())); }); layout->addLayout(intervalLayout); layout->addWidget(tblView); return imWidget; } // ------------------------------------------------------------------------------------------------- void DevicesWidget::createDeviceComboBox(Spotlight* spotlight) { m_devicesCombo = new QComboBox(this); m_devicesCombo->setToolTip(tr("List of connected devices.")); for (const auto& dev : spotlight->connectedDevices()) { const auto data = QVariant::fromValue(dev.id); if (m_devicesCombo->findData(data) < 0) { m_devicesCombo->addItem(descriptionString(dev.name, dev.id), data); } } connect(spotlight, &Spotlight::deviceDisconnected, this, [this](const DeviceId& id, const QString& /*name*/) { const auto idx = m_devicesCombo->findData(QVariant::fromValue(id)); if (idx >= 0) { m_devicesCombo->removeItem(idx); } }); connect(spotlight, &Spotlight::deviceConnected, this, [this](const DeviceId& id, const QString& name) { const auto data = QVariant::fromValue(id); if (m_devicesCombo->findData(data) < 0) { m_devicesCombo->addItem(descriptionString(name, id), data); } }); connect(m_devicesCombo, static_cast(&QComboBox::currentIndexChanged), this, [this, spotlight](int index) { if (index < 0) { m_inputMapper = nullptr; emit currentDeviceChanged(invalidDeviceId); return; } const auto devId = qvariant_cast(m_devicesCombo->itemData(index)); const auto currentConn = spotlight->deviceConnection(devId); m_inputMapper = currentConn ? currentConn->inputMapper().get() : nullptr; emit currentDeviceChanged(devId); }); const auto currentConn = spotlight->deviceConnection(currentDeviceId()); m_inputMapper = currentConn ? currentConn->inputMapper().get() : nullptr; } // ------------------------------------------------------------------------------------------------- QWidget* DevicesWidget::createDisconnectedStateWidget() { const auto stateWidget = new QWidget(this); const auto hbox = new QHBoxLayout(stateWidget); const auto label = new QLabel(tr("No devices connected."), stateWidget); label->setToolTip(label->text()); auto icon = style()->standardIcon(QStyle::SP_MessageBoxWarning); const auto iconLabel = new QLabel(stateWidget); iconLabel->setPixmap(icon.pixmap(16,16)); hbox->addStretch(); hbox->addWidget(iconLabel); hbox->addWidget(label); hbox->addStretch(); 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); } } Projecteur-0.10/src/deviceswidget.h000066400000000000000000000066701451344070600173570ustar00rootroot00000000000000// 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 #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 { Q_OBJECT public: explicit DevicesWidget(Settings* settings, Spotlight* spotlight, QWidget* parent = nullptr); DeviceId currentDeviceId() const; signals: void currentDeviceChanged(const DeviceId&); private: QWidget* createDisconnectedStateWidget(); void createDeviceComboBox(Spotlight* spotlight); QWidget* createDevicesWidget(Settings* settings, Spotlight* spotlight); QWidget* createInputMapperWidget(Settings* settings, Spotlight* spotlight); QWidget* createDeviceInfoWidget(Spotlight* spotlight); TimerTabWidget* createTimerTabWidget(Settings* settings, Spotlight* spotlight); void updateTimerTab(Spotlight* spotlight); QComboBox* m_devicesCombo = 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; };Projecteur-0.10/src/enum-helper.h000066400000000000000000000034551451344070600167500ustar00rootroot00000000000000// 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) { \ return to_enum(to_integral(lhs) | to_integral(rhs)); } \ inline ENUMCLASS operator&(ENUMCLASS lhs, ENUMCLASS rhs) { \ return to_enum(to_integral(lhs) & to_integral(rhs)); } \ inline ENUMCLASS operator~(ENUMCLASS 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 == 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) Projecteur-0.10/src/extra-devices.cc.in000066400000000000000000000022261451344070600200300ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #include "devicescan.h" #include #include // Generated during CMake configuration time 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) { const auto it = std::find_if(supportedExtraDevices.cbegin(), supportedExtraDevices.cend(), [vendorId, productId](const SupportedDevice& d) { return (vendorId == d.vendorId) && (productId == d.productId); }); return it != supportedExtraDevices.cend(); }; QString getExtraDeviceName(quint16 vendorId, quint16 productId) { const auto it = std::find_if(supportedExtraDevices.cbegin(), supportedExtraDevices.cend(), [vendorId, productId](const SupportedDevice& d) { return (vendorId == d.vendorId) && (productId == d.productId); }); if (it != supportedExtraDevices.cend()) return it->name; return QString(); }; Projecteur-0.10/src/hidpp.cc000066400000000000000000000720601451344070600157670ustar00rootroot00000000000000// 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; } Projecteur-0.10/src/hidpp.h000066400000000000000000000347641451344070600156420ustar00rootroot00000000000000// 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); Projecteur-0.10/src/iconwidgets.cc000066400000000000000000000031251451344070600171760ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #include "iconwidgets.h" namespace { // ----------------------------------------------------------------------------------------------- bool isLight(const QColor& c) { return (c.redF() * 0.299 + c.greenF() * 0.587 + c.blueF() * 0.114 ) > 0.6; } bool isDark(const QColor& c) { return !isLight(c); } constexpr int defaultIconLabelSize = 32; } // end anonymous namespace // ------------------------------------------------------------------------------------------------- IconButton::IconButton(Font::Icon symbol, QWidget* parent) : QToolButton(parent) { QFont iconFont("projecteur-icons"); iconFont.setPointSizeF(font().pointSizeF()); setFont(iconFont); setText(QChar(symbol)); auto p = palette(); p.setColor(QPalette::ColorGroup::Normal, QPalette::ButtonText, isDark(p.color(QPalette::ButtonText)) ? QColor(Qt::darkGray).darker() : QColor(Qt::lightGray).lighter()); setPalette(p); } // ------------------------------------------------------------------------------------------------- IconLabel::IconLabel(Font::Icon symbol, QWidget* parent) : QLabel(QChar(symbol), parent) { QFont iconFont("projecteur-icons"); iconFont.setPixelSize(defaultIconLabelSize); setFont(iconFont); } // ------------------------------------------------------------------------------------------------- void IconLabel::setPixelSize(int pixelSize) { auto font = this->font(); font.setPixelSize(pixelSize); setFont(font); } Projecteur-0.10/src/iconwidgets.h000066400000000000000000000015001451344070600170330ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #pragma once #include "projecteur-icons-def.h" #include #include // ------------------------------------------------------------------------------------------------- /// Icon button class used throughout the application's widget based dialogs. class IconButton : public QToolButton { Q_OBJECT public: IconButton(Font::Icon symbol, QWidget* parent = nullptr); }; // ------------------------------------------------------------------------------------------------- /// Icon label class used throughout the application's widget based dialogs. class IconLabel : public QLabel { Q_OBJECT public: IconLabel(Font::Icon symbol, QWidget* parent = nullptr); void setPixelSize(int pixelSize); }; Projecteur-0.10/src/imageitem.cc000066400000000000000000000013371451344070600166230ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #include "imageitem.h" #include namespace { const bool registered = [](){ ProjecteurImage::qmlRegister(); return true; }(); } ProjecteurImage::ProjecteurImage(QQuickItem *parent) : QQuickPaintedItem(parent) { setRenderTarget(QQuickPaintedItem::FramebufferObject); } int ProjecteurImage::qmlRegister() { return qmlRegisterType("Projecteur.Utils", 1, 0, "Image"); } void ProjecteurImage::setPixmap(QPixmap pm) { m_pixmap = pm; update(); } void ProjecteurImage::paint(QPainter *painter) { painter->drawPixmap(QRectF(0, 0, width(), height()), m_pixmap, m_pixmap.rect()); } Projecteur-0.10/src/imageitem.h000066400000000000000000000011221451344070600164550ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #pragma once #include #include class ProjecteurImage : public QQuickPaintedItem { Q_OBJECT Q_PROPERTY(QPixmap pixmap READ pixmap WRITE setPixmap) public: static int qmlRegister(); explicit ProjecteurImage(QQuickItem *parent = nullptr); virtual ~ProjecteurImage() override = default; virtual void paint(QPainter *painter) override; void setPixmap(QPixmap pm); QPixmap pixmap() const { return m_pixmap; } private: QPixmap m_pixmap; }; Projecteur-0.10/src/inputmapconfig.cc000066400000000000000000000331671451344070600177130ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #include "inputmapconfig.h" #include "actiondelegate.h" #include "inputseqedit.h" #include "logging.h" #include #include // ------------------------------------------------------------------------------------------------- namespace { const InputMapModelItem invalidItem_; } // end anonymous namespace // ------------------------------------------------------------------------------------------------- InputMapConfigModel::InputMapConfigModel(InputMapper* im, const DeviceId& dId, QObject* parent) : QAbstractTableModel(parent) , m_currentDeviceId(dId) , m_inputMapper(im) {} // ------------------------------------------------------------------------------------------------- int InputMapConfigModel::rowCount(const QModelIndex& parent) const { return ( parent == QModelIndex() ) ? m_configItems.size() : 0; } // ------------------------------------------------------------------------------------------------- int InputMapConfigModel::columnCount(const QModelIndex& /*parent*/) const { return ColumnsCount; } // ------------------------------------------------------------------------------------------------- Qt::ItemFlags InputMapConfigModel::flags(const QModelIndex &index) const { if (index.column() == InputSeqCol || index.column() == ActionCol) { return (QAbstractTableModel::flags(index) | Qt::ItemIsEditable); } return QAbstractTableModel::flags(index) & ~Qt::ItemIsEditable; } // ------------------------------------------------------------------------------------------------- QVariant InputMapConfigModel::data(const QModelIndex& /*index*/, int /*role*/) const { // if (index.row() >= static_cast(m_configItems.size())) // return QVariant(); return QVariant(); } // ------------------------------------------------------------------------------------------------- QVariant InputMapConfigModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch(section) { case InputSeqCol: return tr("Input Sequence"); case ActionTypeCol: return "Type"; case ActionCol: return tr("Mapped Action"); default: return "Invalid"; } } else if (orientation == Qt::Vertical) { if (role == Qt::ForegroundRole) { if (m_configItems[section].isDuplicate) { return QColor(Qt::red); } } } return QAbstractTableModel::headerData(section, orientation, role); } // ------------------------------------------------------------------------------------------------- const InputMapModelItem& InputMapConfigModel::configData(const QModelIndex& index) const { if (index.row() >= static_cast(m_configItems.size())) { return invalidItem_; } return m_configItems[index.row()]; } // ------------------------------------------------------------------------------------------------- void InputMapConfigModel::removeConfigItemRows(int fromRow, int toRow) { if (fromRow > toRow) { return; } beginRemoveRows(QModelIndex(), fromRow, toRow); for (int i = toRow; i >= fromRow && i < m_configItems.size(); --i) { --m_duplicates[m_configItems[i].deviceSequence]; m_configItems.removeAt(i); } endRemoveRows(); } // ------------------------------------------------------------------------------------------------- int InputMapConfigModel::addNewItem(std::shared_ptr action) { if (!action) { return -1; } const auto row = m_configItems.size(); beginInsertRows(QModelIndex(), row, row); m_configItems.push_back({{}, std::move(action)}); endInsertRows(); return row; } // ------------------------------------------------------------------------------------------------- void InputMapConfigModel::configureInputMapper() { if (m_inputMapper) { m_inputMapper->setConfiguration(configuration()); } } // ------------------------------------------------------------------------------------------------- void InputMapConfigModel::removeConfigItemRows(std::vector rows) { if (rows.empty()) { return; } std::sort(rows.rbegin(), rows.rend()); int seq_last = rows.front(); int seq_first = seq_last; for (auto it = ++rows.cbegin(); it != rows.cend(); ++it) { if (seq_first - *it > 1) { removeConfigItemRows(seq_first, seq_last); seq_last = seq_first = *it; } else { seq_first = *it; } } removeConfigItemRows(seq_first, seq_last); configureInputMapper(); updateDuplicates(); } // ------------------------------------------------------------------------------------------------- void InputMapConfigModel::setInputSequence(const QModelIndex& index, const KeyEventSequence& kes) { if (index.row() < static_cast(m_configItems.size())) { auto& c = m_configItems[index.row()]; if (c.deviceSequence != kes) { --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}); } } } // ------------------------------------------------------------------------------------------------- void InputMapConfigModel::setKeySequence(const QModelIndex& index, const NativeKeySequence& ks) { if (index.row() < static_cast(m_configItems.size())) { auto& c = m_configItems[index.row()]; // 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) { c.action = std::make_shared(ks); configureInputMapper(); emit dataChanged(index, index, {Qt::DisplayRole, Roles::InputSeqRole}); } } } } // ------------------------------------------------------------------------------------------------- void InputMapConfigModel::setItemActionType(const QModelIndex& idx, Action::Type type) { if (idx.row() >= m_configItems.size()) { return; } auto& item = m_configItems[idx.row()]; if (item.action->type() == type) { return; } switch(type) { case Action::Type::KeySequence: item.action = std::make_shared(); break; case Action::Type::CyclePresets: item.action = std::make_shared(); break; 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(); emit dataChanged(index(idx.row(), ActionTypeCol), index(idx.row(), ActionCol)); } // ------------------------------------------------------------------------------------------------- InputMapper* InputMapConfigModel::inputMapper() const { return m_inputMapper; } // ------------------------------------------------------------------------------------------------- void InputMapConfigModel::setInputMapper(InputMapper* im) { m_inputMapper = im; if (m_inputMapper) { setConfiguration(m_inputMapper->configuration()); } } // ------------------------------------------------------------------------------------------------- InputMapConfig InputMapConfigModel::configuration() const { InputMapConfig config; for (const auto& item : m_configItems) { if (item.deviceSequence.empty()) { continue; } config.emplace(item.deviceSequence, MappedAction{item.action}); } return config; } // ------------------------------------------------------------------------------------------------- void InputMapConfigModel::setConfiguration(const InputMapConfig& config) { beginResetModel(); m_configItems.clear(); m_duplicates.clear(); for (const auto& item : config) { m_configItems.push_back(InputMapModelItem{item.first, item.second.action}); ++m_duplicates[item.first]; } endResetModel(); } // ------------------------------------------------------------------------------------------------- const DeviceId& InputMapConfigModel::deviceId() const { return m_currentDeviceId; } // ------------------------------------------------------------------------------------------------- void InputMapConfigModel::setDeviceId(const DeviceId& dId) { m_currentDeviceId = dId; } // ------------------------------------------------------------------------------------------------- void InputMapConfigModel::updateDuplicates() { for (int i = 0; i < m_configItems.size(); ++i) { auto& item = m_configItems[i]; const bool duplicate = item.deviceSequence.size() && m_duplicates[item.deviceSequence] > 1; if (item.isDuplicate != duplicate) { item.isDuplicate = duplicate; emit headerDataChanged(Qt::Vertical, i, i); } } } // ------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------- InputMapConfigView::InputMapConfigView(QWidget* parent) : QTableView(parent), m_actionTypeDelegate(new ActionTypeDelegate(this)) { verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); const auto imSeqDelegate = new InputSeqDelegate(this); setItemDelegateForColumn(InputMapConfigModel::InputSeqCol, imSeqDelegate); setItemDelegateForColumn(InputMapConfigModel::ActionTypeCol, m_actionTypeDelegate); const auto actionDelegate = new ActionDelegate(this); setItemDelegateForColumn(InputMapConfigModel::ActionCol, actionDelegate); setSelectionMode(QAbstractItemView::SelectionMode::ExtendedSelection); setSelectionBehavior(QAbstractItemView::SelectionBehavior::SelectRows); horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed); setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); connect(this, &QWidget::customContextMenuRequested, this, [this, imSeqDelegate, actionDelegate](const QPoint& pos) { const auto idx = indexAt(pos); if (!idx.isValid()) { return; } switch(idx.column()) { 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.column() == InputMapConfigModel::ActionTypeCol) { const auto pos = viewport()->mapToGlobal(visualRect(currentIndex()).bottomLeft()); m_actionTypeDelegate->actionContextMenu(this, qobject_cast(model()), idx, pos); } }); } // ------------------------------------------------------------------------------------------------- void InputMapConfigView::setModel(QAbstractItemModel* model) { QTableView::setModel(model); if (const auto m = qobject_cast(model)) { horizontalHeader()->setSectionResizeMode(InputMapConfigModel::Columns::ActionTypeCol, QHeaderView::ResizeMode::ResizeToContents); } } //------------------------------------------------------------------------------------------------- void InputMapConfigView::keyPressEvent(QKeyEvent* e) { switch (e->key()) { case Qt::Key_Enter: case Qt::Key_Return: if (currentIndex().column() == InputMapConfigModel::Columns::ActionTypeCol) { const auto pos = viewport()->mapToGlobal(visualRect(currentIndex()).bottomLeft()); m_actionTypeDelegate->actionContextMenu(this, qobject_cast(model()), currentIndex(), pos); } else if (model()->flags(currentIndex()) & Qt::ItemIsEditable) { edit(currentIndex()); return; } 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; } } break; case Qt::Key_Tab: e->ignore(); // Allow to change focus to other widgets in dialog. return; } QTableView::keyPressEvent(e); } Projecteur-0.10/src/inputmapconfig.h000066400000000000000000000055231451344070600175500ustar00rootroot00000000000000// 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 #include #include // ------------------------------------------------------------------------------------------------- class ActionTypeDelegate; class InputSeqDelegate; // ------------------------------------------------------------------------------------------------- /// Item for the input map model. struct InputMapModelItem { KeyEventSequence deviceSequence; std::shared_ptr action; bool isDuplicate = false; }; // ------------------------------------------------------------------------------------------------- /// Input map configuration table model. class InputMapConfigModel : public QAbstractTableModel { Q_OBJECT public: enum Roles { InputSeqRole = Qt::UserRole + 1, ActionTypeRole, NativeSeqRole }; enum Columns { InputSeqCol = 0, ActionTypeCol, ActionCol, ColumnsCount}; 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; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; Qt::ItemFlags flags(const QModelIndex& index) const override; void removeConfigItemRows(std::vector rows); int addNewItem(std::shared_ptr action); const InputMapModelItem& configData(const QModelIndex& index) const; void setInputSequence(const QModelIndex& index, const KeyEventSequence& kes); void setKeySequence(const QModelIndex& index, const NativeKeySequence& ks); void setItemActionType(const QModelIndex& index, Action::Type type); InputMapper* inputMapper() const; void setInputMapper(InputMapper* im); 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 public: InputMapConfigView(QWidget* parent = nullptr); void setModel(QAbstractItemModel* model) override; protected: void keyPressEvent(QKeyEvent* e) override; private: ActionTypeDelegate* m_actionTypeDelegate = nullptr; }; Projecteur-0.10/src/inputseqedit.cc000066400000000000000000000461321451344070600174020ustar00rootroot00000000000000// 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 #include #include #include #include #include namespace { // ----------------------------------------------------------------------------------------------- // Returns true if the second Key event 'equals' the first one, with the only difference, that the // first one is the key press and the second the release event. bool isButtonTap(const KeyEvent& first, const KeyEvent& second) { 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 return (e2.type == EV_KEY // special handling for key events... && e1.code == e2.code && e1.value == 1 // event 1 press && e2.value == 0); // event 2 release }); } // ----------------------------------------------------------------------------------------------- int drawKeyEvent(int startX, QPainter& p, const QStyleOption& option, const KeyEvent& ke, const DeviceId& dId, bool buttonTap = false) { if (ke.empty()) { return 0; } static auto const pressChar = QChar(0x2193); // ↓ static auto const releaseChar = QChar(0x2191); // ↑ 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(lookupName.isEmpty() ? QString("%1").arg(die.code, 0, 16) : lookupName) .arg(buttonTap ? pressChar : ke.back().value ? pressChar : releaseChar) .arg(buttonTap ? "" : "]"); const auto r = QRect(QPoint(startX + option.rect.left(), option.rect.top()), option.rect.bottomRight()); p.save(); if (option.state & QStyle::State_Selected) { p.setPen(option.palette.color(QPalette::HighlightedText)); } else { p.setPen(option.palette.color(QPalette::Text)); } QRect br; p.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, text, &br); if (buttonTap) { QRect br2; // draw down and up arrow closer together const auto t2 = QString("%2]").arg(releaseChar); const auto w = option.fontMetrics.rightBearing(pressChar) + option.fontMetrics.leftBearing(releaseChar); p.drawText(r.adjusted(br.width() - w, 0, 0, 0), Qt::AlignLeft | Qt::AlignVCenter, t2, &br2); br.setWidth(br.width() + br2.width()); } p.restore(); return br.width(); } // ----------------------------------------------------------------------------------------------- int drawKeyEventSequence(int startX, QPainter& p, const QStyleOption& option, const KeyEventSequence& kes, const DeviceId& dId, bool drawEmptyPlaceholder = true) { if (kes.empty()) { if (!drawEmptyPlaceholder) { return 0; } return InputSeqEdit::drawEmptyIndicator(startX, p, option); } int sequenceWidth = 0; 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; } const bool isTap = [&]() { // Check if this event and the next event represent a button press & release const auto next = std::next(it); if (next != kes.cend() && isButtonTap(*it, *next)) { it = next; return true; } return false; }(); sequenceWidth += drawKeyEvent(startX + sequenceWidth, p, option, *it, dId, isTap); } return sequenceWidth; } // ----------------------------------------------------------------------------------------------- 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, const DeviceId& dId, QWidget* parent) : QWidget(parent) , m_deviceId(dId) { setInputMapper(im); setFocusPolicy(Qt::StrongFocus); // Accept focus by tabbing and clicking setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); setAttribute(Qt::WA_InputMethodEnabled, false); setAttribute(Qt::WA_MacShowFocusRect, true); } // ------------------------------------------------------------------------------------------------- InputSeqEdit::~InputSeqEdit() = default; // ------------------------------------------------------------------------------------------------- QStyleOptionFrame InputSeqEdit::styleOption() const { QStyleOptionFrame option; option.initFrom(this); option.rect = contentsRect(); option.lineWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, &option, this); option.midLineWidth = 0; option.state |= (QStyle::State_Sunken | QStyle::State_ReadOnly); option.features = QStyleOptionFrame::None; return option; } // ------------------------------------------------------------------------------------------------- QSize InputSeqEdit::sizeHint() const { // Adjusted from QLineEdit::sizeHint (Qt 5.9) ensurePolished(); QFontMetrics fm(font()); constexpr int verticalMargin = 3; constexpr int horizontalMargin = 3; const int h = fm.height() + 2 * verticalMargin; #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) const int w = fm.horizontalAdvance(QLatin1Char('x')) * 17 + 2 * horizontalMargin; #else const int w = fm.width(QLatin1Char('x')) * 17 + 2 * horizontalMargin; #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* /* paintEvent */) { const QStyleOptionFrame option = styleOption(); QStylePainter p(this); p.drawPrimitive(QStyle::PE_PanelLineEdit, option); const bool recording = m_inputMapper && m_inputMapper->recordingMode(); const auto& fm = option.fontMetrics; int xPos = (option.rect.height()-fm.height()) / 2; if (recording) { const auto spacingX = QStaticText(" ").size().width(); xPos += drawRecordingSymbol(xPos, p, option) + spacingX; if (m_recordedSequence.empty()) { drawPlaceHolderText(xPos, p, option, tr("Press device button(s)...")); } else { drawKeyEventSequence(xPos, p, option, m_recordedSequence, m_deviceId, false); } } else { drawKeyEventSequence(xPos, p, option, m_inputSequence, m_deviceId); } } // ------------------------------------------------------------------------------------------------- const KeyEventSequence& InputSeqEdit::inputSequence() const { return m_inputSequence; } // ------------------------------------------------------------------------------------------------- void InputSeqEdit::setInputSequence(const KeyEventSequence& is) { if (is == m_inputSequence) { return; } m_inputSequence = is; update(); emit inputSequenceChanged(m_inputSequence); } // ------------------------------------------------------------------------------------------------- void InputSeqEdit::clear() { if (m_inputSequence.empty()) { return; } m_inputSequence.clear(); update(); emit inputSequenceChanged(m_inputSequence); } // ------------------------------------------------------------------------------------------------- void InputSeqEdit::mouseDoubleClickEvent(QMouseEvent* e) { QWidget::mouseDoubleClickEvent(e); if (!m_inputMapper) { return; } e->accept(); m_inputMapper->setRecordingMode(!m_inputMapper->recordingMode()); } //------------------------------------------------------------------------------------------------- void InputSeqEdit::keyPressEvent(QKeyEvent* e) { if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) { m_inputMapper->setRecordingMode(!m_inputMapper->recordingMode()); return; } if (e->key() == Qt::Key_Escape) { if (m_inputMapper && m_inputMapper->recordingMode()) { m_inputMapper->setRecordingMode(false); return; } } else if (e->key() == Qt::Key_Delete) { if (m_inputMapper && m_inputMapper->recordingMode()) { m_inputMapper->setRecordingMode(false); } else { setInputSequence(KeyEventSequence{}); } return; } QWidget::keyPressEvent(e); } // ------------------------------------------------------------------------------------------------- void InputSeqEdit::keyReleaseEvent(QKeyEvent* e) { QWidget::keyReleaseEvent(e); } // ------------------------------------------------------------------------------------------------- void InputSeqEdit::focusOutEvent(QFocusEvent* e) { if (m_inputMapper) { m_inputMapper->setRecordingMode(false); } QWidget::focusOutEvent(e); } // ------------------------------------------------------------------------------------------------- void InputSeqEdit::setInputMapper(InputMapper* im) { if (m_inputMapper == im) { return; } const auto removeIm = [this](){ if (m_inputMapper) { m_inputMapper->disconnect(this); this->disconnect(m_inputMapper); } m_inputMapper = nullptr; }; removeIm(); m_inputMapper = im; if (m_inputMapper == nullptr) { return; } connect(m_inputMapper, &InputMapper::destroyed, this, [removeIm=std::move(removeIm)](){ removeIm(); }); connect(m_inputMapper, &InputMapper::recordingStarted, this, [this](){ m_recordedSequence.clear(); }); connect(m_inputMapper, &InputMapper::recordingFinished, this, [this](bool canceled){ 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); } }); connect(m_inputMapper, &InputMapper::keyEventRecorded, this, [this](const KeyEvent& ke){ m_recordedSequence.push_back(ke); if (m_recordedSequence.size() >= m_maxRecordingLength) { setInputSequence(m_recordedSequence); m_inputMapper->setRecordingMode(false); } else { update(); } }); } // ------------------------------------------------------------------------------------------------- int InputSeqEdit::drawRecordingSymbol(int startX, QPainter& p, const QStyleOption& option) { const auto iconSize = option.fontMetrics.height(); const auto marginTop = (option.rect.height() - iconSize) / 2; const QRect iconRect(startX, marginTop, iconSize, iconSize); p.save(); p.setPen(Qt::lightGray); p.setBrush(QBrush(Qt::red)); p.setRenderHint(QPainter::Antialiasing); p.drawEllipse(iconRect); p.restore(); return iconRect.width(); } // ------------------------------------------------------------------------------------------------- int InputSeqEdit::drawPlaceHolderText(int startX, QPainter& p, const QStyleOption& option, const QString& text) { return ::drawPlaceHolderText(startX, p, option, text, true); } // ------------------------------------------------------------------------------------------------- int InputSeqEdit::drawEmptyIndicator(int startX, QPainter& p, const QStyleOption& option) { p.save(); p.setFont([&p](){ auto f = p.font(); f.setItalic(true); return f; }()); if (option.state & QStyle::State_Selected) { p.setPen(option.palette.color(QPalette::Disabled, QPalette::HighlightedText)); } else { p.setPen(option.palette.color(QPalette::Disabled, QPalette::Text)); } static const QStaticText textNone(InputSeqEdit::tr("None")); 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()); } // ------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------- void InputSeqDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { // Let QStyledItemDelegate handle drawing current focus inidicator and other basic stuff.. QStyledItemDelegate::paint(painter, option, index); const auto imModel = qobject_cast(index.model()); if (!imModel) { return; } // Our custom drawing of the KeyEventSequence... const auto& fm = option.fontMetrics; const int xPos = (option.rect.height()-fm.height()) / 2; 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); } } // ------------------------------------------------------------------------------------------------- void InputSeqDelegate::drawCurrentIndicator(QPainter &p, const QStyleOption &option) { p.save(); const auto squareSize = option.rect.height() / 3; const QRectF rect(option.rect.bottomRight()-QPoint(squareSize, squareSize), QSize(squareSize, squareSize)); const auto brush = QBrush((option.state & QStyle::State_Selected) ? option.palette.color(QPalette::HighlightedText) : option.palette.color(QPalette::Highlight)); { QPainterPath path(rect.topRight()); path.lineTo(rect.bottomRight()); path.lineTo(rect.bottomLeft()); path.lineTo(rect.topRight()); p.fillPath(path, brush); } p.restore(); } // ------------------------------------------------------------------------------------------------- QWidget* InputSeqDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& /*option*/, const QModelIndex& index) const { if (const auto imModel = qobject_cast(index.model())) { 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); } return editor; } return nullptr; } // ------------------------------------------------------------------------------------------------- void InputSeqDelegate::commitAndCloseEditor(InputSeqEdit* editor) { emit commitData(editor); emit closeEditor(editor); } // ------------------------------------------------------------------------------------------------- void InputSeqDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const { if (const auto seqEditor = qobject_cast(editor)) { if (const auto imModel = qobject_cast(index.model())) { seqEditor->setInputSequence(imModel->configData(index).deviceSequence); return; } } QStyledItemDelegate::setEditorData(editor, index); } // ------------------------------------------------------------------------------------------------- void InputSeqDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const { if (const auto seqEditor = qobject_cast(editor)) { if (const auto imModel = qobject_cast(model)) { imModel->setInputSequence(index, seqEditor->inputSequence()); return; } } QStyledItemDelegate::setModelData(editor, model, index); } // ------------------------------------------------------------------------------------------------- QSize InputSeqDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { if (const auto imModel = qobject_cast(index.model())) { // 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(); } } Projecteur-0.10/src/inputseqedit.h000066400000000000000000000054301451344070600172400ustar00rootroot00000000000000// 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 #include // ------------------------------------------------------------------------------------------------- class QStyleOptionFrame; class InputMapConfigModel; // ------------------------------------------------------------------------------------------------- class InputSeqEdit : public QWidget { Q_OBJECT public: InputSeqEdit(InputMapper* im, const DeviceId& dId, QWidget* parent = nullptr); ~InputSeqEdit(); QSize sizeHint() const override; const KeyEventSequence& inputSequence() const; void setInputSequence(const KeyEventSequence& is); void clear(); signals: void inputSequenceChanged(const KeyEventSequence& inputSequence); void editingFinished(InputSeqEdit*); public: // Public static helpers - can be reused by other editors or delegates static int drawRecordingSymbol(int startX, QPainter& p, const QStyleOption& option); static int drawPlaceHolderText(int startX, QPainter& p, const QStyleOption& option, const QString& text); 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; void keyReleaseEvent(QKeyEvent* e) override; void focusOutEvent(QFocusEvent* e) override; QStyleOptionFrame styleOption() const; private: DeviceId m_deviceId; InputMapper* m_inputMapper = nullptr; KeyEventSequence m_inputSequence; KeyEventSequence m_recordedSequence; // 8 KeyEvents, also equals 4 Button Presses (press + release) static constexpr uint8_t m_maxRecordingLength = 8; }; // ------------------------------------------------------------------------------------------------- class InputSeqDelegate : public QStyledItemDelegate { Q_OBJECT public: using QStyledItemDelegate::QStyledItemDelegate; void paint(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const override; QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const override; 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); private: void commitAndCloseEditor(InputSeqEdit* editor); }; Projecteur-0.10/src/linuxdesktop.cc000066400000000000000000000116011451344070600174060ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #include "linuxdesktop.h" #include "logging.h" #include #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) #include #endif #include #include #include #include #if HAS_Qt_DBus #include #include #endif LOGGING_CATEGORY(desktop, "desktop") namespace { #if HAS_Qt_DBus // ----------------------------------------------------------------------------------------------- QPixmap grabScreenDBusGnome() { const auto filepath = QDir::temp().absoluteFilePath("000_projecteur_zoom_screenshot.png"); QDBusInterface interface(QStringLiteral("org.gnome.Shell"), QStringLiteral("/org/gnome/Shell/Screenshot"), QStringLiteral("org.gnome.Shell.Screenshot")); QDBusReply reply = interface.call(QStringLiteral("Screenshot"), false, false, filepath); if (reply.value()) { QPixmap pm(filepath); QFile::remove(filepath); return pm; } logError(desktop) << LinuxDesktop::tr("Screenshot via GNOME DBus interface failed."); return QPixmap(); } // ----------------------------------------------------------------------------------------------- QPixmap grabScreenDBusKde() { QDBusInterface interface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Screenshot"), QStringLiteral("org.kde.kwin.Screenshot")); QDBusReply reply = interface.call(QStringLiteral("screenshotFullscreen")); QPixmap pm(reply.value()); if (!pm.isNull()) { QFile::remove(reply.value()); } else { logError(desktop) << LinuxDesktop::tr("Screenshot via KDE DBus interface failed."); } return pm; } #endif // HAS_Qt_DBus // ----------------------------------------------------------------------------------------------- QPixmap grabScreenVirtualDesktop(QScreen* screen) { QRect g; for (const auto s : QGuiApplication::screens()) { 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()) { pm.setDevicePixelRatio(screen->devicePixelRatio()); return pm.copy(screen->geometry()); } return pm; } } // end anonymous namespace LinuxDesktop::LinuxDesktop(QObject* parent) : QObject(parent) { const auto env = QProcessEnvironment::systemEnvironment(); { // check for Kde and Gnome const auto kdeFullSession = env.value(QStringLiteral("KDE_FULL_SESSION")); const auto gnomeSessionId = env.value(QStringLiteral("GNOME_DESKTOP_SESSION_ID")); const auto desktopSession = env.value(QStringLiteral("DESKTOP_SESSION")); 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; } } { // 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") || waylandDisplay.contains("wayland", Qt::CaseInsensitive); } } QPixmap LinuxDesktop::grabScreen(QScreen* screen) const { if (screen == nullptr) { return QPixmap(); } if (isWayland()) { return grabScreenWayland(screen); } #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) const bool isVirtualDesktop = QApplication::primaryScreen()->virtualSiblings().size() > 1; #else const bool isVirtualDesktop = QApplication::desktop()->isVirtualDesktop(); #endif if (isVirtualDesktop) { return grabScreenVirtualDesktop(screen); } // everything else.. usually X11 return screen->grabWindow(0); } QPixmap LinuxDesktop::grabScreenWayland(QScreen* screen) const { #if HAS_Qt_DBus QPixmap pm; switch (type()) { case LinuxDesktop::Type::Gnome: pm = grabScreenDBusGnome(); break; case LinuxDesktop::Type::KDE: pm = grabScreenDBusKde(); break; default: logWarning(desktop) << tr("Currently zoom on Wayland is only supported via DBus on KDE and GNOME."); } return pm.isNull() ? pm : pm.copy(screen->geometry()); #else Q_UNUSED(screen); logWarning(desktop) << tr("Projecteur was compiled without Qt DBus. Currently zoom on Wayland is " "only supported via DBus on KDE and GNOME."); return QPixmap(); #endif } Projecteur-0.10/src/linuxdesktop.h000066400000000000000000000011211451344070600172440ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #pragma once #include #include class QScreen; class LinuxDesktop : public QObject { Q_OBJECT public: enum class Type : uint8_t { KDE, Gnome, Other }; explicit LinuxDesktop(QObject* parent = nullptr); bool isWayland() const { return m_wayland; }; Type type() const { return m_type; }; QPixmap grabScreen(QScreen* screen) const; private: bool m_wayland = false; Type m_type = Type::Other; QPixmap grabScreenWayland(QScreen* screen) const; };Projecteur-0.10/src/logging.cc000066400000000000000000000160531451344070600163110ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #include "logging.h" #include #include #include #include #include #include #include namespace { // ----------------------------------------------------------------------------------------------- void projecteurLogHandler(QtMsgType type, const QMessageLogContext &context, const QString &msgQString); void categoryFilterInfo(QLoggingCategory *category); // Install our custom message handler, store previous message handler const QtMessageHandler defaultMessageHandler = qInstallMessageHandler(projecteurLogHandler); const QLoggingCategory::CategoryFilter defaultCategoryFilter = QLoggingCategory::installFilter(categoryFilterInfo); QLoggingCategory::CategoryFilter currentCategoryFilter = categoryFilterInfo; constexpr char categoryPrefix[] = "projecteur."; inline bool isAppCategory(QLoggingCategory* category) { return (qstrncmp(categoryPrefix, category->categoryName(), sizeof(categoryPrefix)-1) == 0); } void categoryFilterDebug(QLoggingCategory *category) { if (isAppCategory(category)) { category->setEnabled(QtDebugMsg, true); category->setEnabled(QtInfoMsg, true); category->setEnabled(QtWarningMsg, true); category->setEnabled(QtCriticalMsg, true); } else { defaultCategoryFilter(category); } } void categoryFilterInfo(QLoggingCategory *category) { if (isAppCategory(category)) { category->setEnabled(QtDebugMsg, false); category->setEnabled(QtInfoMsg, true); category->setEnabled(QtWarningMsg, true); category->setEnabled(QtCriticalMsg, true); } else { defaultCategoryFilter(category); } } void categoryFilterWarning(QLoggingCategory *category) { if (isAppCategory(category)) { category->setEnabled(QtDebugMsg, false); category->setEnabled(QtInfoMsg, false); category->setEnabled(QtWarningMsg, true); category->setEnabled(QtCriticalMsg, true); } else { defaultCategoryFilter(category); } } void categoryFilterError(QLoggingCategory *category) { if (isAppCategory(category)) { category->setEnabled(QtDebugMsg, false); category->setEnabled(QtInfoMsg, false); category->setEnabled(QtWarningMsg, false); category->setEnabled(QtCriticalMsg, true); } else { defaultCategoryFilter(category); } } // ----------------------------------------------------------------------------------------------- QPointer logPlainTextEdit; QMetaMethod logAppendMetaMethod; QList logPlainTextCache; // log messages are stored here until a text edit is registered constexpr int logPlainTextCacheMax = 1000; // ----------------------------------------------------------------------------------------------- void logToTextEdit(const QString& logMsg) { if (logPlainTextEdit) { logAppendMetaMethod.invoke(logPlainTextEdit, Qt::QueuedConnection, Q_ARG(QString, logMsg)); } else if (logPlainTextCache.size() < logPlainTextCacheMax) { logPlainTextCache.push_back(logMsg); } } // ----------------------------------------------------------------------------------------------- inline const char* typeToShortString(QtMsgType type) { switch (type) { case QtDebugMsg: return "dbg"; case QtInfoMsg: return "inf"; case QtWarningMsg: return "wrn"; case QtCriticalMsg: return "err"; case QtFatalMsg: return "fat"; } return ""; } // ----------------------------------------------------------------------------------------------- // Currently all logging is done from within the Qt Gui thread // - if that changes and multiple threads will log, this needs a serious overhaul - NOT thread safe void projecteurLogHandler(QtMsgType type, const QMessageLogContext &context, const QString &msgQString) { const char *category = context.category ? context.category : ""; #if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) constexpr auto dateFormat = Qt::ISODateWithMs; #else constexpr auto dateFormat = Qt::ISODate; #endif const auto logMsg = QString("[%1][%2][%3] %4").arg(QDateTime::currentDateTime().toString(dateFormat), typeToShortString(type), category, msgQString); if (type == QtDebugMsg || type == QtInfoMsg) { std::cout << qUtf8Printable(logMsg) << std::endl; } else { std::cerr << qUtf8Printable(logMsg) << std::endl; } logToTextEdit(logMsg); } } // end anonymous namespace namespace logging { void registerTextEdit(QPlainTextEdit* textEdit) { logPlainTextEdit = textEdit; if (!logPlainTextEdit) { return; } const auto index = logPlainTextEdit->metaObject()->indexOfMethod("appendPlainText(QString)"); logAppendMetaMethod = logPlainTextEdit->metaObject()->method(index); for (const auto& logMsg : logPlainTextCache) { logAppendMetaMethod.invoke(logPlainTextEdit, Qt::QueuedConnection, Q_ARG(QString, logMsg)); } logPlainTextCache.clear(); } const char* levelToString(level lvl) { switch (lvl) { case level::debug: return "debug"; case level::info: return "info"; case level::warning: return "warning"; case level::error: return "error"; case level::custom: return "default/custom"; case level::unknown: return "unknown"; } return ""; } 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; } 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; } return level::unknown; } void setCurrentLevel(level lvl) { QLoggingCategory::CategoryFilter newFilter = currentCategoryFilter; if (lvl == level::debug) { newFilter = categoryFilterDebug; } else if (lvl == level::info) { newFilter = categoryFilterInfo; } else if (lvl == level::warning) { newFilter = categoryFilterWarning; } else if (lvl == level::error) { newFilter = categoryFilterError; } else if (lvl == level::custom) { newFilter = defaultCategoryFilter; } if (newFilter != currentCategoryFilter) { QLoggingCategory::installFilter(newFilter); currentCategoryFilter = newFilter; } } QString hexId(uint16_t id) { return QString("%1").arg(id, 4, 16, QChar('0')); } } // end namespace logging Projecteur-0.10/src/logging.h000066400000000000000000000044141451344070600161510ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/Projecteur // - See LICENSE.md and README.md #pragma once #include #define _NARG__(...) _NARG_I_(__VA_ARGS__,_RSEQ_N()) #define _NARG_I_(...) _ARG_N(__VA_ARGS__) #define _ARG_N( \ _1, _2, _3, _4, _5, _6, _7, _8, _9,_10, \ _11,_12,_13,_14,_15,_16,_17,_18,_19,_20, \ _21,_22,_23,_24,_25,_26,_27,_28,_29,_30, \ _31,_32,_33,_34,_35,_36,_37,_38,_39,_40, \ _41,_42,_43,_44,_45,_46,_47,_48,_49,_50, \ _51,_52,_53,_54,_55,_56,_57,_58,_59,_60, \ _61,_62,_63,N,...) N #define _RSEQ_N() \ 2,2,2,2, \ 2,2,2,2,2,2,2,2,2,2, \ 2,2,2,2,2,2,2,2,2,2, \ 2,2,2,2,2,2,2,2,2,2, \ 2,2,2,2,2,2,2,2,2,2, \ 2,2,2,2,2,2,2,2,2,2, \ 2,2,2,2,2,2,2,2,1,0 #define _VLOGFUNC_(name, n) name##n #define _VLOGFUNC(name, n) _VLOGFUNC_(name, n) #define VLOGFUNC(func, ...) _VLOGFUNC(func, _NARG__(__VA_ARGS__)) (__VA_ARGS__) // macro 'overloading': // - call logDebug1 for one argument, logDebug2 for more than one argument (up to 64) #define logDebug(...) VLOGFUNC(logDebug, __VA_ARGS__) #define logDebug1(category) qCDebug(category).noquote() #define logDebug2(...) qCDebug(__VA_ARGS__) #define logInfo(...) VLOGFUNC(logInfo, __VA_ARGS__) #define logInfo1(category) qCInfo(category).noquote() #define logInfo2(...) qCInfo(__VA_ARGS__) #define logWarn(...) VLOGFUNC(logWarning, __VA_ARGS__) #define logWarning(...) VLOGFUNC(logWarning, __VA_ARGS__) #define logWarning1(category) qCWarning(category).noquote() #define logWarning2(...) qCWarning(__VA_ARGS__) #define logCritical(...) VLOGFUNC(logError, __VA_ARGS__) #define logError(...) VLOGFUNC(logError, __VA_ARGS__) #define logError1(category) qCCritical(category).noquote() #define logError2(...) qCCritical(__VA_ARGS__) #define LOGGING_CATEGORY(cat, name) Q_LOGGING_CATEGORY(cat, "projecteur." name) #define DECLARE_LOGGING_CATEGORY(name) extern const QLoggingCategory &name(); class QPlainTextEdit; namespace logging { enum class level { unknown = -1, custom = 0, debug = 1, info = 2, warning = 3, error = 4 }; const char* levelToString(level lvl); level levelFromName(const QString& name); level currentLevel(); void setCurrentLevel(level lvl); void registerTextEdit(QPlainTextEdit* textEdit); QString hexId(uint16_t id); } Projecteur-0.10/src/main.cc000066400000000000000000000445441451344070600156150ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #include "projecteurapp.h" #include "projecteur-GitVersion.h" #include "logging.h" #include "runguard.h" #include "settings.h" #include #ifndef NDEBUG #include #endif #include #include #include #define XSTRINGIFY(s) STRINGIFY(s) #define STRINGIFY(x) #x 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) { os << s.toStdString(); return os; } struct print { template auto& operator<<(const T& a) const { return std::cout << a; } ~print() { std::cout << std::endl; } }; struct error { template auto& operator<<(const T& a) const { return std::cerr << a; } ~error() { std::cerr << std::endl; } }; 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; 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"}; // --------------------------------------------------------------------------------------------- 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_}); } // --------------------------------------------------------------------------------------------- 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 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); } // --------------------------------------------------------------------------------------------- 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 (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() << ""; 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."); // Early return if the user not explicitly requested the full help if (!fullHelp) { return; } print() << "\n" << ""; int maxPropertyStringLength = 0; const std::vector> propertiesList = [&maxPropertyStringLength]() { std::vector> list; // Fill temporary list with properties to be able to format our output better Settings settings; // <-- FIXME unnecessary Settings instance for (const auto& sp : settings.stringProperties()) { list.emplace_back( QString("%1=[%2]").arg(sp.first, sp.second.typeToString(sp.second.type)), getValuesDescription(sp.second)); maxPropertyStringLength = qMax(maxPropertyStringLength, list.back().first.size()); } return list; }(); 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.additionalDeviceOptionSet()) { addDevices(options, parser.additionalDeviceOptionValues()); } // Print version information, if option is set if (parser.versionOptionSet() || parser.fullVersionOptionSet()) { printVersionInfo(options, parser.fullVersionOptionSet()); return 0; } // Print device information if option is set if (parser.deviceInfoOptionSet()) { printDeviceInfo(options); return 0; } // Check and trim ipc commands if set if (parser.commandOptionSet()) { ipcCommands = parser.commandOptionValues(); for (auto& value : ipcCommands) { value = value.trimmed(); } ipcCommands.removeAll(""); if (ipcCommands.isEmpty()) { error() << Main::tr("Command/Properties cannot be an empty string."); return PROJECTEUR_ERROR_EMPTY_COMMAND_PROPS; } } if (parser.cfgFileOptionSet()) { options.configFile = parser.cfgFileOptionValue(); } options.enableUInput = !parser.disableUInputOptionSet(); options.showPreferencesOnStart = parser.showDlgOnStartOptionSet(); options.dialogMinimizeOnly = parser.dialogMinOnlyOptionSet(); options.disableOverlay = parser.disableOverlayOptionSet(); 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.logLvlOptionValue()); } } } RunGuard guard(QCoreApplication::applicationName()); if (!guard.tryToRun()) { if (ipcCommands.size() > 0) { return ProjecteurCommandClientApp(ipcCommands, argc, argv).exec(); } error() << Main::tr("Another application instance is already running. Exiting."); return PROJECTEUR_ERROR_ANOTHER_INST_RUNNING; } 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 PROJECTEUR_ERROR_NO_INSTANCE_FOUND; } ProjecteurApplication app(argc, argv, options); signal(SIGINT, ctrl_c_signal_handler); return app.exec(); } Projecteur-0.10/src/nativekeyseqedit.cc000066400000000000000000000310211451344070600202310ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #include "nativekeyseqedit.h" #include "inputmapconfig.h" #include "inputseqedit.h" #include "logging.h" #include #include #include #include #include #include #include namespace { // ----------------------------------------------------------------------------------------------- constexpr int maxKeyCount = 4; // Same as QKeySequence constexpr int keySeqInterval = 950; } // end anonymous namespace // ------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------- NativeKeySeqEdit::NativeKeySeqEdit(QWidget* parent) : QWidget(parent) , m_timer(new QTimer(this)) { setFocusPolicy(Qt::StrongFocus); // Accept focus by tabbing and clicking setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); setAttribute(Qt::WA_InputMethodEnabled, false); setAttribute(Qt::WA_MacShowFocusRect, true); m_timer->setSingleShot(true); m_timer->setInterval(keySeqInterval); connect(m_timer, &QTimer::timeout, this, [this](){ setRecording(false); }); } // ------------------------------------------------------------------------------------------------- NativeKeySeqEdit::~NativeKeySeqEdit() = default; // ------------------------------------------------------------------------------------------------- const NativeKeySequence& NativeKeySeqEdit::keySequence() const { return m_nativeSequence; } // ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::setKeySequence(const NativeKeySequence& nks) { if (nks == m_nativeSequence) { return; } m_nativeSequence = nks; update(); emit keySequenceChanged(m_nativeSequence); } // ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::clear() { if (m_nativeSequence.count() == 0) { return; } m_nativeSequence.clear(); update(); emit keySequenceChanged(m_nativeSequence); } // ------------------------------------------------------------------------------------------------- QStyleOptionFrame NativeKeySeqEdit::styleOption() const { QStyleOptionFrame option; option.initFrom(this); option.rect = contentsRect(); option.lineWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, &option, this); option.midLineWidth = 0; option.state |= (QStyle::State_Sunken | QStyle::State_ReadOnly); option.features = QStyleOptionFrame::None; return option; } // ------------------------------------------------------------------------------------------------- QSize NativeKeySeqEdit::sizeHint() const { // Adjusted from QLineEdit::sizeHint (Qt 5.9) ensurePolished(); const QStyleOptionFrame opt = styleOption(); constexpr int verticalMargin = 3; constexpr int horizontalMargin = 3; const int h = opt.fontMetrics.height() + 2 * verticalMargin; #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) const int w = std::max(opt.fontMetrics.horizontalAdvance(QLatin1Char('x')) * 17 + 2 * horizontalMargin, opt.fontMetrics.horizontalAdvance(m_nativeSequence.toString())); #else const int w = std::max(opt.fontMetrics.width(QLatin1Char('x')) * 17 + 2 * horizontalMargin, 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* /* event */) { const QStyleOptionFrame option = styleOption(); QStylePainter p(this); p.drawPrimitive(QStyle::PE_PanelLineEdit, option); const auto& fm = option.fontMetrics; int xPos = (option.rect.height()-fm.height()) / 2; if (recording()) { const int spacingX = static_cast(QStaticText(" ").size().width()); xPos += drawRecordingSymbol(xPos, p, option) + spacingX; if (m_recordedQtKeys.empty()) { xPos += drawPlaceHolderText(xPos, p, option, tr("Press shortcut...")); } else { xPos += drawText(xPos, p, option, NativeKeySequence::toString(m_recordedQtKeys, m_recordedNativeModifiers)); xPos += drawText(xPos, p, option, ", ..."); } } else { xPos += drawSequence(xPos, p, option, m_nativeSequence); } } // ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::mouseDoubleClickEvent(QMouseEvent* e) { QWidget::mouseDoubleClickEvent(e); e->accept(); setRecording(!recording()); } // ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::reset() { m_timer->stop(); m_recordedQtKeys.clear(); m_recordedNativeModifiers.clear(); m_recordedEvents.clear(); m_lastKey = -1; m_nativeModifiersPressed.clear(); } // ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::setRecording(bool doRecord) { if (m_recording == doRecord) { return; } m_recording = doRecord; if (m_recording) { // started recording mode reset(); } else { // finished recording if (m_recordedQtKeys.size() > 0) { NativeKeySequence recorded(m_recordedQtKeys, std::move(m_recordedNativeModifiers), std::move(m_recordedEvents)); if (recorded != m_nativeSequence) { m_nativeSequence.swap(recorded); emit keySequenceChanged(m_nativeSequence); } } reset(); emit editingFinished(this); } update(); emit recordingChanged(m_recording); } //------------------------------------------------------------------------------------------------- bool NativeKeySeqEdit::event(QEvent* e) { switch (e->type()) { case QEvent::KeyPress: { const auto ke = static_cast(e); if (recording() && (ke->key() == Qt::Key_Tab || ke->key() == Qt::Key_Backtab)) { keyPressEvent(ke); e->accept(); return true; } break; } case QEvent::Shortcut: return true; case QEvent::ShortcutOverride: e->accept(); return true; default : break; } return QWidget::event(e); } // ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::recordKeyPressEvent(QKeyEvent* e) { int key = m_lastKey = e->key(); if (key == Qt::Key_Control || key == Qt::Key_Shift || key == Qt::Key_Meta || key == Qt::Key_Alt || key == Qt::Key_AltGr) { m_nativeModifiersPressed.insert(e->nativeScanCode() - 8); // See comment below about the -8; return; } if (key == Qt::Key_unknown) { return; } if (m_recordedQtKeys.size() >= maxKeyCount) { setRecording(false); return; } key |= getQtModifiers(e->modifiers()); m_recordedQtKeys.push_back(key); m_recordedNativeModifiers.push_back(getNativeModifiers(m_nativeModifiersPressed)); // TODO Verify that (nativeScanCode - 8) equals the codes from input-event-codes.h on // all Linux desktops.. (not only xcb..) - comes from #define MIN_KEYCODE 8 in evdev.c KeyEvent pressed; KeyEvent released; for (const auto modifierKey : m_nativeModifiersPressed) { pressed.emplace_back(EV_KEY, modifierKey, 1); released.emplace_back(EV_KEY, modifierKey, 0); } pressed.emplace_back(EV_KEY, e->nativeScanCode()-8, 1); released.emplace_back(EV_KEY, e->nativeScanCode()-8, 0); pressed.emplace_back(EV_SYN, SYN_REPORT, 0); released.emplace_back(EV_SYN, SYN_REPORT, 0); m_recordedEvents.emplace_back(std::move(pressed)); m_recordedEvents.emplace_back(std::move(released)); update(); e->accept(); if (m_recordedQtKeys.size() >= maxKeyCount) { setRecording(false); } } // ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::keyPressEvent(QKeyEvent* e) { if (!recording()) { if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) { setRecording(true); return; } if (e->key() == Qt::Key_Delete) { clear(); return; } QWidget::keyPressEvent(e); return; } recordKeyPressEvent(e); } // ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::keyReleaseEvent(QKeyEvent* e) { if (recording()) { if (e->key() == Qt::Key_Control || e->key() == Qt::Key_Shift || e->key() == Qt::Key_Meta || e->key() == Qt::Key_Alt || e->key() == Qt::Key_AltGr) { m_nativeModifiersPressed.erase(e->nativeScanCode() - 8); } if (m_recordedQtKeys.size() && m_lastKey == e->key()) { if (m_recordedQtKeys.size() < maxKeyCount) { m_timer->start(); } else { setRecording(false); } } return; } 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; } return result; } // ------------------------------------------------------------------------------------------------- uint16_t NativeKeySeqEdit::getNativeModifiers(const std::set& modifiersPressed) { using Modifier = NativeKeySequence::Modifier; uint16_t modifiers = Modifier::NoModifier; for (const auto& modKey : modifiersPressed) { switch(modKey) { case KEY_LEFTCTRL: modifiers |= Modifier::LeftCtrl; break; case KEY_RIGHTCTRL: modifiers |= Modifier::RightCtrl; break; case KEY_LEFTALT: modifiers |= Modifier::LeftAlt; break; case KEY_RIGHTALT: modifiers |= Modifier::RightAlt; break; case KEY_LEFTSHIFT: modifiers |= Modifier::LeftShift; break; case KEY_RIGHTSHIFT: modifiers |= Modifier::RightShift; break; case KEY_LEFTMETA: modifiers |= Modifier::LeftMeta; break; case KEY_RIGHTMETA: modifiers |= Modifier::RightMeta; break; default: break; } } return modifiers; } // ------------------------------------------------------------------------------------------------- int NativeKeySeqEdit::drawRecordingSymbol(int startX, QPainter& p, const QStyleOption& option) { return InputSeqEdit::drawRecordingSymbol(startX, p, option); } // ------------------------------------------------------------------------------------------------- int NativeKeySeqEdit::drawPlaceHolderText(int startX, QPainter& p, const QStyleOption& option, const QString& text) { return InputSeqEdit::drawPlaceHolderText(startX, p, option, text); } // ------------------------------------------------------------------------------------------------- int NativeKeySeqEdit::drawText(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(); if (option.state & QStyle::State_Selected) { p.setPen(option.palette.color(QPalette::HighlightedText)); } else { p.setPen(option.palette.color(QPalette::Text)); } QRect br; p.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, text, &br); p.restore(); return br.width(); } // ------------------------------------------------------------------------------------------------- int NativeKeySeqEdit::drawSequence(int startX, QPainter& p, const QStyleOption& option, const NativeKeySequence& ks, bool drawEmptyPlaceholder) { if (ks.count() == 0) { if (!drawEmptyPlaceholder) { return 0; } return InputSeqEdit::drawEmptyIndicator(startX, p, option); } return drawText(startX, p, option, ks.toString()); } Projecteur-0.10/src/nativekeyseqedit.h000066400000000000000000000051771451344070600201100ustar00rootroot00000000000000// 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 // and QKeySequenceEdit do not support native key codes, which are needed if we want to // emit key sequences via the uinput device. // // There is also no public API in Qt that allows us to map Qt Keycodes back to system key codes // and vice versa. #include "deviceinput.h" #include #include #include // ------------------------------------------------------------------------------------------------- class QStyleOption; class QStyleOptionFrame; // ------------------------------------------------------------------------------------------------- class NativeKeySeqEdit : public QWidget { Q_OBJECT public: NativeKeySeqEdit(QWidget* parent = nullptr); virtual ~NativeKeySeqEdit(); QSize sizeHint() const override; const NativeKeySequence& keySequence() const; void setKeySequence(const NativeKeySequence& nks); bool recording() const { return m_recording; } void setRecording(bool doRecord); void clear(); signals: void recordingChanged(bool); void keySequenceChanged(const NativeKeySequence& keySequence); void editingFinished(NativeKeySeqEdit*); public: // Public static helpers - can be reused by other editors or delegates static int drawRecordingSymbol(int startX, QPainter& p, const QStyleOption& option); static int drawPlaceHolderText(int startX, QPainter& p, const QStyleOption& option, const QString& text); static int drawText(int startX, QPainter& p, const QStyleOption& option, const QString& text); static int drawSequence(int startX, QPainter& p, const QStyleOption& option, const NativeKeySequence& ks, bool drawEmptyPlaceholder = true); protected: void paintEvent(QPaintEvent* e) override; void mouseDoubleClickEvent(QMouseEvent* e) override; bool event(QEvent* e) override; void keyPressEvent(QKeyEvent* e) override; void keyReleaseEvent(QKeyEvent* e) override; void focusOutEvent(QFocusEvent* e) override; QStyleOptionFrame styleOption() const; private: static int getQtModifiers(Qt::KeyboardModifiers state); static uint16_t getNativeModifiers(const std::set& modifiersPressed); void recordKeyPressEvent(QKeyEvent* e); void reset(); NativeKeySequence m_nativeSequence; std::vector m_recordedQtKeys; std::vector m_recordedNativeModifiers; std::set m_nativeModifiersPressed; KeyEventSequence m_recordedEvents; QTimer* m_timer = nullptr; int m_lastKey = -1; bool m_recording = false; }; Projecteur-0.10/src/preferencesdlg.cc000066400000000000000000001052671451344070600176610ustar00rootroot00000000000000// 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 #include "colorselector.h" #include "deviceswidget.h" #include "iconwidgets.h" #include "logging.h" #include "settings.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) #if HAS_Qt_X11Extras #include #endif #endif #include LOGGING_CATEGORY(preferences, "preferences") LOGGING_CATEGORY(x11display, "x11display") // ------------------------------------------------------------------------------------------------- namespace { #define CURSOR_PATH ":/icons/cursors/" static const std::map> cursorMap { { "", {"No Cursor", Qt::BlankCursor}}, { CURSOR_PATH "cursor-arrow.png", {"Arrow Cursor", Qt::ArrowCursor}}, { CURSOR_PATH "cursor-busy.png", {"Busy Cursor", Qt::BusyCursor}}, { CURSOR_PATH "cursor-cross.png", {"Cross Cursor", Qt::CrossCursor}}, { CURSOR_PATH "cursor-hand.png", {"Pointing Hand Cursor", Qt::PointingHandCursor}}, { CURSOR_PATH "cursor-openhand.png", {"Open Hand Cursor", Qt::OpenHandCursor}}, { 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, Mode dialogMode, QWidget* parent) : QDialog(parent) , m_presetComboStyle(std::make_unique()) , m_closeMinimizeBtn(new QPushButton(this)) , m_exitBtn(new QPushButton(tr("&Quit %1").arg(QCoreApplication::applicationName()),this)) { setWindowTitle(QCoreApplication::applicationName() + " - " + tr("Preferences")); setWindowIcon(QIcon(":/icons/projecteur-tray.svg")); setDialogMode(dialogMode); connect(m_closeMinimizeBtn, &QPushButton::clicked, this, [this](){ if (m_dialogMode == Mode::ClosableDialog) { this->close(); } else { this->showMinimized(); } }); connect(m_exitBtn, &QPushButton::clicked, this, [this](){ emit exitApplicationRequested(); }); const auto settingsWidget = createSettingsTabWidget(settings); settingsWidget->setDisabled(settings->overlayDisabled()); const auto tabWidget = new QTabWidget(this); tabWidget->addTab(settingsWidget, tr("Spotlight")); m_deviceswidget = new DevicesWidget(settings, spotlight, this); tabWidget->addTab(m_deviceswidget, tr("Devices")); tabWidget->addTab(createLogTabWidget(), tr("Log")); const auto overlayCheckBox = new QCheckBox(this); overlayCheckBox->setChecked(!settings->overlayDisabled()); tabWidget->tabBar()->setTabButton(0, QTabBar::ButtonPosition::LeftSide, overlayCheckBox); const auto btnHBox = new QHBoxLayout; btnHBox->addWidget(m_exitBtn); btnHBox->addStretch(1); btnHBox->addWidget(m_closeMinimizeBtn); const auto mainVBox = new QVBoxLayout(this); mainVBox->addWidget(tabWidget); mainVBox->addLayout(btnHBox); connect(overlayCheckBox, &QCheckBox::toggled, this, [settings](bool checked){ settings->setOverlayDisabled(!checked); }); connect(settings, &Settings::overlayDisabledChanged, this, [overlayCheckBox, settingsWidget](bool disabled){ overlayCheckBox->setChecked(!disabled); settingsWidget->setDisabled(disabled); }); } // ------------------------------------------------------------------------------------------------- QWidget* PreferencesDialog::createSettingsTabWidget(Settings* settings) { const auto widget = new QWidget(this); const auto mainHBox = new QHBoxLayout; const auto spotScreenVBoxLeft = new QVBoxLayout(); spotScreenVBoxLeft->addWidget(createShapeGroupBox(settings)); spotScreenVBoxLeft->addWidget(createZoomGroupBox(settings)); spotScreenVBoxLeft->addWidget(createCursorGroupBox(settings)); spotScreenVBoxLeft->addWidget(createMultiScreenWidget(settings)); const auto spotScreenVBoxRight = new QVBoxLayout(); spotScreenVBoxRight->addWidget(createSpotGroupBox(settings)); spotScreenVBoxRight->addWidget(createDotGroupBox(settings)); spotScreenVBoxRight->addWidget(createBorderGroupBox(settings)); mainHBox->addLayout(spotScreenVBoxLeft); mainHBox->addLayout(spotScreenVBoxRight); const auto presetSelector = createPresetSelector(settings); const auto resetBtn = new IconButton(Font::Icon::gear_12, widget); resetBtn->setToolTip(tr("Reset all settings to their default value.")); resetBtn->setSizePolicy(resetBtn->sizePolicy().horizontalPolicy(), QSizePolicy::Minimum); connect(resetBtn, &QPushButton::clicked, settings, &Settings::setDefaults); const auto testBtn = new QPushButton(tr("&Show test..."), widget); connect(testBtn, &QPushButton::clicked, this, &PreferencesDialog::testButtonClicked); const auto hbox = new QHBoxLayout; hbox->addWidget(resetBtn); hbox->addWidget(testBtn); const auto invisibleBtn = new QPushButton(this); invisibleBtn->setVisible(false); invisibleBtn->setDefault(true); const auto mainVBox = new QVBoxLayout(widget); mainVBox->addLayout(mainHBox); mainVBox->addWidget(presetSelector); #if HAS_Qt_X11Extras mainVBox->addWidget(createCompositorWarningWidget()); #endif mainVBox->addLayout(hbox); return widget; } // ------------------------------------------------------------------------------------------------- QWidget* PreferencesDialog::createPresetSelector(Settings* settings) { const auto widget = new QFrame(this); widget->setFrameStyle(QFrame::StyledPanel | QFrame::Plain); const auto hbox = new QHBoxLayout(widget); hbox->addWidget(new QLabel(tr("Presets"), widget)); m_presetCombo = new QComboBox(widget); m_presetCombo->setModel(settings->presetModel()); const auto normalComboStyle = m_presetCombo->style(); m_presetCombo->setStyle(&*m_presetComboStyle); // style when no preset is selected m_presetCombo->setInsertPolicy(QComboBox::NoInsert); const auto deleteBtn = new IconButton(Font::Icon::trash_can_1, widget); deleteBtn->setToolTip(tr("Delete currently selected preset.")); deleteBtn->setEnabled(m_presetCombo->currentIndex() > 0); const auto newBtn = new IconButton(Font::Icon::plus_5, widget); newBtn->setToolTip(tr("Create new preset from current spotlight settings.")); const std::vector widgets{m_presetCombo, deleteBtn, newBtn}; for (const auto w : widgets) { w->setSizePolicy(w->sizePolicy().horizontalPolicy(), QSizePolicy::Minimum); hbox->addWidget(w); } hbox->setStretch(1, 1); // stretch combobox connect(m_presetCombo, static_cast(&QComboBox::currentIndexChanged), widget, [deleteBtn, settings, normalComboStyle, this](int index) { deleteBtn->setEnabled(index > 0); m_presetCombo->setStyle(index == 0 ? &*m_presetComboStyle : normalComboStyle); if (index > 0 && !m_presetCombo->currentText().isEmpty()) { settings->loadPreset(m_presetCombo->currentText()); } }); connect(newBtn, &QPushButton::clicked, this, [newBtn, settings, this]() { newBtn->setEnabled(false); m_presetCombo->setEditable(true); const auto le = m_presetCombo->lineEdit(); le->setMaxLength(35); le->setCompleter(nullptr); connect(le, &QLineEdit::editingFinished, this, [le, settings, newBtn, this]() { auto text = le->text().trimmed(); m_presetCombo->setEditable(false); if (text.isEmpty()) { text = m_presetCombo->currentText().trimmed(); } if (m_presetCombo->findText(text) >= 0) { // Item with same name already exists text.append(" (%1)"); for (int i = 2; i < 1000; ++i) { if (m_presetCombo->findText(text.arg(i)) < 0) { text = text.arg(i); break; } } } newBtn->setEnabled(true); settings->savePreset(text); }); le->setText(tr("New Preset")); le->setFocus(); le->selectAll(); }); connect(deleteBtn, &QPushButton::clicked, this, [this, settings]() { if (m_presetCombo->currentIndex() < 0) { return; } settings->removePreset(m_presetCombo->currentText()); }); connect(settings, &Settings::presetLoaded, this, [normalComboStyle, deleteBtn, this](const QString& preset) { const auto idx = m_presetCombo->findText(preset); if (idx >= 0 && idx != m_presetCombo->currentIndex()) { m_presetCombo->blockSignals(true); m_presetCombo->setCurrentIndex(idx); m_presetCombo->blockSignals(false); m_presetCombo->setStyle(idx == 0 ? &*m_presetComboStyle : normalComboStyle); deleteBtn->setEnabled(idx > 0); } }); return widget; } // ------------------------------------------------------------------------------------------------- #if HAS_Qt_X11Extras QWidget* PreferencesDialog::createCompositorWarningWidget() { if (!QX11Info::isPlatformX11()) { // Platform ist not X11, possibly wayland or others... const auto widget = new QWidget(this); widget->setVisible(false); return widget; } const auto widget = new QFrame(this); widget->setFrameStyle(QFrame::StyledPanel | QFrame::Plain); const auto hbox = new QHBoxLayout(widget); const auto iconLabel = new QLabel(this); iconLabel->setPixmap(style()->standardPixmap(QStyle::SP_MessageBoxCritical)); hbox->addWidget(iconLabel); const auto textLabel = new QLabel(tr("Warning: No running compositing manager detected!"), this); textLabel->setTextFormat(Qt::RichText); textLabel->setToolTip(tr("Please make sure a compositing manager is running. " "On some systems one way is to run xcompmgr manually.")); hbox->addWidget(textLabel); hbox->setStretch(1, 1); const auto timer = new QTimer(this); timer->setInterval(1000); timer->setSingleShot(false); auto checkForCompositorAndUpdate = [widget](){ static bool compositorWasRunning = true; const bool compositorIsRunning = QX11Info::isCompositingManagerRunning(); if (compositorWasRunning != compositorIsRunning) { if (compositorIsRunning) { logInfo(x11display) << tr("Detected running compositing compositing manager."); } else { logWarning(x11display) << tr("No running compositing manager detected."); } } widget->setVisible(!compositorIsRunning); // Warning widget visible if no compositor is running. compositorWasRunning = compositorIsRunning; }; checkForCompositorAndUpdate(); connect(this, &PreferencesDialog::dialogActiveChanged, this, [timer, checkForCompositorAndUpdate](bool active) { if (active) { checkForCompositorAndUpdate(); timer->start(); } else { timer->stop(); } }); connect(timer, &QTimer::timeout, this, [checkForCompositorAndUpdate=std::move(checkForCompositorAndUpdate)]() { checkForCompositorAndUpdate(); }); return widget; } #endif // ------------------------------------------------------------------------------------------------- QGroupBox* PreferencesDialog::createShapeGroupBox(Settings* settings) { const auto shapeGroup = new QGroupBox(tr("Shape Settings"), this); const auto spotSizeSpinBox = new QSpinBox(this); spotSizeSpinBox->setMaximum(settings->spotSizeRange().max); spotSizeSpinBox->setMinimum(settings->spotSizeRange().min); spotSizeSpinBox->setValue(settings->spotSize()); const auto spotsizeHBox = new QHBoxLayout; spotsizeHBox->addWidget(spotSizeSpinBox); spotsizeHBox->addWidget(new QLabel(QString("% ")+tr("of screen height"))); connect(spotSizeSpinBox, static_cast(&QSpinBox::valueChanged), settings, &Settings::setSpotSize); connect(settings, &Settings::spotSizeChanged, spotSizeSpinBox, &QSpinBox::setValue); connect(settings, &Settings::spotSizeChanged, this, &PreferencesDialog::resetPresetCombo); const auto spotGrid = new QGridLayout(shapeGroup); spotGrid->addWidget(new QLabel(tr("Spot Size"), this), 0, 0); spotGrid->addLayout(spotsizeHBox, 0, 1); // Spotlight shape setting const auto shapeCombo = new QComboBox(this); for (const auto& shape : settings->spotShapes()) { shapeCombo->addItem(shape.displayName(), shape.qmlComponent()); } connect(settings, &Settings::spotShapeChanged, shapeCombo, [shapeCombo, this](const QString& spotShape){ const int idx = shapeCombo->findData(spotShape); if (idx != -1) { shapeCombo->setCurrentIndex(idx); } resetPresetCombo(); }); emit settings->spotShapeChanged(settings->spotShape()); spotGrid->addWidget(new QLabel(tr("Shape"), this), 4, 0); spotGrid->addWidget(shapeCombo, 4, 1); // Spotlight rotation setting const auto shapeRotationSb = new QDoubleSpinBox(this); shapeRotationSb->setMaximum(settings->spotRotationRange().max); shapeRotationSb->setMinimum(settings->spotRotationRange().min); shapeRotationSb->setDecimals(1); shapeRotationSb->setSingleStep(1.0); shapeRotationSb->setValue(settings->spotRotation()); connect(shapeRotationSb, static_cast(&QDoubleSpinBox::valueChanged), settings, &Settings::setSpotRotation); connect(settings, &Settings::spotRotationChanged, shapeRotationSb, &QDoubleSpinBox::setValue); connect(settings, &Settings::spotRotationChanged, this, &PreferencesDialog::resetPresetCombo); const auto shapeRotationLabel = new QLabel(tr("Rotation"), this); spotGrid->addWidget(shapeRotationLabel, 5, 0); spotGrid->addWidget(shapeRotationSb, 5, 1); // Function for updating all spotlight shape related widgets auto updateShapeSettingsWidgets = [settings, shapeCombo, shapeRotationSb, shapeRotationLabel, spotGrid, this]() { 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) { return shapeQml == s.qmlComponent(); }); constexpr int startRow = 100; constexpr int maxRows = 10; for (int row = startRow; row < startRow + maxRows; ++row) { if (const auto li = spotGrid->itemAtPosition(row, 0)) { if (const auto w = li->widget()) { w->hide(); w->deleteLater(); } } if (const auto li = spotGrid->itemAtPosition(row, 1)) { if (const auto w = li->widget()) { w->hide(); w->deleteLater(); } } } if (it != shapes.cend()) { shapeRotationLabel->setVisible(it->allowRotation()); shapeRotationSb->setVisible(it->allowRotation()); const auto& shape = *it; int row = startRow; for (const auto& s : it->shapeSettings()) { 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()); spinbox->setMinimum(s.minValue().toInt()); spinbox->setValue(s.defaultValue().toInt()); spotGrid->addWidget(spinbox, row, 1); const auto pm = settings->shapeSettings(shape.name()); if (pm && pm->property(s.settingsKey().toLocal8Bit()).isValid()) { spinbox->setValue(pm->property(s.settingsKey().toLocal8Bit()).toInt()); connect(spinbox, static_cast(&QSpinBox::valueChanged), pm, [s, pm](int newValue){ pm->setProperty(s.settingsKey().toLocal8Bit(), newValue); }); connect(pm, &QQmlPropertyMap::valueChanged, spinbox, [s, spinbox, this](const QString& key, const QVariant& value) { if (key != s.settingsKey() || !value.isValid()) { return; } spinbox->setValue(value.toInt()); resetPresetCombo(); }); } } ++row; } } }; connect(shapeCombo, static_cast(&QComboBox::currentIndexChanged), this, [settings, shapeCombo, updateShapeSettingsWidgets](int index) { const QString shapeQml = shapeCombo->itemData(index).toString(); settings->setSpotShape(shapeQml); updateShapeSettingsWidgets(); }); updateShapeSettingsWidgets(); spotGrid->addWidget(new QWidget(this), 200, 0); spotGrid->setRowStretch(200, 200); spotGrid->setColumnStretch(1, 1); return shapeGroup; } // ------------------------------------------------------------------------------------------------- QGroupBox* PreferencesDialog::createSpotGroupBox(Settings* settings) { const auto spotGroup = new QGroupBox(tr("Show Spotlight Shade"), this); spotGroup->setCheckable(true); spotGroup->setChecked(settings->showSpotShade()); connect(spotGroup, &QGroupBox::toggled, settings, &Settings::setShowSpotShade); connect(settings, &Settings::showSpotShadeChanged, spotGroup, &QGroupBox::setChecked); connect(settings, &Settings::showSpotShadeChanged, this, &PreferencesDialog::resetPresetCombo); const auto spotGrid = new QGridLayout(spotGroup); // Shade color setting const auto shadeColor = new ColorSelector(tr("Select Shade Color"), settings->shadeColor(), this); connect(shadeColor, &ColorSelector::colorChanged, settings, &Settings::setShadeColor); connect(settings, &Settings::shadeColorChanged, shadeColor, &ColorSelector::setColor); connect(settings, &Settings::shadeColorChanged, this, &PreferencesDialog::resetPresetCombo); spotGrid->addWidget(new QLabel(tr("Shade Color"), this), 1, 0); spotGrid->addWidget(shadeColor, 1, 1); // Spotlight shade opacity setting const auto shadeOpacitySb = new QDoubleSpinBox(this); shadeOpacitySb->setMaximum(settings->shadeOpacityRange().max); shadeOpacitySb->setMinimum(settings->shadeOpacityRange().min); shadeOpacitySb->setDecimals(2); shadeOpacitySb->setSingleStep(0.1); shadeOpacitySb->setValue(settings->shadeOpacity()); connect(shadeOpacitySb, static_cast(&QDoubleSpinBox::valueChanged), settings, &Settings::setShadeOpacity); connect(settings, &Settings::shadeOpacityChanged, shadeOpacitySb, &QDoubleSpinBox::setValue); connect(settings, &Settings::shadeOpacityChanged, this, &PreferencesDialog::resetPresetCombo); spotGrid->addWidget(new QLabel(tr("Shade Opacity"), this), 2, 0); spotGrid->addWidget(shadeOpacitySb, 2, 1); spotGrid->addWidget(new QWidget(this), 100, 0); spotGrid->setRowStretch(100, 100); spotGrid->setColumnStretch(1, 1); return spotGroup; } // ------------------------------------------------------------------------------------------------- QGroupBox* PreferencesDialog::createDotGroupBox(Settings* settings) { const auto dotGroup = new QGroupBox(tr("Show Center Dot"), this); dotGroup->setCheckable(true); dotGroup->setChecked(settings->showCenterDot()); connect(dotGroup, &QGroupBox::toggled, settings, &Settings::setShowCenterDot); connect(settings, &Settings::showCenterDotChanged, dotGroup, &QGroupBox::setChecked); connect(settings, &Settings::showCenterDotChanged, this, &PreferencesDialog::resetPresetCombo); const auto dotSizeSpinBox = new QSpinBox(this); dotSizeSpinBox->setMaximum(settings->dotSizeRange().max); dotSizeSpinBox->setMinimum(settings->dotSizeRange().min); dotSizeSpinBox->setValue(settings->dotSize()); auto dotsizeHBox = new QHBoxLayout; dotsizeHBox->addWidget(dotSizeSpinBox); dotsizeHBox->addWidget(new QLabel(tr("pixel"))); connect(dotSizeSpinBox, static_cast(&QSpinBox::valueChanged), settings, &Settings::setDotSize); connect(settings, &Settings::dotSizeChanged, dotSizeSpinBox, &QSpinBox::setValue); connect(settings, &Settings::dotSizeChanged, this, &PreferencesDialog::resetPresetCombo); const auto dotGrid = new QGridLayout(dotGroup); dotGrid->addWidget(new QLabel(tr("Dot Size"), this), 0, 0); dotGrid->addLayout(dotsizeHBox, 0, 1); const auto dotColor = new ColorSelector(tr("Select Dot Color"), settings->dotColor(), this); connect(dotColor, &ColorSelector::colorChanged, settings, &Settings::setDotColor); connect(settings, &Settings::dotColorChanged, dotColor, &ColorSelector::setColor); connect(settings, &Settings::dotColorChanged, this, &PreferencesDialog::resetPresetCombo); dotGrid->addWidget(new QLabel(tr("Dot Color"), this), 1, 0); dotGrid->addWidget(dotColor, 1, 1); // Spotlight dot opacity setting const auto dotOpacitySb = new QDoubleSpinBox(this); dotOpacitySb->setMaximum(settings->dotOpacityRange().max); dotOpacitySb->setMinimum(settings->dotOpacityRange().min); dotOpacitySb->setDecimals(2); dotOpacitySb->setSingleStep(0.1); dotOpacitySb->setValue(settings->dotOpacity()); connect(dotOpacitySb, static_cast(&QDoubleSpinBox::valueChanged), settings, &Settings::setDotOpacity); connect(settings, &Settings::borderOpacityChanged, dotOpacitySb, &QDoubleSpinBox::setValue); connect(settings, &Settings::borderOpacityChanged, this, &PreferencesDialog::resetPresetCombo); dotGrid->addWidget(new QLabel(tr("Dot Opacity"), this), 2, 0); dotGrid->addWidget(dotOpacitySb, 2, 1); dotGrid->addWidget(new QWidget(this), 100, 0); dotGrid->setRowStretch(100, 100); dotGrid->setColumnStretch(1, 1); return dotGroup; } // ------------------------------------------------------------------------------------------------- QGroupBox* PreferencesDialog::createBorderGroupBox(Settings* settings) { const auto borderGroup = new QGroupBox(tr("Show Border"), this); borderGroup->setCheckable(true); borderGroup->setChecked(settings->showBorder()); connect(borderGroup, &QGroupBox::toggled, settings, &Settings::setShowBorder); connect(settings, &Settings::showBorderChanged, borderGroup, &QGroupBox::setChecked); connect(settings, &Settings::showBorderChanged, this, &PreferencesDialog::resetPresetCombo); const auto borderSizeSpinBox = new QSpinBox(this); borderSizeSpinBox->setMaximum(settings->borderSizeRange().max); borderSizeSpinBox->setMinimum(settings->borderSizeRange().min); borderSizeSpinBox->setValue(settings->borderSize()); auto bordersizeHBox = new QHBoxLayout; bordersizeHBox->addWidget(borderSizeSpinBox); bordersizeHBox->addWidget(new QLabel(tr("% of spotsize"))); connect(borderSizeSpinBox, static_cast(&QSpinBox::valueChanged), settings, &Settings::setBorderSize); connect(settings, &Settings::borderSizeChanged, borderSizeSpinBox, &QSpinBox::setValue); connect(settings, &Settings::borderSizeChanged, this, &PreferencesDialog::resetPresetCombo); const auto borderGrid = new QGridLayout(borderGroup); borderGrid->addWidget(new QLabel(tr("Border Size"), this), 0, 0); borderGrid->addLayout(bordersizeHBox, 0, 1); const auto borderColor = new ColorSelector(tr("Select Border Color"), settings->borderColor(), this); connect(borderColor, &ColorSelector::colorChanged, settings, &Settings::setBorderColor); connect(settings, &Settings::borderColorChanged, borderColor, &ColorSelector::setColor); connect(settings, &Settings::borderColorChanged, this, &PreferencesDialog::resetPresetCombo); borderGrid->addWidget(new QLabel(tr("Border Color"), this), 1, 0); borderGrid->addWidget(borderColor, 1, 1); // Spotlight border opacity setting const auto borderOpacitySb = new QDoubleSpinBox(this); borderOpacitySb->setMaximum(settings->borderOpacityRange().max); borderOpacitySb->setMinimum(settings->borderOpacityRange().min); borderOpacitySb->setDecimals(2); borderOpacitySb->setSingleStep(0.1); borderOpacitySb->setValue(settings->borderOpacity()); connect(borderOpacitySb, static_cast(&QDoubleSpinBox::valueChanged), settings, &Settings::setBorderOpacity); connect(settings, &Settings::borderOpacityChanged, borderOpacitySb, &QDoubleSpinBox::setValue); connect(settings, &Settings::borderOpacityChanged, this, &PreferencesDialog::resetPresetCombo); borderGrid->addWidget(new QLabel(tr("Border Opacity"), this), 2, 0); borderGrid->addWidget(borderOpacitySb, 2, 1); borderGrid->addWidget(new QWidget(this), 100, 0); borderGrid->setRowStretch(100, 100); borderGrid->setColumnStretch(1, 1); return borderGroup; } // ------------------------------------------------------------------------------------------------- QGroupBox* PreferencesDialog::createZoomGroupBox(Settings* settings) { const auto zoomGroup = new QGroupBox(tr("Enable Zoom"), this); zoomGroup->setCheckable(true); zoomGroup->setChecked(settings->zoomEnabled()); connect(zoomGroup, &QGroupBox::toggled, settings, &Settings::setZoomEnabled); connect(settings, &Settings::zoomEnabledChanged, zoomGroup, &QGroupBox::setChecked); connect(settings, &Settings::zoomEnabledChanged, this, &PreferencesDialog::resetPresetCombo); const auto zoomGrid = new QGridLayout(zoomGroup); // zoom level setting const auto zoomLevelSb = new QDoubleSpinBox(this); zoomLevelSb->setMaximum(settings->zoomFactorRange().max); zoomLevelSb->setMinimum(settings->zoomFactorRange().min); zoomLevelSb->setDecimals(2); zoomLevelSb->setSingleStep(0.1); zoomLevelSb->setValue(settings->zoomFactor()); connect(zoomLevelSb, static_cast(&QDoubleSpinBox::valueChanged), settings, &Settings::setZoomFactor); connect(settings, &Settings::zoomFactorChanged, zoomLevelSb, &QDoubleSpinBox::setValue); connect(settings, &Settings::zoomFactorChanged, this, &PreferencesDialog::resetPresetCombo); zoomGrid->addWidget(new QLabel(tr("Zoom Level"), this), 0, 0); zoomGrid->addWidget(zoomLevelSb, 0, 1); zoomGrid->setColumnStretch(1, 1); return zoomGroup; } // ------------------------------------------------------------------------------------------------- QGroupBox* PreferencesDialog::createCursorGroupBox(Settings* settings) { const auto cursorGroup = new QGroupBox(tr("Cursor Settings"), this); cursorGroup->setCheckable(false); const auto grid = new QGridLayout(cursorGroup); const auto cursorCb = new QComboBox(this); for (const auto& item : cursorMap) { cursorCb->addItem(QIcon(item.first), item.second.first, static_cast(item.second.second)); } connect(settings, &Settings::cursorChanged, cursorCb, [cursorCb, this](int cursor){ const int idx = cursorCb->findData(cursor); cursorCb->setCurrentIndex((idx == -1) ? Qt::BlankCursor : idx); resetPresetCombo(); }); emit settings->cursorChanged(settings->cursor()); // set initial value connect(cursorCb, static_cast(&QComboBox::currentIndexChanged), this, [settings, cursorCb](int index) { settings->setCursor(static_cast(cursorCb->itemData(index).toInt())); }); grid->addWidget(new QLabel(tr("Cursor"), this), 0, 0); grid->addWidget(cursorCb, 0, 1); grid->setColumnStretch(1, 1); return cursorGroup; } // ------------------------------------------------------------------------------------------------- QWidget* PreferencesDialog::createMultiScreenWidget(Settings* settings) { const auto cb = new QCheckBox(tr("Enable multi-screen overlay"), this); cb->setChecked(settings->multiScreenOverlayEnabled()); connect(cb, &QCheckBox::toggled, settings, &Settings::setMultiScreenOverlayEnabled); connect(settings, &Settings::multiScreenOverlayEnabledChanged, cb, &QCheckBox::setChecked); connect(settings, &Settings::multiScreenOverlayEnabledChanged, this, &PreferencesDialog::resetPresetCombo); return cb; } // ------------------------------------------------------------------------------------------------- QWidget* PreferencesDialog::createLogTabWidget() { const auto widget = new QWidget(this); const auto mainVBox = new QVBoxLayout(widget); const auto te = new QPlainTextEdit(widget); te->setReadOnly(true); te->setWordWrapMode(QTextOption::NoWrap); te->setMaximumBlockCount(1000); te->setFont([te]() { auto font = te->font(); font.setPointSize(font.pointSize() - 1); return font; }()); logging::registerTextEdit(te); // Count discarded logs connect(te, &QPlainTextEdit::blockCountChanged, this, [maxBlockCount=te->maximumBlockCount(), this](int newBlockCount) { if (newBlockCount > maxBlockCount) { m_discardedLogCount += (newBlockCount-maxBlockCount); } }); const auto lvlHBox = new QHBoxLayout(); lvlHBox->addWidget(new QLabel(tr("Log Level"), widget)); // Log level combo box const auto logLvlCombo = new QComboBox(widget); logLvlCombo->addItem(tr("Debug"), static_cast(logging::level::debug)); logLvlCombo->addItem(tr("Info"), static_cast(logging::level::info)); logLvlCombo->addItem(tr("Warning"), static_cast(logging::level::warning)); logLvlCombo->addItem(tr("Error"), static_cast(logging::level::error)); lvlHBox->addWidget(logLvlCombo); const int idx = logLvlCombo->findData(static_cast(logging::currentLevel())); logLvlCombo->setCurrentIndex((idx == -1) ? 0 : idx); connect(logLvlCombo, static_cast(&QComboBox::currentIndexChanged), this, [logLvlCombo, te](int index) { const auto lvl = static_cast(logLvlCombo->itemData(index).toInt()); te->appendPlainText(tr("--- Setting new log level: %1").arg(logging::levelToString(lvl))); logging::setCurrentLevel(lvl); }); const auto saveLogBtn = new QPushButton(tr("&Save log..."), this); saveLogBtn->setToolTip(tr("Save log to file.")); connect(saveLogBtn, &QPushButton::clicked, this, [this, te]() { static auto saveDir = QDir::homePath(); const auto defaultName = QString("projecteur_%1.log") .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd_hh-mm")); const auto defaultFile = QDir(saveDir).filePath(defaultName); QString logFilter(tr("Log files (*.log *.txt)")); const auto logFile = QFileDialog::getSaveFileName(this, tr("Save log file"), defaultFile, logFilter, &logFilter); if (logFile.isEmpty()) { return; } saveDir = QFileInfo(logFile).path(); QFile f(logFile); if (f.open(QIODevice::WriteOnly)) { f.write(QString("%1 %2\n").arg(QCoreApplication::applicationName()) .arg(projecteur::version_string()).toLocal8Bit()); f.write(QString(" - git-branch: %1, git-hash: %2\n").arg(projecteur::version_branch()) .arg(projecteur::version_shorthash()).toLocal8Bit()); f.write(QString(" - qt-version: (build: %1, runtime: %2)\n").arg(QT_VERSION_STR) .arg(qVersion()).toLocal8Bit()); f.write(QString("\n------------------------------------------------------------\n").toLocal8Bit()); if (m_discardedLogCount > 0) { f.write(tr("Discarded %1 previous log entries.").arg(m_discardedLogCount).toLocal8Bit()); f.write(QString("\n------------------------------------------------------------\n").toLocal8Bit()); } f.write(te->toPlainText().toLocal8Bit()); logInfo(preferences) << tr("Log saved to: ") << logFile; } else { logError(preferences) << tr("Could not open '%1' for writing.").arg(logFile); } }); lvlHBox->addWidget(saveLogBtn); lvlHBox->setStretch(0, 0); lvlHBox->setStretch(1, 1); lvlHBox->setStretch(2, 1); mainVBox->addLayout(lvlHBox); mainVBox->addWidget(te); return widget; } // ------------------------------------------------------------------------------------------------- void PreferencesDialog::setMode(Mode dialogMode) { if (m_dialogMode == dialogMode) { return; } setDialogMode(dialogMode); } // ------------------------------------------------------------------------------------------------- void PreferencesDialog::setDialogMode(Mode dialogMode) { m_dialogMode = dialogMode; if (dialogMode == Mode::ClosableDialog) { setWindowFlags(Qt::Dialog); m_closeMinimizeBtn->setText(tr("&Close")); m_closeMinimizeBtn->setToolTip(tr("Close the preferences dialog.")); } else if (dialogMode == Mode::MinimizeOnlyDialog) { setWindowFlags(Qt::Window); setWindowFlags(windowFlags() & ~Qt::WindowMaximizeButtonHint); setWindowFlags(windowFlags() & ~Qt::WindowCloseButtonHint); m_closeMinimizeBtn->setText(tr("&Minimize")); m_closeMinimizeBtn->setToolTip(tr("Minimize the preferences dialog.")); } } // ------------------------------------------------------------------------------------------------- void PreferencesDialog::resetPresetCombo() { if (m_presetCombo) { m_presetCombo->setCurrentIndex(0); } } // ------------------------------------------------------------------------------------------------- void PreferencesDialog::setDialogActive(bool active) { if (active == m_active) { return; } m_active = active; emit dialogActiveChanged(active); } // ------------------------------------------------------------------------------------------------- bool PreferencesDialog::event(QEvent* e) { if (e->type() == QEvent::WindowActivate) { setDialogActive(true); } else if (e->type() == QEvent::WindowDeactivate) { setDialogActive(false); } return QDialog::event(e); } // ------------------------------------------------------------------------------------------------- void PreferencesDialog::closeEvent(QCloseEvent* /* ev */) { if (m_dialogMode == Mode::MinimizeOnlyDialog) { emit exitApplicationRequested(); } } // ------------------------------------------------------------------------------------------------- void PreferencesDialog::keyPressEvent(QKeyEvent* e) { if (m_dialogMode == Mode::MinimizeOnlyDialog) { if (e->key() == Qt::Key_Escape) { this->showMinimized(); return; } } QDialog::keyPressEvent(e); } // ------------------------------------------------------------------------------------------------- void PresetComboCustomStyle::drawControl(QStyle::ControlElement element, const QStyleOption* option, QPainter* painter, const QWidget* widget) const { if (element == QStyle::CE_ComboBoxLabel) { auto fnt = painter->font(); fnt.setItalic(true); painter->setFont(fnt); if (option->type == QStyleOption::SO_ComboBox) { auto custom = *static_cast(option); custom.palette.setColor(QPalette::ButtonText, option->palette.color(QPalette::Disabled, QPalette::ButtonText)); QProxyStyle::drawControl(element, &custom, painter, widget); return; } } QProxyStyle::drawControl(element, option, painter, widget); } Projecteur-0.10/src/preferencesdlg.h000066400000000000000000000047411451344070600175160ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #pragma once #include #include #include #include class QComboBox; class QGroupBox; class Settings; class Spotlight; class DevicesWidget; // ------------------------------------------------------------------------------------------------- class PresetComboCustomStyle : public QProxyStyle { public: void drawControl(QStyle::ControlElement element, const QStyleOption* option, QPainter* painter, const QWidget* widget = nullptr) const override; }; // ------------------------------------------------------------------------------------------------- class PreferencesDialog : public QDialog { Q_OBJECT public: enum class Mode : uint8_t{ ClosableDialog, MinimizeOnlyDialog }; explicit PreferencesDialog(Settings* settings, Spotlight* spotlight, Mode = Mode::ClosableDialog, QWidget* parent = nullptr); virtual ~PreferencesDialog() override = default; bool dialogActive() const { return m_active; } Mode mode() const { return m_dialogMode; } void setMode(Mode dialogMode); signals: void dialogActiveChanged(bool active); void testButtonClicked(); void exitApplicationRequested(); protected: virtual bool event(QEvent* event) override; virtual void closeEvent(QCloseEvent* e) override; virtual void keyPressEvent(QKeyEvent* e) override; private: void setDialogActive(bool active); void setDialogMode(Mode dialogMode); void resetPresetCombo(); QWidget* createSettingsTabWidget(Settings* settings); QGroupBox* createShapeGroupBox(Settings* settings); QGroupBox* createSpotGroupBox(Settings* settings); QGroupBox* createDotGroupBox(Settings* settings); QGroupBox* createBorderGroupBox(Settings* settings); QGroupBox* createCursorGroupBox(Settings* settings); QWidget* createMultiScreenWidget(Settings* settings); QGroupBox* createZoomGroupBox(Settings* settings); QWidget* createPresetSelector(Settings* settings); #if HAS_Qt_X11Extras QWidget* createCompositorWarningWidget(); #endif QWidget* createLogTabWidget(); private: std::unique_ptr m_presetComboStyle; 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; }; Projecteur-0.10/src/projecteur-icons-def.h000066400000000000000000000030321451344070600205450ustar00rootroot00000000000000// 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` 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 battery_6 = 0xf103, // svg/iconmonstr-battery-6.svg 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 media_control_48 = 0xf117, // svg/iconmonstr-media-control-48.svg media_control_50 = 0xf118, // svg/iconmonstr-media-control-50.svg plus_5 = 0xf107, // svg/iconmonstr-plus-5.svg power_on_off_11 = 0xf115, // svg/iconmonstr-power-on-off-11.svg share_8 = 0xf108, // svg/iconmonstr-share-8.svg target_8 = 0xf110, // svg/iconmonstr-target-8.svg time_19 = 0xf109, // svg/iconmonstr-time-19.svg trash_can_1 = 0xf10a, // svg/iconmonstr-trash-can-1.svg }; } Projecteur-0.10/src/projecteurapp.cc000066400000000000000000000650571451344070600175560ustar00rootroot00000000000000// 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" #include "preferencesdlg.h" #include "settings.h" #include "spotlight.h" #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include LOGGING_CATEGORY(mainapp, "mainapp") LOGGING_CATEGORY(cmdclient, "cmdclient") LOGGING_CATEGORY(cmdserver, "cmdserver") namespace { QString localServerName() { return QCoreApplication::applicationName() + "_local_socket"; } } // end anonymous namespace // ------------------------------------------------------------------------------------------------- ProjecteurApplication::ProjecteurApplication(int &argc, char **argv, const Options& options) : QApplication(argc, argv) , m_trayIcon(new QSystemTrayIcon()) , m_trayMenu(new QMenu()) , m_localServer(new QLocalServer(this)) , m_linuxDesktop(new LinuxDesktop(this)) , m_xcbOnWayland(QGuiApplication::platformName() == "xcb" && m_linuxDesktop->isWayland()) { if (screens().empty()) { const auto title = tr("No Screens detected"); const auto text = tr("screens().size() returned a size < 1. Exiting."); logError(mainapp) << title << ";" << text; QMessageBox::critical(nullptr, title, text); QTimer::singleShot(0, this, [this](){ this->exit(2); }); return; } // don't quit application when last windows (usually preferences dialog) is closed setQuitOnLastWindowClosed(false); QFontDatabase::addApplicationFont(":/icons/projecteur-icons.ttf"); m_settings = options.configFile.isEmpty() ? new Settings(this) : new Settings(options.configFile, this); 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 = 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"); logDebug(mainapp) << tr("Qt platform plugin: %1;").arg(QGuiApplication::platformName()) << tr("Desktop Environment: %1;").arg(desktopEnv) << tr("Wayland: %1").arg(m_linuxDesktop->isWayland() ? "true" : "false"); if (m_xcbOnWayland) { logWarning(mainapp) << tr("Qt 'xcb' platform and Wayland session detected."); } if (options.showPreferencesOnStart || m_linuxDesktop->isWayland()) { QTimer::singleShot(0, this, [this](){ showPreferences(true); }); } else if (options.dialogMinimizeOnly) { QTimer::singleShot(0, this, [this](){ m_dialog->show(); m_dialog->showMinimized(); }); } // Create qml engine and register context properties m_qmlEngine = new QQmlApplicationEngine(this); m_qmlEngine->rootContext()->setContextProperty("Settings", m_settings); m_qmlEngine->rootContext()->setContextProperty("PreferencesDialog", &*m_dialog); m_qmlEngine->rootContext()->setContextProperty("ProjecteurApp", this); // Create qml overlay window component m_windowQmlComponent = new QQmlComponent(m_qmlEngine, QUrl(QStringLiteral("qrc:/main.qml")), m_qmlEngine); 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; } // Setup screen overlay windows setupScreenOverlays(); // React to multi-screen and overlay disabled changes in settings. 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); } } else { QTimer::singleShot(0, this, [this](){ if (m_spotlight->spotActive()) { emit m_spotlight->spotActiveChanged(true); } else { m_spotlight->setSpotActive(true); } }); } }); // Re-setup screen overlay(s) when a screen is added or removed connect(this, &ProjecteurApplication::screenAdded, this, [this](){ setupScreenOverlays(); }); connect(this, &ProjecteurApplication::screenRemoved, this, [this](){ setupScreenOverlays(); }); // Setup the tray icon and menu setupTrayIcon(); connect(this, &ProjecteurApplication::aboutToQuit, this, [this](){ for (const auto window : m_overlayWindows) { window->close(); } m_overlayWindows.clear(); }); // Setup the spotlight connections. setupSpotlight(); // 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]() { 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(); }); // 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(); } }); 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(); } for (const auto window : m_overlayWindows) { window->setFlags(window->flags() | Qt::WindowStaysOnTopHint); window->setFlags(window->flags() & ~Qt::SplashScreen); window->setFlags(window->flags() | Qt::ToolTip); window->setFlags(window->flags() & ~Qt::WindowTransparentForInput); if (window->screen()) { if (m_settings->zoomEnabled()) { window->setProperty("desktopPixmap", m_linuxDesktop->grabScreen(window->screen())); } const auto screenGeometry = window->screen()->geometry(); if (window->geometry() != screenGeometry) { window->setGeometry(screenGeometry); } window->setPosition(screenGeometry.topLeft()); } window->showFullScreen(); window->raise(); } m_overlayVisible = true; emit overlayVisibleChanged(true); } else { m_overlayVisible = false; emit overlayVisibleChanged(false); for (const auto window : m_overlayWindows) { window->setFlags(window->flags() | Qt::WindowTransparentForInput); window->setFlags(window->flags() & ~Qt::WindowStaysOnTopHint); // 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 && m_dialog->mode() == PreferencesDialog::Mode::MinimizeOnlyDialog && m_dialog->isMinimized()) { // keep Window minimized... //Workaround for QTBUG-76354 (https://bugreports.qt.io/browse/QTBUG-76354) m_dialog->showNormal(); m_dialog->setWindowState(Qt::WindowMinimized); } } }); connect(m_spotlight, &Spotlight::spotActiveChanged, this, [this](bool active){ if (!active && m_dialog->isVisible()) { m_dialog->raise(); m_dialog->activateWindow(); } }); } // ------------------------------------------------------------------------------------------------- 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]() { 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 }); } if (m_aboutDialog->isVisible()) { m_aboutDialog->show(); m_aboutDialog->raise(); m_aboutDialog->activateWindow(); } else { m_aboutDialog->open(); } }); 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); } } }); connect(&*m_dialog, &PreferencesDialog::exitApplicationRequested, actionQuit, [actionQuit]() { logDebug(mainapp) << tr("Exit request from preferences dialog."); actionQuit->trigger(); }); } // ------------------------------------------------------------------------------------------------- QWindow* ProjecteurApplication::createOverlayWindow() { QObject *object = m_windowQmlComponent->create(); object->setParent(m_qmlEngine); const auto window = qobject_cast(object); window->setFlags(window->flags() | Qt::WindowTransparentForInput | Qt::Tool); return window; } // ------------------------------------------------------------------------------------------------- void ProjecteurApplication::spotlightWindowClicked() { m_spotlight->setSpotActive(false); } // ------------------------------------------------------------------------------------------------- void ProjecteurApplication::cursorExitedWindow() { if (m_spotlight->spotActive() && !m_settings->multiScreenOverlayEnabled()) { setScreenForCursorPos(); } } // ------------------------------------------------------------------------------------------------- void ProjecteurApplication::cursorEntered(quint64 screen) { setCurrentSpotScreen(screen); } // ------------------------------------------------------------------------------------------------- void ProjecteurApplication::cursorPositionChanged(const QPoint& pos) { setCurrentCursorPos(pos); } // ------------------------------------------------------------------------------------------------- void ProjecteurApplication::updateOverlayWindow(QWindow* window, QScreen* screen) { if (screen == nullptr) { return; } if (window->screen() == screen && screen->geometry() == window->geometry()) { return; } window->setProperty("screenId", quint64(screen)); const bool wasVisible = window->isVisible(); const bool wasSpotActive = m_spotlight->spotActive(); m_overlayVisible = false; emit overlayVisibleChanged(false); window->setFlags(window->flags() | Qt::WindowTransparentForInput); window->setFlags(window->flags() & ~Qt::WindowStaysOnTopHint); window->hide(); window->setGeometry(QRect(screen->geometry().topLeft(), QSize(300,200))); window->setScreen(screen); window->setGeometry(screen->geometry()); if (m_xcbOnWayland && !wasVisible) { if (m_dialog->mode() == PreferencesDialog::Mode::MinimizeOnlyDialog && m_dialog->isMinimized()) { // keep Window minimized... //Workaround for QTBUG-76354 (https://bugreports.qt.io/browse/QTBUG-76354) m_dialog->showNormal(); m_dialog->setWindowState(Qt::WindowMinimized); } } if (wasVisible && wasSpotActive) { QTimer::singleShot(0, this, [this](){ if (m_spotlight->spotActive()) { emit m_spotlight->spotActiveChanged(true); } else { m_spotlight->setSpotActive(true); } }); } } // ------------------------------------------------------------------------------------------------- void ProjecteurApplication::setScreenForCursorPos() { updateOverlayWindow(m_overlayWindows.first(), screenAtCursorPos()); } // ------------------------------------------------------------------------------------------------- QScreen* ProjecteurApplication::screenAtCursorPos() const { #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) return this->screenAt(QCursor::pos()); #else const int screenNumber = this->desktop()->screenNumber(QCursor::pos()); const auto screenList = screens(); if (screenNumber >= 0 && screenNumber < screenList.size()) { return screenList[screenNumber]; } return nullptr; #endif } // ------------------------------------------------------------------------------------------------- void ProjecteurApplication::setupScreenOverlays() { m_screenWindowMap.clear(); const auto currentScreens = screens(); if (currentScreens.empty()) { for (const auto window : m_overlayWindows) { window->deleteLater(); } m_overlayWindows.clear(); return; } // disconnect any connected screen signals previously connected to `this` // and connect to geometryChanged signal to update overlay windows on screen geometry changes for (const auto screen : currentScreens) { disconnect(screen, nullptr, this, nullptr); connect(screen, &QScreen::geometryChanged, this, [this, screen]() { if (m_settings->multiScreenOverlayEnabled()) { const auto it = m_screenWindowMap.find(screen); if (it == m_screenWindowMap.cend()) { return; } updateOverlayWindow(it->second, it->first); } else { setScreenForCursorPos(); } }); } // Adapt number of overlay windows depending on multiScreenOverlayEnabled() and // the number of screens const int numOverlayWindows = m_settings->multiScreenOverlayEnabled() ? currentScreens.size() : 1; const bool wasSpotActive = m_spotlight->spotActive(); while (m_overlayWindows.size() > numOverlayWindows) { m_overlayWindows.back()->deleteLater(); m_overlayWindows.pop_back(); } while (m_overlayWindows.size() < numOverlayWindows) { m_overlayWindows.push_back(createOverlayWindow()); } // Default behavior - only one overlay window that is moved across sreens if (!m_settings->multiScreenOverlayEnabled()) { for (const auto screen : currentScreens) { m_screenWindowMap[screen] = m_overlayWindows.front(); } } else { // multi-screen overlays enabled: assign overlay windows to screens auto wit = m_overlayWindows.cbegin(); for (const auto screen : currentScreens) { m_screenWindowMap[screen] = (*wit); updateOverlayWindow(*wit, screen); ++wit; } } // If the spotlight was active was active when calling the setup function, // make sure it will be activated again. if (wasSpotActive) { QTimer::singleShot(0, this, [this](){ if (m_spotlight->spotActive()) { emit m_spotlight->spotActiveChanged(true); } else { m_spotlight->setSpotActive(true); } }); } } // ------------------------------------------------------------------------------------------------- quint64 ProjecteurApplication::currentSpotScreen() const { return m_currentSpotScreen; } // ------------------------------------------------------------------------------------------------- void ProjecteurApplication::setCurrentSpotScreen(quint64 screen) { if (m_currentSpotScreen == screen) { return; } m_currentSpotScreen = screen; emit currentSpotScreenChanged(m_currentSpotScreen); } // ------------------------------------------------------------------------------------------------- QPoint ProjecteurApplication::currentCursorPos() const { return m_currentCursorPos; } // ------------------------------------------------------------------------------------------------- void ProjecteurApplication::setCurrentCursorPos(const QPoint& pos) { if (pos == m_currentCursorPos) { return; } m_currentCursorPos = pos; emit currentCursorPosChanged(m_currentCursorPos); } // ------------------------------------------------------------------------------------------------- void ProjecteurApplication::readCommand(QLocalSocket* clientConnection) { auto it = m_commandConnections.find(clientConnection); if (it == m_commandConnections.end()) { return; } quint32& commandSize = it->second; // Read size of command (always quint32) if not already done. if (commandSize == 0) { if (clientConnection->bytesAvailable() < static_cast(sizeof(quint32))) { return; } QDataStream in(clientConnection); in >> commandSize; if (commandSize > 256) { logWarning(cmdserver) << tr("Received invalid command size (%1)").arg(commandSize); clientConnection->disconnectFromServer(); return ; } } if (clientConnection->bytesAvailable() < commandSize || clientConnection->atEnd()) { return; } const auto command = QString::fromLocal8Bit(clientConnection->read(commandSize)); const QString cmdKey = command.section('=', 0, 0).trimmed(); const QString cmdValue = command.section('=', 1).trimmed(); if (cmdKey == "quit") { 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.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"); logDebug(cmdserver) << tr("Received command spot = %1").arg(active); m_spotlight->setSpotActive(active); } } else if (cmdKey == "settings" || cmdKey == "preferences") { const bool show = !(cmdValue.toLower() == "hide" || cmdValue == "0"); logDebug(cmdserver) << tr("Received command settings = %1").arg(show); showPreferences(show); } else if (cmdKey == "preset") { logDebug(cmdserver) << tr("Received command preset = %1").arg(cmdValue); if (!cmdValue.isEmpty()) { m_settings->loadPreset(cmdValue); } } else if (cmdValue.size()) { const auto& properties = m_settings->stringProperties(); const auto it = std::find_if(properties.cbegin(), properties.cend(), [&cmdKey](const auto& pair){ return (pair.first == cmdKey); }); if (it != m_settings->stringProperties().cend()) { logDebug(cmdserver) << tr("Received command '%1'='%2'").arg(cmdKey, cmdValue); it->second.setFunction(cmdValue); } else { // string property not found... logWarning(cmdserver) << tr("Received unknown command key (%1)").arg(cmdKey); } } // reset command size, for next command commandSize = 0; } // ------------------------------------------------------------------------------------------------- void ProjecteurApplication::showPreferences(bool show) { if (show) { m_dialog->show(); m_dialog->raise(); static const bool qtPlatformIsWayland = QGuiApplication::platformName().toLower().startsWith("wayland"); if (!qtPlatformIsWayland) { m_dialog->activateWindow(); } } else { if (m_dialog->mode() == PreferencesDialog::Mode::MinimizeOnlyDialog) { m_dialog->showMinimized(); } else { m_dialog->hide(); } } } // ================================================================================================= ProjecteurCommandClientApp::ProjecteurCommandClientApp(const QStringList& ipcCommands, int &argc, char **argv) : QCoreApplication(argc, argv) { if (ipcCommands.isEmpty()) { QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); return; } 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, this, std::move(socketErrorFunc)); #else connect(localSocket, static_cast(&QLocalSocket::error), this, std::move(socketErrorFunc)); #endif connect(localSocket, &QLocalSocket::connected, [localSocket, &ipcCommands]() { for (const auto& ipcCommand : ipcCommands) { if (ipcCommand.isEmpty()) { continue; } const QByteArray commandBlock = [&ipcCommand]() { const QByteArray ipcBytes = ipcCommand.toLocal8Bit(); QByteArray block; { QDataStream out(&block, QIODevice::WriteOnly); out << static_cast(ipcBytes.size()); } block.append(ipcBytes); return block; }(); localSocket->write(commandBlock); localSocket->flush(); } localSocket->disconnectFromServer(); }); connect(localSocket, &QLocalSocket::disconnected, this, [this, localSocket]() { localSocket->close(); QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); }); localSocket->connectToServer(localServerName()); } Projecteur-0.10/src/projecteurapp.h000066400000000000000000000057751451344070600174210ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #pragma once #include "devicescan.h" #include #include #include #include class AboutDialog; class DeviceCommandHelper; class LinuxDesktop; class PreferencesDialog; class QLocalServer; class QLocalSocket; class QMenu; class QQmlApplicationEngine; class QQmlComponent; class QSystemTrayIcon; class Settings; class Spotlight; class ProjecteurApplication : public QApplication { Q_OBJECT Q_PROPERTY(bool overlayVisible READ overlayVisible NOTIFY overlayVisibleChanged) Q_PROPERTY(quint64 currentSpotScreen READ currentSpotScreen NOTIFY currentSpotScreenChanged) Q_PROPERTY(QPoint currentCursorPos READ currentCursorPos NOTIFY currentCursorPosChanged) public: struct Options { QString configFile; bool enableUInput = true; // enable virtual uinput device bool showPreferencesOnStart = false; bool dialogMinimizeOnly = false; bool disableOverlay = false; std::vector additionalDevices; }; explicit ProjecteurApplication(int &argc, char **argv, const Options& options); virtual ~ProjecteurApplication() override; bool overlayVisible() const { return m_overlayVisible; } signals: void overlayVisibleChanged(bool visible); void currentSpotScreenChanged(quint64 screen); void currentCursorPosChanged(const QPoint& pos); public slots: void cursorExitedWindow(); void cursorEntered(quint64 screen); void spotlightWindowClicked(); void cursorPositionChanged(const QPoint& pos); private slots: void readCommand(QLocalSocket* client); private: void showPreferences(bool show = true); void setScreenForCursorPos(); QScreen* screenAtCursorPos() const; QWindow* createOverlayWindow(); void updateOverlayWindow(QWindow* window, QScreen* screen); void setupScreenOverlays(); quint64 currentSpotScreen() const; void setCurrentSpotScreen(quint64 screen); 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; QPointer m_aboutDialog; QLocalServer* const m_localServer = 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; std::map m_commandConnections; bool m_overlayVisible = false; const bool m_xcbOnWayland = false; QList m_overlayWindows; std::map m_screenWindowMap; quint64 m_currentSpotScreen = 0; QPoint m_currentCursorPos; }; class ProjecteurCommandClientApp : public QCoreApplication { Q_OBJECT public: explicit ProjecteurCommandClientApp(const QStringList& ipcCommands, int &argc, char **argv); }; Projecteur-0.10/src/runguard.cc000066400000000000000000000025311451344070600165060ustar00rootroot00000000000000#include "runguard.h" #include namespace { QString generateKeyHash(const QString& key, const QString& salt) { const QByteArray data(key.toUtf8().append(salt.toUtf8())); return QCryptographicHash::hash(data, QCryptographicHash::Sha1).toHex(); } } RunGuard::RunGuard(const QString& key) : m_key(key) , m_memLockKey(generateKeyHash(key, "_memLockKey")) , m_sharedmemKey(generateKeyHash(key, "_sharedmemKey")) , m_sharedMem(m_sharedmemKey) , m_memLock(m_memLockKey, 1) { m_memLock.acquire(); { QSharedMemory fix(m_sharedmemKey); // Fix for *nix: http://habrahabr.ru/post/173281/ fix.attach(); } m_memLock.release(); } RunGuard::~RunGuard() { release(); } bool RunGuard::isAnotherRunning() { if (m_sharedMem.isAttached()) return false; m_memLock.acquire(); const bool isRunning = m_sharedMem.attach(); if (isRunning) m_sharedMem.detach(); m_memLock.release(); return isRunning; } bool RunGuard::tryToRun() { if (isAnotherRunning()) // Extra check return false; m_memLock.acquire(); const bool result = m_sharedMem.create(sizeof(quint64)); m_memLock.release(); if (!result) { release(); return false; } return true; } void RunGuard::release() { m_memLock.acquire(); if (m_sharedMem.isAttached()) m_sharedMem.detach(); m_memLock.release(); } Projecteur-0.10/src/runguard.h000066400000000000000000000006201451344070600163450ustar00rootroot00000000000000#pragma once #include #include class RunGuard { public: explicit RunGuard(const QString& key); ~RunGuard(); bool isAnotherRunning(); bool tryToRun(); void release(); private: const QString m_key; const QString m_memLockKey; const QString m_sharedmemKey; QSharedMemory m_sharedMem; QSystemSemaphore m_memLock; Q_DISABLE_COPY(RunGuard) }; Projecteur-0.10/src/settings.cc000066400000000000000000001235041451344070600165230ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #include "settings.h" #include "device.h" #include "deviceinput.h" #include "logging.h" #include #include #include #include #include #include #include #include LOGGING_CATEGORY(lcSettings, "settings") namespace { // ----------------------------------------------------------------------------------------------- namespace settings { constexpr char showSpotShade[] = "showSpotShade"; constexpr char spotSize[] = "spotSize"; constexpr char showCenterDot[] = "showCenterDot"; constexpr char dotSize[] = "dotSize"; constexpr char dotColor[] = "dotColor"; constexpr char dotOpacity[] = "dotOpacity"; constexpr char shadeColor[] = "shadeColor"; constexpr char shadeOpacity[] = "shadeOpacity"; constexpr char cursor[] = "cursor"; constexpr char spotShape[] = "spotShape"; constexpr char spotRotation[] ="spotRotation"; constexpr char showBorder[] = "showBorder"; constexpr char borderColor[] ="borderColor"; constexpr char borderSize[] = "borderSize"; constexpr char borderOpacity[] = "borderOpacity"; constexpr char zoomEnabled[] = "enableZoom"; constexpr char zoomFactor[] = "zoomFactor"; constexpr char multiScreenOverlay[] = "multiScreenOverlay"; // -- device specific constexpr char inputSequenceInterval[] = "inputSequenceInterval"; constexpr char inputMapConfig[] = "inputMapConfig"; constexpr char timerEnabled[] = "timer%1enabled"; constexpr char timerSeconds[] = "timer%1seconds"; constexpr char vibrationLength[] = "vibrationLength"; constexpr char vibrationIntensity[] = "vibrationIntensity"; namespace defaultValue { constexpr bool showSpotShade = true; constexpr int spotSize = 32; constexpr bool showCenterDot = false; constexpr int dotSize = 5; constexpr auto dotColor = Qt::red; constexpr double dotOpacity = 0.8; constexpr char shadeColor[] = "#222222"; constexpr double shadeOpacity = 0.3; constexpr Qt::CursorShape cursor = Qt::BlankCursor; constexpr char spotShape[] = "spotshapes/Circle.qml"; constexpr double spotRotation = 0.0; constexpr bool showBorder = true; constexpr auto borderColor = "#73d216"; // some kind of neon-like-green constexpr int borderSize = 4; constexpr double borderOpacity = 0.8; constexpr bool zoomEnabled = false; constexpr double zoomFactor = 2.0; constexpr bool multiScreenOverlay = false; // -- device specific defaults 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 }; constexpr Settings::SettingRange dotSize{ 3, 100 }; constexpr Settings::SettingRange dotOpacity{ 0.0, 1.0 }; constexpr Settings::SettingRange shadeOpacity{ 0.0, 1.0 }; constexpr Settings::SettingRange spotRotation{ 0.0, 360.0 }; constexpr Settings::SettingRange borderSize{ 0, 100 }; constexpr Settings::SettingRange borderOpacity{ 0.0, 1.0 }; 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) { return (value.toLower() == "true" || value.toLower() == "on" || value.toInt() > 0); } // ----------------------------------------------------------------------------------------------- #define SETTINGS_PRESET_PREFIX "Preset_" QString presetSection(const QString& preset, bool withSeparator = true) { return QString(SETTINGS_PRESET_PREFIX "%1%2").arg(preset).arg(withSeparator ? "/" : ""); } // ----------------------------------------------------------------------------------------------- QString settingsKey(const DeviceId& dId, const QString& key) { return QString("Device_%1_%2/%3") .arg(logging::hexId(dId.vendorId), logging::hexId(dId.productId), key); } // ------------------------------------------------------------------------------------------------- auto loadPresets(QSettings* settings) { std::vector presets; for (const auto& group: settings->childGroups()) { if (group.startsWith(SETTINGS_PRESET_PREFIX)) { presets.emplace_back(group.mid(sizeof(SETTINGS_PRESET_PREFIX)-1)); } } std::sort(presets.begin(), presets.end()); return presets; } } // end anonymous namespace // ------------------------------------------------------------------------------------------------- Settings::Settings(QObject* parent) : QObject(parent) , m_settings(new QSettings(QCoreApplication::applicationName(), QCoreApplication::applicationName(), this)) , m_presetModel(new PresetModel(loadPresets(m_settings), this)) , m_shapeSettingsRoot(new QQmlPropertyMap(this)) { init(); } // ------------------------------------------------------------------------------------------------- Settings::Settings(const QString& configFile, QObject* parent) : QObject(parent) , m_settings(new QSettings(configFile, QSettings::NativeFormat, this)) , m_presetModel(new PresetModel(loadPresets(m_settings), this)) , m_shapeSettingsRoot(new QQmlPropertyMap(this)) { init(); } // ------------------------------------------------------------------------------------------------- Settings::~Settings() = default; // ------------------------------------------------------------------------------------------------- void Settings::init() { const QFileInfo fi(m_settings->fileName()); if (!fi.isReadable()) { logError(lcSettings) << tr("Settings file '%1' not readable.").arg(m_settings->fileName()); } if (!fi.isWritable()) { logWarning(lcSettings) << tr("Settings file '%1' not writable.").arg(m_settings->fileName()); } shapeSettingsInitialize(); load(); initializeStringProperties(); } // ------------------------------------------------------------------------------------------------- void Settings::initializeStringProperties() { auto& map = m_stringPropertyMap; // -- spot settings map.emplace_back( "spot.overlay", StringProperty{ StringProperty::Bool, {false, true}, [this](const QString& value){ setOverlayDisabled(!toBool(value)); } } ); map.emplace_back( "spot.multi-screen", StringProperty{ StringProperty::Bool, {false, true}, [this](const QString& value){ setMultiScreenOverlayEnabled(toBool(value)); } } ); map.emplace_back( "spot.size", StringProperty{ StringProperty::Integer, {::settings::ranges::spotSize.min, ::settings::ranges::spotSize.max}, [this](const QString& value){ setSpotSize(value.toInt()); } } ); map.emplace_back( "spot.rotation", StringProperty{ StringProperty::Double, {::settings::ranges::spotRotation.min, ::settings::ranges::spotRotation.max}, [this](const QString& value){ setSpotRotation(value.toDouble()); } } ); QVariantList shapesList; for (const auto& shape : spotShapes()) { shapesList.push_back(shape.name()); } map.emplace_back( "spot.shape", StringProperty{ StringProperty::StringEnum, shapesList, [this](const QString& value){ for (const auto& shape : spotShapes()) { if (shape.name().toLower() == value.toLower()) { setSpotShape(shape.qmlComponent()); break; } } } } ); for (const auto& shape : spotShapes()) { for (const auto& shapeSetting : shape.shapeSettings()) { const auto pm = shapeSettings(shape.name()); 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()); map.emplace_back( stringProperty, StringProperty{ StringProperty::Integer, {shapeSetting.minValue().toInt(), shapeSetting.maxValue().toInt()}, [pm, shapeSetting](const QString& value) { const int newValue = qMin(qMax(shapeSetting.minValue().toInt(), value.toInt()), shapeSetting.maxValue().toInt()); pm->setProperty(shapeSetting.settingsKey().toLocal8Bit(), newValue); } } ); } } // --- shade map.emplace_back( "shade", StringProperty{ StringProperty::Bool, {false, true}, [this](const QString& value){ setShowSpotShade(toBool(value)); } } ); map.emplace_back( "shade.opacity", StringProperty{ StringProperty::Double, {::settings::ranges::shadeOpacity.min, ::settings::ranges::shadeOpacity.max}, [this](const QString& value){ setShadeOpacity(value.toDouble()); } } ); map.emplace_back( "shade.color", StringProperty{ StringProperty::Color, {}, [this](const QString& value){ setShadeColor(QColor(value)); } } ); // --- center dot map.emplace_back( "dot", StringProperty{ StringProperty::Bool, {false, true}, [this](const QString& value){ setShowCenterDot(toBool(value)); } } ); map.emplace_back( "dot.size", StringProperty{ StringProperty::Integer, {::settings::ranges::dotSize.min, ::settings::ranges::dotSize.max}, [this](const QString& value){ setDotSize(value.toInt()); } } ); map.emplace_back( "dot.color", StringProperty{ StringProperty::Color, {}, [this](const QString& value){ setDotColor(QColor(value)); } } ); map.emplace_back( "dot.opacity", StringProperty{ StringProperty::Double, {::settings::ranges::dotOpacity.min, ::settings::ranges::dotOpacity.max}, [this](const QString& value){ setDotOpacity(value.toDouble()); } } ); // --- border map.emplace_back( "border", StringProperty{ StringProperty::Bool, {false, true}, [this](const QString& value){ setShowBorder(toBool(value)); } } ); map.emplace_back( "border.size", StringProperty{ StringProperty::Integer, {::settings::ranges::borderSize.min, ::settings::ranges::borderSize.max}, [this](const QString& value){ setBorderSize(value.toInt()); } } ); map.emplace_back( "border.color", StringProperty{ StringProperty::Color, {}, [this](const QString& value){ setBorderColor(QColor(value)); } } ); map.emplace_back( "border.opacity", StringProperty{ StringProperty::Double, {::settings::ranges::borderOpacity.min, ::settings::ranges::borderOpacity.max}, [this](const QString& value){ setBorderOpacity(value.toDouble()); } } ); // --- zoom map.emplace_back( "zoom", StringProperty{ StringProperty::Bool, {false, true}, [this](const QString& value){ setZoomEnabled(toBool(value)); } } ); map.emplace_back( "zoom.factor", StringProperty{ StringProperty::Double, {::settings::ranges::zoomFactor.min, ::settings::ranges::zoomFactor.max}, [this](const QString& value){ setZoomFactor(value.toDouble()); } } ); } // ------------------------------------------------------------------------------------------------- const std::vector>& Settings::stringProperties() const { return m_stringPropertyMap; } // ------------------------------------------------------------------------------------------------- const Settings::SettingRange& Settings::spotSizeRange() { return ::settings::ranges::spotSize; } const Settings::SettingRange& Settings::dotSizeRange() { return ::settings::ranges::dotSize; } const Settings::SettingRange& Settings::dotOpacityRange() { return settings::ranges::dotOpacity; } const Settings::SettingRange& Settings::shadeOpacityRange() { return ::settings::ranges::shadeOpacity; } const Settings::SettingRange& Settings::spotRotationRange() { return ::settings::ranges::spotRotation; } const Settings::SettingRange& Settings::borderSizeRange() { return settings::ranges::borderSize; } const Settings::SettingRange& Settings::borderOpacityRange() { return settings::ranges::borderOpacity; } const Settings::SettingRange& Settings::zoomFactorRange() { return settings::ranges::zoomFactor; } const Settings::SettingRange& Settings::inputSequenceIntervalRange() { return settings::ranges::inputSequenceInterval; } // ------------------------------------------------------------------------------------------------- const QList& Settings::spotShapes() { static const QList shapes{ SpotShape(::settings::defaultValue::spotShape, "Circle", tr("Circle"), false), SpotShape("spotshapes/Square.qml", "Square", tr("(Rounded) Square"), true, {SpotShapeSetting(tr("Border-radius (%)"), "radius", 20, 0, 100, 0)} ), SpotShape("spotshapes/Star.qml", "Star", tr("Star"), true, {SpotShapeSetting(tr("Star points"), "points", 5, 3, 100, 0), SpotShapeSetting(tr("Inner radius (%)"), "innerRadius", 50, 5, 100, 0)} ), SpotShape("spotshapes/Ngon.qml", "Ngon", tr("N-gon"), true, {SpotShapeSetting(tr("Sides"), "sides", 3, 3, 100, 0)} ) }; return shapes; } // ------------------------------------------------------------------------------------------------- void Settings::setDefaults() { setShowSpotShade(settings::defaultValue::showSpotShade); setSpotSize(settings::defaultValue::spotSize); setShowCenterDot(settings::defaultValue::showCenterDot); setDotSize(settings::defaultValue::dotSize); setDotColor(QColor(settings::defaultValue::dotColor)); setDotOpacity(settings::defaultValue::dotOpacity); setShadeColor(QColor(settings::defaultValue::shadeColor)); setShadeOpacity(settings::defaultValue::shadeOpacity); setCursor(settings::defaultValue::cursor); setSpotShape(settings::defaultValue::spotShape); setSpotRotation(settings::defaultValue::spotRotation); setShowBorder(settings::defaultValue::showBorder); setBorderColor(settings::defaultValue::borderColor); setBorderSize(settings::defaultValue::borderSize); setBorderOpacity(settings::defaultValue::borderOpacity); setZoomEnabled(settings::defaultValue::zoomEnabled); setZoomFactor(settings::defaultValue::zoomFactor); setMultiScreenOverlayEnabled(settings::defaultValue::multiScreenOverlay); shapeSettingsSetDefaults(); } // ------------------------------------------------------------------------------------------------- void Settings::shapeSettingsSetDefaults() { for (const auto& shape : spotShapes()) { for (const auto& settingDefinition : shape.shapeSettings()) { if (auto propertyMap = shapeSettings(shape.name())) { const QString& key = settingDefinition.settingsKey(); if (propertyMap->property(key.toLocal8Bit()).isValid()) { propertyMap->setProperty(key.toLocal8Bit(), settingDefinition.defaultValue()); } else { propertyMap->insert(key, settingDefinition.defaultValue()); } } } } shapeSettingsPopulateRoot(); } // ------------------------------------------------------------------------------------------------- void Settings::shapeSettingsLoad(const QString& preset) { const auto section = preset.size() ? presetSection(preset) : ""; for (const auto& shape : spotShapes()) { for (const auto& settingDefinition : shape.shapeSettings()) { if (auto propertyMap = shapeSettings(shape.name())) { 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); } else { propertyMap->insert(key, loadedValue); } } } } shapeSettingsPopulateRoot(); } // ------------------------------------------------------------------------------------------------- void Settings::shapeSettingsSavePreset(const QString& preset) { const auto section = preset.size() ? presetSection(preset) : ""; for (const auto& shape : spotShapes()) { for (const auto& settingDefinition : shape.shapeSettings()) { if (auto propertyMap = shapeSettings(shape.name())) { 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())); } } } } // ------------------------------------------------------------------------------------------------- void Settings::shapeSettingsInitialize() { for (const auto& shape : spotShapes()) { if (shape.shapeSettings().size() && m_shapeSettings.count(shape.name()) == 0) { auto pm = new QQmlPropertyMap(this); connect(pm, &QQmlPropertyMap::valueChanged, this, [this, shape, pm](const QString& key, const QVariant& value) { const auto& s = shape.shapeSettings(); auto it = std::find_if(s.cbegin(), s.cend(), [&key](const SpotShapeSetting& sss) { return key == sss.settingsKey(); }); 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(); const auto max = it->maxValue().toInt(); const auto newValue = qMin(qMax(min, setValue), max); if (newValue != setValue) { pm->setProperty(key.toLocal8Bit(), newValue); } logDebug(lcSettings) << QString("spot.shape.%1.%2 = ").arg(shape.name().toLower(), it->settingsKey()) << setValue; m_settings->setValue(QString("Shape.%1/%2").arg(shape.name()).arg(key), newValue); } } }); m_shapeSettings.emplace(shape.name(), pm); } } shapeSettingsPopulateRoot(); } // ------------------------------------------------------------------------------------------------- void Settings::loadPreset(const QString& preset) { if (m_presetModel->hasPreset(preset)) { load(preset); emit presetLoaded(preset); } } // ------------------------------------------------------------------------------------------------- void Settings::removePreset(const QString& preset) { m_presetModel->removePreset(preset); m_settings->remove(presetSection(preset, false)); } // ------------------------------------------------------------------------------------------------- const std::vector& Settings::presets() const { return m_presetModel->presets(); } // ------------------------------------------------------------------------------------------------- PresetModel* Settings::presetModel() { return m_presetModel; } // ------------------------------------------------------------------------------------------------- void Settings::load(const QString& preset) { logDebug(lcSettings) << tr("Loading values from config:") << m_settings->fileName() << (preset.size() ? QString("(%1)").arg(preset) : ""); const auto s = preset.size() ? presetSection(preset) : ""; setShowSpotShade(m_settings->value(s+::settings::showSpotShade, settings::defaultValue::showSpotShade).toBool()); setSpotSize(m_settings->value(s+::settings::spotSize, settings::defaultValue::spotSize).toInt()); setShowCenterDot(m_settings->value(s+::settings::showCenterDot, settings::defaultValue::showCenterDot).toBool()); setDotSize(m_settings->value(s+::settings::dotSize, settings::defaultValue::dotSize).toInt()); setDotColor(m_settings->value(s+::settings::dotColor, QColor(settings::defaultValue::dotColor)).value()); setDotOpacity(m_settings->value(s+::settings::dotOpacity, settings::defaultValue::dotOpacity).toDouble()); setShadeColor(m_settings->value(s+::settings::shadeColor, QColor(settings::defaultValue::shadeColor)).value()); setShadeOpacity(m_settings->value(s+::settings::shadeOpacity, settings::defaultValue::shadeOpacity).toDouble()); setCursor(static_cast(m_settings->value(s+::settings::cursor, static_cast(settings::defaultValue::cursor)).toInt())); setSpotShape(m_settings->value(s+::settings::spotShape, settings::defaultValue::spotShape).toString()); setSpotRotation(m_settings->value(s+::settings::spotRotation, settings::defaultValue::spotRotation).toDouble()); setShowBorder(m_settings->value(s+::settings::showBorder, settings::defaultValue::showBorder).toBool()); setBorderColor(m_settings->value(s+::settings::borderColor, QColor(settings::defaultValue::borderColor)).value()); setBorderSize(m_settings->value(s+::settings::borderSize, settings::defaultValue::borderSize).toInt()); setBorderOpacity(m_settings->value(s+::settings::borderOpacity, settings::defaultValue::borderOpacity).toDouble()); setZoomEnabled(m_settings->value(s+::settings::zoomEnabled, settings::defaultValue::zoomEnabled).toBool()); setZoomFactor(m_settings->value(s+::settings::zoomFactor, settings::defaultValue::zoomFactor).toDouble()); setMultiScreenOverlayEnabled(m_settings->value(s+::settings::multiScreenOverlay, settings::defaultValue::multiScreenOverlay).toBool()); shapeSettingsLoad(preset); } // ------------------------------------------------------------------------------------------------- void Settings::savePreset(const QString& preset) { const auto section = presetSection(preset); m_settings->setValue(section+::settings::showSpotShade, m_showSpotShade); m_settings->setValue(section+::settings::spotSize, m_spotSize); m_settings->setValue(section+::settings::showCenterDot, m_showCenterDot); m_settings->setValue(section+::settings::dotSize, m_dotSize); m_settings->setValue(section+::settings::dotColor, m_dotColor); m_settings->setValue(section+::settings::dotOpacity, m_dotOpacity); m_settings->setValue(section+::settings::shadeColor, m_shadeColor); m_settings->setValue(section+::settings::shadeOpacity, m_shadeOpacity); m_settings->setValue(section+::settings::cursor, static_cast(m_cursor)); m_settings->setValue(section+::settings::spotShape, m_spotShape); m_settings->setValue(section+::settings::spotRotation, m_spotRotation); m_settings->setValue(section+::settings::showBorder, m_showBorder); m_settings->setValue(section+::settings::borderColor, m_borderColor); m_settings->setValue(section+::settings::borderSize, m_borderSize); m_settings->setValue(section+::settings::borderOpacity, m_borderOpacity); m_settings->setValue(section+::settings::zoomEnabled, m_zoomEnabled); m_settings->setValue(section+::settings::zoomFactor, m_zoomFactor); m_settings->setValue(section+::settings::multiScreenOverlay, m_multiScreenOverlayEnabled); shapeSettingsSavePreset(preset); m_presetModel->addPreset(preset); emit presetLoaded(preset); } // ------------------------------------------------------------------------------------------------- void Settings::setShowSpotShade(bool show) { if (show == m_showSpotShade) { return; } m_showSpotShade = show; m_settings->setValue(::settings::showSpotShade, m_showSpotShade); logDebug(lcSettings) << "shade =" << m_showSpotShade; emit showSpotShadeChanged(m_showSpotShade); } // ------------------------------------------------------------------------------------------------- void Settings::setSpotSize(int size) { 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); logDebug(lcSettings) << "spot.size =" << m_spotSize; emit spotSizeChanged(m_spotSize); } // ------------------------------------------------------------------------------------------------- void Settings::setShowCenterDot(bool show) { if (show == m_showCenterDot) { return; } m_showCenterDot = show; m_settings->setValue(::settings::showCenterDot, m_showCenterDot); logDebug(lcSettings) << "dot =" << m_showCenterDot; emit showCenterDotChanged(m_showCenterDot); } // ------------------------------------------------------------------------------------------------- void Settings::setDotSize(int size) { 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); logDebug(lcSettings) << "dot.size =" << m_dotSize; emit dotSizeChanged(m_dotSize); } // ------------------------------------------------------------------------------------------------- void Settings::setDotColor(const QColor& color) { if (color == m_dotColor) { return; } m_dotColor = color; m_settings->setValue(::settings::dotColor, m_dotColor); logDebug(lcSettings) << "dot.color =" << m_dotColor.name(); emit dotColorChanged(m_dotColor); } // ------------------------------------------------------------------------------------------------- void Settings::setDotOpacity(double opacity) { if (opacity > m_dotOpacity || opacity < m_dotOpacity) { m_dotOpacity = qMin(qMax(::settings::ranges::dotOpacity.min, opacity), ::settings::ranges::dotOpacity.max); m_settings->setValue(::settings::dotOpacity, m_dotOpacity); logDebug(lcSettings) << "dot.opacity = " << m_dotOpacity; emit dotOpacityChanged(m_dotOpacity); } } // ------------------------------------------------------------------------------------------------- void Settings::setShadeColor(const QColor& color) { if (color == m_shadeColor) { return; } m_shadeColor = color; m_settings->setValue(::settings::shadeColor, m_shadeColor); logDebug(lcSettings) << "shade.color =" << m_shadeColor.name(); emit shadeColorChanged(m_shadeColor); } // ------------------------------------------------------------------------------------------------- void Settings::setShadeOpacity(double opacity) { if (opacity > m_shadeOpacity || opacity < m_shadeOpacity) { m_shadeOpacity = qMin(qMax(::settings::ranges::shadeOpacity.min, opacity), ::settings::ranges::shadeOpacity.max); m_settings->setValue(::settings::shadeOpacity, m_shadeOpacity); logDebug(lcSettings) << "shade.opacity = " << m_shadeOpacity; emit shadeOpacityChanged(m_shadeOpacity); } } // ------------------------------------------------------------------------------------------------- void Settings::setCursor(Qt::CursorShape cursor) { if (cursor == m_cursor) { return; } m_cursor = qMin(qMax(static_cast(0), cursor), Qt::LastCursor); m_settings->setValue(::settings::cursor, static_cast(m_cursor)); logDebug(lcSettings) << "cursor = " << m_cursor; emit cursorChanged(m_cursor); } // ------------------------------------------------------------------------------------------------- void Settings::setSpotShape(const QString& spotShapeQmlComponent) { if (m_spotShape == spotShapeQmlComponent) { return; } const auto it = std::find_if(spotShapes().cbegin(), spotShapes().cend(), [&spotShapeQmlComponent](const SpotShape& s) { return s.qmlComponent() == spotShapeQmlComponent; }); if (it != spotShapes().cend()) { m_spotShape = it->qmlComponent(); m_settings->setValue(::settings::spotShape, m_spotShape); logDebug(lcSettings) << "spot.shape = " << m_spotShape; emit spotShapeChanged(m_spotShape); setSpotRotationAllowed(it->allowRotation()); } } // ------------------------------------------------------------------------------------------------- void Settings::setSpotRotation(double rotation) { if (rotation > m_spotRotation || rotation < m_spotRotation) { m_spotRotation = qMin(qMax(::settings::ranges::spotRotation.min, rotation), ::settings::ranges::spotRotation.max); m_settings->setValue(::settings::spotRotation, m_spotRotation); logDebug(lcSettings) << "spot.rotation = " << m_spotRotation; emit spotRotationChanged(m_spotRotation); } } // ------------------------------------------------------------------------------------------------- QObject* Settings::shapeSettingsRootObject() { return m_shapeSettingsRoot; } // ------------------------------------------------------------------------------------------------- QQmlPropertyMap* Settings::shapeSettings(const QString &shapeName) { const auto it = m_shapeSettings.find(shapeName); if (it != m_shapeSettings.cend()) { return it->second; } return nullptr; } // ------------------------------------------------------------------------------------------------- void Settings::shapeSettingsPopulateRoot() { for (const auto& item : m_shapeSettings) { if (m_shapeSettingsRoot->property(item.first.toLocal8Bit()).isValid()) { m_shapeSettingsRoot->setProperty(item.first.toLocal8Bit(), QVariant::fromValue(item.second)); } else { m_shapeSettingsRoot->insert(item.first, QVariant::fromValue(item.second)); } } } // ------------------------------------------------------------------------------------------------- bool Settings::spotRotationAllowed() const { return m_spotRotationAllowed; } // ------------------------------------------------------------------------------------------------- void Settings::setSpotRotationAllowed(bool allowed) { if (allowed == m_spotRotationAllowed) { return; } m_spotRotationAllowed = allowed; emit spotRotationAllowedChanged(allowed); } // ------------------------------------------------------------------------------------------------- void Settings::setShowBorder(bool show) { if (show == m_showBorder) { return; } m_showBorder = show; m_settings->setValue(::settings::showBorder, m_showBorder); logDebug(lcSettings) << "border = " << m_showBorder; emit showBorderChanged(m_showBorder); } // ------------------------------------------------------------------------------------------------- void Settings::setBorderColor(const QColor& color) { if (color == m_borderColor) { return; } m_borderColor = color; m_settings->setValue(::settings::borderColor, m_borderColor); logDebug(lcSettings) << "border.color = " << m_borderColor.name(); emit borderColorChanged(m_borderColor); } // ------------------------------------------------------------------------------------------------- void Settings::setBorderSize(int size) { 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); logDebug(lcSettings) << "border.size = " << m_borderSize; emit borderSizeChanged(m_borderSize); } // ------------------------------------------------------------------------------------------------- void Settings::setBorderOpacity(double opacity) { if (opacity > m_borderOpacity || opacity < m_borderOpacity) { m_borderOpacity = qMin(qMax(::settings::ranges::borderOpacity.min, opacity), ::settings::ranges::borderOpacity.max); m_settings->setValue(::settings::borderOpacity, m_borderOpacity); logDebug(lcSettings) << "border.opacity = " << m_borderOpacity; emit borderOpacityChanged(m_borderOpacity); } } // ------------------------------------------------------------------------------------------------- void Settings::setZoomEnabled(bool enabled) { if (enabled == m_zoomEnabled) { return; } m_zoomEnabled = enabled; m_settings->setValue(::settings::zoomEnabled, m_zoomEnabled); logDebug(lcSettings) << "zoom = " << m_zoomEnabled; emit zoomEnabledChanged(m_zoomEnabled); } // ------------------------------------------------------------------------------------------------- void Settings::setZoomFactor(double factor) { if (factor > m_zoomFactor || factor < m_zoomFactor) { m_zoomFactor = qMin(qMax(::settings::ranges::zoomFactor.min, factor), ::settings::ranges::zoomFactor.max); m_settings->setValue(::settings::zoomFactor, m_zoomFactor); logDebug(lcSettings) << "zoom.factor = " << m_zoomFactor; emit zoomFactorChanged(m_zoomFactor); } } // ------------------------------------------------------------------------------------------------- void Settings::setMultiScreenOverlayEnabled(bool enabled) { if (m_multiScreenOverlayEnabled == enabled) { return; } m_multiScreenOverlayEnabled = enabled; m_settings->setValue(::settings::multiScreenOverlay, m_multiScreenOverlayEnabled); logDebug(lcSettings) << "multi-screen-overlay = " << m_multiScreenOverlayEnabled; emit multiScreenOverlayEnabledChanged(m_multiScreenOverlayEnabled); } // ------------------------------------------------------------------------------------------------- void Settings::setOverlayDisabled(bool disabled) { if (m_overlayDisabled == disabled) { return; } m_overlayDisabled = disabled; emit overlayDisabledChanged(m_overlayDisabled); } // ------------------------------------------------------------------------------------------------- QString Settings::StringProperty::typeToString(Type type) { switch(type) { case Type::Bool: return "Bool"; case Type::Color: return "Color"; case Type::Double: return "Double"; case Type::Integer: return "Integer"; case Type::StringEnum: return "Value"; } return QString(); } // ------------------------------------------------------------------------------------------------- void Settings::setDeviceInputSeqInterval(const DeviceId& dId, int intervalMs) { const auto v = qMin(qMax(::settings::ranges::inputSequenceInterval.min, intervalMs), ::settings::ranges::inputSequenceInterval.max); m_settings->setValue(settingsKey(dId, ::settings::inputSequenceInterval), v); } // ------------------------------------------------------------------------------------------------- int Settings::deviceInputSeqInterval(const DeviceId& dId) const { const auto value = m_settings->value(settingsKey(dId, ::settings::inputSequenceInterval), ::settings::defaultValue::inputSequenceInterval).toInt(); return qMin(qMax(::settings::ranges::inputSequenceInterval.min, value), ::settings::ranges::inputSequenceInterval.max); } // ------------------------------------------------------------------------------------------------- void Settings::setDeviceInputMapConfig(const DeviceId& dId, const InputMapConfig& imc) { const int sizeBefore = m_settings->value(settingsKey(dId, ::settings::inputMapConfig) + "/size", 0).toInt(); m_settings->beginWriteArray(settingsKey(dId, ::settings::inputMapConfig), imc.size()); int index = 0; for (const auto& item : imc) { m_settings->setArrayIndex(index++); m_settings->setValue("deviceSequence", QVariant::fromValue(item.first)); m_settings->setValue("mappedAction", QVariant::fromValue(item.second)); } m_settings->endArray(); // Remove old entries... m_settings->beginGroup(settingsKey(dId, ::settings::inputMapConfig)); for (; index < sizeBefore; ++index) { m_settings->remove(QString::number(index+1)); } m_settings->endGroup(); } // ------------------------------------------------------------------------------------------------- InputMapConfig Settings::getDeviceInputMapConfig(const DeviceId& dId) { InputMapConfig cfg; const int size = m_settings->beginReadArray(settingsKey(dId, ::settings::inputMapConfig)); for (int i = 0; i < size; ++i) { m_settings->setArrayIndex(i); const auto seq = m_settings->value("deviceSequence"); if (!seq.canConvert()) { continue; } const auto conf = m_settings->value("mappedAction"); 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(); return cfg; } // ------------------------------------------------------------------------------------------------- void Settings::setTimerSettings(const DeviceId& dId, int timerId, bool enabled, int seconds) { m_settings->setValue(settingsKey(dId, QString(::settings::timerEnabled).arg(timerId)), enabled); m_settings->setValue(settingsKey(dId, QString(::settings::timerSeconds).arg(timerId)), seconds); } // ------------------------------------------------------------------------------------------------- std::pair Settings::timerSettings(const DeviceId& dId, int timerId) const { const auto enabled = m_settings->value( settingsKey(dId, QString(::settings::timerEnabled).arg(timerId)), false).toBool(); const auto seconds = m_settings->value( settingsKey(dId, QString(::settings::timerSeconds).arg(timerId)), 900 + 900 * timerId).toInt(); return std::make_pair(enabled, seconds); } // ------------------------------------------------------------------------------------------------- void Settings::setVibrationSettings(const DeviceId& dId, uint8_t len, uint8_t intensity) { m_settings->setValue(settingsKey(dId, ::settings::vibrationLength), len); m_settings->setValue(settingsKey(dId, ::settings::vibrationIntensity), intensity); } // ------------------------------------------------------------------------------------------------- std::pair Settings::vibrationSettings(const DeviceId& dId) const { const auto len = m_settings->value( settingsKey(dId, ::settings::vibrationLength), ::settings::defaultValue::vibrationLength).toUInt(); const auto intensity = m_settings->value( settingsKey(dId, ::settings::vibrationIntensity), ::settings::defaultValue::vibrationIntensity).toUInt(); return std::make_pair(len, intensity); } // ------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------- PresetModel::PresetModel(QObject* parent) : PresetModel({}, parent) {} // ------------------------------------------------------------------------------------------------- PresetModel::PresetModel(std::vector&& presets, QObject* parent) : QAbstractListModel(parent) , m_presets(std::move(presets)) { std::sort(m_presets.begin(), m_presets.end()); } // ------------------------------------------------------------------------------------------------- int PresetModel::rowCount(const QModelIndex& parent) const { return (parent == QModelIndex()) ? m_presets.size() + 1 : 0; } // ------------------------------------------------------------------------------------------------- QVariant PresetModel::data(const QModelIndex& index, int role) const { if (index.row() > static_cast(m_presets.size())) { return QVariant(); } if (role == Qt::DisplayRole) { if (index.row() == 0) { return tr("Current Settings"); } return m_presets[index.row()-1]; } if (role == Qt::FontRole && index.row() == 0) { QFont f; f.setItalic(true); return f; } if (role == Qt::ForegroundRole && index.row() == 0) { return QColor(QGuiApplication::palette().color(QPalette::Disabled, QPalette::Text)); } return QVariant(); } // ------------------------------------------------------------------------------------------------- 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 const auto insertRow = std::distance(m_presets.begin(), lb) + 1; beginInsertRows(QModelIndex(), insertRow, insertRow); m_presets.emplace(lb, preset); endInsertRows(); } // ------------------------------------------------------------------------------------------------- bool PresetModel::hasPreset(const QString& preset) const { return (std::find(m_presets.cbegin(), m_presets.cend(), preset) != m_presets.cend()); } // ------------------------------------------------------------------------------------------------- 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; } const auto startRow = std::distance(m_presets.begin(), r.first) + 1; beginRemoveRows(QModelIndex(), startRow, startRow + count - 1); m_presets.erase(r.first, r.second); endRemoveRows(); } Projecteur-0.10/src/settings.h000066400000000000000000000255531451344070600163720ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md # pragma once #include #include #include #include #include #include struct DeviceId; class InputMapConfig; class PresetModel; class QSettings; class QQmlPropertyMap; // ------------------------------------------------------------------------------------------------- class Settings : public QObject { Q_OBJECT Q_PROPERTY(bool showSpotShade READ showSpotShade WRITE setShowSpotShade NOTIFY showSpotShadeChanged) Q_PROPERTY(int spotSize READ spotSize WRITE setSpotSize NOTIFY spotSizeChanged) Q_PROPERTY(bool showCenterDot READ showCenterDot WRITE setShowCenterDot NOTIFY showCenterDotChanged) Q_PROPERTY(int dotSize READ dotSize WRITE setDotSize NOTIFY dotSizeChanged) Q_PROPERTY(QColor dotColor READ dotColor WRITE setDotColor NOTIFY dotColorChanged) Q_PROPERTY(double dotOpacity READ dotOpacity WRITE setDotOpacity NOTIFY dotOpacityChanged) Q_PROPERTY(QColor shadeColor READ shadeColor WRITE setShadeColor NOTIFY shadeColorChanged) Q_PROPERTY(double shadeOpacity READ shadeOpacity WRITE setShadeOpacity NOTIFY shadeOpacityChanged) Q_PROPERTY(Qt::CursorShape cursor READ cursor WRITE setCursor NOTIFY cursorChanged) Q_PROPERTY(QString spotShape READ spotShape WRITE setSpotShape NOTIFY spotShapeChanged) Q_PROPERTY(double spotRotation READ spotRotation WRITE setSpotRotation NOTIFY spotRotationChanged) Q_PROPERTY(QObject* shapes READ shapeSettingsRootObject CONSTANT) Q_PROPERTY(bool spotRotationAllowed READ spotRotationAllowed NOTIFY spotRotationAllowedChanged) Q_PROPERTY(bool showBorder READ showBorder WRITE setShowBorder NOTIFY showBorderChanged) Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor NOTIFY borderColorChanged) Q_PROPERTY(int borderSize READ borderSize WRITE setBorderSize NOTIFY borderSizeChanged) Q_PROPERTY(double borderOpacity READ borderOpacity WRITE setBorderOpacity NOTIFY borderOpacityChanged) Q_PROPERTY(bool zoomEnabled READ zoomEnabled WRITE setZoomEnabled NOTIFY zoomEnabledChanged) Q_PROPERTY(double zoomFactor READ zoomFactor WRITE setZoomFactor NOTIFY zoomFactorChanged) Q_PROPERTY(bool multiScreenOverlayEnabled READ multiScreenOverlayEnabled WRITE setMultiScreenOverlayEnabled NOTIFY multiScreenOverlayEnabledChanged) public: explicit Settings(QObject* parent = nullptr); explicit Settings(const QString& configFile, QObject* parent = nullptr); ~Settings() override; void setDefaults(); bool showSpotShade() const { return m_showSpotShade; } void setShowSpotShade(bool show); int spotSize() const { return m_spotSize; } void setSpotSize(int size); bool showCenterDot() const { return m_showCenterDot; } void setShowCenterDot(bool show); int dotSize() const { return m_dotSize; } void setDotSize(int size); QColor dotColor() const { return m_dotColor; } void setDotColor(const QColor& color); double dotOpacity() const { return m_dotOpacity; } void setDotOpacity(double opacity); QColor shadeColor() const { return m_shadeColor; } void setShadeColor(const QColor& color); double shadeOpacity() const { return m_shadeOpacity; } void setShadeOpacity(double opacity); Qt::CursorShape cursor() const { return m_cursor; } void setCursor(Qt::CursorShape cursor); QString spotShape() const { return m_spotShape; } void setSpotShape(const QString& spotShapeQmlComponent); double spotRotation() const { return m_spotRotation; } void setSpotRotation(double rotation); bool spotRotationAllowed() const; bool showBorder() const { return m_showBorder; } void setShowBorder(bool show); void setBorderColor(const QColor& color); QColor borderColor() const { return m_borderColor; } void setBorderSize(int size); int borderSize() const { return m_borderSize; } void setBorderOpacity(double opacity); double borderOpacity() const { return m_borderOpacity; } bool zoomEnabled() const { return m_zoomEnabled; } void setZoomEnabled(bool enabled); double zoomFactor() const { return m_zoomFactor; } void setZoomFactor(double factor); bool multiScreenOverlayEnabled() const { return m_multiScreenOverlayEnabled; } void setMultiScreenOverlayEnabled(bool enabled); bool overlayDisabled() const { return m_overlayDisabled; } void setOverlayDisabled(bool disabled); template struct SettingRange { const T min; const T max; }; static const SettingRange& spotSizeRange(); static const SettingRange& dotSizeRange(); static const SettingRange& dotOpacityRange(); static const SettingRange& shadeOpacityRange(); static const SettingRange& spotRotationRange(); static const SettingRange& borderSizeRange(); static const SettingRange& borderOpacityRange(); static const SettingRange& zoomFactorRange(); static const SettingRange& inputSequenceIntervalRange(); class SpotShapeSetting { public: SpotShapeSetting(const QString& displayName, const QString& key, const QVariant& defaultValue, const QVariant& minValue, const QVariant& maxValue, int decimals = 0) : m_displayName(displayName), m_settingsKey(key), m_minValue(minValue), m_maxValue(maxValue), m_defaultValue(defaultValue), m_decimals(decimals) {} const QString& displayName() const { return m_displayName; } const QString& settingsKey() const { return m_settingsKey; } const QVariant& minValue() const { return m_minValue; } const QVariant& maxValue() const { return m_maxValue; } const QVariant& defaultValue() const { return m_defaultValue; } int decimals() const { return m_decimals; } private: QString m_displayName; QString m_settingsSection; QString m_settingsKey; QVariant m_minValue = 0; QVariant m_maxValue = 100; QVariant m_defaultValue = m_minValue; int m_decimals = 0; }; class SpotShape { public: QString qmlComponent() const { return m_qmlComponent; } QString name() const { return m_name; } QString displayName() const { return m_displayName; } bool allowRotation() const { return m_allowRotation; } const QList& shapeSettings() const { return m_shapeSettings; } private: SpotShape(const QString& qmlComponent, const QString& name, const QString& displayName, bool allowRotation, QList shapeSettings= {}) : m_qmlComponent(qmlComponent), m_name(name), m_displayName(displayName), m_allowRotation(allowRotation), m_shapeSettings(std::move(shapeSettings)){} QString m_qmlComponent; QString m_name; QString m_displayName; bool m_allowRotation = true; QList m_shapeSettings; friend class Settings; }; static const QList& spotShapes(); QQmlPropertyMap* shapeSettings(const QString& shapeName); struct StringProperty { enum Type { Integer, Double, Bool, StringEnum, Color }; static QString typeToString(Type type); Type type; QVariantList range; std::function setFunction; }; const std::vector>& stringProperties() const; void savePreset(const QString& preset); void loadPreset(const QString& preset); void removePreset(const QString& preset); const std::vector& presets() const; PresetModel* presetModel(); void setDeviceInputSeqInterval(const DeviceId& dId, int intervalMs); int deviceInputSeqInterval(const DeviceId& dId) const; void setDeviceInputMapConfig(const DeviceId& dId, const InputMapConfig& imc); InputMapConfig getDeviceInputMapConfig(const DeviceId& dId); void setTimerSettings(const DeviceId& dId, int timerId, bool enabled, int seconds); std::pair timerSettings(const DeviceId& dId, int timerId) const; void setVibrationSettings(const DeviceId& dId, uint8_t len, uint8_t intensity); std::pair vibrationSettings(const DeviceId& dId) const; signals: void showSpotShadeChanged(bool show); void spotSizeChanged(int size); void dotSizeChanged(int size); void showCenterDotChanged(bool show); void dotColorChanged(const QColor& color); void dotOpacityChanged(double opacity); void shadeColorChanged(const QColor& color); void shadeOpacityChanged(double opcacity); void cursorChanged(Qt::CursorShape cursor); void spotShapeChanged(const QString& spotShapeQmlComponent); void spotRotationChanged(double rotation); void spotRotationAllowedChanged(bool allowed); void showBorderChanged(bool show); void borderColorChanged(const QColor& color); void borderSizeChanged(int size); void borderOpacityChanged(double opacity); void zoomEnabledChanged(bool enabled); void zoomFactorChanged(double zoomFactor); void multiScreenOverlayEnabledChanged(bool enabled); void overlayDisabledChanged(bool disabled); void presetLoaded(const QString& preset); private: QSettings* m_settings = nullptr; PresetModel* m_presetModel; std::map m_shapeSettings; QQmlPropertyMap* m_shapeSettingsRoot = nullptr; int m_spotSize = 30; ///< Spot size in percentage of available screen height, but at least 50 pixels. int m_dotSize = 5; ///< Center Dot Size (3-100 pixels) QColor m_dotColor; double m_dotOpacity = 0.8; QColor m_shadeColor; double m_shadeOpacity = 0.3; Qt::CursorShape m_cursor = Qt::BlankCursor; QString m_spotShape; double m_spotRotation = 0.0; QColor m_borderColor; int m_borderSize = 3; double m_borderOpacity = 0.8; bool m_zoomEnabled = false; double m_zoomFactor = 2.0; bool m_showSpotShade = true; bool m_showCenterDot = false; bool m_spotRotationAllowed = false; bool m_showBorder = false; bool m_multiScreenOverlayEnabled = false; bool m_overlayDisabled = false; std::vector> m_stringPropertyMap; private: void init(); void load(const QString& preset = QString()); QObject* shapeSettingsRootObject(); void shapeSettingsPopulateRoot(); void shapeSettingsInitialize(); void shapeSettingsSetDefaults(); void shapeSettingsLoad(const QString& preset = QString()); void shapeSettingsSavePreset(const QString& preset); void setSpotRotationAllowed(bool allowed); void initializeStringProperties(); }; // ------------------------------------------------------------------------------------------------- class PresetModel : public QAbstractListModel { Q_OBJECT public: PresetModel(QObject* parent = nullptr); PresetModel(std::vector&& presets, QObject* parent = nullptr); int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; const auto& presets() const { return m_presets; } bool hasPreset(const QString& preset) const; private: friend class Settings; void addPreset(const QString& preset); void removePreset(const QString& preset); std::vector m_presets; }; Projecteur-0.10/src/spotlight.cc000066400000000000000000000546461451344070600167120ustar00rootroot00000000000000// 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" #include "virtualdevice.h" #include #include #include #include #include #include #include #include DECLARE_LOGGING_CATEGORY(device) DECLARE_LOGGING_CATEGORY(hid) DECLARE_LOGGING_CATEGORY(input) namespace { const auto hexId = logging::hexId; // 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) : QObject(parent) , 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(spotlightActiveTimoutMs); connect(m_activeTimer, &QTimer::timeout, this, [this](){ setSpotActive(false); workaroundLogitechFirstMoveEvent = true; }); if (m_options.enableUInput) { 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 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. 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(); } // ------------------------------------------------------------------------------------------------- Spotlight::~Spotlight() = default; // ------------------------------------------------------------------------------------------------- bool Spotlight::anySpotlightDeviceConnected() const { for (const auto& dc : m_deviceConnections) { if (dc.second->subDeviceCount()) { return true; } } return false; } // ------------------------------------------------------------------------------------------------- uint32_t Spotlight::connectedDeviceCount() const { uint32_t count = 0; for (const auto& dc : m_deviceConnections) { if (dc.second->subDeviceCount()) { ++count; } } return count; } // ------------------------------------------------------------------------------------------------- void Spotlight::setSpotActive(bool active) { if (m_spotActive == active) { return; } m_spotActive = active; if (!m_spotActive) { m_activeTimer->stop(); } emit spotActiveChanged(m_spotActive); } // ------------------------------------------------------------------------------------------------- std::shared_ptr Spotlight::deviceConnection(const DeviceId& deviceId) { const auto find_it = m_deviceConnections.find(deviceId); return (find_it != m_deviceConnections.end()) ? find_it->second : std::shared_ptr(); } // ------------------------------------------------------------------------------------------------- std::vector Spotlight::connectedDevices() const { std::vector devices; devices.reserve(m_deviceConnections.size()); for (const auto& dc : m_deviceConnections) { devices.emplace_back(ConnectedDeviceInfo{ dc.first, dc.second->deviceName() }); } return devices; } // ------------------------------------------------------------------------------------------------- int Spotlight::connectDevices() { const auto scanResult = DeviceScan::getDevices(m_options.additionalDevices); for (const auto& dev : scanResult.devices) { auto& dc = m_deviceConnections[dev.id]; if (!dc) { 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) { 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 { // Input event sub devices if (scanSubDevice.type == DeviceScan::SubDevice::Type::Event) { auto devCon = SubEventConnection::create(scanSubDevice, *dc); 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 (dc->subDeviceCount() == 0) { // Load Input mapping settings when first sub-device gets added. const auto im = dc->inputMapper().get(); im->setKeyEventInterval(m_settings->deviceInputSeqInterval(dev.id)); im->setConfiguration(m_settings->getDeviceInputMapConfig(dev.id)); connect(im, &InputMapper::configurationChanged, this, [this, id=dev.id, im]() { m_settings->setDeviceInputMapConfig(id, im->configuration()); }); static QString lastPreset; connect(im, &InputMapper::actionMapped, this, [this](const std::shared_ptr& action) { if (action->type() == Action::Type::CyclePresets) { auto it = std::find(m_settings->presets().cbegin(), m_settings->presets().cend(), lastPreset); if ((it == m_settings->presets().cend()) || (++it == m_settings->presets().cend())) { it = m_settings->presets().cbegin(); } if (it != m_settings->presets().cend()) { lastPreset = *it; m_settings->loadPreset(lastPreset); } } else if (action->type() == Action::Type::ToggleSpotlight) { 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){ lastPreset = preset; }); } dc->addSubDevice(std::move(subDeviceConnection)); if (dc->subDeviceCount() == 1) { QTimer::singleShot(0, this, [this, id = dev.id, devName = dc->deviceName(), anyConnectedBefore](){ 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); } }); } logDebug(device) << tr("Connected sub-device: %1 (%2:%3) %4") .arg(dc->deviceName(), hexId(dev.id.vendorId), hexId(dev.id.productId), scanSubDevice.deviceFile); emit subDeviceConnected(dev.id, dc->deviceName(), scanSubDevice.deviceFile); } if (dc->subDeviceCount() == 0) { m_deviceConnections.erase(dev.id); } } return m_deviceConnections.size(); } // ------------------------------------------------------------------------------------------------- void Spotlight::removeDeviceConnection(const QString &devicePath) { for (auto dc_it = m_deviceConnections.begin(); dc_it != m_deviceConnections.end(); ) { if (!dc_it->second) { dc_it = m_deviceConnections.erase(dc_it); continue; } auto& dc = dc_it->second; if (dc->removeSubDevice(devicePath)) { emit subDeviceDisconnected(dc_it->first, dc->deviceName(), devicePath); } if (dc->subDeviceCount() == 0) { logInfo(device) << tr("Disconnected device: %1 (%2:%3)") .arg(dc->deviceName(), hexId(dc_it->first.vendorId), hexId(dc_it->first.productId)); emit deviceDisconnected(dc_it->first, dc->deviceName()); dc_it = m_deviceConnections.erase(dc_it); } else { ++dc_it; } } } // ------------------------------------------------------------------------------------------------- void Spotlight::onEventDataAvailable(int fd, SubEventConnection& connection) { const bool isNonBlocking = connection.hasFlags(DeviceFlag::NonBlocking); while (true) { auto& buf = connection.inputBuffer(); auto& ev = buf.current(); if (::read(fd, &ev, sizeof(ev)) != sizeof(ev)) { if (errno != EAGAIN) { const bool anyConnectedBefore = anySpotlightDeviceConnected(); connection.disconnect(); QTimer::singleShot(0, this, [this, devicePath=connection.path(), anyConnectedBefore](){ removeDeviceConnection(devicePath); if (!anySpotlightDeviceConnected() && anyConnectedBefore) { emit anySpotlightDeviceConnectedChanged(false); } }); } break; } ++buf; if (ev.type == EV_SYN) { // Check for relative events -> set Spotlight active 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 // 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_virtualMouseDevice) { // forward events to virtual mouse device m_virtualMouseDevice->emitEvents(buf.data(), buf.pos()); } } else { // Forward events to input mapper for the device connection.inputMapper()->addEvents(buf.data(), buf.pos()); } buf.reset(); } else if (buf.pos() >= buf.size()) { // No idea if this will ever happen, but log it to make sure we get notified. logWarning(device) << tr("Discarded %1 input events without EV_SYN.").arg(buf.size()); connection.inputMapper()->resetState(); buf.reset(); } if (!isNonBlocking) { break; } } // end while loop } // ------------------------------------------------------------------------------------------------- void Spotlight::registerForNotifications(SubHidppConnection* connection) { using namespace HIDPP; // Logitech button next and back press and hold + movement if (const auto rcIndex = connection->featureSet().featureIndex(FeatureCode::ReprogramControlsV4)) { connection->registerNotificationCallback(this, rcIndex, makeSafeCallback( [this, connection](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; if (!m_holdButtonStatus->nextPressed() && isNextPressed) { const auto& nextHold = SpecialKeys::eventSequenceInfo(SpecialKeys::Key::NextHold); for (const auto& ke: nextHold.keyEventSeq) { connection->inputMapper()->addEvents(ke); } } if (!m_holdButtonStatus->backPressed() && isBackPressed) { const auto& backHold = SpecialKeys::eventSequenceInfo(SpecialKeys::Key::BackHold); for (const auto& ke: backHold.keyEventSeq) { connection->inputMapper()->addEvents(ke); } } m_holdButtonStatus->setButtonsPressed(isNextPressed, isBackPressed); }), 0 /* function 0 */); 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::addInputEventHandler(std::shared_ptr connection) { 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) { onEventDataAvailable(fd, *connection); }); return true; } // ------------------------------------------------------------------------------------------------- bool Spotlight::setupDevEventInotify() { int fd = -1; #if defined(IN_CLOEXEC) fd = inotify_init1(IN_CLOEXEC); #endif if (fd == -1) { fd = inotify_init(); if (fd == -1) { logError(device) << tr("inotify_init() failed. Detection of new attached devices will not work."); return false; } } fcntl(fd, F_SETFD, FD_CLOEXEC); const int wd = inotify_add_watch(fd, "/dev/input", IN_CREATE | IN_DELETE); if (wd < 0) { logError(device) << tr("inotify_add_watch for /dev/input returned with failure."); return false; } const auto notifier = new QSocketNotifier(fd, QSocketNotifier::Read, this); connect(notifier, &QSocketNotifier::activated, this, [this](int fd) { int bytesAvaibable = 0; if (ioctl(fd, FIONREAD, &bytesAvaibable) < 0 || bytesAvaibable <= 0) { return; // Error or no bytes available } QVarLengthArray buffer(bytesAvaibable); const auto bytesRead = read(fd, buffer.data(), static_cast(bytesAvaibable)); const char* at = buffer.data(); const char* const end = at + bytesRead; while (at < end) { const auto event = reinterpret_cast(at); if ((event->mask & (IN_CREATE)) && QString(event->name).startsWith("event")) { // Trigger new device scan and connect if a new event device was created. m_connectionTimer->start(); } at += sizeof(inotify_event) + event->len; } }); connect(notifier, &QSocketNotifier::destroyed, [notifier]() { ::close(static_cast(notifier->socket())); }); return true; } Projecteur-0.10/src/spotlight.h000066400000000000000000000051361451344070600165420ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #pragma once #include #include #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, public async::Async { Q_OBJECT public: struct Options { bool enableUInput = true; // enable virtual uinput device std::vector additionalDevices; }; explicit Spotlight(QObject* parent, Options options, Settings* settings); virtual ~Spotlight(); bool spotActive() const { return m_spotActive; } void setSpotActive(bool active); struct ConnectedDeviceInfo { DeviceId id; QString name; }; bool anySpotlightDeviceConnected() const; uint32_t connectedDeviceCount() const; std::vector connectedDevices() const; std::shared_ptr deviceConnection(const DeviceId& deviceId); signals: void deviceConnected(const DeviceId& id, const QString& name); void deviceDisconnected(const DeviceId& id, const QString& name); void subDeviceConnected(const DeviceId& id, const QString& name, const QString& path); void subDeviceDisconnected(const DeviceId& id, const QString& name, const QString& path); void anySpotlightDeviceConnectedChanged(bool connected); void spotActiveChanged(bool isActive); private: enum class ConnectionResult { CouldNotOpen, NotASpotlightDevice, Connected }; ConnectionResult connectSpotlightDevice(const QString& devicePath, bool verbose = false); bool addInputEventHandler(std::shared_ptr connection); void registerForNotifications(SubHidppConnection* connection); bool setupDevEventInotify(); int connectDevices(); void removeDeviceConnection(const QString& devicePath); void onEventDataAvailable(int fd, SubEventConnection& 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_virtualMouseDevice; std::shared_ptr m_virtualKeyDevice; Settings* m_settings = nullptr; std::unique_ptr m_holdButtonStatus; }; Projecteur-0.10/src/spotshapes.cc000066400000000000000000000164701451344070600170570ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #include "spotshapes.h" #include #include #include namespace { const bool registered = [](){ SpotShapeStar::qmlRegister(); SpotShapeNGon::qmlRegister(); return true; }(); } // end anonymous namespace SpotShapeStar::SpotShapeStar(QQuickItem* parent) : QQuickItem (parent) { setEnabled(false); setFlags(QQuickItem::ItemHasContents); } int SpotShapeStar::qmlRegister() { return qmlRegisterType("Projecteur.Shapes", 1, 0, "Star"); } QSGNode* SpotShapeStar::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* updatePaintNodeData) { if (width() <= 0 || height() <= 0 || m_color.alpha() == 0) { delete oldNode; return nullptr; } // Directly access the QSG transformnode for the Items node: updatePaintNodeData->transformNode->...; Q_UNUSED(updatePaintNodeData) const auto vertexCount = m_points*2+2; // Create geometry node for colored shape auto geometryNode = static_cast(oldNode); if (geometryNode == nullptr) { geometryNode = new QSGGeometryNode(); // Set geometry const auto geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), vertexCount); #if QT_VERSION >= 0x050800 geometry->setDrawingMode(QSGGeometry::DrawTriangleFan); #else geometry->setDrawingMode(GL_TRIANGLE_FAN); #endif geometryNode->setGeometry(geometry); geometryNode->setFlag(QSGNode::OwnsGeometry, true); auto* const material = new QSGFlatColorMaterial(); material->setColor(m_color); geometryNode->setMaterial(material); geometryNode->setFlag(QSGNode::OwnsMaterial); } else { const auto geometry = geometryNode->geometry(); if (geometry->vertexCount() != vertexCount) { geometry->allocate(vertexCount); } if (auto material = static_cast(geometryNode->material())) { material->setColor(m_color); } } QSGGeometry::Point2D* const vertices = geometryNode->geometry()->vertexDataAsPoint2D(); const int numSegments = m_points * 2; 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); // Vertices for (outer) star points for(int seg=1; seg < numSegments; seg+=2, theta+=deltaRad) { const float x = cx * cosf(theta); const float y = cy * sinf(theta); vertices[seg].set(x + cx, y + cy); } const float dist0_1 = std::sqrt(std::pow(vertices[0].x-vertices[1].x, 2.0f) + std::pow(vertices[0].y-vertices[1].y, 2.0f)); const float dist1_3_2 = std::sqrt(std::pow(vertices[1].x-vertices[3].x, 2.0f) + std::pow(vertices[1].y-vertices[3].y, 2.0f)) / 2.0f; const float maxInnerDist = std::sqrt(std::pow(dist0_1,2.0f) - std::pow(dist1_3_2, 2.0f)); const float innerDistance = maxInnerDist * float(m_innerRadius)/100.0f; // Vertices for inner radius theta = -static_cast(90.0 * M_PI/180.0) + deltaRad/2 ; for(int seg=2; seg < numSegments+1; seg+=2, theta+=deltaRad) { const float x = innerDistance * std::cos(theta); const float y = innerDistance * std::sin(theta); vertices[seg].set(x + cx, y + cy); } vertices[vertexCount-1] = vertices[1]; // last star point = first star point geometryNode->markDirty(QSGGeometryNode::DirtyGeometry); return geometryNode; } QColor SpotShapeStar::color() const { return m_color; } void SpotShapeStar::setColor(const QColor &color) { if (m_color == color) return; m_color = color; emit colorChanged(color); update(); // redraw, schedules updatePaintNode()... } int SpotShapeStar::points() const { return m_points; } void SpotShapeStar::setPoints(int points) { if (m_points == points) return; m_points = qMin(qMax(3, points), 100); emit pointsChanged(m_points); update(); // redraw, schedules updatePaintNode()... } int SpotShapeStar::innerRadius() const { return m_innerRadius; } void SpotShapeStar::setInnerRadius(int radiusPercentage) { if (radiusPercentage > m_innerRadius || radiusPercentage < m_innerRadius) { m_innerRadius = qMin(qMax(5, radiusPercentage), 100); emit innerRadiusChanged(m_innerRadius); update(); // redraw, schedules updatePaintNode()... } } SpotShapeNGon::SpotShapeNGon(QQuickItem* parent) : QQuickItem (parent) { setEnabled(false); setFlags(QQuickItem::ItemHasContents); } int SpotShapeNGon::qmlRegister() { return qmlRegisterType("Projecteur.Shapes", 1, 0, "NGon"); } QColor SpotShapeNGon::color() const { return m_color; } void SpotShapeNGon::setColor(const QColor &color) { if (m_color == color) return; m_color = color; emit colorChanged(color); update(); // redraw, schedules updatePaintNode()... } int SpotShapeNGon::sides() const { return m_sides; } void SpotShapeNGon::setSides(int sides) { if (m_sides == sides) return; m_sides = qMin(qMax(3, sides), 100); emit sidesChanged(m_sides); update(); // redraw, schedules updatePaintNode()... } QSGNode* SpotShapeNGon::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* updatePaintNodeData) { if (width() <= 0 || height() <= 0 || m_color.alpha() == 0) { delete oldNode; return nullptr; } // Directly access the QSG transformnode for the Items node: updatePaintNodeData->transformNode->...; Q_UNUSED(updatePaintNodeData) const auto vertexCount = m_sides + 2; // Create geometry node for colored shape auto geometryNode = static_cast(oldNode); if (geometryNode == nullptr) { geometryNode = new QSGGeometryNode(); // Set geometry const auto geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), vertexCount); #if QT_VERSION >= 0x050800 geometry->setDrawingMode(QSGGeometry::DrawTriangleFan); #else geometry->setDrawingMode(GL_TRIANGLE_FAN); #endif geometryNode->setGeometry(geometry); geometryNode->setFlag(QSGNode::OwnsGeometry, true); const auto material = new QSGFlatColorMaterial(); material->setColor(m_color); geometryNode->setMaterial(material); geometryNode->setFlag(QSGNode::OwnsMaterial); } else { const auto geometry = geometryNode->geometry(); if (geometry->vertexCount() != vertexCount) { geometry->allocate(vertexCount); } if (auto material = static_cast(geometryNode->material())) { material->setColor(m_color); } } QSGGeometry::Point2D* const vertices = geometryNode->geometry()->vertexDataAsPoint2D(); 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); for(int seg=1; seg < vertexCount; ++seg, theta+=deltaRad) { const float x = cx * cosf(theta); const float y = cy * sinf(theta); vertices[seg].set(x + cx, y + cy); } vertices[vertexCount-1] = vertices[1]; // last shape point = first shape point geometryNode->markDirty(QSGGeometryNode::DirtyGeometry); return geometryNode; } Projecteur-0.10/src/spotshapes.h000066400000000000000000000033431451344070600167140ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md #pragma once #include class SpotShapeStar : public QQuickItem { Q_OBJECT Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) Q_PROPERTY(int points READ points WRITE setPoints NOTIFY pointsChanged) Q_PROPERTY(int innerRadius READ innerRadius WRITE setInnerRadius NOTIFY innerRadiusChanged) public: static int qmlRegister(); explicit SpotShapeStar(QQuickItem* parent = nullptr); QColor color() const; void setColor(const QColor &color); int points() const; void setPoints(int points); int innerRadius() const; // inner star radius in percent (between 5 and 100) void setInnerRadius(int radiusPercentage); signals: void colorChanged(QColor color); void pointsChanged(int points); void innerRadiusChanged(int innerRadius); protected: virtual QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* updatePaintNodeData) override; private: QColor m_color = Qt::black; int m_points = 3; int m_innerRadius = 50; }; class SpotShapeNGon : public QQuickItem { Q_OBJECT Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) Q_PROPERTY(int sides READ sides WRITE setSides NOTIFY sidesChanged) public: static int qmlRegister(); explicit SpotShapeNGon(QQuickItem* parent = nullptr); QColor color() const; void setColor(const QColor &color); int sides() const; void setSides(int points); signals: void colorChanged(QColor color); void sidesChanged(int sides); protected: virtual QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* updatePaintNodeData) override; private: QColor m_color = Qt::black; int m_sides = 3; }; Projecteur-0.10/src/virtualdevice.cc000066400000000000000000000120641451344070600175270ustar00rootroot00000000000000// 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 /* 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 (%1; %2)").arg(m_userName, m_deviceName); } } // ------------------------------------------------------------------------------------------------- // 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, const char* location) { const QFileInfo fi(location); 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::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::shared_ptr(); } struct uinput_user_dev uinp {}; snprintf(uinp.name, sizeof(uinp.name), "%s", name); uinp.id.bustype = BUS_USB; uinp.id.vendor = virtualVendorId; uinp.id.product = virtualProductId; uinp.id.version = virtualVersionId; // Setup the uinput device // (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 relative event code bits on virtual device for (int i = 0; i < REL_CNT; ++i) { ioctl(fd, UI_SET_RELBIT, 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 const auto bytesWritten = write(fd, &uinp, sizeof(uinp)); if ((bytesWritten != sizeof(uinp)) || (ioctl(fd, UI_DEV_CREATE))) { ::close(fd); logWarn(virtualdevice) << VirtualDevice_::tr("Unable to create Virtual (UINPUT) device."); return std::unique_ptr(); } // Log the device 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("%1; /sys/devices/virtual/input/%2") .arg(name, sysfs_device_name)); 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) { logError(virtualdevice) << VirtualDevice_::tr("Error while writing to virtual device."); } } } // ------------------------------------------------------------------------------------------------- void VirtualDevice::emitEvents(const std::vector& events) { emitEvents(events.data(), events.size()); } Projecteur-0.10/src/virtualdevice.h000066400000000000000000000026371451344070600173760ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur // - See LICENSE.md and README.md // 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 or mouse class VirtualDevice { private: struct Token; int m_uinpFd = -1; QString m_userName; QString m_deviceName; public: 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"); VirtualDevice(Token, int fd, const char* name, const char* sysfs_name); ~VirtualDevice(); void emitEvents(const struct input_event[], size_t num); void emitEvents(const std::vector& events); };