pax_global_header00006660000000000000000000000064141253721230014512gustar00rootroot0000000000000052 comment=82d9cfac1082a5cf28debb79a7be732fabf4dfd4 Projecteur-0.9.2/000077500000000000000000000000001412537212300136445ustar00rootroot00000000000000Projecteur-0.9.2/55-projecteur.rules.in000066400000000000000000000020331412537212300177340ustar00rootroot00000000000000# 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 supported Bluetooth devices @EXTRA_BLUETOOTH_UDEV_RULES@ # Rules for uninput: Essential for creating a virtual input device that # Projecteur use for forwarding device events to the system after grabbing it KERNEL=="uinput", SUBSYSTEM=="misc", TAG+="uaccess", OPTIONS+="static_node=uinput" Projecteur-0.9.2/CMakeLists.txt000066400000000000000000000316441412537212300164140ustar00rootroot00000000000000cmake_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-9;-checks=*,-fuchsia*,-modernize-pass-by-value") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules") include(GitVersion) include(Translation) set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) find_package(Qt5 5.7 COMPONENTS Core Gui Quick Widgets REQUIRED) find_package(Qt5 QUIET COMPONENTS X11Extras) set(HAS_Qt5_X11Extras ${Qt5_FOUND}) find_package(Qt5 QUIET COMPONENTS DBus) set(HAS_Qt5_DBus ${Qt5_FOUND}) # 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() add_executable(projecteur src/main.cc src/enum-helper.h src/aboutdlg.cc src/aboutdlg.h src/actiondelegate.cc src/actiondelegate.h src/colorselector.cc src/colorselector.h src/device.cc src/device.h src/device-vibration.cc src/device-vibration.h src/deviceinput.cc src/deviceinput.h src/devicescan.cc src/devicescan.h src/deviceswidget.cc src/deviceswidget.h src/linuxdesktop.cc src/linuxdesktop.h src/iconwidgets.cc src/iconwidgets.h src/imageitem.cc src/imageitem.h src/inputmapconfig.cc src/inputmapconfig.h src/inputseqedit.cc src/inputseqedit.h src/logging.cc src/logging.h src/nativekeyseqedit.cc src/nativekeyseqedit.h src/preferencesdlg.cc src/preferencesdlg.h src/projecteurapp.cc src/projecteurapp.h src/runguard.cc src/runguard.h src/settings.cc src/settings.h src/spotlight.cc src/spotlight.h src/spotshapes.cc src/spotshapes.h src/virtualdevice.h src/virtualdevice.cc resources.qrc qml/qml.qrc) target_include_directories(projecteur PRIVATE src) target_link_libraries(projecteur PRIVATE Qt5::Core Qt5::Quick Qt5::Widgets ) if(HAS_Qt5_X11Extras) target_link_libraries(projecteur PRIVATE Qt5::X11Extras) target_compile_definitions(projecteur PRIVATE HAS_Qt5_X11Extras=1) else() message(STATUS "Compiling without Qt5::X11Extras.") endif() if(HAS_Qt5_DBus) target_link_libraries(projecteur PRIVATE Qt5::DBus) target_compile_definitions(projecteur PRIVATE HAS_Qt5_DBus=1) 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 9 VERSION_PATCH 1 VERSION_TYPE release ) 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. configure_file("${TMPLDIR}/projecteur.1" "projecteur.1" @ONLY) find_program(GZIP_EXECUTABLE gzip) add_custom_command( OUTPUT ${OUTDIR}/projecteur.1.gz COMMAND ${GZIP_EXECUTABLE} -9f -n "${OUTDIR}/projecteur.1" WORKING_DIRECTORY ${OUTDIR} ) add_custom_target(gzip-manpage ALL DEPENDS "${OUTDIR}/projecteur.1.gz") install(FILES "${OUTDIR}/projecteur.1.gz" DESTINATION share/man/man1/) 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 gzip-manpage projecteur) # 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.9.2/CONTRIBUTING.md000066400000000000000000000003661412537212300161020ustar00rootroot00000000000000# 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.9.2/LICENSE.md000066400000000000000000000020401412537212300152440ustar00rootroot00000000000000Copyright 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.9.2/README.md000066400000000000000000000370151412537212300151310ustar00rootroot00000000000000# 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 * [Motivation](#motivation) * [Features](#features) * [Supported Environments](#supported-environments) * [How it works](#how-it-works) * [Download](#download) * [Building](#building) * [Installation/Running](#installationrunning) * [Pre-requisites](#pre-requisites) * [Application Menu](#application-menu) * [Command Line Interface](#command-line-interface) * [Scriptability / Keyboard shortcuts](#scriptability) * [Using Projecteur without a device](#using-projecteur-without-a-device) * [Device Support](#device-support) * [Troubleshooting](#troubleshooting) * [Changelog](#changelog) * [License](#license) ## 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. * Vibration (Timer) Support for the Logitech Spotlight (USB) * 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 keyboard events instead. (You can still disable device grabbing with the `--disable-uinput` command line option - button mapping will be disabled then.) ## Download The latest binary packages for some Linux distributions are available for download on bintray. Currently binary packages for _Ubuntu_, _Debian_, _Fedora_, _OpenSuse_, _CentOS_ and _Arch_ Linux are automatically built. * **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://api-prd.cloudsmith.io/v1/badges/version/jahnf/projecteur-stable/raw/sources/latest/x/?render=true&badge_token=gAAAAABgPebvngKb3w0EsZUr_IHIIzlfYCipDOGxcJdzMRGI3BLdVsLf62Na7Cg6q11ps7yNgv3kR9KXyxJyjFFbPs2eTAGzvL-UXTonyqSY5D1fwva_o_g%3D [cloudsmith-rel-latest]: https://cloudsmith.io/~jahnf/repos/projecteur-stable/packages/?q=format%3Araw+tag%3Alatest [cloudsmith-dev-badge]: https://api-prd.cloudsmith.io/v1/badges/version/jahnf/projecteur-develop/raw/sources/latest/x/?render=true&badge_token=gAAAAABgPd_g3txb3xWrIHsaUrhBB7hOamTwfPVpR7xGUELEaQ0pGnxFnXO1cqTPAMDcTjRsHM2zAjx00OXU_5ARSQDofAUe6lIqKrKNykiMhVT_jlZAy-4%3D [cloudsmith-dev-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 ``` 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. ``` 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. settings=[show|hide] Show/hide preferences dialog. 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 ``` 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. ### 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)_ #### 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: ``` 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.9.2/cmake/000077500000000000000000000000001412537212300147245ustar00rootroot00000000000000Projecteur-0.9.2/cmake/modules/000077500000000000000000000000001412537212300163745ustar00rootroot00000000000000Projecteur-0.9.2/cmake/modules/ArchiveExportInfo.cmake000066400000000000000000000005321412537212300227750ustar00rootroot00000000000000# Fallback for version generation from pure git archive exports set(GIT_EXPORT_VERSION_SHORTHASH "82d9cfa") set(GIT_EXPORT_VERSION_FULLHASH "82d9cfac1082a5cf28debb79a7be732fabf4dfd4") set(GIT_EXPORT_VERSION_BRANCH "tag: v0.9.2, master") # needs parsing in cmake... set(GIT_EXPORT_VERSION_DATE_MONTH_YEAR "2021-09-30") set(HAS_GIT_EXPORT_INFO 1) Projecteur-0.9.2/cmake/modules/ArchiveVersionInfo.cmake.in000066400000000000000000000012341412537212300235460ustar00rootroot00000000000000# 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.9.2/cmake/modules/GitVersion.cc.in000066400000000000000000000012361412537212300214030ustar00rootroot00000000000000#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@"; } } Projecteur-0.9.2/cmake/modules/GitVersion.cmake000066400000000000000000000540731412537212300215000ustar00rootroot00000000000000# 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_DATE_MONTH_YEAR "" PARENT_SCOPE) 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() 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) set(${prefix}_VERSION_DISTANCE ${${prefix}_VERSION_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 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}" 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_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_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() 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.") 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_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_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.9.2/cmake/modules/GitVersion.h.in000066400000000000000000000005451412537212300212470ustar00rootroot00000000000000#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(); } Projecteur-0.9.2/cmake/modules/LinuxDistributionInfo.cmake000066400000000000000000000042361412537212300237160ustar00rootroot00000000000000# This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md cmake_minimum_required(VERSION 3.0) # 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.9.2/cmake/modules/LinuxPackaging.cmake000066400000000000000000000320431412537212300223040ustar00rootroot00000000000000# This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md cmake_minimum_required(VERSION 3.0) 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.9.2/cmake/modules/LinuxPkgCPackConfig.cmake.in000066400000000000000000000053551412537212300236040ustar00rootroot00000000000000# 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 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.9.2/cmake/modules/PKGBUILD.in000066400000000000000000000010401412537212300201200ustar00rootroot00000000000000# 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.9.2/cmake/modules/PkgDependenciesProjecteur.cmake000066400000000000000000000027761412537212300245050ustar00rootroot00000000000000list(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.9.2/cmake/modules/Translation.cmake000066400000000000000000000143741412537212300217050ustar00rootroot00000000000000find_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 ${_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.9.2/cmake/modules/travis-ci-bintray-deploy.json.in000066400000000000000000000020061412537212300245330ustar00rootroot00000000000000{ "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.9.2/cmake/templates/000077500000000000000000000000001412537212300167225ustar00rootroot00000000000000Projecteur-0.9.2/cmake/templates/Projecteur.desktop.in000066400000000000000000000003241412537212300230430ustar00rootroot00000000000000[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.9.2/cmake/templates/copyright.in000066400000000000000000000024011412537212300212570ustar00rootroot00000000000000Format: 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.9.2/cmake/templates/postinst.in000077500000000000000000000001701412537212300211360ustar00rootroot00000000000000# Make sure uinput module is loaded modprobe uinput # Reload udev rules. udevadm control --reload-rules udevadm trigger Projecteur-0.9.2/cmake/templates/preinst.in000077500000000000000000000000021412537212300207310ustar00rootroot00000000000000# Projecteur-0.9.2/cmake/templates/projecteur.1000066400000000000000000000063671412537212300212020ustar00rootroot00000000000000.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.9.2/cmake/templates/projecteur.bash-completion000066400000000000000000000075441412537212300241240ustar00rootroot00000000000000# 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= settings= preset=" 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.9.2/cmake/templates/projecteur.metainfo.xml000066400000000000000000000025601412537212300234320ustar00rootroot00000000000000 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.9.2/devices.conf000066400000000000000000000007401412537212300161360ustar00rootroot00000000000000# 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 Projecteur-0.9.2/doc/000077500000000000000000000000001412537212300144115ustar00rootroot00000000000000Projecteur-0.9.2/doc/CHANGELOG.md000066400000000000000000000057761412537212300162410ustar00rootroot00000000000000# Changelog ## v0.9.2 ### Fixes: - 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 ### Fixes: - 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 ([#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 ## 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.9.2/doc/LinuxRepositories.md000066400000000000000000000135531412537212300204510ustar00rootroot00000000000000# 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 availabed 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 ``` 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 ``` 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 ``` #### Ubuntu 18.04 ``` 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 ``` apt-get install -y apt-transport-https curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/gpg/gpg.544E6934C0570750.key' | apt-key add - curl -1sLf 'https://dl.cloudsmith.io/public/jahnf/projecteur-develop/cfg/setup/config.deb.txt?distro=ubuntu&codename=focal' > /etc/apt/sources.list.d/jahnf-projecteur-develop.list apt-get update ``` #### OpenSuse 15.1 ``` 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 ``` 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 ``` #### Fedora 31 ``` 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 ``` 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 ``` 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' ``` #### CentOS 8 ``` 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.9.2/doc/screenshot-button-mapping.png000066400000000000000000001205031412537212300222370ustar00rootroot00000000000000‰PNG  IHDR2bÝB(®sBIT|dˆtEXtSoftwaregnome-screenshotï¿> IDATxœìÝw@UåÀñ/½÷PÎÜifî̽5·e–•©eNÜ»¥Y¹RÓL-ýUîÙpæVp‹,A‚ (÷üþ@¯ [½ÂÍçCorïyÏû>g\Îsßsî¹zoùûûoddäŽB!„HKK‹ ~O¯Aƒ‘’Ä!„B×Ü¿?R%IŒB!t‘±±±‡ª¤ƒB!„xZ’È!„BgI"#„B%‰ŒB!t–$2B!„ÐY’È!„BgI"#„B%‰ŒB!t–$2B!„ÐY’È!„BgI"#„B%‰ŒB!t–$2B!„ÐY%€/+KKKFEJJ ³fÍB­V—tH:ÉÚÚšîÝ»ãççÀž={ؼys G%„xQ$‘¢AAA¸ºº¢R©HMM%11‘'N°{÷nîß¿ÿÔíš™™akk‹¹¹9†††¤¥¥=u[VVV|ôÑG¨T*¦L™òÔíh‹¶Ö!@¿~ý <<œ„„nÞ¼ùœ¢BèId„(„*•Š . ( >>>´iÓ___¾úê«§n7&&†É“'“––öLI €‘‘žžžÄÅÅ=S;Ú¢­u¨¯¯ŸŸiiiÌœ9SFµ„x I"#D­Y³†ØØXÜÝÝ "00222èÓ§eË–ÅÆÆ†ÌÌLV®\ɉ'033£C‡Ô¨Q}}}Ξ=ˆ HJJÂÔÔ” &p÷î]†dÒtîܙʕ+£( ÁÁÁlذÔÔTŒiß¾=µjÕÂÈȈ7n0{ölMŒ|ÿý÷|ñÅ\¹r¥À6™2e qqqŒ7SSSæÍ›§‰ÉÐÐ0ße{‘ë0¿øŒŒP©Tñí·ß°råJxð€ˆˆ<==ùüóÏ5# ¥Eaë$¿„äEÏ÷HAÛ¨°¶Ë—/O§Nˆ‰‰ÁÌÌ @>…%DÈ}…ÐEQøæ›o8tèæææØÙÙqòäIæÏŸÏƒ4×\•}[FEEáîî^Ò! !„ ˜‰Ljê݇P«3µÒ‹J¥ÂÝÓ‹«¡¡%J©åîá¥ûÔ£mzõrI‡"„â¡b%2wîÜA½RÀ)MÔj5*•>õŽ^ä¦+ûÔ£mYÚO !ÄˤØ¿.­Cÿ¥™¬³‚éÒúÑ¥X…âe ¿~‘d@F!„x®$‘y¡$“B!ž§ÿÔ}“’’ yªyoݺõR]ûо}&Mš\Òa!„ÏD§™û÷ïçx¼jõ&Nš¢yܧ߾˜3·HmÕ©WŸ'Oæ;=--íé‚|cÆŒÅØÄ•¾VÖ6Ô«_Ÿ ’‘‘ñÌmwéÚ…F=‡(…Bˆ’£õDæÜùótéÖƒr~Tð¤eë6œ;þ™Û;o>ïþ¨À:íÚ¶áµúõŸ¹¯N]»±rÕÏÜNÖÍHŠ^ââbiÓº5±7b8x`?ßÈìÙ_кu232ŠÝ^öÒ³G^oðÚ3µñ\ÊS¨S¯>k~ZûìÛC!„ÎÓz"Ó·ÿ;ØÙÙ±å÷ߨ´ñz÷ꉫë³ß5-=¥a§Žxµî³%@zZú3·ñ´Œ±··§b` ýûõe÷Îüõ÷߬^³FSgÅÊUø`mkGû¹yó&>åÊñýâ%šz¿ÿñήn¨Õj:tìDÐøñši~ù… þþ›šQÞÏøøøÛ¾uëmÛµÇÒÚ+[&M~<&„B¼(ZMdRSS‰ˆˆ {·.øùU b` ow•½ûö§rµWð(ëÃk¯¿Á’¥ËP«ÕšùÏœ=K»ð­àO£&ÍØ¾cGŽö·ïØ“«;N®î˽û÷:üS mÛ¶ìÞ³G3ï®Ý»iÓ¦5z*UŽ6ÿüûoúöëOиq\ºxU«Vago_`ÛsçÏçöíÛœ?Â¥‹èÝ»÷S-Ïó¶qÓÿhðFc|+øÓ¹kw.]º¤™–™™É¼ù_Q±J5<ÊúP½V¦MŸ¡…(„B¼(ZMdLMMiðÚkŒ=–í;v™™ó¦g‡fô¨‘>ð#?Á—sæòÃÊ•$&&Ò¥kwêԩ;Ý;ôÞ@Þ}ï}NŸ9£™¿y³fD\»Jĵ«Ô-däåð‘# ôü!ÿ>ÄÔÉ9x(gòóþ 9t€™Ó§1aÒdN~Üׄ qš¾žMqó¹Ÿ÷ðp'.6PøzÁBzõêI‹Íquuaä矱yóf@¡}ûvìܹ“ôô4@aóæÍthß.Wûß}÷={¾M¯^=ñðp§nÚ…¶meiItL4qñq8::àííUÌe{þiÌþý:üS†ÂÞ]; ð§K·ܽ{€¯,dÕ«ùnÑBþ=|·ÞlÁÉS§Ÿ{B!^­ŸZúqåtíÒ™±A¨Y».Ë~ø!Ç)!w77ÜÜÜhÓºC?Â?d%2¿ýþ666Œù9žžžtïÖ•·Z¾É«ŸRQ©Tadd„JUð¢,ÿaíÛµåíîÝquqÁÓÃ3WòåÊáâìLëVoQµjŽ9¢™f`` é«¤EDDâàè@Lt4Ë–-ÇÔÌS3 ZµnËýû÷INNæµúõ166f×®]=vŒ[·iܸq®ö®…^£œ¯o®ç j{ÈéÞ½;Í›¿Iƒ 9vü¸Ö—»0«V¯¦SÇŽthß//O&Ž`ç®],]þ#F|Jƒ×^ÃÅÙG'§’ W!Äs õDÆÄÄ„áÆrìÈ!¦LžÄô³Xºlyžuýüü  22вe½ÐËv+Uooo"#£ž*Žðˆ*T¨Päúö$&%=U_ùzÚs/Ù_¸p‘?ÿü“6­Zƒž|8x0©wR4%ýþ=,-,ÑWéóvü´ögÖþô3]:wƨŒQ®v]\]¸|ùJ®¾ j»Œa&OœHXèU^yåÚ¶mÿôË÷œDDFâí]VóX__OOO""£¸wïñññø•/ú> „¢ô{a¿ÖÓÓ£u«·hÛ¦ »÷ìͳNdd$...¸»»žcôæÚµkxxd]£bldôð”IѸ8»]äú*•JÓ·‘‘iéÏã‚ßâåSSS¹qãgΞaé²¥4mÖŒ ^£gÏ€Bÿ¾}Y¶|96l **ŠK—/qùÊeM¾Ão¿ýÎê5k8ðrfY¿¿;`?­]ËÊU+‰ˆˆàä©“¤¤$Øöé3§ '33ƒ*U*‘œœLffÆS-ãóâáîεkašÇ™™™DDDàéᎉ‰ V––\/Æ> „¢ôÓj"sïÞ=¾^ø gΞ%22’¿ÿù‡¿þþ?¿ÇïŠ÷îû“°°p9‚oѵKg ë£Ó‰‰‰Ì˜5›ˆˆÖ­ßÀÖmÛéÕ³'>>ÞœcË¿£¯ŸõE”mÚ´fÁWó™6}þy£QcöìÙ£i#Àߟzõ^ÅÇÛ›ÚµjåÙO«VoñÝ¢o˜;w>þi×¾aáá¶ýÛo¿óJšØ;:3gÎ<–-Y¬‰éEHJJ":&FS’“SèÓ»¿nÜȦÿýðð&>ü$UófÍ€¬u5gÞ<Ο¿@dd$§OËõ1B¡ëôãbŠü¶øø‰T«V=×E»ù‰Ž‰aÔè±?q‚Û·“ppp¤UË77v ÆÆÆ”÷À××—k¡×026¢}»vƒ¡¡!§Ïœ!hüD‚CBðòôdäçŸñf‹¤§§óÑOسw&&&Ìž9ƒÚµjÒ«o?|¼½Y´p}û¿ƒ¿¿£G~ÀW ²bÅJRîÜ¡¬—‘QQ\< @yÿ–-YÌë ä˜7""‚Aƒ?äܹó8;;ñ˺Ÿñðð(òJÖ××gß¾½tìÔí|VGWé±ñ×_hÔ¨q‘÷)ȺLXXxŽç>xDZqÓÿ˜÷Õ×DGGS­ZUfL¢9¥˜’’¸ñؾc'†††X[YQ¡By–/]’W7yÒ××çÔ©Ô¨^½Èó!„Э&2…y2yЦ””nackÃÝ;wøjÁBbbn°jEÞ×ë$‘yBpH4hXôïyB!DÞNû—sT¬¨dF™'$%&f%1ò5 B!Ä3«^£ÿü¥½/Ú•Dæ qqq(’Ä!„ÏM\\œÖÚ–D&J¶ÿ !„âYÿ‹ ‹C™<)2*#„B<Oñ…ÖÅ"¿Î‹¢ètI½{—ØØØãÉNèÕ«Åž/ãÁ¢"#Ÿ¹ÿ›7oj}o%$ð =½TÄòd‰%=-­D÷ÌŒ âò~ðÀALMͰ°°¤G·ŸºŸ¤ÄD”è²¾ìåý÷ßÇÂÂSS3¶mÛVâñH)á¢E:™ÈÜ»wŸr~ìÚ½[+í+ŠòBʼyóiÖ¼…æñƒxðàÁ3·Û»O_.\øÂ–#¯’ײ|0øC~þyŠ¢pðÐ!¼Êz“p«Ð¶-ú–^½z«¯'Ëž={¨[÷UÒÓÓµºÜƒ?üiÓ§XçæÍ›T¥:!!!/l{Ä'$àîîAttt‰î‹/¦[·n¨Õê<§«5ŽŽŽ$ßNâ§5«ŸºŸ ~~9r¤D—õe/ß.ZDòí$|||Pgf–x:wéÂ'C‡æ¨…‘± .\@QΜ9KË·Záàè„““3¯5x3g΢( M›5ÇÂÒ S3\ÝÜi×¾=GCQ&MœÈ¼y󉌌Ì7ž 'Ò¼E P…U«Váã[[;¼ó.÷îÝË5ÏùóçqrrfúŒ9ž¿wï_ΙC:u±±µ#%%%ï~Õê|÷íüÊýû÷126ÉQÖ­_¢(\¿~Î]º`ïàˆ““3íÚ·'**ªÐ6ûõëÇõèh6üòKÞû®¢;ÎçQ ÚG£VíÚXZYÓ¸ISBCCsì[Ÿ}6G'ÜÜ=˜3wn‘ÛÍ^âââhß¡VÖ6T¬T™]»v¹'K||< ^oȧŸ}–o?úS3ó|§´Ìù½?üè#jÕ®{›òèMù‹Ý¦RJWÑ&­&27bn0{Ö ¢"®±å÷ߨU«&ÝzôdêôÏÜö˜Q#ñpwQæ–ßFPx¾¶fÍšôíÛEÉJ*Òž’xæF)Z¬«W¯aþüù|ñÅlŽý—ÅßOý×ê?sÿù- r,EÁÑÑ‘±cÆ R©Š°<ùÜŠ²Þ¶lÝJrr2Ý»wÏG®müðù.]»booϾ}{Ù±sï €»»Š’uŸ1c:×£"Ù¾}666´lÙ’øøxÊ•+GóæÍùfÑ¢.–Ä[ $ÞJ SÇŽ(Š‚½½=½{÷æè¿Gسw·n%2aâÄBÛ,S¦ ƒÀüù_=·8ó} ñàYÐ6HOO§k×n4nܘ³gNãììL¿þ4ó.^²„õ6°sÇvV­\É”)SÙµkw‘·í£2ä“¡¤¦¦ròÄqÞ}çºuïAbbb¡}ŠŒŒ ÂÂÃ102åâ¥Kš¶>1’n=z`çèÌž½YŸKÏÈÈ`Ò䩸º{aneK›v l»PJ^ç÷ò?÷·nÝ:ªW¯££o½ÕŠóçÏk¦%%&2`À;88:âàèHÕªÕØøëFP.XHã&MrœC¬S÷ULLÍhÞ¢( 1ÑÑôxûmœ]¬X‰Õ«Wkêgöö9úúþûÅøø–ÃÁÑ‘nÝ»köíèë×qqucË–-îG‰‰‰XZZbfjб‘ÆFF¨ôô@Q040 M«Vxzx`bdDff&åË•Óô]Ð~Û¡];Ž?NLttë(çkpïÞ½8;»äØçMLÍ8sú4( ?ýô±´²ÆÏ?€ø¸8Ͷœ0q"®nî8»¸2fÌØ|¯]*hlß¶»wî0eÒ$<ÜÝùbÖLŽ9™3g@QX¾l9C?BÕ*UhôFCztïÎâ%K m÷÷ß~ÃÕÍ›7nÏï¿ÿÎô©S)ëåÅ'C>ÆÅřի×ÚGövPB¯†²léZ¾ùfžûøùóç™7o>ß.ú&ß¿u…-ó“åêÕ+¼ûΦN™œO›ùüM•¢“eÉ’% ø^žeùòåùÏ«E/ü™>½{addÄö;˜:m[¶ncÇö-ìܾ•_~ÝÈòVRÖË‹*U*³gï>ͼ»öì¡]Û6¹Úœ>ck~ZËOkVqáÜY¦=|Aå×vaòÌ&ÈëÝÆ¾?ÿäý3räç>tˆŠ+ÒªuRîÜAQzöêÍõë×ùsï^Žýû/Ö6Ö\¼t)kþ'NüýçŸÜŠãß~C­VÓ³WoTz*N?Æì™3üáGœxúcæ¬Y¬ýy-«V®àìéSLš8AÓn£7ÞàØÑÙºy »vïæ»ï¿Ï{Ë–orôèQ>ýlD®S!¡¡¡–)ÑÇٽk'¦¦¦´lÕŠ;wî¢( 3fÎdý†õ¬\ñ;¶o#""‚wß{/ßeÉŠëÑ>ý¸Î£ß [OnîîìØ¾ãÇŽbhhÈ'ÆÜW¶räß©Y³æãçPrÄñd<¦¦¦4zã †Æl&###×¾ðhþôôtVý¸KKKüQ…Z5kqùòrÇrä_jÕzË… ©^½ºæqµjÕ¸uë±±±(ŠÂöíÛ 9wޱcFçˆ[­VóãêÕŸ ÆÁÑ ¿€@¦MŸ¡¹öä?63vÜXfϚɿ‡ÐµK—‡Ëfææ4iÜw7·÷£ØØXÔ™™L˜8‘ŸÖ®ÕìÓÙKY_+UƨL>Ì—_ÌÆÙÙ™·ÞjIíZµØ¾m;Š¢°dé2‚ÆŽãõ puu¥R¥JYí(àî›5jT§KçÎ8p0WìõëÕc×΄……Q¹j5ºvëΕ«W5±™›™áå剿Ÿß÷-©©©lݶEQXºl9AcÇQ·Nüýü˜?o.[¶l%&&&ÏeÉzNɶìHEYOÖVV”óõÅÇÛ›α`E¾ýî{M’ðÙˆ”õñÅÉÅ• '2|ØP,--QGnܸ‘g,ŽÙb‰OˆÇ<Ûé ss ëæQiiiŒø|$sç|‰Áø³êEEEqëÖ-f̘NصPÖ®YÃ7‹±~ýEaõš5ôèÞƒŽ:àååI‡öí5û°…¹9«V® råÊîGžÌš5}}¾œ3—:uêæJÎ._¼ÀáƒHOOgà{ƒP¥ÐýVQò\?Ù÷‹¬m•;±yôX__Î;¾J¥‰ûQÍ›áãíMƒ×^cø°¡¬^½š«¡¡XXYkÊíÛ· Ü ñ¹N×YZXOJJ iii˜™>žnnaA\||¡Û¶Zµj¬Z¹3SSââ151AOOïq] â‹ÐGövr¬Ç'^kö ss Ú·k—k=g/-ó»ï½§Yw£FΣÏ<ÚÌço§Ý-cnjɺ¾ñᥥcÇŒ)`žB»Ïä…'2jµš¨ë×qptàîÝ»$''ÓoÀ»˜YÚ`fiÃì/çpóá—5vh׎}þÅ­[‰üºqo4lˆµµUŽöRSS¹K9_ßÏÖvA”<ò~><<_Íc•¾ ///Â#¹†‘‘eËzekEs8Ïñÿ'§)(D߈  b%lí°µwàߣG¹{“»©©ÄÆÆâíëG¤9—ÂÑÑ[‰‰yÆ_§NmÖ¯ÿ™à3¨ôU¼Ùò-î¤ÞÍ[£2øx{s-,ŒÔ{©ÄÅÅåèÛÇ×€°ˆð<—%÷s£,îzrpt %%…ŒÌŒ|ûÊþ£V«ÑSéië蓞ž–£NZz†((˜˜3rä焟eö¬™L˜8‘Eß~«‰.hÜXÜÏÑ£GXµr_}½€o¿û.kPeÝ4!#33ÏXTÙb±³³#åîÍãä”dììíXýÓO”/_žºuëäZÎôéøøclbLÕjUhõÖ[ìÞ»…ëÑ×ñôòÌcýçü)h?rsw£OŸÞŒ;šƒþÁØÄ„o¿ÿ>G}}*U®Ä´iSøßo¿qçî÷[Í|*}2óX?²LÍãG÷<ö#C~ûmÀ·|&NšÄƒŒyîeË–%:&77Wܯ)fæfn[;;îd›–5={{;ÌÌÍ022âÎÝ”óÚÛÛºm³·gkgKê½{dª3Ÿh§ð>òûyrùd<`úôY£mšy¿f Zæ ãÇkÖÝ'Ÿ Ésûå‹üü×~fÏž…¡!††Ìž=«ÐúÚôÂoˆ·bå*ÒÓÓy³Es,,,°¶¶bùÒ%´mÓ:WÝÊ•+À¯7±öçu úI®:¦¦¦X[[qùÊj×®¥y¾°¶ ¢  ä•B>©ÉÎÃÓƒ«G1233 ÃÓÓÒÒÒˆ‹‹ÃÞÞ^ÓÙÞ©@V›*• ÒÒîkÚrwsG¥RqåòE¬­­s…ceeÅ•+W¨Y£Fîøy«^¶ jóãääÄìY³ð$øìÙ±AV…‹‹ ÆÆÆØÛÛsõêUjÕ¬ À•+W²Ö‡»GžË’kÙ•ÇïÞŠ³ž kTïQLúúúy÷•ƒƒqñ¿z¢¬—ÊQÿìÙ`ôõõquuÍÕNÛ¶mرs';vìäƒÞÀÒÒGGGM{7mbÿ ôqqñûµÏÕ–½ƒ½æÝ4€_… œ8qRóøä©SØÚÚbooÏ¿ÿÁÎ]»0·xœ¼oÞ²…ÛIIŒ„J¥âÒ¥Kxzd ó¦ÞKÅÆÆEQðððàâÅ‹9ÖóÃ_rÄdbbœï~”þ9FܲÓÓ{üž¨ ýöѼqññ888äjKÉ6ê`emEJJ wïÞÅôá¨Ã£vE¡flÙü'Nž¤K—n¸¹¹ñÎ;rÔ¸pñ"^^^£Ï‚¶_… Ì›7Ÿ```À7¸yó&*T@Q*T(Ïñ'©_¿>§NÂÏ/kZAíf_n_T*§NŸæ•jÕ¶sšß-´|e{ý<š'<"‚×¾‘£š¹…7b®cff–c}ä·Ì®®.¸ººäÚžš>Ÿ|îqÅ‚ã:iÁ‚¯€|¶yvz:~j)évÑ11üûïQ&NšÂ¡Ã;f¾>Yïàûõéø  üóÏ~¢cb8uê4 ·4ó¿÷î»L6ˆˆ:uìgúõcÂÄÉìÙ»—¨ë×9zôX‘ÚΗ¢ä.d}¡äõë×5%ùömôïÇúõëÙ°á®]cìØq¼õæ›øûáïïǧŸŽàúõëœ9s†ðˆˆœ}deGè>>Þlܸ‰ëׯsâÄ *U ¤j•* þð#Î…œ#úz4‡ÒÌÛ·Oo¦L™Æ¾}û¸~ý:Ç{|QU®øs/Óï¿ÿΖÍ[¸Ê¥‹—X¸p!ÆÆF”õòE!2"‚ûÆÄ‰“¹wïo½™u1í€þý˜6m‡æÂù ö)-[¾‰‹³SžË‚¢`iiɹsç¹ï>Ùþêk=i~8-¿¾²—jU«pòÄIÍãžo¿ÍÉ“'7.ˆÓ§O³}ÛvÆŒÇÛo÷ Œ¡!©©©Ì;S§Nξ}ûØ·wþšõ›’’BB|)·135åÇU?2jÔhP”B—¹à’×ßò~^ÊËU´H«#2®n®Œ3ŽQ£ÇbccMê5X»æGÚ´n¥©3mêd ðî{DÇÄàêâšWbgg @¯ž=Ä»`llœg?S§LÂÀÀ€AïHtL *”çä± m;_JÞïÆŸÀ¸ñ4?þèC¦MÂ7 0û‹/ˆŠºNêÕùmÓ&LMMXóã þ)ÕkÔÂÉɉ{÷R±´°xؾò°»¬gNŸÎðÏF°té2ªW¯Î¶­›Y÷óZFŽE›víHI¹C9__vïÞ‰‰±1ãƒÆ¡¯¯ÏGÂ7(_®üCîwFʳÄÄÜ`ùò¸víúTªT‘õë~ÆÁÁÈŽ6üSÂÂà `Ó¯¿`ee…¢(Œü|jµš~ýp?-Í›3sÆ´—eàÀwôþ`Öüô5kÖÐÄd``P¬õôä»ò¼úÊ®EóæÌœ=›©S&£§§‡¿¿›6þ¸qãùö»ï±°° [×®L˜„¢(ܺu‹#Gþeá7‹HJJÂÉÑ‘ÖmZ4EQpsueô˜±Œø|$¦¦¦øúø0zÔH½7EQغu;-Z4ÏsjÚ¤ Ÿ~ö9‘QQ¸»¹àÏ‚¯ç3räh“’h×®-#?ÿ<Ïy>š6wÎ ÿô3êÕo@™2†Ìœ1ƒúõë¡( _åË–2{öŒø|$FFÆÔ¨Qn''sèÐa®_¿N•Ê•óݶnÝʦÿýÆÝ»wð.ëÍÌ3hÓº5Š¢ŘqãÀÁÞžfÍšj. Üo·mßNùòå([¶l®åTPÈ>òillÄŠ–1jÔ/Yб±1U«TÁÊҒظ8>:Œ°°ð‡h7äÝw²>&üê«u7~Ÿ އ‡ŸÆ{G8žTÐ6000à§Õ«ùèã!,Zô-5ªWgéâï5í èßË—¯ðfËV”)S†Ñ£FѸQ#E)°Ý¨¨(s†Ê•*¡Réä×=µäädâðñö.éP„/ˆJ¥bÇŽí2"ó"ÉùÜÿ¦jo8¦M.b/¬Ÿ*•+©Þ……škŸ„/m¿Þ%‘y‚]Ö}GôôdTF!„x*• µZÖúDæ ¶¶¶œ;B@@ z*=@¯¤CB!t‚ZQsþÜyl%‘yq*UªÄÙ³Áœ:y’„„„’G!„ÐYvvYwÈ®T±¢ÖúD&•+W*é„BQ/×G&„BñŸ"‰ŒB!t–$2B!„ÐY’È!„BgI"#„B%‰ŒB!t–$2B!„ÐY’È!„BgI"#„B%‰ŒB!t–$2B!„ÐY’È!„BgI"#„B%‰ŒB!t–$2B!„ÐYÚl<8$„ÄÄDâãâ´ÙxFöØÙÚXÒ¡!„Å¢µD&8$„ÌÌLÞhÜL[]ˆçèøÑ#„„œ£bEIf„Bè­ZJLL¤zÍÚ€"EJZµ¹•x+¿Í)„B”JZ‘‰‹Ë:F !§…Bè¹ØW!„:K«û*2$#„B-’!„Bè,­ŽÈÈ€ŒB!´I»‰Œd2B!„Т—òÔRFF%FæÌK“&MK: !„¢T+U‰Lù ~,]¶L«}dffbnaIppH¾uöîÛ‡aN:kÚ¦ÿý•¾ÑÑÑÚ “×ê¿ÆÛ=ßÖjB!„®Óò52Å=µ¤<¼?›öNI)j5éééûÊ»ŸëQQ¨ÕjFͶ­[4Ïgff2zôhnÞ¼‰«‹‹Öâ¬S»uj×ÒêºB!t]©‘Éî÷?þ °R%lìì165£IÓf„‡‡Nº¯bïè„…•5¯Ö«ÏÎ]»4óO‡Ž4wïÙƒ}Žö«×¨‰ÊÀF›äêûÆ›Tå9¡Ç5Ïÿ¼nŠ¢àééÉÍ›7 §¬/&fæ”õñåë 5õ§L†O¹r˜š[àåíÃÇC†œœ¬™Þ®}\Ý=013'°R%¾úzjµ€ù_}Mƒ×jê¶nÓ7OŒMÍpqsgþW_k¦eff2uÚtœ]Ý4qŒ;¶x+\!„ÐAZMdŠ{£üìó…†^ÃÄØ„³gNsòÄ ôT*>üx p+)‰£ÇŽñןròÄ :tè@ë6m9yút¡í>z|èÐAîÞIaûöm¹êÇÆÅâãëËG}È´é3Pµ¢0kÖlÆŒ£ƒ7oÞDœœøí·ÿƔɓ6|8—._AŽŸ8AÓ¦M¹pþkÖ¬fÿþ xw ¦Ÿ¿ÿù‡©S¦páü9&OšÄä)SXôíwyÆ|ààAfLŸÆ•Ë—˜?o.ŸÁÑãÇQ€3g±xÉÖ¬þ‘Ë—.Ò®][þ=zì©×¿B¡+^ÀˆLq¥›ššâææŠ¿>>”ýû÷ç¨[¡B9|}½1âSZ´hÁâÅ‹ól'÷sP¦Œ!ÆÆF”)c˜«nBB¶¶6 ùø#þúë/Î;ÇöíÛHNIæí·{`ckKll, `llD•Ê•°··£W¯ž¸»»|VÓ–££#îÔ¯÷*K–.fãÆÜ¼yC‡§§;žžtêÔ‘1£G±èÛEùÆìêê‚››+]»v¡fìÿç@aá7ß0qÂx7n„››+.ÎÎO¹î…BÝRjO-=ÉÑɉääd233óœ^±b ¡¡×ÐÓÓCy†kK±±±ÁÞÞžþýû1oþ|æÌÏÐO>ÁÀÀ[[’nßàÔéÓ4iÚ[;œœ]‰{x N1f}³tèµk….Ca¹•˜Hjj*±±±<Å’ !„º­tÝOyâßl¿«ô²r.E­ä5XAXXn®® €µ57bnänO}•>¤ÝOË7¾Û·“173>2„ÊUªbddĦ_ÌL͸tøî{4hЀ-üŽJ¥¢Rå*ù ¨–u›‹[¾Ó-C®uòD]•J…¢V051ÅÚÚš¨ˆ(¨]'ïõ)„BüG½€â=M6“ב<÷‘ÿ×_¥nݺ8p€ß~ûm[6 ¼ÆˆÏG²víZ^ýõl•VÐÓƒr¾¾lذ7WWnܼAÍ5rDp'%333@ÁÇ»,ïz777,,Ì33SnÝ”‡#D ÷ïߣL™2èééåˆñð‘Ü >‹ÆJÃ×_ÇÓÓ]3}ûŽ”õ.Ktt4³gAŸ>½óYþ¼gõÓ¹S'¦LŠŸ,,,8vüxëQ!„øïÑr"£]ß}¿˜ƒÞÇÕÅ…o~MƯP³F ¾üb6cƒ‚ˆ‰¹µµ¯¾ZW3ßÜ9_òÑÇCøîûÅÔªY“}{wçh÷îÝ»˜™™jÏùò‹ÓÍÌ̸ À×_ÏgÈ¡|ûÝ÷¨T*\œqppÐÔ½sƒÖmÚ’’r‡† _ç‡'ÿ~X±cc#ºuíÊèQ#‹½¾˜=“¡Ã>¥q“f`cm#§š„B¼ôãbŠü¶}÷ž½tèØ‘¢ÜÛdÓ¦MtìÔmŒ œ:}š5ëvï¥7ëЩ •*VdÊä‰yN·µwbúµ4iÒø™ú¹~=EQ°°0':&†÷Þ{ŸN:2ô“!ÅhE¿þB‡ž)!„â©éé±iãFš㸨ý,@g7ò¸¾¤TÊ뚟¼ê<ã2üüó:¾˜3‡Û·“qvr¢[·®|4xpñÚÕ{¶„Bˆ’ ÕDF[9FÕªUÉH¿¯Õ>ž‡¿nò1!îfÓ‹jøða >,×ó¥yÝ!„ÏC)¼ØW!„¢htæ>2B!„OÒþ}dd@F!„Z"§–„B¡³´–ȨTrÖJ§(àèèȦM›J:!„ÿAÚº½GéúŠQ‚ê×M>†-„â¹Û´q£ÖÚÖZ"£V«³Š’ÌèE¶—BˆçKO»ïåüB!t–$2B!„ÐY’È!„BgI"#„B%‰ŒB!t–$2B!„ÐY’È!„BgI"#„B%‰ŒB!t––¿4R!„Ð-Á!!$%%[Ò¡<5GGlml ,éP´®Ô$2/rÇ)ê.1 !„Оà233©\µzI‡’ÃÞ]ÛéÔµG±æ9~ì!!ç¨Xñ¿}\)‰LpH4hØø…õyâØ¿nàÒ“BíJJJ¢a£¦%Fž¥x_†W½Fmþùk–¢)=JÅ52I‰‰T«^ EQ^Xy¥F-oéTLB!´«TŸNR”b—¸Ø¸’ŽZëJňL\\Š¢.‘~ šVÚbBñò*îˆÌË¢T$2ÅØH™™™„^»Frr2–x{{c`PŒÅ)ê׊sǹ|å ŠZžJEùråŠ5¯¶¿ê\!„îÊ+‘ôþû|ÿÝw¹gxIŽ)¥'‘yxz¥0÷ïßgíÏ?óûïp÷î]Í󦦦´jõ½zöÄÄĤÐvŠ´y‹Ó­[·P«³Fn.]¾Ìĉ“4Ó&Nœ@…òåP©TØÚÚ>{LB!žJxDøúø”t(OåÉãÑðáÃyðàÆ cîܹ9¦¿,Ç”R“È(EHn߾͘±ã¸råJ®i©©©lØð ÇŽcÆŒéØØØ¼˜† Nbb’æ±±±±æ÷™3gi~·±±fÅ?üõë×cìèQ/¼ïç!ûñhò”)ܾ}[óøöíÛLš<™ñAA%Z‰*ûv!¬Z­fÒä)¹’˜ì‰Àµ°0&NœDfff¡m>kLŠ¢°tÉÖþ´¦Ð²tÉ’"µ'„xz~•X¶|…VûÈÌÌÄÒÆžsùÖ9{6˜-[cëàŒ½“ õ4äìÙ`­ÆU’Öü´/ï¢JŸ:}½úôÓn@Ï™Z­föìÙx{{cffFݺu9räˆfzZZzzz9ÊÏ?ÿ¬™¾bÅ <<<033£o߾ܿÿ)#Q…eË—žkjDx8‹—,ÖÔË*ÿ}¥æíDaò½{÷’ã9•JŦ¿Ò¡c§;Æ…‹Ù¹s-Z4×jL{öìeî¼y…¶5|Ø0š5+éB¢(¤§§X§cç®Ô©S›ýí###ƒã'Nàîîþ‚",ÝÒÓÒuîM[rr2Geùò帻»3f̺téBDD5¢§§GJJ úúú”)S€àà`Èš5k¨R¥ :t`òäÉLŸ>½Øq<úÊ€þýп_¡õ^–“K¥hDF]`Ù½gw¾óåe÷ž=…¶ù¬1)ŠåaÆëï™5jÔÀÜÜ??¿¬¶PŠÔžâù˜4y*å*`icµ#ƒ>̽{÷€¬k%êÖo€£‹V¶ÔkÐ]»ÿ?a;wÕ<Þ³w/vŽÎ9Ú¯Q».F¦4nÖ"ÇówïÞåZX}ûô"00€*U*Ó¿__ll¬ˆŽ‰¡k÷·±st¦‚EVý¸Z3oFF3fÍ& bÌ,mð,ëKŸ~4Ó ŒL9}úŒ¦þëšðÕ‚…šÇ+WýH@Å*Ø:8Ó¡Sn>ü(qXx8µ_­³›&æVVªÊŸý•£ßI“§âêî…¹•-mÚu²FF‹‡—®î^Œø|”æ´ZAڴ뀇—¦Ö¸y”Í#ÀïlÆÀÈ#Sþúûïcº~ÆÍZàìæ™¥ 'MaÀ»ïѳw_M{II·)cbιsç ǧœf–6ø”ócÁ7‹ ·0ÖÖÖlذFQ¾|yLdd$©©©$&&bee…™™ÆÆÆ£Re^—.]J‹-èÚµ+þþþ±xñb͵•Åñ4·õ(Ì£mefiƒ_@%f}ñ%o4i†…µže}Ùð˯š¾?9Z³Z¶j[ìøµ¥%2o¤ÐÐkšº+V¤~ýzÔ¯_EQxõÕºÔ¯_Ê•+kꄆ†²Ÿ=&åÑgõ~}ûP£zu ™äü… L›>“E‹¾ãË/fqáÜY†Ê‹Ïî={2t8óçÍ!øÌIîßOcø§#¸t›'N²ÿï?9r†¶mZÓýí^š‘ìé3f±æ§µü´fÎeÚ”ÉL6ƒ-[·±cûvnßÊ/¿ndù+ åà¡CLŸ6…ËÏ1oî—Œø|ÇŸÐLoÝê-îܾÅÛ·hðÚkÆžŸÀßÿß{wsåÒy H»¶mØ·ïOÍzï¾}øx{€³“¿b÷äi IDATmú•ðÐ+Lž4៎àr×U>‹;vP¿~}LMMˆ%33“±cDzzõjîܹ£©{þüyjÕª¥y\£F ˆ}ŠûÕh#‘9xè³fNçêå ôíÛ›±ãÆóNÿ~Ÿ9Éûï¿ÇÀAššÊÞ}ûX¾b{wí$:2œ¹sf;~m)E‰LÁ#û©£–o¶`À€þôï×EQèÓ»ô§u«·4uîß¿ÿ̸H;ÊÃE!Ûïÿåqr’õ»\##Ä‹dgk‡‹³3¯¾Z—Ý»±ÿÓ+”/¯#>N‹æÍX¼ti‘Û.S¦ ÆÆÆšSÙýoãzõz›aÃ?÷¼?ß|ûŠ¢pòä)<Äüyspuq¡MëVÔ©S›­Û¶ðÝâÅL™<‘VoµÄÃÝbœŽZ°p½zö Eóf¸º¸ðùˆálÞ²5G²^^”õòb|ÐXâã¸pá"ß/^ÂÄ A¼Ñ°!înnT©RµZÍ7ß~Gи1P©REúöéÍæ-[Š«« n®®tíÒ™5ªóÏÇë^¥Rå¹(Jìå|}qqvÆÅÙ™æÍšrçî]þý÷(lÞBûvY#ÆÆÆT®\ {{;zõ|w77‚ƒs^–ð,Ö­[ÇÒ¥KYšm_)[¶,óæÍÃÀÀ€™3gRµjUˆÇÜÜ\S×ÒÒxº{†?‘)Z»NNŽ8;91|è'üŸ½ûޝéü8þ¹÷fïH"‘ABDì­D“ $v­ªÝR”Ö¯U•_•¢ãW5ŠêRZ£5jÕbÖ„ÒÄY„Èžwüþˆ\"›9Iž÷ëu^œõœïsrï¹ßóœçœCíZµ˜6åRRRø÷ß0,Ì-HKK',< s5lXêøËŠdúÈÀ“Ö‚T«V˜˜¾ü*ç3¹\Îöm[xëíÉù:OU«V­ègÀÈJò.:¦ÜErþ}j¹‚þ¯¡ø²J“ Ï£º}uNœsssâÅ“––ƽû÷q­W/Ï2©©©$%%1jôXÞ3N»í¦M›”bOä°¯^Gñ _šØŒyµ‡7›·l¥ysOvìÜÉÎ?·pñâ%f¼ÿ.„\@__Ÿää”bû4•Ôºuë˜:u*»wï¦áS?äÎÎÎŒ3€Ù³gÓ¼ys–-[F`` ¶¶¶yZhrëdggWúJý™,ÝòFFF˜˜˜hmbbb‚IIItî܉ïV~ˬ>æ½÷ÿÃ'ÿÃÐׇ”2ž²!™D¦¸ ÏfÚDæÙõ ^¾YÑ¢R´È³PÎ?h´ŸÍSÉ‹æÉÄ—%‚îÉåò"¿ƒ·#"prrÀÊÚŠ˜ØØ—S(èéé‘™™Yì6e2ýüÙ³wýµ—E æ#—ˉŒ¸¥í3ó4++KîܽKëÖ­òÍS(˜››‹'ÍòͯåìLçŽùêË’5ùËå9Éš‰‰ VV–„…‡çÙ®¹¹9VV–üøýjú¼Ö»De¾­'ûÞÐÈ0ß¾+mì#G gÊÔwiß¾¶6¶´iÓ€±&Ò¹cGvnߊ\.×ÙË=Êäɓٻw/mÚ´)t9}}}ÜÝÝ‰ŽŽ Q£Fœ={V;ÿüùóØØØP½zõRÇ»¿ÿáGΜ9Sà2mÛ¶åÍQû=ÇɱL&{ò»õÌøðaC6ôuÖoØÈ¨ÑcñôlVhâý2IæÒRq}HúôyMÛüÉ*vÿµ'Ï5hÈùÒø÷íS¢¾-/SÞKKOnu{r°|’¼”äÒ’HdáåÚ¼e+wîðÛú ü¹}#† sÇŽœ;wžõ6Mtô““(™L†k½œN‘QQyú~@Î3­}ñ%.„p;"‚t÷ÆhÚ´ žžÍ7a"W®\%*::O˜ÆñÉÜyœ>}†°ðp‚ä}á_§Žøê‹ùçÚ5¢¢£µM!§ŸÞ?ý¬ëß°°÷ =jÿ˃‰ŒŠâÌ™œÞQ#G2ûã@Ž=FtL !!yøðÅÞWßÕ•³çÎqùr(ÿ\»FtLÌsÅÞÓÏFÃÔiÓ3æMm‹UîïAFF&J¥R;ÀÒÒ’+W®>×íÏÓ¦Mã£>¢Y³fddd‘‘¡ÝÖž={¸pá·nÝâ×_eÇŽøøät;v,{öìá?þàúõë|úé§Œ?^Û¸4r+ÆŒ~ggç|óõÆÈ2é®ÅåË¡¤¥¥ÑÄÝ…BAbBbñ+¾’IdŠ»›Ç±†cFzf +V®Ò>Ù0×ÈÃqvv*¶Ì)gÈù $cÙ’ÅtéÜ€-¼øâó…Ìþ8únùϳh×¶­v½¯¾üœ?·ï ‘»ïý'ïÃÕâ=âäÉSôz­/Ü=;n"}û¼Æ§sÿ‹L&ãÏ­›ÑÓÓãUßž4r÷àiïj連?o.;wÄ¿ß:vîÊù !yÊ^ºd1r¹œv:ãÞÔ“´Ô4길ðZï^,Yü?æ/XH#w^éÖƒ•h?Ìûô À„‰“hÐÐqßB£Ñ0Þ\zõôcôØñÔwkÌÀÁ¯sãÆçÝÝôíóÝ^y…ίtÇ»‡/§OŸy®Øõôô˜0aãã=j”vúÒÅ_sèðakS½FMT*vv¶¼=q§OŸá—µ¿–*æ””Ο?ÏÌ™3166Ö?=~ÈéöíÛyõÕWiܸ1 ,`ñâÅôë×€&Mš°zõj¦M›†——-[¶dΜ9¥Ú~®§”Y~€¹…¹çÀæÌúðƒ2;9¾|9¿^¯QÍΞ½û0ãÝi´m[xËÔË${Sâš8H@¿~%Ú9[·n-Õ²>¾¾%ºmÏÞ}üøãÏd=“¼@N“Þ#‡çéô[¹\ÎÞ={x¡˜ fɲåÅnoÊäItëÖõ…bA7.^¼D‹ÖmÉHM’ôÓy7oÙÊÌÿ|ȰkåJ•±uëV ZÞaäóÇÆßxÕÇ7ßô)S§°ä›ÅùæÉåröüµ»üSd2¶nÙR²8/ëݽ[‰‹—Ì7¸¤Í`>¯ö …—ûƒ‚¸zõÉ)ɘ™™Ñ¨¡¯öðÆÎήD唿®¥¢88ØóJ×.Å–åà`_lYâ®%A¡PüF,Yüu¡óªJwI$2666¨Õjd2Y‰Zellª1dð ç•$Ëå¨Õjlll^8¦† ÝhØÐ­Øm[IbA7š5ó@™™Vü‚嬿ú÷-´BŽÒžìV•“cIô‘©V­W¯^Ñ&2™¼ ‡œÄäê•«T+"ibL‚ BÙ²{Ž»‰^–ÒV–z‚P$É$2Uå-‚ e2ž¼˜®"«,õ¡x’IdrNŸA(*Ç÷±²ÔC„¢H&‘'N‚ Måø>V–zT5YYY”wB"¡DFq¡üÉÐP.ÉT–zT-ž-ˆå~lÔKÙÞ‚EŸó¿ÿ-æ—5?áçëóR¶)èž„ž#£C1ƒ}š™Zððáƒ2ßÖëÆãìRÔÔÔB—‰ŠŠâÍÑcq©×›ê´hÕ–o–,-÷ý$†—7¼Ö7#S‹¿^¯•{|UuøÏ‡³02µ ß€Ay¦gffP·~CŒL-8vìX¹ÇùìpãæMTjU‘ËÌûlF¦4häŽR™]¢r÷áÕ² ÿý$Ïô«W¯’œ’Bxxx¹×½bÒ Zd* —щñÔé3ÄÅš֨ ÝÖà¡Ã9{öµj9ãÑÔƒ7o#þ–š¦TŸ//¯æÚÿï:€w÷nÈd2š5ó(ÇÏBéêQÙDDÜ`÷_{ ¹H³füüËZ¢££¸s÷®d÷Oaq¥§g°rÕjîܹ˖­Û8 ±åݽÉÕþ¡GîyÊ^±|oMO›Ö­%»/„âI¨EF(”ÔTüzõ¡^ƒFXÙØãàX‹w¦¾KjjgÏž£S×î8:×ÁÚÖ–­Û±úûµ_Ö7ÇŒÇØÌŠÝíàõa#16³âà¡à<Û±³wÂØÌŠµë~Ë3=--³gÏpúïc Úíðk|üÑ,í2/^Âǯ7vöNxx¶äç5kµó”J%Ÿñ®n1·²ÅÙÅ•î=|‰ˆ¸Ã›716³¢}§®\¹úÆfVtóö-QÙÓÞ}ÆM=±µwÄÚÖ×úöãöííüĤ$&O™Fš.T³sÀ·çkdgg3ï³…¸5nJš. :œèè˜çúûTÿ3›íÛ6³}Ûfí´­›7±}Ûf®>þ»mÞ²€›·nalfEóm€œ¿S£&Ͱ¶u vÝ :"Ïß)..ŽÑc'àT«.õ4â?~DFFÆË­`õ8Yøò‹P«Õ,þf©vzlì=Ž;N£&Ͱ°¶ÃÖÞ‘ÁC‡ )n>À›·Ð²u;ììèÒ­'OÒλþï¿øõꃽîM›£V«‹¬×ÆM¿óàÁFŽÀ²oW景V«ùfÉ2ê7tDzZu<<[rçÎ]íüo–,ÃØÌ ·ÆM˜ýq ݼ}Ù¸éwàÉ1 qSOœjãßo aááÚõ‹;¶åC2‰LîÙ“ŠžÚWéé>BBB¾>¯bnaÎ÷?üÄ׋¿A£Ñpçî]Ξ=‡™™)ÞÝ»q72Š)Ó¦³båw2š<ûþÙ¿C®áÃ^gÔȸºÖË‹±±n êлO?~ýmé阘£ÑhˆŽŽÁ§çkœ>s†ž=}IÏHç­Iïtà †ÙsþKà'Ÿ¢T*éÕÓFÉ¿OòàÁƒ|ñ<鱩)QÙGŽãÖ­ÛtèÐ7·8Èø‰o£ÑhP«Õô0˜~ü;[[º½ò uêÔA£ÑøßO™ÿÙB¬­­iß®-Û·ïdøÈQåÿwéÃó}Ÿý ø÷`×î¿Ðh4ìÝ»ÿ¾¯iÿN·oGйsGjÖtâÏí;xÕ¯)©©¨T* zõ6ÒªU X²t9 ~^æõ¨ CÜý8Ú´nŶ?·sçÎ]¶ïØÉ­[·i×6'‘Œ‹Ëù®™™šâX£úPËÙ™íÛwòqà'h4ÅGŠ›¿gÏ>F¼1šG ôìéKhh(}Gff&þ >|¦Ôªåœó9*âïöÝ÷? P(øïœéо§OŸáüù Úùs?ϳf“‘‘IïîØØT£F íñÎݽ1£FŽ``ÿþ9ë<žžûYù8ðæ¶•JEsÏfìÝ·ŸW}{‘ ýÌvl©ŠƒTHæÒR¶RUÞ!TOï+{{{~Yó§Ïœ¡Ç«~>ÂÌ™ï£RåœÙ¼Òµ+K—,æüù ¼Ò½ß®\ÅØ±cP«s>„J•šl¥ õã¥êñxnžóù¢…˜ššæÛ.Ào¿®cÆû3 >ÌØñçxoæ|ùÅ"èÏŠU«ILLäý÷fðÎä·¹p!„¾ýÙ°ñwÚ·oÏÊUßa``@ðÁ éãßÇ T©Q*sb×h4d+Ud«TÇsb(ªìÎ;kcÿæëÿaggGí:®?ñ7陜;žã'þ¦q£F9|}}}’’SX¾b%ffflÚ¸c##F¼ñ&‡áæ­Û8;;—Á_Rzär J•ê…¾ÙJdôì釉‰ ;wýEJj:Ûwî  _@žÏØ7_ÿGGG ~½ûöóçöØÙÚrúÌY:vìÀêU+ÉÌÌ«ek6lúƒžjõ+ËzTd>DOO)SÞaØð‘¬üî{Μ9ƒ……Æãï“§ˆ{ð€l¥ ÷&Møk÷N”J%ããiàÖ˜óçCÈVªŠ=Ž7Ñ_°lÉ7´lÙ‚ºuë±pÑçìúk/NŽŽÜŽˆ CûvìÞµ€j¶öh(ø÷ $ä".„ÐÃÛ[[ÄñóÝ?²dñרT*¾Y²  ¢fÍš@ÎÇLù8În¯teÞ§sáñ6rƒ*µšŒÌ,V­Z‘¡!Áƒ°±±á­·'óÛú lÜ´™7ß|£Èc‹žžd~N«É´È/ÆÉÑ€G œïåÕCCnߎ@£Ñ “É€ë›äêZ?·n&äüYÞ™ü6‰‰‰¼õöd¢¢¢´×á¿øò+j¹Ô£o@Îu컑‘ÄÆÆ’™™EÍšN8>ŽûiÅÅVTÙÏÒ××ÇÎεZMRR7oÞ eËÚ$r.cdee‘’’‚[Cwj¹Ôãðá#Ë}9wPT6fffôï@rr26näøñxz6£a!¯ h×®-·nÝÒ^9vì8µ\êQß­ÉÉÉDE‰¿Eq”J%)))XYYÒÓÏ—Ú¬þþŽ?ÁˆáÃptÊùÎ%$ä+öîÛO›¶°³w¤ÑãK.éé–ýìq¤¸ù¹Ç~QË%'‰ˆŒŒ$:&ç²­««k‰êµvݯ$&&2mú Ž9 À–-ÛHOO'&&†ŒÌLœœµILiÄÆÆ’‘™‰£“#666xxäìÛ·ó-ÿì±E(?’I!¥ÔL%uÏ÷:·Ùï©y†ÈÈH23³´‰ƒ••%ws;ü=.CóøòMn+LTt4õ 9ÐÄÄÄP£F \\jóéÜOØý×nܸIXX8ÎÎ9’úóÆÈÚu¬¬¬°±µE&“{””í¶rãµ´´È)?:•J•ïÒRQeT÷ÜÄH­ÖàX£/]B¥R!—çäòèéé¡§PðóO?`ff¦-×½q£*óùÔE³ñÓë¿5qk×ýʇ³f“͘Ñojç=ûwÊí‹ààà€ó㢆 òåç µeËd²Å&Åæï—%!!sssd2cÇŒföÇÈårÆCzzN?º„ÄD4 o½=‰””~üa5¶66ôîãP¢ãHqókr÷î]æ~ˆWó'Ãk×®¥í»rîüy”J% …B;ÿÙ¿[VVlÞÀé3g8}æŒv^rr2nßA€_ôôôˆŽŽæÞýûT·³Ó.“{Œ‰Ž.øfFƒ½½=F††DEEqÿ~vv¶„„\|\çb-Uñ³&’Idòý: …ËŸÉäù±z|×îݤ¦¦ræìYè ]:wfÕwßóé¼Ï8~ü§ÏœÕ®ŽFƒ§g3ÂÂÃ1r ê×§gO?† ¨ÝdVVmÛwÂÚÚ '''îß»Ï7155Žqc길ðÍ’elÞ²•»wïbemMTT+—/ÃÈÀ€ž~¾ìÚýݺ¿JÆnœÉÝ>¬­¬hÚ´ —/‡âãÛ ‹>XYYÒ¨QCþùçï½ÿêԩãGñ 4íÛ½œzTPÉ[ŒA£aİ¡¬]û+^^Íq©]‹[·nä´"h4(ä ²³•ƾzõ'•à8RÜü1oŽâĉ¿Y°ðsÚ·k‹B¡ öÞ=Ž:@ š4q'4ô :u¡AýúO:û>ówÛ»w têØ‘nÑNÿáÇŸ™ñþL6nÜÄà´ßá:Ó¢…<`é7‹iæÑ™LÆö;>â 233ù}ãú'ÛÑ€B.gìØ1,[þ-]»yS»v-Nœø[[úù÷-öØR?kR!™KKe}·{ežÝW%×Sèqàà!2Ò3=züg&À×ׇ9„‘¡!;wíF¥TÒ²e ìís®QΙMçN¸yó‡Íé„ûTÙÉ)©øú¼ŠJ¥âÌ™³<ŒHçNøcÓllmp®åÌî]Ûiß®!/qèÐ!T*Y™h€Å_ÿ¾}^#öÞ=ŽŸ8¡}’gnù?|ÿíÛ·ãÒåËœŸ¹CÇŽ1|(†FFù–%øðš6m¦ ¿ack£ý; 4€Ø{÷Ø·?Ñ11ÈŠ2¯GERRS016FXXZròïc|»|)ÀØÄÈiÉÐ+W~KÓ&Mذq_}½Z·j•g?v)n~¿~¬^µ‚Zµjqäè1Nü}RÓÒPèéñǦ ПGØý×ìììèÒ¹s¾:åÞUàß7Ïô×úôF¡PpøÈQîÝ¿Ï‚Ïæóî´©Èär<Äý¸8’SR¨W¯‹~†•¥%{÷íçöí’×?—˜óñG¼7c: …œÐÐ+¼Òµ ;·oÃÊÚºÀÏø³ãUm Ù£¸˜Çtà ýž>£*ÜÖ­[K¼lPP­Û´)iB lß¾ƒQ£Ç2|øP–,þº¼Ã)V@¿>r”A{iîéYÞáTYr¹‚ëׯQ¿~ý.kÑç_ðϵë@__ŸÓ§N`gk«ß¾cg®]»Nè¥ ö•zº¬GUVÜq¤¢gÝ:}êÞÞÞÅ/(“±uËJ¼¬w÷n%ŽC\Zªì4T¬}[Ñâ­l4ý Nž<Åß'OR·n]¾ü|v66—[sÖC ø})öµPŽ$“Ȉï€n½Ö»7ïç<Ôª"ìÛ-ü®ýEˆ·²Òe×’§ÿ¦¹e?íø‘Ã…Î{QU¸‹ŒNw©hÇ¡r’L"“••UÞ!B•§P(ÈÎήðßÇÊRAŠ'™DæØ±£å‚ ÅÆF¿PPYê!Rdnn^Þ!Jdüûõ+ñ²Û¶laÀà¡e T]'¡m‡ÎåÆ «,õ):yü‰‰‰å ¡D†b^&‚ ‚ð,É>>|øá‡ôíÛ7_2#UMhh(/^ÔIII\»vM;ž˜˜HJJŠvüÊ•+(•JíøåË—Ë-ö\\\ =ñèÛ·/ÿýïµãŸ}öNNN3wî\ÒÓÓuÓ³Û,Š««+§N*t¾®c„ò&™DF£Ñä”J%“§L£uÛùæ=I“&±`Á&MšDhh(¾¾¾:‰;)) ,,,P«Õøûû“™™©“òA([¶lÁÓÓjÔ¨ŸŸ_‰~ä D·nÝ8}ú4óæÍcýúõDFFòöÛoóé§Ÿ2thé^¡Ò©S' zzz899Ñ¿.^¼Xà6_ÄóÄ&R'™>2O;|ä(~BXX8µœ_üzûÎ;Ù½{7¡¡¡˜ššæ™wûömºté½{÷°··gÆŒL™2¥ÄeËårüüü˜0a5jÔ`ûöí|þùçÈd²Ž[*ªŸþ9Oˤ——K–,!)) €V­Z¡¯¯ÏÖ­[066ÆÂÂB;®¯¯_¦ñݸqƒÁƒóÙgŸ@||<¡¡¡»î°aôÿ¿pátîüäN™™™…žp¥¦¦²jÕ*¦OŸžgzLL _|ñÇ'..Ž~ønݺqåÊòlóE› TT’l‘¹qãcFbÞÜÀœ^1/Ø"óã?¢R©¨[·.¶¶¶Œ1‚„„رc‘‘‘Ì›7iÓ¦Vâ²ÍÌÌ DO/''466&Ý­ç’ IDAT00ƒRÇ)•…¯¯/þþþ4jÔ¢££iß¾=7ÆÅÅ…˜˜âããñóóÃÅÅ{{{®^½Š««+þþþôêÕ«Lã»~ý:jµšwÞyWWWZ·nÍèÑ£ó,Ó¸qc qssãСCÚéþþþÌž=ȹLsêÔ)d22™ŒuëÖðçŸj§k×MNNfΜ9ÆdaaAõêÕqwwçË/¿$--£GæÛ&À¶mÛhРØÛÛãíí§¬ñãÇcllŒƒƒ‹/Î3¯°Ø¡¢’L"ó´7G½ÁÈáÃËuÞÅ‹9r$aaa\ºt‰ððpfΜ €‘‘Ú§fÍšåz}^*“ÁƒÓ¼ysš7oNxx8C‡ÕŽ5Š[·niÇ›7oN×®]_J\mÚ´ÁÖÖ–>}úpöìÙ—ùí·ß§ÿþ 4¨Ð¾oíÛ·'==ôôtíe›×^{M;íéÖš’ÈÌÌdÍš5ddd`nnžoþÑ£GYYYyÖOII)r\„ŠÅÆÆ†3gÎP§N¼½½iÑ¢AAAy–ñôôÄÙÙ™Ù³góàÁþùçŸË’Édadd¤=ù’Ëåy¦bkk‹»»;©©©ØÚÚbkkËš5k´å¼ûî»X[[cjjÊèÑ£ñõõ¥Gù¶·lÙ2ÌèÑ£©Y³&uêÔÉ·LÆ µ}mZ¶l©mÙ)(6A¨è$ó)~öò‘v ÿ´Òª]»6ׯ_׎§¥¥i¯…3†fÍšKTTT¾g`h4:tèÀÊ•+¸sç?~üj+Uƒ¹¹9–––XZZ¢P(òŒ›››£P(´ã¹ÃËR«V-V­ZELL }ûöÅÏÏsçÎå[ÎÄÄsssâããŸ{[Ó§O'$$„   LLL !$$„h—™3g¡¡¡Ìš5‹fÍš±k×®_ÉpëÖ-7n\âmÛÛÛ¿Pì‚ u’ìì«kÇgΜ9`kkËÒ¥K2d€ö±êäë¤+“Éøé§ŸèÑ£©©©L˜0)S¦Ð¡C‡—^A¨hr["""HNNféÒ¥yægffrìØ1ôõõqss+166fΜ9üòË/ìÛ·??¿|ËÈåòŸDå»k17IÓÓÓC&“øÐ@kkkœœœ˜3g;wîä믿fÆŒù–srrâîÝ»%¬]ÞØ ŠM*:É$2E$^´—ý¸qãˆŽŽ¦ÿþ$$$ðÆo0kÖ, §™vòäÉ|ûí·ÈårjÔ¨]žõ½¼¼Ø¿?ÞÞÞŒ;–E‹½P<‚PÕL˜0½{÷:ßÉɉÈÈÈ—OHHçΣ}ûöräÈîܹƒ»»û —]¿~}/^Ì¥K—Ð××ÇÒÒGGǯ¯§§Ç·ß~K·nÝèÓ§õë×Ï3àÀL˜0Þ½{Ó¼yó<‘Ë:6A"É$2eI¡P0wî\íêžÖ±cGBBBŠ-ÃËË‹˜˜ œokk+nk„ âÑ£GüüóÏ̘1ƒÌÌLêÔ©Ã×_MŸ>}Jt<(Š¿¿?[¶l¡cÇŽ˜˜˜°bÅ €œ»$KÒÇ®mÛ¶øùùñþûï³mÛ¶<ó†JDDãÇ'))‰zõê•ø’\Q± BE%D¦€$`äðaŒ>ì¹^ÄôÖ[o1yòd¾k©°$¦(¹ïZR«ÕåúÎ'A(OÆÆÆùžáô´¢æ•…W^y%Oاyzzæ;)É}\'±˜6mÓ¦M˳¬¡¡!7n,U<áááù¦mÞ¼¹Àm&%%1hÐ Þzë-’““Y°`ÎÎÎÆúìºÏ› Hd]¶e<{÷AyÊ}×’ TqqqÚg*=mÅŠÅ®›ošè¤šß¹sçxóÍ7‰‰‰ÁÊÊŠ®]»joF„ªH2‰ŒN_-B¹ðòò*ï*½W^y…Û·o—w‚ ’IdD#‚ BiI&‘™Œ T\…½)Z¡¬I'‘™Œ ‚ ¥$™DF¤1‚ ‚ ”–d•RUÞ!‚ ‚PÁH&‘רAA(-É$2Ç+Õò')£HA¨,߯ÊRA '™DÆ¿_¿/»mËÚvè\†ÑBÕuòø‘Jñýª,õ)’ÒI‚dÔêòŽ@A„ F^Þ‚ ‚ >>$$$hÇ;uêTÞU¡‚P"Sö6mÚD÷îݱ¶¶fçÎ¥^É’%ܺu+ßôÄÄD|||øðÃéÛ·o¾D@—*C„Ê/44”‹/j‡¤¤$®]»¦OLL$%%E;~åÊ”J¥vüòåËå]A*É$2FƒJ¥â«ÿ-Æ­qSªÙ9ЩkwNŸ>£ÿ¼þ&MšÄ‚ ˜4i¡¡¡øúúê$$|||°°°@­VãïïOff¦NÊZe¨ƒ H…••üñGžiëÖ­ÓiAÊždúÈäJNNæì¹ó¬Z±''GæÎeèˆ7»vå¹Ëܹs'»wï&44SSÓ<ónß¾M—.]¸wïööö̘1ƒ)S¦”¸l¹\ŽŸŸ&L Flß¾Ï?ÿ™LöÜñVÖ:UÇÏ?ÿœ§UÏËË‹%K–””@«V­Ð××gëÖ­caa¡×××ùA ‚P!I®EÆÂ‚_×þLçN©W·.ãÇ!22ŠÔÔ´çn‘ùñÇQ©TÔ­[[[[FŒABBìØ±ƒÈÈHæÍ›Ç´iÓ +qÙfff¢§—“ˆA©ã¬ìuª___üýýiÔ¨...DGGÓ¾}{7nŒ‹‹ 111ÄÇÇãç燋‹ ööö\½zWWWüýýéÕ«WyWˆˆZ·n fff´mÛ–}ûö•wX‚ jÔ(nݺ¥oÞ¼9]»v-ïµ=zÄ™3g8zô(/^¤_¿~ôêÕ‹òM„§H&‘QªÔù†¿oæ§5¿°tÉ7y¦—VVV®®®XXXàèèÈĉµgV!!!tëÖ +++ììì¸ÿ>YYYyÖOII)rüe¨ u„ЍAƒÔ«W™3gâëë˪U«Ê;$Až"™Dæéμ† 71}Æûü¾q=õ뻾PgßÚµksýúuíxZZÆÆ9-V=##ƒ|\e2?ýô=zô 55• &0eÊ:tèðÂqUµ:UÏÑ£Gœþ&ÉÉÉ,]º4ÏüÌÌLŽ;†¾¾>nnn/5¶:uêpéÒ%†ªvéÒ%êÔ©Sè:·o߯ÉÉée„'B I&‘yº¥åƒgñÞŒwqoâNúã;ôõôžû…qãÆ#::šþýû“Ào¼Á¬Y³X¶l“'OæÛo¿E.—S£F ìììò¬ïååÅþýûñööfìØ±,Z´è9kùü*C„ªk„ ìÝ»·ÐùNNNDFF¾Äˆ`ìØ±Ì˜1ƒzõêѦMNž<ÉêÕ«ó]:úã?h׮ǎcÛ¶mìÙ³ç¥Æ)BÑ$“ÈäJMMåâÅK\¼x‰9Ÿh§/ùækFŽþ\e* æÎËܹsóÍëØ±c‰:ïyyyƒ¡¡aómmmËô¥v•¡‚ %ãÇ';;›ùóçk/É.^¼˜#FäYnÅŠŒ;GGG¾ýö[IuHAJ‰ÌãPSÞ/r™’xë­·˜ my£¥£Nã©Z5°qž$ä³ [ŒhYg+#¼Mj–й¾õx˜šÍv†ñýàÆ¤f©½!TGÑ”þØRؤLm_„²$¡D¦ì½êfC{Ù›2{w8Gn>Â@OδNµèáfƒ‘ž‚m¡÷Yr4‚lUþƒŸ«­ M«çKdÌ |Û¿1Mk˜ÑÊÙ‚i^Ï“ÌBUSÏÖ“'O65PàR͘z6&˜(Ðh44°3à¾q 9Úñ”L¾D¶”yŒ› †7¡å7gP©ó¯¼¸O}®Å¥±òï(Æ´vdp³êXéñÙ¾;…¡žœÌ—p ¨mmÄœuhloJz¶š×~¼HÆãí» TF’IdrŸ#~ãŸÌσ‡PfgÓ¤I>H‡öí^¨ü»×¡™£9«OFr9&…‡i9×ñǶq¢e-KÆl¼‚J­aaï¼ÙÚ‰ïþŽ,Q¹¦9ILjVÎW.“ñu_7ÞÝv,•Hf¡üüƒÞݵãÛ:QÛÚˆ”L×ãR™¾#ì©å ¾0µïßxb“3 MÌׯ‘··\ãÆÃtd2ׯ‘Fö¦Lßþo‰#Üñf3~<ÃÖÐÒµ"¿ßµ÷’³˜¹3 =9éÙ*N¾ÓŠëC¹^Ž—Ôáe’L"“K£Ñ°wï>Zµj‰B¡`ÐúLxØ?˜™š>W™ëZÓ±®5~!=;orѶ¶%»¯ÆqçQ¿œ‰fJçZ%Nd48~ë\ºGÐÄ–LÝvQ­ÑHî*¢ ¼g¢¯ -[‡Axi3” û5”±mœX5 1w3øæÈNÝIÔ.s=. •ZÃê“QŒníDÝjÆ\»Ÿ?áÑ@¾ÎýMÞioµ¯É`O çXüvK¾ Ž`GwAæêÕÈ–‡©Ù¬9 Àw'#ÝÚ‘ú¶&ÚäK©Òh·•ûtå짦 Be'™Dæé³ âã¸wÿ>S¦LcïÞ}üôóÞš8á¹ÊŽIÊÄ¥Ú“$Ȩ; êÚÓ«±«Oæ½4Ô½¾ Ú×dSH,ãÛÕÄËÉ‚‰\}®x¡ªHÍRkï@R©5¤f©´ãiÙ*ÔMž;”’³”eEÁg±ÉÌ ºÁÁ·x£¥#Ëû5dÄú˨Õy;ûf(sN€,Œäm5yöÿÏnóÉ´µç¢ÙrùÖÆúü4¤ ƒ×^ )CYHy9ìÌ ¨emÌ©©­Ÿ”¬;Sý"×OЪÉ$2¹Ôj5r¹™L†ƒ½=­Zµdï¾ý«Î•ɶ :j¸U7¥±½)!QÉd«4xÕ4ÇÁÂÒ mç(jüééÒæUƒúv&d«4¤d©ˆKÉ›¸Ô~bn¤Gusí´Ô,{¯?drÇZŒoW“]W Òh°2Ö㟧úô<Ç„ z¸Ùr/% ®ÞK)bïBÅ'™D&—±±12™ŒýûƒËe4mÒ„÷ß›NÛ6­‹_¹j†oßåÛãwóÍ;q;KN—  ødï ô²–—®¤ùW?wŒ‚ ”…‚›d, ôq·cFô2¢“2ùâÐ-‚oÄãVÝ´u º¬”?•9O÷úÕøiH2”jæÝà`XÎS‚¤fÑnÉÉ#}·smÞí\[;þËÙhþwø6ã¿Â»k3²¥#j†àðx>Þ^h¿é$2‚ TZ!!!ìܹ“³gÏrïÞ=âãã±¶¶ÆÁÁ///z÷î——Wy‡)B$™DF#Z%•ή]»øàƒ -ü Ìþù'4jÔˆ… Ò§OŸ—¡ dA*¤¤$† ÆÎ;011ÁÛÛ›=zP³fMªU«F||>>\½z'''Ž;Æ_|Qh“ËÒÒ’… ò÷ßãìì̵k×ðññ!>>þ%E.BE%™DæñÛÝJ8‚ 5†Aƒñï¿ÿR«V-N:E‹-´ó³²²¸víZ‘exzzrêÔ)j×®Mxx8D-îh¡’IdtÙ"ããメ¡!FFF\ºt©ìƒ/BDDFFFÒ¶mÛ¯Wê T-ëׯgÿþý˜šš²}ûvœœœ´óîß¿O÷îÝ *¶œ5j°cÇÌÌÌ8xð k×®-˰A¨à$“Èè²EF¥R±|ùr222ðððÐY„×®]+ô š˜˜ÈÂ… ÉÈÈÈ3½víÚddd°fÍšR½†¡2ÔA¨:²³³ùøãøàƒhÖ¬™vÞÅ‹iժǎ•üéÝM›6eöìÙ|üñÇdffê6à2Á7Ê; A¨r$”È”½M›6ѽ{w¬­­µwSddd0uêT±²²búôédee¸~hh(?üðC¾é‰‰‰øøøðá‡Ò·oß|‰€¨ƒPÙsóæMlmmy÷ÝwµÓ7oÞL‡¸sç—.]bÛ¶mE‡`êÔ©888p÷îݵ䔷 &°~ýúò㹉ï¼PQI&‘Ñh4ù†””TÚ´ï„_¯>y¦?I“&±`Á&MšDhh(¾¾¾ÌŸ?ŸC‡qøða.\¸ÀñãÇY¸pa‰ËMJJÂÇÇ Ôj5þþþerYê TN;vì oß¾˜šš°råJHjjªv¹Õ«WPä0cÆ ŒŒŒÈS¾®;wŽ>}ú`ii‰©©)-Z´`Ó¦Me²­ÂtêÔ …BB¡ Føùù±ÿþ—C÷îÝY¹råKݦ èŠd™‚Œ?‘{÷î¿p9;wîd÷îÝ;vŒ~ýúáä䄞^ÎçAAA >œúõëS§NÞ{ï=Ö¬YSâ²år9~~~üòË/lß¾víÚ!ÓñÛv+C„ÊëäÉ“@Nß®\ƒÖ&Û¹\]]éÒ¥K‘ÃÓ„s×Ï-_—‚ƒƒiß¾=lÛ¶sçÎñþûïS­ÚË}ÉdLL _|ñ±±±Ó²eK|}}9{öìK‹Aœ´™d™g[c~Zó <`ê;“òÍ/­ü•JEݺu±µµeĈ$$$àîîÎöíÛ‰ŽŽ&33“ÔÔTîܹSâ;%ÌÌÌ Ô&ÆÆÆb``Pê8+{„Ê+66í´ÜËŸ}ô‘6)ž:u*ÁÁÁÚaÿþýyƃƒƒYµj•¶ŒÚµk9?öº6yòdüýýY·n¯¼ò 6dÈ!x{{ãêêÊŠ+´Ëþøã4hÐ¥RI`` öööÓ«W¯Ë?yò$íÚµÃÜÜœV­h9 IDATZqæÌ™Bc±°°ÀÎÎ777>ýôS,--µ}ІŠ““†††tëÖ €èèh €••õêÕËsâ²víZêÖ­‹...ÄÅÅ»À»ï¾‹L&Óþ­Îž=KË–-122¢zõêìÙ³§´»X^ É$2O»wï>s?ÏêïV P(^¸¼‹/2räH¸téáááÌœ9€ ààà@“&M¨^½:óçÏG.—#—Kk×T†:•׃°µµÍ3].—3oÞ<¶nݪ½tù´uëÖñÙgŸz‚bggÀÇuïíÛ·¹rå 'N,pþÀÙ½{·vüàÁƒÚ„eÞ¼y¬[·Ž 6ÆgŸ}–oýû÷ïãççG¿~ý¸qã>>> 0 Ð¾k¹ÒÒÒøþûïyôè‘¶“ÿîÝ»ùôÓO¹{÷.ßÿ½ö6w¹\ÎÕ«Wùúë¯7nÿüóéééŒ3†… òðáCþüóOìììŠ\'×_|Azz:éééL›6Î;éS§hÕªUév² ¼$’ù¥SªÔÚá£?nNN5Q?n…yz~ieeeáêꊅ…ŽŽŽLœ8‘}ûö9Ê?þøƒøøxñöö¦Q£FùÊxú:?@JJÊóUô9U†:•Wn’’””Tàü¾}ûrúôiêׯŸgºJ¥â£>¢_¿~$''ç[/11sssÆ{ïÞ=œ œ?xð`öíÛGBB†гgO §ïÏܹsyå•W¨Y³fž;´r­]»Þÿ}ªW¯Îœ9sˆŠŠ*ôQ “'OÆÄÄSSS.\ÈêÕ«µ­/µjÕ¢zõêÔ­[—óçÏsüøq–,Y‚££#}úôÑ> YOO®\¹‚žžž6¶¢ÖÉ¥§§‡‘‘FFF@ÎC ÃÂÂÈÌ̤N:ØØØ<Çž„²'™D&÷²Ñ¿ÿ†qàÀAÞ~k“ËI¼Ø¥¥ÚµksýúuíxZZÆÆÆù–»zõ*k×®åõ×_Ï3}óæÍ´k×N{Ö9wî\m'Ä—¥2ÔA¨¼ìííœË¹ââ∌ŒÔ¦¦¦¸»»ç™öèÑ#¶mÛFëÖ­ó=0/·<Æ›û£Uà|OOOêׯÏo¿ýÆéÓ§IMM¥K—.¤¥¥qïÞ=\]]‹,?::šÿýW›XXX “É ½DÈÕ«WIJJ"<<œ±cÇY6ä\ÆË-ÿäÉ“ÄÆÆ¢¯¯ÏÞ½{9zô(ŽŽŽ|ôÑG(•Ê"×)ÌêÕ«±¶¶ÆÕÕ•×_]›ü ‚ÔHè]K9víþ‹{÷ïãà˜÷L©Ë+Ý9|èÀs•9|øpæÌ™C@@¶¶¶,]º”!C†9g„‘‘‘3{öl<==™6mZžõصkÓ§OG£ÑNppðsÅò¼*C„Ê«iÓ¦\ºt‰àà`üüü€œÏÜñãÇK\Ƶk×hݺ5k×®¥oß¾ÚÏh“&Mt¯««+µk׿§Ÿ~¢K—..3sæL>ÿüszöìÉ!C000ÀÀÀ+++ÂÂÂhÓ¦M¡åתU .\¸P¢xìììòô/*J­ZµËåÄÄÄøê‡6mÚpðàAΞ=KïÞ½qvv¦]»vE®cdd”¯Ã¯££#¿üò ‹-bðàÁL:• 6”(F]ÊÎÎ&;;¥R‰™™™¸d.ä#™ODnkËÔ)“IˆÓ æÏ£K—Î zî™qãÆ1qâDú÷ïO³fÍèÖ­³fÍrîøiÒ¤ Ë–-cêÔ©8pCCÃ<ëËår¾ÿþ{† BµjÕ8tèP±gdºVê T^¹ýG¶mÛ¦ýŽæ^*)jxösjkk«íà›[@ïÞ½uó×_ͺuë?~|¸4lØ0Y–¥Ç\6lÐ;ï¼£¨¨(­ZµJŸþ¹:uꤎ;jΜ9—dÜ6›MK—.•···ú÷ï¯6mÚè‰'žÐ‰'”““£‘#GªiÓ¦j×®Ú´i£Ñ£GŸ÷9Ò™­O%%%ºúê«•œœ¬¬¬,½üòËjÙ²¥š7o®]»viÖ¬Y—dü®*oooåææêÈ‘#Ä *ñœ]KµL £FŽÐ¨‘#\>íõ¨Q£ôÄOhãÆ—ìÿ6›Íå¯$Kw1Ï‘¤¸¸¸jÇêñ´¯[ßvÛmJMMÕáÇe·ÛÊn&Hò ¹”Û<épæåç)rÕ•ðаŒ;Vo¿ý¶8 {ï½W©©©ÎcÕÆáphéÒ¥•ŽÅRZZª!C†hß¾}jÑ¢…ž~úéºz%ÙÙÙJHHPPPÆŽ«{ï½÷²®5ËÌÌÔ]wÝ¥?þX999²Ùlr8Ä <'d\ÝâÀ³”ŸõºgÏžZµj•n¿ýv-Z´HÁÁÁµ>gðàÁ•®ièСZ¾|¹üýýõÑG]ò¯^_HTT”Š‹‹/ë:qa§NÒÁƒ•œœ¬%K–(++K6›M!!!ÄLç1¿ýKwîkõ¥C‡Z¼x±üýý•ššªîÝ»_ô.Š•+Wª{÷îúôÓOåçç§E‹©S§Nu­€€Éßߟ£ý6 ü¦oõêÕÚ¸q£¼¼¼äãã£Ñ£GË××WûÛß«C‡qN¸Šx4˲´eËùúúª[·nЉ‰QAA{ì1ååå)--M'OžäÛJ Ÿ‘x,///eddÈáp¨uëÖr8’¤ãÇëÉ'ŸTß¾}Õ®];­X±‚cÇ4P„ Àcùøø(,,Lòóós~ö%88Xû÷ïWQQ‘V®\©ÄÄD„×@2e·Û(I•"Å××W ŠˆˆÝnW@@ßTj €Çªm ‹Íf“¯¯/ÇŠöæ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±`,B‹Æ"d€±¼ë{å6lÜTßC†ñˆéß¿}ˆ]KÀX„ 0!ŒEÈc2ÀX„ 0!ŒU÷Ç‘±Ùê| aªÓùß?¬ËÅ€®ÎB¦_¿›êjÑ’ê0d‚ƒƒêjÑ’ø°/0!ŒEÈc2ÀX„ 0!ŒEÈc¹wN;<€Ë!Ãi€§°åçf[ûຠ€‚‚.þì.m‘qeÁuûc2ÀX„ 0!ŒEÈc¹|™ôýûUTtLyGŽÔÅxpMèøø¸ú à<˜/]ãîüæÒqdÒ÷ï—ÍfW§ëº¹<@\:[¾Ù(›,%3à‘˜/ÝãÎüæÒ®¥¢Â"uêÜU²,.õxé|]WNŒùòòÍo.íZÊËË“eY.­u#//¯¾‡¨ó¥û\ß\þŒŒUáï¨/œ´<ó¥;\ŸßÜ8ûµEeÖ3N>&`¾t•;ó›ë!S¾/ ÔŽùò²pù82–e]òKqq±®j­•+W:o;qâD¬ëb×ïîr.טž­®çuiiŠm§¼¼Ÿ.øØ×^{]7ß2 ÞæÇºœß.IÈdggkä¨QjÛJAÁêØ©³æÌ«ÒÒÒ‹´¯¯¯}ôµmÛV–eiÀ­õç¿ü¥ÖÇ/]ú‰š†…ËÇ×O¡MÃԾõzî¹É*((pëM«º~w.Ó¦O×C=챿hÀååΟí§N’Ÿ€æÍûïj÷:tHŽÐ¦ú`Ñ"Y–¥¸V­ôè£(((ð‚ËíÑãz ½H½Ìu=¿¹þaß*+ÊÉÉÑ 7öRtt´,˜¯èèh¥¥¥éùç§è»ï¾Ó› \ÔrÇ?óŒsù?ÿü³tžTXX¨€€íܱ]§OŸÖ®]»5&eŒvîÜ©ý럮¾¤jëwÇÏ?ÿLdœÜ™²³³UVV¦é3f衇~«&Mš8ï{ñ¥—T\\¬œÃ‡eY–š5k¦I':×u>]ºtQ—.]Üš£~éüX×\?EUù;ß3fÌP£F´"u™n½åµ¿æ6L ßOÿøÇ»Ú°~½dYš:õÝwßçó¾øâ EFF9¯GFFé‹/¾8»?ÑÒÓÏŒ—Ÿ€üüª×\–¼¼¼¦¨ÈHõíÓ[Ç ך5k$ËÒìÙ³•ؾ½¡MÓ¢¥ ŽÕ©’M™2U­Û´U‹–±>|„Žæç׸þŸOžÔĉ“Ÿ –±­4þÙguúÔ)çc?üŸ•ؾ½‚Cº&1ñÌQ-ié'Ÿ8Ǽzõjɲ´qÃõîÓWáÍš©ç 7jó7ßH–¥Óg«ûßÛ¶9—Û鍊4oî<ɲ´jÕ*uÿõõŠˆˆTx³fZ¹re•÷àѪÎ]q9üãòööVÛ¶m4þ|çíÐâÅKtà =uøpŽdYú÷¶mòópÎOwß}âââPl«8ç|"ËÒ¼¹óÔ鍊œËêÑóÅ´h)GhSõîÓW³fÍRçÎ×)8Ä¡®]»iÇŽÕæÇ¼#Gœs\ù¥ç 7J–¥ì¬, }àEFF©Ý5‰zï½÷Ü?žŒ‹~ñ®¥Ï–¥*ùÔ¨Q£J·÷íÓG±±-µ,5õlÅYgþªøüŠË«ð³,iú´—õÓ‘\ýt$·ÖMOå?ïÙ»W‹—,Vbb¢,ËÒšµkÕ«W/íÚ¹C+W,WPPfÌœ©%ÿ\¢wþþ7-O]¦ÌÌL 1¢ÆõOŸ1CË–-Ó§Ÿ,ÕgŸ,Õ‡þ¯þþ÷wdY–Výµ>\ž}VÛÿ½Moÿõ-…††J²””4Ð9æž=z(''GwÜu·î¼ãíøî;ÝÜÿ& M~йõ¦êû)KÎ÷hçβ,K›¿Ù¤M6ª[·n¿hÓàòrg·JNN®BBB4iâDÍ™;ÏùÙËW_{]¿|º\×E99‡kœ ×¥¥é¥_ÐÎíßé³^Ñø ´yóæ³sÜÙ9ƲtôèQmݺUkV¯Ö¶­[ÑLsæÎÓœ9³õí–-ЋӏqOW›CCCs\ÚÚ5 ð׸±)*++Sòƒ¿•Ýf×–Íßè•™35úñ'ôý®]n½®r#d*ÿrrss]ã`ZÄ´PNNyˆÈù&Zª«ÒÀ+üìåå-ùøøT‘Ö™ÍoáÍ"¢nݭȈH½ûwœ÷‡‡…+<<\mÛ´‘eYúë[okò¤çôëîÝõ«¶mõúk¯êÓO?Svvv¥õ—––jþ‚75a³úUÛ¶j×®LNÖ§Ÿ}&˲ô—¿üU÷¢¡÷߯èèhuíÚÅùúì6»sÌ6›M ~ ˆˆ=õ‡'¦gÇWVV–¾ûn{Í!Séý•š¨yóæjÙ²…›4©ðX—πˬê|y1—¼¼<9õéÝ[­bcõî{ï)//O .ÔØ”9!ÊÉÍ­2oœû9*2RQQQ|Ï=êÜ©“Ö¬]WiÞ¨øØæÍ££a<ªÅÅêqýõŠm©ß?ü;mÞ²¥ÆùÙÇÇGeeeúý£Ã4lØ0ÝyÇÚºõ[¥­_¯?ÎzE‘‘‘JJ¨n]»*uYª!ãúûìÆñΖÝY¡¡eegWº­Ü¡¬,uïÞM–,Ùl:W…:wˆ ŠÏ;·ÝF•~ªi Íš5Óç+—+ @M›6•——W•å{þ‰'”››«¸„8çmñ ñ’¤™á\ÿñâã***ÒðiÄc#ÏÜnYJL¼F–,eÈÐwÞYÃØÎº\ÖÙJOOWhX¸ó6›Í¦ìÃÙê`k/I*«öž”_¯ø÷êïÀ³U//FþÑ|9BBdÉRJÊM~~ŠrQŸ>}tUë«âpèèÑ‚JK®¸žŠóF³fÍ”ŸÿSµ9¥êóB!:^\ì¼'ج¢¢¢çgIš0i’üõÂÔ)²d)ëÇ,IÒÕ×$:_VV¦ëº\çòëwg~s/d*$Ó€´hÑbyêòö>·¸´´õÊÈÈРAI²,KÁ!!úñÇ+dÅ–ÿlY–|}}*í~©i ^^vµjÕªÒs+±Âm¾¾¾ Ó¾}ûÔµKIRzzº$©EL‹Jc Ppp°Þ\0_ƒ%U^¯e)22RéééÕÆæãS}Ì111JL¼Fk×|]ãëhÒ¤‰²ÌV‡í+Œ½<øª¿.'!ž®ê|y1 Ð$@–eiР$MœôœfÍú£–~ü‘,Ë’¿¿Ÿ Ï}CWR­?Ûì¶3ÿ³\eN©öXÛ¹û¤sÇÖ­i~^½úk½ÿÞûZ·n¼¼¼dY–b¢cd·Û•¾w·BBB*¿®nbqc~ûÅö}nâåååiðà{µ>m½8 %Kþ©ä«ää΄ƒe©gëµeëV-YòOeÊRvvvåå¹"Y–â㕺|¹233õͦM*9û-¦s—šÇRiYUn{ä÷¿Ó´i3´>m½v}¿KcÆŒÕÀ·**2¢Úúû`²¦¾ð‚Ö®Y«ì¬lmÛ¶M?ååI–¥ß=ô–,Y¢÷ß{_?üðƒ¶mÛ¦cEEJHˆ×æ-[´}ûvíÞµ[ÙYٺ箻”ž¾O3gþ—dd(33Sß~û­sL={öМÙsµ{×neÊÒ‰ââ*¯£–×Ǿ%ð|µýù}žË±cÇäïï/Y–ì6›&<;^÷Ü}—z\ÿkɲà篂‚‚ssDÅõœ¹Rû\(«æçU{¬j\î©’IIј1O)ºy´Nž8©ŸOžTâ5ítm‡ýøÚ¹c§²e)m]š[¯ßùÍ­©¸?+,,Lk׬VPP îü%vè¨é3fjÌSÐóæ:שcGMŸö²¦¾ð¢Ú_ÛQÏM~þÌn§Jux¦ÞžúÓ*))Q—.ÝôȰÎϱTú|¥J·Uý Í™å»mü3Oë7ƒïÑï~ÿˆnM¤-ZèÍùo8ï/--•···,ËÒ”ç'ëÖôبÑjmG%?øöíÛ/˲4`À-šýúkš3wž:uî¢ûî»_Ô ¤$õéÝK7ßr«’n»M›¾Ù¤ˆˆfúäãôå—ÿ§7ôRž7jþü7ë|õ³d·ÛÕ»o?uîÒUÇ‹+6¶åÙ÷êöª~¾àÁj™£Îw9~ü¸üýýׇÞ?D~s󺿿ÿ™]K•æÎêóhÕ¹°êœRég«ú|SÓr×¥¥iÏž½š6}†Â#"©×v’$-^ô¼½¼tûwªcçë4vÜÓ*vãÀ¶îÌo¶üÜì‹~Ö竾Э“TVVVëcúõ¿YñqñzõO³äò€.·¢¢"­ß°A÷ ªÛÿ­æQQõ=¤ ²ÛíJ]ö™úßÔ¯¾‡¨ÁÅÌ—¨Îùí¯ªwßù»Æ=3^m~ÕN7ÞÐSK/ru—Õ=ƒïÕþŒýzéÅ©ŠŠŒ<ïkó&Œº Í—¨Î÷ë42¤iøyºbåç ÓѼ\—Ws¹¬HýìÜSþ…3eœÐ¹¸«äBsjEž<¯þ"—#d*fþ‘œ‹~.ÞOð|®n‘¹Ø9µ|ÙW¢:ß"ãp8TVV&›ÍÆ~¿zb·ÛUVV&‡ÃQßCÔ‚ùÒuîÎo.…LpPv~h© â™IDAT¿CW_ÝN6»Mç¾mŽËÃR™U¦ïw~¯àààú  Ì—®r~sé[K’´7}Ÿ •ŸŸïÒŠpi8몳G&x&æK׸;¿¹2žÂõâxB‹Æ"d€±`,B‹Æ"d€±`,Bëÿlbiµ¼zIEND®B`‚Projecteur-0.9.2/doc/screenshot-settings.png000066400000000000000000001714721412537212300211460ustar00rootroot00000000000000‰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äzJJIC€âææ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çFELL _ý5!!!¹Æ*DYg`` º´ döü1K—.-¶QId„( iÓ¦ñàÁƒ—Ï*kooššJ¥€\/Ïd=Ö-[²M\ÅÈÈÈü TWW'==]µ=ëÎ]]] …ê›üËêÌ000P]î)¬‚\¢ÊRØsXÐøKk¿˜<|øÈý5zYÝ;vìàÀ´jÕŠvíÚÑ·o_fÏžk¬B”u£GV%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Ô«WjÕª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…„ ¦¦ŽGz%ÓÎù³§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¨ö¸pfÍ[`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›cDZpá"úõë (éÖµ 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Ù¢£Fa÷ž_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ÄÆÄbiacåÊ…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"ÂÃslgý†õª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•bC©Ì`ð  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þ|‘yyK- ôÖ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¶loçœ ÚýðýÆŒ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§NJ<>‘·ÄÄ$bcc153åqR -á^d$Û·ýX¢í¦¥¥õß—zsS\}(ó.aóæ¢ßi)Þ<ýû÷gò„q*[>BE¡`玼Ӫec‘^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—.áâZ4ù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ñãÇã]ª Ñ1xöì™$3Bˆš$2Bè¹—IÌË„æÜ¹s̘1“>¥xõP’!ÄM]ï‘ ÿMb^þ÷Ô©SÌ;ŸRe‰ŒŠ&111ç“ùœ !ò¹k)ˆŽ‰ÁÎV¿–öì©©i)bþNm£££±³³Ë…óµZJ¥B­V³k×.<<<Ò•—.WÛ7(X°  ÔMB¡#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Ç£Ø À£G8yò$©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š2j5kóóæ_Òµï×§ êÕ¥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 ³æÌ£œ¯?Îî^L6•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&MN¹²e¨TÉïõÓÒÒXøÕ×,[¾’„„líliÛ¦5“'Žàâ¥ËMøœËW®âîæÆøq£iÒ¸³æÌcã¦ÍÄÄÆ¢T*iØŠ9³¾ÀÈÈ(WQñ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ä8F“î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œ;wjU«`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ÀÜÜŒFxþü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>–ššÊ£Gqrr|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|èþ›¤L6øøøÉŠ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})22Fƒ­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(,XmÛ¶±k×.Š/Îýû÷Y·nÝ;Å%ôÇ'ñއ™¹¹ö ’™¹9ãƒÆéõʾŠ'1‘YŽhßþ¶nåƒxð kk+íLw!Dþ“ššÊ£Gqrr|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' µZD"!/?üüü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Õª=vwß}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„çȃ‰LzFF÷\£Õ’–žŽµ\^¡ ê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 OV[Xca.+1¹}°œh\ º9é?t^W* øõOÌ`## UŒ ùk4Z"¢£p°·Å͵0‘)((àÚõXÒÒ3J¥¸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ìílusóòÈÉÉÅÖÆ†¤äÛ˜˜˜àîæ‚‰‰ 7òó ÈÊÎ~o©éÜNICáá¦w¹Z­.’àI «TjT*Ò‡’#©*uñ~BåQöD¦¢#~6 HJJªè0„2zøx0a0a„â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ô¿vVKNnІ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£F4p@á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ñ¾n4kÚ„!Ã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&“•­»– ϵZZ­ÆÙÙ©¢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îžÞ¼Ú­''Ozªû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ö­1E^$åÚÙwé’ÅlÜô=#GáСÃD]¹Â®Ý{Ø»oÕqp°gÖì¹\‰ázLÌ#·¥R©øëÿ###W7WIIM!(0fÍš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*•YT*ÅÆÆFïºAADG_àì¹sLž23§Ï`bbÂÌLòó󈎾N·×^}l,366¦gϬ߰‘Ö­Z±{÷~Ýñs™·ààà@vv6F—ÌdddàèèX¬ìÚµk9{ö,‘‘‘H$FEÇŽ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èЀ€–-[F5غ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ËFIAA}û„•ú5é³råJLMM `ÕªUlݺ;;; ðÂiU«V`æÌ™4jÔˆ&MšàííÍ™3gøå—_H$˜˜˜°}ûvþüóOªU«F\\ß}÷Ý£v+”³kcß›6+¹\w*°’ËyoÚÔ"e áñ ƒ©‘‰‰‰åbDË>^òŸÖx”BNN#FaÝÚÕ´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.^Œ@jdD5Ð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Ì™ýOUè<]à[­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¨ëêêBOТ…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&KdbccQ0ÄÆÆ:­"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£F2ˆ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:‘ž––7oOF +kjXYžÿñÇñöñÁÅÅ•áÃGsý:(EB|<Æ=‰³N‡NçÂܹsïÌ›·l1´³wïÞ ßÎR¤<°Å„*ôÔÒšµkIIIA)Eðîݤ¥¥Ñ­kWÖý¼¿~ýÈÈÈ`×®”R4oÖŒˆ Šl³´1I‘"%éïçÇòåËùjùrÓèehIDAT’’“ó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ÝÓ³!GC¡èï×]»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×ÚßÉ"oOF +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ƒüý ¡—oOFk8zsïö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Ç˜ÂÆ@[[[RSS9aÜ,j 3M)þv.~"sσƒ=QÑÑùžW(®FEáà`B¡Ñ`ظw71y~»{ܦ %åýÑét:t€óçÎs=šŸ^ƒÎEW@ËŠ”ÔbcciàÙÀð\Cφ\޼’gù·Rn‘œœÌ 'áà䌃“3K>ý”˜ØŠK—/Ñгa¾x( ê¨kÑDDDÚq­SFCôõ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.9.2/doc/screenshot-spot.png000066400000000000000000004771211412537212300202730ustar00rootroot00000000000000‰PNG  IHDRc´TEsBIT|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'OdÓ‡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õêµ8N ¸ëî;Ñ+ÁÏSKK «W¯áè±ch5ÆŒÉcåÊ•8êêêxsõß8{ö~Ÿ—‰'ñù;Va4;›„ CO2cB!†½?|TÃ>ªé¶|öhz%~´eu-~~qO©=­•Õ{/0'ÏÆƒ×ÇãöªüfCg*ݼºçBØ`¬ÁégÓ‘`°ô·¦29ÓDÉÿúV4sç´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»ÝÎÏÿí_CO:ÅS¿{š³gK54GUKs37-Y 7,àÍ7WsààŠÎ1oîus¶ä,V›o~ãXLFþç·OQUUMYY9TUW“žžÁ׿ú<^/½ô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À‰'ø?_úÒàêÙ¬6ZN@¨‰XÝ… ©‹¸cQj0óŒé…;ïú<üãK;z”cGvZo6[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!Äà>ïâçëÊC3ã{"^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úºÝÁ™Ö³²²08RSû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µ¨Ú¾·ù¾„Bbö°ª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ÈDB©ªú×ñqûýÞ¶EUƒÿÂl+„~4ùIS!Äàè.Nø,B!Ä@…ûÑE4!„ý¡#h»+Á˜¸¶u¼  — ë˜ë– 벟dÄ„¸¶uýàL!D8:4TU æÇäR\úcí· ÌTB­å3%Ä00TÇï Ì„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[‡Ûã%Ðbo1M×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Øl6FMiiiè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ªÓºúúúP7a•û*³ý¢à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 8G;ƒÑÀ²% /é¹­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ÌèQLR@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£Gbîœ9¤§§±eë6ÞüÛjþç×ÿ5U”––V¶nßÎþ©¬¬$7'‡ ãÇ3göLÇ=O@UÑ@hxí³gKø§ÿû8ÿýë_’’œÌ‘OrôØ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è…VzÓ²™‹—‘“_Ü*¹­Ñw;fÆî[222øðÃ8p #FŒàçŸÀÊÊŠÑ£G7)£wX(žîlÛ¾“'=ÈáÃ:dPss3F^û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>Æj59zhn®. <ˆ¸¨ÓõŽ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‘HDn݈;{_Ÿ廹¹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&•àhkR!£º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ˆŒ«Ð¸^"]uNkpù&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 4c§"±¶²$ЯÃ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¦¯õ#˜Éˆ(XKqa 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@àߎá1t: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;LBNNN«Ê^~é…&õΚñÏ?ûŸ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¾Ïج™3J%H¥fÍœQ1!¤øž¦¨¨‡¶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â2zÚ%Ê#Ö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žO5pÑ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ˆ¼qZoõê¤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Ù½w4°ärE™—Ïš{[ä2f“‰¬l½ÕXì‘šš†“£s¾ÏV£Ñš–FFfJË6•J…RY²uY­Õ§R©,ô{vÇz‘>œÕkÖ2lèœÕj¢£¢q÷p§VhµZÂÂÂ¥f@ôïÏGŒ““~þ~d¦gtþ|OOOjusä†ùz½ž˜˜*Uª„ƒƒ©©©$''ãêêŠ\.'''‡èèh¼¼¼,½’^^^œÜŒï-ª…²víZªW¯N§NJ«ð`Ý ½|ù÷üó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Ù²2X(·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Þ£GdÖ›Ó€¼ßþ.Ý{³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ÌìwPÕ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üû{!q6Yó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П-`ðÀþ,]ö¹¹¹,ýv95üù`Á\ž}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² BY1á¡TQVUŠ'Ëí¹æ\Û; ºCcüZ—rD‚Gü¦BÅSS݉gûz·rÍ9¤k¯–rD‚P|¢1&~¢D±Xó÷ñèÞû ÚvêÊ¢>É·-ôÚuFOãæ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ësI¥ìÛ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þ8pFM‹æÍ¬/<<‚7o0áùçQ;;Ó°A}Ž9€oõjVWcŸ«‹Ž¶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Áy4?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•ž=›®‚ ±'NN®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?Å嫸lq'&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à€þ48dge5y-÷¦µÄëõ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Ûw7h`³µÍê Å!ë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°npø)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¡ÚòÀCpÉå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’š4Uvh‘ 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þò >¯—ÓNLB|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¼?ÿCN29ìù[,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Ñi5u‹”¤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—¿ßÏîÂâ&ûªHliiprv15Š‚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 =938³™’Òè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…ÎJiOcšé„Öˆ`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“©“'ñìS3ëÒ™|ð᪪ªÂž÷koÌ¡OßÞ<ñèCœ1í4Þxó-ìöýcà½^/o¼5—Œôô°u臗“›“C§ì¬&ïÿ÷o’™™Éc<Ä»ž_WýìÅÕjµ,_ñ3GôŒ ‚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¾5Gz€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^QJþCHHH ==/¾ü—ËÅšµk±VWÓ£{÷6ŸÿÁ0 üøÓ Ün7[¶üAyyy“É%–.ûž††þغ½{÷’Û%‹ÙÌÆ›p:],úâK"ùWR«Õ’¿{7>ŸªêýY0‡RgK’q»Ýüºj5n·›üüÝìÞ³‡Ìz‹kjj¸ö†Ù¸qÿD7II‰$''ñÅW_ÑÐÐÀ/«VQQQIïÞ½=r$/þë¹àkÌèÑL2¹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’òü¯¥ëê³½{ömk ×£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¢Dr#´¬°°ˆ^;ó›¦)ö·±ãÄ×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¬51`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•ø„ÄÛ"–²|½tbZÐ(¦[¶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ñAALxyÚ´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¨ÌkIX&ñHIMåø‰“?qò†–±“§N“““Ãé3qœ¶Ž9bá¼9Lš8jáá|ðáÇ]¾/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»ÙŽŸŠ#,4r¾ ±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&…REzhBââÁË —Ìþ Û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Þš3sç/ðçÎ]Ì_´Ø*ßÿÀÒï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èhu蘳Î;ÏÅK7v,^žžÄ4¹‡÷ì fH‡â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Α8Hn~ž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«Ÿ|úOyœ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Ú1fùŠUlÛ¶/úõéãPE àÞ{»ñãÚu¬\ý#Õ«…3ùµ ö’¸\ÞÓOaé²å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©õ¿ÉôŒŒ2U–²â  -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:]XqfXÏ«/ëŒ -cB•f4­º;$$$°{÷n† b³‹¢\.cäÈá|ùõ7løå7Ò3J_K­Ñ ‘Hxhà=4eË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¥œ:uV‹Ò­¤â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;‰¤ΠG1ZA¨|: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Œ U9Ó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›·ªÀ+Çö­¿óðÐGJs`ÿ^’Î] VÍÈJŠJ¸UNžN@£ÕÑ¿÷½¨5:þ;t”¨Z5Ë+çÏïᣱÌè×µZM~AUeËÖ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ÿí=ÿÃÇŸ~΋ϣ{×’¥]&OAû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!AF ŠŠ+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Ŭÿ'$¥ð "Šûá‰OH2Néõ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"ñ³y3ÄÛá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š«×®óÞØÑøûùå!"#3FƒTúøEúÉYƒ•«VãîîAn¯sôØq~øqu.=¾X²/Otz=ÇŽŸàüù‹è z5lHËÍs”´(*’d7Dö"«îw¸ö2w¶ï¿ÿž˜±ç†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#MnÞ'“>þîÄÆÅ£Õ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›F1dÈ`³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§û&OFJr Ž,^¸ Ç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&=ûµìÿ„9Xg‚"± 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ÇÄâáæ’ox’#<ã ¡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œ·¢(ðÔœ1g†1UCFEÒ”§–˹ö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]1gŽ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_¹Û©Gxqɪ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ï>Ö®úÁ¼ïó/¾"559³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Àäi3H$ôë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‚Á¹AD"É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%®WqDù¢âñÈ-¦Â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¡ÑhJ¥(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@*• hM¦'‘‘Qæ,|7nÜ`݆4nÔ˜ýß Q£W8{î,Ú¾C·¡C³~Ã&<Šƒƒ]:u¶!œù!‰:dë7læà᣸¹¹0bØ;ØØ/\ M›Vü²}·üBo¦LšXH±ì;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ÅÁÞ>ïFtý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)™qY…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ŽG5˜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 .¡,³vvvdffb0H$¨ÓÓ ª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Çž¤äÄb1A5̪/•×"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?Gg¸±†6VF®FS¤aøÐAf뙜’BDänÄÄâà`On]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ê¢"ä²â‡–”Ô44O'Màȱã¼óÞ‡´kÓ¦Jô¬—ìÃÉÌÊÂÑÑÑdÌAá  ¿ FƒT*%77µZ˜¾FÏvêÈgÓgqèŸÃüûï>™ø±ÉöÜÝÜÈÎÍ)uncÎ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Ö¯¬1G‡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$aeeEhZ4oÆ•«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¡GnGbtsueÆôÏØ±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ÄÄâà`On]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Ã’\;·Ÿ’áîÇdVƒ¦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­mlJ¤¼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"•JJ%Ø+D]F.—ããí…J¥"6ö&%«‘‰¨‹ŠJ%¸»¹SCüéG\˜ñÙ•¾ .°|e8­Z¶bèà¼ñúH~[¾‚ÝíÁÑÑ‘>½_ÂÛ˽^O½º¡Ìš=WWW¿2€„„/YÊ˽^bÁ×w‚±DEEÖ!Œ¦–{®ßÞÈ­äH$Ú·3¸ã^nŸwóBnD¬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áp8P…Ñ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©T8Nt:>Ÿ­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 þ®Ð1v%å”Ûª8”{µZZ­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ËïLe¶îÜÃ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µZJ¥:é%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íÃÕCgnV2ޱ*ƒ€-\,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è24$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 šÂÞŽJo6%Îìæß˜Ç,õ!Ô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±(/ã}üopç¢{¹õ曈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Ο7FÃð¬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«6EFÆpâ“ÝxN 1¤ðŽ:vâ“]Ä u‘ç^G£«žø‰õüê®!¨t2*]íP™Ír=ûŽèqø[¿•Áíu£Ñ«š¿Ù?¾ €ß@–e<ÔâŸWùÞ0¥óŽo‚rª›—(¥é×hP¯ÎÂ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Ú[1Azl×á?Ü=}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«Õ8N|>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;£´¬kbM³­ºÞ°qÉI‰Üyû_P©TN§Ù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¤ €;ÞÆ¶íÛé? cFf×îÝ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:r5Š¢¢¢æþïóùÙ¹c'Æ Z>¥ë¨#mƒÍfkñ»ò ‘Í_dµUgS)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¹Ä±5v{›×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Ð#º;>>>—IMzÆ„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áùçðwznqÖæÝ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Ë–-Ü2z47IQQûì'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 IDATuFCrJ 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{µZJ¥²/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ËNJ¢}‡ö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Τ¦’˜tVCëˆVøùú4vÑ„B\#$B!ˆ¿É@`€ µZZ­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‡í<ÿúr5Y]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€ µZR©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!€²± „âê²ß3VKjZ‹…ÈÖ‘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½×OgÃó 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·±–ý¬Âl6Ae=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ˆ¦CB6@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Å ®îÓrAÙ—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Š5qUþ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Ì„§±¯ÃÆÎ_!ĵObB!DHà%„ÂêÊ¿ÈÑ”Õ4GÅë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ípB!\*µZùõè Χ_Dí¥¦S»(š‡‡p)3‹cñ‰\Î/ÀÛˋ֭ZÙ²Þ˰æÓÏÙ¾cûo^jÅìYOÖ{~£FŽ`ÔÈümé;µÞ7í|ýb •&ÀóÏÎotEýؾ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üرøø8OJâ·Þ¶ÿž˜˜@Ìðî¸ýwtëÚ…—-!00i¿Ÿ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-¥wn˜Í%=~’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íz3Ÿ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Ü(FJ©"*< •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ªŠKm6’“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­VN9Í Ï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¤gf4`³Ùøîûƒôêù@­ò_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õxFC§í+­›óöò¢°¨ˆ±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!„¨ FJ©"*< •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øôóåÄ6aÑ‚y ú>ûü ŠÌf” gÎ&ñê+³™0~,›¿ÚBAAa­Ú(„×R*P4t B!į’ÝnÇj-ÅhÊ`Gâ>öà\rÊuÇedfã Áõ$0gætzõxFC§íÉÉÉ©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ÜFJ©"*< •J‰Ÿ^‡¿ŸŽ"s±k0f..¡¸¸„à@ÿ‰1''—õ61iâ³ÄÄ4åü…dV¬Z]c>?½Æ“Þ]\北£Gdô¨‘œ>{–ÿþ1mZ·!À߯Nqz7òÆZj¥¼¼•JEII 6[Y­b騡=+W¯åÔ©ÓüüóiFQ«|ÕÑé´˜‹ÍØ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ÜÕ®-ó,"001ŽÆ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.9.2/doc/screenshot-traymenu.png000066400000000000000000000314411412537212300211410ustar00rootroot00000000000000‰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>„9mh#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ýæ­hq¤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Ê™á—ÑRFOF•¢Éã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ÒQni#Pñˬ)D³.C´ìÆ©ÇLÀ˜‘Ã1zäpd¥ë¯ºÆ£¶¾+¿\‹_®Å›}ŠöHºÓyÎõ½¹û±1pb½;ûæ·áL¯¯Ó9ÆÚ£hÙ]‹ÚÒ퀬‚BD2z€ƒYêJ-) ¦½»¹Ì’ŠlyôP+·óÂv­aI…62Ç€týæL1WyYÝjoæxŽ]»÷:èŒ,/w¦]†å6zÙ®ðBG™Ê4t}úÔBmºË ÑkæØ}J"@cúÑ–íRQe–uð\wëŒW?t_T'\Ή1.ŽAY³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¹‡ÏØ¥pLYÆu³¿æçÞk^¸ç+ÈGqÅ0äñ©ú~¶*¬Çê^Êþ1¨ˆúd ¬fEþ˜ ¸ U·¼OŸý»7 •¶øÜí0qã“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ÆßÅøzOtÛ¶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"9e‘ª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.9.2/docker/000077500000000000000000000000001412537212300151135ustar00rootroot00000000000000Projecteur-0.9.2/docker/Dockerfile.archlinux000066400000000000000000000013111412537212300210750ustar00rootroot00000000000000# 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.9.2/docker/Dockerfile.centos-8000066400000000000000000000005561412537212300205520ustar00rootroot00000000000000# 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.9.2/docker/Dockerfile.debian-bullseye000066400000000000000000000007011412537212300221460ustar00rootroot00000000000000# 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.9.2/docker/Dockerfile.debian-buster000066400000000000000000000010301412537212300216220ustar00rootroot00000000000000# 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.9.2/docker/Dockerfile.debian-stretch000066400000000000000000000010311412537212300217730ustar00rootroot00000000000000# 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 Projecteur-0.9.2/docker/Dockerfile.fedora-30000066400000000000000000000006541412537212300205710ustar00rootroot00000000000000# 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.9.2/docker/Dockerfile.fedora-31000066400000000000000000000006551412537212300205730ustar00rootroot00000000000000# 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.9.2/docker/Dockerfile.fedora-32000066400000000000000000000006021412537212300205640ustar00rootroot00000000000000# 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.9.2/docker/Dockerfile.fedora-33000066400000000000000000000006021412537212300205650ustar00rootroot00000000000000# 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.9.2/docker/Dockerfile.fedora-34000066400000000000000000000006021412537212300205660ustar00rootroot00000000000000# 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.9.2/docker/Dockerfile.opensuse-15.0000066400000000000000000000010521412537212300213240ustar00rootroot00000000000000# 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.9.2/docker/Dockerfile.opensuse-15.1000066400000000000000000000010511412537212300213240ustar00rootroot00000000000000# 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.9.2/docker/Dockerfile.opensuse-15.2000066400000000000000000000006271412537212300213350ustar00rootroot00000000000000# 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.9.2/docker/Dockerfile.opensuse-15.3000066400000000000000000000006271412537212300213360ustar00rootroot00000000000000# 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.9.2/docker/Dockerfile.ubuntu-18.04000066400000000000000000000007531412537212300211030ustar00rootroot00000000000000# 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.9.2/docker/Dockerfile.ubuntu-18.10000066400000000000000000000010301412537212300210650ustar00rootroot00000000000000# Container for building the Projecteur package # Images available at: https://hub.docker.com/r/jahnf/projecteur/tags FROM ubuntu:18.10 RUN apt-get update RUN apt-get install -y --no-install-recommends \ ca-certificates RUN apt-get install -y --no-install-recommends \ g++ \ make \ cmake \ udev \ git \ pkg-config RUN apt-get install -y --no-install-recommends \ qtdeclarative5-dev \ qttools5-dev-tools \ qt5-default RUN apt-get install -y --no-install-recommends \ libqt5x11extras5-dev \ libusb-1.0-0-dev Projecteur-0.9.2/docker/Dockerfile.ubuntu-20.04000066400000000000000000000007571412537212300211000ustar00rootroot00000000000000# 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.9.2/docker/Dockerfile.ubuntu-20.10000066400000000000000000000007571412537212300210750ustar00rootroot00000000000000# 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.9.2/docker/Dockerfile.ubuntu-21.04000066400000000000000000000007371412537212300210770ustar00rootroot00000000000000# 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.9.2/docker/README.md000066400000000000000000000005041412537212300163710ustar00rootroot00000000000000# 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.9.2/icons/000077500000000000000000000000001412537212300147575ustar00rootroot00000000000000Projecteur-0.9.2/icons/cursors/000077500000000000000000000000001412537212300164575ustar00rootroot00000000000000Projecteur-0.9.2/icons/cursors/cursor-arrow.png000066400000000000000000000002531412537212300216320ustar00rootroot00000000000000‰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.9.2/icons/cursors/cursor-busy.png000066400000000000000000000003111412537212300214550ustar00rootroot00000000000000‰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.9.2/icons/cursors/cursor-cross.png000066400000000000000000000002021412537212300216230ustar00rootroot00000000000000‰PNG  IHDR¹‡mð PLTEÿÿÿÿÿÿŽôÃìtRNS@æØf'IDAT™c`À  ã²èÐÖP  Œ«Vp­š€à“mÜ=XKô ŒÖ "«IEND®B`‚Projecteur-0.9.2/icons/cursors/cursor-hand.png000066400000000000000000000002371412537212300214140ustar00rootroot00000000000000‰PNG  IHDR¹‡mð PLTEÿÿÿÿÿÿŽôÃìtRNS@æØfDIDAT[c``a€€L(=Xz”Žj€Ð‘ JcZd˜ ¥§N Ѫ¡šf @¦¦†& Ð«V-ÀJ£7ÖU%ÙIEND®B`‚Projecteur-0.9.2/icons/cursors/cursor-openhand.png000066400000000000000000000002401412537212300222700ustar00rootroot00000000000000‰PNG  IHDRµú7ê pHYs ‰ ‰7ÉË­RIDAT(Ï…Ðá…Ñïý_úútW–&›D„êNtë^a¯'Ï`O#9#åV*ˆ~®õÀßWa`'Ê-$ò2d_=€Jì«;Ò€>x0tš¯ªbIEND®B`‚Projecteur-0.9.2/icons/cursors/cursor-sizeall.png000066400000000000000000000002561412537212300221460ustar00rootroot00000000000000‰PNG  IHDR¹‡mð PLTEÿÿÿÿÿÿ~ïOtRNS@æØfSIDAT[c`À\ ´f„^¦˜¦†5€éÐP0­Áè ¢—0:H„§0:ˆ%¸BD&p†A‚†‰ÃÔÁôÁÍ™ ³f/Ü0waÛtêçNXIEND®B`‚Projecteur-0.9.2/icons/cursors/cursor-uparrow.png000066400000000000000000000002041412537212300221730ustar00rootroot00000000000000‰PNG  IHDR¹‡mð PLTEÿÿÿÿÿÿ~ïOtRNS@æØf)IDAT[c`ÀL+ 4W„Ö kÓKC€é©¡ ‰†¡0`­ç B7Ò~2IEND®B`‚Projecteur-0.9.2/icons/cursors/cursor-whatsthis.png000066400000000000000000000002771412537212300225240ustar00rootroot00000000000000‰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.9.2/icons/icon-font/000077500000000000000000000000001412537212300166535ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/.fontcustom-manifest.json000066400000000000000000000067671412537212300236510ustar00rootroot00000000000000{ "checksum": { "previous": "a9504014a04ad54718a75911f452551882ad71440ff3fac45f0dad3a5042a9ef", "current": "a9504014a04ad54718a75911f452551882ad71440ff3fac45f0dad3a5042a9ef" }, "fonts": [ "output/fonts/projecteur-icons_a9504014a04ad54718a75911f4525518.ttf", "output/fonts/projecteur-icons_a9504014a04ad54718a75911f4525518.svg", "output/fonts/projecteur-icons_a9504014a04ad54718a75911f4525518.woff", "output/fonts/projecteur-icons_a9504014a04ad54718a75911f4525518.eot", "output/fonts/projecteur-icons_a9504014a04ad54718a75911f4525518.woff2" ], "glyphs": { "iconmonstr-arrow-73": { "codepoint": 61707, "source": "svg/iconmonstr-arrow-73.svg" }, "iconmonstr-arrow-74": { "codepoint": 61708, "source": "svg/iconmonstr-arrow-74.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-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.9.2/icons/icon-font/README.md000066400000000000000000000022531412537212300201340ustar00rootroot00000000000000# 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.9.2/icons/icon-font/fontcustom.yml000066400000000000000000000002351412537212300215770ustar00rootroot00000000000000font_name: projecteur-icons base64: false input: vectors: svg templates: templates output: fonts: output/fonts templates: - projecteur-icons-def.h Projecteur-0.9.2/icons/icon-font/svg/000077500000000000000000000000001412537212300174525ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-arrow-73.svg000066400000000000000000000001761412537212300237510ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-arrow-74.svg000066400000000000000000000001771412537212300237530ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-battery-3.svg000066400000000000000000000003211412537212300241720ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-battery-4.svg000066400000000000000000000003421412537212300241760ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-battery-5.svg000066400000000000000000000003611412537212300242000ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-battery-6.svg000066400000000000000000000004001412537212300241730ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-battery-7.svg000066400000000000000000000004171412537212300242040ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-connection-8.svg000066400000000000000000000011641412537212300246720ustar00rootroot00000000000000 Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-control-panel-9.svg000066400000000000000000000003531412537212300253100ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-gear-12.svg000066400000000000000000000041701412537212300235240ustar00rootroot00000000000000 Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-keyboard-14.svg000066400000000000000000000004251412537212300244070ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-keyboard-4.svg000066400000000000000000000004111412537212300243210ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-media-control-48.svg000066400000000000000000000001731412537212300253530ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-media-control-50.svg000066400000000000000000000001641412537212300253440ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-plus-5.svg000066400000000000000000000003241412537212300235100ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-power-on-off-11.svg000066400000000000000000000010341412537212300251170ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-share-8.svg000066400000000000000000000004261412537212300236350ustar00rootroot00000000000000 Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-target-8.svg000066400000000000000000000013631412537212300240220ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-time-19.svg000066400000000000000000000012231412537212300235470ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/svg/iconmonstr-trash-can-1.svg000066400000000000000000000006311412537212300244020ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/templates/000077500000000000000000000000001412537212300206515ustar00rootroot00000000000000Projecteur-0.9.2/icons/icon-font/templates/projecteur-icons-def.h000066400000000000000000000006711412537212300250550ustar00rootroot00000000000000// 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.9.2/icons/projecteur-icons.ttf000066400000000000000000000104201412537212300207660ustar00rootroot00000000000000 €PFFTM“!ÊœôOS/2Oö])X`cmapâbéTôbcvt DXgaspÿÿìglyf+Që0 8head¹›‰Ü6hheaðÆ$hmtxÁи:loca~l\2maxpeÐ8 nameÁÐ Èpost%ûUãà ȧY _<õ ÜM-†ÜM-†ÿÀÀÀÿÀ.Ÿ @.ñLfGLfõ„PfEd€ ñÀÿÀ.À@ »ªÈ*UU@*\@  ñ ñññÿÿ ññññÿÿÿãÿD****Lt¢Ô F4b¸(n®@‚Žœ™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(GG -")UÿÿÀÀ !7%.767'53&7&3#3#3#$#"'332654.#"#632S # N ŒU*’þ]ªª+««@••À}XY?F&,Fd-O.,&F?YX£ #B Ž994N+*++*ñ}@dF/N.@*ÿÀÖÀ'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@@ÿÀëÀ&A%'"&462&2'654&"327#"&4#"&462'654&#"32%ÆJC!D.<##FjK'2G22$ 5K¥Y}}±}& dF/N.dG¯bVXA%##VK5 #22G2*KjÜ*}±}}X*' Fd-O.GdÿÀÀ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®%B,¢ñ$W     6 XH  Ï    5projecteur-iconsprojecteur-iconsiconsiconsFontForge 2.0 : projecteur-icons : 13-2-2021FontForge 2.0 : projecteur-icons : 13-2-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-50ÿÿÚ‡oÜM-†ÜM-†Projecteur-0.9.2/icons/projecteur-tray-64.png000066400000000000000000000104641412537212300210600ustar00rootroot00000000000000‰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Ó§O2))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.9.2/qml/000077500000000000000000000000001412537212300144355ustar00rootroot00000000000000Projecteur-0.9.2/qml/main.qml000066400000000000000000000170071412537212300161010ustar00rootroot00000000000000// 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.9.2/qml/qml.qrc000066400000000000000000000003771412537212300157440ustar00rootroot00000000000000 main.qml spotshapes/Circle.qml spotshapes/Square.qml spotshapes/Star.qml spotshapes/Ngon.qml Projecteur-0.9.2/qml/spotshapes/000077500000000000000000000000001412537212300166265ustar00rootroot00000000000000Projecteur-0.9.2/qml/spotshapes/Circle.qml000066400000000000000000000003731412537212300205450ustar00rootroot00000000000000// 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.9.2/qml/spotshapes/Ngon.qml000066400000000000000000000004611412537212300202430ustar00rootroot00000000000000// 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.9.2/qml/spotshapes/Square.qml000066400000000000000000000004571412537212300206070ustar00rootroot00000000000000// 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.9.2/qml/spotshapes/Star.qml000066400000000000000000000005741412537212300202600ustar00rootroot00000000000000// 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.9.2/resources.qrc000066400000000000000000000011631412537212300163660ustar00rootroot00000000000000 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.9.2/src/000077500000000000000000000000001412537212300144335ustar00rootroot00000000000000Projecteur-0.9.2/src/aboutdlg.cc000066400000000000000000000214201412537212300165420ustar00rootroot00000000000000// 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 { // ------------------------------------------------------------------------------------------------- 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"), }; 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") .arg(projecteur::version_branch(), projecteur::version_shorthash()); 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.9.2/src/aboutdlg.h000066400000000000000000000007551412537212300164140ustar00rootroot00000000000000// 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.9.2/src/actiondelegate.cc000066400000000000000000000317241412537212300177210ustar00rootroot00000000000000// 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 QSize(w, h); } } 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 QSize(100,16); } } 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 QSize(100,16); } } } // 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; } 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())); } 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: // None for now... break; case Action::Type::ToggleSpotlight: // None for now... break; } 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; QMenu* 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]() -> unsigned int { switch(item.action->type()) { case Action::Type::KeySequence: return Font::Icon::keyboard_4; case Action::Type::CyclePresets: return Font::Icon::connection_8; case Action::Type::ToggleSpotlight: return Font::Icon::power_on_off_11; } return 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; QIcon icon = {}; }; static std::vector items { {Action::Type::KeySequence, Font::Icon::keyboard_4, tr("Key Sequence")}, {Action::Type::CyclePresets, Font::Icon::connection_8, tr("Cycle Presets")}, {Action::Type::ToggleSpotlight, Font::Icon::power_on_off_11, tr("Toggle Spotlight")}, }; 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; }(); QMenu* menu = new QMenu(parent); for (const auto& entry : items) { const auto qaction = menu->addAction(entry.icon, entry.text); connect(qaction, &QAction::triggered, this, [model, index, type=entry.type](){ model->setItemActionType(index, type); }); } 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.9.2/src/actiondelegate.h000066400000000000000000000036561412537212300175660ustar00rootroot00000000000000// 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.9.2/src/colorselector.cc000066400000000000000000000057131412537212300176270ustar00rootroot00000000000000// 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.9.2/src/colorselector.h000066400000000000000000000016311412537212300174640ustar00rootroot00000000000000// 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.9.2/src/device-vibration.cc000066400000000000000000000373351412537212300202070ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #include "device-vibration.h" #include "device.h" #include "iconwidgets.h" #include "logging.h" #include #include #include #include #include #include #include #include #include #include #include #include #include DECLARE_LOGGING_CATEGORY(device) // ------------------------------------------------------------------------------------------------- namespace { constexpr int numTimers = 3; } // ------------------------------------------------------------------------------------------------- struct TimerWidget::Impl { // ----------------------------------------------------------------------------------------------- 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->setMargin(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); sbHours->setRange(0, 24); sbMinutes->setRange(0, 59); sbSeconds->setRange(0, 59); 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(hours.count()); m_impl->sbMinutes->setValue(mins.count()); m_impl->sbSeconds->setValue(secs.count()); } // ------------------------------------------------------------------------------------------------- void TimerWidget::setValueMinutes(int minutes) { setValueSeconds(minutes * 60); } // ------------------------------------------------------------------------------------------------- int TimerWidget::valueSeconds() const { return m_impl->valueSeconds(); } // ------------------------------------------------------------------------------------------------- struct MultiTimerWidget::Impl { 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)) { 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 (size_t i = 0; i < numTimers; ++i) { timerLayout->addWidget(m_impl->timers.at(i)); m_impl->timers.at(i)->setValueMinutes(15 + i * 15); 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() const { return numTimers; } // ------------------------------------------------------------------------------------------------- void MultiTimerWidget::setTimerEnabled(int timerId, bool enabled) { if (timerId < 0 || timerId >= numTimers) return; m_impl->timers.at(timerId)->setTimerEnabled(enabled); } // ------------------------------------------------------------------------------------------------- bool MultiTimerWidget::timerEnabled(int timerId) const { if (timerId < 0 || timerId >= numTimers) return false; return m_impl->timers.at(timerId)->timerEnabled(); } // ------------------------------------------------------------------------------------------------- void MultiTimerWidget::startTimer(int timerId) { if (timerId < 0 || timerId >= numTimers) return; m_impl->timers.at(timerId)->start(); } // ------------------------------------------------------------------------------------------------- void MultiTimerWidget::stopTimer(int timerId) { if (timerId < 0 || 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(int timerId) const { if (timerId < 0 || timerId >= numTimers) return false; return m_impl->timers.at(timerId)->timerRunning(); } // ------------------------------------------------------------------------------------------------- void MultiTimerWidget::setTimerValue(int timerId, int seconds) { if (timerId < 0 || timerId >= numTimers) return; m_impl->timers.at(timerId)->setValueSeconds(seconds); } // ------------------------------------------------------------------------------------------------- int MultiTimerWidget::timerValue(int timerId) const { if (timerId < 0 || 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)) { m_sbLength->setRange(0, 10); m_sbIntensity->setRange(25, 255); 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) { if (sdc->type() == ConnectionType::Hidraw && sdc->mode() == ConnectionMode::ReadWrite) m_subDeviceConnection = sdc; } // ------------------------------------------------------------------------------------------------- void VibrationSettingsWidget::sendVibrateCommand() { if (!m_subDeviceConnection) return; if ((m_subDeviceConnection->flags() & DeviceFlag::Vibrate) != DeviceFlag::Vibrate) return; if (!m_subDeviceConnection->isConnected()) return; // TODO generalize features and protocol for proprietary device features like vibration // for not only the Spotlight device. // // Spotlight: // len intensity // unsigned char vibrate[] = {0x10, 0x01, 0x09, 0x1a, 0x00, 0xe8, 0x80}; const uint8_t vlen = m_sbLength->value(); const uint8_t vint = m_sbIntensity->value(); const uint8_t vibrateCmd[] = {0x10, 0x01, 0x09, 0x1a, vlen, 0xe8, vint}; const auto res = m_subDeviceConnection->sendData(vibrateCmd, sizeof(vibrateCmd)); if (res != sizeof(vibrateCmd)) { logWarn(device) << "Could not write vibrate command to device socket."; } } Projecteur-0.9.2/src/device-vibration.h000066400000000000000000000045021412537212300200370ustar00rootroot00000000000000// 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 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 int timerCount() const; void setTimerEnabled(int timerId, bool enabled); bool timerEnabled(int timerId) const; void startTimer(int timerId); void stopTimer(int timerId); void stopAllTimers(); bool timerRunning(int timerId) const; void setTimerValue(int timerId, int seconds); int timerValue(int timerId) const; signals: /// Emitted when a timer times out. void timeout(int timerId); void timerEnabledChanged(int timerId, bool enabled); void timerValueChanged(int timerId, int seconds); 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.9.2/src/device.cc000066400000000000000000000311521412537212300162030ustar00rootroot00000000000000// 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 "logging.h" #include #include #include #include LOGGING_CATEGORY(device, "device") LOGGING_CATEGORY(hid, "HID") namespace { // ----------------------------------------------------------------------------------------------- static const auto registeredComparator_ = QMetaType::registerComparators(); const auto hexId = logging::hexId; } // ------------------------------------------------------------------------------------------------- DeviceConnection::DeviceConnection(const DeviceId& id, const QString& name, std::shared_ptr vdev) : m_deviceId(id), m_deviceName(name), m_inputMapper(std::make_shared(std::move(vdev))){} // ------------------------------------------------------------------------------------------------- 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(); 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; } // ------------------------------------------------------------------------------------------------- SubDeviceConnection::SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode) : m_details(path, type, mode) {} // ------------------------------------------------------------------------------------------------- SubDeviceConnection::~SubDeviceConnection() = default; // ------------------------------------------------------------------------------------------------- bool SubDeviceConnection::isConnected() const { if (type() == ConnectionType::Event) return (m_readNotifier && m_readNotifier->isEnabled()); if (type() == ConnectionType::Hidraw) return (m_readNotifier && m_readNotifier->isEnabled()) && (m_writeNotifier); return false; } // ------------------------------------------------------------------------------------------------- void SubDeviceConnection::disconnect() { m_readNotifier.reset(); m_writeNotifier.reset(); } // ------------------------------------------------------------------------------------------------- void SubDeviceConnection::disable() { if (m_readNotifier) m_readNotifier->setEnabled(false); if (m_writeNotifier) m_writeNotifier->setEnabled(false); } // ------------------------------------------------------------------------------------------------- void SubDeviceConnection::disableWrite() { if (m_writeNotifier) m_writeNotifier->setEnabled(false); } // ------------------------------------------------------------------------------------------------- void SubDeviceConnection::enableWrite() { if (m_writeNotifier) m_writeNotifier->setEnabled(true); } // ------------------------------------------------------------------------------------------------- const std::shared_ptr& SubDeviceConnection::inputMapper() const { return m_inputMapper; } // ------------------------------------------------------------------------------------------------- QSocketNotifier* SubDeviceConnection::socketReadNotifier() { return m_readNotifier.get(); } // ------------------------------------------------------------------------------------------------- QSocketNotifier* SubDeviceConnection::socketWriteNotifier() { return m_writeNotifier.get(); } // ------------------------------------------------------------------------------------------------- SubEventConnection::SubEventConnection(Token, const QString& path) : SubDeviceConnection(path, ConnectionType::Event, ConnectionMode::ReadOnly) {} // ------------------------------------------------------------------------------------------------- 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{}, sd.deviceFile); 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()->virtualDevice()) { 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, notifier]() { if (grabbed) { ioctl(static_cast(notifier->socket()), EVIOCGRAB, 0); } ::close(static_cast(notifier->socket())); }); connection->m_inputMapper = dc.inputMapper(); connection->m_details.phys = sd.phys; return connection; } // ------------------------------------------------------------------------------------------------- SubHidrawConnection::SubHidrawConnection(Token, const QString& path) : SubDeviceConnection(path, ConnectionType::Hidraw, ConnectionMode::ReadWrite) {} // ------------------------------------------------------------------------------------------------- std::shared_ptr SubHidrawConnection::create(const DeviceScan::SubDevice& sd, const DeviceConnection& dc) { const int devfd = ::open(sd.deviceFile.toLocal8Bit().constData(), O_RDWR|O_NONBLOCK , 0); if (devfd == -1) { logWarn(device) << tr("Cannot open hidraw device '%1' for read/write.").arg(sd.deviceFile); return std::shared_ptr(); } int descriptorSize = 0; // Get Report Descriptor Size if (ioctl(devfd, HIDIOCGRDESCSIZE, &descriptorSize) < 0) { logWarn(device) << tr("Cannot retrieve report descriptor size of hidraw device '%1'.").arg(sd.deviceFile); return std::shared_ptr(); } struct hidraw_report_descriptor reportDescriptor{}; reportDescriptor.size = descriptorSize; if (ioctl(devfd, HIDIOCGRDESC, &reportDescriptor) < 0) { logWarn(device) << tr("Cannot retrieve report descriptor of hidraw device '%1'.").arg(sd.deviceFile); return std::shared_ptr(); } struct hidraw_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); return std::shared_ptr(); }; // Check against given device id if (static_cast(devinfo.vendor) != dc.deviceId().vendorId || static_cast(devinfo.product) != dc.deviceId().productId) { ::close(devfd); logDebug(device) << tr("Device id mismatch: %1 (%2:%3)") .arg(sd.deviceFile, hexId(devinfo.vendor), hexId(devinfo.product)); return std::shared_ptr(); } auto connection = std::make_shared(Token{}, sd.deviceFile); fcntl(devfd, F_SETFL, fcntl(devfd, F_GETFL, 0) | O_NONBLOCK); if ((fcntl(devfd, F_GETFL, 0) & O_NONBLOCK) == O_NONBLOCK) { connection->m_details.deviceFlags |= DeviceFlag::NonBlocking; } // For now vibration is only supported for the Logitech Spotlight (USB) // TODO A more generic approach if (dc.deviceId().vendorId == 0x46d && dc.deviceId().productId == 0xc53e) { connection->m_details.deviceFlags |= DeviceFlag::Vibrate; } // Create read and write socket notifiers connection->m_readNotifier = std::make_unique(devfd, QSocketNotifier::Read); QSocketNotifier* const readNotifier = connection->m_readNotifier.get(); // Auto clean up and close descriptor on destruction of notifier connect(readNotifier, &QSocketNotifier::destroyed, [readNotifier]() { ::close(static_cast(readNotifier->socket())); }); connection->m_writeNotifier = std::make_unique(devfd, QSocketNotifier::Write); QSocketNotifier* const writeNotifier = connection->m_writeNotifier.get(); // Auto clean up and close descriptor on destruction of notifier connect(writeNotifier, &QSocketNotifier::destroyed, [writeNotifier]() { ::close(static_cast(writeNotifier->socket())); }); connection->m_details.phys = sd.phys; connection->disableWrite(); // disable write notifier return connection; } // ------------------------------------------------------------------------------------------------- ssize_t SubDeviceConnection::sendData(const QByteArray& hidppMsg) { ssize_t res = -1; bool isValidMsg = (hidppMsg.length() == 7 && hidppMsg.at(0) == 0x10); // HID++ short message isValidMsg = isValidMsg || (hidppMsg.length() == 20 && hidppMsg.at(0) == 0x11); // HID++ long message if (type() == ConnectionType::Hidraw && mode() == ConnectionMode::ReadWrite && m_writeNotifier && isValidMsg) { enableWrite(); const auto notifier = socketWriteNotifier(); res = ::write(notifier->socket(), hidppMsg.data(), hidppMsg.length()); logDebug(hid) << "Write" << hidppMsg.toHex() << "to" << path(); disableWrite(); } return res; } // ------------------------------------------------------------------------------------------------- ssize_t SubDeviceConnection::sendData(const void* hidppMsg, size_t hidppMsgLen) { const QByteArray hidppMsgArr(reinterpret_cast(hidppMsg), hidppMsgLen); return sendData(hidppMsgArr); } Projecteur-0.9.2/src/device.h000066400000000000000000000151171412537212300160500ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #pragma once #include "enum-helper.h" #include #include #include // ------------------------------------------------------------------------------------------------- struct DeviceId { uint16_t vendorId = 0; uint16_t productId = 0; QString phys; // should be sufficient to differentiate between two devices of the same type // - not tested, don't have two devices of any type currently. inline bool operator==(const DeviceId& rhs) const { return std::tie(vendorId, productId, phys) == std::tie(rhs.vendorId, rhs.productId, rhs.phys); } inline bool operator!=(const DeviceId& rhs) const { return std::tie(vendorId, productId, phys) != std::tie(rhs.vendorId, rhs.productId, rhs.phys); } inline bool operator<(const DeviceId& rhs) const { return std::tie(vendorId, productId, phys) < std::tie(rhs.vendorId, rhs.productId, rhs.phys); } }; Q_DECLARE_METATYPE(DeviceId); // ------------------------------------------------------------------------------------------------- class InputMapper; class QSocketNotifier; class SubDeviceConnection; class VirtualDevice; // ----------------------------------------------------------------------------------------------- enum class ConnectionType : uint8_t { Event, Hidraw }; enum class ConnectionMode : uint8_t { ReadOnly, WriteOnly, ReadWrite }; // ------------------------------------------------------------------------------------------------- class DeviceConnection : public QObject { Q_OBJECT public: DeviceConnection(const DeviceId& id, const QString& name, std::shared_ptr vdev); ~DeviceConnection(); const auto& deviceName() const { return m_deviceName; } const auto& deviceId() const { return m_deviceId; } const auto& inputMapper() const { return m_inputMapper; } 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; } signals: void subDeviceConnected(const DeviceId& id, const QString& path); void subDeviceDisconnected(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, Vibrate = 1 << 16, }; ENUM(DeviceFlag, DeviceFlags) // ----------------------------------------------------------------------------------------------- struct SubDeviceConnectionDetails { SubDeviceConnectionDetails(const QString& path, ConnectionType type, ConnectionMode mode) : type(type), mode(mode), devicePath(path) {} ConnectionType type; ConnectionMode mode; bool grabbed = false; DeviceFlags deviceFlags = DeviceFlags::NoFlags; QString phys; 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 { Q_OBJECT public: virtual ~SubDeviceConnection() = 0; bool isConnected() const; void disconnect(); // destroys socket notifier and close file handle void disable(); // disable receiving/sending data void disableWrite(); // disable sending data void enableWrite(); // enable sending data ssize_t sendData(const QByteArray& hidppMsg); // Send HID++ Message to HIDraw connection ssize_t sendData(const void* hidppMsg, size_t hidppMsgLen); // Send HID++ Message to HIDraw connection auto type() const { return m_details.type; }; auto mode() const { return m_details.mode; }; auto isGrabbed() const { return m_details.grabbed; }; auto flags() const { return m_details.deviceFlags; }; const auto& phys() const { return m_details.phys; }; const auto& path() const { return m_details.devicePath; }; const std::shared_ptr& inputMapper() const; QSocketNotifier* socketReadNotifier(); // Read notifier for Hidraw and Event connections for receiving data from device QSocketNotifier* socketWriteNotifier(); // Write notifier for Hidraw connection for sending data to device protected: SubDeviceConnection(const QString& path, ConnectionType type, ConnectionMode mode); SubDeviceConnectionDetails m_details; std::shared_ptr m_inputMapper; // shared input mapper from parent device. std::unique_ptr m_readNotifier; std::unique_ptr m_writeNotifier; // only useful for Hidraw connections }; // ------------------------------------------------------------------------------------------------- namespace DeviceScan { struct SubDevice; } // ------------------------------------------------------------------------------------------------- class SubEventConnection : public SubDeviceConnection { Q_OBJECT class Token{}; public: static std::shared_ptr create(const DeviceScan::SubDevice& sd, const DeviceConnection& dc); SubEventConnection(Token, const QString& path); auto& inputBuffer() { return m_inputEventBuffer; } protected: InputBuffer<12> m_inputEventBuffer; }; // ------------------------------------------------------------------------------------------------- class SubHidrawConnection : public SubDeviceConnection { Q_OBJECT class Token{}; public: static std::shared_ptr create(const DeviceScan::SubDevice& sd, const DeviceConnection& dc); SubHidrawConnection(Token, const QString& path); }; Projecteur-0.9.2/src/deviceinput.cc000066400000000000000000000631141412537212300172660ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #include "deviceinput.h" #include "logging.h" #include "settings.h" #include "virtualdevice.h" #include #include #include #include #include LOGGING_CATEGORY(input, "input") namespace { // ----------------------------------------------------------------------------------------------- static auto registered_ = qRegisterMetaTypeStreamOperators() && qRegisterMetaTypeStreamOperators(); // ----------------------------------------------------------------------------------------------- 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(); } } // ------------------------------------------------------------------------------------------------- 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 &d) { QDebugStateSaver saver(debug); debug.nospace() << '{' << d.type << ", " << d.code << ", " << d.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; } // ------------------------------------------------------------------------------------------------- QDataStream& operator>>(QDataStream& s, MappedAction& mia) { std::underlying_type_t type; s >> type; switch (static_cast(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); } 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())); } return false; } // ------------------------------------------------------------------------------------------------- QDataStream& operator<<(QDataStream& s, const MappedAction& mia) { s << static_cast>(mia.action->type()); return mia.action->save(s); } // ------------------------------------------------------------------------------------------------- namespace { struct KeyEventItem { KeyEventItem(KeyEvent ke = {}) : keyEvent(std::move(ke)) {} const KeyEvent keyEvent; std::shared_ptr action; std::vector nextMap; }; struct DeviceKeyMap { 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.size(); } private: std::list m_items; KeyEventItem m_rootItem; const KeyEventItem* m_pos = &m_rootItem; }; } // ------------------------------------------------------------------------------------------------- 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 && !m_pos->action->empty()) { 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) { 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); }); if (it != current->nextMap.cend()) { previous = current; current = *it; } else { // Create new item if not found m_items.emplace_back(KeyEventItem{keyEvent}); previous = current; current = &m_items.back(); // link previous to current previous->nextMap.push_back(current); } // if last item in key event set if (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(", "); seqString += toString(m_keySequence[i], (i < m_nativeModifiers.size()) ? m_nativeModifiers[i] : (uint16_t)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] : (uint16_t)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; } // ------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------- struct InputMapper::Impl { Impl(InputMapper* parent, std::shared_ptr vdev); 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); InputMapper* m_parent = nullptr; std::shared_ptr m_vdev; // can be a nullptr if application is started without uinput QTimer* m_seqTimer = nullptr; DeviceKeyMap m_keymap; std::pair m_lastState; std::vector m_events; InputMapConfig m_config; bool m_recordingMode = false; }; // ------------------------------------------------------------------------------------------------- InputMapper::Impl::Impl(InputMapper* parent, std::shared_ptr vdev) : m_parent(parent) , m_vdev(std::move(vdev)) , m_seqTimer(new QTimer(parent)) { m_seqTimer->setSingleShot(true); m_seqTimer->setInterval(250); connect(m_seqTimer, &QTimer::timeout, parent, [this](){ sequenceTimeout(); }); } // ------------------------------------------------------------------------------------------------- void InputMapper::Impl::execAction(const std::shared_ptr& action, DeviceKeyMap::Result r) { if (!action) return; logDebug(input) << "Input map action, type = " << int(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 (m_vdev && m_events.size()) { m_vdev->emitEvents(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 (m_vdev && m_events.size()) { m_vdev->emitEvents(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_vdev) 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_vdev->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); } // ------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------- InputMapper::InputMapper(std::shared_ptr virtualDevice, QObject* parent) : QObject(parent) , impl(std::make_unique(this, std::move(virtualDevice))) { } // ------------------------------------------------------------------------------------------------- InputMapper::~InputMapper() { } // ------------------------------------------------------------------------------------------------- std::shared_ptr InputMapper::virtualDevice() const { return impl->m_vdev; } // ------------------------------------------------------------------------------------------------- bool InputMapper::hasVirtualDevice() const { return !!(impl->m_vdev); } // ------------------------------------------------------------------------------------------------- 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 || (!impl->m_vdev)) return; // If no key mapping is configured ... if (!impl->m_recordingMode && !impl->m_keymap.hasConfig()) { if (impl->m_vdev) { // ... forward events to virtual device if it exists... impl->m_vdev->emitEvents(input_events, num); } // ... end return return; } if (input_events[num-1].type != EV_SYN) { logWarning(input) << tr("Input mapper expects events separated by SYN event."); return; } else if (num == 1) { 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 if (res == DeviceKeyMap::Result::Miss) { // key sequence miss, send all buffered events so far + current event impl->m_seqTimer->stop(); if (impl->m_vdev) { if (impl->m_events.size()) { impl->m_vdev->emitEvents(impl->m_events); impl->m_events.resize(0); } impl->m_vdev->emitEvents(input_events, num); } impl->m_keymap.resetState(); } else if (res == DeviceKeyMap::Result::Valid) { // KeyEvent is part of valid key sequence. impl->m_lastState = std::make_pair(res, impl->m_keymap.state()); impl->m_seqTimer->start(); if (impl->m_vdev) { impl->m_events.reserve(impl->m_events.size() + num); std::copy(input_events, input_events + num, std::back_inserter(impl->m_events)); } } else if (res == DeviceKeyMap::Result::Hit) { // Found a valid key sequence impl->m_seqTimer->stop(); if (impl->m_vdev) { if (const auto pos = impl->m_keymap.state()) { impl->execAction(pos->action, DeviceKeyMap::Result::Hit); } else { if (impl->m_events.size()) impl->m_vdev->emitEvents(impl->m_events); impl->m_vdev->emitEvents(input_events, num); } } impl->resetState(); } else if (res == DeviceKeyMap::Result::PartialHit) { // Found a valid key sequence, but are still more valid sequences possible -> start timer impl->m_lastState = std::make_pair(res, impl->m_keymap.state()); impl->m_seqTimer->start(); if (impl->m_vdev) { impl->m_events.reserve(impl->m_events.size() + num); std::copy(input_events, input_events + num, std::back_inserter(impl->m_events)); } } } // ------------------------------------------------------------------------------------------------- void InputMapper::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; } Projecteur-0.9.2/src/deviceinput.h000066400000000000000000000205221412537212300171240ustar00rootroot00000000000000// 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; // ------------------------------------------------------------------------------------------------- /// 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); // ------------------------------------------------------------------------------------------------- /// KeyEvent is a sequence of DeviceInputEvent. using KeyEvent = std::vector; /// KeyEventSequence is a sequence of KeyEvents. using KeyEventSequence = std::vector; Q_DECLARE_METATYPE(KeyEventSequence); // ------------------------------------------------------------------------------------------------- template QDataStream& operator<<(QDataStream& s, const std::vector& container) { 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; } // ------------------------------------------------------------------------------------------------- QDebug operator<<(QDebug debug, const DeviceInputEvent &ie); QDebug operator<<(QDebug debug, const KeyEvent &ke); // ------------------------------------------------------------------------------------------------- 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, }; virtual Type type() const = 0; virtual QDataStream& save(QDataStream&) const = 0; virtual QDataStream& load(QDataStream&) = 0; virtual bool empty() const = 0; }; // ------------------------------------------------------------------------------------------------- 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 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 virtualDevice, 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); bool recordingMode() const; void setRecordingMode(bool recording); int keyEventInterval() const; void setKeyEventInterval(int interval); std::shared_ptr virtualDevice() 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.9.2/src/devicescan.cc000066400000000000000000000301441412537212300170500ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #include "devicescan.h" #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.size() ? ids[0].toUShort(nullptr, 16) : 0; switch (busType) { case BUS_USB: spotlightDevice.busType = DeviceScan::Device::BusType::Usb; break; case BUS_BLUETOOTH: spotlightDevice.busType = DeviceScan::Device::BusType::Bluetooth; break; } 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; } } 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)); } } // For now: only check for hidraw sub-devices that have support for custom "proprietary" // functionality/protocol with Projecteur built in. // TODO check if _Projecteur_ supports additional "proprietary" device protocol features.. if (eventSubDeviceCount > 0) continue; // 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; } } Projecteur-0.9.2/src/devicescan.h000066400000000000000000000027321412537212300167140ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #pragma once #include "device.h" #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 enum class BusType : uint16_t { Unknown, Usb, Bluetooth }; const QString& getName() const { return userName.size() ? userName : name; } QString name; QString userName; DeviceId id; BusType busType = BusType::Unknown; std::vector subDevices; }; 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.9.2/src/deviceswidget.cc000066400000000000000000000315661412537212300176030ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #include "deviceswidget.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 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 } // ------------------------------------------------------------------------------------------------- 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); }); } // ------------------------------------------------------------------------------------------------- const DeviceId DevicesWidget::currentDeviceId() const { if (m_devicesCombo->currentIndex() < 0) return invalidDeviceId; return qvariant_cast(m_devicesCombo->currentData()); } // ------------------------------------------------------------------------------------------------- QWidget* DevicesWidget::createTimerTabWidget(Settings* settings, Spotlight* spotlight) { Q_UNUSED(settings); Q_UNUSED(spotlight); const auto w = new QWidget(this); const auto layout = new QVBoxLayout(w); const auto timerWidget = new MultiTimerWidget(this); m_vibrationSettingsWidget = new VibrationSettingsWidget(this); layout->addWidget(timerWidget); layout->addWidget(m_vibrationSettingsWidget); auto loadSettings = [this, settings, timerWidget](const DeviceId& dId) { for (int i = 0; i < timerWidget->timerCount(); ++i) { const auto ts = settings->timerSettings(dId, i); timerWidget->setTimerEnabled(i, ts.first); timerWidget->setTimerValue(i, ts.second); } const auto vs = settings->vibrationSettings(dId); m_vibrationSettingsWidget->setLength(vs.first); m_vibrationSettingsWidget->setIntensity(vs.second); }; loadSettings(currentDeviceId()); connect(this, &DevicesWidget::currentDeviceChanged, this, [loadSettings=std::move(loadSettings), timerWidget, this](const DeviceId& dId) { timerWidget->stopAllTimers(); timerWidget->blockSignals(true); m_vibrationSettingsWidget->blockSignals(true); loadSettings(dId); m_vibrationSettingsWidget->blockSignals(false); timerWidget->blockSignals(false); }); connect(timerWidget, &MultiTimerWidget::timerValueChanged, this, [timerWidget, settings, this](int id, int secs) { settings->setTimerSettings(currentDeviceId(), id, timerWidget->timerEnabled(id), secs); }); connect(timerWidget, &MultiTimerWidget::timerEnabledChanged, this, [timerWidget, settings, this](int id, bool enabled) { settings->setTimerSettings(currentDeviceId(), id, enabled, timerWidget->timerValue(id)); }); connect(m_vibrationSettingsWidget, &VibrationSettingsWidget::intensityChanged, this, [settings, this](uint8_t intensity) { settings->setVibrationSettings(currentDeviceId(), m_vibrationSettingsWidget->length(), intensity); }); connect(m_vibrationSettingsWidget, &VibrationSettingsWidget::lengthChanged, this, [settings, this](uint8_t len) { settings->setVibrationSettings(currentDeviceId(), len, m_vibrationSettingsWidget->intensity()); }); connect(timerWidget, &MultiTimerWidget::timeout, m_vibrationSettingsWidget, &VibrationSettingsWidget::sendVibrateCommand); 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); const auto tabWidget = new QTabWidget(dw); vLayout->addWidget(tabWidget); tabWidget->addTab(createInputMapperWidget(settings, spotlight), tr("Input Mapping")); auto vibrateConn = [spotlight](const DeviceId& devId) { const auto currentConn = spotlight->deviceConnection(devId); if (currentConn) { for (const auto& item : currentConn->subDevices()) { if ((item.second->flags() & DeviceFlag::Vibrate) == DeviceFlag::Vibrate) return item.second; } } return std::shared_ptr{}; }; if (const auto conn = vibrateConn(currentDeviceId())) { m_timerTabWidget = createTimerTabWidget(settings, spotlight); tabWidget->addTab(m_timerTabWidget, tr("Vibration Timer")); m_vibrationSettingsWidget->setSubDeviceConnection(conn.get()); } connect(this, &DevicesWidget::currentDeviceChanged, this, [vibrateConn=std::move(vibrateConn), tabWidget, settings, spotlight, this] (const DeviceId& devId) { if (const auto conn = vibrateConn(devId)) { if (m_timerTabWidget == nullptr) { m_timerTabWidget = createTimerTabWidget(settings, spotlight); } if (tabWidget->indexOf(m_timerTabWidget) < 0) { tabWidget->addTab(m_timerTabWidget, tr("Vibration Timer")); } m_vibrationSettingsWidget->setSubDeviceConnection(conn.get()); } else if (m_timerTabWidget) { const auto idx = tabWidget->indexOf(m_timerTabWidget); if (idx >= 0) tabWidget->removeTab(idx); m_vibrationSettingsWidget->setSubDeviceConnection(nullptr); } }); return dw; } // ------------------------------------------------------------------------------------------------- QWidget* DevicesWidget::createDeviceInfoWidget(Spotlight* /*spotlight*/) { const auto diWidget = new QWidget(this); const auto layout = new QHBoxLayout(diWidget); layout->addStretch(1); layout->addWidget(new QLabel(tr("Not yet implemented"), this)); layout->addStretch(1); diWidget->setDisabled(true); 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, 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)](){ imModel->setInputMapper(m_inputMapper); if (m_inputMapper) { intervalSb->setValue(m_inputMapper->keyEventInterval()); imModel->setConfiguration(m_inputMapper->configuration()); } 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; } Projecteur-0.9.2/src/deviceswidget.h000066400000000000000000000022631412537212300174350ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #pragma once #include #include struct DeviceId; class InputMapper; class QComboBox; class Settings; class Spotlight; class VibrationSettingsWidget; // ------------------------------------------------------------------------------------------------- class DevicesWidget : public QWidget { Q_OBJECT public: explicit DevicesWidget(Settings* settings, Spotlight* spotlight, QWidget* parent = nullptr); const 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); QWidget* createTimerTabWidget(Settings* settings, Spotlight* spotlight); QComboBox* m_devicesCombo = nullptr; QWidget* m_timerTabWidget = nullptr; VibrationSettingsWidget* m_vibrationSettingsWidget = nullptr; QPointer m_inputMapper; }; Projecteur-0.9.2/src/enum-helper.h000066400000000000000000000026641412537212300170350ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #pragma once // ------------------------------------------------------------------------------------------------- #define EXPAND_( x ) x // MSVC workaround #define GET_ENUM_MACRO(_1,_2,NAME,...) NAME #define ENUM(...) EXPAND_(GET_ENUM_MACRO(__VA_ARGS__, ENUM2, ENUM1)(__VA_ARGS__)) // enum flags macro (cannot be used inside class declaration) #define ENUM1(ENUMCLASS) \ inline ENUMCLASS operator|(ENUMCLASS lhs, ENUMCLASS rhs) { \ using T = std::underlying_type_t; \ return static_cast(static_cast(lhs) | static_cast(rhs)); } \ inline ENUMCLASS operator&(ENUMCLASS lhs, ENUMCLASS rhs) { \ using T = std::underlying_type_t; \ return static_cast(static_cast(lhs) & static_cast(rhs)); } \ inline ENUMCLASS operator~(ENUMCLASS lhs) { \ using T = std::underlying_type_t; \ return static_cast(~static_cast(lhs)); } \ inline ENUMCLASS& operator |= (ENUMCLASS& lhs, ENUMCLASS rhs) {lhs = lhs | rhs; return lhs; } \ inline ENUMCLASS& operator &= (ENUMCLASS& lhs, ENUMCLASS rhs) {lhs = lhs & rhs; return lhs; } \ inline bool operator!(ENUMCLASS e) { return e == static_cast(0); } // enum flags macro (cannot be used inside class declaration) #define ENUM2(ENUMCLASS, PLURALNAME) \ ENUM1(ENUMCLASS); \ using PLURALNAME = ENUMCLASS; Projecteur-0.9.2/src/extra-devices.cc.in000066400000000000000000000021671412537212300201200ustar00rootroot00000000000000// 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@ }; } // 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.9.2/src/iconwidgets.cc000066400000000000000000000030661412537212300172660ustar00rootroot00000000000000// 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; } // ------------------------------------------------------------------------------------------------- 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.9.2/src/iconwidgets.h000066400000000000000000000012421412537212300171220ustar00rootroot00000000000000// 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 // ------------------------------------------------------------------------------------------------- class IconButton : public QToolButton { Q_OBJECT public: IconButton(Font::Icon symbol, QWidget* parent = nullptr); }; // ------------------------------------------------------------------------------------------------- class IconLabel : public QLabel { Q_OBJECT public: IconLabel(Font::Icon symbol, QWidget* parent = nullptr); void setPixelSize(int pixelSize); }; Projecteur-0.9.2/src/imageitem.cc000066400000000000000000000013331412537212300167030ustar00rootroot00000000000000// 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.9.2/src/imageitem.h000066400000000000000000000011171412537212300165450ustar00rootroot00000000000000// 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.9.2/src/inputmapconfig.cc000066400000000000000000000303541412537212300177720ustar00rootroot00000000000000// 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_; } // ------------------------------------------------------------------------------------------------- InputMapConfigModel::InputMapConfigModel(QObject* parent) : InputMapConfigModel(nullptr, parent) {} // ------------------------------------------------------------------------------------------------- InputMapConfigModel::InputMapConfigModel(InputMapper* im, QObject* parent) : QAbstractTableModel(parent) , 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) return (QAbstractTableModel::flags(index) | Qt::ItemIsEditable); else if (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"); } } 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; 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()]; // TODO if action is currently not a keysequence action.. -> just change action type? 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; } 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.size() == 0) 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(); } // ------------------------------------------------------------------------------------------------- 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); } } } #include // ------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------- 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, actionDelegate](const QPoint& pos) { const auto idx = indexAt(pos); if (!idx.isValid()) return; if (idx.column() == InputMapConfigModel::ActionCol) { actionDelegate->actionContextMenu(this, qobject_cast(model()), idx, this->viewport()->mapToGlobal(pos)); } else if (idx.column() == InputMapConfigModel::ActionTypeCol) { m_actionTypeDelegate->actionContextMenu(this, qobject_cast(model()), idx, this->viewport()->mapToGlobal(pos)); } }); 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.9.2/src/inputmapconfig.h000066400000000000000000000051531412537212300176330ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #pragma once #include "deviceinput.h" #include #include #include // ------------------------------------------------------------------------------------------------- class ActionTypeDelegate; // ------------------------------------------------------------------------------------------------- struct InputMapModelItem { KeyEventSequence deviceSequence; std::shared_ptr action; bool isDuplicate = false; }; // ------------------------------------------------------------------------------------------------- class InputMapConfigModel : public QAbstractTableModel { Q_OBJECT public: enum Roles { InputSeqRole = Qt::UserRole + 1, ActionTypeRole, NativeSeqRole }; enum Columns { InputSeqCol = 0, ActionTypeCol, ActionCol, ColumnsCount}; InputMapConfigModel(QObject* parent = nullptr); InputMapConfigModel(InputMapper* im, QObject* parent = nullptr); 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); private: void configureInputMapper(); void removeConfigItemRows(int fromRow, int toRow); void updateDuplicates(); QPointer m_inputMapper; QVector m_configItems; std::map m_duplicates; }; // ------------------------------------------------------------------------------------------------- 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.9.2/src/inputseqedit.cc000066400000000000000000000407021412537212300174630ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #include "inputseqedit.h" #include "deviceinput.h" #include "inputmapconfig.h" #include "logging.h" #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, bool buttonTap = false) { if (ke.empty()) return 0; static auto const pressChar = QChar(0x2193); // ↓ static auto const releaseChar = QChar(0x2191); // ↑ // TODO some devices (e.g. August WP 200) have buttons that send a key combination // (modifiers + key) - this is ignored completely right now. const auto text = QString("[%1%2%3") .arg(ke.back().code, 0, 16) .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, 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, isTap); } return sequenceWidth; } } // ------------------------------------------------------------------------------------------------- InputSeqEdit::InputSeqEdit(QWidget* parent) : InputSeqEdit(nullptr, parent) {} // ------------------------------------------------------------------------------------------------- InputSeqEdit::InputSeqEdit(InputMapper* im, QWidget* parent) : QWidget(parent) { 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() { } // ------------------------------------------------------------------------------------------------- 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(); return (style()->sizeFromContents(QStyle::CT_LineEdit, &option, QSize(w, h). expandedTo(QApplication::globalStrut()), this)); } // ------------------------------------------------------------------------------------------------- void InputSeqEdit::paintEvent(QPaintEvent*) { 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()) { xPos += drawPlaceHolderText(xPos, p, option, tr("Press device button(s)...")) + spacingX; } else { xPos += drawKeyEventSequence(xPos, p, option, m_recordedSequence, false); } } else { xPos += drawKeyEventSequence(xPos, p, option, m_inputSequence); } } // ------------------------------------------------------------------------------------------------- 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.size() == 0) 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; } else 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; 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) { const auto r = QRect(QPoint(startX + option.rect.left(), option.rect.top()), option.rect.bottomRight()); p.save(); p.setPen(option.palette.color(QPalette::Disabled, QPalette::Text)); QRect br; p.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, text, &br); p.restore(); return br.width(); } // ------------------------------------------------------------------------------------------------- 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 = (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; drawKeyEventSequence(xPos, *painter, option, imModel->configData(index).deviceSequence); 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(), 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 calc size hint from KeyEventSequence..... return QStyledItemDelegate::sizeHint(option, index); } return QStyledItemDelegate::sizeHint(option, index); } Projecteur-0.9.2/src/inputseqedit.h000066400000000000000000000051111412537212300173200ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #pragma once #include "deviceinput.h" #include #include // ------------------------------------------------------------------------------------------------- class QStyleOptionFrame; // ------------------------------------------------------------------------------------------------- class InputSeqEdit : public QWidget { Q_OBJECT public: InputSeqEdit(QWidget* parent = nullptr); InputSeqEdit(InputMapper* im, QWidget* parent = nullptr); ~InputSeqEdit(); QSize sizeHint() const override; void setInputMapper(InputMapper* im); 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 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: 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; static void drawCurrentIndicator(QPainter &p, const QStyleOption& option); private: void commitAndCloseEditor(InputSeqEdit* editor); }; Projecteur-0.9.2/src/linuxdesktop.cc000066400000000000000000000112431412537212300174740ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #include "linuxdesktop.h" #include "logging.h" #include #include #include #include #include #include #if HAS_Qt5_DBus #include #include #endif LOGGING_CATEGORY(desktop, "desktop") namespace { #if HAS_Qt5_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_Qt5_DBus // ----------------------------------------------------------------------------------------------- QPixmap grabScreenVirtualDesktop(QScreen* screen) { QRect g; for (const auto s : QGuiApplication::screens()) { g = g.united(s->geometry()); } QPixmap pm(QApplication::primaryScreen()->grabWindow( QApplication::desktop()->winId(), g.x(), g.y(), g.width(), g.height())); 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_Qt5_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.9.2/src/linuxdesktop.h000066400000000000000000000011171412537212300173350ustar00rootroot00000000000000// 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.9.2/src/logging.cc000066400000000000000000000157101412537212300163740ustar00rootroot00000000000000// 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(unsigned short id) { return QString("%1").arg(id, 4, 16, QChar('0')); } } Projecteur-0.9.2/src/logging.h000066400000000000000000000044171412537212300162400ustar00rootroot00000000000000// 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(unsigned short id); } Projecteur-0.9.2/src/main.cc000066400000000000000000000341661412537212300157000ustar00rootroot00000000000000// 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 #define XSTRINGIFY(s) STRINGIFY(s) #define STRINGIFY(x) #x LOGGING_CATEGORY(appMain, "main") namespace { 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; } }; } int main(int argc, char *argv[]) { QCoreApplication::setApplicationName("Projecteur"); QCoreApplication::setApplicationVersion(projecteur::version_string()); ProjecteurApplication::Options options; QStringList ipcCommands; { QCommandLineParser parser; parser.setApplicationDescription(Main::tr("Linux/X11 application for the Logitech Spotlight device.")); const QCommandLineOption versionOption(QStringList{ "v", "version"}, Main::tr("Print application version.")); const QCommandLineOption fullVersionOption(QStringList{ "f", "fullversion" }); const QCommandLineOption helpOption(QStringList{ "h", "help"}, Main::tr("Show command line usage.")); const QCommandLineOption fullHelpOption(QStringList{ "help-all"}, Main::tr("Show complete command line usage with all properties.")); const QCommandLineOption cfgFileOption(QStringList{ "cfg" }, Main::tr("Set custom config file."), "file"); const QCommandLineOption commandOption(QStringList{ "c", "command"}, Main::tr("Send command/property to a running instance."), "cmd"); const QCommandLineOption deviceInfoOption(QStringList{ "d", "device-scan"}, Main::tr("Print device-scan results.")); const QCommandLineOption logLvlOption(QStringList{ "l", "log-level" }, Main::tr("Set log level (dbg,inf,wrn,err)."), "lvl"); const QCommandLineOption disableUInputOption(QStringList{ "disable-uinput" }, Main::tr("Disable uinput support.")); const QCommandLineOption showDlgOnStartOption(QStringList{ "show-dialog" }, Main::tr("Show preferences dialog on start.")); const QCommandLineOption dialogMinOnlyOption(QStringList{ "m", "minimize-only" }, Main::tr("Only allow minimizing the dialog.")); const QCommandLineOption disableOverlayOption(QStringList{ "disable-overlay" }, Main::tr("Disable spotlight overlay completely.")); const QCommandLineOption additionalDeviceOption(QStringList{ "D", "additional-device"}, Main::tr("Additional accepted device; DEVICE = vendorId:productId\n" " " "e.g., -D 04b3:310c; e.g. -D 0x0c45:0x8101"), "device"); parser.addOptions({versionOption, helpOption, fullHelpOption, commandOption, cfgFileOption, fullVersionOption, deviceInfoOption, logLvlOption, disableUInputOption, showDlgOnStartOption, dialogMinOnlyOption, disableOverlayOption, additionalDeviceOption}); const QStringList args = [argc, &argv]() { const QStringList qtAppKeyValueOptions = { "-platform", "-platformpluginpath", "-platformtheme", "-plugin", "-display" }; const QStringList qtAppSingleOptions = {"-reverse"}; QStringList args; for (int i = 0; i < argc; ++i) { // Skip some default arguments supported by QtGuiApplication, we don't want to parse them // but they will get passed through to the ProjecteurApp. if (qtAppKeyValueOptions.contains(argv[i])) { ++i; } else if (qtAppSingleOptions.contains(argv[i])) { continue; } else { args.push_back(argv[i]); } } return args; }(); parser.process(args); if (parser.isSet(helpOption) || parser.isSet(fullHelpOption)) { print() << QCoreApplication::applicationName() << " " << projecteur::version_string() << std::endl; print() << "Usage: projecteur [OPTION]..." << std::endl; print() << ""; print() << " -h, --help " << helpOption.description(); print() << " --help-all " << fullHelpOption.description(); print() << " -v, --version " << versionOption.description(); print() << " --cfg FILE " << cfgFileOption.description(); print() << " -d, --device-scan " << deviceInfoOption.description(); print() << " -l, --log-level LEVEL " << logLvlOption.description(); print() << " -D DEVICE " << additionalDeviceOption.description(); if (parser.isSet(fullHelpOption)) { print() << " --disable-uinput " << disableUInputOption.description(); print() << " --show-dialog " << showDlgOnStartOption.description(); print() << " -m, --minimize-only " << dialogMinOnlyOption.description(); } print() << " -c COMMAND|PROPERTY " << commandOption.description() << std::endl; print() << ""; print() << " spot=[on|off|toggle] " << Main::tr("Turn spotlight on/off or toggle."); print() << " settings=[show|hide] " << Main::tr("Show/hide preferences dialog."); if (parser.isSet(fullHelpOption)) { print() << " preset=NAME " << Main::tr("Set a preset."); } print() << " quit " << Main::tr("Quit the running instance."); // Early return if the user not explicitly requested the full help if (!parser.isSet(fullHelpOption)) return 0; print() << "\n" << ""; // Helper function to get the range of valid values for a string property const auto getValues = [](const Settings::StringProperty& sp) -> QString { if (sp.type == Settings::StringProperty::Type::Integer || sp.type == Settings::StringProperty::Type::Double) { return QString("(%1 ... %2)").arg(sp.range[0].toString(), sp.range[1].toString()); } else if (sp.type == Settings::StringProperty::Type::Bool) { return "(false, true)"; } else if (sp.type == Settings::StringProperty::Type::Color) { return "(HTML-color; #RRGGBB)"; } else if (sp.type == Settings::StringProperty::Type::StringEnum) { QStringList values; for (const auto& v : sp.range) { values.push_back(v.toString()); } return QString("(%1)").arg(values.join(", ")); } return QString(); }; int maxPropertyStringLength = 0; const std::vector> propertiesList = [getValues=std::move(getValues), &maxPropertyStringLength]() { 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)), getValues(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; } return 0; } if (parser.isSet(additionalDeviceOption)) { for (auto& deviceValue : parser.values(additionalDeviceOption)) { const auto devAttribs = deviceValue.split(":"); const auto vendorId = devAttribs[0].toUShort(nullptr, 16); const auto productId = devAttribs[1].toUShort(nullptr, 16); if (vendorId == 0 || productId == 0) { error() << Main::tr("Invalid vendor/productId pair: ") << deviceValue; } else { const QString name = (devAttribs.size() >= 3) ? devAttribs[2] : ""; options.additionalDevices.push_back({vendorId, productId, false, name}); } } } if (parser.isSet(versionOption) || parser.isSet(fullVersionOption)) { print() << QCoreApplication::applicationName().toStdString() << " " << projecteur::version_string(); if (parser.isSet(fullVersionOption) || (std::string(projecteur::version_branch()) != "master" && std::string(projecteur::version_branch()) != "not-within-git-repo")) { // Not a build from master branch, print out additional information: print() << " - git-branch: " << projecteur::version_branch(); print() << " - git-hash: " << projecteur::version_fullhash(); } // Show if we have a build from modified sources if (projecteur::version_isdirty()) print() << " - dirty-flag: " << projecteur::version_isdirty(); // Additional useful information if (parser.isSet(fullVersionOption)) { print() << " - compiler: " << XSTRINGIFY(CXX_COMPILER_ID) << " " << XSTRINGIFY(CXX_COMPILER_VERSION); print() << " - qt-version: (build: " << QT_VERSION_STR << ", runtime: " << qVersion() << ")"; const auto result = DeviceScan::getDevices(options.additionalDevices); print() << " - device-scan: " << QString("(errors: %1, devices: %2 [readable: %3, writable: %4])") .arg(result.errorMessages.size()).arg(result.devices.size()) .arg(result.numDevicesReadable).arg(result.numDevicesWritable); } return 0; } if (parser.isSet(deviceInfoOption)) { const auto result = DeviceScan::getDevices(options.additionalDevices); print() << QCoreApplication::applicationName() << " " << projecteur::version_string() << "; " << Main::tr("device scan") << std::endl; for (const auto& errmsg : result.errorMessages) { print() << "** " << Main::tr("Error: ") << errmsg; } print() << (result.errorMessages.size() ? "\n" : "") << Main::tr(" * Found %1 supported devices. (%2 readable, %3 writable)") .arg(result.devices.size()).arg(result.numDevicesReadable).arg(result.numDevicesWritable); const auto busTypeToString = [](DeviceScan::Device::BusType type) -> QString { if (type == DeviceScan::Device::BusType::Usb) return "USB"; if (type == DeviceScan::Device::BusType::Bluetooth) return "Bluetooth"; return "unknown"; }; for (const auto& device : result.devices) { print() << "\n" << " +++ " << "name: '" << device.name << "'"; if (!device.userName.isEmpty()) { print() << " " << "userName: '" << device.userName << "'"; } const QStringList subDeviceList = [&device](){ QStringList subDeviceList; for (const auto& sd: device.subDevices) { if (sd.deviceFile.size()) subDeviceList.push_back(sd.deviceFile); } return subDeviceList; }(); const bool allReadable = std::all_of(device.subDevices.cbegin(), device.subDevices.cend(), [](const auto& subDevice){ return subDevice.deviceReadable; }); const bool allWriteable = std::all_of(device.subDevices.cbegin(), device.subDevices.cend(), [](const auto& subDevice){ return subDevice.deviceWritable; }); print() << " " << "vendorId: " << logging::hexId(device.id.vendorId); print() << " " << "productId: " << logging::hexId(device.id.productId); print() << " " << "phys: " << device.id.phys; print() << " " << "busType: " << busTypeToString(device.busType); print() << " " << "devices: " << subDeviceList.join(", "); print() << " " << "readable: " << (allReadable ? "true" : "false"); print() << " " << "writable: " << (allWriteable ? "true" : "false"); } return 0; } else if (parser.isSet(commandOption)) { ipcCommands = parser.values(commandOption); for (auto& value : ipcCommands) { value = value.trimmed(); } ipcCommands.removeAll(""); if (ipcCommands.isEmpty()) { error() << Main::tr("Command/Properties cannot be an empty string."); return 44; } } if (parser.isSet(cfgFileOption)) { options.configFile = parser.value(cfgFileOption); } options.enableUInput = !parser.isSet(disableUInputOption); options.showPreferencesOnStart = parser.isSet(showDlgOnStartOption); options.dialogMinimizeOnly = parser.isSet(dialogMinOnlyOption); options.disableOverlay = parser.isSet(disableOverlayOption); if (parser.isSet(logLvlOption)) { const auto lvl = logging::levelFromName(parser.value(logLvlOption)); if (lvl != logging::level::unknown) { logging::setCurrentLevel(lvl); } else { error() << Main::tr("Cannot set log level, unknown level: '%1'").arg(parser.value(logLvlOption)); } } } RunGuard guard(QCoreApplication::applicationName()); if (!guard.tryToRun()) { if (ipcCommands.size()) { return ProjecteurCommandClientApp(ipcCommands, argc, argv).exec(); } error() << Main::tr("Another application instance is already running. Exiting."); return 42; } else if (ipcCommands.size()) { // No other application instance running - but command option was used. logInfo(appMain) << Main::tr("Cannot send commands '%1' - no running application instance found.").arg(ipcCommands.join("; ")); logWarning(appMain) << Main::tr("Cannot send commands '%1' - no running application instance found.").arg(ipcCommands.join("; ")); error() << Main::tr("Cannot send commands '%1' - no running application instance found.").arg(ipcCommands.join("; ")); return 43; } ProjecteurApplication app(argc, argv, options); return app.exec(); } Projecteur-0.9.2/src/nativekeyseqedit.cc000066400000000000000000000303511412537212300203220ustar00rootroot00000000000000// 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 } // ------------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------- 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(950); connect(m_timer, &QTimer::timeout, this, [this](){ setRecording(false); }); } // ------------------------------------------------------------------------------------------------- NativeKeySeqEdit::~NativeKeySeqEdit() { } // ------------------------------------------------------------------------------------------------- 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 return (style()->sizeFromContents(QStyle::CT_LineEdit, &opt, QSize(w, h). expandedTo(QApplication::globalStrut()), this)); } // ------------------------------------------------------------------------------------------------- void NativeKeySeqEdit::paintEvent(QPaintEvent*) { 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; } else 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.9.2/src/nativekeyseqedit.h000066400000000000000000000051741412537212300201710ustar00rootroot00000000000000// 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.9.2/src/preferencesdlg.cc000066400000000000000000001045661412537212300177460ustar00rootroot00000000000000// 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 HAS_Qt5_X11Extras #include #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}}, }; } // ------------------------------------------------------------------------------------------------- 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")); tabWidget->addTab(new DevicesWidget(settings, spotlight, this), 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_Qt5_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_Qt5_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 (s.defaultValue().type() == QVariant::Int) { 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*) { 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.9.2/src/preferencesdlg.h000066400000000000000000000046361412537212300176050ustar00rootroot00000000000000// 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 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_Qt5_X11Extras QWidget* createCompositorWarningWidget(); #endif QWidget* createLogTabWidget(); private: std::unique_ptr m_presetComboStyle; QComboBox* m_presetCombo = nullptr; QPushButton* m_closeMinimizeBtn = nullptr; QPushButton* m_exitBtn = nullptr; bool m_active = false; Mode m_dialogMode = Mode::ClosableDialog; quint32 m_discardedLogCount = 0; }; Projecteur-0.9.2/src/projecteur-icons-def.h000066400000000000000000000025431412537212300206370ustar00rootroot00000000000000// 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 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 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.9.2/src/projecteurapp.cc000066400000000000000000000573571412537212300176460ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #include "projecteurapp.h" #include "aboutdlg.h" #include "imageitem.h" #include "linuxdesktop.h" #include "logging.h" #include "preferencesdlg.h" #include "settings.h" #include "spotlight.h" #include #include #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"; } } // ------------------------------------------------------------------------------------------------- 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().size() < 1) { 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; } 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_settings->setOverlayDisabled(options.disableOverlay); m_dialog.reset(new PreferencesDialog(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; 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(); }); // 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 = std::make_unique(); connect(m_aboutDialog.get(), &QDialog::finished, this, [this](int){ m_aboutDialog.reset(); // No need to keep about dialog in memory, not that important }); } if (m_aboutDialog->isVisible()) { m_aboutDialog->show(); m_aboutDialog->raise(); m_aboutDialog->activateWindow(); } else { m_aboutDialog->exec(); } }); m_trayMenu->addSeparator(); const auto actionQuit = m_trayMenu->addAction(tr("&Quit")); connect(actionQuit, &QAction::triggered, this, [this](){ m_qmlEngine->deleteLater(); // see: https://bugreports.qt.io/browse/QTBUG-81247 this->quit(); }); 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(); }); connect(this, &ProjecteurApplication::aboutToQuit, this, [this](){ for (const auto window : m_overlayWindows) { window->close(); } m_overlayWindows.clear(); }); // 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(); } }); // 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(); } // ------------------------------------------------------------------------------------------------- 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.size() == 0) { 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 == "spot") { 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); #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) connect(localSocket, &QLocalSocket::errorOccurred, #else connect(localSocket, static_cast(&QLocalSocket::error), #endif this, [this, localSocket](QLocalSocket::LocalSocketError /*socketError*/) { logError(cmdclient) << tr("Error sending commands: %1", "%1=error message").arg(localSocket->errorString()); localSocket->close(); QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); }); connect(localSocket, &QLocalSocket::connected, [localSocket, &ipcCommands]() { for (const auto& ipcCommand : ipcCommands) { if (ipcCommand.isEmpty()) continue; 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.9.2/src/projecteurapp.h000066400000000000000000000055451412537212300175000ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #pragma once #include "spotlight.h" #include #include #include class AboutDialog; class LinuxDesktop; class PreferencesDialog; class QLocalServer; class QLocalSocket; class QMenu; class QQmlApplicationEngine; class QQmlComponent; class QSystemTrayIcon; class Settings; class Settings; 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); private: std::unique_ptr m_trayIcon; std::unique_ptr m_trayMenu; std::unique_ptr m_dialog; std::unique_ptr m_aboutDialog; QLocalServer* const m_localServer = nullptr; Spotlight* m_spotlight = nullptr; Settings* m_settings = 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.9.2/src/runguard.cc000066400000000000000000000025311412537212300165720ustar00rootroot00000000000000#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.9.2/src/runguard.h000066400000000000000000000006431412537212300164360ustar00rootroot00000000000000#pragma once #include #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.9.2/src/settings.cc000066400000000000000000001210611412537212300166030ustar00rootroot00000000000000// 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; } 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 }; } } // ----------------------------------------------------------------------------------------------- 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() { } // ------------------------------------------------------------------------------------------------- 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 (shapeSetting.defaultValue().type() != QVariant::Int) continue; 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 (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(); } 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 (it->defaultValue().type() == QVariant::Int) // Currently only int shape settings supported { 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; cfg.emplace(qvariant_cast(seq), qvariant_cast(conf)); } 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"); } else { return m_presets[index.row()-1]; } } else if (role == Qt::FontRole && index.row() == 0) { QFont f; f.setItalic(true); return f; } else 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.9.2/src/settings.h000066400000000000000000000255501412537212300164530ustar00rootroot00000000000000// 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.9.2/src/spotlight.cc000066400000000000000000000330211412537212300167560ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #include "spotlight.h" #include "deviceinput.h" #include "logging.h" #include "settings.h" #include "virtualdevice.h" #include #include #include #include #include #include #include DECLARE_LOGGING_CATEGORY(device) DECLARE_LOGGING_CATEGORY(hid) namespace { const auto hexId = logging::hexId; } // --- end anonymous namespace // ------------------------------------------------------------------------------------------------- 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_settings(settings) { m_activeTimer->setSingleShot(true); m_activeTimer->setInterval(600); connect(m_activeTimer, &QTimer::timeout, this, [this](){ setSpotActive(false); }); if (m_options.enableUInput) { m_virtualDevice = VirtualDevice::create(); } else { logInfo(device) << tr("Virtual device initialization was skipped."); } m_connectionTimer->setSingleShot(true); // From detecting a change from inotify, the device needs some time to be ready for open // TODO: This interval seems to work, but it is arbitrary - there should be a better way. m_connectionTimer->setInterval(800); connect(m_connectionTimer, &QTimer::timeout, this, [this]() { logDebug(device) << tr("New connection check triggered"); connectDevices(); }); // 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_virtualDevice); } const bool anyConnectedBefore = anySpotlightDeviceConnected(); for (const auto& scanSubDevice : dev.subDevices) { if (!scanSubDevice.deviceReadable) continue; if (dc->hasSubDevice(scanSubDevice.deviceFile)) continue; std::shared_ptr subDeviceConnection = [&scanSubDevice, &dc, this]() -> std::shared_ptr { if (scanSubDevice.type == DeviceScan::SubDevice::Type::Event) { auto devCon = SubEventConnection::create(scanSubDevice, *dc); if (addInputEventHandler(devCon)) return devCon; } else if (scanSubDevice.type == DeviceScan::SubDevice::Type::Hidraw) { auto hidCon = SubHidrawConnection::create(scanSubDevice, *dc); if(addHIDInputHandler(hidCon)) return hidCon; } 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](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()); } }); 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.flags() & 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.disable(); 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 if (!m_activeTimer->isActive()) { setSpotActive(true); } m_activeTimer->start(); if (m_virtualDevice) m_virtualDevice->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::onHIDDataAvailable(int fd, SubHidrawConnection& connection) { QByteArray readVal(20, 0); if (::read(fd, static_cast(readVal.data()), readVal.length()) < 0) { if (errno != EAGAIN) { const bool anyConnectedBefore = anySpotlightDeviceConnected(); connection.disable(); QTimer::singleShot(0, this, [this, devicePath=connection.path(), anyConnectedBefore](){ removeDeviceConnection(devicePath); if (!anySpotlightDeviceConnected() && anyConnectedBefore) { emit anySpotlightDeviceConnectedChanged(false); } }); } return; } logDebug(hid) << "Received" << readVal.toHex() << "from" << connection.path(); // TODO: Process Logitech HIDPP message } // ------------------------------------------------------------------------------------------------- bool Spotlight::addInputEventHandler(std::shared_ptr connection) { if (!connection || connection->type() != ConnectionType::Event || !connection->isConnected()) { return false; } QSocketNotifier* const readNotifier = connection->socketReadNotifier(); connect(readNotifier, &QSocketNotifier::activated, this, [this, connection=std::move(connection)](int fd) { onEventDataAvailable(fd, *connection.get()); }); return true; } // ------------------------------------------------------------------------------------------------- bool Spotlight::addHIDInputHandler(std::shared_ptr connection) { if (!connection || connection->type() != ConnectionType::Hidraw || !connection->isConnected()) { return false; } QSocketNotifier* const readNotifier = connection->socketReadNotifier(); connect(readNotifier, &QSocketNotifier::activated, this, [this, connection=std::move(connection)](int fd) { onHIDDataAvailable(fd, *connection.get()); }); 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.9.2/src/spotlight.h000066400000000000000000000045031412537212300166230ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md #pragma once #include #include #include #include #include "devicescan.h" class QTimer; class Settings; class VirtualDevice; /// Class handling spotlight device connections and indicating if a device is sending /// sending mouse move events. class Spotlight : public QObject { 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); bool addHIDInputHandler(std::shared_ptr connection); bool setupDevEventInotify(); int connectDevices(); void removeDeviceConnection(const QString& devicePath); void onEventDataAvailable(int fd, SubEventConnection& connection); void onHIDDataAvailable(int fd, SubHidrawConnection& connection); const Options m_options; std::map> m_deviceConnections; QTimer* m_activeTimer = nullptr; QTimer* m_connectionTimer = nullptr; bool m_spotActive = false; std::shared_ptr m_virtualDevice; Settings* m_settings = nullptr; }; Projecteur-0.9.2/src/spotshapes.cc000066400000000000000000000164571412537212300171500ustar00rootroot00000000000000// 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; }(); } 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); QSGFlatColorMaterial* 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 float cx = static_cast(width()/2); // center X const float cy = static_cast(height()/2); // center Y const float deltaRad = static_cast((360.0 / m_points) * (M_PI/180.0)); 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 float cx = static_cast(width()/2); // center X const float cy = static_cast(height()/2); // center Y const float deltaRad = static_cast((360.0 / m_sides) * (M_PI/180.0)); 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.9.2/src/spotshapes.h000066400000000000000000000033401412537212300167750ustar00rootroot00000000000000// 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.9.2/src/virtualdevice.cc000066400000000000000000000074131412537212300176150ustar00rootroot00000000000000// 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 LOGGING_CATEGORY(virtualdevice, "virtualdevice") namespace { class VirtualDevice_ : public QObject {}; // for i18n and logging } struct VirtualDevice::Token {}; VirtualDevice::VirtualDevice(Token, int fd) : m_uinpFd(fd) {} VirtualDevice::~VirtualDevice() { if (m_uinpFd >= 0) { ioctl(m_uinpFd, UI_DEV_DESTROY); ::close(m_uinpFd); logDebug(virtualdevice) << VirtualDevice_::tr("uinput Device Closed"); } } // Setup uinput device that can send mouse and keyboard events. std::shared_ptr VirtualDevice::create(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::unique_ptr(); } const int fd = ::open(location, O_WRONLY | O_NDELAY); if (fd < 0) { logWarn(virtualdevice) << VirtualDevice_::tr("Unable to open: %1").arg(location); logWarn(virtualdevice) << VirtualDevice_::tr("Please check if current user has write access"); return std::unique_ptr(); } 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 // TODO Are the following Key and Event bits sufficient? Do we need more? (see all in Linux's input-event-codes.h) ioctl(fd, UI_SET_EVBIT, EV_SYN); ioctl(fd, UI_SET_EVBIT, EV_KEY); ioctl(fd, UI_SET_EVBIT, EV_REL); // Set all rel event code bits on virtual device for (int i = 0; i < REL_CNT; ++i) { ioctl(fd, UI_SET_RELBIT, i); } // Set all key code bits on virtual device for (int i = 1; i < KEY_CNT; ++i) { ioctl(fd, UI_SET_KEYBIT, i); } // 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("/sys/devices/virtual/input/%1").arg(sysfs_device_name)); return std::make_shared(Token{}, fd); } void VirtualDevice::emitEvents(const struct input_event input_events[], size_t num) { 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) { if (const ssize_t sz = sizeof(input_event) * events.size()) { const auto bytesWritten = write(m_uinpFd, events.data(), sz); if (bytesWritten != sz) { logError(virtualdevice) << VirtualDevice_::tr("Error while writing to virtual device."); } } } Projecteur-0.9.2/src/virtualdevice.h000066400000000000000000000023201412537212300174470ustar00rootroot00000000000000// This file is part of Projecteur - https://github.com/jahnf/projecteur - See LICENSE.md and README.md // Virtual Device to emit customized events from Projecteur device // 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 // Device that can act as virtual keyboard and mouse class VirtualDevice { private: struct Token; int m_uinpFd = -1; public: // Return a VirtualDevice shared_ptr or an empty shared_ptr if the creation fails. static std::shared_ptr create(const char* name = "Projecteur_input_device", uint16_t virtualVendorId = 0xfeed, uint16_t virtualProductId = 0xc0de, uint16_t virtualVersionId = 1, const char* location = "/dev/uinput"); explicit VirtualDevice(Token, int fd); ~VirtualDevice(); void emitEvents(const struct input_event[], size_t num); void emitEvents(const std::vector& events); };