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.png000066400000000000000000002052471451344070600221640ustar00rootroot00000000000000‰PNG  IHDRcc@È}sBIT|dˆ IDATxœìÝgxU€ágfÓ !�„t%¡w�*‚‘ª(E;Š] ÒDìŠÊ'vET@Š€"Ò”z' ©$!¤@’Ý9ß�Ý„’@�° ï}9—ìîÌé3óî™™�Ö`ø‹J×A)…¦Yÿ¯ 蚆aX@Shh˜-tMCÓ4 C¡¡ CYÿmÝ4M”m=0 º®S’®k躎ÙlÁ¤ë(¬ùCa: k:†2lïk`M]×@ÓPÊ@³•Á¤ëÖréÊ0Ðu…eÝNÓ44À0 @CÓ5, 𮣡aÒ4 ,fLºš†¡ tMÀ0ºnÂPtM¥P(ë¿ÑΩ—¡ [^Ö¶²4­°þÊZ.¥PJYÛX)ŒÂöV¶ü-†�©0 …I³ÖÉšž²•˺¾f{]ØöÖ±Ö×ÁÁdËßÚfJ)¬Ý§ÙÚ[a2Yë§i&”aØÆ„µÁM&‡¢¾1”²u…hØúÎ0е�µnÖl¬}¤°Ž#]Ó°(…†µÎh`1¬i~æè ÛʨaÊö™®[Ƕ±¦ÛþoQ “nÍïìøP¶úZÓÖlí®AQÝ,…ÉAð¶…­]‹  ,ÖüL& [¿Ùš²k½¬cB³ÕGþ‡®éÖvÐt”2 ‡š¦£ëÖþÅVÖµ²õ¹¦iè¶ñkg÷…ÂÁd­“¡Šï; G“õ}ÃZ%ð¾g1 t]+joM³¶�ƒÉ¡h\J¡ºnÃÖV†�®Yß×°®§éÖöÒÐ1í; ) ‹²�‡b}o6›Q8šlûæÙ±lQ¶6ǰ�!Ývüp°î;šõ¨c( úÙ†*Vlû‡a;6~VX6³Ùz<2 ë1K×­cNÙ:Ádr@ëþ§ëE}nÒu,£¨ß ûÅb¨¢qY4‹Ñõ²Xÿ_|Ól´ÂÛÅbÁA×Ñt‹m4é&J�cšm_0XPÖº)kËYTñãDáñÙv\V` PÊÀ¤› ›½XY­û„VØ#Zá‡g�[…ÇÏB&“õØfFÑqVö�mýÂ㨦[ÿoÒuÌë1Úzð³ ˜-Ð4L…cÛb`2ÙÆ³2ŠŽ¥`2YÇ´2ºI·‘”Qtì+<‡(¥0[ L&�Â#—µmëëgM‹-O ãœã�µ¯¬}­ÛÆŽ-Ý„íp†í…V¬m m¶ºZ; ‹mÀ˜t eÛ_ Ï£&]ÇbX0é&,†Û¡Ðmç0Ãb`VŽººn;¾Öã�aך^xîT˜t,sÑqפëE»Qñópáþ£ë&ÛqVa¶X(zá~­éEÃÁAÓ1ÖóŸÂ@¶ã¦­®…íi=Wž¬Ç½Âs‚a;&˜Š�]Ö+XÛD)ë8/ŒYLzá´¾gØv(ëºÖ}®ø9©0 (?Û~jEÇV“n²Ž]ýlý Û>fÒ¬1ƒEY(v@(:>¡ÀÁä`-·fÛyÚÚ¶öÓM¶ó�u[‡¼Ôcä%㢗8‚!„¸ª9Ù»ÿ‚Cß/‹é«”šnÉ×ÅÓq¬@ú¥}^Þ6%?»ØôM¥¼ sémVZÞšm)>}R\(UFšz‰ÿP–®S¸}ñuŠoo”xïlX{þúÅ£½Ä:%·1н.ú2R"�’ë–Tüý‚R>Ë/e³î„9¬1ºbB!„Wžƒ‘�» ]1!„Bûp2 ΙýB!„W˜cB!„v$Á˜B!„I0&„BaGŒ !„BØ‘cB!„v$Á˜B!„I0&„BaGŒ !„BØ‘cB!„v$Á˜B!„I0&„BaGŒ !„BØ‘ƒ½ „¨\šwKîyz(õ²Wðá; 9\`ï]{L¾�é;¤7ÔÀMeûû'¼½ðf{LñŸ Á˜W S$}ÇŽ¤Kˆ'N:æÓd�8ÎÁíkøuéßÄŸV—”¬æH°¯ Aø:i.¸´tÀØÝ5²õù‡¯ÿÂ1ã’“ºü*©ýÐüé4â>ºÕ6qòH [Oêd'�ÀryK/„¸ŽI0&ÄÕBsÆÓÛgý4‰û’fxIóµ¨âÄ”ékH»„xˆ[Ì´—wᓜ؜KĬEô%$´^)Wá•Ô~¸Ô$ª†änä»w¿d{þe/¹â:'Á˜W#… ³g°4ÉÀ!¸Ͼ؇°¨¦DyüÅÚütÚ—j…PÕ=?�¾žÊ×ûƒè< �›„SUÏ&aÏzý¸˜í'Ìhžm¹gÜ0êe¯bÚ‹³Ùk½J]ºÞÞ‡öõBñqÈáøÎ•ü4ç7ögY£Í=‚ýúÓµiM|� È<¾–o¦ýÄ^[õê=yaFO0Òøã�—˜sÀRnšz`ÆŽïOXʯ¼6y>±îíyìµbe2Õ,µ^_mΡÂñÓ¥´ß�º”QvÍÁMCsoÃ#´Ìì˜9†×碕׎Υש¼¼p‰¢÷ƒhâ�·‡ ZÁ)’ö­eÞìEì]“ÌÕ4¹)Äõê*üz+„(¤98`P ¥@s ¢~ó(B¼Î»c+;Ç‘�áK·Q�qû 5qN?Ⱦ$¨Þ¤'=q;Q.¥$êJ�Q£Ð*íø¶Å·ÀÃ#Úᣦ º>üC:Ô¥JîQví>Bzf&™Åf„Tn<Û×®eíÚMÎTNóBõ,­^'NW<»Äöë~eWy‰ì\·–µk×±;ÅŒº@�/%/ÍÑ�uÃñwËåèömìKw"¸ñ-Ü7¨žåõ�Q~Yôðn ¿­%áIl]»–ÍRH=™)�˜W ™âj£Òîî'¨��¡�xš¹»6³'G�‡m�ì�,üÌ:Ëå=”W"œ1âñÞk ‰³øÐñ‰‰ ‰º�N aßÞs“w¨Ý‘Náα ùì“?HÕ‚Éý47Gµ¤�×ZÖv¢K¤ *i9ïOý‰ÃÅ‚°Âoo*3†ÅßÌ/ºgÌ!ºü4ÿºØº«W‰RS§÷£ô®c²¾42øçû™¬N,%œ¸”ö+§ìk o;½Ÿß¾=[¶ Õ¹h»KÉ+g‹>ŸÍ¾ª]3i 5#ëbú›µJï‡è¡å–e­É@åeplûJÖîN"W"1!®Œ qµÑ\¨V£.~æÓœ:q�MÛVóëÒÈPpþ$“†³·š¢ !–D3@&ñq™¨h_¼}8—w€eÿ:6$·¦kpOž_›ƒIy¸xeñ×ôYl<KÜ)ƒZÞ­ú„ÇN;�ôÇ',¼@šÊ|�ýIfê7gГδÈò&ÌdŸö*éBíQz0viÛ]j^EÛ—Õ7—Ÿ®©v?žBFbgÜÜ­—, Cîâ*!Á˜×:sK?üÆÀþtjIm=—ä�¿±xî/ì:­Ð¼q”20`>Ì/ïÿ�Óý{Ñ®~õiä�JáØ~À’�ùïÏàÌ€[¹!º *rRwáì¦AÖµß;o$ªVŸN!ÿo7´‚ ¤iijüóoñ| Ík4¢™:CNz‡ÆqÊÞÁÊ^æ•ÚKÙîRó*TVß8)'] gxDШM}�Ó¤ÞÀÂÿ ÁÞm/ÄUîrÍ|]ˆÖµkW¹e@ˆÿ"Í�½î¤]½:4ªYcßwŒŸ¶Š Ùã…âª"3cBüWi¾D4jB“Eæ‘õ,üa�bBq’™1!„B;’}B!„°# Æ„B!ìH‚1!„B;’`L!„ÂŽ$B!„°# Æ„B!ìH‚1!„B;’`L!„ÂŽ$B!„°# Æ„B!ìH‚1!„B;’`L!„ÂŽ$B!„°#-##CÙ»B!„×+™B!„°# Æ„B!ìH‚1!„B;’`L!„ÂŽ$B!„°# Æ„B!ìH‚1!„B;’`L!„ÂŽ$B!„°# Æ„B!ìÈÁÞwðàA²²²HKK³wQìÎ××///jÖ¬iï¢!„—Dþ6å5æàÁƒèºN³fÍì]”«ÆæÍ›ˆŒŒ´sI„BˆŠ«p0Gnn.ÙÙÙ•U&Q <<‚Á³Ó�gS…BØ“]Ï¡sçÎeÏž= :ggç²WÌßÀ÷�ccð(>_¶–u¿Çë÷ö¤að¿¿ÊjÎËÃRîÙØ�ÆCÆ2ªsõÙXŠü|³ÝNüº‹~~ÕªÑ�NC&ðã²×i´þEÆ|m U&›>z€ÎÑ�Tq÷'ªçó,Š·@Á:F×ö ÛÇ EeÏ]8‚€Ð‘,?“ÍìUh8~ Ö¹Àlbf>J׺þ¸»zP­Þ½ÌIRe§ IË™pk}ª¹;ãR5„¶“6\xR!„ø¹¢ÁXñßDzX,Lž<WWWÆŒSþv™9�âCû;ï mT8aµšÐùŽþ´ôÖÀ¼‹OGt¥e½„‡R»yzoIEW égðp�¦D††Õv ã~>À™béçýú05¨Þ„çÖ– røyDM:¾¶Ãpœfÿ�/2°maÁ!DÔmFç;ßgKáfê$KG·¥nx5væž÷Ö‘^Tí|6ŒkAõ€Â`á%ÎJþŠûÅ.(@�»�1„7YXµh§”Á‰�ÑÜ~:|°žCûð�>‹{�M¢©)Ý»x³yÅ_d*…RyüóûT‡n´q.L[¡”AÆ’'éýÔßÔ›´„˜ƒ»YõÕh:ù«²Ó6 øçÝ'x/w8K�¦“vh _Œ¨‡©‚õ»ì¿»¦²‰ùv4ý[Õ%,´6-ú<ÅWÛO� ¤ âX6u76Œ $8Œ:M:Ðÿ•?9%SlB!.Á Æ’““éß¿?o¿ývÑ{…³b�<òån¯ù´åææ¹Ì}~ï/ÝËÉâ·d©TönN ñ¸ålÜ´–yS»�þÙ=Œøßn ãøw<>â’»¾ÍÒ�ë˜óL8�¹›)ër‹’pêþ>{bc‰=ö7Sn(ï6tEæãòì:Ÿþžµ[7óÇkm9¹y7I…'bÍ�¦}Îïÿfé[퉟ö8o¯/Œºi5qGcc‰=ø!½*ötÀeæJ@ –ôTNZÒùõ˘Oä….øßÀ#cà¾úWÖŸv¡m¿î¸®ZȪ, `+‹—gqc¿ñ(žœ:É’O„!¯ñú�-¨JT‹h´òÒÖp¯â�9a71ñ§qò‰ n˜'ö}ôAqò·>1†ú/ýÌú ™Òr?S‡?Ç’tä±åÍaŒœïʈÏþdóÖ¿ø¨·Æ¶q”w¡]!„(Ë Æ<<<ظq#/¿ü2©©©šÀÉ}³ñ^þ׋¦-{óÔ'ëIµ®à@Õà‚«‡Ñ û3L{ª {gÿÀ¶ƒ¸…ßñWà=¼üäMÔ ¡ñm“xþ–L~úî/NÛ¶ÖtGœ��qvr¡¼Q™¬øzý^bòíM­@hˆo‰'.�¨Mh`uoÉ�MÒØ¼9kQ5t'[^Žv¾Ï*—¤„“˜|ü©J*ñ‰gˆÿä¼ÝÜpss£jÏOHÈÏ =KáÖñNnuþ�ŸþÌ¢`ë|etáÎnÞçMF2±ÇÍ„Ô ;·=ŒòÒÖ©ÿÔ7̘Ãû=kQ³ý}L_oç{¸T&¿»Œã¥Þõ Ž¢Çs五ÙKÓQùëùöûãtów·Ž  Z0aÕÜì@ !„¸–]‘xÀÝÝ�©S§rêÔ)&L˜P¡Y±BšGz?óóÿÞÆò)-9ôÎP†MßÃùÏ-êT‹¬‰gJI†…¤„$‰ ¤èö2gBÃ8“˜h}Z²"ŒTâ̄Ԯ‰ËŬ¯{á㥑›súª»IÜ|è[>_é@Ç[;â©ûèïLä�™›Knn.¹¹gÈËZÂ}¸và®�UXþý"~›3Ÿœ[‡Ó­äƒ¯º�þq‡cÉ?ïýòÒ®CÿÉsÙth 3ZïbÜ/ðûìÇHåx’…àˆÐ³?Õa !"X‘”˜‚%+�„SþÔªUE0!„—Å›œ>|8Íš5cÆŒ <øâgÅJÒ=©ÝëyžíW…ݫ֓rÞ£‰ŠS ‰äøTÃO7€Š;B|QÔ–GÜÑd\ñÒÀÁdBä‘1Ñ’^?oHIHÆrᵡèt­“"?/ß.�™‘›NRR"ñ‡¶³ò› ÜÖs,;[Mæ�»‚Ñ5_zëAÖÌç˜0o G“RH8´�-‡²leu¦Íˆ»Yò,#gk ¼§ó¹—(4_zÞÝ‹‚oÇ2föF'$pxÇ6gù”“¶AꎿØr,�U•Z ÃpÏÉ$Óžwðëþ˜8~4î샖8ŽÆkT÷Çäæ�·s:‰IrQR!ÄåqÅ‚1]×™6mZÑëŠÌŠ‘ý³güÈê‰;~Œ½}Ë7+Óð­U oP9ìYý;Ž%»å{&N_KPß~4sÔ í=„É3?m%âÙñó^ýµ · i�+&ªG„bÚ±Œ_v&�xx{“®n®Xâv±ãD‰¨@óåÆÞm95w*oþv�ääÃlÚ°ïâfØtÂÂÜ9¸b[â“8³‹ø+r>×põöÁ²ð>"C¨ݬ;MßFè£óÙ¸`õœ¬ëÜñ1ó'Öfýs=hJݶƒ˜¼<¾èg8LõFðP›,ÒjÞËý­K{òU#àö�XðZb&ÜJÚµhÑw óŽå¤mæÀüçÐ$oï`ÚMI¦×»/ÑËóJ´‹Õ™“É$$$Ø–DR³Ü¸yh/øy Síæøñ},{}2ߟéÎàî¾h.7Ч›3Kßz™»I‰ÛÁ†˜”‹ Î…BˆóUê/ð—Ô¡C~øaV®\Y¡Y1#3�C>çýé{‰Ë(ÀÙ'‚f=§2s\\YœXówÏÜà S-¼Å—϶À x0ïÏ<Ÿ OÓcZ¡­øæ,ƵuÀ»×3Œ]2ŠWoiÉKî5øöÞèq/wÌy�Ç_jÏ?u.V�àAïðQÂs¼òlO>Êp"<Ò‹Skœ.xÍÊ™Ž�¾Dχ^f`ë7q îÎÄ>ahDeÿ\ª#^ßÅ©×/°šV•–Êʇ?-ýs=„û=Éýç¼éÁàŸ3\”†-ú„?ú¤ÄÆe§ÝvüŽŽ¿˜zT†ÖMêL³I…¯MÔxäÖŒŸÊ×'2nr?Ú$*šÜÊ ³&q«¯T¥ûËŸ1öù LІdªS×ÿ4D8á(×-…B\-##㢯šmÞ¼™&Mš`±\Ü<@LL ]ºt©P�V¬XAÆ /~ƒ‚?y¦ÙãhÓ7óV§Êÿƒ<*ë8‡ÒMøx»c:�Àúé�ñôÁáüþí]Uò<£ÉdbåÊ• 0 r3ºýüóÏtîÜù¢Çæ¿q:ù0I†>Žä§lcÖ˜‡YÞég–5iÕw2Óºø@L!„(®Rƒ±ˆˆˆÊLÞÊñFÞŽÙQùùظuDz-ã®X~âjã@ãÇ~bãcö.‡BˆÿŠkîO !„Bü—TxfÌl6_‘ûrÄùäòdù®¥±)})„¢P…ƒ1‹Å‚Ù|þO­ ao26…B\‹*Œedd°nݺÊ*‹¸º.W–KS­Zµkrlþþûïö.‚BˆJеk׋^·Â3cýû÷¯è&BTºvíÚÙ»B!óæÍ«Ðú2Í"„BaGŒ !„BØ‘cB!„v$Á˜B!„I0&„BaGŒ !„BØ‘cB!„v$Á˜B!„I0&„BaGþþëÁÎ�;9yò$©©©ö.ŠB!ìÈßßêÕ«WiyH0VÂÎ�;1›ÍtèÐÁÞEB!ÄU`Ë–-ìÚµ‹úõëWJúr™²„“'OÒ´iS{C!„W‰fÍš‘‘‘QiéËÌX ©©©(¥ì] !„B\E*óÖ% ÆJ!Á˜B!® ÆÊ ™B!4M«Ôôåž±ÿ Kn ÇSOÛ»%œŠÝÊÖ#ÙT8Ì5g‘˜�NÁ¿É^å�’’Sñ¼+ÈÈH&5ï‚…!39…3•\–sä¦&’žE3=_Á ’Ó-eü×hêzºãéé‰ÿà¹d_R&Šü“‰¤æÈ*ûÉgéÈpªxzâîÄK.¸Sq]»&ƒ1•öÃ#jrß/'+å䪔ºK[§Þ@HÇ7Ùi¶½Wp†3ÿ6Ý,~E¯é{(¸"õ(c)Y#�ŸFuaä÷±X”âÌŸOÚ�/â,HËÂÑ}©?l6‰–‹Ìë¼ÅàįOÑ¡óK¬É­Ä:iÌâzLZGN9ëYâ¾æÞÖ}yggÁë#ñSú×½ƒ™±fû� e!öË»iyûûìÎ/« -ò0KÓN‘2ûvÜ/%Ÿ¼ßy¢ak&¬Ï·c]¯÷Å‘î3Ž’™¾†1uÁb÷òÈ"Ë¿_*Såc–]¼ÑÁGG�ܨZ½­{?Ì›Kòoçl4ÏúÜzï½ôŠv§2&Ïë„‚M¼ÔØ›Þ_¦b\¶ŽÕ i7”û·%PS¨œŸÒ†©Û/à ÚZ‹ ¯—ŒÅÒª†?žîUð�hJÏç“düËüK­KQ㢔¡fFÜ7€¦ÞÚÅíÊ«ÄræÞ}a1 ž{†¶.Y|7ЛÈ'W‘_lË‘÷éìÝš©;Í(•ËÞïGsk“P|<<𠌤U¿×øë”B™w1­G8~UÜpquÇ+ &Íz<È{kR°àÍ­/ŒÂã‹ødYAÏI–½<™íÝ^bd´ ¥rÙýõÃtªå‡—o Úßÿ9Û³ÏßîÌ®éÜäI›Wwc>§þ‡øõíGèÓ¦ի߯¬4£ô|�‹Å—S³¹ÝËgçÂÅ‹A?ä ”AÚê·Þ)šêU=©Z½]ùŠ˜œ ¥©r×xîÊx‹—æ$–¾/qî8ù7ûÀů_^œüû†·ÃÇ«u»>Å܃ygÇMò¼< 1AU«R½a&,K(x\\ß^0�ü£Ì¶Ñ^x‡´`ð;ëH+g52þæÍ¡D�^{Î?;RYúX#ªøgþ™ÒÒ(¿ÎͳÂãNY®Ò¥2U^0¦ÎpêdÞŠ!áøa¶ÿ1‹ç:å2{Ø t{åŸK¼ü`ãÅãÇ1 ¶ãå*í9Îï¬'‰ËÚ±àÓþaÆ?ØoÊ( ¯@a\†—õLt¡“‘…¸™�0â“LzN[¦˜Í¬øzdVÞ�|‘�AFFI|5À¥ JD[î|y.vïaÃ÷PeÑS<ÿ]Ò…¿¬87cäÈF¬þp{Í¥­SN_Wd¨@]ËëãäRƙ±nŸ°vë2^ _ÎÃÃÞ!¦@¡, ÌuŸšïfΦÍüü�_�/ŽY.˜î9e(73ûþ7‚ø1fñV6~Õ—oáéiç·µ%•¿?†[Ú à­-'Ëè‹|öθ›Q 3qÑJÿ’Sn�+œge;e‘Å>KeªôË”Nž¾øù݆þ£g²ôãî{ýq>Üc»oäô¾{ª'�üñô­Å��~Ǿ<°ì{ƒ<[ðÊ®ÂûK Xÿl~·GFÞoŒ åáßm7Àäbþ }i\wOêôÿˆ}–²Ó¾¬TÛ¿|”nõªã]5�¨.óéÖÌ¢oøFÊ*ÞÖŽZÕ¼ð¬@D£yü§ã›Þÿ›ÿGœa[Ù¼‡7ÚzâêêŠWÏ�I0  v1î¸�:�>øG¶ç¾O¶‘U˜xÞa~w-Â}©êS�z?a¿À±¯†èMÕjµhß'l;/úµptï~ ê÷ãþ^Í©Iƒv}¹»gPœXü,]šÔ$л U|ÃhÚû¾ÝYì~¯3øilš„øPÕ?’vÃÞfuªq6ùRêRœyëšùbnÎ…Úɪ f="«QÕ»:Ñ7?Áœýy™W.-øמýhé|q]j>´‡C¦–Üy_7EFP«Ñ� Ú™�¢½EÃÙÓ/oÂêudĤ‘Ü�·�-Ì Uå¦þ�ÈX¼€­çÝä¦H[þ jô¥O�φÙßqü¦1LìWŸ°èÞ¼4¶;'æ|ÍêÂécu‚ÅÏ�æïÞñbëâ_>Îð÷k�ðMäÛ,þâiúµ®M@U׳Oä¨tÖ¿w7í"ýñò¦ñ OÙc¶~d9ô·†×áÞùiÖþ,c'OrÊÝw\\\pqqÆÑÖ¦�6ôl_�_OÜœLàP�ZggªË·:A½úÒ|ßB–.ûÞ±Bù+¥FÄ( wuræ2È¿¶š�\vÎ|ˆNµðªâKpƒ{ø>ÑÖù*‹•/ÝHíj^xWo@ÏÑ?°¯Ô›ôÊïƒS¿Í|ËmŒ{®u#3xò´Ü÷ ³·š1ðõŒ˜ø(íjÖ õC¸/|-³~>‚¥Üt©?ÞM­ˆ~|vÄ(?ó.~øv;��ÂðfÔê<šqƒYúƒé=ÝJ½j�¿óFýÏ“—¦ %¨Œ£yu>ß…óâj’––ÆØ±cyà�Î[ÆŽKZZš]Êu…ïÓè}·V�aÉoq䲿¥Ûµª“ßÏÁ•ã ûíüðDÞL·X¹*ÑzB¶`åÊdZtëDÕsöø3lœÜŸ{~ñæ±¹[9°gs¦ÜJ SÙi_èPVD|þûéKžáöç¶ÒhÊ2¶ïXÁ›mö0þ¶'YpÂ@™÷ñ¿¡wð¿Ì¾|´z7v,âA¿þ9�iýY"?LQ<³2…ôôt’ÜGuóNÞz/?º?ÂOÛ÷±öÝ–ìw/olÊG©Ólœr;÷/ªÊ#ßýÍî«ùvâ-„ë …ŽïMã˜ÿÏnv,ŸL½¿Ÿgäû;Ͻ¼¥L4ìÞ�ê_åîg¿ä¯Øœbßj ²�la›Û]ÌÙ²‡]ëfóXõ•<Ñ÷ ž0P*—u“î`䲞š·™�}Bß“Ótï±”QM¡¬×Žly«ûE´“)´oÿº…=[æñXÕ…<öô,âÊÍ˶`ó¶34lÖ‡Â÷ Ôoê`�¹shy 7»ÿÊ C'2gsgÊ él™=�íî-hUÛ„R ÷M©�´•-I%ï‡Ë'fS ®�›RCW(K»veÚ Ûl�[t#"sv³3Öl_¿Žã…Ý·3ý¹–¸/÷é5Ìœ}˜¼Íià�OHSúMø•økÿ%ÿð(w¾Ë�ÓVó'3†Gãf+·æAÓÖ­iè åŒ#ˉdÒÎlgö˯2ý»?9”uî%Pó®W¹Á·µ;Oàè­o2¶“»õ³üòÆ­¿Æ4©¾�MÛr/j_+9Sf[#íg&Œ]K½·6—|„Ms_ä¦j¶ËÞèøuz–ÙkcØôóÓ.{˜Á¯ü}^_–ßg8¼sùµådËÛ·> صë$ù{cØK]Úú]é‘4ªïľ˜=”›®ç ´iÝ„pO((/�ܽìO¼û47Õ©Nµà‚<õbyœ›&èŽÖûsœt ¶}Ç·»›ñøËƒ©ïïK­^cx y,¿ÿ~ó™5|õu"Ý^|›mjIãè •4ÜC¢¨@H£Ûyø¶pnÜD†qn}<»¼Éò_^ ÉÑܤ;Ä�{³‹>×Ý«Z�àÚmñî[ 1ý‹N`œ^ìÙIt{á †5'¨vgžzk‘ë¾â§ƒæRëR| •Áöï‚‹h'½j8ѵ‚oŽ#oÁ}óz¶ç—Þnçô‘%‰„TOœÏÛ¡JÝÑ”B ȧ|ÇȰM¼rk=ê¶½›×—+vÙ1�?F7¦FD(�átxq3Q�>EO[°S­:ÕH&!¹d0v†ä¤SøU¯f=±Ùd倻‡›íD«ÀÝ7²ÉÊ6P¹xó¥�tå ›ÕSJaŽßÉîS5è=ù;6IàÀâ‘8~}£¾9ŽÅHcÙ÷¿á:x"/õªGHP$mz´#ÌÁZ?üº1éû¯x²•ªœq¤Gôgâ+Ãiä”Ī·‡ÐªÍýüXì� =z,kãØ¾èEÂÝÍ�öbVªüq«Jó'°š…äÄÔRnì>ŒöËÙ~²ýßÁ§ m?HºÅ�€º5ñ+ ú5wtéAóÈj´¾›×žnGìÜظëm:ùxâé鉧_?f&g•Ó²³rÀÝ·Â7<Ü çT–ìlrœ=p3�-»›‡YÙä_ o=Ú<ìïÆÓÅŒrÒÉËÍ"ÛpÇÝ]+ö™;dŸ"Ûp¤þýŸòÃÛÕJkÇâ¯-ÄÏ~‰OÝždr?Ûø+õXg”Sç4ßDOO<=«Òà¹u%î+åYf>²Èb¿ÅÇLJѣG^%_—µT¦+Œ™“HH?o´¬23ùyXuÜÝÝqw¯B«©»±œL'Ýp Éí]?�%ÉfŽü2Ÿ=-o§Oð¹EV§â‰;U�îç¾_nÚå±ÌN(ù¾%…¸ƒàa8¾g ¥Fˆ"1>‰üøXÜjQ'¨ØMê»O…Rg¥ÎÍÜxœ¤3kÓÀ||ë1fm™ié˜OÅŸåOx˜k‰²ZgwÎ5¼|¼!7‡œónúu °Ýý¼>gûwÌå—ù<Øë ~I7J”M¡œjR'Ì 1> sÖq²ý‰w+ú\©A˜–D|¢¥ÔºœWwÛç(…¹Bí¤Ð¼|ð*È!§ ôv;w1PJ‡¢“•†¦�Ål>÷[¾ÅŒM·¾v¿™Çß[À–ýÿðIŸL>Ü›ñksmcÁ‰¶/-gýú¿Ù¼y#+¾yç�úÐߌ(44 ŒóÚÛÀb(tÍV—“\VNQPbdg‘‹î±ß¾ÊO5Ÿâɶngûµ0­¼ÓäiU«�·‹U çþîÎløsy–TS ª‡c*>†ÕÙ{ö £Ìq¤Ð‚n`à°ûxì…wø~õž¬2�—?ÞrÎSºº‹7‘�åÕ‘uÙöÃ|ö›Ë·–Â>,F)Øêx¶?54T±¾*Öß½xãû©³öqZ×nÆíãpèLiû+T ÃãD"'‚‡óÅšõ¬_¿žõ«§ÑÇǽœ>Ь�ON6Ù…OòÚ‚,7w4wÜó²É)º÷Í ;+Gw7ÊíÛ3å¤ãèê�»–Cv¶¥Øg9àá‰{y³MpîÌWî:¦½s”þ/ ±n;Fœ{ü(»ÎUé0a…µíÖ¯åç'��m.-ÏÂú•vì”E;/>>><óÌ3Ô©S‡gžyæ‚�˜Rÿ©`L‘´àsžlHÏ›C1yT£š§C禒““c]rO“½á9ê™À¡Ñ`†ÖYÏ?¯bîÜtÖŸ±šGn)>š]âýòÓ.·”¥u„­üç¼§ù¨süðѳ³&Ç8 �Áþ˜|üð>�Hb†Q¢3mÿ.~@ÔtLä“—v]Í?·®|°/•'NpâÄ ÒÒÓØñÚ 8ºU£šk*GŽe—R^JÌ$ªŒ§ìl‹c`îåZe¬bÅöüó/-˜ˆK¿_t÷@ÝS9|4»è$iŽ=Ä1@p€Vj]JÖ½ðµB¡U¤� ÛÆ¶mYy�í#ü¼3IM-|ÚÒ�€ _NîÛKr±ŸÊÈÝ·›#PÝïܧ;q §ËS¹»V<+W°"€³§þÕªZ‡–½Ÿæ¡›4¶ýµ…e½¼wBùáï[òIQg|ýÜIO9a=AkÕˆ®çMÜÎÝdÙ·œ];8ìEý ,V-YCÜ¢‡¨åé�‡G÷.ÌaÛäÖ4}q#æjÁT'ž#Ç ëUÀ™3fœ\œÑ4?ªèÄ<\ì²\)ãW)(w[œ"iXǙɩ%.wî(0.0n”BYN�’~~>E³FÅÇnñ™ÜÜpÉ9AÚéÂ|Š_ÆÔðmý Óo%fÁC8̹Ÿ‘Ÿ±¶í9û±…ÔC‡Èò&ÐŇˆ¨h¢££‰®Š—C9}ìD�zQ8ˆa�-È3Rw±3¹:õ¢=q¨]Ÿºj/1{m}`>HÌî|ê4¨‹©¼¾ >wöÖT^:Îu¨_ã$»w%Ú‚º‘¦2úªØ¾Uø:?f KïæÝξxxxPµÃ›ì=5—!A·ñMzñ}ÇTN�«àT‡¨èh¢££ˆ¬æZnžå;e‘å*X|||xúé§/*SêÆò2SIJ<Α]kùñ�atùác¦1*Ú®�¸k óÆ�á›�ILIâèÎMìM³UÚT‡!÷´äŸ×îcÆñ[¸·OµóouíÈ]wø°dâ|²f?Çޱ{ë~Ò\.�ö%8“‘ÄñãÇmK)Yîô¸»/ê‡ L\°“øø=,™2ޝÏôbXO?œšô¥OÐZÞy~›âSˆ�YͦØ2¦åèœÄª«8œÇîí‡9Óøv–7Ç|Κý ¤¦Ä³wÓn’ Àµ=ƒnófÙ”Ñ|±ö ‰±ìÙv�ô‹®ž…Ë>äóÅÙ}$–؃›YøÑ�ì0ER7Üz¸·�eëö¿—åoNäÛ“�¸£»?šK{† ®ÆoSÇ2{s, ÿä½±38|Ã0DšJ­KθºBâ®í$œ9·�Ži§‹h·¬âÉ;Ô¡Q={¶ïÁzû±#-ÝI�Mo0òåŸÙ¸{ÛV|Ì/ÎCï5”îÞù1syÿëålÞwŒØØýl˜û‹»R«N…q|~v:ii)$ÛÍÚï&ðÖ¯ùÔkÕ7 oÏú4 aõ’»—õGqrg ÖûÌ�h=øªÿñ6//ÜCܾ_™úö2¼ÞE7o†þx‚¬¬,Û’Äç·ºÓdü¶Li…“wܔǼ·§³19›”�Ó™¾Ì�^}Ûà¤yÓcD_´9/0æÛ8šœÂñ¸d² ãòÔåLz/ïoÊ-g�fÏŠ…¬ÛKÂñClúq2ï,7Ñ®Sœ°pø·Ù,Ùt�ø„Xv­˜Î¸�÷P»Wwj™À±I9ãP™»ˆ9^“F Ü.ؽQmh麚OÞYÆÞø$b“‹=Àr”Mëö‘œ•‡SPCêú+²N>dbædbÉ© ìýí]{sÑÃÓì¼°ËéWðì:”>¦Ÿyõ­Œ�aîäø'j(ƒš8¢õfh羚òŽcÓçSøâX[îê_ÓÒÍÙ8�wMá�tU~:õ8¸;>œÄìm±^ý¯Î- ×°žøjìþb$ƒŸ�wö! ²v“–SØ~2«hþ#C½Íç¤S^�…•£ò‚1Í/oGÖ>Û˜°ðZ4¾i8¯®teðÌu,ß<éòú|>èx˜×û7£fX-ZöÍO ŸÚÑ ¹s$½ÌÉxyˆnUJËÈ“›^›Ï'=RxÿŽÔªÕ„îÎbWþ…Ò.ß¹1@>kÆÝ@tTQQQDEÕãæ×·âÞãMæ¼Ò˜mãzФIWƬ¯Ë„9ïÐÇ”K^œõ�Oc`³z´ô2NNŽÖ™«³™¡LMx`âp\º‹õ[2àùÒ›0zögôÏý‚û;5¤Nt+ú>9‹˜l…Rîtzù¦ßœÂÿîjKƒ­éóè7ìÊ+ýÞ¬¢|Š–3dßÎÏ/� [ËF4jÓ—çW0òË�¸7\³Î,äíãÛǻѬQGYäèY0¨º†R®´÷=ÿë–ÄÛšÓ°ýƒÌ«òß~2‚0­Œº˜Ý¸éÞ‘DnÃcß$=%©”ªX;{­Êj·s~2Á�Ž·´%eù2vÛÍwjösfŽ Êo/пc[ºÝÿIßäÇ÷úà‹Á©ô£lùúÝØŒF�Û3èµ=Ôá[Þêï¸àèÁ†çšP³F¶º…§ï¡îÓßñÍ£Qè*—õKVáØåZ:–üf¥Ø¥ wÿÊÒxëe'çÏñÕÍÙúlgšv|‚uÑS˜5¾®e|+;Û§ÜñîŒôø�Á Ãi0ôŸŸÅ+]=P ¼{¾Å�ï´'ö�;hU‡Æ}>#­~"Ü4,§Ž°yýzvÄå`”5ŽN§±wñû<|K 4¼�Û§l¥îsßóÁÕ@�!aó÷LÒ‘Æõ›ÑíÑ1Ý5“ÙO5´Î|9–7n ÒW,áïànô¨k*ãÛg±W¹…Iÿ»÷Ò¡a o|›#u›RË ,)øèñî4 «Nhãá, z’)÷ÖC×üˆnÀ?ÏÝ@Tít~l>î÷~÷O68÷²šm)¯ð¼™W¾zŽà_¤m³îL:r|ñ8õJ äÎ÷¾à^íKîlÙ‚þ3N3øóé Õ.�®Avìv6¬ßÌ‘LãéèÔù3z%óz¯f´¹ûg¼Ÿ˜É·zƒÊ'i×zÖmÜOÚy?ˆ\r/±XG“íõ¹é”[çrgJÏóüã©,²\{KeÓ222.:—ßÿ�þýûWfyìnÞ¼yôèÑøș™rå“z4–|w?ª8[Hß3—çïù˜ðÏ6ðjÛ‹ü�»0ˆ�Ñ‹6 ûò÷¢‘Å~Ò¡²Tr;e¯à‰ÇüÖ:¦w÷ªÔÇï�„oÜáCþ´Šq¥Í$¨~Ö–·#¿cÕ¤–¸TbY®JæýLëÙ�߬dáÃ5Îû6X°v,-5ññßSi#1×>s ¯tìKÜø=|Ôãj>æ Q>]×YºtéEÇ@óæÍ£k×®�¾ü¡ðR\¶HØËâñC˜ºòi¹àÑœ[žþœçÚ8]‘HûÒ©óo¶¯L•ÝNî72zlºLx›¿;N¢UeE@ê$Ë^y�ÝÝßàÃFe”ÝŸ~/>Êg½ÇñÉàE¡ èüÂr2+{šFóæ¦~�qX»€å©Å3³ptñ/lîM¿fNø´½ŸçîiEÕË©(òóÍ%kw˨ÎÕ/ßàö¼•÷6neëÖ­lš}?5]Ú2þ�-lݺ•ÍKŸ¥¥[múŽ~š[k:^®íêÌ©“˜z~ÀÖÝ;øgå\Þº¯>qŸ£ûÝ39p1VAø®¢ßÌãcï¤�ë%Y!®i•Œþ öE/€B¡”AöÑ-lóÁ‚ÝG8¼}>�V™ÏÃ�}I¬EaIßËÆ˜PF¯ÞËá½kùr˜ÆwwÝÊó«²miªXþ½�R LÑ<ûW&999œ\ò AZñrXHMIÃTç>^yÐ…¯ßü�DC¡ vðñ;khõìzæ’œbÍKº…) 6s8!–�ÓZ°sÒ(ÞÛ^€RÄoÛÈ韲õà!v,šD“]ès×g±(”%™�âiöêßì?¼‹?ÞëAêôÛðÆvòÕÙ™;¥ʒ®�ñ4}�ìcÍôŽ™z“W�A©Ól˜Ô—¡ß»2rÎ6ŽÛÉìÛ56m>Ê銶ÿ%ÌZyuÀÍ®X°,éìì”å‹ÙIxï¾4r4ˆû´/5û}z6è-ØÊôÛ[Q'4”ZÍ{ñäW;l÷�å³ùÃèÖ2šˆ�"=ÈO' Ò>E»úµ #ªÝ ^ù½X^ä³a\ ªö ósøyDM:¾¶ë….EúÆ<Ü£)‘¡aDµȸŸp€<Ö½9€NÍ£¨LHdSº?:“9%*©¹à]=ˆ   ªû¹ã 9S5ÀöºšŽòLÃFŒ^U(Ò–Ž£wû&Ô !8,šwOæ�ñÃéÚ´aáQ´ò&k ƒruŠm_=Eÿvõ©EÛASø-Áz£ q|Ï÷ï@ãZ¡‡Ö¥ëë[®Ø¥Wݹ ¾YF IDAT>~†EÓvÀh¾øa<õ6MeâÜDkÛ—Snuâî ˜›ÞÚ�™<ÖLìL£:á„„Ö¡eßç˜ÄZóö©t¨y/ósÏ/‡‘ò'¯mOtx!µsË[›¯X!Ä•r•ÎŒYinþ„†T'¸v‡ÿ³wßQQ\m‡³KYzï¨ VÄÞ{7*öÞP£ILbÔ˜D»ÑDc“/š¢&jbì »ÆÞ{E°ƒ…"‚ôΖùþ+«Üçœ9vÚ;;³wÞ¹÷Î �è€ù™£\È)‰V¸y•Ä­T%Z}ú3}£Yºhy”çy,XBi¤B¥Raløxc–ŽøûñXØ:RóÃO©|^T“¸s “ûòyoì­ 6:&ë¤dâNEowì¬ðî:”Nî7¹ü ú@BåXOww¼êôá‡?Fàvp!«o䜴 °-QšnžTë8‰…j¸ø_Nåy¶1ÀÚµ$®®¥¨Úù3׌æä‰[h3±èï;´œ2—a�¼pq.‰‡³Ùëk.3oJ¯֜ܰ�»Ù’6x ›®T {÷Jy?UX² ê�¹l=v”õ“*sqÚ`&ïIDFÍíS‡‰o:›ç.plÃZÚ)°®7œ?wœàâÅCüÖ:†¿ÆüÌ‘œZN ©3õ(·îÜáÎ�ßñ5ztUºðŒ<—{­f³ãÄQV}^ŠÃcñÍÑT@CdÐ92ZÍáà™3]÷%eOMá³ù—yþû&dRC/hÖ�å'.pvÏl„,à÷ÀªL]}�»¢q诌› 2q;Æ3xFõfîàôñ¥ R®fĸõDÉ »ÄásŽ ÛtŠ€³{Xø®7EU÷¦,Ù�¾�µÝyŒäâ�ìû±ôÆîܹÉŸVÄ#|ýΦ#_B¥?_ÎØFþýô5\˜?�?Óz³úÜu®ŸÞœ¾åÅkCAxëèu2–›ÂÆku )yõë•,(SÞ…Ôð0âe$"Ï×J§&!!K+ \z2ºO2‹çüÃ_s6ãñápšZ`c­$!.2ñ'ghsoÜí¬°)Ñ�EaZtº¼×lP¦^R¡yµá(p*WËÈPî” (l±·‘HINE—Jh‚åËYQ%õzwÅõÌZ6ÜÔ‚Ö­'¸zºy=¥×–A·¯KY÷Té2‰1íÒØ¶îY Â*{wÜìp)통J‡2x—°ÃʦÍü:S!þ7br¾C …�ÆÆÆ>v@ëݼ‚ÃÎï2ýÓ”uu§j÷¯×>�u+?XŸ‘�3.NΔ¬Öƒº”"øô¹nâ•Llpv´Ã©lú·÷DkêŽOW\ʶ¡oÛ„_ºB².žÝ+¶£è>–ÑMJ`ëR‹!Ã;`zl§Ò³¤°ÂÕÃ;wJ9ª^,¨¢ÂÁÑm\ ‰ÚBÄ�C•1ÆÆFH€„�gE<¬°v«Ëà^µÈ ¾ND¾Çº„©…šÈk\ŠHÇкeÜÌß‚~y‚ �zc’1 ŸB8ƒˆ;Q;:c%I˜š›!ÇF“gΣDI&™O9ÛÊIÄ'Ș™›"aB£�ßÇmíH&žoɈ�^(‘053!)! YÈÜ¿$ ö¯œ‹'öÖzÞó|z·qmøÂqÄÅ1¯¯]&!,Œd{g ìyþ0Ý”Ìì°3Ž!ünz¾s¼J†Uûлü%V­D�q’UëïѨoÜ utãæî@Fô=óÜ%i\[3ž^MªSÞË‹J]~å²FGáÞî£%2"Ü=pP�bL‰RN¤ß½›GÂ¥ÀÆÎ)-•´—ÖßN�…•%¤¥e7�J˜Y˜AZ:éºûDÜËàî’>x•(A‰%(Ý{ ÷2ˆOÖ·û2ÓˆŠL@ic‡%Ï·:”ßúѪfEÊ”®@“iÇQkµä¿•”ÿð~ë”ÊŸ½kSË÷Sþ:'îXá­óF%c�Ð\gßæ\½Íùu˜¶Ñ˜ÎýšaŠ�zõp:³ˆÖ�çöݻܹσ 5ÃRx¹G°oý>nDÜ!ðÜ�G“9™¤T05˺3NYv_¾ß ß±ŸÑÁ6ë ßÄLER|":YGV%˜Lfzjù±„QGÔ©�ì»x‹°ëøõË_¹Tµ7]Ë+¬+p÷œ ãæ‰óýÜ{ö¦Î³´E™4¡gc6MûŠÕ‰¼u†Cç"_ ™í9(½è5°>w×­`ÏŽ•lÎl‡_{‡BÖ`¤r+$ •³ VyÌ ‹XÅø/÷áö…?g¯sqãH|$V ”2™™O9A+qvuB½I؃ ƒÐ[÷P9;ç¹>$^$�„ü`±Y¯è‘‘%;í�ðø`!¡¡„††Fø­ôwЯúÍ­µ,;l@ƒ6õ1W·Òef&ê\_cÒŽéŒZeÀÀeǹr™ƒS®ÉUå…ïW Ù}z/?Ô¼ÌÌ¡ßpàñ'΂ ¼áÞÜdLNàôÜÔ-W�fŸ� â·kù©}VS�qã‰,œàÅá‘�)Wʓ꟟À¥–7ö À &ÏŠéêžø”®ŒïçþÜÈ�¹èRHN“ìd Éž?lcÍgµ0@ÂÔÌubiUøø§±¸ýçGg+¬\|ù'½<^ö9U[2š;óNeÊTëÉ‚Œ~,]6ŠŠj¾tDí�F×e©Ôþ[î´˜ÇÚ©õy¦Æ(ɆŽ?­âëŠçߪ<^µý˜� Fƽ¶ó¹玃i—éϘ‰Û°ì9ˆfæùL®K$òN$÷ï…ptѾÝeKÏ~�Éóf:Y‡N– ™éhd EÎv)l)YÒŒ{¶p6,’Ûƒ{äD­ DÇ~4¾÷7“ÿ·�ëa¬ŸÂÌí–tï×(ïõ½N’ ­z¶$yÅ4¾ß@hÔ}"op+¹ÈktiqDEÝ#âV‡×ÌâÝÞÓ¸\ãK¦ôtAQ@ÜJwÜÒO²uû5îF\çÂõXd�ŒŒºL2ÒÕèž–ªK*LUñ\ ¼EŠNGÌ¥„Å“*[âY±¦©I$Šü‚ ¼eô§/¬A-¾ Lzð¯ÇÈý$ŒÌ5ºæ7\LÈú[`X‹1;×ÒÛ,�eIv4ùj¾ÊkE Ü;ÿ�ƒ�ÿ÷”8ª2å\b~�Ró›‹<˜¢ù$6\˜”Çtj@‰k÷ß9=­ÆS:²[ÒbÆæµz¬×9ŠG·ß¨5óÃÃr�7§ïúúfÿ—šá@‡Yü»‘§YðÑ”õkb™ÏV¼t–-ÒÛ�u:0j`Õ§Öz(¬ËP­Üzþ×¶Ó4†Øy5¤×ÜeŒm` <~#(\{2iü ¾œÜ†ÊÃ2P˜Za_ªŽÆ`L“O&ÑnØtzÖýC·w˜ºúçG’,…[_æü�ÈÄ)ŸÑö1”¨KÏ–0±�Yžë{½$:ÿÈ¿qS™:½ B“�,KÒdì¿[¶È¢2±¶A»êSjm“00µÁ­lMš¿·„ï5ÁͰต¥z3Œþ¢µÔÖT:Ÿ-cÇ2ýÀgüØ¥:_%ƒ±…î�[cúxNfX›~ÔaØìO˜ßh#�·Mçƒyç¹—*aáVßé?Ð&¿D_á $ÅÅÅú"|÷îÝtíÚµPÓúûûÓ­[·ç,?š3©Þâ #ž’Œé…dVtsc¦Ï>Îæ•Œeîb˜ç»Hÿ„䑌= g¾oKߟN—Ž¡}9ôüŠŸfö£âsTý¬_¿žæÍ›£Õ¾uïa*v”J%çÏŸ§ZµjÅvæ|Íš5+êPA(FüýýiÕªU¡§¥5cÏóܪÂPÖ˜N@<�«Žþ1£Ïºxú@ÖóÍmØŠya¡d�~‘�PRcÌ.®Žyüs}þnAAÈñæöAAx ˆdLA¡éO~Aoh4šbÛÇèm’Óü]œ÷ç«ê*!‚ð2‰dLx‚V«E£ÉëU›HìOAýöÊ’1…B´€¾‰9zôhQ‡!¼DbfÝÙ$‚ð¢ ûD‰g%jÆ„G4lذ¨CA½ó*/ê^Yõ•®p/AA(ÖD[¢ ‚ Bɘ ‚ Bɘ ‚ Bɘ ‚ Bɘ ‚ Bɘ ‚ Bɘ ‚ Bɘ ‚ Bɘ ‚ B¯CA„·L`` ñññDGGu(ÏÍÁÁ[[[¼½½‹:”WNo’±×yàvëcL‚ ‚�ŸÀÀ@´Z-•+W.êP±wï^ºwïþLóœ9s†   *UªôŠ¢Òz‘Œ¢Ñhhܸñk[çÙ³góÝÁú“ ‚ $>>ž¦M›uy’eù™¦¯Q£‡zEÑè½è3OµjÕ�eùµ Õ«W'..î�ŠIA ò&7MæåmÛž¼èEÍXttô3gË/k½ù�Ó·˜AáMVçÕ7�^$cÏE“ÄÝ[w¸—¨ÆÐ‘ž®XõÖÈ)D†D�¨•‘”–¸–vÆL*â˜AAO䕌mܸ€Î�;¿îpôFQ§/�(LÆ,§…rxÕB–l:É�d YsH(ÍݨÕÞ�¡ýšáaZp$I…Ë’ò�ICrl<é:™Ô«Ë™úÍV´€Ò߉SéWÞ @¡ÂÚÖ<ß/¼°1 ‚ Å‘ŽÄÛÖ–¥ZisÞÄ3ÆãçÔM›6=HÆ:uêôÈøâr^Ô›d,§ßT¾ÓÄ�æÏÉ3X-…G§”Ñ&‡qbõwœ âËiQßþŻÓæ2ËÇLbK´îág†*T†÷Ù3ëöd¬pèÀôyïã£7߸ Åš&ƒ Œ1eÒsБ‘®ÁXeôzW+Dznxs~op”ãã½Q¾Þµ¿¹Ï©›6mbÓ¦Mþ߸q#²,?‘�zÑ�(¸ƒ»ú&k¿ùöÑDLRb¤2Bù s–I Þ̬é+¹‘Qp‡ùŽIY‰÷æ­d劬ÈwXÉÊyïQIùâ1 ‚ðÓœabeK|EóJKƒÔõôw©Í·š§L “pvÃZTÄÙÂKgÊÕïËï2_eTE*iy7,Ë~ÁQuASj¸øm]\¬#ùuö²hÂÙ=Ë�†^¶˜¨¬p¯Ù“ïÜxœ%-£‹‰„$å &ôX•š=2�Kÿ|@ÃR–˜X¸S÷Ý?¹�òbá<žˆôùÛNo®‰òOFdbö,fEPê#”¤jÌ—K[pâÝ)ìL|8mÚÕÕüµ³ßú:¾P5nÁ ’LòÞoé7ûùÿ~ ©ûùr¦´z3«•AxËÈj2Õ<=áË8ÌÔÞŸq¸áÿX5·% â¸~ò:3J½¥ÉÌ@û†];Ë ØuÁž!d‰[<;'ùñéÀ©Ô½ö+Í�ANJ$ɸ+ÿ†/§‡)€„ÒÈ€ôãÓè5âMe¹w0?û ¤×Ärœý¹)fÏGö9µcÇŽtìØ±À銋7£fLñ}çHÍcßȲüd�"§sqß"u¯¸f,g9’» 5)c)�dMùÚå±Q€dY†šìPJϰ,AÒ98­U½œ°2UajãA½Áó8—]£ YÊ{ *âfc†ÊÔ†’5º1qC0h87¥ ÖÝW<¨½ÉÜý%Ü?dWNå–æ3ë¨000À´õ„çêm!Ç_áJ¤ÍúÑ´Ri<Ëפ�_êÛe]NªooaR·Ú”¶·ÀºT=Ï;GRNñ•q“ÍSzRÛÃS•6®å©7øo®k�”Uô´®Â¤³Ù5rº;ÌmnM³9·Èêz›À™ùТ’+ÖNx·ÏÖ0-Úà% ©_w;sT* œ+µgüÖ0´9Ag³a|gªºYbfá@¹®ó¸ªÒ.³bt;ª–´Á® Í>YÁÕŒ¾zÍæt«E9WÌT&X¹W§Çw¹Ÿ«ˆNß臵�F¥±?3ߨuwÖ0²EJÚš¡2s ÖÔøvÁq€?I9ßyÔb:ØTaò9 Ú è^Îk3S,ݪÑãû#ľàéA²kϬå?3´…^åñވθF_çF|Ö‚uñq$šÛãh®B¥R¡Rc¨Èäè¿K o=ŽoºûPÊ»3ÓÆ·çþò¿ÙŸöìq<ÏcŸòõؾ²ñl‡ӾfH o\,Ͱ.UŸ÷þ¹œõ»P‡°vt+Ê;˜¡2³Ç«Ã¯\Öæ¿ø×åÍHÆ4· ¾­ÎJº$#œ+Õ§aÆ4¬W;¥ eë4¤aÆ4¨ìб £½ÌõkHư¦Þ qô®fŒdZƒÞc{SÝD¸ZoÆ ¬‡uÖÒD2&Â3Ð~áír!8„ ]_SáØÞÿ_ Z@•“�%øüàe‚/a‘Ÿ‚•~�˜p¨�íGÊŠŒ=Orr2qÛ>À-×Ù@²oŠoÝ–ŽĬMAÄænÍTòcŸ�¬6ÁÆ NΩ˅qƒøî´Hãè”ÎôÿWKŸ‡¹|™#_yqùÄbu�ð8™˜M£è6éMæ寕�| ü—!#W)ƒœp�ÓA%sè*7¯Ÿäß^iüùÞWlŒ—�tNLëÊ»›l±æ×/eÕ7ðT¦rhR†(Ç´Ý׸±o2%w}Ì¿_'ßs°.ŠK'éùÝ)®‡\áÐܦܜ9„o=ÌâŒ;."29™ä¤küÜÄ0ߨu÷ØÚ‰Q®ršÕÖ¦YÛFèŽìåtö"“�ìá´]KZW2@éÖžo6œ&8ü6Ç®Mдá̽ø´&åç‘NÀ®ÄUoMcû¬[wÿ÷ÓÏóï”oøeÙ>‚“³ÏGºHc)Y¹–ÙM;f>Õ(“ÄÅ;Ϟɼôd,{_Õþñ7ï\eû 6Ì\‰4t)Ç®\bÇ(¶|6‘µ÷e2öÌ`äÆ 'îÞ%vüÔ O=éx÷f$cº 2tU0¡BÛw2dC6£¤�;� aÈ�!¼ÛÞóœvÀÌ Ò_KÍØƒ‰ÖÐåúãáŸ"áYIÙ¹áîâŠg­þŒêíÉõ㧈Ë)*V¸•.‰[)oZ}ú'3|£Yºh©ù.3gÑJ£¬ZcÃÇÎHÊr ÷?ÈÂÞ&ìø¬žešðþ/‡¸§Íùå, ªÅ虨ìä@¹N_ñaíÛìÜymú/ §õô?ù¬�7¥ÜJâånU¸îr,ÛÿÞˆ²ïTƵôÀÞ­�é†ÙÁË©�QXâ\Ê —i=zÍÒOqì²2²øŸ»´�ò ï5ðÂÕ½,Õ+¹c”~�%Ë£h;a:�Ë9àT¥?_ú•æüûˆ.°¸5ÀÚµ$®®¥¨Òy4ƒkDsâø­Iœ¤0̪A26Æ@*LìÖ¸—vÅÁ©¥�M±nÕ™&I;Ùz:HåÐÖ˜µëDm#ÀÄ�ŠÞîØY;àÝu(�ÜoqùZ��Ù IÍÍïÓ÷/¦-A…ì]¯,Ý“o¿Buã»ì›Õƒ*Õ±:Tr2I)`faþ a�Ì-0#™¤äg?g½ôd ,]qt,I½÷ýhd e«àáZŠzïõ§¾&ˆ€` sKLSnxåjSGÊ–sEõÌ[ðjèMŸ±|)l°µV@ŠäöÏþ€ý€¤jÊø%Í91âkv'=ºÃ$ô&ÕAxQ lìl‘RSH•É®qÏE² lygR�…/ÃÃÛšž�dáM÷I‹é>á.oœÁ‡v¤KÚav{‡q7ý£ËYðyÎ:t2î•cÑ&…žìLµrù%`O‰HMXD:a;|±ýëÁ‡h•-ˆMÊc•-¶fi¤¤Èȉa„&:RÕãÑLrR ìñsÁlàƒ`‘ªÄ«çÂÖŠ(l±·•HIIË;úg��ìÚÑ«ùgLÝpšÕðß©¢ãÒ¨�‰?õ_Œý�ÿ.†“bh†2IKsÝ˸XW¼t m¾gðúMŒ¨düpÝÑoH#>÷_7¨ÇäßFÐm†f�’”‚ެ99‰̱0×Ã^ÐÆ–X©ÒIÍé×dl‰¥a:ii`Øt*ë™Æ„‰�ñ[ ¿‰³™îW =Ø ½IÆòÍ€%©âcÅúðØ'i‘÷<ÞUðT檹zÙ1=¶þ‡M–ònY‚ �àé…Záw¢1vtÆJ’H23Cމ&F�_˜*”(É$#³e’Â’Š]§1uï :î>HtgÍÞaÜõ� ²ì,–�“É}BÃÓ¡¶ù“Ë20ÃÌ0Žè-Oœ~ö8;ã5ê?¾«Ëã· hÂ_˜„”Ýú ™;ádEÈ­d¨ý0M•Ìq´°§ÿÂ,ô5-x[ŸêÑÔÖ@©DÎÌ C&«5æ™c$;Úûù2æ‹Õì¬Ïë^ø×7m s‡}Å…Ö8¾­9®ÒIÆ×~‡àˆ>'öø}_Òõ³[ôß°ƒ‰ òI˜�ËQµ‚Š9‘QhUññ±åÎÅ ’ä*ØH�xž`óJT.ùìm|9çÁÍ›7³eË–<§éСC¾�ûó'!I¹Û¥rý/YReà�l0™K+?£×°~Xøœcz�¢O…ôªîèéÕ”FTñm�—Ñc‡Žö.gw�&ì±Ú[ÉЃ¶k z Í�…ë3 gýNåìƒM~øQÎxÑD)ÂK¥¹ÆþÍ'¹z› ë&2m£1�û6Ã%õëãtv?®»Àí»w¹s7ž½Ž =ðr�`ïú}܈¸CÐù$æ.‚’ޱø§eì9w•[wBÚ·ˆ…;ïãP¾<޵úзô!¾ût>®„u/”K'ƒ¸«LšÐ·›9›§�cÕù;ܽuŽ-û®äZo5ÖJeó/s8p-œ»áaÜÏ©Á�ìh7 -IÉTÿsÜŠŒ""øç‚“ ®Ý3i€^¶l›:Ї®q›K箣jÊ€žæøOÃÒ7¸É­ÀÓ\‰y‘òV‰ki”ç6³öBá×Ït׿¹b·ló.=¤U ÿb+%øQÝ�uÙù¶LfzjùaÒ$cj wÏžþŒÛ  dîç‹°û_Ö0&#=�ôô Ô:€4‚vús(èáa×9±r³¶+iÒ²&FÑÀ¯?®»f2É?ˆ;—73uæ6lû¦™É³{9ç»:СC‡'Æç|þ*Î�º°³ '>M�ƒ�7®†I$$êǹWo’±‚E©ÎŒàCîZQY}�måJîƒR2£bŸt÷R¾pòSø>c™$Åß'!Yºdc’HÕ�.9�ûñIdf-Lôáå’95wõÊW¤ÙçÇñþf-³Û[#Æ�&°p|�jLy�ÒÔøâε*â  jðÑ·C1]Ý‹Ê^UðýÜŸà\}±uqÑ\;üÃ}ëP¡l%ø�¨–ÿcÝŒ˜Õ`ܺ%ôH]ÀÀúe)éY•vÃ� `Iëï×ò}õsŒkQ¯š=™},áaB¢pgà/¿Ñ5y>ݪ•Æ£B&V¦š‡N½æ±aJ9ŽŽkKe�’Th؇i;Ã(°ï?´øn ÚF1§W-Ê”©Æ;,!(Ó‚–³60·I³ºÖ tÉ2Ôîòën¼Hgx »®“˜Zï“–¡|£ÁÌ=‹ãóÄ®jÈûƒKr?¹Ã–Ïzˆ«Ae>úi î;RÕÅkW_þI/‡—½0§õ°O(sôS>ü'ò™¢–ïãð¥�ñÁÜÄLL¬è´8YÃå�³Ò¼ž¥«Òaò*NÞÈŸý�‘U½)¬þ¥gFÕ£\í9ìók¾}öÇZÀ£çU___|}}ŸúÿË='Ê$üËè¶Þ¸ØØQú�…(†þ�ÏéÇãZ¤¸¸¸BoíîÝ»éÚµk¡¦õ÷÷¦ißyçtº~rr7ÿû�!"ãɰ%#'êú�ad‡2ôF$…BÁÿý÷Ô SÖsÆÞ�sºÀçŒÕ¹˜ -òÎXA1 ‚ äМ�D�W˜¾†ÞÏsV|M2Ö÷ÅiB)þ øŽºúqÞ{ëùûûÓ£G�¢ã k×®¥M›6O|žÓ\™WM™B¡`ÇŽzq^|Ö¼¦U«V…^vÑ7”f+\¬Âã�Ïù_�¶Ø}€Ó—o•¤ÆÀÜ�’jФUsª8‘Õ$Xðú^FLJ—*4knAþ×Yxº(½ãò9cA„·I^IXq£ɘ��:�I’ ® ì½iÙÇ›–yŒ+LB£P(ÐétØÙÙ½pLÆå;ðQùWY`l…‰I!‡A�éÄu3î¶‚ønE… /žµÒ¡¸TRèEŸ1[[[.]ºô ùyÕƒN§ãÒ¥KØÚÚ¾Q1 ‚ BAŠ:„§z–ç‹åœ‹C%…^ÔŒùøøpñâEΟ?OLLÌ+_Ÿ��¶¶¶øøø¼Q1 ‚ BAlmm9sæ 5kÖ,êPž I…¨—N§#((H$c¯SåÊ•‹:„'ècL‚ ‚�ooo‚‚‚8pàÑÑÑEÎ#víÚUèis*)*Uªô #Òz“Œ ‚ ‚ðr‡æm¢}ÆAAŠ+‘Œ ‚ ‚ !‘Œ ‚ ‚ !‘Œ ‚ ‚ !‘Œ ‚ ‚ !‘Œ ‚ ‚ !½x´ÅîÝ»‹:AAŠ¡gy¡÷«¢É@Ë–y½iR„·ÑÕ«W)_¾�/uÕcoËvBqµgÏž¢Í”‚ ‚ EJojƊ˛١¸“$éÁ‹€ßdoËv‚PôD͘ ‚ B5c‚ ¼voK�ÒÛ²BþäÌL´FFúsÂÞ:zsl‰MŠ�·%‰y[¶CxŠÌŒoàÇ‚{mY¼€¶F¯z…éœü¦ Ý~M ó_{øAQ8_ IDATµƒ=Ò«^¥ D3å[KÇí_[b©2£Ì胨_ñÚwM U�ò4œrêéë’ã8ý×Ú×(…½¥ÖÎe¨Ûy4+¯k^qtÂKŽáŸÎV¨Tª<« ‰¹Ð@͉¯*a¦2Å¥ÇRîêr�JÞËÈòf¨ÌÊ0úà«.©ž‘Oø­hRµùM¤áâ·õ°0Qaæ=†#i…[tže¦œÆíË׉I¸MÐ�xtù-@x«ˆš±·–Ìï4ëêýÕ}Ã2a9u) ÷6Ú§¬K&Ö]G®æ¾¡#kÔÃ&3‚Ë'n’n¡û¿˜)|�’®Õ[ÒZ§Ý=öpGª4­Š“ «¹aøJ�íü‰š±ÂÊ ôv$:tÄïø‰ßÏödZM#@Ç�e?²üޏOhX ²lUÔÁ>$óàØÊÚ×yL“¸‹_ –�[K™³q,õ{T£õ´2ÓšîóŽá9"�Òµ¼Pá±-¼^¢f¬ØÐreñZÔ¬@);¬¬ìñ¨Õ�I[CÑèÂñÿ¼-u½=p¶µÄʱ4uº}ɪË)Ys_ý‘F–&X6ù‰ëZÐL§¶… V­#T÷pÁÿk†¥‰ æ•Çsò‘‹\ —Ž'V§Äフß·“ÝG¹uý_ú:e[é7ðŸÐ“úå]°µ/EÍnÙ|+3{~™¸Sóù¤]UJØYaeïŠWÕFtùî0©d°eˆ¦¦.¼·-Hc]?LÌ<ødOf�ËVŸþ™î�«Q®¤3¶VÖ8•i@ÿrÿA)˜ÆµõèY·4V–Ø{ÔåÓÍ÷‘‘‰=þ+´©F)[œ+4çý?N/JÏ—È’VSÖ±iÓ&6­ûІ†€a}¾X½‰MÓ㊮fNô[€öÚl[š`Qç2ó?¦4»ùί>îvغU¦Ýèe\JyJ(ÂóÓEqO� Èš+,ž³�8È8ͼß’,²–{‘Ñh�ôC_Ó¬R ¬,°rð¤v·Él ˪žRŸþ=šT§¬›V–¶¸UjÁÐÿä^víUAã‘“X2šŽµ½p²uÄ«~?¾ßw/»J&éÂ_|ؼ<ÎÖV8WÃÞ|kºd"ýç³þ®Õúõ£Š2žÿæ-#8wMš:Œ�3ýhTÎk Ü« dÉÍœBóñ2S͡ɭhÚ¬ÃÖ$eM’ÌÆ‰Ý©ía�•­•Û�⟀¤¬$íA¹í‰«ƒ5V¶îTi7šW Y='è ½IÆr®0Åð‡‡_.²¬å^À!N^ C*ÝœvM=Ñ\ÙÁì!Ÿ²"R‡¬‹ãÒá£ÜÎĽ‘/­¼ Þ1—¡G±é¾.×ÕÿÃeË�í;P`Y©=f`·ZØK¹ãQâUÅsIË­ÅÃèýål ˆF63˪Ù�Ø;®+ƒ~ÞN¸}Sº¶páÞΟÔÿ'Õ2šëóèßñ3¼‡]�whWÝš¸ëg8pê™�\®>ºír!–­ =þ3׈RU¤UÛú8']`ýÔALü/ YÖ½i¾fë ¼[¶§iE\œ­ÐÞX€_—/YzVƒO‡ŽTS\`Ù=½)]Qïû·tx¤¼À†V�š`N2·$QÖ±w5”õí@EEÇtúY¾ëÑ‹ië/c\» íʦptþ‡ôžzˆT=ØÖ·jÐÞ'*F†•©WÓ„˜Í¿±ì¦†¸í¿óï #jÕ¯†‘¤#6*­,£43ǾdÚvïAK�L.ï˜Íûã7/ËhCO³÷ôU¢Í}x§]œãO±b|7zξHf�ãuD®Nç�æs(Þƒ6]êc²‘©}ßgñm-râÆ÷ú”NÄb_·-M=MÐ<,LžÜ.Í5–.ÜG²ª'N`p}§ó÷ÙŒìi’84±#=§¯ãb¦'M|[SÕÙG)ûxΫÌ|¬ìúª3~?m禪2MjÛ}pwÆêm®r;×úíhémÄíóþÑ|®iô`¿¿ƒ¾Ð›fJµZÏú ¼ñth´Yš¬Ó¢V«ÑêdÀ�Z-`IÏ4–ôðaäÞ>›Jïæjt2`P�³óqÉhVùÕåÃ-ëYà?…VõuY ެE£V£Öd_úåü¯• û–£ùiz tjÔ¹:=Øõú…å‘*ÆÍÝÂŽ9Ÿ±}îJ4ÁÜùãhn¼•ùË‚ÑÚtæ›çÐÎ<†ï6`Ì¡õlÎ;ëp(AÂóÃåìŸÕÃ#_R½ã<¢å¬mÓè²¶U§Q£VëÐf¯W§U£¾Ÿß²G1J«$,ZŒeÑœfÜú¹ ¦pì`iÍY5w-á²ÝþØÏÂ.9ÍjÎ3Ÿƒ J*�YÀÂeÑ�µ¡U÷l]³�Äv�0}åûøÍ¤P(Ðh4Ïþ›Wk³k/th5jÔj°lÝ“–ÖÛÙ´s=»cª³õ$™ÊòtîXY}5ßcºY©¿Xx!UÃÉü3¿?N©»Q4~ÿµŸZ—F†¯h;Š£Ìh¢cuHÆåøI%B†®å¯?ýÑœÝD¬­/ß½kÄØˆ�Ž"S­ÆÈgË7Žm&éw—ЫÆç¹t�à´”Ëù½6Ë¢9-Ñ�›N‹V?rvábŽÿŽšù�ÿè.ÎßÄ=©ïþüSërÓ³+­fdíöpz¸­Ä?L‡IÓélñ‚«z3C=ýX�.«œ{¬íQ}ö–žËĬeo:¸ºAÏFL<´—‹ö1¦j L¶òÛ⫨M›1{Ï:†–PæÌÉý§”™·²Ë2Y«!3z;ó–† ±hÇÜÝËéë˜Ì®‘uè½d+óW‡ÐyØcå¶Ãv>¨4€Õq"þc<,_×^”Þ$cÂk&Yáêj†$§’œšÇÕ�ä@�ºe1Ø|–»a÷xPëž«Å3S8ÒèÓÅú(Š€]+ùmÆ÷¬Þû#L­É™Ña„§Ëè’60¬Ò†\óÄpレðдâS·&¦ð”›täu¡£�ÊoÙ�÷ÌUââꌒ ¤¤¤"kù¦A6(GÝÚ¶¹ú�h  G‹šÀZâùÃÃ%EE¯S½©w~{I6ïà×É™Mÿîbíª*¤KǰJºWÌ£h{䘎$AFœN&ãÐ8j—÷p2“(îk€’1áhˆK–‘l¬)ùN_ú•YËœ¿†ómj&½G»’›˜.ÉÜKH$ ‘›§2òëå ‰% H yÝ꣪T—ê– ‚¢Â¸›Ç�ŒÏçN¸Y{‡E=˲èÁTDß‹&Ñ0Šd�„Mi/ì üý¦sbÅZnhØÇícÆèÓ�‡™¤%rórvMo�oÔÂÒe”ekSÛEYП ‹'"CFYÎ[ 0£Re/”ò="Âî¢ÅâÑŒ\pµW ßN!%UKq/æ›Bo’1}ª.|;ÈÈ<Ù´¹›¥‡ã$Yrvu|:!ÁahQ`ko‹ÂÌ S t‘aÜUËxäLŸ=¯¡‘!’$“‡F–ó<°äähî+íq0q ²ï~H?ɦ÷6ýQÖN8HHªŒ™ÿMr É×ʆDí°EA8·oÜB-WÈÕ ² ¦æfHr,a1he›Gšh%Ûü–m€¼?÷¶<Öä©pÀÙQ wnp. �«UvB¦ÀÁÙ %©”ìû³û—~pîVØ”Á.Wsƒð¨çn"È5}Î1 &4}o ÞËg±mÊ t©*ô뉗BFÖæwLÛaá舅BŸaüñM³O¾’Òž Æï?}lêÐWrB<‰:…¹¦FÕ8¨ó&'Ũƒׯ8}fèâHHÝËw£~cFm†Ïÿ’v¶'ù¶Ïwœ„G˪ìï^LHŠŒÂÆ;E㠳Ϸíi?íW>®f’¡óR¥±¼êŒ¥B&öüI‚3›Páñ› rïê”ý,ߎ÷N®åÆÅî`ÅÖh|Ûd•=ÚÐ Dë¨ìü09Ê»Ì|´œV8¸áj,qöN¢uø8%pîZ”¸¸9¡�S;ÆA’Î/Í7‡Þ$c‚žP°àãÁœ¶¼Ã¡]wÑY4¥k[ ëÒ Œ!G.ÿËð.‘Ô2¹NX®&HSï*”6ØÁåµcèš°]%FþýõÔ.h YÔ�†ß…âQ±4öÆiÜ ¸@š¬¤Dµê¸Ùy3¨‹ »VeÎgã9QÍUz,÷<†³¹am\;vÀsþoÌîEÇ‹µ±=B¤²®5 ñiP«Å›82£}Ž–!.×Ý’M›|–]À•£¢�4eöé]¬Ú˜à>ØfDcÑóo~ë=€ ¦rjõ$>©Cy™Ähhý¿•4x¹{Eȇ�Ï`>i=��·ÅƒmWws{´3ìSŽi?ú–]ï1fìEjyZ Ž‹ÄºïRþª_T[óvÒ%'‘¢IeЉ¤Ä³ÏÇôZÅéªÓ·Œé† &ÈÉI$ÉJ ”@F�ÇcfBìÏx�‰ß>¿!ÿ’z~Ç2”êÜ•:FŒW•Â˯?�ú�3>#²^%œ R‰N¨ÎäÍ P9  _¹ÕÌ9÷=�Z§^ÉNgÆOnSâ޵숖Q5�Íÿ¡¸)tD,êF�ϰí6¢zùâ×Ù™]+wòy³f¬«î ±2Íf/åã<ËÌQ�®Äê†öñ`ûÂÿø¢UkÖ”ˆã쉻Èö¾ îRW^ŒÞ4¤u'¾·sxðåæú?«ë}îš²Gj„$2Ã�³}Ïe´¥[3záo ,!!++3rþ� ¨ëDÂÙl=…©GÔñÂe¥ùybG¼-£8µ}ǯ‡q?Q—+-Z—Z´ð6'öÊ)Žž¸D‚U%Z}ô++&7ÄÚÎögÁȶ”SÜâØ®Ý ŒÆH¡%Q+c\÷+Ï@m‡8Îþ·‹€ ²ëÿd°í<“??oKyƒ›ìÛüW5.”¯Ù�ÊŽrË–¿ú%÷•µ„û€¬ýq0�]S¹¸'ûcˆOI@ª0œWO§o]{ö³kß)n¦Z=Ø÷ú=<ïoþÁÎÉý94l惡¤Ä³ï0ÚÚ<öȉ§ÓªºL\³”¯ºVÁ<ò4{víç\ K)ô ¢ì*Ü KN"YIe‚12Øvä—Ãg8ò[W�Ae‚J99‘dÃ&Œ™3Š6åÔœþ÷üðËî:”¥ZÍ2Xåzd�‚.íÝΉ(kjöý�&6@%4^­ß<Öý4„f%2¸zx7{Ž^%ÑP�&M‡l\‡ñ«þalÇJÝ:ÄÖ$Û•¦jƒÊ8ç¾!IËÎ5;‰•�©Û©m®qNm;QÏÒ¯eK„5íg¯ç��[á)ßà𮽜 �%%Y›O™™»œ6§éô5ü5¢¥2ƒ8r6»zùqõoôrɹ àñsèãå¾òô…WèhvïÞM×®] 5­¿¿¡§Ý½{7uêÔ)l« ½Ä÷-šóÝÕš|{b •Ò›<=Oê£ã¨Õy>Q­çÊò>èÑ“‰„( ®^½JÙ²e_pIiüþC%´ï07UíùíàßôvÎ>v_ñ1ýò¶Cx›‡à9xæVøK (~Aã!·“'OÒªU«BMû,y�¿¿¡— ¢™R„7•6œËÇ�³ëH"ªR-;ûgz9ë÷E„ B^ô&Ó§êÂbIQ‘1û#“ý¯¾ïƒú385#ëYO©~ƒ¼´&…¬»Â�.üá±ðŠ�i}lê(Œ:,$ü~ö?yüö /úHo’±ÌÌÌ‚'á�§T*Q«ÕoüoþmÙAŠžÞ$c‡.êAx�"##‹:„—âmÙA(Ž,,, žè5Лd¬K—.…žvÆ ôèÑãF#«tüøqêÕ«WÔa¼°·e;¡8:~ü8 E G�¶AA(Žô¦fLá¹É)Ü>¶�M;öqôL7B£IHU£4±Â¡„Þ5мm'|xb..AAÐ3z“Œ‰;’Axfr"�«g1iÚïl¾�6�bäJàm_ÍüŸaY®=Mú†¯úUÅZ¼¶O=¡7ɘ ³ÐEdÖ»~|½ý²„ÂÌ�Z-}ñmU� %œq²5ŠÈ°«œÚ³�-»Ozu ³üv²lÉþùg-œŸýåÍ‚ /›HÆAxãd/ç½öCYz-L=yç㯙9®?ÕmónƒüÉdæÆ°êûqLš»�à�Sißè2ó·þÍ òy¼xPá5Ò›Þo⻤Axýtø¨Ý»,½–�A‰ÌÞ–í?ø=5Ë¡°©Bß™[8{p]< É ^ÅûíßcM¨ö5E.‚�7½IÆA ¤¹Æƒ‡ò÷u5Fe³âˆ?£k[ó ûWú-¯'çóÔu ËŸ°öð>¨hŒ&d9ïûýB�xn« EHo’±—V3&ßã¯vf«P™WgÊ9MþÓgDqéx÷t…�Usÿ ‡O†�ZøYž‰.øgšZ¨PaÒðG®æÂ½¸n·PŒè¸½x“öÄ‚U¾]ÿÝK<ìi¡½·‡ÉmñéÎø_�£tëÄÜõßÓÒ~ÍÈÁˆÃM„¢¢7ÉØË¤Ó)iù{(iÉçøºzþÝâÔgfÑé�o9ªt7™×Ö…Šc£~úÒ [öíÆlá~A‰ŒÍ¡…¿²ãvÞÊñ'øû§U\Lyôs…×h$¥»¤†Z]¡ß­V\·[(&R2û»]ÄÉfÔŸ8�Q>9}½dÏýF¯ú¾|s šÂ^_Vø˜?¦6Å‚D|ÿ=»“_Uà/‹ŽÄÛg9’_ÍŸ o¢·2{BÆM¶ÏLó N˜aá\‘Æý~äpücEšÂ�Ú½ÞgH+O^Ê=VºHöý>›Í!OÖRÉq‡™êÛ–¡_ô§MϹ¼Šê¦âºÝÂ[)i×"VÞÒ¢tëË„a²ï>Ê dípš6Éú›ÈÈÜø��6°!ŸaÓÁ¤ ¤ÌЉ ,¥D¾†EÛâô;É‘cYûQSÞ_q§Ð §^‘Õ¤g¼‘‘ Â+§7wS>Ñü¨�bϤnøèÄÞƒc©ð¼Y‚æ:öh¨À|>}?×vAs�ãÇÒ°6üACæÔ2�šÏ¹ªÂ’ã3µC7–W£Œ*÷Ô™´ï)±}Í'T6}I+)®Û-¼¥28¾m71:%%º …€š+óºÒü“D>xÀ˜š‹ Þ£Û‚ü—fXw�GÆRδ1ý{”fÞ�ÁìÝv„´^x¹‡¢LÜ™ÅLŸök™aŠ«w=:�™ÃÏ}ʼœ‹Ÿ‚h™Õ¤�%¢•”¨¬œñªÞ’~_Låóv½ŽtaÌk[™¾çØ7Ê£˜ÔBáéM2ö�šðóùvÊÏl¾šLŠç‹,K&fýD&ìóâëSSÑ0ûóòTn�³¶Ü’YÞÕ…>8?½$sñï/ùtÆ*ŽÝIÃ̳¿îû‹º¹æÐ†­apÓDŽÜÍæQ>¨ –Â'ß9ü×/Ž�«ícØÚ¿‰_ˆÁK+¡Šëv o-íMΜ»�N2§~³šd5PR¾çD>ÛÈÄm¡dÊ ¬ËÖ¡ªkþ�«0ð.‰©”µŒjÍbóÓuâÏ�溦U_Z©(¿ï šu\Œa߯ùý¿–”³L#ôü!n8Z½¾„DN'!NMãYégKF|(gVNfx7_÷ŸcfÝבŽéÈÌÐèwÍ£ !½9 >ì ñ!¡Ø¿»œÿ¦×ňy´E:G6ï"©Ñ`U0,xòG#"~Û§tøô$•¦í (ä ‡þK3LJµJºØýŒë<‚Ë=V²zd!@²ªËÇãûPÖ(kY’EuÞïGÅÂ. @Åu»…·–6Œ[aZP–¤\里d×€1O°eBS•`@õQkØ»?û³‡½»v=òÿþýûÙý{ܳK?£ÒåðP‚6ì6a/³¿&€_FÿN|¯%ü·`êzS®bMZöý”a�o0¶¬-ÿÏnr”‰þ»–ÇqJdãÿUG*;›¡2µÅ«ó\}"6QûgÑ·¾öfæ8WéÆÌ÷Ÿšð[9àààL‰²µéòÕz¹„päX:m ú×§œ“ÆF¦8¶žÃ5-¨oofB—š”²1Žÿ8K’ �ÊÅ…C¨_Ò c#3ìË`YDÖV<}€L}ê‰R’�Lz±6C&þØlzUwÅÜÈs'Fn×ûŽ{‚ðJèM2ö�Þƒ¿ãë�µqxÑ:|9…û1i˜º¸`õ¬¯>‘ãÙúçè?‹ïûÔÂÓµjUÄ)û““N1«{¶ÔœÏ¦™Í°Ñ§W«×íÞ^r*))2(̱´x¬ØRºÐzúNޝAmëÇ‹4™ØåÒeÆA¢ŸÒ]I²°Ä\Ò’I}‰]š´7¶³õª ݆¼ƒÝã¿Ãø¶sàÌö=Äd'8ÇöžÄªYKª¤s|jgm´aäºB®�`Í·ñ|¬<ÔÝ^È îsH꽌€Û�,íÅƱ£€|FN¿ÇÙå²-ÂooGÚpNï½A•§¹̱?û⩻Ƚ°Ê|$›¯ÜæÌ¯u¹ð¥3N©‘cÖ2á³Cøür�û‰‘\Ü4•ÖÎ P?}žì�¦á�WIIK#-~Ý CXøÅdn´YÁõ¸ÂN¯at]³—óå ÂFo’1�Fóä “‘eÚÇ>/4É{;i÷î‘ø¬õãº{Ü Óà^ºTž}*4!ÛY}B¢l­²Øäñ-jRR²›M²hSSÈxÆž[qÝnáí%al,�œAzz^µž]aÿÑÅô/gLîÜGÖD°sbêõœË™„<æMOË:F �1z‰%¢.6šœpsÎk¡Æ4èÓǃkÙ-CæYöVÒÊ·>ÆXôwí¾žËû ½pu/K ÷Ç~�:n®û›ƒ%†1sD=\í=hõåhÚ$ïbÇÙ¼î‰Îd÷pO,ÌL02s£ÉÄ«Ôùn53Zçô�3Ä®¤'Î.xy8 �[Æ’ÀÚ|>Ë�*N”ë<ž�êÜfçÎkè -°0ŽãÆùëÄêÌq©XGhò™'«ROBa`„J¥BelˆæD_9ÇÍDÖ%*âi+®î„âIo’±¼Ÿ)–÷¸Â3¦NËF^ƪ›ÏØþ °ÇÙQ"4ä6y=Ò°êx¶¬ìDȾ ]yçÑg©˜Ù¼2=þ$M–Qß\Éàuùêàëºu°¸n·ðÖR8ãê¤Èn®Ì¹ Ó’NXX؃!Ö¢ïTÌ <×gwãÒ‘å BÖ�¢YÃ!,¹ôèñ¨ »E¨”În¼ÌWU*l°#Š»QyW·ÕHÏ,YJFÀvv¥¶¢sS3äÄ0Bñôȯ–HËݰ»d~Cm3UV‚c×�õÉñÄÄæµ>#LÙÇù‹W¸�DBèqþý´>OË}´wø›¾ŸQ^¦YË6ñdÔA5ñ1±è,;ó¿ S)èCªº{ÓqÜznd0Ož_�C®a¸å*zU(AõÞ3Ø.žö&Oz“Œ½ν¿flå3|Ù®3×!ðz0WÏdÝ‚µœOÉÔ õ ÎÄóH�›dGûAP/ËËNAHÀyBT5àÖi.›çVçÀGøéb®úÃÊ|úû‡¤|ó¿\¾ËÒ£¹Úe.¿®[‹ëv o-/ªT2CÒÅrêøÕ¬cVÎÂ¥)Q¢D>CIªŽÙŸ}a!“ô7ï6lÊÈ ·³obÑrü$÷´*ï*”{‰É˜Ò«9ÍJ†±aÙóÜ&ÞÞ�ó æ³lí’;  µHæN8™Fr+¿öFŽÎŽ×þ–‹)餧§“žžA¦:ž¥]ò¾yÁÔѯҥpµ5)ð.N…£3ŽfmY––½ìt223þ¹1†(phð óö^#xç'.ócÈ!�ï|_û`\ü¶÷ä’Jdɲ½¼0\›ï’òRðvdÊ¡»¾—7« ;™Ɇ*+ÙÕ»‰<à÷sr†,˲¬‘/Ϭ-›T™"ŸWçš-å’¼üÓ6rSÙÐÈBv©1NÞŸ®“cv�“»ÛÊïü!ëd�|wïL¹O�R²µ±�lhî$û ^.‡=¾êSò¸Š¦O/2þ“ßwvù?{÷_Óùpüsï͸Y8¬@7 IDATbd"bk� híªU#61j¯š­š­Ñ¢ÕV«ª~VK)j¯¶(µGÌ¢6AH"f¶¬{ïóû#A��h"·É÷ýz�?ιÏ=çy.çÉ÷ç9ªïŸñO¾ºA�mYAr°RVyT!Ÿájs„RÆÀ%ªK'ec¡Uvîª|ˉj[¨é¹ßQJ©„ ‹Ô{•\•­…•r,ÖFÍ¿pR}×Ô[å³Ö)­UU¸j'5Ã?Ü,ê/‘;\;—‰Í‹ðhÕ½Ä+,éÝ�ïÏ%¢-üÓ?®�É+ï?ŸéÆ:”Ìn«2¼ûÑRæupEFó…ȽÌfΘB¼ˆ&_c¾Z2 {77 äíV_s(ìy 9 |Ç3%E ¦"Ž1³]}ú¬¹�ɦ<,žAs§W i‹ôcKx±·O²ú£ši>Õ(„ÈÌ&S©.m‘ú&„ȽޜĆ¥ýxÍÖ@Ð#yû­|¹å /^@%ŽÀíßÐ¥f=†m$Q_š‹7ñyGé•Bd+³ Æ„"}tjñ;~ÿ”†…,xpn£š–£tÍ.Œún%{Îr;"&"osýì>Ö|?†nuJS¦á‡,;�έ>ŸlÚÅü6…_Í˺…â9ÌfΘB¤Ÿç:óÇß ˜7n8Ÿþ|ˆû—òåþ¥| €­N‹2IÙ™®±t¡j—�ùæóÔt•êOa̦6’áG!DF霫3`ÞAÞ·‡ÕK–±vó.ž¸Ì�FLF# AgãD± Õ©Û¸�ºµ§®—� K !ÌŠÙcF£¼Cñrl=kÓíãÚtûP DÝ»Od¬��#ù 8`%Ñ—ÂŒ™M0–¡€ !DZ4V88¹á�ÝùBˆt2›`lÿþýJïïïŸE9B¼ 9åÎ)åBd³ Æ|}}Ó�výúõT¯^= s#„ÈJþþþ9âÎ)å"72§†”,m!„B‘�$B!„ÈFf3L)K[!„"7’ž1!D—ÈÍýÿãý.Ó8œ˜ÝyBˆgI0&„ȱb.mä³v)]û}清ÄbBs$ÔBˆÇpç‹'�dÂÜÝÅ'Õ-i¿ƒRa2�V++à !²‡ôŒ !rŽØklýª UJ× ÷Ì]%ØŖ,Ò¨éŒw1·ÿ[”i1�k¦W›U!„xÈl‚1¥Tº7!„x‚)Œ“ËFѸ\9šŒ\ÊÉ0È[¡3_ýy–S?¶¥àÓ5]b(û¾ïIµ2o1`îA.mý–ÅgKÖ…Âl‚1ñ�‰ÈÀã¿MZa§!:”Ðy—§ÈEv0æÍJT¬XññV¹5³Î#—¾mFµ._²õj,–î5yî~.ý… Š`ýôècâ ¾¬[–:Crì¾çj}øa×.¦Ö°ÎŽ’¥Épÿ2‡O!!âsÄßæ¬ÿnI¯¦ø�“`,£T"qñYxç«û¬P‡>˯“êUŒ§ù¼V ú¬�L3X"ÇQþs’“'Sl§.póAÒ]�‡²ñ¦é˜_9~~³úTÃ9­±–e¨W×[�ú|ðóaΘǀ·Ü°|u¥I#ó;óöÈ?Ï¡7º)>î_?P‘xl-Má€<™!þãÌ&{4 ™Ì__÷ n7ò:ºP¬z'¾ÚsSz‡) G[Ö�ׇmç^ʤ‰þŒ,i�ïÒ¨—Ϥ)ˆ9�\h4'�@ ˆZê‹]±ØÿDåÎâvx ß— Os©´ƒ°˜•´³w ÞÌË<Ýofº>Ÿ&ùliµì_”_ˆìfQŽ~ ¾ …KʪKCþÚãøãìi~ŸÚžry^4ßš*c¶såüv¦w­H¾¬¨Õ=–·Í‡c‹Ÿ¹ýÔ kºò-µì¼ù`_B\8³)ÂŽýÄ-+S$¯+›ü­Ü”!¿>[ÇdDâ®!+6„�ÿ…Ÿ@ˆWÀl‚±‡Tä)þ:U€÷foçØáuŒ*vœÏzMfO†úêã8ý}'üfŸ#sïu ñ³í‘2Ý!41š=_|Æoa)sɶ)“Ùnäέ»i’B˜=m>J×zO}Ê€K‹[­Ö¼í©O÷i4ö.8ÛfáÓ“š4éØË�+Øt+å½h$`í*ŽnK‡jVYwýL¡ß9‚ºuF°Ç¥;³·çôñmü8¢!å\ÿÕ•O‚TDB îw/nÊÀ~+jõlÃ� ~LØŸFOPì9– mÄë…ò`›×‹Ú—q!ˆ=ÊÄ*ùðùä±€ŠÜÉÐ2î4ÿ)09ˆI`ï0/t ›ö¬~™ *Š¿ô£®w~lôyñªÕ›9Ç"Òò o™€oy7ì¬íp«Ø›7Ro“šîÜäŽu5êý�©sÎaxxüòOL]íDí7ópïÖ�är(îî›NçjEÈ£·Á©Ô; ]vžX·~lŠCÑ!ì~Ø�g¼À´jöÔþî&ÁÑ9½©[Ú['J5ÍoA2MüÇ<¸Éù“'8q"Åvêá™ ämÔ‰wm÷²rcÈã�ñ2kW� X»T¶LëLE̯´q(ǸcÉwµé:3ë8$Ý“(îlFÍ2ä·µÂÒÆ™r¾#ù샖øqÄÆÖ‰RM'ò×�‡5Œ‰Û»¦Ñ©†7Nvö¸•oÍç»ï>[ÿNñÝðÙ„·_ÌÖyƒiV­,%ËTæíNÃèWßMšuðtžôù(Vg Ë.<®0Õíù4²Ö ÑXRqÒ) Æ3Ìë\ƒ’®X[ÙâòÎL.Á´™‰­*RÐ^��[yZ~ü�2,)r³ ÆRÇé{ «PŸ7 d¤«Á±æ§,ÿÌ�…݆²ñÎÓÕÌöŒkÅ€Ý%ùlgWwO¤ÈŸýéýÃ%Œ6U=wº0íè-v}2�U%¦2«»gò�eÉ[__ &6–Øð¥´Îðœ_Åý߆ÐrÄq*NÛÉ…‹û˜ñæYƶxŸuwŸ ÇŒ—gÓ¥ã|â;-áXÀyvÌhMÑ4&·˜îÞá~ÞÊ ÿ¸wþ7�­‘IeÝýÝ,®¶ü„Qo挙n,¤G«iÜl:�C—ϳu|1vöoɘÝpnП»»Ùy6©âW·wñ×¹R4jàA؆ÁøŽ»HíY‡¸ziýu‹é>h¡æÚ](D* §gвJ%*UJ±Uû�-©FB/ÉẵÉÏþë NŽÆŒV³òt9:ûU@›æ=“Á )b®ç„C/þ¸Ìõãó¨sé;¾>Q™¯ÿ<Ãåcóy;ðK{ ` ü‘÷ÚÌ$ªÃRNžæßÛ|Õe [¢Ÿ<«ñòf~¿àNëž�H­úM»‰yœ'ûl:ȵ62Øq=ý/äFòo¡qéɦˆXbc£9<îu,ŒÁÝq™òS�ÀÁù�ðRgø¦}G~¢¿ž¸Ì©5Ð/÷£í´“²€¯ÈQÌ&3 Om±ü:€n ðñw}ðV�?KKJöû‘ïÞØÎûƒV<ª ˆÛÍÏKoÓäã)ø–tƵBF¿çÍß[vpG�¾ò(æÓ2³í›¼·ªŸÿðž�~) Z +ôz=zkËT@Sà,ÞÉg�½ýÃ̓~›“[„*Œ?~\~_ðEÛ )ò-?ûš¶øqãÓ­S#WÖ-çp±�|5òJ{¦lݦTsK}ùÊØû÷xàX€Âoc`á ÌXˆ!t3Vä£ïM(ê”—Ø»w‰ÁĵU?±£à@¾ט2žTî<�É­ÂYúÓâ 7£eÅ~ÿãFwÿü�#^ÍiV2‚?®Gç7‰q ŠâT¨ï�lƒÝîÍÈÌ?bBd5ë|*≧gŠ­ˆ3v™:riC­÷:âáÿ ¿^6N.[ÎŪ]éTJóÜ{ðÁK\McS€‚nθ—iNïVÅ1ÚyR©”…Ê4§G O®Ÿ:M¤2quÍ"öîÇ烫SЩ( F §aô6¶2¼1Ý¿Ã=\)ä–j-—®ükl�)ìáN¡µxpsì�àÄ£Ëè°ÒëÑë­±²xøÃ[R ˆnÎîxuFó÷/,<é虃©]Üï·ú3slMÎ/Z‰ÆDb6ÁØ“k‰%på×¾4}ƒŽ‹¦oi«—[gL[�ößÿÀ;G†3pñãI÷**„�ˆÖtvI ªôvT™|cø}î›ôTèØŽR¡W +׈º…2ö3i uaáÑ”C {ø¬vòüÓmn)ì]”G3F,<ñ.¢ }j>—‰[!·ÀÃ�´—”6",òæÃѲ=?hÄ™~à—Ù³ðk(}ÊYá˜Ï"‰0 Oo<=q¦§h1wbƒƒ ×¥U»Ê\X·�K†»l^»¯¶myM{‡ �8‚æ6ÁQ¯G¯×“§Ñ\B¸%]câ¿Ã¢Âhv\ãÚµÛ…94·ÍÜëXUéN÷r§øyñ ãö³hYõ{vÀSû‚{0ÕÛ)½÷˜–„®ÆYôÿú>†¶ÁU£ÁÎÎ"ÉT: z¸£®]&ðÑyã¸p›‚É«ÑR´]7j^ZÅÊ�kùuYºt*‹Në„›‹5ÞÃvõðw‹K 1f3½]åU2âPqÜ &Â�òTD^½ÈÍŒ<­cL áUü1ו¢[ßÚ-ý‰?6.du¼/}[¹¢áE÷àSç±°ÃÎ2Œ;÷Ò—i�4(¶]5  PJ‹‹› ÖU§ðOÌÃ{8ž„Äp~ñ}r΅λu‹±~én"Ÿ-XÆòÿ0_�¾n�.!ž„Ä—º‚¸=¸JÀ£…ÄaW®îê�û ¨Büw˜M0ö¨çËp–9c—�gè÷ +oI\l,±±q$_v~ ùMeFs#�÷“¿kS—®ííYûɇ,ñ¿ÌÍÛ¡\ûççï)ÀÈ幘t§'s¦OäûY-87ö}~ºfm¼¼ì9ÿÇj†påï“Æe4;ùx·WkԲьYsŠ7ΰqüHƶ gsg4klm!䟿 ŽÓàÕ®o‡ÎæýÑ«8x‹[ׯs'6µß@ ¶vØj«ª Û…:íÇ0¨– Vvvè¢"ˆ4i)Ú¶'õoþ�¦lå|P Ç—}ÈÇëñëY[@ãÞš^M‚™ÓóSUëI'o-h дk¢ŽdüÚã\ ½MHÀ ŽD™í¦"‡I<Ê”†]Y’²·ÆDÐ2?Ê•oÅ„µçyq'­‰«s›S¢rW¾Ù”ÉO\?MKÁvð�_Nÿ¡ëÈÛµ?ï8$þ=¨ÁÖÎ㵓¿�–•¨Yõg|Ç®‹ÁÜ ¾Á�/s×éðnÝ…jçg2üë-œ¾q‹ÛÁ9ròú³¿ƒeU†Oí€éÇμ;t>»ÀåËg8¼e ‹þ ¡È ê�ç±ðô¦Hì~Ö®?KpÐyŽ�OåÀ¢Rz”?Æ—Cf±7 „«æ2tê>JuïFUKÐØÚa“x™§ÂÉ@‡›fÇl‚±‡Ô½Ãø_ˆäà'Õ)èæ†››nnEè´4õ›5]4xwê—´sX\|µ‰j_áó–ð,ä…O‹Y}É€ºµŠ±“¯Óá›Ñ¼a£!“)L­wœ‰ãÖsWéi0r¾‘3y§¸>m?c[pF›×ò7ÿžM_ûp|dJ”x‹¡ûÊ0uãlÚ8k{öL‰ýCè»(M‘,þíSÊMƒR…(TÆ�5øP¡°ÕS=†&bb`ak›<ü©¥h×yl]Ô›’É-H�­úÈp"h‹ôdѺѸÿÑ›7¼KóÎÄ+Ô›³‘/êÚ''ÎÇ»:a{KÑr@[Ü5Iywí0��“Jr`TCÊ.D‰êí™´5H–ËY̽�vOl6X=ìÑ@ô…õ|Ú¶¥ë bžÿ­´ÿ8'ž`áÜ]Ü8ù ¾S–Ší¦²ùjNztlÌûï¹q7Ò‡ýª<šžðü{PK¡–ïÓÍa-݇o"FëÁ{ßÿ�ÖÑÿÃ÷uO —hÊ‚ˆòT,j—á‘]ñ,[û>kûS«„¥kÓsÆ~î\À_ïbzQòÚbï1yd1v÷«DÑRué³ðRm×Z¼Æ‡«VГi_Þ‹r­~ ÖokGUÀ°xÍ�¡Íc˜Ýi<ûd™øÓ„……¥;ÆÙ¾};­ZµJWÚuëÖ¥;íöíÛ©^½zz³Á¶mÛhÛ¶mºÓ !Ì‹¿¿†îùQÑØ0} c¦oàB” ´y)×n$Ó¦ ¥aô4|ª|ÊÙÂÃÙ}áÞ²Óýãü/˜QϘ¼(\ñ²4ö¥ð�°–“çwñ}Ÿê¸è"8½b,Í^+CÃI;Ÿ| ÍïC�™»9wd Ö/Œc¹v´©d^ï¦BäfŒ !Ä¿eU°ïÏÛÏùcËÝÔÛøëì\·—�Tghp,ß™¯·Ÿ#`ó‡¼n^/§Bä"fŒIϘ"shÉûzG>ÿý4gþüšn>Ð=ob•ÆŽüùÌýÕDBˆœÌl‚1!„È\z<|Èχ/pxápÞ.’þwW !Ä«dñâ$Bñ¦+€Ï{ß°µ‹£¬M%„0CfŒÉð£"Ké,�XLaŽÌ&3åÝB!„È}Ì&Kÿ À…B!r³ Æöïߟ¡ôþþþY”!Ä«�SîáœR!Dö1›`Ì××7Ýiׯ_/«^ ñ–SV®Ï)å"72§†”,m!„B‘�$Bä8*ò <@„<¤-„ø0›aJYÚB‘) ×XÚ« }֙بÝ˪ÞæSÑ !D*¤gL‘s¨HLlGÿ5Á�7Ù0¨-c÷„#M=!„9“`L‘C¸öKOÚqŒ˜äèK=8ÁôŽÝY Kç!Ì—Ùcò¢p!ÄËSDîŸ@Ûk 6>YG˜nndp۱엺CažÌ&Bˆ—e¸º„ž¦q<&µ€KñàÄt:¾·é B˜#³™×*=^Bˆ—Ë…#Á”é>š±8·v:kÏA)ßá´-g•”LÊá³ð.o›�yBˆg™M0fÖT ×�#¢HM^wÉâÎÄøÛœýûÞ(‡kj—RñÜ�ÂÆÝ ›¬Í‰ÿ6”k?†ÏÚ?Ü�eå…ïX{J·ý„É�²3sYÎpÿ2Çoè©PÁk|ò4Ñ…«R*¿&»³öþëùâåÈ0ez$îgróŽÌ:™õc‰Ç¦Ñ¢Ñ$¦þyüÚ÷(ZçKN¦ñ¹B‡›OK:vjIw]vg&‹ ˜ß™·GþA¸âÿbLýÌ>ýjÆcMñq¤¬ŠÔ½Õt)ìI� /ùë+οæÂl‚±Gô.°âƒ–Tõr"Og¼ÞhÇäí¡Ó;�ßx†o$�ޭΛ|…y½A/¾ÝsS—ÁtuµìÞà‹óÆ”G ˜þ&vU¦rΘæW3@¥^É™‚ø¡¾ .í–që鉧ù¼šn}ÿ$!3² „Y³¢ö˜_Y¾ìW>®Ÿ�C’±œ_>œÆåܰ·²Â&*5›ÊÞ¨WŸÃߟðš¾4£?ÝŠ{À–>…ÈÓd73=%îB±bCØ™¢RÑ8¼FóÞ½iVÖé×"ýÌ&{ÄÂ�rïdζ“\8µ�oÞ dú ¯ñOoO�ŠånHÕ¿¾È½;Áœß·„¡E�ò‰oo–d´¶ù/1…jàîúI|y 6ÅŠÐã™~4–ˆÛ·‰Ë¶ ñª(¢±}›?W£²ïž7žžNç^kÐ÷Xľ³8µc£ÚW£¨Ý«Ï‹Eùö´/s�5+Ž<Ù ‹ÙÉÊߢ©Û±nŒžTb< O·p­JÓaÂ'´)aù/s,Dîb6ÁØ£ž1ì)[ï*sÅÑÎ  ûbÅqÑddi ÖùÈ›ßÏrué5eoÅ�àèE ¸³º¥]±±²Á©tcÆüò¨×̲�Éí+ãá ÇƱ0µ¦æÉ80š#“kâRvÜ~¹ŠÞ´™‰­*RÐ^��[yZ~ü�i›*ê úÔÂ+¯}¾bÔ�zˆT“šnz×…š5-XèîÜæÎÃãqYùacÊ8Û¢wð jÇiìºmÂxaÕl}˜|úa7^"F” _ëe„)H ÜÄ8ßÊxæ³ÅÁã ºýï8Ùø÷Nˆ§(noêOå25x§á›”õéͺЬîO�áÒY.éªáׯ1‹{Q¢b}:v{›ÂÚ¤|Þ?8“îoy‘ÏF�ƒ{E>Ü�éúJÕ-G!G=–úüøŒ?D"Š»û¦Ó¹Zòèmp*õC—�'öEHIW–ö*¼vþñ�Gýµ’ßãб™3ÁÑ9½©[Ú['J5ÍoAIõ@êùu{>�¬5h4–Tœt CŸôu/H¿mÉ!_|ëF7çu7;ô¶ùñnù?.Ÿ_ ‘™M0ö¤x6¼çJ�Â>tZãÆèi]ñ~Ù©†ûûe 'ìß F) @CþZ#Yyè27ožaI³;|ßo2;ããyfthËü„Îü|ä2'~g–_ÙO9$pé§®´™ï¾£©ËKtÄÎðMûŽüD~=q™Sk _îGÛi'S ²¢Ø>²Ã•å³íç 8¶‚aÕSïþp—»±ÎÔÿx u�ì#ñ€âÞ†¯ùÑГOßÛ»¡Ü5IJÿ_zýáÆÈßÏpùè"Z‡G›nó¹îõ ‹]dÇî›I•£ñ";wÞ¢j£ºä5üÃWí»°Â~›ÎrlV5NŽêÊÔ#2�M˜ ÆæyK¸«E\ÀRæþv7[Và·¬ÞœÆö¿1²ýÇ,?JŠSðz¶œÂõ·g²çÂNÿµ�~•í0Ý9ÉÎ#n ß{…�À¿Y=°<º éÑj7›ÎãÐåól_Œ�ý[2fwLr££d§®T¿³žûö�G²mÅfx·+Í À½ ƒñw‘Ú³qõÒ&úëÓ}Ð2B©æËиôdSD,±±Ñ÷úSO„Åá?±%ïmÈÇ�5§¸rñ«¦4ÇK÷œ:Xˆ\Êl‚1ƒÁ�bÓñî‚@‚Îïa~ËÛLjóÜM|ôù‹Å³mhiÜݜɛǕ7F¡Ì°‘4sM ct®¥(_Ô™¼ŠÑ°o^ »À…»& '–ðãñÊŒš5Œ·K{PЫ<¼ì“ƒ#7V÷£ù'Q ]¿„ž%¬Ò(Èq&TvÄÞÞ>yËCùq�{× ÿ“>Œš9˜ÚÅ=ð~«?3ÇÖäü¢%<ÓÄíaéêHšMøš.UŠR¨XU|ß.�ê#°¦ûw¹¯òáT² #ÞƒÅ36rÏp‰…3¶S~è ª»å'OØ�¤`,n7 —„Òdâ z¼á…G©Œün(Å÷þÄÊkåhѼGÛÂmÆ«›ùýrUZ6uÃø÷RŸ®Ê‡ÓºRÞÕ™’-Ç2à�@þüó"™2NˆKcK!��+6M~< eÏü%­{'XË Ï#LjX ÏÊ�˜²9�L\_µ€m2c|s^/Rϲ•(ùðéAm^<¼ âìêI17k®­ú‰òí¸Æ”ñð¤rçéLnÎÒŸvð #ù)ÒŽîõ#X·d;Q€º»‰_¶èiÕ­!yÔ}þX¸�ß$Æ5(ŠS¡¼?² v»7óhÖÃùzø,·+½½Þ+‹§~åøÝü´(„&“¾§Ï[Þô(�ÏkX‘v,Dne6ÁØ3+íc‰½{yÚ|6†wcÖ³bOl†)­¨õéNœø‡³çNq`Õô³ñî·ç0ò€³KÓðµÂÈ“×:_ò�Á„ÉÆ @Bl=ñJmù Éá5¿äRž …ž³¨„E9†¬;Ή'’·ãl^þQe ¹ÁM[/Š=Z·BC¾b^ä½uƒ›OE4*ê&¡œñ,üâE,LaDàH>Gª¾?”òÛg2wñ÷Ì nÅp¿ÂX8æÃ16œˆxPQÁG¹PÌËþÑ÷užÞÕÜ$(DK¥vm(r`-¿ß2puÃ:ξю–…´oq3nC½mÑëõèm¼º'‘ð{÷eˆA˜ =oO^ÁWïÕÅǧ]§­ä‹ÆÙ0Iëan¼š0bÎVÎ�áçÖü¯uCÆì�&èz)F‘..d$$(<½ñ|”VOÑbîÄ“¡— h\ñíÕµñ'6Þ5¼úgþr䙲 IDATñ£G[0Ý!($Ž ¹MpÔëÑëõäi4—�„0î¿ä<Ä�H¼ž™$—v,Dne6ÁØó¥ñaš4è]pus£ gª·Í w4ÛsŒ¨ Ÿ2p+…'ì"0<’›{FS1¹’Óº¸áüàWn§R+hòÑzþŸLv[N;ßÏ8”æQÖ(âMñâÅmžNúG-s]AÜ\%àÖÃk(®\%ÜÕƒ§ŸÂ×8¤ ý-.D¾°ü*2‚H ;ì¬A[Ä�!-®ðÙÀ…Ø÷DÐØÙcK$Q*ù¼· ¸ýè¼ÆÀË\Uîx¸k±¨Ð™.¥°bÍ.V®¼HÝn­)¤Mú}\ì3/(–¸¸8âââˆOˆ'àÛZÈt]a.4Þdø¢�;¶‹Å#jâd�õil½h4êsz—¼Îö¿(àæ‚ ð…ý: z¸£®]N‘6Žk7±)X�¼,›cã¾tÊ¿�Ÿ–ï`É"ÊõêI+@ë„›‹5ÞÃv•|oÇÅ%�³™Þ®i\Dg�.!ž„4*'�½+®¶·¹r-ú‰ã¦çÔÁBäVfŒ=ìõ2…aýÊÝœ¾LÐåì?�ß,Þ¡Yuë ôŒ)â£îq÷î-‚¯žfÏ’Ñ|þ[<¯W¯„�2aR âcãHT´Éu�ee?º”:̧óçÙܼqž#gCõúhì+1tÅ*úÇMÇoèF^fþ¾E¥.ô(Œ/‡Ìbo@WÌeèÔ}”êÞ�ª– ±µÃ&ñ2'N…cÐ×¥g7w¶|ÜŸY;/r+„«!©f*:Šz;l542ߺ=Ûç5,��¶*’ðHèëУ«+[&çç#×¾ø_ ›I@Í´/¡]I:÷|ƒÃS{ðCлôjé‚°¬Ü ?ï=L2‡]烹}ëg�榴h…YQDpýz7²ï?gÂÉåLÿi3‡Ï]%ðÚyö/ÿë/ÛR²taJ¶éÊ›×g3tâzNÞ$èÒqNÝHmÒ”–¢m{Rÿæÿø`ÊVÎr|Ù‡|¼Þ¿žõ±Eƒ­�-Æk'9~;46Øéïsîï¢S+ºu úö.ƒÿ”÷øîR=úw-�@S€¦]›µp$ã×çZèmBNp< *͆ …§7Eb÷³výY‚ƒÎsìüSsólêеC~~?˜¹{.È™ã¹gJ»~aþ…È©ÂÂÂTz·U«V)ƒÁ�®-#i·lÙ¢ÂÃÃUxx¸º÷÷Õ¥ª—*`c©,mœTñZÝÕŒ]�*,ùóððpµjÕ*•&Ãi5½¾«²³Ð(�F§¬ìœTÑŠ U¯/ÿT7”R¦uhFGU¹�½²ÔZ(½ƒ³*R¾�Zuˤ”R*áÚ&õqËòÊÝÞRYÚ:«2ýÖ©{q[U7wÕ÷Ïx¥”Rñ§¾PoæñP�×ÜR¦—6^ùVÕ´­ª>?gHyT]þº†²­œxã5Á·¼r³µR6.¯©oR� ÉÉÏ©ùíË*'ïÁjg‚R*.@­ÓB•w·W–:Ke[ÀSUò[ .¤¼„RêÁÊvʾè0µ7A¥.r©òµ+©FJLÚ�½ V|ÐH•* WVöUåÓÔ®[ÆGÉMakUgKUꣃ*>Åiâ¯nPc[VP…¬”…UUÈg¸Ú‘ö?‡©9xð`žý�ZÑV¯@¯Zþ™…×y^9LêÎŽ)ªCÍ2ÊÝÁJé,l•SñšªëW»T¨Q)¥ŒêÖÎiªcÕÂ*�•…ÒçóVí~¼ªâ�ŽUeí[«åÑO�kïtÕù å`¥WùK4PC–žS’?5^_¥úUvS.�V«h£ÙDy¨¢>=ex&WJ)e ýYµÌk¡Š ئž¼L˜:üC/U»xeca¡l�J¨?œU¥Tbjù2ÝU;&¼£Š9Z) [WUiäõ þɺRÅœUˆ5T¥�m•¥•ƒr÷£vÅ>¯~qþ…È,T[¶lÉ’¸fÕªU鎭”&,,,Ýý;Û·o§U«VéJ»nݺt§Ý¾};Õ«WOo6ضmmÛ¶Mwz!„yñ÷÷ÏÐ=Ÿ1±¬l—Ÿ«¡å/·YŸ…¯CÊÚr!²’¿¿?4hÐ ]é3׬[·.Ýç3¦LÏ&„�Årâ«z8YY`aa�……~kâ�x6¾—/ù˜–ùk1õXF–ƒBˆWÃl‚1!„x96Tüp)óü<ÑšŒ�FŒ*i�1eLÚ7iðÄó{'\ûlåù¡Ôó85¹*®~«‰~^ª3+˜¼ð‘OU¼*lßmL¯�iØî{N=x錑»¨h‚ÏžâäÉ“ÉÛi®G> ôâ©û&Tt0gO%§9LtÂ=þIÞ?sƒ¨lŠ' Ç'ñVÙÁlŽMåCÓUæ4v§ÌGûHP·Ø>¡e�m±´ÎK•‰G1¤£˜!O_óy¶1äµjLH3’Íä¼ ‘ ˜M0¦”zf3Eú3©¦y›-à–éññ—8;‘&Òº÷¯Ü0fn¾ ññ_"K*l›µf±uEŠëËQöÁç4m7‹$ "‡ˆåüòá4.熽•6ù‹P©ÙTö¦ÖEž’Ö™ªíûг�: aïTú~Iǵ¸zžMCÊr6�À'O3í­¼XjµhuVØ9¥r³!Ì;r?©×ý©k¾¼ô5P…O2ßÉT¦Vì΢`k4yÿíÉ486îM½ãCè:ã5¶~øÖO'Q�û!#flâØu#k÷fú‚)4+ÅöÁÕèpö}þ9„’º[¬î\•�¬gq䧦ĭ¦3h 1pû~¨gõâ,i-q}w&[ýÂXq'ýV/"|Þi,Ì&<ÂŒY¼FŸùk¨ÿ ¹%¤±Æ³jql¿]Mùph¬(RÕ ‹ÞÌ[S� 4vÅ)ëbϧ+×1È}Q*fa h<=�νÖPøÓEìó-…]ôUŽ�Rµ{Ñ7í©ÜóS*`âö?ÿp§œ/]Þ*L>-€�›Ïiª¨ãüús(Õ4¥ØÃÈJÅ–H­¯Oók×|Ä޽ʡ%ãÜÈuüúMyÍçe¨Bäffó§ÿÉ^1#�‹‡0én?f¿_ÝSŸ¿ ­ë»ÌX6˜øÏ;3~ßÓMSŽ ƒñw‘Ú³qõÒ&úëÓ}Ð2BÉËÛSfãòç\$xÍ|è߀™_5Ç)yÒ¯u‹E܉�%öAßÕIG h«1plGJX%�DãP‰>c»RFÿRÅ"wÑàõ†-ñõm›e½(êéŽ&ôŽÕñV9/ŠzD{óÿ„¹S·Q ÊzÅÓÙÀ� áØ¯Bƒ–¾´l\—,¬ —ÎrIW ¿~�©XÜ‹ëÓ±ÛÛ~ẍ́݌©èнÞçÒ�ý[I÷Ñ,kåÀkŸÇ |ˆ·NƒFcƒïÒ¤ú+©¨A£óàý��‡UøæO]ÍùTF¬�qvvdz̛´Ÿ8š–{ù}Oä3ׄœýeï”rÂÆÒ »žThô‡ŽLª06ô+M[kìÝ+ÐúóÝÜMQ5§•7!DêÌ&KÉô ~Ï ïPú™.¬—¥Á¶ò~žäÊ¢Þ£ù3,EÍ¡îóÇÂõèü&1®AQœ Õàý‘m°Û½™± q¬ÏäÙ�šÐ˜ÚCüi0ó+š9=~üJ£µD¯×£·¶–ž-!^%ãøU¥R¥JTªÒŽÿ� `a×äýJ•¨ÚkAçÓ±j¥GÇ|Þ™ÊáW0qß²zsÛÿÆÈö³üh(ñO'°(K�Ÿpå$+üâ™ßs$ëÃSolZÖšÎŘXbcÃYÕÉx¹Fà#†0ήYÁž0òæ}ú»Šð-ÃxwÀnŠ}²™s791«÷ýOòpÞ«ÆŽ7†¯æØåÍ­Oà”î|ºçq ÿUÞ„È…Ì&t0 I[â~Ÿ4�[]>¥›'M e2>þÜ`øW± ä€ùL+µ–þ#~çîÊÅt‡ �8‚æ6ÁQ¯G¯×“§Ñ\B¸¥ Žov¢…Ë ®™Þ qõü¤ëIxKÌõÔþòñ)!DiÝ;±øÀZyaRÃbxVîÄ”Í��Ò:S²¼7… – þð~Ô�?Ì�³©×o�Öz=z½5–ɵö“�@G'TÅÕÉ — cØwk� :áäT�¶?ßN~;�ÃJã\ /v¶x­ó"ë�ç£FO½r@…óǼ$tœÆ7]ªRÔÍ�¢žÎOMí°Â½ôk-èA¹fÃé^å‡ü¯ñ°3N¨BdŒÙÜ&‡ ÏÌgÚþZŒê[ ¥x8*ùo‡)ÑzÒ퇯¨´eŸíI~”Ië„›‹5ÞÃvG\\qq $Æl¦·«ˆåØ´¡üâ:”QUvñÑë¹�œ �•Oü3ÙRÜ[Ó›òµ>`Ë-¨NnL¹6 ¸.ñ˜™D‹ÞÁGGGÐëtèíî;âho�Vg�}žÇòØ`ñŠÖÓ{5aÄœ­œ :ÃÏ­#ø_놌ٛÊ#”ÖùÈoû/Þ0`A…~ãø‰ß2ŠªN¾Ì:|‚'Ž0§­SrãÑŠ7'ìäïS'ùctUòVÇï†ðúÓ£¦[Þ0àYº6é¹´6òiˆ‰~ Kðñ’Ìl¿‘Àí[ùçÚÚ^œâøÊ6ÇÿÏaÏ„…Oµ~̘²Ÿ^¿'URš4íÚ„±ƒG2¾ú ú½é�UL¡xSÉÛ�„ãÓè÷ƒ#÷~Á»òœ«:Œjò³¯…¼‹¢›»‘U'Þ¡¹}(av>¼VP h(àû_miD÷�®eú�WšòÝÖ÷(b6!°ÿqºR|°ó`$<ðAQ‰0c'õS$‰V0{W4VNx•t#;^=©±õ¢Ñ¨Ïé½²:ëþºˆ¡ù3)РÒ̤lÚ'––Ž®r“Ê‹µÎ–x<!LîpÓçsÇ£�3…ÇÍaøÖ& û¾-ÛG¼Æ‰Ú|8€Ð ›ðæÅƒŒ#[•fÞ„Ïg6aAR¯—–bC¶q'<œððpÂÃïsê‹ØÕŸÁ™mCñÖfBÏZ wý–Iõ“« ®æ±qRIŒjHÙÂ…(Q½=“¶a20oÄ,ô-KY õèÌôqÞl9…½1 ´ϧ5.ðq /Š×èÆLÿ{�+T‹Â´ž³•;ybëܘo·,¥g©L›'„xH…±v€¯¿þzš[…Æ_ò÷+\ä5áär¦ÿ´™Ãç®xí<û—ÿ�õ—m)YºÐ¿\>B—Ô<¾‘U'‚¾t”Ó!ìn·®Ä‡ßtåîÔ�Ì:÷ÔШƙFmë¾d,“~¿ÀÍ›—8¸÷ iLgËü¼ ‘ ™YÏXV°¢Ñü[Ü}ú°Î›�Ûî3ðá¾&/U.`÷ÀÏœaðŽ» ~üEŠ ÚAØ ‡û•ºö,CÓº¼EaZÏÿ‡f-V–ÏÆ¾š‚ýÙÞ?C%B˜;Eäý«Yø Ó‡p;Ö‚|E}h2i-_µwBó÷¿9wr#p]W>®áÅpût�·›¹­�ÑÚƒØòâ³Ø¾ùã¼Îà1Ëè´®uŠO´î±€åAï3f@5¾¹gM±’y0X»cýž®ççM‘:3Æ´é¿™à Ç* lP›¡fònJ�V–ûÊ£wSÆP>odM¾„È14Xêí°³Kû^×ÙZÿË©ŒåÇ©ÞX~Ý;6õ�+OáLÊv¬2ïæÃÊ ¿uQø%ﺇ¨§[{v/h>Í¢ SÏ>õÎ5� WÞ¢cònÊkª#E»ýÀŸÃí±xpƒÝ_¾GŸóoRÞЦÌ+€ýßÍpÞ„æŒeÚ‹Â5®ôÝGßÌ9[¶Iz7åðìΆæE%y'”P›g{™ß™}†Ë/úþ½PBŸ>å�H’šÉù�®=p64‚D½åêuãÇ9=ð0›‰-Bä,fŒ !Ä %fBµBLÈŠsË‚Ë�ؽý‡¿Èîl‘k˜M0–i=cB!„ÿ!fŒ !Dš¬[±,ÊIJì·Bd™ „B‘�̦gL†)…B‘™M0f4_œH!„"‡1›`ìß½\!„â¿Él‚±ýû÷g(½¿¿åDñ*ä”{8§”C‘}Ì&óõõMwÚõë×S½zõ,Ì�"+ùûûçˆ{8§”CˆÜÈœRò4¥B!D6’`L!„"™Í0¥,m!„BˆÜHzÆ„B!²‘ÙôŒ !Dšâ7ÑÓÓ�•ÑY؃®oÁ¢ëËhk›u—BˆÔ˜M0&ÔBˆ´ˆ‹‰!&& ë c<©†„ÙÀl‚1!„x!Ë*Œûs)Ý=2o†…º½œîõÆs ÓÎ(„c6ÁX¦õŒ©[,hZŒ�;Œh-Ë0jï&U2›b¦›)à[êUáš*S9µg%t/øRn.»È4zœ<‹SÜ+ƒ1[gl4™v:!„Ȱ9�ßdÒñöìÄFÿý ‚ñq ÏMa<¿–¯9FT*Ÿ©ðC,úfÿÄÙ“ˆáø—4õIJS¹Ùtþ¾¹Š~U“ö}ÞþŒƒ‰Ù]!„È¡ÁØ“ØÚÇ•|5§pô‰?úñüÞà ×>[y~8ñ<NM®Š«ßj¢Ÿ—êÌ &/ãKdI…ícb³Ö,¶®Hq}9Ê>øœ¦ífñO¦%¹¹ìBd£„?éëfO‡5ñOŽZê‹�çvK¯œ"™Ùc)c¢H|óKNß #,,Œ°Û¿Ñ÷_MØÕàØ¸7õŽ¡ëŒÓħ–DEptNoê–vÅÁÖ‰R�Gó[�T8Û•¢@ý™\4ê«ýŠàÕc#w“‘¸õ�qÐhÐèó×°nݺ¤míbU-N‡oW'û…¡U-±(×›yk’Ò¬™Û“².�ùteÒþÚE©øß›R)„È�ÌæÏßãž1‘a¨|Î8ÛXcmm�µ•Ú=g/Cëú.3– &þóÎŒß÷ô &Ž ƒñw‘Ú³qõÒ&úëÓ}Ð2BÉËÛSfãòç\$xÍ|è߀™_5Ç)yÒ¯u‹E܉�%öAßÕ±JW~4ŽÕ8¶#%¬’N¢q¨DŸ±])£©â=Wn.»È¡4x½aK|}›ðfY/Šzº£ ½…cµF¼U΋¢žÑÞ<Å?aîÔmTƒ²^Eñt6pãB8vūР¥/-WÄ%›k@Ó•%ô¬^·5©Üvgc �Ò¼< J˜Ï´Rké?âwî>œTnºCPHAs›à˜ùŸÞh4‚V›zå«q D)w.Oz ‘k˜ýôU�{9Êæ�"àv, }úze^@ëáÇŒ)+ñéõ{Òù4hÚµ c�d|õô{Ó«˜Bñ¦’· ǧÑï;Fîý‚!vå9Wu#6Ôäg_' yE7w#«N¼CsûPÂì|x­ ÐPÀ÷ ¾ÚÒˆî]#Êô;=®4å»­ïQ$CàÜ\v‘éJñÁÎ;|€‘ðÀ E%ÂŒ�ÔO‘$ZÁì]MÐX9áUÒ�WöêI�;îŠg.` bre›ÀùÓQîupOu!ãx‚¯ßÆÚÅ GYˆVˆ\Ãlþ4>ê3\eïÆ=œ¾BÈÕ“ü1ýKÖÜ«@í7 3zÆÐR¸ë·Lªï˜Üipí0��“Jr`TCÊ.D‰êí™´5“)€y#fÁ oXÊ­Gg¦�ófÓÈ)ì�ÑP õx>­q��kxQ¼F7fúß{¼H©EaZÏÙÊ��<±unÌ·[–Ò³”u&äÿßÈÍe9– cí^ýõ4· �¿äïW9WLWšv]«8³/ÃíâÔ¹SìZ8ŒAóîP«[Š=¬} Ù¹ñ—nrbõX&­·Æ×¯Þ« …ÙÎìzÆTôUvÍÍ’cW¸g´£àkõèõó,úÙ÷áXÑhþ-î>}XçÍÀm÷øp_“—ª°{à‚gÎ0xÇ]?þ"Åí lÐÃýÊ ]{–¡i]Þ¢0­çÿC3ƒ+Ëgc_MÁþl¥_n.»ÙMG©!+YŸø!c'µâ�à8l=|h1q_÷õB ˜TGfúQµ{&—ŠøN]Ë·ïæÍ”Q!ăÙcÇz|¼ñÿ«³$°m@al†šÉû5XYfì+�ÞÏo@ù¼‘�oææ²‹ÜKƒ¥Þ;»´ÿ¿ël­yå¯8Õ¹QoäRŽ|N˪ŒÜ¾†Žv¯,WB3c6ÁX¦½(\ãJß­qôÍœ³e›¤÷3ÏØ—rsÙEî ˆ¼J¨Í³=­ïÌ>Ãå}ÿ^(¡OŸòN$ I !D¶0›`L!^(ñ0ªbBVœ;¶¨<…3O¯Ã,„ÈuÌ&Ë´ž1!„Bˆÿ³ Æ„"MÖ­XebYvçC!²€Ù,m!„B‘™MϘ S !„"72›`ÌhÌ”·� !„Bü§˜M0öï^.„Bñßd6ÁØþýû3”Þßß?‹r"„xrÊ=œSÊ!„È>fŒùúú¦;íúõë©^½zæF‘•üýýsÄ=œSÊ!DndN )yšR!„"I0&„B‘�Ìf˜R–¶B!Dn$=cB!„ÙÈlzÆ„"Mñ›èééÇÊè,ìA×·`Ñõe´µÍºK!DjÌ&“aJ!DÚ ÄÅÄ“…õ„1ƒTCBˆl`6Á˜B¼�eÆý¹”î™7ÃBÝ^N÷zã9�igBˆŒ1›`,ÓzÆÔ-4-ÆÀF´–eµ÷“*ev1þœI,C�ÒyÑdòÙLßR¯â%˜ÐT™Ê©=#(¡Ë‚ ñ_¢ÑãäYœâ^™ŒÙ:c“7±B¤SŽœÀo2éx{ö b£ÿ΂@ †ßG4¤ïÒ+¼ð�šÑÇYúÕZÎ¥‘P…bÑ7+ø'æÉãZïá쎊ãþâXMÈè‰)Ž1­‰+VLÚ|ê1iÏYæu¨’´_©6ŸìIÄpüKšú$¥©Ül:ß\E¿ªIû>oÆÁÄlÊ¿J üF¦lº¾¬äÈ`ìI‰ì烋½:­+;'¼|Þeð‚c„¿¬ïQ IDAT‚GEfá”_9“Ê«7UØ>&¾Û˜^#:Ó°Ý÷œz�õù"GPÑŸ=ÅÉ“'“·Ó\�|@èÅS)öM¨è`ΞJNs6˜è„{ü“¼æQÙÔÊ1ŸÄ[e³96•MW™ÓØ�2í#@Ýbû„f”u¶ÅÒ:/U&Å€‰ø¸„W’׸3 éU£ŽÖ–ظûñë=†xâåuÂBd³ Æ”ROn1lþæ}ZV/EA÷¶üŸ½ûŽŽ¢êÿ8þžÝd³é!R ¡7é¨ ¢€H¥Z(*>*àφT± È£€…¦>Eª Mz�Þ{϶™ß $¤@Â.ð}�“™½sçNr2ûÙ{ïÜY˜¡ž}­š5“Ÿš€õ®o‰ÏL#öÐ:æ=àÎê§îæ?«rkå\ªÔª¬¿xý®¡,tiGcc+ZΠÿ½ŸpP™׈"Ž-žÄ�­êâa0àê[ŸöwMgK^%»éè|ߣ<Ü'=`Þ2�Ç>ÊeIJã¤&ã×§[rä­Î�ú™üª6ź›—ZºÓo~ZõzÙÕ>÷ ›¾ÃßñiÄíšÍã2FväÍý’Æ„¨)3gì\Zîv¦z€_Ÿ`ꇿòy³Pü|.oR‡ÎÕ __\||©;ñuÆ~׉Ÿ÷ÄbÐN~ΰ;^f]B.ªO3î˜4—¯^ì�¯˜N±üµgyuÁz"s]¹ýmV-»ÿÜÖ’±n½FmcЊu¼q“WÕæ�éœ 0‡µ£²˜Ðn�ÿ¼€ì/áä0ñXæÔšG¿\JïÂÒh¡¸Ð scÜfÿL›lõ;;ãäô_,íE¡Š{cZzðÆ�ËyJÅN»Z¼Ú½ÏèqK {c n†{~ÿÐw¯lO:>üPI=x�´VƒÓ=Œ::+I&¶‹¤*-oK¾M¦Ëøþ4¼Üy¦Öãì;bäÖg‡Ñ2ÀP²­À‚Ù¢ÉÔ !j�üõÿÛ+VÄŽw&ð]£÷ùmþ$ÝØ˜@o#úszÍ.�™ä-ßóÛÉ@ºÜŽЇ `ú¯{‰NŽg×G�9ôú>:`ŠÙþú \Y‡§—àô‰üôö@"ιÀì™Å°û×Ðù‹L«jï.LxiM %{(žíyô¥ûia¼ÌÓâz øqCßA Ü�›ZFÞ Jr Þ]î {«Â£K:ÀÁ¬zÜzG7ZF„Ó ÀJÜñlÜw¢Ï Á º³�µx´F!Rß…Q�ßI»Æ4i×›ÜFØ™cš71µ]F7šßÉ”ßâKç æ³hˆ'­_Ùƒ°Z­˜·ý‡FzEqeð÷%]kÅ+Fã©((úPžÜðï�¥–½�/§ÿ̱J'´ž¡’ºñ]Fvk„¿»uÛ eƦôÒ°eÅjÉàëþ.(Š‚sË—Øm¬G˜ÞÉEQp¹m. 2÷MˆËâ0aì¬â¿øvÑiLÿ¼N׺¾ø…u`ÈëkH¸¬q�ìÆÐ nþÞ^„ö~—Ø[¦òÌÍ%/»†Ò¢e(~>´úƒB£8z¦M|½ ‘~Ó>æÑî�mB‡Ö¡Jë,>ú%£‡~†ßŒ_ùlp°þ0…¸ÆÙNóըδoßžö�îå³£§øæþÒïÛ·§ó¸EÄŸø’�ÛŸÝÖáöéì¼÷�»äN�ßxñ¾—Y¼;Ó…œZ2vá6ŽŸÞÏ£L|ùð‹¬¸ÈDVçžïs¢ ˆ¢¢l~é €ËÝ H+*¢¨ðÝb¸ävª1óyðž9ä ÿž1‡ønp*³ÆLeÍ™1P%ˆqÿË¡¨¨ˆü}oÐÑ Ð·`ò¶<ŠŠŠÈ]ó8!rñâ²8ÌŸ�ÕjÅjµbŠ=ÀáÜîzý{¶�ŒãØ/�áôß±LX‹¹´Lõ)xÝý1;÷íçà‘£ìÛô-ÃÓ^¢×¨…Ä«Ù;?åá[š\Çï�!Ì�·¡ªZn­ïg\_vlúÓe Sê\ëT·.õB"hÝsÿ÷hWr·næ�és{‘ý7~Ê®Ä\²cWðhé8¤âD�[*§£Ë›&«'üñ%ü:ÁÂûƒF0÷èùŸ{Õ˜/ÚªomÏFC%eͳôhóË3%Ž Qst=½ñööÆÛÛ£^�ÑãÌ÷Þx{¸ Ó»àáuÎ6/Wœ®Ðºbƈ~Ú—©[ʹ…Ò¥¾nE—ñ„'Ú>÷{öícÏšÉtöÌ';÷±oß.æ ó¯`ú„�¤ø$L‡Þ¢³{I¸2ú�bY~6™2ö(Ä•âpøuA!+ DÇY¡‘°Q\lÅàj¼¬ä¨ç’‘ž†¾8—¤£ëøøÃ͸tþ�Öz•퀆¹¸““öï…ËõîîËݯNäóº¯pWc²“MÔm\ÚX_n~c “»3jÄ«´ýë]zx–¾Ô`,s^ùƒ¾¾IZ¡Êö± õõZùÊê’BÔ}3žÛ�ÆsØÈŽ9N|ž>Ü@ïsŠäk0wc?ƒ?Mëb�GO*nÜ1y�üØ•åžÀ:°L ª>)ÞI¯G3›0iàQzIqö"ÄTͽ~¡¡„ž½¼è€‚ŽÀº�¸tϾ¿^(»°ty«gèôè1c2ËK!jŠÃõŒáÓ‹Á½L¬xÿ¶'瑲ýc>ýÝ�þoÄù’zÆ<ƒQVÞOh`!�ÛsÇøù¤Þü.ÿ[0–PCžœ=™Ð5chè‰gP?7£‘¿ðä¶™¿òU¿T>Öžˆˆ¸ýÑ:÷¥r×ìoxÜö)�¼ºéœ[Í]h6n¿x'nጜ¿†yCB‘}!j�–Ųñ¸á†.úÕöΙ콂‹¼š÷/æý¯W³óh1ÑÇØºø3Vœt£ió�˼è iŽ~Ï/ü´/ž„ÈÝJ¬Z/VqVñññ¥_ ¤ä¨4:†.Çæ0é½5ŠK!5á»öÇ–›Ãp§Qh"ë—®çdb,‡öž$Wr™—ÅázÆPêrßó‰™4•1mß!ß»9&Ë[} ×gjëÙ”Õ¡ÃÙPýȳϦ4YÑ:ÜX íâZ¢àltÇÝýâëz7—+Ø3­àßë%–ly©ü—;¾Íás5ô勤3 ÊÀ¨s>ì…?³™¼g.Øß½#Ï,;Â…›/Ê©Ó�X˜~‘—ëöšÂâSʾp^»ÎÐ:ø#þüQU�.„¨„ı{P¸Äck‹y¬fj³›’gSN²w3„p,š™Ü´d’]Ë~À¹}îaNV¶F2ÉV™–‹Yƒ*/(„5Ìa˜BTʲ“׺„ðZmÔ- . !ìÄaÂX�õŒ !„B\E&Œ !ÄE¹ aQžÊ"{·C!j�Ã,m!„Bq=r˜ž1¦B!ÄõÈa˜Íf³w„B!®8‡ c—öp!„Bˆ«›Ã„±­[·V«üöíÛk©%Bˆ+áZù¾VÎCa?Æ\å²+V¬`üV™c&ÄÕê³î Ol1aÍËEÑ]}«­jh8¹{1¯§‹\‹„¸J}ÖÝq®=Æ„×k~ÿrŸ~xU0g$.ön†â K[!ìBQ®îË�â8ª…W9‡é“¥-„¸Îh¥_W««¹íB‡â0aLq½‘4&„ aLq5Ðûðúà ¹Ý¹�aËæµù§Y'«ì!®0‡ c2L)Äu¨ª÷œu¸ÖfSôhÒá%„¸â&Œ !D¥Ôæ/;Í/ù5—˜W?¦ ¡m�Õ(„Õã0a¬æzÆœrO^ U-bÁ’ƒ|–Zóu=|Њ1> :5›7¾8Ư©[ˆÚrI3Æ4�¬Übbsk®ŠÅŠéÚ$hBˆšâ0a¬&éÐØùç^ž:h©¥ ¦žž·´`\úa†oµR«OÕÔ{óô}ä®ÜËÌø²g£¸ñѸ¶.<ÌÂì ^wv§;ŽîÎ$êÜ—Ô|>^°ƒO=ëòùC>ÈúÂ~ª}Jã’Î�±wGp§û™ÍVÖoŠ!½mCî«£66m<Æç–º|ÔÇ�lùé¼±Ḭ̂»ƒ¹Ajq&3–'rð¼68fÄr÷r§¾ṞÛeµÐàjÀÓj&ÃRcMBÔ€k2Œ�O¡]�ø ƒÞÎ ªÅJjV›÷Å1÷`yµyh�lEóÝ{Î †úº XzŸß~u”å…—yŒ ’”ââÁ .Þ˜÷d%ÏaŽèR»¡4�~®4õ,ýЬ1èÐê¸Ñ4@¬œ0€¢hàF=Tƒ7�J˜¿Mõ à^Þq®dÓé0 bV+*äÄí}Zq_ÂAÆì(ºôæé}˜ü`#´Õ{x+æ‚Z† ½�ÑéǹwSV‚›4àõ›h륣(6†1Ë’IÔéÐÙTä!j–ı’aJ•„yýi9iæ ^×7žÄú=Ó¹ñ&ðº¹П>Éd£]hÖ8„©·5ÇX°�i§%¥a?—’Æ.¶OyÛˤ¸ þnúª�$æÈ£ÝêÒ'Ô½JZF>›vG1ë”;Ó %îç|šRSÇÕÞ¼>/tõ§}'ôf Q‰ÉÌü_{*ºÔiVOaE¾ À¥OÝîOáßGpÄ„ÎI#;4‚ßú+¼ùÕiþ®Âe3¸C+–ÝXÌóóOñ×¹=q¥R»9Àý;Ѝ0‡ q�p˜0VBÁ­ioîQ¯ô“—�¤�¿°ù´�†�¨«¿ôš5«�\“³Éʶ=q¬lц>�.èN‚Oï «O=:SÛþ9Í;óÈÐé}S8ã[{Sß ’ËÄ•éçÕíÝ œ/x²qÅ>K¼„a'#}{D0¾¥ÁNVNžNföúv—ÛkæD»N¼Ð±MŒ‰ifÜÕ{̺W9µ�eD²óìL#)¹˜â�‘Dº”|Ÿœ¬bSÓxó×\Œ€f-æT¡ÊÜÿ�`  ÙL¯å$àÎWC°‹cÆÒbL:‚½¨_hCCÁI¯\ò4Åàέ Ú—Å™ :ÿ`¦ßáGÊÖ“Œ=YD‘ÁH H´PÉóVl=ÇÑÒïô^î4vÊçÛ#¤›8û+8_´±Îth]]T*» J¶$G¦³¿GwÖ×óשÓ›S�·y°ìD±1!J9L;3�ß§× |Ñ«d›š¼„Ñ7.¯[™öÁƒ„)Z•ï„¿8¿Ðnö±p0¹ôS`~/ÏbZ¾†_x}fßÙ�QQø,MᆛšñFã>øe?[rÁßE%Aƒ¥µ¹ó^?ÿ~„y—ÄÐÑ®{s^‹Ègæ²}l7Ы1ïõ³2zY ”ökÞ�÷»øeí!þ“¢R/"Œ7zÉóñÄUêR;Æ4+‘ÑYD¢Ã·Ž‘'ÀÍ™¼¤öy tÜÜhl1³;ÚBpgŒ(ÔõÑS”UÀŽ 3g>ë(åu–Õ�;£záyü#~Ï*ùpÄd°ÀP\7¦+ãKlwýœJP§¦LmëI„§Š2ysáér«W\<ÚÅ�üYÄ—f§:®Ô×òXp ›ãf±©çîäÄ­··§�»ƒ©ˆ¿÷FñæŽ\²ÑÓoPÇÒy° SÐëëðÖSÝx È:x”»ŽƒâÄÜgƒ�Û0êïÂ’9³:];㜞Æî‚’ š—Éêø<ÓÌ�S™ä—4€VÍ| LNæÏ ç¸ qs˜0V–‰�MçzšÿçulxÝb€g³Æ¬ŠÐprÒãiPÈ8Åkg®`V3Q%ÿ͉Lec?"ê(�éÅàV¶n8IJ„’²iyz@ÁÅ/ˆé-}ÈÙr„é'Íù”§Ðå¶líuî&¥ôÒìäÅ –Îl]ÍÊd+`bÁ†dz� ¤¯O*ßœwטÝ[ø`:zŒ¹' ±)§sI¼5à²~6BØÇ¥&ŸsöQ éßš§‚P ùbQ$E·µæ™z%]8–øh†nÐóÎè0š—ö ©ù)ŒŸÅÎr‡ÚªÓžŠËê||èYÇŸfŸ be«(â›Å™—ª�ªaÖtt öÆ3&’{·æavÑQ\½«Ø"kbÛÌ�xv`}t%±>Årþ”MåÐ?Ç™iÁ5(˜74æñ„}¼_Ne¶L^žÉ@UÑÂ|Ñ Syæë(¶« ©j%7/YØp$›goóç&c&¿zOú6vfïÎ %‹ q–Ä1«õü)¡ZÎ*æ-:�ê=€ÿŒo‹Þj½¬I£ù§¢³!«¢ÃÇÇ“A=#ør€Êè_Ò( ªËn©K÷®ªŠÍYa—ŠÁ@]ƒ…ã9åÇ,¿P?ü•BægZ*h›Æþ¿3íø¿K`è‚ù¤_Io–âl ÐÙÂñsÖ¥Ps‹IÄ@ pnSœñwWH�3Wz§«³îìíú(`tÖa²¨z¯˜×�Ño̤T\Nµ©˜­çwÏ™ M$Z°Âù—i…–7µæãvFôŠ‚»‹BËÇ}±¢²{Ó^8œÎË‹­Œ¸±�ëÀó9™,ÙË·Q&J¦m©¤g’˜¯AA¿¤1 Ø]ü…³tKX­*æÒ‹›siÛ,6séÈ9<‚eýýñ\\ô(Ã:1D…¢È“ ú#‹œSil¼µ 9óÇa .aþÜæ’͇‘f¹ q‡ cç¯3¦‘±j«Ò!pÌ ð¿üuÈ4«•Ì‚’O‰éù&¾:èϽ7{ÑH_@ë¾ hsŒÑKsHÓ§ïoA Y,dX� õÖCò…qK#ñÀ fjaÌÜ”¼ŽóCfù¡­¸ÐD\ö9wSmgÛf1“jq&Ì[�’\Øt^FB0s싸f!5_£nœ)þ7l]@çÄûÃýÙ·1¿ðp¾¹Õ‰¯¾;ÉŸ²ž˜p(—;�_Ãl¶‘oR@³aÒ4L–Òï‹EEU Í6òKç;©f«vñújŠZl!gü݀욪UãÄ?ÇyPAçÈ;CÜøiQ4»U0—\cÌ9Ù,ü#›ÿnt¡[‡†¼vwK¼–î烤 «²’S¬áꬻäyk–ø8Æ.L@¯sã�¡8m>Â×iªÅZÒ#gÉaÅ1 s[ûr$�æ­|QNžbKÑeý„¸æ8L;_[Öl¡@ñcÐÀ¸ÕD•z=ޮΨNzü}½ÑÁ sr4§4…6¥Eœ�t8«ç\Ž­¹üvÜÊG7…3¬ žÍÙžî é)¥·iVþÙzœWÜ[3ý®0N,‰aoù0/ΚË/G,|rS8sâØn22 W]Bâù=K�J‘Õ‰ˆ@#n El9˜É“™œx𝣋±zp½ J57�™Û}ø¬_(uœ Í�&V¯= b‘\ê «çÝYÄÜÅQðô2dP`ÃaÆžSÜ ˜ñC6šÍBB–…2—:g¬’rjv»ó¸­…Ÿ'æP¦ƒLÓPQ0Ts†Õd!Õ:¬˜59ùfRÊé*×,&¶íŠey³Öô®ïŠþÂ0V]š†M¯ç¼Ú­VÒó�3ª†s¡™”¼ó5äÀ�T¢Æ0°�J‹+«–f—ýYq�s˜0v^Ï—å[w€¡Ý;/»W¬°Ð ³¶¹‚f³‘�WÄ¡¨X&þ�JŠ ~ؘ@ó^Mø¹ƒ3Ϊ�ì‚b~-°±só1¦uçñ»Û2Ù ‘�žÌ”ÅçN«·°yÃI–ŽnÉ«7e1zc.Õ[:LeïÖcLS"?´/;Y9y:‰ÿ¬O!Aly¬Ø“Ç;]3:ö_žŠbâºúü§K3~îë„Îf%%=ƒËùÀ胑Œ§ ŸõreûÚ#¼y±9mBØÏ¥ÌÐ*?/ééݧ ¯G\ü–A5'‰‡¿Žf_9µÒ7¦æóÝ–tnЄ�Õ8¾<’K¼IÁÛÇ�†j¿Å›ˆÏ3Э‰7a¹E¸¸)$¥–·«F‘Ü t**ÛIçFYÙ›XD†UGPh½|Tb2M¨\Þƒ=m¹Å$;Õ¥wcWŽ%‚Ÿ³…cUx‰š™ÆOñÁL¼3 —ôDÞK–J!.ä0aì\ZÆqާÛÐÕkLC¯Ë]^cß_¸õ¯‹—ÈŠ�çÙoË›Á XŠX³ñ(k6^°}åNVŸ-“Çì;˜}á¾j>³£L•¶äÏ9gƒµ˜ß7å÷ �QR ‡weள{sòHã�D]ü„J÷‹>xœ�Gl6Mægt)ÝP—s»ceûV·îÊ˦Gžâáåõ˜Ð¥o�À[QÉÊ)`çÞBVÇæóÖÚ÷nʲ�‘ϤŸSÊ©ÅÆöýÉÄÜÁ+­ryjÉ|+5/™Çæ�_ÒÝèB«Ö¡<Ðˈ¯“Fnn[·göq+Z…K[TNÍNãÓ]uxõö¶¬ÒY‰ÜÉC›sJ¦K¨Ì]¸ï"{šùco&ãû²gc*qr1¢ ‡ cJ݇Y™òðåÔ@×>ØÞ«öžMyµ°ÚÊ9÷sžMéD6¿^ùf Qâr‡)ÏÙhµªY.^¡jÑPËÛ÷†)÷=­_]YyA4Rby-&±ÜWSNF3ödôyÛŽžûA¯TNL,㾈­ôhÙq LYrá‚8¥lÙ¼9ïŸs7°úœc�ûRb¹g°²kÛn«´eD�âŽÙ§ª¿£× ‡ c5÷ p K—î`i ÕvM*}6åÇön‡ÕíåRtx¸9ã_ÎPÝöu{XÙþ®Îø—Ù¦;g¯òöì{¾{¥e„¢:&Œ !D¥tŒÝ™ñµQwÖΑ &„¨ Æj®gL!jž1!Dmq˜0&„¸ÎhPåç›YÓ™úá_L­Õ]ܾz”Ý(Ÿ…5ä2ï¯Bˆk[¹AL!j�ÃôŒÉ0¥× MÃfSÑlTK1µÖÅTÕnŸÔ�ÂÂòWT-&Š‹‹QÍrí—HQPèt%ÿ*—»”“¸Z9L³Ù*{Ú¢âZ i*ÅÅ&¬Ù¨Óþ Võc-{¸#ÑÑÑ}Ý’�LR’ sÖ¥Õ/.ÕUX.hº¢èÐ9ÑÝÑŒ(J5Ç ®Æ.|P¸â¥ªðL33PÝç‡]97n¬ðõImœÈÊÊbbøiޏ†ÍM Açd�„±ë•ı­[·V«ügݯâOGB\ÇŠ‹URRJž=aÂ;·Fûš;w.¯´.&(HÅh”÷µë•ı>}úT«¼��O-µDQ› ‰‰‰aÆ önŠ¡M›64hÐ777{7庳víZ{7�»)…B!ìJ˜B!„IB!„°#‡™3&„g(—¹Þ’¬[(„¸šHB8¤§Ÿ~šnݺUkŸ¿ÿþ›9sæÔR‹„¢vÈ0¥Â!uëÖ��žžNzz:¡¡¡(ŠBzz:mÚ´!==___|}}IOO§cÇŽ‡7ën^jéN¿ùi¥K³jäl{�n~Œø.ŠšXéPMÝÄÌQ]‰ðuÅàâIÝf=yôûSÈ’ÖBˆŠHϘÂaedd0qâDžyæ6nÜÈþýûùòË/™8q"Çà‡~`ñâÅÕª;ÿŸ÷:ôkÞ^Ë‚1—1Ô²Y1é^ÞŠμ߾¡k]�ÔÃ;H ­‡,å)„¨ˆôŒ !V�:u˜1c3fÌ`àÀ�<óÌ3̘1ƒo¼‘3f0jÔ(F�ÅŒ3hÛ¶mkÕ(ØÿÃ~„ñåU,y¢%FTR7¾ËÈn�ðw÷ n›¡ÌØ”ŽF¿>„ïðŸÈ-­Á9‹®=øà´úoµj"G�åÞÿ ¶¤ëÀ± m_ºv”–Ãîy�pkó <Ýüivç~‹·�mSÖÎÏx¬WSÜ Ü|©ßº7¯n(5–9·xróGÑ”ÍÊžWZã9dù¶°îã£Áhä…ÑÙ€G½¶ �±‰ô³Sêò9¸àInkê�›Ñ�€cù!Y«¤­Bˆš&aLá°¼½½™2e ;vì`ܸq¸¹¹1oÞ<>ùäxúé§yúé§™2e -Z´¨B�¹ÿ|ÀÐþ³ÐO]ÅOOÝÀ™e6Õ˜ù XÒç «×ùÑéÁüðà›¤ïùž—ÆM¤ï(�½+{ŸXLüê~xy¦°ŠM™yVã’P5§q™ K( ”ßEVI»Ë´Ï_…‚üB45…Øx+¡=`8·ŒšVA[5p“ç'Ö4YŠEHB8¼zõêñÃ?0iÒ¤ó¶>œ%K–T«.ÅÁ�ÿ ¤î= ¼§?οþÎ[== ¬ˆKçñìûëš”3ãþæ �Þãsæß�?êÜÇÒ.†²…Îr¿ÃÌ~é,zh-;,ƒ©èB£gÿàðÌ®„8'ê–Xmàsá±7ÜÝ42Ò2P ¿`8CWq»Íeξô_ tþÔ Tˆ;ƒ™F¥óæ(Ý~±¶ !jƒ S !® ƒww÷ó¾ŒFcå;–Gñ¡Ë”¥,{ >½ïæGª4:†.Çæ0é½5ŠK!5á»öÇžÍ3ú�ðü퇘öì��¾Ÿv¦5™ß?›ÃO›÷s2&–SûV3gÁ_X¶ ‘Á�þ÷÷#ï›yuÙ¢“SI<µ�=§òÐÐÓhÈ:EÎåÅÙˆLLàøæ5ìL.�0¯Ô¡K÷æD-~�ovœ&1)ž„ SiG™¾Òv_ügàGÿïÂòÝ‹<ÿýN'&rúÀ>NçùVÐV!Dm�ž1!„Cúûï¿ÏûÞ××—¯¾úªL¹s{Æ.ܧBŠ=¦ýÀûû»ðÜØé¶á9-ËgÒ”'èùZÎ~46›5ó뢔?=†ÿ[µ�F6-»\…šMÊÁÕÌž9�cñ9ØÜêÒ¢çH¾ùq œþ¿d>Ïó“ûòQtŠO}¦­dÙ„è›<Åw‹ÓÿÂpÚ¾”�WÓð³@Cô´zæKÞ?öÓú4g|±žAát‚ o<þâí®ø@нŸókæó<÷jZÅàR¯/ÿ²†ç+jkÕÂBˆ*R²²²ªüagݺu 2¤Je—/_^å²ëÖ­£K—.Um>>>Õ*/„p ………DGG³qãF&L˜PnÇz’•üœBs?>{/3|æòϧ}ð¬Á#”a;È�nb׳±üò`d–ÖµkîܹÜzë­„‡‡ãææVù¢F­]»–>}úT©lurÍòåË«\/È0¥ÂišvY_5Êz˜ûS'¼?óœŸçÇ™µÄ„צBˆŠ8µåå]ù¼|%�©¿�W÷æ]É# !ìHzÆ„B!ìH˜B!„IB!„°# cB!„v$ø…vñQ¤+MúÆÞÍÂÎ\¹õV{·AØ›„1!„ݸ…6·w„°«Âøcön‚p2L)„BaGÆ„B!ìH†)…¯(1’Œí+É?ùæœTÐÀàˆGãŽøu„kp{7Q!.™„1!„ÃÒlV|HúöàÕª'.þ¡˜ÒãÉ=òé¯À¿Û`B=ƒ¢w¶s‹…¢ú$Œ !®¨ª>;R³Y8õåsÆ¡þˆWðíp\ðñàþãÉܳ–øe³0¥ÅÒð‘÷¯h s÷r§¾Ṟ5üDLq�©ñgªŠ«ŠÌB8¤„Rw„&>÷ã� ('�:¿�¢àÛñNšLøŒ‚ØÃ$¬üè ¶Ð‰Ûû´â•æ”Ê ; ƒÞÞmB€ôŒ !PQb$éÛWÐ`Ä«¸†4ESm$¬ükAácÞ(SÞ5¤)¡C_ vÉ[•Î!ó äÑnuéꊿ^%-#ŸM»£˜u¬µ6Oê Å�±#Zód°�¦a6YˆKÍaõî8þeÂrEÚ``Ø=méwú�î1]™óB\”„1!ÄWÙ�LÆö•éӡ/¶¢|¢þy‘»p iJÚ–Ê”w ¨�o‡;H]¿�Œí+ ú|¹õz†…óÕ�¬Ç☱4‡“Ž @/ê^ÁaFE‡‡QaÏÆ}L9bÅ`t¡E³0¦ÞÝ�÷3'éJ´DÁYuöç]‹dˆRHB8œ¼ÈÝøÜp+( 1)ˆ>€)5–´ÍeØWËx5ï†WËäÞR~¥:wFõ Âóø FüžENéæ˜Ìvê<™4®Íwïeü~3*P§Us~½±�Ç¿�å°b¤÷MáŒoíM}ƒJjL,Wf”9„oX/ö ¤«¿kN6‹þ<Å7ñÖrƒžÅl%«È‚Vd!yg<[·¤m°]²ž!ýò@7êòãb»4‰Ï:<Ñ+Œa®xY Ù°ý43öP€Ž&7DðJ7?š»+æf2ó‡HV僓×ÅöÐÑ¡WþéX3xá“ì æ•ÛêÑÃߊ‹X¹æïFÙªýûBT�„1!„ñä¤aðÀ«y7šLüœ¨Sp«ßêì0¥9;ƒOÐyû¹ø‡bÎN-·N��=ëXøóÏì³Aì,µ€-Q†Fxã½?�,t´©ïA~\‘ªŽz6ã�Æ|ðË~¶ä‚¿‹J‚íέß+ˆ7ﮋmû î]n¦a‡&Ìì߀ãßœbkc�Š“3Í›ÑÃÃÊ® ªÎ•–õ]‰üë Ÿ´áꬑ¬¸ñàÀ&Ü‘ÅÓ_gceV¿&ŒKÙÏÇ9~éB8gïÌé äûSZìÙ×ÎÌ+N�!kï&DÒ`Ôkè�î˜Òã1x–[§Zl!gü݀첯[’ÓX•Óš»šº°6ɇ®ÎÙ|¯¢ Ô5X8žSÑ4w…OÿP¾{&äì6�¢ò»Q�2•ûÿ>ÌëÇ,›,d«N¢×{¸àäÅ‹�táÌl8EQHIsBgÎdÖ z4äÇö{ÄòÞ¶LR*Ú‡rz¼43+~?�WÏPf>BVTnLd{¾Ìg¢¶IB8Ï&�È9¼…zý'÷èßäù«Lk~I¢Ê?õ…1‡ðlz#¹Gþ£qÇrëT³sØ�Æm-¼ø<1‡2dj!Ë÷å1²}ý=ëàv2†ífМ,dX� õÖC²õ¢mÎ,°`NNføâDb«�_Š MÄçXªtã€Zh&Ó’ÍW_ã—¢²¯g%&óö�)|Ä«ƒšðzá~Æ'U°�bÀ¦–�ÄoËËæ›UÙ|çáÅCýš1³·‰�¿¤—ÖBÔ( cBˆ+NQ*¾“ϯë Òÿ^NÖÞß ò y®L™èï^¥8éáNÇØ€ÌÖPœCƒÑÓʯTÍç»-éÜ>  «q|y$—x“‚·�; Õ~‹5“z,‘µ]š1¹½…Ÿ–æP`Íå·ãV>º)œañlÎÖðtWHO±Pd…Àw�Šˆ‹Lã`—žï\Èœ£djzê¹jœH»üå*¬)é¬ÎnŸÞA$þ�Åébðñv"7¹�t�‘ê)$¦š(Î/ ªztï£YIȱÑÐ�ާ‘atÆ–YŒK +Ö¬"Ò-&"³­8û8áR^Çž¨Q•ý=ˆkŸ„1!„Ãq n‚_×AÄ-�…1(×�¦eÊøÜp ^÷MEgp¥(áñËfáßup…kŒ¥Gžâáåõ˜Ð¥o�À[QÉÊ)`çÞBVÇšQ-¹,>PÄ æÙüvv‰ ;7cZ÷p¿»-“ ÙéÉLYËöýÉÄÜÁ+­ryj SWêyáæ†|}“WÕJô‰hž\k"õrÃŒ­€¯VF¢ïÆÛ£#ðÓ«d¤§2íÇh2�ž ¿=œ>>N8ÙÌœŽIàýý…¨6.ºOºYeÇ®6ÜÕ€yã°æeòÁÒŒ]›ðD}¡â&¨ù|üÍŽ‹¿nËæÍyÿ”ÙlÉÍâ“•Y|rá æ4^þ&�—Ë©ê¢ûÖ¬4^ýo¯ž»qÙ^WÜz!D-�0&„¸¢ª:$£è�iôèį˜MÌâ7Hùs!Þ­zž]òœž@Îá-§Fãßu0!ƒŸ½äçRê¼x÷�:©ElÙɛǪ6—Kˆš"C•×7 cB‡¥è� »çEü» !cûJ²nÄ’“”ÜqéÙ¤ F¿^áÐdU¨¹)<ùIJM4Y!ªM˜Âá¹7¹è#Ž„âj'‹¾ !„BØ‘„1!„B;’aJ!„ÝÆ³w„Âî$Œ !ìâ™&EL˜0ÁÞÍ®æÎ�kï& ÔB!„v$aL!„ÂŽ$Œ !„BØ‘„1!ÄUCQEaĈönŠBÔ cB‡uäÈV¯^}Ñ×UUeîܹØl¶+Ø*!„¨YÆ„éðáÃôìÙ“O>)ï1×%Ž?΋/¾ÈÈ‘#¯`˪C%7f{Nç;ô³.­™'Ù¹?SuwÔÌdÇ%’£V^TqqÆ„Çb±0fÌZµjŲeË.Z®E‹¬]»–åË—óÅ_\ÁV‘–ÉÏãoáÑű8L^Ñ,›Îm��S_Žæ¶W‘­�–ñ3cÂ0vev¥ÒºgÝ[NduQU�%„(�„1!„Ãùé§Ÿ8|ø0ß~û-...–íÞ½;/¼ð¯¿þúŇ+ÕÓ|ÐÃå윳ó¾ xë�£ sqlñ$îlUƒWßú´¿k:[ò.³Z5žywrǼ‹‡Cų5y„»Zº£Ôò±„%$Œ !ÎÂ… ¹ë®»ˆˆˆ¨Rù'Ÿ|’””Ö¯__~][~š¸¸8âNϨO†|uªäû¨5<Ó\_ƒ­¿|¶Cï3zÜRŒcðבãX¿€É÷u!ÜýrkV1›¬÷xš3üµW¸§‰síKHB8 C‡qÓM7•Ù>|øp†^æµ��êׯÏáÇ/R£�€BCC ñÇ]§àêwæû@<õyìýêqnm䋫чˆž�0ƒ„9š_þïnÚÔsÇàìJ�ÐVÜ:õwr4�ì]sy¸GCê�qñ ¤Qç|~´¼Þ6•Ô�ï2²[#üÝ=¨Ûf(36¥— ,ÖÈ#Dê»0êñ;i×8‚&íz3â�ÛÓÖýÌÒ�†�¸8ñ ïÂÈé�h-ÝY«ä\0³åÙôŠ‚âz?_8QÌü;�Õ æñ?ÌU8wÀ¼‰©í‚ð0ºÐüN¦ü�­ªÇBÆ„v (€¥¥¥Xfû’%KX²d O?ýt™×üýýÉÈȸ„Öhdþö4ƒžßC»w7püÄ_|xÓ^ºûI–§k@1;¦Ýͨ%nŒÿé1qGX|¯Âî=ÑjÂ÷<<ðŽwÿ€M‘qœÞñ-"wp$­ìàœ3Ÿï™CÞðï9sˆï§2kÌTÖäŸ_ιë@îôø�ï{™Å»“ÏŸX¯¦ph{<ßÙÍÉè£lœÓ�ô�‡2dæ,•ž €3Ýß;NAQEÙß3´ÂQàŠÏ§–Œ]¸�ã§÷óÃ(_>ü"+²/åXׯÊþĵO˜Âáøúú’žž^i¹E‹±uëVzôèApppõ¦e±jþRõï kKýú­ôæ{Œu[Éü_ÒÑL›™ÿM,}^ÿ„'z4¢^ÝúDÔ=3ŸJ%öç/Xëû¾5˜6au iTßrG=U¢–.`sØã̘ؕ`ÿpúLžDßü?X³Çr^I]½‘,ܶŒ§ìbZ߆4è8’·WÇ`>[ ߰†„…DÐîîWùúåÎúz!»Ì•œ :'F££‹sÅožû™ÆдM#B‚›Ð{ÒãÜjÚɶ#gºéªq,!®cò·!„p8�:ubÆ •–KLL¤W¯^|øá‡Ìž=›|°úSS‰K°Ö(ÙmN hT_#1>[nq9A4kæS΄vñ1ñи•O±²‘Ÿ„éÐ[tv7–¿Q,ËÏ&#³l/š1¢ÏÏ[ËÑøÃ|;4‡Ï†öeê–òn[ÔÔ´ ^Éq$Z*>—êN¤×*<÷r¸ÔÁ×­ˆ‚™)&DuHB8œQ£F±jÕª æ€ýËb±0iÒ$†Žª^Â}{º@BƒõÄ�Šþ·çÉÍ©…z¡AèÝýðsÉ !±¸¼�ñ ðCKŒ'©Ò2uÖ Ä¥óÛ,(¦¸¸˜âbfK6ß ¾øø�âÁ“gðHÓXÖýyk™9ññäû×%йâsÑ¡ÇÉIÃl2Wib½Rá¹—» ZiÝÕ;–×3 cB‡3|øp:vìȈ#ÈÉÉ©Ò>?þø#7Þx#G�­ÞÁ”: 7mѦ.=@\Üa~yõE¾)º›‡ ¸Þ½]X9m2?ìO 9ú6ïM.�¤®§ÉÝ÷pñ¹¼0{#Q)‰Ý´�HSiüP\psƒÄƒ{I(ÖÑh躛ä÷Öp(.…Ô„ìÚ{Îðc óþżÿõjv�"&ú[ÆŠ“n4m‚@Ëçàº5ì9OÔŽ<ÿîFBïÁ�†JÎEçGD„ÇVýÌŽ˜DNïÝOL±‚›»¶èýìI½ %ž{%Ê=Võ~5B\/$Œ !ŽN§céÒ¥deeѽ{wöîÝ[é>M›6e„ äåUw1.ß�óë{Øóâ-4iÒ�gþjÁô_ærO€Jîžýo´ÜÃÔÞMˆè8šyû Áà‚A§VϱhÁ}XŒ¢MƒfÜ>y5Iš Fð ïi²õi[�Œ®ñx-{ÏeOгI(¡Íoæá·rþ\�ÜÌ(v}ówiN£&üÆaÚL[Æ'÷ùŸ�«–úç4µkD‹;ß"æ¶ÏY6­ÆÊÎ#}^|—Á¹s¸½q†½É !ƒžäÏe<4éW ÎûÑT|î+ïX޶ž›ŽAÉÊʪròºuë2dH•Ê._¾¼Êe×­[G—.]ªÚ |||ªU^á ‰‰‰aÆ L˜0¡Â² Œ1‚­[·róÍ7Ó­[7<==9uê<ð»víâÛo¿%""‚õë×�¿¿Í·9)’DÕO¦ä]|þÄh~볞¿_j…^Í$úDÆ@ÜÈåäÿ^eÔd+ïý/ƒ½k¼)%KO4xea4Ÿßn¨¼üeªðÜkýè×¾¹sçÒ«W/4h€›››½›sÝY»v-}úô©RÙêäšåË—W¹^�ž1!„ aË–-üöÛo4lØ�õë×óÝwß…··7�:ubóæÍÌœ9“¢¢"fÍšU ­°rtáÜÙ¾uýh|˳l»á¾�TF´´õLÞ�¡ø…´eا&úþ}î®� vÅU|îBˆšádï!Deú÷ïOÿþý/úz�:u:t(»víª…£;ÑqòŸœœ\þ«JÐ0¾Ø?Œ+ödLC_¾HJ¼B«øÜ…5C˜âšðÓO?Ù» BqId˜R!„ÂŽ$Œ !®8�N.=Bœ!B†)…W”^¯?{רܹsíÜ!ìÏÍÍ ½^n‰¸žIB\Qz½///Haaᥭš_ƒ4íÚ\¾&ÏëZ}�µ½ÏK§Óáææ†———„±ëœ„1!Ä¥×ëqwwÇh4b³É" µírB™½ÃÊõ@¯×Ÿý×/ cBˆ+JQœœœpr’Ë�B€LàB!„°+ 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Ÿ>}ìÝ!„B»�aJ!„B;’0&„BaGÆ„B!ìH˜B!„IB!„°# cB!„v$aL!„ÂŽ$Œ !„BØQ­.úº|ùòÚ¬^!„âªWka¬wïÞµUµB!Ä5£Ö˜··wmU-„BqÍ�9cB!„v$aL!„ÂŽ$Œ !„BØ‘„1!„B;’0&„BaGÆ„B!ìH˜B!„IB!„°£j/ú*�8B!„¨9JVV–VÕ¹¹¹µÙ!„Bˆk‚——W•ËV«g¬: !„BˆÊÉœ1!„B;’0&„BaGÆ„B!ìH˜B!„IB!„°£j¯3vòäIòòòÈÈȨ�öˆJøùùáííMÆ íÝ!„×8yϯžK}�®Ö:c'OžDQÚ·o_튚³gÏE¡Q£FönŠBˆk”¼ç_šKy�®Ö0e^^žüR@‡d^!„µJÞó/Í¥¼GWk˜2##M«rGš¨EÒe,„¢6É{þ¥«î{tµçŒÉ/F!„¸>È{þ•Qí0ò˱7EQìÝ!„× yϯžKy�–¥-„B!ì¨ÚaLÓ´ÿRÓ—ò`D#ù% UÓÐT3EŶZ9V•ŽI_6Š‹LW¬ÍB!Dm«í÷²â�ÏÑ"l0_ÇUöžoaïôn„Þ<‹CV{¼Ç×î{t�„1Kâ&æLèOûþxyùÓ }&|¼™$KíÑ’þcÇÒ¯¹Øâùb@(¾ˆÅv‘òù¿Œ£¾—+F£7O?B[öàÞðO–zi?´s�I?x ¦ßDØKÉsÐ_´BQ]—ô•ÿ#}ý¸ý““X/xÍ;Ÿ�A>Ü»8MÓpjx�Jû:J%õêí>šq#o¢®r¥ßãkÿ=ú’&ðŸ{ 5i�õ~˜�ã™öÍtn ÖHø{3^»›Û~ËúÏXÙð©sS†ýßÔ’úU&“õß“*§¸-?‡¼À±ü´ñunTòI�ÜÀ§/ü‡»¢ôü³d4u«1Ï=þ% «É„Må¢mB!®6—.ÔÔ$’-ùlŸ5�ÿ�ú’�>gB@.ëÞ™ÁŸÙ6nLMǦy  ¹“ç_Шì0¾=Æój�ª•=Ïe¿Ç׾˜3VÈæ™SXæ1‘—¾Í˜[Ûдi[z=ø.Ë=ŠÓ’)ÌÚV XÙ7­#÷ý@~éžæ?Ÿ""üIÖ™ó:ž �à©?Íg^eë ÍqwuŵÎh–›Ê9´Þ ßê…Óêæ‡˜öD7 ·oå ÕÄÎÙø©E(~^^øEÜÏ’4 Š#Y:ùnÚ…úâЈî÷¿Ïæ4µôp¿è?ÏüÀ‰³mÈçП¥ß !øúøÚî1~N)ùåÿú®®¸º7âÙMf@%móû¯¤ÞÚÕx5ýSòpÃÍ­â_ó~pÖ ×S¾á�atðöÄÔ®ãb��×H¥·~Û”EGغý"Á#§K“Ê-:÷£y¬};·�ສRÒ/ªÚÆS+uÀ*nw&ìí£dZ,XÒ?g˜KÍ-À²-œ¤ÿòɺx´;ÓN[DÒw{Ⱦo)æØŽù<`,`ÿœ<¿Ý‹)›qâÛXÉ^BäØœ³Vÿ qoŒdÒÞö¼¾í$§v¼J›]yaY"Ū�ìmÓ‰˜v�N¯máPÂv-ŸBß ¢âúp,¿X,X²N² �3Öóg\äRò†ÎÁÓñ¬šÁ§g±#Ïî7�jmMU­i T[¿BÑ�ê²EgÍÌÄ¢eâËÃÉŒ]ÄΜ’ÚºwI ÉCg2µ—'Y—2J�"Ù�c½HÂ�TºÍßOÂéãìþ°ÉïŒg~\a¥]'UU±e'r(!€{"éÔnæÜ±Ÿ…ŸšypñNŽÛÍÜ xÞˆþ¼Z�Ç{,›Í, æoçæáÏc“Gh=ÁÂÑcÙ ›ÀÆ£§Ù÷AŽÍË»ñE�²MY¿0fÍ"# ¼[y£©:MK|¼ +#«ú n¿•gw­<œ¡ Ñ:ãêꊫ‹J ‹´¥,cˆ¯ £‡ŸÐ1lpClÌ3(* ÐÄÔ†Ö&#>íZ£/ŒcÕf½ò.QÝýñ½cSÞ{‘vûW²ñlqåñ âX½îƒ¢g3¤½Sp$SGrtÇ2¬Ùl]±"ç1ox(þÞ­èÚ“R²E㌋«+®..h+ç6ýƒ}­Ç1÷ùx{ú1`Ê_ ÏßÍŽÃEÕÃhÙkR¾Æ*k¨d…Bˆ†U— R`É¢ ¹'­ú½È³­ÿÅâuç¹nþ'K6x0vâ@üŒz 33¹R­à„Þ§5ÞÞm2‰¨ÐL8_zö *ÕM�£� £O#Ç Ä󺞀жø´ aäèûÐ�:Æ©¢ªCÁÉÅ×ë‡Y0a!yOÆòá#>X�¬eMB7&½9Š;MFÚ?<�ñÝSص+±Ú¹·›ï̘â‰ÑIé—Jö~íïh½„9béÜŠ1§š¹–ð}M Êïg³6!Âb¥d™j¥5W¬›jkB![íÃ…�ìË9 ÷ ™S;¢&†óѼOXkÞË�° ,rFý®9jN6ÙV]Y °§ìkÅ€ÑWò¯”¼ ²¬>–³òÇ�Ò´9M ÏS`+½Ö´M‹R)¬1cä²çõçùÌm [_ïMST®¥§b.Œczp ¢ËÖnSiu§«ª6øç€Õ/Œ93 ¯�Ï6|ɉç^&ĵâ~‡×³ù´‰~ó;¡UÁM玚•A–UE§);DW6ì»B´N*E׊°©*5�ýWU´z|ðs¶»­ä«òoRy�Ñyã­Ë )9[w= `Mù™óª]½”J])ÜM˜š¹ìË»W8‹ƒ&H=wžkjM*. ÕhQ‹®QhSqWZxµÀµÛ3ìß9™vÚ*OuÍ�s6YŨª¶"ÄUùA“0&„ÂQêÆrsòPÝu¸ƒ_ rÎCL^äÅÓ‡aòÝÝ /›\›Šo Íû`VR m¥!ªôžö×Ëš õSE©¨§•2†�Ü=³yi½73wM"Ä¥4Ì™¼0¹‡3ãÄWŒ6VI�P‡ëötÜ?}ýS2jÌ{üûp2ééç8´åoDE}LÎ�9Lý“ Å¯ç=´<²Š6#Ålæ‚9§üP_åyâïß”3Û7Ÿ’NòÑã¤Öo–4éCÔ¨–ì|+š/¥�vv‹¢cH ‹"¢jJrëKd„Ž-o¾ÂÚƒI˜3.’ròg,*(ž züA®¯{�W¿Œ'9=�äÇHÎÓàè‡öÈ7l:–JÚÏ?’�®8,’»ÏÄýÑN.\"#í,‡ŽÿB€ó]„u¿Ê>Ž!îlæ´T2 $v !„¸•©\ÍÍ7÷’Æ„K7ÆOIŸˆiÃ4m[&lKg‚£ç!„BÜn” ìÝ´ÙÀêÕ»|ùòï~ŒøãÈë)„¢1Ô¶3ö{sAÙsߎ¼3f0°Ùl(Š"{È¢Ñh°Ùl GOE!ÄmLj~íÕµF×*ŒéõzèÔ©Šr£¿þ-‚ÍfãÔ©SèõzGOE!ÄmLj~íÕµF×êÝ”‰‰‰äææÖª)þ8ƒ½^Oûöí=!„·9©ùµS×]ë0&„B!þ8 ýç–„B!Äo�0&„Bá@Æ„B!H˜B!„IB!„p cB!„$aL!„Â�$Œ !„B8�„1!„B’0&„Bá@ÿòãà­{22„IEND®B`‚Projecteur-0.10/doc/screenshot-settings.png000066400000000000000000001714721451344070600210620ustar00rootroot00000000000000‰PNG  IHDR2bÝB(®sBIT|dˆ IDATxœìÝwTTGÀáßR¤÷"RD£Q{7j,ˆ`ï515Qc‹]cì]cÔ˜/“K4ÅJìÆ†bM”¦ (Röûƒ°‘P�ø>çpŽ»wîÌ{﮳ïÎ�;«ðê7F©u� ¤#„BQ¤©i¢^­!j’Ä!„¢¼ÑÈH%íúIÔ$‰B!Dy¤™‘ŠZi!„BQT’È!„¢Ü’DF!„å–$2B!„(·$‘B!D¹%‰ŒB!Ê-Id„BQnI"#„BˆrK!„B”[’È!„¢Ü’DF!„å–$2B!„(·$‘B!D¹¥QÚñ¦244dÒ¤I$%%±`Á222J;¤rÉØØ˜ž={R­Z5:į¿þZÊQ !^Id„x‰iÓ¦amm�ššÉÉÉ$$$ÀÁƒyöìY‘ëÕÓÓÃÔÔ}}}455III)r]FFFŒ9555æÌ™SäzJJI�C€�âææFxx8qqqÜ¿¿˜¢B¼Š & ©©ÉÊ•+IJJ2¿À�5ŠçÏŸ³hÑ¢biG!^ÂÌÌ 555‚ƒƒQ*•899áíí�³³3+V¬(r½QQQÌž=›”””WJb´´´°··'&&æ•ê))%uÕÕÕ©V­)))ÌŸ?_Fµ„(C455±··gìØ±,]º€1cÆ`mmMxxx±µ#‰ŒôÝwßñàÁlmm™6mîîîèèè�––Fÿþý©\¹2&&&¤§§³iÓ&ÐÓÓÃ××OOOÔÕÕ¹|ù2Û¶mãáÇèêê2cÆ žùªW¯ÎÍ›7³ÕŸUþÎ�;ªz¦L™‚‡‡ZZZØØØàêêJLL 3gÎ$---GŒIIIlÞ¼YõØÍÍ-ß: êßÇö"oooªV­ªz¼eË–<ç©ö4þ”””"wQÛ{öìëׯÇÔÔ”yóæáêê €““Sž¯ÑË^_MMMUÝ/^äêÕ«(•Ê\Ï£â’ÈQ@ÎÎÎ$''Íùóç9tèP¾åMLL¸{÷®jîFDD...XXXäHd²ÊÛÙÙ±lÙ²lÛLMM±´´ <<<×$&¿òª3«…BQ úrcmm�-‘ÑÖÖγlQÏa^ñ?zô¨T÷KHH@__ ß×èeu‡‡‡³oß>ÞyçF�ELL _ý5!!!¹Æ*DYg`` º´ döü1K—.-¶QId„( iÓ¦ñàÁƒ—Ï*koo�ššJ¥€\/Ïd=Ö-[²M\���ÅÈÈÈü TWW'==]µ=ëÎ]]] …ê›üËêÌ000P]î)¬‚\¢ÊRØsXÐøKk¿�˜<|øÈý5zYÝ;vìàÀ�´jÕŠvíÚÑ·o_fÏž�k¬B”u£G�V%1Y“}³›Ñ£G3wîÜbiG!JHHH!!!8991mÚ4RSSqppàÁƒ\¹r%LJâÍ›7¹sçvvvôéÓ‡¨¨(ttt¸}û67nÜàÆ�DFFbmmÍìÙ³‰ŒŒÄÄÄDõÍ&>>SSSÆ�ÏãÇ�ñ÷÷çÆ�ùÖ™ššJDDöööL˜0A5ÂPV¼ìœä•�¼îý²ä÷½¬nºtéBTTzzzr–(מ?NDD«V­R�¾,]º”Ñ£G¿ò�š/’•}…(!J¥’5kÖpêÔ)ôõõ133ãÂ… ,_¾œÔÔTÕœ‹ÔÔT óCkÅŠœ8q===jÔ¨�¥¥¥ê[}FFË—/çôéÓèêêâîîN… Ð××G©T²qãF"##qttÄÙÙ}}ý—Ö °aƒƒ±¶¶ÆÝÝ�¸¸8‚‚‚^ÿ ËEAâ/ û½¸^¯ÑËê®P¡zzzÔ«W�jÕªqãÆ 6mÚT°%D´hÑ"æÎ�›mÒzRRsçÎeñâÅÅÖŽâ�wÞ‘ÙdB¼fÞÞÞ¸»»ãääÄ•+WŠõVD!„x“È¥%!JA­Zµ¨X±"7oÞä§Ÿ~*íp„¢Ü’!„B”[2GF!„å–$2B!„(·$‘B!D¹%‰ŒB!Ê-Id„BQnI"#„BˆrK!„B”[’È!„¢Ü’DF!„å–$2B!„(·$‘B!D¹%‰ŒB!Ê-Id„BQn)b¢äׯ…BQ.ɈŒB!Ê-Id„BQnI"#„BˆrK!„B”[’È!„¢Ü’DF!„å–$2B!„(·$‘B!D¹%‰ŒB!Ê-Id„BQniv‡[!!$%%Wñü瘙›ad`ˆ““ci‡"„Büçê·–n…„ ¦¦ŽG�z%ÓÎù³§A™�³$3B!D±*T"s!0�­Ú”d<ÿYÚ�¹™9ÉÉOxüøqi‡SêôõõÑ×ÓÇÆÆš;wï•‹óòbÌB!ʆB]ZŠ‹�ù­ì"‰‹�ÃØÈ'g(P(J;¢Ò£T‚%w#¹~ã&*h–ùóòbÌwïÞÅÖÖ¶´CBAæÈˆ¢³µs@dd¤—v(¥NMM [{®_ÃɹJ¹8/Y1‡Ü¾YÚ¡!„ø[¡¥ É™E™ÿ°~]222PSS'%%¥Üœ—¬˜Ëú%0!„x“ÈíׯQY½lRZ²ÎGy:/å)V!„xþÒ’ È!„¢Œ(ˆŒ²ÌþÅÇÇqñâÅ"íCRRb Æ'„BˆâV®/-=}ú4Ûãõ60~üxÕãÎ�}™5kv�êr©Z�ÓgÎä¹ýÙ³gE R!„%¦Ä™K—.Ó¦M[ŒML153§AÆ\ºtù•ë�óÙgôéÓ7ß2ݺw£E‹¯ÜÖ;ï´æËuë^¹ž²¤ÿÀÁ,Z²´´ÃB!^IæÈî2‰¯Ÿ/õëÕçÄñc¤¥¥qþ|v¶6…®çßž§¤ T*³×“õÏ¿ŸëÓ«Wábþw}KIIùû Ñë½D4÷óy¬]·žÔÔTôõõ©V­*~¾¾ ìß �W»sÞ§“7ÖÖåsa·z 1zäHúôîUÚ¡!„(e%:"óäÉBCÃ0 ?ÕÝÝy»fMˆ‰‰ >�}±¶µCGO÷·ÞbÅÊUddd¨ö¸p�fÍ[`dbJ­Úü²{w¶úÙ½5 MÔ449|äHŽö}ýº0mútÕã]¿üB577´tt±²¶¡u›¶ÙÊûàtõ ¨dcËò+³m;nœª­×%6.ž¶mZtå¿ïÙM¿¾}X½z }ú =ýÕnWîâçKƒúòSB!Ê·B'2…™Þª«§GË–-9j¿ìÞMZzz¶íG�ã³9sºÆìY³˜=g_¬ý%Gë6miܤ1øøã�éѳçTûwìØ‘'�“xò8‰&MšäßÑãÇéÙ«7'LäÖÍ,[º„ÃGŽd›Š;æã1ܸÌêU+ùdüxΞ?¯Ú¶pÁU[¯sª¯––¦¦¦T«V•^=zðó¶9yê?oß¡*óÃ�?Ñ QªTs£ÿÀÁÄÄÄP§^6»EUnßþýT¯Y‹ŒŒ  ¼ UÛvïù•ú cëàH½†�ˆ��Ï·î‡ÒwÀ@œ\ªá\Õ•Åeä2ÕŽ�»hÒ¼%ÎU]éÚ½'7nÜPmKOOgÙòT¯Y »ÊNxxÕcîçóJ1Z!„¯ªˆ#2ÿÿe×úõíËÇcÆàäìÌš5kP*3Èúx···ÅÞÞŽ.]üøtò$¾Xû dÛ¶m˜™™2gö,0 >>�ذaƒj_55ÚÚZhkk¡¦¦àŸ”áß)„’5kÖн{7 €­­ •+ÿ«,¸ºVÅÆÆ??_êxzrüØ1Õ6 uU[¥yÇRgg7jÈþ8rô(“§Leîgs8~ô0)))L�>€wÛ¶áè±cª}�9FÛ6­QSËþ²Ÿ<õ#Gĸ±c8sê_¬Z…©©i¾u¯]·ž¤Ä$N?Êé“ÇéÞ­k±gQ?~‚�ÇŽcìÇ£ñ?°77WºõèÅ“'OX¹j5›¿Ý—_¬æÌ_'iÿn[.\ ,娅B¼ŠŸì«««ËÔ©S¸}ë&Ë–.eÊÔi¬Zµ:ײի» @Xx8NNÎ(^X�ÌÅÅ…°°ð"ņ»›[�Ë[V´$>!¡Hm•4kkkbcãØ°ñkºuíBËͱªX‘QŽP%9íÛµãÏÃGHMM`ß�´o×.G}ßlÚL×.~tëÚkkk<==^Z·¡�Ñ÷£‰‹‹ÃÜÜ{{û×qèùÚ¼e ]üüðíÜ{fNŸ Šù«¯ÿÇøñãhÒ¸1•¬¬°¬X±4ÃBQ ŸÈq ü|}éÖµ+¿ÿ±7×Á“°°pl¬­A öö„ܾ�2C©Ú~óæM*;8€´µ´HIyžûÀGöÁP‚µ�5wîÜÍm°&×XÔÔÔTmkik¿0á·ˆÅèÞ½{˜››p?ú>ß}¿»ÊNØUv¢Wß~¤¤¤�””D½º^hiiqøð.^ äáÇ4mÒ8G}áá8::æx>¿ºßo(¾�;Óµ{O:vêÌÅÀKÅ{�Eq玎•U�ÕÕÕ±··'âÎ]ž>}Jll,Õ\ª–VxB!J@‰.ˆ—œü„ p!€°ð0:ÄÁC©îîFÖ§ûÞ}û¸r›cÇ�±pá"úõë (éÖµ qññL›>�аP6oÞÌ/¿ìfèÐÁ€—*U8wî—._"(8ˆÈÈHŒŒ  #66†²‡ÌXºué·[¶°wï^¢££9|øð¿ŽçßÇ÷Ï¿]ªTá÷ßÿ <<œÓgNóüyJ¡ÎCqf27oÝâø‰“´iÝÈ�°Õ_ä�p PWW§‹Ÿ/ÛwîbûÎ�tòö¦B… 9ê´ªh¥ {Q~ukjj2iÂx.œ;CÍ5èÛ@±cQÙÙÚ¦zœžžNDDöv¶èèè`dhȽÈÈÒ P!D±+Ñ_¿Ž�OàÔ_±|Å beU‘Î>>|6çŸEêŽ?Áÿ¾Ù„¶¶=ºwgò¤‰˜››³oïïŒû «V¯ÁÉÑ‘·~OOO||:±c×.š6k�®®_¬^�Ÿ¯›6}˘qŸðí¦o²ÅÒ«WOÂ#"6|8‰‰I8;9addT ã?~}úô£z�šXWªÄþý{3G†^ƒ§OŸòàA ±q±\`áâÅ4¨_Ÿ®]üèÕ³;Ã?EOO¼¼êðôéS N�°ôïÛ‡wÚ¶CGG‡­[¾Íµ�¾½{1äýaÔ«ëE“Æ�ˆOHÀ±rå|ë¾zíF††˜˜˜àîîÆw[·’žžŽººúk9/>$2*JõX_OŸþýúÒ§ßš6i„Gm¾úúkUÒçíÝ‘%Ë–áR¥ úúzÊü!„(ï 1Q.8xÈ¿.])®SóŠlûq+­Zµ,–úò“˜˜HLl,f¦¦$%=fþ‚…Ü‹Œd׎ŸK¼mP°cûÏ´hѲP·M>o>_|¹ŽÔÔTtuu©êR….~~ 8MÍrÐÚÆÚuë ÃÀÀ€OÆŽaà€þªíÝzô"1)‘}¿ÿ¦znÀ !¸ºVcòÄ ü´ígÖ¬]KHH(f¦¦lýn nn®yÖ½xÉRÖmØÀÓ§O±·³gÂøqtöñ)ÔYQWWçâÅjÕò(Ôy©×°Q޹RÃ?ÆÌéÓØ±sËV¬$22’ZµÞfÞgs¨Z5órRRRS§Ï`ï¾ýhjjbldDÕª.|ýÕ†BÇìéáQà}„B”œ¢%2Å´0œ©EE¶ýðz™Ã‡�0xè{DEGcllD³¦ÍX¶d1•*Y•xÛ(Š–Èü×5‘)ª¨èh”J%úzúÜ¿͘qãñîØ�aï¿Wà:$‘Bˆ²¥Ð—–ŠsÞj\Ìýb¯3/Íš7ãö­9žm‹²açÎ]¬þb-‰‰‰XZZâëãÃ�ÁƒJ;,!„¯ sdJà6!^ƒÃ?`ÄðJ; !„ŨD'ûŠ“$0»Üî�/ëÊKœBñf(Â�F"}y¥¥eÈ™(•™‹–§ó’³Bˆ²A.-½Fééi¤¥¥•veŽœ!„EU¨Dæß¿Ñ# H –––œb$?üø§Ný…}egâã^Zךµ_ҧ߀BµUPÕÜ«sæìÙ"íû¢�»~¡i³–¯\�x3•h"³s×/xxÕÃÀØ [{G:xûðôéÓ’l²ÀŠ[zz:†&æ\½z-Ûó­Û¶g݆¯²=׸aCz÷êQìq ñ_”žžNÏž=qww'%%%ß²e¥_Q*•|öù<ªTuCßÈgW&Núô•ëÍ«Ÿy‘£“#C‡ÆÈȰÄÛº|ù mÛuÄÔ óŠ•hؤ—/_y¥vÿM©Tk}âÍRb—–n‡„ЫO?>›3‹Î>�HˆOàÊÕ«èèè”T“%›R©äùóç9žOyž³ã­[׋ºu½Š-f!þ˶mÛFPPãÆ�CKK+Ïre©_ùvËw,]¶‚o¾þ ww7îÞ½ËãÇO^¹Þ¼ú™YU¬Èô©S^K[~]»S¯^]Žù“´´4Î`[—©„(ª‘¹qã&Œ1œ*ÎÎxyÕaÐÀìÛ5Þö@×À÷·Þæð‘#ªçÞ¾ƒJ¶öèšPËÓ‹#G�ª¶uö늭½#z†&¼U³6+W¯!##Cµýôé34jÚc3Kê5l̹sç ÛËêð¬[ -]Z¶n«znÜ'ÐÐÒECK€«VÓ´E+ÂÂé۠V6vèèå8ÞôôtæÎ›�µ­z†&8U©Æ”©Óˆ�OÀÇ· Æf–˜˜Wdögs þ"QF½ø <==�Ù³g£££ÃøñãóÝïUú•ÔÔT¦MŸ‰£sU*ÙÚ3xèû$$< JU7Ö­ÿgTµYËwX´d©êqÛv™3÷ólí\»„»»�¼;RÅÙ™æÍšÑ±C{ s>KýFM°¬dƒ‘© ›4ãÀÁƒŠ%KnýL–ÀÀKhhéª. %$~ „(kîß¿�¯¯/K–üóÁ•53bÄ*V¬˜ïþ¯Ò¯Ìý|>?üøß}»‰CöÁà¡ïвe Nœ< d~Èž;wžS§þ2ç�œ>s†V-Zdk§Cûvœ9s–1c?áÎݻٶ=Lxȹsç9ì�€s§ñí샷���—^K–Üú™¼ôìÝ—»wïqâèa/œÃØÄ˜ë7n¨¶ÛÚÚpèà>.]<�¦¦&£F\ ¶ôôôhÙ¢£F�a÷ž_IOOÏÑöÇ�æzÐV­\Îø “T¯‹UÅŠü²s;á!·˜=kcÇ�çæ­[L™:�ƒ‡üùm÷.û¤ÖÛ5Uõ)•Jzöš—/°dñB† ÿ� àà—žñf*±DÆÌÌ”¿NÇÑÑ�6íÚãU¿!‡üý³•yûíšØÙÚòéä‰ÄÆÆ|€JVV8;9QÑÒ’�>ÅÝ{÷²Mj³··ÃÞÎŽ.~¾Lž4‘µk×°å»ï±ªhÅ'cÇ`iaÁÔ)“¹™ãzn~±¤Ž * ­­M… TÏihh ­­�¶¶vžç¤²ƒ•˜>mJ¶ã]³f-3¦O¥e‹ØX[cUÉJµ�¡‘!Q‘QÄÆÄbia�cåÊ…y„(Sôõõ9}ú4sæÌ!&&¦P£1ðjýÊú _1sÆ46l€»›«W-gϯ¿MÛ6­ùóÏÃ?q‚jÕªrüÄ 2228sæ,ê9.7iÒ˜#þ ÃÕ½~]»sëöíleªº¸àìäÄøqciÛ¦5ë¿úꥱdÉ­ŸÉMPp0‡üýÙ°~-Õ«»ã`o�‘aö¹3ÆÆÆTuq¡Š³3�Éñ'³mϯ­];¶Ñ·ooÆŒýgWÖ¬ý2ÛˆškµªØX[ãçÛOOŽ�ȼÛL[[›5ÞÂÜÜŒ¾}zckcÕ+Wؼe 3gL£AƒúT«Z•6­[«ê»pá"'Ožbù²%XWª„wÇÔ«W—ßÿØ›ïyo®�ìkogÇÚ5«¹J'ïŽtðîL@À…åtuu100 >!s4cÛÏÛ©]§.Æf–4kžyyæyjî×q«»» @Td7nÞDÏÐ=CLÌ+¢P(²u/‹­0uÕ‹Ç›œœÌƒ˜ÜÝÜr-;zä‡ôìÑ�6íÚÓ¤yË<¿… Qèééñù矓˜˜ÈŒ3 5“¥(ýÊÓ§OyCggÕv—*U §Mëwˆ‹�'0ðûö`ÐÀØØØpöì9þØ»�wÛ¶EC#ç” ê³kÇÏܺ„ºº:­ÞiË“'¹Ï“©îîNHHØKc)¬°°p´´´prt,PùŠ––$&&æ:º’]]]¦~:™[7‚XºdS§ÍÈv™èßu'üý¥30ðï´i‡™¥V6v<ˆ‰áùóç$''“�ð�ʹր³‹«ª>}ú ÷£ï(^ñæy-·_ëèè0mʧ8ØÛ³ÿ…ëÄÙQS T*yøð}ú àÓI‰�‰æÈáCùÖ®Z{ÀÎÞŽ5ÞâIb‚êïÙ“DÕuë‚Ä–_êêêhhh丫B[Kû¥wZäu¼ºººåšÎR¡BfÍœNè­xÔªE§Î~…jGˆ²¦ÿþxxx°víZzõêUàј+L¿¢££ƒ……y¶“¬ËööЦõ;ü²g¿ýþíÞmK»¶mùõ·ßÙýë¯tñóÍ7–JVV,[º8×à,aááØØX¿4–¼ú™¼ØX[“’’BLLl�Ê«©evûJ¥²Pm) ü|;Ó­kþøc_žug�Ö öo׬Á½ˆ0"Bockcd&Fææf_¿žkövv¨©©q7 /ë IDAT¾\õ¸0£1EíWÞ:”Y³?ãäÉS\ bä¨�騡=Ö•*Чw/Ö|±mmª8;ÓÅÏ— 7rçÎ]Ú½›sÂíÎ]¿°{ϯÜ !øúu–/_‰¶¶6ŽNÿŒŒlß±“ðˆ¾ßú¿ìÞC¿¾½_K^ýŒ‘‘W¯^˱f•»»înnŒþx wïÝ#0ðááÙ'óæ%¯¶²$''³`Ñb.\¸HXx8‡üý9xðÕÝsA~QV?õìY iii( Õ¶aï½ÇŒ™³9pð QÑÑÜ‹ŒTm«Qã-jÕz›÷†}Àի׸ɉ] âE%–È$�™\ý²s;´y·=nÕk2êã1ef 2Qö(b¢ ¼ÑÁCþøúùA).^dfiÅO?|O«–ÿ�U ïEF¢T*1Ð7 2*’÷?A?_>=ª´C¢X>ü'­Úä}‰77‡öÿNóæ-^^° ¼„gÝú<{’˜ëÜšâvóÖ-ŒŒŒÐÔÐäêÕ«tëÙ‹ßvÿ‚‡Gío[ˆb¥P°sÇÞiUðÏxùÑÈ2à‡bñ’¥€“§NóüùsRÓÒJ;Q†hjhP¡B6¨GO�Ò'IdJѵ  >zH샘Ò¥ÈÌ--056ÆÕÕ­´CB¼‚ß~ß˾  ..®´Ãe�™™·Cnó &†öï¶-ípT$‘)%ׂ‚ÈÈÈ æÛž¥J6þöÒ¥{¯BísþÜi‚‚‚qss-¡¨„%éÜùö8È�J;Q†ÅÅűÿ@AEKK<=j—vH€$2¥æá£‡4oѺ´ÃÈ•R©,TyϺ;|¨„¢B”´“§NPÚaˆr" gg'IdÞteúrR!€˜˜2|~Åß«¼o.\À¯kw,¬¬11¯Hý†MøyûŽbŒNQPe*‘)‰Î+==óŠ\½v-Ï2—¯\¡]ÇNXXYS±’-Mšµàò•+tíâGófÍ^)†‚ÊÈÈàÃ?ÄÚÚ===¼¼¼8zô¨jûîÝ»ñôôDGG¾ù曊D (ÙøõF"ÂÃsl�gý†õªr¼äâ’R©d㣰«ìLmϺ\ Tm{·ƒ7«×|Q¤(SRR4x(ÃÞJèíl\ÿe‘ê)ÏŠëýùù¼ô8¸"zu!!¡ôé7€^={pþÌ_lßö#]ü|ÑÑÑ)íÐ8zôÍZ¼ƒ¡¡!?ÿø�8ÆØ1ajbRÚ¡ ñF*3“}³:¯9³fâÓÉ›ø„®^½öÊ�—R©äùóügWwíÞ“zuëräÏC¤¥¥p[[[zõìñJ톚š�:ubÔ¨Qèèè0oÞ<ú÷ïOXX¿þú+ãÇ�§nݺüôÓO 2„Æ�S¥J•b�C©Ì`ð � 4ð¥å@‘o}'OýÅù€ „Ü æû­?2sÖvíø™ß~ÿƒ;wî0ìý÷Šgxx 2dð ´µµ‹TGyW\ïÏ”ç)ef’÷�›7ÈÈÈ`Äðahkkã xÕɾÞÒÛuˆˆ¸ƒƒƒ=kV® Y³¦lß±“�>KbR.Uª°lÉ"š6mÀÝ»wù`Ä(Ž;†•UE´´´¨W·®ªÎÓgÎ2~ÂD®^»FµªÕX¹b)u¸»¹Q³F 臉±1�™ä̘5€ÚžuÑÒ5Èö÷äI2)))Lž2'lœ˜0éÓ"_ÊjÛ¶-®®®èéé‘‘‘AÕªÿt�ëׯ§gÏž8991~üøÌóvãF‘ÚÉO®z_ò—uuuÒÓÓIOOçyêsÔÕÕIMMeâäOY0ïs455óÜ7&&†~QÉÆžªnÕ™¿`ééé<}–9Zgdj�–®Œø0Û¾ñ øv醙¥æ+ñÙÜyªmÛwìĽÆÛ›áöVMbãâ8tÈŸºõaae�™¥`ó·ßQ½f-,¬¬éÒ­G¶ ¢§Ïœ¥ió–˜YZѰq3Î�?¯ÚæãÛ' ŒÍ°«ì̪Õkò<Î+W¯ªÊ¶i×!Û„Ö¨¨(zöî‹¥• ®Õkðí–ïTÛ^|ìÞó+ÕkÖBßÈ['Þíà�­�á#Gadj‘k<{~ýMõ¾>zôXž±–4///ÌÌÌðëÖƒóy¬m²ù›¯ ºˆogz÷íϳgÏhÔ°Gûsëú5š4iÌÀ!™I²R©¤k÷^èèhsæÔ ¶~÷-ÆFFªúbbbèäãKg‚¯^¦uëVôìÝ/Ç— ðð®ñþÐ!¹ÆO»öÞ4jÔ�s§ÿbô¨‘ôîÛŸ€ r-ß«O?>|Äaÿƒ|µîKÖmøŠ5_¬2ç4=vÿƒû¹t…aï -܉â Qf™ü:¯„‡9wþ<þ÷sîô_töñÁǯ«jöÿáÃG6üC&OœÀÙ¿NñVõê´ë؉Ç�Ÿ¨ê8~ô0�âcømÏ/ÙêÖÓÓ¥E‹æŒ3–=¿þ¦ú�ÌÍéSÇyCÔ½\]«1bøèéé2wÞ|~ÿc/{ÛÃÞß÷°}ÇNþ·is‘ÏEÏž=±°°`÷îÝ|ñEî—]8€¾¾>õêÕ+r;y)îD¦~½º4kÖ··ÞæÿÛÄìY3X·~ÖÖÖtìÐ>ß}óëè³ÄÇÜçQ| «W®ÈöüŠ•«HLLäJà‚®\¢oŸÞ=vœÁCßgÊäI] 䛯7bnfƵ  ”J%ÏŸ%àìêÕ­Ç¡Cþ|‘yy�K- ôÖuN?B¥J•˜7!³gΠwßþTv®Ê‡#G“‘‘‘mŸ‚vôZZ™¯‹†Fö+¥F†FDFE‹……•+;°~ÃWôîÙƒ>½{akkK½º^ª}ôõõ°±±ÁÁÁCCV±–>½zѦuk*UªÄø±cTï«‚|øYWª„µµ5ݺvÁÓ£6'NœÌõüáâRgg'F�Á‰“™å.\¼ÈÉS±lÉ"*UªDÇí©W׋?öîÍQÇÚ/×Ñ­kè‡�� •+WÎQ¦ZÕªX[[ãÛÙ'GÈL$êÔm€™¥Í[e®œ�úü9÷îÝC[[KKË\Û‹ŒŠææÍ[š˜chbŽyEk ÑÑ÷³•335à^dd®õ„‡‡ãäè„BñÏåÖ*Uœ Ëå–ñðˆp´´´°±±Q=çâìLDÄ�üN�â_ÊL"ï¼ÜÝÝù{îHxx8ÎNΪmêêê8Vv <¼`ëMèêêòéä‰ÜºÂ’E ˜6cfŽoýY¶ïØÉ/¿ìáÛMÿCCCƒ'O’ILLbðÐ÷U�àâ%Ëxð +÷* ìííùòË/¹ÿ>þþþªm—.]¢yóæÌš5‹ž={¹�|eÝR]˜¿Bølî<:vl¯šô{#è Áׯ³ç×ß²•{ÕŽ~ä‡ÃéѽíÚ{Ó¼å;ªÑ‡ÐÐ0œ��_²w¦¨¨h¾þf“êµíäÛ…gÏRHLL*ð‡_KKKâ^Ú¦¥¥%‰‰I¤§§�yÜ®ÕUíœ>s–èû9×? Ç͵à¿wUÐxJ“ŽŽS>�„½½úçZFM¡†R©äá£Gô0ˆIÇÉáCÿün�½½=Ïž=Ës {;[j¼õ‰ ±ª¿'‰ thŸýR´³³ööölþvKîõØÛ’í‹Ð­[·©loŸK›ö¤¤¤pïÞ=Õs7o߯ÞÞ.ï"„È¡ÌLö}QVçµåûï9xП¶msþ&Qxx6Ö•€ÌÎãvÈmÕ¶ôôtBÃÂqp°G]] R °œ²B¡À·³ûöïç�}ûùáˆlÛcbcýÑV,_Šýß“��>ÆFF|µa]žúŠJ¡P ¦¦¦úvCûöí™>}:|ðA±¶õ¢¬Nø«�_çy»nýúõ4pÀß�<‘¹qã&ß¿•‹gY°h �6 B… 4hPŸààëøtúgNÇ‹}V2S˜Ž¾B… Ìœ>�‰ã?aò”itöëÆ�°ÛTªdÅ­Û· T‡��-�7bñÂù9¶e}ø�ùëD�êRSS+Єڬ×[©Tbgg‹ššá¡·Tó¶òbm]©PëѼ�¶–v�þ�¼�—.p‘õë¡¥¥Å±ã'¸sç.îîùÿÂzFF†j”ðéÓgÙε›«+�5dÈ{ÃX¼h–ÄÇÇ£¥¥dÞý5}æ,æ~>ŸÞ½z¢®®F\|<µkÕÊÑÎâ…óéÕ§45éÕ«'•¬¬ A]M�®]ü˜9s63fÍfðÀ�;~œÝ{~å°fRehdHxx8±qq¸¹eÆ4üÃÑ,^8Ÿèèh,\ÌG£>ÌѦ"oefD&ðÒ%þ÷Íf‚ƒ¯Ææo¿ËÑyíØ¹‹ˆˆ¶þð#»÷üJßÞ™sÞ2˜­?üÄ�?m#44LµàT‡öíQ(8;;ñóöÜ»w/Ç…ääd-^Ê…‹ �ÀßÿOúw·œ�攩ӨQ³¾�}xöìÏžev–ýû÷cÚŒ™;~‚¨¨(._ès�˜˜È¦M› âÚµkŒ1ccc5Êœ¯0þ|j×®ÍСCUí§¦¦º�—Éú02xvv9“;;;è_¤ud&Nþ”�F�ÂÊÊ g'GΞ;�R©äì¹ó8;;e+ûbGãÆMŽ=Æ‚…‹yoHÁn¾tù2¤§gP£Æ[$%eŽr 4�­?üô÷{ìó\(n@ÿ~üï›oT7oqëVfÒܵ‹·nßbîçó #""‚ /ø\D�·Þ¢ÖÛo3ìƒ\½v�ÈÈHNœ<•kÙ.~¾lùþ{öíßÏýû÷9räh®årS¥Š3çÏŸçò•+_'**ª¸¡Ð&<äÛ-[hÚ¼%µ<½X²l‹Îé|*SV®XÆä)Ó°°²¦nƒ†¸¹¹RAK …BÁ�?|�ƒƒ;ùRͽ§þ:M�·ª™sköþþþþIÝ ñª×�5_ä~;¿O'oŽ>ÄÍÛ·éÔÙ¯zL˜ô)·CC173ã÷ßvsüø <ëÖcÅÊÕ|¿e3žÏÇñë샙©)Ÿü=Ïê‡ï·`hh@³­òþ¼?tHŽ/PBˆü•™™¬Îkâ¤É¤<NåʪÎ+kRïú _ñÁˆ©T©+—/SÝVÙ¢EsÖ­]ü ¹{÷.žžžüñënôõõ€ÌoP£?Ëú _QÇÓƒƒûÿ™_���À_§O³bÕj>|HÅŠñéäÍì™ÙWüLOOgó·ß¡T*12µP=ír ŸÍž‰¦¦CßFTT4•*Yñí¦oT×Ó *>>ž5kÖ0|øptttðòòbÿþýpôèQÎ�;—í–ô>}ú°eKîÃÜEõbbòéäIL˜4™ÄG‰@æ7ÊO'OÊVFQÀDæÏ?séò¾ß’9zà€ì?p;'Z¶l�oçœ ÚýðýÆŒO³­Ð70(TG¿{÷¯¬\µšä§O©\Ù�õ_~�ºº:í۽˚U+Xºl#G„™™{~É}1³ŽÚ³|éæÍ_Èí� ˜:e2Uª8«>ü&:…å+W¢¦PÃÛ»#_ãz6 …‚�Ûbì'x·½7�?¦Š³3Gʱ4AÏ݉ˆ¸ÃðG“””ˆ“£F¿w^¦“wGvý²›­Z£££Ëê•˳�޽NÍš5Å¿Ùþ\·½]³&)ÉIÙž{ýÏ¥™÷‡ÉóŽ"s3³|_›º^u8t`_�b¬ãéÉ�½¹Ïƒó¨]ÿƒ¹ÇoaaÁñ£‡³=Þ²ù›\Ëæv¬Bˆœ 1Qþ:}ð�?¾~~ž…™™iŽI˜…xéuë7âIbÂ+×UVìÞ½›®=z—v9üüã÷´iûnŽçGô1+W,ϱMMM�½üN§N�J<>‘·ÄÄ$bcc153åqR -á^d$Û·ýX¢í¦¥¥�õß—zsS\}�(ó.aóæ¢ßi)Þ<ýû÷gò„q*[�>BE¡`玼Ӫe�c‘^Eä”K¢ºrù²<·Éo-• .\`è°áDGGcddD³¦MX³jÅËwoçÏ�&((¸Ì¬…&‰L)P]NtmËÆAˆ,Éwƒ�ìïSñßõðÑCš·h]ÚaˆRTعšžu9vøP ESx’È”™ß+ÊyŸ¾är’ 7�ÄÄ”�÷�$2B!Ĭ¼ß=+‰ŒBñË-‘öÁ¬ûòËœ;”±%ÊÍ‚x¥‚¢ (®¾ 55•K]B”gJ¥2Ûߘ1cHMM%55•1cÆäØ^”KQ%©\$2�—.áâZ�4ùYùbQÉP [cíÒCˆB+ξ`üÄÉ|:uz1Dõz,_±’6í:¨?}ú´£ÿ%/&)³fÏæÑ£G™ÉŠRÉ£G�˜5{vÎd¦ )S‰Ì®_vãU¯!Æf–Ø;VÁÛÇOþ³æAKC�?Þó AeãBï;µµí\ÍK *!ŠÇëè ^¥3>wþ<�ýºbae�±™%­Z·eßþýÅ]N 6¤W�=vGçª%Ú^LL ;vDGG‡ªU«²?�ã;zô( 6DGG+++FŽIjjªjûÙ³g©U«ÚÚÚ4mÚ”����[…P²ñë�D„‡çØÎú ëUåÊÚ�efŽLHH(}ú `ά™øtò&>!�«W¯¡££SÚ¡•*·Šz k`G;CÔ BãŸòÍÙ{ì¿ÇöK÷ �K.í…(Ve½/8zô:u¦WÏîìøyÚÚÚìܵ ß.ÝY·v ýúö)‘vëzÕ¡®WÒRSÉÈ(ÙÛã?üðC’““¹ví;vìÀÏÏ�;wî`bb’­\åÊ•™;w....„††âããC�:u8p ©©©øúúÒ«W/vïÞÍøñãéÛ·/'Ož,ÑØEá(ÿ^jað � 4ð¥åJt…ñ"(3#27nÞ ##ƒÇáìì„WOè—­ÌÛu006ã­·ksäÈQÕóÛwìÄÖÞCs<½êsôè1Õ6¿®Ý±w¬‚¡‰95k{²zÍÙ:€ÓgÎÒ´yKÌ,­hظçΟ/ùƒ- :v†lêUƒ'ÏÓ³ë:½¶\bÓ¹H=ËVÿêô=¢“ž—r”B¯’ê îÞ½KÇN¾š˜SÕ­:þþ™­Î‚ö£ÇŒ¥k?Ö¹–&�áUÇ“Ï?›Ã„ñã?a’jÁË—õ=ÍZ´Âļ"6v•ù`ć<þÏÿåí;vâ^ãm ŒÍp{«&±qq¬Z½†­þYïåQb"Zºhé0gîç >‚>ý¨¶‡……£¥k@dddaN?±±±ìܹ“… âèèȸqã°¶¶fóæÍ9ÊÚÛÛÓ¢E ¬¬¬ÐÐÐ@]]'''~ûí7?~̼yó°··gÙ²eœ:uŠÀÀÀBÇ$Jο/ä¯,)3‰Œ——ffføuëÁù€€\Ëlþæk‚®âÛÙ‡Þ}ûóìÙ35lÀÑÃþܺ~�&M3pÈ{ª}Ž?Áì™3¸zé3§Oã³¹óX·~�9tÚÉÇ—Î>>_½LëÖ­èÙ»_¶¥4MjåÈŸ·â™òûMÎÞyDXüSöÇr:üÇFÖ¥žƒýêX³{HmNT�?Gxa ¥�ºš‚á í84¼§?ªÇ*¿Üß«QɀͽßâÄèº|×·Õ­ô_Û1 ño%Ñ(•Jºvï…ŽŽ6gN�`ëÓ&N IDATwßbld¤ª¯ }AXX8AAÁ <(GLC $ááCN�ú È¿ïXµr97‚¯²sÇÏìÙó[¾ß d^6<ô}¦LžDЕ@¾ùz#æff9Ú324äQ| �âc˜÷YnÞ¼IFFªç<== ʵ|pp0ššš4lØ�þýûÓ´iS‚‚‚¨]»6™ƒÿÖÖÖXYYåY�(’È3SSN?‚£ƒíÚ{S¿aüý³cz»fMlmm™Ÿ¸œ:¸¹¹‘””DXXnnnúÈê¹ÀÃÃ�²eÊðãê5|T£zº²~\�­� U«VÍ´íÏ=³fÏ%,,Œ çÏ`eiIÿ�ƒµõ¸}çökã042"9“?Ö†ýŒ ~þ¬]¿;{»×Žæ¼NñâÅQ*•œ??¿“<<|„Í›~¢u`«tu_uî3jÆ&&”)_{'öìÝ�‡‡M7bé×_±è«Å”.W‘À6í K¿¶‡££#S'Obè°TªR•µë×kˈJ¥¢k—NÙzßÿ¿¥K—bhhˆ··7ß|ó [¶lÁÊÊ €øøxŠ+À™3gèØ±#%K–dĈ :”#FP°`A¶mÛÆ®]»(^¼8÷ïßgݺuï—Èyÿ�ÿ2>hfææÚ+HfææŒ§×+û*žÄDf9¢}ûغu–âÁƒH¬­­´3ÖuÁÎÁ™�ÖQ·n�Åð_ÏŸ?'4,ŒãÇ�óÕsL\2¿“è¥Ò…VË�2…Q*á~\2?��ä—‹9úieFm¿Á©Ðx¶,ÉíG‰,=öïòí ¼¸k)ÀÛ[ÓBÜ{’H‡Õ©èbÎôÆÅXu*‚_.=¤¬ã‹}”´3E­Ñpèö&ízýжȿžß¿ÎP¯jÔ¨�»›&&&¯­ŸššÊ£G�µ—M2£ç‚÷í}Ÿ{ââãIMMeøˆQXYZòÕ¢/³Ôî÷ß§m‡ÎYÞORR¶¶¶œ>}oïן¿„þûeÓ4ʰý³¡ÃXüÕ¢ eJ¥’];ÿ¤E‹YÚGVÎZ Û¶n¥~½ºYêôèÒ’ÈÜÕ¨§ôûùj¦e5—ü­ýyøo72”«Ò4,>Æâ£éŸMsî~M—ÿ{[ëåȧôÙ”ù>„yCÀ&Ü ¹K‹æÍ™=kF®í'))‰yóæI“Ÿd28±xÑÂW–éÛˆL¾Od¢£2Þ] „>Ó¤¥2¢– ó÷ß¡€¡1(ôæ °È†÷}îy_7-XXX0ðŸ'#‹ü!»ëÂÈ:2Bˆ×Ò¤©è^µ(}Ë&õihrw)zña³±“[¡?tÙYCF¡P V«±Îd‘F]‘DF}óÏ_;MJ;Ð¥” ©Ïâ%™¹ÆÊ‚³gNé: ¡C …2‹¯IÌ•«W´¿õA¾¿´$D^ز9;þÜI¢ê:[ï$``b.—™DŽóöö!8ø:‡î%6ZVÜýíÝ»;Ëu­­­±´²Ò«µ€$‘BO………Ѷu [ý µ&˜_Cþ‡�±™$3"ÇùøÈÄ]‘wI"£cÏï_×uBϨS’€z¨T*BCCéÑ­+~ÚHÜO{Ùyã6 MH·‚šB|À$‘Ñ¡Së:¡‡^>›&%%•JÅ­[·èÓ»k×®¥ç�[ØXYbfV8ÝýrÚ†�›r­o!„ÈI2F-„žJKKC¥R¡R©¸yó&ƒ «xIb=æéÓ§zw ¤Bè‚$2Bè±—£2)))\½z•‘#GR»cb%™B$‘B¯½Lb^ŽÌ\¸p�ñãÇã]ª Ñ1�xöì™$3Bˆš$2Bè¹—IÌË„æÜ¹s̘1“>¥xõP’!ÄM]�ï‘ ÿMb^þ÷Ô©SÌ�;ŸRe‰ŒŠ&111ç“ùœ !ò¹k)ˆŽ‰ÁÎV¿–öì©©i)bþNm£££±³³Ë…óµZ�J¥B­V³k×.<<<Ò•—.W��Û7(X°  ÔM�B¡#2"£ç>B�ú¨T*]‡’NƒÆÍøcçÎwj{ýú jÔªKLllG—¿¸»»Ó«W/ùî»ï¸xñ çÏeÍ�+Ù°öGþÜþ+ññq¨Õò!ćG=¦V«4�ñãÆP°`A7k‰µ½36.ø”­H÷^}¸rõªNb{—Ë/Ûz{—¤nÝÚÌš3/Ózqqñ 2”>eqró¤êGóË–­o½ß¼ÈÓÓ“cÇŽ1sæLLMMùé§ŸpvvÁÓË‹J¾¾Ôü¨U*ûSÔÃCFc„$IdôØî={ùßÿžÒ¾m>ŒfÚ”I_¹À¶Íquq¡E`[¢££uéÛ:ä6ü´‰'Oâ2”Mø|2—/_eýšU>°—I‚(^¼¸¢Ô�%JpìØ1NŸú‹Û·n2xð`ž>}ÊÊ•+q÷ð$5-ÌÌÌ066ÎÕò„B_É™O�íܵ‡¦M¥ûKÛ̬0¶66x{—dúÔÉ<žÈ‰¿^<¹V¥R1}ÆlÊTð£¸wJ\\<©©©XÚ:rùÊ¿#8�š¶àÛï—0kÎ<ÊûVÆÉÍ�b >Ф¤$mÝ“�¢ZÍÚ8¸xðqÝ<|ø0]¬6n¢R•¸y– s·žDÇÄd©m)\œ�9pð`†ã¿~ã ÔÃ߿ŋyѤq#Ê—+«-ßñÇNêÔÀѵ(ÅJ–¦Aãf|H›ö�hÕ¢9?̤‰ãQüçY?fôØñÌ™õŸ8BrrA&e©-@¥J¾œ:}&CL�°lùJ–­X©]¶ÿ¥£ÇŽÓ«oºtêÈ_ÇóÓºÕœ9s–gÏž½ñXííìØ¸~ W/ŸgbÐ8‚&LâÎÝ»\¿q�þ:v˜GR¹’ßk�/·h4þ:yj|}+â`oÇ£Ø À£G�8yò$©ižçÆÝJB‘‡H"£Ç2»£güÄÉx+‰³›'Ÿ~6œzuëPûãZü¸z-ãÇ�¡JeJ–,Á‚¹³Ù¹kw†Ñ“W±²´ÄÞÞžÊþþ´kÓš“ÿŒôüúûv\]];z$îîÔ«[']\ß/_I‡öm©W· ò)»vïÉR[;[[¢f¼<6jÄ0Ì�ͦŸÁ»L‚&LÒ&4?¬ZMÛ6�ôíÓ w77<½<³ø®‚¡¡!¥K•ÂÚÊŠíÛâääȵkÿ>¼ÓÔÔ''GÜ\]133{íñå† |å¦F†T,_K LLLФ©2d[·lÁÏÏ�ÈÈ’B!„øÐH"£ÇÔj5Jeú/ª1£FpòèaF ûŒ2¥KóóOë(P IIIÄÄÆâYÔC[×Ó³(aaáÚ/¼¬þõnkkË“'Oxð W—WÖ�ŠŠbíº 88»ãàìNûŽ]HNNæÿûßÛ‹/î4uZ¦eÚ·eÿž�ü¹ýW>ÌÀO†¼8¦ðpJ–(ñÊ> Å+�õò•«´l‹»WIŠ•,MlL,*UÊ[_n(T¨f…M(UÊ ‹"(•J”J%…MM¸uý±1ùý×­x—,NaSSIf„4Idô˜­� �=J·Í¢ŽŽŒ5¥RÁ7ß-ÀÈÈkkî†ÜÓÖ½{7WW (@áÂ…³<:£T*µ‰€«‹ wîÜ}åí½..ÎôëÓ‹¨ˆPí+6ê>fffol ûÛ7¬“S®lF ýŒ}û Ñhpttà~DÄ+ë[XX¼òX‡ N™Ò¥¸yí×._ÀÉÉéµû~Ýñå¥RIáÂ…3Là544ÄÝÍ…reKSçãZxyyQ¨P¡\‰A!ò IdôX¹rå8áb¦e,˜;›³æhçwôìÑ�YsæqêïÓܸq“‘cÆÑ( !T¯V•¯—~ËÍ›·ˆŒŒ"111Kq´jÕ‚ø„x‚&L"äÞ=ÂÃôï$Ó.�:²fݶýö;DrûÎ]mLoj pîü*V(Ÿa¿ËV¬äø‰“„…‡sáâ%~Úô3%K–@¡Pб}{6nÚÌö~Ÿí;þL×¶zµªüüËVþ:õ7>äñ?£Kii/’ª¤¤dÒÒRß8¢ñºãË …BûúÿÛ ±¶²ÂÒÒÃB…òÔhŒÌåâÔۿû’Èè±€õÙ½gï+?•*ùÑ ~]&M™¼¸ìÔ:°½û  i‹@\]]øvÉbmýùsf¡T*©Û°1þÕ>âùó縻¹¾1kk¶ÿº…7oòq݆T«Y›‚… j/5nÀ¼Ù3™¿`~UªÓ´y+>š¥¶aááܺ}›õê¦Û§F£!8ø:ƒ?ŠÕ´mß 3³Â¬^õâ£fMóù„qLš2�j5kóóæ_Òµï×§ êÕ¥k÷^”÷«Âž½û(SºóæÌäè±”(U†¢Å}HKKÃÚÚú•ÇÿºãY“––&‰Œ(�FCZZæÓr‚âILd–Ï.ûö °ukÈâ éÁƒH¬­­00�'!¼ôüùsBCÃ8~â8�;vxmÝ”*UfÁÜÙ4nðž"|¿ÆOœÄÇѬ\þÝ;õóèñcŠ•,ÍÅsãæúæäL¼Þ†�›¨Q½îîn˜˜˜¼¶njj*�=ÆÉÉñ•uBBîa^Äc##”JežIB¼�FƒZ­æyb" ñ Úy›¯¥P°mëVêÿ¿?n_G2 =V¨PA¦Núœ  “¨_¯n¾[¹õæÍ[¬Û°‘ƒûvé:‘Ëììl‰‰‰åù³ç(P@×á!Þ“´´4ÒÒÒ°·Ï½gêI"£šlý¸zõZ–û8rä(nE½r,&!rŠ$2yTRR’®C"ÇlÞ¼™àà`ºté‚¡¡á+ëݹ{—N]ºÑ¹SGΟý›m[6Ó¶Mk��Ìž;�½úÐ( !‡öïeÿžÝ4oÞ3s³wê79åõÉ\v©T*bbb9yìwn]gíêU¤¥¥Q¹Z ö8�µ>RU¨Õê�Kˆœ ‰ŒÛ¾ãOjÖ®�£kQJ–.OÛ�IJJâø‰“ø”­ ëð„xkÍ¿¤LKKcÚ´i3zôè×¶»yójµšO¢˜—þþ•èÕ³Gº:eËûbbfA©2å9tø°v»J¥âóIS(êUG7z÷íÏ“'q+áÃ÷Ëþù¸n}æ-øRûï€ÆÍ˜>cfºý<ˆŒdÚô|9“&N R%?Ê•+˧ƒááîÀ©SS£Vm,¬í¨Rý#Μ9«mß¼e ®îž˜˜YàìêÁW_/I×ÿÈQc004ÁÀÐD»mõšµø”.‡•­�mÚñ0:€ûÔm€ƒ³+¦æ–L™:=Ó÷ÏÒÊg''>ú¨k~ü�íÛ1pЧÚÿÑ11téÖ;GgŠ•ðaæì9¤¥¥iÛÇÇ'hcš6}Æ«þ7 ñ^I"£§BîÝ£w¿´kÛšãG°aí�´jÙ###T*juÎ=™Xˆ÷åáDz`Áí¶—£1ƒÆÞÞþµí+ûûccmM«Öí8{ö\¦uÖ­ù‘ÁWhØŠŽ�»jG/gÌœÍÆM?³~íjöïÝMhh½ûö nÝ:?qx1ÚyæÌYNžü €ÔÔTNýý7õêÔI·Ÿ½{÷¡V«éÓ»g¦qDÇÄдEK[µäæõ«4h@ûŽ�IIIàÄɓ̜1�[7®±ðËùŒ3.Ý1Í™=“§ñ�yÿ€}û÷óÙ°,Z¸€+—Γ””̈‘/¿G±�8rä(‡ìãöÍ` è÷Ú÷ñ¥�ýûrï×®СSâââ8rè+W|Ï÷ß/çë¥ßhë)b®�i|ÐØ,íCˆÜ&‰Œžº}ûjµšýúàY´(~¾éÚ¹“¶ŸOžŠ»WI\<ŠÑªM{îß�àÁƒHšµlM±’¥qpvgæì¹:9&! .Ì©S§˜>}:111Ù�°¶¶â¯“Ç(ZÔ�†�›à_µz†K#åË—ÃÕÅ…ñAc‰�}Äõë7X¶|S&NõêÕ(åãÃ’¯±}ÇDFEаàØñã”,Y‚cÇ�£V«ùûïÓ reÿtûy�½� Ì4Öuë7à`ïÀ¨ñ³µeâ„ "¿mßÎî¤q£°ã�?ù}ÇÚ´ÌÐwíÚµHKKcý†Ÿ2Ý·«›+eË–áYÂí+éYÍš6yE¬Jí\#C£ wo¹¹ºòÉ �éúK~þ?ÌÍÍßüf½Â7ß}OQJ•òÁÍÍ•äädîGDhËoݺ£]—çEL)o½/!r‹$2zÌÅÅ™… æqãêEš4nD»Ž]¸pñR¦u5 ³æÌ£œ¯?Îî^L�6•JN:Bÿ(•J-Z¤ýwvFc.^¼Ä«~$øúu°zÍZÂÂÃ)]ªÔÛöïÛ—©Ó¾àĉ“\ æÓ!ÃhÖ´ NŽŽté܉¥ß|‹‘±ż¼hÓ:�å+W~ŸÆ�2ôçåéÉgŸ~Â'C†2oÁ—œ?�àë×Ù¸égnܼI»¶m¸}ûÓgÌänH¡aaœ?!KÇY¼¸îÜEhXÿ}š””zöèÎÊU?²ù—-Ü�ˆàæ­[Úd,«b¢c çð‘#téփͿláÛo¾F¡PPÊLJ�>ªÁÀAŸpãæM9Âì9séׯE=‹’’’Â/[¶r/4”àë׳µo!r‹$2y€‘‘cF�ÀÕÅ…ƒad”ñ¯µÝ{ö²lÅJ~Ù¸�û÷n3eòDE+ěլY“Aƒáíí�­Ñ˜'qOX½vÕªM¹ ~Ì_°�óçÒ¼YÓ7¶�8!ˆíÛѹkwêÖoˆ»»«Vþ;ÿ£Eóf( :´o€ŸŸ/ÖVÖtîÔá• ÓÍŸ7‡ï¿]ÊÚuë©^ócªÕ¨Å’o¾%&:Göîú“ýR©r5üü«¤›8û:£FŽ %%…2å*Ò­G/DFÒ¼YS/ú’³fãSºuê6`ÿþƒYê¯`Á‚ØÙÚR³v]Š—,E÷½Q*•œ:qŒúõêiëý¼qæææÔü¸½ûô§ÿ¾|öé'89:2mêd† †o¥*¬]»>Kû"·Él-=uùÊU.\¼H âÄÉ¿¸�·wI<ÜÝP©Tüúûv|+V )1‰´´ë;¤¦¥¾qA1!ôÁ7ßdíKý¿jü1‡|œiYùòåHM~žnÛ£èç£,X�éÓ¦0}Ú”LÛžnÛÕËoAéÒ¹]þ3ÿ¿¶z IDAT*WöçÐþ½™–ý76€­¿ü¬ý¹¨‡'ŽþÿMèÞ­+Ý»uͰ=³cÿ/{;;Ü}eùKv¶¶lX·æ•åãÆŒfܘì_"7I"£§âââØðÓ&&NšJrr2îînÌœ>U;Ä=1h,£Ç‘œ’BŸ^=˜8~­[ѼUâã077£Fõj:> !^°´°`ÿž?ß\ñÿµBˆ7‘DFOÕü¨;wüöÊòáÃ>cø°ÏÒm[0w6 æÎÎP·l™Ò<‰‰Ìñ…Ȫò*ê:!D>%sd„B‘gI"£ òðk‘ÈçT‘H"#„BˆP …"W/)K"#„x/$‘âÔۿû’È!„"Ï’DF!„y–$2B!„ȳ$‘B!Dž%‰ŒB!ò,Id„B‘gI"£Š,>�Ï׿ãÆža{9_~Þ¼%§Ã"�¬~N…B—de_=÷ýò”(^ŒÞ½zè:!„ž»L\|±Ñ1ºE/ÙØÙbea�··�®C9H=׳{W&M�N¹²e¨TÉï�õÓÒÒXøÕ×,[¾’„„líliÛ¦5“'Žàâ¥ËMøœËW®âîæÆøq£iÒ¸³æÌcã¦ÍÄÄÆ¢T*iØŠ9³¾ÀÈÈ(W�QñV«)WþÍç‰üâÀÞ]´iß)[mΞ9Epðu||¼s)*ñ¾I"£çüý+Q¶LzöíϱC°°(òÚú ¿úšU«×°üûo(Q¼8_}½”óç/ðøñZµiGïž=ùvébŽŸ8I¯¾ýÙýç*”/Ç•«×¨W¯cG� 4,Œ^}°|å*†|2è}ªâÄÅÇQ»N]‡ñÞi4šlÕ÷õ«ÌÑCûs)¡ 2G&èÝ«þ~~ 1*Ýöu~ÂÆÁêÔ`Ùò•�Íǵjâè考½�¶þ¯¿ýŽ•¥Ç�ÅÝÍ�Î;дqcV¯Y«­cei‰½½=•ýýiצ5'ÿ:õ~RñN>ØËIM¶_11è{•OɈL±pÁ<ªÕ¬�n’o³&Mð«X#c#‰‰�Å»D‰Lû §¨‡{ºç^xzåü… ™Ö·µµåÔß§sð(„"gewDFä?’ÈäEX²x!ýF­Vk·ýÿKMEŠ˜sÿÁ*‘ñ:¹«‹ Ûwü‰F£Ñ&3wï†àæêšé>•J¥œ$„z-³sÔ€�øþ»ï26�‡—æ;ri)©W§6 Ô'..þ•uZµhÎÜù_r-8˜°ðpÎ_¸øoY«<~ò˜/fÎ!4,ŒŸ6þÌ;wÒ½[×÷¾Bä8�F“î5|øpT**•ŠáÇg(Gþ8{üä /]Òuù†$2yÌÓ¦`eeùÊòéS'S±|yšµlMƒ€¦߸�¹¹ÖVVlûågNœ}Æ„ ±\8sŠí¿nÁÒÂ’ªUªhË+”/ÇοqÿÞmŽÞ¯½õ`ýšUL?NûïAú±ë�ßsø¨„x³ºõb\¸Æ…‹àìêAËÀ¶ìܵ;Ëí�=FQ¯Ì犽ôëo¿ã_¥:Öv¸-Fó–­ILLÄØØ˜¾½{áí]ò]C/ܺu‹€€ÌÍÍ)Q¢+V¬ÐuH9LhXùÃJÂBC3”†…†²lù2m½¯·W½zu:uèðæŠâ½‘92ùÌ/[·±øë¥$ü/;;;Ú¶¢ßÞºKˆl‰ŒŠbάtêØ�°ðpvîÜM§.Ý3j$ãƒÆ¾±}ªJ¥�K–™»wCèÒ­Ó§N¡e‹æ<~ò„«W¯all À¸±£sìXt­M›64lØ��7rêÔ)ZµjEÅŠñóËëÍh4/þ?÷îÕ“Þ½z¾±ï¸bueÿJTö¯ôN}ˆœ%#2ùÌ�OqëúF„qùü¦Lšˆ��ä«"ï)lf†­­-~¾¾LœÄâE ùbæ,îÞ @¥R1iÊT¼JøàâV”¾ýò$.NÛ>>!C3 M̘>cfº¾oÞº‰Z­fð xyyâ_É�ž=ºiËíœ9pà ±�iûxùªþÑÇDFFÒ±sWìœñ.]–µëÖ¿‡w%{RRR¸|ù2�:uÂÒÒ’F�áááÁ�7tZŽùÿ—�²òz“ÖmÛãV´æ–6”«èÇ’¥ßh㯗,¥N½ëõ ùlõ6ÒöÙ¥[úô@rr2A&âîYwOÆŒOjjj.½ 6Id„yB·®�144d÷ž=Ìœ=‡M?ofíêUìݽ“а0úþó%PÄÜœøÇ1Ä?Ž!hì˜t}ùûûcmmMëv8{îÜ+÷icm­íãÔÉc˜šš0zÔ4 �»vG©TrñüæÏ�àO†pýº~%… ¢AƒŒ=šÐÐPÖ­[ÇÓ§OiÒ¤‰®CË1¹‘È;vœiS&sõÒy¦Lúœ/fÌâûeË3Ô›5sá÷ïóý²åüúÛïœ8ù æÏ`ƬÙü¹s»þØÎ®?·³eë6V­^“ãÇ/äÒ’"�P(899ýÏbfËWüÀÜÙ3©^­*_/ZHyßJDEEiÛ¼êñÖVVœÓºã‚&`jbÊÓ¦ù"Y*î]sKÌ-m8õ÷i¢F¿Ãæ¼””Y¼x1aaaìÚµ‹Ù³g³mÛ6]‡–sÞbeßì*Uʇ��{™–U¯^ kkkRU*ªüscųgÏIHø½ûö×~>æ/XHô‡ºúr.“P*$úOß>§«×¬#%%…€† 166ÆÖƆ;wîP¥²?·oßÀÝÍ•»!÷HNIÉR¿ÆÆÆL?Žu6°oß|ÿY-û¥Ã‡�°fÝzNÿuB;ßÌÕÕ¥RIhÈm,õxtãüù󄄄ЧOhÚ´)û÷ï'00PÇÑ北#2+VþÀéÓ™¯D^µjUzõìñâŠì'2÷BÃprrÌ´lƬٸº¸P²DI† É�?¬À̬0Eаbù÷4oön·j‹7“Dæ=+P &&&lظIÇÑñj&&&(P@gû��‹#22’ðûìܵ‹/~Åøqcñô, @ß¾½™öÅ <<<°´°à³á#hÚ¤1ŽŽ/¾pRRRزu•üüHLLLwÉçâ¥Kœ;w�jU«`hhÈÑcÇ ¿O©R>ébP©T 6œQ#†ãââLRR …‚²eÊP¡|y ÌäI±´° ä^(5ªW{oPxzzbhhÈ’%KèÖ­7oÞäðáÃLŸ>]ס嘗‰LŸÞ½ˆŠŠ"<<<]¹««+={tÿ÷TGdvï݇‡‡"#™¿àKºu풡Ε«WùzÉRNÿucc#*øú³k÷nн{7>Ÿ< Šyyò0:WWW¬­¬0/bNhh(±�acmýno€�Dæ}300ÀÜÜŒF�xþü9ê—·æêb“š÷µ‹\ìþÝw Èík%¹Ø½"“Ÿrk'J…ÌÍÍtvÇ›³“ã'N"hÂçXZXàëçËúµ«iÖôßIª‚Æ¡V«éÚ½'ÉII4n܈ysgàèèÈÔÉ“:lÉ)Éôï×—Ó§iÛÆ=‰cíºuŒDrJ îÌŸ;;]ÿ'NœäÆ�›LûbÓ¾˜€““!·o°mËÏŒ5†FMšóôéSŠyyqäÐ~í-ÜúÀÖÖ–_ý•‘#G2räHéÛ·/½{çŸyÿ�#3>hcÆ‘Ÿ€ysÆ�KWG‘ÅDæøñã¬^½C##Ú·kËØÑ£2Ô9z,#† £hQ&OšÈ¨1ã¨[§_L›BÁ‚ôí?€ÈÈ(X»úG¬­¬hݪ%k×®cÔè±üøC~[×çýS<‰‰Ìò7ľýlÝ:Ë탑X[[Éí¿ÿ¡ÑhP«Õ¤¦¦¢V«åYFBï( ”J%(•ÊtÍLjj*�=~åÐ;ȹà}øý÷ßiÛ¡³®Ãx¯~Ù´�†�2lÿlè0µ(C™R©d×Î?iÑ¢Å+ûµspfã†u™NþÙ—•s„–BÁ¶­[©_¯n–û—³Ê{¦P((P €N‡ì…"ßÈä�ÁÅ‹¾²Lžµ$r–$2B!ò¬ìŽjg¥~tTÄÛ†#t@!„Èlìlu‚Nd'‘Q*•¨Õj¬e‚m¾¢_÷W !„x+Vœ=sJ×a¼w …2‹/jµš+W¯`ee¥ë°E’!„ȼ½}¾Î¡ƒ{‰ý€^Û»7ëOE·¶¶ÆÒÊ Ÿ7Wy†$2B‘O¼\_ˆ‰\ZB!Dž%‰ŒB!ò,Id„B‘gI"#„Bˆäøà­Ö¬å7 \vt¤¬oE]‡È!„È7Î�ø ÿƒu†ÈçüàìñºCK!„È' “’är’ÈuNJLÒuZ’È!D>!{Åû¢OŸ5Id„"›?yÂÅK—t†Id„ùTbbb®õ½rå*ÆMȵþ…Y'‰ŒBïÔ­ßãÂE0.\gWZ¶eç®ÝYnäè1Šz•xc½sçÏÓºm{lœ°´±§jõšü²e뻄.„xÏ$‘Bè�Ȩ(æÌšAXÈm~ÿm+þ•üèÔ¥3gÍÉRûT• µZýÚ:GŽåã:õ177ç—MùëøQF Š•¥eN‚â=‘DF¡— ›™akk‹Ÿ¯/'±xÑB¾˜9‹»wCP©TLš2¯>¸¸¥oÿ�<‰‹Ó¶�OHÀÐÄ C3¦Ï˜™¡ÿ¡ÃGÒ¢y3~üa\‹’%Kо][êÖ­@LL ÝzôÂÑÙ�>¥™=giii™Æúºº4hŒ³«æ–6L�þEN¿UB|ÐdA®S�>ýÒ¿o>ýd0­[µÄÚÊŠQ£Ç¾±®"w)žÄDj²Zyßþ¶n š¬5yð kk+™à&D>–ššÊ£G�qrr|e9¼³æ.`ò”)ºC|¦N™BИ‘Yª›•s„–BÁ¶­[©_¯n–c‘!„BäY’È!D>á®ëÄCŸ>k’È!D>‘ldD¤““®Ãù\„³3*cc]‡¡%‰ŒBä¾5ªqª^=]‡!ò¹¿ë×Ç·F5]‡¡%3ï„"Ÿ(ïçKjófüІÊâôŸµm„xWÎÎü]¿>ŽÍSÖ·¢®ÃÑ’DF!ò¿fM¸ìäÈŽ2e(”˜D¨®ù‚; 26Æ·F5½Jb@!„ÈwÊúVÔ»/!r‹Ì‘B!Dž%‰ŒB!ò,Id„B‘gI"#„Bˆo’È!„°ìŽÈèId„BˆXf‰Ì€�øþ»ï26P(r;¤l‘Û¯…Bˆ˜F£I÷>|8*• •JÅðáÃ3”¿Í¥¨Ü$‰Œ"_JLLÔu™ZôÕb6nªýwjj*©©©:ŒH|èþ›¤L�6�øøøÉŠFC||‘DF‘'tëÚCCCí¥Ÿ™³ç°éçͬ]½Š½»wFß~´õ‹˜›ÿ8†øÇ1�“®¯{÷B ¾NŸÞ½2ì§O¯ž<‰‹ãäÉ¿8wþ<õëÖåê¥ó¬]½Š'NÒà mý¯/âæõ«lÛú Û·ÿÁº ?/.oõîÛŸ Aã¾r‘X‰�µu¦ÇvìÈ!âÇðÇößhÛ¦5û÷@¥RpàÐ!Ê—+‡““S¶ß³[·n¡V«ñõõÕnóóó#888CÝÆ�³wï^Ö¬YÃÇY´hýúõ 88˜Š+b`ðâFW'''2íGä=y=‘‘Û¯…y‚B¡ÀÉÉ‘èâZ¾âæÎžIõjUøzÑBÊûV"**JÛÆÈÈ(Ó¾^öáâ육ÌÉÉ …BALl¬v›­�-...¸¸¸ðýwK©V£ÑÑÑØÙÙQ®lÙullhÈåËWX¶|�;v KçË¿»¸¸¼òØ *¨�µNíÚ(P€ýÐ( €Ò¨Qì½IÿOll,&&&(•ÿþÍjnnžébf5kÖ¤]»vŒ5Š`oo¯½|KáÂ…ÓÕU?"ïÉ~b"·_ !D¶©Õj""`gkKbb"111xyyiË‹{ñó½Ð°7öegk ÀýÿLf})22�Fƒ­�M¦mKù¼xXÞÝ�{<‰‹£G¯>¸¸ÅÆÞ‘;þ@õÏ¥¥��{éâË*Ú¶mÚµëIIIaÏž½4mÒ$ÛýØØØðüùótó…°ýçøÿë‡~àâÅ‹\¿~�PµjU7n¬íçéÓ§é꿪‘½¼¥:;/="#2BˆªQ=]Ù?®ÆÖƆªU«fÚ6ôŸDÉÙÉ‘Y³çÆ…óg°²´¤ÿÀÁÚzŽŽܾsûµÇT @ HNNëð¡ŸQÁÏŸµë7`go§=Æì*^¼8J¥’óçÏãç÷âa’çÎ�cðàÁênÞ¼™víÚaeeÀÔ©Sñöö&66æÎ�Kjj*DFF…��þ<Y¼½—#2+VþÀéÓ§3­SµjUzõügî–B!„x£ø¸8"## ¿ÁÎ]»øráWŒ7OÏ¢ôíÛ›i_ÌÀÃÃK >>‚¦Mãèè@JJ [¶n£’Ÿ‰‰‰x{—L×ÿwß.¡vÝ*D—.�14,Äo¿ogÁ—‹X»z¦¦&Úº§þ>Í•«W1(`ÀÈ1c©Uó#\]]IKKC¡P�’œLJJJºO{÷êI§.ݨ^­:uj×âÑãÇxy¦¡Q(xyyòË–­8;9õð!~¾¾xx¸Óªe FŽÄ qoýÚØØÈرcY±bÛ¶m#""‚nݺШQ#¼½½Y´heÊ”aË–-´mÛ{{{¾ûî;\]]±¶¶¦iÓ¦˜šš2aÂ>ùäÆŽKµjÕ(_¾ü[Ç&ôÇËD¦Oï^DEEž®ÜÕÕ•ž=ºÿ{ JÏFdäÒ’Bï8;91~â$Š+IËV­ùûôÖ¯]ÍÄ AÚ:‚ÆÑ¾][ºvïIý†�pwscåŠe8::2uò$†A¥*UY»>ãíÔ•üü8°o¡aá´hHÝú>|„Í›~¢u`«tu£¢¢hѪ µj×ÅØÈˆµ«W0fÔŒML(S¾"6öNìÙ»š4nÄÒ¯¿bÑW‹)]®"�mÚ–q�Žùsgóûöí”.W‘±ãÆk·4•JE×.�Þé½\ºt)†††x{{óÍ7ß°eËí¨K||<ÅŠ`òäÉT­Z•êÕ«ãááÁ… øí·ßP(,X�mÛ¶±k×.Š/Îýû÷Y·nÝ;Å%ôÇ'ñއ™¹¹ö ’™¹9ãƒÆéõʾŠ'1‘YŽhßþ¶n�åƒxð kk+íLw!Dþ“ššÊ£G�qrr|e�¼|.hÛ¾#¥K—bêäIïeqññ¤¦¦2|Ä(¬,-ùjÑ—Yj÷ûï¿Ó¶Cç,ï')) [[[NŸ>�··÷Û†+ò¸_6m a@£ Û?: €Å_-ÊP¦T*ÙµóOZ´h‘¥}då¡¥P°mëVê׫›¥¾A.- !„^iЄ»!wiѼ9³gÍȵý$%%1oÞàííCpðuÜKl´¬¸+²nïÞ¬?YÞÚÚK++½ZCH!„È'||dâ®øðÈ¥%!„BäY’È!„"Ï’DF!„y–$2B!„ȳ$‘B!Dž%‰ŒB!ò,Id„B‘gI"#„Bˆ<+×}{¸”"gÉï¸B—r5‘IKK““œùœF£!--M×a!>P¹úˆ‚ÔÔTž'&¢ÑhP*_JëÀ@†ù•JÅ瓦PÔ«Ž.nôîÛŸ'Oâ´í£cbèÒ­vŽÎ+áÃÌÙs´+"nßñeÊUÄÚÎK{:uéÆ˜±AxxÇÔÜ’V­ÛŸ½ÉÕB!D^–§Fd^åâ¥K4`â„ñÜ ¹G§.Ýøæ»ï9|ß|½GGGBBBhØš*•+Ó§wO øjáÜÜÜX¶l�š6ãÖõ`,,ÒÏ퉎‰¡i‹–Œ;†m[6³dÉ7´ïØ™ë×.S¨P¡Lã=f©©©|ýÕBí¶3g³qÓϬ_» K †|6œÞ}û±mËf:tꂉ±1GàáÇtïц}6„»!!›síÊ%bcb lӎȨ(¶ÿ¶ ¥RI`›v|½t)Çå»,ÄÛ“;…¹%O%2ÃFŒd̸'Þ;roo¬­¬qtpÀÑÁ�N;pìØqm"S®\YlmmhÀåË—µ}¸¹»Q§vmfÍü‚õ~bûŽtëÚ%ݾ׭߀ƒ½£F `â„ æÌ›ÏåËWðóóÍëÎ]»Y½v§NÃÈÈH»}ÙòÌ›;›êÕ«°äÿػ︨ë?€ã¯ã@8Ʊ7K‘!Ü;Í­Y® ÷žiŽLÍÌ•3-ËÑR3M³L-ËÊÝÏ™©8RAQAY²¹ñû=E…9ôó´ï£»ï÷óý~ßw|ï{ïûŒïwù2Bj…’�˜HZZ‡æúµ(<ÜÝ ¨^�iS§ðÙòŒ7™L†³“ÎNNôêكϟ'$¤°ÿM÷n¯=²FI*ʳÁ‹—BQæõ Yff&*•ºØ�+A Uª¦¥ï¿Ç©ÇuSU??½åœœ�HIM 55�~áê¡ÀÖÁ™_û�ü‚|½ë¡ðRóf±e 7¸……Ü ¹-¶ÎH$‹•MKKgÄÈÑ,^¸€€êÕuósrrHR*‹Ä]­jUb®Ç‹©©)îî÷—Wó#&6Vo¼¶v¶deeëžÛØÚ�‘qGoYA¨hÖÖÖ�œbbbÐh4ºçeuöì9üª R©Ê!úŠÓ´ÅKü²cGE‡!«R%2övöx{yé&½åŒŒŒt#’æ/XHLl,ÿž9Íí¤^íÚµÄíççç��·w±ež OBBj�•‘ª›r³2èÒ¹S±²Ó¦OǯªÇ )2_&“áèèÀ•«Wuó¢®\ k† Oòòòˆ‹�¿¿<êj‰¿0% Z´%>„ÊB­Vóæ›oD^^Þ#ËnÿùBë7ÄÊÆ…�_yµHŸ¸g%I©ÄÓËSs+d–ÖxûVcȰ$)•Ou?â®á‚ðh•*‘ɸ“Aâ­[º©4' µZ�D"!/?�üüübwÝÍÊÊâF\—£¢1j f¦ftîÔ(üyáÂErsséÕ³W®\eî¼ù\‹Ž&&6–Ó§ÏÛß©Sá¬ýæ[fLŸÆ­¤$]¬·’’>t(³ç|ÈѣǸÁ[cÇÓ¥s'Ü\] ¤Y³¦Œ5†K—/ó¿ƒY¸h1ÃJˆáy³eË"""èÓ§¦¦¦%–»zía}úÑ;ìMNŸú‡í[·Ð³Gwd²g?‚1??Ÿ„ÄDŽ=LlôU¾[¿ŽÏŸ§{Ïןy,‚ð"«T‰Ìäw§â¡ðÑMÕª=v�wß}s™9A5jaëàÌîÝ{ð¾[ãâçëKvV6Á!µiÒ¬J¥’Ý»~ÇÒÒ€Ñ#GðÏ?'X¿a#®..ìùówöí?@½�©[¿!ËW®*¶¿ï7ÿ€Z­¦]‡ÎEbUx6'½?}o¼Þ‹Þ}ûÓºM;¼¼|³fµný7oB.—Ó¼e+ÎðáC÷Ö˜§ðî ‚áx°–A­V3gÎd2“'O~äz—/G¡Ñhxkô(ªúùQ¿~= P¤LH­PÌ­lªQ‹¿þ÷?ÝüG�¬êÈ—_Ýÿ¶l݆�–~¬{Þ¾cæÎ›¯7&[hÖ¬)Ç¿]d4ã£öOë¶íqq÷ÄBnˬÙs8|ø5ëÔÅÒÚŽz “øPóõ·ë7\;Gºõè¥û‘TÒöáy'IU&”ºÞrï¾ýtëÞJYÕù×_x¹]ñ¦—GÙ·ûw^z©U™ÖÁ°Ý;ܺu‹‘#GÒ´iSÞyç6oÞLXX“&MbÉ’%ºuô� nßN!¤Vj֬ɼ¹³‹t´?{öu4âøÑÃ89;ñå—_³zíZ®_�ÂÌÌŒY³ç²qÓ÷|ûÍ݈A¹ÜŠí[·0|ährssY¿n-¹¹¹Ø;¹Ò¾][¶ýô#*• g7~ÿõ]'}(L¼}«qårÞ^^¤§g0xè0RRS9°w7À#÷y/ÞóçNéT=°ïLš@ß>½‰ŠºBÿ�ƒX¼pú÷cï¾}ôèõ&?nÞDHH † ���-7|«w{®..Oÿ�)åI"aû¶m´y¹u©W)×QK¶66ìÛý{™×áùdiiÉñãÇÙ¿? ÀÎήԵ1öövü}ì0 .¢]ÇNøúú²pþ‡¼ÜúþI¯V­šóÞ´),\ü‘‘—¨]»Ö#G ¶o×–ñ&pøÈªW÷çð‘#h4þùçÆÆR4¨¯7¦FMš!‘HHMMÃØØ˜?oÕ-{Ô>ï©ê燱qá©xùÊUx)|ðþt|}|pvvÖ•]¾b}û„Ѿ][Þ�<‘׺÷*σÛ„A¹íµj×)ÏÍ ‚PÉXXX0þ| ÄÌ™3iÑ¢Lš4©Èö£(<=ù|å >^òK>þ„ί¼ÆÑCÿC*•)gnnŽ••)©)�1Ø®mn§¤pöì9víÚà�XûÍ:Nœ8Éî¢Cûö%&¿ýò3ž¤¦¦òÛÎßuñTä>õõë‰�‹G¡ð,ñµßLH`×î=¬ûvƒnžJ¥"£Œ×è„çI¥ê##Bå׿BCCùüóÏ +umÌÃd23¦¿‡—BÁî½{õ–12’ Õj;bÐÊÊŠvmÛð˯¿²ó÷?èØ¡=Û·ç·�¿³ã·ßèѽ[‰q88:àâìL`@“'MÄÚZαãÇ»O}^ ¢¢® Ñhô/÷ôd̨‘EFOæeßA.—?úÍ„瘨á™222bÙ²e´hрѣG—º6æìÙsœ §qãF˜ššrèÐaboÜ 8èñÿï�ôñöÖõW¹7b Oï0Þ÷6îîîTõó£G÷ntîú*ùùtìоÄí¦¦¤bnnN²2™Mßo&--�&�=vŸÊ¤â£.{õèÁ¬Ùs˜8i2cÇŽÁØØ˜¼ÜûÃÑèO¿ƒhÔ¨!�7";;‰D¢«é„‘HdAxæš7oΨQ£8pà@™jcRÓRùvÃwLž2•¼¼||¼½Yºd1¯téÌÙ³ç¹îûÓ§¡ÑhèÝ·?¹y¹têØ‘¥-Ö-ïúJƾ=ž7^/ìsR·n(övö´jÕ²ÈÕ¹ï©R¥ .ÎÎ4h܉D‚µµœ��¶ýô#µk×*Õ>æèèÀ¾Ý»˜4y õ6A£Ñàéá�×ÝœWºtæ³e3oÁB®^½†ÜÊŠïO‰ŒðB+×QKYYY(•ÉH¥Òbíׂ T~jµµZ�££%–#A(CµtëVrk9æ2FFFÅ.F'Bå¥ÕjÑh4ääæ’”¤ÄǧäDFŒ`¡¼”k"cbb‚…¹y‰w‡¡ò“H$d?pÏ/}ÄFAÊK¹ŽZ’J¥¢Fžs‰D4 ‚PaÊ}øµHdáù&>ã‚ T$qAA*-1üZ„r'F0 ‹©´#Ÿ„HdA(wb£ ¼xÊ2²ñIˆDF„r'F0 ‹«4#Ÿ„è##B¹#áÅUÞ#E"#Â3!Ax1•÷g_$2‚ ‚ TZ¢�L%tþÂyÒÒÒH¹�RÑ¡Ï;{;ìmm ®èPAJE$2•Ìù çÉÏÍÅÇË/ïŠG0p§ÂÃéñzXÙÖ9yœ‹/TNQ ‚ <="‘©dÒÒÒ­]»¢Ã*m)ïVOhÝ<°·œ¢AxºD"SɈæ$¡ÌʘÈ$''—C ‚ OŸHdá9WÖA„ÊD$2‚ðœÓ—ÈÜN)¬Ù³·³+¾‚&-B%"’••�©iŒ�ÅŸExzNdRSSuóRRR°µµ-²\¤1ÀňÒÒÓHNRVt(ÿ™ƒ“#v66Vt(B9ߘäÜ…‹ ·²¬èP„çȃ‰LzF�F÷\£Õ’–žŽµ\^¡ êbD�†šµêVt(Eìßóç…I``@9E%T4ƒIdn§¤yùJ±ùff¦Ô­]³"eòm®ÇÆQ?´V…ì¿,n)o“œ’ŠJ¥FffŠ»«3æ2½eSRÓ¹yK‰Z­ÆÆÚ OwWŒî6'dçä—Hn^æ2n˜V1y–/Exê ™Ì¬,Ô*U±¥j•Š;™w°² ´P(-=�—Zµ­è0ôú/£ðýµ¯œ¢ �Á$2¶66Ô¯{Xq~^>ç#.áêì\�QUjµšìœ\¼<Ü016ææ-%×cã ¨Z¬lnn±ñ xyº!33#:扷”¸¹8¡Õj¹‡­µ5¾ÞÜLH"æF<þ~ÞÏþE O�V[Xca.+1¹}°œh\ º9é?t^W* øõOÌ`## UŒ ùk4Z"¢£p°·Å͵0‘)((àÚõXÒÒ3�J¥¸89àîæª»‡CfV6Ñ×cÉÊÎÆÌÔ…§;v¶6¤¤¦q=öùùØÚXcZ¥ ÊÛ)¨TØX[ã_Õ·L7µÒh´ÄÆÅ¡TÞF 8:Øã­ð ??Ÿ“§ÏZ+™Ì €è˜Xòòò ð¯Ê�ÌL¢¯ß ;'™™ ?/,-ŸìÖæR©…»î¹£½-Qé…MFFEïB‘œš†•¥¶Ö…M .NŽÄÝLÄÍʼnŒ;™h4Ü\‘H$¸»:s>2ŠœÜ\dffO£PqĨ%áy"Žgáay¯¥ë1±hµZ|½½tó"£®¢V«©H5?n)IHL@¥Rq!âr¹%µkãæêLäå+dfe…µR##êÖ¡fp ™™YÜÉÌ"( µj“��ÃÍÄ[eŠñF|<)©éU§F`unßNáVR2¦¦¦X˜›“–‘¡+›–ž�½�-\Œ¼Œ½�-uk×ÄÆÆšÈ¨+Oýƒ™q' sY±$ ß‹•›ËÌP©Õ¨Täæå#33Ó%‡&&Ƙ“››ÿTãž-­V[æI •¾ãuøˆ 1Bÿñ\Ñ åÎà™Ô´t’”ÉT¯æ§û"ÎÉÉ!#ãU}½‘Ḛ́–öëH¼U˜È$ßNÁØXŠ—§f¦¦89:`ogË­[÷«�ŒŒ011ÁÜ\†ƒ½R© ssÌÍeØÛÛ’™™U¦8“Px¸a.“an.ÃÉÑ�”Ô4ìílu�sóòÈÉÉÅÖÆ†¤äÛ˜˜˜àîæ‚‰‰ 7òó ÈÊÎ~o©éÜNICáá¦w¹Z­.’àI «TjT*Ò‡’#©*uñ~BåQöD¦¢#~6 HJJªè0„2zøx�0a0a„âIù‹r@¿À *‘Q©Ô\¹�·—§®Y 7/¿°é©JÝ<333òòótËÍjú033#7OM‚±±1�úþs©1jµZoY}Ôj5jµšËW®q쟓ûç$q7(((lº²³³!==•JÅíÛ©XËåKÉÏ/ ''W·Îß'Nèš¼žTJZ:7âñõöÄÌ´ŠÞ2R©´è¨uáccc)ÆÆÆ¨Xvo¹±Ô`Z …ÿâÞɼ,Ó `ò”i¼÷þåº�cÇÛÏŸ”ÔÔrÝÏ‹äÁ$eöœ9¤§§ëŽÛôôtfÏ™c05Œü¹‹šuê"·u Ó+¯>“}æä䔪ÜótlT"{333\œ�ŠÌ75­‚F£%?ÿ~b’››‹iSÝòÜÜÜ"ëäææ–øe^¬/cû6J¥RŒ�¥øW¥qƒz4nP�& ëQ+¤ð&{÷jz’SRQÞ¾�“£½.N sÝ:÷Ö»×—çIdfew3?oÏGvè433%;ûþ�ž�“‹±TЉ±1f¦UÈÉÍÓ}ð T* T*ÌÌJx…JáÞÉ<3+›´ô ½SVvÎUñŸÈ´mß+{d–Ö¸{zój·žœ÷»�ì툽OÌ�8œ�ÉȸÃí”TjÖx AÒM ªT©‚“£1±qK¥˜™™QPPPäbv.NŽÜˆ»‰F£ÁÞÎö~œ±q܈»‰££=$¨TXZ˜ •“••õŸ®#Ÿp Gd234wOÐ$H$põz,¦¦¦x¸:ã`kCä•hÒÒ3�™™‘˜”Œ½]a"%·²ÄÈHBÂ-%ö¶ÜLLÂÂ\&:úVr÷¾°Íefdfe©‘ƒÂfW™™éý/v¨‘‰‹�gÑ‚y„…½I||)ULL -=ƒ3ç.pæßó$<ÐÉØÓݕظ›¥®¼G£Ñ��“K|bgÏGê¦{ýtÔj ¦w›åÌÌLQ¸»—p‹È+јËÌpqv@"‘àëåAÆ�L.^ºJA� /O÷÷+TV±[˜Ë@"¹ß‚$‘`a.3È>––VØÚØP#8˜óærçN&—/G…ý[>˜5?ÿ@<> >’Ô´4ݺýÆÛÏ+{Úuì @\\]ºvCnë€`0û(²¿õ6\³6Ž.nôèõ†®ÿL||^Š—›˜˜P½š_‰Ë--, Ö_ãæê¬Æ àîê‚»«‹î¹‡›+n®ÅÖst°ÇÑ¡ä_Þ ¼z—I�ŒhX/´Ø|+K B‚õ_aÒÕÅY—œ•…‘‘uBô¿v�VKNnІxÛÙZcgk­·¼¹LF@5ß2Ç ®‡Oä–æ22³²u�^nh£– X¿a#Ör9AA…Çùü…‹øáÇ-løölml7a"C‡�`ë–øóÏ],^´�Î�:�™™…V«¥çëaxzzðϱ#deg1~Âý/â}ûö3~âD6oúŽ�5:b“&Oa÷ß�|û6æÜéSX[¯†ïÔ±#Ë>]ÎÄwÞeâøqxxè?'¼öjWÒS  lü~3Ó§Ï`Ø�ÁhµZz÷í�««+gOŸäTøiÞìÝ—úõêPýi¿�O$11‘3fpôèQ~þùç ‰áiŸ�6 eËÖ¨…«‹ _õ9_~õ5nnntéÜé‘ë†õé‡LfÎ_û÷rëÖ- й¹ŒqcßÒ•IQÞB*5*vë™O?[NFFçÏžÆØØ˜¬»ŸÉk×®QŤ '�ÿMNNó.¢}§.œ ?‰……ùc�}k¹œØëW�Âþ üźußr`ÿÜÝÝI¸™PìuTöcÓ`¡|h5Ü]�Jî/$<ÿôœø-ïõ£Ò÷¥` ‰Ì¤É“™>ã222ÈÍÍeά™ØX&à_¯^Ëâ…óiҸ˗}B­Ðz$&&ââRø#Eáé�££#ŽŽŽü{þ<§ÏœaÇÏ[qr*ìƒJÖÝ& «>§OXíÚ^ÍvòÄ tïõz‘xüü|õÞ­y³¦ìß»›�–,%(¤6íÚ¶aÑ‚ùøùýA`dd„™™‘‘—˜üî¾[ÿ-~~¾„Ÿ>ÍÑc}gggºtv¥aƒúüñçŸõe0uêTÞzë-¼½½+,†²'2�ï¹ô£E,ýhPXS²`áb¶þô#½ûöç豿éܱË?[VdÄgdä%>µ¨HÜÝÝ©^ÝŸ©S&³|ÅÊ"‰LI÷ϳ–[s3!er25CBpt¼¿ÌÒÒï»—YýÕøV­ÎïüA¯ž=yìßó`S–•\NvNW®\%0 ë€âÉxe?6 ¦iI(R©;ÛÇž[•õ:23g¼ÏÉãG9sê7|˲O?cÕç_�““ƒR©ÄÏï~ mÕª…�¯ÇÄêÝV||DG_ÿOÇ~ýzuùbÕ ¦Ïø€€à¾ßüƒÞr•ùØ52•Œ�ý³ï\'TneILŒŒŒÐh4Ò‰óar¹5ÎwoQâããÍÖmÛ9tø£G�ÄÑÁ�«W¯Ò°A}®\)¬J÷RxêÝ–B¡ 77—ØØXŠâMØžž4kÖ”%‹>QÌ...|¼d1¾Õø÷üù"—‘X¸ø# ˜7wv‘}}[›'ÁX^~ùå±x¨ïa½zõ8yòä3‹ãÞñ¼zÍZNœ8¡·L£F�4p@áIé�ÿË—£Ø´é{΄Ÿ`ÑGKiÚ¤1UªT¡qãFDF^âÕ®¯èÊ*<äåå¯Kf¢®^EQÂ1ø°*Uª0ëƒL™üÓ¦Ïàµî½¸q·IèA�†Øq¸¹¹"“Éyì_‹¾N^~ñËŽôéFï°7ÙüÃ� :œÚµŠß?°2›¢F¦’±·µ%"2²¢Ã*‰Ä¨”“�FÃù ç±5€Z¼ÌÌ;$ß¾MtôuÖ}»�Ý{öмYS†ÌœçqôØßDDD2vü:wꈫkñ¾n�4kÚ„!ÃFpöÜ9HIIÑ-п߬[ÇO[·OTÔÝÄãüüË~ým'×®EséÒe–}¶333|}¼‹”»zõK–~²�— ÕjÉÍÍ¥  €�5¨]«#FŽæÂŋܼy“#G�ý§÷¬<½ûî»Ejí>ùä^~ùågšÄÀýÆ!ƒáéY[·m'&&–ÈÈKÄÇÇóïùódgç„T*%-=½H,•ýØ52•L`P0/\$üÌRn§<~á…·gÏ®R—µ³³ÅÖÎŽà àrŒèñ<ÜÝ™MWmÿ(‰‰‰|õõ®EGcl,%¤F ~Þº'''è·°ñûïÉÍÍÕ�¢{ó Ö­]Íö­?2ñ�wéÐé233©êçÇÁ¿ö!“•|M¨Õƒ‰É{Ó¦òîÔid¤ÞFn-ç½iS‹”‘”2‘9pà/Îý{žMß­`à€ìÞ³O/_Z·nE·×Š_Ðnó¦ï˜0i2-[½Œ¥•Çá­1£Kµ¿;~ã³å+ÈÎÉÁÛÛ‹¯¾X¥»ß_AAãÞžÀõ˜‚ƒùuÇϺ‘Gû®®®Ìžùo�ŸH^~Ç ¥y³¦ 9†ääd˜0~�6àÜ¿ÿêb©ìǦ$U™Pêtuï¾ýtëÞ½ÔmŽ7o&`oo§·£“ χÂ+X§à¦gäß=â\P¹ìرƒžoô®è0Šùé‡M´kß¡ØüqoöÛùìÓeÅ–ñç¿Óµk×r�ïiX¾b%Û¶ÿÌ�}{*:”§¦4牄íÛ¶ÑæåÖ¥Þ¾8«‚ •‡žÒŸ-û¤Äe†2 O(?¢�Œ ‚PiTÖQx¥5ö­1ÏUm̳ jdA„"œ_¨‚ü—Qxöv[ áé52‚ Bv66œ:y¼¢ÃÐ뿌Â3„Ë åGÔÈ‚ EÉ_ö�œ¤¬èpŠ(Ë(<{{{líì | 7 –HdA„bõßN �hZA¡Ò‰Œ ‚ •V¹'2•mè› e#>ã‚ T¤rMdÔjµ8É ÂsN«Õêî#‚ð¬•kg_•JEvNZ­#£Âáp‚ <´Z-�†ìœ *:A^PåšÈ899¢T&“�•­»– ϵZ�Z­ÆÙÙ©¢CáU®‰Œ……å¹ A¡…Ÿ çxøßh�óIOMˆ/*kW)U†6"´VhE‡S„¸ŽŒ ‚ ×¾ã¿sòÆn2CŽ£¶Iªèp„ t�¦9±çriùI´®_ü.äE$2‚ B1ágÃ9·›ôÚ¿Vt(‚�PÛ$‘^g'Nƒ�™3µCêTtH€¸ŽŒ ‚ Çñð¿¹ãe˜÷[*V¦÷ß;õwE‡¡#A¡­q¾hNôRÛ&¡•æUt:"‘AŠ{…G1¤ãC$2‚ ¼� HJªø‡ØØX®]‹®è0¡Ò‰Œ /¤ÉS¦ñÞûTtŒ~ëm~øqKE‡!•–HdA08mÛwÄÊÆ™¥5îžÞ¼Ú­''O�zªûxÚ·Oùië6š·l…µ�#.nžtìÒ•ÏŸªû¡8‘È‚`pââãY´`7ãbØõçNììléЩ É·oWthz-þh)ƒ† £}»vìÛó'»wýÎ+�;!·’Wth‚ð܉ÌÈÉÉ)6O¥R¡R©ŠÌëÖ£sæÎ{Va ÂsÁÒÒ [j³`Þ\îÜÉäòå( °Ë³fã爇‡¡ÃG’š–¦[·ÿÀÁxûùcecO»Ž�ˆ‹‹£K×nÈmð fÿ�Eö·~ÃF‚kÖÆÑÅ�½ÞÐõŸ‰��§mûޏ{z#·u`öÜ‹¬—��ÀÜyóYòÑ"ÞŸ>�zuëR3$„Ñ£Fâå¥àô™3¼Ü¶=ήÔkИ_ÛYâëV*•ô0Wwþ�Á,\ô‘ûöí§A£¦8º¸aïäž½{Ÿð]„ʯÜ™ððÓ¼Ö½'vŽ.Èm¨ß¨ [~ÚZÞ»-µÔÔ4Ž›‡r[jÖ®Ë÷›xìzBáãWdžZ­FnëÀ… ‹ÌïÕ³­^jùTã„EAAë7lÄZ.'((€ù ñÃ�[Øðí7ìÙõ1±± 6B·ÎŸîbÖ3¸É—«V¢Õjéùz2™ÿ;Â÷7`cm­+¿oß~ÆOœÈ'K?âÜéSäæå1iò’oßæà¡Ãìß»›Ëç1lh‘øöìÝ�F£aðÀzã¿�’BÇN¯Ð´iNÿ›qcߢwßþ„Ÿ>­·|XŸ~¤¥¥ó×þ½¬þò ¾üz5+W}Àň´Z-gN� üÄ?4lÐð¿¿±‚ðœ(×+ûþïàA:vîJÏÝÙöÓ¸¸¸pöì9ììlËs·eòλS8sö,Û~ú[;[.]ºŒ§‡Çc×+P ÑhŠÌÓjµäçç+Û;ìͧ¯ ¼(&MžÌô�‘‘Ann.sfÍÔ%_¯^Ëâ…óiҸ˗}B­Ðz$&&âââ€ÂÓGGGù÷üyNŸ9ÃŽŸ·âäTxƒËÐÐP²²²X±êsú„…Ñ®m[&Oœ@÷^¯‰ÇÏÏcãâ§Ì¤¤$œœœ011Ñû:~Úº ;{;fÏü‰D‚··»vïfÍšo]Qôʨ‘‘—8tø×¢"qww§zu¦N™Ìò+7ö-,--pwwÿOï© <�ʵFfìÛxµë+¬_·–—Z¶$ zuÞx½/·n;ýû±wrÑ•U©T›šsöì9úöˆÂÛs+Ú´ëÀw7Q­z2Kküª T&�¤TÒ§ßœ\Ý©êÈü…‹tU±{÷í£nƒFØ9º`cïÄî=E«b/\¼H§hÔ¨!ÕýýéúJêÔ©­[þíú ×ÄÎÑ…n=zqë�ášé雚clj^¤é¨nƒF›šÓºm{º÷|�fÎà×ßvR£fì�\t¯-&6V·njj ÁÆÞ {'‚CjóÓÖmœ:NƒÆM±�Ûâê¡`×î=OøÃ5sÆûœ<~”3§N°q÷,ûô3V}þ999(•Jüüî׈V­ZøøzL¬ÞmÅÇÇcff¦Kb–��ÈÚuß"·u@në@×n=ÈÍÍ##ãÎcã´³·C©TkR¾'&&__$I‘x¯Ç�5&6SSÓ"‰J5??bco<6AxQ•["s=&†‹#1|èã ëñÇŸ2{Ö\¿ÅW_®"''‡a#F1Þ\’âØ¾u ŽŽ¼Ö‡´´4þµŸ5«¿äË/¿fùÊU\¸XX{îÌ)΄Ÿ QÃEöóJ—άXõ9+V}Î�;EOZ{÷ícÜø‰,ûd)çÏ�&77�‰“&ë–[[ËÉLO!3=…÷¦MÑÍ?vø ™é)ü¹³ø=J®EGc&3ãÜ™pÂOÇÈȈ±ãÆë–¿Ù»/qqñ9øgOŸÄÆÖ†K—/0á�É´hÖŒ¸˜hŽ>H½ºuÿÓ{+•�\n�³³3>>ÞôìÑ�—^jÉ¡ÃG�Éd8:8põêU]Ù+W {)<õnK¡P�››K¬žäÀÓÓƒQ#G�‘𬛲ï¤!—[=6ΗZ´@­V³é{ýMÒ …‚kÑ׊Œ’ºrå*Þ Eñ²ž òòòˆ��×Í‹ºzE ¯K„rLd’nÖ\x”¢™¦$ …'NŽŽøúø`llŒ¹¹Œ‹#066¦fÍ""#9tè0_~±Š€êÕiÙ¢Ó¦N᫯Vë¶cii‰‡»;Þ^^ÈåEGLŸ6•Ÿ-ã»�›ðôöcâ¤Éº„fùŠUôíFûvmqsuåÝÉùmçïEÖ733ÃÌ̬H•s•*U033£J•*z_—¹¹9înnT¯ÎÄ osøÈQÝkÙ·?_õ9ÁÁAx)X?¯µ\NÔ•+äåçáãí�½½Ý~oÁÐefÞ!ùöm¢£¯³îÛ ìÞ³‡æÍš0tè`æ|8�£Çþ&""’±ã'йSG\]]õn+0 €fM›0dØΞ;GBB)))ºåú÷ã›uëøië6âã㉊º¢KŽÇ×ׇ·ÆŒfìÛãYúñ2NŸ9Cdä%~øq —/GѳGwRn§0sö®_�aÃwÙñëo <(üAtáâErss ,ŒsÔ˜q\¾ÅÁƒ‡X´x Æ ~²7Sžcå–ÈØÝý’½Sïr‰DBY.ã`bb¿ýÊ¡ÃGðôöeƳP©TÄÄÄbjjŠÇƒU±ÕüŠ4×½ùûÈ!þÚ·‡½ûö3ppa-Ò̈́֬]‡…Ü ¹-]ºv#77—ŒŒŒÒþÎNNddd V«¹~½°ZÙ×ÇGoÙ/¿X…­­-ÕkЧ߀"Í\‚ð<ñpwgò”i¸{zS·ACV®úœ÷ß›ÆèQ#� ¯÷êIßþiÓ®^ kVUâö$ ?lÞ„——]ºv£zPÇþ>NH�`ºtîIJ�—²`áb‚kÖ¡uÛöì{hTÓ£|´hŸ¯\Îw7Ѽekš¶hɪϿ I©ÄÁÞžßwîàðá#ÔmÐ�O?[Á¦ïÖS74€‘#†óωlظ €Í›¾C.·¢e«—2|$Çá­1£ÿë[)Ͻrëì[ÕÏ/…‚uë7ТEóbËmmlÉÈÈ ++ ‹Rm³AƒúìÝý§N…ÓõµîxxxмySòòòˆ‹�×%3QQWñÒSmû8µk×bÊ»ï0|äh´Z- OOZ4kÆÒ%‹‹•535#/¯hÇ^©Tб±1yy¥¿™–‘Qa.©Õjqws#//¥2Y×lö 7WWÖ­]Í‚ùÖ§&¾Ã¦ïÖ—ñU ‚áÛ³ë�G.711aά™Ì™5Sïò¤ÄøbóìíYýÕ%n³_ß>ôëÛ§ØüZ5k’—ýø¾2½ÃÞ,±ch�:ìß»[ï²æÍšr9â‚î¹££#ß­_§·ìØ·Æ0ö­1��E^$åÚÙwé’ÅlÜô=#G�áСÃD]¹Â®Ý{Ø»oÕqp°gÖì¹\�‰ázLÌ#·¥R©øëÿ###W7W�IIM!(0�fÍš2rÔ.]¾Ìÿdá¢Å 6¤T1®Xõ9âzL áá§Y¿a#��H$èÏšoֱ姭ÄÅÇs9*Ѝ+Wðñõ!??ŸŸ¶nãzL ‘‘H$ªúùéÊŸ:^¦÷+((� À@Æ�Ÿ@\|‚CjÓ¨I3j×®…µ�µÞm%)•Œ3'WBjÕÁ¿Z5F�(¼nÄ�›7!—ËiÞ²ƒ‡ gøð¡Œ+ů­VË… ã‚ T$1üZA(æbDiéi$')+:Á€889bgcC@@`E‡¢#A¡ˆ‹h4jÖª[Ñ¡ålÿž?éñzX™Ö9uò8‘”STe#A¡ˆ´ô4^jÕ¶¢ÃžmoZ·‡þÚWNÑ”�HdA„"DsÒ ¦Œ‰ €Ri8LjHdAáVÖC#AAx�éKdFŒ À—_|Q|©(™'$zö ÏC‘P^ HMMÅÉÉ©¢C„ õp"3qâD ˜0aüq‘冕ƈDæ‰\Œˆ@­VÓ¢åËŠ <5á'ÿ1¨ åeò”idff²ú+=¿8+‰eŸ~Æïîb÷;ÈÉÉA&“UpTBeó`"3gî\ÒÓÓuÏÓÓÓ™=g̘Q¡•ЏûõHKK¥NhýÂŽRbÓs2…Ö­OjZJ…~¶Ú¶ïˆ•�=2KkÜ=½yµ[ONž:õT÷ñ´ûü¸å'š6 k;G\Ü< ëÓ�ÈÈKOukÒ¤ ao¼ÀÁC‡ññó/×ý)•Jºté‚L&Ãßߟݻw—XvÉ’%x{{cccÃk¯½F||¼nÙ‰'¨]»6fff´hÑ‚k×®•kÜÂãh-kÖ®!6&¦ØÒؘ¾úú+]¹ÂÉpˆ™'�¬L~ê'CA0ÉÊä Ý\|<‹Ì#,ìMâããYúñ2:têBäÅó8ØÛWhlú,Z¼„y 2}ÚTV,_FZj+V}N³–/ñ¿û *—ý6¨_�õë *(@£Ñ”Ë~î3f ÙÙÙ\¼x‘m۶ѽ{wnܸ�­­m‘rÛ¶mcéÒ¥üñǸºº2bÄF�ÅŽ;((( [·n„……±cÇ&OžLß¾}9zôh¹Æ.”L«-/²Ÿ­Û¶R +{kÔ$ùöm–¯XI«—ï_ï%=#Ss+LÍ­˜;o>#F�¦O¿ºåׯÇ`jnÅÍ›7Ëü÷HNNfûöí,^¼&Mš„››ëׯ/VöäɓԫW�ÚµkãììÌ›o¾Idd$;wî$33“  P(øä“O8vìgÏž-sLÂÓ¡ÕjË<Q#óÄ ï�*OÂÀ$PPPÀú ±–Ë *ì„<á"~øq ¾ý[ÆM˜ÈÐa#غåþüs‹-¤s§dff¡ÕjéùzžžüsìYÙYŒŸ0I·�}ûö3~âD6oúŽ�5:b“&Oa÷ß�|û6æÜéSX[ˋŷgï~$ ýúö.¶lð � 9Z×wåðá#,^´�—[¿Ä‰“§=f,R©”Q#GàììÄö­?âêêÊ®]»2líÛµ£jU?:Ìà¡ÃYµâ3Z¶hNüͽ5SÖr9±×¯`llÌ�¿þ¢_ÿA`bbÂþ¿þ¢VÍš¸¹¹•ùï…F£!44T7¯nݺDDD+Û±cG>þøcÖ¯_OûöíY¶lÆ  ""‚:uê`l\øõãææ†‹‹ ÔªU«Ìq O®²‡‰DæIÝë[ ÂS5iòd¦Ïø€ŒŒ rss™3k&6ÖÖ|½z-‹ΧIãF,_ö µB둘˜ˆ‹‹ Oqttäßóç9}æ ;~Þª¥JVV+V}NŸ°0Úµ-¬Ý˜˜5‡ó.Pµª_}½šÞo¾AŸÞ…—’÷ðð(ñ}333Ó=nõÒKH¥RöíßO‡öíÙ¿ÿ:´{Ü[¯Wrr2æææݯȗËåz/ŒÖ¼yszõêÅ;ï¼Ãˆ#pvv¦oß¾ºíXZZ)_Òv„g£ì‰ŒaýÚ‰Ì2Äj6AxÌœñ>½zö ;;‡SááŒ7++K €R©ÄÏÏOW¶jÕÂÇ×cbu‰Ìƒâãã133+q¨uBB"»÷ìåÛ ßéæ©T*22î<6NGGGnݺ…Z­F*•Y�T*ÅÆÆFïºAA�DG_àì¹sLž2�3§Ï`bbÂ�ÌLòó󈎾N·×^}l,366¦gϬ߰‘Ö­Z±{÷~Ýñs™·ààà@vv6�F—ÌdddàèèX¬ìÚµk9{ö,‘‘‘H$F�EÇŽ9sæ „‡‡)_Òv„g¤Ìßa†õ�gp}dÂOŸ¦{Ï×qtqÃÖÁ™FMšóÓÖmV‰ôµ3qÒ¤bó«ùWgÓ¦Mÿ©=òTx8Ý{ôÀÑÉ[;{5jÌ–Ÿ~úOÛ“˜7¹Üggg||¼éÙ£;/½Ô’C‡� “ÉptpàêÕ«º²W®>öRxêÝ–B¡ 77—ØØX½Ë===5r©Éº)ûNr¹ÕcãlÛ¦5R©”�›6[¶îÛõ´mÓ¦Ä!Ñ×cbqss`ÄÈ1Ô !öúU¢¯^ÆÝý~ó�«« W®^yd¦ffäÝM|4áíqüþÇŸlظ 'g'6¨ÿØ×¤OµjÕ022âôéÓºyááá¿æÐ–-[èÕ«vvvØÚÚ2{ölΞ=Krr2���œ9s•J@BB‰‰‰z·#<÷>÷_¯^Ãð#õNk¿Ywÿa`‰ŒAÕÈZ„“£#)))˜šš0 ? L£† hܨ!ÙÙ9H$]MÏ£¸¹¹ñáœYŒ?¥RI›6­IOKgåç_púÌNÿ»Hù]{öâííÍÍ„–,ý˜~}û V«ÈÍË£Š‰ ’:+ 4�°>ýhÒ¸ ­^jÁí”ü|‹ÆæëãM~~>[·m§^ݺäääPoo/^{µ+“Þy—éÓ¦–ê½×ÇÁÁ�nݺ1eÊV¯^ÍöíÛ‰��§_¿~tèÐ�€€–-[F�5غu+={öÄÙÙ™/¾øOOOìííéܹ3LŸ>�1cÆ0eÊ7n,úÇT {ß#C"11‘7nYîééÉÀýïߨ÷ŽAÕȼ=a]_麵«iÙ²Õ«ûóz¯ž´nÝŠýûàäâ®+«R©05·âì¹s€þQ 7}Oõ XZÛQ- erá�R¥RI¿ƒpuWàÌÂEéN"ûöí§A£¦8º¸aïäž½{´Vϵ8Ð2dð`¦OŸÎ?Ç�?Ô�¦ðqA~>3g΢šu<^ 6œ´ÔT½Û›0a¯¼Ò…oÖ¬¡e‹æT÷¯F¯ž=iÝê%öïß�‹‹«®¬ª ™¹çΞ­–O?ý”!!ØÚÙãá© =-�M›6ŒÜÚ†ê�$+• Õ¢LJ¢ÿ€¸¹{Ä¢E‹P«T ÕG»öíñðT`ckÇœ9s+üz'bzüôõ×_3lØp½ÓÚµkK^·‚y¸»3yÊ4Ü=½©Û !+W}ÎûïMcô¨Â˦OŸ6•×{õ¤oÿ�´i×/…‚5«¿*q{‰„6oÂËË‹.]»Q=(„c'¤F0]:wbÙÇKY°p1Á5ëкm{ö=4ªéQÞ3šU+>ã§­ÛhÞ²5½ÞC£Ñpè¯x{{){äÈš4kAŸ~xµkW¦L~€eŸ,åÿ;ˆ§—/®^¨ÕjèÔ±+—ʲO?#¸fºõx�˜Ø¢×ûpuueöÌx{üDê5lĆ�uËF�IAA}û„•ú5é³råJLMM `ÕªUlݺ;;; ðÂiU«V`æÌ™4jÔˆ&MšàííÍ™3gøå—_�H$˜˜˜°}ûvþüóOªU«F\\ß}÷Ý£v+”³kcß›6+¹\w*°’ËyoÚÔ"e áñ ƒ©‘‰‰‰åbDË>^òŸÖx”BNN#F�aÝÚÕ´o׎èëѺ“BXŸ~Èdæüµ/·nÝbÀࡘ›Ë7ö-.FD Õj9sê*•ºØõ¦·FF 4 fÍúôëÏ?Óµ‘ß+¿`áB~Üò#ß®û&L˜ÈÐáÃÙ‹Ž5ú IDATòÃzß—¥K—ßϽíÝý?ý_«ÕrøÈZ´hÁÌ3HIMÅÄÄ„‘£F³vÍjÚµkÇõèhìííÑjµôîÓ™¹9û÷¾/ƒ‡ E&“1ö­·HNNæÐ¡Ãœ?…µ\^d_‚á:d‰‰‰ÄÆý…¥Px2hà@ƒýîÙõÇ#—›˜˜0gÖLæÌš©wyRb|±yöö�¼Šo¿¾}tµ#ªU³&yÙ�ï+Ó;ìMz‡½ùØrsfͤuëVÅæ7mÒ˜ÇK¾–Jß>½éÛ§èȨÁÁŒ}kŒîù»“'ñîäû£±ÒÒÓQ©T,_±’¡ƒ•XcUZŽŽŽìܹ³ØüÜÜ\Ο?O›6m°´´ä‹/¾à }÷éêׯ/†[�‡Ï‹ÌgÜÛãu�^nhç ƒ©‘¹u÷š �ê�ÿ8÷F)øøxcllŒ¹LÆÅˆŒ�¥Ô  2ò‡á‹UË©^ÝŸ-š3uÊd¾Z½F·KK ÜÝÝñòR<¶�¼09Õ׿@ËÐ!ChP¿>cÞ{w¾¶ð?­–ÕkÖ2cúû4jØ�€êÕYöÉÇìÜù; E¶u+éîîîÅöó`füðþï/G‡Â‘Õýý‘J¥Èd2.^Œ@jdD�5ÐjµDFFrøÈV.ÿ ÿjÕhÞ¬ïN~‡Õ«×Ù®¯�ÎÎÎ8;;ë�GL†7Mï=är+ÝUbär+¦¿÷Þ#ÖùÏAÁÀ´k߉€ ˜˜˜°pÁ¼rÛOnn.}ôÏ÷m-ž[÷¾K˜>[ö Ÿ-ûÄ`kmd0‰ŒýÝêÉø.Ô$‘HÊÔÁÈÄÄ„ß~ý…ÃGŽâí[�fÍF¥Rƒ©©)îî÷›©ªùùûÅZZÚ.(vïÑgŸ-ãïãÇù~óf]ùìœl”J%>~>ºr¾~¾\��)²%;»Â¡›7ãõïI")üòyèrf÷bx8Jcc~ùe;GŽÁ¯š?³fϦ@UÀõ˜Â÷ÅÍÝMWÖÏÏ�Ø7Š\$Mÿëÿ ýßâÅ‹016ÁÄØ„Å‹=¶¼P>’ãõÖÆ”—þ>Bò­Ö®þª\ïÁdccÃÈ»wK*ŸÿòÉ�LÓ’ŸŸ/ …‚õ¾£EófÅ–ÛØÚ�‘q‡¬¬l,,ÌKµÍõë±û��œ çµî½ððð yÓ¦äåå¯Kf¢®^EQÂh‡ÇÑRÂõnÍ‹µµ5«V­`È�ah4´h133Ãáúõ //~åJáˆOÏ"ÛóññAáéɆ ßÑ´iÓb»±¶±æÎ�;deeann®[WwÀÝýZzp›õêÖeço¿~ú4½z½�»»;M›6!//�¸¸xÝh‰+W®àééYäÀ5ăX(�åË?JQ-,ß�ƒ“ ý")Ë9ÝÈÈ�Fƒ½Ý*Ä`€%‹Ö§ULL {W®E_Cj$¥I“Æ8ØÛ3{5R7t¯$*•Š#GŽR§NÜ\]qrt$5%•ÀÀÂÑ £ÆŒcÉâ…$&&²hñÞ;æ‘Û+QIÕlÌoÓº5Ú·cÓ÷›ïV•hüp#G à}y ®bQ©T=vŒ:µkãêì‚££©©©T¯N“&�7n Ì'ñÖ-–,Yʘ1£ï®[|[ÂsJü}_xv66œ:yœºõVt(Â3 ‘”¶qF‹F£áÂÅ ºNÞ†À ™W»¾ÂÁ¿ö1mú º¾ÖµZ�¯¯£G�äå—[³q÷Lš<…/¾ü ™™µkÕÂÆZÿ…¦”J%cƽMtôu,ÌÍiݺ#F…Þ¼é;&LšLËV/óÿöî<.ªªàøgÆdS¶pÁrÜ—ÔÔ7TÜrK3³Ò¶Ÿe‹šiiå^–Y©O›Vše�>¹+‹i®åÎâ†Ï#(È"$›0ç÷2‚ Ë£ß7_ÃÜsÏýÞ;sÏùrï�;6¶¶L|þ9^}åå’}Ÿ#÷©™ûÑ‡ìØ¹ËPÚÔ·ÑëõŒviééôéÝ›óçØÖ€ý ØÅ¬÷ÞgȰ'Ñëõ4hPŸI'òD÷î¬úþ[¦OŸÁ¿¾þKKKZøøPÓÎ.»-Cþ‘ý &6–×^ŸÂåËW°²²â‰îÝxþ¹ (¥øqõ*¦N›NÏ^}°±±a„gyéÅIrDæ!£‘×÷¡×¤ISNŸ>û¸#wÜ}Ðì4º®££#ö•ê¾?š„Øh£{­À `† jô_lQQÑ8::xëîÁ¦M›èÛÏÏäß8+DyÒjµìؾ £êgffo¸±[Aô¾@qÆô 7lÀ·g£Û—^¥”ä…xÐÈûYaN$‘)-¹fD-0.&Ó“D¦¬)%×È!ăΈ¾>>>½^À¹óç™={ŽaÚìÙïóè#� Õjqpp(—˜ÒÒÒøyÝ:6mÚÌ­[· Ï[YYÑ¿¿cÇŒ¡F�¥�§œH"sGff&@±²Ñ‚(eÚ#2Ÿ}¶”mÛ·³kç£ê§¥¥aiii²x 2ìÉá´lÙ‚Y3g–ërËZjjjž�955oŸ¬Xþ¾¾¾™¢¢Ó×Oyã  ¿çî‹,Xhxlo_‹Uß_.1ݼy“ïÎ$"""ß´””Ö¯ÿ�#aþüyØÛÛ—:¦òP)™n=|9xð:ggZ´ðaÊë“ñíÙ³\–Ÿ••…�½÷í¥E ŸRµUÐé?¿ÿμyó8>;;;Z´ðá—uëJ”ñvêô5kÚ•,õíçGÿþ~üß«¯8}Ë–­ŒöYþùçlmmñðpg̘1¼6y2UªT)vl9† Š»››Éº>ø�U«W’o¾ùÖ[ìܹ‹Ð�S¥ZÆŸþɈ#‰ŽŽ2ÊœÙïQ³¦ûàW¯Ò£W\ëºcmgÏì9pøð_tîÚ�ZŽ::tê‘#G�ìrê´w õûõ÷7,ã~óähÓ¾#U-¬èÑ«?­YË#�›Qæ&ž�4!6öFÑ+’sŽòN9wîz½ž—^œ„gÆ´mÓ†gÆ= Jyå ]ïJ�ºn89ëèÚ­;���†yƒ‚‚èÐñ1\\\qÖéà‹e_УgOPŠ+—/Ó©sÜÜ=°«Y Ÿ-سgO®ó¤Š·§N£†•55¬¬óÅŠ*UªàìäDÝ:uèÛ»7žÏæM› u~üñG¼}|pqqeøðÄ\¿JѤi3¾ùæC½-[¶àQ¯>ú¬,FŒ™}.øÎ´¿¦[÷'pÖéèÜåqŽ9J1qâ$ž7ÎPoΜ�ÌÛ·‹\?PÜLJ2l»�>š JáêZ›àà`PŠ?üˆ&M›áè䌳NÇË/¿BjJŠ¡íððpzúúR³–=µëÔ¥Sç.œ;w.ÿë EŠ“•¯¿þš^˜X`ùî»ïî?o1ûú‚ÛÈn§I“&X[[жMlllhܸqNcF¶Wº˜‚‚‚î?_ƒ‚JS9¨4‰LŽ””¾ýn ‰øx{w#޽{ÿä�à@"Î�fÒ¤ˆ‰�¥¿ÿ † Ĺ3aôéÕ‹£ž"##ƒàÝ»ùnÕ*‚võß+,ùd@¡óä8¸o/ÿÜŒgÇÖͤ¦¦ò¤—˜7÷Cb¢ÿÇÆ¯ÇÙÙ©Èøs2âœÒ®m[yrøŽ=šgZBBG�%0`‡d�¿?C†ãäÉ“(•=*¥8zäoþ>üíÛ·G¡î¼ç‰‰‰?~œ?vsêÄ ôïϘ±O“ššŠRÙõæÍýˆø±Ä߈Í[Næžóøòå+lÞ²[”R1å�7ødñbŽ;JZZo½=¥ú÷'8x·aÞ  `ú÷÷C£ÑdÇxçêù˜˜üa�¿?a!!ôòíÉè1cIOO§{÷nS^�^¾¾|óíw†�ÀÆÚš:uêàáᎭ��aÇÈ=¿‡»;îÌxgqqqœ9sÆ0­J•ªXXX`aa‘ÿ ¨àÚµk¸yÔÃI§£ióæ„……ñþ¬Y(¥øê«åŒ5___\]]ycÊëlÛ¶ ¥ 0�ôôt”RlÛ¾ ÿ íæl‡µkÆÅÅ…×_›Œ““Ó§M#êÎvïÙ£ÑÑÑœ;wŽnÝ"üôiìkÕâÔ©S¤§§sàÀ~zùúæ‹;>.ŽZµjñÖ›oòåWËÉÈÈ`Ù_2pàš7oŽ……11±dee±|ÅJÞyg:M7¦Y³fŒ3†­wÖ¡°õËÙÐ9Û®J•*wwÚ\ÛÏÁÞ:´oψáÃÙ¿ÿJ)~]¿kk+ÌŸGýúõhäé™�äU‚Ž]Š”‡­¼;cvv¶¨;?vv¶¼;cF!ó¿¯/¨ä‘ÉÝ Êݸá±1ëQú˜rŸ:êÛ·ÏM˜À„;Éܸ§ÇòÜ„ ôïïg¨“––Vê˜ÊC¥¸F`ÖÌŒ9Glmm ­͹óç±¶»{EµF£!úÚ5ô÷ã_+¾bÆ»³xëíiÌ™ýO�Uè<]à[­Z5¶oÙÌŒ™ï±ìË/yiÒ$Þof‘ŸjÊÙQrss¯ËçŸÆÂ…óùléç :ŒÝÁA† jsÏÓ´iNž:u÷¨†áÿÜK ÏRræ¯ae‰�� ñ ¹¦ç�'wKÎÎÎìÝó))·èݧŸ}º„îOtG¡ˆ¾M@` ?þô“ažÌÌL’’“x챎XZZ„ÎYG||ݺwÍUÔµh"""pprλݯGÓªuKÚµmËÎ]»hа]:w¦®[]‚‚¸GÍšµhÑÒ'_ü ‰‰ÔªU _ßžÙWûÿð߯ZÅöm[Q(jÕªEâÍDn¥Ü"99™&Nb⤳£R /¯æE®_îµÈ¿ÝÈ·ž:�3‡F‘}tëÑGE£Õ�ÿõ«${¿‘E‹òê«“ � ß�8úaľ|·Îý8ÏlL[e“ƒƒÑÑÑ|üÉ û™M¿oäÅ—_É“èäÔ/ív*•&‘qvr¦~½zFÕu÷pÇÛÛ‹£*púØ1O1æ©Ñü¼îÆOxž–-[:�RŠªU«f_›Kûöíܵ�£G�á?x(nnnLšø|¡±©BnHdiiÉôiSY»ög‚‚‚èÕ«—aù9ó\‰Œ¤víÚÙÏåŠïnûä›'÷c­Vk¸HÕÒÒÂpÄä~±V©¢¥N�Ú|ºä¦MŸN·n]©Y³&nnntîÔ‰wN×ÜkäˆáüúËzt:g†b¸ò>gWRJáææ†—Wsöïû3ÿò•bÐàAlÙºOOOz÷îE�ºuX¸p1QW£äïŸoý’““±¶¶B)Å«¯¼Âä×^ç±Ç:âíí…R +++nÞ¼‰µµ55kÖdåŠåyþÊÈi³°õ³°° ##£Àmgøk$×zh´ZôÚuj³cçÎ|óçüÅ$„(Ë–-ŒØ5F& Eµ“ëºá©‚Ó/”AL-[¶0$2yæ»Ï<-[¶(<.#b*•æÔRq røpî<.^ºÄ•ÈHŽ?Àÿ®^%$$”””¼š7§J•*ÜL¼Yè<�†Fžž¬ÿíßüïêUŽ=Fff&ìÙCRRµëÔÆYçL|B|ÑÁ©¼B�:uŠVÿÀÙ3g¹|ék~ZÃÿþ÷¿;§¼²ßÿùÏïDFFòË/¿²eËVž=*ïÅb÷´ix.÷aKÃ4 Ó=6dÇÎ�DFFräï¿É¸ó)¦»%õüÒ´IÞ™ñ.(ÅÓcưjõlذ�«W¯qþ<"" õŸ?ž-[·ðóº_˜0þ™¼1Üy¦r`–‰LmWWvl#(x7mÛ?F›vXöåW„„„Ò¯ÿ@œ]ñàÏ›S^§cÇ…ÎðÉÇ‹ø}Ófš6÷á­iÓ‰‰�ååW&£«í†w‹V<úÈ#¼4iRÑÁ©¼çøiÍZzøö¢]‡Ž|ºt)óçÍ¥_¿¾ädºß|ó-íÚwdþ‚…,ù$û“7¹Ï¯tε #2wÏ~îõ×&“‘‘AÛ¶í™ðüD¢££óÖUÙ;WîçæÌ™Íš5k9qò$ýúõå“�±hÑÇ´jÕ†>ýüÞý‡¡î£�>BǨ_¿­[·ÎÕ.†¸\\tlÙô;»wÿA§.]éÔùq–/_i¨ëêêB�OТ…T­Z•AþÑéœi×®mžØ”RüóÏ?XÕ¨�RŠêÕ«óïßÖÓ­kWC+k+oÞD)ÅûïÍ¢oŸ>Lzée¼[´dÌØq\¸�}[aëçê꬙ïòÆ›oÓ±SÖ¬]›oûæ^Ï{_›zõêñÓ«Y³ög¼|Z2çƒìOÛÙÚÙæ[')R¤T®‚2.i(²­;}BBB"IIIèõz®]»†^¯')9‰„ø„ì¦(Ÿ˜êÔ®Íóž½gÅWËWpûöí<Ï?3îiÜÝÝKS9Ð$ÄFI`P0C†5*øÀÀÀ‡îî§7n¤o??Ã-©‹B—®Ý‰‹½^ê; ‹Ê'üôi\]\ÐëõìØ¹‹Ùsær ‹ŠMQ­VËŽíÛ2dH�Ó�í냂wóù²/Š\Þäÿ{•ž=ž(—˜vìÜÉ·ß~OÆ=É d_:þ™§п‘íSnFç 7lÀ·g�¢ëÞ!£g3dªFÕ-þ<Â<ÄÇÇ3ṸtéÕªUÃË«9k×üTfwðB˜Ž1û¨1ývmWWz<ѽȶj»ºÙVYÅЧwoÚ´nÍ®€@ÂOŸ&99š6iBŸÞ½pvv6zy•�$2e­‡Û¼½š“{w>ñÀp°·ç྽ù'Èë,Dågäiœ¢ê5mÒ˜¦MZÇèe–QL9œyjÔÈ’/«8õLL™2&GW„¼•åÑ�²b®1•IdÊ�££#z½�Fcôu2B!*�œ[X8::Þ·Ny÷õæSy‘D¦ 988~:Œ¦M›¡ÑjME‡$„Âh ½Òs:ü4… ÐåÛ×›oLåE™2äååEHH('Ž'..®¢ÃBQLŽŽŽ88:âÕ¼ù}ë”w_o®1•Idʘ··WE‡ „ÂÄ*c__c*fyC­Û´ãÏ=Á•*&!„âaVÔØ\Z&KdbccQ�0ÄÆÆ:­"bB!f…�Í¥eÒkdT®ÿˇ¦Èå“Bñ0+zl. “&2 Š<rûömªU«–][)¢££INNÆÎÎWWW4ã7€qU‹ŽI!„e£Ãx‰˜6‘QªÈëQ¶mÛ† ÉÉÉlÞ¼™›IIØÛÛ“œœ„��-ƒüý6l¨!Ù)�˜„ÆKIIáŸ[·Ð9;Wt(Bˆ‡�iO-©¢�~ìÙ³—ÐÐP7nÌ;ïLÇËË ­V‹^¯çÔ©¾_õ=äÃ?ÀÖÖ¶\bBïéqÏмy3æÌž]nËLKKÃÒÒ²Ôí¤¦¦R£F�2ˆHQQLzC¼œ¤á~%))‰¸7ÐétT­ROOO4 111h4Z´ðáãŋѹ¸ðÁ‡¡×ë‹l³´1I‘"%‰OHà¹ç_ÀÍÝ{GZ¶jÍÏëÖ¡”Tö¿rŠ¥Oß~üëë¯KÝÎÜyó7î™ ß¶R¤< Å”*ôˆŒ�� ß}÷­á÷¿þú›ŒŒtæÎ›Ï[o¾�µµ ;vàÍ7¦ðÒ˯LÏ¥»LylT!4S§NåÔÉ“¬ÿõìí8{î,înnÙû’UŽ×ž¥§§CìÇéééÒñ0íW(UhÙºuW¯^¥È¼}›ÅÌÜyóøø“%,\´ˆô´4ªU­Ê°¡CØPd›¥�IŠ)ùKxøiúôéC‡öíyô‘F ìߟ–-ZdO~øáG\\\qÖéxùåWHMI1Ì{âøq|{õB§s¡}ûlÞ¼”â§Ÿ~¢~ƒ††z»wïÆY§#+3”âà�ƒ88:‘ž––7oO�F +kjXYžÿñÇñöñÁÅÅ•áÃGsý:(EB|<Æ=‰³N‡NçÂܹsïÌ›·l1´³wïÞ ßÎR¤<°Å„*ôÔÒšµkIIIA)Eðîݤ¥¥Ñ­kWÖý¼¿~ýÈÈÈ`×®”R4oÖŒˆ Šl³´1I‘"%éïçÇòåËùjùrÓèeh�IDAT’’“óNGñD÷îùû/¶mÙJ@` +V®D)E\\ýú s§N>tˆW_}•±O�ãØ±ctïÖ�ëׯöë}ûöñÏ?· Íþ}ÿ~ëØ‘êÕ«çY æÍýˆø±ÄßȾ7T`PSÞxƒO/æø±£¤¥¥ñÖÛSQJ±ôóϹyó&'�'4äO�~*»~~ý ítîԩ·³)j1%ŸZ¢ÐHMMŪ†J)bbbÈÊÊÂÑÑ[t:g”R$$&¢”ÂÊÊŠÔÔÔ"6ˆ÷‘)"&!D~Ó¦¾�‡»;ËW®àý÷góÌ3ãxoÖ,lll@�››uëÖ¥nݺ òIöï?Àk“'óÛ¿ÿ�£ƒïÍš…F£¡^½§Øµkß~÷=Ë>_JÓ¦MÙ½{7 4 8x7>>>ü±g/ÞÞÞìþãzûú¸¿V©R {þê«åŒ5___Þ˜ò:ÃGŒD)…��ÑÑÑÄÆÆâíí�“SÎ4 Õh íä´%„(kf|uçç~œœœˆ¾�km†Îï›6³aãØýǰ²²bäÈá(×ccpp°/´=Œ¸Ñ]Q1 ! 6jôHF�ÉÉ“§xîùˆŒü/?ÿ¼æÎÔ»û•Nç̡ÇQ(®\‰¤AàÁ0ÝÓ³!G�C¡èï×�]»6l(/]bÉ'³jõjž}ö8À§K>η¿ª{–}-š€À@~üé'Ãs™™™$%'ñÒK/’””Dÿ�þ4jԈŋÒºu«<- !LÉœ�Èq`«V-Ù¿o?-[´@«Õ0fÌhRRRùñÇŸ3æ)¬¬j ÑhPJqàÀA||| ÿ‹Icd"#u Qb>>Þ¼ùæ^}urö' ï¤9û•F«5Nvwwã÷M›Ðëõ†›[F\¸€‡‡J) ÄW}ú±qãx¢{wzöìÁ‹/½Ì† iÔÈOOÏ|û«¥¥…áBÝnnntîÔ‰ æóÌ™ïòæ›o2kÖ{<9|"Îaa‘¿!„ 16—F…^ì;Ø ìÙËÅ Ðüüñä0̟˨Ãñ0­FÃÿû_¶oßÁ�Aþ¥¿ ¨¨ù¥H‘’¯¬X±’}î#òÊŽ?ÎÚ5?Ó¤q㻌ï�'ûóØ 2˜øøx>øàC®\¾Ìš5kÙ²e+ãŸJÑÒÇww7Þ{6ƒùccmMçÎ�xwæ,†?ùd�±x6lÈŽ�;‰ŒŒäÈß“‘žÎÓcưjõlذ�«W¯qþ<""@)BBBˆŒŒDŸ•‰—W3’““ÈÊÌ4 å왳DGEsìØ1FŽMÊ­[$'%1bä(Nž< JñÖ[o³fÍÚ -¤H1ËbB&¿³oaí8;;óüs˜óá\f¾û�<=hÖ´©a¾+‘‘|øá\†?9ŒzõêڞƘ�UDLBˆ¼”R„‡‡³ì‹/¹ví¶¶¶tîÔ‰¥Ÿ-ÉÞ—ÔÝzw~wpp`Ó60mú –¯XIƒúõY½ê;Zµli¨?îé§Y¸h1¾¾=QJ1røpƒ=zT�ûêë¯MæÙçž§mÛö¸Ö®Íæß7Ò¯__>ùx‹}ÌÅ‹±µ³eú´i4lØ�Í›·ðåWËIMM¥^=¾üâ ´Z-ýýüØ´i3½z÷ÅʪŸ.ù€’””LfV&âêÕ«øx{sèÐa,--¥ÿ¢˜Œ›KÓ~Bl´ÑK fÈСFeW7n¤o??ôz½Qí~ûÝwtèÐ�vmÛ`_˞ě79vìîÛϘѣð÷Xd;Z­–Û·1dÈ�RÇ$„BˆÒ+jlÎC£aㆠøö4þžqþ={2/’œüq7n˜"QG''jÚÚÒ°aƒŠE!D!d¼,ž’Žoź�LÄÅ‹h4ZZµi_ìEÙ9vä/4(<%™BˆJIÆË’)ÉøV¬SKÉIÉ´jÝ”’R�¥u›vrsB!„¨Äd¼,¿ñ­X§–âââPJkÂ4âââ*:!„÷!ãeÉw|+ö52*×ÿ¢¢È—v !De'ãeI|+Á·_+É2+˜|ù¸B˜/‹«$ã[ñ™œsYB!„¸¿ /ÓÓÓ9rä(�;wª�å—§bßGF)Uæ%%%ÏF�`x.55Õ$Ë2vù%m§¼bBQ¹•×xPP9~â¾½zUh å5¾•I"Í‹/½„G½úØÚÕ¤e«Ö|¾lYYYFmiiÉsÏM qãÆ(¥èÓ·ÿúúëûÖß¼y ŽNÎXXÖÀÁÑ oŸÌœ9‹›7o–h£Ý»ü’”¹óæ1nÜ3•ö…BQ¾Êc,XÿÛoty¼+5kÙãêZ›~~ý9u*Äp$¨"‘òߊ±ï= Љ‰¡Ëã]©[·.+V,§nݺš;—©o¿Í§K>¡ZõêìÛ·[[RRnbxÐÿ+ TÞÏ|ÏŸ?ŸjÕª±kÇvúöî�wóæL|þyÖ®ù‰~ø‘Ç�RÌž=‡#Fæ ÆÕµ¶áwW×ÚßÉ"oO�F +kjXYçÿ¬9Š*UªàìäDmWWžèÞ�ž�}ûö�R,]º/ooìqs÷àfb"·32xÿýÙ<òhcÜ=êñ ILH(pùéii̘ñ. zâQ¯>Ó¦O'óömCÝ ÿÞ€—·75kÙÓÜË+û®� 6oÙbˆyïÞ½ >L·îOà¬ÓѹËã=r”"óömjXYsêäIC»=zöä‹e_€RÑ¡ãc¸¸¸â¬ÓpÏ6BQ©Ý;v•a‰ŽŠâ£¹sY¼hïÎx‡6­[ããåÅË/¾H=�»ãÄ�ú…��J¯gúôwps÷ –½úæ»ßfÒRL¥>µ´mûÆ<õÕªUËóüÝ»S¯žÛw츓ªìŸÜóçn/×cÌ›ûñ7b‰¿{ßCO9�Ï�?Ï/¿þ‚——J)öíßO×®]9FÀ®�ØÙÙ1Á~]ÿ+«W}ÏÎÛ‰ŒŒäù‰ \þ¼ùóÙ¾};[·lfÛ–Ílذ‘U«V£”bïŸòÜ /ðÎô鄞:Éwß|‹ƒƒ ðóëgˆ¹s§NÄÄÄà?xƒüý ¡—oOF�k8zsïöDaØFááá(¥8zäoþ>üíÛ·Ï·þB!*/Sž~  B¯×3þ™q÷©“7†ÂÆÀàÝ»Yµz5»vlçÊ¥‹,Z´¥T‘cXe9µT‚D&ï‹‹›[݃qws'&&'!{ 6 ÚyòœÆsW©R ,,,ò¯¤‚èèhœu.ØÖ¬EûquqåÇV¦;;9ãììLãGE)Å7ß~ǬwgÒ±Cš4nÌgŸ.aëÖmDGGçY~VVËW¬ä�w¦Ó¤qcš5kÆØ1cغmJ)¾þúF�ÉèQ£¨[·.íÚµ5¬ŸV£5ĬÑhX»ög\\\xýµÉ8991}Ú4¢¢¢ -8‘ɳ}ÁÆÚš:uêàáᎭ�M¾7¨BˆÊëÞñ²,KLÌut:U«V-¸yÇ˜ÂÆ@[[[RSS9����aÜ,j 3M)þv.~"sÏ�ƒƒ=QÑÑùžW(®FEáà`�B¡Ñ`ظw71y~»{ܦ %åýÑét:t€óçÎs=šŸ^ƒÎEW@ËŠ”ÔbcciàÙÀð\Cφ\޼’gù·Rn‘œœÌ 'áà䌃“3K>ý”˜ØŠK—/Ñгa¾x( ê¨kÑDDDÚq­S�FCôõhÃ=ôù¶ÉÝöT![A!DåVø(Vº{{bcc¹�y»Ðq˜1°M›Ö|ùÅ2f½÷>^Þ>¬ûå—"Ç0S®[q•,‘É•=õéÓ‡uë~áöíÛyž?pà —.]¢?”RÔ¬U‹k×®p"ÿcKK‹Â]¡¨REKýúõqvvF«Õæ›®ò´g‰““.\0<€»›{žå[[[S³fM~X½Š±×¹{�¸1ìùc7J)\]]‰ˆˆÈ“…Eþ˜ÝÜÜðòjnhçFìuâoЯo_´Z-666D_‹Î» râÎÙÞ…dÚB!*¯{Ç˲,]œ¬¬,Ö­û¥Àé9‡6ŒG�ÉÑ#1sÖ»¼0q§OŸ.t 3Ù™òHdÈÙ@wÊÌïǰaÃ9tðW._æ×_×3fìÓŒóíÚ¶¥èÜé1Ž?ί¿®'êjÑÑÑyۻ󲣞 ²cçN"##9ò÷ßdÜùÓÝRp,yÚºç¹ ÏŽgîÜù:xˆ3§Ï0eÊ›ôë×—Ú®.ù–ÿôØ1Ìž3‡ýûöÍÉ“'‰�‹¥?n¿þú+k~ZÃÿû_Nž<É?ÉÉxz6äè±c„††röÌY¢£¢:x0X°`!—/]"22’'NbêܹŸ/]ÆÙ3g‰ºEjJÊ=ëQvC !„(g÷ë¿Ë 4¨_Ÿ—_z‘×§Lá³O?ãĉœ=s–õëãü¹sØÙÚ‘••Å©S§Š¯^½Jhh(©))4kÚ”*Uªp3ñf‘c˜ÉJ1•(‘É�=999±ß^ììl2ìI¼|Z2oþ¦¼þ_}±ÌP¯UË–Ì›û³ç|€w‹–Ìœõ:ܽ€õîk®xýµÉdddжm{&_ö­Z·eĈQ\¾r…þ~~tïÖ•^½ûâ7`ù[6ýÎîÝЩKW:u~œåËW–¹äãÅhµZº=уÖmÛq+åõêyÜÙù¯¡¹7ÓBQ‰s¢eÞÜ�X¶t)k^G�ž½èöDV¬\ILl,�<Òˆ#†3ú©±EŽ�¡!¡ 2”:n :ŒÉÿ÷*íÚµ-r 3E)Éø¦Iˆ�6z®À `úöóC¯×ß·Nß^4lÐ�%Ÿ,ÆÎήØ•·ääd>̈‘£ =E�Úµ+:¤"iµZvl߆oÏŠBˆ3^ŠüJ2¾•ú†x÷úqõ*Þš:�G›4ãñ.�ùõ—uÅ]D¹:l8/]äÃfSÛÕµÐu«,Ì!F!„xØ5^ŠüJ²½Jõ¥‘µ� ­º+ �ZN$ÆÅ{1åe׎mw1—7œ¹Ä)„3] PÔØ[™ÇÜ"•G"“;ÃL¸cô<¢ìÈöBˆÊÏTGdŠ{ÍyŒ0ù{{{ôz=�FÎûU­V‹^¯ÇÞÞ¾¢CBq2^_IÇ·b%25íì?FÓ¦ÍÐh5îê&ʉB¯ôœ?MÍš5+:!„÷!ãeq•||+Ö§–ÎG\ ))‰„„„b-H” {{{jÖ¬I£;weBQ9ÉxY<%ߊ�È!„BTÅ¿!žB!D%!‰ŒB!Ì–$2B!„0[’È!„ÂlI"#„B³%‰ŒB!Ì–$2B!„0[’È!„ÂlI"#„B³%‰ŒB!ÌÖÿè×ÙCñ?.IEND®B`‚Projecteur-0.10/doc/screenshot-spot.png000066400000000000000000004771211451344070600202070ustar00rootroot00000000000000‰PNG  IHDRc´T�EsBIT|dˆ IDATxœìÝw|Tç™èñßÌ™ÞÔ{¥ˆ^ ›b°cÀq‹“\·o'N6[rãÜ$»›ìnœM²ë�ã8.±ã8q `Ü0`ŒéÅ4a0UB ^PŸ>çþ1Ò 2ªfÏ÷ó�™9å�÷œ):ÏûŒ .˲eËHIIaõêÕð›ßü†œœV­Z5´-„B!„ÖKÚˆŸ´7S\µx^D+³s÷'½®ojjæè±œ=WŠÃa'&Æv;ƒÁÀæÍ›imm%>>“ÉÔm›·Þz EQ¸ãŽ;7n;wîÄçó‘‘ ôvíÚE\\³gÏfîܹ”••ñÙgŸ1iÒ$Æ�‡ßïÇd2ñÀ0nܸK?xqUPU5tîßÞ½{™sÝÜÐvf“1’ÕB!„Q¬m�èÖþÛß|˜––V›hnn¡¥¥•ú†FZZZiim¡¾¾·ÛÓg¦lÒ¤I$%%±eËžþy ˜7o‹€ÆÆFŠ‹‹ùæ7¿‰ÑhÄd2QPPÀéÓ§™9sf¨œ¼¼¿Ÿ–V'@·€¬¦¦—ËE\l,f³9UŒJN§“Ú Pâãã"]�¨¥Mð]¦FG@Ö›�ai4ââ⨯¯Àáö5«¬¬ 5CB!Äà45;‰�±b4C�˜üØ)†ƒö>à:��êZºcÍÍ-Ä'ÄK Ö…Ùl&Q«¡¦ºV‚±^\M1Š¿3„y<Ö­[ÇøñãIIIA§ÓqæÌ<Èç>÷9L&ãÇ�gãÆ�Üzë­ÄÄÄPSSƒ×ë%33³_u2›ÍTWWãr¹ðz½ØíáÓÖB!Äpçõy1Œ(Š�F#�˜VÚé2Œx}õÝÖû~Ìfs”´3‹.F£ _ÀéjD5�ʈEßç`3a™™™ìß¿?” KHH`ùòåŒ92´Í¢E‹Øºu+o¾ù&N§»ÝÎôéÓûŒåååqèÐ!ž{î9222X¹rå€ê(„B ' ˆ‰a©ý=ÝÓ[[ HÖ9?½kËŒE׃  8¬ýìÙ³™={v¯Ûét:.\ÈÂ… îÿÎw¾ÓéqFF�=öXè±Ãáࡇpý„BˆáJ11\õýÞŽžëhquÑEÓ{'3#�ëæÌ +3=ÒUB!ÄH &†»ÞÞãQt9-®2Á< o£»ï¼=ÒUB!„Bˆ+Bj¡ùXL!„Bˆ«�\G‹AÒFºB!„B\­$ë�œŸÞi¯†ùÅ„Bˆh÷w:ǽ¿+bÛ©–HWå²QU•ß>ó<'Nž‰tU„bXÐi4š`<&ýn…B 3=_B«»ó7Šìzfްp׌8LúèþXÝäãÑ—Ïñ…¹ñ¬˜ÙºÔÔâtºÉÎΈh=„ˆ.Ã;±á÷ûùûøGl6ÿö/?D Ãûü\*�ê0&'J!Äðç@Uƒ—w6PRçᇟK’r÷å¬!)§«hÊ´�-!%9“Ñ8¨ýüÓ£º¦ºÓ2»ÍÁ¨Q#Xµòv’{ܮ݊ۗ³ì¦%¡ÇÇOœä¿Ÿü_fLŸÁC|¹ççÓhqØmŒ5šeK—�•™Ñi»º ø�Ÿý V«µS{öíã�/þ‰›–,aÕŠå®ßPùxÛv6mÚÌOüø�—-.�ª2è†f'O�dÓ‡QRR‚ÇãÁáp0&/�¥K—?´$µÃí`ŽSáõNé !„—ÛWoLdñ8;-îkÕóÖ' )qâòª˜ô~¡„—Ÿ¾-•W÷\ ¼ÞËÎwùÑñ&Þ9ÔHy½­rŒ¬šË´lsè9þîOç¸Ðìã[7%sýh+ª oîo`ûÉfª½ØL 7Œ³qÏŒ8´mÉ8U…u‡Øt´‰Ú&‹ÂŒ\ ÷ΊÇbÐð¯•r®ÖÀ_vÔñ—uüñ+¹uš>˾÷wEüü®tF&ƒ§wÔñÞ¡¦�´òË’z=î®Î–œ'';ó’^‹ŽÁŠªªTW×ðÚoòû?<Çã?ø§°ÛõfÛöLœ8‘C‡ãr¹0™LaŸÏëõRUUÍö�»ùÅýš¯>ô“&„¶3›Íìùä.XÐiÿ={öa±tÐR?!z²gß^þò—W阹p¡ŽÝ{vsì³c|ï»�ãˆl6\\~Q5ϘBq¹©€¿­å¢Fl¶ ´E1¯í½@Q•;ÔzõÁ^ÝYw±?œ¬pñÄ;|÷æf�°„}žç·×²¡°1ô¸¾ÅÇÚ}õ¨ªÊ}³‚¿x¿¼»Žw4„¶©kòñAa#ŵ~z{Z�ÇП²û«§ãîÊç÷SZ^Áì™ÓT~o4 ÉÉI,Xp=Oÿþø|>tºþÿNÜÒêäÐáB¾ÿ½ïPSSî={¹qÁü°Ûêõz22Ò¹ûÎU :^~å¯üì_~Œ^¯`ìØ1ìÙ½¯S0ÖÔÔÌé3g=jÔ¥¨öz9íñºY³æ-@eâĉÜrË-$ÄÇQS[Çûë×SYQIñÙsLšÃÑ£GyýzªªªÑétŒÉËãŽÏß�ÍjÅétòÃÇ'#3“eKobõêµ8�N ¸ëî;Ñ+ÁÏSKK «W¯áè±ch5ÆŒÉcåÊ•8êêêxsõß8{ö~Ÿ—‰'ñù;Va4;›„ CO2cB!†½?|TÃ>ªé¶|öhz%~´eu-~~qO©=­•Õ{/0'ÏÆƒ×ÇãöªüfCg*ݼºçBØ`¬ÁégÓ‘`°ô·¦29ÓDÉÿúV�4sç´8ܾë·Y6)†/ÌŠãL�›ÿx»‚’gªÝp£{fŸf⥯æöZÿþ”�—<ð>]�;œâ³%d¦§¢S”—ÝÑÚ·Ö±ö­u¡Çz½‘Ó§²òöå½n×îW¿üE¨ÏÚÎ]»¹nöLfÏšÅÚ·ßå|i)™½0b³Z±ÙlÔÔ\ Ð�F=“&Ndç®Ý¬h«ËÞ½ûX¶l){öìíó8ÂÕ¯�¢ F¼{÷}Âw¿ó(­N'ÿù‹_òûgþÀï»—„„D~ûÔÓ|°a÷ù>ZZ�¼ðÂK,Z|#7-^LEyϽøZm-Š ìBº¶.ø#OvvzEéq»ÝÎÏÿí_C�O�:ÅS¿{š³gK54GUKs37-Y 7,àÍ7WsààŠÎ1oîus¶ä,V›�o~ãXLFþç·OQUUMYY9���TUW“žžÁ׿ú<^/½ô2ŸìßÏíËoC«í8– CM2cB!†½Ž}Æú’Ö!3Ôäò S4Á@¬M‚-x_jZü¤::ÿ9mñtÁ±«Úfnoð¢F¯Ó`1ôÿ"»?e÷Œz¹ŽJë!#ÖîlÉyÆŽ¹ôìLǾVòÜ /ò¹[nÆn·õ¸]8EÅÅ”WT2{Ö,bbä�ÉcÛö�Ü{÷�}Ö# (�§[½nö,^þË_¹}ùmTWWS]SKÁøñaƒ±Áô»~Þ<;‡�ÔÔTFŽAvVpà—±cFST\/¸}~?·,]†N§�››ÃÔÉ“8t¨p@Ï'¢—¦í#èíCÙæÝ÷ÞgÏž=46^lšìóû:m£( K—Þ„¢(LŸ>�àt»¨¨¬ ??ŸÔ”`Ö÷ñÿûƒÐ¾§NŸ ¬¬”ÿ´óh‰UUÕ¤¶ekÅåqñ¯‡üØ"„B tÈrÙMÁ?“>¿J½ÓO¬9„U5ú:lÓ=Sd3—i5ðÒ×F ÓvÛ„ÂÒà…’×§Òâ`5†Ù(Œþ”ÝQ£3º_Vïéq;EéùBÀårQU]˲% ûUÇþš2i"©)ɬ{ç]îÿò´ï¶í;Q~~ðø�:-/>[Â�w¬ìµÉc]]­­-¤&w¾ÈÌÏC àçÄ©Sœ8~’©S§ Ó]Z&°#›íâ@ Š¢`³^lâªètÔàkÕÔÜ„Õbéôܱ±Ý³¸âê•”˜@IÉ9<^}ø¬ôž½{Ù¸qcŸå™Ìf”¶¬µÁ,KmÆ0hûèaXC�®çb›%»Ì¢n�úúzüþð¿úÅÄÄ ¸=¹B1Xùi&L-.O€?n«ã+ hvxc_°‰Qn’k˜¬V^Š­&˜‰zmowLë>ŸYn‚½Nƒ×§òÊž Ü7;ž’:7¿|¯ Ÿ?Àw—¥0%ëâh�%5^¼~•QI}— `3)4»ü¬?ÒH^²‘ÂR'GJœƒ:Å%ç1™Œ$'%jÿÞ|îÖ[xæÙçYºd1iiýë�æv{Øà_ÿêW˜�O>9À‰'ø?_úÒàêÙ¬6Z�N�@¨‰XÝ… ©‹¸cQj0óŒé…;ïú<üãK;z”cG�vZo6[HKM§¹98ÏàþûÙ`?ŠN�Åj£±©‘'žøßýÞc�êÑùNð#ÉÈ̤ôüyžøå/CëRRRÉÊÌFQ´¤¥¥söl1?io¦¨Ñ’œ˜Èc�}¥C"Dæzºö¤¹JNT{ªµ§€ì¾ûî Ýß¿?%%%¬\¹2´Ì8ȉ*¯”íÛ·SWWÇòåËûÞX!ÄewÛ$½†w ©¨÷¢hadŠ‘{fÆ‘Ÿjêq¿¯ÎOÄaVØúY3õ->Œ-ã3Ì|qÎÅLÖ}³ã‰µêx÷Pš}ÄXtLÊ6ó…Ùq¡AAVN�¡¸ÆCm£—»“NÓ¯²˜G«Çϱ2F�–Ó‚ó�,wáóÂWº%%ç™=sê`N_¿Üvë-üþÏR\|–ÜÜàg= �1zôh¼/3¦M L]7g6¿øå…çèXŽÙlaôè‘üã÷#++ü|iéiiÄ'Ä3±`B¯uî­~ßûΣü÷“O3àæ—v»�û¿üEÖ¬{›uï¾Ïˆœnœ=mÝ6à²Ä•0¸‘ð&ŒÏ·¾õ 6nü�ââbÜnv»ƒQ£FqÓ’Å$%%’�Ç©S§9qâ$v»•åËo£¾¾ž�›>Äãèò¼áê\öµ¯<Ì›[ÍñÏŽ£UF�ÉÊ•+еµs~øáY½z-EEEøý~233¸ýöå�|�%ƒ ®’ #B4q“nTik£üÒÏÿ!ÂÕ�ÚÚÚ×ÅÄ\œøN«Õöš!Ø»w/EEEÜ}÷ÝCV¿ÁHflÛ¶mÔÖÖ²bÅŠ+P3ÑQ×t¿ªªÝþ�Ðm¸O=õ�}ïûøÛ~4ˆ�é߀Bˆ«ß#:G}‡IŸ¯%E%¥dg¤† Ž„.ü~?%¥ŒÈîTö“O>ɬY³8zô(---deeqýõ׳wï^Š‹‹˜;w.“&M m?yòdŠ‹‹ihh 55•¥K—v ;Ú²e 'Nœ µµ»Ýμyó;v,«W¯¦¨¨€ßüæ7äää°jÕª>ë{ìØ1vïÞMCCV«•)S¦0£—¶òB!†Ö…V?ÍÎà¯Çz­4%âZ"3DõNÎMﮪÑ0šššº-KHHp9ï¼óF£‘|�ÇÃêÕ«1›ÍÌœœ«$ÐØØÈòåËÑjµ¼ñƼúê«,Z´ˆn¸�cÇŽñá‡2zôh,–àHH.—‹%K–`³Ùضmo¾ù&>ø`ØöõyyyL›6 «ÕÊ‘#GX¿~=#FŒ`ÕªUa3c½Õ·¥¥…÷Þ{�¥K—’——‡Ûí¦µµuÀçD!Äà>ïâçëÊC�3ã{"^1ÌH4Ö;97½êß8º°fÍš^o«±±‘ââb–,Y‚ÉdÂápPPPÊHµ7n‰‰‰ÄÇÇ“��MVVùùù˜Íf&L˜@ àB‡‘�òóóIOOÇáp°xñb)-- [‡ôôtìv;Z­–qãÆáóù:Í1�úºÝÁ™Ö³²²0�8RSû7•BˆK§S‚ÃÌ[Œ «fÄ’+Á˜BˆþÑAt¶Plt£§ÛÁjhŽ:õôÓO‡–©ªÚëü6›­S_6£Ñˆ^¯ïqäÇö ¨¾¾žÌÌî„ËËËÙ»w/x<Á9_z*«¯úÆÅÅ1jÔ(^~ùe&NœÈ´iÓ°ÙlaËB1ôƧ™xå‘}o(„¦TIþôJÎNotÐ6 xž§5kÖ°råÊoÃh4¢Õjùö·¿}Y†èm×ÓÜ���¼þúëÜxã�ÌŸ?«ÕÊÿþïÿöXN_õÕh4¬X±‚3gΰÿ~^xá.\HAAÁ�‹B!„bèiUÚF�‹tM¸Ù1‡Ã@eeå%Ö®gN§“¦¦&»OŽyîÜ9âãã™4iqqq ág\o×ßúŽ9’;3ùóç³m› }+„B!D´»æúŒ™L&Æ�ÏÆ�©««Ãï÷SYYÉùóç/©Üââb<N§“�7’’’껥×멬¬DUU, 555TVVÒÚÚÊ–-[:•c6›©®®ÆårÑÔÔÔg}�'OžÄívãr¹¨¯¯Çl6_Ò±!„Bˆþ‰Æ„F4‘óÓ»¨ÚþrõX´h[·nåÍ7ßÄétb·Û™>}zØþ]ýU__Ï‹/¾ˆÇã!++«S=çÎ�ËÖ­[q:�,X°€)S¦ðÆo`4™8q"vûʍòòò8tèÏ=÷¬\¹²×ú¶¶¶rðàA6n܈×ë%))‰¥K—^ÒùB!ª§æùB ½½Çj­&js"ŠEõ¤Ïýé36˜¡í‡Ú@&uÑK&}BˆÁ9_VEb| F£á²öÇ"RTUÅíöPS×@fzr§u'χÅ"-“ºr»ÝÔÖÕ’7"7ÒU‰ZZˆÞôáåÌŽ !„bhØmfê›ðx½’!ÃŽªªx¼^ê›°Ûº\»�Ɔ\.gj½\.'õõõ8d”ï^µMú=Ã)*ŠÒã0ïá¶B!DdÅ8‚­ªkêñú¼®�CO¯Óc·™CïõŽR’ ª¨¯oÀí®Š@í¢“Á Çjµ’”éªDµ¨Ú¾·ù¾„B�bö°ªB\ ÒR’ûÞHˆ0‚“>GK$v•zôÑG#]!„B!ÄUF{1%&™B!„B\)Á18%B!„Bˆ+J‹*CÐ !„B!Ä•¦EÓž“ L!„B!®”àhŠ‘®…B!„B\ctÈD�B©ªú×ñqûýÞ¶EUƒÿÂl+„~4ùIS!Äàè.Nø,�B!Ä@…ûÑE4!„ý¡#h»+Á˜¸¶u¼  — ë˜ë– 벟dÄ„¸¶uý�àL!D8:4TU æÇäR\úcí· ÌTB­å3%Ä00�TÇï Ì„B´ õ ^@F×…ã«o¼EyyEèqZZ*÷Üy{k$†³ž²\ïwÍz… ÆÔ‹Í~£í3%„MÛ–}m3íåIP&„B×±¿X¤/_}ã-Ê+*{\_V^Á¯Ÿ|&ô8-5¥[pöÇ?þ‘úúú˸ÿþû‰‹‹»ôʶyê©§X¾|9YYYCV¦ˆ¼ž²a×õøyée°!ÄÕe°­þl+A™B]‡ñ#~áXVÞs ÖÓö]ë|Ï=÷„î8p€sçÎqûí6£Ñ8¤Ç)}„†�Þ2c�@UUC·]ï÷wäE!ÄÕ!\€ÔÓçYUÕNÛwͦõluÝ_!ĵ#ØLQŒÈ"}áø�o><à}ºÖÙh4†îët:´Zm§eáö¹Œ }õë|u\z\/„¸úôgŽŽËzú¬w Ìz ¸$K&„צ`3Ŷ¿!�ÐÈŠ‘óäSÏõ{ÛG¿Ñ{ðÖñB¹£gžy†åË—³uëVêêêxøá‡Ù½{7'OžÄétb·Û™3gyyyœ8q‚={öÐØØˆÅbaÒ¤IL›6-TÞ§Ÿ~Ê–-[hhh ..Ž›nº‰„„„µˆ6]ƒª®�V{ðÕ5¨*�û !®=Cá²Wá2bíËÕÛß L2!„¸vè@E�’>c0°öûªoOY+ŸÏÇæÍ›Y°`qqqèt:FŽÉäÉ“±X,=z” 6����Ïçcýúõ,Z´ˆÑ£Gãv»immíT¦N§cáÂ…86lØÀ–-[XµjÕ@[D�¾2c½5Oìô/@m ¢á3%ĵÊïPS[‡Ûã%Ð��bo1�M×mûtÆ:nо¬}•¦ËúžžøZˆÇLzm¤« „§»8~G÷ RD àÚµ¯úª=d'TUeÞ¼y¤¥¥…Ö§¤¤„Ö�3†Í›7ÓØØˆV«EUUÒÓÓÑétèt:¬Vk¨LUU5jÉÉÉäåå±cÇŽè8—bÐzI±§ Y×ÌXOï=!Ä•SU]‹VÑ’šœˆN§ xÿ®ªž¯ŽAWøÛ¾ÖõõÜÃMQI)IÉ)}o(„ÜN @ûˆŠÑpᨪý¯C‚±pÍÃ](WVVràÀªªªðx<¨ªŠ×ë%))‰ÜÜ\^}õUÆ�ÇäÉ“±Z­�Êj¿ 0›Íøýþ¨8—bðºcí¯gÇÀ«ýuî”u|?Èû@ˆÈqºÝ¤¥$¡ÕjüY 7G¸~d}5]쫼�4‹B1üè.¦¢¢%ëj¬¯úv¼8îú/˜›››Y³f óæÍcöìÙX,ž}öÙÐ6Ë–-ãìÙ³>|˜W^y…yóæ‘ŸŸß­¼ÞžS\]z¨£§à+”-•`Lˆ¨P.C nàŽöÀ¬}ÛpÁT_}ʺ’€L!†¿¶<@ð—þHH0ÖW}Û/�»n×~ÝÊÁ1€ IDAT¾üܹsÄÆÆ2vìØNÛøýþÐ6™™™dffrôèQvíÚÜ£½œöíÚ/Уá\ŠÁëO3Å®xøýþàm €ß¼ø“÷�‘ðwÿþïI_£%ö§¹a×À¬}Y¸ ¬}¹\BqmÓ]Ò>J²9C˜ë­™bÇL†Á` ¶¶–ÊÊJl6 •ïóù8{ö,éé騪ʅ 0 �úŒum–Öñ±¸zô4€Ð-zl¦Ø±™ª¼„ˆ`ðMÆûˆõt.aíýŽÃ•ß1 ëú<í$XBˆáMG ˜ƒèø?0€`ìR2c³YäççóöÛoc43f ‹¿ßOss3GŽáã�?ÆçóÏüùóCûvͲ…²#Qp.ÅÀ„ ÆÚï÷6xG×A<~?þ€dÆ„ˆ´pßÿáôg �®Y×@¬§lX èqBè®™1 ¼„âÚ£‰;KmÚþ‰ï}%ÂÕ�ç_ük¿·}èþ{/cMĵ¦ë¯×áú‹ݲaÿùÏæ�‡&Ð6ŽvŒÝŠ"2ΗU‘™ž< }zj®®ib× L«Õ†]Öq]Çe}e×úª×Õ¬¨¤”¬Œ´HWC!"N¼hŒžÌXrR•UÕ}n—š’õÃG¸lX»pY±p€ßç' JfLˆHóû}ƒú ö5,}OW»ŽAY @«Õ†n{ë'&™1!„¸öè4úŒù|¾Hׇ›/è÷¶ÑP_1|„ë×nRçŽ}ÆÂfûŒÉ{TˆÈñûý}~ûj¢ØWF¬§àª½¿\lªØ�…{i®(„×&Í”)S0ͲB!zc4¹ñÆ™0aB·¦‹íAZ¸ èö¸£á I3E!„êþ�B!Íív³uëÖnƒût¥µ·©\2Í‹Bˆ«—.ÒB!†›ÖÖÖNÍšÛ›'v Äz›\z8eÁ„BôL2cB!ÄeÐu~ž&�Bqí’`L!„¸ ÂÎCØaäÕ®Ógô4½FO�…B\ý$B!.ƒp“ć[.è’ÀK!® Òg,‚fΜɬY³B�Ýn7MMM?~œÃ‡wúÃ=Pz½ž;3ÒÒR>þøãK®«ÍfÃl6S__�×ë½äò.‡Ëy>Û�?ž‚‚âââp»Ý|ðÁ”••]r¹Bˆá§ý;§ãèˆí�YÇI Û—·o+„âÚ!ÁX¨¨¨ µµ»ÝN\\óæÍÃáp\R¥( v»�øøø!©ã¤I“˜:u*k×®åüùóCRæår9Î'@NN .¤¡¡� ×ëihh¢Z !†›ŽÁXGŠ¢3`]ƒ²Ž�÷ìÝÇ‹z™ysçrß½wã÷ûyô±ïa³ÙøÅ¿ÿ¬ßu©¬ªâ¹^¤¼¢’Ó¦ò¥ûîíTÎ`Ë�fn·›ÓgŠøôØqªkjq»=df¤1rd.ãò1�‘®¢BH0 öïßOQQV«•/~ñ‹Œ;ö’‚—ËÅ‹/¾ˆÇãªj^5.ÇùHOO`ëÖ­œ={ö’ë)„ˆ^ŸÿüçQ…uëÖát:°X,ÜvÛmø|>þö·¿õYFÇfˆ­­N~þŸ¿@ƒ4:…g ‹-dÅòÏ…-£/»÷îåÅ—^æÉßü*èuõÁ†M¡Òš[Zú,ójwî|)6}DCcS§åçKË9_ZÎî=Ÿ°üÖedefôZÎ_þ+.Ô‡ëô:b&OÏä‚‚!¯·Ûíæ©g^àË_¸‹ÄÄ„!/¿/~¿�ÿyêYî»{)))Wüù…¸I0eZZZhnn&&&&ÔŒå‘Ga÷îݤ¥¥‘––FQQ6l@¯×3gÎrrr°X,ÔÔÔ°k×.ÊÊÊP…¯|å+TTTðæ›oÁ¦‹óæÍ#77­V˹sçØºu+.— þZ;mÚ4òòòp8¸\.Ž?Nbb"ÙÙÙ¬X±€ââbÞyç�^Ë´X,<øàƒ¡mfÍšÅÌ™3Y·n¥¥¥=[¤ÎgODz|ùòÐ9¸í¶Ûhjj⥗^êu?EQõ|íû�8q«ÕJrr2�‡#Gްoß¾^_¯�;wöùZ�?žÉ“'‹Çã¡¶¶–õëׇ.:…¸Ö)ŠBRR+W®dÍš5@ðû/>>žªªª~•Ðh4ÁA;h¬дýß›ž‚0EQxêÉÿî´¬°ðÓ>ëÒÔ J~è¦NžŒV«íVÎpñéÑc|°i »�óç’ÜØTÕÔòñÖ465ñÆêu,]|Æ�ëµ¼ùóf3cÚT|^/E%çyïƒMXLòF�Òº�F¾ûè#CZ¦"ºI0%Ú›feeKuu5�@ ô+çìÙ³9{ö,¥¥¥¸Ýnn¾ùf²³³9}ú4uuuLœ8‘•+Wòúë¯SWW×í9n»í6’““9tèƒ�‚‚ŒF#o¿ý6K–,aôèÑ”••qüøq N§“ÂÂB´Z-™™™ráÂûUf„;¶K5˜óÙÛ±„;íYÇþœƒ�>_»ö×£°°�üü|fϞ͹s稬¬ìñõê«ìÔÔT.\Hss3{÷îE§Ó‘šš Ô„°nÝ:V®\I||<+W® >>žººº~¿u¤# ‚ zƒžýÉ�P­Vú‘H«Õ¢ª*¥ee¼ø§W¨­©!;;‹ : >Ÿ�o÷ïCÍ úoÿNee�>ö=îºëó,\° Ó>¿øå¯)>[ ÀsÏÿ‘ &ðÈWî³Ybmm}ý Š‹Ïâóy™2y2_¸ç. Ã@NåuòtQ§@ìK_¸³SsĘÙ™é¼ü—7hljâƒM[0Mä�ѯòuz=y£FðIb••CŒEé»(Ä•"ÁX¸õÖ[;=öz½lݺµÓ²ÒÒÒNIIIdggSRRÂûï¿ÀùóçYµjÓ§Oï–]JOO'==�ÂÂBvíÚ/,rrr0�Øl6F�MiiièWà®ûgffræÌ™PS—¾Ê쯮ÇÖU\\&Lè´lïÞ½=nƒ9Ÿ}Kqqñ Î�ÏçÔóµïWYYÉÚµkp:�Ì›7�””|>_�¯W_e·÷#Ζœ§ª¦†Y3§áñzøíÓϳlÉB¶îØÉ¬Ó˜:yµµulÙ¾ƒòŠàù=2—Å7ÌG§×óêkHOKaþ¼ëBånÙºƒÊªjVÜvs§fн•óü‹fîu³È“À«o®¥¥¹™‡îÿ"å¼¹æmþî« (�/÷*«ªø¨í9Í&Æ�eî왡õõ lÜü15µµÄÄÄpóM Imk¶xî|)Ûv즶®­¢��‘ÎM‹oÄh4âõzùß§Ÿcåí·²c×^jëꈉq°lñ�¡ý‹ŠKøxûN›šÈÊHcĈ(ä�/ ×cع{/Ÿ;NKK+v›•É“ ˜>erßo!¢”cQ ¢¢‚ÆÆFÜn7µµµœ9s¦Û{¿ßßéqûÅAqqqhYYY^¯7´®£¸¸8&NœÈĉ;­s8¡ ôÓ§O÷»Þ}•ÙÒÏ~ ]�­+»ÝÎäÉ�¿h:Ôc06˜óÙ×±TWW‡}®¾ökÏPôùÂe6Û—™L&�ð¯W_eŸ;w¯×Ë”)S1bÇŽãàÁƒ}¾Bˆ� bªJ  *^¯‡üðÿ�:öûÉ�§ª²ŠÆú,V+ßyôØmv~ÿìs”””ôø?~üüóDccc�}Æþñûßå·O?çŸ~Êw¿ý(yy£ûü¼vüUÕUdffò­¿{�×óÏÿ‘=ûöqç+Ñé¢ïòaýÆÍ¸ÝûIggûù¾õîzæÏ� ª೓§X0wv§çí«œÜœlŠŠKÈ“‡Ó餢¢E§£¦¦–ÄÄNŸ)&;+³[ æóùX½î]¦L,`Õí·ÒÜÔÌê·Þ#6ÆÁØ1£8úÙ >wËM˜ÍfÞÿ`ÛvìáÎUËAUÙ°é#Æå�áîÏßNss+«ßz‡=ûö3ÞuhµÁ÷ôÁC…|îæ%Ýöw»Ý¼óþ̘>…éS&SV^ɦÍ[ж½oû:æòŠ >9pˆûî¾ƒØØX*«ªyëÝõädfF¤��C!ú¾M¯Aœ áú´.………œ:uªÓºúúúP7�a•û*³ý¢àR‡j.))á·¿ým¿·ÌùìëX¢e¿víç4ܹí«l¯×Ë+¯¼Â¬Y³=z4sæÌaìØ±¼öÚk¡Œœ×:³Ùj¦ØþCH||<+V¬`Íš5ýÊŽuÊ~µ/« jÂ7««¿À¸ü|ÒÓÒÐh4̽nN¯ÁØåRQY [\üóï¶.3£÷�/¢UOã¡ô6NJÇ>c=ÉσÅb ä\ .—‹çÏC§Óa2™˜;g&¯ÿmËÝHþ˜Ñ|´u%çËÈÎÊ ¨ø~ŸŸ1y£ðù.É%çJ{-gĈÞÿàCPUNŸ)"!!»ÍÆéâb8Ut–S&v«kÉùR<³fLC«ÕÏóiíÁåäIÄÆÄ0jD.{öî¬Ñ„2olö™“�E}}ç‘…'Oì¸{ö‚¨T˜5=øÜ9Ù™Œ‘KQɹ~³Ùd"P9y¦ˆ¼Q#IKMáëýŸ^_!¢�cW©ÚÚZ²³³),, %%½^Z×QûÅ„Õj ;/V{æ'//�Çw[ßàuì+ÐW™í¿¼v^?ZçÐéëX»_O#› v¿v½½^ý9–ææf>üðC¶lÙê{–––ƹsçz}^!®Ë—/bíM�Ûƒ³åË—óÚk¯õYF§>cmÍõz=?ýñãhµÚnýÆz(e¨i@ômMÂÂihlŒÊ`lÙ’…¼üê¡,XÉù2òF�àö[—ñÁÆ�XºäF 8�G;ƒÑÀ²% /é¹­Vsè¾Ëå$PyòwÏvÛ®±¹™Ø˜Æä�äØñdgeðégÇ76EÑu Æú*';3¿ßGYE§ŠŠ™›ƒÕjá豌=Š u1"§Û¾­-­˜MF´ZmÏÇc6…î·÷glwàÐa ?ýŒ¦¦f|>/�€ÊÈ.ÏcµtÜ_ íït¶b1›:=wLœJúyîbc¹ëŽå,ü”7V¯C£ÑP0>Ÿ9³¦£Ñô|Ÿ�êêjÞ~ûíÐgcÍš5,_¾¼ßßwìÖ1óÒÓH‰ñ±q¨À±Ï>£¼¼‹Õ¶í;û]çÒ²2ÒÓÒÑézÿ1§?RÚ¾FäŽàë_}‡Ã~Ée^n11–.^ê ¶uÛ²3ÓÉÊÌe~ Øz`ë¶¡ÇK/ìW±Þt ¥ív;z½Žo}ýaè!È.ŸÏê·ÞãúëfQTt–ûî¾£Û6ý)'+3ƒã'OSr®”Ù3¦cµXؼe;…G?#5%«ÅÚm«ÍŠÓéÂï÷ukÂØ—¢â¶lÛÉò[–‘™‘†Á gÓG[iiií×þF£)8X”ª†Ž©¡þâôý9æ´ÔTÒRS�`¿¸·Þy»ÍFÁ„ÞGÅ"ZI0v{ÿý÷¹îºëÈÍÍ%''‡êêjÞ{ï=jkkÃfV6lØÀìÙ³ÉÍÍ%++‹†††N}“6mÚDmm-cÇŽeÖ¬Y¸ÝnNž< À‰'HOO';;› &PÙÖ„¥¯27oÞÌ¢E‹ÈÉÉ¡¥¥…Ç3ujïÍ="¥¯c‰–ýÚõözõV¶Á` ##ƒ1cÆ Ñhhjjâ£�>¢¦¦¦ßÏ-Äpn1§ÓÙ¯ŒXG]›)z½~øÿ~Òm¾±ôôtþþ»ßÆá°ÓÔÔÌÏÿó  ­5BÉ1£!8`Òüâ—ÜzËÍÜvë-ªc8y£G‘žžNQqÑÅfŠ-)IIüàŸ¾µ#*æ�ÁÒÅ7ðÁ¦-446ñ翾Á‚ùsIjkŠ_]Ú¾}þ±¥‹oè÷HŠý•‘žŠÅlæã»˜5=ø÷nϾý446²üÖ›HOKÃjµ°ùãí$%%„íïÔŸrFæf³yëŒi)É ÑÃ�C…Ìšþomvf:&“‘í;÷0{ætšš[xû½˜¯{§{kƒÑÀíý˜ôy0EÇÊå·ðѶ<ÿÒ_•ôÔn˜?¯K=DzmûnnZ´`Ðåäææàß¼•ܼìP&iDN6ûfTnn�宺ýV6nþ˜ß?÷F“‘qcF3ybªÚ{@5zäNœ<ÍŸþòz½Ž)“'²è†ëycíÛümí:VÝÞ}âòެV+Ë–ÜÈŽ]{Ùµ{99ÙLž<�Â#ÇúuÌcÇŒ¦¢²šWß\‹ËéÄb±0vÌhÆå�éõy…ˆfš)S¦D¦Aº¸¬Ú'\.++cõêÕ‘®ŽB\sî»ï>t:Š¢ ( :�­V‹ÞáGgÅ ¢5‚V¯¢Qht~4ú(þà?­Ÿ€ÖƒªøðãAÕoý7>Õ�_ãÁ�ŸêBUUt:�E5 `@ÁØvk@ãסh h4ªm@�& CãWÐøõhTøðoUŸ‚ÚÚÿ)Jª¨¤”¬Œ´ËV>›#ž:}†O�� º¶�ÛCFz£FåR0.@S°ˆËgמ}œ-9Ï=w®ŒtU„ˆÉŒ Cf³™9sæý™O!ÄÐÑ™UÌñ`ÊnDks£µzÀÜŠÏèħ4àV;�ˆntÔ®zh4š‹Í»Þ¶ñ¨-xÔ¦iÛ`ÝɬšxLšŒz_ :¯ Åm— µÕ„ê‰îK£ÑÈ„ñã˜0^úE U ðâŸ_cÌèQL�R@k‹“£ÇŽ“?6/ÒU"b¢û›T JZZyyyTTTðÉ'ŸDº:B1¬h´Á`Ë ¦¸ÆXCŒ?xëð¡søQ * i›BÁV§çk¸:Þ�&-j-j[?V]Û¿¶AuS0X#½ÏŽÁƒâ±¢qYÁi$ÐbUF·�i4Zß8Ÿ-Ûv²ïÀALF#£Fæ2kÆ´HWMˆˆ‘fŠB!DZ=Ø3TliÌI 1~ ?ú?z[߃¤µÍî„ÏŠõ4‡`4k½1kc1«qÕX þôîX´-±êmà×^‘fŠBq5�̘B˜Tì`MõcNõaIñaˆ“‰ÐèÇI[3y°ÿÙSR°©élZ›ìø›M½%„ÚcB!®9ŠábÖË’êÇ”âÜâE1Hc‘Ë©)PI•8\´šö¡‹5a'›/ƒ;ms0{ðIG!ĵAó×?¿$y„B kZ» mL >k=.c ­ºJZ¹¼së9]î>›(×fŠ}q:]˜LáG3´i“±úÓ1y“Ð;ãÛ²gæ+\C!„¸2$3&„bxѨ( ͨŽF¼æ:œ†jš4eøñDºf¢šU4kªÀ@kÄ¡ÉÂâM fÏšb Ô9PÕá  !®MŒ !„¸êiô´I xU´šK©×GºJbˆøT7uê)ê”S¡¾gñi£±¹³14§ ÖÄð`Ì~!„ˆ"Œ !„¸*i->4 õxì•4ÏѤ)‹t•ÄR8E�þÄ�#!‹O.†Ö44µñø�úHWO!úM‚1!„W ­Ý ux¬4Ki¡*ÒUÖ8G£î8À›JŒ7£3m]¢ŒÔ(„ˆzŒ !„ˆjJ\+�¸:ÜÖ šô%‡L¢‹æ@ÍJØÀâˆ'Æ?“3¥> ½5ÒÕBˆn$Bu”„fq5¸¬åÔ+gñÒé*‰«Lk ŽVMX>Á`µ—> ³+]}2þ:G¤«'„€cB!¢„’Ò€?¦§¥Œ JU&\GÑèÐkMè4&t#mpØwoÀ…WuáS]x.ürþBš ÇÀ~Œ„ìÑÄ9G¡¿�A ÞéŠ !†! Æ„B =�Š’^�'SWÅpôZô$貉×ç¯d§d“ dcÐÊ�è¥AKœ>‹8}cl pû›©öœ¡ÖSDµç 5®3T»ÏàÇáÚö¬6pŠZã),éñ$¤äcnÌ%P‘jÿšŠ !D_$B1d´v7jr­¶j•ã‘®N¯ìJqJñJñ¡¬WZ¤«5l™æIdš'…–Õ{ÏSí.¢Ú}šš¶@­Á[ÁZ†&Äì 9vvçH”š42©´âI0&„â’)©�xãʨ7Ÿ¢™ªHW§ñúltÙÄj³IÐå� ÏA®3”¸bÚû¤åÙæàQ[©v�¦Ú}†ZoÕî3Ô¸ÏDÕ@!Uê§T™>Å‘�JœwƆ,U±‘®–â*%Á˜vΗ–âr¹´�Éd"3#ã2ÕHˆáIÕøÑåTÓê(¡Vwœ@”57s()¤êÇ‘¢Kªn,�d½¢�Ac!Ã<‘ óÄвzÏyJ]ŸRæ:B©³�zoYkxQc ‚F¥%^Or|¶–‘Χ¢QeÀ!DÿI0&†—ËÅè1ã´Ï©ÇÂ.÷ûýìÝ·Ÿ�»vQT\LzZ£G�bîœ9¤§§±eë6ÞüÛjþç×ÿ5U”––V¶nßÎþ©¬¬$7'‡ ãÇ3göLÇ�=O@UÑ@hxí³gKø§ÿû8ÿýë_’’œÌ‘O�rôØ1îüüh5}÷§xæÙçq8ìÜ{÷]a׫ªŠ }–Õ×ñõktüÄI~üÓåé§ž$6æÚþZɪ¦5¾ˆRåH¤«ÒI¼.'|éóI5ŒÃ¢‘ŒÅÕ.Ö�I¬!“ Že´øë(uRê,¤Ìu„j÷™HW?^Ê9Ö¤åOÅÖ˜‡Zšéj !®Œ‰aÉjí½ÃýöíÛ)(( ¦�‹éwß}Ÿ�›7sÓ’Å,\x#.§‹C‡Q^QAzzäe÷ù|üþÏÒÚÚÊœ9sHJJ¤¡¡ž­[w0eò$†*kiiåá¯=Âã?ø' &ô°M ÕÕ5¨�(—þËð‰“§ú z®Ôñ‹‹”Œ:œñg¨Ò-33'ëÆ�ªKªa©ú|iz8ŒY•xÆØn`ŒíÊÊË?n7/½‰ÔÔNŸ9Ãôþíßÿ€ïï1’;=ç–­ÛØ¸i3ÿú“àõzY¿a#»wï!>!�Ñ£F²cÇ.¾ü¥/0~\°éùóe¼ò×W9òéQÌ&·Þr Ó§MaÓæ�øÃ³ÏðÈ75‡èñ÷õ••W°éÃ9qò4n·‹éS§rë-7c·çjljæ½÷ÞçPa!còò0›;g]|>mù˜={÷áóù˜3g67.X€Á g8PRð$�¥ÂðiD'i6j­mÍÇ‘ªG’¾·Ï®¸Öh4 Y–ÉdY&3¨p§Ôu8ÔœÑåïþ=~¹ùñržÝèã I�™‚¹n�ê¸+^!Dt“Y+Űæ÷ûQ;d±NŸ>ÍÖ­[¸ë®»zmΨÓ)|éK÷ñìó/ðÖºw¨ª®»�ÓåB£Ñ°jÅíܱj/½üg:@}C?ù—Ÿ� ?x?‹ÞÀ[ëÖ±ö­·˜3k&;vî$PV^NIÉ9¶ï؉Ïç ôÿ³wÖáQ]ÿ­ovãîF’à îîJ)¥Î[§/m©ëWo©÷m J‹»/ž Jâî»ûý±HC” ÖûësŸ>aîœsæî•93gÎ\N'íòe:ø´¯§ÛÉÑ‘áÆòù—_qðÐJKKojãéˆò òéÓ§7ƒà�?ײoß�Úò;v±zÍŸô ëÉ‚ysQ*¼üêkäääâèèÈK_`Áüy|øþ;tèPß–²vÝFŽü}”qcÇÒ¯OoÊÊÊILJªsNaavvv<ôà4‚ƒƒøàÿ>âÊ•,:‡óØ£ xeÉK|øþ;X˜×ïÄ4·ý�ýFr™ w7wfÍx�óæ’˜œÌæ­ÛýýóÅ—_‘ž™É”ðIøûùRPPw¿¬?×m "2’ ãÇ1er8§OG°uÛö&¯ÏÝŽÔº�_,™Î;É�Ÿ¼#Ž˜Jdާ"”Pã9Œ4{•Á¦Ï`4JpÄaÉ«¯ã‚_`‹—¼Ú¢ºc&„³~ýÆ6²ìöb¯ì@gópÆ8¼Ž2á!þøZM“~œ?_Èå¬üëEŧPU]cP[b.]$®l/)6¨òŽ@bUhPù·›Ùÿýšô¬Öíøñ¯ÛHÍÌ»mõêsàä9æ¾ü-¯-[ËÏ5«Þž¿öñèOñèOñî7_ƒy>…§ß_Þêgè…�V��zÓ²™‹—‘“_Ü*¹­Ñw;fÆî[222øðÃ8p #FŒàçŸÀÊÊŠÑ£G7)£wX(žîlÛ¾“'=ÈáÃ:dP�ss3F�^ûw``I‰ItíÂá#ckcÍÔ)áH®®£’Éd¼õÎ{ôëÛ‡Ú³ìÛ︜ž�‹³ññ øûùw–Ô´Ëx¸»q.>?ßz³Bb±˜‡¦O£k—ÖoÜÌ/¿.g„ñôé†Z­ªcãø±cjÿ®©Ñ°æÏ?éÝ+ŒòòrV®þ�§ÿóA�ú fžî\¼˜È�C‡™0n ŽŽŽØZ[Õf�ÌËmøÃXPXȺ xeÉâÚY0/¯v¬[¿¡Îyþþ~ ìß''G–¯XIzF:ÁAAØÚè×Z8::4¸f¬%íoè7²¶¶¢w¯ÐÚ²ݺ²eÛv˜Îùøx¢¢cê¬[355å¯}ûõ× /�uë7ðÞ;oáæê@¿¾½ùö»5r8Ré½÷Š[–Pc—Æ£XÊ)¼#ËÂl¥Þ8˃p•c%õ¸ý�êêj®dç{þnÎŽtëÔæ:ÿóÔÌŸ;»Åõ6®]ÓÖÜyl”_êÀp»—ø-¢ ûÀ2\+ɨ¸yâ&CRP\†D"ÆD­¤B[DŠä *»l,ƒ�縡Ëÿw-lMk¥CÕÚzõ1RÊQ)P)ä¨UòfÕØ¿û÷#îì96nÞR¯¼ªª†e+wòâü±Èe-ÿö9“€ÚHI€·K‹ë¶†Û­¯1‚€@3ùþûï‰��'))  ‰ˆˆ`Ú´iÈåÍ{99:2oÎ,F ơÇyê?ÏòìÓ‹èÒ9ä¦çÛÙØRU­ß'11‰À€€ZG ÀÅÅ€ìœÚ{{áçëC|B.ÎN?yŠÇaiiIÂ… ¸»¹rüø zöìY›ÅðŸˆE"ü|}ñéÐ� /±s÷n¶mß΋Ï?‹£ÃÍ“Œ8:Ú“••Maa!…E…TUUáærý…$‹ èèO|Â…f]£’““«¿¶vÍ®£Vé�§êލµ¦ý7þFZ­–ã'NpæL4IÉ)\NOÇH©ßÈ5#SŸ�±!g0ûj;_Xüßze%¥¥÷T¶E‰E5¶iä¨ÏQFÎm×/)q‘ã"ÂE‚Bd|Ûm04eåüuø(ö¶6Xšß�îŒÌ+üµ?û÷$¨S�<¼€7Þ~�•«VðöëK7N?p³bÕjŽ?Éñ'é×·/åee9v”¾YF@GN�ŽàåW_'++‹‡ÌcÕïðý7_âáîÞ¨ ñ ¼ôòR.\ÄÎΖÅÏ?Kÿ¾}øbÙ×”—U 4Rðó/+¨©Ñ°mÓ:ìILJâµ7Þ!*:++ }x>ãÇ�m²Íf&F˜ëß/Fì+ÝëØ�ÄÒ£¸°�ãi»AR�…YÝH‰ŠÊjR2s)¯¨B.“àhk‰™±>L¹¢ªš´Ì\JË+‘J$ØÛ˜ceV÷žÕé #;7ÇëI<Ê+«8ŸGEe$5eÆšöÁ߸Ú5—³òùî�½\HÊÄÔĈð¡=èßÍ€ÕÛ�RQYƒB!eëþÓh4Z>]<k Ò®äñõêÝ$¦eaifÌcÓ†àã¡@k¬læK˘>ªkv£²ªš ƒº1v@g’3rørå.Ò2r°43f渾tíèYÛŽ£Q l?IeU S†õdDý Cjfßü¾›¤ËÙX™óИ>tñר,XúùE¥<ýþ/„øz°x~Ó¿_cõ».Ñ ©üðç>²r °·±àцàåbË®¿£9|ú‰ÿûäs2¯\ADÓ‰s>ýü+ÆŒÉÔÉáÄÄÄòÒ’Wé×§7"‘77W>þä :úû²uÓ:LML�Éôë/êY† ÈŸ}Ä¥K‰,xäqBBB8þ<‹žy¾žž¹sfñÌ¢'13Vcf¢wÆÌÔF˜›¨QŠMð5Ìä×vñàôGè »Îm@£)ª­Ÿ”ž�©±^.¶•V�|9?/g¤1‰iY˜›¨ñp¶¥¢²šK©W06R¢�_ïVå£TÈQ)¯üed`i¦ÆÚžÒò*2vbí˜�¥EG–ýø7=}yaÎhÒ®äñö·ëññtÂÁÚ G Vl9D;g;>yq&ÆFŠÚuÉþ´…Îþ,Y8ž3çRøàÇÍ|óê\¤RI£eb‘ˆc1ù𹹘r…~ØÄˆ>AȤVmù›¾]|@BʾZ¹‹.þµƒ‚q—.óásÓkë  @*óɯ[ ñódÉÃã8s>•Ï–oãË%s0Q+ùvé|æ¾ü-K›„‹½e³î] Ñz�]—_6dX¯N èîÇo[�°|ã–>6‰þ]ýø}ûQ’3rps°¦¦FÃþq¼¼pñÉé\LÎàíÿLå™waƸ¾˜q,ê"Þ®ö­¾ž­E,w!�OÏ 1-›w¾]GX°7æ&êFõ}ýûtïÈ�Жo:L^aqí³©R*®ÏŒ)å¨�ôƒ��Q©TDEÇðíw?R£©¡£¿?½Âz6kÖcQÖ©UíÜs4Ú¹\�úfõn†„ubXXî>IYe% ïW5vŸ}ðãfzyßô9jL_c÷|c2 ñl kÆî[¬­­yüñÇHII!éêz¥3fÔ™©jŒÊ«ÿ�¨ÔªZÇ®)ÜÝ܈��«]�žžŽ\.ÇÆV?zäÓÁ‡ÈÈ38x˜ÐÝ173ÃËË‹¨¨h<„··7 ŒPiµÚÚµe׋ÅúY¦FÂË.§§ãêê‚™™VVú,_©i—¯ËÕéˆ=w/OÏ:õnšEò&ÿdkkƒ\.çJæ­wîuÚ†ÒÚö×Ö×éøkÿ~ôïO§ÀÚyzbeuýcoogGjjZ½ub×°²²D.—#KpvrªsHoá|;©«y%’ﱟ4“ý·Ýs‘ÓÓx6ƒMŸ§«zÚ}çˆÝMDFžaõš?™:eûvoçÍ×^¡ß>(Š&ëâæê‚©‰)½BCi×΃‚‚B IJNfÆôéóÌ¢'ôÛZ4•JÅÉ“§¸xñ!ÁAlß²¡¶“o¢6&//Ÿ·^_Š­� J¥‰DBrr i—ÓX8>Æj5�9zhn®. <ˆ¸¨ÓõŽgé�Ëog6}ù,<;�)ÃzzÇ(53—gÆ-` ã#,êö‘Ûl�ÊR‡·›=?½µ°¶i¤”S\RΣ ÆÒL�\.E,‘™SHfN> ï‰R.£{`;l,L‰½x¹Ñ²kLÔSµÁ¾îÈäRr ô‰NJqÒH½’‡¯§#Ÿÿwf�茛ÕËÉ/!ýJS†õ@©�Ó=°öÖæÄÝ ÏÐ4t]@ÿ[ëˆ\&¥{€ÙW×I¥†öêÄæýúH™ÃñØY™ãî¤OJååæˆƒµ*•‚ 7\ì-).­¸¥ëy+Œéß•’Àö.8Û[s>)³Q}Åedfç3ªo0*¥œé£Ãê|»Ý­yå‘ñÌß—Þ�;ú=D½½Ú1qüX–¾ò_¦N™LÜÙsœ8yºYv^¾’‡›£uÓ'þƒªêÖî>Á´×— ä–’SP̨¾Á(r& îV[ÖØ}–‘]@Vn“wÃH)¯÷5¤¾ç›’iˆgS˜¸¯ cÈ�!ìܹwwwzöìÙ¬º……,yå5 €‹“# ¥’”ÔTþøc-óæÌj–CÖ“­Û¶³æ�utíBqI)+W®fê”ðÚ6''G<==Xõûìll¬ñóõá�?×1{æC ¦i�‹;ËÊÕkèÛ·wíš²¨èhNGDòßÅ/\oKA!G�ÃÁÁ‘¬¬,~ÿc-³fLG"‘`nfƃLå—å+˜6u æææœ:}š´´Ë,\0¹\†\.çLTr¹ccc”WGÓâÎ�C¡¬Û©355%|ÒV¬\ÍS'cfjÊùø„f]÷k(¯vOEDbog‹«‹ ¦¦&­jCˆE"Ü\]9���-lܼµ¶Ü×LJí½Y¾rC B"•yæz;MLŸ4�ŸYÎäð‰XZZ’——GVVC‡ nQ{o'—lŠ-Ï“'‰¿­zÕb+\åÁ¸(Bp–5kEàÖ `Âø±¬øm5˾ù޾½{3 _zöèÞ¤Cf¤Ò‡å‰Ä"”J‰­VKaQFFFÈ®® 166FÕÄþŽ×øï‹ÏóŲ¯YøØ“ ‚Çy˜É'èõˆÄ¸¸8cbR÷YÏ/(ÀÌÄ´VŸ!È+,ÅDeT;saoìBúWú/&&ç{c¿¢wG[ŠkôÉ›¤R 55j¤$qƒ¡ãÙyE˜¨�Pü#«ª³�%™Ù\L½"p°6ÇÊÜ„�+§J’�v`ãæ‡yQÇ:G‹D`ge†Ú¨îïUR^‰F«å�ç?¯óï¹ŨUÊË®an|ý7“J$h¯~Í�Ð�ÕÛŽòÎwáC{0¨gÇFë—–¢V)‘ÞQaj¢¢°ä払n¤¢²Š‡Uû÷ÒÇÂño×ôlLC×`û¡(6ï;E~Q)U55ØX\�^Þ«O¾ý3Å¥ì8Ͱ^×·¸¹ö›‰Å"ä2}ÇZ‡®Ñk}�†®ç­`nz}ý³±ZIiYE£úJÊ*QÊ嵿ƒJ)GÕŒ��NÇÅK‰DEEKyE9¾>>5o¶«¼²ªÎ,psÙ¼?‚ÀnuB6KÊ*0R\oƒB.E~u vAqÃ÷YQ©ª^YsôAÃ÷|Qiy£2 ñl ΘÀ}ÏüùóÑh4Èd2ÜèôFTF*æÎžÉ™3Qœ9s†Ò²2Ú{{óüsÏàÓŒl‚–¼öêËlß±“¯¿ý{{ÆŽE÷î×GyÄ"a¡=¹t)o/}–8‘HD�n݈;{_Ÿ廹¹1dð Î�;Çî={Q©ðõñáµW_ÆÞþúz-#¥’ääTÖoØŒµµ5³fL§[×.µå#G ÃÜÜœ={÷‘““C@€?K_ùo­Ã(“Éxtáì]¿ž¬¬lúôéM×Î!,\0�-Û¶#—ÕOã>bØPT*6n¢ªªª6‘GsqqqfÒÄñlÛ¶�v^í5|x=g¬¹íoŒY3¦³výF¾Zö5þþþ :˜•«~¿Ún)O>þ¿®XÉo¿‹Ÿ¯!AuÃSG й™{öî#5-www:´¨­· ‰y9UIdEPCEÓ „½ÌY.Š`,$w~±ô¿ {;;ž{zÏ=½ˆôô öîßÏŠ•«‰;{ŽG.h•Lccc***Ðh4H$JËÊ(k ›é?177cÉâX²øŽ�8ÉcO,¢wXWŸY¥²~ÇÑÆÚšÂâ"ªª«ë½ovîÚÝh˜bƒv˜¨(-¯ ¦FƒT*¡ ¸ŒŠªjDˆèhÕ‡cë·ð¿'“Vy’IJ㜫IG&“ “JÐh´ètº›~O4-ÙyEt¸º6äF¤1Îö–8Û[R\VAbj&j£:2³‰£À4U7”YÞh õ�ñ›9¢–¦jR¿¾÷h=[ò K,k •’yû1ob?bÒxÿÇ�ùºamnÒ`s5%eׯ'@Aa)¦M‡l+rÖ|´¨E6^ãf×%'¿˜ÿ­ßÇK ÆããáÀù¤ –­ÚU[®6RÒ�ï×þÅ•œ|Bƒ¼›ÔÓØµn ±H„æêò€�–Òòæ¿ KÊq¾ú9+.)ÇXÝø¦ö*¥œÊêj´Zb±ˆŠÊªÚ¿ÆˆŠŽaÇÎÝtô÷eö¬‡puiÙ»ÚH¡ ¬¢ªEuJË+Ø~è ïþ§î’µ‘‚ŠÊ*4-‰˜ÒòJª®FÁ4vŸY˜ª)ýGYsôAÃ÷|S2áÖŸMÁ¸ïQ*•,ZÔò—¼\.#8¨S££B}{÷¢oï^uþmuþ¶µµaÆC6ªkäðaŒ>¬Î¿ 2ˆ!C5ZÏÄƾ}zÑ·O¯FÏS(L™<‰)“'Ý´\,Ó»Wh�¬‚ÿ¤G÷®u6¿è×·ý®.¼ê\ ‰DR›}  ¨¸˜ ›6#éG—Ì›SG–D"aÕŠ_êØ4iÂx&Mß MÍiS¿‘¥¥%óæÌª×®kXYY²èÉǨ®®©}áÞø»H$z÷ £w¯°m¸�xfPhv–QÒmѧ©q–ã*ÁUŒåmÑ+Ð8ŽŽL`*Ó›±ž²1¬,-°µ±aÕš?7f4Ÿ|ú¢f†oOœ2�ž}†Î!AØ] Elªoëä䈇»;_-û†ysfqî|Xþ#ù�¥™3S5ÛG1 ›kv«½ÿ»ÏTJ9޶úßhüÀ.$¥góÁ›yãÉpœí,Ô ßó¶–&�Êlˆ–<›‚3&pß¡2Rq&âd‹ë´-×6f63½;²ÊµC†GÝN¤6%TØ^ MÑòÎjk�‹ÕxÊzà© ÅAæ×t…û˜ªªj6l߃D,A"Ñ —’ñòt§oX÷;m.^bÌøëƒ4â¥W–2aü8:vlü÷{ù¿/²ô�·øøÓ/˜7g&¦&&(”M;Ýÿyê ^{ã-R/§aeeÅ# çco×ôLö§}À‹/½Ì�?ÿ‚­� Ï=³ˆvž­ßú@$ñéâ™,zçgÞX¶–ÇŠ•¹1�‘HÄOo>ÂcoþÈ—+¶ãîlËûO¿F?ÒÊÏ`¥ÙÌî kÉÊME&•àhk�R!£ºFC~a ¾ž7±s°µ 53—ʪdR öÖæÈdzGÀÃÉ–äôl²r‹jeV«ÒI`-ª®í°8[}S™‹fŒ`Ùª]lÞ{ 3S“‡ö¨u+kŒi#ÃønͲr 13U>´;Væ�Ïp‰D"MÁ²Õ»Ø²ï4¶ÖfügæÈ:¡k£ûwæë÷óÝš=¸:ØðîÓÍhi='kBü=xöý_±±4ãáÉHÉÌáÍoÖóÖS“}8¢T,aXX`£²n¤µ×3|h¾ÿc/kwgXï l,ÍjgÊšÂÁÆ‚'Þú�FËì ý0Q7­oî„þ|ÿÇ^Vn9Âè�Q)•Èåm»Ž¹[@;þ޾@¯«kК"¯°„C§ÏòÉ‹Õ+‰DÌÈwkö°jëFö ÁDm„V«kò>{fö(>_¾ƒÍ�ÄÂÔ˜‡ÆôÂÙβQ}Ðø=ß�̦hî³)Zµâ—;°‹Œ€€ÀíbÿÁCü¹vŸ}|óMÛŠ„ ‰Œc­E§Óqìø žy~1‡öínq×½JréIâK÷s¾ø/jt- Ïj fbG¬ËB§»¡«Þ¡·BUu _¯ÙKUU ÏÎq§Íi�¦Äo):�Žè„4>ýuß¿>ß Ïæµ}Æ^|î™:ÿ^UUÃoÿ�ç�mUzû3÷æP¯€€À]�¹¹R™” 7““›CP` æÍ±Û€Ä>ŸrÛ dI£Û\—1Š´S„á*¿ùÞ{mËg_|Å×ß~_ª÷ñ§Ÿóñ§Ÿ3vÌhìíxòñGÛDï¶;yÿÿ>&'''Þyëõ�#à¦î‚›º íÔaœ/ù‹øâýèh»í< µé*Óqô Á8ÏmVýp0�¦ÑjuÌZ¼ wg;ž›3òN›Ó&ŽLà×�((*ÅÆÂŒÇ¦ iõ³¹ióVF�Áž¿öñçÚu¸ººòèO1|èPF�Ò;³r¹”G¦æ‹ßvð΢©­Úøùߊ0©·Sq IDAT3& pŸ VW£uN!WI¹¨°Íõ¹+ºá!¢G›ëº¹3cwŽø’Ä—ìãBÉ¡6×¥Y`WÕY†'Ú²–g°¸{ÜV�û‰s%VçÉ•œos]®ò`<=ñ”÷D,>#í�ûà¥å¼z %û¹Tz¬Ít•éòI”íÄÖͳŽèÒ›—=V@@àîCøŠ ÜÃHÌÊ©vL"Ó(’ÊÛT—£,€vŠžx*B‘ŠšÞ·F@à߆X$Å×d^ê^Ä—ìã|ñ>RÊÛ.yN–.–|³DÕÝP\ñB[$$£¸×œ1�{±G:…æçÚ<]½ƒÌwEÚ)BQˆšÞ3H@àߎL¬ÄßtíŒÃ8_ü %H+�j]Õº2’%û°rNâ$R„½üî%¤ùm¿®@@ÀhµZ´Z�>}r#»ÉWVVRT\ŒX¬_ƒ! p¿!³.Cë’D®q Դݲ_ ‘;ÎtÆIÓe…)ù”em¦ïnE£Ñ Ñh°¶²D¥fš�RlB'³1x÷"¾xçKö‘Yq®Mtåj/�«ºˆK‡¨²}Ñä '÷Rccõ�¶A@ It::�Žêªj ‹Š±µiØ+,,ÂÈȹB^gÑ»€Àý€Ä%‡RÛ³ˆ‘Ð6ƒ –WÜeÝq“õÀDüïNQ¬ÓéÐiµTTT’�“‹›«àŒ ´µÄ’`ó xªC‰/ÙG|É>²+/µ�&©¢¿±¶ÏÅB,¬%¸�J„™�{"*«ßÏE"•¢P(„ê÷:‘±W¹f§)×"Âðƒ "‘˜öò¾´WöÇZâipù÷2"‘ˆ²ò¶]“'pÿc&³§«ÅT\U!Än%¦x:�Æàzr´ñ”™ç`¯è�(É‘Nø Ü­kÆî-D4z(‹iƒ~ª€ÀCbVN•óEÒ•'¡�¢ídh/ï‹—¢OÛ(¸×‰�H„­€a°S´Çζ=J?bŠ·“^cpeÚ<.)¶ââ†Q¦¯�ÜC@à.EpÆî)šr(„& Ü/Hœ (µ�#Wß&ò"í•ðVôÁLìØ&:îZûNÉÎÉ#æ\<¥å«ÕøÑÁK˜uÐãg:#_b‹vS¸• m‰Áu¤Šcë\€y~Ú a£h�» Á¸K{¦‘kq†R²ÛD¾‹¬ÞÊþ¸Éº´‰|8M ¿/žî.äsðï¸:;ab,|~ôXÈ\èe5{¥/±…ÛH,;npYÚXÊ-r±•wC”ìnpù­GøÜeˆŒ«Ð¸^"]u�Nkpù&k¼ýðVôC%27¸|�ë Ô©TŠX,ÆÎÆ ©TŠNkø5B÷>^ê0”¾Äm'¶h…ÕW *¿X›I‰j .Þ½Q¤w@W*ì( p7 8cw»BÊíÏ“-�k“õaòîtPÀAêoxá�{.{;k!=¾@ƒ¨%–t³˜†ƒÒ—è­ėì7¨|�NCŠd.˜ä Í¶0¨|�–#8cw b÷ ,£(e\¶¥Ä™öÊ~xËû# #â·�NGdte•ôïÕãN›#pàbŒ½ÂG#?b ·‘S•dPùDRn“‹µ² ¤ ›D ÜIgL@@@à#Rhк_"Ãø5TV6¢Z'ÌF*$ޏÝh4Zþ>�µµ}C» š�L¬$Èl<ö ?b‹·S¸† [.ЦRjœƒS»0diíÑU ]B�;�ðä ÜA$V¥T:�çŠ,Êà²m¥íi¯è‹·¢¯Áe 4�c§"±¶²$ЯÃ�6EàÅ^ÙA(|‰)ÚNFE¬ÁdWëÊI’íÆÉ£uVG´y&“- Ð<gL@@à®dùÁ³$g—P ½_SŽ‹qYnŽAåÖÔ(ÈHò#3¥=å%•ÀNƒÊ¿×±“VäfÍ«ám2XRZFú•,®dçr.቉DBïЮ´o'ÌR ´ Ó¡W|ì ¦x•Ã¥Á¿Ì ¬ìó±T… K³7˜\�¦‘FE~£ÁæbbjŠJ©ÄÎÎöŽÙ p÷±üàYŽFD£I>ƒJitßmâ-’èh7Ü|«Q(´èŠ '»8CÊåÃ*2#"�Hà ¾_ÐÁù’b. F‹Ž×Â{¶™*cµŠI£‡!•J‘Édµ‡Tzÿ0´-–rWz[ÏÇJîFDá:²+/Lv®ö¥&Ù8xôDœÔ‘N©¸HƒB &,*ò4¡½ú´¨Î… ñ\ÉÊÁÎÖÚ`vÜÛ$g— I>ƒT"!ÛÀ³Fw¹Z„ÿh)îƒÅTUNvÚiˆßSEv|¹á„Þ‡ØX[S½‹(Ÿ€;mŠ€@«ð3‚©ÌžÈÂõ\(9d0¹ºBÛqk×yŠ/º*aà@@ ­‘š˜ÜÙø`/¯öÄD £·×)ÐJQ)•dçæÞiS ŠÊRïˆyõT®Vñ»!~”çßgÓˆm@vN.NN\©‘ßiSZ�³Q æ2G̤D®C««1˜ìdé_¸zT£L툶LxNÚ’6òhùÆ8EEE†7C@@àÞæ>ó)L®Îˆ…Ö+ÎÔ¿.ì»Ï.X[#\.�ûc©5½­çc&³'¢`ùÕi“�":„“KêŒ@´EÂÞxm…Á�1�® v)¸‡±ôã7ZŠKˆa=€Ìˆß-"ÃpÉÕîAÍFc*Õ‡-&•�0˜ÜË¢ã88Uc* F“kl0¹×1üÌ˜à‹ Ôbë#Æo”‡Ž†uÄ.ìÓ¿НS<à®îйÜ�ˆ{În2˜Ü ]Û*ÌD!èr, &W@@@Ïm™ûò«¯xìÑGëW  �ûÇNüFI°ñ6Ü{®<_Günñ{Dh5+ p`.s¦¯õ#˜Éˆ(XKq�a eébÑØTa%í†6ÓÊ 2ôÞûÇß?üð=55úE¥ßÿð=sçΫS.¦Òî0�†“§Nó÷Ñc$%%ãàà€W;Ozö쎣ƒbíº |òÑÑŸp�¥¯¿ÉWŸй¹Y«å\JLäÔéHbcã‹Åxz¸Ó¥Kg|:´o²nuM 3gÏãéEOÑ¥sp«mh[tÜËSí®]¥ø�cá†jGöyñ{!íô½{]î„k(p"I 1Ÿ¤[,XOZ…a6”ÏÕ% ±¨ÄFÜ]º�Ad €aW’×;P:V­\IiiYmIii«V®¬sNSßÃÒÒ2V®þ�_z™õ6QuCè’ÒR^X¼„cÇo->úèÑã¼þæ;¼ýÞDÇÜ_‹/<Ä¢§Ÿkuý;wsôØñf�[]Sô‡fqòTDƒçèt:´Ú»«´uûV­^ƒ·—³gÍ _ßÞääæ’‘yåN›Ö »÷ìåõ7ßA&“1vôHFކB¡à||ü�6Mðè%%p’ 7Ã͈%�ˆÕ:ÒNL¤€€À}Œ—q/úÙ<‚¿éƒÉ,Ð¥�i¾œ —(D@àߎá�1�t:vîÜEVvv½â¬ìl¶oß^{ž® olûŽ�ä0í�©\LLäÐá¿kË6mÞŠ»»]»tnµ¹©©i|öåWôéÆÔÉḺ8·ZÖýH^~Eņۑvçî=¼õî{“w«T×Ô°eëv¦N gäˆat  {·®<<.Á�:Ýiónʹóñ¬Z½†×^Y¸1£¢sH0á“&0zäÈ;mžá¸»|öfã=PJ§‰L 4p\U1ë!b%ä§Pg,K8nñh3>þ$~�!ø†ðÔ- š�»vãÂÖí;šuþÁÃG9fþA]îÊGŸ~~Ë6¼ðÒ˼ýÞ‡·,§9X+ÚÑ×úzZÎD)6ÌVFÅÚLÒM÷¡sK2ˆ<�;m°fLÿÿÁƒ1xР&Ïk*LñÔéÓ„Oœ@`€?EE…œ‰Šf@ÿ¾Ä�=Çþyëõ¥ˆÅ­÷)s®îcÔ³Gw E«åܯ<0eò�6¡�ÑÏ\”””Ô+‹¯ÏjTTV²eË6"ÎDQVVJï^½|{ûú ïü‰üü|fÍœÎô¦bnnV;‹wöïðxyyñð‚ytD¥º>,¾ú÷?ˆ‰�eôÈ ¤™¯_¿‘ÈÈ3Œ7†)á“8ɶú�‰êêj>ýüKªªª˜3{&ýû÷å»ÿGLl\­Ì“§Nã`ïÀSÂéÞ­+Ÿ~þ%ii—¼7:ãÆŽå׿yæ  ]úú›¸»¹²ðáyxzzðö»ï“›—WOΕ¬,Þ~ç=l¬¬™>m*ÎÎN$%%Ö;/¿°?æÍ��JeÄ7ßý@u�†IÇ3vÌh<=ÜyÿÝ·yîÙ§ýýn½BCùà½wÈÎÎæ©§ŸeùŠ•deÕ ±573cäˆatô÷£[×Îv$)9k++z……ÒÎÓƒví<éÖ­+úë{>>�è˜f>4�À€ŽtéBß¾}jåææå±nÃFÂ'MħC{Ú{{Ñ·O/6mÚBõÕÄ77RXX€±±ºÑ6þû(666L�< ¯vž1sÆtþøsyùùõÎ?ŸÀ±ã'˜=k~¾>øthϬÃÙ³gkÏ>lÆ�ÅÏÏ÷¦Në¿™ZD§pLj æˆ%ƒ¨µ‚#v¯#‰5¸?Nv QU]�B.ÇH.7¸®õë7â‰';!œÎÝøœv™a#Ƕº¬¹ÈdRF Õ¯KŠ�¿Ðè¹�†W^cc5ÿÚ‰N//}¹\ÿ]Ý´e+¾÷/˜Ãž½û8pèp£e%¥¥Ì_øÕ5ÕìÙ¹•Æòè“‹ÈÎÎ!''—7Þz—þü}%1Í\>dð â¢NãåÕâ¢Nuš™ÓlT_cí335áà_»˜þà4â¢N³qíšfÙãêêJjj*ßÿø3ß~ý§Ž¦Oï°FmxañÚ{·çÄ‘,˜7‡>Ú„[ùP:˜ôo–Þ¦ÐêjH”î¤Æ+‘¢þ7K@@ iîø>c:Qã<ÜÝxçÍ×ÉÉÍÅÞΖä”Töüµ�7–¾BIi)%%%ØÚØÖÎòÜȵæçŸ~„•¥~ÍÎΖǞXÄÙ³g 舅¹9öv¶Èoò�ÔjµÄ'\`ÚSðéЀví½øþ‡Ÿ1|qgÏqùr:/<ût­=={tãø‰“tô C‡öôî €›«?ÿºœËéú¸›qÍ‘¸Æ5G¢Kç>B�Ý8@ÿbvuqaæ͜={Ž^a¡uä;~’öí½™0~,"‘?_ÊËˉŒ¬›©)¬gÏÚ }Z­–=í£° ;[[½#«TâìäxS[ïNŽÌ�=“áÆpøÈQ=ó\£™mml¨ªªôm<~â$g¢¢INI%==¥R @fæÜÝ\Ìš˜›«wzÿ÷åze¥%¥õê™^M/))m´=‰‰It¬’è⬟íÌÎÎÁĤîÚ�ŒŒLÚ{{×>'ææøúú�‘‘I`@G”JÅßÝ'YŠð-Å»¿áu\<¨#v3”åL¤ÀæJv‡��F­VÑ7¬;2™á?½ãÆ�!(¨/½²”ß~ù‰Ÿ~þ•ŠŠryx@«ËšCuu ›¶l­•Õ‰„}»¶×þíÓÁ›í;w¡Õj]Ýêf`ÿ~¸º8Ó+4”Ï>ÿФ¤$\]\,Ój´ðøc ±07cöÌ|÷ÃOlÙ¶ kkk´ £FÃÂÜŒñãFySÛšËÑ£ÇÔ7kÆC ¶O,s“Ýš…¹¹þÝ?súu2æ8x¸Q[j45ÄÅűnã&z‡…òÀäpÜ«=‘‰•Änký…¸�dÉ_¸¸U¡L눮LXò! ÐnË>c�!¢éNŒ‰‰1&&Æ”——³|Åo̘>�Ì+Wøô³/(+/§[×.<¼`FW;À×hn³1Äb1Ó¦Næ?ÿJNN]»t©u¢***ˆ‰ÑÏz5„L.¯ã(6Õùμr…¢¢"fÍ­û!ôõ½ù##%¦¦¦T×T7Ù–kÜèH$'¥pâÔ)öþµ¿Î9yùõê¥$'ãÕ®]í³9˜ë×MÕhî�3GÂ'ŽG¥2bÅo+ n:‰Ç¦-[‰��eäˆ 8€¸³gÙ²Uÿ1Öh4h4ÚëÊdúÐÔ%/½ˆ©iݰ%õÕkw#&ÆÆx{{q)1‘öÞ ÇéëtºzOÖµŸí濟‘¸þ¿‹ÕY‡&p#K�&Iñ5\€Áù]·UDeñ½¸bN !ìl¬ ;œœ¼=ÉÈ!0oƒ0Å311øûùw¶6¼üVÊšâ¿üÊÿ~ù;{;æÍ�̓S_g¬ÓéxïÃ�ؼe…EEh®Fhµ×ߓס®½oÌ¢|³²¼ýlÿ›o½Ë›o½[{nÚåt$R}7ÇÌÌìjý[ß´¸1}�µOîÙ¾ÖWêØ±n¿¥1[^õeÞýàÿxíõ· íÉg}ˆ™ÊžPË9ÈDFD¬m•Mÿ$U|Gç*Ôi!‚C& ÐÚ`ÍX iÁ{ië¶ØÛÛÓ¹s/.^‚ùsñóóã«e_yF¿fç ÑÁì×·þ~~œ<}š¯–}ƒ·w;œöÀuy-pNšê|+ ÜÝ\yô‘…uÊ$’¶‰Å6R1lè`ô¯²pc¨å5t:Ðêv,îU*++ë%oQ«TÍJ £ÕêØ·ï“Ã'Ò)0€+W®§Ä···#5-�‚‚›ΎYYY"—Ë‹EÍš-‰DLž4‘O?ÿ’Žþþõêèt:D"înnDÅÄ0jäˆÚv\NÏ@.—c[g­—þ´³³ãüùxò jg‹ ‹Š¸pñ"ãÇ�iÒ®25ø�6œ#¦ÓÁÙ­:b·€æÖóÜ…ˆÅbœ츔œJÆ•lƒ;cO=ý»vï`Åo+ضm;L�BNNN«Ê^~é…&õΚñÏ?ûŸfÛù÷Ñãüòë |`*Ï>ýÏ/^R«¿µ89è߃¯/}™IÆ×)Û¼U?ós-Ä;+;ë–t5¥ïÈßÇZÕ>éU§ñšãYpÕÞrm­zsl:xCââ¥DÖmØÈ�?ýÌÖíÛ™4a<*‰¡–³‘‰Ï_…!ÒÚ¤‹Oàâ,G™Ü ]å�ïb Ü ~͘Nײ£™rã.°c×n&‡O¤ª²Š+YY8;;a¬VáììL^^ýu07v0¯q­ƒéèØ²09k†Ââžcï_û9w.#¥Ÿ¸œžÞl9ÿì|ßxȤìlmIÏÈÄÔÔ¤N™ƒ½}‹ìm.®..dçäâäèPGŸ¥EýÑCÏvÄÇÇßò>ašÍ-Õ7$…<÷âKlÜ´™S§#ˆ�;˶;Y¾b%'ŒkÒ!‹E¸º¹p:"R¿ sD$›¯†ëøtè@{oo~[µš„ ILJ""òLm¹©‰ áÆóËò•DDF‘œ’JDd;vínP§Ÿ¯ãÇŽaéko°eË6ÎDEÅÚuØ´Y¯;,´'ii—ùcí:.%&Ëò¿1eò$ÌÍÍ�ˆ%8:8Ÿp�Ì+WðéОÎ!ÁüüËrÎÇ'�pá"¿.ÿ� N�øút¸Å«|!’ˆð-û¿a^ŸÕ½NGÔ:Á»ß¨¨¨dã¶=deç¢ÕjÉÍË'77‹6˜ûô£êÔ‰ÍÖrü»¹uš—_z¡ÕemAY™~ïQµZEJjZíû°´¬¼Õ2;‡ackÚ?ב››Ëú ›èÒ=Œ#£{×®ˆ%6oÝAn^>›6mmZà-èkª}¦¦&H¤R©¬¬¤  ­V‹£ƒ©”C‡�’—_Àž½ûnÙ–ŒŒLúôÂÿ~ùw7WB{ô¸jÛu‡N&Vj5›0«YÈD† CO¦Êõ,ˆ…~�æÐfÙwîÜMBÂÍÓ§vèàS›Ì¢9îXyy+V®bÆôiØÚØ Õê ìÈÁƒ‡ißÞ›3g¢˜ñЃõêÝØÁ>l(b±˜;wµ¸ƒ¹{Ï^066®M”q-~{ì˜Q|óݘ›™aooOaa!eeeôèÞí¦²nì|‡Oœ€¥¥yyùdeg1tð Ú{{ÐÑŸ_~YΠAP(¤§g`l¬¦S``³mn.¡=z°yË6þ\·žà  ´Z-.\$8¸S½lZ¡=z°mûN6lÚDP§NTTTÓ"}FJ%IÉÉDEÇ¢ÑÔÐ)0ðކÁ©T*fÏœITT‘g¢)++ÃÛÛ‹gŸùO�¸üƘ1ýAÖoØÄW˾Åßß�!C³jµ~Q¶L&å‰Çaùo«xë�÷ðõñ!ø¡�Æ ÁÔÌŒ½ûö‘–š†»»� ê‰D 6/¯vœ>ÁÚõhçéA·®ú¤6æ,}å¿ìع‹o¿û{{;ÆŒE·«Y7Åb æÏeõš?¨®ªbæŒé<ºp»vïeõï ‹ îÄÀþýniëˆû‘Žc¥ø3Ì5)/„¸­:nmb@à.E©TèÇé¨X*NF V©ì苵խ‡Êý“šš.g¤ãéáÎ�C‡ñ÷÷»å²ÖòÏL„»vïÁ/0„Ý»óõWŸÑ§w?ý²œ#G�ñÁ»oñâ’W˜¿ðQÞ~ãµVéS*•|ýÅg¼ñö» >ss3üB{ê£e^þï |ðá'„O}�IÆq&*ê¦ ’ ¡¯ªººÁöýþÛ¯( ž{f_ó=!ÝÃ073cÇÖ�˜››ñÄ£ ùþ§ÿ1sÎ|œ6…'N6�ÒTÛçÎ�É/¿þÆGŸ~޹™9óæÎfØ�Áõätµx™Øˆãy+)ÓÔÜn)É’¿ðð”!¹ÐvÛ8Ü/ˆ6mÚd°¡‹Ã÷Ó¹óõ ‡«×¬!ç?[ÛØ0%<üú?ˆEœ:q¢Ñõ[ë7n&9%™ßÖX IDAT…Çy¸6!AVV6¬]GÚåtöïKÿ~ýnÚ©///g×î½Dž‰ªÓÁ¼–`!""’>ú„ÿýðíMxÔÔhصg±±q¤¤¦âæêJÿ~} ª='*:–�˜”Œ«›+�C‚èʾýزm¼ûV™�†#ãøÉ“u:ß×’€”••³{ÏÎDÇP^V†«›+}zõÂÏׇ¯¿ý+++Â'^GXøØ“Lp*½Bë&Ü}ŠÝµë6ÔI!ÿãÿ~F©4bÚÕØþ+YYìØ¹‹¸¸s¨T*<=Ü4h öv¶õRÛ§gd°}ÇNââÎáîî†N«£¤´”Å/L˜0®^ØEch´ZJJJ±h !@~A!ÆÆj$w™Q]]Ó& ÷ïGžû#ÍéMµûÞ-øŽ”8Qj�¬‰%Y»UGâA&Ðb\œ�‘y‚#/iÖù5 ùùØÛÙ6[GôÙd2R©™LV{H¥Ò:‡D"A"‘ ‹ë"‘¨Î- �h ±ÅÛ9ž·’ÂêŒ[–%Iq¯"ìC& ÐwÆBB:×ù·Ÿ~þ™²Rý´½J­Ÿ…¨c€XÄ©“'›•LCàîc˶íÄÆÆñümJS/;cÍçntƼI œ(A®ºuYi:â¶@Êñ[—%Ð:gL@àæœ/Ùljü•äTÖߺ¦¥ÈÅj\J"Jv¿eY÷+m¾Ïج™3�J%H¥fÍœQ�1!¤øž¦¨¨‡¶YÏ&  AÜùã—¿Q†qÄr.è÷K9~çÛ%ÿ¤ƒq?B-çà ¼õ�Õ*m)êCàœfËîOnË>c æÏo°Lø Þ[üºâ7¼½¼ppp ??Ÿƒ‡Ž´h?�{ —®RüGKP`™OfœŽ¸Í�u^xï ܽxª»#)9žÿ©å·$«L›ÇÓ#Ø;öE—n×t�†ßg¬…ΕÐ%¹wÐjuøtè@TT4ëÖoÄÚÚŠY3¢£¿ï�6M@ Mpè$Á´û[ K‹Ð‡&æ% o=�»U'db½Cv©ôï[’U¬ÍDbö7¶ÚÞh3­ d¡€ÀýAÌŒµÐbßïÄb]»t¦k—ÎMŸ, pËÜY§ÅÆGŒÿ()n·þŽJ:ª#n‹Ž¢æï‚!ÐÖ´ró]�öÊ„YÍA.6â\ñÞ[’U KAbqËšèr ŸQT@à^åŽf8‰Äè´:LLŒï¤wfâjÊË+êmŒ~»°ðã?J†M{Ã8bÑëGìnÂÆÚšâ’l¥•wÚ�»+¹¡V³ñ1p˲ru ÚœFbUbËîÚ Ïvsý;:�–””Tªæ§6øöî;¼©ªàø7«IÛŒîRVCöÞSö�½*‚"à@@†óuЍ(S‘-ÙŠÈ.»-tÝ{&m’÷�B Ò&mÚÒ çó>y›{Ϲ¿srï¹çÜsááè¥#Ú¿ Æë§ñpw\ºÚÇÌcýÌx7°¢;â.)H:©ÆM%íj)'”œÙLZz:ŠF=iæçYÞÑ‚VîM ×§0˜²JÙæµ§�º•ì·Xþ‹DcL„G’DiD_å É’ðçÕÈ©u•=K!*áa’­7p98”f�ÅDYÂë±n -Ýž¤¤K�$˜BÈð:�DYòeEá¿DL_#Â#Éäw�ŹçÓȱ? UJ!"áaG\b·íF&“Y^zƒžö­[”wx‚PjZ¸>EŽ)›ãIkJ”ÏMþÁ¯ªyhÝRŠL*>ù¹ óåƒðˆÒhµ8©Tx{{•w(BKVv6i©©¥ž·ºN¹Æ ˜ÒsJ”�GJS’ÝÙo.Ýõ…²åîá�V­ÆÏÏ·L�ãW½ 5ý�Ëå( Î]¸‚³³3MŠ MááÓÂu$9f=§“7”(Ÿ›Gñ­¦�Èj¥™ Tlò¶í;–w Â#,4ø*Ñ1±T 2áÑ1±H¥Rüj”zÞrï’*%GÀþçj;u£©ÿH¥bR†òöÇž� þT±Òœ<ñ7aá‘øùŠ >A( ©2¯‡ÌœÅù”vç“cÎ"^s/O5¦¸’O"�Ìåƒð ¬Y“ ³gÊ; ¡‚ÉÊΦa£ÒŸ^â¬'½Ú52‰A^‚QÚµ5�ÓÖm:E¥RŒN(sñÎeÍšµäàþ½eL!Çl\…Bñ@�)’“LG —¼!‹WÒöÛ�O²)G÷ Ô™­0g(K1BA¨xäÅ< B©KK³' ù•ÅÐD}å+DQ²Æ€sZºŽ ± ÆlÇ�Åøøø2ˆDm:E%ZºŽ$ǔ͵ û×oŒâ Õ+»àܰ£„ŠG^Ü»‰‚ ÿEfß0"å‡K4À[Y‹®#pw(Ûg�„â3‹s™ Tî¾´pAFn1ú«vç)?L€¯I¸_©Å&�\œÀ„r%)ÙT¸‚PRŸ¢œ�c6íÎC%ÕÐØe >*1ùB…$Ne‚P¡ø¨êÒØe ã–�m²oŒÙl$Öù8>>LQ T rqþÊ“D\A eLªÍ$Ùõ i¦èåÓÄe0u4ÝJ)*¡´tcñÅ øféÒûˆA‚Pæêhº‘šÃ_‰+ìÎ#Í�£ë\2Ú`Ju*Åè¡b�æÝN/ñ*§—A(3f‰‘¬J—ˆ5](Q>õ´=hâ2°”¢Ê‚ù_ÿ›òúrrrÈÉÉaÊëSîÛ.~|áÁhâ2�zÚ%Ê#Öt�¬J—0KìÝ •ÕgÆ/^‚V§eô¨Q0$áQb®7§�F#'Nžâ¯cŽ��5hÓ¦•}|8tø6næ³O?*•ã] á½¹ï³øËÏqqÑÙ�Ï…‹—8DÐù‹T©ìC­š5iß®-ŽŽªR‰Ó–]»÷¢Óiiݪ%&“‰$eÜãpíÚ5öíÛÇ•+WhÑ¢}úôA£Ñ¸¯É÷—2w‘˜˜Hzz:ŽŽŽxxxÜ·ff& ddd ÓéðôôD.Ï›m±ªª!Mtƒp�:—i¹„ºç\6wîÑŽÇ�T®˜çfA°—ý íÂC '7—ß·ïdÜØ§iÕ²…åýV-[`2UÌ‹µó.²k÷^Þ}k&•*y[ÞoÕªëÛÀº_~åå—& •–mÕS#†—iþÙºu+¤{÷îøûû3cÆ ‚‚‚hÞ¼¹e?™[: .§1™2©U«–¥·ÎÙÙ™äädRSSqvÎëéŠ��ÅÛÛ�¼‹'''®\¹BZZ�'PCÝþ—R°Ç�ÆÕøqã?nœÍýAx°j¨Û“šÍ¡øoíÎ#Ó”H’ËiܳÛcLT—bt‚P~dC‡ }ïè‘£üºþW6nÚDLL :��‹Žÿœ@©RÒ aK‚Ó§N±fí:Ö¯_O|B<Õ«UG© ò ö’póæ ¼½½Š´·ÙlÆ`ÈÁQUøP¼ìl=H‹0\Îd2³cç.jÖ$Àß?d·Ó‡GDpúì9$À/ë7°s×n †üýü�J¥DEG³yË6~Û¸‰]»ö��˜„oõê(•¤¦¥±iË6Ö®û…7n’��Èå+WèÛ§7*•ŠœÜ\ö8Ⱥ_~ãÀ¡CôªU­ŠL&+°ü‹—,¥{÷®4n”í™TJÕ*UøaÅÏÔ­[ww–-ÿžk×ȈŒä×õë9s&µZ�—§'�×£µc×.6nÚ†�›�ˆÀǧÚÛÃøL&û› ›6³í÷íÄÆÅ¡ÕêÐé´,[þ=‘7n¢P(˜2m:‡�å·�›ðõõeÝúõD„GR¿ÞÝÙ¯‡0uú ºu늃Cá‹ßÆÄÆRÝ×/ß{)))|øá‡Œ;777är9III„„„вeK˾ÙÕÏÍÙ|Ÿã�ú‹��ÇÉÉ gggrss¹~ý:UªT±,Æ+‘HÈÍÍ%嬎qí ‘H �S¨.^¢víÇŠ•F\¹r?¿"ío2›ÉÎÎF­.úpÕØøDd2R©™LfyI¥Ò_‰Äæ l~jô3 \ñŸqì?xŽJ�=VÛò^ëö�éÖåq\\\îÛÿû?1jÌ8¾^ò 5kð Ã-wS§Ï$0 77×2ÏÓÚçPÞÊZd›RmNwéò\tºσ™æœœTȪé˜ó,B©PPµjU»b. ӦϤq£†–›€¥©(å›2íMZ6oŽ££c©ÿŽå?¬À§R%4šü�ä²,{Y(¬> +Ÿ½î­ù¶­Û8xèO>97WWnܼ…“£ Ì·—мg(Ù‰'Y±bcÇŽÅÕ͕Ç�ðõâżñÆ4rÑÉö¨Ûµ{7ÁWƒ ÜöØcµéÖíþ™èÊû™1…\ÆèQO²xÉ·ddfѦUK<=ïF‘��  ô#33‹Ï¾øOOš7kŠB¡ÀÏ·:mZ·³™ß6nâ÷;xrø0ŒF#_/þ''G†ŠÁ`àôéü‹oÚ´…ðˆ ì�T*eã¦-dfe2 ß÷Å‘–žNHè5ÆŒ.ø9NWWjrëVµkÕàô™³tîÔ��ýûÁü…ðþÜÙøû£R)qvrbÈà�(•JŽþùK–~ËìwßA!—±cçnŽþù'C† ÆÕEÇ­[Q÷5„+ûø0gö;Ìž3�žO�5pÑi1sùaÅÏ èÿªÛiΞ ¢[—ÇQ;F¬ÄÄÄÛeÌÑàååÅéÓ§ï¾Q=’Hɱ|ûÜ™È!11ïâøNc.''ÀÒ»ÃÅP“¿ßDú¢ø]û¯°gÑçá�#ÇHII³4Ä”ŒV6 ¥õ6’˜˜„ZíÌ®={9á"S_›d5MFf&-Zßíýuss£SǼûÖ ”ʲ½ÑºeïÅÚüØ1Œ;†1ãŸ/£ˆ*¶Ðk×þyZ#•Èiâ2ˆ”œhÂ2ÿ±;ŸHÉ1jT¯Õlîûάvç¿ ¢”/*ºd3ÇŠŸW¡Õj4 _©ç]X}–eùäkÖ®å­Y3©_¯>�·Ÿ�¸÷¼vû¿×¯_ÏÐ!ChÞ¬*¥’I“_ãÖÍ›øV÷-³ …ÿ†žÝ{�œ”tß3KžžtëÚ�‚¯•Êÿª}Û¶øûù³s×n&¿>�>½zÒ£{7¼¼<-û¸ètôíÓËòwÆõ §y³¦x¸»Ó¾][˶–-[°cÇ.ž>Œ+Wƒ :>ßdZ­–ý��˜ÈÆÍ[X8¾ÕóN*�:¶ç»å?Чw¯ûnr¤¦¤àd¥1ãíåI|üÝÏ QƒúôèÖ€ºuëÍáÃG ð÷G&“Ñ­k˾&“‰m¿o'%9…ƒ‚_ÛÀô©S¨[·À}½‡ŽŽ**ûøàááAÕ*•¨Sç1RSS ¹FƒúuÑëõì?p�‰/½XhìÖÜyÎëß '¥Rɵk×0›ÍÈtY$¨ƒà_CLããã¹uë�ט3™L@Þä-�¿§ÁUQmfmŽí›‹Ùl.ó I„ÒQQ‡æ tëÜ/w Å}ßßÒ4xàV­]Ç_Ç×·:¯O¶Þ»×_G¢Q;s+*Š)ÓÞäçU«ynü8BB¯1{Îû\¾r…JÞÞ¼1õ5:wêÀÕà`f½óÁ!¡x{{1sú4/¶y >`ÍÚu,˜ûöÏËö�»X½öLf3/½øýŒ´´tæÎ~ÛjZkq¶nß™µ+Wàç—w}Ó®SWV­ø??_Z´íÈ„çÇóÊ•¼ðüxÆŒ `u›µúüjÉR²2³Q9*YñÓ*rs�ìØºŸJtìÒƒøøxú @ÇŽíYúÕVËfëx¶òܽw+W¯%[ŸÍ«¯¼ÄÓ#Ÿ ôÚuÞ}o.^ÂÛÛ‹EóçÒ¤q#›e¸ÃEQ•ƺ�¤äD‘”sÈû}¿u+ £ÉtßÄVz½ž[QQdee£P(¨R¹2NNŽœŠßÊŸ?g1ñé©–}W­ý…Üœ\Æ>=’µ¿¬çÐáÃŒ5’Ö­[Yö‹‰�eÍÚ_¹ŽV«¦O¯žy7T�¨èV­^KDd$...<óô(òÎ}7oÝâ§•«¹�‹‹ŽaCѰ~}›ŸÀ‰“§8pèfÌôéÕÓò=³÷xÖÊwåj0+W¯![¯§U‹æùâ(‹òÍxëRSS™·`!õêÕã• /X¶Ÿ:s–aÈÉ¡_ß>E*»½¢¢¢Yµf7nÞÄÕÅ…ÁƒúÓàvBB¯±rÕRRRèÕ«‡eÒÄ—ðöò²ZŸÖÊg­ Ö¾g÷Ö‹Ü`0P¹råBï*æ=ûl&##ƒà�‚¿úŠ/¾ú*ß>))©ö®¤ð` >œïø�ÌÌL ïù›áÇúý�P1.t«TöáÙqcéÝ«Gÿ<ÆkSßàõ×&Ó¼Y“÷÷òôÄ`ÈëU1™LÿçgÏÉ­[·,=AÑÑ1øùV/tÖÄ„„¼Þž™oÝ?Ë[FzÆ}é4Ú¼áƒY™Y…–%.!Áêð?_vïÙgùûÊÕ`Nœ<Å•+Wˆ½Ý�Î5æ’—Â�ß{h5ºw홳ghP¿.¡¡×�J¥Ö°o2Œ;CîôfÝ¡×ë @"‘ ÷!Ár_Z*Uª„Á` **а°0jÖ¬isçBþÎ]ÛÓ‰‰4iÒD4ÄþCŠß{0Ÿ­!'U÷0Ý!•JQ*•´oÛ©T†“Sñ‡$s�äää T©0›ÍL�>ƒÎ;ðíÒ¯øë¯¿™öæ,öìüWŸ¹˜þýúòäðaœ?�YoϦsÇH$«ÛÞ™õ&ïÌz“  n,ž=ÄÖM¿qéÒe^š8‰^Ý»8báŽAúóõ×ßp58˜Z5kbÈÉaÓæ­,ÿv‰ÍòZ‹Ó™TÊ?'N²mÓo\º|™_y•>={âáá^è6ww7«õéë[�ÿ}öõëÕaûÖ�h5Kãýлiש++¾_F�"Ï´õùÙÊóÄÉSlúí.\¼ÈÄIS1tLšò�wîÀ²¥_óç_ǘ8ùuöï݉ƒBaµ ÷òsnAjn4â—`2çr+* W7WÜ\]‰‹�·Ü0ˆ¼q�Z�oõê¤gdI­Z5qª¢Gê{ƒØ¸x¼<=ÈÍ5rúÔi^|þYž>”'‡åë¥÷?£¶ôÛå4mÚˆ /> 2ˆQ#ŸdÕê5ùN…ÙúûvþØ¿ŸÖ­Zò츱 ¹çY £ÑˆÑXxwþ]½=k.Z�ï嬾\²V£ÁÏ·:áæ—ššÊÕ«ÁøÜî©*ˆÑhBz»tþŸù5�þþŒ=ŠgÇ�µYÞâhÖ¬&##“ÓgÏÒ¥s'”våuçÙ…äää|ïÇÅÅáïï�¤r ·äÇ M/‘HP*•xzz’––†Á`°üàßià5Ñ ¢¡®4lذм„ ÈžŸ« ''‡m»þ`ÝÆßÙ¼c/WCÃÊôxGŽå³O>"6&¶ÀYF Ó¦}'ê7nÎÓ㞣Eóæ<5|QQÑ\ câË/áìäD·®�ãë[�“'Oy7ÛNœ8Ihè5š6iÌÎß7[.Ú¬m³eìÓ£quÑѶM+üýý8wþ¼Õý FŽΊŸW°}Ç.ªW«ÆcµkÙ}P)•4nÔ/OOBBBÉÈÈ &66¯^Tª¼!ŽfÛ×xÖX+ƒµïÙ¿ëE�ÎE[ÀaÌ`6a6›Q(ä4nܘ¬l=UªÞÿÐdE""”� /æ�é·ù½�”ÿ÷F¯×ß÷l„³“R©í‰L&3bø°!4º=ÑMLÌÝiw+Uò&òÆ ’“S ìswwË›lD*± ï³F"‘0lÈ`Vü¼Šõëç»[l4Ù½w�4°ärE™—Ïš{[ä2f“‰¬l½ÕXì‘šš†“£s¾ÏV£Ñ�š–FFfJË6•J…RY²uY­Õ§R©,ô{vÇ�z‘�>œÕkÖ2lèœÕj¢£¢q÷p§V�hµZÂÂÂ¥f�@ôïÏGŒ““~þ~d¦gtþ|OOOjusä†ùz½ž˜˜*Uª„ƒƒ©©©$''ãêêŠ\.'''‡èèh¼¼¼,½’^^^œÜŒï-ª…²víZªW¯N§N�J«ð`Ý ½|ù÷üóOÁ´nÝšgžkIñ I$¼<Üñpw%9%µÌc¥ÉÃÝ�äÔ 998Üþ÷�·WÞì·..:Þžù&oÏ|“¿ÿ9Á+¯¾F‡víð©ämu›-÷ö~'%'ãâj{ö@�FCß>½yÁ"Â##éݳG‘Êh-N©LFNn.99¹–uïHLL$ðö³!IÉÉèt:«ÛlÕ'€ªg§.ÊñŠËË˕ʑÇz~,NÔrj°óÖf̵òžÑ5�–›+ …©TJ�{fܼ—V£Á±Z¡9¹|ù #†ÚnHë´Z2²²ÈÍͽo8œ‹N‹B¡ä³O>(°GÈÙÙ™Æ2bØP®³ô›eÔ­S7WÛ³Nf¤ß]ß2=#gµs™ÏÑɽA�ÑhD&“‘••EnnN™—¯¸lÅb­VCFfF¾Ï69%Ž*†œL&R©”l½>ßðM{X+CBBb¡ß³“ПíÛ³yËV¾üò+Ξ;kyø½G÷n8;«ùvÙ2ôz=õëÕåÍéopáÂ>ýôS6oÝ‚ÂA�¡C!!¿òísrrbÜØ±$%%óûö�ü¼r5·nE1mêÚ¶i]¤<ÆŒ…J¥bñ’o9w.ˆ=º[¶)r^�ø9¹¹Ì_ø¿üº×ý¨õêÕƒÞ={ðÇ�|öù—:|EáCù¤R)C ä¹ñ㉉�cÍÚuüsâ$þLŸú:•¼ó_ì¨ÕÎ;vŒ/¾ZLxxo¿5Ó2áF§Žhß®-ËXÁž}ЦM+Ô÷ �ì÷Ä´kÛ†m¿ï`ñÒo9„©€…BÁK/>ÏùóرcQQwgjr{ þN�:”hÈ €ŸŸï¾û.ÁÁÁ,[¶ �FÃkï�"Ñ9o†J½^O\\œe²'''T*±±±„††‹»»;UªÜíÝwQ{2¶Ãl.Šbòäɸ»»³lÙ²2�X(·W’vü8ªU»†µjÕªñÌØ1–ýÄ‚óÙzÛvýAl\&“‰èØxâ’ðp/½©ÉË’�O%üýXúÍ2²²²Øµg/ÑÑÑ´hÖ€!#FrüŸ“�F¼oI»s=bm›-?¯ZCzz:ÿ‡°°p6h`;0nÌhvïÝÇ ý,�[¬ÅéáîÆÁC‡ÈÌÌbñÒoîë}ýiå*RRÓøëØß\¿~�¦MšXÝf«>mqrr$((£ÑHT´í…�‹r¼âæéåéI` ?_/ý†ÌÌ,Ξ ¢ï€¡$'Ûî½,LÃÊ�È8_‰ä„LL&q÷L@%—ËQ*ˆ�‹Ãd2‘••EHèµ|“/iµ.¤í¤I÷*–õ"­qwwÃÛË‹ßwì$;;›à�P¦½9‹¨è¼¥�||¼Ù¶cz½�ëaaÌy¡åœ²ðƒ�¸‚ÉdÂU§C¡p(ò÷zÿÁCdggsåj0±±±øûú•Éñ\t:4-!;;›ßwîâÎ3²eY>¥RIXx8&“‰Ä$Û½[¶b±‡««+•*y³cçn §Îœ!9)‰Z5j Ñ¨qÑj9tä(Ùz=[¶lƒb,]SPù¬•ÁÚ÷ìß$¿¬]SþãÄ„G–D*áä‰4lP´ÙˆŒ&éé¸Z6—”œ‚Z팬à ¤œœÜ|ÃE„eË¿G«Ñ2bøÐzÜ{%&&2qòë|¸p>U â\�sAçi×ÁvÏ”ÄÁHFÀ_Ü"ïyƒÁÀÅ‹©[·.E|6­™Ë0:x<šSe?,Ö¯[MÏ^½ó½7}ÆLËì£Z�–-Ì·]*•²cûï<Þ¹h= ¹F#IIÉT*⚈A—‚‰Š�çJð5ô99h5jš4¨O�_ËÐE¹\nuý1{Ö+Ž;SÛ4L 8$”wfÏåjpU«VfÖ›oкUÞ°æ#þÅÂEyóîîî<7þF=9Âê¶�Ðk–Ùûî5xÐ@ÞŸó.-ÚväÙgžfÝú ô&½ú#†åô™³ŒsÿbÞ£G�dÖ›Ó€¼ßþ.Ý{³uÓ¯T­R´ßkeؽg/sç/";+‹Ñ£žb×î½|ñÙ'Ô¬Hëö�:d6nÆ�“ÃS'3bh^¹¬m³VŸá«%KùuÍÊc]¹f-_|µ½Þ@íZ5øeuÁûÝËÚñ¬åim&ÉðˆHÞž=— .âîáÆ+^d`ÿ'ŠT†Â:|„Å^C×$ww7’’’ð÷÷ÇA¡À`0póVÙÙÙÈå2<=<ó �ÏÈÈ$,<œU2 ³AFTT´eæ»{µi݆§G=Il\<+~ZIDd:�Ž>½{Ñöö,w±qñ¬\µ†ðˆH´Z5}{÷¦u«\¼t™_×o >!�FK�î]éܱƒÍò½þÆ ºw}œÃGÿ"77‡~Oô¡C»vvÏVùÎ_¸Èšµ¿�™™I—®�9|øO¦O›‚‡»{™”`ÿ¡Clݶ�œœ\ªVöáÍ7ò†îM›>“éÓ¦àu»GvúÌ·˜6e2^^^Vc±fÅÏ«øûxþgÄôïGÏîݸÅÊUk¹y+ 7†LíÛÏ�ž;ž5k!;[O�î]Ù÷Çf͘Ž>;Ûj}Z+Ÿµ2XûžÝ[/’ukW‹Æ˜Pn¤)'O>�±òPc‡�üÉ�C‡xkÆ›H¥E»ˆ,jcÌìw�PÕ6Ëß ( ´Ú‚ž�½Ÿ�ªݽ¦àæP½Hû Óúu«éѳ×}ïOšü_|þÙ}Û¤R);wl/óƘB¡@.—[¦µ¿ówEiŒ=,ôz=³çÎ';+»ØÏËÙãß�•¢n —hˆ`OìÿˆÊ¾`w�ÙO {´ŠÏl6sõj0Ë\Á Þ/÷ßTyE›ÍNxÔˆ/àÃÌd2sàÐ!:´k[ä†XQɘPžÄ·ïáÁ¥K—™ðüs¥žw†ÛeRL·ìN__׋zÚû{S„ÿ¦âžËĹïá!“É8{òïzÌcGصM°®ž¶ÑúË¥l·½sRL·Ð¸]FßÒöÎÂ#§YÓ&4kZðú±åI.î åÉ\îH<Ìžv|¹ßßÏ—Õ?ÿXêùJ|opCzÌîô~ÔÓô¶½£ðŸQÜ©íÅr,‚P1ÕÓô&*ë"ñ†0»Òß�£¦oeÌáUK70A(#â¡¡|‰ "¡+Ï|Iu™$:Ÿ-t{QÔÓö¦’ªà©’…ÿ¨b,ö,•H1›Ì¸¹¹•W´‚ ¢’ª6õ´%»Y–è|©Îöº“‚PÈŵ°P^$ f³9ßTê‚à¤R|…5ïo0e{“` µ;ïZêNÔ/á‰^¨x$Ež¢ØŒÉdâÂÅ ùÖ…¡â¨¯íMTöE®¦´+}‚)g¯ª8¤4*åÈ¡ôÉ+ƒk£Ȍ3‘‘899•w0BãííEtL,çΞ&-5Õò¾sÍTŒç0ÉrìÊ×!GCÜE›³6”V¨B±gÏ®"ïëææ†V§ÃÏWLÞ"‘Bª¢�®ÑÙ—HÍ�µ+�(ùqüªxc¾Y©”£„Ò%?sætyÇ <¢Ôj5NNNÅš*Zxtüû{!q6�Yóe*@eWžM�†Ò(p`)D'‚ ”¥jŽM¨¯ëß ?Ú•>ÇœE²ö<.I®˜3•¥œ ”"yQ×wA(W•o‘( ¶;y5E#j*;—^<‚ B™ª§íETÖE®g·½sâÌÑxû"½^³”#„Ò#&ð¡Â“¹d“ª¹jwz¥Ô‰šÊÎ8I\J1*A¨˜žýL©çÙð06mÚ’ï½Öí;^êÇš:}&!¡×ŠKY:úç1ú†^¯/Vºç¤)Sùö»ïȱ$g™õ´½¸[ìú¿ãjæ!>ýn99ö o/®y qìØýK-,ÿaQQÑ¥vœiÓgkßβPXùöìûƒ—_�Ì˯Næô™3¦-¬Î c«ìÖêúÛï–³s÷ž"ëA�—w‚ ¶=o’*±M±ZÊ.ø:´(ň¡hŒ&§N�%*6•Ê‘�P3пL޵~ÃF“P«�Ùµg/ç/\dêk“¬¦9f}z÷dôSOZÞ;uú /½:™Ãû÷â P°eïesAB¯ÞƒKvv6³Þ�ÍÒ¯¾@©Ìê¶àƒ�Y¹jõ}û6iܘU?Ým=È8çÍy�¾ýÑ­küýŠ”fëöí,Y²Œ›·n¡Óé:d “^y¹Lã´G u{âO«ñô)~Z“ÉDhÜFN�"MayÅÏ«Ðjµ Я#Íóά¾]z ±’8~âÛ·ï"!1gg'ÚµmC¿¾}JœoaåëÞµ Ý»váÓÏ¿,4mauVÚ±Œ9’9óæÓ¸Q£ ó˜ŒhŒ ‚P¡É\³HÒ^±;½·¢65•K1"A(ºKWBÉÖx¢G²² üsúþ~Õ‘ËKÿô;xàV­]Ç_Ç×·:¯O¶Þп/›7ÿž¯1¶mûNzõè�ƒBÁ¼°fí:Ì}��ûçK¿}ç.V¯ý“ÙÌK/>ÏÓ#óòùjÉR²2³Q9*YñÓ*rs�ìØºŸJü}üæÎÿ€7oà[½ æÍ¡~½ºtìÒƒøøxúš÷wÇö,ýê «±„„^cöœ÷¹|å •¼½ycêktî”÷ï¾EÛŽL{}‹—,#3+‹—^x–ñÏŒ-R�®ýåW5h@�ÇîÎì:ëÍiÌzsM[¶ã—5?S#0À²½°8W­]Çñã'8þÏ :wêDVf&þ}Œåß,¡Aýz„^»Î»ïÍãÂÅKx{{±hþ\š4nd³>tZ #†á›eËù`Á<›åÊÈÌä­wÞãÛÅ_Ñ´i®]»Î«¯½N‡víhÒ¸­ÛwfíÊøùùЮSWV­ø??_Z´íÈ„çÇóÊ•¼ðüxÆŒi©ë¶YûŒ¬•¯c—dHRÉR¥£õÍA£VS½zµ"}~III89:"«�ôV¦dg›i¢¢¢Yµf7nÞÄÕÅ…ÁƒúÓ ~}KV®ZCJJ ½zõàÐá£LšøÞ^^¬ýe=‡`̨‘´nÝʒ猷Þ!55•y P¯^=^™ðBÞñ¢cXµz-‘‘¸¸¸ðÌӣȻaËšµ¿r=,­VMŸ^=isO¾§ÎœåÀÁCrrè×·�w²~®ËÖëùyåj&¾4�ÀÀ¢cbøfÙrêÕ©C@€?¯¿1ƒÞ=»³gß~zõìN—Î�lÖ‹µòYc­Î¬Õµµ²ÛŠÅÙɑڲs×nž3ÚfŒ‚¦(B…fô¾Aö Å�H¤ÔtèˆNZ¹”£„¢ ¿qƒFõC¥TâåáÆÀ>Ý‘Ëder,©TŠR©¤}ÛÖH¥2œœm¦éÛ«—¯\åÖ­( ¯'a÷ž½ ¾Ý€xgÖ›\ÿr1ƒ¡Huºgï~ºwïR¤}mÅ)“É8ÄÚU?±}ÇZ¶jÎ}ú°gßLšòMš4âÏCðÆë¯1qòën­³VŸwôêуƒ‡c2™lƘ•™…Ñh¢’·7 �ծŞÛ,�?kdR)ÿœ8ɶM¿ñÑ¢ù|üégÄÇ'XÝfë3²V¾CìF•ëÉó=gS¿^ý"7ÄRÓÒÐj5¤˜nap³¹¿Ùlfù�+¨Y#€E æ1 ÿ|ÿãO¤gd°jÍ:ÚµmÍÂùsILL")9™;s’?9|(‹¿üœzõêÝ—ï¢ùóP«Õ¼3k&‹¿ü<_ãàÛï¾'0П�-`ðÀþ,]ö¹¹¹,ýv9�5üù`Á\ž}f,¿mØDìíc!!¡¼5sÏŽˆ�›-é cÐ0šÌ¸ºº —Ë©Z¥ óÞ{×Òø“J$\ aöÛ3ÿÌÓlÜ´™ÔÔ4›õb­|ÖX«3kum­ìE‰¥Y“&œ?sYßK4ÆA¨°¤né$9_¶;}-‡NÔTv*ň¡èL&Ùz7£Ø°ußÍ…+!ezÌ#GŽòÙ'S´glÔj5�:´gÛöüuì8Z�†F éxcŸ�«‹Ž¶mZáïïǹóçÐ8«ILLbþÜ÷ðòôD¥R!»ÝÝðËFŽŽR©¤{×.ÜŠŠ²«¬wDEEs=,Œ‰/¿„³“ݺ>ޝouNž>>\ ³Z/e¡(u]ܲ߫²�zƒÞr3 ¼‰aŠ‚ TXÆJ7É$Á®´®òjÔR=^Ê BÑ�FÌf3r¹Œ}º“‘™Éá¿NPÙÛ ŸJeó¬Âÿû€Ï>ý°Èi ìǧŸÉ Ï�gÛöôïß·Èi=ï¹0tuq%5%ïâF"‘R­ZU4Í}iV¯û…ZIlLƒž*UJÖsŸ�€N£C¡¸{IãîæF|âÝßwwË+ä LFc‘òNÏÈ@­V—(¾{9Þî­”H%¨TJd2&“‰ÔÔTŒ¹¹4nÖ*ßþÑ1yϾX«Ï{©ÕÒÓÒ ’·ÍX^|îYÆŒÉñN²gßô0˜¿[J½ºum¦½·>uZ©©)V·ÙúŒŠZ¾úº>Dé/¯¿n3F“ɈTš×ï�nŠÅàŽ"©ð ©©i89:#—ßmj4RÓÒÈÈÌBéà`Ù¦R©P*í[fåŽÌÌ,L&#“¦LÍ÷~Rr2J¥gGÇ|±ü›N{·¾ä2æ"ôŠöê‘7ôðêÕNŸ=Çœ÷ðÚ¤W¨^-¯ÇQ«ÑZöurt"33Ój½”…¢Ôµ=e¿—ÊÑ‘ì¬,p-ÿ‰½DcL„ Iî•F¬ÓE°sA€Cÿ-== �Özƒæ^ŽŽŽtêØžN·‡Tnܼ•zuë"•Éȹ=ì+''÷¾‹îÄÄDomKJNF§ÓYÝV”Ϩ(åóVÖ¢¶úñ"7ƤRY¾a›qгTó(|±w­VCFf¹¹¹–ç:“SRqqÑá¨RaÈÉÁd2!•JÉÖëÑ볋Ga\tZ %Ÿ}ò‰$ß¶„„D2²²òÅRZ¨_¿.õëç5¼ÿ:ö·¥1voc>=#g''«õRÊ¢®ÿ-;+Ërs¤¼‰aŠ‚ TH�²Ìö !ð”àЦ”#„â«VŇ —¯’““KBbññ‰¸»–ÍŒ½d2}úôä£O>ËšT„^•;~^µ†ôôtþ>þaaá4l`}xcRr2r™âãøiåj’’S,Ïž899„Ñh$*:Æf >>•ð÷cé7ËÈÊÊbמ½DGGÓ¢YÓ"—£0�\ ¶}âòòô$0П¯—~CffgÏÑwÀP’‹1Œ*8$'G'¼<=mî»wß~}’�Ðkäææyãç/\´<�åáîÆÁC‡ÈÌÌbñÒoî{¶æ§•«HIMã¯csýúuš6ibu[I?£{¿®Yõ¨¤z¬Hé”J%ÙÙw/â3M‰d¹>[§««+•*y³cçn §Îœ!9)‰Z5j Ñ¨qÑj9tä(Ùz=[¶lIÑ/£•J%aáá˜L&“ònbèt:||¼Ù¶cz½�ëaaÌy!¸»»áíåÅï;v’��MpH(ÓÞœU¤…9söï/ü�¨¨hŒF#ñññ„GDàyÏwæ�ÈÈÌâò•+ÄÄÄ`µ^¬•Ï^eQ×÷º…ÒA‰‹®bü‹ž1A*™w qŽíNàеÔö‰ ”µFõêp:è›vìÁÙÑ‰Æ ëâZFw“KbðÀþ 2‚óîöR…„^³ÌjpèÐf½ûƒ äý9ïb4™¨[§6ýà70sÆTÜÝ\ ÊÞ¢ÎcµéÜ©§J•*Ì�ýWCBxaÂDVÿücÇŒfáGŸðÞû ©]«¿¬^i3–�?XÈ;³çòãO«¨Zµ2Ÿ}òa© /ìÖµ3{öþÁ}zikqÞé…(ÌÇ,äíÙsùáÇ•¸{¸ñÊ„‹Õó°kÏ:uìPhÏÚ½º<Þ‰ó.ðÜ„—ILLÄEçBÏÝ9b8_z‘¹ó±ô›ï=ê)ªU©bé)ðõõ¥÷0ää0kæù¾Ó…m+ÉgôïïÄÌщζý<±V£&5--_Ï]Œü Z {ÖíeÏÞ½–÷ôïGÏîÝÿÌV®ZËÞ?àááÆóÏŽ³Zr ieÛk² BY�1á¡TQVUŠ'Ëí¹æ\Û; ºCcüZ—rD‚�Gü¦BÅSS݉gûz·rÍ9¤k¯–rD‚P|¢1&~¢D±Xó÷ñèÞû ÚvêÊ¢�>É·-ôÚuF�OãæmèÙw€¥ž^½†£ÆäÛwöÜùÌ|û=æ-ø€º ›R·aÓûzÆ®‡…ñ̳/Ò´e;º÷~‚�›6Û<^Qlß¹‹ö�»Ñ¶SW~^½Öò¾µï„µÏÈÚ6kŸC‹¶Y·~=�ºö¤EÛŽ|ÿãŠBc®åÜ¥L €Þ` ,<œK—¯Brr²e?½^Ïõ°0.]¾BHH(iié$).!Ugñú38|ô(3ßz—×ߘÁž}ûˆˆŒäåW'ó×±cûûo^›ú“¦LåêÕ¼sØÚ_Öóò«“yùÕÉ÷õ8…„^ã½¹ó™2u:»öìå­wç À´é3‰½ýßÓg¾eù{Ûölظ™­Û·3eÚt&M™Fbb^Þ•«ÁÌy!¯N™Æ¼‹�ˆ´ä1ã­w¸uëó,äåW'óõÒo-Û¬ÅÍÇŸ~ÎkS§3gÞ‚Îß�ḰzJN4ÆA(7f¯8Ò‰µ½cZQEÙ¨”#„²sìÄš6¨‡\V6³¶I¥R”J%íÛ¶F*•áääh3MVfF£‰JÞÞ8(\ÄÿÏ’OaÇ+Š³ç‚Øºé7>^´€�?ùqqñ6ÓXûŒ Ûfës�I¥ìÛw€�ë×ñéG‹øüËÅ †�ïëÜœZΈŒ¼�““µkÕ¤jÕ*DÇÄZÒݸqÓ²ÍËÛ‹7o’”s£G4R‰„³gƒ˜5óMž?–-[·“››KõjÕXüåç4nÔˆaưøËÏYüåçÔ¯_€'‡eñ—ŸS¯^½ûâZµfíÚ¶fáü¹$&&‘”œLQæyöòòää©ÓDÝŠâ½wÞâãàâ¢à·�›èÜ©Ÿ~¸�:ucÃ=�ðEóç¡V«ygÖLù9¯LxÁ²­°8Íf3Ë\AÍ,Z0�ýŸàû"ýö÷·°zJN4ÆA(‰™Lu„]i•gijbÂÈͨªTö.Óã9r”Ï>ùˆØ˜Xôz½Íý=<Ü™4ñ%FŒÃó^áû?é¢ûŽqÏŒÅÅEG›Ö­¨È™sAV·EEEs=,Œ‰/¿„³“ݺ>ޝouNž<€ÆYMbbó羇—§'*• Y5^Ë"–ظ8nÝŠbÌèÑ8;91á…ç,Û""oÁ¤‰/ãääH·®�S¥Š'NœÂÍÕ…¶­[±yëVþ8p�FM‹æÍ¬/<<‚7o0áùçQ;;Ó°A}Ž9€oõjV�WcŸ�«‹Ž¶mZáïïǹóö¯i�­Ï�ÿ³wßñQ”ùÇ?3³½¥÷ UZ�"D)*ˆý,¨XÏrv±{zöö;½³Þé) 6EÅ"*‚ HG©�tÒ6Éî&ÛlXIv³Ë01n‘ ´ŸÍlc`¿æOÌcíùÿ ¤ÅýóÙ'Û\æê+.gæ…ð˯«ùfÉ·L�~&oü÷eúö ÿ3–œ´?å2ÎGmmMÈm••Ä™ãP«÷ß~$%&RQèQ“$™N�²1›Ímn´Ú£-5ÖLFc°N½^�F˜Ý·¶¶¯ÇÃÀÁÚ”)- ¤>N;ý4žû׋üíºkYðÉB¦�~jØãU[­Ä™-MÎaŸpÇ çÀ¥â¨­© ±wôÂ}Ðô»¤V©ñ…XS2[?€xO/eK‹ëŒz<Ei²M¥Rðx<ìõo"¥o`‰œà6EÁc íŽz´ *U �×éthµº6••�INNF¯oÞ˽ì‡YüíRj¬5xÅíHÑëõŒ3б�ip >ù”¾}ú + îÆT$·ÛÓäÆ  ªªŠn]s�ÆÀ ..ä¶ä¤$¬µ5¸Ün4j5{Ë+HKM –ÓéÏÒ$íѳŌ£Þ�ÇãA¥RQWW‡Ëè¡LMMA§Ó³jå-®�9qÂxîða–ÿ´’+~æž;ï{¼”ädjêj›œÃ>áŽÎ�c¬ª­Vâ“[„ûNDª-ŸC¤zXFò“w>~¿¿Y@¦R©ðz½M¶y<ÔªÀ±“úÄvqw½N‡ËíÆçó!Ë2 N'NgCp»¤ÈxƒK�Ç‹ÃQߤ¼FÓôs¨ªªæƒùqõ×еk.;wåóö¼w©�‹»ÃüîXkjƒi‘BûiŠ‚ vJ‚«&º‰;ÒÕ½é¬ã Bû)Û[Njr"r Oé�´ÅK–rÆÙç³}ÇN<……lÜ´™Î�;�œ”Ȳï¿Çá¨çÅ—_Áï÷7)?çí¹ÔÔÖ±båÏìÚµ‹¼AƒBnËÈH§kn/¿òêëëùê›Å”––2dpÛ° z6lØ€×륤´ì�ÎýPÛÒ’´ÔT’’’˜÷ÞûØìv^|å?ÁþÔ”ºuËå…—_Áá¨gÝú œ:ýl¬�c£4 'Ÿt�=ù4ýúõ£SvvØãeee’›“Ë/½‚ÍfcÕê5œ8z<;vî {¼pÞšû6›�Ÿù•üüÝôï×ÿ�õµ´­=>‡!]¦Buåååø|>¿ÿ±§Ó‰Z­F«ÑP^^�Ï磶¶·ÛƒÑh ¡‡%Áõ±f6›ˆ·XøþÇå48�,\øHûo¿-f37nÂét±è‹/ñ‡¨k»ÝŽ¢($$&R[[Ç·ß-Ãf³Óа?ÈÓjµäïÞ�Ï磪:|:aBBééi|ñå׸\.Ö¬]‹µºšÝ»GsÚBD0&ÂaçM.£žèrÍ;©¢¢m)‚p4°ÛZH3:L?–qcFqÅ5×’7lΜÅ�sÁyçpý_¯æ�9s3~"~¿ŸNYYÁ^€.]º0å´éÜpómÜ}×í$ð½¥m’$ñô�ñÓŠŸ9v"/¼ô ÿ|æIL&S›Ú{ÉÌ‹xì©gÈ6’o¹õ�ÎýPÛÒZ�Þwo¼ù6ã&œ‚Á '!!¯/Ðóñô�ñ˯k5n"·Ýy7W^~i“ž‡3¦ŸÆÎ�;™1ý´à{Ûwì Τøý÷?r÷ý§Oÿ<î}à!ž{ö)~þe#ÆNàÎ{îãö[o öH†;^k¼>}z÷äô3Ïá–Ûïä®;o%)1Ð3î;ê3ji[{|jIÏY#®Âna…EŤ§¥¢Õz:³³³°Ùíü±u{Ë+è”�ì=ÔX¼øS"›Xª¤¤48Cá¦M›˜3w×Þp#oÍ ÌByÞygóÕ×ßp×=÷c¶˜1èõ¨Ô�5ÑN›:™Åß~Ç�wß‹?)I‰Áž²ÖdggÑïø¾<üèãüû¥W8ù¤ ˜LFþõâËÁ}&LÇ-àÆ[ïà?ÿ}-l;%IbÖ¥3ÙòûÜ~ç½|¾èK®¼ü2t:ñû¶½IóÞz£-A¸ ¼>6›½É/üƒ5y:Ô:�Žì¬¬CmžÐ’ÚKÝñ˰Jù—5ÊILŠ›M‚Ò)ö „Û°ejµ•J…Z­¾T*U“×¾õÇdYnö’$©É hqÌ‘0|Ô8Þ}ûMrrºD´M‡JW>ßC�§<â²Irw·�‚ßÛÑ<~¿Ÿ­[·ñÚoòÄ£5?Ë‘%ÆŒ NCCÝ{ôލÌö­[Z|ßëõòëª5¬X¹’]ùùdfdн{7F NffË~ø‘?Z4¤øü~~[ó7oaã¦M$'%sÜqÝ1|éé‡>{›ßïÇ1I³’3*‚�˜×ëeÃÆMÁmŠ¢`1›1 $&$4›¹¬³fPÈ@Ìçó5¹iAŽMIšr�ÃX_óYÄe+}ÛIL…±Yœ}õšßøpÁ'ÔÖÖ’”˜ÈÌ‹/¿§„ Œ ’ÑhŒI=‹}Éâ¥K9yâIŒ?ކúÖ­[GIi)™™19Æ¡jp:yç�÷ÙüûNžxô§¾¾ž5k×RV^“`ìëoóó/¿rÿ½wr]õ–æwdff`0ÀïÇéraµZ±ZkèÒ¹s“ÁË�´¡ÇÌ¼ì †œ0˜o¸î�Û)Bh+ü.ªm‚p¸DŒØ�»0›`lpÞ 牱ÎBËD0&­ðx<|¶è .»ô†|ø°!øüGOvïÒ¥Ë(*.âÞ»î$.Î|ø°¡GU;T)u”j¶5{_¯Ócj  M@|\<………”••Ñ©S`{'õ ²ÕgsA„?±ÃPr CÙåø%â²åòº&�·BÌ&(´/å¬3Ïøû‘n„ ´•ßïÇår£1 ´ªºšôŒÈžf••“˜˜Øä=ŸÏÏ¢/¾ä¸îÝéÚ8zŸ}é»÷ìaíoë@‚÷?˜ß8 ‘›ÜÜÜà`àŠÊJ|²�wÞ}ŸµëÖãv{‚ƒ…ŸxêY**+éÕ³Gã1}̾û^**«èw|`M"¯×ËwÞƒV£m6þÂjµòÔ3ÿÇ—_ ZZj'Àok×ñÁü�XðñB*+«ÈÎÎB§Óár¹¹pæedffðÕ7‹™÷Î{lÜ´™ôôtâãù÷‹/³ðÓÏ)¯¨`þG øþÇ™2yŸ÷þü�xç½÷Ùµ+ŸÔ””àÔÖ/¾ò*~¿ŸUkÖðæœ·Q«Ttí§F½3Ø&¿ßÏÞ½å$&$ Õj‚ï˲„F«¡°°ˆøøxT*} Sس®–'žz†‡~„Õk~#Îb¡KçÎØÒ2;³mÛv¾[ö=O<õ n·›±cFGð-„ØÚ[Q ¶o\XkcÃZÖÒ Žž1c‚p´“�púläGŒùpcÖ$#WE?ž ´…˜MQZ¡R)\tÑü÷õÿ±ðÓÏÙ[Þò àú†$IbÆôiœ9c:sÞžËoëÖ`­©áï=~¸ü²K˜0~, ?ý”OÒ&†ÂO+Vàk\8±¸¤„={ XþÓ <�³SSXTDÏ^=𻤴 —ËEN—ЃäW¯YËœ·ç2rÄ®ºr.·‹W^}-x € 6Òïø¾\qùe$&&ò‹/ãt¹8ç¬3™1}]»æòô“�1ûöÀ X{÷–sÿ‘Ó¥ ×^s5ݺvå‘Çž ²²*Xç³ÿ|¯ÇËEþ…ã‡v§V¿£­—?8ƒ“Ëå"AÉ¢lƒŸ“'ŸÊÀýyñßÏsâ°aL›q6‹¿]ŠN«eå�Ë3z×^s+\Æe—Îló±A„Ž)Ç0”$Mç¨ÊVkþ@28cÜ"AhJ¤) B£GŽ kn_|ù5»éV¦N™Ì¤S&6YŒ2>>ŽÓ¦N þ½ÿ~äïÊgÈà<–ÿ´‚Ô”dÎ?ïœàdjµšG{‚qcÇгg^zõ?—Ð);‹­[·Ñ·Oo6mÞBAa¹9]ø}ëVúöéÝâ˜ÕUUh4 †ÐÓf´àcfœ1�Áy�4?�VÇ-·Ï¦¸¸„ôôt† 9�¼A�í*Eá˯¾¦²²ŠÌŒtâããÐiµMfœüqùOœ8|8O@çNÙ|¼ðS6où�Ñ£F0uò$Μ1¥k1»¥’6_{Y’Ðétx<:iòxò™9Ü}çÁ kÐÀTVUñäÓÏ2qÂxzöì�Ùl&55•ž=›®‚ ±'N�N®a8•®=—­ñ’šR»£ æ¡-D0&adefrŬK™:e2?._Î�7߯m·ÜÄ ­,H™–’ŠËí`×®|ú÷ë×dVÀ}é„åô8®;}z÷bë¶mtÊÎâ—U«™:e2‰‰‰lÛ¾�œ.�ùå—_9ñÄ[LMŠOˆÇårQ__˜£v»ƒ;wòâK¯òâK¯6ÙV[[ Æ´o}—{ζ+?Ÿ_W­fÉÒ¥MÞ¯®®þY§ßŸNê°ìnµ®–øý~Ð*z’ü½Y¼äV®½æª&ûä ÈC?ŠÕZÓ¦µsA„cOŽi(ëk?Åå«�¸l�q'&D0&´Œ Bef¤sîÙga4ykî¼`/R(~¿Zß!7Ž9ñÄaüºj5ƒôgíÚu\sÕ8øî‡8 ?7mn5å.=-Hí)( Ž;;˜Z˜�ðª+fÑ£ÇqM¶%Ä'„=‡Ö ¦LžÄIÆ5yß o*i5TÈDTCC 5$SéOš8·ƒÒ}—e1†FAhY¶®?¹†¡üa[qÙ2ßF,)ð•GÿûRBcÆ!§³y®¸ÁhNÎNN—.lÚ´98&  ¸¸�FCJj ½zöbíÚu|ÿÃrF F|\Ý»wgýú |ÿÃ�wÜqdd´<�~bbž/ü”:›­Ùv¿ß�F£fà€þ48�dge5y�-÷¦µÄëõ6ù{§Î�(/¯ +3³I�‰‰Íayã*ðãkö~k|>å$&&0 }F£‘1£GñÛÚuMö[¿a#£G�ÄbÙ?‹äÁíA„ãШÊùñá6—Ÿ5‚°Ÿè„VXkj¸÷þ™xÒ:ee¢ÕéØSPÀüùqŬKÛ��y"‹¾ø’æ/`È�<êlvÞyç=Î?ïâgÌÊʤk×\Þ}ÿ®¿î¯¤¤$Ó§w/渀Ë.¹8äbËO>‰âÒR{â)&NOjJ ŽúzÖ®[ÏðaCéßïxN?íTžyöŸõºätÁá°³aãfΚqF›®…N¯gWþnÖm؈ÏëaÀ€Œ¯�mÛw�7h`³µÍê Å!ëohh@–eüøq¹\X­5x<vMgí`n»å&λàbRR’é߯¿ÿþ�>þ$¾ÿN°ž¤ÄDV¬ü™+F’$†‹î—¯ ÄJiY›·mÃQïÄd4Ò¿ooz÷èv¤›%Çœ\ãp2t½)iØqY»® ½Ú¡U‚ zÆ¡U½�Ë/»ku5Ÿ-ú‚9oÏ¥¸¸„;n¿•#NlS‰ <øÀ}8� ¼üêùöÛ¥LŸv“'�ÜG–$F6Öw\÷ÀMš$I $z÷ ý @¯Óqåå—qÆ´ÓÙ½»€7æ¼Íâ%ß’œœLrr2}ûôæöÛnaÓ–-üßsϳð³Eh4j\®¶Í5hà@†ÊÜyï°nýFêâããùûý÷⨯ç•ÿ¼Æ¼wߣ¢²Š•$ÕÊ®�õ³mûvvíʧ¶¶³ÉD·®¹ôJ…V Œ_=j$¾ÿ?­XÉսޗÿÄÂó9qø°`=W^1 —ËÅÃ�>ÎòŸV´éÜ¡=ý¼f-½{ÇY§MbäÐ6›�„“5lß±ƒƒNˆ¨Þu¿­¢{7ñ´º=HÝó)°|q9ƒÏ)qw�¤Ê ¿³ …6lÙÆÒVÒ¯o/r:gcwÔóýÊUœuúd z=*• •Jrý1±Î˜ ÄÎ^ç6>)¾»·*üÎé꜊¼«{;´J8Ö‰4E¡Ã1è ¬ûmUÄe„öÑ`lûtöJ×ô�˜ð§7$¯??ýú¿þ¶µFÍèáCÐ4Nª#Âᕪ=ŽlCþ¨û.â²]!&D0&ĞƄ'3³åÉ.„ÃOIp`U"›Ò~ŸtµÈÏþÜ|>+W¯cP¿¾ätÎÆf·óÓªµ¤¥¦g _� 1—¥ëU0VÅ,ñCðY�±o”pLcÆAh7¾„*Ü8".§•�"þôêlv<�³3P™ä¤Dâ,föVTé¦ Â1+Kß�bЏœËoÇWÞ-Žu"¡Ý8�¥Q•KSõ$AéãÖÂáeÐëñz½•àóù¨¬²RUe%Þ"zÅáHIÒä�©;>ª²õú¢·FDš¢ íD6;©SlººwŒ[#‡ŸZ­bø ƒØ²u;«×oÂd42°_â�tÓᘖ¥ïÇNûʈËYå]˜LƒñÙtíÐ*áX%‚1AÚGRõX#.¦HjÒU":†ôÔd²3ÓQ«ÕÁ— GV–®?*I�ÇœÃW…/¡lÙíÔ2áX$ÒAh®(SSU=IQ‹eA„ö‘®ëIF”©Š "UQˆ1Ñ3&t8N§“Úº:d9°n�pø)vâv{".kôdQÚ°·Z%Ññz½x½^’“1Ä2‚ÐdëûQPÿ[ÄåjT»1èóðÕ‹^n!6D0&t855µèõz4ZM“…R…ÃGé\�UcEA‰¸lŽa j1¦F8:øý~ü> NÊ+*éÒYc‚ÐdêúEU®ÎWJzb¥Å¸E±JcB‡£¨ThµZTªÈ!6|–r$"‚SUÇ‘¥í�$‰ÏN8ºH’„£¾þH7C„ÉÖO†®% ›#.ë4£AcBlˆ.¡Ã‘e™(â!F$µ›®0ª²iªž"ŽN’„¢ˆï¦ t’¤�帱u>’Úã Ç*Œ ’HM,ÇY¿a#}úç5{½üêbRÿæõkxîÙ§bRבp¸>‡XÉÒ÷#^�UYŸÉãÖÇ"Œ ‚3nCt7šiªž$±¶˜Ð±øü~V¬ú�n¹]8cê)ôêÑ�Vü‚Ïß~©Mk×oà„¼<êëë©­«#=-­MÛZ²aã&þþàÃŒ5‚åË–0ëÒ‹ùdág,øda0 yýÍ9,üìsúôÏãëo³}ÇNúôÏãžûäÌsÿ€†{JBmƒ@ 3ý¬sé?x(c'Nâý? n»pæ,&�:�Ï}Á˜ñ§Ð/o(‹—,¥¦¶Ž>ýó(--åí¹óèÓ?�igž,·ió.ºärò†Žää)§±ü§•ÁmïÍÿ�!#Æ0ᔩ¬þm]D×úÒ™³yýšàëš«®dÓæÍôéŸÇ³Ïý‹s/¸˜ACFpÃM·àñx¸ù¶Ùô0˜ÊÊÊ`cOšÄgŸÀ5×ÿ-ØÜ3ÖÚ¹‡ºf¡ÚòÀC�pÉåWqÊ©Ó8÷‚‹xþß/r°‘|ºhQÈkªÎpŸÃ�?­`Ú™ç0à„áŒ?e o¼õvD×»½•Ĩ{ÇœºŠ·F8‰`L„˜iÐD7yGšJÌ¢(t<õõ x}^r;g£Rr;g£V©¨h‡Þ±�Å}M IDAT?^HŸþyüúë*¦Ÿyƒ‡�¤¨°ˆÉ§N¹-”=È’ŒA¯gÖ¥—°æ—åœwöY!Ëiµ¾[ö=Ï=ûçŸ{oÏ�ÇO+~¹Íf³qÅ5×’œœÌK—0ýôÓøûƒóÛÚõtîÜ™‚‚þûú›¼úò¿YýórÆŒIœÅÌK¿ࢠ/`óú5,ü(Ðh³Û¹òšëp{Ü,ùzS'OâÚ¿ÝDyy•üã‘Çé×·/¾ÿ7nŠþh¤ÑhøôóE<ýÄc\}Õ,–|ûßÿ¸œS§LÆï÷óÝ÷?°nýÊËË™:y/ÿûy6¯_Ób½­�{¨kª-*•Šuë6pÿ=w±qãf�`à �,úâ«�×,T�¡>€ÙwÝK�ãzðëOßsÕ³xá¥WÉÏ�.­=Ö¢7æPÂ÷. B8"!&ds69ò)íU’š4U�vh‘ Yz�™ü‚"|>Åe{±9ê©wÄ~ñè3ΘƢ… 8p›×¯áö[oæ†ë®áËÏ? ¹-”ñcÇÒ£GÞ˜ó£ÆMä¦[îàÇŸV´¹MÆ�£Sv6g͘ÀË—‡Üöý�˱ÕÙ˜vú©ÄYÌ̘~:_}¸Á��·pÉE¡WÏhÔj4š@p×ZgãÊ•¿`µZ™>í4âã¸ì’™¸].>ÿâ Vþò >¯—ÓN�LB|3Î8½ÍçðÆœ·š¤)—@8iü8:wÊfÔˆäçç3fÌ( F#ß.]À7K¾à´©S«µsuÍBµ ;;‹¾}úp|ß¾dfdPc­ yÍÂÕªÓ×ãõ°yóf,ü”Ñ#GðëOß““Ó%ü…> ²t}QIšˆËÕø �Mb1xáШŽtA8º—–ÐPïÄf³…ÜÏØÍ†;³2ä>-18ÓY½n+°5Êþ9%$$g6“Û5÷H7Eh'²,3lð6lÙʺ�[èÜ)‹ø8 ²ª}�^·qcðæzãæ-œ1í´6mk�Á gþ»o³ô»e,ûa9K¿[Æ×‹sÇm7“7hPØòI‰ ÄÅÅP]m ¹­¼"�òo ¼giüUU 'Q"|ó^ +Q@•5PöáGçáG¾_XTŒ¢R5iC||BØs:Ð¥3/æŽÛnnòÞö;ë Ô©V«p¹\hÔjÆ�÷K¿Ãår±té2@ffFØcµvîá®YkmÐëuÈrà™¼F£A’$|>_Èk®ÎÖ>€‡¸�ÇŸz†z€#NäùgŸÆ`8ò³èÆk²IÖv¥´á÷ˆÊyüN°Ô�íÈŸƒðç%‚1AZU\Z>`lI˜ñ%JŸ|Ê †ˆ�ÑÍ<€ÁiyÑ6ñ¨±zÍÎ:÷/‘•Yõ3»òw“{”<b/-5™¬ÌtÔj5Š¢°ðË%˜M‘ÿœ„sã-·óÍâ%Ì�÷_|ñ%9ÿ<***ZÝvßݳ[­ó©gþÉ‚…Ÿ²ø‹Ï8yâIØìvÆ�4‰E_~ÅÐ!C€ý7áÖššfå«­�÷¬�ÿOHˆ¹--5¥q[ h«i¬3µñý}LFc®H@VF`b†‡þ~gŸ9£É¶Ï}Ñä8{Ë÷¶¹Þh�:eŸ/ú‚ù >fW~>wßuGDå>÷¶^³H„ºfû‚ÍhL:y"“NžÈŽ�»XðÉB^ÿß›,úòËfÇ8RR¢ÆÜú*RÛ¡E±B¤) ‚Ъ†z'ÙÙÙ˜Lư/)Þ†JQ"~e{��ÿ§��^ƒ¥®®îÈ~ÈB»úö‡äár»Y·q f“‰x‹%æÇyîÙ§8`Ÿ}ò¿¬ø�œ.]ؼ~ ÷Ý=;ä¶P†œ�‡µºšgþù<G=;vìÄérÑ«g/232PT*~\¾’ªj+K¾ý®Yù¥ß-£¤¤”�~ LikmÛ¨‘#±X,,ül5µu¼?ÿCN�29ìù[,f•Š]»vát:±Zkðù| ÎHJj |¸€ÊÊJ>þäSN6’ŸVṵ̈!C�…Ï}EeU5Ÿ~º¨­—;j#GŒÀl1óòËÿEQ©˜:9ü¹…r(׬5¡®Y8­}%%¥Œ oÌy‹œ.�1|8Æëö–¤Ž.K¡^ÝþA¼Ð±‰`L„V…KMÜGRùp¨¢Èœ¤ê8½Bþ(þ;pf5¡ãé×»'[þØÎÇ‹SY]Èá'´Ëq<E%ÅtÍÍaÍokéÛ·O›¶…2nì}ø!Vþü 'ŽÏ 7ÝÊŒéÓ˜}û-ÄÇÇqõװuÛV.™u%S&Ÿ€Ïï –Ðÿx.¿ê¯Ì{÷}.¾èBFŽr›Éhäåž§®®–ñ'óÃËyòñGèÝ+ü?Z­–Ûo½‰M›·�7l$§N?G}=:�Ž—ÿý<Š¢pò”i<ÿÂK\ý_qâ0RR’¹ïžÙ¬_¿žsο�Ñ£Á¢ÛãiÓõ9xÌØ¬+ÿ¶ŒZ­b¸ñTTV0|èäì[ã¬Oÿ@–À7‹—´©ÎC¹f­ uÍÂiísÈÈHçòË/á­¹ï2hè‰ÜyÏ}\qùeL>åä¨Ûk)Ú®Q•³ÉÅH*_ø¡Ò¼·Þˇ ^Ÿ›ÍNBc¾zKª­5˜LFYè?Ñ5&‚pôHÑv‹ªœ])�qK„c‰èŽy…%{q»ÝÁ¿«U*´5‹ ­&òE ý@¸þ§Ë…µ¦§Ó…P« =“ Y–¨¬²¢¨â-æˆ�8TYk¨¶RÕqnÜõ;ñJ.tZ-²Ü¶Þ5Iªö$ñ€ž±ššZ|¾ýƒº}>?5Öââb;® ¨¤ µZEjrÒ!Õ³}×TŠBN笵LáÏ%E›‹‚/îð; Ö_H’©ŸM×N-:2Ñ3&€V«!%)�”¤LFn¯—’² lvGDõTVY), ý„ÌåvSº·¯×‹Ål"!Þ‚V£ÁfÛ,·×‹×sôÎÎä÷ùQ«Ud¦§’ÙÓ€J¸†R1€$Ug,JøÅNÿLöõ‚ÕÙlx<žfãÃ<µuuÁýB-�ÚV.··Û{Èõ‚ ëâÕÙQͪèö7„(ˆž1AÔŠ‚ɸ!Ö8‹‰Š*+•Õ5èu:%vÏ-öz@"=-ù€1C~¿?8KZzÊ¡õr´7Ÿß�"+èuZˆ·G5se¢ºc¥(Âþ1cFƒcˆ°c„í“+z²Ab&YוRç—së*Qˆ~±máØ%‚1Ah�$I$ÆÇawÔSg³Ht¹ÜTÕÔâtºP£^O|œI’(*Ý‹ËHmص§Y’èÒ)³¥Êñûýø|>dEirÌ}ŠËÊQ+ )ɉ—–ãt¹šµ/§±n·ÇKUu N'Š¢`1°˜M±¾$Møü>äÆÌ©�n�¬D¥ƒ¥(B“4ÅXðù|lü};ɉñ¸=löztZ i)ô�t˜í»ö Q«ILˆcçîBÒS“‚)‹•ÕVŠJöÒµK6&£›ÝAYy% N´Z é©ÉMB‚ ëR4Ñ­7Ö -ÇH¯·F8ˆ4EAh…¢Èh4j\�ãÉÜ/Åe借äÄ,&#µ6;å•Õ$'ÆcÐë�e™ŒÔdÒR“[¬×d4 Ë2%eåÔÚìø|¡oà“ãÈHM™¤ÄÀ¡q�Á–Ïç§´¬?>R’mª²ÖRg‹,½2R~¯·ÇMiå^JëwÐàt5Õ 0‹fÑ綨³ÙÑj4d¦¾S;w6[˜Öd4�”OYy N\.7%e$%Äa2¨oh`çîBô:-�²30èuìÚSDƒÓóë ‚ðgíôöbñg!Z"„YÁ㠌ǩ©­C‘eÒ’“0tX̦`ï™ÛíA«Ñ RdIB§Ó¢Ó¶<ù‡J¥��‘†Ùh¤ÚZKAq)5uu­eZ�&PŸN‹Ý^�Z­&®±§ÎæpàõùHMJ ×a1Ñi5�u‹”¤HÈ’„1Ã_oC’¤@@ÖÆž!“œLRG Æ ÖÖW[$&Ä“žšLB|9�³gg¬i¶_FZ2j•Š‚âR ŠKQ) i�´™½Õô:2ÓS±˜ŒÁ÷­5bœƒ Â>)š®XTm_Ü}Ÿ:or¼­Z$tt"MQBðú¼¨S �.:�¦I:¡N§iÜæF­nû�“,KÄÇ™±˜MØìv¬µuÔÙd¤¦´:>­Öf§Áé$〱fn—¿ßÏîÂâ&ûªHli�iprv15Š‚JQ¨oðáõx‘ÛpÕ�ÑÈÆvmã‘ùX°È×Sd­FCCCó-Y–错Ǝ݅tËéL'mhpât¹X¿yk“2n·§Y=BìÔÔÙX»a36‡‹ÉÌ ƒú“Ó9ûH7K„Vhɺ®ÔÚ"_²Åk´BElgÌ:>Œ B+|>n—ƒ¥qªÚï³7ÓÑ®Ý+˳ ½^Oaq)6»�¸¦³÷x¼T[k±˜LMzÜ$YB–eÒ’›.(,¦Å„ÝÚý½3mFåŽ×+¶eÃüø[ýÒÉŠø,hº‹¬È˜Œ†fß•JühO¿¬^G—NYôíÕƒÊj+ËYMfFš¸î‚pKV粓ÈviªÅâÏBÄD𢠴¢ÊZ ’„ɘà@£UÓàt5 8œ�ãm4uð½pã€ZJGTd I’Z-YYmE–$â›>qÓ¨Õø|>•*˜Ê¨ÓiѶ’"+ûÒ=š@Š›ßïÇç÷{aÂIè`‹=ïqšbÑ›×ëÅårcÐk[<~AQz½­FMAQYðû¦×jq»Ý�†&¯öþ®ËìŽzõõô:®k =93�8³™’ÒèIáðHŽrñg·ª6Æ-ŽâÑœ X×Ëæ¨¬åõP_HéJJŒ¦üÅ›ÍÙëÙ[Q�ÙdÀëõQe­Ál2¢n|Ê-Ë2^¯�:›¿ß�Ùll–ˆVYmÅércÔëP©Uø|�ýYÆÔÂtè6{=Žúâ-æàd"ÄŒ=ÖÚ:ö–Wg1!Ë2.—Y–±˜Û' Ðâ2tz-²§/^Ü/’$¡¨Ú–Ùá{n´/¸r8ê댵@­V£×ë‚%Ú¢¦¶�Z�$Aye5Š,“ßl¿½å•8].ŽëÚ¯×ËŽüÊÊ+ÈHK!%9�êµì.(&!ÞÒ¸šƒÄ„8 z*E¡Á颦Μ F84õ  z\n/ZÌ„1#°˜MØõGºi‚ „�¢�n�ªéx øB{Á˜ N§‹rg˜`C«Ñ�‘–ŒV³¿×@­V‘‘–Lµµ6pC¬(Ä™MÄYö߸šLFꨲ֠ը7¹(s`FG}›'¸^WœÅÜl_€Z[`@°µ¶kíþɲ3ÒmJM¦ÊZCµµŸ?°³ÅÔ~¿$ ã-Xíµ8ÝåxýEF­R·i”^NÀ¢¤·[ûލÆCƒ^‡Íîh6ä,Ë�µÙöõ®F0Ƭto9�ƒAO÷ÜÎÍÆÖ74°·¢Š´Ôä`*kJRå•ÕXÌ&Œ=Ýr:Qº·‚‚¢R$Yn’òšžšÌžâR ‹K1uÏEiçq‡Ç‚}¯N«á¼§¢V«),.‹f¨  ‡Q¼: £* »'²¥[ê©Æ/y‘üâßO¡íD0&ó²3Ú>k’VX›©5j•Bfzèú´ )‰¡SÃ2ÓRZüsKE!%)1ä>±f40§Ëì1y�æér¡˜åd¤š!}` «Ñ §ÎîÀߘ&(ÉFƒ¾É>Rƒ±8‹™”¤„·uÏÝßËØ¯O�&Û2ÒR‚³&B HìÚ¥åÉ#t:-=ºvÌôÑ#ÅdÔãpÔ7 Êkëlävét[%B82UjÄÁXƒ¿ÅèÂgÓ·SË„Ž¨cÞ ‚Ðî$£+üN-0*­³v� 3ôHR 7ÑÔˆE3µ½ðç¤×é0 lÙº�ÇËî‚"l6[Ø6‚ yæ(¦·À Ön"#zÆAˆŠ_k�ªœQNŠqKŽ"-W&ƒ¾Õm‘¤) NCòú³vöçï&ÎǨCZLGáèbR…ÎJi�Ocš�é„Öˆ`L„¨øu Q•3+‡7¥òpŠtvÄhfSþ\,&FŸˆZ­¾A8úYÔQcZ‡H;""‚1A¢âVG7…¯QŽîÜŸA¤i‡áö—e™þ�AÚŸY•U9—ª]øÝ!Hc‚ ´Êdj}Šs·b‹®Î�¦Øö]eYÆïó“�ÐòÄ‚ ‘cŽ2MÑ%׊`LˆˆèI¡U½ž‚·¹TÖˆëSÐ`�;î’$·ñ%áóùظi#–8KøŠA„ÃʤJE%…žù¸%N¹ºZ#td¢gL„V¥§§QZZÆö;±Ùö÷„)zên»ñÉӺX¾î×X7ó¨ñÍ7_µyß„„x,qqtÍÉi¿ ‚ Q1(q˜U©T»[~ Ù‡¯ IëÁï·ØBÛˆoŠ !¥§7Ï›W챬�“12Ô=ß}lŒZ&‚ íǤJ‰8sû�ô ø�­§ù Â�D𢠓ôÑ­£b’:ðx1A¡C±D»ÖX”³ Ç&Œ ‚1Ÿ.º5ÆLxÁgA¡c1«£ ƼÚè&¸ŽM"!b^­#ªrƼƘ ‚б˜£\kÌ£Á˜Ðv"!bž(×3uà5Æáh6|Ô8òówéf„õú›sèÓ?�>ýóøú›Å-î3íÌsøøã…1;æ­wÜÅö;[Ýëãut­]϶|¶±<^,ê´D¹Ö˜[UÃͷͦªêè›Yñ¶;îbïÞ½1¯÷µÿ½IIIiÌëý³Ø²åþñèã¸Ý‘MlbA¢àRE÷ÔÏ(‹ž1áØâv»)+¯dÓÛé’�ÉÐÁÛå8�>ñ4oÏ�×ìýA2wÎëírÌö0뒙̺d&3g]Ùê> ?ú ¦Çܱ3ô�|¬�×ѵv=ÛòÙÆòx±¨3ڵƜrt,c¡¨¨˜'Ÿù'Ï=ûdð½§Ÿ}ŽQ#†3|ø°v;nIil1»£žm[·òÑ'Ÿr|ß¾œ{öŒà¶�7³`áBÊË+ˆ‹³pòÄ“3jdØ:‹ŠŠy{Þ;—”‘ÇŒ3¦1 ¿&ûTUUñÈãO1~ÜN›:¥Mmu¹\Ì™;—k¯¹µZ|¿¸¸„¹ï¾Çž=˜ŒFF�ÁÔÉ“ip:¹å¶;8mê¦N™,zÆAˆœSŠüiŸ^6cTDϘpìpÔ7ðùâïØµ§€Äøö]OîîÙ·±yý6¯_ƒN§gá‚ùl^¿¦I öõâ%Œ™p CGŽá­yïßß±sΜÅÀNdÒ©Óùmíº6óç_~åÔég3à„áL;ó6nÚÜ6dÄ^ûߌ7‘Qã&2ç€@1Ô¶PþñèÁž•ƒ{ªvåçséåW“7t$'O9�Ò¦vŽ™p [·ncÚŒ³éÓ?�k®ÿ[›Ž·}ÇN.œ9‹ÁÃFrê´3ùnÙ÷MÎï½ùó{Ò$†ŒÃëo¼öÜÎ8û|~ùuu“÷0x�'ä9lݶ�³Ï¿�' ç”S§±ô€¶´fî»ïqã-·sâèñÜuïß¹é–;:r 6n {~¡Žêz¶æ½ùrÉåW5yï©gÿÉý>¶l¨ã…ú^·õF ŸÆîíûƒ §ÓÉ®ü|6où�mÛwàpÔ·Ùív¶mÛÎïleÓΦŸe(%%¥<ýìsÜtë<ø�GÙ°qcpÛ-·ßÉË—s×=÷sËíwòÍ’%m®7”U«×pÇ]÷rû]÷49÷ƒ{Íî¸ëžàßCµåÎ{¸˜<ú×Þp#/¼üj›êlMEe%÷ÞÿËW¬¤svv“m~¿Ÿ×Þx“©“'ñìS�3ëÒ™|ð᪪ªÂž÷koÌ¡OßÞ<ñèCœ1í4Þxó-ìöýcà½^/o¼5—Œôô°u臗“›“C§ì¬&ïÿ÷o’™™Éc�<Ä�»ž_WýìÅÕjµ,_ñ3�GôŒ ‚Zmyn§“}¿r³‡³,âz4Þ$¶•Åþ ¦ DÊâ­Á@Rçìð;ƒ^ÇSNF¥RñÇö]íz¬¶Xµz ø>›6oæú¿ÝÌygŸ…F£áo7ßÎøq£ùÏË/ðÓŠ•\ã-,]ü%šžð¶ä‰§Ÿå Îå¬3¦óϽÀSÏþ“7_ Ü„)²Ì¯«VóÙDzå÷ß¹úº˜:iÉÉI!·…rßݳ¹ïîÙ-Þà_ãmLžtÿ~þYvîÜÅU½ž¼¼<ºtî²�ßû5#Ǟě¯ÿ‡îݺ¶éx~¿Ÿ[︓qcFóêËÿfÅŠŸ¹möÝ|óåç$ÄÇ¡È2K–|Ç‚ùï¯õEü�¦õ„ssº°§`C‡ ¾·+7Y™™¨Tª�çðÜ¿^dÚé§rþ¹ç°qã&î¾÷Æ��$I­OQÖmØÀ»sç0mÆÙ̾ãV“ùfÉ·ß·OÈó u¼P׳53¦Oã…^aë¶mô8î8\n7ò)¯½úRز¡Žê{É9¼SpeÎma6™èÒ¹36»�‚‚zô8I’(.)!!1�Ä„Ê+*� ágÞXôëۇ믻†ßÿƒ×ߘÃ?|“ш,I¬[·�»ïšÍž‚=¼üÊ?v,*աݾïÊßÍý÷ÞEAA!/¾ü*yú�Q¨¶<þÈ?¸ã®{¸ùo7�‘Y Ó’ä¤$þïé@ÏÞÂO?§Áé n“$‰8‹ŸÏh—¬ ÓjÑêô!ë,¯¨Àjµ2å”I¨T à«o³å÷?8apŸ,üŒÁyƒ"N·ümízÆŒiÚ3WPXDyy9wÜv3:­“ÑÈ÷Þ@ƒ3ðÝèÝ«'?­üYc‚ ´®¶¼¹¼‚œÊ*P©@’�“Ül­�|ÚÞÜ#wom‡V Bü~p¹Ø‘•A…ä'¹S§#Ý¢Ãæª+f‘˜Ïè‘#Ðê´”–žNï)ØÃß®¿�ZÍÄ“Æóêk¯±jÕFœ:¥é£÷÷÷®�|Ò»´ÉöË.½„øø8N>Œîݺ±vý&Nv[¤vïÞCaQ!×\y%jµŠþýŽgå�ßµ¹�‘*))eW~>óß�‡Z­bâIãéòjgV¯^ÃÄ“Æ-_ëÎ�Zþss»°§ �ââ¾]¶Œ‹þr>»òóÉÉíö «V­fÈàÁä È—ŸÒ¬þ– ìן.�;a1[5b~¿Ÿ?þØöü¢=^k4j5\p.o¾5�Gz€E_|EçN�èÕ³GÔuî)( ù½ŽäLªTÊœÛp¹\¸\.RSS�$ ‹ÙL…¦»Ã�N«Åív“”˜ˆ$I¤$'£nÃŒŠÕÕÕ”••qçí·„ýùâ˶oßÁÀý˜tÊɘMFúöî�Z£¦Új%%ùÐf&ž0~&£‘Þ½z’––JþîÝÍÒõZÒm‰Æe—Îä_/¼ÄÿÞœƒ¢R3ó¢¿`4„ƪ«­$$Äc·Û¸ëÞû¹öê«III¥ÚÈôÙ¸y Õ55œ9c:ï¾??¢ö”––’�Ù´W¬ºº³ÙŒN«m±ŒÏ'OœÀ˯üWc‚ ´ÎÝà¤Ky~Y†Æ®|¿Ù ^oÄuéíà·ZcÝDAˆœÁ@ׂ"6&[K-$'íïyR«Ôø¼^lv;^�‡�ƒ›^¥e០Ï{ï}Þ˜ó6{ËÊq¹œdee¶z¼8Kµµ5mÚ©j«•8³µºå[špíŒTEe%qæ¸&ÇKJL¤¢ª2ø÷–®u(]ssY²ä;¾ûážzúÿ>t(»wï¦knnØs¸çÎ;ø÷K/sÍu ®ûëÕœ{Ö™aÏCßxó*É:�EQðù|aÏ/Úã…ráùç1ù´3¨¶ÖðÞûó9ÿüs©¾ÚÚÚ�ßëHÎA¯ ôy½>ü~?›·üÞd»ÇíÆ«R!ËJ°7R–e´†ð·ØµµuôFT*%øžÙl¦¶®.ø÷8‹9øg•¢àoì:ö‚™Œ&޶Í�Üm‰”Çã`ÿ�sΞÁ (,,âÕÿ¾NN—.!C?~âââxñ_ÏðóªU€DMM _}õ ×_{MTmªw:ÑétMÞ“$ðù?÷+WþLÇž>ítÆŽ @Zj*)©É"¡uõ�±ÿ¡VGˆ¨n·Ÿ/¬I’„×ëŧ ?«žÅbÆî°Æ 5¦Zkj‰��þçA«Óâö¸ƒmpÔ×£7‚ûØmûÇIÙìvŒ&#’"ãi|pàñx›Œ‰‹V¬ë,-+£¡Áɰ!CèÚ5—ôôtvlß2KNL¢ªª�Ç ~÷îÝKÞÀþ¬ß¸‘;wpóm·7)S]]ÃÅž¶Mz�ކ†¦Ci©©ÔÙìÔÙì >ŒáÇñß×›�=ù¤“Ä‚ „á÷7}©£ ¨Ô^¥y]â%^Gò%�š’B·n¹¼ðò+8õ¬[¿�S§Ÿ�Õº§ªÚjE¥¨ÈÈÈ ¢¢’9oÏ£ÚZƒí€ÁðsÞžKMm+VþÌ®]»È4¨MÛ"•••InN/¾ô 6›�U«×pâèñìØ¹«Mí4ôlذ¯×KIiøñ°étÍÍáåWþC}}=_}³˜ÒÒR†4Ž;‰F×Ü*++Y¿~#Ï>ù8ëÖo`ûŽtëÖ-ì9œuÞüòëj¼^/i))èt:B ;äó w¼H¯ç>—ͼˆ¯/aÆôÓÃŽWõõõlß±¯×‹Z­F¥R¨ª®ÆçóQ^Q�JþCHHH ==�/¾ü—ËÅšµk±VWÓ£{÷6ŸÿÁ0 üøÓ Ün7[¶üAyyy“É%–.ûž††þغ�½{÷’Û%‹ÙÌÆ�›p:],úâK"ùWR«Õ’¿{7>Ÿ�ªêýY0‡RgK’q»Ýüºj5n·›üüÝìÞ³‡Ìz‹kjj¸ö†Ù¸qÿD7II‰$''ñÅW_ÑÐÐÀ/«VQQQIïÞ½=r$/þë¹àkÌèÑL�2¹M�@Fz:EÅÅMÞKMM¥kN|øu6;6»�ÚÚæ³l×½›è!Œƒn\ýŠ/ªYµG=cÂÑã0c.—›O¾\‚"+(JàµmçnºwÍaìÈö›b:RO?ñ÷>ðÿ{ãm’’¹îš«Ã>™ïÝ«'ãÆŽfú™ç’••ÅCÜÃÖíÛ¹êšë™÷ÖÿèÒ¥ SN›ŽËíæî»n'á€:[ÚöÛÚu\8ó²à>«Vf¤»è 8÷ì3™6ãìà¶ï¿ÿ‘»ïÿ;gÎ8ƒ‡¼Ÿçž}Š;ï¾�×ßœCjJ ·ßzݺæâ÷ûöó’™ñØSÏð÷‡£g�î¼?ïm¶ïØòxO?ñ÷=ðoÌ™Kvv&ÿ|æIL&SÔŸ�N§Ãçó3fôh’““˜1ý4žù¿çéÖ­+½>ä9Ü|ã <ø�G((*$))‰¿^s%éiÑ÷RI’òü¯¥ëê³½{öm�k ×£RTüå¼ÈR[:„þ^Grygpvàv9;+‹¢â*+«P©R’SP”@/KfFÅ%¥ìÝ[NRR"–8>èßy’$1ëÒ™¼=÷]ûÉɉ\yùeÍRÞ"!Ë2—\|!ó?\Àûó?Âb2ñ—óÎ!11°´ŒÏï§Sv=ò8�›sΞ�ÙølO›:™wÞ›Ï_~͸qcHIJ öj…3aÂ8>øhóÞû€ìÌ fß~kÔuÖÕÙ˜}÷=MÞûnÙwœp ̺äb®˜u)/ü”·æ¾C\œ…éÓN£sÆÿκd&oÍ}‡ÅK¾#))‘+f]Ú꘮H Ð�ßÖ®cÈ ƒ›¼Õ—ñ¿9os÷½÷£ÓéèÕ«'yƒš/o"Í{ë ñxPøÓðú|Ølö&¿ÔVm­Ád2¢D�r#´¬°°ˆ^;ó›¦)ö·±ãÄ×5vÛF? [b*.ŽŸõoó$¯—êj+éimOEÛ°e[ãsjµ:øR©TM^û5Y–›½$IjòBÎ’w´>jï¾ý&99]"Ú&›œN'<ô õ üó€õ±Ž¿Y°¬"üÌŽën; ÛwÆVáèàr¹xࡇ¹öš«›MoߢgL„Ðüþ&=ZÑ÷Œ©DϘpôßEA8*x½^†ŽCŸÞ½yþÿž>ÒÍiF-G×KåW<ý�M„XÐh4\|á¼ùÖÛ̾í–& ?·…Æ„c^aÉ^ÜîýmÕ*Z�‹Å„6ĺ0­ñC›þöù|Xkëhpºp»=¨�NKœÙÔdf¥ÖT[k©­³Ñ¥S;OAàk|ùUÑMà¡öÈGdæ%Ah‘ø.¶«§–�d›pìQ…u«>ÒÍh•ZŠ.�Í/‹`ìXÒ§w/úôîUYŒ  Õj°4Î&äñxq44PRVArb<&£!Léý*«¬8êè”zÑC§ËÍÞŠJ$I¨×c6ñx<ØÌ�í8ªpãêS¢ Æ—3&EÄ‚ ´�Z½~Ukü²'Æ-:*Œ  V”&AWœÅDE••Êêô:Š»ñg~¿ŸŠÊjÔ*©ÉIÈòþggññ–£ïIZ³ž11�‡Ðˆï¢ m ’¢LS”D0&´�Æ¡’$‘‡ÝQO�ÍN|\`¡C—ËMUM-N§ E‘1êõÄÇ™‘$‰¢Ò½¸\�tÇ]{Š�%©ÅB›£—ÛMvJZ“@ š¦7úý~jêlØíõx¼^´5 ñ–�©“vG5uu¸ÝÔ*‹Ù 2õ TV[IŒ�ÃZS‡Çë¡Sfzøµ…šMѧD÷ Fí–Ä °pôßEAÚ Ú1c>Ùã–•Æ¡Š"£Ñ¨q5Ž's{¼—•£ÓiHNLÀëõP]S‡Ûã!59‘äÄx¬5�1`iɉ´¶Ð‹Óé ̪¦ ýãWY]ƒÝQO|œ•J…Íf§¤´œÌôT4šæƒCmözÊ+«°˜MÄ[Ì48�”WVãóû›¤`VU×`±˜‚³µ…uP0æ�2M1¸Î˜  ÄwQ„6ˆ> ¿è³ €Æ!$EV‚ëaÔÔÖ¡È2iÉIL1-SQU�ÛíA«Ñ RdIB§k}Ào`²ŽÐA�Ûã¥Îf')1>HtZ Kʨ©­#%9±YkM-Fƒž¤„À´ÿ½¯×‹ÕZ¬ %9�¶í“øžMQí ¼!•[xGñ]¡ ÔQ¦)z=cBÛˆ`LþŸ½û �¢Ú0þnËnzo„!t$ôÞ¥(EP¤( (ŠbÃÒQA¥‹õ*vAŠ(¢‚‘fé¡§ZzoÛwï‡ÀÂB²- %œß}æ¹ffÎ9ÿ™’=sš F“ùåµ:*•›Õ?*•Ûåcz Çþ9É2´ZÛoÌ®Ìîè~Íb„‰•R‰Vwã/x“É„Þ`À×ÇÛj¿J¥¢ðr7ÇkóqJµŒÉÅÂíD´Œ ‚àW'ð0ID˘à±*® ”Ád2¡×éq»²^D©ßÝ®´�9ž¯ÒÍ ƒÁ€Þà@¥¦Ô|�Ÿâ£\“‚\©Œ]ÞŒ2ã ûÙ,cÆÄ&¶Ûe«bÚuêFRÒ9—ÒN˜4•ø„ÄÛ"–²|½t�bZÐ(¦[¶n³yn«ö�III­Ðò5uÆ|½tÙ-)ÛÎÜÏŠRÑÏÙÍ /ejûø„Drssm¦«Œ–±y ßåô™¸ Ï÷VYúÝ ¶þ±½Ìã_}³ô–ý;¾™D˘ ”!;7$¼¼J&ÀpS*Ðh´˜ÍfKë’V«-9vÍ.séµ6 /Oò ÉÎÉ#8Èi)5¹+ j4:^òËù–Œ7S^ÓÅðJIR©¹\†F«ÁÛëê¬�­¹L†L&\|KwÝW£ ëŒÉLä ˜«Þ`áu“*c™Ù;u†"µ/OOš5iDý:µoJÙÎHH¼ý¿ �=Š1£G1jÌØ[J•p+îç�ðœM�ñAA�LxyÚ´hÊÞý‡Qk*öKÚµ6þ¾™NÝzÒ¡k¾[ù½e»NÖ-U»ö°üÜåÞÞœ9Ç€‡Ó(¦ã^ïPYgââüÈš¶jGï~øsç.«ã[¶ýA—{{Ó¦c¾[¹Ê²?>!‘£ÆÐ²mGú ÄŽkÒÙŠÓ–½ûþ£WŸþtèÚƒy ßu(þ?¾ÿXíÓh44mÙƒÁ`3Î�?]ÂÂwßç£O>¥U»N4kÕ¾Ô7ø‡¡kÏû¸xé’Ýxl•׺CV¯YC×÷ѺC¾þv©C׸wßô8˜¦­Ú1`ÐŽ?a7ÍŠïWóÒ«¯Ñ¾sw¦Îx“—_�D›Ž]ˆ=vÜnœ¶ž WŸ3[×PÖ}Y½æ'F?ù´U> ¿Ïë³Þv¨ÌkI�X&ñHIMåø‰“?qò†–±“§N“““Ãé3qœ¶Ž9bá¼9Lš8�jáá|ðáÇ]¾/i,Züã_™Èë³Þ&1ñ¬%Í¥ädæ.XÄ‹¯Ld欷8zìX©y'&žeêŒ×ÉÌÊ`Êô™$''óÖœ¹<÷âKüoÉç–sSRRY´ø^ž0‰YoÍ!öš<_}m [·ýÁ¤©3˜4uÛwìtú:¯7sÚ>ùèd293§Mµªˆ½úÚvÿõS§¿^Rö8”gZz:ïø?^zu’¨Œ ”´8edf“‘•MAa2™”ðÐ ¼¯Y{L¡�„Ùl"#+‡¼‚B|½½,fxyy¢tS��›G±Z�±Œ®ˆJ77"ÂCpW))V«ÉÊÉ¥°X�»»ÊÒç1È¿dòŽüÂBˬˆÕBƒ-?oOO<ÜUdåä¡Õjñòô 8Ð�FGzVj­–� @§­.Õ• <.oF…Éé.Šr½ôÖñ›Ø®ÝnÒ˜±>=»R3²R©”Ðà@är9f“kã.qäh,¿¬ÿ‰Eóæ°èÝ÷ÈÈÈ´›f×ö-øûû³aÝN=È’�?t¨¬>ú„ôã¿v3ïíÙÌ_ð®Õä>ûdýO?ðî‚y,z÷}t:f³™ “¦Ðªesvý¹•W^z‘‰“§‘“›çò5Ìœõ� ÌÖM¿ ts£¸¨Ènš¨Z59á¼Õ¾³I爨V ™Lf3Κ5k°ióâââÙøË:þÝó'!!ÁVy¥¤¦ñê¤),^0�ê¶[ìÝ™TÊì`ÝšÕ,^8�>ú])㇯7ÑbF ʾ¿vÒ±C{.~ßn™LÆ‘ØX¾_±Œ�›6Ѧm+ú÷íËÖ?¶Û�ÓÖ3áêsfëʺ/ ÀÙÄ$ÎÄ•téÓéõ¬ÿù†?2Ô¡2¯we­±ð°07jˆ·—× çH$ò ˆŽ®MdõÎ]:‹Á`{)o† Äò«0›ÍüðãºuírC‹Ó_ÿüCµðpjDFZö=8 ?gÏžåï÷²yó6F�|‰DBP` ï-ZÀóÏ>sÃ3ië˜C$RüýôÐ@Ø äeÆç_~Mtt çÍaЃXòÅ—–kßðëFÚ¶iÍ{ çñøc#øé§õ7L–�“Ë—ß,åÉ''(0€yï¼…———¥òóü¸’ʵÙlæ«o—R·NmæÍy‹�úóõ·Ë(¼üo^*‘p&.ž7fLeÌã�±nýÏäç8­’J$9Ë´©“yrÌh6ü²Ñîç°ä󯈮Åü9³EeLª‡‡U#²EV #$( Ôõ¼”nn„…Q³z8ÕÃC,kŒ]¡�˨BÍêá„…!—ËÊ,W&•èï{ùüjT!ÐßÅå4‰?ª‡‡–ähÕR*•HÍêá%•8Jº@V ¦fõpª…ãéqu(wQ5"P–2-¾-f³Ùj3¸Ò2¦—b6™Ä&¶Ûj»ÙŽŸŠ#,4�r¾ ±aôc#ñ÷ó¥Cû¶DEÕ*ó-tEððð`ÿþ$$$Ò¢y3~ÿíg«ß‡O?5†?:wì€R¥$5-�””TÎ&%ñÂsÏâéáAÏÝ©Y³t9ŽôŒ ’“S5r$žŒ{ú)‡ÒEEÕäü…‹$'§°|UI+âÙ¤$jEÕ´§·§ÙÙ9¼3ûMB‚ƒQ©T—»ƒ—Ðj4>ž”Ô4¢k×fÖ3¬ž ½^Ç’Ï¿ ßûêþ™““CZZýúöE¥TÒ¬i !ÁÁÄ_. gÏ{ñôô¤Aýú„‡‡“˜”Tá×­ûz÷ÂÛË“Æ ¢pSØýÜÓÓ3ÈÌÊ¢OïûP©Tb̘ v˜¬[ 2ƒÓ­ ƒ˜IQ¸ÍÜÄçÑl6s8öÅ-Ý;µ«Ô²‚ƒ¯~!ó÷ó'?¯|-N¶LŸ2‰�?]¸çǃžö†><ÈrüÊn…\�Éh$37_o_«ÙgÈÌÎr9޼Ü<¼<=-yº»»ãæfÿËsí¨(þøc;vïfá¢÷hצ çÎ�£vT™YY6ã”H¤DFVÇÛÛ»Ô¼¿Y¶€zuë:t öʃÒï§=+WÿÀ·Ë–“ž–�N§%"¢šCñ¸{”Ì (‘–,Õ"“É0™Lvã´÷L¸ÂÞ5”u_F<2Œûû?HNn«XÃ#� q97gT”_³~¨ÜÍìðKŸ^=ïeÞ‚EŒ9ò†Êëî aƒú„„„Ü�.!1ÏÒãÞî•UQL&«1™ŒŒe‚Õñ+’aƒñëÆM|òég �~}ï§S‡–ó¶mß@µjŽ=›ùùx¸{Z½ìööö&¿àjë—�÷Õáî;}}θv&k¹Lf÷s/,*ÂÓÝÝr ¢eLû®érèò;Nf`›Ø*u» ŒF{þÝ�T&£k‡6V­'•áÚq,9¹¹øùû •ÉÐ_î:£×¬¾¸¸ÊÏÏ—S'³ó�Í,˜û .&%5Ífš À@róóÐé¯N(”ž‘Ièå/š®ÄéíãM±ºØÒ5¨  �Îþ¸¼ÚQQ$§¦°{Ï_4mÃ7K¿ãÂÅd¢£jÙ�°¹žäðaC™ûöl¦L›i™èÉGÊsVJJ*sç/böë3Øû×¾ùò3—ór4NWž‰ÊºoooúõíÃÛsæqîÂúÜ×Ûå8\rͯ‰LjYbÆ`0R\¬¾zšÙÌ?®¥w¯žü¶i�Ær¬¨XÍÎ�»éß¯Ï Ù§¤¦±cç.&¿6�ôŒ :Ty×RŠsç/‚Ÿ¯ …’ÿ}ø>Ÿ|ô�eëЮ-žžž 2˜¹ïÌæñÑ�ñÓÚõdç\ý=Õµs'F�ÁÒeË-ËúØâããMQq‘UWÀܼ|üü®)¼æ÷FaQž•ØÁ¾>>©Õ–k•1Al3Y�µ‘œÿ2©“ný!±‰íúí&Ø{à0�þÄ4ªSÊûnÅ* Ù»ï?’’ÎÓ¤ A�ìܵ‹âb5Ÿ,ùŒëÇlxx¸‹ÑhtøËóÃÆ³ï¿�FB/wÕ³×#-<<ŒÚQµXòÙ¨Õj6oÝFjj*­[¶p(ÎÒ„†„ÈÊÕ?PXTÄ'Ÿ}áP׸ÚQµÈÊÊâèÑc,^0�#Gc‰OH ::ÚnœöøøúУ{74làÐ8­ò–WšœÜ\ä29ááádff±lùJrró,ck\a/N{Ï„³ÏYy¯á‰Q#Ù²íøÀÕej\ 7iìŸt£þêWlooŽ;ŽV«cã¦ß­&÷Ú¶ýO¼¼�JuuŒ|HHoÎ~ƒÁÀÐ!ƒðº‹«®�Úþ­9s�òOmÿôScXºl9ÛþØŽdåwßÞœ¾‚PŒ&……Eøûù–yNNn^^žÈ¤¢á·¼.^¼DÝ÷Aa¡e_Ƹ4 ýœ›š[¥“ñÔe„›. €}>àð€~ƒÑHNN.a¡Žw‹=‡B¡@.—£P(,›\.·Ú®TÒ¤Ré ›D"±Ú€rOˆ w­V˳ßA£ÖðþâåÊkÉÙAhŒ…öO¼†·$ŒÐ“®ÍÞ(¸n⤩LšøJ©cìnW¢eL„2¹  Õ‚J—ÀÊõÎÔËŒ˜Í&$âÕ�p;ðñ�âb¼µö§áÎc4iÓ¡ �6äÃ÷•;?Wº)ʸqFfA(�¨Œ ‚P&…REz�hBââÁË —�Ìþ Ûke`òÁÍ…ñf‚P‘Ìj5I­[¢òv½ ˜ ·/™LÆ‘{+$/½IƒÑlݨbÀ±éð…ŠµhÁÜ[‚ÓDeL„2ù„‡’/…‹¡!¨/OÙ«ˆŒ¥È3Éé¼ö>x?næòõÛ„Šà­Õ¡òö"0ªÖ­E„Ûœ+­b ZÆljʘ 6ù\7 _ç�ŒA‘êt>Q!xËîœ>Ü‚ ‚`@TÆ„Ê%f8Á)2\›Ø€s“~‚ ­æj˘Ä(Ú;Ljʘ NqõŒÞ,*c‚ Â�ÅånŠÑ2&8FTÆApŠÔäÚƒ¨Œ ‚ wW+c®þ­î>¢2&‚sL®ÍˆhÝA„;ŒËÝÍ¢›¢àQÁ)“‹Ý]üƒ&­óõÒe4ŠiA£˜lÙº­Ôs Âúõ*¬Ì “¦Ÿ�XæñŠ.ïN`ïs8yê4~„¦­ÚѼuV¯Ys ¢„©3Þàë¥ËnIÙ•ÅÕ <¤&ׯW_k⤩¤§§—;ŸŠòÕ7KII¹q¯Û-Λ­¬ûâ(QmÁ)£k-cbán”ž™ÅñSq©5xº{Шa]b5¸Õa9lÌèQŒ=ŠQcÆ–yΆµ?Vh™ ‰eWÄ*£¼;�½ÏaùÊïiÔ°ß/ÿ©TŠÉdºÉV].·Œ¹øâòv–’êz…£*+ï}-c‚ 8ÇÅ <Ę1ánc2›Ùwè(�ê×åáþ÷ѹ]+Ž;I~Qq¥”·wßô8˜¦­Ú1`ÐŽ?a9ÖºC¾úæ[:uëI§n=Y¶b¥CÇlykÎ|KkÍõ-Ug“’xüÉghѦ#½úôUy $ IDATgÝúŸŠ³Ë½½9s&Ž ¦QL ƽ0Þ¡òâ1j -Ûv¤ß€AìØ¹ËêúV¯YC×÷ѺC¾þv©Ýk[½æ'F?ù´Õ¾…‹ßçõYoÛ-¯]§n$%�³üܱkËÏgââüÈš¶jGï~øóšt ‰g1j ÍZµç¾~9tøˆÝ8�Ÿ8I£˜¬[ÿ3ëÞ@‹6hÖª{÷í·›¶,+¾_ÍK¯¾FûÎÝ™:ãM^~um:v!öØqÀöçw­C‡�еç}\¼tÉåëÛ÷ÌÖ½þøÓ%,|÷}>úäSZµëD³Ví-­ÎÄ¢7«ÈÉÉ!éÜ9«ciié$§¤ Õj9›”ÄÉS§‰�O át’å¼ë[Ž&M�îpKÒÁÃG˜2}&¯Nšbuí§ÏÄ1ëí¹¼øÊDÞš3�sç/ðçÎ]Ì_´Ø*�ßÿÀÒïJþ]§¤¦±hñŒe"¯Ïz›ÄijÅ1eúL’““ykÎ\ž{ñ%þ·äs‡âtµ¼´ôtÞÿð¼ôê$f¾9›þ½ºˆwJJ*‹ÀË&1ë­9Ä;f9öëÆM¬]÷3¿lÜÈ+'1þ•‰dgçØ=f+N[±Ø»/e¹ö™•1áŽb6›ouw=1›¢ 8F*‘пWw"ÂCA"A§×£tsÃÝ­röÏ_´˜Dzï¯�tìО…‹ß·“I¥ü·ÿ¿®ÿ‰…óÞaÑâ÷ÉÌ̲{Ì–™Ó&sâèAºtétñ^šHË–ÍØõçÞ]0�ù‹Þ³|Y´ç®í[ð÷÷gú5œ8z�%h·<³ÙÌ„IShÕ²9»þÜÊ+/½ÈÄÉÓÈÉͳ\ßì`ÝšÕ,^8�>ú�NgóÚ8€³‰Iœ‰‹@§×³þç_þÈP»åÙòÁGŸ0à�~ü÷Ïnæ½=›ù Þµü]ÿÊk4oÞ”¿wmçµW_æ…—^E§×Û̯q£†œ8z�^={0mê$N=ȉ£éZÊgâ(™LÆ‘ØX¾_±Œ�›6Ѧm+ú÷íËÖ?¶¶?¿+RRÓxuÒ/˜Gõˆ—¯lß3[jÖ¬Á¦Í[ˆ‹‹gã/ëøwÏŸ„„;Ë•–1???´ZmÉß2³ÙLnn.þþ\¼x ê׫KHhk×üFaQ‘Ý8í‰�O`úÔ)<ùÄhÖ®ûƒÁÀOëÖÓ­kg/˜KÆ X{ù…G«–-¹xñ"é™ F??_Ú·kK�èh�u蘳Î�;ÏÅK7v,^žžÄ4¹‡÷ì f�H‡âtVJJ*g“’xá¹gñôð g�îÔ¬YƒZÎyú©1øûѹc”*%©i¶[%Ü †jiMظi35"#iP¿žCå•ÅÃÃýû���H‹æÍøý·Ÿ‘H$œ¿p‘óÎ3þ…çððp§g�îDD„³¿ý<+C³&1Ô¬‰�·�:t ::ŠÜË•M{ŸŸV£áùñ/ó³ÏвEs€r]_Y÷ÌoO/²³sxgö›„£R©�ÉdNÇ¢7•üí’H$ø“•• @^^>nnn¨T*ôz=Z�Ž�à`¤R)>ÞÞøùŸ`7N{îëÝ o/O7lˆÂMANn.Ó&¿F×Î�P(4oCvvI\Þ^ž4¨_Ÿ½ûöpôX,îîÔ­[‡ŒÌL222x __”J7š5�!0 €¸JŠÓÕòÒÓ3ÈÌÊ¢OïûP©TÔªU“E æDNNiiiôëÛ•RI³¦1„[îµ»JEAa!��ޝ¯/nnnH¥R›ÇlÅi+–Š"7Š/¶ÂÀl6c6›Ñiux£"T³Ñµw8z³í7Ñ‚PU…1d`2³sÙóï~úõ¾?_Ÿ /gåêøvÙrÒÓ2Ðé´DDT³:hùo__òóó:欜Ü\|½}ʬtÚ‹ÓY™YYøzûZ•@föÕÖ½k¯O!W`2íæ;â‘aÜßÿArróXýÃydˆÃå•eú”I|üéÆ=?$ðü³Ï0ôáAäççc4hÖ²­Õù©i·fŒŽ»‡;©•J‰L&³¼µ÷ù}³l9õêÖµì+Ïõ•uÏì‘H¤DFVÇÛÛÛj¿³±èÍWÇŒøûŸ€ÑBNNþ%­bƒ™LfUIt—{’_P`7N{|}®Æ/—É0_þvîÞöí’—›‡Á 'ðšg¼m›Öløu#ôëË?ÿî£M›V«1™ŒŒe‚UW*x§Z£u©¼Â¢"<ÝÝ‘Ëo|ù›Ÿ_€‡»§Õ1ooo˽– %((ww÷Ò–uÌÖ}Q*•eÆRQä……åoB„›Ád2a2ñ­„/1‚ã$zÑ2&Î’J¥D„‡’xî)i^KIIeîüE|ñéÇ4oÖ”ÃGŽ2ã�YVçdgg]; ¸\aòõu蘳‚ƒ‚È+ÈG§×㦰žQΑ8�Hn~žUy附†„”+_oooúõíÃÛsæqîÂúÜ×Û¡ò¤2úË/ õzƒÕr??_fL�ÌŒ©“Ùûß~žñe:wìHHH0*•;ûÿÝmy‹;räó>l(MšÜÔi3Yûã*”Je¹®¯¬{jó^¨TÊòs6Ã5•1™L†¯¯/))©hu:|}JþËårŒF#f³ÙR!ËÉ.¢idÉ¿#‰LŠáò ƒÁHq±Ú©{p½ìì~\³–ŸGíÚQ$žMbùÊU–ã͚ưbÕjNž<Í©S§6¸¤òêçëƒB¡äýwç;ÔºX^®–çëãC‘Z�Á`@.·~©ãããMQq‘ձܼ|KwC7·²g²,혭8³²²ËŒ¥<®}&äþ~®ÿÂá.äâlŠjSùß Â�D£Ñ²iÛ.:µkExXYÙ9deåÐ"æž /+'7¹LNxx8™™Y,[¾’œÜ< ‹Šðòô`ÙòÔ«W�'NpöìYZ4onIo똳""ªU«Ÿ|úO�yœS§ÏðâËX¾ôk´Z­Ý8=<܉��%ªVMÒ32  µY^xxµ£j±ä³/ûäìÚó©©©´nÙÂåk¸â‰Q#éÝo£F·T¼ì•ÀÎ]»ˆ¨V�/¾úÚj|ÓÃÆ3yâZ¶hFèå®s „Åÿ–|Æ“�?N\|<ÓfÎbÅÒ¯¬¾dºbÊô׉=vœ_ׯ)÷pGž3_ztïÆÆß·°pñû̘:¹\×WÖ=Û÷º,ÎÆRl°n% ,é¾`¹Ÿ …¥›™RXXHvZõêÔ)¹'ÞÞ;vœÀ€@6oÙJyG¿!“Éð ?¿€í;vRXX„F£A¥R!—ËiÖ´)?þ´–ZµjTÒ¥Î××—ððP~Ý´‰Þ=z’œ’̲嫘øÊx!Ñfy‹æÏeæ³ùvÙ ªW¯Æûï.ÀËË«Ü÷UåîŽ\&çÑaC,û$‰Íò^xöf¿3�%Ÿ}ÉÈ�ai½y奙õÖ;\¸t‘ÀÀ@ž7–°Ð’/t‹æÏeƳùæÛåðü¸gð³ó9L›<Ñî5ì?p€gŸ[!-!Žúd ¯½ú2íÚµfï¾½Œìñ¨UÚ1��fùŠUlÛ¶/úõéãPE àÞ{»ñãÚu¬\ý#Õ«…3ùµ ö’¸\ÞÓO�aé²ålûc;¾¾¾ zh ¥ò3æñQ,_ñ=Û¶ï ((€±O>a¹×®²§­XÀµûrí3!ù~Å21=�P¥ääæáéé�\Vyý{ïv9-×QŒscJÜ¥Þ ñÿ™Y¼îÍcUZ9�†i¯¿Á’�?Dy g•0hññ VûN=Xî|�«ˆŒ=Š1£G1jÌX§Ž•7– kt:OgÓº]'ËÏtíÒ™×§O¹¥Ÿepõ~–•ÎÞççÊLŠÕª…ó÷ DVK',TÌ,8F´Œ ‚à…ÁµE-‹L®M,w*“ÙÌÞ‡ñó­ü…`¿ÿáGš6iBÃõ­öŸ‰‹cð##hÚª½û àÏ�»,ÇÏ2bÔšµjÏ}ýrèð‡ŽÙ²aí�œ8z…BÁ†uk¬*b­;taõš5tíq­;táëo—:g—{{sæLL£˜Œ{a¼åØÞ}ÿÑoà`š¶jÇ€AC8vü„ã7­ ¶ò´Ë[sæ[ZÛ®o‘‰OHdĨ1´lÛ‘~±ãšë³u_ìùgÏNŽÞÏ÷+–r&.ŽïV¬àãO—°ðÝ÷ùè“OiÕ®ÍZµ'%%pý³u5Ï÷î£WŸþ´hÓ‘‹Þ£ß€A\º” @»NÝHJ:g9·c×–ŸmÝO[Ï‹­t¶>¿+×ðü¤gˆ‹O ¸Xm9¦ÑjIL<ˉ“§ˆ‹�§  Ð*_™LFýȦü¾y‹C÷R@TÆAp‘TëîRºBcvG"··ã'ÏQ ÿ›PÛºíOzõº÷†ý|ô èÇÿìfÞÛ³™¿à]Ìf3ã_y�æÍ›ò÷®í¼öê˼ðÒ«èôz»Ç\%“Jùã�¬[³šÅ çñÁGŸ ÓéìÆ¹kûüýý-•»%hÉsþ¢ÅŒ>”}í¤c‡ö,\ü~¹b´—§­XfN›Ì‰£éÒ¥“U~f³™ “¦Ðªesvý¹•W^z‘‰“§‘“›g÷¾8Êh0¢×ëQªJ&�¨Y³›6o!..ž�¿¬ãß= ¸þÙºšçë³ßæÑG†²ëÏ-Èä2Î&%a2›ì–WÖýÛÏ‹­t¶>¿+×ðÚôg áÂ… –<ÓÓÓñõó¥aƒúDT«FjÚ�‹B׫ބcÇŽ[Ò‚=¢2&‚kÔ.%+3* w‘´ôLŠÕêDÕ¸)å%$&Ò n½ö{xx°ÿiѼ¿ÿö3‰„ó.rþÂyÆ¿ðîôìÑ�ˆˆpöï?hóXy=ýÔüýèܱJ•’Ô´t›qÚ³ö‡U 6¥RI¯÷’œ’Rî+:Ï””TÎ&%ñÂsÏâéáAÏÝ©Y³\½ŸeÝ{ÚwêÊ=ÍZñØOѺU+:oO/²³sxgö›„£R©�Édåúl]É3=#ƒ””T>/OOÆ=ý”k7ñ:®>/e¹ö´Ò|¼½Q¸)(*.@*•R\TŒV«ÅÃúu¢oÈ#Ä»&Z�ÖRÉ{Ę1¡Ê¹xé�Æ©4*•Šê•QÕd.vs)�XøY¸[h´:NÅ%еS»›VfaQ^^7.8;}Ê$>þt ãžxþÙgúð òóó1 4kÙÖêüÔ´T||¼Ë=Îj_³¦1Ü׻͛5­´òfL�Ì=M¢’ø¢1;÷ö¯À”‰Ñ0/Tq©iddç°î×-Èd2˦ÕiéÔ®u¥”éíåEaaá ûýü|™1u23¦NfïûyþÅ—éܱ#!!Á¨Tîìÿw7R©õ¿ÉôŒŒ2�U–²â  -3MJJ*sç/â‹O?¦y³¦>r”oÌ*W•‘gP` ¹ùyèôzÜ Ò32 ©Ü‰Tª'ò°õ¹CIeÉÖ„+Îæ™š–FQq1z½…BN^~ÍÕ±XR™ ½¡¤â£×È/pl Wž[®\Ãÿîäëó�Qd°îÉ!“É #<,Œ¢¢bÎ_¸€——'ŠËŸ§»ÔS‘�Z�»Gù+›ÂÝA|ª$OOO›ÛáÇ1 –ŸË²qãï¬Z½šºuë0æ‰ÇéÖµ+Y™Y¤¤–ÿÍpE9âQÞ|}S§L¢^½zÌ_ø®K³Ž9Å,Eiôu:™Ú”C¾ñöº‚PjÕˆ`ð÷3d`†=Ô�zÑQÄ4nH»VÍ+­ÌèÚµ9wÃþ‡‡ gß0�„^îZ&‘”¼Ù�ŽŽâK>£¸XÍ‘£±ô8˜ÜÜ<›Ç*KYq^áááNll,F£‘”Ô’ñ:9¹¹ÈerÂÃÃÉÌÌbÙò•ääæQXTärŽäYZ,¶„‡‡Q;ªK>ûµZÍæ­ÛHMM¥uË.ÇéªÊølmåBpP+V­¢¨¸˜ÿ}ú’k*lA�ìܵ‹âb5Ÿ,ùÌáñVöž[Jûü®\Çß.¤@—�Z­&>!ãåʄijc6›Q(äH¥Ö…¹›ýINNCé¦ÄÏ×úïc^^ϽøÇŽ•r¡j•1ᮓ’’ÂÂ… ;v,G�-ó<ƒÁÀ¯7ñè°aôïÛ‡f1Mh×¶5Ï<ýÍ›7»‰ÛIƒúõhÚä=8€Ö­Zr&.Á~Ârr3¹6!A–á|G"@ÏÝØºmû û_yéEf½õÍ۴牧Çñ츱„…–´,š?—}ÿ¤S·žLœ2�±O>ŽŸŸ¯Ýc¶ 4„F1-Ðëõ–ëa+N€Ñ£F2wá»´hÛ‘—^�@ÃõéÖµ3 åéçÆ3æñQøûûñô¸8tøˆeF½ýûðò„I4ŠiÁœù‹l³•§­Xâ-yîÚµ‡i¯¿I£˜Ìxc6‰„Eóçò÷?{騵'ÿûô3ÞwA©ÝJoW?[Wò”H$Ìzcß­øž®Ý{áç냇ÇÕqÇ/<û ß.[A—î=1›ÍDFD 7lÞO(ûy±—Jÿü®\Cì¹½œ>ÇÅK— D&“BJJ 'O�&éÜ9‚ƒ‚-­bJ³æž{—kìšpw‘|¿b™˜îE¨RâhÚ¼U™Ç,XÀîÝ»ñôô䫯¾ÂÓÓ“#‡öS'Úz ®Á`äù_bÈà‡éÙ£{©yíܽ‡•+¿§ÿ¾>|„¢¢bºtîDï^=‘ËK~ygfe±yËVŽ;A`` ­[µ¤SÇöÈd2æ/\Lýúõxp@L&S¦Ï¤y³f<:¬d¶ÑhdòÔ<Я/]»v¶*ÿJ·Á©S&Ñ´É=@ÉŒ]sç/äÞîÝh×¶�åܸøþܱ“3gâ¨];Š®]:Ó¸QIwγIçX±rC‡ æ÷Í[HHLdþœ·‘ËlÿsÿîÝ‹¿¿?u¢£YúÝò’nŠ÷4ÆÔð$§ô[ÈÉÉA£ÕâããMPP ¹“ÉÌÑØXêÖ­CNN.Ô¬Ywwš¹?HKÏaÎ|¬‚plj=‡B¡@.—£P(,›\.·Ú®t_”J¥7l‰Äjl~ÉÓh4Ü× K>þð†éíávÒª}gÖý¸ŠÈêÕou(7ø;ëöå¬r:]Xqf�XÏ«/�ëŒ -cB•f4­º;$$$°{÷n† b³‹¢\.cäÈá|ùõ7løå7Ò3J_K­Ñ ‘Hxhà=4�eËWpèHÉÚ*¹yy¼9û0ÓOŒæÞî]ÙðË/ü¼áWÚµiÍßÿüƒÉT2½orJ çÏ_௿ÿÁp¹ÿü¥KÉ\¼t‰ú nœ!튋/rúLGŽÆ²úÇ5èõz5l`9~êôf¾1‹ÚQµxvÜÓ4¨_�·Þ™Ëᣱ–sŽ?Á÷« }»¶Œó*•Š ¿þÆ–­Û¸ÿ¾ÞtíÒ™¢bën?ñ‡S¹”œŒŸ¿‘‘Õ1™Ì\¸pÑêž'%�ÃÝ]Eõê–EH³M¢eL*ƒJ¥â�Ùo2eúL´Zí­GîH™ú³.¥ÛºþîíÞUTħˆ <„*+%%…E‹Ñ£Gúöí ÀÒ¥%‹hòÀØÍ£sÇÔŽªÅ¦ß·0þå ôís?÷õîi5àÚÏÏ—þ}ûX~މiBÒÙ$Z·lÁ_ÿCHp� béæ P(xgî|ºuíBýúõøôó/¸”œBdõΜ‰£q£†?q’ /U«&§Îœ¡q£†6y·|¥ÕÏ‘‘ÕINIÅǧ¤áúŸ70tðÃôìQ²þPtí(ò ùiízšÅ4±¤{jÌT«”Ì*µþç Lšð ÷ÜÓørºÚ¬ùi�åü +wÑ{v¾—Ë‘J¥œ:u�V‹Ò­¤âQ­Ú ]_²õçÑ™Šp“–]Á5�:´§S‡ö·: A°iÿ?»ou¥Ò ÉÔ¸6æúá^£1eUþz‚BÕ"*cB•õå—_ræÌ’’’hܸ1yyy:t€áÃ‡ãææØÔìÕªñÔ˜ÇéÛç~öüõ/½2‘‰¯¾L«2]‡‡ Ó—,Ôyöl1MšX*bPRQÈÈ̤^Ý:4jØ€3qqDV�`ßþôís?ÄÅÇS«f öíû�öíÛÛìšt¥›¢ÉlF]\Ì?ÿîãÝÅï3î;xxzpøÈQúö¹ß*M�ÚQ|¿úŠŠŠ-ûnWû¾gdf¢Óé +µÌ¢¢bíJ$bŸÞa¬Ž KeìúÎ…¦L²Œç —:7ë¥ ‚ T¦ ]"ùÇÖw»–·4Sî­ÿ'ÜÙD7E¡ÊzöÙgñòòB§Ó±`Á¾øâ """èÑ£‡ÓùU cèà‡ylÄp¾[±ÒҵгÙLYS;I/�iß¾-ÿí?@vv6‡!:º6͛ưÿÀA2239vü mtQ¼>OOOOºuí‚ÑhäÜùsp¹Ëà •9Ëø“Òó2MW¯¡ …£ÎLãȶ4¨_ßjóp·?¥oŽQtUAn/Y:׺(z™«�Q|­œ'ž¡Ê â…Jf¾:þàç|ºãô¨Q`*|A¸éu:zvëHHP ¥›¢ ·�¹Øåií¥E~+8áî!—98E· Ü$HÐêt·: á:Æ<"ƒ(&Ó©tÆ ²ôçSØŸ¤DîD:½•�–|Anšòô¥/åb‹—4D,ö,”‹3&ÜY$8¼Æ—pó˜ R< ¡Ë�«ŒdDeL¨ºôz=¿nÞŽL&ÃÛÛ‹˜Æ iT¿Î­K„ë¸2‹"€—©fƒø^"¸NTÆ„;Š˜Eìö¥Ò¹ô%×$ª®Aýï³Ìš˜›_À_{Hx¨X Vn'Yz׺(ºëC*8án#ªò‚ Ty‘ ƒÆ(YoLª:‰DBHP A�þäæåßêpA¸Ž«-c u@G"ÜmD˘PåhµZò �Je¢KãM$71ùK0¢w*]š>�u,ž’ÐJŠLÊÇh4b4 ÀÃÃñe04Z›·ï¦C›–„‡…�šžIfV­[4­ÄhApV®î"™.-ö¬‹= å&*cB•“——�»»;nJ7«…R…JfòÄOI®ÔÙéêMhÜÓ¨îV¿R„ò0›Í˜M&4-™YÔ¬áxeL¥t#枆8r í~=>Þ^´iÑ_ŸJŒXg]ÒÇ`v~r0_I$¦B1°P>¢2&T92¹¥R‰\.»Õ¡ÜuÜu!䩜ïv˜iŠ£�¬{%D$C"‘P¬V;�®FD8µkFZ¦µSÛ Âí'YsÌ¥tÑ£C(?чK¨r¤R)ˆÆ°[B¡öw)]šá4ÅæÜ ŽF*�D‚L&^ðBUSdÌæ’:Ö¥´JmPG#Ü�DeL¨’D×Ä[CRàZ÷«|ciúS� Tñ;Eª¦KêXrõÉ.¥•º6q• \KTÆA¨0ÆL/üÌ5]J›f•1AáærµU,PZS¶ÿ)”Ÿüh¬kýd¡¼¼¼¼ðpw',Lô¹®2Ì<Ôäz8;‰¤ΠG�1ZA¨|:s±Ëãżt5À,ZÌ…ò“׉޾Õ1UD|B:uq.MüRÓÒ ‹&Vм`p|Â9‹lÃyRõ§ˆT4¯ø Aá:—Ô±.¯/¦, ÃTÁñw'¹——ç­ŽA¸‹Õ©S�Ø#‡nuB2føàN�$Åé´iúÓ¢2&‚ Ü—Š]ë¢è+�ÄœåÚ„U‚p=1fL¨`f§·‚‚‚[ªP)Ìz^šê.¥M3œÆl6VpD‚ ‚`Íl6rÉÅ.оúZ˜õbvU¡bˆÊ˜P¡Ìf³Ó›Põ( \˜nˆ#Õp¦‚£AkÕÇHÑœp)­²¸ZG#ÜÍDeL¨XÎ7Œ U�9ÓO‚]J›.fU„»^£˜¼ôêk7ìŸê1&M|…�¿of⤩¼>s� \i×pâèÁR÷ÿ·ÿ={Ü[©e w‡‹.Niïk¨‰I­¨àh„»™¨Œ êúªXQQ¡¥‚VXTˆ§§u…L"šÆª,Ea(�O—a8M†!‘`ym˾Ĥ$d29M›·ªÀ+Çö­¿óðÐG�Js`ÿ^’Î] VÍÈJŠJ¸UNžN@£ÕÑ¿÷½¨5:þ;t”¨Z5�Ë+çÏïᣱÌè×µZM~A�UeËÖ1W�=ÆÌ7gs6) žö†><ˆž÷÷cðÃ1nìS–sÿþg/O=ó,{ÿÚ‰··w™yŽ{a<»ví WÏ|°x!yù´ïÔ€å+V²|ÅJêÔ‰fÃÚmÆh/–³IçJ½€£Æ�™•Å‹Ï�cÁÂ÷ÈÉËå½…óéÙ£»UŸ~ö9òøÝ»vaÏßÿ°`ÑbÎ�¿@@€?£Áã��tâÎ •-UsšÇ‹©Ô�p·³ù×Àl6“xîâ û½<Ý ²ü\¬Ö�›_€N§Ã]¥"Ðß¹\ l¼;]­\c2]�øÕd2S\T„‡ç5óž‹5:ª®,܃|Q›óœJf0H3œ´ªŒѽG°ò89²eË6ìüs[%#ÜJç.^¤[Çv¨”J¼½¼x°o/䲊ÿû¸~ý¦½þ& ±ì¿¿ß@Æ�}²Ìc¿ÿö³KåòÔ¸çhrÏ=,ûæ+¾úæ[Þœõ6u£ëP«V-’“S­Î?ñ��6+bK>þ(é¦x-_ovÿ¹•ÎÝ{1rÄp¦MžèPœ¶b‘H$e^Cóf1Ô¨QƒC‡óå×Kù|ÉÇÔ®uÿí=ÿÃÇŸ~΋Ï�£{×’¥]&O�AûvíXóý ~Zÿ3‹ßÿˆn�;‹ÖïÛÈ%ÍQ f½Óé<¤Hs‚Ä”öB…²9fìJ‹F€¿/ÕÂB,›¿¯¯åµFCjz&J77‚ü1�$§eX} î"f3˜Í¨ÕŒFã ãÃŒF#jµÚržY´ŒUY¦%Þ:×ZzÒ®7v§uM4»ð¿Ì;ìûL&­Žó—RXûËfÖý¶…ã§ã+¥¬ÀÆ ëhÖ¬)'Žäµ ¯ðâóãøý·Ÿmsķ˾£QL &NšjÙ·kÏ_2à�~øúxóÐÀؼu+Q5k’œ’ÂÅK—hÓ‚-[·qáÂEj–³2âÊ|O¶b±u ~~>Œù( ê×ÃM¡ÀÍÍÍ’wZZ:¯MžF‡vm÷ôXË~ƒÑÀ‰'X·á:wìÀï±ÛÌ%»(ú™¢0ª*8áng³2f2•üæS)ÝpW)-››ÛÕ¾²Ù9ùx{zèï‹—§a!A�F ŠŠ+7rá¶t¹Ž…J©ÄËË«ÔM¥TYÎs鯫pÇP†¹”.Ípš\ã�­òw 1«¨%/ŸÌf3r¹Œ�}{Ñ¥}kNœ—ÿ?;;‡¨ÚµHIIeÛÛQ(ü¹s—.&]»V¹®Ï•'¶b±u Jzn”u¯ŽÆÆRT\DâÙ$t:�eÿì7f¢Öj˜5ûz÷y€§Æ=Oq±ÚéØ…Ê‘¥K"ÙÅ.Šî¢‹¢P lvS4™KZ·¤’Òëlzƒ�V‹ÿå·GR©w%……Åøz—2aƒPÅ9ùÇRtS¬Ò$9�(üÝÑãÜ­©˜TÃ)üd®­Wvˉº•( ä2Q‘Ցɤø@^A!áa!ZÖK¯¾ÆÖm°bå*6mú�GFfff™ÇfN›ìRy¡!%³¥æäæ�÷öÎ3<Šª Àïöì¦÷FIB'téUºtD¥KA¤ ‚€4AåC± Hi*HïÒ‘* $!½g“lÿ~„,Ä$›d“М×g³3÷Ü9gfv™sï¹ç$g…#{x¸SÁߟ˜ØXŽ;Á �ýÙµëw<<Ý©S·–õZ‰%],Ùð$v¶¶yö]¿~}F Ìè±ãøiÝzF @‡v¯Ñ¡ÝkܽÂŽ]»Y½f-{þü“>½z––™E "㙆´"ËÉE¶H’Ý…E�§€0Ŭÿ'$¥ð "Šûá‰OH2�Néõz€\ ‘eRºGÇþ[Xž%ðòbHTâh°.<çß¡Š/yÍ|�5Š‘£Få=3ö¬(5ÊúzsãV:�žø„Dââpuv,X°ˆ,ûâsêÔ®Íï»¶sîô üʗ矫—˜9}ŠÅcÖÒ¬iSØýû’SRÙºíWºtêH€¿?ê´4®_¿Á¨áC‘+dܹLÅ Še£ƒƒ=©”��4 IIÉ.‰°¤‹% ƒ‹³-š7£Aƒúü°j ñ ‰DFFÑ¢u{~Z÷3~åËѤQ#lóqèž>V¦´w¡Æ$á> ”<®“ɤˆDàä`�£ƒ=)ijâ²F‘ô†ì™³œ³"±(k½�À�"Ö^C_~lÔÞVÉEéno)amžÿp˜ðát::�Ž NÈc@Bø¼¬Ô¬†V§cçÞœu25Jà…(›è˜Ž;Á?7o’™‘‰Ÿ_yêÕ­CÝ:u�ËeìÛGG½Z´ûSH\Óˆðß�ÁTôšºÊ>ÔQöàÈÑcôy³_žmÒÓÓY»v-‡"$$„V­Z1qâD||žÍï϶-éÞ³·ùóœ9s¹ÿà~Ž6åË•ç“Ofš?‹D"vnßFëV-óì399™¯–ÃáÃGèÞ­+cF�ÄÆ&kybRÝzôaÒÄ tëÚÅj½wîú�Uk~B.—3î½1´lÑÜ꾬!{–P\ÈïíÊUkðpw£{·®¥¬Yzƒ�ÄÄ$¼< ^xíæ�¬PE©™LfÞ¤RiŽM"‘ ‘H‹Å¹6‘H”cÌÿ(g6p:am‘å$")¢`Lp(¸±€@)ò›¦ôÑ?”ƒÙ#L¯×çpÆtz}‰¿Ä ¼¤<'­[µ y³¦�&FFòÇ{‘H$´iÝêY«V ñ ,\´„Ê•+Ñþµ×°³³%*:šcÇOP¯^ÖsBb"ñ³y�3ÄÛáTΟxqѳÈEè.SÍ&ÿeµZͨQ£°µµeìØ±(•J®^½Š]^ÆŸ"Ù¡Ü«V¯æþýû¹Žß¿Ÿïø�‘#Fä:–߯\Ett4³?™ÉêŸÖ²uÛv ÈrN—ý-µjÕ KçÂ…VåÅÍ›·>ê]–}¹„ÀêÕñõ±n6³8¬Zó¿ý¾‡]Ô�Êæad$âgôL ¼xdS M?g•¬«¨’àˆ ”Öƒœ£p™-b±Ù£:b …uz*eÖ(­Ñh"=#³Tb➊œíê9áuw÷ j•¬Ð”êÕªb0øûò•»yëb±˜ÆšëûÕ¦&íÚ¶5¿¬¾ýfßg©bV¨¢}Ñ�±}0áºKùß»w/qqq¬Y³™,+Ëk£Gk4ž%Ù߃¡ï¼ÃÐwÞ)°]AìýsÓ¦L¢u«ÄÅÅqèð èÇ©¿N³qóíÛƒ¤µ«Â#"èÙ½*•ª€ÖϳfLÖ*¼@„¨Ï™yÓ*YÛLëÊ´if¦­–ø„$\]œQÈe¤¥g�””‚£ƒR©�VCrjn.Îæ\�ˆŒŽC"£�ËINMC*‘ä›}Hàe籟Ÿ›••ÑÞôèïçdjì_è´:<=r†#ßåȱcݹK€¿-š7#°z5BBCÙ´y }z÷fÿþܽÂ‚Ïæ •Ê8rô(gÎ�ÇÙÉ)Ïìááì;p�  ;T¬@‡í)W¶ Z­–!ÃF2gÖLNýuš«×®óÞØÑøûùå�!"#3�FƒTúøEúÉYƒ•«VãîîA�n¯sôØq~øqu.=¾X²/Otz=ÇŽŸàüù‹è z5lHËÍs”´(*’d7Dö"«îw¸ö2�w¶ï¿ÿž˜±ç†NU_¹R%‚îÓ¤IcnQ¶l’““™ñÉl~6__Ë!™ij5ë~ÞÀ¡ÃGÐjµ´iÝŠw† ÂÉÑ‘]¿ýΰ£(P€£‡öç £�ŽŽáÇÕkØà ööö4nô*cÞ…³“&NÂÓÓgg'öí?€«‹+ƒö§y³¦fùˆˆ‡¬\½†ã'NâëãC—NéÓ»'R©”ÑcÞgÛö¸y•¡|¹r\<÷�†õ7±cçn’“SxµaF F•Ê•˜0qåÊ•cÂï³aÓf>˜�»ðù3'ñ÷óC«Õ²qó~ÿc/:�ŽîݺÒï­7±±Që¾¼8„ª­›!F–ê)dQ(5¤Q1”/ƒF£%#SCFF ¹ ¥BA¦RARJƒ™LЧ›+¶¶JsJ¼<ÜHJN!%5 ¥� ž.BøÈ•Gï &“ Ĺ2`2>ñ¢*z>œ±¸¸X‚î£×ë çà¡ÃŒ{¬ùø­ÛAÌ™7Ÿ¡ï ¦m›Ö„ÞÀg 1eÒDjת Àõ71·Ñ©C{Zµj‰�� ;wýÆ©Ógx£wO”J%AAwrœ7&6–Ùsæñö[}iÓº%÷î…0áb>›;ûG¡v_,[Nß>½¨_¿^ž¹ëvÕ®U“C‡�ò¿¯WÐ¥sGªU­j!΋ºuj³xá| z=ß~¿’Ê•+šÓ<ïܹ›ûгG7Äb1;vî&=#�î]_·úú¢qõ©Bœ¸èÃtWpTºäÚ¯Ñh8xð Õ«Wgß¾}\¼x‘zõê1bÄÚ´icµ®%AI2Œ3šƒÞaöœyV¯Æº5«XñÝT­Z…®¯w¶(k07þC4 އD"á»V2|ä»l\¿––Í›ñÅç‹øpÒŽ9ˆ��o¯ÜÏÙ‡“&ãììÌÂùó°Q(¸ø÷ߨ>1‹¶ÿà!ú¿ý&Žÿ€k×oгϛÜ÷uj×&66–.Ý{Ò½[W>_8Ÿøøxf}:�ðˆ>úpÞYëmRRRعû7&NOÍÕÍr¿îØinòÔ_4jÔ�¶�ú-W¶,»~û�›7oÑð• ÐÏbâ ;;;¦NžÈù —X÷óär={t§N�:æpâ'qttÄÑÑÑ|þ4µšžÝ»#‰ˆOH`Ç®Ý,øl.åËeý6´lÑŒW­¡s§ŽÅZ ªJ.ÎEwÆt¦LÒ”¹³*f× çÍ7ßd̘1\¿~�¶mÛrüøqš7º (ž¤¤‹8תYƒ£‡ö�¿_ynüs“µ?oàÀÞßIJN&11‘òåÊå9CxöÜyvÿöW/�ÇçÑZ0?¿òÔ¨]ŸS§þ¢u«–xyyàïgN ò$ƒ�sç/òé¬4nô*uëÖÉѦM«– {gÍš6áîÝ»lÙº�:µk³mûNÊ—+ÇÌéSÍ¥P ½û¾Mÿ~oáW¾<žžØÚÚR¥r%sŸ!¡¡ÔªH«–-‰DÔzâ»ö$îîg &ü²m;‰‰ILœ0‘HÄÇ‘,ýrÇï7=î÷V_&LœÌ˜Ñ£�Ëå…½ /(¡içЭ+¼m¯xNcx^r¼Y©Õ鸻º (F8’À›Ï ¤�Ö²èó(w zNŠ>÷}£=º½ŽÉdB«Õ|÷Ë–ƒ‹‹3•*UäÊÕ«tîÔ!‡L@@›·n#M�nÞ'“>þîÄÆÅ£ÕjññÎ=Ë�ÍýМ¿x1—#˜�˜dþ[Qˆ—E¹\NÓ&�hРׯßà·ß÷pôØqÞó.Ê|JââãY¿q3CÄÙÙ €øø¦}<3W{uš§b¤ä6F»aïìE*QE–MUÝ%Y…£ìñµÌNÒñÞ{ïѺu–3Û¬Y3ÂÂÂøùçŸ_0g¬àï�‹‹3..Τ¦¦2ã“ÙÌŸ;›»÷î1tø(RRSéúzg–/û2WˆxðÝ{4|¥�Ùðòô¤i“Æß½—oÇ'‘H$Ìžù1S¦Í ,,œ.�;˜ ´VÍš¬ZóW®\¥u«9jRV«Z€Âòœ‰èÓ«'ÃF½Ë¸ y£w/š4n”«®å“„GD0sö-˜gv0³×õlÓ>Wû¤¤ä\~^.’uQ„¤Ÿ±JÖQ\b=KX#�œäøWÍEHº!P\òxÕë-¥4¾Æ›D" …‚ÀêÕxµá+Ü ¢Rż •f‡Rý»Î^6ÙÅH-½˜+m•tìÐŽ6�œ‰lT*e>–QÈåÔ¯W—J+ðáGS¸vý _©Ÿ‡n&~ݾ“�ÕiØàññ왕Ó§âà�3s”m13šÔrÒ+�ª*º3–)O 4ýµ»™÷©T*6lÈÇs´õõõåäÉ“Åҵؔâcýí÷+©@§ŽhѺ˾ZJófM=æ}>]£Õærøär9r…‚ü¼‚K_æüù ¼Ñ§WŽ—aWWär9b±(‡>e|}ò y,*Òx/¤XÒª>—ëyyç�wØ·o_ûoÞ¼I�:uþ-þTÉ®™Uè­�߃ó.²rÕ¦O›LFF&!¡¡T­R'GGªV­BäÃÈ\2þþ~œ9{ލèÇÏal\/ýM¥J‹dWÙ²e5b8¿nÝĺõ9}úl¾mÿ¾|Ù\«¬fÍ?y óãAw‚Q*•”/WμO§ÓåêÇÁÁ�7z÷bãÏkQ*•üòëö<Ï·oÿA~ÿ}Ó¦NÊ‘YÒ×Ç¥R‰X,¦JåJ9¶¼ÖŸ ¼<˜0Y�¸C&R¡H = ”>B10�’¥ˆáY¦ç$µ}v“É„F£áÚõ\¼x‰n¯gÑíÞ­Ÿ/ýüüüç—_·3uòÄ|ûtvr¢{·®lÚ²xBBCs´iÒ¨¿ÿ±—_wì¤n�:�F‚ƒïR·níB;dûöäÞ½{Ô«[WWW2228qêÞÞ^T”íñI’SRX¿a#­[·Â`0ð02ëÞÉÑ{{ÞèÕ“uë7ñFï^¸¸8“��HLl Ú½V¸‹i­'½k IDAT}Œ=.ž‰‘_/²lHú9BÓÏá¯zÕ¼¯W¯^¬ZµŠ¥K—Òºuk._¾Ì×_Íßÿ]l]‹C¶sµjÕjΟ?Ÿg›F�1dÈ`³DA¤¥¥ñÉì9ÌŸû)åË•Ãh4Ò¦uK¶lÝÆ« _áС#ÌŸ÷i.¹Æ�^¥c‡öLûx&£GŽ@"‘°òÇÕ¼Ö¶5M¾ Àšµë¨X¡..Îܺ`Núð×é3;~Wgþ:s–?þØËþ½¿Yá†ß~¿’EŸ/¥s§Ž$&&1gÞg̘>Å&hggǵë78rô8z½Ž¶mZsðÐaL&(SƇøøBCйSîzj±qqÌød6öG¯Ó|w\]]˜:i"2›i“'áããÅÇQÜpŸáCó/; ðâš~ÎêÄîÆj℈1�ÒGpÆž"‘“ÑôÌ ôBΞT©R™Y3?6‡(V¯ÆÔÉ9zììÝG劘1m ó a̦G÷®`2±ì_ã[Æ—õë™ë™8992kætöí?ÀÊW£R©ð÷£0kˆ²yµa$b1W®\ã^HÞÞ^Ô®U‹&�åBvåÊUbbcùcÏ^þس׼êäIÔªHÇŽíqptäðÑ£„‡…ãçWžZ5óNœ` 6)eÁ­èÎ@ˆúlgÌÃÃ͛7óÃ?0tèP4hÀùóçŸùÌXö Ä°¡ïEXXXŽÃeË–eÈàA�/ 1ˆ±rÕ|||èÙ#+ Q,óùÂ,ü| /aøÐ!4mÒ8—œT*åÛ¯—±ú§uÌ›¿‰DBûv¯1x`ÿB×&ÓéthµZ¾ûa%7þ¹I�À@6¬û)GB gg'vìÜÉé3çxõÕWصc›ùûáååÉžßv°òÇÕ|0a"þ|ðþ{tëÚÅ,ßîµ¶tëÚ…ÙsæÒ´Ic^iPƒÁÈÎ]»¹|å*î¼3d oôî•K¿C‡ŽpÿÁ¾Yñ߬øÎ¼ÿ—Íiݪ£FÇÝÝ�uë7pëÖmjÖ ¤M«V…²½8hµ:výy‰X‚Dòxkߺ9eËX.G P|BÔùÏÞ„­Úÿ9�ÝxÙmÞ°NxÖJ„ËW®R§N½B¶6&ÃUîWz “m[6Ò¡c§û&O�FJr Ž,^¸ Çq±XÌÞ=*™ÆóÈ„‰“puueÆô©ÏZ•RCo0�˜˜Tèß*€k7ï “É�J¥Èd24Z'Ï^¤kǶØ(H¥R¤R©ÙI‹Å¹6‘H”cƒÇëUò'^Ê·“ª�-²¬«¸".wÚcÒ ÿn ”>ÒËW®>k^"._¾Tè¶vv¶¨T¶Ez¹yQ±Ç˜tlSË“äPxg,›T},!ê³Ïµ3¹“¶,Z0ŸqŒ7ÿýïã%� _àùäÌ…ËÔ«hÎ0+ Pz„¨ÏZåˆ8dT1�§†´VÍÏZ�ÿ’8/”ŽŽd˜’‹,’~ŽÚNÝ�‰¬Ë8ùTÈùúßW_æ{¬¨k-^<""³¨øúi²J�)Ãêµb*± ²D_Œ7(žŸ8.�ÿ †DŽ™EËä—MDƵb­x¼,YEJŽnß¡zëžy�¢¢>KDÆ5«d] U1&=ûµìÿ„9X�g‚"± x]´Jöžú,•íZ•¬B%HQÃ_ô0Å/—~þ¬Ux®IU§“ªNÇËS(0- ð4¸WŒ;eŠŸ0+&ðTfÆž ú‡N¸*ª­TšsÜ(T}–ðŒ+¥¡VÉ`*ü&~”UÔÅÅåYi+PÊDÇÄâáæ’o�x�’#<ã ¡V:c¢@ŒQ®%¬‘€€egL@@àÙ`¡J-[¨¦±˜ôtµùs¦1�Û©GKI±â#‰ ¹‰0�\¿qGG¡žÍËŠZ�ŽJù¯qx‰¸�z”LcšU²ö`Mž.B˜¢€€À³#Æ;'wÒ°œñJ.W ÕjHMMA¯×p*~+a§ÕØeú=E‹Æ�û ÝÖÅÅGGüÊ—+E�ž%µkTC&“=k5^zî«/¤>f•¬ƒØ Iœ·¢(ðÔœ1�g†1U�CFEÒ”§–˹ö9»khfûbÖæ(Y‚ÔÇЬ›sÖUØfS Œ¦( ðL‘&x#±r\(D{š0Ýß%¬‘€€€€À‹Æ=õn§±JV‚ EráÂæJÁx¦¢pÑU±JVoÒqOûW k$ ð¢q;íz“Ö*Yj`Œq*a� ‡àŒ ¼P¼è)ÀòF™býz©ÍB´ÏwÝ1�Ò#(í8A©Ö­°S” 6ECpÆ^,L`4 Ëk_6ô÷Ýp×Z%kÂDˆö F“¾„µxÞ1šô¥Ådeê oQ]Œá^%¬•€@á‘„[�“É„ÉdB«ÑbÐ /Ý/"“e¢¸ß°Jþ¾öÔÃ!ÌŠ <[¤ÎNB¡Q�çe|¯[È=û™©MÇ`4`Ìgbô'H¾­Ä9µF)k)ð¢£t';'ªúÕ~Öªƒ ´£ÜS[·fX‚ »äÊB]1�gŽPgL@@à¹Aá‚»k ¢d—sìÏÔ¦£#‘�±…¥®jïãxy&! vòGGQ+` QֳͯVç… [¯7:h =zt+v_«×®cÉÒ¯øjébÚ·{­PÇŠÃÄÉÓxwÔ*VÈ™¨¡Q³Vl^¿?¿ò%ržÿ¥u�ŠJ¦1•Û©G­–÷6ÕãÓ÷WÑ®Mk5zµä+aÂÂ#Xûó¢cb‹Dôé݃æM›(·jÍZ:w쀷wΙ¿�&OcòGððð(-•K”¹óæy�ò³ÏZžåuœ1�ç yœRï[èÉ4ï3 ˆì5€c³fÙ$fD‘„«ƒçSÐTàEE�’zÁ$-Sê犊ŽãŸ;wHÏÐ`gkK­ÀjT«\¡ÄÏ£NOç•FYk&Å îîôèþ:ãÆŽ)‘þwoÿ¥Dú:xCbÐÐE:VîÞ»W¢ýåÇí ;¼=`—Î�2ïë?h(oôêA�ÝømϾýv%âèèHŸÞ=7vŒÕ÷ïI9777ªV©Ì„Þ§ZUëÊ…†’¾GOÚàääD“Æ�™3k*•2_¹Û©Gx�qɪsÊD*TI™9ýùu²9zì8åÊ–aòÄñˆÅâB'1‹ŒŠ*eͳöç 888г{×ï{æô©yîšöYâÜ… ìÙ³�ø„lmU4mÒ˜®]:©Áx®0£Ĉ-:bÙ¤¨pP:#•³cy“I2*\Ɉ•ú¹Î^ºLÃúuð+ëKš:�#§ÎR¾Œö¥r¾Ó'�ag«âÖíÛ 2‚vmÛR­jΞ;ÏœÏNùre™?÷SjV èΦϜÍ�à»xzz0mòG´nÙ€¹ó±ióæÏ™�cf,$4”Oç.àêµë¸º:3fÔzöèže·…óYÖm¿²gï>Ö®úÁ¼ïó/¾"55�9³fX”mѦ=qqqtëÙ'ës‹f|÷õÿÌÇ÷<Äú�›ÉÔdòþØwØïmîÞ á“Ùs¹ñÏM<==XøÙêÖ±>´U�žÎÇ3góʯ©W¯.÷î…ðþøiÞ´)•+Wò¿ñ×ñ#ddf²~Ã&Þ>Š=¿íÄÅÙ‰à»÷˜õéüˆu60zÄð<û¬\³,wÒŽ£Ñj‰ŒŒ$##©D‚»»NNYõÂ4 “Édøúø˜�»�¿4,údƒú÷3Ϻ9vœsç/0å£Ízmؼ½NÏà�ýˆŒŠfÃÆÍ< ÃÉɉ!ûàÀï{ö¢Õh‘)d>|ƒÁÈì™ãââlÑîÈÈ(6lÚBxDÎNNôêÙ�š5jð ,Œ…‹—˜Û�9›Ž9fÔ(jÔ°ü]šúñLRRR˜;���Œ=Ò|üÒå+=v­NG×.�ÍßwKöYK~öß½Çú ›HNN¦cÇö?qŠqk§‡›·nãø‰@Î{T�}–lˆŽ‰aÓæ_ ½�ƒƒ�;v ñýæw]ò#S£áçõyïÝÑT¨@Tt4߯\E`µjøóᤩtêÐŽ‡²Š‘wìÐŽ6­Zæº.Bj{�çyl9";«dÕ™©$§%`2!l–ÿFé;bJ…"ë„€H$B*•"••î8¨ÉdB«Õ¡PÈquu`Ñ’/è߯/çN£i“Æ|þÅWæöË–¯ [×.œ?}‚…óæ°hñR²k:Μ>…®^¢E‹Ü™Jßûà#êׯÃñ#ûYºx!‹–|ÉýažÏzvïFȽP‚îÜ@«Ó±s×oô{«o�²ÇïÇÙÙ™Ý;¶ñÏÕK91€ /±ó×­,]¼�%K¿B«Í*g'š7m‚ÂFATt ÂÂyö€qï�A¥RòZÛÖøúzsá‚uaqnn®Œ{ï]Þ0ˆ£Ç²zí:bŸx„üï_Q¬^�ÐÐDFFÊ{cÞÅV¥âµ¶­)_¾/>¶!/Û“’’ ½ŸA`ggÇÄñïc*Dh\q¯Yãf- ¬]ŸÞoö£I£F¼õFŸ<û,[Å…Óv ÕjÑjµ¸»¹!‹Q*•T­R¹\n>æááŽX,ÆÁÞ™\†:=]mŒ1Nyê`ogKÕ*U8{îW¯_C©RR©REbãâˆ��¥k—Î(rêÔ®…«‹ w‚ï ´±!5-��ýûáèèˆ\.G,¶üš�˜˜Htt4]:wÆF¡ NíZx¸»ü¨ÏÒ¢CûvØÛÙX­2¹ŒÄ¤¤í³Kö©Õj¢cbhݪ666Y!ަâ¥S±dCLL,qññtjßüüʳdñ<ÜÝ,^K88ØÓíõÎ,Zò%Ë¿ù–‡“œœ’£Ík¯µÁÖ6ë¹òööæ^hh®ë"„) <—ÈcË¡,› Ó£6“‰,—¬`24i¤¨ãqµ÷.=^hL¢ÒwÆŒF#g.^¡nÍ@üÊ•!M­æ¯ —ñôpÇ©”Jtœ>y {;[î?xÀäi3�H$ôëM6nÙÊOëÖ‹V«Á××Ç,óñÔÉ|ýíwŒ;D0öÝQôíÝËây“’p´w@–Ï,Ÿ¥óYKÿ·Þ¤ãë=HLJfËÖm¼õÖÅîÀÍÕÕü·L*Ãh0�¦VcÐë©S?皢¨èâ­S5|ƒú÷ãÜù‹8t˜ÎÝ{ñÓ�ßá÷(ü/¿ûW †¬0qññ8Ú;æ¸G®..Ä%Ä›?çe{rJ J¥Ò,ggg‡ÊÖ¶À󦤤ëš�>yŒkׯ3ëÓy|ðþ¤Riž}–i‘N‚Á¹A�D"ÉsàÀ`0b2™øçæ­ûMér”0XÐãÕ†¯°û÷=tíÒ™ÓgÎѰaÒÓ30 Œ›01GûìvbÜÜÜP*ó_çöoRRRQ)m‘J×9³··'%5µÐ}XƒãaÒR‰“ÑHF¦Æ¢}Ö`É>uz ¹Ü|ÌÆÆ…ÂÆês�å{¤P(°U*sèòoòº.ѱ}VèaPP0_¹Ê§óæ3~ÜXʕ͘r°ü[¯RªHOOÏu]gL@@à¹Dk�³{ ÊÓVɧ¤' R8`#S•°f/¥ïŒ¥¦©Ñëõ”+ã�D"ÆÍÕG{bââJÍ‹ÅøûùÑ¥SŽ?I›–-Y°h +¿ýšºujsùÊUfÌúÔÜÞÉɑӦ0cÚΞ¿ÀØ÷ÇÓ¼iS¼½òO„ãîæFrj Z�.×L_dd”ÅóY‹½½=]:wbÞü…Ü £S‡öÅî3?<<ܱ±Qrá̉g7žÄÖV…F«Ád2™�„”ÔT쟸ßJ¥’–-šÑòQèçŽ]¿1áƒ÷ÍÇÿ}ÿŠêŒÝøç&ƒ¼�›«+I)É9îQLlžd‹³³³#33ƒÁ€D"A�žNººàš´Ö^³'iÖ¤1eÊø²ú§µŒ9"WŸÑš Æ|A¬æ:�ƒÁ�ãZg#“I‹Å¹ÖÛ•×·Ælù»W§v-6lÚÂÍ›·¹uë6oöɘprt@&SðÕÒEùÎËåE›õvp°G�žõ;!•f½’'%§ÞXƾ¢bÉ>¥� Z�£ÑˆX,&S£A£É, GËX²!>>uFF]J ¹\N�ÕÍ!¤§Ïœ5;ci©©ðè·4M­ÆV¥Êu]„0E�ÿ<_~µœ~‡˜·™³æpàà!tň“ε:�~‡pýÆ?%Þw¦FÃá£Çønå�|4yË¿þ–ƒ‡��ššV(ùã'N2þÃI%®Wq�Dù¢âñÈ-¦Âo™Ú ’Rc0MÂ&l¹·§¦¨R*1 „EDb4‰OH"!! '‡Ò-\o2™ˆˆxÈŸûR¥Je“’�J¤x{{ϺõILJ6¯ÛèýfÖL�Á`ÀÓÝ zóõõÁßÏ�ß~OZZ.^¢qóÖܽRàùŠÃ;ƒ°ÿà!zvïZ¤pO•Jɵk×0 DFEØÞÃÝ� üùæ»ïIOÏàÊÕktéÞ‡¤Gë­òÃÛË G¶þº�Fé¿ÎðàÁªU©ÂÁCGèÑç-‚ïÞC¯×ÎõÿPîQhg6ÿ¾…%>>ž/—-'1)‘Î�:âííE€¿ß}¿’ŒŒ ö8HTT¯Ô¯g±Wg<ÜÝÙüË6Ôéé|µìkD…p®¬½fÿæ£ ðãêµÄÆÆåêóÀí5œþç ƒ™L†B!'66£ÑHzz:·n¡Ñh�J¥(rbËÈÈàŸ‹a N[.•J©S»6¿üº?¿ò¸¹e…±9::âííÉï{÷¢Ñh³ØÌ[€ºϵ³³3^^žìýs?Z­–K—/“”˜H劭î3…BAèýû�F žÝzÚöÙÛÛáäàÀñ“§ÈÔhؽûwÞ-ÉË>K6¸ººàéáÁ{ÿ$33“;ÁwùhÊôBýäÇå+W™·`1‘‘Q âââ¸ÿàîO„>zuz·nß&::š r]afL@hݪÍ›5Åh4ñ02’?þØ‹D"¡MëVÏZµBÇÊÕkPÈå¼Ò >M7&11‘“§NQ¯Ù¿ž5Æ;œ=ªM¨UòI (dv8ª\ n,ðŸâi,“ɤ4jP—›AÁ\¼z;[[êÔ¬Ž‹sÞëUJ‚ÆÍZYéÁÛ´nÅØÑ#‘Ëå´jÙœî½úâëëËœYÌÈÑï±ñç5Løà}>�ûaḺºòîèxyz|÷ž9!Àñã'™þÉlzõìÁ¼O?aÙŸ3uúLV¯]‡‡»;“&ާB€?&“)ßóMš8žþƒÞ1÷yáBVÖÔýûÑ©C»|�MŸò6J%R‰”·ß,ZˆâàAXðùRfÏ[@•ÊÙºq}�2K-`Ƭ9¬ùi=®n.Œ=ªÀ ‰DÂÂÏæ°pñRæ/üW>™1 o¼¼<¹~ãÃG�!!!'G':´oG¿7û¢y”t"¯ûW7k‰L.§~ݺü¼f …ÂlÃÌYsøiÝÊ”ñá«¥‹Í‰1,1óã©Ìžû_.ûšáCã`o�ÂÆ†¿/_±x�¬¹fÿ¦f�@š4nÄ˾fÁ¼Ùæ>w_úžŠ½RñôpC"É ï*[¦ ‰‹B*•âåéa¶½Œ¯/#‰�O@*• h�M¦'‘‘Qæ,|7nÜ`݆�4nÔ˜�ýß Q£W8{î,Ú¾�C·¡C³~Ã&<Šƒƒ]:u¶!œù!‰:dë7læà᣸¹¹0bØ;ØØ/\ M›Vü²}·üBo¦LšX�H±ì;pð 4îÞ­+Ú½fѾ7ßìæÍ[ÙµûwÚ·k‹J©D*“êågŸ%FÊÚuë9xè0ŽŽŽôêÙÝb@AÔ®U“ûð¿ß’–šŠ­­-õêÖ¥eóÇ �<<<˜=gz½ž¾oôÂî‘.O^Ñæ ëžÎ*f�§DbR2vv¶H &ñåWËñð§G·×Íûö8Èõë7˜8áƒÕM­NgÄè1LŸ:¹XižŸÄh4²|Åw¸¹ºÐ·OodOŒ�&ÄâÂ…?q’í;vñÕŸ—ˆ^%…Ä9ƒ û_Ð{F`(Djû£”ÛáéP¹¤øÿ¸ ¼<ØŠÝ0îmNëGi† Bo0�˜˜„—gá ‚^»y™L†T*E&“™7©Tšc“H$H$Äbq®M$娀 !zÑÐh4Ìšó™™|õÅâg­Î“ÉÄÙsç™8y'�|fÏ^¢.Œ1_ò0ãºUòŽb<v˜d½ã$Pz˜L&‚‚î°ê§µ,š?ï¥ù�+l!iafL@ tZ]®¸úàà»9vŒ ;w ð÷£EófVÏÊøʦÍ[èÓ»7û÷àî½|6©TÆ‘£G9sî<ÎNNT¬�»Økxxû$(è+СC{Ê•-ƒV«eȰ‘Ì™5“S�æêµë¼7vt®:/7þ¹Åµ«×X²xaG Èáˆ�&Î�?ÏÙóˆ|IõjUißþ5¼<󲯿’Ñ{C¢qš/&Ï«ä3´i¤¤ÇãjWü/O+›¢@É`0hؤÕ«Uã_.)X@ XìÝ·ŸÅK¿$.._oo|6百 ßHÙgµ#à’YSpÄžC.^ú›_wì"%%W ðÒ8bEApÆ€¸¸X‚î›ãø:̸÷Çš�ߺÄœyóúÎ`Ú¶iMèý|¶`S&M¤v­š\¿qƒq�:´§U«–ØØØ°s×oœ:}†7z÷D©Tt'Çycbc™=go¿Õ—6­[rï^ó.æ³¹³±JòŲåôíÓ‹úõëáåé•K÷ððp«W/0dÿ�ƒ8xˆ·ßzg'G.\¼Ä'³ç2Þ§92j׿Ò@{ßY%SJÁ�ó 9=™L…�üé/Šx>1šžV¥1�’@"‘påâÙg­Æ†NÚ—j‚”¢¬>Åõä=VË;‰Ë!�+g1ƒ¢À³¡~½ºÔ¯W÷Y«Qj,Y¼ àFΘ€‡�ãð‘c9ö………ãïW‘HĮݿӧwO^kÓ€ÒRSÙ¾s—Ù1öÎ`|¼³Ò©§¤¤°s÷oLœ0žš�2ìøûóëŽ�æö'OýE£F iû¨ßre˲ë·ß¹yó _ÉJ§;h@?½Ú0_Ý£c¢qs·¼&*%5•Í[aÂï›õõ÷÷ãnH'OþE�î]sÉXcsi¡‰—!Ku;ëœ1ƒÉ@ª:‰-‘ð³'�•çEpÆžoÔ†n$ï%ÓX¸DTyá’YCJáÓÍ ™ôq˜`l\RÈåuwwsçNp°Å6±±qhµÚY»Äb15ó”ÍÔh¬²¹4ɈP ©¨Â I/¸q¨u)(2âqR~Í�ÀKŒ¦( ðÜs#åOBÒÏY-ï.ªŽ$ªìS(d! `=‚3& ð"‘…BA`õj¼ÚðnQ©bîu^ÙmÄùÄ7 ´´6Ei«¤c‡v´iÝ:Ç~•ªð£xeÊøòÛ{HIMÅÁÞ>ïF�týk.@$"ïD'ùè\�Í¥‰N-ƯD잉‰‚ 1æEjf ‰iÁÙÄ^nL´˜€ÀsMXÆßÜHÙkµ¼L¤Ä)¥&¦tE j% PòuÆòÀd2‘”œŒ666Ô¬FHHHŽ6¡¡÷©^­Z¾Ž“›[Vè ¥åÊ–%6._oÊøú˜7gçBëX½:•*VàÏ}ûÑësFÅg;‚Ùº„…‡›��&nÞ¼E… �÷™²œkm.ML&©÷ň2¬w¤t)™q�Y…B…í¿½ <ŸèŒ™\KÞC²ÎúPÞú†˜"¬O[. ð´fÆxœÀÃd2¡Ñh¸vý/^¢Ûë]èÞ­Ÿ/ýüüüç—_·3urþu;œ�œèÞ­+›¶lÞÀÁÁ��ÐÐmš4jÄïìå×;©[§F£‘àà»Ô­[»Ð™T*¡¿·ùaå*¾ùö;Ô¯�““#IIIœ8uŠ‘Ã‡áâìL¿·Þdý†M¼ýV_¸té2áŒ> •Ê–¸¸xnüs“Š+Xes©b2a2Az¸Û 6Ä™Vu£Ö¦ �Äc/w+a^$L"Áx^¹‘º— ´c7ÌWql"+YC! ðtœ1r&ððôð J•ÊÌšù±9D1°z5¦NžÈÑc'øcï>*W¬ÀŒiS¨˜Oc6=ºw“‰eÿûß2¾4¨_�ªUª˜�;992kætöí?ÀÊW£R©ð÷£¨©¼<=˜4q§þú‹+W¯rçN0åÊ–å•úõ‘J³¾æ�;uÀÉÉ‘CGŽG�5˜5ãcsÆZ5éÖµ k~ZÇûï�±ÚæÒÂdÊÚ2E(•ˆ\­sÆR5‰ÈĶ($¢îÿ*ÂĘ€ÀóITæm®'[žࢮ�1YUB ”.BÑg�—Ž¢}~ètzd2aìÃZ®^»Ž.ð ™Ædó>—š”Ögز“;ãbã[ê ¼€ØIÜQoûR}nÔ¬›×¯Åϯ|¡u|’n½Þ`è �ôèÑÍ*ykiи9¿mÿo ÉŽ²™8yïŽAÅ'«‹KIôY,s(ö+®#•}c#l‚g ž;!íÚ´¦Q£WKB=‹¬Z³–Î;ë9˜ðÑfNŸŠ‹Ká—+äÇÓ´½$(IÛK’µ?oÀÇÇ›vmÛ”XŸO„Þž‚#V|Lä\ç“¥@Y^ƒA¬³ª¿4m2‘;Ùóõ£/ðt0‰ŸÎ8d\|"×o¡ÎÈÀÉÁ‘Æ ëãíéþTÎ]TvoÿåY«P wïÝ{!ú°Ž)û¸žü§ÕòŽbTqUs„'Μ>µøŠ’Ȩ¨§v®Âð4m°á Q@@àÅàÑš±lÔÑ"ä*pMÎ_Ær‡¨uIÈÅJdb!ÛÖ�§‘ÀÃh2qúÂßÔ©Y�¿rDÅÄqâô9útï\jçÜóç>6nÞŠÑdâÝQ#Øï-îÞ á“Ùs¹ñÏM<==XøÙêÖ© ÀÜù‹Ø´y óçÌÎ13öJ“|ôá8V|»’ôŒ Þ9Œ¡CpñÒßÌœ5‡˜˜F�Îæ­Ûøñûoð÷ó³¨ãÙsç™1ëSÔét{=çµ°¤g‹6퉋‹£[Ï>YŸ[4㻯ÿW \Hh(ŸÎ]ÀÕk×quuf̨ôìѽX}Z²¡ Ûç|¶ˆðˆpÊ—+Ëü¹ŸR#0«eÐ�;LŸ9›;Áwñôô`Úä�hݲE�}߽ǬOçqëöm¼<=™4q<­ÉYº–ìË� ›·pîÜÎ�¿@«–-ÉHO篳gXõý·Ô¬hQ—ÏÜ6mÙ– kWãçWž ;wødÉDµ¯â`ÄËÓ {û¬dM�†‡‘‘ddd"“ÉðõñÉ?qVz=ŒqlÞº�ã'N0¨¿³CNšJÏ]Ù³g­–NÛÑ®m[ó±NÚqàЀ¬ŒÇ�fПœÍ˜<íc>šðLýx&)))Ì�ŸUè700�±£GxÿnÝaýÆMdj4¼ú¨¾h6‘QÑlظ™aa8991d`ü9rì8çÎ_`ÊG>qo¶¢×é<°ŸEÛ£cbØ´ùBBïãà`GçŽhüèx~ç+Œ ›·n#.>w7 èOùG¥t,]kK¶["22Š ›¶�³“½zv£f�žÏZûžäÞ½V®^ÃÄ àæêšoŸ'NýÅ…‹—˜0î=³ìö�»ÈÈȤÿÛopáâ%Ž?)dSx1È^3öä–*Eœ©ÊªàkŦ5¤£Ö&a4"lÿ±íQâÐR%##ƒÑ€¹2H%üË•A&•ŸXjç¼rõ¿íü•% ç³dé—ÄÆÆ0nÂ$êÖ­Í_Ç3éÃñ¼÷Á‡huY³Ê3§O៫—hÑ¢Y®þ$b1‡eǶ-|ñùB–-_�V«à“OçÑ»WwŽÚÇÃÈ(¢¢£s•ÏÈ‹™ŸÎå­¾}8°÷7r9éjµù˜%=�Þ�³³3»wl㟫—ÌNSArï}ðõë×áø‘ý,]¼�EK¾äþƒ°bõiÉK,ZòýûõåÜ©c4mҘϿøÊ|lÙòtëÚ…ó§O°pÞ-^Zà �Édbâä©4¨_—ãG0áƒ÷ùhÊt“²©,Ý?Köå‡D"áʵklÞ°Ž={÷ÒðÕ¼Þ¹3.PK,[¾‚†½¿Ú�è¨h">@¯×sæì9Z>ñ;zŸOfLœ1�ƒ,,gjr½2"ˆLký1Ô†$ÔºÄgžf]ØžþVÚ(mˆ�Ñhäat iéd¤g”Ú9€³“#M¿Š¿¿W¯_çAX8Â0î½1¨TJ^kÛ__o.\¸T¨>GŠ‹³Í›6Aa£ *:†¤¤dBïßgЀØÙÙ1qüû˜Œ{¸1±±<|É °U©=r¸ù˜µzZ’»ÿááŒ1;[[jÕ¬Á™“GÍ£öÖôiɆ‚ؾuýÞì‹B¡ ]Û6<ŒŒ4S©T\¸p‘»wïQ¯nþücW¾ë³‰ŒŒ"$4”÷Ƽ‹­JÅkm[S¾|9.^||ÍòºÅy&êÔ¬Eùreq°w Y“&T¨àORRr¡tÉe¥8ÂŧÑh4¨T*sò,­V‹V«ÅÃñXŒƒ½=2¹ uzzy•ÈûÄê˜t’Ï•M‡öí°·³%°Z5dr‰IIæc¯½Ö[[[ªV©‚··7÷þ• ¹¤HNN&!!‘Ö­Za£PЩc󱨏8bccéÚ¥3 …œ:µkáêâÂ�à»ØÛeévö\VAì«×¯¡T)©T©¢ÅóÅÄÄO§ö°±±ÁϯöñÔÉ|ýíwŒ;D0öÝQôíÝËbqññ8Ú;æ°ÏÕÅ…¸„xóç¼î_šZmõ3¡|"(‹°±Q ‘H0�…Ò%/Â3®ØYÁ­H“yÆÒÃÝgg' #&“‰nÞÊ!£ÿ× ž§ö Ñ.êþ$Žöæ¿¥IŽÁ{óß*¥Šô9%…Z�ŽR¡@úè7A.—#•ÊHOÏÀh40nBÎr2ÙNã« _a÷ï{èÚ¥3§Ïœ£aÂÃüÒÔjl•Jóùž¤ óYâØ‰“<|„ä¤dôz®O€ƒ‡�àãóø;[Ð5kÕ¢9³æÌ#M­æÄÉS´hÑ4G;GǬçLpÆž{¤*#zC& ±-Cîp ÔP)ŽÕm0É­›qÐ3I×' ‘È�ˆ„ŸÅ—•Ô ­^�ÜÑPpãbâéᆯ�2™ ‰DÂî?aoWzNÒ/‰II89;ãááŽ��’ gN .¡,³vvvdffb0�H$¨ÓÓ ªgï`OzF:z½©TJjj*Z­Àj=-ÉED<$95­N‡\Vð‹^aúŒŠŽÎ×KDFF±`ÑV~û5uëÔæò•«Ì˜õ©ù¸““#3¦MaÆ´)œ=�±ï�§yÓ¦x{å_¸ØÍÕ•¤”äöÅÄÆáéa9ëgi<é"–HÐ= Óéô¤¤¦’aJänòn’MðöòÂÛË µ:�aaØÙe9¼b±˜jU«ä{^Q ²È€­)––š �®{šZ�­*ë;+’ˆÑ²~7ôzéÅœåVª”h´ó÷(##½>ËÑtrt@&SðÕÒEyÎ�Ö©]‹ ›¶póæmnݺ͛},;32ÌÏî“t¾üHHHä—mÛyÌhü¹Êú�› ”³d»%ìQ§«sØ�”œb.Ó“ÖÚ—MËæÍð+_žµÿgï<ã*ºünMÝô^ �zï)*Ò¤‹¢ØAŠ"Uš¨ Ÿv’ФW©Jï�Ò{²Iv³åû‘°HvÃfC‘ûò\ždïÌ™3s'{ï¹sæœå+˜C«Íï3€ÿÞ·ŸÂÂBaeL@@àñ§šwúDP&x¢É/û�Ow ìZ& ò»bv;yúÒ½qÉ1QLàÉFî¨ÅÙÑ•:AU�ë…ÔâôùKœ:w Ow7Ú´jVemiu:ê„ÔâÅ>ýP«ÔLšø®%ùz̛çSgð˯+pusá�ÑoâääHÔõhC$A€ýû2ù³iôéÝ‹YÓ?3ÚÞ”O&2mæç|½è¼>r8 VÖÖFëˆD"¦Où„i3góÍ7ß2|øœ��Ñê´Fõ¼ÍðaC˜óÅ—L›5‡Z5k°:|…Éz‹¾ú‚‰“§ðóoËñpwgüG|W5sdëCy„Ô®EX‡v¼Ô§?¾¾¾Ì˜ú W£¢xcô»„ÿþ c>x�é3?'îV<®®®¼5z^žå¯ŠÝÏóæ0eê ~]¾??~9{{{£õ*2ÖŠ)]Þ}ëMf|>—¥Ë~dÈàWíjÍ•¢�È­¤xzx�˜˜ˆº¨©T‚»›;²’Õ5?__n%$’žža8'‘»¨ùhš£O¸Œ“ Q .\¸Àò•á´jÙŠ¡ƒšì‹‡‡ÓfÌB£Ñп_ìK\p_èÑ�ˆUkÙ¾caaíqwu1¬”têÆš?Ö¾j ~>ÞLÿQÙ Ü5fƒö'"r5›7o¥Sç0ìííÑ•D9b8+VF°{÷^ìy¾{wìîrnÙ²GŽaHçW*Ü÷7^ÉoËW°û¯=8::Ò§÷K†ÕWSí•…ŸŸ/õê†2kö\\]]üÊX¼d)ãÇ~hvß�Õ9b+VF²{Ï^ÜÜ\õÚ«X›øî1·÷2°ßËÌü|. ê×£v­Z&eÊ­äH$Ú·+ I§×ãïçËŒÏç IŸþ{<ŽIŸÇr‚“%Š5[†£Ä‹æ6Cñ“ ™Àž„¤Ï� ½^Ï‘£ÇøèãIÜ»û±ÓOàñ#FyŒ}iß‘Yo¶ Wq \ãÃÐåXÖí÷ÞðõæRTTDxäjÔ*5£^µÜrÂʘ€€Àm¶ ŠÌÚd¹˜oŒek“¸¤Ú‰ƒÔ±·µøo±}ç.æù5iiéøz{3çó‚!&`’¬¢xNgo¨”!"œóêYܰ:�ޱã'RÍߟ7^i´¬è»o +cO:��N[ì cU~ä*aeL@[ï,IòŠ…í.�ëgin3±ÐCaeL@Àèôö¥}Ç™ìÍ•’ã¯o…Õ•ªsùx˜HííÌWR@àQp;¬y‘ºˆìœ\<Ü+FXàéÄ*¡ÎÕ3ÉÝ0]¸.îF!ö Ôº‡5xz9�½¡Ò†˜›¸&¶I!T},T�‡ƒTX=x’!B¥V?j5s4é¶(lB)ôÊ @o~t° …Û±»S].¼�¨ Qy9•õG¥dØŠ]pÎj„6Ãt€�'Áx²a±œ(ÿm´ñn¸fïl ¥.“‹…;ÉÔ˜¿M@@@ài'Mu�ÓYÈÕ¤UJŽWAË2£' <ÉOµOÂ^ �Aå‡WaÓJÉHÒ\æ‚jEú| i% ðô Ö)9•½žøÂ³•’ã¯kƒ(&ÐtA�' agº€€ÀARr ……äæä™·�[ײðÈji¶ �ÇW77ìí ¨þ¨UøÏs*krvUJ†‡8›„tz‰…´x|Œ1�Çž¤äÄb1�A5̪/•×"M±%)fë uˆ¡š¢#5¬;˜-C jØóç^îÿŠé‚wqâøbbã¨î_EZ \ÊÝ]é}b ±N™ …0öÿYcL@@à±§ °�ú ™/ G�Ÿs[¢m·¢×›ƒKK!Ѻ¿ð·­…·uóu¨ô–¥¥I“æìû{w)óøÑ²m‘+~{ªW{öéÇÈaCéÕ«g•·õÑÇ“xëÍQÔ2[FÓVíØüǼ½½*­ÏÃìûm /r&{…º\³eˆD<”ÍÑ%º>3nS&OÄÅÅÙj>þµ‡õ60êµWiÔ°a…ëþôËoôèÖµÜë9sö\ºtêHË–-,¢«@ÕQÖ¬ÌõŒ1�ÇžuM, QlþÏ´ã¦d¯Ù2’ ¯r,sm\Gâ*zjGôw‘¼ü|ì4mTŸ€j~ogö¼¬X~ßç�6dåòŸ-ÞޓȦ?Ö<´¶®GG?´¶*ÂÃì;@º:–c™«H*¼Z)9þšvˆb,¢“%èÒ¹]:wâ«E‹¸nbR’ÑóS&O4W-�Ç€Ê\?Áxj°J¨…·‰œ6[F´ò_dbkZ»¼Š£¬òo¬,ƒþWÆGOœ¡º¿/¡µk’ž™Å¡£'ðñöD*µìíwò„qLž0€ÆÍÛ°:â÷ûVevíþ‹á‘ª yï�·:¨Øµózô >›6“ /áééÁÜÏgШa£í­Œ\ÅÑ£Ç9zì8a:P�ŸÏ?GóÓ²ï¨W7”¨ëÑL�>‹ËW®àåéÉø�>$¬C{àþUº6:³ò·Ÿ ¨ÎÕkטsgÏ�ÇÕÕ™·ßEï^/Uj¬¯^»Æ”y“( >‡w£"¼<½P(ŠÃЫT*)((D&“áëロ­ —._ÁËÓƒ”Ô4t:îînÔsíŒUB-._½ÆŠð U*Z4+�éVBËW„“�˜„““#ý^îMýºuMê¹eÛvÔ*52+{öìE«Õ1mÊ'¸¸8“˜”ÌÊðHnÆÅáääĈ¡ƒ 28äÊÕkD®^KZz:în 2˜êÕŠÝ '~2…œœfΞ@hh(ïŒ~€ÈÕkÙàÃ2¬¬ü½o?G�g¸±†6VF®FS¤aøÐAf뙜’BDänÄÄâà`O�n]iUÒæ¸�'ññ¸1xx'·ÿxÒ'ŒóFÇÌØuHLLbeÄ*âoÝÂÙɉ>½{R¯äœ1™æöÁX{æêrÅÈ,ïúŒ?‘Þ½^dÛ¶�¨ÔjºwëB—Î�ï›/B4E�§½Ò Ez=œÄ•Û't%÷oŽeE�¯5?‡™€…Ñ›qT1Êüò ¨ýLR©?/ “Ìß»XŽŸ8Ɇu«ùrþ\|¹uIÎÆ÷ÇŒ§Q£ü³ãÇ~È»ŒE]TdT–D"á̹sD®\ζíÛiÞ¢)/ôèÁŸíA¯×óÑÇiÚ¤ûÿþ“1¼Ç¸ “ÉÌ2ý÷²hñz¾ø<Çþ=ÀÜY3˜7ÿKƒ¡mŽžS&OàâÙ“´oßöþ~ˆÅüõ×^Ö¯]ÅW_ÌeÑâ%†q‘ˆÅ;~‚-ÖñÅÜÏYðÕBÒÒÒM¶·Ï.œ��Ù´~-Ïž¬�!0eúLöïËŸÛ7c%—“¯TΕ×÷ç»wçÒåËÄÞŒ ¨HÃÎ]ҧ׋&ûþîãhÒ¤!ûÿÞÅ—óç2oÁ×9æŽõ¢ïÑt =íúVÃ×LJ¤ädù¸ø[ØÚØR»VM<==ˆ‹‹3\[‘HDNn.ÁÁAøûùrãL6É!è•V¬ˆˆ¤}Û6Ìœö2©•ªÐ sÓ–m´hÞŒ¯¿˜Ëˆ¡ƒY·nC…^Ìxx¸sâä)™6åÌ›�““#ßÿø3ÁÁ�|1w6}zõdé?¢ÑhLÊ\·~aÚñÕü9„„Ôæ�wF€¹ŸÏÄÞÞž)“'±dñ"ƒ!0°_–,^Dhhh)yM›4!>>ž”Ôâ|�FË©“§hݲy¥ô\úýO×dÞì¼6b8ëþØ`hÃÜ1+ï:èõz~úõ7ž©ÄÜÙ3y©ç üüëròJæ¶1™æôÁX{•ÑÅØ,ïúˆE"Μ9ÇäIxmäp6mÞf¸FwÏéÙsçMvZ@ *P88`km�§§Ç£VEà)B—ꌛuS”öié Ì–s>{;2‘ ­]^E&¶² †æPÖØ›£G°léÒû+<„4……ØÚÚ .Ò°aÛn:µo�ƒÂe¾ùó®2¼ñúH\œ�hצ5VÖV$%…7ãnòþ»o#—Éx¶sG¾ÿé'Ž?IëVÆ÷>4¬WŸêÕüqP8жukôz=W®\#11‰11¬� G&“òlçŽTÿ¾'NœäÙÎ�Ê´µµåøñ4kÒ„Æ�²cëÆãÍÖÓœq©æ_ìJúêˆá899Òªe jsúì9žíV©öÊ"%5•„„D† ‚L&eô¯óÃO¿¦ûÞž·W IDATºe 6nÞÌûï¼Íž½{Q(ìiÖ´‰Ñöbco+žÑ£F!“I©_¯.‡î­P{åQ¤SaS÷&I’(ÜUnØÚÚòL�`Ôj5jµwD" iò4”ùùØÛÙàîæ†T"ÁYáNâáLÒå"ä²l222é†T*¡{·®ìüóÎ^O+++¢¢¢x¦F ‚ƒ‚˜>õÓ �·�µ5¹yy <Ãç©ii¤¦¦òâó=�J¥4lPŸ�»þäZÔuBj×2*sò„ñ†Ÿ5¨Ï™3• 寰·£v­Z9z”ŸïÁÙóç°±µá™gj˜­gJJ*iéét®+R©„€€ê,˜?§Bú”7fPþuÈÈÈ 99™‰ãÇ!•JhØ >Ûw¸u�† ê•iNŒµWÍßÏ,]²³�ÏASt}® {;BCB�ÉedfeáîæVj¾H[·m_a�–æúµ«$%§à%d“8|ƒÛ#«\‡SY YÑÚõU )&`.÷î;v,E%oòÇŒÃW_}UêüÃÈXxÛ>´¶’3 ÷óÈd2â’Nãeàæz'‚L*C§Õ’§T¢ÕhhؤôCvR²ñý-6%.f"±kk+$ :�Ž´ôtŽÈdw\1]]\HË0½ªôÉÄ�ùßwKýÎû ‚wÞz“þ/÷!''Çl=MQÖ¸”uÎÑÁ‘œœªY ÏÎÊÆÞÎÎ0f666ÈåÅ/yLõ½ç‹/°hñÞçmÖoÜDÏŸ7Ù^fVŽ ‡R×è6æŽõ±Ìp¼[*II•VØ<ÜÝqvvB«Õ¡×ë¹xér©:š»VÛn»îúªÛ�t,}[Je>6VVH¥Å!íår9R©ÌPg@ß>lÙ¶�%ß-<ߣm[·6ÙbÜÜÜî3òó Ðé´¼?æ£RŸgfe™”¹ïÀAvïù›ì¬l4š"\ïš;æÒ¢y36mÙÆ‹Ï÷àßÃGiÞ¼i¥ôÌS*±³±1ŒçƒPÞ˜Aù×!''[»Rí) rrsMÊ4§ÆÚ3WSsÐŽ ÃÏR‰½N”ž/Ò‡â«! PÁÏ<ù3æïß±_/\̱' ¿Ѿ]ÂÂ: ³ð¾¥2ŸQ£ßfòÄ�©jÙˆ|yJ%ýõ«Q¤¤¦R7´�5¤æ3æ…ƒPRRSÙ¶}}z÷ÂA¡(qQ±¸jŸ<£££ù믿¸rå Íš5£G�(Šr˧¥¥±iÓ&Î^>AëQ޵³ÆÉÉ©T™üü|ÒÓÓQ*•8::âîî^™‘ÈÄÖ4s~°Ðê殕±3f’�}ç¡9;;›ÓgðÙgSªJöv6%M:Ãg9¹y>Fáô=<ܱ¶¶áøáˆÅ–Ù¹àæêJVN6ê¢"ä²â‡–”Ô4�4�O'Màȱã¼óÞ‡´kÓ¦Jô¬—ìÃÉÌÊÂÑÑÑdÌAá  ¿ �FƒT*%77µZ˜¾FÏvêÈgÓgqèŸÃüûï>™ø±ÉöÜÝÜÈÎÍ)u�ncÎXËŒàhf$‰o//¼½¼P*󹇽}±‘)‹M®.ùÒ Y|MÃï6¶6¨Ô*´Z-‰„‚‚4š;œ��úõe@¿¾\¹v�¥Ë~ NH\œ�Ê_ ¹üþj'Gd2+~9Ѭžgdd²fí¼÷öh‚‚‰¾Êðˆ ×/�† ê³2b—.]áòå+ èÛ§Rz::8 ,(0̳{IÄhJ^Fh4ZòïYÅ/kÌ üëàà @™¯,Õ^VvN)WÄòdšÓc홫‹©9h÷α^_|áxTGn%ob– cX{>ûtŸNžH‡íض}'|ÔjU˜„ÄDæÍÿ’èè4n܈>½{aggÇ‚¯²s×à ߭R©IJN6¼é¼u�!Ã_%«ûDÌåæÍ›LŸ>�€€^{í5²²²X´h*•ªÌòÌ›7�ììl^öúŽÿó:9wEk,(( ** üüüÐh4ÄÆÆ–z .�ž£‘œÎÞP=¨(·÷'üøÓOÄÆÆrïþ°ØØX–}ÿ½¡ÜÃøacm�½�-—®^G£Ñw‹¼¼<|¼Oww‚ƒùvé2òó 8söϿԷR·ÞÞ^°tÙ°óÏÝ$%%ѬIcÜ\]Ø·?ùù,Yº¬ÔµxyÀ Ž;�V«ÅÓÝkkkD¢ªÑ³",_±’ìœ\þ=|„7nиQ#“}°µµáܹshµZ“’Ë] O\]] _µš<¥’%Ë~0MVf&5k˜ÿ’ØXŒµg®.¦æ 9Ü;_¤<„’€À㎻»µk¿±«R­VË©ÓgèÔ1ìÑ*V4-¿¯ˆ QüÔó$’â¥ôF ëÓ A}¦Ï˜E``@•¯�ùûù2qü¸*mã^6oÞL¯^½èÒ¥ ���Lœ8‘sçÎÑ´iÓûÊß¼y“Ë—/3nÜ8<==�ºDí;KnZ6ÅeRRRðôô4Üôlmm¹rå ¹¹¹†·â÷R¤/àhF2±5¡ŠnUÒWãÜ~ù꫌|µ|·Ñ‡u±Yãúœ>w‰¨˜XiÛº™Y.BUÉ‚ysøtê ~ùu®n.¼3úÍ m¢/‘HÄ‚ys˜2u¿._‰ŸŸ ¿œ�½}qT½wßz“ŸÏeé²2øü}} «Lc>x�é3?'îV<®®®¼5z^žžfëu=ÚÕ`ÿþƒLþl}z÷bÖôÏLö¥zõêtá%ÔEELž4ç’öŒõ`ø°!ÌùâK¦ÍšC­š5X¾Âä˜MŸò ÓfÎæ›o¾eøð!8;;£Õi+Ô÷^/½ÀÆM›˜yWŸLõ}ÑW_0qò~þm9îîŒÿèCÃ*`EÇúBîŽfDößzzx�˜˜ˆº¨©T‚»›;²’•7?__n%$’žža8wû~U”'Æ6%]Ai¯‘HÄ �ý‰ˆ\ÍæÍ[éÔ9 {{{túâ—c/õ|‘ˆÈÕ¤¥§¡P8У{Wœ�L¯Šcäˆá¬XÁîÝ{qp°çùîݱ³³#:ú ¾^xg|£¢ëF¿—{Q¯n(³fÏÅÕÕ•Á¯ !!�ÅK–2~ì‡têÆš?Ö¾j ~>ÞLÿ‰‰I†‹.\`ùÊpZµlÅÐÁhÙ²GŽaHçÒÞåéiŠ7^ÉoËW°û¯=8::Ò§÷Kx{ÿ�½Ð£«Ö²}Ç.ÂÂÚãîêbX)3†±ë0rÄ0V¬Œd÷ž½¸¹¹0êµW±¶¶6)ÓÜ>kÏ]ŒÍÁŠ\¿²ðóó-5_DÖ¯¬1�G‡HÄáR¿žéP´Z�޼<¥á†X™YÙØÛÛ!© {Å× H¯ž/>Û²u;YYY |çË/*ê:ïÛÇÕk×  }»¶„Ö ŠCGD®¢ïË/³kן\�¾ÁœÏg •Êø{ï^=†³“5‚ƒY¾be)7Åøø[ìüs7W¯^£Fp]»>G5?Ôj5#^{ƒS§pèŸ9{î<ï¾3šÀ€€RúŸ9{Žï–ýÀsgBßÍÆÍ[ˆŠºÎGc> H£aø«¯óÚ«ÃÉÊÊæô™³V§G÷î†}{.^âÀÁC\�¾�ƒƒ‚–͛өcÃM355�]»ÿâÂÅKøxyR«VM:wêD\|<“>™Â·ß,äÔé3üøó/¥ôøðýwXøÍ·,üò <<ÜïŒÿ7‹©[§]ží\î5:{î�ˆˆ ##ƒwÞyç>9ÉÉɼþúë,^¼˜€’qœ?>­‡yâ–�F£áÌ™3„„„`kkk¨—˜˜ˆZ­¦zõêåêà(ó¦µë«Ô²3ZNÀ²¬]΋={=P‘HĦ�ëéÖÁta@£Õ’™™õ@û[Ï]º†L&C*•"“É ‡T*-uH$$ b±ø¾C$•:në.Põ‰²�s%o/ÿ¤ÿBvQb¥eªº!¹QÓtA�ÿ â{<9„C8êqûÿGMZZ*W¯EqñÒevþ¹›Ýí¡uëV†ó—¯\å³é3 ä­7_§V­š|>ggΞ3”9á«Ö¬¥e˼6rÖÖÖlÙº�]»÷е˳thßî>‚”ÔT¦Í˜E@õjŒ~óu‚‚™=w>é†2_-ZLõêÕxuÄ0¼<ïÏkë!!µË4Äjsâä©R®{§ÏœÅÃÃ��ú!‹ùbÁWä)ó�b·ˆfM›ðÎ[oÒû¥ž¬Y÷§NŸ ++›é³f#“I9|(:t@¡PÜ·/¬q£†¼ýVqèÞ)ŸLbþÜÙÔ«[___.^ºd(—ššÆ±c'5cÿ\FÉ9;—ÎGâááQì¦Vžžž :”ùóçsðàAvìØÁ… =KuMGCÀÙ={(äry)w“òÈ.JäXfÑÊ#Ü�Ê¡7㟀€€yD+�p,3Â"†XuMGÄ1Á¦ üGx}ŒæprrÄýxÁÛËËàÞÖ¡ýK‡öí‰D\º|™zuëâíõàÉ“o¶÷NVVVDGG£×ëË\A â÷ß'<<œ¸¸8^ýu¼<|ßôÄV \¼¯žX,&??¿Bz¥©np,3™È[ÓIR,ÃãšôYàÉåv¨w�ÒÄåŸáXfiª•–UMßùÍôúÇËuW@àa"ìx¤<&¶ýûõ¥WÏÐëõ¨Õj¢®G³hñ·¸¸8óÌ358sö,=ºw-U'((ˆÈÕk +JPù6©ié¨Õj|¼Ë74bcnrìĉû Á»7÷ZÉåFuwws#&&¦Üó·�–òVÎär9µkÕ$1¡ø g~~>‡þù—óç/’�”Ä­[·xáù܈�¥v­šfGH¬_¯+Ã#ILJÂÛË‹Cÿ¦m›Öf¹]Ýö�/º'©J¥"((¨L™×¯_gúôéÌ™3‡š5krüøq~þùgrrr2d²ø¢O¨¨_¿ô÷¢N§+å¶hŠÄ‹Í G&¶ÆËÚxô0ËðàÆØcòå# ð‘Tx…£™á$^¬´,_}s¬ãê¢S[6j±€À“†T°Å%¢ÇleV$aeeEh�Z4oÆ•«W I+Ë* ÅÖËâvô=c‰6v6tëÚ…NK'Bµµ­xÎ _o¶íØI~~~™Ã�˜XÔ¯�µµu© æw£Õj‰Eèt:~³OO/ºu{kk+¶nÛq§ ^"óÃJûúøP»V-.\¸„H$âÜùó¼1j¤Y²n‡£ÏÊÊÂÅÅÅðyjj*���eÖ9{ö,-Z´ nÝâ=Š­[·ÆÉɉ &†ÂÞ�ó 4,ˆ"×8C=µZý@yPâ Nq43œ6®#q•WÐî <(�×W‰€ÀŽtu,G3É+8UiYÞ¢FØ%ÔG—oüe£€ÀÓÀÃKÖ! ð¡×ëÉÊÎF„kkkꆆpãFi—Œ˜˜Xê„„”k8¹¹'|4Ö¸š¿?©iéøúxãçëc8\Œ¸ÞKݺ¡øx{³{Ï^tºÒO¤‰IIlܼÅhÐÂÂB®\½†¿Ÿ)©©œ¿p‰n]»R»�ØÛ߉ÎT­Z5ƒ `E¹{oŽX,¢}»68x�³çÎÓºUK\ï2¤gggºté¹swöí©T*Ž=Jóæe»uZYY‘ššŠæ.£ô¶+“Épvv¦Ehg®nÓâ! Š�ꬬ¬ûr‘U„hå¿ËŒ Gc:´µ@å¸;d}…Áz¨09šä’ý°ÿVZ–‡(‡”Fèr*îm ð_FªGÏ¥‹—øtÊ~úéGlmlyeÐ æÌ™Ã�8}úcÇŒÁÙÙ™mÛ·sâÄ lll©S'„ž={BÕ^½z�¿vïæÊÕ«Ó1,Œº%ònDGóÛòßyeà@¶mßNTT_.X€µMåB[ üxLüoðÐëõ¨T*Î�¿À‰'éùB±óRÏž|ñå×888@||½¬¬¬8uê4žžžøûûá PZ§ßÿø3Q×£ûáûêcyôìÙ“©S§âàà€��ÀÝÝ�zõŠ÷ñ%&&²qãFúöí‹››M›6eݺu¬\¹’víÚ‘žžÎŽ;èÖ­[I¨û;2íûãÖÆ™[ùg‘Ëå†ïºårîš9¿"¬�U!·�«Ÿ~ú™cÇŽ•Y¦eË–Œ1ÜPC@@À4éêXŽeF¾Ë*ƒ«èS£M7ïûT@࿈”»ßÞõóüùóôÊ@š5kŠ—— -B¡PðÚk¯!“ɈºUü°¥×séÒe>�2…7ÞxƒçžëÂ�˜¦N›Æ§Ÿ~B£† Ñëáܹsèt:ž¾�;wÂÚº¸®À‡�»vqíêµ2ÏÕ®]‹gŸ½ÿ�ý1±ÅJððôð V­šL�ò‰ÁE1´N?þˆ½û°uûNjÖæÓI¨QŽ ãmz½ô"èõ,úæøúùÒ´IcC>3(t1uÊdvîú“~ü[[[‚xÐý,þ~¾Lžô1âßÃGHKK£N�:|ðÞ;„ԾϒT"aãæ-¤¤¤Ò°a}Æ~ðVr9Vr9ãÇ~Èú�›8vü$Í›6¡G�nGbtsueÆôÏØ±sÿ½wj×®MQ‘ºL�^îÝ‹mÛwR£F0Ý»uÅA¡ÀÝÝ�-šsêÔijÕ¬\( >ûì3þúë/¶nÝJ³fÍ0`ò’}v‰‰‰lݺ•zõêáææ†‡‡3gÎdÇŽ,*ùNkÕª�;w6¸�Þ-3ãàÚŒtû†â ¦J(‹Ë¹{Pë hîÞÞŒ4¬.Ü¡z5ÿRúŠÅb^îÓ‹—ûÜŸÿ©š¿ŽŠrƒŠ<ÁÁÁ¥òŒ•j§Z5ìììxæ™;"}||9Òø>µ»eŠ|’I’ï#W—T)=£•ÿR¤+¤¹ó !Êbp·ëìäIùxâ$r²sppt`ò¤‰¥ÊˆcL@À(qùg,¶GL!öÂ=«º$W h& ðßB¬/'ã“•\nÈÅ"‹2d0?üø#‘«Wq#ö†á\�ª€S§OS³æ3¥ò·Ô®Á•«WÉSæäÊd2³rÁÿžœýû÷ÇÆÖÆð»�­ ýû÷/·üÓÂmCLTj5ûö IãÆUÞÖÙ³g™8q"OÔ{/úOLçŽaX[?Y|D±øu Æj:}Ù)*Jšêÿ¤ÿ‚F_H¨¢›e|ÊyЕö‡±2Ÿ_PÈ߇ãåᎋ“C•¶5yÂ8&O@ãæmXñ»á�ø6»vÿÅŠðH U…¼÷Î[ ô ×£oðÙ´™\¸x OOæ~>ƒF Ì•öÔé3|8îcVþö3~¾¾åÊ\µvÛ¶ïä·Ÿ¾7Ôýâ«…äææ1cê§lÛ±“ðÈÕèôzÞzsC ŠW[¦NŸÅå+WðòôdüGÖ¡=GŽcÆçóˆ¿OõjþÌž9�º¡uøßwK)È/ÄÚÆŠß–¯D£Ñ²}óz¼½½8rôŸN�Ž2¿€ž/ô ¢«W^ß�`ËÖí¬Zyg5qêŒÏQ«‹˜3k3gÏ#"r³gL£W¯ž†r7bb˜>sgÏ�ÇÕÕ™·ßEï^/m¯"Ü;Ö�_tâhF YQ$&%£V«‘Ëåøúø`St­P¥"áV…*2™/O/ƒËybR’a5ÁÏÇ�F¶=Å�œ’BDänÄÄâà`O�n]iÕ²W®^#rõZÒÒÓñpwcØ�ÁT¯æ_áëq/ýÃñ'óþ»†Ïþذ‘‚‚B¿2ÀhÝÕk׳wß^ìííKdã>žÄÇãÆ¼->žô ãÆ|€‡‡·X¾"œ„Ä$œœé÷roê—¤Q™øÉrrr˜9{¡¡¡¼3ú “ý0&31)™•á‘ÜŒ‹ÃÉɉCTœÖeìø‰ôîõ"Û¶íD¥VÓ½[ºtî �õŠð U*Z4kZáöÌ¥<=oÆÅ1wþC¹ÃGŠWŽÞ~óMêÖ­cö˜›K掋1¶lÛŽZ¥Ff%cÏž½hµ:¦Mùg"W¯eÿ� <ˆ–%óŠ¿ÏV¬Œ ;;ñ¿‡�°woqà‚2ÆJŽ�»vsþübcnrîü $Ï�úôéÍšµëسg/7nİÿ"W­¢ß~ /caL8„£äÐ#ð0‘I¥„ÿþk¹ûÅ£G½N¿¾}Yû•A]ƒêE–YmË.JäPÚ/œÎÞ`yO;Úþ!¼´µ±¦W÷.´mÑ…™Ñ8-Éñ'Ù°n5_Ο˂/¢VÞyÌx5jÀ?û÷0~쇼ûÁXÔ÷$S7FbR2c?žÈWóçâçëkTfï—zr#:†«×Šƒ=©‹ŠØ°q3ƒö7È;sö›7¬cÁÜÙ,øòkRSÓÐëõ|ôñDš6iÄþ¿ÿdÌï1nÂd2³²˜·à+êÏÑCûhÓº_|µÐ ¯zõjlß¹‹kעضy=‡þ�‡‡;S¦Ïd`ÿ¾ü¹}3Vr9ùÊŠ­|«W^ߟïÞ�K—/{³8¸LQ‘†�»þ¤O¯‹eNžÀų'iß¾í}í½ûÁ8š4iÈþ¿wñåü¹Ì[ðµANe®ßÝcýûÁYü�ð=ÙE‰$%§àââLHíZØÛÛ‘œr'=GJJ ŽNŽ„Ô®…¯�IÉwÎy{yZ'…½=Vq¡ˆ£kÎ-ýþ'‚k2oö ^1œul ¥d�ùºõëÐŽ¯æÏ!$¤6lØX!ýË£U‹æ$'%s+!�FÃá#GéPÆØÞKÿ¾½Y+ª¾/ IDAT²xS&Oz 67mÙF‹æÍøú‹¹Œ:˜uë6¾gæ~>{{{¦LžÄ’Å‹*dT˜’ùý�?ÈsgÓ§WO–þð£!m‹X$âÌ™sLž4�×FgÓæm†s+""iß¶ 3§}†L*E¥*¬P{æRžžÕüýY²x 4 _¿—Y²xK/¢nÝ:•3csÉÜq1†‡‡;'Nž"1!‘iS>aÁ¼Ù8990°_–,^Dhhè}õVF¬¢Më–Ìù|âÓ§NÓ°QÃ’\;·Ÿ’áîÇd�Vƒ¦HÍ–­Û˜;>ÿüsˆ ÆT’Xµnh(ŸNžÌ¥Kùfñÿ¸pá"S§N!$¤v…:"ð4#˜c¦Q8TíªÂƒ ¾^“êÚŽ¦ V€|m&‡Ò~áXf„Eä=Õ<ÀK ±HŒ^§/•,üià�×GââìD»6­±²¶")9…›qñÜŒ»Éûï¾�­­ Ïv¯7Ç�Ÿ¬�LUa!ï¼ÿ!ï¾õ&M¿ä1&S.“1hP~û½Ø¥rÛö�Tó÷/•~cøÐ!8;9ÒºU 8{þ<‰‰I܈‰áÝ·ßÂÎÖ–g;w¤zõjœ8Q¬ç«#4 ?VVVté܉„ÄDƒ<…�=™|>cîîX[[#‘HHIM%!!‘aC†`gkËè7^¯PŸ�Õ3Öwg'Z·lÁÆÍ›سw/ …=Íš61Ú^lìMâoÅ3zÔ(ìíì¨_¯.‡î¥z5ÿJ_¿Ûc-«Í3=ŠÈÌ/·à @\œ�‰D8(Ýe܉Åbò•ù¨T*lmmËt9Ï=çIÞ)¯;c–’JZz:ݟ늵µ5ÕY0înLž0žíÚ"“ÉhÔ >Ò¿<¤R):´3DI>~âînU�••QQÅ+ŠÁAALŸú©!J¯¥e¦¦¥‘ššÊ‹Ï÷ÀÊJNÃõquqáZÔuCÝ®ÏuAaoGhH2¹ŒÌ¬,²³³ÉÈȤcXÖVVtïÖµJûP=-�©¹dθÃÆÚšÜ¼<†„££#r¹Üdôe¥RIrJJq{ÖÖHG¿Yli¶¼+IêêÈâƒÛÖ°D,¡G÷îôè^:LÛÝÖríÚµ¨}OíÛçî“) ‚—¢@۵¶&êÚj<óèËôbä7Cð¯®&N|Èté 8”þ+EºBš9B&¶²€–O"QESèÑét\¸xGGÇ*ÕéqÃÍõN$;™T†N«%O©D«ÑаI‹Re“’+¶Wã—å+¨yWÄÒœœ£2@·z‘™•ͪÕk8°_©rî%èÎNÎädg“–žŽ£Â±T0$WÒ2Ò_µš_—¯ %9µZ…¯¯�¡œH$Æßß…BQª�ì¬lìíì 2mll�ËMÿý«gªï=_|�E‹—ðþ;o³~ã&z¾ø¼Éö2³²pT8”ÊT{¦pvsàŸô_8š‰ÌF„V« #3“ôôtŠŠ4èõzä2™¡Ž·—)©©†•9wwœ�� çýumøëÐ1TDOKcj.™3.Æ!ÆÍÍ­d[VÅPæ`%—Ú“>Mí?DB� àééARr gÏœ"7'çQ«€T¡Áº¡jÏ‹ÈÛ™ù-'²ÿÅ=«RmſԊùóÏ�.ëâ₃£#Õ«U¡FOîX[Ûpüð³ré Пzõê2qòþX�•••I™ …‚ç{tgÖì¹ÄÆÅѽës¥ÎgÝõ –™•…“³3n®®dåd£.*2)©ixzx�˜˜Äœy øá»ÿѨaNŸ9˧S§—’im}¿‘¥pP�_��F£A*•’››‹Z­2ÙgcõLõýÙNùlú,ýs˜ÿ=Â'?6Ùž»›Ù¹9¥ú~›Ê\?‰­Žy+I@«Õ ‘H)**"))™êÕü±µµ%??Ÿ„„;+�‰o//¼½¼P*󹇽½2™ ]3¬ãë¢É?Qª-G”†1»›ŒŒLÖ¬ýƒ÷ÞMPP Ñ7bX^yoš6iʪÕkIIK¥I£Ê»ç‹$b4%«F£%?¿ÀpÎÎÎŽýú2 __®\»ÆÒe?P'¤.wªJy2��ɬXøå¼Z¹²±µA¥V¡Õj‘H$ ÑÜYõ´tÌÕÓ\Ì�K¦ÆÅryÅ 7C{ÖÖ¨‹ŠÐétˆÅâ’ny*ûkØûRßê%w‹È0†‡»;ÁÁ�|»t¯�Áµ¨(&O™ÎÊß~2ì{0†ƒ£�;†±mÇ.¾øj!ŸNšP!™¯ÂsÏ÷dØ�A÷¿¯Œ ¤v-.\¼DLL,õëÕÃÅÙ‰ À–.û�Q¯½Êþƒ‡HJJ¢Y“ÆÜŒ‹G*‘âííMZZ:ËW„“™•MžR‰½�]¹º{zxàêêJøªÕôéõK–ýP¡FcõLõ].—Ó¥sgæÌ_@½zõð÷3eÐ×ׇÀ€–|·Œ×GŽàò•«¼÷áG¬øíg‚ƒͺ~ÙEIx‡ep*ë|í|(((@¥RcccƒFS„ˆâtD�†ôŒ 4Z­áÁñzô ¼<=±µµA&“"÷Ý_×ëøºèóï7|]]]ðôð`ëötíò,qñ·XöÃO|4æ4EEH$œ]\ÈÉÉeÏÞ}äå)),,¬t §g;…ñÙô™têØá>#Ð Ο¿€«‹+;wýYê fμ/x¹OojáìèˆL&çîédeeELl,žždeçTÈÀ)O¦££#ÞÞžlÙ¾�ç:?KBbËWD0nÌûØ™óNŽŽ(ì;p�Ö-[°uÇNî‹nªйzÞæAÇL©Tš5—L�‹¥Q(ìqrp`ÿÁC´lÑ\Xx´³OàIF¯”#� Á'XF‚Í¿‘yµp/½ŠÖ½p’TÝþ�ªG­.b㎿�ˆ%H$ÅǵèXjСM Ó æÍáÓ©3øå׸º¹ðÎè7+dˆÝÍ”IèÙ»�;†Ñªe “2­ml�J¤¼2 ´‹¢V§£NH-^ìÓµJͤ‰áêâlÐsÊÔüº|%~~>,ür>ööö„Ô®EX‡v¼Ô§?¾¾¾Ì˜ú W£¢xcô»„ÿþK¹:‹D"¦Où„i3góÍ7ß2|øœ��Ñê´Fûjªž©¾÷zé6nÚÄÌéŸ>‹ºMÏÞ} ¿ïß�ÉŸM£Oï^Ìšþ‹¾ú‚‰“§ðóoËñpwgüG\9ïA¯_º:–£™á¸ÖÏÅÚÆ•¨ë×Ñëôx{y"•J�J%Ø+D]�F.—ããí…J¥"6ö&��%«‘‰¨‹Š�J%¸»¹SCüéG\˜ñÙ�•¾ .°|e8­Z¶bèà�¼ñúH~[¾‚ÝíÁÑÑ‘>½_ÂÛ˽^O½º¡Ìš=WWW¿2€„„/YÊ˽^bÁ×w‚±DEEÖ!Œ¦�–{®ßÞÈ­äH$Ú·3¸ã^nŸwóB�nD¬ZËö» k�»«‹a¥ì¥ž/¹š´ô4 ztÓã¡S§0Öü±žðUkðóñfÂøÒ®{eaLæÈÃY±2‚Ý»÷âà`ÏóÝ»›4pD"ƒö'"r5›7o¥Sç0ìííÑéuêCYDGß0zÌÑó6�:=ؘùùù–;—Æ�ýÐìq1‡ÄÄ$C$H¸ÿïaÀ€¾DD®fã¦-ˆ"ÃWÏÃ� ‘XÌ©“'©_¯b¡Sµ:yyJœ�Üh2³²±··Cb†Ë�€€YˆõP3šDûÐé�?ÈU”jòÆÔ³~é3¦ T9­–Ì̬Z¡=wé2™ ©TŠL&3R©´ÔqÛP‹Å÷"‘¨Ô<wŸªD¥R1uÆç²ð«ù�Z�§ŠÄ‹Ë\E´Ò2/�Ä")Õ‹:!‰®ºÇk^¹µJͨ×_­P�ÂÂB´:=2©„ ›¶’”œÄûï¼UÅš <Íèõz*¿f+ P„€.ÿt"¸ŒOM)ɇ)Ò˜®c‚›ê“äë2©cÝ•`ùƒ¿ÕxÑjµ4oÝž:!!|óõÓ,Æ¥ÜÝœÎÚ@²êªEäÉÅvøv(¾þqA§Ó1vüDªùûóÆë#+\ïf\<ßÿð*µ ???† \…Z <Íœ8yŠuë7’““ƒ(b¥°2&ðh(~»«'**Š À€ ÕVÆwÄAñ¤¹£@ŸmyV";êXw'Ôú9d"[‹Èxp„•1�'µNÉ©¬ œÊúƒB]®EdÚŠ]ðV¶5$t0©ð¥.ðhУGOÜ͸’wÿ tÑ~¸i¥d¸�@Ij¥å©ôJN¬%O—L¨U7œ¥Õ- ¥€€ÀÓ@šê:§²×s!g—Åd*Ä^xæ´†xÓÁGL#=}úÔ£ÖAà)ÅÞÞ[[ÛÇ2Jž€@eÐÅzáªm�Äý$9â‹È¼¦:@®6•:Ö]©.of™ÿ]¢òr:kñ…g-&ÓIT ÷¬Vè<-&S@àiGZÑÀ G®9bÏÓd‰c,"3Is™Üüdòt©„X=‡X$lû(�N¯átv±[b®&Íbr]EÏàšÙ]Rå2 ÜA¸“ TÚ'šFH|ä¤K,³i^©Ëäh~8¹ºêXwÅAìm¹O>YEñœÊZÏ™ìÍ•ë! Å1µ1º4g‹ÊŒ1�*E—∽®!b_©Ò “{©p7¹ÚB¬ºâ'o`1¹O&1ÊcœÎÞ@Lþ1‹Êõ5Â!¥Út{‹Ê(F0ƪMš-6Euñ¬&%Y~Æbrã‹Î’«K!OŸJm«g-&W@@àÉâlöfNe­'³(Þ¢r}õͱK¨�6G´% PU±¿ÚläÑuñV5µ¨ÜlmGòçX~ùº ‹Êxò¸tù ½ú¤AÓ–4jÖšUk×>j•LÒ´U;“IÛ-Û†kQ™“>�ÊÏ¿-·¨ÌòÈÓ¤q íö¦}W®! •Jußç—¯\E­V—+»š¾-vq�ÑUÂ3n™f׿›™³çrøð‹ÈzÛ{±äõ(aeL@@@à!¡SÊ�\ Á'XF‚í¿–“«×r¾pkÉ>²çð’†XL¶@åHMËàüå«( ±·³£a½:ÔªTeí­�¤NHm"WüŠX,F§ÓUY[�–ø‚³œÎÞ@TÞA£åTêû 1ST×tD~3�úñyLœ2yâº=�§—Çç¯L@@@à)@¯–ÀåøÖ�“æp–I «>FnI>²Ví-&WÀ|ŽŸ>GýЂüÉÌÎåÀ¿Ç¨æç‹ÂÞ²·ß /Ñoà`Ãï6nà»ÿ}C‡öm‰ºÍÔ鳸|å ^žžŒÿèCÂ:Ï‘ÿ}·”‚üB¬m¬ømùJ4-Û7¯ç­÷>dò„ñ4oÖÄ ·°°�m:pâÈ!Nœ<ÅŒÏç+žêÕü™=s:uCë•éííÅ‘£Çøtêt”ùô|¡G…ûhN¼½½LÊݶc'á‘«Ñéõ¼õæ(†À‘£ÇÊíß�˜¦ÏœÃÙsçquuæí7GÑ»×K÷É>uú Žû˜•¿ýŒŸ¯¯ÙýkÖº=ãÆ¾Ï’ï~ ¿ €!ã[áÖ4‡TÕu£2¯\½†F£!êz4 {{ªUó7œÏÉÉ!=#½^‡»»;¾®�x¶"åˆ=+W|Ë͸8œœœ1t0AA�&ÇòÊÕk¬� P¥¢E³Ò^‰Iɬ �¼OæßûösôØq&Œk(»2r5š" Ç"rõZö8À°ÁƒhÙ²…¡\rJ ‘k¸‹ƒƒ==ºu¥UÉùòÚ3…±ön%$°|E8 ‰I899ÒïåÞÔ¯k:*ùØñéÞµ þõ7ݺv¡SX¶lÛŽZ¥Ff%cÏž½hµ:¦Mùçÿ³wßáq÷ãÇß{ýNº;õbuÉMnrýpÆ”/„@ ! ¡0Å”Pcz€ð ¡À�fL³Á€+lÜ» ¶du�ºt½ìïÙÂÂÒž¬jüžç„övæ3eå››ÙYÅ2(Å¢tL)M¥öº�X¦(‚ÐÃ$Y ¹iÄVN!LîÚ‡8Wû޲Ź„ïË©õwiÚ©›söTR’ú R©ˆ�ŽD£Ñ ü]žÏàA™سƒsΞÉ=‹îäÀžسƒ©S&!Ë2·ßy7£G�`Óú/¸õ–›Xx×=ÔÔÖ�’’ÌšÏ×’››Çê�?໯×MZj G �¶ÈçH~ }ú Ñhxâé¿så—²õ›�Lœ0ž§þþ\óûÚJàþ‡áòKðÅš�Ñët8ìö åëhÚc÷ž½|¼ê}ž~|1O?ó,6[ÓvðJå»ñ–…Œ5œMë×òÌ“�óÄÓÏRp´°Eº¥eåÜvçÝüýÉǃÄ‚•O­RñÕWøÏŠg¸ñùIìñ¿M…+/hÙôï‡F­¦oF:ƒe¶ˆØ22ÒILH ÷ûjâkf¢>ÒŸW^yƒŒŒ4žz|1ó/¼€—^ý7>Ÿ/h~K–¯`ʤ‰<òà_Ñj4¸Ý®æc¯üûõVÓ=jEEET«wŸÏÏÎ;™0n —_º€_xžÁƒŸ”ßK¯¼FFß4žXü0×]s5ï¯\ÕœN[ù£”ßGŸ¬f옳xö©Ç¹æª+yÿýUȲ4M•$‘“›Ç÷-âÚk®âƒUR_ßôE\LL4Ûw줴¤”ï¿—§ŸXLX˜5h”bQ:¦”¦Rû ÝG Æ„3J{þè�Êû¡7ù ¢±Œ!Ö;¬KÓõìs}Ê7ö×ÈuoèÒ´©ºâoÊþC¹ÄÅFa2õìf¥¥eÉÏçÆþDˆÉÄÙ3§“’’Ìöí;0‡„R]]Ãß~�˜èh jµš´´ŽQRRÊ’å+€¦¡Ô´¦/V¾³œ+.»½^Ï93gPRZÚœg[iVØl”””ò›_ÿš“‰?^ÿ»n-C{\}Õ¯ ³2aüXÒÒRÙ³oŸbù ŽRT\Äÿ{BCB6tß}½�”:n—‹?ßünüÓ5rD§Ë0ïúL¾w¿FMø6´F¯×Û®ò)‰ŽŠB£V3 t:‡?1Q½Ï€­²›ÍÆùóæ¢×ëž5ŒÈˆró”gáêêꨮ®aú´iôzæÌžÕ|L)Msh `ËÖ­ìÙ·£ÉH¿~}󫨰QYUÅœsga0HMMáé'#&:ªÃeF¯×“——GiY9éé<ôÀ}H’Ô®sÏ>{!!Me���çp~>Fƒ�†ÆF®ºò ¬V+:�•J´ J±´uL)M¥öº—X¦(œYd‚Þ@ŒÅ„3„¿*mc}RÍT„nÇG×}YáËÁæËÅæÿ�~ºéDkºï^¥Ÿ=YÆïïØŒ–,ËìÚ{‡ËÍôIãº8°à*«ª°š­hµ?þ“Aeu’¤"))³ÙÜâ¼ô´4¾új6oæ©§Ÿeܘ1�žÖ´¤iÙÛïðß7—PQnÃãq“�ЧùܶҬ«­#4$¤9£ÑˆN§ï¶2´GttTóÏáaáÔ×Õ)–¯¦¶«ÙÒ"–ŸúÏ›Kè߯_»bP*_™+›¤9•dëÞGçj:.Ѿ@0zu(©Þh‹úÓXô r €Óå&ðsó­··xoMm­bZv»£^�FÓ4Öéth4Z§bšcÇœÅGŸ¬æüysùö»­Œ|‰\£ÝNˆÑ؜߉‚å×Q—-˜Ï'«×ðâ¿^ æÍ�ͤ Úu®ÅliþÙd4áp8�P…Ñh<¥2(ÅÒÖ1¥4­K›í't/�_ÜÜ+œdYF–eo»Òž5Œ¥ËßæàÁlÊæ²óƒægµX°;�Í}éD-C0!!!\vÉ.»dÙ¹¹¼ôò« ÊDDxXÐs .¶ég»��fÈuº“ûQ°2(ÅÒÖ1¥4kjkÛl?¡{iƒ¯Õ„ÓA  ðcµZßgµ˜©ohÀíñ R‰•¸ÂdŸmÜP,‰&jŒ9]št%TzÞ¤PÚGª4‰htiú?g~¿¿ßßb¥½¶lßETdÃõ^}ÇÇÇ‘ž–ÊK/¿Êï¯û-›¾þ†²²2Î5Rñ¼ô´Tªªª(**æÃ÷ßášßý“ÉÈå—. ¦¶�ZC||<••U¼¹d5µu4Ú턆„´™flL ‘‘‘,{ûæ_ø+^|ùÕv}XîhÚã­¥ËÉ8€ý’Ÿ_À°¡C)//o³| }HKMåŽÌ﮽†CÙ9Üô—ÛYòÆëdÛÁbµ0sú4V¶–§þþ÷-ºë”Ê÷ÉÖ7Ñ�ÚGyd5*OçþS©T8�Nt:>Ÿ­VK<ÃÉû¸„iÄCLË÷[­VâãcùdÍÎ�y6%¥%¼¹d9 o½™…¶ ³Z1›-lÜü5Æ�åÓÏ>‡c3xÁÒÔh4 ÏÊâÝ÷W’ššBTTðk-22‚ؘ>]ó³Î9›Â¢b^~õ5n¿õâãb;T†`{â).ž}3Ò ·ZÑju´w¬·nÃú$$PXx”òòr22”W*«3¥XÚ:¦”¦RûWWWÇ¢ûþÊ øC† ê@ ­Ñ„»IP~.ôz=ÑA¾…„Ó–ËŠT‹9)–2ÓVd¹k7{¨`U Œ3èo˜ŽI þ�®Ð1�v%å”Ûª8”{µZ�Z­fò„³èäƒXW’$‰§ŸxŒûx˜ÿ¾¹”ÄÄ><÷Ì“„††*žg0d¦LžLTT$ýê<žyödd¤c2™6u2¿š) <üÀ½ääåqýodÙ[ÿQŒå¡ûïåÁGó�ü“«¯þ5áááøƒljÒÑ2ã”9€óç_‚ÇíaÑÝ·NDx˜bùžÿûSÜ}Ïý¼þÆ›ÄDGsÇíiˆ�èþEwqÁE—0sú´æ]þ”Ê÷À÷³ö¾‘4Þǰs#ºä ÅÈÈÊÊË))-C¯70=åô%¨ÎYÓæ9×^s5K–.çË/7`±„2oΜ ƒI’¸âòKY¾â>þøSfÌœFhh(9Ю4Ç�;‹-[·ðë™ÿ×ü»ÒÒ2YüXóÿïß¿Ÿ7—.cü¸ñ\uåå\ÿ»kyãÍ%|ùÕ:¬V+ó/úñÇfŸ:R†`ùýê‚óY¾â*«*1›-Ì�3‹ð°öý �‰‰áÁ‡Åçóqé%ó¿´8N© J±(SJS©ý„î#­Xú¦¸»Fá4$¥Q¶ ;Ý’~’vý ÓHÕ�é–ôéöÌE«Õ¢ÑhÐjµÍ/�FÓâu|�¦R©Nz_j}â’k±ôúç)¯ñköׯÇÖnI߬Š#Æ>© µ[ÒÚ¶ðÎEܹðVbbb‚¿YøÅx‚ œ¦äÉD%„`ˆ>@•*»ËÓ/ôî¤ÂŸƒÍŸGý4¬ê>ÁO¡KÕx Ù_ÿ9ûêWãò7vK1ªÁ„Õ 'PÙ-é ‚Ðqb0&‚pó‡Ú8]¢…Rý÷]ž¾;`g�ãcʽ¹ô×O¥¿aZ—ç!Bëö×Îþ†Ï)qîë¶<’1–d¨ïÙG*?zúÉÇ‚¿IøÅƒ1A„Óœ¿Îˆª~If*-;pÒ¹í™[Sî=D…/›/—†™D‰mð¡Û”»sØW·š} Ÿwù}¡Ç™TÄ9Ç!å§�{~OAÚG ÆAÎ’¬FÎK%&1”úè}ÔH‡»<Yö7mƒïË#C?� ýxBUâAè*õ¾2²6�ݸžJ÷‘nË'JÕŸðÚÈ%±Ý–‡ ]C ÆAÎ þ¢(¬Î1âÃ)Õí ;žp^ã;Ê6ßQ <ÛÈÐ�'M?“*¼Ëó„_ »¯ŠœÆ�d7n Ìu¨s’H’Ça*ËÄ_ݹ�&Aèb0&‚p†ñU™PWe‘œM}Ø!j¥ünÉÇæÍÃæÍ£À³�Tý82ôÐKâž ´—+Ð@vÃzr7QäÜÓ­yEªúÞ8Ž&Ñ= Aèb0&‚p† éƒÕŽ©O<Æ]øpvK>¥Þƒ”zRàÞF†¾i¦L+º%/Aø9ðœä6n$»aG�;º5/­d¢�o ú’¾b“A8‰Á˜ °&y( IDATÂÌ_gDU—IBb4�‘ÙT©»~ üãJ¼{)ñî¥Àó=iúñ¤ëÆ£’Ä?#‚p\@ö‘ݸ�ÜÆ�¶oéöüb¤ÁXë† —Ä"Í+g&ñ¯¨ ÂÏ€¿(Š�+ÆÄ8ªL»pJuÝ–×QÏNŽzvR ßFšn<éúqÝ–— œ)r7‘Ó¸�¼Æ¯»=/“N¬ç,´¥éºnÏO„î#c‚ ?»²3ˆ‰‹À“G…fo·æ—ïÞJ�{žïÉÐO$Y7²[ó;UTV±ÿP.v§‹£‰A™ý6h`o‡%t¡ÃöïÈn\GNÃ&䘟êÃHB«2 TDŠÙ0Aøƒ1A„ŸY8úêQ$%ÅRkÝKåÝ–—L€Ãîÿ5ÝOfOºv< ú¬nËïL�e¶îÜÃYÇ‘œØ»Ýɺo¾#5%™«¥·Ã:©À¾�û²6à“=ÝžŸUÕ‡(ÇHT%)<â¹a‚ðs!c‚ ?C²G?$AHLeúîÝDÀ�‡×Fò=ÛH÷Ž%]?‘xí nÍót§’$Î;g:�$ �׋^§Ã¨ËÊÎdEÎÝd7¬'Ǿ ·¿±GòL Œ#¤r þJK7<ÌB„Þ$c‚ ?c>[(ÛpRÒc©³ì¶mð�óìr­ãˆ{ ‰º$ëF’¬�†_îî‹å¶J¾Ù²ƒ�S'ŽE«ÿôži¼²“#ö-¶o!ß¾W ga‘ª "ìYȉb»zAø™’V,}S|É"‚ð  sâ‰ÏÇf܉W�å§Í$I;œ$ýÂÕI=–ooÛ{0­V‹F£A­VSY]ËöÝû˜wî ¬4Mó1µZ�J¥:é%IR‹Ðü_¡ûUyò9bßÂÇVŠ�Ý{扴’‘xß ýÔ‰íêáçL|='‚ð á¯5¢®Í$!)ІˆlªÕ9=’o™÷ eÞƒp­%Y7‚$ýHµÃ‘øå *T* ñ±.(¤´ÜF˜¸gì´%#“ïØÚ4³o¡ÁgëÑü£¥A„ÕA.ŽtÂ/€Œ ‚ üÂø £ ©¶Ÿ@�9‡z©¸Gòµª8èú’ƒ®/IÒŽ Q?œ$ípÌê˜É¿§¹\nÖ|¹‰IãFCUu UU5Œ6¤·CZQç-#ß±•|ûVŽ8¶öxþaªd"œƒQ—'#;ô=ž¿ ½C ÆA~�d»òÒ‹Ç]L�ùP·îºøS…Þ�zwrH�@’n$‰úÄk2{,ÿž`0è>l;öìǵm'!&ÆdÞÛ¡ '(rí!¿q+GßQå9Úãù[U}ˆpAW�‚¿Î(6è„_1Aøó× 6ƒˆðx,1EÔ„ÂAe�å_ã/¦ÆYÌA×$éF�¤N’n$z)´ÇbèNÉ ñ¤§$¡Õj›_Bïs8bÿ®yÌpöx ¡ª¢ÜCÑU¥¨ tÂ/”Œ ‚ økLH5ý‰Šèƒ/¶ˆãœÔöXþ^ÙÅa÷·vKŒ¦‰ºá$ëF©I뱄Ÿ¿ w.Gì[Éwl¥Ôu°Wb0©"ˆöf¡+OA®±ˆûÂáN ÆA„f�êPTÕ‰�NÀ}”*Ã~<ôÌ6ÞÇUør©ðårÈùqºAÄi§ø‹Ú‰Qè:Už|Š�{)ví¥È±»¿ºWâÐKfb}Ã1V¦ã¯´Šåˆ‚ b0&‚ ´Âg3£¶ &>6Ot6Ý~|ôìR.‡\Ëa÷ÿ8ìþzU±šÄi3‰Ód­ÍèÑX„3K™+›b׊�{)qíÃÕCgn�V2ޱ*ƒ€-\,G¡1AÚä/·¢.FBŸ$Ü‘ùØtûðËÞ�ðsÔ³ƒ£ž¨%-1šÄk3‰Õ $^;IR÷xLÂéC–ý9÷QâÚK‘s/¥®}øz¡ŸžH#i‰“GbªéK ,R,G¡Ub0&‚ å/ GSNbb ÎðÃØ´{‘{i¡•_öRêÝG©w1šþÄi§Ë$N;-Æ^‰KèYÙÑ´üб—b×>J]z;$$$âIh]%Ñb&‚"1AÚÍ_�®(‚ä¤ìᇩÔìïí�¨ðåPáËa�óc"4)Ç–3$N—‰I ëíð„.d÷W7 ÀŽ-?´¹÷vH-Ä1sCñÀfAÚM ÆA„Sæ/ŒF_AJJ ËQª4ÙèÝeaÕ¾ª}t­Å¢Ž%N›Ù48Ó À¢‰ïíð„¨õQìÚO‰kÅνÔzKz;¤Ôh‰a¡ötEqȲX2+Bû‰Á˜ ‚Ð!’¬ÆŸ‡ž8’bà�(¥Þ˜G#½õþrêýåä°5:"´ÉDj’ S%©I!R›"–4žf<²›ëlîÃTy�`s¦Ò}ŸìéíÐNbQÅîÍD_›HÀŽ H½” gõ‚‹/z°·ƒAÎl²Ý€T…©1‹&­V‹SUÕÛa5“ñãTSé;B‘w¹î�vÿ�ï>j}ŸåFÔ’½ÊÜeyVTV£V«Q©T¨Õêæ—J¥jõ%IRÐÐüßS1nÒ4Ξ1�°°î[¶9züdΟ7³9ø»o¿séé¨Ìv �»ÉnXÏκ•l©~‹µ+Éw|O…;—F_%�vî?XTTŒ^¯G£éø÷Ìefµ¢V·=»# &Î5Ké(Te ÈŽÓ@ÿÈâÇÑkµ$&&v{^¯ýç âãâÚÕÚrë»3z4Fcçë¶'ËÞ“ù-¼só†rÊçvE�É:ÒFÝYgbfLAè2�4$a’ í3Ox!µº¼}€t{5øm4øme*´Dj’‰Ð¦¡N&\�L¤:�êÔ?ìü”Ãéâ‹5_1$s#† êtz?µø‰§Y²tÙI¿1|8Kß|½Ëóë·¿›ç0Už#T'®g½»W� -ou{Ü]’NkLª"ý1Ö§"—E"ËÒuOØý÷ÜÝcy•–•õX^íÑ“eï�ü:âtk£žÖ‘6êÎ:ÓÔÔÖu[â‚ Â/X� HA�.¶wX šÂÞŽJ��o6%Îìæß˜Ç,õ!ÔO¢e }BúaQÇ!¡jwªYfËö]„Y-Ý4÷ܵ�{îZÀÈ1ygù[ôÍHoñžµ_~Å’e+p¹]Üôç?qÕÿÀ‡�ð×aÿ�ƒÄÆÆðøßfÄð¬ ynÙú=÷=ðv‡“ ΛÛâXÞáxøÙ{)¬;D|?3.�ÊÜ@½¯‚ìœ\4©>¶æ` %9¹é�Þn·›’ÒRœNZ­–„>}0™šfGܥǎiÔj¢££šgú²srñù|äýpø”Ò´Ûí”””â ³žTÆHU_,ξhkú¨ %lØ´™œœ\rró:d·›ƒ9ÙÜüçHMI¦´´Œ¥Ëߦ¨¸˜ð°0æ_tC‡ šf3î\x+111ܹè^Þz 111—”ðæ’e””–få’‹/bرóJËÊYºlG ãš«®$==-h­xç=6mÞ Ào®¼‚qãÆ6»íŽ»¹èÂóY½úsÜsfŸÃ93g6›3ë¾øj=³g�ÃŒiSƒ–áî{ï§¾¾žG?ÀàÁƒùó¯gvN.K–-Çåv3ö¬Ñ-޵Uöõ7±õûmܵð¶æ÷.]ñ>¯�«¯ºB±ìå,_ñ.Gò °XB™;{ã�VjÛ¶<úØ“\º`>ýûõmþ�Çãáö;ñÜ3O°c×n6lÜ„Çëåüys™>uJs}®xç=*«ªˆ‰Žâ7¿¾’”c×CGÚ(X,¶ÊÕ™RœŸ¬^ƒÇíA«×²nÝüþÞ/á=~=(Õ™R,í-ƒ&4´óßø ‚ B›\û61“ËÙlÿr‘�õ»>'árã£h´ÛÙ~t ýû÷C’$ôïGvv©©)èõúic %%9™F»�ÂÂÂæó ‹°XÌ$'%áv»)8ZˆÉdB§Óu8Í’ÒRÂ#‰ÇVYI @ƒ‘>r!ö4(�AöjZÌ‚©T*ŽpçÂÛxäo�±àâ‹0[ÌìÚ½›”ä$^ûï <ˆÿüGÊæõÿ¾É#=@h�åd}²š±cÎbʤ‰=Ê›o-cèàÁH’Ä+ÿ~�aCsÓŸÿÄÁC‡xéÕ³ø‘‡‚.ɼüÒ\~éþùÒ+'SI»wïåžEwq´ð(/½üo¦O�ŠF£A%IääæñÀ}‹(,*âŸÿz™Ñ#Gb±(/ã}üo�pç¢{¹õ曈��S|,_Á”I™ŸŸ�;vò‡ß_´ì/½ò#GfñÇ믣¬¬œÿ÷âKdddÕ-u­Ô¶m‰‹�Áf³µ•W؈Œˆh^F›—÷÷.º»¹ý&Oœ€F£áýV1mêd&ŒˇŸ|ÊÊUrëÍ7k£`±t´Î”⌉‰æÃ?!%%‰ï¿£ÑØœ^O_Ju¦K{Ë Q«Úÿíž ‚ t˜_…\‡¾0ŽÄ˜�ø"˨7ý@§ù’IB…L@júHî Ôà Ô€/÷¤·ªÑªŽÄ¤ŠÄ¡—ðÛõÔ›¤¤�¥.§Yeèéè›]ÿ»k‰còÄ è zÊÊ›6Z9Zx”›o¼�VËÙ3§óÊk¯±mÛÆŽIƒ·œŸ�z_9 ^ Þ ê}TÙ‹H¼b/®¡¾¨þÃÈ &'Þo‘=2­ƒ˜˜$$IÂb6S©«Äîp(H<�‡˜˜è“ÎÓiµx<¢£¢�$ £ÑÈÀýƒ–Y)Mƒ^�×ë%2"I’ȈÎbû;_3Cw&_²âSôÒSR‰‰ŽÂd41(s ²,S\\BMM åååÜ}ÇB45󆱿³hòò~`xÖ0ÅXõz=yyyôëÛ—Œôtzà>l••Øl6Ο7�FÃð¬a|¾ö ró~ sà€ u dÖ¹ç` apf&Z�–šÚZ¢£¢8ûì„„„0pÀâãã9œŸÏðaC;•_kêêꨮ®aú´ih4jæÌžÅç_| /ûÀزu+çϛ˞}{1šŒô;aÀÐšŠ •UUÌ9w�šÔÔž~ò±vå×Qmµ­’ØØl••TWW³{ß>¦O™Byy9±q1Íïi«ýî¹ëŽæ÷ŒÈÆîÝ{:{°X:SgJq ¹êÊ+ZÜ;Ø[×Ck‚ÅÒÞ2ˆ{ÆA„ç«0C…kH ᱕8¬G©Re÷Úƒ¤ƒ’šæŒuþRêü¥Ø5.:'æ~!l‘¶âHv¡'”jW2!êhL+Z•�Ê€^cB«2 S™Ð©�?þWmlú½ÚˆVjúY«6�EFÆpâ“ÝxN 1¤ðŽ:vâ“]Ä u‘ç^G£«žø‰õüê®!¨t2*]íP™Ír=ûŽèqø[¿•Áíu£Ñ«š¿Ù?¾ €ß@–e<ÔâŸWùÞ0¥óŽo‚rª›—(¥é×h�P¯ÎÂlO[,yŸnÁ;Ò Êéêô:$è´ZT*�€L}}&cÍ�€˜Ífê‚ÆzÙ‚ù|²z /þëe�`ÞÜÙLš0‡ÃI àçæ[ooñþšÚÎ߇i=a¦K£V#~œ´˜\Vk2šp8�ί5v»£^ß\g:��¦iV6XÙÇŽ9‹�>YÍùóæòíw[3¦åÇÖ4Úí„�-Úè¸îªë¶ÚVI\l,»wïeïþ¬\ù!ûõ§ÂVA\lló{Új¿�›¿æËuë©«­ÃçóÙ©ø•béL�)Å)¡"**ê¤M\zëzhM°XÚ[1Az�l×á?Ü=}HŠÍÄo­ÄRJ­ú~¹}»è�®¼>/>Ÿ�Úºz@B’À-yð4Ú )@òŸ¼Cb[;&žøÿjIƒVe@#ÐJ´ÇfÛŽ¾FÜYÌ*×õhÿøasàu%|åz]IÓ "éü ¶y^G¥Q‘6ËÙÊ7Ê.~mi|4 t$IÂï÷#ËMi­VƒJ¥:åo©•Îóz½ÍyœÊ€¬µ4Õ’†H©re8ï½´œ¹×OFV«q:�ø|�ÛLÄb1cwØñù|ÍË‘jëê›ïG“Ô*|þ¦~íóù[,] á²KpÙ% ÈÎÍ套_ePæ ¬´Z=Ï=óD‡vÒì¨Æ†ˆkúàßh·b2-CGMFÜ7~¿õOÚ!XÙ‡g céò·9x0›C‡²¹lÁü ùY-ìNg‹6:®»êº­¶�o{wÓ¸ØXÖÕlÄsàii©|¹n=²,Ó¯o†b^ÕÕ5¼ûÞJnºá�¤§§qøH>K–-ïTüJ±t´ÎÚ§NwòRéÞºZÓžXÚS±FQA8-øË­�“�aç$Ž\Dbýt¢üÏØg�éu:Â섇Y‰·¢×ë1 „˜:W¿ìÃåo¤ÑWI�·ˆ wî w‰™�›¿ÆårñégŸÓÙ'……‡‡ËšÏÖâñxرkµ55ôïÛ´tÎb6³oß~Ün«×|Öbø±'ž"'7�@ @¸ÕŠV«C’ÀjµË'kÖàv{8’ŸÏC�>†ÝnïT¬Á¬Û°»ÃÉ¡ìlÊËËÉ8¶!ŒR iI^~A�@€êšà³aV+f³¥ÕvVö¦¥aY¼ûþJRSSˆRXRv\dd±11|ºæ3\.¹y?°ð®{(-+ﶺn«m•ÄÆÆÐÐÐ@~~¿ûí59’OiiñqÊ÷yÙívÔj5áÔ×7°nÃFí¸\?~¹rªm¤KGë¬=q¶¦·®‡Öꬣ±üô<13&‚ œvüU¡PŠžâó�ëp‡–Ñ +Ä)‹]€»RbBÅ%¥TUU£Ñ¨‰ŽŠV|ÎÖq}âã))-£¢ÂFdd�ºùƒy°4###(+/§¤´ ƒAOzZZÐó’)..¡²*�FC\lL‹Í:~šæ�ŒQ„Òˆ0LáÝW6²bËZ,–PæÍ™Óül¦+.¿”å+Þáã�?eÆÌi„††�;¾i½$I\{ÍoX²t_®Û@TT¿¿î· M³—çÍ�Íò·ßcÍgk™6m Ñ‘ͳL¿ºà|–¯x‡ÊªJÌf sçÌ"üØn‘×^s5K–.çË/7œT†¶”––5ïþ°ÿ~Þ\ºŒñãÆsÕ•—-KLL >ü(>Ÿ�K/™ß|¿ŸRf̘ƻ+?`ÙÛï’Ø'ž»î¸½õ N¨3¥vVöqãÎbËÖ-üzæÿµ»ì×ÿîZÞxs _~µ«ÕÊü‹~Eü±YÀî¨k¥¶m‹N§CÈ 4‹ÅÌøqcøàÃ�ˆ ²éFbbC‡ æÑÅ�É•ÿw%%%¼ðâKÜqÛ_€So£`±t¤ÎÚg[zãz˜1£õ:ëH,?=OZöÖOÓú‚ ‚Ð’Êì†È¼¡e4英cë‘|edü>?Zmû¿ÃtºÜŠnníAÎíY¦(´Í¬ŠÃêKÁàL@UE ±÷6L9“ýtûzAº�˜AÎ�=4Ä¡&Žã"£jñšËi4Ñ •övxB/°ª’°zSÑ;ú UGp6Ý£q&=”Y„_.13&‚ œñ$­ut=^« ‡±˜Z© KÓ3c§—HU_B=Éèã�«Â‘½Á—U ‚ œŽÄ̘ ‚pÆ“½j|%áH%á„Hý°D5"›ëñšjpél4ªJñÉžÞSè�¤Ç*%aòÇ¢wE¡j #PmY³_‚ œñÄ`LAøy‘%ü63ØÌ¨I 0›]¨¬v|!µ¸ U84å8¨êíH…V„ªb ôÁè�A대z3�Æw 0A~NÄ`LAøÙ 44€Hôd`Јw [êñ«qêlØUexewд„®£• X¤DBüqèÝÇf½jC‘}MOÞ/A~îÄ`LAøÅ‘}*|¶P°…¢¢Ï±Ù3wÓ왩�±‡¶»\4-¡ýÌêXB}0¸£Ñº"�Íz5íx(göc¾AN�Œ ‚ M;5ô@:2ÐkD…;˜ëñèk±ªÐ¨=¸Tu¸©ïípOkÉŠ‘p „£õYÐyÂQÛŬ— ÂOµŒ2’Ô¹]›rróxðáGyñ…ç ³}ÿ³Ï½À÷Û·Ÿôû»î¸�¬aÃÚ<ÏëóqõoÇm¹…Ñ£Ft8^AAh͉³gR í˜Ã¬„J~Ô¡^¤²ÞŽlpáÕÖãU7âÑÔâ”kññó^ ˜Tèáèt>+*· •'züv’Ür‡C1ë%‚p²ƒ±çÿñÿÈè›ÁçÍíÑ ¦O›ÂäI[ü..Vù ã‚ ‚Ð$YM A À4ýcªŒ@˜Þ�Ê䦸ö(j³UˆŒz'>M=nU]�Ç\YUEyyI‰ X,– ç˜¤ŒR$ú€­ÏŠÆŠÚ .²Ó€ì>yqÍñÙ®Ÿ~¥{뻸ÿž»‰ˆïdI:–ækÿyƒ¹³gßuŸ-º"Í¥G?Î93¦3nÜØnÏëtë=Yö®ÐeWz`ù_­ãƒUðûë~ˈáÃ;œÏ‰:Ûî§Å2ÅèèÐÛa‚ B§Én5~· W¡…ý9y8ìNT**•�6–™³æ ÑƒZj½ŒJ'#©HZ?¨ ñ#«|HšÉC@ò"«}$/~<øq7ýWòà“Ýød’$¡‘ ¨Ñ¡–uh$=jY�*YKb¤U¸–�?\Kˆj(éúH ’_ƒÐB@ ~5’O ~ ²O�ìПT¶3uiaiYÙ‘¦Ðõî¿çîËëtë=Yö3Á93gpÎÌüýùº4Ýζ»fç®=dfàÚßý€ï·ogÅÛïð«óÏã²Kàõùظi3ß¿ŸßǸ1c˜:e2:]ÓîëXóÙZöîÝKÿ~ý0Ü~Öëõñò+¯2jÔHÆw`Tn·;ørÝ:8DEEà¼ysHèÓ§Õ÷oß±“ ›6STXDzZS¦L"kØÐæ8?_û%»wï&2"’iS'3|x–x�¦ ‚Ðmü^ÇÂj1£ÑhP«Õìjü.5²J…¬V¨5½$IjúùØ¡5?y84´}+AvN.+Þy�ʪ*b¢£øÍ¯¯$%9©Å{ Ö~Cº;}›÷ñê5xÜ´z-ëÖmÀïðàý÷¡§´¬œ¥ËVp´°�°°0®¹êJÒÓÓ‚æ—�“Ë’eËq¹ÝŒ=kts^›¿ùÛ¶ïàÖ›olþÝÊUâtº¸òÿ.S¬Ë¶Òã¼ûÞû©¯¯ç‘Å�0xð`þüÇ냞W^QÁòïr$¿‹%”¹³g5–éhšJeVö¶êº¸¤„7—,£¤´Œ°0+—\|Æ éTšmÙ°i399¹ääæ1tȯ�«¯ºB±ìJýº£u­Ô—”êº;®€mÛw°aÓ×ÈÈÌ�=‹éS§M³£eWü[ p=œHe2Ñiµ<ùøb† ÎdîìY<ùøbΞ9€U«>b×®Ý\tá\vÉvìÜÅšÏ?Àï÷óÏ_¦¬¬ŒK/YÀ A™ÔÖÖ6'ø©©«Ãáp(¤²ÒFNn^‹€Á 'Ädââùò—[nÂl6ó¯—^Áë;yåù¡ìžyöyÆ�ÃÍ7ÝÀäÉÑhš&þ¼^/Ï¿ðO<×þöj¦OŸÊ«¯ÿ—}û­dAAè(Ÿß�V«í‘¼Þÿ`Ó¦NæïO>Ffæ@V[ŽLLL4Ûw줴¤”ï¿—§ŸXÜ|Ï÷+ÿ~�ŒŒ4žz|1ó/¼€—^ý7>Ÿ/h~K–¯`ʤ‰<òà_Ñj4¸Ý.Æ�CyY9Å%%ø|>¾Û²•©S&�³­4ƒÅùøß!44”ûïYÄ‹/<ßâ²Òy/½ò}ÓxbñÃ\wÍÕ¼¿r¶ÊN¥©T%JuýÑ'«;æ,ž}êq®¹êJÞ²,w*Ͷ¨T*ŽpçÂÛØ¶};ýôcÌèÑìÚ½Y–yí¿oЯo:�/~„_]p¯ÿ÷Míö é*•A©>•\~é^|áy|r9$‰Ý»÷rÏ¢»¸îÚ«ùèãÕÍiª$‰œÜ<¸o×^sÛ|g& IDAT¬ú�úú† ù)õ %é×£G�¢¨¨¨¹?ú|~vîØÉ„qc‚–]©_w´®•ú’R]wÇõp$¿€¿Þ·ˆk¯þ +?ø�ººà.u´ìmµû©\ªýû¡R©HLèƒÑ`Âbµ’˜Ð‡ÈȪª«ùàÃ�¸dÁÅ Пþýú2uÊ$>þøS¼>Ù9¹ìÝ·�«¯ú5Æaô¨‘L=aô©×ë¹÷9cºbAÖ­ßȃ?Úâ V«9{æ ú÷ëKJrãÇ�åð‘|êNðW]SChh(£G� -5•áYÃ<(€Q\\Â%_DzZó†1~ܶ~¿-h% ‚ BGù|~¾ß±›Íß~ÏwÛvQ\ZÑmyÝs×L�< ­Vˈ¬aTWW·ë<£Á@Cc#W]yV«�N‡J¥ÂVY‰ÍfãüysÑëu ÏFdD¹y?(æWWWGuu Ó§Mà×3gö¬æ¼4 S§NfÝú�lÛ¾“è¨hcTJ3XœmQ:¯¢ÂFeUsÎ�…Á` 55…§Ÿ|Œ˜è¨§©T†`”ÚV¯×“——GiY9éé<ôÀ}íZõÓÑþ’ž’JLt&£‰A™‰‹‹ÅnwPSSCyy9óæÎÅ ×3[ž}ö BBB8`ñññÎÏït~­éh¿6‡6ŶeëVöìÛ‹Ñd¤_¿¾me Ø¯;S×ÁúRkuÝ]×ÀŒéÓ !sàbccÈ/(PL¯;úÙ©\Š÷ŒUU5nѽ÷ŸtÌÞh§¬¬œÔ”dÅ]Uªà.½d^p^«Ç²srÙ¶}ÙÙÙ?~à?y¤:hà@xæ¹0cêT²²†6/™,+/§¾¾žk®kù-EfæÀ ± ‚ BGM3•J…Z­Æîpr0ç¬3áÁw>U7Í—ëÖSW[‡Ïç%22²]çI¨ˆŠŠjq›€Ãá$ðsó­··øýñ­måg·;0êõh4M»)êt:4šg§M™Ì?J£ÝÎæ¯¿aÊ”–xµF)Í`q¶Eé<½^OˆÑØœ_{)¥iµXëE‰RÛ^¶`>Ÿ¬^Ëÿz$˜7w6“&LèTšJtz’ tZ-*•Š@@¦¾¾“1¤E�™Ífê‚Ï*µU†Ž¶m{X-ææŸ5j5ràÇ;"-æ7¸1MAWxuTgúõØ1gñÑ'«9Þ\¾ýn+cÆ_æ×h··Ù¯;S×ÁúRkuìïD§ò³þØ~¡!¡AÛ¯;úÙ©\Šƒ±ãK+î»çî“v^ Åï÷ã÷wßí¼{÷íç_/¿Êo®¼‚±g�¦¦¶–gÛ¸é.,ÌÊ¢»ïäÀ�|·e+o¿û×þöj†Œ^¯'5%™þôÇç¨Õªn‹]AŽ“$‰0«‹9»ÃÑ僱êêÞ}o%7ÝðGÒÓÓ8|$Ÿ%Ë–·ûüã÷�Ÿ(ÌjA«ÕóÜ3Oœ4Ó¢”ŸÑdÄíqã÷ûQ«Õ8�N|>oó¹F£‘Ñ£Fóö;ïQQicÔˆà�§QJS)N%JçUUUcw:ñù|Í·=úv»]1¿0«³ÙÂÆÍ_ãr¹øô³Ïùéf÷gϘÆÎ]»?vL»;Ji*Åyœ^¯'¿ €@ @uMmÐó"##ˆ�‰áÓ5Ÿár¹ÈÍû�…wÝCiYy‡ÓlO½´&XÛ>öÄSääæ·ZÑjuû<Ùý%<<œ¸¸XÖ|¶�ÇÃŽ]»¨­©¡ߦ¥s³™}ûöãv{X½æ3NütØVÚÓ¶Ýa݆ ØNegS^^NFFzÐ2@ë}BIgúµF£axV﾿’ÔÔ¢¢”—Њýº£uÝѾÔ]×Àú�›p¹\dçäRQQAZJªbš�íg­µ{°ëáDê„„>&&6­ÕÞ·?……ÅDEEPU]CBŸx´ Ÿ¬þŒˆˆ<Gްgï^úf¤Á�9|äaaaÔ×׳}ÇN²³s˜7w’JÅSÏ<‡Ïç#-5¥Õ|÷ÝVÂÃÃ8 ÿIÇJËÊøß·ß‘œœL…ÍÆê5ŸST\|lí©™ï¶lE§×EuM-{÷íÃðSYYÅŽ;‰ˆˆ`ô¨‘D„‡säH>û÷íÇbµÐÐØÈ�©©©!.6¶]-‚ ürɲŒÇãÅh0´ûœ†F;ƒ�üÂbò Khhl¤_z*‘a-vN”NØ1Qéu\kßÜZ,fŠKJx÷ý•ìÛ€ /8Ÿƒ‡±{Ï^âcc¹÷¯òéšÏ¨®®fÇÎ]|ºæ3ìƒeRQa#'/�IO^ÚÖ¿6lØÄ{+WqààAæÌšEzZªb~'Œ'66†�>þ”Ï>_K¿~”••3aÂ8LÇfü�ë7lâ7¿¾¢ùwJ$IRL³­8›ÏWI|ðáÇ|ºæsòrs›ËªtÞÀ�X·nï­\É¡ì.8ÿ< Ðá4ƒ•¡-Šu=~‘‘‘¼ýÎ{¬\µŠ�»ö0{Ö9dy\P°4ÛRp´�ÚÚZ†g ãËuë™ç›o¿#11�‰ãÇñÊ¿_çáï'.6–œÜ<Þ~÷=’™6u _­_OÞG@�••ÅìsÏÆjmšt8œ|ùÕWìÞ»§ÃArJ2S&Mb�¸oLAÂÐØh'\áéŸÊ/,A£Ñ R©Ðh4Í[Û¿—¬µ­íOu0v&ñz½,[ñ·‡ßÿî·½Ž ´ ôÐ`AèÒ²·þÛñu„'ðz}hµ§Å3¤A¡K‰ÁX×ÜrÛ$'%qýï®mq£½ œÄ`Lèi]6A„Ÿ+1AºƒØNPAA¡ˆÁ˜ ‚ ‚ B/ƒ1AAA„^ c‚ ‚ ‚ ½@ ÆAAAz�Œ ‚ ‚ ‚ ô1A„_ˆG?Îwßmé’´¾øj7Üt 7Üt ;wíê’4êÖ…wQ]]Ó®÷¾öŸ7(--ëÒü;“f[u]XTÌ£�=ÉM·.ä–Ûî`ó7ßt:Í£ÚŠeá�‹¨¨¨è²|ºS[ehOŸ?Õú V/]ѯOå: ¦«ûKG´§/uu;Ó™zOiA„nøáÈQªëêÐétôKK¥O|l¯Åsÿ=wwYZçÌœÁ93gð÷ç_è²4;£´¬kb�M³­ºÞ°qÉI‰Üyû_P©T�N§ÙqvTw´C[¶nÛÆêÕŸSU]MHˆ‰‰Æsþ¼¹�N·­2´§ÏŸÉõÙ]]¾îÒÓqv&?1A„nRX\†Çëå¬Ãðx|äÎ'66µZÝåy—”ðæ’e””–få’‹/bØ�!¬xç=6mÞ Ào®¼‚qãÆ°aÓfrrrÉÉÍcè�!xÜnædsóŸo 5%™Û9³ÎዯÖ0{Ö9̘6U1ŽÍßü�mÛwpëÍ76ÿnåªq:]\ù—)ž›�“Ë’eËq¹ÝŒ=kt‹c¥eå,]¶‚£……„……qÍUW’žžÀÝ÷ÞO}}=�,~ €Áƒóç?^ô¼òŠ –¯x—#ùX,¡Ì�=‹ñÇêF)M%mÕõÑÂBòéæ÷}·¥é[ôþð† Ô¡4»#NPîKJ‚Ųc×n6lÜ„Çëåüys™>u  ÜFmq¹Ý¼µd7þé�dd¤SV^Î˯¾ÆàÌLÒÓÓûniiK—¿MQq1áaaÌ¿è†+_wÔgÞ‡Y²t9uuuÌž}.›6ÃÍ7þ‰Ø˜Åzéh,¹ŽÖoÜÄÖï·q×ÂÛšß»tÅ;ø¼>®¾ê Åò)]GiÛG{’Ķ¿¾Í¿óx<Ü~ç"ž{æIÅ:ëhœJi*éŠëH,SA„nRQYIZJ":­–0«™q£‡£VuÏ?½}²š±cÎâÙ§çš«®äý÷W!Ë2—_º€_xžÁƒ·8G¥Rq¤ €;ÞÆ¶íÛé? cF�f×îÝMÇ%‰œÜ<¸o×^s¬ú�úúÅ8Æ�CyY9Å%%ø|>¾Û²•©S&-Ã’å+˜2i"�<øW´ n·«ùØ+ÿ~�ŒŒ4žz|1ó/¼€—^ý7>Ÿ€Çÿö¡¡¡ÜÏ"^|áùX•Î{é•×Èè›Æ‹æºk®æý•«¨°UMSI[u�œ”Ä‹/<Ïð¬,.¹äb^|áy^|áù 1¥4»#NPîKJ‚Å’—÷÷.º›ë~{5+?ø°¹”Ú¨-·@&<< �FCbB�<ø×æúmõ]Y–yí¿oЯo:�/~„_]p¯ÿ÷Míön«Ï¥Ëßfâ„q<ö·‡©®®¡¦¶©õÒÑX:r�5Š¢¢¢æþïóùÙ¹c'Æ� Z>¥ë¨#mƒÍfkñ»ò ‘Í_dµUg�S)M%]q‰Á˜ ‚ tY–ñx}تjøßÖ|ûýŽ—v[~z½ž¼¼{!!! 0€øøxçç+¦§Ñh˜:u2ëÖo`Ûö�DGE“˜� x^]]ÕÕ5LŸ6 ƒ^Ϝٳš�Ù*+±Ùlœ?o.z½ŽáYÈŒˆ 7ïÅ4•Ϋ¨°QYUÅœsga0HMMáé'#&:*H�ýüu´/3ëÜs0‡†083­NKMmm‡ÛÖb1sÁysyâégyáŸÿ⋯ÖQWWßâ=­õÝššÊËË™7w.½žáYȉŽ&/H~e·Û)¯¨hê×ýê|�[.Mm­^:ª£×‘9´©ž¶lÝ Àž}{1šŒô;avª5J×QGÛ666[e%ÕÕլߴ €òòrbãbšßsªuÖžë½+ÛÚ‰eŠ‚ ‚D{f~ªé^ µZŸÑ#p¹Ýì?”KD˜•ˆð°.�ñ²óùdõ^ü×Ë Á¼¹³™4aBÐótz’ tZí±û˜~,¯ÅliþÙd4áp8NJ㧦M™Ì?J£ÝÎæ¯¿aÊ”‰AϱÛõz4š¦o¾u:�‡ÃI àçæ[ooqN°KJçéõzBŒÆæü„u´/cµ˜›Ö¨ÕÈ�N—»Cm 0ûܦ¥‡99yìܽ‡‡]Ì_nþ3ÉII@ë}·¾¾“1¤E»›Ífê”g|;Êîp¢×éšó3 èõ†ïi­^:œ_'®£±cÎâ£OVsþ¼¹|ûÝVÆŒi¹Ä±5�v{›×QG¯Û¸ØXvïÞËÞýX¹òCöëOÅÿgï¾ã£(ó޶$[’ìnzBK½†ÞK‘&x Š'¨Xï;Rõ¼SQÀvÊù;ôÎr‡$ ¢"í‘b©JMI( ÒIÙM²ûû#É’ÄlÉ&!@¾o_cÈÎÌSfžÙìwŸgž¹x�°Ð+÷ÛÖö˜9+§§iºâîu$Á˜BáŠ�ZM´ R©P)•„¡T*0øùb0øQPXD€ýÑÇLJ©wLaêS8‘�À{ÿ|ŸÎ�:×9𻜗ae‚.ççã£×»ÜG§ÓѧwV¯ù” —.Ò»gO×ûèu˜-fJKKQ©TRRR €ÉhÀËKÃÛo¾Z«gûedd’_XHII jµ|ª¬¡ÚRM<=·¼½½éÚµ³}¸ç�?í¶c5µ]ƒÁ�ü‚ü*ç=;'“ÉXO5ªJ§Õb).Æjµ¢T*)2›« ¬÷üêpõˆîÎÊØÕ;v‚ãÇO0uÊd—ù ‡×‘§ç6,4”mY;°=JëÖ‘lÝö-6›�vmÛ¸�FmÊÙPܽޔ¥V+²È"‹,²È"Ëo—’ÒRŠKJ0›Í”ºqÿ@uA�œ>{Ž’’Rró.“››‡Ÿ¯ë`Æ‹_}�ø„D¬V+þF#^^ÞÔÃÈ2¶mßN~A!ÇOœ ==�6m¢ÜÚïæ18xˆ�ýû¹õáÇd4âçg`Ç®ï(**bÃæÿAù�5F£‘ððPÖoÚ„Ùl!)9™_^L~ù}>P6$(9%«ÕJfV¶Ëý aæÍ‘�x’Ùóž%í|ºÓ4¯Eõ]κ´¥Ú–Å�s[“ƒ‡~áåů‘–vžÒÒR.]ºDÊéÓÛ·©©íúûûʦÍ_c±XØð ÙYY´o{e8^}O??_L;¿ûž"³™uëÖ—uC»©¶e©Ëu¤V«éÍ'Ÿ­%22‚  ×Cv�]GžžÛÐÐòòòHNNá¡ûg�””LZÚyÂÃÂ\0ÊÙPܽŽÔ—/;? B!DSfµZ±ZK1 ®7®¦u«–œLNá§}Ñj4DE¶Â×ǧJ ·Mœ@lÜ.e\ÂÏÏÀ¸±£ñ7™HK;oŸ� àÈ‘#|¼r $"¢¥ËtCBBxᥗ)))áÎ;&ãëãéSI¼ñÖÛömˆÃ�S&eÃU*ƺž¸@¡Pp÷]w·†¯¾ÚÀˆ‘1øúúb-¿¿æ�÷±be,[·nÇ`ðeüرøT:–#FÄðÉÚÏYµúZ4 gÞœg\î÷ÈC°üãlýfF£‘É“n#<,ÔešŽ8;Ö÷L»Ë­ãàIšõ]NGmɵ- ¸>·5‰îÞ�”Ó§ùû²w¹œ—‡��½zödx¥öVSÛ-Ëï^V¬Œcë¶íððƒ÷£Õ^:XSœµù¡ƒ:=žS§N!6n _®[Ï-£F¢×éP{y»>˜ÊâL]¯£ú²{Ïn¦�ü½ý5WíÅÙuäɹõööÆfµÑ¥s ?èÇç_®#,Üy0V—rz¢¾®#EÜÊ�k?^!„N%ŸIE­V£T*Q«Õ¨Õ겡‹å‹R©´ÿ¬X …ýgõ¥B}L¤à®Ùs0wöÓ„„„¸Þ¸’ââbVÅ­Áb¶ððC÷7Pé„pÌÓ¶Û�l6ññ |øŸå¼ºèå«z-‹k— ’B!D½±Z­Ìš3ŸV-[òÈC4vq„htûöà³Ï¿$77—À€î½gºbÂNzÆ„Bˆp#ôŒ !„hXòœ1!„B!„hŒ !„B!D#�`L!„B!�úà¡_» B!Ä5ËÏ`ÀG§#¬S !„5Q2¬±Ë „B\ÓN&Ä“v>½NϤB!ªSƒL¦(„B8Ó¦];~=t°±‹!„⣺ë®ß¿ÐØ…B!®u'j5T1;7¯ÊtõÕgÓØ7ÄÔögΞã�¼Ç'k?ç_oÁ×ׇˆV­ÜÚwá¢%h¼¼hÑ¢E•×?ü÷rÂÃÂðóó­uy¥y­zzö<úõéƒN§³¿v½ÕáFT—6x½¨©�mùfo¾õ66m¦Y³pÂÃÂÜZçŠÅbáñ§fÑ©c'üýMUÖÝÇÚYýœqT÷ÙsÐ#º;>>>—I�MzÆ„BˆúVRRÊîý‡P**a*zvïLpP`½ç·pÑÒÒÒª¼¶ì�¥öoß±“V-[0÷™§P*•X­V·Ó~þÙù5¾žvþ¼g…u’æõäF¨Ãõ®.mðzQS;5r£FŽàoKß©Õ:W¼½½Q©Tèôºß¬»޵³ú9Ó�uWÛ$B!œó 7J­V1¸_oûCŸ‹KJ8zâ$F£¡ xåÛãO=óóæ^ömøé3gXòÚöí~Ú½€™ø]»vvšfÜšOÙ¹k÷N»›úÛ×ÍîyrssY¸h1]ºtáÑ?>ⲜÎÒ<—šÊÇ+V‘šv“ÉÈ·O¢{×®.Ót¶_ÚùtV®Šãô™3˜L&fÜ3�¨¨ÖÌš3ŸI¿›ÀÆ�ÿÃl±0vÌ(F� À‰øV¬Š¥Èl¦ß>n×ÁYš‰'O±be,999Œs ;w}Ï�ý‰Ð�‡uÛ¾sññ Ä'$Ò­kW,f3ÇâOðÄ£3‰ŒhEZÚyVÆ®æì¹sø›LLž4‘nåuŸ=wsg?MHyús<Çì§Ÿ$$$Äãc挣ý¾Ý±“=?ïeÞìYömWÆ­¡¤¸„ûî¹Ûãsä¬ zÚ–yyñkÜ9e2íÛµµ¿f±XxfîÞ~ó5.\¼Tëcæ*ÍO>ûÜa;kH�½®j°âìX;kgë7nÂb¶à¥ñbÛ¶í”–Zyáùçðwzn�qÖæÝI³¦ú9ãê½nÿÁClß±Kq1Æ�ã¦áeóq¸{)m”Ý5&‹,²È"‹,²Ô¼Tü¿.N$$Ñ&²*åÕ}ªL«–-YöÎRzDGsÇ·³ì�¥,{g©Ë@ à®;§°ì�¥téÒå7ë–¼²___žvËÞYêV æ*Íuë7Ò¿__Þz} 3î™ÆgŸ}�;_;Ûï_|D›6­y}É"&ÿn"ï½ÿ%%%( ú•gÌãÁîcÝWíëVÄÆ1lÈ`¾ð¼ÔjÌæ"·êà,Í•±«Þk?ÿÒžŸ»mBÙøâd‘EYd‘å_lÔIFf6�îߣÐi4I;ŸN›¨(^üëŸÝºGÎÑ~/]ââÅ‹L?�Æ›ÑÝ  !ñ¤}ßÑ·ŒÂÏׇ.�:áåíEVv6999dffqSL Z�†±cFת5¥™ŸŸOú… eijµLºmØÜ*IHpz�žÎ�:J~~YYY¤§§3~Ü8´ =¢»Lb¥ú5Ä1«‰³ýü|}èØ¡»÷ìà—ÿ¢Óëh×®­ÇçÈÓúy*44„‹—.‘™™É·;w�žžNhXˆÇÇÌYš�iá A­V×KZ:­–¼Ë—¹gÚÝ�F¼½½QVúBª¶çÖ�6ï*Íú¬Ÿ£üjÓ&äž1Q+¥¥VvïÙÍwß}ÏÉ“'iѼ9íÛ·gð�Á´hÞœ¤ädfÍz†÷Þ]FhhãLm³� Ê:¼é6dzBˆë�­Ž—ÿ™s©´Žto²Œ¦lê”ɬ߸‰eïþ0~܆ äñ~…X­¥<ñô3U¶¯üáÌhð³ÿ[­Ra³ZÉÏ/@§Ñ V«€²ûLÔj/·ëQcš…h¼½íijµZ4­[éyk¼P(ÁÛË«üž?¹¹yèu>ö4üüüÈÍËs™f]ŽYM\í׿__Ö­ßÈ„ñãøñ§=ôë×Ç­ý æãéiý<Ê¡C¿òë‘£¬]û%ÛµçÂÅ „…†z|Ìœ¥y£P $((¨ÊÄ7•ÕöÜºÓæ=i/uQS~…Ef·Û„Zb1QëÖ­cË–-Ü2z47�IQQûì'55•æÍšSÜÛl4Zœ¿qÓ&~úñG^zé¥k2=!ÄõGQ‡®±B³™‚"3þåCs„c>>>L½c Sï˜Â‰„ÞûçûtîÔ™³ž9ÚÏd4àå¥áí7_­U¯ˆN¯Ãl1SZZŠJ¥¢°°�’’â:ÕM§Õb).Æjµ¢T*)2›« }ô„ÁàG~A>%%%öoú³sríÃÀ*%%¥¥@ÙÐÀ‚‚Bû¾õ}Ì\í×#º;+cWsìØ Ž?ÁÔ)“ÝÚÏSž¶%GÂBCÙ–µËÑ£´nÉÖmßb³Ùh×¶�Çup–æõÄY;ðövÿ‹ W\µùkEmÚÄÕ¸.®k%%Å|¹nÓ§O綉éÙ³'äÑ™�Ò§wïÆ.žB\“²³s1ünÈÞÿw IDATu�FCrJ V«•Ì,×CÇ\YüêëÄ'$bµZñ7ñòòvkîGû�FÂÃCY¿if³…¤äd^|y1ù.î©2�øùرë;ŠŠŠØ°ùàòî.çüü|1 ìüî{ŠÌfÖ­[_ÖÕUþþþ„……²ió×X,öf®öS«ÕôˆŽæ“ÏÖAPP�[û¹â¨ ºjKËÿ»’_^ìö}d¡¡!äå呜œÂC÷Ï ))™´´ó„‡…y\gi^‹kgí¬¾¹jó ¥¶ïuµij[ƒ2q#©x¿ÊÍËå7íF¡À†Í¾ÍŽ�;IJ:Å™3gèÝ»“'OÂ`(›A¬¨°ˆ¯·láÀ�””Ó³GF�ƒ�¯Öoà§Ý?±ð¥—ì³—­\¹’½{÷ñæ›oØÇüñ¹œ—ÇÌGgV)ÆÒ·—Úgš|ûí„„„ðî»Ë8{æ 6näøñ´k׎ñãÇQ6l(;+‹�›6±oß>t:=�;wbâĉ|øÁ‡ÓB4!u§h6›Ñ”3kH•§¶¯˜õ«òÔöµ•–vÞžÀ‘#Gøxå*È=Óî`Ĉ>Yû9«VB‹fáÌ›óL͉¹™æm'·†K—ðó30nìhüM®{2œí÷ÀŒûX±2–­[·c0ø2~ìX—ÏR(Ü}×�ÄÆ­á«¯60bd ¾¾¾XmV·Ž‹#S§N!6n _®[Ï-£F¢×éP{yÞ6 ̸—+ãØºm;AA<üàýhµeÃo7†ØÕŸ²ió×ÄÄ #80ÀÞƒQßÇÌ�ý èËî=»™>ò÷µÚÏ™#jnƒ®ÚRBb"ãÆŒv»'ËÛÛ›ÕF—Î]0ü8 Ÿ¹Ž°òYK=©ƒ³4�µ³ÁûóÆ[oÛ×%&&3<†>½z8\wç”InÕÕ‘#j>ÖÎÚY}sÕæʈµ{¯÷Û„â³O?‘hL¸mçÎ],ýûß¹gú4LhHp•õIÉ)Ìž=› &еK4 qqqtêÜ™éÓîÆjµòÖ[oc)¶0q”J_­_OQQ! ,àì™3Ìž3—eÿø?BCC)..æÑÇ'##ƒ¥o¿E‹-0[,<úèc<üÐCôï߯Jþééé|óÍ6:Ä�? -š7'ýÂfÏžÃ=Ó§Ó¶m[‰[½š×^}•  @/Y‚ŸŸ#FŒÀËˋĄDnu3™5¦'„hb ~Þý=¢»»½Kò™TÔjµ}j{µZ�J¥²/J¥ÒþÓ�A_)Ê�×ÃÖÔØl6âãøð?ËyuÑËrN¯²ÌÌ,^{ão¼üÒ_ëu"!<¡–Ž1QÆ%*ª56ldæÌ™L¸õVÆŽSi²Ž²5vôhûkIúóí¶o™~÷Ý?vŒ~ü‘½÷��e= åáGáÈá#tíÚ…–-ZOhH(§SRh×®ñ ´hÞ‚”¤d²²²èоÝof8 Åßß„V«¡Eóö"íܱ“Áƒr˨Q´jÕ’µk×räè†̉ñÜ{Ï=tîØ©,¿6m�¦'„hjäÂu·oÿ>ûüKrss àÞ{¦K Öâã㉉&�˜¸&È0EQkÍ›7ç‘GæÖ[dzs×wÌ|ì1æÍ›Cß>}©øÀb+ÿÊÆÍæä– m<—šJ‡ °¯÷÷7Ñ¥KgÎ¥¦Ý�aDZoÿ~† ÂÑclj‰NHHû÷ï禘Ž;Ê�!C0šL¿.É•�L•×�JJbÏž=lÙúM•m332Q(•LŸ>�÷?ø€ /п?"#"�¦'„hZu¼_H€Þ½zÒ»WÏÆ.F“wµž,„;ÔòmŸðT³fáÜ5õ|ô:–/ÿ/½{öªÖœª·­²{ÊÊnb¯º®lhNÙë];waåÊU̸ï^~üñGîºk*&“‰wß}�ì¬,¾ÿá~7ñ¶Ò¯”­­jþ>>zÆ�Ǩ›«>…]¯÷lŒqÝ»ueÏžŸùûÒwhß¾=3î»­N[czBˆ&¦®sÛ !„5P×åiä¢é)»]Så5½�Šò <®Lmo»2CQ¥×ÂÂB9vü8™™Yø—O/››“K||·OžŒÍf#¢|–¥o¾ÙFRr2QQQhµ"""øzËN�J¢}‡öNf@²QRZZe}«V-9zô8Í›7ÿÍ„XÛ1nÜX ÄÃ�ü�¾}ûЫWÏÓ+ϦjZ•¯¾Nq}SÈßJ!„õOînËÊÎæÙçþÌ-£FѲE 4Z-§OŸfÍ'ŸððC¡T(]uŒÑ±c'úôé͇ý›[Ç�C©T²iófzöìI§N�À^j51dz*6ŽÑ·ŒB_þ À!ƒ±rU,ýúö%0 ÀaG•V§#))‰C‡~¡¤¤”ž={0xà`Ö­[ÏêO>¥w¯žX­VâèÝ«7áa¡üoËVš7 ÇÏ×�ÓgÏ`2™ÀVszûöíçó/¾`þÜ9 bãV“••ÅÿøŠ-^{ãM ÀÈ#êý<!®>éBÑäÎEá6=?ô âÀÁƒäÓ¾}{æÏ›K§Ž«mm«ñßj•’Ç}”¯·laUl*¥’^½z1êæ‘¨”Wž·Òµ[>ýì3¢££í¯uêT6¹Fõ«ëÕ³ à¿+VÐ¥sg:´o�¿¿‰—^|�M›7óÞ?ÿ‰^ïC›¨Ö(°QRZBI±…õ6’’’BddóæÍ!ªuk‡éy{©1øùÚ§Ú×ëõ™‹PJ¥___¼«õ !®gòÍ¥Bˆú§X+a„B'Jûöî•©í…BÔ+™MQ!„pA!ã…B4¹gL!„pIþX !„¨Ò3&Üv×ï§9]»ò*•D!®.OÿRfeçr:5³¹�VCd«–´l^¯eBqýR#SÛ 7Å­Zá|iKBˆ”ÍÃû´N$ž¢}Û(Bƒ),2óëÑ„¢×ë깄B!®GJ×›!„Mœ‡_6i¼¼¨èWS(@©R¡RÉŸ^!„eÔÒ™!„B8¦P(°ÙløúúÖzßvm"9–pŠ„“ɨÔjºtl‡Z-O•BQF-Sä !„ŽØ°aãÌé3èõúÚíi³qüdQ‘­ ¢°¨ˆã‰§0�øúÔ.-!„7&õÁƒ» B!Ä5Ë××½^O³ð°ZíWXX„µ´”à@”JFƒz½ŽœÜ\ Æ„B ®Í,…Bá�Æ«ÕÊ¥Ì,BƒƒÈÍ»Ìå¼||¢$BQF® !„ @¥RÑ¡mΤ¦’˜t�VCëˆVøùú4vÑ„B\#$B!ˆ¿É@`€ µZ�Z­F¥R5v‘„B\Cd~]!„B!„hŒ !„B!D#�`L!„B!�cB!„BÑ$B!„BˆF Á˜B!„B4 Æ„B!„¢H0&„B!„�@‚1!„BÜPžž=�ÌÌ,·¶ýðßËIK;ïQ> -á§ŸvWymË7Û˜ùø“Ì|üI<èQºMMC³åÿ]É–o¶ÕKZ•yÚ^í7{î.\¸PE5¨Í{�§þõÁ‡lþz‹Çû«ë±,B!„¨$7÷2)gÏQd±à£×Ó±}[LÆÉëù^"##£Êk÷N»›ú7H~7Š´óžbÏ?;ÿ7¯�9‚Q#Gð·¥ïÔ¥Xn)2›™5{.¯¿º½€gŸ�û¦ßM‡í<ÿúr5�Y]yÚ^êÒÎĵmÚÝwóâÂWèMXhH­÷—`L!„h6›�ã‰'iÑŠ°Ð`²²s9r<žÁýû j <Ÿ|ìÑ?„§¥�geìjΞ;‡¿ÉÄäIéÖµ+ë7nÂb¶à¥ñbÛ¶í”–Zyáùçðw˜Ïö�»ˆ�O >!‘n]»b1›9‚'�IdD+�óK;ŸÎÊUqœ>s“ÉÄŒ{¦ÕÚe½OÄ'°bU,Ef3ýûö©Zw'iÎîyrssY¸h1]ºtáÑ?>bO3nͧ\ÊÈ $8ˆ{§O#¢UKâÖ|ÊÎ]»€Ú¼çRSùxÅ*RÓÎc2¹ãöIt/?.999,øó_˜ù‡?еkg·ÒtÆQ¾Ý±“=?ïeÞìYömWÆ­¡¤¸„ûî¹»NçÁÑ1ó”³¶äl]e§N%ñþGÿæ™§Ÿ$(0Ðã:8k/θÚoÿÁClß±Kq1Æ�ã¦áÃÊêçáypÖΜ³Ùs0wöÓ„„”s<Çì§Ÿ$$$Äéu›~á±qŸ�”œ‚Áà˸1£X~MxZ§i:©ƒ§ï®8»6}ô:†Äæÿ}ÍŒ{§»•^e2LQ!„hfK1V«•Ðà@TJ%a!A¨•*ró._ÕrØl6>üÏrÚµ�bÉ¢…Ü6ñV>úÏÇ\ÎÏ $$˜}û�–šÆ Ï?ǯ.Âä¢÷N©T’”’ÂÜٳػoí;´£_Ÿ>ÿ‚˜áCùÛk‹éÔ©#k¿øÒ¾î®;§°ì�¥téÒÅͳPfÝú�ôï×—·^_ÂŒ{¦ñÙg_`³Ùj•Fm8ªCŸÞ½9{ö,.^ ¤¤”û0h@?Àóóàì˜yÂY[rÕÎ*dfeóÁ¿—óàý3\b®êଽ8ãj¿ÄÄ“<·`>Þk?ÿÒ~¬==ŽÚ™»Ç¬&ήÛ÷þõ!mÚ¶æÕE/ñàŒûølíö¶åi¥éªž¾ÔUïž=9|øˆG׳cB!DÐx{¡@É…K™Øl62²²)2[°˜- –çÒÿû‡ýÞ›™�?É©SIdee‘žžÎøqãÐj4ôˆîNHp0‰‰'Ðiµä]¾Ì=ÓîÆh4âíí�RéúãATD$!ÁAèuz:wêHXX(ùùçwñÒ%.^¼È„ñãÐh¼éÝ�À€Ê÷s$''‡ÌÌ,nЉA«Ñ0vÌhû:OÓxvÞ†‚——=£»“™™érW4 ‰‰‰¤�O§MT/þõÏ( ûz£ÑȲw–ÖºWlμùösž�}åþGuðóõ¡c‡ìÞ³€_ÿŠN¯£]»¶×Ô1sÖ–\µ3€âb ïýë}n7†¶m¢Üʳ!λ+£o…Ÿ¯]:uÂËÛ‹¬ìì:�GíÌ�c戣ëöÂ…‹\ÊÈ`ì-£ÑjµDFFðÆk‹ ò¸ÎÒtV‡†z/××f³ðpÌ3YÙ9n¥W™ SBˆ&&õüy ‹ÈËÍmì¢\ü |t:ÂÂBkµŸB¡ CÛÖ$Ÿ9GRÊ‚ƒññÑ£P5Ü÷ 5 SLNNA¯óA­¾28ÒÏÏ�ܼ¼²r¢$((�NW«¼¼5Þeû+ÁÛË ¥R‰Õj#77Ï£ü ±ZKyâégª¼ž•�í´ùùè4{~ÞÞÞ¨Õ^uJ`Ç®ïØºí[r²s())&Ð�^W¦N™Ìú�›Xöî?AãÇ�aÈ AuN·ú=cœÕ¡¿¾¬[¿‘ ãÇñãO{èׯlH×µtÌœµ%Wí `ë¶í4kÖÌí<â¼»b4øÙÿ­V©°Y­™=>ŽÚ™;ÇÌG×íåü||tº*iVð´-9KÓYê½À]Z�Ž¢ÂBð7Õj? Æ„¢ I=…BI·î=»(ו“ ñ¤�O'¼–™Éd W€ µZ�R©d÷þC赚*eÍ ?ò ò)))A­.û³Ÿ�“[e(¢··W£çg2ðòÒðö›¯Vé-rE§×a¶˜)--E¥RQXXHIIq�ÒÌÌÌâ“O×òøÌ?ÕšSIɬXëöþŽøøø0õŽ)L½c 'xïŸïÓ¹SgjùáÍ®êÐ#º;+cWsìØ Ž?ÁÔ)“�kë˜9kKî´³áC‡Áò�Wðìü9xy9oç uÞ=áéyÇíÌÕ1S¨”””–eCW «¤[Óuk4È/,¬’f]ëà,Mguhˆ÷‚Ú(*,D§¯Ý—Z Ã…¢I),(¤MÛv€M–Z,mÚµ£   ÖÇûБã\¸”III I)gÐiuøèõµN§.üýý eÓæ¯±X,ì?x�ì¬,Ú·m{Måg4 eý¦M˜Í’’“yñåÅ仸ŸÅd4âçg`Ç®ï(**bÃæÿ ·ÓÔh4$§¤`µZÉÌ*û–þ$‡­S>àºjµšÑÑ|òÙZ"## þ$ßÿðc�ëSÓÒÐxk0k?[®ôŒ !D’——GÎpCËsc(Ou‘-›s*å §RÎ`2èÔ±]”Ì9…BÁ3îeÅÊ8¶nÛNPP?x?Z­öšËï�÷±be,[·nÇ`ðeüرøøø¸Ìïî»î$6n _}µ�#cðõõÅj³º•æˆ1|²ösV­þ„Í™7çZ´hN·®]xyÑ™öû©¤¦¦òβ÷˜þû»ì³â9r„�W®bà€� ØŸ7ÞzÛ¾.11€˜á1Ü9e·Mœ@lÜ.e\ÂÏÏÀ¸±£ñ7Õ¯à´sf=À€}Ù½g7ÓGþ¾Ê¾žœwò«É©SIN�™³¶än;»ëŽÛYøÊ¢»w£c‡uªÃˆ¿m/îðd?OÎà´�9;f·ŽCìêOÙ´ùkbb†`ï)s摇`ùÇ+ØúÍ6ŒF#“'ÝfEàiœ§é¸uy/p&>!ƒŸ�þýúÖ¸~ÿ�ƒtíÚÅ£^7EÜÊ�åϲB4ýÂÀAC»×¥øŽÑÝÝÞ>ùLª}x¢Z­F­V£R©ì‹R©´ÿ¬X …ýgõ¥BC ±BQ³ØÕŸàïobÌ-£~³.¿ �¾Â¬§ž�çŒ !„pÍH÷X-I$„MÖÙ³çøÝÄ[k\·rÕ*FÜ4Ü£@ $Bˆ&ÈæÖ½â …1!„h²æ<ãx¨í#=X§´%Bˆ¦ÆfÿŸp—MzÆ„BÔ? Æ„¢‰±ak°aŠV›�½{÷ò믿òË/¿L‡öí2dááa,[ö.£�éÓ¦5Hþ Eb1!„ A‚1!„hjl¶‰ÅŠÌE¬øï 9˜1£éÕ«ìÛ·Ÿóéé„……•‚×ß-k2LQ!DC�`L!D½ØºõΞ=Ë‹/ü£ñÊt݃ÂV>µ°B!®�`L!š›­þ§ðÈÎÊbÅŠ<÷쳌Æß¦¯P\é+ÿÀfµñã�?òãO?‘šzŽÎ�»0~ÜXÂÂÃ(..fëÖoøþûïÉÏϧcÇŒ?ž-Zp`ÿ~¶ïØÉ¹³géÕ»ãÇ�ÇhªýC7]’qŠB!€²± „âê²ß3V�KjZ‹…ÈÖ‘N·³ÏRþûÆM‰��eèСüñD«Ñ0Á³\¼xl6¾þúk¶mÛÆ”ÛoçñÇ£s§N¨”*°ÙØûó^>úèß 2„?ÍüK1ÿX¶Œâââz¯ŸÌ?)„¢!HϘB45e]cõ*##ooo|ôz÷Ò¶Ann.+V¬dîœÙôìÑ€6Q­IEê¹sD´Š¨ß J0&„¢H0&„MP}÷ô˜L&, èõzy—åŸ~ñ‹…Vöò(”JºwëÆ‰øxlØ:toþí-þ±ì]† J§ÎQ«Ôäçç“�˜HÂÿýÿ¿ÿ«’~NNn½×O�gÃó 9•|†"³½VGÛ6‘„ÕkÙ„B\¿$Bˆ¦¦ž3Ö,, €Ó§OÓ±c÷ Q>©‡¢<<« P€J©l´k×–7ßx�}û÷óåº/Y»v-�üáaøÓ¡C‡ªùùûPï=YÞ3Ÿ˜DHP ­Z4'ïr>Çâ ô7¡R©ê·|B!®KrϘB416lØlõ»øø3}Ú4Ö®]K^nÞo·±–ý¬Âl6A�e=DgNŸµog-µräÈÚ¶mkM¯×1tÈ`æÏ�‡·Fî�»ðòRÓ£G ‹Ì4oѼʢ÷ÑÕ{ý<éi+2[0›-´h†J¥$(нNOfVN=ŸQ!„×+Õ”Û'½ÐØ…BquœOO'¼yóyÐW«ˆN?Áº¯Ö£@A~~>)É)|µa^ÞÞ„…„Ÿ�@rJ aaá4kŽV«á‹/×Baa!ß|ó ýÂ>€N£eßþ¤¦¥Ql)æì¹³|÷Ý÷ôèу֑‘ðþ`2š@¡ õl*Û¶}KÇŽQ*êù»F…‚´Ôs„……º½Kjú.çÈO?À`𣤤•J�¿É€B¡@©T¢P(ÜZ®EfvBˆ…cBÑ„œOO§Yóf 2…—ZM¯^½0 $$&𿯷�š–FdD+:th�ŸŸááa?Ï·Û·3|ØP:vìˆ�^ÏÎ]»Ø±sAA�<òðCúpîÜ9~øá6lÜÄ©SIÄÄ ç¦˜TJ%!!ÁtèО={ö°~ýzÎ¥¦BTd$^^^õ[9¤¥¦Ö*;1ƒœÜuõèaÙ;K4!„¸–HϘBÑ,–b>Š¿É„ŸŸPsoY}:•œÂÔ¾}°X,âo2¹µÎ™Q7ßÌÐÁYþßU¼ùÖRÌ�M³fá$'§°"6Žôôt|}}?n C âÏ}‘Áƒ2vô-ö4Ž?Á;ÿXÆSO<ÆÛÿ?nu3'N$�š–FçNxè�ûQ©TNËá(?€ÃGŽòÙç_’‘‘AP` Óïþ=QQ‘ìúþ>ÿb:­–=¢kyD…¢aIϘBÑ´Z Cú÷¡{çøèõ1wíÌO?ífæãO’��ÀË‹–ðÔ3sÈÈÈà¯/¾ìt�»‚‚‚˜øˆƒ‡°}ÇöíÛǰ¡CÉÍÍu¸î®;§¸]ŸÔ´ó„††`2ìÁN~A&“‘°ÐPrrrøåðFÜÃîÝ{0šŒôkév^Õ9Ëïø‰x¶}»�˜aØô»‰ü{ùíõõ-ú*¶ÏÉÉñ¸ BÑ$B!@EÐ¥T*A \?ÐÙÓž±Gz€×ÿö6÷L»“ÑÀ’×Þä…¿½{cð󣨨�””Óü鑇ط?©©i„‡…{T7€.�:9Ì/##�VÃÅK—8•”@‘ÙBûvíP(Uü¼w?ݺtaÏž½—A!‚zúS› €ggÞ×ÈÅ¢ñØÊ¯ƒê¿Ûl6‡‹Õj­òsãÆ�LüÝ$¬Ö²}ý|tW½Bˆ2™™™ÔjŸšÈìhQ©T( l6HLJ!;'oo ‘­šÓ<<ŒÒR+?üü³½7 …‚³©é4 ¡{—Ž5æã©ÒÒR233 áðÑc´ŠhåÖ:wlÙº•í;vѼY8O?ù˜}Æ™úk>ýŒgÿüWüMÜß½´lÑ­VGD«VhµZºuíÂŽ�» Ål6;̧â`:ÄÌÇŸ¤Cû<ùøL‡ù…‡…Ò¥sg¶nÛÎñãñÜß½,ÿïJþïï2oÎ,îºóvÖ~þ%K^{ƒÁƒ�”œDiùPF!„hl uHdù'Nøó£3·4B\C*®Êÿ®)«ø·ÕjeýúõLš<«µìÞ _ Æ„h4YYYøûû;ÝÆÑðÀš~W*•öŸÕ‡Vî«üºJ¥r¸]Åï®fYtV^!„×7uÅÌF(@­–Q‹¢éªÞ3Vñš£E©TÚƒ°Ê²T*•}Ö4¹¦„h<*•Êå5è(¸q4õ|õÅY UÑsVy[gSØ»Â(„âÆ£¶¿áÛ䃣hÚœc€½·«òÐD¥R‰Íf£´´Ô¾�Z­¶o+×”�Ç�`¬²š‚$GA™;½d•0G=`Îþ,½bBqãSÛ û=còÁQ4e•ƒ±êÿ®ÜVýgEÏXÅ6Ò3&ĵA­V{t : Êjê«þïê³%VnîôŠÕõÞ1!„×56¨˜æ©â†M]å{ÅàJ¯XEÐUùgÅ¿+¶Q©”`­z�‰âê³Ïd脳)w‚±š3GZMœ»½bB!nLj”ʲž1¦(š8gÃ+†#Vô†AÙ e•ƒ±+3¬©Q(d˜¢�­.÷ŒU^çhR�êC[Ž‚¯š~BÑ´”MàQþþ/ESVÓÐÄ •gN¬È Åo~Ê0E!®µ½g¬‚£‰4ªNÕ‡: ¾j²è(oé)Bˆ¦C�B6@Qö‡Kˆ¦ÊÙsÆ*÷ˆU¾_¬ò,û0Eµ …µì—\SB4•JUëkÐÕ°Eg=dÕ'ö¨¼�³à­zzB!šuÅýbØäƒ£hÚ\Màáè~±Ê=c …•RTÜ?&×”�ÅÝ`ÌUT}¡«¡†Õ_«è)s´ÞQOœBˆ_yÏXÙOùà(š²š‚±ÊxTŸ¨£Bõ€L©RAù*¹¦„h<Æ=ålæCG½Zµ Ôܹ_L4!„¸q• ¦W(e6E!*©|ÏXåÞ°ÊAP%«ø½ìß̦(Dcsg6Å ®îÓr�AÙ—5GO$�~1//5íÛ´¦yx( …‚̬Ž'ž$?¿­VCÛÖ´l^%M™ÀC!š®*w6Ë·øB”©Æ*dÕƒ2¨Ú+Vñ»ªÒ‡?¹¦„h<žÜ3V¡¦à«¦ß '“Ï`).eø ~äòëÑxÂCƒQ«Õ>O»¨Hš……’™“ÃÞý¿„Vãý›tÜ)‡Bˆ‹ºb&Elò-¾ªb•{¿*‚2¸2™Ge ¥’ŠW䚢ñTž4Ó}�ý^ùµ´ôtúôèŽV«A«ÕpÓ�þöõ1ƒûÛ¿´ ò7¡öRƒÍZe ¸„¢éRÛgï�aŠBØ9¦X}T Ȫø“kJˆÆS›aŠ5q�UþBÆb)áü…KìÙ•JEd«DE´üÍ ‰'“Ïà�V«•{Å„B F¡,û–NzÆDW}jûÊX…ÊSØW¼*÷œ) l ¹gLˆÆæI0ænt%+{�P«UŒ6ˆËùü|à‚üM˜LFûöÇN‘_P@¯è®ˆ !„°S—÷‹òæ/š¶Êí¿rïWå{€ßòþ „âJ0&Ã…°«tÕ€Uî9«yh¢cB\ êë¼=lB!šuÅcÆ@þ8Q™­ÚÐÝê½c5-P†Uì&×”�§.÷ŒÕ·k¥B!®-j…M� ›ë-…¸ÁÕôaÉÙðÄš>èUÌ„�§±¯ÃÆÎ_!ĵO�bB!D�Hà%„ÂêÊ¿ÈÑ”Õ4�GÅë5MèQyÛ*½a …}œ¢\SB4.¹…B\Ë”Ò/&„B!„WŸùÖP!„B!®:ec@!„ köÜ\¸p¡^ÓÜòÍ6f>þ$3’Öjßÿ½œ´´óõZOÒtç¸,\´„Ÿ~Ú]—¢]w–ÿw%[¾ÙÖØÅpË™³çxyñk<þôlžœ5‡]ßߨEºf\‹m÷éÙóÈÌ̪òš'å¬Ë{HMùÕå=ÒUY\ÕOíp�B!\*µZùõè Χ_Dí¥¦S»(š‡‡p)3‹cñ‰\Î/ÀÛˋ֭ZÙ²Þ˰æÓÏÙ¾cûo^�jÅìYOÖ{~£FŽ`ÔÈümé;µÞ7í|ýb •&ÀóÏÎo�tEýؾc'­Z¶`î3O¡T*±Z­�]¤«bùWb0˜tÛ‡Û\/mדrÖåz¯ïãâª,®òSc“»Æ„BO%žLÆl¶pÓÐäçrððQBC‚Q)•<|Œ®ÛHnîe~Ø»ŸÐÐ`|tÚz-Ã�S&qç”I<9k.óçÌ"¼< ¬°ÿà!¶ï؉¥¸˜ ãÇqÓða¤�Ogåª8NŸ9ƒÉdbÆ=ÓˆŠj]§òœˆO nͧ\ÊÈ $8ˆ{§O#¢UY:ÿ¹çÉÍÍeá¢ÅtéÒ…GÿøˆË²œKMåã«HM;�ÉdäŽÛ'ѽkW—iº²wß~¶ïü6Æ�m?.qk>eç®]Ü;ín èoßgÖœùLúÝ6nüf‹…±cF1jäH—u_¿q³/�Û¶m§´ÔÊ Ï?Ç‘cÇØ»o?O?ñ˜=�µ_|IaaÓ~?ÕiùgÍ™ÏØÑ£ØòÍ·Œ=Š1Ã]–%ýÂbã>!)9ƒÁ—qcF3°R+œ:•Äûý›gž~’ À@§i&ž<ÅŠ•±äää0fÌ-ìÜõ=O<ö'BCB<>·Žœ>s†%¯½aÿý§Ýe=3ÿðºvíLZÚyVÆ®æì¹sø›LLž4‘nåi::þNótVNGùmß¹‹øøâéÖµ+³™cñ'xâÑ™DF´j�ëÏÓ¶;{îæÎ~š��æ.xŽÙO?‰V«cÑ«¯ñø£¢y³fä]Îç•ÅKxôO¤e‹æNËr">�«b)2›éß·�Ûåôôzwvn�åŽß —��§eq–_åö¢,{D­B!¢M›Ö¼¾d“7‘÷ÞÿÀ¾ÎSŸ}þ1Çò·×Ó©SGÖ~ñ¥}Ý’WâëëËóÏ.`Ù;K«MÎʲnýFú÷ëË[¯/aÆ=Óøì³/ì3Û:KÓ•¤äþòçJ…‚C‡~åÙóxð�ûX÷ÕF{9�Õ=$$˜}û�–šÆ Ï?ǯ.Âd22°?Òϧs.5€’’~Ú½‡áƸ,¿R¡ >!‘¿þy̸‡Ï¿ø’ÜÜ<—eyï_Ò¦mk^]ôθ�ÏÖ~Á…‹—ª¤�™•Íÿ^΃÷Ï (0Ðeš+cW3xпò™™YdegÛ?ezzniÕ²%ËÞYJ�èhî¸ãv–½³”eï,¥k×ÎØl6>üÏrÚµ�bÉ¢…Ü6ñV>úÏÇ\ÎÏwz\qTNgù)•J’RR˜;{{÷í£}‡vôëÓ‡ƒ‡¹<.žò´í:b0øqǔɬX‹ÍfcÍ'Ÿ3|˜Ë@ `ElÆ fá ÁK­Æl.r«œž^ïÎέ³üÀñ{�3ÎÊâ(¿êíE)ÏB!®Ê¢ÑhHLL$í|:m¢¢xñ¯®—GŒ¸)_:uì@hhÉ))níWÓñçu×iµä]¾Ì=ÓîÆh4âíí�R©D­V3|øP¶}»€½ûL‹æ®?ìÜ|ó|||èØ¡áááœJNvZ– .r)#ƒ±·ŒF«ÕÁ¯-&$8Èžfq±…÷þõ>·ŽCÛ6Qö×¥™ŸŸOú… ܃V«->g+2xµÏmVVéééŒ7­FC�èî„“XžŸ£óàŠ£rºÊ/*"’�à ô:=�;u$,,”üü‚»þ\qÔv�éݳ'AAA¼ÿáGdff1zÔÍ.÷ÉÉÉ!33«¬Mh4Œ3Úí2zÚ&<=·àù{AmUo/rϘBá¡ÒR+6l¨Ô*n>˜¼Ë—Ù³ÿLüË¿� `ÜÍ1\¼”Á�_�0¨_oôõ½û°zͧ\¸t‘Þ={º�ïå¼< ½R½ÞiYŒù……”””8ì©>t‘,ÿxÏΟƒ———Ó4uZ-–âb¬V+J¥’"³Ù>$íjŸ[ƒÁ�ü‚ü*õËÎÉ­2ÑÑypÆQ9�å—��ã0½†¸þêB¡RRRZ @II)…öueÃ×r˨›Ù°i=¢»¡Õ:ÿRI§×a¶˜)--E¥RQXXHII±[e©K›ðäÜ‚ã÷gÇÅÕÛ‹òJ &™BQ[ÍÂCˆ?™DII Ù9¹deçb4øQd¶ðõ·ßq)3 ›ÍFvN.Ù9yøÕáÃ~}3�„‡‡²~Ó&Ìf IÉɼøòbòóó]ïì@~~>*• ÿ€rsóض}—/çSTtå^�FCrJ V«•̬l·Ê²øÕ׉OHÄjµâo4âåå]åQ©5¥éŽow줨¨ˆñ \¸p�Ö‘ ZwgnÃ�ƒ‡Ø¿_­†³nÛ¾�ü‚BŽŸ8Azz:mÚD9-K``¡!!lØ´™¢¢"O2{Þ³¤�O·§©×ë‰îÞ�-[Øï s–¦ŸŸ/&ƒ��ß}O‘Ù̺uë˺‚¨û¹­-ÂÂBÙ´ùk, û$;+‹ömÛzž¨“rzš_C\uaðóãðá#˜Í6nÚ\%2غí[|}}øÝÄ ôêÙƒ¸5ŸºLÏd4âçg`Ç®ï(**bÃæÿáî\ u½;ãè½ÀÙqñ¤,ÕÛ‹ SB!ê sûv:rŒ-;¾ÇÛË›Ží¢0øùÐ¥c{‹§°¨�·7íÛ¶ÆTi(̵à�÷±be,[·nÇ`ðeüرøø8O�Jâ�·Þ¶ÿž˜˜@Ìðî¸ýwtëÚ…—-!00�i¿ŸJjj*ï,{�9³ž`Ĉ>Yû9«VB‹fáÌ›óŒË²Ü6q±qk¸”q ??ãÆŽÆßtå[rGi:cµÙhÙ¢9/½²„’’bî˜2 ??_ÒÒÎÛgG8rä¯\ÅÀ¹gÚ]ÓkÑ¢¹Ëº;ã­ñF¥R1l¨ë‰;* á…—^¦¤¤„;¯�>z½Ó²<òÐ,ÿx[¿Ù†Ñhdò¤Û/ï]«ì®;ngá+KˆîÞ�íÛ;MsêÔ)ÄÆ­áËuë¹eÔHô:j¯²!zu9·µ¥P(x`ƽ¬XÇÖmÛ àáïwٓ㊳rzšŸ'×_…-[·²eëÖ*åëÞµ«GmàÖqcˆ]ý)›6MLÌ0‚()-å|ú¶mÛÎü¹³˜0~¯,~�C¿üJt÷nÓS(Ü}×�ÄÆ­á«¯60bd ¾¾¾XmV—×X}_ï®òsô^àì¸8+‹«ü*·…wË®åž�Ï–-qZ!ndÕgnª˜!©òbµZí?kZ–-[ÆS³fSZ>þ:ÀèWSVBˆ« --�ððkc8 ®³*n ³…‡ºßíýªO»}-°ÙlÄÇ'ðá–óꢗ¯‰!xB\«*Ý3&ŠB!ÄÕfµZ™5g>ÿßÞ�Ç7]ä�åêA›¤gz(”[A(äZ`AAD×uQD�[Y�Û�/ëz­(÷)®Ë±Ôrï*ʵr(´MzÑ;mÒ¦Éï�”@¥MK�õý|<Iæ33ï™O ™ÎgæÅ3OOhèpêìÐá#lܼ…‚‚7¶v=ñ{¦–¥bB!„ G©T²ôƒwë”÷�·Ö|P=é|w':ß]û�G„à\Y)2!„B!„¨WWï‚&2!„B!„¨7µ»%µB!„Bˆ[êšÁ˜L� !„B!D}‘™1!„B!„hWc²ó¨B!„BÔç`LáúC!„â¶úôË1™Ò¯{ç·‰Lœ4™‰“&säèÑ[R×´³ÉÌÌt{̼‹øî»ïoI}¿Ë¿\ÉÎo:ŒZIIMcþ·˜4e“§Ngß� R•¦NŸENnÞ-/÷íw?Àf³Ýòrïµé³W_ŸÏ¡ÃGê)¢ú§nè„Bˆ_³r»�ŸNž&=# µFMëØ"ÂB+c±XÙûßhÚ8’ؘ&·%Žœœ¾X±šsçÏãÓ¨÷ß×�? xGÞt×”~ý@  _Ÿxúõ‰ç½%Kë5ž¹sfÕk}âÆìÞ³—è¨Hf¼ô"J¥»ÝÞ q|ÿÃìÞ½�™Ó§ºÞ{iÆl^|áyy{ã¡ÑàïwKë¼|9³¹µúÎýÊ~îüyÞ} ¾¾¾´nÕŠ‡G�ħ‘·Û|—/çÔªÏ^õ•[k}²ZK™2m¿Ü—cÖŒiDGE¹^_sfe!„âF%�KÆj-¥w�n˜Í%=~’C0j• ‡ÃÁÑã§ÐúúÜÖ8>üøSGGóôSã)*2³ü‹4kC›Ö­˜6c63¦MÁ`00cöËL›2ƒÁÀ7[·Qj-Eã©!1q7ååv^›û2þnÓLé¬\µ†K))øùù1~ìbbšÎßvÿã¶nÝ�µ´”�úѯOf½<—‚‚æ-pÞ¬¸mÛ¶<÷ì3nÛ¶ïÀøñÐa¦¼ð¼ë½M_m¡¤Ä˜GGר7?:Ìî½ûqà`ЀþôîÕ€5ë6°wß>Æ�yŒnݺºò¸kÃé3gY³nÙ—/cbÜãchíürU]Ÿ�8uªÎm˜:}û÷cç·»пñq½jŒ%#3“ÕkÖs!ù":�/ƒô§û5m¼âüù |üÙ?xiÊd‚Ý–™tî<+V®&??Ÿdï¾¼ðü_1Ü~&ÒŒF¾X± £)??=£NûvíܶûRJ ‹ÞzÇõú»ï�³—ÿügÚµkƒÉ”ÎÊÕkIMKÃßÏ�ÇrWE™î>»uuéR*QQ‘®×ÙÙÙ”–•Êñ' åãOþÁO'N`03iâ³èõzö8À©S§yæé ìÝ€“'Oñì3O³wÿ~ΟOF©TrèðüýôLzn"��|ÿÃ,ÿb…³Í“&0mÊbbš¸-³¾]¼”B«V­xþ/&=#ƒ/W¬bã¦ÍŒ{ü1;¾e×Þ}XJJh×®O<>µZEjZZµ}f³ÙxaêtpØÑét,zsž«¾òòrÖoÜÌ¡#G(µ–Ñ¥óÝŒóHE?TߟõíRJ ��Ì{í¯×¥]Û/²�‡BqRMé´iÙOüõÄ÷èNº@X¨�Ö÷¶Å`4š0M<4|¾>>„†˜9}*mZ·ª1¯ÁÌ¡ÃG0M¼6÷eÞY¼??}�i}òÍš5åíE ñÇ¡|øñ'®K©” ÇŽýÄœÙ3yjÂ|ýÏ­®´EoÎÃ××—¹sf³lé’bÝ»ÞKFziF#6›�ï¾?H¯žÔª.$_䯯ÌfÂãØ´y ùù<òðH–-]BÛ¶m¯Ëã® 7E\¯¼÷ÖBZ·nŦ¯¶ÔØŸ7Ó¥BÁ™³I¼úÊl&ŒË毶PPPXc,~ô)Íš7eñ‚7xjülÜô™YÙ•ÊÎÉÍã“,ç©'ÇXc™+W¯åþûº±ðÍ7ÈÉÉ%7/ϵÐÅÝgâëo¶ÒõÞ{xÿíEŒ;†�¿Âáp?Ų¥KèØ¡£F=IJ¥KX¶t íÚµÁápðéçˉmâó6ô|öù™ÍnÏÃÍHII%:úê`ìâ¥ÂCCP©T¤¥¥a4éÙë/˜‡R©äà?�–f""<Ü•/-Íèz�žžÅ™³Itíz�3ŸJÍO'NÐõž{èσýúºÚS1³î®Ìú–’’JtT$J¥’ð°0ìׇóç/°ýß;ùáÐa&?ÿ¯Í}…ÔÔ49\sõ}¦V«YöÿÞgÂø' «Ü®=ûö“‘™Åk¯ÌaöÌiü÷»ïÈÍs^êè®?ëÛ¥”Z·ªúßàkûåšÁØ�wƒBq'³ÛíX­¥MìHÜGžœKNq¥geç`±Xiy{¿$eç\F«õÅËËë†óz{yQXTÄØ1�¡×ëñðð@©TºMËÊÎ&++‹!ƒáééAÇí àlÒ9W¹ýì‡Öׇ¶­[£ñи¾,Õ…Z­¦W¯$îÚÀ�‡ŽLdDD­òÇ÷ŽÃ×LJ֭Zb ùâÅZå«® sfN§W�Ðh4têОœœWžêúìfÛзo<>>>´jÙ’°°0Î''»�%33‹ìË—ø`¼¼¼hÒ¤1ï¼µCp�«Ì²²R>üècþ0h͛Ÿޯ®L³ÙLFf&½ãâðòòbø°!àp^2XÓgÂÓÓ“¤¤$Lé4‹‰áõW_¹©KhsssÉÈÈ`ð AxyzÒ±C{ ÁÁ$UÔçîs]W©Æ4G7v½¾”’Bd¤sÆ05ÍHŸøÞ´Œ�ÅÛÛ›¨ÈH,¥VÀ9+yõ<§¥‰¨ø7Áh4Ò¯o<-ccñòòB«õŧQ£«Ç�U~FÜ•YßRRS]3§ …µFMyy9 ßîbÌ#£ 1 ×ëhÕ2–¬Š_¸ë³+ÒÒÒˆ¯|Ùw|\/&?ÿ|||0¡V«]}VSÖ§””Tö8àZ;qÒdŽ=v]¿8/St¸þB!D-•—Ûqà@¥VÑ·×ýqððÿðÓѨ‘7I.rÏÝío{ ¨óã ”áí}ýúŽêÒŠ‹K°ÛËyaÊK•Þ¿vÀ¥×i]ÏÕ*Ž›\ç׳¯¾1Ÿ"³™}ûгçýµÎ«×ë\Ï}}|)..®]¾jÚ°gß~w‘Ÿ—�Ížkêª$IDATVF`ÅŒ¸ïÏ›iƒN{µ �¼¹ÚP],Ef3>ÞÞ¨Õª*ËHHÜ @xøõ3U•i..ÁÓÃÃU¦——žžÎ_Ôô™=rßlÝÆ²¿ý0xиï¾Z·ÿ— iäíS©}Z­–‚B猡»óPYÙÙ”•Ù¿f=è¥K©têäüù6M <È•–““KlÅ×dJ'2âj›ÒÓ‰ w¤Œ&#�>2êjš)�ˆkŽ5¦‰¾æÒÈk�«®Ìúd³•“‘‘AtT´ë½³IçˆiÚ”4£‘’’bÞz÷½Jy1pßgW¤MtîØ¡Ò{{öíg÷ž}äääRVVJˆÁ€‡‡‡³Ìú³>¥¦¥ñòì™×ÍX^JI©Ô/kÆd &„BÜ(�F�J©"*< •J‰Ÿ^‡¿ŸŽ"s1EæbròòØ‘¸·R‹ÅÊ]mZÞÒ8 EE™Íøú\¿6M¡Rb+/œ_žŠ‹K*¥{xhª-»ª4?½�Æ“Þ]\o„x{{Ó¥sÖ®Û@fv�;uªu^s‘Ùõ¼ÈlÆç&Öïåää²~Ã&&M|–˜˜¦œ¿�ÌŠU«+S]ÞLŠ !4äj5r‹^§Ã\R‚Íf«vó‡^= IãÆ,ÿbsfMG£Ñ¸-ÓÛˋҲ2ìv;J¥‹ÕŠÕjjþLøøø0zÔHF�Éé³gùðïÓ¦u›:ox¡Ói1›+µ//¿ Ò¥ˆî>×7Ê”žNpPªŠK�m6’“ytôHl6Y—/ä´Z­¥œ¿�ÌØÇ#?¿€r›Íu 襔ÊËË ¢°°‹ÅŠ!8€ÂÂ"J,BCœç¹°ÈŒÅj%¸"ý weÖ7£Éˆ§§—kM–ÅbáÐáÃ<=áI ò ‰ŠŒdöÌé×ås×g•ÊO32ôšÛ‰S§Ø¾ãßüi¢£#ùîàA~þù @�ýYŸl6™Y„T¬Ó½Ö/ûåšùZ� !„7*<ÌÀ™s°Ùläå�›W€^§%*"ŒÁýz»M¢"iѬé-ˆ„ 4mܘµë7RXXHNnûèöî߀N«åøñX­¥lݶý¦ÿÇ×ëõ„……ðͶmX­¥\HNæõù 1›Í5gÆyÉZòÅ‹ØíöÚ ¼o|GŽ£{×{ohw¹]{öb±X8}æ,™™™4mܤÖyÉl6£R©ð   �ÄÝ{(*2c±Xj•¿®mHܽsq ?Ÿ>MFF͚Ÿ�%00€ƒ�mÛŽÅbálÒ9¦Íœƒ)=ÃUf£F�èÐþ."£"]ëÂÜ•©Õúâ§Ó±wÿ,V+_ý (œ_%kúL,\ü6gÎ&a·Ûñ×ëÑh<¸™q¼¿¿?¡¡!lÛþoJKK9|ô(y¹¹´h޼ãp~ÙÏÉÉÅb±°~ãf¢£¢0 M&v;?�8�Åje݆�´jÕ‚?ŠÌEØòòóÉÊÎfÝúM„…† P(HIM#´â9@jj!ƒërÊŒŒ Ô*V«•ÒÒR×z8weÖÆò/Wòúü…5®Ù«�‹—RˆŒˆÀn·“’šÆÿ}øíÚ¶¥y³‚ Adfe‘’š†ÅbaÛö|¹Ò9°w×gWX,ò »f6òĉŸiMxD?ÿ|†Ý{öãëë\�[SÖg¿¤MèuÚ*ÆÙ/GÈz1!„¢.Ú´ˆå؉SìÜs�­bcnëfÕùóŸ&°üË•¼ü××ñôòäžÎ]¸¿{wþ0h«×n`ÛöדàÀ×LY]Mÿ+V®&!a7:�/ƒħŠY¹ªÄÇDZ~ÓfV­]Odx3§¿Äùóxçý\Ç$%%×+އG:/kòðô@¥RѳGí6î°;DEFðÆ›‹°ÙÊ5r8Z­/&SºkGG€'NðÅÊUtïÖݵ3[U"##¸«][æ/XD`` c�Ñhdé²™>õÅã©KÀ9ûùÚó±Ùl<>ø4jä6–gžžÀò/V�ðm"z½žÇzý,Á#£bÞ›‹èÐþ.Z¶há¶ÌÑ£G²zÍ:¶|ý öëC#ooÔç%bî>Æaõšud_ÎF«Õ1h`üýê¾ ¼B¡`Âøq¬X¹†„ÄÝð§§ž¬ÓºÉÚhÓºMGóúü¨Ô*Zµˆå©'ŸàÜù ÜÓ¥3;w&²ü‹•´ˆmθÇÇÎ_”DFF0÷Õ7ˆŽŽ¢U‹d_¾ @š1�ð°0W©ÆT"¯¾ A§ÓñÒÌ9h}}5r�;ur[fmœMJbЀþ·dV;%%•3gÏðü䩸ûûדøÞq®¶÷ëÏ’¥ÿ‡Ýa§M«V<2Úy ¡»>û¿¿ý�'Oºê¸réë»o-â®¶møôóåÌ}õ âãzrÿ}ÝØ´y ÷vé\cÖäVöË¥”K_?+×÷‹Â#ª�‡p°ño‹oºr!~­~ù›‡ÃqÝÃn·»þ®ê±lÙ2^œ:�òŠuzmUU !ê�Éd",¬öÿ‹;[YY«Ö¬£ÔZÊŸž~²¡Ã©“º¶á—·&¸8Μ9˧Ÿ/gñ‚ùwäýìÄõrrryë�÷˜ÿÆ«wô½Ëê[CöË5µÉ‘B!îýÇrL¦ô;¦¾[yþv~›ÈÄI“™8i2GŽ­uZ]M›1›Ì̪ï-UUµ½¦8ëûü‰ú'·ÞB!nB¹ÝÎO'O“ž‘…Z£¦ul a¡8xˆ¼ü×±*¥Š}î&©ûÿóV­^ËsŸ¥mëÖ7UVVv6*•Š€€€[,ÿr%:�ŽáÆT{ÌÜ9³nY}¿¦ôúý"_S}·òüõëO¿>ñ¼·dé ¥5”ªÚ^Sœõ}þDý“Á˜Bq’Î%cµ–Ò»G7ÌæŽ?Iˆ!µJEY™�ºuA¯møÀ[­¥ìØù-wwê„1ÍXi0ö”iÌœ6•Õk×sñÒ%:uìÈ„ñcøv×nvïÙKAAM›4æ©'Ç¡ÕjIIM%2"‚å_®äð‘£øû3éù‰øûÝ–ø×¬ÛÀÞ}û7æ1ºuëêJ›:}Ãÿ8„­[w`--eà€~ôëÓpÎf̘6ƒÁÀŒÙ/3mÊd¼¼¼Y°ø-&=÷"ÂÃ),2óæÂE<÷—g‰ŠŒpËé3gY³nÙ—/cbÜãchÀ7[·Qj-Eã©!1q7ååv^›û2þ˜Ò3X¹j —RRðóócüØ1ÄÄ4­±íîê«®}ƒ�Y/Ï¥  €y жm[ž{öL¦tV®^KjZþ~~Œ>”»Úµc÷Þ}œ9s–3g“¸«];J­VN�9Í ÏM¤Iãh·±¸«ÏÝùËÈÌdõšõ\H¾ˆNçË ýé^‘ºØwà?üxè0S^xÞõÞ¦¯¶PRbaÌ££kÌÿã¡ÃìÞ» èOï^=k<îÚþ¿rÓg!„â&¤šÒiÓ²9žøë‰ïѵJ@YYžž ¡Óö;èØ¾=ÍšÇ�f4¹Þw8ØÊËÙ°y C‡ æýwóøcÎ/¥;v&ðã�‡y~â³,zóu8Øwà))©¤gfЭ۽,^0…Rɉ“'o[ü�<<’eK—жmÛëÒ” ÇŽýÄœÙ3yjÂ|ýÏ­Øl6·åétZF�ÁŠ•«q8¬[¿�¸^=kˆlÜüq½zðÞ[ iݺ›¾ÚâJ3‚9tø&£‰×æ¾Ì;‹àç§à£O>£Y³¦¼½h#þ8”?þ¤Æ8kªÏ�EoÎÃ××—¹sf³lé×y‡ÃÁ§Ÿ/'¶y ‹ÌcØÐ?ðÙç_Pd6£T*¹pñ"3¦MåÇC‡hÑ2–{»táè±c5ÆR]}àþü}øÑ§4kޔŠÞà©ñO°qÓWdfeßTÛ«Ó½ë½d¤g�f4`³Ùøîûƒôêù@­ò_H¾È__™Í„'Ʊióò¯™ù®Ž»¶»ã®?Åo‡Ò¡p8÷ïB!Ä ±ÛíX­¥MìHÜGžœKNq¥—•Ùøvïþµs‰ûþKrJZƒÄ™““Ë�ÿ~Oÿû‚Ñtu0–›—;Æ ¦ElsÔj5ØlåìØ™Àã�=BˆÁ€··7S^xžA�ššÆƒ}ûÐ26///´Z_¼¼nàÙÿÁ~h}}hÛº5 ¹yy5æéÜ©AAA|üégäääÒ¿_ßZÕ5gætzõx�FC§í+­›óöò¢°¨ˆ±cC¯×ãáá�R©$+;›¬¬,† „§§;´'0 €³Içnª¾ºÈÍÍ%##ƒÁƒáåéIÇí1“TKLã&‚ƒhä݈6­[‚Ù\|[bÉÌÌ"ûòe>Ø///š4iÌ;o-Ät[êS«ÕôêÕƒÄ]{øñЂƒ‚‰Œ¨yß;_Z·jIHˆ�ä‹o*!Ô 8™ B!nPy¹Tj}{ÝOaQÿ�?þ~zõ‹œ3¹yùüpô'ô:-þz]½Æ¹yË×ôxà>|}|0‚ÉÈÈÄáp P(0�„††Ñ¤qt¥<—.¥à¡Ñ^e™©F##îzm2¥^õ±õA¯»z)¨Z¥Âa·×*_¿¾ñ,zëÆ=þ8 Eív3Û³o? ‰»ÈÏËÇf+#00Е¦@IPPÞÞÞ•ò—`·—ó”—*½_›A£»úꢠ �FÞ>¨Õ*×{Z­–‚ÂB<<=œmQ‚‡FƒR©ÄnwÜ–XŠÌf|¼½+År­[]@\ϼúÆ|ŠÌföí?@Ïž÷×:¯þšŸ]__Š‹‹o:ñû¦ÙHQ!„¨ �F�J©"*< •J‰Ÿ^‡¿ŸŽ"s1þ—¦( üýð×ë(*2×ë`,9ù"‡`Ûö®÷³²²0 ¤�4ŽŽ¾._~A:]ÕqšÍfŠÍÅ‚ƒ(,,¢Äb!4$ä6´àæ(TJlååØlå—¸Òœ—'nâÁ~}ù×¶mtìp^^^nËËÉÉeý†MLšø,11M9!™«VW:ÆÃCs]>?½�Æ“Þ]\ëA_mês×¾êètZÌÅfl6jµsû€¼üüüôäåå×9–ºÐët˜KJ*År;ëðöö¦Kç.¬]·�Ìì,:wêTë¼æ"³ëy‘ÙŒ�¯P·ó \Y3&·B!ê$<ÌÀ™s°Ùläå�›W€^§ÅZZÊÎÝûÉÎÉÅnw��“Kn^~õ<+¶~ãf† ²¥K\�  `ך™´4‘‘a×å $33‹Ô´4 ‹ÌlÛ¾ƒEo¿‹Ãá %5•�ƒkP‘ššFˆÁ€R©¬×¶Õ†N«åøñX­¥lݶ½Ò×�„Ä]øúúðÇ¡C¸»SGÖ¬ÛPcyf³•J…@…$îÞCQ‘‹Åâ6Ÿ^¯',,„o¶mÃj-åBr2¯Ï_ˆÙlv›¯¦úܵÀÓӓ䋱Ûíää:gáüýý aÛöSZZÊá£GÉËÍ¥Eóæ7Ýöªês'00€ƒ�mÛŽÅbálÒ9¦Íœƒ)=£Î}]}ãã8rôÝ»Þ{Ý Ð�]{öb±X8}æ,™™™4mܨù<Ô•»þÌÏÏgâ¤É?~ûÖjŠÛOíPTÌŒÉ`L!„¸amZÄrìÄ)vî9€‡ÆƒV±1贾δ–-øéä,V Þ^^´kÝmÅoÒëÃ�‡SRRBßøÞ•Þ�Œ'Õh¤SÇŽMFÛuy££¢ˆ�ëÅ’¥Ë°Ùl´nÕ’¿<ó4 …‚”Ô4ÂîàR�©D„_? «‹� ìLHp½6tíÛµsí(pâÄ ¾X¹Šîݺ3vÌ#nËûà¬^»�mÛÿM\\O‚°•—“ž‘Ibânf͘À�Áƒxsá[ûßOthWµåEFFpW»¶Ì_°ˆÀÀ@Æ<:£ÑÈÒe2}ê‹nc™0þ V¬\MBÂnt:_ˆ��ûÏCMõU×¾+âããX¿i3«Ö®'2<Œ™Ó_B¡P0aü8V¬\CBân‚‚øÓSOÖ8+X›¶WUŸÉ”îöü=óô–±‚„oÑëõŒ>Œ°ÐGµõ=ôÇa¼óþ®2“’’ˆëG—»;V›öðH祵ž¨T*zö¨ÝÆv‡ƒ¨ÈÞxs6[£FG[ñ³^Ýyp×öû»w­1ΪúSü¶(4Qí ‡sjlãß7t©Q(ä*E!„B!„¨gʆ@!„B!~�”²VL!„B!ꟲ›¢BQGåv;?�®—+o~~>³_ù+ÿügÚµkSc]Bˆß/Y3&„BÜ„TS:mZ6ÇÓÃ=ñ=º£V©®;îØñS´ŽmVeÚí¶qóWÄõêÁ{o-¤uëVlúj‹+mÑ›óðõõeîœÙ,[º¤Ò é£O>£Y³¦¼½h#þ8”?þ›ÍÀ×ßl¥ë½÷ðþÛ‹?v 7~…£âv9îÊtgÅê5ô|à~æ½öW4j5V«¥ÆXºtîLjj*™YÙØlå9|„ûºÝ[cªãp8øôóåÄ6�aÑ‚y ú>ûü ŠÌf” gÎ&ñê+³™0~,›¿ÚBAAa­Ú(„×R*P4t B!į’ÝnÇj-ÅhÊ`Gâ>öà\rÊuÇedfã Á�õ$0gætzõx�FC§íÉÉÉ©1OVv6YYY <OO:vhO`@g“ÎàééIRR¦ô šÅÄðú«¯ PÔý;E~~>99¹ôŽ‹ÃËÓ“�ú×*­¯­Z¶äûƒøßñŸðnäMllóÛP�ÜÜ\222• …BˆQ^nÇ�•ZEß^÷SXTÄÁÃÿ#ÀO‡ÿ5—#ž½p‘Íj¾4îvÙ³o? ‰»ÈÏËÇf+#0°æAaqq v{9/Ly©Òû¹yyŒ9‚o¶ncÙßþ ®X´Z-…Wg¿tZ�ëy#ïFßh“…5 *Æa2C&„BÜ�F�J©"*< •J‰Ÿ^‡¿ŸŽ"s±k0f..¡¸¸„à@ÿ‰1''—õ61iâ³ÄÄ4åü…dV¬Z]c>?½�Æ“Þ]\北��£G�dô¨‘œ>{–ÿþ1mZ·!À߯Nqz7òÆZj¥¼¼•JEII 6[Y­b騡=+W¯åÔ©ÓüüóiF�Q«|ÕÑé´˜‹ÍØl6Ôjçï­óò *­÷+*,„Ðçs³ŸF�êÔn!Äï›Ò!3bB!D�…‡8sî6›�¼üró Ðë´®ô¬Ë9øëoê¾›a6›Q©TøPPPHâî=™±X®®Çòôô$ùâEìv;9¹ÎY#½^OXXßlÛ†ÕZÊ…äd^Ÿ¿sź©…‹ßæÌÙ$ìv;þz=�×6±ª2ÝñÓëÑjuìÙ·‹Å¿¶ïàÊ/ŠkŠE­VÓ±CÖoÜD“&� ªU¾êøûû¶íÿ¦´´”ÃG�’—›K‹æÍ]Ç$îÞ�¹¸„ŸOŸ&##ƒfÍb\iùùùLœ4™ãÇOÖØn!Äï›Úá¸òO� Ê„BˆÕ¦E,ÇNœbçžxhÜÕ®-ó,"00�1�ŽÆh4²tÙ‡LŸú"ññq¬ß´™UkׯÌéÎËú&Œ‚+W“�°�ΗÁâããã*{õšud_ÎF«Õ1h`üý®ÎŠUWfu �=ò0«×¬ãŸÿüñ}âðõõÅî°× @·n÷ðýÁïy¼Ï£•Ê­.ßùóxçý\Ç%%%×+އGgÂøq¬X¹†„ÄÝð§§žÄËëêy4 ¼öÆ|l6��ï5±!Dm)Ôm œ7~ÞôáÛ � æÊ.`×¾þåÃn·»þ®ê±lÙ2^œ:�r»óËC€^[UUBˆz`2™ kè0Äoд³™1m ƒ¡¡CBüÊ)8÷S”]…B!„¢þüõî³§Œó˜IEND®B`‚Projecteur-0.10/doc/screenshot-traymenu.png000066400000000000000000000314411451344070600210550ustar00rootroot00000000000000‰PNG  IHDRþÖñi? sBIT|dˆ IDATxœí�wœTÕÙÇçNÙ�íË–K•&Õ.Ä’X°ÄK4*MOŒ¯ 1šf^5*F£!6Ô×±$JµKg)Û Ûwvîyÿ¸§Ý»ƒ ,»8÷ù~>ÊÌ�sOyÎÓÎ9wfÙw®»•3  `Î5çC›1XªÀ9w>ƒQN¾–÷r}͹ÎTCœç®{�ë Œsq‘9íˆ×à6˜è§óDq.î—•rÎ`Á6:$GX¢grÌx­K\¼bNóâ3挓«O¸ë:ƒÓgù‘Y�,gÃ-[/²ë € ÀâŽü:vŒ!ÀmØ`àF¿”øD'ä¼8×,€ÛJÆŽ|Ũ9gÎ;K¼î0L®…¯>}a܆Å9bÌräkÌ¿”·–³)Pç^0æ Mô�Ù¶3.&îBÐúæÔÉ™Ô< €«1é¹3ÇĠŤuÁ„µyqé´1rÞ, °ëPB&j®Œ9,pe[œ9¥�1BéƒeÈ“YB8ܱB5ŸÞN›“ǔŢƒb*C†h«÷Îÿ˜pæ Á¥q­Œ9儤Й¬ 6×Ê͘3jp\¨©ÒI.”žÃ‚ ›1ØÜÝ×xi:-xC:Üq¦Ôdºþ• çüO=“ÎȸOŠÚÆhqÆ�׌ëÖ™9'®Irr8bŒ©rœAÉE™—rRÆ5¡ à¶–¡î¥Ó¾”i¥ÂQëvd#´c–¾EO’Ò3Y•olÊ£‹²F�/�¸r•�¨‹\ŽØÕ¨œÓw¹c¢(dKe”ÞÖ,%•� e«±3‘/zSi-M¸Œž3K?7ŒÔ4N³[®qˆ®I]fÌ宕ƒÓF.uÒÒŸÇÁ5RÆT s9A0'c•Æ-mÇVוٻt—‰2æêUéŸÅ›+»u/§g íÆrõ˜3¡]ù›ŒªÆ0]©½òõúïœ9³¥Ð<3¸_Üû‹k`·6µk¯/ÿ³d´2¼œ%ÊÛœÁf–Ò{í?�ˆ/#Ñm¹až2Ò8çhª«AUÉ&D¢ ÈFª·mBsM%¸ÓíÙ14ÕT¢ºd2YR¢õ¨Ý¾ M»kÛV•2—8¹^VÁˆøRA•ô”ceÊà™¼bª�K•´*šÊ![gM¦Kç"¥‘J©úÆ Ys'Rº©:Ê ýáª^ç¥ÑÁ†ÉQˆ^ˆ@£÷¸k¢â-ÙtKʽ‰q2)rpæ^¸q¡QŒn)0W)÷5·ƒÔ�Ä5|Çà9DNaÈݸÇ݆w¹©Ç æÑpò20Ê5ù8{Ê(ÀKKVcmI™§LG¼ktgÞ DèåŽ ®r<®~êZÕØLobÌ%äæ·ˆ´r“Ö´es§M.%‚Œqá@¹žµiäöJNªb J‹Î=hç•)xù¢5źM[°xé2,\ò)6íªÄ¹ÇŽA{ Œ g*,©¸J°\¹.ÚQK aA–�¢LMmcçƒ1iÖ=3T)÷µ†êJÄšê0vè 7a&Ž�~……`Œa`ÿ¾< -ù/–~¾`ÀäÇã„ÉãqÔ˜Qèݳ'8çЯ† 쇖.ÊâÍhKÍ@jv¾Ó’aìf�åp ŽòK»`ž’¦�‹”Ÿ cÒ'zÓQ9A™v÷ss‰À´üÜÑL*¹ÔVá(d®ìŠ›å¨Õè“�1áÜ!6†9w%ˆN–à, ”Ü¡\ â0Nu`Ö¥5�Á3öaΞ2 ó~†P0€ Ž‹YϾ¥ïá¶Jûµ¿v&¦Á) çÍ´µï� Ùzb�Ê…j‹áõ;ò½ •I碚àLeÝ{)AŒ uJ%½6 7,Lr];8$ÑqijÒ÷·%eáª;þ‚3Æ�oiÀ¨~¹¨knÃ�gOÃ/¿ +RíË4Í`©÷Π, ¤�äLó˜)±òa°õ&‡ÑW÷{S® @]ÙvŒ1çŸ~*&Ž�ô´4õù€¾}�Ÿ›ƒ>½z¢­ÅYª\rÎuøaH‰DT¹¢AQ�Ÿ‡‚¼<4?7ËÖl@Z�¼ŽžGuÂØà2<¯R:îÈñ:�Q+‘œt•H…pÕcBÅ^†9x@GqÀ%?@¦ÞL8_ÅUYÓ°Œu¹pv\tÆ\Ò1h£6—Bª.ú˜aËL銜Iݶq”§Òç‹ë=†k¾{,ä÷€ ³9Ò"IøÃg¨ë[Ë«ñÈ>rM—!.·^iÑê9””Žkw8)�ŽÃí2ÔkÆŒ¬É{Œç)«< lÊH ¼ƒáR)Lër”ªg»î1Ò�´è cè‘d¡¨¨ {¡¬¾åUÕH ‡ñãéSÁ£mž¾pÙ¥'å€Þ¤ Z"ààb]oÖÀÔÚÇÑM3ãµ[ F0œ„ªšZT×Ô"mW÷DÛÛQW·å•U%%#””ŒòÊ*ÔÖíF´Ý(mGuM-ªjj '! «ö”3ã?Î�µ øWEoéèÄk®3,3;r^pU‰Zû‰{T¢ì´1ÃpêQ‡iƒãfÇ\/µ2™=xÚuE%½ž÷nªÊå±Üä*šš¥µ& #—ŽÃå¹1^§œ¹ä“­Š£ê$Âî`ôpíw�Á5§Nép½¿(O‡¼WÍ%‰k³VØ‚£Ó³j5*á 83²=waçdFÜaºUÈLD÷r2R<þ¸™j§Õ˜7UXêÓ ;Å¥�öÝñÛrÛÄmLÎ0Ç êƒô´TØímX]Z��‚Ç_{×_r ³Ó°ô« `VPΟ³Î•ÞÏ4ȧOj]º§v±� ̇Ì~¹üšk½„#©Øº}Š7l@cc‚ )‘ÚÚ¢øj]1Þ\¸Ͻú&¾Ü¼%e•(Þ°m­­HMIAcSVµo¼·ó¼‡M¥•È*è‹@0¬e^!Ê>(' Ãɉh(—‰svQ‘éÀ¼’—¨çÕqÓÆ ôÑÃ0 g„ƒ6ì¬TNžyv)¨ØP•Øl’éð¢öeäÆTwo:p­×õD0=AG®Œ»ÆÉ�>ÈãJ½.âj¬RCÍ�³i£‡nžûªzíåæ¹¯¨ÏÞ]Yì�ƒº¦˜»Œ«|‡ùÓ›²2@º‡/lŠél×ü\j ĘÐ#=ÇY„³&�¤á´� ÀÍZ ƒãʳè´Dùò‰ˆt ¢ cíâ”±»ºÿ|ñU|¹v-.œ~RR"ø÷‹¯àÃÏV!­g!ò PUU†Ùÿz_®]‡‹Î9MMÍxæ¥W±|M1Ò{öA�>ƒ`y›’ý±9œõœù¹2þŽnÖùǹ.OôÜÚ•)HÃ17é¦�êRöãŽ(ÂÏÖ˜RQí1•2k'"S|K%e,#rq}— ,ÞYÆZ§uÜå�-ñ^fê\Ü%M@îYF_åçN†Øá±!Ü<÷UY#Üšííi<½‡h�ëÌÊc¤Î{£.å ÌôøÌó{WfgxnÔ­æÊþqìáE8zä`¬-)Åü�V`{E�³Ÿ&wàyx‚»ÇW�–bãJ¤^c7'ÁˆÜƨÆbþÞAá ¾homÁÇk6ã'ç�¼þ!þðÀlÜñ‹Àmއ_yV0$ÒGéôúß3rç×øÌqz†ŒÞ‰3§YÖ׋!#=#-­mØYZ†Hr2›š‘ž‘�`8Œ˜ÝÆ9Âá0ÒÓ3ÐØÔ„;KÑÜÒ‚–¶6¤ed" öc`VH8=�Î^n’6¤NáîQëNˈ&N=¤tîžO#òM=SãD¸ã�X°ì+Q½q´$Óãºy& wÄ`œvÔaê5ygå:¼»r�’¶rpR`S†¨Œ÷Z˜{ÆÕÃ@N�™*ï®Që±3H|^Ý‘¨³xÑäñG Æ)ã¾^ï¯^�…«d&ÁôÓw†Kù»7NL[” pöÑ£‘“žŠ'ß^‚�ÕuÊ -¶ÚU4w#åsñr‡\7£ŽL ½¶ö‘f‡¼Fd�¡­¾CÀAD‚XZ›[�ˆbÑšmX°ðCœwú)ààxø•…„‚rTN?9ƒ|Y U¼7‚‘£2:xæQ»¤ø(?�¶µ¡¡b'XKÎ>uΟ~:è8þèIxñõ˜7ÿÿÐT“ 0 kÁ%眅s¾w* {÷œxì<÷òkxyÁ;hINCz~oCIÆ®º™~ë>9OÕé§ÓÜCÐN9Æ¡–16¸^ ˜çûÌŒ…ÀÔ1ñ�^"�ÿ�Ͼ‚LéÍ>ÊÇ{ã±øó °Gãgå:¼»¢X‡|s³ÒÜØ4¼£v؞ǠåX}Òß Ð·Hùê Á�dÝ:bšÒò:·9Â(íÍ`ÑÁÁpê¸qå±pu1®.†- wò�ÚÄäÃdœéVd µäF«¸í¼cÇbp¯<¬ØX‚ýzaD¿^ªMË6nfBðr¼rM)7z¸8šPú¤Då`Cn¯Iw`ŠÍÙ°˜Ÿ…@ÀBíî0+€Ô¤88^~÷C\rÆ)ÃøÓÜçQ²« ß?ýTÌ8ýxØb£Œ‹�"zË]e&Û†z¨Äq:Ýr ¡ãžD¼é5Ëì\»Göï…ûgÝŽ¯¾û÷SŸõ.è‰ýàB<ö·{1åð"LY„Çî»WýàBô*è©Ê ìß7^}îŸu;Žìß ;×®V ¯Q(±ŒôÜ0zÕ/ÃùI¸¹Æ7FÀmãÌ]9i@|�kY“ã�"lO̲¬¡X+–¯ÛËŠ÷V¹ .×w²?† w9ãOšL—̉ã’RÓÑÐЈe+V ººy9=�•™Î9¶ï؉gÿï5Ì�÷ÒÓÒðæ{‹QW_�‚¼\¤§¥�sŽ�[¶bÞüÿÃSó_AESiù½ %«†Ôù¸4t)où¯1ÿL]7ÖÂLoØ©¡†éw ÜlÇø\~�“‰îúLÝ$LEíÚ»lιw{,†-åÕxwe1œ/ù˜ý±tØ�£PX:3RNSuHGyéˆdf£ë•m97Yri ¤¥wëõžƒ–•«/pèø°ŽøŸÇùl-«F{ÌÆÖò¼·z�˜O)8§�Êù‹Aj»r‡,f™s«çÈ)ÏaÛk¶íB$)ŒïŽ?œsõÎCE]ƒxXΈ¢N{Üy^îoœ©ïj Ó�ê:Ä[õ™iI´¥£hŒ¶ƒsެŒt´ï¨okÁŠU¥¸éº«ðâëo¢1fáÍ¥«0aá8å„cqÑôïàøÇ"*g¢6:\ÃÁ¸þ¦�ÌVÌ~z£®©Ô¦ñ'§¦! ¡²q7þ³ø¬ß¼“ÆŽB(§ËW¡n÷nL=þL?ðñ§ËðÙŠUX¿q3Æ�…h4Š%ËWa}I)l Éé™&GŒ6…*çä>�Õ) ûù0·ÎsuÄ%ëÑãÉÚÄ¿÷üð¬8³Õ‘›æ¼*¾a¨×ÍúÑa1¿\‘™}”cX$ÖüPeL‡Ìõü©±ªE¼×)¯øL$ ê‹]ôºž{oV­s)ÝË(�ñM¹ÅÉ0âÀ”CÑ•CGkXüùFYÎé”sšcžé›Gì®å”<²UÙžÖ™/¨½:8⟯ÇêM;0aXŒìߟ8Añ…gˆbÜ9¶s&T>×íD-ùõMîjZûfˆJ^‰ðVäçdƒÑ;#‡Á “ŽÂy»JÑÜÚŠ¦æVüwåçøõu?BFZ*èçšî‹¦Ÿà±7>„9m�h#fz™h*¼ñ¯cj&QÐ0 ¡ä¡š°bÃ6lÙ¾ùÙ™èÛ»¦ ÆŽ:ùy€‚žùз–|¶o¾·å5u¨n‰!˜š‰´”TXÁ ˆðâˆT)®¸&&U=`"©V›6Ì9Çgj. …4Wv=Ðnìì{"îÞ`ƽúÇK,!_­rw?m'‘’.WB‡gÇAsÙM¡s®ÌF°ê² ÎÍGs½TK@©Ë†ÑoÅZXGÕxæìÍŠÔü¸ä²§{ŒØlTî~▋즣ãqD çXÕ¤W>Ò§ÉO¸uZŸ†0UÀPרŒ·—­ÁÛË× +-AéMu%z}o¦ÜlEM8 §=~ê£ F ê§j¼íW?ÅcÏÌLJŸ­BK ¡dùˆ«hÏæhÛ]�!…ù¸dúw1õè‰`.ž~:¸mãñ7—À Õè9—ëZGÂÞ ež×òóx“ 8ØÌ`� ’Ó³ÐdÇPVUŠ^¹=pÊIÇcÒøqª\A~N>éxd¤§aοŸGiU-"9HNÏTʦÓr3z‹×\#RNÌ2ܽWÙÔ zö_´‹²aÎ)²½ŽR1ßiÅsZ°Ôþ‚V8î½]á »ÖÏ*"ë € ãW³g~£E9½‡ �¥cGL O6-Ž€½’§Þý‹?\qfœÄ££¶3ãß=æ:h«Ÿ8ã°¤øÌ>©�glœs£ffVóë¼²fÖ¿ÚÄ9PSßËV¥¹š³Ÿ´—væÐô'²œ~çýv³,k�ãH±±×�â‡7ý‹ÖlC,5¡HDx@qc`–…¤¬\”4Ú¸÷éWðÚ; U—œs&N9rÃGÛ”�ÙI&ãT*$•Öû]Ó!xa %GÉÌFkŒcÃæ­(Þ° í1ýµÜöX Å6aýæ­h�q¤df#”q¥^¦ó”‘ÍÑofîi¹20q„ÉÝqBÆsU¥1µQ&î÷,™�ÍA=¯à 0³ZÙom|4‚1µtÆ¢’ós#Â÷ Å9Ô²SõKM’tÞ~§9†”ô§0„̰¥¼ûÊ–òj5æ=ù92Á‘Ž“Aý"”ÅÜg"º»\ ]ù3¹wä÷Wf-¾²¤°gS^?Ë%ï lÈ<'5’OÇH ¥`®)7ÿ¯×ZÐzFlp47;_d ‡B8bP,+©�fI…÷Š‘„ì6Œ(ì’g�¬L€mW™ˆüÂ7w"¥0TWÜ#ÇgÎ÷|nŽ‚Áy’Ïú]•(™ÿ*ÖmÚ‚SN8‡ sœšóï‡øxÅçhæ¤dåªlFJÉ•2ÝÀµ.3ó_f<Ìã HÊ™á—ÑRFO�F•¢ÉãP8¿ç:"•jåq®X# ™ë_*Qó¡3ùpÜeäüº½””�º¤NÌ4Ã\ÆHi¼6�¹ k“Ë+!tÓ}>òŸ�ŒqygÝ4f¹}Çßxpé w×,¥cþ¾¡ÒÙŠòÐrÊ�c[óèÖÈ—\c6—Ù2Ktõ™lêu·_ö>ºàTá5)NÓða¼s K_‰57⇧Ÿ„ Î8IáV¯]�ÅK—aõÚbTÔìFc[;Úc6RÂA¤%‡0lð@Œ9§7ÉIa@k[Ͼ¶s^{�äTcB¹ña®ë•½C’{È^�ýu“(G¨FÉnÇÐÖÔ€æÚ dGÂ8ç´i€—ÞxÕÍmˆdå!œšf\òøeÝò QÒÉ©¸k­®4Û%W×ÚÚ>ÜCLeÌ”“Q¼ke­ä7[�g<´`´V;CqBšŠNÆ#¼Z�…¢ªñy—‹Î}¶øL?˜b„Èä ô5ÊÜE9yÊ#$¤dçè‚Û½ºì�£w®Ù®§•¿Ó›Œsæ$ ú¦38%G9w–á”]ƾ綵2h·Ã¦^w«Ë¥»uÒQ�ni#Pñˬ)D³.C�´ìÆ©ÇLÀ˜‘Ã1zäpd¥ë¯ºÆ£¶¾+¿\‹_®Å›}ŠöHºÓyÎõ½¹û±1pb½;ûæ·áL¯¯Ó9ÆÚ£hÙ]‹ÚÒ퀬‚BD2z€ƒYêJ-) ¦½»¹Ì’ŠlyôP+·óÂv­a�I…62Ç€týæL1WyYÝjoæxŽ]»÷:èŒ,/w¦]†å6zÙ®ðBG™Ê4t}úÔBmºË ÑkæØ}J"@cúÑ–íRQe–uð\wëŒW?t_T'\Ή1.ŽA�Y³8räî9`X²t({߬•õKûàØ´ënU™°Žû¡FÇXetZ·ÄäÇ31SMd}¦wr… ´4Ô!£ÈJOEFz2ÓÓuõõ¨khD]}šBRZ¦k—bbãT ×I…wjd´ëØ›†ê›yÍ”�§—æp6pÌ'³˜§a³ó›g\mnÅQl³]¡ø¶X²™m\¥Ì\ý®�[™µ"KC4�‚ÜÓwvœ{÷’OF|#Èú+3 —³2lJßb¤ÖL~È< ù09V÷ïݙǸrdng'¯›ãÓ£‚!)ÓJÌuó·s…;„~g¾.�‘:ÐÁ¯[îZM™˜zb1éL¡Òˆ ·Ãj}e\³¹ú52€Á8ã×Ñ*¤}ž»N­XJ„ ˆ¤g‚ƒ¡–5 6ÐPg”HÒ“‘ Ó‹ÏŒ|Þ4&9Ì0p¹‡ÏØ¥pL�YÆu³¿æçÞk^¸ç�+ÈGqÅ0äñ©ú~¶*¬Çê^Êþ1¨ˆúd ¬fEþ˜ ¸ U·¼OŸý»7 �•¶øÜí0�qã“RÁ� #å¦cfºFYVýä»2h®åϤ³ÓÒdp!7ú#'Ê<ÖU­pÃU)¿¯¤31ÌÄ�Éê>é�æ´*�¹Í3s·ñigà~ŠÒ@N/ Žï¸œ3y‡ùM>¦otçq«uõÅ™®3¨:æYké !¨M;ÑÁ­o(FÇ1tÎýÆõ~øL½ŽïÃÍ_ êX Gü•ÜNÀŒ^—å©Y)¸±.Ö)�päpÌçñ�•±;è(¢0<ÛiÙbº.¹,PÊmZ‹Aü¸ÎÅ—£¸04}þ.»ÀÌãâ*rOUJƒó:2³=i ÐíH¹yc¬ÄŒÐÃ=‹×JùÎÌ¥„”‘Ò%3Ý®9oæ\«–DçL³ô¤Ô†ŸñJØ­‰®šÅ© ƒ~”yÊÇ»ÏýZ;Kü ½îq $^qÝ×·Ÿ¸ç_Ó€yœòÌSF^3EÚ¡.+ÌIDAT¢Ÿ¿âqʹ£ž6©àÆN=ä²Eç"ÝwÙbú0Ä”‘èç¸Õî¸]e·\%™ê—3RWŠêdN Ë2]„ «Ð›yæ¦ï0~§iƲ—ò?·œe5¤áj—j3]Wá“C2³QÈ_uê˜Åèfœ>ØF/Ì�ËøÏÕ=C\Öd̵,çtY»0Ç®ÇfTbúCÈÇx¿~cO–ï¸ÀÀ¦M›ö-6c‚ ö¯Ã"Â�á„9¤ ¿±© ë7nDcSSww… šCÆðw××cgi)2zôÀÎÒR쮯ïî.DÂÜ{‘ƒOm]*ªª�WP€P(„¤¤$T”•!‹!;+«»»G G·GüªšTÕÔ _=„B!äöê…êÚZ”WVvs "ñè6Ã眣¬¼uõõÈëÕ Aaô’` €ü^½ÐØÔ„�¥¥q¾6JÄþÒ-†Ï9ÇÎRçWwò °âwò,äõ쉶övìØµ‹ŒŸ :‰.7|Û¶±}çN´Û6r{öŒó )n˜0þç(Ù±¶ï�Y‚ ¾ ]jø±X %;v–…œ¼¼½½IN^Á ¶íØáúÅ‚ ¾9]føÑövl-)A(9=rs¿‘ÑγìY99HŽD°µ¤Ñhô õ” Ÿ.1üÖ¶6lݶ )ééÈ<Àã¹ôÌL¤ed`ëöíhmk뤄¿8è†ßÒÒ‚mÛ·#3;é�RgZz:2³³±uûv4·´tJ�á':õž†ÆFTTW£­µUÿ¯e!7/É‘È^îþf¤¤¦Â²,lÛ±œ6üˆ‡1†pRòzô@ZjêÞoØ �fø ��(-/G�œ$u²‘ï‰äH}úõÛ{A‚HZ››QZ^Ž‚üü6þNKõ+ªª�Ý…FO~#)A�œTTïûߨ�fømmm�žÎá&)A[kë×Ói†OOÕD×жÖí_Ò!¢ë!Ã'B†O>„ Ÿ |>Aø2|‚ð!døáCÈð ‡�á„!Ã'B†O>¤S¿�Ÿ��ݙՇ’-[¸ŠøáCÈð ‡�á„!Ã'B†O>„ Ÿ |>Aø2|‚ð!døáC:õɽîâØ£§àÚ«®Tï£ííØµ«ó_~ËV¬ÜïzÓRSqý5WcèÐ"”—W`欻ÑF¯�HÂ𠬼wÜu7€¤¤$Lž87\7¿ùŸ[QQY¹_už1›Å—1B¡Î?÷5v ’““±ióf<ñäS¨ª®Þk¦LšˆégœŽ¼Ü\ÔÖÕá�÷ÞÇë Þìâ™ 5ÒðÃá0&M�ü¼\—ñ\qé%xòéyØUV†ÖÖVüæç?CcS~sëmˆ$'ã—?½õõ xí�xâÉÂb ðØÜ'UÝ7\{ÍïÙŸv  ;;O<ùOTVVáÊË/Å—^‚;~? påe—¢wï^øóßîGUu5†¡º¦f¯ýÉÌÈÀuWÿÿ˜3Ÿ-[Ž””dd¤wÕ4‡0 “ê÷îU€§Ÿx O?ñžxä!\|Áùø×3Ïa[ÉvUæégžÅÚâbÔÕÕ!/7G>sžz ¨¨¬ÄÂÅbô¨#÷ØÆ¾Þ³?í|¶l96mÞ‚ÝõõX²ô¿è[XÈHOÇ1S&ãñ¹ObÇÎ�hiiÁªÏ?ç|¯u§¦¦€1†µëŠÑÔ܌ʪ*lÚ¼¥“$N|›I˜ˆ/7÷ ‹¡¹¹¹C;f«×yy¹`Œáï÷ýE]³”••ï±�}½ç@Û©©­E@,QzöÌG,CÉö߸?»J˰lÅJüþöÛ°ðƒ°à­·QS[»Çv ÿ�0†/7÷ö•¦¦fg-?ãú}þ#„]u�‰Å,pÄÿC‰{«›sŽûxcG�ÂÉÓ¦âO˜…þkøÑ7î‘X$LªÿM©¨¬ÃÀý¹{LJËË PPÐs¿ë^¾rîùó_ñÌó/à‚óÎݯ~‰…o ¿±± |ô‘³qÖ«‚Á €áC‡vû=&uuuøtÙr\~ÉÅÈÎÊB8Æá#CzZÚ^ëXÆ�‹”Hiii(虯²"Æ~ôÃËqøÈÃöé=‘X$Lª¿?<ùô<\øýópÓ/~Ž´´TT×Ôà�7ßÂÚâân¿ÇäÑ'æà]€ßßq¡vî*Åœ§žB}CÃ×Ö�‘‘�ïœt®¼ü2$…ÃØV²�>> 1bØpl+ÙŽ/¾üj¯ï; nÛ¸êŠ+ðèO €‰“¢kaÓ¦Mûæ Ï8¬]¿GŽÓU L,ÂW^Á?ÏΟ�p8LÆÿ Y½b†P¾Mõ‰îAnBžt 8ã´ÓmkÛ¯MOâÀ Ã'º…éÓ§cúYgá;'�„h4JÆßÅøz�OtÛ¶mÃyçž‹_z 6çxwáB„B!Jû»ŠøD·�F±uëV\~Ùe¸â²Ë0y´Säï2(âÝB[[¢Ñ(Ö¯_�«®¼sæÎ…mÛøï²eƒù2ñ‰n!‹!�"�¢¸¸7Þp~ö“ŸàÈ‘#koïîî%ÑmH£—‘åÊ•¸ý¶Ûðû;îÀ��Éø"døD·"�^:€åË—ãÞ{ïÅ]wÞ‰¾……dü 2|¢[1�^þ»téRüõ¯Åï¾½ Ô¯ �G§îê׈_…!ˆ=á5bÛ¶�FaÛ6Þxã 8ÐõùŸï¹W_wv••ÑN'BÇyD·Ò¿|ï{ßüyó0{öl|çä“qÍŒЇy,ËB8) É‘~'B©>Ñm 4‹-Â]wÝ…ÔÔTüûßÿÆá#Gâ°‘#‘ž‘�Ììl¤gf"9�e‘ªv&$M¢[:t(-Z„<þ8Þzûm̘1 ˜3g®ºâ pÎaY�ýA€$Jt ï¿ÿ>þvÿý˜ÿòËxjÞ<̘1ÉÉɘ;w.Ž?î8dge�ÛöÞ+"ö‹àÖ­Ûº»„ψÅbøãŸÿŒ·Þyéé騨ªÂ‡Œ«¯¾ÿûßññÇ㈑#ñá'ŸÀ º»»‡$j·¬¨hh§|+¢¹­} 茪ˆƶm;e ¼õ"‘‚¡ g~>f?ø ¾øâ Œ1ç]p¢±˜úœÐ”lÙ‚H8é€ê Ã'ºÎ9lÛ\Ñܶmäçæ"=- _­YЦÞè çã<¢KaŒ!'}·, å••(-+C() –e‘ÑDÈð‰C2ö®#á¤|ÍUWâõ—æcô‘Gtøì¥çžÁ#GvC¯âÐ"á òÄ ¨­­ÅäI»¼íK/¾7ÿú—]Þ.A|SÊðˆ¬¬,<óü ˜Zò rrz hðàeN9yúßûð¼§1ëΙÈÏËSŸ…B!\uùexü‘‡ðìSOâ÷·ß†ÂÞ½YYYxý¥ù®òSO<³¼0óÖ[pÞÙÓ1iÂx¼òÂs˜yë-y´±ÿ$”áOš0Ÿ~¶Õ55Ø´ysÜt¿µµ÷?ô~ü³_`÷îݸó6m ×_s5ÆŒ…;gÝ�]v••á�wß…ää½�̼k^xée|òßOqæyçcæ]³:ulÑ™$Œáôì‰�úã³åËŸ.[Ž)q ᢰvÝ:”•—ã¡Ù�¢°wo :˜vÒ‰xðáÙØV²õõõ˜ýØãˆµÇpÜ1Çtõpâ ’0†?yâì*-E}}=RSR°fíZô),DŸÂÂ=ÞSßЀÊÊ*öFaï^°mëÖ¯WŸÇb1¬-.þÚ:âÛHÂìDMž8½ ðÜ¿žò\Ÿ€ç_|i�÷Ésc†ø�œsú²‘p$„ágefâ°ÃñË›nvýÚ¯Ÿ�)“&îÑð³³²�“Ó[·• ²²�@ƒ †�8NaXQ–~ú©úí·œœ(¯¨Øc_,ú±â[@B¤úÆ…êêWš}¼EC† '§‡º6vÌhD"defâÇ×^ƒµÅÅØ°q#jëêðþ¢Å¸aƵèÝ«))\uùe°¹�>ú ¨¬ªÂ‰Ç‡ŒŒ Œud‡=„šÚZ èßiiiÈÍÉé’±Äþ�†?yâ|ôÉ’~iÕç_ ¡±“'å9=zà‘þ�þýppüîî{Tù~k‹‹q÷ïîÄã?„‚‚ž¸é–Û�F÷=ð ÆŽ�9³Á.¼ë7ltµ·ä“¥ˆÙ1<1ûa\í5wÐqзóâ[Fg|;/!">Aß 2|‚ð!døáCÈð ‡�á„!Ã'B†O>„ Ÿ |>Aø2|‚ð!døáCÈð ‡�á„!Ã'B†O>„ Ÿ |>Aø2|‚ð!ÁÖ֖Ω‰~]– ºŒ¯³[ïoOÆ#!~^› ;ü±XJõ ‡�á„!Ã'B†O>„ Ÿ |>Aø:ÎKäßöënB¡Pww�ØGÈð28â›B©>Aø2|‚ð!døáCÈð ‡�á„!Ã'B†O>„ Ÿ |>Aø2|‚ð!døáCÈð ‡�á„!Ã'B†O>„ Ÿ |>Aø2|‚ð!døáCÈð ‡�á„!Ã'B†O>„ Ÿ |HBýA ƦŸq:&Ošˆ�ý`ç®�X±j5æ=ó,Úöñ¯Í0ÆpÎYgâýE‹Q]Ss�{LÝCÂD|˲pûÿüÓN:/¿ò~òË_á©yÏ`ÈàAøó=@zZÚ>Õ“œœŒ3¾÷]ôíÓpéÅáæ_ÿò`v� ºœ„‰ø'O�Š>……øé¯~�¦¦fÀ®ÒR¬Xµ 3o½žÿ}üã‰9{­§¹¹W\}­z$Œ˜@EüïŸ{6žŸÿ¢2zI,ÿžy§Ÿv*"‘²²²ðúKó‘Ÿ—§ÊL=ñÌ~ð~õþ…ÿ Æa歷༳§cÒ„ñxå…ç0óÖ[ºl<q0IˆPINFAÏžøâ«5q?_W\ è[XˆòÊÊ}®wæ]³pÅ¥?@ŸÂBÜuϽ�ÒW‚8HˆˆŸ—— ¨®©Žû9çÕ55èÙ3¿+»E‡, aøUUŽÁgefí±LzZíÒ„ ! ¿±© ••>l¨ëzaïÞøù�7`hQÂá0J¶ïè¦Ä¡EB>¼øò+8ÿÜs‡ÕµÚº: èßwͼ €Ý»w£½½�“ÓcŸë¶ëôþDw’0†ÿŸ PSS‹?κ ãÇ�EnNú¢º¦©))ÈÊÊD(BCC*«ªpâqÇ!##£G‰)“&î±ÞšÚZ èßiiiÈÍÉéÂÄÁ#a ¿=íwþ.Y‚ ¿yð~üâ§7¢´¬ W͸Iá$œsÖ™€ûxcnjƜÙ�à^ˆõ6î±Þ%Ÿ,EÌŽá‰Ùãúk¯éªáÄA…õë×�wFE6cè;`@gTuP‡Ã°m[¥úñm¥dËXüÀÌ6!Îñ÷…¶¶¶îîA2$LªOľC†O>„ Ÿ |>Aø2|‚ð!døáCÈð ‡�á„!Ã'B†O>„ Ÿ |>Aø2|‚ð!døáCÈð ‡�á„!Ã'B†O>„ Ÿ |>Aø2|‚ð!døñ-‚àÏjK:ÍðchmnÞ{A‚ ö›¶–X�À×Óyß¶QYQAÆO‰––TVT€wÂ…é´?¨ÁðX •••°c±Îª– �eYà±:ãO¸vê_ÒaÐÞNq0è$£hs� | >Aø2|‚ð!døáCÈð ‡�á„!Ã'B†O>„ Ÿ |>Aø2|‚ð!døáCþ©*H+�ÁIEND®B`‚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.png000066400000000000000000000002531451344070600215460ustar00rootroot00000000000000‰PNG  IHDR¹‡mð PLTEÿÿÿÿÿÿŽôÃìtRNS@æØfPIDATx^=α €0 Ѥ€ØæA"9µ³OhØ�-‰œƒk^cK?ým¸ :V-Ë@GêeèËö„Õΰqßo…ªÇ_NÓèpÄoYÖ„^]IºuZ•IEND®B`‚Projecteur-0.10/icons/cursors/cursor-busy.png000066400000000000000000000003111451344070600213710ustar00rootroot00000000000000‰PNG  IHDR ’g PLTEÿÿÿÿÿÿ~ï�OtRNS@æØfnIDATx^­Í± …0P§¹ ~ÓÜ)~6ˆ�ŽiÒÒ¤dJ°ŽÛWO.l(Þä@ X`×@ tüJ!Ö9ˆ©lˆ&³çßa²qµÒÁ‰lÀÆQ] ˜~#-Íæ�«Iå î8øÅø{4ß笸ñíu4ŒIEND®B`‚Projecteur-0.10/icons/cursors/cursor-cross.png000066400000000000000000000002021451344070600215370ustar00rootroot00000000000000‰PNG  IHDR¹‡mð PLTEÿÿÿÿÿÿŽôÃìtRNS@æØf'IDAT™c`À  ã²èÐÖP  Œ«Vp­š€à“mÜ=XKô ŒÖ "«IEND®B`‚Projecteur-0.10/icons/cursors/cursor-hand.png000066400000000000000000000002371451344070600213300ustar00rootroot00000000000000‰PNG  IHDR¹‡mð PLTEÿÿÿÿÿÿŽôÃìtRNS@æØfDIDAT[c``a€€L(=�Xz”Žj€Ð‘ JcZd˜ ¥§N Ѫ¡š�f �@¦¦†& Ð«V-ÀJ£7ÖU%�ÙIEND®B`‚Projecteur-0.10/icons/cursors/cursor-openhand.png000066400000000000000000000002401451344070600222040ustar00rootroot00000000000000‰PNG  IHDRµú7ê pHYs ‰ ‰7ÉË­RIDAT(Ï…Ðá…Ñïý_úú�tW–&›D„êN�të^a¯'Ï`O#9#åV*ˆ~®õÀßWa`'Ê-$ò2d_=€Jì«;Ò€>x0tš¯ªbIEND®B`‚Projecteur-0.10/icons/cursors/cursor-sizeall.png000066400000000000000000000002561451344070600220620ustar00rootroot00000000000000‰PNG  IHDR¹‡mð PLTEÿÿÿÿÿÿ~ï�OtRNS@æØfSIDAT[c`À\ ´f„^¦˜¦†5€éÐP0­Áè ¢—0:H�„§0:ˆ%¸BD�&p†A‚†‰ÃÔÁôÁÍ�™ ³f/Ü0waÛtêçNXIEND®B`‚Projecteur-0.10/icons/cursors/cursor-uparrow.png000066400000000000000000000002041451344070600221070ustar00rootroot00000000000000‰PNG  IHDR¹‡mð PLTEÿÿÿÿÿÿ~ï�OtRNS@æØf)IDAT[c`ÀL+ 4W„Ö kÓKC€é©¡ ‰†�¡0`­ç B7Ò~2IEND®B`‚Projecteur-0.10/icons/cursors/cursor-whatsthis.png000066400000000000000000000002771451344070600224400ustar00rootroot00000000000000‰PNG  IHDR ’g PLTEÿÿÿÿÿÿŽôÃìtRNS@æØfdIDATWch`€‚ ¡! Æ ÆU+AŒi lS³€Œ© R Œ#ƃ1B[Áº¦†&°-3¢VHM›Áa̘e,€1TDÀÖ«6@D8à ¨SÀ$ôÝW�/qIEND®B`‚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 €PFFTM–’Äé4OS/2Oö]/X`cmapÄnø•øjcvt Ddgaspÿÿ,glyfà2+�  headŠ™ãÜ6hheaðÆ$hmtxÁe¸@locaà"ºh8maxphÅ8 nameÐ&1¼ ÀpostÂw‡•ÔWÏ¿ˆ^_<õ Ý5¬³Ý5¬³ÿÀÀÀÿÀ.” @.ó�LfGLfõ„PfEd€ ñÀÿÀ.À@ »ªÈ*UU@*•dH ñ ññññÿÿ ñññññÿÿÿãÿþD****Lt¢Ô F&T¨ô^žú,nzˆ²Ü™U.±/<²í2±Ü<²í2±/<²í2²ü<²í233'3#ˆwffUþ«3@@!!7!532#•þ–j+þ@Õ  ªÕÿÀ€ J @@!!7!532#%#5•þ–j+þ@Õ  þ›@ªÕÿÀ€ J €€€@@!!7!532#%#53#5•þ–j+þ@Õ  þ›@•@ªÕÿÀ€ J €€€€€@@!!7!532#%#53#53#5•þ–j+þ@Õ  þ›@•@–@ªÕÿÀ€ J €€€€€€€@@!!!7!532#%#53#53#53#5•þ–j+þ@Õ  þ›@•@–@•@ªÕÿÀ€ 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*kkÿê–7>?55"7!3#!€ $+/+# ••86Gã+þUéŒUU+J2(G��G -")�UÿÿÀÀ !5%.767'53&7&3#3#3#$#"'33264&#"#632S # N ŒU*’þ]ªª+««@••À}XY?F&,FddF,&F?YX£ #B Ž994N+*++*ñ}@d�d@*ÿÀÖÀ'3!!754&"26754&"26754&"26!5326533@€þ€k   j   k   UþVz r @þ€UÖ Ö Ö Ö Ö Ö ˆ**UÿÀ«À'333«€V€@À@þÀUÿÀ«À###«€V€ÀÀþÀ@ € #'+!!35#3'35#3'35#35355!%5#75#þ@@+@@j@+@@k@ªUU€ÕÿUjj@€þ€+@@V@–@@V@–@@@@V@@•@@U@@V@@ € #'!!!#53#53#53#5#53#53#5#5ÕþVª+þ«V«@•@–@«@•@«U*ªUþÖUþ€€U@@@@@@@@V@@@@@@U@@ÿÀëÀ&>%'"&462&2'654&"327#"&4#"&462'654&"32%ÆJC!D.<##FjK'2G22$ 5K¥Y}}±}& d�ddG¯bVXA%##VK5 #22G2*KjÜ*}±}}X*' Fdd�dÿÀÀ2"&4$"2643#'755#5–Ô––Ô–X°}}°}ÿÀÀjjVjjÀÀ–Ô––Ôk}°}}°*@UUÀ@UU@*ÿÀÀ(%'74&'7'6'#5#"&546732U«J/,.6V-'/6&–Uk b‰7.(-pP À� ]_1ç1S" f;ùÕÕþa- ‰a;f !T1Oq@ÿêÀ–@€ªÕ*ÿêÖ–!!+ªþV•þV•ÿÀkÀ$"&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ÿÿÜ'k‚Ý5¬³Ý5¬³Projecteur-0.10/icons/projecteur-tray-64.png000066400000000000000000000104641451344070600207740ustar00rootroot00000000000000‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYsÄÄ•+tEXtSoftwarewww.inkscape.org›î<±IDATxœÕ›}T”Õ¾Ç?Ã3Ì00/€ /&Š�M 8yzQL�™¯ØQRñÚÊŽuQsÝUz*ת֪n¥…­å x³¼W3ë,W%˜ rÌME`fžûÇ3£ã8Àø®ßµžÅb?ûÙÏoŸßþ½í=z‡PJÀ£—~]@³í¯Ðǘ÷<{¹'´±±±3322ž‘J¥Þ®:yxxX+**ª6lØ� ÚyÀHè 2 ñÀ�§…¾a}ÿý÷¿þ (É{࡞ùý÷ßkÜ @°Z­–×_}'ð ¿Ç²ß(� §OŸ¾à‚ ÝÝÝÝ‹/Þ Œ\.™ J`™3gÜ&@¡³³³355õŸ@,àu�çpKP½QAŒFcÛ¸qãþˆBô$$ì¸eœÑÔÔdHNNþÌjoZì8xð`�È}lz nì¸é¯wùòåû>&è-@"‘ÜÐôz}ã?üPSPPðû7ß|³8 ˜oVÀ;�¾�H$’>5 ¡¡áÒîÝ»kòòòÎîÚµë4p 8 \ ÜçZÐÀ„³gÏ^t^ÛV«ÕRUUuîã�?>0bĈπùˆQàÓ³gÏÞ½víÚƒR©t  Å½ev_Büyß¾}¿ ‚ tuuµWfee ýHâ��@ Ð]TTô» ÂîÝ»•J¥Sy€I�ý Å¢I“&}¡R©þ x988øm¹\>ˆ†!Ç_öZTTÔ—ƒ¡ÕF±�)¢ða@LÿþýW�ÆÎ­[·žR^zé¥ïÞ}÷ÝCÀØ>øà×íÛ·_–¥§§Z­V‹�„ßÏË¡/�,€¸´Oœ8q¤B¡�) oÀ?00pPLLÌ@¹iÓ¦?ûì³Á±±±Q[¶lù¿œœœ#&L¾gÏžÕR©ô/€Ú�wÞU¸+Œð5jÔ ƒÁ`Ì‚ Z­V XŽ?¾÷СCMË–-Kê333ÿçĉU)))úöÛoß¾IpWo ßÈ‘#µƒ¡Ñ·[t:�‘ ŠÜÜÜ“Ï?ÿ|xddä³Ù|xÆŒymmm-cÇŽ¶gÏžûŽw…�aÑÑÑ�ƒ¡ qyX5�§í¾~Ë–-y­­­æ¬¬¬€µ¢¢¢pÉ’%ß ‚`HII¶{÷î·î'ÜÀçÉ'Ÿ|Ä××× ¹¹¹°VoĘÁh2™æåå]HKK8 ¨Ù¸qcîW_}uÄ>иqㆬ’J¥£¹Hp›€Ñ£G·ÿÓÔÔdº‹B¡ðV(¾ˆ„\X¿~ý*•Ê{éÒ¥iˆÕÒ^xaseeå9ûó“&MŠËÏÏÿð8÷˜w^ì ø�92ÔÞpùòåfÄ%`‘H$<ôÐC¶±šËËË÷>|¸vÁ‚±*•*è0›ÍßϘ1c»Ñhl±�ñÜsÏžÉ=&Á�—Ê€þ#FŒÐØêêêš°Ù€°°0bÌÐTäääü |å•WңĚß~û-oÉ’%{íöîúB€Z­þ{ww·Ùž e‰«V­:$‚0wîÜ­ˆÁ’ÐÉd²¿766¶^¸p¡ÙÛÛûyÀð&oÛ¶í�sn±sçÎ2à9Ä`é®OÜa\>qâÄQR©ÔÀb±t×××°-�Nç�¨Ðj2™ŽV………ù¿øâ‹³µ 8”‘‘‘sòäÉsŽ/˜úè£Cì í@'¢ú[Ôjµ�ペÏ>ûì_V«Uxùå—“s9Ð`2™öΚ5+¯£££ÍñE“'OŽ+((p4Œw…„¾ðtö£Ñ؉ø5¯Ø�F£â*Ð\VV¶¿¼¼ü|TTTмyóf¶gª�=ºcÙ²eûA¸¦N0eÊ”‘ùùùo"’àÏ] ¡/ä@øÐ¡Cì ­­­& °‚`Ðh42Doa¸¨Ü¼yóo‹/~ˆ|÷+²³³·äåå•:¿pêÔ©#mš�„h7îiAµ_\\ÜÛŽëðáÃg€¿CW®\ù/[Æ÷;ǵ&�L&{Y¯×A¦OŸž „#NÈèïãã³äôéÓç]T ¸ÃšÐ—ø<ýôÓqŽ MMMÝØ4Àj=šN§óDŒ÷ZM&SiQQÑi€%K–Œ!ºU+PßÑѱwæÌ™ù���ר€)S¦$äç篒¹ƒšÐRÀ/>>>±Q¯×[°€­Ö§Ñh¼ pÏ Ô|úé§? ‚`=zô 1cÆLફëªËÊʶ/]ºô{\Ô §N�Ÿ——÷6w�„Þ�!ñññÇÆ¦¦&3âä¬V› øùùÉ÷—€49räÇcÇŽ�“H$¯¾úêDà!®n™™€ãÙÙÙ9e®„˜6mZÂ�$¡7är¹|ÀÀ�ƒZŒØÜŸÝˆ«T*®_ Û6m:0~üøèøøø1€q"Ð œ3gNÎÙ³g/¸dÚ´i ;vì¸#$ôF€Ïøñãã½¼¼dŽ�ÍÍÍöTlnÐÇÇÇÇËËËÕ) аnݺ=ƒ¡ÙÓÓÓ3++k ¢1ôtÃÑ] 3}úô„;v¼Åm&¡'$€"11q¨ó [&h«Õ*H$�Øóg´šL¦²ÂÂÂ3Ó§O�2))iÐàÁƒDDD„êt:•J%W«ÕR???/___ûsŸ|òɳ«W¯cµZ-mmmf³ÙliooL���Æ?þø£íÔ©Sž_ýõÅôôôÁ½Mfþüù� ‚°233s5ð ¢Wéõl‚+ÉÉÉÃ\u¶%B¾ˆYÝ™Lv…¤øøøƒÁð–ŸŸŸ²·^'€§§—V«õè«{ŸX°`ÁhAV.\¸ðmà}�àL€'àŸ��ÐßUg½^ϤI“2¦N�:ð‰'ž‹ŒŒ¼"±Ì‡î‚Ñhlmiiimhhhojjê4™L’ööön“É ¨Õj/©TjU«Õ^:�N¡Ñh”*•Ê_*•ÞÒùÂÌÌÌÑÀÊ… ®¦œ×‰xºººz}DDD°sgA¬‰Ä¥«kll¼TRRr©´´´­¼¼üÂO?ýT©×ëë�È´vïáx¢Ü13”!®[?À?11qp||ü ¸¸8MRR’::::¢§ÃÚ½aýúõ?.Z´¨OЯ_¿W-Kw'àA„ºººº?üðçøøøÏ€ÀSˆ&B r œ¾\kåí—ÌÖ®´M^ËUŒRµZíª… þoqqq…Åb1÷-ÙU¬[·n?07½CxFFFn_ƒÖÖÖþ‘žžžüâáè›ðrDÏr;25 ¢vø!¶ L�‰‰ù ¨¨¨Ô~ÃdggïRú"ÁˆY³fÍuŽ(,,,U*•ÿ‰èªt6!ïFáÒQ“ÓRSS·vvv¶¹KBnnn1¢»½Î»Ù!/))éñ÷¬”J¥ ¿xŸg ï<Sé'æÎ�»M«»$¼ñÆ…ÀzˆXý�¿ †æž˜7o^¢*ÞëÐDû’yéÒ¥Zw ¨­­mDÜ‚»’8Zt¯ÐÐÐ`???z@hh¨/=‡»w^ …B¦T*ÝŽ;‚ƒƒu�¦?=œ]ÖéF£±µ'/_¾Ü4jÔ¨Uˆ›rî žˆžbR^^^©»__¡¥¥¥˜ÍµÕ¨+ð’¾øâ‹’Þ¹téRcFFÆà Äaçrø�€Ä&ŸùÊÞ½{�ÝÈäA>ÿüóbD×ê2Ð’�2™ì¥£G�žêm «ÕjÝ¿ÿÉ9sæ¬þŠ˜¸„ 2+ãÖ]¡Ý*w–‰±±±YÙÙÙ?ÙbÞŽ9rR.—/D´=Ê&øúú.)***ܰ°çÏŸ¯ß´iSiFFÆ?}||f£ å@ĺ_?Û—SÛR!!{ä§FŒ!‚€þˆfbV8)))iå{ï½÷ÝÏ?ÿ|Æl6ßPdÿ^;wî<¢P(^B °®1஘�Ú„NNKK›ùÎ;ï$‡‡‡‡ºèwººººª««/WVV¶œ:uª¶®®®®¦¦¦öÌ™3”——×›ÍfbŧÛö@®>|xhxxxHÿþýƒ £�ŠŠRk4š�r_8þ|ÍŠ+lÛ¶í ¨çj-£Wx ~�áÀ´´´%%%7†ºBGGG»ýr 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); };