pax_global_header00006660000000000000000000000064147673012170014522gustar00rootroot0000000000000052 comment=53be5b207c899e3393e5e3702d66fe315eb73a07 Quaternion-0.0.97.1/000077500000000000000000000000001476730121700141035ustar00rootroot00000000000000Quaternion-0.0.97.1/.clang-format000066400000000000000000000163701476730121700164650ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2019 Project Quotient # SPDX-License-Identifier: LGPL-2.1-only # # 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. # This file assumes ClangFormat 18 or newer --- Language: Cpp BasedOnStyle: WebKit #AccessModifierOffset: -4 AlignAfterOpenBracket: Align #AlignArrayOfStructures: None # As of ClangFormat 14, Left doesn't work well #AlignConsecutiveAssignments: None #AlignConsecutiveDeclarations: None #AlignConsecutiveMacros: None AlignConsecutiveShortCaseStatements: Enabled: true AlignEscapedNewlines: Left AlignOperands: Align #AlignTrailingComments: false #AllowAllArgumentsOnNextLine: true #AllowAllParametersOfDeclarationOnNextLine: true AllowBreakBeforeNoexceptSpecifier: Always AllowShortBlocksOnASingleLine: Empty AllowShortCaseLabelsOnASingleLine: true #AllowShortCompoundRequirementOnASingleLine: true #AllowShortEnumsOnASingleLine: true #AllowShortFunctionsOnASingleLine: All #AllowShortIfStatementsOnASingleLine: Never #AllowShortLambdasOnASingleLine: All #AllowShortLoopsOnASingleLine: false #AlwaysBreakAfterReturnType: None #AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: Yes # To be replaced with BreakTemplateDeclarations AttributeMacros: - Q_IMPLICIT #BinPackArguments: true #BinPackParameters: true #BitFieldColonSpacing: Both BraceWrapping: # AfterCaseLabel: false # AfterClass: false # AfterControlStatement: Never # AfterEnum: false # AfterExternBlock: false AfterFunction: true # AfterNamespace: false # AfterStruct: false # AfterUnion: false # BeforeCatch: false # BeforeElse: false BeforeLambdaBody: true # BeforeWhile: false # IndentBraces: false SplitEmptyFunction: false SplitEmptyRecord: false SplitEmptyNamespace: false BracedInitializerIndentWidth: 2 # Initializer padding inhibits this, making indentation inconsistent #BreakAdjacentStringLiterals: true #BreakAfterAttributes: Leave #BreakAfterReturnType: Automatic # ClangFormat 19; default value anyway BreakBeforeBinaryOperators: NonAssignment #BreakBeforeConceptDeclarations: Always BreakBeforeBraces: Custom #BreakBeforeTernaryOperators: true #BreakConstructorInitializers: BeforeComma #BreakFunctionDefinitionParameters: false # ClangFormat 19; default value anyway #BreakInheritanceList: BeforeColon #BreakStringLiterals: true #BreakTemplateDeclarations: Multiline # As of ClangFormat 19, has unintended formatting side-effects ColumnLimit: 100 #QualifierAlignment: Leave # ClangFormat 14 - except 'Leave', updates whole files #CompactNamespaces: false #ConstructorInitializerIndentWidth: 4 ContinuationIndentWidth: 2 Cpp11BracedListStyle: true #DerivePointerAlignment: false #EmptyLineAfterAccessModifier: Never EmptyLineBeforeAccessModifier: LogicalBlock FixNamespaceComments: true IncludeBlocks: Regroup IncludeCategories: - Regex: '["]' Priority: 16 - Regex: '^>$GITHUB_OUTPUT echo "reftype=$type" >>$GITHUB_OUTPUT Build: runs-on: ${{ matrix.os }} needs: Prepare strategy: fail-fast: false matrix: os: [ ubuntu-24.04, macos-latest, windows-latest ] qt-version: [ 6 ] override-compiler: [ '', GCC ] # Defaults: MSVC on Windows, Clang elsewhere composition: [ own-quotient, static, dynamic ] exclude: # Unsupported combinations - os: windows-latest composition: dynamic - os: macos-latest composition: dynamic - os: windows-latest override-compiler: GCC - os: macos-latest override-compiler: GCC include: - os: ubuntu-24.04 composition: own-quotient check: appstream # Use one of faster paths for validation - os: ubuntu-24.04 # Use the variation with external libQuotient to do CodeQL analysis # (libQuotient is analysed in its own repo) composition: dynamic check: codeql env: QTKEYCHAIN_REF: 0.14.3 QUOTIENT_REF: 0.9.x VERSION: ${{ needs.Prepare.outputs.version }} steps: - uses: actions/checkout@v4 with: submodules: ${{ matrix.composition == 'own-quotient' }} - name: Install Qt (non-Linux) if: "!startsWith(matrix.os, 'ubuntu')" uses: jurplel/install-qt-action@v4.1.1 with: version: "6.6" cache: true cache-key-prefix: Qt modules: 'qtmultimedia' tools: "tools_ninja${{ startsWith(matrix.os, 'windows') && ' tools_opensslv3_x64' || '' }}" # Install on Linux via apt to test against Qt coming from the package manager - name: Install dependencies (Linux) if: startsWith(matrix.os, 'ubuntu') run: | sudo apt-get -q update sudo apt-get -qq install libolm-dev ninja-build \ qt6-declarative-dev qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \ qt6-l10n-tools qml6-module-qtquick-controls qt6-multimedia-dev qtkeychain-qt6-dev - name: Setup environment run: | if [[ '${{ matrix.override-compiler }}' == 'GCC' ]]; then echo "CC=gcc" >>$GITHUB_ENV echo "CXX=g++" >>$GITHUB_ENV elif [[ '${{ runner.os }}' == 'Linux' ]]; then echo "CC=clang" >>$GITHUB_ENV echo "CXX=clang++" >>$GITHUB_ENV fi echo "CMAKE_ARGS=-GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo \ ${{ runner.os != 'Linux' && '-DCMAKE_MAKE_PROGRAM=$IQTA_TOOLS/Ninja/ninja' || '' }} \ -DCMAKE_PREFIX_PATH=~/.local \ ${{ runner.os == 'macOS' && '-DOPENSSL_ROOT_DIR=`brew --prefix openssl`' || runner.os == 'Windows' && '-DOPENSSL_ROOT_DIR=$IQTA_TOOLS/OpenSSLv3/Win_x64/' || '' }} \ -DBUILD_SHARED_LIBS=${{ matrix.composition == 'dynamic' }}" \ >>$GITHUB_ENV - name: Setup MSVC environment uses: ilammy/msvc-dev-cmd@v1 if: startsWith(matrix.os, 'windows') with: arch: x64 - name: Get, build and install QtKeychain if: "!startsWith(matrix.os, 'ubuntu')" run: | git clone --depth=1 -b $QTKEYCHAIN_REF https://github.com/frankosterfeld/qtkeychain cd qtkeychain cmake -S . -B build $CMAKE_ARGS -DBUILD_WITH_QT6=ON -DCMAKE_INSTALL_PREFIX=~/.local cmake --build build --target install if [[ '${{ matrix.composition }}' == 'dynamic' ]]; then QTKEYCHAIN_SO_PATH=$(dirname $(find ~/.local/lib* -name 'libqt?keychain.so')) test -n "$QTKEYCHAIN_SO_PATH" fi - name: Get, build and install Olm if: "!startsWith(matrix.os, 'ubuntu')" run: | git clone --depth=1 https://gitlab.matrix.org/matrix-org/olm.git cmake -S olm -B olm/build $CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=~/.local cmake --build olm/build --target install - 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 -DCMAKE_PREFIX_PATH=~/.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: matrix.check == 'codeql' uses: github/codeql-action/init@v3 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 cmake -LA -S $GITHUB_WORKSPACE -B build $CMAKE_ARGS -DDEPLOY_VERBOSITY=$DEPLOY_VERBOSITY \ -DCMAKE_INSTALL_PREFIX=$INSTALL_PATH echo "FULL_INSTALL_PATH=$DESTDIR$INSTALL_PATH" >>$GITHUB_ENV - name: Build and install Quaternion run: cmake --build build --target install - name: Perform CodeQL analysis if: matrix.check == 'codeql' uses: github/codeql-action/analyze@v3 - name: Validate installation (Linux) if: startsWith(matrix.os, 'ubuntu') run: | find $FULL_INSTALL_PATH -name 'quaternion_*.qm' LD_LIBRARY_PATH=$DEP_SO_PATH \ $FULL_INSTALL_PATH/bin/quaternion -platform offscreen --version if [[ '${{ matrix.check }}' == 'appstream' ]]; then sudo apt-get -qq install flatpak flatpak install --user -y https://flathub.org/repo/appstream/org.freedesktop.appstream-glib.flatpakref flatpak run org.freedesktop.appstream-glib validate $FULL_INSTALL_PATH/share/metainfo/*.appdata.xml fi - name: Make a package if: matrix.composition == 'own-quotient' && matrix.override-compiler == '' id: package env: DEPLOY_VERBOSITY: 1 run: | PACKAGE_STEM=quaternion-${{ needs.Prepare.outputs.version }} case ${{ runner.os }} in Linux) sudo apt-get -qq install appstream libgstreamer-plugins-base1.0.0 \ libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 \ libxcb-render-util0 libxcb-shape0 libxcb-xinerama0 libxkbcommon-x11-0 \ libfuse2 # Make sure OpenSSL ends up in AppImage - linuxdeploy doesn't # copy it automatically cp /usr/lib/*/libssl.so.* $DESTDIR/usr/lib/ 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 # NB: $OUTPUT is both used in this script further below and # propagated by linuxdeploy to appimagetool as the target path export OUTPUT=$PACKAGE_STEM.AppImage QMAKE=/usr/bin/qmake${{ matrix.qt-version }} \ QML_SOURCES_PATHS=$GITHUB_WORKSPACE/client/qml \ linuxdeploy/linuxdeploy-x86_64.AppImage --appdir $DESTDIR \ --plugin qt --output appimage ;; macOS) OUTPUT=$PACKAGE_STEM.dmg cmake --build build --target image mv build/quaternion.dmg $OUTPUT ;; Windows) ls -l $FULL_INSTALL_PATH/quaternion.exe # Fail if it's not there rm -rf $FULL_INSTALL_PATH/{include,lib/cmake,share,qmltooling} OUTPUT=$PACKAGE_STEM.zip 7z a $OUTPUT $FULL_INSTALL_PATH ;; esac find $OUTPUT -size +10M echo "path=$OUTPUT" >>$GITHUB_OUTPUT - name: Store artefacts if: steps.package.outputs.path != '' uses: actions/upload-artifact@v4 with: name: quaternion-${{ env.VERSION }}-${{ runner.os }} path: ${{ steps.package.outputs.path }} retention-days: 7 Publish: runs-on: ubuntu-latest needs: [ Prepare, Build ] strategy: fail-fast: false matrix: type: [ macOS, Linux, Windows ] steps: - name: Retrieve artefacts id: get-package uses: actions/download-artifact@v4 with: name: quaternion-${{ needs.Prepare.outputs.version}}-${{ matrix.type }} path: package - 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: "`find package -name '*.*'`" # Globs don't seem to work: https://github.com/cloudsmith-io/action/issues/21 name: ${{ matrix.type }} summary: CI builds of Quaternion, ${{ matrix.type }} 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' && matrix.type != 'Linux' uses: ncipollo/release-action@v1 with: token: ${{ secrets.GITHUB_TOKEN }} commit: dev artifacts: ${{ steps.get-package.outputs.download-path }}/* # It tends to false-prerelease things but that's better than false-release them prerelease: ${{ contains(needs.Prepare.outputs.version, '-') }} allowUpdates: true # Create as a draft, update without touching the draft flag draft: true omitDraftDuringUpdate: true # Never write name and body - this action is to upload files only name: "" omitNameDuringUpdate: true body: "" omitBodyDuringUpdate: true Quaternion-0.0.97.1/.gitignore000066400000000000000000000003201476730121700160660ustar00rootroot00000000000000build build_dir .kdev4 .directory CMakeLists.txt.user* .idea flatpak/app flatpak/.flatpak-builder flatpak/repo CMakeCache.txt cmake_install.cmake Makefile quaternion_autogen/ .cmake/ .qmlls.ini .DS_Store Quaternion-0.0.97.1/.gitmodules000066400000000000000000000001121476730121700162520ustar00rootroot00000000000000[submodule "lib"] path = lib url = ../../quotient-im/libQuotient.git Quaternion-0.0.97.1/.lgtm.yml000066400000000000000000000010761476730121700156530ustar00rootroot00000000000000path_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.97.1/BUILDING.md000066400000000000000000000252161476730121700156300ustar00rootroot00000000000000# Building and Packaging Quaternion [![license](https://img.shields.io/github/license/quotient-im/quaternion.svg)](https://github.com/quotient-im/Quaternion/blob/dev/LICENSES/GPL-3.0-or-later.txt) ![CI Status](https://img.shields.io/github/actions/workflow/status/quotient-im/Quaternion/ci.yml) ![](https://img.shields.io/github/commit-activity/y/quotient-im/libQuotient.svg) [![](https://img.shields.io/matrix/quaternion:matrix.org.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). (Why `LIBQMC`, you ask? Because the old name of libQuotient was libQMatrixClient and this particular variable still wasn't updated. This might be the last place using the old name.) ### 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 Bookworm; Fedora 39 or CentOS Stream 9; OpenSUSE Leap 15.6; Ubuntu 24.04 (noble) - Qt 6.4 or newer (either Open Source or Commercial) - CMake 3.16 or newer (from your package management system or [the official website](https://cmake.org/download/)) - A C++ toolchain with solid C++20 support and elements of C++23: - GCC 13 (Windows, Linux, macOS), Clang 16 (Linux), Apple Clang 15 (macOS) and Visual Studio 2022 (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, development files for libQuotient 0.9.2 or newer (from your package management system), or prebuilt libQuotient (see "Getting the source code" above); libQuotient 0.8.x is _not_ compatible with any Quaternion 0.0.97 release. - libQuotient dependendencies (see lib/README.md): - [Qt Keychain](https://github.com/frankosterfeld/qtkeychain) - libolm 3.2.5 or newer (the latest 3.x strongly recommended) - OpenSSL 3.x (the version Quaternion runs with must be the same as the version used to build Quaternion - or libQuotient, if libQuotient is built/installed separately). #### 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 qt6-declarative-dev qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools qml6-module-qtquick-controls qt6-multimedia-dev qtkeychain-qt6-dev libolm-dev libssl-dev ``` On Fedora, the following command should be enough for building and running: ```bash sudo dnf install cmake qt6-qtdeclarative-devel qt6-qtbase-private-devel qt6-qtmultimedia-devel qt6-qttools-devel qtkeychain-qt6-devel libolm-devel openssl-devel ``` #### macOS `brew install qt qtkeychain libolm openssl` should get you Qt 6, the matching build of QtKeychain, and good versions of E2EE dependencies. You have to point CMake at the installation locations for your libraries, e.g. by adding `$(brew --prefix qt)` and similar for other libraries to the first cmake invocation, as follows: ```bash # if using in-tree libQuotient: cmake .. -DCMAKE_PREFIX_PATH="$(brew --prefix qt);$(brew --prefix qtkeychain)$(brew --prefix libolm);$(brew --prefix openssl)" # or otherwise: cmake .. -DCMAKE_PREFIX_PATH="/path/to/libQuotient;$(brew --prefix qt);$(brew --prefix qtkeychain)$(brew --prefix libolm);$(brew --prefix openssl)" ``` #### 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 Qt 6, using their official installer. 1. Make sure CMake knows about Qt and the toolchain - the easiest way is to run `qtenv*.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` also helps. 1. Get and build [Qt Keychain](https://github.com/frankosterfeld/qtkeychain). 1. Install E2EE dependencies as described in [lib/README.md](lib/README.md), section "Building the library". ### Build In the root directory of the project sources: ```bash mkdir build_dir cd build_dir cmake .. # Pass -D if needed, see below 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). ### Install In the root directory of the project sources: `cmake --build build_dir --target install`. ### Building as Flatpak If you run Linux and your distribution supports flatpak, you can easily build and install Quaternion as a flatpak package. Make sure to have flatpak-builder installed and then do the following: ```bash # Optionally, get the source code if not yet git clone https://github.com/quotient-im/Quaternion.git --recursive cd Quaternion/flatpak ./setup_runtime.sh ./build.sh flatpak --user install quaternion-nightly io.github.quotient_im.Quaternion ``` Whenever you want to update your Quaternion package, do the following from the flatpak directory: ```bash ./build.sh flatpak --user update ``` Be mindful that since Quaternion 0.0.97 the Flatpak app-id has changed: before it used to be `com.github.quaternion`, now it's `io.github.quotient_im.quaternion`, to align with [Flathub verification rules](https://docs.flathub.org/docs/for-app-authors/verification/). Normally, Flatpak should seamlessly handle an upgrade; if it doesn't, make an issue at either the main Quaternion repo (https://github.com/quotient-im/Quaternion) or at the Quaternion Flatpak repo (https://github.com/flathub/io.github.quotient_im.Quaternion) - we'll route it as needed. ## 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 Qt5Something - make sure that your `CMAKE_PREFIX_PATH` actually points to the location where Qt is installed and that the respective development package is installed (hint: check which package provides `cmake(Qt5Widgets)`, replacing `Qt5Widgets` with what your error says). 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 ([like in this issue, e.g.](https://github.com/quotient-im/Quaternion/issues/185)), double-check that you don't mix different versions of Qt. If you need those packages reinstalling them may help; but if you use that other Qt version by default to build other projects, you have to explicitly pass the location of the non-default Qt installation to CMake (see notes about `CMAKE_PREFIX_PATH` in "Building"). See also the Troubleshooting section in [README.md](./README.md) Quaternion-0.0.97.1/CMakeLists.txt000066400000000000000000000276431476730121700166570ustar00rootroot00000000000000CMAKE_MINIMUM_REQUIRED(VERSION 3.16) if (POLICY CMP0092) cmake_policy(SET CMP0092 NEW) endif() set(IDENTIFIER "io.github.quotient_im.Quaternion") set(COPYRIGHT "Copyright © The Quotient Project contributors") project(quaternion VERSION 0.0.97.1 LANGUAGES CXX) if(UNIX AND NOT APPLE) set(LINUX 1) endif(UNIX AND NOT APPLE) include(CheckCXXCompilerFlag) if (WIN32) if (NOT CMAKE_INSTALL_BINDIR) set(CMAKE_INSTALL_BINDIR ".") endif() else() include(GNUInstallDirs) include(cmake/ECMInstallIcons.cmake) endif(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(CMAKE_AUTORCC 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 error=return-void) # Switch these on CHECK_CXX_COMPILER_FLAG("-W${FLAG}" W${FLAG}_SUPPORTED) mark_as_advanced(W${FLAG}_SUPPORTED) if (W${FLAG}_SUPPORTED AND NOT CMAKE_CXX_FLAGS MATCHES "W(no-)?${FLAG}($| )") add_compile_options(-W${FLAG}) endif () endforeach () foreach (FLAG unused-parameter subobject-linkage) # Switch these off CHECK_CXX_COMPILER_FLAG("-Wno-${FLAG}" Wno-${FLAG}_SUPPORTED) mark_as_advanced(Wno-${FLAG}_SUPPORTED) if (Wno-${FLAG}_SUPPORTED AND NOT CMAKE_CXX_FLAGS MATCHES "W(no-)?${FLAG}($| )") add_compile_options(-Wno-${FLAG}) endif () endforeach () endif() set(QtMinVersion "6.4") set(QUOTIENT "QuotientQt6") set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15") string(REGEX REPLACE "^(.).*" "Qt\\1" Qt ${QtMinVersion}) # makes "Qt" # Find the libraries find_package(${Qt} ${QtMinVersion} REQUIRED Widgets Network Quick Gui Multimedia QuickControls2 QuickWidgets LinguistTools) # ${Qt}_Prefix is only used to show Qt path in message() # ${Qt}_BinDir is where all the *deployqt tools reside if (QT_QMAKE_EXECUTABLE) get_filename_component(${Qt}_BinDir "${QT_QMAKE_EXECUTABLE}" DIRECTORY) get_filename_component(${Qt}_Prefix "${${Qt}_BinDir}/.." ABSOLUTE) else() get_filename_component(${Qt}_BinDir "${${Qt}_DIR}/../../../bin" ABSOLUTE) get_filename_component(${Qt}_Prefix "${${Qt}_DIR}/../../../.." ABSOLUTE) 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() set(QUOTIENT_FORCE_NAMESPACED_INCLUDES 1) if ((NOT DEFINED USE_INTREE_LIBQMC OR USE_INTREE_LIBQMC) AND EXISTS ${PROJECT_SOURCE_DIR}/lib/Quotient/util.h) add_subdirectory(lib) if (NOT DEFINED USE_INTREE_LIBQMC) set (USE_INTREE_LIBQMC 1) endif () endif () if (NOT USE_INTREE_LIBQMC) find_package(${QUOTIENT} 0.9.2 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 () message(STATUS) message(STATUS "== Quaternion ${quaternion_VERSION} configuration summary ==") 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 ${${Qt}_VERSION} at ${${Qt}_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 () message(STATUS) # Windows, this is a GUI executable; OSX, make a bundle add_executable(${PROJECT_NAME} WIN32 MACOSX_BUNDLE) # Set up source files target_sources(${PROJECT_NAME} PRIVATE client/quaternionroom.cpp client/quaternionroom.h client/htmlfilter.cpp client/htmlfilter.h client/activitydetector.cpp client/activitydetector.h client/dialog.cpp client/dialog.h client/logindialog.cpp client/logindialog.h client/networkconfigdialog.cpp client/networkconfigdialog.h client/roomdialogs.cpp client/roomdialogs.h client/dockmodemenu.cpp client/dockmodemenu.h client/mainwindow.cpp client/mainwindow.h client/roomlistdock.cpp client/roomlistdock.h client/userlistdock.cpp client/userlistdock.h client/accountselector.cpp client/accountselector.h client/kchatedit.cpp client/kchatedit.h client/chatedit.cpp client/chatedit.h client/timelinewidget.cpp client/timelinewidget.h client/chatroomwidget.cpp client/chatroomwidget.h client/systemtrayicon.cpp client/systemtrayicon.h client/profiledialog.cpp client/profiledialog.h client/verificationdialog.h client/verificationdialog.cpp client/models/messageeventmodel.cpp client/models/messageeventmodel.h client/models/userlistmodel.cpp client/models/userlistmodel.h client/models/roomlistmodel.cpp client/models/roomlistmodel.h client/models/abstractroomordering.cpp client/models/abstractroomordering.h client/models/orderbytag.cpp client/models/orderbytag.h client/desktop_integration.h client/logging_categories.h client/main.cpp 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) QT_CREATE_TRANSLATION(quaternion_QM ${quaternion_en_TS} client/ client/qml/ OPTIONS -no-obsolete) add_custom_target(trbase DEPENDS ${quaternion_en_TS}) QT_ADD_TRANSLATION(quaternion_QM 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 ) target_sources(${PROJECT_NAME} PRIVATE ${quaternion_QM}) if(WIN32) set(quaternion_WINRC quaternion_win32.rc) set_property(SOURCE ${quaternion_WINRC} APPEND PROPERTY OBJECT_DEPENDS ${PROJECT_SOURCE_DIR}/icons/quaternion.ico ) target_sources(${PROJECT_NAME} PRIVATE ${quaternion_WINRC}) endif() if(APPLE) 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) target_sources(${PROJECT_NAME} PRIVATE ${${PROJECT_NAME}_MAC_ICON}) endif(APPLE) target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_23) 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 (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} ${Qt}::Widgets ${Qt}::Quick ${Qt}::Qml ${Qt}::Gui ${Qt}::Network ${Qt}::QuickControls2 ${Qt}::QuickWidgets) set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 23 CXX_EXTENSIONS OFF VISIBILITY_INLINES_HIDDEN ON CXX_VISIBILITY_PRESET hidden ) # 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 install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} BUNDLE DESTINATION . ) 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 \"${${Qt}_BinDir}/windeployqt\" --verbose ${DEPLOY_VERBOSITY} --no-multimediaquick --no-test --qmldir \"${QML_DIR}\" \${CMAKE_INSTALL_PREFIX}\${CMAKE_INSTALL_BINDIR} RESULT_VARIABLE WDQ_RETVAL ) if (WDQ_RETVAL) message(FATAL_ERROR \"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) execute_process( COMMAND "${${Qt}_BinDir}/qmake" -query QT_INSTALL_TRANSLATIONS OUTPUT_VARIABLE _qt_translations_dir OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY ) set(MACDEPLOYQT_ARGS ${PROJECT_NAME}.app -dmg -qmldir="${QML_DIR}" -verbose=${DEPLOY_VERBOSITY}) add_custom_target(image COMMAND mkdir ${PROJECT_NAME}.app/Contents/Translations COMMAND install "${_qt_translations_dir}/qtbase_*.qm" "${_qt_translations_dir}/qtdeclarative_*.qm" "${_qt_translations_dir}/qtmultimedia_*.qm" ${PROJECT_NAME}.app/Contents/Translations COMMAND "${${Qt}_BinDir}/macdeployqt" ${MACDEPLOYQT_ARGS} DEPENDS ${PROJECT_NAME} WORKING_DIRECTORY ${PROJECT_BINARY_DIR} COMMENT "Running ${MACDEPLOYQT} with args: ${MACDEPLOYQT_ARGS}" ) endif(APPLE) Quaternion-0.0.97.1/COPYING000066400000000000000000001034171476730121700151440ustar00rootroot00000000000000GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright © 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.97.1/ISSUE_TEMPLATE.md000066400000000000000000000027621476730121700166170ustar00rootroot00000000000000 ### 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.97.1/LICENSE.spdx000066400000000000000000000010601476730121700160620ustar00rootroot00000000000000SPDXVersion: SPDX-2.3 DataLicense: CC0-1.0 PackageName: Quaternion PackageSupplier: Organization: The Quotient project PackageDownloadLocation: git+https://github.com/quotient-im/Quaternion.git FilesAnalyzed: false PackageHomePage: https://github.com/quotient-im/Quaternion PackageLicenseInfoFromFiles: GPL-3.0-or-later PackageLicenseInfoFromFiles: LGPL-2.1-only PackageLicenseInfoFromFiles: LGPL-2.1-or-later PackageLicenseInfoFromFiles: BSD-3-Clause PackageLicenseDeclared: GPL-3.0-or-later PackageCopyrightText: Copyright The Quotient project contributors Quaternion-0.0.97.1/LICENSES/000077500000000000000000000000001476730121700153105ustar00rootroot00000000000000Quaternion-0.0.97.1/LICENSES/BSD-3-Clause.txt000066400000000000000000000027101476730121700200330ustar00rootroot00000000000000Copyright (c) . All rights reserved. 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 above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER OR CONTRIBUTORS 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. Quaternion-0.0.97.1/LICENSES/GPL-3.0-or-later.txt000066400000000000000000001034171476730121700205220ustar00rootroot00000000000000GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright © 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.97.1/LICENSES/LGPL-2.1-only.txt000066400000000000000000000624561476730121700201010ustar00rootroot00000000000000GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, 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 this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. 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 not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the 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 specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "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 LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY 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 LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. one line to give the library's name and an idea of what it does. Copyright (C) year name of author 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 Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. signature of Ty Coon, 1 April 1990 Ty Coon, President of Vice That's all there is to it! Quaternion-0.0.97.1/LICENSES/LGPL-2.1-or-later.txt000066400000000000000000000624561476730121700206450ustar00rootroot00000000000000GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, 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 this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. 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 not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the 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 specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "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 LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY 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 LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. one line to give the library's name and an idea of what it does. Copyright (C) year name of author 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 Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. signature of Ty Coon, 1 April 1990 Ty Coon, President of Vice That's all there is to it! Quaternion-0.0.97.1/Quaternion.project000066400000000000000000000204551476730121700176260ustar00rootroot00000000000000 . 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.97.1/README.md000066400000000000000000000520051476730121700153640ustar00rootroot00000000000000# Quaternion ![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) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/1663/badge)](https://www.bestpractices.dev/projects/1663) [![](https://img.shields.io/matrix/quotient:matrix.org.svg)](https://matrix.to/#/#quotient:matrix.org) [![CI builds hosted by: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=flat-square)](https://cloudsmith.com) Quaternion is a cross-platform desktop IM client for the [Matrix](https://matrix.org) protocol. You can find general information about application usage and settings here. 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). ## Downloading and installing The recommended way to install Quaternion is as follows (make sure to read the notes below depending to your environment): - on GNU/Linux - using your distribution's package manager; - on macOS - from Homebrew; - on Windows - from an archive at the project's [GitHub Releases page](https://github.com/quotient-im/Quaternion/releases). The source code is [hosted at GitHub](https://github.com/quotient-im/Quaternion). ### Requirements Quaternion 0.0.97.1 needs Qt version 6.4 or higher. ### Linux 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 up-to-date list can be found at [Repology](https://repology.org/project/quaternion/versions). Popular distributions satisfying the mentioned Qt requirement are Debian 12 (Bookworm), Ubuntu 24.04 (noble), Fedora 39, OpenSUSE Leap 15.6; anything newer than that should be fine, too. On top of the Quaternion package, you should not normally need to install anything in addition; if something is not working due to a missing dependency, it's a bug in the package - please report it to your distribution's Quaternion packager, _not_ to this repository. There are also flatpaks for Quaternion available from Flathub. To install, use: ``` flatpak install https://flathub.org/repo/appstream/io.github.quotient_im.Quaternion.flatpakref ``` These packages are built with a suitable KDE runtime. You can install them on any distribution that has Flatpak - even if it's older than mentioned above. Please file issues at https://github.com/flathub/io.github.quotient_im.Quaternion if you believe there's a problem specific to the Flatpak package of Quaternion. ### Windows 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. 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; current Quaternion builds distributed from GitHub Releases need OpenSSL 3.x made with/for Visual Studio (not MinGW). ### macOS If you use Homebrew (you should!), `brew install quaternion` installs Quaternion along with its dependencies. Otherwise, packages published at [GitHub Releases](https://github.com/quotient-im/Quaternion/releases/latest) come with everything necessary already bundled. ### 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 of them come bundled with fairly recent (not necessarily latest) Qt 6. - 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 older snapshots. - 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 Quaternion from. In case of trouble, ~~show this label to your doctor~~ send the URL to the binary you used in the chat room (you may have 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/); if your language is not there, ask to add it (either in [#quotient:matrix.org](https://matrix.to/#/#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). German, Russian, Polish, and Spanish translations are either complete or mostly complete, as of this writing. 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 than the classic scrollbar when you need to move around with bounds of the timeline constantly changing, as is the case of a Matrix timeline (older messages get loaded as you scroll back, and new messages can come from sync too, making the classic scrollbar jump around); with that said, 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 in the timeline (with the full image downloaded after you click "Save as" or "Open" in the context menu) but be aware that if a message doesn't have a thumbnail at all you won't see anything (see also https://github.com/quotient-im/Quaternion/issues/601). - `show_spammy` ("Show no-effect activity" in the menu) - when set to `false`, this setting tries to clean up the timeline from events that don't contribute to conversation in any reasonable way, such as messages from a recently joined user that are all redacted - a typical case of moderation applied to spam. - `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 name or hex code (3- or 6-digit) of the colour you prefer for text you sent; by default it's `#4A8780` (a brownish tint of teal - no science behind that, just an arbitrary shot in a color picker). - `highlight_color` - set this to the name or hex code (3- or 6-digit) of the colour you prefer for highlighted rooms/messages; 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. - `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). - `quote_style` - the quote template. `\\1` means the quoted string; by default it's `> \\1\n` (i.e., `> ` prepended before each line of the quoted string). - `quote_regex` - set to `^([\\s\\S]*)` to add `UI/quote_style` only at the beginning and the end of the quote; by default it's `(.+)(?:\n|$)`, meaning that each line is quoted with `quote_style` separately. - `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 if it's still displayed after that interval. - `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, meaning that user IDs will be turned to hyperlinks - `auto_markdown` (EXPERIMENTAL) - since version 0.0.95 Quaternion has experimental support for Markdown when entering messages. Normally, Quaternion only treats the message as Markdown if it is prepended by `/md` command (the command itself is removed from the message before sending). Setting `auto_markdown` to `true` enables Markdown parsing in all messages unless you prepend `/plain`. By default, this setting is `false` since the current support of Markdown by Qt is buggy, and the implementation in Quaternion has its own quirks on top of that. If you have it enabled (or use `/md` command) feel free to report any bugs with it at the usual place. - `paste_plaintext_by_default` - set this to false (or 0) if you want to paste formatted text by default. Quaternion uses Qt Keychain to store access tokens and database pickles. If the secure storage supported by Qt Keychain is not available, Quaternion will not be able to store your access token(s) and pickles and will automatically disable E2EE to avoid unrecoverable encrypted messages. The fallback file used by Quaternion pre-0.0.96 is no more used. 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 room members if the server supports that; 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). 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. #### Failure to start on Windows If you try to start Quaternion from a path that is in your `%PATH%` variable it's very likely to miss all the libraries that reside in subdirectories of the package. Make sure that you start Quaternion (either from the command line or Explorer) with the current directory being that of `quaternion.exe` binary. #### Some older messages don't get decrypted in E2EE rooms Unfortunately, this is a limitation in the libQuotient code. The E2EE backend of libQuotient is currently being ported from Olm to matrix-rust-sdk - aside from being maintained, unlike Olm, matrix-rust-sdk provides higher-level API withh all necessary bits and pieces to decrypt messages, so libQuotient won't have to reimplement it. Subscribe to [the respective pull request](https://github.com/quotient-im/libQuotient/pull/820) if you want to be updated on the progress of this work. #### 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 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 file) because most likely not all necessary Qt parts were packaged along with Quaternion. #### 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 - it should be 3.x, not 1.x). #### Logging If you want to see log messages in the command-line console (by default, they are sent to system log on Windows and some but not all Linux systems with journald), set `QT_ASSUME_STDERR_HAS_CONSOLE=1` to force the output to be redirected to the console. When chasing bugs and investigating crashes, it helps to run Quaternion from the command line with increased logging level. Both libQuotient and (since 0.0.96 beta 4) Quaternion use [logging categories](https://doc.qt.io/qt-6/qloggingcategory.html#configuring-categories) to allow fine-grained switching of logs for a given part of the code. Quaternion and libQuotient use different categories; this text only describes those for Quaternion, make sure to also check [lib/README.md](lib/README.md) for libQuotient logging categories. The most practical way to configure logging in order to debug a problem is via the `QT_LOGGING_RULES` environment variable; the Qt documentation (see the link above) lists a few other methods. In all cases, you need to provide one or several clauses that look as follows: ``` quaternion..= ``` where - `` is one of (see also `client/logging_categories.h`): - `main` - `accountselector` - `models` (Quaternion backend for user and room lists) - `models.events` (same for events) - `timeline` (C++ code for timeline visuals - very few log lines and not very informative unless you know what to look for) - `timeline.qml` (QML code for timeline visuals - this is what you likely need to figure out why the timeline looks wrong) - `htmlfilter` (conversions between Qt and Matrix subsets of HTML as well as HTML import from other applications) - `messageinput` (message entry box) - `thumbnails` (the code to supply images for the timeline) - `` is one of `debug`, `info`, and `warning`; - `` is either `true` or `false`. Bear in mind that all logging categories for Quaternion start with `quaternion` while logging categories for libQuotient always start with `quotient`. You can use `*` (asterisk) as a wildcard for any part between two dots, and a semicolon is used for a separator. Latter statements override former ones, so if you want Quaternion to log at debug level except, e.g., `timeline.qml`, set ```shell script QT_LOGGING_RULES="quaternion.*.debug=true;quaternion.timeline.qml.debug=false" ``` You may also want to set `QT_MESSAGE_PATTERN` to make logs slightly more informative (see https://doc.qt.io/qt-6/qtlogging.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](Screenshot.png) Quaternion-0.0.97.1/SECURITY.md000066400000000000000000000046031476730121700156770ustar00rootroot00000000000000# Security Policy ## Supported Versions Only the latest stable (i.e. not beta) version of Quaternion is supported with security updates. An effort is put into supporting the 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.97.1/Screenshot.png000066400000000000000000012572231476730121700167420ustar00rootroot00000000000000PNG  IHDR8# pHYs+ tEXtlogicalX0eD tEXtlogicalY0d!.IiTXtwindowTitleUTF-8windowTitleThis Week in Matrix (TWIM) — Quaternion7 IDATx^wtfS @ WE Mz (.vEĆ_ņ D^E "JEATz,rޙ]'7wADDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"0 gzv"""""""""_2 [oM)֗5ȹK!̾QDDDDDDDDDDyu zEDDDDDDDDD$gp~>ϝ/:./sȅS!zA+_.dh}O'""""""""""lCsOgz9&?}-gsl9Ss~_'/ ?rɹy9!s~^#gFEDDDDDDDDD䟕[Sٶ7g0s_[~Q/*@9['6˗69Ֆך?9ͩMDDDDDDDDDD/P;6yYrjO9/Ǭ>jP#"""""""""op6!c|}Lf-vyg&v>_yv6_Z&""""""""""`j˭b֞g6v._f-0⯟m_jDDDDDDDDDD䌜Bk_mf_o۔S?s_.<)9jK}m_5_9rjj-Afn㫿]n*9s9mέ䵟U^jxn9|y7֞s s:W<ֶ}?VM9|WKKY^Bh}r -fi:jrh#bṹ}6Onj""""""""""eio߾6[kb=myu.rNǚm}!mfmjP\kfn_ -ǘ5{?;_mf?s.66L8W}mnk}iٶB-/ks;/}W__rjldǙu Y#)hcY|mk-""""""""""vBh[m֒S 4۾-~M1myڬyٶks׾H ͚p׶ }mmf6Ԗ` }{=H ;8kDDDDDDDDDDK|9|f0+47CvX{W_{fm;uYL ܪŞ[|ڭzNks;Wpomm<˓Zۙ>ʁ;[ֹk篞/ۙAX[AUǙ>Uks;'y'"""""""""rkmgѭm_̺#>[5{~k74'9r-ֹ}tnXWWȿ`^㹵Sɘaz'˵ꙶlkD|N!.[hnusm_~N56z˙_| ڭʓ3͵m.foZwڦF>O7hr; pEDDDDDDDDD"p8(VukWe柆=hx+TBy_ܷmsΑrVnib̚7lլg-}IIIU."""""""""/rHII#O:kS))))a[ly=4gvV?gy= }msv}Nw6޹hXn""""""""""_ѬqC.Yxn][F_j1Fks~62/bf/^]{#˫EЬAFR4$Ĕ8^5V`Z³<1eNr-4 e˖ O-Trf+7i;\:޺Xm2x+xF[}SX|@@@@\BbigJ(Nwga"gm+LÊGhXG^l_\|@n<2u^v_^t6|^v3d7l# vUcӃ3@r =IM 2Qʖ+/gǮfs\ujQ f- fE#$8m[rX 6o5sL6-9}vǟ˕Aپs8h68T颖ϴ\A@UxFs5_r y"};ZVo@#ORchM@d&g!P\'}Ku"r .ned ?p7˖-,ƽ,eJ4_,[w }6P\tBJJv|#׶ldxrCԭU˿cхxi,ySrWжe3nE\|ٜ-rxwغc^{bظe~82fqini6L]شu;7ٿ!ozw_Ϯ}NzlZOk݂=q~uj{SII8NUyqD&8{~\ޙ }wmh22|\9_ׁQLib`يp_WXtp-,!#sDDDDDDDDpztc5jʚ]5Ub~Ko1wk[XsG{`{lV0U#p>xA(`l?wǀ;FѴ!/\9  sՕWz}C?DPAjZ} 8>ok7lbMf9W{[ԭ]t:W6!!Կ.l^V<ƅ޾oV V-USj2')TGC+#]p'TV"dĺoQ ?Q6˹LBBHKO }Y _Frr _̞筵j~ x,ylw4n؀;̗sU(]Cn^t{N-kq\Tt!!}/5V!$8jVʱ;Fb~ˍԯWLY_d@\۪9Ea=Lp*'N>w..ugj߭9u4/mZ6qsT/z~ANIJ:͔e,ǹ_?⚫?rq 9Chawhr -Cnaz:پk7NJff^xخ UTfɼZ .@R%k}6oλS>{v4 :L׎ݣ 3g\eVq*)~תQm[|nEk[S~j׬ξQby֪M[Uw~y.+W֛o]g9)Sݯ@\|<>3SXt{ymՂKe?y牋O`KгK'wě}n5i>-S'c5~#7 Kxi۸M0oOJ*C~߮d7+p\ 8͚{ScЍ-Sv;\M7}9~L!ȈjT/6M՝Ǵ5ٜ'}8eز}'ˈa~Xq1 n$ު/[~>؃w+`v 0ѾT+>{{^z]6۹̥_n<>zۀ[y?yY̺ک]vVL*(\.!!!.I…q\rC֯~c'PH(CobБԪ^ EZ5p\|;N'5kTLR+;q\<4N4j)Хc;w\߷' ~è_b ,7lndqu*V(KF  3E钑thۚEռE(}w݆v:[ D jp5b]\.x=mZ6c4n؀!7 )4V|]{h).I-q8'KFR<.i3gSHVʔ.Šs,:߯vuk`NwXn_dY٫]׮QN-\.5U`s9u*N~-׮֐dErr ]dD%{.\%# Td$^ii~eJ&##bԽs_+lٶ"EBٹ7:kK`` jTDǜ;\=GRjlϸHPtIyu*)SE?~26s$Y쇧m '$$@'OzWPeWNQee= M[ b4itM5$#cLSgұ='$''4j:k7+~.~Y˾1vSTP-^FPP?5g^x'(O~de!9߻;ڶiϹ~U<=\ռ߼m'/fMdlpu{m5Jom=ݱk.[*// Gk`V3JH&<4P;UH?UB? ,Ǚ21oRS4&U浻9 .$:EpPם~2}&"#177F&.>_e7+lt5Qf5nL||<v+鷋%Q*O|{ @^<{׶jNff&%##ܟ<_{;gGy x3Y~'!1Sغ͚}k[6gMHMK#6.`Nje}AV`+Ud΂%7~A`)\II}\}ɓ8x5S3 ~=4ʖqܷ]\f:3NLq"Jv%ܟ'OzڬglhP5Wc!0 A_GX"Z>~wKF@TJF{Б#r c 﫹 H^}7˿[ϫ^:<3fwgLq31J'lF&g=\.v Px~M[kod9/ +ƭCe2K`2E7ҽsGȬ1|<+t\:ڵiI6-u&|gPPwǢyp8$p:gSڶݱ]*WGv]]Դ4BB_v[uh*='ٱk]:s@׾s^3$8,ױ/fOrݿ%==6-sHցN'm[5ry>232S.;Rfu&;߯}yI; _}[4o.=.] Izz/Q9Awsϟ_DDDDDDDBr\.cOg|h~l3XD_FD׻GWx-v|>~X#୵/3gݞUٷ.ھ]` "۷fzσвJdm<;ĕrpF Y3eWJel޶-vrCTo=A\ìՒ=uyYG=1zC6lJbyoظx>p&`PxK$c}i>S .}ʚ۳+;wdM9&BC ܋x4:EKS6t}_~Zk!-x(sKѷg7^{ ڵnI Yl9}s﵈ȅlٶݿn6Ͽ3K^1lj̒W9ěl޺W_{ﺕ4banexZ!UcԭpB>Xo f߽SиǜYJb_ IDATO #nkqGƦ½{Gs;?Jg;Dl,O0M۶EjZ*)cZ>,brLy{M~ p2..yM۶p88~$GÅ#K==?ޚ_sO+ϓ΂%_ŌYsan4'F*=1)iY1=#I~BBAٮ{f~wљ6XHIKe$a,=u:YQx^| >>bŊ2晑u }2G|^ \8˔өV27:@Zճe;vؽ_r26y\\pPzUoRRS\CA|4 > A7rPh>l>7:ĝCnO.,Xg,0(Ws}z0c3h`nXUJff& f4 6s6͚\ӭs~\ ;ۿ!fŽY1wRzṳrm̘5_x_`G?я~G?я~\,?~-[Bٴm;! AXb""""""""R09͚ҀTyk;3=ݶlgڶV?3Z3_H~SИ!mx{0M?c2}lKt|Ok̈ۨ\bޘ0[_ᒐut:=o/iizbgG'T!8x5kӪ5|6+=fv zv#1_翡h7\ס-zu]]&{332~\.'O?SȲm1)h.8+""""""""}w-SƖ-G.4?-m6ȿ5ۚ^ƞ-g2fdҹfnYǚ}5> /Vq""""""""""ri!3f3ط}ퟳ7anwÚ """""""""""ٱ%3f3{Qs.B]'szxNyDDDDDDDDDD>}l?U%/}r9~#fݼXs~Sa{߷FDDDDDDDDDDkvڅllneȢ-Us\Fĺp7d޴nDDDDDDDDDDD| ⶶ?u^l_k͙;%3e{ng7>/0/"""""""""""b5k]` *wa¾6PP)"""""""""">[,ֶ6KA۾n¾k[Z :EDDDDDDDDDfΩf_}N&`_ks^O/""""""""""bq2n_kmn$&ωyn:/>cN6"6_7cr[:߿+We\.YKJJb/q\fӿ>""""""""*4o.6.DmQ6! hס~tшOH`ufۼy3]uf+VDD{nңWOm޻tڕ6^r>unylj7| s .GvYff9۶1mt,""""""""._9Y|m?+_~%͛7Y.ڰqڵ̟?,wקHh(_/lbٴkߎ&p=CJb)ƌK% wʈ#fK3=KڵYx1eт$&&2nxk60o3 87HO7$&$.""""""""j2d_Yyu[N+87o׾Ь|;~~Zňq8˯YgϙK(:wɓ'>Gnݷ/ -3klpQ3h`o٫CÆzjg(K~͐[os.~D~^;~;śoE(wŎ;8ܹ9ڵ׷W-\H;ؾc~;{ %%QXs{hذ:?00BBB(F^Pnψqտ?xxp9=CXx}Cǎ̛?/I>}ܵ ˾{?i԰!N"E0WxHKK㍉0$'0h`?NXlYٱc٫{\b]3>՞={ӯO<$}1oL%Ka odɒ%{ LJ~Çӵ{n<ݻ/,rgnʩ-W ՞ [̇oͣyDFFҫGOfϙm۴yK-&D8f͚,YǏ畱Ǿ}߰wߛ[oLdW_q߽2GOH ===K dN8΂ xYpfF۷mZfڵ,_Ӧ3wlpg R=i֬ʖ%))زe oL7ۇƈG!%%J*v{zPPaV t*^'S{oL6ٳfQzux):'1̝=GF` $&3\:{xӱCFç}ƞ~#33E gMc|6SK*V_yBBx駉/9K~^{ܹc3g?gO?nzyvm[a 4}J||))Ɍ3c_aάټګ̛?,P;,gF{x ,YV-[f7xscqn9]n 4 g) n޼u|2333w{F Ճ}?$&&%KmӚe0GINNΓ=q8TVysRX13mzi޼9aŊq*W}ޭEUWqp ,,'OUlтN;ҩcG[)F3w#yL/X#G]Y7nՊbEUW5 0-[hѬ9_o`ʔX~ … ,0S>p:9۶/FbJ DHHQԩ$&&b k+гGOջӧOշW^y%G|y'h԰!K&Otvw}DFFe q iiҭ[WnSjI*9|0111ݷ>}zpyݺԯ_ b zE.cһg-X{B ey<5meUo>L2'Onv۶M 9skϓ sv͚dɓիWsQV5k~Hh(/o#1*U>*TB w^J(mψv1xҎ9X0v3̌,u駞3h5jC>H5̮f@(>|up@-nDfMڵ[ +FJr2͚5qɯke,]5c>WZ4oNݺuxw$E{GD~KC֘s|_-Eq tOShQ~swk׮tڕ mƻ&1l0>:>cիw&Ϋ3O}e ƿ+|:o''SR%nv:o[F= ͛7Rڽw;?ϣT3%^DDDDDDDafȾds_ͮ@w 8'n1fϗfϦSǎY^=3w:BRRS)Qy8BCCg*8<_frp8ܗ{*))K߼F'$$0mtp_`v+0Wֿre˲rn%}{9nIv}P7l ))eШQ#֮]ڵKfUno0wA]ZxxO ~#""r|_/vMƍp:4hЀaHII!""ﺋ:w>oEԩS_:=r݁u:?5yv(jլYOyۉa7se_5w hxSn7@}`mӑ#Gyӻwޡf7k?D\|< O<ԪU/^_T;vU'ض};11iP8=7ncNxe|'+Vk^Kb)b/cyze-[w{666}ӧOЬy3&OWb N'%yBZן/@F ZhүyKw+z!Cet}5jB떭`>7o7Ӧ9˫RJRTiV~}M7w^F=7thղ%Et8?'aZgNr_?@.wK̗'_ z%_N*WΦI)_|z`` ݻug9{U6o€z7v,ύS<… # [0)] 111y~4\z/+Tz4haÆ|D;_d+FLt4=wTҭkWޘ8͛QdIpOKկ/72qan8;{Ï^zlݾݻg:zΠ[n;nc 7PP!\K7[n}= bŊ5&Əx/ձp1׿?8~|˕c=Z-0|N/=sGSl}'N(o;wE)%H< .REDDDDDDDDDD.9fbs.$""""""""""rQu%۹k """""""r) 4"""""""""""x@y^DDDDDDDDDD.wGeAD uhҸqs:dѡCGod EDDDDDDDD.E; $$$ѻWo*T[8z(G_TQʫ@JJ &'EE>r߭ѣ7a<]uW>l۶njKnz;㮻Eߨ~[>p\L3[kwmƏ{bŊegDlش[ ׵kӧ/2x-|7{E#,Z([^|y:tE8r~7e ϳgӦkn ~15v-Mɓ'ͦ|6m:ٔ'O'3m>?MΞ~3DDDDDDDUرc>t*^3FQJUfϚӧw>> Lݿ1}4fϚEyvsoiӺ K/I&<ՋSNf|1&Эk7fϚŴO?gOaϞ=$''se+ eرcy @zڙ!ƒ@˖-X~ "+2o}Ӹi3ˠ!}vƼ"k~<}l۶lј_qf[[hܴ)4nڌ yk=حZOͦ\-X쯕I.]iܴG<}-?YcNNɓYͷfS;iϿ"IIIw+ФYsg6иi3i֜x|H6mѫ7qfs}RL>2#Fh$OWS> ƛn"_5".> 6p L^=#WXI>  6n ˖-5MpWP\94hޯw%GEڵ~BCCXԩi?\R^j֭[{kZ/fpI:u*xFTPCzɫ2eҽ{w/_Ws—)[61eީu\:/JӇ[IJJ"==ݻ3Nڷk+W?*lѕꑐ/$$$矽֭[OBB r7|a jԨa6*--eM믿HJJ")))1dff1fWDD?\c7r'~JLH/ IDATƍiݪ|u$$$PLiom6W[ u- dzq&vMLq 2]$$$p*]+y}. ;p\DDDDDDR [4oN}ӻ'O/Ç= @L; /Q;"2:&H[[81Ezۜ@cI'x +={Ϟ|>G%$V<,jӯ_,;n:ksA :kih">aaʼnrH^YX!9ϖ W˗+eNxgg[xxp:)ݿ~ɧij^TZ+W~GjlcbxiڼW]ݘ fǎX5m ϔ\O'sU <ۧؼe +TbŊ8qGFE6jӖ_~T|7ߢChܴCﻏcǎ^O?F7x;s1wx 4vr,.[o/rƷ˗SB_#i4jrz9q:v":&_~Őnѣ؉ {O}8q;v__=tLswĬsiYx1׏EzVZZ˖-hִ)O's.6l%((_lڴl/yy.{t2{nWu֡y4oޜz??!pw=< gfphaaT^%&&aFZri%$?.)U$.B>II =ȋ}w_sŸLVXmw+_bPŗ3ٓ;v2~H:C4n~\WǾ”'6.gsve˖Jln7l؀`7}Ioc|Jƽ:G?g0yʇ e_GGGF &>ڷGPrl܀VAhܹ{01}:G߿ϑGiִ)xyyѰaC6o []۶@GNDE`2⯿.SޭMn/ ApquYxyX;AJRڶٙ{K\\/</}~Ȱ#CD?gikkk:vȘ^zjd]ӛЁ-2S6.AA|i\O>9ioO 99 O79zz7dgeѵkWƌmaÆ 4nԈW˜HU==_grڶmKqVqQ/IrIx봂?} joVZEVV✜b" (..ѱ]Kpp00p!2d0#F g!X;;iSW9666 877P;rD̄AZ@ժ^Ztlllppp0_~=Cʧ``AkLzش~w||' .妫E=$''Ӯm[YJ==zo֭[UF?wMo< 4_h{t qq mW\\F!;'.1wT*deeacc ŋ G\\ҥ^zElmM'7j53?x\>TR?lmm {{{:URƾ 4yűҨaC"W^19yAhL<==;\x/Z,ONOpVjE hZ;@mj4hgl%.иq# x__?~*պt?_[ƿcǿhڤQRL e .ЩC;0K4hoFNQ;#FK.(FXI@@.\,CF<"233qww}j 򰱱AV#"O{h߸AH Пˣk&[oرhD8|. P -_ htXW5_0bP1\]\Ѭ序GGGP > <;:?={`Wn(O?̰C yޝN:, ?M>((((((((((((((Q+(((((Yo.Oiڴ)˗ʤwX+(((((((((((((((((((((< xHˣxzzbkk+O_OHH)(((<6ϠII?6Zʳq yy^VlړR=}eE XW eX_aU-֗>*NzI;[r YuKV\aKtI~<('&Y FYu"<"O+~{{Lymw4{w xѴ#G='g[O? g\z`Iz.޿#˾].DZj͚Riߑ^}ʓB9ϸOf&##CUa9B>`0Ƞ˯cla߁S5;Ͽ"4:aK$eb-I`^7Ik0GOз={KM_iߑR'ׯsmy߆3}P*S/l߱=c,dlZ<\2jbbb(**G?4?ܑ?01!;'Ε(((` ڕN;֯ debW|dd>:eGq]R;xTrycIY.(fN p^8y%X\~7? "n]`~rXɸ}Te yEIfA܏9$W kD]!3HeKwڵ t4VxT 氲rr7a^cPq˜KBi+Bڢ9H~ضǦhXv-]:wfkiժ<ɿY3?0j6iot󣃟IMk5 z0?h4fTӦ8:DfMHNIf\II|0c!Ҿ];YA?v VVVlٺ)'[avvRNVI`ԖyX MѐP\\̥>L~[Yy '{("##ٳg/Q035׷//+PXX=^={n=vAS|5Jq޻Q(DDF2};4&k&s=~;7nVe};c O~J)W(?z׫=_J.z,yYY{69Qӑ"_wS/yڽmTsT:s'{(]aT-sw-+2qe+!A+~u&~l?;ZӾ@P,Cb3m!q7!Fc!$o o.?G"_mG BqfX5jM҆M@Qm𩍶pza{лoM:1j/\`ȧ ??6m.zyxV2e姏է/!ٜ`d39} #6n_3wvfS VYKןCq"* t۽~13SS5HY$|lckkXXYYmll#G >B@v,""#ds v4k?Rɓ ?~CYhD?ΐaMuv&:58I3NLLPV;۵c] 6Zs]Uñ1Jegoo X Ï?6yZ9sԣ'O>Uʪ0|ҳw0}B;WټeIڑ!q?GGGڷkGh@-yzʒ3ɔN{^tՋ'@zz:MLpHeo,)g֭ DpHBeV)|6!]vv6=z&C/'gӵ{eXuo76ow;nz~}%=ص+;v]ѫ79 6ݹXYje(>8cƨ0FE.>!7ͅ2IS >>!70W`w߳m6ms/hr=`Ȱ&,]K3m۵ܹs$%%¢3cGtՋ9s!b)xrr2,য়!6Bq}:Ȱ2ɘ9s2~]੡CtI2w_ddLЕgĉt F>}ٸi3yyeY>y2]Vx4'òwYlܴt>3LODd$͝Ǎ7?rej:vfktg<=sз_s./g`veooq ""#hҤ Vܛ+~5ܗǍ WRxغm;C kWFz䲭Owƿ*}Bѷ_>Sپc{p?ēC=V]Aa!9}?}}2wȺ:g?+$#㺯oc̡KP7z!::.A7` һKgY\e?cc gF'!!, +z;+) 6yw_hEű\K^g+1{.vzïݑL\s^_a' VyEg4Fc}>FxG .}}:*)1o3(,`зe{=ub_Iy]rWbKtZ2Ne`,{2(.d_D'Λ,'n/'9s[~~)6iA>ét|w΋pUr.'e3(}., +v<ސTƭKɼ^Z}E_ Iy.d' ]}PK*ʤ/-_iLYmLU,";w۴_/hx8v`5ӋQBx1̜4AUisPֺUA n$ӥ/:WIb惶|GrVU'YCV[7x;祸-*藗#_R ?Qoop{y!iRYֻXٷgf' P|2?GshIOg۾쟿E;׼QfZ JMooڴn- YYr%S~Cҧ_.^ gp!.[8&ԫ[HN>='NвSR`=) IDAT,X۷מ.^4D~~>/K&M{wŢE`&v `՚5/\ .>=%!!D,9nYY cݨV|4bvvA؎ԫWNRY՗޹%_/f c2Ir]:e޹hڶiá3{63?CIJN&|Nf.% xc$b;wbooǧ|';op]tJ}KmLjѭ~ƨ0&"2p6m@<;f4֯7IɄW_^%l~n9K.{eeLk߾v`)OĤD,E&aeeEpp06n2mشR)Q-^N:oO8[6nd?p|mӓ422iݻ3{tO>op0'#3Wdƻrh>uw%EʓZM&Mp~f }1w6esÔ7C 拯r5s2,~ŕ+Wٵ};6g]ٻ,N;2}Ts6-455;w>zG.^a AF6]ǻhެ{w3o/\X|-!'1<Ѽ9>5kʣhڴ)ҘQT\lГlɫW)]޿φ9? fl \aMض:}: s[6mի㏴lт={{Ej QÆ&אGSSy+s׮-9cd ȸX9|8hެ+t7`N.2|套ٟaPh(a;wq:㈉).J.K;Il 9xQqP&ժZP} nGhÖ q'3UQ܈=;ГIߎZͶ:u=wYz6ѹA;ŻF+r:>usH*`ǫ܈y_ek͗oJf9:/'BWmdO >ݚͪQѲr 긲⹶kEfc;\Mbbe\z/浵x5>&vb|{'$ljnaY=Wek>m.e`,{5#7R8s+;+b"xۙB t>Ss{9U/l<{?OaKvYǚ.нQUvډ:8ԜBiHʔ)pOޘ/%ş\9ż/w˖ fdkjdxT-rjc Ը'?/-*rIbN\]\ppp&.>Si[?IosiI pq.[[#6z!+Ձ0C:!~-WڐhD&Ѷ^f`85_!/GGxQX‚dK@tkcNCQdS2.dWX*_!n%ӓ m_kK"\]*իˈMmsS_:sЁ 65K֩@~M> wInA8;9Im3tܙ:F?3ʰ҉'hӺ5FW_TTkբ|zEժUM/otggN,HT*U==W.w~S|ƍűHZ-ǎ#S8rDR>z΁j頳 ݋/|߰!'5c :L /X[[`|}N>ɉvmېI*t ,<ABE ~)m&328ŠRs-R nl:AYxzC e\q+++nݾ͠Ё\6Xn߱Ё+Kv}*~YXD/%ؔݼ}tUtm艋$mK&]LΊ\/=/cxRc"e}[#TAܱF=4[m'6O0`)4\ps+fd$zHI^)Ujn*σ-+GFj]G{ 9|ύ-cM>˗/-(cFVGƤ$K 7i^Z52i;ܵ<<t6/*^ ˢyf|G7y=ڷgoѸQ#txguww'!Ɇ䉅6aL˖-DTݣm6Vj/ox {36nƽ¤7׋˜4DVZ**#K}uQSSRSbSIKwr qt,{O^|s:j@F捝 `Cbf$H-͡$}-7Ʌfd`L@}]A+D^Oe0v\HڽNLoզ@RW4ZmѴ`]Mj9(iE&ݍ&0 H* aaos0?JYH9_2y|jՃMq9Hs[iߋHIN /kG[DWW ׅicm%D/i=!&Z{oI|OĆ]Z%"SتDH؉RWr9mS@w?cQfձQw6m9\[ Kh!U]R.j#:7CȽ4$~& ^* b![Rv?-i9~&OBH9dWmZH?*=Tߠb2 &­^6y!7Ds^Ae#\]ݒRw3?X9Ě* i '(!zԫh\GH؁PDAhB%/wMv@T>Be_ mE&P EhgPK6h,=;!5įEl8ѵ5C2 6?tJ/#DV( klڴ2a3H)6~8~U:صhIv4Y(,,d5K>zU'(|||8Enn.5kODd$|ۼejNOeιZ曙ie*su bμԬI@@'\]\_rr ¤)SXY=?.hӺ5v>}m;3Vӓ5kyc=zUlZhۦ l߱/`t)nnn ~Iy#DI=IMƌ㳹xi߲ӬYS."ϴiҸ1*u099{dg.$QTf[/OƔ"o_B%33_~]o:NFkTOFFAyyZZONNv%e 9Cf˖$%'3&q)1B6mXO҆fr%廞|z ώ_-KetD\uʓK}~*-=ݠxOOOѱfoe~E4o=YAVvnʧ"2VZCDx^2NgfJבOTۘ`N,]-7 MsќQ zU_Y:𰸹&֌]Ffl]:w?בHGZlID?NNJ)[唗1nnn~?5eQYW,2(Xepww?Y8oOgUO 8/qS ssF-Rq]q'=aW* jftvw=GgjoSKqx9հOصNʌVϦs 8YQPeKt-Ƹ;ڐgAf~NVd,1#cqd\:EjѮ+orF*Nv+ h=_)zAZUE4F_O?Ci[uR]z2Yg^GHT-;֞7KpFk۞MM-+΀jPi7)iyhU iz$)ѭ H5BNE5 ͦCQ:&x rnBM!0 uG#6yzĆK)KKSCK FKeKjKekg DV WZf'\FIA .Yk;G3c=)lI(<6`5b&=MQU{hRh"!fAwBYJykL_ʘ+z.|RX, \]y hNC=;h/C9ȸ%b@yry(AF}"G#܏Dk3CTi)Ys!$B) Ϝ냏1aKbI_0llll(:.)ua6l~]ﮰ0TkNdС'+,26c5Gٻ0Xi>t7KۅcvG݇1kѢo .NQ[/0KW 2<|ѩ?IIɄM瀒?HC8rtp`5Ϡmٲ%jjlf84p kxwIHHFa9 1|P~}% n %-[رFdP-]-*,Zd؀ϧfM*7OcλQ\\5݂ptt$77OOnݺe7o^~xZ>++2}=[jݯڊmmw\ٙ^={]j3~HHL0lКɡÇҹb+̵tU!--Ͱ/Deш\NEEEC|W|cӦU+f.6d_ȓ8|ag}ԭSl?WX<==hҤ 7I iii LꥥR,krʫعSrciۦRTTz,ѣ vʶ; g˶ml ˑk҄oMW{q#@R_M1lz}uVܙEĥҲfݞ$)b X+RMzfX>،O5gឫx N&oJ|~uHYdq$>˰HoQә1(HC2@&s@_O=|uB^ߪ%:*JF;+^ζTs53>53J <,h"263S uestjvu\I-iS#}Űyߍ1R6h(\sZćxF[PK繛 j֧qF:cmFH" uVrPCNT\>Ƹ;[kJaXk.ih !.pJe ֦/BN4zY*:}$y9ZZ$%dHJh S.K. <wT6%1U#HAzez2KfM ޻?BBD?052ߗ!3 kOHjՉZ@[^9l`㆐$mgx*DߗK!:{ˢ(b1 ܏ݎpB,m!dDy${8&Y""bgTΨ5k4>Vmp4ma*7w2\M/4y<&樏k2lȐR5lH ٸi?I\Yt҅Çi3׻ <;zkΝYrFAjՒs>krN;`"ԩnwލ3gSTFʽ{,7%bEbggG??ED_2)9 oǤ+!CظiS1L;vDb`b:p!xfӆY8ޙӶm[4h@̚NO^dg :Yk:p {W#-jݻ723g|R4e!|W /;8u)GI= <嗘9C~]}&s/WL>NNhr4~zTzj$&%1p6mJEA8;ԓO" V_|lf\'NMoJc5lJ{^|p[*[wѷ_V6G~L6LJJ 3ߟQ`ޜ9LժU#1)aCTX bceVճYYN'S ֮]xrPWFxs} |}M/?ḵefI0Iu "OӶm^7,7̩XNcӦͤ#iۦ zh2Ke4jTj4ؐɈQHINyfS|**S=ȱc,+m 0q8.bgghwO>_|s/bemJ0`8 tŘ瞧6m6܅5M&Xz˥dNz9) ;k<2򹔘E,sñyS"t0oh@z^=xS@RV'&1|jDH+2/%"9Xi\J"F*ʔIJEr4q)QRzNCE.%fѩ;v_t|:Q9:3gPsy;'{k4|^L䓝 i^<ە^MyfoϑLZמtkTG.%fQPVZ}U㋽W<mhWǕ=Rj"cÜgͺk8R mrBQŝR\b"d%$$٬I6#\r6߆,AQ((p -j &,P跷a@(dIhؐ k b0U.> SMjcu_S]3R=9.ew1|:^ǻ]v;_[xoٵ??N=NtBB(Rf@)//j!$(=YJ3ѪTHWVV['`-Av O`%9Q(ð_ wJbχ}oj (} qJ'Asc'yHG>d1Jڡu*R/_ΠY Qh!G^y}H>f7yߤUț&)Nz!EW>d eP°ޅvR&D-"e,6 yWY:Ӥ[BP(O6v?UJ pFw6fQ HZlsNxva/IF^Cm34 #Qb [f7R~l@,G]W"e->W{7>S1N-@:]yuSvDCj}t=`Jg7C))tXڹ{ڷ # rMnݚ YAF! ;w.SNeH 9Q;t:uݠv;EEz"##j,úi{f Wz WYG ZN֦s!)--nWGQQAV pWZZǰHJT;9+F4M ES0X,wlvZ r6LTTTΩJdYvQM԰MofY}4?2Jj"^O@@`Q*aZ1̈́uW[]j]tSZZd&**+NEEEDDDԺAu۴Zr^ǻ1|t W6{ۻ +n/y'NwC{DGH@<+ a6[IJjJ>5d1u\9;INGVV6fy+TWGq<6$IR\6WTթvdX}'`꾯w.o_ǻ6!!!n[M[]Auؐe] >ͽl6;vsyYS}~|G-Ǥ>۩>՝ϵÅPZ:UmUF 캖s\_B=~|h4>;VPՕwг>5,;io&lN#c;)4^uOPSrJL<(؝qu~#TDŽwіiS`Hꃯ0r),3~AU nFEwqq$Emul+foF:%AۮBC ZV(MEi?ұwd,Pv`򜺀~yQCp35g;WjNjfu7x?(a _(JpȞHsBoMQZނCC0 >ī|țB鏽>6HEՖn! v@.]!H<?.P}fj[̕fYTݎ 7OO7mBNòC3s#q/\ّ;vW:~|8?|'{yXtrSV0շ8E3磈0_DU(jpWuR uoS}2uG)mj_Ҟ>֓bW;xǥ oYAN{")\ },U ̅P-.nKx:-?BKA#   W)hW~=: A@JS Isz[]9:uqPszde(T,>qi}t[9sYMAAAA:{2 /%q_-^m۷sɗ](***%5{jvғe.H+(c<ҥzOl6c6'jlj6nbHNfTT=gReVf]U40x,`Ɂl2Nz \Cӆw冪{mr  Fӓ؇,l/Oy=/Fz#Pj NyO`š\@Љ2 <7Gv5>oQwsg0YyJ=% }'υ{y]Y8[=233'ό0}ҧF_} )<Ӝ>s㩩LF9jA6zb3Ogޓ/~={ׇ5]?`_z÷ߚVĀj' ik>?',ܟ=ލשi ˼.4dޓԻ}{vo. _}Z.5ǎemJVx ;۹sYibZǪ܇:FQkEx.R\D~c:(.RXf i`uqh(q;{uOAAΗ/p5s&|&LCO^^9iSoz "##yeM(B֭x' >Ysӏ̜}>p?={`=4eF#ӦqÌ\s|quSS;{(vC<‹0fhȲBlL,jב$^5kbWbyGܩ>j6[N2c}*Zvs(H ОW#5v=+~N2oQr fFQm}Zs+bٹݼrUgf})=xs 5;҉cH%OoIT\k:23hT(DQiT kGbx M%,P˲z5qXv07B`t8\>Q\eQm<|j$gynyN`춦ݮt&uM`EJ._ӅuGp4-Sʼ|ug~ܓɖhy.>5).)Ï>W^f/qM7sM7ҥ,]Yرk'E0nDFU<X7m6x?ѷO!,s\u<3O?EΝ=pӸqcnV}=)2h/ +0~,rK]iMÿ"; ,,IM/)|8;ͣ0+.ޛ֕(*pۂ}>l>d.ojct*6Ӗ}㔚+S{%1O>rr%F50=]tI gO3ǺJ;ZYbLg[O{\%{2fWvE!OЧYd3S<&H,qLHVIE nz?*;όMʳ9|N#qL۳@6_l;aZۖN aS5p<~O-j#5ųx tW[wwӇ/1MAA-A.9v$SEӏ7֮[,ˤU>@r2+-iӦ<3Тy V\eiժ!!!b20 9g.wϝêyرs'99-gUJ 苋xr$VX΢9rԵ^`TpխmvVY Yr7x? EEzJgT*U}ۿ_}˖rǬ5g.fmX,u cw,Q4(M\eFTYu8mcYmo[ IDATquD{0t}{( =޽zǶۙ=k&Ǎꫯb/CҺU+V,[}qܵ6|!+/]s`~ii)wyw[~ӧ3{٪Fq#Ii'Or!ߖ,f62+H˭dWӪ&v@>;{7RQG*?Na)5zdTj3Y>gn0Y;/kŻ|1*ß֒w `Iv3sPsvn|qc/I•=pM((l>H?rV]rv`ޞҕVD~մ,fњ ><;4 e܁<e'^[+Wwv]uG9}+5k̘qeJ J.( dT`fUxt\;V+Ww橥)<н:Mi|p}w~s oŽ?`u]|k3 to{yfK5^w6E=TX<냩]Xv޼+ga?צnzgfn?pmW7讶Եk֬ZɚU+oX, :cAAA% Ӧ!2qqq\6t۶oGeEaʔ눉FeX+j`0r$o؀jIW| 6._U;vCHHH`eX~rGmvN[oIǎP:M3nX~IիXb/FXh(^={Ӳw߾:oSӱqZvJɓhZmjMh5ܷy$9l2W$618',}S )1'ƵiTz7vD!3-6;2J,5@hCid7 e@huheŅ:|.܂ r fGpjkYG"*@ kɂ2-?B=;9˵=תLȞ3͕HtK {ptv.iT=p}&n9]@u-RIqQ\9w,bp%ЯZ]I p}vפ0rcؘ*O8šWcߗkp$&-~ LxXwv-nDFF2hWUimТEs23($ֱ!\.qL@hCZF"5tJ8\=ǥb^ͩnë;Ncc&QuUL@?̕՗iV!3&QA( ?:p5ocTtfXmv2f},::̚{51ehd ,1# oKB>kPhsF[w5v!mERmIBxG _ݺ%9GAfQA|zcO2+Sr&?Jz6@,I2Sz5&,k/Ih4Hy#/gq  *FMrr2vz=/eg[llk822 >rχ}̂_m}q1O=͉4l6 2ryyyDG(s<³[oq-cڔ)̼64.խөcG _~<(}z]۶ޛ𐛛Ǟ{8tHm9ĀMÏ<ʟԱ:l6v[օqw #DF<==_8cKѫY$wykƝ 09mM+dVK/M\AO,ykd c_~wf9!`-Y%&<_B;5D d{]W4K-D\QA갳U-Do Hr*oZE봞<#,ov3S<93ݒ9S v+?Wie^ ::|v |^~>Q^u6yyyhcZdd$kެDDDeY&>woj @#k۫qJϪ\ruGItB&y*_dsuD JFuN;O줪iBy9{=Ϣut Jͼ6gs>[;*)}4vt/1-u CN9emP(~ޛELtI #yoY ԳG4 k֮e$'ǀj^ؑ `<[o!TS S+dT?,`_z1k4-xnQvӫi:L@vSTncB(Yμ+:᧑Xw4 `8**XWv G :/3^\a%X~Y Z$I,gk͑RԤ.sRuyQngCPޚc<flz&^S[رs'_-~Dz  BUUUJPϿA>|8'Ndĉk'ֻE+&-[ѣgE>2~Yȕy}kW^97z|ۚ}C(1}f  @Tt4ǏfG+wE^^$ѵKZlIuO5tr>HT5,,_X(Dp<88Z/w eÆv:|zz鮼P7{dYVTs,yU=:RޖVD&,?[NeDXJM(@~?N󔞮i?(dT,PGkͩ-wR8GMl0oW/0ԀӐ,? -KΡsban-n$_ƙ"xLu>.2,Cҷy$:L@ߝJW ٳse;xan|w<6d(7Q$'_@g]CzMVv6yQcbx>W%Ȕ^ҫ1 L՘$5ieꯥYT L#Of|$ZE_+SSqR9d*m^벝5\cQ]|KOM˭L|oUWK*8yZwb׫>>u#C_kK>1Un%q'l_AׄS ]Y Jͮ+5yZO Se> 8/S:$Dzwoz1Y%U[Ƨد)T42C䯥jgh߉"Gk{}ܑG~|W~թrSOҼY3y  K|v9cPbG$ZK\JCslVIZthfLAZ;rH+Byy9U;m۾ v=K7]?zNN~^>:udܘ1{Vc,{Iƌ@TT$E<Ӯ}z cbs￉''7뮹CfvOϛǯK#0 _nϏAqi$$4X_LR${MTLs7,Z0yǐ$뮽[oY=jjGC a|͵$%&M7@PP}vF_~ńI3i5ǟ|{kq^yyk9FLV;s ܙr6u\Q77ikk9i\K˭ id Fs%m罙kЗ[SuJO=Yv01A1RRa%CoBЗ[\eN+ &+yF3fsۉ":X ZϘxe\#~j +Y=Lk:s85=eL0O/;Lh&\&w61kִs/kL&2PK&4 $cy2{ !.TGQY%e ,4罙>sdTPnq8HfqVuԎ% &JZgO=Eqq Zn&n׭ދ}ۿIW]E|\<2%]&|]Cs2{_znښVFr?1-_܈!q gi=o#yF5z8Ljq#HVQ\(-? emcU9T^ުXmvtZ>Nݎ]Ǵu;STNxj7৽YMlK[=u7{H -JڒSfq&sڙZ*5288w9um e{[ i& 8kId qlE1>"yt!3Gvq?c$>ԟ-"6t+ڒ\0oaFe\FdW1!WJ̔ITT_݌ t/wO ŅpF_qBLVFbuY" EQ1z- 'NvXwܵ9fp Nmђkk_~鱜  B]x}yOwKn/q9q ;ߵx9a;w>} ( d y +anw cUOO?gt4 ̘~=7xy%~y>sUj $YU˖i<CeĉdfeʫqF$If1d`|i7~KDDƏ%\ЩcGe=th{2S0ugcǎѢE Vo\>j4)0vBBfX0DE{""""\ӬV+%%bblXsNZ)--d2v¢""#}v"=UWMoө--k` $8cS> ;3YRK(gcl(vEZ]]A%$ j k?1缳ewTf,2ʮ2p6Ǐĸ y֭%C͆` 2lǻr yϫpaw{+ y.Wq]- __r=Z~uBGuu=fs_KܹAAeZIKKsGm:w9|0{,wc^lJw+a۰x6>{oYa!.#d+)<2QINѵeHoy0d`dYf={plddezqbb`)88 O.{NGtty}T6gMؘ5iY]mz.?g <3g5a*lSwrUDasjr+?j1u.Օ#א8q)V׎-lss^wk֤_A|Y,BCC'_2?dI6ECoUjd(B8?VGSO2bph4ۗė ͙3g1rǺ3g7s0fxƎ3>GEvgYq#oFjz 9ӧ3nDr2,qmM7e\;e*e񧟲p"ۼ+H?MLL4QQQSǎt)JKK1r%5O蚵k0i͘q]-^IW2nDNN0I|kE^^^ `+_#/3n<̬,^}u>3}C`qڵb9©S|ĩS8^su111d9@`@ vέ؅Cc|<he'JvSou߻tBp?א CUk]\EA័EL .vu xɅ-F&OET1lX}Rwd3&?Utٷwp{E\\۶o{nS^^j~]bX|=wS\GfF&…//?b֙aaaOx关i֬ǎ#))0>x]p<#\"xgD5Qm<]h6د(q <3A}V |ݭ5PAA$aZ1dggӼysO_*.|ŎlgiˍŘ*m'm$a*/E_BBH@Sؘj8J|t`许]v|=NǵW__}sx/mvBCճ`Ȫ5kho=I}zri˖̼6 廷xk>8uk0v$VKzFFW`e `0Ыg~ZG֭Q)SHPjjf~+ ?mT>Fsۇߖ-X_eP5w`@999"_ NGϧ U`PAAABj Yft=tc5 рnd0;Z뛌[R5n ӨQA|cQQ-UU^^yyx-ӛ5=IjXXkXkUT8x{OFEF8>3Q#Ul6~QL>@aٰ۪c=:!POt[FP~մ,|pa3>slaphiΔO"NGRRdAAAArQ4-Cmde&*Z,+ ĘցȪD5PK*Þ=z Ik֮e$'STTD=0dEQ\P;2:+1j,&&$,Z=^vENp$п?+WٳTXf-/ɲSeeeUW}N֬\A@@,]C\ IDATnbbb=j4O=,W{?":*bk`P[{wx(\;I#Ђ    E um-"01Xt v(ѭٟg!!!1{{?lq:Cy[oF$I8={\ Dӱc~MMbY|9C @b2m ԧwoٳw/8Z>>|{*4Z &gn& y]L&|-dtfu;.=¥PWP?@e+ԛٰ]"z=<jС,]uL~]]x AAAAA\-:3WevW GW2{9Z踅Zs0)JJJjDG31 3sͷФqcڶiCϞ=\-"<W6mZݽ{⃏>b |*7<0QҳgOZnZ:^9+/|/Ġ!CȑfA1aҕqHfgԨQ|WL4 ?ӯQ#yɧ<>3uuLZn ((<#Lb((SAAAABS]D{s\r{nacu4w?ǻ1|w nFj^s_d2eZ*jw$ $Il!s8pf+h#rYY6[ t:eeehZWފ "0j)1 &'7Ą<*++{DGEgz@@.,,$44},knHOddkZC0XV… ߡ:u=M7VC]x{# [uCAAAA0m۹}|9^/vW:+;Ɲc68^ [;jC(*T$Z@p׮]3z4ϜWLi X[u֫-_/ΛI޳\Z-nN@vu)2Rawu 7!dY&&W{B|ٶs$AAAAA[\2x@$]sٰq#yyyzϾoߞKǖUpi٢9!Aޓv))С)y( IA"- Պ&hkIRR%fQYYYS\aY'eu j"AAARYy$Ah0%{OrFa{O5i҄M,_jzyEQ%hz]    ¥@ 𿢠;v=FAzzAA 6, pUȮ]h֬9m룏?!--D:v`uL&q?nJ퉎;v6Exn    ԅ pޭ;Mj5 4OXlvH1MyϮQyy9'NдI~srd·l:gDGF5X^AAApIrڥK6F+(,$55͚[ǎcҹ3Z^đէOW'oބߟ#''jo!-1q\w͵pA6l܈^'66 &дI6f3,رㄅңG/߰G`VҴqFEPP_.XӮ &!)+&L؎nGQ233(1hբcǎƶI9B&IM5jA|WQZj^gYtlݶSiPĤD_vKKhߡ= zarr otc?XAFz_?PG'?#etӍargeILL ==+WFuL Μ>ŋXd }229rd`4Gӵkrrp9pBrsrhҤ 1r8uSTTHhHÆ Wܹ͛`1t uֱL6$   \lvɪ5'ӹSg<{?v1dzcvOW';+^Odee؈6[⽈p9A`6)I=vSi?z٘L&mJ\l<;t L:K-dZ:6nO>CqhL>rϤSt*SK ~voNFkۑy|U9rc1ct2سw/ oߞ‚"슝x 0[,֝xWXX8qIޝSpqI=q}3r̹.*L&~w5j+&hbȲAVV&ϲ+!>.‰4t(:U{@jJ*t7|Î4PT\$ ZG}DUU53gd+ {4 ?hhld`NVJJU&/|************3i1$Å^Va;)--e/xLS-V+'Ml}ޛށ|@DI!"yKkn%22xo͊+9r( 7f^Fa͚߿.Wq sl޼;vs3K=5kײo>b(,̐!a?\j%;{Gp|JW񒙙ɼs++rManWl۶)S'?bjjNOqWtYWq)I.&..6PP0A=%sUWq&J#)9'DGEW_E#=UVr`N pk|Bn7ϿWobf˹;{&nS***?"<DQ Q6tp'N ]QQ/V3h`+ z}n^{'NpE1sL=oEa0CmM|uFrsSUU֭[B什j)4 NƯ׎C?qN;GPe%FQ}= 2~p "z>GC޽3޼鿂 22t+V0 ѣL26*? </m6Z rB (lrE7!+xe U)0hh#y rt vMT+ k67V[ѦDF'?e+z$F$K4X0EDc4t 6:6ϫbo.K,VO|/;xl+օ,+7'j:j-e SLfc=NUĚ9b۶mx^N<ɤ^q0pO 7ANN8z? _|r/:RSRC=J ڑeC|SCLhkG Eߔ&Z0DL|r".o~a}Qx_"6&QQ)ޫnEr|֮]KyE9Æeߪl6+h"IJNbLK}ܸq Ljje{Xf P\ $'p|śll+)`)ۗDN:hժ8Ν>]_20'K.$䚀?tovպbfXVG9s6%%44 Aec2;]zz~}Qv3g8z()IX9ʠ)81?ZT>'NGшje?/1An}1*t$'sϯ Û~oTTTpu? /Gݻs o[o ѣ,}]uG(3m_hZ+VM_{ӧM`ɒH( lj.[{nJ|'r"~xd%L,ɞčC}9yHOA@BFF&W*MUQr@Ʃ^ɋa_z.$٫\W.@I%I )7п;mFLt40E0dp ۶mgΝdggŪ/p:\\wOM5 c;UDGYhjB~u]Dv@ٴi#&LdaILL$33-[ IÆ)p[Ay!vg3`&MȠA7Z'IՋA{w[nciܹC\`nEEE'8t:ee +EK48 FCidy2itm߆bK|+M#************WTEqɂojhffRy0n& |KiD1 toXgGVYNl6`ZX|+ccckkjػQimmE.qg#xVXiDl!p>6ٷrkiUUUTo߾MvSeYm*߅[.=_ZF~K_x7?_/S&ؼe nO»|/dggyNUU1g|6_A>Ͽ+1~XnƐko0tz/} $I ׬[LJ.#&:+X|9F={z֐ P^%&. 7Gx韞Aرڵ%j5tI>_/1#FQW׻wSZqFco)C(SƻK?aĈ̞5+W;rۭbs5?}q("}S7hWN39JɅ44Omaޜ9h.DFD0gE]~ @޽;gvs^Ťiزi?&c5%h5:,>(a@V6* vlgԑ|u/?Wr9^)5/_m]mېe Y˼sꟅ,X~mm,\IYa۾ ^$ގW`5pA4D1Ylo ax^ Jͩvp Jym֬[OF~4dqA2 [ţz&4tLFǫ+Ę7fghTn";|g_r/PztKueXqt)WO#0G************'o㨟mE2@A?޽{f h4~YYY\}ݾ3+-pp vo~ ~5rÎ#a4}q 66&G1'fq1mƎb5_z;ʢ8/}k'%%a<\IZOlf/^%===y+# HGknb`&W/lN''Nbqx%8v8w~{`vl6yPw/ǣ?̠Ax _Q<ߑxGÏ:>/|3'N?N2O=Ŗ-[e 镖ƚkjf::dWZʟZhZn̛Y~DQ ~a(;J㟨AFfƴ'| K]a??>++Ë?%, N";@c)~_dKRɦ/S}/}_/;[nWZZxU_\iKbMʑG}XW /0zԨ.ə4Z6[9S@\6+ǎ^ 2fCш!* 1BORb( d8ANCllnYVM͙JjTr$n#ObNa%8ZXK X̑qHNoILRIU>|So=BmaN !>LEb2NGÊ@|To(b-զ$&1zXҺx /(Shb-hzDQl1#b`ъW`d6a1"mdWqyM- > `2,T& i8ɩ2δVcE /6G1L0zx?e'Q|h-z$y=AXܹҏFA)29]]_(ahfLƯ~K Ǎ'#C._,/Vbr$,Z[[1|Z=Ŵ˸qys?nljm%:* RΚkr/ mmm^'W.`DY,>VfǍeͺuY_}V.^륪޽{s1L qcINVDܜAg*q© oeOSS| X(KJJbeOӫW/NwϏb`0PYy,5^ H-QbwyEh D둜.EDEH^ YdD$N,xܾp] v#cj`ddU9h)N(^=^Q (󫢨ED<޳όGetvg2+! c6=^ |i5?$3tX>#)@+vތVlj;COt׈Z$Y*96S4($!B&ۢ <Hމ^^^̚qXk}?|_~-%/ٹ/_Ox$T}9j************QQ]VPB 'Mf+ȭ܂Dsf3w@>>>.d<Æ%w 4 Z_[nӉd2uy?oƎv\{NB.+&Nτo~~B|߾}{|Z6l6w6==X'`k4nmݿɏ1#Fg6ȑ#9rdHٿ+ly+7[[DWjb0hu;.*hSg=?ƈh2Rqy؝m4r$%tX8: Z눳b05{ީmJۢ2g݁6mO<ODG)+}A .k|E `6+Lߕs9Dw C=h&TJp~/'%IBц=w׋#8qr>U9O0؜v:lm <ǫOf4Dh(^Ƀ,K4s1!>lw.T1[۔Mڰ!&&fLC0PU;%il^*l&$$ЫW/>@wl6O3p @~uϸkYn---,\t%nSeݫܰoy}%120[L޽)-EegMೠr#In dɷm5"^ (粯@EE|zAE^.>GK+ ъHng`gsIA|0dYKrqyo.6G ZfDA$Bg 0GVCS[ xz,F\D,#ci7H16# Z;V92kRm4Xk#q;|NG@JÊme(`4DtwVC9cӍ#5'cզLT~b)w4V92Ov<^zwƈs6>ʂa}>zbПz?X1n$'sfߣ#:b1^ES\NO2[D kK 7t31.b<+}13>[l]o dfdGᤉ]wN\\/O955죠`;wm[Cf"""x'bih4vŽ wq'Nd/+V0$?MFIMM E{0bp^/o\6o^`sq6& Y+ڼ2rd粕 IL65kk ÃV9u5`֯ۍNǏ3p`vH;|1\6rDš#L77?\+| ǒOAO{ʺF4,^Ǡ-ȦFE^LGtN>߬DC)rnCi0cm7 zzm Nׅj+X<Ndxݫd//[CMܹۛl3zyVCMaeͶ!:L~ kJ)=hE5Pyz;KI2e=.{ _v7gZsЛXW%8NߍAܵeLX=w#1^#뭛yinh=upϢ7gws]wvg[ջgPXZZ w.Fc\BnLxSZ$I{u'4 Fc$iTלnw4VwЧO^|9Iy/@^^=)v >nIA8ay +-55k0F}anZt~do?}?@%*{﹇pp0k"""yM+~i:sÇc͆NcΥ2oBRR<({~+|iw ]jRF碾YI puQy]LJr2kj]w7xpH;Y|U~8 ؇2)׈VAhZi A8;B'VG xV;Ř"1Yx6=HZV F^I)v5l%ڤ|vC=:!ր%2QTfc-)!Z/?1Z;uWXK*o!ԋl6{Xm #bht,(/Pu!}˼..d;RSS;9s.B$I455e |tVc;vӉq45ɆFɤEp\]bd}W~Wx]f?X JpDQ7'w\\'z>U|HKMսQq Cz.ZYߍ+WЫW*Y2)r`F:H66E0uHp$O$w2NWU-@CM#U'"CDjLGewq9iJ\WJTz@ֳb.̔ׄ͝7UQQQQQQQQQQQQaZy/r;Och9dkDFfi'zGfgp$vʓRKrV_Vܭmpq9YȔ._CZF6dwTTTTTTTTTTTTTTT%{9qCc@yItmm-laf9ٳ1X[[ٰa#Ga`7kMhnn&11YfȒ^"6..v,y^i\h55|rkNc60~<#GW_E#=UVr`Nپc=l9Ǐث)..p$:=rssWB;O:֬YK\`!]w,_Nfp'N >~8;&N?Iy}UU̜9aÇS$`lm<Q=UTT~,NI%6bhu"4ZQu+w a63AˋJLf̡Jr "$a%;ks$'pSLuiΙ/MJ>Cǎǣ x}5 $= CWd>Lyy9\p@2dm Gddeq&%33Aec2;]z`6[:b0p8ch=h".VU҂$I\|EL.,Den7%= Sзo_9yF1`d42wl;JJ9udOUU5}3f ǎhOnOן tyC۷_x Ci҂ A@#jhDQDE4FAѢ(iW (vgֵyed%(DT%(q!蜹 !_/hJiƻ!9=f3,_Oeݎ|fyJF9mH?]7 sP?+=]+.w]Ud?xHV#;}?.I8q4eWͤwF " >xnv3 jщ2rkm4vxH^76;_>ōPQQQQQQQQQQQQQQ9+Ǐw )3RFͩSgĈ!(2b h4lظ K ?ut([+1lP,E,:e2c/Owiiiɓl޼埯 ///Cd2֊EPW[Kcz͊_wÏ< E92mmvN:N%11 Va FFFfΘzF#nՊF6U&C~* «0,+2_VwID⫢BX*",#<YV|S| >~A9HxW>kҙ +bue=ῶl|,+?`{ }Cʃ' Dv`=P)v縔ϘLJNy^|ރv݊|UÓ;E?[˝4QQVS;pH%v1lX Mnv/$;;Jf}yůZ#G;;G>}B 8ݻغuӧMcΝ HNIahAa&L&#k^EdgM62aD'tZEoIp\PAp88NFCٱk^0ovbϞb͝K鈂Np`6husiWSL߾(+/go0a"Æ%!1cǏSS[KSs3ʔU碤Wxk[?XۄUe%~,I;$Jk!e#F |@N;s]߳j~;~_1-f&tM;t#wNoAH?]Wzlo×Dx`1$",v' N+}|@{o+Gil;/υ=\0n8Vm&>^2s9VKEEEEEEEEEEEEE_l68L&esθX"#3--Jp'cF!w v.O?%UW^ū``NSN'|vGrJ d6aka6Yt8/~e>"-5î=Ew}r.x'y/qH%I)@aa!C ذa<mmm\q$%&2axy/x1c6;o&̘1Gx8PVθq E1& >>~y_|_A1m:&K)2OC`%%n󍞔`Q^ĠƗC;|g/?|iCO9/7lQȒ v U5J xM A*FFb !$ d>D&dg'XWt ;N׾tN1dno+Bg> ^2g*oyA: Q\wwz^[Þsp`z*1s|eK1Z[[4aBx1v1(''=%4k̴5UE $4 n ux}]xZO՟m^oYB+_>Ⱥ}׫?'3iPxw%^ʏ[=tv1lȲf#22[lttt~[Ng@cۑesjJ^ݎd it:eH\.ZV׽^:::h407NLLL^0---\.bcc+2c1ݎ}wq)޽{ɉI5Qk%>>[8wxA`̯gݾ=w/?˾Q:8Lx A`etZZ_\jhhdYѠ xABdAD$dAPB@Pȗ'ZYDޗv}k<.XFW&9~!<_r$Fg^)9pѠr_@ah2_*POt8;.$/^F&!D@5&31#un>J~c㣨>lIP*AzM U `UAi*M)*("E @"Ho*]i ddID0̙9IB\O Hsڃ3qj%) Cu%]?okIyj%|یUu3fUjmSple=,J5gƓйk~3s뻄d\e[?`r@ @ N6^uN+P l^^h^ 2kuww 77$qEE^`qbYCWq>++ wwwzZ{Dp<\guYl8CZ<{scAAb,6=06=kk-Iup3xp׭RAo8FV4ߓMZ:t}GJlj2+825Px _N,pd.kfҡ1~7Gdfst]?ԛd&raJW!@ cZIJJVV{\gff*ZOJJ _,V 11=<eДX8+#\W=kubVTim6?=UmJݙW%Y*Fа&]Ŕd 47MFv*Mt”qz`A6/lfFoӤJ'[s%,뿮lt#r,+eU+X+X@ @ #-[䋩ӲEKmпBviZ:m; 䳻춝=d) ckL V̵kfh4btsh` FAv1  F{|a0`0 uzzIzJ{k_0J`;p4вMx-dPo/vdbQVK%yҩd/8*[{ơut&qW:C^GףzM:9wY`8s,5Wzg>L&Z6oБV'вiC?;h1r-9eX*"yZV2^nf*9i"+X<[Cz @ /dCxx ܴl[ OF4i69Gvι;0\"_VmQ*V_^?z6m"`ϗ,Y8w](dA.p# ĎB7hlʂ<` 5|T.8-WT,cJ5 GzŹ*N=U9-M!zŝ_m)mgUfk {4''Sy)wu,@ 0hv:p&n/ {@ @ $'7l o ++ z^ZmJvN$V;V9Cg_m[msu:d6vUtVt*=[g (ex۵]hvc*X:mEV{HyؐHAPb ~?v^3'/Uql@ @ @ <`32ILtέ[kQu:z΀þ:[%ZVzټWzI|W'׫o*\9X%+VYpDl:Yb׎Jķ[\S جQl!"# 6Jg\RU/jz{Y9S?6:\ϊQD[n@ @ @wa4[  A\rHJN$=-MnZx, vMx9W<] vD'jQ.U⼔Hڱ*&jl]T8u{܎%9a rUZwiʣqQ4P4TsmΚ8؏y͵O @ @ łɔ^oSU?j0HܥCuuxosKBnLHۍS U_Z] p":lyWvmOmNԌR68U4pk{دTV)TbPrUB][^?j%%%CqHKMIX*@ ӓ\Mb˕:F#\] hdVQU+ uZ&mI"~IҪwN6McvW>}Tw,cٔppޥ@[=ӎ7W_.*]tR6ug 늚mirg*g_~s`Րxbjt@ @ Îhhzqn!GvoJv2=,*)n4+yDe.۝tlr'(r>ڗJ:rY j*O%[޶El6>;fŭ~ +a[o lVHʣ]Ej3aZz*ϞW|aZE.^T`\:uF] [,݈֭9׬ z`\;{zdffj@ @ [y$<ؕ^W*:zy":JgS"ۮ6~SΰUQTIe>oTx!eyǍOz"KnV5tVƿJtGx"a ##gi1l>}9L&/b]Wtd2崯X,ڠ+r崯F+u>EܙZF݂t؃ҩ'˖F@ o3AӦuk@ <ܼq,GQח-[H]ع+T%l6sM j֯DJ([6HUlΦM̜9ʗ_/0tp*UHu@ @ xY#! of# Пӧr3|P]!GyK]}N&N'O1k淤7֭3ݺwfT!cXp:];,YTNAXҢE3:gN_/PDq:v gӦ̘9[cYl\x{TZʜo~,V+!A5UT ;+1|ĩgvh[LtvX d;oQjzۻ+0a8?j\ 2@m2L]֫˛_ӣ}lJFoc/\޽{8.\ݻwiҤ#G^g̛=F^z73`@_|5k8},.^"8( RphSWo F@e\"5 /@  @𷐖p]l9#))d&Ⓨfzz5@zZ:on u?l6yS4i,]=V,Do0xrSYz|kкUs^=O?]9DRnmRSR5r,^:)S'݂;vx')T~wnݼŲ bԫ_17#~`Edged>V{ z&} {qdggسGΟe Օ-*طo?/6U̝Ξ'z6f:W-mVL2]IgS̘> ߟVs/|3+.Gɒ%:Km2&| e˔a._²U޽TI^}…|7,\4r rrr1}6`ʅ7O?ɔo>6RRRXv=o|kШq'MFsU?jӶZӬy%ry @ @@ݯ~y޸q_~9F?|@7VG믿DѳWw&}II\zK>4Ov}f`0ٞ[$Ԕ'i߾ GߟFPx17iO?^jS׎xܺy<^1eDvl` 00_`I IDATm&vC&aJ50 :u?;HFF[dЁJڰ)S ֭MT^+b0r*!!~&'L2ڹv䐚Jgi_5 %r͍#ؿގޣ+z`ʖ-MXz1ժU`00ܸqS) 6%9/U_jSWT6=so525&@ #o&%%dSޕZz ?70x{ks.vzb9r)NLQF^ْWGXw>ݻw r4H||_a)W._êUZ2ܾu ir@}K`; w9yzA#+3zްYIt횼5|NII%H:iet=.@ GMNN/ZT(b9p5^}/MKkCHҬ+ &qjfMF9q⤃'YdǸ\p.i}xpswz!Zz=JWzۨtMn'pdرrrrH$4TZU~-vCff&ѱsklԯ/]i_HIM4 {U9tgUjUw. @UٷG)nfwU6nҐI1',Y_?_5oBjZz'附ȫ󳳳Yr fsYS⓼7l0g¦иQћa6KN:׳*ilRrEbsk׎4l@@=YYv/ _^'#Ss(iҞO"0K Wmꪟ Ӧ!!Aԭ󴃟@ @ 7 EFFnn/F Q0|=@jj*ō"..4 fXAfXrO?]KOtrU“LJJ+qYmzPrEeSjBB>b(L&www^|9~%kV˓AC9l EfL6"0 #Sx110I8ݻ>wIfMZu~,Uvګ)^UsY.^rNL2.Ċޤdy dffb'ޜkF9) i<+̙k ٽrssI1\ts*)@999def``˖nS$~ idee蟧a6IN6^b`X0 ~CjjyDj-)xxz|a\)x{ImSI(1.<3(2Ic~}QPꧢi~<*m+@ ÞOzpy4 dil#m\ٝ s.`mg춨V@V_^?&hz4//`XLJsmS[Faa#fv֯]Ѽҋ^ \iՊ0|J.MMY'Nd2RRSXz ?ϾݻHIMawbTv)'в| zFEQdIԮ͆?hpq>2g"z&}"++bcwְ!K/W_Zժ 6#w A̖-lLݱC{ @PըJfr*aètMmSlBMZ FC80͜9}OuNw7hS;"0a&9CpQ/YI66z=Aycoo7? AJ^_(AA}|K|Gn7gvFPPnWXVt:]1(M]SQT @ fhX.qŗkҔG;w1 t֍k̡Çڹ3ܻw={NdDƌfO3* DNN6;DMӬiJ(;իVi&I*UuCȶ;F.3+%۹ZDGӺUK0Lԫ[ǎpwwd5r?±2h=CZ,VmZPzU1QjeZmENJ; .$ܫX"o6!*Il߶"ݺwF< TeJk@ @ (ܲ!t:]:wKΘfN<ɴ/ӷ_?֮^EϨt,&۶mE} 1|(ԯ{CʕOO`%JP~~`4秺61[N^j%̚5_pǎQNoџ9snwϟ icbTyd޾=3?U qwwJT8o< xyyGhq:tCpognZ/@ @ Y4`0VH68wY`n:6nQQdeeQti֩ټ%WJȈ"#"0L|BϮX̛Kp_Yv/cمgڴayPڷkF;l޲ ُoxD&NO'OF@ @ i9{ݻ=vIΝ\ IIz_DG+~lBʕzFh~Mi-}RRRZ F2AAAԪUV+Xl7mR֘|}}yk@zFE)ACU{HLLaHOOwg`4gSw>(U HIIѤ@ @ #`B>y1i' _LJ'B)SY3gٔ|vnmTnc?^k '^^~fSNo#G4߉^'44D02@ @ @o呵_!!!|1u*%++-z1B,iii,^5Wbm+Zgdj9>cǴ aAWܬ߰[ni.mf6=6Zᅵ$Sqqm~ wwwm@ K5kiII0Ţ `:uJ@DF^1۶iT’l2w i0lmG[=_ծZY4h'NbO8ɂz}!g;++C)IϞ5v!ak۶孁xg? 55Ur?utg(ع?lz+|g\z// u]*222yop7mFg='.Hc~X#":tf?zXš4U6{>g\5mFZ=֥t27mFHEDrIF#III|0v6z}и޽˹NyOhQ?:ϼO\Or?VWٹ{[ |W @𰑖H ^^Ibb" vXf[m>vTTIiӊ짧gЫga=X+} ''G/gIܽ{OB>tA]$Μ=K֭ 6 @O!cǏ='M[{TON> &ѡ#M߸)!K䑚JH6?omx;1qdPzWafz=G-x FʕʦG^$$$ٱhV*7l]D$az-^3iڢ[ccإ ϴkOxdo~E)SЩ3;Aܹc_hÌh<;wϋ/);vC.k{G(+RA؉ԩNܺupbj׫G||J[VL /rYYټ=hM]D$QfΚ3iӶ=zRg5 &Ѽe+iɓ'5z4[C\pdQYف+W*޸q䳢wI|}؉]uxf\Qͭ[hӶfYϧMviڢ -&!##z>ߛM(x2 xxxAygP߸ A~߀lظ!}^ AN9C^$<##X,ŋxo_enZ|:w!CGG 5z4C.]ccG^8a7c0s,f O> Bst֝fQΛOS6fq1k7`AZ|y>첿9Qg^a-@gqcBѸq#n\1RWƞGn޺W^~ooot:}_{vEٲe4(U M{y|tL P<]:wFs1 s&+laĨQyyGrO0 S1۶1pL3클hFr6ܼy~فpN_bl:uJH:u06\U?gtz^-m:uQf7l:|7""<fN\gBA㻰s٣9Wެ^={Ѽe+ @RR!Ǝ勩S޼ sY%ݻ]c]t:X JM<%܎gh5 &Ɏm1 83f̈́I3z4;o㋩Sy1ʗ=G~ &Gȼ 8ykV"z&ʔ)?tH^=iӪK/RrMֳe޳G<|J.MMY'Nd2UzK.qubcʪիIII%6&۶a0zη afV,[ƪٽg7׮]jrq>2g"z&}"++qqlkپ5_zk(ːi^"ڷ/ɓ[U˗3v;:vt6l"6֘ڴn;=iƫ} GMY(˯M\k-T|"~~~غy3׮!zVbw"yA۷tgƉ'[{wd҄ 73kר_&Nk.Doa\vዩSZ̛3:|/g`7߰uf D&d";;5lŽ!mosSDy.LLLĔe#ҹ31[Ql۾]σӢEs6f)`WӠ^}qԭSeksϼŽ[u(b7lX̘ iӜ k/]|ruҥ1/Wzu*accX"ȫt2([bre--yJIMaђ%;ݵpMtNyo*Vx-l\_i'33fYf-YYY.?Wrz^-sܩ? IDAT6mֳ{^w ֭پ5^/, 9Y5Z\}&EYwnF/dYPxORWdS2.#ٻk'[bH饔@co׮,]}wWg+a6\σG n/ i&t{+/Cm#GL|)ڵmKb<۵  ԯ|!- =uZOzPvmnݖ6rwwgXjլť˗1 FÆ`֌< vJNN6&]s׮BxOz=ŋBܸqd?gT&l:v$&&mZ^jW+Yܽ{.Qti.BmZ&44HWMV-d2QnnF;F@@c=1c-5SDFaa`YV-vl߆h…/^+Wm>v-,%/IFt-J,I:ux駹uKe6}W/֭ߠҘsƋ/U/s3W ULo4 ѣJ8Gu0LЬibo/tZA~gѽ۳a4_7nH㰨|p!-\̯fx套(ڥ3DӥsgF#K0}4W7yMJ|͚ϑ#Gu/l, ڥ O<-5Fݻ$L[DMzF:5?S>)Lv𷑞T'^^I,]4cҥK󿐖FRR۷Sqqhb0c 5iBehذ7n#)9Ç }@!{wbwJ/ԸCL6ܡ\ٲ,n.?WJϣ)J~ vzJ(xF Ԯ)\Sg]^fxyyFgr N\=hq'E͚6l2/ʽ{0cGnb4lWC_#GѠ~}{T, uJ޼ySٲeHKKWl0X M6i?OHOPV:"yaիgi@Ls;>^2e ]Fga0HϬv|EOʯ_|||2nsss=f ׯ`gRTDkFGEEjjrm6[ծK-cނlVjIIq짔T}@ҬYS>x֭CX,\ܺu:k|Ŋ|R$T-88N1 ,7f1g\ʖ-˛oAxv.?WzN5 X,feRϯeʔV~\c6 Lo|u/U=~PP2g?kly.{ڹ[mBFA`a! C@pp;w%$$`/&/$DZ6f Y1KA̶m:xzz˦[K;11 w{?H_$̛Kp.FmO/+/Ipg|Ǐ僺oݒ~e}`&2"ȈL&}~+6V́oŋՌ>|wwMu:Q=ziʕ+˳]7Ԥ*SRS ,Rɯ\ټTT/KRذ.Ƈ6n,t:ԯy(h|?׫'C}-[R,4J+Gر:}w #X(ݺJm1eJ1Lvv6zN/l," ٗF=x𓅜{/!''ܴEk.Y2 jդpqWf3C}w̛hړEnn.>>> ~dhV+_K/S\|777v:-Y% sYjjr*=)UҼY3VZ͛hFZ8yjqҸqc 9&(PH_$L>[իWcs.A*U\~쥦*/rn޼<5QUgB~ͳW9ˤ;(55UWAϣWOUEmVuy 3eTϻj @  ~CW &ׯύ7:DiѢ6oQD6QfM Wnܴ!_5k\z ?P6-,Fѩݢ"F͚5z OU}ȉXj[buǎԩJ5F̬WQV-VZ-],vԒ?ު8jbu6[lAID6t6m7=X> ))"3ҕZYYY,{rss6,jjjj)l6ѣ;{aƍtM2/[A^eԩ[H}^UTW(v/^kרU,R߼ye4f &qܹB~(ʳj<uÄɓ! ^999ߟxt:j֤B t[{^El9y w3j2l8ȫ>j%Jc.WGm4BCCpssSCo׎sϳjw{V/JzNԸ+ K.D>)J} 箝+[鋇Œ11NEMQA@ H2sΟwgzC +Wr9"#"A%/]KoVs .o<Ælf-O]}җضϴQDFF&^^WeddN^m\|ww7z쩍.xbLx󞓓s;߼yOKzY^}ٱ#,3h`4l,C ak3rh>?c/Cs=r}5jI\+9c0lH3$\{_d6u8G~siII'rUWrϽo=nG{[nÇӴiS;ƨ$I4`w.g3-؛%_LRR}ͷ3fFAA}}ǎn`hƄdzif.b|ƌkUUws/sL?8Y3Ltۤ {&LDSU|>`ǎ@ݺu4;yC>Cv׻7>ҥ8܇Jbb"'9sNcBCyw8;1;m۷oGOCTXxpLFfP}xݳ7kι_@$AUU^|\u}&9y,W]s-{n8}ƢsO4ڵkǘ'ƌ@%7Qm3/;VZ1c.k~3w.,Yd5?#=C8mu{nӋhiۧ:^?Aӄi[aAvyٌY_ݏ&[b,^[n]tP>fZO_?;i֬Y^#wx)DqTnuܸĿPdgΛc&sWpʠ /ꔩx=;'Lv̛?QÆq=Yl9&+6ޫpgtm`E!AX[t6u mȈ uyF"{qq1 u> BTUU}!D"BP>4"RSS p8n7p2 U~cOv04.Eh4jv%%%( vb؈n퍩iF\̌Z"Fyy9!2k u*bܚu~9iGcǼ.Sk. 9,Fڲ6mg릮`ǃy '''$&&պCq>9ϳwo>=pRSbM4eed8J03Cs.F"JKa;O?/x4:qSwυڬr"HG(4$ #gqW0|0.4uSǻAz0+3 u}C9r<^EQ?ZqM7B=7.wB]{}{)ЃлW/:v=Ζ-gB#` wI͹)* >cQ߹D' _מȺ!9!63mf: : hfZfZsusv杸|UUUw_/jG~~uCjj ƏUʉFU֬]kOFuqv1sqns;0~<C%ӶM|50Oz n?>ïIVVV?|VYCee%k۾X{{yy,Sn?p;=:!Oo^r ټf$=~:Pl P_ ym,nx6>p:.rڵm^z#Ȳ\薒>)b1l!x҈qƎy]5sPTTO ߆^7u}fu^Çr9hsOn:gŷ+xͷj96g; YcD"̺ ,h9D=>P; :7~f5աҘϻ/}Nڬ!Zj~%@sL kQ=>:C{JCdQZumC9]׹Kߗϧ}[onuq>}'O.:u]y>4tql>Ѯ{l}:$չxw7.NsZR~?III$%%Q<!ÿzAs||5?v,~e"??koa`%\qU8$Fyk׭>Ip5oޜGy~ŷ2vF͛ommٲN_m߆&$xts!;GM0P@:Djz&RUKKw!RVQ$+D GHH$L&GFj:{绻!SDfMgufĠXrry䡺.-tCÞ|m:lK&@ ~4iĞ}FaFRSX ))w"sϯSo۔ݻ3w\< >CF6uݻylٲ\vӎ,K=wD}6굼R/_sL}u[߯۵`9gszTܹ[70 |>a{eddd0h޼9Ng}n{n*hٲ%۶mAj7sW!/<|Lj.\oWH4,3Tu_}5x˗૯v,=fF(bƇrggcm=O 3KJ pJq̑.8ڶM6|jMrr(--)/- EAa!MČۂs R(+W ( [j[L}͸AѣW]w7bM<-;v!9)!X\  G!%':H hZp$J84]5 YQUPƃ%?rFeeiWƏI[m{]l?fdK((dƲ(^>]?>\2@ @ oa; kbbb}u/&׻7|r%qz6lecsYg=CS^{h4J4E$$I"7Ş{<Ѓ<<ٻ]~)S_`eD":thONN6]va֬={6' `ohWg}N֭zQU/-bۣ}C|{`/-b@TUV-x/K6mDuuźw7,&2胃LT;ņ ϋ/K/Yfb%D4?:v -#ESRTBBԨ#G%| ~"DjjC!啔WR^*LYy5a#Q4]pf' hL*U8zZ7o-K^yWz@ @ xhX*++c7lG!<9y2GlIv܌YYYLzl"o@Vv6"zIUk1f9;:}ǎn`hƄdzif.b}.7ĉipW\v=qcrw)v(IxneMN8{'>}<̡;pWOkE5v'"I=׬Y yV<t}|86mʾ8sls9ch֬)ݺt寷'#ԌQ`COAFz:Ç CeT0>nF6ާS?1q  m۹kXf'D$tRHHxT$'Q$Тtd|ɤ$'S,'#!!Mǃ%-OZ@PuMU%5x@渶:6=߲`tbO sdct]S),uΌ$G:q _!K2MĀ.h.wOn 7Wh+u8;_2fwoQVUB6g W𷩧sAq}u5wMӏpKIO@ @ M|uKřk+ik1\{͵L[k;o,}O^.neys]\4v%DbbPU@q1YYȲiE4ee< Ȉ+A|"t](@VVfXpU QqcQVV($%%lΛ˯NMKÖ-\xŬX]gtm`E`so[8Y[11_;ߢEAmܸٚu6k9Izh4͠In.$ S^YEuU%,E" ǻ蚆/OAwrN9f.{KN%&0kYs1yYm!-buX$IbX|\r[rՐ4&ϼp9/ql1>\2z1e,]7xě_<Ƌᦑ8 2G':&ϭp8={h+*..nW"!ُAJF.e%AV-^Lt"x"Q^Q*"pY$pu5! $zj؇p~O"3|q6#{]}k::TQôn҉۾ P$ɤ$dpjNm}kxY$Rv.p+Cݹl?`t$xDz;VL cڞĺ]KXç 9>^+~nǓn@ @ Aal<(&MЬiSx!nw=3>de ]$HHB%MHd.vmEFZ~ISIK𓓖Jۖ-Q<"LeE% ^=R+ ϋjP5LoRVZA<3Y<:j vViJv9%4hEaИ!F9HO2pԈd7h5^3DVJSӳ,\ct*·۾i< {;@ @ AaxLolBjbyM"I x}>"::P?lgÏ(,+#XUI8(LbR"rJ I:Ľ2E4zƔ?ɝ+ɻmyfknzi0*NCjE3NmJIE!LQ;DdiwjѭOyBk @ @ A0!1Ov(yILLdgѱE3%4_͚6c箜x|?>fԉ'%U -ƛѪ * 1mp .&`KZ*~]?Jwywx=Fe?'Xp]FQ^]co`;8_B8ZMd65׋d۟Ybz?ݜ@ @ /4$'T($''z̠s^s:٧w/>%`-+"J$H(*(x<(("2"#˒1lo0ڴcTfʻ $vs/)YA'm-WA3 _׹6>rǘqEdW /ֵ E7 2pe+Kn3+_~}ٗOzVs"%Ț/&X\ʱ폠mӦ4ȤS^ ZfeͶ?##y3R WK >?]2BUȺ|s1D0U%dh˪IB?@UE4*<&C@ @ _/sL 7r}oB%l#fZGtPu̼ʹHiJ;N3fNw~`5 $]2t[dK7uu]G%Cҭuk4^~bj?=F 逹~XFGc?V5?F_Y}]A:XݙZ}r.)Z[60DZp?xDRBiFHҒ2+ B *ǟǧҢJb1ݙ RHH@jz*j8wAw3cb7>@ @ ??izֺBn :d`#i:i:YG$C筝hRG7{;gbbwveWsi5YS`IHvb՗b kyxch 'áHLL$#+jn.)ȊGBz%eA~a;:t&,{IƯ$Sh@3m P+lþ@)|hU踶b/@ @ sq [wǣxxxαXI8ua[ CdwoŬBT$ !1TM%1%IV阔G*2e$Z7oJiyp¢B4$ɏRJhlu 7n%=%E4]b@ @ S@Su4bW5$d44fJךdIȮiFt3FC7eY6D8Bи$w[L`4ZTZ7Vyl Q OojL:Y%x^1B!A]xx}H@(XLvV٨v(~QT_F5|F루$@bRd^dDQŃ$]ax<z{'19o_5k.@ @ 놠KhQ )y a]AWu4YC$I5vC|uMds[@2z@Mxθq=8ijb?1k y0oVyLHCP> xᠦIG(8ヹox/wLRV:^ATH=- IN iIT$FTaJ&yXϚu6ISRJVn.hiH#$U,GIIIٳIJJ~\! @ @ ,)H(Iݘ)kz馗k^7[c O:\4M:?ˢƱg״PQ UR\@SUR<@4E#Q*Br $cz ;;]PPP@8mےHm)((pZ`rX㠘$Ȓ "$3º[R؝6]7LKFtѤ!&vB2va\uFujňodfɪYsTFlxAm:ql:*%C̖5d+ `gnhLF :_M$coMPVkDC!˂$gS^^Ɇ];PN$Ĥ1ݻw( ٲe+ر=f۱^,4ɡm6ٳ.kᪿXq߭ZEݹ[ @ @ FZZkի̴eeem; :dEA$:jV]Kcѣ{w>;f͚vZhrX GFeEA$!J) D֖uC|mUarc#z]0&on,rccWYY8oHFVڈ<֝6Gc- I@@1Ex]6u]CetdTd>q=^$]A=$&&RZ\'1ne+QBa$HQ$DSdvH̙(#ZPEr4&T]Iyy)D--#AdJIMDBމUD 'CXOa銀 ?/Dnn.>˗૯vW.?n+ߛFRRpuruײ{|a~t"={s@ @  >cGyf$''SYYiש %%űA(wߥGӣg޽\ynՊ3f =!I !DPJT=.hDѥ(: $!5oʦ8odI2D_XiK}zC7 d$rS;wեׄu;MbHjj7ޝ1KV IDATeY~tcdzsğwƍ*sİw1c;c;:b,mb藺VhiAٗ JZ2?V%*JRX]MIl) Þ|KeD#\|?MΠ2= E" {ط7vS> b񨨨$77/)f_k]t梱c9Ȏs;ɓr"Z(++W@ @ c >cc>ڷk~u7Ѓ]o̙<UUU}>\a/ׇi +.E*dYFQddE2fްyY6xIBdk^r7?Ir(Fb J]t97c6vLzBo0u+cwb7zwF #s>j:A3*h5ai4fx֍F%+~̎]-&.hӣ%e!Gv xazr9MU$EINѬ*`ɧpDlefw_!@N*v|'#|ZDVF+d,ӇK.뮽&cr 7~z4Ucٴy3^t1=z<yd]wN8acxl nB @ a5kL{}9t䑼(-[ҶMzvlٲbo0~L:LN>n_K\ 6ʛ~޵cm3m=梘kik7~͒O YΎxQx &lNׁo5mq 0`ۊ}{,Y>d$3mqlc֯yzR06u;6V$G+/Ǻֵ EIAA6lŷtm`دYlQs6QJF򦠅*.مZ]LLd2$5!KNZ7m Gw!o~=v%Q>:"'f>1^Dz4`Yu*b͉UU6])..FVA @ UUu~ښ~-z`z6aFN<@ȵuL[먙ֺi!#Zigމ;xz+W"BG6ţȆخ(d^vS_3.Մ[1&552C|\cY0ԮBdժ;`5}6,*͘cIL.s 5݈oLj+o3Co?&9p H|J&$"ySARR/z4? {V($؊.@ @ J/+ph] +tIF$ŋI$K*do"JbѲ|H OW!f#Ɋ1[xB Aƛ+@ @ @ ӰqCn[ʑH&fz>Q.@ @ "ߘP]HH~v| ՜u_oWd떭 : wq-TU%*cK0g‘0g/8p/Xh FzZZLwb%v~ч@ @ VdAˣ:} +X o,~|v{Jƒn{,o|e;R:o/EUu=?`U UoX]7Վq;owpS***xu۶1owZ4%?,[}6vTUU[oFEP@ oOL}UN9TÜ;xmƮݻpL> wuV~9,Ym`}~~mY=@y#='QU瑹 VElu=Uٸ}%<>zL]I1nj*õi+p TmSoepkrstSOWf7zM(**b *L:mn4}y0:1>= ~Âߚrx!s@ ?Bٻw/NYYY\}U\~e15xlظ۶1jh};ﺋQG?~n;eСoz> lʨѣ).. _/'%^j{~g8h#G⋅Əxm;u}D-ݫMiٱcL]>ƍ\H=/DjX򪫹x) 8Bt]gڴi9|417x|$N=N>ˮe˗m^u5\w̜5Sf,^ߟΩ3r(Xf 1g3waع5n}"ceVj Ec2rw_%9~(Ln/f*?o8m:d{|]d%xbȒDqe쏄sl~`ka>i 5py |\Ln]7'56y*j'gL>a^s=U\śvQV% (䣵><0+6xgqqM7Ϳ Y\\ÏNt@ ~s=g3?͛7B͟9sس7l%%%_~+l- /G[ν+ʕhHKM ~5^x˖qǓoҚ EEۇB\w]o_t߭ZEnn.7\wIIIPXTk;1b=><7G3g1s,NxW\q9?< ۶?H ք0[b8ٵkMs"-ZwxSwNt2{˞-",(e@ʞ+MfH&ii~z䌧'g<~Ν?G\| <9f̜I̾}+WfKxsX~XʑBRr2AAۗ/gTV/OY֮ܵeԩS? RPn=VYÙsgUӼ"&<`Ƭ\p@*W v/)93grmdD6?v, t&Lzn VѮm[iݺug Q:2iN>ʹ32uRrKqL oQKKIƺt3ћ Zuկ4dSWgF %3{T'ˍ+q|Y,@Ǫٶ->6{&`X|6+Aβyxc}sdKKuMxqW0{5 `x&IhtFzu 7. HՆYu~^ ow9̎ݔ,d2hZ.WM΂Pz(8}?+q fZת˭$ U”,SO79fӘҥ*.NQ"pO,# ?i_փWg|w!sT=n™ :z); *%Ys!Jz1w-f1+vRY_qiٔ f~Z*=H6޶+JFZ%p1h )d2I-qcUQB= &ֆIe,_ ?,2=0$Dޢ#nr pJdX৫ٶoo{0$# oT Eg4|Mj2m=Iʕ;{6 e]r0<-[ҥ)l xC!׏7bJ4Z jdάYҀtލEڥ ~5kb6f 4nOofUp0{5jpfϙKjj*,<1㜯],Zh-hOIKKc餤S#O9Ç}Lff&r1p@^liZz؇$:we_3v,gΞLJ93gҨ8e6YɄjZE>ؾ'5lȴ)SXlٺ L`` ӦNO}.$)*бCGX5FQiYlmӆA^o\0ͱthߞ~}p%fϝ:CB`C\m) qfzt;nJŏ6)* OO/ԎlfuoJž0L4m҄ 6QR%Wf}O&FN['23ӨcUoIFbI ?O8(ObI:lє[ȅm3n^oQ#ywӾSHWl9 gknpC֜xHbXl\ d"m'9t3eqp|Kd2Xz.$ݣ콚Ht\~C}DSJz1cu Oo4Z"m+p`\  dU/ZP-\ړ]Np%G7cs5V5нv8l>F72{ FP`5.ƳՆӌ5™ÚCMr%.-ÛyXJ-{ۮнV8F5NktF6}̴nx}ENOcɡ;|R]~ oQ>s-M7fӰ&Ou>;ZLÉ\Kb54.m"UG˫5c0WQXǟL7\ \z7V -+Z)I*|p7YJ4 ts`ƍdfjطgE.WWTwz{Pzu壹sx: Ĥ$ޛ>gdOmk@E7cVvٗӮ7L&ƽ6e׷#FưDKO~=1[A?F.sY D)Cbb"7bc)W,*TTɒ_}5FHNZ**իDFFƍB˖-9l pLNXBBB]gQ(in/:kB 0|WΫq_d^덨NWPw IDAT5yxx祛rYӃiO5'q =aa唐o$Np&Ϟs=о={0jhu܏>7s[e9z_xZkTGǎGnH'|zME 2bp=zD\|Z­]%f.<Πl=uN*Mm5Mme>($ҼB0}mzйF޲(ɹts-UDxSeTlV].| v>S>0rJ)L; *$#ShVzl}5VTyc~r]=q/ 1론Y`b]k.(םgd SAFWrzC\&Ad O2]=@@!hX&8۴ѵf8Lj&Mf.>ʠ-Xj=j`c{MƐ4Ŧ"=iRZfD{RQT9!tf=7 `2k?dYectjW"= Id-J:ЯaI$ 傽yMFǭ$-mo0 mɠNI]ξIB kr̃t4VT{Y/:ώv|r#-K[)dM<[=:Wq[utB<_7n3+:{&0 ˖~AJJ ܡtܽwWBx6* lc@RRNL;t $${Ю][""J IL̔' I]:wTɒxxx7&anZhOߟT>DVV;ubڔ)sqn߹.zjNi-׶ٱc'˗-O=::~$n %JжMkujтдIצiۦ5~J%jpSh$ɨ[.ۛڵk?f/r[nʽ{SDFFRM2-jBxb.3[~J% CVC4  r@VV>|򤧧wDZm6r)_iii\vQn]222jا)hժY777aaXoF#;w!hܨ!fr ֠9ATkJ$kwSn|bTqMq}m<=5u7+22wb6$'OAp W >Whh(O0 hrXf35%”냎P|8w.L.^شi35}vX2}87n[Ns:rdoYf jLJ'Oc2/ǰ#׹ zNZXr9\O<wNlոL,Cn/ϼc$-9}-x*Q*/7q`N6VH2rI6G0u3u}ո( չ%fS2Ay$|紌3h5?2|5$rV0mj@ad=̠R=GF#VCb g<[y~v*iuWҲ|5 P*vU?Z}xNzo{SMm6YnOG9?4 ". /~yid9s W余#'y!..7Ǎ 2 Zmܴ4~L&TI냰QQqdf5EQ6m&%5'gNLL$!1_qNY&   6nLh4:]r *ZmsrmQ(hPخȝ֘w/!8cۗ5}+C|P{Bޔ+_, G_~Zhڴ)lݶMص;0>uss#>!kׯkjݪ[m廵k9r$7lѣ̚5^۽0e;MǒW+/1Z9͛5}v|h_,]JxX.^Ci-HN˥,[=`kڥ+A4iܘgΰʕ-UtzYעٳsPN9t}xR7b -Kp&7_r#3Ӛ[@ I|_SNvT:Nx[%SnzVZxc8CFzZ *%KmooVA.))[ң{w{W?kڧfX+Tص]u4j핌 1G3jH,,O#=#o VzĈ7boG˖pz?d!1bJi}=٨СĹiٲo ҽGo@TǎTi +RŊsb^cSNcINIHMK~x(V^͇LZxۥfNa^~y _?@޽kC >m͚={'ӧ_?3{6+-(ݺvEGi۾=~}m6\t5n"HڵYaֱ}ԮŁFd*;+ b#ܸqB ǖɬyu I/x̵D{P`l2Jc03uȀ~VOOQz6S]dF .Ǒ)jyo`}ݕz;Ҳ|L9AZJZSo$q;:y L&Vy{e}ܩdekf+ p]'Q=@ڪ7H8Ӳ Lz,g,>uo*?RvdxvuU+Facڔ֕Ul9^{ܣ >o器yYLlݭWۮST̃tܰ\u7KoTͬ>&BaB)=8k-7Y\[)}5=5OZ~j_./9WaLJ]WX~؂1bUr+Ѻv_{_23\? ^Zol meV+fNNwa>ݾ-dm*{iTE fS)4w.mVaϏ=":z7EaXV-ֶܽ&=&͛Cۂ_-:_ \[KHJJ~zعe~~~thߞL z7F$11LFZ(_<EXXeʔad|niO\Α5&.>Ξ5C/XuP( ^6[o~4ޙ0&i,{xNU9Zk׮͛7~Wt_22 k=uPr%jլ1cdeeѨaC8s,rϞ;k\ tl޺zr6Q|ybol^^~j[,]̞zY# !S_^~=_y!? IC fȐ| .vmN p=s}xZ5adee!I|5{=F7r68 mP*i_ KR3g`e;rssM6p"˔N㸚=sMzz+߀5~ƾIFFFWU* ` ;'f (sN߿Yn+Fލ7+LQEopl߄슎l6g~ QxcHxСC{~* PLztF/Fjykx/X@ hذ_~teO۝÷RHXuzn [F oBo2&jTd-,zofP)ėn up1}YdU-1/.7S20'1z=UVobDY/r fv]IGĽkc5(EwN\6m /B"A9:#K~j6gs֖CAFOfǨj!:q$ږka]OА[2Iٿ_xbSmuN*:TP6؛NKPՙO❶7z~ۅZ0EXq9wv|q|w! DJ*VBqn*>;LB}I6՛ ,7?? Sg-?`dLaQkC$>w |H ,u&8}?%QSNc5x{{31j4~ jpQrx׏.UӔ8j߮gϝKkş-t mpyz<lO['TZa2j @||hނl _]~A_{SǏ`cx>l`N<ɨѣyiSUF>} |[썉\v=Uw}jTCuwWj<}^B{wh0.TJe2jrU0^5eVEP8Ϛi, z5h0d($>y`BLf;{Sʟ-߻ (Jjj*^^޿ 1LX,Vf3:6s?tĹoQNO]NM:?`4ںi̶﹟[١bá#kV;h5ԭ]uU0b\|T'Oо];2u^ma{]96&zs.]G$(8-Zy  yW9|s9nKOVPb%b4-Wg2tY&FUʛܣCP&t)#SebX= 4'!b_6o/\NJ/QUgALVdpߑ$IT}(C^ݻm[X,Piȵ*z(Y  uS @UL`` lk 7trJ,]zuQn=j6k6bOjR.0_JO9r;z8+ǝ&e  yzzvj[=߬\ARawv   T* |m`kAѭk?]@EUp%SvJAeIAA'0A?T"<dJ     S  !!hh4AP@!!    ߞHA#߂$IԨ^GO@r($g    ? !ˉ,]ҢQMAAAAWAAAAA%AAAAAA"/     AAAAA?*|A233P]{koAAAAAAK+V`X\#I^cǺ*V˖%d2ӰcǏMڵWܾs۷ձ AAAAAl 5k|0ͬvuP>۶oNi̜=::wGdiM=Fq @JJ ׮_s-     ggg]{Jӑ;'OQb|O3vٴi(xs>KJJko7j1c\{    G f IDAT Sٮ߸A\\<7bcx2TNNJ߲%,[U5k`ڌz|kV3ۇbAޥf̜=ҥK3x 97߮d e]r0<.1{\j C_B`_)Q*޽w֯_~EThB*V`vmXf6u jq}C qct2l6䙦Mٹ+E 0utBCC5b -Bdsݺ~dڵM&4q5">>W_F>}tFGSr5K޽Xa#ƵXAAAAAl VѣG( xa2>ss?d֌:SMFEڵi߭쉉afٸiih2Rdd^g=Ԩ^1{h\>7NFa1l8;wlR>_@ iղS9ڶCiڤ C ޽AjTj[ŋlٲ ?uS`|, |]:wF5cy6$Fu?{&W^eM| ۯs)>[-c7ǐV#Iw۳JŢ%ػ; rj]AAAAAx קRtЁ^u=͛L8//Bk'55CEN6ehRBx6* l)^ III;~?VEf쉉Iƴm^NDiujAzS֠ڵB[ ޽rIuRn]&0ӦR;7&Q-ZӧI>}^D F$=J^# {׮E     4?={_eDDDpY1z4 e\'ϧF̚1U0h԰!R)n\d&11D^~eY&{.ۃ\R]nǛY6 )BCC]{=Ţ%KPou0+WΩ_`` IIyGZZAAAeAAAAAx777^Exxx{$gQJU=z̙3gR >c0e4ktԉΝ:Vvj9CdX,xZiQT,Ym[܆^-_Nhh(/^S$In)tlݶ͞QPPNԙj쒔RIff{\|[AAAAˋ^t]ݻy"""x__^חGΝTZY3s%o4=B GXrо=, `7od2+zS9OӨaC8s,ئ=w.׮] M.]6 [hYZ:Va41d2d2:u|?y2g,>;?OpE?@떭8x=~%q\թ]}!KAAAA[`άY ߟ,W]c-P`|J*E)O\%&%كЉII䮔J% cר[Ӱ W_ѩKW]֮e֭Ĕ'#x ϿL"cG>|TVAGL8`RSR_>+V`M'ijhBI 55׮b64q7bcyi@6M7f!xcvf C(W,ʝ^v3MҶmΝ?O',4d>C l\u1}-w"D :v?&Ob*~q?2*f =zY F 7 9F[ G"Ks 'U]EA׃قhb6qMWw~0AAAAiyD\~99~>sOOnt}ۺs?=lGXиaY*ܛƳg^?KiZ F!'GGPPSZLJj*A;u~\|||RR;߱8Q~W[m?+-G~JeOI%IӃgQwu?H_9z|Ν:&O%$)1gb$ ooO,f Xmf6= ҹ$>IBq=%Nzuu, 53yUsW{>bul)gb NTcv[ʍ'\]{]ǖu=#   "D,?̵2O/O;vP1rw: b΁='PSFy=PH2P(0Y,XL&FWʡSdgI70Y,`XzZ,3:t!ֆ0nq#yկto>N~m>%C*f4֝?tk|x*w,pxQ.j@r#w$'7&5zB뉸y9_q)Yc:ɒqqSxp7"|;OJϖoh(7`安܏BN3N+40iyԫԑl^͔o:M7y:oؕ-|=[ΣKU úƥ;n$\EDjq:^1AAAA^9Ar.i)fk"AHf l իu׈;s*MCR*1Z,H ;\]&!۝RaAX YxVG4ڕwz?:/ڏO{,"3;l6vtM{N.gO ̈́(Z;Ƴ* (:_k\~ujO8t~SyY9jpJ!1>FtMlq/$/gVvd 1>Y)24$ǂ,vCѪK,:+w[^2u37vsX{T)muMzrKrY52}e7l ,>   $j p-6l8B m#•.G7P&ԏHo5F_|J٬>9z"+VrgscN$aƂ$B& BU*L KL]٩_Si[ouEoa4)\؇'kמ}x]ǿ#/Qס4> ܍Q/K6ؕ-Ĝ^Au4zamL&Nse܍!]>̬͋nx^vnZˣn n`UTA=8ta)wlo\^24DQy?ZNGoAUjkͺ7ziHxg>ܻ{lW邀`5cbgLSأQi]]bX@ "v]ewcΙ93.b4M'{yO{ϙo}|}M}:?m;MXxk?t:ߟ}=w﵍eG?-  ^;mօWщݟ-˼Nd/&r=xiocYmX,*xQ]Z=z=/yrM[QX\Bw+q{45b,xMn|`.V& Q':l)=ўLQMMMQّZ+*GϤm=J}!~[_&[+). / rs3NlG}4Pb㤹gĘ M)/%qM5[^@Eh?^Z^}aP[۷͏{8k1J@a^w?^^r?݋z /0ԧOw[6kгtoQ2m[Oߊa]AAA].'⻋z| .w\xDd'xBh&#ƌi,#M-"ʪՇˮn`5PȖ22|ش_:eOQ)L$}8(AX]ea|6<<aY,sk3dWfg24W {\ڝǪԸeẺ߮I"'' 6zy$bqZ[tNWTH~E)mmc64nG#Oq.ySx---0WϬnP='f~r$o_|JIQ/λq>^Ͷ5AV-<ʋ.cm#JY@>/:{W}E,1Q߸t:I,C}1;'jc Fڭ yU5JTg\¬}5{u&:WxpޕsKM[1ߏnydoQ:?K66ʚOZ̯_pPdYe}v    ttup\qqǻ\: h? l}/h_-~FͲx?u47{VQ24DmUSe2eCM[^~Nw1NvJ{6'q8ʖNw}<9WǼnek8i1ϐc9"- 篚6m$jvbEn<;?eŤp E,f9 ͤۓwP/vxDN#L1F8`txpUl[}s(ujiV ~N`י؉1@v[Gjkׅ,9X, ޹S= UsYiwۚUeЃ-Ewn swq?^ )֛)gRm3gܼ4Zّo;{ /+yq=O   k%xODT*E2$JJ|T YPD8Zh茡vzL)ه P}>x^6_AY" /k?*c| o?7mA Zxw<,8+MA^צn1P,ߵή,>ٳӗ?^d];q~wQTIb#nDZc6eE*ȣg"e%tVJN,:AڅR 6GFG2xt4PmMPޭx8 ȍ{f9{r9Lu}V~vӾ7qVP8Y^\ 'L#~iq(HssqgyҌK{x .m:Sv @RYGN"'[CRF E~s崶7aF @2Niޜ{⽴5r5c3g/q e}9pSxa|Fco 99Z$?='O;^ώ|v4{;k6~9׎'~ȾcqC     /gu9zX[m#4JsT:M|¼xk&O/s%GvsTw/=8981bl/VSGCtYDyxLzM =KaeOuUϨ'żNPlod )d̦0Gku'̛36ZFY۲[_ڮcۖ~x<3e3zǡP>/,ZFERw2/>1e,'cR YV,Ǵa)(iLك$-)/Mq-y=PKCnE,{g1} KrýQ:%j=Haa\K2l{1]סe?ޗgbvRl[EI1XP/=U:t(/5zil#[@" dw4-u(2̷   .+54g+ϔ푫CIiJS@Z)/5i#uU1!Y6f x/4Kc6bj$m IcY8(q,Ի㝒Xky¯eDoRqZ[g_Y.-Ȫ9!|3 ݋!xnBȷ``xcNT;=@B!9L\^w (Q9*:F xl }o^Qn\j5n˰f9ePoY*kޟ[*)J}7?XnTźzqqmm!/eň1ڶ5li˵)ȋRRV'ߝ1'QfMdlao^TGuk9%ȉ'x^^әŲn|֘;p_fm{ 1X_8/.7'kggՃdG{Йo;/   ¿) 5`۶ڵm%ƻXVömkkZTעEx5]}@uzc DETLT j!_﷎ z&xګסp=wbS3ws=9pnB~L`|狊!L7x1q3d75acf z\QM IDAT;b».z??;jQw\eq~l:1-**{a3&OoE&5E{2Nفek8Xgf-U\Ggsi- pC-*5~ m~Y`ޚz^{u*c!_ i< {Фa\ k ߿Я<ߛ4ᮁл@>(X9+XDNs+XܶuFT|ZIk+mMlV:< /AOq݇2txVӚLR_?%AAAA4]N"=mQ @&ꂟR$u?%s+N5}6itp苘G*au"vF_iAZ˙N5 &t>5xBmvZԧ{} 1P*eWuS܉)'^cĀ(QY<[ Tk?¾^{? 0՛hryX-.Ϋu5]Aw%+_z^)ꇟS ~@3Ҹۘº5A֨7j6#)r:奄l.MuB؅p]12v'wG.X1%-r65΅>;z0T|kH9ֵ,'l>=9v F]B]]V['NӣGAAAA['DgmB,uVu0*4b!5M-ʮb'ǃ48/BgΡdQQXoa B`?>(db N3g+=rGR-Cт4CGcöx9=_[IBy\zlYWvU?\C{2Z5q8^tym O|ۂr [BKGֆ,_May)ÆejzwZZ1G4<,dp:>QTTĨQ$e˗!۶7l@{{    Yrm²l/=޶m/vlRvp)yח ;dQwϐ-;^9:YR?Qo'U[ΐUD雫7ZEK= PT_鷉.|.x( P}- ~ 7u7Mi䄻>o\Oa=hYfIoy@]Wxr 2?"1yUJ-lmb^18^Vu[g<7t:MӧY|ʚ|{ާ{@2d{ӳgO47l7dG˩eÆ /   ®ƍY{iF#%KHR?~}FkG ; tY>ѭ[ؖmݽ6@EHC` )_B)j)+5DXSD5N뾞j oS\ Chl<&!0XlQbYYmzZ_Wig*Pr:]oStڳ_@s7}̾ޟjsv"UH8CNyO>|K-ò,8pa׳r>D"ʕX]9ddx)'ӫW/֮]oɑG݆&L3iDCKK /͛GII ݻw睅 4h#Gͧ_>jAAAAUb={6`԰fС1f -]~mwN<'J[مLoY6Z1v"H u]lu48oaAw'UT&"~>zNT{}Q3hq^o>T^ᏹzdQժ`"lڋh9N"~)vN׾D1ﭲD jΉCφ˒蟗[BvzMiҏ2tƌ@[[;ovN"7׷'ZZZxVTDRE JnNϲcZ[[ikkW^ ً@"DRYAAAAv=mƣsptޝT*bÇ '3e cFy39#gϚ'tiްz7A˲ٱ 0|) QgeiyT Qq,[AnI /6N Yc#v'~F&娦=x.1jDT7kabx/Bc;D!όEo 1e-Gp~=b\:)-+:}Ǽ/3`I$< ݚ\d,틫AAAa?G{̷;iS2r.b_z 7ϧBnN hkoٳ; L)}}θ̲m:SySiJsU^ Oo}I Aj,]1GG_w-fQf});9j2P3999*q]׋H`RR}sssCRz.lsD   еHӸK<9t:M*ۙ=+54{̋| 3@{PiRuRVi pTY;F5ζY6 xA r,+$~7_ڑeAAAA&CwوbYuft     kyqg.V8i^~ᤗ8񸗏Ǽ v{ae{/l;]ͮbu8FϠ 5q4¨3NfNΘ}q-#|:o4lo?St?%'AAAAkxXz%e[mkAV˝n[ca6ຶ1^ n[8Q)xg^$_Z꥔ZTV6_FDb5DxÇ k5~!l:5C@c"qB5z6qhF)v/%jBF     ฤ)\ vc9؎88:BSaC*> aޯRb|d\O{{{7Puf;ϰe}.r*Ck6Fe&5X=)y |pCy"l|AAAA῞.);47B<'KN<^hxk7B]s} u6|뭨9uyF7%Qs;    k N']uS .?_&_-a-K~~g4.Ƴ~xYcl߮<ףl;2Evu9y]ֶrDl?Kݼ={|w^F+"uF&i'o_ϰOtH]{,zoo~YܵA9+K6|_?_X?.-v_uuul\5gUɉV}!-^5NXlo)    _.].M&J2_6-⇐ z(J$uTJ]T" uqTJ~"aF凜G<w嬿_@[9-exFMWnJ5<\yϣ; ʰC 9߉0*+1|xLcc#=0㯾 *m&   ]7Hӌ1#F:4%R)ƏO}]yBV1|8.dx+$zW6#H V2Y-g5'&p LQ^A]CF N"5z}2dCժrڰo`lY}wU߅]ukG߽?(-})T6nǙѳ+ih}N㗇 ێ'ݦ??z⹿`KK ƒȉfqڅؖ?knxn|q?nv9Ǘ>ǯ^Oѻ'Nt"3n?;ΙSN\[[niӦ2nXo=FF\\&NQG 矧ٳfr-ǘ1,ZYDvYclf9H$8ãÏ>"''Όө^!g|^ 7Dkk W7\tTVU]Cm]mhΚ~Qjva157r #Gy9Ù0a/y1֮[9u]}9-^EQQ'x}5v(/͛O2u[xi|R$[;pCAAA率Ga̙$rsRQQC<F"/'|OiW<^_u9n}('N]֖jwႃΡ '̿6.%>socCS 6%s3fLKи&~AD[ZXװd:ɺσ琈23lg?~+k?f(t!LaAcc#:.b?\x->CV\EIWrŕW\5,]ǝw]e\Lkk+sg}j.B."X֭[߿ȯ1f̘>/7yf~prWq&.]FKK+^w=忼 ^}+Vp]UVQUUe\̴iS[֭K/K.6~Y|)V|1?;<.bƎ \~٥L:7b_v)'N_/   QbGϞ͠ݻ7Wd:t(c1chRnRTf 8Jh.dx_m#y}m,#o[^ٿ"e* J<,k!8 _.VFV(<2ilcI + s+37Ӭ=hh^Ck7mUQ߲zD2hhcw'|6 O_3JC}5ؾێ.yŽ8|ؖMI^1'I^<ۯ{/^6r3L7uxrPs7o}SċҫWO^}5= IDAT{' :PU]شy3TO8{,AuzRUUߏ|J˘6uW#%AAA(..PYUŐ!Cغu+~2zD,#J.O>w/e6m5^cɡ2J߯/0W°aCǃ=ojj*rrC#SBh3jH\|AAAa۶m<:gΘAIR0IToÆ21GSZZʼyYgvض4ZnŴwcq|̶=[,cYN+quY ȫ{ 4AVuTs\%sx' KT*[ټ@i BR'y?&Ӎ\¯n@U5*b` jVu߲,b76˲xç!REגlefڠI\?{)g}?sb[|# x]Z7/qDy߼c7'Ǖ/]jYi_dj<*ݫTWϣ6&MI'zM-[FnE!{!Zq&TUW1p@t|hmm嘣 SUU EOD[{O> TVUqءu쵗߾~YAAA:9?ά3ӻ7m)R4999~y څt4v;Sѳ={b /, 7!/%['ƛ'p4ˡzeeKa.)KuFku4| B>9t֜/h~:3$+XY֧5]} ]t: kp}Ҧw,+ߴq}Ԝ{O_~_y/TV8`k֮^O>iSҷOu0}i6ndM7cRWW?AAAhoogs1;!CXj+N76zmxTWWS^^ީ]NG(wQ%-ۓomq]bM,'2BШ5^(#>,fb躤J6󟕂w]3^+FwP*BYeoi7Gu "d ZW^dtq],:L{i` |qu%{sg{~mOq0㗙|Q=t1k?n&w.|o9E?^'3g+7ss B|R]D⣎<l&HpeYTVU2rWsǩ6NO8˗qWPҽ;--rI 4ѻWW]MQa!cm[KSRZ† (J?#۹꫱,|8=}8ڋ+>+;--!jN8xuaV1Ʊ~zb8=zx X~=EׯB.AAAA~Hcc#<7v,ӦNeq]b1np 7ϧBnN hkoٳ; Leu9PFeHu>:+檼N*07zɓ&ҫ'1R"<޶lbq;1 Uc:<>JUM;n6SYH{c@}j<4Xu)oe.O=V0O>Xe{T{j۪rAׅz(5Q.bl:!;K-cQE}Ԩw_ʁfp]zvhfڛYXN̎ZQ(ʰ8Cms=Вl(wߚZjCaaZZZ,異,d6uU!I$!v{G:)**B   ¿t:;t:M*"H=+54ƏXbS=ru4:M| H48SW#Λeh띀bZ5'zBe间ưm퇱lk jp-Vصy Q$"rGx!,TW,=cąw,kb|GB6aYi6}l/qu:X88K KײSzzKń-崼ᳰ,鿊NE|4u6l˦W];'uuu,X#g^P(999DYEl|_k۶YAAAAwe̋ytft=ގB?jqR4Auo,#L61 K <)XTDeohkao^ OlA>TaQyVOe6{=]s6Xual| ܎T{~Y_\G@ BYxу" Ï>,UAAAΈ.(IgIwz?߷mHױm ]Uʬ?)k{4=]?ow=gԦh}7dIFA[kCB:\#\9b(o47ǻX..Â{XdܕlkCkA1tP$   _Iy":'*V8>ͮu"<^OlȣB~_LVy?4aY9;h[X> Ͳo7& D*"Cw}4?8o/p oQ84-[^4DhYAAAAaGt9޲c^Hb,U!e^非Dv -)kif)^%Rw]U F@G >lQ}vu#mza5ix^eSckފae ݫѢ&}e/tU/w\" ] д9jAAAA ]N?p&AAAAAAA"~^7/jAAAA(]o[0&.ޛ@]3q= ==g/'po~IJe˸[>lXβuV{li_?J?{0ch    '|>]E ῀Ow.uuDfcs8m׎8?9<_u;8jLyio f͛͞gIt6AAAA᫥ 8y|˭,˗/3dL{2{NȹM6tRjzhםbe<:gN kpWs='w,ao0~DOȥt:͆ ǓOrpWy{t8?;Qs*=͟ρgפ2m_?>Lw͵qj]='Ndω9p |iÌþƞ'1eھ<#|JFN?;#=~{6lx݋9j$}'?a_+G[oWpA3Y >+wy''r27p~/T2M;8q{Okmy睷qP͝˭ޓ&q S[W/iSaF^ׯ睅 5r$?    uxu>tIt.;qNslNuT%S$;\w,;CSSMMM7Oih'K?N>[̞5/M#>Z{QRR =|wŇ}:n]%?O)֍+~KJK˨|3g455^X<cǍeg0u::7m_o΂ ؾ};Rn6mbY|wߝrfLN޽xgK0~x,bI{wټy3`ɓ0ct~;LgO޸g{gIvI'_[ؖG;\\o<Ȇ ۧ/|2sX-Fkk+_x!}Ṯ{[oMA6>燼 :dr7cshhh`!\z|gp"lf!QVV+ٲe s)x<‹<ӜӣͳN('''ǷYoO:{oL&+s2c..,[.Cp'}QQуmoS^^Nee~;*+Yz5|a̙ٓo} {y6?J ۗCSO?ͷM?/˟X{Y^^m9y&w/,b{sSQQ`Mtt$鹁^^^Fm]_:tW.[K/[ӷ/55)rss9ȣټy3 ^}޽{I~AAAA᫧Kw] }{$e\n6X5kFZvN ƣbW_|ɥ;m-|z~lݶw>ظa?P8CS^^ζF;>#...ߪ:` WLCll wCy"IIIUV䐗PUDF ۵;3/ygvNg8 (m4n8\6l߱7-@qEQxljcLt177.˝NJJ NcrW[pDf^ʌrMHOg/ʪެYdegssع%K~f1`![洡iZXx_-[bRZZʇ~āzM\qUNګgO{}vëE1 msl ~϶hтݺ{9bDB!B!WU+yvΉ}V7F~#p_ =wOw#;˘ bƍ\y\yu쩩Đ#55r}Vyf '%%7m 1rY\w ll|>o+/Hݙ?[7oA֭I8yq=w SO?qPRZJf,,,,"'''O>Cӧqԩn,\dصƍg׮]<#޹|LX{u||M}BNr9唓+Yp!FO>fСi݆>}zѥk{~ E&ѧwo<ӆ F4nv.\'3l0~fcƎe۶m\4aB6%`86hD\\\zŗc[nexG\~gݺ4ZI!B!B;p(j2t?ԧB09CnF~Z^xy#Z6hzXdm۴O>||6s&N0|>ٳUN'@f&?~M4‹&fsZk8Qgdtܹ"mFup]s Ͻo̜mS^^˯˯n;8x ZdʕBi8A^^>_z.wfΤ[PƺE6OϘKjjj\uwpw|e\vn{=8$%&EӇΠAT5\>JJJHMM 6lÆ  `XxᇹBLL D̻0d`r %%%y#z|Eb>{l8!B!BqxSٰ?b{J z@W\􁮠Tt#ߴiS&Mko.kР;ub^xnNz=w IDATŨ#< =v,o6gUW^?#=Ʈݻر;v#-5VZ̉^w05֭[Yl6m 3+ bfʹiݺ44^!NfrM7ӂ8N36:G}UҸXn=}z˹_l޼[o/K.DVv6<8.ϫӗ={uVv{hҤ ڷ OrAx"+WbUد{"--?MެQU3Pφ64]p~TIQիzbFz1QYllZf 0s_tJ6of֬Y,X۶eСG !B!BcEMAQ''p >_> '*^ZdSb%_qRSЩq5UW^A{60a$ƞs6&L`erI~pk.e;Ջ>w}O:ٳ#9))\U+9<\8"JKK1L 8x"oZyz`_vc2nF=nLO6mK(-FZ46lHnݸ箻ذq#N{qwӱc,ZȔ3 f Syo֬_|9{zFxYz 111'(/+g Sٸi}7mBjkEqcyQB!B!ıh1~VEAu\{PpQCOZeee*@Gº\.iiiX,c׋x<^p_)0lr.l2u..XibEdG 9 O?|o磰zi:jxBy&O?J~RSR'd%w"G:F6mLGd՚~!!B!B! ~5w=V&{֭w*6-}m1(zp;8;U mGG2wbpFd^Aa@ {m'a*|'8Yh;Npz|8ONJ|`Bf<,ތ>~߾>˯qde͛7@f&v;cb^i32V憩7BB!B!BW9""Jh#ЮХc{{.iii:p [l9lJzz:=޻"##GM HII1/c?_ ˡ9TB!B!&x!ĿHpĻJ? 6rl2,>4Crjަm6lAJm4jDXn_Ǫ74W`4槇މG`4>ܭ{YlpeΗSc*8B!B!- !5t h,1q)8\:=IW\T57~|Gּ)3Hk()ihvTtk&6%MyMs5t>{ ;2R麎 O / ^6]ԉ#suB'kgS98˙+ħ'3tڥW`s){1j 0W"M̼1:v~‡Q y%̻Yv-Cnm54]YyhߌQO_G~}Ѥ9~XoF=3ͻ79݆5Ns1jU?omnKFfq-R'%o>~/o|AwNq:^:53ʨz_N}?~Wukw;xs-]Zp[wRu$sA!B!Wu~3Xȶ~ 0|'l~cYang^w7XKFBhTMPM 5xCB ބ0ևb1-rbtiWF#⪆ Efa@|c׉]w~rvCEz[n!(/\ͤ#~x?Vsƅ>@Ig9J~8g1[.%]3 (sF]`X޷|~6|=9>3۸8_9W?Ub͉ ln#?JAvEU C0oV)ZUS#͇Z#$0fƍ3,6DE`/ƢBSǦ@YL"(h'-wѶCKՋ!Oë(vh j'6 E/Wm֢'s*7]<=H\z2Xӓ)ޟu|N-krWP/͏&>#!L9w>aIiGqZbzIxu~Kqh61R:+`K))5O8}.b,,EF@kIm($-{(- !B!B]d @0La^>ٙQ\TDYR<.'^ Ǎ1 c u1f!od~cLJ (&DW׍Uq/" WU-mr#Z4"]M,F -VPp*/!`xO3Ҷzu<:FMKJJ-q4oDY~!eN/)4mLzF R쪎̉cmq綊Uk8 K{:$6HZ+ڋ2?kWܙw)P[;_pA{׿AZG дW{.qr7n| ֳo~Pn>c|X:jIiր&=77L1'elzָNϩ`{Y12>>=%B!B!ߣNC⢨|\"~0n ߍ G!"x]9^R 0+#ؕu(𮪨Z"}UK*کjU#+Y %Ѡ*_\슑G=>2 ]9'55$véYQ ]&QrfcfbJڪu7OCގ|{kt04^,z|6;~\>3*to#oCUMmQ]<do~E('6JYNt[-cܥyX8FK)]۱NcN6WWe#(-b̯<F\/mld!>)tB!B!Nu2 NfCTrYArmGIlwḰHJDPj{xt7Cyngܮ*3~} q4(E2qWHIcw^xc(lxr1 ;پB.<#/(=&EC؃ڽox>'M9ӝ'?2Ήri؉WEFf{[>_|nc}k&}p)qiI|ys4׉'waǏkymt90F߮{߈:άt<sUZFMp5^a$W1OY!B!Sݰbsyh?ļFCZp;-׶vhmn#e+V>ݧWO|~{j`XX:hVih bZCuF[MӌKXTP<8ڝpx xFW)k#m\ WC)+NSD0sj{HzpM7өcGsYf-ڷ7M X7tldPPQ@1ΥF@](<7k ݫD2_Gnܠ z"JF(_&YLS@u'X W$ Ksrg!'V5R(B@PjEU+2d.@YحQ5OO?{-_.$+W;6׮!!B!BZ +/d&p6-[uPSPUqHM%TpT5 ^=UEu׶5))g"{i<j8udvrhdU؋iXK'1}Uvmy΄B!8z75 !1ageƢ' 5ꪘ,4 xydW'(5&U #&X}סv(ĮayCFD죎!|7+#$t:f gl11tn U(/-%`w@ayF׬!}8rݽdV|~j)kܐ'D!B!BWUKJ0DᵨlSJńFP=Ƙj^NVYq r|p,=MJ|dZ^Į}ǃS\gaONErhx|B} X^ܥ%*B!B!ĿU hU{E\<":i9!_iTjİ`ԘUEnTnb;\(\}}F|0h Hex q-4hM@`E3wNJt$(zbm8I$B!B! yyy[G׮]iҸ1 :oO6ṁRTT5k(++I&EQ-Eծ;p3t4͂#ߍ{E=<<ۄ*Jd_QPBpKcAJ7LK0SZIŷg;PSS(6X1qOGhL4;&gװN~RW*B!BGn7~1[K.̝7"rssޭ={'+;;@ ٳ9iSN>$v+W\TNF*#kM*GLZ)1ktyĹm"0oWkrߜB6qLh|FŶB+zH9(PT-2~j;Nєm㈳[P4 JL,N\r &KL!)XuR,y,]6^hn 'fw#k*+Y˻: UǼ[`Ɇ#9V!B!Dݷi٢[i&tԉ6aZ9hҸ15y|yoߞt:uHNnnju2 ,3Fp:%@jQp4`UsuphbbcbHJM"#/[;͆YqAѽAׇ;6܋"E }_}yh J2s1]ϐ;7^w #O-pK`tm}[j:^'WBTY(AAIfx_S4Rӥi _S<齮Uj6^̊-_Юى$ĦUNVF 2ҍRR^)'ק&G !B!ۊHMM 痢Ǯ]MVV >wSOE4, Zl֭_ϐAlU G[J(,]R7+T޺]A'i}طT)|~A;wfaT+<3zQLEUE`?tG Բblz%.D)ح )MFs<(~|2b, UA{yXcc&Jqzt|6P&]uKj~Sz˞f(w$<@y}9iw=j0%},̅=,m~y|G&i8DvP`1o oB76^ tM}_yۉIksMB!BQS^^/++#!!!ߧwo%KX|9  iӺ5mZf޽|LE:,?iB)FT/PV, XSHHL"A l WDjQQt#ߘ~߬z]ѻh^_p-{~W0n4>Xo~s#\@O|6~^Fwl|N|nYU]ϱe]ZgK.%qoVI#=9[-'qϋ<7J=Kx}e/Ղ%qRWaOGy.8=<;bӹfp7 R[q|>$1왔: yfK3Ψ΁m$lT^[ԖQ?`<ίV>OViw\?γ]Bzm3+&OF46ՕB9 (:NnR 6 ..$΁RF\!acQrJP.iM'R @ESt<2ӋFx:Eփ<^6gW/w'7 ߕǎ)7X,TġaӢ >7~Xc/$֖' ?jxVK eBfοtZ<ꑉWYΧ[tk3[ ho~Ey"J~Oj7wvM̵+(,͢aj+w7rW)BCBthvL|=6:8Z f߰jL8?_!B!ı!==voi״)-[i&ޘ9:k֛o~thߞxD%%>rQ5 !Ut]GUO]inV 6-XrKp{*:~"  E%^HMf 56ij,6; j(\[ކ]BIQ~_Yf프B IIn5ғ$ĦaPPenM^ ~;hjѧ,x{r8$omE:Ǒo<.F KXVo]%g<ɬaw(5O{\qb$ ?n37 )sD>O'.KKlLfv K"\.B!p)Կ?s[V.8<^/cᄈܧȀSNvG咯\T&x!Ŀd4JeՆl&iha(4 K\Mc {<^ @WU4{%^ hE痐Ԡ> tsEݚf㺱3Lo\|)Iq<6~f,ԻAS"`?*r #n($ƥ0pyH֧qBdmg[ {/-GR|߬|_ oV*ƵWumeQ!SJB}i ( :!B!u[u)b֪IU*Օhu>_RRBa.$%99j!ĿO bqBiH#u87}9[)EVNH7{n`IKjʚm_aҲQw|v8/y˟sa86]u7;^Ffۗ,;d7DzM0+to{:?}kw"faɆh~<{n"_/ faEǏDIpD8Utk3%ͮ8! _Vo˷k^#hT{mO1wSIcXlzSeǁ5Bg8"_!B!VO>?^Mt3Ofכv*!Ss ~\M2y ^:}/Y%vkL#>&(a/UOf3ߵ zaT7N6ZȜ%ѥՠC9eO7rvg/`|OJrB.8_,}Y?ˉҺqO[B-K/ĎgsB6Cr| : EQu};U/o 4ז=j*Y-1\0~3w[/8>i{}j_6Mz1Ǚq{k(f#?~sS!B!B#CJFCZp;-׶vhmn#e+V>ݧWy^z ngiǟ|›3b}hF p8&MRjRpek:,~f.☶if:uh.>"֬Cb';cU<'Rc((X\=v2eV;pOp n־> v b[.DlGG2NG;/ޟ̓OFUU֬]wEVfw~òb ~# I^qX,-*mժuOuo3EQkݶgMu5y~esZK)*=] uET+%&<أdffw^64˯KU\g#x`8.V^Ӧ…06l9sHF9Ͽ"y9z <0wtޜMx}n.cOT!B!⨩# ׭Gu lWϞԯ_+VЭ[WdfFt:֥+/K&_ou^ˁx(--ʫ>ȠSKvhѢwu7V-[{~N6O;⛯!KKl̹2 !B!Bed|bG1jOK)..6G)*.f5Lp<6s>~ܔ+VHq8$$sIZ$ߑGy筙2!B!B!B2>BZZ9*94l\%77!!i>Ka7mn璋'ѰaCꥥ( ;v`ڵds§ףc̛gg| 8,Ջ,֮[Ga˖-l߱χjԁ łFQ{غyk]ٱ}';wqȶr'}6kYh 0uUu/Ɔ ŇW<DzF~n}WuƞXh1T>>#}Ode4uGzIG֮]ϖ[M~0/r?)m~c.>l~T)P^nIE_1Qv:?X{<5}_YB!9u6oE-w~[Mll,/"_͝>u|UjJ>zC~'r˭1~8npx&NsϿ9s>GFG/gQXVrzɂ8cyǸ9q ;LiӦ ZDUN; .!#СǷm :4~ !mN͛,RE*͛P\00e-|wD/U?l%9֋-179u͵URZʺu@O3f{?.`n,NjsiVJJJMjm|ѧb8Pg/@SgfQr˹Ng>86V^GYȿʑ<{ oÞ=bkݿg*hq<81F:sΙ哯e=ؾ3wK]@039υLr EYȨQ1nŜs΄П>ѣc`%l޴͢Qp3ͫjjVw}aA!w0WY=1?ґ|ko0 !l EQܩr:>{nz;nF:ܤF'\ oq'4m$wUw|۶|l6[_߾\łi 4QPP@ll1ᶏ<nHab\))),^np{!DݒOHԁHNJaJ+~,RRp;zcVlL eevyW8бl,Y=Q~7>7x蘖̴i;.F:ɗMQ{>䖛3͏8s1aö`|٣Xޜ9y|,v;/< s<0sgsٹ>}bbbxgx6./_G~ƫ=G Xdӧ?J߾ŗg츦|ݏ}t>mƏ? 'Lfݺ_j(+<ߡnݺЭ[sq%wJM"?~/߫B!DMt&s72 U֥KN6ɗ_Q#2h۷G7K2C:vbVʃ/8v۷?jf1t0-^H'2x(_}=nu+jc<3ErrHƛCvx<^~ V\CӦMhؠ>}˖K.7fd୷âi"F{]w͌=O>4nܘ^z=ZnRPP /'Q ~-Lx3~RG Ww2.> Ŕ)e`fKy㍷q9]h .gTz|O|Boϟ=rȵ^i~͍>\s͍\uetܱ׬ǓlܠS3^`eh%\slٲg{,V+.O3S5FcÜ($wpҫ{pwpJGQiҤwEAz7;HAz.d/K6 '}wv2ΐlظSri9|(i՝N]f}gO1pp=yFBD{j'Ze?~53vէefb!C>_>A@QЗɓc>||@& ٱ}nَNC?@J8vgΜ'99z28'12eϋ+ע |ao[#~t,ˬYIJJP7eʹoFth}SƌD hѢ)ܭ 4Q\-UZӲ=ĖԯW֭ɞ]?qj?k1:w+zvHKK#L:ŋ 5-_~YE~}Y|,^=#F MsNظqZ58q?,f9s,ϟ=G@ƍ,Y#3gOa՝qc'ƥKpׯ IDATI~{6nXANmٳ71Oٱ7ϲeݼžXt16YF̛Xȫ֫Hzz:GnZl۶*U*_܊m6Pdql`N=)Z0{=r(.dq[?Ɖg67ʶ_/_[1v̝L`F@^Yn9gO~{6h6Co0i%˦Ռ7%K~+ۚ:}fc/T*nc>`/'իUaݺӧϨX<,-~,_gϟq}&M ̟7&Mr5-_ży3XgqOH -M-["-YȐkn+>.AtaK\BxȌsiѼ)7dĈ!;vR /DZ0bRBy?$q&ϟQ\Y֛Ug/_V8~4j 11^ĉǟ5:P`*IBRu:4e,ڔifS`A֯+C֯$MTfΚϔiذ~ӦOdϞ>WM{.2ê?rRN=/MCٸa{tbW@Z5R r툭6lV(~kHMIeŊ`CU6quo]j3XR_/AG? =7:cm#j5mJݳQPPPxqfB?>+%zxxQFZjƏ?|ǎM@q^z: /?rg 5hP7˪-Zbr/eJL6)[`Xk)]! .Ӧu hiڴǎZR6mARѹKfϞBLt =&O<|`YckתWjs{m3Ѵi#0g9~x}B$'JU8zyr h, l;j ~Qyuo6mƦXb-_|%;,|lєw>FMQkxzyr!]=vy5qС4oF-zSl=^En׬_7o1p@ $""B6NNN${%~X3Ə2 7L3g&3o</yݻIJJ&66 ȗ._f6ň=aի+*UH؋eʕtwCLpp+NIeFՍc 2*spqq}:JmK '22ҦM J\25+툭=6+0ٳgGЪU3.](9}G\8R9o5/^ڴ ޽fɯ=B5Z5{A(((((ϺQPPPxqww#ʬ89%le@R#@<9co11xy5_BiIB̢}U7 ƙj fΘǟݢd⸸8qPkw/Xf#[~͜)THabcfgˡINˈH&M2< @_tpeYDBgxK"Eĸ_"""~l/OOORSRZ2F`L>J3hP?}r8 C&L @yIJJ?Qf2K]aVRMNWFÅ (??/#UK?4 {Pۣ7%Kfr0coo/bbWDFFe$ÆJɗ/y选q5N"30L{<nR9QݳA>Ǚ3W߾cFdٲ 93m۶Gfic&1~& $Bl~Oo!¦h4%T\cGOҺM+=μzWDr ՛bfh3=gaa,vL nEpp+1^*ֵ_\Jffdd% U7l9RX75+ &$&$ms5r5^|TT_A& AeعsUbٰJrW|'?LE?UjfrZ֮ѻWw5'\udS&6_<kF6wKe`M,S-ז>G\8R9o|c{cV/vȞ6SlA{? =D^i>W Y||3tԍDE덯 yB~N*'YyF<<̘ܳv\ocNr鏫lܰlٲСfqNu7o'χ>EF F *Uʭ-St+a/y 3|iР>#G~,ҸqC7nHB|lʘ1ؼ9gS֬@@?ӧOʕ?,οU\:99Ѻu @|l<) I네=jsGZɕȒ~bB*yɝ;?uoeIZ ӿ__iplwH@j&uߏ"m|0n)SgK+V$q zLwDXE''#IIIY08(ckq!WW7Z-Z^'Zh޽3}})nkM =1E|5e..,\0?ZZZmnnoJ'))&))P(?Ju6qeKOtݕc_{d4 v[vPAAAALqv:X\Et=TXS?ҽ{_өYi:B%ϗgCE1KH2x Ϟ>{/[hHKgQVZJ"9%>+Q#y6vvs@|~O~x)4۳޼)]vöoe$%0;*44#ʔ.͹~;(^WN N<-teY$''sQ֮ 3{ׯCBbMFI)f秥i222f<8ZZݻobn {ru ܒ3C~#0?(WCmnȵh4^q) {Af;rzb3mEtt,"˖b ҥ?x)*W_?~ҴqCo%e63w5;owt5nSgE]=Δ/wcOrr 9 nnyK0I>*W_+TR ggg֭U:ذ)xԦPD1NԬϫ)pqv]4lX9sxL<#RlizߝٳҳGz_d7brY=?d"fa7sM͑#Gt\3hݻ+s|quΧ#?f|ݏ.^{̑jVmH~41)ڇ0fDܽ.CСwC~䯾9R))̙2eK[|6ŴWIvfruQRybbb6X4EٙAdD$%KIc4{^k3W +guDES>ׇܹr(f„L6?_b)_)F+T吓AN.o׺M ?>5 իWaʕؘ8EPPKT*+WdѼߧ;;ƍ` 9r 22GYum ْݻ0bD wg#ܹ'OҼ޿mA^L>EZj:ƍ7^r-[Rp!:tlҮ}w\)P ?ӦM`߈Kf4i, 5KDbۋ~> 'jxV^E>|h{ɞ= $"af̑AMGc^GKvZ8R{)+eԨC?~t;cDN2t `ah3fb#vPAAAA"ka[ei6|7~j lt1|7~f3|fz>{najUM͛-ԛK.UJ Ұu6/^fneʍ7w^z899QpA*U(=11xyy?wXeN]`H#%%Z{jỸDG.NHOOFAJzz: x[}HJJFQ"11 A="dh3p2Zgct:bb2Djj>>^YdJBӡr?aoh[ZaĈE+ e6Wo`a777:KYu<*'){ժu3zsws37NGBb"^&322HHH0ۓC{eYj%n"h;|}] ˖}'=FqgޛBr$&&զW4omdĈ4nԀ[X)r [oɦ$'-{6vX>YV7X{JUʐFFѠV>}.s⣏[#E*w x][ofuܛcFRbyQثz6Yj(-odƌ̕>|XvƈiZb#vrXdOrIPbUBݺEZ5?R%G35|O-36~ :oS 5eʖ"W\t䤬e˖*U+#MZꋢri؃FHgZz鱄sk-ak/^*-[6ѝ #bذ!,~~f*ꠛyRբS *un kuz9FLt Mg9B.O֯zcͬ{uUÛQ$Ś|xի,]5XΈJ2$޶BF5U;` 9aJÆٺu'Wő'RiƂnaO%gK֙)rXeGdS# +uhĚnHU0c\AKsifxޚ";Kk^XcAh4VJfMp ]ioA=-!'{rb#vPAAAA,[\JAAAmBR#G4<Ĥ$\ɛ7EgKJ(̙_C;wбC[i4ػ )̛7=,:\|/2sdqcL=NNNL:ia~v:[m'99Ezn]Tk^G6ص{4Mra-7+zocެzXX;w6 xl?gض}O>x*ӸY3լ%4W>8yR_c޽-^Ɗ+s55a7?0\~]V45 IDATӉ%55οKގ;9r4^}uҔܹ{zs-ۈzs(:?p@0*|2S1qdo%oB0 k-<3$$$p}<}* -T֍v$W\رM7Kx)ڽ;ǎ'>>e{|i0w$$&0ut^}NnuB޿3^ҡ};i,!mZ{xx0hLS?d1|зs'''޻KxnpO2etݻQkHI:kzjre0>s&4j5))`Κũ8uw]HC?=MƤ(Q4XWmϏŋYu{wRhQƍ+qYl* Ctt43f͖k]4n̬9~J=~8o%^#շږϘ5(zApP4ʿ ʺqrrb҄_ظ8e ˗Lj2KGdxƍ\6*HTPPPPxfLƖ͛Xj1vh1+?,Yž;1m:x0 dIT^jŨޝ#FE=ݧ/vc۶TR K3g}:uJP68;;3p@UG*(u???͙ͨcܭ;-ZKb8֨]sQVPPx{8kȖ-ٲeۛN;R@r ={Ofiܬ3gaiڼ=;ѫ;hҖtу7naμ hDVtً)^'ÇS~ …o,i4i֜iJ7_N#K>> aV4wً7ˣ)k#" nj݆ZuŨb}ݼmwYV <Yr̙7m n'ÇexZ]5kي̘9 vt̜5MҮ=۶υѤYs222vmH6b`Cbcbo@HVMiи 5cm :;w֣Yr&;gΞeƬ-cjj*gΤUP0-0hhn'{e`-`ل Ceˆnnnhj4gwzz:}5qf 2Dԑ1z8ϜI-iֲUY8{;w1]0O@0؜7Rn=kzaF.]iDP֮[/>ڽN]RAIxxx9Y͖msĦ/'>?uҭGO޽K>}U|OӧrjSNa}׈%}3gϟܚmwҖ 1k\r#N 1{рݷs L|Gf&a[*FPVYKU בo[Ke=!!1)X 5kM͇5}>xcƍcE4mނ zZe`=Þ?E` o,y ~F4k,?^H^ܵ2a9ÀAiܬv /gm|f͙+>;AmBV_#ʴ5ٓbIndmzVbccȨ,#**JHJJ+$&&f 7B||Bp#2*JHMM6W]XcJU =jS\J™3gV+ całV  Z0qWRSSp!8иYsA .^ת-ڬ 'N5mˮݻ' ZVrvwZV=gХ[wAk 7V+lкm;A 6*tMjK :wV+߷0c,A B=+Ve6=4j,?qB6xMhXPDIaBZZ&TV]8yꔠjVAZCjV+|[ϞfΞ- jPVm ZVXkUPϛ*ϦͿ %˔v)hZSB2e硡Š]qrE^bΎ;vV+$%% 6Nkׅe /_ZPN3}L&L5ۼ V+[@tgfyvtS5{x=z }-YN޽V,\$n#hmԿ-6=)&&F^paA ?լ%ܸqCrPr(T\Eqp Nvq׋=z̞3Wڣ[݄ ZCg ׮]Rׯ Ttgb{iZa‡ BZZ0paK-'ٻOTrBhXYB5 /aliZiB VhQQQBjՅGZVxDQfl%WRSS] .2˳YMζt B̈́gϞ IIIBˠ UPBHHH4o.lߡ !,ZoZBˠ A+ mf̚% ϟ?j֩#Y<[dz EKuT)M&ҼH-gυb%K SM^iiiV:m9c7T֥vD 7mv: }ޱsPbEa޽V;&*[NHLL{ :T3o?  6_NG ,Ӆ[n *Vnݺ%g|c\cʴ-_ ҄Kb_QztMo7sl UN<)hmaIndmۚsz  o6CN R]2>[iɃr(r(r>veao7\=cg 񇆱N喆zʆR cy cloضa0b21˥Yxg1ix[{nSvSBZhAuywwwe{yy" ]٥x{yV o!a#F0lDֵ+իW`E 8P?5,sӧء=j???ݷƍI\\UTFMTTǎ#))VڵmKzzqqq ᅮ.[.\`/W͚#G1]SN9C oݽkWi0[n >>Ν: yW.ESܹ{(#o<,1.U&y˴ ií[E_{#M4rsDv\\\8zW5j4+ǵX.\H=pN:p1N(T ;u%[F,ɰ9y:s,^TR8<<ܩW\ٲΕ'Np1GR뱣kNb[\xxZooΜ=˽qwwgE)S,(\]}%DFFr} (9rвEs0eܹtI [w>^˴ ?JEP` #[lvаACF 9rw1N!xxxV^d]79fM7^:ʕ ʔ*M:u'{*Yoأ^rI 9sy[͖MG3ST*AK`5?#߶3B[-ZPzuӉRv¶;({SտF=wɉbŊqp~ ,hcma~9s֭QTf}Keգ^sHx<Ԧ."[J%+cocΝ;=%|CѢEF{dZiMV.z0 S-MDbS|.j!Y8NNxysfѣ)=0գ\,z1ݺu?Xɪ5k(_< 2::lSw/¹tyS "%9ڵj1mWZcQZ53J(gʉ rPB $x!'EF?$##H1([l6Ujt q9x_ܐ/<"?vŋ#[QQfo-%**J|q!{e|H|||d WΜwc>e7S28CTt4^^^jْ۶ȑ??-,. {?Gd'##1cq5ʖ)Kl.ddd37ID>ob//_Mƴ-߈%"'Owb@iӆ~yf޳>:9_#L`ԩ,Zمn]пGYb1k>s '{G2XK)mCB6|cGfoQNm@\fob1L ,JBg,4Y7͛mjemO:VkxԹZ1Eo?|}}yy{^)-dMn-=ϫʷ̫musT[lX:Y(n:8;s) *LDx 47u-atgaHV ;m-ُ(||2>w{0͛I]iDN޶-?Fqɱa[M踵5IӚ<((((\NdObN>(LH~$%UJ6:Y茶kcSj5m* VPPPxeʕ+K}Ltٓ6+V/3o۷nTɒf/fMy0t[4o 0S["U+Xj5 CXO̙Y 9yxՕMрi3]t_W,_fBeGkxzxE4**J\'CҪeKQ;v2KY\kj5CbA<}i3f0cl3YyOL?7o<o_vϿqÆ}Le9";mٳgGղ{^I?Hbb"ٲ;_ rL8gϸ r/CNȗ/;Y1MCBxN[\]b~~dF\~pM:ǔɓj$߸F#N:sss#)1,Ӧ{ӧ/7}Ǖ+W JLLҔʕ*Օ˗/{>@V89eıVZ5k2';hh [$orm)6UQ۫O LbĞvzزI@*3K7Wo[W%)1 777i0ء֐RԱ#pBkZmeKnOE||ZŖY(dm|l9'Of\|9j%ِ긵 +~ISAAA]A+8q8ɟ9d CJ:SRcv]oN;DRֵcO ##$Ag;~۷o)[B^,Sط71F paE+::G")){ǟYӦ'dKJbb9 3RSSYjZVt+W\,XE 3w|n7TPMYa;w[Uh9s+W.=D|ɉ'6bmF]/_cN>Ë/tMPO!55mV6 ;$|Ԭ^xpIqPȑ)\P7U .ݿϣǏPU*Wݝf$z$ʥmڰq&=zLF 32֨^аPqSظ88!k/d]me_ܽkl2EoRϴN>C*;NNNN899WA&+HmKˏG۞-L툑Ϟ'wn0#r6;vرcعڃo,ST)m207nczf/+VePkԤ{ddm.UiSlڼlVSl2Ki*(((+ʈOGe&f͘Fw>t:nn|`YR^5PL0q_guh5mJFk|9a"-x#dQE{>Kn͗SJ,˗1:>x-U˖T*2tq7lȘ/鉀@ZZ:̦`򐑡N:8ӧ̝=;KtTV/#yÇ}By8;ud᢯i/앝͛rjCB撍ݻѢy3O%KGL2wݕfMZ\_ZU~GZgN[=ڶXL7FfϜ_b}$$$Px1/RŶDϏysf3jJ*3Y.$/'LdEbXQT4n֌˖qȠ;6!dϞfia^N]:uFb۷ :&ݻQre*U=[ݷd8ʠCiЬYSqVJZ;>׮]cԨ/Ͱ%{ְ՗y d{zzp<>*URd TISAAA]q+ΡbW_6b$ 7jntWnO.]c?;f3=N=F&YRPPxMʖ)# ~-_DiFF._ukH bШո iiiZ׭[ٶm;֬tDEEmBB))KmATt4~~T*y" IDATt:⋽ t:d6*yX~_҈/yLMM%999KǣV.hTjk`D"#lٛ*Yhfy|<95XF BuGť]>@uC~m}ֺw5{}շNA sve˗R`#?܎f֭ۤ_6npk nbcya,/W`ILLK΁HhH;okR/772:5pb*k\nmA~L98]vF3.R濟<$y: y^ӢU3.ȒrMkK{~M L݋#xAAAA.'"_%SQž"oBPx{2(##\%eVZaGPP* ʽ˭;/!;AAAA)s8o/ϫ5NȘm.$IyfJF60Vd#8+:    ": I*q,kh/+N;q2|K V'XVL ‚,V^xpédʍF>#^}%߽[7^z&vn?p81.~Zƻ=mno>܌:$Iٓɓ&qA}~:6nW^fXGJ%d٘&T*wMF1c#5%7x%˖p" N$FCll /*:grӜ:Żϳtr֯믾@EEE ky7TRxnh>Z_x ϝ$"2)O=Evg=2v^{uV\#y嗉cw`Zyؼy ,3Ӧ+FTT{CjfMΕWv~>RԤe ֲ6#>_t?ݺC?`>v+7RGss6{ 2v̧4Ce֭ŮݟRYUL|\GNzZ t1K \=9$i~60mju_yCx!uToiq[?_ΣRNƽaDDYY_}4#^!>ݴvNfQ 8Ʒ -b͟s,[|!   ŸBp%s2(v#8Mf[\ˑS%.F&H.S%QRT*ghVkȲ̠ڥ l߱ANn_yaa!5cOY7kHHhqz/R;{jfPRo]<}!&*ڷ`Wtڈkũ + 8sf+&sCMmѴl1.`2w Ao?@\lseǎc"#SWcيɄF{ccqٻ+h٬ivr޻l}]p:,[1tzt΄'vG^DRbye1`   ;j%Wpq\kqYT.aj6asvIX]`uI8qĐ8] :¢"\.WA¢"STnE{S<.bj+,,noev.W([5Z,:|]&???p1l31!!pv-N'|жM[`N'.p=#yT[Ӎn\qS:<-S(87APH>roxV(.!A#4.0Mu4NEBb#v;8,csJTs{sqĜ'!"ܝΤr+&!6Ի|Oz6JiTQj&$w.\<""£+0rfTV侁r)+wk5((Νnj\NLv => IAp}yO?[m\>o\\#GQZz^@ ,%s",}}toHXm5SAAA%c&LeRJ4 wb0+$ Bj$nϹd/t IXv-WWvAJKKܩjw0Ye$Oʪ*ܭ)ˉ&:;`\XYha`Eٵ{ڶ%$$=zU߽n*^ݺ'hI] @eew|ع5?$$$-v2tPn&X"F;m2[ĜkI/SR}ع JOSx&F6\*(uh c-ʄ!cWݝwJp7GF>ը(<7^YuػHJ]lV,2 +] *}=UYy.EUE8Xvcy>] WwD'Ҥqo~p'}| K?Jfkc3b:s78(-͂ڹ$ICSN^k|AAAA~Rű;]u|(#[iJ6 e_<-j{y~ l݊쉠:|ɏ?wNBB1:$q)vC'mJll mڴf2w Ŋ+ۧ*j5ڕ< xZϜ=G.ZRqmwJJKxj&w>jxM:}SƠZY\ *""½Aw{BA~~}YneeeL<* _o߾,a3Y|9ڷ'2\wA8wvRoJDVFErlI'Š %H"TFAv:PJNtZĢU˅:$&| {M=8COPPiiz$AYYVzB~t"i|0ZM|ްӛػkh}R8>)'$v_{_}ߓnk;׳5ܽh>:=|N~!vW~`b[0blFw ˯8ߪMTWٶ]JJO7!8?x߻]N+/ǟ@CiI ;wyeӳG^~U8L^~oNy ӗH,ٓ|M'M^hψQC$NC >gĨQՌ7C3e3|~e\wM$'%?&L ,,cO2b(BBBS.RN;\ d\@&ȸtT]F%I8VV'.ˁ$CLd-9U`$Xm HwzٳbOJx@kپNo"8X ",LG 4_eᆵQkw [hh4߶z#I)]`IѲBCظ5** sԔd[Ig2Z t{7)+;KTT7xr7o!-;އxvj>}%[]Q(k{\=o;a9l;JrR'oo5hSM,_8Kxn*UyfP]mr23Wllc+<.oc̍QQQa3_{דyl%ύ׽X\`CBB{BAAAA_3-|>Úqgf򼔞ag=j׶?ޭ;y]*xkW%sewN\ !I'H{`XJRt d2BpJT*C@nq~!&ZkMp8+ehZT;6,r|wru*د_BR9.&kj}{*P*T~ZG[vL}4XAAA0'MUl;Hm8U=?X^6:<:pt eϸgqi_ӗ ]R|bsRZiu$~q:pС}Ͻy- 'ЧhQO`|;_HN?J?=-ݝ fjm;DKoBMBAlϮVq!磔pwKvߨKhю0rTYi)6Wl$42 0jpaq$FPQ) =qwwpDV΍HN IDAT$o>_#0wz}z;kI#9xh!C?C\g3جTͯI+,kuc+P}{Л,q넆Fӭ띁ł    2J/"7lmddd|blV+.uJUw xOw@"\i)PWh jLը5j4Ad*,DhHKk0*BPJȲ,<-/?#G,;9vG@`'hF_%!   ¯ _jJξ2n-/Oj@yMB[qeƍ"(7VV8ƅ#. B"(HʌD!I(dX,($ Yv\Ź<ӹ㶅DRbGǢق     J97.^Q,T)Х4#5*+Aj@ nnwp8aK:92'KIvw_AAAARAR*dJ!{Ǖ5 Ut#JUPp"\NIj IEnxKTBQJ<ېdy={64V+7nrղq&,K>}}+++cѹx̙f^^h}P/NM7cAAA6D xA6>-=4139ٟ]D^N)]Z&($J1Z($'J Px#{sSp@7KkΞLz~\~ql6p-1:~*p+ʮ4i҄cǏ;|;wgE]۶( \CluOuϿ{ȑ#l߱~Gaǎ/:"2SMb,b#пl/_}+7Y`!zg믓,=wE skR$+4 6̯LAABAPJ.$$<ݥg&` !89%dhђtꬦSe$9C|!Mtƅ Ir՝~ ggܩS?gϒ8/}]w @jRR"*ե'Fe7xE6IKx L&~g,:/^;];{>2L{gƍs^Zz{nSZZ3 H>((o:` !!W}:̢ŋ!&ֿczݙ۶og}lѢֹеo>|]s)  _pбcGX,l۾L-hۦM[1ܵ LRR]tSX#&pRJx@Cx^Hj߃NiAHS%K+9W2z4gǁm䳇i10UUؤ E 𕕕|w8q޽zqv (//gƬYLfwƍ7\Ylj+ #Gѹ;Ӯm[vCaQ!ィj%<"kG` 6iT*~-[ɸci(۶f:ZlI4ϑɲ烏pĉ x'd"D]wM$)1m*7'$$!cHJLXٳh4a5 bcDFF8Mywn=6]ө-  GZ|;> :$˖1[bmӆmڰtrt$-1ѽ;WtȺ~nse׮jʻӧ).){pEf(A|I $ݦe$$JǩbAJT*NPp*C%+ U!;(Gm- {Voˏ˙#|1f͘ΓOf >}}~'yYn l6 .d-07v,_YecǎsqL9 9|s>KZZ3gL>Ȃ BRzc>3SҮ][> ?Βx촩İw>RSSG$L&yvTRR֓˨2stشi3@ 4w'$4js<=? 17++ Fngsۄ[S<8y$S\\ngOؤGYv-\.rss7mڴfOa&Μ=K\\,EEx~H\.-^L>}ju9u*}p̜>F }Yvͤ>Œ矧M6,\3ɓe *,*bt&,d?Y3S^Vmjjix9,Ulޟ+WrTSz3c0~:R8s o.Vd25X E A_o+y^OQq1cnfͤYӦ,Y4p5ujղ%j^OjwF_|wM$,,ֹNSS׵ @PNYYyAAA#;~&ӬY3Ri׶- ҹ3 {sp8p8 ߟ+:v$&&6[STTDpp0iiS4`@˾lh2ή%*2V8[y|NAcpQ 8y8U)%J++wBAt2Jk'`8IQYdv'<2ֻSYYDP)OΖ[ILHj  8(`{***np8/(pp˘ qϮy^GoD$4 )p8jw QUey,[ 7ҷOob=9۵oŋ  ε ;84-ݦ\f_|))jz)(JBCBٻo?QkdimVNYyM P{D)((tT*q\T*1ra֭r]=8Gs-cwS!88<<$&ܼ<>zRRӻ7u\ճ{:$U?@z=۵`-4m҄+Mҡ};.Z ;\8|{n22Zh:tr˘uJyӧwo˱lrp8]>rnj5ȲLIi {ァ괾Kvrc93cǎ7X[ǍCɗߤq6l跞8k\_2d`V0V^_ﵫ@qR  pItd>MQQ,]Ҳ2RON>C~ RSRnL&X,|8y>}z#I z: z O>uF:N ٳV*F lڼ:7Lf2|:?5G-ېܼ<7|QQialV.RAM0\R(F$OlnA &55gyM0﫯7[ rr !&&rUUNkG`X駞װ訨s'OO8fw^'Q#o_errsIKMhwFfsuX,xG*++r<.Z̭ǡuYBIRR;~M?WcGjvڍ` ,%=-Yi^;h7>=4>myv4Be~nz=C +;;Nܭkݻw?@yjoPZn\|egѣ4Ȥ$'cX7B|/]J.]jΜa˶7ՁJD /Z̜=͛ 2<3m*IIˍL23ì3h׶-V+yxwYx Ett4>3!2:k֮%SOҮǺ}zs+8xgc2PT5N=:p1?k֮7ߤRJ;{rp8)+)b%9ˉ (XX^&SXn&ZZDrb4UR^aeˑ몞DFFL~n9%7_߾[ޝ0gԔn3|Jq>) ={$>.ݺ1 rE+cG֯@r pu oK={pA> QѴlՒFVQՌ9_~ؘF#־vp~ uu$%wBj4 /=/ M6 oN6}ѝ۷3}LwιV#88/x[oCx, JwANn.aaZ-/V+q\7j5rN55\~ЯO_.^ĮݻkW0M>zhu܉(J( FE\\,_̛DŽݟ'_~NN֛ÐCtt4FsEnnoߛ3bp>3gP(P߽Z=Bll _|͆.Zǭ0bp>#t.L8ODGG3y$.6rD)PVj5,s6;Ç1 QT[箻${Rdaۿqs!̜5pN 0 7qZ7T E ^OZ=9O>ːC\0t: rW8{V*SO2NIشiCfׂ牆V(m5|ϭok5;~ɽwVAA?^ll,Zdܹ(JRSRhҸ1W'}FHHJ؛oI0DVk˾}3x)6n̗FMywXz5˗.a{%G'1ot|,Y g<,]V#?ɳӦ]W_v竢! c֌ ߏx <ӟ'--ACrN'C ٳޭGcٺiߣ[>r_wCkǾ~~y?\L6(NuhH(UUՔPHDNL 83!$Aѡ};'.|W:}Aҽ[/,UVbb;nUWWxS\57 vy[ZV2 oGTz⹢V[+V]]n'<|VY/xڦ/Y1hZjrLxg-w/?I?Պ}hf&͟O>~4Def3R,f}ZuX IDATjmTUY$P]z|]Ⱦ7t>}7{$Inp8TWzÑ$ ӉB~R}տ_[Ԝ_ T׹Uy_},Ä[k_AAAc9NdY;^ev_qƭ;ɢcSUlIn|43ztl3^3tx3t.tP|e qi_ӢZz5}73v-|8g޽zowߥo:t`ݚM&N}v5 Ɲ.s?Vwĉ>sptdBлUZƯ6_lXӧQT*wAe*ڵmÉ'pǩ$$}yt0WTP)P*UD?{E?ps{-J.! PK( zETXnAE{E@( R|d;K(:+~'$kH=|+:@&iu8 Xm6@LLu'.KWVbEG9WNɜ5u[9uO<$I(Tm{27FS6HKF{F{(_]z=999'z6[ѬiSJ zѹSG8QQeffr|`RTAAA ʌj/`L^u3q[i|ڂ‹\2rs ػwK'wǟ|ʄٜE:v(3Mtt4kferEJ^ݩlXDPP.k|G pVÆ;'gaѳG$o`@IL[2o   ¿ݟ(Ob2|&.k׮^wIHUWӲE "Jn޶m;%6mA͞Q򥊊Ĕ|'\.{{'W/ 6[C3grUUV05wE$6     B%]#^yeƍQˑ#̜5J:_?up }֫cSO`AAAq3w,dYcǎo[!--gmX&L/G~A>9__ƌz&AGi\GEf3fʓS1GEMVhܸ޽{'?!1;iӟc…4n%pUvNGNQEYh4n YqS`% 2R3!y\7((8d+NŎNF2 5 $(hQP|-7Çpp4y-5sH\AAAA)w?q]ɟ俾信qcc{^kVM 졇!yzO{qcvYmnH$쎗e\f$IBQdYVCR\lGe`2c2U<"@vv6aaa CL ~nz+rc}?3:U/}b'=4v+ٖs8\6N+}(ZɀQ6bQ(j`qq(Ѡ7fE!H$YqBy?MgeO Qcq z-AAAAJH`Vg̊<uV7#Gұ}Gߟ俳qWc wJNJcw^{~ug!IcF_tEFcal4 ўjTQ$2;/Ƴ/%:߅"ٖ,\lv+Ŏ"bdʼn.$Sq 6buZp).\7iAP2.JÅ8p(Ep]UKz:.{1VK4\CW,N}BT4qAAAA+퀏EB]ŸdG1Zp +n 7 IE#i1LEѢрAoB B+PPp)vl\nF B'P|۟[6q([~5iwϽCly(L :J~ZFfoQM/KqOGAc X 1 ˎHZtRhkMh4Z$I_f)<$*G3{8b9NˏK7pRlɧS(pK!xp>^땻    ϨQjxAo5jkA7A2$QQZ=_s#.H 0uԼP"i9<3r8C݊ h%-22\L4vg>g.|l&},NlNĖwlS w-N};yqxl5cR0gƚEf85mwM%_R=Ӽ_^Nls_6.~14UAAAA*.tU kԩCXH1e;HVKDpuBcҴŲI9 ^Ckjq:/&LfN1 q\Xv3(pf!]2,s`j\#?}%;^n`ϣz47_緍9uk/r iuX3_is|uX_]AAAJn4-Hsq/Ή魺A Bt q+-Vd;&3&C0!0d)a2n hZDdD!ki[ׄ`$ݮ//ѠtHn=_~F#PNm:w'4skrsuV>MZCHtunH!8Fz#;Toڌ[PqA}md: S{޹knY] Q\. Mzyx%.7_1ћL>    T|Bt qp( #9M4z.I"7ZNhԡATnwasPlHz]GHe/։Q+v?c'ԋd"Ium[4b4ݧb9s:ck=$:k ZD7tOG^AAAAʸj;ZRɲgT .o'c2Vf=- %2RR%ﬓB|rn`CWuz#&=`ˤر;]ΒVHֈ7$v?kE^c7Ш{O9unҔK;~6o܁O 8XFd\}p0u۶CvN|r1~-sg|NIܐ0CHϺ    B\ Ue2D`;ɴut ©Lx}=[Kq>LdBˎrQ` ǥY[ڋȷH Сq+F.vk |Gk^CݶKFʃA .nM Ÿmێ1~`EܾCH^{H3U}G-\LxuKz=- '7|AAAA*wLpXezEEE$XɸcZ+b;hӦ]wL˖-]+WҾ];j֬ɲWKfP@֬MbP@Lb"ʫeE8)f2/Ihnx ):k)9:~Ǡ7!+nN+VGWN Lu&  :b_\AC.nZVn(; SD55    py$ɹsxꩧ(**_#77ɓ'SPP࿨Jz2bd=bcر?q?NTn7yy4cri'^USUv ~OKOD={tA,WX\./͘AQQyyygʕ,'&Ƃ75g.i s5Ԋ&1-u#Vk5%2d@!i$  q/=&AAAALbۍvs>il6yxayb'gddp:v訮p81sӧMc6><;<}75od'p8l]۶|gdm̘9_|jժ1㕙 JH 9i-~39zx۶oM3dg<1?7hX7겔Y+V&.g,%_}'~vt/x-:thϦ JLdl޲JBY ,߯m69ݻܧ.\S& q!-]JҚԫW^xJ:7l`erVHxֵ {RSղ>D5t9(,."e民$Dj IDPuE&-Ź$&}$5BҬMUG/ uAH\ՑOAAAA_ڵ ^Ott44jԈ .ФI+FL ZlIdDʕ`j`0ec 6[1'NDӢ=-jժ'I cF{FS7ߤQFDT`ݴiӚF^˂_'NQk<(A Ӧukc0S6Nas!#:,fN:8q=zxG+U}J<&%qt:p: JH`А8NvEn]QcH}upY7pui@`1%(|Ν'eQ\&jE3*R(; \d7cuQRE)IEbÍAhbAAAA2L >VKZZ7vldff 6Ͼr*^?^eYfSOsAnF,e^?w ,(**"$$?YJ8Vl|tZ#:IO%0m,l !,( B{yt\\Q|p)A(8Q4^AAA_e6h8v8]?WF8^O€̚3aC/j]_λHvkuZ~*u͛/p!YɄ@вE Z _SZ={V}^x%lV+noʯh,VGie֝eIn#se˖ܹ S6^cZYOpp8Ȋ -vkp[q)2A&#a5,n6\; < ~\ NR`@aq6Yg0i2\ettYql؝~:T?(l=7/lΥՊ/˜De>KKRU'_ܺkn<*..tމUWuOqĉrۙ+Wry SʶAڱMQQ߭^O?!P{ty:}uJZ@mde?_W6 uw TV?O gWu^g֭/򑓓þ}. /<<ƍ;bĨ$&U[ZoÇ#++/wh?!GP5˴qq:oybʓ/-WXX͟ǜy9y:vbь;q8~h߮-{R/g\#a1cز{}>K֭֬oeߟ={ܧ5{ o|FONSG Rvm3hP z$!+Q̻wYf>wYWVnN'N9Zzqy\$ v|l|ܸ ȧIZPv(.cA-OOбc9~rlܴU߭OS|d ӟg|'1Oͷ'a>KZk__Av| `W4tOډ'i۾l߱~=bc1zL#6{Q@vԽe~q\4cwݝ7U^_˶fZ6TKᕺܺr*6L,G t݇__f͙[^ϭmѶCGgߪy]~N IDATVvzK^?83Ooʼn'+͑G*lK3f\gUE={TSL>#3+'N{Z* y,~d FV3/u؟!uG|)Pl&vyzKl@s۸qjSvm~KFu6i1%4$32_Vg>cߓ=TM)|غU}EK/H=m't:;}lem\^5[f;iۦ m۴OvMJFڷkOQQ=v8=0z_7v8P;!&F^W\].?ɻ]>} S>ɜYۋl&Nzw[#'!ޯJaQj忈NS2 :Dl^jfcOj*7tO(pU=rI刊`|GW8""ZvS駦 *1pxޟBBB0@* QQQj绷0}ҾߺF *\]$HhkMnB"BO]cA+ۉ8nEFt>Jb'HB> ! 3ѡuf_|Ȥk޽Wc<17ɬ9y Go\</ %o?"y^#Vx'KX&LHnn.a؈>#֭c¤Inqǘqq?ZbV]ˊ+%?\L_|Aj| {Fþb%ƎP2r\Xhס#.[gA>}Ե+k|t`@A}n2ѽk7:j'YVb{bj>۶mG^9٣;.[擶|cƗKоSg֬][av1~=c{#6fE~^>?]0x1Q^?s =c{o[m+8ְABCQ5bbZp!OMJ>t҅oɍ7ܠS=v:2z8222 @{PNo6[1F$XMKK Xv;3fΤ_|s9[ɰyCй {:dμyH\<8y2YYP2 gEQ7UzÐY!p\…ӗ6bZ70yջvU)mϒׯ'~@wST`Ҍ|ر_{O >N]2tu[ڠ$XA8:v˯ޕ9lH୅ yIݻ_|M[0۽ro7.N]2aDyY̙ۤ3 ^}ufϝˇy(/O*qVgEDc:PO4V5z8^_O? 6\}cN&LN KN}U;U$`w5 9N]ma**/O*8fr9t^^<4>qqC?ӆ{+FVt*@ǡpQ( '.'MPG\C<=m?A\fNL\L`\}p0cLzҥ[wf͙֯@6_|omSUq[i.[ypFzt4_l5)i_n]~G6={]v9Y_$v_c9ܶ&&2R6ւtWM#z$x.%dI͊%A9É F'p\T QFL$tC5r;k]EnC5"cZo |_㏱u&-3͙ЃL'=L"9i-.T?N:II^dz< Yjubڴnͅ ص>+VҾ];vM X 뒸.Hc!|чonyx2x|%` zI+qz>cyurO̙7wYHҚL>&NzݎFɓ\`ú$y)f̜EyX[^[\9-[ $$=/y۶m]۶mۆw@IG}֭߫/OR:,###Gб' [["--PӖ'&Ҷm\vDnHI!yV&.gՊDNyM ^XX}Od½f5yYs_h:6"9i-V$~*#0vFAΝXrngy uʲ!%G~۶үO^{ =_rg1}46nXk̳˃tII+`୷С=R6*1uG׭_3O=m[ԧ?zJ[-[|Ka/@b@,,,dyGرu+Ǝe҃!r 7֭YÊHZ8u:cF&OOpv˲|FƎ_߾B%hZ^~ܙ*q h"+Pᒝ]\@!6̂lNau1"^29FhP.uCk ]E 6йcGԯ@۶m8^ż{RVݺz:{ztƦMc@:uu;YB9~uɓhZ·˗Cɤé{2h@"""cZ׏OW, ճ'j]@Νg߾}ܵk  I&SC{F%MJGnaX-: < ̇qS˛Zj訛o>1$ItEٶ};:vcIMro:W#-[$y}2L?.3rٟ^'.:j?++g`߹GvEn]Qev&<,[[݂b!44Ν:ayݻyn6 :MίʸY[m`Svm7o^ֵsgmJBO `0ec -[ɓhuZt:T$S6F1`׹<ϟN:8q1xNÇ c XH"}+! CѺ;ٷ\[5bGž}((cyÆ!44Nǐؽ6)t:X, JH`8;>ع=z~(O˖-ٸa=ZcǎéSQttu5I6mp:j 1$4l؀gU*R{ڽH:u(8d . Fpj*qc$޵v_LϞ=U$Igag2u9e~%˳*Qe: Prjl5kH8 e6sG>]wӵKWN\?۵reӁZ J 44VK֭9{\@[m NGs',#'3b;JUQN[H㾢28,եsgyk.n7V355[nYk$I``QDoc.F bt?>(Krzh$44?*U*Ru~U6*ˑխ)/_ AYu]Qvme111F _C +NB$Fb Rdq!ˠqp. ~Z.D@  C5ѸE[VG\X$dҸ2222}f3[##/^3GEUܵg?GÆ ӄ0b0 Lڥ DFF/ɧSOӦukx15mGY222hXҡZ*22,zYn999>㢢.d_Դp¦y~< a%,ݻuo"2{RSyDGGQvm~'A] km|ŗ 4+Wy0hPBM~|uԱ#QQQ=CFQQFFF22λ4ꫝ|jIvVךK8I5kx)+oǂFҨe(Y8tр,(^\0(ꖿ?O>7HHHΝEV.^tr܀A>WY|8`(:pϗhJd0B tVEEu0#3(eY:W>mcFf&[g^16iR(\5m.[_zzz(*C% ZI.d$QF `j*ÿ۷qk20LP㺼2Y(pDQ%'xѻt܉Խ\ȠU[hצ ~qq?[nu>yKg,+C@u-zu5_e>mb[XORYq9ߎV̴ϑA>}xG/[Q;D9mM ʼTNyd杻v1v[cǎ'5U/P9D{Efm}G4@m+BFz]ٕO:*MmU)=%_?A{X,߿o,&<._Cq+qlȸ(nCQh$-j(\ :ZC.&Nѡt:d>[A](+e6G1?BTPOsAA,''GUpsힸo6? qRn]Zr 6l`$Q*qׯ? '9%E]^٬]RNNWؽt8݇瞝LܩOyfMsΝؽ']v1_v^={/.9HfvrL߿kpwIl}oa>#WڵY4jn^K:-~_+0p bH@eUYT6K"((5k}h.nybμyX< >|VZz_)vkVu+od25V}qԩ2GBz 8~"Z(-OWJc»=2IWtW`XXOϗyhbbbbxMϭ{cOU~$O8N\ֹ%++(s BxB]h Seڼʶ,tMnE lܴjؽgEEEX,<RrWU"'UWs$#nEq.;բ.d0#Vd)۱9IFB fBQ!+nt $YAQ8\ױVNKq'.d6mI?Nzou6tLLL 5j`cYY>qeVunZZZILr|| :z^E%FÉՋ2{֥+[ߪ^lFp$~~W Y"AAAL~ΟϺd5ޞT~KF7%P{s:~+ԺZQڵ+׬U]z5-[$뒓mڼY;ۣ[w-OTd>H-ٶc:C ꊊoG|*sRʪhEGE۱ߠ_iݺ5Iһ_?G Kݱc:_+..f]SV(~ۼ: uZܩ|NLyG+u\ǻQ* N:+[ aƬY  Юm;>]h߮uŗ_U| íѝ!CS̬,ΞETTT<4 O 4@ll/k.9ÆSV-Υsm|#+{̛*</,aXI8>q;x !%>qT_߾[̼9TQQdgӪU+K3y)MJ&Ms߽ߢBx,xu5-PYUE|zǟ|J#cnjOXME gnyW 0xpj֬ ]ϣ<^qFȲBǎBZZsgFĻλ[M4U[Ԏ"BBB3wS>EHXx8=hIW9iڤ $#6X'N)O=E Y(\{ 9 ˅,˼'Ƅg7'lKv/%$cG5Xe}Ǝe¤IrYaSe̸۸;W@ma0dp$D޽DLK}pYgãB.(Mz *"콀4B HGzG7A:!RHfG%YM@@833ޙٙ{sa0Oޮy9T*}M~sW8q$HIIa…vF#?OC5jЬiSw8"OLv{&NG2k233q8v+4BN1\f#55bŮovh4V >\б[y][ףܼ]' ;O\|џ,%Tб6Q>h`MQ߃޾(LAFxۧ.d*u?JvC9 7{ެ8Y#,ɄQ횗Ja6?]m;:quk#ADd$M5|xC[xО=nC;̞ǝƳ:̒3{:7if^ӧ$(( 4?Y~K.vZEN4ȓ'R 6Oa[|9))|Dd$NKo#pU(EZu*QкyÅFmưk^n޾*v]B{?{:~8#F? 1_7QBf̚ҥw6s+v""#P Ξ])?̢3gdqn={pQzEPP~ ҴI t>c, _UjUkق+VYַO_f+eʔaرܹgwܹ lW^{5م(bө`u>cb!jTO&rfΞMݺuSqUsSqٻ۶ӤߴP(+D!NMu64{QxȓmEv*YwR۷?]멧HJJb߾y~:nzZiÆ O:k.-[2cL=JyukfϹU4iޜ;1qf9￯uF ߟ_gMU:T.&|om I!v gv8s)jݻuqcSzV?B!$ &Of_nz;{ُ-&%%s%6֝\ج\%ã+Wi޼7l`y~pЫ(] o: >SZ 'Hreŗ\~z\eƍD^z565jdR;*+3櫯XƌUYظXXwOIDDFuVzz NGBBqBAy׳: 6m۶ȑ /^Œ3Dv#RR/ۡÇ)U$J@:s9_.F+ј9M]RlBEa\eY~iT*?M)Lo>T*&NDrr2.]QxzuNS={2o|nj۽v(JZLҴnݚÇLvp8fv;ժ=Ȟ8uڶ\l߱ADDqqqKP|6x0իiXA{W(՗_pȭiӦ(Ji׮III޳:?+Vd3ӱXI&̟;7N,34lpSͦM{` r頦o>w #n¶U2{~-ZUԙvq/YBLt ZŨ(bcc%^x޽~šÇxj|G"e St1 kr1.8Ғp\\ ;EJOMCc@aͤBJ,[UkɣT+A$11$vmL] JT?5zzt{s5B!B!=B7y4oޜt;ƒض}cƎ=lvPP$""#xw-6]Hdjj*}'>A2e0fpd7?ҡ}LѣGN:u+ rw N'J{u&ݦ'xwqZ7 .>S0yyezk) ߵ]|'I)Ilٺ}[׮̘5k0+Vt- +Qd)ZRY& 0kjp͇п?f⣏?FPР~}F NN|HxJ›[0fqTVճlĔL4;@tI9GN )q(:lNI&'OFBxV}2b/఺iUhU`4tQ(d@#B!B!M 6oر 4iBÆ iޢ.@j%h$44جY<%ef#88reG:lr`>ǎcСtԉS0_۷gԩs'[nJ*TX ŋsl6 ,3*V@Ra4{}b^x T,F q}̝=+>|3f_ӺU+z\w+ v+R2u*TvDKaۢ9wf@<ׯ'3gb|f ElGv|:x0x&L 44Գ`WlQ?GE\|<IWj ?P(P8\ : C*P2,ba\Fľj v V/bUPP\X/!B!B{$oӧ3w.'""ѭ;:{`ZUʖ`ƌѾ?N!s8sYwȗ+WǏۂ箬;s:- FCl\Tx? eԩ[7~gиQ#vù{?___ qYw67{v)W,ke- +;vqc4pM6A3'{yvUg8Fn硇jjjwYN$_Jnȑ\&--`UR4::?4z* :_QkԨ4j ]JaWSV]| 3B w֢#=!B!B! I߄/agٲ l:N;ЫSlݾUWk.~->6Y#ۯfuү_\TݻqFf̜ɚ2rpF'QRE5kF3T*o=%%53cL|||x3ɗA3~7TAz?4'MjZx][.[΋/o1r ΀W_F޽z1/.{HHC &$$jTΡÇ1ҳGO>Yb9*TѣmC>?moDY&F pP(h԰!^~HV+?ױZ\r˲ ȑ#^q $GzZ*Nk ?C`Tzj5 F ' SU+ljC8,&.'!Nh1P;Щ5R^.}H}$/B!B/( xδ"+20g\=3TgTCMP=3ervE3NKMKto3$IIIzM[V E^9NS Zb+/$555O3/9&*-=_<=33ÑgMMKpXt_~%?n]&L(oۂ"JZZ*tYfr NغA&wMK*SQkvyꭻP46MFo r>A7!B!B qW|G,Y#GPn]*w]wQvm^4ȳ`5kSZȏnԩ4k֌aUYq>T*JJ ŠU$ XV2ӌ(f(B U8v(Rp-[jap:,hvү&`4A%S&4+VU֏I4\. N|9z_~5bUvΞ:M X33qmQ3{6{ NuBDJ9z.d8vmJYJksbJERC/nӽ}B!B!ĭ% xqi޼9͛7 :>kǮU*Wo ߿u5L1D_BD^…+I(, z9TnДI&YӪ(@&. h4oӑhGTFMbYwVVanjogP(ThOO|߽K`LJfjszKRL#K}Ywsϯh8G({bWIB!B!➑a"fBAxRn_DR(g @QSZu’q(SSAT\&5U qZ J%jR$߻!x}.8H5l>|M^OcR!=wGcTvyM?}GZʍ ۘalCYE!B!BBB3SPbr/ajJ0*AAѢpYu4n؜3DZ9.i_ vJ |p@Phf#rھ>*ۣswy9~1غh>ِsp5gnЪ9\X)T˙=9`:|2kNEg?)]y'I&%6`ng]B!B!ĭ! x!=C֢J,%\4j'v[AANryNS}(FJ.Vi/ŌB"tYC rr9pb(_1aGPPqZ:gvg>9`Z/]N'0=^l6Ci,: ? TՇy,"253PI[ A g5Ysl"[@ه3pΎ-$B!B!n%I !N*Re(ffaAzzcJ2WRIH tr%KR&,ӆbF݂idRN_\ IDATJT p@ Fg/g}d2S ACNXcrJkObLN'Jft7!3hN,t;`-^A>rŌ/B!B!w$ (-򁊔Z198,NFNG<4#>ZNZUBb‰&4$4 iN-F;. UP z%.dd;CV=Z# UO9yP* -gڳvoT-*3,B!B!*B"OI-W31 z`4Y0_&VM (!՘naƬPcBJF ..8]!5xu:=UwLG<B!B!]J !?a6p4j"0PX0z.;'WΞ_%DK_ *$~j\jW1ThMatr*Jb~11y'^ʅB!Bܘbez_! x!=R.Da4}A l?FJ0Zmk`a23Ը=p\TjA>V̈ B!B!!B3bҕR}OiV+ N+I)h Z*}u)@TU}u\uTBrJ*A~|  鞫 B!B!!w}>==Դ4uibtV)4c˰㧅d‰J"&*-Q]a'&[\ȥ tZ-J43VS`օP|plݶbX,J]}B!B! :y$F#ԭY HJJʷ,==}Ezz:K#T*q\ݷWPxq7jR]wVl̜=Hw3lEy}9,\Nbb"g^wO?gr.#G2} ϰQX̙(Jv Vk&fjʅTN%%69Y/6?Ī 5% U_`(V'_?p:UQDfv~oƊ/3Q6YU!B!VYÁC,h4?-oҼY3.\={ؾcIII4k҄"]{djzX,L&4g[Q٠~}ԯ={ظq6&q\T6cUZ]zL~*_2حVT +ft5:$ (5^!DYKů}ժVBqpG/AFBJEZ [4d-,.\!B!_Zp۶o,bu4_ȓ'ݱ)‹?c-[RJ? gΞrq1^0FC.]ݵ EѺ))) :J*œ=pb͙ͺYZy#?jLg7g6QQx韹<ᆬx|>z4ʕ~عsfLשSP(|>z46nBRѽ[5B,.ZVS:Jl lLV|z4X;NJBcVY[2ȰI0U(C@?2vNg*N\ܽS~'e|¨3h4p8(].?[G/_vOkP_Lzx~, AVmMV.B!BۯlٲDx9zAA+[6O-QT( w0 Nʊ^B#-% cF sncآ\ع˗rb^c]&MzW˱X32dSϳ,7mƥY:R/z.^!B!8x -[,rJ( nge)]Ub6IJJFӇʱa<4 ZmV~׮]֛T*BBBܱ# ><@n]ݿԫ/_񬒯bŊ'h԰!JqϪBd5c3gb(JGbj2bYq:|AR`w)UddJBD6`*ՒNW_Y !1 v )kϿ)%^C1%'oκ >ŝ;6e{ɛJdϏ?Oޟ&rlB)ɄTctl|+c ǟQe<9fz4@*jCӷCYU!B!ɦ͛ b޽F8z(5kSb0jժEZ`0PxqVC';=hrLHH|jzW*8<)g:8(<1! *aǜZaGTRkQ8l&rPN݊naDg%3-cUPPjp4B'p:Xb{#Msz?#Ͽ;H3R"rJvO 㗙YyGPZ %%53k%>/`7yb<Խ'baUB!Bq5lؐ?Lɒ% A wXr%yĝ|0 X"xoDRyoKx8*VߟtwٕآRrىךə Bo> 5Ll4 A5v3.\ZNK&j/8ά.evH Wv:ȹpV*Qp\v~;Joقn#BE&319.ri4M>đpy_Upt|v;6BVZ+h3|jiB!B&%% b1|}RSS9s ع[ٽ;x+WGzz:=IkݺtrϢwThXPwrV͚lظ֭Za2YUj̖ S,Ϝp2_@dd$ժUsD]xݣBcLAԢ/͜Jkl@PeutVs:v93RB. IMp}9gD? pnQhz+ׯ?Ԡܯd) oVS/j fu:u  {R/UcvN :OHPkO( @fR;#%˖sK&ib \B!BOb|޻<| fzR%5W|}}QT,^bYmw֕ݺBvg{wDVRXf{o4@Jضe3IIRxe@zA= y___Z{^!?u3'uO7n-[QM;RcbȈ% oj >Q$>"9+ ̾=1Ne9v jR,Z.]&HOr~##ܶ=;^B!B!ĝrW'J%~ڳ~[ j6Wz#7fBTVg8@q @7H:w߰┪]7 lY˖ͫ_O?d՛)UO`ۯ_o osk/Z>0Մ<*SÎo0c[Tj5UwgB!B!g'xδ"+20g\=3TgTCMP=3ervmzZ¹\.wf-^ݻwVkhִ);u"(OFd „C5ڳgK, U*W kBBBt/J(=vdvrr2oC>۶tbf͙þxXUT'f{߾`"V+7&~$}4̘Ymܲ;of?6n={<8sc\Ν ZܶaGD`0̜Ņ7Iɜ>{sqsz JMKI70܈ >ǍgѲe$&'q1:7Dfoףv<}9 yEv,y;os_?o&?wҳs<޽;û0j8悟B;w1NFF(HɓlٺNq<бWm4l܄K0{\Z{>)ݰq#iݶwx+V0u4.[~1cY~=G.ҪM[K-A|l3}ͷɗ_}MV ڍe˳/c3t؉]S9jtSA6m9!KG~ϱd2t# ٰ߿hظ _^ƙ3gl?ZicmW_r_ 9n噾Ù5{yxzC8s,»@\\û+Wxx##>ncr$Nq㿡uvtٓ6Ҷ}nGO+JƏCGl2W<11PThwcѭ;f2غm Bחg<ÚЦukF `@Pиq#._ JfTTn:Dr\bx)[)w8\r%O,GɄ5T tj?9!֏>J,ҩ#e󄆆zגv*Wr%YeJ&00NF抌Lhbj]82o)@F}Y?.]RJyy;N':ʬ-ZSlZM+W֠8WZ幈;"-55~OS&{3,:Ě?yoŽQQ{7-6{x8mZ^\#7݌צ۱r_r_c.]1_l]Efu"3!oꄵf͚4nܘ-[дiSY_F#111j1hZ8wcj׬ϓ&aZy2moFXoƍj*lظUkְdB:|8/^wҦukŋx tɏ?Ld 扎FQ̏L& [y m_we( &N?$ϏnYA'OO?FwthMŋ(G~OBb"Xh(Uʏjcȑ97>>>yoٶs'Sgl2Vy7wiۺ̜Ż Aݺ^.DE1fE˦xW"=-w>g?:pqqiHJJ*!::ν矐dfe0--sMAÆ+Gtɳg,[h|c򸱸|:j 0j0\ xSi$ARXa#gϟى:waźu,_0/OOGaöm=y J+d|WsңSgڷng{dͦSߟ"HΓ[pvfقx̢+Ӌ1ÆRD |ST:NbΝ$&&Q LF!C9x0Kj ۶V%KIOOgQ08p(!1)%JQkseS'r;W.&LerI2w:kk6Mre-u,[wT*Be?vT֬Ii >?_""4f ͩ@6XZs A.f&>MJjt˕P;dQìYB=4j45c׾};&O۷f?OoKCdŗx,Z̋W/QԔ(Z1Æu!%]cT\waŊ!55гK}:&NKv Bx"Wn 9%WmbbXt)!^#J^2{B.3v:d&Yn._F~: =RTP>=/7T7/6Nl &ŌӽԨujI{~&Ju!ٲe ſB+JiӺw=ڴnBy+%))NZOL???>i CǎHLbٲT;+[nɜ:ziu.iJvmHV-9|\3Ð:Rv-5jȒyss>k7of?}HP(\e+R=[t|ߺǃ`Yü3ؾa==:ufi!Hxڴx*RRٳe3zdݦO|J`ooO=M\y -ǍgKKK΃TTLשÙo#x9+ o2| A'ORϗ Z]~e]ĖU+ټzQ;ĘIѩ37g̙,]g/^0}\ ώزft.hݼ9-5:EvHZZGOJ@`YeŅ˗߻7wN͚7B餧xJF̞[YxmXgԪVMPLYe cV^Kl0ŋ 4ûvRzu~Z1 }R/7QQQ|RMY  !TM[[Ə7'w\2m.>իرqE bz,B\vr`v2J6lӄ}d-:w.oŅ4czm̳[CfOJ\3t(&6.&T4֙KӒBe_|^޾O x>LЅ+WPT~k7nP@hBݶ)Ʋl\ CzZHքpI~;D"a#K-sibAdŗ{bcc͎ رa=•+&) !KC)T-P=4$޼&>Ɩ+7n6Iн{ƛ?rHK+Teբ̚7GR5%3\رs gQយ?Ņs`\ZjEufWsndk( sy5}z2='1wogdd0| ۶n1*B;`3MK6D7S=}JF1>0BٝQBy{PW?H(pvvjJzJ\rB^{Εk;i3՛7ɝ7!hڰވi\.NNru[6 ӧiKY*9shg`hT.R[[4;s<իTreJ#ˌlְ}JLf'K$<*SdTJՊ5\yS|#C8}2+/OJp>Mo`mD7\Nڵ8].:&{ҰNnQr%z#XN+S{{;*W %{WVVV4oܘW5ƯS8^\ r:yrX"*+C_ 9&~F>y\L.6D"7ߐ;gNe˔Y']΄~HXx8yȑжM͛4&+bkkkkCдad+SpMlN`I/]e&wl[=xd`4+5j=r& sq&0& NBAztZ!AAԨRw7W2Q3{nA&k<=Szɉ!/y @^hޤQdCaF!T-zr@AZ[wжe N AQT)Es'TRk++Z 'GG nyӦ$(gS7^m qcqg/ hT/{d Kn*/!2:c;7 """"G2rYl~,!;ݻܾD% #ɓ\qSǏakkKFF5=M/BB8} 6kƂ y,N=7ذo#ínaۛ^E4kтH.]´)SLO,""#`sw=MO3ܠIJJ .]FXj֨I7W7zgn`dĊurr2ʫ\J&QF\]]Pfuܹ{#G뢅3:c}|oi:7(J^v*T*޿'T*9z466l\aJxx8Mp"퉍3m{|͡0DgT*ˆ( {xx8gȿE)/...}'QM֯? /RbE\\\pqqaڷPlY QQQF_h%*:UM36.N!""Oe."DN :jO&a?j+vfJ%A1p& p)O/Xۨ~}&͘ɰ9{gSzuN;GF !eea]HtJ%?_')QVtze0-$[l;T1#JuN͎{řBI'wJv!} Bs$QQfavBVGDGe,\`, 縸8 ,:LY=wS"~2 Ji1,aQF+R&oB:(;%#%DFFɮ~梥zm }]4eHZ8| {R?w'"*eJS\9v?@ڵ LRx(m][T!zZH< dޔFP [:̥%z}V2gbfMgۖftBR[:*W`v[wжE N=W w*O< OdFN}d!<*hּgG,a9&seT*FMk]ΦEDDDv|jktWClvhۦ5'Os'MszojK$%%X TJReZ;##1c1jp6hH֭9} 3!6͛IMM䵋T  44AAWRdμyiՊ%K'OnJ.ży\>322<U`DrRjZ 1p965W-;vAzOh9stwsOnѮ%888Pdv_VlپOfO j$;X7Tb4 +;!0ӶD"oB:(;( \u3W%:b%SLe7--Դ4-&GPDMljT; 엃G||~D%#"J#"ȓ')٢AHЬE ڴkOLTR˜L/5bʕxyyѲE l5c:SM׏Ңysiؤ)-[TRݝ2vxwLMA\V-ZpaZ ?`02;ϟ~Kй39b~#GϹ4nL1+\ʩg?n,'Gr*L2pT̹guHoceeE~}MRвE .Dƍiס#%}4^K iѼ9=MVz?>2` }x~=gy6e2ݧFԪS___ڷӄٹ{7w= ;/Gcv6؊oɓ+f:f gRT#KʤjB/Kxd>mk7oR% ww<=xUK3*&F: )%/^Ǐ bMSٺc'h;U#1Q3'DՊ|㦾 Q1+]Z0>%@3JNѬPr%.^7T1}eprtLRlݹJ1+kx>lrFud5x[qz?r?Pʧw||y;H[~νA!xJ>+ŋ6U*glQrD,_ ~z#*+tqrr6m TJrJ-CRQr 90}-٥+W퍫ܻ Hx5h uY0疰w׮Xs.zݻY?̆箐>zk \NVV-`J8u.Pɳg)U9]ի*U׭3|JrmƱuΔDZZ;RI_|swukKJ35a,[,a\\\(: e٥r@'ΜERR?u;(OB:O~@lJ/KRjר̌Sߍ|VcK$bi߆X<=y[6v…yph>x(4mKZZ-:n]L&YL2t:TrJ\5ΖukVL&ޞ׭Su=vv }kxZA} IDATFxHJgO7/d>ysG.۴nzer9666F$guŋ}ckkkVYƚSL&۷d2i_BF>y`lΜ9ٳk'QQQ>8rв~}7/VܺqtŋdJ@,_Ǐ2~nja⌙",":žKeԪV RsNWvۏ-ai,]QF ._A&^m}1iL8QQ2XUxLEdt4SƍU0=z0vdT"fj U*Ve89sNV4 !xZT\ӯWf.ex eK!am܈Ѱq,ws4WzӦE,CZ5W /ӡWo\SfpWŅ1s<܈\2zIG3c\Μ?OBABץhR>.b}Be9SF vG~MoFu,Z3L-CQݺ8^(R}jz (J?'NBV!4)!Hm 3\(\ eKB6ҡ=v,[ccm#>[-s!T-Q`A mucƏeg.̜?5j Tɟ/2#JmxtfEt7Wcb7$i\7oߢcϞxyyѶEś,޿m2g/Rҿ_dtll#o-6q5x0Roj,kOww~5Oٙx&` ~I*hl,S)XYYPp"#)^zZOծ:`N=-5UC [ osl[ fժ=0%`KʴVZ NQδ,!=4gtӳP{dZc߷[z>>>54X̐<BS"P0~kt6 vvFUC4ܑJ"6.^(JJJ>oRT"A*D߱qO$&%'BTP(b J36.NVFg8;߄"1Tjm lmlf_҉A"$[$(smVR~>)9R'wV<;$hCdB !T 5-hJHH YHOO3ՑPT'$L&Ch(zCȳ(7RIl|Tɠ!)=d0W_Z-Ĥ$T*_JJJ J*KaNt!B Kz_qY;f Y1ժTikghgJ @Wk ~66t/"9.U˗qA.^D n&rd欟1$9'R)3=,7rݻYӵ|yr|BMCVYN4y1d(Jɖ۩WfY~DXB&}b!wEج93+Z|Jc#^(/ 1t%g{!+L]!B!T}?\Y[YY}"7ً 3?o^IjU?;2L,]s&J|IAQ14jNru5:ɮiksd%_sy-;bNgEXxwg[bs5+YӅ.1o1KT*SdPJ$l=J:= [*×m֚ s" }-,mVX""""w~Oᅣ.WT"1ųB^t?^^w#JnZzŻHJJΎ6tE_HLoHAPIF$"""""""""W c>̮ݻMwc8y4~mKLQ*˨T*RR @7nԿÇwϞ?K<~4S(',JcΜp_+WsRSSqW5޻w6 ^{)PjQV-.Z1ͿWP(}z(=ұKݟ̟~BR#֮[o;K= zNiOIƫ_O"2Zk>X0F%8L##<ꋯx.dzSI>QɋDB"ZkoK%&k""""""""""_hς-[c.._CLO< f޽ص{7n3aM~Hn9u8gϝٳȑ#GB]_ DŀMbLP_˜:}:cckiJsLni?&84[+)keepA()J(DJ28$^B.ЯFAZhOǞ2* rgTZxONJG 4:J{㋈;Ǻک2Ԥe(AB*bc#G."(יo+DFrR:qT*>~;vr]?b<~F XL9 ?Znû8y ~Б?VYbԫoڴkϝwWoN>gϨ[t֝rjT2zyV7};u7Ї0%"2?@kkO:M شe ]۵ӧT*Yt4$r Dxxgвuil\INIa9x!o IKKcth@L (_1ܨaCfL߯/E ۷z\]]ݫ'S&MsZGUTa1-RcGh/}c-R'''ƌOE8q(g[ "!ӫ79%Oc٦0a$ǎlTܽ˼ Xrǎf@ sF?J%?h<8~K̩̝=y jz7l{7ǎ&L>42H$|IXx8ciRRS8sÇ ceNG>xCxoܠo>aC?vLÇаA}CxC"E$ jk RSybO+QHDdfJPiv>}#Gؿ?3gtTZwo?xo/('ٹ"v2k Ǝ-X&B9V-_3x\ɓ(^sO3Ο 5HS#jҰd{`UhZ:*xS'[ @QrI)*& faT˱R3TX˥t G vN}E$ kV!5CőAUYخ,[!R M$2!jgZ5H`Wf}4咒$,>3_/ 楘~1OmÕ<Wph`UQ zOBJCvcpT]Xu%7^kBD'sIȞw޳Jlo O0/c,U[ Cw'5i@< 2 OHઌoX'ؐj kIQhWfo\xW1V ]جYq>D!ßa+5?JMЛ8* x%r:qfx v(ǬcOxi?)(ÁC3ei~Fj49'cRm0&kjGrѯ2l K4AU}ED' ȿ>=CJOw{w'#rQ7>FRr@BBj  C > E[o#G"#"Q?|`?prtn<~򄞽B y!7nБ#,]*+q:"""6|{D"+W.F< 3t[ GqqqL<7o0o/x{{=LYB.^ļsXv qqL6 Ɲ>zZvÆ'Sj[+WңGw6_!3s GGG,^'cǍ'g'#G jTRSSQ&ۚ2[r%II9(Rt n]о];n޼ņyV-["zKGluBƍ@rr2D&gz_c@?ȱkۆظ8n޼Eִkӆg?5 aee'N$"2 eFlll8zuW(JNН;X[[x4ʖ#KdrrQR)M4!76668qvmbcc#۶nEoѭKR)9(\>T,ϵҹ3Er khҸ1U*W&###K^ăBT*G%:{#HdCb\TSH=WF.Z&g(ɜq%$*53Z_dR 4+K%ZF\*! q.64)PTЍ.YS9g8m{c#h#gSWѸYSr9R'O3ɅL4*TBA{mza^V1OmHHɠB>W2 wMhy4弝IJS߼.N:4ehC!{> Ǫ7\\{C2J$Zhi08 zd+ׯG) Pˑc IDAT/'i=+t#C&BLʩa)ǙQȤR cKN֛XZ&_>8Yqe4!8XYخ,%s9ODDDDDDDD?6#TqE&XwKY(n̉w.w%:6 R7feE?dd(5r;v݃Q#Gi?|ѣX5(t;;[+T={ 叽P0l(*VpBܹ{WWWݣ~i^cQ˗.!7k׭gՊ8::'ɘ?w>FE6d0?6Nv!ʢ_~x1Z""_Jjհ,PjתEKSQZ]d2ׯgيY 05l@XX8nQ|g'gRSP*#ܧtXT*B/̙C;,<( I!.mDJe^G4&3%,,B 찳x8;UL1gnKP>͋T9`d#'9ݸ~f:vd֌۷~~,*&3fc~|?qn#lBQq1yyyA. >''B3oѢEc>X͛5 o**+%бcGu۷/͛7nrTWWs.Z6f9+ع{sYLΟݏ=Ν;yҩsVCMQFGϡM֜3|8. #XVRZj~+*V%ҥ3>PqĉtؑN?tN.̙;+W's|a>xN\E}*;+R#u')~.g~12;4O7++ b>x&s|TAa!<3?xN; /+.+.={xࡇyFvv\ugq*NCU?{^7<#lÆePSov\_JUUl?6uܔWTX7+ɬ&ʹcpM7s4ɡCp9jLCפ׺~ldׄH05sT۝PDM>s/M9C2fJiilծR4M6~&?gf~}TxiTe0?@-=Mt;첒VN쥬:OM,[ `8EN:rǙ0= +#N}U?݅gw{TMUn~,Zͧ&AҪx/mϧ6%|| *aMg:u eg%y4w ĺ~n!Buئٽ'o~#4"#ŃfI )ilٜHbr h(/6m믿rw߳jt]cV#P;>6={Y|<{͠MC@{ظim۴᭷߶f8{&MI8m[f ƣrR=1GӢEs,~G-_g3xD"VY~?O= {vq'3pYǃ!]xNonԨ|̚=#Gy3K|MӘ3gN<Ѩa`g3[1ln'x4p TUU2h/X`())[n*R@=TU3v߯?M4+W_;Yu0`}<Ǻ0ޭ[A/(-3//g8UUN9ŋٸq#55ƌԆԩ,^͛sl>?;HCN8?a}k7ԩf ^ӟr1 >󒐐u^Ï< ae~?><X@iEQ)''ݻw[9z\g˖ӣM=z̔bA }5i5''N:1cLk3sM\cޘ_X-RPt<.rHJpSk: 7P rG [u5f87#5Oc9{&aàq6$;%-Mug(zo+dWIݚl,rpD݇h=o|>3װ.yׄrKmiv\ KBL` mw z*I^I^:7[4+]OX#}&*77nd'{8YiJB }v9?>U n K*PsT>2:sӿ}6fiw@!Bq3 ʪյ ^/^y+(*8h:x<*.U%9^xOeU}S&OFQ5a-[ƺu؛G&MzǺio>D!;wb2h@L̐ƛofҥKgto]ND08_?r)[n;9ؽ{76n裻)'˯oI2塇`ڋ/r3Wyi4Oc*Y~)//g` +**$&&ZCUuť||4{ 0|Hޝ)O+OII?3SHJJbx^:th`?;|LzmIC/Q#zMqq1GҦ5|g[\zO8iժÆQiҤ1];wgӾ]sLNRS CL}a!~/;|<'X5/97̙()-eqу,M+\x*^[F0obքpDgѦB>=Ŗ"^j7S_<ȶJc >75aM,\ӟo[Oe'zc]஍^GKHh(ռb?䕳"X85an,`G=v؅[UXSE)Vym.23g2* GdُMV{4z #˲bNЍT<.U壵8s.-) .ajւJT 3C[ OIT#[f&1g]>ǟ Rr=kj5{ׄPPx{]e0[+wRk1+h>:^ɚվ"mT$x\<f}GPiȌo2/QhLy ßnqV<5inB!\MvGbVmu}u|^s=`'ؗe+zo^!z=?ҥxWҋ`~ru Y|X3j} ߏ{@% b23%)l23qjFqq )**"55:v05j"/~B]PDqrY):ﹾNII e?Ԃ dee5IK)IINrRQQAuM5M_^~F%Xaܘat]uy9cU~poHC@?e~?u.97>qS7kE4E:oph:&*|WC<."&9m%5zlKAU6`KAT;8:5 sƽǥZyhhuw>j?q(]ש k$zhw @8?g% :GpVva Su[sFMʳBҕ΢"~ܰ;z Xk\01_ÀfnG_us]mݾmܮ(gyt[#P(At]'! iDVJ"233E6w%%%+:EEŤԝ:!rrjߊ&if̝ e{޽yZzC˖-MiOi5g>oo?VPysnÍRyMݜ }Y,ZĔO61UB!wg)h'.Yv0f麎uߩcvT`'|3dpF-\׮s.ܶlªkU|bB '|^yY$'%ѻwo:yJq+\yZq1}pA9oK.>^K0&f,+˱mNB!B$'Ըqc H88ЫgO $&&:PǏ?n{n4m*=6ٓNP f2󁡿֪Uqw~Z+q#G,uJ8oi1qL3g$)9!B!~/ݫ{|MG5ax&,}>;WVb=#2񖛝B!B!'!kw޽{sWzjgB!B!8$/UWӳgOgM=a:J!B!BBBߘihY QcHE5;vF\TU~ !B!B>_((((RQUUq\UumAQ~8q0tttuаՀj12ֶ9Wj9ƍX(f砾c'Vyv=uűQ{hٟ֍]>YeFyq~x<,Rj eV`\q&B=õn-FqE),'G/W,#Ϯ)IwZZ|'!pix+nWݗ^M?s>B!B!o갛k:iE#qer#f j\}Q6%6 ڨs5ףAmuEVZIbnsWjoWjN&:7i}C./iuk'(.b +AAd6ZeՋ?G(GQ<۷;W g=pk즦pu4r9Cbݶ>B!B!o}815x6F8r'k|޾m?F4:jH7cwLƶ]t[1W5ژգ TΣ f)mK*^QAdܸ`Dz4rK>ȑ: 49_i_ =)^t?Oړ([f!% 5B!B!wpXZECU5t]5(fޜ5U[@W0o=xc;7k1ʭ - ^mu`0Fd:AF$ nǢ拹]jK\7ˬ\AEySvSݪsY<1$WK4-+h//xn%%u>kͯ̇I%3ûu}<\9-5oz[ȲB!B!. u-Fp,Qbm\h(CGZs?]~8z8UЪU+gŪkС= *!B!Bqvx#`k׳sVnUU@<`mj_z1jdXpp,'יcnUg89ޞ2L'Ҭ3(a|um%mȾg/})vMnrÛ>9о⏩OL{ZjI^տu /ЧwogB!B!8|4*Fͪu]ǥ]nTWx՜5UEA΢7++YV[<1ױ0ȘhWXt*1a9o 7׬z4n1Xz4=y-([3Aĕ9ؕ{/eP>֝m=:~G@a '~5\MՋΝ;9.6o̪59曝B!B!;l𱘬jH K}3ߣvc]5YXl6*uwۆ9+*wVX $U+fގc; 7`X`>΁Ywg[!=b#Em{kpE_t 2WYh۶-cnjWh!B!Ba(F7aYt2fx겥cwE5x3o8V3vgVc *[[X{[H̠k8o#3`̤G1FcU_?+D\yMGw&ѕlF tۇ={mQ}{/. P 5=J*e%dd(/)c(D(AQ](P(HF kʞ<={gqcB!B!\]>>j[ͭz4nݍ|l{Cwň7֝D Fc_gկdGESPG_44񃏟5+v+_kmr݊ڢ#Ǹ[{¨7q76ZX{0x@N|JFN6{T{l˅ khD/OIiɔq+!Ao*b֑k^:NηYB!B!aBiD"\D"0{wl@ @rb9iI4MM"!!ሦ4"%GZZ* eJk&!Gը,OUrj" ZfٴnԑfV߽ &ӳ q1 ;:EQzs.;}2'5fYm䔻hg 俻 `eY^]O\<\DT*XsVlGuv|(*_Ҫё1B!B!DQm[@ua%|t uPUc6iN(B9hKyoM[%eF*rz5P5UՄB(*t-D(Eq#a|qOp'ҫI,^?² sBj3lwP H֍:fWsܑg(*) N>&lݷG ћb39h7du|>O!B!B4 N$&9)ugB<^a_nV^1rh+*/Dx3j3})Gy$m۴qV6fΚqK&MUoRYYɌ8ox:u L}w;ݻ;yR"fgIL&BEHyHLuHMLy(a"ۼwupGRILJDMɠbEGq6:yӻhъ{?+ɿoH\ҩ|˸6CSSZYȌ;j/:%(*1E[x4:n뎟/B!B!w/(+RUYEyYmڶs4kޚǝ%\HL'3=̌ gWqN:Tz{\QM(3yʜUlٲ۶9wz֮],7Oyy\K4nJN]8~:Lƞz*gp<33Iz#.-ē! TZnli{" 0pg5p [ï_fo/ וش<Vn U|65eYog_ &٩MYv:"9!N&tkw-W !B!BQ`(D0RXXBFЎ&MsiޢS,77R5#x= O//x¸7t6~]֬Y,zھ{]Hq>+طok`Pʸ<ЃP'@1G"6Tɾ\jb\DHNNq<3N͛ҡEK2z| ݲͻvP:w'q] Q#.\ E jg';˜կq}\)T} \@bPIL`j9X{+vfKw?kf{# 5Fzrp%/B!Bs4Q]{'cmd*dbO)(w]]Ւ@RRǦ [ <0y2}F4~Q>SҘ0,託M3HINfLfV^C?Byy9. qy xϿ@uڷowM"''g…s p.傿xݜؿz̘9i/L$bvu r߯O>EqI '#?(snz|> F3y~SUU-[hٲgqÇ 9ny\kYs\oظ,<6m2jX 8kP^zynu"7t#={ //{￟۶( 88Aqf̤~ر*{g1s=wOQGquױu6.Rn23чݛ/ }}Onn.W]qO<$~9:NCKJx=r'9 nEYa.EPS DβJ|)Pܤefm.60Ne4oڄٙddTPaW5(1ifkO1fs쥜s̰5~RQ]Jf5|ttn7 q? FU#u8.ՍcHqV-;9!}GR2Կk5Uk[Q>'k[!B!Bg4Ptg@=WəpIiќFWP@8i:sWTTPRRrx-nF/ZYgO> g3Ϙ5fϜ@.Ν:ka-7⪫꫘7w^|}Vt _kyt~Z{3'ѵK|)? <@u<>1~}foyxgG\~e\yEa:>=gŋ26:u3Osزe `pl߾;~]kvѻW/e[qm,^n'1#''D-Ϸ'v̛;3gif5m9,.*3xi,^EQxqb2x0s~BuqÏp V3f2wX83]wS:^|gEEE%}rM#5yW;if֭_{ϕ_nG$HMM#EÕ@$@(fSi9+6me?(R DBTUJ .t-L$N<;))T4:驸]z嵂wB\LTDo=F]irB!B!ġq].UUŗAU5et?-vRIYd6œ@AAEEEήjw 9*n`FljNMۇyF t 8tƟ{ρj% 0MrҠ,\9srBA~?Ç cEB!rrqзOB|״jҚ}کҨQ̝IW=n|c0~('m۰g^~V|m߾=͛GVPUq璞agaF+s2qgן֭[:eMם^e}r2zH/\h9ÇOV.sӽ{w}:f1xMY٧nlJnn.;vl } =9֧3rss;Iq$nrrg̸xlܴ13fx>cg?HD2QnCv.7^J#*7o'X^EPMFtɦGo}۷CFݲ1MK4OGB!B!aFQ"˖/W|Fd6oMNtvo\wTBAWc6:Β̩2)*,~{KEDFo9Ç3kl11v>ǵ]@֭jp,8yqܜrx-B!v|6rgf6YHf-)-+#93f`n2=R=n)h4NN&1!95e~\55UhN!E#r$!) BP[D$vA!B!BxK1~qi\ےB?o"9P8@fFΧoP#P g8;$R(//3njaguʤ,OvvGq_-[W5W]řg7Fhpis]w:(23@yix/--%99Ցܱcᦛ9i@С}{_BEҌ S].r=rsd>7oB:v KSF ^ŗK㯳?3IKM#0s,>٤f͚ IHH>iGH"$ :$V`oLU!)1MSV<$"xHrilՒ/6l%=%AQ]hz׋ B!B!⏯v$0HFJ:5ب-ZCxUjI3<՗իW3bήݻlriGn7III$%%]n55F۾}/B] IDAT3hxNUU}GN8?AĜoxtTx֭۶og],]|g3   %%%t-TUU9ɦSN̘98k0k=z̔bAK|IEE -MdeevlM.Ï49s0MsaC#2jg5U֬@ +F8&( ~N{e)Ѓ#6ФIg1EL=kg{Y<$/ uMFHtjR>t]'.b*J+PY[u KZvi٤Wͷ޶Z}(UW[o,!B!Bx)(+eXe;L57\MAQ>S M.6&Mdy| +ҭ{@u԰Ϧy~`5RhuѩS[ڿ_眑#3jT/$wO EQ8CTUSOodY{.K2z( 9{gO'q \r_i֬;UU9o_.诼]-q۸ҶMЁ'LnCΩM<>1g1;wIw[ee9nʌ4GwNtlӴiSjʌ;'P@ @JJUfı"%%dge*Dx$wnzv,UVZMNn6L-Ĕ$5d6mIjV_<0Gx\O Nӛ&QҳsеdO۩5YBh1ͺF9TWV/_4d0}iZn a:Zgzw:wȘѣ)(,|]ggIuF-7=GgB!B!ƍWϞ԰xɒ6[ّ@_}EOmڵk\}ii)VSN+bJgAyt~ܰ;z Xk\01_ÀfnG_us]mݾm>|gnUUQ`0@8]|(T9h> V|' rvuȥt@*=-VP;  Kvv $$$T3g}NNvq{1yͷY꺮( 99ٵΉ7׆r('zoZFUUYn޽&7 TݤZq.k Vػcy[wQVPDq^>y(o2M#QFm>wdL6mъV-iiُ5{v\ٸ ⫯= ľْMUog9G&>63dp a+ddaڠ!۷z#cT +>R44ÆcMR[)44RUd$hsdB:N֝7g`!|l۾S:uwf6WJ_7_{FLTnNޝ776@tÈɴ>-ӧMp7 q]wy4m҄۶q}ӷOoJ\aVlܴϣr*:uÏN`⣏Хsg֯ѣ9GͲ,>,y9//#*x!B!Ծ];,ebp8Ljj* \v%~|>O=38e˖Ū\| f PSveCa2|عa.Đ yg{ ixҲlQIQzI$$zPS<䦓j ArYXΊ sQA ?e(<()'i{COyNhڔ 1d`l*>cQ#GRVVFfffУbIxXCFA|^}auرJnNM6n]|t `-fӰlع߼Ot9TZ֔_C ヒc/B!BӠAv B|>ޞ9`0ȉ͚<4ʸgS^Q7`ii)?3˗_}#wBI(pTcs7Yt Z8BUB$ͩGEOV tQ? (:OKl[.)?J0~B}SSS׷/s[nŋyվŃɉ3jR^^L}\sU;qծ **i`q'k_Gmp:y͞kk٢wq{\뮋;B!B!oJۓO}v̜5 OR۷f:Z%5MCu,bE b$/8nxøӳٳ{-Jb9}Ax¨z+" ULvSPPYKՆT}U;F0^lI\6"V2+W%7[ngANvv5UU}tU|}ZM~4Zn3SI4O>aӲER5GcժU|"\.ÄB!B_CN~'t; h߾},'~?oq&)| uބU!tRRQnou|r^ՑZArӠ$"痒2~Ufw` DM"A/K}۱{ hTE剷@x<<u||>>c瓙a̜5ʸEAQ‘]? q' x<h6P)[y_J G!B!M6c;w"[^o"|>ذq#'6kF|zy&yyy塪*yyy dѸ]'*|b;ʲ=u0fX+OdS0@R&/XH׋hX' P6HʷT/h\5rO> Wf\3lHTEOر#߱cjLdΝL8:sѶm[kI*]:wK.嚫 /i&BȫSݻH=Wj=j{ZM{Vji;k?V|ΧtuKBc͏vZ j6Lf8s|ڂظ5ĂE=(d̰PP9z'Cǎ;5Ls' mɡɭp\X러mTY-q9 # ~K Tw:ޟ>f>KAA!iVwIݺu)--#33#+]!b1MoUpJ̌Cn [ G3B!B!HUU GCGc6!Y߽kp#Rsִ5G0kYm<ծ}^[⹬B?x n6sђrQ(*\ =XQEsf`KoB()=9h(梢dvTDInr6AK͓U~㞿߬`挷8';,BQes;5(t:6z=A%Sf!B!B14~⸱n.!ՑBht)uqd͝HCE!TJa@  eg>Cj$N{"hIiX _ƯjJ>l>= J˖-iڤIbjj_sUa!B!B?O, ou?{o_[GKÿ1'5A̝7;w&vG7>EEE y75Q0;3w&-[?Vj Nw3 UŞDs`bOi`d RwAw36 s)]AAqJaLř3P0t )zv.O<>v?\tCN !B!BqWl}i>+bF||嗉ٽz1j0H{+l_PSky׫OYv/rb7)--e 4M3ϖ-zs NҕStKߟMPyEaLk]oD>/nۊ_ 60tnθxii)z3ڵuKx~tl6o֬wf&^rL2,'= UhvTTW2'͞39Tllu;LxҲ0Ly꩸M,:z*oR^^NII)~Zj}T"M7fdNBnNǎ>K~xbb7q晜ӳg\ `rwqBӦX|۶팻"4h߇Yxoy ,_%CSǎqeBA9\ɨ;]͓+I;,CgEQ531f4/!\#Rݓx B!B!8\^5O4 K^1Xƛnb-ߕWp"&NsӞg!ѳWo gԔ)<õE;yG9O_zwߍd  }Ϗ޽3zE;o__mO~4_wE՛{ c++**ƿpN>b͟ .7/r 7r=ݯ?_jV?> ݇a#GGKݯ?wuG%_\pGKPXX]G7` !~) N$x"rﱄy4d(c@fyߟgCB!B!dz?Mv(EqfaOK&TZ87ovQ\\̆:6nu;w3ߞAff&/2w?fɓi}r[OF~~#j%,^?N)+/gWA]s Gw^DQf͚ͼ9sB0 .dIu|r%_yݻw'-5GKά_QGsV3;С=>47ofsIm~tVOGUULJn]kbsҥ3=>)S&˯ ^mسgU*[W^zk}FNe-|>{qf=ɧqu׳}~g>XRJ>ٽ{7PH$r9sq\pxn:Z'tRbPXTAv:-K0۪(./lf%(E1L`)H&/ rFO+115zI4U$]+W*3B1hxqHRU27Ai~- !B!B4+uK̈́JВ\XQuJê[m۲&:w|ai۶-K?f_6֭֚eUUh߮=ڷ'))&tnn.}ws/ws/Yqcɦot҅<.]Bۓ۲_l6;wRʕ3EQh֬-^LFb]?\<\u;w eKϷb F idff2x`|ӻwq~>;JAaWgfc WXEe%_ ƍp0rp|1>\4MqY=HMIҩclv[le;Ƣ*99ufZ2nC'{;thƩPPY?$P/,gP'.{2IdF".<@H%ư(ۑM.G RTkS>bJ~';?: Fyxa.om!B!Bъ*,]'S+ {JL5rp^{M:ɏm[wXF sR=}0eTlm:ߵegĝgSV^a~ǝ~mZt`aʚdfdĮΎݺ{:Xŗ^^NPP ì.R^^NfRͩj5*//E ڇwu}v!33Ғ^)))0 rsؽo|Ï?K)sCsddds׮9pdԴX[T̚262j[>|&>Nr'=j\qOѵK??B o`8@('0(H%jF1-@IdQ/Q,nsb5Wui@7":!b 0-=b[XjvB8Oy}bq7Hӳ'?w>X IDATI-=<<)|jX TUI ̙;ԔT0ܡCK<?UE\.t] HNWTxsдIM}U>.]EFzzܸC'EfffgUTF4-qؿ?m# H$8i1(ˍi(FAPpl.4Ձn FʉZAB\T'3O?ر<\~%_~`eaШsWμ):QY[ΔrZmwߵ{,~ */fZ |B!B!o)ASwЙn3d &<6Æ%vb0/ :( H8*(`O1Gؤ"ٺm[lc_nemmۖ@ @jjJ,!̳BUUYlْA6S5'&3id6@˲ͩ^qٲelڴPzxmh}j{r[>_!o%ݮnW]Ébۗyi Jݒ8B!B!XF)--U#)_#Zrv%9#G3ۤcXa=armOw3ߞAΝKodi&o.&CͼqImRRRmˍ7P^=z˯!Cp:3>{q30G3o|+*3f4;tm( Цukn:nf|!qաMrL~Ipr-ӻyuҧwofvܽ!CXb9}N:JZ&,\lj'6+HvP='N\.ڵmӧjy(1QPXE\@RR%%%8^/˞=iӚ5ea:{{uK/cͷbeqr4iܘ޽{1,h4?Y$ExfS?=QVY@'CuRSAѲIt 9qm Ž2*jF6J"Q?{o i)vwoi [>y7\Kr__>^%sދX;^}>0|ˮ Ӣ%'=I]c,_vƛxr8a2- _˖e{z23i5`}grS!B!BqtL=Wj=j{ZM{Vji;k?V|ΧtuK|>L$V ***HJJp$vfaPV^NVf&b&a`1MoUi|scټkxJKKIII#HܘJ˵uSbӴXE"BMӘ5gs^MLӤ44Mcǎ0u߮f;υ^/yi~2/~˲HNNN9{r$s'vGƛKĮi:1Z ZL f8s|?$/6`؇i%V˹2¨zYhvPm{hը>I) i[9?."a*CŔR'j0(ni9:oUڵ`w؜?k7ྔoÕXtm|1g'yťt{!^ro^4EQmv1ycn7gW2ތiܶq3 j`.xs&;۲yְчhXFM{1B!B!GCGc6!6lH]n H1Z{k:`u9{jfUVymJ'=iU몪n,˨jmYY+JLdH$;[oo݊`7zڰq#]r)wqM4矧9=8ߓC9{rJJJSR?Z+u;68s6&hIvgv$q`NMET2RHNaw)4lpD8lUDUEZ6;w'9y(h4 ~@?Jf|W\E dfPINq6`#!y ,_R\ vZWm+S]U/{N<7- Y[FB!B!Ǜ#vuԱcb /cٹkXPڴnͳOOew/ر8LOyfn-L6PLs9,IQnlKqmDPTR aCTlv ӆi(&8]i:AG *66jGUT,㸋b?alƽ#ƃ H߀\3ԋ&c~ ݨќ52 f^q)6[|դilU$UF~u9,(޸ʂ]dЬh!B!Bq$$ֵ+ݺ֪_6rpF>*O=Χ zÄGI |sMZ5r(gP8l6 ŝLzj In;((2 KI`%( eg`۲/ӧШnǽ6_?@yΠO]iX۸u&2y/"noOq(N{Mz{d7NW( yuF !B!B#! x!q#byKngy46H&#ӨR,Pa$9R9Nv $6(, Zhf9PUc=pKKal~d)'ՓzPʶn?A~ z6|k哥ʝ;h]kyhԘugbOJa. b۟7UӟI8q2۾\Ah3d#($B!B!' x!qH#R'@IBƩ&oǰlvE*)LBz`4BXש @Q#T5m\htb/9y 7M եa5+!qc2+r$zu<7ƝG]qh;| - /x?IȤjwQ\]3Y|_UUEכOzZɉa!q0D$G2=L0|g'ke F1 `9;Kᩃ۞AfR6 'E=~"F# ͅڈ! =ij-!B!B1|X,ʕW\Kbg;EQ9[mcÆ /K)67XniِRBl$m-RUYT(Q\N^Jvj&H9P w6ݏi(:f vLB!B!1FkG|0M^~믽ݞsv^dg0s[qH$B8 0YKڵ=0^18͚5xM,_,.^QYI>}7{ 4}c4M#//a咋.yG`Z3~G$<ͅ?Ti٤pءt%;]mgdg2z(B;- ÊPCYp'Id*C%#3˞iE '.)w.B]AbH!B `0H$I R8& 2_UC N>X|ҥQ7`(s𙙙Ly![2M0ZӁ@F/7_z #G թ?vh`Ħ)񖐗QI  FeǙ²:˨jV8.LVE11ԀB!B!8IyY̚=;.6g}٫7#F?7[+dEL4=!C)**g}<>)Sxk=T ӉpꩧЧwV:q~***ƿЧzǍ7Dyy9wkgѫo?ΛfԔ)ݫ]O;W_{=ַ9X2o]t1et܅{իW3at֝ M:y8h1-R,P4pih] xE*( TJUp7P)%5'e`!z]}DQ=H8*t !B!B$ `ѢCawn:Νo4pw?ONeYL6== 6Dd&MI|>Z*-x} MbƍdRfxs0{fsfd[oʫR\\|y8˲0-f'՝f#=3zv 6] 6BTwTba TRC3Muk`ZQLSnz!B!B1[T'm۶e9wPΟp8i۶-K?J_~Lnn.[7^о]{:o@Y͊r\.^(Y>zK.emhߘK/8q~RSYq~ڶmZ[ʕLzl"ЬY3>ZT6999]tAuÆIMISx^=~Zw-[`8ԯOvv6fѤqcRSS]\LQQapt#jXa8Nvivb&VQPVfSleYi7@U۝5'6fE4B!B!۴i>N;b`_~'H֭㮩|CsΉz5~өcGTUz$bƛ;t(g۶6@aa!7t3:ۧSNŦ8gne$o-Z0b0>`1yuxi C C}}ӷO[SR@fFFllNvvkf&..‹/7ʏSj]kӴsyq8**v͍eYh/7%q\ VÌ`:i(*Ze`qmRH`"B!B!_oUi%gΞM֭iӺ5>ԭ[7ڽ}w%x-G^^K>_~ŗՎ6R=M~$ٓ;,^eq \z5_,_^kx̝GjJ*pysСC"R\\ߺW4M#;;'kؠg~:tĀ9kF9MӸٹk=L[o"x$/;;3Į{#o *vC3@ P1(`oE'-Sٹ9rN'@tTUfPTP5 ݊rLmwl&ނB!B!Tvϖ-ŊQ c}^x.:ur*)(,  iҤ '4i¦~]/ p vnb t xΐA$F ^M^%0/ :( H8*(`O1GؤX}ښ6iB۶m8iR6ݻ8qJj^{ヌg݉4y2>S";;-[2w<@lك9S(,,5k0 } 6$=kq|T ** H݈)a ɑCKfOU)h씆d&#ɑ]u( `* '%'cu?I[SZ3%* }@)B!B 4H gRSSyxW0JguPZZO?L]c:ڵmڵ[N9Xw̮x䡇7v,@ .~0jONL hڴS#5rޜ1&vqر\}u߰0l'ƌoϠK\tɥ\sUƘK.IP3mw/_ݤx{Fjj*UUU 8;o-n\An]3$v9GnnRSRDL~#o7o>3:ӯ&2ʢ;ҬYġ{#o`QB* pz4^Ų:%U;HOa`:( BV,ﲥ`ל(&?&e;6ٴIOz=) %B!B B0\v%8fϙ÷}G9&fY -b@5*c  .Jb|RQ\uj{Vs5GgMY⫕Ov>S[:o~ȽwŸcaPV^NVf&b&a`1MoUiifPoU,ˢUx^/n ÑuHeeeI]~yޏrCǪ Y#=N;"2уɤs9)gqoas)&a#ۑBĬ®Qm6Ғ⶧(&BGCb_R3#l/S2hk/޾zvIon8}_EQb7IטN"#[܈fs0jk̒)wQW>W6=97u `"I+u`u4;u[ӈ4rlN]SD!B!6h4 @aQx^3#//UUp c%k#Wxc2xr}=BqZ9 > ˲(۹o.NwϿ24v[ߠw<+h(G= ۾=[LIe7SSq&LN%qs6,K n$IK6}O::' @n6#vlYi~eB!Bǫ`;:~MImpiѽkW^z\.rNϞL:Jff&TU儦Mh{ɼdddPZVF~bD?mBq<=5]Y7( w߭waj}_=O XQNWɻ\MHאh0Gu̻wWT[Uxw'o'AczPbB!B!Cӹ+k׎mF]}q7=<3N?p8'Bq*~gã8Orq%@q-Z--NP(RTbZJ^,BpwAC\sqe#$w>ϓ'ٽM2;8n.WϢaߡhSjp|(jqֽ`ۧ@!oN,sjVs j47777lo    ¿빍0< t:4o1~0}4WRR'N& p q@~ ""Y    3,#bԙ[жC0  X|mڵIعӱYhӮ=z^\t #GudΝL<2c&xJb09~4Z,^nKF?AWY~4_sv4wJeۮ>AѥWo͜ 7pw4Z-QڳA#FJIIK3X,Ҭko|:irnrvKiهvyEqAfk.%(-kjDdb\z.[Io\LBGV]xQAAexkԪY VK^]61l3$k֬ݡ(J"#(RoRN\FZS6Y)˜o1o ,eһg\]]ӧ7N"-ٚ%B< $}p0%JS֑0A ~~oWls.ڞB+k0{ |۷{fǁ#Ge톻ϞCRR2gkt}Ӎh4Ӆ'Eoqghq=%6OZfNr OfY.u!o]@Ȟ,_c7}Pvau&||9CUCx zRo%m[> mZ3R>h-6a8rs֭:}9??_ݿFp}ʔ*O}C`B܎i#2+-4e؇d\'55L#-,+U*U/C\\G|e9t!P!c0x__zv5>&?.XRl>>?馹i|b">aѲ1tVo؈g!i"_ zws.!95F^fQ\!wwݯOEձhJF1M7vWT_&à׃7wxW ~r7G1h  ?3t7VAҦU Ba$è1ѺE3:9}իO:܌?K޽Ԯkk\|`0[.bx(2hK4I:omu 1X,f+÷|=*Ī fTfPe2vGb"FBF&{o3C)J`Z9&EL4MA̔C1H0PKIʾlN9p;7 YsRKMgȟKMZ'CcvNj`_w)ۗU 4drxN *e+sNqNf3YGZfGc4#^v;\ɗ I>( 6o"Q3aMM*TAx6hY]-S,'#9NMMT_^: jȳ4Zdl}^8ʳyJ&SOvt4QXW,ѣ1gn}P_dS(͂q*|ެ(n ^6}\ŏƥީ4V+M1r+eӭ #S9`к8{cl켑^_7۟ t6|꾚zm3`G9~h]HTn%Ikvżlد"2lj{"YgBc̜a0YdӕX/qgm nVbHeޜͰB" ,{8jQ* 5oޜmw8bnݶ5jc]#³O<| Ks,X];ubgtnOjZ_]~K<}00/J8HBRJ)?Rػo?M5ӓ4/E˗qqqaTZ;w!W(P(h #hӲ%ŊER~ ;R`Sh&)ݺ! LRw׺uE&QL.[B>u zujc2sKzR&κUC.衤Qi/<ԢJi$LQKp)T .rk8OPDfHԵ\9@Cʾ(2_*"G)E݉UgDω f)A/.=N4:VC"緎Ù|\4,e{=4.ɑ;<.r/x஠e9o2(낗JNVufJyR6ZQ6P,?W^ ]lOT/FfD~^JJxTUskUA.᪔郈8lue\Yٳ}llnqLVHЙpXۻ q7ň\&C!0HE.ce?߱vUp;)F<\|ߺ8 9OAAw=!h64&L_NTɒ/o{Dix_?Jĕ,f ŵ ֭YM xk;|8xtW¨_w(W,/Yhw Q 5rJrZ딩 9)Ub 7o`ryZ57QJe<=yjV*zS@{zszwKZZ6`să1\S+* VOyd3?W.>o O3r ݪӧ? FA\d2^zEi2 ̙ ZDL%Jdy]|y.; @Z59?Ĥ$  ٽkcFIMKgrW,=zᣡChr#Ϗ0 E̜` ===B rL=&CG2vTPZ4kg?>wKlfwXxmd2__G 챭qQiijŒ:,̉d_шhq|$fΝǢ9T<}#?Q( xGu,~cFgW[oЪysiVyi=R~F CBnTʑ;jHWJg(h-J4RQ pUd k3Gu\f[4֔up9s[M ܕÙdP9 燴.4/ƶXbf]btrլ6ո[%GI̺9f4;n)IQ>nRkqz~wAAߕ_ϱ|CpG( :frǣr<%@k4A_x*Y rGSp{;ή{$ߏʕ*' ONm~܊ lfN%fy6?MlFTJÆxiiҰqxHII tt:@.3\ #dOog͆:I 7U9Az>v4ۀɲUk3vt,:VYG 2&zyy9v$Ŋn9W6dۮPǍ /3s|fyiArJ9-x~ER1>>bXKSY|%..J=."Ysy#yH,,|?Cڝ8}^>/sԡ#=~&6GدqZ L\-x-g`r꾖s+1}EddŜצ[8PKoH~Պq|p@M6SŠ1Bz'vL+}/|f4jP?6o$_)^:̜7W֩cGnxxx0k|? g)G`r9ukbg_0YMz/S87۵X͸fЈ%PJ%ڴc"ڶnH˫uɫMy~DvMJZ*ݻv/'6S-ydžd{^KIL-VmL+t_I~Ȑqur!^k"Ak":@xTp-jXx:^5U8v4Tq."42n+"R)(nj:k/%q-^O֜y-Vs763J4m[\ƩZz (J=j>*&Akl0Q'o]JZaw^*ꎯS2 s\!;2|=[t3Uݹlv4jn'nh-J}`fᷲ@9W\ oTZEZ i32dL&u;m26\NB!98FތŊ5sMWY'u#6.ͫ}xy=VE!J-8G /gGb̺KMvJIz[yt!cT\dͼ* \KE)aXٗ=X)S0Zrwzw6F$xgmx|\厛3  ¿/iizkӗk3+_ ww];kz/; ϛX4/9q4ժV&peZ4i M?-)urQQy.j[Ȱ4ujՒf=}AhFEZk*ϊOJI"FT?__m- )xg+')9/OODZӳ3^b)0LOTlFV[jrXVRRR+O[zz:j?_`ͨ2(o+gz5[m3VާrcF ,y]A៴pΓ nr*4j1`|?g|76fw`nlqjgΤ3A/V EplJc5r:)l[ 𯊺uIJN&U,Zg\YS̋J"iAWgr{\u)s^iQTT BPh[s;L_UP...$'yiAq%7qmY}͐[?!K^m"Ҿ+!<̯u:ۯ0~abOۆ$^IÆqf@> Ͽ4D%({,zAAAp&AernќnsZ-/^ 9,? ;BvelYAx6>wN1)^4[p⦔Өί׫㡔'2\[ CH]A⏿i'  {_LgϓϿRZ2UTf=U& ؾ-۷& 9sS0] iS\ٗ7JKAAKAAAAAA      4'[l1I     T= ϛ%I    s&,, ^/MBӱo>ir=[pui#YPsz4{A,zznYÅYb bGw݊f9>-kػe Of    /{}}}qss&g49cH9~Mn߈`I\=R+bED_fiI ޜO;ҦYwGڎKҽ9v9 q٥GؾfɉҬ:QwWebŀn ޜ̲OnN # iѠcE:&ʗ?WcٌIulrdւ_ ޜ^kVlbD ؓtoδ1#P.f΄X9L ٰspd]8 C$}'lۺlgH,S}1{sn FaXViV=v .H ܹ?su&Hhvzk?-Yb%&I-[&g힦?oh\GOw3,_N>ʭ۷m۸w4 PO{[^5iiw}  di.?_Al6;^7l,<ڍ[]44+O*N`z$:s\xFM5xoE2Ye2Ө1u:OEdybŠak²'pdӽ}e.[icRٽykL8ڻ^ t58j5h.v _`k:[݉NjR"e+UjӨl5rsSܿ}v;rb%xw7^_(t5F]Uհe{t5Y[2'ԩ)4\ͷa&i2I8nEU [n%,<\\`{6i2'O:O &9$$$pi|2/&?jT?Zq08iy n޺%MU(~9 ~ R:y&wݓn1L8n)))yi qEir6QDݼ)Msq#'[;'=R^ p x^s i?>/&IRa#"#s  ³?^w4"S,Ckz:M,-%Gmum" Gp,H>c+]ASTY=@FJR~0SR5 2:^>a#bP&]ƨסrs'E'$rd"Μ@&Ѡe;z? ̖s9g;u*UjWpټ>`uʒa ~˛q#ШM0VkqϩJ,9+S*gad9kՎmtmT2oB!HIJM܉~dwhتDQ?ءob  s'}Cd>i:J%ϵ\oYYD.=;tQW.rpfJT]6urye*U(_ ۮjcǎg^5l(oH wA8Q~ϼtA\jUP|Ii]P&t$U͟%>cu߸t|:5%IJE)Z4Wly2Vz`1qwQ$;NG0p8ضz;-ey{O[1,8wt?L+- SS}ɔ29k T:{xM!ϙj#91qsԨ Nﮞ+Gvҭ@{;ȚAPf]:x\n;-Xub$[MZ ~K샻/IΎcf~ϏA XV"\Ku(Y"/]e_gwO/J̍ 57i.l`|HIJd)" N>M^iܑ]|JGÇ\tٱmF{$'' &}IRRc[{өk7iw SgO6ɫ[6ߙdGNѷ٧/ B~ImܶzӤy,9^eyP~W޳gMZ{ܸ|bQASG]c]NRt(e*VB)Q"GlmQ Qv}^8Mb\fׯ0{Wj؜?sF&#cZTR㥛;Tz^ޜ>MZeGO|u^iAqs U\TTkqTdj6h8ʍ?FYI(Tx 7.crqaqٻ{{ fZ. &Zu^Mj5u=S)R_z(R4F.:.ĊS,RV]z8җϘ@Re zLjj*a %df9b~>z4W dn7 kO?L Z͠0x vng3\zQGӵKb_;G&1l4‹իww(&L)S02eضuVN yo z:b,]n>[45{BBBdyӏc dm,YDvkjFK\\n|4dߏNg){YuW?;w8>׮]J۳ɓ&2i\c:{a\uϦ-[Yt a{v1ZeD\ʖM ٹ*+3jpng קwnْzz*۶lfǶ? ߿=z^G&q-.]̶[sf:>>bS㥗Im47a"~ {wԩ|=[iN1BP.^C!2`ҥc!;S\9dh^Cc`p ӓ3fиq#fM e_x8z/M#tNf>6f!:bhJ*ذv-_`ŪUV]Oc28{?Oœٳع}bЏ:JJJ"5-r(3>c,`x88nƍ@] ٹM}'W=:l=͖Mٺyy}:6.1}ǯSs6ׯz%y{jU2b0FQ6yӾ,7 Yf H01!6xۇL&6.KeyP~k6G1ӥd9 r2d21sl,\ąٰn;ot|Cֵg2<[rk>ù]3i,[?nߞimx[NW_b4sd2 *1/ \>"ܲ,^t#X(r1W-Y BN8HJUs;MFiGضuitT"#.PxB2DӰϜNʼu2 gژ$'b6Z$="ht;q݁)Hb|cX~ IDAT)n{>NCᢤv\8~sM5, GnzGo01 foJ/S1z͂(UI4 ~"Miո{zIs_bq1{Kub-LF.:JTwH 'ZFD '>>AEv>N #[<T?  cOG(VZ ,l))tj TdI7|Žp0 '==1'UXq7j@Z5)^8uVZخm~?f"!!7)Ur+L%puu͒׬iS6i·E4aB79BV ,Ro>Y .Lp[F b2r|r JE>ɳ3꫸BWq-pHh({p>}zs)Ҟr7C.Spaʗ/ǃЯor9)W,gk4><{_/ERw5k[( J#M=JΝ帻UR>>\zlO6}qW([ ìi֭l۶yc5*/ OtQ E@d8VTE_d)0uVd~؇ܸtl7 >xZd2N#9!J/*T'He+T1+ŗ1_P$ޝm:mm~PUo8LeвR6F@џPYRd(e .l&4$-Q_r9'&ܴ{cl׮$moю7o:4iLv+j~0_1i~vٻu_!($]e^鱥prFWLDiĮ ?s^CZ:`RZ"m"Z9cc;beϝd2;!VΙMmX+%%_sz">>>$&&Y~~~6uޤݿfoL}{BrJ } B5MחcCNd2 usK&Rˬ<ȴ3Tܸyn=0jZmǐA(WJTV:_ 8|Ƣ~I&X6/pu_kCjZRţ<)F*lɤ2vfKNn.QQe `00iN=CȚ89)1 m>Uxy?6}}瞣@3HIIcl,o1-7guӇ,:Ʋ0L:tysͩ+[j͍{b0d|,[U}FXX#clmb^7ަ%Zo8*4Ť*2Tu|IΞK /P(ؿ?R1Gjp뼯늨vViORiw.~\e9nO:]m^==comvUZL.8*~,9GhWV})QRߑ7n0};mOsk   <%=\ǥn}b{Ǐ;햼=NT;v{7H3\Xe;@x:ݺ X{PZ$Oҙ8TǐW'Rik;H&-YB xc21W+i"o?I7ŕ价0l]9b ?Wٿ}l rXܺz7pux)mVX@-yq ܛOMfor{sj<9ٸcT: ٵgOoQX]d ɬKr6.|ymہo`9*U`8^OӹPZQGvը99Lxjeb/}i7_K,yrҺ,xs7++__L-Al|l.QQQ1_zzuRzu aYIL4ɱ+j5J?/ߝ3ϙSL}|9N8)D`"/晞=ش9P&XDR,OJ*{ʑ&V6/mNe%MB!3~lPRС8;;1vx~\]]ٺma8;;ٸ)ή>Uyxs?WR^rLƨ#5rwݹs;=ޟ?Ϯ]M9kA(JBCB9| 55##IMK YjT7_ EFFbGt:v̈́IQ:aaeQQ?.ےJ폭̘6ծͣ]5 q4Z5Xf͜;3fp9ޜ0?Rc6z=-Ǿw"vhH6| XKŰ0;463ҨQCXIF 1L^{ҥKh4\S( ڴn +6&::ZXڦu+vi Tgdd  ˢEcZggΞAƶt:#F"%%DBtT+W&7'h8hn^QQQ$\͛u䇑eh4ş;ʺUqҔ>r>lrU3YlٺXmX'YhkcҮ㛇[3"00 *E,XK/zSXJHAAF#_z=̧=m5nӉmy,^X]9٘=Jyȷ4~p0fVpL[4 sՍSssPyz[kD/vC_nؤ9 dUQn|Oc1x&њu^J2>kukW6mv;tynveEo>n|?zO,꫌9=z"bb[/@jۺ5ޞJ :_-ZGh7jIJ+XlP=u>} fD$Ǎcђ%TT TBLNLn݈pXˣmsքmތhՕ/vl@-пKHrKL!CzAiڷklj'ҭG~-[}͚_,V(4o֜DfF&!!JUyVپcI):*W+/D)RԯWĔP6i‚E8s  iz'\\Q֫ÎۇvK̜57BLvyHʕz<6b0ޞ6]SNGYαeiW#|K;gTI05##3UƎ?P&## 'n+Wv\  /SRth^b󰝗<M,ErCfyVXg'˴KAAJJ3s~~Pdb#VPlzWg0!8o}ɑcljQñӺ.nlKM PZ+.c F |}}rh4X;zR)RNV&_a4L۷إ+gN@^4_T6oӁ9ΦAzDբ~ѣF>rJ<‹=y2+UO?%}'(iL& ENNN}q<v^+kAA']؅'/2.oQJ 뀧%E <\uֳ?W. .9ӓҴIbA#H9bc mK!--  ݞ £_dRF#˗>} 7J :{^Ad11dO7'$) zMTTjN"`2H$x*Kq){4jؐF t0َE    )= \hL\wޒBܪTD_>)>0J$yr/H=v |p IDAT86l 4oք|S)V-[2{,d2gϜe|2Jƍ3iD<==ҥ3,fæ tkދS)"/X~6cMFt- )pZ/nYSnqya!0܆J:OC{'9wAKhR-^9*6w#N\< c'Zןx>?>ڶDLǙֻ>Mdܻ{G~牮ӟs> uz^>a<̌[932N]撛ߓr ZQNA_uAnN2]Z9aZ۷`4mU Du"^!9[3c95    #ݸ~PǴgk96eᄑ,Z~ҥKgyEm꼼 zsۻwN/w:˒߽)h/Xf4kטS.֠gTܱ    SOAۑ QleH]ϊ+i٢%o@?[(hԨG|2nnpڵkӯ_??PtTƢnR >k7ILK^=2RADބҌ<2Ӵyq%-)S PyvÝ;SOϾvwrR0AJزi 7ys%}te 7$;ޖ^ٍ]HN@=>u7N:6AAAAx?h4yd2 99r14ndmyfd2)ILaVVoLx /B^~t7~\.WXꊇJiiiIsVTҵKWVdGPP 4.Hn~.*wkYAAƏ Пc￟T\OL4-W.Z_9+r*` =̝ysXoOD:upww 77WZEdd wKR<=НA6h Lѓo H$MZF^ZgysPzzި9-z?c;eUPǢ'JxWvr9؝qOOswܸ'S?xJ܉;ܻ{W/7ժҪ[Dß9DAAAῆr )_(gfSn-4dd/ԒAf2^77!ÑWoMy(zs'9/5MLN9)|.s#*VjNJAAAAj"/%AO޽xqЇjЯ?/s'']|z-<=<]JWpu)b7HNNETRձZ o$K@0 97nq=K> BJ: $|) G.!A4r_ʑq`JHv"noBAAAA!_{s,.L&c;8?ժW;3fcQ&O?diIшWpyÐfls29} 99ťd9FǪ'Z5>Z±Ty̞3B]Xl@?p\6ű}&s1c_r+}֯g8~t[mNl(vE6m_SRSiۑX.ZKڵAAAJAqy2}+M) 5mAvmK` ֬ND^BM!iܽ~4Kt:>c/իWʤȤQAAAxZ]t'Nؕݾ}qq߸+JɎ;&Cڵ?po4xAg\=w\[I$SӂDLlO|T¯r:]!&)oğ|&4K!%}Y3ILJr:H="wE[Ӣv1;ѹk7ΛOQۛ j̙7N]ұs{ː+|t)mcbhҼ|4‰'Xbc;.}sMغmSNe7ڣ'iiyu:wF=XhzիW@ۘfff2lHo@Ν;@NN;w͛޻Hl{tރIEzMLN 9̬,޷~Iltѓ6X~'tA.]ݷGK]v::tLu{vyVml]kѭGO:wƀAcv- j 4kي:s1Ǝ`L2|@4ozF.ZLۗmwСSgK Ksox[ ǟ~ٳ 4]ѽ3]%hӯ?tһfs!t܅ΝYxIع +дI6m¯k~˾ٳw/{=a|AAAM8~$ej 6ѬIڴj{z-[{CVӢY3RRS xAgrڍdbR(1(w Rj$¬ _HT(W=j-]k箒#GqM ZCoڔT͘E ټi# 6 DFF0wϹp1uqTV &[seW;X|[Vk/Irr2Ʊ3gkصc;ݺvaڌ(,ԱcN7j`I/_͛69u K-i&ڱΖmmI[tr2);m㛯bߟRV*uq7S% *Zf+R<׭%nسGC?a$ztΎ[mT3F䷧ضy3>Zʯk~CrQ>#>mb5z YH$_'9%[6̙7Vm[k,h'O x 6oȈahZ-K-d2Ut \}ƍˁ}{˒? //T ٲu+5##ٹ}͛ jپc6m~=p-ʼndddmWf0U`2|(F MdJ濿x?\\yw,ؼi#Pg]٣[7rM/-}%#_/ͽW}) BPЩcG._ի n9    ԮM-j5~~AhHU}f\p¬㯙L&Μ=Kճ'M7 3rt:4ZWnÍ)z wU? }!~7hHXpjxή&qF…w[0@"`GR|yjCL vmdЯ/NNN 8cǎY{[amҸ1z#8ٹ}2W!JP*<;pdegAKvv6:]!]tfVs^|ùsII`^ѭRgz=cQ0~_emG> T*%ߟʕ+q^"Դ4\J= "E nnlݺԴ4*W_Ķi߮вE <=<8vkO/]|@:uHJNmZRMzu+%ޭЪE *Wd 4:T8@mE./-ÓS̹:JEzuݍ͛ <<9xpssca4nؐ5jR={tѣ$''ӿ9.Jk?x   M:=٪X"8xǎJhۦ 2Sazz:\i&T*s~{~YĻx0?¿ͱHF0LhM&&qUR3(4ys-yɸ<0y 5Q*(2B~F&٩idw6-y9{ H(ͲOVgiWm7u,^ցNmd2sX>#,,Ç1)))Tx{{jO*;2^RԺS8~8ϛPy)xnOD*)q_'E8 F222^G@Gj͚HW$ )))$چU/IY>ٳ`R %畗_~Fի3e$׍yU_bڷAAA_ R) V+UTKO͛ҹ>,wjjFFҶM9;ݫ"OU^0*k 0"hp%NJ$R-H33PxÈ>3IZ*\H%ɸ}р@VCaA..*$HxWt =D9*:*}S`aI-jժUøoO HLLc03o/^t|9rKG1j)))H$\2VGAAAGP(˳[SfgFV "((TJPPIII 1);3^R'>ށ`0 %79%z/`3H2;9{y!)T BT. ?5$\IFKHd8+W+/D)RԯW4F gtgggLX0],_gzJpǪ 4ʅ0L8K/!,YȱsքmތhՕ/\`zt3}H͈HƏǢ%KW_eQt Ӟ 6A4hZ*T(u3MeSnerbڷ`xCJJ%tDP` It֍"""8y=z"0 Դ47___khղ%>} )W{   B233UԪY&s_BFwQ?w! 1  EA AR) /ZP(G.&:kٹ1;Xd #wZM%hǎ]ȝ;7111g2U&{k8je4Wk㫍q>8ok: >W5MJC%yv@Ee9{-*]rujGS>DH*oqo%6)Ŋiqv+erP8)")F//VnG\ʔ#O'lm|2;6!6*j m^ f1ӓVֵkP$Ƀf^'<"7WWj5^"NFbcc1 8)zGEGc2GrrYpO?ߪrQY|@xxnn6>'%%<{*^OttOg;` 22Ugz0ߧJyɼd07}oJ?w|VY#iDVW_2d l9=*UR.ʖ˗DvI5qttdt%F[̄əjjqΝ;I}8 'w#HVG\KEƆ$[+*¬vͅ&26 .I"i66ْP>THXتTH{ʏuY|BC_Ѧ29S~UYQ2l2v<۷HXX-jݳWVghʹG'=?1c(Y+V§qNV:ݛ/QWAAA!g5 pVu߾YeYO^ʕ_7ܵ q#Goy&MBTڱ7)L`%˗bŊ1e4tM1adn7+Ç4n\d߷nIϘ2iMT*pvvfCcEЩc# ;?rL21~֜9xUVI@eBufъ'Mٶ}qj]cU`KعsUzңۻ_oAAAmOHH 99YlQj؇z=nDuٸyYL28zOߦ-G #'sGӇ py4<ʗKNfiQ -Z,3ǎ%W\h)#iBY :vѠ~}N8.ԥ+!I}yӦ' f`leJdE&.›=r2IxjTNՕɂ    eY/ra!GUJLL˯{qf:D 7iJ3_?v)nfէ/QQ2Vsk -0h^e;i3fP~7mիW;n7e+ܹK3z0ݯc({{{e򈈽v !#KDFFrutN3_?2omJҰfjdWy2%KU%>@N;L&>ϟ{'ܹsZ-׍P5jE 4p1 m     D|:6nD@.T*Uعk2 zÆQx1:wHrr2fdq>xͷ㉌`jš|X<(Ú\OnB=+V &qAA4jؐߏV͚|u/:oǏQÃׯ/_`;RbgUf.0mLN3ӦϠ?Ao<|76T)\B *ߊ?] 2,]֠j* kkk>zd/+VYPΌ3ɉCSZ     DÇxЁ7)1ad8aر`CTX{`ee'O}w?e`"E]\־@ڴnVLtt49rX\`!>U *+QP!z4V\/^hh׶-m@dTgϝu K۷o3rh.^LR㜇ܽw-\Gy9ɓtB@.9sgй aƌi'~}oĊHNN&11,mj4e*'Obił     w6?eӖɓKΝ\tIn޺uaf,* =G˕?R6z zYpus#66PV\źueJ&!wn9]^@@l݆h8wz>OXYYSx19m),\Ozچ;՗_zz2w,LF~xVDN9 [D RbM ߟ@qrr$&&MغlT3gɓ'a-ܽww/ؙ4M-ٵgU==)Rr?CnܸoE9j\\w[.Oc::8{7,!!۶ѹSoONcNr.g2O\AAM=3H5=[[[e,99۶о=KHQƍq*VȦ_ar>{]۷`\N,w46i򼛛+Æ fmfSz=-)Z(U==9x {ҡ};e,Cz3~D9Օ',Zm;= qޞŋQx1ȗ/ŋbݱs'wa#6ŋL2 У{7r:ug([\~3,X즂){F6@XX7nP&O,Z{慄W :p@_l9o1[.]~UP&+W9ɔi?$3Q9Lԝ;w{(=}۹kG3[6*t?n[l\,SM#99eplzw^Oe$'YO0c,$IRfKWBB":jܿ'ƞ> sj~\]^ůe+|[dA)CϠ7`9r.Сc'ʟ~9ZV9 IXb%0utVZ[ƷEK:uʟ׮ ZTVn޼I@.4iXb%΅LC; ҸISx{IK/ӮCui!@Ke*|}R~|5gO"YY[p"PN]Ͼt-iպ ;L`T͸o1 ڽiKKڝvPl߱Ԯ:rU9ߒKY{Ξ;S8r]0))Edd$/>͚3p`"""Xl۾-f9f^NX:_P0֍4nҔf~FSwF熒kDf~Jo9_+jbĨ9u(rg֜9YTõҭ;|h˼ }"'dV~ݸ V4kA=w]y&55;;ݞ]֭H ݯ03fb|ݻ ѶCn1lLJ}a`q :dAh_+̛N ߇cۻ_?veֹk4\'O¿ߛF>M٤/qqq3ghѬ~O~dp<׏~-4e* cڵg/Z#Gt dAAo{¥K0c޽ԮU;~ⓟ -[VlllxO8iq㭍 ` @n @~ P(|*@M.hn@`3NԷTO?f?9|Rߛ'''K/^ZmR|LR5Y>deJJJx!ĦYiIҬsҤtBBBҤTFMڵkid>]z5MڛNHQNJ9=B*UeNm.U)-[Btү7J>MI:Nڼ7)SgII|Tǻt]II̕:u*t:i RnJ)+J'~]jD>w#?>vt:oE$N'EDDHuKt:ݠt̛N'~]][~ߤYs[O)11Qع4oIIK_*uMޮfIMSN[XɧR\\)5k![A <|(t:uJqqqҳgϤ˖L&xRJNN|Tt+W$OҨ1c3ǝvIU,ݷOtұǥr>Z'$N'mڼE][JJJXriVn޼)}TtMi]RJ{H:N:zT֣k},BZz'~Z$t:),<\XI6wRݤ)!!Aɧɓ%qj֮-ݽ{Ot %$N'/dWҔi%N'm߱SU]Z~.t:铞=3gJ:N:u&vPԿ3kIj߱~ezD9-u2=?:g:k@>s͓Z~^^tRЁeToZsN'}W5k|;ܭ4slIgU&爃ӧH~oTHJsǎTR/_(r[6/@y2Ky!<<'oL?^ E ҧW/}ݺrjgϞųJ;-ӮM찶m֜~}>խSW&C{upz~)+-%,,QhQc*lllҹyt@tt4Zm2-| z?##[qrlo*K:TN]ΜMN/>̙`3Ȼ^=Po5 ڶm)e"8{[2"8w<ݻuЮQfs/^`r$ +R2>dN>͹ \ٲ4l@ƭ۷V-E )ϜG*/w Qc())5?ţ{}\N+;~)S%, .]In`GǔJcƛtz}7s8ɓu3^^>lTkOQpppٳar)Ì),ۀiٺ Q8pzn'444sՅWLr5LRQpaf9  7t:jHJJޞ~ưMUrz~Emm<,ZP^ҹ]:wR&+ԩS[* jPqqqm͉ǟ~w߾=t,OW̞;۶wo+yfEi(66F QQQfyqsVhOWqݝM2qU56X6vH+@yܘ}4~zktq66EFʍ䑑888(rswwpܞv iog_ዯ{i:u ãy攥Rk1qdϜ .™g GhѢTf K蘛X/_B6AիQn]0s(to06ڟfpՄx$I{A?qĬf2)ϊuvyP͋.1gǡoD*M@'OƳjJW77n߾Iy+I9ɡ.LՉ(Y6TX||5M"IPLz~)vv5c &xWOnIvٓCѩcmwpB<{Owޞ8EâFa L4kaR6o.c>ʗ7/ӧNɓ(fϜmʜ5c:FdǎDDFҥKgzzy8ة\]];{#G͍0V*7eT2*xx0?捿Vi֜KZlI剏g TTI+ʠcccCtt4v%4$ <5/8y2.>!L|J`]faA֯o舄DrygqZ̙7Wd߷6},Tǚ4i?L lmlڥ3M0nV,]js۝JJꛥP2ݶ~\\,:GcO*kkkԮCN)X.Rb#oNIOÆ t2mے?_~B_bάzul :wJrJ\zmZ+JJY(X :j\:Kժ|ի7'O3k*^_} u3qןP &[bi9} PZ5krm6 V$&&о=,aḺ=AV)U#"!җQez{刮#:l꫕(V&Ʀ":t*̂^L6Iwz/P\9e2\~RDDr٥;qll,I!TF#INN Z,LLL in (%$$-;<<ggg𔸺5`^F0r.e̤..f'N2}ȍ鉋C4]{uⰲJwWI@Ѥi&cnj!,Z@%XXXkzZ;:daYSm[F,],O*^OxDr[ӓuң뉎4&xVeTw4a)xU&Tǎ` ߟ4u 11yu:j:M=1`8DVX:/[Ƴgϙ6erKGz,]Rqflm RZzAtt42ކ'FԌi^dAAt:pd`9Qr7nޤvC$Ŕl|S_uy7}d7K yExAܹsgQ*uTkmmo666_M)do;ZNaWiԘHg~9|l';~eL1ͪ>ikkfpԐ GFq뭭32JE޽|h }\Vʕd3g8v˾_BѢEXZ<{0vh"W\lX3qv IlܰAAޞ]W+C(ߋ4 dJAAAAo!hq111DG9O=+     9N7ϝ7k Iin,V/2hrQڳĄ:(e 7;͚6U.;{nݦ/rٱu6-B}ooeIHHŋ:|D~߲e4Gc0ҍ+II>jQTr|Dn&/[/n     Ul xVOe`0ǟjE3o^5k':˖-gyfi95e4VYL3utjpJHHP"ۻoSM˗2e46mޢ̖m[m^T慧WuΛqw>SMc?snbOBعY9XœJUiБ7nq牓'SUUAc'ktBըXœ;qUع{7֯gʴi\~]^GAAAAw|BBdHHHH)^;ٳ;~&>>lsgvͣGֵ+..? ;?%K2h@(_-gOw #8zĘ5܏=.п__4V,\a#FonX7ߟjUxFWj䲳˯{j4`j.>}1l8xV    _'

^n=N^7+#=W\ŧYs&M WܵUyQ :Hjh|L<yϞܽ{O5]#pnʖ-\DM{Ɠ'Oŷ|C/QÆDDDS     cNlmmmޜ3gϲ?QQi'''ӧ_?y¯صgߌC8xa;~%/ūZ5Ŗ߶о| ?e…|ի7G$99ϞtrztFLl,GoV˾@|[[[aCh4 gx׫g?` !,Y`V^Mҥ-ƿ }pŒ>!ÇĚի`Q,_#GQ`ǾyKJH*U*ȨJ%yv{Ӯm[T*>Ӹ1ƍ8x~G*`ccÒE<|SOS@ *d      111=v zȝ;7-Zj oO$F+Wr2J|cFC4jԐ?+%G׷uąfeܵ ujLzpQR;7_Ozt'=§=zg)=ݻ@@DFFr:8^ǩV*ŋK,ٴWIET"㦧':&{.W.V-_&fCWܾ}RJq4領Z-#G!AZKʬ9sV*3[trF-&Nl`wH''~X"pF     w :]:wƲ?(^˗/bJݻ3wXFh+@P_y9gBIIwww#"2*T<Ƹ테={۶Mk/\H lll]6d``Μl۱ ȍZmܹh4lmmIHH9e;̚ ?q"Jb xU(ݷgΞGn5Jv->d)tԙ=qpp`Ԉtҙ-ǧ=/T#0!0xPشW89)JAAAAAxǩ ÖܵFCHJJBSpa.]̣G|rb]ϟgGbb"SM'_޼q~ba}UA^Wq y'r+|&\\\hÑG9zvvm\͢gm6ZȡL8A%J+a$&&2et9QÆ8v];͘12a$u1643g2qxƍFQf˶h=[~RJѸQ#{{Kܹ˺p! ˋN:9u\``,YH4    w.=[.\H?U+i䈔8;wښ|y-ZNC_7n0sj5PF ֬{%ry5?}Ӫunݾm/:u <<-mir/JNJ $$?]h4O(͛*UЁGpaBBBhߚs/o^|7&)m;vp)-JtL4+VbŪU8̚#Gtk4VlSMg lݶCcccCŸt2+Vbڌ?l2rϞ?g qꤜ&    w:Sֵ+fݸP`<)Bɒ%)ܽIhg^vÀ~1[qf [Út܅1lϘIϿ`ӯpqq 뇃4lPjڳ{{{Kʔ.4׬Qŋl߬S6^^^U2L%ɼ{SvxK&|;ZMΝqPLc? IDAT/ʆU*Uٱ8r(!,Z ǍɛI;o_9z^ѣ[72/R\r+W.erYz5S.AAAA?mwssco.3 *U*K[Er7nNƍ` ?~e_d@$}*C7    `0`mJyBףP!K36cI_5le{\ٲXT B(/ o!^.{AAAAH vvָ:ak:\^'))0tHYwKA! TSaWw^qzi^B(e{\~?;*Rf3#$ d5@RLd2YAAA 6.X{{;5 v.X[+ ٺ Bf2jx{Kc&EFb(+h43d~s(P2K>xK\TR8C&l®AM)! å [wJ+a?v?UTA$NϪSyeVBoMwAEcd4d5_Rt'ꖫĽCRQRiH6#   Vj\P2R цߩnNAy$ !9*! +++m0ع 6u ׌{FKDbT".]ڣٲ"Պ$,-'NiJ)oVObT"$x-QϮ<å vvd TJ{C#A8+ !"*]Pj!B OXqr}~C>|YΧ^xu?^z$F$rf~ V'Ք!   V 64lmhޟ'ߩxA$IT*T\OIx1`T*T_K)$[g&M9sѸFdnj^$ά8ÙgxsQgjI޵PTkۮtrSvٍ"^+ɎOW>w\C ǩGǫBmW57jq] >fۑqqGRؾ"ͦ7 G |PZ/iS!'b^İ{n.IG*h1#D>$oټߒb)?痟SrAy1ul1"h2 F5 W+; 6[{d9MWyz)ӝRܘܥ2/fT\3   _bv&#\qAz17orZ@ If%ܵS,4jPVPKZ`V7ɕ:!x'F{n h\"`M=w$><vc6vHa>ǂ'>c;adle*r=jeVV=r7Gj~]Sn|W>%K+   _tq );CL>],1}:2oʪ7or-ZW.zHIxp>۶nL̊+: A~r>Vmo/{|.{ ל^t|t^ކLl'OgRnOQw#CmV*wfUҪ];j!Ч:ۦAAAAg аA]N5j$R|l۶ }a!mڴW^!44 ǟ|Ž Q~~Osir9CeIݻo?|>> 8f2)))? @lX5yjt?qO W{eSO;yy'ؼy3@.xyel.^=ݻQ(?}˗QTtؑ3f5ZM:0!%%ZAhа!N__`ף=Z"],k'} WO?ڷ`0Tǟ@T0`>CTj !5%^Z-ͣ}\\ߓRdXsh܅ gҞ Î%/@QhQ5hFRc"[D=ɻL'cP+0,7 w'WUEȧG1QҖ o>:ce|}ݎ_6 ]\nػ!,8gVxg=Cލ!)^ih0 `[z^XVo~N}=A:ٷ>|vN9CTd$Q@ֈ1|٧]/зOrw\x0nX4 /X?Ĉa#X`!%%%   ?aׯcNgΝ;y:֮][.mLlپ // b[>=6n ##BpzoР!(44 \_MJ;vЁWf`0hg Xf 'OCl6MFN޽{eǎ̞]1~8}75bc[2axBCBye,n\OBB5]0бCgؽW aԨQnەddb)ٽg-ZGl13cLlƔ)S\ԩd2 3gϏ-[ʊo%$$;s *':]T&o]? נ2 ʇ"Č )fGj:+J[qr/K{08auz[wo>swղ՘oư5, j}3 `Pz)2 [ Wo7ܗ&kn/~}Wά>W=/CDsgdϠã)gR*u\7'?֗rrI~wdr{g[ o_z =99 @D~8T)/   Tr~rJvrུ7cc;wdǮ(r:w\w*dc6[RYHH0Y 6sڵ;vh2x/ E+R~=i>-͙G9:fHg+==TW2y###ILL$#ٛ *srwRh8sR/x57;S.2S^șG*sdeeuf̳L{Y6[Gx{//tiQ:""\י~ ߟlBBBg[}OK4^tW ɗ((6IAAj < 7R+ }}TA9hqjȸȳX;6رҲB`[d%9%ݼ}2a K/QUw7r/ĂWMݶ{(JҖSz3|8?ЄjGt3E_øl4|>a>R5ې.DIn Po9'=^Aqf1!$wܿ~B8ZhA.uSxpCP ln=m_d)*F㧑r   s |D3vQ223PL2LeƱӉ 44|vl^ao ?{6v::@==VHy9׹9RYNs><,0i}Zv :7<+rO{趵 AAA] |F3l{V$']"w%v^ǟHIIa /Ob2X嗌=4ZjENݶUTgdŋLuU:uDPP6n9yvl'22v1߸M|2={R:3~<~1aw}ߞ|||jdfd+P]fM._ÇxBTR c:}7˗P(߯-߮[}OYY s=72+24 8ei84ؘh֌&F$fA`Mf/ZoQ@&rT.wAAAAV2e 34]6~7==(YY޻3kL4  #G8y2jq׷oGaSvmweoC!hެfˋ֭Z3o\>FRO̜1sWƠoY*իٳ=мY3N9Cqq1>>>,yc1/\sӝyBCC?w*V^?E`֫bوEhԨQߎ[}Og@ln ]tf2%2Š F֛ڲ zcP;ZFI v^uA2;R]&wAAAA _{&Mx)+Y^\6sY^fZ:p͗N5Uj|TהvM4)sJ<Bʥ>1 T.orRRR\ҿxf_FrWd,^WgbR`l67, QVoGeӄd֭h4 )͛5,?q~7:{{wװK7ɀ?B 'bv(l0/"#?H`V O8}pDw#s ˺d1!   p8x85kzBKcFJNNrmm]?/^KZ\Sk \S+`w-Ny{ey.4wJEppp;,bۻ;@``_گ'BAHHpwAΪf9Vw\ZUpVoGEѣǸr*cǎ6| 00b(cҧa7d/jŮVbsذY,x{U)bpPb2a D K2q#?/^,0L< vsU9Y| ׮]̙39L+8rgP ׯ_Kͽq'HKOKcǤ1@*c6;-ZAAA_;o^=s]E^^^̚9sYڶmYt1zɃJ/8vf3vLbZj'n'RE6ơPdOlbaAZ29v8\r:iVڣ$vrxƍ$|| &;;5?k.c[m}8֭˥˗ Rm_ӹS'Uwh8xMxѰh?@xxm߫%%%\r]{Frr ~-Bߚ<cqo\qЩ l۾ӧOK4i˜Q YVvcG),,pѣWmAAAkDGERupG޽;]s>Y$XV, rM.j`Z(.1P[ 2~Kddaڰ L9(U Wcw88j6n%$m۴,۸q111\<QnÁ.9QF0nX*%)YGZ_@?jBUtu:vhae22 ]vn!=gF oII:ڿM=%50DEFߐx-/<0QQU_#:\ IDATtӻ7f(._¶myg?w;vlөSGCQQoe߾ ֵ Æ@q߽+nwЩcBZZ:˾9_+9vsY}, 9$$&;vѣ6|}}d1L,\}aX,f&ODvN6{jР~}f%'ga0ze&iiv-=kۖAmW*YL>+Le2̤VL ;vpIdeDEE9Փ6/r6>İC*msіetԪ~_unj ɦM1LJ J1g߸ϟI'db\x \N:u?Tbvod2222HKK7˗ӹSG,iZƏ˫pYűeVd2O<\BAnݸp&T   w<~^CQYa>>>x{{WXA\zD)oYz5z 44=a*|÷2͛Mğ;Gl˖4ot7xA1lV5jJIB\.l6aŁS\\LFn6J(K? ??lvsjS/^z< y}"7k*  ,|c1}AVv:!^z6lH~~>O<aaa|t)+Ǎ򘞾ԩSO<>>X,}̘ATTEE\]Nvvٯ"+;,}\J6oJfM׷/̙7Xj֨AfV999`Ya/%!!ܼ<|1Yltoim3O?MZZyy̟;۶4p aƍ8pWj֬AfM `!z>#&MO֭)))a%۲۶f jתV^*9%ooشy3Wxjٻo/g̙g`2xkJ%k~^+6Svm)IDFHOOw+OHHQU[k;6HnH⃏>dтۿYd?psA=XDD3~vI!u2]{ЫgrC]FJJ*MVT* 'yOz0>[Tn$`OI^3ԻXK&&& g/O?N:kK#/?uчh4fCP`Q*DGEBHHFҾ}k7t:ƍKxs>>]Eڵ=z=iie26@I:5k֠{nLΝ^^(U<%t t;!խK·64_7n `ٹkr͠L.gĉ$%%Q+&}y_ji^b[`XVtIӧ7{c gڜ[eYvT Y RVjխK^^i:}^=zxVs#˱fE.w"/?jE&dh¦pfjaXp8l,6 |UPjԘ-f+E%Bt% ,ZmV<ݎի떫:0 \-Kh lJXX~~deeŭSSGbMTT$/\fnˢ"bc[KNbfsR 7ʕl(6d$PTTDӦ7LKK{nO }ދn1l[d2>ciﴬ"Ɵcޜ۷{w+h4|橸4֩CQQ1j}\|.;c4UOAA_AA?[G^p"*+oؠߙ%!!ʿ;J) L&-2, inAfCfs Wȝ=-Ws]Rb1[Qef|  93d"bdr m6A޼J %JFLF!ID>j`0Xb>]zkT<9%֋ 9IIk7LJJ [U~gh4 FѭWbAKferi԰;9xJJ1&Z/8%&$JA$z%7jĄJғNRyr2RisVWϞٓ .ŗ_2vmF#q2^8@Νh48qۘeq8ڽuM^^&r_@NN.ڷl6W   w,m!Ctv6ԯOIJJb˶m<ÞU$>j5dpw8 ;-? Bdd2J9\f~e2r9F.R)z|# \u(;Gi ]xx8+vLOk֠j9biiiԌq.qcbjRXTf'88\=VϜ=ÀkN.))Ixv;ÕB5(..&//O L< oooj5YYW/(`0/sv"##>>ߵZ-.gI7bQׯ 0ЙnfMt؁HilW쌌 SRݳw;wBPKeמn}֮]vr5!Y3gHe%%%S uۈx:pՋf&- ÆljH_zZ:!!{C˗/K׼g^B]J%qOPڳ>=(Jz}FAA~91c9r r.WUdω']X##"lp&$Цukv;N:VS. JtT4^LvCsna28rt/о3bcٸi3[mgrF#{0}:AAAƶd?nSg2|||UOAAdb݆ 9R;r\oi_L֮_>n6߲z?&LhT(дfd/,Kw;*/Pvpjrrv"#|=X),1`ڰ;@8sT\/U|gμy57v,(J\qܹ3aatЁKϗ֭ZӺU+?@&MINIfvRгsNq9Dƍ@Ѡh:do!!гg)7x@BCC  &8(h84|||X[L{f*ԫWOZKNRWy̲:wȑ#GXp2ƍZv ֈO>%"<B$%5oo|}}Y[M&B>tGuJԣ~GgP:${ú MAAWVEB,D3lPBYj'Nt~|)))JSR m@RSÏxg4p K-cr4 O>x^ԪUiijBCCd3>PݫbcIIIAPH9qKsWU']onʓ7nuM4+V{iذkjrj/\D@?99׏0BBB8KJ8aYӺU+|}}>/pӦ>-VduV2c)vE륥`~zL}IԩsAAA;ş;^gڵRYl˖T ˻tG|/ZfwQRRfK f4m҄/ ?? vu^J"(0¢"  l j___T*w4o,/]]+\SpMU5_:ո5e?=~<Ap͚yG8I&7|uy>PJ cP)ex{imf)Plj`٬JRQ@L05NX-dBT:%VEz,8y8r6mn p80h4^傱V͆#'ՙƧ@Vu̲FƫXTJ}^&)=zb1#CVe{[6;jW훞ʩ?`hQkV5}8v+R3L(U*s6-dTU=OU­d4TʝOKfjT3Rvv \iwOv łZ|KJU   Bu9lVko{ZLFV+3~"P*h41j۪|+?/^KZ\Sk \S+`w-Ny{ygaey. ܽ<Il?g{ڪ9.   P]w|޳AAAAA{,JRC<أL9UR\\ 4qBW=7nNjj'N`!lظN;Fo;q/]+ #^^7S׮]CV1v     xkl, WVWno]ŕ{*7mcny,XH]Vbb"d zi[jI[%$&yW]@Bbgx􅅞_l۱]\}DNIHHp[kzGNh.`0Pwu/    B`l6{Wd2a0<9~w6hPn2&NdkߣGv:*Bm?p)bɼsh԰g7͚6eqnϝ!Ǹ dc/ƍ$^9/Ĺ3ٳk'| :uȀ#    BK ._&n{0adyo9}`)8y= H0i2?Ev iÏ=&{YӗÊIuϝcĉ 4!ÆaFi'[C0h0Ӟ{qN9cz==soKdС:g vi0BNk2jX{ONJ~~~fs1``ヒj ??ǟ|{3fxَn` 9v:2g|N8#ع Kz[{ J>}7`Ԗ_~֯Ï>f[o!CY;ZkGff&&MӬ['~ZzcL<$TZZONJ?p-X`vvy=zˈѣٵ{} ##s8ʌyn4b L }F&J~xgUAAAAAnA(..&99RI^^ɨjlR'Oh/ӡ}{ƍCVrQv͆uؽc;OkGQaO Vf3;vyfݽ7/wd2QTTO=͓?[f[osEi_e}7ğ;5kؾu jbkA5SO&Mib(**懟~d+ШQC֭_/s4???Yq߭Xξ=1|g `̙İ}gƳjj>S 9{wbŷCj֬ѣܜ˚Vq:֮[?ĺYbf-^ٳٻ{.ΙK~~>=M4g1嗐$&&ž %##ȰC((Ok(.. T4tN^9s66mX+WXWn/{6oʏW8pIIINT8"b[䑇m6m6nAAAAA5/]۶4l؀>{3rp|/F/W\3㏩_._RRR€?wg ѯo_:ob!++#GGmЭkv pÇbF3lP/¯8AAAtÇd2r[Mx{{3nXo(=غ}GahZT*#= zv1GX 8;wp\.GbaҶkb eCBVSF BCCп?JuOFf&j{2kP((J)Y2رc A.P(x%/3o>}sNn'rz٣!(J&NJ2fƌ7~˺r{VL[P^AAAAQz9z;wͯg^9y<3(*j׮y9͛5c믱|Jf2qq 4nȳj9~~|6L223ejCZz*#FΝ9MfV_,eR4jؐ|8Jr|.]PT:|:됕ŽSn4D^^\*+3 ˭nRVy~eKP/+mvl63_s4oFfn+?n2]V͚5ճ'?#\ @VCCC r횗GF嘘i*l޲\~"=jWnc3z Ղ    b}4* IDAT TWTEP(*L R*55SNӤqS8y7&##C |+Οϻo%^3z=._O?={enDݶLhh(5j`vvح,$$iSҿ_?[ ,;;`p.%=j[nN 6L zi-^t|`|Y~IٓعkGgxyyaZٲug57e{Ȟ{=KaN>ϊőٳ;~>c6=0jH} :Z ۧ7>sN/yn.&,,aCט‹n՚?E0t\_zm۷STTDÆ x<4lPv!厗nuq}ܙVf1bhd2oDzS"1>ŋ     EEee |T(\Skv͗N5yMoG!]SڳOg]̝*&:WQ*RDpp[ NNn.AA)șVV]Laa!Lүzࡇ5r[зg>,ZvVnV׹DZ */KOUz=^^^q*b' BNAğ>RMw+or*&Mı 4o̳?rI6iYd2ɡZAAAI&T(!$8ʘ/ҥScvM-ҩ5olK׼wQU)t t:ATtQ`U&.*۵kǮk];EPtP.@BoB$$L{Ɠ{$ ={ܹp3Rۣmr_>ߧf͞|gk _ ZjO>7oɓѱxi<ǿۭWsb7n!Q\O SGILdNDDDDDDD GDtGAqq:Z2ZDKΨKn "\Kdff"&jk1ADDDDDDDDN<<ԩSG4|<˷Q#:Z-HjZbODt9N.;CDDDDDDDp8N\-1'"""""""""Kɤ^}1'"RG^pǀ1s:/DD"{V-ۍ;wutiVzxqv9UU-]^?§|{0d`DDD/s8y4l6 ФaCq3镲nvh5!R,[& !!&g/ZXk2BX mZ`Ҍedw9eCcyZoj f,OMEr@?|9+8uvh huc*jؘDEEe""oK,7|޽{aSqQ(ʚh17hAwGq1#X;QEy4v#ԡ.K- xtso`ܨоj B\l,c;H?pуV|ߟRSI8t(7h=?;PVx""""""Uw/ G*l61GUb1{\ 2ܗ[h׮]bؿzU۶o>nwq~-ѫg/[!!![J>{7vfߕP wwrq q7!*"{Go{tTt`4ڰ5o/7`GZ<ЯH][}Pgjb-}eN?t 02> ǃ:j_{`Mp\X,>3ڦ=ϝ{`Z6o琒ȷ۱8 Lh޴ _w,+۴ڵC?矑}I?eV!!۞؝{n 'N+Ppl2ΝЮeKi&%+׭GѺUd>lR eЎ#ƭV+ڷjM۷ѣl2KXiayNlܶ n4_yy:{:m֮݇hӰnfxs+UC5QRR' dn_O0;ooP}9+pt8pGrf:e@vv6ej#;;O=/4nO<87xyx<bcZ]?,]u7ԙ3x< EXy3N<уcላ?Z8;#uj¸Q#q{Xz5N'v_݂Çaz8ysJ6mZㅗ^Fqq1 S&O;yK-AB|V;#L6YqhbBCoiӰjjDGGCrricݨY&^x9m{1?Gy?Νqm׮`!]zx#viÆ@=y4zQeVuj7_}ڜE_ZnϒbFjHi fmi?BxxyWD#8z82Oa2O_Tl6-@ctn,'Q71VnG^X'+;3٤w t[mH[ rGl2{. QKDDDDDDT1 %vǎjٳgaap2k?oڼ:Sٳ ET垻qqXn/]yg#<< -Œ3ѦukE'Ncx=k=s7JJJ؇pD&}7RZ@JJ R;wF4l䣏Og~V߹pMpM+_pGިX v\.EQ{Xobxa(RgՖ͊ԾxtzFl!6\.B$EE/3_.BBBp ڻv@Lx`PeO[k2ϧQ\\ Q>/r8{kt""<t:`$ácbv㷽Rt3t!Xj  I3fto-QT%h.N;ydܩ{`;K7ddd'|fگANNVo_s4] Чwo@[⥤YYYX~=jDGSǫHt:,^pM.$Mµ]:o$tj@qdi߮];,_Gbb":=l6CCD(O%&&?g}<^|y$$ėK.EoF\ ݺ!F lڴ & { 0Xv-żby4jP}y9[^IBjx@RS#t\X,ˍӧOӸQ7jبL_Ŋho#=w@F a2ՒW޽j7ӧOT 5kDP[zgϖYaCȟCQ0k,<}_N?7| #O}I=߆eVc񊕥KsH]wZ!S.q2|ѣHDUMXf&>2ёQξ})ƍѱM|1e*bjD|^>th[Hiܰ^'Oξ}*|fذu+7i2V+:kfl9regS)w8~0^z />[?:ۍMOzIo?qd|3e*.Z}=]vލ֭ZJufŸ`5 oP Պɓ&Y2' -s\p8(/N%ND.n"R"JzeG{l2Yy:E^P M&+ޫw pYG[;#k6nĘ!CԋXVX'Bج! ywy GӦe }DŽ7S`9SYYYxd2ƀdF^p2|O<㑓;"990cL0vSO>v㉧ƨ#D[vؐ!x11L9[fM}]zBV .lھ6mFlL Fswok,BǸnٷwvkJ@E$mr_,;cXKK?2nj>m"jyv#;'q5kVIӜG <[rs,`6vrۍܼ<Ĩ/˅\Z6R\\"DFFb`ƬY={LNKإ4DDDDDDDDTrZ}g ٌ*.](PٌZ j9`VR|f\;￟''l6 QERȿ%jtx|]&^DFFƎmW] 셅4X~ga\uu 0|5p 8P-]""(*ʈ ӥiZ:DDV|\<=HǑ\Y|s.DDTY6 7͛Ѱ~DEE"OOvhܠBBBidr?2C/b G&͐'OtSH&`XM+òWS`]lԭSG-Q͍+MWZ> sy ]Y*%WWvDDDDDDDDDDtY 6_fn\%hX1hQjؘDEEe""""""""""c,yr@Oh^@;_}Tf<0f4gؿz]tgkڵn#,, >h;@ΝҢt"""""""""ʓ@B,AȉQoh_RR'C t0%%%P9s ʵ׬]|evpzw!-^D-yegg#mOZCW^f':tԮע-_`2`X0c֬r蒦̨. dNPLjAE$mr߬Ӷhmjmֶimi[6C֮t>DDDDDDDDDDt5ph[.ֶmsi}vim{A܆ UG'{szr-SR~IDDDDDDDDDDW={DS͐RMWXU-AMRo E_WHUNDo""""""""""+CdlsMwc#r_|U>FMwʢ!e͂Q&sYo.NTz$ysK{"""""""""""(ٱ+˛ z5טFԓлM@"""""""""""(ٱM۫m~T$.Bo/J.}O""""""""""ٱ|Yjۗ@yU$7btxjDDDDDDDDDDD.%CNWMjWJUF'^zFKB썎ODDDDDDDDDDW!ɯ۫A^zE=DDDDDDDDDDDPcegԼY/g} 6;7:u5l<'DDDDDDDDDDDPz7ʗdjPGѫ 6E mqQDDDDDDDDDDD0ȐlYi5U*xջ usy㗰 vySe5˩e+2zz9mqC_pq """"""""""R- k'9~U&ף^{t>' HPc5t˞2j/Ux蜠W/HM)lNz,J'(*^ڗoSotc9W3gdҢGoL{W| 7q b\B x""""""""""~2e JVnbXB<L^Q/Hlr8,8^ڞ=eFDDDDDDDDDDW"Cɜ6#hQPnRrMl"8moyoV!R[{miaRC=_ySK^-3]f`ZՐ[=Rh.j[ݫb}xt\+O]|}rMU^'=JЬesD[tI/6t57K)Bx}_]j 5Y0[r.?.Bwyr%|F1G ʼnBE..mY&i]'^x^ xq|/jz8ѥ_Ьfr]j_n\=:vy9xWEثuіUXUz/_g/SR |OPޭ<ĺ ec}ʫOC7&uFDDDDDDDDDt ՚\7HzE[EX.oS3b\1]Jv,PY j?h =:Q-_I`r^vwk{)xSAM&DDDDDDDDDD96j~}@]r0{ztՈ:V@ltBn.ڤ\K+σ6G|]X5|7E=^&ODDDDDDDDDTݩ9b{nն{0^c{礷Ȝ2*wMmVi =R,:q4.`uVo›tBwqD.՚ ѫUgzZ3 E+6&^QW_1yN uU&Wy[1V\{\E[c#o5zmjDDDDDDDDDD #include using namespace Quotient; AccountSelector::AccountSelector(AccountRegistry* registry, QWidget* parent) : QComboBox(parent) { Q_ASSERT(registry != nullptr); connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, [this] { emit currentAccountChanged(currentAccount()); }); for (auto* acc: registry->accounts()) addItem(acc->userId(), QVariant::fromValue(acc)); connect(registry, &AccountRegistry::rowsInserted, this, [this, registry](const QModelIndex&, int first, int last) { const auto& accounts = registry->accounts(); for (int i = first; i < last; i++) { auto acc = accounts[i]; if (const auto idx = indexOfAccount(acc); idx == -1) addItem(acc->userId(), QVariant::fromValue(acc)); else qCWarning(ACCOUNTSELECTOR) << "Refusing to add the same account twice"; } }); connect(registry, &AccountRegistry::rowsAboutToBeRemoved, this, [this, registry](const QModelIndex&, int first, int last) { const auto& accounts = registry->accounts(); for (int i = first; i < last; i++) { auto acc = accounts[i]; if (const auto idx = indexOfAccount(acc); idx != -1) removeItem(idx); else qCWarning(ACCOUNTSELECTOR) << "Account to drop not found, ignoring"; } }); } void AccountSelector::setAccount(Connection *newAccount) { if (!newAccount) { setCurrentIndex(-1); return; } if (auto i = indexOfAccount(newAccount); i != -1) { setCurrentIndex(i); return; } Q_ASSERT(false); qCWarning(ACCOUNTSELECTOR) << "Account for" << newAccount->userId() + '/' + newAccount->deviceId() << "wasn't found in the full list of accounts"; } Connection* AccountSelector::currentAccount() const { return currentData().value(); } int AccountSelector::indexOfAccount(Connection* a) const { for (int i = 0; i < count(); ++i) if (itemData(i).value() == a) return i; return -1; } Quaternion-0.0.97.1/client/accountselector.h000066400000000000000000000010111476730121700207200ustar00rootroot00000000000000#pragma once #include namespace Quotient { class AccountRegistry; class Connection; } class AccountSelector : public QComboBox { Q_OBJECT public: AccountSelector(Quotient::AccountRegistry *registry, QWidget* parent = nullptr); void setAccount(Quotient::Connection* newAccount); Quotient::Connection* currentAccount() const; int indexOfAccount(Quotient::Connection* a) const; signals: void currentAccountChanged(Quotient::Connection* newAccount); }; Quaternion-0.0.97.1/client/activitydetector.cpp000066400000000000000000000026061476730121700214570ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2016 Malte Brandy * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #include "activitydetector.h" #include "logging_categories.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); qCDebug(MAIN) << "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.97.1/client/activitydetector.h000066400000000000000000000014131476730121700211170ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2016 Malte Brandy * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #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.97.1/client/chatedit.cpp000066400000000000000000000272661476730121700176670ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2017 Kitsune Ral * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #include "chatedit.h" #include "chatroomwidget.h" #include "htmlfilter.h" #include "timelinewidget.h" #include "logging_categories.h" #include #if QT_VERSION_MAJOR < 6 #include #else #include #endif #include #include #include #include #include static const QKeySequence ResetFormatShortcut("Ctrl+M"); static const QKeySequence AlternatePasteShortcut("Ctrl+Shift+V"); ChatEdit::ChatEdit(ChatRoomWidget* c) : KChatEdit(c), chatRoomWidget(c), matchesListPosition(0) , m_pastePlaintext(pastePlaintextByDefault()) { auto* sh = new QShortcut(this); sh->setKey(ResetFormatShortcut); connect(sh, &QShortcut::activated, this, &KChatEdit::resetCurrentFormat); sh = new QShortcut(this); sh->setKey(AlternatePasteShortcut); connect(sh, &QShortcut::activated, this, &ChatEdit::alternatePaste); } 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); action = new QAction(QIcon::fromTheme("edit-paste"), pastePlaintextByDefault() ? tr("Paste as rich text") : tr("Paste as plain text"), this); action->setShortcut(AlternatePasteShortcut); connect(action, &QAction::triggered, this, &ChatEdit::alternatePaste); bool insert = false; for (QAction* a: menu->actions()) { if (insert) { menu->insertAction(a, action); break; } if (a->objectName() == QStringLiteral("edit-paste")) insert = true; } menu->setAttribute(Qt::WA_DeleteOnClose); menu->popup(event->globalPos()); } void ChatEdit::insertFromMimeData(const QMimeData* source) { acceptMimeData(source); } void ChatEdit::switchContext(QObject* contextKey) { cancelCompletion(); KChatEdit::switchContext(contextKey); } bool ChatEdit::canInsertFromMimeData(const QMimeData* source) const { if (!source) return false; // When not in a room, only allow dropping plain text (for commands) if (!chatRoomWidget->currentRoom()) return source->hasText(); return source->hasImage() || KChatEdit::canInsertFromMimeData(source); } void ChatEdit::dragEnterEvent(QDragEnterEvent* event) { KChatEdit::dragEnterEvent(event); if (event->source() != this) chatRoomWidget->checkDndEvent(event); } void ChatEdit::alternatePaste() { m_pastePlaintext = !pastePlaintextByDefault(); paste(); m_pastePlaintext = pastePlaintextByDefault(); } bool ChatEdit::acceptMimeData(const QMimeData* source) { if (!source) { qCWarning(MSGINPUT) << "Nothing to insert from the drop event"; return true; // Treat it as nothing to do, not an error } if (source->hasImage()) chatRoomWidget->attachImage(source->imageData().value(), source->urls()); else if (source->hasHtml()) { if (m_pastePlaintext) { QTextDocument document; document.setHtml(source->html()); insertPlainText(document.toPlainText()); } else { // Before insertion, remove formatting unsupported in Matrix const auto cleanHtml = chatRoomWidget->matrixHtmlFromMime(source); if (cleanHtml.isEmpty()) return false; insertHtml(cleanHtml); } ensureCursorVisible(); } else if (source->hasUrls()) { const auto& urls = source->urls(); // Only the first local url is processed for now if (const auto urlIt = std::ranges::find_if(urls, &QUrl::isLocalFile); urlIt != urls.cend()) chatRoomWidget->dropFile(urlIt->toLocalFile()); else KChatEdit::insertFromMimeData(source); } else KChatEdit::insertFromMimeData(source); return true; } 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 QStringView ColonSpace = u": "; auto lookBehindCursor = completionCursor; if (lookBehindCursor.atStart()) punct = ColonSpace.toString(); // Salutation else { for (auto i = 1; i <= ColonSpace.size(); ++i) { lookBehindCursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); if (lookBehindCursor.selectedText().startsWith(ColonSpace.left(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.toString(); 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); chatRoomWidget->showCompletions(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(); } bool ChatEdit::pastePlaintextByDefault() { return Quotient::Settings().get("UI/paste_plaintext_by_default", true); } Quaternion-0.0.97.1/client/chatedit.h000066400000000000000000000042771476730121700173310ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2017 Kitsune Ral * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #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); bool acceptMimeData(const QMimeData* source); // NB: the following virtual functions are protected in QTextEdit but // ChatRoomWidget delegates to them bool canInsertFromMimeData(const QMimeData* source) const override; public slots: void switchContext(QObject* contextKey) override; void alternatePaste(); signals: void cancelledCompletion(); private: ChatRoomWidget* chatRoomWidget; QTextCursor completionCursor; /// Text/href pairs for completion completions_t completionMatches; int matchesListPosition; bool pickingMentions = false; bool m_pastePlaintext; /// \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; void insertFromMimeData(const QMimeData* source) override; void dragEnterEvent(QDragEnterEvent* event) override; static bool pastePlaintextByDefault(); }; Quaternion-0.0.97.1/client/chatroomwidget.cpp000066400000000000000000000727171476730121700211230ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2015 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #include "chatroomwidget.h" #include #include #include #include #include #include #include // for last-minute message fixups before sending #include // to produce plain text from /html #include #if QT_VERSION_MAJOR < 6 #include #else #include #endif #include #include #include #include #include #include #include #include #include #include #include "mainwindow.h" #include "timelinewidget.h" #include "quaternionroom.h" #include "chatedit.h" #include "htmlfilter.h" #include "logging_categories.h" using namespace Qt::StringLiterals; static auto DefaultPlaceholderText() { return ChatRoomWidget::tr( "Choose a room to send messages or enter a command..."); } static auto AttachedPlaceholderText() { return ChatRoomWidget::tr( "Add a message to the file or just push Enter"); } 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") { m_timelineWidget->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) { auto statusMessage = tr("Attaching cancelled"); if (checked) { if (const auto filePath = QFileDialog::getOpenFileName(this, tr("Attach file")); !filePath.isEmpty() && (statusMessage = attachFile(filePath)).isEmpty()) return; } cancelAttaching(); m_chatEdit->setPlaceholderText(DefaultPlaceholderText()); mainWindow()->showStatusMessage(statusMessage , 3000); }); attachButton->setDefaultAction(m_attachAction); 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, [this] { QApplication::clipboard()->setText( m_chatEdit->textCursor().hasSelection() ? m_chatEdit->textCursor().selectedText() : m_timelineWidget->selectedText()); }); // When completion is cancelled, revert to showing 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); } setAcceptDrops(true); // see dragEnteredEvent(), dropEvent() auto* layout = new QVBoxLayout(); layout->addWidget(m_timelineWidget); 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(); } Quotient::Connection* ChatRoomWidget::currentConnection() const { return currentRoom()->connection(); } void ChatRoomWidget::setRoom(QuaternionRoom* newRoom) { if (currentRoom() == newRoom) { focusInput(); return; } if (currentRoom()) { currentConnection()->disconnect(this); currentRoom()->disconnect(this); } cancelAttaching(); 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] { qCWarning(MSGINPUT) << "Logged out, escaping the room"; setRoom(nullptr); }); } typingChanged(); encryptionChanged(); } void ChatRoomWidget::typingChanged() { if (!currentRoom() || currentRoom()->membersTyping().isEmpty()) { setHudHtml({}); return; } const auto& membersTyping = currentRoom()->membersTyping(); const auto endIt = membersTyping.size() > MaxNamesToShow ? membersTyping.cbegin() + SampleSizeForHud : membersTyping.cend(); QStringList typingNames; typingNames.reserve(MaxNamesToShow); std::transform(membersTyping.cbegin(), endIt, std::back_inserter(typingNames), std::mem_fn(&Quotient::RoomMember::disambiguatedName)); if (membersTyping.size() > MaxNamesToShow) { typingNames.push_back( //: The number of users in the typing or completion list tr("%L1 more").arg(membersTyping.size() - SampleSizeForHud)); } setHudHtml(tr("Currently typing:"), typingNames); } void ChatRoomWidget::encryptionChanged() { m_chatEdit->setPlaceholderText( currentRoom() ? tr("Send a message (over %1) or enter a command...", "%1 is the protocol used by the server (usually HTTPS)") .arg(currentConnection()->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::showStatusMessage(const QString& message, int timeout) const { mainWindow()->showStatusMessage(message, timeout); } void ChatRoomWidget::showCompletions(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) { matches[SampleSizeForHud] = tr("%Ln more completions", "", static_cast(matches.size() - SampleSizeForHud)); matches.resize(SampleSizeForHud); } setHudHtml(tr("Next completion:"), matches); } void ChatRoomWidget::insertMention(const QString& userId) { Q_ASSERT(currentRoom() != nullptr); const auto member = currentRoom()->member(userId); m_chatEdit->insertMention(member.displayName(), Quotient::Uri(member.id()).toUrl(Quotient::Uri::MatrixToUri)); m_chatEdit->setFocus(); } void ChatRoomWidget::attachImage(const QImage& img, const QList& sources) { if (currentRoom() == nullptr) return; // TODO: // 3. If the URL is local, detect the MIME type and the suffix // 4. Otherwise, prepend "image/" to the format string in QImage // and derive the suffix from this string const auto tempFileName = sources.size() == 1 && sources.front().isLocalFile() ? sources.front().fileName() : QStringLiteral("image.XXXXXX.png"); m_fileToAttach = std::make_unique(tempFileName); img.save(m_fileToAttach.get()); m_attachAction->setChecked(true); m_chatEdit->setPlaceholderText(AttachedPlaceholderText()); mainWindow()->showStatusMessage(tr("Attaching the pasted image")); } QString ChatRoomWidget::attachFile(const QString& localPath) { if (QUO_ALARM(currentRoom() == nullptr)) return tr("Can't attach a file without a selected room"); qCDebug(MSGINPUT) << "Trying to attach" << localPath; m_fileToAttach = std::make_unique(localPath); if (const auto error = checkAttachment(); !error.isEmpty()) return error; m_chatEdit->setPlaceholderText(AttachedPlaceholderText()); mainWindow()->showStatusMessage( tr("Attaching %1").arg(m_fileToAttach->fileName())); return {}; } void ChatRoomWidget::dropFile(const QString& localPath) { if (const auto error = attachFile(localPath); !error.isEmpty()) mainWindow()->showStatusMessage(error, 3000); else m_attachAction->setChecked(true); } QString ChatRoomWidget::checkAttachment() { Q_ASSERT(m_fileToAttach != nullptr); if (m_fileToAttach->isReadable() || m_fileToAttach->open(QIODevice::ReadOnly)) return {}; // Form the message in advance while the file name is still there const auto msg = tr("%1 is not readable or not a file").arg(m_fileToAttach->fileName()); cancelAttaching(); return msg; } void ChatRoomWidget::cancelAttaching() { m_fileToAttach.reset(); m_attachAction->setChecked(false); } 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; } std::unique_ptr contentFromFile(const QFileInfo& file) { using namespace Quotient::EventContent; using namespace Quotient::Literals; auto filePath = file.absoluteFilePath(); auto localUrl = QUrl::fromLocalFile(filePath); auto mimeType = QMimeDatabase().mimeTypeForFile(file); auto mimeTypeName = mimeType.name(); if (mimeTypeName.startsWith("image/"_L1)) return std::make_unique(localUrl, file.size(), mimeType, QImageReader(filePath).size(), file.fileName()); if (mimeTypeName.startsWith("audio/"_L1)) return std::make_unique(localUrl, file.size(), mimeType, file.fileName()); // TODO: video files support return std::make_unique(localUrl, file.size(), mimeType, file.fileName()); } QString ChatRoomWidget::sendFile() { Q_ASSERT(currentRoom() != nullptr); const auto& description = m_chatEdit->toPlainText(); QFileInfo fileInfo(*m_fileToAttach); if (!fileInfo.isReadable() || !fileInfo.isFile()) return tr("%1 is not readable or not a file") .arg(m_fileToAttach->fileName()); currentRoom()->postFile(description.isEmpty() ? fileInfo.fileName() : description, contentFromFile(fileInfo)); cancelAttaching(); return {}; } void ChatRoomWidget::sendSelection(int fromPosition, HtmlFilter::Options htmlFilterOptions) { QTextCursor c(m_chatEdit->document()); c.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, fromPosition); c.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); currentRoom()->sendMessage(c.selection(), htmlFilterOptions); } void ChatRoomWidget::sendMessage() { sendSelection(m_chatEdit->toPlainText().startsWith("//") ? 1 : 0, m_uiSettings.get("auto_markdown", false) ? HtmlFilter::ConvertMarkdown : HtmlFilter::Default); } static auto NothingToSendMsg() { return ChatRoomWidget::tr("There's nothing to send"); } QString ChatRoomWidget::sendCommand(QStringView 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 == u"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 == u"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 == u"leave" || command == u"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 == u"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 currentConnection()->forgetRoom(argString); return {}; } if (command == u"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 == u"kick" || command == u"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 == u"ban") currentRoom()->ban(args.front(), args.back()); else { const auto& userId = args.front(); if (!currentRoom()->isMember(userId)) return tr("%1 is not a member of this room").arg(userId); currentRoom()->kickMember(userId, args.back()); } return {}; } if (command == u"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 == u"ignore" || command == u"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 = currentConnection()->user(argString)) { if (command == u"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 == u"me") { if (argString.isEmpty()) return tr("/me needs an argument"); currentRoom()->postMessage(argString, MsgType::Emote); return {}; } if (command == u"notice") { if (argString.isEmpty()) return tr("/notice needs an argument"); currentRoom()->postMessage(argString, MsgType::Notice); return {}; } if (command == u"shrug") // Peeked at Discord { currentRoom()->postPlainText((argString.isEmpty() ? "" : argString + " ") + "¯\\_(ツ)_/¯"); return {}; } if (command == u"roomname") { currentRoom()->setName(argString); return {}; } if (command == u"topic") { currentRoom()->setTopic(argString); return {}; } if (command == u"nick" || command == u"mynick") { currentConnection()->user()->rename(argString); return {}; } if (command == u"roomnick" || command == u"myroomnick") { currentConnection()->user()->rename(argString, currentRoom()); return {}; } if (command == u"pm" || command == u"msg") { const auto args = lazySplitRef(argString, ' ', 2); if (args.front().isEmpty() || (args.back().isEmpty() && command == u"msg")) return tr("/%1 ").arg(command.toString()); if (RoomIdRE.match(args.front()).hasMatch() && command == u"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()->localMember().id(), args.front()); } if (UserIdRE.match(args.front()).hasMatch()) { auto futureChat = currentConnection()->getDirectChat(args.front()); if (!args.back().isEmpty()) futureChat.then([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 == u"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 == u"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 character %1: %2", "%1 is a position of the error; %2 is the error message") .arg(errorPos).arg(errorString); currentRoom()->sendMessage(QTextDocumentFragment::fromHtml(cleanQtHtml)); return {}; } if (command == u"md") { // Send the part after /md and one whitespace character after it // (leading whitespaces have meaning in Markdown) sendSelection(4, HtmlFilter::ConvertMarkdown); return {}; } if (command == u"query" || command == u"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); currentConnection()->requestDirectChat(argString); return {}; } // --- Add more room commands here qCDebug(MSGINPUT) << "Unknown command:" << command; return tr("Unknown /command. If you intended to send a message, start with // instead of /"); } void ChatRoomWidget::sendInput() { QString error; if (m_fileToAttach) error = sendFile(); else { const auto& text = m_chatEdit->toPlainText(); if (text.isEmpty()) error = NothingToSendMsg(); else if (text.startsWith('/') && !QStringView(text).mid(1).startsWith('/')) { static QRegularExpression cmdSplit(u"(\\w+)(?:\\s+(.*))?"_s, QRegularExpression::DotMatchesEverythingOption); const auto& blanksMatch = cmdSplit.match(text, 1); error = sendCommand(blanksMatch.capturedView(1), blanksMatch.captured(2)); } else if (!currentRoom()) error = tr("You should select a room to send messages."); else sendMessage(); } if (!error.isEmpty()) { showStatusMessage(error, 5000); return; } m_chatEdit->setPlaceholderText(DefaultPlaceholderText()); m_chatEdit->saveInput(); } ChatEdit::completions_t ChatRoomWidget::findCompletionMatches(const QString& pattern) const { if (!currentRoom()) return {}; ChatEdit::completions_t matches; const auto& members = currentRoom()->joinedMembers(); for (const auto& m: members) { using Quotient::Uri; if (m.displayName().startsWith(pattern, Qt::CaseInsensitive) || m.id().startsWith(pattern, Qt::CaseInsensitive)) matches.emplace_back(m.displayName(), Uri(m.id()).toUrl(Uri::MatrixToUri)); } std::ranges::sort(matches, [](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; } } void ChatRoomWidget::dragEnterEvent(QDragEnterEvent* event) { if (event->source() == m_chatEdit || !m_chatEdit->canInsertFromMimeData(event->mimeData())) { event->ignore(); return; } checkDndEvent(event); } void ChatRoomWidget::dropEvent(QDropEvent* event) { Q_ASSERT(event != nullptr); // Something very wrong with Qt if that fails auto* source = event->mimeData(); if (!source) { qCWarning(MSGINPUT) << "Nothing to insert from the drop event"; return; } event->setDropAction(Qt::CopyAction); // A default, but you never know qCDebug(MSGINPUT) << "MIME arrived:" << source->formats().join(u','); if (source->hasUrls()) qCDebug(MSGINPUT) << "MIME URLs:" << source->urls(); if (source->hasImage()) { attachImage(source->imageData().value(), source->urls()); event->accept(); } else if (source->hasHtml()) { if (m_chatEdit->acceptMimeData(source)) event->accept(); return; } else if (source->hasUrls()) { bool hasAnyProcessed = false; for (const QUrl& url : source->urls()) if (url.isLocalFile()) { attachFile(url.toLocalFile()); hasAnyProcessed = true; // Only the first url is processed for now break; } if (hasAnyProcessed) event->accept(); } if (m_chatEdit->acceptMimeData(source)) event->accept(); } QString ChatRoomWidget::matrixHtmlFromMime(const QMimeData* data) const { QUO_CHECK(data->hasHtml()); const auto [cleanHtml, errorPos, errorString] = HtmlFilter::fromLocalHtml(data->html(), { currentRoom() }); if (errorPos != -1) { qCWarning(MSGINPUT) << "HTML validation failed at position" << errorPos << "with error" << errorString; showStatusMessage(tr("Cannot insert HTML - it's either invalid or unsupported"), 5000); } return cleanHtml; } void ChatRoomWidget::checkDndEvent(QDropEvent* event) const { // `event` may originally come to m_chatEdit or be a QDragEnterEvent instead - all that is fine if (const auto* data = event->mimeData(); data->hasHtml() && matrixHtmlFromMime(data).isEmpty()) event->ignore(); event->setDropAction(Qt::CopyAction); event->accept(); } int ChatRoomWidget::maximumChatEditHeight() const { return height() / 3; } Quaternion-0.0.97.1/client/chatroomwidget.h000066400000000000000000000060041476730121700205520ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2015 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #pragma once #include "chatedit.h" #include "htmlfilter.h" #include #include #include namespace Quotient { class Connection; } class TimelineWidget; class QuaternionRoom; class MainWindow; class QLabel; class QAction; class ChatRoomWidget : public QWidget { Q_OBJECT public: explicit ChatRoomWidget(MainWindow* parent = nullptr); TimelineWidget* timelineWidget() const; QuaternionRoom* currentRoom() const; // Helpers for m_chatEdit ChatEdit::completions_t findCompletionMatches(const QString& pattern) const; QString matrixHtmlFromMime(const QMimeData* data) const; void checkDndEvent(QDropEvent* event) const; public slots: void setRoom(QuaternionRoom* newRoom); void insertMention(const QString &userId); void attachImage(const QImage& img, const QList& sources); QString attachFile(const QString& localPath); void dropFile(const QString& localPath); QString checkAttachment(); void cancelAttaching(); void focusInput(); //! Set a line above the message input, with optional list of member displaynames void setHudHtml(const QString& htmlCaption, const QStringList& plainTextNames = {}); void showStatusMessage(const QString& message, int timeout = 0) const; void showCompletions(QStringList matches, int pos); void typingChanged(); void quote(const QString& htmlText); private slots: void sendInput(); void encryptionChanged(); private: TimelineWidget* m_timelineWidget; QLabel* m_hudCaption; //!< For typing and completion notifications QAction* m_attachAction; ChatEdit* m_chatEdit; std::unique_ptr m_fileToAttach; Quotient::SettingsGroup m_uiSettings; MainWindow* mainWindow() const; Quotient::Connection* currentConnection() const; QString sendFile(); void sendMessage(); void sendSelection(int fromPosition, HtmlFilter::Options htmlFilterOptions); [[nodiscard]] QString sendCommand(QStringView command, const QString& argString); void resizeEvent(QResizeEvent*) override; void keyPressEvent(QKeyEvent* event) override; void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent* event) override; int maximumChatEditHeight() const; }; Quaternion-0.0.97.1/client/desktop_integration.h000066400000000000000000000011301476730121700216010ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2017 Elvis Angelaccio * SPDX-FileCopyrightText: 2020 The Quotient project * * SPDX-License-Identifier: GPL-3.0-or-later */ #pragma once #include #include inline const auto AppName = QStringLiteral("quaternion"); inline const auto AppId = QStringLiteral("io.github.quotient_im.Quaternion"); inline bool inFlatpak() { return QFileInfo::exists("/.flatpak-info"); } inline QIcon appIcon() { using Qt::operator""_s; return QIcon::fromTheme(inFlatpak() ? AppId : AppName, QIcon(u":/icon.png"_s)); } Quaternion-0.0.97.1/client/dialog.cpp000066400000000000000000000062561476730121700173350ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2017 Kitsune Ral * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "dialog.h" #include #include #if QT_VERSION < QT_VERSION_CHECK(6, 6, 0) #include // For std::views::adjacent #endif 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) , 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); } QLabel* Dialog::makeBuddyLabel(QString labelText, QWidget* field) { auto label = new QLabel(labelText); label->setBuddy(field); return label; } QPushButton* Dialog::button(QDialogButtonBox::StandardButton which) { return buttonBox()->button(which); } #if QT_VERSION < QT_VERSION_CHECK(6, 6, 0) void Dialog::setTabOrder(std::initializer_list widgets) { for (auto [w1, w2] : std::views::adjacent<2>(widgets)) setTabOrder(w1, w2); } #endif 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.97.1/client/dialog.h000066400000000000000000000066561476730121700170060ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2017 Kitsune Ral * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include #include #include class QAbstractButton; class QLabel; class Dialog : public QDialog { Q_OBJECT public: enum UseStatusLine { NoStatusLine, StatusLine }; static constexpr 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 = {}); static QLabel* makeBuddyLabel(QString labelText, QWidget* field); QPushButton* button(QDialogButtonBox::StandardButton which); using QWidget::setTabOrder; #if QT_VERSION < QT_VERSION_CHECK(6, 6, 0) static void setTabOrder(std::initializer_list widgets); #endif 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: QString pendingApplyMessage; QLabel* statusLabel; QDialogButtonBox* buttons; QVBoxLayout outerLayout; }; Quaternion-0.0.97.1/client/dockmodemenu.cpp000066400000000000000000000033631476730121700205440ustar00rootroot00000000000000#include "dockmodemenu.h" #include #if QT_VERSION_MAJOR >= 6 # include #endif DockModeMenu::DockModeMenu(QString name, QDockWidget* w) : QMenu(name) , dockWidget(w) , offAction(addAction(tr("&Off", "The dock panel is hidden"), [this] { dockWidget->setVisible(false); })) , dockedAction(addAction(tr("&Docked"), [this] { dockWidget->setVisible(true); dockWidget->setFloating(false); })) , floatingAction(addAction( tr("&Floating", "The dock panel is floating, aka undocked"), [this] { dockWidget->setVisible(true); dockWidget->setFloating(true); })) { offAction->setStatusTip(tr("Completely hide this list")); offAction->setCheckable(true); dockedAction->setStatusTip(tr("The list is shown within the main window")); dockedAction->setCheckable(true); floatingAction->setStatusTip( tr("The list is shown separately from the main window")); floatingAction->setCheckable(true); auto* radioGroup = new QActionGroup(this); for (auto* a : { offAction, dockedAction, floatingAction }) radioGroup->addAction(a); connect(dockWidget, &QDockWidget::visibilityChanged, this, &DockModeMenu::updateMode); connect(dockWidget, &QDockWidget::topLevelChanged, this, &DockModeMenu::updateMode); updateMode(); } void DockModeMenu::updateMode() { if (dockWidget->isHidden()) offAction->setChecked(true); else if (dockWidget->isFloating()) floatingAction->setChecked(true); else dockedAction->setChecked(true); } Quaternion-0.0.97.1/client/dockmodemenu.h000066400000000000000000000005101476730121700202000ustar00rootroot00000000000000#pragma once #include class QDockWidget; class DockModeMenu : public QMenu { Q_OBJECT public: DockModeMenu(QString name, QDockWidget* w); private slots: void updateMode(); private: QDockWidget* dockWidget; QAction* offAction; QAction* dockedAction; QAction* floatingAction; }; Quaternion-0.0.97.1/client/htmlfilter.cpp000066400000000000000000001051451476730121700202450ustar00rootroot00000000000000#include "htmlfilter.h" #include "logging_categories.h" #include #include #include #include #include #include #include using namespace std; using namespace Qt::StringLiterals; namespace { using namespace HtmlFilter; inline QRegularExpression operator""_qre(const char* latin1s, size_t size) { return QRegularExpression(operator""_L1(latin1s, size)); } enum Mode : unsigned char { QtToMatrix, MatrixToQt, GenericToQt }; class Processor : public QXmlStreamEntityResolver { public: [[nodiscard]] static Result process(QString html, Mode mode, const Context& context, Options options = Default); private: const Mode mode; const Options options; const Context& context; QXmlStreamWriter& writer; qsizetype errorPos = -1; QString errorString {}; Processor(Mode mode, Options options, const Context& context, QXmlStreamWriter& writer) : mode(mode), options(options), context(context), writer(writer) {} Q_DISABLE_COPY_MOVE(Processor) void runOn(const QString& html); using rewrite_t = vector>; [[nodiscard]] rewrite_t filterTag(QStringView tag, QXmlStreamAttributes attributes); void filterText(QString& text); QString resolveUndeclaredEntity(const QString& name) override { return name == u"nbsp" ? u"\xa0"_s : QString(); } }; constexpr auto permittedTags = std::to_array( {u"font", u"del", u"h1", u"h2", u"h3", u"h4", u"h5", u"h6", u"blockquote", u"p", u"a", u"ul", u"ol", u"sup", u"sub", u"li", u"b", u"i", u"u", u"strong", u"em", u"s", u"code", u"hr", u"br", u"div", u"table", u"thead", u"tbody", u"tr", u"th", u"td", u"caption", u"pre", u"span", u"img", u"mx-reply"}); struct PassList { QStringView tag; vector allowedAttrs; }; // See filterTag() on special processing of commented out tags/attributes const auto passLists = std::to_array({ {u"a", {u"name", u"target", /* u"href" - only from permittedSchemes */}}, {u"img", {u"width", u"height", u"alt", u"title", u"data-mx-emoticon", /* u"src" - only 'mxc:' */}}, {u"ol", {u"start"}}, {u"font", {u"color", u"data-mx-color", u"data-mx-bg-color"}}, {u"span", {u"color", u"data-mx-color", u"data-mx-bg-color"}}, // { u"code", { u"class" /* must start with 'language-' */ } } }); constexpr auto permittedSchemes = std::to_array({ u"http:", u"https:", u"ftp:", u"mailto:", u"magnet:", u"matrix:", u"mxc:" /* MSC2398 */ }); constexpr auto htmlColorAttr = u"color"; constexpr auto htmlStyleAttr = u"style"; constexpr auto mxColorAttr = u"data-mx-color"; constexpr auto mxBgColorAttr = u"data-mx-bg-color"; #ifdef __cpp_lib_ranges_contains constexpr auto rangeContains = ranges::contains; #else inline auto rangeContains(const auto& c, const auto& v) { return std::ranges::find(c, v) != std::ranges::end(c); } #endif [[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() != u"p") if (reader.atEnd()) { Q_ASSERT_X(false, __FUNCTION__, "Malformed Qt markup"); qCCritical(HTMLFILTER) << "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() != u"p") break; // Convert

elements except the first one // to Markdown paragraph breaks writer.writeCharacters("\n\n"); continue; case QXmlStreamReader::EndElement: --depth; if (reader.qualifiedName() == u"p") continue; // See above in StartElement break; case QXmlStreamReader::Comment: continue; // Just drop comments default: qCWarning(HTMLFILTER) << "Unexpected token, type" << tokenType; } if (depth < 0) { Q_ASSERT(tokenType == QXmlStreamReader::EndElement && reader.qualifiedName() == u"body"); break; } writer.writeCurrentToken(reader); } writer.writeEndElement(); QTextDocument doc; doc.setMarkdown(mdWithHtml); return doc.toHtml(); } [[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 = options.testFlag(Fragment) || 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 = QStringView(html).mid(tagNamePos); static constexpr auto commentOpen = "!--"_L1; static constexpr auto commentClose = "-->"_L1; 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 = u"<"_s; html.replace(pos, 1, to); pos += to.size(); // Put pos after the escaped sequence continue; } if (uncheckedHtml.startsWith(u"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, u" elements are not allowed in Matrix"_s }; static constexpr auto HeadEnd = ""_L1; 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 = ranges::find_if(uncheckedHtml, isTagNameTerminator); const auto tag = uncheckedHtml.left(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 == u"html" || tag == u"body")) { // Only in generic mode, allow and pos += tagNamePos + tag.size() + 1; isFragment = false; continue; } // Check if it's a valid (opening or closing) tag allowed in Matrix if (!inHead && !rangeContains(permittedTags, tag)) { // Invalid tag or non-tag - either remove the abusing piece or stop and report if (options.testFlag(Validate)) return {{}, pos, u"Non-tag or disallowed tag: "_s % 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 occurrences 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 auto MinAttrRE = R"(([^[:space:]>/"'=]+)\s*(=\s*([^[:space:]>/"']|"[^"]*"|'[^']*')+)?)"_qre; 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 = u"=''"_s; html.insert(m.capturedEnd(1), attrValue); gtPos += attrValue.size(); pos += attrValue.size(); } } // Make sure empty elements are properly closed static const QRegularExpression EmptyElementRE{"^img|[hb]r|meta$"_L1, 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 % ""; // Discard characters behind the last tag (LibreOffice attaches \n\0, e.g.) html.truncate(html.lastIndexOf('>') + 1); return { html }; } Result Processor::process(QString html, Mode mode, const Context& 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 static const auto freestandingAmps = "&(?!(#[0-9]+|#x[0-9a-fA-F]+|[[:alpha:]_][-[:alnum:]_:.]*);)"_qre; html.replace(freestandingAmps, QStringLiteral("&")); if (mode != GenericToQt) { // Handling control codes (excluding, for this discussion, \n, \r, and \t) in HTML is // somewhat messy. HTML 4 and XML 1.0 and XHTML 1.0 all disallow C0/C1 control codes in any // form. XML 1.1 allows them as numeric character references (aka NCRs) but // QXmlStreamReader only implements XML 1.0 and doesn't accept them even as NCRs. // Meanwhile, QTextDocument emits control codes to HTML without any conversion, formally // violating HTML 4 spec (https://bugreports.qt.io/browse/QTBUG-122466) and, more // importantly for this code, upsetting QXmlStreamReader (#900). HTML 5 (which Matrix HTML // is - assumed to be - based on) formally disallows control codes too, adding \f to the // allowed exclusions (see https://dev.w3.org/html5/spec-LC/syntax.html#text-0) which gives // us the right to eliminate control characters from Matrix payloads, even though the Web // generally seems to admit them as NCRs. // NB: [:cntrl:] doesn't work because it includes the allowed \n, \r, \t static const auto controlCharRE = R"([\x01-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f])"_qre; html.remove(controlCharRE); } if (mode == QtToMatrix) { 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("
"), 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 } } 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.trimmed(), p.errorPos, p.errorString }; } void Processor::runOn(const QString &html) { QXmlStreamReader reader(html); reader.setEntityResolver(this); /// 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; decltype(reader.characterOffset()) bodyOffset = 0; bool firstElement = true, inAnchor = false; while (!reader.atEnd()) { const auto tokenType = reader.readNext(); if (bodyOffset == -1) // See below in 'case StartElement:' bodyOffset = reader.characterOffset(); 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 == u"html") { if (mode == GenericToQt) writer.writeCurrentToken(reader); break; // Otherwise, just ignore, get to the content inside } if (tagName == u"head") { // is only needed for Qt to import HTML more // accurately, and entirely uninteresting in other modes if (mode != GenericToQt) { reader.skipCurrentElement(); break; } // Copy through the whole element - having // QXmlStreamWriter::writeCurrentElement() would help // but there's none such do { writer.writeCurrentToken(reader); const auto nextTokenType = reader.readNext(); if (nextTokenType == QXmlStreamReader::EndElement && reader.qualifiedName() == u"head") { writer.writeCurrentToken(reader); break; } } while (!reader.atEnd()); continue; } if (tagName == u"body") { if (mode == GenericToQt) writer.writeCurrentToken(reader); // Except importing HTML into QTextDocument, skip just like // but record the position for error reporting // (FIXME: this position is still not exactly related to // the original text...) bodyOffset = -1; // See the end of the while loop break; } } if (options.testFlag(StripMxReply) && tagName == u"mx-reply") { reader.skipCurrentElement(); continue; } const auto& attrs = reader.attributes(); if (ranges::any_of(attrs, [](const auto& a) { return a.qualifiedName() == u"style" && a.value().contains(u"-qt-paragraph-type:empty"); })) { // Hidden text block, just skip it reader.skipCurrentElement(); continue; } tagsStack.emplace(); if (tagsStack.size() > 100) qCCritical(HTMLFILTER) << "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 == u"span" && attrs.size() == 1 && attrs.front().qualifiedName() == u"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 == u"p" && tagsStack.size() == 1 /* top-level, just emplaced */) { if (firstElement) continue; // Skip unsetting firstElement at the loop end writer.writeEmptyElement(u"br"_s); break; } if (tagName != u"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& [rewrittenTag, rewrittenAttrs]: rewrite) { tagsStack.top().push(rewrittenTag); writer.writeStartElement(rewrittenTag); writer.writeAttributes(rewrittenAttrs); if (rewrittenTag == u"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 != u"body" && tagName != u"html") qCWarning(HTMLFILTER) << "Empty tags stack, skipping" << ('/' + tagName.toString()); 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() == u"a") inAnchor = false; } tagsStack.pop(); break; case QXmlStreamReader::EndDocument: if (!tagsStack.empty()) qCWarning(HTMLFILTER) << "Not all HTML tags closed at the document end"; if (mode == GenericToQt) writer.writeEndDocument(); // break; case QXmlStreamReader::NoToken: Q_ASSERT(reader.tokenType() != QXmlStreamReader::NoToken /*false*/); break; case QXmlStreamReader::Invalid: { errorPos = reader.characterOffset() - bodyOffset; errorString = reader.errorString(); qCCritical(HTMLFILTER) << "Invalid XHTML:" << html; qCCritical(HTMLFILTER).nospace() << "Error at char " << errorPos << ": " << errorString; const auto remainder = QStringView(html).mid(reader.characterOffset()); qCCritical(HTMLFILTER).nospace() << "Buffer at error: " << remainder << ", " << html.size() - reader.characterOffset() << " character(s) remaining"; 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 `` // NB: all `continue` statements above intentionally bypass this firstElement &= (bodyOffset <= 0 || reader.isWhitespace()); } } template inline QStringView cssValue(QStringView css, const char16_t (&propertyNameWithColon)[Len]) { return css.startsWith(propertyNameWithColon) ? css.mid(Len - 1).trimmed() : QStringView(); } Processor::rewrite_t Processor::filterTag(QStringView tag, QXmlStreamAttributes attributes) { if (mode == MatrixToQt) { if (tag == u"del" || tag == u"strike") { // Qt doesn't support these... QXmlStreamAttributes attrs; attrs.append(u"style"_s, u"text-decoration:line-through"_s); return { { u"font"_s, std::move(attrs) } }; } if (tag == u"mx-reply") return { { u"div"_s, {} } }; // 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 == u"code" && mode != GenericToQt) { // Special case ranges::copy_if(attributes, back_inserter(rewrite.back().second), [](const auto& a) { return a.qualifiedName() == u"class" && a.value().startsWith(u"language-"); }); return rewrite; } if (!rangeContains(permittedTags, tag)) return {}; // The tag is not allowed const auto it = ranges::find(passLists, tag, &PassList::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](QStringView attrName, QStringView attrValue) { auto colourableIt = ranges::find_if(rewrite, [this](const rewrite_t::value_type& element) { return element.first == "font" || (mode == QtToMatrix && element.first == "span"); }); if (colourableIt == rewrite.end()) colourableIt = rewrite.insert(rewrite.end(), { u"font"_s, {} }); colourableIt->second.append(attrName.toString(), 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.toString()); continue; } if (aName == mxBgColorAttr) { rewrite.front().second.append(QString::fromUtf16(htmlStyleAttr), "background-color:" + aValue.toString()); 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, u"color:"); !v.isEmpty()) { addColorAttr(mxColorAttr, v); } else if (const auto& v = cssValue(p, u"background-color:"); !v.isEmpty()) addColorAttr(mxBgColorAttr, v); else if (const auto& v = cssValue(p, u"font-weight:"); v == u"bold" || v == u"bolder" || v.toFloat() > 500) rewrite.emplace_back().first = u"b"_s; else if (const auto& v = cssValue(p, u"font-style:"); v == u"italic" || v.startsWith(u"oblique")) rewrite.emplace_back().first = u"i"_s; else if (const auto& v = cssValue(p, u"text-decoration:"); v.contains(u"line-through")) rewrite.emplace_back().first = u"del"_s; else { const auto& fontFamilies = cssValue(p, u"font-family:").split(','); for (auto ff : views::transform(fontFamilies, &QStringView::trimmed) | views::filter(std::not_fn(&QStringView::empty))) { if (ff.front() == '\'' || ff.front() == '"') ff = ff.mid(1, ff.size() - 2); if (QFontDatabase::isFixedPitch(ff.toString())) { rewrite.emplace_back().first = u"code"_s; break; } } } } continue; } if (aName == htmlColorAttr) addColorAttr(mxColorAttr, aValue); // Add to 'color' } // Enrich mxc source URLs for images with the context so that NAM could resolve them if (tag == u"img" && aName == u"src" && aValue.startsWith(u"mxc:")) { auto url = QUrl::fromUserInput(aValue.toString()); if (mode == QtToMatrix) { // Make sure the mxc URL is just that, with no internal extras QUrlQuery q{url.query()}; for (const auto& k : {u"user_id"_s, u"room_id"_s, u"event_id"_s}) q.removeAllQueryItems(k); url.setQuery(q); a = QXmlStreamAttribute(aName.toString(), url.toString(QUrl::FullyEncoded)); } else if (context.room) { a = QXmlStreamAttribute(aName.toString(), context.room ->makeMediaUrl(context.eventId, QUrl::fromUserInput(aValue.toString())) .toString(QUrl::FullyEncoded)); } rewrite.front().second.push_back(std::move(a)); } // Generic filtering for attributes if ((mode == GenericToQt && (aName == htmlStyleAttr || aName == u"class" || aName == u"id")) || (tag == u"a" && aName == u"href" && ranges::any_of(permittedSchemes, [&aValue](QStringView s) { return aValue.startsWith(s); })) || rangeContains(passList, a.qualifiedName())) rewrite.front().second.push_back(std::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 (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 constexpr auto Marker = "$$"_L1; 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(u"^( *[-+*] {1,4})(?=[^ ])"_s, ReOptions), OlRE(u"^( *[0-9]{1,9}+[.)] {1,4})(?=[^ ])"_s, ReOptions); static constexpr auto UlMarker = "@@ul@@"_L1, OlMarker = "@@ol@@"_L1; 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 { 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 { QString toMatrixHtml(const QString& qtMarkup, const Context& 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, const Context& context, Options options) { // Matrix HTML body should never be treated as Markdown Q_ASSERT(!options.testFlag(ConvertMarkdown)); 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, const Context& context, Options options) { return Processor::process(html, GenericToQt, context, options); } } // namespace HtmlFilter Quaternion-0.0.97.1/client/htmlfilter.h000066400000000000000000000137021476730121700177070ustar00rootroot00000000000000#pragma once #include // #include // For Q_NAMESPACE and Q_DECLARE_METATYPE namespace Quotient { class Room; } namespace HtmlFilter { Q_NAMESPACE enum Option : unsigned char { Default = 0x0, //! Treat `` contents as Markdown (toMatrixHtml() only) ConvertMarkdown = 0x1, //! 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, //! Remove elements previously used for reply fallbacks StripMxReply = 0x8 }; Q_ENUM_NS(Option) Q_DECLARE_FLAGS(Options, Option) struct Context { Quotient::Room* room; Quotient::EventId eventId{}; }; /*! \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(QString::size_type 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 QString::size_type 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, const Context& 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 * \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, const Context& 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, const Context& context, Options options = Fragment); } Q_DECLARE_METATYPE(HtmlFilter::Result) Quaternion-0.0.97.1/client/kchatedit.cpp000066400000000000000000000175601476730121700200360ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2017 Elvis Angelaccio * * SPDX-License-Identifier: GPL-3.0-or-later */ #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.97.1/client/kchatedit.h000066400000000000000000000101471476730121700174750ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2017 Elvis Angelaccio * SPDX-FileCopyrightText: 2020 The Quotient project * * SPDX-License-Identifier: GPL-3.0-or-later */ #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.97.1/client/logging_categories.h000066400000000000000000000015551476730121700213730ustar00rootroot00000000000000#pragma once // NB: Only include this file from .cpp #include // Reusing the macro defined in Quotient - these must never cross ways #define QUO_LOGGING_CATEGORY(Name, Id) \ inline Q_LOGGING_CATEGORY((Name), (Id), QtInfoMsg) namespace { QUO_LOGGING_CATEGORY(MAIN, "quaternion.main") QUO_LOGGING_CATEGORY(ACCOUNTSELECTOR, "quaternion.accountselector") QUO_LOGGING_CATEGORY(MODELS, "quaternion.models") QUO_LOGGING_CATEGORY(EVENTMODEL, "quaternion.models.events") QUO_LOGGING_CATEGORY(TIMELINE, "quaternion.timeline") QUO_LOGGING_CATEGORY(HTMLFILTER, "quaternion.htmlfilter") QUO_LOGGING_CATEGORY(MSGINPUT, "quaternion.messageinput") QUO_LOGGING_CATEGORY(THUMBNAILS, "quaternion.thumbnails") // Only to be used in QML; shows up here for documentation purpose only [[maybe_unused]] QUO_LOGGING_CATEGORY(TIMELINEQML, "quaternion.timeline.qml") } Quaternion-0.0.97.1/client/logindialog.cpp000066400000000000000000000262711476730121700203650ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2015 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #include "logindialog.h" #include "logging_categories.h" #include #include #include #include #include #include #include #include #include #include #include #include using Quotient::Connection; using namespace Qt::StringLiterals; static const auto MalformedServerUrl = LoginDialog::tr("The server URL doesn't look valid"); LoginDialog::LoginDialog(const QString& statusMessage, Quotient::AccountRegistry* loggedInAccounts, 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)) , deviceId(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.get(), [this, loggedInAccounts, knownAccounts] { auto userId = userEdit->text(); if (!userId.startsWith('@') || !userId.contains(':')) return; button(QDialogButtonBox::Ok)->setEnabled(false); if (loggedInAccounts->get(userId)) { setStatusMessage( tr("This account is logged in already")); return; } if (knownAccounts.contains(userId)) { Quotient::AccountSettings acct{ userId }; initialDeviceName->setText(acct.deviceName()); deviceId->setText(acct.deviceId()); saveTokenCheck->setChecked(acct.keepLoggedIn()); } else { initialDeviceName->clear(); deviceId->clear(); saveTokenCheck->setChecked(true); } setStatusMessage(tr("Resolving the homeserver...")); serverEdit->clear(); m_connection->resolveServer(userId); }); connect(serverEdit, &QLineEdit::editingFinished, m_connection.get(), [this, knownAccounts] { if (QUrl hsUrl{ serverEdit->text() }; hsUrl.isValid()) { m_connection->setHomeserver(hsUrl); 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.get(), &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 if (!knownAccounts.empty()) { Quotient::AccountSettings account { knownAccounts.front() }; userEdit->setText(account.userId()); auto homeserver = account.homeserver(); if (!homeserver.isEmpty()) m_connection->setHomeserver(homeserver); initialDeviceName->setText(account.deviceName()); deviceId->setText(account.deviceId()); saveTokenCheck->setChecked(account.keepLoggedIn()); } else { saveTokenCheck->setChecked(true); } } LoginDialog::LoginDialog(const QString& statusMessage, const Quotient::AccountSettings& reloginAccount, QWidget* parent) : Dialog(tr("Re-login"), parent, Dialog::StatusLine, tr("Re-login"), Dialog::NoExtraButtons) , userEdit(new QLineEdit(reloginAccount.userId(), this)) , passwordEdit(new QLineEdit(this)) , initialDeviceName(new QLineEdit(reloginAccount.deviceName(), this)) , deviceId(new QLineEdit(reloginAccount.deviceId(), this)) , serverEdit(new QLineEdit(reloginAccount.homeserver().toString(), this)) , saveTokenCheck(new QCheckBox(tr("Stay logged in"), this)) , m_connection(new Connection) { setup(statusMessage); userEdit->setReadOnly(true); userEdit->setFrame(false); initialDeviceName->setReadOnly(true); initialDeviceName->setFrame(false); saveTokenCheck->setEnabled(reloginAccount.keepLoggedIn()); 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.get(), &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.get(), &Connection::loginFlowsChanged, this, [this] { serverEdit->setText(m_connection->homeserver().toString()); setStatusMessage(!m_connection->loginFlows().empty() ? tr("The homeserver is available") : tr("Could not connect to the homeserver")); button(QDialogButtonBox::Ok)->setEnabled(!m_connection->loginFlows().isEmpty()); passwordEdit->setEnabled(m_connection->supportsPasswordAuth()); }); // This overrides the above in case of an unsuccessful attempt to resolve // the server URL from a changed MXID connect(m_connection.get(), &Connection::resolveError, this, [this](const QString& message) { qCDebug(MAIN) << "Failed to resolve the homeserver:" << message; serverEdit->clear(); setStatusMessage(message); }); deviceId->setReadOnly(true); deviceId->setFrame(false); deviceId->setPlaceholderText(tr( "(none)", "The device id label text when there's no saved device id")); connect(initialDeviceName, &QLineEdit::textChanged, deviceId, &QLineEdit::clear); connect(m_connection.get(), &Connection::connected, this, &Dialog::accept); connect(m_connection.get(), &Connection::loginError, this, &Dialog::applyFailed); // Lay out controls on the dialog auto* mainLayout = addLayout(); mainLayout->addRow(tr("Matrix ID"), userEdit); auto* homeserverLayout = new QFormLayout(); homeserverLayout->addRow(tr("Connect to server"), serverEdit); mainLayout->addRow({}, homeserverLayout); mainLayout->addRow(tr("Device name"), initialDeviceName); auto* deviceIdLayout = new QFormLayout(); deviceIdLayout->addRow(tr("Saved device id"), deviceId); mainLayout->addRow({}, deviceIdLayout); mainLayout->addRow(tr("Password"), passwordEdit); mainLayout->addRow(saveTokenCheck); setTabOrder({ userEdit, initialDeviceName, passwordEdit, saveTokenCheck, serverEdit, deviceId }); userEdit->setFocus(); } Connection* LoginDialog::releaseConnection() { return m_connection.release(); } 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).then([this](auto) { qCDebug(MAIN) << "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(), deviceId->text()); } void LoginDialog::loginWithSso() { auto* ssoSession = m_connection->prepareForSso(initialDeviceName->text(), deviceId->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.97.1/client/logindialog.h000066400000000000000000000033331476730121700200240ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2015 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #pragma once #include "dialog.h" #include class QLineEdit; class QCheckBox; namespace Quotient { class AccountSettings; class AccountRegistry; } static const auto E2eeEnabledSetting = QStringLiteral("enable_e2ee"); class LoginDialog : public Dialog { Q_OBJECT public: // FIXME: make loggedInAccounts pointer to const once we get to // libQuotient 0.8 LoginDialog(const QString& statusMessage, Quotient::AccountRegistry* loggedInAccounts, QWidget* parent, const QStringList& knownAccounts = {}); LoginDialog(const QString& statusMessage, const Quotient::AccountSettings& reloginAccount, QWidget* parent); void setup(const QString& statusMessage); 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* deviceId; QLineEdit* serverEdit; QCheckBox* saveTokenCheck; Quotient::QObjectHolder m_connection; }; Quaternion-0.0.97.1/client/main.cpp000066400000000000000000000143701476730121700170160ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2015 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #include "desktop_integration.h" #include "logging_categories.h" #include "mainwindow.h" #include #include #include #include #include #include #include #include #include using namespace Qt::StringLiterals; namespace { inline void loadTranslations() { // Extract a number from another macro and turn it to a const char[] #define ITOA(i) #i static const auto translationConfigs = std::to_array>( { { { u"qt"_s, u"qtbase"_s, u"qtnetwork"_s, u"qtdeclarative"_s, u"qtmultimedia"_s, u"qtquickcontrols"_s, u"qtquickcontrols2"_s, // QtKeychain tries to install its translations to Qt's path; // try to look there, just in case (see also below) u"qtkeychain"_s }, QLibraryInfo::path(QLibraryInfo::TranslationsPath) }, { { u"qtkeychain"_s }, QStandardPaths::locate(QStandardPaths::GenericDataLocation, u"qt" ITOA(QT_VERSION_MAJOR) "keychain/translations"_s, QStandardPaths::LocateDirectory) }, { { u"qt"_s, u"qtkeychain"_s, u"quotient"_s, AppName }, QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, u"translations"_s, QStandardPaths::LocateDirectory) } }); #undef ITOA for (const auto& [configNames, configPath] : translationConfigs) for (const auto& configName : configNames) { auto translator = std::make_unique(); // Check the current directory then configPath if (translator->load(QLocale(), configName, u"_"_s) || translator->load(QLocale(), configName, u"_"_s, configPath)) { auto path = translator->filePath(); if (QApplication::installTranslator(translator.get())) { qCDebug(MAIN).noquote() << "Loaded translations from" << path; translator.release()->setParent(qApp); // Change pointer ownership } else qCWarning(MAIN).noquote() << "Failed to load translations from" << path; } else qCDebug(MAIN) << "No translations for" << configName << "at" << configPath; } } } int main( int argc, char* argv[] ) { QApplication::setOrganizationName(u"Quotient"_s); QApplication::setApplicationName(AppName); QApplication::setApplicationDisplayName(u"Quaternion"_s); QApplication::setApplicationVersion(u"0.0.97.1"_s); QApplication::setDesktopFileName(AppId); using Quotient::Settings; Settings::setLegacyNames(u"QMatrixClient"_s, u"quaternion"_s); Settings settings; 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"); QIcon::setFallbackThemeName("breeze"); } else #endif { QQuickStyle::setFallbackStyle(u"Fusion"_s); // 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); qCInfo(MAIN) << "Using application font:" << font.toString(); QApplication::setFont(font); } 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(QLocale(overrideLocale)); qCInfo(MAIN) << "Using locale" << QLocale().name(); } loadTranslations(); Quotient::NetworkSettings().setupApplicationProxy(); Quotient::Connection::setEncryptionDefault(true); MainWindow window; if (parser.isSet(hideMainWindow)) { qCDebug(MAIN) << "--- Hide time!"; window.hide(); } else { qCDebug(MAIN) << "--- Show time!"; window.show(); } return QApplication::exec(); } Quaternion-0.0.97.1/client/mainwindow.cpp000066400000000000000000001565201476730121700202520ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2015 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #include "mainwindow.h" #include "accountselector.h" #include "chatroomwidget.h" #include "dockmodemenu.h" #include "desktop_integration.h" #include "logging_categories.h" #include "logindialog.h" #include "networkconfigdialog.h" #include "profiledialog.h" #include "quaternionroom.h" #include "roomdialogs.h" #include "roomlistdock.h" #include "systemtrayicon.h" #include "timelinewidget.h" #include "userlistdock.h" #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 #include #include #include #include #include #include #include using namespace Qt::StringLiterals; MainWindow::MainWindow() { Connection::setRoomType(); #if Quotient_E2EE_ENABLED Connection::setEncryptionDefault(true); #endif // Bind callbacks to signals from NetworkAccessManager auto nam = Quotient::NetworkAccessManager::instance(); connect(nam, &QNetworkAccessManager::proxyAuthenticationRequired, this, &MainWindow::proxyAuthenticationRequired); connect(nam, &QNetworkAccessManager::sslErrors, this, &MainWindow::sslErrors); setWindowIcon(appIcon()); 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); loadSettings(); // Only GUI, account settings will be loaded in invokeLogin createMenu(); // Assumes loadSettings() is done, to set flags on menu items 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...")); busyLabel->show(); busyIndicator->start(); connect(accountRegistry, &Quotient::AccountRegistry::rowsAboutToBeRemoved, this, [this](const QModelIndex&, int first, int last) { const auto& accounts = accountRegistry->accounts(); for (int i = first; i <= last; ++i) roomListDock->deleteConnection(accounts[i]); }); connect(accountRegistry, &Quotient::AccountRegistry::rowsRemoved, this, [this] { const auto noMoreAccounts = accountRegistry->isEmpty(); openRoomAction->setDisabled(noMoreAccounts); createRoomAction->setDisabled(noMoreAccounts); joinAction->setDisabled(noMoreAccounts); if (noMoreAccounts) showLoginWindow(); }); QMetaObject::invokeMethod(this, &MainWindow::invokeLogin, Qt::QueuedConnection); } MainWindow::~MainWindow() { saveSettings(); accountRegistry->disconnect(this); } 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, [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"), quitShortcut, qApp, &QApplication::quit); // View menu auto viewMenu = menuBar()->addMenu(tr("&View")); 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)); }); viewMenu->addSection( tr("Dock panels", "Panels of the dock, not 'to dock the panels'")); viewMenu->addMenu(new DockModeMenu(tr("&Room list"), roomListDock)); viewMenu->addMenu(new DockModeMenu(tr("&Member list"), userListDock)); // 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"), QKeySequence::Close, [this] { selectRoom(nullptr); }); // 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); }); using Quotient::Settings; { 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(); } void MainWindow::addConnection(Connection* c) { Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection"); using Room = Quotient::Room; accountRegistry->add(c); c->loadState(); c->setLazyLoading(true); roomListDock->addConnection(c); c->syncLoop(); connect(c, &Connection::syncDone, this, [this, c, counter = 0]() mutable { if (counter == 0) firstSyncOver(c); // 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, [this, c] { statusBar()->showMessage(tr("Logged out as %1").arg(c->userId()), 3000); dropConnection(c); }); connect(c, &Connection::networkError, this, [this, c] { 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::UserConsentRequired ? 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, [this, c](const QString& msg) { reloginNeeded(c, msg); }); for (auto* r: c->allRooms()) systemTrayIcon->newRoom(r); 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(); QString menuCaption = accountCaption; if (accountRegistry->size() < 10) menuCaption.prepend('&' % QString::number(accountRegistry->size()) % ' '); auto logoutAction = logoutMenu->addAction(menuCaption, c, &Connection::logout); connect(c, &Connection::destroyed, logoutMenu, [this, logoutAction] { logoutMenu->removeAction(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); logoutOnExit.removeOne(c); Q_ASSERT(!logoutOnExit.contains(c) && !c->syncJob()); accountRegistry->drop(c); c->deleteLater(); } void MainWindow::showInitialLoadIndicator() { busyLabel->show(); busyIndicator->start(); updateLoadingStatus(accountRegistry->size()); } void MainWindow::updateLoadingStatus(int accountsStillLoading) { statusBar()->showMessage(tr("Loading %Ln accounts, please wait", "", accountsStillLoading)); } void MainWindow::firstSyncOver(const Connection *c) { Q_ASSERT(c != nullptr); statusBar()->showMessage( tr("First sync completed for %1", "%1 is user id").arg(c->userId()), 3000); const auto accountsNotSynced = std::ranges::count_if(*accountRegistry, [](const Connection* cc) { return cc->nextBatchToken().isEmpty(); }); qCDebug(MAIN) << "Connections still not synced: " << accountsNotSynced; if (accountsNotSynced == 0) { busyLabel->hide(); busyIndicator->stop(); statusBar()->showMessage( accountRegistry->size() == 1 ? tr("Account %1 is synchronised, have a good chat") .arg(accountRegistry->front()->userId()) : tr("All %Ln accounts synchronised, have a good chat", "Only shown with 2 or more accounts", accountRegistry->size()), 5000); } else { updateLoadingStatus(static_cast(accountsNotSynced)); } } void MainWindow::showLoginWindow(const QString& statusMessage) { const auto& allKnownAccounts = Quotient::SettingsGroup("Accounts").childGroups(); QStringList loggedOffAccounts; for (const auto& a: allKnownAccounts) if (!accountRegistry->get(Quotient::AccountSettings(a).userId())) loggedOffAccounts.push_back(a); // Skip already logged in accounts doOpenLoginDialog(new LoginDialog(statusMessage, accountRegistry, this, loggedOffAccounts)); } void MainWindow::showLoginWindow(const QString& statusMessage, const QString& userId) { doOpenLoginDialog(new LoginDialog(statusMessage, Quotient::AccountSettings(userId), this)); } 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(); Quotient::AccountSettings account(connection->userId()); account.setKeepLoggedIn(dialog->keepLoggedIn()); account.setHomeserver(connection->homeserver()); account.setDeviceId(connection->deviceId()); account.setDeviceName(dialog->deviceName()); account.setValue(E2eeEnabledSetting, connection->encryptionEnabled()); if (!dialog->keepLoggedIn()) { logoutOnExit.push_back(connection); } account.sync(); dialog->deleteLater(); addConnection(connection); showInitialLoadIndicator(); }); 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-2024 " % 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
" "Qt Creator
" "CLion
" "Lokalise
" "Cloudsmith" ); thanksLabel->setTextInteractionFlags(Qt::TextSelectableByKeyboard| Qt::TextBrowserInteraction); thanksLabel->setOpenExternalLinks(true); tabWidget->addTab(thanksLabel, tr("&Thanks")); } aboutDialog.addWidget(tabWidget); aboutDialog.exec(); } class ConnectionInitiator : public QObject { // No Q_OBJECT - the only thing needed from QObject here is // connect()ability, without things generated by moc public: ConnectionInitiator(const Quotient::AccountSettings& account, const QString& accessToken, MainWindow* parent) : QObject(parent) , connection(new Quotient::Connection(account.homeserver(), parent)) , userId(account.userId()) , deviceId(account.deviceId()) , accessToken(accessToken) { // NB: ConnectionInitiator is intended to only handle errors until // Connection::connected() arrives; upon this, ConnectionInitiator // deletes itself along with its QMetaObject::Connections, and a new // lineup of error handlers is set up in addConnection() connect(connection, &Quotient::Connection::networkError, this, &ConnectionInitiator::onNetworkError); connect(connection, &Quotient::Connection::connected, this, [this, parent] { parent->addConnection(connection); deleteLater(); }); tryConnection(); } Quotient::Connection* getConnection() const { return connection; } void tryConnection() { connection->assumeIdentity(userId, deviceId, accessToken); } void onNetworkError(const QString& error) { using namespace std::chrono_literals; qCWarning(MAIN) << "Network error at initial connection:" << error; QTimer::singleShot(10s, this, &ConnectionInitiator::tryConnection); } private: Quotient::Connection* const connection; const QString userId; const QString deviceId; const QString accessToken; }; template inline QString accessTokenKey(const KeySourceT& source, bool legacyLocation) { auto k = source.userId(); if (legacyLocation) { if (source.deviceId().isEmpty()) qCWarning(MAIN) << "Device id on the account" << source.userId() << "is not set"; else k += '-' % source.deviceId(); } return k; } void MainWindow::invokeLogin() { const auto accounts = Quotient::SettingsGroup("Accounts").childGroups(); bool showLoginDialog = true; for (const auto& accountId: accounts) { Quotient::AccountSettings account { accountId }; if (account.homeserver().isEmpty()) continue; // Not even the homeserver filled in, skipping const auto& [token, legacyLocation] = loadAccessToken(account); if (token.isEmpty()) // The account is saved but not logged-in continue; showLoginDialog = false; qCDebug(MAIN).noquote().nospace() << "Found an access token for " << account.userId() << '/' << account.deviceId() << ", trying to connect"; auto ci = new ConnectionInitiator(account, token, this); auto c = ci->getConnection(); connect(c, &Connection::loginError, ci, [this, ci](const QString& error, const QString& details) { // Double-check that the error is not due to the network, // older libQuotient does not distinguish between the two if (details.startsWith(u'{')) reloginNeeded(ci->getConnection(), error); else ci->onNetworkError(error); }); if (legacyLocation) { connect(c, &Connection::connected, this, [this, c] { qCInfo(MAIN) << "Removing the access token from the oldkeychain slot"; using namespace QKeychain; auto* delJob = new DeletePasswordJob(qAppName(), this); delJob->setKey(accessTokenKey(*c, true)); connect(delJob, &Job::finished, this, [delJob] { if (delJob->error() != Error::NoError) qCWarning(MAIN).noquote() << "Cleanup of the old keychain slot failed:" << delJob->errorString(); }); delJob->start(); // Run async and move on }); } } // By now, either no accounts were found or whichever were found are // retrieving their access tokens (or resolving their homeservers at least) if (showLoginDialog) showLoginWindow(tr("Welcome to Quaternion")); else showInitialLoadIndicator(); } std::pair MainWindow::loadAccessToken( const Quotient::AccountSettings& account) { using namespace QKeychain; for (auto legacyLocation : { false, true }) { const auto& key = accessTokenKey(account, legacyLocation); qCDebug(MAIN).noquote() << "Reading the access token from the keychain for" << key; auto slotName = qAppName(); if (legacyLocation) slotName += " access token for " % key; auto job = std::make_unique(slotName, this); job->setAutoDelete(false); job->setKey(key); QEventLoop loop; connect(job.get(), &Job::finished, &loop, &QEventLoop::quit); job->start(); loop.exec(); if (job->error() == Error::NoError) return { job->binaryData(), legacyLocation }; qCInfo(MAIN).noquote() << "Could not read the access token for" << job->key() << "from the keychain:" << job->errorString(); } return {}; } void MainWindow::reloginNeeded(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 dropConnection(c); // NB: waiting for libQuotient to support soft logout showLoginWindow(message, c->userId()); } QFuture MainWindow::logout(Connection* c) { if (QUO_ALARM_X(!c, "Logout on a null connection"_L1)) return {}; // libQuotient takes care about the new location but not the old one using namespace QKeychain; auto* keychainJob = new DeletePasswordJob(qAppName(), this); keychainJob->setKey(accessTokenKey(*c, true)); auto keychainFt = QtFuture::connect(keychainJob, &Job::finished).then(this, [this](Job* j) { switch (j->error()) { case Error::EntryNotFound: 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); } qCWarning(MAIN).noquote() << "Could not delete access token from the keychain: " << QUO_CSTR(j->errorString()); }); keychainJob->start(); return QtFuture::whenAll(keychainFt, c->logout()).then([userId = c->userId()](auto) { return userId; }); } Quotient::UriResolveResult MainWindow::visitUser(Quotient::User* user, const QString& action) { if (action == "mention" || action.isEmpty()) chatRoomWidget->insertMention(user->id()); // 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) { // 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. account->joinRoom(roomAliasOrId, viaServers).then(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) { using Quotient::Uri; 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 (uri.type() == Uri::BareEventId) { if (!currentRoom) { QMessageBox::warning( this, tr("Can't find the event without knowing the room"), tr("Open the room that has this event to scroll to %1").arg(idOrUri), QMessageBox::Close, QMessageBox::Close); return; } chatRoomWidget->timelineWidget()->spotlightEvent(uri.primaryId()); 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) qCDebug(MAIN) << "Opening room" << r->objectName(); else if (currentRoom) qCDebug(MAIN) << "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(); } qCDebug(MAIN).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 = static_cast(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 auto map = std::to_array( { { 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& userIds = connection->userIds(); // 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 + userIds.size()); for (auto* room: allRooms) { completions << room->id(); if (!room->canonicalAlias().isEmpty()) completions << room->canonicalAlias(); } std::ranges::copy(userIds, std::back_inserter(completions)); completions.sort(); completions.erase(std::ranges::unique(completions).begin(), completions.end()); auto* completer = new QCompleter(completions); completer->setFilterMode(Qt::MatchContains); identifier->setCompleter(completer); }; setCompleter(accountChooser->currentAccount()); connect(accountChooser, &AccountSelector::currentAccountChanged, identifier, setCompleter); } using Quotient::Uri; 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(const QPointer& 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; Quotient::NetworkAccessManager::addIgnoredSslError(error); } // If a message box above is left open for too long, the reply may timeout // and self-delete (#688) - double-check that it's still alive if (reply) 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::Settings{}.get("UI/close_to_tray", false)) { hide(); event->ignore(); return; } if (logoutOnExit.empty()) { qCDebug(MAIN, "Closing down"); event->accept(); return; } event->ignore(); // Got some finalization to do, can't exit yet qCDebug(MAIN) << "Logging out" << logoutOnExit.size() << "account(s) that don't stay logged in"; auto dlg = new QProgressDialog(u"Logging out"_s, u"Exit now"_s, 0, static_cast(logoutOnExit.size())); dlg->setMinimumDuration(1000); dlg->setAttribute(Qt::WA_DeleteOnClose); for (auto* c : logoutOnExit) logout(c).then([this, dlg](auto) { // By now, MainWindow::dropConnection() has already worked, in particular the logged out // account was removed from logoutOnExit. dlg->setValue(dlg->maximum() - static_cast(logoutOnExit.size())); if (logoutOnExit.empty()) { qCDebug(MAIN, "All accounts to log out on exit have logged out"); // NB: this causes a new QCloseEvent coming on MainWindow but logoutOnExit is empty // already, leading to the early finish of this new closeEvent() invocation qApp->quit(); } }); } Quaternion-0.0.97.1/client/mainwindow.h000066400000000000000000000125451476730121700177150ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2015 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #pragma once // QSslError is used in a signal container parameter and needs to be complete // for moc to generate stuff since Qt 6 #include #include #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 QNetworkProxy; class QAuthenticator; class MainWindow: public QMainWindow, public Quotient::UriResolverBase { Q_OBJECT public: using Connection = Quotient::Connection; MainWindow(); ~MainWindow() override; void addConnection(Connection* c); void dropConnection(Connection* c); Quotient::AccountRegistry* registry() { return accountRegistry; } // 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); QFuture logout(Connection* c); private slots: void invokeLogin(); void reloginNeeded(Connection* c, const QString& message = {}); void networkError(Connection* c); void sslErrors(const QPointer& 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: Quotient::AccountRegistry* accountRegistry = new Quotient::AccountRegistry(this); QVector logoutOnExit; 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 showInitialLoadIndicator(); void updateLoadingStatus(int accountsStillLoading); void firstSyncOver(const Connection *c); void loadSettings(); void saveSettings() const; void doOpenLoginDialog(LoginDialog* dialog); Connection* chooseConnection(Connection* connection, const QString& prompt); void showMillisToRecon(Connection* c); std::pair loadAccessToken( const Quotient::AccountSettings& account); /// 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.97.1/client/models/000077500000000000000000000000001476730121700166445ustar00rootroot00000000000000Quaternion-0.0.97.1/client/models/abstractroomordering.cpp000066400000000000000000000017041476730121700236040ustar00rootroot00000000000000/****************************************************************************** * SPDX-FileCopyrightText: 2018-2019 QMatrixClient Project * * SPDX-License-Identifier: GPL-3.0-or-later */ #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_front(&AbstractRoomOrdering::groupLessThan, this); } AbstractRoomOrdering::roomLessThan_closure_t AbstractRoomOrdering::roomLessThanFactory(const QVariant& group) const { return std::bind_front(&AbstractRoomOrdering::roomLessThan, this, group); } void AbstractRoomOrdering::updateGroups(Room* room) { model()->updateGroups(room); } RoomListModel* AbstractRoomOrdering::model() const { return static_cast(parent()); } Quaternion-0.0.97.1/client/models/abstractroomordering.h000066400000000000000000000062631476730121700232560ustar00rootroot00000000000000/****************************************************************************** * SPDX-FileCopyrightText: 2018-2019 QMatrixClient Project * * SPDX-License-Identifier: GPL-3.0-or-later */ #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 QVariant& g1key, 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.97.1/client/models/messageeventmodel.cpp000066400000000000000000001140261476730121700230630ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2015 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #include "messageeventmodel.h" #include #include #include // for qmlRegisterType() #include "../quaternionroom.h" #include "../htmlfilter.h" #include "../logging_categories.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { // TODO: move to libQuotient; duplicate in profiledialog.cpp //! Like std::clamp but admits a different (usually larger) type for the value template inline constexpr T clamp(const auto& v, const T& lo = std::numeric_limits::min(), const T& hi = std::numeric_limits::max()) { return v < lo ? lo : hi < v ? hi : static_cast(v); } } QHash MessageEventModel::roleNames() const { static const auto roles = [this] { auto rolesInit = QAbstractItemModel::roleNames(); // Not every Qt standard role has a role name, turns out rolesInit.insert(Qt::ForegroundRole, "foreground"); rolesInit.insert(EventTypeRole, "eventType"); rolesInit.insert(EventIdRole, "eventId"); rolesInit.insert(DateTimeRole, "dateTime"); rolesInit.insert(DateRole, "date"); rolesInit.insert(EventGroupingRole, "eventGrouping"); rolesInit.insert(AuthorRole, "author"); rolesInit.insert(AuthorHasAvatarRole, "authorHasAvatar"); rolesInit.insert(ContentRole, "content"); rolesInit.insert(ContentTypeRole, "contentType"); rolesInit.insert(RepliedToRole, "repliedTo"); rolesInit.insert(HighlightRole, "highlight"); rolesInit.insert(SpecialMarksRole, "marks"); rolesInit.insert(LongOperationRole, "progressInfo"); rolesInit.insert(AnnotationRole, "annotation"); rolesInit.insert(RefRole, "refId"); rolesInit.insert(ReactionsRole, "reactions"); rolesInit.insert(EventClassNameRole, "eventClassName"); rolesInit.insert(VerificationStateRole, "verificationState"); return rolesInit; }(); return roles; } MessageEventModel::MessageEventModel(QObject* parent) : QAbstractListModel(parent) { using namespace Quotient; qmlRegisterAnonymousType("Quotient", 1); qmlRegisterAnonymousType("Quotient", 1); qmlRegisterUncreatableMetaObject(EventStatus::staticMetaObject, "Quotient", 1, 0, "EventStatus", "Access to EventStatus enums only"); qmlRegisterUncreatableMetaObject(EventGrouping::staticMetaObject, "Quotient", 1, 0, "EventGrouping", "Access to enums only"); qmlRegisterUncreatableMetaObject(VerificationState::staticMetaObject, "Quotient", 1, 0, "VerificationState", "Access to enums only"); // 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 // NB: this makes all roomChanged connections order before modelReset // connections 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) { qCDebug(EVENTMODEL) << "Disconnecting event model from" << m_currentRoom->objectName(); // Reset the model to a null room first to make sure QML dismantles // last room's objects before the room is actually changed beginResetModel(); m_currentRoom->disconnect(this); m_currentRoom = nullptr; endResetModel(); } beginResetModel(); 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()) { // When historical events arrive, make sure to update // the previously-oldest event (e.g. to move author mark // to an older event) const auto rowBelowInserted = m_currentRoom->maxTimelineIndex() - biggest + timelineBaseIndex() - 1; refreshEventRoles(rowBelowInserted, { EventGroupingRole }); } 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, { EventGroupingRole }); }); 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::fullyReadMarkerMoved, 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); qCDebug(EVENTMODEL) << "Event model connected to room" << room->objectName() // << "as" << room->localMember().id(); // If the timeline isn't loaded, ask for at least something right away if (room->timelineSize() == 0) room->getPreviousContent(30); } endResetModel(); emit readMarkerUpdated(); } int MessageEventModel::refreshEvent(const QString& eventId) { int row = findRow(eventId, true); if (row >= 0) refreshEventRoles(row); else qCWarning(EVENTMODEL) << "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->lastFullyReadEventId()); 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; } namespace { inline std::optional getTimestamp(auto from, auto to) { for (const auto& ti : std::ranges::subrange(from, to)) if (auto ts = ti->originTimestamp(); ts.isValid()) return ts.date().startOfDay(); return std::nullopt; } } 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 closestPastTs = getTimestamp(baseIt, timeline.rend())) return *closestPastTs; if (auto closestFutureTs = getTimestamp(baseIt.base(), timeline.end())) return *closestFutureTs; // What kind of room is that?.. qCCritical(EVENTMODEL) << "No valid timestamps in the room timeline!"; return {}; } QString MessageEventModel::renderDate(const QDateTime& timestamp) { auto date = timestamp.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() != Membership::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(); } inline QColor mixColors(QColor base, QColor tint, qreal mixRatio = 0.5) { mixRatio = tint.alphaF() * mixRatio; const auto baseRatio = 1 - mixRatio; return QColor::fromRgbF(tint.redF() * mixRatio + base.redF() * baseRatio, tint.greenF() * mixRatio + base.greenF() * baseRatio, tint.blueF() * mixRatio + base.blueF() * baseRatio, mixRatio + base.alphaF() * baseRatio); } inline QColor fadedTextColor(QColor unfadedColor, qreal fadeRatio = 0.5) { return mixColors(QPalette().color(QPalette::Disabled, QPalette::Text), unfadedColor, fadeRatio); } QColor MessageEventModel::fadedBackColor(QColor unfadedColor, qreal fadeRatio) const { return mixColors(QPalette().color(QPalette::Disabled, QPalette::Base), unfadedColor, fadeRatio); } QString MessageEventModel::visualiseEvent(const Quotient::RoomEvent& evt, bool abbreviate) const { if (evt.isRedacted()) { auto reason = evt.redactedBecause()->reason(); if (reason.isEmpty()) return tr("Redacted"); return tr("Redacted: %1").arg(reason.toHtmlEscaped()); } using namespace Quotient; static Settings settings; return evt.switchOnType( [this](const RoomMessageEvent& e) { using namespace Quotient::EventContent; if (e.has() && e.mimeType().name() != "text/plain") { // Naïvely assume that it's HTML auto htmlBody = e.get()->body; auto [cleanHtml, errorPos, errorString] = HtmlFilter::fromMatrixHtml(htmlBody, { m_currentRoom, e.id() }, HtmlFilter::StripMxReply); // 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()); // A manhole to visualise HTML errors if (settings.get("Debug/html")) cleanHtml += QStringLiteral("
" "At pos %1: %2") .arg(QString::number(errorPos), errorString); } return cleanHtml; } if (const auto fileContent = e.get()) { auto fileCaption = fileContent->commonInfo().originalName.toHtmlEscaped(); if (fileCaption.isEmpty()) fileCaption = m_currentRoom->prettyPrint(e.plainBody()); return !fileCaption.isEmpty() ? fileCaption : tr("a file"); } return m_currentRoom->prettyPrint(e.plainBody()); }, [this](const RoomMemberEvent& e) { // FIXME: Rewind to the name that was at the time of this event const auto subjectName = m_currentRoom->member(e.userId()).htmlSafeDisambiguatedName(); // The below code assumes senderName output in AuthorRole switch (e.membership()) { case Membership::Invite: case Membership::Join: { QString text{}; // Part 1: invites and joins if (e.membership() == Membership::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)) { const auto& newDisplayName = e.newDisplayName().value_or(QString()); if (newDisplayName.isEmpty()) text = tr("cleared the display name"); else text = tr("changed the display name to %1").arg(newDisplayName.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 "); text += !e.newAvatarUrl() || e.newAvatarUrl()->isEmpty() ? tr("cleared the avatar") : tr("updated the avatar"); } return text; } case Membership::Leave: if (e.prevContent() && e.prevContent()->membership == Membership::Invite) { return (e.senderId() != e.userId()) ? tr("withdrew %1's invitation").arg(subjectName) : tr("rejected the invitation"); } if (e.prevContent() && e.prevContent()->membership == Membership::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 Membership::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 Membership::Knock: return tr("knocked"); default:; } return tr("made something unknown"); }, [](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 EncryptedEvent&) { return tr("Could not decrypt the event"); }, [](const StateEvent& 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")); } 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; static Settings settings; switch (role) { case Qt::DisplayRole: return visualiseEvent(evt); case Qt::ToolTipRole: return QJsonDocument(evt.fullJson()).toJson(); case EventIdRole: return !evt.id().isEmpty() ? evt.id() : evt.transactionId(); case EventClassNameRole: return evt.metaType().className; case AnnotationRole: return isPending ? pendingIt->annotation() : QString(); case AuthorHasAvatarRole: return m_currentRoom->member(evt.senderId()).avatarUrl().isValid(); case HighlightRole: return m_currentRoom->isEventHighlighted(&evt); case Qt::ForegroundRole: { using CG = QPalette::ColorGroup; using CR = QPalette::ColorRole; if (evt.isRedacted()) return QPalette().color(CG::Disabled, CR::Text); auto normalTextColor = QPalette().color(CG::Active, CR::Text); if (isPending) { using ES = Quotient::EventStatus::Code; if (auto s = pendingIt->deliveryStatus(); s == ES::Submitted || s == ES::SendingFailed || s == ES::Departed) return fadedTextColor(normalTextColor); } // Background highlighting mode is handled entirely in QML if (m_currentRoom->isEventHighlighted(&evt) && settings.get(u"UI/highlight_mode"_s) == u"text") return settings.get(u"UI/highlight_color"_s, u"orange"_s); if (isPending || evt.senderId() == m_currentRoom->localMember().id()) normalTextColor = mixColors(normalTextColor, settings.get(QStringLiteral("UI/outgoing_color"), QStringLiteral("#4A8780")), 0.5); const auto* const rme = eventCast(&evt); return rme && rme->msgtype() != MessageEventType::Notice ? normalTextColor : fadedTextColor(normalTextColor); } case 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->has() ? "file" : "message"; } } if (evt.isStateEvent()) return "state"; return "other"; } case AuthorRole: // TODO: It should be RoomMember state "as of event", not "as of now" return QVariant::fromValue(isPending ? m_currentRoom->localMember() : m_currentRoom->member(evt.senderId())); case ContentTypeRole: { if (auto e = eventCast(&evt)) { // "Promote" text/plain to text/html, because we emit HTML for Qt::DisplayRole anyway const auto& contentType = e->mimeType().name(); return contentType == u"text/plain" ? u"text/html"_s : contentType; } return u"text/plain"_s; } case 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->has() ? QVariant::fromValue(e->content()->originalJson) : QVariant(); } return {}; } case RepliedToRole: return evt.switchOnType([this](const RoomMessageEvent& e) { constexpr auto TurnThreadsToReplies = true; const auto& replyEventId = e.replyEventId(TurnThreadsToReplies); if (replyEventId.isEmpty()) return QVariant(); // The current event is not a reply const auto* repliedToEvent = m_currentRoom->getSingleEvent(replyEventId, e.id()); const auto result = repliedToEvent ? EventForQml{ replyEventId, m_currentRoom->member(repliedToEvent->senderId()), visualiseEvent(*repliedToEvent, true) } : EventForQml{ replyEventId, {}, //: The line to show instead of the replied-to event content //: while getting it from the homeserver tr("(loading)") }; return QVariant::fromValue(result); }); case 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 (e->isReplaced()) { return EventStatus::Replaced; } } if (is(evt) && !settings.get("UI/show_alias_update", true)) return EventStatus::Hidden; auto* memberEvent = timelineIt->viewAs(); if (memberEvent) { if ((memberEvent->isJoin() || memberEvent->isLeave()) && !settings.get("UI/show_joinleave", true)) return EventStatus::Hidden; if ((memberEvent->isInvite() || memberEvent->isRejectedInvite()) && !settings.get("UI/show_invite", true)) return EventStatus::Hidden; if ((memberEvent->isBan() || memberEvent->isUnban()) && !settings.get("UI/show_ban", true)) return EventStatus::Hidden; bool hideRename = memberEvent->isRename() && (!memberEvent->isJoin() && !memberEvent->isLeave()) && !settings.get("UI/show_rename", true); bool hideAvatarUpdate = memberEvent->isAvatarUpdate() && !settings.get("UI/show_avatar_update", true); if ((hideRename && hideAvatarUpdate) || (hideRename && !memberEvent->isAvatarUpdate()) || (hideAvatarUpdate && !memberEvent->isRename())) { return EventStatus::Hidden; } } if (memberEvent || evt.isRedacted()) { if (evt.senderId() != m_currentRoom->localMember().id() && evt.stateKey() != m_currentRoom->localMember().id() && !settings.get("UI/show_spammy")) { // QElapsedTimer et; et.start(); auto hide = !isUserActivityNotable(timelineIt); // qCDebug(EVENTMODEL) // << "Checked user activity for" << evt.id() << "in" << et; if (hide) return EventStatus::Hidden; } } if (evt.isRedacted()) return settings.get("UI/show_redacted") ? EventStatus::Redacted : EventStatus::Hidden; if (auto* stateEvt = eventCast(&evt); stateEvt && stateEvt->repeatsState() && !settings.get("UI/show_noop_events")) return EventStatus::Hidden; if (!evt.isStateEvent() && !is(evt) && !settings.get("UI/show_unknown_events")) return EventStatus::Hidden; return EventStatus::Normal; } case LongOperationRole: if (auto e = eventCast(&evt)) if (e->has()) return QVariant::fromValue( m_currentRoom->fileTransferInfo(isPending ? e->transactionId() : e->id())); return {}; case 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::AnnotationType); for (const auto& a: annotations) if (const auto *const e = eventCast(a)) { auto rIt = std::ranges::find(reactions, e->key(), &Reaction::key); if (rIt == reactions.end()) rIt = reactions.insert(reactions.end(), { e->key() }); rIt->authorsList << m_currentRoom->member(e->senderId()).displayName(); rIt->includesLocalUser |= e->senderId() == m_currentRoom->localMember().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 (authorsCount > 7) { //: When the reaction comes from too many members r.authorsList.replace(3, tr("%Ln more member(s)", "", clamp(authorsCount) - 3)); r.authorsList.erase(r.authorsList.begin() + 4, r.authorsList.end()); } qmlReactions << QJsonObject{ { u"key"_s, r.key }, { u"authorsCount"_s, authorsCount }, { u"authors"_s, QLocale().createSeparatedList(r.authorsList) }, { u"includesLocalUser"_s, r.includesLocalUser } }; } return qmlReactions; } case DateTimeRole: case DateRole: { auto ts = (isPending ? pendingIt->lastUpdated() : makeMessageTimestamp(timelineIt)).toLocalTime(); return role == DateTimeRole ? QVariant(ts) : renderDate(ts); } case EventGroupingRole: for (auto r = row + 1; r < rowCount(); ++r) { auto i = index(r); if (data(i, SpecialMarksRole) != EventStatus::Hidden) return data(i, DateRole) != data(idx, DateRole) ? EventGrouping::ShowDateAndAuthor : data(i, AuthorRole) != data(idx, AuthorRole) ? EventGrouping::ShowAuthor : EventGrouping::KeepPreviousGroup; } return EventGrouping::ShowDateAndAuthor; // No events before case RefRole: return switchOnType( evt, [](const RoomCreateEvent& e) { return e.predecessor().roomId; }, [](const RoomTombstoneEvent& e) { return e.successorRoomId(); }); case VerificationStateRole: { // If evt is EncryptedEvent (i.e. it wasn't decrypted) originalEvent() will return nullptr const auto* const encryptedEvent = evt.originalEvent(); return encryptedEvent ? m_currentRoom->connection()->isVerifiedSession( encryptedEvent->sessionId().toLatin1()) ? VerificationState::Verified : VerificationState::Unverified : VerificationState::NotRelevant; } } // switch(role) return {}; } Quaternion-0.0.97.1/client/models/messageeventmodel.h000066400000000000000000000065301476730121700225300ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2015 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #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, DateTimeRole, DateRole, EventGroupingRole, AuthorRole, AuthorHasAvatarRole, ContentRole, ContentTypeRole, RepliedToRole, HighlightRole, SpecialMarksRole, LongOperationRole, AnnotationRole, RefRole, ReactionsRole, EventClassNameRole, VerificationStateRole, }; 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; Q_INVOKABLE QColor fadedBackColor(QColor unfadedColor, qreal fadeRatio = 0.5) 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 = {}); QString visualiseEvent(const Quotient::RoomEvent& evt, bool abbreviate = false) const; }; struct EventForQml { Quotient::EventId eventId; Quotient::RoomMember sender; QString content; Q_GADGET Q_PROPERTY(Quotient::EventId eventId MEMBER eventId FINAL) Q_PROPERTY(Quotient::RoomMember sender MEMBER sender FINAL) Q_PROPERTY(QString content MEMBER content FINAL) }; namespace EventGrouping { Q_NAMESPACE enum Values { KeepPreviousGroup = 0, ShowAuthor = 1, ShowDateAndAuthor = 2 }; Q_ENUM_NS(Values) } namespace VerificationState { Q_NAMESPACE enum Values { Unverified = 0, Verified = 1, NotRelevant = 2, //!< Unencrypted messages }; Q_ENUM_NS(Values) } Quaternion-0.0.97.1/client/models/orderbytag.cpp000066400000000000000000000216541476730121700215220ustar00rootroot00000000000000/****************************************************************************** * SPDX-FileCopyrightText: 2018-2019 QMatrixClient Project * * SPDX-License-Identifier: GPL-3.0-or-later */ #include "orderbytag.h" #include "roomlistmodel.h" #include #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 } auto findIndexWithWildcards(const QStringList& list, const QString& value) { if (list.empty() || value.isEmpty()) return list.size(); auto i = Quotient::findIndex(list, value); // Try namespace groupings (".*" in the list), from right to left for (QStringList::size_type dotPos = 0; i == list.size() && (dotPos = value.lastIndexOf('.', --dotPos)) != -1;) { i = Quotient::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 QVariant& g1key, const QVariant& g2key) const { const auto& lkey = g1key.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::ranges::copy(tags, 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.97.1/client/models/orderbytag.h000066400000000000000000000025031476730121700211570ustar00rootroot00000000000000/****************************************************************************** * SPDX-FileCopyrightText: 2018-2019 QMatrixClient Project * * SPDX-License-Identifier: GPL-3.0-or-later */ #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 QVariant& g1key, 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.97.1/client/models/roomlistmodel.cpp000066400000000000000000000525641476730121700222550ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2016 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #include "roomlistmodel.h" #include "../quaternionroom.h" #include "../logging_categories.h" #include #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::newRoom, this, &RoomListModel::addRoom); m_roomOrder->connectSignals(connection); const auto& allRooms = connection->allRooms(); for (auto* r: 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()) { qCCritical(MODELS) << "RoomListModel: Invalid tag at position" << index.row(); return; } if (tag.startsWith(RoomGroup::SystemPrefix)) { qCWarning(MODELS) << "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 { qCCritical(MODELS) << "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) { qCWarning(MODELS) << "RoomListModel:" << room->objectName() << "is already listed under group" << g.toString(); continue; } const auto rPos = int(rIt - gIt->rooms.begin()); const auto gIdx = index(int(gIt - m_roomGroups.begin()), 0); beginInsertRows(gIdx, rPos, rPos); gIt->rooms.insert(rIt, room); endInsertRows(); m_roomIndices.insert(room, index(rPos, 0, gIdx)); qCDebug(MODELS) << "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::changed, this, [this, room](Room::Changes changes) { using C = Room::Change; if ((changes & (C::RoomNames | C::PartiallyReadStats | C::UnreadStats | C::Highlights)) > 0) refresh(room); else if (changes & C::Avatar) refresh(room, { Qt::DecorationRole }); }); } void RoomListModel::doRemoveRoom(const QModelIndex &idx) { if (!isValidRoomIndex(idx)) { qCCritical(MODELS) << "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 qCDebug(MODELS) << "RoomListModel: Removing room" << (*rIt)->objectName() << "from group" << group.key.toString(); if (m_roomIndices.remove(*rIt, idx) != 1) { qCCritical(MODELS) << "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(); } inline QString toUiString(qsizetype n, const char* prefix = "", const QLocale& locale = QLocale()) { return n ? prefix % locale.toString(n) : QString(); } 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 (const auto& r: m_roomGroups[index.row()].rooms) unreadRoomsCount += !r->unreadStats().empty(); 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->localMember().id()); using Quotient::JoinState; switch (role) { case Qt::DisplayRole: { static Quotient::Settings settings; QString value = (room->isUnstable() ? "(!)" : "") % disambiguatedName; if (const auto& partiallyReadStats = room->partiallyReadStats(); !partiallyReadStats.empty()) // { const auto& [countSinceFullyRead, _1, isPartiallyReadEstimate] = partiallyReadStats; const auto& [unreadCount, highlightCount, isUnreadEstimate] = room->unreadStats(); const auto partiallyReadCount = countSinceFullyRead - unreadCount; value += " [" % toUiString(unreadCount) % (!isPartiallyReadEstimate && isUnreadEstimate ? "?" : "") % (partiallyReadCount > 0 ? QStringLiteral("+%L1").arg(partiallyReadCount) : QString()) % (isPartiallyReadEstimate ? "?" : "") % (highlightCount > 0 ? QStringLiteral(", %L1").arg(highlightCount) : QString()) % ']'; } if (settings.get("Debug/read_receipts", false) && room->timelineSize() > 0) { const auto localReadReceipt = room->localReadReceiptMarker(); value += " {" % toUiString(room->historyEdge() - localReadReceipt, "", QLocale::C) % '|' % toUiString(room->syncEdge() - localReadReceipt.base(), "", QLocale::C) % '}'; } 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 directChatMembers = room->directChatMembers(); if (!directChatMembers.isEmpty()) { QStringList userNames; userNames.reserve(directChatMembers.size()); for (const auto& m: directChatMembers) userNames.push_back(m.htmlSafeDisplayName()); 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)"); } static const QString MaybeMore = ' ' % /*: Unread messages */ tr("(maybe more)"); if (const auto s = room->partiallyReadStats(); !s.empty()) result += "
" % tr("Events after fully read marker: %L1") .arg(s.notableCount) % (s.isEstimate ? MaybeMore : QString()); if (const auto us = room->unreadStats(); !us.empty()) result += "
" % (room->highlightCount() > 0 ? tr("Unread events/highlights since read " "receipt: %L1/%L2") .arg(us.notableCount) .arg(room->highlightCount()) : tr("Unread events since read receipt: %L1") .arg(us.notableCount)) % (us.isEstimate ? MaybeMore : QString()); // Room ids are pretty safe from rogue HTML; escape it just in case result += "
" % tr("Room id: %1").arg(room->id().toHtmlEscaped()) % "
" % (room->joinState() == JoinState::Join ? tr("You joined this room as %1") : room->joinState() == JoinState::Invite ? tr("You were invited into this room as %1") : tr("You left this room as %1")) .arg(room->localMember().id().toHtmlEscaped()); return result; } case HasUnreadRole: return !room->unreadStats().empty(); case HighlightCountRole: return room->highlightCount(); case JoinStateRole: if (!room->successorId().isEmpty()) return QStringLiteral("upgraded"); return QVariant::fromValue(room->joinState()); 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 qCDebug(MODELS) << "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.97.1/client/models/roomlistmodel.h000066400000000000000000000110731476730121700217100ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2016 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #pragma once #include "abstractroomordering.h" #include "../quaternionroom.h" #include #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::ranges::lower_bound(m_roomGroups, group, m_roomOrder->groupLessThanFactory(), &RoomGroup::key); } auto lowerBoundGroup(const QVariant& group) const { return std::ranges::lower_bound(m_roomGroups, group, m_roomOrder->groupLessThanFactory(), &RoomGroup::key); } auto lowerBoundRoom(RoomGroup& group, Room* room) const { return std::ranges::lower_bound(group.rooms, room, m_roomOrder->roomLessThanFactory(group.key)); } auto lowerBoundRoom(const RoomGroup& group, Room* room) const { return std::ranges::lower_bound(group.rooms, room, m_roomOrder->roomLessThanFactory(group.key)); } }; Quaternion-0.0.97.1/client/models/userlistmodel.cpp000066400000000000000000000156321476730121700222520ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2015 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #include "userlistmodel.h" #include "../logging_categories.h" #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 #include using Quotient::RoomMember; UserListModel::UserListModel(QAbstractItemView* parent) : QAbstractListModel(parent), m_currentRoom(nullptr) { } 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); m_memberIds.clear(); } m_currentRoom = room; if (m_currentRoom) { connect(m_currentRoom, &Room::memberJoined, this, &UserListModel::userAdded); connect(m_currentRoom, &Room::memberLeft, this, &UserListModel::userRemoved); connect(m_currentRoom, &Room::memberNameAboutToUpdate, this, &UserListModel::userRemoved); connect(m_currentRoom, &Room::memberNameUpdated, this, &UserListModel::userAdded); connect(m_currentRoom, &Room::memberListChanged, this, &UserListModel::membersChanged); connect(m_currentRoom, &Room::memberAvatarUpdated, this, &UserListModel::avatarChanged); connect(m_currentRoom->connection(), &Connection::loggedOut, this, [this] { setRoom(nullptr); }); doFilter({}); qCDebug(MODELS) << m_memberIds.count() << "member(s) in the room"; } endResetModel(); } Quotient::RoomMember UserListModel::userAt(QModelIndex index) const { if (index.row() < 0 || index.row() >= m_memberIds.size()) return {}; return m_currentRoom->member(m_memberIds.at(index.row())); } QVariant UserListModel::data(const QModelIndex& index, int role) const { if( !index.isValid() ) return QVariant(); if( index.row() >= m_memberIds.count() ) { qCWarning(MODELS) << "UserListModel, something's wrong: index.row() >= " "m_users.count()"; return QVariant(); } auto m = userAt(index); if( role == Qt::DisplayRole ) { return m.displayName(); } const auto* view = static_cast(parent()); if (role == Qt::DecorationRole) { // Convert avatar image to QIcon const auto dpi = view->devicePixelRatioF(); if (auto av = m.avatar(static_cast(view->iconSize().height() * dpi), [] {}); !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(m.name().toHtmlEscaped(), m.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(static_cast(m.hueF()), 1 - palette.color(QPalette::Window).saturationF(), 0.9f - 0.7f * 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_memberIds.count(); } void UserListModel::userAdded(const RoomMember& member) { auto pos = findUserPos(member.id()); if (pos != m_memberIds.size() && m_memberIds[pos] == member.id()) { qCWarning(MODELS) << "Trying to add the user" << member.id() << "but it's already in the user list"; return; } beginInsertRows(QModelIndex(), pos, pos); m_memberIds.insert(pos, member.id()); endInsertRows(); } void UserListModel::userRemoved(const RoomMember& member) { auto pos = findUserPos(member); if (pos == m_memberIds.size()) { qCWarning(MODELS) << "Trying to remove a room member not in the user list:" << member.id(); return; } beginRemoveRows(QModelIndex(), pos, pos); m_memberIds.removeAt(pos); endRemoveRows(); } void UserListModel::filter(const QString& filterString) { if (m_currentRoom == nullptr) return; beginResetModel(); doFilter(filterString); endResetModel(); } void UserListModel::refresh(const RoomMember& member, QVector roles) { auto pos = findUserPos(member); if ( pos != m_memberIds.size() ) emit dataChanged(index(pos), index(pos), roles); else qCWarning(MODELS) << "Trying to access a room member not in the user list"; } void UserListModel::avatarChanged(const RoomMember& m) { refresh(m, {Qt::DecorationRole}); } int UserListModel::findUserPos(const Quotient::RoomMember& m) const { return findUserPos(m.disambiguatedName()); } int UserListModel::findUserPos(const QString& username) const { return static_cast(Quotient::lowerBoundMemberIndex(m_memberIds, username, m_currentRoom)); } void UserListModel::doFilter(const QString& filterString) { QElapsedTimer et; et.start(); auto filteredMembers = Quotient::rangeTo( std::views::filter(m_currentRoom->joinedMembers(), Quotient::memberMatcher(filterString, Qt::CaseInsensitive))); std::ranges::sort(filteredMembers, Quotient::MemberSorter()); const auto sortedIds = std::views::transform(filteredMembers, &RoomMember::id); #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) m_memberIds.assign(sortedIds.begin(), sortedIds.end()); #else m_memberIds = QList(sortedIds.begin(), sortedIds.end()); #endif qCDebug(MODELS) << "Filtering" << m_memberIds.size() << "user(s) in" << m_currentRoom->displayName() << "took" << et; } Quaternion-0.0.97.1/client/models/userlistmodel.h000066400000000000000000000033711476730121700217140ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2015 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #pragma once #include class QAbstractItemView; namespace Quotient { class Connection; class Room; class RoomMember; } class UserListModel: public QAbstractListModel { Q_OBJECT public: UserListModel(QAbstractItemView* parent); void setRoom(Quotient::Room* room); Quotient::RoomMember userAt(QModelIndex index) const; 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(const Quotient::RoomMember& member); void userRemoved(const Quotient::RoomMember& member); void refresh(const Quotient::RoomMember& member, QVector roles = {}); void avatarChanged(const Quotient::RoomMember& m); private: Quotient::Room* m_currentRoom; QList m_memberIds; int findUserPos(const Quotient::RoomMember &m) const; int findUserPos(const QString& username) const; void doFilter(const QString& filterString); }; Quaternion-0.0.97.1/client/networkconfigdialog.cpp000066400000000000000000000104041476730121700221230ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2017 Kitsune Ral * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "networkconfigdialog.h" #include #include #include #include #include #include #include #include #include #include #include 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, &QButtonGroup::idToggled, 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.97.1/client/networkconfigdialog.h000066400000000000000000000013021476730121700215650ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2017 Kitsune Ral * * SPDX-License-Identifier: LGPL-2.1-or-later */ #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.97.1/client/profiledialog.cpp000066400000000000000000000431511476730121700207110ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2019 Karol Kosek * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #include "profiledialog.h" #include "accountselector.h" #include "logging_categories.h" #include "mainwindow.h" #include "verificationdialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using Quotient::BaseJob, Quotient::User, Quotient::Room; using namespace Qt::StringLiterals; namespace { // TODO: move to libQuotient //! Like std::clamp but admits a different (usually larger) type for the value template inline constexpr T clamp(const auto& v, const T& lo = std::numeric_limits::min(), const T& hi = std::numeric_limits::max()) { return v < lo ? lo : hi < v ? hi : static_cast(v); } } 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).toDateTime() < other.data(Qt::UserRole).toDateTime(); } }; /*! Device table class * * Encapsulates the columns model and formatting */ class ProfileDialog::DeviceTable : public QTableWidget { public: enum Columns : int { Verified = 0, DeviceName, DeviceId, LastTimeSeen, LastIpAddr, ColumnsCount // Only for size validation; do not use for real columns! }; DeviceTable(); ~DeviceTable() override = default; template using ItemType = std::conditional_t; template static constexpr auto itemAlignment = ColumnN == Verified ? Qt::AlignCenter : (Qt::AlignLeft | Qt::AlignVCenter); template static constexpr auto itemFlags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemFlag((ColumnN == DeviceName) & Qt::ItemIsEditable); template requires std::is_constructible_v, DataT...> auto emplaceItem(auto row, const DataT&... data) { auto* item = new ItemType(data...); item->setTextAlignment(itemAlignment); item->setFlags(itemFlags); QTableWidget::setItem(clamp(row, 0), ColumnN, item); return item; } void markupRow(int row, void (QFont::*fontFn)(bool), bool flagValue = true); void markCurrentDevice(int row) { markupRow(row, &QFont::setBold); } void fillPendingData(const QString& currentDeviceId); void refresh(const QVector& devices, ProfileDialog* profileDialog); }; ProfileDialog::DeviceTable::DeviceTable() { // Must be synchronised with DeviceTable::Columns static const QStringList Headers{ {}, tr("Device display name"), tr("Device ID"), tr("Last time seen"), tr("Last IP address") }; QUO_CHECK(Headers.size() == ColumnsCount); setColumnCount(ColumnsCount); setHorizontalHeaderLabels(Headers); auto* headerCtl = horizontalHeader(); headerCtl->setSectionResizeMode(QHeaderView::Interactive); headerCtl->setSectionsMovable(true); headerCtl->setFirstSectionMovable(false); headerCtl->setSortIndicatorShown(true); verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); verticalHeader()->hide(); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setSelectionBehavior(QAbstractItemView::SelectRows); setTabKeyNavigation(false); setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); sortByColumn(DeviceTable::LastTimeSeen, Qt::DescendingOrder); } 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(Quotient::AccountRegistry* accounts, MainWindow* parent) : Dialog(tr("User profiles"), QDialogButtonBox::Reset | QDialogButtonBox::Close, parent, Dialog::StatusLine) , m_settings("UI/ProfileDialog"), m_avatar(new QPushButton) , m_accountSelector(new AccountSelector(accounts)) , m_displayName(new QLineEdit) , m_accessTokenLabel(new QLabel) , m_currentAccount(nullptr) { auto* accountLayout = addLayout(); accountLayout->addRow(tr("Account"), m_accountSelector); connect(m_accountSelector, &AccountSelector::currentAccountChanged, this, &ProfileDialog::load); connect(accounts, &Quotient::AccountRegistry::rowsAboutToBeRemoved, 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); // TODO: connect the title change to any changes in the dialog data // button(QDialogButtonBox::Close)->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()); } void ProfileDialog::setAccount(Quotient::Connection* newAccount) { m_accountSelector->setAccount(newAccount); } Quotient::Connection* ProfileDialog::account() const { return m_currentAccount; } void ProfileDialog::setVerifiedItem(int row, const QString& deviceId) { // TODO: switch to Connection::getDeviceVerificationState() when it's available if (m_currentAccount->deviceId() == deviceId) m_deviceTable->emplaceItem(row, tr("This device")); else if (!m_currentAccount->encryptionEnabled()) { // No E2EE, the column is hidden } else if (m_currentAccount->isVerifiedDevice(m_currentAccount->userId(), deviceId)) { m_deviceTable->emplaceItem(row, QIcon::fromTheme(u"security-high"_s), tr("Verified")); } else if (m_currentAccount->isKnownE2eeCapableDevice(m_currentAccount->userId(), deviceId)) { auto* verifyAction = new QAction(QIcon::fromTheme(u"security-medium"_s), tr("Verify..."), this); using KVSession = Quotient::KeyVerificationSession; connect(verifyAction, &QAction::triggered, this, [this, deviceId, verifyAction] { if (auto session = verifyAction->data().value()) { if (session->state() != KVSession::CANCELED) session->cancelVerification(KVSession::USER); } else initiateVerification(deviceId, verifyAction); }); auto* verifyButton = new QToolButton(); verifyButton->setToolButtonStyle(Qt::ToolButtonFollowStyle); verifyButton->setAutoRaise(true); verifyButton->setDefaultAction(verifyAction); m_deviceTable->setCellWidget(clamp(row, 0), DeviceTable::Verified, verifyButton); } else { m_deviceTable->emplaceItem(row, QIcon::fromTheme(u"security-low"_s), tr("No E2EE")); } } void ProfileDialog::refreshDevices() { m_currentAccount->callApi().then( m_deviceTable, [this](const QVector& devices) { m_devices = devices; m_deviceTable->refresh(m_devices, this); }); } void ProfileDialog::DeviceTable::markupRow(int row, void (QFont::*fontFn)(bool), bool flagValue) { Q_ASSERT(row < rowCount()); for (int c = 0; c < columnCount(); ++c) if (auto* it = item(row, c)) { auto font = it->font(); (font.*fontFn)(flagValue); it->setFont(font); } } void ProfileDialog::DeviceTable::fillPendingData(const QString& currentDeviceId) { setSortingEnabled(false); setRowCount(2); emplaceItem(0, currentDeviceId); emplaceItem(0, QDateTime::currentDateTime()); markCurrentDevice(0); { emplaceItem(1, tr("Loading other devices..."))->setFlags(Qt::NoItemFlags); markupRow(1, &QFont::setItalic); } } void ProfileDialog::DeviceTable::refresh(const QVector& devices, ProfileDialog* profileDialog) { if (!std::in_range(devices.size())) qCCritical(MAIN) << "The number of devices on the account is out of bounds, only the first" << std::numeric_limits::max() << "devices will be shown"; clearContents(); setRowCount(clamp(devices.size(), 0)); const auto* currentAccount = profileDialog->account(); for (int i = 0; i < rowCount(); ++i) { const auto& device = devices[i]; profileDialog->setVerifiedItem(i, device.deviceId); emplaceItem(i, device.displayName); emplaceItem(i, device.deviceId); if (device.lastSeenTs) emplaceItem(i, QDateTime::fromMSecsSinceEpoch(*device.lastSeenTs)); emplaceItem(i, device.lastSeenIp); if (device.deviceId == currentAccount->deviceId()) markCurrentDevice(i); } setColumnHidden(DeviceTable::Verified, !currentAccount->encryptionEnabled()); setSortingEnabled(true); resizeColumnsToContents(); // Reduce the width of the device name column if that would drop the horizontal scrollbar; // if the difference is too large, cut the name column width in half to keep it reasonable if (const auto overspill = sizeHint().width() - width(); overspill > 0) { const auto cw = columnWidth(DeviceTable::DeviceName); setColumnWidth(DeviceTable::DeviceName, overspill < cw / 1.5 ? cw - overspill : cw / 2); } } 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::defaultAvatarChanged, this, [this, user] { updateAvatarButton(user, m_avatar); }); m_displayName->setText(user->name()); m_displayName->setFocus(); connect(user, &User::defaultNameChanged, this, [this, user] { m_displayName->setText(user->name()); }); auto accessToken = account()->accessToken(); if (Q_LIKELY(accessToken.size() > 10)) accessToken.replace(5, accessToken.size() - 10, "..."); m_accessTokenLabel->setText(accessToken); m_deviceTable->fillPendingData(m_currentAccount->deviceId()); refreshDevices(); } void ProfileDialog::apply() { if (!m_currentAccount) { qCWarning(MAIN) << "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()) m_avatar->setIcon(QPixmap(m_newAvatarPath)); }); } inline QString errorToMessage(Quotient::KeyVerificationSession::Error e) { switch (e) { using enum Quotient::KeyVerificationSession::Error; case TIMEOUT: case REMOTE_TIMEOUT: return ProfileDialog::tr("Verification timed out"); case USER: return ProfileDialog::tr("Verification was cancelled"); case REMOTE_USER: return ProfileDialog::tr("Verification was cancelled on the other side"); case MISMATCHED_SAS: case REMOTE_MISMATCHED_SAS: return ProfileDialog::tr("Verification failed: icons did not match"); default: return ProfileDialog::tr("Verification did not succeed"); } } Quotient::KeyVerificationSession* ProfileDialog::initiateVerification(const QString& deviceId, QAction* verifyAction) { using namespace Quotient; auto* session = account()->startKeyVerificationSession(account()->userId(), deviceId); verifyAction->setData(QVariant::fromValue(session)); verifyAction->setText(tr("Cancel")); setStatusMessage(tr("Please accept the verification request on the device you want to verify")); connect(session, &KeyVerificationSession::finished, this, [this, session, verifyAction] { if (session->state() == KeyVerificationSession::DONE) refreshDevices(); else { setStatusMessage(errorToMessage(session->error())); verifyAction->setText(tr("Verify...")); verifyAction->setData(QVariant::fromValue(nullptr)); } }); // TODO: when the library supports other methods, ask to choose instead of opting // for SAS straight away QtFuture::connect(session, &KeyVerificationSession::stateChanged).then([this, session] { using enum KeyVerificationSession::State; if (auto s = session->state(); s == READY) { setStatusMessage({}); session->sendStartSas(); } else if (s != WAITINGFORACCEPT && s != ACCEPTED && s != CANCELED && s != DONE) { qCritical(MAIN) << "Unexpected state of key verification session:" << terse << s; session->cancelVerification(KeyVerificationSession::UNEXPECTED_MESSAGE); } }); QtFuture::connect(session, &KeyVerificationSession::sasEmojisChanged) .then([this, session] { QUO_ALARM_X(session->sasEmojis().empty(), "Empty SAS emoji sequence, the session seems to be broken"); auto dialog = new VerificationDialog(session, this); dialog->setModal(true); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); }); connect(this, &QDialog::finished, session, [session] { session->cancelVerification(KeyVerificationSession::USER); }); return session; } Quaternion-0.0.97.1/client/profiledialog.h000066400000000000000000000035541476730121700203610ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2019 Karol Kosek * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #pragma once #include "dialog.h" #include #include #include class AccountSelector; class MainWindow; class QComboBox; class QLineEdit; namespace Quotient { class AccountRegistry; class GetDevicesJob; class Connection; class KeyVerificationSession; } class ProfileDialog : public Dialog { Q_OBJECT public: explicit ProfileDialog(Quotient::AccountRegistry* accounts, MainWindow* parent); ~ProfileDialog() override; void setAccount(Quotient::Connection* newAccount); Quotient::Connection* account() const; private slots: void load() override; void apply() override; void uploadAvatar(); Quotient::KeyVerificationSession* initiateVerification(const QString& deviceId, QAction* verifyAction); private: Quotient::SettingsGroup m_settings; class DeviceTable; DeviceTable* m_deviceTable; QPushButton* m_avatar; AccountSelector* m_accountSelector; QLineEdit* m_displayName; QLabel* m_accessTokenLabel; Quotient::Connection* m_currentAccount; QString m_newAvatarPath; QPointer m_devicesJob; QVector m_devices; void setVerifiedItem(int row, const QString& deviceId); void refreshDevices(); }; Quaternion-0.0.97.1/client/qml/000077500000000000000000000000001476730121700161525ustar00rootroot00000000000000Quaternion-0.0.97.1/client/qml/AnimatedTransition.qml000066400000000000000000000002171476730121700224620ustar00rootroot00000000000000import QtQuick 2.0 import Quotient 1.0 Transition { property var settings: TimelineSettings { } enabled: settings.enable_animations } Quaternion-0.0.97.1/client/qml/AnimationBehavior.qml000066400000000000000000000002151476730121700222620ustar00rootroot00000000000000import QtQuick 2.0 import Quotient 1.0 Behavior { property var settings: TimelineSettings { } enabled: settings.enable_animations } Quaternion-0.0.97.1/client/qml/Attachment.qml000066400000000000000000000024401476730121700207550ustar00rootroot00000000000000import QtQuick 2.0 import Quotient 1.0 Item { width: parent.width height: visible ? childrenRect.height : 0 required property bool openOnFinished readonly property bool downloaded: progressInfo && !progressInfo.isUpload && progressInfo.completed signal opened onDownloadedChanged: { if (downloaded && openOnFinished) openLocalFile() } function openExternally() { if (progressInfo.localPath.toString() || downloaded) openLocalFile() else room.downloadFile(eventId) } function openLocalFile() { if (!Qt.openUrlExternally(progressInfo.localPath)) { controller.showStatusMessage( "Couldn't determine how to open the file, " + "opening its folder instead", 5000) if (!Qt.openUrlExternally(progressInfo.localDir)) { controller.showStatusMessage( "Couldn't determine how to open the file or its folder.", 5000) return; } } opened() } Connections { target: controller function onOpenExternally(currentIndex) { if (currentIndex === index) openExternally() } } } Quaternion-0.0.97.1/client/qml/Avatar.qml000066400000000000000000000013051476730121700201020ustar00rootroot00000000000000import QtQuick 2.15 Image { id: avatar readonly property var forRoom: root.room /* readonly */ property var forMember property string sourceId: forMember?.avatarUrl ?? forRoom?.avatarUrl ?? "" source: sourceId cache: false // Quotient::Avatar takes care of caching fillMode: Image.PreserveAspectFit function reload() { source = "" source = Qt.binding(function() { return sourceId }) } Connections { target: forRoom function onAvatarChanged() { avatar.reload() } function onMemberAvatarUpdated(member) { if (avatar.forMember && member?.id === avatar.forMember.id) avatar.reload() } } } Quaternion-0.0.97.1/client/qml/FastNumberAnimation.qml000066400000000000000000000002371476730121700225750ustar00rootroot00000000000000import QtQuick 2.0 import Quotient 1.0 NumberAnimation { property var settings: TimelineSettings { } duration: settings.fast_animations_duration_ms } Quaternion-0.0.97.1/client/qml/FileContent.qml000066400000000000000000000071321476730121700211020ustar00rootroot00000000000000import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.1 Attachment { openOnFinished: openButton.checked TextEdit { id: fileTransferInfo width: parent.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) } selectByMouse: true; readOnly: true; font: timelabel.font color: foreground 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; HoverHandler { id: fileContentHoverHandler cursorShape: Qt.IBeamCursor } ToolTip.visible: fileContentHoverHandler.hovered ToolTip.text: room && eventId ? room.fileSource(eventId) : "" TapHandler { acceptedButtons: Qt.RightButton onTapped: 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 TimelineItemToolButton { id: openButton text: progressInfo && !progressInfo.isUpload && transferProgress.visible ? qsTr("Open after downloading") : qsTr("Open") checkable: !downloaded && !(progressInfo && progressInfo.isUpload) onClicked: { if (checked) openExternally() } } TimelineItemToolButton { text: qsTr("Cancel") visible: progressInfo && progressInfo.started onClicked: room.cancelFileTransfer(eventId) } TimelineItemToolButton { text: qsTr("Save as...") visible: !progressInfo || (!progressInfo.isUpload && !progressInfo.started) onClicked: controller.saveFileAs(eventId) } TimelineItemToolButton { text: qsTr("Open folder") visible: progressInfo && progressInfo.localDir onClicked: Qt.openUrlExternally(progressInfo.localDir) } } onOpened: openButton.checked = false } Quaternion-0.0.97.1/client/qml/ImageContent.qml000066400000000000000000000033551476730121700212500ustar00rootroot00000000000000import QtQuick 2.15 import QtQuick.Controls 2.15 Attachment { id: content required property var sourceSize required property url source required property var maxHeight required property bool autoload openOnFinished: false Image { width: parent.width height: sourceSize.height * Math.min(parent.maxHeight / sourceSize.height * 0.9, Math.min(width / sourceSize.width, 1)) fillMode: Image.PreserveAspectFit horizontalAlignment: Image.AlignLeft // The spec says that the attachment URL SHOULD be mxc but is not required to be source: parent.source.toString().startsWith("mxc") ? room.makeMediaUrl(eventId, parent.source) : parent.source sourceSize: parent.sourceSize HoverHandler { id: imageHoverHandler cursorShape: Qt.PointingHandCursor } ToolTip.visible: imageHoverHandler.hovered ToolTip.text: room && eventId ? room.fileSource(eventId) : "" ToolTip.delay: Application.styleHints.mousePressAndHoldInterval TapHandler { acceptedButtons: Qt.LeftButton onTapped: { content.openOnFinished = true content.openExternally() } } TapHandler { acceptedButtons: Qt.RightButton onTapped: controller.showMenu(index, textFieldImpl.hoveredLink, textFieldImpl.selectedText, showingDetails) } Component.onCompleted: if (visible && content.autoload && !content.downloaded && !(progressInfo && progressInfo.isUpload)) room.downloadFile(eventId) } } Quaternion-0.0.97.1/client/qml/Logger.qml000066400000000000000000000012301476730121700201000ustar00rootroot00000000000000import QtQml 2.15 import QtQuick 2.15 // This component can be used either as a logging category (pass it to // console.log() or any other console method) or as a basic logger itself: // if you only need log/warn/error kind of things you can save a few keystrokes // by writing logger.warn(...) instead of console.warn(logger, ...) LoggingCategory { name: 'quaternion.timeline.qml' defaultLogLevel: LoggingCategory.Info function debug() { return console.log(this, arguments) } function log() { return debug(arguments) } function warn() { return console.warn(this, arguments) } function error() { return console.error(this, arguments) } } Quaternion-0.0.97.1/client/qml/NormalNumberAnimation.qml000066400000000000000000000002321476730121700231230ustar00rootroot00000000000000import QtQuick 2.0 import Quotient 1.0 NumberAnimation { property var settings: TimelineSettings { } duration: settings.animations_duration_ms } Quaternion-0.0.97.1/client/qml/RoomHeader.qml000066400000000000000000000142541476730121700207200ustar00rootroot00000000000000import QtQuick 2.15 import QtQuick.Controls 2.3 import Quotient 1.0 Frame { id: roomHeader required property Room room property bool showTopic: true height: headerText.height + 11 padding: 3 visible: !!room Avatar { 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(implicitHeight > 0 ? headerText.height / implicitHeight * implicitWidth : 0, parent.width / 2.618) // Golden ratio - just for fun // Safe upper limit (see also topicField) sourceSize: Qt.size(-1, settings.lineSpacing * 9) 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 readonly property int innerLeftPadding: 4 TextArea { id: roomName width: roomNameMetrics.advanceWidth + leftPadding height: roomNameMetrics.height clip: true padding: 0 leftPadding: headerText.innerLeftPadding TextMetrics { id: roomNameMetrics font: roomName.font elide: Text.ElideRight elideWidth: headerText.width text: roomHeader.room?.displayName ?? "" } text: roomNameMetrics.elidedText placeholderText: qsTr("(no name)") font.bold: true renderType: settings.render_type readOnly: true hoverEnabled: text !== "" && (roomNameMetrics.text != roomNameMetrics.elidedText || roomName.lineCount > 1) ToolTip.visible: hovered ToolTip.text: roomHeader.room?.displayNameForHtml ?? "" } Label { id: versionNotice property alias room: roomHeader.room visible: room?.isUnstable || room?.successorId !== "" width: parent.width leftPadding: headerText.innerLeftPadding text: room?.successorId !== "" ? qsTr("This room has been upgraded.") : room?.isUnstable ? qsTr("Unstable room version!") : "" elide: Text.ElideRight font.italic: true renderType: settings.render_type HoverHandler { id: versionHoverHandler enabled: parent.truncated } ToolTip.text: text ToolTip.visible: versionHoverHandler.hovered } ScrollView { id: topicField visible: roomHeader.showTopic width: parent.width // Allow 5 full (actually, 6 minus padding) lines of the topic // but not more than 20% of the timeline vertical space height: Math.min(topicText.implicitHeight, root.height / 5, settings.lineSpacing * 6) ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AsNeeded AnimationBehavior on height { NormalNumberAnimation { easing.type: Easing.OutQuad } } // FIXME: The below TextArea+MouseArea is a massive copy-paste // from textFieldImpl and its respective MouseArea in // TimelineItem.qml. Maybe make a separate component for these // (RichTextField?). TextArea { id: topicText padding: 2 leftPadding: headerText.innerLeftPadding rightPadding: topicField.ScrollBar.vertical.visible ? topicField.ScrollBar.vertical.width : padding text: roomHeader.room?.prettyPrint(roomHeader.room?.topic) ?? "" placeholderText: qsTr("(no topic)") textFormat: TextEdit.RichText renderType: settings.render_type readOnly: true wrapMode: TextEdit.Wrap selectByMouse: true hoverEnabled: true onLinkActivated: (link) => controller.resourceRequested(link) } } } MouseArea { anchors.fill: headerText acceptedButtons: Qt.MiddleButton | Qt.RightButton cursorShape: topicText.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor onClicked: (mouse) => { if (topicText.hoveredLink) controller.resourceRequested(topicText.hoveredLink, "_interactive") else if (mouse.button === Qt.RightButton) headerContextMenu.popup() } Menu { id: headerContextMenu MenuItem { text: roomHeader.showTopic ? qsTr("Hide topic") : qsTr("Show topic") onTriggered: roomHeader.showTopic = !roomHeader.showTopic } } } Button { id: versionActionButton property alias room: roomHeader.room visible: (room?.isUnstable && room?.canSwitchVersions()) || room?.successorId !== "" anchors.verticalCenter: headerText.verticalCenter anchors.right: parent.right width: visible * implicitWidth text: room?.successorId !== "" ? qsTr("Go to\nnew room") : qsTr("Room\nsettings") onClicked: if (room.successorId !== "") controller.resourceRequested(room.successorId, "join") else controller.roomSettingsRequested() } } Quaternion-0.0.97.1/client/qml/ScrollFinisher.qml000066400000000000000000000104701476730121700216150ustar00rootroot00000000000000import QtQuick 2.15 Timer { property int targetIndex: -1 property int positionMode: ListView.End property int round: 1 readonly property var lc: root.lc /** @brief Scroll to the position making sure the timeline is actually at it * * Qt is not fabulous at positioning the list view when the delegate * sizes vary too much; this function runs scrollFinisher timer to adjust * the position as needed shortly after the list was positioned. * Nothing good in that, just a workaround. * * This function is the entry point to get the whole component do its job. */ function scrollViewTo(newTargetIndex, newPositionMode) { console.log(lc, "Jumping to item", newTargetIndex) parent.animateNextScroll = true parent.positionViewAtIndex(newTargetIndex, newPositionMode) targetIndex = newTargetIndex positionMode = newPositionMode round = 1 start() } function logFixup(nameForLog, topContentY, bottomContentY) { const contentX = parent.contentX const topShownIndex = parent.indexAt(contentX, topContentY) const bottomShownIndex = parent.indexAt(contentX, bottomContentY - 1) if (bottomShownIndex !== -1 && (targetIndex > topShownIndex || targetIndex < bottomShownIndex)) console.log(lc, "Fixing up item", targetIndex, "to be", nameForLog, "- fixup round #" + scrollFinisher.round, "(" + topShownIndex + "-" + bottomShownIndex, "range is shown now)") } /** @return true if no further action is needed; false if the finisher has to be restarted. */ function adjustPosition() { if (targetIndex === 0) { if (parent.bottommostVisibleIndex === 0) return true // Positioning is correct // This normally shouldn't happen even with the current // imperfect positioning code in Qt console.warn(lc, "Fixing up the viewport to be at sync edge") parent.positionViewAtBeginning() } else { const height = parent.height const contentY = parent.contentY const viewport1stThird = contentY + height / 3 const item = parent.itemAtIndex(targetIndex) if (item) { // The viewport is divided into thirds; ListView.End should // place targetIndex at the top third, Center corresponds // to the middle third; Beginning is not used for now. switch (positionMode) { case ListView.Contain: if (item.y >= contentY || item.y + item.height < contentY + height) return true // Positioning successful logFixup("fully visible", contentY, contentY + height) break case ListView.Center: const viewport2ndThird = contentY + 2 * height / 3 const itemMidPoint = item.y + item.height / 2 if (itemMidPoint >= viewport1stThird && itemMidPoint < viewport2ndThird) return true logFixup("in the centre", viewport1stThird, viewport2ndThird) break case ListView.End: if (item.y >= contentY && item.y < viewport1stThird) return true logFixup("at the top", contentY, viewport1stThird) break default: console.warn(lc, "fixupPosition: Unsupported positioning mode:", positionMode) return true // Refuse to do anything with it } } // If the target item moved away too far and got destroyed, repeat positioning parent.animateNextScroll = true parent.positionViewAtIndex(targetIndex, positionMode) return false } } interval: 120 // small enough to avoid visual stutter onTriggered: { if (parent.count === 0) return if (adjustPosition() || ++round > 3 /* Give up after 3 rounds */) { targetIndex = -1 parent.saveViewport(true) } else // Positioning is still in flux, might need another round start() } } Quaternion-0.0.97.1/client/qml/Timeline.qml000066400000000000000000000463771476730121700204540ustar00rootroot00000000000000import QtQuick 2.15 import QtQuick.Controls 2.3 import Quotient 1.0 Page { id: root property Room room: messageModel?.room readonly property Logger lc: Logger { } TimelineSettings { id: settings Component.onCompleted: console.log(root.lc, "Using timeline font: " + font) } background: Rectangle { color: palette.base; border.color: palette.mid } contentWidth: width font: settings.font header: RoomHeader { room: root.room } ListView { id: chatView anchors.fill: parent model: messageModel delegate: TimelineItem { width: chatView.width - scrollerArea.width // #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 */ } } // We do not actually render sections because section delegates return // -1 for indexAt(), disrupting quite a few things including read marker // logic, saving the current position etc. Besides, ListView sections // cannot be effectively nested. TimelineItem.qml implements // the necessary logic around eventGrouping without these shortcomings. section.property: "date" readonly property int bottommostVisibleIndex: count > 0 ? atYEnd ? 0 : indexAt(contentX, contentY + height - 1) : -1 readonly property bool noNeedMoreContent: !root.room || root.room.eventsHistoryJob || root.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 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.readMarkerVisualIndex <= indexAt(contentX, contentY) ? contentY + contentHeight : originY }) } 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) root.room.getPreviousContent(velocity * eventDensity * 3) } onContentYChanged: ensurePreviousContent() onContentHeightChanged: ensurePreviousContent() function saveViewport(force) { root.room?.saveViewport(indexAt(contentX, contentY), bottommostVisibleIndex, force) } ScrollFinisher { id: scrollFinisher } function scrollUp(dy) { if (contentHeight > height && dy !== 0) { animateNextScroll = true contentY -= dy } } function scrollDown(dy) { scrollUp(-dy) } function onWheel(wheel) { if (wheel.angleDelta.x === 0) { // NB: Scrolling up yields positive angleDelta.y if (contentHeight > height && wheel.angleDelta.y !== 0) contentY -= wheel.angleDelta.y * settings.lineSpacing / 40 wheel.accepted = true } else { wheel.accepted = false } } Connections { target: controller function onPageUpPressed() { chatView.scrollUp(chatView.height - sectionBanner.childrenRect.height) } function onPageDownPressed() { chatView.scrollDown(chatView.height - sectionBanner.childrenRect.height) } function onViewPositionRequested(index) { scrollFinisher.scrollViewTo(index, ListView.Contain) } function onHistoryRequestChanged() { scrollToReadMarkerButton.checked = controller.isHistoryRequestRunning() } } Connections { target: messageModel function onModelAboutToBeReset() { chatView.parkReadMarker() console.log(lc, "Read marker parked at index", messageModel.readMarkerVisualIndex) chatView.saveViewport(true) } function onModelReset() { // NB: at this point, the actual delegates are not instantiated // yet, so defer all actions to when at least some are scrollFinisher.scrollViewTo(0, ListView.Contain) } } Component.onCompleted: console.log(root.lc, "QML view loaded") 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 } } property bool animateNextScroll: false Behavior on contentY { enabled: settings.enable_animations && chatView.animateNextScroll animation: FastNumberAnimation { id: scrollAnimation duration: settings.fast_animations_duration_ms / 3 onStopped: { chatView.animateNextScroll = false 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() } readonly property color readMarkerColor: palette.highlight 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.05 radius: readMarkerLine.height color: chatView.readMarkerColor } 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: chatView.readMarkerColor } } } // 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: palette.window opacity: 0.8 Label { font.bold: true opacity: 0.8 renderType: settings.render_type text: chatView.underlayingItem?.ListView.section ?? "" } } } // === Timeline map === // Only used with the shuttle scroller for now Rectangle { id: requestedEventsBar // Stack 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: palette.mid } Rectangle { id: cachedEventsBar // A proxy property for animation property int requestedHistoryEventsCount: root.room?.requestedHistorySize ?? 0 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.width height: chatView.bottommostVisibleIndex < 0 ? 0 : averageEvtHeight * (chatView.count - chatView.bottommostVisibleIndex) visible: shuttleDial.visible color: palette.mid } // === Scrolling extensions === Slider { id: shuttleDial orientation: Qt.Vertical height: chatView.height * 0.7 width: settings.lineSpacing padding: 2 anchors.right: parent.right anchors.verticalCenter: chatView.verticalCenter enabled: settings.use_shuttle_dial visible: enabled && chatView.count > 0 // Npages/sec = value^2 => maxNpages/sec = 9 readonly property real maxValue: 3.0 readonly property real handlePos: topPadding + visualPosition * (availableHeight - handle.height) from: -maxValue to: maxValue background: Rectangle { // Draw a "spring" line between the shuttle and the center of the control anchors.top: (shuttleDial.value > 0 ? shuttleHandle : shuttleDial).verticalCenter anchors.bottom: (shuttleDial.value > 0 ? shuttleDial : shuttleHandle).verticalCenter anchors.horizontalCenter: parent.horizontalCenter width: shuttleDial.availableWidth radius: 2 color: palette.highlight } handle: Rectangle { id: shuttleHandle y: shuttleDial.handlePos width: shuttleDial.availableWidth anchors.horizontalCenter: shuttleDial.horizontalCenter height: width * 1.618 radius: width color: palette.button border.color: scrollerArea.containsMouse ? palette.highlight : palette.button } opacity: scrollerArea.containsMouse ? 1 : 0.7 AnimationBehavior on opacity { FastNumberAnimation { } } 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) } } component TextInScrollArea: Text { height: cachedEventsBar.width // Because of the rotation, height becomes width rotation: 90 horizontalAlignment: Text.AlignLeft verticalAlignment: Text.AlignVCenter padding: 2 renderType: settings.render_type font.pointSize: settings.font.pointSize - 1 } TextInScrollArea { id: totalEventsCount visible: chatView.count > 0 // NB: anchoring occurs before rotation anchors.bottom: parent.top anchors.left: cachedEventsBar.left transformOrigin: Item.BottomLeft text: chatView.count } TextInScrollArea { id: eventsToBottomCount visible: chatView.bottommostVisibleIndex > 0 // NB: same as above, anchoring occurs before rotation anchors.top: scrollToBottomButton.top anchors.right: scrollToBottomButton.right transformOrigin: Item.TopRight text: chatView.bottommostVisibleIndex } MouseArea { id: scrollerArea anchors.verticalCenter: chatView.verticalCenter anchors.right: parent.right width: (settings.use_shuttle_dial ? shuttleDial : chatView.ScrollBar.vertical).width height: (settings.use_shuttle_dial ? shuttleDial : chatView).height acceptedButtons: Qt.NoButton hoverEnabled: true } Rectangle { id: timelineStats anchors.right: scrollerArea.left anchors.top: chatView.top width: childrenRect.width height: childrenRect.height color: palette.alternateBase opacity: 0 // Nothing to show at the start property bool shown: (chatView.bottommostVisibleIndex >= 0 && (scrollerArea.containsMouse || scrollAnimation.running)) || root.room?.requestedHistorySize > 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 { padding: 2 font.bold: true 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) : "") + (root.room?.requestedHistorySize > 0 ? (chatView.count > 0 ? "\n" : "") + qsTr("%Ln events requested from the server", "", room.requestedHistorySize) : "") horizontalAlignment: Label.AlignRight } } component ScrollToButton: Button { id: control anchors.right: scrollerArea.right height: settings.fontHeight * 2 width: scrollerArea.width hoverEnabled: true opacity: visible * (0.7 + hovered * 0.2) display: Button.IconOnly icon { width: control.availableWidth height: control.availableHeight color: palette.buttonText } AnimationBehavior on opacity { NormalNumberAnimation { easing.type: Easing.OutQuad } } AnimationBehavior on anchors.topMargin { NormalNumberAnimation { easing.type: Easing.OutQuad } } AnimationBehavior on anchors.bottomMargin { NormalNumberAnimation { easing.type: Easing.OutQuad } } } ScrollToButton { id: scrollToBottomButton anchors.bottom: chatView.bottom anchors.bottomMargin: visible ? 0 : -height visible: !chatView.atYEnd icon { name: "go-bottom" source: "qrc:///scrolldown.svg" } onClicked: { chatView.positionViewAtBeginning() chatView.saveViewport(true) } } ScrollToButton { id: scrollToReadMarkerButton anchors.top: parent.top anchors.topMargin: visible ? totalEventsCount.width + 10 : -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) scrollFinisher.scrollViewTo(messageModel.readMarkerVisualIndex, ListView.Center) else { checkable = true controller.ensureLastReadEvent() } } onCheckedChanged: { if (!checked) checkable = false } } } Quaternion-0.0.97.1/client/qml/TimelineItem.qml000066400000000000000000000646651476730121700212730ustar00rootroot00000000000000import QtQuick 2.15 import QtQuick.Controls 2.3 import Quotient 1.0 Item { visible: marks !== EventStatus.Hidden enabled: visible height: childrenRect.height * visible readonly property bool authorSectionVisible: eventGrouping >= EventGrouping.ShowAuthor readonly property var time: dateTime.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) readonly property bool pending: marks > EventStatus.Normal && marks < EventStatus.Redacted readonly property bool failed: marks === EventStatus.SendingFailed 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 > chatView.contentY && y < chatView.contentY + chatView.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 > chatView.contentY && y + height - 1 < chatView.contentY + chatView.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(root.lc, "Read marker line bound at index", index) } else { chatView.parkReadMarker() console.log(root.lc, "Read marker parked at index", index + ", content pos", chatView.readMarkerContentPos, "(full range is", chatView.originY, "-", chatView.originY + chatView.contentHeight, "as of now)") } } } onPartiallyShownChanged: readMarkerHereChanged() Component.onCompleted: { if (bottomEdgeShown) bottomEdgeShownChanged() readMarkerHereChanged() } // FIXME: boilerplate with models/userlistmodel.cpp:115 function memberColor(member) { // contrast but not too heavy return Qt.hsla(member?.hueF ?? 0, (1-palette.window.hslSaturation), (-0.7*palette.window.hslLightness + 0.9), palette.buttonText.a) } property bool showingDetails Connections { target: controller function onShowDetails(currentIndex) { if (currentIndex === index) { showingDetails = !showingDetails if (!settings.enable_animations) { detailsAreaLoader.visible = showingDetails detailsAreaLoader.opacity = showingDetails } else detailsAnimation.start() } } function onAnimateMessage(currentIndex) { 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: eventGrouping === EventGrouping.ShowDateAndAuthor color: palette.alternateBase Label { font.bold: true renderType: settings.render_type text: date } } Loader { id: detailsAreaLoader // asynchronous: true // https://bugreports.qt.io/browse/QTBUG-50992 active: visible visible: false // Managed by onShowDetails() opacity: 0 width: parent.width sourceComponent: detailsArea } Item { id: message width: parent.width height: childrenRect.height component AuthorInteractionArea: Item { anchors.fill: parent HoverHandler { id: authorInteractionHoverHandler cursorShape: Qt.PointingHandCursor } ToolTip.visible: authorInteractionHoverHandler.hovered ToolTip.text: author.id TapHandler { acceptedButtons: Qt.LeftButton|Qt.MiddleButton onTapped: (mouse, button) => { // Qt 5 passes the pressed button inside mouse.event; // Qt 6 passes it as a separate parameter if (!button && mouse.event) button = mouse.event.button controller.resourceRequested( author.id, button === Qt.LeftButton ? "mention" : "_interactive") } } } // 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: settings.timelineStyleIsXChat width: if (!visible) { 0 } anchors.top: authorAvatar.top anchors.left: parent.left color: settings.lowlight_color renderType: settings.render_type font.italic: pending text: "<" + time + ">" } Avatar { id: authorAvatar visible: (authorSectionVisible || settings.timelineStyleIsXChat) && settings.show_author_avatars anchors.left: timelabel.right anchors.leftMargin: 3 width: settings.show_author_avatars * settings.minimalTimelineItemHeight horizontalAlignment: Image.AlignRight forMember: author sourceSize: Qt.size(width, visible ? settings.minimalTimelineItemHeight : 0) AuthorInteractionArea { } } Label { id: authorLabel visible: settings.timelineStyleIsXChat || (authorSectionVisible && (!actionEvent || authorHasAvatar)) anchors.left: authorAvatar.right anchors.leftMargin: 2 anchors.top: authorAvatar.top width: settings.timelineStyleIsXChat ? 120 - authorAvatar.width : Math.min(textField.width, implicitWidth) horizontalAlignment: actionEvent ? Text.AlignRight : Text.AlignLeft elide: Text.ElideRight color: memberColor(author) textFormat: Label.PlainText font.bold: !settings.timelineStyleIsXChat renderType: settings.render_type text: (actionEvent && settings.timelineStyleIsXChat ? "* " : "") + author?.displayName AuthorInteractionArea { } } Item { // 0.0.97: it used to be RectangularGlow, maybe bring // MultiEffect once we are both legs in Qt 6? id: highlighter anchors.fill: textField visible: highlight && settings.highlight_mode != "text" Rectangle { anchors.fill: parent opacity: 0.2 color: settings.highlight_color radius: 2 } } Item { id: messageFlasher anchors.fill: textField visible: false Rectangle { anchors.fill: parent opacity: 0.5 color: settings.highlight_color radius: 2 } } Item { id: textField height: textFieldImpl.height anchors.top: !settings.timelineStyleIsXChat && authorLabel.visible ? authorLabel.bottom : authorLabel.top anchors.left: (settings.timelineStyleIsXChat ? authorLabel : authorAvatar).right anchors.leftMargin: 2 anchors.right: parent.right anchors.rightMargin: 1 clip: true // TextArea clips the offscreen part thereby breaking horizontal // scrolling, hence using TextEdit here 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, '>') } function inlineAuthorLabel(author) { return author ? "
" + author.htmlSafeDisplayName + " " : "" } selectByMouse: true readOnly: true textFormat: TextEdit.RichText // FIXME: The text is clumsy and slows down creation; move it to C++ text: (repliedTo ? "
" + inlineAuthorLabel(repliedTo.sender) + "
" + repliedTo.content + "
" : "") + (!settings.timelineStyleIsXChat ? ("" + (verificationState === VerificationState.Unverified ? "" : "") + "
⚠️" + (time ? toHtmlEscaped(time) : "") + "
" + (actionEvent && !authorLabel.visible ? inlineAuthorLabel(author) : "")) : "") + (actionEvent ? "" : "") + display + (actionEvent ? "" : "") + (marks === EventStatus.Replaced ? " (" + qsTr("edited") + ")" : "") horizontalAlignment: Text.AlignLeft wrapMode: Text.Wrap color: foreground font: settings.font renderType: settings.render_type onHoveredLinkChanged: controller.showStatusMessage(hoveredLink) onLinkActivated: (link) => { controller.resourceRequested(link) } TimelineTextEditSelector {} AnimationBehavior on color { ColorAnimation { duration: settings.animations_duration_ms } } } TimelineMouseArea { anchors.fill: parent cursorShape: textFieldImpl.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor acceptedButtons: Qt.MiddleButton | Qt.RightButton onClicked: (mouse) => { 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: (wheel) => { 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 || autoload || progressInfo.active ? content?.info : content?.info?.thumbnail_info sourceSize: if (info && info.w && info.h) { Qt.size(info.w, info.h) } source: downloaded || progressInfo.isUpload ? progressInfo.localPath : !progressInfo.failed ? autoload ? content.url : content.info.thumbnail_url ?? "" : "" // TODO: show thumbnail or failing that blurhash before loading maxHeight: chatView.height - textField.height - authorLabel.height * !settings.timelineStyleIsXChat 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.italic: true leftPadding: 2 rightPadding: 2 text: annotation } Flow { anchors.top: annotationLabel.bottom anchors.left: textField.left anchors.right: textField.right Repeater { id: reactionsView model: reactions ToolButton { id: reactionButton padding: 3 leftPadding: 4 rightPadding: 4 readonly property color fgColor: modelData.includesLocalUser ? palette.highlightedText : foreground contentItem: Text { text: modelData.key + " \u00d7" /* Math "multiply" */ + modelData.authorsCount textFormat: Text.PlainText font.pointSize: settings.font.pointSize - 1 color: reactionButton.fgColor } background: Rectangle { radius: 4 color: reactionButton.hovered ? palette.mid : reactionButton.down ? palette.button : modelData.includesLocalUser ? palette.highlight : "transparent" border.color: palette.mid border.width: 1 } hoverEnabled: true ToolTip { visible: reactionButton.hovered contentItem: Text { //: %2 is the list of users text: qsTr("Reaction '%1' from %2") .arg(modelData.key).arg(modelData.authors) textFormat: Text.PlainText wrapMode: Text.Wrap color: palette.toolTipText } } onClicked: controller.reactionButtonClicked( eventId, modelData.key) } } } Loader { id: buttonAreaLoader active: failed || // resendButton (pending && marks !== EventStatus.ReachedServer && marks !== EventStatus.Departed) || // discardButton (!pending && eventClassName === "RoomCreateEvent" && refId) || // goToPredecessorButton (!pending && eventClassName === "RoomTombstoneEvent") // goToSuccessorButton anchors.top: textField.top anchors.right: parent.right height: textField.height sourceComponent: buttonArea } } } // Components loaded on demand Component { id: buttonArea Item { component EventActionButton: TimelineItemToolButton { anchors.top: parent.top anchors.rightMargin: 2 width: visible * implicitWidth height: visible * parent.height } EventActionButton { id: resendButton visible: failed anchors.right: discardButton.left text: qsTr("Resend") onClicked: room.retryMessage(eventId) } EventActionButton { id: discardButton visible: pending && marks !== EventStatus.ReachedServer && marks !== EventStatus.Departed anchors.right: parent.right text: qsTr("Discard") onClicked: room.discardMessage(eventId) } EventActionButton { id: goToPredecessorButton visible: !pending && eventClassName === "RoomCreateEvent" && refId anchors.right: parent.right text: qsTr("Go to\nolder room") // TODO: Treat unjoined invite-only rooms specially onClicked: controller.resourceRequested(refId, "join") } EventActionButton { id: goToSuccessorButton visible: !pending && eventClassName === "RoomTombstoneEvent" 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: palette.button border.color: palette.mid Item { id: detailsHeader width: parent.width height: childrenRect.height readonly property var boldFontInfo: FontMetrics { font.family: settings.font.family font.pointSize: settings.font.pointSize font.bold: true } readonly property var boldFont: boldFontInfo.font TextEdit { text: "<" + dateTime.toLocaleString(Qt.locale(), Locale.ShortFormat) + ">" font: parent.boldFont renderType: settings.render_type readOnly: true selectByKeyboard: true; selectByMouse: true anchors.top: eventTitle.bottom anchors.left: parent.left anchors.leftMargin: 3 z: 1 } TextEdit { readonly property url evtLink: "https://matrix.to/#/" + room.id + "/" + eventId id: eventTitle text: ""+ eventId + "" textFormat: Text.RichText font: parent.boldFont renderType: settings.render_type horizontalAlignment: Text.AlignHCenter readOnly: true selectByKeyboard: true; selectByMouse: true width: parent.width onLinkActivated: (link) => { Qt.openUrlExternally(link) } MouseArea { anchors.fill: parent cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor acceptedButtons: Qt.NoButton } } TextEdit { text: eventClassName textFormat: Text.PlainText font: parent.boldFont renderType: settings.render_type anchors.top: eventTitle.bottom anchors.right: parent.right anchors.rightMargin: 3 } } 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 { readonly property string sourceText: toolTip text: sourceText textFormat: Text.PlainText readOnly: true; font.family: "Monospace" renderType: settings.render_type selectByKeyboard: true; selectByMouse: true } } } } } Quaternion-0.0.97.1/client/qml/TimelineItemToolButton.qml000066400000000000000000000006011476730121700233010ustar00rootroot00000000000000import QtQuick 2.6 import QtQuick.Controls 2.15 Button { id: tButton contentItem: Text { text: tButton.text fontSizeMode: Text.VerticalFit minimumPointSize: settings.font.pointSize - 3 color: foreground renderType: settings.render_type verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter } } Quaternion-0.0.97.1/client/qml/TimelineMouseArea.qml000066400000000000000000000001761476730121700222410ustar00rootroot00000000000000import QtQuick 2.2 MouseArea { onWheel: (wheel) => { chatView.onWheel(wheel) } onReleased: controller.focusInput() } Quaternion-0.0.97.1/client/qml/TimelineSettings.qml000066400000000000000000000046171476730121700221640ustar00rootroot00000000000000import 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 bool timelineStyleIsXChat: timeline_style === "xchat" 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 /// 2 text line heights by default; 1 line height for XChat readonly property real minimalTimelineItemHeight: lineSpacing * (2 - timelineStyleIsXChat) readonly property var render_type_impl: value("UI/Fonts/render_type", "native") readonly property int render_type: ["native", "Native", "NativeRendering"].indexOf(render_type_impl) != -1 ? Text.NativeRendering : Text.QtRendering readonly property bool use_shuttle_dial: value("UI/use_shuttle_dial", true) readonly property bool autoload_images: value("UI/autoload_images", true) readonly property var disabledPalette: SystemPalette { colorGroup: SystemPalette.Disabled } function mixColors(baseColor, mixedColor, mixRatio) { return Qt.tint(baseColor, Qt.rgba(mixedColor.r, mixedColor.g, mixedColor.b, mixRatio)) } readonly property string highlight_mode: value("UI/highlight_mode", "background") readonly property color highlight_color: value("UI/highlight_color", "orange") readonly property color lowlight_color: mixColors(disabledPalette.text, palette.text, 0.3) readonly property bool show_author_avatars: value("UI/show_author_avatars", !timelineStyleIsXChat) } Quaternion-0.0.97.1/client/qml/TimelineTextEditSelector.qml000066400000000000000000000034311476730121700236100ustar00rootroot00000000000000import 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 int selectionMode: TextEdit.SelectCharacters anchors.fill: parent acceptedButtons: Qt.LeftButton preventStealing: true onPressed: (mouse) => { 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.linkActivated(textEdit.hoveredLink) } onDoubleClicked: { selectionMode = TextEdit.SelectWords textEdit.selectWord() } onReleased: { selectionMode = TextEdit.SelectCharacters controller.setGlobalSelectionBuffer(textEdit.selectedText) chatView.textEditWithSelection = textEdit controller.focusInput() } onPositionChanged: (mouse) => { 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.97.1/client/quaternionroom.cpp000066400000000000000000000260741476730121700211600ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2016 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #include "quaternionroom.h" #include "logging_categories.h" #include #include #include #include #include using namespace Quotient; QuaternionRoom::QuaternionRoom(Connection* connection, QString roomId, JoinState joinState) : Room(connection, std::move(roomId), joinState) {} 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) { qCDebug(MAIN) << "Saving viewport as the latest available"; setFirstDisplayedEventId({}); setLastDisplayedEventId({}); return; } qCDebug(MAIN) << "Saving viewport:" << topIndex << "thru" << bottomIndex; setFirstDisplayedEvent(maxTimelineIndex() - topIndex); setLastDisplayedEvent(maxTimelineIndex() - bottomIndex); } bool QuaternionRoom::canRedact(const Quotient::EventId& eventId) const { if (const auto it = findInTimeline(eventId); it != historyEdge()) { const auto localMemberId = localMember().id(); const auto memberId = it->event()->senderId(); if (localMemberId == memberId) return true; const auto& ple = currentState().get(); const auto currentUserPl = ple->powerLevelForUser(localMemberId); return currentUserPl >= ple->redact() && currentUserPl >= ple->powerLevelForUser(memberId); } return false; } void QuaternionRoom::onGettingSingleEvent(const QString& evtId) { std::erase_if(singleEventRequests, [this, evtId](SingleEventRequest& r) { const bool idMatches = r.eventId == evtId; if (idMatches) { if (!r.requestHandle.isFinished()) r.requestHandle.abandon(); std::ranges::for_each(r.eventIdsToRefresh, std::bind_front(&Room::updatedEvent, this)); } return idMatches; }); } const RoomEvent* QuaternionRoom::getSingleEvent(const QString& eventId, const QString& originEventId) { if (auto timelineIt = findInTimeline(eventId); timelineIt != historyEdge()) return timelineIt->event(); if (auto cachedIt = cachedEvents.find(eventId); cachedIt != cachedEvents.cend()) return cachedIt->second.get(); auto requestIt = std::ranges::find(singleEventRequests, eventId, &SingleEventRequest::eventId); if (requestIt == singleEventRequests.cend()) requestIt = singleEventRequests.insert( requestIt, { eventId, connection() ->callApi(id(), eventId) .then([this](RoomEventPtr&& pEvt) { if (pEvt == nullptr) { qCCritical(MAIN, "/rooms/event returned an empty event"); return; } const auto [it, cachedEventInserted] = cachedEvents.insert_or_assign(pEvt->id(), std::move(pEvt)); const auto evtId = it->first; if (QUO_ALARM(!cachedEventInserted)) emit updatedEvent(evtId); // At least notify clients... onGettingSingleEvent(evtId); }) }); requestIt->eventIdsToRefresh.push_back(originEventId); return nullptr; } void QuaternionRoom::onAddNewTimelineEvents(timeline_iter_t from) { std::for_each(from, messageEvents().cend(), std::bind_front(&QuaternionRoom::checkForHighlights, this)); } void QuaternionRoom::onAddHistoricalTimelineEvents(rev_iter_t from) { std::for_each(from, messageEvents().crend(), std::bind_front(&QuaternionRoom::checkForHighlights, this)); checkForRequestedEvents(from); } void QuaternionRoom::checkForHighlights(const Quotient::TimelineItem& ti) { const auto localUserId = localMember().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 = member(localUserId).disambiguatedName(); if (!roomMemberExpressions.contains(memberName)) { // FIXME: unravels if the room member name contains characters special // to regexp($, e.g.) roomMemberExpressions[memberName] = QRegularExpression("(\\W|^)" + memberName + "(\\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); } } QuaternionRoom::EventFuture QuaternionRoom::ensureHistory(const QString& upToEventId, quint16 maxWaitSeconds) { if (auto eventIt = findInTimeline(upToEventId); eventIt != historyEdge()) return makeReadyVoidFuture(); if (allHistoryLoaded()) return {}; // Request a small number of events (or whatever the ongoing request says, if there's any), // to make sure checkForRequestedEvents() gets executed getPreviousContent(); HistoryRequest r{ upToEventId, QDeadlineTimer{ std::chrono::seconds(maxWaitSeconds), Qt::VeryCoarseTimer } }; auto future = r.promise.future(); r.promise.start(); historyRequests.push_back(std::move(r)); return future; } namespace { using namespace std::ranges; template requires(std::convertible_to, QString>) inline auto dumpJoined(const RangeT& range, const QString& separator = u","_s) { return #if defined(__cpp_lib_ranges_join_with) && defined(__cpp_lib_ranges_to_container) to(join_with_view(range, separator)); #else QStringList(begin(range), end(range)).join(separator); #endif } } void QuaternionRoom::checkForRequestedEvents(const rev_iter_t& from) { using namespace std::ranges; const auto addedRange = subrange(from, historyEdge()); for (const auto& evtId : transform_view(addedRange, &RoomEvent::id)) { cachedEvents.erase(evtId); onGettingSingleEvent(evtId); } std::erase_if(historyRequests, [this, addedRange](HistoryRequest& request) { auto& [upToEventId, deadline, promise] = request; if (promise.isCanceled()) { qCInfo(MAIN) << "The request to ensure event" << upToEventId << "has been cancelled"; return true; } if (auto it = find(addedRange, upToEventId, &RoomEvent::id); it != historyEdge()) { promise.finish(); return true; } if (deadline.hasExpired()) { qCWarning(MAIN) << "Timeout - giving up on obtaining event" << upToEventId; promise.future().cancel(); return true; } return false; }); if (!historyRequests.empty()) { auto requestedIds = dumpJoined(transform_view(historyRequests, &HistoryRequest::upToEventId)); if (allHistoryLoaded()) { qCDebug(MAIN).noquote() << "Could not find in the whole room history:" << requestedIds; for_each(historyRequests, [](auto& r) { r.promise.future().cancel(); }); historyRequests.clear(); } static constexpr auto EventsProgression = std::array{ 50, 100, 200, 500, 1000 }; static_assert(is_sorted(EventsProgression)); const auto thisMany = requestedHistorySize() >= EventsProgression.back() ? EventsProgression.back() : *upper_bound(EventsProgression, requestedHistorySize()); qCDebug(MAIN).noquote() << "Requesting" << thisMany << "events, looking for" << requestedIds; getPreviousContent(thisMany); } } void QuaternionRoom::sendMessage(const QTextDocumentFragment& richText, HtmlFilter::Options htmlFilterOptions) { const auto& plainText = richText.toPlainText(); const auto& html = HtmlFilter::toMatrixHtml(richText.toHtml(), { this }, htmlFilterOptions); Q_ASSERT(!plainText.isEmpty()); // Send plain text if htmlText has no markup or just
elements // (those are easily represented as line breaks in plain text) using namespace Quotient; static const QRegularExpression MarkupRE{ "<(?![Bb][Rr])"_L1 }; // TODO: use Room::postText() once we're on lib 0.9.3+ post(plainText, MessageEventType::Text, html.contains(MarkupRE) ? std::make_unique(html, u"text/html"_s) : nullptr); } Quaternion-0.0.97.1/client/quaternionroom.h000066400000000000000000000070361476730121700206220ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2016 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #pragma once #include "htmlfilter.h" #include #include #include class QTextDocumentFragment; class QuaternionRoom: public Quotient::Room { Q_OBJECT public: using RoomEvent = Quotient::RoomEvent; 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); bool canRedact(const Quotient::EventId& eventId) const; using EventFuture = QFuture; //! \brief Loads the message history until the specified event id is found //! //! This is potentially heavy; use it sparingly. One intended use case is loading the timeline //! until the last read event, assuming that the last read event is not too far back and that //! the user will read or at least scroll through the just loaded events anyway. This will not //! be necessary once we move to sliding sync but sliding sync support is still a bit away in //! the future. //! //! Because the process is heavy (particularly on the homeserver), ensureHistory() will cancel //! after \p maxWaitSeconds. //! \return the future that resolves to the event with \p eventId, or self-cancels if the event //! is not found Q_INVOKABLE EventFuture ensureHistory(const QString& upToEventId, quint16 maxWaitSeconds = 20); //! \brief Obtain an arbitrary room event by its id that is available locally //! Q_INVOKABLE const Quotient::RoomEvent* getSingleEvent(const QString& eventId, const QString& originEventId); void sendMessage(const QTextDocumentFragment& richText, HtmlFilter::Options htmlFilterOptions = HtmlFilter::Default); private: using EventPromise = QPromise; using EventId = Quotient::EventId; struct HistoryRequest { EventId upToEventId; QDeadlineTimer deadline; EventPromise promise{}; }; std::vector historyRequests; struct SingleEventRequest { EventId eventId; Quotient::JobHandle requestHandle; std::vector eventIdsToRefresh{}; }; std::vector singleEventRequests; std::unordered_map> cachedEvents; 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); void checkForRequestedEvents(const rev_iter_t& from); void onGettingSingleEvent(const QString& evtId); }; Quaternion-0.0.97.1/client/resources.qrc000066400000000000000000000025121476730121700201020ustar00rootroot00000000000000 qml/Timeline.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/ScrollFinisher.qml qml/Logger.qml qml/Avatar.qml qml/RoomHeader.qml Quaternion-0.0.97.1/client/roomdialogs.cpp000066400000000000000000000457641476730121700204240ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2017 Kitsune Ral * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "roomdialogs.h" #include "accountselector.h" #include "logging_categories.h" #include "mainwindow.h" #include "models/orderbytag.h" // For tagToCaption() #include "quaternionroom.h" #include #include // Only needed because loadCapabilities() implies it #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(); selector->addItem(tr("(loading)", "Loading room versions from the server"), QString()); selector->setEnabled(false); account->loadCapabilities().then([selector, account] { selector->clear(); const auto& versions = account->availableRoomVersions(); if (versions.empty()) { selector->addItem(tr("(no available room versions)"), QString()); } else 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(!versions.isEmpty()); }); } 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, [this, room] { 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() { if (const auto* plEvt = room->currentState().get()) { const int userPl = plEvt->powerLevelForUser(room->localMember().id()); roomName->setText(room->name()); roomName->setReadOnly(plEvt->powerLevelForState("m.room.name") > userPl); alias->setText(room->canonicalAlias()); alias->setReadOnly(plEvt->powerLevelForState("m.room.canonical_alias") > userPl); topic->setPlainText(room->topic()); topic->setReadOnly(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; }); connect(room, &Room::upgradeFailed, this, &Dialog::applyFailed, Qt::SingleShotConnection); 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()); else QListWidget::keyPressEvent(event); } void mousePressEvent(QMouseEvent* event) override { if (event->button() == Qt::MiddleButton) delete takeItem(currentRow()); else QListWidget::mousePressEvent(event); } }; CreateRoomDialog::CreateRoomDialog(Quotient::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) , addToInviteesButton( new QPushButton(tr("Add", "Add a user to the list of invitees"))) , removeFromInviteesButton( new QPushButton(tr("Remove", "Remove a user from the list of invitees"))) , invitees(new InviteeList) { Q_ASSERT(!accounts->empty()); 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); // Add button initialization addToInviteesButton->setFocusPolicy(Qt::NoFocus); addToInviteesButton->setDisabled(true); connect(addToInviteesButton, &QPushButton::clicked, [this] { auto userName = nextInvitee->currentText(); if (userName.indexOf('@') == -1) { userName.prepend('@'); if (userName.indexOf(':') == -1) userName += ':' + accountChooser->currentAccount()->domain(); } invitees->addItem(userName); nextInvitee->clear(); }); // Remove button initialization removeFromInviteesButton->setFocusPolicy(Qt::NoFocus); removeFromInviteesButton->setDisabled(true); connect(removeFromInviteesButton, &QPushButton::clicked, [this] { if (invitees->currentItem() == nullptr) return; delete invitees->takeItem(invitees->currentRow()); }); connect(invitees, &InviteeList::currentItemChanged, this, &CreateRoomDialog::updatePushButtons); invitees->setSizeAdjustPolicy( QAbstractScrollArea::AdjustToContentsOnFirstShow); invitees->setUniformItemSizes(true); invitees->setSortingEnabled(true); // Layout additional controls auto* inviteLayout = new QHBoxLayout; inviteLayout->addWidget(nextInvitee); inviteLayout->addWidget(addToInviteesButton); inviteLayout->addWidget(removeFromInviteesButton); 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() { addToInviteesButton->setEnabled(!nextInvitee->currentText().isEmpty()); removeFromInviteesButton->setEnabled(invitees->currentItem() != nullptr); if (addToInviteesButton->isEnabled() && nextInvitee->hasFocus()) addToInviteesButton->setDefault(true); else buttonBox()->button(QDialogButtonBox::Ok)->setDefault(true); } void CreateRoomDialog::load() { 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; auto* const account = accountChooser->currentAccount(); QStringList userIds; for (int i = 0; i < invitees->count(); ++i) if (const auto& userId = invitees->item(i)->text(); account->user(userId)) userIds.push_back(userId); else qCWarning(MAIN).nospace() << std::source_location::current().function_name() << ": " << userId << "is not a correct user id, skipping"; account ->createRoom(publishRoom->isChecked() ? Connection::PublishRoom : Connection::UnpublishRoom, alias->text(), roomName->text(), topic->toPlainText(), userIds, "", version->currentData().toString(), false) .then(this, &Dialog::accept, [this](const BaseJob* 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 (const auto& uId: connection->userIds()) { if (!Quotient::isGuestUserId(uId)) { // It would be great to show a user's full name rather than MXID; unfortunately, // this implies fetching profiles for the whole list of users known to a given // account, one by one, and that can easily be thousands. auto* item = new QStandardItem(uId); model->appendRow(item); } } qCDebug(MAIN) << "Completion candidates:" << model->rowCount() << "out of" << connection->userIds().size() << "filled in" << et; // } } nextInvitee->setModel(model); nextInvitee->setEditText(savedCurrentText); completer->setCompletionPrefix(savedCurrentText); } Quaternion-0.0.97.1/client/roomdialogs.h000066400000000000000000000050021476730121700200460ustar00rootroot00000000000000/* * SPDX-FileCopyrightText: 2017 Kitsune Ral * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "dialog.h" #include namespace Quotient { class AccountRegistry; class Connection; } class MainWindow; class QuaternionRoom; 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(Quotient::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* addToInviteesButton; QPushButton* removeFromInviteesButton; QListWidget* invitees; QHash userLists; }; Quaternion-0.0.97.1/client/roomlistdock.cpp000066400000000000000000000305001476730121700205740ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2015 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #include "roomlistdock.h" #include "logging_categories.h" #include #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")) qCCritical(MAIN) << 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()) { QMessageBox::StandardButton confirmation = QMessageBox::question( this, tr("Forget this room?"), tr("Are you sure you want to forget room %1?").arg(room->displayName())); if (confirmation == QMessageBox::Yes) { if (QUO_CHECK(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::deleteConnection(Quotient::Connection* connection) { model->deleteConnection(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', Qt::SkipEmptyParts); 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.97.1/client/roomlistdock.h000066400000000000000000000041251476730121700202450ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2015 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #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); void deleteConnection(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.97.1/client/systemtrayicon.cpp000066400000000000000000000102071476730121700211620ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2016 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #include "systemtrayicon.h" #include #include #include #include "mainwindow.h" #include "quaternionroom.h" #include "desktop_integration.h" #include #include using namespace Qt::StringLiterals; SystemTrayIcon::SystemTrayIcon(MainWindow* parent) : QSystemTrayIcon(parent) { auto contextMenu = new QMenu(parent); auto showHideAction = contextMenu->addAction(tr("Hide"), this, &SystemTrayIcon::showHide); contextMenu->addAction(tr("Quit"), this, QApplication::quit); mainWindow()->winId(); // To make sure mainWindow()->windowHandle() is initialised connect(mainWindow()->windowHandle(), &QWindow::visibleChanged, [showHideAction](bool visible) { showHideAction->setText(visible ? tr("Hide") : tr("Show")); }); setIcon(appIcon()); setToolTip("Quaternion"); setContextMenu(contextMenu); connect(this, &SystemTrayIcon::activated, this, &SystemTrayIcon::systemTrayIconAction); connect(qApp, &QApplication::focusChanged, this, &SystemTrayIcon::focusChanged); } void SystemTrayIcon::newRoom(Quotient::Room* room) { unreadStatsChanged(); highlightCountChanged(room); connect(room, &Quotient::Room::unreadStatsChanged, this, &SystemTrayIcon::unreadStatsChanged); connect(room, &Quotient::Room::highlightCountChanged, this, [this, room] { highlightCountChanged(room); }); } void SystemTrayIcon::unreadStatsChanged() { const auto mode = notificationMode(); if (mode == u"none") return; int nNotifs = 0; for (auto* c: mainWindow()->registry()->accounts()) for (auto* r: c->allRooms()) nNotifs += r->notificationCount(); setToolTip(tr("%Ln unread message(s) across all rooms", "", nNotifs)); if (m_notified || qApp->activeWindow() != nullptr) return; if (nNotifs == 0) { setIcon(appIcon()); return; } static const auto unreadIcon = QIcon::fromTheme(u"mail-unread"_s, appIcon()); setIcon(unreadIcon); m_notified = true; } void SystemTrayIcon::highlightCountChanged(Quotient::Room* room) { if (qApp->activeWindow() != nullptr || room->highlightCount() == 0) return; const auto mode = notificationMode(); if (mode == u"none") return; //: %1 is the room display name showMessage(tr("Highlight in %1").arg(room->displayName()), tr("%Ln highlight(s)", "", static_cast(room->highlightCount()))); if (mode == u"intrusive") mainWindow()->activateWindow(); connect(this, &SystemTrayIcon::messageClicked, mainWindow(), [this, r = static_cast(room)] { mainWindow()->selectRoom(r); }, Qt::SingleShotConnection); } void SystemTrayIcon::systemTrayIconAction(QSystemTrayIcon::ActivationReason reason) { if (reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::DoubleClick) showHide(); } void SystemTrayIcon::showHide() { if (mainWindow()->isVisible()) mainWindow()->hide(); else { mainWindow()->show(); mainWindow()->activateWindow(); mainWindow()->raise(); mainWindow()->setFocus(); } } MainWindow* SystemTrayIcon::mainWindow() const { return static_cast(parent()); } QString SystemTrayIcon::notificationMode() const { static const Quotient::Settings settings{}; return settings.get("UI/notifications", u"intrusive"_s); } void SystemTrayIcon::focusChanged(QWidget* old) { if (m_notified && old == nullptr && qApp->activeWindow() != nullptr) { setIcon(appIcon()); m_notified = false; } } Quaternion-0.0.97.1/client/systemtrayicon.h000066400000000000000000000022551476730121700206330ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2016 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #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 unreadStatsChanged(); void highlightCountChanged(Quotient::Room* room); void systemTrayIconAction(QSystemTrayIcon::ActivationReason reason); void focusChanged(QWidget* old); private: bool m_notified; void showHide(); MainWindow* mainWindow() const; QString notificationMode() const; }; Quaternion-0.0.97.1/client/timelinewidget.cpp000066400000000000000000000305741476730121700211100ustar00rootroot00000000000000#include "timelinewidget.h" #include "chatroomwidget.h" #include "logging_categories.h" #include "models/messageeventmodel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using Quotient::operator""_ls; QNetworkAccessManager* TimelineWidget::NamFactory::create(QObject* parent) { return new Quotient::NetworkAccessManager(parent); } TimelineWidget::TimelineWidget(ChatRoomWidget* chatRoomWidget) : QQuickWidget(chatRoomWidget) , m_messageModel(new MessageEventModel(this)) , indexToMaybeRead(-1) , readMarkerOnScreen(false) { using namespace Quotient; qmlRegisterUncreatableType( "Quotient", 1, 0, "Room", "Room objects can only be created by libQuotient"); qmlRegisterAnonymousType("Quotient", 1); qmlRegisterAnonymousType("Quotient", 1); qmlRegisterAnonymousType("Quotient", 1); qmlRegisterType("Quotient", 1, 0, "Settings"); setResizeMode(SizeRootObjectToView); engine()->setNetworkAccessManagerFactory(&namFactory); auto* ctxt = rootContext(); ctxt->setContextProperty("messageModel"_ls, m_messageModel); ctxt->setContextProperty("controller"_ls, this); setSource(QUrl("qrc:///qml/Timeline.qml"_ls)); 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(); } ChatRoomWidget* TimelineWidget::roomWidget() const { return static_cast(parent()); } void TimelineWidget::setRoom(QuaternionRoom* newRoom) { if (currentRoom() == newRoom) return; if (currentRoom()) { currentRoom()->setDisplayed(false); currentRoom()->disconnect(this); } readMarkerOnScreen = false; maybeReadTimer.stop(); indicesOnScreen.clear(); indexToMaybeRead = -1; m_messageModel->changeRoom(newRoom); if (newRoom) { connect(newRoom, &Quotient::Room::fullyReadMarkerMoved, this, [this] { const auto rm = currentRoom()->fullyReadMarker(); readMarkerOnScreen = rm != currentRoom()->historyEdge() && std::ranges::lower_bound(indicesOnScreen, 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()) { qCWarning(TIMELINE) << "ChatRoomWidget::saveFileAs without an active room ignored"; return; } const auto fileName = QFileDialog::getSaveFileName( this, 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(); } const auto pos = std::ranges::lower_bound(indicesOnScreen, 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(); auto menu = new QMenu(this); menu->setAttribute(Qt::WA_DeleteOnClose); if (currentRoom()->canRedact(eventId)) 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::AnnotationType); for (const auto& a: annotations) if (auto* e = eventCast(a); e != nullptr && e->key() == key && a->senderId() == currentRoom()->localMember().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::ensureLastReadEvent() { auto r = currentRoom(); if (!r) return; if (!historyRequest.isCanceled()) { // Second click cancels the request historyRequest.cancel(); return; } // Store the future as is, without continuations, so that it could be cancelled historyRequest = r->ensureHistory(r->lastFullyReadEventId()); historyRequest .then([this](auto) { qCDebug(TIMELINE, "Loaded enough history to get the last fully read event, now scrolling"); emit viewPositionRequested( m_messageModel->findRow(currentRoom()->lastFullyReadEventId())); emit historyRequestChanged(); }) .onCanceled([this] { emit historyRequestChanged(); }); } bool TimelineWidget::isHistoryRequestRunning() const { return historyRequest.isRunning(); } 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); qCDebug(TIMELINE) << "Scheduled maybe-read message update:" << indexToMaybeRead << "->" << indicesOnScreen.back(); } void TimelineWidget::timerEvent(QTimerEvent* qte) { if (qte->timerId() != maybeReadTimer.timerId()) { QQuickWidget::timerEvent(qte); return; } maybeReadTimer.stop(); // Only update the maybe-read message if we're tracking it if (readMarkerOnScreen && !indicesOnScreen.empty() && indexToMaybeRead < indicesOnScreen.back()) // { qCDebug(TIMELINE) << "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()->fullyReadMarker(); return rm != currentRoom()->historyEdge() && rm->index() < indexToMaybeRead; } Qt::KeyboardModifiers TimelineWidget::getModifierKeys() const { return QGuiApplication::keyboardModifiers(); } Quaternion-0.0.97.1/client/timelinewidget.h000066400000000000000000000044351476730121700205520ustar00rootroot00000000000000#pragma once #include "activitydetector.h" #include #include #include #include class ChatRoomWidget; class MessageEventModel; class QuaternionRoom; class TimelineWidget : public QQuickWidget { Q_OBJECT public: TimelineWidget(ChatRoomWidget* chatRoomWidget); ~TimelineWidget() override; QString selectedText() const; QuaternionRoom* currentRoom() const; Q_INVOKABLE Qt::KeyboardModifiers getModifierKeys() const; Q_INVOKABLE bool isHistoryRequestRunning() 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); void historyRequestChanged(); 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); void ensureLastReadEvent(); private: MessageEventModel* m_messageModel; QString m_selectedText; using timeline_index_t = Quotient::TimelineItem::index_t; std::vector indicesOnScreen; timeline_index_t indexToMaybeRead; QBasicTimer maybeReadTimer; bool readMarkerOnScreen; ActivityDetector activityDetector; QFuture historyRequest; class NamFactory : public QQmlNetworkAccessManagerFactory { public: QNetworkAccessManager* create(QObject* parent) override; }; NamFactory namFactory; ChatRoomWidget* roomWidget() const; void reStartShownTimer(); void timerEvent(QTimerEvent* qte) override; bool pendingMarkRead() const; }; Quaternion-0.0.97.1/client/translations/000077500000000000000000000000001476730121700201025ustar00rootroot00000000000000Quaternion-0.0.97.1/client/translations/quaternion_de.ts000066400000000000000000001766031476730121700233240ustar00rootroot00000000000000 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> 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 … 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 character %1: %2 Bei Zeichen %1: %2 %L1 more %L1 weitere %1 is not readable or not a file %1 ist nicht lesbar oder keine Datei Attaching the pasted image Anhängen des eingefügten Bildes Can't attach a file without a selected room Kann keine Datei ohne einen ausgewählten Raum anhängen Cannot insert HTML - it's either invalid or unsupported HTML kann nicht eingefügt werden – es ist entweder ungültig oder wird nicht unterstützt Unknown /command. If you intended to send a message, start with // instead of / Unbekannter /Befehl. Um eine Nachricht zu senden, beginnen Sie mit // anstelle von / 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. Remove Entfernen 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 … This account is logged in already Dieses Konto ist bereits angemeldet (none) (keiner) Saved device id Gespeicherte Geräte-ID MainWindow Loading... Lädt … &Accounts &Konten &Login... &Anmelden … &Quit &Beenden &View &Oberfläche &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 … 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 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 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 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 Loading %Ln accounts, please wait %Ln Konto wird geladen, bitte warten %Ln Konten werden geladen, bitte warten Account %1 is synchronised, have a good chat Konto %1 ist synchronisiert, viel Spaß beim Chatten All %Ln accounts synchronised, have a good chat %Ln Konto ist synchronisiert, viel Spaß beim Chatten Alle %Ln Kontos sind synchronisiert, viel Spaß beim Chatten &Room list &Raumliste &Member list &Mitgliederliste Dock panels Dock-Paneele Can't find the event without knowing the room Kann das Ereignis nicht finden, ohne den Raum zu kennen Open the room that has this event to scroll to %1 Öffnen Sie den Raum, in dem sich dieses Ereignis befindet, um zu %1 zu scrollen 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 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 (loading) (lädt) 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? (no available room versions) (keine verfügbaren Raumversionen) 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) Forget this room? Diesen Raum vergessen? Are you sure you want to forget room %1? Sind Sie sicher, dass Sie den Raum %1 vergessen möchten? 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) Main alias: %1 Hauptalias: %1 Direct chat with %1 Direkt-Chat mit %1 The room enforces encryption Der Raum erzwingt Verschlüsselung 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 (maybe more) (vielleicht mehr) Events after fully read marker: %L1 Ereignisse nach vollständig gelesenem Marker: %L1 Unread events/highlights since read receipt: %L1/%L2 Ungelesene Ereignisse/Hervorhebungen nach vollständig gelesenem Marker: %L1/%L2 Unread events since read receipt: %L1 Ungelesene Ereignisse nach vollständig gelesenem Marker: %L1 Room id: %1 Raum-ID: %1 You joined this room as %1 Sie sind diesem Raum als %1 beigetreten You were invited into this room as %1 Sie wurden als %1 in diesen Raum eingeladen You left this room as %1 Sie haben diesen Raum als %1 verlassen 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 Unknown Unbekannt %Ln byte(s) %Ln Byte %Ln Bytes %L1 kB %L1 kB %L1 MB %L1 MB %L1 GB %L1 GB 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 %Ln unread message(s) across all rooms %Ln ungelesene Nachricht in allen Räumen %Ln ungelesene Nachrichten in allen Räumen 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 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 Loading other devices... Lade andere Geräte … No avatar Kein Avatar Set avatar Avatar einstellen Cancel Abbrechen Please accept the verification request on the device you want to verify Bitte akzeptieren Sie die Verifizierungsanfrage auf dem Gerät, das Sie verifizieren möchten. This device Dieses Gerät No E2EE Keine E2EE Verification was cancelled on the other side Die Verifizierung wurde auf der Gegenseite abgebrochen Timeline Latest events Neueste Ereignisse %Ln events back from now %Ln Ereignis zurückgescrollt %Ln Ereignisse zurückgescrollt %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 ChatEdit Reset formatting Formatierung zurücksetzen Reset the current character formatting to the default Zurücksetzen der aktuellen Zeichenformatierung auf die Standardeinstellung Paste as rich text Als Rich-Text einfügen Paste as plain text Als Klartext einfügen DockModeMenu &Off &Aus &Docked Ange&dockt &Floating &Schwebend Completely hide this list Diese Liste vollständig ausblenden The list is shown within the main window Die Liste wird im Hauptfenster angezeigt The list is shown separately from the main window Die Liste wird getrennt vom Hauptfenster angezeigt RoomHeader (no name) (kein Name) This room has been upgraded. Der Raum wurde aktualisiert. Unstable room version! Instabile Raumversion! (no topic) (kein Thema) Hide topic Thema ausblenden Show topic Thema anzeigen Room settings Raum- Einstellungen Go to new room Gehe zu neuem Raum VerificationDialog Confirm that the same icons, in the same order are displayed on the other side Vergewissern Sie sich, dass die gleichen Symbole in der gleichen Reihenfolge auf der anderen Seite angezeigt werden Quaternion-0.0.97.1/client/translations/quaternion_en.ts000066400000000000000000002711701476730121700233310ustar00rootroot00000000000000 ChatEdit Reset formatting Reset formatting Reset the current character formatting to the default Reset the current character formatting to the default Paste as rich text Paste as rich text Paste as plain text Paste as plain text 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 Currently typing: Currently typing: 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... Can't attach a file without a selected room Can't attach a file without a selected room 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 character %1: %2 %1 is a position of the error; %2 is the error message At character %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. Attaching the pasted image Attaching the pasted image %L1 more The number of users in the typing or completion list %L1 more No completions No completions %Ln more completions %Ln more completion %Ln more completions Next completion: Next completion: %1 is not readable or not a file %1 is not readable or not a file /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 /%1 <memberId> /%1 <memberId> Unknown /command. If you intended to send a message, start with // instead of / Unknown /command. If you intended to send a message, start with // instead of / Cannot insert HTML - it's either invalid or unsupported Cannot insert HTML - it's either invalid or unsupported CreateRoomDialog Create room Create room Add Add a user to the list of invitees Add Remove Remove a user from the list of invitees Remove 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 DockModeMenu &Off The dock panel is hidden &Off &Docked &Docked &Floating The dock panel is floating, aka undocked &Floating Completely hide this list Completely hide this list The list is shown within the main window The list is shown within the main window The list is shown separately from the main window The list is shown separately from the main window 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 Unknown Unknown attachment size Unknown %Ln byte(s) %Ln byte %Ln bytes %L1 kB %L1 kB %L1 MB %L1 MB %L1 GB %L1 GB 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 This account is logged in already This account is logged in already 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 (none) The device id label text when there's no saved device id (none) Matrix ID Matrix ID Password Password Device name Device name Saved device id Saved device id 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 &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... Can't find the event without knowing the room Can't find the event without knowing the room Create &new room... Create &new room... &No-effect activity A menu item to show/hide meaningless activity such as redacted spam &No-effect activity &Room list &Room list &Member list &Member list &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... 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 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 Loading %Ln accounts, please wait Loading %Ln account, please wait Loading %Ln accounts, please wait Dock panels Panels of the dock, not 'to dock the panels' Dock panels Account %1 is synchronised, have a good chat Account %1 is synchronised, have a good chat All %Ln accounts synchronised, have a good chat Only shown with 2 or more accounts %Ln account synchronised, have a good chat All %Ln accounts synchronised, have a good chat 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 Open the room that has this event to scroll to %1 Open the room that has this event to scroll to %1 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 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 Could not decrypt the event Could not decrypt the event 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 (loading) The line to show instead of the replied-to event content while getting it from the homeserver (loading) %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 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 Verification timed out Verification timed out Verification was cancelled Verification was cancelled Verification was cancelled on the other side Verification was cancelled on the other side Verification failed: icons did not match Verification failed: icons did not match Verification did not succeed Verification did not succeed Cancel Cancel Please accept the verification request on the device you want to verify Please accept the verification request on the device you want to verify Loading other devices... Loading other devices... No avatar No avatar This device This device Verified Verified Verify... Verify... No E2EE No E2EE 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) (no available room versions) (no available room versions) 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? RoomHeader (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 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 Forget this room? Forget this room? Are you sure you want to forget room %1? Are you sure you want to forget room %1? 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) Events after fully read marker: %L1 Events after fully read marker: %L1 Unread events/highlights since read receipt: %L1/%L2 Unread events/highlights since read receipt: %L1/%L2 Unread events since read receipt: %L1 Unread events since read receipt: %L1 Room id: %1 Room id: %1 You joined this room as %1 You joined this room as %1 You were invited into this room as %1 You were invited into this room as %1 You left this room as %1 You left this room as %1 (maybe more) Unread messages (maybe more) 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 %Ln unread message(s) across all rooms %Ln unread message across all rooms %Ln unread messages across all rooms Highlight in %1 %1 is the room display name Highlight in %1 %Ln highlight(s) %Ln highlight %Ln highlights Timeline 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 VerificationDialog Verifying device %1 Verifying device %1 Confirm that the same icons, in the same order are displayed on the other side Confirm that the same icons, in the same order are displayed on the other side They match They match They DON'T match They DON'T match 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.97.1/client/translations/quaternion_en_GB.ts000066400000000000000000001741631476730121700237050ustar00rootroot00000000000000 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> 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... No completions No completions %Ln more completions %Ln more completion %Ln more completions Next completion: Next completion: Currently typing: Currently typing: At character %1: %2 At character %1: %2 %L1 more %L1 more %1 is not readable or not a file %1 is not readable or not a file Attaching the pasted image Attaching the pasted image Can't attach a file without a selected room Can't attach a file without a selected room Cannot insert HTML - it's either invalid or unsupported Cannot insert HTML — it's either invalid or unsupported Unknown /command. If you intended to send a message, start with // instead of / Unknown /command. If you intended to send a message, start with // instead of / 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 Remove Remove 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... This account is logged in already This account is logged in already (none) (none) Saved device id Saved device id MainWindow Loading... Loading... &Accounts &Accounts &Login... &Login... &Quit &Quit &View &View &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... 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 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 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 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 Loading %Ln accounts, please wait Loading %Ln account, please wait Loading %Ln accounts, please wait Account %1 is synchronised, have a good chat Account %1 is synchronised, have a good chat All %Ln accounts synchronised, have a good chat %Ln account synchronised, have a good chat All %Ln accounts synchronised, have a good chat &Room list &Room list &Member list &Member list Dock panels Dock panels Can't find the event without knowing the room Can't find the event without knowing the room Open the room that has this event to scroll to %1 Open the room that has this event to scroll to %1 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 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 (loading) (loading) Could not decrypt the event Could not decrypt the event 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? (no available room versions) (no available room versions) 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) Forget this room? Forget this room? Are you sure you want to forget room %1? Are you sure you want to forget room %1? 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) Main alias: %1 Main alias: %1 Direct chat with %1 Direct chat with %1 The room enforces encryption The room enforces encryption 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 (maybe more) (maybe more) Events after fully read marker: %L1 Events after fully read marker: %L1 Unread events/highlights since read receipt: %L1/%L2 Unread events/highlights since read receipt: %L1/%L2 Unread events since read receipt: %L1 Unread events since read receipt: %L1 Room id: %1 Room id: %1 You joined this room as %1 You joined this room as %1 You were invited into this room as %1 You were invited into this room as %1 You left this room as %1 You left this room as %1 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 Unknown Unknown %Ln byte(s) %Ln byte %Ln bytes %L1 kB %L1 kB %L1 MB %L1 MB %L1 GB %L1 GB 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 %Ln unread message(s) across all rooms %Ln unread messages across all rooms %Ln unread messages across all rooms 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 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 Loading other devices... Loading other devices... No avatar No avatar Set avatar Set avatar Verification timed out Verification timed out Verification was cancelled Verification was cancelled Verification did not succeed Verification did not succeed Cancel Cancel Please accept the verification request on the device you want to verify Please accept the verification request on the device you want to verify This device This device Verified Verified Verify... Verify... No E2EE No E2EE Verification failed: icons did not match Verification failed: icons did not match Verification was cancelled on the other side Verification was cancelled on the other side Timeline Latest events Latest events %Ln events back from now %Ln events 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 ChatEdit Reset formatting Reset formatting Reset the current character formatting to the default Reset the current character formatting to the default Paste as rich text Paste as rich text Paste as plain text Paste as plain text DockModeMenu &Off &Off &Docked &Docked &Floating &Floating Completely hide this list Completely hide this list The list is shown within the main window The list is shown within the main window The list is shown separately from the main window The list is shown separately from the main window RoomHeader (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 Room settings Room settings Go to new room Go to new room VerificationDialog Verifying device %1 Verifying device %1 They match They match They DON'T match They DON'T match Confirm that the same icons, in the same order are displayed on the other side Confirm that the same icons, in the same order are displayed on the other side Quaternion-0.0.97.1/client/translations/quaternion_en_US.ts000066400000000000000000001741571476730121700237470ustar00rootroot00000000000000 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> 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... No completions No completions %Ln more completions %Ln more completion %Ln more completions Next completion: Next completion: Currently typing: Currently typing: At character %1: %2 At character %1: %2 %L1 more %L1 more %1 is not readable or not a file %1 is not readable or not a file Attaching the pasted image Attaching the pasted image Can't attach a file without a selected room Can't attach a file without a selected room Cannot insert HTML - it's either invalid or unsupported Cannot insert HTML - it's either invalid or unsupported Unknown /command. If you intended to send a message, start with // instead of / Unknown /command. If you intended to send a message, start with // instead of / 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 Remove Remove 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... This account is logged in already This account is logged in already (none) (none) Saved device id Saved device id MainWindow Loading... Loading... &Accounts &Accounts &Login... &Login... &Quit &Quit &View &View &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... 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 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 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 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 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 Loading %Ln accounts, please wait Loading %Ln account, please wait Loading %Ln accounts, please wait Account %1 is synchronised, have a good chat Account %1 is synchronised, have a good chat All %Ln accounts synchronised, have a good chat %Ln account synchronised, have a good chat All %Ln accounts synchronised, have a good chat &Room list &Room list &Member list &Member list Dock panels Dock panels Can't find the event without knowing the room Can't find the event without knowing the room Open the room that has this event to scroll to %1 Open the room that has this event to scroll to %1 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 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 (loading) (loading) Could not decrypt the event Could not decrypt the event 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? (no available room versions) (no available room versions) 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) Forget this room? Forget this room? Are you sure you want to forget room %1? Are you sure you want to forget room %1? 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) Main alias: %1 Main alias: %1 Direct chat with %1 Direct chat with %1 The room enforces encryption The room enforces encryption 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 (maybe more) (maybe more) Events after fully read marker: %L1 Events after fully read marker: %L1 Unread events/highlights since read receipt: %L1/%L2 Unread events/highlights since read receipt: %L1/%L2 Unread events since read receipt: %L1 Unread events since read receipt: %L1 Room id: %1 Room id: %1 You joined this room as %1 You joined this room as %1 You were invited into this room as %1 You were invited into this room as %1 You left this room as %1 You left this room as %1 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 Unknown Unknown %Ln byte(s) %Ln byte %Ln bytes %L1 kB %L1 kB %L1 MB %L1 MB %L1 GB %L1 GB 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 %Ln unread message(s) across all rooms %Ln unread message across all rooms %Ln unread messages across all rooms 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 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 Loading other devices... Loading other devices... No avatar No avatar Set avatar Set avatar Verification timed out Verification timed out Verification was cancelled Verification was cancelled Verification did not succeed Verification did not succeed Cancel Cancel Please accept the verification request on the device you want to verify Please accept the verification request on the device you want to verify This device This device Verified Verified Verify... Verify... No E2EE No E2EE Verification failed: icons did not match Verification failed: icons did not match Verification was cancelled on the other side Verification was cancelled on the other side Timeline 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 ChatEdit Reset formatting Reset formatting Reset the current character formatting to the default Reset the current character formatting to the default Paste as rich text Paste as rich text Paste as plain text Paste as plain text DockModeMenu &Off &Off &Docked &Docked &Floating &Floating Completely hide this list Completely hide this list The list is shown within the main window The list is shown within the main window The list is shown separately from the main window The list is shown separately from the main window RoomHeader (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 Room settings Room settings Go to new room Go to new room VerificationDialog Verifying device %1 Verifying device %1 They match They match They DON'T match They DON'T match Confirm that the same icons, in the same order are displayed on the other side Confirm that the same icons, in the same order are displayed on the other side Quaternion-0.0.97.1/client/translations/quaternion_es.ts000066400000000000000000001262261476730121700233370ustar00rootroot00000000000000 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> 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: 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 (none) (ninguno) MainWindow Loading... Cargando... &Accounts &Cuentas &Login... &Iniciar sesión... &Quit &Salir &View &Ver &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... 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 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 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 &Member list &Lista de miembros 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 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) Main alias: %1 Alias principal: %1 Direct chat with %1 Charla directa con %1 The room enforces encryption La sala impone el cifrado 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 Unknown Desconocido %L1 kB %L1 kB %L1 MB %L1 MB %L1 GB %L1 GB 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 TimelineWidget Save file as... Guardar archivo como... Save file as Guardar archivo como Redact Redactar Quote Comillas Open externally Abrir externamente ProfileDialog Account Cuenta Access token Token de acceso Timeline %Ln events back from now %Ln evento desde ahora %Ln eventos desde ahora %Ln events cached %Ln evento en caché %Ln eventos en caché RoomHeader (no name) (sin nombre) This room has been upgraded. Esta sala ha sido mejorada. Unstable room version! ¡Versión de sala inestable! (no topic) (sin tema) Room settings Configuración de sala Quaternion-0.0.97.1/client/translations/quaternion_pl.ts000066400000000000000000001506021476730121700233360ustar00rootroot00000000000000 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> 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… 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 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 &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… 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 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 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 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 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) 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 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 (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 Unknown Nieznany %L1 kB %L1 kB %L1 MB %L1 MB %L1 GB %L1 GB 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ń 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 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 Loading other devices... Ładowanie innych urządzeń… No avatar Brak awatara Set avatar Ustaw awatar Timeline 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 %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 ChatEdit Reset formatting Zresetuj formatowanie Reset the current character formatting to the default Przywraca domyślne formatowanie znaków RoomHeader (no name) (bez nazwy) This room has been upgraded. Ten pokój został zaktualizowany. Unstable room version! Niestabilna wersja pokoju! (no topic) (brak tematu) Hide topic Ukryj temat Show topic Pokaż temat Room settings Ustawienia pokoju Go to new room Przejdź do nowego pokoju Quaternion-0.0.97.1/client/translations/quaternion_ru.ts000066400000000000000000002273641476730121700233630ustar00rootroot00000000000000 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-участника> 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) или ввести команду... No completions Подсказок нет %Ln more completions еще %Ln подсказка еще %Ln подсказки еще %Ln подсказок Next completion: Следующая подсказка: Currently typing: Сейчас печатает: At character %1: %2 На символе %1: %2 %L1 more еще %L1 %1 is not readable or not a file %1 недоступен для чтения или не является файлом Attaching the pasted image Вставленное изображение будет добавлено к сообщению Can't attach a file without a selected room Невозможно прикрепить файл без выбранной комнаты Cannot insert HTML - it's either invalid or unsupported Невозможно вставить HTML - разметка некорректна или не поддерживается Unknown /command. If you intended to send a message, start with // instead of / Неизвестная /команда. Если вы собирались отправить сообщение, используйте // вместо / в его начале CreateRoomDialog Create room Создать комнату Add Добавить Invite user(s) Пригласить пользователя(ей) Creating the room, please wait Комната создаётся, пожалуйста подождите Please fill the fields as desired. None are mandatory Пожалуйста, заполните поля по желанию. Обязательных полей нет. Remove Убрать 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... Запрашиваются поддерживаемые процедуры входа This account is logged in already Эта учетная запись уже подключена (none) (нет) Saved device id Сохраненный идентификатор устройства MainWindow Loading... Загрузка... &Accounts &Учётные записи &Login... Под&ключиться... &Quit &Выход &View &Вид &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... Настроить &прокси-сервер... 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 Открыть комнату из списка комнат 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 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 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 во внешнем приложении Loading %Ln accounts, please wait Загрузка %Ln учетной записи; подождите, пожалуйста Загрузка %Ln учетных записей; подождите, пожалуйста Загрузка %Ln учетных записей; подождите, пожалуйста Account %1 is synchronised, have a good chat Учетная запись %1 синхронизирована, приятного общения All %Ln accounts synchronised, have a good chat %Ln учетная запись синхронизирована, приятного общения Все %Ln учетных записи синхронизированы, приятного общения Все %Ln учетных записей синхронизированы, приятного общения &Room list &Список комнат &Member list &Список участников Dock panels Пристыковываемые панели Can't find the event without knowing the room Невозможно найти событие, не зная комнату Open the room that has this event to scroll to %1 Откройте комнату с этим событием, чтобы прокрутить до %1 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 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 из комнаты (loading) (загружается) Could not decrypt the event Не удалось расшифровать событие 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). Сервер может перестать поддерживать её в любой момент. Вы все еще хотите использовать эту версию? (no available room versions) (доступных версий комнат нет) 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) Forget this room? Забыть эту комнату? Are you sure you want to forget room %1? Вы уверены, что хотите забыть комнату %1? RoomListModel Invited Приглашения Low priority Неважные People Люди Ungrouped rooms Остальные Left Покинутые %1 (%Ln room(s)) %1 (%Ln комната) %1 (%Ln комнаты) %1 (%Ln комнат) %1 (as %2) %1 (как %2) Main alias: %1 Основной псевдоним: %1 Direct chat with %1 Прямой чат с %1 The room enforces encryption В комнате включено шифрование 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 (maybe more) (возможно больше) Events after fully read marker: %L1 Событий после отметки о полном прочтении: %L1 Unread events/highlights since read receipt: %L1/%L2 Полностью непрочитанных сообщений/уведомлений: %L1/%L2 Unread events since read receipt: %L1 Полностью непрочитанных сообщений: %L1 Room id: %1 Идентификатор комнаты: %1 You joined this room as %1 Вы присоединились к этой комнате под учетной записью %1 You were invited into this room as %1 Вас пригласили в эту комнату как %1 You left this room as %1 Вы покинули эту комнату как %1 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 Unknown Неизвестен %Ln byte(s) %Ln байт %Ln байта %Ln байт %L1 kB %L1 Кб %L1 MB %L1 МБ %L1 GB %L1 ГБ 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 упоминаний %Ln unread message(s) across all rooms %Ln непрочитанное сообщение во всех комнатах %Ln непрочитанных сообщения во всех комнатах %Ln непрочитанных сообщений во всех комнатах 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 Device display name Отображаемое имя устройства Device ID Идентификатор устройства Last time seen Последний раз видели Last IP address Последний IP-адрес User profiles Профили пользователей Account Учётная запись Display Name Отображаемое имя Copy to clipboard Скопировать в буфер обмена Access token Ключ доступа Loading other devices... Загрузка других устройств... No avatar Нет аватара Set avatar Установить аватар Verification timed out Время проверки истекло Verification was cancelled Проверка была отменена Verification did not succeed Проверка не удалась Cancel Отменить Please accept the verification request on the device you want to verify Пожалуйста, подтвердите запрос на проверку на устройстве, которое вы хотите проверить This device Это устройство Verified Проверено Verify... Проверить... No E2EE Нет сквозного шифрования Verification failed: icons did not match Проверка не удалась: значки не совпадают Verification was cancelled on the other side Проверка была отменена на другом конце Timeline Latest events Последние события %Ln events back from now %Ln событие назад %Ln событий назад %Ln событий назад %Ln events cached %Ln событие закешировано %Ln события закешировано %Ln событий закешировано %Ln events requested from the server %Ln событие запрошено с сервера %Ln события запрошены с сервера %Ln событий запрошено с сервера ChatEdit Reset formatting Сбросить форматирование Reset the current character formatting to the default Сбросить текущее форматирование символов на значение по умолчанию Paste as rich text Вставить как форматированный текст Paste as plain text Вставить как текст без разметки DockModeMenu &Off &Скрыт &Docked При&стыкованная &Floating &Плавающая Completely hide this list Полностью скрыть этот список The list is shown within the main window Показывать список внутри главного окна The list is shown separately from the main window Показывать список отдельно от главного окна RoomHeader (no name) (без названия) This room has been upgraded. Эта комната была обновлена. Unstable room version! Неустойчивая версия комнаты! (no topic) (без темы) Hide topic Скрыть тему Show topic Показать тему Room settings Настройки комнаты Go to new room Перейти в новую комнату VerificationDialog Verifying device %1 Проверка устройства %1 They match Они совпадают They DON'T match Они НЕ совпадают Confirm that the same icons, in the same order are displayed on the other side Убедитесь, что на другой стороне отображаются те же значки в том же порядке Quaternion-0.0.97.1/client/userlistdock.cpp000066400000000000000000000145321476730121700206050ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2015 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #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().isEmpty()) 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->currentState().get(); const int userPl = plEvt ? plEvt->powerLevelForUser(m_currentRoom->localMember().id()) : 0; 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 userId = getSelectedUser(); !userId.isEmpty()) m_currentRoom->connection()->requestDirectChat(userId); } void UserListDock::requestUserMention() { if (auto userId = getSelectedUser(); !userId.isEmpty()) emit userMentionRequested(userId); } void UserListDock::kickUser() { if (auto userId = getSelectedUser(); !userId.isEmpty()) { bool ok; const auto reason = QInputDialog::getText(this, tr("Kick %1").arg(userId), tr("Reason"), QLineEdit::Normal, nullptr, &ok); if (ok) { m_currentRoom->kickMember(userId, reason); } } } void UserListDock::banUser() { if (auto userId = getSelectedUser(); !userId.isEmpty()) { bool ok; const auto reason = QInputDialog::getText(this, tr("Ban %1").arg(userId), tr("Reason"), QLineEdit::Normal, nullptr, &ok); if (ok) { m_currentRoom->ban(userId, reason); } } } void UserListDock::ignoreUser() { if (auto* user = m_currentRoom->connection()->user(getSelectedUser())) { if (!user->isIgnored()) user->ignore(); else user->unmarkIgnore(); } } bool UserListDock::isIgnored() { if (auto memberId = getSelectedUser(); !memberId.isEmpty()) return m_currentRoom->connection()->isIgnored(memberId); return false; } QString UserListDock::getSelectedUser() const { auto index = m_view->currentIndex(); if (!index.isValid()) return {}; const auto member = m_model->userAt(index); Q_ASSERT(!member.isEmpty()); return member.id(); } Quaternion-0.0.97.1/client/userlistdock.h000066400000000000000000000026151476730121700202510ustar00rootroot00000000000000/************************************************************************** * * * SPDX-FileCopyrightText: 2015 Felix Rohrbach * * * * SPDX-License-Identifier: GPL-3.0-or-later * * **************************************************************************/ #pragma once #include #include 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(QString userId); 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; QString getSelectedUser() const; }; Quaternion-0.0.97.1/client/verificationdialog.cpp000066400000000000000000000051751476730121700217370ustar00rootroot00000000000000#include "verificationdialog.h" #include #include #include #include using namespace Qt::StringLiterals; VerificationDialog::VerificationDialog(Session* session, QWidget* parent) : Dialog(tr("Verifying device %1").arg(session->remoteDeviceId()), QDialogButtonBox::Ok | QDialogButtonBox::Discard, parent) , session(session) { // The same check as in Session::handleEvent() in the KeyVerificationKeyEvent branch QUO_CHECK(session->state() == Session::WAITINGFORKEY || session->state() == Session::ACCEPTED); addWidget(new QLabel( tr("Confirm that the same icons, in the same order are displayed on the other side"))); const auto emojis = session->sasEmojis(); constexpr auto rowsCount = 2; const auto rowSize = (emojis.size() + 1) / rowsCount; auto emojiGrid = addLayout(); for (int i = 0; i < emojis.size(); ++i) { auto emojiLayout = new QVBoxLayout(); auto emoji = new QLabel(emojis[i].emoji); emoji->setFont({ u"emoji"_s, emoji->font().pointSize() * 4 }); for (auto* const l : { emoji, new QLabel(emojis[i].description) }) { emojiLayout->addWidget(l); emojiLayout->setAlignment(l, Qt::AlignCenter); } emojiGrid->addLayout(emojiLayout); if (i % rowSize == rowSize - 1) emojiGrid = addLayout(); // Start new line } button(QDialogButtonBox::Ok)->setText(tr("They match")); button(QDialogButtonBox::Discard)->setText(tr("They DON'T match")); // Pin lifecycles of the dialog and the session, avoiding recursion (by the time // QObject::destroyed is emitted, KeyVerificationSession signals are disconnected) connect(session, &QObject::destroyed, this, &QDialog::reject); // NB: this is only triggered when a dialog is closed using a window close button; // QDialogButtonBox::Discard doesn't trigger QDialog::rejected as it has DestructiveRole connect(this, &QDialog::rejected, session, [session] { if (session->state() != Session::CANCELED) session->cancelVerification(Session::USER); }); } VerificationDialog::~VerificationDialog() = default; void VerificationDialog::buttonClicked(QAbstractButton* button) { if (button == this->button(QDialogButtonBox::Ok)) { session->sendMac(); accept(); } else if (button == this->button(QDialogButtonBox::Discard)) { session->cancelVerification(Session::MISMATCHED_SAS); reject(); } else QUO_ALARM_X(false, "Unknown button: " % button->text()); } Quaternion-0.0.97.1/client/verificationdialog.h000066400000000000000000000006531476730121700214000ustar00rootroot00000000000000#pragma once #include "dialog.h" namespace Quotient { class KeyVerificationSession; } class VerificationDialog : public Dialog { Q_OBJECT public: using Session = Quotient::KeyVerificationSession; VerificationDialog(Session* session, QWidget* parent); ~VerificationDialog() override; private: // Overrides void buttonClicked(QAbstractButton* button) override; private: // Data Session* session; }; Quaternion-0.0.97.1/cmake/000077500000000000000000000000001476730121700151635ustar00rootroot00000000000000Quaternion-0.0.97.1/cmake/ECMInstallIcons.cmake000066400000000000000000000261071476730121700211220ustar00rootroot00000000000000#.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. #============================================================================= # SPDX-FileCopyrightText: 2014 Alex Merry # SPDX-FileCopyrightText: 2013 David Edmundson # SPDX-FileCopyrightText: 2008 Chusslove Illich # SPDX-FileCopyrightText: 2006 Alex Neundorf # # SPDX-License-Identifier: BSD-3-Clause 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.97.1/cmake/MacOSXBundleInfo.plist.in000066400000000000000000000021231476730121700217030ustar00rootroot00000000000000 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.97.1/flatpak/000077500000000000000000000000001476730121700155255ustar00rootroot00000000000000Quaternion-0.0.97.1/flatpak/build.sh000077500000000000000000000004301476730121700171600ustar00rootroot00000000000000#!/usr/bin/env bash flatpak-builder --ccache --force-clean --require-changes --repo=repo --subject="Nightly build of Quaternion, `date`" ${EXPORT_ARGS-} app io.github.quotient_im.Quaternion.yaml flatpak --user remote-add --if-not-exists quaternion-nightly repo/ --no-gpg-verify Quaternion-0.0.97.1/flatpak/io.github.quotient_im.Quaternion.yaml000066400000000000000000000032661476730121700247700ustar00rootroot00000000000000id: io.github.quotient_im.Quaternion rename-icon: quaternion runtime: org.kde.Platform runtime-version: '6.8' sdk: org.kde.Sdk command: quaternion finish-args: - --share=ipc - --share=network - --socket=wayland - --socket=fallback-x11 - --device=dri - --filesystem=xdg-download - --talk-name=org.freedesktop.secrets - --talk-name=org.kde.kwalletd5 - --talk-name=org.freedesktop.Notifications - --talk-name=org.kde.StatusNotifierWatcher cleanup: - /include - /lib/pkgconfig - /share/man modules: - name: libolm buildsystem: cmake-ninja builddir: true sources: - type: git url: https://gitlab.matrix.org/matrix-org/olm.git tag: '3.2.15' config-opts: - -DBUILD_SHARED_LIBS=OFF - -DOLM_TESTS=OFF cleanup: - /lib - /share - name: libsecret buildsystem: meson builddir: true config-opts: - -Dmanpage=false - -Dvapi=false - -Dgtk_doc=false - -Dintrospection=false - -Dcrypto=disabled cleanup: - /bin sources: - type: git url: https://gitlab.gnome.org/GNOME/libsecret.git tag: '0.21.4' - name: qtkeychain buildsystem: cmake-ninja builddir: true sources: - type: git url: https://github.com/frankosterfeld/qtkeychain.git tag: '0.14.3' cleanup: - mkspecs - /lib/cmake config-opts: - -DBUILD_WITH_QT6=ON # When linked statically, Qt Keychain fails to work with all kinds of strange DBus errors # - -DBUILD_SHARED_LIBS=OFF - -DCMAKE_INSTALL_LIBDIR=/app/lib - -DLIB_INSTALL_DIR=/app/lib - -DBUILD_TEST_APPLICATION=OFF - name: quaternion buildsystem: cmake-ninja builddir: true sources: - type: dir path: "../" config-opts: - -DBUILD_TESTING=OFF - -DBUILD_SHARED_LIBS=OFF cleanup: - /lib - /share/ndk-modules Quaternion-0.0.97.1/flatpak/setup_runtime.sh000077500000000000000000000003401476730121700207640ustar00rootroot00000000000000#!/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//6.8 flatpak --user install flathub org.kde.Sdk//6.8 Quaternion-0.0.97.1/icons/000077500000000000000000000000001476730121700152165ustar00rootroot00000000000000Quaternion-0.0.97.1/icons/breeze/000077500000000000000000000000001476730121700164725ustar00rootroot00000000000000Quaternion-0.0.97.1/icons/breeze/COPYING.breeze000066400000000000000000000222301476730121700207770ustar00rootroot00000000000000The 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.97.1/icons/breeze/README.breeze000066400000000000000000000002401476730121700206210ustar00rootroot00000000000000The icons in this folder where imported from the breeze icon set. Repository: anongit.kde.org:breeze-icons.git Commit: 00f3ea7a763dde4d676ece8186c1cdbe52f6c2fcQuaternion-0.0.97.1/icons/breeze/irc-channel-joined.svg000066400000000000000000000074161476730121700226540ustar00rootroot00000000000000 image/svg+xml Quaternion-0.0.97.1/icons/breeze/irc-channel-parted.svg000066400000000000000000000074431476730121700226630ustar00rootroot00000000000000 image/svg+xml Quaternion-0.0.97.1/icons/busy_16x16.gif000066400000000000000000000526741476730121700175520ustar00rootroot00000000000000GIF89a  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///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.97.1/icons/quaternion.icns000066400000000000000000027043131476730121700202730ustar00rootroot00000000000000icns 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>_HlQt-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 io.github.quotient_im.Quaternion.desktop https://raw.githubusercontent.com/quotient-im/Quaternion/master/Screenshot.png UI screenshot https://github.com/quotient-im/Quaternion https://github.com/quotient-im/Quaternion/issues https://github.com/quotient-im/Quaternion/blob/master/README.md quaternion com.github.quaternion.desktop com.github.quaternion.desktop https://github.com/quotient-im/Quaternion/releases/tag/0.0.97.1

0.0.97.1

  • You can now verify Quaternion sessions for E2EE communication (have to start verification in Quaternion for now; the other direction is not implemented yet)
  • Change systray icon when new messages come and Quaternion is not an active window
  • Event numbers now show up in the scroller area if the shuttle scroller is used
  • Various smaller UI tweaks and fixes
https://github.com/quotient-im/Quaternion/releases/tag/0.0.97

0.0.97

  • Use libQuotient 0.9 underneath
  • E2EE is always on now
  • Add nice replies visualisation instead of relying on reply fallbacks
  • New look for the shuttle scroller and scroll-to buttons
  • Clicking on scroll-to-read-marker button now loads history all the way until the marker - no need to click on the button repeatedly
  • Display images embedded in rich text messages
  • New login dialog
  • Forgetting a room requires a confirmation
  • Fix: actually log out accounts on exit that are not set to stay logged in
  • Regression fix: message text again uses the font configured for the timeline
  • New application id, to comply with Flathub verification requirements
  • Other code cleanup, performance and visual tweaks and smaller fixes
https://github.com/quotient-im/Quaternion/releases/tag/0.0.96.1

Changes since 0.0.96:

  • Fix regressions in attaching files
  • Allow `mxc` scheme in hyperlinks (MSC2398)
https://github.com/quotient-im/Quaternion/releases/tag/0.0.96

0.0.96

Changes since 0.0.95.1:

  • Qt 6 support
  • Beta-quality support of end-to-end encryption (E2EE)
  • Fixes and improvements in the timeline display and text selection
  • Ctrl-Shift-V to paste content as plain text
  • Fixed problems around drag-n-dropping/pasting some content
  • It's now possible to re-dock floating panels from the menu (especially relevant on Wayland)
  • Using room-specific member avatars in the timeline, not just in the user list
  • Performance and stability improvements
https://github.com/quotient-im/Quaternion/releases/tag/0.0.96-rc1

0.0.96 RC

Minor tweaks and fixes

https://github.com/quotient-im/Quaternion/releases/tag/0.0.96-beta4

0.0.96 beta4

  • Restored workability with Qt 5 (but only until the final release; after that, only Qt 6 will be supported)
  • Timeline scrolling with mouse wheels and touchpads is (should be) no more sluggish
  • Pasting HTML from web pages and other applications works in much more cases
  • More discernible text colours for state events and emotes
https://github.com/quotient-im/Quaternion/releases/tag/0.0.96-beta3

0.0.96 beta3

  • Switch to libQuotient 0.8 to make E2EE opt-in at runtime
  • Complete transition to Qt 6
  • Other minor tweaks and fixes
https://github.com/quotient-im/Quaternion/releases/tag/0.0.96-beta1

Switch to libQuotient 0.7, with its new read receipts/fully read markers API and experimental support of E2EE; fix a few bugs. More to come before the final release!

https://github.com/quotient-im/Quaternion/releases/tag/0.0.95.1

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

0.0.95 final release

  • 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.97.1/linux/io.github.quotient_im.Quaternion.desktop000066400000000000000000000003711476730121700252060ustar00rootroot00000000000000[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.97.1/quaternion.kdev4000066400000000000000000000000631476730121700172260ustar00rootroot00000000000000[Project] Manager=KDevCMakeManager Name=quaternion Quaternion-0.0.97.1/quaternion.supp000066400000000000000000000003231476730121700171770ustar00rootroot00000000000000{ operator new(unsigned long)[Memcheck:Leak] Memcheck:Leak fun:_Znwm fun:_ZN11QCursorData10initializeEv fun:_ZN22QGuiApplicationPrivate4initEv fun:_ZN19QApplicationPrivate4initEv fun:main } Quaternion-0.0.97.1/quaternion_win32.rc000066400000000000000000000000611476730121700176350ustar00rootroot00000000000000IDI_ICON1 ICON DISCARDABLE "icons/quaternion.ico"

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.97.1/icons/quaternion.ico000066400000000000000000002235361476730121700201120ustar00rootroot00000000000000(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.97.1/icons/quaternion/16-apps-quaternion.png000066400000000000000000000012241476730121700234620ustar00rootroot00000000000000PNG  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.97.1/icons/quaternion/22-apps-quaternion.png000066400000000000000000000020061476730121700234560ustar00rootroot00000000000000PNG  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.97.1/icons/quaternion/32-apps-quaternion.png000066400000000000000000000027711476730121700234700ustar00rootroot00000000000000PNG  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.97.1/icons/quaternion/48-apps-quaternion.png000066400000000000000000000051171476730121700234740ustar00rootroot00000000000000PNG  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.97.1/icons/quaternion/64-apps-quaternion.png000066400000000000000000000074101476730121700234700ustar00rootroot00000000000000PNG  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.97.1/icons/quaternion/sc-apps-quaternion.svgz000066400000000000000000000101531476730121700240470ustar00rootroot00000000000000AWquaternion.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.97.1/icons/quaternion/sources/000077500000000000000000000000001476730121700210665ustar00rootroot00000000000000Quaternion-0.0.97.1/icons/quaternion/sources/quaternion-green.svg000066400000000000000000000431171476730121700251000ustar00rootroot00000000000000 image/svg+xml Quaternion-0.0.97.1/icons/quaternion/sources/quaternion-red.svg000066400000000000000000000431151476730121700245500ustar00rootroot00000000000000 image/svg+xml Quaternion-0.0.97.1/icons/quaternion/sources/sc-apps-quaternion.svg000066400000000000000000000431021476730121700253400ustar00rootroot00000000000000 image/svg+xml Quaternion-0.0.97.1/icons/scrolldown.svg000066400000000000000000000034161476730121700201310ustar00rootroot00000000000000 icon_newmessages Created with Sketch. Quaternion-0.0.97.1/icons/scrollup.svg000066400000000000000000000042361476730121700176070ustar00rootroot00000000000000 image/svg+xml icon_newmessages icon_newmessages Created with Sketch. Quaternion-0.0.97.1/lib/000077500000000000000000000000001476730121700146515ustar00rootroot00000000000000Quaternion-0.0.97.1/linux/000077500000000000000000000000001476730121700152425ustar00rootroot00000000000000Quaternion-0.0.97.1/linux/io.github.quotient_im.Quaternion.appdata.xml000066400000000000000000000204271476730121700257520ustar00rootroot00000000000000 io.github.quotient_im.Quaternion CC-BY-4.0 GPL-3.0+ Quaternion