pax_global_header00006660000000000000000000000064141275732720014524gustar00rootroot0000000000000052 comment=dcbfdae98ee45861aae954c92ba49d948b0728a1 Quaternion-0.0.95.1/000077500000000000000000000000001412757327200141035ustar00rootroot00000000000000Quaternion-0.0.95.1/.clang-format000066400000000000000000000075151412757327200164660ustar00rootroot00000000000000# Copyright (C) 2019 Project Quotient # # You may use this file under the terms of the LGPL-2.1 license # See the file LICENSE from this package for details. # This is the clang-format configuration style to be used by libQuotient. # Inspired by: # https://code.qt.io/cgit/qt/qt5.git/plain/_clang-format # https://wiki.qt.io/Qt_Coding_Style # https://wiki.qt.io/Coding_Conventions # Further information: https://clang.llvm.org/docs/ClangFormatStyleOptions.html # For convenience, the file includes commented out settings that we assume # to borrow from the WebKit style. The values for such settings try to but # are not guaranteed to coincide with the latest version of the WebKit style. --- Language: Cpp BasedOnStyle: WebKit #AccessModifierOffset: -4 AlignAfterOpenBracket: Align #AlignConsecutiveAssignments: false #AlignConsecutiveDeclarations: false AlignEscapedNewlines: Left AlignOperands: true #AlignTrailingComments: false #AllowAllParametersOfDeclarationOnNextLine: true #AllowShortBlocksOnASingleLine: false #AllowShortCaseLabelsOnASingleLine: false #AllowShortFunctionsOnASingleLine: All #AllowShortIfStatementsOnASingleLine: false #AllowShortLoopsOnASingleLine: false #AlwaysBreakAfterReturnType: None #AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: true #BinPackArguments: true #BinPackParameters: true BraceWrapping: AfterClass: false AfterControlStatement: false AfterEnum: false AfterFunction: true AfterNamespace: false AfterStruct: false AfterUnion: false AfterExternBlock: false BeforeCatch: false BeforeElse: false IndentBraces: false SplitEmptyFunction: false SplitEmptyRecord: false SplitEmptyNamespace: false BreakBeforeBinaryOperators: NonAssignment BreakBeforeBraces: Custom #BreakBeforeInheritanceComma: false #BreakInheritanceList: BeforeColon # Only supported since clang-format 7 #BreakBeforeTernaryOperators: true #BreakConstructorInitializersBeforeComma: false #BreakConstructorInitializers: BeforeComma #BreakStringLiterals: true ColumnLimit: 80 #CommentPragmas: '^!|^:' CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: true #ConstructorInitializerIndentWidth: 4 #ContinuationIndentWidth: 4 Cpp11BracedListStyle: false #DerivePointerAlignment: false FixNamespaceComments: true ForEachMacros: - foreach - Q_FOREACH - forever IncludeBlocks: Regroup IncludeCategories: - Regex: '^>$GITHUB_ENV echo "CXX=g++" >>$GITHUB_ENV elif [[ '${{ matrix.compiler }}' == 'Clang' ]]; then echo "CC=clang" >>$GITHUB_ENV echo "CXX=clang++" >>$GITHUB_ENV # Use one of faster variations without own-quotient to do CodeQL analysis # (libQuotient should be analysed in its own repo) if [[ '${{ matrix.composition }}' == 'dynamic' ]]; then echo "CODEQL_ANALYSIS=true" >>$GITHUB_ENV fi fi echo "CMAKE_ARGS=-GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DBUILD_SHARED_LIBS=${{ matrix.composition == 'dynamic' }}" \ >>$GITHUB_ENV if [[ '${{ matrix.composition }}' == 'package' ]]; then mkdir package fi - name: Setup MSVC environment uses: ilammy/msvc-dev-cmd@v1 if: matrix.compiler == 'MSVC' with: arch: ${{ matrix.platform }} - name: Get, build and install QtKeychain run: | git clone --depth=1 -b $QTKEYCHAIN_REF https://github.com/frankosterfeld/qtkeychain cd qtkeychain cmake -S . -B build $CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=~/.local \ -DQTKEYCHAIN_STATIC=${{ matrix.composition != 'dynamic' }} cmake --build build --target install if [[ '${{ matrix.composition }}' == 'dynamic' ]]; then QTKEYCHAIN_SO_PATH=$(dirname $(find ~/.local/lib* -name libqt5keychain.so)) test -n "$QTKEYCHAIN_SO_PATH" echo "DEP_SO_PATH=$QTKEYCHAIN_SO_PATH" >>$GITHUB_ENV fi - name: Get, build and install libQuotient if: matrix.composition != 'own-quotient' run: | git clone --depth=1 -b $QUOTIENT_REF https://github.com/quotient-im/libQuotient cd libQuotient cmake -S . -B build $CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=~/.local cmake --build build --target install if [[ '${{ matrix.composition }}' == 'dynamic' ]]; then QUOTIENT_SO_PATH=$(dirname $(find ~/.local/lib* -name libQuotient.so)) test -n "$QUOTIENT_SO_PATH" echo "DEP_SO_PATH=$DEP_SO_PATH:$QUOTIENT_SO_PATH" >>$GITHUB_ENV fi - name: Initialize CodeQL tools if: env.CODEQL_ANALYSIS uses: github/codeql-action/init@v1 with: languages: cpp # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main - name: Configure Quaternion run: | if [[ '${{ runner.os }}' == 'Windows' ]]; then # DESTDIR doesn't work (and is not necessary) on Windows, see # https://cmake.org/cmake/help/latest/envvar/DESTDIR.html # NB: Using ${{ runner.temp }} (or any absolute path?) for install # root on Windows somehow confuses the shell code using it # (because of the volume letter?) - therefore relative path here. INSTALL_PATH=Quaternion-$VERSION else INSTALL_PATH=/usr DESTDIR=$GITHUB_WORKSPACE/install echo "DESTDIR=$DESTDIR" >>$GITHUB_ENV fi echo "INSTALL_PATH=$INSTALL_PATH" >>$GITHUB_ENV cmake -LA -S $GITHUB_WORKSPACE -B build $CMAKE_ARGS -DDEPLOY_VERBOSITY=$DEPLOY_VERBOSITY \ -DCMAKE_INSTALL_PREFIX=$INSTALL_PATH -DCMAKE_PREFIX_PATH=~/.local - name: Build and install Quaternion run: cmake --build build --target install - name: Perform CodeQL analysis if: env.CODEQL_ANALYSIS uses: github/codeql-action/analyze@v1 - name: Validate installation (Linux) if: startsWith(matrix.os, 'ubuntu') run: | xvfb-run env LD_LIBRARY_PATH=$DEP_SO_PATH:$Qt5_DIR/lib \ $DESTDIR$INSTALL_PATH/bin/quaternion --version if [[ "VALIDATE_APPSTREAM" == 'true' ]]; then flatpak run org.freedesktop.appstream-glib validate $DESTDIR$INSTALL_PATH/share/metainfo/*.appdata.xml fi - name: Make image (macOS) if: startsWith(matrix.os, 'macos') && matrix.composition == 'package' run: | cmake --build build --target image PACKAGE_NAME=quaternion-$VERSION.dmg mv build/quaternion.dmg package/$PACKAGE_NAME find package/$PACKAGE_NAME -size +10M && echo "PACKAGE_NAME=$PACKAGE_NAME" >>$GITHUB_ENV - name: Make AppImage (Linux) if: startsWith(matrix.os, 'ubuntu') && matrix.composition == 'package' env: QML_SOURCES_PATHS: ${{ github.workspace }}/client/qml run: | for f in linuxdeploy linuxdeploy-plugin-qt; do wget -c -nv --directory-prefix=linuxdeploy \ https://github.com/linuxdeploy/$f/releases/download/continuous/$f-x86_64.AppImage chmod +x linuxdeploy/$f-x86_64.AppImage done PACKAGE_NAME=quaternion-$VERSION.AppImage LD_LIBRARY_PATH=$Qt5_DIR/lib QMAKE=$Qt5_DIR/bin/qmake OUTPUT=package/$PACKAGE_NAME \ linuxdeploy/linuxdeploy-x86_64.AppImage --appdir $DESTDIR --plugin qt --output appimage find package/$PACKAGE_NAME -size +10M && echo "PACKAGE_NAME=$PACKAGE_NAME" >>$GITHUB_ENV - name: Archive the install tree (Windows) if: startsWith(matrix.os, 'windows') run: | rm -rf $INSTALL_PATH/{bearer,qmltooling} ls -l $INSTALL_PATH/quaternion.exe # Fail if it's not there PACKAGE_NAME=quaternion-$VERSION-win64.zip 7z a package/$PACKAGE_NAME $INSTALL_PATH find package/$PACKAGE_NAME -size +10M && echo "PACKAGE_NAME=$PACKAGE_NAME" >>$GITHUB_ENV - name: Store artefacts if: matrix.composition == 'package' uses: actions/upload-artifact@v2 with: name: '${{ env.PACKAGE_NAME }}' path: package/* retention-days: 7 Publish: runs-on: ubuntu-latest needs: [ Prepare, Build ] strategy: fail-fast: false matrix: include: - package-name: macOS artefact-type: '.dmg' - package-name: Linux artefact-type: '.AppImage' - package-name: Windows artefact-type: '-win64.zip' env: FILE_NAME: quaternion-${{ needs.Prepare.outputs.version }}${{ matrix.artefact-type }} steps: - name: Retrieve artefacts uses: actions/download-artifact@v2 with: name: ${{ env.FILE_NAME}} - name: Upload artefacts to Cloudsmith (interim builds) if: needs.Prepare.outputs.reftype != 'tags' # Tags will go to GitHub Releases uses: cloudsmith-io/action@v0.5.1 with: api-key: '${{ secrets.CLOUDSMITH_API_KEY }}' format: raw owner: quotient repo: quaternion file: ${{ env.FILE_NAME }} name: ${{ matrix.package-name }} summary: CI builds of Quaternion, ${{ matrix.package-name }} description: | The builds produced by the continuous integration; only intended for testing, not for production usage. No workability guarantees whatsoever. version: ${{ needs.Prepare.outputs.version }} republish: true - name: Upload artefact to GitHub Releases (tag builds) if: needs.Prepare.outputs.reftype == 'tags' uses: ncipollo/release-action@v1 with: token: ${{ secrets.GITHUB_TOKEN }} artifacts: ${{ env.FILE_NAME }} # It tends to false-prerelease things but that's better than false-release them prerelease: ${{ contains(needs.Prepare.outputs.version, '-') }} allowUpdates: true omitNameDuringUpdate: true omitBodyDuringUpdate: true Quaternion-0.0.95.1/.gitignore000066400000000000000000000003051412757327200160710ustar00rootroot00000000000000build build_dir .kdev4 .directory CMakeLists.txt.user* .idea flatpak/app flatpak/.flatpak-builder flatpak/repo CMakeCache.txt cmake_install.cmake Makefile quaternion_autogen/ .cmake/ .DS_Store Quaternion-0.0.95.1/.gitmodules000066400000000000000000000001121412757327200162520ustar00rootroot00000000000000[submodule "lib"] path = lib url = ../../quotient-im/libQuotient.git Quaternion-0.0.95.1/.lgtm.yml000066400000000000000000000010761412757327200156530ustar00rootroot00000000000000path_classifiers: library: - lib/* generated: - quaternion_autogen/* extraction: cpp: prepare: packages: # Assuming package base of cosmic - ninja-build - qt5-default - qtmultimedia5-dev - qml-module-qtquick-controls - qml-module-qtquick-controls2 - qttools5-dev - qt5keychain-dev after_prepare: - git clone https://gitlab.matrix.org/matrix-org/olm.git - pushd olm - cmake . -Bbuild -GNinja - cmake --build build - popd configure: command: "cmake . -GNinja -DOlm_DIR=olm/build" Quaternion-0.0.95.1/BUILDING.md000066400000000000000000000260731412757327200156320ustar00rootroot00000000000000# Building and Packaging Quaternion [![Quaternion-master@Travis](https://img.shields.io/travis/quotient-im/Quaternion/master.svg)](https://travis-ci.org/quotient-im/Quaternion/branches) [![Quaternion-master@AppVeyor](https://img.shields.io/appveyor/ci/quotient/quaternion/master.svg?logo=appveyor)](https://ci.appveyor.com/project/quotient/quaternion) [![license](https://img.shields.io/github/license/quotient-im/quaternion.svg)](https://github.com/quotient-im/Quaternion/blob/master/COPYING) [![Chat](https://img.shields.io/badge/chat-%23quaternion-blue.svg)](https://matrix.to/#/#quaternion:matrix.org) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) ### Getting the source code The source code is hosted at GitHub: https://github.com/quotient-im/Quaternion. The best way for one-off building is checking out a tag for a given release from GitHub (make sure to pass `--recurse-submodules` to `git checkout` if you use Option 2 - see below). If you plan to work on Quaternion code, feel free to fork/clone the repo and base your changes on the master branch. Quaternion needs libQuotient to build. There are two options to use the library: 1. Use a library installation known to CMake - either as a package available from your package repository (possibly but not necessarily system-wide), or as a result of building the library from the source code in another directory. In the latter case CMake internally registers the library upon succesfully building it so you shouldn't even need to pass `CMAKE_PREFIX_PATH` (still better do pass it, to avoid surprises). 2. As a Git submodule. If you haven't cloned Quaternion sources yet, the following will get you all sources in one go: ```bash git clone --recursive https://github.com/quotient-im/Quaternion.git ``` If you already have cloned Quaternion, do the following in the top-level directory (NOT in `lib` subdirectory): ```bash git submodule init git submodule update ``` In either case here, to correctly check out a given tag or branch, make sure to also check out submodules: ```bash git checkout --recurse-submodules ``` Depending on your case, either option can be preferrable. General guidance is: - Option 1 is strongly recommended for packaging and also good for development on Quaternion without changing libQuotient; - Option 2 is better for one-off building and for active development when _both_ Quaternion and libQuotient get changed. These days Option 2 is used by default (with a fallback to Option 1 if no libQuotient is found under `lib/`). To override that you can pass `USE_INTREE_LIBQMC` option to CMake: `-DUSE_INTREE_LIBQMC=0` (or `NO`, or `OFF`) will force Option 1 (using an external libQuotient even when a submodule is there). The other way works too: if you intend to use libQuotient from the submodule, pass `-DUSE_INTREE_LIBQMC=1` (or `YES`, or `ON`) to make sure the build configuration process fails instead of finding an external libQuotient somewhere when a submodule is unusable for some reason (e.g. when `--recursive` has been forgotten when cloning). ### Pre-requisites - a recent Linux, macOS or Windows system (desktop versions tried; mobile platforms might work too but never tried) - Recent enough Linux examples: Debian Buster; Fedora 28; OpenSUSE Leap 15; Ubuntu Bionic Beaver. - Qt 5 (either Open Source or Commercial), version 5.11 or higher (5.14+ is recommended). Quaternion 0.0.95 and earlier does not build with Qt 6. - CMake 3.10 or newer (from your package management system or [the official website](https://cmake.org/download/)) - A C++ toolchain with C++17 support: - GCC 7 (Windows, Linux, macOS), Clang 6 (Linux), Apple Clang 10 (macOS) and Visual Studio 2017 (Windows) are the oldest officially supported. - Any build system that works with CMake should be fine: GNU Make, ninja (any platform), NMake, jom (Windows) are known to work. - optionally, libQuotient 0.6.x development files (from your package management system), or prebuilt libQuotient (see "Getting the source code" above). libQuotient 0.7 (in development as of this writing) is _not_ compatible with Quaternion 0.0.95. - optionally (but strongly recommended), [QtKeychain](https://github.com/frankosterfeld/qtkeychain) to store access tokens in libsecret keyring or similar providers. #### Linux Just install things from the list above using your preferred package manager. If your Qt package base is fine-grained you might want to take a look at `CMakeLists.txt` to figure out which specific libraries Quaternion uses (or blindly run cmake and look at error messages). Note also that you'll need several Qt Quick plugins for Quaternion to work (without them, it will compile and run but won't show the messages timeline). On Debian/Ubuntu, the following line should get you everything necessary to build and run Quaternion: ```bash sudo apt-get install cmake qtdeclarative5-dev qttools5-dev qml-module-qtquick-controls qml-module-qtquick-controls2 qtmultimedia5-dev ``` To enable keyring support, also install QtKeychain by ```bash sudo apt-get install qt5keychain-dev ``` On Fedora 28, the following command should be enough for building and running: ```bash sudo dnf install cmake qt5-qtdeclarative-devel qt5-qtquickcontrols qt5-qtquickcontrols2 qt5-qtmultimedia-devel ``` and QtKeychain can be installed with ```bash sudo dnf install qtkeychain-qt5-devel ``` #### macOS `brew install qt5` should get you Qt5. If you want to build with QtKeychain, also call `brew install qtkeychain`. You have to point CMake at the Qt5 installation location, with something like: ```bash # if using in-tree libQuotient: cmake .. -DCMAKE_PREFIX_PATH=$(brew --prefix qt5) # or otherwise... cmake .. -DCMAKE_PREFIX_PATH=/path/to/libQuotient -DCMAKE_PREFIX_PATH=$(brew --prefix qt5) ``` #### Windows 1. Install CMake. The commands in further sections imply that cmake is in your PATH - otherwise you have to prepend them with actual paths. 1. Install Qt5, using their official installer. 1. Make sure CMake knows about Qt and the toolchain - the easiest way is to run `qtenv2.bat` script that can be found in `C:\Qt\\\bin` (assuming you installed Qt to `C:\Qt`). The only thing it does is adding necessary paths to `PATH` - you might not want to run it on system startup but it's very handy to setup environment before building. Setting `CMAKE_PREFIX_PATH`, the same way as for macOS (see above), also helps. 1. If needed, get and build [QtKeychain](https://github.com/frankosterfeld/qtkeychain). ### Build In the root directory of the project sources: ```bash mkdir build_dir cd build_dir cmake .. # Pass -D if needed cmake --build . --target all ``` This will get you an executable in `build_dir` inside your project sources. Noteworthy CMake variables that you can use: - `-DCMAKE_PREFIX_PATH=/path` - add a path to CMake's list of searched paths for preinstalled software (Qt, libQuotient, QtKeychain); multiple paths are separated by `;` (semicolons). - `-DCMAKE_INSTALL_PREFIX=/path` - controls where Quaternion will be installed (see below on installing from sources). - `-DUSE_INTREE_LIBQMC=` - force using/not-using the in-tree copy of libQuotient sources (see "Getting the source code" above). - `-DUSE_QQUICKWIDGET=` - by default it's `ON` with Qt 5.12 and `OFF` with earlier versions. See the next section for details. #### QQuickWidget Quaternion uses a combination of Qt Widgets and QML to render its UI (this _might_ change at some point in time but certainly not in any near future). Internally, embedding QML in a Qt Widget used to be done in two ways providing very similar API but different underneath: swallowing a `QQuickView` object in a window container and using `QQuickWidget`. Historically Quaternion used the former method; however, `QQuickView` implementation in Qt is so ugly that [it's officially deprecated by The Qt Project](https://blog.qt.io/blog/2014/07/02/qt-weekly-16-qquickwidget/). Quaternion suffered from that ugliness too (see, e.g., #355 - a completely blank timeline despite the QML engine being up and running). As of now, Quaternion uses better and more reliable `QQuickWidget` when built with Qt 5.12 or newer. Unfortunately, Quaternion's QML is tricky enough to crash the less mature `QQuickWidget` code on Qt versions before 5.12, so if you have to use an older Qt version Quaternion will build with `QQuickView` by default. To override the defaults you can pass `-DUSE_QQUICKWIDGET=ON` to the first (configuring) `cmake` invocation to force usage of `QQuickWidget` even with older Qt; or `-DDISABLE_QQUICKWIDGET=ON` to force usage of `QQuickView` with newer Qt - by the way, if you have to do that because of some problem with `QQuickWidget`, please file an issue at Quaternion). With `QQuickWidget` considered stable and reliable now, `QQuickView` mode will be entirely deprecated in Quaternion 0.0.96 (that will require Qt 5.12+), and later removed. ### Install In the root directory of the project sources: `cmake --build build_dir --target install`. If you use GNU Make, `make install` (with `sudo` if needed) will work equally well. ### Building as Flatpak If you run Linux and your distribution supports flatpak, you can easily build and install Quaternion as a flatpak package: ```bash git clone https://github.com/quotient-im/Quaternion.git --recursive cd Quaternion/flatpak ./setup_runtime.sh ./build.sh flatpak --user install quaternion-nightly com.github.quaternion ``` Whenever you want to update your Quaternion package, do the following from the flatpak directory: ```bash ./build.sh flatpak --user update ``` ## Troubleshooting If `cmake` fails with... ``` CMake Warning at CMakeLists.txt:11 (find_package): By not providing "FindQt5Widgets.cmake" in CMAKE_MODULE_PATH this project has asked CMake to find a package configuration file provided by "Qt5Widgets", but CMake did not find one. ``` ...or a similar error referring to Qt5 - make sure that your `CMAKE_PREFIX_PATH` actually points to the location where Qt5 is installed, see above. If `cmake` fails with... ``` CMake Error at CMakeLists.txt:30 (add_subdirectory): The source directory /lib does not contain a CMakeLists.txt file. ``` ...then you don't have libQuotient sources - most likely because you didn't do the `git submodule init && git submodule update` dance and don't have libQuotient development files elsewhere - also, see the beginning of this file. If you have made sure that your toolchain is in order (versions of compilers and Qt are among supported ones, `PATH` is set correctly etc.) but building fails with strange Qt-related errors such as not found symbols or undefined references, double-check that you don't have Qt 4.x (or Qt 6.x) packages around ([here is a typical example](https://github.com/quotient-im/Quaternion/issues/185)). If you need those packages reinstalling them may help; but if you use Qt4/6 by default you have to explicitly pass Qt5 location to CMake (see notes about `CMAKE_PREFIX_PATH` in "Building"). See also the Troubleshooting section in [README.md](./README.md) Quaternion-0.0.95.1/CMakeLists.txt000066400000000000000000000267321412757327200166550ustar00rootroot00000000000000CMAKE_MINIMUM_REQUIRED(VERSION 3.10) if (POLICY CMP0092) cmake_policy(SET CMP0092 NEW) endif() set(IDENTIFIER "com.github.quaternion") set(COPYRIGHT "Copyright © 2016-2021 Quaternion project contributors") project(quaternion VERSION 0.0.95.1 LANGUAGES CXX) if(UNIX AND NOT APPLE) set(LINUX 1) endif(UNIX AND NOT APPLE) include(CheckCXXCompilerFlag) if (NOT WIN32) include(GNUInstallDirs) include(cmake/ECMInstallIcons.cmake) endif(NOT WIN32) # Find includes in corresponding build directories set(CMAKE_INCLUDE_CURRENT_DIR ON) # Instruct CMake to run moc automatically when needed. set(CMAKE_AUTOMOC ON) # Set a default build type if none was specified if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) message(STATUS "Setting build type to 'Debug' as none was specified") set(CMAKE_BUILD_TYPE Debug CACHE STRING "Choose the type of build" FORCE) # Set the possible values of build type for cmake-gui set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif() # Setup command line parameters for the compiler and linker if (MSVC) add_compile_options(/EHsc /W4 /wd4100 /wd4127 /wd4242 /wd4244 /wd4245 /wd4267 /wd4365 /wd4456 /wd4459 /wd4464 /wd4505 /wd4514 /wd4571 /wd4619 /wd4623 /wd4625 /wd4626 /wd4706 /wd4710 /wd4774 /wd4820 /wd4946 /wd5026 /wd5027) else() foreach (FLAG "" all pedantic extra no-unused-parameter) CHECK_CXX_COMPILER_FLAG("-W${FLAG}" WARN_${FLAG}_SUPPORTED) if ( WARN_${FLAG}_SUPPORTED AND NOT CMAKE_CXX_FLAGS MATCHES "(^| )-W?${FLAG}($| )") add_compile_options(-W${FLAG}) endif () endforeach () endif() # Find the libraries find_package(Qt5 5.11 REQUIRED Widgets Network Quick Gui LinguistTools Multimedia DBus QuickControls2) # Qt5_Prefix is only used to show Qt path in message() # Qt5_BinDir is where all the *deployqt tools reside if (QT_QMAKE_EXECUTABLE) get_filename_component(Qt5_BinDir "${QT_QMAKE_EXECUTABLE}" DIRECTORY) get_filename_component(Qt5_Prefix "${Qt5_BinDir}/.." ABSOLUTE) else() get_filename_component(Qt5_BinDir "${Qt5_DIR}/../../../bin" ABSOLUTE) get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) endif() if (USE_QQUICKWIDGET) find_package(Qt5 5.11 REQUIRED QuickWidgets) elseif(NOT DISABLE_QQUICKWIDGET) # QQuickWidget only stopped crashing in Qt 5.12, use it by default if found find_package(Qt5 5.12 QUIET COMPONENTS QuickWidgets) if (Qt5QuickWidgets_FOUND) set(USE_QQUICKWIDGET ON) endif() endif() if(WIN32) enable_language(RC) include(CMakeDetermineRCCompiler) if(MINGW) set(CMAKE_RC_COMPILER_INIT windres) set(CMAKE_RC_COMPILE_OBJECT " -O coff -I${CMAKE_CURRENT_BINARY_DIR} -i -o ") endif() endif() if ((NOT DEFINED USE_INTREE_LIBQMC OR USE_INTREE_LIBQMC) AND EXISTS ${PROJECT_SOURCE_DIR}/lib/lib/util.h) add_subdirectory(lib) include_directories(lib) if (NOT DEFINED USE_INTREE_LIBQMC) set (USE_INTREE_LIBQMC 1) endif () endif () if (NOT USE_INTREE_LIBQMC) find_package(Quotient 0.6 REQUIRED) if (NOT Quotient_FOUND) message( WARNING "libQuotient not found; configuration will most likely fail.") message( WARNING "Make sure you have installed libQuotient development files") message( WARNING "as a package or checked out the library sources in lib/.") message( WARNING "See also BUILDING.md") endif () endif () find_package(Qt5Keychain QUIET) if (Qt5Keychain_FOUND) set(USE_KEYCHAIN ON) endif() message( STATUS ) message( STATUS "=============================================================================" ) message( STATUS " Quaternion ${quaternion_VERSION} Build Information" ) message( STATUS "=============================================================================" ) if (CMAKE_BUILD_TYPE) message( STATUS "Build type: ${CMAKE_BUILD_TYPE}") endif(CMAKE_BUILD_TYPE) message( STATUS "Quaternion install prefix: ${CMAKE_INSTALL_PREFIX}" ) # Get Git info if possible find_package(Git) if(GIT_FOUND) execute_process(COMMAND "${GIT_EXECUTABLE}" rev-parse -q HEAD WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} OUTPUT_VARIABLE GIT_SHA1 OUTPUT_STRIP_TRAILING_WHITESPACE) message( STATUS "Git SHA1: ${GIT_SHA1}") endif() message( STATUS "Using compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" ) message( STATUS "Using Qt ${Qt5_VERSION} at ${Qt5_Prefix}" ) if (USE_INTREE_LIBQMC) message( STATUS "Using in-tree libQuotient") if (GIT_FOUND) execute_process(COMMAND "${GIT_EXECUTABLE}" rev-parse -q HEAD WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/lib OUTPUT_VARIABLE LIB_GIT_SHA1 OUTPUT_STRIP_TRAILING_WHITESPACE) message( STATUS " Library git SHA1: ${LIB_GIT_SHA1}") endif (GIT_FOUND) else () message( STATUS "Using libQuotient ${Quotient_VERSION} at ${Quotient_DIR}") endif () if (USE_QQUICKWIDGET) message( STATUS "Using QQuickWidget to render QML") endif(USE_QQUICKWIDGET) if (USE_KEYCHAIN) message( STATUS "Using Qt Keychain ${Qt5Keychain_VERSION} at ${Qt5Keychain_DIR}") endif () message( STATUS "=============================================================================" ) message( STATUS ) # Set up source files set(quaternion_SRCS client/accountregistry.cpp client/quaternionroom.cpp client/htmlfilter.cpp client/imageprovider.cpp client/activitydetector.cpp client/dialog.cpp client/logindialog.cpp client/networkconfigdialog.cpp client/roomdialogs.cpp client/mainwindow.cpp client/roomlistdock.cpp client/userlistdock.cpp client/accountselector.cpp client/kchatedit.cpp client/chatedit.cpp client/timelinewidget.cpp client/chatroomwidget.cpp client/systemtrayicon.cpp client/profiledialog.cpp client/models/messageeventmodel.cpp client/models/userlistmodel.cpp client/models/roomlistmodel.cpp client/models/abstractroomordering.cpp client/models/orderbytag.cpp client/main.cpp ) set(quaternion_QRC client/resources.qrc ) # quaternion_en.ts is updated explicitly by building trbase target, # while all other translation files are created and updated externally at # Lokalise.co set(quaternion_en_TS client/translations/quaternion_en.ts) QT5_CREATE_TRANSLATION(quaternion_QM ${quaternion_en_TS} client/ client/qml/ OPTIONS -no-obsolete) add_custom_target(trbase DEPENDS ${quaternion_en_TS}) set(quaternion_TS client/translations/quaternion_en_GB.ts client/translations/quaternion_de.ts client/translations/quaternion_pl.ts client/translations/quaternion_ru.ts client/translations/quaternion_es.ts ) QT5_ADD_TRANSLATION(quaternion_QM ${quaternion_TS}) QT5_ADD_RESOURCES(quaternion_QRC_SRC ${quaternion_QRC}) set_property(SOURCE qrc_resources.cpp PROPERTY SKIP_AUTOMOC ON) if(WIN32) set(quaternion_WINRC quaternion_win32.rc) set_property(SOURCE quaternion_win32.rc APPEND PROPERTY OBJECT_DEPENDS ${PROJECT_SOURCE_DIR}/icons/quaternion.ico ) endif() if(APPLE) if(NOT CMAKE_OSX_DEPLOYMENT_TARGET) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13") endif() MESSAGE(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: " ${CMAKE_OSX_DEPLOYMENT_TARGET}) set(MACOSX_BUNDLE_GUI_IDENTIFIER ${IDENTIFIER}) set(MACOSX_BUNDLE_BUNDLE_NAME ${PROJECT_NAME}) set(MACOSX_BUNDLE_COPYRIGHT ${COPYRIGHT}) set(MACOSX_BUNDLE_SHORT_VERSION_STRING ${quaternion_VERSION}) set(MACOSX_BUNDLE_BUNDLE_VERSION ${quaternion_VERSION}) set(ICON_NAME "quaternion.icns") set(${PROJECT_NAME}_MAC_ICON "${PROJECT_SOURCE_DIR}/icons/${ICON_NAME}") set(MACOSX_BUNDLE_ICON_FILE ${ICON_NAME}) set_property(SOURCE "${${PROJECT_NAME}_MAC_ICON}" PROPERTY MACOSX_PACKAGE_LOCATION Resources) endif(APPLE) # Windows, this is a GUI executable; OSX, make a bundle add_executable(${PROJECT_NAME} WIN32 MACOSX_BUNDLE ${quaternion_SRCS} ${quaternion_QRC_SRC} ${quaternion_QM} ${quaternion_WINRC} ${${PROJECT_NAME}_MAC_ICON}) target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17) target_compile_definitions(${PROJECT_NAME} PRIVATE GIT_SHA1="${GIT_SHA1}" LIB_GIT_SHA1="${LIB_GIT_SHA1}") target_compile_definitions(${PROJECT_NAME} PRIVATE QT_NO_JAVA_STYLE_ITERATORS) if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.16.0" AND NOT CMAKE_CXX_COMPILER_ID STREQUAL GNU) # https://bugzilla.redhat.com/show_bug.cgi?id=1721553 target_precompile_headers(${PROJECT_NAME} PRIVATE ) endif () target_link_libraries(${PROJECT_NAME} Quotient Qt5::Widgets Qt5::Quick Qt5::Qml Qt5::Gui Qt5::Network Qt5::QuickControls2) if (USE_QQUICKWIDGET) target_compile_definitions(${PROJECT_NAME} PRIVATE USE_QQUICKWIDGET) target_link_libraries(${PROJECT_NAME} Qt5::QuickWidgets) endif() if(USE_KEYCHAIN) target_compile_definitions(${PROJECT_NAME} PRIVATE USE_KEYCHAIN) target_link_libraries(${PROJECT_NAME} ${QTKEYCHAIN_LIBRARIES}) include_directories(${QTKEYCHAIN_INCLUDE_DIR}) endif() # macOS specific config for bundling if (APPLE) set_property(TARGET ${PROJECT_NAME} PROPERTY MACOSX_BUNDLE_INFO_PLIST "${PROJECT_SOURCE_DIR}/cmake/MacOSXBundleInfo.plist.in") endif() # Installation if (NOT CMAKE_INSTALL_BINDIR) set(CMAKE_INSTALL_BINDIR ".") endif() install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR}) if(LINUX) install(FILES linux/${IDENTIFIER}.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications ) install(FILES linux/${IDENTIFIER}.appdata.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo ) install(FILES ${quaternion_QM} DESTINATION ${CMAKE_INSTALL_DATADIR}/Quotient/quaternion/translations ) file(GLOB quaternion_icons icons/quaternion/*-apps-quaternion.png) ecm_install_icons(ICONS ${quaternion_icons} icons/quaternion/sources/sc-apps-quaternion.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/icons ) endif(LINUX) set(QML_DIR ${PROJECT_SOURCE_DIR}/client/qml) if (NOT DEPLOY_VERBOSITY) set(DEPLOY_VERBOSITY 1) # The default for *deployqt tools, out of 0..3 endif() if(WIN32) install(CODE " message(STATUS \"Running windeployqt at \${CMAKE_INSTALL_PREFIX}\${CMAKE_INSTALL_BINDIR}\") execute_process( COMMAND \"${Qt5_BinDir}/windeployqt\" --verbose ${DEPLOY_VERBOSITY} --no-multimediaquick --no-declarative --no-test --no-winextras --qmldir \"${QML_DIR}\" \${CMAKE_INSTALL_PREFIX}\${CMAKE_INSTALL_BINDIR} RESULT_VARIABLE WDQ_RETVAL ) if (WDQ_RETVAL) message( \"windeployqt returned \${WDQ_RETVAL} - check messages above\") else() message( STATUS \"Quaternion and its dependencies have been deployed to \${CMAKE_INSTALL_PREFIX}.\") endif() ") install(FILES ${quaternion_QM} DESTINATION ${CMAKE_INSTALL_BINDIR}/translations ) endif(WIN32) # Packaging if(APPLE) set(MACDEPLOYQT_ARGS ${PROJECT_NAME}.app -dmg -qmldir="${QML_DIR}" -verbose=${DEPLOY_VERBOSITY}) add_custom_target(image COMMAND "${Qt5_BinDir}/macdeployqt" ${MACDEPLOYQT_ARGS} DEPENDS ${PROJECT_NAME} WORKING_DIRECTORY ${PROJECT_BINARY_DIR} COMMENT "Running ${MACDEPLOYQT} with args: ${MACDEPLOYQT_ARGS}" ) endif(APPLE) Quaternion-0.0.95.1/COPYING000066400000000000000000001045131412757327200151420ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . Quaternion-0.0.95.1/ISSUE_TEMPLATE.md000066400000000000000000000027621412757327200166170ustar00rootroot00000000000000 ### Description Describe here the problem that you are experiencing, or the feature you are requesting. ### Steps to reproduce - For bugs, list the steps - that reproduce the bug - using hyphens as bullet points Describe how what happens differs from what you expected. Quaternion dumps logs to the standard output. If you can find the logs and identify any log snippets relevant to your issue, please include those here (please be careful to remove any personal or private data): ### Version information - **Quaternion version**: - **Qt version**: - **Install method**: - **Platform**: Quaternion-0.0.95.1/Quaternion.project000066400000000000000000000204551412757327200176260ustar00rootroot00000000000000 . cmake $(ProjectPath) -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=1 mingw32-make clean && mingw32-make -j4 mingw32-make clean mingw32-make -j4 None $(IntermediateDirectory) cmake $(ProjectPath) -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=1 mingw32-make clean && mingw32-make -j4 mingw32-make clean mingw32-make -j4 None $(IntermediateDirectory) Quaternion-0.0.95.1/README.md000066400000000000000000000535051412757327200153720ustar00rootroot00000000000000# Quaternion Made for Matrix [![license](https://img.shields.io/github/license/quotient-im/quaternion.svg)](https://github.com/quotient-im/Quaternion/blob/master/COPYING) ![status](https://img.shields.io/badge/status-beta-yellow.svg) [![release](https://img.shields.io/github/release/quotient-im/quaternion/all.svg)](https://github.com/quotient-im/Quaternion/releases/latest) [![](https://img.shields.io/matrix/quotient:matrix.org.svg)](https://matrix.to/#/#quotient:matrix.org) [![](https://img.shields.io/cii/percentage/1663.svg?label=CII%20best%20practices)](https://bestpractices.coreinfrastructure.org/projects/1663/badge) [![Language grade: C/C++](https://img.shields.io/lgtm/grade/cpp/g/quotient-im/Quaternion.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/quotient-im/Quaternion/context:cpp) [![CI builds hosted by: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=flat-square)](https://cloudsmith.com) [![merge-chance-badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fmerge-chance.info%2Fbadge%3Frepo%3Dquotient-im/quaternion)](https://merge-chance.info/target?repo=quotient-im/quaternion) Quaternion is a cross-platform desktop IM client for the [Matrix](https://matrix.org) protocol. This file contains general information about application usage and settings. See [BUILDING.md](./BUILDING.md) for building instructions. ## Contacts Most of talking around Quaternion happens in the room of its parent project, Quotient: [#quotient:matrix.org](https://matrix.to/#/#quotient:matrix.org). You can file issues at [the project's issue tracker](https://github.com/quotient-im/Quaternion/issues). If you find what looks like a security issue, please follow [special instructions](./SECURITY.md). ## Download and install For GNU/Linux, the recommended way to install Quaternion is via your distribution's package manager. Users of macOS can use a Homebrew package. The source code for the latest release as well as binaries for major platforms can also be found at the [GitHub Releases page](https://github.com/quotient-im/Quaternion/releases). Make sure to read the notes below depending to your environment. ### Requirements Quaternion 0.0.95 packages on Linux need Qt version 5.11 or higher; for major distros, that means Debian 10 (Buster), Ubuntu 18.10 (Cosmic), Fedora 29 and OpenSUSE 15.2 or newer releases. The packages published by the project at GitHub (see below) come with Qt libraries bundled; Linux packages, including those from Flathub, use respective package managers to pull necessary libraries automatically. ### Windows You can download the latest release from [GitHub](https://github.com/quotient-im/Quaternion/releases/latest). Since there's no established package management on Windows to resolve dependencies, all needed libraries and a C++ runtime are packaged/installed together with Quaternion - except OpenSSL, because of export restrictions. Unless you already have OpenSSL around (e.g., it is a part of any Qt development installation), you should install it yourself. [OpenSSL's Wiki](https://wiki.openssl.org/index.php/Binaries) lists a few links to OpenSSL installers. They come in different build configurations; Quaternion archives provided at GitHub need OpenSSL made with/for Visual Studio (not MinGW). Normally you should install OpenSSL 1.1.x. Older releases (0.0.9.4 and before) used OpenSSL 1.0.x but neither that version of OpenSSL nor those Quaternion releases are supported; don't use them unless you really know what you're doing. ### macOS You can download the latest release from [GitHub](https://github.com/quotient-im/Quaternion/releases/latest). Alternatively, you can install Quaternion with [Homebrew Cask](https://brew.sh) ``` brew install quaternion ``` ### Linux and others Quaternion is packaged for many distributions, including various versions of Debian, Ubuntu and OpenSUSE, as well as Arch Linux, NixOS and FreeBSD. A pretty comprehensive list can be found at [Repology](https://repology.org/project/quaternion/versions). Flatpaks for Quaternion are available from Flathub. To install, use: ``` flatpak install https://flathub.org/repo/appstream/com.github.quaternion.flatpakref ``` Please file issues at https://github.com/flathub/com.github.quaternion if you believe there's a problem specific to Flatpak. The GitHub Releases page offers AppImage binaries for Linux; however, it's recommended to only use AppImage binaries if Quaternion is not available from your distribution's repos and Flatpak doesn't work for you. Distribution-specific packages better integrate into the system (particularly, the desktop environment) and include all relevant customisations (e.g. themes) and fixes (e.g. security). If you wish to use features depending on newer Qt (such as Markdown) than your distribution provides, consider installing Quaternion as a [Flatpak](https://flathub.org/apps/details/com.github.quaternion). Both Flatpak packages and distribution-specific packages are built in a more reproducible and controlled way than AppImages assembled within this project; unlike AppImages, they are also (usually) signed by the repo which gives certain protection from tampering. ### Development builds Thanks to generous and supportive folks at [Cloudsmith](https://cloudsmith.io) who provide free hosting to OSS projects, those who want to check out the latest (not necessarily the greatest, see below) code can find packages produced by continuous integration (CI) in the [Quaternion repo there](https://cloudsmith.io/~quotient/repos/quaternion/groups/). A few important notes on these packages in case you're new to them: - All these builds come bundled with recent Qt (5.14 on Linux and 5.15 on other platforms, as of this writing). - They are only provided for testing; feedback on _any_ release is welcome as long as you know which build you run; but do not expect the developers to address issues in any but the latest snapshot. - In case it's still unclear: these builds are UNSTABLE by default; some may not run at all, and if they do, they may ~~tell you obscenities in your local language, steal your smartphone, and share your private photos~~ scramble the messages you send, interfere with or even break other clients including Element ones, and generally corrupt your account in ways unexpected and hard to fix (all of that actually happened in the past). Do NOT run these builds if you're not prepared to deal with the problems. - If you understand the above, have your backups in order and are still willing to try things out or just generally help with the project - make sure to `/join #quotient:matrix.org` and have the URL you downloaded handy. In case of trouble, ~~show this label to your doctor~~ send the URL to the binary you used in the chat room (you may need to use another client or Quaternion version for that), describe what happened and we'll try to pull you out of it. If you want to build Quaternion from sources, see [BUILDING.md](./BUILDING.md). ## Running Just start the executable in your most preferred way - either from the build directory or from the installed location. If you're interested in tweaking configuration beyond what's available in the UI, read the "Configuration" section further below. ## Translation Quaternion uses [Lokalise.co](https://lokalise.co) for the translation effort. It's easy to participate: [join the project at Lokalise.co](https://lokalise.co/public/730769035bbc328c31e863.62506391/), ask to add your language (either in #quotient:matrix.org or in the Lokalise project chat) and start translating! Many languages are still longing for contributors. ## Configuration The only non-trivial command-line option available so far is `--locale` - it allows you to override the locale Quaternion uses (an equivalent of setting `LC_ALL` variable on UNIX-based systems). Version 0.0.95 comes with German, Russian, Polish, and Spanish translations. Quaternion stores its configuration in a way standard for Qt applications, as described below. It will read and write the configuration in the user-specific location (creating it if non-existent) and will only read the system-wide location with reasonable defaults if the configuration is not found at the user-specific one. - Linux: - user-specific: `$HOME/.config/Quotient/quaternion.conf` - system-wide: `$XDG_CONFIG_DIR/Quotient/quaternion` or `/etc/xdg/Quotient/quaternion` - macOS: - user-specific: `$HOME/Library/Preferences/im.quotient.quaternion.plist` - system-wide: `/Library/Preferences/im.quotient.quaternion.plist` - Windows: registry keys under - user-specific: `HKEY_CURRENT_USER\Software\Quotient\quaternion` - system-wide: `HKEY_LOCAL_MACHINE\Software\Quotient\quaternion` ALL settings listed below reside in `UI` section of the configuration file or (for Windows) registry. Some settings exposed in the user interface (Settings and View menus) are: - `notifications` - a general setting whether Quaternion should distract the user with notifications and how. - `none` suppresses notifications entirely (rooms and messages are still hightlighted but the tray icon is muted); - `non-intrusive` allows the tray icon show notification popups; - `intrusive` (default) adds to that activation of Quaternion window (i.e. the application blinking in the task bar, or getting raised, or otherwise demands attention in an environment-specific way). - `timeline_layout` - this allows to choose the timeline layout. If this is set to "xchat", Quaternion will show the author to the left of each message, in an xchat/hexchat style. Any other value will select the "default" layout, with author labels above blocks of messages. - `use_shuttle_dial` - Quaternion will use a shuttle dial instead of a classic scrollbar for the timeline's vertical scrolling control. To start scrolling move the shuttle dial away from its neutral position in the middle; the further away you move it, the faster you scroll in that direction. Releasing the dial resets it back to the neutral position and stops scrolling. This is more convenient if you need to move around without knowing the position relative to the edges, as is the case of a Matrix timeline; however, the control is somewhat unconventional and not all people like it. The shuttle dial is enabled by default; set this to false (or 0) to use the classic scrollbar. - `autoload_images` - whether full-size images should be loaded immediately once the message is shown on the screen. The default is to automatically load full-size images; set this to false (or 0) to disable that and only load a thumbnail initially. - `show_noop_events` - set this to 1 to show state events that do not alter the state (you'll see "(repeated)" next to most of those). - `RoomsDock/tags_order` - allows to alter the order of tags in the room list. This is a comma-separated list of tags/namespaces; a few characters have special meaning as described below. If a tag is not mentioned and does not fit any namespace, it will be put at the end of the room list in lexicographic order. Tags within the same namespace are also ordered lexicographically. `.*` (only recognised at the end of the string) means the whole namespace; strings that don't end with this are treated as fully specified tags. `-` in front of the tag/namespace means it should not be used for grouping; e.g., if you don't want People group you can add `-im.quotient.direct` anywhere in the list. `im.quotient.none` ("Rooms") always exists and cannot be disabled, only its position in the list is taken into account. The default tags order is as follows: `m.favourite,u.*,im.quotient.direct,im.quotient.none,m.lowpriority`, meaning: Favourites, followed by all user custom tags, then People, rooms with no enabled tags (the "Rooms" group) and finally Low priority rooms. If Quaternion doesn't find the setting in the configuration it will write down this line to the configuration so that you don't need to enter it from scratch. Settings not exposed in UI: - `show_author_avatars` - set this to 1 (or true) to show author avatars in the timeline (default if the timeline layout is set to default); setting this to 0 (or false) will suppress avatars (default for the XChat timeline layout). - `suppress_local_echo` - set this to 1 (or true) to suppress showing local echo (events sent from the current application but not yet confirmed by the server). By default local echo is shown. - `animations_duration_ms` - defines the base duration (in milliseconds) of animation effects in the timline. The default is 400; set it to 0 to disable animation. - `outgoing_color` - set this to the color name you prefer for text you sent; HTML color names and SVG `#codes` are supported; by default it's `#204A87` (navy blue). - `highlight_color` - set this to the color name you prefer for highlighted rooms/messages; HTML color names and SVG `#codes` are supported; by default it's `orange`. - `highlight_mode` - set this to `text` if you prefer to use the text color for highlighting; the default is to use the background for highlighting. - `use_human_friendly_dates` - set this to false (or 0) if you do NOT want usage of human-friendly dates ("Today", "Monday" instead of the standard day-month-year triad) in the UI; the default is true. - `quote_style` - the quote template. The `\\1` means the quoted string. By default it's `> \\1\n`. - `quote_regex` - set to `^([\\s\\S]*)` to add `UI/quote_style` only at the beginning and end of the quote. By default it's `(.+)(?:\n|$)`. - `Fonts/render_type` - select how to render fonts in Quaternion timeline; possible values are "NativeRendering" (default) and "QtRendering". - `Fonts/family` - override the font family for the whole application. If not specified, the default font for your environment is used. - `Fonts/pointSize` - override the font size (in points) for the whole application. If not specified, the default size for your environment is used. - `Fonts/timeline_family` - font family (for example `Monospace`) to display messages in the timeline. If not specified, the application-wide font family is used. - `Fonts/timeline_pointSize` - font size (in points) to display messages in the timeline. If not specified, the application-wide point size is used. - `maybe_read_timer` - threshold time interval in milliseconds for a displayed message to be considered as read. - `use_keychain` - set this to false (or 0) if you explicitly do NOT want to use keychain but prefer to store access token in a dedicated file instead (see the next paragraph); the default is true. - `hyperlink_users` - set this to false (or 0) if you do NOT want to hyperlink matrix user IDs in messages. By default it's true. - `auto_markdown` (EXPERIMENTAL) - since version 0.0.95, and only if built with Qt 5.14 or newer (that pertains to all binaries at GitHub Releases as well as to Flatpaks), Quaternion has experimental support for Markdown when entering messages. Quaternion only treats the message as Markdown if the message starts with `/md` command (the command itself is removed from the message before sending). Setting `auto_markdown` to `true` enables Markdown parsing in all messages that _do not_ start with `/plain` instead. By default, this setting is `false` since the current support of Markdown by Qt is buggy, and the whole functionality in Quaternion is, again, experimental. If you have it enabled (or use `/md` command) feel free to submit bug reports at the usual place. Since version 0.0.95, all Quaternion binaries at GitHub Releases are compiled with Qt Keychain support. It means that Quaternion will try to store your access token(s) in a secure storage configured for your platform. If the storage or Qt Keychain are not available, Quaternion will try to store your access token(s) in a dedicated file with restricted access rights so that only the owner can access them (this doesn't really work on Windows - see below), with the name made from your user id and Matrix device id, in the following directory: - Linux: `$HOME/.local/share/Quotient/quaternion` - macOS: `$HOME/Library/Application Support/Quotient/quaternion` - Windows: `%LOCALAPPDATA%/Quotient/quaternion` Unfortunately, Quaternion cannot enforce proper access rights on Windows; you'll see a warning about it and will be able to either refuse saving your access token in that case or agree and setup file permissions outside Quaternion. Quaternion caches the rooms state and user/room avatars on the file system in a conventional location for your platform, as follows: - Linux: `$HOME/.cache/Quotient/quaternion` - macOS: `$HOME/Library/Cache/Quotient/quaternion` - Windows: `%LOCALAPPDATA%/Quotient/quaternion/cache` Cache files are safe to delete at any time but Quaternion only looks for them when starting up and overwrites them regularly while running; so it only makes sense to delete cache files when Quaternion is not running. If Quaternion doesn't find or cannot fully load cache files at startup it downloads the whole state from Matrix servers. It tries to optimise this process by lazy-loading if the server supports it; in an unlucky case when the server cannot do lazy-loading, initial sync can take much time (up to a minute and even more, depending on the number of rooms and the number of users in them); in the worst case, a larger user account without lazy-loading may crash Quaternion when using Qt older than 5.15. Deleting cache files may help with problems such as missing avatars, rooms stuck in a wrong state etc. ## Troubleshooting Quaternion uses libQuotient under the hood; some Quaternion problems are actually problems of libQuotient. If you haven't found your case below, check also the troubleshooting section in libQuotient README.md. #### Continuously reconnecting though the network is fine If Quaternion starts displaying the message that it couldn't connect to the server and retries more than a couple of times without success, while you're sure you have the network connection - double-check that you don't have Qt bearer management libraries around, as they cause issues with some WiFi networks. To do that, try to find "bearer" directory where your Qt is installed (on Windows it's next to Quaternion executable; on Linux it's a part of Qt installation, usually in `/usr/lib/qt5/plugins`). Then delete or rename it (on Windows) or delete the package that this directory is in (on Linux). Bearer management functionality is officially deprecated and does nothing since Qt 5.15; if you face connectivity problems with Qt 5.15, file an issue at [libQuotient repo](https://github.com/quotient-im/libQuotient/issues). #### No messages in the timeline If Quaternion runs but you can't see any messages in the chat (though you can type them in) - you have either of two problems with Qt Quick (if you are extremely unlucky, both): - You might not have Qt Quick libraries and/or plugins installed. On Linux, this may be a case when you are not using the official packages for your distro. Check the stdout/stderr logs, they are quite clear in such cases. On Windows, Mac, and when using Flatpak, just open an issue (see "Contacts" in the beginning of this README), because most likely not all necessary Qt parts were installed along with Quaternion (which is a packaging bug). - If the logs confirm that QML is up and running but there's still nothing for the timeline, you might have hit an issue with QML view stacking order, such as #355/#356. If you use Qt 5.12 or newer, please file a bug: it should not happen with recent Qt at all. If you are on Linux and have to use older Qt, you have to build Quaternion from sources, passing `-DUSE_QQUICKWIDGET=ON` to CMake. Note that it's prone to crashing on some platforms so it's best to still find a way to run Quaternion with Qt 5.12 (using AppImage, e.g.). #### SSL problems Especially on Windows, if Quaternion starts up but upon an attempt to connect returns a message like "Failed to make SSL context" - correct SSL libraries are not reachable by the Quaternion binary. Re-read the chapter "Requirements", section "Windows" in the beginning of this file and do as it advises (make sure in particular that you use the correct version of OpenSSL). #### DLL hell If you have troubles with dynamic libraries on Windows, [the Dependencies Walker tool aka depends.exe](http://www.dependencywalker.com/) helps a lot in navigating the DLL hell - especially when you have a mixed 32/64-bit environment or have different versions of the same library scattered around. OpenSSL, in particular, is very often dragged along by all kinds of software; and you may have other copies of Qt around which you didn't even know about - e.g., with CMake GUI. Entries in PATH for such programs may lead to the operating system choosing those bundled libraries instead of those you intend to use. #### Logging If you run Quaternion from a console on Windows and want to see log messages, set `QT_LOGGING_TO_CONSOLE=1` so that the output is redirected to the console. When chasing bugs and investigating crashes, it helps to increase the debug level. Thanks to [@eang:matrix.org](https://matrix.to/#/@eang:matrix.org]), libQuotient uses Qt logging categories - the "Troubleshooting" section of the library's `README.md` elaborates on how to setup logging. Note that Quaternion itself doesn't use Qt logging categories yet, only the library does. You may also want to set `QT_MESSAGE_PATTERN` to make logs slightly more informative (see https://doc.qt.io/qt-5/qtglobal.html#qSetMessagePattern for the format description). My (@kitsune's) `QT_MESSAGE_PATTERN` looks as follows: ``` `%{time h:mm:ss.zzz}|%{category}|%{if-debug}D%{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif}|%{message}` ``` (the scary `%{if}`s are just encoding the logging level into its initial letter). ## Screenshot ![Screenshot, thanks to nep-quaternion@packageloss.eu](quaternion.png) Quaternion-0.0.95.1/SECURITY.md000066400000000000000000000046051412757327200157010ustar00rootroot00000000000000# Security Policy ## Supported Versions Only the latest released version of Quaternion is supported with security updates. Also, an effort is put into supporting the latest released version on most recent stable releases of each major Linux distribution (Debian, Ubuntu, Fedora, OpenSuse). Users of older Quaternion versions are strongly advised to upgrade to the latest release - support of those versions is very limited, if provided at all. If you can't do it because your Linux distribution is too old, you likely have other security problems as well; upgrade your Linux distribution! ## Reporting a Vulnerability If you find a significant vulnerability, or evidence of one, use either of the following contacts: - send an email to [Kitsune Ral](mailto:Kitsune-Ral@users.sf.net); or - reach out in Matrix to [@kitsune:matrix.org](https://matrix.to/#/@kitsune:matrix.org) (if you can, switch encryption on). In any of these two options, first indicate that you have such information (do not disclose it yet) and wait for further instructions. By default, we will give credit to anyone who reports a vulnerability in a responsible way so that we can fix it before public disclosure. If you want to remain anonymous or pseudonymous instead, please let us know; we will gladly respect your wishes. If you provide a security fix as a PR, you have no way to remain anonymous; you also thereby lay out the vulnerability itself so this is NOT the right way for undisclosed vulnerabilities, whether or not you want to stay incognito. ## Timeline and commitments Initial reaction to the message about a vulnerability (see above) will be no more than 5 days. From the moment of the private report or public disclosure (if it hasn't been reported earlier in private) of each vulnerability, we take effort to fix it on priority before any other issues. In case of vulnerabilities with [CVSS v2](https://nvd.nist.gov/cvss.cfm) score of 4.0 and higher the commitment is to provide a workaround within 30 days and a full fix within 60 days after the specific information on the vulnerability has been reported to the project by any means (in private or in public). For vulnerabilities with lower score there is no commitment on the timeline, only prioritisation. The full fix doesn't imply that all software functionality remains accessible (in the worst case the vulnerable functionality may be disabled or removed to prevent the attack). Quaternion-0.0.95.1/client/000077500000000000000000000000001412757327200153615ustar00rootroot00000000000000Quaternion-0.0.95.1/client/accountregistry.cpp000066400000000000000000000007721412757327200213200ustar00rootroot00000000000000#include "accountregistry.h" #include void AccountRegistry::add(AccountRegistry::Account* a) { if (contains(a)) return; push_back(a); emit addedAccount(a); } void AccountRegistry::drop(Account* a) { emit aboutToDropAccount(a); removeOne(a); Q_ASSERT(!contains(a)); } bool AccountRegistry::isLoggedIn(const QString &userId) const { return std::any_of(cbegin(), cend(), [&userId](Account* a) { return a->userId() == userId; }); } Quaternion-0.0.95.1/client/accountregistry.h000066400000000000000000000020141412757327200207540ustar00rootroot00000000000000#pragma once #include #include namespace Quotient { class Connection; } class AccountRegistry : public QObject, private QVector { Q_OBJECT public: using Account = Quotient::Connection; using const_iterator = QVector::const_iterator; using const_reference = QVector::const_reference; const QVector& accounts() const { return *this; } void add(Account* a); void drop(Account* a); bool isLoggedIn(const QString& userId) const; const_iterator begin() const { return QVector::begin(); } const_iterator end() const { return QVector::end(); } const_reference front() const { return QVector::front(); } const_reference back() const { return QVector::back(); } using QVector::isEmpty, QVector::empty; using QVector::size, QVector::count, QVector::capacity; using QVector::cbegin, QVector::cend; using QVector::contains; signals: void addedAccount(Account* a); void aboutToDropAccount(Account* a); }; Quaternion-0.0.95.1/client/accountselector.cpp000066400000000000000000000035271412757327200212710ustar00rootroot00000000000000#include "accountselector.h" #include AccountSelector::AccountSelector(const AccountRegistry *registry, QWidget *parent) : QComboBox(parent) { connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, [this] { emit currentAccountChanged(currentAccount()); }); const auto& accounts = registry->accounts(); for (auto* acc: accounts) addItem(acc->userId(), QVariant::fromValue(acc)); connect(registry, &AccountRegistry::addedAccount, this, [this](Account* acc) { if (const auto idx = indexOfAccount(acc); idx == -1) addItem(acc->userId(), QVariant::fromValue(acc)); else qWarning() << "AccountComboBox: refusing to add the same account twice"; }); connect(registry, &AccountRegistry::aboutToDropAccount, this, [this](Account* acc) { if (const auto idx = indexOfAccount(acc); idx != -1) removeItem(idx); else qWarning() << "AccountComboBox: account to drop not found, ignoring"; }); } void AccountSelector::setAccount(Account *newAccount) { if (!newAccount) { setCurrentIndex(-1); return; } if (auto i = indexOfAccount(newAccount); i != -1) { setCurrentIndex(i); return; } Q_ASSERT(false); qWarning() << "AccountComboBox: account for" << newAccount->userId() + '/' + newAccount->deviceId() << "wasn't found in the full list of accounts"; } AccountSelector::Account* AccountSelector::currentAccount() const { return currentData().value(); } int AccountSelector::indexOfAccount(Account* a) const { for (int i = 0; i < count(); ++i) if (itemData(i).value() == a) return i; return -1; } Quaternion-0.0.95.1/client/accountselector.h000066400000000000000000000007111412757327200207260ustar00rootroot00000000000000#pragma once #include "accountregistry.h" #include class AccountSelector : public QComboBox { Q_OBJECT public: using Account = AccountRegistry::Account; AccountSelector(const AccountRegistry* registry, QWidget* parent = nullptr); void setAccount(Account* newAccount); Account* currentAccount() const; int indexOfAccount(Account* a) const; signals: void currentAccountChanged(Account* newAccount); }; Quaternion-0.0.95.1/client/activitydetector.cpp000066400000000000000000000043031412757327200214530ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2016 Malte Brandy * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #include "activitydetector.h" #include #include #include void ActivityDetector::setEnabled(bool enabled) { if (enabled == m_enabled) return; m_enabled = enabled; const auto& topLevels = qApp->topLevelWidgets(); for (auto* w: topLevels) if (!w->isHidden()) w->setMouseTracking(enabled); if (enabled) qApp->installEventFilter(this); else qApp->removeEventFilter(this); qDebug() << "Activity Detector enabled:" << enabled; } bool ActivityDetector::eventFilter(QObject* obj, QEvent* ev) { switch (ev->type()) { case QEvent::KeyPress: case QEvent::FocusIn: case QEvent::MouseMove: case QEvent::MouseButtonPress: emit triggered(); break; default:; } return QObject::eventFilter(obj, ev); } Quaternion-0.0.95.1/client/activitydetector.h000066400000000000000000000031711412757327200211220ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2016 Malte Brandy * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #pragma once #include class ActivityDetector : public QObject { Q_OBJECT public slots: void setEnabled(bool enabled); signals: void triggered(); private: bool m_enabled = false; bool eventFilter(QObject* obj, QEvent* ev) override; }; Quaternion-0.0.95.1/client/chatedit.cpp000066400000000000000000000251211412757327200176530ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2017 Kitsune Ral * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #include "chatedit.h" #include "chatroomwidget.h" #include "htmlfilter.h" #include "timelinewidget.h" #include #include #include #include #include #include #include static const QKeySequence ResetFormatShortcut("Ctrl+M"); ChatEdit::ChatEdit(ChatRoomWidget* c) : KChatEdit(c), chatRoomWidget(c), matchesListPosition(0) { auto* sh = new QShortcut(this); sh->setKey(ResetFormatShortcut); connect(sh, &QShortcut::activated, this, &KChatEdit::resetCurrentFormat); } void ChatEdit::keyPressEvent(QKeyEvent* event) { pickingMentions = false; if (event->key() == Qt::Key_Tab) { triggerCompletion(); return; } cancelCompletion(); KChatEdit::keyPressEvent(event); } void ChatEdit::contextMenuEvent(QContextMenuEvent *event) { auto* menu = createStandardContextMenu(); // The shortcut here is in order to show it to the user; it's the QShortcut // in the constructor that actually triggers on Ctrl+M (no idea // why the QAction doesn't work - because it's not in the main menu?) auto* action = new QAction(tr("Reset formatting"), this); action->setShortcut(ResetFormatShortcut); action->setStatusTip(tr("Reset the current character formatting to the default")); connect(action, &QAction::triggered, this, &KChatEdit::resetCurrentFormat); menu->addAction(action); menu->setAttribute(Qt::WA_DeleteOnClose); menu->popup(event->globalPos()); } void ChatEdit::switchContext(QObject* contextKey) { cancelCompletion(); KChatEdit::switchContext(contextKey); } bool ChatEdit::canInsertFromMimeData(const QMimeData *source) const { return source->hasImage() || KChatEdit::canInsertFromMimeData(source); } void ChatEdit::insertFromMimeData(const QMimeData *source) { if (!source) { qWarning() << "Nothing to insert"; return; } if (source->hasHtml()) { // Before insertion, remove formatting unsupported in Matrix const auto [cleanHtml, errorPos, errorString] = HtmlFilter::fromLocalHtml(source->html()); if (errorPos != -1) { qWarning() << "HTML insertion failed at pos" << errorPos << "with error" << errorString; // FIXME: Come on... It should be app->showStatusMessage() or smth emit chatRoomWidget->timelineWidget()->showStatusMessage( tr("Could not insert HTML - it's either invalid or unsupported"), 5000); return; } insertHtml(cleanHtml); ensureCursorVisible(); } else if (source->hasImage()) emit insertImageRequested(source->imageData().value()); else KChatEdit::insertFromMimeData(source); } void ChatEdit::appendMentionAt(QTextCursor& cursor, QString mention, QUrl mentionUrl, bool select) { Q_ASSERT(!mention.isEmpty() && mentionUrl.isValid()); if (cursor.atStart() && mention.startsWith('/')) mention.push_front('/'); const auto posBeforeMention = cursor.position(); const auto& safeMention = Quotient::sanitized(mention.toHtmlEscaped()); // The most concise way to add a link is by QTextCursor::insertHtml() // as QTextDocument API is unwieldy (get to the block, make a fragment... - // just merging a char format with setAnchor()/setAnchorHref() doesn't work) if (Quotient::Settings().get("UI/hyperlink_users", true)) cursor.insertHtml("" % safeMention % ""); else cursor.insertText(safeMention); cursor.setPosition(posBeforeMention, select ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); ensureCursorVisible(); // The real one, not completionCursor } bool ChatEdit::initCompletion() { completionCursor = textCursor(); completionCursor.clearSelection(); while (completionCursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor)) { const auto& firstChar = completionCursor.selectedText().at(0); if (!firstChar.isLetterOrNumber() && firstChar != '@') { completionCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); break; } } completionMatches = chatRoomWidget->findCompletionMatches(completionCursor.selectedText()); if (completionMatches.isEmpty()) return false; matchesListPosition = 0; // Add punctuation (either a colon and whitespace for salutations, or // just a whitespace for mentions) right away, in preparation for the cycle // of rotating completion matches (that are placed before this punctuation). auto punct = QStringLiteral(" "); static const auto ColonSpace = QStringLiteral(": "); auto lookBehindCursor = completionCursor; if (lookBehindCursor.atStart()) punct = ColonSpace; // Salutation else { for (auto i = 1; i <= ColonSpace.size(); ++i) { lookBehindCursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); if (lookBehindCursor.selectedText().startsWith( ColonSpace.leftRef(i))) { // Replace the colon (with a following space if any found) // before the place of completion with a comma (with a huge // assumption that this colon ends a salutation). // The format is taken from the point of completion, to make // sure the inserted comma doesn't continue the format before // the colon. // TODO: use the fact that mentions are linkified now // to reliably detect salutations even to several users - but // take UI/hyperlink_users into account lookBehindCursor.insertText(QStringLiteral(", "), completionCursor.charFormat()); punct = ColonSpace; break; } } } const auto beforePunct = completionCursor.position(); completionCursor.insertText(punct); completionCursor.setPosition(beforePunct); return true; } void ChatEdit::triggerCompletion() { if (!isCompletionActive() && !initCompletion()) return; Q_ASSERT(!completionMatches.empty() && matchesListPosition < completionMatches.size()); const auto& completionMatch = completionMatches.at(matchesListPosition); appendMentionAt(completionCursor, completionMatch.first, completionMatch.second, true); Q_ASSERT(!completionCursor.selectedText().isEmpty()); auto completionHL = completionCursor.charFormat(); completionHL.setUnderlineStyle(QTextCharFormat::DashUnderline); setExtraSelections({ { completionCursor, completionHL } }); QStringList matchesForSignal; for (const auto& p: completionMatches) matchesForSignal.push_back(p.first); emit proposedCompletion(matchesForSignal, matchesListPosition); matchesListPosition = (matchesListPosition + 1) % completionMatches.length(); } void ChatEdit::cancelCompletion() { completionMatches.clear(); setExtraSelections({}); Q_ASSERT(!isCompletionActive()); emit cancelledCompletion(); } bool ChatEdit::isCompletionActive() { return !completionMatches.isEmpty(); } void ChatEdit::insertMention(QString author, QUrl url) { // The order of inserting text below is such to be convenient for the user // to undo in case the primitive intelligence below fails. auto cursor = textCursor(); // The mention may be hyperlinked, possibly changing the default // character format as a result if the mention happens to be at the end // of the block (which is almost always the case). So remember the format // at the point, and apply it later when printing the postfix. // triggerCompletion() doesn't have that problem because it inserts // the postfix before inserting the mention. auto textFormat = cursor.charFormat(); appendMentionAt(cursor, author, url, false); // Add spaces and a colon around the inserted string if necessary. if (cursor.position() > 0 && document()->characterAt(cursor.position() - 1).isLetterOrNumber()) cursor.insertText(QStringLiteral(" ")); while (cursor.movePosition(QTextCursor::PreviousCharacter) && document()->characterAt(cursor.position()).isSpace()); QString postfix; if (cursor.atStart()) postfix = QStringLiteral(":"); if ((pickingMentions || isCompletionActive()) && document()->characterAt(cursor.position()) == ':') { cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); cursor.insertText(QStringLiteral(",")); postfix = QStringLiteral(":"); } auto editCursor = textCursor(); auto currentChar = document()->characterAt(editCursor.position()); if (editCursor.atBlockEnd() || currentChar.isLetterOrNumber() || currentChar == '.') postfix.push_back(' '); if (!postfix.isEmpty()) editCursor.insertText(postfix, textFormat); pickingMentions = true; cancelCompletion(); } Quaternion-0.0.95.1/client/chatedit.h000066400000000000000000000055211412757327200173220ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2017 Kitsune Ral * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #pragma once #include "kchatedit.h" #include class ChatRoomWidget; class ChatEdit : public KChatEdit { Q_OBJECT public: using completions_t = QVector>; ChatEdit(ChatRoomWidget* c); void triggerCompletion(); void cancelCompletion(); bool isCompletionActive(); void insertMention(QString author, QUrl url); public slots: void switchContext(QObject* contextKey) override; signals: void proposedCompletion(const QStringList& allCompletions, int curIndex); void cancelledCompletion(); void insertImageRequested(const QImage& image); protected: bool canInsertFromMimeData(const QMimeData* source) const override; void insertFromMimeData(const QMimeData* source) override; private: ChatRoomWidget* chatRoomWidget; QTextCursor completionCursor; /// Text/href pairs for completion completions_t completionMatches; int matchesListPosition; bool pickingMentions = false; /// \brief Initialise a new completion /// /// \return true if completion matches exist for the current entry; /// false otherwise bool initCompletion(); void appendMentionAt(QTextCursor& cursor, QString mention, QUrl mentionUrl, bool select); void keyPressEvent(QKeyEvent* event) override; void contextMenuEvent(QContextMenuEvent* event) override; }; Quaternion-0.0.95.1/client/chatroomwidget.cpp000066400000000000000000000701601412757327200211110ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2015 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #include "chatroomwidget.h" #include #include #include #include #include #include #include #include // for last-minute message fixups before sending #include // to produce plain text from /html #include #include #include #include #include #include #include #include #include #include "mainwindow.h" #include "timelinewidget.h" #include "quaternionroom.h" #include "chatedit.h" #include "htmlfilter.h" static auto DefaultPlaceholderText() { return ChatRoomWidget::tr( "Choose a room to send messages or enter a command..."); } static constexpr auto MaxNamesToShow = 5; static constexpr auto SampleSizeForHud = 3; Q_STATIC_ASSERT(MaxNamesToShow > SampleSizeForHud); ChatRoomWidget::ChatRoomWidget(MainWindow* parent) : QWidget(parent) , m_timelineWidget(new TimelineWidget(this)) , m_uiSettings("UI") { auto* qmlContainer = #ifdef DISABLE_QQUICKWIDGET QWidget::createWindowContainer(m_timelineWidget, this); #else m_timelineWidget; #endif // Use different objects but the same method with the same parameters qmlContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); { m_hudCaption = new QLabel(); m_hudCaption->setWordWrap(true); auto f = m_hudCaption->font(); f.setItalic(true); m_hudCaption->setFont(f); m_hudCaption->setTextFormat(Qt::RichText); } auto attachButton = new QToolButton(); attachButton->setAutoRaise(true); m_attachAction = new QAction(QIcon::fromTheme("mail-attachment"), tr("Attach"), attachButton); m_attachAction->setCheckable(true); m_attachAction->setDisabled(true); connect(m_attachAction, &QAction::triggered, this, [this] (bool checked) { if (checked) { attachedFileName = QFileDialog::getOpenFileName(this, tr("Attach file")); } else { if (m_fileToAttach->isOpen()) m_fileToAttach->remove(); attachedFileName.clear(); } if (!attachedFileName.isEmpty()) { m_chatEdit->setPlaceholderText( tr("Add a message to the file or just push Enter")); mainWindow()->showStatusMessage( tr("Attaching %1").arg(attachedFileName)); } else { m_attachAction->setChecked(false); m_chatEdit->setPlaceholderText(DefaultPlaceholderText()); mainWindow()->showStatusMessage(tr("Attaching cancelled"), 3000); } }); attachButton->setDefaultAction(m_attachAction); m_fileToAttach = new QTemporaryFile(this); m_chatEdit = new ChatEdit(this); m_chatEdit->setPlaceholderText(DefaultPlaceholderText()); m_chatEdit->setAcceptRichText(true); // m_uiSettings.get("rich_text_editor", false); m_chatEdit->setMaximumHeight(maximumChatEditHeight()); connect(m_chatEdit, &KChatEdit::returnPressed, this, &ChatRoomWidget::sendInput); connect(m_chatEdit, &KChatEdit::copyRequested, this, [=] { QApplication::clipboard()->setText( m_chatEdit->textCursor().hasSelection() ? m_chatEdit->textCursor().selectedText() : m_timelineWidget->selectedText()); }); connect(m_chatEdit, &ChatEdit::insertImageRequested, this, [=](const QImage& image) { if (currentRoom() == nullptr || m_fileToAttach->isOpen()) return; m_fileToAttach->open(); image.save(m_fileToAttach, "PNG"); attachedFileName = m_fileToAttach->fileName(); m_attachAction->setChecked(true); m_chatEdit->setPlaceholderText( tr("Add a message to the file or just push Enter")); mainWindow()->showStatusMessage( tr("Attaching an image from clipboard")); }); connect(m_chatEdit, &ChatEdit::proposedCompletion, this, [this](QStringList matches, int pos) { Q_ASSERT(pos >= 0 && pos < matches.size()); // If the completion list is MaxNamesToShow or shorter, show all // of it; if it's longer, show SampleSizeForHud entries and // append how many more matches are there. // #344: in any case, drop the current match from the list // ("Next completion:" showing the current match looks wrong) switch (matches.size()) { case 0: setHudHtml(tr("No completions")); return; case 1: setHudHtml({}); // That one match is already in the text return; default:; } matches.removeAt(pos); // Drop the current match (#344) // Replenish the tail of the list from the beginning, if needed std::rotate(matches.begin(), matches.begin() + pos, matches.end()); if (matches.size() > MaxNamesToShow) { const auto moreIt = matches.begin() + SampleSizeForHud; *moreIt = tr("%Ln more completions", "", matches.size() - SampleSizeForHud); matches.erase(moreIt + 1, matches.end()); } setHudHtml(tr("Next completion:"), matches); }); // When completion is cancelled, show typing users, if any connect(m_chatEdit, &ChatEdit::cancelledCompletion, this, &ChatRoomWidget::typingChanged); { QString styleSheet; const auto& fontFamily = m_uiSettings.get("Fonts/timeline_family"); if (!fontFamily.isEmpty()) styleSheet += "font-family: " + fontFamily + ";"; const auto& fontPointSize = m_uiSettings.value("Fonts/timeline_pointSize"); if (fontPointSize.toReal() > 0.0) styleSheet += "font-size: " + fontPointSize.toString() + "pt;"; if (!styleSheet.isEmpty()) setStyleSheet(styleSheet); } auto* layout = new QVBoxLayout(); layout->addWidget(qmlContainer); layout->addWidget(m_hudCaption); { auto inputLayout = new QHBoxLayout; inputLayout->addWidget(attachButton); inputLayout->addWidget(m_chatEdit); layout->addLayout(inputLayout); } setLayout(layout); } TimelineWidget* ChatRoomWidget::timelineWidget() const { return m_timelineWidget; } MainWindow* ChatRoomWidget::mainWindow() const { return static_cast(parent()); } QuaternionRoom* ChatRoomWidget::currentRoom() const { return m_timelineWidget->currentRoom(); } void ChatRoomWidget::setRoom(QuaternionRoom* newRoom) { if (currentRoom() == newRoom) { focusInput(); return; } if (currentRoom()) { currentRoom()->connection()->disconnect(this); currentRoom()->disconnect(this); } attachedFileName.clear(); m_attachAction->setChecked(false); if (m_fileToAttach->isOpen()) m_fileToAttach->remove(); m_timelineWidget->setRoom(newRoom); m_attachAction->setEnabled(newRoom != nullptr); m_chatEdit->switchContext(newRoom); if (newRoom) { using namespace Quotient; focusInput(); connect(newRoom, &Room::typingChanged, // this, &ChatRoomWidget::typingChanged); connect(newRoom, &Room::encryption, // this, &ChatRoomWidget::encryptionChanged); connect(newRoom->connection(), &Connection::loggedOut, this, [this] { qWarning() << "Logged out, escaping the room"; setRoom(nullptr); }); } typingChanged(); encryptionChanged(); } void ChatRoomWidget::typingChanged() { if (!currentRoom() || currentRoom()->usersTyping().isEmpty()) { setHudHtml({}); return; } const auto& usersTyping = currentRoom()->usersTyping(); QStringList typingNames; typingNames.reserve(MaxNamesToShow); const auto endIt = usersTyping.size() > MaxNamesToShow ? usersTyping.cbegin() + SampleSizeForHud : usersTyping.cend(); for (auto it = usersTyping.cbegin(); it != endIt; ++it) typingNames << currentRoom()->safeMemberName((*it)->id()); if (usersTyping.size() > MaxNamesToShow) { typingNames.push_back( //: The number of users in the typing or completion list tr("%L1 more").arg(usersTyping.size() - SampleSizeForHud)); } setHudHtml(tr("Currently typing:"), typingNames); } void ChatRoomWidget::encryptionChanged() { m_chatEdit->setPlaceholderText( currentRoom() ? currentRoom()->usesEncryption() ? tr("Send a message (no end-to-end encryption support yet)...") : tr("Send a message (over %1) or enter a command...", "%1 is the protocol used by the server (usually HTTPS)") .arg(currentRoom()->connection()->homeserver() .scheme().toUpper()) : DefaultPlaceholderText()); } void ChatRoomWidget::setHudHtml(const QString& htmlCaption, const QStringList& plainTextNames) { if (htmlCaption.isEmpty()) { // Fast track m_hudCaption->clear(); return; } auto hudText = htmlCaption; if (!plainTextNames.empty()) { QStringList namesToShow; namesToShow.reserve(plainTextNames.size()); // Elide names that don't fit the HUD line width // NB: averageCharWidth() accounts for a list separator appended by // QLocale::createSeparatedList() appends. It would be ideal // to subtract the specific separator width but there's no way to get // the list separator from QLocale() // (https://bugreports.qt.io/browse/QTBUG-48510) const auto& fm = m_hudCaption->fontMetrics(); for (const auto& name: plainTextNames) { auto elided = fm.elidedText(name, Qt::ElideMiddle, m_hudCaption->width() - fm.averageCharWidth()); // Make sure an elided name takes a new line namesToShow.push_back( (elided != name ? "
" : "") + elided.toHtmlEscaped()); } hudText += ' ' + namesToShow.join(", "); } m_hudCaption->setText(hudText); } void ChatRoomWidget::insertMention(Quotient::User* user) { Q_ASSERT(currentRoom() != nullptr); m_chatEdit->insertMention( user->displayname(currentRoom()), Quotient::Uri(user->id()).toUrl(Quotient::Uri::MatrixToUri)); m_chatEdit->setFocus(); } void ChatRoomWidget::focusInput() { m_chatEdit->setFocus(); } /** * \brief Split the string into the specified number of parts * The function takes \p s and splits it into \p maxParts parts using \p sep * for the separator. Empty parts are skipped. If there are more than * \p maxParts parts in the string, the last returned part includes * the remainder of the string; if there are fewer parts, the missing parts * are filled with empty strings. * \return the vector of references to the original string, one reference for * each part. */ QVector lazySplitRef(const QString& s, QChar sep, int maxParts) { QVector parts { maxParts }; int pos = 0, nextPos = 0; for (; maxParts > 1 && (nextPos = s.indexOf(sep, pos)) > -1; --maxParts) { parts[parts.size() - maxParts] = s.mid(pos, nextPos - pos); while (s[++nextPos] == sep) ; pos = nextPos; } parts[parts.size() - maxParts] = s.mid(pos); return parts; } void ChatRoomWidget::sendFile() { Q_ASSERT(currentRoom() != nullptr); const auto& description = m_chatEdit->toPlainText(); auto txnId = currentRoom()->postFile(description.isEmpty() ? QUrl(attachedFileName).fileName() : description, QUrl::fromLocalFile(attachedFileName)); if (m_fileToAttach->isOpen()) m_fileToAttach->remove(); attachedFileName.clear(); m_attachAction->setChecked(false); m_chatEdit->setPlaceholderText(DefaultPlaceholderText()); } #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) void sendMarkdown(QuaternionRoom* room, const QTextDocumentFragment& text) { room->postHtmlText(text.toPlainText(), HtmlFilter::toMatrixHtml(text.toHtml(), room, HtmlFilter::ConvertMarkdown)); } #endif void ChatRoomWidget::sendMessage() { if (m_chatEdit->toPlainText().startsWith("//")) QTextCursor(m_chatEdit->document()).deleteChar(); #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) if (m_uiSettings.get("auto_markdown", false)) { sendMarkdown(currentRoom(), QTextDocumentFragment(m_chatEdit->document())); return; } #endif const auto& plainText = m_chatEdit->toPlainText(); const auto& htmlText = HtmlFilter::toMatrixHtml(m_chatEdit->toHtml(), currentRoom()); Q_ASSERT(!plainText.isEmpty() && !htmlText.isEmpty()); // Send plain text if htmlText has no markup or just
elements // (those are easily represented as line breaks in plain text) static const QRegularExpression MarkupRE { "<(?![Bb][Rr])" }; if (htmlText.contains(MarkupRE)) currentRoom()->postHtmlText(plainText, htmlText); else currentRoom()->postPlainText(plainText); } static auto NothingToSendMsg() { return ChatRoomWidget::tr("There's nothing to send"); } QString ChatRoomWidget::sendCommand(const QStringRef& command, const QString& argString) { static const auto ReFlags = QRegularExpression::DotMatchesEverythingOption | QRegularExpression::DontCaptureOption; // FIXME: copy-paste from lib/util.cpp static const auto ServerPartPattern = QStringLiteral("(\\[[^]]+\\]|[-[:alnum:].]+)" // Either IPv6 address or // hostname/IPv4 address "(:\\d{1,5})?" // Optional port ); static const auto UserIdPattern = QString("@[-[:alnum:]._=/]+:" % ServerPartPattern); static const QRegularExpression RoomIdRE { "^([#!][^:[:space:]]+):" % ServerPartPattern % '$', ReFlags }, UserIdRE { '^' % UserIdPattern % '$', ReFlags }; Q_ASSERT(RoomIdRE.isValid() && UserIdRE.isValid()); // Commands available without a current room if (command == "join") { if (!argString.contains(RoomIdRE)) return tr("/join argument doesn't look like a room ID or alias"); mainWindow()->openResource(argString, "join"); return {}; } if (command == "quit") { qApp->closeAllWindows(); return {}; } // --- Add more roomless commands here if (!currentRoom()) { return tr("There's no such /command outside of room."); } // Commands available only in the room context using namespace Quotient; if (command == "leave" || command == "part") { if (!argString.isEmpty()) return tr("Sending a farewell message is not supported yet." " If you intended to leave another room, switch to it" " and type /leave there."); currentRoom()->leaveRoom(); return {}; } if (command == "forget") { if (argString.isEmpty()) return tr("/forget must be followed by the room id/alias," " even for the current room"); if (!argString.contains(RoomIdRE)) return tr("%1 doesn't look like a room id or alias").arg(argString); // Forget the specified room using the current room's connection currentRoom()->connection()->forgetRoom(argString); return {}; } if (command == "invite") { if (argString.isEmpty()) return tr("/invite "); if (!argString.contains(UserIdRE)) return tr("%1 doesn't look like a user ID").arg(argString); currentRoom()->inviteToRoom(argString); return {}; } if (command == "kick" || command == "ban") { const auto args = lazySplitRef(argString, ' ', 2); if (args.front().isEmpty()) return tr("/%1 ").arg(command.toString()); if (!UserIdRE.match(args.front()).hasMatch()) return tr("%1 doesn't look like a user id") .arg(args.front()); if (command == "ban") currentRoom()->ban(args.front(), args.back()); else { auto* const user = currentRoom()->user(args.front()); if (currentRoom()->memberJoinState(user) != JoinState::Join) return tr("%1 is not a member of this room") .arg(user ? user->fullName(currentRoom()) : args.front()); currentRoom()->kickMember(user->id(), args.back()); } return {}; } if (command == "unban") { if (argString.isEmpty()) return tr("/unban "); if (!argString.contains(UserIdRE)) return tr("/unban argument doesn't look like a user ID"); currentRoom()->unban(argString); return {}; } if (command == "ignore" || command == "unignore") { if (argString.isEmpty()) return tr("/ignore "); if (!argString.contains(UserIdRE)) return tr("/ignore argument doesn't look like a user ID"); if (auto* user = currentRoom()->user(argString)) { if (command == "ignore") user->ignore(); else user->unmarkIgnore(); return {}; } return tr("Couldn't find user %1 on the server").arg(argString); } using MsgType = RoomMessageEvent::MsgType; if (command == "me") { if (argString.isEmpty()) return tr("/me needs an argument"); currentRoom()->postMessage(argString, MsgType::Emote); return {}; } if (command == "notice") { if (argString.isEmpty()) return tr("/notice needs an argument"); currentRoom()->postMessage(argString, MsgType::Notice); return {}; } if (command == "shrug") // Peeked at Discord { currentRoom()->postPlainText((argString.isEmpty() ? "" : argString + " ") + "¯\\_(ツ)_/¯"); return {}; } if (command == "roomname") { currentRoom()->setName(argString); return {}; } if (command == "topic") { currentRoom()->setTopic(argString); return {}; } if (command == "nick" || command == "mynick") { currentRoom()->localUser()->rename(argString); return {}; } if (command == "roomnick" || command == "myroomnick") { currentRoom()->localUser()->rename(argString, currentRoom()); return {}; } if (command == "pm" || command == "msg") { const auto args = lazySplitRef(argString, ' ', 2); if (args.front().isEmpty() || (args.back().isEmpty() && command == "msg")) return tr("/%1 ").arg(command.toString()); if (RoomIdRE.match(args.front()).hasMatch() && command == "msg") { if (auto* room = currentRoom()->connection()->room(args.front())) { room->postPlainText(args.back()); return {}; } return tr("%1 doesn't seem to have joined room %2") .arg(currentRoom()->localUser()->id(), args.front()); } if (UserIdRE.match(args.front()).hasMatch()) { if (args.back().isEmpty()) currentRoom()->connection()->requestDirectChat(args.front()); else currentRoom()->connection()->doInDirectChat(args.front(), [msg=args.back()] (Room* dc) { dc->postPlainText(msg); }); return {}; } return tr("%1 doesn't look like a user id or room alias") .arg(args.front()); } if (command == "plain") { // argString eats away leading spaces, so can't be used here static const auto CmdLen = QStringLiteral("/plain ").size(); const auto& plainMsg = m_chatEdit->toPlainText().mid(CmdLen); if (plainMsg.isEmpty()) return NothingToSendMsg(); currentRoom()->postPlainText(plainMsg); return {}; } if (command == "html") { // Assuming Matrix HTML, convert it to Qt and load to a fragment in // order to produce a plain text version (maybe introduce // filterMatrixHtmlToPlainText() one day instead...); then convert // back to Matrix HTML to produce the (clean) rich text version // of the message const auto& [cleanQtHtml, errorPos, errorString] = HtmlFilter::fromMatrixHtml(argString, currentRoom(), HtmlFilter::Validate); if (errorPos != -1) return tr("At pos %1: %2", "%1 is a position of the error; %2 is the error message") .arg(errorPos).arg(errorString); const auto& fragment = QTextDocumentFragment::fromHtml(cleanQtHtml); currentRoom()->postHtmlText(fragment.toPlainText(), HtmlFilter::toMatrixHtml(fragment.toHtml(), currentRoom())); return {}; } if (command == "md") { #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) // Select everything after /md and one whitespace character after it // (leading whitespaces have meaning in Markdown) QTextCursor c(m_chatEdit->document()); c.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 4); c.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); sendMarkdown(currentRoom(), c.selection()); return {}; #else return tr("Your build of Quaternion doesn't support Markdown"); #endif } if (command == "query" || command == "dc") { if (argString.isEmpty()) return tr("/%1 ").arg(command.toString()); if (!argString.contains(UserIdRE)) return tr("%1 doesn't look like a user id").arg(argString); currentRoom()->connection()->requestDirectChat(argString); return {}; } // --- Add more room commands here qDebug() << "Unknown command:" << command; return tr("Unknown /command. Use // to send this line literally"); } void ChatRoomWidget::sendInput() { if (!attachedFileName.isEmpty()) sendFile(); else { const auto& text = m_chatEdit->toPlainText(); QString error; if (text.isEmpty()) error = NothingToSendMsg(); else if (text.startsWith('/') && !text.midRef(1).startsWith('/')) { QRegularExpression cmdSplit( "(\\w+)(?:\\s+(.*))?", QRegularExpression::DotMatchesEverythingOption); const auto& blanksMatch = cmdSplit.match(text, 1); error = sendCommand(blanksMatch.capturedRef(1), blanksMatch.captured(2)); } else if (!currentRoom()) error = tr("You should select a room to send messages."); else sendMessage(); if (!error.isEmpty()) { mainWindow()->showStatusMessage(error, 5000); return; } } m_chatEdit->saveInput(); } ChatRoomWidget::completions_t ChatRoomWidget::findCompletionMatches(const QString& pattern) const { completions_t matches; if (currentRoom()) { const auto& users = currentRoom()->users(); for (auto user: users) { using Quotient::Uri; if (user->displayname(currentRoom()) .startsWith(pattern, Qt::CaseInsensitive) || user->id().startsWith(pattern, Qt::CaseInsensitive)) matches.push_back({ user->displayname(currentRoom()), Uri(user->id()).toUrl(Uri::MatrixToUri) }); } std::sort(matches.begin(), matches.end(), [] (const auto& p1, const auto& p2) { return p1.first.localeAwareCompare(p2.first) < 0; }); } return matches; } void ChatRoomWidget::quote(const QString& htmlText) { const auto type = m_uiSettings.get("quote_type"); const auto defaultStyle = QStringLiteral("> \\1\n"); const auto defaultRegex = QStringLiteral("(.+)(?:\n|$)"); auto style = m_uiSettings.get("quote_style"); auto regex = m_uiSettings.get("quote_regex"); if (style.isEmpty()) style = defaultStyle; if (regex.isEmpty()) regex = defaultRegex; QTextDocument document; document.setHtml(htmlText); QString sendString; switch (type) { case 0: sendString = document.toPlainText() .replace(QRegularExpression(defaultRegex), defaultStyle); break; case 1: sendString = document.toPlainText() .replace(QRegularExpression(regex), style); break; case 2: sendString = QLocale().quoteString(document.toPlainText()) + "\n"; break; } m_chatEdit->insertPlainText(sendString); } void ChatRoomWidget::resizeEvent(QResizeEvent*) { m_chatEdit->setMaximumHeight(maximumChatEditHeight()); } void ChatRoomWidget::keyPressEvent(QKeyEvent* event) { // This only handles keypresses not handled by ChatEdit; in particular, // this means that PageUp/PageDown below are actually Ctrl-PageUp/PageDown switch (event->key()) { case Qt::Key_PageUp: emit m_timelineWidget->pageUpPressed(); break; case Qt::Key_PageDown: emit m_timelineWidget->pageDownPressed(); break; } } int ChatRoomWidget::maximumChatEditHeight() const { return height() / 3; } void ChatRoomWidget::fileDrop(const QString& url) { attachedFileName = QUrl(url).path(); m_attachAction->setChecked(true); m_chatEdit->setPlaceholderText( tr("Add a message to the file or just push Enter")); mainWindow()->showStatusMessage(tr("Attaching %1").arg(attachedFileName)); } void ChatRoomWidget::htmlDrop(const QString &html) { m_chatEdit->insertHtml(html); } void ChatRoomWidget::textDrop(const QString& text) { m_chatEdit->insertPlainText(text); } Qt::KeyboardModifiers ChatRoomWidget::getModifierKeys() const { return QGuiApplication::keyboardModifiers(); } Quaternion-0.0.95.1/client/chatroomwidget.h000066400000000000000000000065441412757327200205630ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2015 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #pragma once #include "chatedit.h" #include #include class TimelineWidget; class QuaternionRoom; class MainWindow; class QLabel; class QAction; class QTextDocument; class QMimeData; class QTemporaryFile; namespace Quotient { class User; } class ChatRoomWidget : public QWidget { Q_OBJECT public: using completions_t = ChatEdit::completions_t; explicit ChatRoomWidget(MainWindow* parent = nullptr); TimelineWidget* timelineWidget() const; completions_t findCompletionMatches(const QString& pattern) const; Q_INVOKABLE Qt::KeyboardModifiers getModifierKeys() const; public slots: void setRoom(QuaternionRoom* newRoom); void insertMention(Quotient::User* user); void focusInput(); /// Set a line just above the message input, with optional list of /// member displaynames void setHudHtml(const QString& htmlCaption, const QStringList& plainTextNames = {}); void typingChanged(); void quote(const QString& htmlText); void fileDrop(const QString& url); void htmlDrop(const QString& html); void textDrop(const QString& text); private slots: void sendInput(); void encryptionChanged(); private: TimelineWidget* m_timelineWidget; QLabel* m_hudCaption; //< For typing and completion notifications QAction* m_attachAction; ChatEdit* m_chatEdit; QString attachedFileName; QTemporaryFile* m_fileToAttach; Quotient::SettingsGroup m_uiSettings; MainWindow* mainWindow() const; QuaternionRoom* currentRoom() const; void sendFile(); void sendMessage(); [[nodiscard]] QString sendCommand(const QStringRef& command, const QString& argString); void resizeEvent(QResizeEvent*) override; void keyPressEvent(QKeyEvent* event) override; int maximumChatEditHeight() const; }; Quaternion-0.0.95.1/client/dialog.cpp000066400000000000000000000067721412757327200173400ustar00rootroot00000000000000/****************************************************************************** * Copyright (C) 2017 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "dialog.h" #include #include Dialog::Dialog(const QString& title, QWidget *parent, UseStatusLine useStatusLine, const QString& applyTitle, QDialogButtonBox::StandardButtons addButtons) : Dialog(title , QDialogButtonBox::Ok | /*QDialogButtonBox::Cancel |*/ addButtons , parent, useStatusLine) { if (!applyTitle.isEmpty()) buttons->button(QDialogButtonBox::Ok)->setText(applyTitle); } Dialog::Dialog(const QString& title, QDialogButtonBox::StandardButtons setButtons, QWidget *parent, UseStatusLine useStatusLine) : QDialog(parent) , applyLatency(useStatusLine) , pendingApplyMessage(tr("Applying changes, please wait")) , statusLabel(useStatusLine == NoStatusLine ? nullptr : new QLabel) , buttons(new QDialogButtonBox(setButtons)) , outerLayout(this) { setWindowTitle(title); connect(buttons, &QDialogButtonBox::clicked, this, &Dialog::buttonClicked); outerLayout.addWidget(buttons); if (statusLabel) outerLayout.addWidget(statusLabel); } void Dialog::addLayout(QLayout* l, int stretch) { int offset = 1 + (statusLabel != nullptr); outerLayout.insertLayout(outerLayout.count() - offset, l, stretch); } void Dialog::addWidget(QWidget* w, int stretch, Qt::Alignment alignment) { int offset = 1 + (statusLabel != nullptr); outerLayout.insertWidget(outerLayout.count() - offset, w, stretch, alignment); } QPushButton*Dialog::button(QDialogButtonBox::StandardButton which) { return buttonBox()->button(which); } void Dialog::reactivate() { if (!isVisible()) { load(); show(); } raise(); activateWindow(); } void Dialog::setStatusMessage(const QString& msg) { Q_ASSERT(statusLabel); statusLabel->setText(msg); } void Dialog::applyFailed(const QString& errorMessage) { setStatusMessage(errorMessage); setDisabled(false); } void Dialog::buttonClicked(QAbstractButton* button) { switch (buttons->buttonRole(button)) { case QDialogButtonBox::AcceptRole: case QDialogButtonBox::YesRole: if (validate()) { if (statusLabel) statusLabel->setText(pendingApplyMessage); setDisabled(true); apply(); } break; case QDialogButtonBox::ResetRole: load(); break; case QDialogButtonBox::RejectRole: case QDialogButtonBox::NoRole: reject(); break; default: ; // Derived classes may completely replace or reuse this method } } Quaternion-0.0.95.1/client/dialog.h000066400000000000000000000103741412757327200167760ustar00rootroot00000000000000/****************************************************************************** * Copyright (C) 2017 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include #include #include #include class QAbstractButton; class QLabel; class Dialog : public QDialog { Q_OBJECT public: enum UseStatusLine { NoStatusLine, StatusLine }; static const auto NoExtraButtons = QDialogButtonBox::NoButton; explicit Dialog(const QString& title, QWidget *parent = nullptr, UseStatusLine useStatusLine = NoStatusLine, const QString& applyTitle = {}, QDialogButtonBox::StandardButtons addButtons = QDialogButtonBox::Reset); explicit Dialog(const QString& title, QDialogButtonBox::StandardButtons setButtons, QWidget *parent = nullptr, UseStatusLine useStatusLine = NoStatusLine); /// Create and add a layout of the given type /*! This creates a new layout object and adds it to the bottom of * the dialog client area (i.e., above the button box). */ template LayoutT* addLayout(int stretch = 0) { auto l = new LayoutT; addLayout(l, stretch); return l; } /// Add a layout to the bottom of the dialog's client area void addLayout(QLayout* l, int stretch = 0); /// Add a widget to the bottom of the dialog's client area void addWidget(QWidget* w, int stretch = 0, Qt::Alignment alignment = {}); QPushButton* button(QDialogButtonBox::StandardButton which); public slots: /// Show or raise the dialog void reactivate(); /// Set the status line of the dialog window void setStatusMessage(const QString& msg); /// Return to the dialog after a failed apply void applyFailed(const QString& errorMessage); protected: /// (Re-)Load data in the dialog /*! \sa buttonClicked */ virtual void load() { } /// Check data in the dialog before accepting /*! \sa apply, buttonClicked */ virtual bool validate() { return true; } /// Apply changes and close the dialog /*! * This method is invoked upon clicking the "apply" button (by default * it's the one with `AcceptRole`), if validate() returned true. * \sa buttonClicked, validate */ virtual void apply() { accept(); } /// React to a click of a button in the dialog box /*! * This virtual function is invoked every time one of push buttons * in the dialog button box is clicked; it defines how the dialog reacts * to each button. By default, it calls validate() and, if it succeeds, * apply() on buttons with `AcceptRole`; cancels the dialog on * `RejectRole`; and reloads the fields on `ResetRole`. Override this * method to change this behaviour. * \sa validate, apply, reject, load */ virtual void buttonClicked(QAbstractButton* button); QDialogButtonBox* buttonBox() const { return buttons; } QLabel* statusLine() const { return statusLabel; } void setPendingApplyMessage(const QString& msg) { pendingApplyMessage = msg; } private: UseStatusLine applyLatency; QString pendingApplyMessage; QLabel* statusLabel; QDialogButtonBox* buttons; QVBoxLayout outerLayout; }; Quaternion-0.0.95.1/client/htmlfilter.cpp000066400000000000000000000775031412757327200202530ustar00rootroot00000000000000#include "htmlfilter.h" #include #include #include #include #include #include #include #include using namespace std; namespace HtmlFilter { enum Mode : unsigned char { QtToMatrix, MatrixToQt, GenericToQt }; class Processor { public: [[nodiscard]] static Result process(QString html, Mode mode, QuaternionRoom* context, Options options = Default); private: const Mode mode; const Options options; QuaternionRoom* const context; QXmlStreamWriter& writer; int errorPos = -1; QString errorString {}; Processor(Mode mode, Options options, QuaternionRoom* context, QXmlStreamWriter& writer) : mode(mode), options(options), context(context), writer(writer) {} void runOn(const QString& html); using rewrite_t = vector>; [[nodiscard]] rewrite_t filterTag(const QStringRef& tag, QXmlStreamAttributes attributes); void filterText(QString& text); }; static const QString permittedTags[] = { "font", "del", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "p", "a", "ul", "ol", "sup", "sub", "li", "b", "i", "u", "strong", "em", "strike", "code", "hr", "br", "div", "table", "thead", "tbody", "tr", "th", "td", "caption", "pre", "span", "img", "mx-reply" }; struct PassList { const char* tag; vector allowedAttrs; }; // See filterTag() on special processing of commented out tags/attributes static const PassList passLists[] = { { "a", { "name", "target", /* "href" - only from permittedSchemes */ } }, { "img", { "width", "height", "alt", "title", "data-mx-emoticon" /* "src" - only 'mxc:' */ } }, { "ol", { "start" } }, { "font", { "color", "data-mx-color", "data-mx-bg-color" } }, { "span", { "color", "data-mx-color", "data-mx-bg-color" } } //, { "code", { "class" /* must start with 'language-' */ } } // Special case }; static const char* const permittedSchemes[] { "http:", "https:", "ftp:", "mailto:", "magnet:", "matrix:" }; static const auto& htmlColorAttr = QStringLiteral("color"); static const auto& htmlStyleAttr = QStringLiteral("style"); static const auto& mxColorAttr = QStringLiteral("data-mx-color"); static const auto& mxBgColorAttr = QStringLiteral("data-mx-bg-color"); #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) [[nodiscard]] QString mergeMarkdown(const QString& html) { // This code intends to merge user-entered Markdown+HTML markup // (HTML-escaped at this point) into HTML exported by QTextDocument. // Unfortunately, Markdown engine of QTextDocument is not dealing well // with ampersands and &-escaped HTML entities inside HTML tags: // see https://bugreports.qt.io/browse/QTBUG-91222 // Instead, Processor::runOn() splits segments between HTML tags and // filterText() treats each of them as Markdown individually. QXmlStreamReader reader(html); QString mdWithHtml; QXmlStreamWriter writer(&mdWithHtml); while (reader.readNext() != QXmlStreamReader::StartElement || reader.qualifiedName() != "p") if (reader.atEnd()) { Q_ASSERT_X(false, __FUNCTION__, "Malformed Qt markup"); qCritical() << "The passed text doesn't seem to come from QTextDocument"; return {}; } int depth = 1; // Count

just entered while (!reader.atEnd()) { // Minimal validation, just pipe things through // decoding what needs decoding const auto tokenType = reader.readNext(); switch (tokenType) { case QXmlStreamReader::Characters: case QXmlStreamReader::EntityReference: { auto text = reader.text().toString(); if (depth > 1) break; // Flush the writer's buffer before side-writing writer.writeCharacters({}); mdWithHtml += text; // Append text as is continue; } case QXmlStreamReader::StartElement: ++depth; if (reader.qualifiedName() != "p") break; // Convert

elements except the first one // to Markdown paragraph breaks writer.writeCharacters("\n\n"); continue; case QXmlStreamReader::EndElement: --depth; if (reader.qualifiedName() == "p") continue; // See above in StartElement break; case QXmlStreamReader::Comment: continue; // Just drop comments default: qWarning() << "Unexpected token, type" << tokenType; } if (depth < 0) { Q_ASSERT(tokenType == QXmlStreamReader::EndElement && reader.qualifiedName() == "body"); break; } writer.writeCurrentToken(reader); } writer.writeEndElement(); QTextDocument doc; doc.setMarkdown(mdWithHtml); return doc.toHtml(); } #endif [[nodiscard]] inline bool isTagNameTerminator(QChar c) { return c.isSpace() || c == '/' || c == '>'; } /*! \brief Massage user HTML to look more like XHTML * * Since Qt doesn't have an HTML parser (outside of QTextDocument) * Processor::runOn() uses QXmlStreamReader instead, and it's quite picky * about properly closed tags and escaped ampersands. Processor::process() * deals with the ampersands; this helper further tries to convert the passed * HTML to something more XHTML-like, so that the XML reader doesn't choke on, * e.g., unclosed `br` or `img` tags and minimised HTML attributes. It also * filters away tags that are not compliant with Matrix specification, where * appropriate. */ [[nodiscard]] Result preprocess(QString html, Mode mode, Options options) { Q_ASSERT(mode != QtToMatrix); bool isFragment = mode == MatrixToQt; bool inHead = false; for (auto pos = html.indexOf('<'); pos != -1; pos = html.indexOf('<', pos)) { const auto tagNamePos = pos + 1 + (html[pos + 1] == '/'); const auto uncheckedHtml = html.midRef(tagNamePos); static const QLatin1String commentOpen("!--"); static const QLatin1String commentClose("-->"); if (uncheckedHtml.startsWith(commentOpen)) { // Skip comments pos = html.indexOf(commentClose, tagNamePos + commentOpen.size()) + commentClose.size(); continue; } // Look ahead to detect stray < and escape it auto gtPos = html.indexOf('>', tagNamePos); decltype(pos) nextLtPos; if (gtPos == tagNamePos /* <> or */ || gtPos == -1 /* no more > */ || ((nextLtPos = html.indexOf('<', tagNamePos)) != -1 && nextLtPos < gtPos) /* there's another < before > */) { static const auto to = QStringLiteral("<"); html.replace(pos, 1, to); pos += to.size(); // Put pos after the escaped sequence continue; } if (uncheckedHtml.startsWith("head>", Qt::CaseInsensitive)) { if (mode == MatrixToQt) { // Matrix spec doesn't allow ; report if it occurs in // user input (Validate is on) or remove the whole header if // it comes from the wire (Validate is off). if (options.testFlag(Validate)) return { {}, pos, " elements are not allowed in Matrix" }; static const QLatin1String HeadEnd(""); const auto headEndPos = html.indexOf(HeadEnd, tagNamePos, Qt::CaseInsensitive); html.remove(pos, headEndPos - pos + HeadEnd.size()); continue; } Q_ASSERT(mode == GenericToQt); inHead = html[pos + 1] != '/'; // Track header entry and exit if (!inHead) { // Just exited, pos = gtPos + 1; continue; } } const auto tagEndIt = find_if(uncheckedHtml.cbegin(), uncheckedHtml.cend(), isTagNameTerminator); const auto tag = uncheckedHtml.left(int(tagEndIt - uncheckedHtml.cbegin())) .toString() .toLower(); // contents are necessary to apply styles but obviously // neither `head` nor tags inside of it are in permittedTags; // however, minimised attributes still have to be handled everywhere // and tags should be closed if (mode == GenericToQt && tag == "html") { // Only in generic mode, allow element pos += tagNamePos + tag.size() + 1; isFragment = false; continue; } if (!inHead) { // Check if it's a valid (opening or closing) tag allowed in Matrix const auto tagIt = find(cbegin(permittedTags), cend(permittedTags), tag); if (tagIt == cend(permittedTags)) { // Invalid tag or non-tag - either remove the abusing piece // or stop and report if (options.testFlag(Validate)) return { {}, pos, "Non-tag or disallowed tag: " % uncheckedHtml.left(gtPos - tagNamePos) }; html.remove(pos, gtPos - pos + 1); continue; } } // Treat minimised attributes // (https://www.w3.org/TR/xhtml1/diffs.html#h-4.5) // There's no simple way to replace all occurences within // a string segment; so just go through the segment and insert // `=''` after minimized attributes. // This is not the place to _filter_ allowed/disallowed attributes - // filtering is left for filterTag() static const QRegularExpression MinAttrRE { R"(([^[:space:]>/"'=]+)\s*(=\s*([^[:space:]>/"']|"[^"]*"|'[^']*')+)?)" }; pos = tagNamePos + tag.size(); QRegularExpressionMatch m; while ((m = MinAttrRE.match(html, pos)).hasMatch() && m.capturedEnd(1) < gtPos) { pos = m.capturedEnd(); if (m.captured(2).isEmpty()) { static const auto attrValue = QString("=''"); html.insert(m.capturedEnd(1), attrValue); gtPos += attrValue.size() - 1; pos += attrValue.size() - 1; } } // Make sure empty elements are properly closed static const QRegularExpression EmptyElementRE { "^img|[hb]r|meta$", QRegularExpression::CaseInsensitiveOption }; if (html[gtPos - 1] != '/' && EmptyElementRE.match(tag).hasMatch()) { html.insert(gtPos, '/'); ++gtPos; } pos = gtPos + 1; Q_ASSERT(pos > 0); } // Wrap in a no-op tag to make the text look like valid XML if it's // a fragment (always the case when HTML comes from a homeserver, and // possibly with generic HTML). if (isFragment) html = "" % html % ""; return { html }; } Result Processor::process(QString html, Mode mode, QuaternionRoom* context, Options options) { // Since Qt doesn't have an HTML parser (outside of QTextDocument; and // the one in QTextDocument is opinionated and not configurable) // Processor::runOn() uses QXmlStreamReader instead. Being an XML parser, // this class is quite picky about properly closed tags and escaped // ampersands. Before passing to runOn(), the following code tries to bring // the passed HTML to something more XHTML-like, so that the XML parser // doesn't choke on things HTML-but-not-XML. In QtToMatrix mode the only // such thing is unescaped ampersands in attributes (especially `href`), // since QTextDocument::toHtml() produces (otherwise) valid XHTML. In other // modes no such assumption can be made so an attempt is taken to close // elements that are normally empty (`br`, `hr` and `img`), turn minimised // attributes to their full interpretations (`disabled -> disabled=''`) // and remove things that are obvious non-tags around unescaped `<` // characters. // 1. Escape ampersands outside of character entities html.replace(QRegularExpression("&(?!(#[0-9]+" // clang-format off "|#x[0-9a-fA-F]+" "|[[:alpha:]_][-[:alnum:]_:.]*" ");)"), // clang-format on "&"); if (mode == QtToMatrix) { #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) if (options.testFlag(ConvertMarkdown)) { // The processor handles Markdown in chunks between HTML tags; //
breaks character sequences that are otherwise valid // Markdown, leading to issues with, e.g., lists. html.replace("
", QStringLiteral("\n")); # if 0 html = mergeMarkdown(html); if (html.isEmpty()) return { "", 0, "This markup doesn't seem to be sourced from Qt" }; options &= ~ConvertMarkdown; # endif } #endif } else { auto r = preprocess(html, mode, options); if (r.errorPos != -1) return r; html = r.filteredHtml; } QString resultHtml; QXmlStreamWriter writer(&resultHtml); writer.setAutoFormatting(false); Processor p { mode, options, context, writer }; p.runOn(html); return { resultHtml, p.errorPos, p.errorString }; } QString toMatrixHtml(const QString& qtMarkup, QuaternionRoom* context, Options options) { // Validation of HTML emitted by Qt doesn't make much sense Q_ASSERT(!options.testFlag(Validate)); const auto& result = Processor::process(qtMarkup, QtToMatrix, context, options); Q_ASSERT(result.errorPos == -1); return result.filteredHtml; } Result fromMatrixHtml(const QString& matrixHtml, QuaternionRoom* context, Options options) { #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) // Matrix HTML body should never be treated as Markdown Q_ASSERT(!options.testFlag(ConvertMarkdown)); #endif auto result = Processor::process(matrixHtml, MatrixToQt, context, options); if (result.errorPos == -1) { // Make sure to preserve whitespace sequences result.filteredHtml = "" % result.filteredHtml % ""; } return result; } Result fromLocalHtml(const QString& html, QuaternionRoom* context, Options options) { return Processor::process(html, GenericToQt, context, options); } void Processor::runOn(const QString &html) { QXmlStreamReader reader(html); /// The entry in the (outer) stack corresponds to each level in the source /// document; the (inner) stack in each entry records open elements in the /// target document. using open_tags_t = stack>; stack> tagsStack; /// Accumulates characters and resolved entry references until the next /// tag (opening or closing); used to linkify (or process Markdown in) /// text parts. QString textBuffer; int bodyOffset = 0; bool firstElement = true, inAnchor = false, inHead = false; while (!reader.atEnd()) { const auto tokenType = reader.readNext(); if (bodyOffset == -1) // See below in 'case StartElement:' bodyOffset = reader.characterOffset(); if (inHead) { writer.writeCurrentToken(reader); if (tokenType == QXmlStreamReader::EndElement && reader.qualifiedName() == "head") { inHead = false; } continue; } if (!textBuffer.isEmpty() && !reader.isCharacters() && !reader.isEntityReference()) filterText(textBuffer); switch (tokenType) { case QXmlStreamReader::StartElement: { const auto& tagName = reader.qualifiedName(); if (tagsStack.empty()) { // These tags are invalid anywhere deeper, and we don't even // care to put them to tagsStack if (tagName == "html") break; // Just ignore, get to the content inside if (tagName == "head") { if (mode == GenericToQt) { inHead = true; writer.writeCurrentToken(reader); continue; } reader.skipCurrentElement(); // Entirely uninteresting break; } if (tagName == "body") { // Skip but note the encounter bodyOffset = -1; // Reuse the variable until the next loop break; } } const auto& attrs = reader.attributes(); if (find_if(attrs.cbegin(), attrs.cend(), [](const auto& a) { return a.qualifiedName() == "style" && a.value().contains( "-qt-paragraph-type:empty"); }) != attrs.cend()) { reader.skipCurrentElement(); continue; // Hidden text block, just skip it } tagsStack.emplace(); if (tagsStack.size() > 100) qCritical() << "CS API spec limits HTML tags depth at 100"; // Qt hardcodes the link style in a `` under ``. // This breaks the looks on the receiving side if the sender // uses a different style of links from that of the receiver. // Since Qt decorates links when importing HTML anyway, we // don't lose anything if we just strip away this span tag. if (mode != MatrixToQt && inAnchor && textBuffer.isEmpty() && tagName == "span" && attrs.size() == 1 && attrs.front().qualifiedName() == "style") continue; // inAnchor == true ==> firstElement == false // Skip the first top-level

and replace further top-level // `

...

` with `
...` - kinda controversial but // there's no cleaner way to get rid of the single top-level

// generated by Qt without assuming that it's the only

// spanning the whole body (copy-pasting rich text from other // editors can bring several legitimate paragraphs of text, // e.g.). This is also a very special case where a converted tag // is immediately closed, unlike the one in the source text; // which is why it's checked here rather than in filterTag(). if (mode == QtToMatrix && tagName == "p" && tagsStack.size() == 1 /* top-level, just emplaced */) { if (firstElement) continue; // Skip unsetting firstElement at the loop end writer.writeEmptyElement("br"); break; } if (tagName != "mx-reply" || (firstElement && !options.testFlag(Fragment))) { // ^ The spec only allows `` at the very beginning // and it's not supposed to be in the user input const auto& rewrite = filterTag(tagName, attrs); for (const auto& [tag, attrs]: rewrite) { tagsStack.top().push(tag); writer.writeStartElement(tag); writer.writeAttributes(attrs); if (tag == "a") inAnchor = true; } } break; } case QXmlStreamReader::Characters: case QXmlStreamReader::EntityReference: { if (firstElement && mode == QtToMatrix) { // Remove the line break Qt inserts after because it // adds an unnecessary whitespace in the HTML context and // an unnecessary line break in the Markdown context. if (reader.text().startsWith('\n')) { textBuffer += reader.text().mid(1); continue; // Maintain firstElement } } // Outside of links, defer writing until the next non-character, // non-entity reference token in order to pass the whole text // piece to filterText() with all entity references resolved. if (!inAnchor && !options.testFlag(Fragment)) textBuffer += reader.text(); else writer.writeCurrentToken(reader); break; } case QXmlStreamReader::EndElement: if (tagsStack.empty()) { const auto& tagName = reader.qualifiedName(); if (tagName != "body" && tagName != "html") qWarning() << "filterHtml(): empty tags stack, skipping" << ('/' + tagName); break; } // Close as many elements as were opened in case StartElement for (auto& t = tagsStack.top(); !t.empty(); t.pop()) { writer.writeEndElement(); if (t.top() == "a") inAnchor = false; } tagsStack.pop(); break; case QXmlStreamReader::EndDocument: if (!tagsStack.empty()) qWarning().noquote().nospace() << __FUNCTION__ << ": Not all HTML tags closed"; break; case QXmlStreamReader::NoToken: Q_ASSERT(reader.tokenType() != QXmlStreamReader::NoToken /*false*/); break; case QXmlStreamReader::Invalid: errorPos = reader.characterOffset() - bodyOffset; errorString = reader.errorString(); qCritical().noquote() << "Invalid XHTML:" << html; qCritical().nospace() << "Error at char " << errorPos << ": " << errorString; qCritical().noquote() << "Buffer at error:" << html.mid(reader.characterOffset()); break; case QXmlStreamReader::Comment: case QXmlStreamReader::StartDocument: case QXmlStreamReader::DTD: case QXmlStreamReader::ProcessingInstruction: continue; // All these should not affect firstElement state } // Unset first element once encountered non-whitespace under `` firstElement &= (bodyOffset <= 0 || reader.isWhitespace()); } } template inline QStringRef cssValue(const QStringRef& css, const char (&propertyNameWithColon)[Len]) { return css.startsWith(propertyNameWithColon) ? css.mid(Len - 1).trimmed() : QStringRef(); } Processor::rewrite_t Processor::filterTag(const QStringRef& tag, QXmlStreamAttributes attributes) { if (mode == MatrixToQt) { if (tag == "del" || tag == "strike") { // Qt doesn't support these... QXmlStreamAttributes attrs; attrs.append("style", "text-decoration:line-through"); return { { "font", std::move(attrs) } }; } if (tag == "mx-reply") return { { "div", {} } }; // The spec says that mx-reply is HTML div // If `mx-reply` is encountered on the way to the wire, just pass it } rewrite_t rewrite { { tag.toString(), {} } }; if (tag == "code" && mode != GenericToQt) { // Special case copy_if(attributes.begin(), attributes.end(), back_inserter(rewrite.back().second), [](const auto& a) { return a.qualifiedName() == "class" && a.value().startsWith("language-"); }); return rewrite; } if (find(begin(permittedTags), end(permittedTags), tag) == end(permittedTags)) return {}; // The tag is not allowed const auto it = find_if(begin(passLists), end(passLists), [&tag](const auto& passCard) { return passCard.tag == tag; }); if (it == end(passLists)) return rewrite; // Drop all attributes, pass the tag /// Find the first element in the rewrite that would accept color /// attributes (`font` and, only in Matrix HTML, `span`), /// and add the passed attribute to it const auto& addColorAttr = [&rewrite, this](const QString& attrName, const QStringRef& attrValue) { auto it = find_if(rewrite.begin(), rewrite.end(), [this](const rewrite_t::value_type& element) { return element.first == "font" || (mode == QtToMatrix && element.first == "span"); }); if (it == rewrite.end()) it = rewrite.insert(rewrite.end(), { "font", {} }); it->second.append(attrName, attrValue.toString()); }; const auto& passList = it->allowedAttrs; for (auto&& a: attributes) { const auto aName = a.qualifiedName(); const auto aValue = a.value(); // Attribute conversions between Matrix and Qt subsets; generic HTML // is treated as possibly-Matrix if (mode != QtToMatrix) { if (aName == mxColorAttr) { addColorAttr(htmlColorAttr, aValue); continue; } if (aName == mxBgColorAttr) { rewrite.front().second.append(htmlStyleAttr, "background-color:" + aValue); continue; } } else { if (aName == htmlStyleAttr) { // 'style' attribute is not allowed in Matrix; convert // everything possible to tags and other attributes const auto& cssProperties = aValue.split(';'); for (auto p: cssProperties) { p = p.trimmed(); if (p.isEmpty()) continue; if (const auto& v = cssValue(p, "color:"); !v.isEmpty()) { addColorAttr(mxColorAttr, v); } else if (const auto& v = cssValue(p, "background-color:"); !v.isEmpty()) addColorAttr(mxBgColorAttr, v); else if (const auto& v = cssValue(p, "font-weight:"); v == "bold" || v == "bolder" || v.toFloat() > 500) rewrite.emplace_back().first = "b"; else if (const auto& v = cssValue(p, "font-style:"); v == "italic" || v.startsWith("oblique")) rewrite.emplace_back().first = "i"; else if (const auto& v = cssValue(p, "text-decoration:"); v.contains("line-through")) rewrite.emplace_back().first = "del"; else { const auto& fontFamilies = cssValue(p, "font-family:").split(','); for (auto ff: fontFamilies) { ff = ff.trimmed(); if (ff.isEmpty()) continue; if (ff[0] == '\'' || ff[0] == '"') ff = ff.mid(1); if (ff.startsWith("monospace", Qt::CaseInsensitive)) { rewrite.emplace_back().first = "code"; break; } } } } continue; } if (aName == htmlColorAttr) addColorAttr(mxColorAttr, aValue); // Add to 'color' } // Generic filtering for attributes if ((mode == GenericToQt && (aName == htmlStyleAttr || aName == "class" || aName == "id")) || (tag == "a" && aName == "href" && any_of(begin(permittedSchemes), end(permittedSchemes), [&aValue](const char* s) { return aValue.startsWith(s); })) || (tag == "img" && aName == "src" && aValue.startsWith("mxc:")) || find(passList.begin(), passList.end(), a.qualifiedName()) != passList.end()) rewrite.front().second.push_back(move(a)); } // for (a: attributes) // Remove the original or if they end up without attributes // since without attributes they are no-op if (!rewrite.empty() && (rewrite.front().first == "font" || rewrite.front().first == "span") && rewrite.front().second.empty()) rewrite.erase(rewrite.begin()); return rewrite; } void Processor::filterText(QString& text) { if (text.isEmpty()) return; #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) if (options.testFlag(ConvertMarkdown)) { // Protect leading/trailing whitespaces (Markdown disregards them); // specific string doesn't matter as long as it isn't whitespace itself, // doesn't have special meaning in Markdown and doesn't occur in // the HTML boilerplate that QTextDocument generates. static const QLatin1String Marker { "$$" }; const bool hasLeadingWhitespace = text.cbegin()->isSpace(); if (hasLeadingWhitespace) text.prepend(Marker); const bool hasTrailingWhitespace = (text.cend() - 1)->isSpace(); if (hasTrailingWhitespace) text.append(Marker); const auto markerCount = text.count(Marker); // For self-check #ifndef QTBUG_92445_FIXED // Protect list items from https://bugreports.qt.io/browse/QTBUG-92445 // (see also https://spec.commonmark.org/0.29/#list-items) static const auto ReOptions = QRegularExpression::MultilineOption; static const QRegularExpression // UlRE("^( *[-+*] {1,4})(?=[^ ])", ReOptions), OlRE("^( *[0-9]{1,9}+[.)] {1,4})(?=[^ ])", ReOptions); static const QLatin1String UlMarker("@@ul@@"), OlMarker("@@ol@@"); text.replace(UlRE, "\\1" % UlMarker); text.replace(OlRE, "\\1" % OlMarker); const auto markerCountOl = text.count(OlMarker); const auto markerCountUl = text.count(UlMarker); #endif // Convert Markdown to HTML QTextDocument doc; doc.setMarkdown(text, QTextDocument::MarkdownNoHTML); text = doc.toHtml(); // Delete protection characters, now buried inside HTML #ifndef QTBUG_92445_FIXED Q_ASSERT(text.count(OlMarker) == markerCountOl); Q_ASSERT(text.count(UlMarker) == markerCountUl); // After HTML conversion, list markers end up being after HTML tags text.replace(QRegularExpression('>' % OlMarker), ">"); text.replace(QRegularExpression('>' % UlMarker), ">"); #endif Q_ASSERT(text.count(Marker) == markerCount); if (hasLeadingWhitespace) text.remove(text.indexOf(Marker), Marker.size()); if (hasTrailingWhitespace) text.remove(text.lastIndexOf(Marker), Marker.size()); } else #endif { text = text.toHtmlEscaped(); // The reader unescaped it Quotient::linkifyUrls(text); text = "" % text % ""; } // Re-process this piece of text as HTML but dump text snippets as they are, // without recursing into filterText() again Processor(mode, Fragment, context, writer).runOn(text); text.clear(); } } // namespace HtmlFilter Quaternion-0.0.95.1/client/htmlfilter.h000066400000000000000000000135671412757327200177200ustar00rootroot00000000000000#pragma once #include // For Q_NAMESPACE and Q_DECLARE_METATYPE #include class QuaternionRoom; namespace HtmlFilter { Q_NAMESPACE enum Option : unsigned char { Default = 0x0, #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) /// Treat `` contents as Markdown (toMatrixHtml() only) ConvertMarkdown = 0x1, #endif /// Treat `` contents as a fragment in a bigger HTML payload /// (suppresses markup processing inside HTML elements and `` /// conversion - toMatrixHtml() only) Fragment = 0x2, /// Stop at tags not allowed in Matrix, instead of ignoring them /// (from*Html() functions only) Validate = 0x4 }; Q_ENUM_NS(Option) Q_DECLARE_FLAGS(Options, Option) /*! \brief Result structure for HTML parsing * * This is the return type of from*Html() functions, which, unlike * toMatrixHtml(), can't assume that HTML it receives is valid since it either * comes from the wire or a user input and therefore need a means to report * an error when the parser cannot cope (most often because of incorrectly * closed tags but also if plain incorrect HTML is passed). * * \sa fromMatrixHtml(), fromLocalHtml() */ struct Result { Q_GADGET Q_PROPERTY(QString filteredHtml MEMBER filteredHtml CONSTANT) Q_PROPERTY(int errorPos MEMBER errorPos CONSTANT) Q_PROPERTY(QString errorString MEMBER errorString CONSTANT) public: /// HTML that the filter managed to produce (incomplete in case of error) QString filteredHtml {}; /// The position at which the first error was encountered; -1 if no error int errorPos = -1; /// The human-readable error message; empty if no error QString errorString {}; }; /*! \brief Convert user input to Matrix-flavoured HTML * * This function takes user input in \p markup and converts it to the Matrix * flavour of HTML. The text in \p markup is treated as-if taken from * QTextDocument[Fragment]::toHtml(); however, the body of this HTML is itself * treated as (HTML-encoded) markup as well, in assumption that rich text * (in QTextDocument sense) is exported as the outer level of HTML while * the user adds their own HTML inside that rich text. The function decodes * and merges the two levels of markup before converting the resulting HTML * to its Matrix flavour. * * When compiling with Qt 5.14 or newer, it is possible to pass ConvertMarkdown * in \p options in order to handle the user's markup as a mix of Markdown and * HTML. In that case the function will first turn the Markdown parts to HTML * and then merge the resulting HTML snippets with the outer markup. * * The function removes HTML tags disallowed in Matrix; on top of that, * it cleans away extra parts (DTD, `head`, top-level `p`, extra `span` * inside hyperlinks etc.) added by Qt when exporting QTextDocument * to HTML, and converts some formatting that can be represented in Matrix * to tags and attributes allowed by the CS API spec. * * \note This function assumes well-formed XHTML produced by Qt classes; while * it corrects unescaped ampersands (`&`) it does not try to turn HTML * to XHTML, as from*Html() functions do. In case of an error, debug * builds will fail on assertion, release builds will silently stop * processing and return what could be processed so far. * * \sa * https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes */ QString toMatrixHtml(const QString& markup, QuaternionRoom* context, Options options = Default); /*! \brief Make the received HTML with Matrix attributes compatible with Qt * * Similar to toMatrixHtml(), this function removes HTML tags disallowed in * Matrix and cleans away extraneous HTML parts but it does the reverse * conversion of Matrix-specific attributes to HTML subset that Qt supports. * It can deal with a few more irregularities compared to toMatrixHtml(), but * still doesn't recover from, e.g., missing closing tags except those usually * not closed in HTML (`br` etc.). In case of an irrecoverable error * the returned structure will contain the error details (position and brief * description), along with whatever HTML the function managed to produce before * the failure. * * \param matrixHtml text in Matrix HTML that should be converted to Qt HTML * \param context optional room context to enrich the text * \param options whether the algorithm should stop at disallowed HTML tags * rather than ignore them and try to continue * \sa Result * \sa * https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes */ Result fromMatrixHtml(const QString& matrixHtml, QuaternionRoom* context, Options options = Default); /*! \brief Make the received generic HTML compatible with Qt and convertible * to Matrix * * This function is similar to fromMatrixHtml() in that it produces HTML that * can be fed to Qt components - QTextDocument[Fragment]::fromHtml(), * in particular; it also uses the same way to tackle irregularities and errors * in HTML and removes tags and attributes that cannot be converted to Matrix. * Unlike fromMatrixHtml() that accepts Matrix-flavoured HTML, this function * accepts generic HTML and allows a few exceptions compared to the Matrix spec * recommendations for HTML; specifically, it preserves the `head` element; * and `id`, `class`, and `style` attributes throughout HTML are not restricted, * allowing generic CSS stuff to do its job inasmuch as Qt supports that. * * The case for this function is loading a piece of external HTML into a Qt * component in anticipation that this piece will later be translated to Matrix * HTML - e.g. drag-n-drop/clipboard paste into the message input control. * * \sa fromMatrixHtml */ Result fromLocalHtml(const QString& html, QuaternionRoom* context = nullptr, Options options = Fragment); } Q_DECLARE_METATYPE(HtmlFilter::Result) Quaternion-0.0.95.1/client/imageprovider.cpp000066400000000000000000000146571412757327200207370ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2016 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #include "imageprovider.h" #include #include #include #include #include using Quotient::Connection; using Quotient::BaseJob; class ThumbnailResponse : public QQuickImageResponse { Q_OBJECT public: ThumbnailResponse(Connection* c, QString id, QSize size) : c(c), mediaId(std::move(id)), requestedSize(size) { if (!c) errorStr = tr("No connection to perform image request"); else if (mediaId.count('/') != 1) errorStr = tr("Media id '%1' doesn't follow server/mediaId pattern") .arg(mediaId); else if (requestedSize.isEmpty()) { qDebug() << "ThumbnailResponse: returning an empty image for" << mediaId << "due to empty" << requestedSize; image = {requestedSize, QImage::Format_Invalid}; } if (!errorStr.isEmpty() || requestedSize.isEmpty()) { emit finished(); return; } // We are good to go qDebug().nospace() << "ThumbnailResponse: requesting " << mediaId << ", " << size; errorStr = tr("Image request is pending"); // Execute a request on the main thread asynchronously moveToThread(c->thread()); QMetaObject::invokeMethod(this, &ThumbnailResponse::startRequest); } ~ThumbnailResponse() override = default; private slots: // All these run in the main thread, not QML thread void startRequest() { Q_ASSERT(QThread::currentThread() == c->thread()); job = c->getThumbnail(mediaId, requestedSize); // Connect to any possible outcome including abandonment // to make sure the QML thread is not left stuck forever. connect(job, &BaseJob::finished, this, &ThumbnailResponse::prepareResult); } void prepareResult() { Q_ASSERT(QThread::currentThread() == job->thread()); Q_ASSERT(job->error() != BaseJob::Pending); { QWriteLocker _(&lock); if (job->error() == BaseJob::Success) { image = job->thumbnail(); errorStr.clear(); qDebug().nospace() << "ThumbnailResponse: image ready for " << mediaId << ", " << image.size(); } else if (job->error() == BaseJob::Abandoned) { errorStr = tr("Image request has been cancelled"); qDebug() << "ThumbnailResponse: cancelled for" << mediaId; } else { errorStr = job->errorString(); qWarning() << "ThumbnailResponse: no valid image for" << mediaId << "-" << errorStr; } } job = nullptr; emit finished(); } void doCancel() { if (job) { Q_ASSERT(QThread::currentThread() == job->thread()); job->abandon(); } } private: Connection* c; const QString mediaId; const QSize requestedSize; Quotient::MediaThumbnailJob* job = nullptr; QImage image; QString errorStr; mutable QReadWriteLock lock; // Guards ONLY these two above // The following overrides run in QML thread QQuickTextureFactory *textureFactory() const override { QReadLocker _(&lock); return QQuickTextureFactory::textureFactoryForImage(image); } QString errorString() const override { QReadLocker _(&lock); return errorStr; } void cancel() override { // Flip from QML thread to the main thread QMetaObject::invokeMethod(this, &ThumbnailResponse::doCancel); } }; #include "imageprovider.moc" // Because we define a Q_OBJECT in the cpp file ImageProvider::ImageProvider(Connection* connection) : m_connection(connection) { } #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) # define LOAD_ATOMIC(Ptr) Ptr.load() # define STORE_ATOMIC(Ptr, NewValue) Ptr.store(NewValue) #else # define LOAD_ATOMIC(Ptr) Ptr.loadRelaxed() # define STORE_ATOMIC(Ptr, NewValue) Ptr.storeRelaxed(NewValue) #endif QQuickImageResponse* ImageProvider::requestImageResponse( const QString& id, const QSize& requestedSize) { auto size = requestedSize; // Force integer overflow if the value is -1 - may cause issues when // screens resolution becomes 100K+ each dimension :-D if (size.width() == -1) size.setWidth(ushort(-1)); if (size.height() == -1) size.setHeight(ushort(-1)); return new ThumbnailResponse(LOAD_ATOMIC(m_connection), id, size); } void ImageProvider::setConnection(Connection* connection) { STORE_ATOMIC(m_connection, connection); } Quaternion-0.0.95.1/client/imageprovider.h000066400000000000000000000043571412757327200204000ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2016 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #pragma once #include #include namespace Quotient { class Connection; } // FIXME: It's actually ThumbnailProvider, not ImageProvider, because internally // it calls MediaThumbnailJob. Trying to get a full-size image using this // provider may bring suboptimal results (and generally you shouldn't do it // because images loaded by QML are not necessarily cached to disk, so it's a // waste of bandwidth). class ImageProvider: public QQuickAsyncImageProvider { public: explicit ImageProvider(Quotient::Connection* connection = nullptr); QQuickImageResponse* requestImageResponse( const QString& id, const QSize& requestedSize) override; void setConnection(Quotient::Connection* connection); private: QAtomicPointer m_connection; Q_DISABLE_COPY(ImageProvider) }; Quaternion-0.0.95.1/client/kchatedit.cpp000066400000000000000000000206771412757327200200410ustar00rootroot00000000000000/* * Copyright (C) 2017 Elvis Angelaccio * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "kchatedit.h" #include #include #include class KChatEdit::KChatEditPrivate { public: QString getDocumentText(QTextDocument* doc) const; void updateAndMoveInHistory(int increment); void saveInput(); QTextDocument* makeDocument() { Q_ASSERT(contextKey); return new QTextDocument(contextKey); } void setContext(QObject* newContextKey) { contextKey = newContextKey; auto& context = contexts[contextKey]; // Create if needed auto& history = context.history; // History always ends with a placeholder that is initially empty // but may be filled with tentative input when the user entered // something and then went out for history. if (history.isEmpty() || !history.last()->isEmpty()) history.push_back(makeDocument()); while (history.size() > maxHistorySize) delete history.takeFirst(); index = history.size() - 1; // QTextDocuments are parented to the context object, so are destroyed // automatically along with it; but the hashmap should be cleaned up if (newContextKey != q) QObject::connect(newContextKey, &QObject::destroyed, q, [this, newContextKey] { contexts.remove(newContextKey); }); Q_ASSERT(contexts.contains(newContextKey) && !history.empty()); } KChatEdit* q = nullptr; QObject* contextKey = nullptr; struct Context { QVector history; QTextDocument* cachedInput = nullptr; }; QHash contexts; int index = 0; int maxHistorySize = 100; QTextBlockFormat defaultBlockFmt; }; QString KChatEdit::KChatEditPrivate::getDocumentText(QTextDocument* doc) const { Q_ASSERT(doc); return q->acceptRichText() ? doc->toHtml() : doc->toPlainText(); } void KChatEdit::KChatEditPrivate::updateAndMoveInHistory(int increment) { Q_ASSERT(contexts.contains(contextKey)); auto& history = contexts.find(contextKey)->history; Q_ASSERT(index >= 0 && index < history.size()); if (index + increment < 0 || index + increment >= history.size()) return; // Prevent stepping out of bounds auto& historyItem = history[index]; // Only save input if different from the latest one. if (q->document() != historyItem /* shortcut expensive getDocumentText() */ && getDocumentText(q->document()) != getDocumentText(historyItem)) historyItem = q->document(); // Fill the input with a copy of the history entry at a new index q->setDocument(history.at(index += increment)->clone(contextKey)); q->moveCursor(QTextCursor::End); } void KChatEdit::KChatEditPrivate::saveInput() { if (q->document()->isEmpty()) return; Q_ASSERT(contexts.contains(contextKey)); auto& history = contexts.find(contextKey)->history; // Only save input if different from the latest one or from the history. const auto input = getDocumentText(q->document()); if (index < history.size() - 1 && input == getDocumentText(history[index])) { // Take the history entry and move it to the most recent position (but // before the placeholder). history.move(index, history.size() - 2); emit q->savedInputChanged(); } else if (input != getDocumentText(q->savedInput())) { // Insert a copy of the edited text just before the placeholder history.insert(history.end() - 1, q->document()); q->setDocument(makeDocument()); if (history.size() >= maxHistorySize) { delete history.takeFirst(); } emit q->savedInputChanged(); } index = history.size() - 1; q->clear(); q->resetCurrentFormat(); } KChatEdit::KChatEdit(QWidget *parent) : QTextEdit(parent), d(new KChatEditPrivate) { setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); connect(this, &QTextEdit::textChanged, this, &QWidget::updateGeometry); d->q = this; // KChatEdit initialization complete, pimpl can use it d->setContext(this); // A special context that always exists setDocument(d->makeDocument()); d->defaultBlockFmt = textCursor().blockFormat(); } KChatEdit::~KChatEdit() = default; QTextDocument* KChatEdit::savedInput() const { Q_ASSERT(d->contexts.contains(d->contextKey)); auto& history = d->contexts.find(d->contextKey)->history; if (history.size() >= 2) return history.at(history.size() - 2); Q_ASSERT(history.size() == 1); return history.front(); } void KChatEdit::saveInput() { d->saveInput(); } QVector KChatEdit::history() const { Q_ASSERT(d->contexts.contains(d->contextKey)); return d->contexts.value(d->contextKey).history; } int KChatEdit::maxHistorySize() const { return d->maxHistorySize; } void KChatEdit::setMaxHistorySize(int newMaxSize) { if (d->maxHistorySize != newMaxSize) { d->maxHistorySize = newMaxSize; emit maxHistorySizeChanged(); } } void KChatEdit::switchContext(QObject* contextKey) { if (!contextKey) contextKey = this; if (d->contextKey == contextKey) return; Q_ASSERT(d->contexts.contains(d->contextKey)); d->contexts.find(d->contextKey)->cachedInput = document()->isEmpty() ? nullptr : document(); d->setContext(contextKey); auto& cachedInput = d->contexts.find(d->contextKey)->cachedInput; setDocument(cachedInput ? cachedInput : d->makeDocument()); moveCursor(QTextCursor::End); emit contextSwitched(); } void KChatEdit::resetCurrentFormat() { auto c = textCursor(); c.setCharFormat({}); c.setBlockFormat(d->defaultBlockFmt); setTextCursor(c); } QSize KChatEdit::minimumSizeHint() const { QSize minimumSizeHint = QTextEdit::minimumSizeHint(); QMargins margins; margins += static_cast(document()->documentMargin()); margins += contentsMargins(); if (!placeholderText().isEmpty()) { minimumSizeHint.setWidth(int( fontMetrics().boundingRect(placeholderText()).width() + margins.left()*2.5)); } if (document()->isEmpty()) { minimumSizeHint.setHeight(fontMetrics().lineSpacing() + margins.top() + margins.bottom()); } else { minimumSizeHint.setHeight(int(document()->size().height())); } return minimumSizeHint; } QSize KChatEdit::sizeHint() const { ensurePolished(); if (document()->isEmpty()) { return minimumSizeHint(); } QMargins margins; margins += static_cast(document()->documentMargin()); margins += contentsMargins(); QSize size = document()->size().toSize(); size.rwidth() += margins.left() + margins.right(); size.rheight() += margins.top() + margins.bottom(); // Be consistent with minimumSizeHint(). if (document()->lineCount() == 1 && !toPlainText().contains('\n')) { size.setHeight(fontMetrics().lineSpacing() + margins.top() + margins.bottom()); } return size; } void KChatEdit::keyPressEvent(QKeyEvent *event) { if (event->matches(QKeySequence::Copy)) { emit copyRequested(); return; } switch (event->key()) { case Qt::Key_Enter: case Qt::Key_Return: if (!(QGuiApplication::keyboardModifiers() & Qt::ShiftModifier)) { emit returnPressed(); return; } break; case Qt::Key_Up: if (!textCursor().movePosition(QTextCursor::Up)) { d->updateAndMoveInHistory(-1); } break; case Qt::Key_Down: if (!textCursor().movePosition(QTextCursor::Down)) { d->updateAndMoveInHistory(+1); } break; default: break; } QTextEdit::keyPressEvent(event); } Quaternion-0.0.95.1/client/kchatedit.h000066400000000000000000000112541412757327200174750ustar00rootroot00000000000000/* * Copyright (C) 2017 Elvis Angelaccio * Copyright (C) 2020 The Quotient project * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include /** * @class KChatEdit kchatedit.h KChatEdit * * @brief An input widget with history for chat applications. * * This widget can be used to get input for chat windows, which typically * corresponds to chat messages or protocol-specific commands (for example the * "/whois" IRC command). * * By default the widget takes as little space as possible, which is the same * space as used by a QLineEdit. It is possible to expand the widget and enter * "multi-line" messages, by pressing Shift + Return. * * Chat applications usually maintain a history of what the user typed, which * can be browsed with the Up and Down keys (exactly like in command-line * shells). This feature is fully supported by this widget. The widget emits the * inputRequested() signal upon pressing the Return key. You can then call * saveInput() to make the input text disappear, as typical in chat * applications. The input goes in the history and can be retrieved with the * savedInput() method. * * @author Elvis Angelaccio * @author Kitsune Ral */ class KChatEdit : public QTextEdit { Q_OBJECT Q_PROPERTY(QTextDocument* savedInput READ savedInput NOTIFY savedInputChanged) Q_PROPERTY(int maxHistorySize READ maxHistorySize WRITE setMaxHistorySize NOTIFY maxHistorySizeChanged) public: explicit KChatEdit(QWidget *parent = nullptr); ~KChatEdit() override; /** * The latest input text saved in the history. * This corresponds to the last element of history(). * @return Latest available input or an empty document if saveInput() has not been called yet. * @see inputChanged(), saveInput(), history() */ QTextDocument* savedInput() const; /** * Saves in the history the current document(). * This also clears the QTextEdit area. * @note If the history is full (see maxHistorySize(), new inputs will take space from the oldest * items in the history. * @see savedInput(), history(), maxHistorySize() */ void saveInput(); /** * @return The history of the text inputs that the user typed. * @see savedInput(), saveInput(); */ QVector history() const; /** * @return The maximum number of input items that the history can store. * @see history() */ int maxHistorySize() const; /** * Set the maximum number of input items that the history can store. * @see maxHistorySize() */ void setMaxHistorySize(int newMaxSize); QSize minimumSizeHint() const Q_DECL_OVERRIDE; QSize sizeHint() const Q_DECL_OVERRIDE; public Q_SLOTS: /** * @brief Switch the context (e.g., a chat room) of the widget * * This clears the current entry and the history of the chat edit * and replaces them with the entry and the history for the object * passed as a parameter, if there are any. */ virtual void switchContext(QObject* contextKey); /** * @brief Reset the current character(s) formatting * * This is equivalent to calling `setCurrentCharFormat({})`. */ void resetCurrentFormat(); Q_SIGNALS: /** * A new input has been saved in the history. * @see savedInput(), saveInput(), history() */ void savedInputChanged(); /** * Emitted when the user types Key_Return or Key_Enter, which typically means the user * wants to "send" what was typed. Call saveInput() if you want to actually save the input. * @see savedInput(), saveInput(), history() */ void returnPressed(); /** * Emitted when the user presses Ctrl+C. */ void copyRequested(); /** A new context has been selected */ void contextSwitched(); void maxHistorySizeChanged(); protected: void keyPressEvent(QKeyEvent *event) override; private: class KChatEditPrivate; QScopedPointer d; Q_DISABLE_COPY(KChatEdit) }; Quaternion-0.0.95.1/client/linuxutils.h000066400000000000000000000017301412757327200177530ustar00rootroot00000000000000/* * Copyright (C) 2017 Elvis Angelaccio * Copyright (C) 2020 The Quotient project * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include inline bool inFlatpak() { return QFileInfo::exists("/.flatpak-info"); } inline QString appIconName() { return inFlatpak() ? "com.github.quaternion" : "quaternion"; } Quaternion-0.0.95.1/client/logindialog.cpp000066400000000000000000000245001412757327200203560ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2015 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #include "logindialog.h" #include #include #include #include #include #include #include #include #include #include using Quotient::Connection; static const auto MalformedServerUrl = LoginDialog::tr("The server URL doesn't look valid"); LoginDialog::LoginDialog(const QString& statusMessage, QWidget* parent, const QStringList& knownAccounts) : Dialog(tr("Login"), parent, Dialog::StatusLine, tr("Login"), Dialog::NoExtraButtons) , userEdit(new QLineEdit(this)) , passwordEdit(new QLineEdit(this)) , initialDeviceName(new QLineEdit(this)) , serverEdit(new QLineEdit(QStringLiteral("https://matrix.org"), this)) , saveTokenCheck(new QCheckBox(tr("Stay logged in"), this)) , m_connection(new Connection) { setup(statusMessage); setPendingApplyMessage(tr("Connecting and logging in, please wait")); connect(userEdit, &QLineEdit::editingFinished, m_connection.data(), [=] { auto userId = userEdit->text(); if (userId.startsWith('@') && userId.indexOf(':') != -1) { setStatusMessage(tr("Resolving the homeserver...")); serverEdit->clear(); button(QDialogButtonBox::Ok)->setEnabled(false); m_connection->resolveServer(userId); } }); connect(serverEdit, &QLineEdit::editingFinished, m_connection.data(), [this] { if (QUrl hsUrl { serverEdit->text() }; hsUrl.isValid()) { m_connection->setHomeserver(serverEdit->text()); button(QDialogButtonBox::Ok)->setEnabled(true); } else { setStatusMessage(MalformedServerUrl); button(QDialogButtonBox::Ok)->setEnabled(false); } }); // This button is only shown when BOTH password auth and SSO are available // If only one flow is there, the "Login" button text is changed instead auto* ssoButton = buttonBox()->addButton(tr("Login with SSO"), QDialogButtonBox::AcceptRole); connect(ssoButton, &QPushButton::clicked, this, &LoginDialog::loginWithSso); ssoButton->setHidden(true); connect(m_connection.data(), &Connection::loginFlowsChanged, this, [this, ssoButton] { // There may be more ways to login but Quaternion only supports // SSO and password for now; in the worst case of no known // options password login is kept enabled as the last resort. bool canUseSso = m_connection->supportsSso(); bool canUsePassword = m_connection->supportsPasswordAuth(); ssoButton->setVisible(canUseSso && canUsePassword); button(QDialogButtonBox::Ok) ->setText(canUseSso && !canUsePassword ? QStringLiteral("Login with SSO") : QStringLiteral("Login")); }); { // Fill defaults using namespace Quotient; if ( !knownAccounts.empty() ) { AccountSettings account { knownAccounts.front() }; userEdit->setText(account.userId()); auto homeserver = account.homeserver(); if (!homeserver.isEmpty()) m_connection->setHomeserver(homeserver); initialDeviceName->setText(account.deviceName()); saveTokenCheck->setChecked(account.keepLoggedIn()); passwordEdit->setFocus(); } else { saveTokenCheck->setChecked(false); userEdit->setFocus(); } } } LoginDialog::LoginDialog(const QString &statusMessage, QWidget* parent, const Quotient::AccountSettings& reloginData) : Dialog(tr("Re-login"), parent, Dialog::StatusLine, tr("Re-login"), Dialog::NoExtraButtons) , userEdit(new QLineEdit(reloginData.userId(), this)) , passwordEdit(new QLineEdit(this)) , initialDeviceName(new QLineEdit(reloginData.deviceName(), this)) , serverEdit(new QLineEdit(reloginData.homeserver().toString(), this)) , saveTokenCheck(new QCheckBox(tr("Stay logged in"), this)) , m_connection(new Connection) { setup(statusMessage); userEdit->setReadOnly(true); userEdit->setFrame(false); setPendingApplyMessage(tr("Restoring access, please wait")); } void LoginDialog::setup(const QString& statusMessage) { setStatusMessage(statusMessage); passwordEdit->setEchoMode( QLineEdit::Password ); // This is triggered whenever the server URL has been changed connect(m_connection.data(), &Connection::homeserverChanged, serverEdit, [this](const QUrl& hsUrl) { serverEdit->setText(hsUrl.toString()); if (hsUrl.isValid()) setStatusMessage(tr("Getting supported login flows...")); // Allow to click login even before getting the flows and // do LoginDialog::loginWithBestFlow() as soon as flows arrive button(QDialogButtonBox::Ok)->setEnabled(hsUrl.isValid()); }); connect(m_connection.data(), &Connection::loginFlowsChanged, this, [this] { serverEdit->setText(m_connection->homeserver().toString()); setStatusMessage(m_connection->isUsable() ? tr("The homeserver is available") : tr("Could not connect to the homeserver")); button(QDialogButtonBox::Ok)->setEnabled(m_connection->isUsable()); }); // This overrides the above in case of an unsuccessful attempt to resolve // the server URL from a changed MXID connect(m_connection.data(), &Connection::resolveError, this, [this](const QString& message) { qDebug() << "Resolve error"; serverEdit->clear(); setStatusMessage(message); }); connect(m_connection.data(), &Connection::connected, this, &Dialog::accept); connect(m_connection.data(), &Connection::loginError, this, &Dialog::applyFailed); auto* formLayout = addLayout(); formLayout->addRow(tr("Matrix ID"), userEdit); formLayout->addRow(tr("Password"), passwordEdit); formLayout->addRow(tr("Device name"), initialDeviceName); formLayout->addRow(tr("Connect to server"), serverEdit); formLayout->addRow(saveTokenCheck); } LoginDialog::~LoginDialog() = default; Connection* LoginDialog::releaseConnection() { return m_connection.take(); } QString LoginDialog::deviceName() const { return initialDeviceName->text(); } bool LoginDialog::keepLoggedIn() const { return saveTokenCheck->isChecked(); } void LoginDialog::apply() { auto url = QUrl::fromUserInput(serverEdit->text()); if (!serverEdit->text().isEmpty() && !serverEdit->text().startsWith("http:")) url.setScheme("https"); // Qt defaults to http (or even ftp for some) // Whichever the flow, the two connections are the same if (m_connection->homeserver() == url && !m_connection->loginFlows().empty()) loginWithBestFlow(); else if (!url.isValid()) applyFailed(MalformedServerUrl); else { m_connection->setHomeserver(url); // Wait for new flows and check them connectSingleShot(m_connection.data(), &Connection::loginFlowsChanged, this, [this] { qDebug() << "Received login flows, trying to login"; loginWithBestFlow(); }); } } void LoginDialog::loginWithBestFlow() { if (m_connection->loginFlows().empty() || m_connection->supportsPasswordAuth()) loginWithPassword(); else if (m_connection->supportsSso()) loginWithSso(); else applyFailed(tr("No supported login flows")); } void LoginDialog::loginWithPassword() { m_connection->loginWithPassword(userEdit->text(), passwordEdit->text(), initialDeviceName->text()); } void LoginDialog::loginWithSso() { auto* ssoSession = m_connection->prepareForSso(initialDeviceName->text()); if (!QDesktopServices::openUrl(ssoSession->ssoUrl())) { auto* instructionsBox = new Dialog(tr("Single sign-on"), QDialogButtonBox::NoButton, this); instructionsBox->addWidget(new QLabel( tr("Quaternion couldn't automatically open the single sign-on URL. " "Please copy and paste it to the right application (usually " "a web browser):"))); auto* urlBox = new QLineEdit(ssoSession->ssoUrl().toString()); urlBox->setReadOnly(true); instructionsBox->addWidget(urlBox); instructionsBox->addWidget( new QLabel(tr("After authentication, the browser will follow " "the temporary local address setup by Quaternion " "to conclude the login sequence."))); instructionsBox->open(); } } Quaternion-0.0.95.1/client/logindialog.h000066400000000000000000000047761412757327200200400ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2015 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #pragma once #include "dialog.h" class QLineEdit; class QCheckBox; namespace Quotient { class AccountSettings; class Connection; } class LoginDialog : public Dialog { Q_OBJECT public: explicit LoginDialog(const QString& statusMessage, QWidget* parent = nullptr, const QStringList& knownAccounts = {}); explicit LoginDialog(const QString& statusMessage, QWidget* parent, const Quotient::AccountSettings& reloginData); void setup(const QString &statusMessage); ~LoginDialog() override; Quotient::Connection* releaseConnection(); QString deviceName() const; bool keepLoggedIn() const; private slots: void apply() override; void loginWithBestFlow(); void loginWithPassword(); void loginWithSso(); private: QLineEdit* userEdit; QLineEdit* passwordEdit; QLineEdit* initialDeviceName; QLineEdit* serverEdit; QCheckBox* saveTokenCheck; QScopedPointer m_connection; }; Quaternion-0.0.95.1/client/main.cpp000066400000000000000000000205421412757327200170140ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2015 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #include #include #include #include #include #include #include #include #include "networksettings.h" #include "mainwindow.h" #include "linuxutils.h" #include void loadTranslations( std::initializer_list> translationConfigs) { for (const auto& [configNames, configPath]: translationConfigs) for (const auto& configName: configNames) { auto* translator = new QTranslator(qApp); bool loaded = false; // Check the current directory then configPath if (translator->load(QLocale(), configName, "_") || translator->load(QLocale(), configName, "_", configPath)) { auto path = #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) translator->filePath(); #else configPath; #endif if ((loaded = QApplication::installTranslator(translator))) qDebug().noquote() << "Loaded translations from" << path; else qWarning().noquote() << "Failed to load translations from" << path; } else qDebug() << "No translations for" << configName << "at" << configPath; if (!loaded) delete translator; } } int main( int argc, char* argv[] ) { QApplication::setAttribute(Qt::AA_DisableWindowContextHelpButton); #if defined(Q_OS_LINUX) QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); #endif QApplication::setOrganizationName(QStringLiteral("Quotient")); QApplication::setApplicationName(QStringLiteral("quaternion")); QApplication::setApplicationDisplayName(QStringLiteral("Quaternion")); QApplication::setApplicationVersion(QStringLiteral("0.0.95.1")); QApplication::setDesktopFileName( QStringLiteral("com.github.quaternion.desktop")); using Quotient::Settings; Settings::setLegacyNames(QStringLiteral("QMatrixClient"), QStringLiteral("quaternion")); Settings settings; // Qt 5.15 introduces a new way to handle connections in QML and at // the same time deprecates the old way. Rather than go to pains of // making two different versions of QML code, just disable // the deprecation warning as it doesn't help neither users nor developers. if (QLibraryInfo::version() >= QVersionNumber(5,15)) QLoggingCategory::setFilterRules( QStringLiteral("qt.qml.connections=false")); QApplication app(argc, argv); #if defined Q_OS_UNIX && !defined Q_OS_MAC // #681: When in Flatpak and unless overridden by configuration, set // the style to Breeze as it looks much fresher than Fusion that Qt // applications default to in Flatpak outside KDE. Although Qt docs // recommend to call setStyle() before constructing a QApplication object // (to make sure the style's palette is applied?) that doesn't work with // Breeze because it seems to make use of platform theme hints, which // in turn need a created QApplication object (see #700). const auto useBreezeStyle = settings.get("UI/use_breeze_style", inFlatpak()); if (useBreezeStyle) { QApplication::setStyle("Breeze"); QIcon::setThemeName("breeze"); # if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) QIcon::setFallbackThemeName("breeze"); # endif } else #endif { const auto qqc2styles = QQuickStyle::availableStyles(); if (qqc2styles.contains("Fusion")) QQuickStyle::setFallbackStyle("Fusion"); // Looks better on desktops // QQuickStyle::setStyle("Material"); } { auto font = QApplication::font(); if (const auto fontFamily = settings.get("UI/Fonts/family"); !fontFamily.isEmpty()) font.setFamily(fontFamily); if (const auto fontPointSize = settings.value("UI/Fonts/pointSize").toReal(); fontPointSize > 0) font.setPointSizeF(fontPointSize); qDebug() << "Using application font:" << font.toString(); QApplication::setFont(font); } // We should not need to do the following, as quitOnLastWindowClosed is // set to "true" by default; might be a bug, see // https://forum.qt.io/topic/71112/application-does-not-quit QObject::connect(&app, &QApplication::lastWindowClosed, &app, [&app]{ qDebug() << "Last window closed!"; QApplication::postEvent(&app, new QEvent(QEvent::Quit)); }); QCommandLineParser parser; parser.setApplicationDescription(QApplication::translate("main", "Quaternion - an IM client for the Matrix protocol")); parser.addHelpOption(); parser.addVersionOption(); QList options; QCommandLineOption locale { QStringLiteral("locale"), QApplication::translate("main", "Override locale"), QApplication::translate("main", "locale") }; options.append(locale); QCommandLineOption hideMainWindow { QStringLiteral("hide-mainwindow"), QApplication::translate("main", "Hide main window on startup") }; options.append(hideMainWindow); // Add more command line options before this line if (!parser.addOptions(options)) Q_ASSERT_X(false, __FUNCTION__, "Command line options are improperly defined, fix the code"); parser.process(app); const auto overrideLocale = parser.value(locale); if (!overrideLocale.isEmpty()) { QLocale::setDefault(overrideLocale); qInfo() << "Using locale" << QLocale().name(); } loadTranslations( { { { "qt", "qtbase", "qtnetwork", "qtdeclarative", "qtmultimedia", "qtquickcontrols", "qtquickcontrols2", // QtKeychain tries to install its translations to Qt's path; // try to look there, just in case (see also below) "qtkeychain" }, QLibraryInfo::location(QLibraryInfo::TranslationsPath) }, { { "qtkeychain" }, // Assuming https://github.com/frankosterfeld/qtkeychain/pull/166 // is accepted and QtKeychain is installed at a default location QStandardPaths::locate(QStandardPaths::GenericDataLocation, "qt5keychain/translations", QStandardPaths::LocateDirectory) }, { { "qt", "qtkeychain", "quotient", "quaternion" }, QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, "translations", QStandardPaths::LocateDirectory) } }); Quotient::NetworkSettings().setupApplicationProxy(); MainWindow window; if (parser.isSet(hideMainWindow)) { qDebug() << "--- Hide time!"; window.hide(); } else { qDebug() << "--- Show time!"; window.show(); } return QApplication::exec(); } Quaternion-0.0.95.1/client/mainwindow.cpp000066400000000000000000001711561412757327200202540ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2015 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #include "mainwindow.h" #include "roomlistdock.h" #include "userlistdock.h" #include "chatroomwidget.h" #include "timelinewidget.h" #include "quaternionroom.h" #include "profiledialog.h" #include "logindialog.h" #include "networkconfigdialog.h" #include "roomdialogs.h" #include "accountselector.h" #include "systemtrayicon.h" #include "linuxutils.h" #include #include #include #include #include #include #ifdef USE_KEYCHAIN #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using Quotient::NetworkAccessManager; using Quotient::Settings; using Quotient::AccountSettings; using Quotient::Uri; MainWindow::MainWindow() { Connection::setRoomType(); // Bind callbacks to signals from NetworkAccessManager auto nam = NetworkAccessManager::instance(); connect(nam, &QNetworkAccessManager::proxyAuthenticationRequired, this, &MainWindow::proxyAuthenticationRequired); connect(nam, &QNetworkAccessManager::sslErrors, this, &MainWindow::sslErrors); setWindowIcon(QIcon::fromTheme(appIconName(), QIcon(":/icon.png"))); roomListDock = new RoomListDock(this); addDockWidget(Qt::LeftDockWidgetArea, roomListDock); userListDock = new UserListDock(this); addDockWidget(Qt::RightDockWidgetArea, userListDock); chatRoomWidget = new ChatRoomWidget(this); setCentralWidget(chatRoomWidget); auto* timelineWidget = chatRoomWidget->timelineWidget(); connect(timelineWidget, &TimelineWidget::resourceRequested, this, &MainWindow::openResource); connect(timelineWidget, &TimelineWidget::roomSettingsRequested, this, [this] { openRoomSettings(); }); connect(timelineWidget, &TimelineWidget::showStatusMessage, statusBar(), &QStatusBar::showMessage); connect(roomListDock, &RoomListDock::roomSelected, this, &MainWindow::selectRoom); connect(userListDock, &UserListDock::userMentionRequested, chatRoomWidget, &ChatRoomWidget::insertMention); createMenu(); createWinId(); // TODO: check that it's actually needed systemTrayIcon = new SystemTrayIcon(this); systemTrayIcon->show(); busyIndicator = new QMovie(QStringLiteral(":/busy.gif"), {}, this); busyLabel = new QLabel(this); busyLabel->setMovie(busyIndicator); statusBar()->setSizeGripEnabled(false); statusBar()->addPermanentWidget(busyLabel); statusBar()->showMessage(tr("Loading...")); loadSettings(); // Only GUI, account settings will be loaded in invokeLogin busyLabel->show(); busyIndicator->start(); QTimer::singleShot(0, this, &MainWindow::invokeLogin); } MainWindow::~MainWindow() { for (auto* acc: accountRegistry) { acc->saveState(); acc->stopSync(); // Instead of deleting the connection, merely stop it } for (auto* acc: qAsConst(logoutOnExit)) logout(acc); saveSettings(); } template void summon(QPointer& dlg, DialogArgTs&&... dialogArgs) { if (!dlg) { dlg = new DialogT(std::forward(dialogArgs)...); dlg->setModal(false); dlg->setAttribute(Qt::WA_DeleteOnClose); } dlg->reactivate(); } QAction* MainWindow::addUiOptionCheckbox(QMenu* parent, const QString& text, const QString& statusTip, const QString& settingsKey, bool defaultValue) { using Quotient::SettingsGroup; auto* const action = parent->addAction(text, this, [this,settingsKey] (bool checked) { SettingsGroup("UI").setValue(settingsKey, checked); chatRoomWidget->setRoom(nullptr); chatRoomWidget->setRoom(currentRoom); }); action->setStatusTip(statusTip); action->setCheckable(true); action->setChecked(SettingsGroup("UI").get(settingsKey, defaultValue)); return action; } static const auto ConfirmLinksSettingKey = QStringLiteral("/confirm_external_links"); void MainWindow::createMenu() { // Connection menu connectionMenu = menuBar()->addMenu(tr("&Accounts")); connectionMenu->addAction(QIcon::fromTheme("im-user"), tr("&Login..."), this, [=]{ showLoginWindow(); } ); connectionMenu->addSeparator(); connectionMenu->addAction( QIcon::fromTheme("user-properties"), tr("User &profiles..."), this, [this, dlg = QPointer {}]() mutable { summon(dlg, &accountRegistry, this); if (currentRoom) dlg->setAccount(currentRoom->connection()); }); connectionMenu->addSeparator(); logoutMenu = connectionMenu->addMenu(QIcon::fromTheme("system-log-out"), tr("Log&out")); // Augment poor Windows users with a handy Ctrl-Q shortcut. static const auto quitShortcut = QSysInfo::productType() == "windows" ? QKeySequence(Qt::CTRL + Qt::Key_Q) : QKeySequence::Quit; connectionMenu->addAction(QIcon::fromTheme("application-exit"), tr("&Quit"), qApp, &QApplication::quit, quitShortcut); // View menu auto viewMenu = menuBar()->addMenu(tr("&View")); viewMenu->addSeparator(); auto dockPanesMenu = viewMenu->addMenu( QIcon::fromTheme("labplot-editvlayout"), tr("Dock &panels", "Panels of the dock, not 'to dock the panels'")); roomListDock->toggleViewAction() ->setStatusTip(tr("Show/hide Rooms dock panel")); dockPanesMenu->addAction(roomListDock->toggleViewAction()); userListDock->toggleViewAction() ->setStatusTip(tr("Show/hide Users dock panel")); dockPanesMenu->addAction(userListDock->toggleViewAction()); viewMenu->addSeparator(); auto showEventsMenu = viewMenu->addMenu(tr("&Display in timeline")); addUiOptionCheckbox( showEventsMenu, tr("Invite events"), tr("Show invite and withdrawn invitation events"), QStringLiteral("show_invite"), true ); addUiOptionCheckbox( showEventsMenu, tr("Normal &join/leave events"), tr("Show join and leave events"), QStringLiteral("show_joinleave"), true ); addUiOptionCheckbox( showEventsMenu, tr("Ban events"), tr("Show ban and unban events"), QStringLiteral("show_ban"), true ); showEventsMenu->addSeparator(); addUiOptionCheckbox( showEventsMenu, tr("&Redacted events"), tr("Show redacted events in the timeline as 'Redacted'" " instead of hiding them entirely"), QStringLiteral("show_redacted") ); addUiOptionCheckbox( showEventsMenu, tr("Changes in display na&me"), tr("Show display name change"), QStringLiteral("show_rename"), true ); addUiOptionCheckbox( showEventsMenu, tr("Avatar &changes"), tr("Show avatar update events"), QStringLiteral("show_avatar_update"), true ); addUiOptionCheckbox( showEventsMenu, tr("Room alias &updates"), tr("Show room alias updates events"), QStringLiteral("show_alias_update"), true ); addUiOptionCheckbox( showEventsMenu, //: A menu item to show/hide meaningless activity such as redacted spam tr("&No-effect activity"), tr("Show/hide meaningless activity" " (join-leave pairs and redacted events between)"), QStringLiteral("show_spammy") ); addUiOptionCheckbox( showEventsMenu, tr("Un&known event types"), tr("Show/hide unknown event types"), QStringLiteral("show_unknown_events") ); viewMenu->addSeparator(); viewMenu->addAction(tr("Edit tags order"), this, [this] { static const auto SettingsKey = QStringLiteral("tags_order"); Quotient::SettingsGroup sg { QStringLiteral("UI/RoomsDock") }; const auto savedOrder = sg.get(SettingsKey).join('\n'); bool ok; const auto newOrder = QInputDialog::getMultiLineText(this, tr("Edit tags order"), tr("Tags can be wildcarded by * next to dot(s)\n" "Clear the box to reset to defaults\n" "Special tags starting with \"im.quotient.\" are: %1\n" "User-defined tags should start with \"u.\"") .arg("invite, left, direct, none"), savedOrder, &ok); if (ok) { if (newOrder.isEmpty()) sg.remove(SettingsKey); else if (newOrder != savedOrder) sg.setValue(SettingsKey, newOrder.split('\n')); roomListDock->updateSortingMode(); } }); viewMenu->addAction(QIcon::fromTheme("format-text-blockquote"), tr("Edit quote style"), [this] { Quotient::SettingsGroup sg { "UI" }; const auto type = sg.get("quote_type"); QStringList list; list << tr("Markdown (prepend each line with >)") << tr("Custom (apply regex from the config file)") << tr("Locale's default (%1)") .arg(QLocale().quoteString(tr("Example quote"))); bool ok; const auto newType = QInputDialog::getItem(this, tr("Edit quote style"), tr("Choose the default style of quotes"), list, type, false, &ok); if (ok) sg.setValue("quote_type", list.indexOf(newType)); }); // Room menu auto roomMenu = menuBar()->addMenu(tr("&Room")); createRoomAction = roomMenu->addAction(QIcon::fromTheme("user-group-new"), tr("Create &new room..."), [this] { static QPointer dlg; summon(dlg, &accountRegistry, this); }); createRoomAction->setShortcut(QKeySequence::New); createRoomAction->setDisabled(true); joinAction = roomMenu->addAction(QIcon::fromTheme("list-add"), tr("&Join room..."), [this] { openUserInput(ForJoining); }); joinAction->setShortcut(Qt::CTRL + Qt::Key_J); joinAction->setDisabled(true); roomMenu->addSeparator(); roomSettingsAction = roomMenu->addAction(QIcon::fromTheme("user-group-properties"), tr("Change room &settings..."), this, [this] { openRoomSettings(); }); roomSettingsAction->setDisabled(true); roomMenu->addSeparator(); openRoomAction = roomMenu->addAction(QIcon::fromTheme("document-open"), tr("Open room..."), [this] { openUserInput(); }); openRoomAction->setStatusTip(tr("Open a room from the room list")); openRoomAction->setShortcut(QKeySequence::Open); openRoomAction->setDisabled(true); roomMenu->addAction(QIcon::fromTheme("window-close"), tr("&Close current room"), [this] { selectRoom(nullptr); }, QKeySequence::Close); // Settings menu auto settingsMenu = menuBar()->addMenu(tr("&Settings")); // Help menu auto helpMenu = menuBar()->addMenu(tr("&Help")); helpMenu->addAction(QIcon::fromTheme("help-about"), tr("&About Quaternion"), [this] { showAboutWindow(); }); helpMenu->addAction(QIcon::fromTheme("help-about-qt"), tr("About &Qt"), [this] { QMessageBox::aboutQt(this); }); { auto notifGroup = new QActionGroup(this); connect(notifGroup, &QActionGroup::triggered, this, [] (QAction* notifAction) { notifAction->setChecked(true); Settings().setValue("UI/notifications", notifAction->data().toString()); }); static const auto MinSetting = QStringLiteral("none"); static const auto GentleSetting = QStringLiteral("non-intrusive"); static const auto LoudSetting = QStringLiteral("intrusive"); auto noNotif = notifGroup->addAction(tr("&Highlight only")); noNotif->setData(MinSetting); noNotif->setStatusTip(tr("Notifications are entirely suppressed")); auto gentleNotif = notifGroup->addAction(tr("&Non-intrusive")); gentleNotif->setData(GentleSetting); gentleNotif->setStatusTip( tr("Show notifications but do not activate the window")); auto fullNotif = notifGroup->addAction(tr("&Full")); fullNotif->setData(LoudSetting); fullNotif->setStatusTip( tr("Show notifications and activate the window")); auto notifMenu = settingsMenu->addMenu( QIcon::fromTheme("preferences-desktop-notification"), tr("Notifications")); for (auto a: {noNotif, gentleNotif, fullNotif}) { a->setCheckable(true); notifMenu->addAction(a); } const auto curSetting = Settings().get("UI/notifications", LoudSetting); if (curSetting == MinSetting) noNotif->setChecked(true); else if (curSetting == GentleSetting) gentleNotif->setChecked(true); else fullNotif->setChecked(true); } { auto layoutGroup = new QActionGroup(this); connect(layoutGroup, &QActionGroup::triggered, this, [this] (QAction* action) { action->setChecked(true); Settings().setValue("UI/timeline_style", action->data().toString()); chatRoomWidget->setRoom(nullptr); chatRoomWidget->setRoom(currentRoom); }); auto defaultLayout = layoutGroup->addAction(tr("Default")); defaultLayout->setStatusTip( tr("The layout with author labels above blocks of messages")); auto xchatLayout = layoutGroup->addAction("XChat"); xchatLayout->setData(QStringLiteral("xchat")); xchatLayout->setStatusTip( tr("The layout with author labels to the left from each message")); auto layoutMenu = settingsMenu->addMenu(QIcon::fromTheme("table"), tr("Timeline layout")); for (auto a: {defaultLayout, xchatLayout}) { a->setCheckable(true); layoutMenu->addAction(a); } const auto curSetting = Settings().value("UI/timeline_style", defaultLayout->data().toString()); if (curSetting == xchatLayout->data().toString()) xchatLayout->setChecked(true); else defaultLayout->setChecked(true); } #if defined Q_OS_UNIX && !defined Q_OS_MAC addUiOptionCheckbox( settingsMenu, tr("Use Breeze style (requires restart)"), tr("Force use Breeze style and icon theme"), QStringLiteral("use_breeze_style"), inFlatpak() ); #endif addUiOptionCheckbox( settingsMenu, tr("Use shuttle scrollbar (requires restart)"), tr("Control scroll velocity instead of position" " with the timeline scrollbar"), QStringLiteral("use_shuttle_dial"), true ); addUiOptionCheckbox( settingsMenu, tr("Load full-size images at once"), tr("Automatically download a full-size image instead of a thumbnail"), QStringLiteral("autoload_images"), true ); addUiOptionCheckbox( settingsMenu, tr("Close to tray"), tr("Make close button [X] minimize to tray instead of closing main window"), QStringLiteral("close_to_tray"), false ); confirmLinksAction = addUiOptionCheckbox( settingsMenu, tr("Confirm opening external links"), tr("Show a confirmation box before opening non-Matrix links" " in an external application"), ConfirmLinksSettingKey, true); settingsMenu->addSeparator(); settingsMenu->addAction(QIcon::fromTheme("preferences-system-network"), tr("Configure &network proxy..."), [this] { static QPointer dlg; summon(dlg, this); }); } void MainWindow::loadSettings() { Quotient::SettingsGroup sg("UI/MainWindow"); if (sg.contains("normal_geometry")) setGeometry(sg.value("normal_geometry").toRect()); if (sg.value("maximized").toBool()) showMaximized(); if (sg.contains("window_parts_state")) restoreState(sg.value("window_parts_state").toByteArray()); } void MainWindow::saveSettings() const { Quotient::SettingsGroup sg("UI/MainWindow"); sg.setValue("normal_geometry", normalGeometry()); sg.setValue("maximized", isMaximized()); sg.setValue("window_parts_state", saveState()); sg.sync(); } template inline QString accessTokenKey(const KeySourceT& source, bool legacyLocation) { auto k = source.userId(); if (!legacyLocation) { if (source.deviceId().isEmpty()) qWarning() << "Device id on the account is not set"; else k += '-' % source.deviceId(); } return k; } template inline std::unique_ptr makeKeychainJob(const QString& appName, const QString& key, bool legacyLocation = false) { auto slotName = appName; if (!legacyLocation) slotName += " access token for " % key; auto j = std::make_unique(slotName); j->setAutoDelete(false); j->setKey(key); return j; } template inline QString accessTokenFileName(const KeySourceT& account, bool legacyLocation = false) { auto fileName = accessTokenKey(account, legacyLocation); fileName.replace(':', '_'); return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) % '/' % fileName; } class AccessTokenFile : public QFile { // clazy:exclude=missing-qobject-macro bool legacyLocation = false; public: template explicit AccessTokenFile(const KeySourceT& source, OpenMode mode = ReadOnly) { Q_ASSERT(mode == ReadOnly || mode == WriteOnly); if (mode == WriteOnly) { remove(accessTokenFileName(source, true)); setFileName(accessTokenFileName(source, false)); remove(); const auto fileDir = QFileInfo(*this).dir(); if (fileDir.exists() || fileDir.mkpath(".")) open(QFile::WriteOnly); return; } for (bool getLegacyLocation: { false, true }) { setFileName(accessTokenFileName(source, getLegacyLocation)); if (open(QFile::ReadOnly)) { if (size() < 1024) { qDebug() << "Found access token file at" << fileName(); legacyLocation = getLegacyLocation; return; } qWarning() << "File" << fileName() << "is" << size() << "bytes long - too long for a token, ignoring it."; } else qWarning() << "Could not open access token file" << fileName(); close(); } } [[nodiscard]] bool isAtLegacyLocation() const { return legacyLocation; } }; QByteArray MainWindow::loadAccessToken(const AccountSettings& account) { #ifdef USE_KEYCHAIN if (Settings().get("UI/use_keychain", true)) return loadAccessTokenFromKeyChain(account); qDebug() << "Explicit opt-out from keychain by user setting"; #endif return AccessTokenFile(account).readAll(); } #ifdef USE_KEYCHAIN QByteArray MainWindow::loadAccessTokenFromKeyChain(const AccountSettings& account) { using namespace QKeychain; auto lastError = Error::OtherError; bool legacyLocation = true; do { legacyLocation = !legacyLocation; // Start with non-legacy const auto& key = accessTokenKey(account, legacyLocation); qDebug().noquote() << "Reading the access token from the keychain for" << key; auto job = makeKeychainJob(qAppName(), key, legacyLocation); QEventLoop loop; connect(job.get(), &Job::finished, &loop, &QEventLoop::quit); job->start(); loop.exec(); if (job->error() == Error::NoError) { auto token = job->binaryData(); if (legacyLocation) { qDebug() << "Migrating the token to the new keychain slot"; if (saveAccessTokenToKeyChain(account, token, false)) { auto* delJob = new DeletePasswordJob(qAppName()); delJob->setAutoDelete(true); delJob->setKey(accessTokenKey(account, true)); connect(delJob, &Job::finished, this, [delJob] { if (delJob->error() != Error::NoError) qWarning().noquote() << "Cleanup of the old keychain slot failed:" << delJob->errorString(); }); delJob->start(); // Run async and move on } } return token; } qWarning().noquote() << "Could not read" << job->service() << "from the keychain:" << job->errorString(); lastError = job->error(); } while (!legacyLocation); // Exit once the legacy round is through // Try token file AccessTokenFile atf(account); const auto& accessToken = atf.readAll(); // Only offer migration if QtKeychain is usable but doesn't have the entry if (lastError == Error::EntryNotFound && !accessToken.isEmpty() && QMessageBox::warning( this, tr("Access token file found"), tr("Do you want to migrate the access token for %1 " "from the file to the keychain?") .arg(account.userId()), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { qInfo() << "Migrating the access token for" << account.userId() << "from the file to the keychain"; if (saveAccessTokenToKeyChain(account, accessToken, false)) { if (!atf.remove()) qWarning() << "Could not remove the access token after migration"; } else { qWarning() << "Migration of the access token failed"; QMessageBox::warning(this, tr("Couldn't migrate access token"), tr("Quaternion couldn't migrate access token for %1 " "from the file to the keychain.") .arg(account.userId()), QMessageBox::Close); } } return accessToken; } #endif bool MainWindow::saveAccessToken(const AccountSettings& account, const QByteArray& accessToken) { #ifdef USE_KEYCHAIN if (Settings().get("UI/use_keychain", true)) return saveAccessTokenToKeyChain(account, accessToken); qDebug() << "Explicit opt-out from keychain by user setting"; #endif // fall through to non QtKeychain-specific code return saveAccessTokenToFile(account, accessToken); } bool MainWindow::saveAccessTokenToFile(const AccountSettings& account, const QByteArray& accessToken) { // (Re-)Make a dedicated file for access_token. AccessTokenFile accountTokenFile(account, QFile::WriteOnly); if (!accountTokenFile.isOpen()) { QMessageBox::warning(this, tr("Couldn't open a file to save access token"), tr("Quaternion couldn't open a file to write the" " access token to. You're logged in but will have" " to provide your password again when you restart" " the application."), QMessageBox::Close); } else { // Try to restrict access rights to the file. The below is useless // on Windows: FAT doesn't control access at all and NTFS is // incompatible with the UNIX perms model used by Qt. If the attempt // didn't have the effect, at least ask the user if it's fine to save // the token to a file readable by others. // TODO: use system-specific API to ensure proper access. if ((accountTokenFile.setPermissions( QFile::ReadOwner|QFile::WriteOwner) && !(accountTokenFile.permissions() & (QFile::ReadGroup|QFile::ReadOther))) || QMessageBox::warning(this, tr("Couldn't set access token file permissions"), tr("Quaternion couldn't restrict permissions on the" " access token file. Do you still want to save" " the access token to it?"), QMessageBox::Yes|QMessageBox::No ) == QMessageBox::Yes) { accountTokenFile.write(accessToken); return true; } } return false; } #ifdef USE_KEYCHAIN bool MainWindow::saveAccessTokenToKeyChain(const AccountSettings& account, const QByteArray& accessToken, bool writeToFile) { using namespace QKeychain; const auto key = accessTokenKey(account, false); qDebug().noquote() << "Save the access token to the keychain for" << key; auto job = makeKeychainJob(qAppName(), key); job->setBinaryData(accessToken); QEventLoop loop; connect(job.get(), &Job::finished, &loop, &QEventLoop::quit); job->start(); loop.exec(); if (!job->error()) return true; qWarning().noquote() << "Could not save access token to the keychain:" << job->errorString(); if (job->error() != Error::NoBackendAvailable && job->error() != Error::NotImplemented && job->error() != Error::OtherError) { if (writeToFile) { const auto button = QMessageBox::warning( this, tr("Couldn't save access token"), tr("Quaternion couldn't save the access token to the keychain." " Do you want to save the access token to file %1?") .arg(accessTokenFileName(account)), QMessageBox::Yes | QMessageBox::No); if (button == QMessageBox::Yes) return saveAccessTokenToFile(account, accessToken); } return false; } return saveAccessTokenToFile(account, accessToken); } #endif void MainWindow::addConnection(Connection* c, const QString& deviceName) { Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection"); using Room = Quotient::Room; c->setLazyLoading(true); accountRegistry.add(c); roomListDock->addConnection(c); c->syncLoop(); connect(c, &Connection::syncDone, this, [this, c, counter = 0]() mutable { if (counter == 0) { firstSyncOver(c); statusBar()->showMessage( tr("First sync completed for %1", "%1 is user id") .arg(c->userId()), 3000); } // Borrowed the logic from Quiark's code in Tensor to cache not too // aggressively and not on the first sync. if (++counter % 17 == 2) c->saveState(); } ); connect( c, &Connection::loggedOut, this, [=] { statusBar()->showMessage(tr("Logged out as %1").arg(c->userId()), 3000); accountRegistry.drop(c); dropConnection(c); }); connect( c, &Connection::networkError, this, [=]{ networkError(c); } ); connect( c, &Connection::syncError, this, [this,c] (const QString& message, const QString& details) { QMessageBox msgBox(QMessageBox::Warning, tr("Sync failed"), accountRegistry.size() > 1 ? tr("The last sync of account %1 has failed with error: %2") .arg(c->userId(), message) : tr("The last sync has failed with error: %1").arg(message), QMessageBox::Retry|QMessageBox::Cancel, this); msgBox.setTextFormat(Qt::PlainText); msgBox.setDefaultButton(QMessageBox::Retry); msgBox.setInformativeText(tr( "Clicking 'Retry' will attempt to resume synchronisation;\n" "Clicking 'Cancel' will stop further synchronisation of this " "account until logout or Quaternion restart.")); msgBox.setDetailedText(details); if (msgBox.exec() == QMessageBox::Retry) c->syncLoop(); }); using namespace Quotient; connect( c, &Connection::requestFailed, this, [this] (BaseJob* job) { if (job->isBackground()) return; auto message = job->error() == BaseJob::UserConsentRequiredError ? tr("Before this server can process your information, you have" " to agree with its terms and conditions; please click the" " button below to open the web page where you can do that") : prettyPrint(job->errorString()); QMessageBox msgBox(QMessageBox::Warning, job->statusCaption(), message, QMessageBox::Close, this); msgBox.setTextFormat(Qt::RichText); msgBox.setDetailedText( tr("Request URL: %1\nResponse:\n%2") .arg(job->requestUrl().toDisplayString(), job->rawDataSample())); QPushButton* openUrlButton = nullptr; if (job->errorUrl().isEmpty()) msgBox.setDefaultButton(QMessageBox::Close); else { openUrlButton = msgBox.addButton(tr("Open web page"), QMessageBox::ActionRole); openUrlButton->setDefault(true); } msgBox.exec(); if (msgBox.clickedButton() == openUrlButton) QDesktopServices::openUrl(job->errorUrl()); }); connect( c, &Connection::loginError, this, [=](const QString& msg){ loginError(c, msg); } ); connect( c, &Connection::newRoom, systemTrayIcon, &SystemTrayIcon::newRoom ); connect( c, &Connection::createdRoom, this, &MainWindow::selectRoom); connect( c, &Connection::joinedRoom, this, [this] (Room* r, Room* prev) { if (currentRoom == prev) selectRoom(r); }); connect( c, &Connection::directChatAvailable, this, [this] (Room* r) { selectRoom(r); statusBar()->showMessage("Direct chat opened", 2000); }); connect( c, &Connection::aboutToDeleteRoom, this, [this] (Room* r) { if (currentRoom == r) selectRoom(nullptr); }); // Update the menu QString accountCaption = c->userId(); if (!deviceName.isEmpty()) accountCaption += '/' % deviceName; QString menuCaption = accountCaption; if (accountRegistry.size() < 10) menuCaption.prepend('&' % QString::number(accountRegistry.size()) % ' '); auto logoutAction = logoutMenu->addAction(menuCaption, [=] { logout(c); }); connect(c, &Connection::destroyed, logoutMenu, std::bind(&QMenu::removeAction, logoutMenu, logoutAction)); openRoomAction->setEnabled(true); createRoomAction->setEnabled(true); joinAction->setEnabled(true); } void MainWindow::dropConnection(Connection* c) { Q_ASSERT_X(c, __FUNCTION__, "Attempt to drop a null connection"); if (currentRoom && currentRoom->connection() == c) selectRoom(nullptr); accountRegistry.drop(c); logoutOnExit.removeOne(c); const auto noMoreAccounts = accountRegistry.isEmpty(); openRoomAction->setDisabled(noMoreAccounts); createRoomAction->setDisabled(noMoreAccounts); joinAction->setDisabled(noMoreAccounts); Q_ASSERT(!logoutOnExit.contains(c) && !c->syncJob()); c->deleteLater(); } void MainWindow::showFirstSyncIndicator() { busyLabel->show(); busyIndicator->start(); statusBar()->showMessage("Syncing, please wait"); } void MainWindow::firstSyncOver(Connection *c) { Q_ASSERT(c != nullptr); firstSyncing.removeOne(c); if (firstSyncing.empty()) { busyLabel->hide(); busyIndicator->stop(); } qDebug() << "Connections still in first sync: " << firstSyncing.size(); } void MainWindow::showLoginWindow(const QString& statusMessage) { const auto& allKnownAccounts = Quotient::SettingsGroup("Accounts").childGroups(); QStringList loggedOffAccounts; for (const auto& a: allKnownAccounts) // Skip already logged in accounts if (!accountRegistry.isLoggedIn(AccountSettings(a).userId())) loggedOffAccounts.push_back(a); doOpenLoginDialog(new LoginDialog(statusMessage, this, loggedOffAccounts)); } void MainWindow::showLoginWindow(const QString& statusMessage, const QString& userId) { auto* reloginAccount = new AccountSettings(userId); auto* dialog = new LoginDialog(statusMessage, this, *reloginAccount); reloginAccount->setParent(dialog); // => Delete with the dialog box doOpenLoginDialog(dialog); connect(dialog, &QDialog::rejected, this, [reloginAccount] { reloginAccount->clearAccessToken(); AccessTokenFile(*reloginAccount).remove(); // XXX: Maybe even remove the account altogether as below? // Quotient::SettingsGroup("Accounts").remove(reloginAccount->userId()); }); } void MainWindow::doOpenLoginDialog(LoginDialog* dialog) { dialog->open(); // See #666: WA_DeleteOnClose kills the dialog object too soon, // invalidating the connection object before it's released to the local // variable below; so the dialog object is explicitly deleted instead of // using WA_DeleteOnClose automagic. connect(dialog, &QDialog::accepted, this, [this, dialog] { auto connection = dialog->releaseConnection(); AccountSettings account(connection->userId()); account.setKeepLoggedIn(dialog->keepLoggedIn()); account.clearAccessToken(); // Drop the legacy - just in case account.setHomeserver(connection->homeserver()); account.setDeviceId(connection->deviceId()); account.setDeviceName(dialog->deviceName()); if (dialog->keepLoggedIn()) { if (!saveAccessToken(account, connection->accessToken())) qWarning() << "Couldn't save access token"; } else logoutOnExit.push_back(connection); account.sync(); auto deviceName = dialog->deviceName(); dialog->deleteLater(); firstSyncing.push_back(connection); showFirstSyncIndicator(); if (accountRegistry.isLoggedIn(connection->userId())) { if (QMessageBox::warning( this, tr("Logging in into a logged in account"), tr("You're trying to log in into an account that's " "already logged in. Do you want to continue?"), QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes) return; deviceName += "-" + connection->deviceId(); } addConnection(connection, deviceName); }); connect(dialog, &QDialog::rejected, dialog, &QObject::deleteLater); } void MainWindow::showAboutWindow() { Dialog aboutDialog(tr("About Quaternion"), QDialogButtonBox::Close, this, Dialog::NoStatusLine); auto* tabWidget = new QTabWidget(); { auto *aboutPage = new QWidget(); tabWidget->addTab(aboutPage, tr("&About")); auto* layout = new QVBoxLayout(aboutPage); auto* imageLabel = new QLabel(); imageLabel->setPixmap(QPixmap(":/icon.png")); imageLabel->setAlignment(Qt::AlignHCenter); layout->addWidget(imageLabel); auto* labelString = new QLabel("

" + QApplication::applicationDisplayName() + " v" + QApplication::applicationVersion() + "

"); labelString->setAlignment(Qt::AlignHCenter); layout->addWidget(labelString); auto* linkLabel = new QLabel("
" % tr("Web page") % ""); linkLabel->setAlignment(Qt::AlignHCenter); linkLabel->setOpenExternalLinks(true); layout->addWidget(linkLabel); layout->addWidget( new QLabel("Copyright (C) 2016-2021 " % tr("Quaternion project contributors"))); #ifdef GIT_SHA1 auto* commitLabel = new QLabel(tr("Built from Git, commit SHA:") + '\n' + QStringLiteral(GIT_SHA1)); commitLabel->setTextInteractionFlags(Qt::TextSelectableByKeyboard| Qt::TextSelectableByMouse); layout->addWidget(commitLabel); #endif #ifdef LIB_GIT_SHA1 auto* libCommitLabel = new QLabel(tr("Library commit SHA:") + '\n' + QStringLiteral(LIB_GIT_SHA1)); libCommitLabel->setTextInteractionFlags(Qt::TextSelectableByKeyboard| Qt::TextSelectableByMouse); layout->addWidget(libCommitLabel); #endif } { auto* thanksLabel = new QLabel( tr("Original project author: %1") .arg("" % tr("Felix Rohrbach") % "") % "
" % tr("Project leader: %1") .arg("" % tr("Alexey \"Kitsune\" Rusakov") % "") % "

" % tr("Contributors:") % "
" % "" % tr("Quaternion contributors @ GitHub") % "
" + "" % tr("libQuotient contributors @ GitHub") % "
" % "" % tr("Quaternion translators @ Lokalise.co") % "
" % tr("Special thanks to %1 for all the testing effort") .arg("nephele") % "

" % tr("Made with:") % "
" % "Qt 5
" "Qt Creator
" "CLion
" "Lokalise
" "Cloudsmith" ); thanksLabel->setTextInteractionFlags(Qt::TextSelectableByKeyboard| Qt::TextBrowserInteraction); thanksLabel->setOpenExternalLinks(true); tabWidget->addTab(thanksLabel, tr("&Thanks")); } aboutDialog.addWidget(tabWidget); aboutDialog.exec(); } void MainWindow::invokeLogin() { using namespace Quotient; const auto accounts = SettingsGroup("Accounts").childGroups(); bool autoLoggedIn = false; for(const auto& accountId: accounts) { AccountSettings account { accountId }; if (!account.homeserver().isEmpty()) { auto accessToken = loadAccessToken(account); if (accessToken.isEmpty()) { // Try to look in the legacy location (QSettings) and if found, // migrate it from there to a file. accessToken = account.accessToken().toLatin1(); if (accessToken.isEmpty()) continue; // No access token anywhere, no autologin saveAccessToken(account, accessToken); account.clearAccessToken(); // Clean the old place } autoLoggedIn = true; auto c = new Connection(account.homeserver()); firstSyncing.push_back(c); auto deviceName = account.deviceName(); connect(c, &Connection::connected, this, [=] { c->loadState(); addConnection(c, deviceName); }); connect(c, &Connection::resolveError, this, [this, c] { firstSyncOver(c); statusBar()->showMessage( tr("Failed to resolve server %1").arg(c->domain()), 4000); }); c->assumeIdentity(account.userId(), accessToken, account.deviceId()); } } if (autoLoggedIn) showFirstSyncIndicator(); else showLoginWindow(tr("Welcome to Quaternion")); } void MainWindow::loginError(Connection* c, const QString& message) { Q_ASSERT_X(c, __FUNCTION__, "Login error on a null connection"); c->stopSync(); // Security over convenience: before allowing back in, remove // the connection from the UI emit c->loggedOut(); // Short circuit login error to logged-out event showLoginWindow(message, c->userId()); } void MainWindow::logout(Connection* c) { Q_ASSERT_X(c, __FUNCTION__, "Logout on a null connection"); AccessTokenFile(*c).remove(); #ifdef USE_KEYCHAIN if (Settings().get("UI/use_keychain", true)) for (bool legacyLocation: { false, true }) { using namespace QKeychain; auto* job = new DeletePasswordJob(qAppName()); job->setAutoDelete(true); job->setKey(accessTokenKey(*c, legacyLocation)); connect(job, &Job::finished, this, [this, job] { switch (job->error()) { case Error::EntryNotFound: qDebug() << "Access token is not in the keychain, nothing " "to delete"; [[fallthrough]]; case Error::NoError: return; // Actual errors follow case Error::NoBackendAvailable: case Error::NotImplemented: case Error::OtherError: break; default: QMessageBox::warning( this, tr("Couldn't delete access token"), tr("Quaternion couldn't delete the access " "token from the keychain."), QMessageBox::Close); } qWarning() << "Could not delete access token from the keychain: " << qUtf8Printable(job->errorString()); }); job->start(); } #endif c->logout(); } Quotient::UriResolveResult MainWindow::visitUser(Quotient::User* user, const QString& action) { if (action == "mention" || action.isEmpty()) chatRoomWidget->insertMention(user); // action=_interactive is checked in openResource() and // converted to "chat" in openUserInput() else if (action == "_interactive" || (action == "chat" && QMessageBox::question(this, tr("Open direct chat?"), tr("Open direct chat with user %1?") .arg(user->fullName())) == QMessageBox::Yes)) user->requestDirectChat(); else return Quotient::IncorrectAction; return Quotient::UriResolved; } void MainWindow::visitRoom(Quotient::Room* room, const QString& eventId) { selectRoom(room); if (!eventId.isEmpty()) chatRoomWidget->timelineWidget()->spotlightEvent(eventId); } void MainWindow::joinRoom(Quotient::Connection* account, const QString& roomAliasOrId, const QStringList& viaServers) { auto* job = account->joinRoom(QUrl::toPercentEncoding(roomAliasOrId), viaServers); // Connection::joinRoom() already connected to success() the code that // initialises the room in the library, which in turn causes RoomListModel // to update the room list. So the below connection to success() will be // triggered after all the initialisation have happened. connect(job, &Quotient::BaseJob::success, this, [this, account, roomAliasOrId] { statusBar()->showMessage( tr("Joined %1 as %2").arg(roomAliasOrId, account->userId())); }); } bool MainWindow::visitNonMatrix(const QUrl& url) { // Return true if the user cancels, treating it as an alternative normal // flow (rather than an abnormal flow when the navigation itself fails). auto doVisit = [this, url] { if (!QDesktopServices::openUrl(url)) QMessageBox::warning(this, tr("No application for the link"), tr("Your operating system could not find an " "application for the link.")); }; using Quotient::SettingsGroup; if (SettingsGroup("UI").get(ConfirmLinksSettingKey, true)) { auto* confirmation = new QMessageBox( QMessageBox::Warning, tr("External link confirmation"), tr("An external application will be opened to visit a " "non-Matrix link:\n\n%1\n\nIs that right?") .arg(url.toDisplayString()), QMessageBox::Ok | QMessageBox::Cancel, this, Qt::Dialog); confirmation->setDefaultButton(nullptr); confirmation->setCheckBox(new QCheckBox(tr("Do not ask again"))); confirmation->setWindowModality(Qt::WindowModal); confirmation->show(); connect(confirmation, &QDialog::finished, this, [this,doVisit,confirmation](int result) { const bool doNotAsk = confirmation->checkBox()->checkState() == Qt::Checked; if (doNotAsk) confirmLinksAction->setDisabled(true); SettingsGroup("UI").setValue(ConfirmLinksSettingKey, !doNotAsk); if (result == QMessageBox::Ok) doVisit(); }); } else doVisit(); return true; } MainWindow::Connection* MainWindow::getDefaultConnection() const { return currentRoom ? currentRoom->connection() : accountRegistry.size() == 1 ? accountRegistry.front() : nullptr; } void MainWindow::openResource(const QString& idOrUri, const QString& action) { Uri uri { idOrUri }; if (!uri.isValid()) { QMessageBox::warning( this, tr("Malformed or empty Matrix id"), tr("%1 is not a correct Matrix identifier").arg(idOrUri), QMessageBox::Close, QMessageBox::Close); return; } auto* account = getDefaultConnection(); if (uri.type() != Uri::NonMatrix) { if (!account) { showLoginWindow(tr("Please connect to a server")); return; } if (!action.isEmpty()) uri.setAction(action); if (uri.action() == "join" && currentRoom // NB: We can't reliably check aliases for being in the upgrade chain && currentRoom->successorId() != uri.primaryId() && currentRoom->predecessorId() != uri.primaryId()) account = chooseConnection( account, tr("Confirm account to join %1").arg(uri.primaryId())); else if (uri.action() == "_interactive") account = chooseConnection( account, uri.type() == Uri::UserId ? tr("Confirm your account to open a direct chat with %1") .arg(uri.primaryId()) : tr("Confirm your account to open %1").arg(idOrUri)); if (!account) return; // The user cancelled the confirmation dialog } const auto result = visitResource(account, uri); if (result == Quotient::CouldNotResolve) QMessageBox::warning(this, tr("Room not found"), tr("There's no room %1 in the room list." " Check the spelling and the account.") .arg(idOrUri)); else // Invalid cases should have been eliminated earlier Q_ASSERT(result == Quotient::UriResolved); } void MainWindow::openRoomSettings(QuaternionRoom* r) { if (!r) r = currentRoom; static std::unordered_map> dlgs; const auto [it, inserted] = dlgs.try_emplace(r); summon(it->second, r, this); if (inserted) connect(it->second, &QObject::destroyed, [r] { dlgs.erase(r); }); } void MainWindow::selectRoom(Quotient::Room* r) { if (r) qDebug() << "Opening room" << r->objectName(); else if (currentRoom) qDebug() << "Closing room" << currentRoom->objectName(); QElapsedTimer et; et.start(); if (currentRoom) disconnect(currentRoom, &QuaternionRoom::displaynameChanged, this, nullptr); currentRoom = static_cast(r); setWindowTitle(r ? r->displayName() : QString()); if (currentRoom) connect(currentRoom, &QuaternionRoom::displaynameChanged, this, [this] { setWindowTitle(currentRoom->displayName()); }); chatRoomWidget->setRoom(currentRoom); roomListDock->setSelectedRoom(currentRoom); userListDock->setRoom(currentRoom); roomSettingsAction->setEnabled(r != nullptr); if (r && !isActiveWindow()) { show(); activateWindow(); } qDebug().noquote() << et << "to" << (r ? "select room " + r->canonicalAlias() : "close the room"); } void MainWindow::showStatusMessage(const QString& message, int timeout) { statusBar()->showMessage(message, timeout); } MainWindow::Connection* MainWindow::chooseConnection(Connection* connection, const QString& prompt) { Q_ASSERT(!accountRegistry.isEmpty()); if (accountRegistry.size() == 1) return accountRegistry.front(); QStringList names; names.reserve(accountRegistry.size()); int defaultIdx = -1; for (auto c: accountRegistry) { names.push_back(c->userId()); if (c == connection) defaultIdx = names.size() - 1; } bool ok = false; const auto choice = QInputDialog::getItem(this, tr("Confirm account"), prompt, names, defaultIdx, false, &ok); if (!ok || choice.isEmpty()) return nullptr; for (auto c: accountRegistry) if (c->userId() == choice) { connection = c; break; } Q_ASSERT(connection); return connection; } void MainWindow::openUserInput(bool forJoining) { if (accountRegistry.isEmpty()) { showLoginWindow(tr("Please connect to a server")); return; } struct D { QString dlgTitle; QString dlgText; QString actionText; }; static const D map[] = { { tr("Open room"), tr("Room or user ID, room alias,\nMatrix URI or matrix.to link"), tr("Go to room") }, { tr("Join room"), tr("Room ID (starting with !)\nor alias (starting with #)"), tr("Join room") } }; const auto& entry = map[forJoining]; Dialog dlg(entry.dlgTitle, this, Dialog::NoStatusLine, entry.actionText, Dialog::NoExtraButtons); auto* accountChooser = new AccountSelector(&accountRegistry); auto* identifier = new QLineEdit(&dlg); auto* defaultConn = getDefaultConnection(); accountChooser->setAccount(defaultConn); // Lay out controls auto* layout = dlg.addLayout(); if (accountRegistry.size() > 1) { layout->addRow(tr("Account"), accountChooser); accountChooser->setFocus(); } else { accountChooser->setCurrentIndex(0); // The only available accountChooser->hide(); // #523 identifier->setFocus(); } layout->addRow(entry.dlgText, identifier); if (!forJoining) { const auto setCompleter = [identifier](Connection* connection) { if (!connection) { identifier->setCompleter(nullptr); return; } QStringList completions; const auto& allRooms = connection->allRooms(); const auto& users = connection->users(); // Assuming that roughly half of rooms in the room list have // a canonical alias; this may be quite a bit off but is better // than not reserving at all completions.reserve(allRooms.size() * 3 / 2 + users.size()); for (auto* room: allRooms) { completions << room->id(); if (!room->canonicalAlias().isEmpty()) completions << room->canonicalAlias(); } for (auto* user: users) completions << user->id(); completions.sort(); completions.erase(std::unique(completions.begin(), completions.end()), completions.end()); auto* completer = new QCompleter(completions); completer->setFilterMode(Qt::MatchContains); identifier->setCompleter(completer); }; setCompleter(accountChooser->currentAccount()); connect(accountChooser, &AccountSelector::currentAccountChanged, identifier, setCompleter); } const auto getUri = [identifier]() -> Uri { return identifier->text().trimmed(); }; auto* okButton = dlg.button(QDialogButtonBox::Ok); okButton->setDisabled(true); connect(identifier, &QLineEdit::textChanged, &dlg, [getUri, okButton, buttonText = entry.actionText] { switch (getUri().type()) { case Uri::RoomId: case Uri::RoomAlias: okButton->setEnabled(true); okButton->setText(buttonText); break; case Uri::UserId: okButton->setEnabled(true); okButton->setText(tr("Chat with user", "On a button in 'Open room' dialog" " when a user identifier is entered")); break; default: okButton->setDisabled(true); okButton->setText( tr("Can't open", "On a disabled button in 'Open room' dialog when" " an invalid/unsupported URI is entered")); } }); if (dlg.exec() != QDialog::Accepted) return; auto uri = getUri(); if (forJoining) uri.setAction("join"); else if (uri.type() == Uri::UserId && (uri.action().isEmpty() || uri.action() == "_interactive")) uri.setAction("chat"); // The default action for users is "mention" switch (visitResource(accountChooser->currentAccount(), uri)) { case Quotient::UriResolved: break; case Quotient::CouldNotResolve: QMessageBox::warning( this, tr("Could not resolve id"), (uri.type() == Uri::NonMatrix ? tr("Could not find an external application to open the URI:") : tr("Could not resolve Matrix identifier")) + "\n\n" + uri.toDisplayString()); break; case Quotient::IncorrectAction: QMessageBox::warning( this, tr("Incorrect action on a Matrix resource"), tr("The URI contains an action '%1' that cannot be applied" " to Matrix resource %2") .arg(uri.action(), uri.toDisplayString(QUrl::RemoveQuery))); break; default: Q_ASSERT(false); // No other values should occur } } void MainWindow::showMillisToRecon(Connection* c) { // TODO: when there are several connections and they are failing, these // notifications render a mess, fighting for the same status bar. Either // switch to a set of icons in the status bar or find a stacking // notifications engine already instead of the status bar. statusBar()->showMessage( tr("Couldn't connect to the server as %1; will retry within %2 seconds") .arg(c->userId()).arg((c->millisToReconnect() + 999) / 1000)); // Integer ceiling } void MainWindow::networkError(Connection* c) { Q_ASSERT_X(c, __FUNCTION__, "Network error on a null connection"); auto timer = new QTimer(this); timer->start(1000); showMillisToRecon(c); connect(timer, &QTimer::timeout, this, [this,c,timer] { if (c->millisToReconnect() > 0) showMillisToRecon(c); else { statusBar()->showMessage(tr("Reconnecting..."), 5000); timer->deleteLater(); } }); } void MainWindow::sslErrors(QNetworkReply* reply, const QList& errors) { for (const auto& error: errors) { if (error.error() == QSslError::NoSslSupport) { static bool showMsgBox = true; if (showMsgBox) { QMessageBox msgBox(QMessageBox::Critical, tr("No SSL support"), error.errorString(), QMessageBox::Close, this); msgBox.setInformativeText( tr("Your SSL configuration does not allow Quaternion" " to establish secure connections.")); msgBox.exec(); showMsgBox = false; } return; } QMessageBox msgBox(QMessageBox::Warning, tr("SSL error"), error.errorString(), QMessageBox::Abort|QMessageBox::Ignore, this); if (!error.certificate().isNull()) msgBox.setDetailedText(error.certificate().toText()); if (msgBox.exec() == QMessageBox::Abort) return; NetworkAccessManager::instance()->addIgnoredSslError(error); } reply->ignoreSslErrors(errors); } void MainWindow::proxyAuthenticationRequired(const QNetworkProxy&, QAuthenticator* auth) { Dialog authDialog(tr("Proxy needs authentication"), this, Dialog::NoStatusLine, tr("Authenticate", "Authenticate with the proxy server"), Dialog::NoExtraButtons); auto layout = authDialog.addLayout(); auto userEdit = new QLineEdit; layout->addRow(tr("User name"), userEdit); auto pwdEdit = new QLineEdit; pwdEdit->setEchoMode(QLineEdit::Password); layout->addRow(tr("Password"), pwdEdit); if (authDialog.exec() == QDialog::Accepted) { auth->setUser(userEdit->text()); auth->setPassword(pwdEdit->text()); } } void MainWindow::closeEvent(QCloseEvent* event) { if (Quotient::SettingsGroup("UI") .value("close_to_tray", false).toBool()) { hide(); event->ignore(); } else { event->accept(); } } Quaternion-0.0.95.1/client/mainwindow.h000066400000000000000000000145671412757327200177230ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2015 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #pragma once #include "accountregistry.h" #include #include namespace Quotient { class Room; class Connection; class AccountSettings; } class RoomListDock; class UserListDock; class ChatRoomWidget; class SystemTrayIcon; class QuaternionRoom; class LoginDialog; class QAction; class QMenu; class QMenuBar; class QSystemTrayIcon; class QMovie; class QLabel; class QLineEdit; class QNetworkReply; class QSslError; class QNetworkProxy; class QAuthenticator; class MainWindow: public QMainWindow, public Quotient::UriResolverBase { Q_OBJECT public: using Connection = Quotient::Connection; MainWindow(); ~MainWindow() override; void addConnection(Connection* c, const QString& deviceName); void dropConnection(Connection* c); // For openUserInput() enum : bool { NoRoomJoining = false, ForJoining = true }; public slots: /// Open non-empty id or URI using the specified action hint /*! Asks the user to choose the connection if necessary */ void openResource(const QString& idOrUri, const QString& action = {}); /// Open a dialog to enter the resource id/URI and then navigate to it void openUserInput(bool forJoining = NoRoomJoining); /// Open/focus the room settings dialog /*! If \p r is empty, the currently open room is used */ void openRoomSettings(QuaternionRoom* r = nullptr); void selectRoom(Quotient::Room* r); void showStatusMessage(const QString& message, int timeout = 0); void logout(Connection* c); private slots: void invokeLogin(); void loginError(Connection* c, const QString& message = {}); void networkError(Connection* c); void sslErrors(QNetworkReply* reply, const QList& errors); void proxyAuthenticationRequired(const QNetworkProxy& /* unused */, QAuthenticator* auth); void showLoginWindow(const QString& statusMessage = {}); void showLoginWindow(const QString& statusMessage, const QString& userId); void showAboutWindow(); // UriResolverBase overrides Quotient::UriResolveResult visitUser(Quotient::User* user, const QString& action) override; void visitRoom(Quotient::Room* room, const QString& eventId) override; void joinRoom(Quotient::Connection* account, const QString& roomAliasOrId, const QStringList& viaServers = {}) override; bool visitNonMatrix(const QUrl& url) override; private: AccountRegistry accountRegistry; QVector logoutOnExit; QVector firstSyncing; RoomListDock* roomListDock = nullptr; UserListDock* userListDock = nullptr; ChatRoomWidget* chatRoomWidget = nullptr; QMovie* busyIndicator = nullptr; QLabel* busyLabel = nullptr; QMenu* connectionMenu = nullptr; QMenu* logoutMenu = nullptr; QAction* openRoomAction = nullptr; QAction* roomSettingsAction = nullptr; QAction* createRoomAction = nullptr; QAction* dcAction = nullptr; QAction* joinAction = nullptr; QAction* confirmLinksAction = nullptr; SystemTrayIcon* systemTrayIcon = nullptr; // FIXME: This will be a problem when we get ability to show // several rooms at once. QuaternionRoom* currentRoom = nullptr; void createMenu(); QAction* addUiOptionCheckbox(QMenu* parent, const QString& text, const QString& statusTip, const QString& settingsKey, bool defaultValue = false); void showFirstSyncIndicator(); void firstSyncOver(Connection* c); void loadSettings(); void saveSettings() const; void doOpenLoginDialog(LoginDialog* dialog); QByteArray loadAccessToken(const Quotient::AccountSettings& account); QByteArray loadAccessTokenFromKeyChain(const Quotient::AccountSettings &account); bool saveAccessToken(const Quotient::AccountSettings& account, const QByteArray& accessToken); bool saveAccessTokenToFile(const Quotient::AccountSettings& account, const QByteArray& accessToken); bool saveAccessTokenToKeyChain(const Quotient::AccountSettings& account, const QByteArray& accessToken, bool writeToFile = true); Connection* chooseConnection(Connection* connection, const QString& prompt); void showMillisToRecon(Connection* c); /// Get the default connection to perform actions /*! * \return the connection of the current room; or, if there's only * one connection, that connection; failing that, nullptr */ Connection* getDefaultConnection() const; void closeEvent(QCloseEvent* event) override; }; Quaternion-0.0.95.1/client/models/000077500000000000000000000000001412757327200166445ustar00rootroot00000000000000Quaternion-0.0.95.1/client/models/abstractroomordering.cpp000066400000000000000000000034411412757327200236040ustar00rootroot00000000000000/****************************************************************************** * Copyright (C) 2018-2019 QMatrixClient Project * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * */ #include "abstractroomordering.h" #include "roomlistmodel.h" #include using namespace std::placeholders; AbstractRoomOrdering::AbstractRoomOrdering(RoomListModel* m) : QObject(m) { } AbstractRoomOrdering::groupLessThan_closure_t AbstractRoomOrdering::groupLessThanFactory() const { return std::bind(&AbstractRoomOrdering::groupLessThan, this, _1, _2); } AbstractRoomOrdering::roomLessThan_closure_t AbstractRoomOrdering::roomLessThanFactory(const QVariant& group) const { return std::bind(&AbstractRoomOrdering::roomLessThan, this, group, _1, _2); } void AbstractRoomOrdering::updateGroups(Room* room) { model()->updateGroups(room); } RoomListModel* AbstractRoomOrdering::model() const { return static_cast(parent()); } Quaternion-0.0.95.1/client/models/abstractroomordering.h000066400000000000000000000100221412757327200232420ustar00rootroot00000000000000/****************************************************************************** * Copyright (C) 2018-2019 QMatrixClient Project * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * */ #pragma once #include #include struct RoomGroup { QVariant key; QVector rooms; bool operator==(const RoomGroup& other) const { return key == other.key; } bool operator!=(const RoomGroup& other) const { return !(*this == other); } bool operator==(const QVariant& otherCaption) const { return key == otherCaption; } bool operator!=(const QVariant& otherCaption) const { return !(*this == otherCaption); } friend bool operator==(const QVariant& otherCaption, const RoomGroup& group) { return group == otherCaption; } friend bool operator!=(const QVariant& otherCaption, const RoomGroup& group) { return !(group == otherCaption); } static inline const auto SystemPrefix = QStringLiteral("im.quotient."); static inline const auto LegacyPrefix = QStringLiteral("org.qmatrixclient."); }; using RoomGroups = QVector; class RoomListModel; class AbstractRoomOrdering : public QObject { Q_OBJECT public: using Room = Quotient::Room; using Connection = Quotient::Connection; using groups_t = QVariantList; explicit AbstractRoomOrdering(RoomListModel* m); public: // Overridables /// Returns human-readable name of the room ordering virtual QString orderingName() const = 0; /// Returns human-readable room group caption virtual QVariant groupLabel(const RoomGroup& g) const = 0; /// Orders a group against a key of another group virtual bool groupLessThan(const RoomGroup& g1, const QVariant& g2key) const = 0; /// Orders two rooms within one group virtual bool roomLessThan(const QVariant& group, const Room* r1, const Room* r2) const = 0; /// Returns the full list of room groups virtual groups_t roomGroups(const Room* room) const = 0; /// Connects order updates to signals from a new Matrix connection virtual void connectSignals(Connection* connection) = 0; /// Connects order updates to signals from a new Matrix room virtual void connectSignals(Room* room) = 0; public: using groupLessThan_closure_t = std::function; /// Returns a closure that invokes this->groupLessThan() groupLessThan_closure_t groupLessThanFactory() const; using roomLessThan_closure_t = std::function; /// Returns a closure that invokes this->roomLessThan in a given group roomLessThan_closure_t roomLessThanFactory(const QVariant& group) const; protected slots: /// A facade for derived classes to trigger RoomListModel::updateGroups virtual void updateGroups(Room* room); protected: RoomListModel* model() const; }; Quaternion-0.0.95.1/client/models/messageeventmodel.cpp000066400000000000000000001057721412757327200230730ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2015 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #include "messageeventmodel.h" #include #include // for qmlRegisterType() #include "../quaternionroom.h" #include "../htmlfilter.h" #include #include #include #include #include #include #include #include #include #include #include #include QHash MessageEventModel::roleNames() const { static const auto roles = [this] { auto roles = QAbstractItemModel::roleNames(); roles.insert(EventTypeRole, "eventType"); roles.insert(EventIdRole, "eventId"); roles.insert(TimeRole, "time"); roles.insert(SectionRole, "section"); roles.insert(AboveSectionRole, "aboveSection"); roles.insert(AuthorRole, "author"); roles.insert(AboveAuthorRole, "aboveAuthor"); roles.insert(ContentRole, "content"); roles.insert(ContentTypeRole, "contentType"); roles.insert(HighlightRole, "highlight"); roles.insert(SpecialMarksRole, "marks"); roles.insert(LongOperationRole, "progressInfo"); roles.insert(AnnotationRole, "annotation"); roles.insert(EventResolvedTypeRole, "eventResolvedType"); roles.insert(RefRole, "refId"); roles.insert(ReactionsRole, "reactions"); return roles; }(); return roles; } MessageEventModel::MessageEventModel(QObject* parent) : QAbstractListModel(parent) { using namespace Quotient; #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) qmlRegisterAnonymousType("Quotient", 1); #else qmlRegisterType(); qRegisterMetaType(); #endif qmlRegisterUncreatableType("Quotient", 1, 0, "EventStatus", "EventStatus is not a creatable type"); // This could be a single line in changeRoom() but then there's a race // condition between the model reset completion and the room property // update in QML - connecting the two signals early on overtakes any QML // connection to modelReset. Ideally the room property could use modelReset // for its NOTIFY signal - unfortunately, moc doesn't support using // parent's signals with parameters in NOTIFY connect(this, &MessageEventModel::modelReset, // this, &MessageEventModel::roomChanged); } QuaternionRoom* MessageEventModel::room() const { return m_currentRoom; } void MessageEventModel::changeRoom(QuaternionRoom* room) { if (room == m_currentRoom) return; if (m_currentRoom) qDebug() << "Disconnecting event model from" << m_currentRoom->objectName(); beginResetModel(); if (m_currentRoom) m_currentRoom->disconnect(this); m_currentRoom = room; if (m_currentRoom) { using namespace Quotient; connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) { incomingEvents(events, timelineBaseIndex()); }); connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) { incomingEvents(events, rowCount()); }); connect(m_currentRoom, &Room::addedMessages, this, [this] (int lowest, int biggest) { endInsertRows(); if (biggest < m_currentRoom->maxTimelineIndex()) { const auto rowBelowInserted = m_currentRoom->maxTimelineIndex() - biggest + timelineBaseIndex() - 1; refreshEventRoles(rowBelowInserted, {AboveAuthorRole, AboveSectionRole}); } for (auto i = m_currentRoom->maxTimelineIndex() - biggest; i <= m_currentRoom->maxTimelineIndex() - lowest; ++i) refreshLastUserEvents(i); }); connect(m_currentRoom, &Room::pendingEventAboutToAdd, this, [this] { beginInsertRows({}, 0, 0); }); connect(m_currentRoom, &Room::pendingEventAdded, this, &MessageEventModel::endInsertRows); connect(m_currentRoom, &Room::pendingEventAboutToMerge, this, [this] (RoomEvent*, int i) { if (i == 0) return; // No need to move anything, just refresh movingEvent = true; // Reverse i because row 0 is bottommost in the model const auto row = timelineBaseIndex() - i - 1; auto moveBegan = beginMoveRows({}, row, row, {}, timelineBaseIndex()); Q_ASSERT(moveBegan); }); connect(m_currentRoom, &Room::pendingEventMerged, this, [this] { if (movingEvent) { endMoveRows(); movingEvent = false; } refreshRow(timelineBaseIndex()); // Refresh the looks refreshLastUserEvents(0); if (timelineBaseIndex() > 0) // Refresh below, see #312 refreshEventRoles(timelineBaseIndex() - 1, {AboveAuthorRole, AboveSectionRole}); }); connect(m_currentRoom, &Room::pendingEventChanged, this, &MessageEventModel::refreshRow); connect(m_currentRoom, &Room::pendingEventAboutToDiscard, this, [this] (int i) { beginRemoveRows({}, i, i); }); connect(m_currentRoom, &Room::pendingEventDiscarded, this, &MessageEventModel::endRemoveRows); connect(m_currentRoom, &Room::readMarkerMoved, this, &MessageEventModel::readMarkerUpdated); connect(m_currentRoom, &Room::replacedEvent, this, [this] (const RoomEvent* newEvent) { refreshLastUserEvents( refreshEvent(newEvent->id()) - timelineBaseIndex()); }); connect(m_currentRoom, &Room::updatedEvent, this, &MessageEventModel::refreshEvent); connect(m_currentRoom, &Room::fileTransferProgress, this, &MessageEventModel::refreshEvent); connect(m_currentRoom, &Room::fileTransferCompleted, this, &MessageEventModel::refreshEvent); connect(m_currentRoom, &Room::fileTransferFailed, this, &MessageEventModel::refreshEvent); connect(m_currentRoom, &Room::fileTransferCancelled, this, &MessageEventModel::refreshEvent); qDebug() << "Event model connected to room" << room->objectName() << "as" << room->localUser()->id(); } endResetModel(); emit readMarkerUpdated(); } int MessageEventModel::refreshEvent(const QString& eventId) { int row = findRow(eventId, true); if (row >= 0) refreshEventRoles(row); else qWarning() << "Trying to refresh inexistent event:" << eventId; return row; } void MessageEventModel::refreshRow(int row) { refreshEventRoles(row); } void MessageEventModel::incomingEvents(Quotient::RoomEventsRange events, int atIndex) { beginInsertRows({}, atIndex, atIndex + int(events.size()) - 1); } int MessageEventModel::readMarkerVisualIndex() const { if (!m_currentRoom) return -1; // Beyond the bottommost (sync) edge of the timeline if (auto r = findRow(m_currentRoom->readMarkerEventId()); r != -1) { // Ensure that the read marker is on a visible event // TODO: move this to libQuotient once it allows to customise // event status calculation while (r < rowCount() - 1 && data(index(r, 0), SpecialMarksRole) == Quotient::EventStatus::Hidden) ++r; return r; } return rowCount(); // Beyond the topmost (history) edge of the timeline } int MessageEventModel::timelineBaseIndex() const { return m_currentRoom ? int(m_currentRoom->pendingEvents().size()) : 0; } void MessageEventModel::refreshEventRoles(int row, const QVector& roles) { const auto idx = index(row); emit dataChanged(idx, idx, roles); } int MessageEventModel::findRow(const QString& id, bool includePending) const { // On 64-bit platforms, difference_type for std containers is long long // but Qt uses int throughout its interfaces; hence casting to int below. if (!id.isEmpty()) { // First try pendingEvents because it is almost always very short. if (includePending) { const auto pendingIt = m_currentRoom->findPendingEvent(id); if (pendingIt != m_currentRoom->pendingEvents().end()) return int(pendingIt - m_currentRoom->pendingEvents().begin()); } const auto timelineIt = m_currentRoom->findInTimeline(id); if (timelineIt != m_currentRoom->historyEdge()) return int(timelineIt - m_currentRoom->messageEvents().rbegin()) + timelineBaseIndex(); } return -1; } inline bool hasValidTimestamp(const Quotient::TimelineItem& ti) { return ti->originTimestamp().isValid(); } QDateTime MessageEventModel::makeMessageTimestamp( const QuaternionRoom::rev_iter_t& baseIt) const { const auto& timeline = m_currentRoom->messageEvents(); if (auto ts = baseIt->event()->originTimestamp(); ts.isValid()) return ts; // The event is most likely redacted or just invalid. // Look for the nearest date around and slap zero time to it. if (auto rit = std::find_if(baseIt, timeline.rend(), hasValidTimestamp); rit != timeline.rend()) return { rit->event()->originTimestamp().date(), {0,0}, Qt::LocalTime }; if (auto it = std::find_if(baseIt.base(), timeline.end(), hasValidTimestamp); it != timeline.end()) return { it->event()->originTimestamp().date(), {0,0}, Qt::LocalTime }; // What kind of room is that?.. qCritical() << "No valid timestamps in the room timeline!"; return {}; } QString MessageEventModel::renderDate(const QDateTime& timestamp) { auto date = timestamp.toLocalTime().date(); static Quotient::SettingsGroup sg { "UI" }; if (sg.get("use_human_friendly_dates", sg.get("banner_human_friendly_date", true))) { if (date == QDate::currentDate()) return tr("Today"); if (date == QDate::currentDate().addDays(-1)) return tr("Yesterday"); if (date == QDate::currentDate().addDays(-2)) return tr("The day before yesterday"); if (date > QDate::currentDate().addDays(-7)) { auto s = QLocale().standaloneDayName(date.dayOfWeek()); // Some locales (e.g., Russian on Windows) don't capitalise // the day name so make sure the first letter is uppercase. if (!s.isEmpty() && !s[0].isUpper()) s[0] = QLocale().toUpper(s.mid(0,1)).at(0); return s; } } return QLocale().toString(date, QLocale::ShortFormat); } bool MessageEventModel::isUserActivityNotable( const QuaternionRoom::rev_iter_t& baseIt) const { const auto& userId = (*baseIt)->isStateEvent() ? (*baseIt)->stateKey() : (*baseIt)->senderId(); // Go up to the nearest join and down to the nearest leave of this author // (limit the lookup to 100 events for the sake of performance); // in this range find out if there's any event from that user besides // joins, leaves and redacted (self- or by somebody else); if there's not, // double-check that there are no redactions and that it's not a single // join or leave. using namespace Quotient; bool joinFound = false, redactionsFound = false; // Find the nearest join of this user above, or a no-nonsense event. for (auto it = baseIt, limit = baseIt + std::min(int(m_currentRoom->historyEdge() - baseIt), 100); it != limit; ++it) { const auto& e = **it; if (e.senderId() != userId && e.stateKey() != userId) continue; if (e.isRedacted()) { redactionsFound = true; continue; } if (auto* me = it->viewAs()) { if (e.stateKey() != userId) return true; // An action on another member is notable if (!me->isJoin()) continue; joinFound = true; break; } return true; // Consider all other events notable } // Find the nearest leave of this user below, or a no-nonsense event bool leaveFound = false; for (auto it = baseIt.base() - 1, limit = baseIt.base() + std::min(int(m_currentRoom->messageEvents().end() - baseIt.base()), 100); it != limit; ++it) { const auto& e = **it; if (e.senderId() != userId && e.stateKey() != userId) continue; if (e.isRedacted()) { redactionsFound = true; continue; } if (auto* me = it->viewAs()) { if (e.stateKey() != userId) return true; // An action on another member is notable if (!me->isLeave() && me->membership() != MembershipType::Ban) continue; leaveFound = true; break; } return true; } // If we are here, it means that no notable events have been found in // the timeline vicinity, and probably redactions are there. Doesn't look // notable but let's give some benefit of doubt. if (redactionsFound) return false; // Join + redactions or redactions + leave return !(joinFound && leaveFound); // Join + (maybe profile changes) + leave } void MessageEventModel::refreshLastUserEvents(int baseTimelineRow) { if (!m_currentRoom || m_currentRoom->timelineSize() <= baseTimelineRow) return; const auto& timelineBottom = m_currentRoom->messageEvents().rbegin(); const auto& lastSender = (*(timelineBottom + baseTimelineRow))->senderId(); const auto limit = timelineBottom + std::min(baseTimelineRow + 100, m_currentRoom->timelineSize()); for (auto it = timelineBottom + std::max(baseTimelineRow - 100, 0); it != limit; ++it) { if ((*it)->senderId() == lastSender) { auto idx = index(it - timelineBottom); emit dataChanged(idx, idx); } } } int MessageEventModel::rowCount(const QModelIndex& parent) const { if( !m_currentRoom || parent.isValid() ) return 0; return m_currentRoom->timelineSize() + m_currentRoom->pendingEvents().size(); } QVariant MessageEventModel::data(const QModelIndex& idx, int role) const { const auto row = idx.row(); if (!idx.isValid() || row >= rowCount()) return {}; bool isPending = row < timelineBaseIndex(); const auto timelineIt = m_currentRoom->messageEvents().crbegin() + std::max(0, row - timelineBaseIndex()); const auto pendingIt = m_currentRoom->pendingEvents().crbegin() + std::min(row, timelineBaseIndex()); const auto& evt = isPending ? **pendingIt : **timelineIt; using namespace Quotient; if( role == Qt::DisplayRole ) { if (evt.isRedacted()) { auto reason = evt.redactedBecause()->reason(); if (reason.isEmpty()) return tr("Redacted"); return tr("Redacted: %1").arg(reason.toHtmlEscaped()); } // clang-format off return visit(evt , [this] (const RoomMessageEvent& e) { // clang-format on using namespace MessageEventContent; if (e.hasTextContent() && e.mimeType().name() != "text/plain") { // Naïvely assume that it's HTML auto htmlBody = static_cast(e.content())->body; auto [cleanHtml, errorPos, errorString] = HtmlFilter::fromMatrixHtml(htmlBody, m_currentRoom); // If HTML is bad (or it's not HTML at all), fall back // to returning the prettified plain text if (errorPos != -1) { cleanHtml = m_currentRoom->prettyPrint(e.plainBody()); static Settings settings; // A manhole to visualise HTML errors if (settings.get("Debug/html", false)) cleanHtml += QStringLiteral("
" "At pos %1: %2") .arg(QString::number(errorPos), errorString); } return cleanHtml; } if (e.hasFileContent()) { auto fileCaption = e.content()->fileInfo()->originalName.toHtmlEscaped(); if (fileCaption.isEmpty()) fileCaption = m_currentRoom->prettyPrint(e.plainBody()); return !fileCaption.isEmpty() ? fileCaption : tr("a file"); } return m_currentRoom->prettyPrint(e.plainBody()); // clang-format off } , [this] (const RoomMemberEvent& e) { // clang-format on // FIXME: Rewind to the name that was at the time of this event const auto subjectName = m_currentRoom->safeMemberName(e.userId()).toHtmlEscaped(); // The below code assumes senderName output in AuthorRole switch( e.membership() ) { case MembershipType::Invite: case MembershipType::Join: { QString text {}; // Part 1: invites and joins if (e.membership() == MembershipType::Invite) text = tr("invited %1 to the room") .arg(subjectName); else if (e.changesMembership()) text = tr("joined the room"); if (!text.isEmpty()) { if (e.repeatsState()) text += ' ' //: State event that doesn't change the state % tr("(repeated)"); if (!e.reason().isEmpty()) text += ": " + e.reason().toHtmlEscaped(); return text; } // Part 2: profile changes of joined members if (e.isRename() && Settings().get("UI/show_rename", true)) { if (e.displayName().isEmpty()) text = tr("cleared the display name"); else text = tr("changed the display name to %1") .arg(e.displayName().toHtmlEscaped()); } if (e.isAvatarUpdate() && Settings().get("UI/show_avatar_update", true)) { if (!text.isEmpty()) //: Joiner for member profile updates; //: mind the leading and trailing spaces! text += tr(" and "); if (e.avatarUrl().isEmpty()) text += tr("cleared the avatar"); else text += tr("updated the avatar"); } return text; } case MembershipType::Leave: if (e.prevContent() && e.prevContent()->membership == MembershipType::Invite) { return (e.senderId() != e.userId()) ? tr("withdrew %1's invitation").arg(subjectName) : tr("rejected the invitation"); } if (e.prevContent() && e.prevContent()->membership == MembershipType::Ban) { return (e.senderId() != e.userId()) ? tr("unbanned %1").arg(subjectName) : tr("self-unbanned"); } return (e.senderId() != e.userId()) ? e.reason().isEmpty() ? tr("kicked %1 from the room") .arg(subjectName) : tr("kicked %1 from the room: %2") .arg(subjectName, e.reason().toHtmlEscaped()) : tr("left the room"); case MembershipType::Ban: return (e.senderId() != e.userId()) ? e.reason().isEmpty() ? tr("banned %1 from the room") .arg(subjectName) : tr("banned %1 from the room: %2") .arg(subjectName, e.reason().toHtmlEscaped()) : tr("self-banned from the room"); case MembershipType::Knock: return tr("knocked"); default: ; } return tr("made something unknown"); // clang-format off } , [] (const RoomAliasesEvent& e) { return tr("has set room aliases on server %1 to: %2") .arg(e.stateKey(), QLocale().createSeparatedList(e.aliases())); } , [] (const RoomCanonicalAliasEvent& e) { return (e.alias().isEmpty()) ? tr("cleared the room main alias") : tr("set the room main alias to: %1").arg(e.alias()); } , [] (const RoomNameEvent& e) { return (e.name().isEmpty()) ? tr("cleared the room name") : tr("set the room name to: %1") .arg(e.name().toHtmlEscaped()); } , [this] (const RoomTopicEvent& e) { return (e.topic().isEmpty()) ? tr("cleared the topic") : tr("set the topic to: %1") .arg(m_currentRoom->prettyPrint(e.topic())); } , [] (const RoomAvatarEvent&) { return tr("changed the room avatar"); } , [] (const EncryptionEvent&) { return tr("activated End-to-End Encryption"); } , [] (const RoomCreateEvent& e) { return (e.isUpgrade() ? tr("upgraded the room to version %1") : tr("created the room, version %1") ).arg(e.version().isEmpty() ? "1" : e.version().toHtmlEscaped()); } , [] (const RoomTombstoneEvent& e) { return tr("upgraded the room: %1") .arg(e.serverMessage().toHtmlEscaped()); } , [] (const StateEventBase& e) { // A small hack for state events from TWIM bot return e.stateKey() == "twim" ? tr("updated the database", "TWIM bot updated the database") : e.stateKey().isEmpty() ? tr("updated %1 state", "%1 - Matrix event type") .arg(e.matrixType()) : tr("updated %1 state for %2", "%1 - Matrix event type, %2 - state key") .arg(e.matrixType(), e.stateKey().toHtmlEscaped()); } , tr("Unknown event") ); // clang-format on } if( role == Qt::ToolTipRole ) { return evt.originalJson(); } if( role == EventTypeRole ) { if (auto e = eventCast(&evt)) { switch (e->msgtype()) { case MessageEventType::Emote: return "emote"; case MessageEventType::Notice: return "notice"; case MessageEventType::Image: return "image"; default: return e->hasFileContent() ? "file" : "message"; } } if (evt.isStateEvent()) return "state"; return "other"; } if (role == EventResolvedTypeRole) return EventTypeRegistry::getMatrixType(evt.type()); if( role == AuthorRole ) { // FIXME: It shouldn't be User, it should be its state "as of event" return QVariant::fromValue(isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId())); } if (role == ContentTypeRole) { if (auto e = eventCast(&evt)) { const auto& contentType = e->mimeType().name(); return contentType == "text/plain" ? QStringLiteral("text/html") : contentType; } return QStringLiteral("text/plain"); } if (role == ContentRole) { if (evt.isRedacted()) { const auto reason = evt.redactedBecause()->reason(); return (reason.isEmpty()) ? tr("Redacted") : tr("Redacted: %1").arg(reason.toHtmlEscaped()); } if (auto e = eventCast(&evt)) { // Cannot use e.contentJson() here because some // EventContent classes inject values into the copy of the // content JSON stored in EventContent::Base return e->hasFileContent() ? QVariant::fromValue(e->content()->originalJson) : QVariant(); } } if( role == HighlightRole ) return m_currentRoom->isEventHighlighted(&evt); if( role == SpecialMarksRole ) { if (is(evt) || is(evt)) return EventStatus::Hidden; // Never show, even pending if (isPending) return !Settings().get("UI/suppress_local_echo") ? pendingIt->deliveryStatus() : EventStatus::Hidden; // isReplacement? if (auto e = eventCast(&evt)) if (!e->replacedEvent().isEmpty()) return EventStatus::Hidden; if ((is(evt) || is(evt)) && !Settings().value("UI/show_alias_update", true).toBool()) return EventStatus::Hidden; auto* memberEvent = timelineIt->viewAs(); if (memberEvent) { if ((memberEvent->isJoin() || memberEvent->isLeave()) && !Settings().value("UI/show_joinleave", true).toBool()) return EventStatus::Hidden; if ((memberEvent->isInvite() || memberEvent->isRejectedInvite()) && !Settings().value("UI/show_invite", true).toBool()) return EventStatus::Hidden; if ((memberEvent->isBan() || memberEvent->isUnban()) && !Settings().value("UI/show_ban", true).toBool()) return EventStatus::Hidden; bool hideRename = memberEvent->isRename() && (!memberEvent->isJoin() && !memberEvent->isLeave()) && !Settings().value("UI/show_rename", true).toBool(); bool hideAvatarUpdate = memberEvent->isAvatarUpdate() && !Settings().value("UI/show_avatar_update", true).toBool(); if ((hideRename && hideAvatarUpdate) || (hideRename && !memberEvent->isAvatarUpdate()) || (hideAvatarUpdate && !memberEvent->isRename())) { return EventStatus::Hidden; } } if (memberEvent || evt.isRedacted()) { if (evt.senderId() != m_currentRoom->localUser()->id() && evt.stateKey() != m_currentRoom->localUser()->id() && !Settings().value("UI/show_spammy").toBool()) { // QElapsedTimer et; et.start(); auto hide = !isUserActivityNotable(timelineIt); // qDebug() << "Checked user activity for" << evt.id() << "in" << et; if (hide) return EventStatus::Hidden; } } if (evt.isRedacted()) return Settings().value("UI/show_redacted").toBool() ? EventStatus::Redacted : EventStatus::Hidden; if (evt.isStateEvent() && static_cast(evt).repeatsState() && !Settings().value("UI/show_noop_events").toBool()) return EventStatus::Hidden; if (!evt.isStateEvent() && !is(evt) && !Settings().value("UI/show_unknown_events").toBool()) return EventStatus::Hidden; return evt.isReplaced() ? EventStatus::Replaced : EventStatus::Normal; } if( role == EventIdRole ) return !evt.id().isEmpty() ? evt.id() : evt.transactionId(); if( role == LongOperationRole ) { if (auto e = eventCast(&evt)) if (e->hasFileContent()) return QVariant::fromValue( m_currentRoom->fileTransferInfo( isPending ? e->transactionId() : e->id())); } if( role == AnnotationRole ) return isPending ? pendingIt->annotation() : QString(); if( role == ReactionsRole ) { // Filter reactions out of all annotations and collate them by key struct Reaction { QString key; QStringList authorsList {}; bool includesLocalUser = false; }; std::vector reactions; // using vector to maintain the order // XXX: Should the list be ordered by the number of reactions instead? const auto& annotations = m_currentRoom->relatedEvents(evt, EventRelation::Annotation()); for (const auto& a: annotations) if (const auto e = eventCast(a)) { auto rIt = std::find_if(reactions.begin(), reactions.end(), [&e] (const Reaction& r) { return r.key == e->relation().key; }); if (rIt == reactions.end()) rIt = reactions.insert(reactions.end(), {e->relation().key}); rIt->authorsList << m_currentRoom->safeMemberName(e->senderId()); rIt->includesLocalUser |= e->senderId() == m_currentRoom->localUser()->id(); } // Prepare the QML model data // NB: Strings are NOT HTML-escaped; QML code must take care to use // Text.PlainText format when displaying them QJsonArray qmlReactions; for (auto&& r: reactions) { const auto authorsCount = r.authorsList.size(); if (r.authorsList.size() > 7) { //: When the reaction comes from too many members r.authorsList.replace(3, tr("%Ln more member(s)", "", r.authorsList.size() - 3)); r.authorsList.erase(r.authorsList.begin() + 4, r.authorsList.end()); } qmlReactions << QJsonObject { { QStringLiteral("key"), r.key }, { QStringLiteral("authorsCount"), authorsCount }, { QStringLiteral("authors"), QLocale().createSeparatedList(r.authorsList) }, { QStringLiteral("includesLocalUser"), r.includesLocalUser } }; } return qmlReactions; } if( role == TimeRole || role == SectionRole) { auto ts = isPending ? pendingIt->lastUpdated() : makeMessageTimestamp(timelineIt); return role == TimeRole ? QVariant(ts) : renderDate(ts); } if( role == AboveSectionRole || role == AboveAuthorRole) for (auto r = row + 1; r < rowCount(); ++r) { auto i = index(r); if (data(i, SpecialMarksRole) != EventStatus::Hidden) return data(i, role == AboveSectionRole ? SectionRole : AuthorRole); } if (role == RefRole) return visit( evt, [](const RoomCreateEvent& e) { return e.predecessor().roomId; }, [](const RoomTombstoneEvent& e) { return e.successorRoomId(); }); return {}; } Quaternion-0.0.95.1/client/models/messageeventmodel.h000066400000000000000000000070251412757327200225300ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2015 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #pragma once #include "../quaternionroom.h" #include class MessageEventModel: public QAbstractListModel { Q_OBJECT Q_PROPERTY(QuaternionRoom* room READ room NOTIFY roomChanged) Q_PROPERTY(int readMarkerVisualIndex READ readMarkerVisualIndex NOTIFY readMarkerUpdated) public: enum EventRoles { EventTypeRole = Qt::UserRole + 1, EventIdRole, TimeRole, SectionRole, AboveSectionRole, AuthorRole, AboveAuthorRole, ContentRole, ContentTypeRole, HighlightRole, SpecialMarksRole, LongOperationRole, AnnotationRole, RefRole, ReactionsRole, EventResolvedTypeRole, }; explicit MessageEventModel(QObject* parent = nullptr); QuaternionRoom* room() const; void changeRoom(QuaternionRoom* room); int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& idx, int role = Qt::DisplayRole) const override; QHash roleNames() const override; int findRow(const QString& id, bool includePending = false) const; signals: void roomChanged(); /// This is different from Room::readMarkerMoved() in that it is also /// emitted when the room or the last read event is first shown void readMarkerUpdated(); private slots: int refreshEvent(const QString& eventId); void refreshRow(int row); void incomingEvents(Quotient::RoomEventsRange events, int atIndex); private: QuaternionRoom* m_currentRoom = nullptr; int readMarkerVisualIndex() const; bool movingEvent = false; int timelineBaseIndex() const; QDateTime makeMessageTimestamp(const QuaternionRoom::rev_iter_t& baseIt) const; static QString renderDate(const QDateTime& timestamp); bool isUserActivityNotable(const QuaternionRoom::rev_iter_t& baseIt) const; void refreshLastUserEvents(int baseTimelineRow); void refreshEventRoles(int row, const QVector& roles = {}); }; Quaternion-0.0.95.1/client/models/orderbytag.cpp000066400000000000000000000241151412757327200215150ustar00rootroot00000000000000/****************************************************************************** * Copyright (C) 2018-2019 QMatrixClient Project * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * */ #include "orderbytag.h" #include "roomlistmodel.h" #include static const auto Invite = RoomGroup::SystemPrefix + "invite"; static const auto DirectChat = RoomGroup::SystemPrefix + "direct"; static const auto Untagged = RoomGroup::SystemPrefix + "none"; static const auto Left = RoomGroup::SystemPrefix + "left"; // TODO: maybe move the tr() strings below from RoomListModel context static auto InvitesLabel() { return RoomListModel::tr("Invited", "The caption for invitations"); } static auto FavouritesLabel() { return RoomListModel::tr("Favourites"); } static auto LowPriorityLabel() { return RoomListModel::tr("Low priority"); } static auto ServerNoticeLabel() { return RoomListModel::tr("Server notices"); } static auto DirectChatsLabel() { return RoomListModel::tr("People", "The caption for direct chats"); } static auto UngroupedRoomsLabel() { return RoomListModel::tr("Ungrouped rooms"); } static auto LeftLabel() { return RoomListModel::tr("Left", "The caption for left rooms"); } QString tagToCaption(const QString& tag) { // clang-format off return tag == Quotient::FavouriteTag ? FavouritesLabel() : tag == Quotient::LowPriorityTag ? LowPriorityLabel() : tag == Quotient::ServerNoticeTag ? ServerNoticeLabel() : tag.startsWith("u.") ? tag.mid(2) : tag; // clang-format on } QString captionToTag(const QString& caption) { // clang-format off return caption == FavouritesLabel() ? Quotient::FavouriteTag : caption == LowPriorityLabel() ? Quotient::LowPriorityTag : caption == ServerNoticeLabel() ? Quotient::ServerNoticeTag : caption.startsWith("m.") || caption.startsWith("u.") ? caption : "u." + caption; // clang-format on } template inline auto findIndex(const QList& list, const VT& value) { // Using std::find() instead of indexOf() so that not found keys were // naturally sorted after found ones (index == list.end() - list.begin() // is more than any index in the list, while index == -1 is less). return std::find(list.begin(), list.end(), value) - list.begin(); } auto findIndexWithWildcards(const QStringList& list, const QString& value) { if (list.empty() || value.isEmpty()) return list.size(); auto i = findIndex(list, value); // Try namespace groupings (".*" in the list), from right to left for (int dotPos = 0; i == list.size() && (dotPos = value.lastIndexOf('.', --dotPos)) != -1;) { i = findIndex(list, value.left(dotPos + 1) + '*'); } return i; } QVariant OrderByTag::groupLabel(const RoomGroup& g) const { // clang-format off const auto caption = g.key == Untagged ? UngroupedRoomsLabel() : g.key == Invite ? InvitesLabel() : g.key == DirectChat ? DirectChatsLabel() : g.key == Left ? LeftLabel() : tagToCaption(g.key.toString()); // clang-format on return RoomListModel::tr("%1 (%Ln room(s))", "", g.rooms.size()).arg(caption); } bool OrderByTag::groupLessThan(const RoomGroup& g1, const QVariant& g2key) const { const auto& lkey = g1.key.toString(); const auto& rkey = g2key.toString(); // See above auto li = findIndexWithWildcards(tagsOrder, lkey); auto ri = findIndexWithWildcards(tagsOrder, rkey); return li < ri || (li == ri && lkey < rkey); } bool OrderByTag::roomLessThan(const QVariant& groupKey, const Room* r1, const Room* r2) const { if (r1 == r2) return false; // 0. Short-circuit for coinciding room objects // 1. Compare tag order values const auto& tag = groupKey.toString(); auto o1 = r1->tag(tag).order; auto o2 = r2->tag(tag).order; if (o2.has_value() != o1.has_value()) return !o2.has_value(); if (o1 && o2) { // Compare floats; fallthrough if neither is smaller if (*o1 < *o2) return true; if (*o1 > *o2) return false; } // 2. Neither tag order is less than the other; compare room display names if (auto roomCmpRes = r1->displayName().localeAwareCompare(r2->displayName())) return roomCmpRes < 0; // 3. Within the same display name, order by room id // (typically the case when both display names are completely empty) if (auto roomIdCmpRes = r1->id().compare(r2->id())) return roomIdCmpRes < 0; // 4. Room ids are equal; order by connections (=userids) const auto c1 = r1->connection(); const auto c2 = r2->connection(); if (c1 != c2) { if (auto usersCmpRes = c1->userId().compare(c2->userId())) return usersCmpRes < 0; // 4a. Two logins under the same userid: pervert, but technically correct Q_ASSERT(c1->accessToken() != c2->accessToken()); return c1->accessToken() < c2->accessToken(); } // 5. Assume two incarnations of the room with the different join state // (by design, join states are distinct within one connection+roomid) Q_ASSERT(r1->joinState() != r2->joinState()); return r1->joinState() < r2->joinState(); } AbstractRoomOrdering::groups_t OrderByTag::roomGroups(const Room* room) const { if (room->joinState() == Quotient::JoinState::Invite) return groups_t {{ Invite }}; if (room->joinState() == Quotient::JoinState::Leave) return groups_t {{ Left }}; auto tags = getFilteredTags(room); if (tags.empty()) tags.push_back(Untagged); // Check successors, reusing room as the current frame, and for each group // shadow this room if there's already any of its successors in the group while ((room = room->successor(Quotient::JoinState::Join))) { auto successorTags = getFilteredTags(room); if (successorTags.empty()) tags.removeOne(Untagged); else for (const auto& t: successorTags) if (tags.contains(t)) tags.removeOne(t); if (tags.empty()) return {}; // No remaining groups, hide the room } groups_t vl; vl.reserve(tags.size()); std::copy(tags.cbegin(), tags.cend(), std::back_inserter(vl)); return vl; } void OrderByTag::connectSignals(Connection* connection) { using DCMap = Quotient::DirectChatsMap; connect( connection, &Connection::directChatsListChanged, this, [this,connection] (const DCMap& additions, const DCMap& removals) { // The same room may show up in removals and in additions if it // moves from one userid to another (pretty weird but encountered // in the wild). Therefore process removals first. for (const auto& rId: removals) if (auto* r = connection->room(rId)) updateGroups(r); for (const auto& rId: additions) if (auto* r = connection->room(rId)) updateGroups(r); }); } void OrderByTag::connectSignals(Room* room) { connect(room, &Room::displaynameChanged, this, [this,room] { updateGroups(room); }); connect(room, &Room::tagsChanged, this, [this,room] { updateGroups(room); }); connect(room, &Room::joinStateChanged, this, [this,room] { updateGroups(room); }); } void OrderByTag::updateGroups(Room* room) { AbstractRoomOrdering::updateGroups(room); // As the room may shadow predecessors, need to update their groups too. if (auto* predRoom = room->predecessor(Quotient::JoinState::Join)) updateGroups(predRoom); } QStringList OrderByTag::getFilteredTags(const Room* room) const { auto allTags = room->tags().keys(); if (room->isDirectChat()) allTags.push_back(DirectChat); QStringList result; for (const auto& t: allTags) if (findIndexWithWildcards(tagsOrder, '-' + t) == tagsOrder.size()) result.push_back(t); // Only copy tags that are not disabled return result; } QStringList OrderByTag::initTagsOrder() { static const QStringList DefaultTagsOrder { Invite, Quotient::FavouriteTag, QStringLiteral("u.*"), DirectChat, Untagged, Quotient::LowPriorityTag, Left }; static const auto SettingsKey = QStringLiteral("tags_order"); static Quotient::SettingsGroup sg { "UI/RoomsDock" }; auto savedOrder = sg.get(SettingsKey); if (savedOrder.isEmpty()) { sg.setValue(SettingsKey, DefaultTagsOrder); return DefaultTagsOrder; } { // Check that the order doesn't use the old prefix and migrate if it does. bool migrated = false; for (auto& s : savedOrder) if (s.startsWith(RoomGroup::LegacyPrefix)) { s.replace(0, RoomGroup::LegacyPrefix.size(), RoomGroup::SystemPrefix); migrated = true; } if (migrated) sg.setValue(SettingsKey, savedOrder); } return savedOrder; } Quaternion-0.0.95.1/client/models/orderbytag.h000066400000000000000000000042651412757327200211660ustar00rootroot00000000000000/****************************************************************************** * Copyright (C) 2018-2019 QMatrixClient Project * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * */ #pragma once #include "abstractroomordering.h" // TODO: When the library l10n is enabled, these two should go down to it QString tagToCaption(const QString& tag); QString captionToTag(const QString& caption); class OrderByTag : public AbstractRoomOrdering { public: explicit OrderByTag(RoomListModel* m) : AbstractRoomOrdering(m), tagsOrder(initTagsOrder()) { } private: QStringList tagsOrder; // Overrides QString orderingName() const override { return QStringLiteral("tag"); } QVariant groupLabel(const RoomGroup& g) const override; bool groupLessThan(const RoomGroup& g1, const QVariant& g2key) const override; bool roomLessThan(const QVariant& groupKey, const Room* r1, const Room* r2) const override; groups_t roomGroups(const Room* room) const override; void connectSignals(Connection* connection) override; void connectSignals(Room* room) override; void updateGroups(Room* room) override; QStringList getFilteredTags(const Room* room) const; static QStringList initTagsOrder(); }; Quaternion-0.0.95.1/client/models/roomlistmodel.cpp000066400000000000000000000521141412757327200222440ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2016 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #include "roomlistmodel.h" #include "../quaternionroom.h" #include #include #include // See a comment in the same place at userlistmodel.cpp #include #include #include #include #include #include RoomListModel::RoomListModel(QAbstractItemView* parent) : QAbstractItemModel(parent) { connect(this, &RoomListModel::modelAboutToBeReset, this, &RoomListModel::saveCurrentSelection); connect(this, &RoomListModel::modelReset, this, &RoomListModel::restoreCurrentSelection); } void RoomListModel::addConnection(Quotient::Connection* connection) { Q_ASSERT(connection); using namespace Quotient; m_connections.emplace_back(connection, this); connect(connection, &Connection::loggedOut, this, [=] { deleteConnection(connection); }); connect(connection, &Connection::newRoom, this, &RoomListModel::addRoom); m_roomOrder->connectSignals(connection); for (auto* r: connection->allRooms()) addRoom(r); } void RoomListModel::deleteConnection(Quotient::Connection* connection) { Q_ASSERT(connection); const auto connIt = find(m_connections.begin(), m_connections.end(), connection); if (connIt == m_connections.end()) { Q_ASSERT_X(connIt == m_connections.end(), __FUNCTION__, "Connection is missing in the rooms model"); return; } for (auto* r: connection->allRooms()) deleteRoom(r); m_connections.erase(connIt); connection->disconnect(this); } void RoomListModel::deleteTag(QModelIndex index) { if (!isValidGroupIndex(index)) return; const auto tag = m_roomGroups[index.row()].key.toString(); if (tag.isEmpty()) { qCritical() << "RoomListModel: Invalid tag at position" << index.row(); return; } if (tag.startsWith(RoomGroup::SystemPrefix)) { qWarning() << "RoomListModel: System groups cannot be deleted " "(tried to delete" << tag << "group)"; return; } // After the below loop, the respective group will magically disappear from // m_roomGroups as well due to tagsChanged() triggered from removeTag() for (const auto& c: m_connections) for (auto* r: c->roomsWithTag(tag)) r->removeTag(tag); Quotient::SettingsGroup("UI/RoomsDock").remove(tag); } void RoomListModel::visitRoom(const Room& room, const std::function& visitor) { // Copy persistent indices because visitors may alter m_roomIndices const auto indices = m_roomIndices.values(&room); for (const auto& idx: indices) { Q_ASSERT(isValidRoomIndex(idx)); if (roomAt(idx) == &room) visitor(idx); else { qCritical() << "Room at" << idx << "is" << roomAt(idx)->objectName() << "instead of" << room.objectName(); Q_ASSERT(false); } } } QVariant RoomListModel::roomGroupAt(QModelIndex idx) const { Q_ASSERT(idx.isValid()); // Root item shouldn't come here // If we're on a room, find its group; otherwise just take the index const auto groupIt = m_roomGroups.cbegin() + (idx.parent().isValid() ? idx.parent() : idx).row(); return groupIt != m_roomGroups.end() ? groupIt->key : QVariant(); } QuaternionRoom* RoomListModel::roomAt(QModelIndex idx) const { return isValidRoomIndex(idx) ? static_cast( m_roomGroups[idx.parent().row()].rooms[idx.row()]) : nullptr; } QModelIndex RoomListModel::indexOf(const QVariant& group) const { const auto groupIt = lowerBoundGroup(group); if (groupIt == m_roomGroups.end() || groupIt->key != group) return {}; // Group not found return index(groupIt - m_roomGroups.begin(), 0); } QModelIndex RoomListModel::indexOf(const QVariant& group, Room* room) const { auto it = m_roomIndices.find(room); if (group.isNull() && it != m_roomIndices.end()) return *it; for (;it != m_roomIndices.end() && it.key() == room; ++it) { Q_ASSERT(isValidRoomIndex(*it)); if (m_roomGroups[it->parent().row()].key == group) return *it; } return {}; } QModelIndex RoomListModel::index(int row, int column, const QModelIndex& parent) const { if (!hasIndex(row, column, parent)) return {}; // Groups get internalId() == -1, rooms get the group ordinal number return createIndex(row, column, quintptr(parent.isValid() ? parent.row() : -1)); } QModelIndex RoomListModel::parent(const QModelIndex& child) const { const auto parentPos = int(child.internalId()); return child.isValid() && parentPos != -1 ? index(parentPos, 0) : QModelIndex(); } void RoomListModel::addRoom(Room* room) { Q_ASSERT(room && !room->id().isEmpty()); addRoomToGroups(room); connectRoomSignals(room); } void RoomListModel::deleteRoom(Room* room) { visitRoom(*room, [this] (QModelIndex idx) { doRemoveRoom(idx); }); room->disconnect(this); } RoomGroups::iterator RoomListModel::tryInsertGroup(const QVariant& key) { Q_ASSERT(!key.toString().isEmpty()); auto gIt = lowerBoundGroup(key); if (gIt == m_roomGroups.end() || gIt->key != key) { const auto gPos = gIt - m_roomGroups.begin(); const auto affectedIdxs = preparePersistentIndexChange(gPos, 1); beginInsertRows({}, gPos, gPos); gIt = m_roomGroups.insert(gIt, {key, {}}); endInsertRows(); changePersistentIndexList(affectedIdxs.first, affectedIdxs.second); emit groupAdded(gPos); } // Check that the group is healthy Q_ASSERT(gIt->key == key && (gIt->rooms.empty() || !gIt->rooms.front()->id().isEmpty())); return gIt; } void RoomListModel::addRoomToGroups(Room* room, QVariantList groups) { if (groups.empty()) groups = m_roomOrder->roomGroups(room); for (const auto& g: std::as_const(groups)) { const auto gIt = tryInsertGroup(g); const auto rIt = lowerBoundRoom(*gIt, room); if (rIt != gIt->rooms.cend() && *rIt == room) { qWarning() << "RoomListModel:" << room->objectName() << "is already listed under group" << g.toString(); continue; } const auto rPos = int(rIt - gIt->rooms.cbegin()); const auto gIdx = index(int(gIt - m_roomGroups.cbegin()), 0); beginInsertRows(gIdx, rPos, rPos); gIt->rooms.insert(rIt, room); endInsertRows(); m_roomIndices.insert(room, index(rPos, 0, gIdx)); qDebug() << "RoomListModel: Added" << room->objectName() << "to group" << gIt->key.toString(); } } void RoomListModel::connectRoomSignals(Room* room) { connect(room, &Room::beforeDestruction, this, &RoomListModel::deleteRoom); m_roomOrder->connectSignals(room); connect(room, &Room::displaynameChanged, this, [this,room] { refresh(room); }); connect(room, &Room::unreadMessagesChanged, this, [this,room] { refresh(room); }); connect(room, &Room::notificationCountChanged, this, [this,room] { refresh(room); }); connect(room, &Room::avatarChanged, this, [this,room] { refresh(room, { Qt::DecorationRole }); }); } void RoomListModel::doRemoveRoom(const QModelIndex &idx) { if (!isValidRoomIndex(idx)) { qCritical() << "Attempt to remove a room at invalid index" << idx; Q_ASSERT(false); return; } const auto gPos = idx.parent().row(); auto& group = m_roomGroups[gPos]; // clazy:exclude=detaching-member const auto rIt = group.rooms.begin() + idx.row(); // clazy:exclude=detaching-member qDebug() << "RoomListModel: Removing room" << (*rIt)->objectName() << "from group" << group.key.toString(); if (m_roomIndices.remove(*rIt, idx) != 1) { qCritical() << "Index" << idx << "for room" << (*rIt)->objectName() << "not found in the index registry"; Q_ASSERT(false); } beginRemoveRows(idx.parent(), idx.row(), idx.row()); group.rooms.erase(rIt); endRemoveRows(); if (group.rooms.empty()) { // Update persistent indices with parents after the deleted one const auto affectedIdxs = preparePersistentIndexChange(gPos + 1, -1); beginRemoveRows({}, gPos, gPos); m_roomGroups.remove(gPos); endRemoveRows(); changePersistentIndexList(affectedIdxs.first, affectedIdxs.second); } } void RoomListModel::doSetOrder(std::unique_ptr&& newOrder) { beginResetModel(); m_roomGroups.clear(); m_roomIndices.clear(); if (m_roomOrder) m_roomOrder->deleteLater(); m_roomOrder = newOrder.release(); endResetModel(); for (const auto& c: m_connections) { m_roomOrder->connectSignals(c); const auto& allRooms = c->allRooms(); for (auto* r: allRooms) { addRoomToGroups(r); m_roomOrder->connectSignals(r); } } } std::pair RoomListModel::preparePersistentIndexChange(int fromPos, int shiftValue) const { QModelIndexList from, to; for (auto& pIdx: persistentIndexList()) if (isValidRoomIndex(pIdx) && pIdx.parent().row() >= fromPos) { from.append(pIdx); to.append(createIndex(pIdx.row(), pIdx.column(), quintptr(int(pIdx.internalId()) + shiftValue))); } return { std::move(from), std::move(to) }; } int RoomListModel::rowCount(const QModelIndex& parent) const { if (!parent.isValid()) return m_roomGroups.size(); if (isValidGroupIndex(parent)) return m_roomGroups[parent.row()].rooms.size(); return 0; // Rooms have no children } int RoomListModel::totalRooms() const { int result = 0; for (const auto& c: m_connections) result += c->allRooms().size(); return result; } bool RoomListModel::isValidGroupIndex(const QModelIndex& i) const { return i.isValid() && !i.parent().isValid() && i.row() < m_roomGroups.size(); } bool RoomListModel::isValidRoomIndex(const QModelIndex& i) const { return i.isValid() && isValidGroupIndex(i.parent()) && i.row() < m_roomGroups[i.parent().row()].rooms.size(); } QVariant RoomListModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) return {}; const auto* view = static_cast(parent()); if (isValidGroupIndex(index)) { if (role == Qt::DisplayRole) { int unreadRoomsCount = 0; for (auto &r: m_roomGroups[index.row()].rooms) unreadRoomsCount += r->unreadCount() != -1; const auto postfix = unreadRoomsCount ? QStringLiteral(" [%1]").arg(unreadRoomsCount) : QString(); return m_roomOrder->groupLabel(m_roomGroups[index.row()]).toString() + postfix; } // It would be more proper to do it in RoomListItemDelegate // (see roomlistdock.cpp) but I (@kitsune) couldn't find a working way. if (role == Qt::BackgroundRole) return view->palette().brush(QPalette::Active, QPalette::Button); if (role == HighlightCountRole) { int highlightCount = 0; for (auto &r: m_roomGroups[index.row()].rooms) highlightCount += r->highlightCount(); return highlightCount; } return {}; } auto* const room = roomAt(index); if (!room) return {}; // if (index.column() == 1) // return room->lastUpdated(); // if (index.column() == 2) // return room->lastAttended(); static const auto RoomNameTemplate = tr("%1 (as %2)", "%Room (as %user)"); auto disambiguatedName = room->displayName(); if (role == Qt::DisplayRole || role == Qt::ToolTipRole) for (const auto& c: m_connections) if (c != room->connection() && c->room(room->id(), room->joinState())) disambiguatedName = RoomNameTemplate.arg(room->displayName(), room->localUser()->id()); using Quotient::JoinState; switch (role) { case Qt::DisplayRole: { static Quotient::Settings settings; auto unreadCount = room->unreadCount(); auto notifCount = room->notificationCount(); auto unreadTilReadReceipt = unreadCount - notifCount; QString value = (room->isUnstable() ? "(!)" : "") % disambiguatedName; if (unreadCount >= 0) value += " [" % (unreadTilReadReceipt > 0 ? QLocale().toString(unreadTilReadReceipt) : QString()) % (room->readMarker() == room->historyEdge() ? "?" : "") % (notifCount > 0 ? QStringLiteral("+%L1").arg(notifCount) : QString()) % ']'; if (settings.get("Debug/read_receipts", false)) { auto localReadReceipt = room->readMarker(room->localUser()); value += " <" % QString::number(room->historyEdge() - localReadReceipt) % '|' % QString::number(room->syncEdge() - localReadReceipt.base()) % '>'; } return value; } case Qt::DecorationRole: { const auto dpi = view->devicePixelRatioF(); if (auto avatar = room->avatar(int(view->iconSize().height() * dpi)); !avatar.isNull()) { avatar.setDevicePixelRatio(dpi); return QIcon(QPixmap::fromImage(avatar)); } switch (room->joinState()) { case JoinState::Join: return QIcon::fromTheme("user-available", QIcon(":/irc-channel-joined")); case JoinState::Invite: return QIcon::fromTheme("contact-new", QIcon(":/irc-channel-invited")); case JoinState::Leave: return QIcon::fromTheme("user-offline", QIcon(":/irc-channel-parted")); default: Q_ASSERT(false); // Unknown JoinState? } return {}; // Shouldn't reach here } case Qt::ToolTipRole: { QString result = "" % disambiguatedName.toHtmlEscaped() % "
" % tr("Main alias: %1").arg(room->canonicalAlias().toHtmlEscaped()) % "
" % //: The number of joined members tr("Joined: %L1").arg(room->joinedCount()); if (room->invitedCount() > 0) result += //: The number of invited users "
" % tr("Invited: %L1").arg(room->invitedCount()); const auto directChatUsers = room->directChatUsers(); if (!directChatUsers.isEmpty()) { QStringList userNames; userNames.reserve(directChatUsers.size()); for (auto* user: directChatUsers) userNames.push_back(user->displayname(room).toHtmlEscaped()); result += "
" % tr("Direct chat with %1") .arg(QLocale().createSeparatedList(userNames)); } if (room->usesEncryption()) result += "
" % tr("The room enforces encryption"); if (room->isUnstable()) { result += "
(!) " % tr("This room's version is unstable!"); if (room->canSwitchVersions()) result += ' ' % tr("Consider upgrading to a stable version" " (use room settings for that)"); } if (const auto unreadCount = room->unreadCount(); unreadCount >= 0) { // TODO for 0.0.96: tr("Messages after fully read marker: %L1") result += "
" % tr("Unread messages: %L1") .arg(unreadCount); if (room->readMarker() == room->historyEdge()) result += ' ' % /*: Unread messages */ tr("(maybe more)"); } // TODO for 0.0.96: tr("Unread notifications since read receipt: %L1") if (const auto nfCount = room->notificationCount(); nfCount > 0) result += "
" % tr("Unread notifications: %L1").arg(nfCount); if (const auto hlCount = room->highlightCount(); hlCount > 0) result += "
" % tr("Unread highlights: %L1").arg(hlCount); // Room ids are pretty safe from rogue HTML; escape it just in case result += "
" % tr("ID: %1").arg(room->id().toHtmlEscaped()) % "
"; switch (room->joinState()) { case JoinState::Join: result += tr("You joined this room"); break; case JoinState::Leave: result += tr("You left this room"); break; case JoinState::Invite: result += tr("You were invited into this room"); } return result; } case HasUnreadRole: return room->unreadCount() > 0; case HighlightCountRole: return room->highlightCount(); case JoinStateRole: if (!room->successorId().isEmpty()) return QStringLiteral("upgraded"); return toCString(room->joinState()); // TODO: drop toCString once on lib 0.7 case ObjectRole: return QVariant::fromValue(room); default: return {}; } } int RoomListModel::columnCount(const QModelIndex&) const { return 1; } void RoomListModel::updateGroups(Room* room) { auto groups = m_roomOrder->roomGroups(room); const auto oldRoomIndices = m_roomIndices.values(room); for (const auto& oldIndex: oldRoomIndices) { Q_ASSERT(isValidRoomIndex(oldIndex)); const auto gIdx = oldIndex.parent(); auto& group = m_roomGroups[gIdx.row()]; if (groups.removeOne(group.key)) // Test and remove at once { // The room still in this group but may need to move around const auto oldIt = group.rooms.begin() + oldIndex.row(); const auto newIt = lowerBoundRoom(group, room); if (newIt != oldIt) { beginMoveRows(gIdx, oldIndex.row(), oldIndex.row(), gIdx, int(newIt - group.rooms.begin())); if (newIt > oldIt) std::rotate(oldIt, oldIt + 1, newIt); else std::rotate(newIt, oldIt, oldIt + 1); endMoveRows(); } Q_ASSERT(roomAt(oldIndex) == room); } else doRemoveRoom(oldIndex); // May invalidate `group` and `gIdx` } if (!groups.empty()) addRoomToGroups(room, groups); // Groups the room wasn't before qDebug() << "RoomListModel: groups for" << room->objectName() << "updated"; } void RoomListModel::refresh(Room* room, const QVector& roles) { // The problem here is that the change might cause the room to change // its groups. Assume for now that such changes are processed elsewhere // where details about the change are available (e.g. in tagsChanged). visitRoom(*room, [this,&roles] (const QModelIndex &idx) { emit dataChanged(idx, idx, roles); emit dataChanged(idx.parent(), idx.parent(), roles); }); } Quaternion-0.0.95.1/client/models/roomlistmodel.h000066400000000000000000000125041412757327200217100ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2016 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #pragma once #include "abstractroomordering.h" #include "../quaternionroom.h" #include #include #include #include class QAbstractItemView; class RoomListModel: public QAbstractItemModel { Q_OBJECT template using ConnectionsGuard = Quotient::ConnectionsGuard; public: enum Roles { HasUnreadRole = Qt::UserRole + 1, HighlightCountRole, JoinStateRole, ObjectRole }; using Room = Quotient::Room; explicit RoomListModel(QAbstractItemView* parent); ~RoomListModel() override = default; QVariant roomGroupAt(QModelIndex idx) const; QuaternionRoom* roomAt(QModelIndex idx) const; QModelIndex indexOf(const QVariant& group) const; QModelIndex indexOf(const QVariant& group, Room* room) const; QModelIndex index(int row, int column, const QModelIndex& parent = {}) const override; QModelIndex parent(const QModelIndex& index) const override; using QObject::parent; QVariant data(const QModelIndex& index, int role) const override; int columnCount(const QModelIndex&) const override; int rowCount(const QModelIndex& parent) const override; int totalRooms() const; bool isValidGroupIndex(const QModelIndex& i) const; bool isValidRoomIndex(const QModelIndex& i) const; template void setOrder() { doSetOrder(std::make_unique(this)); } signals: void groupAdded(int row); void saveCurrentSelection(); void restoreCurrentSelection(); public slots: void addConnection(Quotient::Connection* connection); void deleteConnection(Quotient::Connection* connection); // FIXME, quotient-im/libQuotient#63: // This should go to the library's ConnectionManager/RoomManager void deleteTag(QModelIndex index); private slots: void addRoom(Room* room); void refresh(Room* room, const QVector& roles = {}); void deleteRoom(Room* room); void updateGroups(Room* room); private: friend class AbstractRoomOrdering; std::vector> m_connections; RoomGroups m_roomGroups; AbstractRoomOrdering* m_roomOrder = nullptr; QMultiHash m_roomIndices; RoomGroups::iterator tryInsertGroup(const QVariant& key); void addRoomToGroups(Room* room, QVariantList groups = {}); void connectRoomSignals(Room* room); void doRemoveRoom(const QModelIndex& idx); void visitRoom(const Room& room, const std::function& visitor); void doSetOrder(std::unique_ptr&& newOrder); std::pair preparePersistentIndexChange(int fromPos, int shiftValue) const; // Beware, the returned iterators are as short-lived as QModelIndex'es auto lowerBoundGroup(const QVariant& group) { return std::lower_bound(m_roomGroups.begin(), m_roomGroups.end(), group, m_roomOrder->groupLessThanFactory()); } auto lowerBoundGroup(const QVariant& group) const { return std::lower_bound(m_roomGroups.begin(), m_roomGroups.end(), group, m_roomOrder->groupLessThanFactory()); } auto lowerBoundRoom(RoomGroup& group, Room* room) const { return std::lower_bound(group.rooms.begin(), group.rooms.end(), room, m_roomOrder->roomLessThanFactory(group.key)); } auto lowerBoundRoom(const RoomGroup& group, Room* room) const { return std::lower_bound(group.rooms.begin(), group.rooms.end(), room, m_roomOrder->roomLessThanFactory(group.key)); } }; Quaternion-0.0.95.1/client/models/userlistmodel.cpp000066400000000000000000000171471412757327200222550ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2015 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #include "userlistmodel.h" #include #include #include #include #include // Injecting the dependency on a view is not so nice; but the way the model // provides avatar decorations depends on the delegate size #include #include #include #include UserListModel::UserListModel(QAbstractItemView* parent) : QAbstractListModel(parent), m_currentRoom(nullptr) { } UserListModel::~UserListModel() = default; void UserListModel::setRoom(Quotient::Room* room) { if (m_currentRoom == room) return; using namespace Quotient; beginResetModel(); if (m_currentRoom) { m_currentRoom->connection()->disconnect(this); m_currentRoom->disconnect(this); for (auto* user: std::as_const(m_users)) user->disconnect(this); m_users.clear(); } m_currentRoom = room; if (m_currentRoom) { connect(m_currentRoom, &Room::userAdded, this, &UserListModel::userAdded); connect(m_currentRoom, &Room::userRemoved, this, &UserListModel::userRemoved); connect(m_currentRoom, &Room::memberAboutToRename, this, &UserListModel::userRemoved); connect(m_currentRoom, &Room::memberRenamed, this, &UserListModel::userAdded); connect(m_currentRoom, &Room::memberListChanged, this, &UserListModel::membersChanged); filter({}); for (auto* user: std::as_const(m_users)) connect(user, &User::avatarChanged, this, &UserListModel::avatarChanged); connect(m_currentRoom->connection(), &Connection::loggedOut, this, [=] { setRoom(nullptr); }); qDebug() << m_users.count() << "user(s) in the room"; } endResetModel(); } Quotient::User* UserListModel::userAt(QModelIndex index) { if (index.row() < 0 || index.row() >= m_users.size()) return nullptr; return m_users.at(index.row()); } QVariant UserListModel::data(const QModelIndex& index, int role) const { if( !index.isValid() ) return QVariant(); if( index.row() >= m_users.count() ) { qDebug() << "UserListModel, something's wrong: index.row() >= m_users.count()"; return QVariant(); } auto user = m_users.at(index.row()); if( role == Qt::DisplayRole ) { return user->displayname(m_currentRoom); } const auto* view = static_cast(parent()); if (role == Qt::DecorationRole) { // Make user avatars 150% high compared to display names const auto dpi = view->devicePixelRatioF(); if (auto av = user->avatar(int(view->iconSize().height() * dpi), m_currentRoom); !av.isNull()) { av.setDevicePixelRatio(dpi); return QIcon(QPixmap::fromImage(av)); } // TODO: Show a different fallback icon for invited users return QIcon::fromTheme("user-available", QIcon(":/irc-channel-joined")); } if (role == Qt::ToolTipRole) { auto tooltip = QStringLiteral("%1
%2") .arg(user->name(m_currentRoom).toHtmlEscaped(), user->id().toHtmlEscaped()); // TODO: Find a new way to determine that the user is bridged // if (!user->bridged().isEmpty()) // tooltip += "
" + tr("Bridged from: %1").arg(user->bridged()); return tooltip; } if (role == Qt::ForegroundRole) { // FIXME: boilerplate with TimelineItem.qml:57 const auto& palette = view->palette(); return QColor::fromHslF(user->hueF(), 1 - palette.color(QPalette::Window).saturationF(), 0.9 - 0.7 * palette.color(QPalette::Window).lightnessF(), palette.color(QPalette::ButtonText).alphaF()); } return QVariant(); } int UserListModel::rowCount(const QModelIndex& parent) const { if( parent.isValid() ) return 0; return m_users.count(); } void UserListModel::userAdded(Quotient::User* user) { auto pos = findUserPos(user); if (pos != m_users.size() && m_users[pos] == user) { qWarning() << "Trying to add the user" << user->id() << "but it's already in the user list"; return; } beginInsertRows(QModelIndex(), pos, pos); m_users.insert(pos, user); endInsertRows(); connect( user, &Quotient::User::avatarChanged, this, &UserListModel::avatarChanged ); } void UserListModel::userRemoved(Quotient::User* user) { auto pos = findUserPos(user); if (pos == m_users.size()) { qWarning() << "Trying to remove a room member not in the user list:" << user->id(); return; } beginRemoveRows(QModelIndex(), pos, pos); m_users.removeAt(pos); endRemoveRows(); user->disconnect(this); } void UserListModel::filter(const QString& filterString) { if (m_currentRoom == nullptr) return; QElapsedTimer et; et.start(); beginResetModel(); m_users.clear(); const auto all = m_currentRoom->users(); std::remove_copy_if(all.begin(), all.end(), std::back_inserter(m_users), [&](User* u) { return !(u->name(m_currentRoom).contains(filterString) || u->id().contains(filterString)); }); std::sort(m_users.begin(), m_users.end(), m_currentRoom->memberSorter()); endResetModel(); qDebug() << "Filtering" << m_users.size() << "user(s) in" << m_currentRoom->displayName() << "took" << et; } void UserListModel::refresh(Quotient::User* user, QVector roles) { auto pos = findUserPos(user); if ( pos != m_users.size() ) emit dataChanged(index(pos), index(pos), roles); else qWarning() << "Trying to access a room member not in the user list"; } void UserListModel::avatarChanged(Quotient::User* user, const Quotient::Room* context) { if (context == m_currentRoom) refresh(user, {Qt::DecorationRole}); } int UserListModel::findUserPos(User* user) const { return findUserPos(m_currentRoom->roomMembername(user)); } int UserListModel::findUserPos(const QString& username) const { return m_currentRoom->memberSorter().lowerBoundIndex(m_users, username); } Quaternion-0.0.95.1/client/models/userlistmodel.h000066400000000000000000000047711412757327200217210ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2015 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #pragma once #include class QAbstractItemView; namespace Quotient { class Connection; class Room; class User; } class UserListModel: public QAbstractListModel { Q_OBJECT public: using User = Quotient::User; UserListModel(QAbstractItemView* parent); virtual ~UserListModel(); void setRoom(Quotient::Room* room); User* userAt(QModelIndex index); QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; int rowCount(const QModelIndex& parent=QModelIndex()) const override; signals: void membersChanged(); //< Reflection of Room::memberListChanged public slots: void filter(const QString& filterString); private slots: void userAdded(User* user); void userRemoved(User* user); void refresh(User* user, QVector roles = {}); void avatarChanged(User* user, const Quotient::Room* context); private: Quotient::Room* m_currentRoom; QList m_users; int findUserPos(User* user) const; int findUserPos(const QString& username) const; }; Quaternion-0.0.95.1/client/networkconfigdialog.cpp000066400000000000000000000124241412757327200221270ustar00rootroot00000000000000/****************************************************************************** * Copyright (C) 2017 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "networkconfigdialog.h" #include #include #include #include #include #include #include #include #include #include #include QLabel* makeBuddyLabel(QString labelText, QWidget* field) { auto label = new QLabel(labelText); label->setBuddy(field); return label; } NetworkConfigDialog::NetworkConfigDialog(QWidget* parent) : Dialog(tr("Network proxy settings"), parent) , useProxyBox(new QGroupBox(tr("&Override system defaults"), this)) , proxyTypeGroup(new QButtonGroup(this)) , proxyHostName(new QLineEdit(this)) , proxyPort(new QSpinBox(this)) , proxyUserName(new QLineEdit(this)) { // Create and configure all the controls useProxyBox->setCheckable(true); useProxyBox->setChecked(false); connect(useProxyBox, &QGroupBox::toggled, this, &NetworkConfigDialog::maybeDisableControls); auto noProxyButton = new QRadioButton(tr("&No proxy")); noProxyButton->setChecked(true); proxyTypeGroup->addButton(noProxyButton, QNetworkProxy::NoProxy); proxyTypeGroup->addButton(new QRadioButton(tr("&HTTP(S) proxy")), QNetworkProxy::HttpProxy); proxyTypeGroup->addButton(new QRadioButton(tr("&SOCKS5 proxy")), QNetworkProxy::Socks5Proxy); connect(proxyTypeGroup, #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) QOverload::of(&QButtonGroup::buttonToggled), #else &QButtonGroup::idToggled, #endif this, &NetworkConfigDialog::maybeDisableControls); maybeDisableControls(); auto hostLabel = makeBuddyLabel(tr("Host"), proxyHostName); auto portLabel = makeBuddyLabel(tr("Port"), proxyPort); auto userLabel = makeBuddyLabel(tr("User name"), proxyUserName); proxyPort->setRange(0, 65535); proxyPort->setSpecialValueText(QStringLiteral(" ")); // Now laying all this out auto proxyTypeLayout = new QGridLayout; auto radios = proxyTypeGroup->buttons(); proxyTypeLayout->addWidget(radios[0], 0, 0); for (int i = 2; i <= radios.size(); ++i) // Consider i as 1-based index proxyTypeLayout->addWidget(radios[i - 1], i / 2, i % 2); auto hostPortLayout = new QHBoxLayout; for (auto l: { hostLabel, portLabel }) { hostPortLayout->addWidget(l); hostPortLayout->addWidget(l->buddy()); } auto userNameLayout = new QHBoxLayout; userNameLayout->addWidget(userLabel); userNameLayout->addWidget(userLabel->buddy()); auto proxySettingsLayout = new QVBoxLayout(useProxyBox); proxySettingsLayout->addLayout(proxyTypeLayout); proxySettingsLayout->addLayout(hostPortLayout); proxySettingsLayout->addLayout(userNameLayout); addWidget(useProxyBox); } NetworkConfigDialog::~NetworkConfigDialog() = default; void NetworkConfigDialog::maybeDisableControls() { if (useProxyBox->isChecked()) { bool disable = proxyTypeGroup->checkedId() == -1 || proxyTypeGroup->checkedId() == QNetworkProxy::NoProxy; proxyHostName->setDisabled(disable); proxyPort->setDisabled(disable); proxyUserName->setDisabled(disable); } } void NetworkConfigDialog::apply() { Quotient::NetworkSettings networkSettings; auto proxyType = useProxyBox->isChecked() ? QNetworkProxy::ProxyType(proxyTypeGroup->checkedId()) : QNetworkProxy::DefaultProxy; networkSettings.setProxyType(proxyType); networkSettings.setProxyHostName(proxyHostName->text()); networkSettings.setProxyPort(quint16(proxyPort->value())); networkSettings.setupApplicationProxy(); // Should we do something for authentication at all?.. accept(); } void NetworkConfigDialog::load() { Quotient::NetworkSettings networkSettings; auto proxyType = networkSettings.proxyType(); if (proxyType == QNetworkProxy::DefaultProxy) { useProxyBox->setChecked(false); } else { useProxyBox->setChecked(true); if (auto b = proxyTypeGroup->button(proxyType)) b->setChecked(true); } proxyHostName->setText(networkSettings.proxyHostName()); auto port = networkSettings.proxyPort(); if (port > 0) proxyPort->setValue(port); } Quaternion-0.0.95.1/client/networkconfigdialog.h000066400000000000000000000026741412757327200216020ustar00rootroot00000000000000/****************************************************************************** * Copyright (C) 2017 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "dialog.h" class QGroupBox; class QButtonGroup; class QLineEdit; class QSpinBox; class NetworkConfigDialog : public Dialog { Q_OBJECT public: explicit NetworkConfigDialog(QWidget* parent = nullptr); ~NetworkConfigDialog(); private slots: void apply() override; void load() override; void maybeDisableControls(); private: QGroupBox* useProxyBox; QButtonGroup* proxyTypeGroup; QLineEdit* proxyHostName; QSpinBox* proxyPort; QLineEdit* proxyUserName; }; Quaternion-0.0.95.1/client/profiledialog.cpp000066400000000000000000000322211412757327200207050ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2019 Karol Kosek * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #include "profiledialog.h" #include "accountselector.h" #include "mainwindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using Quotient::BaseJob, Quotient::User, Quotient::Room; class TimestampTableItem : public QTableWidgetItem { public: explicit TimestampTableItem(const QDateTime& timestamp) : QTableWidgetItem(QLocale().toString(timestamp, QLocale::ShortFormat), UserType) { setData(Qt::UserRole, timestamp); } explicit TimestampTableItem(const TimestampTableItem& other) = default; ~TimestampTableItem() override = default; void operator=(const TimestampTableItem& other) = delete; TimestampTableItem* clone() const override { return new TimestampTableItem(*this); } bool operator<(const QTableWidgetItem& other) const override { return other.type() != UserType ? QTableWidgetItem::operator<(other) : data(Qt::UserRole).value() < other.data(Qt::UserRole).value(); } }; /*! Device table class * * Encapsulates the columns model and formatting */ class ProfileDialog::DeviceTable : public QTableWidget { public: enum Columns : int { DeviceName, DeviceId, LastTimeSeen, LastIpAddr }; DeviceTable(); ~DeviceTable() override = default; template using ItemType = std::conditional_t; template static inline constexpr auto itemFlags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemFlag((ColumnN == DeviceName) & Qt::ItemIsEditable); using QTableWidget::setItem; template inline auto setItem(int row, const DataT& data) -> std::enable_if_t, DataT>> { auto* item = new ItemType(data); item->setFlags(itemFlags); setItem(row, ColumnN, item); } void markupRow(int row, void (QFont::*fontFn)(bool), const QString& rowToolTip, bool flagValue = true); void markCurrentRow(int row) { markupRow(row, &QFont::setBold, tr("This is the current device")); } void fillPendingData(const QString& currentDeviceId); void refresh(const QVector& devices, const QString ¤tDeviceId); }; ProfileDialog::DeviceTable::DeviceTable() { static const QStringList Headers { // Must be synchronised with DeviceTable::Columns tr("Device display name"), tr("Device ID"), tr("Last time seen"), tr("Last IP address") }; setColumnCount(Headers.size()); setHorizontalHeaderLabels(Headers); auto* headerCtl = horizontalHeader(); headerCtl->setSectionResizeMode(QHeaderView::Interactive); headerCtl->setSectionsMovable(true); #if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) headerCtl->setFirstSectionMovable(false); #endif headerCtl->setSortIndicatorShown(true); verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); verticalHeader()->hide(); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setSelectionBehavior(QAbstractItemView::SelectRows); setTabKeyNavigation(false); setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); } void updateAvatarButton(Quotient::User* user, QPushButton* btn) { const auto img = user->avatar(128); if (img.isNull()) { btn->setText(ProfileDialog::tr("No avatar")); btn->setIcon({}); } else { btn->setText({}); btn->setIcon(QPixmap::fromImage(img)); btn->setIconSize(img.size()); } } ProfileDialog::ProfileDialog(AccountRegistry* accounts, MainWindow* parent) : Dialog(tr("User profiles"), parent) , m_settings("UI/ProfileDialog") , m_avatar(new QPushButton) , m_accountSelector(new AccountSelector(accounts)) , m_displayName(new QLineEdit) , m_accessTokenLabel(new QLabel) , m_currentAccount(nullptr) { Q_ASSERT(accounts != nullptr); auto* accountLayout = addLayout(); accountLayout->addRow(tr("Account"), m_accountSelector); connect(m_accountSelector, &AccountSelector::currentAccountChanged, this, &ProfileDialog::load); connect(accounts, &AccountRegistry::aboutToDropAccount, this, [this, accounts] { if (accounts->size() == 1) close(); // The last account is about to be dropped }); auto cardLayout = addLayout(); m_avatar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); cardLayout->addWidget(m_avatar, Qt::AlignLeft|Qt::AlignTop); connect(m_avatar, &QPushButton::clicked, this, &ProfileDialog::uploadAvatar); { auto essentialsLayout = new QFormLayout(); essentialsLayout->addRow(tr("Display Name"), m_displayName); auto accessTokenLayout = new QHBoxLayout(); accessTokenLayout->addWidget(m_accessTokenLabel); auto copyAccessToken = new QPushButton(tr("Copy to clipboard")); accessTokenLayout->addWidget(copyAccessToken); essentialsLayout->addRow(tr("Access token"), accessTokenLayout); cardLayout->addLayout(essentialsLayout); connect(copyAccessToken, &QPushButton::clicked, this, [this] { QGuiApplication::clipboard()->setText(account()->accessToken()); }); } m_deviceTable = new DeviceTable(); addWidget(m_deviceTable); button(QDialogButtonBox::Ok)->setText(tr("Apply and close")); if (m_settings.contains("normal_geometry")) setGeometry(m_settings.value("normal_geometry").toRect()); } ProfileDialog::~ProfileDialog() { m_settings.setValue("normal_geometry", normalGeometry()); m_settings.setValue("device_table_state", m_deviceTable->horizontalHeader()->saveState()); m_settings.sync(); } void ProfileDialog::setAccount(Account* newAccount) { m_accountSelector->setAccount(newAccount); } ProfileDialog::Account* ProfileDialog::account() const { return m_currentAccount; } void ProfileDialog::DeviceTable::markupRow(int row, void (QFont::*fontFn)(bool), const QString& rowToolTip, bool flagValue) { Q_ASSERT(row < rowCount()); for (int c = DeviceName; c < columnCount(); ++c) if (auto* it = item(row, c)) { it->setToolTip(rowToolTip); auto font = it->font(); (font.*fontFn)(flagValue); it->setFont(font); } } void ProfileDialog::DeviceTable::fillPendingData(const QString& currentDeviceId) { setRowCount(2); setItem(0, currentDeviceId); setItem(0, QDateTime::currentDateTime()); markCurrentRow(0); { auto* loadingMsg = new QTableWidgetItem(tr("Loading other devices...")); loadingMsg->setFlags(Qt::NoItemFlags); setItem(1, DeviceName, loadingMsg); } } void ProfileDialog::DeviceTable::refresh(const QVector& devices, const QString& currentDeviceId) { clearContents(); setRowCount(devices.size()); for (int i = 0; i < devices.size(); ++i) { auto device = devices[i]; setItem(i, device.displayName); setItem(i, device.deviceId); if (device.lastSeenTs) setItem(i, QDateTime::fromMSecsSinceEpoch( *device.lastSeenTs)); setItem(i, device.lastSeenIp); if (device.deviceId == currentDeviceId) markCurrentRow(i); } setSortingEnabled(true); } void ProfileDialog::load() { if (m_currentAccount) disconnect(m_currentAccount->user(), nullptr, this, nullptr); if (m_devicesJob) m_devicesJob->abandon(); m_deviceTable->clearContents(); m_avatar->setText(tr("No avatar")); m_avatar->setIcon({}); m_displayName->clear(); m_accessTokenLabel->clear(); m_currentAccount = m_accountSelector->currentAccount(); if (!m_currentAccount) return; auto* user = m_currentAccount->user(); updateAvatarButton(user, m_avatar); connect(user, &User::avatarChanged, this, [this](User*, const Room* room) { if (!room) updateAvatarButton(account()->user(), m_avatar); }); m_displayName->setText(user->name()); m_displayName->setFocus(); connect(user, &User::nameChanged, this, [this](const QString& newName, auto, const Room* room) { if (!room) m_displayName->setText(newName); }); auto accessToken = account()->accessToken(); if (Q_LIKELY(accessToken.size() > 10)) accessToken.replace(5, accessToken.size() - 10, "..."); m_accessTokenLabel->setText(accessToken); m_deviceTable->setSortingEnabled(false); m_deviceTable->fillPendingData(m_currentAccount->deviceId()); if (!m_settings.contains("device_table_state")) m_deviceTable->resizeColumnsToContents(); m_devicesJob = m_currentAccount->callApi(); connect(m_devicesJob, &BaseJob::success, m_deviceTable, [this] { m_devices = m_devicesJob->devices(); m_deviceTable->refresh(m_devices, m_currentAccount->deviceId()); if (m_settings.contains("device_table_state")) m_deviceTable->horizontalHeader()->restoreState( m_settings.value("device_table_state").toByteArray()); else m_deviceTable->sortByColumn(DeviceTable::LastTimeSeen, Qt::DescendingOrder); }); } void ProfileDialog::apply() { if (!m_currentAccount) { qWarning() << "ProfileDialog: no account chosen, can't apply changes"; return; } auto* user = m_currentAccount->user(); if (m_displayName->text() != user->name()) user->rename(m_displayName->text()); if (!m_newAvatarPath.isEmpty()) user->setAvatar(m_newAvatarPath); for (const auto& device: std::as_const(m_devices)) { const auto& list = m_deviceTable->findItems(device.deviceId, Qt::MatchExactly); if (list.empty()) continue; const auto& newName = m_deviceTable->item(list[0]->row(), 0)->text(); if (!list.isEmpty() && newName != device.displayName) m_currentAccount->callApi(device.deviceId, newName); } accept(); } void ProfileDialog::uploadAvatar() { const auto& dirs = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation); auto* fDlg = new QFileDialog(this, tr("Set avatar"), dirs.isEmpty() ? QString() : dirs.back()); fDlg->setFileMode(QFileDialog::ExistingFile); fDlg->setMimeTypeFilters({ "image/jpeg", "image/png", "application/octet-stream" }); fDlg->open(); connect(fDlg, &QFileDialog::fileSelected, this, [this](const QString& fileName) { m_newAvatarPath = fileName; if (!m_newAvatarPath.isEmpty()) { auto img = QImage(m_newAvatarPath) .scaled(m_avatar->iconSize(), Qt::KeepAspectRatio); m_avatar->setIcon(QPixmap(m_newAvatarPath)); } }); } Quaternion-0.0.95.1/client/profiledialog.h000066400000000000000000000045731412757327200203630ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2019 Karol Kosek * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #pragma once #include "dialog.h" #include "accountregistry.h" #include #include #include class AccountSelector; class MainWindow; class QComboBox; class QLineEdit; namespace Quotient { class GetDevicesJob; } class ProfileDialog : public Dialog { Q_OBJECT public: using Account = AccountRegistry::Account; explicit ProfileDialog(AccountRegistry* accounts, MainWindow* parent); ~ProfileDialog() override; void setAccount(Account* newAccount); Account* account() const; private slots: void load() override; void apply() override; void uploadAvatar(); private: Quotient::SettingsGroup m_settings; class DeviceTable; DeviceTable* m_deviceTable; QPushButton* m_avatar; AccountSelector* m_accountSelector; QLineEdit* m_displayName; QLabel* m_accessTokenLabel; Account* m_currentAccount; QString m_newAvatarPath; QPointer m_devicesJob; QVector m_devices; }; Quaternion-0.0.95.1/client/qml/000077500000000000000000000000001412757327200161525ustar00rootroot00000000000000Quaternion-0.0.95.1/client/qml/AnimatedTransition.qml000066400000000000000000000002171412757327200224620ustar00rootroot00000000000000import QtQuick 2.0 import Quotient 1.0 Transition { property var settings: TimelineSettings { } enabled: settings.enable_animations } Quaternion-0.0.95.1/client/qml/AnimationBehavior.qml000066400000000000000000000002151412757327200222620ustar00rootroot00000000000000import QtQuick 2.0 import Quotient 1.0 Behavior { property var settings: TimelineSettings { } enabled: settings.enable_animations } Quaternion-0.0.95.1/client/qml/Attachment.qml000066400000000000000000000022771412757327200207650ustar00rootroot00000000000000import QtQuick 2.0 import Quotient 1.0 Item { width: parent.width height: visible ? childrenRect.height : 0 property bool openOnFinished: false readonly property bool downloaded: progressInfo && !progressInfo.isUpload && progressInfo.completed onDownloadedChanged: { if (downloaded && openOnFinished) openLocalFile() } function openExternally() { if (progressInfo.localPath.toString() || downloaded) openLocalFile() else { openOnFinished = true room.downloadFile(eventId) } } function openLocalFile() { if (Qt.openUrlExternally(progressInfo.localPath)) return; controller.showStatusMessage( "Couldn't determine how to open the file, " + "opening its folder instead", 5000) if (Qt.openUrlExternally(progressInfo.localDir)) return; controller.showStatusMessage( "Couldn't determine how to open the file or its folder.", 5000) } Connections { target: controller onOpenExternally: if (currentIndex === index) openExternally() } } Quaternion-0.0.95.1/client/qml/AuthorInteractionArea.qml000066400000000000000000000010011412757327200231100ustar00rootroot00000000000000TimelineMouseArea { property var authorId enabled: parent.visible anchors.fill: parent cursorShape: Qt.PointingHandCursor acceptedButtons: Qt.LeftButton|Qt.MiddleButton hoverEnabled: true onEntered: controller.showStatusMessage(authorId) onExited: controller.showStatusMessage("") onClicked: controller.resourceRequested(authorId, mouse.button === Qt.LeftButton ? "mention" : "_interactive") } Quaternion-0.0.95.1/client/qml/FastNumberAnimation.qml000066400000000000000000000002371412757327200225750ustar00rootroot00000000000000import QtQuick 2.0 import Quotient 1.0 NumberAnimation { property var settings: TimelineSettings { } duration: settings.fast_animations_duration_ms } Quaternion-0.0.95.1/client/qml/FileContent.qml000066400000000000000000000060471412757327200211060ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.1 Attachment { TextEdit { id: fileTransferInfo width: parent.width selectByMouse: true; readOnly: true; font: timelabel.font color: textColor renderType: settings.render_type text: qsTr("Size: %1, declared type: %2") .arg(content.info ? humanSize(content.info.size) : "") .arg(content.info ? content.info.mimetype : "unknown") + (progressInfo && progressInfo.isUpload ? " (" + (progressInfo.completed ? qsTr("uploaded from %1", "%1 is a local file name") : qsTr("being uploaded from %1", "%1 is a local file name")) .arg(progressInfo.localPath) + ')' : downloaded ? " (" + qsTr("downloaded to %1", "%1 is a local file name") .arg(progressInfo.localPath) + ')' : "") textFormat: TextEdit.PlainText wrapMode: Text.Wrap; TimelineMouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton hoverEnabled: true onContainsMouseChanged: controller.showStatusMessage(containsMouse ? room.fileSource(eventId) : "") } TimelineMouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton cursorShape: Qt.IBeamCursor onClicked: controller.showMenu(index, textFieldImpl.hoveredLink, textFieldImpl.selectedText, showingDetails) } } ProgressBar { id: transferProgress visible: progressInfo && progressInfo.started anchors.fill: fileTransferInfo value: progressInfo ? progressInfo.progress / progressInfo.total : -1 indeterminate: !progressInfo || progressInfo.progress < 0 } RowLayout { anchors.top: fileTransferInfo.bottom width: parent.width spacing: 2 CheckBox { id: openOnFinishedFlag text: qsTr("Open after downloading") visible: progressInfo && !progressInfo.isUpload && transferProgress.visible checked: openOnFinished } Button { text: qsTr("Cancel") visible: progressInfo && progressInfo.started onClicked: room.cancelFileTransfer(eventId) } Button { text: qsTr("Save as...") visible: !progressInfo || (!progressInfo.isUpload && !progressInfo.started) onClicked: controller.saveFileAs(eventId) } Button { text: qsTr("Open") visible: !openOnFinishedFlag.visible onClicked: openExternally() } Button { text: qsTr("Open folder") visible: progressInfo && progressInfo.localDir onClicked: Qt.openUrlExternally(progressInfo.localDir) } } } Quaternion-0.0.95.1/client/qml/ImageContent.qml000066400000000000000000000025561412757327200212520ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Layouts 1.1 Attachment { property var sourceSize property url source property var maxHeight property bool autoload Image { id: imageContent width: parent.width height: sourceSize.height * Math.min(maxHeight / sourceSize.height * 0.9, Math.min(width / sourceSize.width, 1)) fillMode: Image.PreserveAspectFit horizontalAlignment: Image.AlignLeft source: parent.source sourceSize: parent.sourceSize TimelineMouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton hoverEnabled: true onContainsMouseChanged: controller.showStatusMessage(containsMouse ? room.fileSource(eventId) : "") onClicked: openExternally() } TimelineMouseArea { anchors.fill: parent acceptedButtons: Qt.RightButton cursorShape: Qt.PointingHandCursor onClicked: controller.showMenu(index, textFieldImpl.hoveredLink, textFieldImpl.selectedText, showingDetails) } Component.onCompleted: if (visible && autoload && !downloaded && !(progressInfo && progressInfo.isUpload)) room.downloadFile(eventId) } } Quaternion-0.0.95.1/client/qml/MyToolTip.qml000066400000000000000000000013431412757327200205660ustar00rootroot00000000000000import QtQuick 2.0 import QtQuick.Controls 2.1 import Quotient 1.0 ToolTip { id:tooltip TimelineSettings { id: settings } padding: 4 font: settings.font background: Rectangle { SystemPalette { id: palette; colorGroup: SystemPalette.Active } radius: 3 color: palette.window border.width: 1 border.color: palette.windowText } enter: AnimatedTransition { NormalNumberAnimation { target: tooltip property: "opacity" easing.type: Easing.OutQuad from: 0 to: 0.9 } } exit: AnimatedTransition { FastNumberAnimation { target: tooltip property: "opacity" easing.type: Easing.InQuad to: 0 } } } Quaternion-0.0.95.1/client/qml/NormalNumberAnimation.qml000066400000000000000000000002321412757327200231230ustar00rootroot00000000000000import QtQuick 2.0 import Quotient 1.0 NumberAnimation { property var settings: TimelineSettings { } duration: settings.animations_duration_ms } Quaternion-0.0.95.1/client/qml/ScrollToButton.qml000066400000000000000000000010061412757327200216170ustar00rootroot00000000000000import QtQuick 2.9 import QtQuick.Controls 2.2 RoundButton { height: settings.fontHeight * 2 width: height hoverEnabled: true opacity: visible * (0.7 + hovered * 0.2) display: Button.IconOnly icon.color: defaultPalette.buttonText AnimationBehavior on opacity { NormalNumberAnimation { easing.type: Easing.OutQuad } } AnimationBehavior on anchors.bottomMargin { NormalNumberAnimation { easing.type: Easing.OutQuad } } } Quaternion-0.0.95.1/client/qml/Timeline.qml000066400000000000000000000737641412757327200204540ustar00rootroot00000000000000import QtQuick 2.10 // Qt 5.10 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.1 import QtGraphicalEffects 1.0 // For fancy highlighting import Quotient 1.0 Page { id: root property var room: messageModel ? messageModel.room : undefined TimelineSettings { id: settings readonly property bool use_shuttle_dial: value("UI/use_shuttle_dial", true) Component.onCompleted: console.log("Using timeline font: " + font) } SystemPalette { id: defaultPalette; colorGroup: SystemPalette.Active } SystemPalette { id: disabledPalette; colorGroup: SystemPalette.Disabled } background: Rectangle { color: defaultPalette.base radius: 2 } contentWidth: width function humanSize(bytes) { if (!bytes) return qsTr("Unknown", "Unknown attachment size") if (bytes < 4000) return qsTr("%Ln byte(s)", "", bytes) bytes = Math.round(bytes / 100) / 10 if (bytes < 2000) return qsTr("%L1 kB").arg(bytes) bytes = Math.round(bytes / 100) / 10 if (bytes < 2000) return qsTr("%L1 MB").arg(bytes) return qsTr("%L1 GB").arg(Math.round(bytes / 100) / 10) } function mixColors(baseColor, mixedColor, mixRatio) { return Qt.tint(baseColor, Qt.rgba(mixedColor.r, mixedColor.g, mixedColor.b, mixRatio)) } header: Rectangle { id: roomHeader height: headerText.height + 5 color: defaultPalette.window border.color: disabledPalette.windowText radius: 2 visible: !!room property bool showTopic: true Image { id: roomAvatar anchors.verticalCenter: headerText.verticalCenter anchors.left: parent.left anchors.margins: 2 height: headerText.height // implicitWidth on its own doesn't respect the scale down of // the received image (that almost always happens) width: Math.min(headerText.height / implicitHeight * implicitWidth, parent.width / 2.618) // Golden ratio - just for fun source: room && room.avatarMediaId ? "image://mtx/" + room.avatarMediaId : "" // Safe upper limit (see also topicField) sourceSize: Qt.size(-1, settings.lineSpacing * 9) fillMode: Image.PreserveAspectFit AnimationBehavior on width { NormalNumberAnimation { easing.type: Easing.OutQuad } } } Column { id: headerText anchors.left: roomAvatar.right anchors.right: versionActionButton.left anchors.top: parent.top anchors.margins: 2 spacing: 2 TextEdit { id: roomName width: roomNameMetrics.advanceWidth height: roomNameMetrics.height clip: true readonly property bool hasName: !!room && room.displayName !== "" TextMetrics { id: roomNameMetrics font: roomName.font elide: Text.ElideRight elideWidth: headerText.width text: roomName.hasName ? room.displayName : qsTr("(no name)") } text: roomNameMetrics.elidedText color: (hasName ? defaultPalette : disabledPalette).windowText font.bold: true font.family: settings.font.family font.pointSize: settings.font.pointSize renderType: settings.render_type readOnly: true selectByKeyboard: true selectByMouse: true ToolTipArea { enabled: roomName.hasName && (roomNameMetrics.text != roomNameMetrics.elidedText || roomName.lineCount > 1) text: room ? room.htmlSafeDisplayName : "" } } Label { id: versionNotice visible: !!room && (room.isUnstable || room.successorId !== "") width: parent.width text: !room ? "" : room.successorId !== "" ? qsTr("This room has been upgraded.") : room.isUnstable ? qsTr("Unstable room version!") : "" elide: Text.ElideRight font.italic: true font.family: settings.font.family font.pointSize: settings.font.pointSize renderType: settings.render_type ToolTipArea { enabled: parent.truncated text: parent.text } } ScrollView { id: topicField visible: roomHeader.showTopic width: parent.width // Allow 6 lines of the topic (or 20% of the vertical space); // if there are more than 6 lines, show half-line as a hint height: Math.min(topicText.contentHeight, root.height / 5, settings.lineSpacing * 6.5) clip: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AsNeeded AnimationBehavior on height { NormalNumberAnimation { easing.type: Easing.OutQuad } } // FIXME: The below TextEdit+MouseArea is a massive copy-paste // from TimelineItem.qml. We need to make a separate component // for these (RichTextField?). TextEdit { id: topicText width: topicField.width readonly property bool hasTopic: !!room && room.topic !== "" text: hasTopic ? room.prettyPrint(room.topic) : qsTr("(no topic)") color: (hasTopic ? defaultPalette : disabledPalette).windowText textFormat: TextEdit.RichText font: settings.font renderType: settings.render_type readOnly: true selectByKeyboard: true; selectByMouse: true; wrapMode: TextEdit.Wrap onHoveredLinkChanged: controller.showStatusMessage(hoveredLink) onLinkActivated: controller.resourceRequested(link) } } } MouseArea { anchors.fill: headerText acceptedButtons: Qt.MiddleButton | Qt.RightButton cursorShape: topicText.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor onClicked: { if (topicText.hoveredLink) controller.resourceRequested(topicText.hoveredLink, "_interactive") else if (mouse.button === Qt.RightButton) contextMenu.popup() } Menu { id: contextMenu MenuItem { text: roomHeader.showTopic ? qsTr("Hide topic") : qsTr("Show topic") onTriggered: roomHeader.showTopic = !roomHeader.showTopic } } } Button { id: versionActionButton visible: !!room && ((room.isUnstable && room.canSwitchVersions()) || room.successorId !== "") anchors.verticalCenter: headerText.verticalCenter anchors.right: parent.right width: visible * implicitWidth text: !room ? "" : room.successorId !== "" ? qsTr("Go to\nnew room") : qsTr("Room\nsettings") onClicked: if (room.successorId !== "") controller.resourceRequested(room.successorId, "join") else controller.roomSettingsRequested() } } ListView { id: chatView anchors.fill: parent model: messageModel delegate: TimelineItem { width: chatView.width - scrollerArea.width view: chatView moving: chatView.moving || shuttleDial.value // #737; the solution found in // https://bugreports.qt.io/browse/QT3DS-784 ListView.delayRemove: true } verticalLayoutDirection: ListView.BottomToTop flickableDirection: Flickable.VerticalFlick flickDeceleration: 8000 boundsMovement: Flickable.StopAtBounds // pixelAligned: true // Causes false-negatives in atYEnd cacheBuffer: 200 clip: true ScrollBar.vertical: ScrollBar { policy: settings.use_shuttle_dial ? ScrollBar.AlwaysOff : ScrollBar.AsNeeded interactive: true active: true // background: Item { /* TODO: timeline map */ } } section.property: "section" readonly property int bottommostVisibleIndex: count > 0 ? atYEnd ? 0 : indexAt(contentX, contentY + height - 1) : -1 readonly property bool noNeedMoreContent: !room || room.eventsHistoryJob || room.allHistoryLoaded /// The number of events per height unit - always positive readonly property real eventDensity: contentHeight > 0 && count > 0 ? count / contentHeight : 0.03 // 0.03 is just an arbitrary reasonable number property int lastRequestedEvents: 0 readonly property int currentRequestedEvents: room && room.eventsHistoryJob ? lastRequestedEvents : 0 property var textEditWithSelection property real readMarkerContentPos: originY readonly property real readMarkerViewportPos: readMarkerContentPos < contentY ? 0 : readMarkerContentPos > contentY + height ? height + readMarkerLine.height : readMarkerContentPos - contentY function parkReadMarker() { readMarkerContentPos = Qt.binding(function() { return !messageModel || messageModel.readMarkerVisualIndex > indexAt(contentX, contentY) ? originY : contentY + contentHeight }) } function ensurePreviousContent() { if (noNeedMoreContent) return // Take the current speed, or assume we can scroll 8 screens/s var velocity = moving ? -verticalVelocity : cruisingAnimation.running ? cruisingAnimation.velocity : chatView.height * 8 // Check if we're about to bump into the ceiling in // 2 seconds and if yes, request the amount of messages // enough to scroll at this rate for 3 more seconds if (velocity > 0 && contentY - velocity*2 < originY) { lastRequestedEvents = velocity * eventDensity * 3 room.getPreviousContent(lastRequestedEvents) } } onContentYChanged: ensurePreviousContent() onContentHeightChanged: ensurePreviousContent() function saveViewport(force) { if (room) room.saveViewport(indexAt(contentX, contentY), bottommostVisibleIndex, force) } function beforeModelReset() { parkReadMarker() console.log("Read marker parked at index", messageModel.readMarkerVisualIndex) saveViewport(true) } // Qt is not fabulous at positioning the list view when the delegate // sizes vary too much; this function runs scrollDelay timer to adjust // the position as needed shortly after the list was positioned. // Nothing good in that, just a workaround. function scrollViewTo(targetIndex, positionMode, saveViewportAfter) { console.log("Scrolling to position", targetIndex) positionViewAtIndex(targetIndex, positionMode) scrollDelay.targetIndex = targetIndex scrollDelay.positionMode = positionMode scrollDelay.saveViewport = saveViewportAfter scrollDelay.round = 1 scrollDelay.start() } /** @return true if no further action is needed; * false if scrollDelay has to be restarted. */ function fixupPosition(newIndex, newPos, positionMode) { if (newIndex === 0) { if (bottommostVisibleIndex === 0) return true // Positioning is correct // This normally shouldn't happen even with the current // imperfect positioning code in Qt console.warn("Fixing up the viewport to be at sync edge") positionViewAtBeginning() } else { // The viewport is divided into thirds; ListView.End should // place newIndex at the top third, Center corresponds // to the middle third; Beginning is not used for now. var nameForLog, topContentY, bottomContentY switch (positionMode) { case ListView.Contain: nameForLog = "fully visible" topContentY = contentY bottomContentY = contentY + height if (newPos) newPos = Math.max(newPos, Math.min(contentY, newPos + height)) break case ListView.Center: nameForLog = "in the centre" topContentY = contentY + height / 3 bottomContentY = contentY + 2 * height / 3 if (newPos) newPos -= height / 2 break case ListView.End: nameForLog = "at the top" topContentY = contentY bottomContentY = contentY + height / 3 break default: console.warn("fixupPosition: Unsupported positioning mode:", positionMode) return true // Refuse to do anything with it } var topShownIndex = indexAt(contentX, topContentY) var bottomShownIndex = indexAt(contentX, bottomContentY) if (bottomShownIndex !== -1 && newIndex <= topShownIndex && newIndex >= bottomShownIndex) return true // The item is within the expected range console.log("Fixing up item", newIndex, "to be", nameForLog, "- round", scrollDelay.round, "(" + topShownIndex + "-" + bottomShownIndex, "range is shown now)") // If the target item moved away too far and got destroyed, // repeat positioning; otherwise, position the canvas exactly // where it should be if (newPos) contentY = newPos else positionViewAtIndex(newIndex, positionMode) } return false } Timer { id: scrollDelay interval: 120 // small enough to avoid visual stutter onTriggered: { if (chatView.count === 0 || !targetPos) return if (chatView.fixupPosition(targetIndex, targetPos, positionMode) || ++round > 3) // Give up after 3 rounds { targetPos = undefined if (saveViewport) chatView.saveViewport(true) } else // Positioning is still in flux, might need another round start() } property int targetIndex: -1 property var targetPos property int positionMode: ListView.End property bool saveViewport: false property int round: 0 } function onModelReset() { if (room) { // Load events if there are not enough of them ensurePreviousContent() scrollViewTo(room.savedTopVisibleIndex(), ListView.End, false) } } function scrollUp(dy) { if (contentHeight > height) contentY -= dy } function scrollDown(dy) { if (contentHeight > height) contentY += dy } function onWheel(wheel) { if (wheel.angleDelta.x === 0) { var yDelta = wheel.angleDelta.y * 10 / 36 if (yDelta > 0) scrollUp(yDelta) else scrollDown(-yDelta) wheel.accepted = true } else { wheel.accepted = false } } Connections { target: controller onPageUpPressed: chatView.scrollUp(chatView.height - sectionBanner.childrenRect.height) onPageDownPressed: chatView.scrollDown(chatView.height - sectionBanner.childrenRect.height) onViewPositionRequested: chatView.scrollViewTo(index, ListView.Contain, true) } Component.onCompleted: { console.log("QML view loaded") model.modelAboutToBeReset.connect(beforeModelReset) model.modelReset.connect(onModelReset) } onMovementEnded: saveViewport(false) populate: AnimatedTransition { FastNumberAnimation { property: "opacity"; from: 0; to: 1 } } add: AnimatedTransition { FastNumberAnimation { property: "opacity"; from: 0; to: 1 } } move: AnimatedTransition { FastNumberAnimation { property: "y"; } FastNumberAnimation { property: "opacity"; to: 1 } } displaced: AnimatedTransition { FastNumberAnimation { property: "y"; easing.type: Easing.OutQuad } FastNumberAnimation { property: "opacity"; to: 1 } } Behavior on contentY { enabled: settings.enable_animations && !chatView.moving && !cruisingAnimation.running SmoothedAnimation { id: scrollAnimation duration: settings.fast_animations_duration_ms / 3 maximumEasingTime: settings.fast_animations_duration_ms onRunningChanged: { if (!running) chatView.saveViewport(false) } }} AnimationBehavior on readMarkerContentPos { NormalNumberAnimation { easing.type: Easing.OutQuad } } // This covers the area above the items if there are not enough // of them to fill the viewport MouseArea { z: -1 anchors.fill: parent acceptedButtons: Qt.AllButtons onReleased: controller.focusInput() } Rectangle { id: readShade visible: chatView.count > 0 anchors.top: parent.top anchors.topMargin: chatView.originY > chatView.contentY ? chatView.originY - chatView.contentY : 0 /// At the bottom of the read shade is the read marker. If /// the last read item is on the screen, the read marker is at /// the item's bottom; otherwise, it's just beyond the edge of /// chatView in the direction of the read marker index (or the /// timeline, if the timeline is short enough). /// @sa readMarkerViewportPos height: chatView.readMarkerViewportPos - anchors.topMargin anchors.left: parent.left width: readMarkerLine.width z: -1 opacity: 0.1 radius: readMarkerLine.height color: mixColors(disabledPalette.base, defaultPalette.highlight, 0.5) } Rectangle { id: readMarkerLine visible: chatView.count > 0 width: parent.width - scrollerArea.width anchors.bottom: readShade.bottom height: 4 z: 2.5 // On top of any ListView content, below the banner gradient: Gradient { GradientStop { position: 0; color: "transparent" } GradientStop { position: 1; color: defaultPalette.highlight } } } // itemAt is a function rather than a property, so it doesn't // produce a QML binding; the piece with contentHeight compensates. readonly property var underlayingItem: contentHeight >= height ? itemAt(contentX, contentY + sectionBanner.height - 2) : undefined readonly property bool sectionBannerVisible: !!underlayingItem && (!underlayingItem.sectionVisible || underlayingItem.y < contentY) Rectangle { id: sectionBanner z: 3 // On top of ListView sections that have z=2 anchors.left: parent.left anchors.top: parent.top width: childrenRect.width + 2 height: childrenRect.height + 2 visible: chatView.sectionBannerVisible color: defaultPalette.window opacity: 0.8 Label { font.bold: true font.family: settings.font.family font.pointSize: settings.font.pointSize opacity: 0.8 renderType: settings.render_type text: chatView.underlayingItem ? chatView.underlayingItem.ListView.section : "" } } } // === Timeline map === // Only used with the shuttle scroller for now Rectangle { id: cachedEventsBar // A proxy property for animation property int requestedHistoryEventsCount: chatView.currentRequestedEvents AnimationBehavior on requestedHistoryEventsCount { NormalNumberAnimation { } } property real averageEvtHeight: chatView.count + requestedHistoryEventsCount > 0 ? chatView.height / (chatView.count + requestedHistoryEventsCount) : 0 AnimationBehavior on averageEvtHeight { FastNumberAnimation { } } anchors.horizontalCenter: shuttleDial.horizontalCenter anchors.bottom: chatView.bottom anchors.bottomMargin: averageEvtHeight * chatView.bottommostVisibleIndex width: shuttleDial.backgroundWidth / 2 height: chatView.bottommostVisibleIndex < 0 ? 0 : averageEvtHeight * (chatView.count - chatView.bottommostVisibleIndex) visible: shuttleDial.visible color: defaultPalette.mid } Rectangle { // Loading history events bar, stacked above // the cached events bar when more history has been requested anchors.right: cachedEventsBar.right anchors.top: chatView.top anchors.bottom: cachedEventsBar.top width: cachedEventsBar.width visible: shuttleDial.visible opacity: 0.4 color: defaultPalette.mid } // === Scrolling extensions === Slider { id: shuttleDial orientation: Qt.Vertical height: chatView.height * 0.7 width: chatView.ScrollBar.vertical.width padding: 2 anchors.right: parent.right anchors.rightMargin: (background.width - width) / 2 anchors.verticalCenter: chatView.verticalCenter enabled: settings.use_shuttle_dial visible: enabled && chatView.count > 0 readonly property real backgroundWidth: handle.width + leftPadding + rightPadding // Npages/sec = value^2 => maxNpages/sec = 9 readonly property real maxValue: 3.0 readonly property real deviation: value / (maxValue * 2) * availableHeight background: Item { x: shuttleDial.handle.x - shuttleDial.leftPadding width: shuttleDial.backgroundWidth Rectangle { id: springLine // Rectangles (normally) have (x,y) as their top-left corner. // To draw the "spring" line up from the middle point, its `y` // should still be the top edge, not the middle point. y: shuttleDial.height / 2 - Math.max(shuttleDial.deviation, 0) height: Math.abs(shuttleDial.deviation) anchors.horizontalCenter: parent.horizontalCenter width: 2 color: defaultPalette.highlight } } opacity: scrollerArea.containsMouse ? 1 : 0.7 AnimationBehavior on opacity { FastNumberAnimation { } } from: -maxValue to: maxValue activeFocusOnTab: false onPressedChanged: { if (!pressed) { value = 0 controller.focusInput() } } // This is not an ordinary animation, it's the engine that makes // the shuttle dial work; for that reason it's not governed by // settings.enable_animations and only can be disabled together with // the shuttle dial. SmoothedAnimation { id: cruisingAnimation target: chatView property: "contentY" velocity: shuttleDial.value * shuttleDial.value * chatView.height maximumEasingTime: settings.animations_duration_ms to: chatView.originY + (shuttleDial.value > 0 ? 0 : chatView.contentHeight - chatView.height) running: shuttleDial.value != 0 onStopped: chatView.saveViewport(false) } // Animations don't update `to` value when they are running; so // when the shuttle value changes sign without becoming zero (which, // turns out, is quite usual when dragging the shuttle around) the // animation has to be restarted. onValueChanged: cruisingAnimation.restart() Component.onCompleted: { // same reason as above chatView.originYChanged.connect(cruisingAnimation.restart) chatView.contentHeightChanged.connect(cruisingAnimation.restart) } } MouseArea { id: scrollerArea anchors.top: chatView.top anchors.bottom: chatView.bottom anchors.right: parent.right width: settings.use_shuttle_dial ? shuttleDial.backgroundWidth : chatView.ScrollBar.vertical.width acceptedButtons: Qt.NoButton hoverEnabled: true } Rectangle { id: timelineStats anchors.right: scrollerArea.left anchors.top: chatView.top width: childrenRect.width + 3 height: childrenRect.height + 3 color: defaultPalette.alternateBase property bool shown: (chatView.bottommostVisibleIndex >= 0 && (scrollerArea.containsMouse || scrollAnimation.running)) || chatView.currentRequestedEvents > 0 onShownChanged: { if (shown) { fadeOutDelay.stop() opacity = 0.8 } else fadeOutDelay.restart() } Timer { id: fadeOutDelay interval: 2000 onTriggered: parent.opacity = 0 } AnimationBehavior on opacity { FastNumberAnimation { } } Label { font.bold: true font.family: settings.font.family font.pointSize: settings.font.pointSize opacity: 0.8 renderType: settings.render_type text: (chatView.count > 0 ? (chatView.bottommostVisibleIndex === 0 ? qsTr("Latest events") : qsTr("%Ln events back from now","", chatView.bottommostVisibleIndex)) + "\n" + qsTr("%Ln events cached", "", chatView.count) : "") + (chatView.currentRequestedEvents > 0 ? (chatView.count > 0 ? "\n" : "") + qsTr("%Ln events requested from the server", "", chatView.currentRequestedEvents) : "") horizontalAlignment: Label.AlignRight } } ScrollToButton { id: scrollToBottomButton anchors.right: scrollerArea.left anchors.rightMargin: 2 anchors.bottom: parent.bottom anchors.bottomMargin: visible ? 0.5 * height : -height visible: !chatView.atYEnd icon { name: "go-bottom" source: "qrc:///scrolldown.svg" } onClicked: { chatView.positionViewAtBeginning() chatView.saveViewport(true) } } ScrollToButton { id: scrollToReaderMarkerButton anchors.right: scrollerArea.left anchors.rightMargin: 2 anchors.bottom: scrollToBottomButton.top anchors.bottomMargin: visible ? 0.5 * height : -3 * height visible: chatView.count > 1 && messageModel.readMarkerVisualIndex > 0 && messageModel.readMarkerVisualIndex > chatView.indexAt(chatView.contentX, chatView.contentY) icon { name: "go-top" source: "qrc:///scrollup.svg" } onClicked: { if (messageModel.readMarkerVisualIndex < chatView.count) chatView.scrollViewTo(messageModel.readMarkerVisualIndex, ListView.Center, true) else room.getPreviousContent(chatView.count / 2) // FIXME, #799 } } } Quaternion-0.0.95.1/client/qml/TimelineItem.qml000066400000000000000000000710501412757327200212550ustar00rootroot00000000000000import QtQuick 2.6 import QtQuick.Controls 2.2 import QtGraphicalEffects 1.0 // For fancy highlighting import Quotient 1.0 Item { // Supplementary components TimelineSettings { id: settings readonly property bool autoload_images: value("UI/autoload_images", true) readonly property string highlight_mode: value("UI/highlight_mode", "background") readonly property color highlight_color: value("UI/highlight_color", "orange") readonly property color outgoing_color_base: value("UI/outgoing_color", "#4A8780") readonly property color outgoing_color: mixColors(defaultPalette.text, settings.outgoing_color_base, 0.5) readonly property bool show_author_avatars: value("UI/show_author_avatars", timeline_style != "xchat") } SystemPalette { id: defaultPalette; colorGroup: SystemPalette.Active } SystemPalette { id: disabledPalette; colorGroup: SystemPalette.Disabled } // Property interface property var view /** Determines whether the view is moving at the moment */ property bool moving: view.moving // TimelineItem definition visible: marks !== EventStatus.Hidden enabled: visible height: childrenRect.height * visible readonly property bool sectionVisible: section !== aboveSection readonly property bool authorSectionVisible: sectionVisible || author !== aboveAuthor readonly property bool replaced: marks === EventStatus.Replaced readonly property bool pending: [ EventStatus.Submitted, EventStatus.Departed, EventStatus.ReachedServer, EventStatus.SendingFailed ].indexOf(marks) != -1 readonly property bool failed: marks === EventStatus.SendingFailed readonly property bool eventWithTextPart: ["message", "emote", "image", "file"].indexOf(eventType) >= 0 /* readonly but animated */ property string textColor: marks === EventStatus.Submitted || failed ? disabledPalette.text : marks === EventStatus.Departed ? mixColors(disabledPalette.text, defaultPalette.text, 0.5) : marks === EventStatus.Redacted ? disabledPalette.text : (eventWithTextPart && room && author === room.localUser) ? settings.outgoing_color : highlight && settings.highlight_mode == "text" ? settings.highlight_color : (["state", "notice", "other"].indexOf(eventType) >= 0) ? mixColors(disabledPalette.text, defaultPalette.text, 0.5) : defaultPalette.text readonly property string authorName: room && author ? room.safeMemberName(author.id) : "" // FIXME: boilerplate with models/userlistmodel.cpp:115 readonly property string authorColor: Qt.hsla(author ? author.hueF : 0.0, (1-defaultPalette.window.hslSaturation), /* contrast but not too heavy: */ (-0.7*defaultPalette.window.hslLightness + 0.9), defaultPalette.buttonText.a) readonly property bool xchatStyle: settings.timeline_style === "xchat" readonly property bool actionEvent: eventType == "state" || eventType == "emote" readonly property bool readMarkerHere: messageModel.readMarkerVisualIndex === index /// The bottom event edge is below the top viewport edge and /// the top event edge is above the bottom viewport edge readonly property bool partiallyShown: room && room.displayed && y + height - 1 > view.contentY && y < view.contentY + view.height /// The bottom event edge is below the top and above the bottom /// viewport edge; partiallyShown => bottomEdgeShown but not vice versa readonly property bool bottomEdgeShown: room && room.displayed && y + height - 1 > view.contentY && y + height - 1 < view.contentY + view.height onBottomEdgeShownChanged: { // A message is considered as "read" if its bottom spent long enough // within the viewing area of the timeline if (!pending) controller.onMessageShownChanged(index, bottomEdgeShown, readMarkerHere) } onPendingChanged: bottomEdgeShownChanged() onReadMarkerHereChanged: { if (readMarkerHere) { if (partiallyShown) { chatView.readMarkerContentPos = Qt.binding(function() { return y + height }) console.log("Read marker line bound at index", index) } else { chatView.parkReadMarker() console.log("Read marker parked at index", index + ", content pos", chatView.readMarkerContentPos, "(full range is", chatView.originY, "-", chatView.originY + chatView.contentHeight, "as of now)") } } } onPartiallyShownChanged: readMarkerHereChanged() function maybeBindScrollTarget() { if (scrollDelay.targetIndex === index) { scrollDelay.targetPos = Qt.binding(function() { return y }) console.log("Scroll target bound, current pos:", scrollDelay.targetPos) } } Component.onCompleted: { if (bottomEdgeShown) bottomEdgeShownChanged(true) readMarkerHereChanged() maybeBindScrollTarget() } Connections { target: scrollDelay onTargetIndexChanged: maybeBindScrollTarget() } AnimationBehavior on textColor { ColorAnimation { duration: settings.animations_duration_ms } } property bool showingDetails Connections { target: controller onShowDetails: { if (currentIndex === index) { showingDetails = !showingDetails if (!settings.enable_animations) { detailsAreaLoader.visible = showingDetails detailsAreaLoader.opacity = showingDetails } else detailsAnimation.start() } } onAnimateMessage: { if (currentIndex === index) blinkAnimation.start() } } SequentialAnimation { id: detailsAnimation PropertyAction { target: detailsAreaLoader; property: "visible" value: true } FastNumberAnimation { target: detailsAreaLoader; property: "opacity" to: showingDetails easing.type: Easing.OutQuad } PropertyAction { target: detailsAreaLoader; property: "visible" value: showingDetails } } SequentialAnimation { id: blinkAnimation loops: 3 PropertyAction { target: messageFlasher; property: "visible" value: true } PauseAnimation { // `settings.animations_duration_ms` intentionally is not in use here // because this is not just an eye candy animation - the user will lose // functionality if this animation stops working. duration: 200 } PropertyAction { target: messageFlasher; property: "visible" value: false } PauseAnimation { duration: 200 } } TimelineMouseArea { anchors.fill: fullMessage acceptedButtons: Qt.AllButtons } Column { id: fullMessage width: parent.width Rectangle { width: parent.width height: childrenRect.height + 2 visible: sectionVisible color: defaultPalette.window Label { font.family: settings.font.family font.pointSize: settings.font.pointSize font.bold: true renderType: settings.render_type text: section } } Loader { id: detailsAreaLoader // asynchronous: true // https://bugreports.qt.io/browse/QTBUG-50992 active: visible visible: false // Controlled by showDetailsButton opacity: 0 width: parent.width sourceComponent: detailsArea } Item { id: message width: parent.width height: childrenRect.height // There are several layout styles (av - author avatar, // al - author label, ts - timestamp, c - content // default (when "timeline_style" is not "xchat"): // av al // c ts // action events (for state and emote events): // av (al+c in a single control) ts // (spanning both rows ) // xchat (when "timeline_style" is "xchat"): // ts av al c // xchat action events // ts av *(asterisk) al c // // For any layout, authorAvatar.top is the vertical anchor // (can't use parent.top because of using childrenRect.height) Label { id: timelabel visible: xchatStyle width: if (!visible) { 0 } anchors.top: authorAvatar.top anchors.left: parent.left opacity: 0.8 renderType: settings.render_type font.family: settings.font.family font.pointSize: settings.font.pointSize font.italic: pending text: "<" + time.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) + ">" } Image { id: authorAvatar visible: (authorSectionVisible || xchatStyle) && settings.show_author_avatars && author.avatarMediaId anchors.left: timelabel.right anchors.leftMargin: 3 height: visible ? settings.lineSpacing * (2 - xchatStyle) : authorLabel.height // 2 text line heights by default; 1 line height for XChat width: settings.show_author_avatars * settings.lineSpacing * (2 - xchatStyle) fillMode: Image.PreserveAspectFit horizontalAlignment: Image.AlignRight source: author.avatarMediaId ? "image://mtx/" + author.avatarMediaId : "" sourceSize: Qt.size(width, -1) AuthorInteractionArea { authorId: author.id } AnimationBehavior on height { FastNumberAnimation { } } } Label { id: authorLabel visible: xchatStyle || (!actionEvent && authorSectionVisible) anchors.left: authorAvatar.right anchors.leftMargin: 2 anchors.top: authorAvatar.top width: xchatStyle ? 120 - authorAvatar.width : Math.min(textField.width, implicitWidth) horizontalAlignment: actionEvent ? Text.AlignRight : Text.AlignLeft elide: Text.ElideRight color: authorColor textFormat: Label.PlainText font.family: settings.font.family font.pointSize: settings.font.pointSize font.bold: !xchatStyle renderType: settings.render_type text: (actionEvent ? "* " : "") + authorName AuthorInteractionArea { authorId: author.id } } Item { id: textField height: textFieldImpl.height anchors.top: !xchatStyle && authorLabel.visible ? authorLabel.bottom : height >= authorAvatar.height ? authorLabel.top : undefined anchors.verticalCenter: !xchatStyle && !authorLabel.visible && height < authorAvatar.height ? authorAvatar.verticalCenter : undefined anchors.left: (xchatStyle ? authorLabel : authorAvatar).right anchors.leftMargin: 2 anchors.right: parent.right anchors.rightMargin: 1 RectangularGlow { id: highlighter anchors.fill: parent anchors.margins: glowRadius / 2 visible: highlight && settings.highlight_mode != "text" glowRadius: 5 cornerRadius: glowRadius spread: 1 / glowRadius color: settings.highlight_color opacity: 0.3 cached: true } Rectangle { id: messageFlasher visible: false anchors.fill: parent opacity: 0.5 color: settings.highlight_color radius: 2 } TextEdit { id: textFieldImpl anchors.top: textField.top width: parent.width leftPadding: 2 rightPadding: 2 x: -textScrollBar.position * contentWidth // Doesn't work for attributes function toHtmlEscaped(txt) { // Make sure to replace & first return txt.replace(/&/g, '&') .replace(//g, '>') } selectByMouse: true readOnly: true textFormat: TextEdit.RichText // FIXME: The text is clumsy and slows down creation text: (!xchatStyle ? ("
" + (time ? toHtmlEscaped(time.toLocaleTimeString( Qt.locale(), Locale.ShortFormat)) : "") + "
" + (actionEvent ? ("
" + toHtmlEscaped(authorName) + " ") : "")) : "") + display + (replaced ? "" + " (" + qsTr("edited") + ")" : "") horizontalAlignment: Text.AlignLeft wrapMode: Text.Wrap color: textColor font: settings.font renderType: settings.render_type // TODO: In the code below, links should be resolved // with Qt.resolvedLink, once we figure out what // to do with relative URLs (note: www.google.com // is a relative URL, https://www.google.com is not). // Instead of Qt.resolvedUrl (and, most likely, // QQmlAbstractUrlInterceptor to convert URLs) // we might just prefer to do the whole resolving // in C++. onHoveredLinkChanged: controller.showStatusMessage(hoveredLink) onLinkActivated: controller.resourceRequested(link) TimelineTextEditSelector {} } TimelineMouseArea { anchors.fill: parent cursorShape: textFieldImpl.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor acceptedButtons: Qt.MiddleButton | Qt.RightButton onClicked: { if (mouse.button === Qt.MiddleButton) { if (textFieldImpl.hoveredLink) controller.resourceRequested( textFieldImpl.hoveredLink, "_interactive") } else if (mouse.button === Qt.RightButton) { controller.showMenu(index, textFieldImpl.hoveredLink, textFieldImpl.selectedText, showingDetails) } } onWheel: { if (wheel.angleDelta.x != 0 && textFieldImpl.width < textFieldImpl.contentWidth) { if (wheel.pixelDelta.x != 0) textScrollBar.position -= wheel.pixelDelta.x / width else textScrollBar.position -= wheel.angleDelta.x / 6 / width textScrollBar.position = Math.min(1, Math.max(0, textScrollBar.position)) } else wheel.accepted = false } } ScrollBar { id: textScrollBar hoverEnabled: true visible: textFieldImpl.contentWidth > textFieldImpl.width active: visible orientation: Qt.Horizontal size: textFieldImpl.width / textFieldImpl.contentWidth anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom } } Loader { id: imageLoader active: eventType == "image" anchors.top: textField.bottom anchors.left: textField.left anchors.right: textField.right sourceComponent: ImageContent { property var info: !progressInfo.isUpload && !progressInfo.active && content.info && content.info.thumbnail_info ? content.info.thumbnail_info : content.info sourceSize: if (info) { Qt.size(info.w, info.h) } source: downloaded || progressInfo.isUpload ? progressInfo.localPath : progressInfo.failed ? "" : content.info && content.info.thumbnail_info && !autoload ? "image://mtx/" + content.thumbnailMediaId : "" maxHeight: chatView.height - textField.height - authorLabel.height * !xchatStyle autoload: settings.autoload_images } } Loader { id: fileLoader active: eventType == "file" anchors.top: textField.bottom anchors.left: textField.left anchors.right: textField.right height: childrenRect.height sourceComponent: FileContent { } } Label { id: annotationLabel anchors.top: imageLoader.active ? imageLoader.bottom : fileLoader.bottom anchors.left: textField.left anchors.right: textField.right height: annotation ? implicitHeight : 0 visible: annotation font.family: settings.font.family font.pointSize: settings.font.pointSize font.italic: true leftPadding: 2 rightPadding: 2 text: annotation } Flow { anchors.top: annotationLabel.bottom anchors.left: textField.left anchors.right: textField.right Repeater { model: reactions ToolButton { id: reactionButton topPadding: 2 bottomPadding: 2 contentItem: Text { text: modelData.key + " \u00d7" /* Math "multiply" */ + modelData.authorsCount textFormat: Text.PlainText font.family: settings.font.family font.pointSize: settings.font.pointSize color: modelData.includesLocalUser ? defaultPalette.highlight : defaultPalette.buttonText } background: Rectangle { radius: 4 color: reactionButton.down ? defaultPalette.button : "transparent" border.color: modelData.includesLocalUser ? defaultPalette.highlight : disabledPalette.buttonText border.width: 1 } hoverEnabled: true MyToolTip { visible: hovered contentItem: Text { //: %2 is the list of users text: qsTr("Reaction '%1' from %2") .arg(modelData.key).arg(modelData.authors) textFormat: Text.PlainText } } onClicked: controller.reactionButtonClicked( eventId, modelData.key) } } } Loader { id: buttonAreaLoader active: failed || // resendButton (pending && marks !== EventStatus.ReachedServer && marks !== EventStatus.Departed) || // discardButton (!pending && eventResolvedType == "m.room.create" && refId) || // goToPredecessorButton (!pending && eventResolvedType == "m.room.tombstone") // goToSuccessorButton anchors.top: textField.top anchors.right: parent.right height: textField.height sourceComponent: buttonArea } } } // Components loaded on demand Component { id: buttonArea Item { TimelineItemToolButton { id: resendButton visible: failed anchors.right: discardButton.left text: qsTr("Resend") onClicked: room.retryMessage(eventId) } TimelineItemToolButton { id: discardButton visible: pending && marks !== EventStatus.ReachedServer && marks !== EventStatus.Departed anchors.right: parent.right text: qsTr("Discard") onClicked: room.discardMessage(eventId) } TimelineItemToolButton { id: goToPredecessorButton visible: !pending && eventResolvedType == "m.room.create" && refId anchors.right: parent.right text: qsTr("Go to\nolder room") // TODO: Treat unjoined invite-only rooms specially onClicked: controller.resourceRequested(refId, "join") } TimelineItemToolButton { id: goToSuccessorButton visible: !pending && eventResolvedType == "m.room.tombstone" anchors.right: parent.right text: qsTr("Go to\nnew room") // TODO: Treat unjoined invite-only rooms specially onClicked: controller.resourceRequested(refId, "join") } } } Component { id: detailsArea Rectangle { height: childrenRect.height radius: 5 color: defaultPalette.button border.color: defaultPalette.mid readonly property url evtLink: "https://matrix.to/#/" + room.id + "/" + eventId readonly property string sourceText: toolTip Item { id: detailsHeader width: parent.width height: childrenRect.height TextEdit { text: "<" + time.toLocaleString(Qt.locale(), Locale.ShortFormat) + ">" font.bold: true font.family: settings.font.family font.pointSize: settings.font.pointSize renderType: settings.render_type readOnly: true selectByKeyboard: true; selectByMouse: true anchors.top: eventTitle.bottom anchors.left: parent.left anchors.leftMargin: 3 z: 1 } TextEdit { id: eventTitle text: ""+ eventId + "" textFormat: Text.RichText font.bold: true font.family: settings.font.family font.pointSize: settings.font.pointSize renderType: settings.render_type horizontalAlignment: Text.AlignHCenter readOnly: true selectByKeyboard: true; selectByMouse: true width: parent.width onLinkActivated: Qt.openUrlExternally(link) MouseArea { anchors.fill: parent cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor acceptedButtons: Qt.NoButton } } TextEdit { text: eventResolvedType textFormat: Text.PlainText font.bold: true font.family: settings.font.family font.pointSize: settings.font.pointSize renderType: settings.render_type anchors.top: eventTitle.bottom anchors.right: parent.right anchors.rightMargin: 3 } TextEdit { id: permalink text: evtLink font: settings.font renderType: settings.render_type width: 0; height: 0; visible: false } } ScrollView { anchors.top: detailsHeader.bottom width: parent.width height: Math.min(implicitContentHeight, chatView.height / 2) clip: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOn ScrollBar.vertical.policy: ScrollBar.AlwaysOn TextEdit { text: sourceText textFormat: Text.PlainText readOnly: true; font.family: "Monospace" font.pointSize: settings.font.pointSize renderType: settings.render_type selectByKeyboard: true; selectByMouse: true } } } } } Quaternion-0.0.95.1/client/qml/TimelineItemToolButton.qml000066400000000000000000000011251412757327200233030ustar00rootroot00000000000000import QtQuick 2.6 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 ToolButton { width: visible * implicitWidth height: visible * parent.height anchors.top: parent.top anchors.rightMargin: 2 style: ButtonStyle { label: Text { text: control.text font: settings.font fontSizeMode: Text.VerticalFit minimumPointSize: settings.font.pointSize - 3 verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter renderType: settings.render_type } } } Quaternion-0.0.95.1/client/qml/TimelineMouseArea.qml000066400000000000000000000001571412757327200222400ustar00rootroot00000000000000import QtQuick 2.2 MouseArea { onWheel: chatView.onWheel(wheel) onReleased: controller.focusInput() } Quaternion-0.0.95.1/client/qml/TimelineSettings.qml000066400000000000000000000030611412757327200221540ustar00rootroot00000000000000import QtQuick 2.4 import Quotient 1.0 Settings { readonly property int animations_duration_ms_impl: value("UI/animations_duration_ms", 400) readonly property bool enable_animations: animations_duration_ms_impl > 0 readonly property int animations_duration_ms: animations_duration_ms_impl == 0 ? 10 : animations_duration_ms_impl readonly property int fast_animations_duration_ms: animations_duration_ms / 2 readonly property string timeline_style: value("UI/timeline_style", "") readonly property string font_family_impl: value("UI/Fonts/timeline_family", "") readonly property real font_pointSize_impl: parseFloat(value("UI/Fonts/timeline_pointSize", "")) readonly property var defaultMetrics: FontMetrics { } readonly property var fontInfo: FontMetrics { font.family: font_family_impl ? font_family_impl : defaultMetrics.font.family font.pointSize: font_pointSize_impl > 0 ? font_pointSize_impl : defaultMetrics.font.pointSize } readonly property var font: fontInfo.font readonly property real fontHeight: fontInfo.height readonly property real lineSpacing: fontInfo.lineSpacing readonly property var render_type_impl: value("UI/Fonts/render_type", "NativeRendering") readonly property int render_type: ["NativeRendering", "Native", "native"].indexOf(render_type_impl) != -1 ? Text.NativeRendering : Text.QtRendering } Quaternion-0.0.95.1/client/qml/TimelineTextEditSelector.qml000066400000000000000000000033531412757327200236130ustar00rootroot00000000000000import QtQuick 2.2 /* * Unfortunately, TextEdit captures LeftButton events for text selection in a way which * is not compatible with our focus-cancelling mechanism, so we took over the task here. */ MouseArea { property var textEdit: parent property var selectionMode: TextEdit.SelectCharacters anchors.fill: parent acceptedButtons: Qt.LeftButton onPressed: { var x = mouse.x var y = mouse.y if (textEdit.flickableItem) { x += textEdit.flickableItem.contentX y += textEdit.flickableItem.contentY } var hasSelection = textEdit.selectionEnd > textEdit.selectionStart if (hasSelection && controller.getModifierKeys() & Qt.ShiftModifier) { textEdit.moveCursorSelection(textEdit.positionAt(x, y), selectionMode) } else { textEdit.cursorPosition = textEdit.positionAt(x, y) if (chatView.textEditWithSelection) chatView.textEditWithSelection.deselect() } } onClicked: { if (textEdit.hoveredLink) textEdit.onLinkActivated(textEdit.hoveredLink) } onDoubleClicked: { selectionMode = TextEdit.SelectWords textEdit.selectWord() } onReleased: { selectionMode = TextEdit.SelectCharacters controller.setGlobalSelectionBuffer(textEdit.selectedText) chatView.textEditWithSelection = textEdit controller.focusInput() } onPositionChanged: { var x = mouse.x var y = mouse.y if (textEdit.flickableItem) { x += textEdit.flickableItem.contentX y += textEdit.flickableItem.contentY } textEdit.moveCursorSelection(textEdit.positionAt(x, y), selectionMode) } } Quaternion-0.0.95.1/client/qml/ToolTipArea.qml000066400000000000000000000003651412757327200210540ustar00rootroot00000000000000import QtQuick 2.0 MouseArea { property alias tooltip: tooltip property alias text: tooltip.text anchors.fill: parent acceptedButtons: Qt.NoButton hoverEnabled: true MyToolTip { id: tooltip; visible: containsMouse } } Quaternion-0.0.95.1/client/quaternionroom.cpp000066400000000000000000000127261412757327200211570ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2016 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #include "quaternionroom.h" #include #include #include using namespace Quotient; QuaternionRoom::QuaternionRoom(Connection* connection, QString roomId, JoinState joinState) : Room(connection, std::move(roomId), joinState) { connect(this, &Room::namesChanged, this, &QuaternionRoom::htmlSafeDisplayNameChanged); } const QString& QuaternionRoom::cachedUserFilter() const { return m_cachedUserFilter; } void QuaternionRoom::setCachedUserFilter(const QString& input) { m_cachedUserFilter = input; } bool QuaternionRoom::isEventHighlighted(const RoomEvent* e) const { return highlights.contains(e); } int QuaternionRoom::savedTopVisibleIndex() const { return firstDisplayedMarker() == historyEdge() ? 0 : firstDisplayedMarker() - messageEvents().rbegin(); } int QuaternionRoom::savedBottomVisibleIndex() const { return lastDisplayedMarker() == historyEdge() ? 0 : lastDisplayedMarker() - messageEvents().rbegin(); } void QuaternionRoom::saveViewport(int topIndex, int bottomIndex, bool force) { // Don't save more frequently than once a second static auto lastSaved = QDateTime::currentMSecsSinceEpoch(); const auto now = QDateTime::currentMSecsSinceEpoch(); if (!force && lastSaved >= now - 1000) return; lastSaved = now; if (topIndex == -1 || bottomIndex == -1 || (bottomIndex == savedBottomVisibleIndex() && (bottomIndex == 0 || topIndex == savedTopVisibleIndex()))) return; if (bottomIndex == 0) { qDebug() << "Saving viewport as the latest available"; setFirstDisplayedEventId({}); setLastDisplayedEventId({}); return; } qDebug() << "Saving viewport:" << topIndex << "thru" << bottomIndex; setFirstDisplayedEvent(maxTimelineIndex() - topIndex); setLastDisplayedEvent(maxTimelineIndex() - bottomIndex); } QString QuaternionRoom::htmlSafeDisplayName() const { return displayName().toHtmlEscaped(); } void QuaternionRoom::onAddNewTimelineEvents(timeline_iter_t from) { std::for_each(from, messageEvents().cend(), [this](const TimelineItem& ti) { checkForHighlights(ti); }); } void QuaternionRoom::onAddHistoricalTimelineEvents(rev_iter_t from) { std::for_each(from, messageEvents().crend(), [this](const TimelineItem& ti) { checkForHighlights(ti); }); } void QuaternionRoom::checkForHighlights(const Quotient::TimelineItem& ti) { const auto localUserId = localUser()->id(); if (ti->senderId() == localUserId) return; if (auto* e = ti.viewAs()) { constexpr auto ReOpt = QRegularExpression::MultilineOption | QRegularExpression::CaseInsensitiveOption; constexpr auto MatchOpt = QRegularExpression::PartialPreferFirstMatch; // Building a QRegularExpression is quite expensive and this function is called a lot // Given that the localUserId is usually the same we can reuse the QRegularExpression instead of building it every time static QHash localUserExpressions; static QHash roomMemberExpressions; if (!localUserExpressions.contains(localUserId)) { localUserExpressions[localUserId] = QRegularExpression("(\\W|^)" + localUserId + "(\\W|$)", ReOpt); } const auto memberName = roomMembername(localUserId); if (!roomMemberExpressions.contains(memberName)) { // FIXME: unravels if the room member name contains characters special // to regexp($, e.g.) roomMemberExpressions[memberName] = QRegularExpression("(\\W|^)" + roomMembername(localUserId) + "(\\W|$)", ReOpt); } const auto& text = e->plainBody(); const auto& localMatch = localUserExpressions[localUserId].match(text, 0, MatchOpt); const auto& roomMemberMatch = roomMemberExpressions[memberName].match(text, 0, MatchOpt); if (localMatch.hasMatch() || roomMemberMatch.hasMatch()) highlights.insert(e); } } Quaternion-0.0.95.1/client/quaternionroom.h000066400000000000000000000052561412757327200206240ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2016 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #pragma once #include class QuaternionRoom: public Quotient::Room { Q_OBJECT Q_PROPERTY(QString htmlSafeDisplayName READ htmlSafeDisplayName NOTIFY htmlSafeDisplayNameChanged) public: QuaternionRoom(Quotient::Connection* connection, QString roomId, Quotient::JoinState joinState); const QString& cachedUserFilter() const; void setCachedUserFilter(const QString& input); bool isEventHighlighted(const Quotient::RoomEvent* e) const; Q_INVOKABLE int savedTopVisibleIndex() const; Q_INVOKABLE int savedBottomVisibleIndex() const; Q_INVOKABLE void saveViewport(int topIndex, int bottomIndex, bool force = false); QString htmlSafeDisplayName() const; signals: // Gotta wrap the Room::namesChanged signal because it has parameters // and moc cannot use signals with parameters defined in the parent // class as NOTIFY targets void htmlSafeDisplayNameChanged(); private: QSet highlights; QString m_cachedUserFilter; void onAddNewTimelineEvents(timeline_iter_t from) override; void onAddHistoricalTimelineEvents(rev_iter_t from) override; void checkForHighlights(const Quotient::TimelineItem& ti); }; Quaternion-0.0.95.1/client/resources.qrc000066400000000000000000000025351412757327200201070ustar00rootroot00000000000000 qml/Timeline.qml qml/MyToolTip.qml qml/ToolTipArea.qml ../icons/quaternion/128-apps-quaternion.png ../icons/breeze/irc-channel-joined.svg ../icons/breeze/irc-channel-parted.svg ../icons/irc-channel-invited.svg ../icons/scrolldown.svg ../icons/scrollup.svg ../icons/busy_16x16.gif qml/Attachment.qml qml/ImageContent.qml qml/FileContent.qml qml/TimelineItem.qml qml/TimelineMouseArea.qml qml/TimelineTextEditSelector.qml qml/TimelineItemToolButton.qml qml/TimelineSettings.qml qml/NormalNumberAnimation.qml qml/FastNumberAnimation.qml qml/AnimationBehavior.qml qml/AnimatedTransition.qml qml/AuthorInteractionArea.qml qml/ScrollToButton.qml Quaternion-0.0.95.1/client/roomdialogs.cpp000066400000000000000000000456131412757327200204150ustar00rootroot00000000000000/****************************************************************************** * Copyright (C) 2017 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "roomdialogs.h" #include "mainwindow.h" #include "quaternionroom.h" #include "accountregistry.h" #include "accountselector.h" #include "models/orderbytag.h" // For tagToCaption() #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include RoomDialogBase::RoomDialogBase(const QString& title, const QString& applyButtonText, QuaternionRoom* r, QWidget* parent, QDialogButtonBox::StandardButtons extraButtons) : Dialog(title, parent, StatusLine, applyButtonText, extraButtons) , room(r), avatar(new QLabel) , roomName(new QLineEdit) , aliasServer(new QLabel), alias(new QLineEdit) , topic(new QPlainTextEdit) , publishRoom(new QCheckBox(tr("Publish room in room directory"))) , guestCanJoin(new QCheckBox(tr("Allow guest accounts to join the room"))) , mainFormLayout(addLayout()) { if (room) { avatar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); avatar->setPixmap({64, 64}); } topic->setTabChangesFocus(true); topic->setSizeAdjustPolicy( QAbstractScrollArea::AdjustToContentsOnFirstShow); // Layout controls { if (room) { auto* topLayout = new QHBoxLayout; topLayout->addWidget(avatar); { essentialsLayout = new QFormLayout; essentialsLayout->addRow(tr("Room name"), roomName); essentialsLayout->addRow(tr("Primary alias"), alias); topLayout->addLayout(essentialsLayout); } mainFormLayout->addRow(topLayout); } else { mainFormLayout->addRow(tr("Room name"), roomName); auto* aliasLayout = new QHBoxLayout; aliasLayout->addWidget(new QLabel("#")); aliasLayout->addWidget(alias); aliasLayout->addWidget(aliasServer); mainFormLayout->addRow(tr("Primary alias"), aliasLayout); } } mainFormLayout->addRow(tr("Topic"), topic); if (!room) // TODO: Support this in RoomSettingsDialog as well { mainFormLayout->addRow(publishRoom); // formLayout->addRow(guestCanJoin); // TODO: quotient-im/libQuotient#36 } } QComboBox* RoomDialogBase::addVersionSelector(QLayout* layout) { auto* versionSelector = new QComboBox; layout->addWidget(versionSelector); { auto* specLink = new QLabel("" + tr("About room versions") + ""); specLink->setOpenExternalLinks(true); layout->addWidget(specLink); } return versionSelector; } void RoomDialogBase::refillVersionSelector(QComboBox* selector, Connection* account) { selector->clear(); if (account->loadingCapabilities()) { selector->addItem( tr("(loading)", "Loading room versions from the server"), QString()); selector->setEnabled(false); // FIXME: It should be connectSingleShot // but sadly connectSingleShot doesn't work with lambdas yet connectUntil(account, &Connection::capabilitiesLoaded, this, [this,selector,account] { refillVersionSelector(selector, account); return true; }); return; } const auto& versions = account->availableRoomVersions(); for (const auto& v: versions) { const bool isDefault = v.id == account->defaultRoomVersion(); const auto postfix = isDefault ? tr("default", "Default room version") : v.isStable() ? tr("stable", "Stable room version") : v.status; selector->addItem(v.id % " (" % postfix % ")", v.id); const auto idx = selector->count() - 1; if (isDefault) { auto font = selector->itemData(idx, Qt::FontRole).value(); font.setBold(true); selector->setItemData(idx, font, Qt::FontRole); selector->setCurrentIndex(idx); } if (!v.isStable()) selector->setItemData(idx, QColor(Qt::red), Qt::ForegroundRole); } selector->setEnabled(true); } void RoomDialogBase::addEssentials(QWidget* accountControl, QLayout* versionBox) { Q_ASSERT(accountControl != nullptr && versionBox != nullptr); auto* layout = essentialsLayout ? essentialsLayout : mainFormLayout; layout->insertRow(0, tr("Account"), accountControl); layout->insertRow(1, tr("Room version"), versionBox); } bool RoomDialogBase::checkRoomVersion(QString version, Connection* account) { if (account->stableRoomVersions().contains(version)) return true; return QMessageBox::warning(this, tr("Continue with unstable version?"), tr("You are using an UNSTABLE room version (%1)." " The server may stop supporting it at any moment." " Do you still want to use this version?").arg(version), QMessageBox::Yes|QMessageBox::No, QMessageBox::No) == QMessageBox::Yes; } RoomSettingsDialog::RoomSettingsDialog(QuaternionRoom* room, MainWindow* parent) : RoomDialogBase(tr("Room settings: %1").arg(room->displayName()), tr("Update room"), room, parent) , account(new QLabel(room->connection()->userId())) , version(new QLabel(room->version())) , tagsList(new QListWidget) { auto* versionBox = new QGridLayout; versionBox->addWidget(version, 0, 0); if (room->isUnstable()) versionBox->addWidget( new QLabel(tr("This version is unstable! Consider upgrading.")), 1, 0); if (room->canSwitchVersions()) { auto* changeActionButton = new QPushButton(tr("Upgrade", "Upgrade a room version")); connect(changeActionButton, &QAbstractButton::clicked, this, [=] { Dialog chooseVersionDlg(tr("Choose new room version"), this, NoStatusLine, tr("Upgrade", "Upgrade a room version"), NoExtraButtons); chooseVersionDlg.addWidget( new QLabel(tr("You are about to upgrade %1.\n" "This operation cannot be reverted.") .arg(room->displayName()))); auto* hBox = chooseVersionDlg.addLayout(); auto* versionSelector = addVersionSelector(hBox); refillVersionSelector(versionSelector, room->connection()); if (chooseVersionDlg.exec() == QDialog::Accepted) { version->setText(versionSelector->currentData().toString()); apply(); } }); versionBox->addWidget(changeActionButton, 0, 1, -1, 1); } addEssentials(account, versionBox); connect(room, &QuaternionRoom::avatarChanged, this, [this, room] { if (!userChangedAvatar) avatar->setPixmap(QPixmap::fromImage(room->avatar(64))); }); avatar->setPixmap(QPixmap::fromImage(room->avatar(64))); tagsList->setSizeAdjustPolicy( QAbstractScrollArea::AdjustToContentsOnFirstShow); tagsList->setUniformItemSizes(true); tagsList->setSelectionMode(QAbstractItemView::ExtendedSelection); mainFormLayout->addRow(tr("Tags"), tagsList); auto* roomIdLabel = new QLabel(room->id()); roomIdLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); mainFormLayout->addRow(tr("Room identifier"), roomIdLabel); connect(room, &QObject::destroyed, this, &QObject::deleteLater); // Uncomment to debug room display name calculation code // auto* refreshNameButton = // buttonBox()->addButton(tr("Refresh name"), QDialogButtonBox::ApplyRole); // connect(refreshNameButton, &QPushButton::clicked, // room, &QuaternionRoom::refreshDisplayName); } void RoomSettingsDialog::load() { const auto* plEvt = room->getCurrentState(); const int userPl = plEvt->powerLevelForUser(room->localUser()->id()); roomName->setText(room->name()); roomName->setReadOnly(plEvt && plEvt->powerLevelForState("m.room.name") > userPl); alias->setText(room->canonicalAlias()); alias->setReadOnly(plEvt && plEvt->powerLevelForState("m.room.canonical_alias") > userPl); topic->setPlainText(room->topic()); topic->setReadOnly(plEvt && plEvt->powerLevelForState("m.room.topic") > userPl); // QPlainTextEdit may change some characters before any editing occurs; // so save this already adjusted topic to compare later. previousTopic = topic->toPlainText(); tagsList->clear(); auto roomTags = room->tagNames(); for (const auto& tag: room->connection()->tagNames()) { auto* item = new QListWidgetItem(tagToCaption(tag), tagsList); item->setData(Qt::UserRole, tag); item->setFlags(Qt::ItemIsEnabled|Qt::ItemIsUserCheckable); item->setCheckState( roomTags.contains(tag) ? Qt::Checked : Qt::Unchecked); item->setToolTip(tag); tagsList->addItem(item); } } bool RoomSettingsDialog::validate() { if (room->version() == version->text() || (room->canSwitchVersions() && checkRoomVersion(version->text(), room->connection()))) return true; // The room is the same, or it's allowed to change it version->setText(room->version()); return false; // Cancel applying, stay on the settings dialog } void RoomSettingsDialog::apply() { using Quotient::Room; if (version->text() != room->version()) { setStatusMessage(tr("Creating the new room version, please wait")); connectUntil(room, &Room::upgraded, this, [this] (const QString&, Room* newRoom) { accept(); static_cast(parent())->selectRoom(newRoom); return true; }); connectSingleShot(room, &Room::upgradeFailed, this, &Dialog::applyFailed); room->switchVersion(version->text()); return; // It's either a version upgrade or everything else } if (roomName->text() != room->name()) room->setName(roomName->text()); if (alias->text() != room->canonicalAlias()) room->setCanonicalAlias(alias->text()); if (topic->toPlainText() != previousTopic) room->setTopic(topic->toPlainText()); auto tags = room->tags(); for (int i = 0; i < tagsList->count(); ++i) { const auto* item = tagsList->item(i); const auto tagName = item->data(Qt::UserRole).toString(); if (item->checkState() == Qt::Checked) tags[tagName]; // Just ensure the tag is there, no overwriting else tags.remove(tagName); } room->setTags(tags, Room::WithinSameState); accept(); } class NextInvitee : public QComboBox { public: using QComboBox::QComboBox; private: void focusInEvent(QFocusEvent* event) override { QComboBox::focusInEvent(event); static_cast(parent())->updatePushButtons(); } void focusOutEvent(QFocusEvent* event) override { QComboBox::focusOutEvent(event); static_cast(parent())->updatePushButtons(); } }; class InviteeList : public QListWidget { public: using QListWidget::QListWidget; private: void keyPressEvent(QKeyEvent* event) override { if (event->key() == Qt::Key_Delete) delete takeItem(currentRow()); } void mousePressEvent(QMouseEvent* event) override { if (event->button() == Qt::MiddleButton) delete takeItem(currentRow()); } }; CreateRoomDialog::CreateRoomDialog(const AccountRegistry* accounts, QWidget* parent) : RoomDialogBase(tr("Create room"), tr("Create room"), nullptr, parent, NoExtraButtons) , accountChooser(new AccountSelector(accounts)) , version(nullptr) // Will be initialized below , nextInvitee(new NextInvitee) , inviteButton(new QPushButton(tr("Add", "Add a user to the list of invitees"))) , invitees(new QListWidget) { Q_ASSERT(accounts && !accounts->isEmpty()); auto* versionBox = new QHBoxLayout; version = addVersionSelector(versionBox); addEssentials(accountChooser, versionBox); connect(accountChooser, &AccountSelector::currentAccountChanged, this, &CreateRoomDialog::accountSwitched); mainFormLayout->insertRow(0, new QLabel( tr("Please fill the fields as desired. None are mandatory"))); nextInvitee->setEditable(true); nextInvitee->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon); nextInvitee->setMinimumContentsLength(42); auto* completer = new QCompleter(nextInvitee); completer->setCaseSensitivity(Qt::CaseInsensitive); completer->setCompletionMode(QCompleter::UnfilteredPopupCompletion); completer->setModelSorting(QCompleter::CaseSensitivelySortedModel); nextInvitee->setCompleter(completer); connect(nextInvitee, &NextInvitee::currentTextChanged, this, &CreateRoomDialog::updatePushButtons); // connect(nextInvitee, &NextInvitee::editTextChanged, // this, &CreateRoomDialog::updateUserList); inviteButton->setFocusPolicy(Qt::NoFocus); inviteButton->setDisabled(true); connect(inviteButton, &QPushButton::clicked, [this] { auto userName = nextInvitee->currentText(); if (userName.indexOf('@') == -1) { userName.prepend('@'); if (userName.indexOf(':') == -1) userName += ':' + accountChooser->currentAccount()->domain(); } auto* item = new QListWidgetItem(userName); if (nextInvitee->currentIndex() != -1) item->setData(Qt::UserRole, nextInvitee->currentData(Qt::UserRole)); invitees->addItem(item); nextInvitee->clear(); }); invitees->setSizeAdjustPolicy( QAbstractScrollArea::AdjustToContentsOnFirstShow); invitees->setUniformItemSizes(true); invitees->setSortingEnabled(true); // Layout additional controls auto* inviteLayout = new QHBoxLayout; inviteLayout->addWidget(nextInvitee); inviteLayout->addWidget(inviteButton); mainFormLayout->addRow(tr("Invite user(s)"), inviteLayout); mainFormLayout->addRow("", invitees); setPendingApplyMessage(tr("Creating the room, please wait")); if (accounts->size() > 1) accountChooser->setFocus(); else roomName->setFocus(); } void CreateRoomDialog::updatePushButtons() { inviteButton->setEnabled(!nextInvitee->currentText().isEmpty()); if (inviteButton->isEnabled() && nextInvitee->hasFocus()) inviteButton->setDefault(true); else buttonBox()->button(QDialogButtonBox::Ok)->setDefault(true); } void CreateRoomDialog::load() { qDebug() << "Loading the dialog"; roomName->clear(); alias->clear(); topic->clear(); previousTopic.clear(); nextInvitee->clear(); accountSwitched(); invitees->clear(); } bool CreateRoomDialog::validate() { auto* connection = accountChooser->currentAccount(); if (checkRoomVersion(version->currentData().toString(), connection)) return true; refillVersionSelector(version, connection); return false; } void CreateRoomDialog::apply() { using namespace Quotient; QStringList userIds; for (int i = 0; i < invitees->count(); ++i) if (auto* user = invitees->item(i)->data(Qt::UserRole).value()) userIds.push_back(user->id()); else userIds.push_back(invitees->item(i)->text()); auto* job = accountChooser->currentAccount()->createRoom( publishRoom->isChecked() ? Connection::PublishRoom : Connection::UnpublishRoom, alias->text(), roomName->text(), topic->toPlainText(), userIds, "", version->currentData().toString(), false); connect(job, &BaseJob::success, this, &Dialog::accept); connect(job, &BaseJob::failure, this, [this,job] { applyFailed(job->errorString()); }); } void CreateRoomDialog::accountSwitched() { const auto& savedCurrentText = nextInvitee->currentText(); auto* connection = accountChooser->currentAccount(); refillVersionSelector(version, connection); aliasServer->setText(':' + connection->domain()); auto* completer = nextInvitee->completer(); Q_ASSERT(completer != nullptr && connection != nullptr); auto*& model = userLists[connection]; if (!model) { model = new QStandardItemModel(completer); // auto prefix = // savedCurrentText.midRef(savedCurrentText.startsWith('@') ? 1 : 0); // if (prefix.size() >= 3) // { QElapsedTimer et; et.start(); for (auto* u: connection->users()) { if (!u->isGuest()) { // It would be great to show u->fullName() rather than // just u->id(); unfortunately, this implies fetching profiles // for the whole list of users known to a given account, which // is terribly inefficient auto* item = new QStandardItem(u->id()); item->setData(QVariant::fromValue(u)); model->appendRow(item); } } qDebug() << "Completion candidates:" << model->rowCount() << "out of" << connection->users().size() << "filled in" << et; // } } nextInvitee->setModel(model); nextInvitee->setEditText(savedCurrentText); completer->setCompletionPrefix(savedCurrentText); } Quaternion-0.0.95.1/client/roomdialogs.h000066400000000000000000000062551412757327200200610ustar00rootroot00000000000000/****************************************************************************** * Copyright (C) 2017 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "dialog.h" namespace Quotient { class Connection; } class MainWindow; class QuaternionRoom; class AccountRegistry; class AccountSelector; class QComboBox; class QLineEdit; class QPlainTextEdit; class QCheckBox; class QPushButton; class QListWidget; class QFormLayout; class QStandardItemModel; class RoomDialogBase : public Dialog { Q_OBJECT protected: using Connection = Quotient::Connection; RoomDialogBase(const QString& title, const QString& applyButtonText, QuaternionRoom* r, QWidget* parent, QDialogButtonBox::StandardButtons extraButtons = QDialogButtonBox::Reset); protected: QuaternionRoom* room; QLabel* avatar; QLineEdit* roomName; QLabel* aliasServer; QLineEdit* alias; QPlainTextEdit* topic; QString previousTopic; QCheckBox* publishRoom; QCheckBox* guestCanJoin; QFormLayout* mainFormLayout; QFormLayout* essentialsLayout = nullptr; QComboBox* addVersionSelector(QLayout* layout); void refillVersionSelector(QComboBox* selector, Connection* account); void addEssentials(QWidget* accountControl, QLayout* versionBox); bool checkRoomVersion(QString version, Connection* account); }; class RoomSettingsDialog : public RoomDialogBase { Q_OBJECT public: RoomSettingsDialog(QuaternionRoom* room, MainWindow* parent = nullptr); private slots: void load() override; bool validate() override; void apply() override; private: QLabel* account; QLabel* version; QListWidget* tagsList; bool userChangedAvatar = false; }; class CreateRoomDialog : public RoomDialogBase { Q_OBJECT public: CreateRoomDialog(const AccountRegistry* accounts, QWidget* parent = nullptr); public slots: void updatePushButtons(); private slots: void load() override; bool validate() override; void apply() override; void accountSwitched(); private: AccountSelector* accountChooser; QComboBox* version; QComboBox* nextInvitee; QPushButton* inviteButton; QListWidget* invitees; QHash userLists; }; Quaternion-0.0.95.1/client/roomlistdock.cpp000066400000000000000000000315421412757327200206030ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2015 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #include "roomlistdock.h" #include #include #include #include #include #include #include "mainwindow.h" #include "models/roomlistmodel.h" #include "models/orderbytag.h" #include "quaternionroom.h" #include "roomdialogs.h" #include #include using Quotient::SettingsGroup; class RoomListItemDelegate // clazy:exclude=missing-qobject-macro : public QStyledItemDelegate { public: using QStyledItemDelegate::QStyledItemDelegate; void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; }; void RoomListItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { QStyleOptionViewItem o { option }; if (!index.parent().isValid()) // Group captions { o.displayAlignment = Qt::AlignHCenter; o.font.setBold(true); } if (index.data(RoomListModel::HasUnreadRole).toBool()) o.font.setBold(true); if (index.data(RoomListModel::HighlightCountRole).toInt() > 0) { static const auto highlightColor = Quotient::Settings().get("UI/highlight_color", QColor("orange")); o.palette.setColor(QPalette::Text, highlightColor); // Highlighting the text may not work out on monochrome colour schemes, // hence duplicating with italic font. o.font.setItalic(true); } const auto joinState = index.data(RoomListModel::JoinStateRole).toString(); if (joinState == "invite") o.font.setItalic(true); else if (joinState == "leave" || joinState == "upgraded") o.font.setStrikeOut(true); QStyledItemDelegate::paint(painter, o, index); } RoomListDock::RoomListDock(MainWindow* parent) : QDockWidget("Rooms", parent) , view(new QTreeView(this)) , model(new RoomListModel(view)) { setObjectName("RoomsDock"); // proxyModel = new QSortFilterProxyModel(); // proxyModel->setDynamicSortFilter(true); // proxyModel->setSourceModel(model); updateSortingMode(); view->setModel(model); view->setItemDelegate(new RoomListItemDelegate(this)); view->setAnimated(true); view->setUniformRowHeights(true); view->setSelectionBehavior(QTreeView::SelectRows); view->setHeaderHidden(true); view->setIndentation(0); view->setRootIsDecorated(false); const auto iconExtent = view->fontMetrics().height(); view->setIconSize( QIcon::fromTheme("user-available", QIcon(":/irc-channel-joined")) .actualSize({ iconExtent, iconExtent })); static const auto Expanded = QStringLiteral("expand"); static const auto Collapsed = QStringLiteral("collapse"); connect( view, &QTreeView::activated, this, &RoomListDock::rowSelected ); // See #608 connect( view, &QTreeView::clicked, this, &RoomListDock::rowSelected); connect( view, &QTreeView::pressed, this, [this] { if (QGuiApplication::mouseButtons() & Qt::MiddleButton) { if (auto room = getSelectedRoom()) room->markAllMessagesAsRead(); } }); connect( model, &RoomListModel::rowsInserted, this, &RoomListDock::refreshTitle ); connect( model, &RoomListModel::rowsRemoved, this, &RoomListDock::refreshTitle ); connect( model, &RoomListModel::saveCurrentSelection, this, [this] { selectedGroupCache = getSelectedGroup(); selectedRoomCache = getSelectedRoom(); }); connect( model, &RoomListModel::restoreCurrentSelection, this, [this] { const auto& idx = model->indexOf(selectedGroupCache, selectedRoomCache); // proxyModel->mapFromSource(model->indexOf(selectedRoomCache)); view->setCurrentIndex(idx); view->scrollTo(idx); selectedGroupCache.clear(); selectedRoomCache = nullptr; }); static SettingsGroup dockSettings("UI/RoomsDock"); connect(model, &RoomListModel::groupAdded, this, [this](int groupPos) { const auto& i = model->index(groupPos, 0); const auto groupKey = model->roomGroupAt(i).toString(); if (groupKey.startsWith("org.qmatrixclient")) qCritical() << groupKey << "is deprecated!"; // Fighting the legacy auto groupState = dockSettings.value(groupKey); if (!groupState.isValid()) { if (groupKey.startsWith(RoomGroup::SystemPrefix)) { const auto legacyKey = RoomGroup::LegacyPrefix + groupKey.mid( RoomGroup::SystemPrefix.size()); groupState = dockSettings.value(legacyKey); dockSettings.setValue(groupKey, groupState); if (groupState.isValid()) dockSettings.remove(legacyKey); } } view->setExpanded(i, groupState.isValid() ? groupState.toString() == Expanded : groupKey == Quotient::FavouriteTag); }); connect(view, &QTreeView::expanded, this, [this](QModelIndex i) { dockSettings.setValue(model->roomGroupAt(i).toString(), Expanded); }); connect(view, &QTreeView::collapsed, this, [this](QModelIndex i) { dockSettings.setValue(model->roomGroupAt(i).toString(), Collapsed); }); setWidget(view); roomContextMenu = new QMenu(this); markAsReadAction = roomContextMenu->addAction(QIcon::fromTheme("mail-mark-read"), tr("Mark room as read"), this, [this] { if (auto room = getSelectedRoom()) room->markAllMessagesAsRead(); }); roomContextMenu->addSeparator(); addTagsAction = roomContextMenu->addAction(QIcon::fromTheme("tag-new"), tr("Add tags..."), this, &RoomListDock::addTagsSelected); roomSettingsAction = roomContextMenu->addAction( QIcon::fromTheme("user-group-properties"), tr("Change room &settings..."), [this, parent] { parent->openRoomSettings(getSelectedRoom()); }); roomPermalinkAction = roomContextMenu->addAction( QIcon::fromTheme("link"), tr("Copy room link to clipboard"), [this] { QGuiApplication::clipboard()->setText( "https://matrix.to/#/" + getSelectedRoom()->canonicalAlias()); }); roomContextMenu->addSeparator(); joinAction = roomContextMenu->addAction(QIcon::fromTheme("irc-join-channel"), tr("Join room"), this, [this] { if (auto room = getSelectedRoom()) { Q_ASSERT(room->connection()); room->connection()->joinRoom(room->id()); } }); leaveAction = roomContextMenu->addAction(QIcon::fromTheme("irc-close-channel"), {}, this, [this] { if (auto room = getSelectedRoom()) room->leaveRoom(); }); roomContextMenu->addSeparator(); forgetAction = roomContextMenu->addAction(QIcon::fromTheme("irc-remove-operator"), tr("Forget room"), this, [this] { if (auto room = getSelectedRoom()) { Q_ASSERT(room->connection()); room->connection()->forgetRoom(room->id()); } }); groupContextMenu = new QMenu(this); deleteTagAction = groupContextMenu->addAction(QIcon::fromTheme("tag-delete"), tr("Remove tag"), this, [this] { model->deleteTag(view->currentIndex()); }); setContextMenuPolicy(Qt::CustomContextMenu); connect(this, &QWidget::customContextMenuRequested, this, &RoomListDock::showContextMenu); } void RoomListDock::addConnection(Quotient::Connection* connection) { model->addConnection(connection); } void RoomListDock::updateSortingMode() { // const auto sortMode = // Quotient::Settings().value("UI/sort_rooms_by", 0).toInt(); // proxyModel->sort(sortMode, // sortMode == 0 ? Qt::AscendingOrder : Qt::DescendingOrder); model->setOrder(); } void RoomListDock::setSelectedRoom(QuaternionRoom* room) { if (getSelectedRoom() == room) return; // First try the current group; if that fails, try the entire list QModelIndex idx; auto currentGroup = getSelectedGroup(); if (!currentGroup.isNull()) idx = model->indexOf(currentGroup, room); if (!idx.isValid()) idx = model->indexOf({}, room); if (idx.isValid()) { view->setCurrentIndex(idx); view->scrollTo(idx); } } void RoomListDock::rowSelected(const QModelIndex& index) { if (model->isValidRoomIndex(index)) // emit roomSelected( model->roomAt(proxyModel->mapToSource(index))); emit roomSelected(model->roomAt(index)); } void RoomListDock::showContextMenu(const QPoint& pos) { auto index = view->indexAt(view->mapFromParent(pos)); if (!index.isValid()) return; // No context menu on root item yet if (model->isValidGroupIndex(index)) { // Don't allow to delete system "tags" auto tagName = model->roomGroupAt(index); deleteTagAction->setDisabled( tagName.toString().startsWith(RoomGroup::SystemPrefix)); groupContextMenu->popup(mapToGlobal(pos)); return; } Q_ASSERT(model->isValidRoomIndex(index)); auto room = model->roomAt(index); // auto room = model->roomAt(proxyModel->mapToSource(index)); using Quotient::JoinState; bool joined = room->joinState() == JoinState::Join; bool invited = room->joinState() == JoinState::Invite; markAsReadAction->setEnabled(joined); addTagsAction->setEnabled(joined); joinAction->setEnabled(!joined); leaveAction->setText(invited ? tr("Reject invitation") : tr("Leave room")); leaveAction->setEnabled(room->joinState() != JoinState::Leave); forgetAction->setVisible(!invited); roomContextMenu->popup(mapToGlobal(pos)); } QVariant RoomListDock::getSelectedGroup() const { auto index = view->currentIndex(); return !index.isValid() ? QVariant() : model->roomGroupAt(index); } QuaternionRoom* RoomListDock::getSelectedRoom() const { QModelIndex index = view->currentIndex(); return !index.isValid() || !index.parent().isValid() ? nullptr : model->roomAt(index); // : model->roomAt(proxyModel->mapToSource(index)); } void RoomListDock::addTagsSelected() { if (auto room = getSelectedRoom()) { Dialog dlg(tr("Enter new tags for the room"), this, Dialog::NoStatusLine, tr("Add", "A caption on a button to add tags"), Dialog::NoExtraButtons); dlg.addWidget( new QLabel(tr("Enter tags to add to this room, one tag per line"))); auto tagsInput = new QPlainTextEdit(); tagsInput->setTabChangesFocus(true); dlg.addWidget(tagsInput); if (dlg.exec() != QDialog::Accepted) return; auto tags = room->tags(); const auto enteredTags = tagsInput->toPlainText().split('\n', #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) Qt::SkipEmptyParts #else QString::SkipEmptyParts #endif ); for (const auto& tag: enteredTags) tags[captionToTag(tag)]; // No overwriting, just ensure existence room->setTags(tags, Quotient::Room::WithinSameState); } } void RoomListDock::refreshTitle() { setWindowTitle(tr("Rooms (%L1)").arg(model->totalRooms())); } Quaternion-0.0.95.1/client/roomlistdock.h000066400000000000000000000055671412757327200202600ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2015 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #pragma once #include #include #include //#include class MainWindow; class RoomListModel; class QuaternionRoom; namespace Quotient { class Connection; } class RoomListDock : public QDockWidget { Q_OBJECT public: explicit RoomListDock(MainWindow* parent = nullptr); void addConnection(Quotient::Connection* connection); public slots: void updateSortingMode(); void setSelectedRoom(QuaternionRoom* room); signals: void roomSelected(QuaternionRoom* room); private slots: void rowSelected(const QModelIndex& index); void showContextMenu(const QPoint& pos); void addTagsSelected(); void refreshTitle(); private: QTreeView* view = nullptr; RoomListModel* model = nullptr; // QSortFilterProxyModel* proxyModel; QMenu* roomContextMenu = nullptr; QMenu* groupContextMenu = nullptr; QAction* markAsReadAction = nullptr; QAction* addTagsAction = nullptr; QAction* joinAction = nullptr; QAction* leaveAction = nullptr; QAction* forgetAction = nullptr; QAction* deleteTagAction = nullptr; QAction* roomSettingsAction = nullptr; QAction* roomPermalinkAction = nullptr; QVariant selectedGroupCache = {}; QuaternionRoom* selectedRoomCache = nullptr; QVariant getSelectedGroup() const; QuaternionRoom* getSelectedRoom() const; }; Quaternion-0.0.95.1/client/systemtrayicon.cpp000066400000000000000000000071371412757327200211720ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2016 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #include "systemtrayicon.h" #include #include #include #include "mainwindow.h" #include "quaternionroom.h" #include "linuxutils.h" #include #include SystemTrayIcon::SystemTrayIcon(MainWindow* parent) : QSystemTrayIcon(parent) , m_parent(parent) { auto contextMenu = new QMenu(parent); auto showHideAction = contextMenu->addAction(tr("Hide"), this, &SystemTrayIcon::showHide); contextMenu->addAction(tr("Quit"), this, QApplication::quit); connect(m_parent->windowHandle(), &QWindow::visibleChanged, [showHideAction](bool visible) { showHideAction->setText(visible ? tr("Hide") : tr("Show")); }); setIcon(QIcon::fromTheme(appIconName(), QIcon(":/icon.png"))); setToolTip("Quaternion"); setContextMenu(contextMenu); connect( this, &SystemTrayIcon::activated, this, &SystemTrayIcon::systemTrayIconAction); } void SystemTrayIcon::newRoom(Quotient::Room* room) { connect(room, &Quotient::Room::highlightCountChanged, this, [this,room] { highlightCountChanged(room); }); } void SystemTrayIcon::highlightCountChanged(Quotient::Room* room) { using namespace Quotient; const auto mode = Settings().get("UI/notifications", "intrusive"); if (mode == "none") return; if( room->highlightCount() > 0 ) { showMessage( //: %1 is the room display name tr("Highlight in %1").arg(room->displayName()), tr("%Ln highlight(s)", "", room->highlightCount())); if (mode != "non-intrusive") m_parent->activateWindow(); connectSingleShot(this, &SystemTrayIcon::messageClicked, m_parent, [this,qRoom=static_cast(room)] { m_parent->selectRoom(qRoom); }); } } void SystemTrayIcon::systemTrayIconAction(QSystemTrayIcon::ActivationReason reason) { if (reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::DoubleClick) showHide(); } void SystemTrayIcon::showHide() { if (m_parent->isVisible()) m_parent->hide(); else { m_parent->show(); m_parent->activateWindow(); m_parent->raise(); m_parent->setFocus(); } } Quaternion-0.0.95.1/client/systemtrayicon.h000066400000000000000000000035661412757327200206410ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2016 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #pragma once #include namespace Quotient { class Room; } class MainWindow; class SystemTrayIcon: public QSystemTrayIcon { Q_OBJECT public: explicit SystemTrayIcon(MainWindow* parent = nullptr); public slots: void newRoom(Quotient::Room* room); private slots: void highlightCountChanged(Quotient::Room* room); void systemTrayIconAction(QSystemTrayIcon::ActivationReason reason); private: MainWindow* m_parent; void showHide(); }; Quaternion-0.0.95.1/client/timelinewidget.cpp000066400000000000000000000310571412757327200211050ustar00rootroot00000000000000#include "timelinewidget.h" #include "chatroomwidget.h" #include "models/messageeventmodel.h" #include "imageprovider.h" #include #include #include #include #include #include #include #include #include #include #include #include #include TimelineWidget::TimelineWidget(ChatRoomWidget* chatRoomWidget) : m_messageModel(new MessageEventModel(this)) , m_imageProvider(new ImageProvider()) , indexToMaybeRead(-1) , readMarkerOnScreen(false) , roomWidget(chatRoomWidget) { using namespace Quotient; qmlRegisterUncreatableType( "Quotient", 1, 0, "Room", "Room objects can only be created by libQuotient"); qmlRegisterUncreatableType( "Quotient", 1, 0, "User", "User objects can only be created by libQuotient"); #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) qmlRegisterAnonymousType("Quotient", 1); qmlRegisterAnonymousType("Quotient", 1); #else qmlRegisterType(); qmlRegisterType(); #endif qRegisterMetaType("GetRoomEventsJob*"); qRegisterMetaType("User*"); qmlRegisterType("Quotient", 1, 0, "Settings"); qmlRegisterUncreatableType( "Quotient", 1, 0, "RoomMessageEvent", "RoomMessageEvent is uncreatable"); qDebug() << "Rendering QML with" << TimelineBaseWidget::staticMetaObject.className(); setResizeMode(SizeRootObjectToView); engine()->addImageProvider(QStringLiteral("mtx"), m_imageProvider); auto* ctxt = rootContext(); ctxt->setContextProperty(QStringLiteral("messageModel"), m_messageModel); ctxt->setContextProperty(QStringLiteral("controller"), this); setSource(QUrl("qrc:///qml/Timeline.qml")); connect(&activityDetector, &ActivityDetector::triggered, this, &TimelineWidget::markShownAsRead); } TimelineWidget::~TimelineWidget() { // Clean away the view to prevent further requests to the controller setSource({}); } QString TimelineWidget::selectedText() const { return m_selectedText; } QuaternionRoom* TimelineWidget::currentRoom() const { return m_messageModel->room(); } void TimelineWidget::setRoom(QuaternionRoom* newRoom) { if (currentRoom() == newRoom) return; if (currentRoom()) { currentRoom()->setDisplayed(false); currentRoom()->disconnect(this); } readMarkerOnScreen = false; maybeReadTimer.stop(); indicesOnScreen.clear(); // Update the image provider upfront to allow image requests from // QML bindings to MessageEventModel::roomChanged m_imageProvider->setConnection(newRoom ? newRoom->connection() : nullptr); m_messageModel->changeRoom(newRoom); if (newRoom) { connect(newRoom, &Quotient::Room::readMarkerMoved, this, [this] { const auto rm = currentRoom()->readMarker(); readMarkerOnScreen = rm != currentRoom()->historyEdge() && std::lower_bound(indicesOnScreen.cbegin(), indicesOnScreen.cend(), rm->index()) != indicesOnScreen.cend(); reStartShownTimer(); activityDetector.setEnabled(pendingMarkRead()); }); newRoom->setDisplayed(true); } } void TimelineWidget::focusInput() { roomWidget->focusInput(); } void TimelineWidget::spotlightEvent(const QString& eventId) { auto index = m_messageModel->findRow(eventId); if (index >= 0) { emit viewPositionRequested(index); emit animateMessage(index); } else roomWidget->setHudHtml("" % tr("Referenced message not found") % ""); } void TimelineWidget::saveFileAs(const QString& eventId) { if (!currentRoom()) { qWarning() << "ChatRoomWidget::saveFileAs without an active room ignored"; return; } // TODO: Once Qt 5.11 is dropped, use `this` instead of roomWidget const auto fileName = QFileDialog::getSaveFileName(roomWidget, tr("Save file as"), currentRoom()->fileNameToDownload(eventId)); if (!fileName.isEmpty()) currentRoom()->downloadFile(eventId, QUrl::fromLocalFile(fileName)); } void TimelineWidget::onMessageShownChanged(int visualIndex, bool shown, bool hasReadMarker) { const auto* room = currentRoom(); if (!room || !room->displayed()) return; // A message can be auto-marked as (fully) read if: // 0. The (fully) read marker is on the screen // 1. The message is shown on the screen now // 2. It's been the bottommost message on the screen for the last 1 second // (or whatever UI/maybe_read_timer tells in milliseconds) and the user // is active during that time // 3. It's below the read marker after that time Q_ASSERT(visualIndex <= room->timelineSize()); const auto eventIt = room->syncEdge() - visualIndex - 1; const auto timelineIndex = eventIt->index(); if (hasReadMarker) { readMarkerOnScreen = shown; if (shown) { indexToMaybeRead = timelineIndex; reStartShownTimer(); } else maybeReadTimer.stop(); } auto pos = std::lower_bound(indicesOnScreen.begin(), indicesOnScreen.end(), timelineIndex); if (shown) { if (pos == indicesOnScreen.end() || *pos != timelineIndex) { indicesOnScreen.insert(pos, timelineIndex); if (timelineIndex == indicesOnScreen.back()) reStartShownTimer(); } } else { if (pos != indicesOnScreen.end() && *pos == timelineIndex) if (indicesOnScreen.erase(pos) == indicesOnScreen.end()) reStartShownTimer(); } } void TimelineWidget::showMenu(int index, const QString& hoveredLink, const QString& selectedText, bool showingDetails) { const auto modelIndex = m_messageModel->index(index, 0); const auto eventId = modelIndex.data(MessageEventModel::EventIdRole).toString(); // TODO: Once Qt 5.11 is dropped, use `this` instead of roomWidget auto menu = new QMenu(roomWidget); menu->setAttribute(Qt::WA_DeleteOnClose); const auto* plEvt = currentRoom()->getCurrentState(); const auto localUserId = currentRoom()->localUser()->id(); const int userPl = plEvt->powerLevelForUser(localUserId); const auto* modelUser = modelIndex.data(MessageEventModel::AuthorRole).value(); if (!plEvt || userPl >= plEvt->redact() || localUserId == modelUser->id()) menu->addAction(QIcon::fromTheme("edit-delete"), tr("Redact"), this, [this, eventId] { currentRoom()->redactEvent(eventId); }); if (!selectedText.isEmpty()) menu->addAction(tr("Copy selected text to clipboard"), this, [selectedText] { QApplication::clipboard()->setText(selectedText); }); if (!hoveredLink.isEmpty()) menu->addAction(tr("Copy link to clipboard"), this, [hoveredLink] { QApplication::clipboard()->setText(hoveredLink); }); menu->addAction(QIcon::fromTheme("link"), tr("Copy permalink to clipboard"), [this, eventId] { QApplication::clipboard()->setText( "https://matrix.to/#/" + currentRoom()->id() + "/" + QUrl::toPercentEncoding(eventId)); }); menu->addAction(QIcon::fromTheme("format-text-blockquote"), tr("Quote", "a verb (do quote), not a noun (a quote)"), [this, modelIndex] { roomWidget->quote(modelIndex.data().toString()); }); auto a = menu->addAction(QIcon::fromTheme("view-list-details"), tr("Show details"), [this, index] { emit showDetails(index); }); a->setCheckable(true); a->setChecked(showingDetails); const auto eventType = modelIndex.data(MessageEventModel::EventTypeRole).toString(); if (eventType == "image" || eventType == "file") { const auto progressInfo = modelIndex.data(MessageEventModel::LongOperationRole) .value(); const bool downloaded = !progressInfo.isUpload && progressInfo.completed(); menu->addSeparator(); menu->addAction(QIcon::fromTheme("document-open"), tr("Open externally"), [this, index] { emit openExternally(index); }); if (downloaded) { menu->addAction(QIcon::fromTheme("folder-open"), tr("Open Folder"), [localDir = progressInfo.localDir] { QDesktopServices::openUrl(localDir); }); if (eventType == "image") { menu->addAction(tr("Copy image to clipboard"), this, [imgPath = progressInfo.localPath.path()] { QApplication::clipboard()->setImage( QImage(imgPath)); }); } } else { menu->addAction(QIcon::fromTheme("edit-download"), tr("Download"), [this, eventId] { currentRoom()->downloadFile(eventId); }); } menu->addAction(QIcon::fromTheme("document-save-as"), tr("Save file as..."), [this, eventId] { saveFileAs(eventId); }); } menu->popup(QCursor::pos()); } void TimelineWidget::reactionButtonClicked(const QString& eventId, const QString& key) { using namespace Quotient; const auto& annotations = currentRoom()->relatedEvents(eventId, EventRelation::Annotation()); for (const auto& a: annotations) if (auto* e = eventCast(a); e != nullptr && e->relation().key == key && a->senderId() == currentRoom()->localUser()->id()) // { currentRoom()->redactEvent(a->id()); return; } currentRoom()->postReaction(eventId, key); } void TimelineWidget::setGlobalSelectionBuffer(const QString& text) { if (QApplication::clipboard()->supportsSelection()) QApplication::clipboard()->setText(text, QClipboard::Selection); m_selectedText = text; } void TimelineWidget::reStartShownTimer() { if (!readMarkerOnScreen || indicesOnScreen.empty() || indexToMaybeRead >= indicesOnScreen.back()) return; static Quotient::Settings settings; maybeReadTimer.start(settings.get("UI/maybe_read_timer", 1000), this); qDebug() << "Scheduled maybe-read message update:" << indexToMaybeRead << "->" << indicesOnScreen.back(); } void TimelineWidget::timerEvent(QTimerEvent* qte) { if (qte->timerId() != maybeReadTimer.timerId()) { TimelineBaseWidget::timerEvent(qte); return; } maybeReadTimer.stop(); // Only update the maybe-read message if we're tracking it if (readMarkerOnScreen && !indicesOnScreen.empty() && indexToMaybeRead < indicesOnScreen.back()) // { qDebug() << "Maybe-read message update:" << indexToMaybeRead << "->" << indicesOnScreen.back(); indexToMaybeRead = indicesOnScreen.back(); activityDetector.setEnabled(pendingMarkRead()); } } void TimelineWidget::markShownAsRead() { // FIXME: a case when a single message doesn't fit on the screen. if (auto room = currentRoom(); room != nullptr && readMarkerOnScreen) { const auto iter = room->findInTimeline(indicesOnScreen.back()); Q_ASSERT(iter != room->historyEdge()); room->markMessagesAsRead((*iter)->id()); } } bool TimelineWidget::pendingMarkRead() const { if (!readMarkerOnScreen || !currentRoom()) return false; const auto rm = currentRoom()->readMarker(); return rm != currentRoom()->historyEdge() && rm->index() < indexToMaybeRead; } Quaternion-0.0.95.1/client/timelinewidget.h000066400000000000000000000042111412757327200205420ustar00rootroot00000000000000#pragma once #include "activitydetector.h" #include #include #ifndef USE_QQUICKWIDGET # define DISABLE_QQUICKWIDGET #endif #ifdef DISABLE_QQUICKWIDGET # include #else # include #endif class ChatRoomWidget; class MessageEventModel; class ImageProvider; class QuaternionRoom; using TimelineBaseWidget = #ifdef DISABLE_QQUICKWIDGET QQuickView; #else QQuickWidget; #endif class TimelineWidget : public TimelineBaseWidget { Q_OBJECT public: TimelineWidget(ChatRoomWidget* chatRoomWidget); ~TimelineWidget() override; QString selectedText() const; QuaternionRoom* currentRoom() const; signals: void resourceRequested(const QString& idOrUri, const QString& action = {}); void roomSettingsRequested(); void showStatusMessage(const QString& message, int timeout = 0) const; void pageUpPressed(); void pageDownPressed(); void openExternally(int currentIndex); void showDetails(int currentIndex); void viewPositionRequested(int index); void animateMessage(int currentIndex); public slots: void setRoom(QuaternionRoom* room); void focusInput(); void spotlightEvent(const QString& eventId); void onMessageShownChanged(int visualIndex, bool shown, bool hasReadMarker); void markShownAsRead(); void saveFileAs(const QString& eventId); void showMenu(int index, const QString& hoveredLink, const QString& selectedText, bool showingDetails); void reactionButtonClicked(const QString& eventId, const QString& key); void setGlobalSelectionBuffer(const QString& text); private: MessageEventModel* m_messageModel; ImageProvider* m_imageProvider; QString m_selectedText; using timeline_index_t = Quotient::TimelineItem::index_t; std::vector indicesOnScreen; timeline_index_t indexToMaybeRead; QBasicTimer maybeReadTimer; bool readMarkerOnScreen; ActivityDetector activityDetector; ChatRoomWidget* roomWidget; void reStartShownTimer(); void timerEvent(QTimerEvent* qte) override; bool pendingMarkRead() const; }; Quaternion-0.0.95.1/client/translations/000077500000000000000000000000001412757327200201025ustar00rootroot00000000000000Quaternion-0.0.95.1/client/translations/quaternion_de.ts000066400000000000000000001746631412757327200233300ustar00rootroot00000000000000 ChatRoomWidget Choose a room to send messages or enter a command... Wähle einen Raum, um Nachrichten zu senden oder Kommandos einzugeben … There's nothing to send Es gibt nichts zu senden /join argument doesn't look like a room ID or alias /join-Argument sieht nicht nach einer Raum-ID oder Alias aus Sending a farewell message is not supported yet. If you intended to leave another room, switch to it and type /leave there. Eine Abschieds-Nachricht zu senden, wird aktuell nicht unterstützt. Wenn du einen anderen Raum verlassen willst, wechsele in diesen und tippe dort /leave ein. /forget must be followed by the room id/alias, even for the current room /forget benötigt als Argument eine(n) Raum-ID/Alias, auch für den aktuellen Raum %1 doesn't look like a room id or alias %1 sieht nicht nach einer Raum-ID oder einem Raum-Alias aus /invite <memberId> /invite <Benutzer-ID> /%1 <userId> <reason> /%1 <Benutzer-ID> <Grund> %1 is not a member of this room %1 ist kein Mitglied dieses Raumes /unban <userId> /unban <Benutzer-ID> /unban argument doesn't look like a user ID /unban-Argument sieht nicht nach einer Benutzer-ID aus /ignore <userId> /ignore <Benutzer-ID> /ignore argument doesn't look like a user ID /ignore-Argument sieht nicht nach einer Benutzer-ID aus Couldn't find user %1 on the server Konnte den Benutzer %1 nicht auf dem Server finden /me needs an argument /me braucht ein Argument /notice needs an argument /notice braucht ein Argument /%1 <memberId> <message> /%1 <Benutzer-ID> <Nachricht> %1 doesn't seem to have joined room %2 %1 ist dem Raum %2 anscheinend nicht beigetreten %1 doesn't look like a user id or room alias %1 sieht nicht nach einer Nutzer- oder Raum-ID aus /%1 <memberId> /%1 <Benutzer-ID> Unknown /command. Use // to send this line literally Unbekannter /Befehl. Starte mit //, um diese Zeile normal zu senden Attach Anhängen Attach file Datei anhängen Add a message to the file or just push Enter Füge der Datei eine Nachricht bei oder drücke einfach die Eingabetaste. Attaching %1 Hänge %1 an Attaching cancelled Anhängen abgebrochen There's no such /command outside of room. Es gibt keinen /Befehl außerhalb von Räumen. %1 doesn't look like a user id %1 sieht nicht wie eine Benutzer-ID aus %1 doesn't look like a user ID %1 sieht nicht wie eine Benutzer-ID aus You should select a room to send messages. Wählen Sie einen Raum, um Nachrichten zu senden. Send a message (over %1) or enter a command... Sende eine Nachricht (über %1) oder gebe einen Befehl ein … Attaching an image from clipboard Anhängen eines Bildes aus der Zwischenablage Send a message (no end-to-end encryption support yet)... Eine Nachricht senden (noch keine Unterstützung für Ende-zu-Ende-Verschlüsselung) … Your build of Quaternion doesn't support Markdown Ihr Build von Quaternion unterstützt Markdown nicht No completions Keine Vervollständigungen %Ln more completions %Ln weitere Vervollständigung %Ln weitere Vervollständigungen Next completion: Nächste Vervollständigung: Currently typing: Aktuell tippen: At pos %1: %2 An Position %1: %2 %L1 more %L1 weitere Timeline (no topic) (kein Thema) Unknown Unbekannt Unstable room version! Instabile Raumversion! (no name) (kein Name) %Ln byte(s) %Ln Byte %Ln Bytes %L1 MB %L1 MB %L1 GB %L1 GB This room has been upgraded. Der Raum wurde aktualisiert. Go to new room Gehe zu neuem Raum Room settings Raum- Einstellungen Latest events Neueste Ereignisse %Ln events back from now %Ln Ereignis zurückgescrollt %Ln Ereignisse zurückgescrollt %L1 kB %L1 kB %Ln events cached %Ln Ereignis zwischengespeichert %Ln Ereignisse zwischengespeichert %Ln events requested from the server %Ln Ereignis vom Server angefragt %Ln Ereignisse vom Server angefragt Hide topic Thema ausblenden Show topic Thema anzeigen CreateRoomDialog Create room Erstelle Raum Add Hinzufügen Invite user(s) Benutzer einladen Creating the room, please wait Erstelle den Raum. Bitte warten Please fill the fields as desired. None are mandatory Bitte füllen Sie die Felder wie gewünscht aus. Alle sind optional. Dialog Applying changes, please wait Änderungen werden übernommen. Bitte warten LoginDialog Login Anmelden Stay logged in Angemeldet bleiben Matrix ID Matrix-ID Password Passwort Device name Gerätename Connect to server Mit Server verbinden Connecting and logging in, please wait Am Verbinden und anmelden. Bitte warten Re-login Neu anmelden Restoring access, please wait Der Zugriff wird wiederhergestellt, warten Sie bitte Resolving the homeserver... Der Homeserver wird aufgelöst … The server URL doesn't look valid Die Server-URL sieht nicht gültig aus Login with SSO Anmeldung mit SSO The homeserver is available Der Homeserver ist verfügbar Could not connect to the homeserver Es konnte keine Verbindung zum Homeserver hergestellt werden No supported login flows Keine unterstützten Anmeldemethoden Single sign-on Single Sign-on Quaternion couldn't automatically open the single sign-on URL. Please copy and paste it to the right application (usually a web browser): Quaternion konnte die Single Sign-On-URL nicht automatisch öffnen. Bitte kopieren Sie sie und fügen Sie sie in die richtige Anwendung ein (normalerweise ein Webbrowser): After authentication, the browser will follow the temporary local address setup by Quaternion to conclude the login sequence. Nach der Authentifizierung folgt der Browser der von Quaternion eingerichteten temporären lokalen Adresse, um die Anmeldesequenz abzuschließen. Getting supported login flows... Lade unterstützte Anmeldemethoden … MainWindow Loading... Lädt … &Accounts &Konten &Login... &Anmelden … &Quit &Beenden &View &Oberfläche Dock &panels &Panel anheften &Display in timeline In &Historie anzeigen Normal &join/leave events Normale &Zutritts-/Verlassens-Ereignisse &Redacted events &Gelöschte Ereignisse Show redacted events in the timeline as 'Redacted' instead of hiding them entirely Zeige entfernte Ereignisse in der Historie als 'Entfernt', anstatt sie komplett zu verbergen &No-effect activity &Effektlose Aktivität Edit tags order Ändere Tag-Reihenfolge &Room &Raum Change room &settings... Ändere Raum-&Einstellungen … Create &new room... Erstelle &neuen Raum … &Join room... Raum &betreten … &Close current room Aktuellen Raum &Schließen &Settings &Einstellungen &Help &Hilfe &About &Über &Highlight only Nur &Hervorhebungen Notifications are entirely suppressed Benachrichtigungen werden komplett unterdrückt &Non-intrusive &Nicht-Aufdringlich Show notifications but do not activate the window Zeige Benachrichtigungen, aber Fenster nicht aktivieren &Full &Voll Show notifications and activate the window Zeige Benachrichtigungen und aktiviere das Fenster Notifications Benachrichtigungen Default Standard The layout with author labels above blocks of messages Autor-Kennung über Blöcken von Nachrichten The layout with author labels to the left from each message Autor-Kennung links von jeder Nachricht Timeline layout Layout der Historie Load full-size images at once Lade Bilder in voller Größe auf einmal Automatically download a full-size image instead of a thumbnail Automatisch komplettes Bild anstelle eines Vorschaubildes laden Configure &network proxy... Konfiguriere &Netzwerk-Proxy … Couldn't open a file to save access token Konnte keine Datei öffnen, um den Zugriffstoken zu speichern Quaternion couldn't open a file to write the access token to. You're logged in but will have to provide your password again when you restart the application. Quaternion konnte keine Datei öffnen, um den Zugangs-Token darin zu speichern. Sie wurden angemeldet, werden aber erneut ein Passwort eingeben müssen, wenn Sie die Anwendung neu starten. Couldn't set access token file permissions Konnte Berechtigungen der Zugriffstokendatei nicht setzen Quaternion couldn't restrict permissions on the access token file. Do you still want to save the access token to it? Quaternion konnte die Berechtigungen zur Zugangstoken-Datei nicht einschränken. Soll der Zugangstoken trotzdem darin gespeichert werden? Logged out as %1 Als %1 abgemeldet Sync failed Synchronisieren fehlgeschlagen The last sync of account %1 has failed with error: %2 Die letzte Synchronisierung von Account %1 schlug fehl mit Fehler: %2 The last sync has failed with error: %1 Die letzte Synchronisierung schlug fehl mit Fehler: %1 Clicking 'Retry' will attempt to resume synchronisation; Clicking 'Cancel' will stop further synchronisation of this account until logout or Quaternion restart. 'Erneut versuchen' wird versuchen weiter zu Synchronisieren; 'Abbrechen' wird weitere Synchronisierungsversuche dieses Kontos abbrechen bis zur Abmeldung oder Quaternion-Neustart. Before this server can process your information, you have to agree with its terms and conditions; please click the button below to open the web page where you can do that Bevor dieser Server deine Informationen verarbeiten kann, musst du den Benutzungsbedingungen zustimmen. Bitte klicke auf den Button unten um eine Webseite zu öffnen, auf der du dies tun kannst Open web page Webseite öffnen About Quaternion Über Quaternion Welcome to Quaternion Willkommen bei Quaternion Joined %1 as %2 %1 als %2 beigetreten Couldn't connect to the server as %1; will retry within %2 seconds Konnte nicht mit dem Server als %1 verbinden; Versuche es in %2 Sekunden erneut Reconnecting... Die Verbindung wird wiederhergestellt … No SSL support Keine SSL-Unterstützung Your SSL configuration does not allow Quaternion to establish secure connections. Deine SSL-Konfiguration erlaubt Quaternion nicht, eine sichere Verbindung herzustellen. SSL error SSL-Fehler Proxy needs authentication Proxy braucht Authentifizierung Authenticate Authentifizieren User name Benutzername Password Passwort &Thanks &Danke Original project author: %1 Ursprünglicher Projektautor: %1 Web page Webseite Project leader: %1 Projektleiter: %1 Contributors: Mitwirkende: Quaternion contributors @ GitHub Quaternion Mitwirkende @ GitHub Quaternion translators @ Lokalise.co Quaternion Übersetzer @ Lokalise.com Made with: Gemacht mit: Show join and leave events Zeige Betreten- und Verlassen-Ereignisse Use shuttle scrollbar (requires restart) Pendellaufleiste benutzen (erfordert Neustart) Control scroll velocity instead of position with the timeline scrollbar Der Laufleisten-Schieberegler für die Historie ändert die Laufgeschwindigkeit anstatt der Position. Request URL: %1 Response: %2 Anfrage-URL: %1 Antwort: %2 Close to tray Schließe in das Benachrichtigungsfeld. Make close button [X] minimize to tray instead of closing main window Schließen-Schaltfläche [X] minimiert in das Benachrichtigungsfeld, anstatt das Hauptfenster zu schließen. Show/hide meaningless activity (join-leave pairs and redacted events between) Ein-/ausblenden von bedeutungsloser Aktivität („betreten“–„verlassen“-Paare mit entfernten Ereignissen dazwischen) Built from Git, commit SHA: Aus Git heraus gebaut. Commit-SHA: Library commit SHA: Commit-SHA der Bibliothek: Open room... Raum öffnen … Open room Raum öffnen Open a room from the room list Einen Raum aus der Raumliste öffnen Show/hide Rooms dock panel Dock-Panel der Räume ein-/ausblenden Show/hide Users dock panel Benutzer-Dock-Panel ein-/ausblenden Access token file found Zugriffstokendatei gefunden Couldn't migrate access token Zugriffstoken konnte nicht migriert werden Couldn't save access token Zugriffstoken konnte nicht gespeichert werden. Logging in into a logged in account Einloggen in ein angemeldetes Konto You're trying to log in into an account that's already logged in. Do you want to continue? Sie versuchen, sich bei einem bereits angemeldeten Konto anzumelden. Möchten Sie fortfahren? Couldn't delete access token Zugriffstoken konnte nicht gelöscht werden. Open direct chat? Direkt-Chat öffnen? Open direct chat with user %1? Direkt-Chat mit Benutzer %1 öffnen? Room not found Raum nicht gefunden There's no room %1 in the room list. Check the spelling and the account. Es gibt keinen Raum %1 in der Raumliste. Überprüfen Sie die Rechtschreibung und das Konto. Confirm your account to open %1 Bestätigen Sie Ihr Konto, um %1 zu öffnen Confirm account Konto bestätigen Account Konto Room ID (starting with !) or alias (starting with #) Raum ID (beginnend mit !) oder Alias (beginnend mit #) Confirm account to join %1 Bestätigen Sie das Konto, um %1 beizutreten Edit quote style Zitat-Stil bearbeiten Markdown (prepend each line with >) Markdown (vor jeder Zeile mit >) Custom (apply regex from the config file) Benutzerdefiniert (Regex aus der Konfigurationsdatei anwenden) Locale's default (%1) Standard der Spracheinstellung (%1) Example quote Beispielzitat Choose the default style of quotes Wählen Sie den Standardstil für Zitate Special thanks to %1 for all the testing effort Besonderer Dank geht an %1 für all die Anstrengungen beim Testen libQuotient contributors @ GitHub libQuotient Mitwirkende @ GitHub Do you want to migrate the access token for %1 from the file to the keychain? Möchten Sie das Zugriffstoken für %1 aus der Datei in den Schlüsselbund migrieren? Quaternion couldn't migrate access token for %1 from the file to the keychain. Quaternion konnte das Zugriffstoken %1 aus der Datei nicht in den Schlüsselbund migrieren. Quaternion couldn't save the access token to the keychain. Do you want to save the access token to file %1? Quaternion konnte das Zugriffstoken nicht im Schlüsselbund speichern. Möchten Sie das Zugriffstoken in der Datei %1 speichern? First sync completed for %1 Erste Synchronisierung für %1 abgeschlossen Quaternion couldn't delete the access token from the keychain. Quaternion konnte das Zugriffstoken nicht aus dem Schlüsselbund löschen. No application for the link Keine Anwendung für den Link Your operating system could not find an application for the link. Ihr Betriebssystem konnte keine Anwendung für den Link finden. External link confirmation Bestätigung für externe Links An external application will be opened to visit a non-Matrix link: %1 Is that right? Eine externe Anwendung wird geöffnet, um einen Nicht-Matrix-Link zu besuchen: % 1 Ist das richtig? Do not ask again Nicht erneut fragen Malformed or empty Matrix id Fehlerhafte oder leere Matrix-ID %1 is not a correct Matrix identifier %1 ist keine korrekte Matrix-ID Please connect to a server Bitte verbinden Sie sich mit einem Server Confirm your account to open a direct chat with %1 Bestätige dein Konto, um einen direkten Chat mit %1 zu eröffnen. User &profiles... &Benutzerprofile … Log&out A&bmelden Invite events Einladungsereignisse Show invite and withdrawn invitation events Einladungs- und zurückgezogene Einladungsereignisse anzeigen Ban events Verbannungs-Ereignisse Show ban and unban events Ver- und Entbannungsereignisse anzeigen Changes in display na&me Änderungen im Displaynamen Show display name change Änderungen des Anzeigenamens anzeigen Avatar &changes Avataränderungen Show avatar update events Änderungen des Avatars anzeigen Room alias &updates Änderungen des Raumnamens Show room alias updates events Änderungen des Raumnamens anzeigen Un&known event types Unbekannte Ereignistypen Show/hide unknown event types Unbekannte Ereignistypen anzeigen/ausblenden Tags can be wildcarded by * next to dot(s) Clear the box to reset to defaults Special tags starting with "im.quotient." are: %1 User-defined tags should start with "u." In Tags kann ein * neben Punkt(en) als Platzhalter eingesetzt werden. Entferne das Häkchen, um auf die Standardeinstellungen zurückzusetzen. Spezielle Tags, die mit „im.quotient.“ beginnen, sind: %1 Benutzerdefinierte Etiketten sollten mit „u.“ beginnen. &About Quaternion &Über Quaternion About &Qt Über &Qt Use Breeze style (requires restart) Breeze-Stil verwenden (Neustart erforderlich) Force use Breeze style and icon theme Verwendung des Breeze-Stils und Symbolthemas erzwingen Chat with user Chat mit Benutzer Can't open Kann nicht geöffnet werden Could not resolve id ID konnte nicht aufgelöst werden Could not find an external application to open the URI: Es wurde keine externe Anwendung zum Öffnen des URI gefunden: Could not resolve Matrix identifier Matrix-ID konnte nicht aufgelöst werden Incorrect action on a Matrix resource Falsche Aktion für eine Matrix-Ressource The URI contains an action '%1' that cannot be applied to Matrix resource %2 Der URI enthält eine Aktion '%1', die nicht auf die Matrix-Ressource %2 angewendet werden kann Failed to resolve server %1 Server %1 konnte nicht aufgelöst werden Room or user ID, room alias, Matrix URI or matrix.to link Raum- oder Benutzer-ID, Raumalias, Matrix URI oder matrix.to Link Go to room Gehe zu Raum Join room Raum betreten Quaternion project contributors Projektmitwirkende Quaternion Felix Rohrbach Felix Rohrbach Alexey "Kitsune" Rusakov Alexey "Kitsune" Rusakov Confirm opening external links Öffnen von externen Links bestätigen Show a confirmation box before opening non-Matrix links in an external application Zeige ein Bestätigungsfeld vor dem Öffnen von Nicht-Matrix-Links in einer externen Anwendung an MessageEventModel Today Heute Yesterday Gestern The day before yesterday Vorgestern Redacted Entfernt Redacted: %1 Entfernt: %1 a file eine Datei invited %1 to the room lud %1 in den Raum ein joined the room trat dem Raum bei cleared the display name löschte den Anzeigenamen changed the display name to %1 änderte den Anzeigenamen zu %1 cleared the avatar entfernte das Profilbild updated the avatar änderte das Profilbild unbanned %1 hob Verbannung von %1 auf self-unbanned hob Verbannung von sich selbst auf left the room verließ den Raum self-banned from the room verbannte sich selbst aus dem Raum knocked klopfte an made something unknown tat etwas Unbekanntes cleared the room main alias entfernte den Haupt-Alias des Raumes set the room main alias to: %1 setzte den Haupt-Alias des Raumes auf: %1 cleared the room name entfernte den Raumnamen set the room name to: %1 setzte den Raumnamen auf: %1 cleared the topic entfernte das Thema set the topic to: %1 setzte das Thema auf: %1 changed the room avatar änderte das Raumbild activated End-to-End Encryption aktivierte Ende-zu-Ende-Verschlüsselung withdrew %1's invitation zog %1s Einladung zurück rejected the invitation lehnte die Einladung ab updated the database aktualisierte die Datenbank. updated %1 state aktualisierte %1-Status updated %1 state for %2 aktualisierte %1-Status für %2. Unknown event Unbekanntes Ereignis upgraded the room to version %1 aktualisierte den Raum auf Version %1 created the room, version %1 erstellte den Raum, Version %1 has set room aliases on server %1 to: %2 hat Raum-Aliase auf dem Server %1 auf %2 gesetzt banned %1 from the room: %2 verbannte %1 aus dem Raum: %2 kicked %1 from the room: %2 hat %1 aus dem Raum entfernt: %2 upgraded the room: %1 aktualisierte den Raum: %1 and und %Ln more member(s) %Ln weiteres Mitglied %Ln weitere Mitglieder (repeated) (wiederholt) kicked %1 from the room hat %1 aus dem Raum entfernt NetworkConfigDialog Network proxy settings Netzwerk Proxy-Einstellungen &Override system defaults &System-Einstellungen überschreiben &No proxy &Kein Proxy &HTTP(S) proxy &HTTP(S) Proxy &SOCKS5 proxy &SOCKS5 Proxy Host Host Port Port User name Benutzername RoomDialogBase Publish room in room directory Raum im Raumverzeichnis veröffentlichen Allow guest accounts to join the room Erlaube Gastkonten diesen Raum zu betreten Account Konto Room name Raumname Primary alias Primärer Alias Topic Thema About room versions Über Raumversionen (loading) (Laden) default Standard stable stabil Room version Raumversion Continue with unstable version? Mit instabiler Version fortfahren? You are using an UNSTABLE room version (%1). The server may stop supporting it at any moment. Do you still want to use this version? Sie verwenden eine INSTABILE Raumversion (%1). Der Server kann die Unterstützung jederzeit einstellen. Möchten Sie diese Version noch verwenden? RoomListDock Mark room as read Raum als gelesen markieren Add tags... Tags hinzufügen … Join room Raum betreten Forget room Raum vergessen Remove tag Tag entfernen Reject invitation Einladung ablehnen Leave room Raum verlassen Enter new tags for the room Neue Tags für den Raum eingeben Enter tags to add to this room, one tag per line Gebe Tags für den Raum ein, ein Tag pro Zeile Change room &settings... Ändere Raum-&Einstellungen … Add Hinzufügen Copy room link to clipboard Raumlink in die Zwischenablage kopieren Rooms (%L1) Räume (%L1) RoomListModel Invited Eingeladen Low priority Niedrige Priorität People Personen Ungrouped rooms Nicht-gruppierte Räume Left Verlassen %1 (%Ln room(s)) %1 (%Ln Raum) %1 (%Ln Räume) %1 (as %2) %1 (als %2) You joined this room Sie sind diesem Raum beigetreten You left this room Sie haben diesen Raum verlassen You were invited into this room Sie wurden in diesen Raum eingeladen Main alias: %1 Hauptalias: %1 Direct chat with %1 Direkt-Chat mit %1 The room enforces encryption Der Raum erzwingt Verschlüsselung ID: %1 ID: %1 Favourites Favoriten This room's version is unstable! Die Version dieses Raumes ist instabil! Consider upgrading to a stable version (use room settings for that) Erwägen Sie ein Upgrade auf eine stabile Version (verwenden Sie dafür die Raumeinstellungen). Server notices Serverbenachrichtigungen Joined: %L1 Beigetreten: %L1 Invited: %L1 Eingeladen: %L1 Unread messages: %L1 Ungelesene Nachrichten: %L1 Unread highlights: %L1 Ungelesene Hervorhebungen: %L1 Unread notifications: %L1 Ungelesene Benachrichtigungen: %L1 (maybe more) (vielleicht mehr) RoomSettingsDialog Room settings: %1 Raum-Einstellungen: %1 Update room Raum aktualisieren Tags Tags This version is unstable! Consider upgrading. Diese Version ist instabil! Erwägen Sie ein Upgrade. Upgrade Aktualisierung Choose new room version Neue Raumversion auswählen You are about to upgrade %1. This operation cannot be reverted. Sie sind dabei, %1 zu aktualisieren. Dieser Vorgang kann nicht rückgängig gemacht werden. Creating the new room version, please wait Die neue Raumversion wird erstellt, bitte warten Room identifier Raum-ID UserListDock Users Benutzer Open direct chat Öffne Direkt-Chat Mention user Benutzer erwähnen Search Suchen Ignore user Benutzer ignorieren Kick user Benutzer herauswerfen Ban user Benutzer verbannen Kick %1 %1 herauswerfen Reason Grund Ban %1 %1 verbannen (%L1 out of %L2) (%L1 von %L2) FileContent Size: %1, declared type: %2 Größe: %1, angegebener Typ: %2 Open after downloading Nach dem Herunterladen öffnen Cancel Abbrechen Save as... Speichern unter … Open Öffnen Open folder Ordner öffnen uploaded from %1 hochgeladen von %1 being uploaded from %1 lädt hoch von %1 downloaded to %1 nach %1 heruntergeladen TimelineItem Resend Erneut senden Discard Verwerfen edited bearbeitet Go to older room Gehe zu älterem Raum Go to new room Gehe zu neuem Raum Reaction '%1' from %2 Reaktion '%1' von %2 main Quaternion - an IM client for the Matrix protocol Quaternion – ein Chat-Client für das Matrix-Protokoll Override locale Sprache überschreiben locale Sprache Hide main window on startup Starte mit verstecktem Hauptfenster SystemTrayIcon Highlight in %1 Hervorhebung in %1 Hide Ausblenden Quit Beenden Show Anzeigen %Ln highlight(s) %Ln Hervorhebung %Ln Hervorhebungen ThumbnailResponse Image request has been cancelled Bildanfrage wurde abgebrochen Media id '%1' doesn't follow server/mediaId pattern Die Medien-ID '% 1' folgt nicht dem Server/Medien-ID-Muster No connection to perform image request Keine Verbindung zum Durchführen einer Bildanforderung Image request is pending Bildanforderung ist ausstehend TimelineWidget Referenced message not found Referenzierte Nachricht nicht gefunden Copy permalink to clipboard Permalink in die Zwischenablage kopieren Show details Zeige Details Open Folder Ordner öffnen Download Herunterladen Save file as... Datei speichern unter … Copy selected text to clipboard Markierten Text in die Zwischenablage kopieren Copy image to clipboard Bild in die Zwischenablage kopieren Save file as Datei speichern unter Redact Entfernen Copy link to clipboard Link in Zwischenablage kopieren Quote Zitieren Open externally Extern öffnen ProfileDialog This is the current device Dies ist das aktuelle Gerät Device display name Anzeigename des Geräts Device ID Geräte-ID Last time seen Zuletzt gesehen Last IP address Letzte IP-Adresse User profiles Benutzerprofile Account Konto Display Name Anzeigename Copy to clipboard In die Zwischenablage kopieren Access token Zugriffstoken Apply and close Übernehmen und schließen Loading other devices... Lade andere Geräte … No avatar Kein Avatar Set avatar Avatar einstellen ChatEdit Reset formatting Formatierung zurücksetzen Reset the current character formatting to the default Zurücksetzen der aktuellen Zeichenformatierung auf die Standardeinstellung Quaternion-0.0.95.1/client/translations/quaternion_en.ts000066400000000000000000002621241412757327200233300ustar00rootroot00000000000000 ChatEdit Reset formatting Reset formatting Reset the current character formatting to the default Reset the current character formatting to the default Could not insert HTML - it's either invalid or unsupported ChatRoomWidget Choose a room to send messages or enter a command... Choose a room to send messages or enter a command... Attach Attach Attach file Attach file Add a message to the file or just push Enter Add a message to the file or just push Enter Attaching %1 Attaching %1 Attaching cancelled Attaching cancelled Attaching an image from clipboard Attaching an image from clipboard No completions No completions %Ln more completions %Ln more completion %Ln more completions Next completion: Next completion: Currently typing: Currently typing: Send a message (no end-to-end encryption support yet)... Send a message (no end-to-end encryption support yet)... Send a message (over %1) or enter a command... %1 is the protocol used by the server (usually HTTPS) Send a message (over %1) or enter a command... There's nothing to send There's nothing to send /join argument doesn't look like a room ID or alias /join argument doesn't look like a room ID or alias There's no such /command outside of room. There's no such /command outside of room. At pos %1: %2 %1 is a position of the error; %2 is the error message At pos %1: %2 You should select a room to send messages. You should select a room to send messages. Sending a farewell message is not supported yet. If you intended to leave another room, switch to it and type /leave there. Sending a farewell message is not supported yet. If you intended to leave another room, switch to it and type /leave there. %L1 more The number of users in the typing or completion list %L1 more /forget must be followed by the room id/alias, even for the current room /forget must be followed by the room id/alias, even for the current room %1 doesn't look like a room id or alias %1 doesn't look like a room id or alias /invite <memberId> /invite <memberId> %1 doesn't look like a user ID %1 doesn't look like a user ID /%1 <userId> <reason> /%1 <userId> <reason> %1 doesn't look like a user id %1 doesn't look like a user id %1 is not a member of this room %1 is not a member of this room /unban <userId> /unban <userId> /unban argument doesn't look like a user ID /unban argument doesn't look like a user ID /ignore <userId> /ignore <userId> /ignore argument doesn't look like a user ID /ignore argument doesn't look like a user ID Couldn't find user %1 on the server Couldn't find user %1 on the server /me needs an argument /me needs an argument /notice needs an argument /notice needs an argument /%1 <memberId> <message> /%1 <memberId> <message> %1 doesn't seem to have joined room %2 %1 doesn't seem to have joined room %2 %1 doesn't look like a user id or room alias %1 doesn't look like a user id or room alias Your build of Quaternion doesn't support Markdown Your build of Quaternion doesn't support Markdown /%1 <memberId> /%1 <memberId> Unknown /command. Use // to send this line literally Unknown /command. Use // to send this line literally CreateRoomDialog Create room Create room Add Add a user to the list of invitees Add Please fill the fields as desired. None are mandatory Please fill the fields as desired. None are mandatory Invite user(s) Invite user(s) Creating the room, please wait Creating the room, please wait Dialog Applying changes, please wait Applying changes, please wait FileContent Size: %1, declared type: %2 Size: %1, declared type: %2 uploaded from %1 %1 is a local file name uploaded from %1 being uploaded from %1 %1 is a local file name being uploaded from %1 downloaded to %1 %1 is a local file name downloaded to %1 Open after downloading Open after downloading Cancel Cancel Save as... Save as... Open Open Open folder Open folder LoginDialog Login Login Stay logged in Stay logged in Resolving the homeserver... Resolving the homeserver... The server URL doesn't look valid The server URL doesn't look valid Connecting and logging in, please wait Connecting and logging in, please wait Login with SSO Login with SSO Re-login Re-login Restoring access, please wait Restoring access, please wait Getting supported login flows... Getting supported login flows... The homeserver is available The homeserver is available Could not connect to the homeserver Could not connect to the homeserver Matrix ID Matrix ID Password Password Device name Device name Connect to server Connect to server No supported login flows No supported login flows Single sign-on Single sign-on Quaternion couldn't automatically open the single sign-on URL. Please copy and paste it to the right application (usually a web browser): Quaternion couldn't automatically open the single sign-on URL. Please copy and paste it to the right application (usually a web browser): After authentication, the browser will follow the temporary local address setup by Quaternion to conclude the login sequence. After authentication, the browser will follow the temporary local address setup by Quaternion to conclude the login sequence. MainWindow Loading... Loading... &Accounts &Accounts &Login... &Login... User &profiles... User &profiles... Log&out Log&out &Quit &Quit &View &View Dock &panels Panels of the dock, not 'to dock the panels' Dock &panels Show/hide Rooms dock panel Show/hide Rooms dock panel Show/hide Users dock panel Show/hide Users dock panel &Display in timeline &Display in timeline Invite events Invite events Show invite and withdrawn invitation events Show invite and withdrawn invitation events Normal &join/leave events Normal &join/leave events Show join and leave events Show join and leave events Ban events Ban events Show ban and unban events Show ban and unban events &Redacted events &Redacted events Show redacted events in the timeline as 'Redacted' instead of hiding them entirely Show redacted events in the timeline as 'Redacted' instead of hiding them entirely Changes in display na&me Changes in display na&me Show display name change Show display name change Avatar &changes Avatar &changes Show avatar update events Show avatar update events Room alias &updates Room alias &updates Show room alias updates events Show room alias updates events Show/hide meaningless activity (join-leave pairs and redacted events between) Show/hide meaningless activity (join-leave pairs and redacted events between) Un&known event types Un&known event types Show/hide unknown event types Show/hide unknown event types Edit tags order Edit tags order Tags can be wildcarded by * next to dot(s) Clear the box to reset to defaults Special tags starting with "im.quotient." are: %1 User-defined tags should start with "u." Tags can be wildcarded by * next to dot(s) Clear the box to reset to defaults Special tags starting with "im.quotient." are: %1 User-defined tags should start with "u." Edit quote style Edit quote style Markdown (prepend each line with >) Markdown (prepend each line with >) Custom (apply regex from the config file) Custom (apply regex from the config file) Locale's default (%1) Locale's default (%1) Example quote Example quote Choose the default style of quotes Choose the default style of quotes &Room &Room Change room &settings... Change room &settings... Create &new room... Create &new room... &No-effect activity A menu item to show/hide meaningless activity such as redacted spam &No-effect activity &Join room... &Join room... Open room... Open room... Open a room from the room list Open a room from the room list &Close current room &Close current room &Settings &Settings &Help &Help &About Quaternion &About Quaternion About &Qt About &Qt &Highlight only &Highlight only Notifications are entirely suppressed Notifications are entirely suppressed &Non-intrusive &Non-intrusive Show notifications but do not activate the window Show notifications but do not activate the window &Full &Full Show notifications and activate the window Show notifications and activate the window Notifications Notifications Default Default The layout with author labels above blocks of messages The layout with author labels above blocks of messages The layout with author labels to the left from each message The layout with author labels to the left from each message Timeline layout Timeline layout Use Breeze style (requires restart) Use Breeze style (requires restart) Force use Breeze style and icon theme Force use Breeze style and icon theme Use shuttle scrollbar (requires restart) Use shuttle scrollbar (requires restart) Control scroll velocity instead of position with the timeline scrollbar Control scroll velocity instead of position with the timeline scrollbar Load full-size images at once Load full-size images at once Automatically download a full-size image instead of a thumbnail Automatically download a full-size image instead of a thumbnail Close to tray Close to tray Make close button [X] minimize to tray instead of closing main window Make close button [X] minimize to tray instead of closing main window Confirm opening external links Confirm opening external links Show a confirmation box before opening non-Matrix links in an external application Show a confirmation box before opening non-Matrix links in an external application Configure &network proxy... Configure &network proxy... Access token file found Access token file found Do you want to migrate the access token for %1 from the file to the keychain? Do you want to migrate the access token for %1 from the file to the keychain? Couldn't migrate access token Couldn't migrate access token Quaternion couldn't migrate access token for %1 from the file to the keychain. Quaternion couldn't migrate access token for %1 from the file to the keychain. Couldn't open a file to save access token Couldn't open a file to save access token Quaternion couldn't open a file to write the access token to. You're logged in but will have to provide your password again when you restart the application. Quaternion couldn't open a file to write the access token to. You're logged in but will have to provide your password again when you restart the application. Couldn't set access token file permissions Couldn't set access token file permissions Quaternion couldn't restrict permissions on the access token file. Do you still want to save the access token to it? Quaternion couldn't restrict permissions on the access token file. Do you still want to save the access token to it? Couldn't save access token Couldn't save access token Quaternion couldn't save the access token to the keychain. Do you want to save the access token to file %1? Quaternion couldn't save the access token to the keychain. Do you want to save the access token to file %1? First sync completed for %1 %1 is user id First sync completed for %1 Logged out as %1 Logged out as %1 Sync failed Sync failed The last sync of account %1 has failed with error: %2 The last sync of account %1 has failed with error: %2 The last sync has failed with error: %1 The last sync has failed with error: %1 Clicking 'Retry' will attempt to resume synchronisation; Clicking 'Cancel' will stop further synchronisation of this account until logout or Quaternion restart. Clicking 'Retry' will attempt to resume synchronisation; Clicking 'Cancel' will stop further synchronisation of this account until logout or Quaternion restart. Before this server can process your information, you have to agree with its terms and conditions; please click the button below to open the web page where you can do that Before this server can process your information, you have to agree with its terms and conditions; please click the button below to open the web page where you can do that Request URL: %1 Response: %2 Request URL: %1 Response: %2 Open web page Open web page Logging in into a logged in account Logging in into a logged in account You're trying to log in into an account that's already logged in. Do you want to continue? You're trying to log in into an account that's already logged in. Do you want to continue? About Quaternion About Quaternion &About &About Web page Web page Quaternion project contributors Quaternion project contributors Built from Git, commit SHA: Built from Git, commit SHA: Library commit SHA: Library commit SHA: Original project author: %1 Original project author: %1 Felix Rohrbach Felix Rohrbach Project leader: %1 Project leader: %1 Alexey "Kitsune" Rusakov Alexey "Kitsune" Rusakov Contributors: Contributors: Quaternion contributors @ GitHub Quaternion contributors @ GitHub libQuotient contributors @ GitHub libQuotient contributors @ GitHub Quaternion translators @ Lokalise.co Quaternion translators @ Lokalise.co Special thanks to %1 for all the testing effort Special thanks to %1 for all the testing effort Made with: Made with: &Thanks &Thanks Failed to resolve server %1 Failed to resolve server %1 Welcome to Quaternion Welcome to Quaternion Couldn't delete access token Couldn't delete access token Quaternion couldn't delete the access token from the keychain. Quaternion couldn't delete the access token from the keychain. Open direct chat? Open direct chat? Open direct chat with user %1? Open direct chat with user %1? Joined %1 as %2 Joined %1 as %2 No application for the link No application for the link Your operating system could not find an application for the link. Your operating system could not find an application for the link. External link confirmation External link confirmation An external application will be opened to visit a non-Matrix link: %1 Is that right? An external application will be opened to visit a non-Matrix link: %1 Is that right? Do not ask again Do not ask again Malformed or empty Matrix id Malformed or empty Matrix id %1 is not a correct Matrix identifier %1 is not a correct Matrix identifier Please connect to a server Please connect to a server Confirm account to join %1 Confirm account to join %1 Confirm your account to open a direct chat with %1 Confirm your account to open a direct chat with %1 Confirm your account to open %1 Confirm your account to open %1 Room not found Room not found There's no room %1 in the room list. Check the spelling and the account. There's no room %1 in the room list. Check the spelling and the account. Confirm account Confirm account Open room Open room Room or user ID, room alias, Matrix URI or matrix.to link Room or user ID, room alias, Matrix URI or matrix.to link Go to room Go to room Join room Join room Room ID (starting with !) or alias (starting with #) Room ID (starting with !) or alias (starting with #) Account Account Chat with user On a button in 'Open room' dialog when a user identifier is entered Chat with user Can't open On a disabled button in 'Open room' dialog when an invalid/unsupported URI is entered Can't open Could not resolve id Could not resolve id Could not find an external application to open the URI: Could not find an external application to open the URI: Could not resolve Matrix identifier Could not resolve Matrix identifier Incorrect action on a Matrix resource Incorrect action on a Matrix resource The URI contains an action '%1' that cannot be applied to Matrix resource %2 The URI contains an action '%1' that cannot be applied to Matrix resource %2 Couldn't connect to the server as %1; will retry within %2 seconds Couldn't connect to the server as %1; will retry within %2 seconds Reconnecting... Reconnecting... No SSL support No SSL support Your SSL configuration does not allow Quaternion to establish secure connections. Your SSL configuration does not allow Quaternion to establish secure connections. SSL error SSL error Proxy needs authentication Proxy needs authentication Authenticate Authenticate with the proxy server Authenticate User name User name Password Password MessageEventModel Today Today Yesterday Yesterday The day before yesterday The day before yesterday Redacted Redacted Redacted: %1 Redacted: %1 a file a file invited %1 to the room invited %1 to the room joined the room joined the room (repeated) State event that doesn't change the state (repeated) cleared the display name cleared the display name changed the display name to %1 changed the display name to %1 and and cleared the avatar cleared the avatar updated the avatar updated the avatar withdrew %1's invitation withdrew %1's invitation rejected the invitation rejected the invitation unbanned %1 unbanned %1 self-unbanned self-unbanned kicked %1 from the room kicked %1 from the room kicked %1 from the room: %2 kicked %1 from the room: %2 left the room left the room banned %1 from the room banned %1 from the room banned %1 from the room: %2 banned %1 from the room: %2 self-banned from the room self-banned from the room knocked knocked made something unknown made something unknown has set room aliases on server %1 to: %2 has set room aliases on server %1 to: %2 cleared the room main alias cleared the room main alias set the room main alias to: %1 set the room main alias to: %1 cleared the room name cleared the room name set the room name to: %1 set the room name to: %1 cleared the topic cleared the topic set the topic to: %1 set the topic to: %1 changed the room avatar changed the room avatar activated End-to-End Encryption activated End-to-End Encryption upgraded the room to version %1 upgraded the room to version %1 created the room, version %1 created the room, version %1 upgraded the room: %1 upgraded the room: %1 updated the database TWIM bot updated the database updated the database updated %1 state %1 - Matrix event type updated %1 state updated %1 state for %2 %1 - Matrix event type, %2 - state key updated %1 state for %2 Unknown event Unknown event %Ln more member(s) When the reaction comes from too many members %Ln more member %Ln more members NetworkConfigDialog Network proxy settings Network proxy settings &Override system defaults &Override system defaults &No proxy &No proxy &HTTP(S) proxy &HTTP(S) proxy &SOCKS5 proxy &SOCKS5 proxy Host Host Port Port User name User name ProfileDialog This is the current device This is the current device Device display name Device display name Device ID Device ID Last time seen Last time seen Last IP address Last IP address User profiles User profiles Account Account Display Name Display Name Copy to clipboard Copy to clipboard Access token Access token Apply and close Apply and close Loading other devices... Loading other devices... No avatar No avatar Set avatar Set avatar RoomDialogBase Publish room in room directory Publish room in room directory Allow guest accounts to join the room Allow guest accounts to join the room Room name Room name Primary alias Primary alias Topic Topic About room versions About room versions (loading) Loading room versions from the server (loading) default Default room version default stable Stable room version stable Account Account Room version Room version Continue with unstable version? Continue with unstable version? You are using an UNSTABLE room version (%1). The server may stop supporting it at any moment. Do you still want to use this version? You are using an UNSTABLE room version (%1). The server may stop supporting it at any moment. Do you still want to use this version? RoomListDock Mark room as read Mark room as read Add tags... Add tags... Change room &settings... Change room &settings... Copy room link to clipboard Copy room link to clipboard Join room Join room Forget room Forget room Remove tag Remove tag Reject invitation Reject invitation Leave room Leave room Enter new tags for the room Enter new tags for the room Add A caption on a button to add tags Add Enter tags to add to this room, one tag per line Enter tags to add to this room, one tag per line Rooms (%L1) Rooms (%L1) RoomListModel Invited The caption for invitations Invited Favourites Favourites Low priority Low priority Server notices Server notices People The caption for direct chats People Ungrouped rooms Ungrouped rooms Left The caption for left rooms Left %1 (%Ln room(s)) %1 (%Ln room) %1 (%Ln rooms) %1 (as %2) %Room (as %user) %1 (as %2) Main alias: %1 Main alias: %1 Joined: %L1 The number of joined members Joined: %L1 Invited: %L1 The number of invited users Invited: %L1 Direct chat with %1 Direct chat with %1 The room enforces encryption The room enforces encryption This room's version is unstable! This room's version is unstable! Consider upgrading to a stable version (use room settings for that) Consider upgrading to a stable version (use room settings for that) Unread messages: %L1 Unread messages: %L1 (maybe more) Unread messages (maybe more) Unread highlights: %L1 Unread highlights: %L1 Unread notifications: %L1 Unread notifications: %L1 ID: %1 ID: %1 You joined this room You joined this room You left this room You left this room You were invited into this room You were invited into this room RoomSettingsDialog Room settings: %1 Room settings: %1 Update room Update room This version is unstable! Consider upgrading. This version is unstable! Consider upgrading. Upgrade Upgrade a room version Upgrade Choose new room version Choose new room version You are about to upgrade %1. This operation cannot be reverted. You are about to upgrade %1. This operation cannot be reverted. Tags Tags Room identifier Room identifier Creating the new room version, please wait Creating the new room version, please wait SystemTrayIcon Hide Hide Quit Quit Show Show Highlight in %1 %1 is the room display name Highlight in %1 %Ln highlight(s) %Ln highlight %Ln highlights ThumbnailResponse No connection to perform image request No connection to perform image request Media id '%1' doesn't follow server/mediaId pattern Media id '%1' doesn't follow server/mediaId pattern Image request is pending Image request is pending Image request has been cancelled Image request has been cancelled Timeline Unknown Unknown attachment size Unknown %Ln byte(s) %Ln byte %Ln bytes %L1 kB %L1 kB %L1 MB %L1 MB %L1 GB %L1 GB (no name) (no name) This room has been upgraded. This room has been upgraded. Unstable room version! Unstable room version! (no topic) (no topic) Hide topic Hide topic Show topic Show topic Go to new room Go to new room Room settings Room settings Latest events Latest events %Ln events back from now %Ln event back from now %Ln events back from now %Ln events cached %Ln event cached %Ln events cached %Ln events requested from the server %Ln event requested from the server %Ln events requested from the server TimelineItem edited edited Reaction '%1' from %2 %2 is the list of users Reaction '%1' from %2 Resend Resend Discard Discard Go to older room Go to older room Go to new room Go to new room TimelineWidget Referenced message not found Referenced message not found Save file as Save file as Redact Redact Copy selected text to clipboard Copy selected text to clipboard Copy link to clipboard Copy link to clipboard Copy permalink to clipboard Copy permalink to clipboard Quote a verb (do quote), not a noun (a quote) Quote Show details Show details Open externally Open externally Open Folder Open Folder Copy image to clipboard Copy image to clipboard Download Download Save file as... Save file as... UserListDock Users Users Search Search (%L1 out of %L2) %found out of %total users (%L1 out of %L2) Open direct chat Open direct chat Mention user Mention user Ignore user Ignore user Kick user Kick user Ban user Ban user Kick %1 Kick %1 Reason Reason Ban %1 Ban %1 main Quaternion - an IM client for the Matrix protocol Quaternion - an IM client for the Matrix protocol Override locale Override locale locale locale Hide main window on startup Hide main window on startup Quaternion-0.0.95.1/client/translations/quaternion_en_GB.ts000066400000000000000000001676761412757327200237200ustar00rootroot00000000000000 ChatRoomWidget Choose a room to send messages or enter a command... Choose a room to send messages or enter a command... There's nothing to send There's nothing to send /join argument doesn't look like a room ID or alias /join argument doesn't look like a room ID or alias Sending a farewell message is not supported yet. If you intended to leave another room, switch to it and type /leave there. Sending a farewell message is not supported yet. If you intended to leave another room, switch to it and type /leave there. /forget must be followed by the room id/alias, even for the current room /forget must be followed by the room id/alias, even for the current room %1 doesn't look like a room id or alias %1 doesn't look like a room id or alias /invite <memberId> /invite <memberId> /%1 <userId> <reason> /%1 <userId> <reason> %1 is not a member of this room %1 is not a member of this room /unban <userId> /unban <userId> /unban argument doesn't look like a user ID /unban argument doesn't look like a user ID /ignore <userId> /ignore <userId> /ignore argument doesn't look like a user ID /ignore argument doesn't look like a user ID Couldn't find user %1 on the server Couldn't find user %1 on the server /me needs an argument /me needs an argument /notice needs an argument /notice needs an argument /%1 <memberId> <message> /%1 <memberId> <message> %1 doesn't seem to have joined room %2 %1 doesn't seem to have joined room %2 %1 doesn't look like a user id or room alias %1 doesn't look like a user id or room alias /%1 <memberId> /%1 <memberId> Unknown /command. Use // to send this line literally Unknown /command. Use // to send this line literally Attach Attach Attach file Attach file Add a message to the file or just push Enter Add a message to the file or just push Enter Attaching %1 Attaching %1 Attaching cancelled Attaching cancelled There's no such /command outside of room. There's no such /command outside of room. %1 doesn't look like a user id %1 doesn't look like a user id %1 doesn't look like a user ID %1 doesn't look like a user ID You should select a room to send messages. You should select a room to send messages. Send a message (over %1) or enter a command... Send a message (over %1) or enter a command... Attaching an image from clipboard Attaching an image from clipboard Send a message (no end-to-end encryption support yet)... Send a message (no end-to-end encryption support yet)... Your build of Quaternion doesn't support Markdown Your build of Quaternion doesn't support Markdown No completions No completions %Ln more completions %Ln more completion %Ln more completions Next completion: Next completion: Currently typing: Currently typing: At pos %1: %2 At pos %1: %2 %L1 more %L1 more Timeline (no topic) (no topic) Unknown Unknown Unstable room version! Unstable room version! (no name) (no name) %Ln byte(s) %Ln byte %Ln bytes %L1 MB %L1 MB %L1 GB %L1 GB This room has been upgraded. This room has been upgraded. Go to new room Go to new room Room settings Room settings Latest events Latest events %Ln events back from now %Ln events back from now %Ln events back from now %L1 kB %L1 kB %Ln events cached %Ln event cached %Ln events cached %Ln events requested from the server %Ln event requested from the server %Ln events requested from the server Hide topic Hide topic Show topic Show topic CreateRoomDialog Create room Create room Add Add Invite user(s) Invite user(s) Creating the room, please wait Creating the room, please wait Please fill the fields as desired. None are mandatory Please fill the fields as desired. None are mandatory Dialog Applying changes, please wait Applying changes, please wait LoginDialog Login Login Stay logged in Stay logged in Matrix ID Matrix ID Password Password Device name Device name Connect to server Connect to server Connecting and logging in, please wait Connecting and logging in, please wait Re-login Re-login Restoring access, please wait Restoring access, please wait Resolving the homeserver... Resolving the homeserver... The server URL doesn't look valid The server URL doesn't look valid Login with SSO Login with SSO The homeserver is available The homeserver is available Could not connect to the homeserver Could not connect to the homeserver No supported login flows No supported login flows Single sign-on Single sign-on Quaternion couldn't automatically open the single sign-on URL. Please copy and paste it to the right application (usually a web browser): Quaternion couldn't automatically open the single sign-on URL. Please copy and paste it to the right application (usually a web browser): After authentication, the browser will follow the temporary local address setup by Quaternion to conclude the login sequence. After authentication, the browser will follow the temporary local address setup by Quaternion to conclude the login sequence. Getting supported login flows... Getting supported login flows... MainWindow Loading... Loading... &Accounts &Accounts &Login... &Login... &Quit &Quit &View &View Dock &panels Dock &panels &Display in timeline &Display in timeline Normal &join/leave events Normal &join/leave events &Redacted events &Redacted events Show redacted events in the timeline as 'Redacted' instead of hiding them entirely Show redacted events in the timeline as 'Redacted' instead of hiding them entirely &No-effect activity &No-effect activity Edit tags order Edit tags order &Room &Room Change room &settings... Change room &settings... Create &new room... Create &new room... &Join room... &Join room... &Close current room &Close current room &Settings &Settings &Help &Help &About &About &Highlight only &Highlight only Notifications are entirely suppressed Notifications are entirely suppressed &Non-intrusive &Non-intrusive Show notifications but do not activate the window Show notifications but do not activate the window &Full &Full Show notifications and activate the window Show notifications and activate the window Notifications Notifications Default Default The layout with author labels above blocks of messages The layout with author labels above blocks of messages The layout with author labels to the left from each message The layout with author labels to the left from each message Timeline layout Timeline layout Load full-size images at once Load full-size images at once Automatically download a full-size image instead of a thumbnail Automatically download a full-size image instead of a thumbnail Configure &network proxy... Configure &network proxy... Couldn't open a file to save access token Couldn't open a file to save access token Quaternion couldn't open a file to write the access token to. You're logged in but will have to provide your password again when you restart the application. Quaternion couldn't open a file to write the access token to. You're logged in but will have to provide your password again when you restart the application. Couldn't set access token file permissions Couldn't set access token file permissions Quaternion couldn't restrict permissions on the access token file. Do you still want to save the access token to it? Quaternion couldn't restrict permissions on the access token file. Do you still want to save the access token to it? Logged out as %1 Logged out as %1 Sync failed Sync failed The last sync of account %1 has failed with error: %2 The last sync of account %1 has failed with error: %2 The last sync has failed with error: %1 The last sync has failed with error: %1 Clicking 'Retry' will attempt to resume synchronisation; Clicking 'Cancel' will stop further synchronisation of this account until logout or Quaternion restart. Clicking 'Retry' will attempt to resume synchronization; Clicking 'Cancel' will stop further synchronization of this account until logout or Quaternion restart. Before this server can process your information, you have to agree with its terms and conditions; please click the button below to open the web page where you can do that Before this server can process your information, you have to agree with its terms and conditions; please click the button below to open the web page where you can do that Open web page Open web page About Quaternion About Quaternion Welcome to Quaternion Welcome to Quaternion Joined %1 as %2 Joined %1 as %2 Couldn't connect to the server as %1; will retry within %2 seconds Couldn't connect to the server as %1; will retry within %2 seconds Reconnecting... Reconnecting... No SSL support No SSL support Your SSL configuration does not allow Quaternion to establish secure connections. Your SSL configuration does not allow Quaternion to establish secure connections. SSL error SSL error Proxy needs authentication Proxy needs authentication Authenticate Authenticate User name User name Password Password &Thanks &Thanks Original project author: %1 Original project author: %1 Web page Web page Project leader: %1 Project leader: %1 Contributors: Contributors: Quaternion contributors @ GitHub Quaternion contributors @ GitHub Quaternion translators @ Lokalise.co Quaternion translators @ Lokalise.co Made with: Made with: Show join and leave events Show join and leave events Use shuttle scrollbar (requires restart) Use shuttle scrollbar (requires restart) Control scroll velocity instead of position with the timeline scrollbar Control scroll velocity instead of position with the timeline scrollbar Request URL: %1 Response: %2 Request URL: %1 Response: %2 Close to tray Close to tray Make close button [X] minimize to tray instead of closing main window Make close button [X] minimize to tray instead of closing main window Show/hide meaningless activity (join-leave pairs and redacted events between) Show/hide meaningless activity (join-leave pairs and redacted events between) Built from Git, commit SHA: Built from Git, commit SHA: Library commit SHA: Library commit SHA: Open room... Open room... Open room Open room Open a room from the room list Open a room from the room list Show/hide Rooms dock panel Show/hide Rooms dock panel Show/hide Users dock panel Show/hide Users dock panel Access token file found Access token file found Couldn't migrate access token Couldn't migrate access token Couldn't save access token Couldn't save access token Logging in into a logged in account Logging in into a logged in account You're trying to log in into an account that's already logged in. Do you want to continue? You're trying to log in into an account that's already logged in. Do you want to continue? Couldn't delete access token Couldn't delete access token Open direct chat? Open direct chat? Open direct chat with user %1? Open direct chat with user %1? Room not found Room not found There's no room %1 in the room list. Check the spelling and the account. There's no room %1 in the room list. Check the spelling and the account. Confirm your account to open %1 Confirm your account to open %1 Confirm account Confirm account Account Account Room ID (starting with !) or alias (starting with #) Room ID (starting with !) or alias (starting with #) Confirm account to join %1 Confirm account to join %1 Edit quote style Edit quote style Markdown (prepend each line with >) Markdown (prepend each line with >) Custom (apply regex from the config file) Custom (apply regex from the config file) Locale's default (%1) Locale's default (%1) Example quote Example quote Choose the default style of quotes Choose the default style of quotes Special thanks to %1 for all the testing effort Special thanks to %1 for all the testing effort libQuotient contributors @ GitHub libQuotient contributors @ GitHub Do you want to migrate the access token for %1 from the file to the keychain? Do you want to migrate the access token for %1 from the file to the keychain? Quaternion couldn't migrate access token for %1 from the file to the keychain. Quaternion couldn't migrate access token for %1 from the file to the keychain. Quaternion couldn't save the access token to the keychain. Do you want to save the access token to file %1? Quaternion couldn't save the access token to the keychain. Do you want to save the access token to file %1? First sync completed for %1 First sync completed for %1 Quaternion couldn't delete the access token from the keychain. Quaternion couldn't delete the access token from the keychain. No application for the link No application for the link Your operating system could not find an application for the link. Your operating system could not find an application for the link. External link confirmation External link confirmation An external application will be opened to visit a non-Matrix link: %1 Is that right? An external application will be opened to visit a non-Matrix link: %1 Is that right? Do not ask again Do not ask again Malformed or empty Matrix id Malformed or empty Matrix id %1 is not a correct Matrix identifier %1 is not a correct Matrix identifier Please connect to a server Please connect to a server Confirm your account to open a direct chat with %1 Confirm your account to open a direct chat with %1 User &profiles... User &profiles... Log&out Log&out Invite events Invite events Show invite and withdrawn invitation events Show invite and withdrawn invitation events Ban events Ban events Show ban and unban events Show ban and unban events Changes in display na&me Changes in display na&me Show display name change Show display name change Avatar &changes Avatar &changes Show avatar update events Show avatar update events Room alias &updates Room alias &updates Show room alias updates events Show room alias updates events Un&known event types Un&known event types Show/hide unknown event types Show/hide unknown event types Tags can be wildcarded by * next to dot(s) Clear the box to reset to defaults Special tags starting with "im.quotient." are: %1 User-defined tags should start with "u." Tags can be wildcarded by * next to dot(s) Clear the box to reset to defaults Special tags starting with "im.quotient." are: %1 User-defined tags should start with "u." &About Quaternion &About Quaternion About &Qt About &Qt Use Breeze style (requires restart) Use Breeze style (requires restart) Force use Breeze style and icon theme Force use Breeze style and icon theme Chat with user Chat with user Can't open Can't open Could not resolve id Could not resolve id Could not find an external application to open the URI: Could not find an external application to open the URI: Could not resolve Matrix identifier Could not resolve Matrix identifier Incorrect action on a Matrix resource Incorrect action on a Matrix resource The URI contains an action '%1' that cannot be applied to Matrix resource %2 The URI contains an action '%1' that cannot be applied to Matrix resource %2 Failed to resolve server %1 Failed to resolve server %1 Room or user ID, room alias, Matrix URI or matrix.to link Room or user ID, room alias, Matrix URI or matrix.to link Go to room Go to room Join room Join room Quaternion project contributors Quaternion project contributors Felix Rohrbach Felix Rohrbach Alexey "Kitsune" Rusakov Alexey "Kitsune" Rusakov Confirm opening external links Confirm opening external links Show a confirmation box before opening non-Matrix links in an external application Show a confirmation box before opening non-Matrix links in an external application MessageEventModel Today Today Yesterday Yesterday The day before yesterday The day before yesterday Redacted Redacted Redacted: %1 Redacted: %1 a file a file invited %1 to the room invited %1 to the room joined the room joined the room cleared the display name cleared the display name changed the display name to %1 changed the display name to %1 cleared the avatar cleared the avatar updated the avatar updated the avatar unbanned %1 unbanned %1 self-unbanned self-unbanned left the room left the room self-banned from the room self-banned from the room knocked knocked made something unknown made something unknown cleared the room main alias cleared the room main alias set the room main alias to: %1 set the room main alias to: %1 cleared the room name cleared the room name set the room name to: %1 set the room name to: %1 cleared the topic cleared the topic set the topic to: %1 set the topic to: %1 changed the room avatar changed the room avatar activated End-to-End Encryption activated End-to-End Encryption withdrew %1's invitation withdrew %1's invitation rejected the invitation rejected the invitation updated the database updated the database updated %1 state updated %1 state updated %1 state for %2 updated %1 state for %2 Unknown event Unknown event upgraded the room to version %1 upgraded the room to version %1 created the room, version %1 created the room, version %1 has set room aliases on server %1 to: %2 has set room aliases on server %1 to: %2 banned %1 from the room: %2 banned %1 from the room: %2 kicked %1 from the room: %2 kicked %1 from the room: %2 upgraded the room: %1 upgraded the room: %1 and and %Ln more member(s) %Ln more member %Ln more members (repeated) (repeated) kicked %1 from the room kicked %1 from the room NetworkConfigDialog Network proxy settings Network proxy settings &Override system defaults &Override system defaults &No proxy &No proxy &HTTP(S) proxy &HTTP(S) proxy &SOCKS5 proxy &SOCKS5 proxy Host Host Port Port User name User name RoomDialogBase Publish room in room directory Publish room in room directory Allow guest accounts to join the room Allow guest accounts to join the room Account Account Room name Room name Primary alias Primary alias Topic Topic About room versions About room versions (loading) (loading) default default stable stable Room version Room version Continue with unstable version? Continue with unstable version? You are using an UNSTABLE room version (%1). The server may stop supporting it at any moment. Do you still want to use this version? You are using an UNSTABLE room version (%1). The server may stop supporting it at any moment. Do you still want to use this version? RoomListDock Mark room as read Mark room as read Add tags... Add tags... Join room Join room Forget room Forget room Remove tag Remove tag Reject invitation Reject invitation Leave room Leave room Enter new tags for the room Enter new tags for the room Enter tags to add to this room, one tag per line Enter tags to add to this room, one tag per line Change room &settings... Change room &settings... Add Add Copy room link to clipboard Copy room link to clipboard Rooms (%L1) Rooms (%L1) RoomListModel Invited Invited Low priority Low priority People People Ungrouped rooms Ungrouped rooms Left Left %1 (%Ln room(s)) %1 (%Ln room) %1 (%Ln rooms) %1 (as %2) %1 (as %2) You joined this room You joined this room You left this room You left this room You were invited into this room You were invited into this room Main alias: %1 Main alias: %1 Direct chat with %1 Direct chat with %1 The room enforces encryption The room enforces encryption ID: %1 ID: %1 Favourites Favourites This room's version is unstable! This room's version is unstable! Consider upgrading to a stable version (use room settings for that) Consider upgrading to a stable version (use room settings for that) Server notices Server notices Joined: %L1 Joined: %L1 Invited: %L1 Invited: %L1 Unread messages: %L1 Unread messages: %L1 Unread highlights: %L1 Unread highlights: %L1 Unread notifications: %L1 Unread notifications: %L1 (maybe more) (maybe more) RoomSettingsDialog Room settings: %1 Room settings: %1 Update room Update room Tags Tags This version is unstable! Consider upgrading. This version is unstable! Consider upgrading. Upgrade Upgrade Choose new room version Choose new room version You are about to upgrade %1. This operation cannot be reverted. You are about to upgrade %1. This operation cannot be reverted. Creating the new room version, please wait Creating the new room version, please wait Room identifier Room identifier UserListDock Users Users Open direct chat Open direct chat Mention user Mention user Search Search Ignore user Ignore user Kick user Kick user Ban user Ban user Kick %1 Kick %1 Reason Reason Ban %1 Ban %1 (%L1 out of %L2) (%L1 out of %L2) FileContent Size: %1, declared type: %2 Size: %1, declared type: %2 Open after downloading Open after downloading Cancel Cancel Save as... Save as... Open Open Open folder Open folder uploaded from %1 uploaded from %1 being uploaded from %1 being uploaded from %1 downloaded to %1 downloaded to %1 TimelineItem Resend Resend Discard Discard edited edited Go to older room Go to older room Go to new room Go to new room Reaction '%1' from %2 Reaction '%1' from %2 main Quaternion - an IM client for the Matrix protocol Quaternion - an IM client for the Matrix protocol Override locale Override locale locale locale Hide main window on startup Hide main window on startup SystemTrayIcon Highlight in %1 Highlight in %1 Hide Hide Quit Quit Show Show %Ln highlight(s) %Ln highlight %Ln highlights ThumbnailResponse Image request has been cancelled Image request has been cancelled Media id '%1' doesn't follow server/mediaId pattern Media id '%1' doesn't follow server/mediaId pattern No connection to perform image request No connection to perform image request Image request is pending Image request is pending TimelineWidget Referenced message not found Referenced message not found Copy permalink to clipboard Copy permalink to clipboard Show details Show details Open Folder Open Folder Download Download Save file as... Save file as... Copy selected text to clipboard Copy selected text to clipboard Copy image to clipboard Copy image to clipboard Save file as Save file as Redact Redact Copy link to clipboard Copy link to clipboard Quote Quote Open externally Open externally ProfileDialog This is the current device This is the current device Device display name Device display name Device ID Device ID Last time seen Last time seen Last IP address Last IP address User profiles User profiles Account Account Display Name Display Name Copy to clipboard Copy to clipboard Access token Access token Apply and close Apply and close Loading other devices... Loading other devices... No avatar No avatar Set avatar Set avatar ChatEdit Reset formatting Reset formatting Reset the current character formatting to the default Reset the current character formatting to the default Quaternion-0.0.95.1/client/translations/quaternion_es.ts000066400000000000000000001373461412757327200233440ustar00rootroot00000000000000 ChatRoomWidget Choose a room to send messages or enter a command... Elija una sala para enviar mensajes o ingrese un comando... There's nothing to send No hay nada que enviar /join argument doesn't look like a room ID or alias /join argumento no parece un ID de habitación o alias Sending a farewell message is not supported yet. If you intended to leave another room, switch to it and type /leave there. El envío de un mensaje de despedida aún no es compatible. Si tenía la intención de salir de otra sala, cambie a ella y escriba /leave allí. /forget must be followed by the room id/alias, even for the current room /forget debe ser seguido por el id/alias de la sala, incluso para la sala actual %1 doesn't look like a room id or alias %1 no parece un identificador de sala o alias /invite <memberId> /invite <memberId> /%1 <userId> <reason> /%1 <userId> <reason> %1 is not a member of this room %1 no es miembro de esta sala /unban <userId> /unban <userId> /unban argument doesn't look like a user ID /unban argumento no parece un ID de usuario /ignore <userId> /ignore <userId> /ignore argument doesn't look like a user ID /ignore argumento no parece un ID de usuario Couldn't find user %1 on the server No se pudo encontrar el usuario %1 en el servidor /me needs an argument /me necesita un argumento /notice needs an argument /notice necesita un argumento /%1 <memberId> <message> /%1 <memberId> <message> %1 doesn't seem to have joined room %2 Parece que %1 no se ha unido a la sala %2 %1 doesn't look like a user id or room alias %1 no parece un ID de usuario o alias de sala /%1 <memberId> /%1 <memberId> Unknown /command. Use // to send this line literally /command desconocido. Utilice // para enviar esta línea literalmente Attach Adjuntar Attach file Adjuntar archivo Add a message to the file or just push Enter Agregue un mensaje al archivo o simplemente presione Entrar Attaching %1 Adjuntando %1 Attaching cancelled Adjunto cancelado There's no such /command outside of room. No hay tal /comando fuera de la sala. %1 doesn't look like a user id %1 no parece un ID de usuario %1 doesn't look like a user ID %1 no parece una ID de usuario You should select a room to send messages. Debe seleccionar una sala para enviar mensajes. Send a message (over %1) or enter a command... Envía un mensaje (más de %1) o introduce un comando... Next completion: Próxima finalización: Currently typing: Actualmente escribiendo: Timeline (no topic) (sin tema) Unknown Desconocido Unstable room version! ¡Versión de sala inestable! (no name) (sin nombre) %Ln byte(s) %Ln byte %Ln bytes %L1 MB %L1 MB %L1 GB %L1 GB This room has been upgraded. Esta sala ha sido mejorada. Room settings Configuración de sala %Ln events back from now %Ln evento desde ahora %Ln eventos desde ahora %L1 kB %L1 kB %Ln events cached %Ln evento en caché %Ln eventos en caché CreateRoomDialog Create room Crear sala Add Añadir Invite user(s) Invitar a usuario(s) Creating the room, please wait Creando la sala, por favor espere Please fill the fields as desired. None are mandatory Por favor llene los campos como desee. Ninguno es obligatorio Dialog Applying changes, please wait Aplicando cambios, por favor espere LoginDialog Login Iniciar sesión Stay logged in Permanecer conectado Matrix ID ID de Matriz Password Contraseña Device name Nombre del dispositivo Connect to server Conectar al servidor Connecting and logging in, please wait Conectando e iniciando sesión, por favor espere Re-login Reiniciar sesión Restoring access, please wait Restaurando el acceso, por favor espere MainWindow Loading... Cargando... &Accounts &Cuentas &Login... &Iniciar sesión... &Quit &Salir &View &Ver Dock &panels Dock &paneles &Display in timeline &Mostrar en la línea de tiempo Normal &join/leave events Eventos normales de unión/salida &Redacted events &Eventos redactados Show redacted events in the timeline as 'Redacted' instead of hiding them entirely Mostrar eventos redactados en la línea de tiempo como 'Redactado' en lugar de ocultarlos por completo &No-effect activity &Actividad sin efectos Edit tags order Editar orden de etiquetas &Room &Sala Change room &settings... Cambiar configuración de sala... Create &new room... Crear &nueva sala ... &Join room... &Unirse a la sala... &Close current room &Cerrar la sala actual &Settings &Configuraciones &Help &Ayuda &About &Acerca de &Highlight only &Resaltar solamente Notifications are entirely suppressed Las notificaciones se suprimen por completo. &Non-intrusive &No intrusivo Show notifications but do not activate the window Mostrar notificaciones pero no activar la ventana &Full &Completo Show notifications and activate the window Mostrar notificaciones y activar la ventana Notifications Notificaciones Default Predeterminado The layout with author labels above blocks of messages El diseño con etiquetas de autor sobre bloques de mensajes The layout with author labels to the left from each message El diseño con etiquetas de autor a la izquierda de cada mensaje Timeline layout Diseño de la línea de tiempo Load full-size images at once Cargue imágenes de tamaño completo de una vez Automatically download a full-size image instead of a thumbnail Descargue automáticamente una imagen a tamaño completo en lugar de una miniatura Configure &network proxy... Configurar el proxy de la &red... Couldn't open a file to save access token No se pudo abrir un archivo para guardar el token de acceso Quaternion couldn't open a file to write the access token to. You're logged in but will have to provide your password again when you restart the application. Quaternion no pudo abrir un archivo para escribir el token de acceso. Ha iniciado sesión pero tendrá que proporcionar su contraseña nuevamente cuando reinicie la aplicación. Couldn't set access token file permissions No se pudieron establecer los permisos del archivo de token de acceso Quaternion couldn't restrict permissions on the access token file. Do you still want to save the access token to it? Quaternion no pudo restringir los permisos en el archivo de token de acceso. ¿Todavía quieres guardar el token de acceso? Logged out as %1 Se ha cerrado la sesión como %1 Sync failed La sincronización falló The last sync of account %1 has failed with error: %2 La última sincronización de la cuenta %1 ha fallado con el error: %2 The last sync has failed with error: %1 La última sincronización ha fallado con el error: %1 Clicking 'Retry' will attempt to resume synchronisation; Clicking 'Cancel' will stop further synchronisation of this account until logout or Quaternion restart. Al hacer clic en 'Reintentar' se intentará reanudar la sincronización; Al hacer clic en "Cancelar" se detendrá la sincronización de esta cuenta hasta que se cierre la sesión o se reinicie Quaternion. Before this server can process your information, you have to agree with its terms and conditions; please click the button below to open the web page where you can do that Antes de que este servidor pueda procesar su información, usted tiene que estar de acuerdo con sus términos y condiciones; por favor haga clic en el botón de abajo para abrir la página web donde puede hacer eso Open web page Abrir página web About Quaternion Acerca de Quaternion Welcome to Quaternion Bienvenido a Quaternion Joined %1 as %2 Se unió a %1 como %2 Couldn't connect to the server as %1; will retry within %2 seconds No se pudo conectar al servidor como %1; volverá a intentarlo en %2 segundos Reconnecting... Reconectando... No SSL support Sin soporte SSL Your SSL configuration does not allow Quaternion to establish secure connections. Su configuración SSL no permite que Quaternion establezca conexiones seguras. SSL error Error SSL Proxy needs authentication El proxy necesita autenticación Authenticate Autenticar User name Nombre de usuario Password Contraseña &Thanks &Gracias Original project author: %1 Autor del proyecto original: %1 Web page Página web Project leader: %1 Líder del proyecto: %1 Contributors: Colaboradores: Quaternion contributors @ GitHub Colaboradores de Quaternion @ GitHub Quaternion translators @ Lokalise.co Traductores de Quaternion Lokalise.co Made with: Hecho con: Show join and leave events Mostrar eventos de unirse y dejar Use shuttle scrollbar (requires restart) Use la barra de desplazamiento de la lanzaderar (requiere reinicio) Control scroll velocity instead of position with the timeline scrollbar Controle la velocidad de desplazamiento en lugar de la posición con la barra de desplazamiento de la línea de tiempo Request URL: %1 Response: %2 URL de solicitud: %1 Respuesta: %2 Close to tray Cerrar a la bandeja Make close button [X] minimize to tray instead of closing main window Hacer que el botón de cierre [X] minimice a la bandeja en lugar de cerrar la ventana principal Show/hide meaningless activity (join-leave pairs and redacted events between) Mostrar/ocultar actividades sin sentido (unir-dejar pares y eventos redactados entre ellos) Built from Git, commit SHA: Construido desde Git, confirme SHA: Library commit SHA: SHA de confirmación de biblioteca: Open room... Sala abierta... Open room Sala abierta Open a room from the room list Abrir una sala de la lista de salas Show/hide Rooms dock panel Mostrar / ocultar panel de acoplamiento de salas Show/hide Users dock panel Mostrar/ocultar el panel de acoplamiento Usuarios Access token file found Se encontró el archivo de token de acceso Couldn't migrate access token No se pudo migrar el token de acceso Couldn't save access token No se pudo guardar el token de acceso Logging in into a logged in account Iniciar sesión en una cuenta iniciada You're trying to log in into an account that's already logged in. Do you want to continue? Está intentando iniciar sesión en una cuenta que ya ha iniciado sesión. ¿Desea continuar? Couldn't delete access token No se pudo eliminar el token de acceso Open direct chat? ¿Abrir chat directo? Open direct chat with user %1? ¿Abrir chat directo con el usuario %1? Room not found Sala no encontrada There's no room %1 in the room list. Check the spelling and the account. No hay %1 en la lista de salas. Compruebe la ortografía y la cuenta. Confirm your account to open %1 Confirme su cuenta para abrir %1 Confirm account Confirmar la cuenta Account Cuenta Room ID (starting with !) or alias (starting with #) ID de la sala (comenzando con !) o alias (comenzando con #) Confirm account to join %1 Confirme la cuenta para unirse a %1 Edit quote style Editar el estilo de la comilla Markdown (prepend each line with >) Markdown (anteponer cada línea con >) Custom (apply regex from the config file) Personalizado (aplique expresiones regulares desde el archivo de configuración) Locale's default (%1) Configuración regional predeterminada (%1) Example quote Ejemplo de comillas Choose the default style of quotes Elija el estilo predeterminado de comillas Special thanks to %1 for all the testing effort Un agradecimiento especial a %1 por todo el esfuerzo de prueba libQuotient contributors @ GitHub Colaboradores de libQuotient en @ GitHub Do you want to migrate the access token for %1 from the file to the keychain? ¿Desea migrar el token de acceso para %1 del archivo al llavero? Quaternion couldn't migrate access token for %1 from the file to the keychain. Quaternion no pudo migrar el token de acceso %1 del archivo al llavero. Quaternion couldn't save the access token to the keychain. Do you want to save the access token to file %1? Quaternion no pudo guardar el token de acceso en el llavero. ¿Desea guardar el token de acceso en un archivo %1? First sync completed for %1 Primera sincronización completada para %1 Quaternion couldn't delete the access token from the keychain. Quaternion no pudo eliminar el token de acceso del llavero. Please connect to a server Por favor, conéctese a un servidor Confirm your account to open a direct chat with %1 Confirme su cuenta para abrir un chat directo con %1 Log&out &Cerrar sesión Tags can be wildcarded by * next to dot(s) Clear the box to reset to defaults Special tags starting with "im.quotient." are: %1 User-defined tags should start with "u." Las etiquetas pueden ser marcadas con un * (comodín) al lado de un punto[s]. Desactive la casilla para restablecer los valores predeterminados. Etiquetas especiales que comienzan con "im.quotient." son: %1 Las etiquetas definidas por el usuario deben comenzar con "u." Room or user ID, room alias, Matrix URI or matrix.to link ID de sala o del usuario, alias de sala, Matrix URI o enlace matriz.to Join room Unirse a la sala Quaternion project contributors Colaboradores del proyecto Quaternion Felix Rohrbach Felix Rohrbach Alexey "Kitsune" Rusakov Alexey "Kitsune" Rusakov MessageEventModel Today Hoy Yesterday Ayer The day before yesterday Anteayer Redacted Redactado Redacted: %1 Redactado: %1 a file un archivo invited %1 to the room invitado %1 a la sala joined the room se unió a la sala cleared the display name limpiado el nombre de visualización changed the display name to %1 cambiado el nombre para mostrar a %1 cleared the avatar limpiado el avatar updated the avatar actualizado el avatar unbanned %1 sin suspender %1 self-unbanned sin prohibiciones left the room salió de la sala self-banned from the room auto-suspendido de la sala knocked Golpeó made something unknown hizo algo desconocido cleared the room main alias limpiado el alias principal de la sala set the room main alias to: %1 establecer el alias principal de la sala en: %1 cleared the room name limpiado el nombre de la sala set the room name to: %1 establecer el nombre de la sala en: %1 cleared the topic limpiado el tema set the topic to: %1 establecer el tema en: %1 changed the room avatar cambió el avatar de la sala activated End-to-End Encryption activado Cifrado de Extremo a Extremo withdrew %1's invitation retiró la invitación de %1 rejected the invitation rechazó la invitación updated the database actualizada la base de datos updated %1 state Estado actualizado de %1 updated %1 state for %2 estado actualizado de %1 para %2 Unknown event Evento desconocido upgraded the room to version %1 actualizada la sala a la versión %1 created the room, version %1 creada la sala, versión %1 has set room aliases on server %1 to: %2 ha establecido alias de sala en el servidor %1 a: %2 banned %1 from the room: %2 suspendido %1 de la sala: %2 kicked %1 from the room: %2 ha sacado a %1 de la sala: %2 and y (repeated) (repetido) kicked %1 from the room ha sacado a %1 de la sala NetworkConfigDialog Network proxy settings Configuración de proxy de red &Override system defaults &Reemplazar los valores predeterminados del sistema &No proxy &Sin proxy &HTTP(S) proxy &Proxy HTTP(S) &SOCKS5 proxy Proxy &SOCKS5 Host Anfitrión Port Puerto User name Nombre de usuario RoomDialogBase Publish room in room directory Publicar sala en el directorio de salas Allow guest accounts to join the room Permitir que las cuentas de invitados se unan a la sala Account Cuenta Room name Nombre de la sala Primary alias Alias principal Topic Tema About room versions Acerca de las versiones de sala (loading) (cargando) default Predeterminado stable estable Room version Versión de la sala Continue with unstable version? ¿Continuar con la versión inestable? You are using an UNSTABLE room version (%1). The server may stop supporting it at any moment. Do you still want to use this version? Está utilizando una versión de sala INESTABLE (%1). El servidor puede dejar de soportarlo en cualquier momento. ¿Aún desea utilizar esta versión? RoomListDock Mark room as read Marcar sala como leída Add tags... Agregar etiquetas... Join room Unirse a la sala Forget room Olvidar sala Remove tag Remover etiqueta Reject invitation Rechazar invitación Leave room Dejar la sala Enter new tags for the room Ingrese nuevas etiquetas para la sala Enter tags to add to this room, one tag per line Ingrese etiquetas para agregar a esta sala, una etiqueta por línea Change room &settings... Cambiar sala y configuraciones ... Add Añadir Rooms (%L1) Salas (%L1) RoomListModel Invited Invitado Low priority Baja prioridad People Personas Ungrouped rooms Salas desagrupadas Left Izquierda %1 (%Ln room(s)) %1 (%Ln sala) %1 (%Ln salas) %1 (as %2) %1 (como %2) You joined this room Te uniste a esta sala You left this room Saliste de esta sala. You were invited into this room Fuiste invitado a esta sala Main alias: %1 Alias principal: %1 Direct chat with %1 Charla directa con %1 The room enforces encryption La sala impone el cifrado ID: %1 ID: %1 Favourites Favoritos This room's version is unstable! ¡La versión de esta sala es inestable! Consider upgrading to a stable version (use room settings for that) Considere actualizar a una versión estable (use la configuración de sala para ello) Joined: %L1 Unidos: %L1 Invited: %L1 Invitado: %L1 RoomSettingsDialog Room settings: %1 Configuración de sala: %1 Update room Actualización de la sala Tags Etiquetas This version is unstable! Consider upgrading. Esta versión es inestable! Considere la posibilidad de actualizar. Upgrade Actualización Choose new room version Elija la nueva versión de sala You are about to upgrade %1. This operation cannot be reverted. Está a punto de actualizar %1. Esta operación no se puede revertir. Creating the new room version, please wait Creando la nueva versión de la sala, por favor espere Room identifier Identificador de sala UserListDock Users Usuarios Open direct chat Abrir chat directo Mention user Mencionar usuario Search Búsqueda Ignore user Ignorar usuario Kick user Remover usuario Ban user Prohibir usuario Kick %1 Remover %1 Reason Razón Ban %1 Prohibir %1 (%L1 out of %L2) (%L1 de %L2) FileContent Size: %1, declared type: %2 Tamaño: %1, tipo declarado: %2 Open after downloading Abrir después de la descarga Cancel Cancelar Save as... Guardar como... Open Abrir Open folder Abrir carpeta uploaded from %1 subido desde %1 being uploaded from %1 siendo subido desde %1 downloaded to %1 descargado a %1 TimelineItem Resend Reenviar Discard Descartar main Quaternion - an IM client for the Matrix protocol Quaternion: un cliente de mensajería instantánea para el protocolo Matrix Override locale Reemplazar la configuración regional locale configuración regional Hide main window on startup Ocultar la ventana principal al iniciar SystemTrayIcon Highlight in %1 Resaltar en %1 %Ln highlight(s) %Ln destacado %Ln destacados ThumbnailResponse Image request has been cancelled La solicitud de imagen ha sido cancelada Media id '%1' doesn't follow server/mediaId pattern El id de medios '%1' no sigue el patrón server/mediaId No connection to perform image request No hay conexión para realizar una solicitud de imagen ProfileDialog Account Cuenta Access token Token de acceso TimelineWidget Save file as Guardar archivo como Redact Redactar Quote Comillas Open externally Abrir externamente Quaternion-0.0.95.1/client/translations/quaternion_pl.ts000066400000000000000000001652321412757327200233430ustar00rootroot00000000000000 ChatRoomWidget Choose a room to send messages or enter a command... Wybierz pokój do wysyłania wiadomości lub wprowadź polecenie… There's nothing to send Nie ma niczego do przesłania /join argument doesn't look like a room ID or alias /join nie wygląda jak identyfikator pokoju lub alias Sending a farewell message is not supported yet. If you intended to leave another room, switch to it and type /leave there. Wysyłanie wiadomości pożegnalnej nie jest jeszcze obsługiwane. Jeśli chcesz opuścić inny pokój, przełącz się do niego i wpisz tam /leave. /forget must be followed by the room id/alias, even for the current room /forget musi poprzedzać identyfikator pokoju/alias, nawet dla bieżącego pomieszczenia %1 doesn't look like a room id or alias %1 nie wygląda jak identyfikator pokoju lub alias /invite <memberId> /invite <memberId> /%1 <userId> <reason> /%1 <userId> <reason> %1 is not a member of this room %1 nie jest członkiem tego pokoju /unban <userId> /unban <userId> /unban argument doesn't look like a user ID /unban nie wygląda jak identyfikator użytkownika /ignore <userId> /ignore <userId> /ignore argument doesn't look like a user ID /ignore nie wygląda jak identyfikator użytkownika Couldn't find user %1 on the server Nie można znaleźć użytkownika %1 na serwerze /me needs an argument /me potrzebuje argumentu /notice needs an argument /notice potrzebuje argumentu /%1 <memberId> <message> /%1 <memberId> <message> %1 doesn't seem to have joined room %2 %1 najwyraźniej nie dołączył do pokoju %2 %1 doesn't look like a user id or room alias %1 nie wygląda jak identyfikator użytkownika lub alias pokoju /%1 <memberId> /%1 <memberId> Unknown /command. Use // to send this line literally Nieznane polecenie. Użyj //, aby dosłownie wysłać tę linię Attach Załącz Attach file Załącz plik Add a message to the file or just push Enter Dodaj wiadomość do pliku lub po prostu naciśnij Enter Attaching %1 Załączanie %1 Attaching cancelled Załączanie anulowano There's no such /command outside of room. Nie ma takiego /polecenia poza pokojem. %1 doesn't look like a user id %1 nie wygląda jak identyfikator użytkownika %1 doesn't look like a user ID %1 nie wygląda jak identyfikator użytkownika You should select a room to send messages. Powinieneś/Powinnaś wybrać pokój do wysyłania wiadomości. Send a message (over %1) or enter a command... Wyślij wiadomość (poprzez %1) lub wprowadź polecenie… Attaching an image from clipboard Załączanie obrazu ze schowka Send a message (no end-to-end encryption support yet)... Wyślij wiadomość (brak obsługi szyfrowania typu end-to-end)… Your build of Quaternion doesn't support Markdown Twoja wersja Quaterniona nie wspiera składni Markdown No completions Brak dokończeń %Ln more completions %Ln dokończenie więcej %Ln dokończenia więcej %Ln dokończeń więcej %Ln dokończeń więcej Next completion: Następne dokończenie Currently typing: Obecnie pisze: %L1 more %L1 więcej Timeline (no topic) (brak tematu) Unknown Nieznany Unstable room version! Niestabilna wersja pokoju! (no name) (bez nazwy) %Ln byte(s) %Ln bajt %Ln bajty %Ln bajtów %Ln bajtów %L1 MB %L1 MB %L1 GB %L1 GB This room has been upgraded. Ten pokój został zaktualizowany. Go to new room Przejdź do nowego pokoju Room settings Ustawienia pokoju Latest events Ostatnie wydarzenia %Ln events back from now %Ln wydarzenie od teraz %Ln wydarzenia od teraz %Ln wydarzeń od teraz %Ln wydarzeń od teraz %L1 kB %L1 kB %Ln events cached %Ln wydarzenie w pamięci podręcznej %Ln wydarzenia w pamięci podręcznej %Ln wydarzeń w pamięci podręcznej %Ln wydarzeń w pamięci podręcznej Hide topic Ukryj temat Show topic Pokaż temat CreateRoomDialog Create room Utwórz pokój Add Dodaj Invite user(s) Zaproś użytkownika(-ów) Creating the room, please wait Tworzę pokój, proszę czekać Please fill the fields as desired. None are mandatory Proszę wypełnić pola zgodnie z życzeniem. Żaden z nich nie jest obowiązkowy Dialog Applying changes, please wait Wprowadzanie zmian, proszę czekać LoginDialog Login Zaloguj się Stay logged in Pozostań zalogowany Matrix ID Identyfikator Matrix Password Hasło Device name Nazwa urządzenia Connect to server Połącz z serwerem Connecting and logging in, please wait Łączenie się i logowanie, proszę czekać Re-login Zaloguj ponownie Restoring access, please wait Przywracanie dostępu, proszę czekać The homeserver is available Serwer domowy jest dostępny Could not connect to the homeserver Nie można było się połączyć z serwerem domowym MainWindow Loading... Ładowanie… &Accounts &Konta &Login... Zaloguj &się… &Quit &Zakończ &View &Widok Dock &panels Łącz &panele &Display in timeline &Pokaż na osi czasu Normal &join/leave events &Zwykłe zdarzenia dołączania/opuszczania &Redacted events &Zredagowane zdarzenia Show redacted events in the timeline as 'Redacted' instead of hiding them entirely Pokaż zredagowane wydarzenia na osi czasu jako „Zredagowane”, zamiast całkowicie je ukrywać &No-effect activity Aktywności &bez efektu Edit tags order Edytuj kolejność tagów &Room &Pokój Change room &settings... Zmień &ustawienia pokoju… Create &new room... Utwórz &nowy pokój… &Join room... &Dołącz do pokoju… &Close current room &Zamknij bieżący pokój &Settings &Ustawienia &Help &Pomoc &About &Informacje o Quaternion &Highlight only &Tylko wyróżnij Notifications are entirely suppressed Powiadomienia są całkowicie tłumione &Non-intrusive &Nieinwazyjne Show notifications but do not activate the window Pokaż powiadomienia, ale nie aktywuj okna &Full &Pełne Show notifications and activate the window Pokaż powiadomienia i aktywuj okno Notifications Powiadomienia Default Domyślny The layout with author labels above blocks of messages Układ z etykietami autora nad blokami wiadomości The layout with author labels to the left from each message Układ z etykietami autora po lewej stronie przy każdej wiadomości Timeline layout Układ osi czasu Load full-size images at once Ładuj od razu pełnowymiarowe obrazy Automatically download a full-size image instead of a thumbnail Automatycznie pobierz obraz w pełnym rozmiarze zamiast miniatury Configure &network proxy... &Konfiguruj proxy sieciowe… Couldn't open a file to save access token Nie można otworzyć pliku, aby zapisać token dostępu Quaternion couldn't open a file to write the access token to. You're logged in but will have to provide your password again when you restart the application. Quaternion nie mógł otworzyć pliku, aby zapisać token dostępu. Jesteś zalogowany, ale będziesz musiał podać hasło ponownie po ponownym uruchomieniu aplikacji. Couldn't set access token file permissions Nie można ustawić uprawnień do pliku tokenu dostępu Quaternion couldn't restrict permissions on the access token file. Do you still want to save the access token to it? Quaternion nie mógł ograniczyć uprawnień do pliku z tokenem dostępu. Czy nadal chcesz tam zapisać token dostępu? Logged out as %1 Wylogowano jako %1 Sync failed Synchronizacja nie powiodła się The last sync of account %1 has failed with error: %2 Ostatnia synchronizacja konta %1 nie powiodła się z powodu błędu: %2 The last sync has failed with error: %1 Ostatnia synchronizacja nie powiodła się z powodu błędu: %1 Clicking 'Retry' will attempt to resume synchronisation; Clicking 'Cancel' will stop further synchronisation of this account until logout or Quaternion restart. Kliknięcie „Ponów próbę” spowoduje wznowienie synchronizacji; Kliknięcie „Anuluj” zatrzyma dalszą synchronizację tego konta do momentu wylogowania się lub ponownego uruchomienia Quaterniona. Before this server can process your information, you have to agree with its terms and conditions; please click the button below to open the web page where you can do that Zanim serwer ten będzie mógł przetwarzać Twoje informacje, musisz wyrazić zgodę na jego warunki; proszę kliknąć przycisk poniżej, aby otworzyć stronę internetową, na której możesz to zrobić Open web page Otwórz stronę internetową About Quaternion Informacje o Quaternion Welcome to Quaternion Witamy w Quaternion Joined %1 as %2 Dołączono %1 jako %2 Couldn't connect to the server as %1; will retry within %2 seconds Nie można było połączyć się z serwerem jako %1; spróbuje ponownie w ciągu %2 sekund Reconnecting... Ponowne łączenie… No SSL support Brak obsługi SSL Your SSL configuration does not allow Quaternion to establish secure connections. Twoja konfiguracja SSL nie zezwala Quaternionowi na ustanowienie bezpiecznych połączeń. SSL error Błąd SSL Proxy needs authentication Proxy wymaga uwierzytelnienia Authenticate Uwierzytelnij User name Nazwa użytkownika Password Hasło &Thanks &Podziękowania Original project author: %1 Pierwotny autor projektu: %1 Web page Strona internetowa Project leader: %1 Kierownik projektu: %1 Contributors: Współautorzy: Quaternion contributors @ GitHub Współautorzy Quaterniona na GitHubie Quaternion translators @ Lokalise.co Tłumacze Quaterniona w Lokalise.co Made with: Wykonana z: Show join and leave events Pokaż zdarzenia dołączenia i wyjścia Request URL: %1 Response: %2 Żądany adres URL: %1 Odpowiedź: %2 Close to tray Zamykaj do zasobnika Make close button [X] minimize to tray instead of closing main window Minimalizuje główne okno do zasobnika po wciśnięciu przycisku zamykania [X] zamiast zamykania go Show/hide meaningless activity (join-leave pairs and redacted events between) Pokaż/ukryj bezsensowną aktywność (pary dołączenia, opuszczenia i zredagowanych wydarzeń pomiędzy) Built from Git, commit SHA: Zbudowano z Git, commit SHA: Library commit SHA: Commit SHA biblioteki: Open room... Otwórz pokój… Open room Otwórz pokój Open a room from the room list Otwórz pokój z listy pokoi Show/hide Rooms dock panel Pokazuje/ukrywa panel dock pokojów Show/hide Users dock panel Pokazuje/ukrywa panel dock użytkowników Access token file found Znaleziono plik tokena dostępu Couldn't migrate access token Nie można migrować tokena dostępu Couldn't save access token Nie można zapisać tokena dostępu Logging in into a logged in account Logowanie do zalogowanego konta You're trying to log in into an account that's already logged in. Do you want to continue? Próbujesz się zalogować na konto, które jest już zalogowane. Chcesz kontynuować? Couldn't delete access token Nie można usunąć tokena dostępu Open direct chat? Otworzyć bezpośrednią rozmowę? Open direct chat with user %1? Otworzyć bezpośrednią rozmowę z użytkownikiem %1? Room not found Nie znaleziono pokoju There's no room %1 in the room list. Check the spelling and the account. Nie ma pokoju %1 na liście pokoi. Sprawdź pisownię i konto. Confirm your account to open %1 Potwierdź twoje konto, aby otworzyć %1 Confirm account Potwierdź konto Account Konto Room ID (starting with !) or alias (starting with #) Identyfikator pokoju (zaczynający się od !) lub alias (zaczynający się od #) Confirm account to join %1 Potwierdź konto, aby dołączyć do %1 Edit quote style Edytuj styl cytowania Markdown (prepend each line with >) Markdown (poprzedzaj każdą linię >) Custom (apply regex from the config file) Niestandardowy (zastosuj wyrażenie regularne z pliku konfiguracyjnego) Locale's default (%1) Domyślne ustawienia regionalne (%1) Example quote Przykładowy cytat Choose the default style of quotes Wybierz domyślny styl cytatów Special thanks to %1 for all the testing effort Specjalne podziękowania dla %1 za cały wysiłek włożony w testowanie. libQuotient contributors @ GitHub Współautorzy libQuotient na GitHubie Do you want to migrate the access token for %1 from the file to the keychain? Czy chcesz migrować token dostępu dla %1 z pliku do pęku kluczy? Quaternion couldn't migrate access token for %1 from the file to the keychain. Quaternion nie mógł migrować tokena dostępu %1 z pliku do pęku kluczy. Quaternion couldn't save the access token to the keychain. Do you want to save the access token to file %1? Quaternion nie mógł zapisać tokena dostępu do pęku kluczy. Czy chcesz zapisać token dostępu do pliku %1? First sync completed for %1 Pierwsza synchronizacja dla %1 zakończona Quaternion couldn't delete the access token from the keychain. Quaternion nie mógł usunąć tokena dostępu z pęku kluczy. No application for the link Nie znaleziono aplikacji mogącą otworzyć link External link confirmation Potwierdzenie odnośnika zewnętrznego An external application will be opened to visit a non-Matrix link: %1 Is that right? Otworzy się zewnętrzna aplikacja, aby odwiedzić odnośnik inny niż Matrix: %1 Czy to się zgadza? Do not ask again Nie pytaj ponownie Malformed or empty Matrix id Nieprawidłowy lub pusty identyfikator Matrixa %1 is not a correct Matrix identifier %1 nie jest prawidłowym identyfikatorem Matrixa Please connect to a server Połącz się z serwerem User &profiles... Profile &użytkowników… Log&out Wyl&oguj Invite events Wydarzenia zaproszeń Show invite and withdrawn invitation events Pokazuj zdarzenia zaproszeń i odrzucenia zaproszeń Changes in display na&me Z&miany w wyświetlanej nazwie Show display name change Pokazuj zmianę wyświetlanej nazwy Avatar &changes Zmiany &awatara Show avatar update events Pokazuj zdarzenia aktualizacji awatara Room alias &updates Aktualizacje aliasu pokoju Show room alias updates events Pokazuj zdarzenia aktualizacji aliasu serwera Un&known event types &Nieznane typy wydarzeń Show/hide unknown event types Pokazuj/ukrywaj nieznane typy zdarzeń Tags can be wildcarded by * next to dot(s) Clear the box to reset to defaults Special tags starting with "im.quotient." are: %1 User-defined tags should start with "u." Tagi mogą być wieloznaczne przez * obok kropek Wyczyść pole, aby przywrócić domyślne ustawienia Specjalne tagi zaczynające się od „im.quotient.” to: %1 Tagi zdefiniowane przez użytkownika powinny zaczynać się od „u.” &About Quaternion &Informacje o Quaternion About &Qt Informacje o &Qt Use Breeze style (requires restart) Użyj stylu Breeze (wymaga ponownego uruchomienia) Force use Breeze style and icon theme Wymuś użycie stylu i motywu ikon Breeze Chat with user Czatuj z użytkownikiem Can't open Nie można otworzyć Could not resolve id Nie można ustalić identyfikatora Could not find an external application to open the URI: Nie udało się znaleźć zewnętrznej aplikacji do otwarcia identyfikatora URI: Could not resolve Matrix identifier Nie można ustalić identyfikatora Matrix Failed to resolve server %1 Nie udało się ustalić serwera %1 Room or user ID, room alias, Matrix URI or matrix.to link ID pokoju lub użytkownika, alias pokoju, Matrix URI lub link matrix.to Go to room Przejdź do pokoju Join room Dołącz do pokoju Quaternion project contributors Współautorzy projektu Quaternion Felix Rohrbach Felix Rohrbach Alexey "Kitsune" Rusakov Alexey "Kitsune" Rusakov Confirm opening external links Potwierdzaj otwieranie linków zewnętrznych Show a confirmation box before opening non-Matrix links in an external application Pokazuj okno potwierdzenia przed otwarciem linków innych niż Matrix w zewnętrznej aplikacji MessageEventModel Today Dzisiaj Yesterday Wczoraj The day before yesterday Przedwczoraj Redacted Zredagowano Redacted: %1 zredagował(a): %1 a file plik invited %1 to the room zaprosił(a) %1 do pokoju joined the room dołączył(a) do pokoju cleared the display name wyczyścił(a) swoją wyświetlaną nazwę changed the display name to %1 zmienił(a) swoją wyświetlaną nazwę na %1 cleared the avatar wyczyścił(a) awatar updated the avatar zaktualizował(a) awatar unbanned %1 odbanował(a) %1 left the room opuścił(a) pokój knocked zapukał made something unknown zrobił(a) coś nieznanego cleared the room main alias wyczyścił(a) główny alias pokoju set the room main alias to: %1 ustawił(a) główny alias pokoju na: %1 cleared the room name wyczyścił(a) nazwę pokoju set the room name to: %1 ustawił(a) nazwę pokoju na %1 cleared the topic wyczyścił(a) temat set the topic to: %1 ustawił(a) temat na %1 changed the room avatar zmienił(a) awatar pokoju activated End-to-End Encryption aktywował(a) szyfrowanie End-to-End withdrew %1's invitation wycofał(a) zaproszenie %1 rejected the invitation odrzucił(a) zaproszenie updated the database zaktualizował bazę danych updated %1 state zaktualizował(a) stan %1 updated %1 state for %2 zaktualizował(a) stan %1 dla %2 Unknown event Nieznane zdarzenie upgraded the room to version %1 zaktualizował(a) pokój do wersji %1 created the room, version %1 stworzył(a) pokój, wersja %1 has set room aliases on server %1 to: %2 ustawił(a) alias pokoju na serwerze %1 na: %2 banned %1 from the room: %2 zbanował(a) %1 z pokoju: %2 kicked %1 from the room: %2 wyrzucił(-a) %1 z pokoju: %2 upgraded the room: %1 zaktualizował(-a) pokój: %1 and i %Ln more member(s) %Ln użytkownika więcej %Ln użytkowników więcej %Ln użytkowników więcej %Ln użytkowników więcej (repeated) (powtórzono) kicked %1 from the room wyrzucił(-a) %1 z pokoju NetworkConfigDialog Network proxy settings Ustawienia proxy sieciowego &Override system defaults &Nadpisz domyślne ustawienia systemowe &No proxy &Bez proxy &HTTP(S) proxy &HTTP(S) proxy &SOCKS5 proxy &SOCKS5 proxy Host Host Port Port User name Nazwa użytkownika RoomDialogBase Publish room in room directory Opublikuj pokój w katalogu pokoju Allow guest accounts to join the room Zezwalaj kontom gości na dołączenie do pokoju Account Konto Room name Nazwa pokoju Primary alias Główny alias Topic Temat About room versions O wersjach pokoi (loading) (ładowanie) default domyślna stable stabilna Room version Wersja pokoju Continue with unstable version? Kontynuować z niestabilną wersją? You are using an UNSTABLE room version (%1). The server may stop supporting it at any moment. Do you still want to use this version? Używasz NIESTABILNEJ wersji pokoju (%1). Serwer może przestać go obsługiwać w każdej chwili. Czy nadal chcesz używać tej wersji? RoomListDock Mark room as read Oznacz pokój jako przeczytany Add tags... Dodaj tagi… Join room Dołącz do pokoju Forget room Zapomnij pokój Remove tag Usuń tag Reject invitation Odrzuć zaproszenie Leave room Opuść pokój Enter new tags for the room Wprowadź nowe tagi dla pokoju Enter tags to add to this room, one tag per line Wprowadź nowe tagi dla pokoju, jeden tag na linię Change room &settings... Zmień &ustawienia pokoju… Add Dodaj Copy room link to clipboard Skopiuj link do pokoju do schowka Rooms (%L1) Pokoje (%L1) RoomListModel Invited Zaproszone Low priority Niski priorytet People Ludzie Ungrouped rooms Niezgrupowane pokoje Left Opuszczone %1 (%Ln room(s)) %1 (%Ln pokój) %1 (%Ln pokoje) %1 (%Ln pokojów) %1 (%Ln pokojów) %1 (as %2) %1 (jako %2) You joined this room Dołączyłeś(-aś) do pokoju You left this room Opuściłeś(-aś) pokój You were invited into this room Zostałeś(-aś) zaproszony(-a) do tego pokoju Main alias: %1 Główny alias: %1 Direct chat with %1 Bezpośrednia rozmowa z %1 The room enforces encryption Ten pokój wymusza szyfrowanie ID: %1 ID: %1 Favourites Ulubione This room's version is unstable! Wersja tego pokoju jest niestabilna! Server notices Ogłoszenia serwera Joined: %L1 Dołączeni: %L1 Invited: %L1 Zaproszone: %L1 Unread messages: %L1 Nieprzeczytane wiadomości: %L1 Unread highlights: %L1 Nieprzeczytane wyróżnienia: %L1 Unread notifications: %L1 Nieprzeczytane powiadomienia: %L1 (maybe more) (możliwie więcej) RoomSettingsDialog Room settings: %1 Ustawienia pokoju: %1 Update room Zaktualizuj pokój Tags Tagi This version is unstable! Consider upgrading. Ta wersja jest niestabilna! Rozważ aktualizację. Upgrade Zaktualizuj Choose new room version Wybierz nową wersję pokoju You are about to upgrade %1. This operation cannot be reverted. Zamierzasz zaktualizować %1. Ta operacja nie może zostać cofnięta. Creating the new room version, please wait Tworzenie nowej wersji pokoju, proszę czekać Room identifier Identyfikator pokoju UserListDock Users Użytkownicy Open direct chat Otwórz bezpośredni czat Mention user Wspomnij użytkownika Search Szukaj Ignore user Ignoruj użytkownika Kick user Wyrzuć użytkownika Ban user Banuj użytkownika Kick %1 Wyrzuć %1 Reason Powód Ban %1 Banuj %1 (%L1 out of %L2) (%L1 z %L2) FileContent Size: %1, declared type: %2 Rozmiar: %1, zadeklarowany typ: %2 Open after downloading Otwórz po pobraniu Cancel Anuluj Save as... Zapisz jako… Open Otwórz Open folder Otwórz folder uploaded from %1 przesłany z %1 downloaded to %1 pobrano do %1 TimelineItem Resend Wyślij ponownie Discard Odrzuć edited edytowane Go to older room Przejdź do starszego pokoju Go to new room Przejdź do nowego pokoju Reaction '%1' from %2 Reakcja „%1” od %2 main Quaternion - an IM client for the Matrix protocol Quaternion - komunikator internetowy dla protokołu Matrix Override locale Nadpisz ustawienia regionalne locale locale Hide main window on startup Ukryj główne okno podczas uruchamiania SystemTrayIcon Highlight in %1 Podświetlenie w %1 Hide Ukryj Quit Zakończ Show Pokaż %Ln highlight(s) %Ln wyróżnienie %Ln wyróżnienia %Ln wyróżnień %Ln wyróżnień ThumbnailResponse Image request has been cancelled Żądanie obrazu zostało anulowane No connection to perform image request Brak połączeń do wykonania żądania obrazu Image request is pending Oczekiwanie na obraz TimelineWidget Referenced message not found Nie znaleziono odwołanej wiadomości Copy permalink to clipboard Skopiuj link bezpośredni do schowka Show details Pokaż szczegóły Open Folder Otwórz folder Download Pobierz Save file as... Zapisz plik jako… Copy selected text to clipboard Skopiuj zaznaczony tekst do schowka Copy image to clipboard Skopiuj obraz do schowka Save file as Zapisz plik jako Redact Zredaguj Copy link to clipboard Skopiuj link do schowka Quote Cytuj Open externally Otwórz zewnętrznie ProfileDialog This is the current device To jest obecne urządzenie Device display name Wyświetlana nazwa urządzenia Device ID Identyfikator urządzenia Last time seen Ostatnio widziany Last IP address Ostatni adres IP User profiles Profile użytkowników Account Konto Display Name Wyświetlana nazwa Copy to clipboard Skopiuj do schowka Access token Token dostępu Apply and close Zastosuj i zamknij Loading other devices... Ładowanie innych urządzeń… No avatar Brak awatara Set avatar Ustaw awatar ChatEdit Reset formatting Zresetuj formatowanie Reset the current character formatting to the default Przywraca domyślne formatowanie znaków Quaternion-0.0.95.1/client/translations/quaternion_ru.ts000066400000000000000000002213071412757327200233520ustar00rootroot00000000000000 ChatRoomWidget Choose a room to send messages or enter a command... Выберите комнату для отправки сообщений или введите команду... There's nothing to send Нечего отправлять /join argument doesn't look like a room ID or alias Аргумент /join не похож на идентификатор или псевдоним комнаты Sending a farewell message is not supported yet. If you intended to leave another room, switch to it and type /leave there. Отправка прощального сообщения пока не поддерживается. Если вы намеревались покинуть другую комнату, переключитесь на нее и введите /leave в ней. /forget must be followed by the room id/alias, even for the current room Команда /forget должна сопровождаться идентификатором или псевдонимом комнаты, даже при использовании в текущей комнате %1 doesn't look like a room id or alias %1 не похож на идентификатор или псевдоним комнаты /invite <memberId> /invite <ID-участника> /%1 <userId> <reason> /%1 <ID-пользователя> <причина> %1 is not a member of this room %1 не является участником этой комнаты /unban <userId> /unban <ID-пользователя> /unban argument doesn't look like a user ID Аргумент /unban не похож на идентификатор или адрес пользователя /ignore <userId> /ignore <ID-пользователя> /ignore argument doesn't look like a user ID Аргумент /ignore не похож на идентификатор или адрес пользователя Couldn't find user %1 on the server Пользователь %1 не найден на сервере /me needs an argument /me требует указания аргумента /notice needs an argument /notice требует указания аргумента /%1 <memberId> <message> /%1 <ID-участника> <сообщение> %1 doesn't seem to have joined room %2 %1, похоже, не присоединился к комнате %2 %1 doesn't look like a user id or room alias %1 не похож на идентификатор пользователя или псевдоним комнаты /%1 <memberId> /%1 <ID-участника> Unknown /command. Use // to send this line literally Неизвестная команда. Используйте //, чтобы отправить буквально эту строку Attach Добавить Attach file Добавить файл Add a message to the file or just push Enter Снабдите файл сообщением или просто нажмите Enter Attaching %1 Будет отправлен %1 Attaching cancelled Отправка файла отменена There's no such /command outside of room. Нет такой /команды без указания комнаты. %1 doesn't look like a user id %1 не похож на идентификатор пользователя %1 doesn't look like a user ID %1 не похож на идентификатор пользователя You should select a room to send messages. Для отправки сообщений нужно выбрать комнату. Send a message (over %1) or enter a command... Отправить сообщение (через %1) или ввести команду... Attaching an image from clipboard Добавление изображения из буфера обмена Send a message (no end-to-end encryption support yet)... Отправить сообщение (сквозное шифрование пока не поддерживается)... Your build of Quaternion doesn't support Markdown Ваша сборка Quaternion не поддерживает Markdown No completions Подсказок нет %Ln more completions еще %Ln подсказка еще %Ln подсказки еще %Ln подсказок Next completion: Следующая подсказка: Currently typing: Сейчас печатает: At pos %1: %2 На позиции %1: %2 %L1 more еще %L1 Timeline (no topic) (без темы) Unknown Неизвестно Unstable room version! Нестабильная версия комнаты! (no name) (без имени) %Ln byte(s) %Ln байт %Ln байта %Ln байтов %L1 MB %L1 МБ %L1 GB %L1 ГБ This room has been upgraded. Эта комната была обновлена Go to new room Перейти в новую комнату Room settings Настройки комнаты Latest events Последние события %Ln events back from now %Ln событие назад %Ln событий назад %Ln событий назад %L1 kB %L1 Кб %Ln events cached %Ln событие закешировано %Ln события закешировано %Ln событий закешировано %Ln events requested from the server %Ln событие запрошено с сервера %Ln события запрошены с сервера %Ln событий запрошено с сервера Hide topic Скрыть тему Show topic Показать тему CreateRoomDialog Create room Создать комнату Add Добавить Invite user(s) Пригласить пользователя(ей) Creating the room, please wait Комната создаётся, пожалуйста подождите Please fill the fields as desired. None are mandatory Пожалуйста, заполните поля по желанию. Обязательных полей нет. Dialog Applying changes, please wait Применение изменений, пожалуйста подождите LoginDialog Login Войти Stay logged in Оставаться подключённым к учётной записи Matrix ID Идентификатор Matrix Password Пароль Device name Название устройства Connect to server Соединиться с сервером Connecting and logging in, please wait Подождите, выполняются подключение и вход в учётную запись Re-login Переподключиться Restoring access, please wait Восстанавливается доступ, пожалуйста подождите Resolving the homeserver... Определение домашнего сервера... The server URL doesn't look valid URL-адрес сервера выглядит недействительным Login with SSO Использовать единый вход The homeserver is available Домашний сервер доступен Could not connect to the homeserver Не удалось подключиться к домашнему серверу No supported login flows Нет поддерживаемых процедур входа Single sign-on Единый вход Quaternion couldn't automatically open the single sign-on URL. Please copy and paste it to the right application (usually a web browser): Quaternion не смог автоматически открыть URL-адрес единого входа. Скопируйте и вставьте его в нужное приложение (обычно в веб-браузер): After authentication, the browser will follow the temporary local address setup by Quaternion to conclude the login sequence. После аутентификации браузер перейдет на временный адрес, созданный Quaternion, чтобы завершить подключение к учетной записи. Getting supported login flows... Запрашиваются поддерживаемые процедуры входа MainWindow Loading... Загрузка... &Accounts &Учётные записи &Login... Под&ключиться... &Quit &Выход &View &Вид Dock &panels &Док-панели &Display in timeline &Отображать в истории событий Normal &join/leave events Обычные события входа/выхода &Redacted events &Удалённые события Show redacted events in the timeline as 'Redacted' instead of hiding them entirely Показывать пометку «Удалено» на месте удалённых событий, а не полностью их скрывать &No-effect activity &Бесполезная активность Edit tags order Изменить порядок тегов &Room &Комната Change room &settings... Изменить &настройки комнаты... Create &new room... Создать &новую комнату... &Join room... Присоединиться к &комнате... &Close current room &Закрыть текущую комнату &Settings &Настройки &Help &Помощь &About &О программе &Highlight only Только при &упоминаниях Notifications are entirely suppressed Не показывать уведомления &Non-intrusive &Ненавязчивые Show notifications but do not activate the window Показывать уведомления, но не активировать окно &Full &Полные Show notifications and activate the window Показывать уведомления и активировать окно Notifications Уведомления Default По умолчанию The layout with author labels above blocks of messages Вид с именами авторов над сообщениями The layout with author labels to the left from each message Вид с именами авторов слева от сообщений Timeline layout Вид истории событий Load full-size images at once Сразу загружать полноразмерные изображения Automatically download a full-size image instead of a thumbnail Автоматически загружать полноразмерное изображение вместо миниатюры Configure &network proxy... Настроить &прокси-сервер... Couldn't open a file to save access token Не удалось открыть файл, чтобы сохранить ключ доступа Quaternion couldn't open a file to write the access token to. You're logged in but will have to provide your password again when you restart the application. Quaternion не смог открыть файл для записи ключа доступа. Учётная запись подключена, но при повторном запуске приложения вам придется снова ввести свой пароль. Couldn't set access token file permissions Не удалось установить права на обращение к файлу с ключом доступа Quaternion couldn't restrict permissions on the access token file. Do you still want to save the access token to it? Quaternion не смог ограничить права на обращение к файлу с ключом доступа. Вы все еще хотите сохранить ключ доступа? Logged out as %1 Учётная запись %1 отключена Sync failed Сбой синхронизации The last sync of account %1 has failed with error: %2 При последней синхронизации учётной записи %1 произошла ошибка: %2 The last sync has failed with error: %1 При последней синхронизации произошла ошибка: %1 Clicking 'Retry' will attempt to resume synchronisation; Clicking 'Cancel' will stop further synchronisation of this account until logout or Quaternion restart. Нажмите кнопку «Повторить попытку», чтобы попытаться возобновить синхронизацию; нажмите «Отмена», чтобы остановить дальнейшую синхронизацию этой учётной записи до отключения от неё или перезапуска Quaternion. Before this server can process your information, you have to agree with its terms and conditions; please click the button below to open the web page where you can do that Прежде чем этот сервер сможет работать с вашими данными, вы должны согласиться с его правилами и условиями; нажмите кнопку ниже, чтобы открыть веб-страницу, где вы можете это сделать Open web page Открыть веб-страницу About Quaternion О программе Quaternion Welcome to Quaternion Добро пожаловать в Quaternion Joined %1 as %2 Присоединился к %1 как %2 Couldn't connect to the server as %1; will retry within %2 seconds Не удалось подключиться к серверу как %1; попытка будет повторена в течение %2 секунд Reconnecting... Переподключение... No SSL support Нет поддержки SSL Your SSL configuration does not allow Quaternion to establish secure connections. Конфигурация SSL не позволяет Quarternion установить безопасное соединение. SSL error Ошибка SSL Proxy needs authentication Прокси-сервер требует авторизации Authenticate Войти User name Имя пользователя Password Пароль &Thanks &Благодарности Original project author: %1 Оригинальный автор проекта: %1 Web page Веб-страница Project leader: %1 Ведущий разработчик: %1 Contributors: Разработчики: Quaternion contributors @ GitHub Разработчики Quaternion @ GitHub Quaternion translators @ Lokalise.co Переводчики Quaternion @ Lokalise.co Made with: Сделано с помощью: Show join and leave events Показать события входа и выхода Use shuttle scrollbar (requires restart) Использовать прокрутку с контролем скорости (требуется перезапуск) Control scroll velocity instead of position with the timeline scrollbar Полоса прокрутки меняет скорость прокрутки вместо позиции Request URL: %1 Response: %2 URL запроса: %1 Ответ: %2 Close to tray Свернуть в область уведомлений Make close button [X] minimize to tray instead of closing main window Кнопка "Закрыть" [X] сворачивает окно в область уведомлений вместо закрытия Show/hide meaningless activity (join-leave pairs and redacted events between) Показать/скрыть бесполезную активность (пары из входа и выхода и удалённые события между ними) Built from Git, commit SHA: Скомпилировано из Git, SHA-ключ коммита: Library commit SHA: SHA-ключ коммита в репозитории: Open room... Открыть комнату... Open room Открыть комнату Open a room from the room list Открыть комнату из списка комнат Show/hide Rooms dock panel Показать/скрыть панель «Комнаты» Show/hide Users dock panel Показать/скрыть панель «Пользователи» Access token file found Найден файл с ключом доступа Couldn't migrate access token Не удалось переместить ключ доступа Couldn't save access token Не удалось сохранить ключ доступа Logging in into a logged in account Подключение к уже подключённой учётной записи You're trying to log in into an account that's already logged in. Do you want to continue? Вы пытаетесь войти в учётную запись, в которую уже вошли. Хотите продолжить? Couldn't delete access token Не удалось удалить ключ доступа Open direct chat? Открыть прямой чат? Open direct chat with user %1? Открыть прямой чат с пользователем %1? Room not found Комната не найдена There's no room %1 in the room list. Check the spelling and the account. В списке комнат нет комнаты %1. Проверьте орфографию и учётную запись. Confirm your account to open %1 Подтвердите свою учётную запись, чтобы открыть %1 Confirm account Подтвердить учётную запись Account Учётная запись Room ID (starting with !) or alias (starting with #) Идентификатор комнаты (начиная с !) или псевдоним комнаты (начиная с #) Confirm account to join %1 Подтвердите учётную запись для присоединения к %1 Edit quote style Изменить стиль цитирования Markdown (prepend each line with >) Markdown (> перед каждой строчкой) Custom (apply regex from the config file) Пользовательский (применить регулярное выражение из файла конфигурации) Locale's default (%1) По умолчанию для региона (%1) Example quote Пример цитаты Choose the default style of quotes Выберите стиль цитирования по умолчанию Special thanks to %1 for all the testing effort Особые благодарности %1 за тестирование libQuotient contributors @ GitHub Разработчики libQuotient @ GitHub Do you want to migrate the access token for %1 from the file to the keychain? Вы хотите перенести ключ доступа для пользователя %1 из файла в хранилище ключей? Quaternion couldn't migrate access token for %1 from the file to the keychain. Quaternion не смог переместить ключ доступа для пользователя %1 из файла в хранилище ключей. Quaternion couldn't save the access token to the keychain. Do you want to save the access token to file %1? Quaternion не смог сохранить ключ доступа в хранилище ключей. Вы хотите сохранить ключ в файл %1? First sync completed for %1 Первая синхронизация для %1 завершена Quaternion couldn't delete the access token from the keychain. Quaternion не смог удалить ключ доступа из хранилища ключей. No application for the link Нет приложения для ссылки Your operating system could not find an application for the link. Ваша операционная система не смогла найти приложение, чтобы открыть ссылку. External link confirmation Подтверждение перехода по внешней ссылке An external application will be opened to visit a non-Matrix link: %1 Is that right? Ссылка за пределы Matrix будет открыта во внешнем приложении: %1 Это правильно? Do not ask again Не спрашивать снова Malformed or empty Matrix id Неправильный или пустой идентификатор Matrix %1 is not a correct Matrix identifier %1 не является правильным идентификатором Matrix Please connect to a server Пожалуйста, подключитесь к серверу Confirm your account to open a direct chat with %1 Подтвердите свою учётную запись, чтобы открыть прямой чат с %1 User &profiles... &Профили пользователей... Log&out &Отключиться Invite events События приглашений Show invite and withdrawn invitation events Показать события приглашений и их отзыва Ban events События блокировок Show ban and unban events Показать события блокировок и разблокировок Changes in display na&me Изменения в имени Show display name change Показать события изменения имени Avatar &changes Изменения аватара Show avatar update events Показать события обновления аватара Room alias &updates Обновления псевдонима комнаты Show room alias updates events Показать событие обновления псевдонима комнаты Un&known event types Неизвестные типы событий Show/hide unknown event types Показать/скрыть неизвестные типы событий Tags can be wildcarded by * next to dot(s) Clear the box to reset to defaults Special tags starting with "im.quotient." are: %1 User-defined tags should start with "u." Группы тегов можно объединять, указывая * после точки Очистите поле ввода, чтобы вернуться к настройкам по умолчанию Особые теги, начинающиеся на "im.quotient.": %1 Пользовательские теги рекомендуется начинать с "u." &About Quaternion &О Quaternion About &Qt О &Qt Use Breeze style (requires restart) Использовать стиль Breeze (требуется перезапуск) Force use Breeze style and icon theme Принудительно использовать стиль и тему значков Breeze Chat with user Прямой чат с пользователем Can't open Не открывается Could not resolve id Не удалось открыть идентификатор Could not find an external application to open the URI: Не удалось найти внешнее приложение для открытия URI: Could not resolve Matrix identifier Не удалось открыть идентификатор Matrix Incorrect action on a Matrix resource Неправильное действие над ресурсом Matrix The URI contains an action '%1' that cannot be applied to Matrix resource %2 URI содержит действие '%1', которое не может быть применено к ресурсу Matrix %2 Failed to resolve server %1 Не удалось определить сервер %1 Room or user ID, room alias, Matrix URI or matrix.to link Идентификатор комнаты или пользователя, псевдоним комнаты, Matrix URI или ссылка на сервис matrix.to Go to room Перейти в комнату Join room Войти в комнату Quaternion project contributors Участники проекта Quaternion Felix Rohrbach Феликс Рорбах Alexey "Kitsune" Rusakov Алексей "Kitsune" Русаков Confirm opening external links Подтверждать открытие внешних ссылок Show a confirmation box before opening non-Matrix links in an external application Показывать окно подтверждения перед открытием ссылок вне Matrix во внешнем приложении MessageEventModel Today Сегодня Yesterday Вчера The day before yesterday Позавчера Redacted Удалено Redacted: %1 Удалено: %1 a file файл invited %1 to the room пригласил пользователя %1 в комнату joined the room присоединился к комнате cleared the display name очистил отображаемое имя changed the display name to %1 изменил отображаемое имя на %1 cleared the avatar очистил аватар updated the avatar обновил аватар unbanned %1 разблокировал %1 self-unbanned разблокировал себя в комнате left the room покинул комнату self-banned from the room заблокировал себя в комнате knocked постучался made something unknown сделал что-то неизвестное cleared the room main alias очистил основной псевдоним комнаты set the room main alias to: %1 установил основной псевдоним комнаты: %1 cleared the room name очистил название комнаты set the room name to: %1 установил название комнаты: %1 cleared the topic очистил тему set the topic to: %1 установил тему: %1 changed the room avatar изменил аватар комнаты activated End-to-End Encryption включил сквозное шифрование withdrew %1's invitation отозвал приглашение пользователя %1 rejected the invitation отклонил приглашение updated the database обновил базу данных updated %1 state обновил состояние %1 updated %1 state for %2 обновил состояние %1 для ключа %2 Unknown event Неизвестное событие upgraded the room to version %1 версия комнаты изменена на %1 created the room, version %1 создал комнату, версия %1 has set room aliases on server %1 to: %2 установил псевдоним комнаты на сервере %1: %2 banned %1 from the room: %2 заблокировал %1 в комнате: %2 kicked %1 from the room: %2 выгнал пользователя %1 из комнаты: %2 upgraded the room: %1 обновил комнату: %1 and и %Ln more member(s) еще %Ln участник еще %Ln участника еще %Ln участников (repeated) (повторно) kicked %1 from the room выгнал пользователя %1 из комнаты NetworkConfigDialog Network proxy settings &Настройки прокси-сервера &Override system defaults &Переопределить параметры по умолчанию &No proxy &Без прокси-сервера &HTTP(S) proxy &HTTP(S) прокси &SOCKS5 proxy &SOCKS5 прокси Host Хост Port Порт User name Имя пользователя RoomDialogBase Publish room in room directory Опубликовать комнату в каталоге Allow guest accounts to join the room Разрешить присоединение гостевым аккаунтам Account Учётная запись Room name Название комнаты Primary alias Основной псевдоним Topic Тема About room versions О версиях комнаты (loading) (загрузка) default по умолчанию stable стабильная Room version Версия комнаты Continue with unstable version? Продолжить с нестабильной версией? You are using an UNSTABLE room version (%1). The server may stop supporting it at any moment. Do you still want to use this version? Вы используете НЕСТАБИЛЬНУЮ версию комнаты (%1). Сервер может перестать поддерживать её в любой момент. Вы все еще хотите использовать эту версию? RoomListDock Mark room as read Пометить комнату прочитанной Add tags... Добавить теги... Join room Присоединиться к комнате Forget room Забыть комнату Remove tag Удалить тег Reject invitation Отклонить приглашение Leave room Покинуть комнату Enter new tags for the room Введите новые теги для комнаты Enter tags to add to this room, one tag per line Введите теги для добавления в эту комнату, по одному тегу в строке Change room &settings... Изменить &настройки комнаты... Add Добавить Copy room link to clipboard Скопировать ссылку на комнату в буфер обмена Rooms (%L1) Комнаты (%L1) RoomListModel Invited Приглашения Low priority Неважные People Люди Ungrouped rooms Остальные Left Покинутые %1 (%Ln room(s)) %1 (%Ln комната) %1 (%Ln комнаты) %1 (%Ln комнат) %1 (as %2) %1 (как %2) You joined this room Вы присоединились к этой комнате You left this room Вы покинули эту комнату You were invited into this room Вас пригласили в эту комнату Main alias: %1 Основной псевдоним: %1 Direct chat with %1 Прямой чат с %1 The room enforces encryption В комнате включено шифрование ID: %1 Идентификатор: %1 Favourites Избранные This room's version is unstable! Версия этой комнаты нестабильна! Consider upgrading to a stable version (use room settings for that) Рекомендуется обновление до стабильной версии комнаты (используйте настройки комнаты для этого) Server notices Уведомления сервера Joined: %L1 Зашло пользователей: %L1 Invited: %L1 Приглашены: %L1 Unread messages: %L1 Непрочитанных сообщений: %L1 Unread highlights: %L1 Непрочитанных упоминаний: %L1 Unread notifications: %L1 Непрочитанных уведомлений: %L1 (maybe more) (возможно больше) RoomSettingsDialog Room settings: %1 Настройки комнаты: %1 Update room Обновить комнату Tags Теги This version is unstable! Consider upgrading. Эта версия нестабильна! Рекомендуется обновление. Upgrade Обновить Choose new room version Выберите новую версию комнаты You are about to upgrade %1. This operation cannot be reverted. Вы собираетесь обновить %1. Эта операция не может быть отменена. Creating the new room version, please wait Создание новой версии комнаты, подождите пожалуйста Room identifier Идентификатор комнаты UserListDock Users Пользователи Open direct chat Открыть прямой чат Mention user Упомянуть пользователя Search Поиск Ignore user Игнорировать пользователя Kick user Выгнать пользователя Ban user Заблокировать пользователя Kick %1 Выгнать %1 Reason Причина Ban %1 Заблокировать %1 (%L1 out of %L2) (%L1 из %L2) FileContent Size: %1, declared type: %2 Размер: %1, объявленный тип: %2 Open after downloading Открыть после загрузки Cancel Отменить Save as... Сохранить как... Open Открыть Open folder Открыть папку uploaded from %1 отправлен из файла %1 being uploaded from %1 отправляется из файла %1 downloaded to %1 скачано в %1 TimelineItem Resend Отправить повторно Discard Отменить edited изменено Go to older room Перейти в старую комнату Go to new room Перейти в новую комнату Reaction '%1' from %2 Реакция «%1» от %2 main Quaternion - an IM client for the Matrix protocol Quaternion - клиент мгновенных сообщений для протокола Matrix Override locale Переопределить язык locale язык Hide main window on startup Скрыть основное окно при запуске SystemTrayIcon Highlight in %1 Упоминание в %1 Hide Скрыть Quit Выход Show Показать %Ln highlight(s) %Ln упоминание %Ln упоминания %Ln упоминаний ThumbnailResponse Image request has been cancelled Запрос на изображение отменен Media id '%1' doesn't follow server/mediaId pattern Идентификатор файла "%1" не соответствует шаблону server/mediaId No connection to perform image request Нет соединения для выполнения запроса изображения Image request is pending Запрос на изображение в процессе обработки TimelineWidget Referenced message not found Указанное сообщение не найдено Copy permalink to clipboard Скопировать постоянную ссылку в буфер обмена Show details Показать подробности Open Folder Открыть папку Download Скачать Save file as... Сохранить файл как... Copy selected text to clipboard Скопировать выделенный текст в буфер обмена Copy image to clipboard Скопировать изображение в буфер обмена Save file as Сохранить файл как Redact Скрыть Copy link to clipboard Скопировать ссылку в буфер обмена Quote Процитировать Open externally Открыть через приложение ProfileDialog This is the current device Это текущее устройство Device display name Отображаемое имя устройства Device ID Идентификатор устройства Last time seen Последний раз видели Last IP address Последний IP-адрес User profiles Профили пользователей Account Учётная запись Display Name Отображаемое имя Copy to clipboard Скопировать в буфер обмена Access token Ключ доступа Apply and close Применить и закрыть Loading other devices... Загрузка других устройств... No avatar Нет аватара Set avatar Установить аватар ChatEdit Reset formatting Сбросить форматирование Reset the current character formatting to the default Сбросить текущее форматирование символов на значение по умолчанию Quaternion-0.0.95.1/client/userlistdock.cpp000066400000000000000000000156601412757327200206100ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2015 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #include "userlistdock.h" #include #include #include #include #include #include #include #include #include #include #include "models/userlistmodel.h" #include "quaternionroom.h" UserListDock::UserListDock(QWidget* parent) : QDockWidget(tr("Users"), parent) { setObjectName(QStringLiteral("UsersDock")); m_box = new QVBoxLayout(); m_box->addSpacing(1); m_filterline = new QLineEdit(this); m_filterline->setPlaceholderText(tr("Search")); m_filterline->setDisabled(true); m_box->addWidget(m_filterline); m_view = new QTableView(this); m_view->setShowGrid(false); // Derive the member icon size from that of the default icon used when // the member doesn't have an avatar const auto iconExtent = m_view->fontMetrics().height() * 3 / 2; m_view->setIconSize( QIcon::fromTheme("user-available", QIcon(":/irc-channel-joined")) .actualSize({ iconExtent, iconExtent })); m_view->horizontalHeader()->setStretchLastSection(true); m_view->horizontalHeader()->setVisible(false); m_view->verticalHeader()->setVisible(false); m_box->addWidget(m_view); m_widget = new QWidget(this); m_widget->setLayout(m_box); setWidget(m_widget); connect(m_view, &QTableView::activated, this, &UserListDock::requestUserMention); connect( m_view, &QTableView::pressed, this, [this] { if (QGuiApplication::mouseButtons() & Qt::MiddleButton) startChatSelected(); }); m_model = new UserListModel(m_view); m_view->setModel(m_model); connect( m_model, &UserListModel::membersChanged, this, &UserListDock::refreshTitle ); connect( m_model, &QAbstractListModel::modelReset, this, &UserListDock::refreshTitle ); connect(m_filterline, &QLineEdit::textEdited, m_model, &UserListModel::filter); setContextMenuPolicy(Qt::CustomContextMenu); connect(this, &QWidget::customContextMenuRequested, this, &UserListDock::showContextMenu); } void UserListDock::setRoom(QuaternionRoom* room) { if (m_currentRoom) m_currentRoom->setCachedUserFilter(m_filterline->text()); m_currentRoom = room; m_model->setRoom(room); m_filterline->setEnabled(room); m_filterline->setText(room ? room->cachedUserFilter() : ""); m_model->filter(m_filterline->text()); } void UserListDock::refreshTitle() { setWindowTitle(tr("Users") + (!m_currentRoom ? QString() : ' ' + (m_model->rowCount() == m_currentRoom->joinedCount() ? QStringLiteral("(%L1)").arg(m_currentRoom->joinedCount()) : tr("(%L1 out of %L2)", "%found out of %total users") .arg(m_model->rowCount()).arg(m_currentRoom->joinedCount()))) ); } void UserListDock::showContextMenu(QPoint pos) { if (!getSelectedUser()) return; auto* contextMenu = new QMenu(this); contextMenu->addAction(QIcon::fromTheme("contact-new"), tr("Open direct chat"), this, &UserListDock::startChatSelected); contextMenu->addAction(tr("Mention user"), this, &UserListDock::requestUserMention); QAction* ignoreAction = contextMenu->addAction(QIcon::fromTheme("mail-thread-ignored"), tr("Ignore user"), this, &UserListDock::ignoreUser); ignoreAction->setCheckable(true); contextMenu->addSeparator(); const auto* plEvt = m_currentRoom->getCurrentState(); int userPl = plEvt->powerLevelForUser(m_currentRoom->localUser()->id()); if (!plEvt || userPl >= plEvt->kick()) { contextMenu->addAction(QIcon::fromTheme("im-ban-kick-user"), tr("Kick user"), this,&UserListDock::kickUser); } if (!plEvt || userPl >= plEvt->ban()) { contextMenu->addAction(QIcon::fromTheme("im-ban-user"), tr("Ban user"), this, &UserListDock::banUser); } contextMenu->popup(mapToGlobal(pos)); ignoreAction->setChecked(isIgnored()); } void UserListDock::startChatSelected() { if (auto* user = getSelectedUser()) user->requestDirectChat(); } void UserListDock::requestUserMention() { if (auto* user = getSelectedUser()) emit userMentionRequested(user); } void UserListDock::kickUser() { if (auto* user = getSelectedUser()) { bool ok; const auto reason = QInputDialog::getText(this, tr("Kick %1").arg(user->id()), tr("Reason"), QLineEdit::Normal, nullptr, &ok); if (ok) { m_currentRoom->kickMember(user->id(), reason); } } } void UserListDock::banUser() { if (auto* user = getSelectedUser()) { bool ok; const auto reason = QInputDialog::getText(this, tr("Ban %1").arg(user->id()), tr("Reason"), QLineEdit::Normal, nullptr, &ok); if (ok) { m_currentRoom->ban(user->id(), reason); } } } void UserListDock::ignoreUser() { if (auto* user = getSelectedUser()) { if (!user->isIgnored()) user->ignore(); else user->unmarkIgnore(); } } bool UserListDock::isIgnored() { if (auto* user = getSelectedUser()) return user->isIgnored(); return false; } Quotient::User* UserListDock::getSelectedUser() const { auto index = m_view->currentIndex(); if (!index.isValid()) return nullptr; auto* const user = m_model->userAt(index); Q_ASSERT(user); return user; } Quaternion-0.0.95.1/client/userlistdock.h000066400000000000000000000044431412757327200202520ustar00rootroot00000000000000/************************************************************************** * * * Copyright (C) 2015 Felix Rohrbach * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 3 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * **************************************************************************/ #pragma once #include #include namespace Quotient { class User; } class UserListModel; class QuaternionRoom; class QTableView; class QLineEdit; class UserListDock: public QDockWidget { Q_OBJECT public: explicit UserListDock(QWidget* parent = nullptr); void setRoom( QuaternionRoom* room ); signals: void userMentionRequested(Quotient::User* u); private slots: void refreshTitle(); void showContextMenu(QPoint pos); void startChatSelected(); void requestUserMention(); void kickUser(); void banUser(); void ignoreUser(); bool isIgnored(); private: QWidget* m_widget; QVBoxLayout* m_box; QTableView* m_view; QLineEdit* m_filterline; UserListModel* m_model; QuaternionRoom* m_currentRoom = nullptr; Quotient::User* getSelectedUser() const; }; Quaternion-0.0.95.1/cmake/000077500000000000000000000000001412757327200151635ustar00rootroot00000000000000Quaternion-0.0.95.1/cmake/ECMInstallIcons.cmake000066400000000000000000000304771412757327200211270ustar00rootroot00000000000000#.rst: # ECMInstallIcons # --------------- # # Installs icons, sorting them into the correct directories according to the # FreeDesktop.org icon naming specification. # # :: # # ecm_install_icons(ICONS [ [...]] # DESTINATION # [LANG ] # [THEME ]) # # The given icons, whose names must match the pattern:: # # --. # # will be installed to the appropriate subdirectory of DESTINATION according to # the FreeDesktop.org icon naming scheme. By default, they are installed to the # "hicolor" theme, but this can be changed using the THEME argument. If the # icons are localized, the LANG argument can be used to install them in a # locale-specific directory. # # ```` is a numeric pixel size (typically 16, 22, 32, 48, 64, 128 or 256) # or ``sc`` for scalable (SVG) files, ```` is one of the standard # FreeDesktop.org icon groups (actions, animations, apps, categories, devices, # emblems, emotes, intl, mimetypes, places, status) and ```` is one of # ``.png``, ``.mng`` or ``.svgz``. # # The typical installation directory is ``share/icons``. # # .. code-block:: cmake # # ecm_install_icons(ICONS 22-actions-menu_new.png # DESTINATION share/icons) # # The above code will install the file ``22-actions-menu_new.png`` as # ``${CMAKE_INSTALL_PREFIX}/share/icons//22x22/actions/menu_new.png`` # # Users of the :kde-module:`KDEInstallDirs` module would normally use # ``${ICON_INSTALL_DIR}`` as the DESTINATION, while users of the GNUInstallDirs # module should use ``${CMAKE_INSTALL_DATAROOTDIR}/icons``. # # An old form of arguments will also be accepted:: # # ecm_install_icons( []) # # This matches files named like:: # # --. # # where ```` is one of # * ``hi`` for hicolor # * ``lo`` for locolor # * ``cr`` for the Crystal icon theme # * ``ox`` for the Oxygen icon theme # * ``br`` for the Breeze icon theme # # With this syntax, the file ``hi22-actions-menu_new.png`` would be installed # into ``/hicolor/22x22/actions/menu_new.png`` # # Since pre-1.0.0. #============================================================================= # Copyright 2014 Alex Merry # Copyright 2013 David Edmundson # Copyright 2008 Chusslove Illich # Copyright 2006 Alex Neundorf # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. include(CMakeParseArguments) # A "map" of short type names to the directories. # Unknown names produce a warning. set(_ECM_ICON_GROUP_mimetypes "mimetypes") set(_ECM_ICON_GROUP_places "places") set(_ECM_ICON_GROUP_devices "devices") set(_ECM_ICON_GROUP_apps "apps") set(_ECM_ICON_GROUP_actions "actions") set(_ECM_ICON_GROUP_categories "categories") set(_ECM_ICON_GROUP_status "status") set(_ECM_ICON_GROUP_emblems "emblems") set(_ECM_ICON_GROUP_emotes "emotes") set(_ECM_ICON_GROUP_animations "animations") set(_ECM_ICON_GROUP_intl "intl") # For the "compatibility" syntax: a "map" of short theme names to the theme # directory set(_ECM_ICON_THEME_br "breeze") set(_ECM_ICON_THEME_ox "oxygen") set(_ECM_ICON_THEME_cr "crystalsvg") set(_ECM_ICON_THEME_lo "locolor") set(_ECM_ICON_THEME_hi "hicolor") macro(_ecm_install_icons_v1 _defaultpath) # the l10n-subdir if language given as second argument (localized icon) set(_lang ${ARGV1}) if(_lang) set(_l10n_SUBDIR l10n/${_lang}) else() set(_l10n_SUBDIR ".") endif() set(_themes) # first the png icons file(GLOB _icons *.png) foreach (_current_ICON ${_icons} ) # since CMake 2.6 regex matches are stored in special variables CMAKE_MATCH_x, if it didn't match, they are empty string(REGEX MATCH "^.*/([a-zA-Z]+)([0-9]+)\\-([a-z]+)\\-(.+\\.png)$" _dummy "${_current_ICON}") set(_type "${CMAKE_MATCH_1}") set(_size "${CMAKE_MATCH_2}") set(_group "${CMAKE_MATCH_3}") set(_name "${CMAKE_MATCH_4}") set(_theme_GROUP ${_ECM_ICON_THEME_${_type}}) if( _theme_GROUP) list(APPEND _themes "${_theme_GROUP}") _ECM_ADD_ICON_INSTALL_RULE(${CMAKE_CURRENT_BINARY_DIR}/install_icons.cmake ${_defaultpath}/${_theme_GROUP}/${_size}x${_size} ${_group} ${_current_ICON} ${_name} ${_l10n_SUBDIR}) endif() endforeach (_current_ICON) # mng icons file(GLOB _icons *.mng) foreach (_current_ICON ${_icons} ) # since CMake 2.6 regex matches are stored in special variables CMAKE_MATCH_x, if it didn't match, they are empty string(REGEX MATCH "^.*/([a-zA-Z]+)([0-9]+)\\-([a-z]+)\\-(.+\\.mng)$" _dummy "${_current_ICON}") set(_type "${CMAKE_MATCH_1}") set(_size "${CMAKE_MATCH_2}") set(_group "${CMAKE_MATCH_3}") set(_name "${CMAKE_MATCH_4}") set(_theme_GROUP ${_ECM_ICON_THEME_${_type}}) if( _theme_GROUP) list(APPEND _themes "${_theme_GROUP}") _ECM_ADD_ICON_INSTALL_RULE(${CMAKE_CURRENT_BINARY_DIR}/install_icons.cmake ${_defaultpath}/${_theme_GROUP}/${_size}x${_size} ${_group} ${_current_ICON} ${_name} ${_l10n_SUBDIR}) endif() endforeach (_current_ICON) # and now the svg icons file(GLOB _icons *.svgz) foreach (_current_ICON ${_icons} ) # since CMake 2.6 regex matches are stored in special variables CMAKE_MATCH_x, if it didn't match, they are empty string(REGEX MATCH "^.*/([a-zA-Z]+)sc\\-([a-z]+)\\-(.+\\.svgz)$" _dummy "${_current_ICON}") set(_type "${CMAKE_MATCH_1}") set(_group "${CMAKE_MATCH_2}") set(_name "${CMAKE_MATCH_3}") set(_theme_GROUP ${_ECM_ICON_THEME_${_type}}) if( _theme_GROUP) list(APPEND _themes "${_theme_GROUP}") _ECM_ADD_ICON_INSTALL_RULE(${CMAKE_CURRENT_BINARY_DIR}/install_icons.cmake ${_defaultpath}/${_theme_GROUP}/scalable ${_group} ${_current_ICON} ${_name} ${_l10n_SUBDIR}) endif() endforeach (_current_ICON) if (_themes) list(REMOVE_DUPLICATES _themes) foreach(_theme ${_themes}) _ecm_update_iconcache("${_defaultpath}" "${_theme}") endforeach() else() message(AUTHOR_WARNING "No suitably-named icons found") endif() endmacro() # only used internally by _ecm_install_icons_v1 macro(_ecm_add_icon_install_rule _install_SCRIPT _install_PATH _group _orig_NAME _install_NAME _l10n_SUBDIR) # if the string doesn't match the pattern, the result is the full string, so all three have the same content if (NOT ${_group} STREQUAL ${_install_NAME} ) set(_icon_GROUP ${_ECM_ICON_GROUP_${_group}}) if(NOT _icon_GROUP) message(WARNING "Icon ${_install_NAME} uses invalid category ${_group}, setting to 'actions'") set(_icon_GROUP "actions") endif() # message(STATUS "icon: ${_current_ICON} size: ${_size} group: ${_group} name: ${_name} l10n: ${_l10n_SUBDIR}") install(FILES ${_orig_NAME} DESTINATION ${_install_PATH}/${_icon_GROUP}/${_l10n_SUBDIR}/ RENAME ${_install_NAME} ) endif (NOT ${_group} STREQUAL ${_install_NAME} ) endmacro() # Updates the mtime of the icon theme directory, so caches that # watch for changes to the directory will know to update. # If present, this also runs gtk-update-icon-cache (which despite the name is also used by Qt). function(_ecm_update_iconcache installdir theme) find_program(GTK_UPDATE_ICON_CACHE_EXECUTABLE NAMES gtk-update-icon-cache) # We don't always have touch command (e.g. on Windows), so instead # create and delete a temporary file in the theme dir. install(CODE " set(DESTDIR_VALUE \"\$ENV{DESTDIR}\") if (NOT DESTDIR_VALUE) execute_process(COMMAND \"${CMAKE_COMMAND}\" -E touch \"${CMAKE_INSTALL_PREFIX}/${installdir}/${theme}\") set(HAVE_GTK_UPDATE_ICON_CACHE_EXEC ${GTK_UPDATE_ICON_CACHE_EXECUTABLE}) if (HAVE_GTK_UPDATE_ICON_CACHE_EXEC) execute_process(COMMAND ${GTK_UPDATE_ICON_CACHE_EXECUTABLE} -q -t -i . WORKING_DIRECTORY \"${CMAKE_INSTALL_PREFIX}/${installdir}/${theme}\") endif () endif (NOT DESTDIR_VALUE) ") endfunction() function(ecm_install_icons) set(options) set(oneValueArgs DESTINATION LANG THEME) set(multiValueArgs ICONS) cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) if(NOT ARG_ICONS AND NOT ARG_DESTINATION) message(AUTHOR_WARNING "ecm_install_icons() with no ICONS argument is deprecated") _ecm_install_icons_v1(${ARGN}) return() endif() if(ARG_UNPARSED_ARGUMENTS) message(FATAL_ERROR "Unexpected arguments to ecm_install_icons: ${ARG_UNPARSED_ARGUMENTS}") endif() if(NOT ARG_DESTINATION) message(FATAL_ERROR "No DESTINATION argument given to ecm_install_icons") endif() if(NOT ARG_THEME) set(ARG_THEME "hicolor") endif() if(ARG_LANG) set(l10n_subdir "l10n/${ARG_LANG}/") endif() foreach(icon ${ARG_ICONS}) get_filename_component(filename "${icon}" NAME) string(REGEX MATCH "([0-9sc]+)\\-([a-z]+)\\-([^/]+)\\.([a-z]+)$" complete_match "${filename}") set(size "${CMAKE_MATCH_1}") set(group "${CMAKE_MATCH_2}") set(name "${CMAKE_MATCH_3}") set(ext "${CMAKE_MATCH_4}") if(NOT size OR NOT group OR NOT name OR NOT ext) message(WARNING "${icon} is not named correctly for ecm_install_icons - ignoring") elseif(NOT size STREQUAL "sc" AND NOT size GREATER 0) message(WARNING "${icon} size (${size}) is invalid - ignoring") else() if (NOT complete_match STREQUAL filename) # We can't stop accepting filenames with leading characters, # because that would break existing projects, so just warn # about them instead. message(AUTHOR_WARNING "\"${icon}\" has characters before the size; it should be renamed to \"${size}-${group}-${name}.${ext}\"") endif() if(NOT _ECM_ICON_GROUP_${group}) message(WARNING "${icon} group (${group}) is not recognized") endif() if(size STREQUAL "sc") if(NOT ext STREQUAL "svg" AND NOT ext STREQUAL "svgz") message(WARNING "Scalable icon ${icon} is not SVG or SVGZ") endif() set(size_dir "scalable") else() if(NOT ext STREQUAL "png" AND NOT ext STREQUAL "mng" AND NOT ext STREQUAL "svg" AND NOT ext STREQUAL "svgz") message(WARNING "Fixed-size icon ${icon} is not PNG/MNG/SVG/SVGZ") endif() set(size_dir "${size}x${size}") endif() install( FILES "${icon}" DESTINATION "${ARG_DESTINATION}/${ARG_THEME}/${size_dir}/${group}/${l10n_subdir}" RENAME "${name}.${ext}" ) endif() endforeach() _ecm_update_iconcache("${ARG_DESTINATION}" "${ARG_THEME}") endfunction() Quaternion-0.0.95.1/cmake/MacOSXBundleInfo.plist.in000066400000000000000000000021231412757327200217030ustar00rootroot00000000000000 CFBundleDevelopmentRegion English CFBundleExecutable ${MACOSX_BUNDLE_EXECUTABLE_NAME} CFBundleIconFile ${MACOSX_BUNDLE_ICON_FILE} CFBundleIdentifier ${MACOSX_BUNDLE_GUI_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName ${MACOSX_BUNDLE_BUNDLE_NAME} CFBundlePackageType APPL CFBundleShortVersionString ${MACOSX_BUNDLE_SHORT_VERSION_STRING} CFBundleVersion ${MACOSX_BUNDLE_BUNDLE_VERSION} CSResourcesFileMapped NSHumanReadableCopyright ${MACOSX_BUNDLE_COPYRIGHT} NSPrincipalClass NSApplication NSHighResolutionCapable True Quaternion-0.0.95.1/flatpak/000077500000000000000000000000001412757327200155255ustar00rootroot00000000000000Quaternion-0.0.95.1/flatpak/build.sh000077500000000000000000000004151412757327200171630ustar00rootroot00000000000000#!/usr/bin/env bash flatpak-builder --ccache --force-clean --require-changes --repo=repo --subject="Nightly build of Quaternion, `date`" ${EXPORT_ARGS-} app com.github.quaternion.json flatpak --user remote-add --if-not-exists quaternion-nightly repo/ --no-gpg-verify Quaternion-0.0.95.1/flatpak/com.github.quaternion.json000066400000000000000000000012231412757327200226410ustar00rootroot00000000000000 { "id": "com.github.quaternion", "rename-icon": "quaternion", "rename-desktop-file": "quaternion.desktop", "runtime": "org.kde.Platform", "runtime-version": "5.9", "sdk": "org.kde.Sdk", "command": "quaternion", "finish-args": [ "--share=ipc", "--share=network", "--socket=x11", "--socket=wayland", "--device=dri" ], "modules": [ { "name": "quaternion", "buildsystem": "cmake-ninja", "sources": [ { "type": "dir", "path": "../" } ] } ] } Quaternion-0.0.95.1/flatpak/setup_runtime.sh000077500000000000000000000003371412757327200207720ustar00rootroot00000000000000#!/usr/bin/env bash flatpak --user remote-add flathub --if-not-exists --from https://flathub.org/repo/flathub.flatpakrepo flatpak --user install flathub org.kde.Platform//5.9 flatpak --user install flathub org.kde.Sdk//5.9Quaternion-0.0.95.1/icons/000077500000000000000000000000001412757327200152165ustar00rootroot00000000000000Quaternion-0.0.95.1/icons/breeze/000077500000000000000000000000001412757327200164725ustar00rootroot00000000000000Quaternion-0.0.95.1/icons/breeze/COPYING.breeze000066400000000000000000000222301412757327200207770ustar00rootroot00000000000000The Breeze Icon Theme in this folder Copyright (C) 2014 Uri Herrera and others This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . Clarification: The GNU Lesser General Public License or LGPL is written for software libraries in the first place. We expressly want the LGPL to be valid for this artwork library too. KDE Breeze theme icons is a special kind of software library, it is an artwork library, it's elements can be used in a Graphical User Interface, or GUI. Source code, for this library means: - where they exist, SVG; - otherwise, if applicable, the multi-layered formats xcf or psd, or otherwise png. The LGPL in some sections obliges you to make the files carry notices. With images this is in some cases impossible or hardly useful. With this library a notice is placed at a prominent place in the directory containing the elements. You may follow this practice. The exception in section 5 of the GNU Lesser General Public License covers the use of elements of this art library in a GUI. https://vdesign.kde.org/ ----- GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. Quaternion-0.0.95.1/icons/breeze/README.breeze000066400000000000000000000002401412757327200206210ustar00rootroot00000000000000The icons in this folder where imported from the breeze icon set. Repository: anongit.kde.org:breeze-icons.git Commit: 00f3ea7a763dde4d676ece8186c1cdbe52f6c2fcQuaternion-0.0.95.1/icons/breeze/irc-channel-joined.svg000066400000000000000000000074161412757327200226540ustar00rootroot00000000000000 image/svg+xml Quaternion-0.0.95.1/icons/breeze/irc-channel-parted.svg000066400000000000000000000074431412757327200226630ustar00rootroot00000000000000 image/svg+xml Quaternion-0.0.95.1/icons/busy_16x16.gif000066400000000000000000000526741412757327200175520ustar00rootroot00000000000000GIF89a  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~! NETSCAPE2.0! , HA$؎AᚶyӱXp:~eD3fy+׮@qU9IGpݸq #Eu-hl(=SGМ&RsΠ.d֬oǬ n'mx<0 ! ,  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ HA$HNsђp\7X0\8~%̳_dMFmUܶqGP6m ԉ@@5:pM:+axv 2݂fZ*KM3Ҹ^½ɶ6~p=۶po} ! ,  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ HA$8?q0\uQX7~ۺ#G bv;fm9ڶ9q`9ln{p`k׸u6YZQz H*O}׍(n;J4iƑEl 7BnL޼L0 ! ,  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ HA$(o׽[:M,?lĉ+G0ar6$z߲lU4qq804h )(?@BdT[R J$X<㶭`9̙&B538X]p5~p˶p`n} ! ,  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ HA$?nS0uuX"i‰wP۫b lWo9e LY6)&M:yAmzv7mcsl719JU%jƯ,ad (.! ,  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ HA$n?m;ۭueX"?eԼy;F7N`wĵ+xN8#ke"M aÀ5hPсPy#mNxM D2,pĆ#Rƌ11Ό5*f ҅9Lx`@! ,  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ HA$-?lSx۪uMH0Ca͸q:.7Nv޶+.W8cث5'[ϟ%Hkn|HڲmA 5~: FH a N2Sp3-Ć[~.M޿! ,  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ HA$ ?kySxP'vH0[6~mۆ-bB'N :vڮ+NV8#-iۡC7[lg\Qu1qQ XU秒/_} '.]wBlwڴL0 ! ,  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ HA$?hq0$vXCXe֌8#:uמ+Ń#-V,gщN7XZt`GkW+m-\;u;JƯ#]d/,fCA$nրoVEL0 ! ,  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ HA$?eiۡuݽXКS]{F8tӆy+N7묽#MuN`ϟqVt 9D#W0&{iXo7v=-[5%9EƋ\eW`@! ,  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ HA$maa ڞuݍXZ5~`Ylp2; @sV0]&p=sG)R ӦnF-8(r| ^_@l&ndƌ@d[oۂV49G; %b L`@! ,  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ HA$^]u]Xp4~RULo"ۂ8 [`:I); Nw:?-zt`9<5.iei&KoKzȽDn4Zcs@~ ! ,  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ HA$m?[Uu1X4~BIKm{\8 :EK|V:?-zt98`]c+OJR+gr 4XCn@x &! ,  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ HA$x?XQPuXY3~49s&+?mb\8d`:AՎHt : AZQd#N8`)WOvx#(L?wرv9rᵫgܻg{ǯ.r|W! ,  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ HA$hm?UA۶u͡XP2~{&y\6lyh@rNV0]nͽjGP"E bNOB.N lǎ+ѩK4~ W/\w-|,q tT]~! ,  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ HA$XQ1˶ُtםX1~r3F?kZQ7}3 :9;Ŏ`6BdF: ZڰaGc:a SΝ;QW~ș37M?p n۶v-l]~..! ,  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ HA$HmN!ö0ٌtqXp0~i j8Yz`:5Ď w֫9?-zt;tծ`;qUNK \fyfp4w׮[En!tm} ! ,  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ HA$8K uAX0~`U?jš0[75 :1Ďvv9رZP-[GT`:Y @uT1FPIlO[΂鎹Cƒ̝LMBf ! ,  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ HA$m?HP؆tXН;~X?ic7r =v{Net= :˜v?t с#NR]c4PT_3j6v$p ӑGr  L`@! ,  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~ HA image/svg+xml Quaternion-0.0.95.1/icons/quaternion.icns000066400000000000000000027043131412757327200202730ustar00rootroot00000000000000icns ic076NPNG  IHDR>asRGB6IDATx _UuL&3Ƀ$!Hx?T H[zZ?jն~Zmmj 3@MNwgpfB $g{k><)yydSpXb/O7ͮOAtnfo}{{Oӕ=MPSp3v{4{;{#|͘1hfۣ)h߳Skte5@0,TED NA8|W<9cagG:umq>09ԞnnᶫnT^jk;j>tVCc @/ MLg/ȫg(?U=\ &9ےr?8GUfwi􂵓t>}buѳ$8~D;9жQoFv]r4ڨ'Yi) LۆҲ7 A_p/n꓋q۴+g0+}5aL¢rn"GЭmFWGJO $ ,O;ݫJ5~5FE`=>?sAR2ۥr-)mUۦ9;}V0TS]AJ^ri9i?mӧ(TW۶v/=ޞ3w .r_8%kol rn:d(8,c5 Z*el )͛ёkcƨx:Yr{L3;\ުí^ig=bc18_J_!CBkpﵱ1 _䰗m]:P518@ԁ4+Yi jMs{؛@="}1sBg4ݏU!)$S5ΣF*^dbuTgomAE `>8tvGֹ֊%gp;u=@z^#wԱOK+[[:޻ݠPc*Z w9>SpuW$D81|q=䕤j6xejY>ӔS+]yE]~MkS';fx<\km8~R)F*e4SWS#v갂kSHӎ67v-h4$HP%ev98waQ7o4ᛱ&GzFG)8k̴F:zI`Z:yAg:n4-f\ܟ`ba@wMM7Mx,K6X32qu#3I{ s:x^#|qrZ30ˣi#Xhz@g:琮b]BH$a?H > ˠ׻dX_zR:v^'`%t~.r ?5DgיQ#K2828[[<-]dZ_\3ml+n\nY5Z}SszSЗNP[/`!}+:+?އDJ"6`1SOg5Ok$*ǖ_wTӝ^gt/xn>6R.ύԝ4gZZ0+4{,eצ';:.I 3øs˾娷> +V+/lyCfEXֿ/|lq֩^iHa@ Ö*ϸ#u۹ulhsugmѺF<FU{Djx̂H[XTrn v4iU_ +uruo~fu]6DnK:y(uڗmcV4E}4/9|;W}@ۚtߓvyN5M#Ȍ4蜭m9W3bAh !:S9|?.O_Qaa\ۗ;5?QSn6(c3R8жLJtǪh6nI4bWFd t-F[Wr\Z͋@ tٹ=[҃kC" zѺN#p^ {%}AiW4h U%:^ %*80KXE9_Í~]B638xw&:IdxI:`! =fa}v >8uWbF4B0XY|@;//Iedq֢LN0'f.[8>~?:N"E}s.ӻ2#JVoOzE=+&M3ɏ6|uv)X̓A#B[MĨo]]zi=/n.G&0*9)' ~k@uD}*'aD Q&'\tͱ>?qϦ9rrE^r`vNOaG7e9y*̽WFw}2-u^l:G8k"hK/4KgeJ:Bպe9xj' z@ue\#v=ޫk*[3pLtڟS_|x6I?nu%G !AQ^tsUg)k#-_ކ5¢' A]IoRWɧ` Mg~XMEgoZg~ƦF)x7W3]ꌥ08 Ynê !9K=v~JyC6긇Y:=J|֙Y:^JOS#$8չ7~4><Θl6yƽ8ugDU 9cB,wFR۫` wZS뫑/i:zAO:X7Q~یް.ݵvQgkB*!Aecaj]2bMreqKϷ=ETVnZnH(&Uqo˘B}S76fUs!"w Pr3l PF#F9EԸt$rQ|ĥo8a=RIs"vL,eח j:/]EvFyLX}vSFsW$ ʽ0m_UҚm9)9a0^7;KbΌ'[9߮r7YY8dg6 4U;VLOf꫺[>%34Sm9de `Ə>şRП[1S3W#81훜=)U:R9m/mU0!X|Z:^Q hK|X%\^U=R[8;;d 䅃-h'OڬD7'r})6AE/OO(4 goz -zLEovCĄ"4!/s"ݹLQ\3^x{l^0;WvVεÊ -<~ ނ: *ߘP2n52[7V* H6ST8ܻ9qaO:m0pgB_Yv6&/T :&W=xq2^pGE]~p>J8C6{+ϜfO윚 TT-%r37ڨ#2|0k|m>;Jon%$tzӋg[i&`&bQA_9͖%.(O_]٢ǥ#娇rӪc'ا2>4Fٺ3zjeA:s|7?=}`GZn-U6l8i yRIbDǨ_$}>T%syqѲNx͑ LpI* # g撙1Z@|7axp:C4-7,^%#^8LOj$Ĺr?I>SQE=bHnmҗ[´!#UyL:ckzNo)P$}&j)}-B9=E Z뢑4'!5!YĖgK+>!Le7;=S-ʭta=KwCӉwYG@K:VUƦa/Ü$4iQ,:hA1a=ټWrόQRWٜڵiF [pDc +iU@`$|Y4wL<0y=.Gt㣺Ρfݜvi`ru=F k,Gh5M<:b$:0%iҟ2Ur#t[X)?8FN;& C6yw<8 hMG$|Gh$Wߢ \*U>~ANaDf.'5=SuU/ђ*]^ll/btVFs0.WnNkp2N]$-b*G6AxhۣMi`vcKCif\K|Uup\ɀЯonI.Л t|r&E:;jjY9zcpHh ̾2cd\[8燒ac9WfpiՙyGBA{9 |.0}j=cZy8օd^eyz0gkj&L£wqkzfRs1|(90$D]:7dZQrFR}\]ș'*:C!]ԏlr&L0r'-m]$̉/goJC43{[P":CI:dY6) ޞ C9zd< یr9$7lR3O6v=C`\-+ .,^]_wt0nĥB{C8rCcBں=mJ俈7<<2S@+'Z`Fvוi^69o)Ǒ1@eAgZS }bdZ3Mk}z: fr^(%քwgNソR/7.Cȸuզ36Vop@86<zgtK[UY?4ۂ͉E僐m* g29V5uNU HNX]^ utk*( 稥pE`фQP ȚmVW Q;N˙ aW9hP}蔃!r,T>Wu.whegJ/ޙ( ΙѺ 53&Gnu]^GG9. ~64E3/ehk,؂N0H7r:ȚmFՅl,]~h \%H։'mگp`kHøXv7gV 22RU\F|D9h&.rhdvѦuFwd*zW]踳/7^rdv Z7:7Kw:\-1ݓS t i }#& [k;RucAFXʠǚ6A j'je2md̟e'e. :X!3 3OEm;х\ѐy v{~9} |SMq9آy+dQwy غ݉LUr㸣 Tr|sX2"tFoŚ'W&>GeZ!Ll8*MÙ8 X`.)S>iǓ*;WlR.#i!6D9G# < t-c>Pf)Ѳ&.Kms+\g4YʢuWw$tuϼ8?iG)kRn[,3So)guc/dgXX+)^c2;lx:!yNї8uG Ӻt 0l#XBdNuY4tHY?ږ`CX*4F" u?fd[,2A ii#8nxձBfo18PvC+So5𿽸7O@C3`A~dr2ٴIgyj:w>eDulݐapFurfnKT-wpQ> 'Jm=E׻fr?8: _{Z+*ceb`Wԍl]:~Y83Z"5A =8L܆Tv:xk~V_Og)>;YdH(wTo[]XNU`Imnc c[[o@ 3d^GFAaAӨ,fuˡ]}9Kz x.H,r9/™Ag7<;86+^T-3 q^V'?DeЬ4]Tri&l\j71SM">JɂV`zק, )]`| . ]B`a'?9/_r$SU+G1ƥViX $ a[(p9Αy(\-i7SU2gUJC5 W78bZ[^\K߲6+ xRY%K5||&SlDp55nlf@E%|w&};ᕇF"w !vj5`ޚȩnG"Udht0؃ TñH==M899+Ō9?ݩ"zBX5m}6[9qL6ٯ> }s~M7!&P Eρf4tB?]|o5?FD˖RmEfe!PS~k:0 y;.jSĦ\1Ex$afw>mByMlU*!D1d14p+V̝*.fm8:ڎz$K8%sL# ުo[t3iW7n)a^IɇܘK3 '`%pfY l:ߍt^eelN  6.K"!g~ىJ2Su6J; `v\ppYZ?60;1}J>_ ۳sWeu$0U|fBw O?'4i`R'skur ѸL{Ͷ@Ogy~°|tEk뺤E#cq$qWYqL})ϛy_NnW .$9e8|2B w6GUG53mȇxPn8&c:9O!pA x e Ӷ,̞F+AKh 9iv ^}M%twv>uySav0TlН<ӛؘ@rGy[Ѧ:j9LW޹5Km/>q ]m&kp 4?҇{;|ODyyQ9{j=JqA[x/OF^A.Ç_iӌ9i]0(GN|i#ݽnT?ӳ7[e7N)y괄\:NIM(qu.$-ӝ|adtB푚e^^{I$B) tgax|m}j$2d&6I.$՗}\IFҤ!ξ[d(Dc07$WP>Nv+x"I3ӻNgM@O\{f :Z@L#7nJ/}gzϴK𻗬 yzUΑ >M:*$/|l &;"C @I OUbբsN'Im. < Vk7hX#v=pc 7GY*E||g:xպ`Rِm$,0&_:lmJE-;f/|ª%8r)}G7LGI՗x02)dYk&@26i7 %[/{>a$]zt[0hO @wPS[ҟgY e }}TNpSMRV m밋~;A;0iYTa^="qx}2-ݹv(?O Z l*Ѵ׽v[/W'?|fJkyHDRf=f9y襓}hM.<\THT"PHU#p^{SWH3^; Ipc #軚 3Ո4 ʹs{F\| =H2h فh-YĨKzWy1>~d~t4$/ҺN]۸P2ЙЏ? mW P݃ケD /M!Y\kwZk=p-xO!\-5\Nw8[U:E|y}z3=O0kD3h ,oW<* Y`:OD@u(Fw9ʨdK_+c`P}!wߚխ8XK`_},pO[O/Qg} eWo;_kzf`Q>>fX2*ᤨ+CWݷ%@Px)sLMoyt[khآڃ<f\Pvy7(Q}ϚX}!`Ug+D~^:49tz ;31LkgK|+uc/5)큔ߺrxzm$=9:wt>&/pNLVS+97)3/ҔsUN7!&DZ[(͞Zt#;;vÃmC~y~C Q(5Oc?rVp~U ڡL %etIJ];?|Uݿ| Rn_OEo;K9˔3F}@z|E`Ŧ%?Rv~zcnR3@n1d.lL'r@3'y." YZ2j~8 [;r :8ru ,8HR%{\pj0ʵtv[ʈ\0,ߊ =c,`zh yH|d<޾Rͨ-ωPSV6e*|Khu1~ S4F6雳G b |yS'/p5:Þd*# qQmji?'3O>imXZ+Իٸ9rG+' ;LWt&1,P[OLFO*b1Y3u.Gxa|5?8dx߇ܾBz "W՜9&4)'¢Lyex+-׭Z4D~#̆EγJu1#ad a}lDE㺣*'C7ڣLW{g*NFO}oژ@{:'ȳ YM/S?*c>ڏߑOr簞 2HB3 %_HF=2 Q9pݐԓ^ÎG$^t՘E,qB3L73 WQgU:$ c+Klc>V{p^(21lph?dfx#KC(z{4VVKh߈5[ v, j'8CvZ_ Apa%?~nfNj'Ǘi36ǺJ=d-1Up}lZaљܝz͠fTZ`o4#Sm_::q3%Aؽ>ѯ|_SiFڃ+c+S>aA07ݾۣ)h߳Skte5@0,TED NA?$5nIENDB`ic08ۉPNG  IHDR\rfsRGB@IDATx mWUιMrғ.DAB#bT,TiYYjegRД"ݯlUF҈1F77yc9' Xܛ{Xħ2a"p.E8"pE<#aI"< ,ٵ_c1'Z>s[XDdG[g6XcL>#D8,@N"cfE!Za"pW&K'#T&,AN|]"'WN=b>"cfE!Za"pW&K'#T&,AN|]"'WN=b>"cZ>, 0e1^D$@#l",ⱈ-ƹ-Ƌ,"pEDۣ-3x,1F`& XEq 'Kxk} 8"Oe"DDˇ]@,"prDiюx0/^2ypںyy7-/8zzm/j:QD _ KK8[Ȼ7,Ow߷ wl=ʧ-U,aZg>E^}sXz]$KKՉ2W9p4-ݩ&@nR ?<-]tiO>K_ >`@[я=׬LLkL lZTqr]@j'/LbA@/Bi })!nZYÕOW{."^m}vF-W[ud*jxZfW08g Z AWLByPlLnنb5?MmE;"ю=Twrb-j/*BjoN30t" Ś:ZqRðJ:t ip@lS_sxg WW޷Wh[` %/Xu^9syi:-7k-TK[}uѪlfB"dѨAKot*>]+~ *?.MB^/imC`4 Y5mNW i]㾰rUl*^)/S1ooDp9"R0X9꤫~UNz=_VêBw[ ;Mʔi&E=ٺ1vPh-b"7X \UmVZMbPE,OU -P_0I7Ka*.Dk|@˿WžW== ND:ɧ nVJnӎ`k6o/֒&,8i5׷=%ZadkV8~T7/Uڶb)}`կ=*zN[!lM.^,J5en7[p/7Dapvnkզ1 t铮OkGf~'VQ?oGЧ88߉?]s~:*~CR4g6ޕiGxi?_` lk6Zp7R:!׸5(͛4wkgYYN N5L Woo~-7eTu>vtګo޼a;.eziq;~vutPL,yQ$uT/^ЙxFAItY==H{< oa^?hϗ48#x֥!luzHV%y"0${U]!)%IA+|}BسQN_vݯ_kP|ŒN9ۖ-ǹMJ )ӛOb!(?j k$eGp4of z Hɽ:EK_G%3GQV"ƅ"o/pCRo7;Nɚ0ToC=!ZlD>o? ݎFRzkp%_Dl-WW IsdǟckZ%{@ WN?`?ݸabsߪO$cy;EfOU>/VoUv*QګK>vi/eO`AK8 SgF'Vl h,Zw굇W>-y.p$gE`x5{7/mxީ^x|{;T箻HeHiӨ*ՙl$P mP9|3 LWrժ؎['tc˔gxApP'{RNuڒs{8 ("k[v&VW <ΟL(#܁hЊ:4/4[8Uf) XA8CfuwXKؗىtxѩABoZE [tD0gyx{l *Է( 8U$wϷڑ0XLXc{$\&w?2%~.U>ZqjBo{]jTfL3 p\`tR 7[H8%ǝꉗ* ]ik^mqPD_}l\1凕=>w+S:2ߩIfӊ74@GY(|(tZ%}q^llYI NϢ 7bBa??kF_pf l4CL/slBÊ>ۮƓ>#\M=;tK/M!n3V_SDdn]e~fI6&]Tm&a1(;yjJnk^6AHm.E`V)-^`b5Zr(~Glw!3A9qЃä:?F, ӫx̣2Ptӕ{9g>~}N|c3( Z $1(Z f K Dor,x(]:h9NR-J=Ya,[ُq+zg2t@nogwf{+~>)XgzQOm?^Y7}v)K[~o˕kҿdц}. 7+:d&\ <ʼn>ƵØErNQ{~gf-E㜄}2#b+?-ͬY|Ǽb!6GEOxIJT|~ ?/m|O bGpRAfMڱp*oҤEWB.}d7rqOCjo mw]O*x8k@풾aڪ߱Y{:8t|ێo麇 x>8<}mY78q_fϳ}Z/lnƗp/vKTphxў?NGh'Qzmhw7EvR;i!V3&}lu%}̷1R*&8IBCE* OBdIjXpl!p)IC gi/9S;=9SCCh#H|=@kb,}^7҉>(4{<Sy%ׇH<~_ICXHN[aɌ`&ŵh)VMw%G/_+)UKZ >iXN%=G8<G&v!0x2hpSWp{gaz9pDo0Un?>=zxz-3Qk5UH#sxy>( sڂ%[/Oų7|1f P'Ljw@84}>"D>jH/rXH\ 6L/xFц#U\Vע[>עqOgt[";HGנ+Eu,/D 6LzNd\zЉQӉ?2}^M s~2Qr6]tD$%Pe9' YdXO/;/`^:+%a%8=HF ZpUĎaĕ[]r#Uش7bdNzܲ.po,OL\f_\JH[9CM>&)~ofp&va=zN=?%x|;ϳWotC]١x>د<^ېzβn 0O0o\ctΩSO|6]wԷ8fvDpU\"d|%1h2QX2M&dk?~j^~a՟ +0'ζNkjf[5|ʖ5dXcf G_e |(c>#{9,O >>ƴnIsEӍw>ߧ@Kr Up?i݃ݣ ,R lNW~hDcl NIzW3f-]ܦ>E;~ <%na[FOd=EO.0PNh^wUjTT.T1e(2hPP2e S]4uTG+y>ݱxݭ{?wuV[ts!2i(gn} ܦ]~c"5,N@;mO;Dp`>tX_iO2]숉hwzx^z|2@v!'_ؿPI>)mwi8(⳾G̐PdO$uҮH>j>[X$jX0>{Э&NicU & -"'6E +( 5bl:S#[NƗ6<$LW*a ٺŚG7LWVl|wޚegnX\E 9n>Wg4avVF#Ì=abR,m+gh Ɠw*a/9) :ҿ;O{Pv` iJ⤕#ܒqm zYÍ|#UgIpt~p5lu!K}o'Lopv'ԅ Ot9LPqtQT8]{-/Oq𰬟}W\4ޢurߥ]HڲMr\ \YْB:ɱ$On ~or?utNd*l`+ژA/9@9t?F[fD|i#aP/-~$L_䰼:#z|w;0}D+Cq'[.cGN"Q=DL`GGgzMKuN~f?_jY+ o]swܢtuʢ>k`2o6FR$Akh]Ѓ~㷣irKr'y:+)Ї^u.M{)1~+O?wyz囦ܨ0=OdcTYÏzaO};WfQ~ʿ:ʗ8N;anQ:4ڷ:j.NZȹ1tQgyea9qƏ~f?hsx_ *7y35e?)op~5޹ٍ!Zٯ>lopOdSgBpY35e6dol`iM99' LizKO95S2\so8k=NONb=zMf P;nT_s8n7\Od^#G?֦" R*?]O[}%*}>yz-IHEW66G-xق.S9fLo#gu[.6>ϡ0t zӗ Ϧ!?O_RۧKv)FG,kKלv'NA^?gV'xyQBU7ir-/ԟ_襵EL(*ȋor"HN'!}MK\:SOܥU_OowMzN EYj 8LAATxk!袘?$B ^yBXGc/um[,C4?'o߸9Y7L|xB@~H6?Z3_(xcP<|[qr`z?x~}tNSjgpoio7Xa]p ŲZAyS3Hͺd}O< a{o ~Gk2lm (!*bȴ70hH(BOEV?xB@x*МٖtVqzzdߡYuJ'@gLbG$nx 3>}':}J @;bbm=\er;8q oް-,Oa'/8־Z#TˏY@Gn.-FACGoq?w?b7d2> AJZĬvRVlMGƴX*%mɚ'7nzsHR1Sc(S(P/} 5gl9dJe4;`z+Mwss#zݤ$IV޾9. ~U+eqNcjK>X^H0x⨐ck 8XȮe%x#cڸpSo;}S;"+,YQ!U1F'ѰJ Pmrѡ8C5wE'?RPg"߯:!ӗAf$/> F w'W Lb4 Z2NF[I^|%_<ZG>u)uy%Ngo~˷LpC3nvc6kkOgZ<9~9)4~~o&v\֓AyZ4ߤR,Gtw>aoTdBA߳}Gswbjs=\o'r\9@2Ah>bz 9~*ٿD#tr;I^UݷbO,6,}{GWIkG'8c-+U(r1rc'?4w+;xm>@Wq̂E2|m /"H^xhfo퇐/Ʋݲv!14b`s#0Ϛ/4QE\ uM?{ 9>EO/P ?l'_kmžQ4*^qA?4#V-ӿkmGkS_Ƴ2HIQ;LZNފd,6C.6j_ i0$V%ml9\l U4l3ga47 ]xQc`ru:M׏>ϏyGnH/,7t6PzLT='^tδ}ҡ~o|OcB7&P\iWtq>f?s\Փ<Nj|o^gDn=ϟ/TB >|U"m/_jrdpwԟ҃;A>p|F N㕤o;>X4_K]K]O[cyH&bI ~N}茿*MZЧW<.N{>׷P#y]ӟ||t<Ӵ_ہ484\j!Fcf٭Qt|}ۖz.y\{ S_Å~#"O~VX,NT+SooW'+wt~D. ;.<=i 6 /o!>LT`,AlZg3]&|_Ga#}6 (^<dpfË>+ߣpz't;{_ݖL{o?R1`'xB ԧvIV8 , .|.{:.I~Tkb8.`{NbVReE -c!Y9~Y <ߘCTru]O̫n]z'[y0\bn@1(-G ^Qܩ'\m"tSn/tB$35/L}Vc&wh'B?.e9Ϳy97ƙ@o,*fwXg(v^#$6x@Z!g|78‹=B9u\oN|'GǕhP'U$AK>u-z|V]#jQ珮{҇n|ފ"x?SҥvCgUo 1K(c5"GӃ2|K4N3l9^qjG!_ BcՉ_#|[EK:O8}3xt- j"l.YX)NSH𸪯g,:-17,O/ݧyv-="01!A-, {ӳܴǸvϷŝ7̓zI*ԟvY-\`g >oI-m LZ ?l\+It͑c/A\}glc O$g`[V+>bs I{N.׈`&74Ks 1][8"px4W|Bï|ێhE}w핪G'kLt+%`Z̶dGҲt,F!]bx/q/Щ{8IdiJz۔lG 'OG£sH=*~ԫY6}'d(8z1h-p[`vW~:~T%l`W ?)&}rQo XFj̡78 jIFT}>*>٣ج{= 6)}mz9*=UҸWpgɜh-Q,Jd Lg9/Ճyc?ݽwܰ #\~ a=ҹ+6ev`-BSk~\[ osN Aâ['t`UO`c8ց =]CS:8xWV|jw5?6;^|TӿMX¡tW<&<3}DDy:[`7F/5o,W!e~ 9%}(_ G f!)SKH{.}x>ƵT] g0^w>^$kpac*s0/(&SܣO1Ykԍ5GhpM͏ }dJZ􅡯~"?GF Ȟyx"*T?zns9e 4`Y$5뷲 ?;d\;ěFA1NaVyJ8i⣵K~^|z3M$-b*8G!.o],φ$ R/3`& 4`Gƈ/^c SzU+Jb2p2=tmK~8jٙﯾy~YLB3*\N2y|qU;|*g1m\G^ONɂYˌ=Hbݲ'KojP};\F;}͠t*2G],RF+^hhss瓅H^qt.},y n3h27)p}831 ~'WѩW\ 4ök>|Cӯ>Ei36M/~0Vq/(p`#ܥVi5|A<]=5oENTyc{*y0;9L>]ɛ,>ZGَIrHG4>aQs$C%XJoPnlO pei#a!J5vqA/}kk>}@O~xO[80G$x3NtЗ@B|_@+{nE[-7o^_,}ȎK}#w q׮1-^Cu Sm{-gpVέ;|dlz}zO %e 2ߦZzƽ:9N(|ʺ֩/6q0!H2AwT\Q l̴h}˿wԃBBI?;LWX/unJ\DQtL|@̯_K4\Es/&]M3ҎYӏ酏bTO=A_E/E +L{F1mN E2~kni#!T+D3uDF_:r<.< ?KoGoxҩsuI?]ma?衍\;;p=utpƕ'"2i-ӗv\ Ok,pL* S>q, {tڕz,XSӃS6N矶izi4]# 9C/̟9]vC x5WT$Flş*C?bASEq}C_GqN3La|_KyĴ,"-z3ꅇovMSw_=?~࿣#:ՍGtYEYhpf_o|r]Rֽd^"cк+ `~-nO`\'0ʗ1 3 z?rs0d i* ;^U蛦sOᾭdü> {{=y&rXs9m"= l]G ;^H[!I4Lt|uSYKzfja{_j,gC."nnmO١Jklo\ ~[~uv{^v-z[m/\f䜼Kiz`c SENӏsԮyȷ""Jcm55A5"YVo%ߋ?gT)r(octEC -W969W?-vyyT䧳#]ޮ[E(3M0$˛jcWo>JSi}ߓ+W!xDh|M =,T+*E+x~%C'g?5}L7l:Q91 x 0O"ni|b%݂ eO+v~uup(bj>Kӎ)Y> xps/P{@cO2pXvyHqњmKg0` F(rM)|ItQH|n={ABo F۫tvufVx (I/i73;wPн4]jrlֵdc=P!ިԬ'Qx>W\Ɖ僫C\rmIvP߬[nG2]}f,h@g_sK<$j%?Y 48ԧ rA1[ ] }Rv !jLxBhh{~ jl֧;*y^PS$˞{m;C -VwAu79}4Vu'nM]k_p{r±5 &$)SI֎t Dp#mv%9܉Es MOxf^ vۺ=} 9{yhZŎ̅˗mmr>79z!f^HI Wc7WrxۀEjC_8~iG|VMFgK&s&|O7\I/-hȺכoG*zÏo 4!lY@2i3` Wd1`y騅~NBϕ5fv@GQ {W=DP.{[&mmdCcuQJ7Mcᥠtg LCI&Pi ~s[kȃ]۳~5|Nİ { ާG=?CXjnUHguA#. |CU#`]>_D[?׻ޚ>ys1EZ ɄΜ* Srk%ct-m]\E7ܬgpYwj|OG< 苎#s=8|C{zh\,J+BF/qЃبSr!b #| | QsJ+?F "i$cUH}`:.xo\$GjGdiykIC/BP (Vd2VÚӵ;PA ) ~.RP ‡Zn?4(\P0 II*$G!$YJ+3>G>Ž_$G[TU>&Ӟu1)G,paFKj8_ ?DOk5%a׈GVlz{ ֔'(ۭG|n-ЙJVjM&qft]y9m >]\p|[+~6Ħ7dmܲƙ [m$81uo{`~H{-.QAW"+1 ,6[ќbW"G_棧ެj1f?ٚYQ<)7]Yb^:)Ʋx:>/\`$O+T!7d|QJL[}^8*3:E_~ZDG8@||Mӧ<:dCrqo@׾xx>\jVoa۫@v$<)XVaW@24Bdb>t+O~]^3@rI}I 4B )EijpQMC!x"fxC1ah^M=hG.hE^kBzfC~1Oć|̹%21tj^" _a_k~q)ϸt>^g w 7` ٮ퀛n-y{~?=m]azfam Iȕt'Kykl6lV^X ptE"\ӗ jJŽS)||8@Bp޵B`U)kwx7yƾebM_`+lF/Vbv1 ƣ6Ԙi1n/5Dsrl `\l$vŏju [2yh ,s۳>ҙۼc~ @aYA/C7hq+Lf=彐 L] ZYB,H"HH"`8;sF f+V7=8ANo$ktcsW[ mmG{,ҏ["f#:5.{2L8Un}!mmG~acb\-^>P/{rZc|qvW!]ovgD,s&*\hӦ%VL^ (ujĦ3XvƎ0a6 zi`Q80,&z\Km'sami,:W׫&Kl>?)E`V`(GŢٰQs4rPO])^7̗EcGU?ch8+ *a9(y8EIoP`> a^O9nčx X;6k,:$f3G"Ցp?1z\ǹ^<"d ޡ7  t'Xh*ۜz*8 W0s aќ5C9I>z0|eJ1Xs#V=֦E46$^1U<ZYZ+i0Vܞ>p3 ؘV$&H?Eb `|r a5}Ebu}"7[,4̹O#)4P#!Y?ǃ5dW} hN`l/C/9{H|c~7YO<]lHa Ǟq$%t5is\N5`aḠ5'\h։_4 \(f5?jPVgС9v3!:͞x'M.v~6]s>Xxov! :w`&bڬ^\c k6n O!4&Tˎ\*RCV_32^}4C +vp"LQ,# )lư0,.gdԻ6Z/&h:.(Y9H# A>E1 #h5I}j?\|U[UyZFmT,/%u 0>;wA=P=jXn}yNi"|Md㺥IЁG!|a)u`h <%H R03}d \J xȥgmL0=RW:͇9N+Ձilc™k'Bfx%mQee ;nn֏a8pIFV IO-Ɔ9fa^qʉuZڿzN±UKs'YJ _=tlg៝ d"U!Rڃ o8mQ.|[r7MFom3L-tK|$D4Ok/h眲Ak|*Q2ݟ =.eyeu{6K[Շܡ_* =]5~#\2SrKr]mhN;jg.~3 Lr%T;'%x Y}aM`51)?A F.BTg/x)RG0`4_~fq6!JPJJ[r0ˌr}|]Q؋E[>W1zL u}YW\|Mv~f>l2AI.pO9UޡXd񴿌p-L8bj_İFv<=b[! EVN[uځ\|u OYR~C65X67\(l6{_TB|W8Z(XZЊ<#biŘ>ЅV| .o)bu‹9 ||%ڭk}-G E>6 /# $p/XCZAc$6eýC_(RQm}+R4iu'R?U#{5QG]z>o6W*i]c~=rdG(d\,!WbDKj;/^azOwD;Cӻ>}ؿe=zEpa\;PeoS \J⪹H/qTb7i*/1lQ,>rٵru,ⱜBi" (X ȍx =5zٹ9@mpvo~}<@c>G"}<_7wZ۠G$ESX"jM޵:tY‚o.kizNsLIɖXHtb6:~"X7&`?h7"({WrEp.<?obL? MchKBΒI?4U,B&p0 G}B ƼUpX#/ mӐc"noɤg ˥b\Wt `U.<ɹ@("&(a:IeBvi'tO@j=ŗ/M:mz1a;`liʄҔ?L怽A9ւ}ILjM?F5>n>[fDq-^ w0G_cLQ#NjT0Sc_b` }h x#ïE9fm3үyb±TA,Rv.֙@_3Aš0l%y5II#RZ{ riMqUmrRsѕo,n}D̒+>i152{t_HhȴAMLGNp.kCQYyvZi@ڠVsI|? 5m)#?둛-ۇ88ݳ? \ 6tpyr9D YaW]xҵ?ᯰV@#.lE/S#)bfO-{ueWFT]ղN £I0 {=0i6}g2s#oc[1={dY/,CɁ#U2#,$ v4tڽ:& Mь}t>8P_wYTuzXQr5(f_v6[|~K뽗?FW=X?ÞǤ: Y r.oCtY*Q&m|!s퇽h6$~c$^ mi'MGK{փƠLq&I;[d[08^D8]'3 d5AWzoA i,]3FXdžlIQ%D:\\Z 8YԝØ96K ,4N";,֏V)џs=dYƒ16Z$v/,G"CL-aaћ–SJ;+>qVZ1534Q['~ YvZ_ަ,jADN 0$$5ݺ[O G_­ (S_ җLe@yڀ^I;B}WP CS̋TR3ͦ?!'%>s&Qo/]GXR/34FsM&ph n|YuO᱒3}%ے,c f(!f 4(Yih25$MV&d*IVB4jIM1 ) gɚ{{OOI,}}+qjKiK?3W?$`*/jыl駶w!gjX'U6B8BhUmkk{.a*U.4BMdx̘nH/󫌩nhP%}J}a۽mt-j*[}۴HZv %VM|{{G[^tN &A/J`P#:Z;&QLhx@D"h vDbV3(yeq=ز=c%驻x`W]Xpe.flPop BOlF?W眒ou;`/.H@']rџz:8'g,|6>j~ڢ1<7p&V8j)-|?,od7-rH'v>mU㇜S$OQЙJ+a 2ٿR",pm'u;@ktڽ]4.xd6L:eڴ_|]#sp+m~^VrZ2X0?WQX'G>=EQsOhKIq]W X }z=dhpK}C}059Pԍ5 ݱvʘn,g'etbŽ;~_i'QAЂ?'k$+Bo02$ylk:B?օo43rEs|,:J%oT!l%޽0sE|kLH5 _ L 6M= 7ƃqXuXӎЯɑ(0쉫!Px}[Ӱ)ºXt$`n|!Q:'X&8)pDBPj5NpVط%OkmO)Pq9 +}c_\Z^=3ON_5<~  ׼+q 'mڎG?-n1"=s/-lݬsȘ \7ڛ k*H5z`O[z-Wzۣv {e?dqDSj~SCh> "|/Cc57_K*}折 !1#̧OG!&uoLJMbjS񲩦Au(#ё\Wׅ77 ?CVpjL56XZÐZ 'raa{܄CCh(1ЉfjpŕB$Z􍭺'b1uykKm/cvz|i_HRJ,8&CI/?tT .h2>x y  = 5Х0&9ID^|U_UiG H,^*=*R-_a}b#J u~fW @O:$ZIs껶vb5 M5*;k d%E_W{a vĎDKsFy6G%O.Ue/W0b Xqxۗm3hs0:KMˊ_5J0:AW&*6~$.-pkmp. m'kO|SSSݭx՘`-ˣZn1kW{sI?un;"Չm8XZQkrDvajHʺ#ZA'?!v w\-_)i6:8 _ڜ[u$ NM e))vJԭ[Z*rmMo4$4p5 (?[l5jTl悷L#a1gw|-îpðnl>vn _~x9Yd~ɶwˉi mϻUA`aax`9Ҙy2  ? QM *גv^n:>0]rruO$v)%7cdT$CәrBݶX@vW tU%A_>$X_ SY#\i瞰rsy륟Ե.kRH97+G-0,NO勣.m>Pu9p@{bz(C?50ߨQ;D _ۂKe>q+W.tUAˇDr;iKZjdZ0Сb6#jMbZPJG6SOoxe>lG.ӫ˳#_u7rU[aV)7o gcGq#峍`xo_R $l\h} MoC|XqtdRjWEY4>NgAx{Q,$(n8^7km''0Y9tP<2>,qOHJ-o{dmJb7`TRB u݂/tdEgu̡V6p\/ cv8{c&/};][FY(L{A-G$Q%\}p7^,9|۟S};5It6L+a5T!XX#WUS@V!?ۃyT}Jh#9skD vJKŃh% [_P m=Tr,`!ht|rPQbTOZ98X?W>{'U)9k%O@LԷⅠG;%o/^XNJ} ;1Y)&PIr] ̤A7o ^kx@(<[get%)jt$0eev6S 3߃?'O.-Y>kЅQJ{8sͰ^W4Hs &}B>&LEsn [랺j~/㬔mѕNAq;^"7wkher}?)=Ft6oOo] ?ee$W>ݳ{jG-n9A +;C1qfqXӵlaWGQ>텃h!=z .@懪k 4\({M] 5`,|0"CM9`ò_Ktlc9L hG 2T 677%xO[5u҂R6^e4[&&Lun,r e“5v|>a4 Ľo)Lev8n-]~P#f%4+5&`*zʕ]09 }V zL;xʟߊFW d[M4Jv4䈃S}nVtcy=>-H1/hѓot~Ov*wbg]X=9DCC %JqDOژm]늄 K JDh vhneVxk69=ft"v?L P0 >A4rķ'>6M&x@F23G|ϰIht þ_SwC, %oׯnҩjS0tݺ[ N M7^Xk0r"18ю / i'Pl*9N}Of$ާ~y+*DgW$Mmjp\Aw8rj0Y'G˵yu:&Pܵucyx@OuXuG5w}ʸ/<0}UGX?3{(͞*솝ï*ϡ(؄!qI{{)f]}rkl͒ͯ\*uIDATd _3/OE-gp=8 W ! k +yS ڧcYK5#hUlS( mP]D0+hxdQ|]/\rIm8Y_:b.k5kDLm!l:aBb 48dٷI=`槷4D /J*h ohL$oAFlla+P9qûV7StM|xf8 )z'%'M\E~KxC=+p^$J=2W<6^$b X[-#Be, ZDv5v4F[:@ /޸}O?t9wl=q|w߲FM<'aآAQ{_YnSGwykpP PeHM 8%r0EQpHGx1sE^O: ?Sg"܏+=AI=p x?F=xۂCZh&_Q1B۾Kj4\/>>A Ma,SgQٌT>|$"P}e?#]-<G1c׷iǭ~R뾄^1чx,UهO>Y a~ܪߑI%&!)]1ek:swĒa ɊE&/\PER=Hgѥb 1YV};~V48cr:y~aWDabsȁ($ŀjs ?*c!3 PisV7X_M1%9RotK(d#Rѣ?IF>>C*&F!!n1|瓎8K TM:9oOQ/>s8GXM!K5m5w^{bFE(4tsӱ |,b-11ʲ&DJ(ϔ~z}-vm)bHp$9o\{˧oT8gpcѽ O[s G4xr-qrSS.>{뎼S6Q6tI9NV[+d yÛ~L,T<3_T~򦝺@N<|u|ubM;+R֡<9)Kf].=p38J,aLx7lpg\BhMCnLӛoC|0DGwp U1"xϭ؀O4UpboolnɚGV/]?^l 8?˷:9vw+If`3%Fm{˧pOo={o9f{ަ) O\ O&>=uueGt|zT޸|ؽ^<0>Z=R:èx*Ve c74{BO9zpQᕣԯ/p}:ϳm)Ùt7>?3$Yj9{K!K[w³EAܭC헜<.ll*]yXs*$L˻`)tgQ%l'3>e^\ PةثO/9onݥ{Ot}EI/#sCe-\$`B46+@Q5{' TI~Dc\S4 Jj ג7ׁK:cU`*$ 6Wv횛|*dE`yl4F7u4 @F|]~&'Gf_ۮGzp|,_ NhCvMS6-.yr8ᨼ-pT⏏|tgp & n5Pp1? Uc9*a' t&q@4oaeZ^ M3,sGD\qhAqx'FcSL{v]r &%_a ^wIJZx*w^(=)9N *Z<ّ~ jb$ڳ[Ť O\6}c: C %g!@&ՔԥJW>/nS|lX_˂+ظ#Du:ª  Fţ!~BQs@NFԤT7^;l`|'G$<*JTc*-QxDR 0 w6޵M{JаZA"kpL6WjOP)'ĠEB P],-\~JCI[~TP2?zv-RGY:Z*_C'psI7AR^r iyS6:V=b=ϟǿ} {kzxKN?#kO[}?*Z>d/-kxTǾ[1?E 6Xwĭ8~x`]Q^S>A0ˆ_evI\%8reoЭO^B_ vwO[p-n捍p!"GgS23aOߣ Lo-Ë߳9P%&UYZٳqOKi 0`pn޶0\s* ^Re1?=Am^W x}{j&?g;G+龯oEaͿǦẉƅ[xK|8qa~^ѥ౴ɬh-SS:N!7\6|߲aΎkۧi!x ͚L\=JE ,xՏJ&T\}Ěld]:!q󞁟KL\l'>}z  LiS0sMoG 睲6Qj|KӷA;;N Gu`'=\}uM~)}DQ:umlj9K/ |ߟc&bNPa5 g2 ?. dԹl=,>#6뼀p/pQˇ_^w^(בRsձh>m"qT%Yյw~?P7'P|GO|ڢse1\;RjS:7l=2pwBw0ĊG?np&q [ +?_ 1 NQ t7=7}A>EU7+sy*.FbĄuCMdTGc ㆤeˇ '!}LX#~o|%7iG Z +-=ʺ:P.L. HrE?ZH&ԏ <{qcjG@|D'lK/{wkFO~q2>{>K}?.V\'>nS`3;W i!pzpA@Aw^j6=ꫯĔGffBo6jSy,GׄQK:Z%q"rxzd+V[puǮ¯o j&MBDEN>KzNX==u[:xU sd'\]7#\6P9V!yʩeN)]E|CP,\ض(6Ә.q`N|h,W_=yoK_t.h9pNOOG/̦oh ̿|>o,[gSNL<3SAwUiU>ak[kR9Zo軽GZ=R H_IRx'gr눞#vTgЧ)kЦpЭoJYmU,c88UO2 u}7h.jYg&eo1 G]{0 b7 !z#88<+Fj3t 燷~>ܳu/{p_)5|Dc-7,ؾxiӯXѥ4 I_I;Dꠂo 'HyS<==8|i $w83Y:vGyA mMtyn.vs ƾJ$Uf"Lixo 5^L^IA9^WL10#=B=X0Dغ0\ۭ_NM cW5cz5m3?}c]?p~, ܻM'^su==[.yǟH@$zB<1盅Ahs 2M,^DmH /En6k!Tw[[tk׬do:CmeSi4ݽjŢĈ.mA k:/‹P_xnث6tܤα0ҷ EgBO/n2/\ +No䠄ov?xz{2fO6lݬP?*9$T95`PpK!CO&b#|p[wK[+~δ?Ag^sso?_Jlt'7w?;D$0I8&fL`ɭ3EEfLǹg1M| K.𝂌;TdcE`mZsşkEmacT_|+/-k;=dH*CǛ"ޒ/y[ #`Pj>kubYR]z{nY$T-(9hxࢁ_LOӑ:/:2I]hŝRlLmnO+ ;zEX;|Iq$o~0fSNYOmփ ._u2? 9gW.Jג|mz;)z6.W`8y?P@St/^$~[OLLn49=9 Ozv.q}B[itBvuѧb)|}?yၜ;W?2$#$\v~!&N[xCy6,Єc۳4?Lk}k\C#MJ%@HbB歍jG*NMyۑsz6et-|e`QXTi4ޣ{qbk[=_ _[~j 9%,Ep{IDtn䏎7z C]ZWhyX9E[KnMZpUjQ'=*d='h|ÆK."0G~>An};?[ʄe1' 'dx*H YwU^,Zelx^##uIp^b@?Vwqk:c\7@Dmx!^NF ~5V}nKO'+{ `x,M2ɇ!H6)jtH,a?c]>|{+|_o~^Piuv,p\OT?g.R(g|VR̰b+ޯ[س'8ah@vUf:TrvlIOHE"&GG3A`΀:MA\Gc.,XLM2C{`yȐ1SXuxf՟"=a!9zOgi/o!] =&?)a^O~BVICfvY>=~F!4>kG>$jURF@O&ܱCAj<M$.G[sjts+懗P0O[gLKYeޒ +C;)%'R'@:,5nmQ .LLzq"=N.Ej[O.! _uKn3,75HJi=M8U:a"Q4o{~GPW7c$glғy1)'x#3~"BĕcilSt?HԐ$ 24MS4} NP SـkGg.dÊDMZ#fRo `UtrGիʺ8XyKxΕG&wscZoat&YCxXHR$WPG!\zU젴Gs6sM(3` YÁ' > ?jgJp :}ЧlkR>QԲhA*ΦQ{d;%XI5p t!?HCk6M[|ycpXƵL3G)?h5ᔔb(!kbgR!fY 56 t}) #Rݰj^C>q+ '!FZ+X*Xk밵7z?j~`;?`|RE>,|}MޙjS/ۗܶlhIq]Zp0]I\&?qIE×r4 2kx56<,yշdk+)lZ$a%x2H& oa{o伤8w{G1&֌a0ƭ]j ÇKl 39fӴ4{@- iؑbM2۔jctoUS*_ ȇ^\b ? !cˉx\\+Ŝ>%mgTigP[~ w=IN%4j"lѥ8~Cy`7dښTj_+1gT Vg#q7a J離;^!8BȎyc_|G.e?agQ } |bnJ舟OrW,dIoo3O7kXA\IRRB槼z0:=\YTxzu9ra7~I&a&U3&rl0LHG,B2{bQ 5&3`4Hd2Hqҙ;衡CB?s{a9WʛB_ӥC+ĹKߺo§%qGk~M_ޮJZ@jT=Yf` D_Ӊ-m"DB0N'dDLr_o4֫͝kf;čE)%>OW:QoaK[H_ ӟP‡.Nv#dZeܱ ŗm51!QcP2aN[IUgR??ao9_ W|38"_=*LYk5Լxd;W ަ!q2W"!/i͗0mTJ2t6eaP#VbZL@it*X],V s0;h?d>&G`R+US-V+ WiqdV᝟?ZM_R<1c/_Xu0'҄&==yt΁`6ђ{Xt-8.xko~ .ƽp@уɿesw)àz.TSҗ-/0nm+՗rѡGb8)orِ'U@x싞×%4͏>o9dOl0>+)' koRVHf' d + .G I1b"L.xP^u$Z|C)Vj4\z_8&@&/ȹh w^{-"d+`Ғ?.8MB/"яꤡW.(~.7SI8ǯ"pqF MnwIؾJLPKSNJ16iSâ:' t^ےe'/vmuN-0gU7D! 2h$1јQL;?93hYL/3xrgţ}fYf8"phLf,xȇfuhYl ŧ2a"pΠYf8"phL,xh0ˬ=!"dgd"1G-ڳ"pE`[fY,,hڊ0,}|6k"0 IENDB`ic04ARGB hj˃˅fnÇˁŇjφхnwoifo {gm_FSm yg~v^g;A` {fTNGA+< jiDžQG@9tK& akfF@93/= Y[MF@:3,%. Rm?92,%_^ KEG29N' U>Gk,P+' 71,dR:K %½ úʷ Թ úػҟ ΰ¨ Ư Ɛ ɥ ڦՓ ٹɪט ʬ ހ߀ 퀂 ހic105PNG  IHDR+sRGB@IDATxmmy>ܓˍeeYe%S$icETP*?돈&jZ-R!D[mU"_ uX֕n{{13?9|;ǘk3pqA\ q0?cjϸZ@!`=ʸ̈́3Vegl&T2n.3?c32qs gWYq0?cjϸZ1WCg\-@Ϙګ!3 gLՐW (X7P`~ʸ̈́3Vegl&T2n.3?c3221 gLՐW 3j0X4T *(M4Df n0@4\ = Q  U4Ah:EC ؀r: Al@JE 6,bPEt+FX4T *(M4Df n0@4\ = Q  U4Ah:EC ؀r: Al@JE 6,bPEt+ڏVd~ƮgsӰ}ޭs}`0mzY:O[z'u:^3:Mf~:ng3:Mf~:5lcwk\32̧azGޫuq4ΦW5~댶glcӻ;;;; :sE0Zvu4ΦWO:zu6|;4~٫θ>^gӫ?u:^3w0uq4ΦW5~댶glcӻ_:zu6|;4~UӰθޡs}M2ul*33O0uq4ΦW5~̧u Ģ& VPBc&@eixC3t7PFh L#gKmf? 縑x*jd?+XAU4 Ģ& VPBc&@eixC3t7PFh L#gKmf? 縑x*jd?+XAeeQ++N'\oG-V}bכ:`l:y+;\ok'NJl:s0y+ّ=[kK5%ε׸jGV>0h}|Z\a=YjOw_Z}6Gڼ>Wf|Z}yk}0X)4>k]Si7u@KOa#5L C4Wg1^fjb:J{u:3iNc5qu3LXښAsl&@DLm!t !t8R[3:df"p̴WY8C~4W+ATګXM!`L{u3iofD: c3"b`jk\ÑښA`S;c_c7ڿczo~=0v0vcaZu;;;C2}^c_c7wg_c7ů0vÿ>na!< 8 a NiB\479H,Rbs$7^..U$s:WP/7$=)M( |E s\lndԋšdN QPu48 xsqto H~ ܀z8T _A8 \\j|^4o.N $)qAuP/R?*9+="]om~Z8k;3P:xy?~~|E'sϏq;;;;h![~Nz a?~޵ϧ_ugu{k;fOs[ŵfK~[g??[;;;@p勤ϸs6כ{Rq?kܛϞϽܑs?-ܵee~ƖfKmq-?~R9{3?-YfN-^+?Zux)[9ڪrԉl禬:y+V~XWvXVNJ˕R]oOYuUs´s[MG[u0+L;05NV=̕+Nm7u`<7e[0sMG_gz؁ϸÏ>? hϸ 3:1?6*y{r|hmn1?-^(S=0x;;;;|S^[Om7\aq|'f\o_?=;zzz{Zkys-f~Ƶ<5v۪w>wpwpwpwx;D[zO[j13yg<3Zkysk|n~^:43Oeq>}nng3:Mf~:ng3u[oo}g\|'2ϸ}>  !.4MA) WZ؄t:+MJAlBBĕ֦%!6%_ BKt/AqewBqT:H8*r]DH4Ħ dA\hibS2!6)A tWZ؄t:+MKClJ@ą) 6%_ B.)!"2"⨴!twBqT:,Bqi :MkxQkys-f~Ƶ<3_4=^z@[idkՙOõz VH´CS6i|Z]WXΦWO:zu6|;4~Ub]aa)xފuilv|+L'4~O4lWNh\Niخ0MGVVHzu6;;;pbk7uVuXSyכ:Xm|Z\a=iuyUsƧUZ+L3V>>Gڼ>Wf|Z}yk}0X)4>+L;4>>eӑƧuigOʧmZϑO+L3ڼ>e#+?WXh|Z}}^q=iuyU ӌO럲ϑ6o+?e#mZ+2 8jz]΁SU0#9W^:G[Gn>sDud:DZ|z~uzu؊yXZaslş9s̓c-onusAlś\mukysͳp-6Dy6;؍fHp?0vÿ~nz;zs;;;;VZQp~czwza_c7_~=a}a<;;Yc7р?gqlQӀ7wU-z ֑u[V1|+?GkY??ty"wUG7:U=-z ֑u[V1|+]xNۮ˓d .Vş϶O~u`_6^~mؗͯ׶v?ۺl;;;;|:pp궟?uOO}xv?SynS3^:x~[{pZ䋢?ozXOya|ZDmtP-w=ozXOwO,UMrNiq^},OOy}>mt8og^`^gs3/O0o/Ootμ< ^-.O۳vNze?UܓY|yw͸Łs_/ַxok3͖ʹ<,3~~pwpwpwp܁Qi{Ͻq[7l7&~ָ7={3?#m3~Zk-͖^Z~{rn?g~[<)=5Z Zu?c~khޭ:{>1oV[uЏmE<(zF[UVc~kOiܪRank5(zXEуX5b:[#xJV̕oMb23==j3X%060?c#U׫4O2+gx6P`~ƕO]j3X%060?c#U׫4O2+gx6P`~ƕmS wpwpwpw;w>_gSz؁ϸw6=ks=v->>3n~\hcV>[|=&}uׁ_y_u:p۾ah#Wtβ۪ _dzpb[docO1?d,'cϸx1ZEgJ<}"3nq3'm)gVa~XS8?様q~M1ZEgJ<}"3ng>;;;;;w3u2?2.g\δz{3?m姘q~r[ocO1? 3ƚb~mg|z{3?r:qKy3.g*d~e,]ϸw1?rm[os-?*xk~'U63On0?mS[;UXʻq9SY'3.bbPEt+LC4 oh tMbȹN M&h M$4Df n0@4\|.SBªQ  U4A4 =Df n0@4:(6KTAda:MADaz(L#A4 oh tM;.t+j?ʋbZO6y9C]ױϧ|.x>Qa~uig<0?:4kx`h\Xgu0?cu[3Xgu0?cu[k>>g0?:43Oeq|'2ϸ}>ug\>f~ l6v glcӻ;;;;|1bg\Ϙk13y0[u+?3yފ5~[4j?o~z#Ӱ]a:s}:mĢatBt1?z)3v#g\0e`~zUz_^gW]'gm%5p Z5isGW_b_:;7tzm=o{Vs6 ;fؖ}yG t8Uܓ̹~9Bd:5\%A,ΒY+q~B|:| ::| :u6rSC&^74~F7274~F72[Eo~0[u+?3yފ5~[4Vşab_?z>|V|subӰ5~3ܯs筘y}~7uV4lWNh\Niخ0MGV;4~UӰθޡs}ͫ;;;;p7;Vlu7uS94xʮ#כz~L:y+V~XWvX)i\aiSvi|Z]Wvh|Z}ʦ#O OOtiu]aڡi)4>k]SiϛG[Vg> k|Z]ƧՙOViuӰƧ5~k|:3iX֙_V~V| 2éEJs-z8SzZGsl>F}Ľt Z|^|OmӃo8/DzuOzzZGsl>F}Ľt Z|^|O.zL9?/9~~\o@S+Vo7u+XgHcڽzSGOn󧝿קWՏw;;; >1vt~=?z; ׃_c0>18kl`F3l2A>;~فWf1;PY`ƕqg<h`F3s56JF2= 3pcX.r:7р㬱QuhDNJ89f* \I?@euWƙlQuhq(:4ϸol|:ez:spc76: _%$vn+~=0v0vÿ>؁S_cm?vwpwpwpw`|GcK{Z~{~nq|-,q,jKŵfKZ[UIs'Œ-qXjΒ^b 'Z,*tP/*K,=ϛc9^iqRtPp7K8,=9-\%b WIz WIXey9OkXa o*KJz4KsE^ۧdVmzsOj2g{ٻ];36~}[Ֆ^?-gϼ7[*[,geV=:rƛ/י;4^̪g5\]g^ie6=:rGO/YW o̦gs|Z^g^xse6=+ ܷ̦gܷ̦geV=:rƛ/י;4^̪g5\]g^ie6=:rGO/YW odS-:~2n6mc>?c}ϸw7؁={/3nk?o؁?mNø1mu;;;с~㮝[0?]׿;[u_qh';Ǹ|d\wϽϗ=[ig<0?:v}z1>:?u:_nׁYGLOzMOMDF$x70?QkCgDdVP:{3? 0? Egru7(^Gso}g_g\jS]|'ien*1?M+CxetSo"]b~+MJwMoe~M 369d~MDF$x70?Q.wMoe~ƭDgzLAlm A{a!^ "W^:"{sJ:?"{E ^ "W^:{.SBϿ5bk~"CKAD~Ľt D;.tOmՃ8?DAKAD~Ľt\z҅"Y1/q1?c׳9iƮwu?ulƦw3?cmlz73lƦw3?cmlz73l̯azG{u0?cu[3Xgwpwpwpwpk13yg ?קv|+L'4~O4lWNh\NY]q:zS?jYt\cZ#ĩ\[wpwpwpwEuzVzXuyxθs:V3V~_g׫gź´S6 +?OtV+L;?e[0sMGכ:X|6_[wkg~ԁZl8vwpwpwp܁с;>?}c_c7~cz~=0va|cnz;pa;;;;lshlc)rrt[\ϸyoT-ϸ^t%.^mtq?l[\+q˽*|.Ģr,=9-\%b WIz h* _ao.ZJzs:/*a.tp7K8,=9[8z78r:o\o.nt¹6^-qXjΒ^b 'Z,*tP/7-\%9K8,=E WIoN%\%=̛%\^b 'y Z/ǖZN-k3ŵ-[8f5KMAyS/}߶ŁS_/S?uow|oWt~;|w;;;Qmnٻ잭Mkkvl~~6nSsV?ַe{6~csMܩZyW^5ϻ:3e0oe]˟zָΚZuПg,4Ju/ϸ^aRΚs_q'j߅m|{m?ϸ\oIMqo>{n?g~sGf\8p׮ϗ[-5qwpwpwp܁QiϽq[7d?cb~U;#|ǎa?f?s-f~Ƶ<5vϸٝoU{I30? gf~ 3cO3?D]ױϧ|.x>Qa~u41lƦw3?cmlz73lƦw3?cmlz73:Xq|'2ϸ}>ug\>f~ 3cO3?D]ױӬXgu0?cu[3Xgwpwpwpwpk13yg U:zixMj\mƦw3ulz43wh\_gӫ̧aqC:^e> ?Uglg\`~ U:zixMj\;Ol^uwpwpwp܁vGA[]U5~3ܯkuӰƧ5~k|Z4iuVg> k|Z]Ƨՙϊ5~[4Vkuכ:էl:0MGV;4~UӰθޡkuyUsƧUO3V_gW5>>g\h|Z}]j\;OlzU´CS6i|Z]Wvh|Z}ʦ#O Ģ& VPBc&@U4~EMwM[a(95(5=o)qAuPR@qA: (W)qu:EA,hbU(t8 ohb TE#X4\tpP]6n$Y*tW؀r:W*\AH-ky?x>,B_s:z^mtq?l[\+q˽*t?K\*q?˽nq-?~Rq?V>{U\EbɉYzr:[JzM.KxsUқA\pt8_ea~^g\aa~^?K,=UK8,=U9\e o*o.pXzr:[JzM.KxsUқA\pt8_ea~^g\aa~^?K,=UK8,=U9\e o{ѾaJL]obGst?-zᵝZopx޹|+++xxtq<> p<<'77Nj`ōtp-AW! gW:_ʗNjgWv|&'W7O4}}}x"|o 7W;ˏoG|?՞Zg<}k{۟?wpw`ݧ9ǴQ?>wxOM?v~p:~G?@sɸhl*nr?*jk~޵-*g]XEƊ󍣥:;^.zX(@IDAT?櫘Wỏ/e~83;SY?|}޽{.>xxwț?$ d>߾Ǥ7Ko1O)>WiLK0m1a>ݜ=~X xܵ~< I<,𸆞ã㉳c Ø`hPAc:+rSd(wG,޸xC?_׿"h8-z{3?V[rl˻|MϽqΗV;{pw7+*㣗_^|TbOF?,G66#PMPtJq[pcCftc|^De pH׿Oc 7 w ļq.c/ح4 ?v%aEc=]h3nm33n|ǎa?f?s-f~&?~zGMev!y5Bv'b=kb{;ﶜX^÷객ƚ{͆pA#wb"uTzIQF;$KkԑhC2>m'̌iw 1< ?Np\h_wq^ y;B ϭ>3.mlteό1'㍴1gX`~iΦ-BSa};B6X:Ww.SB/m:ZN Vx+·}W7?>|'ώ@?$olUkr~pwҴ[pr ϸ18|O熛 O4GZ/ #E:cQo?Vn~tu (Fd((A9P|*R|/?8~P#r}>~>cOvaSGIEb%k@"7 KT+ϫ? Ib7v@xl֡ɩ bBnrb#HSu; 4 'Cha ܼ!wxpΓg7o}_-Noq0oia=Mńzv&x͛-5gsmn1÷6;ϝ7n.y{qyWy}H6vl m mߴ&wwa1=a>#B=p ǩ-aҝTz~nhč_(:Q*^Bn0`-~mI[Nwb$+ t\wu#Nc@h@w##.ݯ#6a<1 xMH?&"w *<;_}kOʟ|&jO~sw8;CGx1ݨ?f?+L1?z)3v#g\0e`~z~2n~[7/gǏ/kC67WWd`7i)T`ڶd/'5c|x9q-}DR-+#QAaգ$wp\OOݸB zfcM,vU(H~ u|lCf'O~7KݼT)ՃA:$.%9fC/]K΀G$|rxi/ҫ_~'0:ᮟ xa*HUW/ 3?ㅑ3"/v -'cUQ+3."141?cUQ+3."141w;ԁ_ܻo{(饫|Otq~K/ Ui;F vg=n3*y(㴡rQhijkӸy5ZK_iX*͍ 8|]_?:g..o?=ȵ#>;nh*ao1?<3ʛc~ƹVycJ<}"3ngV3F_[逇Ud~ƭtr<873❋~Û|#ׇ^~v++SA)69^k N>`#9t_ax7ObbW!4 1̏=›#?B5U9IkBOL,\||pxvbCOUl|=%),wL!).ո$-#e"/nX梗23qm2w*;96կ?y?@=7m_GK6tA=\x~FC_M~XVh,{ڼ6@붬$[W9Ɉ]RmV†h(|]ܘ?q(%Ґ[;` 3x b \q M[`#=nvC WLwq(|t#dZ2>!9lKJHzߐ;yy[O_ӟ[;e6QK0F_[逇Ud~ƭt֨s}OØk]{?|n~P6WdXo6\xſߔl'mF9!Jk=~yN0c1\TV>7Pp̆Mu "avɿ?4BD8dXgG|aL,%T)nqD,xRiy{eďf dCIJJڋڀoEn_9{'W?Jאc~ gh?׋3̧ mnq\esR뛅A·X::[yJñt~kmuny~bu=8&m,Aw@ ksɇ.?\?w<|DJ ؿ,a"d2t1k atmr04"z8?hsWn=g16a"6m!5;}PyDz|>y z-}|s=Dz KQIW>N{q~i 9d?37_|;P{\_ r5:zH>Xu4-X 4QV3m3?㭼9p4s7|5~O9ҼR\i8dz5߼}'}sf[6+f+جW|O^ĸoVq0א-f-vzaC Vڥ7ŲDxc:y>4v'푧;\|:mLzT_ؼװ;n6,_O<cwR=Ib ]kGЭkdZDGI_OHR Fe.:c%);SW%1wS]5iLZ#GQɚޒ*;W7_zx׾W'g;`Lh9־q½|^4~[zOKu R/ixÒn.e~%gl*e~ ph\_*1K8z4~qԘ/N ;Q~O^{pϽ|WW?&H^m[4G#n>ts9̧nTЎl\܌Vܗ'&zfiŚ5;9VE{ HaLrXjT԰ɂQKrM%'n>wyyaʏwp܁42;  mnncw3 ƯmjazS+ّNfϟ7(ovW\|a#nTRTBWSJJqva][#ԎEq6.OAzp̭oe8^Q=ɢz?`T#Y)c86]m|~*7ZPD 4 thp!x x7a3>E:bX8 n)yk[yJϱt~k]nK?Gm~p1_gE^ـ\<'o>ux4_45=u F~5:ӄ ߴ&=aH$d1 :y]>2,-ȱA6v[~I\(}CO cw2qfON]1((zc :}NB^c !+}bo O #[Zs[1TIct>y5Y ]5!1b!]~r~" 1tbb-?tryo.oo.8|-?σ7ty"<\Z5Š䕸aoE(pPp$̛HZq0azX/h~Ia|`G9d"Meua.o$FJ1q[q&A%C#F[y_DR riCW87䦑w<ܓϨwy]IJ*>xʅMpɚ%oj9b28uǍ>4>PoYhXU6t瀔4x19q}aPώ{7γ7w31yOo.jzNy+\V>[9\9\8Ǜ[ڼ\i9ykh5pwg oY,|SM#=wa?j4Z(ZjdQOjx}:>3]:i]:bxL6ga[r|}fL6jDh/<͂{Ǵ Sޱ CWvYY984P#GڜJ/i:.7%nӊJn o pc Ǜ̓p!pAdyguEr\%f]tg8돧C G)Sө2Ŵ~Yvyu+?Grpw8K|K~=C?<77ˋ뫫W}L~ߟxp<n߹5Q6~{o{s/j)p^?O|xQ?t/z[XsqiEZ8pO(Z|6*ssw6Ȇ>wlS E؈6H nAh^#MSMHh]U=}>RKΔ" YOa93tr)J~i);qya؟ShAk$7đMz;0E&;Mg<0R@vB:dez| J!@boZGKq>|< bIi AH ݙpx@P}/Qntwϸnn7luȦ~ CvxLS.stÙ7?bo8w^|$74Fӫ/Jo\{ٟz7Dh+XruW*\A: ~CEa\A::CEs\eա"9VPp&s@gAı;lz~=o.>O/^~˛{?. 7 ]isvc]=lR9s=lMe|>I=<II{Jy7?uc}VwW}v8<y|ɧ+ lal p~iAy6<>܉"Z;  aN!KkNphlf*p>20h]{5WeުMx,-@ ]R~}wO5İc癡GK=q-C.m#6)BF?b>^2NÍN$ MBG\w!HbJqTLx#/ˏ@]=aуW`Gƅߗq!cV|7?w'#8ŚDZ019V3'2o _!ݓo'}wp^\3;_lusu\o>%/yKcǡ&3+\Ksu{PhRv#Wggë6Kj̧WPjF/EfSPLaMc8Yǥn Dl ^tͧEũه7?G?^w||N_25A+wE k+/vp݌S&ɻ{?W}5|,7 QNyjj" OX-@9X5d~D2?j"`~ fB3ޤj2*mʮE"wіIsuwן~Dpl#8h $^ @lM l@`jj-e,B.l&)#⇷ǷoQCc8eA)߳u==Vb;?tZbU=ҙ|CvۡsHܱȚT;ff9vB`9TצK < N:ӺsJᰎad@ȤzsGZg=cJuJ|-g2 hus_wxInw <xYb)~`z#F$g;~KReZEs2zP/t׃A50ބ7|^+zv#x?}".تFȦ⦻]; ac +"J#Rmx[?9FR=޺4Rә;O=~=8| }o収Iq&y F8; 돇 s?¶D MLt|~y0l%?^?l86?uL p^j8q0" 1N+v {c:tq]!%ޟȸGڙ1;1#m+LZ"}x)ѷ%Q%0(֕3N I䖱(b`bņ9.,ue؜e=qB:Ďd)+AVN%1><'sg MMQ(ϛ45:!dҟo@"j_?^!5e^ O7$&#̧s}dJd(AxJdx(AxJdꡯB*4n _OIB+h^[Omz~cGiWn?ҽFHgrl&ݿ'w{4L/jFZL6ߊ?_͆IˆBz6( 6& Lq-!DGrasQF0&'SJtN w~r i`ScZZ(m R7n0.2:;HqmPf9&9.T9y2ZXH҈| G8:Xs' iK+k1L{_0nGЈchm<:.+91'@&q7e◟\]}%~xuA8p?Gf48<{3?RU;gt;Cׇ~xo?'NQ+7g9?Gq\4j3V÷o?$7Q%M"6(G{^_O"d(7p#v4;M̈́ 7W͢>Zuzȳj?E58IW6I)5Bz (Up"mUY+Q#hD+Liٮ7#`;5io3b01E0.x>+mXSO9%uwT]qOR@5Qqh/#],u9/sy//o4cwp܁S;zgoeT8{3?63?zzH~asyp3?&7ǃ?,n~p̏]xw.菿yMZ/?lWßK R!>/ bI 7`ۈ9CǢ` i_xwhO8uC[(1Ȏaiz|op<z!GD;ʡ`ohgAi.mqY,mUÆ?¦G! Iۗp1 q *y`a1V"[Ii~ <sw\E%t}kyz޻ܖea[nWEnB(,B!҆ c#dYeHزC?PEZLh`YPb!YBnv#?QU]1kͽ>{Wk9sZʣƹO"Rz.0϶ N!Z] f<\$'=%q$1S?] n C|um}5rz=g:gߔp!2 C00~eh 'gMOL灵 ul8r{w..xR}o݆O{l3cywτjUK^7>K]O[7{=P oŏ*ʩsX❶c=G.\cKȇ?y&6_s}u`?=r_[_tMŶ,4ڡbL{c/H6s#Ů)9R6b"&)3qjf ١ś;3TgqΏ$2|VcDY{S],>#8:Uph/OSKp*n+;=kQa2Vreo`I}ok11(LHʊ&4 QOcGe2{>!6;O<c86n'6ͿfC~`L?6{1^s0l(rNjx^wtgnd,cI#($%ޖ\VÞ?W8E[]FQ|?W<ȥ_RK W{_\g߉EXFNx|g<rf/['[,jj] kM lf R,KY;Π5"(ƯcGLDL5ƭU^)? m$M g, wR!{lunHvb46)0~ bA{Z2@6(p@e|OX~JEm佖OPUg5.6AυDXO8>xj CN>;>ܝǗ|3r\*T`RVߒD ClYm,͕ c=GJ[FQz%?W"4+FQc U୺_ _Suq[ ]6t~s4{=7ʳ=J??9W٬5h.6 ; x*64w"&= '`5}x$C[&tfku) 3_OZTp'BTLmO$61!9J,sQ 43 X{r< z_Kt3q}<zO$i(6;xî[-Þ?cGQ>Gy+M歙Җ ,X*T+n?+`q>V7-^/~gA7Cmcov֚ra΍NyFꝆC]wŷ8]mF.>wɀd ibpa/>//.{k,K ,سtk0$f^4H8$:Î?IXHFQ.Г GQ>IB2rd8I $Ñ_߾k端Y߃JDmFf}[sq P38JZ1%Ϸ߆D4x#s@7qtW nAEY_q $|fsf$7R3Re3>Ϝ5vWd'ppj%[֡7@sVDtLc$\hqIBl~Ś?r^5/*Ξ0:9!64+p*x[)Ds.|y=\@wDtGz&ފ4>"wѺOĎk#{\(aHҡ9&G7M_)WWw}ɤS)7p,67{=Dzx;cF0葿ɼ /mRKOō_?[~5EOy}<"x˺Y `o=`/?>k_Ao܁mcS6 zP3wa}3X+}1͚Q;ƫ8}3G8u=ӌ|?iFQ>}[^_}:U>'1۾ amKE{EuL\$v@[f_t⭴zk?ʽXG|lC󂽽]2jް77ԡh=M#jj?@tq N/`~"MY\:ɦNx(|& JmQlцyIE:qG¡zM92 6G*h`1{(#/ ~ly)zۈ>ku:ga':f9|wgGd:}|:Nh'd+K<'p(@RW||/~O}/]ߋQάO֍|@I4 vGQpb%ƾ?ӌ|?iF&[vrQqΎ3{YU5G]O'.\c=-ϲͻ8_M,ŸJi7%"5nz7w=?&vsLWů`]x}q釃z gmVvhe&2 m{X14p)"v"68=^ 4g5ay:~oGz}da(5Zs [  ݢ*# 24N򔇘 JXZڠ*")T+3Z 5uph4 8i ~mSTu)[5!xuġG."=8m`uy>wt">:X{ۍ=phTy9&k]q{K _»O_?4ꖷ=8Ӳ9ui5l~qZi-krQӹ>'gpDdLK6TT ]FvKUѾmu%f< ^yk n߅>4"^Shn .NkXrAܦ)]b{ޯݷ^Lnߋ>qn%=?8э|C.]3#G]<]}yȷKg_y+]RK ?vn\v"q|G.mS>]].K/-Lf'U2z:s/p=f_y~VB9c㦠餍 ~ U#-1\LXE>o. x#{0=!zF ؊VBϝT {uչ`X-lrlNLڂ,ݖhsHM",M15j41XH 6$Թ}Ty]? > LZbedK-F`VIRPfEMf=?лnJ!_K ,X*P*)Z{F!v?ڗx+0Py;Pθ]3*ogm]2wWj?#OWٟvvvo x\r];׸d}N $\rf+r2F^ =(=6pbYʱ/ڃEn;֏2ͷϙJfA 'DM+ >A4#uIӥzR9,`D4tB9q- i\ .X [7 TE/.1(nDVC$ Z\y5u^zSۧTM ʉc+_Ry`b^/NmAg.gX㍗O+`l-M dQV, #0UDߞ{Q%~-p{.~/~>@~d%rT]o{`9ŷ˾gŷވ8?گ|ei疁Io8OBq~Q1Hpv{qG8p9 8c1-.'19n!q['19\KoÒ;(umUn'c"0,jO᪾΃}ş?|-IMde\;zz?-y')ݠXV&4BrMAlj]TJEؓms48fJ16fP1u:ĐOnLf3 qjQ&.20"vYsrL3`<6Jz Lhlmi(jk^PDs94 ƙ9UK_1拹`<`wL+u%D۹!uǐ8OFp;lcޑ qxB?%+7( q&y|OQs 0W5h]I|ĂCVJX"?'n9T'm`| c_ eV>bcwJi4,u3P~}7*gPoFcen7ű~+wS,)&wPw~?TĻ_s3|?ֳ +)`cے֬\7ЭQ7|mOo19MRzŷ0֘DD *ɜ:j3H1u"\`/>{L9).3Zh.uxxaW;{bq{,:vm| =gS8 fQ>&fj?}q |2i-&5'lpʂ'^ܢi~0R]78ݽ8ԏD'}SMCG&MPy&#~&wPw~?VcGnG8?w~[w}C-K +#ug~ߍbx]WϕwKBݧ]ݴpP07^'V{rjWnʛ$knԾAOV 9R؂>8T4JƖLmogxt?"Fս(Wx\ &CF'6viJr:6bMu/(d 0}#h%0:yދ/>GGGN*]rWwSX A`#_ꧾ4㌍gG:w=:@Ysʪd]@zÔr DR)) ¸3盦>z<<tK ,xU@Pzg Bo4xbvox~ 3sK=/swnGkWDoUJ[-"$ۙ x,Eana#b$mdBˈt|؛ [l'9QrL28p@ LtHc`R_c(r x>aRg'.:7-?CKŤrD@>E+sd(Wx8Wu|?$cL }q f}UWBKJCD >Қrh 1[c(l?&xJ؟V5U-▏)VEh/NEf#famGffr)kF,'D!t3rܿOߺ<?*agD/w6xEvS% QiBN -">cp<7$z O)MUjŵ*/tƺZp .mЖW(K=ǖT2"<Ʊ((-c[\Nbr?  t*oowEZG S^Kzn^a{7Ay/߅ƺ7%ZcyWb׭S]׺X f >\N!CA+BOݾ\ݾZCŨ^7+9^hBxƧ ({ xqݢ-$+@Y?`MoI?E4Fza&i gͿ#=K7fґ&)&Yah5.WAr2"S. Cz=C#m9kV_"|r':O 2-"FJ~^j{Gr~F~~UQd%`ʿ<߻}PWH|Y?-z!x8!./X*TM\xl5x%=}wZhrXRc@ZWz* 6;V4ӯ^gѭQ !oDe.Q-t9vd:L[LqCKD!fSd)̀̉5#afjبh =Gl6!c\-ofErUi%gbҥ0ʚf>04IL)5!뗱/6ilaePkfrE(t+FCIDs%սE5e3Scn`ʋ;|qLm2dI<3 sMqoqYuMmz"|v|O3 ܨD&&vχOc7[ν_wZ`fR7Qx?|m|a|{f2]WaE=wq~kg[/p^^%PJRɺ$G/$6~/-|?_?>܌m u5ȅIbD1¶ꜸxC[9 \)nl(p&>&(ZFYoO Yt6̓x'%nh&6iz P gϦ+aR:* ^S7Y0̯2\ <H=' m)}  yܨk@S%tJ_b20 kR[/x|k`>a友)̭??}ǯ\G}|x':qIꙷYv6NG(7ű꭮ouqS@żW0nc(wSxA=jMMq?zռY}/zwaqeW/j{G^/k -Ny~%O,X^}67x/9ٌq4hD#LdҫrJ]ջX瀻}l š=d #3th)5~őGK#QUKMkäV!Cqo{*ZQ5bq OApl&2D>kd}hekeռU2~h_PGɏY9O՜h:l]?C=[gy4/z#|2)Wxs̎hZ2d[A9y—y w?v __O B{g熗m|rl4qZn&Wx'Yݾ8=L6~(y7G8ocQ*n")[\d2J[6ű~L_tӦ8oq=dMQ3ΛX?[{ޣhG%yxW(.K}(Х{_}صvn|+_*/2P+`D-R~L-GTgn_3~w~ִى5-`+=7o d|8hV[t}qHcA&Pm3O-6YX4 Q‚(sDԵ(2AP[ni¿CAy¹Y FI,YA(7SiW_r\ks RD_zlUpFh䂜5)2 ?"܇4=FDXUsYΏܼfsxT!o[6LY"J(0SyC6caY7 "= OIwsԷD(IanH8)O9 u.ͩ xg?>xv8z?r\X{ڒfsiK ,xTqW`xeocbکh}qyX 1$9V^Pj%!)ڏo?^ɘJ8Ut7vpOֹO[7ɳnFcwr;@@!\*<ֹ7د0\cs`Z7{vꫭɉQi9m)/m,u>Tz1h?jC[ׇLM<@.|crs d?'?pdklzև~]nsTE1\WmT?WɎ3 {+=v*\a׋SŐ10x}&B2seLX0&3qC%ǹXC"q̚ϮIBpq<~ٛҖ ,X*Ʈtfph*oqڽo~vXi, V7^Zr8-α`R~tϟ˰q;L=[88BZPEfJ@V9g ɒ9Gbiע)W[nik̏XPx8f4icr00yǡ-ԩ`d1σ9U=J|GdxGHd!z^!} t#ޜ]*mQgE*:őTS-#,8+9݈1ʁl^/fI؈4I|Ī_D`Vzrq G1O:;O!buJBIqhՆLJNٕLWS(' ʈ3ίb?xeOnvl~69k4;}j1S@]Akus:n?u O瞬-OVJ-|\p{Zuvxtr~xQowX]]aѻLYF]~QGf,#&y+?~q{wcT[𕻜o}mI鵒bsض-H\} [TzCYϞ+9Nҷm5[4 c1]mg?8sk|}_Y*,=F'bT$RSX2mO##Isif  @b&b}V\7ʽ| F"zxb>S]i< |KA%!1yb, |{s ^0kl)ԡt"Fz `C`84MS+0GLku> /fCG o< pOi]I4r 0|e?_qO.t&x Eo$a31zwF0lcy p{/'?|7?0ȵa%Io$:0nĿIhO&M=i76no$Ӱw~OڝM~'ўw'F.ѾhmS؜)XS1c=Gy#(ru굛|пك;. NNKa=uƾЛWl~Ɵog 16 }Poĵ0MsȜ~EmxmAN/@<&+EQU]?CXJesJ-wsqomRo3Рa0U/ё`J+ €kX\T kŨ"DhC\)i˫GJMĭ4\oo'm<22r-Kt~\ɣCMnΝQf;~ ;xT'YE+!>J6n7l[]-X*TQzvogTv!Î?<|K";ܼq7[Xq#Z]PƑgկ &-,kh~rF]P<'W.Sɱڮ"J=r (g Fv4T~n]-ie(oqi+nqGyILY<ޛ~ۅϦ%g6uo;V)ԏcMq??ʍk_{N{ԛ4xf4^'V~?~+m6ގcI svqx8(̓s,kl,P {6cJ'?dbP }U%/XWv`#lx9\54[Rx0ArM1cdDNw$;Ɠ1 FN^M"pvl׾:z$9S" m5R|/܊˩ ș0WjbL|>}EO[ ؇޾¥S@=Dm 3 j>a ;Bּ+U uyuٸȂ2NyDns}'٥>fG}{u4#(Ǿ=i5ox?wcL[ 6i!{-PH`ԞrYyjCNMnظN>Y Q:,=_5]GG2*x90!R6\Q g C(J(&58¡?siec㧑ՊLR#^`gۤl\Uun~ڦ`m1k;I4{nƒN`Q7ǜ%e6C 8Ke{ׇ̅'#*jW\ܸ~LoAӼ|Ӟ笞CK?qE8o=WFskA:@`X ՜M+e)3e7els5p~J*}!\ϥ:qi ax'~:<8ث%"m{ŗW_{Kg\m;#}awӞ"ylk8@1U7i)tD~8c(ڣ,\c=G4Q:?y?ʧYFQӌFQ>M,Wq{~Ͼ!nj{f)p!5Wz=tj%?^w^XǗ.V =l~rWcK8OY>쵐a\Jc3E'rs*KůCY`Ƭ8C2)Nq~ǧ'[z(/ÓyF8P4e2LJ (HGdr/M zD12/hVy^%P#/s8983l=|^w/\g.VONQDF[Ϗs} {?~ knX/wW9ЌGy-|tK:ZaGr턏@?턏M潫 ;iRKA;ˋcb&rV]ݲψM|_B2>pV[+p6.oIuM96Sx[k)7-Sm3+8|>빐#fH+]lwnJj~[,lwCȦ@9'6]|\(o5WuŮ<of\cnk{"O_/}|c̕OڰQozş_[}Gߥٟ`A.X*T!U`~t|0>P?4G2֙tE~\ |gC)U:k?Fs%c7= |05o:&IRpqq<0)7?>(a\_Q3#(ϸTZ{GQ>i1A66^Oz RK/?{pg 'uWSQay\zfe`vOcu<.>ߝL>x@7=uA-4E\b$.;D ڴ\3 q%҇|y IجĈ C\$mIG]$bPX=^ `ua,g[f4.υ6E `y(!sY: @{ĵTmWO,ݬF0 q>T,ɑp\yFpt]t.&θ0(XJ6*3oYcП2N"2,G:YI!Ҏ!][6Lڔ`d}%TLʶbƱ1'<0LBP} eė|~-D@~lst·=F.51+/.7wr^K ,x*{ةÏϠ vG]nK }X Ͼ?UgUVW^'k[VaZr:#, 9J+ޞos6g,+F2r+=:pb9զ8me2I` w2vD6ns> s~dѓHR!ib3=RM0u=GL.{ =aqgQ+l}~kBn`ÁXv]<iJ9gۓ@9n ZL\Gb4G;[gPc)ɉ˵˥h=5*#7 ;qܟx;qܟx;ܟ<@8~b5jLjc5L!GQޏe?3wnF훙|X7vLYF]~ӟ՗:쫰 __1aX7=^d2ǢzaFhVc\v2GpK$ɀZbc5ȱ}0ppиVҹ2ۓӻ2W%IAZ@&ݚiz~\nnrJ?C-9H8?B0Lm}E\OHWdک5͚p&&D[)WS6_͉cc6}Ŏm:*-%67 kF8|*ǶW{,k~szs`%o6ġ75Wc3l2!8NC fzT?m'_ySdClkP͏!:i8sz?oO[#8ؑCu :q~M>w\ގOv' ZHFQ.Г GQ>IB2rd8I\\4?گ8|z.~ i(V|RKn~7o?ybW/LѼz]I ̼;P)XRo ~{XzgoBsCWn=1as#N>^36p`&'nj }R761uMI+xTӬA5͎[ÿ˻F}Jm|KVǡG*(9{F'GΌ*ژT8?c*AsnKa~yM9_-!ȂW1PEeo\N=&Wк9A^;:S*0N2yEOL^ĵAu)SѦE:`\6R`$J$N5 d?07w6ۨ{2Ts kܦ>sO7RKNYN@džvKU]컭Gn툑omuh͸1풷o>zϾ 7_;_n4.l m.K/.rN}ň~Ľ֧9x}?hhTa9D 3*Iڰ Yx^M k}T1 dccQk1η"LCoW.?<}1#VBI2 Q5it]DZ TMDg){z=penȈ9a?E\dsEQ/j|Ɣ܃<H^sM ۧqF9Vg+PrN/31PbrҮ )$yn]b]=>?# W6uc3SJe,խVPRov}"y@q{ZXȞL?Qs_SdG "5ƥ^޺%湺}./.ֽoy̻n;F{®܍|+s>c% :Tξۺě*mG]Oށm&uUq<9(ncPCPCPCPC3X8q0ڝs-]_}/Hmm51Т-~X"UTэo,^,^-K0Zw ,N&k8{]k驔sV yrH# /OcM8^̛6nJWIe|bΩL{:#opm-x3A(ȱmDu&b9k]8qA 4%4ye)78草S|W㾰ae䎘҇YV9xSx|t@(V:O`s|G%<Լ&ѯ/gd,jjdKOd.l"$e7ꚱ(LĉI%d6 x'c_Q籁 O!o'\\\܇5$}yxǷZO1)Gc%znG#=ctEc"ڬ}:]v(|<9Ǽct^Ô{Pzc8%@I"8^X ~[=gv#<Ohw;OHAZ|h,Zg n~ l4-]Sw3e?@yA*^?]_=xOD#~m?Tܦ;o㞳stmsXy.6u=hiK ,xhO^|ŵ.)j6H\PYu6訞SVOݾXݟ|i*fӵsDsLz<*U]pNbka+Kpk ߐs!5/ XZn>VDo>Rta"MIgRhb¼\kmyudX]p-&?@I4,ΔVZ9 S7U6a~{khu!r]׷]Xڐu\@5:r:?{Mq=5 P.l+y@<TZB fcQ }n@IDAT^~.eT`RTv34ǘڗx۟qZ껽>>χ?ꍋz-_+@)j}.6У̳_1دUO~WhH \Msru-:8Gz1#)vsTE4=3Ao "|&ؑnH|,CԻC QKEW%ܲ2w<5x~"U*5.Q*/&:Br@c5Gl`r)7;9?D|Ic63~23^fJ0\])_Xv.%Shc鑜:6^eMe챟N'q,.<{ξm\#9u{>R? M`p cg(m`Hc[_gq g6ie/u` 5i-^nt{UN˃%ek~L0I䍳՗>yw`w>t ̔~RJ,Iqy<}'Gw}8c lc@qjpjl{,pcK}2I-\Y?~__|;;[6,`"UrERhG+-*WF^ #_ /g@F>qfbNDOiS1O F~ ,DEԣ)mN F>`G/}G6OyEBQdZp3\,˸&=⃠OG;[J]|1}xc _փ!wiDuV3w_$h5Xy:>l~;o]/ã}ꡗRKP~]Xx*VF` ?+ZO=.E/ڻ7A۠-ѓ)Eߖ=frI` l|y:=㴋$&<F=mEƓ ⩦K.$^54^XϬ{hUK9N$⤕[Li[?d3휬Lÿ“uq[9L厃՜EX|_+d;l+$G:Βu"s8;b>4NFtm?NqRT 3Pzٕ~RZ~׿ . V4XpjkxK@X?E` Q!|1Yy5ۡ<{ꇾ=yg쨍Vd^x/WwuLB{m9[k+o2myomS¹~߫`8|m>#(omO{˿/EﱄE0ZᠵYmC28i\ ~?aϯH&fo%nmڐHlcSciY<ড়<Ě9ft69d0ә+f$cϕx8R [w`!^`3ySeɕUR6E4^lRf`3媈Ɵ_`柫X܊#oM;3e20l6Y ̹IjW%㵊ڱ !ܩX|]rH5$^Yfd#46#KGŵo}>1s1{Iv)50_cc ίB~a&ہƴA':Ŗda0- _txf(qj=lJbԛ7܇Tyζ% Mw6z 9" nb\(7@2I^gCh6& jl.4 O# `҃[ckOxeiտx o(2s ڼ`k(f4=6O½x]iwްSbߞ"l 5'GK[*T`x㯾֜˝q1-8l{}S `ǵR쳱1F9M $  L6B~[crhc b- iQı=>u<;h YΛwӄ f6!PۇzRC_>b8TmA6 NVE9X8LՆd.JHmPv`=M'T~"k#2y`t.ߎm>nKMrgzl욍Z* >ɓj(BcS0mǘPG]|2 2G D8*n[U\) E ˹+֮VoM1J'KiqR霃Mګޜަ{q|,_< OыvR@Y01ηj9'U3g^o\ >jT[z% V^qutYOK>'.v?Nk QvؠU#5zѷ`^yV";'q=-P2 ٢fK;@Oj=x XM-[#֑@ R@; ~b}GH8Hi nutwm̧ۂz_@oNVCP ]hוLqP!WO0KX7_A96>dGҽy|Hj<:(0]k9QEX0P.fGNq}D^w.B6JKXOAl<ӆ;7w~I(嚄QkxT1]C%AɺOWҦ,u:NEum@GFXߖ+%^0{wn?s>7zc~ [`XQ>@%ށo`lGmg%wű}h;ECՏjg^3Ss.c~{{?<&W)lq꠷I0:,wb|~~gn_}QQe!kSe;⹗dˆżDb }9z,K< fL v -#W]c #<З9EF Gu<˜("A6Gll98O P O HFG\7fN\FܐyTa5H500(b1f?;F<2G>t[d p(#~)t/|6W 9_Y8T9HJT׏u#czN2p C &zψAȣrдشk 5/|lzۛ>⋈8!5ė]Ƙ(Cc.dszi29vz0]5?}'58axRrO|g̽[o:wQo>1F7w>~z7R<6omK1|x۪sm(xzB_?_r6׵n$6Zl1:d`` o⫄!?]u`s"Z<+:ʥȆGo]'Xۀ9 )tZJh!#LmㆭXB/Qs\œC4<CG 0.z VWR qQ[yxCf-ǝ u=uadKuכ*G̐qlvh ]ucY be@uڻ,@hyQZqBwMhKNq&$0ic|~}/8L-NS`EH" `5r ǰО}Ȣ0 dIpVcTt%`?YQW9;CeX5UQµ5=mx my]0l~%cl9k>z$8I&01CcB3fU ,gPM65 D}1N1\436NE$h\R6\cIaGVWa|Ϥ|n<~3:N8O'^} .',뛙Zzaw#N^ox(ڇy()>ẋ:c]wLcYUy<Gڣ r-`Z5@Ipb1[lF4Ϋ"DrܛÂzeV)8B7|{֨"qӧ])=oz#Bqs#FT;+{*xK!LOW80^N~"#[ѹ5'/͇G3-QOzl8v.@8IeMZ MO'N˄Î l5&;<Ȝrˣe9VQ-|)V8tވ' ƫ9?ƲKcebP'[u>?GoO<箜oD*ɩ+koE`>֟;׿xĿ1'Ǿ\?t<3_=d;ZA,@!>+Xdz;/W*/8쨨Uޑ~Wyaϊx) vMRpMBɛ& \bz5[^MUuYIҢB/mu4fX/=^vHfq8IIjA33|Qbbơ4js c:6ǂKšu8P>nAOkq$k%g-+U ٹDpGGWzmŐ4`X:x5oΨ,tVu^'KxP#>g>(gرQGܐU",e.'ۜ&}Kf^>mdX^p~h)j"3%=k̞6][8'z@:)$[~ңIoO_zԔ`_hq$j"/eb9.#sֹ&TK CsYY]}P 18 t _4~c_ބ?)u31ld]3sc=st-SBL./3C]ޒ,`XX2rW3bX.WgUI~]{MgKoHuqUOU6iޙar/3exV 3;[JW#B "8t'Q>SMKeO /|+~.XYr /&۾'?x?^zc;4# NQ_4xVy?Q:KrGU(W#ӪUnQ#o~1y&_՚D|ÁgUŸ/~$8Oaam:JKK/][n$c&DXŢڥt'# 2 `l`7 [|kP̣H!ag,^Pk?SZ2PpvlNeRkȂÐڣe^d`t2.ꖃ'bll64@Ҹ28Fs;s!q!K^riRAd pDi}0c\ M)n>K؜ٟ:ls/ q %3Uls|縕Xs|+T=ח3kkWQ.YhŚp氫>'ϼ& #:[c\e6c1nnL=|V?.:#qqBy6azLmZ߷qg޷+W^m޷.?vџ:u>a^C#CPb]M4?xujj߷r;g=x;x x# }?U2ٱ6B?IsGO^;0u)O( r ǖ&A6(A=~ıO.E5FUΒY-/*?n .};O/.0GzLzu s_֎isd <"0s1m$)ny$ϸX,:s?.#ߣz/rxXe?ri})h--*H$y=oOרǸyH PI3rYmW1 __ZAxC`1c1ʨ.y\s!^sVg$6Y<}dX=˔" lu^ ༟ aо\vnYZ-Nx,Q/-OhbܷqIN Y-X2A6UIRh8i#!6'~r@1(XKxTU(gJ=ǰ*8sM~ڳt}SD9G_ kG_׏oߥΟJW?mκu/)Je *{1_n03 NJ4g>qa fl H+Rx-xaݮ T 5@#)M9nzF,C!V7nS.B&6Gdw{eCG`*ַk +9!Qs[&_9 „Ѽ,戴شL|Mx"'lO#Be~f:ajQۧG&_ҽ{gQ}N3i*3 HGu2ISqTID8LL\qTo(5*;f!f#PoNތ}=_ i*ߜ"zvӌK.^;5&m6;DB 8DB'_<#AL:Tg NzW&3ln.Lt\PH]sFw0 ,8v&s%>vdI4Um7=l踉C;RhIq.q$V%!Fm}6nk.$X.X)e^NkdLJu88`3Ds-* i0dlP91yDehX࿏/el/Cs `c-t s3 _W9K[) ŵ@FUv}Op>ю+ׁ(]QZc_"Un+-T(s,Zbrb-e6j5dkIl7ƤM$g>6b2'OHƻ5 Fކ[`udv/ws& ȧY__eqھxW^~rl(t_Y +g 6T9yC/l|s 9j_ QQ8+b\|}9\]<2D-;M؄$|˟ .YۧL$O]n,pwOoWd8 ZH^LE}\[1t= =0AK$ Przw6rL^`)hf^>c em^4DE)2rB<eb!369b'^5H c b+D՟ΐ&qkEΖ3Z1{v `ιtOPO~h}6QEij_sQX5(y"3LaElM M+k_pp8.Io.8!51QPmׇ|P\pbܦ%lw{[K@`QvVIPLLa'_)f+ i%ԮvCg0m >79=YX&x2s#ds9T xsWeΈ1ZcyuBK&Y3Q2+e̱" r~uٟ!жu Bc"X`"$cSe &zi%4#nLB & .±N,}h#k&5k@;mzɄ9o\Lj~G67 Á+|8;Q238~ ̹ 9naI'Ɇ" lP-2G[_PѸʐφZ!DbC xYGלҗ,l#Nݗ`>Uvd7t62*,nC:̾GE['⭣oc ؓ]:wWs]튣zW9Q=ծ8ޕw#y~6T4ǫ꿩N#uT%\ЍZ^nMFsYqpۉ(OKĨOy((KtU6^# *6J[|iۡ`$H]l1-');_@_d- GRN`?s4{x| `<K_蓓Ejbv3=?|W{ _ SLGxCn.x2?- Բgcߊ0# Pe#Lbq@q" =@D#얇#P4>d gA rGk~<$F[3sV"" V4(2k<Ɔmb!;Ei Lҩs҆`1@g'5n|덗T ,gFKODo`x97l˕ hߜ}0 67goD[6}0ͭm*75Wao*75Wa-r(8tp9:u~Wݺ EIm^L 45DLF04qn1Tm͖8Ȟ?M(M_9/ ;(}SpԖY (@d(KK,`9\?/cY}M9(>QT{Txa ;yX=XV7##&mA$HvOlE41u ZHއz/م_y{ڨWq0to&o<Fs7 o۶}LXڦ}耈umaFjhFO 3 S[TnG=rE7,EX0"6f^)@vɹζg8&G'WTJ1TƦ[{-Kkt˷FG?Yrm7 c[ B&F66.BOV  &F;vX#(~.C<|a'jkw⭱>_t{c8ßA@t&f ev¤z}Ή/'waGhl-E-b><9IM 3Wl,-V#D~pDAnX' s),%(~Oo޽ ʅYbbj$[粥Gfʺ_KfD97^aRT 3913}}V.گ" 1#J% f,֟Э.xz,+ZY| @85o|7> n>w< `,o36 M@NMj]qo[v+QBtEsc0r`(ۨYvd"œ?v/F0_*Φb; </ix΀( Xxȟ);{HAnʍBk%?k/·q\ހ VNijcO#8m%L_Kn8~o˿OwAԾ4ОSyǛo[o=N_@IDAT]v0Zg>5}ƘRZOaq${CCWH*Q zS5;>tEWFOtg~28$jJ'OZ KZYÐXK4u_񻿳@j£~D7nL<FD̔R5̛1,-ZN@jiT/1q-9[q~Qe\2%4d(b93[\ۗ׳ !UZgRO)$Z ӳ>?0^9{[zSw 7Q/n#l7 Ƒf!_ ai$_1ݿ_s@Ѧm;D@ְ5"\*'ґ6.jX*E PӦqdm VҬ4Ŝi)`P85,Ĺ2Vp&ڸ(tqZuo-.{eEhȉul9ZFxsqbw$}P/ <>~Ú?L ILw"]ùƓVz+5jkn"厧TވD`" r8G>{?t.{)$>(&ö$&1y6X9| & YʀRav%B^hg8,ؙ"ExUC2fS3Nj3*~\8 lt)ނ)D\?zӄLG{@sA3|Mvxژ‘K߲s<Y[;A,NfF 7-}Zo)Qzj 9Mg4-^=7arxD[nme,wu+܊.Og;,,jgqT7R|MWN_J m<~Ņv@dž,͵G av&g~#)WSFX$U^ ? PX7x>"yt>Shy@˗;"즰PX(#BG҄喙i"lߺ \$M(ڞ?Wr^l>>Fs2oy'pxF1_Ox'@!׻zv~^GC9a^%#߅ /xiCAef Pv2IM&|6Ge%zc-.I9FVxbsq? }[[8Ukh[V&wJ`0vTC;졶>(GFZhVMrh\ӘUqDQ9YŶaB@jspcmO'zj|| ??U{ÃnFzt |uמC}şx76NV} H}Uu3]%CPN֫7z\I"ܤ@.3юSN7l...3u-FT5*9X"ծ/mIi/c r{btG<;荓?=_e7Aqg>Qsn:@LPq7bТ6waY>ׇ"C8<PMǵ_L6_^iTsݔL(AԶF,OnӜ}Ԧ~_ǒٳ.[ ٳsM,ZU|ٟ,'Nd7?~ O 3?"v jMqFjDௗ|6]<9bAoX%t5Q#)ۀX-?N9;ot_YE~-0g_f%_x6b}yB>j 'Gp}r/!|V-5=p_VQ,[)]SŻ]eqߕwY]xc_xɟ.O5S>=`Q*f^i)jېBe_ U—V e{؜z~i~=쥏?S6-oO˲xWxջWeq]e.w_SV/eVǃV;)ըY7SLu['&9 7ޅyjB`tb]sij4Ʋ.@ Sx_7M Aa)}2r6NFNq׍OHm'VqfoŲ6Swp䚅4\dHuγJ6Nb,_"KfDˮozI>ձ~$8ĤxZqBTEľ|xds^wF3^cμOS'Gm7WtI)q6Kf6&!-'9΄99{.&אHQpf9&q|_vSP6ќ d6`8C?eU\XMG1-|e]5nV5Si?y.|&QCja|>/x8<0kG;>|d$h0rqGcOb╦5^KbL*ݮMטSVA ө$RI# *M:󘌂D0M-/th\QT rps y7X4bSHjmF[qI^YrX:mv*8r݃a7|x<5Z4W&sZc#>“'>/jJްCUiazWŚ%[lWXX' SYdR۸lf⩱uwڝ;@.^mgQ?cYJX蔒 usOCQyv1Iނ"|a;i,Jb?|}]b>8]>K{eñLW{j+<|⌀'ۄH(p|񯃄ycU MtO̗ۙSj7>'F\pf7 GY ;~1 RqB'kwSkc&3y|.-ѣqY x3\@V[Hgb]x/۰MS읎i~?ջߩ8}t  'ÿ~$`fTd_x\MŜy\ u4Hۅ9U4Z ~ />ߏ-¸Q;?.NCVk pM')U1S"Y~N| `IqpDŽo|W^b1*9o"WLkעo[y.W?[WmC~nOvR7#?f~h;: o5lB'p~&gEf 1g񥂨_DKN3̑,]5)U5@D٘s78NFq;$#Fdh; 2Z=1fά(0c7y1=<\ͥG6KD/z#>-twϷ^}ۺ>cge3f2~w\=廍Nq+yY-\:~"n?i<ncoU-*pi37 =~`Gŕ2jmqE^&R\v@iK K] s[u~n]dnb{YFp5q\%i-be-N1/1<#LWۑj`bEJ'xiůŧ`O~ۘ*Sc _cKo]ѼerV^ƿLm귌߶r]&o_W}Vm2e꿭N%{'b1!~Ggm/ ~S߅}NVs=]7j^)EhҘ)no%O0lAӆd *g A騉 G WGui` JfJrTK5aKl\4&g+Xt<|_9|ud.NgͿLtV=+]{X\1ï?w+#4FwBUǦ&=SL&6;$E vix!a-TJIn5=j {_N-uΡd[V(sE/^'a~7_~#CYg񚹣r/xu_2{>r;_1N:+ךvb8ETukX]±{y!w4 Am-m/Ϻ2lY&Ec=RQI|5|yՕ}_t 6vW}wۦ$_")DTⅫ,[(ԋ ye3U cbS@91 *sz G;KšHyRiĩi<_akORT^ҷ˘PIp|k TAM} K1Tu+w_c^3֫pѺ)/2TmQu[W߶(ݺ-뵪zQ:jSeθχ?/@cNd:/+M6ԓ}x_@›3 H:bZ19Y nN?X?϶cy"Ew178v\`͊(t0DShՖ›̅$z[Ǒ#eH5 `ϾrWV7XM|cxX5J 4ZxZ<(Fǔplk n;K8E7?G1LXid Fd]PfΰL)b%ꐈi)ajbsW/,F u*;Z:ȦtrH5~%x{ j|pdZwU*Uw{(8]:k?:sßCUgv!gax6n9 zL)<&rapƦR( e9,Q"$y262 /Y>D+#"M;c]ܔS8dc͒P}޹BjUY6y]JcO޻+,ۼѿqtqk'Oqfܬ՞Ts=n13fK ʻf'ՀsP*-R)رش1i7uώ_>+u׏άᑍ^_m?Nr2dzn\cpݑ=%~#^h7&u /7O0xG`}|lw8u+X%A*7"[\lϟ* bl.voW]KqV1|?gbFh'&rVb$2,e5 v/]Tr>^A=~ϖb. I^3 =:4UTU]RYo(sKZQ> ocwb\<_KrOɛTlwU*=[y]:gn܏)nUkf+Mb<{Uk2^ΏpXyyߝi־T4ۚ z$n`nlT/3ӣzc\9FeXTy3yt]*,t1p,)ioj۸YkrnGRGJuԡu,|v_znx*.y9w~8pz.^;2؏W&|p` qj; OSIlkݤFN7aZPK&mv\仐؄nyd# gή6_3O}_t'_ X9ȇ8Fǔ(CWy1+v:Uލ}ѻWyc7Mnޕk=ލ!&PZ6_kLq Ǜ~2>q6wɌ;U)(l d,K-Q8O"f&ӗŒeϾ n+EF[|`gD>'?1~sOlrlt?x)k'm96NOQ])DG'kűl%) ϋܩY-yXXX$ ڜ - 8"+' 'Kgqm%9ˆ|9rlrߊ+`\?h#l j!,kN,r 2gwi~TEۋjϪSq+eK8ŻNy㫟k_~8UҟU8*NU<վoYqToůcV+jRU^ẕWy+N+\2U*oE©+2WZ+@oN^Ai;?|rC~ۉJQq6 5u5/ /܄-KɧYiY( is@9xw0ÈANadeM‡o$fӐjImSLl(9u- . K=^Эi^߼, 5dȡ獏=QO5u}2K2paBiħI?oSڷ}*ߜ}~%SΪWCTyʷb$b,\u,kg}K7OǞZ;tvQfWP&p}yVitP~GI5ҎP"eh_CɾZ[B]SۓI59p[(9vFM%@,?P{ 7o{w}`4B*+}Օ#_eUW9y׸sJWr*W&ߕyݡmkuK+mUޚxc埓Ь+ߜfk(ލoz%{Nhډhixto͛ A溱e,59IR4-O\flH糞 ZJPㄙd@CXHe9)\l/ą%B!$&b3 ~äL%Fƞ'Դ(͡9BAMތGx?;~+~+=o?xڻeV囓O9^;m/.=Ŗxk\gѲi|3?+~7rӎ@ޣv ;|'tΗek;GkǃtބbCNM'85)O*Jdb[S'SmB,o'W[騖q _T/$3evOB8 "3xh xy|}Q> ˈ*o@Wy- @PsZ+@sվj-S囓"tij]|ėAxWpoN^ƳnWٶ_|s}8|&_>=oއ|OvĂ,TI'O;$δ^?Nv-O8' Z;9Zۙ@КS{|hcV0GSd|`"&9|Jt@[A [r[3il~A-.s%k#.g]_ޱwǾ}"nR}m-Zk SmstW\W̜}ծ>O;'gͶe3'/W.bnN⭺ʑ:cibW#?kx67Ul6aǝc0ċ;jLlz,/xcI ~ljڅF%,? [G-pECTԢL0 .ӦcXv>6s(>;D^}}onbU]m7'og,uoN^wnڗ񬫯|spsͮ2l;>Sqj1l8cKqTOaSRS}ֳ17}x|Оٔ8oJgfI8nഫY"wiQ$q 5Ôp_ZAɄaWXf~ gۭ4JsM^mlMMOǓiEmeɰbV89qetsCfԹ|Kģ_ ߽7(m[n뿮z]仍6VqTKVֲ+W7?Ͳ QE1W؟ `RmSf MakctA8L9@!4B*H=Bm)|r*ϊZVU):>ʖ5WGخF,|쨬Fyƨ ! o؞'υh]w O.G.>ϧ6O6\9e}xJ^DW8by tSy#C>xVh!TM5B Wl /W/?wNuf g_kC?;J}JG͐OvYm"{dsk#V\ >ԐCo2)Ҫys5 E"Ť[O@[ձlJ!|dэs(#z9])⿐6(s^e-Kb,?UKi]M+~^W^etgWz_#2~'qb .qlF##U{yܩÓ:@ ]9 xjڐi7 ,n9kM&xmA<`-%XvRe9E?֚_`FR i>HBrN=+B.-u2fy)rTBy2Ƌ<_wphb[Ktqʷ 'sK/2SSǪ}n\Y͚+ߦlؔ ݬX7g_f꿶̽P#p;8/;6}`th.+ ?Ez}rvƘ98laLnvf,goKw`C&,VnXbڌ#q4)=0[1)5'KP4ԞcEE{/,~ aFSh=##(G`O]~ß>[ٞ4=pOk,͗ RNF"p|3l?tQ]OSE?}|ta*.:d 3_?k Y5 ˂`f<Q'|?̨#0Tpdo~}kRFFw )<sWZW_gWWyW_\ʕkx{}3NK̴xjhj'y6B 6ڟyd]H"cͻQ,unlF`Z)e~T,$K?Yÿ:;LsZb'_E@3?K8'5ݟt %6bfũ%QnawYl)-lJ1\ƅsß og-N)lV*yJI=˹}+rխ],nW*g̑W7p_{axO<1q>6<;0vj٬2A qLtfvwRHWhP/ @MYm4[~ey jdZŸd6X?yZ颠~O.4Mt_C!_m@`qxq?SaveM#aJXPѱ_q8~o nӏF[@Zߏ燯OKkϧMM+~Wy*F_3:꿫<sW7:>k˱يD2~XۛXH[%DS%5䝉K;k;(fũWn|9>_qN|NbN& j'碨6O,y˹K>^IcӚ#?@ #I08mkYH^%x/&W8x8_<-bgH,qTQ8'U:9+՞c^t+WP }Y2w&3MLkgR@ͫLS3 G!n#qTۼzs;D#y+6:vi,% zvf/$ZeůIf}+WOS[Oe?{xJ=8#ɎI-.[q2N|SSr] -,)8puI0X"URmRddĈmE@,%I}2f?oth>Ya{)d.G|rXӇ%0:,F_8۳-K۔]\S>s2eœy[/ƜjQ`%Gn3sx_Wg sr]ZyrŸ  +LVY=y'?_$Eiw/!@ okraqaԞSl>֟/'3Oi<>S|/:),XꊊmS-y692ĸ/[fN3F MӡaqzXD q,qc5:gG.jz$cf50l: hK* zt$<Njvۈ *مMX>!> 쬺LvX% FZL7$l5n<'di9:-՜+<2H86B|Ɍ=QܸF2ǵʌOCv~7ހ6ض { ]CE>]\V&᳽涊W1srLtۄo/tngA~2p7&.FyAmU%5sXxWa6 珆x|ˬLfSbYYxۦ3x2,[,٧'%oO͇Ϗ_״nuņ/?CXEkZ㑓㉰&`DDSoq\Ruezn=Gُ'\ևr"\/_˖6Nyڴ,VϣM[|ֹ㪗6z<8&[0PxxclkPMxx%K'/ip 7NQxJ>ZjN\k1Sx/be-pqƗ KTE|~" *X^77R4^%fOSȝaJĤ~Yl.չ߄AOY-n7 5dvd#u-5^N:B 4"-@гJPt9(d}x2<Ŏq'g.~s8E\6~/Ax 8%k=݋Ƒ /'\vR-#V+ͭvJrrolY%MfUߡ/[Jy>ϙsn OB7qny}z탫cZՈE/ N8 tLm;s[_};[,hmEO-|zA5Z8#WnLvcvWGߝޟЅ{lаO5}g5Wno182_@M+D [2w}OT6`Pg{Cl 9Ԧt5K=y(|J"bzmspA4G?|qr8`&F:mCxOhKGW߇='Ϣ,_~]O?s> /\ܢlgg*j~H]6{7o9Csu2ظ]pmϵohj=F߾{q7W Ə}GqcgMuˮs7unZSKIy2Bm!$RX"".  S"." *M.H '!dI-5 ԒZ-swo;~ZsO|iTsw, ˎU 1<>2:W3Tbm<=NYylOQ<ʗe?^CؕdwUilT9֘p K9UK̝GA?Οyl'[07;sMN? G[8OH?uӵb"ѥa6奝==9~W5L|6u&ɿxʧŶ,yՋ?·} #lh_#cd%H~ _} 6T Mz,=SG|u,!i_Yff.m}+6ȏk9\cZK}q wJ/^{I9ep g8o7"wxN.ޱ k6ʥnw]yiR;u{k xQ~|} k㢾qJ%79|ўI*PٯߢnZtT5 uνOlw kKn#}>%`$,9j{S!f6ű sjv$;DS.]KN$r Zr__42^SǂpQ 'ݡE,1O6QVw;ekSMuoT?6q_xgnxzP{HGA*AM{c_7_\_VM˾kVqձ %x`^/o["Dx`f|jE/@ys[^&F,L(q[gZ\ArAi袢꜂,ziۆɘ2;B}gv^e3 "=)m*oQ'i/~`:~ܔ| 4йw6 Nw\9]e C4N}+祟hO\gxCIcZ{9 gO c ؜jSvx(faba{@[cgm|a)Zy]2zS7y8~ zb$Rq锻( v^9f_qcck1VԊt<:u`_* 8M圚=~)9M;MQ=/]o{剭l.e=:ιvX C~6lm;]?t3e}U݂jϾ F[X}%N6YQ@4mHŭU/0姾b󨉗IcL{3ͷ\>EXUIvU x/Z%Iۢ=0B]9.$w{>-~sxUvlif/;m/7n'g0S[b>.Xqh.E6_MV3ng cLI"γy82db;̧G]zQ9 n ފ/|U{|xRΩy M+ H1CV.vt6lW-wz(z:ꔏGPzkr2GK"F!3zNI5y T&j!w\oOvax>y9w찬&km@51dcw$ܚ/u)S}f:+xP645SB6#xlL(ku~8vdB0̒dLs֥_66<]onz=/zG-sM]_*/DKzK+Ƒ_!Zr K)Ƒ_!Zr K>ݑ?^0լ_f+۰G I2S(/$Lϋja3#d~*#qExz+",+oapGm`). SsBDJv4mTGwq#]3=fXXݾ۽KuE*Grq/?}݇g <7noq?kWn(S y X=PMģy4v|X7嘜Nty6ǿf0RlPtn*8£au^D1>/cqS+zW-Y H⢷@pҾ/i݉b'7eok X <ƴ2) 8QC/cPks>W+PWgH8#W;ak[W\(?]a Dz(1/< C%l2&9] PUp(e5~fZwE]}W~7Z94U`}Gyu}t~̃yĵjo?~3lW~3) ϲ3eyp3 S<Ϙ.[Vd[w Wgє 1ozFsW\|qr&z^kAf?mW Ӕu,Y~$D3s-8 / 1OKy od&#l%eS,t\٨J\I a* K1PR%H CIs=n7'/}v+p:2 .W^U p>c=- c$.'V\e-8 L;,*OQ^%S;η^O {^"kQY??)@^ ڜ*Cģ0KpBlK YQ5~9~C;o{öBrN?p??g ̖힨|T{qt9eGfiH|aAM;S`x!`y [fWnp)q~z$yCKu>3sLsn?Rgq4]й)ՁT27Pq %a;ΥͯJ6Wس@f ˿e pq|;8LC,Ւ1,gYiث鼕}!E}B߹Rа%u+ `;(de bmbB ey,b o0OD<=f`OCL MWNЗ-k,?s¥_ONݦ\g8v~|{^^q,ErStov8W&}j߱ݿBU-G}zyq=//bCo7~,ozr7s1Yw zt\  #++,ޤ`JO}=km=A:듳 j+u LGq~rV$ô=3hq~ޘpgˍNJ#zN6>/KmyI+$97Gw(dze mmeD Lz\_p`(0.j*,4"vR*M. *sg1\"B'1Y+C:0Q"b݋!ˌq ^p.Zp!HLpN4Z/= Շl3;5Nk݋['/W3'~n's䳗-AϷ1ߞ|{y/x8  V` ݹooP mYdjz,s9wƵٛ<"E\sFfk9 &WeLzE'|# n?gfyi]`$"pzKjBy63|í.c5SmڂZOy-rsFo^ ڹ(ES.sl/mLrl}T ZE̅7c }*XݛDm 6M,8j{ ?k6~m^oַ+z<^zrǷז\:7[y܈mO~ߵvr+pX*w-<^^exݘg_^ًpq›́Ez1*~~Y^g 'z)~}Y滷ro+hYc /ѿ*ݵEhE~e@yu|ҏWZkSh*ˆUMsCX v~{9 hA0z5e`M)zUZ6?4o.bP/c!cSm8g|}/z X|#MbƐ׀ s@$!#x?R< QӼ?;SfGͯg68< ^~d*V`\4ϧh6WxT$ db_1yrlŠc|"C?e,.:+?'?ςnz}_<=$ƧE]4R<.M{O(={۔^r rZ8G<{ylskʥS}?:]z̧'͐&J ǘ&#Ցq5I1"j6$Xş]+HEڻ(G6X n{'ظpq(<#ͲfՏjW<,t&{<,jlZK(G|~eA o^~ c;2jh:nhcT.~1A2B`\iox6s``~[~ܥA|t=/?d%*yE韁y)z|t~ ϔNqOr[%>cj~VolcZ9?apx֡{ۨKH\ &Z:U= ˦xꋯ"?/ƢU]G}1` PSHq&t2.qˢ 1n`j% 䱦"[5 zs< _ȝ3['k:_G27D4*k ۴E@<%>Pjx>,> OlD@3\xv4:\}[=}fУMܨL xFN\K$AAsLk%W,ۈL] :@VsN_/ٻ#_w@%?Y|#O_ylc ]4]7{ygJ8pY_R15{yzQ?+[OkL%Fӣ <{SRTS(Mrj[ro6)1V3iر/l7-?xU3s+TSΡ:U??zGuhƿ_??=ε,)* 'iX!Lԕa 4¸Gv-{غO\=i6o2mgܿ#{>a/SBrӠ_$5"޾x=~r{{":l8 ۛnoacm=Ix=X"5kgw1{Yff5؅N(>C6}[/"vJ0l_KdÔ*r/zw-"P  l^rU~2OB3;PbuIΌ|E~]QfRi#^ͧ~2 y_=c`N_9~[rФk`S2XI.%$am,GExQBϗC/ᣖ}c@E}7T] s}߇A!{R0 ϵͻ( _#Ş_Uf{!AߧWewZ>aNGNد'=çĩ51o3^g0^7gs>EP=BmiabzL RXT@n|v:o@`˽q m=u"\>.lۖ}z5kQ yS,(hyiێQ, dƥA[_}+n`/7} A Ƶ F˿>c A!%}Le|Pfq҈y!asv]Wd&c\T⅓<`LV9؋mm4/{͔9z)۔_Uy{1>jحs|>{˙$7Ht"'-Plz]Pƺ\,^sXPkHG$3e>e`E1>7rVkU2)R貇\PG=hO e 5_^eպf@\fd.A>2i$3![G1'D|áU:>9Y^WɆkMeM9j cGOK_w{x'/IL'rtP)Owx?Lc5,59 I h[]|Q.`*2[ 4ٱ3<^m}Qz9m"9xw7rv v?[ն(*:srNcF)y|}M{3lS1.;X= E9t'Cf9Z0//9TZK.+,9?NyBy+<#XȻH1qkHP?>Փ/O]I@Z#iĔŅ_x5#ƭjFW!幠783M5RH7E@>6(LX< 02بH28 " *[L1ГN;GlNL O}j{w{;bqٓ#yOK8+%\Q{"[Yq/'HpޗxZRd~}Sq{{+N-bd}GASlQx=+U|%;71]mzoBsO,ny8tp#fxMcՒF$2YAic̛6fO9ɼ3YhHA82Bu-7c43y[`'DZx䊸9u92dX|8{~Shc%?1  ]Pͦx,⋋z8agV4GɰmWoAx_# (G[^)/ܷ3 F MuH65U.>\'DeG{ҙ/1+|@ Wr W,Zt ӫ`0P`4@ʍ83P ܢT2|×&_mƦ&t3^I67ׯ|{?UYGG ׻#)M~GC=gUyNTKyLJ8@Txښs'q8qʙv}}tU=|2Fh щ]}B<.۱B ḱ(J|g25gT^u@VW\=XEAB$#AĠ2EO7xX~/ܽ5|3F;?*S4OUFs}$&qsH>B]:ыVL%ΜMxf/:P}X%ŒkY>U3OFb |"~+lpX>B}R<[O56Zgɖ"lTФ+myʉ8 qyeWREc%M(ܐK+>5yb 4k8zؼ~)cXez@s(}򏟽;+˷ ,QZ.¯ZE| ,ת}VZo~6BY俔? Jr'j; 1==''[.X_:T%G =(c }-dd>`/#.NL&s6Ņl3 h.Ovy@q\A#x.$e@3{PРe\7-}ܕ=6yG7겒~0xދ~طZ쓐+.Fg/SIsVMQ7o hiIOp<`/)rKg@_0 }]i/8c+fVnMeh]&H.zy^8œe52(漾U"Aȅy{Ą0uY^ż Lc C>=l,RAfM{?}Ʀ܎xWۛ 1ue{2^~\Nx`йrioŒ 7E6Y S[RyJ-L%_YpNݼ&U}/8؂wOȟuxP×ފ;Z01dZj*/$V_OԣEG$|ztUh\oߞ9)BϜ}rSH=OUGb_{l}x}H`g.0 g}ѱeݔ?|~<,X,mv rbҵ@1Xy%umX= pXJjb-0rUxWF şKYq7(I|P;ru H%EV<}sxI6/yIEf/qV[?u|}_<=h_짝>pt Vz\8P D3YئGl1SqA=zbQ~0}SL6Q^fX8o|LI{!x7 Uajtģq׼Fz*\d8i616z <qq?zr 뛹p"tH8t^vs9=atʲOKMS@ttx=F.jx-nTv~^kϪ 0c)ں`TM!3K`P?.Mu)fǽ" $yj5]nl#!/Uozy:y׌^ }{& OH3Qg'8'pFSR+% ~n툃v)>rx wzxu=rkr pqBo:.7@h?p c <d]Ac)xs{A x 6{E|e @4~WŽxć ,T&c^lqK4l _@GSvډ1%{ Ryt("Ǖ7߱sٕdͯēqϰUcz̍YpYh ÿ~ïۆ>o}/}}W_xCAȘg<v3jONat(P@4ぺMH[8A$0<0@9 | P BQp{uv!?+9\T@E/,1K=3?mb7DckXrR@F>k|:=x_xy9^DO1Z/#Ou k[s ]D܋Qh3Yx'_|lA{7IRY< xszb0=zT>^z}xiR׋{~Wz0^5e$ˎ 緧x~TLJxWl;R`W(K-;bϛ5>P<5THҢ)X-[9NALq]@nGLZBeZf%St͠^c^dx⃱bmHSzѾ]|/[U\e蕒Jyu @IDAT~x*#d.Y^^p bD< P0E? szq~"Go_ 3>D ?R#57,yc7ňB-vssGVR"80Ń{.iHP :rڸG&0}8Gnt1Dc 㘣?8G! zl8P_Oܱm_h#"),g sD-WlL6"x- ċ** d>F9/V3zO۸ihW)h6phJ各|5庩/wqy9ïzfB*p(aP^47X=|]gme&к^?o\O+2\e3`Gp3g:..i\0,k8E\Î12 q*Lϣ--Hu4#_A!Xѧg&F2 z5E|(G>>S?)WΟ*z1'\M8JD! 0-3*ckvq3ǯK//Uw6L(%MMdį_p9@tʩL$fS0 t wJ W(P0[̏caϱ8 *k>c2;V, s{`6~MCoF=i3o l'_1hRLgO4zsI2G9)^OO|ʎ^m/ 8tǫ5)k~'{<7`Q OA&kс7Awڗy7_X ( !M}Quh,^}Gz ɥnc91u@; pjL6 $Lu_pr1pRyd3 iY]$=D c͵+Ϲv}G/82<#ZgG_7۹؛&S/c5Eꩋ{w>tz~~6 YFjtr^a\۱s8&NrxR@e&Bԅ/z 22]Gmܸ6#j.ʱxl}gM:W.ȂaDh5#-6֣Wp 0Rcؕ1%L JR:Q\3/@@&Dk|S}KB ޏl<ݏA!j֗ðGD|;R|#*">acxMnK;Q[yufWÌMǣl8^8KUY #Dz>vx-\@mq9ϢfbWgR|tNDdGII,_q~M1dyڿӶϏlŘO`k-<>7AIa=/uz|QG/P=[ ,gU ƒOO{?O ݊ "͋[i0\- `/iRLumKwXlzEr(TKJg*7 =|pGOCX -܀ZU-Qx\g oڿlcʿӆ 9XI{!-+cIUa$ ^p7>5/\MHV ˦.29D+I|e3]SC2qzA6a,xr*/IyLďX0s'K5&@-;{O?|;^yd8VaU<@*rO8!U_Q:%dVƣ:cꢉ hn-j͔_<ld07 $/rc &d X#V"igqM h~K ~+1-c_O塲ի1vo;>|'oa=2=*VT{x,Ш3{}1vo#OG@6 (3{ lG1uxUK6p=|=]1WeĴf߆~: o_^Zo 7*f(X<8pTox`_yScʂe3 AuM36H4rSf2f:a4鋂3UĘG1öܑkG~L^~/\ A5F;w^t|}?>cGϞCzxj0“ lՃ3 ƶ 8 -|%G𒇠:?MwFdr^7 'hyg[[6 Mϥ[ﵺuJ?k}mcxӟ:p)&{mG8nX衇^D&zO=].Mu8f@`ŋ~(b / jLB@Mn'c2LУaӍğ @ ?_;s90Rtbl .q?`dr, k0@04@:BMD%|̛i  PaN=e0y÷M `3G]~>IэXFY<4b k$@co_nߕJӜ\i?9~Y"VT%(6ǰ!#3zo/dC{kܒY0)laT#%ZmqCyxs5`mכ\F~C`}1w؋0کOͦ#q8P{ONx}.c{ex{YUSEbu-W ")?٧)1~Scolע2X<)ޛ>u}&Mv"u.jƆ5 MlOmV"zׂ20)Bx^8$J0xb9)+DwiTj{BQǰ_ yI8f/ꮂ-RUO^ez}_bzYDS3cwWe"cUq{LGVb2[i2 x0^T= &"?NޞÕfʘxTĘohM AS_;td_308xqىGi̢B WtQ˒W6q6VA^kN_p=F[!(F~9j-K/D_ zR*-y~HOо1=%q/S{qgMfM` c\]m+Zap`\5Y `IU5nǀ"\`O]%t|-\K'IŀyUV'>?/3w}*7-ꅟ_xcKy󚲍VSp{<V{$^eJ-v7EiVVE辞+p֋l?l$U3s_9=o=0|v~V yH9=_qB6d`E\G$kIg 9[$3N t EI>&djVOU*^2 Nmg=r}_^Ef/}op?+4k.pƋn !aNj]:LEGe?ooJ[1i&VWuboyQT-/N(H-ؐ0\~r>V [0;i @Oނ-Tq,]QE:[^<4ry/4*ʪ!?mlK4\v }L]l_̗ jykOv=Wr{<ڙ}W[얎R4Z0}Rix\3:u-GBڏYy +y^'o?3c[몍H&zw``i$3G[ 2 t'rlp'#='5t Y!Q`ywvԩhV{]+XlU@,},l}:ThA4&ۥ+HH)>~@GUiCF;cГ2peVt6&6 }<:S.1 EZ+ #S#`7?M6OdO֓6SCHncgmy׭3(C+[۠Edmӡy¼ !tWa,tCG)k6bqko7<|o#1Ok2]G8o]ٯ4hsI-lM欄d7X+O$JOc'§/dU}+揹D|~CUQx]ТaGS 5=5EmǨ7I]뻔;A_unn%~o}86pxJs&C J|L -2khUӨ0 3εXd֢Ij.Vl =j}xDErx. ?y_W{ Vjz}DDV.-?큼|^mvk{x-,[pɣ}sq#>g]hku[!rr GGʠ[FT!^C635Gѡ׸-Z,ՁFxX!bP:<zSEYauq8\᷅Oz-33ǫ_}mo*ϯ-Ve9 jǦ.s ov}{9BctI~%_AL5_ˏ9syH:іEAebSeS Y tjȧM} S_ic>I8y$8z_c#:jG*-?*e7?+-zdt%Gs5p 6k=81wF5o@Qs )3exw 9:yl}xO9=733G~%G'uEq_u1cc ,>Y>~$`zBUOH8ՖT4>蔡A#y=75hf +A "O-8xk3jk,ã{G 5|Ysڻ[)GY; Ip_Ozt(JW"{xTzD,=/W䊣_agscm'Bo FY԰^ _(cѸp־d&X[3Tԅ}1Df*B:OAh_ԹP VL,x)?`TUVp8ʊA@tn2Ӆ2~3 EW:B#ak2;no gmz`肰s,ngQ Ը8+w'3.q8)o@ai5Qxi;3MX΅u{LaX/63 o/Y8kWs,S56z q&pG!g#G Ez FS8 ǔD9Qǀ @K3del' 0]IEɨ\4k?Y4l /FS5z{=+Fh@,@tϟ *cvQ$Zb8a>ʱƾ<1yottn8rI/cV?ܨ5/eCou] e8~5"c6M=o> uyrU[_9ąaɀrD%Cz@8eZ^b BTa~K*9™ ?lK+}>]2w{^^LKz^^hI`KVa {ɏJdxm=*U{^^?jmο> `m틹 vAş2k?)tljVz荧ȱPv^~\*s˸k2p >Xp ?e8 a~F>˚f!"Co`O""x<#yb/E{)ⷤ_M~] 4E=m7fm<ꏛ}swX"> ɮ/vU^!28է9 &Ll]p#ksy JG C̐O=_D+|sC]ؐH/?ū2#WOCCˁ@zIy/z_52g}dxԾ1rżz#,nP{^L$#瑠p Pk~Aȑ@, Z'`Iay:("g)A:h.a .$COg8G;+I.W]yUGgoquKk>|LBEFjy%t\W+0L2o0&ٯ9`M~f+zİ_8z$28 ,(Ս;!>ySߣ\#{h5OhF{g8߫[ 58d}]DloN8~PpVsv#.tH":"+&fJ25d˭SЙ\]EIz,e& @*XZ-x"G̩0up<ɡW 9flF`fbtQcs3fgF'd60nwڸg„^qnͱ\X{?kGo~ү;3EcC,W?}Dž ]x+K-헶N/~іgUBy8q 4nIMsP.H% z4qJ7ɎSc|Ӛ}=mkYh3R\S-* \ J8!L\ Ѥ56B49sc*ZϽG9ZL;AVu^+CR@R\Gxx)Fnp8_Prr}lqUkȗZN8 dyl#KjYTPrca,^/xe0x؟9Uxmwv<O~g'- f7yNxrcyc+3(#/i C=4~^BH ε8V1#$Vd0ֱlܚ_:n9OB1/n2z<9lH^qWw{^bYxLTXRaP/MqK?<3SJD1f|K9~O8^k`S<34e+ؗ3kE匄AxT]>6`ױ԰<{)cTX4V]-ţ22YM@dc~vz9c*X>~'^vOl~c96Õڏ JRs5gɃ`'rqyEGzѴ\ڍz󹐌 0}>C&X`̀Fc@ME=ԏ cD1=NŀI*=ݰy?;u~$܌?Ps(Ȍ劷L~><2⨟$E_e#'ҏ=o/:Q۳r.+1|c>S:Q?{ygJ8p="yC:Q/};5.6w^g6S8؟klꙝ.tH|n\bM0 a"֘J=Dm:-iBO}6F`lC4*pa-+ge,c$r1``̂5*ez{.$43B'[)HAbSrENT΍q$elW2qIIpaA#\P6q$4WLOwOLyg}~>G3V׻zֳ{9|;]q7}|njFL}wN?ۦjaܣqZ>ĕ󇦟Oṕ#GPpԺ(J/ 'O >?--ve|)cQ8?\$%%<%}g xd81.8Vv?פaQtJ׻#vUKa_`w^d\"RG/I@\2"v6S1 'e?sI >te1n}EI$1%LI~F|qϬ0JYa'Tу-_tt>ո"WW|ln/a6>pb7Mw\[tՠOq^">]H\VNxq928NO.XvRu}6;.!ZU)+NMɵL.&̖1&C*(kxZ C(Q< ?' ѩP`!9:*\ κ0{ON",j6Ϙҹ`r$J$ŖYO|1 ,IYJ7lVVZ ŏXw/atV-nSqUl3MlPf&'NzWc8gܰ{\abM#q =!t)o/iub0Zt'_~[_28/v#c+^yv:k<:@?mv9( 1rrҮ!@(ajtX_:j WrӦf?5fC\`Z;^Z> -1Zw+H9' 9j>,}ug7ѐv0GO1)i4#6^gc4kFQ_xԯvm񝈵 {:FQߓn+|{FQߓn+|{Fm+m`c٫%߫_m'>xsw=Z#SFoFMI_>ᅦ$h]@ȧ ^!<h!IOQxD>Ѭ8sF9łe$lyd;eOk,7Znވo\7?>'_|ZU& |nQi# GlxpujG)j>$4+vHAԌ17xX\B1E= | @&'Y}b<\Q?cGm*lvJhK8 c:rtX LYK3(9{}:iCE?b,n`Z:S@GhMF~Bua~l3cH/gJ`>%e?QOC%'XO=^0g7k uecqYFvR h Q.ѐ/\Mt[ [8&i\#6ꡥ5Q|g)45{My:pXwc<l 6Ga#i=Ύ-sW6OFrJA- _ۧWMm\Oc蹶)eӷGqn:{r _90qPy,EUn8ƃ0dيdf'x^y>]򑛁z# )!گw(@,b %i~^dL5H4}:O*A3?ğu @ (3/|5ˎE$i4R:X.Fmmci{Gc9|%ۜgSn5v]Td]jw7lk캶XV6_c׵gď|u9/|ceio|gbQźlhAv^Ye~Z#JlP(7NDfg S;L㽌1h |mX7bJ;.(NH(!ZN] l'/S;]Ll2#қ!Tg[B9_2gx5qL+ۦ?gpW=q#ǒc%+X~?O?茶 |' Ǖu4@ T _Fb{=i} TJ^?NAR?_Y4T0IhǛzDU+)#-37z?1c^6&V:q+:ʯSmZQ1$1'0c[f8%ez< ZS>j<۞?U\B_>cnA,m4$W=לvX;[ L+3q{0 Ċ߁E~]'4i_&LE/¬8AnH>غ{?ٝyzIVf@IDATҬyVi%;pn#A 3)l^uMAn[8l-~|N)i2_Pph!0ฦM4ΟtC"qI|৆ |1/% 8>8O_.ji)Po3ֿM>zl5v]y,+n[Y+j{,bʰeߏ}=mWlG}U͖}GfvxOێo$PGh)|fΆވ8~E9rj@av8t*i#0j=bfodVٞhfR?CQ~A3ݝcs32g*'y}_j'Y܉t| [*w{R?8#UVt/ rqb=-@^ ^!hp7lxYSeL7>$W ef.Yt@+8OK@,1vEf┖)(n68GLu^mֈx'nb^z5<2sQS90kj@eh:h^Os._M_SCeU #3G$E')B;Un6庞6~Mos >zqz9| qrr>(\wLY6\r:A2f^97/%{աmdȁK_rdzYy?{]֘|* .QI/s:NXgBR簄N[ڳݯv + ;S6d ubfY>~҉CXo+"Ne@g A]u%LJF땏qG ?[agaY}}SoTQWPܷNme3 Dm?\gpohJ md.5?#g^w~_s۟A;Bϫ8Tۼ6gF3'#yٺ%&2,,MjLg5–`frC!$6A"Y/mIYōCw& dۄ[`<j/k"M(I8'і, ۅ?V4f#`sdye>8c|"`rxz߫f fSg*Qر=bgK#v tע_؄:_gĵtxڼI%{+zƆXMVWK}`d<vi=kF<ʇ~{|Y0ݥqy>H|{Au |Pd(Cl0D% |=Gg68n2-=!&?W~D/F@eIH;1Gbrl$p]Y/R.tYGlx_[8ϙ=P a'E;3jDɎ3_;Gwץ?K /Ο4~x:^Gϵ_wnsǂN=s`®CI*p'@ǼlAZHʶcХY̗ΪX]MЃzJ=;[]L q]:_&Q#Q4]nj#*D;l>r~ϴ:i0ڵ̷OO/|q|DokخyV67Y 6m$5vE~9j֍1i<_L㾎oī=A'ٞ~~',8-e rsr 7+nrDHdu=xL٨th G|ѴzL>2 IMf<֍aqdzl܃(g=x Q6iǍc|{~|Rʏ~şRr\7K>A:3®iج2m49r^~hբ@f#(OOfk(p(hl ⩨"e&9Cĸ$Zd@~QSc¦1m_ D1@؞_pΈ,B!{$K|{D T kǣAH 2|0 zS$s PMegj|c\+.fKߘrb/s=Fj?[$74/zWKQ$FgXPw>pNyr;(qn3=~׫NO'r]j k<~BѺ-1 6xH=%P^pYC|ŬFL6Ŧ?UbIG{r]=k$_􏀹_Y}·z.=~\F}*qu_x\gD [}?|w4Wgc[5_SZ+Q*Xpzgi|G`#ᣗ{Zۣkg^pצi*V˦Ƞće!&m7P+<ѵ[5ż3I܂*2pE6dh T't!73d b{Z&4=gk7hS>MjL>1o<6=vU,99fRM^9ՈXq&5:1sg ĊhiBI&YA':Yd򋯎eksWk1Qbpk(} p2ƴ6aN89mvw]AߡJS:ڜ RA;0C70y ]խ~\ fD- 'H)3  ]2đ)cu]xÊtҚGra(ttcd7_ۦ/}?<=t.PKN:z01|KI_/qՊ|n>|3UnD1a ՓHlGnf(],Mƛ2H\8އ&v}P']C3V_'V\rt'N{ސHȚc>=m[|1ϖ+O-ЖqKH5/]:zt ~ yzh/?,?nNs Wykįs,AƌSo~n $c~odL\(:`K3E!pgr gZ=&cLvSa A+<_}MLI~ym'kLm(Y{sO?OL-2Q&ψDx#4'< mcTcbMx'`_5&#'F>CR/ji<&0#q> k?"+ u^M2z$`<5 …Ȫh1O Jh42PWrM4,n$H TN̟mzNj~4Q,Gج~isvJu iC'tZE);/gC-x`b'ĜGD4 bub>%f-w~T}ynǷ-M6ga?QmnS򰾄?>kC׫M'8Ȝmu+'׸Bᰱ_ț- ,u|#B1g^Q6dz+BVN]+1R\10Ix G5gs>xuL69磗_s6 <Fh#=lT A@:9WtyKxO=x kImo|eWpIq9qp|h,+c"N~/G+2慡N8t|_H|ޱ*6-EKc ꠠ*jvX8o|t5?uj&||cw'. NJrچ_77?'g_/ǂ[J~G`fԋQkeKMmyx/$`\$'FGkx?7ZAs Ibh)}$puNxZ{%1i #mc.~/}饷'E(^;>Ǘ/,=[. ǚᛇ8 ࣮o\*C_7[5ƞ6DOŽ`د͘m5*Dt6Z HTYWwUީǖ+.횒),T;u02~m h1(>'!f3҅Z2R~\%}> h/ߘ#_Trwg~'xğ\wBgQvLDp-~!R gMY 99:&-[ {T-Ǩ{{"*Tӊ?ڕ'G/ (h'𧃡$h׃a@C.o9to"n5xᬏy=_y\|#_0};pVMѹת-l c #+?nY iS l$/RRt~G;:#;A|ԄNG'*6q)!d2?(x; ҷo톤t [-uۦ{Ϧ f^,7cE'n?x9+Ǚ!~pfM}v8'}v2F$@c|}MD ~i ySɖzn|3訳w^ʹjzGu\MnjMM jJSp3/wfwBҝF p l J3|>8(6wJ׋ia!SՈbcF:S(i&-~//k;N.a,r@\ ~ycZ2ppߗ dUɜ(Au޿/,dۮaLLfvdKLh)t3Z4tO뉹)xW%+Gὂ7;>c tMh=c3pcz(|m%dk_|s=j4B~ZnNc6.]}#>^isEۛ\\qG:K+ (m ; oj!=:'c&CE[ 넃߁7iش_)9 ?mdvdِq#?If¸ݴR{K0` =MmYx^t$%L?ۦ㛙3n}ogy{ʵOww6͝_xN2[LV'1 p|B$a)3qMje̅U&~9aڝ/rH[O֔\ߎ4}>ǘޟ2eغQ.eBrŰ?ԙ( | Ra ͣW_+H~:iK; Kk>5APT 22 BF ͥېc>G?J?[fgoɝM-Фȶ*Q#SX8^{ϙD밬i^[gw1Ul$p,%^N?+&Ƈq9fĸmiQM1Ī,˓9OK'g<ѽѾ)әkΝ iOGf.#:ܔ]wNR/| [ v1dͥ*&9s?2vO <ܰ`ynqǙ[pv|s~F>FsR_̣L@َ $M͟Rk#3V; nIDlнZ4Y8x|dqcX̄,KRc+r$|+\y)5kF Ψg? ܴ.)PXjծE0cl%yAfHmsɯb?3 Jӗs¤TߡWwn_X:ck1UlxZMGqn:~"÷?\x^x<1<9X'ZC)FKifĿ>D~ʲ\U2eâX!ELKVQGTd-mWR$* (mmC秄7UyD]:wDt'_3{7|H6 ,?ɖ#9znzEw]/|}#FwC6%ާ۰S6M[6q9uj4¶[xch9"hY-tGmKXb8g-^߾g?*"h&5fV룗] 8&ZMqTs;_CnJD[mdm;y(jjd)4u}p+']͸hX8\xA]vcI|2BqI?;L!GƜǟSno7eٶ]z=%ߨnʷC0}~ T11tq]#'&I]J; TڄwE<Ѫݘ[[%8E'5W9ZbEBqN~dT`^ѿ%P_o<_y|;^}63񶛄ו:O_'ŧ/n;eձ`WR|;#wf̲ې)O☍;ډ#&ik͋1 Sb3[/o֡x" 'M^e+0^y8}3tyv.w>mvdgyG^LL3S)%@LpkBG=֕Ds\1t@E=_^.3kU8I2vKm,|08$Ý}H*OXRʔ.Gtں' N.]t522R::Fy2F I/ꐌNLޡP\]I՛|Zeg=D6NVNX*[g3YOYzOE_>Ǽ i4QU c@s=l#S4O}3l'1>F,Md}GiMtw/tqLֵ67|}FΛV%1Zo9aG2h VMQTʀOuB1:bԲ}3vyn㑏Jy3~]&*%y>^Փ)"y_$~eI:#u4C~ wOgߝ+ ?Ffq];+76Я|:~_B1};CǾy_!`8|\$?~_[ɇkZ%-\Sߕ?69!ySun?\$cě-5"z΋=<[V) 1M\[< M ;PIg?`G,^!5>#0d%1g%EbV} Wк)95(Q/ڬ4v, 06ŠdK~Zhkb*I[ }.rĀc8Rv!c[:E=릛>e(Ui>sXfI:};Jc쎣 dO>#G#Yk . ס8(nE0G Z%/S{#Pxx(qPB O=#<=Z83}.vhG"k\sLy#1~O=4*'#|NJo11~S|+{7&bηϸz j4.k]n^5\[xV=#q>_~E 8N&osϜ-m [u1#Hs@ߴJ^xĦA)kb[YP 0 Yǡ1ɑa=Bؼz>;C[<.’i/^h8p/?^vvH\}mx#K̮}fɏ=2x~$eMj"zZ$צn1VT?PNy&w+/R1Fu>i {ę1+>e#`Z HӤδL vDɬN1F+ӡZ\ 3#K<Ͷ9G< jƗEh/sИ58skڛ(MQ|^G4rT+f?o7<[<#YF_rG|vl4rL3=G/c# #-x>HFÚe;"Ɖ:O$q=V~ >c40::׾lzݝ&0t8v+G[^ӄF>Y2OZ3 "ăzM%3O_×oz޴f1e,-q0:`K@o mjiln9ZC^ѬlK^׏gD4YPN֫s;;.lG|SOm+l:#ENC[&3ZcRō}6af\fa*/6_[O>FA_K8fՃCDp&_^cZZ;"sAfu5DzkLQoՂ단 0Q~A0pL&ݱJ@ 5+}%t֬[-3P_Zv\QZ,^áZA=plVlZ>/<XmL/?[9f2E.c/8:knhkq<8ԅޞnwWAx-" M߾(K+,Ć=X"kw~D/,0N0QI'f\ :z|l~7ů2Dkvtӟ?7 H8 967?7?Ə>oTGFl<_?Vqhg H=Õ'%q in8)8hc<xn_$Wu0y?_vqp×OŗPȕL0Z1m9>>4:ǐq$ 8Lh3 )n ]/CxT\m&F]<~a35i#Z,nu&tay-!#m<'YJ*8WƊ?'OBIᙔY޴kV`QIx줩J_@1ĭqWY_trإKD;w`3a[ZgS8U+ Mf&9T ^p?MTCHtݷMg7S)AW|_yKoÏSs^_zb<[~@TuvPO"xx\'0Zjwl^C ?ɉ.jБ68۵U:+U偦G 1WR/fH!`sM{O}Wz}u٩zFbO=i#WiGQ?coZ#N=ifw?|;:&'"66k!y~?k=O)@<+ynM3!qe&G ݨ Y%)\=#x4œdT~4沼A$\2G'6eo(pm\dq0[nK6Qz*|'hCHTڹ^Y[J_ק9V(-4FuhVTQp6b1V 1_9毄XO*_dpI-igVՠ"+R8+Q+QD{"R((;欓שJQNڵa# ӁO.N1}krǸxnB8֩C䔤uqěa{Q Iw&SH&e\7`KjAi5g݁=_d=~INpS=fa,Fcu2=z?XCVp|_M.8jy1ߨvookzkg妐CF}S}ciG})f, ->8Zq[מ^…Hk8O~׋yjŇmJf**#=7!"&vAgHgclR陨E/Mqx lf:#rpCy3@'XNh=|L ~O(}1>mqu1pP>ϡMŜy,WV3r;yf:#sUK^uj3/롤!q4#/b y]%>5ّ黭"AW@= ydC]+!{Z3gvU0r9*49b%BiHG臩^mxcMH=`V0,!I>@IDATX_;T4LQk~[&+cR (h&ڪ_6_HzWow,ű4lm$?_v7›=O<;9-fz }~0 W>g6f8G=2x%$5:"R,# 48=s|_l864Zr5y738^TGQ_ |3CWU\qo.&vKr<#W춶Y.GQ_Ygs%?K1lcK9,ߧO^M. :xhӢ>$]s?<5FFM@FrىP/ktoڔ2V%ЧB7}뎦>pčt/XxZ.@|-/M_2N9<_Fb?PwzrƧ r>M\z%pDbaf˜C]OxD.{CVTQQ,s9_l\O(w1cPګPN/ƈ3f"ogl}P)81'|=sMǺf 9j;kt."3G\SJHbkWaE,riGψYZ81̨t:9=^9\f$/N=p< -aF})fy,p#6}6籴}||%ixnTE#GQQ1~ȿM|c,S}tpD22Ÿ0,vzs稛D0yaR\Ep#x(H{0[R/w8;b+2OO,-=54`ceLp[^~8}kvg4c뿏?3lz~E~eXˀ0h /DuiTU ppV@&O6*Qi820aCM,~{K"E!=c7V"J!O'n{.^nD5,/ߔE VMj9 P 1TZґbZ,0,2=I߭بhϣmZtR`9шc^B 0Hty-qwto:^]ޟ]yki<{?߾e;kT1h&]y1z4--0k% ~OA&@TSalDm#.ٽB_3#bL;a-qdql9>dˇ~vƸlf+Om6b#g m%ۊ<=C~6RNea2ޅjV$le=b)x>c],UoL22_s2<'VR6B1i.hB![uVַ jnm~̡y4񿘉aYYlg-lx=wJFj|oƼФ4]"#\162֣xAmaUN~ϧj g4sO]\Os-߁r#dۦo$@2m7]smѱXMص8l |c9gȷ)is*_hzjcG>prrxt^\H|xğ?/QohSZhŕa}{O,f1̹UhSXE,tBU v@!Ơc*8 A| @EuȚy*O-|xsɣ>J˃<֧|[p4jvp.ON2F݋iB Xj |l+^ T<z&*X\#̞{#M9m'D>i(!__UKdgwnE[pS:R|6籴ʑo^cǶX~Gm3J)Gmz]ޔoY$cnˊy5v]y,+n诱3JGmKcKmY|W? \(=狿jgm@IDWyَd Mo |} x^RYMF~ߟm=f󘎑^ֵgo^Ͽiq,YiƱ9_̓xqPe6?h37>T?ׇg鏬ƂZ" 7&GOqSz#y`\jʯq ?=E0)aMQ)m"8ri S҈"5E8eMnC$#?MvfǣӱM㩳p%6ѿa~ʑϺ{^}~|'gg'1EmJ |%@ 5̓L6C抐JDWfͨL$8?IK8ANy& iZ~Hă͖ISrcp0ۦ)Y@Ux!vNw^ jmI&h^zm|c!m]?7FmWnכq#߾yW.vV|_Ο]$ś_>.\DzW{FM`. .)`a1$# joS2 |q&y6&Bkıj =`C}$k%cۛL_eF/|ΠT~1 58|ǷMw+|xV۹|Zq4e1_֍bNH0 M͘G6>X{ά/TJ.Qa>|sZpTlH9GW] i+ ;qM3GѺ Fp,iG[há.c#1Gkr k'zL,1lEv"YloO 5]hV&kj )5e·qN|k`NlƎƟ4l]|nǟx"ݶ/$Ggݒ_t-okw? m˰7m(ӛ6;OS,4Cg..WEw>Ҧ6!!LR d1$1O?a_xHCjK&Yuޫy_ 8/xDr|7No N:Z#߾eQ8yoqvr_+k_}3w+ʳg>oi mO~ŗPqv!n\M>qsXky^gJ^E@)h I31<$z;]I$MqHpC2m8N+8ˎkoWV[#ͺ%1O>x]-z;s gXsZYF:ںNuM;4n;X:NqӒO$d:Σ! CV ,(æ!wOqu\=O?acBVG%[VPx|lFFF'9F.G7c^|,|.C¢VHcĨNjsͿl`Wç=ͦ$D0R%li#@Z xzƠ#ᯯ_~|ɏ=>]~V6Gi^W>_wtz;Ws7._92gMWaG_w%aycȈe8)k{yVγs#؁Q_%z 6gQ3Ofs O>sI? hk:~x c5ȷo}G|gg}iC3ʝaHg}9?ߦ}YOI>nv7微|s {o:=y&&|ǥ>^}mv,[axƟwaT PLpC<\%HB[ePvDȩ5(] =&4Ζ9Vd"î3;0d,xq83$>{dBN|[OL/hЙb*I=_ěw}:`|)"iK@k!GxIu|f@Q'T>ڽ>ՇV[J5e3kQ ✣Տ9}| Sfb _i]C>I5\i[K$kg:տ-kg4庨룍Lhtk |̦G}x ݳ}Iכ\K捵{E,, B` +T[XrW]lH>nk|GVe=^mckg.ME3wČ;|W}n@ܼ;C~&:+up2D6+>9oqyo##dSйc/P ;(?;[#0#gXZ jE|"LG,fOO58lsqV|E@Z0WRۻO8{#W٢>Z=2fǜk>ڷ5vězhmR̶<*߶xY'Կ.ۆ7:-1^3濏3O^t8)Ɖ8q-):;zpTNwTNfig@oq$Anj_\4 2tE=G{Yьw:Ia!W%3L6EOz }4Q_7g6_O~Әn[,-%P cvlz-ucNTSg9z,@JS":,#u&#Rq)-c XW9\Or04rdޠ^V>FY^nQg-OPFHZpN2-%~7< ѦGtg}0 P 1a`oBz>\c(j k׼y5G#zS_0=;~Og1q Ѹ|ǩ~u7MͷN :^OCև7:=u{,QY\Qyĺ"H1D\rkȬ3sH QON2HE(IoxXcyj<gx{=:8vτqܣ8t}miګ;j?P?\ǩgp!OI7!Y/I~ Ͻa|;mb'c͠/>X0X͗  QoH3C&\<πg*Qo= /ic d>MD 8ązDx`Ȉ]xp/i~Qz>Cgoo'G&h3n1G6MjQͽ43?iVJ/VzT|hx4HI 5mtxI#'[nVh`e 5u(=k=Ͳ=秱yV8fO;ҼKwhj>$ߛn8R'!LWO6? !>ϽgxN0T>ƞ<K/2Gz\Od_>PS|Hb?Z'u>ȍ[U[hd5hN[Xw˹ĕK*jЂJ"O7˗3W_OD#w"m 1k)|sֿ n}U>c1_Y*߅O+e^;siϻPK]hq^\V&S7M=/?u\T?t#o` Gư|x /:Q?oDGbiPDiJQD`ҤQ џ[%nמGJ|Qo{^y,ۤ3c>/},~򴉏JKh|ڙpT=\t'eۺBѺTo 8_uz㋁i?2 @S3qgh/Bb`PЦ&1 sEL֜:wƕojq?Ѡ^7QmP}_bcFZ9?-Aa^K{[V|q1շǒO~ǵX5;<g^Иz;RY `|gl:G[8#a X7_ C8GKxHA)?FSBw>Jv#q@t~SE!|95euQwxzV+?Rs?݊w Jb^h٦2-_ ߶n7 ?t\T чh?>ʠn18enNDF9+H\Ǡ[jm8aPgH6OFK_ ga |t 8^׋yf{8}#$Z;-:~uOV^!mzlzKz<{|u ^~\~gsx]<>r$jey剻s\r ;qET@r)(B^ć.<aI NU)סhH_玈%ttᘯ߶((hY]7O?o]3݁?F ᇧ/=7kcZ+2oW{Bw$j;"<|Z |x6s<'G3\67'GM'ֽg^pyQo Nv~3'2yW3<'YkL0| M9poh!4 q}V ϟ }"S[CimyARN]XK'`yWÿۛ~U'W_7⏪(\|?s / hk8VL0kM2.^nEDT"9f¤m"ez VPz8_P$͘|ep]4.%3Vupda]Ս~l|9!L5mFځ:%o'}K $DShY-6冧A1JVq|hܱj.K_˥mv4'Q tؖw|C':X_7 /7/\ :=4j]q]Z1)czlt:*U,cW\ID~2H_1 s01w繬_NOSc}6Oկ<';5J翚ϝ?& !kM~Zؾ)vG}!sOuҸM]?]8e>{>y`+%1<~l =jyC3 0ܞF\b?\K8UIb4&>#8ɵ 20*jLW䎺TzÓ˹,ui_? \e}]>?޿45:9w7_]G7Rì(;RM.jw&}Sǭcqw/|ױNwiؾٽ {f'D`>?q+Oq9pl|k{O@=tNس8/.>PEިq!I*Kp.Ō+f [%$WħM w\T f=75KRF^*Q+$w{ӿ I1vH/%cX?ҙwt.rs|Ѻœ֙4<\hhQeVax3c' ~A(9 Ne*=^0VPc#,+amH^">05s40ylu8^XqK@aj} 'tk4 ԥTPԕILښW nZGiƐ~4]xz/wowq0+4P*d{v=~t~x@<Lj]Ⱦu:ّgݔϘW0w܆oُur˝wWFPP/GW T_ +^m}~MN-iJNaN(̑R8Lf"%5u l(z7I1חoR 䇟3Ŗ 66]'G.ؾ Fwue}Gʷ)ȫwJsX'kN΋S/7; Oϣ^z,v .GJэqc]2A`j:tO"itoZ5+ aԥ26ӣŧc>׹k#7M:c*/~ӏOQZwMCΖvF.l 8Z{m}a.wf!@ᯰpsŮv X"k,²e ̎28KT[gڽn,9Ls&i ztđi**ɯXMܨ3qlګy> Dx[Jר5 n Ϋ.MDHRCoڽYIE? kT0H>sC7 IkxC+ނ~5&1Cuvf>n,+ۣ~2ټ(4z?@oVRUԲe>]]ssϋm9\%iF cX?B!{dpE4B)0C4 ',OP>s PFG)FݡCϠ'ͪȕ ۽jwP\cPoago;ώ{x_{++͹ϝ89sԏڽ˯YkI IMi$fdFN$s!ʒ)`oIbKu=e]d٤p`\ .!a~]aoOhF#΅|:>nΛ/ ۊZM) ئֻoh\†~ _3Nozum=D?ȑKڭKTâ>b#vH9~,>ldJ:RkRKJj>1ow "!\%)_O^3]U>"ץo5qhąg_cןڀ@zirY@ka>q^pjj?qzXWf4[t <80tf&V"%kI9Onq,,ׅ+Y|d_XiCVtj'e6dEnurGp(|o@qAl-$?ꎳ]T^`J[ddƹk(m[OͿN+א"V3UH8|Sar 0(û#\X !Yr&Đ׿ډoA`x{Yc{<Ώې>:&٬5À:j>?JGuw9޼w ӷ>%w \ye^K*u?pzzB8XP&E~<Ư˱^gWw(\:%:ABٖB֭ڏc}Hp5',~oa8dKOdIqsNq"&V\\N\m+)uqVtrX0UF\cMA A:a.I?M*e3_ 5j쪇x<-y|߫_qymEmɘmy._8 QM=6AЩ:Z~rs-dB7ǏGY 8#-s~+?1ڱ8Dq12!["Q֙+"N0?2zํG]BQ֙xW3pt,g} P_D1yZtCm,ew(e*WaJ_D*#*pd o\)ẹ`/YEj=}%U ~?H{=>/o'//wRwb6lvf>*פh>J>׵1|CMǪ)G)jto5v稯F`={?qO>?vN3vQY<}W_۵"DO}N_s)8 H ԋPrk\\Sxö ΍)U^$ȸai8sֽ^qZo6$?NĻLy+`IH?^3`p)}Ӡ9휌K?ԹS@FQp[aD%"8q#ՠgưId/ڢWFG`Id>M8-Ifw^$`M$pjn@@S!&ؙ8Sq@IDATce 5 z<Эן!m65T]S3n|NSc[~bv.cɸK}7uѿnv (]b#_to2zszۻ}ge۱pOoٟ!"XR/fͩqwY4{rukKY\x睟{@e;whiO'EN?Vnp\:VWii6@1|m\M|'"O]醓8]7PG}'FQ?ա[>&vR'*Gwgw9|aM};z~wyEgGJrxϱ:Id ID\\OtȈ6N"d<T>K"r 8n aEg$:^1//@ziDF$4bµE0~yI>Z'NSn[I#mN{ ?Y^ >V-wHE@ T`A2JR :qy-qkBg؈Y@rNK?]!YX^ѯlSou܊4X3Op*9eZI b= !Xڨ^`:U`ӵ>$qL]o;3(Y,q' =`<b 㔺aW\/~Mˀ0 78(7K~2&pȗ!#8Gt݆.3j7? ha.ߒ|?$ՈH~r}/nO )}\W>E,kTΗ諔y Wl2/҃07|uzSS8݄ηFߏUIYg+بh;0okӧ3x؈?Ki$ɓ/25|*dcĉh“-qҵO(N&%O gOz"81˝O v,UX%LG(ՄeG&7hTKEOsts~n\?x?ƕaL+(;b-K2zmsT{m9aFU|qcU#P?]WjԃD5sGBZƷVYGTy|slV5d3S1{ζO|f:1?-S+G,qk(Y {}3+2\KgVGg]TמxfSr )ģgqq`mtF1wU7Sa JnIG^E<ABVa!7z:sEyV֢)g!'9Xu;Jhy`aM `GO\8Ux{s LjqvǘSĩ9Hu _T[O}0S>DO0~řPF"a밎SbJ<>X֜ɕoMNe͢ _:7Hˈ:SfXwL[:<~įSe|vcQߑfg?;G}Ga#L#posww{g?;͖H˗Yw;JE\ʸ5*<U0wzqiNTevPu<ƢVMzTd1IۯSA7-8hHBHF8(E*E|y~/y-;n9R?tagߒzDe_hj+wֶAa!c%n챻p)2oX q e<~HZЀNjM/Tf Z$/kK;o`zI?2˨L.6qgPwoG`wzy*F!{*AeS7D ڙO;˼MbnՒSؿ̟o|5\ҹz>GAgyIz2NvlR]DIS#=W%P3@0kJf#/fӗsϲC{ן_3B@ug0Ux>\KU5t^<wW!@4!n7.릿]֠K?$'iuqHв՜4ʹv|y7=-kT2[n|{]Y;i7&^8CGQ?4ׄ<7Mv6rSH CF}S}ciG})f,-A>ZNO߉ |>;lQ q<fO*Ny aP WUyEqY/rՃ C]F"VD qoxU7@YL y}EQᣎ|4#s\i 0/YW02Aۮ??frKv)7O{gOMR>눣pQ"T<'ZKx3p:iq:sJ8!( p6:ǎꏠ=qtE152EѾ|4{w?n)gc̡"{7'?/\q\iK{; T"J4*-;g&un<6O#͙k3}/sه D,62,X̱}Wħ/~>ZGvϺ|O\xvO5X~m{Ƹ9}}W|;]s\q6Ō>gC >NY|$$4/?C [[>dӡJl_͊hA3 J#zs8a杻EOm#6ԧ|h#Cvt([ ~k^ w_]ᖣIRmci&9X|3^&<#w 6,m_#/Ō6UGb͝r ?R:X.FQ_YgsujzŎs籜pz}G/'J),=tI3>9zXd_aslؤxv?X+aK:Dx}NQd-pȫtQdR-0.FKHG^}rz=Q훤YnŽ^c/'GوU9ڧ@MB@gGch`F?qz.fhgD|b=9(b7cz`i,mޖ&sB2QI.+T_!gcϩv+qq9ž%ב1#~ AYzv<`!$-:8yqG-U w!*".X-ևKuP6֘ S=.f3G;DY'\ XʩFa-~6J>-]\gE|aкC &L'TsnXPoUƇ5 m~֪6ǮZr }l#4>;3YDrE]iF%i옏o}zYK 7.i|E _fuNgVG]4+_'iv_1#-CBҔL5۲EDR*K˓l2zLy/gI2R篜ﯷ+6mwFar|}8w|c!nQXԨG}UGc?#1ߨ(y޼3+`Ir1&~a=?;758݂RnF8Ao!Rg{#K\! Wx"ELʔySM] *ΧྞPg:c Z+nޟ,>_Oy3zşH]+B尒Ma}q虯*[<Ӿ8 A)='sWS+*Ǩ+*<ϙx"H1O<[T Zϩ;C8~} Z2#(E\Ȱi4V똴+3,LM?Tl~vx$tZgLЎs9A{\~DzoZc\[UfTM/ ?|Yl74 i2vȨƁFd }S_\viw_p^) %|k&~v G)]nk>qHm'3SEUj[77/vc[}kn? {͑WL '_(NkO45eH,ʬv"h,f~9c(NA'7@W\:d rilvч#u<}AW}Uez#v软=1ݖhKLGQwო~?XY>bC wƆ5%t[ ۖhfrDy }=h`Τ6#t̖5fs$+Hn0dKI7vLPכeDV,[:v5xXxlo2w.aUWWYHV LB&P޼QдE`hx 8l,3Uz%qABeµy}V:̗Yugۨ7b~I-O09Jus9UsIӱXun~wu=?Lc75a#ߨON=5xVkV{mWY (霏9Cg=2k|.az4K_?[{I} hW^8k`'>E3@œ)J  xfGbPeyE`,ew<2yvD?jgX'Vv(?{wWv=Sxv|]yuR:7bw>|g_A#nͨ3RI:[q,gYDv5}]<ϳ@#e|Ѐ\H_pÆaծc\>pcI[uE,<,l& l`>G+uomz˝'OPȨux]FQg,_矘;,ccYq&$020hM|=6r(P̃/KN$!sWGrx>ă4.?)\D cJQ1L Hn cX`H$T9VHI03gIi <eV~YOL#::)vZD)3/L9<^~23m1zFֹt]^xv19}\W،IY4 #}+ kU19V4Eq H(3Pz5yowc͎do815.-zY.F%/ֵ[9Ϩfc|L#~z{];*c˗Qyr<C| - ڽNͥcu4^w3sx,Dl\bΞٵ^N) ml#rtAG6q\rkIGK/ELH^YFnjmKp?/)iԡz~s}:#Y0W]dAҶBt-ZL'EdZH{~RjI9HXbBj4ڇ" [4h1`KM:᣹S h[ 3O7`͌ڜ{[2GnDz8/ExC79ƒ1]nT4I@J Ф8'Оי%1eMwoexکhFg_:7=g-~9*ķf-M!)| :1#61ۆZV_y7OQn[^.s @`bcAJ/S-Y}3=l3\(8ƳP_=z}GtD<C@ox]X0#KlX|:\sQ䟲|v{w>ߐ~{q7GmyGmci{#6Ǝsem5vX&RA.ƾmqGa8nۦ; -XN;1纷d3""uoq2ȘxP'[x^MH Ppzbi2ți1lΩDF^,|\Krьq畺<z?k\g>DMwO1cnQv78Syi+jcLKSRV9Fc|Ci*s*#8*j?c\:G;?CutU=C]%mFGzdA'W>I8]oNuG/KD/{?}RhT>nDԛo˚ZC+{+kX†TdOvF\:7{sw]#ۃ?& ;kh^O t4{'ʖbbrSXw1NGm`l1E~{ X#6I6w`Z?o.{Px0ȷM?*z_e8e;5ߺI`ooo?Q9O罸פ<>%R'ºLY`y˛!F7DTeo FE'td:(! mGk>Ew$QWN'B-gc, xG>s s`ӋY_Jl+M4RaYK ~KH[[{ A¸].dvK v@+M1Ju&%,j]>.T. ^#z>[rQ-L6OXw, yNi7ݯI=y`Ayθ_J_]IaבoY_к隣yNz]v7]:ckj20 z5 i81X1!eN|Z!:1#،1ހ4*~noߦNu+VVjk-K^];.J6)+v+n99 -ߥ L_")og_;t1(TOix9h&2W tncL w2m39Ϲ|KqĻm6ۍonzmt4j5\~ü的zsLDTTBMkJw2a`yNm>\x<>~7RS^|ֻnЇ3:t˒1~ϩuk̫`}:nlσZ9{Me@f!LWN_G9?, 5Xwܒt*8}knX9gxz4#{>#;;g%TV<_E:'miaecW|,6 &y(1amzUqIf.3D?ϴW_~F[Ҽ9X֯m>>Vo$&_'H6T_Bڴ+o|c?,_S|.I_oci:NM]\x¥ۼ0)cE)ĵq\A`G;zN[!yѨ+_tԲgqu2D|$V93ƦD/t{@|z޴7}+|=2M:&Ш"66I>~rr00g޴TSA1E1Ŕ|to$;^HR[K֦PGrYW ?u#!5T/+~ER(_[A\%~]<},7D 4]4\\̽v:qg-_UrT|Db6{R/"uD2aRk6cy vkc[۟[;nszF.gLx'FK1W>: 7 HF%s<ӡ@]_0'uD+wQ=$j`&ak月ra!|288(08tZݛB{>ځD|<{W~2O4&^' ݏ1#AurQDڷ5vzhmRNfQuG}U7:yT1~]Gqc9^x O>IQ>ad9>pJ "pN ̗hJY'יEJi6rzxoT#WxbXMX-Q;_?o͈icS񉻗AG+[AX)oXWsd}YLk٭/'olɽB2-oldl_`|0;^Lؔ JJ-89xd;cYgY&yiSbz>iY0ykPAzƈ? c,CU>ٱIpĭUQQHu[ph育Ķ:12CTȇ?SR*2ܴە!f.淋:}yDLز~*6J(GT@ƶa_쯈?k_\NP|0춬a>5}ucn,gh7D5G1@3>~9S:|wof.$d[wڢr_?P>u<+V ! M);x23~JSD Ec7jf 4m_Fl GA"C-S.J73MFfьy8;b[@f!#Qy6(qА@L~'i7߸^[r/4+l_؇X^q^A[Nd uDݵJ*uF+9gb5?x#a&x:>U] LaВ_Љg>tl$:\,9z}^>r:U~^ZmH5b!~[>W' 19l<4cIsCj|ſY~r=)ts>:}=5>Yu1s#O?Zi |buӘ2բ}Ȃ)».:I@<}@lc%+|?dnf|Ex??wqo.7 r/?,+`!>DG[ {u@M_z+,u[X룿^_{% roКݘ8x{|N]MX>Z݂!Dtx֊ͩ6[ïu0Iz?t>痤^p~[M 'c94$_9Spp) s=z=>0ǿѯ7ru^?uF_O?_}*I%NZuxkAu<mJsֿ+^]_|t#NtFqI֗I7S^9hi>Ml}&iI>@LMbb8} $=y& <!USOrSQX0)/bezyCɇ%**d#nݟ#Pqm]ئW̺x>鑳~h9~93o޵x=/\N}5COi}-&E$9s~d,k<2">yzHDꥆ iX!'0*Osg*LMYu)9XOB_Y5G[>8NI׷~T=pǪX15{ݖ^.L Oo7{]dίԾݡy_~M}1"x6|ٟ0"yгägc[^ VoD!~vD6R!禶 eߵO_]ZfYm_Lġ^_=/(xz(NڏCל<1M'h'[pCqrɒ\? o`nG$ʦMTL'm>8G3;b+(3sΟ62+jX߹87-83G|vΪ/ GsJ &u{~[֝x#ɧAPé'[ >HR=H':N&}V,`|\"PXam7_1[z=NIK9gz)1Y)KQD|ۜs񴜮T)N.zTQ[#⣎_sˀBP4= >o%uDxd8́䎠>Qb`1~a:l%ͧ@M<GW08?&p-@Yc8ئ#~=_XV4w {>G|k~9~t5޶MrďXFs 'Ͼxb{w,Ƨtm0;IÑԌ WGxͩ H/3zz"VCWVl)ד\S]yo7L_W֘|Olm{Hk&Wq~lARy\|s#w/~QµKeP>]OWY=E/{G7d5uVC\[%J`5,rojJưc'4᢮U:/YW9[BʸMܘ nj:9y]jJH+ki}4b.9J18^t/sx{}h!G2L60cb>Bp 9T>_/FDa>SWq̔VEYF\_CŻ>OXF κ6Z"YZ Vϡ[&⊉JxMX¢OFms2| i'-WЅ/͘b/qٷbxZ}b>Pl+$cy~ voX;yA[V~\}Wb~o;޽& !kM|h]-ُ#A/^:iA@IDATcް݈ߋucn/>⯌j+ RS/R7,7w|XoZ-;-gU2ygT[a\dor>zͽ0! XLk6L_%8՗:Vq3ჁTKK11RVR@_YOɟ㥹o9^ݗu1Y_,~[jw&a?:Ts>~Js6q6A0_~vcmzpœU3~tMLٝ:|  x? P"0c,<sA Jat V1µcU9^$DˮhF>343dXr_<# R.* ՛.k̛wi9˱+<+żߵSc2ȑ/d:&ʓcyb<2]$c)}]DwrZn?_'˞oMTMPBby+TCc3Q1@۽ 2e]U?E8>zd_/"fm~֧-!M7ϖ mL^D]OĬgq|gwӚL54Hk.j#Exkp"}D77X͹G@[uo oQYnjHI mKeqza1̵ 5~5LyKs2EO0pNqD.?^WdR; MwY);Fk'bv&H;_v?~*ʿ_1|w~ą s?>pߵ+?ٛm|`}o+{|/ }jlIar!EXE"mdRG1gܸ#8[ȼF*"I]c7H YteW7 j(9/>k}z>VwWǟ}i0@˵ωXI7~\-C"g@["cZ ΠPkzjxQziiFA܆kn׺($09ޟ;釩40Nvge,ӥО7@VOrj3}ܩ_5hctXFmR?bp5PQ¾:1طN.3sϮ$U*[{rNk՛#fuiՆ <53Dc'8M!Ǐx iؐ(o/+ %my__BD~TȊcU.Υ[:ܜм K? ]ܟv_.z¯5Wf7Zryؔ_? /qsz̵)I§2Xm 8~|  T_7Eֺ7Su٭k>_HlQt5-based client for Matrix networks

Quaternion is a cross-platform desktop IM client for the Matrix protocol.

kitsune-ral_AT_users.sf.net InstantMessaging Network com.github.quaternion.desktop https://raw.githubusercontent.com/quotient-im/Quaternion/master/quaternion.png https://github.com/quotient-im/Quaternion https://github.com/quotient-im/Quaternion/issues https://github.com/quotient-im/Quaternion/blob/master/README.md quaternion https://github.com/quotient-im/Quaternion/releases/tag/0.0.95.1-rc

This is mostly about bug fixes, including more accurate scrolling back in the timeline (to read marker or previously saved position); actually coloured user names in the timeline; rich text pasting from LibreOffice; and a few limited HTML injections. Also, the "Scroll to read marker" button will load more history if the event with the read marker is not loaded yet (though you'll have to click again to actually scroll after that).

https://github.com/quotient-im/Quaternion/releases/tag/0.0.95
  • Revamped read marker and "scroll to read marker" button
  • Initial reactions support
  • Tint for outgoing messages
  • Improvements for the shuttle dial
  • User profile dialog with editable name and avatar
  • Initial Markdown and rich text entry support (still a bit experimental)
  • Different colours for different user ids
qt qtkeychain quaternion The Quotient Project intense intense intense Quaternion-0.0.95.1/linux/com.github.quaternion.desktop000066400000000000000000000003711412757327200230610ustar00rootroot00000000000000[Desktop Entry] Name=Quaternion GenericName=Matrix Client Comment=IM client for the Matrix protocol Comment[de]=IM Client für das Matrix Protokoll Exec=quaternion Terminal=false Icon=quaternion Type=Application Categories=Network;InstantMessaging; Quaternion-0.0.95.1/quaternion.kdev4000066400000000000000000000000631412757327200172260ustar00rootroot00000000000000[Project] Manager=KDevCMakeManager Name=quaternion Quaternion-0.0.95.1/quaternion.png000066400000000000000000006457471412757327200170250ustar00rootroot00000000000000PNG  IHDRL IDATx|TuW̝.}1V)w+s_EОP'QӊNFQӊULi2@fP3(]jZԝz;5#cj935uTB|< 9?{>=S("""2fD">l!""""""`u] Ixrh@DDDDDDdocj!"""|c/4?gb5DDDDDDDDDDd,&`dwhW@DDDDDDDDDD>MsK~%LTDDDDDDdʛ3y&%[)Gg&o4ogƖ~V>'""""""U(8"""chNs""""""2>LεIVgGjmʦٽyGN}v&fA95s3B1j̒fDϱ C5ήw@r"|Aωȅ AAl xeKV)FYw"Ac,捡tC7Ě?q6E0q͸+p3誇9~=lgH(˧&}aTbvfag"2J5fG}r]WEb̿1rK&_ KL?͕Tdk /`!©3,'!u{Yn xs˝,F=Z7_}_-$9ݼ g }9uHFpFҸ(:;! nLvĤ+bu厁Uc#W]\qo[|& p[nTZ4~1FzX0MdeUUDDDDDDd8dfrIɉ4 &GA'7snvsdž63,o sR` RGdfǧ䕰 G/y9 `۟Ku0z<(5ukҿ@b9C?deE[iEk9ӹdY7ycp_qf1Vز2V9Y b4.^}#E/H]/Os[B3}2j?|o\K❗KWxyw ܰmn\-?}-.BP;#]X |o-\[md)!~t[|??}V[zœI߿ >{??le[_ڸnCnz lYeW8Y5=^,YߟS;-PQZ+6FĚ6&}+Z"CV&| Wq]s>#[}̼ iG1TL;f)le hw)fjjba|GQqU ¥w˛C<|n϶%V]lOM6a7絜jǃ U9깣=ע9: ~{ 'ș'4""""""rʟ3dr%H,յv/ۚz]s)]M۟`omN*Wu^v_aC{I[ʾLBE: hvPtѾ0~j;6uc.pBAٓL‰EM~ǂc6f|KP k:׵<Xl;KPW|ӳ6/N ϵR z&̓!)5 ~ǡS"[s߽}ov|ǃ[Kcy?L;CK;E+z5$F@@r=0O y8! """"""23}ր؊/gh?&dF#s=th)3|Bʹ/"a[὿#V'kd=YUu_uxx/jgXH䗻7bv#M>XA( ̼H._\}kwxh/]R R3<hpq?;ٽˏnz2h2{MLv"cߎReY̚ s2^Aljkmi}.QnYgl3/I-_vrmk{)r 9{Oɔv9}1:8cm,T]kpcX}-*U(X):Cx  $ɱ/i=fr)۔lNϢ3l{+s8xجEN^ :jb33feYRʿ}@ʒZ7CD090=ol'5ôZZ01_=2DL_+/QNQ^Uzy!(Μ 7b ,r;j"~ǂ=Mr;K!m,.vq!qĉsrsm.ł_vgC;l|{µN^"> dsL{m8ck,L'泚D,819Ġiςעs%+vz 5$5noZ؆ɥ~?!H+E yˀi &oғ 4 ඈ2 *s!DODpcg߉6? ȥ_g@`ٔsf!{̠Oj9JZ벤XHY_87YL6pfbA< xp& ʦ~;ህq=-\L6>7D ϪK v mZ~T;w;qd=3sm7x=ҕMͨ48.fWNiDŽ/+#0f1mx=0MZ[0aO;SCo5N4Ctx]bG~wEޣ6v5|>׏__s ɷ| )\CqQVJH9~9puHx-Ȃb\g&48VL^ӚłɶXnNSaz>̱ìo 'À xNO+6c.cap@p29a}5B䬅uƤ+̖Är;`y;Z̟xPY[L{suuقڎ%_}ln&C@_Yy0 ֆ=avX[i{?Hăf{`ŷ[RD=",v(w=X-7עRbC=א|`H7+""""""2L2X4~9Sv^`pIN7ӷ06lV_g716xclߊQg]7 w[CۚRl/\>fp7;N~iA_zo79e#F=6dKpƖ^m4&|k)i׭vUBg[;6K &,lx3uo[E0.c&f:erF3{l귙Fӎ]ϯ=hhcl}=Sm+˶lxm]>p_jq C{ ~GŐɥ? ݢd<:?iwxs*Ӊ6v9H/:8!`4&9 iv~-qbA<|c2PՎ3Xev7`LY#mLeN{T8mǎVo=I uUfNz.C2hJ׎ۿ2Y JRg83;8&mCoYlKk5(ĸ&6W9`=>`a6Hۨ+isi^J/s!B y(""""""2ʶ]sDuN_%JHCtኬ, xi98l0_3ls{k4Ʃ_bro9P_bC$[tg"A^q[6b'#""""""R+Q 9dèlyY[aw:8\9Xfӹ&:[#O)Ƅrc """""""2ED8NDDDDDDD;80(%7r;ivEDDDDDDDD#Ls^U8,E} ^m'8R "'_F siut`YVo7D_gZr?5{QdƕnY\Cv*sϗKϸyxodڬ&#[- ֹ!ݽ>eL-<;B+sPuO;ec78B;'nnGyZ[a7B< x?cwX}GkVzqM6ywWH)vsϯ!""""""""c9Lvu7lc {^ 1ǜ؋cs+u#m]Yf֩4 fkf'n`5Ʈ@:w46 Avߗ׳gOfYPd09W_>:^(HL+罽D~uzpd[~c]̙bZk/R w0J'*,W;ⷼggyVQKeGuGn2Ɍ,`r;_XxXz/k#|g㽵̹՟/O^;>™m߇ꅶLV/cbƕzV'sz[}7̨p2}Mfj}˴:s<8+m*ݧ}&5/t>#+≮ɌkfŇ?jb<73*]\zz'QϡXg2qUn8,W_|{>7 ?=um-䳿jHo8d}"[[GXJ񄏰U\}EDDDDDDDƫLŋ8h64 \W %65˚;aE=NE!&ŭf8 y'vghs'Ys%rvWڛ_llLf#Ϝmbp M, Lr`xz^7kaX|`(6uA6eN^nS I/\~˚r*M * Avv;q؏epOO9a*kpS0SOk^L߈lgm__\gص{>r1%7S=1»[vG=Sups<23}tDjr/ꋎ3{q>he͚Bv7kewP 8 8m{Gy*O#b+ [uxuoPc̜j,\*\'p#Mܿ2!emČ^P&GN5ΕMæ-7QϽY7ٹʍV7ahcGLf,׻| g6eU/$h5#DNC&=ﷳ%HYe=+/ E|l5~&ύw_9c/nqs֛5g5[xxnLN]}{YݭmpخkV8t'ɿ 8H_d3rT֖elZ[ejp;{Zugqpk깱{qs_h~?e@q԰rjp 8[aT/u5=:ɉ'v6sXUz>"e').rMK87j²b?e.\t=Nt~PUඛ=kO,%:+70Âop#>.Z?L'\5/o8^sʩe;P DΔq~s }ezX')+N>9ڝ0ys<}$csnKK;RQ‹Aʮ,"""""""2ɟ(wzt x_|F< >a" ɻRfgJS0+?k|Gz|7ױjt+sR|NQN9S\Xa"ue"M3 !<Bܬ|z?YK`wZX錥[\z>^7LJP6~#) &\=$ig7fǞFY*SHi`TLMy$ ofk~^\*ahT;v'>?_DDDDDDD6`g-X3Kќp (L |Yj`i_6 8h` =c4fʁZNt}k;u&RM5k6gk5xV xQ#[oaDp079{|oða{*{`4z;v?{> vS@_fi8ܷ9ۘf}?;<]b\_;5:V_h͌_)sZX}`lÄO[XO4#sg&嚁ٿ"-q\[C<%KdN_x_Ô]݈ǖ> m+)ҍ{bV̵ŮM/uXACn[[h3 VxHoh5=̠X002;ֿ9 fb>y~}9wRH`e 8SV.EXtIl@DDDDDDDSA;w, $X[,gW;^]?w]#wyveiޏm\b=Z̚z;/~V?lW>Ȝ7L Z2;v^~ǧ%Kz+^Vms_lk׼r4@Y)U%U.*f(_뛅^l}q.C UKGxq{4ѽ %~~8-gcW긽68KX\zcKIqh=o(9f]R]wCX jپ`v>SNa,,Zg=_G[6gk ;`J{~V/߇'--`sp7x}EkCiX:^Gv.F,t1w&L3mf =݀w'R]rda˖=6ʞ}{°9QeMC f]rI;-qAe]=k?lfʶXЮk^vle{𐉉)S\sg ko.e׻X~Num1xv?N+#7 VpϢͣ?tl#v]""""""""秲mFW4MzNG8dN{%"c;sr*qO=n^hWFDDDDDDDdɇ'Cƍ;h+&"X9PQl""""""""3YDƿ/4-Lb7{}m\'5(ȅ'{0ׄ+=Tn7DNX6\cDDDDDDDDJ@3Ed /v5FG|hBDDDDDDDdlSdKd+k 91,k0ßF""""""""""2Vdg &ϟ暈Xu2L!]]Ѯȸ"#Gd2s Ѯȸ"#'kȸV @Ykk"""""""2$>o+%R:1KYW2" 3gjlRmŴDJÏB)ͅm7""""""""#'d\<bmuelc+mTV:Ni%EF"#4ocKJ!"""""";`mu8ȹJsX9_**̩r|m7}[j6(""""""WU{n!Ew4)dؘp2z,r0^_7bŽ޿kg# B ԝ{N1άuC_] 7 V|TlTL:8z> sX 6线DDDDDDDJo}>\ ͦi$3-,xxT ƞ@ 6kngRgz*!jZ>bڹՑX~ŴF;x7&bĽ/nOO.|be=ٱ-I)9r~S̙;v۱3g"9 TDDDDDDD}īa6kXpg#injѽe^2`2xV lj7s<,MML8x'نeKeχn͖>0ϯ牥fU: _cBe6>`Ov/~VK

2X7gHƿѨpñ!DŽژaq/J%Fvs39\Utr [+vn_gnsFz.wcc+/RC :oZw_2}^Sr{eo8Ǜ~ȏK/BNDN/&|׃%3{h."8'ԡQ6!^ څQR&vs7s5kG^GIdERrUI{o uB7 v;_ N&0RG=-~?'wyYE6H.GkÛtVDZ`>8nJFxcRsA$T 'l :W%N߰C_Ƞ9IRΞ"yr^W[KTe`v#v9(ysUvߠ%fqj-n1Zq|$u6 ~>E(_N畽=O\#/#E~=\RyeNɵ̃Oq ^uӕ)]4y~UU )SjipP/r ^Q|1% Cg+vUd$Z9/gDLTd|0U0DD6[^ٽvpW69L\FՇ?OI)QsAj$ 1]49"o~L˰xb=?J͙)2Y#qe9i$'|)<E:ŇtN3y?i%)1=f\LOȻH>oh!ѕO8o;fgط~}}1c}-w/ue"^|=,W/e(wR|w2uo?Uq5UQgOg|u00_2Wam e%ydٸIAS&@iHÇX/O1`YlVk׹vnIÇ?cp'|\qܨ۟^n|i,1qTI$ד'!>F!ߌLرff`&윧 fKX{m4GqmshIV=CLd?,D/It(OGm%"UHJ/̦1q 5d04?M|KTU9sUK%Stu8_G~j8*!XlVr* n.[°ÁՑ BX\QWHXyIHZpx[r!J&r%_#! I6Dw˵{vqL9M4>VjM},24O} 1\þ}+z_Te3_}g==۽-j7[m8 I7Ba(WqSJSDZ[k(cT A ;ծ;kT{OUT1HO3Nۚo__nR_xdwe#y& 7_A\޹1?t` 8e=2mtcu]e<Vm)[O1^8 NpO,eLm%kzIm5͛EAVI gGbWym7e\D9N5ޅUŲp/uGfUg#D:_T&pH| 4 y2 qIu P1jxVjjB0M>[F@uc3F7q&R֖e=lYІNoΛ:_ %@zɹM)37DxK쌔=j|Y9{+wKY:A_@H?y~ K),(Nu(%`TzE=5|6y>QUgýbrhd}W4#0O5FO;'v/O;>?g>.,s?rT6K柜{;qIU^%Vo\`S}A^Nhh[n89>w{xd:_϶ڏG>S8KԇpS%`x3E .>՗زcXQ N}9 %HPiyg-6H9$g=fÍ$x+뽜5e1Ry q e3~=~=^dm+/+>4ê.g^UhPeq䵍j2(`2NE8:|snxM''tZ|Eq+p&&ɿw!A-.ʾZvUK (9\OIPa_~3Qz'@4tJ)rˤ08=y"/V)bQӧW}N=~wJٲ'_֋R73x_Ћ}}OT4c d쉍D^N&/{ ?$g3ma*ۻ/W)zx*}'|#xO@7-`ɟ~Ǽ<{'X_@H sc5Z8oEvg~e))c@d],uT!O]3R]H? j\_$m~)~"S?@K۷C<3P!{uOՏַCuCqyw#_ W=$Oxg&b?$/#_x7!r;w4_x o(Hv_~?0n p㋥6\ҋ.诋9~:q'?q#)n@&vN8Tbg[_쏜u\_pP׋Z1r!{W:>حuu}?$8Z3"y{IflNIO?t A妀x<.[S #~ǛiƉ;toʤt1&exnyiϝU۸V*%Y/Swyxgn)>0uӟgVIn[)M^=ïlD $V[O/HaQ5Qmw'ݒLa/v?ƏTZxe(n d0@'sKޒm+zkCCrQ;cv(~_u;dɳCvvΕ(=9ŭ~_ruq+йx%Jb.wÐ ?W@[)1"{_8ħ/ܾ 3(\0W°Lm\ l %zWHH]&)o&{vK"v)d 4~O~rs(̔"SӾ4p8^ ‰qȚ9+  7Y*bTCvSA*;E哥Zg~= q qXyG?vlb {R,慑ūQO,# ҶL(/k1\%SO_,o4fc/8_в< 'RML-iyd;\%:WH-X,H܆vI unWa;:,J|= 9nZ8{h7ƪ㵭ϓ^o9Yu sIr |azl;ʳ/=z0<.TI鉯cT~XدAc^DbJ>C0 s{}ɯ;:B~V/Mw>{C%G'Yg˰?~sI%u<|-?WL ױ7ʆZ]'SF. t&s]<,?JC۵+ޫ/~!h[WS~7&otWʻ{lrsG[/q,Ce+8a]ys[b.za"Ѽ7$ 1ȑ&]2iI uiAZ-D}QVe":: aQ=i9KMCQTĊ]C1ScOi-E#;bƙisƞ3G|mְ7ZSOp0cCKS>n2gSlǓsڦ=77UωA\5:6iRd# F68(IPO]>i13K19Nο5%4-e1W:\ Z;kx ި3 X{{$Jv(CI.n: x>@Wb3B{=7:+a-<HLJ|ǘ-+S=V6$ժ"t)r-ZYS#2hՒzyJEj8yP}/KYU˄йP›U*z- X~/^߰{M*jd Gß:ah@5&j8k-V1#TĔy +YZCxDbE\I| NŐ sgO+%7_w]dγ.?I".;w7:;yދ|s_pʆ-|G_ٔ1^ey ,Ԍʊ 켌5^Xc/.ŸzmR|\3x >)nf-V\?_fᓋ.׉=33u31 ]gL~!rzpʟO||k/w?J\UJ7{S5؎%{˵)G/mY'C+6@ :}ch+toNuiH*m؛[*(бZIXbw.6쒛ܽ *JZ|봃F-sf=kUFa~>*I#"fFlŽ;ܣI$3TU(䈺}/XF~G2:Z?eGA0xc_e\q_W\SΒ`(|ן,h,-Cme }/{#*GZdn칦gE_]oo>}!N"䱹]? p5B<8Y6XFEUNkeqg+o:c,wbkۚRkQ-UM |<hn\uu|'מue'_WnM`t~8HyWb^Wl!VUf3Ěo'on0r+_w_c\ [zBƎ_Λb;?/شq ԁa2]hoţ%thC &[.^ed^+UKf39.щ\{d섹6z :O]}_ PI.9[Α!L :F?bW _һ)22pn~~qFxKP ׏Mj|8#|~^<*00ԓexr.j~ *q̉ΠnNY'sn71En#>ި>1Ng{zmF|bHmkb;|~mj9=BCƖ] yܸupZ ?1bKd,&dŽoN~Iuв~[G`k{i\Ak\CŌudV4^ciN؆ʜ, q?ދ<<:0_U|PvXs8Pryy6zn}Ia!9">r4mU⡣$PQRTΩnHBp}I~Os{?^Փ(BX1@ ʧ{6 lI9q"9+)FڕK%&ޙοGk5+p. HiL-elPSK 8KJRU0FI V_jB9FZ'h2H|$rfaH9p o@q=a"@p5td]AtϬ܌cqmSj&\Ö&jEA)P>k- Sg~']u[`5bh_yG>?sې)c;Jےq2|4l.]cIK誕k"|X@n !DU-o?<3JKKl656,aE9I/godǂt81#Wi,?7+V&z?oziWk-ZϪ;W92y*dK_S%OdʿbV[rQ_WP1k1^]۽/[/nh=O4凷qŻ]t/+/t_?~ӎ}.Ś7x W ue~/T#6Η79+۫WƍhGS*unv]+q*..zؗo|3S1kJAǚ+A{QchRX[Dc~6:JTHkC܇ ' M&=rp1z&pOΘr(ܮtY>Z=,  1uyG/:I O (?z^CŰ$r0󍭯m$W3kGE]YT -i&Ñ9f>5~]yfoʯ :dp#{y8!_HOhdœ/&֩`ԫG%cn<{v+F'O<갱Vͳ `qd6:f" &C 2+yI,]J:B'ڑ3?Jw_N%R+;!i5W% ew;3]WSKdF~\z3̐\tܴg }$6Xwbd{eػ?Qğ|i<}oՃ;%/%_HN%/EFNc츳W=NU¯~(o_|E{GJ>.u={=)S\!UW{]`Z`,*aTƋy`ri~oLXXxgDäW تpSz=ޫؐדy:=+-M7x/s/*g*x>TPUҊtC`57zlֽ< 3YZ2&]1oGbm>yY0ez'$ o0Ƃ5o*62'~&8O\Uma3g?:?t*"CI~z1q76L辕,[]Z*FZ-r%C*K9tָ\}RS.9^s1GqIzTKekv?S)#^r|c.'o{ukCZU)Ǚd\Ϳʭ'^%9 #_x |d(d4tt֦ T1 0Xl\1,SG6@j/01}tv>#< k}Ͻ{'YϗO,=[ 9/sKgXʿ^}_{,n?Kwc]se+n"ǕlyQpŗv>mbe+G޾p(ML~R%GȧN\|Xm|CwC5`/gvHfDfmݷqlMMjc܇8:xs;DDvY?3.9;\0gFFH9}7xrCa 1fոYk;lX8\3Ϻ'.tX a w1یcoǧ G5a*Ӽ$TgM>AwT__ i' q[,>/>SaS%+YobIT<ƛXW,?v]6C1:pK >6łF㰥@%Y-ߩ@F;Pg+!J\BGb%F*S @I,n$a46]t|! gv"Op]q#.m7On8=<~KZ]I LBq٣|*0ƙbeq'EeBzrb*-m3n}R?I=~_6iRr&׬qxp6~aL]3Bd O$݉LH:T 2bsKc֑a FRd~y%?NK\u4w~7 5?~:]yg,}~(_.w+ye Ӹ|K/RS/ʥhc9HD Im9}za &nUoALqzXt %^HSFJ3˃]肴.µKڒ%ě_S#e̯Ԩ5[b%͛Kdx[:((6Nj~#]T4DYkNBΤZjJw?8YzU'-}dhq+[܈:%G+*s͋!K,)iFP IL(zBe@HJ^`P5+իmΤ8l(yɩ]èNʱ -Qg~;P7JCsO{ |W1弡o_vP\+RGz6P_%C{Iہ|E\$gT&o6%,mrYlw;]ÏtrK{\3&u3"~,擐p(7Z-5i-6ȁ Z걮~sTGo9Hm'jLhbFɍVoTk!tgloşo8߻bؗn>OOU[hfE*^v1Hf%6e`sdY% :ezE#:`/ѩѠ,^7|81b ^:u:oʓaY x'֚&.UҹoH-gW .:Uo L6 Պ`-Lӹ^Giq)o$r|IbcN"MOkr= 9GoPDbZ7ì2J% f1*h׃RLN4iYBm[ҫ1=b`NJ>AOJN$C } VsFƲE]9=`?d}5gi/1jQxp Q0uSE ij!.ج!UMV˯T!cWSpβj@0#O +>S2s=o[W}Sozpᾌ`m/Î~mu|&Ek1 om̫U[l."AaIM1Ryh&Y\ASa貰 |>\O.G{9C*>/J-VEyGrpN%'akE|'!E{o~=sx\Ȱ|?mM.j業E2+mrQm!G^<^:^PKu-##BO $RtI[WTJU&Zt#q|#D[׭se}xm ZwQo.䉝''{ eOH0=6[V2B+r0Kq)z*ߞȹrܔ; ,u(Z9Rk:̏Ngэkso8Ƨ|m$Wn΄C kX? "}TR c9+R:(P>D,',Ζ?`5]ST@IDAT C6[B8C2u g-3s~̰ 6XEX:lgR`7Pn~EEo (nW~ɶf;<]b+CIY.8/>@?붬Y0"G gQyzvga@ܔcGh$+T><|{ꌻ7&7_̧w~굔/uEseOXM#DWɘq|hJgk<|#1|mQ9voyrmT MCPH?N/QSfGU;쬽^,SPM2:|@< kp9'1SOpPiGm"e039H/kBFX%5;:,%ʗ&^Q4q5?uUa ep/[\Zc'~ (8SIIքh2Ԫ$`sSc'IkcQ8d$eds=`9Vc HZ$]L0꣼,>X@m~#h&D\I_ Qe=m3a^z1iQK2Co1|~x=ȭdϮXh=W|:G_gv\&?ʘ6a=1v:ԝ8\zq'$//kdŒ~b}Guw( ߅byU{S~͊hW;m"b¥DW PEz j x ťI2稱N`UD\O@4krҒ2yG<_LÍdҼSs_^$U 8/߂ud?9@ >Qy8۶F`@c!W(rvXFV0NkO`OLm6s☐Xg|O kZGk<)zTà(瀭tBM5è؜o͓S]9J|l]~>?w|^;c9w[ 7wOŸΊg5Ձ}ji1~Bt<2/9EU藋;Ԥ tZ+qV>GшĿ3Ms:s|7?FmZH}tyk?EO/x{F <'W+Su^aGƓ@S@^~Ag`M~nS:M)/yݻgߣ5Iy<2S_A `ĩx.//Zy_=o(}׮\ys 9/OK^Yum~ٴ: [>/*/Ե'BSIOu+*RiK ϋ2/7"S0x>|Y`~ms]+*^*j%ݽ93l ^;uӏ=LfN:WŏE<ӆ9wLyUis1$1p\3^f!@Gu5Pq'3z]7೐<iX XqT»P~QΣPkw>I>֧ x.GT yd.`˅)蟫/h!_ᚥ">O bi2"dCKWGOXg<ӯyTLzZxgj o>\p}y}igFጟ%M|#'a>׸t4=?ϵ@;/ү' X>XHBH*҅&x9d3+rav62zSjO < p2迉1SKo 䭰W񅚹͓6{Mߨ˴yFP/~Jnѭy|N[NF?_T//Zy_=oE)@+[/7 <ۀ-o֟䌾L׍%>y,42[ך6?T~N<0|Ry?xg 0hzR@kT=>sG8\oOz`מ'zq v}C9V?_Z|a" m9tϓe=Czx=@`g4zT?@t/h? 8X 6mOAE?yͬ 2~0¶YS䔿 F@@tay M|B&!L‡wqxYY#?kq\NT֔w}w͇pkJZG ~r?٧FoEU?䵎W=mPMs%/wu%V[ŏ$u9.t e)|%`OxB#v'az F/Ki_/5|Cﺶ%W.3>LWo~\Uo06Z0:5%I0"$|A-18_h%t~5MK_1t"uc+@\c >.c=S~ePLi') nW:THg5~mӤCH]ȱlCgsoEN7)EG|' =]^]or\%U|ŚR1O,f5t2Ev[X{E{’?:sz񩣄XNkyB[+Z:.LL5E_Id}}.|ثDD<+~; j7Խ!/W8/U?Uj9D+65Au'OQ|gO@l&|[C;üx?6nM?)?v}GI$k{ߠZEnVe6Ͽר=ms`oeo3NjGlʼv)ruzk]MP"} L(/\o^1}IXޝǑygp+'uYϗ>_-7ׯ7BU?GjA 独;1QWlrģD˛y~\G)o~\Yݐ)lmFW1h{x?h}IGOPvq/'2%5t>:yIt"";!C]NKad,4o&7zgP$ids˃,HkZBm8lsjc\+NbL$igękGI569*1|e|8!-mZ]eE264HS'64 )PۄO8<̌Y6 X*o]z1=T@q+aNBID*Y6A͑oGLmnǗ|82ah_Un/6Z1cplebH>ˀKf<~Gg?/\3$^r#Yg{zwrtW/`*\LK<A_nA.U٤ &qS )U5џDeƌm0Kgn /s>Sf='LD5&+o_yŶ@EK_ul1Ӛ;z-ΕsN@o̘uEِ 6 N1. >r5F1XFXD|`0=s5qURA`eo0SQJ*|#|]l)7?QxՖBDa.l4hRp}3j_h 䋅|9IL[A[K_\e 1Gw\4CCݰխfUxܫOVU#sw!]29گϨ nFZ^ʳtg.~Y_Gf7wb \hG^].9ߔZZ*+5,Gn8 >E,kJ#!NJml$*DN,V42=tn*+ku`'Ÿ!hC]wG^0>oEIoyDAgi>JWc44dTP5+@;$ M|57jRXn,{lC0RR)1MD槤4g Ϊnƒ>-v}S) b7Ιv>{=\QB/QQX[[$t$`^A\nj\t9|db`&9 WG1=Ǽ+ en|0HM=@)8/$8lc<<\&Т.G{~:S2?:;Yg1wO/0p;6藾?{;ӔغKMgbɈD)/S5O!GkT٥'@g9T`xN^8&nZ sN{:Mw-(e/W~(Fm [_ꍬ"湄}&)f.~wxm:OЈշk@NƕLB6\ Lܘ6ffΚېG8hF@>,FIJccû" a'[rd} {='w̓U% R;o8>2Ig|<2w#eT,Lმbi.P{َ1UZ %/]̓_I": R)g^?/Ԝ[eXicW3g;]x"C{' ͸_H,H:hY`@)o.XP$<:J+z`I'O :v ׎_?{mp}?/7 y79Du=<dz|A7_p;|J|/y)ChOכy>!W_To|8TwGSu]jct"EBQ XH c˗\"|X3-Zkp}KEwTgYeHMK0[=3zx<5~߷}#/n ߔ .N?gLw#'W_n#9l!Aø{@,O׀#e |gy#%ݨ_RJɈa}90Q%c^B? 7t+xYXo4smW 0l&o1NؕDNN}0^*6s8`e:K#`w,9 d+ GZ`!^ն9c0x\<6KqJqgA:aCs΅k-ȣX;JTH1@GAW]y Og}\s;m"_'=9T,9cD9^:c\R5ì6_ҥ0Gg{?:M\s;c̵k{]8N%mIŦoYt5wkg1#]xi*-Lm&$}3PxÜNTꔉcuJA7,̒@Fa%!g9弫%n6$ !뤟%Q(-ƃuĔQ p%ã4?;zf>4/>஦~?8>xa.-@s1]O,c$!͡-׏-k}p踱Hk]@%16m<%.u8KuCpW} ~ؘsN>HgSjo|sSyLr&~RC8<7ppûv yg߱'d΃w _:[q4N8Ӓ0f GgrRJ 늫g~8T$s {[eg.9GhY6f*|l9ES|Oͷz|Q!x?cXV!n؏|\o eW_K'T6nw~"^1"|`"/ʆgg DITi_DrرȺsh&i'75 @oF9GH3) ,UB|kVm~)^ڠkޱs@3˙3,~LHţi'A,h.x>Q=GhbˉWu)o[W~tys=q:'e~ hø_ּ(r]c4yY[h;+uG'I径[<}R:~C%o_}MS ٗU]_NLlpqlO6QfnG^8ƞ){_TQB[0؉Mْ2"C/YeW>wg0omshN3E8 Shk~ID nc{.}E}7ug)cƕ "q*Cx:^Gx0>̴u g<# i@͵[ W3Ch)%ڇ/J5,7~=Mߒ_z>~}e<uQ2!;z>k@Q`F c-:6_-|W*_Q|ز-WuBQ[8(g{xy7_^gnbq L,ɸ< xJLI|Jw{O\C6]3ҭq4џeH"hj5Pxd8R:TGڳ.Ǿa.-Gwoď+5u:~~|3 )M=~;Oot:n3FJg|o<\Mʹ&٫wFk>op?W>;Yײ5iNf4Û-Ȟ=:79E+I k-D0D'e4SGX?Ϗwm~#.oW  xEI gw$sg&mQU8t}hkkTSY/ zn+ݗ^mY\KQ])Il#&'ăbj}PhV#04BQ|M% s36SIUjg=W=w;~5 (3oƍ9 K]oX(\8%_|n|(ߟb  *mݯ!GYbX´19{RӬs8x(̀yhʾu}-Q79-6w=ZeD(~]x7p>OY?,eLRO3 Y2sq='6/ k7m\'N!l;\:s#Vnx|Tv> {ḩ&Okx5s`x#FKcvL̾ N6hGqE1~y߯0y1b{wa6}tc,i"kd^]2KK^DD7pA:e1]Vqip=&߅2?WG\ԫoŃ}V<>ջ_ԍחgng4*dtaz3A#bfħ(4<',; /ϙM"F|#OP{ݰʗ\;c];*SNyjɹC֍5r(S Q2ƜS'uY=fľ9[D*l0O(8bnUF[Ec4<2<j%hsi{V/LuJ֬|,kYĆ?{>FOCWz暮'>UǞ+xt2@ikm+푓gXgrvRiLys>B95jGzg~~6uwO2~Ww.{mh_brreQ}ƹF)ç<Z/œxW Od0;meJ䖓:hA|a=?S0SF% <.-..m_Z y>0?,?(fY'8WͼJ윯qş `ރ)|hܤ|d"Ik ۹sT6zǡb2#lm[Yƿ["G*e!7"Rrr mo㗖2m`-my>#Glq{#>x sοo8WƮ3kt۪q^91;!š; jD*^;y_?xE?%>\]-:3{'x;}M5Jl^+Zr/9tGh;Nqu5mkS&{\w&XGխnsWدqn,1Q$51mefS8Ic"γTA0(Ѻո[z,`pU++.'+ G` ' R2jLacҕ/Yb9pz_4(ۺ=3sfRh9|vѣq&d9MVkT梌=%vn{#g{'ܭ}x=eO|ɵRB~WvWð6ƷytQ;|'kVOԙAfP8Q α"V0Tx6ΥU<>`$6#/鈉178F7_s³Ԭ{+vfY? V?[1gy.H;Ɲggu^) wݻt'[=.|k?w؛bLK>=±iJf5֠!ϥ6~V|YȆ(vL-wƶ˵JRc΃'uNEP|w&3~K|X_a9ge\n+ULq0bm7eg❰ceX._/{O;_W\^]x1z-}=f ь3˟=by~Z߃녀L=so.X_.Tm֯ _g:il;#XTV*#jYg./6u>ġ׃NnՂ>0ks*&3"n=^C-Q=Ǵ<BBWAݬ-Z4쌹@׬I#r2SC 9U"twCN:vU^th>g*Gi6wqGO y&ڐc_EI,9́؟WjW5*gOt{I& XʛkTǁX0ŋ z~%agnf5i}fk˯M\۟X=[Ο'vsjjv@8&[} ;ŀ|'@5=@sq΢Q187at̟< RGj 0ÅuM§n[tuA ˏ^zᧁdg>Ώ7f &'ՙϜ`"g}YK>Y?&'ՙϜoH ッ/^69TS7iKKwi#iyG= x"}Qܹ\21P#3(Rj bkIq?NcGH_r5^B|xՋ[^CN,umxO9zLm-I-{ϒX Cf uۈa=r@IDAT~H+iQ|U o pFfO?8fO/CQZXar >Ooqk_N Uބa>IUdc.wfv=vr'kc/@q̱d2 Ei2CtNd_S]k5R|Ο4 fUGv<J ø_t>tIL J,ek5%ѹ=|x2cqρ{7.YDj^o9SdΉ璝Us`)XWi6WT! x\6}g_<_xiZ?Oo-}G'^/lTJʩT47]7 ?zcOP$y/#h{|vpaxYcI-&1{kWr^O7Yw%gY?LTǓ9q>z}>~ړ3ɘO5|y5A:/aIwcghGg36f-$mP\ JSt;*WO[Q4⓳|dlS]IHILe=Mq&Ww p|ތwhjK">1~`ysA-k֨WW+#r2 +4۝,ٍnPV`'|]凿a55nVنl8WM]G|hr1<jVؘcm'ƶt|O].[:3edv6mL{016/|GRw2,}OWb܋A -걹8W-<~0;go3vַb~sϯ^5PNZ,Bi/2\MpG ScȴU_ː|[>?k??O_D,_;b(܂9[1'7?,_+}sm^G矢v{⼵ Wk^{8(&:?#pfͮ#:;$Њ"08dB640Э@ &G^3Ʉ! ҕӱ5\kq},eXwF0i8xw1ixBa2F iS&j2t/ Nuv\U~vȣ]%g~#7 rYgEhz0ѷBg;F7~4?Up=FF73%cT@Rzg KE(+ ` &N3^hYPڟ\[>ݞ<42w9cj>/?w=196N\̂1}<lu݂&׋nL@Nȝ'8@腃u8O/<9w#vsilq]xcع_}[?[z'[/W+ӹi~u8G㍹t(13E0Ej KX2q:,ujҌQznr6BE1=2chðƯyF֨~>sXO{ySc#.cS``a{.kdEnYSggr| 'ѽ 5\fyz {ip On<.g}^c @c5h>+-1C!7ˠqG=< x 0oI\&?pq %?sg׋3[|q!ƥʽ]__Q҉0'ՙ8 g~bu;N?q)8LGBbӏ:u?w~AgAGfܻw?{@[t(. lI3A;y0e8]!ŭkH0umMӮn%ҸamF3OHV$. ۽5x%|:Au3q>_\ƬsM#+3Ïz/ސ>%ߢV<޺Fm|-:|F~O+g{">x7Q+ '!V oC?o[tgCNgrXiɽoY?ϙz qC=UGs~sK,)|\{6o4;O{Ls<** Wy2Cj8F_ڟxE9 rY)^x􉣎A PDFl ߏϵs~A.X#14#?i|IJ͈抨q~FDYP"^#!g~ _ x_ 'oy$|:[7õl೙ٹPğy<օw9_e;gOov1Wmzzqq|HwEv 8o~'Lb3Bqzwˎ;}}%3q>Jsn<(߃\|7>;d 2mnެ^mvT 6|?U^mx^YL$P7T%B/M5]6~AA!Q>vßwz౮1#V Kad)d%aC\//,O9jQ0Hh Yw:sjQ3Q:K_q1W]MLc|Q[-|d_A??Oi[eh<;n<0& ]!:'[s+CY1WV-_^16OI@.W2聖Yy{ D+2nwH!`L']YP4 G_ +'/jͧ'`4$zu^ϓ-Mo=TIw-u5kt%-7J凣58*4~=}'{Tr><,'5?-AʧdZM5d-&&;:OT74i/Ӻ;Ӡ?noO<>qVvR};ۃeqMeQ [S۳KLdTE羥RrtWIc2 _w=dqgc_sK;ο/cIq|ɱ9,;8cz|{<`9I^7y|}ElzoǾtH|0_ͰzkoæT9Ӯ5."f_'޺69ӵ_9/ &9?]:epc>=`q{fŦAnG4LMxz\;!Jp _|ٻ e\4UY( *EoO ߮Ux>Ϙ5YXxG猟g}S߇>~}j~wǘXeu\V~ Ɵ"$E~׳fcUp8a}$&9V GNhbD@3Y3o`h Ϸh|ד򚂊+#eK0ĨbéRxy\N|#k K%bM>p$1a:GcVt]CIfRc y1IT|* zP/~pkI2+bj<8[Ec[pP[kgw<0tg,Ǔm}0:_dT m\< \k,X˸8f.>a@TWx'΁eT1~7Øk<O2G ck=nA|7c} #q1o1>G)Q#༦駋JzeKN{'Q3$xuMqAb,>k,ėNB:DJصXC uc.CbEz7#-V_8I*~b+LypM 5s[nMȒ P8,\oIU .xq$Lƈ|,DQN{+hcK1Q9yU{t.L`ԫ.O|f5Y'vy~P?ίf F,z+| T0G0d |l;-ά1e5Pr[]|ΣygF=95 4Sc|aQ%"`?p 6E:KO.ZD@<hcԙ7/Gv[?W߈y}>>%_gs}}>[Yvb d,u~eL/&Ig' \iN5d9w=A z3|C1>GWWy#-~xjR+~~A(?V|Bgks6|f'ο ~?tWY8H'/2+IPW>C|I  Wk(YV>̓UODT1_¯# p'0SC  Ki=A`"UF j:c^**>_ďa~d乃]nK7ŕ: {'xc ~*| 89K:su;ǛGsL>|>]+6~76n͍;Ylk#'8r":ŋ̶a`Kuo.J4,`xpdIb&ƍc|1^ Ex"+J=Zk\3~;iGWΆ0Sη_Ńh6I3q󱚟|V3,;|Issn.mm{ {+7 ~L9tFBc` " qS|Q1o2sr*VƶR:泣TyXՎ1AiW̘7a!eFmLEl=ߘǂݗD@e,s-|wSxU΁]/ Uj5/:2%\%".QQNp =<_iO4=?g(%gY=klHn9ǎd?oIFzt3Όry_Pw_w_j c3c,>^Bq*9qŗĘ|~B򉡯|L둞 >;bkŠux5f9p"QF≂c1>0N֦R96t}w|J"|~xz||a#|}|6I3>v-2f g_?zv?<мnJHX: ֣-\sΊ icgy<nVcsȌ<2 I8[xs-$ߛ@Hg!GE ~]t&aL+gў g*=)-3"h7[zπ$0-T I?PFHO!T}tǼ@x.[!8(Y֑! Q%9ѓzTdBFHgpn'3~kU= s/_"qz=/yXgc^9D&+K;/B%:sluF̆E;ܸG8rk;y5nd\6CzAD=ծ&>`?F+<5>;H~!15WJ/38AQGH3"|0g1m\O 9gKjkxo܁Gݡ ocnx,?o89XG<>>ݱ?qVw>92. k.ǟ[n\xՅO@M~4lκW?mԛ1/nd?1X̯s+@]I=Z5)5*xk5|9, - '}ew8K\YOl::@9HVC1?;#woSNnSԐY  >%a]Q?½_/sv~q<"|OOķEmcC]{/~(w|0İ-/ۧ>H@3wDt[tw}iΝQYx2[ňJ4E*6f>>k&0|17$fYoYbqZs?[zks}/Z6Fo4Nk,oGQ xz0Q%䜏VM[ԁ:|;E_ g+׮es=*ƒ#8:%&}Ճş@r-rG`*(5'I&褠0%̪)Qb9ўlM΀G e^##}2H+Jb9'Ђ,huxHhCd+ C㈋bBRU+PK[ʟXX#Р k}|t⒦:!{M3P[}ylߊg;$cͿOwe3~G^yo腀x;@i]7tn[Xc&1BbAzi̯nR02$>$ ݯH.\IjwO`:YDG{vW -m?/iYw7ƝoY\[s~C,OպX\Tq;ʋ)SHebn5c~u+HtPCv'J=ǘS G:c,!fG=k=-FBמWDuw~4xhV&Tl-'M-LnLq1/HI.3e#t/ a^QFz&BntPwABwQyExƪq#Ӱ AUƋj/|ti0(`nKnZxeGv(Ve W:QG0q΢N654Z)Ogo.>o[K}Ǖ_IJ9jaDo:*/M9̗AOX39$X\< 0qӽ@Sez>RӬ2ar;r_s(dc2aK|[})ߋrm@yz.pS'N/ yN H080p>eU C9bȓ 6'D9fA=z,ojG]`c@)KxIGn!U琨3`NSb? IbmLJF}/& cpύǞM>A4nT[„:7n҅5Ёsi7\q8Ke3:t06>m/)c‭1͒ KE돾Y{^t?rq8ypmq+׵<.W㛷hӪqן,G !1#pU8b*nVxI3k۞ '/;}~q{W)C.ۘjW@Y8q&gW7ycMmzl;*O\:qyĦ>{=}!ٵPT^&P-+2"\QGIb@Q [WغXGDD/uz3hE${^Q=osGkőD13v>i#ѥ Q`81m V_]6T}k < Ixaho@V2n ~ muN| W/.?Vߎؙ3N8UaF rƎT:Zs$uL{I~,hDMQ 6ڜt!69}ɛ.f_PYaL!+8_:G(ȍKFSءvíxaoG :~ہW\#6<,s`8v>?Q'Pbmy)8Be~`)xaq銚Xz˺Sj\L^ưˆ:noge3vtdd)iERt[7%ΏX8n 3ZOP t~g~f{U|`9xɔU`\."Gu1j)&9XOPq浲8ʗ'6*A烲[IBEW8yȚ&w~( Χ<Lz섞9b%*=GDL6ţÖ́@䱬ALN CnmOY8Ue~ʬ@0;H'NzbE骃spo.x}ru3mȣ2|?~ ేEtoIp<&o։WŔ\O~w0&6D_]$@+Yo{>7| <jDHkyNl]w0|~O"ϒ4yaz.Py yo|kwK?}q%>]Ħ{!,Cݐ>A܊.$; }xF[J:"$R`$M iC*,5kՒ(2j%c\ c#>ġ8LZ.bы.;nG{\̹o[lηӷ8Nb;Y~w\~o}Z+%'&? 曥3'iN^|xέb0JTʐtz|.2VHwHT%lgFbyY ^+^wo@O.)H֑lB|'9Vdu !ժ]hb8ҡGMs<*}Y<󑯷s]7,V=4rmǬyѾ;q޲Y* _pr$OM^aoCBndy+`C8ΧtCaW~Yn䓫?CÞ$4ӯBCa5v}*x/d<8T3 ڎ:G3i]k~ɒ- ,CCm"r>O$ "jwn+7Q}~#_ώR_@AlXSgD1,;G&? 8I|OfOb6Jɰs>M'|̭/ywsZ3e~`|S6y6 iCS ƁOyӚ|ǝv]OU3t(zrԧlS g}o99_Oܯ<>LA^t%qg1wnF܋Q^tly#znޘ#c^ !(SS"Y;0k}hC=5h2<+ `MK|-Kǯ\Xtu[|~_Qq| k??3?ç"˩uQ?F>2oͽbxP1UOڡkliGbh¤?Æn_GAً:<댛BhL;)8In{mCx9LzcGu$yZ0jM8Iwf)0˧$EZce-#(%镏NAo ނty!pO|6Vr|goT;_aފ$sZ쾥mxMAo єfwxsP[PMb4rpE{TjW>vyA1)<'//3G vՎ@IDAT*;9C1Py5 e_i;S^ֱSK3 p*# lV_j181KtOuue/  =Yw3Wn+%aO*gTؓ;ۗ~Ã7ax+7[xf1`#*P=6l6 Yf= ..|5"Xeʙ"=2J] [z2hP 1]$ٕK!Z.8p=?ߐU x:ۇ!c1K9~۷$1Q:s\q;{>3;?O- x1 ?)Sar)(9KFR)\'vEݯL+k>^%׻u(9K3_}O>-?| qlz+_:ĸc/vIoh:~7IT~20?MҶ mdcP++aẔ =ML rS4LWBy l'$q3NII8\ns!fS'u!v*Vu!<ξ4j&$a[[-Ҭ \[l B}tr>-Y/q5|Bp@~C.Q0 .sFŪ_C ?Gl)=NJ^xma_?:\0z~74^1^X»/+xGXf8Dxcf,W/xr @`@O%NV蜍f:mi0*,lp/y`h+sF|'7> ,D|~.iY8lj/<?|u^~~ۏ3,{OgYISo|'WzkD{Ts]|Vȓv c9K(#~s-m_lpSAŎu؟b [;;1q\FmWi}q3ƾnwŔu{\m;fO:m3G׏wtٍ4>叿tKi C䱯/?\?єV땰) v@ ك)G>hMfUF[qy U4ъzYyj|HzKbcFV#8hCªtlHGw$κ!~+]ycGUW(Nur~.]cAOzy xr,xcPjtQ9Yke25 DG:;G~vh[vF|p(#>#~iq5j=*;s3A^ى: 0FYhqח>乇'%-ٛʠqOIn;{Ttu{9‚ũe%K{06zB&Ɨ~Tzs-%"cw=`o/Yqi|<q?̺qC0T1y+x!{操' }1 ?(X?gTBȲgvG;|084\"> },wM;7JG5`ed|nc2 Qc=Ѓ3I8(Jb xp7OJ/s8¢%[HŢхC.y=vyDODkGvbg# N{c,;~ٺ=S|g88\Squ&8w}nˈGVܮGNy_ [G|Z%:.At`_6еi3Ïؕ݋Ҫ#6<6 6|b ()g pđ,0E%qBO09 JD|'LZ))Hf£Bk.w>ICfƺS ȎSCxOV=y$>vA8\ <̧5^#zadK \\F2_ 1R- ( ۿ5|c<4@p|.$9\1XVJO3 ʓGt(eRgG.?<+G[y\Nvp\r?G)V҉j 4eYS(̧ v?'CAqE 0#^U֜ ?$F^92!Hv jrIz4(">C21w C(;+=7srŞƕ28nGodnׁIYP%cz3 uưOBW戢28b@ojQjΚ 3:u1|1<+q։p5yr4*IKO|Qf eDĐű*QL<9$;lճST&i°psva[sA>>N!S~ #CfY?2ΙOAyd?G9J9IBWj>)?Wߺ&.nP<a~ؖF4wko;`B-\N֝Z68* wju}awuY$]~˶11yca;c K֜&鵮,D$D?t#W9>n7w̕+3g=5yKt G^Ыx{Dqan73NwO~zR8gL咎wϽ5?ė8'>,LsfYC9o ya}6k9g3GR |n8\@cK-q\EnGO۳!./27{7"~麿K>/wE/|Et;.%ߝ_2wz;H>ywX{|,;.?ݽ>x_GoX_G|@v69 rzm<_a\`g!7[(=>I#RDa $!G,:d%A'>R!'1Oj /<.szvv} CKK<^[ĸLxog{oi^Eaȹo+Ja[{f-Hː.=ᾡd,%9g%L Tꡓ|LФ=Z_|&]kfNzUh~H-b:O].OŘxa/Y_P15^^RtxǎmRiU}ԛS8]C0c z;\)VZ% )> ]ϩWŲ$ޞ fIIxy㦧JSß|~'I9 - 5ŕYf<aCj=M1,[w`dݸ娈PLq.F.-׆{Ɯ/(~rMxo'5zDhK!AK//pH bV:vUК I?k]|o4$I 5.ߔ=N8oԽ_M^03? {.ߓVο˷=n|+=;|m'.?t̝Wqp Py(y2:y sǘ9oqO=ΒO?3rthԉeNOfcx&$=4ȡlӘqq)ZճgRXj6% 4ח߅.}E=件ݗ~ڿϿw~xkk/^dA6OZ^{b ]aN$J LȶB۲L6יb"|9 |&pDkfAV;xoAp0(N5b"U' Ш/FY$oĂRCJ.q_P^8b2I[_Djfs,<=֭T.Ӎ|q*^5:ΉTfmX-#Lf-t0W6<4/ğ!MWz>N`ϸ+\{C_P% ٮLx!;9L\ bk1ee)(-˯ QO97lޅ+S~ 3(Ԩ!^0k9wmUn1sd0ikϽ'%By\n27v&<7|v]~Λ|v?7v])<7|v]~Λ|ҳy &WpԓP+tħmx,5vzCg}c|/ [wDLB~hӣQxꅑRq|H-O҆_wgCWL,݅#wr߽q%'΋]ƻ~6笻I(9F=ԟ7ĝw*Sql֖']gνm~Μn9dTpmcҗ)C4>pKʋ@#[ &]ίtȍ9Ѱ*Veor…hakHX޷XRu{cidBѝX#F 5lC`2ju{w&U;*|$na치J8`KxWD2Mgv'21 8K|mq܏5xа:`/t x Bnʧ9{М=hSd2M re@Uz~/M_c/ yb/kBi */z#/֯,n=G6Y|S~ʝO.5Z\?aџ|s PW;^5OCE_'G[m-t6Sv 7&lP]7I~K>.q~Qw5ݏe<S&C}\pm]c'>8-'BT,"@G9+ЂQ\EG4;z F]}y@( Zx#<%?h:|yb p<]tq@\s}vҀÓnTL"+m عKs jh߬E궱$Vѹ J[\jف);f\G]b6bbT! @w(I LWB7J5U_yM(;` ${1+WR*kOnəX SAJ>9/P&،Ü;\+f/oD_)֞o3UU 7&c17ٔ/l~$y9o< )*Uyca<ܐ G0/<MXYAy\y?˒)>i9IAy\K k_(caĥ> h]nyM?L<ɗw;C铗<z>wP w?x}{=ΛŖ$GywϮK};.Oq%ο˗|~U|lpw: 1'?wf?,ӿ.;8CRL?̗l`ijb#V='W]e=Or" r>EQ4 T^RP7/HR_nοw!y pw"_%d֏~{?$Oޏu~֛e~ae idpc͋ 4.G`]G:Ǜ )H(]++/>4?R1+Cy\LN :\Rd1뙀 VبGBճeܣUl]nҌSJʹV\CM/Ex3c ]+>;voӦ~k f7E¬=?~#~fgxZ"zSBAԱ CKu/w-'&8z& _tQ9渚7pG& FnOz//:"O̧۠j5yva' +c>n-2{)AW)F[@(2f"(ѝUFlϽ&/|wr /#D7?|_98#J Zժ#{ш]^Aۺx;M_y(qbA\A'WfEdǁr\ 4>a tOEC{5yS["6Ӭh 7/ҺnrȳtRF㩧|p'?2K )dyeLmb,JR,o,x~UO.Qlo`gSC]ѭ 7wf; ;bh,rN6ck|UQ1G vwN]|?U, j3SUe6겳<:95n+ zCQw*n(Q k~ M.4H qrtCCr~q>tC̱B1#v87qJ xAg/Fӎq d㸃0^O?<;r"{8ݯbNtD-n|U5='n%շW[?{o_|Y`O5f4@4EϞ G#EZwUZW5!FE<]lLqc\lY"GE { ]> [q%V54FMv/20F,4Ser|ʹ1P :Bj_ٜ 'Zra,8 ť)ş>s:Μp.}g_ߣ<KpwϳDMͨ/|$H4Mho6ܮ7ޯ7Hg ??SD_0E&=u*;8 !ѳ|@3p'%mGt>|w'Om}=Nʗ'w_y^uK|Iq1bd`̎mz?8 'u~r'ۍ]ߗ7E&nM7WdwNi(K{XG]דQj$6 'x_$EU.kvA҉{y2d2E缙 Gr(7P(~9D_MĔnxGJȅ9,W(8-/Ǩ*A{F:@д9>ĕZ0 YyN{̲ʤcK,zBU[6J&P:G#ncx3VxOn!&GZysPR#1.i ]?}f>6o?}_!, ĵ1*0$moc[?3 9܄ؼ8 Nn/>c"`;}=++ v \2C[U@deơ_fSg~# Tr >++ْvdkwd>cԛbcԷ!rBPPk͌(}c@QPyZ%sy+K kBDf8*~W>k5`2qK˸=xM~".rR9WlkFIZMz k ;14l9qz5Nfb/h 5lD<,sW er2}}ix_x9Rg,V1"yX{R=9!aHyd\@uHECR4}$u _t Oh"ǥP&-wxa^~~6R~/*w;>w}>ۥ9o L'C|~?8K^2^W^O|'sP꧲2 >t2v" ~Q,mGxGW>ʻ Juq87׼WSyaK~OK u?<9_p8.}]M>}ݾݵ)qxdy/]CWƷ~o3l>{oջÇZBxO(yA)%GhUGDp8񺿫O 8FBuo/U?X\U){g1i$!_KY X84k_\+xB~WbrhXGYU ٹ q0[sqDx|uA!u&Yf e~~aH\-o/\VJ&8xr)L1GzԀkġ3^& rFC\'\ǿ/D#Eӕ1' uSɟe3S)gs$6Q'!wڴ\"&_z58k\A<VM6?Gc=߆ "UTv?e"|M;[.;=ݶ?e~'>ytH6P%5U? ~ Yv5vVs ـjj"`$:_iw>wP_&_ G$l?g` <0 _^}'nh9q?4Η8w?A'9yyxNj}O55럶k{gx/la擅W#讳,~FGv.E(_m8%Q ["9Lӓ1ᒽl{rL:pU\<̑82 7!LoD^aFojcD9n$,RBz4sirp3剏}/ߴwwݟ2㱥?O 7; ow ~/}19<׼x'$jom↹Ǹ]o']y!0ECbWfX'8PūAEhmB<"Pc_AN̏n9l['R3F˼'R@χrEKP'&\gB8ej{A`wV+&n[#9ʑ|Ijz25{~3Kdj |"#y d@+t"}쬇.rS"W1ʣYTp\1(2MچAk޲{!u:D.;ų+szR'֡$vd<)昫̅T5;cW C%ӏ &ƨ܇U'}r͒jks&Lǯ{Pp֕"כ|`K]85 ffON_KՔf^3QGCO02ׄmq{:]5K(OɛX#+m ӗ‡GNgO_7yK 4\&'?>=%t|pu_ork0'=!}x!u}q"~xy`'\̐O;u)xWX$wuFQE皏8K !fKp%?v'VZzPSORc%;kx`v[O򨄘|If>}M\أNO@M"7K? !z1v{O߻o'S7O۳x|?}/w>ÿ DLov,jzG}W o$d5˿G>8>c)= ,(C|+gn_<{-W8+CxN? ̋nm\pI_et͢F >)\ ~)Ska 1m K>pz923?5yߏ[򽭞ݾ׷;Eş7H2ۧ=Aa).7>>O f: I҃@gHl9ZpfʓpF0VǂS'9@)!E&j_9f^KA;uO/Kauѧ/Y#}w'-_| /ٿG>_[?&`.?ҿwoͻyM7FOf/{DZx@yy3/ۊoDgZANܼ{|6%(bDE<ӏJ?{%y}듏*I OyN_<;M)nOJx0{pwmODPvy/K488\CwX7R~eh﹅Aq%>O&8 ?Վ^wM~xPt徿.Yq ߠ !^aS32byK. R,;2A&{Gxq|eS{ ݇a>1F^K 1_wgxd+NV:^S{k<%^TJQϠg^9 eVlv+GuLd3tSE|ܼywMۧ?9 55+'__~^7c{sey?T2R_4; ڷu`< mo0+59v*׽ 'p7nzSN7v>꽏ڿ <%>HuZB 'z:t=̓:9(' QC|SVz /t]OryNX+B9$"3҄+BCvZ, <g庰^Teb2>⟞vE3uM=Ǿ,uo~#? {o??"L5=W:lkI7juPk'#@Gov?(dD:P{^%\.ec.FP ?ݡxƴ~<_l Ȩb˱L@O&m]T W̧xd!S28H&`針JfdžxeFF}'iH>W5  kxGiaނgX}kFªp=@;V I'5@5~ %p$aM!*I#~f}_cJ@)0r2Do(_ɫ@X U'|NIQIVL}Q3 rx"-ʣxL/H3pBc+p:+ap'|. 4o;sW-swҖv7~!&Tk_|A(UI '@rw)2=e-V[G'㝷yfйnߏQ-LCZА&UL-hIVUl{o1Dӽ_~_~|o뙚O5o9Iܪ_8ߗ_%kqkGM>daI ?avNyv$L7ƌh*|rU"pNy2wA*Ro=bݻ!Ӯ:H3az@J ا:^= j88? _1Y @IDAT ʷJgxpuh.p=Ȭ7NU{Oq+yK=~厀EϞ"l|MR՗ G9SȬiD\K âR2e |MWq.Uz䳅gOzj k]gs ~!u>˸ď2z_v, WU=:僁ƪϘ*C˭ x@c૸^ }|Byk-!:_&}>8'Enʑ3o0%FqPδ!(+^LrL 2* -V} ΝP"|(0'SٲXx|@LL$F¦l|S__m7mx dKݥOzZ5tŝ9d61ٕu+_`z11)ZYgeH9 d־vvj)uKOWX3̧cj+{)z'M>KxǓ>)q=5ƙ(e^7u;1)fQW*Kol!{DP/cI&}e#=g- Pvxク:rVLr1S+=卧b/\%wha- r-9uW Dm+elpS& nK߮đpqVl "q ??'TmR{֦Z/0z,ƠL1($_T+Z6/־[.bqw6pQqHHI[~僘W˜RH39ت?ca8|dRe(GPg>#_˴%==iƩxz_a0K j]ȽijѸ19c  m39Ady?*<%cy)b~i=ANyzzI&_GE[G?gxvz $9ۧs⮸']Q27ϥ4sMGٗph_W?4/H3'=yi+{bDb>R>8*pKCuÝ;{Mc`/6KOXBGvI~pSMƯ}/S=GP?G0}^ O9/-XHu/sZ%uӊAZI8v4S?%|=?3H琹~ƾF+Vt`Ārh8e4_W/U991Ka {G kׂ,cVH *c'MD4L 93#,qD8.2h[T.DflZ@$N %7[x5fQ~a8zh=zSBpf+ :\9  {B&Sؼ{^Ol^N%)L䉌P?%w>lj\k2kY`?S)h^<ꨫ`#0]PO* ^^u,Y.?frS`Xk c?.q@s-^g6^i '{'O zPV'חUm|/Wز&iϢe6֏'VZ[4|H 8=} "/ї 5HBsc6ȇ c‘~y}sNn}OR_w $s[oJ8!GxP/plPL5QX(zГN:2E`A5('Z+'{1- NPhn}4yTԓV& }BǬcޚe!lF&4~K!\q1=?}_'|# 574<Àzh q qsʅI.66sdK;|݊U I8 8Kv7s5ٙ'x]4e']OHͽRVww<;H\3 Tr˞Ӎє3 ך(d R[sêdol[җe!37 $ّEo0]~C %ȡ4ʅDlZPӑCQjζ%c- KjfޚWYzj:kv|* \nLՁ$!s_׶ ^W@ee jTQJHz_MnqeVմGiVXwWPGz8yZ\9}0Ŭ9\C."eũ8fQ"޿ dds sn:n='C߸䓞g'os/u݉o51KOu tܘ?=qZVv ij;xpF'=G}M|3sDRk0YgoI=2miTJM $k Ȏ4,!v5,}pGޯ7ހ狼޸z'|}/ߧ]z>|{;/B9&K܏w伂/x_t&<n?#BR+'xtf3מh#[8NefOцYr+V"naGd~X58;^*Z~/G QIyqhscA#nr>._ ,*JVbCX yl_z_~-Yi\:ߩ;Է<)#~Zo_ܘy-SO uq\o .t *B9"C/Ba*$Ԑ-]%cly2fs`a<.%> +gZ@*8Ѯ=|+#Whs a ᠳWo T]Alh }_I*shey aԑ\pQ^aՒ1K_svȠjGG?/nW?}O=QֹԬkGtaֿH5W%_ٻLMl%jY?1L/gzB O &#5+._@. 2-jYv -\YI!cgb9lG& 04Ѯ+pȅ8Y^,sM?-wA!/3y[/ruXܬ!LKP|:r͋:*u+tU]YkĪ@^+̯dE }4A= ?ip_E,c=t3pW*5,KX#1]xhϚ.T%X" ֛&7̬g+2Ń=jbDEp.Erϸ( CS]VTL$FW}rđъ%LUS48g^X ] 89Xd)rQ0s1qO{lgxl y?1>ˌW~ -}r+y֖sYi9`}$ ը}Ƴ&-D{D Yޕkh&ZWqF~^O h[ \ x3w>է0hov&{#|<0>.3|P$xBHqXuzhrS<׫yUv3?ş'}O3~|i?=kU1,lX"'W͹>qGNe7yrh\i;ٟ>8ePYWSOlֽcW8)_tzO 35{Mk=1c(?c} CbE9Wvg06QbL_AotKrEЈ"a J{Fjc|3ɓ?Uv&jXq+upTz&K1SS'/OXW=:JJJ1a1 H"B kC>8+n(RvK:1AXB :Uſ@Ks+uLb(/OkŚ$r zy+x;=ͺdG69f!v썙g%*~UDLxpNIvEs䦕U8mrRܽEPE `4>:(T-G;.>1e2d󹲟LU+uzk|xag$⥭vDxHĉ*SC •]O5 |uN]zq7y싯Ѱ?SjAFZ? CbV &ºADU{sj/"F=0b.QagTu+3YLy>Q\ ⨫O\y@Fg 3~DtRWk`M>N>*&%!Zq"@!ȱg_i] *@Ք~azW+ 9TEfn p,|@}331\CA쌁F;Uc|pX5 %upyԙґGuX=3&aR/$дFL'rg&^`u}?ԘY}Lt%C=go:M:3(̆񘯗_Qud&bF^Ҿxf Cu^(DAovGQ=3aYudaIÙ\~&n|Gq/᠍?3/c0TAjG2E;Ũ0t :u=ƚw:QWl` rg84 N2qSpu38s~{A>G|׶e]b/pS{=|oB|~I΄gi5My[ȟ$iCc;x`YyIFFFu;zQo쯧vyDzοd<2wA@N'>8x{S<7xX;:PՃO: 'Q,C_hϏn9ch,ۗLR<(y`fz֧w$o 2!1d?r_4PW\rŧbO8 G[?v_ZDZj$ W)rN3N<ݳn[g ƚ P/8C5#p85G[%6ȗDD|zV1s>H jW#FBvm)=|k{JӕyhG96'}18|#iyf: bU[/=l *lQͪ2Rr=akԘ@ݏY@UΡ$=aò(k RI/5!al 圠0:0Fr!;^YOOy7yS=uQIX2/tV4 +GϚ^7bm[b8 gsAk4r+hA VvK 2B{)ek~zM=tYk>pC$7 71'r\WӐb&kJ.?(C?7,Kش% 8^ʬoG\K/{}Vh>~c_+Fz$wo쯧vyDyοd<2wA |~lE88JO?^ぬbk蓅y3O?vPQl_Vfz"PT=V$.h얿WO-ϑs5Sgvx"'cG"7bCxes%-b@x` T/= JZڂd5^.:!&{$TG[Ż *•G |812bָU=ju=PRWT.c_ 5 ɼ)a4O}DrŜGIFW8>Er\W#NJMPeWd h-BY1Q:a_^~õR5l t>u}:*>m>W\̈́MN_ U[EsWK!Nv!G,ly>|; 򌵗 3%K>s.UYu0`1W~|[E"uCPY[`@_^ ~t%f17T˓Ds|gޛw59{S )Iރ0 Y[8@;Ob-g387QɿO5|kˣ Aο Ý$ %D I>ywEwwC#W~kǗ5ys%HL|)l8Jp[2jˮ!.|6cg7iGQZŒ@\}2W[yQ1D_ 'S~ `3YﱌhN D2j {fR/=u\zIb r1QyDpv=g@N5R-G)-;_0ii K'ӮBs13F\cR8#KT'`~jkcL ?R4O9cjK; dEW^k^ <x9/9|Yf#N.uY>Td51]H9ۇ{é ׭MxAMd:G\U/f%seτr\{+Tל]nkrO79+]\tfPsXdZ\5N8HQiCaғE=S<3ȟlU{x K3|ཷNWC;l4 g`tz鵗?5Z/]"_ο˗|^Dp_[h凎CF{/zNj)@,䔙U}Regx X#DN~F ߒI^Z?YSE >]IOL3Ǻqy3*^NaG8_1(0R={zt۝+gRk6MwNaQRd?RVk`}=6[3-\ʚu$PIakrO A~);G~H%5f`FKs|hf' .r2ƙw+}!Rv+X/e܏O0iKUAyEVGw񗊹*)<= c-/JN&>fА,VIΡyAy$ɉjrl5A6Ycd&*"ikJRDk@2dJ>g@EÕeJųdX,VGBm\p?]*>xϷtL}^cV]5EpqL5Gpq 0r>PASƽIR@r' "ae%6q)H|'#:@ѫUDG%¿ #zzG>o;]7|'{v]՝;.߉Au'λwAu'οw"h{P]ڇ |TǓ!|’|8IʁYv4}A[PELHa0d &@=viFN.ɏq4Uc &|@^"9|3DzQ3 {#is`'5iXkOuӺ=V 2u Rc7gBI>ԠWz]Jq|Won0iNwNo,0yGTPg'Ą=Gs/~RIRѰCZ22 mE$:ݛoaDaT?Щȕ<k1] <dV `99[2.t଑VV KF6٘K`I( \cS<دgX b\ޓ0s8Q}L}VP2eZ!4:V9f*>ˋ yJI" 5U+%[b+; iX)P|i i㰭V*)+x뜱NCLpeV9{s~O]?2=6Ms"7a\ usb6<1"R9ZM@e)&f\Nٺ#ƫ"4nGo_LgKeqiȋi,~t6zzb\Dl9C_ڇEO n8`Ga(.їIeο7K)|.q_|.q7gDA/->i2`$R:ҡ|@> O[y'zL's-oICTg>l^cyC'e,Z\x[}YuRq')*>\]/see>2I' nT&q B{kOy0v P= F)bbF$QzE2gQ0+V_2p.Kr ɍcb -hYJz~?$o 9V+H:+uX󖖘@kǘARDŽ+΋*Gƪ+)1Sס/ԯ UYz3@䯱.}x%ez@]靄x8W_@?j `Bh}c|®zOT"i֚&/0\Q);R퓺/.*CYq\_zI90qf9Kcy$ f/ŭ׌&".J#^^YsS=GG5C [W]Mդ0Ɵ~ۙ;]R_H\%>{FLZFbiwY,Օ'*:P$eqa[/n̙㲖q&YSz֯A? 8㟞mMb^g??IRBKE _⤏R5]C_w]'nο{8_|.q_|.q/ܳg?|Z,yr>ӍsqlMT/!+ ;:+LH#@X~maFsbKhqC@O/50yjkYA"$?*<|R$v̳Hm[W|'%"TAK{=}Q<=>yCd7Y=#[bqüCRAR񥕥G+57 \ۊiPX 1s\ FEeV<ji>"ǰ/en*k\$ >hSɊ#>񊚑Vufi|ֲA쐘C9>F2!#Wrx3EA ugZ/tES{%Iȯ{v `ȉP{X>ExG!yݝnO#*kg NtgGgfuUC~VoGK%Nq:e^Q>^fѕ1\\nrB+9mVz)(C}Їa|rp-0ּ}>ߌ$bs݃Hzk#n}SOyY)d{E >݀.>ƒNYSΑUlȾ1#}jBd$ ~_-p:kO?w^ܝ\xt0WX*0!sus3^\Vz"s[5p/_tam}A;h&Hq?|?a=8/^P,W?gt\c=~>VwGw_:3yu:>_gKp')g5 _w]ZpEܜx_G@x֟S$4yutkQ i]!-lj=y&P1ϵ8׳K'W(􃄺[k+1ϑPFpŎobؓ}lϩ7C=e`KbbbGOx["\OSsqi4㊅0-_RLSͩ}]"1q8?ܺURcX,`΀h4>BFLNy;Id#Z&OױgdZ9;bW [կB`Gc}XZPoG{K K=hnq(,Gjt@0qvs7\Z 'Fw;7/a6#X8@ U*W/6:r;^H!d8NW >w޾< w)laȃ ^cq'mہ#r?_fkfk5+1fBj5{UWwW^ Jo |`l6l/{ڞ3슸u#x ؇6<; o(WX uQPm}w8"C|wF,] 8 |5ZxAJ_,C@k Ł9.(q@֘X* :/D;у! "MX _<*5> Z9NO$q+>y9Zkߘ>)ҭc/Gx|>W{tCW߁gsx]Mi\q3+nr\;ָKw_qǚ1MCI>q5:K@n:d)RvֱҀ_/Cr$NYp^ `8}kA^sm7< ~r{ýXҋH~| wگ|گ|贋x.x>sW{?sW>f?q:*jWcW>!@\o֋df1Yy69ALA2a@yq3\9{6%[|@L_L.NzX 6ei~Gz VY4/|  >xaDh:GQ4 ̃{R 훜 rײG|}C/jCt&9|<fKϛ@i+4}e}v]H b]-܋ @خk'sYpͷϛbr}k{-N*}Z)^tS7Gx!T vJpܩ߀8'RMԤ4kTY: d@7q'|n}PXzǨaNԦg>=j0!f`= Z'C"kݦ=ԫ/§\A|+b [k=15%|^N}@IDAT1p\OXhQ5؜ַ 96fIULHll^x8GM94c;ߎo^|sT.f?gtǸf?⨯:?3>}>q7^}n֫>SQW?O76lz9en HۜM v bĊgtb7afŮ>S:CɋMڤc^X ?Yμ!+"d=A.;rpDX>$SOȃr-_1qs6LIkqsg5Q㵿𓕞b3hQf^ۓzM>tkʤFG9򅈐v9㞹jD@5\qLP{L'-|')jI ?oMP?zwW|GXT1ntr uT/cXhs!:;]B>d&[pDZdkg$NУ K(Ȇ?>fp0ru{s> rFįIZ.Pz:g;5:k9,?A CK#!i1I:82ȳ1JsJj*1H?S/\,Ϗ;\N7_}oc4-S f{}cw6ǧ(}15DG|p;~j 6G"?Atw_-:rmM^!1 X/{7|sSsG?gtwt^;=sZO_5yIAE4^yoǿU{{/!_yR("UFqx_Tl06>y^deL佫)籋fY=FaTs )Uw7)-nD>./'/K=`[ka}qȮûv1>/Q1`l4(GLMwY`18:j炨~HFˤ":KG&q-`NfF nkve#'O՛"4OXУ ؕD`j2\`S'nmSw+Þ }8R9]ߧZ]hkw>j=`J?>Xw.ꨙUGAs *SQ~ HW|( gPAe y0qhʏVz4 ٧/(ۍOyku\rkw\F^IFXLe ;bD8/| [L=-FR^ }0 K#:l Oa` bOb\7/cຮtMO'?6G~tgnxxp%0.gAWΧ. ? +(hFƆr=뵟.`U Px#x=ke{b8'6bok'G5Co?bQqFcq=G| Mx?$\ uW9aUKhar31R$&;k}k&fyCNfz\˖1ZY>X?Es'ÖyXZ"*>ݫW`rI{U\C\K] *trܕ;ctb3 al\LkiӇԽPo Y  9y-r|w;w_g8mjVZ[c7z1+7~;,0Asjo^/@x8$FżK1Sgby\S$e0>>HJ!19ΠvDc߇s?y浸r7wC3֫YY3/?#GX9ƿ'IP?>Ho*w׫/_篵7|/tɘ͊]goXlLj7at6h1 ։F뎽62у#|m7@zw'c 2dIG^5 .bXw@Zڱq5ێ_>ňc2)b 8WWR?bk| 7z`rk6MOpC/=‡~vyG~0/;Q5Di訇`#5Ի$Gr~9O3<:?$0k^z;:1SB| >FĆuz?q]I5A[z'QK||WHs>̝oqS FB3wzg8ϰ)*Fdhu( osz`p-Oӝ~ [뇃>&w=ۜ0Мö1++sք~]$S+csfOM^ s?MJGBUP{JOSk3UM|(Zl翲aٽ1}O,ֳ4N=F3o}Jrqz9h2Vρ[#?_?A\%dUsr9N{}G/G=  xs=&cٮ7k]}9ԃ:T} cͬ õq,CLX-|h.g_]~ٖRHQ+a? lH za@T[xߎͿtJ(z6Gߜ3~|CΗy!NRL~}?M>~ųFiS_ۀ\L0wTDV~ C_L6qRV>c|H RcvS :0\L/_SGCk@Z|!A}|3}vY~c+zo~@`Ѓ=7lF'L!|HMui9%.SipTxQF Z9,1 >xL2>YNk.1[nmt'Kr ܊4 @ؐ;7.B~s1Hg8w<]|L~/]}}C|(!)J4(YD$s$\]jZ1y[4<)|S@M%}a GI䌏~9됋%:꺢}ør` L~q:3r{fS2 䞍?z|/&X%%qd3HocK+o?w?{@𫾼 ɘOu74Vռ7M":v>Z*aqܱ?m*Co4r,9WLY1ϰx2y9fyԼFTS'냠Wf6ScqF0(׵f}VLF ^CҲV #0TJvB}zzB73` kM 4 挗\v,4ү'bVWkl:c&gϝGK5S{?1~ ŘՀ~rW=:°e},Æ˸_ol@&>]8)-tM@81ԣ[qMw.׌{[G_"1CQ[ksӇxW,s71y bq|ǰpl9 QcN̉^cExGSY6]ep.m7Jaf\-';׸0vL|YrĶ9bI<+;;K>h~).kjir/Z\I6G(D5A{jĕX5^?V"BU&a ţ甝fW&y98t{Wu_npZC,uxzX"2wד>fȈ,66.s(C:_.Y4]~ >&duZlU״;Ń^#Vܽda]"j^~(\~ĉ̡epy^ג؜M& ~>ugawA<.p^ms>_T Q LLe1Ce띘LսzIK{;T9q΍!0ZԫI(l&6X@ Üȡ{$?d_عySKr苚pP <ȑDO84zqMXXMMͷ||aeݎvk"(l;$T_0>My9zlj}:?GM?ͯVtepҎ|əgN>ΘS>߷sr=[u搹N܄3§?q;w;S}a}>K%>?j1Y$gr%yn,-K:+z^#K^?=]}9`h32Vб79OЬFm7fk7IldiέNlsCFrMC[ygy!~>gn)͙|ñ=)Vse-,#,ĽůV`{5u?D(x]QуOop='A /} C_+߿Ѻ2~[kRk/{iu>;*ȷȄm0a \dij7rNdyPLŸb 1lK /S$2٨q*@ +paYV<&V+,gT|t5h0.("ǛtFQu#aMzq] }S >tڲ-rA5Nʑǃc9jgJg8^GyNKq. _C0ƫc㪙ELgC6n#uƃii~ '~9Y;nt(C L$@pz:N2}4ZG߸hC G%:.R:c_͌Cc ~OS~ =Nᇣ5i4TXWøsᚇq@>"w^lj[nݼ=`:;5* ^O rv>3v-Ks;VGnev&.rX}_ƕɪAOc32+^湱(m{4p{] aRү?EH-Ej@c;[׼*Qlste/oR]*\ײ3y}Y Y}|>yyXa9`e{bNH|Nh'/ƕ "!c=sϻ; HF|d)cdw"'͟~n:袳pe_?gԕ}3R*еfG@s%J>?wXm}׽c%R)9inh@zaO+;/OtOXߝbp@>@jaȨN?&xztjDLr0Clj!cW? >qyפC|_ר|ſe\z_?@7 Yŗ%ZK:WL^iz·Al=y%D7=aKZZDК:Q۱ηdv犳Q/{\шKΣMMu#`7_p.CءYzr)1ǘnHZxVExa]?zc92iM\N|}3)aXLTMjk~9$鈯G"M u lfsSϑ/D^׽u_Le?x Fh_:ɳ#<8dy5cxs~ 36ȮIՍzGq$Y~r̛@GgMznxL:B`^FEPW{i*A7e5k,&?<[z5sd i +:mdYUSs+?t)ۙ>hUY-tz׺5Zs6w}ށۺs ̈A c=RABu'_;bYk}UJ\*`*|~Bcîʗ9ꛈGǮXK -\ ^H&ϳrC}BYğeᥧ,*)]SI5Kt7?:sұ?;:熄\hqh1p/cr!9^ZcNQʷ~?=2ԧGPSvh3=_81uZkA;rp?om(^\@~H"ﵱKqC _޽ҟ~âx)7e M`1 wN1l}k. ޑ uЌ9<@X"`s]WY'ɧ_vy/eŭtf2d0$1]|*&2铳i~&uwf/w:3Qx G u]F !irwt^]FZ~M&}ԼKIgtfNU}o1q}>ᙂ:PDdu:;mz 2p|nU_j{b}{vgM="SXuOazO觞DǾOcs(Ջg5C^i%ÔUŇ{x xߏ8\a(b)Oxu'-6z-6Ckй9Ͱ08ucsǟdukT!ţu6rMHSfg}jG:'+3;ǟpv >~Ow5Px^3LS'c'uxIJ YK@öNzWAFu_.vRX&ιSあa>y_8B-^_H$r*o.4^8ZeRw֑m1R_u_~c2D3;_x O}]s}2וx7|n 6 lI0}Wn oݧ8F%,O©5cpb V O*iqn_NPAyp̛jʇdLK 򎢂°{і_lɱgW8YN N~׿I;|X9"Sk//ҊI0nM\Jg;ڊ;J䵫XIbYqvDFft,yK {\ok`?20~z%0gQƻ{vv,M*Us2N콏Eu;p 3 edb`q+~89cZ9ٱ6Ť@`y.gKoguOStYZ?XCz=fOh ׾нdhj} E!k"AXK6C|y՟&i?SޓY::(k61ޫGcaMm1Ɵ#^ sأ|h`~q RG$WW ,Os1CjkX׏1v%c47}F;西3K eE#^Wk [5$i+`ְ`kޗ.F)Ċ6%w|؜'}E<̓c5N@x]s{c-M7KYIus8TRVoݵp_]/5y7z4f;>x|w#|_Bizo"{Kv6n⌱~a6ngSsvΈ2yt'LGQ Iav$==|a/2~/oFRDAg'V11z^ac2cx;:}0"~Oh)cdH'0馧Ukx(z>۞|c1p_b}j<0 5£y>IuaڙcL?p1oZN>I Rы^F௹"e\ߔJ.#v8:ȕShuk^btuZz~?y݄IvEV=I+2z,ܭ~0J K6|rx Y~lx6Z]|ldʾpAszuɝ@3@<狃q!vfcW1${jc%:TQ@aaMw$L^Ph9`LFpX?<, :G;Q[BjAm}s(V$|l4<*$aұ=YK ػ?2M1)Zehtdpc%aˤC2)s~x-YE3%G4OsD?uh6P%`Z:p%c}N>P's^U:w`֯qlwT ߶Mst^6]֗|ǣH`:,zAQXW=\iNs,zڰ&/y5dSuRXWg+VIfrڿ^?L9jk|)Ǫ,ڎ?yc0g=s5[:Mes]v~n߾+"v`L |"e/vZt~a(tFn~X?H.Tӟ(sMIĞ ȣ3k$g]AcM]D'DPH=1r:+Up=NmKIX7ؕ7YDu3Vwĝ-:GΈ${N[ȺYkS%"8dr4O9V8wI]ﻮSDQJWښ eP Kx꤮ KVIKfŽ;^\8M̳¹gGj9inX9j0H.[ώD{yY^϶!.  \Q Sbxx#Mj,qG+gE>1qZZ\j&`#% c O.a0/ϳ۟W%Fs2(Ȟ58!Bh$K[Տ[Mv{x?c2}}|@AaV4Macd8MZW4W- <.{܅(~9rzSgIg]zݾ*q|sz-}v~z(om9{|_޺Ro46LlbvrV3 zWXv49ʥ˳7Fk"FҽPGYc'=J1x'Ó9śm-S_g#.= kx\7ݫ;ՠlG.8x򞣓˃fޣEsXrM_#7n +28qzd\3B=So׸~M\ Kԩ][2)Ӣ=8I}U\ցṟhf:um֊ v>7̩xj[TÔ@4谱N:y[!l{䂠G$u>PB}ZTRSC\yĮ^ŭ$q@.&Jfh~SSSW9 yrt7:VM` 0 1Ԙ*(0u kuPS}Iw`?->Z!qX7 %a&#'uF6\=^^ T {o9]9N-3su!\g G Dө OY`#YiG|_/33t_0 wݮ HܬmvƼ,400x l^>}77þMПB/܍9yn?CwCy˻/6z~r%ҁw_}Ph7#$,)-TGfE8&yk%ϒz䙉N;8|RZ]s^?VL1;: 9wIPv`8qڮ.s+CuBܧ_OwݷuŔ[g]Y< gs5iI.s6 Gv, c?mAv]}:TuF;[!M6$ԙ"Rks_X3Y+{?\dj6getM=]JbqC^|/NoN㓰:_38 h&_C ri4L{pkoqS)twI?YRPOCBpؙV4<޾NȽ+3qj,^Zsŵq߬oan kwDZM=%7_qʜv-;nOhײ?i)Cxq jmE'ɅZ[l2ԩ=䓵־,ǚE%,}AI?;~@*G=^ذ7; i⤇cw`/8wt'ٲ0N^ ]8ú_N΍s>.kf=_޸֙=_4ݡ_7<v+Rn)vq_j/S;q\:F}@?{/r>xi Qb:px y~+H{ڥ z眵ٱ$8s7cOK=`bܱ/$J󺶛XOL^?g{\B" 纝~ugߌ' ~Kw.%0r^IM1I=Sq'k=GՅa~VB'W֘y;ÿ/d^j=uzdGɦ]-~/΅ C؃9ֲ?0] 嗟^m9x>qV?G~@v$|.쉚s&~YA}<~<3=ugt:ୟ|?,r?;~>Ew>u}Sw@IDAT~Ɨ3w&w ٗ.cTb巄rړӯ֖R/Te̕ llmymMcg~39xͤSo6X<;IMZS~ /@wi?ѯΕ[{}!?:?|һo>e^?-0=7БDWW$f}IGt9%kPz>oTةΰ;cxxQRY/O:{37崕JFg=RƾN/R*&aF֓!1: WjMNV:Oopee5h.#x.:vuZNq8>XcL?#ːbŇװ5}i5'ã:]_8!|EgG6>ԾO0n3L pLkD$:[\ }c_c|>bs;ʼnfNDXVFf]3&`j̏e&tG\g_SN3Ry]Ac^_EHJq?p>?w_{/:#ԾZ-"7腢O@woj8s&pmrͽy8%_;osD#u騗 Vx|skc;;Tw^wW-O%o>9#_p`LK~1+d'@,y Ν_+֟ELLNS9מ1~]7{Vn9 h϶_y(f+q~/O`49㪁Jc }]b;.0?vэnݎgGWf8:ϸӇ2/q Y۱ylj,4Úrƨnm nS:Q%t=-B9U2e~Rd.a#!x^ lPK'q|kz xZsm.Jyrs sQJ|_S{'fߎٵ 祼l/x],9+Ųu 4q_CGGNczx}{LZo$#ݯS-"XWʷl)ܩQܞn.17#z q`n/2/"r";""ɝo7MwM$;7tzۖ'0_% #;U%&%Nc3űω9AZo1VNgAtf  }u[@c /GXe;+E;ecݲv.?~3 "PσP{5p+z#Q>; _Oc'ZCQcrYLw𤞝c x'_ݙY8ui['51@|YkXo) 걮Q#sr[{qΡe`ZvO#qNk_ak^2V/f'E)Qba%b'<&B~!͉1&y1O }qt K(cid;ھj\leOI/%Af*h^m^X3VI1J/sFX`Iz%5wlFnSDz8=YCC}$p`Y  jFߗQ OdfIba @ bwjYnf]Wh G}'\s:*>Ci%>ۧ#>^~ڤwP`o6kRfԁo<:u<򮰭d@WLGs\WxwϮSG>}|m޸ۯ7~{ ۯ7~{ _Ơx_JY_a~)txۏrU#ܝn?yWpw(9_u:ӇOo7ڼd7Ma](/DVo^cݍY^Wl^,q-y0yh|TlTMu[lM?Y܁x^?Վjvxc {!$ Lgm ~ZRTPumUH/tKĔ}7>chgS+34=]}up$#w:~BsqzS9&u1fՐ+CYZdU󄽹Fh*,䣫W {cNMpyĆ4Q_7Br"50WZ^eb<.^3("a̍]|i&v(M/ԒZ KUIxh@|hwTxIlYf=]D ^d֛D?WN:2v|xz7x%W"ڸs=,.YEG+,]u {brE-MǙ &p!u%SWK*)gw{D3O;]_P0߅`,Sz4|b9F}{ o 7v49c{WժNkuۏuF֝n_яt|ο$I4y:ӾڼzwQst|G9Ѹ?|:kGr11R]%-盆iS䄞pJm+'{ozO #:> ^*]NMR>K\/Ax~;>;/@.G܂aȺsxCB0 ]%62ZCq\u=A$\C&U9+ Gd}^!,Ԕ#oaa Yc5"8XsdH0g~pQG뺸ej$S!C\uS+0cc\zISC!r/KSqfmL$,oj|*?;&27܀5 ;> 3KĆJ(O0]Od8OZ)ht5_k۲ɪ"鋧Su5!٘g]P0up$z؉ЙDsP1K:|Y&nOĚʘ\0Wst?pNzz∣~@eă9e@Owg."OGL9ܿ/7V'[٬pz-l2t?Zz@K9Ƚr˽st<=;$rGqm1t]V>',U~7]z믿n5{g>sFǧˍ5ױG|XY_~zw_:w>GGOko„y9vʶ)?dn>lvu:l">'ߞ[ *my=i(~'Q_'k 35E&r\E.osr*,0\1:0idCvL1eϵُ*gF(BX/ ٣rZG:HccWdSթ\M35Y'N_{SByBxRs&jIACpc~Qݩn?-_mۖeǾ"zg}GHzCi&ҏ|K+Wr:=~)]~'0w}D.Yww+_fwk%?@r{;$ߌ+?;kikx~x{:z};}GoL^#_aœ@{Tcvv;#&0Iftpuِ=f ,q2/. Pz׈(vzԌp4'Acl.k7C=ֲJU4GX0|̩pzEy1.j1Wx>G|>[BRa̜1ˆ,37䄡sƬ0KpjlvfQ_{5ћ>FSbŸki:u*iXMy;q?@pL?+mw!=Gѧz:dzj]?x/:e̽AZhXw_7ə./9'+*mǽVn+\*Ra NWD9g:ұ%qO$?;u>M]z}FUۡCo}|34ov^L 5{7|o&~3D؎g쵯hgst|DsD];ww8}NֆiSW4뢹^We]'s̟ԕ8N*[5cFjalVͰFWfdwVHkj)(T'>h{!agrͦ]R)XjFa>z4øF@s4Ϙb,#9 αb'mu:o~lǨ&W/!;0Cn`ku+"s-ωMECpL69DuV_;I0OR5@##*7v}#={sv_k 9ƍq5LN]nwnkI^+5h|C> yowϨ"*fy@xe{XDdeeQ>zE5FR0a]lsÏ˳)3F.ӃcL&ԗ9=6i~f/n'4c r|e??_eá3:>?⨯:?;[{W>3G+3"yOgS9߂^װ}utŰ{sf߄O3jˎ"S; v'a*n(+g.0fj=? uњ‰Q@yyVz[5"Vnvֺ↰"m[}eQީ{_MnWSyюzrKR/5I4gIb/V H+w0xR5Rzt9ߺNЮ!z!Vc63qϙ 4#Q;ˆOگ+&c~,O/zĔ.3wQ Z_$M>ZzN,N뗜w jg4Gu!m@RZ''oYT,u;angOrR2XKSwݶ . isq 5ϻM7z4֓XԹ^J_l. 9|=O33cy4Yso]H[q)Щ=U50_8Rww|Y9tH%5}bG7kTwʩ^ul@gWca8!S]Wuh]] O-~sl_gc:iUn9 Y6ӿ$ d\`&cy:ќ^͸aZ'v WLuإBItrgU ί]PNK?vjwg e΁A{-ciXRODJ+m׽&n&= nZ}{|k*"nV?:;49j·iO]֞qcGD]5LҤTJcb-\*[ww _KF6Zjﻮؾ&mた测c|K|qq%q]x*d]bk~wh{QKQy?ǿW{Gw(^;klZ{3lZ{3g{? w$[fTJ 1^>[\DQ֜tJ; QVBSykB@Ş{kx[qE|@ż̃YCp[tOceLXUÙ|p2l^9:ʯ(qkoqdtUw{Ao~e`ɾ7#{Xc5 F)q5ixn-_`%i~ލú)wjs71rL~oVr8:3:=27uza ~۠TY/_> F|+7GM0|G'q8sQƣY[G[}}}_%y}` lkX/?Jَ`EpЃ {PLN9b8#Y Ǵ!q1IVq8ߥbM YIo K&Z{Փ>ȿ_"F{31aJ>}Ϳ8u4 e?c*2sX! 8fJv[P}*@oJwb3RBLÝ\g * }\ sqL q;@z# msJ^!` 쩏UupOh|uN.s=E4Cypћxh j.˂#J w}\_%cNN$msn~Sd428vc=Aw ftҷ}P[u9١^ sֳs'a,YDr^qB$*uu> GoI֐3s2z?Q ;0(m;豇=Cj5TC\@0rb7]ɠJ.Q|g_ 5ع~hOJr,Xdc7 x h>"}g=u择T;J( *0uĭjfA~hN| q!A6q^ R#yߺ?y53w_ $zqpr4T9w#ҕz ։>cv(7f#3;K.DZ$ / nGF L4c ص[,}%C>kzKEͽ%~B0)qB/T} HU! >_k,9e'Of>t?r=fc"r?m؄:^W^𚋌O|߰NhY Ԗ/9gpjWw }mͱD\1wfUנxy܌gx=5ZWV?x9j%+/vW"z|vp1sy`'FV!0:& wd.^IhpWf#˶a40[dfE(p|xN#]h4P`Y>,ƛ !1L~=~$I' uɏXp=x3~ (뀜R!4n m(D'`; HU7=֖^w ]#)'#ua^" Y|(͓T0G/ukq5JOxpWͺ5^ע;lV/ko1x~xv}^jraZS~7w~?Or׮.=܇&*}=KRokW9ͤ#v]zYcZ?aYa5G_|={4RF?pef9&pgG3N}*oKITzqa9s9T[qs=q̚I.{ )q.P#PfP9PI+>]e(I4O:βH_Wsk4Wݿs ͇vmxsWZ:ejiyLJy&sd+߷n"#G(-rc}͙-@z'z1~.Ga+6[xkEMK?uc{qrnr|}Ss3j8{Di}'X3lb1A:~z'vuWwu^'ێ~a3Pm.N῅&{N@6&p Ux>0U6>0Os]Ĩ75*?P\V;K|2g3O;+{m&n^"8*jgYJh_}>}G͗RXg:غP^4֯ɚ@}%RkY6t>vZOG{ȿꕳzPO 0Ss-֋>u6s3Hw#`׾[a/Ij˅ɤgɮ? &2 O;~T=6}aع}rк3`"k=r}n=q̋> &?k fzX ;?qs5.r)Zf/syhTj";\Cr7ns[Wf+(x1TSRuro aX=;@ (=`!) ~$gdS]Y*WD*uU*$B {~JǕx]H[Gc'dU|+6ZTѰdM Asj_㉎b=GcǍᴬ&F&>psRwZCE=_/RZ;}ku]Fsȭ^rMۤzq|z-ſtSϏ尾3gI{w kQah@qVuk_0 ?7(w~1׷jueCEz6>{M:=zͰ 0-zd vqeC]>~] <&][Y=*p'%]JiԷ!ތyaM-#q߉]uZk؉;AV7,/]Oh4 cp2'^#^nr\o 0 w*XUCRrpudjW.xTa<chf=m?w{0:bsױ$/ =\C6E9Ǒ̴:!w|#2K<_&Tyc_ףiaf 6y=# 8X'zI9G(ycgYY `@f\<t&%<@ݛpl<_c%{]a6u^3m-ysjN?;Nx3&?x~z#}~z~^#)4;o| 6* f[f_JoOp\X梠MGCG+YFz{PlzΥ1k%?ַ!#=>X_4Ke.1r9|fsKN`f8m}O sev/I:d[qjL$ۿ7wQ"5ʦlpZbĢ}Zw2=s.8og,sfU^ Bjޞ\+: 3tm7r:5r,SLZ!"mY!"=ϱºslߩ|!d!1eѢ]{A41/\ZVV6X3*A_Oƒk935 VνrY/9ObwKg|K^|a>K#XLtp:/>X=yxGoB~.^W?}H;ڙeԯ]ĿܝbX4oC'nxSrI|g w3F %ܤlCלpd^lpQryJnPZ*OrXx pVgM^a]&\d:^R_˿.O|Ă_B3+Scdk3jѱ؝ɷ&1k6G,= gY^'CMUj"Sej=и4\c{/tJG~[:yE᎖ | .C8{WN>UXt[ܒ԰mk$-MEoFqq- YzM)z&~L~2G5|;tw䍙 ЩNd}g,$j IO5]G1-q p=O/Gs|\7p<!m/+n8\clmr}\_>_}pހp}oot ta5^/V'/9xd3u][p.¼T._+DY_m*hN0aͿ?y[YP*Ѓj1s=\G֡z{ &6z9:5Cs1W_ˍ"֍X)2#CZF\,? s"SJĈ/u^@s&%wqˀ3/Br7(D}$PgkHdL!qkRgEgz-L)#[x :`ņG!I)YzQd6҄KawZ >p'3|ҫ"F(O Ya?Hƛ.#h^bMfW aoUbhÕ\7=[6 Hq.Lxk-Ou8 R] dȱꏥsIC}~򵿝h]udh#k#VU sobI:4e]N1'fk} TY~.Nmc)FFZ-;e{M]n~q,,\zzd/b9lco)P=q$o[{^56#7]|NoƏ)Au_ſg|_X{ƥW33#漥x_k~ym_Ԡ;KchH77y c^&CVNp -3zI\Dp%}^IQ<5̛Mo&/+Z-on}g)^Pg!z_g 5ɇ8/}` !M$TRќε1ϐsFd/جcp͍5WzF# sKFkX]V_qDK0# P(M25+Ԡx &#_;EmTEUxߎG!Fhᦨ㺹X qj|`顭ԧf̾^񈼅E="Yʛ \ЮC$e ws;!)S'X'Hf(Q\?_8tQ9>˚9@Ĉs W1\t7CaǑZ$:  |D_ПњiPs.;#~?)@IDATM {M+Ξ%)w^Qd$ý#t4 KĔZ$G‚I-1ќA6"w5(vdCn{z,|cLp.}'W9}9sWhzt8χM΢O4FkۥfU/E[$xR1^'yZwb2΋^s.d/gji`Zkq~eX8QEXN#)s6$&%ǎJSܗ'qzl=t6Pwc=Zi=^}0\w ǐ8mkQ㨅/ 'OxHs~w?;Yع0[?\-;RBy)-7#PK̼8i|͟3r=!hlIP++^eu_Tyα{rS*מn7Ѽתԛk^kߢMMen8fիB)ouSPuw5Rǥv-0{"aM/ }de{2 ~ f.G4Kk,ZܝˬVxs(Mf?eL5,xVHYuYo,~L5ℙXO/^vLOdJ&a}H[}\-([ji?' "B^_i;ևڋڐ\o@9uzz̗Z}96>t)?)Z+#E__'3xk0_T~Xs7ߩ99o SwwÄLOGmG֔8|ϟDO>}gv9ͦu#s=gas̽a3y<<0XfCa8xAFq)Av9.~re]dKxn{^y/'}r+t\N͛vEӼ]Lo _ H5+FJ`Oe=Ȝjz2-HKPyV6t$6H' :t5~P>~O:#)!s4\pQ{:1yc[:Pɏ=S9UULĞ^Nr ߜ}[&̒oN/,9$ۙCe`X4~;k~V:ҩgx84N&KءYi.SF[CqY'RG3U躞W!YN {L,AVk>L/KM2c sucW.w\|hIudŁ!cl|#h (aL0;'|#JrqP2G/{Uʸ|yY(X>r~-lJ@q֏9>#cS:\Y(u'%iߕf ͍uZlPۣWF "1<끿Zv8m?aĽ ci>kgck掉 Bw>F_yZ;'|{"qqzq_G>7V3(ߣS̏Wr6Mdw,¤b6 7/7& 8_9[T68a )#bl}_ S+!ED:5rq`kM> Wb!- V!)jryMF[?n8Q'i:ʦ,:_f3|hLcq@ʱaY}poN~/㵎-V&ElqN W& g|},K-oU'e]izJ~P,u:5.]&Be,5Q푼}/B._>P־E-ae~x(1[.NpLĆڗٖiWfeRr4h &7Qciy^{$UjE}RU^6W;r,W׳)uʵLRsk=Rξ%Ki{} ~ j}(ڬZ_kE#ӵ~wL|"-\Ypa N쥾9I1e \3PcGz' rC4o Hq?my )wԃkڿy鯦qE~OxÆ}d9 /2uxy!cʩ/{hf)7"ycmsکmΉ WbϣɌOf4g Ι® Gcdȝ'I$yA|B";biCFfLrHZb}Ҝ{=%\\k5TaJ#YJtBo_ߠ\]vD#09::Rl]W0 2!_>}ל~ѵy-=߀x c {9>j3ӆn/u~HVj){/v4̕(p^ |5pք#GwtO` Ӵ;~#jz?7~.^_|'}6rϼdGY ȋ?,qigcspq.C/(Z*%;17?8 ?s 7.A\xQ@2Y#v+Ǫ~اvg$nm i['ODêr-`)eN~P5Xk׬]_@ZȭaS\w6}G[7P0=Du&9anDZdR,G!YkBq~:vypaZkJ|WC|e9Ss Zk43gXGs?ṯicQUo)]:/T!L:?G45wj<<;hw˘ h~5άj?D&k1&c%mk̙sO~-oU2=^3f}uo:K-gNOyF쎢wϻG™xMv2!-ิqhsK>swzn("ɝnez?+$;+ ?n }pk_|w?\֛?|bC- 59imbԹ̓]|t{ }b<)=+e=^%Fң xWDky^z3Xkn4U덛5p[?v-r2b΁f|+#nvټ{^/x:e #$R"V=f^^cyuzN@QJcm]Z|y$fI?;jy\b+ 1xN?źVu`Ύ_Ewl>i7JÎنXA5 bn/SU# B'_3.BzXmU=KhRWkgsUSb0ӳoNb&ygdk. stSsDCb5:4#<4+yL5oy1Z C>7#`/sFs'g <1::D7+ܼpүj2}Ng>e@Gdl;i'pٟYjH`::PǾ_ȃY+[Zs) S׫#}XA-#z`;9szRS2~~~ ǣx+s{: +w.Y<7v'X$X!M^·+sT3k֬H__7YC=8: k9ڌs=/Sweyw_fˌױ̾/)!?nG׻__Oi`gF5k*Z2ol2W21GIb5\8- fh7Ө #ה`gj`mLcf-Bz^,VX;.+ϩu+;  dϭSU8yknM PQ0&gj)%'O Ѭ3|ݲQr'!k#V1&ÑU\9}9ө-\!Ups?}2/\|%By5bz>lz?/8:$4ɘ=zq)@%\~*h7J]9w0RyDN=b d:o>Ȇk-D$S/VbeS'=:t曁_)Cv_gI7`6q}_u3GS/.*=; mY u;RhfɨVV;W41Xz翫tXf< O!zw(sys_PFOoCwj9{}Oc+lWmȱkyD ~qUfM XՖ._Os@u !\84NI|}ϷW&e ߪŒc;m@79r=3VACZOǰ akn̄{#T5Z0:zqqIn؝n;~'ͻawnwfޣ؎0 nXu:hG9|wۏr$z*[d,m'+`F6h6)QxQJ8ބ3R^759&Ƌ#:>͛ CvX:a#h}=ʀ0<3SU%IjH_  9#\@~U݅;st2ohD|D\]*x=xrB _1fY'{9ܽ;Ts3Jltʺ85_+<8ENR_8d󼎅 |'atX1wD\wbak$w-V۱"“£gދs :t™;fF#Ӛ\qqRXz/bŚPiN.'~gɰ^Gϟ_Ok5sK25wlswΤġj}4V c>!ʿdɶ>S9f-W}9+~%u|Y?G9w_u:}_{KԤ?wQWi}k^w(癯:wQ3_u:.ܯqtzN7i֛1升)soeݡPZ[$֛D;LDY:.&51ϮDgf.?tuJE4̤qTTl3 6vխ$f^|0^QvP@VgaZ( gzB< _W'$9? 7ŀo4Kf$>D;EGVqO K>g()ˑ $ܞX u$+fOp?WwF7ӧӓO`.z*ܱ]wM5-a}pZ|宪0EyZ?Uh\cĻbXx$U0 r(ge}?:LӺϲffS^˥5-!C{LD gpgM>$f[>sO?6Jqxp, %LK}W`qW8/޲)aqW6kUc;~sU=~weT^>1wchw}bn N{_>}O_o!g#]&v7;k~䅮̵Qxے=`f7;#cC\oz0{W* bV039Ef{϶-n335#n$ؼ8c#=91 w'V7Fй/$i>kCto^ ׸s簐kmӚ7yI&mL3UMgӤ*\f6ScX5M޹,EA(߰j\1:E;y ߡ!3+859'~۹#mLYY? _scK%àqzuvC!Pϖ\W%׀rxnÊRJBm.?3s'6F|-Wsx ckqj19&.e UԸ|䕖qUӝk@H,*bW0\[]0M|ʴGG8 =;k]-z䂚)JBӧ u_;"!Ӷ~ d6~k1kxǻuM=nmA'Ɍ0׭::uOS={?R=x~[Xߏ3ycy?οs=Dy޲|X7ҏd'6@ ^̓MC^&3b5Lb&7M.^DI!7B2Z)/xEe\p\9HĿDၞ&Wx/eK%6e$rObjcsbOcCEw0f9Rcp36谟|%I>8¯~ %<y^8e[~l1DgE躿cc V7d%7^Q٭s%7~Tk[QN] :8͋ĺ=6T=UȷH֓+V,n%l z˹Gf]<R/'͵5HkLGR!y wHnUr19 ]j n M?>O e<|CD{mY7qp}#'.kye^?d GYk˷7TĽ#`f%®vaXe߼`ozN\KFgk1pϟWVrx)oSpq>~n9۶yo5Y,pPX>O/>e-{wҿſk⥣؎'b}Gaogyu:>)^k_/I =b7ytho9߂9Nw4.N%Jk 'a$:['݌0a.gHixLJoHFŧ-NpV#9ϗ͵Rf~(l뢹`d`F|%$&qŅ:+WAek3zD8? kꛢm>r>=(@F0ٳ6FUc?`mv]_䚐p|3_ ZEJH>U!콠Ж9IAx6GT"tfy&G})V#]4C{'2 WBL=8 :E>R["-VNmV#ܧԲ v*H=vZpw3bh]8 f}qACΣ`'eqd}tI?2fetmv(\&y#\}>fBͺqܥ$@Mln4v!~.ע_j'ũKv'7.gtf?w&yͰ9sЅKk*>GeFn'%Y~ͧMMνNt;f~:j/\ D3$X~AǤOErcx=~sF9|oُ8N-{̽ϫsOܝ-}6QJLg}>W{?sͫs޲iNJ>zm~Sfю¬KF4ܠٝE/v~>fľlJ`$ Ҡy1|uݸSg4AY M\8]dۥ`~ux 7䇏+Bzo"I/8Mj:}Յ"Kvʩ53~2ՙ zg&Gё+guT1VݹF$ŀY_)W5<#^c]}1c{XFzDL38.OGNO"AMܕ E i2G}Xs 'uxQҥWq[7xIFl[X QyX|V :U)pWlP-rwL,jhk8F@ؓetḆЫ7:L1_׿76/ϴ;[vs~/b܆NZa4^jpksB=dz(mcWӨЧZ,!O$83 =yfsFGoI\t}◆ {Nާ{oOx'S؝-);o} 3W?ӗ߲|WGڣf6l\xuJΞ5z=swW4vCZ6;(huع>Sy2njf.|0ﶓiUԮ9W̌VaIN%@V\_YNL3p8 Jyj eع/|w|9~'#GFVS{֟R}Muy}Bu,W~+)X;m}j+1T6!H1[Sv4"pppV3>gWrworOɮa9\F­5o[lD% rS/xrGuJH!c+ `[^I+o IJ5ZO`QؕQZg׹pcT=' Q)ј|pPY1C׏q28Mtcƅ7p!ݓŜ3x>bgVg8Y T2qڐz(=rO(#sQ ~{S3Cr}ՂFHs,bq&IiИVfxqya:|Ū;S2 ޳zEv?#~&& x#c`d>z?_k>n?[._@gcLŁzkܯMx|y@?o4Pp~-ApVGEGCcMbk/?bg^wP{YO]`|o'y}xzf^nqאl%o?r06sqnXUP"R7x]R̛`&ᩘCD+ Nm1`j%t;n䤿&8|ou;*jicڐ\}zNstk77 q"W ")-y._㇛bDD.;FiRC%)^<~So:N\G\!M| rc;XYty6d+%mu$5h憱XL>:Ƿ.e>T*&9^W|bG9&sƓ; _18LԡFi`k8l︃p>sGS"OQ+  P`J.J\d=4&.Jcu5W|y`  DZ&8"A&W;`4~mc;3=]Lr:乳. @:mSc̩ǾίҕF[2x)NǮ?6 {y;#豞r*zխ?%Xp_}"7\\#2)~QA^uDIa\~;׫aGyMjP@^tCG:v9rd ZhOp\s2Gy JHMeأvYscQf~ܔ_Zѳ[e%'?Bґ9Ol3ףGV?no@˯lݚü.g~-ˮSU]MvS"ARr, ,Ű ӂe!  C @<%!I/ q#GlJI-~41ZksvUb荒ךs9Ɯk9nkdz]N5sLE|4w8wdmycI/g?,ȝ.{I=zW]{|gmF{%Y G*&<5)ҋ#/69ߨWߩ}-m5O߯G{Lz Ե>{L6gp.ڊ{s\漘dv=+}!q6яNloDƗ^癘?*ʳ,Cg Ah^[KSaMh Cb!9RV[ek895;$D,0jQܥ)":*c;V}Y+rdOB\jo\(.x8S7&J/뫸d@IDATdʎrWFտh_:&3w0|'럩{|ŭOW>oWm+)ݵz:$[qPz+C \'ˋ]wWZ>Ll\[_5,}GoT,RIJXEv$kꋤӾpDωD%݇cXv\ +'7w.UWԌlWy;x%si-8@lל%z7Po"~g }\C(`l| z>L8'h机?bM;10{L=*V$%>.̸4o~X$⬹8'=Y+ig;6TW}Ӵ<qrp2*a{!_7C=ڄnL“/ ,jP:9}!/fx(Hpl9.F596&KL ^ӎ154NiF^fl7_և+9JcqgPׯk]lg54ĽVt\)f=۾h$cޖӘZ.G0! c52Z;OF"ͦS݊?o+/d}bg%<ԬG}ar}~ ~R{\*/<56l6_62xE7b3[Q^Tr57w&$fm2V.K= \}F,y*=? 5G\}f? r2J`נڕXcIh>*[둼#>Fi]կ؈vm,.{i>FN}^ծg:>qb nCr'L={"NVYmכ~KO=).'X"~><!җ*$u3z:,?K3GٍԘ}6oSѰ&4Y~4Ѫeů::8ڬ0sO-Y]%_Pm ?d}#Q'GoQ+NGDtzۑz(}"`4~FQdY&.;B|כ“Rw} UB1tZ06@q,.N>\riGCevތa2Z'# 1V n)j8SmGס|<+^#e¶!{rxX$bby7M[g~[#cyM:7< V<;}g imE뺅h`/1FոbSW|v=05k\1.7s<`U&'Zwm)mQG;hMIBx-vSh-}Bߨ-~Ԝ/5/;ܖؐ ~e`4@#UsVvYщ{ٰ;A qRK7SՂ }jumAkD?]3RUFqVx& |I`]8+Bݰ ˬ7:!/yc16YQ%N"2WuW gNw=d rgY jlB a*p_S"Xzi_?$GW^_9F'o%OZrFF:1!nMƀznG BGCjmݫz/\P A^KiH8Ռji~(#h=/+N#HkEMp#\΢:B֑i)8ټm:Z hlݙHEIClRāA&G5q5H~O֯0hqW:uCLP:@ '}GxmI}ǤXu(6|lY_\ZџÔSq p|;Ͽ i*%^_k{%3p!RY/Uft꾬kCy` z%P p~.5*hNLu1ƿ~;CuPC@]6ū:\``!ӿK_5%a .&QC:̟q?^L6FϨB2p^m7taʻ 3kE/vk S/U|}q$׉5л[5&}47jRkz+ؾ{Sf `!؛FrXfB˗ŵJ{#=ޔkqHqH-כ®MEp21}FyΉM>皒1W,NM+Qנjӟ IЀ #/ ^G!Mu:Ac"ۨFqS6|먞snǖܦߔ{|'@}&i7^!I&)$D%[ d 9sdtXR!xFO6Fqk[Ԡh8Du%9tcd/sOXGmr:S+v=(؂IxC1s-e@ (4S+/~VbbfR2OE?l)X9lS:檶}h:Ig:ȥ}i=ܭӁ/eg-֢V}kG&o譳XemY|cZKU L #`0(=ut]'Sf*E&Uu:1:_-25߀nXv N#H#E~ #ǘq@C<:ľ2F;C߮83`nB=UR$YaePי sd}| \zoc_ǩZTeNqekk51k>\yJɟ$}ekH/w} Avkavrm 5<]qdC)m yhCTplll2ܰfc;bͱ gƟ:M)2-47mpy;= zuI6z_lxLꑇWy', Xƀ0E¥9p)3:i=q,cִ^;GaLK_qOWڐG?=/V(P&5zj:y=sY5W_qą˪Y JLL䩄Wi܀B(e}5Λ ?erƏ㨕*:|GiuΣzbS^M>c<2Ez;73j(E+Q e J],Gc/)@D'g#qըT.}b7kqlAj3yޜ Meu?N0fj(Y`DьU[k 71~sVM#7Ǻ-ױ@XgY cߒHb5Og axM^՟p>s*<:o쿡Y?B erKEx쎏C 8܌H1g=;)5oX?ퟯ~jq}ǥ=GϞv ͉D;#㺭ͬb7(<v5pD+SntԐ/$-uz9Rʐk3T&7TYByăabj͵I@2̱5(8F^ R{dq2l{IR8zkYJ1zM}%>FD;Um\bqke"_dSz yFu/iL!2%Z :W܂{&9n)6#[ ҂wybX8WI~yTkV3ǮGk?SHjdqaTq,6A>Tc%u>|>U#{0{^mvuwf1Vj5J&"Ϭ5V׳(R2&j\=5)|Rإ׶<H_J!v~XC4=oH,ucҀq%$1.cկZ]SfHbԙZG3^jsc&oaѯ#W8tI?xu) O현+c5iGqAǍ6Z@W9aeW ?سz܌M+_9םhkoGrWׯ5ȏl=/ wxMc\^I/ޫS|~ uߨo煯Ze=2#\彨Ɂ@1]n; ^M᪙![P=޷~dmkĊwákOL-"ۄDTKi!Y>u]'n.~r  zֳ>`檲cl~UGqOD"c4%}=H=iR-) ]&*r=JE!ZRVdUu(]=e$SY(Xxks+} Sz*eAxQ xq+o=,0oѦbgѺNůvu y+N&u7\)N@[{;10#EǠV@:,A/b883u) vZ 9#1#~?P#@Ѐ<[mWk~0V2rLӁ7clLuO=*8 06z][k| 1]^ECB7he&!OTOlcaРNy~Ř(~>^Yr=EHIqzΉ[!H\C|yQ$3O3[ dV2(ߵ󦥡?NFftlC4{qfpc!Vǰ[ߙhj9KCөN]m_P_\ Zdğ Hq?6YrxYv%d|ROf|^H6k AKr_fGMQjɎ֖M1)67f)H~D^8,W09XɃg;Gez7ި9Es0j>y/Z%}ȥY=JJcl$ڑuT ƣX'Nq"PM`YW9 dhk.Np% .ap{E!oYЕ!_d2Y(GW!H\ }XAߣ&9WD٘rIs0tL'-٦-=7EUBNu.M& \;QrdyfuV( \~>V3%>Gςb rzDjBˤ#.|YS`Md-;Ͽ jXRG]a!st3y3_9z!_:S)m}5N "ɷ-2UЇvȑbG})Sc'~KPqbQBc<ױ|p'z|.eq}ǥf?J|^Pf~\4jɆ'o'l ɾ~l!7y#q"PzrY5+g5hji߳0~XhU{_o εEXhN+[5itٹ|w0f=J(35\yE\ g8;:{2UvsB=HLvJ,W}[\xi蛃g>@qTWo*v륒͂"CkYc|5Q]c9I#!oTU8yi88F}it-8}+C_A*ܺ5F3{c9=Y<ׯ\H=x.FB{<{gWu:GhN|*El8yl֘ď{!xX] u,>5J5.z&9_{LQϘϪj׎uKbQ#vZX*4jm#V^cfM}[_ArJDO\wjvZB`uL UΚc82~.`31Q = quuӞq%6ؚ2|竝SLJ5hx_q z\cS;ēAq^ vޖɱ2I! FWK0Nf^3/M'}d5Hu9¼e釰SOV@vٶ:qFS)*BW^?|Zk*oӿ 7Ҁ `X6Fl6 /Qp.w&ef _U\Pp#h]f?ֳ5pzzNE'~YOls cݰqqqUeǔ FxeِpARL!FD9:xjQwpPXO&0jHaK=tM /f9lRj|` bSY(g}ݣNygͣћk{sZ1zK px-ȇW]u'ZPI =g?+PըUNې J|>rbN,9c FYCa[!d]~nT)4zۭ Q㣟zDWClS65>D==Rkp聮-G̯qqV^?q<'?#|c\vctQ9yE !X>R+OZǨ+_WN;Luy~׋_~,oE!ܿ5+M|3 B~\`@[R׏D^qS΂Hj/&>_ۢz<\ι~+ [/:_D/kƏL 8}Gg]wfQ˿S2eE?Uz=z{{U?uN믜򍧧o>U޸{z)[%uFwt#.F'd$Ѵ׼䫜ņ<. ItgrCK{83~v]%*x"K܏JS^O14IWRNUأq<FरN@?}Z,]Q_+(IJtrLrcx 5WrRqTLӡ.{^!U\tֻ^WF=[?/Dq9L]YjQ$S88u݅-S'e 1q>=D}ts+Nm4p2uCn'_BM^W(ھ55Xft|]'#j-_Qme+/+x%詮nq,Z!:U¡1~i:z=<"ZN]CgԪ>kB>?xi{"L ֭FqX`r9H*T )IU~T>~0`ͦ`S)tQIN'_/=n>㗿TPm85NU. hSgwy\1r{.ۣ8sVT_YvްGYru~txʌ?izYݳw[e"fØ'zVJ߳ު״avuvT!m007yr;wZ}ţl5b}y|y~z;Ɇjuq([\5w6a7M~;aU $T$f/kc!VrkG|LZ1g8ʕ}A|ӀJ .LO^`S uU;jʆU9ι6_||̝<L /4Žk3DI6KIK\FC>kd #_4L/N.fC6y/S>8;~N~}A೏ܣnm:*T[C^|}\THc^e]KVß#> kM m<կ<կ|b" NY4\Sь !J\9q[)Uz?< $ a֛U8q˔r=f쀆ZRz<~)GezHre|]з VubϵONub7;:PK=gnxCضF-dYQ&31TPhHoŇy79eJHtQ"c07qQ|+tZ| 6I(OUWjШc8f8c%A+v9A|SNlCx{Ȉჰg8ˡ/@ú^NsMCYtWϽDz@p40@u 6~BzluT-}vYR/:K=zw#4.>ƣүzL xs)]WFowfz.]odR Z@ hY9ח(?~]J !*. x4mSy!ן:Ey=`ku=G8'>Ye݆kq;n_s-nky}- =,2Gno_q~ރ<~I::4ks򋙶󦓝x_O\]3wOG|b Tgiu;YG~/wmo* U'Goև~Ϻ>] [~OOwXﱞשb)Tޒsr=\+.ϸzt+"e~)hig92ӿLf+ޚ7֍zADW2~ٌfyxb^=t4yeh' Eu~THp;뭸0q? 묜N|v*;7ke~~Š]~yЃcΊFӣlw(::[|,qyz+<Q^ҷgR+хZ:R 멬C?n ]*2DQ.(It$V!(ߩ/Q r,VC[i^=c_JqWeZ7޿~5#qߦe ngϔ tV;f~hv7˂7xE7w{{q|E:0D|HBo49`]42ڛqx1֎Δ"D xٍ r:lく=FIs=tZppc_֯N=sEf 1uT\5.ۡG(k;EXi09gAZEQuyjcL!wݾzο3]W>c헭O^?/'O6'MWWja++ 6E7L){;]'=dcm9rzK55:1ꍃ *q3ǂd8}܍״O&/ .foXr\8\k>>7>~[Kk9p^Јa155uExeKOZ>1$Όז< [Z;ok7r'|߃JT8>2v˕%aM?L$'ݏ5yg\[XI^Cjp=~z'uQ><=#ff1N_Yo³3Aa{R_v^L0EرSpy>ih{\F+@pz yL6]Kf/[s VV@΋7h*X *ycnln_Lzο/@}1uI/w/iGgѣ5>/[xeο۷e;n_ʹ7V޳ߪ=Q<®-o7l(`g5 z<yԨӿvoۭ2_ԃWNY}1?ҫq\2\n!aNF|]"Ñc ^>TD}˜Z^NbceuY\W,(˺ExF5.Y Z)qg=h/vC3'n A "5'jjGe^3un$x9xY#Va?u5WWj5HnQ89\A&W` >$rьK/Ve*!4аTb1[ӇxήuE0kVĎNz] yZݏS%yV>d+d`%ShO3s_GF%YA@7VÒTǘ12Z?ߡp^wh!{Zk0E՝^qBI >@{W6Eթ$AԬs~-یʷ(V^<\)5CQv*Nsӟ9]8 ϽN-%obX._aAvDw[ a;O2 iYsz"s|}j/D~ڠ.a%JZƗ\CX V~͢|qph2GэݿETIQh*?2"\^(f!?Xmrؖrn qK!Zj|1y c^Ǭ;G>YѣoJ\Kz#m]6~~`OP1#SoԀ رK˞xMNS?e r_ϙC.֜:k.]z6猌grſ/qvο;0mct2ކ}/&v{ǿ;nw~Q=g/oQ&N7l9|nJb^5q~@Uz YNl6\PO ۞|LMeokY庿+Ae__%WO_VT?8/iz.(G֊gt-% ùLpj}7)O2DRqMs{2h Xg([NGt}=Ê?K-x>w XmzImx;b$4[5HŦK}8[ugy&i"ٿ;oS-Kp^C]eڦ=u kbk;YL )e8Ύ~Ώ$ k}kGrNu]Erq:Yz#As'vGgyk13~O{mfM%TE7wٷP]_Ezο U$p+;n_Is5l}%հ&oKbaxxD罓ͅyxczgD@IDATskścn;j_03w5;A B)+N^[izl8օ71†S5w-X-k> e w×0yƹLZpԐDxiάЮͳmſ-%|w[ͻF{l<3،f07E3:/nz}[{,:8οۗrnE'%ܳ[O}UA6[gzGd0΀7+6M|IX%[bkoz4l)Gq ޻d6Č̟7ؕ\`sx9=FGf=p\m_&ϢS5XEop j/O믝ӟ Mu>2 .zqM>$fF1'u;c'F/2&lyw/%S'F ]4yƼ*Lj=*@> մNԴV*~ȍݎZKag,&Щi>M,szɴӯ3(W93fa\ϝ=ҵ !ٔ8iVWiG8v32Sk^L!Ԩ8W/T0Z / 3J,,]4y#j;Naɟ=&zNK`WK_Γz&K=Lk=q pٖWBu}ls9saߐ:c%Lcؘ~Wr7=Z,,狻%l帋_/:_ǝ.{w_so$Sd5e7ͣqſܛG;]M[5-m"d/ei?f>bY$M1[%%kݱi.>lM"`xS΋$ԬNfۧva0 2wkvn0*c4]o OKN{ ̌dKWV_5 uf ga1N_1i˨ GUc6j~qҡq"VlojOJ>2 jq]C2҂0ƗV&m}\dW_k;i7iPLPG_NC,WqjB_ü:=W`Ѩ8TL;u5F3QzMO>AKޥӞ dStwH|$d|rZphh&'c+ō~Rt[_tjf] q:0ߩ:|Mry`4Pze:&vX6ѯ\c':c;x?Cu24.OX F5Kwŷ<|%@lxuCr{24ʧ"C99zu㇧?WS\ʈ\r`hsnFpJ?&,oZ3kLOt2Ȝ}zt_^N;Js|/.zȻe;]̗w,{w>ϟ=?Q;~k'Jsbrq4(^ȏ,056?^coi^,%!*5gǠُ^Sb85؛<շ}˓}m^@v >߷?=ï>=}jx6YJujZ#}?Kf>G:f BDu^"Yp^Y?m}gӹ܏EaT5w'-7;1~.U4̰ó &pƙ]0?t^}Š 2<+P7L*OHFԡxó85umgz^sUHٝ; UR!ZﲵC,ňyQ9lVV{.q8ֺn%xXq/(%fK1Ws8T_}X"\]`70+t%Q^Cs)LSh2i+|).zK1 Z6I9JiI)f~a bՆI\[6J1o>:}~ _877ooa̺K~_jYhk7z~>%6Fɕ0_WM+~--TWvHo?W n<++$= ; dϿhOjKym[oX6u;8V.8y+1o<& ,~X~ld-1=Ɨ>nTL=#$~4܇'x#e& o> ;&W_xx;U}WkGyqbRk]mxkDwZZ,C0&y Koh_QI\ubR=8HZ/rFU⎫B[W;ƗxH "|9.>vL%B0=q}<3m?j=9[o9 ѭ9Q<51AoѥAp =/kאcgt`(-FX~Y)r=pNsf)h'8#} n_ط$&_|cCO&hY+Ow1zéL)Ty{Pۜ%AqߣUQ?X/@=mi#j\ q'vH Lsn{b]_i#ݷYX͓Czelj9%B~_=]} >YǝRƘ3B>vHK`a]=3rq:oR4bnR|wrxPQ^/ʿ_Y;EO5@3|`ו_Qh]{֋>}sU)ꁾ ț^,D%e64XyiA1P(O‹Zܨq ~*Q75%{$I5khk*U_k4"|zXgIy?<׹\/mi}zR *q`$sA<[r+zIq z^@5/Et$cM=#\mn)b :9'zvspGcJB&ʫ{ۭyܹmόԩy(nGEԢL}uJG)>ce'ϡrIF L/ (TT_0tȢ׍o#ȸt_ ݞ{0kD:zKȿ׸(c_c7c <LFfBV=clN}M¿h~K|sI'Z/q.{wCrX#6e7SG[{]c7?ž rX{*+~է+(6.xf͊qoZlKH:Jܲؤ|$oF9²Bgn軞}R.y`=Thcykz`Ci{ /ۼ݌ߨNdlФ_9s/W:_.Cƫ"YX8+ńbԑ¼U1Z-J^Q՛?7I;_c면ˡcO"(k~ ~)߈E_eG}A\@Ê`Nl顫;I#"Cx 4%aO]_豞Z##Z=I덾gGޙ N뎿5`I4nzu N&=?9+Q?wH\+ߟ\T2%~88ΣqN5~qrӷ/.f:?sG}1;ޤ=c=g(r=56RZ/+C/3;w> =UQ^QO;?7c7?Iz鷯Կe懟#-W<=Xvx3~x8f+N;=6>S rZ4Ƈ $<ߜpFƟJ7T%^7G=9|_GuӼ1-I7ԫxr3SuU#^xo骥ꡢ\5jqEvYJ /?L똫o.qjQyf5)_ir<;-]HCoȎI%*]68=vU ڨmlqh*k!1O J_u&гS'.Sc;j?.lAױ U]S:i'#W,xE\.>*B |S4Yu z%~啖KxԺ]ÏS|s#?Ȣu 4דϜɱe|3ؔ9 >khqZTޜqQSL6OCR8UN|sSumpŕ& w[FcTH!#|M#y_>mM;z' a뒾]i\i,QO0)L"8"䗗).i>2!M_Ǚ.x.շ΁ @"Mu`1S{u ^׏X4Uy>6_%CWZYIvl4Yh{ZQW.kx4_">/,u\j6s3_s~ozH\ WI՘Dp\3[s | ~FG4|tkY‹)`S_{C2 3}Xii?RMmF){p Öcmcd|NecpQ38{"F{zT4mNa|,qbCM> (m,޼CΣ(cm}~8Qd&.-7:Ey\mc+|q[>jA[|V>♯6 x[|盁lA"_8ot ,fS٩ZAdDD_W8싂l#IpJn+lmFǧOHR ʪ:x{jH![<Ԡ0~(@1 _ĨQPtbVӺ*3z^>(􋯞>Uߝ&}"C,65~s7ٷ0ay?dZ*2/S6T PRv{}2W΂g ՟r |;n{ ̟ cϏm}P?8z؄|4~W6~?? z>o_w|?I}.UzmaFdԔ iUE oʕ/|su.ȼ5≻LほQe jЋD^j> ?yq{,[Ԭ@?{ m4v7_e[m~ӣ߮W)5,Uz8rV~aEN뉽En;H!ldIVF2+#913l:K?܏^==;q/pZkzWJx{][>r}%>4q ʐ> >[!J`NAZj}->+K fޚ± #Q2bťV\**@݀2p ֓/Gϕ0ǂ25!=\/yT' [чbxK;sbmSm~ˌj^ܧ9WM~7hiı&Zȩ~[PSf3^MuR4.S[2s<UzQ(6X}/SpbvqRdk*"rTrXOOxة#]׍i蚿_~ _Hݏs>Ayҟ50k'~8nq-ʑj+=-EV<4H~5-%f:~"Y6!69\'A@Q2+NUOιrS6~tL#}m;9羁$y$;EJT/IUTOIJURTTT*1K(Pp  .=ܳӿg1Zk+ 8wc5Z6<7#K/W'|S/DԷ[>(DU5It1,b:_j?{| ץ?*>BN/{e+L۴yMu 'BmW[6CW@[baS ?q>'9+~l,ujB&8^fz麑SGic'"߿e3MzL] j۫oezG ?xŅ'$#Ж¤TNL ~;NZ nٳN> P(q VXKGڅ:|JW5C:Sla$j|%( ]Wr!wgnP[ "FR-}3VlkؒG''Dž|ɯ]dқ$N1QU-^_Kqty0…⯨L?^G/n])eHel|:B)׸|Ʋ~"/J;\;X ~Ӻ+ C\5 c gIYufPXW:h<4ftH[ysw=={sYj]_ (ʬC|qUZˮ1M=w01a~uaO\s.ub9<ԛ#/j2Z0|b<腺5u>8S/\=^~vɗs݊ĥyZ/7ui~SY*t~'(Oީoo?U}I!|= 2`~l>`>:2zb,[7N`jT 2թl#^?rk%IOs@_Q䉯^I}o*`5xԏ #EK|sbj_ӪU+pS0zRM|7ݺ2O~sFXRŖkxZPHcoZh`FMѺPuJ$,5P5vIW#Y׼T2o2{ra=PB rTgysM T+ {M;O`g~zgYXC3)/kK>FDt'WCy0 }Lw߹ę|s,vDq<,]rt╳kPg_ qq8mB/Tmzf t!ג sU߰î)R-;<'_N?ޔR㛭;S!d?k=;×KvKSc7c6_sk"?mhn5s~MU4q+ʯ|>pA+/k M("a_l-Ez#A-X;NW<["xvRw+LP.up¿r3B`xpZ #t_yGO^,rG=γ.j=%<9[Cٷº} j]ζ1d)eȶެwpkdV2x{b+Gl \ba ! XrY =lE6;Uyi*`rȋ1P$|ĵSA5 =tyn&IuVHr@E.6Ь 1p\ʽޫ⿅cc9xqE j5MZ=fXdVT1Q)|Fe|-p3|/\5F=7sd-Nö'i + 2%3ˠQr|.LΉ[)񎤾Wz:>Ǭoz^J]z;',1ky/\|Zg6߯ދ_>͵ٛYTޫj{BlN 2x/}tŽ`Oe@p>4Þ WqmFʛÙhc&#ɩ,e8 ̙rM`x?u$d?"r)(*kFB-g=Y>!XaW\8t×:4aoYa8L#f0-|YEf-r-T22Q8(u d.saS'~̫ŕNH';䇜JJē?#GW҃[ï+/p>_DX^MG ]j˵яz#8y iʎK,ɘlb=~H!3۸/ѓPst 66Z:ߩڒN!u%XZi*`OBl21yd#_CA. igf7Mc?cBZK3!I^h>W+i<>MCghm(z1|?'T,Cѳ G?׏/>~?wBfDy;D8|z.JCeoio<_6ė.ͨ,ޏLS/nMv'O%+BhVbX̋l^Ɋ+_Fαkq_ ּ;@3##mELUHt,8u1{u5;~̳ӛo_1zy<ɟעUchѡJ]=H'i_-z ?ዳlzFw! Ibџ;ԏ%=a~6/x.)k/J4뺶e L[NduG;5D>@U@M|&s,*[M顫#~ >5b@.RF/1gtQZ/k!eFnMX%_·g#zt-PH'aRQ.U!5pg #"VJG9u@ͿGzp$7I=aFQarbn11#$x0G}*Y/z!mS@V^dz>4a̋oH*O.E:|c u|"9t؊b:,xGJZ89e[9SzOO>4NW3{_EU\u<{_s3sLP읳cFͼ:ۗLɋ&~㪰F[&:bǾMyX&({_Od~k5wRo*,EG( Ek&?|/n~_f[" P|)/& SOƖn I72S>٩ Ɣ|ys\ o7Rը'M>נ> ҇Lm#V Oj Ͽ\1~J]2E+靷ߪڞ ?8}t'rU*߉0/r~:!]fQqعM>r s2Jj/&+Yv&*Y}R|񴔳 ?ӓ,.TT"@;P`nwA5g"~u~t?4|茽j]M1dg>܇du9\9Ьetj^Orzo05ݚkT2IA +/]96ZG1E&QN=#ך{ĹoP>[PcȺeչz[+۲Rvn(J'MaoBT[ddXt)9#获uj]b햸ڥ&եY_]UY1‘yNHٽ^du+XOwcMȑ$\j:O=;^%a:^7^<eEZQVU͒j6„S0scW}P.3%/E~{̗rdceu'yϥَEߞw_oVXGB]0] 􇾯 Z!}?T>rD&ͼŃB?هkg\nͿh7ιёDu[2gZ;EqCYͅ<2Ò_ 4|I ^y1c }Ƅr]0>Z|7?{Xo'O(SON?(@=/:~ɍn!ˑCkSzV6q)߰%THL!58p[;׃)\ʇ#\}ԖГyQ:9"$`p=qS(Hc ;v30[RhiJ:sF+A)x䗡q_u$Jyk>mљX{mCq+Sn\?(ד\K`$ֲɜt"&s,1Kwx)$(׆?➬=Mk? Lm_9PO4.*+0ܛFYJ&wbӈiVll_O>vu?Y( I8<ט'nY$#," 8'-~ky<sI2Ζr[@pލ(W_e}|,z۸r.u#^YkofWys)Wgk>| f\ռq?4}4w}~U}Ww][ TyRA ֓=>}|B64zCZ7e,88ȓ ~BFm}S}nt̞|w`k,H}6ubdٰYnC95V u!gϯן;zQ$?8՟HsM~$kP k"hȇ E/RqГD0 3ԍ^ɒw8ɐBPwWi$a[4?b [ݻ۸5W0N gjZԛEVdaQVؒ?SvڴZpzzW6\(ѡ?X̭{OYs>!46˜jI :*R/DSZ@g0۰S:{2^jMH-̭B77Ľ85+u=k2 (cZ' ;a_|B 3d;w̮U" O4hp?ǶeGG&Į3zIv.TC`:yʠ= G ](:Qsl]C<;O/S,Z/ "r8M fM}N}2aԥ=mLWYѾ^LOY+v]E^Ѱ+>1͔SfqzP7O?_A@IDAT_M?y6%[d"Su{_\~m ɢMѲ>[qFXySoQNTʬ[n9'/ %qO:/Èg|P/ctM-ćm Z5w^!|zpl^ ʀx t@I_ & }*r,j/ZBkYia\x)]K.s8 pxr]:p@NTT=Z_$o!qHHGj/ /), k˚S`/ +XtxSX?uפH>S[ґ>:.| ~5o)/ymh}zETŨݍ 9/~{SO#Qq0:X/Caf׬dXO~qC}=!J,`Ɠ{Z>Kga;ZwgQk J  }'opmuV]:>&p U6ߨԃ?U-?<{^E/{-'tęC]tC__o?yF/2軌?/U_nS/9غS .\k~jׯ=־cy~-{߸W(o򤏏Ňoԗ~'/ DMlSxVھ vE:3Ek\d'exT;4S`[ f&6pLƛxFM[`gй땟_x_Pp^<UW8Z}DO<9֋gv~_eJ6^Ț@c<HeBijk*Km}-%C?֙_:ݼz*O,f vtD$ r^6 Y8yȚzx]&G*&zy!X.8+3hPR|05x.('H' 9/VYdg.TCӪa^|jA&{A~}k‹$XU1v'p%)b{PG'qDpH*3ïדE.Cf 6-=-z]HƋV5߁8tᎯ?NO<Ҽpo=)7'ϐ-T`:S,.3Iq~ ĝDŽJ$ȏsW?Q޴aԝ,O޿o>Vwĭ:3Oۥx]ZVtCg|w$';%KIXqwv`Sӏ˓B ŒP@]Eܧ$> @/cT kd$d9X_&rPp\g)̿8F&tFi]lu)]=,_oڽ*bBS?M`99rtڣ`()~Uĕ+gSk=.mkmmqG~*Y8›waˀdߺSI6F X,5׺0)Ȗճ߲̊5:,cg:_{׏EW֓l.AF!|:]9KpZ`EZE/‘ JS9`E}ٖiwy[U ]_k_~qz= ?aLTYi@Űu.H׸hؾ?Ygb7HU0>dE=}ꭺgK dUуHD?n1NWzGLmQ+l!璉^WQ5n%|3V)l\H(. sc#Wl^Q]O1,oTɆϨS PD Ksרbsu(\ W %@/WBͭ4ءt=Do~t=o\]_1kܜ^BົCs7q%rYO1 7wJuǚxw|sEO,j8 'Aq[+N3~q|OX'I$e5^%_UuJMё_;_mW}{<"/w]s͖u> w|؀䐯}WZЕ|25oȲ^ `͗.?EE_x0|xN裨6A5reb=Ҫm/i~ÖCR ܫ%"( c>:`MVFF*~Kc l`u-V2XOrtԞq]/$'T4Gsk$+y,5ΪMwU38+ s>sĹes˷*}y7 / &wV^pI:F?IFщ-]ǓI;g6zl?R-9Ii$ƈ:Nt+~u5HZCIʌKP*(QN.+sH>\^l_9v}2wtG:v]-˵VFDd Т4q RmzoLZDa9=5QcQ[|5D6t#2Rډ Bq8@*2X=9n9ٕǚuk̚z?$>`L86 z{Iu"O.<߸.Z->G^aɿosXn|J'L`.E_|Z;$ ο Lw]-I߬켻Z;$ ο Lw]-I7wjڭno{EER;Y, {M79Ʈ?QUPw]R;w{?:8#1ػa+X/\O|1Qrhtm:LhfGg赶_y_~w23Htwqa-vLS]"^αďYoYlmX N[oh!P7Pu|017q=_;4cF*cA(4oմw׺.Bc8S"y~M1<@I^J?p:ΨF7Ry̫ ~ LOlL#W )T]ʈLaeٰBɩ׮u^b-5^ŴElo6La62OB]uTiXl7@D|@2IYŵئcoTt91MS-w8KKߌ왒EX< ?Pd_ Im3/A}\gP15߬/O'usj~Z=s5=Uwq9p{hq/&{뗋)YGٯR'F̞q.qYf]F>nݥ?.vuyvA~[l|-2KrK1-yvw]sWOwyosf;GyB󋛖H7ԢYjʖ%gV*zy1-/4G]E;_CO~%lDw,KɃQ!:͓PU`9kv}@ .A|1 W )luǺySh7 !Ud| s_AۼW^MarA5lu|8ܛdq*ff+=pKNZ/]+ۮj];Sѱȉa]ڟtE3˙Sh]m`2F?`Y 4BOk)W𓯏= ׺ O$sK:o?C>Hsk[OqVo!~u=aN۞:2MssD-{~W0gr f%ܥ pԞGl*w5v'.W]6-=${%=A+fK_c͓'rſk%.ZK|߼[u3darˑ~&{gl%)1v©|])B̼9&%OMƞ:gNމt=OfY?p.|/2Wa7ӗ*dv?xz"WX<= wԯIR VQ"WWH/PpLF9_v9;#.%)(:'ߔt@ƋO}Q"r~!9ЕOz h1?ò(gNk.['Hv Nޒu$s$Q/q8|pαNq' +?oZksd>SK:JCL8;2:X%y"+8xяhE`w ;ù+.}ǚ/1ׯ|kOW[L[Xqѓ<T_7EV9M^>hqSjR&߼zK= aؑo մ@+x\Fɋ|+¶|߉F9 Jo w981߯7։RFr|3W_]:9KxBv߷M?<||p"_^1J)z?s.s3D^Bſ/qĖAUv'.>rJdϲMfmcN~F[;ct:w.84/U<* C/Zc#l GLIne`:Kj|=:䰞 8Níu0ʏ^#HL&zny#rm#{c{pf]U P}a A}g32\G3"għ@w𩧞ZG_5f>XBH`TkbT">̶U o淕ܤ0f?v:iV}y)Gf U;?!^rdME(?J]O) LMP0etХ 5xt"9j'3A;&;o<}{Ͽ? ]_U>z?x~4:dvDT~u$WзʴΗ Ӊ̬{ΗN -ceg.B 3fKCل4_}? {xhG'g?{=f={C.Cwn_}O7> [\m, lFldvQcj_̍|V=ϖCy㚰GMVj-wY ]> yޜزGm׷scu<(^[NOp_QDgYB Pu&j,F ;~kc678qVV]MR.q7Y^ ";w"G.%R-> %Q]59xlḴƐUh鞉fٚa 7+]ׇ>W׳396ޡ&Hr `iB\7:}ZH~Hjh;=bi\\5:FutiiZV1>x:Ad#cz&uS6ZB?x;$~j>Eջ+^tǠhKw1~ޮ?[ٶ|nG~UƼG#÷OX{˺#)ʴGgtELǐC}3jU!7#/_Rx=QשK4p1G]kכ9|?7+g[u(3_A6b2IO Mɼ[8S гѹ~^G55D3jiC#"5h:1?5櫡~o'3mP}moSC*HH)#*zϥkJ֌HI? N>({~)pHy~ z.ɱpW?i k>ğ «Krƿ)a~I`%84e=$.u*9Z@٥R73~›uI 0'_ +0:< T6Si u~'pP^@jmCy}}4uY2].}Se28usK}xlu}Zů|¬~Qg ,Ǣ^9<'5GGs=t=O?x/Ӻ>|Zc<iDupu|e,]OZ׵b'Ox7Fkr]S (|g μ^LXYTN^b?u\Wqzx8J蜏6yzx"x׬wĬcVKka_뽤?HFtk>?cF7jJ^UO<]_C‡+A'|7bm1o=OSTx1R^&OCb@X=,2ݥ Ocu2% 陉 ~I.(G?|@WY%J׌3]z7Uyc\,--ښPB;g7%g_?wwVB⁏zJuNaoT%YOu}1Sj(o=\Nt\?OUwSJxV&UI+Y"a iFm=?'gA:E*#r- _SoNlr5̂4O#?&-P(9a#>L| ; ?C$jDq24fH°c w|r/~2L#D>zq=Q#FzUoIZYr1[8q@dh?ֳDz/>፼O>K;6ͩ^ EGH,4:&d:&Oj^<oS{yq[ Ƨ2%iA'Z߁g?7ޮD@O5p=]ϼuINV_""adw뢶vflu =! ^/ xP;]K:Qz; Ŭwԅ^]DZϝ/;Z|Cʊ,~,G͙o k4ǟHN5Xu0Fzouu=^U7+_ ?~dƭ<=}[B>c#oj%'CsTjm4^ۘ34bw :GN,>b,%:@bhҵ>ݏ+tL<5>XV g*$ߩ?/\Ȇ߮]Tr| 0Vl^^¦~D}D0ₑS%myt|R(>ݯu(ЊK`*)rjH6;Ƈ%zy@ :'yx85h!M?5c>%hcJ/X" ߲z qw4^(2+ҺCjs. hy}QΒF~Qon^iuљK&|ZFԣ*u+(֡ӨB=JE#׏|z_aF&uu+j@&d=^8J:hTg'ORRq2vDSk?w~e= 5: %Vh1̀ZuYc8@$ܓ~TvɢuNq}d$ ;rHYfk'qyXO VI{ǷGC)O?\bc=RW eq//S,JK.zud}u؟tobou~F}Ӳsp|/9L-z|Ny}:|1#B?:+|oHtbď̸F+y|ɟސ7PJp5<5zn,Z:_?'~Wc57>|"]6l&7ͯ`aʦCkc]قgWzoxDiH"mWL&yMW:ޜ)#Oj@"?ʩ"b?N$#u<B,joܜ~+}g;ߊ׾~_~ 1ݓ趹'ŀi˲6q 3~;d,2"+97\?KHſ_SN_CŀЧRz\S 2h;*pSɛ7"(B_Y״N+|ɦ4͗xzkkd'gK? =3F!DV j4~I)6xltØr>br_֩}]Oқ!c=e1)oY~WSjA@4gk>1 0{_;5kzTQ)(G/W_qN9K8.ds1@%EGV6ho(^@3;zdK{DOOKڨ~\k/ `FކA.CyWʿA)k"taǯFObҵ WR~dObk~/#€X4"1g~o۟{M#G]3e}}ʝk5en麟_~uXp - s+Z?V涁PkaƓH+>7\o>#ut_;p5.(kxC^[/>/ |g%,?^m=o6klsl.HmZ/?,V\o[lsy/ LxثTGmɤ 3`'n\~ ?c!Jge/7y8?9LR42Y@>}x&D0e׃&O?7#-.g4ԓһ_W³eudǺKx;0 u[W"3Щ}=-b~J(T9Q?usbOGEٚJIW7©ۉI fv5ތIN:'?q?]÷GǗLsC+6*8cv>a?۫$6p3?g#NH# ~: [ UЙW~i8[ct&u|i?0eNJǢrCG~ỳ~>_:5u;૮|?фwM~.|cBONx|gf?\o3WMymVg4'=% |Ⱦ_bk 3! m!_ٝ4PN>U+5_o1~~g,&f#?8*O*8_V#տOi/\;]ڌG;PlEKf*L.ʓon8q)T%ybV@ϑѺ7Z-I~"md\ ?"Ow!YW%d&H_vT}ku"=\Se4[UZss9%ȯtY>&Q6߳pwD{&ZsiqDz jD`8^3v* uǼGfLb|嗀,7MP c݁ yXaıkAn8'kię| ; J<К|F 2UiorSM&f~Dr,F=;$EvᙖT˚6t|0oO0Ye3Na-vN307П805Ai]&ՉL뽍S{WO_k?:_+/%.z!)zYFϼ2MD"3w~c~Nh)腩]3;"$랞`X^:Zz*SEzb-[wMޣY$>':=$cO&{T՞gCg'BҳIf/oГZ5"~\,&?ŀ_==;iyUc&`[)Yg)֍y=v?z9ԡ ѹZsXswnm^%{th=I)T̰w)Vc:$q6k)A:Sּ9>_{_n^\(cMk^9x3Ij`+U8Kw$Ӳ$0t} ?D j^6_n 'hS3F9EpY|$OGR:R܅w n9“t˟}7~Fg^G&_:?71֕..,BAz^^#[gBױɕHW(Ρę+e4[.'n 1=5IV?Ia.ϽuwV>=UW$=m/0ψ)|U{|kpPn˓kyb>u{Ho|YqM~͓?6Ulu|P @^617-o{NXUנ]VYYX`7|5Fj?0ڤrSp,Uoa:&vpLjTrDŽk[CUͤm65g@Z~ @#zXv}q9g~u>@nX̺&ǯ'CB֙?O|ugWbz1 \uM*r/A!y}aܟ$x` &٩l?r*M"O2,"Xgcq^F?fKi#5DZZqLr0~%%K~ |ʣ)TM- v!4xVgkRlOsbrTit y ut,yڳ'"g ґh#z"q:??Y[KE@#7:#'>he~"i>r\4roNlid f^ =:g\ٽ KGHvq)dìQjۣuϹ'xWƺ&Fyi !g/ۧ/v_ydg_gOj_:koO%vs&*bSlQDs]ߑ_tEp̩=47&ztg) s@}BLyy'^ǜn{s}F=WYWw[\k1)}*={z4^ԇc.4䌙7:ֺe1m06g"w7Yj[kŦ(ҐMxUTPک͘X@RAvݣC̽UwždT%Lj-o pFh>9mxL,1'C P?}3T VV#Ǽ @2wЋzo[8yلzդ>Fggy6cTx .󞫎gNsȸ̭ݢJmk(qu*zunsVǒ1vZwNMnV|t>V)Rty6qp8J`25:>S 6 Zԃ{;p90}׸GRµ; z;<V`lK> E hLnZL8f)'ր7%_x tv&2_лL rtDw==g?]iׇGVuՑO_{>\]Jq>UZ߱>#»]Zc\9G 4#޻Z]a-blŗh HȏБcbVHȇIc$'I&ɱY"+e MɒHQl7s1\sVݮnΞk9s:ܺu/f#kۚ@Coa;Ŭ3aăz3~ j:#eܺe|ze;X ?J\]׳>Qcx뫥ͱ1y+hlL*69Bͱ"̎VK\Р0HcP/&/v6@Z8Cq`]a)+OΗGe( {! ]nwoI25<Bz Q&= k&{En=Jd?3D^vYkM0&I;NZkyy\6]a2tp|a 2pXw*HZDs)i\k;fo}s뱞^>?>{4yKGR_|-~ӷ9x-w.f`{E\7r;?WnO>嘏t;pg3Pm_ ~O$ħsq[sO,+Zaaii+ou=w!fd)"^[rkB{ml&#"#x䫮Qpld!Yk>#) 2~D|_횷]}ﻞoOO1la5AsE}( : ~; PaHQ^ה;n'ro-MnxPn_x8+8 -Sw+o:O q x䇰Ճ&,~щ'?qA1aw,b+ yòQ8FlRd-\.en޸~~Tr?_ϕj-8O@sL0C1 õ<&i8:!"։X+M]\8wPL_#?ۭ~{G&fr}p*Kv9 ȻSvQ7p} }_7 Bډ18 A9|<%EE ;w=dǀ:(%abV;YJ^W%t1즦MRA7Yˢȱ1thb=#GCg/Ziޑly}ӂ(Ki)Mt.Ê~u̪3 EI6'KgՃ/= awǬb-낗yFqX]öa~4ȳ;gAl060 GP/of}~NgM}2 vg_:Sc~E kCRE,:\Ws`+ix# AɃt $H>3s.T{GePt#9잰zur=!1u]<z}iX/eDaWnn]o﮽WbgĖŴ4Xbؘ4¦f@\ym萭m=7ƍEŨ\YF:W Z; zsrqp~VUj%Ngw|qU;>}1X'3cgjx޴^cXúzQAO'^GVLյ`3L~[q f^&NPdN$v1ZCGm-~ʿAyoǵ<._'z^z c& mhXs~@O_;Z}/v="U zh 8: ς@u y@"~}Sp=O~YA?ΡcO^~u8 }13xJTy[磘$)>PcƤ;홶tu5`3izr_-ik#ē׸ l`1:tĸ[KN=7\c0Ci6j 0RL)pqOPxk0Կb¨Lfҕm7v;綎v {_IǬ73'a7AcUgv\x:zZ_DW=4\ŽN\f¼~c#aݏfc%Eu]`/<|0:gMt&p_3iΆ6C4=[)k{U蝣fӛ`?QZ6z8M' myn*8F('xݗe&,[ǃ- .Gb)6]J1A|+N ~2.UR_!/}q^7SzCcﻱ{.pk/,8;G n? %y;@|#/Y%4 (x8%}/{u~|C٧y-t"?yX<;$ 똿<¢% y!T'RKr[K]`~:~:`6cOtee<[Am:Gp*,pLpBe 9Z c. YX3 ayM~$OF>I9 O.\,0Q%:>ybz@EUu6N O\~ׁ~۶|N'Q 5ڹN%o/Oug O 5Q^c<5g{kzzyz@/Za1ξ-`߭@8uv72z3my`pǙl~hw,|7ޱya[upW/=|?(odLaP;G|M'ǻ?}7_|Ww/3"{O\H,~\8qî128\ZħU ăPR(|nJzI)K/ ،cGfkHz+7s\}> ¾&f/$Uŧ^n[_oO_׏iZhd2ct\G1|B+(;PFr+~g4Q?笭2~#kp^Xkr!y@p=xG}qyQŀWXX 0ӋEi4W<}eR^?4o1h2w-`kڂf73Zv- @3%'Fc#85O~_Lql~`VPV1[:o9KX]]q)4]J؄t5ވ  =?t6hmo{\ڠQl Lj1$RU NbQ3lȂ6yܓ%z_]|RC»x6DHEAC @ԮXb1O?' ZyEY7 磱pY1Қ; )`%$/ԷeA0H4Wo6WY:f7,|v̨>8Lr1H uDXZlH(DVh*kGx5_$L$Ndc'p!lG{Mݟ%h^Z@k1e KrѤc-;b'\\:[Z?G 1fcy9P9$cְ s}(=PX̣{.c|FFS1`/d/1˩Ke@D=|0YwM Aq)ʿ";)3+oV?KAVտC?op-!~-``{ԈhsӮ {o'/Qmh/Of~OD kTL|apI͢gPf(}ITo! yV40+, +Aڮa۟ݻ7'//,c$AyV,?"p5(qjDj6تvS2u2x/Llhda=HJ9 C >ћ82&-~jR>z66M;^S#0 )Kg.  / 1(7HHQ^$+ZUlH12@sЉZXJWZ6D랕Egy<!]DpӫrдK=YE58wJ<#U5!z<1cfw;Hj~o :g\b yd\G s'{AT[|>$7W뻿vgj?KAQ[?[+33nLDs r\yT1 ae$R\HclӔ;.Tp<5EW:Y-4W#v^lK;\ߤ6_i"ׇd |qq<֮?'4D׿׀5f~z- >?X&HzѣPy}<c]Ϲբ{b@_'psXXWk^71Xh/lX̑s9p ^V5>5\ 'K~|mЊ(<axYOyHNGKb= '@~!5^!yD$z,G19=Kd`=S"S yz.K?XRĐB%oB>"ĩVb,Q*Y H!ai@c>`ZeFCeh Mf v[$ :>!$ Gup]ќO:c}ᖶ$'dql#`<B4sth&3c݇{**A8(A.jo8x=Mvڒ_-w+kk:9eཷ(e:LΫx翟ۖ~VՇwћ6?stz|kVޯ|wrGdA!^ԺJc!cl{C# cColS//WXKnyXšd #7W3(cXV7}]ul߲+Eoٕwj֘ul|WScnWߪ9n_C1O={BÍ {KlXx έܸ:ƭ]?A&Ŕ4L }TFȄ@T"HTf թ4&MØLUlƛ3 7b+N[Xʿ︱J+t뇗bEK2i+pElvⰕ7kų/*-0<'R!z\-1N;=޻~8j_>|/m=YfO5ٿJDVi:AhX #qsp,2sjr[̧Z Jr'8pSg&fPo$_6iDw g9tiD1,|h-*Fu?P!ɋ~u%?jj| Xo:B_S^7/6׾Zo(#ւqɵikl[;u8$zP::9\k^Dk|\9礅ƛYPEzH^ҘiOQr(TF lmCHVaxdլa=ۈ`i5gTW^`/% =dpqEiyEr\ctZ7z+$U 7Ɓ<=6?\&Z\7I xMf^&eЃ7:!.ܧ QCp.mp/ p~/t 803.jǚWD5Pm q~X㶟lzqI˧Q/'L/2$pX>x)N]c'7 K=ZGP P.g'QغŠZ.+9sss ܱȶ^ XY_&:ߡ1yA򥏔IN 1~baM~s L-H:JӸ?Q(q^|?΍:5__'K7NZux7M ~V`A ju%E:ѺZw0fqZ#a=^l;뀓R5p d 'MFBSjP5K:k%qy%P~̵^+?:vt`=O3<yU48^6<~01:)f.F2 +j?TǷc7_zǚCjUZ6>O>Gދ/?81̩+w.vƩ9qYk6`0Bp"4lo  Uua}_cU05}CG|>4zBa~Rg4ʜPw@=Sӕݖ^]~-ciT_))\_[YnWS~]Ymǝ_Zo`wmǬ<^{hlێ;ſ{uV w?[qo5~kۻ)\#1\D0] 76{2XPTFzC~ ^#MvƑB+O<* * c݉/⥻{<ӝō<{ϷWD DPG~:K/JaQ%M(O@/No0iZIs {D#l"́}<:}"Y|(o``nOȆT_'J#t]PϽLI7/ kQx}:O/dJQ$gLV7ݧX|H9RZSs p8>;1~La< |DuQtSxO=g|>ƪM!15N(p$~i0`AIpz44aAC#XlE$!~Ywz+븭?";cw__($.lk-UaPFZH +Aio~E=J.37C)qEjIng}S >("sֱEaK/ 7-&:Q0?WU|Yg{߻Z?Ccv)נW?xmS)5??Bh[Mo{qil>~ DŽv:o&i{Vn)6^c4_$o޸ђ8Y/`8 04 0?u?QͨzpuX1#ĸ@>a-!C/nݯK_sƜ­ߪo}}'~^ǫ<טG,͘ 4~;@(1NA$WnHN`d_BoFb]OG[1e.n>O+=ߪ$D޼dM!a}x q_ lފq0;3CUdQ^uXLdEF8RgJ*z|Qsat~@>-|}CQi8=jM Lc;QuA~?HV _9sC^\$*F=1*b:$`DH%T B~5:\+-Qxy98,XuW=wgT!ߥckh?gwG x\$8Hت~,˜4NwK8-3vxK+{|q` 涿Л繻ߊ_;&:ش8WB>hqgH[z,2QԙH#UbFY[Y賟Qi&0al \uu]Iw7:r[A4zk Pv!܊V<d]5 2Hjw"1DVuCZ7q0GzYH#8j`Ս"YZ?U?N=8h+:;LDWPo,34kىCWh,gńؤ^Z?%U%Ws }J4Nx|-=Šl_=&v#{g_!K _o9`9M+^ŗGFMcapzz3Fk5u+)}Nʃcg&$ޑ0q(6UNᘃ)ײ@IGZ(C2tYԛ1}b5fma:e9 y%8<%/j%9^,\ww[=/};݌_)- 'aak=4:x$+.#q;W~h~ К'D;5A #,: M9[teX";gEg3262;tȖs  i;6Pz/JtO̫G#u@JJ[ "wZw>/lF^#263-of< {4I&2Hl̋Vf ,}}_E`R'j>ׁB{"Vx7dcSb6Pd!ZrcXcZa&cɇaAa nܱs6@l?F8kG̈́ɜ-v|uHFY`]hk}U6ر*zW,uVh?*& gWOoT:[?l ȸߤo|848\f!8p;gwqg}] N|Z׼_<ӱYQ-U pe?d amR4]_QzT;_G߽Z;7;8įE^X{J\OͰ3o_}wx~i gM/aաcғ|XRXu@YcR AO~9׆Q8<_Ԍ'Z_ַmQsY:[ֳ~퍜#魍_<[G_4zl?߹Əʿoi؊jk~?W>ܗbS[ ^!᧯O q;L?#Ҙc~M@ܨ/AE,UC$~Yt=/ycYPx%`g;}v~sww>̇]YC1nw, wOcywb1#N>HHhrTX|f`z[<-.b"ozGOafb.Sϲ*pd9#g,Di``X>;~2~  eaG!&OJX&D :zQ8DLqӜԨc+5PY׶3ꌵ1CsLΫ`|Ύ+?/" <9c5CCrgc"B@}1_۫tR8x́CX58/v?g}յ0ct8֏͗sq9!zF>3/bvZcN鹇ozzlpQ՚hj=yO~q߼gEvidjeɴ "VE~!Tx&:ۼtzpƚ5w~[SͯYSO՟l-J>YKGl:?5kG$F 7)l檏 M x"w>p<*3VD~r=o<ke\ 88.,`У1|/^~#X<^Lzޒݮ?vxMǓ0_Ov& : [~_ąy7V;+u/bu@IDAT%[R% fMLǘ~2@s='Cm$tSop/Nf m>F 7(zš2?sg/p=,)lut,&Nd>Qt#׬t O DIKuU=YF Wa-M4l_I>8yͭ ׅ6X_UsQu)zċ1nL`M_5w'ZMaէhJFeT͏T:G`I7 qџ8Q>k3kGWL>xG FĽ1Z<?.#Hٚq[1c_xʷ0ͿχՍO5^u =#yxP7,~j >C9`?05]7yNAz"vϵO/zxbǾb KtfY7:sq>V{^qSžm)xkec7,C,z2*qg& OqFΌI8Ti.b<`j/Ƀ4EGYrηn0y"pͯө;㟻ٟ+zNjGϻ5w??ݿ}h" v\40/烛,@;W;)gBɩicel?D ~0qǵL\I l0^o< GsW5}D+OȆA>u7xQc~E oT.mCmL}|fu793LZ?)ׯ8?;BNBlD/xeqآ쓏+#??lx~簍C"qO??sk[_Ǽo7*9oϗLl<#c_Oxĥ^7Ռ|9!zᤏX j/&q _cQ6ҚO~6dc?[?\ٹy#z9ό=7:s'N 7آ?G_ z"<||0{]|6v"xq뫱hR%&H{h8gs<6U)EDQW>^8PLNz@qֆ+=1_(a;FlTi_*3z1ufu/=}.~gi}ڂ|΁;}G,LV._U>^ZA:wg@=:/M}sY@^uyO]@QQ G:,=+WDgWFiDѲH̼~<|]J.p&Cmpd0JOM2^;R4q"ddAVxȑrz%؏>=D:1K>u3N8B!d +($ kz$ 8KGG 3; hv?zpE%oc'ʡq ԛO0i$ ~  J/΄"86}|D>U#yõD5S=8TS^Qz>zzD+#/eV[qx 1+?~q#`܊_^.[vGi=q]%N6|ʚ1%~0zfAr e9!/hUkO?bO\Ezaq8t`bRlc W<j[F4VC[ᵂ}>1I^ 'q~pbO|{[i|l^`[Ʀ H+^$b 71xeMU$G6T#KϨwzQ:>o`)MX壶GO_{^Z?eO)wuF6_q`_ӓ&a=ZPx`@= |p4|Βw2yn {yd`7a3(2Lx *eb̐'X I;mbc+_nͯ>o~6OLL89zrk>ca<3ꡜ덽~< >a賝k𩛑sk<,21lA|T=; D; }i9y_qOo=k}ǿ?}hګ_Շa˞?$`0r_d{ǁtř)]>̻Z;/{4,වC]ΧOv5QTy$=nʷ kw"8bl|E)sV?>Tb rJ" vU\?G$ Np{j&k}ͫdMQ1z5-Wpm zq#'exfx_'u٬Tpu?\τ5׎j̧ʫY: O-aJ0x2S8s,5p!>|l k*?a]}٨lC)ą\YozomPϵ]h{&X/80>|obOſ_¾%# O܎_AZcWa<{5>^'|x魸C>0xt>~@tC{QN|.uF|;Yb:Du:UYJٍ+L[C:o]etߊz^52|KvOy ۯ.; 첍>סڶ ?ڢ=e3{x"_Z5z-Y957TA5rZ^\g h4oG뻟oٽĀ 7 7F~l(X7oMRz ߆I&^Dc;@qsҤF5_Y pjCN7JQ3xy0asX-#%4bBGġe7" jr (x^OؤuI?4^U S'S/v^:Jg'IXbǩ\ ѨY#¿X}#Rf|k25:Q?QMz^wz1YPvF})z&_fc^ϺVn>n*r㰑>5/x `@dg@Ʋ2 HjRlл`.C= .=PeB<힊ko;Jy1j~6w>׿oqTmU~t^[O(iQq]KWDcxS+zy]&mzrm5,ٖB q]I#C_zt eY  9(0}GhI0#^LMo6BӁ.Gt tBςSu@Vc4)Jy\\5:19]AL4);8{G"x՘Ϲ8/F;t#ih,{r_zA&eN>0,ԋ:r>ɬ#5O=^y))tYt1#A1\c̮>ȑas5+/E]ݻ^/nݳcnpzyk9;|ջ]g]g2cMriYI{846%[uU5q)C%tg/];tgd(u=' )KQ&uFuHKn$ Nug8g[o$=wkbƝcKT=M?ťJ]j. >{}n& [6}9~<Τ{w} $hMبnYeCcyܱCGA}砟ۂAC眇dX~2ʹI-~XRnSK0Vb|f̵}).y:+Wr Y.!pk87N:p9O^|)@zn>%V׀|J:o| VkJ(!g}@¼ 0"ᓃ6gxng ^ǔ ~ g YB{c^9sTղ#ŇE| Y@5Itkퟪ7^/'1B, V/`8t>q>z^Avk˕t3gb\Q1#fD_sꁳ\Yt&1bb`TŏNk}qߙ],_?cNLp`#|{j.y47Wl.C|QgV0~{!:d1O}NȃG#=z>/<.Wj캞P_ ؔsG>]|cÿ"b_=ϼ88Ɖh~uH++4_PnYp8ac>Wv-s&d8vE1^%J@s^l/w}C/ThS˛WpFM_Q߬ڮğ %&)X`ς5(9 b<@c?!VqS_+6oi?Uk@I0XGoU>< jjLzkEkU"+E>WN1q@ (XQ[n A1D7!y24',Rx I C 5sR[i:öҞD)1 2 &|=P ouլR;}{W}t jsua/8:4"8Nwz7^Sj49Vej5|Ĩ cC8_LǑ}|^kNgUQgB8_x z kΣQ\FU]FUqZTgû[o>3ū_((3\$yǛ)Fl>#'_p> '?'vƙ@o k1~F}*"!zm"IrJ{*!79_`yg9/ 5gwH:[6^Ϡ WR<qPgĝW/'T '7'M+; 8WW^+ qH8c@G1?sԼ`#4Zxc`y:CGX`QuB@>5Gd)n LIg:>xfaQ1 =3g%J9ׄ6Al.4>Ak̴&]7z,$ q@?҉~@Ǿ_/`Kab'G.֣rd2 BHْ60a+nq |/(Zz 9|2N} j8qQ(%YIae&9#ɓG_ιwƟտH7fៜAWI+B±tyr__zjCQW Ì2]59%g0Ʊ>L&jZ$*e=C3]y"wh>K3l\}3k}#ye+({$^&ε[9:ƜcαWwݷ}k]w̮I0Ia&~u@@7a/ ]M|Af\q 0~ ^/8HbDǿDgybhX9H6-zQYv@I.M By㆝u!硅(K2IP0zb53'tӮ1YE!Y#s=hgWBb5a͇cΌ=Z2ء'w<ɒ+}uX}OUdLVY7 +} _Xzl>N, x&>@u5t~Dos`=(z|G#^bjt>^s*gݲnx.w_ؽ_#5P͏t 7^ÆQx*6p\X@Bavbִ_pqؽxV#כ~SCe]{߶&16K{zs[rYy=? ;:xzc@l ]>, l8LÁϹN~&@ ."p'UpPP\⸑ƈҜ+~g1k9:ۉ No:eM<@0Ib1@II Z͝c8{xkדൎ~p2H#VGv8pc8& twswO{n% 6;޶~CtRϩcƑFND.Lb=E5yQXEx. ugĜx8rj5{E zbn c^[{!D@ e(}”k 5Ne}ș tQx 8qNQ{;Hm6εeǚh 01ub۽X`MĐ~BY,=97rd`G Z Y4xj_rZ-#4<#%BÂnL+- GFM>to;wDӲy3BYO7G^}c;p}ea?? ¯~˷Gh`jb>pqg~g ybP˘o6[E=?yq@ VSŕkqM(7{%7hgVye g}"m ߮zʿϼ]vE4=[vz|EX\[Xzwf]Ze]kfM?M6 {SlxQ$<ޕ齕6zhwW/ǥ7][lPԦ ׶yfоy[R|jݬq֑C8,\Oa>˟?$I/f8V{{.QԽIr0mL΢+ }֯zjGG;z5ʳO_Ӽms$ <474r\v#o&/!3Zq5EyaH$z`)yN6!=^fZ4}_|ϗ^C; T ދ_az_*`y>_BRU"ڊ{ uU*Sy4<hgQD~X*<|Hz:4ꇟ=}]mW||q=XDg^,X -L@A*+ %٤z ƾXu@|oî~5Wt(* i`f'.GZ/k?e'/>@_ﯾqX`/9?fe=k=[ǷlaKҳBxDq˳\-i˾ztlK?_ koIi9>Th$Cm pELllhdsv+sZ-G9mmEk5ȡfҋK" x9~)Ï|Sgڛ0Ia晸y? [[{|ߓr[7XO'Mk苿Bdžj~Y%=ׯyrqVV*Ues}e[a4{V> PagK @r Cjq3bk?*E*w`8 U5n;1_e .Ec#~>e۵׳רKsCぴ~rh cE?Yy=~CJo?/~}/뿮iu'D;}x!}s;m0T~瑏I0nI*{T4i~Z 7vO5ۊ#֏8;ӏ7Zִ:Gmǟ[϶ׯc8Zxw+ޛݖ^acR 5@Rc@%2;P8qe'TS!?Rĕة `'?16lc<(6 BB<}{a>;Cwv|'L?1j\.>'odY2gLF91Gt w#A풮`p39 ]t3W}eٖԛ >؍Oj}%ės~F)~(E71B!FUsM~MjuXO8J'y[RPdih ua$=q$ +q4f3*a`G`K2ix8Lgz@i&yx/CALbDऀ)chܸcMI65 @-d|B#@-S 9qFlDr˲sڔ3OS%_3x15o/ڭך )[^yMع/ O*KRs;?qe͏G/?f-m9u*J\wG}邑h $8^q{A& zJ&3a6妙d7)KVBWwCef~RU^Wyh_壱{U*{R|4u@z'cy.Q,Un?'/8?Kڍ3<93g,`#8'nB <2&f[8ۍ2 (i+6\XcpoL%| ϖG/0[;Ӿ^lU,@AvUӎE.8p!x٧ooy γ;F8% T\k"5$kG,PNBqu)e{3k [CT<$/y:yBJ.xW%(%0ǚ ıc~} &!{NbIJpmV!;vn1q޳\/b 64|@ fW0?S3c *.)G| -"f1f?]#g  zp<m֖Ȅ_9gp4Q`HY\^OGuyV=@ W=_st}M_3fZpWk|`?=5z]W&l?# 1jф(%Y>ḷ%iogxgp[:3F@?rjGx678q߯/G\Y*Y]|d_#+LX$mŠQՕGxvj7\gwbv8udu? h~eE|(=D?beC:dĺ pqi7xPx\V9Ifyf4[l餌.w~OMFvY^^%gNMۧ<|ž0{C c?GtV$e߇ΌĹOWngZhKR$?@{!LJ_K=ɣ@t6稧dR9g"Fw'OIpi&/LjıL,~'Eee3=+8l]ͮ>9lAbSwGH؉wU,ܷs5Ku F| _BV9Q߻#UC}=]ftj}أ0nۉ[tBh@!ZJrz PzkF fxSd6CEuʞVKwxٷG[6޳ mCE:\ۑړkfi`d8U_哊#O/= [G*G_G*GK5^w/{g8Qdĥm|ؾ .{dapQiuCT O>H 5uM]Oxr-ty=8zē,_ m@Z/̸xg` 'qu{u^p+C9Wk*WKF?z͞ÒFoTݏVADC08E*H}xe$9Nsx` &<Ւ .CK뵕z#;&PE@0{h0"Q\q_nQ?(>I9Wq,QOe \.|}(s#E/Mo2姶aSY0k`FdjM6/pYcB=L+v'pg;;ٯcl9\,8NE7 nr{9]ì9$}r.?k[ W<Irg 5Ç7IanjvemAE]MsTxvjk kUvIsӃ;3)-c|#in9ТW4hבk8lr*6\̃Xui|G&/rv_ MtS] [멲p'MdS\ywm83ngߵy _䳦|Ov(on$>%Os`>?l"3襓\#\6ì|u-Yz{뙃Q/T0)^\ɋHL5Av&T[z -o;@6LЅH7ufOCѝ|VW  Nf%™XФA{?3}醦2@IDAT>wujoK-!ïap=83XCPi3N4:řgC^֏~exs\Ɔ\xvwة 5s@&34ˊ7\ n֣%3 Mp ӵwdwkOFkkߠx i9Շm(_$ugHiF ;9>rdG!} .![.{=+.cK3[79}~Y4>͛9&r?Fߙ'}6Xͼ3ΠXҷ=\l%Yxknxk@s2>%oVp9{y(آmrs\X,^6j/tSMqkdqnO$^MdSM6%#6Yq*U>*_&/;?.`͓!otjM!}5$ajԧBvU* 2O\ԥ| _c&az`o3Ӳ {.b96.N?k%; oNrO] [Kr>p}}̩ C3YxlS{4ltPxq68^A.'5Yrg0$щ, XBEYT#v:1R,Z>5pbz|ӥL#t\{0ilBa72̄-/"J j`U=!~@=lw+fB:B%DoZ΄O5bxh!3;2*8bb*șXX?m|h3}=u6q,JghDn15 ̹=%\0gmu.pƱ |ksRqZ[O^DD -qq²vlȩ>8y|a{sCe Ȧnm|d=FzξߍEȓY)e{ _?8̽O'չ19! }wa*}֤SAjJ?-H&& ĕ=ٵQvqe iBYϊcg*|"!>;g/E65wEX1ɮo "ϓ {=]ED/<ܬ4#fv w;lo44@nK2>I[՗T7uY㝱Cmу]nQJkxP;1 lQ֮i[)F8gBFg yQ*}_y5:}dQٍ1W!wt<QAZ{ΒUAgV?|ug|KMWumC:{/MvjS~% XyRPE {*o pqd;PEV=Nך1?*McN f!urW >I95LRK}z#꿯s_dSsS곃mr][+_wm8umbd}#!^?UתKs/a .9_|R 1EJ_'@œ bx<>ZPelF") xqq?x}-tZh̀~ Yk5Y-A5jgV͙ƃ|aƯX%,_zukzW? 8O90kYšzt Vʠ \/9O  gnKD(Cvn wskYj_ 5 3Si/pel0!k&nmt¦[xLiF{.QzuX'ψFkg3KΖ=.ܻ0ٍm].Pa4ܝuB1Zl/QI@b|bt(1SQ`oT½MzM!Bi[h{x>jH}6 }2z|Y  w~wNu:z_{֯e<_z8oʾD*g7?85\JP ` +gxTWYi%1qS ?kGzVu + }4 uw֦+I1ѩN2za>v2} _B h~!nw_y(+7HJsW(;«)O2l']{Գ6V+} $e!f_F;g'k;~Υ/O0#q"jgc[ə\+pIW>!GF8]jǃ <g 9u9N3r0!S{ק_,95ʿ\}åq72(|ӭc@@Q6nqt,^xgos?sO ZvзzPw^`Hm՞\gOrVmrkcƐo7{[dt\Yes1!VgQW 7, `ys=_=z96S_߽4~#\0-B~8śgc +@ <9 Σ+aغ]>#[@#4\}wX H(;1vx}HtTȕ.:7Lb/| pc>9P7Frcxa2M_ 2ۓx۝XvUd`KWWr,ܶ.{>!ZfL-ӿoY= yTmU>zjz̾χ:n==V1^ov I!dbHv2rZ֒S)0p2]kAv2mD"f;Wq!T6͍q:_GW'#[;z_{\^˷ kk˫}V:miVduqO]?3NbMApV'Yمǃ7i2l /6=ncE[è2N aruov k"mS L d߾d]}d`+fm6wNiA ?{b }0$>S, @,lbW&v~R7d_,"?фġXBζTRryTt];'kqB<~79kTBc^0ݰ//&P6(8a@F2[䎷+_W:ރvYy h@{Nn6,Ĵk=N2;͈>l$$L^hy4ȃv8MpCXDAȤi4Ƃ@5 |3 e.$<8 +* W+H;kq ."6=˙ ! ߚ > uŠ,>ŶQX,n e/{NO?`m2׀##K>v{|l~bG |䲐Xz;n6=l#1͐eb'~WaS9=/H| kk>o]xz~W.doo7]t<7].}ƻqnLbpȋ Nv'J`M2dbSupUkΐy!v_/85ٙlnx8\x?(o#t֚x.tsmpx<cR>;*Kb%kz$Fː=7)ܩ1-NQgP3T(C2`:*c2YɑU5p 6f#DN`_Fȧ?US+#-Kz5f=#ăxXx>x.>eH#!A^= D{PxK5ÉE2y`Ͼ˾,e,^Evm?s5l5~HudLm&ᅣ\Af=c6jeL +hg}e\φx ?N=kֻ^( G%?P6e'exlOnUI#lza Y"/1-ax2d)k+G @l+2=n_"SQ+xcSE惘9h/m 7rOr}AP񀋞#/}F |էހw[؀l8q=AD02we m`_ ȼ|ɏI{PK(Bir2'~!9/Hx!Q zb`V™sxn6g6] O=2\lbࡖx?6s#a2/T=*"glqЇ 1<L͜|oaM(@ rV! >8ZN↋S3 lVtʇvUk$YiH'<7ѡgz1=1vN}CltzYA>ԛNPԭx0)S|:}ǫn5ཿ@}|]wYIq+"?aw>m^ax!XZ@{  (0G*ŗN3'qCdێc̗߮|L4~' )>P?uڢ rkg.z׏؏]Co왃eg;]`0v}t hL4ԝZ.NSN3G2S1eluE6ౙt:F%]ec RHo?y8=pש;Sov>OԕS{J(d0'Vצ6"?ykqcnp`+oD wL<9p4yK11gh^w`a* O:-nSwF84`9Yػc0;Wߏ3BC&M+ֳ~tc=9/ z +(1y%i|a:0C/ܝ(L#*6$ vŁ #.]pE[uLEktC*oDŪz+8k ܗ'[vVesaZ 9/;775u>s\'sWN^u5)})2織TqVa5t9|@qņ #fѮ oKM9yl~W"ƲONz(?T35x|~?HiᓟZsf4g[[/Kշ+{d,ftk0Ǹ|޳L~jK0lt8߹)Ou?N< R)@ï6l}8;nv?˺zOtk=4}=[+nKfjg\-QO\u?#@8|uE5kqO])kԼGTxxg?vm31wp63gϜ |ɘ^| kAغ~9;(Qq=e;P/`hOWJ{|F7ce|ӂH_%e.ޤʠe\[ckӼ^憎 -hj"|Aׄ2|HtQ#9|tI9bDnޣs ^`Orރo&Xۚ#`P6C9$ | 1#u@ntOu k3m}O5\dϻ>uu|rz?׎޿+]Ԋ\JQiKU)DkV.LV S8/T. GzV7CgS<6 ˼>ő^]o 9Jr/ឋԕݔ>]5me3q 4muP$6ś_K+,#t.Nz%þPi$+_(Ar]/W4n\mgO$ڇw_oęCvz. /ڋliюExJ:es-NW-53JO\oGk1q6ߑ(>%^zGIB/ɴ=Y.|:jӯ}t.TGz_}F-cOvL.c~[(|kw?{E iOB1S0j!x73ˆP(aV{j[{{vpm R4I¯%Va(隕^`|%w]ϿƄX_c/W{+]gmx9u C?|O>8SS[8I?{t0.J0*EV5lf%~'YhJ Z l2B/f@*]+vQi0,uο &~׿tYq8ӦF~d۝s[ڗa/Ipcm⋳rs̫x4)@4{0 .c ph`N%OgVg`)݇yYQm|(bc a3rјBzv8+^0#! <yzXFwy:M82X=H3녌W׾}Όpӽ?E+5~N$CC< ͍k8b*WonZ!Oݡ3|0¨*:V >;1$Fno#?Bb:W5h^VQF"ٻTrڐ/mQJy"OܴnnU7>p^c}5^[AG\G8[W~*f,cԴ_9A6qVc(ʷYq[bjbc 9^7.,jiyxQ(βwK;d<%k}o9'xvu%0*B2]}O">q)G:oLwKMuxGmvv/c'Ooz-ӭg* _dwyn^g/}}Q)AqP(5QPlvnz3S89ٜy_ XA-&fxÿB !`I.42؜c#zA,w ;P_v|$gxr^G?SDzuL~pk:e%nGQjAEGE|xۯr.ʓ/=1ǯzuN^q46Ҳl/S1Lim`` ]71[xm!P}[?c#& KȦ8M\^X0Nu^wdy8̻I9<ķ,]g,^~`DQӎ꿯sf|g7{i}Cotmv;?An7+FȪϲNuBA!dzi.(LnlE\ZB]6/΂T(*hlEz̾ NO/M`1[6%/8ǿ~IRO(: YE'mP8 /w r]^f7'n2Gz`'3C48YBJ `p%yy4wa%.T[(Žk};?g. 3^ ӷGFLEF.V:Nh@Syf8!v!䶼ld fv[r,x3e{ M͸`- yks;46ݜᖹ}ԌҜ v'7osuw͙oO߿8wiGIa[Ö8w0ƒnI Kpz>+~+?jKS *&zg;+5*LOdY|"A6t dž\cp >遇37e/ɫ%qbUt&}4xW l?dl <]96sZbyx6Q!k^DZP^"qCr 8I'o|+dߨҶx_sӻ?uyzԾE=)g=x-l-9$׹Ù0M^y@5af;;):悾e,_*.=կ_Pe0ǒS ]>& /h(*,}#xrm9@x+~&`+Xъӱâh։YZdW؅yLX xSob66mxB>I*Q!yy? fxY\,KG#dr:V|}|?m}5%FjsƇO6E_aM| +7CtgxgMݯ[xzKk 7QX<,f)fx~ٹo>E)1 > ־t7'[hp#Uɝ̪Wdt_厼9g,^P}V,*{IjWOy$|wzwwqNS=8$% jpp6?C"NiI.@S),E|Jv"'zA3'bYq|;gc#5[Qp3DD)Ed lL6{x9lkfcKuLxO3;D/z}+-+ewa^7^n?%w\S( xYA9< LygPr?lМ'L #rϚ\zalď"$sɼ|׾ +tj,ceTŜAxbl1T@#a`GenP ʟk(lz̙m%=_z<7ka+7gYČU'xgC!{xx@Mw^+pŢH%.z:/lN"xBR%;?nSN;~o'g'[x謨΃ '.[]$(f¿bpRIæA.$U*~znY`P8$$@Lkv&4+eps6R3+nsMu[?G6rGV8yО@*'. a J?cuV6$6+excٮa0Gӱ5`|'@0l8kZ~nJRv<Ń'|m(.Y8[y+_W{#Y{[wnW6}g 1Tԗwm~@%؟hmG3Fn4%o{3ъmsbI͚012 TT|^m}Kdxa-ŐfS%/ x'cPc9pHa0+Cda\ 8/b ~}f?>l|@ \ Œ$ö423~++~yS_]kUo+Jv;d;>LW 2w9;]ΰ6hX@\uM>zkaXbOBQqK0ˤ(ukLpX O_'&x'?I-Ά7Oʄiܵ6 /fd >C}0*L\tñ줃Ȯ:Fi>Л ){!4{l7 ZcrPYyPC ۻ_x?:zA7ˇv`lkZ\X\MgcYva1kH'~LuW?w|+o]R\_Xyg-Ϸo;2~=bzֈf} 9tTWCBVϜs`[p$3`uYs^Hs$wN`ȲF6G|,|g]yOwC'SG&giTXحɁXg~IYxN.K,uo_` rOiRʄ?e1'ph]CPI$fqC: ,%eex()> P{-}_福?qmxR5|sR11e>p`@C=& WaEN_3RqS,zIn7 =crFdAKs}Sdva n=3j}XmrVkrwX|WҨT>^ 7>p]y5x^wZrU9?da3 FpceM aD6kAGb cE.raRr>LVJ9:s=J:yU 3A*O}F~x7$5ꗬ:5ôYgؤayom뜽}wLاs/;u~W~cWU*o%0@  }I՝9h/Z%tZflXޘ^t9ӰQk`ԙ-$r'Uo 2 'LEy}5 q~FOұ걹&E DU1ڦģvczK%^?bV_bO_>w~0rHmjYm'UR]ں)C@%%f$[r5}"Od1`dvǨ~ZD *<1M_wO/ݯ{|n7Kڦ{f} ȣv_哈9*3fx7;wէ_:<oN٧`væx x1-Y|ϒ !DcpKt 6kk|-5[@;? x/2]HeWr.@O{O3!/B|Ġs̼xw1N-'b|s|Y6ۥ("lJp$l lE8`4@R" BTF֬u>&ZgVg*EOzy/zY>i/cx_|no ;fnv;{Nn\FWV;;{ qtoЮ#vx|CnFlPLG;jzOVs{MCŮQEosL ;A/tJχ_~¾?k΋]ou{?!-%}u5u=9j62z58r9?j?P[_c(~lW Aoz/ rb_?X|oFO;u}4O<crىs;`MiĉWtK)ɿ- ;)sA`| '4+r+}NMmq3r>|ݟlߴFYneEGY4lb< 'Ee3a;">(|?\:.rADR1–q ׳>uԒ~x| QH* \ɝp3{U$goJ,سX5 PƓWw~O/4}Yx!/2 ?ŷNw;==aq'&yѩŪf꓌=ц.V:ђN cUs:z_Y=il֒}y_-kA9fxXx! w4oܭes'4#ұ%첌K1f0_~#vWC_zQo]{/;q1 Qw|zs+ǟul v OÒlr,Sow4} ]?l-;47yy'OY] kXzg[#2ءȠR\hN-׬gdD9q(XON(@r۸T]7O8Y@|;]Ӊ#ȰZ_Zo=Y+Qg $T$ae+`8쾐d L|g-*C`u0 |5k NRkrBRp(EcTt`oʋE,<:u/(I9!%#6YMhބ6=xGq4Kg[OεUj\fW^_|k8%4uo?o? h`08ϰC2בˇ&X,8BNk5vo:E Ձ-#7 "@G6{|A|>pܙ*?~)k^og]_ɧs|a9knEshb@02,@U+NPǹ[dOGg>\ҋI}A&" r+ wMqZ>_{n@;eS<)8# |#s`N- "ДGTY6*<Q62d? xH&GWt}Foe{Kg]l8e;pߙ[}TOGU4#9ۭ;z!ŕY?z6) >s5İ4Cg`BΜbͫ=!^g o[ Q;}6hY9_[Ѵx_فGO>DWÓ(m|M~26~ص'XGQopXHp-@ n sFյ~瀞v NNmw|Ț9_tYY|i &{gw>cS^_D}„bX]l0ѵ2\|:s}"GqrFy PǰH9vtesǙKkoe0ӥ`G#`l1_wg^sЛxKkON~nEU e;qA7p$K"7H(UmUܰO/ ~& MVHɌ |aK/Ԇom~Pt|?ҳcո CVs&?oso}3ܺoeoWEpw_;qufVhh'@m䓱)+1 ,64A 9p05!GCo8Ío=l۾@*/ٯ `K({9t=bEMx+|ksl3ְM /O)LP {wFV&k\}SE"VMKG cX^85{,X)/W5##Pc:E^z@ e;[eGC}[k/Ⱦ oטW#Ε/E禷O ࢽ)=`'0R:@c}XwB@\[I={`[:ys 31A"Au#va$@ȴBғl&+$dG 8LH $ELL F@7^kܻNsyzޙreTխSeό͌K̗ZE2L]}kU*OOug h*+]Ab2jd-IKXd2]p4v&LGq/Q%LSjS0Fw%?;?8eKBy/>"Zͯ:9!kq[nŢSUue2|3&#Ae}9=dWf#Q[ `y`me]qyQe]s{ :#^@5_hD l(OgKZZm}[L+d$#/k:j)Սc(v,"-GEDAW]A77_-18)ڋ/UN}KZm.&Grb ,U/YbBSڭƻv>C-|_n3۱!K#m\.9NroEPVB1m5}8@dGU.Gi7 ?zu_Յ4,g_#<U2ͩYcbǏ?QWag7Z04ce+#ø+Ip'2}Oܥv 8NO&`}*UH^g`n+xbLOLDZg<0alNp]"1J@/i|ٌ`zH-r&Wlŏ> 7}V3@Ma{:2Nރ<>gvWf}JkB%rDNQI_H՜()Aأc C YȭNJs= TI 佌l[| Ox ʉKSKH3؟9= _DZ \p7]>&Q%F]%y!</zŴo\Mfi^j&bQtdLdyjH[k+3QqG!@s190l,S~ћ3$5A;[M`l|r%hSOŎl5/jś%Y&r2 .ݦm7TϺ=~8;3P;B(c^8t-`Sq6:fy t#\E3 I;e%Mi hQ$5vCQ{BS]GtI䉷R/Hc6 Xkj KI'%{0"?BqKEX4HcHGx߇\T2`)BnSz& + &ÞKpKhvIKenSLmRdb2!?*6A[sls ࡀ8_?k{|N5o=|K;$Lrك0~)>v3=8\wkWVj=Ϝ>ϲ|Է^}<=<\(k_-xxf4k`t&He;v[x^(r\(5LÚla̟=LqUĚk"_863%@" C"6#B4uM]~uBl2||ba"W3Efgg]pOSוu-W>xT14x{]1[ʶ^>`ey/y& ܝ:FSF-[d^92LhOɦw6I{MS|S3+3F|f80YHCKȮ8շ mͅ,MU}'?{\3(^9\sF|Ӌ-+<_sr8mv2?N#;K;낏K&F|[3 ~6A4,͗(U}d~g{WY#˓LF_>PF.MF벴-K:>㽎xؽ.oí_/{S:e.,K M2q2f<67O2QO}|廎smƛO΁ ;CXrIvr@W5+vMAg{&Bh3مw:7Cg|ll&g-׌g ,oMFd*|z|%4{+ ^yx#gjJ5oK(;U933[b+![m$#سs@[W2dňɘ܉ÔK ?JBvXQrѺ\AŅ`B. Y~pzf[9X JZb=gms_mmC~Z?U_ Lj2/} kL|ek̅ |k -cgqw*zjqs hd*HL9b {s[6/Ordlp~4UzΖqEm2+QBM+Zk%z uzQ/ ^'xq?ȼ{OI߿?3ד6}SX+_*YJړ:Q(WP9dZl$Yv(ZYl/gz$uMKnR6o_40[-ϿȲ;*A|HzM{_xʕq$v/?9 2d9/De;u۔_*+^+{^i?Ĺvvlli^]sw[\7M;ito욽ǀAM7+:'21-h34`84D; e$P譳lG/ ?t?XIƳʿozd~^ov#^wA6+CP|/Z`e 5l >rkd"4Tkh1RT PJHi}Uݍšc_fdګo_oO93N#H蚁ţ%49 ֿ#7=oh~SƐ.h|2*#?_܉ߑKrG<_n3f/y3p%)1Z_ѺI>'I8Kg3fr'9X+ɔp?q6`r :^K<ƎQ@L!kuV%c4e{ 2@D3+P77sXOF@+0$7BLd3i i/V.wPwp-j]Bt<(7JD6 ߀ hegybFA擻=p' \|9oyꎫޙY>Ō(QWWIq@PYdޚ9Ll/t$2LK 7#_c=י%7yO+)PP۶'( Im㴳 j4ZʼnN =rMUOKlB 5|}`#]^u=7V]UWx=gB%]8O|ڗL}q<vm{W 3sO׌;H|tTvt])c2uCa,t͉tHFJvL+k7C^,}j#v,((4 z@;f7rfS{9Aħ^W_(W>!!Z\ (bEGa":qȾ`>Y3'>@<&?b}A= NdQ4 E5\ѳ{\QgqŬP0n1\VJDGLn[h0V@Au+j8\UbfdLuߖO 3l|g×k~GW<еsϥ b.uWw$֔Ѯg/~G!{_dG&9"]q;[jGpee~LD߶m|\׎a 9Fz1b}js~!t/9WsDM"AyN\A5 J}-!Rv'2+FPx~@&AKo/zߪ6.V1OdI3ő~1Y'? V%}_G^5^εxx܃C|Nv~C $G$v"WZН&T%LBg8aZ' Î$͸~Lyk<%ĊCgij韝gxQjDv7s)^^'u'2kJM^9KOj<KBI%.>A!6.R*`%8Lɜ"$@ Ml Wy5W鱥xڐ1H17,PIL\r:i)էJ1fφ^tȟĤln I\!7Ӊ8Ec',n>u맆quj*(Y~tzx +ퟭD\C}\%{=W^?^_4D'\4ezP\랙+XG;2@q8̖nj|Vեp;c)?՗ЃO5޼\=T*/=?.=_0vm-G1-ۂJxGkLi oƓ#]5YN4TJ+{lG7BNōvsg+~SMc8>Al]o5f8h͵ErK)­Ӯ; N[գֱVŹv1aC>M׉'x }Ǿj\s#ƻw};~nwd)C7{_rol= A'4EGc6|̃͢ M茅yj@"g4AZ3˾51sR|$sN1#Nosic-}p)_<ԒF$T(sBFDL'*DP n3Ŗ޺xޖKC ܈dkBYWEѠG'fBbe"p$~"ߛdU"S=TK1_[9i᫺2Gݒt-rl^WՖ3G*胁9{/[*>^̺P6GƐO7L]<`ZuTC:9e}Kf;eP#ek}r%Ԯ'ύRgі\SN1UKOyVåXmp$S&/Ưv`Wr}lWSOμ^Ď2}u+3i6 LJWҷl˒-#[gMB"x j]ΤSk!)nј*A DN5C"C n;{<s12Oy͍OziWOk[-V]Xh]^_ܥxjT}䯞#?8}>$08'Н| YM,hTܳ]7oPDP5fq51~g >K<~_V~eN5fbVCUK-)JKC*DG9Oh<\gfsQx>(d ֢rDOya+x b@9&֖j`V ѷgVɾݐuݩN^MC]^O\Z%eRQ,kyhizv̳,;ufww~w\_oĉ(<1فvN:֤3hBl$,tOZdDrWߑ0HH-(KVR\hFK.CóKb2 qf7b^NӥR0]5-@ քX'[-:A@";w25U6}~x3sLT3=3`1dF2=:$};֓'8mAan/Ne<g syFUaiOgs3q,>t4~|?BM,ʔ/;>[N]++ǣ6YRLQ_=r`[mm?" [-g}qr&Ce?ۦ|>*C{S,G\12.pC~OW#ۃX{}* Xst2q=jƶL)9u/'51h Q&cHلFCYک]7S/qƧ>n3>e3b*0>q]j!ܸ폿{y]ͥxkĺ&)Z^:^Nx:19lM+>3gyaw-~U~~pl:_8ޒ>o?S~89r&#zTE^[NpuxNrMq'ĝ(O|O9&gs|>M ٭Ɓ,,Ų@d#5u;l!hr;<4cHl$Z1bH-hJveժ}_}p ,>psmG()lsm]?2<ۦ4A2z>R2ua< !.K\UC5:m['y?9Dw1۶1S4^N'W=0| ݗWC5 R}ͺ'G Xwc9e\*3Mq8S[xKd}#~ƏA6Lb`˥ar)j9% qB0Gotg ee|8%9ѬGv7io/q_xXݤR6rǫmROMG]uC|u!a7W&y^3b0>bԜ\0@rV2G #}Ж ti).;/W)rj{>_8hX7 7N庩Ӗ,-__kI{8_a4m $$HƧSCÈeame2ȱ2}n ID:V c2?bL hP}S#6Xc,-/0x&N7:axXLK '~֏ZNbj8P-:6~t8~8~gs:|!7px"^5U202ըb}Pq?GkIK✱’sS=1O\@lTɉzwbO:]9?{bwLmOfvSXUϯ MO 9th-!-őggpThŢRO@q&a_C"-JbDl['|jgYZ ^nȗS^o܏}/Eo/v=/t+ܽYOs=>gO;G@@=붪/ c'wy'|Bn.*{ڃy\xNۻU|)f.o']6D]3Nߚbk3mɄÄ,ҳg/+sBg7LI<xNBKvYzKV|_[ibן/~OHϜ+sсOU9LW90x(iekrQ܄?iW.ۄ x sIYo3\)%{Xt?w̰ b) mGЫ$E2o}TNr)63--z^bI2.bҼON׳\]_Ynڒ(@W07 0SCU;ٯ&Pm,n{ưSE{}@d(э,SL]̧ z^i@/A1/ X`*oQC (S[2xy]9w\8 铿u1B%v z-oH]1`)^_{oIq_J9s {Imx __uw3쑀DCu2{s_|ٱ.g|n-dZ X;}NǗN?7{of\rD|M5愙X h-\lI&64OQ)*DQu6Tkc5w2`E(|=?W/ÅmMۊ|/}{ Uljl Z֫xu^:uk~ ح^5ijxZ*[29TL,umݖb0K1V;aU[_1㰟V|o^O%c ^@$܍Þ V5ț}}KP&8qb>77xDǓ,gueܗm'wޅU[ŽPja,\WE<+r]8/G3ZA+n %XdH-UlQ821ѪSZDrhb4HubR69ҏ|UA1}p= 6pP> .l@IDAT1=EֶAIi8px%S88H!{OLE]K12^A>;~{v`O t.y֫϶ַf|Q=nWc+[>l2pM* FM"ς`эr^_o'N9[p:ܯiyqj|(Ɍk,$?P8Ӹ#?|rx-Nsï=:7xqd.ωƉ}=ϜgYѥs_sW T{܊ɠoϊQ`įX=ΨV}v؏\},F]"-sхe?eR*RKO_5ƾ~L_ۮxp1& bbK[龞hqǎ6|-)G4q/ K}_l{raf_X;oJ-?"km.+(B]^_}Wąp]y_sn;T}+D(r9(?)(/kq6NL v=YA f&pt} h 6z&ѯ$>3dHzT~}jxa=ֳ#7K& pY/8CC)Rs9gp=ާ &ێ<ވ|s÷ZO-G^f2f׮$c]Hί(j3Z::Αy?#xuS{(Ŭ8rOCĖx!EU2k D5쥾WҸQ9S"?K%i?〉v$ymKVbvkcgtKV}A8Q9!5f7nXbnoF/{5qӗL ol5<EŰΥx[* Mź+%LRT_j.t}{^^ݽnǿ|JHqƵ[CVS%DudsZG&>YG5d1`|(dEy*> eMڒlInNW:=Gs^`dn<>]7;pn=@3}̃|Y\UHY3ݛL՘YرyԯieΚKⳍq"1dr ;4&oDY)uP9W#\ܞ-!.)n<}L_MZ>\{c&\e氭?QyMGxpVcـ]&VZ+?݊'e "i=NCxg}+"eG_5/_Tʢhj$)zLzc:;c( --HVU2EqW..ZZHtFA/߽D8{Ym=/o+xz"spiTW\>>wЙS?@m85# ;݁# m=``/_7x&gO[9tVx94sMg|B޹} at?xtX:rґwJ/pqpvwo~O O_5&/v29`cq,C8ZWkw-ҥU|K\|-s:كDZ"h#pd%Z[fg}읗_\継~{a)~߫G?xj?f@'V[ mw!'1<gvbԺ3cMSn!ϱx۫CGfYq8_uhCÿ xAZP%|S%ԣY*@vIlYEɴkbWd!b}UeĢva-8%1d# [X>HX 3[?pLB/rtNغv6c?φg+q7g@l^]Ѽ_V9XL|ͻo<2x})ֶ]:p8rR?L^?Ôs%jTm-o+VZ߱urʰĥ̃D= 7 7ȟud|ךYܿġH!xZ^&z<:yNml>KC$nO<A@}? 7}~|n>suӿdYڲS/g6=/o#F{9c/DŎw~M򛻷Oa QJ3's.ycO 9냮(L f[Xy$_Z)\}<=98w/A_Γ{zx ڱA?UF`lrAMqKLV&2 '[~u#P] oYF0T g {ircř=_ E xK>Ċ:%Pq糉el$.RG,k+YCAW\Cņl=JC%NOf, >YkVBE8c-jR9V,{F2;59Ϣlc;: <%i~vفtINTw <ւ!5दm)i"b8ղSzNojV r_m.> 8i וj ~VegGj Ns~${a؉Ea FI[aBNj_SH4OlhtB9`4ԵZſt G\F>/v-l)Bco曞 _O vJRߜsf<7<]dMyį>rZ Y"̼xIO,c@`\Ic2'5hp%=Zj[Rms2(h95y<#'o]2LT;kԛ`G[U>b'W_@~חq4\Q3+ƭC^f+=/7նROjc}Tlc n%Es)*A1mruv~}Wp6%s̛?u;Ux./0іx!%l*DuVx-d*_ʖ#qClk|Ǔ=oF׻2,~)PQcx >BEŖƋ)Bz(6 cA! 9t!M#ywwޞlV$t+ݞ$Ed)'-_Xzy0P{Wto\z^ӽm2uצQ{?goG7'g7\ҌPqg¸ϖqi.1`Y^_C. ab> kds0jc"g?ѹz#bckk'bFtgT B$gl Oo2o6*VŐ_*U؞Wʦ8}+{n{^إ1&ē'@tvq-[ؾ| O{9cewuFOU随c0dh>N '~_cԥs"7G}DˡűK|3Po³-ɴv(NT}1WgqP4_EE=&cfUԫ%C/*F P8˙Ox"q-c<"@nN,eJRCoi@^iFc,@lBF9L~VOD K]!+xLza2Nц hs񍖟YSg߃/Ͻi=c >ˀOJȾG|1:57[ԅP +{j:AX1$ L Ͼ<\Ogg|ctsͣTr9,BƿC zZrb2,Nn0k*8T9Ak,߼27%9?s?q~oA rjGZ|}(NVl>q@@`~WVJstoYIgDaE[[? eU &rF)+m`Y  6 Gy,M@`ls b3/Dpv:wN${c9uxJNT=ѽOmh qַ쯭JͪmcӨ]hT#tme.IK)dkXl%RR+1E}c|Lێrxͯ"Ʒwb9M /tK4?#ї>9Ft_]]QNl1ZY"[ُJeByhoI9ezl[ M3 "_=K}!oIn ,=h\zYy[HcIn݃qyoCƥTn \x7~ao:!Gkjw'Obx'SE-tQ9BÇ4soF䓷n7ɟ@#{M}=e/r}29w950"#p'.\;H~_C\q@ۖ8yx,red> l /KI\MǼd! 1i ;ZAax78C6 p![p 㺏GXh`~p .66e^@wI^\tP NHOO['1 NSvR'*__p'n_73d`ࡀWཆ[3T~hƧ1:Fˢ{e-휰,^3ϱ"-uXUO+*4y IZ gtID56;8ySz͑l8ş1=$-/Z*lR8ZnE`,z){,'竀O]n-uZ}'I^'K t >Jag cIv#K|+e1eSK0&ݾ "s|w~ vABsE9O/ŋMmgPW)9Gm-;W%[bd}G򅊧؊v/{USoINqJ?<􇿺{ă+a |J.h,tūVcfvLR_vlmߚA$w?Z?aحk?en<289>0r-m5#{gxSc0(+ ^ӷ96JDŠz3\䰺D\ZT_ZևrԀ@Y~!e2 bY^R9Y~z$sށPcaXĺs8]`pGZH+''`Ȟ=dA Qw6<{lތmwOOw%HBӍ!MW)j@l.Rsmgj.Pti.VAAonZ\n4L7 "t='ty ٸ^$A1M fv>\3`$DGj' })Go3ng߾Ǒq=߬7HA7ss;^(..tcI6u/{ҍK X;9?ZTLwKNQ W&VdQQ@.:M'`bsq*XG̗޼`?&)e~l]t$dX/2C`ƓTIҷ䟺g`1zvbosOiW&rcL#h u|tF?y^sǼKU|C'qG]m#c(&Fu0#N\{ׯ!$ s!m%?٧ydO-az~d)j+bHJu}|d'_ $gX'P.-WPO쓤+K|Kԓ%Yܞ\!pۖQlݨW~y+sӏ}^܍{;ίޕO.xRf`=3Obx 4>61~}_;&XR,jut㈑j#{rYQ/m4Uaa;E'.|.kS>yZX{w;@N23Cz~§wZ£C!Ukd~q'۝x/T!y]=Ը J:$+ δI#F,`&0y5P-l1IWDؔlFu]k"?%g..sޅTU{|ke)rc/(3Ӹh+<^sx|i_O\kDy)ۙZ oCWc ى~j~ܶBY5*gT ͛6ͧ!_⣝>\:E3> mϟe\>*gjW8Ge`lSM)u77wZ9ݏ&9%_oŊ}<ɲ3>m/xszOobuɒ$%rZ3/߾Sa}MG4C7$'_0ge[_n` g"`Bظ?lāhL|Eb㊁\1DrNŸy~"&Р1Bx)r2瑆Y,_(Dvl¯8l|(`_oI#0|}MJZ0LK89Kx(൸HşbV&YY> f"οdqcc M7F(G+_Ej>>ߏkvoߊPmϕU>~֟1T8b=UDo8GnD4gUEUr^mm={d^x3ؾ4p;Inru Y1ދy߀3Tt>bBk-itn `$͚g9^1Ddmzwm,ǫlMƕ-q̃'; f>Oc7 _\ v[h'_oj<]R,3YU2N6Os!rSR]| (u|Kh %^6M·Zq;ZA`@<+nߏ=|/#䗞x‘c֣ϟEK?9W zE|JK,-tƓ]b H|Nq,asuٴh F_+݇kkT/>2g%HE.O3C+C` _*jI أx )pɽ#25Mo8[1ϝ/~C1`執byU?%ߎi\K1nקl)t"|, b쀎ec&sN(~ l&w~!‡2Y󲼂Aum;(ݿ9őLHdf;f2ynמa 'OY_Gw|r5oCv|Ϭob*0W/ *2eA ҅2>"qva vB6Θ&Ӳxf dvL4ɯ9wprnrMG#>򤿏r<4~Mrc _xth0A[9x"l/;Ŗe29 _Y:]G{z{~/;}_;n9|y@X? j>0Lϗ"Ny%*ns&`!P`y ßN7\{yl '?v}Ixܤ壪vB*e8['tL2&ΥI _!e.‡-+@H Ĵ'ecA<^~!G(k VW~%4xɄu.Ebco71rlZ)cG)`OZkV/Nᯭ7// Vbړ_o5ԩS@<E1Wސ6Fݥ$H}ҽ,xo:|'wtd8֭vB%5ާפY Ƈ?7|Nw-4UĴ PPmFG?We.Kձ$n ̗HtwE;ۛ1ɿhhKhyh|l ɌXN68aҌvcPNf(%Y%[ yCWZ[.%ZGٽ]̧]~{ w誉viFN3lnQr>җAUstf&H~P 4oe{lҩ-Y'=[}N|yh,m|1ns^[̰y>Qy\F#-veL8|Co8b78/eWxDte%H|",wxx2D<,_A"[SzŗN̟Cg<3<~k|c'Y>ٮڠiݟ ^A&VC R\&C:ݦ[ Oa G+Ƿn'PXwFg"7#ќ0>hs&76zoÏ)yQ@lKoet /|U*Ӕ1MύOɳNy.v=|.Wġg1O4z+@v}3OVp.N|pP}mej_c܋?vW\qS,c5s*?[ų1ic0o'z&KRǟ1aģ?~+bj?nV84|u'c#z|I;':lV++S^._;Y@͉3okHZ18Kh'4^DglH@XbAZ1 I p_t[*8t' `xۙcP8̽FVX[~6^-Oanx(5'.L4.p#ˏ;zw&{^PvGZ(;z\(:ً~3yZ`Z0{uaVL>1 ޤxl3-K 7Yoo5 [.klbNA9<tqb?à.0lagSFx=N!ffps>^Ǻ=Y0˃-BZԔgCrcR[V.p z@_6X0KTI9!O񙧦mwn| J4Jzإ_#*޶N<nUJ׏}x(}ϵ*߷϶殏އ!Ϣ<#'p~$6ڷżcg2۠T0%R\ C\&ҤL}05wϣW@+ OGm2]Tm>:zᾧ1S/2߰ƒk>8ɒQ'1SJxޅh!/I7ْу|%`rrrY{ˤkNag&Bem9]m^~0!q|'}j8^[9A^^q^Mb/[㘩tlXW'G+QzuQu, ǹ@\9{{Ls~Oz0].u#8Okd[j|#^kRJ|CEάJa*P/3/0ʉ֋xUυ'|>xsx\j~=yiOn0wNjl2QԸsOrzkL3aVicO¾}{P@^Ӆ_^wGW+GP0cث޽QU HPY*`S0[2H^}SƗ-\mIQI6 Sdk Wvc7'oԘ(g*tS\{ƹ0K[Rϩ 2.4)¯n#&qśԮzE~_?|pW?&;]H=;h LKTx\j w_`oWGe_/-Y'SX$#gxq$+S>jKupV#q+@;\-D9l10탷FI\j gX,1\X'UeNF$[bMX1 g8$N & `bZiB),-111&_ɸ5:)U<v|c0N׃q3:˄g^DiC^fĚǓ?X-[y٨0['azqΞodMieuv^ύ1A$HIWTW,RR"vJIW9?'#Q%?SEŤ$EHqI1 @Lݯ|Z{uϹn7w=k{w9Z_ښ(3[9T %. \FOF4NGxstvũ@L.[Sl|0۰<1ΈҭJc&\|Q< `e]Lc4.o=lf~d~XKF\0Mu*/؊=VS(@IDATyavw!6`P zc*y-lZ eP{үJn\P^T~OKma=v}J;J{nPh4-O6N7ZR֚cT{˹QCSySܬ<*LaڸdK/Jg#ޘ׃[mm=XkcѼB+h}'7K+<e5x -ace5O c*]Y}bj ~^xǢ]/oEpM5@1O Ϸ8(bW!JlDqh˓P+z9fq1|Z -'yLnn^RKR4֫o$.b٣[Y5^Hlml.en]!cģz OU.,3*S>@6<]oIk' 3[HC}b;>l./6enV Jw x ܾV~N 䑲6Ic97^V`ŝ]Q|;PF "n;W>gV/qr vtrFpwp ~R>L6f;~ܦqb6M,I|Mvˢ:[֥r@ }x|a=ʟ9x+>ckaG dD%2RXN3Ͽ:VL lm{UI5 S:B~_-Q>Q_v aE5M8z;vPcl` (),QJds. rpǦrl*jDr} ,rz _|p wyuqR >eubd/h')'[ɜy rFA_)\TeE{3^_&WKFY}3W*I~#M.FuX\6e"h5^WeʙSCGwwǮp61vG4gN$Oyعÿtװfpki9q9z~~n{njк[[r;b_vr*H\6&AkM?ӻm܊ϧ |\5[o?7<>~hlW atnbu|cI~9m2pW"p_P珴T'IQ ϫe>_[X)^.,p12uDt-=ApFK8ĂkQMl~F@nOx91mioTXsOI"$y ikc]tc =  {'_oE3f<_|hGpz=e-cgӵi/g,Oz SxVI./G^ͺjЮk =2]x= /~99V}` Kebe]؉xU"Du̡AE7X>ګO(޿S(êoTFy| gYbQL>^Pgץ#7$./MGGUʧIBcjnTAi$d}N& 8oK>_p}،yxU5]$9<<O^f`{Wpg1ËQKjuJkyaC^ڽF j:O\DōD?hu~A—HF{1q 6~O2TpW zdU[f5p[٧-qmEN>Sz|۱_#GYߺٿ0xS*#r#~Xptu۝)_`:m.CJ?5 p,F/0TNӕ* |xHMͧ058#(PCegMO,v(EA=Z^*p@Od?;xx3A*0 UIm2#*f;[qqO=3⎡8n>Hǣ{ql̈1TQeN'4bB$B^("fi>b? 5"^nQW|07_u7)گeZ>RR(K/㣟moj[Y=~0z_IVsqznwmIBzDXRlkܟ OR;ْFㅳjs ƕѷ|a`Ms=sTvm]QK G=VU^|DsSJeE{2}{SO|حfdd{ 59v),ej&Ž4"Vl9s+fJ4ewVuhėdu(/6քAO}O {íW- 7F,ouI{n9iC{Ó>E/&Iq?抅NkaS 18Pǥu~R.M|;@]L%aƑj~5__|JĩU/ư!h.[ IaUgu(GGKX@ݛO(S_ :%bg>9vw|.D)bT-L>(<!rh^|]bW볞}46b낹\BW6 לSg $JX LgWmxjG&X3_{' ?ki[>K,la%NUኯIA=)?TG)MHx*6TPT'hTgfqmOKacq-a b>Nӟ8@IiX@>r# 8SsYd,GygR9gy|Yv8;[M^{UV6f,os+W~p^l6~0F#0Oq`8r N@׾:X"4DB\eRn)!ɋ<ܴxއdLE+_Foe㹣V:F4lC3`}%ul]jKeĵmGk‡ع7,:Y]s9EAA. WqƏz4r*~JJ+e=2d@1Wgg"8G_|ܵyy$ؾxF!gr1j-ڨC^ '*(< q hfױFp~1FaQ#Ols3XVXZ_TD*{ԕzH +_( ,WÖ-0?qKLL|8Ԥemp1ƒHyy.[fɋh!kEM'_&/@cp+H*_8Rb &{*z^ψCpY*J߳7̶&*ߨc3p=pYm]bK@sWXOVy8~/H\Č /|S a5ȦzxS[t* cI(Ƴ66xt1HI}=vilZz_FCr`z|(^8lÚzʱx~bPoG#ٷ;^zȣ2ݑY%7p{i֧ix$.t*TEЂQ NJn \]Ko"=lLWk@b24˝Pɸxgb;^ Ċl&QJV ;;&y %z _/ATꀹ~j#\CP9g9>g9TY>(~U>_?`e9ڶg,o#2Zߌeb[ϲa^u+mmONso.G'#p*^!cI󂷟A]7@ݔ^ӹ˜EP0hhK p|p];~=.\~q_<?f+q>c,t\D#yú#ۈ e'!s:wď$3}O\6n7q#ZpNeOU3¦wkdDx[?:|:Y& ?>ks7p6(jeOd2,tŽwr^qO `sգ5|n"e޿s TºДwy~9\zoz \D5> eP5iZ/^\<?E0W2ƨEP|SFT!QN]6߈zZ60%Ae8OTe0'ݟcCAhgsނ񳼅Dr!gy ):_rM>]u[2~Y d,o!E.x s27~羊nua I3OaAQx 8'+i5h=PlQ.t|,rA17ES (_nߤ/Fmɳßܷó(Z-eϴwo$1EM8)|.`y^nKlCY.h o=<]uy6-bc/[ݳ;W= f >",lހWϢOu}PTTh2|] kcvoBq4Pg 0.X~؊ZŽOí^s"Vܬ_]=r(?WvaAnVK |\kx4TNP򸚝cߛ;D',!*ҸR'u|3bE}J(]uct[c{4մlq|ߔp?gE eJuJqeNcbR Xe\9ԘbPU"DVGGA_sr'h+d>b'F]kǃ&~q Q>.Wio*6k.RqhP[VK)eU"2Y`Y4`a)ςyTŴGЃDE\9~JDyaGtYj+Nb[=_u 65WGPa궠lIÚbT ܀]W ~G/} (I`RE7F1 "X-*3Ul6Wi 8}vF'4K}/](QXqq˂OūҏуOx#*̷%.e-#7gyYl)hSY)_X̔NyD2%N_y' Os-J7/ o[F5}xcd{O[N34Vel`7vJykwYf+xm +A^!NX"+G섚1x(wp ƚ_6K湈[bD]:|s|Ec,_EO >ǭ\~˵ӴXIzOP~Qqٿ&`Z_˦+See*h7:k)5AJյz4l?]mnn\-Wr W1VSx\KG:d,[bV 7%=:]~ F ז)H´lZ:]W^:@7^>WǰӯÖWoFJ^p`'>6'ǔhS1 GTH3ޜcx~scx>W|fsQY^V&M0d,/Ė 沘?'/Z6`.xsruc]l_ ;`? Tmn%O(7pp;p8|Z=a{zu`s-0mWb y؊N;b.z9tV1&h\@@>6Ll2x7:W]}'ܦp/\۵߃;xk,.o 8?b4>veT`0̣+Y84ZvG:7P'?BG*čÝZo4s]:h] &"mou#QԖg:;")i0O*h\廏N7׼˄sx7 |\dZju,á rg5.[}aFɷ I8i]*}=\l: \Zq]ǰb !BO}h>Wm6FvV/0 nYθczȪ`tVhrE˙蹬@ڏGUoMds?:[S4.7 4'p~89Ѻ?V.pkuN+g?9~]y?şsޜ}?oN^E>?=?\e~C*mY|VvqGH9S%W} C _@cʎFv(oW rOM - 9ԝR3y/=`4o2`jB6fߨw~`~瑳8?=vZm~G/+_2o D{->1(?mS!ya'/%[c9^:p'@Ĥ֧x7ʱu&-y@*i/cN#׾1ʟ6{+6 :F EGvQ]SSE>";:WصNڣ_+U4sr ܐ?vw`\9}Cy ˗+KyD(1;Lv>',i o_.༹V9nٮۂ{oߋ'j7>#SӉTxp&r[~Zis`|q+ )$Jl\nK՗꿭R˙5ٝ9T61-) Bm:%=w_߂ۭ ԿLWu9 +_{QoMT\Mɶ[UԐ؇6 ́g4|8V( 3[UI:k.iTEe, مAuS@q599f^Ny?0SQ#xsrͼd=f>2ޤ#/Ҝt]y|DlGLK|#.RnOKdHUCj|n^em-]4aFLoS!:D@2d5ݹÓMx=/?}nx98ȫ ?eS)*1961? xMLJ]|O>LKclt5۸|QWr)QƇp6~8ьc../A0ӕf탅xG䚯4)ҜXPe3+{[-d4xj9 2񗸻_57'Ux/a8/;/w-܇}{%e۠U)~NNY2/:`E+e4@O1=r8R~֦f.H4-LFY1\ >x= c~BhA?'I6cj T1]϶p?檍˃5!e m>)K0ݏZ4b|n++šO‘F^9 5R9e Vǯb1twZ#3FDvdDE5!y ^xk/[!dJ/J99XGBc޹U"Û{ֱt:%u]Yquyו)}ɇ_3g|l;:pXlrMp^} 3lp *.;-{簻\Vv8vv 228Wg3xڛihE*]1d\b㡳K+Z.W?OW7" G}nlk/'yիdHI䘣e'>5W `hx"}+o5 \΅?OzObѲ-->Ve8EZdoڕw'SHVc`TٲLտfi:*u;v*>z]sd; lK|ٔ<ۡ` &gspPFh"ض0xo_/U9ưlQȋ 4hs~j԰?W#{}_y.r6ʟ|p]DÜ WHѹ;99 ):9I& 2ֵwՏTs;a]9j..?=9xGܾ;>l]ymc 2yEǯ+ [4[S/PxU6vp-nkg?=19#b)t| ,W]2%ȓg졁 9V0UoZ#u`x">&6fs:XFu:Ok|F~ZƬZJ_*MoI(ց=pn&k\b=q,6\ןr^[90%__ y>rWn31]g 3Qo V6\Gq7laeI+N&҂V_)$^w'壘t6Z {typ<[Ε1h6ɧuÊ^:y<3Au,1_gᚪ3Ƶ`YHlͳ(X E7UeP+ŔL'.m`/z 8R}̠ E"=gDhu9yKO*U-nwzѪ5RdYzQu)׻Lb|)cc]đc]&($+H)b[5)%o }I}Io|:j>\2_tqQՙ!L<'dʖ X|WkVT_*k_E:͚6&/Zpx|k=WUR vKa`g9|:uT}>'Y|~l]`XE>{C>}# \x1>.FlFlqJ,ۿMwI2/6ҿ;PkՕ)([Voext9 E*YϦ2 6uf_uaP 5Cׯ2OZ_aa|Ƿ4|K|7Wdi!Xi0 k+[ ,?v0ZꑿYjeCjښ`h%-8#CrX,x҆0F>tngzѺv0xng;Ow _)y|YEhcAxg~s;ù 8N2At@q'˰w>y;hQ8 by lýY] )V޺;Dn:GC|g| n{Q$eǽf,zؘ }zK ?KW4 R}'M|`Z !fa-en g)=͔ni)-,JajS$P2I  @bPAx/%/orOcN| CnţCùmxu}u:n[ 1-V+{)kumU[mq+>1Ƨ+S@o=|w6<%xs[IYm>~l*#mֶ]Q!|+y' TL"7x?SjDe,2 D F/{O=lPL?anB`L. ?0CQQc.|h_p/N.Fǰ>l1\jm<=ERBP68.ДH!B1i>Uҕgx(wHG,?#hϺ( eX|G+HeWQH<^zwMwAjۺ0^GqjN02e!xv/p="a\\F<(Gup~}?܍xA[_I6_b;|0 ƃ S:v ВG5Y!=whcs8ԹL$v(GpF8buc"/LigvG{u- 壸cqOův|x^5wTً,_g{]?4x׋\_?ʂ1-uyܟ^iCW19\-q-ܾ=^KaLY:YXO-bo69WVJ2xf ,O힥 9?h#6?$&{Zlq;Aw.|V [>(@2 ;biCQ/Ev9rKQck^x~BW>3E~t |8e :)e:J>X9]ǑxkqR|ԯ斛x-vN"{yHkt<<$_>R7ܘ`nb' 0oɩ}M|-#lT$t1F CÊp^iI K1[)Tf+ȫRHabtm*cJ[ۭ2eH)X2*U-|;o!q>_??G*X]^|)H!-|&$J@IDATe%Oo 8j*+_mXx=E U>WUjF,1E3*|:W~;/UNܷ M}c으[nbc2GrxplV=_Wg r2t'dl{?Xc^T0CP9zn43LX ѣ9Iq*f--q%8ZmBm`I y8*)|VCMQ?9{$ߡ+cE(G1NE P.ZA xH4FiÚ(K'Ϗu]M/i?$ Vn]^ [\3o+:Vƥm˯:l[{]8~u r,6oM<},pɿƵ)ZQk<\<}Xq0÷97Ď="DxZߦ[_gݻZQc;58_l?V5/GHYbID/ rBLosY*ߎXkvưIg 3Qe!^^z6tu'>U k#-'J>աV߁-JlRc~tw?/U*ߠǍ9A>cwح!"=XVc7w3R)=&J.#k'PEo6*p,ihisn۫c' / +.щd_`' E?' onbe/ngu`D%Wrf115՜?f>mՠQ;ݺdA.we\sSݝm~ۿUT:)R wK-GPnρv,k9n#.<5Y+ }%0Χ9ZԺQuod:nSlbW/y}2RS(grEJ'KjG@"2l>wjwxmh=WaZiĢx,≲D^Ku޹#jՄ8P*OO|;־Њ=P+_̷PXB]X Jt1ߊ]aN}Mȣ8x?6^<]8,.O\cr956(#eHW=X?ƙ@ _G\AJ#J (B ] J;-]ht']b 뤢!:+|N>@?PS_pR;q Es[1<+z x= w!nJbU^`?hGkiu<剴plûʽؔ/'N>^뻼zV Xru!m|!lM㎭U5;bB;(E~Tm4G~aknT׶)$4 S{5G'c_E +NR`qHIiH@i KMnMy<:ޏ]rRXe,q7ߵó6kV1st6G%m['|*SxsTΗk?/3*)B?}ơ#;a?謽;4mYӸp`@PN`IF#N_NKTMG ԽcYtZ"EZT h[g=rkk} S.-V =g.?y?828&^DVgq7 p-aZ(W7H}}&a^Z8<7쥛C>eFŒ/5,0jHOK7zy7FyBuJ q9)[=b~A^?$__Cj Ez0,ePbkJGJnDZOz~ ot7|z~d:+OS/^zЇsv<5`o |y p'?Ci0USj̏nom\p$v/롥R*1^jߎKmWM@]-=,񼟆~ `xy:wƇ4=ǭ"˧:7G .8GWI$/9ҹ<ǘ9~ɻ,j2l?/ٗk|ʷ?Ϝ}#qonx籣mW]J9;QI0vBnfˈ)uR q3znuA]']kE.MUA=x)W:cWyELc bU(U=un=/] Q]R>1m.TWɏg)ڳg'zO>f'\~1Y| x5X B8CMi[̮2R@E.P_SE^> p0*>#)>VFmWz[+(vh(QGqɻ>ck?KCJ5_V.y[;;|o3RV_7p}%Sp\c:bTkP9}Ln hL=3 /&xKO#ѐ47&bgSX/9*Uh7v`u}Tq_̷ȍx|wֿڽ676~_…s-B,x_(٦:י2w*2|A;j8eA#ʈV˭ݑ5aku,~_ wnwWi|>oyU>VvoYxN OZSt縵p#|3QNU[ }o!G-,;<[Ԣ厕Zdu`"}dR5c1V' @Gao>~xAA0b3Iɓ=tv7nxsZ{Oڲ.c)\)9{Go'з|v?7S;37,}z@vb+TzCrLxTʴUg]󀴶1></IN`ڣ..H#Okʫgu.#ʲ^̷ݑy.+$|C!g7]$t+o?/mͿ5$ԗ6Fs%Nz ZnrJMa_(,|Q`wE'cT{iNKN&fY"ơX!xe~hcE͡Q d͏xpnpMŹ8y_|Yv?E' |V?F-!`󞿁oybiXۢ55>Z(xM Kj!Agl;5a.Mw(7Bz?{8 E,ژ?s5$NCu6|% vָRJP4v橀1=i+r:/:/fpl19؊ %Ddo?g> 9(Z q[jxK}OU>,X`L ΃2~?&Cg'M2?0M^QL#x8}?j'iֶ(^خ]uYbiPs g9WrP9gs/|ήK195oF>E -u)?|Ez|Yy\ٯ6j}GqJUv2v.S~QqrxChmOĥT@G'ՐKm[)|D!X+ YvRvJjx&pP~d>2"KO\`)?7IV/`ER,KB~ nC7S'?g>䧚pdr˞V;(/y/?+k'`Y/)P{yXOxvh<+%qBjW17O l 6DABN":&꣏J%<>xUxĭW|BԔKOUE/ܿXFt5/2>}@xS<)|œzv*_ڛ('mj B=TDu=q懯#ڽY\fX)jWu9"m sws ?@Wp^U(}Zi.0b u*>kw n< 'crr|z]q օ+O/kN>.5Jn><9Oؘ!OL]lZK!Jdclؐyh #Xyʼn>/~]lv8hum& 1lK[/Pix1xe7!a3Z>ĊssU1 |u/m'G: JCG$&|@tਏ]Wyo ?_->P1yCx-aZ)Zsү"n|= /}X쮍@&sEhkXM*uW/hXs5+jW|H̗׺K8A/Q |eѥe##)^Aw|'M]752xfy3騌,?˛ND[~N9 í mw09˓ 1>T|뽢b$Y,6՗kH¼FcKQ!@`rZ3͚q3n7}˞]v' ,Ėp?݃Iȇx gArp^j8<ûo:2eGvdx͇[8dCmc߸r;ꦻb+|E_pn_1N]q-by R|S'8|Q\7\?K˥ϼ7_ybxˎcc{.,߀'c{_᫁vݗ}sH|ԇex>_q^zR/hLS*ӸU'/ŝ*#z9 /?4\Oss``_oǠp8luEjE10|<}~o]Mt :&t':k= tUyVE=lXʲ77 l>hM |R*5EZn LN+ $VpxwIOewnUqғZYiܖ/X8U2-HCܾhk/9lq./_okWs<ŅQ|w k\_m\=us.Y9_巌&53~'`:VǢ PıI]Tu{ YX4;_dVy+pwˆp<_,xhs3?'*c N*OUM)J1%>Լw 5Y~Q~ 1ZWY559g+wtBݒuoogoWcþ,/ Ґ3~7\@b8GY]qh|xg(~ ]y As0MwJ͘˰;7y;;kzH^i,\r 7.t1{g3r㺖>m*fJcO_Oye~S|IOԟ>w? ip{7ݷ~w~<}." ϵ׭o/>"*_@tj!s͂y)[_3 > v[qq:6Nzj=,D6Yƺzʼn.}tx@[S,:.uz=8R߱qax+N]>\i^6?֙"kZU _΁ͿY̓:<'Ldž|܆ O]@3t屨WJDWZ1gyE^hw3~7^Q?a;7O>vɉџXn<|놝݄C yf#驻qJa0vşКwK]yN59vӵ784P[ʴR_*`J٪͙Bal䬍OOyԄ]=Yya%wX"D<(wW'ݫt@&ϟ,~!e G~dվMmD?`"l@WVuRm|Y3TNz7d*Ǹ|t@(8>p?eG 7Z`.q]?':pL'U0Vc1SYT@F1l0cMXaJThŖLj`U"m}vih '&U;ƨ\x.|8F-y-TY>(~U>,6^a6< 6_G+o@|g#Tv0dv49%fŘ.%9_q].xԓ>i6E}h9W,vE6o?5|ϼ_{VyQY^RF@m >xgu?N>= >?Eq'Cy@H lUmSHTkr>FGW>=\}bT6x DScc1-ZJWb֬;2~^tUkU|تYUB߽;>=^f"/);]滎*\oqMܵ+ccR<Zw cBXחಢ:খ'i@EO<Νc٫NLG$(>`8vB]JUxfyiƌ d,/ Ґ_6ƯmWJd Y _ۜ6L@L9_ d,'?N2_Й-ou3َʆ}O'gN>b\ 1WX?8.XڑP *~^#>zTb4-d; jғ3il~ҽ1JPXTK:2bZn[k 1nsolxKW]űum`[gLoƆ ły=<%|3% /$|=^hS{1Hy~h=N/)>r5cxN<m?Y :2gyUbWY52[1|~|Z\GԳ'e3|VU9a#&mQTq0Ç\knXmxޯ~43~Xʜ6nnn><5l__w>ug N_am_R`^K$ݸg+O8wT1c+ΆfTz+piW~c(zgz_+jkVk;qMf9̦f(uuP Blx'@)cze|b^r[_S(+pp.Tg;):U϶#-) ί~z|joS;r[sKFxm$W*( /"ժRyDil: eK}̣T/@wXæ]|?ˏ%؎ w]2o8 lW,U'g+Zq| kPPU,`&.,쯸 >l˵/Ce㺀2dȲ RL+_PME7X'FyD׉=kX]%fgj6.~YE'aY^m'3~bt#:<3S1?'aH3|ULն­Wqxs ܬy?gtY^n=gy]}<}$H},Hi3eߖPxdž/m-moAgf ̱ԛ.l:۔a6ΤAo<vCur<q |n'Ur(rǨ(tḃ}zφ<_CrLs,&<6G!G>S:F|9$;i\Kz}M_:;ǹgk<3)ofk>,O1|93f)Z*sYvI2F6k1'oc<>-Y=&ϼz,3ޜv -}76crU72vP;^q#LG6E-}+rH\^^'<"Tm3R_ʔɢ!Gξkq_v@` )M(.Ɋ}zEМ|Ё[\oOA?'_̷|/ۇ>Cij[Kx+;4ffws?dhgBc ʃȨFv0-E,(欯0"DvV 61`d `qv!+^붾؝uaIWes1W ӷwvwWt9R}RQ. 1 k-4*<7i$˖lfCʏΏt%թh] 4URnR UNbHuWh7-l6$۲5ے-};󬵞{>}}zֳg;'qHJ3'g_{;'gmSgN\9y.[_g̥lVŋyauy`cC"FӡMoc5Ƈx~qo/^cg|:Po!>P_ggĞ4|R3 >o6PSvd&@֋ɋ'./X֫H7Ug>bcm-K^x^y7\w_K9z,fnyF yk1BzMLdt=xxBDS0FE~/8K$Åe7蜼,*9޾ 2L7'/Z6W\&unDuNߌ;V7۳T[qoN}6KqTKx=mQ)|stZ\۷3盓{{3;w~mazа&[MLrxwp`j< j am.BY`F'qTڼBUp."pb] cCۀ"s2!R(`Q@44z L{xE%OP mSmP, \"zWFx 1͘>lkGD\CŮy|rcO=rḧyk\8?|xqYK~| p/+_sV{]GO^=Кp9/<./xb/ǹw2̗wAfޢ%aM}e .%z%?@f|Uj~8/w<]W^ԈB;Tun\uemZt?:ו-qim○\˯#>٠'>^#@bxJ8*QICIsic@ Uq׬ܬ[ 3'whdWMIJڈ>nv0i 7@I:MrIˬ'Hh/ |mx2@?r 'pCoIcGzANT>Hދ-^g!|/UK\X>\XC <1x 6iжx),KMOiU8:0ڽ1<{NT6W\®[On q #~45 ^Au۔r4.ZFF >D|Iu{Oj ~Y`K%krG š­ZW#\○,Uu{qWW9׵Oő~]9x9uSqH?翮]S|s8k{/ dJLR8c"LZM *N;'QovĞhPQk&RѮR(-qN+O-|&YκOH-_ռJ<x7K-Oz2l``lԃ,E! eFV_+_];#7cV.-u]x*)Fe 25_Ik]9HF5翮!ÏhTs+W@2!Qxo劧|xey?PDҏ(:jo宴D l#c1 S<~h.W^eBqOg6򧍊(9%}dT>pfo:kqt83 #Tqg0x|xnXr-hdt~Z&kZIxFP-OdB\,=#Mc1 +OjeNؾ~|3nu@d"2 JBmF)y[C>dٷ_xg\|xe7~}֛AR|p O3 []jPb'av>L-xVn'm4XusUoXSfa uOJǯ1~>_OWx#t|_u{GqKS̟)CjUSmM e.z/zoNVo.\f٤',iZs.fM{3mɵ>6;bho6>Y_쎶G<]XC Y<{!W= N(la!q h~i7/cZ ACLMx J:dovO# xF Guo]Ps/^?rC 0>1:].*ΞX4NQA'Gn6<RDb g? ̄dB-GQǂx7s~ΰ{Nqߚ Ϟg4}; `_D9O1쮒C'8&c猪<6sgꆉlx.ZGf\H.+/ծuů?i+yD4[͎?'8hGH5aw"|)Ս8*OΓuzOL/:q6M8;:}}=v ÆծCN1GpiKƶBu ZƒUV+'V7y$SXרjy&GؚibCЄ(ZU9 A>z_^-ϸ|^[m~mpj7yh\Oܮop$uտE<0ht,Td.MPC94ld?[)k @Ax6xYzyKGpOI)="ȮI;#|x\~ qy Nv֨zS+\aHOsag:I=;i?EhPӮ0 P]pQ ̗vʧ`#vMr7~1x>X+oQ1 &.z1P}-#⋨Ev=92'>.x[_!dkʾ&[֕jkK״+ڹf{>jq˔cmTsNNuƳj..dr޷/t=3&{ )L-˵Nڪ3q@IDAT&!LW{z2ǃnH,rG'chRCc25vtU=AD?Eǔ|1*׻/zh,εcX/xg3mF񠳗nٌu:_0GwЌ5>Yfȭ'[=3|{1L>埫*qe_VqFw50wUw'pmUM|7Q*F?]\YK2C+jףgBagtMx&h{6i|.d;Ё\,d3unzx"۟^yeZHV=- `gXoO n\Œ}̶crk df])& #6Ư'gvV{!^>1*_HGwpJCWT.@6NO­R+UWJWstMRi8nlWc8oFCD5ޅ_d"ujn$i.i%-Z=gѳý 0{Of_?<7bWU_I7|O;7|?yF] kewU:k+\ml7pٕ'݊E[*Ҧ\b |h&'eeUG|rҗ6(fӆ ugxsOZwTs[n=9|n~ЋwإyZ{9\V|cbFe lM 9j0iؤпI>H{YL/u}o#\rX6xou4cry_p[ح?ko/ :/3OO\Q!H!\e+j;HwzoKaxb&-85R!"l~h7<5PNxkKH X늧O''_/?VU\ez)>w)Fqnk=jO{ 3;iş>8/폫taӾ&0oȴ0@gj9 X槡^,,]^c69gx<ST`͚:2Puq$(&K Zo?zw~Z+p^`?u:J7w>ϼmLI $ٔYvƃE>dl\%x*T9@_w~SZ7(ȠBUss:m`sqdV4ߥs/))^u fy vX?ѴhYf-0Ç6+JW&a6dЋ̒UC/&U4w>vv3Ա$8ס^u:RjU/⛫G#Y?l-SSzzʏ2>{ï~nç~SZ-֊Ԉi2%IVQ ^܂V2/ n&qkL%"|r|+k{!v^C@a#jтXOP|s|gw{ E2q3l/h:lQGzս}]{`^0r=_'_r7rnTq}:wpএ֝ ;_:?sc+!R?jW'8 J]q^%0˒#VœxjOxcGٗJԏZUo.[G߅+bzRS_9PQh ˂D?1_0Ke*3!\Q Kc^DhN\c'dFG0bI;Jm)9L Cz?r^f#')5/y͸ͪ\#sZ!|wt = !^>AQ7Th~[ ˟zOcw|E`~mV_1[wmc厷IQ:cOuo>^^:XM>rnx g;Oݹn<6v6TeN҈ /4sM[Ih6Ԧ):bE9a*0yxaj|C23ݘo8AT[{5GujӉs  2<+<`35%k)/+.?Gm:~*F7oE 鲯춽UFhzGMnJ)dM~ lm_wu]yƜi/GsJ'cjWKUv9G[~^cǕL94#=] J3)XɿtbPfM$CxX)ZE0^eMNmw\ٹ,&;{YGN RܺJ3`QaXy _踇CXV[ Cb[ŝmK޹8ʯ|O⫆ʫ(q<@f(zMO闒aүA:?_J6bҏP~Ub/dDsB]@~ +(S~՘zhow_o뿥*2aJQxhS,lF[ 1WFh ?U^{w\5Q{R$[8\ Σx#rU?|QמG|Rǻ_}?#^Y?6'qIeV|6 g p@W<0Oܬfö08Jm/ɾ7~n⯞WcVkB}|vLl%YM}_ DR5/ѽP 26˔̟/0w~d[|jG,9#<6(WGn5'+m, Fd:zQ|ՄMOzQRL˵_KȹQ,AK=JG^ȯ\&?a涯eIcFaG3zx4=/}ѻEj.YW;o8SanY?98#ȩ!PPTƂMћNl|3!ڦ:1P[IDبirL քbk>M]˨eX,i dcsX[E#H}/|6FM7hJ^Jϗ_y9{E l-VE:ef'aOp[ť*r7 <վQ˥mힿ#sŻO'h6W>w`OxW?U?{:vs8ߠb (aNdM#>@?xggп`;_l:W汈TxP4X3jxCPi -)劓w75bfC" κX&ih.q nZT)5jŀX$7kohk(C}u|FƔnBkbCt3HX0[zz[Mx"vwg[=77N{|ƛOz᥷ N /p{㕀<p_J}]6^??}/|߇36}smȗgv^-rzq|iۉDúC* (49k#=n0#FcY=q9~3OM]\2 fPG8#"_"OhQFGq<;UF1RA߿y/] Y$8>04AUWԹ>ePq45TQf%um7e„ɓχ{ya=Dͣ{^gXzlK<p9劣Hldz\i(BS?_/zp58,Z;9=}jAq&n/lr<(\TWKĉQ_zU+̅'I8YXNEjv5ő-^#``)d.MrMtUn`M褦F!HJ VPi\^o%a/G}#;-xy|^x/85%OP4*} '۲6WMe]nUvɫڧpҫo_ˮ:3vU2\s{ʟznc2_Ϝ).|d/ܷGIjn!YupO-;⚲hem@K~k`% [vc9.ƫU(mY;݂qͥ'SSт!|u(V0>;<_[N'XN&kJKsY/\4[Dǀh*2^ @z`(QdH %x~lVj&xgeV∧eU8S<o-e{%y ƣx[Dяg/'V=/o%H"{9A{y+AIʟ}=\ yxwx.)/K?U։mp U ާ [AjyW~\yr0 ,#&(c!}qŽ?1{-`a|[pO\BD3i ij<ϱ˜0E4(al`>P%AD:hD3`kvWb-T*` ɞfpwKxD%^9aBH`ᮇg= 7^5+b]iAs}?}x.ede۔6j+Й) ׌a(>rɀuHXYV'xی4MB3k=dK&s| [ʵat/D5V!@OtKC ݨkˣNP|Qמ?{gӥo[<3xF#xů<+54y95Oc9p97? c~.)%!Ҡ¸6н.p6(M 7 #F/p?2ׄ08@978 h+s5SljR_y2.KQE~s/i sJĻ^p 7odR[tU0D^xv9"ƥM%=fy.1y<,7p2X#./NX|xƌUceF}6z`; |kp<"t߿?&D3hb M<׸vLtJ_66N2Y<:IuNޒaLlx?{3Ag["#sO?32Rh,ʩL]&5"pOAvϟeKjbyI''1c>YGG]-~Q<#\/Wdž׾Mo/{cڢ|o 6J8^0Q}u|O:`G&(u|7*M8Qxõ^gWI_ā4!!'tr1&,:P+Ωg_tn /|эm(}֟\QՇ"[YqTr(ބg8]^u8Xfܴx M *na-Z;lt/X*KY<;d &N=mx͖%窳Fa"9g+[̂4³Qh㉔0#KBu"p'UqjORˮ~\qBR-,s_)K=[IQ$CTg֫;Za6aZqp`lד4?UhQ>fz3սG WtHuYhIÈBqT@\x\z^)1\c>S:Q=1Gٷo+>&<Ouor;VS8{"߻8?~ε8&/V }|I;7: ϼYn4ZNIqT/ƫ=w0.EEF.I2mHj8-MM[e /E_DZIg}Ycm.B\iD=Aį4|̍kKLta|zdĹK^dr>:뙟h"t_< Y3xK&^eqձp#O |M'x?|R_!0˝Ah}JZՊ:c|escGu޷{80ԝţ1G/(~ַ166k5a ܱFܥT #7Sf wIW)ceCg:15H٤Iq-WKTz\xeqO Cԁ:i<"i-:ra[V{c[O<(㭖t~%᪙Wh8A]K׾=o/62f<#%aUîg Grc_9*||\H?!5{^NЦY)iKƌW懡bKSՋ%Ӊc)jՇxOco=d;G8'}Dlͮot(' 7fmع/Spɯxm4ȝkEqcw ˟n(aYWIƤĒQu(*fc&Ng$lx*IȢXu.I [8\tѫp-5֖u]_.p7jíxZErA`_'xՁ㨵'_͚xkow_/!AmoKΡhIH†Sw0ߑ (WƱ盓:[TަNqTo{Kqz ]G6q(emG68q(em{Ͻc˰S8kM&j9b[t"hr"r0܀<z899B:U'*”ɉׄГ19j5# JZ:C|>)x40M\#d`sǂfcɨgI'L5pUe/܍ux䖣"m[zjq5:c֢Ib^Qx6*4c|\ |[ fjg>r˧Xs}+^8MY䞼fvpS)_<獕 _?C}SUҚW~\ඊ< l9M6$qv,lwE l_>LuQ-ˋp6o^0~054%فE/+ׯo&`&DcXO,j>7~Ữ^z~ool7Y!LBIn#?mֵ ,} ݈rRzu>dV3 |IKz)bR'1'o={Q#Џ׺rE=~q_W^ξh]/2?'W\;v9N.} bkRIW\ _ O_TMcf5+ra>y3wC6')?sC9cb@r/3^RgA1"#AsDT>F9$Mn%I9 (|-~V˟ۍƐ@ڊf`o3of?WD['cI9%SQ;B$DQ|$`s5 O<o}3Ƈ?%jpŧ{;x|e|$Zb>?YE"<Bsyyxˊo@hO1<>q% jGIHE+R3%ϋCPMY}pj7c0(7]X5U=ߺ2I1E=e?sH`i|bQ_sW"Q* &Y& ;>,Ui@ai\#E#|9}k  B^ze}rą>SeEESe2F^\(M =ڬM ; WoXå{{q\+hF○bI6J'cFh#ʟlj{?s|C,:b`a8 Dy '-٢)7 ]F6kgѪ(:r9A lB?%P#ď je$6M\͡B)_W"9_CM4u+rCm}fYx "I>A[Ѵ5~ *jWT Xȑ#D3 _a~gS&MA[RM^'qFpn\^:5a _o( ,,֎/Yx ^|qs|>~ || ?{^/멥dWj#.nä(fr\oZ{eNK6i:94c}_$ *5H:.3usweqc[ z2oDP4eS(Ts0n򣠒i<@g4J֬6|esm:N 8o~UOǵKo÷G{b׉u\l)l;H핓-m-#GTkEW U<_絷v[R7T+ʥMKߢ'/Ț cCh;9P lx|G>m 9 g'^7o.0ǣL)#-eҋ~# ~fH DBTX GA2^tj<1Nd <4{rGh7|8vd#ODv{֔|YmΗG 3qs?xϻΗ<^v##DgD+WWCb T>@_gkג'ɇ2)*߸7+SJ:'  \woz+OGra{lU`mTrm-xۗK6k<%?_g[%^We"  r1`Wкt]n4{qAQF`B@f):OkJwT~ B$[7 /ww|>|5/۳/.؛Q|iGyCq}x>x|)z`BKJc A:K/dj}z.\ FBnEt@\rP`X[3  4EY%5 M0:jYd8<. lQ(D]D5d=U8 swkN62PǑ[sӥ\dXcȌx=w/q$s/ܿۿYw?ws=-x@4 CeD$*,5eOrL.'}mCK"6reW)`p,ǛQWKQl?W2cHJ~#kWCA󏍞^G>6Ěw\v".?s` 岥alw,d-Ff̸u1J>3]*L:;d j@<o j2u,wM{߉p ?_f .o?Qd`;⟶OCKv~XSqRbV&+/ʊ)>>0^;d5= @IDAT͈]6zoN!o?d*Q\xlV)c},?r_OmO%Ze5~)RP@>"#94>㍧x׃jto+87cdg"~T[>,|Kw~<$6n+NF8yGmh "Ge OO%0Ɯ bSKk)m`S0z`+Ei4.j5Wr'/O8֑ll;ڊ#QE5v:d#fō\rGђeɢ E\q'BvBgc`Rf}M.p'ulӓGN]`R\ }V19;^8|,ׅH9`:ߛB.w <4#CsK9R+X+IWLkf+y946h0Mc XaT _W620CfHu@& eתּG<dD6$.\&fxu:w 3?vF (~5qJpY:ܶ|_@3moywe{)4>3Z<5^xxZn~¹WL(I';U*dӰd ? i3(tqh劓^1M xI*W\`bbpMP/wISGcug|dD̞r3=(odLģX~jS´ b9 cff3$ڍIeIp~ f(W pA%#rPŋl!(~ -#┌g*135- 6^1 )mrsPܚ>g{rb?b l2^,@xAHW =02=SmK6pa]!MC;d7j 'Nl?Hraz( c>[}o?6x[75;_dƒ&Xk,͏Se'_4!}Ө 2Flq`xk#>V@Dcز7aAl҃o_Ѭ)wXgaX3rkqf>|O(h>['&gYB<=M5]_Nм|َ|gs/sr|6mbO׭:F1\wWJ<屵x'{<wS׶jNۊ{7ؿ08_aMM|VS'>{4jyi| Xwë =FFAbU("6Kű1\q&e,>>>53yIXp6&qe18td..8 '!"ۂ{?| tX7Ռ! G^TVYP q%?J0*q8X*ƙl 7e&;g?Ykȑm6Xd tOb񉱋X HbbbKm1-"hܣ򱎒+H)- s#K-Sn7Λe ^M) ,;2Cm7'o6%~b,:q*òM?PtmXG/ħ-ROUY? #Gp*|̑4_mM;c~0=L c$}U$r:/eQ#)^[,l{Ojۋ;Uh}c1ޏxmݔF>iݴRQ=S}e'LxX567׾oa]<:o\s3ЕOyl֋i/NӞK,$+8z/W#|GۈNo1F[^OxKO_}ޱc?ly<)ɇ6Imm+_~h8xxBEUC6?/:yF^&[00>ɕ3LM8+9!2?׼I_::ȗFdGq@:"Q?mNuh}4$kV-t$AV&!G9ԯHƙ3ʠ90r5(%efam )HF| stz "3y qq ^e8Tg]vTEX N:ˮ܂(G#D6՟f}ZaQ6n`1qU(בӣ,Rb{19gQ{,g?Jc 3g:J@5ھ|-;:bm©#l[J6h:~aC۹ϴDm@s h+aStvH_v} e3J'*ՇߙjV3}xRc嵍HEP2vCgvk)jY}7Rgc|hi/&8&\:ǡL1:j`7ݜ[3UtZn0u錭q|W\CkmV1p5$6:x*FV v$V?2TߣS(pЮ`؎OA`%]h^:flP[x Tn2 2+ d {Zl`Z/rlKHe b:lm㙻Ñ2^6*¨k Y\ s LY+PcX5kJg)/~;??8yԽ;j9O\:+1=hG36 dp'b!dA;‰[QyvS"F"9s -9XiE2m9vM#tVQ=<]67(h]4 2b 6`ؐaa',WTx&M.sYLLiNd3PnF DO6䛢 5QͫqHKi[bO&f6YCTN6+o8=Uys| lhif6$&|(,؜1pk i"+ cMbi!,Y%X{bD5> ;URB}RyA?_K~s\bC&ۑ l WW}XA߽L)JK<uUhO5%f~{ ~MP,:oYm6wg)D*_+oyinj[D[( }G߆{Fc5]T 6&ٹF8!NqO^IiIb0%Ru,)fQ@볏k@Q *;L-׳=4Pr0ŋxL"a۰Gt(_Mb(6_kzM.}\KtR[kxR|AS+F/J2~A%nac 'cQ,R4^6e,|sm&))Sߘ~{kny,xGRWDNfތPRMNg*عpz#3u% 5+Cm *3vY:N 5GLO'%JqB1/n5Km2ThpP.:6Pb{Ԭ`"dMz_S27mEfnP5V8)[8`moH=Ɵ>{߿p*;(;ܦɡ񙘹GeL_[XPxL:O,U/8f<..9xذf"#ִAU F|VU3<5xՖ[VUg6Or6Z}[G^^pز(vphi$SۉRYz^K:wqkڣX vbE ?a'NX"bDḌɱHd)/Y&_$(^`EQU.PoysJoG}ι^{{9k{o>Ɯk1^{mJB|]Dh#mI570Tdrjfl6+TUtZK;ۢqcG@NєZ}M6q%h1p ^}{+Ç1^[]qn1>Nm)ljG#5b@@J-|z|Z|S<D9t*sƗL:7׌Zm|۷>%Uj綧L0+HO1j1WU綇Q:=n=骽C\qz_}?i\CG)ȆqE}1ßmh- 0Ÿ=q0blѲ^Hb~=_mѥtUkw :ۡuVq׮UZ~ѧ |2Ͱ#~:_S aƈ <8CWQӨ:YxYZB>-yߡVg ^z}q6`~m9nYxdFDE1d8HvmB|cWEGT}|خ8v?D+y 3f:P Wc^& ? @ .Gf+.YGN{.)Á@O#fD.7~d>\q%Ezug80ndVj8C+?1 `188Eh}?xGC;60x(a˛mi+(>n=URkwh$Cem) Ǫt1tn~܀q۝R֝cX)..p'蘿yUVG x̋n(?~# gv&g\sւf1gn6zBO2u;_%(9x  GNα9d<#j#3h>xFzN&%ns,mzĸ8Є6˥LRJMc>6O}ԯ>Z??+[r4Oxx~$h .f]"4DOhzj >l>T ܶENDU5iT?I1g_yzv-:Zi7.vwn_Lv5{xttD4i|v:*w{ ݾ`{y~O>7w-raq"pe"눽}SOڗz<,5'`)y-GZ$ 8 Mj%I26nW^Qӌ6uNҰ6WBaUMA-o<U6tu1-zt8lHatLNZ )Yx[BrZrYĘt[PTb@B  y*Pj8#͜>ΏHUrT)\O?Qe(QpmFDIj]OqA]y@==?Rpf=H/|`Pr#|gF cpJŽo}XÛC08& ~ -_[B@X$1j\s? B8)yxD/#3QNi~p)b1iҢ g;Q8Q)|~5_Ƥm?pv؄%V/t^~mƲyC;Xxm"}:oýs_mOM=td<pnw\cc?%|ͽ?hWc{\K.m~b] ɇm{Gr޹}ԧmNEpg [ԉE=+q_, yj=1!hy^~e00xu :F}>>C8%|SGy@:J҂-8Y6Bn$֋a}\` U*/)G]:ON\9=.ƭ˨m;SF$!=<|cejE-.Y_v`|!YB/pq'Bcqemi|ȳ`Ox$X7U82˾?]^vݾ-ڝ۷?zN`[kq@ݖqwzKy:.wn_ʻwwRޭ?{ˏ<̿g~ɗk^L@.ő8t˸]p.7wOw<=OIB.C /+}qZzX0bư8,zC>zYx}^gYg&O5-@\L (Ddfx*81cJz15Z .H!1[ q<7,4(ugM~C' XʜCE/wP縋w{i9IX$PIQr{gŗWy[hQ֡YgL?m[3wR47aC>f|Y%'%:_lҺ>pzO|~|F!A]ʙҗGRyKr|x\q3|=K [9^g;nS8l^ pr6:gKSn=k~b.wn_ʻwwRޭΟ6:cͿ-j;}>}]x4}}vVwM_#ȿxBDײhA.?~[)`hʲ̷ûC=?L@:68ܹ(ޒ\ds -aqn1Y 6 @S7]ˣor O6D ]Ic#h0fn:.Day >cme< Y!9a#iǨGhS 7ҕ@S)9 :> Nli8 bg0`p$%^FU*} 8;܃W(t}/dEN_j堐'ḣ6Z#ڦF•qkp o5SiD/<4`KĖ;@񅭺kU(1tgLϱ1 id1*KNq̢ ן9@@}䮲_MI8~xASo&x!" OGPahz /J;=/+?k7pK+D"3e Ź7=ȓ5g.=#n&} դ[8;oA~\otnVy g-WS;W|z[wSI;f_W7~7{#BH]K߮`hn{=z7;\ æfh"AvTw}fGa8ܵM2䀈S8>*Un`]o&S%^sL(A;l )|6? 5=!#u5@&6ͥ$8 h:1dA;tHEJ9FCbzB_qkSs?6ѭ>K\((3I\Ώ*0:/ #&ZԤ {QEiX 3jGA 6ˎLjG]Omu( h/.8hwmN>mC t.s%z(ω|J/ٷDG)[ cv$,@t,kN5Vgw!zte| G\51_'\Q{Hq=ֹk1w8ng8}qtqGwscc|!>jS{(wPnIOm^IG9:~V^?)Stx]DZ՗-ۇrZe΃nu/n6M] 'vY7;޵=!s E'SC"o` J쇋~_HeӃ=J!_t WӅ?`,g-#$PWc\3- `878 Q֘Sl.La}IEh$/3ʜe8q#05_WUM?xuyJm#7 = p8VMAq(㣲g'QxϝF$st<I>'/3$%!2SJH=C>28| r| 0tLܲ l:>+o[mvuw( ZDm]O2^rN-tXz©v!O$3pxo>CX:;>)Stx}h\IGkmOd*IkmZNIGmWwbv 哎5\ZΖO:jp!t_c=^szNvǯSw[CtvnUL:j_k;9=_gS._%n~Ҧ^ u"PE.X'DV92yc_ ɏnN{}_OUjU=-ԽJ=aF=(7\BnT D#)OmRmXXc9L؛V [^I^GTJk,My%QoZnngnQ*|T1ZpO_ Pg&77-R_1DG}p8{mЖfm:>tIcco}g ʌ–O5U(R\s~<S5z]%*2+>|zB!jxh\߷nոԪ,F{ж얎ZE+:5LL-:/=<2~>ngֲ¹t McUxͶXxP_'2.O|p}>{) tW`֒;sG؎d 9؂6Fo^xQ/c꠵d΂}'>%~ \mtS(3QKt(tFPfF d6~̲b; Qݦq3H<"'G_TM֦asWm2Q_쑓=qr>c:C(&Qa"9_QA\|n8ӝ=0 +6hEI>7;F0p²A6t~T}ïP)_یЃ6vg9UqJ$sX8"C=iLg^='%dхlnBl~>/^O8 gd|"FU?OhV5@is(Ƈ>eLvoG^wj㍃sR׎'T:/"=} POqy}($unH(}遤:1"tKEw̾H$zQ/9t+_}WOۗ}}{ߵŌ@E"|hgc|;mtWQQ~^6}1 o?f \hεA_ped\}`3B|t%k0A2UZkD ~bl"Q \̫`80O"Cw̉.vȘ&Z3d7sp2@TE+TQЩx'>J\5rNɑK=r%S\zWSq}4ULmQNi9,@{K?}'hvBm'0a'ps -Kbd3q{@ԋ/RZq;Mu['ӎ<1YǫЊs@q/!sNPΟZ(|QXe% r!߈ ZOr*r"n#Jٜg4Dmvow߶;ũv`~AԹ:p~rl[3y.jc g[yKt;f/ؗw^29Ɵ/C)C}(W1騕^>`wnl^z{%ix5w/wkV_:jpwne׾7~{o7Ӿ}[w\ 0ȶ}!?m]bAUvD /v(~-ܞ2E>}lbk98cMҋLVv=a5.fT\SZG<4S1͹ǯܷONpTe|*9':pCA!l[r5daim 'aڥU%c,9cdO* szJM[)9a͠ӆ&c$0n<6NG82TS`JIqh!EY:IsVBk\ڐ` FG9F-9ݔ)mq%jlw1^'qbb |aEMjeZ!٦6,~?e{Ҟa:ל.\ f=Ax51ԉ [`J:~;g=Yɭw^O:j1/V\;fܭUD%R*ZmucvKGm՗No|-5+Qm\]c|S-j;1[ykm=5w/5w/wkV:zد{> /5;KD\Fu)*Xnw/|~-7v}Q( o!"6㏚ݍ(n7HՅ~"rט%[=u.S.+&1f=NO壴MCj€}q,I< ZJ,Úbg8C eI>]ar. 6!9Hbac6}Kԥ߸9.Ҟ^病@xt4W!y[\ryzr?/sfb.._lU}paE+J<Sb=KncA >I{O+Yj7UZ1{>Goge8w/6L [Dwj{XT-19٧ngw ε [[Ż?x=j;ΖNo>s_?~ng կ纈e1#Eb߰ly|?/ ܽ{ puQ*lb̰S~ E#h̟Q㰁'!׷EtS [姷ąhIm;IeE Dz 1O rGƣlWm]I2,T>Q<8hy-a7l`gNkK:|هgg).6(`| q`̶;_ո?C_-|1`*~$^gkvC'lȷ8D}}ϸG;FW>Ԡnj>1>;>VĹ|/7^ϱ;1fO glm} K@IDAT_w1{ ̱Mh1r{>?G]s{Hq>_j=oOyY—ZkQX~+ 3 h\go_| Fxy{/6%X " :¼'ׂ ЂwxB^h苀5Bm4c-:^\ad&J-213!u>DRxMnAT@k1cAX(J n{42[ 6iжyft+Ř cꂊjqE?~Pn|6ɉKne.u۽GLq!2հ&* YĄU Fs=SzxGowձ[I$Κ`Èz#Hôq 3>-yJY:CkKä[^C]9 KRj:8_+_a\a;ݟˆ-H#Zk=!<ְ'5>ڮ=ص]*cv 0ok1I _9_mz}l/Cq^s ▵Ժ_p)?̟E # O)qgN!GmF29mxCo֛ЇͫBǔpQ^уȅ!<C_Rp;sϻ# F /l$;cG.fBx?i>`?1$&$ދ%zکn ΞQr=Qkz˨†jܡ/x}k6i{}Ag b<vಈ!& |g6DG|1:1d5^d L{6|@[a-C;O>쓙@Mz|Zzix,E /9+/[?!iQk[-Go'qtUqGm/]֣b/㫶"f9j6b7GoM9ކX֓[sxů]sNr 2f+gy{77mWoaͅk\9&C n/{v~L=,.ܣ';9|nEb1>3ݯX['1' 3%ɿ"e{_Ьa6ko񬤹+18㔓8<>o-u/|*^i19nJX|S9ͨcf~]7~/JFpX(ǟ\%0tz:~"x{{4F\Ȅ v6M>* 6?4nc.8Ϋ~7o7ڂ=EڲhbynGf#w>wwcw;|a I%f nAkNEj 9x="wM@AlYC PդK gIÐ91N 7Z4zAZB@LFbNdu o8i%–sac>:Lq">7r:"_MnU\*1FXcèx c}- 1>T0F(4s/8GS|9b }h~(?юf8Rl0nF涗^+>~!EdzAGcCl,p>5_}㡚g/0=_nF>crCO!+,˲XجNxʛGIy Fm ? I_.'L2 x͏㕂滘b't>Yr&Lخ/n?yfx-ݱgN-kAu%&<>Z9^1r|s]u=fw|9/wĩUWS3_vS?''ێoqI_]_qns㦶?KZe|ߗj|KOϯ:~X??_o/~~T|v\6\coQȇ6. Nd}_wxS`7\+~l7tw^j+b,*ĉvlԂЖca[/tj#Zw,Sun̍cQ]ymCn6dr~#<(r[O˒ 9kl)^.WlmU?|>yCSȹOz>0nO P%t ;E\x8IY(p%u :>/9jqp :,J-Ymj2БhFS`[e|8u\:%e:=o`{U0o_Dv Y׿*s"O6r|Ç#^b_)Lo<|ָr~Z^$#?e`w0׋Q^__?^w-=?7X@M[7dC@֓oqw}j;x!g jj.|>.=''[׾Ҿ\~+|_ʏKQrxՀ- bK(.ԀxbwoiG{gwc5惿:nn / H''hxuG14]s|FznT,6#ZOz+b1P6fAZ$b091qC'Ͷ,˖XC ?U_M2;\щ* *%;@'X |0SXR|s~5|@؍[11[ X Zjkb+?~D/>&KЦՀ qG:)b~P0c^`-Dd9C~t٧Z`1fSN6= n%N3PqBc6w`2-)G5`y6>m+.EŇCq٠z4% 0ceɖ$SSq66C_9ʦWsZ=x/]O#ppF}xؓ"f^?dYZaE=G/qOBxCm>_j!J7VMx1Zz_M=.l~&iz?wO̸fnf3s_{;w7͖1_v\}ŸP:XLcRve-wv1~/xUH˲ZoԬlAWm[Ʊ[cL~%#n+GY9;4rS\aVɓ'UVȧř:]/J_yZGzifx3ǂ)-}؝ 4@{>t,zV`zޚ?|'m}gpjoi|1^1׃6b9~%է:agfu=lq?HǏ>o(ql@s0T~㦲GyC:y #q( z[PmlKY4:DY-;]k8kqC+aQg+m9"ȱ"8ًp, =s.lprpb4,2mO9ys8Xh1fg4̂7aY68F'3w4L|(k>>/aA[  $:r7:8U=lXx5N{]JUOI.1M*+v5"_֛9W&?(Y9OL%-vcLэ#qO؞4n=Yfy((kRkbm\xs:as|zF u o:K:7Ԟ~.vQ\̤5յ6jCo.Ā}XDlX3+ybW7H>ч c9:D<z%n~p鯛n{L2sc˖Q=Ub,SxPbNA? @ڳǫBOgقx,e)qұ1FK] fN8y\9l ltxu"4xU|FGA쳷i}ղo7I+Iqf|ӗ֣3X[sQo[ykk詎5{mswJӸO0+]Osql.??s?p+>[KџgUB\rU@nǸUԺ]2Za{"? Gmoׁqs /A&ٍgƊ~N aB21#)u8t8Vb 2^ 2eWAiy;8θբi Ku9;U,x24k$hQ5e:>BYw7qϡ;H1  aȳ8I`cI`?[Z9' SuH:tP4{lUA̹`EԬmy[Ka`j}Ǯqpu 3TpU>xsiAG#9üx|Y˩b ΡSfz͞CXPވ7ЃNcq՚ѯq&vd5\D|_?Ko{ێ ]ŵVW}؀nb;пӻuՖlwz9U;^7>Nqi9eĻ_i'-w}=!?ZnFv߽yWv?xξ0׃iIPQk>OT8r }zT fV1Y˹4\ yus ag xfța`xuh!]\|.)ylf?.c*}ISB1(VZ.cl|c3WZ1!z5)wx|LG(_m >Ģ٨_U.^SS~KW0q4=ʷ0-&P{?lrp0M[`F5ƒ}W]a EKK1i&uTYȫ~>_w7?e|eeL_qRby3>b\ 2.bv+o|͇~{}{C`[Q PRTK>cV3 XmG1oq_R7Bbjn+T)xCRzzί#n%VLz]+9xH7!W*qhH+;*0LouXv K谸˛ ^ ǖPw4~daqp0sD6e^QS{Ps^FRfh'}"B8{~ Bjbxz<mRCck1s|7̏Z98T}pnng㧼=bñS-OҳN̳Ñ:E$'o3> n_)؂jIѫ2˥>ݾw+w{+R:S.?'?zlR_H,?qcQUʶ| .k\"Q@8x_kvϱ?#?k[M^ 5617w#Zr@aZaM~ʵya 6O;@qF? ho{BG6&}uS򛛋sJUXZ'׫jnjeh,zuxAXW[>1s; "At3ϒF ޟ5|SAO 96ׂFf%Z?tE'킙ZOr GF\FNŘ_gsg~XCt/p1eⓡ Z>Xs!1n}I^uNzg$lmY'o] *]|_:ɷ6|YZ%`%bp `_Ou[j;#mW Rl/?swݣU[4>_{|wz}6>G7ys_/ɛݽ[GK ]8^o\~r;.Bq`Xn z~=w7}nzC")CqZ@}69ACN`>xa!-pǺj]L|#UG A}SdE[ˇ[lN#`"Z :\[uoVϹL_=sīV9Ębm1iO9"W2Ʊ&[G릅fb4hprd,r"<aSk00 0jA>tM`9q& kl %q|ϱ5n;>Flw^e-|7ĤRM>`Զ2G Vvq9qT=MӲLtDrӆz׶mtVbZN'KPHng9J'-F=0b(O} PE:^%JI;K 6=B]Wvn_Eɝx1~o|/G<|]J_oKUG:q5?Y_{Æo~3f;&o`Fl |gn{ vg>;X|Mve}qbQַ̀bF7nkb +X$5itl XqWJ8:6y6B&gֶ> VXVp`EYX*S6r9"Bhox-W 8ꉀ8QIN@ɳP>8)`my=84,` k7C6]#E,SilVU0xQpcn4HkҔ~>9?\Cr'ts$xb@ۢ!))s|&灉?Al-  ǁl@[ i8BQn[|_x7ubE}z,ߏ3/ic<0 us\Əx~U>COtz5_8/Z/c̃jj7iE-Vm0 q<=(U#gm}#ou~efWl<<˜;mgx:C;\QaQZ}86yu>Was|̭?4NykT^yj>:䇥yl1Y_/vdύwnw },x|S/N? 'D_N3Uznuvdz?#o?mI?c7J t+t.Zd׋pIǥ@#f_,`ykv^EϵOwh5֊j(C {>#>1ʁHNX"\(?57b!|jB-؛„y+0|^97[6e,Nx`4SȢF#}Ez!R=B+dŹTʉݘI"+KwF'>>#hZb 2XTA󚶥Zm#-9?5S7-aq΂)c8&#l:DB"E] ? j`g~28&<N%kUɖ(yBvŨ֣ BVgBQmLWy8~n}a{ \D1^O6zPQ_9v+C>[r6 QyN)|R-%.j @znb#Sk[=C|W6~2?=?l* ̩Ș k3?/҈݈ ~}w{_ݾD.gĔp[#anߖwnwWo>~wz>ݾ2w{qϿ%^wWֈU.x$U}~)Wd;츶] În? >{s36O/eZ?ሥx8bF GIqT|P;?mTv Sq.tjnUSX nU띩6vUWkiW6rb/''?I,+uͯe=׌s\OjwnWV_:jpwnUL:j_k;r|S}-?/vo~!vi}k\c-}ApEGV$G_|/{vϲWsDT_-e'N/_Qˆ43ՎܛYRB}>;CN! pS@"xLtO٢hyH6İ}|\E#pf], TU}.{@A,_~^AD>]'.1_зMi#$J@^ZMBbOJdbp5`-@V Ǡs!? X>g)qt$L!1YoLFjGr9΀:sg>q]uc|Q+6z}Rz^s|wz;>5vTOwzfW~e6_i˰o2ͻ;Xo0U\[ѧw;|q{_Gk_ho ұnDjp֦QD.F5z)73iaM:@$;,P.2϶ЗZq IF1t>nǀ:8(Fn0h4qDŽx9&GWj-m/bC"0XykC&0lky;zvgwێَZkv5{o''w5w+vvhڻsg?᧾>an?k{[͋6^wphx::J(ϾE'zzAR6I‍&IJZj Z]7x5 rj.X1e[;Vlt\^X$j,35;xtfc(R>}43)6;}t_HKK3̦PM,'(Rj «8XvČSlp{=Hc+'w»-!g x˟"6 OV$Vrڭ"؞pkV]J1mV,î7=+gX=>? f}3^ݳ[u?,]ꛁz]sx3kjwNw>}>K<}}Ǘ tnߎ}̸wvݟ}f%vv=2nږ)07l-ZK@~-־z6kmXIT#GqϛH+mB󄾽1;-9|^) I=1ydEFsdrhR9[|ֺ,GkPLX(^I {T}8, v@__'p[q| lw 㾙v "/sModD[s~+>_)=a;6?rɚ-|Op]Co/{Sڎ/3Wk^y3f{=gfg±]#?ڗb-zkv͊Oˎю>kAEbbozg=(wi{/i::m騽6No;ugIϧ;%t.HGuٗlQD\#J?{>O3$Qڵ㸸s iMڨ&`Qs Crן &}D'4gnllr)Љi 5 H]7&JrXU" Tipcэ:o wFbDEĻ~RNPh,ۣS:=D8jnL+gi5|^@#rKz9j~Ȟ] \ 3/dĕ1Zl78F2Q̓t&YΪ獟5c.NW92O27kcNog6ٛ֠Kױ!\ΜP&ڶ͵DfZ|'{3)^7Wzgp- 2yW]OXWJK:j/"gEԋ8fg⅝c=~Lucv&^9dZ;N ;wz'f.Lϗ|9s~GjͻG/ MWbrMeM8;kJ#c~VV)}A.^Wmk3xxOwe]/Ժw\ qT"^V&r}G}*gۯ g Ơ\~&Üc[{arUIت2A1j29C1sn@Y1\ =_0⑯@x"V~կ>Zl , ^c;b8O=W Kw}wx|kf_F%aO?>̾KÞ}}=㗌==\02z./7}o_xv+~Ѯ;Ɵ8K~3s'@p~Zm'>dMv7sy߻m*[H <D(EL qB4 n!0h 7VYqҽOl-q$S/t=P r r~A-ĢGްƘv=G>㪟Z3,Z48&x̟ysaAh)$-Fesݬ@{x*mE6Z '&,ǧq]o1x] ;beI?7k}V>͹ՈHތZ7fT3q*9 u5$Nf^)FMQb>,C:PfO E1ʖo'pd8:i#nzAz wV~mTߜ="m"xr{1~pXW|n𪀈IҒq'>?x8-}}N?>Q&aw|'$\<I]_!MTud_QbJ:jWAWtJGWU]GW)UOZOWD%sݿ[NkQ -kv.v_(;*L{YA=~e!`_"8YrYSkǖ@FES/8mNulcQ2 i$mG}&L䎎ǹBπ\DKJ4H&%x^J>x]tV@ŸZ~08NuDZY8TWĥ+MAJ}:ފBr\nI9\ djߏcW({'>sFō.5TNc4AX7^p SS,JvNYj5W  Ł'ڥwчǵ7{X:hsj:mӴucAX7N;@IDAT8ؑ±C $ɉ#Z@!' r$l9-(`ى}?iw~wsVVX֬ZU{sϙYcT9kﳿ}}A!Dp O}E-%SCW5V G:9uYt1=^(Jqy۟Cڙ|#'ILhs)֑ΧOb)k=4=7]'^kMc=iOK{_׾ꯝn_no/nR6` 0-_曀ufWoE(¯~9ݿx:}svwL7Wf~P2G“ otU~O?@NL/p+$pu-?ÓTyo֔'C>Q|:֣ء_<>)L|lZ=j4|7AY7*2=֐Cr0'YOk)_a:1Bk)Mt2t}IOȄwޕ8_P 'tbCzs 4+1š*sDPh> B-|*0OcËRzJ|o: GC"pq}SO9yP$S6 y@؄kNS>K& GcF7_7u  z>sCDu̚RawDyS:/!L~&!PQg`ʻ_ wgu,G]Xbʕ'ƅO7LL&B%Ǔ9pw1+!~cJAZjktM$S 2N# 3&&\ hX'Y欃TzEM^š@ux@ r\{|6^kzp=_Uz[hMZDZx'l%"mHǒ[_]HH 1ꈥZiK?+ ~x ~dwY݃1_NK1zb ?Ǽ/GO__;;x? J"9p?RzHEL+?t?+~R7|Nߖ_|)@ns>) Ar#Sf7ZK {hfo7vq=o.x7m*~,N;t_>ˍ9ɧk/7iy;!|\kJ~#;urTc 1tMD} WԯSM93)~ t>5H`H}T_jzd|V~H"L%IԳq Vg7"~`/? O~Ai ~MT#C:W᚝%H Rs,a}7| )/aH" G!~kxiљ߸%OqP3X;Z?^_VT\?|ĹKڏzZ\ur}ԅ7ؕx ?k8_!Q_It^n Yo9xq=Z?w=ey E&7L*j 9.#A>Vea?1v:U;x|wȧ}Sx/Mi$BoZҷ7"_5 'Ĥ1KimNcHa-*-kAx7*^{3CUywRH>ojϴ ˪*F~;^63=}x[t2fMJ#wA&t4:(_Ú4rd8`X>#_4k,z,zhr OXXxmڄeYঘO3-ӥñBEt|fFgVĆzn>'1Rg J:|5<PβGTX -}q'CdTc6Ahp>ϝ38Vn>œ^[RJ'Ã7oņ& :K)9ė8  O0h^ u"K0&P?~E?~w}_qRdd/{HOb#o_cW-uV> i=a}5hO{OU˒?GgY{z^sW?gW~H~s'?K@poTU\95Wr%Cл2铋x k=ro_?w.=w:Տ.R<S /D%ś'GIjظ,*G ̆<`P!v_=xدW) Ky:#1ʴ$s;})a=Sp_?N5]H?zfcU: SXqD_\ $VGVƂͧ4!OS#7iWʯQ/*jc`?? 05=%SrK=$yz䊘I7-NW(~S4я"y/D)sqǮ%O@H+׏OPg5E>\SAGz1?c1p+ΦxMs#2:q}QS0NMC:(0ZMyJ< Xo9<_ Q"ud8q?;yk)?=Z:yZKI|;@xg|,]~'k{ͦP֟IvH0=Dos\pzSTP5>KU+m_6ڐOӈ:SNx#*6igũbPNɾK3\w2?ieL; jīP=Ncw—̰RXfbgH_l|UNz hۊJ|(|.+mGN3#m$jy&sq})I8ՂvxnԲ6 lule  2fYңЪ$:t O[!a8=4eR~O >'>5WqNe QJbLt0i ӞTf$]=U_Y0ψX8ү5̧1^/̢ިdՕ,:XYʲ3>:gЋgwOf'&I)8Ӯv\ZdA .s0*#6~&F_أ_St&`7MU17<yLIͷ`4"{$돏5gєn"oOe}@2uҊE 7+͓98֖`c]t<b14x"~޺j~{.JR,6npn橒g``^CԃMG)[|%#h=xR&%WG^Us_zO>w}Z`;'2 '_^ћ=8I|MCugO1<| BH9df*ja 6@9L-g7//!t2/sN7OJ[?Q*z3a,&Ĩkb6+tT{a@jUlNx1"N9ij Vp&U;%j+qD0+~4:\aF)?6v<Lu>\8318u̅5$ā12cgdl8lyI'93֧UD<@65EB?7:W8_-:ҋ50ׂ)o8y|߬)oVHuxW0s$MfC+&a__z:+>] q}S)6=/bг.XbD0/z|r?~p~~ˏ^rs&Kß?zS +HrdRH-[M$7G1B09Ih\a LA |6/_1t+> Ce`C9q /"`B8*eG@e j0y@x9_4z2riBt@'9_ [D2{@8%4NzLtlV&0#$[#6)sEh$SF}F_g% ŮW^h _62;@  tnT,d3-2^ϰ%u*'o)Ӱ?֥M텎uq^*^ HZcwto*ZGkDklUx੍GuW8F[uqzݯm|L˳x܎,r7=t^ f8+h,<t,bZ&&2 u%0OG%%>)s_ZBt>X\ghfp*#LUKyEQ(螒~G /0zh84p W\}991J,,zNd =l6Oz qIeZXn_og!jpu%7ƁH"0|>s+2)DL?TOwчq&כ/JisX,LZ7▴42٠Q/jU?`|yKjՇ͟A{yԁrI./S ?~ u=C^:Dӄ@\o9:]oO?{Oww;K>%&QxPxOf 7MX7:'dz(xs&ct>+v+垴kO}@+h^zVj8X!zh>=1'S<,FiJD\GQB7-X'}YfכCОx,׋<4c&-V (oOoP 9iGԋ1_QQCrQ`<~b^9d7xCsI ?nFXobx@Z\qÓaWiɿ[\ɾLJ!ꤎ;d)k8Z0y W 9\MO_C0׷o.}/y|-?NygG?/O/w?, W_QywwR O-{ ^JoJ' ZʓT}"'n]F“hk'_a(Oyɻ\=DS]Mq%($u~D!ScUńo=)Y9o ˋ3G󥇼Vf_W^ן|1.Aױ~Xa; o7g]C_~ϧvasKuϴo\Gq%s2pTy`٢OWX/֍8_ˎ"-p&J._b?QrWϤJ 0Ҡ7w//_Pz)YA38tƈMQ"v̎[swy0?Nn W~gnO>o Sn3y1y=[,ovw#z!7&C;\zX:2ג/{ ʁzӺ1?JRS~et-]S$z[y)ZqQ{ыLq`5iSy[O[}\3,ڕ4Zy0,j5eZK{'ŗY5.Gԛ}/W۟\ƃ m9/6,0?im?U;;;?o;nyb7=r"COopC[c8J{RV5[^~ׇ_?OucV A(Okߵ. VZAloC(zGyjXwģ<}yտw^Go^>&g$7 Kec TZp, 5OcZ~ }|N_L` p#nZOגIք6b6/:sZI#>3@imG#i_A\7 kdnHF Ӌ1a(!c\{'B r@7.'g/5u X0k]{ײ Ԗ}#`@a KmElyiok}W#3(Q_KRA6_$WtmKyI9o2?U$ 缁8?D*1?HwC%Nsz"ib~wZC M.7g/ >GnG_+x#rkAx3iܼs[$q:' KOޥ)[r=қVjz. _L:1F'49>Gy8WE뼟eGj6oE_tdp%8`m(-# ƈ *u/s~Ӯ:nsңY9?YYv~Ysis~Bޓa Kx}~ELy">~-1||uA?g/T׷/L?9h0?Md`缂 7U~Cwp܁>~#7/w9~;?6Ǖn^`%yh`qsѨI3$Q18tn:̵Ag]'3Fw.1 E̥Kd5ƍ#wC'.J٤2g M0=]`8ji@N4ɍ8L5g)6t0?*{ؓd hK8dj0nv9=Psb4}[eĀJOMƓϻ \Oޱُ/1 Sܻ/1i4/p&DB_O}<>M+t+ )#9w6?+oc_-~fhk缏};f>u7sa~oko|gW>$ &x:}D> 7Ư.wM[1]rskyWh+%|7pOy5h=/Z|Zá\zxt^K3b֡8=Z)|u<[jl/鸤W=c]5';eU']CS <ɱ|9Z[8>拹"s V,/ #z˵w'W'O'o\QG#YQY\G|V~TW#Ԙ΃1c shb1v^O՚ :k e_Aq;cUltWkN/_~Żo[P7MZ/D`-2 o&R^@LVMq¹J8zq>~SDZO"q²N_=ߥ?o_*o{-~xߛ3޾\gg6>>]e>+?ګy;~[>]k7^=ݽx7s>*78!/noۧ/@_#, nyď2lꔻ7zӗzccv5Η-OZ+?;-~?Z=<z7;EgYܜ;N~!l =7\{c&F{\yGz3=sk>zPzP瞴?p{Wg)6m5~55:c=ůi%픋5,-MkE'?z$Һ =*]Kkf}p 2MNJG^)Fza#bxsμ[\+r\i)c`KXD$*%*^b/鴇`Y%΍R~W۟O~Ժ:3<J֟m3omθ5rD+ՓmghBF8m&pFϲ~msF}jz8s&sQ㳧w22c>.]oOw>z[&믗[S'3 Zd(ru?nzWJ׋+qkr}n:i|1:#"bdzʫǶv>%m̱s{HcY+T?tIuiNsej@3vįx]Epv0cȃ*1ZҤrcIvrW:nt-\@'е`mXր. Q_> @?wo ~̜!^{?%9K {7O)sI[_*ԇF~PƯgiuOPp`^z0w>]KT_199b:t\ڃj~Ra)~X9`tu|Xҋ*Z=2>v?S04[5^8tPXc>Ō{N9y_'T|`uܟU׃_ˏuwo_o^xF9p7ܟ^%?P?ts;vKѠ7P7 7r)7qo7߀s]qz=c=ZOo:St}| |_UOGzo^OU9WGqOo~׿V{#|zXBFX пr)ߏ xߏ6O¬fqYQ>hQX*gϱyuogϱyu8e 縍e 縍e9X_? >,/ ɻ7C ޔo oH܀a rÓEŁӛN:ܠj},7IjΟ=:#]Kסsk,3it+ 5OLj's x#qOXzqo.a8^O|щu#1;#s@^{+(?qwʗ~7?onߓ?w: qwp}W޿~߼kyowMkg}O U=/ v ~:ҺɦPN3ΥkQu)VVٷӹt<_u`.qMÞ!}4g^Z)֟2_K}w?v?~ݗ?v?ٝǾ_ߟqP|Q%s?x}GsK>b|IGyK}-R/1?祾QRs^;:(?yzs缗sy/?3)޼^~o)27/`kE;[:/K}GK:?[o)Ηtx(9/g~ΏRy/Gכ3?37g~{K9%ҋtš,~>/F..eq3ޗ}qz|r?ֿ~]ϸ;;;;X+*Cmkym6Ѿv:=wk펵uP[~^[ͭvݳsϝڵ~cmOrtmltn6ئ[u_S,]:-F੍TZ- m#TZ m3੍mktLu3SױQ:*੍ueTpe xZcժm<}n6ئ[u_S,]:-F੍TZ- m#TZ m3੍mktLu3SױQ:*੍ueTpe xZcժ\z7䳟U[>փ1#Xspzcu?>X7lgqo{m?ϭ;zct6wpwpwpwx +*c<>s>Mh?؁9?Ǫ]|ؑOߛ3?3~tzcu?e~soW⭱DS]CtQ#Q־k]J:⡃-KBO}A;Q־k]J:⡃-KBO}A;Q־k]J:⡃-KBO}A;Q־k*^bƀ!9|DFg s>@bvi.^s3?͆ Ϲ\f~Λ h.^<>s1 a~Hdy0?$v)\oמ<>sle缙h`~΍v/;;;;F)j|Ms_mlhkl|H9?¹1?G8z#5gK뵸ecOm6sfs hfhC0?mlhkl|H9?¹1?G8zokN;;;;/*pޜ9~9soV?߯3IDATk9oϹZg~[, [u缕3?Vo8~9s9{s缗sy/?3?͙^~8[y+g~έ:sgᙟsYxo3?|9{y/?3?͙^~g~ߛ3?;;;O~ў0?gmk5C Yyv=ۣ▍͸`>+g?m6sfs hfhC0?ml69omk5C Yyv=ۣ▍͸`>+g?m6sfsAhBjA:;@aF.zp~Ld?a/@S2 t1 aYhw!fwCC@Gz}qu]Ώ'1e c*AZfb916 >t:yV)7sA|V~T}?w42AugGugs}"s>J<9p"[yuw}6jsfG0ﳵW+ϻ3 ~w{9og`~+g?m}Y>]n3#ټ;;;:`^ט]o?ϾZ|V}͸?cY}ukϪU6>|ͮZ\glvBzfgUrgs6;fWm}}6|Vn3#,~ϾZ|V}͸?cY}ukϪU6>|ͮZ\glvBzfgUrgs6;  V5uAU U@UU 9V5wan+96 (>5=wu 0:PR@q<QdUAjA:Xsj=5=òVsmP|jz6)q<`uޥ.x@:(w)q:;(V-^~9Yy+?-~sos?sޜ9~9s9{sroV> 3VgZ,|ξάZ!i9Y|VVUk[|r,zζάZ!i9Y|VVUk[|r,z;;;;~ER\5 pAFN41'ǨYq3:ǰYq3:ǰYñWa}ns,wUXykX쫸^}i?i]׃cbȌQY9fua_9fua/@c:ȻXNH!u_R7_~=z!uCcwpwpwpwp9hczzNnϱ~^3~c8/_cu?ϱͯױ>H?Rk6u<}9J:/wJ:/w׊]WH~bu=8?&Z~>Fmao).1Ǩ,,-9fT5 Kq7S|)`} ^+;f\?п!BVAz&u@6Z S:*Z  gYP¤aFaJǔa^E[aR`Cwpwpwpw=cs]o9̸֞={#cg\o;;;OEeYg~Ǻ8';06X?y?;җ}q79{s缗sƏ]oy<]~A52)VZ<9ű|V9Z}G묃(~V: ?W8AS?V oiCc+O-uϱ:k[qӊg=vzg~myf~m6sfs hfC\[y{n|7uϸ;;;;`90pn֙V> [u缕3[|VZg~[, [u缕3?Vk9ooUn֙V> [u缕3?Vk9oϹZg~[,<[g-~[|u缕3?Vk9oϹZg~[, VnYu_k9oϹZg~ΫЄXbUU+t8V5w\üK%\Z~"^fb91e c*Aj@C@чb vgK KOc,A,#T82rL:cXm!} uv"Snz+K&,?%&g宷*Y؆p6,'->|VnYuUg>+uϪ3[|ukulo}6|Vn3#,~Urqas}ͮ2یmkB[~rgUnYurϪ[\;;;w+*gנo[->x}~9Yy+?-~sk|Vxߚ33gUnYuk-~3[sg>+z+?ʹ5;`}6|Vn3#,~Urqas}ͮ2یߪﳯU_3X|V}ݮZ\glS$UM vPUBcU ?@Ħ` hjbS0t8jaU vPUB|i@:(w) P] pm(ʪV V5uAU U@US tx:MxuW5wAU Īt8vPݦE ܥ.x@:(w)q:;([Y5oUlg?yξz07Y/?g5g_W[fܟ|}]mgq[}u¯V> Vgzngr93Vߚlvx[!Gpo^~_+3sukoV~Ưg5g_W[fܟ|}]mk|o묕¯Y+?mG;;;{~E|νǮwn?܏>?,ip?.G+x~ǿ_ݠ p$ذu ""7@ti?KK~b~ʚ13| 13| {:}AZ?rG_u_ξݗu=8?&5 9cfX5 9cfX2 t8;*Ivwpwpwpw`=ǮKW9k{Oseu9ց:'Uc-Xަt)=yFE(t0ܨR_@G̷գ_LuȒXQ-z:dIu,(b=S66 R%̷hp Q1rxKyR1rxKyR1V h3!J:oc-E Q|[=XT,`RgCt0_Ro)3!Kzoc-E,RKp:t?[k>ϧ<{;;;倿b9Vosklsvm?X[/ ~ZկjG=p?i]k;$M6K自uW^WOmc-ju+3USXmCú#Sjp6c|VcQm}Y>]n3#꫖H}6Z͸Omg>T=~'HmZ੍ꙷ,UjQژwg:+lwϊ=ޮw03K y'jcݞ׮pK/ƞ\jBzAt=81&c|˵<>s]o9̸֞={#cg\o;;;OEeYg~Ǻ8';06X?y?;җ}q79{s缗sƏ]oy<]~A52)|9YQ>hK:o-E^|+_-XS+`Rŕt0_ӊ?VZO滴{;;;сKzc"j {O^c|8s_/q>Vu?y^\|?/(dތ;;;q_T|2έzs缗sy/?3?͙^~g~?:޹sz}uG snw_[ϹP9ocϹІ`~l4sn3!66 }k[9so;f>u7swpwpwpwp,έ:sgᙟsYxrz}ZOO9[y+g~έ:sgᙟsYxo3?|k^cxsߟSꟅg~έ:sgᙟsW Vpj^yK1D>2rL:cX,A,#T8fчb D>6:>ݗu=8?&ǰYXF@q k:e tǰ,C;Ac͋| ]aDz}ZqOm;f>u7sa~okmk4Ymz|Vfq>g9Yy+?-~sos?sޜ9~9[;`}6|Vn3:pwpwpwp:|VzXY}vjsfG0U6>|ͮZ\glv͸`>+gU?*Y͸fWm}}6|Vn3#,~Urqa[}vjsfG0U6>|ͮZ\glv͸`>+gU?*Y͸fWmFAVpj>bZ| M-Alj>TU-V V5uñru6 (Bq.twA@KMEAAY Īt;ZñujjbS0tP4@:PUt;ZXVA4A(Bq.t86uYye{+歊w7r؏jgߚ 99qkn+V~lv#Z-|ngUrgs6;#,>ٙgmaY,>n+Ϫlvfmθ5rD+?s6;V[s[!G3>g3omosv;#,>ٙgmaY3wpwpwpwxX^>r,Z9E0gh{Ah{Ah{Ah{Ak.bk+:x ⡃ߊbk+:x ⡃ߊbk/|V?t-|o:|V?t-|o:|V?t-|o:|V?t-|o:|EloCVHfa~gs>Hfa~g ,3ӁgAIdCsEy#X_81&c| '߱g^xmw=>}w?v?~pwpwpwp~^E'clgqo|j]3kxu;Z,{srm?ij'?lފ-a-8Zj્-[Z8ZS[݁{^Sk8k0]5aSjj:p`gz W |sSWK |VlZ:qW[:mq́6pa]/6p`\/j<À6qjuϊ5\5K jc V=.H{OǪ\oIό޺>cgOŁk_/owĵrYH~^[g-=;;;:௨n\oIό޺~[9_;2vr2?-Vq?\s3?G+?4=~|V侣Q~7hzXO>+-{u޼:gR|o,|?*t0?J<"~Hbbrf8hi8h)Ҹ^њC:I;MNs~̪>5?Ͻ_4A& 0s^Aq.VOs9_ 缓~O\Zopwpwpwp~^'cp>?n׳=jA\s3?-^` sUe~k8z0޺~[9_;7uG snwQ ~[9op`,tI cwpwpwpw(S;~y8:-1?祾QRs^;:{jz/s^{^jqy-O-91?<8编=5s9?zJ}yJ<9pwpwpwpww_a༞ױԣz:$s^Rb~+gC^O(֩:S_Z=9gC2?u,(缞ױԣz:$s^Rb~yK=zu>բZpOOk(缞ױԣz:$s^ł&Īt;Z| M-Alj> 65wAU Īt;ZX4=A(Bq.twA@YE=Īt;ZXbUS+x:MA<@Ħ0;ZXbUU+t8r:E ܥ.x@:((XbV^sks65m}fa~o9c_w3?뎾缏}VfhƾF3f,~hfhC0?ml69yf~m6[yms}6|VƾF[\_30?}n|7uG sǾf~+_3X\oc_ m3?m4sn38pwpwpwp~E{zmVƾF[\_30? m3gmk5C Yyms}6|Vn-=ی}6jsfG0U6>|ͮZ\glv؆`~+oc_-~fhk65m}fha>+ocіm}Y>]n3#uQhBjA:X 65CES tUU jA:UM pmeMA@K]"tPR@nSGuwPVBVpj>bZ| M-Alj>TU-V V5uñru6 (Bq.twA@KMEAAʢyb+?oߚ[Vr,zfgUrgs6;#,>g?ks6;kg#5쬕Bn+ϪlvfYu[!GX|V=g3Ϫ 99[{fs]!` VgzξZZߊw܁VQ0G KY0t8Ӭ@~x4 &6hM\ `x49vXy>l:  'ذ  ':: 6,e 6,e DXwOn9Ӭ .Cc7quM\ `ip&@D`Rs&@D`S  Bp$ذuw 0繝:C~ߩ~=nϯRzH~=:Cꆏwpwpwpwi8ைp?RzH(C_RzH~=:p!]<ƢJq<=cN-E;JznTo)OI8Z|9p"s>J<9p"s>JyX9F/Q:)`Q8Jy[_<{sb/auo)-t0_?^R?p^;:-1?祾Q^u[}G/ţK}bҋtvw^ۥ]@_~ݏϧ~>}|EOvgswpwpwpwp,ˡ]Z g|j}Wt_dw~}|r?ֿ~6oE(G|wԹ'ֵYYyA}}|Vlgt>˼y[ۙ;J<>˼Vޮa頾> >+3owX:owς6+lw޶/GgX}q}GsK>+rQ3ȍ$a NrP-CPKi "9.Ori:4>f?ͧ8Coﳒ'޶5}+e<zm^kV&ʎy*VjfzO_}yԕV{Ÿs?c}k ܷɓzo~G^>ث~|꽯w~s o{e>n;~#GGއz} '_8ve>SO#p'?HDInSmZf>vOnՌ:O<|+̧љa>u',d(3Sާyg>JDa>O:߹BLLLLLLLmņǼ/_̧Vg>5U|j5~Ss^̧V+Nߗ{tϙOͫO]̧U̧sSwo;}_Swߧg>u3W3wϙOͫO]/ԜW5|Oyk0000000O"HVyIENDB`ic09eԉPNG  IHDRxsRGB@IDATx YUޟUCwBED$bԥLVbbh&Nf,qVƘYę8&Qii5$$̨Aci[?a!ihi{y9~߽Uu=~콟s{sre>|g>3(~QOg|_m<2_}'3>⯶}Os~/_>?EW>'9g|/j|]WRh"~^r2~x Sj)yv;U`g:UO鳵StgxNؙNSl--;7^SvS>[K>EwN~捷e'xEgD>‰f/;y~^k?z 'SDw)hg^Y43>E[yǻ(~Q||/W^3G7^|@@@@@aâ\EMx;/>kQokSMƛ7kEk?/~|%\n)BK]K+R_~~swP]g9N;L`|@m5qχ\@@@@@~^^}>8|%烔zzzX><}\S{z}4z=X>|h+C[~~0>z8oW,=h+C[χ> iaZD 䋄 e}3>E|2g>3(jd>Es>3_x2|g~Q9g/y3(~QOg|}^^^^^^X|E26>o:?~a7x/j \z/r.e>[g}3>^):23j)‹N^T)*^zE%‹N^T)*^zE%g:e'} .[㘢‹N^T)*^zE%‹N^T)*^zE%g:e'a6?_kT~‰N^vYyD~~Q.o^qDY/;ѬϼpY?Ntٟfn#?)^vG43/hO/WEM+>e'N4xى6>SN{zzzzzSAGףף@m5N{=z= \SAt:>Y{=z=X><u[S{z}4z=X>|h+C[~~0>z8oW,=h+C[χ>  -K  +wףף@m53>|h+C{ xhr/jM'3s7u<>>}|S33s7u<>O>2Es hƋ\LN|S%ErϢ9K>FWNY"ǽYxI?V7 /]9gъf%+'^CAơqe*,g%.Sgь? /p#Sn㳿E-j|Es>S~fzu~fEs>S~fz./wWWWWWW0T` eQ}浟~^~?9xgAA_>>r|:K5 _[%~Hdx} .-߅:kiZt\Dz>:2{3ާ߃ӏǸbߏ/z}{zzzzzCh+C[χ> \SAtꇨסׁσ>yp;5H?AWG3׃χ}>~׹^^^^^^Qv^^}>H|@KE~@@@@@@@H|Pg>3_}'3>E{mg>'E|2g>3_x2_}'3>E{mg>'E|2g>3_?}^^^^^^X|06>[4ޢ9g/_>3x{o^x{o\/_>3x{oBd^;씏| /:^xQɧSx| /:^LE3^,e*v^\ݪd.ӌ|ƋϸYpY.hrE~/,\ /x3‰f/;g#+e':e/>e'N4xى6>SѬm^qD/;){DY/;ѬϼpY?Ntٟt++++++p*:?>cl_xSv>W:a?SJǛa_P<N@}yσYA)_z> @}>F??|  +x^zzFFCmFAL^^^Vσ> Vm>^zzzzzzzX+\yxSMϬAAMϬfD,e*,g%nnv^\^Ǖy2͸g˼pf\3^|e^L3./>2/\qnw;AkW423.eqxyFT xDw)hg^Y43>Es>S~fzu~fEs>S~fz./wWWWWWW0T` %sM>Xڏ~I7آo^x:K5 ';QɧSxri z-Zy%v^\E29Yp+h]^43^tnPiy8Dz#zxvۿnG4wW.{==hz<_Atg#+ei@@@@@aW ?m|h1k~~m|h:yp[[[?'V [O9-[Xy]"Y&AسKK[$t]:}qk8uqs#GW} oZ/YI_WU^`9^}>x|VVO,8<4ȟ9؈dn{˰5܈M*6k!$]%[LPڅ`Oriyln-} O/n}9^E٧p::v烝 p]4{WW`+w[}|yi}sxI?xV lzp[5\cwl`8߸Mhqە@`uUPA/z3` oE,V52.pGa?;-/-C[[^t_vB{z~++v;$yG?#Ë+ѿo9 Yvi7|o{DLto{7EguYkkHíG҇paA/%|.ztm~ǚ+tؽ B]:zc+×Kq}Bh_Y? `b UY{Q}VYɧ9uu `gۋleőu7qE \ ]>a_66\@/rz5o}ͣ֗_g_׻_-pQOm h1Sbz*Ay~7?xҭ|sXW#"ܶg|e=FnF.ʮ"ps'Ƅ9qQ.S76~{p8b>Џ留nzY^s؊Ԟ8=}ñ216PG}ȷҝXC( Pa.IB؊OH6p1ně%wcxAoS?##=Lg|絟㍟1[\y3>m׻=ҝ8a^[mo3O=M5{6Q@m!ϸa97UɻV[[Fo'{^x|⡯́mj6^H`_CD o|6hxs8 t~sʵK|6g|3>L[~zXuQ1hkю}ɧax5iw/޸wqeŸ _[ضoRk{~hoW~U'!; k6Cggf%siTUoAOjKnÓZ| z ҋ}1iϾ#6~ e1cG}E^ũ]Xqw@칭+ jSTzE%‹N^T)*^zE%gzP4yǷtyPN43/hOm|8YyD~n?)^vG43/h}OVo 7b{y6'l2ʇ=+:٬iSa^ƗMa ՀH9Fnyqq+i}O ƽRxD;Ov3OC<]/,"o,9yƉq5rXA1"_#.,^~,vR@wYyWJo_N{K|g_ɀMxxf X-+FmeӐ[x [!7iG7r^]1\ܘ(]R6nx#.@Q!~ |Mw cbv'٘t"dLfG {R[񀋁/ iW]3*s\z<^ K_qE$<\6J¸lA}qqFz(g&fgܹku NN7,˼_?wf?xnclal8L{C`@==@߅ޟ3p^_r4#[B^,-YZ/@>/i@|'Y}6QW^k2"rNt^y#:+Oѽ8=^o*J^^V7wؔs0 > vn꺽Ml-7ͮ7~U\|>M(ƞOC^v1F@Fz~D0lp|ֆ%}#kbO'|Kz/ ዌv>orއ e7fU<_qS{o_p ΥRc=3U@nM6%lbhu-+ٙlJzsܹ_3}t2iC0ʇzOGj{#E}٣iiX. a!߆!F:aH{P&\y 6z|?o<3#m精?Nlл mؘHԻҗ_׍};G?"@;kcۘgaV- yEs' ~) (ջ `XWr z}"tb[#p~m_/N{e4*5~ۯpۅn_̇׽{}6b, b;&$0hy4މwZf"7 ڛ)Z~nPsSiACLAq|3>n`K>>$)z6̰3&C,kp_nЊȁDd CoTAL^ F㥟vͫ׺Ǘ¦{4NᢷzGN)s}qx=|0dդ7^}R{__úo.oUoVcͰѸiDQ?WO>7}LrD|Kqf7ҺmyT=7hݺkfdxFS  oĻӛqx*LoTq5߉oe7m=bi,ˮnٟͧMOڏ6ph)t2ʮ/ 6솖xKY58,#4ɇ_LM|_/Enfɰ uCod#c ø g6JAl?30o(qK=#b׫Fά r((ێ?]_)[5_ (P5~Ŏx[][X}&VݛمY̍A/DOc~{"}vA46@ Claz@2fLs%==l0zKj9c-J~Q{ <Ƨ3E457IJ@m9,"'X> ?7—%\ ܼ4<85PEߎ7Gr^Nja>̣}!^ \ 9^>s9MEE[d^ݶ?TY43junŗ|&w`خ<8\6SIs oOSM<Ê،&8K9k/M6N$'٦E <[/_`GLzR@,{[3a Eal1tFSm5n.JZ[Hu`\%j jF5^y5M5?Zo<6 b/(? nN턒|[:F@6Ӹx _OQL|62_qOf\ pF'πph, ^XgLYxHynNt^D絟8ϋWy+SWѬϼpx-jٿ%x"nVZmm&者l,w>7~g+dsxc \h (1t k2g~Q9g/l^W߷qխa>ͤ..X 07>+!D|>7lCZ۹}rrW6V1 H:sYոO>kJ1V4z(u2'gq چi{' 0`D.ӱ)J3 6mM%.V|n\u`?[|ԀM'eb-Hy;<2hdMȦp3 -wiT.&$cx>6Sk &Sv>1,zY݊Y^ j>ܿfxsîhXc6.[g>oIѯ{c^2^hoxN%[q1K+cG]os#;bAQD5d:3{ 4%B _A}c'? Z/ZYޡ/ag4rV6q|eyûXȁ@aAb:)>\/;&*StQyOzE%‹N^T˥ڎMpny# u=,_ErKpJkuJ#WW=eڴ3=ioyM> Q|qsDߒܴؓZo94VƅvBX Q&-R+菄9z}ἽMt5ؖx枼7xoF8>b.T8GU8+>GqcyMx^~T20P U G)Vx?ԼP/;)+OѽGqD{o͙t+x{7n]]>+&[v}պ] RƯYdFlOxcMتekO'd0#JncnIa˔bLBnB(E|@)&mnroB$ߵ J̦O{g[OZҲd,K1i4XLG:w5@ct^ڬ F<#f7Go_XeOwmOaa􅭭7\뮗!zwvyuX*fz7V0L/<[ _ӦQlƾnVaa8BVjr;;ayc|^ՇqG`Fĥsm=qqE(Bx[XMfEem5w X͚!Ξ"S-xH w wJ֞#A53P97mKߤ VA+1?c B%_[QV)DCk>G^#In`n;h1ڗbњi;x?9uqO݌`M④;̇ͨ }!^2Kw?qqeۏ rh?.Mz=l ˛w,/݃DÃ-#Z6[|a7=rֿ .#yR$n4p~>n >f1dm90v= l+?fʺY?3dX7}ZwluwY2n,{yp3\Dȳcjp'C6o;+$+ʋS{=oocG`G{Vg]AP/b|1'?uA47tG?ل7&zky51߱Or49I'\==ӰR8z)X|=p/A*]S=(k<s56ΒP *[&kMu~LcpҮe3?o|WkM[Jx/z(f: kiN<f7ݯtqV@e>g?}gWdzeh.f :ds4.M8O2Eqi|ap?,.ȯrzrxdbԏUlX_y2>f#8<2/z yw ^qjٳp%QɧնSKyStSpG43/hOm|8Yyfь|ÿ|=ouO|J-b[xυwXC|,zͼ`ZDFm4:J9Pxa,i-WL]qrl)닼ˣ9DUk S1.5 V PSJV" h|1Hnj>p.zO ^eF Ņqzɛϛl>K#B=XMqGcyr~_n ՘A@Ќ8s[qcǏ _dnږS|kn㳿wp/>:;A{vxbebkx2f" ؀?W_j쎼Z}r{EVs*e8-q͐0 rJoÇ)"EVF~up#셬K nۄ}#\`2bbDVǮX\Hc !ʜ|5]< &ض@0_|k V }:~y/ SKKWڛ^\{_v^ j瘻?xRg~qXz|7\j>Dxc%}p)QU|vۚCsٸf̄B4l,#g#&,zv–LT(.5kRj/S,qopf"!.EhPa&J_ EEnBP 3a!gց!H3ut`m\kOo+m7o~G܉/1=*W#,G\@+'.=XIf0)mڱk^ۙ:pŏ􈋀.0Gc8z<'80 8q?^sTu;Y>#9(usَ|Lr4B9 l0umqݕq([s^j>xy>O:o'5@)㛁Yfc;/2n7 >놥_2yʼoEAp0}[C:|ئpYVW.?˿lEO3'D)ύ\mEV%/iŢYx&j2J zgŧ# آE:K6i~xE@X'2>Hk\.N,km󢍣p8x4Rp ^.̢; +FNYT^u&ƏVV qМ͂AT3G}v'L]E_ѡl[ "vϻ2[?oW@T{]}z"|Oq#3*SZ㬃^x0Fuab2p ;wdM aQ_8Ï ɒb1cW5R"GذlPh1Q<{> (C %_~cᗾ=qwֳfGA8MXk5U<?zՍ E?uv$ fW@e+@&~_!/0Ԃ&v΅i C#;όg =77Rφ7:Xk8M_G7S^}l\bӦ\e=pkȽX qK7Qr\|?|zs8W aYsq3X[Ww\c`/Ő6ʇ=ƣ`-擪ޡ38wA[%Kg V`]~ lPXX9bBiR$raUq̎}o8Y6 Zy({Y̢Ns侬6!6xP*xHɶ-?mr!^n_ǭ4k VӣOsy2~_.1x_w:?vQ^N=Twgo}?}TeT ď} G9/ZX٢eb嵨ij ]&Wm\L‡hm^ LIOũcsp[pSk<"Pm.ne~,o82b,6tg0za[tՏvh$ur,(Cƹ!HA8_`KdzGEQ%xS$!X:瓦0XnjEmvbۅB+CW7n 'y~km^sy!|D b?h{ȸa*"lkMYvwccUlB0KFf4'ޖ~㉥(Ff(P{9'U ] #A~E^z\CF:0gEu1/y DJe ]W/ 晃0qqvm(Mneɼoy yͽ& D#gɘ?Ә!m[+ TGRކ8t<1iia1c1 X~ox?9G4j˸q׎ ?7~>s;Ws#NEkE;Pdon߼]~.,6گӲik %tt;(VcK$jq3I|rߜ>ܚ.~+1)F}͏Y޴mqH^MཉI"\; Sx] udhoaJNq~>tز2||>Pڗ yo1%ŭ*ΔPRn/(h0nݺ.CSv*<ފ,\;f̲Iʽ^[Dly܆ M)Zv4ⱉ6t4|߹[|G<3q!qՔGxCpT툧e?3?/~Q3.B6'P6!׊9V__Bu#p`_v{,b|3??dlN,,\79CeIae.F5<7Ϋ&3 h]3Ǹ ,w* /On8#8ñ=Lz7H^F`Ï !8>ȉǟX MHTgv};>-=56>Msaei3BGp`ܐ:y>}Hk,7]qYbɒ}:7/e]q|PN7D^oxzmn㳿6Yy_xCBkS4pP4SRى^v_/9$u[YC?.XNx71.'8 bW9(U`,R i9e_ ,#;D3kG_7kp7%kSf_ҡ s%-~ʹy?0xli - Oų>z.~G,Oqyg0 /NϜ/=Ǟ(ЩټA`Ԭ!Rێ`eD?7[-0OW/&D)[Jel<&'È% a|4ޒe|_p&LIu8|+&ةl)u%WN(˥&l)Æ*Yv_)h68蔽p;qdy-8ٱRƟ\mi -[c",XL 荭nዙ-TK՜囖'zai1M#*9˾cؔ?``;&oЂG8y>Ơ oï0ݰ<<6}Ήel|i5O8<_jǷç ːj^c$^x@-8|wØy@3}V|ng<:Fć}cZg:6laA}GR+seLAC'ޕTP_~+Oדx;/G3-;6;g1[xŐ,OgvZ!$g٦7P,fNE=z`jZYx/<XO [oe Db`b>s-[,s_Ƴ6= _[j YKշv%^T#8Y|~ܩUG$nj̫9d[rW`Ъ} l s,MjmG 0F#k^ &#8'@2of977rߓV@S\P;/uz뭭G2pE0H wҤώØ Vq#D<3{NTxf]^X]uט=? ^2|>ƕ^Svq=?qaE};~b^_'ݖ.ypq( K=?[T>6Ӷ?:(!qӫ7@7swE7lF\e-sžqdz/F`ǓS 8GΈ7l1p*Es/OS6>t>h"|dJO_8tyۦ€r"4(XH9';=(3;o[kXĉN7e?oԜ.ssAEpVXh*KA1>"bó~|Fl;=WwukPހM _qXdolM [9qlSzGcmb G:Kh錄_)3;9On ;8CU\ v< ZOSç~+Q#Nl"~GZ<+NʏhF G|wn]q_<q{^uWƸ p+rEǃi+7AM\ǻmCk ҂Fh؇'[.rqbŁ鏙pXf{]#0V# zk.+N DznrZ['0 MR41.eG}<,dx٨>S2rsxY?b7>rax'/څ,y'#9[h:}BAX v:~b>\(>~ԅ"r .$NQ55*=ũ>2ޱK;PgDwXu~%g톧> gYY:g[?RlKMsx/ROT[_;Ocgz7^"Շ.eN}Sy@u|ɏ ̟7DaNg 5|̯>Z+Թ]3.%EϽ}xbk dj }h+:ɲl/pr?wyWزO|Y3md\ll]3 p X*_aXzd YDKUهh&>kS)bK |Y /w?VżJKʩIt Z|aPM?—MQ҇7U?s崩+c^:9{ڑ_2v˾?x ig1]@3csK.k0V!Op_'d0S>\4^ݷ=-Jػ#&|w~ު}Noܧ.l}GocXwO,쯵Wg_m[6䯵wpp |s(.n ,3[3Fq;vx}>d1d>e9a|Kbtܽ gѿP'nF$w_@+V[CڍkZóoxF<|^W8IXjz=N؉>s̙vx3:x͢3n8%e'?;{n8>7gyWm5ʷ}@E.v;bv o٩j AV5^"VkxZV~,,!iN5^ǣ;:]<6|JmyEEt^m#MCÀDywet.wc-eqfPeeNJ_7͋e ZkN3K__(mu Z 0uE`H?EYǕSy]]]w`N‰6>SѬϼpY?Nt^DX<'vTI n`*.F6fo_8_Vbs~9"ı.6}q]Ҿōc-DCm__j9PnWݾ2 ??ÿSc{/=zpo=2q<#2pW Q;)Rͷ9Sf8'l Q&y g q ofT}R5\S,v1+0fK#@_/n|u?ݸ?z>YڎUnL>{ϋ_>|Emc>]m,XxkU|C )7a[B;ԏpU."·:u[}pi$b.(ʵ3vETm](J~HF537w_%W?Ǐ=.1ɱ[m???3sg>l6{4k4q.,PL'6Qx~l\ѻ] eW6A]<M6߰*P(AZ])=XN/] t Euk~kNokHW$͏m=t]>[Nm+KK?u8~.CnBn, ub1BH ꅥ]!WfK_ ӐflqA!"OHjLBQƿ:Y+v۟)߉ҟ|/ lIF [[6/Yޑo>h}(| #y oO?{|Z5i:1յaSrr>'6|wxeapLuöf Sfzfבr^.^b|9z/̼g骝쬨eqͿ,qro_$|Q#.;m @0_\-,DƢn?E#| 4vA~bd/xסĭW}>hܗ(╻hqSKő-iƉϘ_|}sOe'bS[L?w;bBm*9%^PfJbw܎\$`G93@?oGJH-7_ކ1 /pMurQ!6(Hego lYh{X]sk/IlY>ŻՕZ?i0uǛ߷\|Wqޮų7Y_>w~bd4:Lyݓ "B.nM숏abH1WX?J0w7zŷ(mum^rRј5"J>6p/ Y0OZ㗞Hπ7 諞L_5ÏH7ހ_2a _v,&imy8uYæ)Fd]S#N+l>D\;v]Gu̿#~'26b">x(2o8#jz Бڭ߷~>N};z/5cnDj \4\X @oޫ*ģF./S@vM)hV/u,!$Zݲ4|MZQ-Ky :6'. c3|Om xmGM]\kyowa? -'q W!_MQ@8.wS?F03*2^A /n9F1XIyJB\/90NՌ)T|_|KƐ/ 1Fyâvd3o>wqkvgڭ䡽Cs͏-ov֏vgOv-'xU,36rb @O/븋 m(&}I`Gb|<ۻX2/~= R/j|.~^:܂wXBr]ï4 rӅd B"'ߖ+xJK?S/TeӺlU8 "_Q*[?n)՘7pk!0|m*d~_pt|B@7pc_'?1k, 3N/U]ۭY]Ō'1b@ǏQ':gloX̏z 49LZMwGаϸ )>η2|(>ׯD2HiDg^rg*hl9}~G {nѳ)`ҤuUΨZ=@J0=/q=^k;x׆gxLK80G%_ϣ'/ǵ·iė=ci% gzԡIt3񋼅s<7CSǹW˯_o_Ϩ?%sqwY G9pxԦ=Kb`9j'~}s.rO5"碊aK-f^֋~o| aokׇH6>as̀GQpX7Vk1'H荗޴X&fVOb=GoYTXaw3|dž/}q{*^JOGtI|8&0q𞍩l3+ΠG#1z?n&1yS:Oؐ}GwK9 ;L&0enS"YcA"1nE g0.wP:*#֯Ю׀^w߹xCmx=ba(Kг4X"`3.Ffxxә`|<-~ V!J+>Z;kv,/fԊ1ՈBÖ|= xs3zmx!.r:~ؼǿD5N:l^ p)]޼6>Ei=A[]SC>)kyS^-@Mߌ+'{[TFEu2|=I4!{kq}9cW_%$$.> scEv>9մX19b1.0>}W6?:ev'.=y!OWmo{Q~%ʨz`v9mFO|9ȍy᥮ם}?͏jd`JxnMDb?p'ra -X?~?%WڋvJeUgĆ/0t  =A_b)x4U,9rᭃ7{ș o;<| _g&6b>lšQ<(fk9zŠ=?g9=Ƈ> ޣsL፩b]G5ԬK< qCS75 &oNo!'(m&2}Pȉ(J++ ­*^'khi-r3D [xKNSyގʘKc*m&^P-]CR2Ft0? 0䓮®zM]yݨp$Nnm(H5\ɗ1Neyy9y_trx>_{Rik1y~ϧM:4#l\qIM?COEj/l4fK,fC8\圹x*]9Y=(hP C_+÷>FQ<>kQ+H1Su.r=Yf[~BM2IYWqBnxuX @p[H+G.02^@Ѩ3p傇+_8z (7oC|7ee>~AzV6y*U ^~x۠[NkCh=/Kg]Ͻii<~q+~k5޽٩M]d}Վwy}ϝ4MᣏCjgf]ӹ^q/=7ּaY\KUƖ]3ek?Z;bZ6Jh:>'/=ug#G2jSo=(ͯ~x?yK3ZZ'Շu XlA1IʆZ6TCmkC'7~}ˡ> vp`Dke\hYv>%a0a\0#p']"o|с?E9~,5yc*m<Ml;h>Z8r?>Rz[9/ey|Ksȗ~ pCˁ )6mm|b?3/l{MH7ɎYxɎ/CH?~';f/ۑx]ʎ/i2]7Hoç!3O*:e u٪A%XjkuY4~rx2Ãx,UoRۍ9 es*OɰʯW%k_֯^įIڸe33oIa,0/LM<"2">mx)1ȿ59/<&W6_[?gMuۮu=or !$HH!@FC(46X*UX6<#PV%j=PZ > 4H$}9;7\_{ϹK,<_s11ZssYχn6bs0y1{? ٣޺GJNyuƛ{q%4-]scJ-A_KYra7F9ӁJ9I„?H'19a*Ců'%Oh}gs~{};w-?U5NxnNp̰x =8hz!:rɺ}]uI|SȜT]xA˓u?vnOa=u~+ڿa7kbo'v)B{ǯC¿hK[|8z> -F^z\[i:cTʡ_+*O$*39[eKG-ȋ,&VKWd'ͱ]+tz[ _U?Aq3_x[QO<|W{xXWo?}nyd>^m,le{ұ 8wމY.Hn:i_!vmfiDE; V{c<ߏ/jT,Adٌ! ]&>~ƀZ1cqԷpk^>˿[[LJMvGzUA pYdowDT7lyhwxkOuXx*96<\v2~cNf!DΧuxˣ</FԗN߹,kI=L/:>=~,;?RZU_K5b*Oqb*hkm3u+|ŒTg ^qzWG n[^t}?tOg'XNzb1%'Nrę?'[N[,|}4nVUT^5X(<0,M ȍG&@ɜ7&cfE26]uVIe}BK.K/O8Txo6-OχުzCd#2E¹rDL=[y Ѡi 骃쏖>!ې1mm4Z– ?v]]8 3R_r_%-_&3+ 2d^bw_agY}Z_Oxn5I?b_zw|Υ˓U#ˊ߼G~:a'`K[ʃyhƽ lԒ?qJT" 7˄bR<AmSoҽWIu|=y'9_{N|,z=S^w'n> $Dq,}!cZ"cP+dݑEyG.>''gʫCVq'އ(86O ȟx寗Xz(qiyI#@wWeyGo+@y8gM}L|= /7]ǢfOe _U}⍷Z#o"|!֌b]ՙ\NXiI@=nhlfSLFLfږa`QI7h\R>6+tE?"Yvwx'o L!t<'; sb.7&!SP?g^lz>B&8!? ˅s}StXm'e{89EW&7.Zݎ?Q=8RoPOhk࢑ed2Xn|/wCҢ%4h3MKl"w _*+\ "hx=N96/3 ?>KsK_r졭O7pq~v߾-s?PLҵYno=*-m.NJ:op!7ɸb'X^ڬ0krdQ捃 JȻahkNuic8ލQ2Pp9+qy@e߻qw~۪<Ƭk_w5͠$$zv Mr-C Y_1̴ơҙZ#9C=:XCIj92h5󟜯1Q菛sܟ_r`Xx*3-ZǤz1[_vw,d6Zh0 (m0/.Q'8o儻|~`?:;p޿cY~o_ӯl]*G0)(5ޠIy>)qi)z<`'qDux7!9D!cuѡٮt1@?$ /5iJj&"j_+tIEjOY)b&XYG|˷~ŋ~D#mQb߁1>>y( l b扻Ư6n}RoBLCE_3nQk/;y救19hwm |;O_Yy6Z~]63v|6o (Y?cm* |ͳ/Z~6bq9uzog|۶M9׶_lC0Ӊؖ]Q슜w5RjI.Aa8XTjiZg[_Vu]a~/}omOZc;pK>ѓ4}ڗVQY{llFt fO[m:_tt)Q@}`G]ПBy_g~L~TƖԋ'ZtO~y){oebWs-ިoٿFp@+s`g?d/"A-1)2mDX~FxOo^s%סϣxKiX~롗>Ea"4J⺙ U+&oF .^A"ڑZì9sեJYO|!RzrqF?tߧ9ǯ\y)S?GA"4 ڻnNk@•%q/9fxO.7+盺5\?|ZUQ׭}+w-~+oo-~+o[B $g [÷6xg?[-+t1*kSVi}JqIi]$)`0Az9+ZmWw^bu.5e_}R% \Y>>I_D3}}ƴ̯}qN=jv@CaM,Tk.7X 7'<@4k=3?<d&t,7w6Ԇg~8&5B߹ORuR9 k)o }r˴p(?( W7/+yk2ztsoA1R5okTRғ"9XreUĘȽi3|$Ժ ֨o;?f^x€|>ȷ"G=8 ^}VAWl@Ÿ<,?kg?=0=߽(q?-_~kʍ;ok[y'襮^x?_cJѫBZ Ev/bp?~߫N?̂Z:\Rz?EBt(*nu|i\d;M7.UwkbՓWgs(0HnE0@IDATq6k.+`7yqbGT[[B_ Rƥv<k? 7q+u'[UX[O a8iXU/ 7IiTR~-yZW\O_=}Dzxd~Wܝh]<-iŨ$سpP u/e5Nm3oG0{{ukՁ-~+oo-~+_( |P܎g,Gxg+o-JxX0,6,v[F`]`S',4Ncᓜ5v'{P$ Fu): % ?0]  'APޱ7}tg SY쌩Q-WLS. Z\gT "7G n8-9ɜx89Ҹr;yJ??`MEx#W*%giH`J~!ܢwD[kk4/˗v1ې /z/ևNtB)\j2t[Qk<(z(zЖdzlO{n;}fა/ܳtgTΓs;o>[V/=iy>|ȱnԽX&:Irɪ>ء(ױxm%E~ҷ!,"tRI<˶0n1|WW}s<=]48̶Yϑݴ3}o>?/k&G2]SyT㺰'(ǍCuD>N پ{:n7Scer= ڈ(ۡ|=;1_nY}-AxmnEc+>!M AlOt?hcuk˞{?rtMP:=k\W{OEC{"&8V`id|sasN,}&S#A7G|k<+@X+%n7b$aci h?IJVr_x+#WFWN>0wN]M)ܖW?>|r_o-diɱn?I+y8.ԟ:yUZe?s%',:tYUVOLʢ6,Xk!dĢ5kb'ƙ7؉'h 5jOd;?"']Ϊ4dsk\jon}Ӑ唱E8cye>mv<΀hkl.HgA07w3/btv6vp]v/~mL"My7L)qPD=S|.oFg݃-_}3Am6Q-~@J7Nt;;~ŜIzLlqx;O_%>S7 Gg{VUgQѼx4gvV$FA>3 Y28?${J^ʬdNqI +?WaX]5>1v/?yr^S.Z 4ޝ뱫Hh0I>rwՎ rדe[9ޅiA ~- ?m-豯eU G'j3׋MؖkMF/~-&nV7J Sէ3Jj||'CoW?I'N|Jxi{8T2L +$h#n~( 4Ya}`/kdll&:v=t~K{]BHw]=]2 Y'[zv3+˔Ѯ1Qƀ*<:loʊg( S~_ih,3I?K+~7뇓Iٹ9wQ{~SG35nWIDőѧA# #TƇ.1_s^c7Ǵ?z#: .Lc9lι/~+ДL~8hƎ)y*Py5ݿ|/rSmm 1)kJi)F/ғ?'KJ@isx KzgW:9ݫ<&Zwأq 197n/wyU^}MVUF=B+Go|}W vv,? 7/ߋEūQ/,^m^lСi! K?Wn]^&_hIϸdLI~O-7GotqUO5_̯##8^zÞAO@1'N9Wq|V|>KM~QZG?7|^CoJиΏ>KЛOEΟ)>}y|}RzM~,'|sE…zeKCe@z"Ð1_8Y(f\`+c/H^ro}y3Qdl4tmSw^eO@fq51egx'8Tb^;/kԻ|*4O8vUoÀK_ۄakמ9?xY+Ɋ&|o$W9p;+y>pC97_zxw~kykoo|ZtfA}<z<0fGc8`!"92Lp\~#y9>22 @YAF˳䡯>"P}ۣQ7k/h=gpɿv*2WBg= Np2@lgm+HWpӯ:Kz‹>+OnyX1}w]wONHmt\'h\15k!N5L}AW]4 6J4ؘ>z$fmx+'&kIk-yR2M}fi)\:$G')h-Ӱ2y͕k" /vυae_RX11FP΅a(ؿkU6UNĝ{Mĥq|=).Ob|7wC>flc<빃xٵ=U?`n(_=|z|F.|zea"R6 lX=!ߌ%8|=}]\ʿRK]zkxm;sU]6尣CEІ-n+:jg\8xslsO+>#٨=zo˯tHma[߆t~Pzqu}P?lmxXEɥaWT[9H \ͷ!>ay2nG> A;$E\GmygͲgu_%[~E@ؔs۝<%W VG+ }O+D1)rdn԰9a՟8*}w]GC.m00Su_JDZ͇O+Q%ΫPy}> _c8Mh+z9TaHhu1M6٭j ZϱL G" "A9Մ'?I5:>o__mx޲ 8RLAJJ^;X;)a 3nVOh-GlO=pbmz'N;|Z)Yo־o-~+_(~٪!?pܸ-~+oo-~+3w>q-vd|ϋJG|ER,{eu Y ve;bUU"Z?, 0GO% E7Lۙ4|!ؽ o~1arLl=J4aKo ,> Lxl\\kuԔO/Vt$Q ]H҄m[;y8o}`Ư++ProC)&cӘ||bFIip 4麾;Ih{*>@1C)k'\<_}kVnak='m\ }0f17IyPO:ށSABwk||*|j ;7alcB@>@(/^TOyOH2;z+uڷo ÞlGiB]6̙K|k{Ͱ6xߟdŤ^RiRgZ2 H@j^|Q2N%*t?* ĀC$Ey4hv,_oO7,tC.%2!L1srhԡqndlpW7E9jcm)tgs6{36z֕׿qˉ{o+4Ĵ/:.>O"gLyރ> %|,~DiQjnA+L~3>:_pCozKN8|ܔ46l7ߣczarvg7so?, wN^tku> ~UG~-z5ceyL ^lP/Ϭj"VXvki&/XerYe9Y!?YVL*@y孿~w읯 LA]dY\^.; O\s%]9t!3z?MaEgb޵>S}-cR#Nlۙ߈AI22ϱsoPMx#[8C{@nAD ˝O'r6Wꖝt^'nԯSR/iOA ?oc?k?ϸlYᕀ̩[ճ~j ʺ>iN=Ţ>h#;zD zRWÇ$ Sc\zך#Cf;kvS|%;'B{ܷte_Nj]:jQzt )Iz,䐿HVM}i<_?Px[_.k_M?leZiegs7cUV{qRNkO )[.{Ե cR D쐘|mXtr >bgj ,9X{9^xᣡ ˄ Ŝ1w&3c(_$kfdFxy;2ʒk1A>#TEvܲڼC,Xmo$`s2~S+Ojzp;R/?PvQ>>S|@L.?OtāqIIf_=ͯӝ[/g_[.tG'c߄{{(*E<ϳ3.'Tx6w6U^2N@YCb78vVHcQzz9M~b/u\ۿO# |#p\>){+Iygxf{Ous=q"O;o[= g{3~t"W$Ŝwē|Sh-_ $ɓDgűe쎖ů!sY; yerI.z[#cLÎ7_)"y,k=N,E,ݍ//esAڃ =uQŇr%{[A{ +N[O[9W]|bϻGxyA^Z=ڙ?|ߍ~S@;U;ض@{|Ĥ|k5bcހ<w|Д=I_v/;~V?X@‘gK[͋8~#]1ďg Y!7&zr?2$XD//cJ|kz z|V]x'}y0ky_k߮/N'ܟC]4J:7cC0G]OCwo91gCVb']E;MS,>K/>_?)yգڱ:zwro7eyMkn0}Ϟ#[F=ÍB@n{DoYDk%^,h|Ѵa/b~sO.ge 6o\nI_'I5F a]\#PYNߚ)'3'&:'\9 !maˀHt , ֎S|") oQ B ®z2n\Rپz&rs򯪅ᵡ)!>c jl9bt/$ݶ[<%:ަ_$(z_ ?uޗ6>9J2kCnA]LQ::l ?Ɂk?y~G=hŏBrzAnrl^)ˆ ƨp:I~춏_A2~oݩ¯Y7߸^l:NDmOmx~0}& dPaj`;~HpAa6k>&+[s֢8rxv <;꺅Gҽ vC냁mб|sϞ> :/aq@sß;qLXju9Ǟy;y_gUp2/y=~V`x\윎U.v򩋔UvjǾd'o'=e^z' NJ`YD0g4KrCg? w {??HYUoxdwkBo\w}mң{Ƴ/_NnYMB]+Xku=Ŭ ;܂qjLyFK*@ %?Eׯ켮3^'r O鋞9SX(7-Aюwut7d]W~\X!G-",EIx9d>HIK}']\$uTu ~~ G2$̧OǞO2c~k}'<_0n7sOx;!vqt?x4!s~k_=".vxR ڍ(ׯ K'<`& qVxO-~Ռ5&>PϰfQf\Yml{;uHyp~EAmyx6}Ͼvyng+I_%h<јe憖=eL9^hqo Kp2k/]"}g=ϚxUQc98>*W,|989pK;%f|y)1F9>8lxWF 20l M-2*߶vwAv3T[د -ؐW8sٛ}TS]m"qXeؙt?G7D}櫋Q_†!ACR3˕-/y4.N7Iċ>?vA"|4?'.}JdR7kjz?L|Pl f9  u >钋vsP*ʱ܅~ԧl-n+oo-~+_(BqdfDzK8 pgQZL9``ێ9'x{O^Yepٯ6 Lw8\~s/^.mu϶M˧o L9Q!wyϼV9 mDVe3'7 XehP[{0jn_/`:t dsrxMX9W/N/ ڮ'8YYp؃/zdIuD .;,>עH~M>>~o=+jIJ'g+?d[F*TM>>//yj⯭CkSvnh JK^UGZ5!p2ɷ\2Ǖ'{Q|;nxrxE ٪C|"뺢ڂIɦ %l+~tλ]@}κ֛6rᚂAe\/_x:=tgVW$"TԓKm\\4@[5n.;H0d3`6A]=b|/U}iL̯t}{>o-~+w gk[|on?ߡA mgzZˑo۾ŷcc; o:,9ɞN7?ܵE'l\ |Y8\8PpB Nh.s+|cE([/5:o|ym۝ toCO?;.D16. dZ=M`j8/:2jcw4.%9U*q{o+GnPI@dn3gw]h=d/kKnoŃo͟W/^ O(wqx n+̅/.ws>&lfwʿ_O{%A,[4Oެ{woS[I[? IlNo< o+_k|<׾<?uY')b&w/^.06c|-~+y mrQwQeH|uh-IxY*Rh=A+8`gb`m\vLTO>!M#ROw/%Nz}`'8J?zN.<ߪ`3OhDm%$ӣYfps|m_[=hѮ3Rr3v0cSU3Nj)JgV#4_6T]ַ\5M۷rЙT┓`'X[>Ӟr+;[th|yk-R w;oېi?nXn{CvL/_^[t>O||0+iv)|&~efW}T8&_:kJ!|SdZ>rC~ 2=v"uo־o şaOmG g_/󬔃1-Z=',ݓΑm[xIV`Yqԭ~ѭ\~y$=qXf݌o~÷c%<=rH/9 d4X9{̉%J>Ff|= cJ R6BWС%'f|vv~H'MmUD|+exⲒk{A8T4} 5_qb񾄨O.:c׿~̗Q,3n-O/`m~+- qݝ/+ f <˞0W냁_۷} wLCvv -_29—zJ׹Xg(k[xM.zq|Gl?@ =Qrqo·qϖoo7o;k '0irܽJ89u4Sd;mJ_P8%[G,夅ɿqg\ EkFWw/uw<{.Eum#8^0 n7uC!2b<mÅt΃zy9\pBO"Co6I*r4/ g@ugϷo=Uߩ*/>OfU_wԌ(|ZU-_jzOKA>4dOH}>o>n|Py׏[m4ݜ6ތgKe<道M牸+&0΄̍o{ϝ:&_zZ8 f掇7!϶d1k W腯ɿۖWs"뭱i"w^5vAۛ]7_6>2mY~+tѹ7/G}&MO<ۃe|> oя8w,, )8ݍ^xƯVبW\syqh/@^Erȑњk Ϟ$zP p.2V}IkZ+\x&y0~-x _Wȸ)\ "sQ֮̿=o7 nx ]ίmq`Rz> Ƴj_~ko޿|5_yk Jv_2s-킿M7)tks^eNtРo}s^3k~[|>5-Q:~|H7ꨂm폴 @8{h)3G CQör *T.]ZO9׹SYi.+.`&|i BJq!s`+9,I_)sLyO6Y>Y%j[01SuXMJ>)R=}Sd O!sCwc7cLkKu_]V͔c p[Ɲ2;VUG/_^,QD]~7A:=m<_[ P3_ wÞ:v{^GHv=yc'/椗߱zAʐsqrH' K8A/k-gZ,ßui >d0 j?/y">k~+x [{U4z(I% 'j%DWg(Ů=k<{$NCoԻdQ,A4ן42'&r8TnE+M5!qͿ7k9ΫX>i0T9&A^Ǵ=3y[fm3]^Lc SAP>-T'}O~|8#^~~k|s=qcX[>8'ৄO$GLgkhl'+~A=~]D]׏:|> -/zp|zhIR.ls::j,!cU<9p:}\iqo,"Ub᏿g?nxÿcxg?zv~>gMٓ&AJ,sPչS+nyۊAR'8󡭽ܛ=?󷜄;q-!/+Ol#ot8U yGG>j0ywE?=2HHfQ)bK:~`yUz^:΄{Oƻ.B?)_a~iq0Yx^ Rc+}= ֑[OpqfN_4էd~COXz}'`i'N˯WhGgI]ŒUĿs\˽uPyt慆sHEV4e 6 o }%*G?$BA27e6|HcGnpи% OC3߮j'^#FcZY >69MVԞ "0? c+): ),@ 1pU ߪhΈ<NIJ>'8~RR=#1XBkEmVI? x GHDWBEhOUo Vx@IDATw S[PH_;?}k?i6ox+t#}c$nj:}0ЯI@|c;м^gx+U:}Wwo_r2'7XJۻA^fe|/H(d?PGxl2D8{ 0 *&D~'۲qQpBm G9|ε{y{6ƴ-kU}0*6bUp)Ep$3 ѯd`uֈ0[s^ 1=j\y5,iaL[{,⍘mwQL7vS|PtηkGAN|]7'nhs+܄>2 u.\qn%Xf??)y@+jSZy571'ԅy8c\[ɼd>ꋎV[?tjyvx^urw46z$XqI'MB;W8XAMc6)>\'wrHcP>&fu=04ix+~ԃA =>"uM8L"o/|4:x6\ψǁ(S۱J^؎#@$nZƺi@Rt=QC1Я)<cı;u 9q㋮5ˌL(0od:bmunrK듈w,];%`9z_>Y?9ur>Y,CH~I&vZ SŶ{V "{}a#~ xP#T&k3! 3ֺi;7x ;h8ט?vsU߲{ՀhD{R`Nch̩g;1GV^I{ر6s=P-zG\gHvN~v`IldV$wpj (c1uUn Xg'1^ n$UF4 /}i[k{Ϳ ʼn^wbmY^Meۆ~/~|8C7A>~Ζ:Yu<tyi8}sDM~$rA,ZSY[C_ykhl"92=vhk@Ā4oo QhJZ部- I\b}  'Iß/3Ȳ68,<]SN>9$:[dbG4Jt ?c.5}`(2F'\II}D 2GZI)2vlRZies3x=ߙOj&n^{]׷~]δj+qJ|a} gn6gw]ww~cs{ Y?p'$u>|;C.hTߑ <E8W(n:@ FVX@mt> w ygî7m2?\G?J/Xo7~(?l gzOP^@:V2:h}P.kŐ^@be:x!L^ oBSdYnE7Yy_0E{>+n7<كߝI<8͹_5uGfڬ{D~lX;|awShyͧ}![? +cƨqĊÌ"ThwЯ GR93E_v9q x$vPN(yH|Y/Z^`_䓸}2' Y6>R`G-GùVɮ}W$]&'jӏ 6CDۖeU\W?*;YLCKW=;n9Zϓѱg$^%#G]bE{*bވBk8ïcl?#Z>í&yZ1CIe5+m%w=\Ֆaf~`𥉉`3 _oUmTNOPO!㠋e{uS:ޝF;zcj<6߭i+; O˫wz?q>ޱg^PqG>\кY5棹[׽eA!˿eEmw `/}}tity D?}O/h[>w^98h}"XǬDq:.Gv,"?$Dz€\v zՆkoGmvc#/G={DE"<`; gf4.Xj)yznJgm=_h3?Q'RGE*˦_+j+2QhF^Ur|5C^2xRh$^A0Q6}c~ݮZ{om;1imT Z"AH ./@H\p(E JE Jۤ4!M4qbm}<|]^k۱\9x3Ƙ|z35_\`}Z7w>u>Is]{*4`"Y^Z#?ZY_@}M︦w>dߊw-~|Jx5ao s͇],'7<'Ka=7RҦ Sh+?Ҥ"4# ╧΃:'98vl/h88p φ|/?t|0X햿>5'VS's_yc8ܓȊshSKbHTe"ӳ=7؏xCO]}ONL~g~^WiOsAdK\ɴȎyX/vOQ vu}]C''ciY/M]s~Xլ.,= !*Ŝ |TC!b->փ oH:?}GN_[AHv"$qs '3I}[P?O qFg>GߝZ"F6O~l5pȇQڸtmxJ!G~wIKPi>KC%x Wl UL$*#'g]P^Ur4>0M1uLˆNF9kF? |> h~0n?/ se¹؀qCEiV;6 wz~b=^#A706f?6q mh:dt0k] aˉ7=={~z7G^?]-?Ȅ9|5-ɫ<$3z9hy[=pM/KD]xSITbW'clD4U|/!hW28oIXHRo[ڇ4,ys3Hc7|ܠ91C1댱;1 NNiTjhU=ix.hF3xh'(jy3] '~NL$$Nc.1pn G2W_: > Wbl1VQ /Rh?_1nL<@'㑗y,uCX9X (Kqpgi.3^qІ%[fGWǨs~M24ckuO=;C<Li͇O@u~[qq.΃YHo/ՐǢwN@nvтO[F":ódGOٽw^|^bB,"NX7lhɇqNpXyK#8qln- oT& Sd׉zź`:h2pCzs $WMɎ }'1O#V3MdAG^Hw]t0L<şO@s}O>vuvMnn^z}:eDY^TYijX(Yc1ҳG񁏟 cv5}xZ݃ɀ4_xFo3W=گp2\)]՗㗟GOX&'I2Jf^d E^bb|pV8;pzp˄9?jM@&T\P+̋FݳFФ>X;r8zx$#ǥ>C?i&Qړ|͋;saنg4~ؾ~?eKmbz>I s77 >AgFO}eMhg,r>vγw{a=~vџIQZox'\֗}Rt2Yd-ϢDEbf[wL~ZȌf8x̗' ~{__m=AE#q|Hv戡֠|G?9_>vК 3':~uq.?qy|~W!0G?wA>PN!7 n;ȷݯ|]]̥\eaZ66y'34$6Dk<i< 4aswر]ՀyINT 3d26Ze3fD֙`?~V}jw Zhi^ [ ?_%18sXGv MpgjSEu@]\kyO V۞?>J`zFB8yWwz678=r3ڪgh}Z$#.x7." /{:"\] ǺgQg*9ddQ+@XycCCɞC#P:4cxL* 80wd"_dTҐۺ2_7!pf>ߒFWu'@7Ytid~ p!c tKO*/oi`x&dϸÎ|Ɲe6sz_`m}YOs: X^],YY8it^;<>(?p/a@ W&t\lzP/~/~}*s0<ּQ5a'|xeZWϕO"K b]qKW\7m4- LO~ɯOwi]E"ke]p 0+:.ǔChqdp_zr[7SM7&y|dzCfl?o?g,g ˍ5GZY1dIf듈 =,E.; 6kwPlzarO& dp#p2"~ uO׃W)g5J5( ^h]ַ{k C~˷I wꙀyS>nqrח(R}g-XC(;_䦱co0_oa/Xǻ`! sbA^ˁ9xiiq rs]`/6&<{V ?iV}" |e_#iG~ ?W1KO΍{ tuy^Mx }K'ccDn7k+OGm5v윚TDMkz/8ɢbk{Wr{}d_տ/bz'dZõi^ x<:4>j+s1mr ne'e'e5O>pО7̅S8&(c@rd& 91Qb1,;ydhV|{,gY>nj_[y],yߜ@UפYybCֿ52/;/Y=^/ii wN*jxG`oz]]\?nWo{\O׿K+tQS5SO'L55}y}Ye$9߁Kk/ GAl< Crp#+ \7XZW1%j&Cc~G<7{Qid}Ғbpg0NNw\yx[n)E(zik"bXLfMN^$z˗E7}3byLߺn4Suްy9ic~x]< &UW+!mօ+}ݻ w3NNܯ7-yad|O7U<=ME>" ixYjƋvv5\x)i-u{,:V`0Y׎5u*H, a?]xeU&oh쵬N V9β̂3j8$]q5U<,90;+_kgCly^N2oW3*."eXfwmtxgHF|Ϸu^oMR9FG'$S4  vNjv 8Açp곉i]07v?_x)sos1zxv~ қHw,.mi\1YX9*[fE@);~H.M&tt鳽z9ǹͽ~g߹Ox*,=rtc5_OZB* p,هGZNqdoa(W6M`]5jGݵoX<7 `sPmk0ృkw|2ť3pڏl>O/g>TNbSȞ+PoA]X-kܾ6'E,{;H;~^e<~m}?ʮ27G1/xN>D%XP\4_Էs>3:iܶcV_j3WW%zdeP_EI(+2z*vrp}B:َ~㾴3BG2V.Rsy߁pe=ݎ9w} V$C^?|O_>Lc<8f[rafG$ʹ h;  #ăQvu50xˎ>\Kj8Q=,ȩ5+|ČaJPیORnG,<;82wSxzWp`qJ|iƧ99CK.1b>=2%+_s-W$2~!Y qijr|˗;[ȦmL)y!dG~M9tw.O޽;dydo3ë~<򑖵V~y_D5Z]iΟ>_ۃvDVc+6pxu\ h(Q/W{OrC[sf;r^ykmds\,&}yva?鯖?/c)벗N}Ǝl&6j(pW>%Z{PA'04pD,¥`3 %L[Ι&.XLVzL4_ЧD#_z_B'瑓zSnrc9ȶ~lw Y:" 2Uz,lG0*CB5m"5G#:X6yϿ'&fǟ}O^޼I\#m\e<gD>dIO=#:cGw8|m{NmbƖB,##a|co| '.'d[Gc=#W&q`|jnv}~cM`>v? ?_-뭿niOF9g֮E Ⱥp3Ӭ0n{ew>/: ~/GGzG*dz䯼3z)Y,4Yʳ-ovV< 75d #rl9HPn~F{#ʮɀmUWU~稝ݶ˻}o״>Om7^s0'eDɪL)^;L0uϜFB^>lNJo̯|M|_xQяX@ÿԏ=zo 7qRiS쓏d~> !ȣx7!䌘'|4^/}5ÿRܱ1eHd$L01o5䏲s?,gtr_Gx[η:b.*D EQ% u`0,hLeywy#GR~cwѲW !O"~}0Լ8U"EZy}pcnE΁&`r\Ձ>>Up!!kӨp; 2.G񻽾gضۣo>v~]}SpGΣԓ\vrf`Q3.Cb}{N|Gs|e(Z38٣Əƛcm.hRVBjQG ޸?ZN֋v&s d6Kz8{y;y>I>',FNz?\\ ﱢ[$+|qӞ7s~}·x9,}VRhV`3A?+-K32[^Y9>n,Y2|t' aCЛ[죗o_Go妯5n'ŕlG?/;ޭ3-+¾ƞh>6sYv[sbnr~ʷ޹f%eOP|nE[]-yu˾n"~2Ts#n4vKHqi %D'k9KmG QlEIsy)TGa3K?^[~f"b6zz$HD܉qmw{艫sl`vnŨBk;'?ܦnJ'`'smdx Y.Xu8gXEIhZ <? #<=srgZ7;t[ Qpl!(C֘mf[Yΰg0r]>'g!|0 1c;hCtqvwbܒ0vSHjֺj~M,ѱ cF **Yڌ7p5IQپLq\4nH2 4(fGμ;^o KA?JՎkՖ QcCzwzF_~M_}/7vs}.^%=ItS=f69xc2w>b=K.[ן-_|O{4~r?,~auГ\,N.TK"2>j=FQl?7u>p,4ZBqzOpw6l:r\}n/ǃ|s/ӭ_կC&#y0SDy`›XsEߴrl}uVY:ְW~њ0p&JX1Dԃ11V8m);~}$uӀ4Ռa?1Ce`RHvr5Si =W|pb߽}:.^Geq8=/\gm6'J%Q65'nxO^lUOP#8?g?(a_fxg:rRMr6|ʵ_ܹ _3r/~E}U<<J)+g7rض̝=X=Z`L#۵_-?>8񻶯9d?)y|#@A垃ca:d0)[w"fř!Voq _2۾7ۨnjgc@I=s@ 5+Σ+Co?O'^õM@Қ,,!R߸|u8C.m>uH AhO}+>o#K=:N72C|;ac[/ٯ% zY؞g@o|7աˇTsh䖃B+Jn^b9 y3sO,8i'++Q>;}Azp4_sƟ~x䇉_ǿx 1ٛd>g`.e cvwN>Im$٧SAs7;3;|+tÔ՗=QE.1:"17^ w~B^3Ͻcܱ#EF5ʂL~w ⑘.?Y/xd{`/#<z @k96P=0jg/[|~'ށx_%О'ґC6p!u]y 9? <' 'IcN'|蝏q>e~=ovS~+733[?|γZf}pHR01`S\)=7 W_R?lc6+{^K4sv?sL}g$D|t"`=L'MFy/y?Bש',qL1uT/ˬg״p e6:ʣ+_Y3S(ɸ7ľ۱N˻["<ż\鑻fҢpqpZ?` L.Ocҏn}7Q,ö,Xg?09~ko|~?+ *K_Q"1Ad/xU<"/B7s*|X0/XDng|wF5wo㫾|Tƻ.66>ǯ}R_ӲfvhV̟b9mOS5axU$Y &b!tzuuSyKMvm -߉)̧ CYY@ڙO{7{&nG]}ogo+6q'Ifgay&Y#g#jrT \^2? ,{JfŞ σ|D<]?~q`V}}x_Watb?|^9YNDJ. 4Hy`Ȝ5BPnFzӮzz{C^~cqkj./.q#W<>鶴l)L(m;dX|H9Qŝy[;"%ǹ=4W뉀s3:'.$~Ẓi'BO;\'?W 'T9Z{iNW kk!_z|b'DŽsك^=QṾ=Վ}z&%K3[L9w!yR~?/]y'2/{IǫI5ߣ”8?wgX4f>i25?vP[2N~0' O{}8_^xnxzALoxOܽ>.<^MYJK֋YBF<kMZ>̓g/J`⎿eCnouߛu=M߆q0^Y^/Eޅl5ogk ט]$y߉9Vꕮ ԁCW\w-?՚V^ hdu@f8ɣiG =]+<]W._EG=MZFwTXJ2?>݆jnfBqHGzȄF]H-t \l"cLOfzG74x1<Ȇnᯆzo2Xix7_1;#%;^򮰚8pɏK"l5%|[/<>FeR ɦ$0XA^Aj)LjP[Ehc|}?s|oV} /?RKЛmmþRLd `_뙀K_N:/ehG&Ƚ"GN:p>O$-9Qk+.HzDg6src,f`9e->Ƿ=p{sqB.ݿ+sgt :/y//S_uyfzo zzo>#^}yNvD5~@(}߰?fORU }w,QCYgcdo{1?F\aɞgF^`i';il9շ_t$3-Mv ɨl45v''_LLk͉?b{s?Yқbˡo4k?gLyUOB[+3~ ;jXqS* :U&%s:Lv36+? 逷cx?F%j߈ti'߸|XYZM׮WZ9Q7&azow'^{&ݘ=07xyШWGg4ZKzs:/KPygE{ƛi1|=!02~eAbvbve`?齗i)d0L=sg_a/2ۍ><' =v`ec߂dY#ny'&;':K*4yFH{z1TuY>FmY=/NfLco|8 >\pϊDr{t_o~z>#,Ƨ哾x׌w}:|W7g/OJ?|;B4ץ_'<W^xoGrԲh:s_p]|gpA2iƝ3f9ɯѠ\.@mqUߨsk};?A92>=ssmr=ouӓUOY奾 % 3zp{\hƇo5 }IB8${:E5 ۵qcO"M>\{v{Q#Vo6'qw%L@ '-|&0dq[Gmc{('`:}gZ;feO>"MMG(5'Ig~&ֿOLG=L=xz۟ hntH"_y˻'EO\=Mw}zחt3푒|:y[,߫g`'PgUcp ❳r5$Ur<</}FznHhn/cD{)l0djMz0@x=A&P:?jL9!'9:^=zGξ=ro͜4f7-[/^{q&Cf6kn^Ζ5}M$לwg^ Ͳ=Yدqc'BN Awe}̱~UjGѫ>w^I䧁k<.ќGpl 9m#Υ47kFGԬk(-)M= ޻/U5v?3byM 6w=e`ʳ䂟 oN+9hFꙇ$.ӏ5cxNb%D[rF-=+>h^p'9?ZeA-|[VOOUIo`8f|/-^jaϛhȧ %%oկow̷_o˕LöfG|򉿈_ ^ ܔ|D0?%_O ^ni79v _6o/6|>ݧ -s0w OzYEE)_v5pll5p4:sf<Ax&r:ﻏ 蝼ę9c/)Zzx 3+ӓ'&Ph{J85 N'm-vf#5?4=-^Pdӎ폭/zw~y?Ô3N-/' ﹏bqI, :Jd: '춼v`cu~rÆ.{ϼaàxI(mek;oΖ*=:4Cs-Գ=?YN|P5$}:y,r6gmRgj[N| hM'kWC{.gVMώ||U|oo%^yo߱7o [M5~1קxX64/X"~@CDx4Z|Üc xzv$pd5g:y@MR|$ ĕW(\LQv~967W̎@):(9CC\:L1(6ۆi[LoTJU$h#LtcMDAP&PWn}4֠>)Ku#PJTŒ>Ny mZa>jx glo, wr'$T 3ˀ`so6a?5֜m>Eh:oi^>&>/?9ӆenrwmRhxG_8!7O<Л7B^zf׏7޼ 'k6e|3,gk_w俅/ZM d樲8?cPvJbhk5<'`-->g=.*oq_YIZ}Lʧ|Ged{Ơ g9@~X+m_"2uNQyrR ٰFg6m?yl_q39ފ/)mÏ>ky15E R|g4|l0 GCdЃ1(X^:A-] rp=|DE,{g '1 -$>l< Љ\[w Go|i[|8mma/;!ׯ-?5Xgo y2EXu~N9߭wQmF[qDD7}GR WZVnW?" G*(# B')iւӛ;W9SlzqFmeG(|l#ys\0_pfm.dxd=:lmԁ^K$}5_3{;E/7A~P?j+>_StuK$mGk.s+Q t'8`&@3H=/i zZ|}9[~pg?q96}a| 7]է~e Gkf| &itGTc<,j p"6qh9pkA8^~Hq$'!NtlvѱPlMzͼ b7?9\م[y`n5Ϝృz]D`'t}V^H)) 0_ACwajhXg7Iԗ8I;|Kͱ&aZl#HZv4 ' _H xR/;wsEbBf1z˳㑑?QL&}xČv(WkMz`" Ruae EeO}(=5 1~9W!'_| s.#?v0(aPz, u T#C:7lLkP&>P¹zEHW͹)''wɝvFeqz ttlW+_fbq/4n^K1L- Xx378Y1/ݖ_h;+)dx~Cx+]牕RS iKϽ)ޟˇK/c$՘@2VgCx§O|qxB}걦.1:(WwhKOg⇹ޔOs>7Ŷxoj"j?i|Cd"=`fded ?Pe2#3Ւ];(`O6ڲ[@$EM\=i#C-s[~}!'7Ի\GNԵχ|cґw N6?Nǟw‰ hrqanu&9_i3׃ ?5lO^|@p)nk/Ͽf^+o?+o}1wB*Yh,[ &e/1D 8?|\S >)on騕\pdѣw$ͬ/ rR[Em>loF#`y`Œ-;k;zGKiV|A.% 3'gcp]<=4Sw be|3o׍t$0v߻sҧuiGl'Admc v%]_>민y ީuߋ5>2wpP0Uh$1O/>xXv^cr a8(&~OQE?a !^j`mRoUh3o |w9xN!}nd$y&rz$;ޅX=(Ý =z3᦭PG=ɂ&PV~O|& @V^: wv~_቟錭o'O< e^q~+'_ʍhp2`֕/{,uq3ANzE OT𣭃||w~:[33s󁝼G:պ(|wſ7N3_܋LCfϴ.OYX>l4D9 19+c1 vsp}V|@;,FG$?8jN>#&o;MVjs` *^k!5A36ippJ߀㓃C5ð"});X`s\Go áiu""ȁpj؃NN ܪ3 .>R86Dss=ei Ş_)L/3T l=12%q} n_C{8Lf5r‹r }n! 3ܶX>Ȼjul ~ʷ|xGxs~^? ȅ5Y&.]IP5߉' ?'̉qޙo]6 ٷ37]^ >v0POvb 5.O!Vc}d3G'Z7cڌ)yo78B$:B>!G= _g@Ÿ|[3|jK/K _xv`GG~8}}ږ,"b?] u3E%q_x9{Y_?'^6xzV2yzF|7zk??:>_GJKw90Xhǘ=r,…j˺ˀE~,<@-gW|]p7ڑMG35~+)=_Ƌ*a8-(w%XAwdEع1"p1|ɵjCs,΃#LYdz$l%I%FlI2yhM׀W!8s)oF NxrFS,k$'\?VGM xտ4Krkr,yK'bR||_|+O+4%+5+Y!` W;yY2z i&V׊~-7}Łu TWM XqGm+yb_[qZþ;u 6̖ O5W +~R"s2֋?$z品Nݟ14tk'!+ߴpFWq81*ϋweYğk bb>==9@\ϚS{mgu+od1<]yB v/],Ȅ&[x6%%>Gxڷ9?T -Gd&FcWMSW½LJh> 3z4r|mM fY9^ aU?:q^n t}X?,㝉wc8;v߆ǵcm}\W_{,Z.lYT,Y`hCN]Fl-G >׿q$%xPYɊgH|mVh""v1u}:7^srmG[k!.D4.t[]ֱ<7&MALˋ?,nG: 1:o4" \86lc Jw9:w;!9Q9^ңOH{>tx%9; }igʵoh_}ZjsPDžl9gU 5:"yᝓ 8X!MC[9Yi7Oqj^$ #IҲy1$J_|[[ &so)G/'6_H@=g m8Q1!(mG,7M+j積CtP^cvҎ~Zu ԇm`l f £vڪ7ώr^Wƃi׭n<,.}& )8 'kCSGSWƉDC7n<](jWѴƪ/1J7OY=EPdi(gZ_:6U{WhlNywϜ-h{nxSI(эR/tO\oX7N[:ӟq_s=iW_}e~ 1H.;+_<˘Q XvSOȜ#Y Q=mJRM1ϨIO'0\ E7 GF sU GĻj3!4na6{# m:VXQg&9yB{ll=D ܏GZ]Qg]e;I}@S'hZMA\5O8EF>sP`r_IO<7{u;PC}<&5<;vn2o.+tG ԇr.[7(c 4>!h,;t*o&@$czdcnq֋4g V~ivsC/n׋UNc|gFX[1tL}P|v?=H~OЇbҏ'2FMj22Y]у>8&1j5p Xڇw0I 7}d<':W>Xe%';0!<}ٯW'dG yM<<ǘi:LL娗݈ ~\W!a.D?0%6tL3??]dNJzK=pBD3~(\Yy:ne0Z;-lD78z+X ̶Ͼ7J4ֿ tW!nuWq~)\^FrZyjLxyXi^V@ي/gC8XA?<zyPpmgV_>$o|_~ ګ˫m p܋p^DYϢZE#,Qo`yqe7Zw+ 3d$^>x2 " a3zcuVKOb.E2FN|}`;WV̟ }dWPI+~S37k"Mlk`37P“ :##c jfu Q=(h=|_[$1n[}" P^Ca񑓐n^QNc~m۲ԩrr 0AQx(!EJA<%B $ ! bc ۀ]*u;u{}_c>N2k{kk_k>.kιQ{ 5Hk ز:ƝT|i<;dz lLom{#u-)%諧w 9vªo~ӰC=B:߳4ui^ӯ 7w'֖o뛦'L&H_}uG>XY.]dS'ʲht1zɽ48Lb@9&''۲;i>+w$Ku%hr7y܋ =ĎէOsxd#>;DT<;aĐ]QH<'&Աd0nQ\Pd#IeYAn15{Zx:\f>B,Փy2f0|7gp2OpG(6:y4TUZx_ r:H] be`u Kڂ_>/8ų+;!Pe`d.kT>_>繹7/z=㷒_0gJ1iCwnU_3q~iZ޳v̓;^9Y_i#MA{BCCP>$ ϱIˁ^,ﺴ<q4A,r,AzNp4[=6uoeW;$N)m<`>t>9|Q oނ]ew.bH_MayOD1ql7!ߋ6:x~b/#&!" FggKRƣϒZ˸boSnr8pt10eCYm|Z_`6?39/cM"8ʶ)5L>=/j ЦsR;(icw#u;hp9wBscw0?3 !@/}^W#Ed_YhF_7l@+ ħ;b;+,砵/τNjjccݸ x^x/`'p2^#&@!qƯxovl$zRq?d!] 7`5M$;<59_?ga9b^UC_,E!>9)6>DS8<)oyOʛ(G[mxL[q N}իweˊ]˝?on]nhnoL0k)eӛ~x~_|y qڸg= 3~h ^~׽X|@ QVW/$(Îj4q#YhBBрe 6p(W>術܃V:b@mNLNg$=<"1OZLީM?% _u:q$ǜy^%Upg41ǠmINU G9rJ3Y{+;yS>f;{>Tybf D|͙v'簊;o66ܭ4:cːp7ŚO^?%}=CuJ)2&SKHh0a8fd<6B3A)=7O[xﲗOl~q pgZm)?#Ɠ7o|K5j%Qoq^#oZԲ>Su։EҦ5iÞ嚵TLܸHNOKE]&Ek_jm`P|],l}N "osaF3? mktN 2ޚ+>3#iU.5qOv`؅K4xRߑ&lK 2Fफ,Z= 'eh Q,% o S\eF]zڒ#63`7@IDAToq9JAxa_tc{\P@0Q/?/A51W>(0&K|2྇ ǘV?~`iMvM,1$vrOsE3QW{Z {Ϝ,ozYM7iY<,b ֭W,"hmD71l6F ]Fˆ{@\oմQ-;ly'EWgniy\+r&= ^KA)[.n5E8׷8Űh}!~1YNjCj38[ ;0i._Gj٢j/z_\bގw< >"kI–㞯}2/[ݎza=ۋ_=+Y b,;v<ɔ(嚈;X=a`W嗾t>39+,Rm/>5.lط)x¿ ND|&E3 Ww_+*_WO=x@b-q< +sl">'qwQR.6*_I*tX$\bCJJnBc( QhX {n}Oc_8\ND!pzđ[?sbRorkb+b^{4.GmELW'OVoISOEI㋱s&6%oYC3ֳ9Ùm~zc'2C5C^}y_5rm)D b8'ƶ^qqK?s&LX5:jS͏)>a>m7)9B5G$ޮU=F(ũyAX/1s-6 Zs>ҏG աKHw]/WYSگ |p_~|/:\*[s'~f  _O]p^^,aV =>:9!oMW%8{J.3T́kq`0"}N@>ol[".x3EjU#WgksujX"dNt$LtAI.m?B8-% {rPf@|6 $7:4;ɇg\SŶO0]WWU~ϝ]|B|_x=ЧZ-8yѣ!6M/J#ެǎr c.'}ҶqLx<wFlLe}ŏorMoC9&xO@s>l9)Y8S$3qR`]d5ICvO=M|*;5Lm&n۹H.7DDJOϤȸb'D+al{-ӓHmN15 V5hěxxTg_VIJ >`>⭋c&{^$LRQ78IcC3%|t`GCAE\qcq)Ls)R+?Kύ!MKЉqTīxQ!TN,E/Xm ~66n9c6*u^ЅH0Ad|Co x/LMqj|ɻA[JLuGYm> ??+TI#ُ>z"a#GؙP[ُh ^_t}=Iؘn-W7T&gڋ#=>9l[[˷|~&<'7޼eXD[?ـKE;K7.J9Xk)!c@~9b!D@xEWx42 %Ǻ &ll|kņ*=֯G.jH߇/ݯbl{@5d85F{ q6{]e>/yݹI%=c!8W=DofӧqxxN1,0M~s9mL92ɬGNm<'Fa3|8?|  *?j^7fMjOJrU9c%_98>ْ+/S% јyXX))aoUYޙiP>zЀ>)?-ql3}/>]e~=<]oy+{s+ -܌W=Ͽr{/xǝVK7+Rb[T^ginXSp_|tO- dP})v?chhS;bJ3 YAM7 w虀W>-vbov{֨dhm"s_V/Ǝ#.r[\ɪShc|( ncp+\,|?A WтCϺERX~(%Sx 5ҏvbODp/"N h;}$pAqA;Jy%t3V y3](aĀǛbH5?nlNx(=2sy䂤4EB Ŏ_wMo-~`gyģC 'ip%mkUx <n3~6SUr{'5$4Zs!_g|od'N}EQ8@: Ϥǯ՛E>]J>Y^{ʋ[c_N0&٬xYЮD$mz:-„lO%uB(ԟe <_ 7y3Z fWģ|F n!NkWY};[n ~X+˹r\?+rk>'_LjPF<(ϣƛw54h%^za,ov>׉WI*?ah$9>4`o" ^>%~D^%EtG/Jdg^m#'?s~37'#~Js;~0}]پ> ћgJۛi<ͪ=ke윐r"Ib_'(b$+g@Vvd8y~&@βcٕ؃'e+y AݩOzR^a;XQ' f;v G g聴Y؎!wm? 2~':_x9[O?{m_\2`0qzm}1;͏3.?Bo=< +T;iS/$xHxTm^ <|ө>mb`W#Vo 6C801>%rۡ'~ @Ufx1ҫeE̖s-&Xrp w0d4v' =T..K0YR &+p!3Ka^HZx)?m !C&\H勓GCa\ W34 Æ><*OƠ+,NmPX3N]2@Gʀw2?y1Ɩ>c=8+DFˇxi8dMFfn[V=/bajX9 °yOF, Ӱw+ e>{U_)zК&PSZi7|Pѱ'|1e(?|ZKK~}g`ty5$kJtՏ3Ǎk4yX=vb |=yqq6 )`h Լƛ*=z8\7|P7+9A{;6= _@vI[zGsZ !G*eLXMpeyf] >:*^_Kɇ,bqK +~uav(#672ŷ|?oSo~߼z[GdC9Qݑ"SxgZ$6Πc?hS/~wg21_B 8m~6cBxZ0ÊaבZ", –ͿOc{_+Nj-XO@|y'>M|x+ο|~/LщՋLj}6Yz:J~<Ss#=dz>y[j_B:^ uMsu\cn3ęz]!]rM0fk8šrZWO?|ѣlPc6N'S~юarLp+QoZXe,pσ7!' Eyᣳp9öp*= kw"_|xj pQzݼ \{w;Wg@ngiӋd1{׽g/3'3{=~kU3Z)fcz c,cCivrrL4 rN{?7#}|&rgRDoKHm@R$ބbؗ-l+8 D?byٝEősA'OoL&K>3xkק9WU~w*_7~xzbA^^CJ)K &īx@ϚH==VmuǷYˎnkO's#yy,{zG69XwqQnۂ 4#^w׉OYJI(8rH2:ɔIl?'ʡ眆&5v2k(b?˾ />b_L\9tpeD#8ƭ1p ۩yjlWg$F!!˖LDo'ߔ$(ɰ$/?'@_%\^<\: C | 7a! #jt _-> rh>>{q1zU@ZΝѯd?fI_ qy(U, M[Gqy$p끐8^Z%f_Y_JX&z \xEbCCl@ =n].PjY#b*@ иpcȿ+9xcub~w笷'Dk*97Ăۚt[:V~''qxlW.{p;5~1wWܗo^UjWYσ?Z{ "L϶qN9yY.֓#ZYBb͢% Fk}pا 'M~Ƿ?=w偿o>\𙇜ȣsT n$~/j+j׷rK W  'CmKJm r֡MW! y9;/ɘ^ęa]\n|ڸ5X$5੯4Ԑ=?zuF+U>#Ż4y2B|6ޝ=zc?T?աEX2> mu08 Xzj30)ȉqtx:x^/{Nj[޼}޸ޔ_{1QCs\l u A3@Z}&$$28 㔎yRӰe]J5rZҚjO;ʵKa9@׃D>򷌏jhv{R gS3qĭst5(\ \8i><5_)1 oMytwluG2z]K1a}<0wҳh_o?5b*&6͍ϱugjo&9v3N -! =GNׯ=wf񸳸]_hS=x8 uEutqC6?do '9qӟy|A\ۇg/ܾ+򑡮LNՋ,G(9R{>˔[8ȸw¯Bwc@:WbNH QU^]ԛt Ycۛ;8tpm[g;6+ʵ+̶oL:JаP}^'+>2L&1 5g [| ix]vmS7(XZ]e#.7|uHWNc}+%=]6tjL|1j? ]N'7'vQ\\iro cz.O|'^ڿU|0ƿ7xN,woԻ\9x='Yljj O +So"XiDiؐu?)5|Nwx{ 3 ' 8%K)38zxY^.uA1<[}X'a۟ފ;s:sǼyƢ X9Z!c*U8S,Z]g;<$o?LW9Nwo?#K*#B~.8(_(=xG`{[E1tu'X{fph ' MB;Nԥӄ(zh|d9dEX:oHv0VV~= N1ŀm$Ԁގ:=4g{~<;9&nEd>]%xx.dcGQJ##XmQ87 Pτ:7z`8w8æ->9F#ޚS׼=.o=-~^]p ~3|fΟz~z_t/<C^HLmϳ`f?g5Yy>_jdw)`REE 9~Uzp=Oaˏ}m] ?w8h{?=E"OOb\{* 8I٥aٝG@?1؍l\+'Q@]jpèzs2B7pt|,`NUg+dЮ[m@ccCf>v8:xk1Nvpw[_V͎銟|wc6]cw꓿Uf?ʣpĜvXM_զBim|tqLb|Yg;eŷ;/'h~ͳί7KZ"gXCzúSiCq0ŲWgw]c!D|cC 9D`?%t&~W{L7pps[ȉ%i +>wy& HԌ} aĆ!rzPG2{qj!|S&ft;.KpՃ!!$vkgJxz,|wLΏ PKxp[,l1] /yԟXxޭ'5b^`l})ˁʕr[! "<{,hp,<9;=zPGvy9ц0x_}ؐȒVVC793'-F!\4;5Om jR;p|w>; C'ᙖ_{4kO]X(9HL-Em;X]}5@VW 8u͗'>ρ Ag*|ޗCt_?n52ÊE&wur=G/~`N:28dCy »h@8ao؍8#/)ᅃ8(mCq̎#y5|Єh4[J^X/lVβ_N>+\"`- 2-<{^o&ߵ>C(8eo},U?5d?uG?L'0}~KE Lx+*bßW3^[ ԓ-=])GE4}/}ڠ7Ϥ/嗽^,k[k܃`Z;GPv b; 3EjY&e04@>&VRw~轉7G>X7TOx8-O7vpL˫he0xnL}b`RxYPP^ ԼOL^-L}Qj<:aN )$7x{S2g |զc)muB{]='?)/< ?Sm'%YoncSy3V=?V~ zvo6# p# `a#񣥞WSVoADuFBG瑀*2@$woɟg^\<8Cl;/}NGe^zYW5ϻW=걿NeU}5~jpމhmGE|9\peSWCMl>XjGw~>7nX 6d +0qX5H@-jF-ሏ׌l=B_Pby`=-2b+5nۘ {?HM>{1a 8֧ae=37OrOlϜeOo>8{d>:;5Y>,BaQR8F6d +z{ۓI|՗tZ x|Pj817#mEX]r^G]ӗ@bj=vķ|Oޓc9OQH%Yspҁ򚃡>ul'a=X䈬X.~ZOL:\$#/!1ԾԷBIΒt!0~B1ۜ}ϰ,^AC?pUލ[Rl덼ou%xwvZ4y,m=?[+? v<Ge{KL\O_tqWy2گno鯏Y^{?h|N0XA,l:qrqbqpˑ _s K6`q>Ɵէ~×;gW/'/&kIX'E1?a4d%?RKʃBv/vmp) ф~v۲bFОꃐyQtsv@LMD=2wFO{=wGfFZp9f SM&p!N-^̶Y/r'%Yt[p<>j>4є068u_:^B=[Hc__{W(y$7TMƍye&cPJBjVh ^C?\^25 n 3ȉ(>ψH8JlV[8ԙaj7d(COȭ̱ƑsLju[x^#ێ`˸un=c19JSk,Iʩ wWS#sb\ |uh)GtDsv?oK+ctƙ ]o q O?u92G<$gmgU?vU.~}~տ[Wi_O>o7ba']Ň'Z,9.t, q$}k漭}?Q=}<y7{Td F6&[ ،1ÓP~<~Qo ,7}cf.uw׾ɬé.nwNjG]N{yf@_9t$GQ׋Ns֗|Ù~ESyǩpZ'AkK;gOyL]n~KnjixJ[>?? ySE\ɡdag3{$ .5ßjހ{6w~|Uܛx9Vplet>_u+^spC/ߋxʭگsx_~|/y 6t!vx4|W?'^y[|v~9oxӷ5Ɠ/PmqXlYFjpt3ȩ`:W{$ 8}ǿ׾Fo|NU[8iYam >R2pGbv7&)RVzl| i_x^x %!8.}g<ө{x T#EFVdck>- 526 ֍6I<5N/oMkoG ̈́ӯЍye 7m9yb *9 $pCJ^Qk`];Cs5_hE17vMq_n߹=Or31Lƴ^Kse{ OޯGAۧ{]9<2iwA~[ vkM:~;^{W^yY,?7id}ڭ=DQtjz! \mc]<҃>0OO>gx{_>tWީ;?zяhx,359@tǯq,`cM4XLxɗ{޾ZIq{^|cW~ `ֽMBQp' nTEX iQ͠ԖN(֮nx d}kv|~?VI` ID朚[aX&@qf 7/|mM"p8u w;:3G5M)歿#?xڦ@/'̐x]vz@LD_SNn^11џElL7u]2Xnn =:+Õ1Ge`n^G,lf\^s87 p`o2A)bCϿ;⳱>޿/ܾ+) c)0v{O{~U_yڿ[7Nᗝ-YfRVNTNn.]Z|9c^'N۹-[xܑY%~mI9Lqp4?ǷhLݎLx@J_II p N-;!z Y#=pM^%eIk-Gr.2փC0>"li"}ϢXL,ON o}-̟l`O]="p|8#3 T`$aFqu"{~^<qY}hU~S|;\xx E'+8҆w-Ag?6"%$^G͕`#~P'[=4 c%y no{瞯m_}exf| bnS̃RLc)0׿uZ ?ad?k|wKޓ<13io|Ti4K_^DYtk  60q[ Mv_ I@;v_ФĩnD< C=ߡռtT6sR[>{;?o袄 1/Y(9cz:ȯq, N lɍP;2A*FRB)Q αs0l|&8eM`G~ G,dGC=>.w`Cĭ¬+;Ic&RX7cN>nt q 3;oXo_(3I_ok@"o۾9c?@`2 1oUWIAƐ13qDXGJ|+o!gFa!6֑ @xA7|Rէ>4kg{3>x uP!6z饳ݙ N- r17 |\/OdcmGW{1#yg@{)lS>neʏmg<` 3 čǰŚP.k̷?'Ko[o o.=VˏMfÒp>LŞćlohHIë|ݭ}vw\ypeMo|Ϭs-2&*4ғ 86ݛH<{1_,҅@,fj, `2ݟ;h|<ŨZmzH>+|zarH=_ JSʛ8d< $?k\~cji/\CU|MQ4>m^;a#̇z,/}Á:uq|]!a?9}˗?|^';@w{e8ޏ{|䀽|KNz+=v∮xz5ӱ?mC 5]UIOg̻7wk&?P|psu!0ϲ/}#lQ5+>kGe|Htx1 an3o |᰿yؔo'L&|ͼ-{fL^TZE% zۤ~|10@8Y2_9F ?'YOI~_O?P7q>'ے*2 W1 k|n@@@fqMhԭ\J6|^'-1lI-KOCej:] *H<ʲ/\()9|I;?XpI\2usg;|gؽ/F"VwZ~|@u]rO-CFAF6}FͶ*Ƿa 򟮇{CD'$٪o/cWȓ7*=Z_n69Tץ|vw8w^WU-k^,6YA:bzq4[b6jt z~y&8Ҭq\*wS|A zǕaj5c8&s5IH$UCȼJl8'7]cp*7 nNc;[fl9WN pۂi#K;Q)6?o v ĵ?0Pz>vek KM31IV0GG(=X7v3ǜ`¯-r->.Hgعx>6 hl$ub.1*EbC^=Famw7Ժ Nm- q8 ~o ~gॳ?yesEPmz "J:yYXҺ۫*ȻGWUо[_{j}_K> ×B|{_ʹ={^PөH%cO}>Ha/b|vO :uGꍀ?)a{,ƕwO+޾y%rcbȠU7o^=筑/?0H9~fa |őil|xѴI|pz8cU-2%? EY3}H21/EC34U[e i gZEM0TsD/w@'(&z9/~mk3_kx8.b|Kz%huU+1[*u_vb1+EQWNG"C!E/,?9r }tqt u}z/UW +?{߯([lM_h ;w@y.yӊ7q-~ޑҀG썧^|nG< 5h`S/?r5ND[͝x!Qmr~{#DK=r}o|u>Oe\՛~zp>$UKvJ~C lw9_+#E0zwM}lO=3~ y>? Iz1M=}bHz*L~vC&ԸӖ:i:Ֆ=/ 3-qvϳK 3)5vB7Ճ;C|?Ɏ;W 1EN{chJAZR$"=9$'V Qc֜F2.c;O.mW.rxݎsp|a'?^2W/nv?;c=f\{m9|k z6;@TC|&<^_ſ[k>W%wp?MB|xoBwh qB5e d<d|G؆>m70E}/X1܃>VF9*Ao9Q 8n/pp]1wyȯё_+ gyeq*|vWa|,ûJ#@ lR p`w@sw@X$o/0a0rX1&-c>g blšѧ.ޫ-a?ѕxas>p@~v)ßÿwbb)Îa?;d]ۑжm˜v걛]/Wp_ww=p!7q'(u4Wj5I|;yJ '^VЄw?q=4;k|\/DwANͫ7'~_PS O|F\ ^P'ePާU˱7q&uy SzR?816yxfp^F$cHqR9?oҳU/EB$ '=lPò_eK<5mhnr8%CpXyuG< I ?N}-'5-~nE*#]clXX?A ?}R&P^|pu]v_xMIc~?~>?ضwo}ڿxÿ7:_5 FNy241 ü@e.=]Y# r]G>~ ([л[دz?}_8̣ p3v>6 ZI<|f^FO4p9)#%^ia;j7MdS3ʖG]~':Ď풯S5oޫ^{s2!oRm0у)>l`HO4AZwRߑߓr@ޮ-1Oy'כvף_x]9w%M"m[GϫGMGx~WǚMӂ%LZvlYv8q]8FGq&I,&oMO;o=J>|{o| _O޴8@=fI;ZPccこV%aAz&lKZm.侹$1y,'yHk#~rIAJ=w[Z4PK1q49sP)!b:Wy\_!nJ[ljDzn[D0y4C ֻϤχu ~3lG(/r-5_3;dF˙O$ib8.k&q?g߲&rSnݗuHA=|~{оD%w&{>>T^t7㼖Zfii!A6kqxud ౬iPj0HO}C|F7fǕbv%4G2ί}2T=9N={_ 7ugcT&O13c _\ 6i x-?\jطťu]O=O ɷB~I)qʆYw r)8hҳ}ߟ`ü1&{rZ#touQx\;;(2HƓg ?*boƑWۗq2q1>ѤzIsmAZoRG XOrk gMuFY bQ`d0`b=NR;nIehS=%i.w]1`ь[@i|oXՀzְ hf >hw8?<"HN=}B] `!ZXrC7JFjC; Y/Z5GUڧԴ|WxE0/pAc#1#HLrMU7q0< l/8*eia7szO/e^5@SQ2&K/_6Şx[9mQHh<`Z,M޼s?Zi({jZhm D:,h\c|=?W6pT=nAGqH(D8omk8S4ki}~ʘpKK]a垂u`g~i|hO`l%&sgncOS@˖{x|XXnM GؘOBϰHW|Hi=sqg98A{!kwܨh . F85Uk# 䶣%dN ^Ѷ-#_~WMc\girG9g>mIp_! ~"jxF=jt+ 0>Jɹph8 >3`nsO4#Z0,Y.?T>٢9A\ٓeU|YArnf[DFsH54X!aA+Hb0X@q"%pZ@rlc<+ϼhm}үqgw z̃=h]xP[_%Y_z;g_qXǃ#NG>Wf>8TWMsu@#|kS 9 o% zJ{tLh;۾?|Y/aHVC &/ ?Y fӤO6 O=M/D.l7' &"M &<_ Ϛe3ȃu>G<)fEU/95 ~_,R t Gb RU k 'i 8_*Fx)]RtOtGB#mͦ_cɡm?bQq9Hms3Q;TGoCoN27^mІ/̰`1c&~ˇommNķY'@6= f?DB| I1H`2찥bӣ;!yY@ˮ7d)>Ƅn]Z$ha oG+|C!w]xI(9͠O'L.b-7F478J^>ISEu g+P|J"n=?Ď4υ$}ȧCbz"{%{6 ۊO춰>e.s}(>O/)x/_|ڴ|6f;sv܊ <])֕>8;3Ł"(?PN\**&À򰎖({4@<_QVm5+7lWi͎i= 6̇ 0m0n} -%6&Lj ,> NeIgyiNI>%eGYGS?~w6ӎ|Y};Si$|fcCkoRAohT M~zfƔH+X .RRhJ¨Rr;0.+xL!Q hy(k:k|@pj!hw@U*;O(=70#1%4_Gh϶Hv5m+:_F_}ӓm`'Mw O:㱦 Y&|鱿T?!tKaxڞolQ^뛿#ڪ||m_mQ ɠXIF^r* mᎅ*vgFeIw]!c<|dBHGO/± S\z{O6GG^ٱ퍀ae[>D\%jfh,7c?ϗcqkѶԝ`h/J@)/ikS{yN|oov.G?;;=)'hg`]~;8݈5 ;i@[X|d6mD5#;M ^j3];`-Eo hc9 jc9@Nm/zh ĞЏ SN$s m3AI 0"eM>Eqɺ`Ȍ؊1~[r>J u$R4툼"~9+{d?G]eԗspC0: 攊=ӯhHhrS1:E(AZjMt8d=U"뫉Ep?~Ӿ^o ۧ@nءc\)^bZG$[a|qH!Y\hE,ɆG쓛p~4<Kd۾G.HKܒ.Rۨ^Ro)_K^:iןalRDx`px,Nc14eMohV')i4# eB5j\q+67>ӏ>*iXL99/< hfP oh9LJ0:2וձv>h@ *RuFxpZEJ?;"頁:ot_>xbUe|#|Q?ߜWfBrS23Flg]p{]_W!'ym"mѫ/-Sx-`ڱ[mMC|~%z؆%٨yi~Կԝ@uTq/1Hb)i/DgAeS8Y.&,AyVe,;m"}R,eWWYntp%FzK^4Qdуʬ>f|#:knx "ߪƳ&^&m@p:1^xA\Cա AK/ސ</bD.Usw;4)'Bl؟کcq ȥ~y6Sw&DW:2Q<&0ӝa,S! - z|ŧFTLJgxbqQdsFBjP7· !b,տb7z2̼Ѳfä1hk1a}C/y,OkKIR3BC1v2+Q,}Yh5G2z4@#iǜcU{ץd? O05f@;>"̗'vQw/?AU\ok87_GF[$Ij5^ Iš) /ZXR3ky0舓l˱M{7$Vi?olqUȳ**SHuƜI/fRh(:XHGZ NB;U;: WeS ;}&w'7y,+#Y˚֕ [:ر>e :~kVG;q^*>OWAZ^~hjB?]Q$GqeYQx%Vh2JdQwe匊O]3^Xg>Ƕo rP\whza;[D֡-[%uzH;-jУמ a>_|G<HڑӶRXOU¯' WV薝!ȑel/ǢUJw| &kݠ /0= 60ɑhv~hg2hg1:VM}UX#{CwM? ƈ3}{ȪN5> =|N ؖ!TeG }A%ڐ#SԤ1r, >q?mI`ը7v1'+A.3eR۔7r,w!=r&vw6@#fnX0"JXF}/>'1^-HHPu׶vt $m6K@OQ+Ǡ,g=K)fȃhF57>Wm:&3d9'sN-M` > {@( =c [V"2 YU<{ fcYʗe{)?Uߞ;ďi52;qA`;LNuF1=M1zŝ4o pY̺QȚr`a6 juZ-?ŋL\k6֧N,5Ֆ^QvdKUW(7Sq:M4 H(@L,>C*Tc-Y"֧LqlSz{ŋmU=޲UyG b#'#5cy,Pzخ>eXGN^ڶS ~cc5'D*W) T\CE]YLGdϺaXXD^d/<[^?<"o0#X%L!G& gx`p9j9#'Z3hkK$3Vh5)9b9õKM˃g;|{p7id"ﭿoGl4aBid^{'2LeGX qNri@2?1ySg9K#C:ꔤ9if듆hRMԒa3n _iȀi&4Kj,اCĭG=;J3NYQJm4=O.h}Thy_rP%upc=ʃ6}o|vae(>Ҍ ū ta&uӏ]B9_K#ehz-\5dy-j'-ocsZ 'hvp2 >zd c zpNN`d otblhafC w{ք6&5}%!Oq 뱱EA.l&thKN7_GLOќraߘ(T*ßd <[.\[oB~*O$p8@zN\_֣5趃HFiB7yhhv}Tk1=Qo\>=k$c(쯬>Vj8>g.󪕏![eclj9* E 1|ۮk˞=}/?ei $`=}>i`-U߲yanǁnť&>fO44f$@]˝}㷴勍+})$*b7OsgVAQJvn :pi>p11QiPѨiѨ4O9IAg 3e˨YsP bo)me=_ؽ}f0xVFRmX@bM(eO(u~ZOG/AEdSs!\x6k[|ƊuP pOՏ?yGvUqRP,1&)/±#ԱjIS *ʧjuqڼCۗiJ~B:]+qiG ۦ92wu2g)߻ fѧ'6g2W@sy_ F)P-F?bH:>cFTi@(EȪz9H$ כO Y\a|{;882dxAVQO6D8Zd6Dۭߨ|?,ȵ.<?|rgQx(7 SNOwdi?9(d-Q*9gٯˏLO&K\8AAa)Q _ @dy+ P@Wԛ"W0]~ƹ;SqFZD?s04ṕ~V<>_ysgNO];kC|orci ApĒpG̡\V<1Xo GySZa~/Y.9h3Od~>G,ƫ1#o= O|طMe^_7O9,;+?M4\;:=3o)|Գ20MIrx1/!lW_IƝ Pdtn#첈eӱ%Ǩmx ע$*vJ9 z,33y&O ?<\`Vw#eu[o0A0Ǭˎ0*IN15+pt4Ӿv@_4ʛ󍵩,S@'lhƂ?S dE\H ҧ4*v}t:rsۅE2?$>Un7MW!x~rEo?ۑtd&PgB?DaR_ĐـAr'\h*aH]+km(K=G93 6nMۦ00A>Å],X}W K@@Cvxzi?n6[ZEOӕG:'8d_):A; mG&ಿy\S]klr79T7 rCGwM1xkA7dA>q:с#ۦko?:1lI4HRRԬ_I'yÒ O^;jq |[5'-Zʸ`-6_ 7;ڠq#]}؏|?`سΤPq<%ظF^BLm2cr˃U߼q3^ +eF~dSmHͬAc,ftk;pa`{4!y䍈QmG;nt NΘv:uz{~Y%#`K!cIC;N){Qӡ޻* q3#<ےffi2H%/LW=\G{*VO9A5is3%>tbHy_lm/~#\Fs]cv(eRib0[08q%9JMr5j`8<5˱!o E7?u& k?ގ2~~8裤LLJFESh>,HX;L?*;0W*MuԆR4=~Kr`';b%ohQa/ Rx미iz͇/mG.|O\4 (> IVPbYןNh-5n<A\Ubf~W'/۾tJ0c}?^8> ⅚GDz\o~7FBYiFybiI J#j3!5bf&65z%f<d3Ym_zm[҄me2jԏ5˒&oć}RFW?/mQ"|_U6AmGFΪz,LCuG<61I';˃(YsbT'6i a;1t`gRF^ ,ۢ;>&M>j*nǾBs9{~{![gVHCL߀lFG(ƭX^AQd=>@IDAT_`di"Pyپ^ 9>v@-=Z>dc.&3lgwO?}ӣ3MX'|OheÆ ׍MW2)|_xhࡱAahRmFKǢ[*?`]7=̎mGCWqCGģpwy&i:֘o4>KhQNlҠ7aA8s6:mrR'>a8ӝ*͘`ˁve? 4Tnxi=bN`Rw}NYjk.WK.m֣zHUm`x6D\4CO;}/~Yك@~C)2EE Q=CڙtvJV.KD4P8xxIO8{W9w;q,|ku?}d~r}\ygqYf=iPu|Es Kz_Kh+#1cK5~i+o{.A }K$v5mIx9]H䋖a.WSr 'E',זa$'=nV; = ,{CL?+NT'QZT!3Jl4+?pK>3vtqD{O A6^XEN;LWд0E71:?i,(Ej;I }"yf|y|5 A?)a-eGWuTj4Q;f{.S?4l \ZGzl ݉ ӻL&U=iCi|@c GHtrc%RK^⣗zC} l_0s sfyIZVLTz1Uᵗ caFhM(<'!|:wPus'M\>dR^x?iNg/c'HGm?|];,h |&b&͂qg U_rCgŧ%I[ ZfڬihqR0GGTON/yJ#xo?}NozYexl]rǸSU"wht|xGME%}C6+>ӣ")|"gMSO3߽5ŃD(=b?{nlݪ`SnÝ{ݾ6]]9YI7cnB;eA<ІgVR . KN|T3}eȬoy2ҕ)I^ w cUL<OglG##qc %ۥ=Ҩ911Yw\2 ;yzcO6}Σ~YWW;[}dCgNÀ'ER )RL`,^T>`oL.+\9y~C@VJkt7-Gzє}c"{#T?꩔oUҧ큮8>m?)-,Ө/`ĆI܊>U+[UFlZ>fWwn;* yBW7}g"&ȖNZD8QXXaqG!l;̴|h"Qpo|#%M_ٿ6n<װNX~6-𦹇Pyp?T3j;loj3A %ϑ6pO3)C[ɒfot.q :*ߎy);4,1KЧ]G@Y'b¨xhgiKQ QdU F| ϰU\O5}*jx8_ \k}Na_'[/4]J.W.}=\N3cjSV;UMWsxl`NM\dl1]έE̱d[75'P -,z}A$ +8)01gcvRiw81Q0 Mn=QA^O'~hܣ`F }b, xNnng|Į^=P65%C.DM@˹}IcEڞ∘5>ΥIC /~k|oوz㏶^7۽VAj@OŇh_\t: 9 RUzVowɞ2b+^ɕ蹿 Ze8Iȏ/9|Pv\8 7s-C,.{|mp?-=6hM^bF~M~9q\Y<$akFG_rr5:y=f4/Y?Bq|%N|F:8iKhM$}6{0yU?X *G;A4Z' ZWr03>4W> N99A߇7^;}v,g^vm7Y Z8I8jGl4ygۃ=r19c߸Te&͛O{;&0*ֿp+kqWbJ5\82\[9/y~N'ZֳCimʚ9ۙw|ȗogP$ 9;ݧWDK4|\0o茺-4Az,K(c=i횮]F[=i;~u<]7U:,ȆXncBNgNOo=.O7*|V6[A}][O;hHC$ޘO3:#;h7L OdğZbOlf\@cUF-W.:0<sG w1|>@DzlGU)R.llh0Hz6)4 wM7B] $KhOF UX5Âx5ZvÀl`buNKtpuFyչM.$?4itd H]~ 6` \ng|8t~&fߦ,>vad}IP2nq/|Że4tKa/؈W]y&c➵Z3$à?cio?r3}`\N\ k,|_?fLdGz&86S7#+ l ^&Bix!:m&QG>ʅ'_xd,X3G8;A'$?ϏMx"? V(7k\lc}qƐiΈTRv[r{ cy^OC67V#xcvj>_vtTҰС״ 2_B_ az0WQQe}TlȚC5rf?q|r`d;C~gOwצq oYOKY;O}g?4veak&E84}T|;΁!MKe1i$0!-b}RnFE7"<+O.| G+ Nk\ji/w쟮C؇Q> TUp?@<Jaȟ&8 ky留q)c).銟)#v_4˰*}#Oc{^¯ʐ@r-'%,EKFth0G&{xzU6)GK,瞷gPtZԤ,;<}CIV3cC,㗅o><;o!UA,2ٔ \SM?}пalmV|U4FVitw!+he@}mtr+3Vf<$OKqBpRG5sfNT\v\GFGh K㣡JY^KxiSן='JA5]~p=A?铦#-\5M)`G=6`#"7ԑ/i>$W/Vho۾I,,6clsxSȿ ` OƕաۤU[oV': <''JH'6J OP xeb"  2><[O6};Xcp R( :kiYWQ FrVAح;1=~eMwtחgZQWbt\DNnxX%624WᲪE+X, ' #eIa<~t}%2cV/->n~6 ]Ol c[i|*Qu-*~ V?nKQX]coF-ć|n= ;onmx!vI>M l?tr@Xxm|K?nzSbKHlAF%ARFn_!uVpw5N \eӚ&jpf<{m ^Rl g1*x7h;t<Ŗ|35sv<6x#UtGom~c +NkmvxhT~0vy6@|4wC^ o`K9 An:;0TڿG*lszZst}Yq~hp[xZ劏f]О=V JWWĈfM9;l/[I '1 R9_x)Nٟ{~o\g[4v魺c ,%A!J{0 i٧2qKBxVfx;;6~otS,4I\T4fg1ɞk ei~kg.F*zpRwOfeSFE/K{y~0bh#ɍ:. ܶlj4(/pIZK ~Ze%v%nm&ނA;6NVEig筿䢽MHr<?Ev*H4lӏ~عޘ씭S {FJg*z0 L@_>s|8 ZxݐgpXV~l/~.]top F3ggI/0Ə4̥;54Z16BYgڋS[a# ja]W cS&Xgjo:3BP9R [R2FW"le9Ec\uzGl2%*1E 4 7CKު0^x__{}O)kO"{*㆛sqi5^?xSYd/|iX4c nb GC $;'ԫg?|/.6;5/hC]5θ{ ;ހr `._F5j f rζhK@-B68$~V/ \Ҏv +s` _/ќFIAOf /hmE$>7>1ȥ;V%}BO-6 z`bx. OѲhZ\X_^H?*j?%k4+_Ђ eGc/zT^]b(/g9@s8ئ͒C i?dyC H ^_ƛ8S/<@4}$[8gSoo\;8 R & 'ݸ oΤY<6yl6̓LRϩ?vN  :9؋p)l^xގvM@o4<%r;+o:g$3D󱈗KS횀|%7 ~T<Ч$k 4TnkQ D3m*I|ƉR>YӹN>2oVrs@vN>+G<3dπ:4د0~x7Ň9LaVO{Ɗ@0(>-N=lo7ze- os9yQ .isAmRlo/Ldq 4KE FhsieN\pN}Leai o8QFŶh36*~> w~7I߫'ig{k1Lh0cs47fD6\G?4gy3tߜʞ؎ Va:ck|>C+W ԪrVOƎgj>6}2tu{NiQwyo=}B9<5;x<xj caym XQ, n8E7<4j1ǯd7NKZT(9c|ݑYnCbS* 0ۋ+/zCw[,k-::DBrcpLw:bUi5)ۑڀ1Kgx@Z9?)* h"Hk*^TrGZT_?nt-:w^rE9k~Xb/lTlh~; ]_7:߂~Yizgw1c;c~gxu{CwAD|p /񱡜v`L9j?ErGѯ&͋?:`_؋<*Khh^Y|3KyWҫ[`]dp\,`J)xJyg&<5rI+$2A?O^S̎ :M\1_wXرlC :!{p[`hv""'YDK l]t'M>sNܸ ]9xpЋqFjxn!AowHX[@64 -Xhbm^< U|5‚Ywp (k/NiД mz5g>l^yUފq>W|Ns*9 5{RcZ NNILZ.!GI3'4l3*E{~ɳVbb+,Ǒl+.9Crʪ1@uB^t4Y #?ؖ b?+rȠd5] ;b}9Vw (_gz"S9΄́@Jy 6ųn(A䍎wu2]Zi; 1OQ֋hxJ8f{ՇnVcN'o.{̞=ڇ1FUg>l픲R;T6y"Hl⢈8kD,.V /-{  ŧʦc1elAit&rR/ݬ~>PY;?“'vt@ #AW$E)3ѩBD_KsDӧO0b{QO?mGk٭$Տ/{;@ʱBPI*]Ra20+#h{>*cz#>t#9N,:rs2%>tGnm?ZIR^ꓷYY*m'm-/귿.)k?$jНӶbkXPt{h,֓n5=;JMs1,۟_Xq>g y>Ԇw  av vJ=@۾XN "*4Rd{x|>ϯx*.KiCg}򧀯߁VT/4rz8Mp /82}bX(%1\~ ~ .wpA8x7]fDqѽ5ʗ[~׽(_ߑ[fee{)_K^[e|݆*79\ay6@weͿrxh5< w#)E JymG*XD4𗐟 q/u@%qHw(>:@㉨-/uV1K$حa\}k;9(v{VWy=ۦĘ~hw4xs wkpXg2hHP=iNĴ`zҵP@v9A<([Y&PӖi4<| cu%%%mG{0u/ _l~= $|z,Ǯ},dوvvg~?#J8˭GG5x]b({:Iu'<#`3%V&p㮅3th*攝"(aMB"2{ݶ Bis>͍ ki(=_}=CS΍XNr,|<Ӽ>q#u]֙omq ~gk.Qu]ۖTC9f`ZVrnn` v^{t۽h<  *t}# Q"泏/R%W~KH|s"׼>G6n |MlEc{R! NG b,m}oK,k^ןsu?q&fau34;K[Қolh?B/+B"Xfi7iQl)CJ^{{dG.]x{k.[!Uю2TA?ve3'edk_c{0ny01д8uL']Vt%g437W$3hy K_^lu68wXp:gGfAэ$]~V+l%E}fƀ?or";oi;i)JML6(n!V:@f}}ːt7TI[(y)PesvGnw,uxS؟>FLz?֋>}CSeO6'(^G+nR !>pn\,q\?R')|CuP5_rώV1ұN^U˾hScāf(d`ڕ[/ r;;}8􈗾'm[k}[Ǡ^;x'v,0\s "+dIU)2dF R ' 31Klp/#O[1YS {WsO6S'\Zrۢ&CCo}Ҽ$snFyrۥ6x#vwCcDl0Z X^W^-#+tJ h_yWtX߱tr#I8AiȜ amq@z,I r[JԗYFZ`{mjx@qÔ hau[~||USދo){hۮ:B6FcLF&TQN*FU c6PcԨ)DP1 67vȲI%Zs5g{yw^kuhۘk٫8/?m<#<{|AJj+ #k?jWXg/ =Xzm:^@4e MZ83_E"--ů:oa/[}?/>qȡJ"ePzu7b `j$q'!a*F@5=o w_3AmϵmO['ughgr;[,p΃2Mȥ>|}k୯?Om/.*92\{p ~!E˟;t6zt.WU{u8[vx!XS58rmz .>GxX VΏ,$`"C3 g.78~0Ňǎ6ϷZ?ޟ}}&祋|w }p :U `{uFMg9Ḱz '&k?Mvӛwˀnϴ/< .#՟دZ{"̟\~ ^,̼ \e[l&UoÏm|U~mڟ}j{Ç~\!@AA[a9qq  /7pCs8Y>meEM}>T42c׆]zߛ|; 9e ƚKA,olMW^)J" Oy\2.&hя1.J~/nwᛟ|;U3{@Z^}3^2a@[_l^2ac/~xg,sW-wq+/\ x*)}cۗi+x]*ɋ" >c^l/{>ܟl\>xw@vu [ `Y 8mhvpkSTLgx駨pN%xQE /*>]zř߱KA,"LLLz0n//a5,$\dDXlٌ/Gk<(b !W PmG}ɗ/~i|[:R^E[K )V/{2/STLojƓ/TgDe,^Tz7؏߾_,'NQ3~*kDÞ\CPp<Ņ_ʫ?5*?>p{5[~kh80n<~]ʟyEi;+#CT^ƒ DʷX8 z`j} GL᲼D'l'~ /pJ[SF~2nRW6o8 /v<Ϲ*W_oǿʶկ>sßm6W[(k:iMxʾp}E0nI ?,FHWg` -&-TZ5NX2!0ԣo,߆> _||`p*vg8Ϧb1-_*|[u9q{FfyY24ِ׷֎3 Z n k8yZ)-x{jFx>Ln>7>믰o>Quj>pI}whRFYq\;}қ Q> Sb~ x>aUή];fξZ޹ē.lD}Ьou7_7ns_S/#t )0kp[P@̦HY8Lc ل+ڔ Ps5V~reкo|AF{[.ĸ{^ڧ=lzpҌuЬZ_e~g}s>s :[SM]s=F-8 ߲-Kj /@|jYk>þV߶4蒜V=#icuΚdfS@O)vY|~Ls?;hͷ|GVo`pc>6ŽyE,yfăt s#.s\hAn2J:jC B`8!V*F [EeFq}[12W|w~ֻ/'bQUT{t99?d/hM->cr5e#}恕L}·2ZVYX߸qo/8-#Y?G]7hw2C:8oC}p/3C~~᎓}Vd>GJ~DD큀h/_'Zߌ. Y˒L=4D/6o]u%i WDM :o߫}k['_Wu]g^cd?|ڄ{?Dl _wװr) L ĘXk.(o~{NIO<"G4</x<#[#S집(P4>\b7'?5>6 AS-mEݬ߫>p Hŵe#.ۋo[;K/^vS4;^ÉvfOG:qS gOrm 2W>4 Fo.uuwKGG1hv/zr|?'xC&w1ÎO,xčLaYX!"S{uy/:c_$Rmx^ /vBGuG<[o6NpR  ,ЈMf@`.pc50!ٹP'ޞ0!K<񼱍Q ?LjO?rzw7þRv-voh|ּno|]ϼHZN[{Už{U} ?'qqE6AOh9?OF0M 8~zE$mQ?clهbh+;O> v/m)DgxGx-!K~M=&Db./yi5̇s+y[p~si{_u cUY^=9vvȆML6m;]][C8uze |h2ې\T_* ,0Sƹ .?RChB[)<7yp{fpl{絚lzDG,56e3Gxw=`?ˌ u{qɣfW)bǻMjгIs@fG{>h 컐/ƃb@~.%?>V@~]\>t$`b }4jOZۇH mErbW ;^ };B;vO`k1_Zm :jB ,5Xd%ͫ|s!8f/8؅BrClS360Ʒ12a!{x[ >q?m<āiD~nH y|Ri\fzvY蟤w|. 7;|;F1uyxE|> 7Ǥw@=GpCn㙊_3<\-e0q  q{*s~2$Ė=%:7DY;ܑw~ }I`#{4Ǜl?\CLcX{c17D-Z3gnoژzp {˷_zC/_]GNS,'怳=36[-Z>'ooY_7jfC{;DxO Ι_qڃ1|N¦xɕg>־Rw l*1n|So{ja0Hʪlne'.O3eX)0qDKq@_LImy|1Chh Gaq .?G&.1bBj2 3gM" p)@Ѱ>>,uã/j?{ro}aDUw{Aks[L&_ş `>t>pz^x9Ο(-Ļ&kV:_|&>^P!Y`GUQMF(HoΪ?=_* r忴x )&5wPxc->ZekJPk{Oyś^MK?~;]!3e?K}#Hty㥡[ )&6 "MPK[`+aC(9 _7fz'p;[xXle$57%OH؜7eK&/Og?. cÓ?HU'_e|گ-{+3ßl{YSjb|_9&e=N868"?y LXNX|1`&D+0# o#{ڎd}Q]>zFn)!oK=ow|Kwg6b2fȖDD^uy^zp}/=>^_νr=͠]uГLo_[;&ij7paaOi¦V)SvE|Ulv,Ӷx-΄_jUr5rofnps,m~ Уsx4-wOgwؽfC9PXp2T<75wx5%7'=hy~#1mN/^ r v 80 tBkAG^jح׏w]2p U9ϢRHU:V6ׯ>m7u8ab2e}^g~]xS^Fb/ǽf x}jQS[|0\YKSi_'36E͸jn-1Ɠ0s NX gX1>'qKGY1QFt1Mo<^\^stx#ã6~jk[{ ?{cvA:k 3WzQhl_ԍb8L:&oHR`}T+< Pz`A(޴j Re ( [^p}UW_|[g04& h,x00K6-"`q++(9E /*^t/J?Gݓ_ۋmV1hɬilrDžUeRU;sWŅLheFUIG\S-[%,>= vOA5_L5*ͺ"LWN}/av\ W˶^Wڇ{\*𸨰+<CƇZ2R)}{O Zw'_֯7Gz`pe\hT[Z leه:ӿ{'|Sw7l_8L/q:nQ_j1uz >06ea÷i07/?SѬe'OqD>‰fO/?>+ds,72L6\j_+b׳p|BF/kY@pbx*m> O4J+# \?b7ZQ}BApQdR Eo1`WHPD--:y[oi myT9azOOpS7TÙn<ծ}C`ޗ77`T4Y4=']qQU1t;Y+<]ם(5R97xDS!,\(MHKq w4)~dA2x]Dc$6]f|ݞ,FV94NԌ-8XCcT,Cy: <Ǩ&V;68\c[o,L"RaM;aԟ+lyi ?+föFƆ9^{ yڜ[.N_o5yM@u޺3yMW+t,@ٳŝzC8ܵXQB"dž{@j@̩ ye)--c(8ÎV}:1z9@<MhXq T3W` qv␛xGx)ұ31İʏX/~(~CRcOŴ:[cHV.C]<7D+rs$av`=xCxN">*vt fh =!n~E7}j-lscR,[8q:‖tÞhfD>Fzhh $MV[=`L5 ;xƣ{zVzfxz-=ú<1ya8NkbߩãBo^w{d̽< ox"}[lQ&~Qgy W znzDǩQOCWN._R^jNLt,/>P'?aq^N6ȵ`a١""braGax搼nӫ\s|4J Ddxkec|4W O|,2t`}?\y#n@~G~Β:[0p4mnu#΄3μy1mϳn &)&1:~]6[᝘2b3`g,\0]F%\} Mdrf  ~KSC׳c^4!q%08qG~C!bt.0qר>t^Hg>шRSxR;jhݞ 'z~]zp֧/wyđd=w0oh <\>+hhqMCf|_g?X7Bf 77K**};xk9"s@X+/tU\(ƾt;]h:pG5GGg<6?>bZ0b2vnaxWڲ4%"&7Fmqr9})h^|- R!:(/f[@y?l5I'G =x]DY5|1<+kx }xFJ tG#>xL.BF{O}/SF`ϰ%?9Fl1ڷwԣzL-E|x'|yjTI<4UO䕯,o> 7?/nTqi|߅9ŸxzCߡ ysSQ:X~ۗ;;VXY?bt&8hϱ/ ؇bY\ҏՕkM& 2^Ne,ς/=s,>/II-ƅ]ܯY"yf35m].Y՞s\qN¹} ooP+^ƫ~ף<:sؕ?زW᝘Z쁛(<ڇw o h/ܯ=S9Oȉ}D1>󱀧'1m.vy{\^Z'x(8!xȿ=0 (7% FF/]ʟE^@јy7v<)#_dc<xwoSI{o bVAt=¡]Ի)%Wh=s|P3%s.ɑc䊎in~gUΫN{=z%_=nr5 aD'?nϞvx簸m!Ap@x[ `/Q{d'p6i ZZ2=f o6c,L6d-FOJ Ȋ>a "Q/y1]fm<2EArLx=X({tg~LjٛÚ`o %834zfd,jwk'ʯ?ܙu\=?+NWaWj~e/8?? Z^ի) Thq϶Y|g~S9gg|7[>}7٦6Ql$)fR('W2,>>~1MLMdl7/0h0-X3j}iYW$XgCh Xz1? : +ub\b$u3>2u<t],Bcغ=r|qlavO2k'P퀍?gPonDτrIPH0'| GAxr &o O )d0t`H?㈱4ճh(&$df^ T/K鶢Kh ^1FLT6X*_P0lku";0|~ _:x@q&MetS٭oxo7Ʒ)Llh_.;ѧ{=91[c-Vؘa6,>u;,99vSz+`\[8}Sf(q|cpD %oe8z{8f/Y#.QHc)dXhgݰBfȁzިvPaEîraĒ34?AGGFpf<56ZaOssdƤB8Z/"JIWh2"s^_1++r`n[OoïG=,^ Y1Bb ;;YU[&WR &BnXik`~7;8)\KԙKG@wnQlQݔ*œn]w7^f!OyWÑ/O}M{8a9O}I& ugyX1 <, gӝJ_t|0L{.:n" wP˸O~ V-O# fqD(?|]͍B<'Eb9_Hh?ٺ|dfJwũ,&ц +(倹2SF1 wHOz3`]mݲ}x/{䉐h.//s}mz}=nԝOO4obs|c~㳿Mmj|Ms>s<˾α֑a/ʕ4T s }>_S.=t0NmR>/P$ާbB < obKs 9m"W]AѧoF-* g|<T=qZJ@ğghq%C Q+ s("|@x./<17}S<} ]БS@Hأy{#3цzȩxD*gx!_%wt< |R` <Z.* t-'_G0#<V@>pm ':s{Zu)گs ,o>'u|ֵ_| س޼8ry6Nr &/eɌn)<qtpyDXXqh9= ZcO3މO>\)q{;q/ !d! C3 7PM`VPuJ=Ny2Kb>!9yńh껝@=VvG>Mu'<<öQiv]p?cg"dL(y<()a88?sUcGM^uH* o@}īs@%i=~2cp7oj"z@I›F*)]˝2ƿu1_懴=Oj08C샩hy~/(| ~`w|<=Zdc+b2 !ddO%ЫU;HnPLA4oÅ+GЕT~tPI!7~g [Kt>!6%>lƼă+CE`2`ψy(5x% t´Ǻ|1].K1/'j=yq|lGlʐ_Acq Ex|C#xpiV9:Jk<seY(?zFJF|4jnjZUy, NT:p5~Kp|gUk54in~w7ZF/Ԓ(OѬi +W%F}َt73{WU3 'eӔ,z.3cO}m >`/y𐱙 "HTl: Z{>LذA5D}OA,S"n !2忩E/YX W K?76Te.$:4Ç)HĐr`gc=TmQGI.Z=@8ܕ7їk6\..<\c#x~|XȑQ`+Y7w?o{c~>nm@h.t~"Ÿ\ΫN{=֭ǿ~Ỳb?·'99?*B{=iٿ{e_ez;6>y:_P8ω W넘̰(%/</RCLlFm`(/PS^Lf0Ob3:困Gz iS|",q'2pp!=Ay Ha:v`͢?A}tzg/ME@S1:q·wW|+ֱn_ۜ<ɥ1f'(VB#!⺸aqǻ|řkpHFzQDFI= a Ɵ+ p f0t{\Yz?Z.@;|'Qz77[X$}a:dȞaH==k8kDqj7?6"7 G=B^^"@0{ XB%IDAT_#hNiO1Fփjy"pH 6&lIº|.ocFr (5I[j\1,)t?y:"jE k1~x~Z|ԉ6p߅G KL]5u]G;R*AGo1&cfzxvmć(druoC  (كG/][zT' h&5b@~ *L(ݔ:$SR-2_xm]"eّ7Gì/oУȬ:;:&×(G9-Ř6x} Cy*J <%>=|bqe`-]guyW^,nMꢖJ?.ڗI2g|7oj||g~S3Ǔy[-v;N^>Zn6<_l_ap~ՏxytZ{ܠ͠ 0:{6.,s)#1~̟}gz#Qkp kԆy@)H`K W6c}AC6r1abE)a^N\C:{T—g%̵xt9z RDց Z >}|RkrLKV ҏ '#ysSxRG؇-/~'umhy47gmv {C~m;qPkem \(xKo>{渭b׷yrŬnF($qBW+W^ M+ rk[bՀ n iq.X2o} 1xP "(X88_!h$qͅ&PbBʙ7m[Ke{9_]z(+8t֌1~CS>5Щ>nǍy9s+ `45G$ަ +Pگ׵g}'3>7?x7\8B8,\Zm3☺:3eD.j…vA{e cۄ| Z>|?U6w/ X W[r_x|{⺋xxE2>[7x)zywQ>VY|~\I7rNYN~.4;D%‹^T9*^zE%‹>Sh?6oDŽO*."zy\2@'\" +b^a8~6;/bH/L.L(wq|/)y T1.mUtސ s 4>S1ix|!iV,Q5x|ݪe8.ȿDux)F"nNu\{wZlzܫ)DE9ֽⶲ/u#8{[)ǹ|ngO~(^۳Nxef/.˅|[Q ,~+ى ':zىf}9^vG4WuGo}k`v3N|p vQK  b߶?rC`x/ېٔc_3n4_!AW;8›!~lj} HMl%.?2WF>bqOJ>xXW<1&2!U|/3qb|@hGdAPCȍC Gjf? y1ܲ~֪c><?<~}s 04o|W&|c~Ω/M;m: >}hEA-uM|!WCEњ}E3/)-;aFͶݽ)}O㣛H,ܺ` <7B=`7Ca !䥵LqPβ[6jSXٹlE fiSsƭAۇcOAXE+˞li#UeHwT\}4T`4bKh<}}}7FsNx %l]40 hPi 7\|UR Ԯ;7 CgwقKe4:9ac&SxMoelLq>tP N"`#?vA"_T1ނo?~Q)_ڻTygVZVZ_}]C~=oR$Jx&k8v|0|0UO#-׈y5 EYl[ y~깸\~FI 9@yꭳgcʷjrǵjbE.6%bEJI,ZEX^@!^)+GYi{@,`ͯǫ=/,kכýx^|g.u5\{=6{䂩c_q{O=~l:;I;zZ"R_EYZ[EOXt)mUKCڅ}p8v`?]` W+Ke<-Z4x-/]ߜhgr6_i :|ĻEwڔar2~..؛^YI{KΪ2wWwDKLc1xtQe:~BP @lI큆SU?\0Whzg}IE]GNg5,Vg"u"2n_22.>ueڰm9r5^  ۅB KJ}0i٫s8 jwj!ã_흳}] ,`}m`5JUf)ײz)#_ Wcƅ1 >_ 9{-VA.CIl|bE(mZh+n X9jm{?lLxŏu&[3I T+ڹzuQ":7g-%B轟e8?jho|U/iӳ.[3l|0FP׽wN ;eK n2dեi-<9/{ /\S<5k+ &e>D^P {+X]i~lmoloN+pzzo/Nw֙ϱF[Y>}|7]sٽVr=-#xt|킌XqZk}7o Mաj1/t;?e*xSxɅT) S4O%.ShƋK.n>}{ ?j̩+l|?XdV`dV-Z3E:mSZ{SxI?E+rܛK }iYMlժ&StM+Ŷ}hmx?t>5'_Y=|mjŶ}hmSx[lۗ~%]5a,‰7>SѬϼpS43>Ms>sAo>o>}Q[ȯ[=X=&m¯ZLRmi*7˺8yFItu5x65J{4D;=>a`~;>c>쐐`x[1uUVY\ƣX`fL%;7Z?y9x//N-Ya{Yyhg>e}3>_xT^^}|eGwvvO evQH$YȴIU~VH>G[;a!ST/p㠝׹^^^^^^QvmV?#C? (L^^^^^^"cVm5DC[ o\@@@@@WlJsuxc~Y?g}'3>7~z\1?WϬ[\o^Dbqh\QE /*^t/Q3X4uwXT' ^/.)|w;\]T˼pf\3^|e^L3n?lv‰7>SѬϼpY?Ntٟf}9^vshg^8Ѭe'OqD>‰f/;g#sD)hg^8Ѭe'OqD>‰f/;9|N43/ho|8YyD~~?WWWWWW0U _o>׶Wdz߃[~|{qxo?_{q=^gӏݨ ީסA?yžpk>AWGgB*χ~>C[>?0>z88hW,=yVm53~>|өסA?yžpk>AWGgB*χ~>C[>?0>z88hW,=yVm53~>@>Z] 4E 0u'xa8o;W^.n8uQsOQh \(uQ9?OQh \(uQ9?OQ衴S149^8u]~]∮k.^qD׵_8^qD>Mь|g|3>7d>|g~]9^o>?M|Mmjn~=^@{zzzzzz殨>W,3.~Soc\M뱩8e.|9~]u9zuǻ.'?ǯ[u9?#stdd^bWq/ܧnݸ‹*9*^zE%‹ح;NEsݦxEpY.hO‹Nnq /:U,^4xEpY.hNEȃ=_#Nw=qz(yo˯Gt^qD7w핧hJ8]c?/|{qݹ^^^^^^Vm5.gxVM5TN(@?yσ>.u\)_z>:z=P~>@?jq·\@@@@@Ab^^|hџme´+/:?>mVcIDOO[~>ո8  {Ŗ4g_~]|+0c+܌|M|2g>3)~SiMͯsu}WRS4?\L/~ /pJ?G^w|+QE /*>n+\]֒O⯛g nsTx9‹J>Gd$gspG43/ho|8Yyhg>e]u=޺]׭Ϻ]|ux w=7E&|@@@@@X|E|~zqz}vGϺ3~x|o\z?6/{qr}< `Q%nn:ޏ\x{;޹Nz<lwx=\/Vr:љ mVϏv>^zzzzzzGKw|^zzC[|h+0:t%uu@yσ~ypOz;љ mVϏv>^zzzzzzGKw|^zzC[|h+χVWML) 0 2^|-M%L%v^Ct. S4O%n ^/.)/^)*\Sx3^ST)z)uv^1!өzJ⥟e:<3/\q.ӌ|Ƌϸ +@\43/g|g>3)~SOg|3>M|2g>3)~SOg|3>M|2g>3)~SOg|3>M|2g>3)~SOg|3 a5o| e>[g]u=޺]׭Ϻ]|ux w=?3+9:22f/A+=(yz3Nt ‹f/.˅)^x)\ /SS,^4Mֵ^t.?| /:^xQsx| /:Mֵ^43 /:rGT _)h78蜽psh78?SѬo^qD/;9{DY/;Ѭo^qD8Y߼∮_vs‰^vY߼OqD~y]׿Dg:8LW^^|hQGpV:L oc`\JGC@=960-"*h_]ǨWJGC@=9639-)h$a\YVTQMJFC@=9730-)&> \YVROMIFC@=:730,)&"2!YVRrQIFC@=:73/,)%"DUROFB@=962/,)%"wÀQOLoL?<962/,)%"zQKHEУ<852/0)%"g&GEB^t61.,ZCEb@A?;t0+(%3UZ=;85a{?""#qE 541.1Hǀ /-+(%!"33 +.&#! ʑ ƾĎȽṵ̂»ΡĻ¥㩤ǻ餡տҞĺǦ螣ة”͒Ѧ뎐ܪᐑ澑П ԢÚ򜒒斒⧓倕 vty~ ݀ރރ߆ ߂߂ 쀇 퀈 ǀʁˁinfo bplist00X$versionX$objectsY$archiverT$topU$null WNS.keysZNS.objectsV$class TnameTiconZ$classnameX$classes\NSDictionaryXNSObject_NSKeyedArchiverTroot#-27=CJR]dfhjlnsx}Quaternion-0.0.95.1/icons/quaternion.ico000066400000000000000000002235361412757327200201120ustar00rootroot00000000000000(Vh~  00@@(26(sXE=6/|)x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"z'JkBx"ttttttttttttttttttttttttttttttttttttttttttttttttttttttttHMz'ttttttuy~ɀ̂υфӆӆӆӆӆӆԇԇԇԇԇԇԇԇԇԇԇՇՇՇՇՇՇՇՇՆՆԆՇՇՇՇՇՇՇՇՇՇևևևևևևևԆ~z']~-vvuuvɀ؉щ&Jx#x!w w؊)¬By$y#y#~"֊""!! *O|({&z%ʄ$$%$#"!! *q~-|(}(ˆ(''&%%$#"!!!*³=~+}*ˇ**)))'&%%$#""! *¤}1-,ޓ,,+*)))'&%%$$"!! )Z0/֏//.-,+*)))(&%%%$#!! )B00000/.-,+*)))(&&&%$#"! &Ebp~ņņ{kW;"(`)A2̋221100/.-,+*))))'&&&OƒܵΚe1!|)>4֒44321100/.-,+*)*))YҠy,ӡК)²;6ܖ6654321100/.-,++3~էăʍ)¸A8887654321100/..2Ä`eث)F999887654321100/j֥yX8$ 1HgʑƇN#)ÏL;;;:98876643211B۷{9'%$#"!! 8خ:,)Ûd<ޚ==<;:988766432bɓA*))'&%$#"!"! ($,;)«?ژ=>>=<;:9887665njǎ6,+**)('&%$##""! ۴4#N)DΒ@@?>>=<;:98877֥ذA/.-,+)))('&%%$#""!ǎL֪e)ÕSǎAA@@?>>=<;:98:޶m100/.-+*)))(''&%$#"hmȌÃ)ĨÍBBBA@@?>>=<;::ܰD22110/-,+*))))('&%$K˔nҡ)ďIܜDDCBA@@?>>=<;֤ٲ:532211/.-,+*)*))('&7ݻ!I )ĝh˓FFEDCBA@@?>>=Ƃԧ876542200/.-,++**))(,, ,9)¶ďHHGFEDCBA@@?>cԩ88776541100/.--++**))թ."! /&)Ɲf֛IHHGFEDCBA@@D޾<988776431100//.-,+**…߿`&$#""! 0ў)¹ŒIJIHHGFEDCBA@צB;:9887654321010/.-,+bʓ>('&%$#""! D[)ĠjٜJJJIHHGFEDCBeX=<;:9876654322110/.-.a.*))('&%$#""! m%)ǓNLKJJIHHGFEDCÈ?>=<;98876654432110/.-,+**))('&%$#""! ֫Nj)ĨٞMMLKJJIHHGFEz@??>=;:98876665432110/.-,+**))('&%$#""! -1)ǚ\NNMLKJJIHHGFbA@??=<;:98877765432110/.-,+**))('%%$#""! r͗)·қQPONMLKJJIHHpܵCCA@>>=<;:98887765432110/.-,+**))('&$$#""! -)ƥvQPPONMLKJJIH\DCB@?>>=<;::9887765432110/.-,+**))('&%$#""! h)ɜ^RQPPONMLKJJSFECAA@?>>=<<;:9887765432110/.-,+**))('&%$#!""  )ءUSRQPPONMLKJ͏vGECBAA@?>>>>=;:9887765432110/.-,+**))('&%$#"!" ÅH)ůTTSRQPPONMLKIGEDCBAA@@?@?>=<:9887765432110/.-,+**))('&%$#"""!1ɑ)ǣsVUTSRQPPONMO۱IGFEDCBAAAA@@?>=<;9887765432110/.-,+**))('&%$#"""!)ТdVVUTSRQPPONvHHGFEDCBBCBA@@?>=<;:987765432110/.-,+**))('&%$#"""!-)ݧ\WVVUTSRQPPOҜTIHHGFEDCDCCBA@@?>=<;:987765432110/.-,+**))('&%$#"""! Hb)XWWVVUTSRQPPϓSJJIHHGFEEDDCCBA@@?>=<;:988765432110/.-,+**))('&%$#"""! !nj)ijYXWWVVUTSRQPԠvNMLKJJIHHGGFEDCCCBA@@?>=<;:988875432110/.-,+**))('&%$#"""! ڳ)ǬZYXWWVVUTSRQPPONNLKJJIHHGGFEDCBCBA@@?>=<;:988876432110/.-,+**))('&%$#"""! ԩ)ή[ZYXWWVVUTSRQPPONNLKJKJIHGGFEDCBBAA@@?>=<;:988876532110/.-,+**))('&%$#"""! Ƌ)ױ\[ZYXWWVVUTSRQPPONNMLKKJIHGGFEDCBBA@@@?>=<;:988876543110/.-,+**))('&%$#"""! z )ڲy]\[ZYXWWVVUTSRQPPONNNLKKJIHGGFEDCBBA@@@?>=<;:988876543210/..,+**))('&%$#"""! m&)x^]\[ZYXWWVVUTSRQPPOOONLKKJIHGGFEDCBBA@@?>>=<;ߚ:ߚ988876543210/..,+**))('&%$#"""! f+)x_^]\[ZYXWWVVUTSRQPQPOONMKKJIHGGFEDCBBA@@?>==<;ߚ:ߚ9ߙ88876ߘ5432110..,+**))('&%$#"""!k,/|__^]\[ZYXWWVVUTSRSRQPOONMKKJIHGGFEDCBBA@@?>=<<;:ߚ9ߙ8ߙ8876ߘ5ߘ432110ߕ/.-+**))('&%$#"""r"4漂`__^]\[ZYXWWVVUTUTSRQPOONNKKJIHGGFEDCBBA@@?>=<;::9ߙ8ߙ8ߙ8ߘ76ߘ5ߘ4ߗ3ߗ2110ߕ/ߕ/-+**))('&%$#""†>㼆a`__^]\[ZYXWWVVVUUTSRQPOONNMLJIHGGFEDCBBA@@?>=<;:ߙ998ߙ8ߙ8ߘ7ߘ6ߘ5ߘ4ߗ3ߗ2ߖ110ߕ/ߕ/ߕ.-**))ߓ('&%$#"͜Gྍba`__^]\[ZYXWWWWVUUTSRQPOONNMLKIHGGFEDCBBA@@?>=<;:ߙ9ߙ8888ߘ7ߘ6ߗ5ߘ4ߗ3ߗ2ߖ1ߖ10ߕ/ߕ/ߕ.ߔ-+*))ߓ(ߒ'ߒ&%$#޼Sbba`__^]\[ZYjԮǕzbUTSRQPOONNMLKߠJHGGFEߟDCBBA@@?>=<;:ߙ9ߙ8ߘ8ߘ787ߘ6ߗ5ޗ4ޖ3ߗ2ߖ1ߖ1ߕ0ޕ/ߕ/ߕ.ߔ-ߓ+ߓ+))ߓ(ߒ'ߒ&ߒ%$ҥ _fbba`__^]\[Z[ЧÍsYOONNMLKߠJߠIߠHGFEߟDߞCߞBBA@@ߜ?>=<;:ߙ9ߙ8ߘ8ߘ7ߘ776ߗ5ޗ4ޖ3ޖ2ߖ1ߖ1ߕ0ޕ/ޕ/ߕ.ߔ-ߔ,ߓ+ߓ*)ߓ(ߒ'ߒ&7zߏ! uncbba`__^]\[ZӭRONNMLKߠJߠIߠHߟGFEߟDߞCߞBߞBA@@ߜ?ߛ>=<;:ߙ9ߙ8ߘ8ߘ7ߘ7ߗ66ߗ5ߗ4ޖ3ޖ2Iߖ1ߕ0ޕ/ޕ/ޕ.ߔ-ߔ,ߓ+ߓ*ߒ*ߓ)ߒ'qL"ߏ!ߏ ߍ۹xdcbba`__^]\[߸`ONNMLKJߠIߠHߟGߟGEߟDߞCߞBߞBߝA@@ߜ?ߛ>ߛ=<;:ߙ9ߙ8ߘ8ߘ7ߘ7ߗ6ߗ5ߗ5ߗ4ߖ3z]ޕ1ޕ0ޕ/ޕ/ޕ.ޔ-ޔ,ߓ+ߓ*ߒ*ޒ)լ&ߐ""ߏ!ߏ ߎ)һÀedcbba`__^]\ЗzOߢNNMLKߠJIHߟGߟGߞFߞEߞCߞBߞBߝAߝ@ߜ@ߜ?ߛ>ߛ=ߚ<ߚ;::ߙ8ߘ8ߘ7ߘ7ߗ6ߗ5ߖ4ޖ3ȕ͞ޕ1ޕ1ޕ0ޔ/ޕ/ޕ.ޔ-ޔ,ޓ+ߓ*-ٴߑ#ߐ"ߐ"ߐ"ߏ! 7ȽŎfedcbba`__^]gҫ΢PߢNߢNߡMLKߠJߟIߟHGߟGߞFߞEޝDߞBߞBߝAߝ@ߜ@ߜ?ߛ>ߛ=ߚ<ߚ;ߚ::ߚ8ߘ8ߘ7ߘ7ߗ6ߗ5ߖ4֯ߖ2ߕ1ߕ1ޕ0ޔ/ޔ/ޕ.ޔ-ޔ,ޓ+lvߑ%ߑ#ߐ"ߐ"" Měnfedcbba`__^][ߦWWWe~ǖԯ[ߢNߢNߡMߡLKߠJߟIߟHߞGGߞFߞEޝDޝCߞBߝAߝ@ߜ@ߜ?ޛ>ߛ=ߚ<ߚ;ߚ:ߚ:ߚ8ߙ8ߘ7ߘ7ߗ6ߗ5Tޕ1ߕ1ߕ1ߕ0ޔ/ޔ/ݔ.ݓ-ޔ,޾2ޑ&ސ%ސ$"!! n«{gfedcbba`__^ԟߧYߦWߦVWߦVߦUߥUߥTߤSߤSf輁OߢNߢNߡMߡLߠKߠJߟIߟHߞGߞGFߞEޝDޝCޝCߝAߝ@ߜ@ߜ?ޛ>ޛ=ޚ<ߚ;ߚ:ߚ:ޙ9ޘ8ߙ7ߘ7ߗ6ɖޕ2ޕ1ޕ1ޔ0ߕ0ޔ/ޔ/ݔ.Mױݑ(ޑ'ޑ&$"!!! +ݹȉggfedcbba`__mߨZߧYߦWߦVߦVߦVߦUߥUߥTߤSߤRߣQߣPߢOߢOߡNߢNߡMߡLߠKߠJߟIߟHߞGߞGߝFߝEߝDޝCޝCޜBޜAߜ@ߜ?ޛ>ޛ=ޚ<ޚ;ߚ:ߚ:ޙ9ޘ8ޘ7:ޖ3ޕ2ޕ1ޕ1ޔ0ޔ/ޔ/ޔ/ծ[ޒ)ݑ(&$#"!!! @μʕmggfedcbba`__zߧ[ZߧXߦVߦVߥVޥUߥUߥTߤSߤRޣQߣPߢOߢOߢNߡNߡMߡLߠKߠJߟIޟHߞGߞGߝFߝEޜDߝCޝCޜBޜAޛ@ߜ?ޛ>ޛ=ޚ<ޚ;ޚ:ߚ:ޙ9ޘ8QLޖ3ޕ2ޕ1ޕ1ޔ0ޔ/Qݒ*ޒ*(&%$#"!!! ZÞ~hggfedcbba`_z\ߧ[ߧZYߦVߦVߥVޥUޤTߥTߤSߤRޣQޣPߢOߢOߢNߢNޡMߡLߠKߠJߟIޟHޞGߞGߝFߝEޜDޜCߝCߜBޜAޛ@ޛ@ޛ>ޛ=ޚ<ޚ;ޚ:ޚ:ޙ9lŎߗ4ޖ3ޕ2ޕ1ޕ1ޔ0kݒ+)('&%$#"!!0v̐ihggfedcbba`_迈\ߧ[ߧZߦYWߦVߥVޥUޤTޤSߤSߤRޣQޣPޢOߢOߢNߢNޡMޡLߠKߠJߟIޟHޞGޞGޝFߝEޜDޜCޜCޜBߜAޛ@ޛ@ޛ?ݚ>ޚ<ޚ;ޚ:ޚ:ޗ5ޖ4ޗ3ޕ2ޕ1|ݒ-+*)('&%$#"!Lӻɖuihggfedcbba`ǃa\ߧ[ߧZߦYߦXߥWߥVޥUޤTޤSޤRޣQޣQޣPޢOޢOޢNߢNޡMޡLޠKޟJߟIޟHޞGޞGޝFޝEޜDޜCޜCޜBޛAߛ@ޛ@ޛ?ݚ>ݚ=ޚ;ޚ:ɕHޗ5ޖ4ݖ3JZ-,*))('&%$#&dãʉjihgggedcbba`ٸߧ\ߧ\ߧ[ߧZߦYߦXߥWߥVߥUޤTޤSޤRޣQޣPޣPޢOޢOޢNޢNޡMޡLޠKޠJޟIޟHޞGޞGޝFޝEޜDޜCޜCޜBޛAޛ@ߛ@ޛ?ݚ>ݚ=ݙݚ=ݙ=ݘ7ݗ7ٵ;/--,+*))('&)`νŜʊkjihgggfdcbbaԠwߧ\ߧ\ߧ[ߧZަYߦXߥWߥVޤUޤUߤTߤRޣQޣPݢOݢOݢOޢNޢNޡMݡLݠKޠJޟIޟHݞGݝGޝFޝEޜDݜCݜCݜBޛAޛ@ޚ@ޚ?ݙ>ޚ=ӪߞDϢ]0/.--,+*))('EvΗukjihgggfdcbbamާ\ާ\ߧ[ߧZަYަXޥWߥVޤUޤUޣTޣSߣRޣPݢOݢOݢOݡNޢNޡMݡLݠKݠJޟIޟHݞGݞGݝFޝEޜDݜCݜCݜBݛAޛ@ޚ@ޚ?ݙ>l100/.--,+*))*bмś̎lkjihgggfdcbbjjާ\ާ\ަ[ߧZަYަXޥWޥVޤUޤUޣTޣSޢRޣQݢOݢOݢOݡNݡMޡMݡLݠKݠJݟIޟHݞGݞGݝFݝEޜDݜCݜCݜBݛAݛ@ޚ@ޚ?ݚ@ԫ32100/.--,+*)Ksї~lkjihgggfecbbuqާ\ާ\ަ[ަZަYަXޥWޥVޤUޤUޣTޣSޢRޢQޢPޢOݢOݡNݡMݠLݡLݠKݠJݟIݟHݞGݞGݞGݝEݜDݛCݜCݜBݛAݛ@ݚ@ݚ?׳9432100/.--,+6hҼŝϖqlkjihgggfecbbȅާ\ާ\ަ[ަZޥYݥXޥWޥVޤUޤUݣTޣSޢRޢQݡPݡOޢOݡNݡMݠLݠKܟJݠJݟIݟHݞGݞGݞGݝEݜDݜCݛCݜBݛAݛ@ݚ@v޿;65432100/.---\x͗ʉmlkjihgggfedbb͐͢ݧ\ާ\ާ[ަZޥYݥXݤWޥVޤUޤUݣTݣSޢRޢQݡPݡOݡOޡNݡMݠLݠKܟJܟIݟIݟHݞGݞGܞGݝEݜDݜCݜCܜBݛAݛ@ޝEܺ<8765432100/.-GkɾªҚ~mlkjihgggfedbbɆoݧ\ާ[ާZަYݥXݤWݤVޤUޤUݣTݣSݢRޢQݡPݡOݡOݠNޡNޠLݠKܟJܟIܞHݟHݞGݞGܞGܝFݜDݜCݜCܜBܛAܚ@ݻ٫<988765432100/9i޻șИumlkjihgggfedbb}ӭ߫cݧ[ާZަYݦXݤWݤVݣUݣUݣTݣSݢRݢQݡPݡOݡOݠNݠNݟMݠLܟJܟIܞHܞGܝGݞGܞGܝFܜDܜCݜCܜB@~{;:9988765432103cr¿ΗΔpmlkjihgggfedcbsЧ߬dݧZݦYݦXݥWݤVݣUݣUݢTݣSݢRݢQݡPܡOݡOݠNݠNݟMݟLݟKܟIܞHܞGܝGܝGܞGܝFܜDܜCߞB@IW=<;:99887654321Zpνљˌnmlkjihgggfedcbc٪ݾ异ݧZݦXݥWݥVݤUݣUݢTܢSݢRݢQݡPܡOܡOݠNݠNݟMݟLܟKݟJܞHܞGܝGܝGܝFܝFߞEAA@@?>=<;:998876543NnۼÞљʊnmlkjihgggfedcbaŀƖsݥXݤUݤUݢTܢSܢRݢQݡPܡOܡOݡQg۽ӭݠMܞIݞHܝGܝGߞGDCAA@@?>=<;:9988765Jp~əљɆnmlkjihgggfedcbabИٸձڹ۽^ܞIܝHޞGFEDCBA@@?>=<;:99887IpxĿ˙љDŽnmlkjihgggfedcba`c֣}ߠIGFFEDCBA@@?>=<;:998Hqtɾ̗љɇnmlkjihgggfedcba`__ɇUHGFFEDCBA@@?>=<;:9Mrv˾͘љˊomlkjihgggfedcba`__^hΒˌUJIHHGFFEDCBA@@?>=<;TtuϾ̘љ̎pmlkjihgggfedcba`__^]\[jɈӝܲ޶բƂbNNMLKJIHHGFFEDCBA@@?>>]uxξʗљϔxmlkjihgggfedcba`__^]\[ZZYXWWVUTSRRQPONNMLKJIHHGFFEDCBA@@EivzʾəљљƂmlkjihgggfedcba`__^]\[ZZYXWWVUTSRRQPONNMLKJIHHGFFEDCBARux}ţњљ̎slkjihgggfedcba`__^]\[ZZYXWWVUTSRRQPONNMLKJIHHGFFEDFdzy迄ÿ°Ιљјȅnkjihgggfedcba`__^]\[ZZYXWWVUTSRRQPONNMLKJIHHGFFYv{|پȘҚљΔ{ljihgggfedcba`__^]\[ZZYXWWVUTSRRQPPNNMLKJIHHSq}|˿͖љИΒ~kihgggfedcba`__^]\[ZZYXWWVUTSRRQPPNNMLKJVn~}~޾ƝЙИЗΒ~mhgggfedcba`__^]\[ZZYXWWVUTSRRQPPONN^vŀ~ƿƛϖЗЗЖʊyiggfedcba`___]\[ZZYXWWVUTSRRQQ\l~ƂƁƀśΖЗЖϖΓȅynfedcba`___]\[ZZYXWWVUTXerDŽDŽǃǂʿåȘϗϖϕϔΔ͐DŽ|tlea`___]\[ZZ[`gnuƀɇȆȅȅȅÅĜɔΔϔΔΔΓΓΒ͑̎ʊɇȄƂƁǂǃȅɇʊʊʊʉɉɈɇɆʼnÙȒ˒ϔΓΒ͑͑͐̏̏̎̎̎ˌˌʋʋʊɉƊÒ ÙĘƕœƒƐĒēԘ???( ȥxՙ@ٍ&ڊۊ܋܌݌܎!۞I,ZȍɏN:KSEթq1͘P+ح(аFجL29N*ըfdΕ?93,&jV_[NGA:4,&7„gjǔgG@ߙ9ޗ4ߖ1E})wh迆RߞGߝ@ߚ:tƐDH۾gէ迈ޤUޢNݞGޛAܻϝ,׵~g֨ش较|_d:WhjʊDž[Geǿsmt۽( @<<tFŇ3ʅ'̂̂͂́͂͂͂͂͂͂͂ʈ/™aՊ' !¬Ԑ4+)%! -2, Ŧ}71//{۴Eʏ ñ>86\D##߽#qE H?:730,)&"}!hVSqQJGCA>:830,)&" I!eYVSPNJGC@>ߚ:840,)&"8"i]YVTQNJGC@=:ߙ8ߘ41ߕ.)&C-s`]НҪXKGߞC@=ߙ9ߘ7ޗ4:ޕ.ߓ*lߎ =轂c`zΣٷص]KGޝDߝ@ߛ=ߚ:ߘ7ˆߕ1ݔ.ٴ˚"XٿgcașߦVߥUޣQߢNߠKޞGޜDޜAޛ=ޚ:DPF%!׶tgcϔkߦXߤTޣPޢNޠKޞGޜDޛAݚ>ČТ,)4ƿ“kgc߼iަXޤUޢQݡNݠKݞGݜDݛAƒ7/,ujgeĐݦZޤUݢQݠNܟJݞGܜDYݶ>62Lǿªzjgd͒ڸ̞əӬϥJC@<9GԼâ~jgdbDŽܱݴʋRFC@Rټëȇogd`]ZWSPMIKgӾŜƄre`]ZWT]j羆ĢŒʼnĂ„‹ſ?(0`ZZxQ9|){#vvvvvvvvvvvvvvvvv@C~ ҅!>Ї&%"!"c͉,+)'%#!!ÑKߕ00-+)(&MzўخٯМyCD֦$!ƑJ5410-5ȏ+3R!ƕS:8641w̗_3!!<{(w!Šl>=:88զ{.*'%#"̖Ң ޹͕"²DA?=;޵A1.+)'&q"ϛ۴"џYFCA?қ߿:420.+*Uԩ3~"įIHFCc?97411//r5&#"ΚÂ"զfLJHF]>:86531/,*(&#"$/!½ROLJiܵB?=:98531/,*(&#"dǍ!ɰSPOLܱfCA?>=98531/,*(&#" "װyUSPOHFCBA?=:8531/,*(&#"Λ>"lWUSSИVJHGECA?=:8531/,*(&#" el"gYWUSPOLKIGEBA?=:8641/,*(&$" K„"f\YWUSPONKIGEB@>=ߚ:8641/,*)&$"?Ȑ#k^\YWUTSPNKIGEB@><:ߙ86ߘ410-*)&$L…*p`^\YǍ鿅jTPNLIGEB@><ߙ98ߘ6ޗ4ߗ2ߕ0ߕ.+)ߒ&io6|b`^\ʛNLߠJߟGEߞB@ߛ><ߙ9ߘ8ߗ5ߗ4Fߕ0ޕ.ߓ+ߒ*ΝCߏ J㾋eb`^ԠWLߠJGߞFߞBߝ@ߛ>ߚ<ߚ9ߘ8ߗ6ŏ@ޕ0ޕ.ޔ-ߑ$" ggeb`oߧXWߦVhhߢNߡLߠJߞGFޝCߝ@ޛ>ޚ<ߚ:ޘ8֯}ޕ1ߕ0jɔ&#!ָxgeb`}YߦVޤTߤSޣPߢNޠLߠJޞGߝFޜCޜAޛ@ޚ<ޚ:ޖ3ߗ5@(%#3ǾÌigeb{_ߦYߥWޤTޤRޣPޢNޡLޠJޞGޝFޜCޛAߛ@ݚ=ߜ>͟ȕ-)(%auigeb۰ߧ\ަYߥWޤUߤRݢOޢNݡLޠJݞGޝFݜCޛAޚ@‹޿5.,)2ԻƎligedںߨ_ޥYޥWޤUޣSޢPݡNݠKݠJݞGݝFݛCݛAQH20.,a±ƀkigeitަYݤWޤUݣSݡPݠNݠKܟIݞGܝFݜCܛAM7520FϽŜykigef߷ݿ|ݥXݣUݣSݡPݠNݟLܠKܝGܝFBfC;975>ẆȔvkigebȄۻxGDA@>;9@vʔxkigeb`{ܲ߸~HFDA@>FvĿɖƀkigeb`_\Z]`SQOMJHFDAV}Ŀś̏sigeb`_\ZWVSQOMJHNl㿍°ʕɈtgeb`_\ZWVSQOYmϾȖ͐Ƃvlda^^ahq}‰äŘǓɐʏʎȌŎ?????????????????????????(@xxaG2}+x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"x"S{AwxȀх։ڊ݋܋܋݌݌݌ތތތߍߍߍߍߍφ!g~,~!׉ #{0˅&%%#!!$GƄ+*)(&%$! #®9ڒ0/-+))'%$" %?JJ8% Y#©È6210/-+*)-lٰӤ_"Ӡʍ#ªȌ:75310/->ӣÁΕ#ʎ>:875314ǍذwN' 1[8e۳#ǑG><:876C|2)'%#"!׬PM%$ƘX@?><:8V˖6.+))'&$"ąt80$éCB@?><:9875310.,*)(&#"!ӥ#VQPNLJܲFCA?>=;9875310.,*)(&$"!q%"ijUSQPNLʒGDBA@@><9875310.,*)(&$""#e"ͭVUSQPb\HFDBCA@><:875310.,*)(&$""ݻΛ#ذvWVUSQ|ب_JIHFEDCA@><:885310.,*)(&$"" #lYWVUSQPNLJIHGECBA@><:886310.,*)(&$"" e#j[YWVUSQPNLKJHGECB@@><:886410.,*)(&$"" O#k][YWVUSQPONKJHGECB@?><ߚ:886421.,*)(&$""I"$n_][YWVUTSQONKJHGECB@?=;:ߙ8ߙ86ߘ421ߕ/,*)(&$"Y+r`_][YWVVUSQONLJHGECB@?=;ߙ98ߙ8ߘ6ߘ4ߗ2ߖ1ߕ/ߕ.*)ߓ(&$j5}b`_][^ٷ˞龂hRNMKߠIGEߞCB@?=;ߙ9ߘ886ޗ4ߗ2ߖ1ޕ/ߕ.ߓ+*ߓ(ߒ&˙ F侉cb`_][tNMKߠIߟGEߞCߞB@ߜ?ߛ=;ߙ9ߘ8ߘ7ߗ5ߗ4J;ޕ/ޕ.ߔ,ߓ*ޒ)Ǐ"ߏ ]ecb`_]ʚߢNMKߟIߟGߞFߞCߞBߝ@ߜ?ߛ=ߚ;ߚ9ߘ8ߘ7ߗ5jyޕ1ޔ/ޕ.ޔ,AYߐ""޳ymecb`_ˋiWd{ǔԭߢNߡMߠKߟIߞGFޝDޝCߝ@ߜ?ޛ=ߚ;ߚ:ߙ8ߘ7ܺޕ1ߕ1ޔ/ݔ.ɖޒ*ߑ$!!!ӻ|gecb`bӭߧYߦVߥVߥUߤSߣQߢOߢNߡMߠKߟIߞGߝFߝDޝCޜAߜ?ޛ=ޚ;ߚ:ޘ8Ρߚ:ޕ1ޔ0ߙ8čޒ)%#!!7þďhgecb`٪jߧZWߥVޤTߤSޣQޢOߢNޡMߠKߟIޞGߝFޜDޜCޜAޛ@ޛ=ޚ;ޚ:ܼrޗ3ޕ1ѧ;)'%#!buhgecbiߧ\ߧZߥWߥVޤTޤRޣQޢOޢNޡMޠKߟIޞGޝFޜDޜCޛAߛ@ݚ>ݙ<ݽزޗ5e,*)'%,غƉjhgecbϔɚߧ\ߧZߦXߥVޤUޤRޣPݢOޢNޡMޠKޟIݞGޝFޜDݜCޛAޚ@ޚ>龃Y1-,*)'Q¿àvjhgfcb羈ާ\ߧZަXߥVޤUޣSޣPݢOݡNޡMݠKݟIݞGݝFޜDݜCݛAޚ@ߟJE0/-,*2ߺɎmjhgfcgƔާ\ަZަXޥVޤUޣSޢQޢOݡNݠLݠKݟIݞGݝFݜDݜCݛAݚ@\420/-,]ſ¦DŽljhgfcnضcާZݥXݤVޤUݣSޢQݡOݠNݠLܟJܞHݞGܞGݜDݜCܚAÌ[86420/Fڼȗyljhgfdiǖީ^ݦXݤVݣUݣSݢQݡOݠNݟMݟJܞHܝGܞGܜDݜCRL:98642;v͓tljhgfdcբִ澆ߩ_ݣUܢSݢQܡOܡPj绂ݞJܝGܞGCA@><:9869jǿ͓sljhgfdbnߥUGECA@><:9;gоª͔uljhgfdb`iբڮiHFECA@>asBIT|d pHYs$$P$tEXtSoftwarewww.inkscape.org< IDATx}yŕy]շԭ[B^$^l{ǻ=3?goݱo33`؀l!07F-HnWuݙoȌ̈VuW~*Uvdߋ/^Df-B -B -B -B -B -B -B -k ծ\P/_lZa0&R -3p0yNpl=R_6%^Sp[=P2o: #Dp_ p&s `٢(]ژ=FMs\X6&tf"W`s!ù{1#4F[xނͷYo\{68!`+'MڔA %.;.DB09aB`Sc@n3Τ.J6W]YֱWY'4 'lޒ7W:M![!*@vII _PzC&C:c 8 W.ٟK۟96=8NMr&ަ \0^"L)9!_BL3Н`0"['G_Z6of}' F,N8Rtx@:ِǑ;@! ;ͩs?q+V|WoJ%~"?ZtG<狯cIqȿcock "p+[&?k375{wG;3qB"0! eG|^m1%i f9ؓ-9v릶'i 6v'Gy`H8Tpxyay$g4 b }_krǍ|1K] kf" ey'"\cy)wwbeډ.A VXX"$D$_η+pR1T-6~侦u, S5e L&ySC:F%~.k{I]:cjyGaQwLExe*boq%i$üC f(#E¾I}Y#E XߦsLJ^|\^DqI=fBҮ Lz4֣n g#Es#Ovذ|"_@ò'*/M?\>u,<a(kvL?[EtۯԚ_ '<~7,c"_\&U]@lPsYͯymSdYMX;R .4N΀tymNqCx`<̠~m@v qS{48DcSڅ|eb6D1=Qnbh0gU&$l-Yȕ9J6G$tX1U{/oHv4P[H3"_;prαobr?؜90E!p7M,7N4?3%ϖ @azϚ|{ae E>˒_m׋9On[?ψ89^WxN펶&K=Ejo.ߝT8^gK\4<كdW*8/OЛ*m_7H[YEO"QS4c]7#/لvb?~ w@%|L>1~pV6%a>+ qM1#^s%D4T̑{J3#Vvi:j"~sߛˉ9 0?!g!b0{/xzn2ܸ.:'Pܣk?҅BF p[hc|5};+ms{f yS(G Tĥ|D^)$81KS{ o9lbu/-hװUr%u?"svLL9k00|:5|xm5E7>4dKEϪp)|?>>{A|xI)p])T10o `N`%]I=V䋆$>rYpÃ{䦕; |Uݻ<.yƔ + dG/k/Ou:tp8GLuᄏpm]?c̉$lEy1db4pï0ܴBͣz^Eoa \6PeK7Y8`-MM'.Ick. a .:՗ʡ|}3M, 9xlܴeٲjjG^p8?K0 3hh|};%3q\{P{s&+d3M'm?^xA%]HuRHuL/1;뗩`So={{p^/q ADo߬ gL4hL BOגwb",,09T"xrQi WoiO@*[^,+|7yO;@10U O,x9K0 uQ'*iCבBtc#N h¿>;D/v)_@h#_6ǽu TGC ~ڲ#WJk^TW(EsNo}wɬi`XFc'WЫ'`9j'pOˣ]Vl, /y\E'|@0u`~lI}١F bڸi=mB.kOX+P粁3fZ4M(xᬐ"vN9& AՎ )}ȈtTCTO|70f_>Hσ؝ÎE%YqlXTI!ǹi4Hƴ7-zo^a(hpo}~ᛗ@ABՇ?(9 m=^,yĻy(ls:q(B 'W-.x 5,GSOi۴Vgb  "MT|r>x@~W6\ P/PI=RДI>8frJ{^ 7D(H{'JJLh̫Tޒ(9sCQɺШMXӟIj?bY@`QՔvlp8[^Wm#xz!%kZ,,)4.i5]1b͌|p|uO<7s]vΘHX6` (ۏD#]6@evX6\_ʭK~K'!ӑx~r$V%S5E4L {]ɮj2ܓ2l\uZ2.m5w;ç'pIpSLuqN 2$N&@Bo-i&]R e2l\цʁ{q@Xq< 4=!L63u}9C4EJ_4򉀵*r_'ŏZx-Ce:@-,F`P6Pߵȇ*8B].`x1/vqe;Ƌ ˒sW; ΁$(ԁ0^⩷))6 6ovXL';fWC%|!|| >(oj a0kaˋ=`. /MJo'8@y}l`_3NаTEOffV4k$>C/:գbiM .C2J=p ;G,#teH'kX➐rV*BcΨ#1Z[r،gdOM9,pp]ܩA'˧z$>V|& Vxe :1 m_]f~ .+0eu4 2up=o:10u?U7hCg1L+/PCHfk "t.(6ǿ ūy=C-C/--eTnHXs8bQY`&IԊ٢qWp9[j\;7@=~5|Q(`b\&vm|_by*כ{_ک cH2~zFchXSxgD'<t#Q==lE[F%T!ȗBK Ehxڗʮ_N*> ʹFk0^xQ@IM5KߖFѰx3X B.M1e 3No&c[ 4uf徘h+Cً$DʐwGO5ߏX^T1<ES`=\1逸SM6 c0û{LYAxVKh!Bp^ƽaQBAa6hE>=`9 `Iwf<e"Tk^)-}dĆ^iSCKW눅rӴthTH%/"`s`爪.\dP@NMfP5)8- -ݶ4eҋAOz]}K%K|3J9ŽCNXÓ6S)<9yB`|<<v GXna%$(ĤWya_}Z޽%xj:fc/ȖS{ǚAoh]#@UgI Cy|aEm rHŽx oa  u2PB l`V*E>=hH \ļtm'0 B$ ,<{ˬ6 HNjkH|ycފ'?}NJ`Yh|D\?>y; BzGDƀ?] r@{CHhq 2S}04oؚJϣɂSiq_[;wjy=Ҹ hc\*b]qܴ"1wq|6 3{ȚaPB?A2޺w©! JGv/ 81E ]΀y~zryyBJi70 _by\71U#2Lyh@G\çe,ʣ.5OʳhoX%jo |ؐ91\MIKLU`AL L5*rA*gx˧$1|nCzjwkgdďJ>?~e%[e=|xFmɱ"pH3\a6C8s^ : \ur LwFu\EЕqUXխ?bMNEy6=rsm&9yY_dk?ݠ6zߖ6}C#|'oؚ ;2_4˳L\Wu>u7Vx("4j~Zwp㘳 prygEe?zᅵގOm肮k mP΃)w~8rYu7z[8cewY]Gl i5_7'Srw ~Fyr#%Ҁ;*LJ9zS ~#ewrBj.!䈗8P q)Ĥc+ :#G")PZѝe/8y44̙Ȣ bnqӑ~t]sVU@J1dBZD" M#q oYT bxiQe]Zu?-)?k|xjXʳܰ1F:v%O#/B^O\ ˸JU=I߼ЉTC B;c;7{37gbF:/<Ǐ_PVg6ڣH0#̩o#];t:E8oA\i/pJ'`ט[ ך #wO) VX-߆pɲ8%,2 X 1`8`84JFwܐhg.i۝9og݋ҽ]6|IۢevqsyܵCޏR#M%_fNdUp;z,-*Pռ3nV~q՝2BD3*]OOdԉ9}Lwa2@,Upt=9O1~[z`8oa:g3eQ5!f+Gt+ju)3v3:[9kuex \WL =eD!#J"'oڱ9a!$woA ,LXCis%4ɱoH`;RYNjTIDAT:ftg`ӊN' jk: 0ƣ'Cã+I`418kϬf Ar>emQ)`iެ13ltz xxh_qZ5oZw.To?]Rj/`a8fS胏0f1]}Ӑw: ̈́y}>ǂv3CSB|%I auyxZృwŅ;gݸmʿЍ~k1"0\-"cǰie.O`yMp~e*P g|n^ kUF_[GjU8KAT8 Ș|>Vplg\@&0q΂X^6[O Yx|CF|PG f :n<7.!`qg>EgRXyP$h:0o%<:p/R;BN4Iv|&ΐi-Dp@P{uQ @ߒƭO>KSgyݑIY#V": A>=-ru:~5(uec<@/n8A,N t-"w X%p O}N5~@0{3ϭ6sLv^@Qs׶?/#~2啋kCRC䥯c/&:^;XEz+Pqπ^/G Gܘ#-}JI2N&UǏi}1~DS+ m6q:%U5 8N=~uvlZXGԭg'L}@4[p0hoFI|L1@`gHsσ;6/ץ̶* 8# σ;?GzyATs7=uKv)e)-=ԼZ6Iŏ`ډ(@):jFCHou_B -B -B -B -B -B -B -B ?Sh 3IENDB`Quaternion-0.0.95.1/icons/quaternion/16-apps-quaternion.png000066400000000000000000000012241412757327200234620ustar00rootroot00000000000000PNG  IHDRasBIT|d pHYs|4ktEXtSoftwarewww.inkscape.org<IDAT8=kAμzJbBbl|AH(/ ""` +?@R b%B 1IsEsX%:O91<#?O:/:u>T%xcB &YpH ~Z~4:LS0>z⪧+P)E̲+KH7+ SEീr)P=>k3GbF$abFIÍkj7X љ@;v6h4S^^ju?H=˛Z ̑ZňBC>GCm@3561un+H#uJ~'6+4[ spv D[Ilh+2j@lf[@6U[:$dK>* D jF25sg\dEj=By y ],?5Aђe!U-afPR._-6`z/:A7q1nowf#?"lIENDB`Quaternion-0.0.95.1/icons/quaternion/22-apps-quaternion.png000066400000000000000000000020061412757327200234560ustar00rootroot00000000000000PNG  IHDRĴl;sBIT|d pHYsXXtEXtSoftwarewww.inkscape.org<IDAT8KlU7B[)HR,$jD67&jpa0DӅc׺.4QVX ARʹәo:qKJҳ:9痛s/BhX=k3[-{⿆ij>[ADS>,5ï|ܪ醚Ņ+ ٦L?#lM}]p1Db5p*hh=+UciMj̧OExRc=AqAس}S9'gP]>n^*Om3/2r|bU*`P bṔYr 8upTSH=X7 U>7){.&J.6(u)ݭYݾ|Lok*\>Sedyً/'r84^ezLQ ^?x&JJYfs?qqv 9U3m3ԃU !UCC66RԛVbf!2q.4U! dqc.%DbpA }mK_ZԜA@.^ ̗fFWLUaޡz\}e :&&5TDǀ;/^]C_uV7 j̚N ^ǟ@qυ+IENDB`Quaternion-0.0.95.1/icons/quaternion/32-apps-quaternion.png000066400000000000000000000027711412757327200234700ustar00rootroot00000000000000PNG  IHDR szzsBIT|d pHYs : :dJtEXtSoftwarewww.inkscape.org<vIDATXm\UϹΝٙlun%P( 1M#RZ'~MST M~j"Qm5DD %b/hݝ;s9m]rrd%?P搵3༊"9:׶}okU{ oYϞƍzp8糫 9"~w?c^k6\ljb)lN1kzL#23[{z=4hrWDajC_ #\NS􊘁XoXW[;Sk~<ݹqPس!ĩ3)z m ?zA;^̻[W+}G~l7H|v^iPxK%@{ Ʉ VVr /l YCGߞVazhs9^hjmbђݻ_l[R@KTc=6'sqťx͠6o?X"F8_d aS///)N*겡p5F%3x~b+LԯǛL<w;-PƵf3%t,\\;h;ks>;*!wB_PŧGKxkѹs[ >p5 8W"0R5ahv<© )c^ԡIsxIuWM6-d6Tz V }b(st 8UAqXr19ܺj«2PJ<ⱳH@Nt'ߖe'8^ɹfn(ļ}!Zk3J|_-t'GgX{eءj+*Cm+Dň(ă+]#qIum  ! DDg>zJ7r5IENDB`Quaternion-0.0.95.1/icons/quaternion/48-apps-quaternion.png000066400000000000000000000051171412757327200234740ustar00rootroot00000000000000PNG  IHDR00WsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< IDAThk\eϹҋݖ,k@-X ʭA E!H5Z` "A##bҫV@K+^fwnsfvt|lvvw88 ݻ|xR&Xtd+:&8iE 挑bۋ8+B?=tQWnlJz-?( UF\GHPAƗЪn̅LҞ-.M!*+={ͅ#_@7]%ߐ##x6'K+G ْ"ٖ&gT /~ƾcN`mhOlI ~ZpBƏ._43jy{a힐z}?&g)P֗6g5;\?ǡ [l̇FOӛ}Ҿ˃+{xO0 $, םǛt(hғ0VY=˲l+@2hl3h2mv ,y5@,˩W sள|Nvo6g(Z"X xCΪsX2Oo^j2ў檥Wf^s(W3#/o= }@$cWc:S|Z\\*Q N `MU_(\=6۳,ݸR%`B]P$ҺJAUߑXc&/HjXsa[b)p,%ް?`T+RјDPLSg=ę֩5UȖ,y{QwZ5=\ZOF1 bGXםc<[K~v &h^p_'1M0TD5$;l_g'e|," DX {Cma Z/0ĚHN GL jVy ,h+rV [!`ThE;VGzwC>a~0P@`U *mO1ojnhI#rRq XLnc5Js<6@io^}/ϊ *;>%r:*}֞dKuBk~liKm룅<He|MUoV:Q?\=ОqZF9"& D yc̔{D I8 N4 $@hM|LF߈D^zC~zNe':NUQF2(E#2|ρ{(/=}ʅ_:Ig3zϐq WΩ9i w屎UXkyg_+|+MK+;KcGX]bn$=(I T)դXǭÚ=|>t3MgJBκʮt$Y8#I^vlWE\='IϨoD8(~ֆQٝURZIU̦Լ%d- ͜qX]Ab]-EgZ/KӴyԷMRaq|_ѬL$aJIENDB`Quaternion-0.0.95.1/icons/quaternion/64-apps-quaternion.png000066400000000000000000000074101412757327200234700ustar00rootroot00000000000000PNG  IHDR@@iqsBIT|d pHYsttfxtEXtSoftwarewww.inkscape.org<IDATxyp]}?.ᄃ'Y%CBS1; -%I` NKeBBBwg%Liô -5CH Y [W/v~},HөΌF{|#czL1=ס>oQlt/\j,P'(C: (@xrC^4OV^c=c&'-SS0eRt4h!ԂzP (Xf=恝{-yNwmBUyp° 5Ot#Hu;~P8 TD O|T5r|+ҿ. 8t0hP4县(-pJ\s2^)w~i(T}ظpVAfͧ>H9E5ժyM|0Xe/74UP|ao]S ϊS'PAU_\59wTƇ/ذbɒ~{ j x|;eugD? g/{YX`玊.!'Z?6үnH P)JI6xnW;G(-gL佡~_^,:ak|o[c[ hiK~({=| ?HbxbĿ%\$O8"}u|R)kB#\#׼up xT%P(IFx2|,NKҁ<5_S0=`2!Jm|mJX+b(~{mN6X1`fx!$O3na#x[1P 5CVx_\x/ +2 o]5s#zX6Wyjk5ikR7' 8qN8g~d7x͏#X27j$;JUo2LC__eX9Ϣ(j/ E-KpZ?OV! ZC6yeJ^ ^ |N", P¥r؛e5IlN׀8R߫aw9k6jJl>zHcp.h\C.g]ᙓHxf #~ts-!6lu~EezlyibUl`EVauY||n-F0߅6rQ`9ÒckE3F`):->.^i\#m6o% 1/aPǻՀp$jV!Pd0Cc LW4) }U\iO gsBZh4=uO\C] 9s2Z (8]=y\?  _| nuD|'J,I|q8+3罼7r[HVP^U왴"vkm1V*CIe2}r|^ZSN\4BI¡Ftg_ьfsaPh}K bf!ަ-pbs<_;L5ȮYhӗa%0X8 [aVqn\d )lVzx@&?bfu\Yyt3]=ےad<"tȈ_M$ 2i1EO>jVeۤqx}?§qCȈR:OT8V{j;K LN:chIWuQfQJv5hXq|M~y(ۨne}u@C0 8sB/=v1[묰YA+8!R1m8>qfm8 rn:*6 7?$ڪChul.iy{TIO69E<͍B-#xdk&YABCH怣esݞL9du%9#o5qu8q,8Z`E`۰}Q-^tOj vUUm-*Gb4Ԣq"gUnq"01 u<jP`N[C^GJa$Qi^wi^m}XGI3[`=իժho(nZ^baOū!@h|c]sFfcFy`=>[Uw|0IC;$>~C^>4ÎZ^F,mѢvoo*˻x99F=,?6LMKD)`ڳ˵{[X/@=MޭE9NTzT"v#'C8⳧ʯt>(@ٕ v{wuW*81sm~٣Y1`v,>AࣕV&heEwBpއ1XѼ? *kǯ/3`rÿi/}- _ckϮjR÷ kV=Z=7-Os݄8'ht !/ܾsl|r#4a~_+FzMO,w~a˿!=hsB=PQm|ffɅ |Et 9@r@^5/a+.>!#o{TiyPȯp\ɂ&nR']`kKm#'l0pY}FN*Pٞ?SS֜ٺܱ&OP }`76h(Y'}'$#W.8! ڜcaC<7;.O$ۣ_v~'hz_?Ȃ'6vt 8\GU/!?DbP tS ́qO>7bŀ͆]3Kq[uUY2kt8٥E5llX6?6[0$Еi,td_ 4v| pz+c&:ٺ˜M͘6 ~.Hcg|$c֗7tu*,öwaIJAY(g3NHP]/C]~zay3c{yZ :#Of(Y^w,czL1=w(UIENDB`Quaternion-0.0.95.1/icons/quaternion/sc-apps-quaternion.svgz000066400000000000000000000101531412757327200240470ustar00rootroot00000000000000AWquaternion.svg[n#Ir}(_aV1Jv^D%)\,^}"N%2=]S++/'Ndpfno/&88Tdi-uyueHo벼fɺ.Lj)>^}nwbR7+6r16M-w3٤/hl]h}U.6Is/C%11"Fx-Ӈx25u5O5@G oC6h ]~W,Zf6+g?c2fɲ\i?w`mznE5zY^_L Ol?H9K}xx^^L0wQU~[2o߰ j4/w1;xYEmV?2[z5 =,-R5\TU4Ob"dbڷT* uK"1@۶o'le~.Ƽu ٔ}i)^䛼|^,jҽ G .&Hj`L>96,#56j; Mo؛l}6l7Ǖ*??y6fq=8&sJ CU+ ƏwkfwH+bku*?mVzjI?gu57 †Pᰵ\Y[ςos"|KSxU]bPas0#jĂ'0/bLwP"K<\ CK#Ņh1;]"Bc~X6q-O( :i9W}uinۧs8OLt'Lz3s*[σ:{Qg.|= jSD5.y t~NOv Jg{D{# V)7$sPL,m+|O oԡߎkˑY)jvmYbfK.((@̈́z Atz"f!VO +*OШg\-G%ڭ V19c{YoOs0;b`:Q򟶳oFlo#wL' K!*%;T<\-SpUtK2u)ǝ-6Z;B+-Q>:={ɺ_\s^-=NXO"[l~\bb,{-^rx8s ggr$~)4RrIcEg_ *^g2)/C"qI~x%sIeK`HG6zmq>C_% o`D?EiΘ̼羦Fv)P%8,Z#:g=NmyAKO܎n)$s;H~M:Hq֔W{lvͼ`[Iﻅ# %a(pOR_q9~ٞYBVk9IßI^O)K 2mp^eյe^,Gg^U@yuﱦ_~N?,=m^>x_.]Ҋ*w}8h^Oؤ~5ԶJ7 l7^Ɨt,m|/Ͱ*%=~ 7.Y<bӦ]TWUhI!η-qJgxճ6h2GJdkV:Kﭴ]@B 4w G?aA%P.тvP^[F9[lַqq| l"Y5+V~u[_n{! l7aӻ2?Y?Ĩ]UKbsV[oˬm?W wP- ^iTeh=[zapʻȿdq #/_ޕeveMixجke QP$F7?8Bz{U0 ~!=(ˍe6tVv9O)w~ǒ-\BpiyWd=E?,o׀rscSQكR.5 1O 1[#ħ1E{6Xg,Ud`M_@Cə~ʙpL.  +B"[3,Q5h1WT%j+h*f-VY1%)n0ZN#H+b`t00V؅TU@WZȃ tbZ7F#wpK`X':qa)8l 0c"*AB7+Q,-3 E炂Y-qJyE  +b&r70gU6BT b8.BRTS`pߣoBeDGê1X&F/FaKgS%(-P7A^o?+{N x}KTZ@9mU࿁YWކ|X] +:dìc[Zk]<G5[&M(-M7Æ(ɳCQ<u![tI R RFVq (rё!%jFXk >r3_H"bn(htM9y {9>=#g\' 2XRiUf23(L V4GF "r"O"CmP4ѵ.`kD&U Lh*z(tџI $R<@\+j$45н"@:xpB!B^^CVs;܄=DidQEj$)V4GXW@X >Gv=C8U'\e34!FXnATã ZqOJR1•<σq$ Yѱ,YS x#d=T4pXD&%~)$2Q@ExUDAOlOz+ *I TX 2ECD +tफ़@ ?qAp /&_0X\vָгcjit/ DdA ᎀ E֥;gҰ.hc°́V s +S2 "!$c-n*ījI`XFn)4DIʄXG2 j =te4te`NUἤ=\_\h |@F^ 6fCppVzކ3hݛ-~F+YI/YQ5A }9xڪON[yS 3&#G&١:HKs:3;z0bE"g"ϥ_$>oBD9l[m]7fBFQuaternion-0.0.95.1/icons/quaternion/sources/000077500000000000000000000000001412757327200210665ustar00rootroot00000000000000Quaternion-0.0.95.1/icons/quaternion/sources/quaternion-green.svg000066400000000000000000000431171412757327200251000ustar00rootroot00000000000000 image/svg+xml Quaternion-0.0.95.1/icons/quaternion/sources/quaternion-red.svg000066400000000000000000000431151412757327200245500ustar00rootroot00000000000000 image/svg+xml Quaternion-0.0.95.1/icons/quaternion/sources/sc-apps-quaternion.svg000066400000000000000000000431021412757327200253400ustar00rootroot00000000000000 image/svg+xml Quaternion-0.0.95.1/icons/scrolldown.svg000066400000000000000000000034161412757327200201310ustar00rootroot00000000000000 icon_newmessages Created with Sketch. Quaternion-0.0.95.1/icons/scrollup.svg000066400000000000000000000042361412757327200176070ustar00rootroot00000000000000 image/svg+xml icon_newmessages icon_newmessages Created with Sketch. Quaternion-0.0.95.1/lib/000077500000000000000000000000001412757327200146515ustar00rootroot00000000000000Quaternion-0.0.95.1/linux/000077500000000000000000000000001412757327200152425ustar00rootroot00000000000000Quaternion-0.0.95.1/linux/com.github.quaternion.appdata.xml000066400000000000000000000065741412757327200236340ustar00rootroot00000000000000 com.github.quaternion.desktop CC-BY-4.0 GPL-3.0+ Quaternion

LX.+""""""Rz= W96^Ak{s7q??O>`bZƳ2]teN*CQm;j?ŵxNg\vqJLkE=LhΣ>>˃O5}5'xf߳8w-?k`wZי='v6X~`[ie3 ܶAN~ 3t/dVel3 l,X~{M~MظZ˜r|7$ T-m7" Y_Էcɞ}socMtwGJRb#G +kyQ9h?oO-@_QÆM-<^iW½x6kdt8+gZtyAz0iF<+Ӹ~_җ0S6%ٽǿ\^K͜+`Znv]i㺗Y55oh[\mf|oo!X솞a f\e6 3=9O+q +99l>@YNBW: q,8'}JfΘb߱m#r*vDAb/3gF.]? B6IQK.VŃi'kY'ifֶvǎ!BUL;xϖZuGnZW< ްU+jTb{%'? go="XngLՋF0]ǃU+cdZ9xON6<4" y;_<)agE;ŴfOjXG'?SCx?iqpSd9z\u;l,)nY.Aߨaɪ:bpS-<σtaڵx8lcS .iS0Ŏط2b{Ħ]aM <"h00 zgK5wGx&vL?I6 H|S\,;Ys𐟃lnu Xabm]0`]Uy2;ue0ybX;^k`"if]̼rcPyڤxAֆy6ڒ|y7p}XxKkؾ>sW;~%y+cz`ړ}p!O *wZL ' ,m҃ZyhsȪɻZY{ŴҍD<+03Mq_5)W:ٟ-ڰـ61Pve612&96Ǝ7YZ+3ղZffC}2&c>eV4S5L)ۖc <@3S~ļx=c,zkk=ie^s/9VcζAguuGefHYC~f\`T9=x0v|9l2"(i^"67u&h%s/MwNG/ r9>8z80Pr(:>fv}Pp0!$g񋕫*.s:omcS̟:ȅBm$zeyIf,fzP VJֲv7_īz΃mn楕dtC-<pp+~ע??ZQcZt T5fNm&XA_`H~s 9* ;׬j䚕@;==/  Lzf+)hw@`wk&8X~o {Wq(Ɩ~;I81Iww gomz> SQ̺nFƴɠڻyم6V:82@ձَmj\NgAe6>L>a;Ջ̟I?~ nXKQHWN6<֔~.3&V1Hz,ˢs˲p8Xs[]Z@,_]:kaL4>I& `-uio&kncD;vssիj^M;?4c3Bb5aYB _,=s'kCszo| $L>]W>d-/r0bK cd̟d z/iN LqX#Vֲ}kS ^|I6F9 d`ej'}m:NXLt};s? :۞P\}49x؏eYi> `T{yB,ogK-| z/[^nŲ,6njacMkX'C!*N\+{w 6<4`&Kc23gbϧ` yj:Yѿ"a/_a#ˎI\T90*8xUn l~;`laˢ nbt;S~AƴF; &L[@mG=϶T۹~7{omm%O.G D'Mf\aN .WZN '.~A~;G]m7}lx Gdl+/wRpPQf$79a -x (z9x|zE-G$wL_ˈR12jw,uEWr\?O5,vm,|d&_<ΒjV>lz aqWaN,_w3dWE?ĺ&s ><Ô}.=/3/s1~siY zƴF+i4g~z}Mlųl#Xvf\]swQ|79kąg]qpvOPJ{~̎L3ͷ$Y p`|v#ǙӱSSz.;as<-peKkbr#gleZY#70Z7s<R.x-:c|?퟉Yl]z>$"W:_++X}LK-PhdʜQom>َiRy3y],'>>)Jy/yČ^-~oH 9c鳸[jٳx;toͭui{"p8Y#Wzw8<`&ȹ\x+lob6*.wsϦ:V])* tɬmemOc} UAv<;p5yi/iqLV9tnL[vɬZ}GYv 0cE@'Sfܜ{;vsG:o0g۷}tv᜙@?Ymv,v?wZ#c\ɸ̫/&xC~N~곆􀮮`?$YrkYi'^kc5¾ijا bVNwb;?`diژ2ϜOK2WTsy0 X? &DسÜ܌F*服rO55v=-_4Z.9FL/W|R~55qmy+0tٟYŔ߿xUָX.+5. ȹxfw͵ؽsn}hovK' n핱, 1[4); =%ДgY3xD" ?idȕ{@g6٠Ӄ@<j{OR90s4> 1!`7ly> ȫ\bb3/$^sG݀YIP.#X.}{+^UŴ׸|A(qw׽!ODĊyb݈Z7fT: ĺy;%  &r{>kO>,UCτX5 pPoYRE^s?O ֦2Ϛ?dpڻbsoyG!'joMkD=LL=FJq!"""c0ū R0y(bmM4?Ú)nd|HNBt țy,DP7oww(70G=z,ĂYCyQnPpcyϜ1 :lj˔H1_ru뗉>:T fY&r*sZ9!vND"yU+sߊX#'M1GǞWB15|⧺=@qtY?t9yG(ףlhLO:K P IDAT102;G8d=β5Ӝa{W;k> ne4v6BtL7JgrS^{o}ltylY58bLWʊi00>͒o`ȕ/9aˋ-T_faY3].vҖÄb`?քeYv֮}1`lx bNmg],[Zk?lFkgd)5}e^i^Qip+Ȭ!޾)"l\L/? PX ugm?@f0A>+VNuuXl42);ufXs+C ]ƟYXapĿ_J+)2<7<]]D z/g-`ALF< f1 m^Nr?  ,lr 3sֳV:xnj _,[ZÞ<% OQ_,bKtkS YDDD>oA⭾ܟ.r!|HL "Vqf8z}g9Jګ1 4?^A/>S9aXˬ368ǶVJ ?ܗ#GO OH>Raa<Vna/9\>`cqޱl8jƏL|?L:w^f{*G\{4uSb̦ŘcR\WWX6%C4FCZ`]l6F@7 ;Aw=#Tuuw!Sdu)wEonzG_juTG=|~{udFh4nhn'ZLhnw벛 l)NMe FB2'@/!LHp.GhvD s"a_Ns2jY$A)[`ײj4Fh4ͬq3!ߑwUFh4F5pO4áZǝhSͷo'<ͱIa c~A?z{ʼnW[Rk>XяFvLN\a.N$r8f 1R)$b+$Bm6e-b$ A8j_Fh4FL;y[{h1HF?whA-T̓7m÷5bG7~H~l+ݓSXm ɳṁIJ; pvPT`Ex1x>8ưH4aA8zgcFh4F܉\e.V|vm aUxܩ$^hԅ VT-V-nq tQ[-t;kyO;`;քmm:W:yl/$&el]Q5[ W:Vvw6w$PEwIour^Lc "|'£>B1*kt X,aֱ֟tvoV;Bp,-RXb=`,]1ߦ'~8 %I4@$>h4Fh4ŝuasv[5wZi[/ZZ5=-I.wqx'_hcſi!*0ũ<~P{ؘM6P|L3N ZɝWTrL/p]2*RaNu/#$Øw{֙NGuƲX5&ŗMH.N_khdGȥ3GNo>BCg _? ̝]6d T<#Ɠ%`_tqhO`0a! k@֟|#i/JbJ&KNX-o͌,y'V_tp'a:Ww/ >ab;'Sӭ J6~TEAci!|h1"Lu>(4\eLH6qxdR"] $gkG!F]\GHLh4Fh43|hLް9muh6^)<8y1fn. 0Tz 0Y26Ge\,YU˖?vi{DVP2MÈܴҶyML;j#eͩ7ʺ6~N-Sx>w+{EM78vJ'=|]Cqʎ~SrX&>rIE";Ȝb~ ɱd/W42̌*ȊҮ9 N_( /es(%P1ZP+4hN>,8z;vs!D+ǥ4?w|]y}wpboogFhy[o̯|d5eW*qI)M盨ߩᩎ~||}SohO(10)q@-.Νn#`.nj;Z@ǀZ 8џ8P_ 6mtbG Ɗ,__oz({U-Hz O1c4 rf]Jc!9FABp)T&;R~~o}NيR];!HZ~<+&E 90.[ZpZ;vRE9]+x&Vr*<B5S=;!6?Z˒~-|-] UNJUջtjui1 <ܮyTT2ܷؗ,jTOh~{YǶmʷTd%~o(\zطch4fmf>162.ȔFW*+߼S˷wvOQ̟wxN:F8d\^``E~V'Dɲo%<ʼn)k ̹ד-GXxaԨĖoQ6ǹ$_1}Hr$W+e7 G bhT0ӈ8فmۼu}žg~[ee&q/voOo{eqնu6vs;u&SS[ Oa΍(0j9\qNw+mCoo@F%TnC5R2q^նJ̷ FSdr'?G5sSsg3 #)& GqwXP]4#$,oj5PKq? 0urHwGLeS ]G%.jM`Ӱ`qQo['Yv c9c"ʦw{Ș%|X:>V@RdI< Ɇ[1뎋CFʿU <#*EHBC~;?XaW'*}PX^\Wg S{fמFRg:1ͱWv{4kO#_jc&73-zf,uƩ̤@˄Rlۦ@ V֢#m|z#Y|`<s0n7SHߩ-t&whH ej*] oY_ 8T,MR*yy6e/Fh&d{(=P.6)+"ǜ:Fs_6,"L:kT<˱/,[^ˡ3Aqq}Kp(wF\ v21„vF!~lX&xSvh,G6$%,UiMGr,c'$#z XȱXUo:Y5E$jR(/+1(BrvQyX& ȨvoaId D>eic߳ro>6?\{$bT8Kрm/|nkO㘥|z%~m~.@8mi[Ӗq.TTq@KN$Z>|rG˖g _mž7ccGkl E*dZnٺ9mJ8^LLo,2)tn+C?lNT!~Ovm`.2t A/{Ȉsde.!rn!RCʱpe'ʪjE*Kn>oT uי=ZGxBWT7QhLUvS9UI ml2B.y#)[X[*;u ˫9oT61{&Br,!$@*ň?w2B-BI=J$cK$S x2(߄8kAHdg{\\N?XC)c+(O #t偨ˋjJ/.GI)9F;RJ à|"" ˯:ɣR|/[g#;EuضMIJ!"|7oz(M@ J)~'h-h.v~ox=ݒ>> 28hֆRS?Hݺkh~y"B>yQm5/kh-*t9q=Xe&r(ThIJOu⤓{ [e:dTdB\22V09|dGRۼfD3=n5 bYL]:B1Bľ&ߩ-f&D%qVAŃ~7RmXJ!aI(W{)60{'gr.Nw^XPyFh⟷_3'eA-y^['UI"ׂ\(A5UxRv6:&^Jװ}cz?Ȁe#ca,[U*_/s[% Xmmˡ=/ Vz_/=]op ÷hdiڀx+k9r\r\k0GǪ& jL >-@-<7&=|$f|iY#o:V=pD0dɚZ]rq4X ?.35Tx|FOO+$N.w;VyQn\1L 0)$O2l{Vy -|Sr##6-y#hs#ə'c?gB!+A{m9s#A˟H,?#𨒎4e ,%EoࠝDX@JdT]CFa>I]3K?^7-T{d?_JH&JzD.8"Ѧ5طl&Dcʦ 5i.)$]*^]Ȼ]*K|c&v7fY"\N ]Ѕ=dq V%rB @Zg"83=rz黦Sٷg|̹bb1;ߝO7Fim?UG(M(|Ȋ*֯y 6֤4Ca0A$bq6h6 Fh ar&e:CFÜ:I4XMU~|+ȩXiϓOC6 ~VoaCFor'@/l_ʐk<(Cz> *X]姼%׵n{^[*`zow]ǼLN<f1Z 2 CDDEgrr.H[a )sĠm='*̬LγDfN|ι\'c*w*u[ؗnM5+*lRljw;8x%~>Hb7~eKiP8R) P?-+݄c9$_Y5~h+gap|3L2#c,EP'PrH^ʎr HDܮ^Y!˖'4KMΝ${C/o1&; n5 .vM:)vSS sYHd ?Xn^>$KE&䟇/t rꝎ$ ?%%{k4F3U&marn ˫XW,V9:MEˊjY/K (-^`ʟ"rCWHwk-2D9fKw /QZrnG&D-XP8~3GW+&!9EiP,!4WBHRJek,WFǩS׋[jx Aȉv.qҤ;.&1҉VwHĢع![ FPT.ϿqٴMj8|cos/ -UJn#psDD89rni% q.tNxfի z!ʼnsL RFR/:_P7?Rp =ߊ=.@ `cc6>F*7k8c*)8 4vEV{}+i}bwo rky7-Ѷm> Sԟn&RJ^~I.6=F1PIv/62*fvݮkz&):>0R7o.K 3Y|xIv`Z[pּOq!8z+fŃFhylmO1jgW԰k{Cώ _ΙmRQmu|LO9Xp4\8d.\(@ţGRf' u .r"FWn*dbSh\>帅 *J"#,nJF(G|€pԱnt{~-]ZOE49}%K}!8wOoڨ;"_s-#6maߞFzuK[0+}A꟨cݚjm5xR:݉a,G~s-(5XW`_. l,qZKٴ&j=Ɠ;XMj([d"tOb3iTsN>ľݍ G%9_i^6o:[pb7G_o,r7K޵X[EI8Hrxb`zL'Ws/HWSg~xz ŰkGC|1P1G,+5Dq<+XrM$^'Sa*w*u ְ2A.vS/8{w7oqf[ٵߩa*![6-xV*sz7FLvbhUXd2 /(6^֏DKP"54FG&k4eYv;&P$+/ N4O$>YB\DT7; EBD 9|L'_iKD^o0 hpYmᏜDQ7q}'4MΝdj]@ rFĠbsطQVfrR3o˖n pv>\ W?Rϵ[wnM3J'tDSӗTVxVlfZ=ۄB/|'+8f{i b)fߧ0 ƩPͱ}ys?08ue~α.ptGZұ\u+mĹyt+^&b\୷ۓdˣ0aݙg뚞I N)ߩ-tocFcosVSvL|**~Nۙfònm5b*_ q6Nx:/KY%h4l1# {4 Cyh$H0CY}IZnfx.zf୷G>\!!-'j4soN!(}}|6bcJX Iz$?se{P .ߔRT2q.1>JTz RA-ř0xJ'ݳ-)k&˾M?gD>fLs'sbooh4B'z'x?rV x3BP Ylr<]@4̹7 */$Vl`6i' U.gT CO7PXr'?G5d~ rx7ozXL,ŸuGupg?~OS^heõ^RuKͅFs;K6 d̄{E k=bZ勵}|#"d ێrxVuWȿQ +AzG JMV䰒hfI-ɢ=aܨ \dL03D @I.VH4yKUY~K9)DHizDZH`XqepHR킈SY7L!G_mcZvi9{͝gFh4LyebC-+ )bc]۲G[ȟκG.\^:LE&bжbB! <!QHT-2J$ZJ-$΅dHNJd2R'1]%!$_)o[4h1bq[-03LǑlXPɤ|i1[ x{WxpZ$Lh4fL:0Q!%m8 ! #J1Rm WT lNҽxdr $LWmp"l-rE ɐ!9q4Ay9RBr=\h4͝g7/Yh4b.i1Y3 ²E\LVNlb[toբl{ [KaEioQ\Htf4t Dn^xŽ_*}ͱm Vo3¥DtLh4Fh4 Bb XAIFu3r#J R2 3"-Q `ж6j7]3Yf{MrK$#V>Cd[HJKt[[L|Nz,8UJADpMI-B"Wɱ1Ify[hb !{Dݩc{HPxM;.x 1 T <,fD)F0z<)#]4oto;&V԰T2??mo ^-au=܀HiPYj+[Hade-^mzSF{=_6&SWfuM0)Eؔ_^af%".#C)0,2푬mkO# "RsBrDtu|໵Fh4F}}FsgS3|vc14?wm$b|P!mdõcsl'=1ͬr'S[^U5]h4 }L"$%F\hQcB7LKB FBrߊZ|nx&RJJef'8=֝k, к}0!>l7#Vu&V@u~j Ku+3c *=]ZC'Uzx,~_I f^Xv3{Ɏe°R'˰RoO>"E'KO1e[187BWܒm+妠rQ\L&e3dXhrb/g;9N'xyX2 ~}U,'`y3v'w40<$ҧ?{/+J ;!'a[[;g1ϡԽtc3p3Yjyc ;w7 R}}A>^U(5ާM4FӘmӱZCNDo^hd=~o?eA?B9bӰT- X) r[?iOQBSB=]_JA}A€3oFDnAa!Z:_376EܚrDzrx%Gdz9%$Q>Qn8Y`I|S9UP9 |t hwOZO93ʼn;8B+U|z#̹]8s;CRrVVbl׶m"7-꟪>){ԙ&xeL>ӈ8فmۼu}ꞿž=MTK*"CND\1B8*Uq*H.|jYSc1R OИXbpT7cG(iy+`0*BaN'NvùiRJ/=Wzg E1[v?JLn pMG=nfsLDzdϻ}H>H@ }}A*+`玆5FofVR(<%R!Paܷ U|mi5c_*)QNn0 MyQlśH>W?U>L7<YW!mj FVB^B#8{390Q\d.(NAj|) tIR^Ƣo Ɯ[颻E73Zayp̉L.,)d RBr$bq6QJ9{Y- sd7~/+VxdC/r5|EeZB5ha` [8|+nU4oJ|>g)%izeUڒz!\K޶n|ݘi1kckTslgZ;OnozƖ\ T>ɎDׯNwrr7B˖;Q{=ED-|ے[ea^oDY+̉N̗9yy\xh8j^kj_oRLGZlSҊaiL.]E@ϵ8s00 ~{/tqx>d{Ss}d;Ӥ:LvnJ/WU% `>psk6-߯MWmz/qh!ycROFeAv!Sh4J!ۉ͝Ф+|1I,%O#JbGAd}dO)aS\7:yC3cvMQ-j_;ٳp}tqJ$S10RC8?%B8/pB8m2Ƙ-!|2#돊[UAp !(Ԗ>?*R9\$&ZbKa[w0%)”{HĢ~[N[mZJJ mb'`ӣ\ؕ@ -0p3L X`&.e O>HLo#@8K(xWU~]5~ά"͏b 9_budU6T3~QRj*zXx8|ЋBiQW)0 ' ]^YEu KƮݍw+mQz>C<- !9q|RrA그9 IDATեYl2͹2p3V rdRNbcf _[ͺ\Wz-Hcu!8q];NΛ_1p#̓0MoR!ߋ)%rH?}\d@nzuGQKJү'x="7!۹^+Mzˤz% 'MFw yޞئeo~VWT=tN\0 <%D_V >Qf"\TTKb )DlIF[QAƋ󐙛k ;:xêK&#XGm`$W5-Kd8:_q!y=p7ŅT)x G(.qNOD#o*rDd<8=]x=ŕ 3=NtcVV3gxSC:nM8J*8wžD, sɹ v/چe˖H-w%r -m5IO-֢KQ6O^?hʛY)aGJm km·O;-XCs-|o>_o\q~r~Q0) }dN餿?DeeS |fkB~q;-)%hGJ<o#|dG2qVw}kP|%k!㜘_͹Qs_b(8TTZvno`wjXVniNK}l}zDězgauJs& {{ȡ~R^N,exƶE%Ks4o`L Wȵ]=mh4f>SvSu36N :ȋW _*dv{*G)E䖍r p Fˆ-)+5(/icuB1+򌨔BsA{/sVXY+a^m!0qi,VQ~y30z9t59ہlvH}i<nfd ι!WZ¾cPa <qKObp'~0!y6YR'Ȭs"eƍpA"=1jK9GJ̨\{睈cbeZҖ\B̾=Y^ϕ^V, @F%2*.$p-`ӆ?-!Xa8&j+9@*`[6y*3)/!܂ל~E>X7TcfryaRT,˱gp0@!5ׂTiKR@c-fs͹`tAuÎx8bcڱwsDh% IpEuuKnC5/tqBM!jXΧL#Gq6꟪?bZ"k}$},փk{Th4|e65#R% /Kl},UJ,5!p9#K|Fb0h[foR{YVNQ%VTsr+'lOXՁr{.]U<ִ u?-|OtﴱE<(/͝l\8"qnn KKa}(THNkwR-?eL10hnJSpҕ^r<\@LcNdIߵ`2)qlSI y1$˲{fB*\*ee+Hj}|lKpx;;4yS-ܱT&jKFc:mYΉp V,_jaЉ\g=wO#W+⑻2*9qKw3hi[4xz>K a29 4{E"x t\9\mLt8geo4b^Uy6mr641h۔36V}i"5լ uK'},brmOtOl?5F-[If>6[;=?8_h i٘˯u~mxnPCh"Ϯ!XcT` I,*Cfo5Vs(OOzm +x3^"wo5g`__3?5xTH,GdpD&T1K[XȊIAs̪'2K$$#&[Ch"rcuqi8Ӄ0PN$$| z8\ =V.]ΊpxTo,L,<~7.wsxv7zUG_oKDm|e@Mt WT8 )%~O5 N[s/wcGNdy*Qy-gڎc /& %D_eGy'jܙN"ˉ׉=Ru+K!SM^v{Th4|e6m%5?^uoOQl)$\b%@z>n (xXaۦxd bD)\9Y]bAEL:. .;Ȥ99!LL &/swG J#d*glض[KSkcI )%^%zP A٩,&o}3YC28h#pDϭB`#_4_ɖ %شMj8|cos/3hk[eww9oMgsn`[ZbPwtw.R8hoMG:i"ItTهd;-2Y@r3hb\2)r>Fv&x'HkLLW\OvNwMBP(sod'5b3&>ּ'֗A:?2m{& MgsԲf K]Ter/e%. C>?zyNo<\'~&xܗ=g՘\w Ŝbvl.D%e"6"G>ҲCtM7oɋ&=TBrSC` 2'l-"r!"&j2a4nͲΎD!%F^GF1YW[",qtxVVVߟѷzvI_+WUXe= m_l ;މOzΛgg&_ۈoȠ"'1MHٟhob\I^;!fg1H3hN>g8gV!22$9vomNM~Ěl 911DVN#(K)3>XYY1U"|~wĜli<| %yݶDTz}s5m BP\.Lx{`p%^>Zgŷ*m:-p)G6:-*`%t-LakD")[SRc,bb]G8%%.+9(`ro +< g l+kO(+LX elvce ~"iAJɰ< A0{e;vneJbͽ MZ^!X7Z{)95|:)x ֺZ6VY-)+,۶6r ulX_b3}lX?Ql۰WmzUn )%wiKr8mmԛV 6Yp Nfbo>?ipڹMwnmUȄf/|=&rYWLv:;~!GtM67שn}-2d E:C@liU:xlhФPGʷ j#חx8wvSoRvc8911_eÝiǎtfxﴶ[_9bʫm{1]M 3VM/yl`e~~Շϼ;D{q\``p')YIz.!득n{綝kT_G B#|yo-W \=ٽiS(q_?PH ߩ_8燚Ek[L.si39[rdVEKcò56+:Kԏ\ŇٲͶv3~ 2"jg%'#QԘJN*g=Ir\JL)Уz ',ClHі9] XYY=V6_|k` QdZ,3n[m)c{':1GL4McY'l[FR1М7ыЋ=t BJ<^~5&cogߋl۱)orF /,}[ؽMBPcG:XV^XMRn僭<zڊ{),ضu{l(/7R+YkjXw[ lnL믵sWӄz:nsb:c)++}u69)Bu^{y:w>Kq\S9bs-H)uUUFqB1d+i(Ul~ӭ+U` rlHڦeyqy5i)a4>͑^72BPL%ǎwrX^3+cG,kMgy BP(Re88;f "WP 3-$vm Zu&I֍&! ŕ G2dB1Ք,Pأ-j6BP(S5./r|y^ 8\9P0g Cg 5L ^3CU\?k5/t54mXa#_-o=23KicJd4,eP|ؙegd(+2JHV(fݻؽi1#,[f9ƕP(˓ooX_KhJοe_wd/4g/8ZRv+G\kO=hcݼz{]k{ҠC7r|U= lv}.YgbJA6 \шrUTTP\̖Eo7k49'8D8c\Vr} A~ЩkKMqpEV6 BP( BTTQu5>zKUsL <%Xe%+ BP( ERjm&oipO' xdv*߬Zq\WT …\ˋ84HBX(O8`.2 o%ջawC3իдQQ)ǫlsY <%BrB EB)6n 9P( BP(LxwMK+C_UJ -֊|eF+/?ULUc+ٰh&'A.MP@#/' B2*C kK!F.1) \/rӭЄ@ o4UU\JiM "dW7-|}DJi}fi>gC;&f;ɝ%(_EE]Փ-c&T"׺㙊/y0SǘBP(ғxo"<|b2ҲFLͼV4%ɒ?+"_j棽~o''%/ܥR/Ýϵ}m4|x;#mOLۏv- Ho=Ho86jj[ y Om!]qy0Ufa PX4`M pvr2cr-?\'ϛ5Nd#qgNyy6W*\l\ϒҌ'Sh_hȺ""Q IDATZݥS13_)uwDڂ%K=]aps *kԠċWw)SqY0pDWY#lˠ Y 7Rrl 3Elií {Ȝf-NecI)'DCPXemef[0p1jL7$ urE4u9aXNȗ/xb.l^QBBP.˽^̙%n漽Bq9Ӵ t_<;oVK]+@=ʥWWuMl,<5ܓ t5U 5P"1sU\̆ͅDDe]3@P"CG& ̌¨ew1'(O?r|)J|,!ޜ;c ~0x B2p|>?;w5;GkkxxkD;w5~][?фg&vifASrE;wlG&A;8P pPc_ݎZz;hw믶mvN˲^^ߚP?PnLrYO4ňCxc&rEmDsj<׏tp;1"9k{_"u1~xeVZgH|WL#UMK4OfU9-ظ\C^wM\*=ue^nHM|CPP\P dX֫Ŕ`}ɡ]1Fu>-;o `i|`-1nQ[q.XTor9* `IY#RZziT}k8;a]愽'&ۓmej{X&aNZ+z$}ݞd񌔽9pARl ۸eӾL縝ELwڗs)%]3%ƨD886M ϷlP( Efro;G`yY !rOT(.#r}ܴ_lp3@/,onmԲ˛S*r9.,oP=7ZxlmniGgD^pv'׵CI^\JH- \2fdDU8 |#dr2V H/7i}/@bN"zuՔac=[j8sW^mc4y)]icVxivν;ȱ# `m NV`]J^9ƺ<щ~ ~ecc!ɶ-0=Bt9 2( Ҋ1(_((h 5`bMv|EyR $[hhNij9}IY)4᷒u+#9%, 2FL!IϤvxlp_R$$P {+ TH)yF6mn}-ȱ`CLpvAɭwpzn}-2hMh AW&0M}/bfLP >_b3Ϸ CY%KZd]P c_NڤܱsHŷkr&B*jXa2r)aeڥV٫Kw%V񁤮Rcu >CRgrOve7'#wT!=&O&=%?e =XHLd4A1`u]T\=_4ISǥ[E=G ե_ޱalb߇2$'6%Vh"A|hzΛpoS9[=S,9om" z}%%.ר[%K,$Nzmҝ&lOJ{~}> wbI8gMe{61L`k Be6 ŕUn{O3Vjƥf,_fmҝIA1Ln…XW~ۅ9V nݩV{G]gs;OJ Pk̰`i}%a 7mdd~(>Zp@pVrԠ st=KxuN#*b2`֭}d;:vN2mm|e7xB`,PBKhY+4 |?z[~ua1r*,Qˮ\dd2tF\`uix7g^>0$)V׏CraZ{?\fu\UxD(_Uh]cBHpE㷶T4=`ͫ4oA0 Zl]C% V/e!I*KmːEy;fFJ1,3-uD|c#&C]5ߑ iô,FډtVy!5pArkP2.>HuDԾ4"㱟>{E+s0C4.t Ja6 ŕFV?U|~?&$ )}K 5[;ͼbCMԄ}ѮZ^Mw"vzlb%bDLi 9xJ!rrj{lbTO/>ݒLrUOX]VVʩ79+/FVVV-a]SAHQ2_:}%'ߑ=nRPf-e|K,IH<\NofΗVيcĞ:6i>LM,\rދi: kh|샴DvSft?Ziԕk큀C8E!  ÊP(fBi2_Σ M3,7h0by˚o{ _^C-m l<6u͜k[^xv27IxWyc 0 B:Co[ ܚ 0"ZHR &KG| `egpA ?\VeANvu'K1a=/l 0 {&y?2hد$7$f&'[Sú55<}/r+*rnCʺ\ f _b4TzxsD&_N\8+C9-kx"ˊn2kzHb7a&Ym3<6r-5\UAfdp c`a Fi =9ݬ[[ZW^mv]N_A#w!k"IN kq] `pM^UWmzUn )%wiK^m;w51 ZFCeR]R.K99`2u̚0%Ou/kֹuD':L|K4'/h2I ?lPXcI|1hk YWY9/1F`I]Mz7.Rk2kRH ׈d3cIvd؉iLهvaeV[;ǃX a2b͚$IzHt$ۚ-xfӾL8؋_kA{\׬S%]RSM?Hl{* bBq%S>`:@Pf6olb'VeKϔQkJ>]$X9't@aA9C_"2L,2+9:`H.Izusu@+Z!p\l,4008Ȳy7}aYMԳ{W3;М᭍yW8{W366 zMǎt"ru]-K@aFR1М7ыX,Y\r彯"UL))q *^teKhCϻ(ߨOLCHT_khR-:&]&@FP(1969vZu`m:em\ͫ5#s@(/Ѩ\${<-MmeACxL5Vq} ;1Mׇvs&o6]g'${<؍.XIĔ᾿VN3[:- xf۾t8؋_coܗs5'[h=/^hz8 BZP /lp[-i2vTxsgS8v/ѺɮSB1 %|^2!dB(yvr2!,7"N./(v"CaY[^7"XQo ɀ!1H -xЖ!ޢ b256ޘIB1 lXǩdgoCP(.oK@ yE B]G~6,4[cZpFsˎnʟGD-ǔɦi)YA+R?xf[H "0dHJ~Ë?]X\r aȔ ፇk aA9DRAY,f-` ]Re`Ȕ 뗉p@&XT1sʐ5ޫ%CYWP( BP(1Lx;j85 i|?8MX3T>%8slA{jߪS\é7{xC,d2yӍ_ϐ$dN˂%(N\)35?^jW Ս֔Vq;;_~e?~`KLw<oo/*vhf*?n װ43>=.&49\`AKKg%('}{[2>|X|{zɔsz;ZmaIZ#'69P,KwR"'\{jTe?e+^kbv 0wl."xF zU!qBqIR(csiX«xud4$XǓs ~Mh 6.ҐNrm1%ֿYcBP( Bț?I HLoAFSxKg4$XyB0:g,<иfN;ex)tiKƍ Ãܴ^PGG\:ډ!q)#gI" w5cj((\fI?ׄ@pƤdTPLs"[=(7ӝVWki(r@ eWYvr(9HCwjeP( BP(#vJi1 c&c!c2D“??Y[5O}.bSq>* IDATBrenu^q$O^9qgsOw߈sȩsr=o~Eϸ'{2ji%e `hDb 3SVrvD sӄ5)޼p"(e.{<̷2H.<Q±p@|)-[ H} &7]ԙ<#hYRV BP( B1%<g"KD3 e3<"!?aAwnC,[9%B >Q !s8Bc A?gae~EUtF-cUF`ΧGߘ|TCR <ڿ6eUYa}-FJd${A59>ARpv^οkjzI-{z%߸gZP'_Vk|yk3ZޱNrJVL!5T MXºiZ6~܆KsI&0飼ë#(+GD1ioDW+ Ȅ ijM"SBy@}\@P@8u|e^~[?gK4y͌jvom۽hN-3D+SJ sZ>SL D@!t-xɇo;t{&Nt'/.<~ȠZr${gMs :/k?cL *5݂w%{;9`JP#&?x{x姼Q=_ý@cRy|`8gxh~=&#L1fJLK5$8܉O/܏y]QEs;eyP {zMUn>^3TQIfbqPZv?h4K;C ⿕|wvz^tZz\4onfϚ)ͭ Es"B$kx((0#h2 $G18 Lݥ"!&`"+K'`a8='( zk9vx58m2pI$C氄x[ cv=`=H᎘W[y६ls76s)vI|&ÄJakOo;T5lhO|_A. ѣr`&=&]PdKL.`O? JK Hlb|_=oջae`K``Z6G9NdP{oSǗݻXS[y9 ;;ȹ>¾Z9׋^deM^odW7C!XrC)}u;k]v{[UWVPVZ# xf΄-7{Dl{w>ф_mcێze+g&vifASrE;wlG&󝻚h?.LZCPö-{cYGgVRZ[&=<^U{9וy@@9LCs&G`IYTV7K=ic֠(?UnkV([.(_`끑Xw`WS6ިq6Ti^79腂uQg AʴO1'bhN)@ٵVF [1jἉCķguխbdmQ!+;duH)(1F%%űqlaǗkI[FGٟ+Hvv{sJ^9ƺ<cGal{t;CC6]{k;E?66lvlܻ;ږCCnͩz4Ʊl_jeeeEu|ecc!ɶ-!]Ϗ"mlm6k#wAP VLxABAGcȀ3h֏e5S0/|إZ0#enl_@uDJ*:Md]p%D#"M#&ːgRxɞ֭ 'L:n-W:nP^Q.%,)ya]Ok:aȐaJ_}3~+.;hECH 6%o H|K4X0{\="F|J~#UjH2agDyA VX8gcc{nЈg@>o2}1H&\+&ìYpgR֨.L0y̱ɦF%66a edd}ج#5u2ݾfޘtזeLt1I'^S(s\w<=56K~33=H^AY}6qĶ&+hCONb?W˛ y&峕6֭Makʃ4QXO,BȆ.$%LNI|j% A1|c!)1d8()%u'%jGQiӦ ɡ:ßLBa ɮvm32q¾ĕ%)3{k@o Kn+N3B RJ^vm妵5ĩuRa <ԭM>]l]2`ד!CVZE֛}>kC!"KzXj9\cRեVcϻu^롏񁤮RcuiX3$e}u-*d^~2mD+ |Htl7n CPwy 1vDh?m"C$;'cT!=&O{?{uo|3JQ .Yِ%n%ؤ  2`R#4D VMKjA 6RԘ 5PZ 1&ENV?fW^@ivv9̙9ρ5~+q+zɢ[g9h =l (VG:#|}tڔʏ(`ڑ([gF6t%DUVML/zC1fcGeZop3j۠#XEdeEA) 鵒nTdښI}#IΩx~*eߞ&Q 3k4tok43SJ_ia7I 2U|i_Wv+l;yvVSkqU̅@ ~4,'|y?Su5a]]J %s|P ŠRQ$rԟPڋ#%mE\(2mv\k>W?sྛw +(-0sG99 ;'YPcOd9I8T\)WBjNI A2s)XQ+t휯So( CfEc#}@ x"\w,~G*|vtl`&93QvJ2bIɨ:jkAvNշ$"=M:.ڋA/3&t1HF3LF3l}jd55:;9U ̜J^Q(ٝ7{+N~&vRer}\ɏjxoNghc؄of+QJ1f!fS`M;ql`2^d%I<d\;3΀o*M ftI!s΍l9 J *|]]lyڎ0lk=5iqR6U=^Du2q3TI,'Z9yju#c vgF -{2鸨8~V3OV#a"a ")wm 2|I"$"k fڦ4"]WG,,_ z.NCMg%ݽ%!P433?$8H6>ZI]FE6kŕoOe_@d"#n5g5JF~? rz{"f]Lnf)$/g^5'lc5ٻAAAa1= Tpxl' _ƋJLnSkmB,ʧ&9q4oCTVQ,-hC4ebËeC@BT$tKH;?`(s#8D5-7׮T|oGfg"R|g8Vg|VEGg'+bCč|b]en;?I%80F'A< T(OG,`tdnc!0rsKk2iS:- PxG2s+#SJx{q?Ohi - bH>qijD!]&ZI0Yt휪oW=MbQBZLx[nd$H) oN>F;3b(ڼC/Do˾/*U3y&k4Y29eN{pP(%O1k0q2g!l9i'P>!,Db{0"BCdJ ԅq#@a+'J}}jyܰ=SNw<pv#@ (>kxT|=a4׎XsV(4II3}iY)G6;ׅg?$.]^ʑS[(8r4HxzKxfTubڊ ]R~;+m/9:n\ю=>E%E|;Q ѩ0m(#-&N _O E[e6k;oBagM<.AG7ޔwKv.sTR~3P6$xO3h Խi/pZ~ 7J6 k%/$''9UϏv$M}g[%Oyh.yL72UwsLO+YL-,(A|֠D `T9& H`lĭ ~q]pOB&T#L`77yqo w_YUAl)%'OekuTd-i8Ėxj$ bf\NLӤBf޹wɦ;+XG7Ws;Wr:/.;:ٴ6%nvn[eӆJDVz.x S<ġ{XˉRW0 xY|̶M׏qǮva⓰qM"Fgg( IDAT` }t_Rt\PyŢ,xYR2WP` C@aHܨ{Sȅ J`M=O4Z\ddhB;AgAbdu J% -붔Wt[mnƲ) g _b c >`)E2!Vctבsebt۔ׄLEǠ+nö΄8!/\+Lx; @%Q_LÎ=y^698y^{9JKdC&kT}Kџ|P8.ᔤ-kWF {ۦthwS-*t,8!/[`rG6{]̘t"jv9<ۮdE3m&0J*_@~rꛮS-X2W$;2MȢG;bGbF3] mfͺڡ;zRg񶯋 xŦ{*t4iܲ| ii7sn©J%EEB 席ʡp@bjcsjs5EKN,xB)ؾ66݅f|~ۺ-@\ R\F3UI*Wgh4cg.+v+V "RT#SAydqL6.3ظlj|[u4TdcF-piR&|Ӛ'L&:|s@Ql @!t!P9"Xh|+] G89 V VCo)ޗ [ Y % HP Tёʎ7f󈟙BdԶFh4Fhb\(/[.盬s Jg}>^>P0\@G&꼴_3ONyiuL_f Lii7\sdeT0CX~` G; %";)+x%|u;Fh4F 9,>&yP_`В|i2+ D%coF3ҌF3eZH ! iqI=CȃA YFTW-JّFh4Fx9+b0x;)t:)(qscM٩0% h4S-&$,*r&yuYu`BS;aYPmޗ>#=TNr@N87^Dom_AAɐj4Fh4M2e=|L0x'>?*`rn/~Kbp@fg\sO5W=SVLwr랑D9 |'7iC9+X8 ^fn!]U4Zpz^[ٲ_=*|{lm|U| :W[x\EAc  5!dC#{~#R|ybSh4Fh`~9 {tc;ܰ<~`C )~|'.="!PYϏ˼@.%7+nBٚ10e>?bW<~׳9l~Ǿۉ 8X5-fMn W26~pe|֩$'_i.!{R)AB*~A{@ٳ0!yHً]L,!L}h4Fh4xe=x)?ŪE@hA >q2+VAKr'jHNE^>z?92E:ʳ)T|J Q򧪙94]b'i7`+/ͽiUsVhg1?]Wdwn~W}_񁟶\,PǔG(+qo񮯕v!k?kb{T~3;D˞Iyhߜ߶&jSW{j&wa;*%4`!?"JؿKe03SLrxfOɝ446?&N"\MݛsÏ8tUG;%'IeDMcrI3>ϳΔpwۯh4Ӆg! \_dbN^!%¤= SM~yCA?7$<7NNX7kD3^ݖznyJV͊wrh'~ n>J6.sF ߴ/w<tI4U@z2gMTp5|m89=eo%[¿ײ.%eEq Io2 n{}rfPb+fG>OU .a)*ﻝ9aȏ5ѕN/VR|RaV D0Q,,+Eԣ,z\`m}D܉V^{{|yfqrl DlkȡZ6/rk$D+a=bȤ^שּׂ|j^JV| nWΜm|@h[9Rtm=*ɖfTjy%TiO=)6Wmӝl|0$hOfD">r;.JyCЯwAl27Ml7{jx6yɢWQHۧh9$o7xh4A8`O!@9@A:@rM'XX$t @v[ T䘘u뜘, A?-ʙ<`P8+"&f17yc줙\s+)}w_}g=ob^mG'?V6 kwv7O*y(Nʈ.r<^va#8^3ysi+:uٿ|<]C5~pyb_U$%'g?R"[FYg{ku#1*54EyE? ws[L58YroUD: Kl vt1U&h 1 N__ |wO~KaI(qCr%6ƛd~H1Ulʕ-#erb{24I/Mu4{hX6Y>_6.3XQdqE)|MFsux{H1gTˊz?b \p5ɛ$ x V(v~ɐ ZʲcyIާv-|.4%=T3Vlj+9RJ ɷ4#߽z2t^nZJoxv5[xqUƁ&/Jd~m)W7!;Ư67ɤ^ t 8YX4{MWnR#n}YL64m=~|n'VP/!Ʒ}>+in]M04 C2/oiΘ)F@]kq"'B3رk' s$ ώ];>Gm+ز` m{F ;:Rb&Ko.Ǫ9r=^sK"^;܄VzCpQe9N[w_gxLB=_þj9юok̳yp}y6էk b\:2Ÿh˶9lw.Iݛ@r^%뗌DYRp.[)0r%ظ@A]u&hL˱.+\X10ra1᭶oׇ:yW(}S:vl{Oq!gT?p;}D6Kէht9ڍ<%}'7bkv*F~۸̠͑:ɠ/9r֞n D?\X+U16#>̱}jmԐu+^ 0D"Yyv޾fׯ00s-}/So|)GAuTZ,`0-'W=`$2hä~Eq|ܫb43w'>rCl,t!)d1P4P'!?oc×= 0hWG1X{[:Ig*",SXJqD~䅫1kj>"T̏W 뗭P.dQl5_T*yqG{4qy<:F&6ҞbJIl@'3lj m+(gE6:$7Jlf3Zib֐E$h!Y3ݹcc%?NRO|k*G ~鶅蕫4jk70BӭCph3Ko.eӝK{捣I .Ԁ'H*&vw@rFr4K@Ğg %De RE*薔>o{OQ 3l@$\XHt-()4(q /,Tlm(Y` =,x"EOml_e!{.+:t oIz)VzG|G/#Ĩx>[!@>HE:mJǏP 5STML/%y $\A1,Hf%; Jz%XdO.cid;q/Ofx34f:x{bNߓgJ8D,rb L$}B73,uץ3&%`mhiFQD)`Ne:##iE")bMhUyG!?hRzL?h)%ok;S_LFyc7ְ=ڀDK~|q. QG_ܳmAR I, D.g9 H)Τ^r8GdС&vXC ꐊ93oeio.@)ePN)6^t1Xc5|5#"f7V y@D JI~_!#g>`o?P1yn\ేܸ>UISrup(K{a9<ֲη;;:7HJuxڍF3Y45+QfACcscr9)YѳMwV8:+$PIo}ݗlAp' X sEHZ8;v_3'Z1r9c&nmfߵ 1!XHJ"`'0 B5eA{s}P8dڦǻ(#%R WQKgTXrGFo_KPIi="|xLWIfdb$}%KN_靏-]@fF3x;G?׆~`̲EYW4h  ٸnDт]<>oKşnn.iAl|-j7WzM5/62͡;)t5_ng֍'vm.&]E,Mn BEnͥ\[DV'xK?xe;&Pw] ³{}]p8+9-!IieXVKba;eg`gfv:E@ :ͅfS0*NiOky`s5e./M)9~؍| IDAT^B8oVǁ1ƨn<_ Gl^mb!97 < "¨ tt+ Gݮ 9X6Av3F (Bȁ8;cQ)07lሸQ("]̺.v[l]@%.6%=^<]Pl/bﱑ/%9ȄTp`JvhXp}qC]i7پ䆬Ll=d6u{,1 EO`$Hdr9-&繇+NsqD+[Z9r=>\ҲR^_TtO.]4IiY؆*ȸFD;+0_HtKt^r^$U_HmtŸey-T} $wjQ#ǚ&ι "o{-PwtƊv.d5PAɷ3cwƦK gyTIOSt}UPỠ( #v9ܴ*M!&i٣mv>O"hx\BgDx9;9a2A&JMid]G3P6$E&:c +F VA{Ӟ tl_LErO՗osn}y2(t 6.|h4ok4Ô}M Ori]ܾ+9qρ&g _cF{+)IWOuoe syWRMXH2Q/ɓg$1Kˠ7zpU<\W&ohFLw +Y03?ʚ8c*V./rՆ& `aZصMwVrU9i !xp3\ǔkVWpr5e6}IáT qǮva\^>F+ߔ, IF`}t_Rt\Pyţ,xYR2WP`؂ C@a(zMI{ #J.SZ*v5OX[H&|NEd2Bhf"zL3#2ybL@Ϳ%zWWj*&`l`qI1j>gǮ~MwUq3,(ⵣ<}_[CAɫߪ̷_ymZn` `*6m9&JJvcYcp9hxKr'YZ$̋.;{ۦthWR(ԵIBўqeC2_C:!@\S׆$GH]̘ܷWӱ}~1^?U_ͩD6 sLOF3mff]kH)xE GzoJYZߨLyoeB/͖`K]:SOsVsNRe"h^=-Ӟns<7Ɖh4W/uoJZ|[W* jѭ8JW媥]rfj'jvNL/6h4S+K)yŦ{*ǧRznYUZc5*-!A1V;,-+{ooii.4!irS >uk=FQ包PỤ̈̌m$jVd3҂FF3yh19 ihD.L&zp^m;/i+<FH>@bjc ugWi!YLCp>畮Uɑ ٯp/FLx[LzڍFɆbR8oySॹJ~NFL+x[<hFh4Fь?zLZLh&=Fh4Fh4G5Ah&tOR4Fh4F|]e ?@1(%怢 BBrDBU^3sbF3Iuܱ''fFh4FLM|Wx;mB"GiP2 JJ٦$r$+卿) UÕP)+n['>Aӌ䡣|>W^jgzƶSD̚;[yc{lkRY\UΎǶon7>\;j چCh4Fh&9 9J=KJ)tQ-ҲRg"&hYBxC97\p0/(R(YצMCtه r+oį *ݘ-Vͅ^N>VoI+1o nwYڶf~3;eO<UcW[;ٷ X\2y^]6{{v7[ZY\VT礷d[;nNAjMDGx@;~ 7[n wrh'~ n>J6.sFoe9Ϟkpo>Q:gߪCrտ'.{K=_U=_Sy)ʏiԳp;>țWTQ?xv?7cwKfI&F ^׻חT}~d+-UmM?N% [z) 4Unc-[c;+ B>9)kՊi}O&,7Xς:h4Fh4bK.V,|:x53dВ\RekO :w;ܴA&[Z6mPfE|Q?Qz+ͥ\:Sñy18T[7%\9J~⇼ /O|K8ю,nD=ӮO,3 zqP)ŤcB'$րF4s&mk_P~ytWn~~38|9\c2ʹ)9TKz^W>,Q#u ]'Z9JY=ܟX~_k#>k/sv/㶿9q06da{U?Γ-4O/JD7xϕ:Bũz?*5E^Ŀ=H^mlwVR:~wCU}Ǐq]]vxuQP`4ެ_e:o_NSΔ| />_th4Fh4X۳ A/rpy1e*@ {- ,p@O7yN8y`,rrOorP-YJUSr>";yvȘġ&|ޑfw#s5CEX>Du܅u >@b1B7cyM"*z:|jg4-eҵ.f+ѻӌ#G"H7!!Um/Sا@8/Ź-̟{lW?gwJ8Etu1edžb>^3}ffVmwqO0XWn^ pmi-^kiNlvnA٠KD+o(JSEOԦKEMkT+V V [A${ =ɶ ;=P3f?6}7 WG!g>Ov7|DHs27j"|rO%κ YD%W6 ,&Jone7J $B4,(:9Gu<Uم؆s4?x%nKs9jB!B!`4,2TNE,Who?A ^#]Ur'MZLHד?98™˼(м&Oxit:tܛH^f_wAہ3Ll^-l !SN8NQv LN=\ GQ(޺(9U&Do3'U53[c 'dcAκ^.?zk6>QyY) '7Ur\̬4Z?S,y 8jLܥ*4~Tg^kk6K^S'vLF8~=<*u|T[j>/;ߢ CA(Y ~y6Uxdx^dN頉}] 9/Z(((*l?4>dh`wO07DH "4vB1Is1?\?7yh5@],4>+5=D(\ă;σ;!?i B43 ≮HM " 4Pǻ)kc2s8xcNv ;8KGs[95[M{$>zCfWknR8C|o5gwG8%B!Bq,Kz{@1ݶ~CvNȜdb\,3A>j:s-ǒ!=\?06pIc-jei2XuwzէC)2D6ֈ :TenjLN9PO(YXoޥ34Yzug1 -U*`bjn} BUu}?}9όp2=îls˳|xm &6T|CwN/ѧ+T0fCEo"yG~=ɟs'նm6ț$/4ꦷ#4 ~&3Sb]\}6Xa U gzP~jw;ǫ1ݫ?B!B!24IşomZři9шIi.P)Oem68`pTiOUlE;KTӴ7W0BNP2=j@C<8]EnIܪr\' [ h-~}A8"πܞ8Yőđ6`rzFk#a D_JOֶm{{ z[W{wRAYV0l'̔UIϠWο=O1dSr4KE pr/W Q„QykH->\k3*pl'^fϩk|lZ^;?fǭ弖esM#|lY|3`Y~9| !*xS\Tt HYSƭaFWr_0պt^Ry"Do)gAs'N?i婽6`^];o{-w,a-Uܰ*h7ˇ7.[Ұ+Bjcr..s˸0ѽ^C<Ij7Zh Kxp}ƈIi5zRX F1tH;to\7g9{F9qwt2-ɔ ƚ*)fD+6P겉6o td7Ks:{yS,X䣠K|rJb :SX`=7T:Q:Pwώo$Iz !B!G҈J5jz<(BPEoT <7W.QN'*ۯZv`wμy? oqaޘ7E㋹͒~r20n*g!>UP.|R쓯K+6O僺v^;τx\rIɷ`_QF;J0Fx7* WVW2X2px/&yUjܿ^IؗWWi'/)UYF`jܮmOaٷu w~aU ;L^N(g^e RQZ!ۑT$gPÝ 530?S\8!ٶ# z*yj/Ugs@lOp>s{;Xxo;&vpar32Esg`w瞖)9!Pw>O 6Hg0inw֤1Is$ cJ vGm;$:sVd:,vo}NwqD7n`~O%tP@%> b;+sC.;`]mjhhm{~\B4\NLϓlflG yfD !B!X^o !64ɳJw^&zlU9hHQhbPcMLMoG/eYdks=s+ɝUλʱ,m-innXQʾ=rm>uiF'fu%It*X~M{.B!Xs"]o qK:<0B'c7*J*FJAe|6rJUWwȟ  gyP-cW[,5KERytu3=h!B!ĉDX7ɶmjYdf(i)va[VNacfqVeY˝Mpb&u/?;ڲe%MZ.+ft7YF!B!ǎumYMyY"QQmk`Y(E# 0 qnl{?79@|/w@Uc_voOukXP% hW_wB!Bn „焘_lCU鹄8*kR6eQ1нoavEHWtMgR{,!>䱛85CEP0k6+Ҵ7ү\W\=^NUl ڰd%yW9_Ax: !B!ǟ~Z?=k1s˸19Ǜq::-JW^3>WmR0ݍilk?5h9Zw^6i =pB!B!_cum0b><84NxejoK׌G?nh~SK/fͳ×=.grylw :@Z9s*"|'5\7MwyV'NRHW4l/R3Z@Ǔַ\ G8&s'`9;lS47pL,Fuǎוql-'_YͫWV DAQ 6 wdCXAQl>AnO|Gyn^M-w^ 7i+WlÕ4McgCJl&պ^i)4McwU5R4KRɟɧxy/ю?B!Bi6۶bCUM-v6)YJziOGXߌQӏTh-bN熹8`*<-5`r{߀1@>>8?/B e}~P$fy4'FC=x)^zeq|vr\]u?>vM uB!B!_cŌ?x? znLBfn y A } {hʤnvưqHGG!c,Cz6n>`6Q ɗYx%knK-oh2/5lm+Yym1 \_M]XNp=]B?o?w./YI߬)ava)# <(}mpǍ;r~'~.N- ?ŏGDƢ?K17Xc67ɟ2+ !B!8vA.P!^-3{рv;!aC uv*k:yqs9fwfoV+WyI{->ϖ/Z'OͿY 5rS+7nSÇMlo~Sc*5l+^Z'/m#BܬšIikukUBc݋^BzHS }0@ꍏφqoX0oJ'Vo}Rm0Jnx7݃'#MW\~J ' O"ÛDjnq%f+kħu^ &XRiAf5A"4&K]bAȜ!K:;iĝ jDpdvk좚ty=[Bk&j+ru7"V>cҿRWsϵ]q$]gk5SXrIotg)/ $60Hr]T[ߞD!d_E+0]B~~ՍMzӝ393CKa;(@kPzVzXzihż:S![`_A\ct՟X6GLA sMg) H[B=g<A˂s!ǻ<33j;gDd:;"DPAS4f6vFς!,?_{87IJx"r9WP8{Z"rO5X՟H?̌>CDp "[j\q_3)K}8':]皴*ñCE؛yZvfɛTJ>NjхKӠ`Y)Ĵ#ؖcgys=B7Sk@`XDe,PJy+l ;kq^ޓ )1㌧8gߠcxWmh X?e}<\ -.! #Kpz)EVLk?TjPłA,FBfdnC‹7gؑEb6l d~ٓt7A'L=4Ӡn'N\h1KѱCm9IfvdQ[J[օK xNFs`vbY1m;t(( 8[UXM*;"lqx4z?}F&bJ6:JG*0)=YnTVʩS}nT3Mut%(p'oiҴ7B5Hm{K{jU7pyQ?)Nsrm~ >%6ܳdzJmF4ǃAcoq46pL!B!IvS$‡J^mpV}(a%l~Ʀ^fqiw 73.yRͯxq]R6@|3orMzNgy8{z%wț?qEh;1Kf84 8#4}{jST?SJYpI5,?SJLU,*hxO﫱N)+HxZE~ԃԿQBc42s#?_S[ȏeY4l 3marV$4N5df䞥d(l;A';ם(η%&N\ѽv!nuّ+B!B1Ba)pNk Y~m˲k{ek=<DZpt?>T5kjNͿx lǃ'^^}9e~AW:oG([$ƍ6xKia[Lw $HB!B!H nV`܃QfLw_X^Ca $J'yض,Gsq>!Ѯ.+B!BFu7RR"z'mZmr |n82YeevB!B1vz[FqM5KaQUZdm,I 9ζm|^)AQ gzXP9nB!B\o'Kc7,&fָ!>-Fh5*.f !B!8Gΐ3[O'Vw[\P}vh}+, U=L !B!8GL?l˝=`v!B!8G8B䱛Vŝw3)E駨\}mnB!B\o q$@lۢ)&?3F%(!➻)AUU^ֲ4!B!"β,qX1`rl^úmXn/*'Owq86hak7p-=㺴QlB!B\o O>Q1T>`e|o]1ơd?AnY2۶QҔQ{&?uq{yc(B!B!p^o[ES$B'j,/m<))Rs&GM=J;>M9LZn[yiJ1ֽn8ܘLjn>ru-Gǫih RB!B!\o7L+ps\?EgOuD(q|HR+#J՘. ]a{Cϖ`#}8 VY ¸+:Smj#:uL^u/4y4jQU9^ zQm  !B!8NrunynWW ˝ٹ8ތi0,̮4i4곕4aƏa)VdDXW3Ȭޛ/af |ؘh pϊ*Wp/(Y]GŶ-AZ6'hFMI"B!"ycym0b><84NxejoK׌kgc#wzx|B87w4ƿR&j:zzO-H 6|N٤n|5*&ZKtp UYW@俬!tR}rÙp/,Is!B!B^P%J;=wW`YMMBVt /`j,((APx\J+}/d/)*K(ǏFCБ-͌>Nma;Mք8sSuv:q_"T9.2Uҳ\pU=|LLs]N@B8bi6;~CnJzn-"6|$i.B!B7V۶炘=hjSr||++e;%|BK)lw=g{%}'MᛦB~=-= sm6:R{O9~|ښ؞jڡt;(qC1O5^fyNnx5TP&* \m[3?ZC/R*|9i5,a%Is!B!B1Msbt rdip_شXˆLHꋕ4 h=pmu-M,h Y{dv - K!J>+&aOj+u5D If=+BaP̗-oPK.DĦJ |e,9J.StSMyn $sY[0ٯk2 ,T&B՝츥214D&>w'֧ N l gQh 'E4_]n&/ L^R I6E-.#;8eUa<5\ɮs ~JHa`oJ<;Z,l?CKʹ2;fRn9Sq5>|)hXW_ >Hҝ*zT}{ci|x&ߠZ7~L;x6p^bEw tNm5Y{a9qp;QC~&k֮hEQ0`..T?uТ1_h)4بY*7__UW#0ȏ&fyig}*o=] חr#,?&L-w|j_Y^\1{Xpk*X~ogns5a- $y7(1~W_؝=~!B!ǎ\os_LȈOME˯+"@L=߻4@WNO#4qHN3.PdA$1FMC!>CeryoUpF&~֝ɣK466YIOubzaRU XwvxHDiUUY~] EskB!B!ΌobI-TJ7a6-ewSѴ~ XEz*ΟFYvAty=~Ãl޿N?,;4yZ ;o~_~İ1So,&$o~Qnn/M3/y{ju7w=e5ȲʧѶft:|le|N~iCyˤ vlQ~ NԹ5UqN4gަ3!B!ClmqH_vDu0&]m_1w/wܞJ^qNE,?zewV/,kϵ۫ /+]w/Ͻ/Ÿ_1$ˆv?Nޞi'524ŔEܴו0k謮.zpXk}R0GQ+Kya}ܤ0*nY҇p WG^]/`7ኂk}6wn_CS 0o~3/S7 9$sɬ[4yB!Bdۓ^V-)ǟkF~!"!^C223a;V ɮ.{(` ٰ.eecg QU+uKcv^v 4;yM/6Q'z>c4?7M3su(/<_s:mSp,U+Z|e9S'9?ڶHB!B8vJ)dq:?_!~ݗ(eroCLB;8ê˹=ʢzc}uMN a ~maт⓪3&7J98R8NCۍRDNckB!#ϾtA ^*22YD\ɯR #R'1Ty imm[kB!B!R'm!N &;1xlX72][W*\'鿿^C/ Y@i|G !B!IIeK/8rb$/a7O*!>{?P/ tk?:8[(0 ݬIB!bI[g9{b팠`؜`kz2 6Gξ@i7egf};Ejvi4:{NcgB!B&m!Nmf}\qs9b~x6U1|m%78_ rI__=$$"c2fh̜\쩭eۛ5̚L)ɧՋ*zcꍡSV-2(JY|%K.`[ç~!B!8I[cC1E-4(;%/wSe؍~>_s.oe+\>|Pˡ0ya>Ҵdg{xwMV:S&ذ8Sc~1yn7/~#6SL6M}{kTjT IDAT9#!/,aͳ䎲'SB!d W? Ѵ7L0^.c˥\~UGATJ}CĩK q6p09r^yi%N߻հ?tͅ_ 86iU?_R]YR\;|pîxEkF OUQԇY>Un|s Dz x ϼopJ~p1s&԰Bp3<@15 y v'+H|(wFl32 j?\e9AgƏZYj,Ϻ-(8#^sىO5e{nņUz?45lXW8!B!3v[Ap8/6"ނ)@A"0i<蜃HǶplgρG_ӆO'Y̎Yq ^>cm,űmIVʈQ1DuD2n(-ğiCK*s(J?>ȏ^=9e,U,aڪ(]>a{ XTOnH쭡ƁUзqO1pmNvɴ!B!bL[j^x1Ī\uIfIik4qfp5g#f@z_h /;+LK5FԘ|1ɽ( E5͟,*uy)_v [k=$*mv퇢~XՕ8幀qMђ%EvN'zZB!B!zCV 7KhfMSaZVnU4N+hH0YR&7kʱ2TaQ/V.z d4\+Yo&dT%4\Rb;m6j_VԮ,\Â_&yJM s /ٙ_#+Fܞi6;kxdlts|}Q%wUP|Jfiw>WOYXG\˥ܬ ^]y!@dWۖku0}]1xkɰ+hlZ7~_x7Hv~hܑt^?yB!B!P3  u1vb.Ffi8"Ĵ,,矫5#NxuWTaS"- n3ذrCwͭ/W)WfuنjCJߺ/$`T!';JM=E.~SI 6Xҙ&-B<gJ! בAZYi__ +,fg< B!B!8nj8G،%h$ӤѭmvN#2I4Iy8cGP8y)]mOxvVV0-.Gñ N0qdZݿsǸ9>j&fw\ h}Ӥ:DCd΀f16tfKFjTϜT¶"md QŚ+yRk&`kA1%2>fCPHS ;S:Is!B!B A[\6}O-ZV sQbfR%nY4?b\6τ/;#\]+GKv-*#}|ie8 ?8afKbnpF?f$"|*ڊoh#lt sTuc {M.hq!~[IMf'a/-3W.pEÑְwfkyݗ^*xƽ\D f{vK6a\(o2ܐ8Tû\ݲN)^ER&yQ[ C6q`G# r4 `q3wV#!k1oDL6P9wߡ8_^΁f0fS(rISM+?ZNeu1hFyc5[+Bn7m9MյD} 1[QE"ܯH(ya@93{Jv48-|c3g%x.cɜ2MH f]%<o+߱<~_B*]6߭4<ۏE򾞰^\R/~lk-#-apm*w3ʚ(scQCtbB_/-sy)+gg $ !B!Co0m cmO2=n<|%vkG7$Z5 hD,t*pE]Tai+C؎<bG2@vy3vxg^%RYS9`#z|zJfKc,v4 ʟZ]'o,bš(8Jn=ژZ 5^M*+Ɨ1-G+^Ó|0d@7d s_Ow׋E痔^bޔO4T0co2c%2kN\'1vl wDmJ  w+x1frۖ2TL|uw+ny{2;ƄfÏ95F'yY:c[X2&qOXi@%֧)/EJpvKر1ʅ7P?s ?Yc|@r-vp9dmm#dh08iaQkX#A p fE9s+<@A3y2֮+gQYYC8yE otkjXl &S@V?+U?dJ8QY&'RN.zk 0K(ze)OkEny-[8:BU),eOP?f m-9{Gឯ) 6d`;z5ɟSpaw*(1QHe5:m8:~Yij=wR(?ZG}P%U-a`E?ZIe4$zl6Db`mqYU XNkOpH }\5ۃ $ !B!Co0m"=Qmm/i5i p/֪p VMq 1@(ȼfSVanġ04[;?vMCܴCD.{++LX0rSgkb@9e|[P8@ m{{jy"5`;|nA1'r YZ>쩫&!B!Gb4:XJ+0n 2ƭ-ѾDk*8ykѸܧ7W A]BumuwlM1CݐJnՖCX_jصaHx?0A併=Y1B OxI ;`٣,VF]4a`ú7I Gm.+av}m~rs,V=ZnլZ]Ak0v !B!Gy!Vä+-&;Ûc1kvg-#|V wd|RYi@0ׇ *i^&\\nk7/^w[:4ע0oO)Mw5v plv=.^左<_I.Ř"] ו^n*, 1sL\@f[GN|2eUrydb_]-c*7ٶE X 5;v`f%Z`^bтblۦpUYtgqӥ>qXX9`_8W>zg 9w|1]n!B!3&60:-gf$ 4?ⴟm9Tr[ۿ{u}fK맼=sEwz9ە=혯&WoQ@bqW@֓|ɶ,[w<9%|kN狾 >ͅb@$EO3gwd7 \j$o'oladJ)f!F)Ŏw" ,5TR?';蒚bǛ fI4sSiB!Bl0Pidç9;'VM!=8 VS_Ϭ,蓤* G)%-u8?I0YD\zyrRt?K(x I^/Q1Ԟ{p76S_8=Z?\Ɣ> `T'KB!B!R74=n9b Ri TnaDy#[L49>FF'a5ޔΙ;'9A.+ccl{K ZUˢymi5]62 TF#5`-B!o߿[-?:c',ɒBFHPcY3),N4Vcvqޔ>m˺.d˥vu}8tܽێ (.ij)Pêe> 3շ@8wif2X!B!N%o q<2ng)4P{8 ;)Fn,#M4h3@ՔVPZtU2bTbp5Ջ2gsۺi-A[?L7hZI~Z[= IDATkoJЈw,,chiSg*ƙnld͹UT1e4͚5Ub6k9[!B!l{3 M{L7kAeq4 8٦IvN2hMJ |Cw1BR&[GDM}>jk዆١Ucz5nD&ɺlM^B)2̰ NՂKQT--{!jG0[6k?ygsۺpx.?K2s{{z{Zm~La_JҼk:ˠ#y@ݚ慩C禂^mg*q&ƺrHB!B`0Ni m(u(b[5M8\&;˗ :UECC,ӆ" H9ͅJ Le Eip4ƹUc;M:y6G$`rBS0=%!6fC/^3Z7KvkY9:qL/RF͑nܶN~Lٕ6?aqW ]J r\+K{P.-&?r2&yL7HvsDSx;hVQ8DjmHNjPdAݜn6Uhlg?;SHB!BSdۉdpg*!JS4|l'gIWx'}4q@'BCR&f)д?j3`L$oˈv?F1AOTܒLqanJOeӭ&8|"~|03=|*'<F?3o+ba>T{˹"6vbB!B!zoklfl[5qh' Y&Zk 1 m0#bʸbT̓,5q>skel!&) (kAiq).p4ji 5@: )2[dLL rMUl yA7Mҙ~'qj圕m3S/ܺ瓺aEM k8^*2mDl՞%ei&W q>Xk䏲{l?HM"i.B!B7vr62q5ZɴGm2Gd&#qf (23Su ǂ?T{7y1v7)L*;͠fXdxptrnU䴍mU8@cvpњFINNN9̏ClU 8_=CM= bE(ǁkLz޳m__ɓ~Wd/wj! L-%pYn㑾elwKTP8d}gn c%ͅB!B1vF)Ŏw"lF)EnIKK2_k}uӢcC)9X 76O7hLtthGsv+3LM*v.#86Ͼ]<6R35Gy2\7DV?:zQkKV\RwROs&?Mn NljS`;9C7'Sa}GX؎&n;sئ>nuGo \ 6*!|z08g6FYSQ~n._q<ɍ=&w{//ᗏeYiŸc:Ohe[rb2tۗK__E P0w tr$ͅB!B1VJa`uhlQJ1sF۶ "!ײyn7aC:>~M5:`t-7VN,|/'anZվUDZqy`θ?,q`b~>)=&MN 򓮣Ɔ nR^ο 7'#HQc]'8(Ο[ \&f3iv`IaE4kT"}Py4_Ɯ+O|WGwd8[]1۶m;Vx'{paɖ$oA~. š0_Wέe,+za7Wzjӕ^9y"\=ϤqnL^5U5>bL4g%%yD˹}X{jQJje9wVtr^X[Ѷ|gKJXlG;!BqL;]Aim;bdMC,4df4urʀR|jscTҽks_5eݍ9Y0fk;0*2yoZjh&c}ө']G$މrU%fыϮ?l*aΰdjx2'jH30F!9- iv0}ЮĬ gNdl~/ʨ^YZgrw fٲXMFeYd5ns+y(ǁ(?p|ɼ[(z okYgu] Gb U/^Ld1d@2%}f#Op=%gQIGiEXݩl:Ts@1-gExƹ;;xJB!88qR$7j;!ߓ9ڢ5i0S8͏844?=]%P8Ϛg+iqe%GBO8G&?8gvdJV5=SKQLP{jxb v"ޞ=:-Q">fSEpF>28hny}o(f?FpfiiP0ǎA҇p Wl{n Qw Jd# GYx,9BUs>|{ un[HYi8]ZkV񓼤gdfފʙ299{\n׎f޶pM7w}¾tYuO=ƍkSO;vF?ы+]1zǟ]k_g-i/@-)a ~̻L}\@M)ES y󋺼̑f)RJa]FXB!kY[ $49kS7kv쌰J7z}7M.:AM3G ՞pl$=̎Ll[PJatKWQuGٶFf7}L?ZvS֚1MGA$P$>ilVx4[`h8D=uh ^sN1[+iw>Y~;ٙy_sɝz3(7rcim Gt3p>L\gcZω^7vsq6`}7^뤢DŪ)Ž]m*&粎774dnC#"lUfee=k4bsmIWqa6 g F&AArc:;Zs˪zL3q}cZ[2.`qs ?k35='{d3=s|ElTV%6oe2Z3tb0bQ]mS1АĶmNq>!?-7"4%ךxUs ^^yv:"uwGr]KBϱ^I|E rl_y # χm0EOcqaLG30ˋ;{Ģo# _/FV]hc T)Shm?SFTIo tKHB5qNvP:QhBf ȝǺY."H7426W Vgђi)) àbIpĪ}ֱn֠Gl*}ذ|6yMe\1 SH6ш9ǤӮZkC>)|)>rEbnbcWˤB2nqޖK&~6ݔQk.r3|PAAܲJի0Mιۘoq|?oodM!a Hp=յl0큳a^yp;z2p&,m><f#6mnf]Ƶzv4=gI k}<{TVCA.6u8PXU9q};n/xiyJ.#+]Rw•<+YBl.rm;t\gZ<2vqW)ź/l9 a]J @4ZcYGtvL|GlxCG`.,8- $𸪅~ǻhK9=yOO>){ԉ=|s2m=SLeM6="pXwvB*ķ- p%0+z!顜Seɘ+*< ZSU埲lPs}@sL' a7^;ًR -ahGko>d;ߛIv>^l\F񙺾s8Հɂ0<5\ʼn _޹xQ8xr\>ndh+V 8Ut-K?KqdE^'ץX̱mS}Ds仟 ag!zAEKaQu7u&NDz?&AAr4Glφ#PH/wF?U~S#;NQ.ɻ(~!QZxFn''^LܦɇaC Y^'v \s!Uװmv^^r_<d}M<ݤ0zԕжBg~7Dω^v)Q#ڡm})uf 4YrLlX-OAA (3X Ns},Zrb#gBazU}Vk4/+{U |cMφ)*S d[k Q1d|@b֚/vN)MFfАybO6q}dU\-bda/?Ё9Ǥϫ] l'jci!+8r'9 IDATи8{dW^Ķm.N =_r<ۺn~Ǻ,+!Xj9z,]\u'o* o2r]|5΁pS}APPƳm۬^Ӏщ  z<^m\ߘR*焀X8yի&+k3/ubGѼu d)+とNv=DkM_j 1ٲ,6mnf\dQ}e6bZDH΍.-Med :n7G[8tnbLhF?ǺYT[æ04dsm[^y=9&uJ)kg``|VLıhICwx҆XVǓO%LĂ!w}l 8 O?=X{;Rs]lj⛊B盌\-rsM]ntơ.N G4a^y=ybw=AA|)~ k,6nhMH`\r/Wӭ.{5a&,Z~oJ{"kLRr{vO̭ûAVW;f>׎U7fϞU.]eu,]^r)N tKkkZ'ӗ@4x3՗+W  l#ۂ0{ls+&RضmXfT.8m\$p,7u9)%&w=G2J1vK J+|,N(ع. P6(u{7[040@i;G}\g/5wbb߾/3x K|KYZiRZj?7#m&v5Y;ۛI$7ubQ#w}=ތ/_除ޯ60 z'C{l轮{nB3zYBldZ[yzL us N7cpn^|yϴbix[=Neͯ9*˧XC!^S`Öz~F?QP   B)m r" _|&DϾ5|<܅ʌ/ϝȵVRHV$2`3mAAAa(ŰmsH7Ȕmnb>r XVru,+;# 3!&ב!@vy5UgRӗ`b(_yMkjS=2@@A K NVq+ϗx{o]2y,nkݞyll36P[S:s@U󓓯ywkswFӼקXs>|gb6|%9Bri2 ɉW\zf+EveAAAa)֚ͅh}IVp\q}z%us<( YW3P./ڼrMw7 aϩm?li!-11-&|% ~&_[hib7hpBH ng]wȞ<)%&fQWage) 7hkvP_jbWW8ݗ%~3D!+ȉ}b1{QGB'gO|AAAa:zDcYV’mro:,º`%kOƉv}"&'޹i.kQ@#JQ.|;Zk^+9Ӟm3\ċ_n'7&j&?]ƛg102^zKX{ڨ xNXgԳz%Ś>pW7stӯmK-칵oj)VLLl*:t`Ʒsk)ԃ|$Es<i_#:x.2K4ގPX.>ߋuYgl>"}G5uxOővtGzcgP4d5Dǝ&$Y00#t4GQY|/Oy`'oAAAa<ӱPJa&^ǃmT|T%_30p.i&FmBr0>S#d0X:xc_۸Zam[&S֭m KFyn*7y64P1Exxef׳Ϸ3SYau|vز_Wx1lr[b67k m}O.`lϭaoy6"T ~ƓevtAo~׿5_Un,?a7V:EZq4ˬbrH]nPQn(Mif|00=)"rW>" ⟾goAAAa<ӱdp0wgvJaX9'`%.|/HEv g = +O0ၾ v)ng?TM jsl s![c . P@! |6 T/zj'ds:;B0>m?P*8|G {i᧻74jmx:']Y~ǷsmԱX㢹A߃2Tϝf3MNSLJaBhe `xC{:/Ͻd>+U|1-d7'8M马 Fx=q!Y y6UsM*Dm.ce*ϓ2JAAAt=ٝ1+Y=7gwsC OG>K28֚6[9Y_O}*}#==:xx5rbq%Nkn2=)FS2?Iȩ,~vٕ2fZ?HP3/sM'8.h\U K ըgP1Ḣ`؎6RN7˩Q)'   4źY۶c|c,j㽩go \lW:ǒ}hH,_GPިFIPxML &ڍi@;673x.׼yi4幇pQ𰛌4U@N@v%?s Ns2SE(<'ʴpŲ;vi* 3v0%踌xoljwMs,9g׍뛑2q_pHDor‘n>xnzqLtAAA&6kok=mӖޗulXR L!:`iQWy1bq>s7DEO^ɩ(twGtY$@EH.bђ?GtwUMecT8a#vX1cₜ8dXcq?x!e.ˆ<> Xx+ mIxR+:w&WSprCcօ B~i ;$d/XG1AAA&&)XE7ضe;}$(ض͡׻ش^(p Y'^ZI<{;By5jz M&T `zV/FjduE_+{+w qzǞNP(   lP7^bǮV޷LOjuFim*~KU#a4;5L@gb m"16.ئزfHEqs:mt>_{3Hz`:OnJy4w|?gVQqOz^\Ŝ_AAA 1RT|l{ٱ:Ӭ5-EՈMHIme̲TZsEf^srZ= Ehh.7&,Nc9.M+*2}2|Y:G&UurdAșk.upPhuYGM?2^bB2لdHΎ\!n} 2wHf3y/GXfLL?w4h gC B|b?YR.^Zs&a~uv9>][%CH"{zظ~:͡#]E+#,Dnɠ1 5U~_,+9&fC,J 2GEHf!Th# .0 =3Mf'] Zy֌>߇/w˝J)?ޚ QAAAͅ ୭"0 - p]GyimMAIB2a)Ħ L,bȲqYN^D=tW0LGV*f öͰ)U Ӥ4BBE'mqӵ|q\N~?oE3+ (p.ݸ(P@E_AAA\Tl{3!~+KlXZvM WW|f;m!RXuٸ(SI9/Ni6 YGub1 _dm 6F5*gτ܍~0""g[{EMlGZXHK3(`ɁLf&㘨|CAAAȍSp93@D4Mv,~,cdт0ݤ[±䨎2_8cZJf6!7ضEQ(uCEE.Űm; `[md>{.   ,0 ArhɶmvTbŒoS9σQfsW L W=yWԋ?I2V;Y\r;lZ]jPZcvBv!yI܃4XFHJ[ʿsv IDAT/ 5ƒjF4^(62ڭmxoz(Q1Գi6cʿ   ٣(b0օp R,#-{R46E ~-<&]l tEwl(?zXnguͫhgWozC_7@3_\ׁB!LoMz=4BDY.P:&zxބ(~lQ4Z[ip9Ee m[(2JT;|¶#C   \PCGQ=˛FG̪zxN*jJ4vzG~3uz<7h{~JV@mvNτk_aʶxa_lB#m7֍4C/!.is\NV1.+G'r`rdxx6Ma]P9/Uj0lk >%CC^a[6 J= Yt_U8 0x>Pl{`{YH ng]w0IXj~4RPP(|LXJOggdFpEp>*+(v%(,cO ]k:lO:9_,9NAAA٠6 r%^1>zu9Ih={ٻ/5ݥر dضqFԑ )vJ2,QȰy  ɕ<,MiL*NhIMhRF66xr   3Nm.ŭw_]:wb;;vmyI(={-̾6*}> '$z8l\L)9RаD]E79)oUTvZ:M,bQmMt_;53㤰^2AAA6o=ȿK(q2vl૝EYdIo~UMaBp[[ZX9n7MOER&GC u,L)SȰ7S^f}I"2S~?jG8㈐,   Ųxbw7_b(^j#NV}M8|;qw;??~KAyaD0-1xw|=-^PʣA,V6,㩰{Z\&} B.O_<8D2# U"(o_0I 3ᑜ9{+jWsm|>mxmg]J>)q  \.L~;Sczs`bةV/|7g?_7!r7/; m_f@O wv}b[7WHC?Z8v oU;`]3٧ @|ʹ#x= #;ieĴ(Q1_B h{Fh9&ea\6ÿ1Ml[cR1d̒FerHkk0RFSL177Wɇ+yLtٕB8̭[yaW ~yd. 3msx7CR7L|>zpzƻӮGQJzU}:`$*ST,ZpzM=9y#;/c&)v 6OqO[Re?-ksY8H0KR2qKͷfBHQcu윤W{.g0twzTQ Bv >EuJ;Ǔ6y(gpv+Fm0 9Q Űr2GxRe [;'Su Ңp|Uj4@j>M ϵ:e[ood퍗,++u{Nɇ+uLD1ٕBvizAAypaPemo,}*$[Gb6SS&?zDs oba ljdŲ 7|1*%M|.{kК>-[)>]m~sқCazzmp56|={?{[4Kި"W}h`t(p[%d` Ïݧ:X}ɂp%1]ԟr۶/ryG_)r)6#!ƭ`ȊGe2 v8usdVrfc yj$ lt֮'bY" 3mF?J)rO';%+G4Mgh`$ŠeuTΛLMRZ0 LdŲ:f(%0x>6j#Qb\~hN6tE l(T&Pm~ͧlox!LjX$–/rϝ?QS^5s3װ^0Xt!tNp} _)-3)\ zFm _ U~هd|6Efv>NFg~vSb֚G8z }<|OzxOhT(aFN lhL9pk]~../ D"U d{'?s{} ^`&c5me2ɗ;&rێ=mmOYnʙ/[vEH-!d ױjqCl3UcۦpG-pdt;ۦ!^ϠOT,4LpJ [fQZk*ʍ W%[DUڑfԶ\WiftD;'ݨNU O.c)JJ3,e5zp;Ko В&G8xamMXniF)U(epD/w>gmvbmݧaU= -0 of띬۲'4:MM֮a6{_]Ia*=MR]^̠\L=9p ~g]s~3]X~0w>iMc]c!eO6rsB;َil]߈;)bL)?.źYZfχ9xdΘM7sC00t`$ºW.߿=OnL;Et~{&"Yv\{D` AsXwҞHAˎ+8z_@k-1@vo&i9x'?7X]Ė1p.Le/-RQ, W^`zA>U%`4FTcX9ǝIRZみP3g™`3|3SojYcdB ?- 3Ś#)=4܅+|1YfUUgʐrb­RPȿ-ELV(&bLKe.DžaMkxtԎe9iRDnh~ms Z's!ֳLĵ+&vi gӦ}_v%2*+}ܺCǻY+\7u;f jn4  gÌj͢a:ͺ5 Sfd#+M͗S9n=-=xNtԩ3tb:+D!~۶m6mnNJLNM b'jPQbBrXQ0tю]8>cZ35.VO%N$)Br P:.jxRXQ[9##{ R BʥXEXغiܴ U~5s }Ր3.؆.fĸПUH·|57.,lY60_lOD.qz74n_>LۂI4F.O}%-+95G]c&=cuaM'~ݷkW~l p.Z r˚z*}>V49z|bLncZוUlMw70a6 gdyGF=M~;0 trPmW+^>_ᮩ;§,s^77omm!_]k^Z7;|T. V>֭gyuMJ_WOL(bs=frޅ|/re6~{2){V=Й  3ǰmJPιqET4w0Zd\hyMirJMۦ֖~u ru(Ot|ch>sx3Qe0Jqyv/U ;yx=9^u.A%v!(f]ŊeebG2vC)yʌK'B&ck Jö:#slVL{_(=NX*rv#z{#UU~ 8d]ykNo]]~UgٹS &>|>@Nrp'>J^^xe2vK21{+Y<׻ߗDO2PA.K*L{ĦlJ=~r nSc60޹ΨN&^H8D}tm.vj_*ss|6̩SٴM;30'uIFԜ~&.dQ+̱ ( % l'zy-u/%fbc_<]P|DLY1bf r'ݗ8^. q Q]PZ%(J]0q٭ZiUh&EIw^q|T=bg%:MHx:OR/LL9:QF֚*ˢl ٰ&yAvB8R.Csu͇\K^}*Red 'IJA>9Y!Yg3x^][|g_/gAuNd#ST ڜfy,u>o߳ba=  ,F9u2.cQX5R,N A܆Uh)f9,bSv(}Om#LJB_WǺY2w%oFx+ іWÜ(Ї?l?'>|./8xv|~|on~~R`k??7x a離 ]zvrȝ52 tIJ⦆`%jzV8,tXQW;Rrd/Ň[tQ1.{Qm5&i[0p!B0kxy|nAAHLZmRR8 p6WMWkS 5.\ ^j<)5SLּ^ޏSA䮿 \)̞2gO>~Y2xOCz*F3mȳ~M&tz/|?<~weE"&_ULQcR x ͅNxHu,tP18}^QmOqlv,1&;G´VU P1Lf/O{9no/ȺYC)e3^P[7q>1mS!6n6;ǃYnpXU> f0Kѭ-7nu[YwSjꨜN;mgmbuX|e˦̭4ve=DбndL,;8{/ȷhfCng$ru[]qUwGLS#j1g13-j + LW1+#p&:41U1:dX]cc sl JE =cZRKzT$^{ϺΥ9Ќ#(3`+`fzM g;i?h}q=c3.ۅ <0Λ BP(Sd\`ptC_k^V;m?b~Q˲8y='8.@9vNd,iC^UwLv AYŸP֚:^(вSJ>WA" XRE)ΨP( Baۑ3D&eij]aLIJ1WB1 iOh˜}xT6'XXMyG$-[3L6%q LKl*Ey0!uCW䙛tee>~wtjxjE=猅|gW$js|kwOmX|O~ "/`ÐWͼTg Wb;KV?%*9{{%xJuRC n~@=HB;kxۉlI\V=yf85Jِ9q$<$ BP(JHV(fdnKN*]&a#W, KmI)%x Kζsx_3&@~%=5RTq۽@Tp_& h]Aڏd}f$aV~-6?Б7NVG :Za Ni?ju0:k},ٞ`+Գvo8 ~|ǫаdw[9VKOdhL 啔 kپGg,tJ6gToWC{xp{>ȞU Բ=[9Oބs~{eL%V\%*B1rڌgs_z+ BPL7SBPtwQ+ByH`Xvĥto0ZYÓJvx(Œ??򛝕 vy5m;\$^oW{0:G;ʱSUth^}7kmM5:C! xjSa">^믂[ҁ=d`V$ +}8Ồw_mXUPJBStgw&ojyW}]te%/~=FaF*uJf0p&\; ,sIP Zr*! @ۋ<9H~4Ho-lk8aa4Xm@dz'7mhEtfp H LfmL83CP( BP( 3m4S[3 @.ɟ;|'DA3^%0%,xIK aρ"}8܅pHʣ֡P VφwG 4V.sd)cyHrmgqY. cՑnIGgMBMÖ,-&Ǣx4f@c$>Ag yQpwz5FP( BP(gnk还 Kl . zYL5%31y)$G'.)(q αD=MsİWr7W `2˞m`0DG{+4bYޯp87X7-lz?U)NVRng%<+Y`W".X_4艄h pŏ,{Sa:OyZV]%iyk'-Y׸bBt[Bzm3Lo{}nN ֣(EN-t2 vQ|+].P( BP( 3m4CK#k0>0K^4S~W(d$& <hH"5O͂GYQY"&?YKl Md6)-0w~~LX^6&IмnM%|k4߾Cz]=?' ͵^W(dVf/YTOxm Yx` _ܶG# EZ\(6=Xtf&'" 2&<?#i\nxɴPw.uwd?>r?+ ŅTOh5;rj/L5l=YXnnGG*{K1u3yg[`&!_HXȃ-!i p@G/8hf$eݯԱc_Έ)c/d+9GSPI1Sh:j\[c݃:^3 G\L M<r+N`؀<7otY3 Ȭ?Wȅ(2ir@{綂V>籝JVA%X`Z6k-x|Eaj?OUl`@8E$u(@]S(:i2z7BtS0˙)r[RzJ!WAp>ZF~k)&gg K΁E>6o0qiyB/GF%].fԝ;),rboF?ٳ4 y8LҪRS:Kzځ3'ɻ~|B299#e_nTOEkZ~3ҧ[1]09Ԋk#Ud`ʮ/lTN95 In|q=BHMӸ{ib쎇+1M>y([o5_Jɞʹ mCPZ-V7yrm] cbJ݄eiLmZ.f_L3q U?ogxD\_n?P(cǃ.atuG[4v/9@F%9EE1;aL㯔Pr/݂H$.Hwdg ~{b}`+M׻3 zM(G!k.@=_!Ko,Sf> ??O7}W_6sX-#DC56eu^''UM16ʲWÊ5>=VK!KUŕh#1S˱c&YE:߽gmO<}ӵG16nS9\l8n\{{${ϵ_CTa/% wZ{v6z n}htXף<̫WtkWwUOTFO.L=[3DDƳ%kj秫;ƒYKiC^@Wwc!V.-C}w 1x;;B8TqN'Z`&-GVȀI A޿U,,lIg90t0{&ÆsѯOȔtq;mLt󱞓#2a?P(%>5ܾ =o{ۗƄftjEyy[=_"r}QH)9|$`N7_{ u+tB9<BC4 :=#0.s bVNJ`:OV_6|$hp m0DWG--סU~@K$̽j ;)d_Yܫtm#'?E ^K-->Waz>) ]{r -#m%qټzzoK}ܻgw_sS?X6ǼP{*_m4&-m gAL/w_oHZoeoc~8Ycǃ8c94Ln'Vbh֧S{[kIڽ2cY}ɏC8u |9JG8L72*djX&?OcZmҹ A3` W./_췦'Ffro۹ÆsѯOȔtݾ{c='u} (T)_졳3{ZYb̏RB$\^aO6h9Jbߘa.rx{KG'E/tؽ MD>:N&]2vljܾ9+xˈpǣՔ. )9iQ"bЏ.d"caiNPAA3,v{&07)۩E]%ؙNY2L߆XvŏQ#Mqk\|5Mhb".9(_::|2nS nwҀP8}Dz :c'CǩUFWzM(v=MJX6;Bgl&OEAŲwn=3ituk*G-;F80RJ g9B>"sR<ϝRHNtkiIAQI\9? |]ǩIqC$x9Z@9LuШ4\{1Q} ..oD E^(lorٳN ӽ\aù37'o^yd"4ʜL?P(58ee\/-#_i9:h9Dyikks2aǎ2*1L#V}}א6}>!%^8 k}'s~FN%i `3Zn{9.V.-cqzga4QeذN}9%>BAl <j?bۺ&es*ga\)j{a2}DLXvm=ѹeIq1 d֟O0fvB15&BJ+c|aNb(;[O IDAT Qp0R&/PH'Bg$B~?J{ynC {妫;Bib7!KM{(;BMA}DR`~ddY %ؙnY@v5 ٓW;(&D9p+3- t=Q'DCA{lu"Et%<kd#A\ jg{6Tn"7Fèm3Hǎb=@OnLiyLX2kj'ub-9,r`gMmogk)`35۪_Gb'Pd s\P3Ll+^ʌmNEWw9 B&}dz6ޞO _Lt?Mnd,øWe Ԑ&B\~IGG(iFݎ] ̽Mۍit̶fBt Qr-6c s44DN0"GXLJ#{>f_UG)}L)0nYTo c5m2أ%&+3TdX_T)#iztbnk3ѫ* 0O ^7qBE^l%bXSC}hL1 OY5ˆc_i^vi{ص#ÚkJIk{v8(S1))lɴ8?v{)/R^] lH{(4?|]}XhσĤ1 X~*ĺ~Ft S;)bjDrOO[TDuچ_O n2nY2NvB1uh9KhorHP8 F!=_8Q4ʗ[=L/ ^&ckb)2o8q}K4)'sSΏ5(ܺ؍kDtH2)-O}:1E*;ʲs#I~RbZQ1bhڍV, ƭD =BIɨP+zҚjȱzh?LL)9f#PͻFm` lxf[L#;aƋNo~"4utMy cْIu~`zkZ}tWv &k2*9v3}*tijdNvB18X^UJx<3OQ#%e<28ыYY,v*{\!|iŞe~X")WLC$NLۅN'N:N(rj E *(JZ\d2/%vD]I%nF&_Omwc&~1y=~ u&N#˃t'ojMӊLV(fB4{[SK,'Rb!x*Xp{X Mtvtu xy6 ?RJv45ǧ~+UV'07O_.hXSAYN<=)*t!`I{cG:cCܱ}1Mc!.IGWWpxVQGn ^7ng|D%^'XdRt;9F=OP:RJhFqMJ,c>DUX9jeIO=$rA"i\]CӐ@SgpFÚ~@\!ax)'K8Kۗy@o^[kd eSZYGyI踿,,oةj9vf*pfy[Z_;fg3bJ/kuQ5_h }]35MMF+U`&=';B̽Ɏ+WW~Ym/!fr7VkdVsd5Sb~t\sjohmwU׀n V>H*o,/3<>mృ&N%oTqFn]!(/_H~xgW(yZZ\P( Bq1b^|{p}# hm&';Bܽ?6Vca"/!@OGuZ[47ч>tۈ\sc[.ށV-)cI"1s> Q7n9:0aڏ!Kg梒-[LBq~Jӡf=؊kUy: 6~"c߆.hedɧٱ?Hm斡 y#߾]x96ܲ'nwYJ)}J0}[ qCRlTE#/m 6{x6Oyh%ߩ䭤0+nf_BW/h3Zڣp׳m3ݾx a x1a5&c:I޼ly7ո+ z 5'sg]{Dyٲ/@g NA[UP2{ju53P+Zh(N8L)˓q3yD B1E׸h }Ӵ{]t) BP( S5s$;ɟ BVnܿl,a| 7Z'}ͨE  ㇍e4gCGG#LQ?x҅ =jA}x&,3 GM BZٱ=j`m֕|wockZNi?juIba^_ռ1?>jqԊd5cNMJȓU2p- '^ULv eW2@_UF3OTt+5zx1o}`3{W1 =̉zJ>ek:F8b$ko|smBȀYN^}|$^ BP( oO.٧BTLMw4c"yg.B~JmkUWjy';<%b.,  iy8(X`Mtmwdh]YE<Z"䯛yGE9pݵ< + n{5ele+]kܾR9=3V} +X:g 2䴸8ˈyhW,\an԰h|c3ViLڶ+&6L @=oqǺZ>VGP/Z5y=mbNn2]cNCIdU( BP( B5V(fL6ؽ5QZ?ߵ8]*yo[#ƪ*/nZ 9 it+mո:#`O țY+y#bϼuL2m[yI,+9l+ظ7'R ~ .fÄFnJԴBP( BP(LYYbZ~)M>mr,'"Kr:w?b g|޴dWTē M?g"//f|SY%%e2$ӫW&5u`3!BF|MdQeyK5X0V xF~@S[pXwg-A|LN{M&2UOJ^#lcD I@8lɹǶ]Դ:)rb}B*/LP( BP(*jPSta0 #j=ᕛxOODxao&l]X%ʂV#hx"oL(y}/9=U,ܓ@yjc0c^%>|V7/ߺ̗*.+>)vA ,9v"%hy*~aNmbyind$=<8jI[`W!a^Eu~v2f?{2]ܺ@#DQNg^o|Cc-!"28,e!H5{[Y؞"&&[ΖUn>8엖,czQdxG⓭\˒։0S!~QAHZ* BP( 2bfy{0z=3RDx4MNv{?l(%딡3,0&zly->oM#ecne>cǶn]\;,E4Y ۜ<ba̩o$Hԍ[NQ "9c >E/p}dJXUϝolq=zp+a| O\p-!y$N9AnBKTvqї(QH HI׌ r(CQ O!%&kdhHir0"[#J=kO;/ɒ&ߴ=<}Gd+/G,_4"77o'#UHX'w\w@7 ]\VGz>o pEyjOv6[dΆO^㳦f歯FJg_rqÕdrكf_#${$s BP( B sa͘p#hah(:+_xa.7?>1dPG47+8'k̉i膬ªIkNV4Dž"3$#-,1!bX!+b1QЄBAPe q<1Oeh_}܊猔~(?̅4M-w&BWǾstKgjūQX֯棗뒎}8z}܋u˓.+M5h }d[d[=o_'}M\7n[( BP( "s&B؜zgb 8A@I#a%++(1C?KG sqӝռ_!)2\-&"G0pۑ0E"&&%Bih9|J~3#yS^'#t%Y:ߌ E^ ls9B!y)%EKN<_ˢ7ķ;1.$_H׻zAsUW )P( BP(SMh t4p IDATs.^D)csQ7asmTzo Eڤ;)FJLa=yzF!y/gDT"*+α&d|2*0,AhrB$ Pqh Se,Nк~ KR{ADsEnj'Βư\Kw],,TCq0z:BIۜRo/>%߉59{V BP( B1L&̅A5:v0=CL>!} {<M嫳%]nWrdNesHdsL%42$T! I)G%$UIBhNR@ `Pljͱubmmm4-jwrfo3;+j5̳Zνt mrA_h`9}bdEvnYCws Q `o7vh,+7 A^9Bk) ݬeK8(f;YFhǫ\Ѡ:esr7  ]'ʲL%<{]'?>EΗXp,VqK[fJbx $~t`. 4T*B!B1R~FHw!*;7mGh?@a1}ٻWb$%v͑3$?YxBTɂd%d Q54sZPrʂxAfg U5*!e ,d^C@l &}C<{S'o<;906?:@8]ء:=v&mٟDeB!B1hFweFQtb \֦w\Zmtkl _>Lw ۉ>B^>K*x@B,⬷zB)nʕхB: PgaE#:?F̀ZiAj0iSoxdC:g$N .",_喒YBZzWu E!B!D*=)`R `,ޅ`q/(]~pVִHp֐CݯvU_ލN 1T0U$e..irP!2y-@֬lW{,@kmB)z>q: mo{:P".][z]=pn.`fKZ|kO u jtIy̱Բ ^%},B!B!NHIB!rM M&:p☘88d'YX+D[ېM1!\_1` bIdQE,D4l[ԛ 'XP24ْZʁ d7/.|ך)!MC:퐞L5S&o&iԏLLl,Cc`mLzp"!K](b}n Wؙ%J~9A:ͭ|$ڶI{): %}, 9-{? + = ^yd6 b !B!JϷ}q $@)qHA ~iF2*9dy*J*wRz) kܬfg//.+jBgyj9g:B!Bqj-E $񗓨z㸉RZɃe :8x56blA|i[!j[Wqw{*6>^2+O*`5TɁM㖓[܌d ,ʹ@ ewaïKQ܂B15avK;fsaskQ80|_ogDvq3m B!BTr\'_(Chr[[6׻+:B lڸZ QR3Yl0 9R:E݊@<9l.i&׸R;nI 3F)al!:/tF9d _9f$g(.=o7V|*B!|{9RhF$+L4&BHӺr-9qm䳁L'޴YMq`2I](njf6+f9n dV&Ԩ3 u͒,B![nr^ YL&d!jQJ WD`*vVBdx繵-P9wV~b}s gr:;n WK h !B!J۾\NBTJB=BR\B N y$rګI$+ {M.kccqH̴͆5 Q,"6h3t>09^ic/\go/ #ksY[^B!B!V2Bi'DM-|SncHBb@W,M֚/$La&RCnV/F{^Q|F2e8JI>!B! & Q#K.T>` )IM([F|&n`qm'eћ;Ikl!ݠo/maj@Tv^&򲗽@t&IB!B!ĩm!H0Y/)WhBq(y^eA\7 JqaIPL>zK[cESSt!/MJJ)!e3Ywlq\s@FB!Bq Uz-X: & QC\vɸL-kqw}RhD [OÝ( ye0Lɔ۪/Oa(TۈOgAФO8ޥ5\mȧѠs^pz̅B!BzRBڐ|BR˦n#IMkȗJSA( skطҭio-!Bm!H0YYJwJA +ܠ7:r!/2pD[21H")6niiEӚv8݀tkʗs/KEKZkzdo>6$zF,uC';jp/پ@n4}&LsUAXjӹr>g!Bjq3e(X.Άy`d]7wb~_!l[osHNb;iqUh݉Th\e玷n)I,&$&T/D Uz` f\[f"S|[F ٶm&%!-Bgavoژ 0*4mB&V)6qƐ?u!jn > MiǭA\B,vq jp#㮓 /[Ok;=5 49 ̹.X#9˼披ڙHx!oksQ'z @!B8uqRXʢqUq7i,k@=mst,-xЗYD*ES,3=p T̅3Q&WYNڠV4Z&d2!5&b4V6' Jy)?["|n7}/;9; Tty'A^ g+ u/ٛW>m: 732r/;g ٝG?䊝ϱM6nifhb2$r^J[(?mVD΋rMEoۺmhÝ&Wc9ϱCLLZ/~C I82rxW_A8bZir8{λz_nbVwwܗ[WrK%ğ 6nis Cp~S0|bT>d_-A\_ctr^','Bh/nxb2Ec4:zF~{|pV y{7^mE͗]Y=n n}G;_uo r]=:乃#|.Tq:8>̿!9ZL*hm>Pj/2υJ^OB!پO #hv,j,S6F){(Ѹ*ZV2OksFɁ\&斺1zؒsme.2'5qHgFdm_O lB.QYWL!d!jnnV { 㖯d4PBJO%2t`1P g ` e 7`엻pxC`*E+y`蓺8]$MkcELMA/޴ܺ-|n׶}6~z{Ǎ05m!7^'$Q)zq]=/qC+=29*xDј7>'8yWzFsQVOU_ f//}=w8E^:Kğ?'> t9/crtlkym>}_sTrUXlm)Ky>`ǥ_cZ<N*y= !B,f$ٸ$<;t{ 4[ So@1k;\w._u3,}V)㛏e n{6^L3jm,̅&i,4ZX'TʡF9b[;fubú(SfMc'Qf3F*FP:RO+= +ld߇V6~I k/vcκ҆B\6uw<:9_7q,kbE_% `2nd"bf`3XL]Xs{fz0[93ΌCS ?whdD{ץ*g_pgϥCe}yMQiDVY4F@2zԛx9\WKQR_5_csYדB!|iGFvb1v^NIJpcfj$S6_z/=}C͗7Kym]7tr.#!}iMTcn퐕2 ] $BM]}_77hc дd8quu%.LMJ.iFlݔg&V(X_m(d`{^Qun=HBm Zf5gt.ɬp^ 52 Q!rGX>Yh74UO~kpk}883g`b"E8 q Zm켪}zUo*VnO~̙q}}zIZkYv }[l_)C},쓠KRK:Gla>GRƬZR+^1U,uVzB!Oqfm;^z>22Բ^ee.rlv0 h}SCxyD,35YoκvQ'Gҵ\r'yn熿l 71S{WW{N IDAT=fc~4BOokX{+j&ŏx%}5P:/\%Z^{𴭸ıTng\t{+gU4d{bY$,DݔiY px>Eul6o_VjBdNt_x˄V*t2iݮH6r\x%/yOz5rsc}y~O -ZK)`2~lvQpcck;>'ֽVr?3 --.7~,ɑGc] AFlJY]< e%=rrΖ:X>|eSzB!ęŬ7ٹ#GF9|h hZc@pP)ԛ6G_JMM79gb˪J`Z*=ߞK$rǀ,e@&aB :Hd BfQ]?n)LyMQoֵ Ѝ8u-s}ļ._C2HQ0Wm6N_cvS >ݎ@Vu'Ej& QC`D$ՄI[>"Bge@q Dá*:q2|+F)BRnSFn$[Cϸۻ˪)zUz[f7d!Bj:^e6UQ&fYMαr4R欞-P%IuY( 3 @1:k(ɤ5f+Bs:QrVe(n6#rj=LFq;ͭ:Ka{v՘uqv~glYGq?4<`|imL 4}с%]5>dæ%_eYG_λzL_F:/k,vǺzy^q5>4BhxMc>2K]c1 <6q{Nok3(YξRB}\9io:)wq_N>!BjoR5NVS&!C 6Ij*LP65"gϷ.\>,51{_Ο_ytvxkC4>B~{D~陧OL#8?l:!7`Xj& Q+ccRt1H 7KR!B![η,ݱT33i\eÚ(,[03m^2@. ܡѣ8* {M~~pb$ZX(ϋV[@7 Ѻ دxA^}"1VϦϣKrO) /1By1{G?S|yɻl~eLBHU !B!HηUb}s Mc$mmCZ;nj9NF׫/l]c+~Kݗw~?}|互umMl>mm|3ZoO,)s!B!BT_η# 1&m;8>Q+"¬WM0,LS^řEBP% J_%7g޽ЅYWzp -wN !B!8Ū[7;VބiҸ: q16nhA-1 & Q#˾5n75JKzHo3덛EB!B!je%UB+Ld׍tLS bUmlRӊ Q#պFŁgsJ;븜\AƓRB!B!j㔔4,,klFo,DBВluV ᅩYYsAxcX(8;{N@;gVB!BQ ҟH8뭞g2?\X&(X5P%B!B[LFn*``xr)@)fi0I;%/ k|B!B!N5)s!DH !jhIݔJ*ipl}aX 4SWY0e? `h";T> @?;%wAkM8ۺιAChu~p,6k քqލ2͹7CX{[7ʲ<,w}!B!s;Ue.aK|}1UyTs}!B! V {w? ?ͫ^.|zVpP(2wٙq&ϱ/Cj(lt9nZ_!B!–|=/r[hU0>X[ZM!N'LNE  ګu4+ 2!TGYJA2lκe.tBPO=g73WgY.oZҵGB!BwriҼ6*r\Leg' z5 & Q#^vg k?e6_wknVp>8[+v`jI;ehJٵlKVMtH(V{w%9zй\> ?它}}S{i?c rS 8ƶ|^n{2D$3YZe7~f2BƭWBkMs>[^GyJ)6;§pZ_!B!1 Ѩy΅tVcz%=_V;hzgph[(}y~M俤Ә^I(wPs#G0mۇix|q.$ ɯh vj@RtLq0{h(stN>BWCkX=y\um~uc;(anڼd##I >T8w|cGYwoUtyG˱%EE$3Y [|2^1bʶȳR񉬑o*BBi v4!Xu"h[ŸG^E\#8EqS8Ё2MwM-|02.yZ<B!By-|{>mD7l~ Leׇ;YlX\ EsִH0fkȍrdҡW;i/^qr'q߲:V4^SC r6c_^' Y'8@Sc[^hm"=1H~ϟtǺB(R uж*(SDi14:}[h6`l$Y2,D,.2U!\sPuuVe(d@g4abqyshyM ^})Ʌ( "6t5vwqyvⒻz䮞_nؼ2r]B!BQo8c~<|aTCO}u[m/Oc4Z,b!bJC>:+U,n06<{<`9ʖh_bdfn$q>?غ?K 1j"N- & QC ;?Ms&yæ֫B@rY2 \op ,:nV4AJUEB!B!DEH9׶/.wS#\%_7Ϋ樇Q9ſ 8St F. ԅ`*A/z%.*vE aq$@vqnlH SAx7~!UOB+/ :q1;-N )s!D,_896c"c iɤWBظe4F6=d^ fC15dy !B!r\h6Ntڼ;ƂIX@ω6`* DgR;O$(KJ]p'3đih^ӑOpl-l[%31H+ l͹9k'#Z|WFYutMEk%mh!VeJZ`5RqwY?[ژ+AgyZDi8gO޲ʭ>zdAk2Kr7i]hgB!B@%|/?P9]6VOp0 921&NDMrIt*Z GF`;ZNEc+=Ei1cνvLD~:SmLk'f8be=ļwþpR+nm =p\~|ū@zv0j6y?sElڸip袹0yf2KM'[ƶU'z IDAT1~a=E{:ähZGSCBВ. 8 av  MMq .zpk'o:XaU,@㤽lf(T0B!BRGHYZK-s/F>jVle럶2]abJE43SyѺV>ܺ3uc ň* )^倶b]'g^=H0YYe7zKc񳀍B8ﴟUaعl`+3 9{>xgB!B VYLL-RbCK)s+'h-I0mܲVJi}yu}Acb^:WI(FB=BP6. &+A[B#0 ɆbVoqhod4dd& !B!E%,v~YrrY;TsLqʜVO@3Rj^`mx g)De8ǁuwդgy$,B!z-%1  4Qxe|cP6/ArmpF$bcϱr?h{2Gy{\vSg!P3K6=\?g !B!ީu-}M H;q,Ec4W<ο `dfNyunf@;3zFcplˊ8`8O8 1MSB@ӭOsK+fǔj.G\waq͝XU0B_gee\.Mh^B!BaL6J)֯/X7xaƓɢvY>}b2ő#tVsn{ 1J[yM;ENR7D(;w//6Neu@sh|6׌tyV dMMt MEZC"SU6(PYwڄL^ fM-;76= -Pj: fo_>>{f|k:n۩yCyU4YhGڎwľB!g'Yg}CISl ga㆖ߗ?=-n"n4㬿4~a' N0M`x|k-|[Q)s!D,EpMq& PkƧ퐙LaSv =nCh8^s>7(̕S[o{Ͻ0VOQJ{YԽܾ붵sӎvpw7WdS]!B!NqX.R qU=<cBmX^1}94-4bsB IJ,_wq"WxTQv໒YaTL넘&I7c8=q8jXjXӵc]L5}p<%:oV,C|όfsf,?x;s>9&ig9a0/)s!d!Q!ݤS5œ2QrCq<^VbDSZR-Q lC=ܱ1 +Txx׆~S)v` s18t!e|MSrxEJ弳2j*`0)0~F2hʌ/#-e3CWN02H7ne0Zk}O'Of&[[3yC Jٓx&<-(smJ9宇۩_:Xc}&i{gJ9emb6;g6׏7YVVTKc}}QޅNjPۿe{x&DB񓑁8w=qyVL ^:A,#n&_V[ӻظlDoIC|ț5 6۾qE-wwY5U6p$8q2:iay|1c7|^ÉB!֭|;ɶ>wq#B!ȗ7q/-6QӼ БKM%t2{jv )sf̢Y\͝Z|&2e8ax 9W޾0s31{]z)C] 15fhFiߌJ*~gI0Yy?KPqJ=8lw`@VPXe!*BCBkTfll:̨$3s2~KMl}|M|KM=3}y =d, ?}9ICEʊ`?q.DgIM)P/qQ7iډ'ZXo۽,X:Kܤ S`cETf{ O"ҋQZ!B|%F)sfB$ (xWg3kglRWZpgF}ׅ,Byo߅T6ΛQmH)$5G!  * 02xjNG㳖|^¹[sEB̓N+C: *'83OΧ*<_#T` *`ڿUGJa%*.]leN~6:_M8bvr#3l39UױP[Jt^]:.Mgp m(-B!D*\bR6٧?|i_;Q4Zxvh1uRT- s5qd+*f0'1Z}yPvZ@@^@kÿ N2(\24oHd!Q@4JբPn]tû!X5PZ%(J0jl6qP)2͞u]oij-[/7>'6L{}gbl\[[2?In'~aדCt3Kؼ.m 籯nr:oDMgL .R G)'g$U P5{ ϗNjP :,B!.NC&"8T- <^Rw=NM9/+WqW^@)[3*|_̡u3ϗb\ RlbNB w*<@PT0 z7I0YyRTwtüț$e+`(`C"i0J%0drx48ee(rKX`f-OxvҴL:gVL=qےղett`פ Gq˝i?5wLa'e7j`.׎ @㊺)qX/f;6[>TWw*b I2gƪ_Écu'I}nGhN(B!ĥ2;hyMU@~/f_wڙ5uY>MKQ@i`⚊{6$P(ѓ1VKE=[KOB̓˦LISP6~8w^ ٠&_g6W\GjZ1p6NBw<{ymʅ!r p74xFD;~31*NP00@DUׅ -w WeVB!8\oWU9q8 baĻ5m7 y[UFnjT;]9'~2YLc4 gcȔK)ma069e@i1O.چ'{=m{zz;&5.:v֡y߮ɡcꔞB!BK؄Tƾ%&<v|֓b]sdr֭ )]TwyܡWQJQy]|[7 / l)5uvФeZMkOwyHi@;Wͩj$=q~HHOsd$=KvnMB̤6k!Zˢ!MLaK*_IiH/ۤӤ`&5^gyXk&~ V/G:]n_c}3ُ:{{n y2CQU5a6g=pX2nő2ḄBn1m0ҝ\l Ia58ZjܖMR~dR Be5RPQnwzNEH=Ѕ ۦ/quty5ոmp7ވ{\B!BKE1e.̨A' l=Rn*]%Cs6ΨrE^s;8jhefY-}UOg7w ͶцxWM*Q1Y"BMB̓b˚3U L JL([9h]hE:@B96젳[RC]EѝsHX5ׅDy`ZOyvIMB!⃦m6ir!t:{4"q*BqN\hA>Eߋ`:`KRϲShw5Y牝yM=[x \03zA!B!BڠKh c)pD\rYގm}QcRsc2<'&oMi +;yl>X#P_{.=8.^.ḄbnB!B!̊*sal09a^Ag.p)-sp.a<<fIMRױe&ĆBS}[-kO򍇻ygMje8!V#Ņ$B̓b !B!bz^okNaRP0nKg[,w WӸ>RvcpjNy5vm/FNuN_x&N7^r1zqy|[Y+qqaIfH\!B!s!c4B)J.%EMM BP'j_P:8?}w ?b ӽwϕ<`忻B LbG/&!B!Wb+BldP jSZ*C!1T^gb O>!,<)B!B!W (JA'4*GE)=T@N~wM``d!B!BWv,pK^;ЁNxi21@ r ۀaN_˅d& 1O.+,;oc.~u<.c1(Ur3W=΃ݼuS>vkGy6{k(H!B!ĕgvNx֝r_ !$LbH p_/={q]>B!<: .& kwB jr`u8eNє&7BZ!25t3y(Xrc5jknb̡?hOyTbݛ&=zxfr+jqݜomra(gx׻\yMSa;=Gz[#RB#9eu !-T. 췶fNP*5>H!{omas& ϷѸ`C+lhhun !c-BKkYYԼzP:qD'5˗<н*2ʙD_%7Uc'#zwњqb1_Cj& 1O.{%\Bd):/ IDAT$$U2V'#{.k2~B!iYrc5J)rʅ!֙_zu184TL1X T9KL07 <ϙǤ |m!d& 1O䶛Agw1vŽݶ>8_Ľy:<*x`c+K lv=C}ꫫy퀭C;cODWWW61?šc yP,[N6qε-2_Zv783_[x[vK=ƶghg=3{;ŤfQ_ne!7菢eyM5߽ Lxro'Q1TkQ[;'ct鿗}} ;vi27""0^ze3Okguԗ[ S&.d?x8&\9cm_'g6pm>S-x#ެPӎ_!k!B|TB8qUވP%5߉h=!XPSS=m@,*S5E(A 8KiB'[ B & 16pAu4y'38DkPJʁ{d;om'N(>86Qy#ܿ ;6A܁}m֕`<ͲjY6֚s;[@w=J{6a5ͫ|5Y'}'7ԫ_V[ӻظW׷εM(pʳn O7ؼ`<3ވ-nbeopԬw~mۿ7M p7Ω=~Z-\Vc[?cg0Iރ]<ͭT,pl'iCqzd+tB>cX_Ǔ:\t>f3W+oAk= ku1Z!"_k9t<1T^$z-{}͚W5C=eA~^09_Ry=Q>;{婝^7N\d|ƴ'up/½2:KU(y y}[I`c-B31FaZbqL{.hv]ri8&iZgLCKբ aY8YR3oaV{Z!"ވljDU8L&*\CG/ :SG ''Y\͝Z|&2e8wg[|Zk"ϾR B#8;BM8Q(fbƯI<"nhH4co0 y}r|_=ϱB!M뢓36$ ќ;_ΒFk@,F"{GY)O8_U0'"j̥3\_owOpw;g} |g+?}'DB.{R[@Sxw&ƵMlu$GSJwIk^Uݙ}u8 zcȠl'G)'3gXW 9>*;SB-w 9t,Bc}39ϬxRX !B)sh^D__"Ea{Rw=NM9ƛa ,t PrdsSS |{g߆yu5+4Tij&v0zEV˟23> ;rjs(nX9glx6w_G4iQ̍t鍃\_N1ƓiN>^ƇwKfr\!tRy 5ф(ING>X !B\iz{m-δ1m6tEMx|/j/e]s]@my z8F)'N$Sw\SR6(w? qslnRo3[7clt&}Oֲ b~gB̓.+ ="$iB`s[gO7GzY^ckcoD[.ry\ c-Bq5 4_ut{S$[H h~g( tp(noP[?G]\>w,М6pGtm?twĉ 3$5-O)L(GH^jN"?X5ׅDy`Wtբ0nZpc‹ry\ c-Bq%W~N&">R%!`4q罭,)/r#B(Fq4ZRR=z*|pp)Q'st=`t\K>~t>%d!\vW=O؛ .s C<ͭ{3>Z!J5WZk6߿=@8p||xywoH{^h+™CQPC˲o.oGkĉ)?u|F7pYrsvV,cHNЅG1$,<2B!B!ܛ31>w[=|x/7W nw>̬Aɖ׹#:\U_Km{<":̇{ԎKǀ@ 1$,B!B̽6[8036K{064 >27x@BnU ŤfuM֜>qװEYR㞭\g% =yj &tuE8t5 T0n SX?&F!gcIϟmu]eeҏ+] ?+bQ#OwsZ1z,s?1=(.O,<@B!B17ˣt%/{f_U?x"6|:~fj _UWKTGOwOcU5e\1|%\w5.eE,x:%.ͽ<3çlG\|d!\uB!B!ĸZ3=~DRG4N̥">/Ќ~-+D9u\Zޯr;'.W'L- ń 3,߆xdbeLB)so=_uvqM,iKDzt;+B!B\o/HW57M~TCZ\N$,<*3;QQZ@TS)E*s=abH/Ⱥ$ eMR/0ƠC.'e@==)q68 {q܆o>tZN=wvx;LNbbB!B+:q+A)p 5C`8L2dG g?_؀\‘3m$9Ӝf{w+7w'aF4klB!B@CB̓BosMm&"N3ly fVc\fT)0kR`_S97f3Au.O#yI. GY_mɔ8_p8'ΖKN(7~s4NG9!B!̅GB̓\d5ә5SaݱJAi 3Y?k}*sFm9;lRamOho^6J%&}.llGzy`7즢 BҊ3MvvRg~Czhrh# !B!Ql wߦ.Qc{쵳$V Q,&%d!Q!Qc@V80N7*ɚ@栖ygd3(t5 R+?<Lnt Xn{o0&q+ZT g\5>ww] G"/8zZVO~s+8B!B1b\d=6h2NʾNwPN#!lW] ⃢BY2e-tc_:9Xo1d}ng3MR)7\8FmS~p:c)ӥ'vUC8ͱ{禾qZVU<όY~;3<;(dyTN*]!B!U4LLY'>+umJ䲏ɦqɒ`$}M=$t.1eggRf>MoI aI8I$4a8a r8`w0p'`J98e2ERԅr]n~6{.~}(!HD?N`?EHݼs{_nୗ9vm(oOWM b !B!^o9Jg˾6 XʖL'p)?*c0~ .ĕJąGv,s;4 h+Q zu2R`n/5R(iǮK཯rN!ϨY!bC+ M{m}J)1ow=[DL.gqL O| 4:,TmKE[O>嗏䗏̝`fB!B!_1e.Lc{4>r㧏dUNеdrtu l?~Pwxcgj½tvG\"$3YyRm7sp VT,p *JKm33^j"1_Ce٣@P!nFSfYxlx8Vs}K>,<~qUC57}1nE㵙n~]WQ<~|'&%x巼 {XCylIGyTȁ-]9730]1~mgRd~0)è04GƗ5~9rV\A~p$03*@",B!B!A1e.*8t6)PƠQi޸_y@7" #Ԅ:fh,'"ǷMx%},. k$ ^"*[WB̓Bo-_tp{]b3Wzf|~߾c6jLGq'J`:ӵVe$; ,۵*[CB!Bq[g }-&nJ]`ˉnFJ`4\nQ!mUՙ@2|_i,~\BB=B̛BoLpv=;OibLL }Z~ʠG4,P>cp" M|$ePeJZ;O@&+S1c(Q*@ hgB!B!.b\$٬dcl)1?\-t^;*'bAaZ`` FzsjOPV(ƴނ1p1q? b2H&i/B>៳ؼ|.m @P8HoI""""""rvv=,zOO ɼ3`;1qnjx'#/ٻ?;͉EEd ["F Peެ8 P "&a8+V&}X,='^~Kf82SBe^T\IxË]ϏPpnovYC"S8R7Y]󖼵żWc-8Hdy&bX*Aؤl1ָ^qܿfDUŜШ؍yG|ǻR.i/n֭mdㆦ6u謮L.®>w޾͛?SYҙ Nvܴ ~8C}l ɶ`K۸~ϣ`9x 8+VƱ9cI?ZJSqH3tf\xDZ,J SL6!q&dFj0aD> L"3Q1YBf;,23jA?x#2#SpFa 1d󣊽oyZ\}l 9BP5ꜫFg$4ltԂ7?MqTkYb emAR%_/|gq1ƥnIX!M~Z˺58K{Mq}ӄ!_:"q}{~+ѠzclҗRfXx,k̅ +"C0-v,7?*vj`^P/Xl }a"-'RAyc*b oZ--G((X:cy{q!3~UR7!j&t?2PeF\<3)4?| 3+V&N=PpgvA5e^HxCu˝9&[>ʍM;^e-c> ;|+T ]Ǚ1'3\wVmwݽOtwYEuk~lo(]lC/޿k{lTvsMXkN['|=̳c#/&ivJDg4IcPLĔ0NfuݑsΡ\ޛϫ2?''}}^?pO/?9;G~in/ԙ,R!svCy GW`Kb fLĄ=cKH::X|q6v6lb8,gBfLT)W9dcbdsndrLF֏*{_Kq&6nhb[8|lLu뛸j}j7͎1]%Qw7`y=c{lի1Ýb"i77;J>vjk]Ld9lԄ7nOrfq&(]wC:7*M_og&EߵY$lNdS\VL ET8XkK}9~;Nv6/\,D]ce5rMsS1YBf;&/-A!M\A|E>oabD(yMppR<!CP,F`(cḋ`q?sg,mn ϴ)&X#lβ ۹ALƎslosݻ&}>ɝsm:bzy}Αm7Plܲ㮻w|~xlCَ!c1S>צnYt<ϣd]v1}|_ڍcӥ3]w >*eXgFx7O~.9vyrdB8|~ --kb6ނ`WRA^{<$fc16_Ly}TW;cc1vܼ7m֮)0By lX ;nmt>qvY'1BHe,R!+E.qK"Sʣ*iFXL$(࿄p08E"~UaSZoh,;`BAWt`ŷ6X.Gb -~ k'qS}Pdo &ع?1e:tLy A&굍?0~~箻wqYC<^ߟu*ׯ.1?i3]?|lΖ:;l5RqyxZcqZTx ߿nM^""""r >Ƙ1cuFE|٨ó Ǚtݲ(t&^6*im;E*dxMLP*ZF+^$c%‹LP`qA{bG-74j'Xկ,Wj|x.`\204 ͐:lEeDNi&\炜=$ހ<."}2ÊQ~]' K`Q=#xK Y4 ` &4'(n^tX ǘ yPgbFẋ_'3_ȣO;&v,ϖ)V7As`q _L=I~l[&NDDDbPN9xmɛ7M?l6O1֚i9f;ޞ Ig2/Լ "炊"2ew|r|؋E},Fqy=dc <~8_f2fJ\tbFD _6{:X[oێqxvҙ y{i+G;>ʝsғfjS3me1{pjV.]cm^NO΃.]7mnY~GI?/ԱgRuP4P'lu;]su[&:FL3ܸ :vvz^NR]8@y+xnN`( y Wj)3Y4f^KNS4=|㯦V:vqǧ[yxgc ^fN[lx ['Yp\xOkpsZ}BoeM] v,Mמ7-O+.s7ŶZמx65ٲ,Dtkgߛ𭷵}+i-?A~pړEDDDDD.T$3>d|tto_V; 1~eaԷg੿Op=Do4\{z..n jBaqRSF#~u΃]1B x<\;[2\RZC4,?;\""""""Ro?δZ~Mgn6nhz-D8G_e-s+,`SCmS>jq+n܅n)bH\C]{{އ >3?,ym_hd@/|`ǭnfCYҽmdGqvr֩,R!gsu ̓_#Ao  \^`< _?od""""""o 9 bYUO7*v8xOv/ݬZyԙZ߷h%d QEp]_6UXJ8lp"uKW[1rqSM7]$Ɇ?K16D}lɥ.Ĺ$eW5q͸$LĨJP!CW37Է~'ƳdCNYgB_Nqs|  Sh!>cvY\jbH\1`}-4,H: [{ q]+,x鯷qC3$h( yOFvK +85V&}R'/$O] ~Ʈ2!ÊeȶYn,9qƋӰtt!P[oɞPGNf bBo' Nd X }'{N;_)]9}-"""""r1Y츭gS!Gd;Y{=1ܙx ZGyL!CsP,ȡ_/&\3qp&2'k~yڅz;Yx*&Ts|^.j]T\[Bӯe&~~`WӇ|$y)̴A⃻X1a+N>x3e7KOŭ$LȅNE*bfɄEDDDDDf0񶵖+m'^3″=|x=Pkۙv…NΊK \L.B4='q"0d!6츹c*AlaM3&'}j)z1?с%Ap"xI.OǙWɢ&V /XlQ1YBz{W """"""5.+:g^ =t*`>Pοh@zһ9f`q#u{} ޛ|.ߝ4V*A#ODGW>l5 \<ml;K%\t 5*&TY8o3~blYY`soPy<9ǩqf޾z kpU-n޶mmC`Wyʃj/kv_r=G J7k?Lyrgm;9{TLX]B x 'c P= Yk9z,e1ܚGۂ%08zUB|۫ m7G%||`X21 N kRy*&T|Wy'2T/vpP? xE'3TW; SHR- """"""ex;w|71d"BNe^kun9AcSnc:q~-8/{N[DDDDD3οHbbH(b0e,w*?0ǡ.Řq\ߧ/-zDgw6y4{Efڮ/ ʾd[-=Ex>""""""Rq=Uvvɹ>|VЙGS4J]4Z*ڎ&su(w*7MOso:uK3\S6!s#;DDDDDD.DoT:E*H1FI7]RN|חliSAxtpJjs_S<K>oq?!7ZGc x[2ԙ,R!3/fN$Kv,H` [Z$}1h23vP P6!C>EDDDDD24u&TȼVcJ6gt#ec0g&^c&)_bzl>zUP E*Gd ̌ |# x5n!K\/;q) υ-Xүe1l.O3ٲx[2TLqu`QAKc I Ef҅fZo⿎3Em 2qL`C(3TG5DDDDDDd~:޾eMpÊ1.awi~]375=x9-j}y["i7Ń7hq(Qق-o* &3ָgix |dHs>y߂17mx@ML` 'dZsG|bH!ooȅM8T/v,"""""" k.힓Ab<x91KD7b-cXK h/@m0FojH9YzȒؐ]em]#]':g3Wa:E/Geե|* ʡH(TqXrEDDDDDdau[X3yPrƐ@@SOƐHnà8k 7ȡvX79PHY>5ɬܳ%Çz2ߝˣO`'2\wEb!E*D1'c̤Q""""""0vCԥ A,>>C`|Β M/XX:T/qxO Bێ]ͭ:cuqW3c9RDd ^ds>&T-X‹ q\ .᧎b!2),dńS'}z}r?S9M,$øBr`eH>~!V +ilvh+f~>ƳG񥬇-qbtg˝1VR{,Bqv/ U!$9EaC 6ǫKQvZw#߽n 3{Ň/nfwy2TH9n_rP5s dq̨P;S^3؁$O `QNeAx7R4d?E7 Q4uxt:X?CCGd )g͠ 1~ͮ1FLt[kq~]ȅun1y1H{ڈ:X 5ܐO `9礏2*<9R^;cw>c AB-C쉴MXp˪f_hT,A|s8ΑB7uP#ewSB-!ucDdz=e,˃& C^+>Y\Bvσos2?"4 CAt<@xQPpWWsYKρLC|l! kBo-ۍ9<L$R 󔊱͜;d].J喕)$U7.~/dzΪeX NY~Su.guK~iQx1 / Kcp:A?/<= of)t~6S_ah2/*&TH9nV%Rl5/qp"Z:p=A1|i KqKC1#'"2-Lw. O6>y|w!~ g*w|Xuh1!C_8bQM{Hśgb4?Y]a&̇"2i7"""""""2} n2[j#7bssdC{&~m}(l5_,q-uK ;K Sݣ9luq cެgL$ͫv}=|)_D~tdgF`CLd1n4S)}c.vY.nq#gEǫʺDDDDDD*}:vycXqyUrHg2cnqvL$XD 4R4:?A_6 j]ukqj1on wcY?A(n㳟1D#pp"6,|8x'}j# TōQe1Xk' ,RA*$<5@^eQ+5}ӭ4EDDDDD*QYy@P<6ZVIPtB2x@< 5\g~hOkgMK#qx=PLN38lss\s YpL A/g#~qwΧup *mr\b^׷P'䁇v6_̝IO2yOUDDDDD"|g@-# O͸4`"7nh,[ MNfx 冣+ 9KzO8e_`,E c-9ˆ%CeZbٷA!ڪL.pDBs1b}iu~MxlݼͩG],j0ɿT<]wv,"""""W7sFH199$i-R\fKߘO~y kN֓LlE"""""rAM[U jA4 b.ӯ&bظ΃]<=A~r4Judlǒ<=> ~i2x<6njf_{tY?WvC &b_n:v=ڡfR1ÇGD8+VL8T;A[H3Yp`0MzԪ6_O%>נEQK]gD[Z """""Z_@!KxT0}d}%Ed 6g<\;5uKp [oTD*DnF\tt>n֭oT"Z+z{+_{LQ"""""r~3櫛y)#1֭i|{c8ޛT8lv NHgᗺKM:Kc[;r[μqh-R9*&T;nZ9ޓlXks;'<Cqt+޿W7 =/RY9Ec:vCKƘ7nhyg2~_-c~_s6ADرEJ1"i7#F)eAdŃ1v*1{Xm7n/_3YBsqvRa[\DDDDDDΎo?δ+vE=_]\,R!8,x$aNHd81)Bs!"""""",b - oW"u&Tb.DDDDDDDBW5$z g(̭zdn~ogvз5,{ -w:Scʎv>ynkkƯ4;ܧH9ԙ,R!nԝ,""""""pr-?_7Ծ۝Yn@7Ir qk,?:S }ϻ6 o<SܲrG|-ó>]˧},"3Q1YTHYx 5޸Ǜ.~y?K16D},6$s.M7,dr5!/z\{OyTx(=3ŝ7}D1"  N¿wR]e[Xǩm,lqd)gX'R8| ɳާԙ,R!Yx 5}̑v\ 26ٚ,~t'k5Qk|L[|K)˧Z 8, Pj"S1YTHYxo_puM=?ͯI0r_F'b |to)2 \Tb.DDDDDDD|Z-v柰 ?nBoe2,^O6rQp)ۘgv6)7E*dxڍRUk!""""""7='q"0d!6츹̸Mp[.\,G}I1" 7v>w z:tb㦦/Ɖ+})JiY~%k)&bBy*œMCq9>N3Q>bOOv_<ƽߋM{лw}Cd R!y1Y>qvqqƾrgvrH77=K6J:ݸ/tq}o/{""""""r~x{U"䜏m W&x"ۛAHJBzm#lj{,ޯxyXvc {5NaHSǶNgS<>}e^Vz^MSVWNd QX6g3p ><ێT]Xk9Rwy+V&x6vܼt&C]]ǒcu>DDDDDDmլUrh> R!y?e+V${^_bF1؜,4q=5l!""""""E*Cd %1P1n8ZytEx3Z7}t՝wE)bOi{ SRp3e4YV!O-¸GC!4$bQHm8mkVbՌT=xD /:Qz,ْuNI{o}T~x '/mʧ 9w()),^U6N9~F}'ksNm9CH(,RDzcc >U3n!mP "Ѳܸ{y3ׯmIH(,R$cndn؄SP,gZy/^`Ƕ΢!""""""g'"ţ2"Ee7s.=)[_h<2˓T, 9{oW_u[1@`R}oCIܦhFm$ `ZaHUOM䬢"Ee7t ##SȈ% <;ߟad2Nj[ۈ';QsiCDDDDDD=۹ 2H6q ;NX"9wގ5͵"%EDnDDDDDDD Tm1O*3)IlO7CN|rS2YTBDDDDDD򍷭g ɕ$d-~.dJx.톬e$_o,R$==)*+cgz"""""""S|2,\bgo'۱#@0ycڏzvo廵qnv(|Ê%|oV"E2FEDDDDDD 'xێ|K$8: yd" 6З Y `1!CEElnǖ)Iؠ ,Das{&Ig(Y%~Lo+8Mu M;3 !ŝS߳N$}2 KcOzZʦf-">wuj]Y$ODdH),""""""Rxo1'\جDt3l\Xzi'|v c (G |;a{ d3lܟyQSƆK}Ŝi%P:Q2YHTBDDDDDDGO b3^R͂c\zvXW;XӬ{"7W%i<.9y^ eCrOG&C:b}i΄uṯ9 ܳpt@7OiX=WF>ݔL))mH|Lϒcʥ;1A088׳T^\̐i-CKSp\"9볳73ohyR[*~yadҴ1&iyGy$F4ϭo=׿_5eL.I}/BLEcF>%W'KЙRwcݸ|kCl"QlHPgr2r[ojz\]t r[v#"""""""omYIS. ɌXz.f(w绸:8OL-4';pa<)=KX ￟}`KH8:vygTY;a1Kk aܿ͂u\NW5L)")S{Z^\>/L08&=:gCa#QqpBrJ`#<+m7sYؒH Y,,O*zk24/k咾ַ=.!lF3'W|;Kh8,}F dm^^L))B=o{aB>!8!C4}O$;ɂ:TsŹ`1Iz1CO HtDwuga8/n͓q".7 m4k⌼+R7m&w}9&d-Ϥ3.0/2G?\8 :J$Ad"wY9B&|dap [ix>&h0:\&73G='.#^-K2|TPg<|o`qY$ӴMƧ0"=@gjIl%6c(7[qE5EHd/ޞgZ﨏o-28K ,{) /7!b^=W0rhjL)l.`=RD慹F]?[;`o:͆MӅZ*c1~eIpؽ zwħQF}re$mhsDsRv. رOPqe<`H,>_ƾcl.mw4ٙ: l"'|vD$Z}SI!,ݴňav7OSKJ^&.êM?̠cGKXd .g/ ~v}'x?o[~4!ec]]OqvlС*[z)5A'q48+M&CU${(GQ'8iA˾nwɘ0/otZKqYy |֑57>zR,^@O/~LO䜶6>97ȂYbͭl}a<kng9vH89 W-aH_/}Gsr=oX$ηna xGyyM7s=~""""""2jwT~;9>}ߟ,&Z %`\j 5-&h2L1d-b>}Z5)m\U1Z{:F9Fj1vqlxNL)S|{W+''pV.펝\H$ዷӗГʽ۷uR,1;dChc 2ᩧ[s_!mhR8h ֎% Cz8`B& JpHe "E2悗͕ u1;d}Fgpey;uRr3a-: +T/Mk~""""""RT-s.фńF_>;aBcFA h0!Nj&%3iZwj Ǚx#9җfKlzWױ$ȹ v:,%%}lZzze\",`Mٷ?O},"g)%E$e7&d0!C_&3\7M1̡pzuc"p:}Ov:\-7ױa}|ݻY3.x? 9̅Hńe v;&yleIݸ{&'6y.ؤzޜ[?[;޷of&\<`;vvR4}wJSl~ 2p?+uP]λpB/nm7W'+:^` D"a?dXy}L뮯eZ2F$敗ۙ;ﶝ |k^cmQd"ۉYkپʅ4YDDDDDDFHqhfHḧ́/yK5Dlg5{XDDDDDDfOH(,R$Zv3rI g÷nWʫ>9"""""""Ng>4 oG%EJ䜊htVuEDDDDDDfvO*oo E01;Rv[+|̏h;Y|Vxs}-=[[ĽT.2<>JMm/':'͑=)\w|OLihG+fZ|Wՙ_)~Xw||uW-\Luу-Hs& ,R4cn4;{[|顈)*d$?|vᏺ;IEdӗwb,}gI?F8PG$ ?d+?3}{V v>VmgYkd^2F㔞lI}&ýZy{dNrPK֤Tm8zGq雓Dݹ?Nߧ29cïgZ ?R'./'fѹA3EH3}r] S!Ɵty>L?wP\拷ֳ2S6 J#]yi͑nZG`~@o }(N~c} ;a0<0ӕqH+bHC'wgP=<)Irh⧏6?eXpwyif&IOOic[9P{ @~:5 _{zv%γDwCP2X>shRl85G].}56͑yk7^. ۤ0kc.bN1vJ>J(trIuUoE"E2~kxv&!#=eFUbyG#GTDD"sKbS#!˝4L\_[_j׃MXkٷkFV8ټO,+MDx[qEDDDDDNS̯c`d. \X{{#v|4fHwsM:C'r,{Ҫ$N{z?M9w&\$i`7q펝\ o/'59y}"xIlJ_۷uR,1;dChc NMJ$&ogZr 9>J9y  7{{^kOֲ'ok!ykqvK3ol孭xkQh^ ?6lO_|a8xs{D 2&iO%IugW+ ?[cIs&ehcs;=ބwiB0m-DCݼDl&3L)"ȱC 뛸jQR5eFyI?NDef5)}Z~_?Ikx{;O..o8W3|?'ײַyY(s7_OM;+ᒚv>_3UJqI!v=ΰn6ݽ+618e~bw80^+y|8OյsADDDDDDNSq6̾7NRCbҌcYfE;L) =|5%A'pZ6oi/(wϷsw+>zLQyfiyAk$rS2YHT"'slUqLа6zNWi~W^렺*w7^No:+_W]x]yG0uӽ+:^` D"a?dXy}򔞋^!mם[%] 6)S]|:peIjV$!wRrnhx;:XvQd } kn:꫷'UB;wȦ'[S(zJCDDDDDDFz|yG7f&\Tb&GF&>~o?2庵w5)#0>lk_<v~NԦmd"I!wo"Ee7"""""""x[xL)"-)<"šdHhٍH))%EDnDDDDDDD OHD.$Zv#"""r&9[()L)-9;lx@!pJa}*!"rQ-R<,R$cniș- Czi*QB._J=宋1u,"rP-R<,RDzc9*(ԡġ2ġRH8=#:__sLD43YHzzRTV0DDDD.xwuy\qfwSrpB#x+(wKF۬eʲ`2d-NbF {}a d-[n|-.ƿN¡3= 9ifHhw3; f~F3WDu"""r2!C~l;?+qDD)H֧yx>{ח&D2AACo|2gs8Ə'x;F97(,RD˲sSj-a=e.k鼈[O4dH9Go>=ev~cpdGg#љY;j= A(zi{ƌ#x_5?jwjŴ%ED˞|λ<.CMgN@t^DDDM6EDu}og%>N dtcV;d!0<x6Ocg.4_H]ruǔy]݊(?V c̮eu ~n~::2*,J;7؈c'F5ED˞vlu|&}cr e;̿_Ug7;Dot?M˻Og|\}W~S)f&I˞zYdMua}oL{Ow݀rxf[]ˑ4SsͭuC>O}W^ny7w̥qE9"dHdf57Q^fKu0Eg(IcW'k4R(H%1")>_|}AwCdÃMXkٷ ,^g8S{^""""""|m%K&.00b-{6 ![cpȰT̟Ec؇bрKɼ,#c+j>0A S*7u%H̅H)<ٚƿwH8L$O$,t\s&\$6eE۷uR,1;dChc vu"""269?o0%cB.gK]J.u8`n=2Ã&ds6Qܲ%g9|c+רr鿗HT{ܧXMad^x%+Fkv}k3 """]$_\jI*oO'&qED5Ջzǐ+ka`=Ci\¬8!-c'ʹ>_d{W$[kqpo7OZ\j|?i?9ǻm3$"E2S!?ohGvޱ27cݸ[y3E5? ""rR Jߟ,DyKEDܐog[OhN.pe#7py}c >ġ+,e &ӇZ9`[Nl4 ^%EFe. gZV+kngÃ~طZv׶*7;x]yG0uB{xclzY7| cڿLv] %̷\?S\fșvہEu,}i8t7=ͩ?.թ/YM:v.B},}y Zd"Etj%xqkolfYqX:~n= |̚Y}}iO&KoO֯<a$ojÃW|F,b㸊DDAG wq'EQ 9yudMuOǁ8Г_?9m~O2|W W,RrXRm\U1:amƧ!5ź7R,KL]7ޱS˹FDDD|6.x;PYKdg |,-Jf+,"EtÅo-\s"D.==3= 9M9()%EdlٍmQ2Y.e7"""""""mP2YHFDDDDDDo"Ee7"""""""x[xgz"-)|e]v1_efCnKVs##"Ee7"""""""o&>ǦK$]gH9),R$Zv#""""""Rxa&f=Ƙsw{Ad(,RD*s!""""""Rxۙѯ'3"%EDe.DDDDDDD /x2yFl6]fdt!~9te 2,i>"[v"""""""o퐛}&8Xfs'5[re٢(.flgUC Cd"R"YDDDDDD-Q% pcc{W{q9>cG|na0wTw<^Jyi7r"+jyEA \\LF&=zi0|[ڸ"FIUb9n\]GiKiˍ;M;zÔ8\r?2f:ooxIKJ/wq~vbqeņ+"QV+~@`ީ>Pj AC 8Qk'}f,qזnptO?\>%ER,R$*s1Y45+5v6o⭃ivλpB?uk9қ& y,S^flf&x71Ilxp!SJcB;6VHr{lbMu5\i>2Id^MsyqEDDDDDdj ~?Y;0Sl ݜq L2|[2CeE'?%vLg_k_*{_rn6 '\~XZc[APYD)S(3"R"ylzi`uR;oLQ0Yl(IcW'k4&ۅSP$%q6oic xGo:ͦǛY{{[#ٌ8N7fTۅ;//7P Ow6'ޮ3&WZ%C.>|rb'~6wD=&'jkf9hd(_/+ICw`<1e1J]gן9f8Qc|^;:M"[do"R$==)*+g\rAYsDq"0D2׬m,K`/IQbanހ[9M>d$A\0,q i~'WA[ϵxE&L(}qQJG 14%=>\ zό:fleӗ_f_kܳp"S),R$*sQ}[n㖛ذG]],O|$fwO#}m"{W &dXǼ[DDDDDDfwaZ0җɭ B duq8C5>.xߒ 0!D|bpdpMʙe۟GIx|OoI{g,\\΍jٽ7S3_|}M<'cWj%ر䱵;'_EDDDDD򎷃cqg(} \:|tyBhZ4'аlaae, O\"E2&^Akoo  lq'Xe2\w}-WrUE!SO9TWg<8<϶Q] o`zxlbN&,ϷaBfRٌ[DDDDDD.x;L0d-#xX<7d0s~c$Iǡqp|y*[qUUdWnm^/r ğZ6CdJ&\o:^|pV.OR( @yK8;$/9cNzreILȰ:< V$ \+[jF0C:湛,}!͗Eaed}Rٟ\fO9J~wM l:Cd"[v[-Sup:2L}y./~};Ϸ}rK~r|,Op{ұ/֐vQa1; *+\V'0pQEB@i0#(&pi.i<b֎\9_),R$*s!""""""Rx=l>Ӭ@9$rnR2YHfpx9YޝK|"sљȅDol"""""""x[8L)ԙyGH(,R$cnDDDDDDDpo˙xgz EdHiٍH)ޖbjC#X6YIˊȉoōa4+])kon2CQLlbJbT]+kE)x|^&P^*RKqN+%EDˊޅo+(b)Ο}VEV._I^,.7j}Xșt͂ WNt&F$=-<~zVQ3+>b7y6-M-J&х&""""""R,$kݥ=?k3PU??k4r0tOg w}^ , Fd"9\ """"""=Y8ṳ1>_U\?^}GQhA䓉,rԣ>wg?wQwߣq{↧'K`y J;bwFw60.jZ"UZ0+UaV\#Llc[9]GHB~0~^וˋssIfg)(+w̛ZKҰ3(3!NS_9]N4q<&hfr`}0 W, i?uyb}lmFx[ ˀ6 Xoş :\_9ݫS4DPAn)ɰJG(~Z]?癘ka^yS#ԿR)S|}9F:P,Cަ@YDDDDDDF'o=tlc{>mկTS8m?3R9 _cD7q0N 1orak:Vwg-pMـc;$fpL|'Jx3F1fO|?r[9\^rUfI嵆&O3yşi㬫LS G#\}vb@[;DQD rʺnQanL5 3 VrMI-oLKhKgcQIZ85>/L( +qvD>,v<"""""""ҷcj6]J>[%MӰ<|c6)|.AmmTekφ5\m kpOWORffJ;ƫ)>[ōgG~g7L<'~0Hj߷TcB,eݟWpZ:)V$7pr ߚex-Nu1 V1POO$t 7cck6]Veq͙6̛ߞfWc%,- jN)Ѱ'dWB=3wu$:dWƁ<0@InNO{'gLeݓSx`]k;bVwu-<_8{nP<6Nןc߮$\cӰ0BxHs noXr}3Tףdyj9+ζX`Io9Icٯ"9eEDDDDDDzj3>~ٝqCko4PXPm~/*1:۞хWyR~H6鱰zFscC6?_P?tɳy'rMG1|}7{F8{ QmUܾEaN`o?1^l6:Qgq.` K \XW>((6obo;|Ȩ0Y$GnDDDDDDDF\kP 4Sa#4o[^l{[%j;P=wq5tjoM*Sjظy{~qj}ֿQky5<2WcXil L~abv\7/ɖ' xnZ0HfLCc$7L\OLy 5oY6!a+d@aȇͨH{C|>[OxM{2ԧ!t:&yEyӖLٶ]<^}?oO^,kQdO] 5Χ[fR;WChZsIJV`A.KUPkG+Mcab㴸4uoגj1 -ϼ3`_'H8gީvo9=L䪗MzGmގe|i2*pŬycYɷ2LNFa_ Ɣqe\3@%ڼzn,K^.ryOszn6kJ}_Gȴ̝_`{L[. sKk3k @aǣ<>MDr$8Ö J8ͿHS8֤aOK"\bʪPnsqz>asr§02l^|0_-B`:.\RYBKc_)#w\ץ)622z+;+a2ytN`_ҋ2\fp:Q7/YaHNɅ )m.R8dڌ;w$hn0ƧX0Şm^c1~9/]7pɜ3Lzgv[a Pzb}lDU,G4,ݎGaxGL&ȑvАٶ)oUo 1&0~RL ĚdNZS)`FΏ,' \r;20P,""""kC:M9\dތ^'|Xtv3۶']d,[^u}o. o_v s)f'TǿdJyP\9lvU7%gq)3Mpm)lҝ_&oI:޻%Rc$gmJr?&QlפxYLG3${wO{&+Lr[?tuh?M6̠x[K:^zifH %~V_YAWm7W0l?4aR) Z69L\[}|{/i} S`b)jbr9u]!YA-gpŭ>d3x(uѩx;xY\6YB}Ú!*P9M9C6"94 [dl[^ m`&}Fiu 14v1be4ܻzLIOK4JnbXVǥ!msrYW( anHO\v9oLdGZl^]M2tknzO^3ln1wh]sVx0^^ Or/Y5W3c |`͂ebk2lݕag oP쳸@ b#e1mBxN 2oJv2qZ9OP,fFy *\Uq!L. LŝTQ6.X/gWd{jZN9 3\UKwbU9umeR<Ň2}EDDDDDJe.Drc5q6SL~w+֮9e8mYWd[b2gC!x;#"""""G'Ϧ^K"C9sG;:3qi'0S\ʞFmGqNfEwvDr5gO HkIv-fޞb]TrygLD֏%2RMe.DrDOGTk0oqiɰ(m"""""[{}%_~C<5xiܞdezx{uaNz{_02n^]+r,Wo ErX |#q3}?xp\?;DhS5ϭ㡽.&Kʸ(+.֭`"""""r(Ϥ^jȼQə e0nmxn {pR- EǢSd$d՝d9VU*C)q!|S' B5v, 1x{|Pr" 9.< 0MMQ*KT3Y$Gn3xGeYqCo#흯&9n?i6rx"9tɕg`O^06VbQޢm<`vV)2N3Erd$V9|27rx~xOAm{USϣմ/^qN1y)2r'rL[攍q&픈H_`8il iߝ;, Qc9n]4lD_v$od\ԬUDDDDDDduMzb},mx9eY?h0&h,saꏈtvqgF՛!]DDDDDDDOmP,#\]VDDDDDDD:h-; ErErgk"FDDDDDD$4ɍ~dp)]]]VDDDDDDD"g.EO6x[$wT3YDDDDDDDDDD0YDDDDDDDDDD0YDDDDDDDDDD0YDDDDDDDDDD0YDDDDDDDDDD0YDDDDDDDDDD0YDDDDDDDDDD0YDDDDDDDDDD0YDDDDDDDDDD0YDDDDDDDDDD0YDDDDDDDDDD0YDDDDDDDDDDw; """"""""""Oz[ab0мfָ\&Ƞ&Ƞ&Ƞ&Ƞ&Ƞ&Ƞ&Ƞ&Ƞ&Ƞ&Ƞ&Ƞ&Ƞ&Ƞ&Ƞ&ȠzpێD!""""""""""Y0x 9O<c#LvgD"""""""""""1R9o@;i\DDDDDDDDDDry="zZY>"-q