glogg-1.1.4/0000755000175000017500000000000013076706620011613 5ustar nickonickoglogg-1.1.4/runtests.sh0000755000175000017500000000046213076461374014047 0ustar nickonicko#!/bin/sh ./tests/logcrawler_tests $* result=$? ICON_OK=gtk-apply ICON_ERROR=gtk-cancel if [ `which notify-send` ]; then if [ "$result" != "0" ]; then notify-send -i $ICON_ERROR "glogg" "Tests failed!" else notify-send -i $ICON_OK "glogg" "Tests passed!" fi fi exit $result glogg-1.1.4/glogg.qrc0000644000175000017500000000163713076461374013434 0ustar nickonicko images/hicolor/16x16/glogg.png images/hicolor/24x24/glogg.png images/hicolor/32x32/glogg.png images/hicolor/48x48/glogg.png images/open14.png images/open14@2x.png images/reload14.png images/reload14@2x.png images/olddata_icon.png images/olddata_icon@2x.png images/newdata_icon.png images/newdata_icon@2x.png images/newfiltered_icon.png images/newfiltered_icon@2x.png images/stop14.png images/plus.png images/minus.png images/down.png images/up.png images/arrowdown.png images/arrowup.png images/darkclosebutton.png glogg-1.1.4/src/0000755000175000017500000000000013076461374012406 5ustar nickonickoglogg-1.1.4/src/inotifywatchtowerdriver.h0000644000175000017500000001046713076461374017574 0ustar nickonicko/* * Copyright (C) 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef INOTIFYWATCHTOWERDRIVER_H #define INOTIFYWATCHTOWERDRIVER_H #include #include #include template class ObservedFile; template class ObservedFileList; class INotifyWatchTowerDriver { public: class FileId { public: friend class INotifyWatchTowerDriver; FileId() { wd_ = -1; } bool operator==( const FileId& other ) const { return wd_ == other.wd_; } private: FileId( int wd ) { wd_ = wd; } int wd_; }; class DirId { public: friend class INotifyWatchTowerDriver; DirId() { wd_ = -1; } bool operator==( const DirId& other ) const { return wd_ == other.wd_; } bool valid() const { return (wd_ != -1); } private: DirId( int wd ) { wd_ = wd; } int wd_; }; class SymlinkId { public: friend class INotifyWatchTowerDriver; SymlinkId() { wd_ = -1; } bool operator==( const SymlinkId& other ) const { return wd_ == other.wd_; } private: SymlinkId( int wd ) { wd_ = wd; } int wd_; }; // Dummy class for inotify class FileChangeToken { public: FileChangeToken() {} FileChangeToken( const std::string& ) {} void readFromFile( const std::string& ) {} bool operator!=( const FileChangeToken& ) { return true; } }; #ifdef HAS_TEMPLATE_ALIASES using INotifyObservedFile = ObservedFile; using INotifyObservedFileList = ObservedFileList; #else typedef ObservedFile INotifyObservedFile; typedef ObservedFileList INotifyObservedFileList; #endif // Default constructor INotifyWatchTowerDriver(); ~INotifyWatchTowerDriver(); // No copy/assign/move please INotifyWatchTowerDriver( const INotifyWatchTowerDriver& ) = delete; INotifyWatchTowerDriver& operator=( const INotifyWatchTowerDriver& ) = delete; INotifyWatchTowerDriver( const INotifyWatchTowerDriver&& ) = delete; INotifyWatchTowerDriver& operator=( const INotifyWatchTowerDriver&& ) = delete; FileId addFile( const std::string& file_name ); SymlinkId addSymlink( const std::string& file_name ); DirId addDir( const std::string& file_name ); void removeFile( const FileId& file_id ); void removeSymlink( const SymlinkId& symlink_id ); void removeDir( const DirId& dir_id ); // Wait for an event for the OS, treat it and // return a list of files to notify about. // This must be called with the lock on the list held, // the function will unlock it temporary whilst blocking. // Also returns a list of file that need readding // (because of renames/symlink...) std::vector waitAndProcessEvents( INotifyObservedFileList* list, std::unique_lock* list_mutex, std::vector* files_needing_readding, int timeout_ms ); // Interrupt waitAndProcessEvents if it is blocking. void interruptWait(); private: // Only written at initialisation so no protection needed. const int inotify_fd_; // Breaking pipe int breaking_pipe_read_fd_; int breaking_pipe_write_fd_; // Private member functions size_t processINotifyEvent( const struct inotify_event* event, INotifyObservedFileList* list, std::vector* files_to_notify, std::vector* files_needing_readding ); }; #endif glogg-1.1.4/src/qtfilewatcher.cpp0000644000175000017500000001135313076461374015757 0ustar nickonicko/* * Copyright (C) 2010 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include "log.h" #include "qtfilewatcher.h" #include #include QtFileWatcher::QtFileWatcher() : FileWatcher(), qtFileWatcher_( this ) { monitoringState_ = None; connect( &qtFileWatcher_, SIGNAL( fileChanged( const QString& ) ), this, SLOT( fileChangedOnDisk( const QString& ) ) ); connect( &qtFileWatcher_, SIGNAL( directoryChanged( const QString& ) ), this, SLOT( directoryChangedOnDisk( const QString& ) ) ); } QtFileWatcher::~QtFileWatcher() { disconnect( &qtFileWatcher_ ); } void QtFileWatcher::addFile( const QString& fileName ) { LOG(logDEBUG) << "QtFileWatcher::addFile " << fileName.toStdString(); QFileInfo fileInfo = QFileInfo( fileName ); if ( fileMonitored_.isEmpty() ) { fileMonitored_ = fileName; // Initialise the Qt file watcher qtFileWatcher_.addPath( fileInfo.path() ); if ( fileInfo.exists() ) { LOG(logDEBUG) << "QtFileWatcher::addFile: file exists."; qtFileWatcher_.addPath( fileName ); monitoringState_ = FileExists; } else { LOG(logDEBUG) << "QtFileWatcher::addFile: file doesn't exist."; monitoringState_ = FileRemoved; } } else { LOG(logWARNING) << "QtFileWatcher::addFile " << fileName.toStdString() << "- Already watching a file (" << fileMonitored_.toStdString() << ")!"; } } void QtFileWatcher::removeFile( const QString& fileName ) { LOG(logDEBUG) << "QtFileWatcher::removeFile " << fileName.toStdString(); QFileInfo fileInfo = QFileInfo( fileName ); if ( fileName == fileMonitored_ ) { if ( monitoringState_ == FileExists ) qtFileWatcher_.removePath( fileName ); qtFileWatcher_.removePath( fileInfo.path() ); fileMonitored_.clear(); monitoringState_ = None; } else { LOG(logWARNING) << "QtFileWatcher::removeFile - The file is not watched!"; } // For debug purpose: foreach (QString str, qtFileWatcher_.files()) { LOG(logERROR) << "File still watched: " << str.toStdString(); } foreach (QString str, qtFileWatcher_.directories()) { LOG(logERROR) << "Directories still watched: " << str.toStdString(); } } // // Slots // void QtFileWatcher::fileChangedOnDisk( const QString& filename ) { LOG(logDEBUG) << "QtFileWatcher::fileChangedOnDisk " << filename.toStdString(); if ( ( monitoringState_ == FileExists ) && ( filename == fileMonitored_ ) ) { emit fileChanged( filename ); // If the file has been removed... if ( !QFileInfo( filename ).exists() ) monitoringState_ = FileRemoved; } else LOG(logWARNING) << "QtFileWatcher::fileChangedOnDisk - call from Qt but no file monitored"; } void QtFileWatcher::directoryChangedOnDisk( const QString& filename ) { LOG(logDEBUG) << "QtFileWatcher::directoryChangedOnDisk " << filename.toStdString(); if ( monitoringState_ == FileRemoved ) { if ( QFileInfo( fileMonitored_ ).exists() ) { LOG(logDEBUG) << "QtFileWatcher::directoryChangedOnDisk - our file reappeared!"; // The file has been recreated, we have to watch it again. monitoringState_ = FileExists; // Restore the Qt file watcher (automatically cancelled // when the file is deleted) qtFileWatcher_.addPath( fileMonitored_ ); emit fileChanged( fileMonitored_ ); } else { LOG(logWARNING) << "QtFileWatcher::directoryChangedOnDisk - not the file we are watching"; } } else if ( monitoringState_ == FileExists ) { if ( ! QFileInfo( fileMonitored_ ).exists() ) { LOG(logDEBUG) << "QtFileWatcher::directoryChangedOnDisk - our file disappeared!"; monitoringState_ = FileRemoved; emit fileChanged( filename ); } else { LOG(logWARNING) << "QtFileWatcher::directoryChangedOnDisk - not the file we are watching"; } } } glogg-1.1.4/src/menuactiontooltipbehavior.cpp0000644000175000017500000000733113076461374020413 0ustar nickonicko/* * Copyright (C) 2009, 2010 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include #include #include #include #include "menuactiontooltipbehavior.h" // It would be nice to only need action, and have action be the parent, // however implementation needs the parent menu (see showToolTip), and // since neither action nor parent is guaranteed to die before the other, // for memory-management purposes the parent will have to be specified // explicity (probably, the window owning the action and the menu). MenuActionToolTipBehavior::MenuActionToolTipBehavior(QAction *action, QMenu *parentMenu, QObject *parent = 0) : QObject(parent), action(action), parentMenu(parentMenu), toolTipDelayMs(1000), timerId(0), hoverPoint() { connect(action, SIGNAL(hovered()), this, SLOT(onActionHovered())); } int MenuActionToolTipBehavior::toolTipDelay() { return toolTipDelayMs; } void MenuActionToolTipBehavior::setToolTipDelay(int delayMs) { toolTipDelayMs = delayMs; } void MenuActionToolTipBehavior::timerEvent(QTimerEvent *event) { // Not ours, don't touch if (event->timerId() != timerId) { QObject::timerEvent(event); return; } killTimer(timerId); // interested in a single shot timerId = 0; // Has the mouse waited unmoved in one location for 'delay' ms? const QPoint &mousePos = QCursor::pos(); if (hoverPoint == mousePos) showToolTip(hoverPoint); } void MenuActionToolTipBehavior::onActionHovered() { const QPoint &mousePos = QCursor::pos(); // Hover is fired on keyboard focus over action in menu, ignore it const QPoint &relativeMousePos = parentMenu->mapFromGlobal(mousePos); if (!parentMenu->actionGeometry(action).contains(relativeMousePos)) { if (timerId != 0) { // once timer expires its check will fail anyway killTimer(timerId); timerId = 0; } QToolTip::hideText(); // there might be one currently shown return; } // Record location hoverPoint = mousePos; // Restart timer if (timerId != 0) killTimer(timerId); timerId = startTimer(toolTipDelayMs); } void MenuActionToolTipBehavior::showToolTip(const QPoint &position) { const QString &toolTip = action->toolTip(); // Show tooltip until mouse moves at all // NOTE: using action->parentWidget() which is the MainWindow, // does not work (tooltip is not cleared when upon leaving the // region). This is the only reason we need parentMenu here. Just // a wild guess: maybe it isn't cleared because it would be // cleared on a mouse move over the designated widget, but mouse // move doesn't happen over MainWindow, since the mouse is over // the menu even when out of the activeRegion. QPoint relativePos = parentMenu->mapFromGlobal(position); QRect activeRegion(relativePos.x(), relativePos.y(), 1, 1); QToolTip::showText(position, toolTip, parentMenu, activeRegion); } glogg-1.1.4/src/filewatcher.cpp0000644000175000017500000000000013076461374015375 0ustar nickonickoglogg-1.1.4/src/loadingstatus.h0000644000175000017500000000214113076461374015436 0ustar nickonicko/* * Copyright (C) 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef LOADINGSTATUS_H #define LOADINGSTATUS_H #include // Loading status of a file enum class LoadingStatus { Successful, Interrupted, NoMemory }; // Data status (whether new, not seen, data is available) enum class DataStatus { OLD_DATA, NEW_DATA, NEW_FILTERED_DATA }; Q_DECLARE_METATYPE( DataStatus ) Q_DECLARE_METATYPE( LoadingStatus ) #endif glogg-1.1.4/src/externalcom.h0000644000175000017500000000335013076461374015101 0ustar nickonicko/* * Copyright (C) 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef EXTERNALCOM_H #define EXTERNALCOM_H #include class CantCreateExternalErr {}; /* * Virtual class representing another instance of glogg. * Sending messages to an object of this class will forward * them to the instance using the underlying IPC. */ class ExternalInstance { public: ExternalInstance() {} virtual ~ExternalInstance() {} virtual void loadFile( const QString& file_name ) const = 0; virtual uint32_t getVersion() const = 0; }; /* * Class receiving messages from another instance of glogg. * Messages are forwarded to the application by signals. */ class ExternalCommunicator : public QObject { Q_OBJECT public: ExternalCommunicator() : QObject() {} virtual ExternalInstance* otherInstance() const = 0; /* Instruct the communicator to start listening for * remote initiated operations */ virtual void startListening() = 0; signals: void loadFile( const QString& file_name ); public slots: virtual qint32 version() const = 0; }; #endif glogg-1.1.4/src/infoline.cpp0000644000175000017500000000416313076461374014721 0ustar nickonicko/* * Copyright (C) 2009, 2010 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include "log.h" #include "infoline.h" #include // This file implements InfoLine. It is responsible for decorating the // widget and managing the completion gauge. InfoLine::InfoLine() : QLabel(), origPalette_( palette() ), backgroundColor_( origPalette_.color( QPalette::Button ) ), darkBackgroundColor_( origPalette_.color( QPalette::Dark ) ) { setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Fixed ); } void InfoLine::displayGauge( int completion ) { int changeoverX = width() * completion / 100; // Create a gradient for the progress bar QLinearGradient linearGrad( changeoverX - 1, 0, changeoverX + 1, 0 ); linearGrad.setColorAt( 0, darkBackgroundColor_ ); linearGrad.setColorAt( 1, backgroundColor_ ); // Apply the gradient to the current palette (background) QPalette newPalette = origPalette_; newPalette.setBrush( backgroundRole(), QBrush( linearGrad ) ); setPalette( newPalette ); } void InfoLine::hideGauge() { setPalette( origPalette_ ); } // Custom painter: draw the background then call QLabel's painter void InfoLine::paintEvent( QPaintEvent* paintEvent ) { // Fill the widget background { QPainter painter( this ); painter.fillRect( 0, 0, this->width(), this->height(), palette().brush( backgroundRole() ) ); } // Call the parent's painter QLabel::paintEvent( paintEvent ); } glogg-1.1.4/src/socketexternalcom.h0000644000175000017500000000145613076461374016317 0ustar nickonicko#ifndef SOCKETEXTERNALCOM_H #define SOCKETEXTERNALCOM_H #include "externalcom.h" #include #include class SocketExternalInstance : public ExternalInstance { public: SocketExternalInstance(); void loadFile( const QString& file_name ) const override; uint32_t getVersion() const override; private: QSharedMemory* memory_; }; class SocketExternalCommunicator : public ExternalCommunicator { Q_OBJECT public: SocketExternalCommunicator(); ~SocketExternalCommunicator(); ExternalInstance* otherInstance() const override; void startListening() override; public slots: qint32 version() const override; private slots: void onConnection(); private: QSharedMemory* memory_; QLocalServer* server_; }; #endif // SOCKETEXTERNALCOM_H glogg-1.1.4/src/optionsdialog.h0000644000175000017500000000402313076461374015431 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2013 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef OPTIONSDIALOG_H #define OPTIONSDIALOG_H #include #include "configuration.h" #include "ui_optionsdialog.h" // Implements the main option dialog box class OptionsDialog : public QDialog, public Ui::OptionsDialog { Q_OBJECT public: OptionsDialog(QWidget* parent = 0); signals: // Is emitted when new settings must be used void optionsChanged(); private slots: // Clears and updates the font size box with the sizes allowed // by the passed font family. void updateFontSize(const QString& fontFamily); // Update the content of the global Config() using parameters // from the dialog box. void updateConfigFromDialog(); // Called when a ok/cancel/apply button is clicked. void onButtonBoxClicked( QAbstractButton* button ); // Called when the 'incremental' button is toggled. void onIncrementalChanged(); // Called when the 'polling' checkbox is toggled. void onPollingChanged(); private: void setupTabs(); void setupFontList(); void setupRegexp(); void setupIncremental(); void setupPolling(); int getRegexpIndex( SearchRegexpType syntax ) const; SearchRegexpType getRegexpTypeFromIndex( int index ) const; void updateDialogFromConfig(); QValidator* polling_interval_validator_; }; #endif glogg-1.1.4/src/signalmux.cpp0000644000175000017500000001155513076461374015130 0ustar nickonicko/* * Copyright (C) 2013 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include "signalmux.h" #include #include #include "log.h" SignalMux::SignalMux() : connectionList_() { currentDocument_ = nullptr; } void SignalMux::connect( QObject *sender, const char *signal, const char *slot ) { Connection new_connection = { sender, nullptr, signal, slot }; connectionList_.push_back( new_connection ); connect( new_connection ); } void SignalMux::disconnect( QObject *sender, const char *signal, const char *slot ) { // Find any signal that match our description auto connection = std::find_if( connectionList_.begin(), connectionList_.end(), [sender, signal, slot]( const Connection& c ) -> bool { return ((QObject*)c.source == sender) && (c.sink == nullptr) && (qstrcmp( c.signal, signal) == 0) && (qstrcmp( c.slot, slot) == 0); } ); if ( connection != connectionList_.end() ) { disconnect( *connection ); connectionList_.erase( connection ); } else { LOG( logERROR ) << "SignalMux disconnecting a non-existing signal"; } } // Upstream signals void SignalMux::connect( const char* signal, QObject* receiver, const char* slot ) { Connection new_connection = { nullptr, receiver, signal, slot }; connectionList_.push_back( new_connection ); connect( new_connection ); } void SignalMux::disconnect( const char *signal, QObject* receiver, const char *slot ) { // Find any signal that match our description auto connection = std::find_if( connectionList_.begin(), connectionList_.end(), [receiver, signal, slot]( const Connection& c ) -> bool { return ((QObject*)c.sink == receiver) && (c.source == nullptr) && (qstrcmp( c.signal, signal) == 0) && (qstrcmp( c.slot, slot) == 0); } ); if ( connection != connectionList_.end() ) { disconnect( *connection ); connectionList_.erase( connection ); } else { LOG( logERROR ) << "SignalMux disconnecting a non-existing signal"; } } void SignalMux::setCurrentDocument( QObject* current_document ) { // First disconnect everything to/from the old document for ( auto c: connectionList_ ) disconnect( c ); currentDocument_ = current_document; // And now connect to/from the new document for ( auto c: connectionList_ ) connect( c ); // And ask the doc to emit all state signals MuxableDocumentInterface* doc = dynamic_cast( current_document ); if ( doc ) doc->sendAllStateSignals(); } /* * Private functions */ void SignalMux::connect( const Connection& connection ) { if ( currentDocument_ ) { LOG( logDEBUG ) << "SignalMux::connect"; if ( connection.source && ( ! connection.sink ) ) { // Downstream signal QObject::connect( connection.source, connection.signal, currentDocument_, connection.slot ); } else if ( ( ! connection.source ) && connection.sink ) { // Upstream signal QObject::connect( currentDocument_, connection.signal, connection.sink, connection.slot ); } else { LOG( logERROR ) << "SignalMux has an unexpected signal"; } } } void SignalMux::disconnect( const Connection& connection ) { if ( currentDocument_ ) { LOG( logDEBUG ) << "SignalMux::disconnect"; if ( connection.source && ( ! connection.sink ) ) { // Downstream signal QObject::disconnect( connection.source, connection.signal, currentDocument_, connection.slot ); } else if ( ( ! connection.source ) && connection.sink ) { // Upstream signal QObject::disconnect( currentDocument_, connection.signal, connection.sink, connection.slot ); } else { LOG( logERROR ) << "SignalMux has an unexpected signal"; } } } glogg-1.1.4/src/quickfindpattern.cpp0000644000175000017500000000651513076461374016474 0ustar nickonicko/* * Copyright (C) 2010 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ // This file implements QuickFindPattern. // This class implements part of the Quick Find mechanism, it only stores the // current search pattern, once it has been confirmed (return pressed), // it can be asked to return the matches in a specific string. #include "quickfindpattern.h" #include "persistentinfo.h" #include "configuration.h" QuickFindPattern::QuickFindPattern() : QObject(), regexp_() { active_ = false; } void QuickFindPattern::changeSearchPattern( const QString& pattern ) { // Determine the type of regexp depending on the config QRegExp::PatternSyntax syntax; switch ( Persistent( "settings" )->quickfindRegexpType() ) { case Wildcard: syntax = QRegExp::Wildcard; break; case FixedString: syntax = QRegExp::FixedString; break; default: syntax = QRegExp::RegExp2; break; } regexp_.setPattern( pattern ); regexp_.setPatternSyntax( syntax ); if ( regexp_.isValid() && ( ! regexp_.isEmpty() ) ) active_ = true; else active_ = false; emit patternUpdated(); } void QuickFindPattern::changeSearchPattern( const QString& pattern, bool ignoreCase ) { regexp_.setCaseSensitivity( ignoreCase ? Qt::CaseInsensitive : Qt::CaseSensitive ); changeSearchPattern( pattern ); } bool QuickFindPattern::matchLine( const QString& line, QList& matches ) const { matches.clear(); if ( active_ ) { int pos = 0; while ( ( pos = regexp_.indexIn( line, pos ) ) != -1 ) { int length = regexp_.matchedLength(); matches << QuickFindMatch( pos, length ); pos += length; } } return ( matches.count() > 0 ); } bool QuickFindPattern::isLineMatching( const QString& line, int column ) const { int pos = 0; if ( ! active_ ) return false; if ( ( pos = regexp_.indexIn( line, column ) ) != -1 ) { lastMatchStart_ = pos; lastMatchEnd_ = pos + regexp_.matchedLength() - 1; return true; } else return false; } bool QuickFindPattern::isLineMatchingBackward( const QString& line, int column ) const { int pos = 0; if ( ! active_ ) return false; if ( ( pos = regexp_.lastIndexIn( line, column ) ) != -1 ) { lastMatchStart_ = pos; lastMatchEnd_ = pos + regexp_.matchedLength() - 1; return true; } else return false; } void QuickFindPattern::getLastMatch( int* start_col, int* end_col ) const { *start_col = lastMatchStart_; *end_col = lastMatchEnd_; } glogg-1.1.4/src/menuactiontooltipbehavior.h0000644000175000017500000000325113076461374020055 0ustar nickonicko/* * Copyright (C) 2009, 2010 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef MANUACTIONTOOLTIPBEHAVIOR_H #define MANUACTIONTOOLTIPBEHAVIOR_H #include #include class QAction; class QMenu; class QTimerEvent; // Provides a behavior to show an action's tooltip after mouse is unmoved for // a specified number of 'ms'. E.g. used for tooltips with full-path for recent // files in the file menu. Not thread-safe. class MenuActionToolTipBehavior : public QObject { Q_OBJECT; public: MenuActionToolTipBehavior(QAction *action, QMenu *parentMenu, QObject *parent); // Time in ms that mouse needs to stay unmoved for tooltip to be shown int toolTipDelay(); /* ms */ void setToolTipDelay(int ms); private: void timerEvent(QTimerEvent *event); void showToolTip(const QPoint &position); private slots: void onActionHovered(); private: QAction *action; QMenu *parentMenu; int toolTipDelayMs; int timerId; QPoint hoverPoint; }; #endif glogg-1.1.4/src/perfcounter.h0000644000175000017500000000354013076461374015115 0ustar nickonicko/* * Copyright (C) 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include // This class is a counter that remembers the number of events occuring within one second // it can be used for performance measurement (e.g. FPS) class PerfCounter { public: PerfCounter() {} // Count a new event, returns true if it has been counted. // If the function returns false, it indicates the current second is elapsed // and the user should read and reset the counter before re-adding the event. bool addEvent() { using namespace std::chrono; if ( counter_ == 0 ) first_event_date_ = steady_clock::now(); if ( duration_cast( steady_clock::now() - first_event_date_ ).count() < 1000000 ) { ++counter_; return true; } else { return false; } } // Read and reset the counter. Returns the number of events that occured in the // previous second. uint32_t readAndReset() { uint32_t value = counter_; counter_ = 0; return value; } private: uint32_t counter_ = 0; std::chrono::steady_clock::time_point first_event_date_; }; glogg-1.1.4/src/dbusexternalcom.h0000644000175000017500000000410313076461374015754 0ustar nickonicko/* * Copyright (C) 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef DBUSEXTERNALCOM_H #define DBUSEXTERNALCOM_H #include "externalcom.h" #include #include #include // An implementation of ExternalInstance using D-Bus via Qt class DBusExternalInstance : public ExternalInstance { public: DBusExternalInstance(); ~DBusExternalInstance() {} virtual void loadFile( const QString& file_name ) const; virtual uint32_t getVersion() const; private: std::shared_ptr dbusInterface_; }; class DBusInterfaceExternalCommunicator : public QObject { Q_OBJECT public: DBusInterfaceExternalCommunicator() : QObject() {} ~DBusInterfaceExternalCommunicator() {} public slots: void loadFile( const QString& file_name ); qint32 version() const; signals: void signalLoadFile( const QString& file_name ); }; // An implementation of ExternalCommunicator using D-Bus via Qt class DBusExternalCommunicator : public ExternalCommunicator { Q_OBJECT public: // Constructor: initialise the D-Bus connection, // can throw if D-Bus is not available DBusExternalCommunicator(); ~DBusExternalCommunicator() {} virtual void startListening(); virtual ExternalInstance* otherInstance() const; public slots: qint32 version() const; private: std::shared_ptr dbus_iface_object_; }; #endif glogg-1.1.4/src/winwatchtowerdriver.h0000644000175000017500000001462413076461374016707 0ustar nickonicko/* * Copyright (C) 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef WINWATCHTOWERDRIVER_H #define WINWATCHTOWERDRIVER_H #include #include #include #include #include #include #define WIN32_LEAN_AND_MEAN #include // Utility class // Encapsulate a directory notification returned by Windows' // ReadDirectoryChangesW. class WinNotificationInfo { public: enum class Action { UNDEF, ADDED, REMOVED, MODIFIED, RENAMED_OLD_NAME, RENAMED_NEW_NAME }; WinNotificationInfo() { action_ = Action::UNDEF; } WinNotificationInfo( Action action, std::wstring file_name ) { action_ = action; file_name_ = file_name; } Action action() const { return action_; } std::wstring fileName() const { return file_name_; } private: Action action_; std::wstring file_name_; }; class WinNotificationInfoList { public: WinNotificationInfoList( const char* buffer, size_t buffer_size ); // Iterator class iterator : std::iterator { public: iterator( WinNotificationInfoList* list, const char* position ) { list_ = list; position_ = position; } const WinNotificationInfo& operator*() const { return list_->current_notification_; } const WinNotificationInfo* operator->() const { return &( list_->current_notification_ ); } const WinNotificationInfo& operator++() { position_ = list_->advanceToNext(); return list_->current_notification_; } WinNotificationInfo operator++( int ) { WinNotificationInfo tmp { list_->current_notification_ }; operator++(); return tmp; } bool operator!=( const iterator& other ) const { return ( list_ != other.list_ ) || ( position_ != other.position_ ); } private: WinNotificationInfoList* list_; const char* position_; }; iterator begin() { return iterator( this, pointer_ ); } iterator end() { return iterator( this, nullptr ); } private: const char* advanceToNext(); // Current notification (in the byte stream) const char* pointer_; // Next notification (in the byte stream) const char* next_; WinNotificationInfo current_notification_; const char* updateCurrentNotification( const char* new_position ); }; template class ObservedFile; template class ObservedFileList; class WinWatchTowerDriver { public: struct WinWatchedDirRecord { WinWatchedDirRecord( const std::string& file_name ) : path_( file_name ) { } static const int READ_DIR_CHANGE_BUFFER_SIZE = 4096; std::string path_; void* handle_ = nullptr; static const unsigned long buffer_length_ = READ_DIR_CHANGE_BUFFER_SIZE; char buffer_[buffer_length_]; }; class FileId { }; class SymlinkId { }; class DirId { public: friend class WinWatchTowerDriver; DirId() {} bool operator==( const DirId& other ) const { return dir_record_ == other.dir_record_; } bool valid() const { return ( dir_record_ != nullptr ); } private: std::shared_ptr dir_record_; }; // On Windows, the token is the "last write" time class FileChangeToken { public: FileChangeToken() {} FileChangeToken( const std::string& file_name ) { readFromFile( file_name ); } void readFromFile( const std::string& file_name ); bool operator==( const FileChangeToken& other ) { return ( low_date_time_ == other.low_date_time_ ) && ( high_date_time_ == other.high_date_time_ ) && ( low_file_size_ == other.low_file_size_ ) && ( high_file_size_ == other.high_file_size_); } bool operator!=( const FileChangeToken& other ) { return ! operator==( other ); } private: DWORD low_date_time_ = 0; DWORD high_date_time_ = 0; DWORD low_file_size_ = 0; DWORD high_file_size_ = 0; }; // Default constructor WinWatchTowerDriver(); ~WinWatchTowerDriver(); FileId addFile( const std::string& file_name ); SymlinkId addSymlink( const std::string& file_name ); DirId addDir( const std::string& file_name ); void removeFile( const FileId& file_id ); void removeSymlink( const SymlinkId& symlink_id ); void removeDir( const DirId& dir_id ); std::vector*> waitAndProcessEvents( ObservedFileList* list, std::unique_lock* lock, std::vector*>* files_needing_readding, int timout_ms ); // Interrupt waitAndProcessEvents void interruptWait(); private: // An action which will be picked up by the worker thread. class Action { public: Action( std::function function ) : function_ { function } {} ~Action() {} void operator()() { function_(); } private: std::function function_; }; // Action std::mutex action_mutex_; std::condition_variable action_done_cv_; std::unique_ptr scheduled_action_ = nullptr; // Win32 notification variables HANDLE hCompPort_; OVERLAPPED overlapped_; unsigned long buffer_length_; // List of directory records // Accessed exclusively in the worker thread context std::vector> dir_records_ { }; // Private member functions void serialisedAddDir( const std::string& file_name, DirId& dir_id ); }; #endif glogg-1.1.4/src/recentfiles.h0000644000175000017500000000301313076461374015057 0ustar nickonicko/* * Copyright (C) 2011 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef RECENTFILES_H #define RECENTFILES_H #include #include #include "persistable.h" // Manage the list of recently opened files class RecentFiles : public Persistable { public: // Creates an empty set of recent files RecentFiles(); // Adds the passed filename to the list of recently used searches void addRecent( const QString& text ); // Returns a list of recent files (latest loaded first) QStringList recentFiles() const; // Reads/writes the current config in the QSettings object passed virtual void saveToStorage( QSettings& settings ) const; virtual void retrieveFromStorage( QSettings& settings ); private: static const int RECENTFILES_VERSION; static const int MAX_NUMBER_OF_FILES; QStringList recentFiles_; }; #endif glogg-1.1.4/src/versionchecker.h0000644000175000017500000000454613076461374015602 0ustar nickonicko/* * Copyright (C) 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef VERSIONCHECKER_H #define VERSIONCHECKER_H #include #include #include #include "persistable.h" // This class holds the configuration options and persistent // data for the version checker class VersionCheckerConfig : public Persistable { public: VersionCheckerConfig(); // Accessors bool versionCheckingEnabled() const { return enabled_; } void setVersionCheckingEnabled( bool enabled ) { enabled_ = enabled; } std::time_t nextDeadline() const { return next_deadline_; } void setNextDeadline( std::time_t deadline ) { next_deadline_ = deadline; } // Reads/writes the current config in the QSettings object passed virtual void saveToStorage( QSettings& settings ) const; virtual void retrieveFromStorage( QSettings& settings ); private: bool enabled_; std::time_t next_deadline_; }; // This class compares the current version number with the latest // stored on a central server class VersionChecker : public QObject { Q_OBJECT public: VersionChecker(); ~VersionChecker(); // Starts an asynchronous check for a newer version if it is needed. // A newVersionFound signal is sent if one is found. // In case of error or if no new version is found, no signal is emitted. void startCheck(); signals: // New version "version" is available void newVersionFound( const QString& version ); private slots: // Called when download is finished void downloadFinished( QNetworkReply* ); private: static const char* VERSION_URL; static const uint64_t CHECK_INTERVAL_S; QNetworkAccessManager manager_; }; #endif glogg-1.1.4/src/persistentinfo.h0000644000175000017500000000530413076461374015635 0ustar nickonicko/* * Copyright (C) 2011 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef PERSISTENTINFO_H #define PERSISTENTINFO_H #include #include #include class Persistable; // Singleton class managing the saving of persistent data to permanent storage // Clients must implement Persistable and register with this object, they can // then be saved/loaded. class PersistentInfo { public: // Initialise the storage backend for the Persistable, migrating the settings // if needed. Must be called before any other function. void migrateAndInit(); // Register a Persistable void registerPersistable( std::shared_ptr object, const QString& name ); // Get a Persistable (or NULL if it doesn't exist) std::shared_ptr getPersistable( const QString& name ); // Save a persistable to its permanent storage void save( const QString& name ); // Retrieve a persistable from permanent storage void retrieve( const QString& name ); private: // Can't be constructed or copied (singleton) PersistentInfo(); PersistentInfo( const PersistentInfo& ); ~PersistentInfo(); // Has migrateAndInit() been called? bool initialised_; // List of persistables QHash> objectList_; // Qt setting object QSettings* settings_; // allow this function to create one instance friend PersistentInfo& GetPersistentInfo(); }; PersistentInfo& GetPersistentInfo(); // Global function used to get a reference to an object // from the PersistentInfo store template std::shared_ptr Persistent( const char* name ) { std::shared_ptr p = GetPersistentInfo().getPersistable( QString( name ) ); return std::dynamic_pointer_cast(p); } template std::shared_ptr PersistentCopy( const char* name ) { std::shared_ptr p = GetPersistentInfo().getPersistable( QString( name ) ); return std::make_shared( *( std::dynamic_pointer_cast(p) ) ); } #endif glogg-1.1.4/src/main.cpp0000644000175000017500000002200513076461374014035 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2011, 2013, 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include #include #include #include namespace po = boost::program_options; #include #include using namespace std; #ifdef _WIN32 #include "unistd.h" #endif #include "persistentinfo.h" #include "sessioninfo.h" #include "configuration.h" #include "filterset.h" #include "recentfiles.h" #include "session.h" #include "mainwindow.h" #include "savedsearches.h" #include "loadingstatus.h" #include "externalcom.h" #ifdef GLOGG_SUPPORTS_DBUS #include "dbusexternalcom.h" #elif GLOGG_SUPPORTS_SOCKETIPC #include "socketexternalcom.h" #endif #include "log.h" static void print_version(); int main(int argc, char *argv[]) { QApplication app(argc, argv); vector filenames; // Configuration bool new_session = false; bool load_session = false; bool multi_instance = false; #ifdef _WIN32 bool log_to_file = false; #endif TLogLevel logLevel = logWARNING; try { po::options_description desc("Usage: glogg [options] [files]"); desc.add_options() ("help,h", "print out program usage (this message)") ("version,v", "print glogg's version information") ("multi,m", "allow multiple instance of glogg to run simultaneously (use together with -s)") ("load-session,s", "load the previous session (default when no file is passed)") ("new-session,n", "do not load the previous session (default when a file is passed)") #ifdef _WIN32 ("log,l", "save the log to a file (Windows only)") #endif ("debug,d", "output more debug (include multiple times for more verbosity e.g. -dddd)") ; po::options_description desc_hidden("Hidden options"); // For -dd, -ddd... for ( string s = "dd"; s.length() <= 10; s.append("d") ) desc_hidden.add_options()(s.c_str(), "debug"); desc_hidden.add_options() ("input-file", po::value>(), "input file") ; po::options_description all_options("all options"); all_options.add(desc).add(desc_hidden); po::positional_options_description positional; positional.add("input-file", -1); int command_line_style = (((po::command_line_style::unix_style ^ po::command_line_style::allow_guessing) | po::command_line_style::allow_long_disguise) ^ po::command_line_style::allow_sticky); po::variables_map vm; po::store(po::command_line_parser(argc, argv). options(all_options). positional(positional). style(command_line_style).run(), vm); po::notify(vm); if ( vm.count("help") ) { desc.print(cout); return 0; } if ( vm.count("version") ) { print_version(); return 0; } if ( vm.count( "debug" ) ) logLevel = logINFO; if ( vm.count( "multi" ) ) multi_instance = true; if ( vm.count( "new-session" ) ) new_session = true; if ( vm.count( "load-session" ) ) load_session = true; #ifdef _WIN32 if ( vm.count( "log" ) ) log_to_file = true; #endif for ( string s = "dd"; s.length() <= 10; s.append("d") ) if ( vm.count( s ) ) logLevel = (TLogLevel) (logWARNING + s.length()); if ( vm.count("input-file") ) { filenames = vm["input-file"].as>(); } } catch(exception& e) { cerr << "Option processing error: " << e.what() << endl; return 1; } catch(...) { cerr << "Exception of unknown type!\n"; } #ifdef _WIN32 if ( log_to_file ) { char file_name[255]; snprintf( file_name, sizeof file_name, "glogg_%d.log", getpid() ); FILE* file = fopen(file_name, "w"); Output2FILE::Stream() = file; } #endif FILELog::setReportingLevel( logLevel ); for ( auto& filename: filenames ) { if ( ! filename.empty() ) { // Convert to absolute path QFileInfo file( QString::fromLocal8Bit( filename.c_str() ) ); filename = file.absoluteFilePath().toStdString(); LOG( logDEBUG ) << "Filename: " << filename; } } // External communicator shared_ptr externalCommunicator = nullptr; shared_ptr externalInstance = nullptr; try { #ifdef GLOGG_SUPPORTS_DBUS externalCommunicator = make_shared(); externalInstance = shared_ptr( externalCommunicator->otherInstance() ); #elif GLOGG_SUPPORTS_SOCKETIPC externalCommunicator = make_shared(); auto ptr = externalCommunicator->otherInstance(); externalInstance = shared_ptr( ptr ); #endif } catch(CantCreateExternalErr& e) { LOG(logWARNING) << "Cannot initialise external communication."; } LOG(logDEBUG) << "externalInstance = " << externalInstance; if ( ( ! multi_instance ) && externalInstance ) { uint32_t version = externalInstance->getVersion(); LOG(logINFO) << "Found another glogg (version = " << std::setbase(16) << version << ")"; for ( const auto& filename: filenames ) { externalInstance->loadFile( QString::fromStdString( filename ) ); } return 0; } else { // FIXME: there is a race condition here. One glogg could start // between the declaration of externalInstance and here, // is it a problem? if ( externalCommunicator ) externalCommunicator->startListening(); } // Register types for Qt qRegisterMetaType("LoadingStatus"); // Register the configuration items GetPersistentInfo().migrateAndInit(); GetPersistentInfo().registerPersistable( std::make_shared(), QString( "session" ) ); GetPersistentInfo().registerPersistable( std::make_shared(), QString( "settings" ) ); GetPersistentInfo().registerPersistable( std::make_shared(), QString( "filterSet" ) ); GetPersistentInfo().registerPersistable( std::make_shared(), QString( "savedSearches" ) ); GetPersistentInfo().registerPersistable( std::make_shared(), QString( "recentFiles" ) ); #ifdef GLOGG_SUPPORTS_VERSION_CHECKING GetPersistentInfo().registerPersistable( std::make_shared(), QString( "versionChecker" ) ); #endif #ifdef _WIN32 // Allow the app to raise it's own windows (in case an external // glogg send us a file to open) AllowSetForegroundWindow(ASFW_ANY); #endif // We support high-dpi (aka Retina) displays app.setAttribute( Qt::AA_UseHighDpiPixmaps ); // No icon in menus app.setAttribute( Qt::AA_DontShowIconsInMenus ); // FIXME: should be replaced by a two staged init of MainWindow GetPersistentInfo().retrieve( QString( "settings" ) ); std::unique_ptr session( new Session() ); MainWindow mw( std::move( session ), externalCommunicator ); // Geometry mw.reloadGeometry(); // Load the existing session if needed std::shared_ptr config = Persistent( "settings" ); if ( load_session || ( filenames.empty() && !new_session && config->loadLastSession() ) ) mw.reloadSession(); LOG(logDEBUG) << "MainWindow created."; mw.show(); for ( const auto& filename: filenames ) { mw.loadInitialFile( QString::fromStdString( filename ) ); } mw.startBackgroundTasks(); return app.exec(); } static void print_version() { cout << "glogg " GLOGG_VERSION "\n"; #ifdef GLOGG_COMMIT cout << "Built " GLOGG_DATE " from " GLOGG_COMMIT "\n"; #endif cout << "Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 Nicolas Bonnefon and other contributors\n"; cout << "This is free software. You may redistribute copies of it under the terms of\n"; cout << "the GNU General Public License .\n"; cout << "There is NO WARRANTY, to the extent permitted by law.\n"; } glogg-1.1.4/src/filtersdialog.ui0000644000175000017500000001364513076461374015606 0ustar nickonicko FiltersDialog 0 0 520 288 Filters QLayout::SetNoConstraint 6 New Filter :/images/plus.png:/images/plus.png Delete Filter :/images/minus.png:/images/minus.png Qt::Horizontal 40 20 Move Filter Up :/images/up.png:/images/up.png Move Filter Down :/images/down.png:/images/down.png Matching Pattern: false Fore Color: false false Back Color: false Ignore case false Qt::Horizontal QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok glogg-1.1.4/src/quickfindmux.h0000644000175000017500000001077613076461374015301 0ustar nickonicko/* * Copyright (C) 2013, 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef QUICKFINDMUX_H #define QUICKFINDMUX_H #include #include #include #include #include "quickfindpattern.h" // Interface representing a widget searchable in both direction. class SearchableWidgetInterface { public: virtual void searchForward() = 0; virtual void searchBackward() = 0; virtual void incrementallySearchForward() = 0; virtual void incrementallySearchBackward() = 0; virtual void incrementalSearchStop() = 0; virtual void incrementalSearchAbort() = 0; }; // Interface representing the selector. It will be called and asked // who the search have to be forwarded to. class QuickFindMuxSelectorInterface { public: // Return the searchable widget to use. SearchableWidgetInterface* getActiveSearchable() const { return doGetActiveSearchable(); } // Return the list of all possible searchables, this // is done on registration in order to establish // listeners on all searchables. std::vector getAllSearchables() const { return doGetAllSearchables(); } protected: virtual SearchableWidgetInterface* doGetActiveSearchable() const = 0; virtual std::vector doGetAllSearchables() const = 0; }; class QFNotification; // Represents a multiplexer (unique application wise) dispatching the // Quick Find search from the UI to the relevant view. // It is also its responsability to determine if an incremental search // must be performed and to react accordingly. class QuickFindMux : public QObject { Q_OBJECT public: enum QFDirection { Forward, Backward, }; // Construct the multiplexer, taking a reference to the pattern QuickFindMux( std::shared_ptr pattern ); // Register a new selector, which will be called and asked // who the search have to be forwarded to. // The selector is called immediately when registering to get the list of // searchables. // The previous selector and its associated views are automatically // deregistered. // A null selector is accepted, in this case QFM functionalities are // disabled until a valid selector is registered. void registerSelector( const QuickFindMuxSelectorInterface* selector ); // Set the direction that will be used by the search when searching // forward. void setDirection( QFDirection direction ); signals: void patternChanged( const QString& ); void notify( const QFNotification& ); void clearNotification(); public slots: // Signal the current pattern must be altered (will start an incremental // search if the options are configured in such a way). void setNewPattern( const QString& new_pattern, bool ignore_case ); // Signal the current pattern must be altered and is confirmed // (will stop an incremental search if needed) void confirmPattern( const QString& new_pattern, bool ignore_case ); // Signal the user cancelled the search // (used for incremental only) void cancelSearch(); // Starts a search in the specified direction void searchNext(); void searchPrevious(); // Idem but ignore the direction and always search in the // specified direction void searchForward(); void searchBackward(); private slots: void changeQuickFind( const QString& new_pattern, QuickFindMux::QFDirection new_direction ); void notifyPatternChanged(); private: const QuickFindMuxSelectorInterface* selector_; // The (application wide) quick find pattern std::shared_ptr pattern_; QFDirection currentDirection_; std::vector registeredSearchables_; SearchableWidgetInterface* getSearchableWidget() const; void registerSearchable( QObject* searchable ); void unregisterAllSearchables(); }; #endif glogg-1.1.4/src/tabbedcrawlerwidget.h0000644000175000017500000000340013076461374016561 0ustar nickonicko/* * Copyright (C) 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef TABBEDCRAWLERWIDGET_H #define TABBEDCRAWLERWIDGET_H #include #include #include "loadingstatus.h" // This class represents glogg's main widget, a tabbed // group of CrawlerWidgets. // This is a very slightly customised QTabWidget, with // a particular style. class TabbedCrawlerWidget : public QTabWidget { Q_OBJECT public: TabbedCrawlerWidget(); virtual ~TabbedCrawlerWidget() {} // "Overridden" addTab/removeTab that automatically // show/hide the tab bar // The tab is created with the 'old data' icon. int addTab( QWidget* page, const QString& label ); void removeTab( int index ); // Set the data status (icon) for the tab number 'index' void setTabDataStatus( int index, DataStatus status ); protected: void keyPressEvent( QKeyEvent* event ); void mouseReleaseEvent( QMouseEvent *event); private: const QIcon olddata_icon_; const QIcon newdata_icon_; const QIcon newfiltered_icon_; QTabBar myTabBar_; }; #endif glogg-1.1.4/src/filewatcher.h0000644000175000017500000000311613076461374015055 0ustar nickonicko/* * Copyright (C) 2010, 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef FILEWATCHER_H #define FILEWATCHER_H #include // This abstract class defines a way to watch a group of (potentially // absent) files for update. class FileWatcher : public QObject { Q_OBJECT public: // Create an empty object FileWatcher() {} // Destroy the object virtual ~FileWatcher() {} // Adds the file to the list of file to watch // (do nothing if a file is already monitored) virtual void addFile( const QString& fileName ) = 0; // Removes the file to the list of file to watch // (do nothing if said file is not monitored) virtual void removeFile( const QString& fileName ) = 0; // Set the polling interval (0 means disabled) virtual void setPollingInterval( uint32_t ) {} signals: // Sent when the file on disk has changed in any way. void fileChanged( const QString& ); }; #endif glogg-1.1.4/src/watchtower.h0000644000175000017500000002523613076461374014756 0ustar nickonicko/* * Copyright (C) 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef WATCHTOWER_H #define WATCHTOWER_H #include "config.h" #include #include #include #include #include "watchtowerlist.h" // Allow the client to register for notification on an arbitrary number // of files. It will be notified to any change (creation/deletion/modification) // on those files. // It is passed a platform specific driver. // FIXME: Where to put that so it is not dependant // Registration object to implement RAII #ifdef HAS_TEMPLATE_ALIASES using Registration = std::shared_ptr; #else typedef std::shared_ptr Registration; #endif template class WatchTower { public: // Create an empty watchtower WatchTower(); // Destroy the object ~WatchTower(); // Set the polling interval (in ms) // 0 disables polling and is the default void setPollingInterval( int interval_ms ); // Add a file to the notification list. notification will be called when // the file is modified, moved or deleted. // Lifetime of the notification is tied to the Registration object returned. // Note the notification function is called with a mutex held and in a // third party thread, beware of races! Registration addFile( const std::string& file_name, std::function notification ); // Number of watched directories (for tests) unsigned int numberWatchedDirectories() const; private: // The driver (parametrised) Driver driver_; // List of files/dirs observed ObservedFileList observed_file_list_; // Protects the observed_file_list_ std::mutex observers_mutex_; // Polling interval (0 disables polling) uint32_t polling_interval_ms_ = 0; // Thread std::atomic_bool running_; std::thread thread_; // Exist as long as the onject exists, to ensure observers won't try to // call us if we are dead. std::shared_ptr heartBeat_; // Private member functions std::tuple addFileToDriver( const std::string& ); static void removeNotification( WatchTower* watch_tower, std::shared_ptr notification ); void run(); }; // Class template implementation #include #include #include #include #include "log.h" namespace { bool isSymLink( const std::string& file_name ); std::string directory_path( const std::string& path ); }; template WatchTower::WatchTower() : driver_(), thread_(), heartBeat_(std::shared_ptr((void*) 0xDEADC0DE, [] (void*) {})) { running_ = true; thread_ = std::thread( &WatchTower::run, this ); } template WatchTower::~WatchTower() { running_ = false; driver_.interruptWait(); thread_.join(); } template void WatchTower::setPollingInterval( int interval_ms ) { if ( polling_interval_ms_ != interval_ms ) { polling_interval_ms_ = interval_ms; // Break out of the wait if ( polling_interval_ms_ > 0 ) driver_.interruptWait(); } } template Registration WatchTower::addFile( const std::string& file_name, std::function notification ) { // LOG(logDEBUG) << "WatchTower::addFile " << file_name; std::weak_ptr weakHeartBeat(heartBeat_); std::lock_guard lock( observers_mutex_ ); auto existing_observed_file = observed_file_list_.searchByName( file_name ); std::shared_ptr> ptr( new std::function(std::move( notification )) ); if ( ! existing_observed_file ) { typename Driver::FileId file_id; typename Driver::SymlinkId symlink_id; std::tie( file_id, symlink_id ) = addFileToDriver( file_name ); auto new_file = observed_file_list_.addNewObservedFile( ObservedFile( file_name, ptr, file_id, symlink_id ) ); auto dir = observed_file_list_.watchedDirectoryForFile( file_name ); if ( ! dir ) { LOG(logDEBUG) << "WatchTower::addFile dir for " << file_name << " not watched, adding..."; dir = observed_file_list_.addWatchedDirectoryForFile( file_name, [this, weakHeartBeat] (ObservedDir* dir) { if ( auto heart_beat = weakHeartBeat.lock() ) { driver_.removeDir( dir->dir_id_ ); } } ); dir->dir_id_ = driver_.addDir( dir->path ); if ( ! dir->dir_id_.valid() ) { LOG(logWARNING) << "WatchTower::addFile driver failed to add dir"; dir = nullptr; } } else { LOG(logDEBUG) << "WatchTower::addFile Found exisiting watch for dir " << file_name; } // Associate the dir to the file if ( dir ) new_file->dir_ = dir; } else { existing_observed_file->addCallback( ptr ); } // Returns a shared pointer that removes its own entry // from the list of watched stuff when it goes out of scope! // Uses a custom deleter to do the work. return std::shared_ptr( 0x0, [this, ptr, weakHeartBeat] (void*) { if ( auto heart_beat = weakHeartBeat.lock() ) WatchTower::removeNotification( this, ptr ); } ); } template unsigned int WatchTower::numberWatchedDirectories() const { return observed_file_list_.numberWatchedDirectories(); } // // Private functions // // Add the passed file name to the driver, returning the file and symlink id template std::tuple WatchTower::addFileToDriver( const std::string& file_name ) { typename Driver::SymlinkId symlink_id; auto file_id = driver_.addFile( file_name ); if ( isSymLink( file_name ) ) { // We want to follow the name (as opposed to the inode) // so we watch the symlink as well. symlink_id = driver_.addSymlink( file_name ); } return std::make_tuple( file_id, symlink_id ); } // Called by the dtor for a registration object template void WatchTower::removeNotification( WatchTower* watch_tower, std::shared_ptr notification ) { LOG(logDEBUG) << "WatchTower::removeNotification"; std::lock_guard lock( watch_tower->observers_mutex_ ); auto file = watch_tower->observed_file_list_.removeCallback( notification ); if ( file ) { watch_tower->driver_.removeFile( file->file_id_ ); watch_tower->driver_.removeSymlink( file->symlink_id_ ); } } // Run in its own thread template void WatchTower::run() { while ( running_ ) { std::unique_lock lock( observers_mutex_ ); std::vector*> files_needing_readding; auto files = driver_.waitAndProcessEvents( &observed_file_list_, &lock, &files_needing_readding, polling_interval_ms_ ); LOG(logDEBUG) << "WatchTower::run: waitAndProcessEvents returned " << files.size() << " files, " << files_needing_readding.size() << " needing re-adding"; for ( auto file: files_needing_readding ) { // A file 'needing readding' has the same name, // but probably a different inode, so it needs // to be readded for some drivers that rely on the // inode (e.g. inotify) driver_.removeFile( file->file_id_ ); driver_.removeSymlink( file->symlink_id_ ); std::tie( file->file_id_, file->symlink_id_ ) = addFileToDriver( file->file_name_ ); } for ( auto file: files ) { for ( auto observer: file->callbacks ) { LOG(logDEBUG) << "WatchTower::run: notifying the client!"; // Here we have to cast our generic pointer back to // the function pointer in order to perform the call const std::shared_ptr> fptr = std::static_pointer_cast>( observer ); // The observer is called with the mutex held, // Let's hope it doesn't do anything too funky. (*fptr)(); file->markAsChanged(); } } if ( polling_interval_ms_ > 0 ) { // Also call files that have not been called for a while for ( auto file: observed_file_list_ ) { uint32_t ms_since_last_check = std::chrono::duration_cast( std::chrono::steady_clock::now() - file->timeForLastCheck() ).count(); if ( ( ms_since_last_check > polling_interval_ms_ ) && file->hasChanged() ) { LOG(logDEBUG) << "WatchTower::run: " << file->file_name_; for ( auto observer: file->callbacks ) { LOG(logDEBUG) << "WatchTower::run: notifying the client because of a timeout!"; // Here we have to cast our generic pointer back to // the function pointer in order to perform the call const std::shared_ptr> fptr = std::static_pointer_cast>( observer ); // The observer is called with the mutex held, // Let's hope it doesn't do anything too funky. (*fptr)(); } file->markAsChanged(); } } } } } namespace { bool isSymLink( const std::string& file_name ) { #ifdef HAVE_SYMLINK struct stat buf; lstat( file_name.c_str(), &buf ); return ( S_ISLNK(buf.st_mode) ); #else return false; #endif } }; #endif glogg-1.1.4/src/quickfind.cpp0000644000175000017500000002622713076461374015100 0ustar nickonicko/* * Copyright (C) 2010, 2013 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ // This file implements QuickFind. // This class implements the Quick Find mechanism using references // to the logData, the QFP and the selection passed. // Search is started just after the selection and the selection is updated // if a match is found. #include #include "log.h" #include "quickfindpattern.h" #include "selection.h" #include "data/abstractlogdata.h" #include "quickfind.h" void SearchingNotifier::reset() { dotToDisplay_ = 0; startTime_ = QTime::currentTime(); } void SearchingNotifier::sendNotification( qint64 current_line, qint64 nb_lines ) { LOG( logDEBUG ) << "Emitting Searching...."; qint64 progress; if ( current_line < 0 ) progress = ( nb_lines + current_line ) * 100 / nb_lines; else progress = current_line * 100 / nb_lines; emit notify( QFNotificationProgress( progress ) ); QApplication::processEvents( QEventLoop::ExcludeUserInputEvents ); startTime_ = QTime::currentTime().addMSecs( -800 ); } void QuickFind::LastMatchPosition::set( int line, int column ) { if ( ( line_ == -1 ) || ( ( line <= line_ ) && ( column < column_ ) ) ) { line_ = line; column_ = column; } } void QuickFind::LastMatchPosition::set( const FilePosition &position ) { set( position.line(), position.column() ); } bool QuickFind::LastMatchPosition::isLater( int line, int column ) const { if ( line_ == -1 ) return false; else if ( ( line == line_ ) && ( column >= column_ ) ) return true; else if ( line > line_ ) return true; else return false; } bool QuickFind::LastMatchPosition::isLater( const FilePosition &position ) const { return isLater( position.line(), position.column() ); } bool QuickFind::LastMatchPosition::isSooner( int line, int column ) const { if ( line_ == -1 ) return false; else if ( ( line == line_ ) && ( column <= column_ ) ) return true; else if ( line < line_ ) return true; else return false; } bool QuickFind::LastMatchPosition::isSooner( const FilePosition &position ) const { return isSooner( position.line(), position.column() ); } QuickFind::QuickFind( const AbstractLogData* const logData, Selection* selection, const QuickFindPattern* const quickFindPattern ) : logData_( logData ), selection_( selection ), quickFindPattern_( quickFindPattern ), lastMatch_(), firstMatch_(), searchingNotifier_(), incrementalSearchStatus_() { connect( &searchingNotifier_, SIGNAL( notify( const QFNotification& ) ), this, SIGNAL( notify( const QFNotification& ) ) ); } void QuickFind::incrementalSearchStop() { if ( incrementalSearchStatus_.isOngoing() ) { if ( selection_->isEmpty() ) { // Nothing found? // We reset the selection to what it was *selection_ = incrementalSearchStatus_.initialSelection(); } incrementalSearchStatus_ = IncrementalSearchStatus(); } } void QuickFind::incrementalSearchAbort() { if ( incrementalSearchStatus_.isOngoing() ) { // We reset the selection to what it was *selection_ = incrementalSearchStatus_.initialSelection(); incrementalSearchStatus_ = IncrementalSearchStatus(); } } qint64 QuickFind::incrementallySearchForward() { LOG( logDEBUG ) << "QuickFind::incrementallySearchForward"; // Position where we start the search from FilePosition start_position = selection_->getNextPosition(); if ( incrementalSearchStatus_.direction() == Forward ) { // An incremental search is active, we restart the search // from the initial point LOG( logDEBUG ) << "Restart search from initial point"; start_position = incrementalSearchStatus_.position(); } else { // It's a new search so we search from the selection incrementalSearchStatus_ = IncrementalSearchStatus( Forward, start_position, *selection_ ); } qint64 line_found = doSearchForward( start_position ); if ( line_found >= 0 ) { // We have found a result... // ... the caller will jump to this line. return line_found; } else { // No result... // ... we want the client to show the initial line. selection_->clear(); return incrementalSearchStatus_.position().line(); } } qint64 QuickFind::incrementallySearchBackward() { LOG( logDEBUG ) << "QuickFind::incrementallySearchBackward"; // Position where we start the search from FilePosition start_position = selection_->getPreviousPosition(); if ( incrementalSearchStatus_.direction() == Backward ) { // An incremental search is active, we restart the search // from the initial point LOG( logDEBUG ) << "Restart search from initial point"; start_position = incrementalSearchStatus_.position(); } else { // It's a new search so we search from the selection incrementalSearchStatus_ = IncrementalSearchStatus( Backward, start_position, *selection_ ); } qint64 line_found = doSearchBackward( start_position ); if ( line_found >= 0 ) { // We have found a result... // ... the caller will jump to this line. return line_found; } else { // No result... // ... we want the client to show the initial line. selection_->clear(); return incrementalSearchStatus_.position().line(); } } qint64 QuickFind::searchForward() { incrementalSearchStatus_ = IncrementalSearchStatus(); // Position where we start the search from FilePosition start_position = selection_->getNextPosition(); return doSearchForward( start_position ); } qint64 QuickFind::searchBackward() { incrementalSearchStatus_ = IncrementalSearchStatus(); // Position where we start the search from FilePosition start_position = selection_->getPreviousPosition(); return doSearchBackward( start_position ); } // Internal implementation of forward search, // returns the line where the pattern is found or -1 if not found. // Parameters are the position the search shall start qint64 QuickFind::doSearchForward( const FilePosition &start_position ) { bool found = false; int found_start_col; int found_end_col; if ( ! quickFindPattern_->isActive() ) return -1; // Optimisation: if we are already after the last match, // we don't do any search at all. if ( lastMatch_.isLater( start_position ) ) { // Send a notification emit notify( QFNotificationReachedEndOfFile() ); return -1; } qint64 line = start_position.line(); LOG( logDEBUG ) << "Start searching at line " << line; // We look at the rest of the first line if ( quickFindPattern_->isLineMatching( logData_->getExpandedLineString( line ), start_position.column() ) ) { quickFindPattern_->getLastMatch( &found_start_col, &found_end_col ); found = true; } else { searchingNotifier_.reset(); // And then the rest of the file qint64 nb_lines = logData_->getNbLine(); line++; while ( line < nb_lines ) { if ( quickFindPattern_->isLineMatching( logData_->getExpandedLineString( line ) ) ) { quickFindPattern_->getLastMatch( &found_start_col, &found_end_col ); found = true; break; } line++; // See if we need to notify of the ongoing search searchingNotifier_.ping( line, nb_lines ); } } if ( found ) { selection_->selectPortion( line, found_start_col, found_end_col ); // Clear any notification emit clearNotification(); return line; } else { // Update the position of the last match FilePosition last_match_position = selection_->getPreviousPosition(); lastMatch_.set( last_match_position ); // Send a notification emit notify( QFNotificationReachedEndOfFile() ); return -1; } } // Internal implementation of backward search, // returns the line where the pattern is found or -1 if not found. // Parameters are the position the search shall start qint64 QuickFind::doSearchBackward( const FilePosition &start_position ) { bool found = false; int start_col; int end_col; if ( ! quickFindPattern_->isActive() ) return -1; // Optimisation: if we are already before the first match, // we don't do any search at all. if ( firstMatch_.isSooner( start_position ) ) { // Send a notification emit notify( QFNotificationReachedBegininningOfFile() ); return -1; } qint64 line = start_position.line(); LOG( logDEBUG ) << "Start searching at line " << line; // We look at the beginning of the first line if ( ( start_position.column() > 0 ) && ( quickFindPattern_->isLineMatchingBackward( logData_->getExpandedLineString( line ), start_position.column() ) ) ) { quickFindPattern_->getLastMatch( &start_col, &end_col ); found = true; } else { searchingNotifier_.reset(); // And then the rest of the file qint64 nb_lines = logData_->getNbLine(); line--; while ( line >= 0 ) { if ( quickFindPattern_->isLineMatchingBackward( logData_->getExpandedLineString( line ) ) ) { quickFindPattern_->getLastMatch( &start_col, &end_col ); found = true; break; } line--; // See if we need to notify of the ongoing search searchingNotifier_.ping( -line, nb_lines ); } } if ( found ) { selection_->selectPortion( line, start_col, end_col ); // Clear any notification emit clearNotification(); return line; } else { // Update the position of the first match FilePosition first_match_position = selection_->getNextPosition(); firstMatch_.set( first_match_position ); // Send a notification LOG( logDEBUG ) << "QF: Send BOF notification."; emit notify( QFNotificationReachedBegininningOfFile() ); return -1; } } void QuickFind::resetLimits() { lastMatch_.reset(); firstMatch_.reset(); } glogg-1.1.4/src/viewtools.h0000644000175000017500000000424613076461374014620 0ustar nickonicko/* * Copyright (C) 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef VIEWTOOLS_H #define VIEWTOOLS_H #include #include // This class is a controller for an elastic hook manipulated with // the mouse wheel or touchpad. // It is used for the "follow" line at the end of the file. class ElasticHook : public QObject { Q_OBJECT public: ElasticHook( int hook_threshold ) : hook_threshold_( hook_threshold ) {} // Instruct the elastic to move by the passed pixels // (a positive value increase the elastic tension) void move( int value ); // Hold the elastic and prevent automatic decrease. void hold() { held_ = true; } // Release the elastic. void release() { held_ = false; } // Programmatically force the hook hooked or not. void hook( bool hooked ) { hooked_ = hooked; } // Return the "length" of the elastic hook. int length() const { return position_; } bool isHooked() const { return hooked_; } protected: void timerEvent( QTimerEvent *event ); signals: // Sent when the length has changed void lengthChanged(); // Sent when the hooked status has changed void hooked( bool is_hooked ); private: void decreasePosition(); static constexpr int TIMER_PERIOD_MS = 10; static constexpr int DECREASE_RATE = 4; const int hook_threshold_; bool hooked_ = false; bool held_ = false; int position_ = 0; int timer_id_ = 0; std::chrono::time_point last_update_; }; #endif glogg-1.1.4/src/dbuscontrol.h0000644000175000017500000000023113076461374015111 0ustar nickonicko#include #include class DBusControl : public QObject { Q_OBJECT public slots: QString version(void) { return "1.0.0"; } }; glogg-1.1.4/src/configuration.cpp0000644000175000017500000001206713076461374015767 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2013, 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include #include "log.h" #include "configuration.h" Configuration::Configuration() { // Should have some sensible default values. mainFont_ = QFont("monaco", 10); mainFont_.setStyleHint( QFont::Courier, QFont::PreferOutline ); mainRegexpType_ = ExtendedRegexp; quickfindRegexpType_ = FixedString; quickfindIncremental_ = true; #ifdef GLOGG_SUPPORTS_POLLING pollingEnabled_ = true; #else pollingEnabled_ = false; #endif pollIntervalMs_ = 2000; loadLastSession_ = true; overviewVisible_ = true; lineNumbersVisibleInMain_ = false; lineNumbersVisibleInFiltered_ = true; QFontInfo fi(mainFont_); LOG(logDEBUG) << "Default font is " << fi.family().toStdString(); searchAutoRefresh_ = false; searchIgnoreCase_ = false; } // Accessor functions QFont Configuration::mainFont() const { return mainFont_; } void Configuration::setMainFont( QFont newFont ) { LOG(logDEBUG) << "Configuration::setMainFont"; mainFont_ = newFont; } void Configuration::retrieveFromStorage( QSettings& settings ) { LOG(logDEBUG) << "Configuration::retrieveFromStorage"; // Fonts QString family = settings.value( "mainFont.family" ).toString(); int size = settings.value( "mainFont.size" ).toInt(); // If no config read, keep the default if ( !family.isNull() ) mainFont_ = QFont( family, size ); // Regexp types mainRegexpType_ = static_cast( settings.value( "regexpType.main", mainRegexpType_ ).toInt() ); quickfindRegexpType_ = static_cast( settings.value( "regexpType.quickfind", quickfindRegexpType_ ).toInt() ); if ( settings.contains( "quickfind.incremental" ) ) quickfindIncremental_ = settings.value( "quickfind.incremental" ).toBool(); // "Advanced" settings if ( settings.contains( "polling.enabled" ) ) pollingEnabled_ = settings.value( "polling.enabled" ).toBool(); if ( settings.contains( "polling.intervalMs" ) ) pollIntervalMs_ = settings.value( "polling.intervalMs" ).toInt(); if ( settings.contains( "session.loadLast" ) ) loadLastSession_ = settings.value( "session.loadLast" ).toBool(); // View settings if ( settings.contains( "view.overviewVisible" ) ) overviewVisible_ = settings.value( "view.overviewVisible" ).toBool(); if ( settings.contains( "view.lineNumbersVisibleInMain" ) ) lineNumbersVisibleInMain_ = settings.value( "view.lineNumbersVisibleInMain" ).toBool(); if ( settings.contains( "view.lineNumbersVisibleInFiltered" ) ) lineNumbersVisibleInFiltered_ = settings.value( "view.lineNumbersVisibleInFiltered" ).toBool(); // Some sanity check (mainly for people upgrading) if ( quickfindIncremental_ ) quickfindRegexpType_ = FixedString; // Default crawler settings if ( settings.contains( "defaultView.searchAutoRefresh" ) ) searchAutoRefresh_ = settings.value( "defaultView.searchAutoRefresh" ).toBool(); if ( settings.contains( "defaultView.searchIgnoreCase" ) ) searchIgnoreCase_ = settings.value( "defaultView.searchIgnoreCase" ).toBool(); } void Configuration::saveToStorage( QSettings& settings ) const { LOG(logDEBUG) << "Configuration::saveToStorage"; QFontInfo fi(mainFont_); settings.setValue( "mainFont.family", fi.family() ); settings.setValue( "mainFont.size", fi.pointSize() ); settings.setValue( "regexpType.main", static_cast( mainRegexpType_ ) ); settings.setValue( "regexpType.quickfind", static_cast( quickfindRegexpType_ ) ); settings.setValue( "quickfind.incremental", quickfindIncremental_ ); settings.setValue( "polling.enabled", pollingEnabled_ ); settings.setValue( "polling.intervalMs", pollIntervalMs_ ); settings.setValue( "session.loadLast", loadLastSession_); settings.setValue( "view.overviewVisible", overviewVisible_ ); settings.setValue( "view.lineNumbersVisibleInMain", lineNumbersVisibleInMain_ ); settings.setValue( "view.lineNumbersVisibleInFiltered", lineNumbersVisibleInFiltered_ ); settings.setValue( "defaultView.searchAutoRefresh", searchAutoRefresh_ ); settings.setValue( "defaultView.searchIgnoreCase", searchIgnoreCase_ ); } glogg-1.1.4/src/quickfindpattern.h0000644000175000017500000000557713076461374016150 0ustar nickonicko/* * Copyright (C) 2010, 2013 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef QUICKFINDPATTERN_H #define QUICKFINDPATTERN_H #include #include #include #include // Represents a match result for QuickFind class QuickFindMatch { public: // Construct a match (must be initialised) QuickFindMatch( int start_column, int length ) { startColumn_ = start_column; length_ = length; } // Accessor functions int startColumn() const { return startColumn_; } int length() const { return length_; } private: int startColumn_; int length_; }; // Represents a search pattern for QuickFind (without its results) class QuickFindPattern : public QObject { Q_OBJECT public: // Construct an empty search QuickFindPattern(); // Set the search to a new pattern, using the current // case status void changeSearchPattern( const QString& pattern ); // Set the search to a new pattern, as well as the case status void changeSearchPattern( const QString& pattern, bool ignoreCase ); // Returns whether the search is active (i.e. valid and non empty regexp) bool isActive() const { return active_; } // Return the text of the regex QString getPattern() const { return regexp_.pattern(); } // Returns whether the passed line match the quick find search. // If so, it populate the passed list with the list of matches // within this particular line. bool matchLine( const QString& line, QList& matches ) const; // Returns whether there is a match in the passed line, starting at // the passed column. // Results are stored internally. bool isLineMatching( const QString& line, int column = 0 ) const; // Same as isLineMatching but search backward bool isLineMatchingBackward( const QString& line, int column = -1 ) const; // Must be called when isLineMatching returns 'true', returns // the position of the first match found. void getLastMatch( int* start_col, int* end_col ) const; signals: // Sent when the pattern is changed void patternUpdated(); private: bool active_; QRegExp regexp_; mutable int lastMatchStart_; mutable int lastMatchEnd_; }; #endif glogg-1.1.4/src/encodingspeculator.cpp0000644000175000017500000000663013076461374017007 0ustar nickonicko/* * Copyright (C) 2016 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include "encodingspeculator.h" #include void EncodingSpeculator::inject_byte( uint8_t byte ) { if ( ! ( byte & 0x80 ) ) { // 7-bit character, all fine } else { switch ( state_ ) { case State::ASCIIOnly: case State::ValidUTF8: if ( ( byte & 0xE0 ) == 0xC0 ) { state_ = State::UTF8LeadingByteSeen; code_point_ = ( byte & 0x1F ) << 6; continuation_left_ = 1; min_value_ = 0x80; // std::cout << "Lead: cp= " << std::hex << code_point_ << std::endl; } else if ( ( byte & 0xF0 ) == 0xE0 ) { state_ = State::UTF8LeadingByteSeen; code_point_ = ( byte & 0x0F ) << 12; continuation_left_ = 2; min_value_ = 0x800; // std::cout << "Lead 3: cp= " << std::hex << code_point_ << std::endl; } else if ( ( byte & 0xF8 ) == 0xF0 ) { state_ = State::UTF8LeadingByteSeen; code_point_ = ( byte & 0x07 ) << 18; continuation_left_ = 3; min_value_ = 0x800; // std::cout << "Lead 4: cp= " << std::hex << code_point_ << std::endl; } else { state_ = State::Unknown8Bit; } break; case State::UTF8LeadingByteSeen: if ( ( byte & 0xC0 ) == 0x80 ) { --continuation_left_; code_point_ |= ( byte & 0x3F ) << (continuation_left_ * 6); // std::cout << "Cont: cp= " << std::hex << code_point_ << std::endl; if ( continuation_left_ == 0 ) { if ( code_point_ >= min_value_ ) state_ = State::ValidUTF8; else state_ = State::Unknown8Bit; } } else { state_ = State::Unknown8Bit; } break; } // state_ = State::Unknown8Bit; } } EncodingSpeculator::Encoding EncodingSpeculator::guess() const { Encoding guess; switch ( state_ ) { case State::ASCIIOnly: guess = Encoding::ASCII7; break; case State::Unknown8Bit: case State::UTF8LeadingByteSeen: guess = Encoding::ASCII8; break; case State::ValidUTF8: guess = Encoding::UTF8; break; default: guess = Encoding::ASCII8; } return guess; } glogg-1.1.4/src/abstractlogview.cpp0000644000175000017500000015666013076461374016330 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2011, 2012, 2013, 2015 Nicolas Bonnefon * and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ // This file implements the AbstractLogView base class. // Most of the actual drawing and event management common to the two views // is implemented in this class. The class only calls protected virtual // functions when view specific behaviour is desired, using the template // pattern. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "log.h" #include "persistentinfo.h" #include "filterset.h" #include "logmainview.h" #include "quickfind.h" #include "quickfindpattern.h" #include "overview.h" #include "configuration.h" namespace { int mapPullToFollowLength( int length ); }; namespace { int countDigits( quint64 n ) { if (n == 0) return 1; // We must force the compiler to not store intermediate results // in registers because this causes incorrect result on some // systems under optimizations level >0. For the skeptical: // // #include // #include // int main(int argc, char **argv) { // (void)argc; // long long int n = atoll(argv[1]); // return floor( log( n ) / log( 10 ) + 1 ); // } // // This is on Thinkpad T60 (Genuine Intel(R) CPU T2300). // $ g++ --version // g++ (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3 // $ g++ -O0 -Wall -W -o math math.cpp -lm; ./math 10; echo $? // 2 // $ g++ -O1 -Wall -W -o math math.cpp -lm; ./math 10; echo $? // 1 // // A fix is to (1) explicitly place intermediate results in // variables *and* (2) [A] mark them as 'volatile', or [B] pass // -ffloat-store to g++ (note that approach [A] is more portable). volatile qreal ln_n = qLn( n ); volatile qreal ln_10 = qLn( 10 ); volatile qreal lg_n = ln_n / ln_10; volatile qreal lg_n_1 = lg_n + 1; volatile qreal fl_lg_n_1 = qFloor( lg_n_1 ); return fl_lg_n_1; } } // anon namespace LineChunk::LineChunk( int first_col, int last_col, ChunkType type ) { // LOG(logDEBUG) << "new LineChunk: " << first_col << " " << last_col; start_ = first_col; end_ = last_col; type_ = type; } QList LineChunk::select( int sel_start, int sel_end ) const { QList list; if ( ( sel_start < start_ ) && ( sel_end < start_ ) ) { // Selection BEFORE this chunk: no change list << LineChunk( *this ); } else if ( sel_start > end_ ) { // Selection AFTER this chunk: no change list << LineChunk( *this ); } else /* if ( ( sel_start >= start_ ) && ( sel_end <= end_ ) ) */ { // We only want to consider what's inside THIS chunk sel_start = qMax( sel_start, start_ ); sel_end = qMin( sel_end, end_ ); if ( sel_start > start_ ) list << LineChunk( start_, sel_start - 1, type_ ); list << LineChunk( sel_start, sel_end, Selected ); if ( sel_end < end_ ) list << LineChunk( sel_end + 1, end_, type_ ); } return list; } inline void LineDrawer::addChunk( int first_col, int last_col, QColor fore, QColor back ) { if ( first_col < 0 ) first_col = 0; int length = last_col - first_col + 1; if ( length > 0 ) { list << Chunk ( first_col, length, fore, back ); } } inline void LineDrawer::addChunk( const LineChunk& chunk, QColor fore, QColor back ) { int first_col = chunk.start(); int last_col = chunk.end(); addChunk( first_col, last_col, fore, back ); } inline void LineDrawer::draw( QPainter& painter, int initialXPos, int initialYPos, int line_width, const QString& line, int leftExtraBackgroundPx ) { QFontMetrics fm = painter.fontMetrics(); const int fontHeight = fm.height(); const int fontAscent = fm.ascent(); // For some reason on Qt 4.8.2 for Win, maxWidth() is wrong but the // following give the right result, not sure why: const int fontWidth = fm.width( QChar('a') ); int xPos = initialXPos; int yPos = initialYPos; foreach ( Chunk chunk, list ) { // Draw each chunk // LOG(logDEBUG) << "Chunk: " << chunk.start() << " " << chunk.length(); QString cutline = line.mid( chunk.start(), chunk.length() ); const int chunk_width = cutline.length() * fontWidth; if ( xPos == initialXPos ) { // First chunk, we extend the left background a bit, // it looks prettier. painter.fillRect( xPos - leftExtraBackgroundPx, yPos, chunk_width + leftExtraBackgroundPx, fontHeight, chunk.backColor() ); } else { // other chunks... painter.fillRect( xPos, yPos, chunk_width, fontHeight, chunk.backColor() ); } painter.setPen( chunk.foreColor() ); painter.drawText( xPos, yPos + fontAscent, cutline ); xPos += chunk_width; } // Draw the empty block at the end of the line int blank_width = line_width - xPos; if ( blank_width > 0 ) painter.fillRect( xPos, yPos, blank_width, fontHeight, backColor_ ); } const int DigitsBuffer::timeout_ = 2000; DigitsBuffer::DigitsBuffer() : QObject() { } void DigitsBuffer::reset() { LOG(logDEBUG) << "DigitsBuffer::reset()"; timer_.stop(); digits_.clear(); } void DigitsBuffer::add( char character ) { LOG(logDEBUG) << "DigitsBuffer::add()"; digits_.append( QChar( character ) ); timer_.start( timeout_ , this ); } int DigitsBuffer::content() { int result = digits_.toInt(); reset(); return result; } void DigitsBuffer::timerEvent( QTimerEvent* event ) { if ( event->timerId() == timer_.timerId() ) { reset(); } else { QObject::timerEvent( event ); } } AbstractLogView::AbstractLogView(const AbstractLogData* newLogData, const QuickFindPattern* const quickFindPattern, QWidget* parent) : QAbstractScrollArea( parent ), followElasticHook_( HOOK_THRESHOLD ), lineNumbersVisible_( false ), selectionStartPos_(), selectionCurrentEndPos_(), autoScrollTimer_(), selection_(), quickFindPattern_( quickFindPattern ), quickFind_( newLogData, &selection_, quickFindPattern ) { logData = newLogData; followMode_ = false; selectionStarted_ = false; markingClickInitiated_ = false; firstLine = 0; lastLineAligned = false; firstCol = 0; overview_ = NULL; overviewWidget_ = NULL; // Display leftMarginPx_ = 0; // Fonts (sensible default for overview widget) charWidth_ = 1; charHeight_ = 10; // Create the viewport QWidget setViewport( 0 ); // Hovering setMouseTracking( true ); lastHoveredLine_ = -1; // Init the popup menu createMenu(); // Signals connect( quickFindPattern_, SIGNAL( patternUpdated() ), this, SLOT ( handlePatternUpdated() ) ); connect( &quickFind_, SIGNAL( notify( const QFNotification& ) ), this, SIGNAL( notifyQuickFind( const QFNotification& ) ) ); connect( &quickFind_, SIGNAL( clearNotification() ), this, SIGNAL( clearQuickFindNotification() ) ); connect( &followElasticHook_, SIGNAL( lengthChanged() ), this, SLOT( repaint() ) ); connect( &followElasticHook_, SIGNAL( hooked( bool ) ), this, SIGNAL( followModeChanged( bool ) ) ); } AbstractLogView::~AbstractLogView() { } // // Received events // void AbstractLogView::changeEvent( QEvent* changeEvent ) { QAbstractScrollArea::changeEvent( changeEvent ); // Stop the timer if the widget becomes inactive if ( changeEvent->type() == QEvent::ActivationChange ) { if ( ! isActiveWindow() ) autoScrollTimer_.stop(); } viewport()->update(); } void AbstractLogView::mousePressEvent( QMouseEvent* mouseEvent ) { static std::shared_ptr config = Persistent( "settings" ); if ( mouseEvent->button() == Qt::LeftButton ) { int line = convertCoordToLine( mouseEvent->y() ); if ( mouseEvent->modifiers() & Qt::ShiftModifier ) { selection_.selectRangeFromPrevious( line ); emit updateLineNumber( line ); update(); } else { if ( mouseEvent->x() < bulletZoneWidthPx_ ) { // Mark a line if it is clicked in the left margin // (only if click and release in the same area) markingClickInitiated_ = true; markingClickLine_ = line; } else { // Select the line, and start a selection if ( line < logData->getNbLine() ) { selection_.selectLine( line ); emit updateLineNumber( line ); emit newSelection( line ); } // Remember the click in case we're starting a selection selectionStarted_ = true; selectionStartPos_ = convertCoordToFilePos( mouseEvent->pos() ); selectionCurrentEndPos_ = selectionStartPos_; } } // Invalidate our cache textAreaCache_.invalid_ = true; } else if ( mouseEvent->button() == Qt::RightButton ) { // Prepare the popup depending on selection type if ( selection_.isSingleLine() ) { copyAction_->setText( "&Copy this line" ); } else { copyAction_->setText( "&Copy" ); copyAction_->setStatusTip( tr("Copy the selection") ); } if ( selection_.isPortion() ) { findNextAction_->setEnabled( true ); findPreviousAction_->setEnabled( true ); addToSearchAction_->setEnabled( true ); } else { findNextAction_->setEnabled( false ); findPreviousAction_->setEnabled( false ); addToSearchAction_->setEnabled( false ); } // "Add to search" only makes sense in regexp mode if ( config->mainRegexpType() != ExtendedRegexp ) addToSearchAction_->setEnabled( false ); // Display the popup (blocking) popupMenu_->exec( QCursor::pos() ); } emit activity(); } void AbstractLogView::mouseMoveEvent( QMouseEvent* mouseEvent ) { // Selection implementation if ( selectionStarted_ ) { // Invalidate our cache textAreaCache_.invalid_ = true; QPoint thisEndPos = convertCoordToFilePos( mouseEvent->pos() ); if ( thisEndPos != selectionCurrentEndPos_ ) { // Are we on a different line? if ( selectionStartPos_.y() != thisEndPos.y() ) { if ( thisEndPos.y() != selectionCurrentEndPos_.y() ) { // This is a 'range' selection selection_.selectRange( selectionStartPos_.y(), thisEndPos.y() ); emit updateLineNumber( thisEndPos.y() ); update(); } } // So we are on the same line. Are we moving horizontaly? else if ( thisEndPos.x() != selectionCurrentEndPos_.x() ) { // This is a 'portion' selection selection_.selectPortion( thisEndPos.y(), selectionStartPos_.x(), thisEndPos.x() ); update(); } // On the same line, and moving vertically then else { // This is a 'line' selection selection_.selectLine( thisEndPos.y() ); emit updateLineNumber( thisEndPos.y() ); update(); } selectionCurrentEndPos_ = thisEndPos; // Do we need to scroll while extending the selection? QRect visible = viewport()->rect(); if ( visible.contains( mouseEvent->pos() ) ) autoScrollTimer_.stop(); else if ( ! autoScrollTimer_.isActive() ) autoScrollTimer_.start( 100, this ); } } else { considerMouseHovering( mouseEvent->x(), mouseEvent->y() ); } } void AbstractLogView::mouseReleaseEvent( QMouseEvent* mouseEvent ) { if ( markingClickInitiated_ ) { markingClickInitiated_ = false; int line = convertCoordToLine( mouseEvent->y() ); if ( line == markingClickLine_ ) { // Invalidate our cache textAreaCache_.invalid_ = true; emit markLine( line ); } } else { selectionStarted_ = false; if ( autoScrollTimer_.isActive() ) autoScrollTimer_.stop(); updateGlobalSelection(); } } void AbstractLogView::mouseDoubleClickEvent( QMouseEvent* mouseEvent ) { if ( mouseEvent->button() == Qt::LeftButton ) { // Invalidate our cache textAreaCache_.invalid_ = true; const QPoint pos = convertCoordToFilePos( mouseEvent->pos() ); selectWordAtPosition( pos ); } emit activity(); } void AbstractLogView::timerEvent( QTimerEvent* timerEvent ) { if ( timerEvent->timerId() == autoScrollTimer_.timerId() ) { QRect visible = viewport()->rect(); const QPoint globalPos = QCursor::pos(); const QPoint pos = viewport()->mapFromGlobal( globalPos ); QMouseEvent ev( QEvent::MouseMove, pos, globalPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier ); mouseMoveEvent( &ev ); int deltaX = qMax( pos.x() - visible.left(), visible.right() - pos.x() ) - visible.width(); int deltaY = qMax( pos.y() - visible.top(), visible.bottom() - pos.y() ) - visible.height(); int delta = qMax( deltaX, deltaY ); if ( delta >= 0 ) { if ( delta < 7 ) delta = 7; int timeout = 4900 / ( delta * delta ); autoScrollTimer_.start( timeout, this ); if ( deltaX > 0 ) horizontalScrollBar()->triggerAction( pos.x() 0 ) verticalScrollBar()->triggerAction( pos.y() modifiers() & Qt::ControlModifier) == Qt::ControlModifier; bool shiftModifier = (keyEvent->modifiers() & Qt::ShiftModifier) == Qt::ShiftModifier; bool noModifier = keyEvent->modifiers() == Qt::NoModifier; if ( keyEvent->key() == Qt::Key_Left && noModifier ) horizontalScrollBar()->triggerAction(QScrollBar::SliderPageStepSub); else if ( keyEvent->key() == Qt::Key_Right && noModifier ) horizontalScrollBar()->triggerAction(QScrollBar::SliderPageStepAdd); else if ( keyEvent->key() == Qt::Key_Home && !controlModifier) jumpToStartOfLine(); else if ( keyEvent->key() == Qt::Key_End && !controlModifier) jumpToRightOfScreen(); else if ( (keyEvent->key() == Qt::Key_PageDown && controlModifier) || (keyEvent->key() == Qt::Key_End && controlModifier) ) { disableFollow(); // duplicate of 'G' action. selection_.selectLine( logData->getNbLine() - 1 ); emit updateLineNumber( logData->getNbLine() - 1 ); jumpToBottom(); } else if ( (keyEvent->key() == Qt::Key_PageUp && controlModifier) || (keyEvent->key() == Qt::Key_Home && controlModifier) ) { disableFollow(); // like 'g' but 0 input first line action. selectAndDisplayLine( 0 ); emit updateLineNumber( 0 ); } else if ( keyEvent->key() == Qt::Key_F3 && !shiftModifier ) searchNext(); // duplicate of 'n' action. else if ( keyEvent->key() == Qt::Key_F3 && shiftModifier ) searchPrevious(); // duplicate of 'N' action. else { const char character = (keyEvent->text())[0].toLatin1(); if ( keyEvent->modifiers() == Qt::NoModifier && ( character >= '0' ) && ( character <= '9' ) ) { // Adds the digit to the timed buffer digitsBuffer_.add( character ); } else { switch ( (keyEvent->text())[0].toLatin1() ) { case 'j': { int delta = qMax( 1, digitsBuffer_.content() ); disableFollow(); //verticalScrollBar()->triggerAction( //QScrollBar::SliderSingleStepAdd); moveSelection( delta ); break; } case 'k': { int delta = qMin( -1, - digitsBuffer_.content() ); disableFollow(); //verticalScrollBar()->triggerAction( //QScrollBar::SliderSingleStepSub); moveSelection( delta ); break; } case 'h': horizontalScrollBar()->triggerAction( QScrollBar::SliderSingleStepSub); break; case 'l': horizontalScrollBar()->triggerAction( QScrollBar::SliderSingleStepAdd); break; case '0': jumpToStartOfLine(); break; case '$': jumpToEndOfLine(); break; case 'g': { int newLine = qMax( 0, digitsBuffer_.content() - 1 ); if ( newLine >= logData->getNbLine() ) newLine = logData->getNbLine() - 1; disableFollow(); selectAndDisplayLine( newLine ); emit updateLineNumber( newLine ); break; } case 'G': disableFollow(); selection_.selectLine( logData->getNbLine() - 1 ); emit updateLineNumber( logData->getNbLine() - 1 ); jumpToBottom(); break; case 'n': emit searchNext(); break; case 'N': emit searchPrevious(); break; case '*': // Use the selected 'word' and search forward findNextSelected(); break; case '#': // Use the selected 'word' and search backward findPreviousSelected(); break; default: keyEvent->ignore(); } } } if ( keyEvent->isAccepted() ) { emit activity(); } else { // Only pass bare keys to the superclass this is so that // shortcuts such as Ctrl+Alt+Arrow are handled by the parent. LOG(logDEBUG) << std::hex << keyEvent->modifiers(); if ( keyEvent->modifiers() == Qt::NoModifier || keyEvent->modifiers() == Qt::KeypadModifier ) { QAbstractScrollArea::keyPressEvent( keyEvent ); } } } void AbstractLogView::wheelEvent( QWheelEvent* wheelEvent ) { emit activity(); // LOG(logDEBUG) << "wheelEvent"; // This is to handle the case where follow mode is on, but the user // has moved using the scroll bar. We take them back to the bottom. if ( followMode_ ) jumpToBottom(); int y_delta = 0; if ( verticalScrollBar()->value() == verticalScrollBar()->maximum() ) { // First see if we need to block the elastic (on Mac) if ( wheelEvent->phase() == Qt::ScrollBegin ) followElasticHook_.hold(); else if ( wheelEvent->phase() == Qt::ScrollEnd ) followElasticHook_.release(); auto pixel_delta = wheelEvent->pixelDelta(); if ( pixel_delta.isNull() ) { y_delta = wheelEvent->angleDelta().y() / 0.7; } else { y_delta = pixel_delta.y(); } // LOG(logDEBUG) << "Elastic " << y_delta; followElasticHook_.move( - y_delta ); } // LOG(logDEBUG) << "Length = " << followElasticHook_.length(); if ( followElasticHook_.length() == 0 && !followElasticHook_.isHooked() ) { QAbstractScrollArea::wheelEvent( wheelEvent ); } } void AbstractLogView::resizeEvent( QResizeEvent* ) { if ( logData == NULL ) return; LOG(logDEBUG) << "resizeEvent received"; updateDisplaySize(); } bool AbstractLogView::event( QEvent* e ) { LOG(logDEBUG4) << "Event! Type: " << e->type(); // Make sure we ignore the gesture events as // they seem to be accepted by default. if ( e->type() == QEvent::Gesture ) { auto gesture_event = dynamic_cast( e ); if ( gesture_event ) { foreach( QGesture* gesture, gesture_event->gestures() ) { LOG(logDEBUG4) << "Gesture: " << gesture->gestureType(); gesture_event->ignore( gesture ); } // Ensure the event is sent up to parents who might care return false; } } return QAbstractScrollArea::event( e ); } void AbstractLogView::scrollContentsBy( int dx, int dy ) { LOG(logDEBUG) << "scrollContentsBy received " << dy << "position " << verticalScrollBar()->value(); int32_t last_top_line = ( logData->getNbLine() - getNbVisibleLines() ); if ( ( last_top_line > 0 ) && verticalScrollBar()->value() > last_top_line ) { // The user is going further than the last line, we need to lock the last line at the bottom LOG(logDEBUG) << "scrollContentsBy beyond!"; firstLine = last_top_line; lastLineAligned = true; } else { firstLine = verticalScrollBar()->value(); lastLineAligned = false; } firstCol = (firstCol - dx) > 0 ? firstCol - dx : 0; LineNumber last_line = firstLine + getNbVisibleLines(); // Update the overview if we have one if ( overview_ != NULL ) overview_->updateCurrentPosition( firstLine, last_line ); // Are we hovering over a new line? const QPoint mouse_pos = mapFromGlobal( QCursor::pos() ); considerMouseHovering( mouse_pos.x(), mouse_pos.y() ); // Redraw update(); } void AbstractLogView::paintEvent( QPaintEvent* paintEvent ) { const QRect invalidRect = paintEvent->rect(); if ( (invalidRect.isEmpty()) || (logData == NULL) ) return; LOG(logDEBUG4) << "paintEvent received, firstLine=" << firstLine << " lastLineAligned=" << lastLineAligned << " rect: " << invalidRect.topLeft().x() << ", " << invalidRect.topLeft().y() << ", " << invalidRect.bottomRight().x() << ", " << invalidRect.bottomRight().y(); #ifdef GLOGG_PERF_MEASURE_FPS static uint32_t maxline = logData->getNbLine(); if ( ! perfCounter_.addEvent() && logData->getNbLine() > maxline ) { LOG(logWARNING) << "Redraw per second: " << perfCounter_.readAndReset() << " lines: " << logData->getNbLine(); perfCounter_.addEvent(); maxline = logData->getNbLine(); } #endif auto start = std::chrono::system_clock::now(); // Can we use our cache? int32_t delta_y = textAreaCache_.first_line_ - firstLine; if ( textAreaCache_.invalid_ || ( textAreaCache_.first_column_ != firstCol ) ) { // Force a full redraw delta_y = INT32_MAX; } if ( delta_y != 0 ) { // Full or partial redraw drawTextArea( &textAreaCache_.pixmap_, delta_y ); textAreaCache_.invalid_ = false; textAreaCache_.first_line_ = firstLine; textAreaCache_.first_column_ = firstCol; LOG(logDEBUG) << "End of writing " << std::chrono::duration_cast ( std::chrono::system_clock::now() - start ).count(); } else { // Use the cache as is: nothing to do! } // Height including the potentially invisible last line const int whole_height = getNbVisibleLines() * charHeight_; // Height in pixels of the "pull to follow" bottom bar. int pullToFollowHeight = mapPullToFollowLength( followElasticHook_.length() ) + ( followElasticHook_.isHooked() ? ( whole_height - viewport()->height() ) + PULL_TO_FOLLOW_HOOKED_HEIGHT : 0 ); if ( pullToFollowHeight && ( pullToFollowCache_.nb_columns_ != getNbVisibleCols() ) ) { LOG(logDEBUG) << "Drawing pull to follow bar"; pullToFollowCache_.pixmap_ = drawPullToFollowBar( viewport()->width(), viewport()->devicePixelRatio() ); pullToFollowCache_.nb_columns_ = getNbVisibleCols(); } QPainter devicePainter( viewport() ); int drawingTopPosition = - pullToFollowHeight; int drawingPullToFollowTopPosition = drawingTopPosition + whole_height; // This is to cover the special case where there is less than a screenful // worth of data, we want to see the document from the top, rather than // pushing the first couple of lines above the viewport. if ( followElasticHook_.isHooked() && ( logData->getNbLine() < getNbVisibleLines() ) ) { drawingTopOffset_ = 0; drawingTopPosition += ( whole_height - viewport()->height() ) + PULL_TO_FOLLOW_HOOKED_HEIGHT; drawingPullToFollowTopPosition = drawingTopPosition + viewport()->height() - PULL_TO_FOLLOW_HOOKED_HEIGHT; } // This is the case where the user is on the 'extra' slot at the end // and is aligned on the last line (but no elastic shown) else if ( lastLineAligned && !followElasticHook_.isHooked() ) { drawingTopOffset_ = - ( whole_height - viewport()->height() ); drawingTopPosition += drawingTopOffset_; drawingPullToFollowTopPosition = drawingTopPosition + whole_height; } else { drawingTopOffset_ = - pullToFollowHeight; } devicePainter.drawPixmap( 0, drawingTopPosition, textAreaCache_.pixmap_ ); // Draw the "pull to follow" zone if needed if ( pullToFollowHeight ) { devicePainter.drawPixmap( 0, drawingPullToFollowTopPosition, pullToFollowCache_.pixmap_ ); } LOG(logDEBUG) << "End of repaint " << std::chrono::duration_cast ( std::chrono::system_clock::now() - start ).count(); } // These two functions are virtual and this implementation is clearly // only valid for a non-filtered display. // We count on the 'filtered' derived classes to override them. qint64 AbstractLogView::displayLineNumber( int lineNumber ) const { return lineNumber + 1; // show a 1-based index } qint64 AbstractLogView::maxDisplayLineNumber() const { return logData->getNbLine(); } void AbstractLogView::setOverview( Overview* overview, OverviewWidget* overview_widget ) { overview_ = overview; overviewWidget_ = overview_widget; if ( overviewWidget_ ) { connect( overviewWidget_, SIGNAL( lineClicked ( int ) ), this, SIGNAL( followDisabled() ) ); connect( overviewWidget_, SIGNAL( lineClicked ( int ) ), this, SLOT( jumpToLine( int ) ) ); } refreshOverview(); } void AbstractLogView::searchUsingFunction( qint64 (QuickFind::*search_function)() ) { disableFollow(); int line = (quickFind_.*search_function)(); if ( line >= 0 ) { LOG(logDEBUG) << "search " << line; displayLine( line ); emit updateLineNumber( line ); } } void AbstractLogView::searchForward() { searchUsingFunction( &QuickFind::searchForward ); } void AbstractLogView::searchBackward() { searchUsingFunction( &QuickFind::searchBackward ); } void AbstractLogView::incrementallySearchForward() { searchUsingFunction( &QuickFind::incrementallySearchForward ); } void AbstractLogView::incrementallySearchBackward() { searchUsingFunction( &QuickFind::incrementallySearchBackward ); } void AbstractLogView::incrementalSearchAbort() { quickFind_.incrementalSearchAbort(); emit changeQuickFind( "", QuickFindMux::Forward ); } void AbstractLogView::incrementalSearchStop() { quickFind_.incrementalSearchStop(); } void AbstractLogView::followSet( bool checked ) { followMode_ = checked; followElasticHook_.hook( checked ); update(); if ( checked ) jumpToBottom(); } void AbstractLogView::refreshOverview() { assert( overviewWidget_ ); // Create space for the Overview if needed if ( ( getOverview() != NULL ) && getOverview()->isVisible() ) { setViewportMargins( 0, 0, OVERVIEW_WIDTH, 0 ); overviewWidget_->show(); } else { setViewportMargins( 0, 0, 0, 0 ); overviewWidget_->hide(); } } // Reset the QuickFind when the pattern is changed. void AbstractLogView::handlePatternUpdated() { LOG(logDEBUG) << "AbstractLogView::handlePatternUpdated()"; quickFind_.resetLimits(); update(); } // OR the current with the current search expression void AbstractLogView::addToSearch() { if ( selection_.isPortion() ) { LOG(logDEBUG) << "AbstractLogView::addToSearch()"; emit addToSearch( selection_.getSelectedText( logData ) ); } else { LOG(logERROR) << "AbstractLogView::addToSearch called for a wrong type of selection"; } } // Find next occurence of the selected text (*) void AbstractLogView::findNextSelected() { // Use the selected 'word' and search forward if ( selection_.isPortion() ) { emit changeQuickFind( selection_.getSelectedText( logData ), QuickFindMux::Forward ); emit searchNext(); } } // Find next previous of the selected text (#) void AbstractLogView::findPreviousSelected() { if ( selection_.isPortion() ) { emit changeQuickFind( selection_.getSelectedText( logData ), QuickFindMux::Backward ); emit searchNext(); } } // Copy the selection to the clipboard void AbstractLogView::copy() { static QClipboard* clipboard = QApplication::clipboard(); clipboard->setText( selection_.getSelectedText( logData ) ); } // // Public functions // void AbstractLogView::updateData() { LOG(logDEBUG) << "AbstractLogView::updateData"; // Check the top Line is within range if ( firstLine >= logData->getNbLine() ) { firstLine = 0; firstCol = 0; verticalScrollBar()->setValue( 0 ); horizontalScrollBar()->setValue( 0 ); } // Crop selection if it become out of range selection_.crop( logData->getNbLine() - 1 ); // Adapt the scroll bars to the new content updateScrollBars(); // Calculate the index of the last line shown LineNumber last_line = std::min( static_cast( logData->getNbLine() ), static_cast( firstLine + getNbVisibleLines() ) ); // Reset the QuickFind in case we have new stuff to search into quickFind_.resetLimits(); if ( followMode_ ) jumpToBottom(); // Update the overview if we have one if ( overview_ != NULL ) overview_->updateCurrentPosition( firstLine, last_line ); // Invalidate our cache textAreaCache_.invalid_ = true; // Repaint! update(); } void AbstractLogView::updateDisplaySize() { // Font is assumed to be mono-space (is restricted by options dialog) QFontMetrics fm = fontMetrics(); charHeight_ = fm.height(); // For some reason on Qt 4.8.2 for Win, maxWidth() is wrong but the // following give the right result, not sure why: charWidth_ = fm.width( QChar('a') ); // Update the scroll bars updateScrollBars(); verticalScrollBar()->setPageStep( getNbVisibleLines() ); if ( followMode_ ) jumpToBottom(); LOG(logDEBUG) << "viewport.width()=" << viewport()->width(); LOG(logDEBUG) << "viewport.height()=" << viewport()->height(); LOG(logDEBUG) << "width()=" << width(); LOG(logDEBUG) << "height()=" << height(); if ( overviewWidget_ ) overviewWidget_->setGeometry( viewport()->width() + 2, 1, OVERVIEW_WIDTH - 1, viewport()->height() ); // Our text area cache is now invalid textAreaCache_.invalid_ = true; textAreaCache_.pixmap_ = QPixmap { viewport()->width() * viewport()->devicePixelRatio(), static_cast( getNbVisibleLines() ) * charHeight_ * viewport()->devicePixelRatio() }; textAreaCache_.pixmap_.setDevicePixelRatio( viewport()->devicePixelRatio() ); } int AbstractLogView::getTopLine() const { return firstLine; } QString AbstractLogView::getSelection() const { return selection_.getSelectedText( logData ); } void AbstractLogView::selectAll() { selection_.selectRange( 0, logData->getNbLine() - 1 ); textAreaCache_.invalid_ = true; update(); } void AbstractLogView::selectAndDisplayLine( int line ) { disableFollow(); selection_.selectLine( line ); displayLine( line ); emit updateLineNumber( line ); } // The difference between this function and displayLine() is quite // subtle: this one always jump, even if the line passed is visible. void AbstractLogView::jumpToLine( int line ) { // Put the selected line in the middle if possible int newTopLine = line - ( getNbVisibleLines() / 2 ); if ( newTopLine < 0 ) newTopLine = 0; // This will also trigger a scrollContents event verticalScrollBar()->setValue( newTopLine ); } void AbstractLogView::setLineNumbersVisible( bool lineNumbersVisible ) { lineNumbersVisible_ = lineNumbersVisible; } void AbstractLogView::forceRefresh() { // Invalidate our cache textAreaCache_.invalid_ = true; } // // Private functions // // Returns the number of lines visible in the viewport LineNumber AbstractLogView::getNbVisibleLines() const { return static_cast( viewport()->height() / charHeight_ + 1 ); } // Returns the number of columns visible in the viewport int AbstractLogView::getNbVisibleCols() const { return ( viewport()->width() - leftMarginPx_ ) / charWidth_ + 1; } // Converts the mouse x, y coordinates to the line number in the file int AbstractLogView::convertCoordToLine(int yPos) const { int line = firstLine + ( yPos - drawingTopOffset_ ) / charHeight_; return line; } // Converts the mouse x, y coordinates to the char coordinates (in the file) // This function ensure the pos exists in the file. QPoint AbstractLogView::convertCoordToFilePos( const QPoint& pos ) const { int line = convertCoordToLine( pos.y() ); if ( line >= logData->getNbLine() ) line = logData->getNbLine() - 1; if ( line < 0 ) line = 0; // Determine column in screen space and convert it to file space int column = firstCol + ( pos.x() - leftMarginPx_ ) / charWidth_; QString this_line = logData->getExpandedLineString( line ); const int length = this_line.length(); if ( column >= length ) column = length - 1; if ( column < 0 ) column = 0; LOG(logDEBUG4) << "AbstractLogView::convertCoordToFilePos col=" << column << " line=" << line; QPoint point( column, line ); return point; } // Makes the widget adjust itself to display the passed line. // Doing so, it will throw itself a scrollContents event. void AbstractLogView::displayLine( LineNumber line ) { // If the line is already the screen if ( ( line >= firstLine ) && ( line < ( firstLine + getNbVisibleLines() ) ) ) { // Invalidate our cache textAreaCache_.invalid_ = true; // ... don't scroll and just repaint update(); } else { jumpToLine( line ); } } // Move the selection up and down by the passed number of lines void AbstractLogView::moveSelection( int delta ) { LOG(logDEBUG) << "AbstractLogView::moveSelection delta=" << delta; QList selection = selection_.getLines(); int new_line; // If nothing is selected, do as if line -1 was. if ( selection.isEmpty() ) selection.append( -1 ); if ( delta < 0 ) new_line = selection.first() + delta; else new_line = selection.last() + delta; if ( new_line < 0 ) new_line = 0; else if ( new_line >= logData->getNbLine() ) new_line = logData->getNbLine() - 1; // Select and display the new line selection_.selectLine( new_line ); displayLine( new_line ); emit updateLineNumber( new_line ); emit newSelection( new_line ); } // Make the start of the lines visible void AbstractLogView::jumpToStartOfLine() { horizontalScrollBar()->setValue( 0 ); } // Make the end of the lines in the selection visible void AbstractLogView::jumpToEndOfLine() { QList selection = selection_.getLines(); // Search the longest line in the selection int max_length = 0; foreach ( int line, selection ) { int length = logData->getLineLength( line ); if ( length > max_length ) max_length = length; } horizontalScrollBar()->setValue( max_length - getNbVisibleCols() ); } // Make the end of the lines on the screen visible void AbstractLogView::jumpToRightOfScreen() { QList selection = selection_.getLines(); // Search the longest line on screen int max_length = 0; for ( auto i = firstLine; i <= ( firstLine + getNbVisibleLines() ); i++ ) { int length = logData->getLineLength( i ); if ( length > max_length ) max_length = length; } horizontalScrollBar()->setValue( max_length - getNbVisibleCols() ); } // Jump to the first line void AbstractLogView::jumpToTop() { // This will also trigger a scrollContents event verticalScrollBar()->setValue( 0 ); update(); // in case the screen hasn't moved } // Jump to the last line void AbstractLogView::jumpToBottom() { const int new_top_line = qMax( logData->getNbLine() - getNbVisibleLines() + 1, 0LL ); // This will also trigger a scrollContents event verticalScrollBar()->setValue( new_top_line ); update(); // in case the screen hasn't moved } // Returns whether the character passed is a 'word' character inline bool AbstractLogView::isCharWord( char c ) { if ( ( ( c >= 'A' ) && ( c <= 'Z' ) ) || ( ( c >= 'a' ) && ( c <= 'z' ) ) || ( ( c >= '0' ) && ( c <= '9' ) ) || ( ( c == '_' ) ) ) return true; else return false; } // Select the word under the given position void AbstractLogView::selectWordAtPosition( const QPoint& pos ) { const int x = pos.x(); const QString line = logData->getExpandedLineString( pos.y() ); if ( isCharWord( line[x].toLatin1() ) ) { // Search backward for the first character in the word int currentPos = x; for ( ; currentPos > 0; currentPos-- ) if ( ! isCharWord( line[currentPos].toLatin1() ) ) break; // Exclude the first char of the line if needed if ( ! isCharWord( line[currentPos].toLatin1() ) ) currentPos++; int start = currentPos; // Now search for the end currentPos = x; for ( ; currentPos < line.length() - 1; currentPos++ ) if ( ! isCharWord( line[currentPos].toLatin1() ) ) break; // Exclude the last char of the line if needed if ( ! isCharWord( line[currentPos].toLatin1() ) ) currentPos--; int end = currentPos; selection_.selectPortion( pos.y(), start, end ); updateGlobalSelection(); update(); } } // Update the system global (middle click) selection (X11 only) void AbstractLogView::updateGlobalSelection() { static QClipboard* const clipboard = QApplication::clipboard(); // Updating it only for "non-trivial" (range or portion) selections if ( ! selection_.isSingleLine() ) clipboard->setText( selection_.getSelectedText( logData ), QClipboard::Selection ); } // Create the pop-up menu void AbstractLogView::createMenu() { copyAction_ = new QAction( tr("&Copy"), this ); // No text as this action title depends on the type of selection connect( copyAction_, SIGNAL(triggered()), this, SLOT(copy()) ); // For '#' and '*', shortcuts doesn't seem to work but // at least it displays them in the menu, we manually handle those keys // as keys event anyway (in keyPressEvent). findNextAction_ = new QAction(tr("Find &next"), this); findNextAction_->setShortcut( Qt::Key_Asterisk ); findNextAction_->setStatusTip( tr("Find the next occurence") ); connect( findNextAction_, SIGNAL(triggered()), this, SLOT( findNextSelected() ) ); findPreviousAction_ = new QAction( tr("Find &previous"), this ); findPreviousAction_->setShortcut( tr("#") ); findPreviousAction_->setStatusTip( tr("Find the previous occurence") ); connect( findPreviousAction_, SIGNAL(triggered()), this, SLOT( findPreviousSelected() ) ); addToSearchAction_ = new QAction( tr("&Add to search"), this ); addToSearchAction_->setStatusTip( tr("Add the selection to the current search") ); connect( addToSearchAction_, SIGNAL( triggered() ), this, SLOT( addToSearch() ) ); popupMenu_ = new QMenu( this ); popupMenu_->addAction( copyAction_ ); popupMenu_->addSeparator(); popupMenu_->addAction( findNextAction_ ); popupMenu_->addAction( findPreviousAction_ ); popupMenu_->addAction( addToSearchAction_ ); } void AbstractLogView::considerMouseHovering( int x_pos, int y_pos ) { int line = convertCoordToLine( y_pos ); if ( ( x_pos < leftMarginPx_ ) && ( line >= 0 ) && ( line < logData->getNbLine() ) ) { // Mouse moved in the margin, send event up // (possibly to highlight the overview) if ( line != lastHoveredLine_ ) { LOG(logDEBUG) << "Mouse moved in margin line: " << line; emit mouseHoveredOverLine( line ); lastHoveredLine_ = line; } } else { if ( lastHoveredLine_ != -1 ) { emit mouseLeftHoveringZone(); lastHoveredLine_ = -1; } } } void AbstractLogView::updateScrollBars() { verticalScrollBar()->setRange( 0, std::max( 0LL, logData->getNbLine() - getNbVisibleLines() + 1 ) ); const int hScrollMaxValue = std::max( 0, logData->getMaxLength() - getNbVisibleCols() + 1 ); horizontalScrollBar()->setRange( 0, hScrollMaxValue ); } void AbstractLogView::drawTextArea( QPaintDevice* paint_device, int32_t delta_y ) { // LOG( logDEBUG ) << "devicePixelRatio: " << viewport()->devicePixelRatio(); // LOG( logDEBUG ) << "viewport size: " << viewport()->size().width(); // LOG( logDEBUG ) << "pixmap size: " << textPixmap.width(); // Repaint the viewport QPainter painter( paint_device ); // LOG( logDEBUG ) << "font: " << viewport()->font().family().toStdString(); // LOG( logDEBUG ) << "font painter: " << painter.font().family().toStdString(); painter.setFont( this->font() ); const int fontHeight = charHeight_; const int fontAscent = painter.fontMetrics().ascent(); const int nbCols = getNbVisibleCols(); const int paintDeviceHeight = paint_device->height() / viewport()->devicePixelRatio(); const int paintDeviceWidth = paint_device->width() / viewport()->devicePixelRatio(); const QPalette& palette = viewport()->palette(); std::shared_ptr filterSet = Persistent( "filterSet" ); QColor foreColor, backColor; static const QBrush normalBulletBrush = QBrush( Qt::white ); static const QBrush matchBulletBrush = QBrush( Qt::red ); static const QBrush markBrush = QBrush( "dodgerblue" ); static const int SEPARATOR_WIDTH = 1; static const qreal BULLET_AREA_WIDTH = 11; static const int CONTENT_MARGIN_WIDTH = 1; static const int LINE_NUMBER_PADDING = 3; // First check the lines to be drawn are within range (might not be the case if // the file has just changed) const int64_t lines_in_file = logData->getNbLine(); if ( firstLine > lines_in_file ) firstLine = lines_in_file ? lines_in_file - 1 : 0; const int64_t nbLines = std::min( static_cast( getNbVisibleLines() ), lines_in_file - firstLine ); const int bottomOfTextPx = nbLines * fontHeight; LOG(logDEBUG) << "drawing lines from " << firstLine << " (" << nbLines << " lines)"; LOG(logDEBUG) << "bottomOfTextPx: " << bottomOfTextPx; LOG(logDEBUG) << "Height: " << paintDeviceHeight; // Lines to write const QStringList lines = logData->getExpandedLines( firstLine, nbLines ); // First draw the bullet left margin painter.setPen(palette.color(QPalette::Text)); painter.fillRect( 0, 0, BULLET_AREA_WIDTH, paintDeviceHeight, Qt::darkGray ); // Column at which the content should start (pixels) qreal contentStartPosX = BULLET_AREA_WIDTH + SEPARATOR_WIDTH; // This is also the bullet zone width, used for marking clicks bulletZoneWidthPx_ = contentStartPosX; // Update the length of line numbers const int nbDigitsInLineNumber = countDigits( maxDisplayLineNumber() ); // Draw the line numbers area int lineNumberAreaStartX = 0; if ( lineNumbersVisible_ ) { int lineNumberWidth = charWidth_ * nbDigitsInLineNumber; int lineNumberAreaWidth = 2 * LINE_NUMBER_PADDING + lineNumberWidth; lineNumberAreaStartX = contentStartPosX; painter.setPen(palette.color(QPalette::Text)); /* Not sure if it looks good... painter.drawLine( contentStartPosX + lineNumberAreaWidth, 0, contentStartPosX + lineNumberAreaWidth, viewport()->height() ); */ painter.fillRect( contentStartPosX - SEPARATOR_WIDTH, 0, lineNumberAreaWidth + SEPARATOR_WIDTH, paintDeviceHeight, Qt::lightGray ); // Update for drawing the actual text contentStartPosX += lineNumberAreaWidth; } else { painter.fillRect( contentStartPosX - SEPARATOR_WIDTH, 0, SEPARATOR_WIDTH + 1, paintDeviceHeight, Qt::lightGray ); // contentStartPosX += SEPARATOR_WIDTH; } painter.drawLine( BULLET_AREA_WIDTH, 0, BULLET_AREA_WIDTH, paintDeviceHeight - 1 ); // This is the total width of the 'margin' (including line number if any) // used for mouse calculation etc... leftMarginPx_ = contentStartPosX + SEPARATOR_WIDTH; // Then draw each line for (int i = 0; i < nbLines; i++) { const LineNumber line_index = i + firstLine; // Position in pixel of the base line of the line to print const int yPos = i * fontHeight; const int xPos = contentStartPosX + CONTENT_MARGIN_WIDTH; // string to print, cut to fit the length and position of the view const QString line = lines[i]; const QString cutLine = line.mid( firstCol, nbCols ); if ( selection_.isLineSelected( line_index ) ) { // Reverse the selected line foreColor = palette.color( QPalette::HighlightedText ); backColor = palette.color( QPalette::Highlight ); painter.setPen(palette.color(QPalette::Text)); } else if ( filterSet->matchLine( logData->getLineString( line_index ), &foreColor, &backColor ) ) { // Apply a filter to the line } else { // Use the default colors foreColor = palette.color( QPalette::Text ); backColor = palette.color( QPalette::Base ); } // Is there something selected in the line? int sel_start, sel_end; bool isSelection = selection_.getPortionForLine( line_index, &sel_start, &sel_end ); // Has the line got elements to be highlighted QList qfMatchList; bool isMatch = quickFindPattern_->matchLine( line, qfMatchList ); if ( isSelection || isMatch ) { // We use the LineDrawer and its chunks because the // line has to be somehow highlighted LineDrawer lineDrawer( backColor ); // First we create a list of chunks with the highlights QList chunkList; int column = 0; // Current column in line space foreach( const QuickFindMatch match, qfMatchList ) { int start = match.startColumn() - firstCol; int end = start + match.length(); // Ignore matches that are *completely* outside view area if ( ( start < 0 && end < 0 ) || start >= nbCols ) continue; if ( start > column ) chunkList << LineChunk( column, start - 1, LineChunk::Normal ); column = qMin( start + match.length() - 1, nbCols ); chunkList << LineChunk( qMax( start, 0 ), column, LineChunk::Highlighted ); column++; } if ( column <= cutLine.length() - 1 ) chunkList << LineChunk( column, cutLine.length() - 1, LineChunk::Normal ); // Then we add the selection if needed QList newChunkList; if ( isSelection ) { sel_start -= firstCol; // coord in line space sel_end -= firstCol; foreach ( const LineChunk chunk, chunkList ) { newChunkList << chunk.select( sel_start, sel_end ); } } else newChunkList = chunkList; foreach ( const LineChunk chunk, newChunkList ) { // Select the colours QColor fore; QColor back; switch ( chunk.type() ) { case LineChunk::Normal: fore = foreColor; back = backColor; break; case LineChunk::Highlighted: fore = QColor( "black" ); back = QColor( "yellow" ); // fore = highlightForeColor; // back = highlightBackColor; break; case LineChunk::Selected: fore = palette.color( QPalette::HighlightedText ), back = palette.color( QPalette::Highlight ); break; } lineDrawer.addChunk ( chunk, fore, back ); } lineDrawer.draw( painter, xPos, yPos, viewport()->width(), cutLine, CONTENT_MARGIN_WIDTH ); } else { // Nothing to be highlighted, we print the whole line! painter.fillRect( xPos - CONTENT_MARGIN_WIDTH, yPos, viewport()->width(), fontHeight, backColor ); // (the rectangle is extended on the left to cover the small // margin, it looks better (LineDrawer does the same) ) painter.setPen( foreColor ); painter.drawText( xPos, yPos + fontAscent, cutLine ); } // Then draw the bullet painter.setPen( palette.color( QPalette::Text ) ); const qreal circleSize = 3; const qreal arrowHeight = 4; const qreal middleXLine = BULLET_AREA_WIDTH / 2; const qreal middleYLine = yPos + (fontHeight / 2); const LineType line_type = lineType( line_index ); if ( line_type == Marked ) { // A pretty arrow if the line is marked const QPointF points[7] = { QPointF(1, middleYLine - 2), QPointF(middleXLine, middleYLine - 2), QPointF(middleXLine, middleYLine - arrowHeight), QPointF(BULLET_AREA_WIDTH - 1, middleYLine), QPointF(middleXLine, middleYLine + arrowHeight), QPointF(middleXLine, middleYLine + 2), QPointF(1, middleYLine + 2 ), }; painter.setBrush( markBrush ); painter.drawPolygon( points, 7 ); } else { // For pretty circles painter.setRenderHint( QPainter::Antialiasing ); if ( lineType( line_index ) == Match ) painter.setBrush( matchBulletBrush ); else painter.setBrush( normalBulletBrush ); painter.drawEllipse( middleXLine - circleSize, middleYLine - circleSize, circleSize * 2, circleSize * 2 ); } // Draw the line number if ( lineNumbersVisible_ ) { static const QString lineNumberFormat( "%1" ); const QString& lineNumberStr = lineNumberFormat.arg( displayLineNumber( line_index ), nbDigitsInLineNumber ); painter.setPen( palette.color( QPalette::Text ) ); painter.drawText( lineNumberAreaStartX + LINE_NUMBER_PADDING, yPos + fontAscent, lineNumberStr ); } } // For each line if ( bottomOfTextPx < paintDeviceHeight ) { // The lines don't cover the whole device painter.fillRect( contentStartPosX, bottomOfTextPx, paintDeviceWidth - contentStartPosX, paintDeviceHeight, palette.color( QPalette::Window ) ); } } // Draw the "pull to follow" bar and return a pixmap. // The width is passed in "logic" pixels. QPixmap AbstractLogView::drawPullToFollowBar( int width, float pixel_ratio ) { static constexpr int barWidth = 40; QPixmap pixmap ( static_cast( width ) * pixel_ratio, barWidth * 6.0 ); pixmap.setDevicePixelRatio( pixel_ratio ); pixmap.fill( this->palette().color( this->backgroundRole() ) ); const int nbBars = width / (barWidth * 2) + 1; QPainter painter( &pixmap ); painter.setPen( QPen( QColor( 0, 0, 0, 0 ) ) ); painter.setBrush( QBrush( QColor( "lightyellow" ) ) ); for ( int i = 0; i < nbBars; ++i ) { QPoint points[4] = { { (i*2+1)*barWidth, 0 }, { 0, (i*2+1)*barWidth }, { 0, (i+1)*2*barWidth }, { (i+1)*2*barWidth, 0 } }; painter.drawConvexPolygon( points, 4 ); } return pixmap; } void AbstractLogView::disableFollow() { emit followModeChanged( false ); followElasticHook_.hook( false ); } namespace { // Convert the length of the pull to follow bar to pixels int mapPullToFollowLength( int length ) { return length / 14; } }; glogg-1.1.4/src/winfilewatcher.h0000644000175000017500000000337513076461374015602 0ustar nickonicko/* * Copyright (C) 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef WINFILEWATCHER_H #define WINFILEWATCHER_H #include "filewatcher.h" #include // #include "inotifywatchtower.h" // An implementation of FileWatcher, as an adapter to INotifyWatchTower. // This is Linux only, and require a recent version of the kernel. // Please note that due to the implementation of the constructor // this class is not thread safe and shall always be used from the main UI thread. class WinFileWatcher : public FileWatcher { Q_OBJECT public: // Create the empty object WinFileWatcher(); // Destroy the object ~WinFileWatcher(); void addFile( const QString& fileName ) override; void removeFile( const QString& fileName ) override; signals: void fileChanged( const QString& ); private: // The following variables are protected by watched_files_mutex_ QString watched_file_name_; // Reference to the (unique) watchtower. // static std::shared_ptr watch_tower_; // std::shared_ptr notification_; }; #endif glogg-1.1.4/src/recentfiles.cpp0000644000175000017500000000562613076461374015426 0ustar nickonicko/* * Copyright (C) 2011 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ // This file implements class RecentFiles #include #include #include "log.h" #include "recentfiles.h" const int RecentFiles::RECENTFILES_VERSION = 1; const int RecentFiles::MAX_NUMBER_OF_FILES = 10; RecentFiles::RecentFiles() : recentFiles_() { } void RecentFiles::addRecent( const QString& text ) { // First prune non existent files QMutableStringListIterator i(recentFiles_); while ( i.hasNext() ) { if ( !QFile::exists(i.next()) ) i.remove(); } // Remove any copy of the about to be added filename recentFiles_.removeAll( text ); // Add at the front recentFiles_.push_front( text ); // Trim the list if it's too long while ( recentFiles_.size() > MAX_NUMBER_OF_FILES ) recentFiles_.pop_back(); } QStringList RecentFiles::recentFiles() const { return recentFiles_; } // // Persistable virtual functions implementation // void RecentFiles::saveToStorage( QSettings& settings ) const { LOG(logDEBUG) << "RecentFiles::saveToStorage"; settings.beginGroup( "RecentFiles" ); settings.setValue( "version", RECENTFILES_VERSION ); settings.beginWriteArray( "filesHistory" ); for (int i = 0; i < recentFiles_.size(); ++i) { settings.setArrayIndex( i ); settings.setValue( "name", recentFiles_.at( i ) ); } settings.endArray(); settings.endGroup(); } void RecentFiles::retrieveFromStorage( QSettings& settings ) { LOG(logDEBUG) << "RecentFiles::retrieveFromStorage"; recentFiles_.clear(); if ( settings.contains( "RecentFiles/version" ) ) { // Unserialise the "new style" stored history settings.beginGroup( "RecentFiles" ); if ( settings.value( "version" ) == RECENTFILES_VERSION ) { int size = settings.beginReadArray( "filesHistory" ); for (int i = 0; i < size; ++i) { settings.setArrayIndex(i); QString search = settings.value( "name" ).toString(); recentFiles_.append( search ); } settings.endArray(); } else { LOG(logERROR) << "Unknown version of FilterSet, ignoring it..."; } settings.endGroup(); } } glogg-1.1.4/src/sessioninfo.h0000644000175000017500000000416213076461374015121 0ustar nickonicko/* * Copyright (C) 2011, 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef SESSIONINFO_H #define SESSIONINFO_H #include #include #include #include #include "persistable.h" // Simple component class containing information related to the session // to be persisted and reloaded upon start class SessionInfo : public Persistable { public: SessionInfo() : openFiles_() { } // Geometry of the main window // (this is an opaque string which is interpreted by the // MainWindow implementation) QByteArray geometry() const { return geometry_; } void setGeometry( const QByteArray& geometry ) { geometry_ = geometry; } struct OpenFile { std::string fileName; uint64_t topLine; // The view context contains parameter specific to the view's // implementation (such as geometry...) std::string viewContext; }; // List of the loaded files std::vector openFiles() const { return openFiles_; } void setOpenFiles( const std::vector& loaded_files ) { openFiles_ = loaded_files; } // Reads/writes the current config in the QSettings object passed virtual void saveToStorage( QSettings& settings ) const; virtual void retrieveFromStorage( QSettings& settings ); private: static const int OPENFILES_VERSION; QByteArray geometry_; QByteArray crawlerState_; std::vector openFiles_; }; #endif glogg-1.1.4/src/selection.h0000644000175000017500000001016413076461374014546 0ustar nickonicko/* * Copyright (C) 2010, 2013 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef SELECTION_H #define SELECTION_H #include #include #include "utils.h" class AbstractLogData; class Portion { public: Portion() { line_ = -1; } Portion( int line, int start_column, int end_column ) { line_ = line; startColumn_ = start_column; endColumn_ = end_column; } int line() const { return line_; } int startColumn() const { return startColumn_; } int endColumn() const { return endColumn_; } bool isValid() const { return ( line_ != -1 ); } private: int line_; int startColumn_; int endColumn_; }; // Represents a selection in an AbstractLogView class Selection { public: // Construct an empty selection Selection(); // Clear the selection void clear() { selectedPartial_.line = -1; selectedLine_ = -1; }; // Select one line void selectLine( int line ) { selectedPartial_.line = -1; selectedRange_.startLine = -1; selectedLine_ = line; }; // Select a portion of line (both start and end included) void selectPortion( int line, int start_column, int end_column ); void selectPortion( Portion selection ) { selectPortion( selection.line(), selection.startColumn(), selection.endColumn() ); } // Select a range of lines (both start and end included) void selectRange( int start_line, int end_line ); // Select a range from the previously selected line or beginning // of range (shift+click behaviour) void selectRangeFromPrevious( int line ); // Crop selection so that in fit in the range ending with the line passed. void crop( int last_line ); // Returns whether the selection is empty bool isEmpty() const { return ( selectedPartial_.line == -1 ) && ( selectedLine_ == -1 ); } // Returns whether the selection is a single line bool isSingleLine() const { return ( selectedLine_ != -1 ); } // Returns whether the selection is a portion of line bool isPortion() const { return ( selectedPartial_.line != -1 ); } // Returns whether a portion is selected or not on the passed line. // If so, returns the portion position. bool getPortionForLine( int line, int* start_column, int* end_column ) const; // Get a list of selected line(s), in order. QList getLines() const; // Returns wether the line passed is selected (entirely). bool isLineSelected( int line ) const; // Returns the line selected or -1 if not a single line selection qint64 selectedLine() const; // Returns the text selected from the passed AbstractLogData QString getSelectedText( const AbstractLogData* logData ) const; // Return the position immediately after the current selection // (used for searches). // This is the next character or the start of the next line. FilePosition getNextPosition() const; // Idem from the position immediately before selection. FilePosition getPreviousPosition() const; private: // Line number currently selected, or -1 if none selected int selectedLine_; struct SelectedPartial { int line; int startColumn; int endColumn; }; struct SelectedRange { // The limits of the range, sorted int startLine; int endLine; // The line selected first, used for shift+click int firstLine; }; struct SelectedPartial selectedPartial_; struct SelectedRange selectedRange_; }; #endif glogg-1.1.4/src/winwatchtowerdriver.cpp0000644000175000017500000003122413076461374017235 0ustar nickonicko/* * Copyright (C) 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include "winwatchtowerdriver.h" #define WIN32_LEAN_AND_MEAN #include #include #include #include "watchtowerlist.h" #include "utils.h" #include "log.h" namespace { std::string shortstringize( const std::wstring& long_string ); std::wstring longstringize( const std::string& short_string ); }; // Utility classes WinNotificationInfoList::WinNotificationInfoList( const char* buffer, size_t buffer_size ) { pointer_ = buffer; next_ = updateCurrentNotification( pointer_ ); } const char* WinNotificationInfoList::updateCurrentNotification( const char* new_position ) { using Action = WinNotificationInfo::Action; static const std::map int_to_action = { { FILE_ACTION_ADDED, Action::ADDED }, { FILE_ACTION_REMOVED, Action::REMOVED }, { FILE_ACTION_MODIFIED, Action::MODIFIED }, { FILE_ACTION_RENAMED_OLD_NAME, Action::RENAMED_OLD_NAME }, { FILE_ACTION_RENAMED_NEW_NAME, Action::RENAMED_NEW_NAME }, }; uint32_t next_offset = *( reinterpret_cast( new_position ) ); uint32_t action = *( reinterpret_cast( new_position ) + 1 ); uint32_t length = *( reinterpret_cast( new_position ) + 2 ); const std::wstring file_name = { reinterpret_cast( new_position + 12 ), length / 2 }; LOG(logDEBUG) << "Next: " << next_offset; LOG(logDEBUG) << "Action: " << action; LOG(logDEBUG) << "Length: " << length; current_notification_ = WinNotificationInfo( int_to_action.at( action ), file_name ); return ( next_offset == 0 ) ? nullptr : new_position + next_offset; } const char* WinNotificationInfoList::advanceToNext() { pointer_ = next_; if ( pointer_ ) next_ = updateCurrentNotification( pointer_ ); return pointer_; } // WinWatchTowerDriver::FileChangeToken void WinWatchTowerDriver::FileChangeToken::readFromFile( const std::string& file_name ) { // On Windows, we open the file and get its last written date/time // That seems to work alright in my tests, but who knows for sure? // On further investigation, in some situations (e.g. putty logging that // has been stop/re-started), the size of the file is being updated but not // the date (confirmed by "Properties" in the file explorer), so we add the // file size to the token. Ha the joy of Windows programming... HANDLE hFile = CreateFile( #ifdef UNICODE longstringize( file_name ).c_str(), #else ( file_name ).c_str(), #endif 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); if ( hFile == (HANDLE)-1 ) { DWORD err = GetLastError(); LOG(logERROR) << "FileChangeToken::readFromFile: failed with " << err; low_date_time_ = 0; high_date_time_ = 0; low_file_size_ = 0; high_file_size_ = 0; return; } else { BY_HANDLE_FILE_INFORMATION file_info; if ( GetFileInformationByHandle( hFile, &file_info ) ) { low_date_time_ = file_info.ftLastWriteTime.dwLowDateTime; high_date_time_ = file_info.ftLastWriteTime.dwHighDateTime; low_file_size_ = file_info.nFileSizeLow; high_file_size_ = file_info.nFileSizeHigh; LOG(logDEBUG) << "FileChangeToken::readFromFile: low_date_time_ " << low_date_time_; LOG(logDEBUG) << "FileChangeToken::readFromFile: high_date_time_ " << high_date_time_; } else { DWORD err = GetLastError(); LOG(logERROR) << "FileChangeToken::readFromFile: failed with " << err; low_date_time_ = 0; high_date_time_ = 0; } } CloseHandle(hFile); } // WinWatchTowerDriver WinWatchTowerDriver::WinWatchTowerDriver() { hCompPort_ = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0x0, 0); } WinWatchTowerDriver::~WinWatchTowerDriver() { } WinWatchTowerDriver::FileId WinWatchTowerDriver::addFile( const std::string& file_name ) { // Nothing for Windows return { }; } WinWatchTowerDriver::SymlinkId WinWatchTowerDriver::addSymlink( const std::string& file_name ) { // Nothing for Windows return { }; } // This implementation is blocking, i.e. it will wait until the file // is effectively loaded in the watchtower thread. WinWatchTowerDriver::DirId WinWatchTowerDriver::addDir( const std::string& file_name ) { DirId dir_id { }; // Add will be done in the watchtower thread { /* std::lock_guard lk( action_mutex_ ); scheduled_action_ = std::make_unique( [this, file_name, &dir_id] { serialisedAddDir( file_name, dir_id ); } ); */ serialisedAddDir( file_name, dir_id ); } // Poke the thread interruptWait(); // Wait for the add task to be completed { /* std::unique_lock lk( action_mutex_ ); action_done_cv_.wait( lk, [this]{ return ( scheduled_action_ == nullptr ); } ); */ } LOG(logDEBUG) << "addDir returned " << dir_id.dir_record_; return dir_id; } void WinWatchTowerDriver::removeFile( const WinWatchTowerDriver::FileId& ) { } void WinWatchTowerDriver::removeSymlink( const SymlinkId& ) { } void WinWatchTowerDriver::removeDir( const DirId& dir_id ) { LOG(logDEBUG) << "Entering driver::removeDir"; if ( dir_id.dir_record_ ) { void* handle = dir_id.dir_record_->handle_; LOG(logDEBUG) << "WinWatchTowerDriver::removeDir handle=" << std::hex << handle; CloseHandle( handle ); } else { /* Happens when an error occured when creating the dir_record_ */ } } // // Private functions // // Add a file (run in the context of the WatchTower thread) void WinWatchTowerDriver::serialisedAddDir( const std::string& dir_name, DirId& dir_id ) { bool inserted = false; auto dir_record = std::make_shared( dir_name ); // The index we will be inserting this record (if success), plus 1 (to avoid // 0 which is used as a magic value) unsigned int index_record = dir_records_.size() + 1; LOG(logDEBUG) << "Adding dir for: " << dir_name; // Open the directory HANDLE hDir = CreateFile( #ifdef UNICODE longstringize( dir_name ).c_str(), #else ( dir_name ).c_str(), #endif FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL ); if ( hDir == INVALID_HANDLE_VALUE ) { LOG(logERROR) << "CreateFile failed for dir " << dir_name; } else { dir_record->handle_ = hDir; //create a IO completion port/or associate this key with //the existing IO completion port hCompPort_ = CreateIoCompletionPort( hDir, hCompPort_, //if m_hCompPort is NULL, hDir is associated with a NEW completion port, //if m_hCompPort is NON-NULL, hDir is associated with the existing completion port that the handle m_hCompPort references // We use the index (plus 1) of the weak_ptr as a key index_record, 0 ); LOG(logDEBUG) << "Weak ptr address stored: " << index_record; memset( &overlapped_, 0, sizeof overlapped_ ); inserted = ReadDirectoryChangesW( hDir, dir_record->buffer_, dir_record->buffer_length_, false, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE, &buffer_length_, // not set when using asynchronous mechanisms... &overlapped_, NULL ); // no completion routine if ( ! inserted ) { LOG(logERROR) << "ReadDirectoryChangesW failed (" << GetLastError() << ")"; CloseHandle( hDir ); } else { dir_id.dir_record_ = dir_record; } } if ( inserted ) { dir_records_.push_back( std::weak_ptr( dir_record ) ); } } std::vector*> WinWatchTowerDriver::waitAndProcessEvents( ObservedFileList* list, std::unique_lock* lock, std::vector*>* /* not needed in WinWatchTowerDriver */, int timeout_ms ) { std::vector*> files_to_notify { }; ULONG_PTR key = 0; DWORD num_bytes = 0; LPOVERLAPPED lpOverlapped = 0; if ( timeout_ms == 0 ) timeout_ms = INFINITE; lock->unlock(); LOG(logDEBUG) << "waitAndProcessEvents now blocking..."; BOOL status = GetQueuedCompletionStatus( hCompPort_, &num_bytes, &key, &lpOverlapped, timeout_ms ); lock->lock(); LOG(logDEBUG) << "Event (" << status << ") key: " << std::hex << key; if ( key ) { // Extract the dir from the completion key auto dir_record_ptr = dir_records_[key - 1]; LOG(logDEBUG) << "use_count = " << dir_record_ptr.use_count(); if ( std::shared_ptr dir_record = dir_record_ptr.lock() ) { LOG(logDEBUG) << "Got event for dir " << dir_record.get(); WinNotificationInfoList notification_info( dir_record->buffer_, dir_record->buffer_length_ ); for ( auto notification : notification_info ) { std::string file_path = dir_record->path_ + shortstringize( notification.fileName() ); LOG(logDEBUG) << "File is " << file_path; auto file = list->searchByName( file_path ); if ( file ) { files_to_notify.push_back( file ); } } // Re-listen for changes status = ReadDirectoryChangesW( dir_record->handle_, dir_record->buffer_, dir_record->buffer_length_, false, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE, &buffer_length_,// not set when using asynchronous mechanisms... &overlapped_, NULL ); // no completion routine } else { LOG(logWARNING) << "Looks like our dir_record disappeared!"; } } else { LOG(logDEBUG) << "Signaled"; } { std::lock_guard lk( action_mutex_ ); if ( scheduled_action_ ) { (*scheduled_action_)(); scheduled_action_ = nullptr; action_done_cv_.notify_all(); } } /* // Just in case someone is waiting for an action to complete std::lock_guard lk( action_mutex_ ); scheduled_action_ = nullptr; action_done_cv_.notify_all(); */ return files_to_notify; } void WinWatchTowerDriver::interruptWait() { LOG(logDEBUG) << "Driver::interruptWait()"; PostQueuedCompletionStatus( hCompPort_, 0, 0, NULL ); } namespace { std::string shortstringize( const std::wstring& long_string ) { std::string short_result {}; for ( wchar_t c : long_string ) { // FIXME: that does not work for non ASCII char!! char short_c = static_cast( c & 0x00FF ); short_result += short_c; } return short_result; } std::wstring longstringize( const std::string& short_string ) { std::wstring long_result {}; for ( char c : short_string ) { wchar_t long_c = static_cast( c ); long_result += long_c; } return long_result; } }; glogg-1.1.4/src/filtersdialog.h0000644000175000017500000000350513076461374015412 0ustar nickonicko/* * Copyright (C) 2009, 2010 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef FILTERSDIALOG_H #define FILTERSDIALOG_H #include #include #include "filterset.h" #include "ui_filtersdialog.h" class FiltersDialog : public QDialog, public Ui::FiltersDialog { Q_OBJECT public: FiltersDialog( QWidget* parent = 0 ); signals: // Is emitted when new settings must be used void optionsChanged(); private slots: void on_addFilterButton_clicked(); void on_removeFilterButton_clicked(); void on_buttonBox_clicked( QAbstractButton* button ); void on_upFilterButton_clicked(); void on_downFilterButton_clicked(); // Update the property (pattern, color...) fields from the // selected Filter. void updatePropertyFields(); // Update the selected Filter from the values in the property fields. void updateFilterProperties(); private: // Temporary filterset modified by the dialog // it is copied from the one in Config() std::shared_ptr filterSet; // Index of the row currently selected or -1 if none. int selectedRow_; void populateColors(); void populateFilterList(); }; #endif glogg-1.1.4/src/quickfind.h0000644000175000017500000001426613076461374014545 0ustar nickonicko/* * Copyright (C) 2010, 2013 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef QUICKFIND_H #define QUICKFIND_H #include #include #include #include "utils.h" #include "qfnotifications.h" #include "selection.h" class QuickFindPattern; class AbstractLogData; class Portion; // Handle "long processing" notifications to the UI. // reset() shall be called at the beginning of the search // and then ping() should be called periodically during the processing. // The notify() signal should be forwarded to the UI. class SearchingNotifier : public QObject { Q_OBJECT public: SearchingNotifier() {}; // Reset internal timers at the beiginning of the processing void reset(); // Shall be called frequently during processing, send the notification // and call the event loop when appropriate. // Pass the current line number and total number of line so that // a progress percentage is calculated and displayed. // (line shall be negative if ging in reverse) inline void ping( qint64 line, qint64 nb_lines ) { if ( startTime_.msecsTo( QTime::currentTime() ) > 1000 ) sendNotification( line, nb_lines ); } signals: // Sent when the UI shall display a message to the user. void notify( const QFNotification& message ); private: void sendNotification( qint64 current_line, qint64 nb_lines ); QTime startTime_; int dotToDisplay_; }; // Represents a search made with Quick Find (without its results) // it keeps a pointer to a set of data and to a QuickFindPattern which // are used for the searches. (the caller retains ownership of both). class QuickFind : public QObject { Q_OBJECT public: // Construct a search QuickFind( const AbstractLogData* const logData, Selection* selection, const QuickFindPattern* const quickFindPattern ); // Set the starting point that will be used by the next search void setSearchStartPoint( QPoint startPoint ); // Used for incremental searches // Return the first occurence of the passed pattern from the starting // point. These searches don't use the QFP and don't change the // starting point. // TODO Update comment qint64 incrementallySearchForward(); qint64 incrementallySearchBackward(); // Stop the currently ongoing incremental search, leave the selection // where it is if a match has been found, restore the old one // if not. Also throw away the start point associated with // the search. void incrementalSearchStop(); // Throw away the current search and restore the initial // position/selection void incrementalSearchAbort(); // Used for 'repeated' (n/N) QF searches using the current direction // Return the line of the first occurence of the QFP and // update the selection. It returns -1 if nothing is found. /* int searchNext(); int searchPrevious(); */ // Idem but ignore the direction and always search in the // specified direction qint64 searchForward(); qint64 searchBackward(); // Make the object forget the 'no more match' flag. void resetLimits(); signals: // Sent when the UI shall display a message to the user. void notify( const QFNotification& message ); // Sent when the UI shall clear the notification. void clearNotification(); private: enum QFDirection { None, Forward, Backward, }; class LastMatchPosition { public: LastMatchPosition() : line_( -1 ), column_( -1 ) {} void set( int line, int column ); void set( const FilePosition& position ); void reset() { line_ = -1; column_ = -1; } // Does the passed position come after the recorded one bool isLater( int line, int column ) const; bool isLater( const FilePosition& position ) const; // Does the passed position come before the recorded one bool isSooner( int line, int column ) const; bool isSooner( const FilePosition& position ) const; private: int line_; int column_; }; class IncrementalSearchStatus { public: /* Constructors */ IncrementalSearchStatus() : ongoing_( None ), position_(), initialSelection_() {} IncrementalSearchStatus( QFDirection direction, const FilePosition& position, const Selection& initial_selection ) : ongoing_( direction ), position_( position ), initialSelection_( initial_selection ) {} bool isOngoing() const { return ( ongoing_ != None ); } QFDirection direction() const { return ongoing_; } FilePosition position() const { return position_; } Selection initialSelection() const { return initialSelection_; } private: QFDirection ongoing_; FilePosition position_; Selection initialSelection_; }; // Pointers to external objects const AbstractLogData* const logData_; Selection* selection_; const QuickFindPattern* const quickFindPattern_; // Owned objects // Position of the last match in the file // (to avoid searching multiple times where there is no result) LastMatchPosition lastMatch_; LastMatchPosition firstMatch_; SearchingNotifier searchingNotifier_; // Incremental search status IncrementalSearchStatus incrementalSearchStatus_; // Private functions qint64 doSearchForward( const FilePosition &start_position ); qint64 doSearchBackward( const FilePosition &start_position ); }; #endif glogg-1.1.4/src/filtersdialog.cpp0000644000175000017500000002316113076461374015745 0ustar nickonicko/* * Copyright (C) 2009, 2010 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include "log.h" #include "configuration.h" #include "persistentinfo.h" #include "filterset.h" #include "filtersdialog.h" static const QString DEFAULT_PATTERN = "New Filter"; static const bool DEFAULT_IGNORE_CASE = false; static const QString DEFAULT_FORE_COLOUR = "black"; static const QString DEFAULT_BACK_COLOUR = "white"; // Construct the box, including a copy of the global FilterSet // to handle ok/cancel/apply FiltersDialog::FiltersDialog( QWidget* parent ) : QDialog( parent ) { setupUi( this ); // Reload the filter list from disk (in case it has been changed // by another glogg instance) and copy it to here. GetPersistentInfo().retrieve( "filterSet" ); filterSet = PersistentCopy( "filterSet" ); populateColors(); populateFilterList(); // Start with all buttons disabled except 'add' removeFilterButton->setEnabled(false); upFilterButton->setEnabled(false); downFilterButton->setEnabled(false); // Default to black on white int index = foreColorBox->findText( DEFAULT_FORE_COLOUR ); foreColorBox->setCurrentIndex( index ); index = backColorBox->findText( DEFAULT_BACK_COLOUR ); backColorBox->setCurrentIndex( index ); // No filter selected by default selectedRow_ = -1; connect( filterListWidget, SIGNAL( itemSelectionChanged() ), this, SLOT( updatePropertyFields() ) ); connect( patternEdit, SIGNAL( textEdited( const QString& ) ), this, SLOT( updateFilterProperties() ) ); connect( ignoreCaseCheckBox, SIGNAL( toggled(bool) ), this, SLOT( updateFilterProperties() ) ); connect( foreColorBox, SIGNAL( activated( int ) ), this, SLOT( updateFilterProperties() ) ); connect( backColorBox, SIGNAL( activated( int ) ), this, SLOT( updateFilterProperties() ) ); if ( !filterSet->filterList.empty() ) { filterListWidget->setCurrentItem( filterListWidget->item( 0 ) ); } } // // Slots // void FiltersDialog::on_addFilterButton_clicked() { LOG(logDEBUG) << "on_addFilterButton_clicked()"; Filter newFilter = Filter( DEFAULT_PATTERN, DEFAULT_IGNORE_CASE, DEFAULT_FORE_COLOUR, DEFAULT_BACK_COLOUR ); filterSet->filterList << newFilter; // Add and select the newly created filter filterListWidget->addItem( DEFAULT_PATTERN ); filterListWidget->setCurrentRow( filterListWidget->count() - 1 ); } void FiltersDialog::on_removeFilterButton_clicked() { int index = filterListWidget->currentRow(); LOG(logDEBUG) << "on_removeFilterButton_clicked() index " << index; if ( index >= 0 ) { filterSet->filterList.removeAt( index ); filterListWidget->setCurrentRow( -1 ); delete filterListWidget->takeItem( index ); // Select the new item at the same index filterListWidget->setCurrentRow( index ); } } void FiltersDialog::on_upFilterButton_clicked() { int index = filterListWidget->currentRow(); LOG(logDEBUG) << "on_upFilterButton_clicked() index " << index; if ( index > 0 ) { filterSet->filterList.move( index, index - 1 ); QListWidgetItem* item = filterListWidget->takeItem( index ); filterListWidget->insertItem( index - 1, item ); filterListWidget->setCurrentRow( index - 1 ); } } void FiltersDialog::on_downFilterButton_clicked() { int index = filterListWidget->currentRow(); LOG(logDEBUG) << "on_downFilterButton_clicked() index " << index; if ( ( index >= 0 ) && ( index < ( filterListWidget->count() - 1 ) ) ) { filterSet->filterList.move( index, index + 1 ); QListWidgetItem* item = filterListWidget->takeItem( index ); filterListWidget->insertItem( index + 1, item ); filterListWidget->setCurrentRow( index + 1 ); } } void FiltersDialog::on_buttonBox_clicked( QAbstractButton* button ) { LOG(logDEBUG) << "on_buttonBox_clicked()"; QDialogButtonBox::ButtonRole role = buttonBox->buttonRole( button ); if ( ( role == QDialogButtonBox::AcceptRole ) || ( role == QDialogButtonBox::ApplyRole ) ) { // Copy the filter set and persist it to disk *( Persistent( "filterSet" ) ) = *filterSet; GetPersistentInfo().save( "filterSet" ); emit optionsChanged(); } if ( role == QDialogButtonBox::AcceptRole ) accept(); else if ( role == QDialogButtonBox::RejectRole ) reject(); } void FiltersDialog::updatePropertyFields() { if ( filterListWidget->selectedItems().count() >= 1 ) selectedRow_ = filterListWidget->row( filterListWidget->selectedItems().at(0) ); else selectedRow_ = -1; LOG(logDEBUG) << "updatePropertyFields(), row = " << selectedRow_; if ( selectedRow_ >= 0 ) { const Filter& currentFilter = filterSet->filterList.at( selectedRow_ ); patternEdit->setText( currentFilter.pattern() ); patternEdit->setEnabled( true ); ignoreCaseCheckBox->setChecked( currentFilter.ignoreCase() ); ignoreCaseCheckBox->setEnabled( true ); int index = foreColorBox->findText( currentFilter.foreColorName() ); if ( index != -1 ) { LOG(logDEBUG) << "fore index = " << index; foreColorBox->setCurrentIndex( index ); foreColorBox->setEnabled( true ); } index = backColorBox->findText( currentFilter.backColorName() ); if ( index != -1 ) { LOG(logDEBUG) << "back index = " << index; backColorBox->setCurrentIndex( index ); backColorBox->setEnabled( true ); } // Enable the buttons if needed removeFilterButton->setEnabled( true ); upFilterButton->setEnabled( ( selectedRow_ > 0 ) ? true : false ); downFilterButton->setEnabled( ( selectedRow_ < ( filterListWidget->count() - 1 ) ) ? true : false ); } else { // Nothing is selected, greys the buttons patternEdit->setEnabled( false ); foreColorBox->setEnabled( false ); backColorBox->setEnabled( false ); ignoreCaseCheckBox->setEnabled( false ); } } void FiltersDialog::updateFilterProperties() { LOG(logDEBUG) << "updateFilterProperties()"; // If a row is selected if ( selectedRow_ >= 0 ) { Filter& currentFilter = filterSet->filterList[selectedRow_]; // Update the internal data currentFilter.setPattern( patternEdit->text() ); currentFilter.setIgnoreCase( ignoreCaseCheckBox->isChecked() ); currentFilter.setForeColor( foreColorBox->currentText() ); currentFilter.setBackColor( backColorBox->currentText() ); // Update the entry in the filterList widget filterListWidget->currentItem()->setText( patternEdit->text() ); filterListWidget->currentItem()->setForeground( QBrush( QColor( currentFilter.foreColorName() ) ) ); filterListWidget->currentItem()->setBackground( QBrush( QColor( currentFilter.backColorName() ) ) ); } } // // Private functions // // Fills the color selection combo boxes void FiltersDialog::populateColors() { const QStringList colorNames = QStringList() // Basic 16 HTML colors (minus greys): << "black" << "white" << "maroon" << "red" << "purple" << "fuchsia" << "green" << "lime" << "olive" << "yellow" << "navy" << "blue" << "teal" << "aqua" // Greys << "gainsboro" << "lightgrey" << "silver" << "darkgrey" << "grey" << "dimgrey" // Reds << "tomato" << "orangered" << "orange" << "crimson" << "darkred" // Greens << "greenyellow" << "lightgreen" << "darkgreen" << "lightseagreen" // Blues << "lightcyan" << "darkturquoise" << "steelblue" << "lightblue" << "royalblue" << "darkblue" << "midnightblue" // Browns << "bisque" << "tan" << "sandybrown" << "chocolate"; for ( QStringList::const_iterator i = colorNames.constBegin(); i != colorNames.constEnd(); ++i ) { QPixmap solidPixmap( 20, 10 ); solidPixmap.fill( QColor( *i ) ); QIcon* solidIcon = new QIcon( solidPixmap ); foreColorBox->addItem( *solidIcon, *i ); backColorBox->addItem( *solidIcon, *i ); } } void FiltersDialog::populateFilterList() { filterListWidget->clear(); foreach ( Filter filter, filterSet->filterList ) { QListWidgetItem* new_item = new QListWidgetItem( filter.pattern() ); // new_item->setFlags( Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled ); new_item->setForeground( QBrush( QColor( filter.foreColorName() ) ) ); new_item->setBackground( QBrush( QColor( filter.backColorName() ) ) ); filterListWidget->addItem( new_item ); } } glogg-1.1.4/src/overview.cpp0000644000175000017500000001042013076461374014755 0ustar nickonicko/* * Copyright (C) 2011, 2012 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ // This file implements the Overview class. // It provides support for drawing the match overview sidebar but // the actual drawing is done in AbstractLogView which uses this class. #include "log.h" #include "data/logfiltereddata.h" #include "overview.h" Overview::Overview() : matchLines_(), markLines_() { logFilteredData_ = NULL; linesInFile_ = 0; topLine_ = 0; nbLines_ = 0; height_ = 0; dirty_ = true; visible_ = false; } Overview::~Overview() { } void Overview::setFilteredData( const LogFilteredData* logFilteredData ) { logFilteredData_ = logFilteredData; } void Overview::updateData( int totalNbLine ) { LOG(logDEBUG) << "OverviewWidget::updateData " << totalNbLine; linesInFile_ = totalNbLine; dirty_ = true; } void Overview::updateView( int height ) { // We don't touch the cache if the height hasn't changed if ( ( height != height_ ) || ( dirty_ == true ) ) { height_ = height; recalculatesLines(); } } const QVector* Overview::getMatchLines() const { return &matchLines_; } const QVector* Overview::getMarkLines() const { return &markLines_; } std::pair Overview::getViewLines() const { int top = 0; int bottom = height_ - 1; if ( linesInFile_ > 0 ) { top = (int)((qint64)topLine_ * height_ / linesInFile_); bottom = (int)((qint64)top + nbLines_ * height_ / linesInFile_); } return std::pair(top, bottom); } int Overview::fileLineFromY( int position ) const { int line = (int)((qint64)position * linesInFile_ / height_); return line; } int Overview::yFromFileLine( int file_line ) const { int position = 0; if ( linesInFile_ > 0 ) position = (int)((qint64)file_line * height_ / linesInFile_); return position; } // Update the internal cache void Overview::recalculatesLines() { LOG(logDEBUG) << "OverviewWidget::recalculatesLines"; if ( logFilteredData_ != NULL ) { matchLines_.clear(); markLines_.clear(); if ( linesInFile_ > 0 ) { for ( int i = 0; i < logFilteredData_->getNbLine(); i++ ) { LogFilteredData::FilteredLineType line_type = logFilteredData_->filteredLineTypeByIndex( i ); int line = (int) logFilteredData_->getMatchingLineNumber( i ); int position = (int)( (qint64)line * height_ / linesInFile_ ); if ( line_type == LogFilteredData::Match ) { if ( ( ! matchLines_.isEmpty() ) && matchLines_.last().position() == position ) { // If the line is already there, we increase its weight matchLines_.last().load(); } else { // If not we just add it matchLines_.append( WeightedLine( position ) ); } } else { if ( ( ! markLines_.isEmpty() ) && markLines_.last().position() == position ) { // If the line is already there, we increase its weight markLines_.last().load(); } else { // If not we just add it markLines_.append( WeightedLine( position ) ); } } } } } else LOG(logDEBUG) << "Overview::recalculatesLines: logFilteredData_ == NULL"; dirty_ = false; } glogg-1.1.4/src/persistentinfo.cpp0000644000175000017500000000630613076461374016173 0ustar nickonicko/* * Copyright (C) 2011, 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ // Implements PersistentInfo, a singleton class which store/retrieve objects // to persistent storage. #include "persistentinfo.h" #include #include #include "log.h" #include "persistable.h" PersistentInfo::PersistentInfo() { settings_ = NULL; initialised_ = false; } PersistentInfo::~PersistentInfo() { if ( initialised_ ) delete settings_; } void PersistentInfo::migrateAndInit() { assert( initialised_ == false ); #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) // On Windows, we use .ini files and import from the registry if no // .ini file is found (glogg <= 0.9 used the registry). // This store the config file in %appdata% settings_ = new QSettings( QSettings::IniFormat, QSettings::UserScope, "glogg", "glogg" ); if ( settings_->childKeys().count() == 0 ) { LOG(logWARNING) << "INI file empty, trying to import from registry"; QSettings registry( "glogg", "glogg" ); foreach ( QString key, registry.allKeys() ) { settings_->setValue( key, registry.value( key ) ); } } #else // We use default Qt storage on proper OSes settings_ = new QSettings( "glogg", "glogg" ); #endif initialised_ = true; } void PersistentInfo::registerPersistable( std::shared_ptr object, const QString& name ) { assert( initialised_ ); objectList_.insert( name, object ); } std::shared_ptr PersistentInfo::getPersistable( const QString& name ) { assert( initialised_ ); std::shared_ptr object = objectList_.value( name, NULL ); return object; } void PersistentInfo::save( const QString& name ) { assert( initialised_ ); if ( objectList_.contains( name ) ) objectList_.value( name )->saveToStorage( *settings_ ); else LOG(logERROR) << "Unregistered persistable " << name.toStdString(); // Sync to ensure it is propagated to other processes settings_->sync(); } void PersistentInfo::retrieve( const QString& name ) { assert( initialised_ ); // Sync to ensure it has been propagated from other processes settings_->sync(); if ( objectList_.contains( name ) ) objectList_.value( name )->retrieveFromStorage( *settings_ ); else LOG(logERROR) << "Unregistered persistable " << name.toStdString(); } // Friend function to construct/get the singleton PersistentInfo& GetPersistentInfo() { static PersistentInfo pInfo; return pInfo; } glogg-1.1.4/src/dbusexternalcom.cpp0000644000175000017500000000702113076461374016311 0ustar nickonicko/* * Copyright (C) 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include "dbusexternalcom.h" #include #include "log.h" static const char* DBUS_SERVICE_NAME = "org.bonnefon.glogg"; DBusExternalCommunicator::DBusExternalCommunicator() { if (!QDBusConnection::sessionBus().isConnected()) { LOG(logERROR) << "Cannot connect to the D-Bus session bus.\n" << "To start it, run:\n" << "\teval `dbus-launch --auto-syntax`\n"; throw CantCreateExternalErr(); } dbus_iface_object_ = std::make_shared(); connect( dbus_iface_object_.get(), SIGNAL( signalLoadFile( const QString& ) ), this, SIGNAL( loadFile( const QString& ) ) ); } // If listening fails (e.g. another glogg is already listening, // the function will fail silently and no listening will be done. void DBusExternalCommunicator::startListening() { if (!QDBusConnection::sessionBus().registerService( DBUS_SERVICE_NAME )) { LOG(logERROR) << qPrintable(QDBusConnection::sessionBus().lastError().message()); } if ( !QDBusConnection::sessionBus().registerObject( "/", dbus_iface_object_.get(), QDBusConnection::ExportAllContents ) ) { LOG(logERROR) << qPrintable(QDBusConnection::sessionBus().lastError().message()); } } ExternalInstance* DBusExternalCommunicator::otherInstance() const { try { return static_cast( new DBusExternalInstance() ); } catch ( CantCreateExternalErr ) { LOG(logINFO) << "Cannot find external D-Bus correspondant, we are the only glogg out there."; return nullptr; } } qint32 DBusExternalCommunicator::version() const { return 3; } qint32 DBusInterfaceExternalCommunicator::version() const { return 0x010000; } void DBusInterfaceExternalCommunicator::loadFile( const QString& file_name ) { LOG(logDEBUG) << "DBusInterfaceExternalCommunicator::loadFile()"; emit signalLoadFile( file_name ); } DBusExternalInstance::DBusExternalInstance() { dbusInterface_ = std::make_shared( DBUS_SERVICE_NAME, "/", "", QDBusConnection::sessionBus() ); if ( ! dbusInterface_->isValid() ) { throw CantCreateExternalErr(); } } void DBusExternalInstance::loadFile( const QString& file_name ) const { QDBusReply reply = dbusInterface_->call( "loadFile", file_name ); if ( ! reply.isValid() ) { LOG( logWARNING ) << "Invalid reply from D-Bus call: " << qPrintable( reply.error().message() ); } } uint32_t DBusExternalInstance::getVersion() const { QDBusReply reply = dbusInterface_->call( "version" ); if ( ! reply.isValid() ) { LOG( logWARNING ) << "Invalid reply from D-Bus call: " << qPrintable( reply.error().message() ); return 0; } return (uint32_t) reply.value(); } glogg-1.1.4/src/mainwindow.h0000644000175000017500000001541713076461374014743 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2011, 2013, 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef MAINWINDOW_H #define MAINWINDOW_H #include #include #include "session.h" #include "crawlerwidget.h" #include "infoline.h" #include "signalmux.h" #include "tabbedcrawlerwidget.h" #include "quickfindwidget.h" #include "quickfindmux.h" #ifdef GLOGG_SUPPORTS_VERSION_CHECKING #include "versionchecker.h" #endif class QAction; class QActionGroup; class Session; class RecentFiles; class MenuActionToolTipBehavior; class ExternalCommunicator; // Main window of the application, creates menus, toolbar and // the CrawlerWidget class MainWindow : public QMainWindow { Q_OBJECT public: // Constructor // The ownership of the session is transferred to us MainWindow( std::unique_ptr session, std::shared_ptr external_communicator ); // Re-install the geometry stored in config file // (should be done before 'Widget::show()') void reloadGeometry(); // Re-load the files from the previous session void reloadSession(); // Loads the initial file (parameter passed or from config file) void loadInitialFile( QString fileName ); // Starts the lower priority activities the MW controls such as // version checking etc... void startBackgroundTasks(); protected: void closeEvent( QCloseEvent* event ); // Drag and drop support void dragEnterEvent( QDragEnterEvent* event ); void dropEvent( QDropEvent* event ); void keyPressEvent( QKeyEvent* keyEvent ); private slots: void open(); void openRecentFile(); void closeTab(); void closeAll(); void selectAll(); void copy(); void find(); void filters(); void options(); void about(); void aboutQt(); void encodingChanged( QAction* action ); // Change the view settings void toggleOverviewVisibility( bool isVisible ); void toggleMainLineNumbersVisibility( bool isVisible ); void toggleFilteredLineNumbersVisibility( bool isVisible ); // Change the follow mode checkbox and send the followSet signal down void changeFollowMode( bool follow ); // Update the line number displayed in the status bar. // Must be passed as the internal (starts at 0) line number. void lineNumberHandler( int line ); // Instructs the widget to update the loading progress gauge void updateLoadingProgress( int progress ); // Instructs the widget to display the 'normal' status bar, // without the progress gauge and with file info // or an error recovery when loading is finished void handleLoadingFinished( LoadingStatus status ); // Save the new state as default setting when a crawler // is changing their view options. void handleSearchRefreshChanged( int state ); void handleIgnoreCaseChanged( int state ); // Close the tab with the passed index void closeTab( int index ); // Setup the tab with current index for view void currentTabChanged( int index ); // Instructs the widget to change the pattern in the QuickFind widget // and confirm it. void changeQFPattern( const QString& newPattern ); // Load a file in a new tab (non-interactive) // (for use from e.g. IPC) void loadFileNonInteractive( const QString& file_name ); // Notify the user a new version is available void newVersionNotification( const QString& new_version ); signals: // Is emitted when new settings must be used void optionsChanged(); // Is emitted when the 'follow' option is enabled/disabled void followSet( bool checked ); // Is emitted before the QuickFind box is activated, // to allow crawlers to get search in the right view. void enteringQuickFind(); // Emitted when the quickfind bar is closed. void exitingQuickFind(); private: void createActions(); void createMenus(); void createContextMenu(); void createToolBars(); void createStatusBar(); void createRecentFileToolTipTimer(); void readSettings(); void writeSettings(); bool loadFile( const QString& fileName ); void updateTitleBar( const QString& file_name ); void updateRecentFileActions(); QString strippedName( const QString& fullFileName ) const; CrawlerWidget* currentCrawlerWidget() const; void displayQuickFindBar( QuickFindMux::QFDirection direction ); void updateMenuBarFromDocument( const CrawlerWidget* crawler ); void updateInfoLine(); std::unique_ptr session_; std::shared_ptr externalCommunicator_; std::shared_ptr recentFiles_; QString loadingFileName; // Encoding struct EncodingList { const char* name; }; static const EncodingList encoding_list[]; enum { MaxRecentFiles = 5 }; QAction *recentFileActions[MaxRecentFiles]; MenuActionToolTipBehavior *recentFileActionBehaviors[MaxRecentFiles]; QAction *separatorAction; QMenu *fileMenu; QMenu *editMenu; QMenu *viewMenu; QMenu *toolsMenu; QMenu *encodingMenu; QMenu *helpMenu; InfoLine *infoLine; QLabel* lineNbField; QToolBar *toolBar; QAction *openAction; QAction *closeAction; QAction *closeAllAction; QAction *exitAction; QAction *copyAction; QAction *selectAllAction; QAction *findAction; QAction *overviewVisibleAction; QAction *lineNumbersVisibleInMainAction; QAction *lineNumbersVisibleInFilteredAction; QAction *followAction; QAction *reloadAction; QAction *stopAction; QAction *filtersAction; QAction *optionsAction; QAction *aboutAction; QAction *aboutQtAction; QActionGroup *encodingGroup; QAction *encodingAction[CrawlerWidget::ENCODING_MAX]; QIcon mainIcon_; // Multiplex signals to any of the CrawlerWidgets SignalMux signalMux_; // QuickFind widget QuickFindWidget quickFindWidget_; // Multiplex signals to/from the QuickFindWidget QuickFindMux quickFindMux_; // The main widget TabbedCrawlerWidget mainTabWidget_; // Version checker #ifdef GLOGG_SUPPORTS_VERSION_CHECKING VersionChecker versionChecker_; #endif }; #endif glogg-1.1.4/src/socketexternalcom.cpp0000644000175000017500000000653513076461374016655 0ustar nickonicko#include "socketexternalcom.h" #include #include #include #ifdef Q_OS_UNIX #include #include #endif #include "log.h" static const char* GLOG_SERVICE_NAME = "org.bonnefon.glogg"; #ifdef Q_OS_UNIX QSharedMemory* g_staticSharedMemory = nullptr; void terminate(int signum) { if (g_staticSharedMemory) { delete g_staticSharedMemory; } ::exit(128 + signum); } void setCrashHandler(QSharedMemory* memory) { g_staticSharedMemory = memory; // Handle any further termination signals to ensure the // QSharedMemory block is deleted even if the process crashes signal(SIGSEGV, terminate); signal(SIGABRT, terminate); signal(SIGFPE, terminate); signal(SIGILL, terminate); signal(SIGINT, terminate); signal(SIGTERM, terminate); } #endif SocketExternalInstance::SocketExternalInstance() : ExternalInstance(), memory_(new QSharedMemory(GLOG_SERVICE_NAME) ) { if ( !memory_->attach( QSharedMemory::ReadOnly ) ) { LOG( logERROR ) << "attach failed!"; throw CantCreateExternalErr(); } #ifdef Q_OS_UNIX // Handle any further termination signals to ensure the // QSharedMemory block is deleted even if the process crashes setCrashHandler(memory_); #endif } void SocketExternalInstance::loadFile(const QString &file_name) const { QLocalSocket socket; socket.connectToServer(GLOG_SERVICE_NAME); if (!socket.waitForConnected(1000)) { LOG( logERROR ) << "Failed to connect to socket"; return; } socket.write(file_name.toUtf8()); if (!socket.waitForBytesWritten(1000)) { LOG( logERROR ) << "Failed to send filename"; } socket.close(); } uint32_t SocketExternalInstance::getVersion() const { return *reinterpret_cast(memory_->data()); } SocketExternalCommunicator::SocketExternalCommunicator() : ExternalCommunicator() , memory_(new QSharedMemory(GLOG_SERVICE_NAME)) , server_(new QLocalServer) {} SocketExternalCommunicator::~SocketExternalCommunicator() { delete memory_; server_->close(); delete server_; } void SocketExternalCommunicator::startListening() { if ( memory_->create(sizeof(qint32))) { #ifdef Q_OS_UNIX // Handle any further termination signals to ensure the // QSharedMemory block is deleted even if the process crashes setCrashHandler(memory_); #endif *reinterpret_cast(memory_->data()) = version(); QLocalServer::removeServer(GLOG_SERVICE_NAME); connect(server_, SIGNAL(newConnection()), SLOT(onConnection())); server_->listen(GLOG_SERVICE_NAME); } } qint32 SocketExternalCommunicator::version() const { return 3; } ExternalInstance* SocketExternalCommunicator::otherInstance() const { try { return static_cast( new SocketExternalInstance() ); } catch ( CantCreateExternalErr ) { LOG(logINFO) << "Cannot find external correspondant, we are the only glogg out there."; return nullptr; } } void SocketExternalCommunicator::onConnection() { QLocalSocket *socket = server_->nextPendingConnection(); QByteArray data; while(socket->waitForReadyRead(100)) { data.append(socket->readAll()); } socket->close(); emit loadFile(QString::fromUtf8(data)); } glogg-1.1.4/src/sessioninfo.cpp0000644000175000017500000000542413076461374015456 0ustar nickonicko/* * Copyright (C) 2011, 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include "sessioninfo.h" #include #include "log.h" const int SessionInfo::OPENFILES_VERSION = 1; void SessionInfo::retrieveFromStorage( QSettings& settings ) { LOG(logDEBUG) << "SessionInfo::retrieveFromStorage"; geometry_ = settings.value("geometry").toByteArray(); if ( settings.contains( "OpenFiles/version" ) ) { openFiles_.clear(); // Unserialise the "new style" stored history settings.beginGroup( "OpenFiles" ); if ( settings.value( "version" ) == OPENFILES_VERSION ) { int size = settings.beginReadArray( "openFiles" ); LOG(logDEBUG) << "SessionInfo: " << size << " files."; for (int i = 0; i < size; ++i) { settings.setArrayIndex(i); std::string file_name = settings.value( "fileName" ).toString().toStdString(); uint64_t top_line = settings.value( "topLine" ).toInt(); std::string view_context = settings.value( "viewContext" ).toString().toStdString(); openFiles_.push_back( { file_name, top_line, view_context } ); } settings.endArray(); } else { LOG(logERROR) << "Unknown version of OpenFiles, ignoring it..."; } settings.endGroup(); } } void SessionInfo::saveToStorage( QSettings& settings ) const { LOG(logDEBUG) << "SessionInfo::saveToStorage"; settings.setValue( "geometry", geometry_ ); settings.beginGroup( "OpenFiles" ); settings.setValue( "version", OPENFILES_VERSION ); settings.beginWriteArray( "openFiles" ); for ( unsigned i = 0; i < openFiles_.size(); ++i ) { settings.setArrayIndex( i ); const OpenFile* open_file = &(openFiles_.at( i )); settings.setValue( "fileName", QString( open_file->fileName.c_str() ) ); settings.setValue( "topLine", qint64( open_file->topLine ) ); settings.setValue( "viewContext", QString( open_file->viewContext.c_str() ) ); } settings.endArray(); settings.endGroup(); } glogg-1.1.4/src/configuration.h0000644000175000017500000000754113076461374015435 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2011, 2013, 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef CONFIGURATION_H #define CONFIGURATION_H #include #include #include "persistable.h" // Type of regexp to use for searches enum SearchRegexpType { ExtendedRegexp, Wildcard, FixedString, }; // Configuration class containing everything in the "Settings" dialog class Configuration : public Persistable { public: Configuration(); // Accesses the main font used for display QFont mainFont() const; void setMainFont( QFont newFont ); // Accesses the regexp types SearchRegexpType mainRegexpType() const { return mainRegexpType_; } SearchRegexpType quickfindRegexpType() const { return quickfindRegexpType_; } bool isQuickfindIncremental() const { return quickfindIncremental_; } void setMainRegexpType( SearchRegexpType type ) { mainRegexpType_ = type; } void setQuickfindRegexpType( SearchRegexpType type ) { quickfindRegexpType_ = type; } void setQuickfindIncremental( bool is_incremental ) { quickfindIncremental_ = is_incremental; } // "Advanced" settings bool pollingEnabled() const { return pollingEnabled_; } void setPollingEnabled( bool enabled ) { pollingEnabled_ = enabled; } uint32_t pollIntervalMs() const { return pollIntervalMs_; } void setPollIntervalMs( uint32_t interval ) { pollIntervalMs_ = interval; } bool loadLastSession() const { return loadLastSession_; } void setLoadLastSession( bool enabled ) { loadLastSession_ = enabled; } // View settings bool isOverviewVisible() const { return overviewVisible_; } void setOverviewVisible( bool isVisible ) { overviewVisible_ = isVisible; } bool mainLineNumbersVisible() const { return lineNumbersVisibleInMain_; } bool filteredLineNumbersVisible() const { return lineNumbersVisibleInFiltered_; } void setMainLineNumbersVisible( bool lineNumbersVisible ) { lineNumbersVisibleInMain_ = lineNumbersVisible; } void setFilteredLineNumbersVisible( bool lineNumbersVisible ) { lineNumbersVisibleInFiltered_ = lineNumbersVisible; } // Default settings for new views bool isSearchAutoRefreshDefault() const { return searchAutoRefresh_; } void setSearchAutoRefreshDefault( bool auto_refresh ) { searchAutoRefresh_ = auto_refresh; } bool isSearchIgnoreCaseDefault() const { return searchIgnoreCase_; } void setSearchIgnoreCaseDefault( bool ignore_case ) { searchIgnoreCase_ = ignore_case; } // Reads/writes the current config in the QSettings object passed virtual void saveToStorage( QSettings& settings ) const; virtual void retrieveFromStorage( QSettings& settings ); private: // Configuration settings QFont mainFont_; SearchRegexpType mainRegexpType_; SearchRegexpType quickfindRegexpType_; bool quickfindIncremental_; bool pollingEnabled_; uint32_t pollIntervalMs_; bool loadLastSession_; // View settings bool overviewVisible_; bool lineNumbersVisibleInMain_; bool lineNumbersVisibleInFiltered_; // Default settings for new views bool searchAutoRefresh_; bool searchIgnoreCase_; }; #endif glogg-1.1.4/src/watchtowerlist.h0000644000175000017500000003134013076461374015643 0ustar nickonicko#ifndef WATCHTOWERLIST_H #define WATCHTOWERLIST_H // Utility classes for the WatchTower implementations #include #include #include #include #include #include #include #include #include "log.h" // Utility classes struct ProtocolInfo { // Win32 notification variables static const int READ_DIR_CHANGE_BUFFER_SIZE = 4096; void* handle_; static const unsigned long buffer_length_ = READ_DIR_CHANGE_BUFFER_SIZE; char buffer_[buffer_length_]; }; // List of files and observers template struct ObservedDir { ObservedDir( const std::string this_path ) : path { this_path } {} // Returns the address of the protocol specific informations ProtocolInfo* protocolInfo() { return &protocol_info_; } std::string path; typename Driver::DirId dir_id_; // Contains data specific to the protocol (inotify/Win32...) ProtocolInfo protocol_info_; }; template struct ObservedFile { ObservedFile( const std::string& file_name, std::shared_ptr callback, typename Driver::FileId file_id, typename Driver::SymlinkId symlink_id ) : file_name_( file_name ) { addCallback( callback ); file_id_ = file_id; symlink_id_ = symlink_id; dir_ = nullptr; markAsChanged(); } void addCallback( std::shared_ptr callback ) { callbacks.push_back( callback ); } // Records the file has changed void markAsChanged() { change_token_.readFromFile( file_name_ ); last_check_time_ = std::chrono::steady_clock::now(); } // Returns whether a file has changed // (for polling) bool hasChanged() { typename Driver::FileChangeToken new_token( file_name_ ); last_check_time_ = std::chrono::steady_clock::now(); return change_token_ != new_token; } std::chrono::steady_clock::time_point timeForLastCheck() { return last_check_time_; } std::string file_name_; // List of callbacks for this file std::vector> callbacks; // watch descriptor for the file itself typename Driver::FileId file_id_; // watch descriptor for the symlink (if file is a symlink) typename Driver::SymlinkId symlink_id_; // link to the dir containing the file std::shared_ptr> dir_; // token to identify modification // (the token change when the file is modified, this is used when polling) typename Driver::FileChangeToken change_token_; // Last time a check has been done std::chrono::steady_clock::time_point last_check_time_ = std::chrono::steady_clock::time_point::min(); }; // A list of the observed files and directories // This class is not thread safe template class ObservedFileList { public: ObservedFileList() : heartBeat_ { std::shared_ptr((void*) 0xDEADC0DE, [] (void*) {}) } { } ~ObservedFileList() = default; // The functions return a pointer to the existing file (if exists) // but keep ownership of the object. ObservedFile* searchByName( const std::string& file_name ); ObservedFile* searchByFileOrSymlinkWd( typename Driver::FileId file_id, typename Driver::SymlinkId symlink_id ); ObservedFile* searchByDirWdAndName( typename Driver::DirId id, const char* name ); // Add a new file, the list returns a pointer to the added file, // but has ownership of the file. ObservedFile* addNewObservedFile( ObservedFile new_observed ); // Remove a callback, remove and returns the file object if // it was the last callback on this object, nullptr if not. // The caller has ownership of the object. std::shared_ptr> removeCallback( std::shared_ptr callback ); // Return the watched directory if it is watched, or nullptr std::shared_ptr> watchedDirectory( const std::string& dir_name ); // Create a new watched directory for dir_name, the client is passed // shared ownership and have to keep the shared_ptr (the list only // maintain a weak link). // The remove notification is called just before the reference to // the directory is destroyed. std::shared_ptr> addWatchedDirectory( const std::string& dir_name, std::function* )> remove_notification ); // Similar to previous functions but extract the name of the // directory from the file name. std::shared_ptr> watchedDirectoryForFile( const std::string& file_name ); std::shared_ptr> addWatchedDirectoryForFile( const std::string& file_name, std::function* )> remove_notification ); // Removal of directories is done when there is no shared reference // left (RAII) // Number of watched directories (for tests) unsigned int numberWatchedDirectories() const; // Iterator template class iterator : std::iterator> { public: iterator( Container* list, const typename Container::iterator& iter ) { list_ = list; pos_ = iter; } iterator operator++() { ++pos_; return *this; } bool operator==( const iterator& other ) { return ( pos_ == other.pos_ ); } bool operator!=( const iterator& other ) { return ! operator==( other ); } typename Container::iterator operator*() { return pos_; } private: Container* list_; typename Container::iterator pos_; }; iterator>> begin() { return iterator>>( &observed_files_, observed_files_.begin() ); } iterator>> end() { return iterator>>( &observed_files_, observed_files_.end() ); } private: // List of observed files std::list> observed_files_; // List of observed dirs, key-ed by name std::map>> observed_dirs_; // Map the inotify file (including symlinks) wds to the observed file std::map*> by_file_wd_; // Map the inotify directory wds to the observed files std::map*> by_dir_wd_; // Heartbeat std::shared_ptr heartBeat_; // Clean all reference to any expired directory void cleanRefsToExpiredDirs(); }; namespace { std::string directory_path( const std::string& path ); }; // ObservedFileList class template ObservedFile* ObservedFileList::searchByName( const std::string& file_name ) { // Look for an existing observer on this file auto existing_observer = observed_files_.begin(); for ( ; existing_observer != observed_files_.end(); ++existing_observer ) { if ( existing_observer->file_name_ == file_name ) { LOG(logDEBUG) << "Found " << file_name; break; } } if ( existing_observer != observed_files_.end() ) return &( *existing_observer ); else return nullptr; } template ObservedFile* ObservedFileList::searchByFileOrSymlinkWd( typename Driver::FileId file_id, typename Driver::SymlinkId symlink_id ) { auto result = find_if( observed_files_.begin(), observed_files_.end(), [file_id, symlink_id] (ObservedFile file) -> bool { return ( file_id == file.file_id_ ) || ( symlink_id == file.symlink_id_ ); } ); if ( result != observed_files_.end() ) return &( *result ); else return nullptr; } template ObservedFile* ObservedFileList::searchByDirWdAndName( typename Driver::DirId id, const char* name ) { auto dir = find_if( observed_dirs_.begin(), observed_dirs_.end(), [id] (std::pair>> d) -> bool { if ( auto dir = d.second.lock() ) { return ( id == dir->dir_id_ ); } else { return false; } } ); if ( dir != observed_dirs_.end() ) { std::string path = dir->first + "/" + name; // LOG(logDEBUG) << "Testing path: " << path; // Looking for the path in the files we are watching return searchByName( path ); } else { return nullptr; } } template ObservedFile* ObservedFileList::addNewObservedFile( ObservedFile new_observed ) { auto new_file = observed_files_.insert( std::begin( observed_files_ ), new_observed ); return &( *new_file ); } template std::shared_ptr> ObservedFileList::removeCallback( std::shared_ptr callback ) { std::shared_ptr> returned_file = nullptr; for ( auto observer = std::begin( observed_files_ ); observer != std::end( observed_files_ ); ) { std::vector>& callbacks = observer->callbacks; callbacks.erase( std::remove( std::begin( callbacks ), std::end( callbacks ), callback ), std::end( callbacks ) ); /* See if all notifications have been deleted for this file */ if ( callbacks.empty() ) { LOG(logDEBUG) << "Empty notification list for " << observer->file_name_ << ", removing the watched file"; returned_file = std::make_shared>( *observer ); observer = observed_files_.erase( observer ); } else { ++observer; } } return returned_file; } template std::shared_ptr> ObservedFileList::watchedDirectory( const std::string& dir_name ) { std::shared_ptr> dir = nullptr; if ( observed_dirs_.find( dir_name ) != std::end( observed_dirs_ ) ) dir = observed_dirs_[ dir_name ].lock(); return dir; } template std::shared_ptr> ObservedFileList::addWatchedDirectory( const std::string& dir_name, std::function* )> remove_notification ) { std::weak_ptr weakHeartBeat(heartBeat_); std::shared_ptr> dir = { new ObservedDir( dir_name ), [this, remove_notification, weakHeartBeat] (ObservedDir* d) { if ( auto heart_beat = weakHeartBeat.lock() ) { remove_notification( d ); this->cleanRefsToExpiredDirs(); } delete d; } }; observed_dirs_[ dir_name ] = std::weak_ptr>( dir ); return dir; } template std::shared_ptr> ObservedFileList::watchedDirectoryForFile( const std::string& file_name ) { return watchedDirectory( directory_path( file_name ) ); } template std::shared_ptr> ObservedFileList::addWatchedDirectoryForFile( const std::string& file_name, std::function* )> remove_notification ) { return addWatchedDirectory( directory_path( file_name ), remove_notification ); } template unsigned int ObservedFileList::numberWatchedDirectories() const { return observed_dirs_.size(); } // Private functions template void ObservedFileList::cleanRefsToExpiredDirs() { for ( auto it = std::begin( observed_dirs_ ); it != std::end( observed_dirs_ ); ) { if ( it->second.expired() ) { it = observed_dirs_.erase( it ); } else { ++it; } } } namespace { std::string directory_path( const std::string& path ) { size_t slash_pos = path.rfind( '/' ); #ifdef _WIN32 if ( slash_pos == std::string::npos ) { slash_pos = path.rfind( '\\' ); } // We need to include the final slash on Windows ++slash_pos; #endif return std::string( path, 0, slash_pos ); } }; #endif glogg-1.1.4/src/utils.h0000644000175000017500000000724713076461374013731 0ustar nickonicko/* * Copyright (C) 2011, 2013 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef UTILS_H #define UTILS_H #include #include "config.h" // Line number are unsigned 32 bits for now. typedef uint32_t LineNumber; // Use a bisection method to find the given line number // in a sorted list. // The T type must be a container containing elements that // implement the lineNumber() member. // Returns true if the lineNumber is found, false if not // foundIndex is the index of the found number or the index // of the closest greater element. template bool lookupLineNumber( const T& list, qint64 lineNumber, int* foundIndex ) { int minIndex = 0; int maxIndex = list.size() - 1; // If the list is not empty if ( maxIndex - minIndex >= 0 ) { // First we test the ends if ( list[minIndex].lineNumber() == lineNumber ) { *foundIndex = minIndex; return true; } else if ( list[maxIndex].lineNumber() == lineNumber ) { *foundIndex = maxIndex; return true; } // Then we test the rest while ( (maxIndex - minIndex) > 1 ) { const int tryIndex = (minIndex + maxIndex) / 2; const qint64 currentMatchingNumber = list[tryIndex].lineNumber(); if ( currentMatchingNumber > lineNumber ) maxIndex = tryIndex; else if ( currentMatchingNumber < lineNumber ) minIndex = tryIndex; else if ( currentMatchingNumber == lineNumber ) { *foundIndex = tryIndex; return true; } } // If we haven't found anything... // ... end of the list or before the next if ( lineNumber > list[maxIndex].lineNumber() ) *foundIndex = maxIndex + 1; else if ( lineNumber > list[minIndex].lineNumber() ) *foundIndex = minIndex + 1; else *foundIndex = minIndex; } else { *foundIndex = 0; } return false; } // Represents a position in a file (line, column) class FilePosition { public: FilePosition() { line_ = -1; column_ = -1; } FilePosition( qint64 line, int column ) { line_ = line; column_ = column; } qint64 line() const { return line_; } int column() const { return column_; } private: qint64 line_; int column_; }; #ifndef HAVE_MAKE_UNIQUE #include namespace std { template std::unique_ptr make_unique(Args&&... args) { return std::unique_ptr(new T(std::forward(args)...)); } } #endif #ifndef HAVE_OVERRIDE #define override #endif #ifndef HAVE_HTONS inline uint16_t glogg_htons( uint16_t hostshort ) { static const uint16_t test_value = 0xABCD; if ( *( reinterpret_cast( &test_value ) ) == 0xCD ) { uint16_t result = ( hostshort & 0x00FF ) << 8; result |= ( hostshort & 0xFF00 ) >> 8; return result; } else { return hostshort; } } #endif #endif glogg-1.1.4/src/inotifywatchtowerdriver.cpp0000644000175000017500000001474313076461374020130 0ustar nickonicko/* * Copyright (C) 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include "inotifywatchtowerdriver.h" #include #include #include #include #include "log.h" #include "watchtowerlist.h" INotifyWatchTowerDriver::INotifyWatchTowerDriver() : inotify_fd_( inotify_init() ) { int pipefd[2]; pipe2( pipefd, O_NONBLOCK ); breaking_pipe_read_fd_ = pipefd[0]; breaking_pipe_write_fd_ = pipefd[1]; } INotifyWatchTowerDriver::~INotifyWatchTowerDriver() { close( breaking_pipe_read_fd_ ); close( breaking_pipe_write_fd_ ); } INotifyWatchTowerDriver::FileId INotifyWatchTowerDriver::addFile( const std::string& file_name ) { // Add a watch for the inode int wd = inotify_add_watch( inotify_fd_, file_name.c_str(), IN_DELETE_SELF | IN_MODIFY | IN_MOVE_SELF ); LOG(logDEBUG) << "INotifyWatchTower::addFile new inotify wd " << wd; return { wd }; } INotifyWatchTowerDriver::SymlinkId INotifyWatchTowerDriver::addSymlink( const std::string& file_name ) { int symlink_wd = inotify_add_watch( inotify_fd_, file_name.c_str(), IN_DONT_FOLLOW | IN_DELETE_SELF | IN_MODIFY | IN_MOVE_SELF ); LOG(logDEBUG) << "INotifyWatchTower::addFile new inotify symlink_wd " << symlink_wd; // (not sure a symlink can be modified but you never know) return { symlink_wd }; } INotifyWatchTowerDriver::DirId INotifyWatchTowerDriver::addDir( const std::string& file_name ) { int dir_wd = inotify_add_watch( inotify_fd_, file_name.c_str(), IN_CREATE | IN_MOVE | IN_ONLYDIR ); LOG(logDEBUG) << "INotifyWatchTower::addFile dir " << file_name << " watched wd " << dir_wd; return { dir_wd }; } void INotifyWatchTowerDriver::removeFile( const INotifyWatchTowerDriver::FileId& file_id ) { /* LOG(logDEBUG) << "INotifyWatchTower::removeNotification removing inotify wd " << file->file_wd_ << " symlink_wd " << file->symlink_wd_; */ if ( file_id.wd_ >= 0 ) inotify_rm_watch( inotify_fd_, file_id.wd_ ); } void INotifyWatchTowerDriver::removeSymlink( const SymlinkId& symlink_id ) { if ( symlink_id.wd_ >= 0 ) inotify_rm_watch( inotify_fd_, symlink_id.wd_ ); } void INotifyWatchTowerDriver::removeDir( const DirId& dir_id ) { LOG(logDEBUG) << "INotifyWatchTower::removeDir removing inotify wd " << dir_id.wd_; if ( dir_id.wd_ >= 0 ) inotify_rm_watch( inotify_fd_, dir_id.wd_ ); } static constexpr size_t INOTIFY_BUFFER_SIZE = 4096; std::vector INotifyWatchTowerDriver::waitAndProcessEvents( INotifyObservedFileList* list, std::unique_lock* list_lock, std::vector* files_needing_readding, int timeout_ms ) { std::vector files_to_notify; struct pollfd fds[2]; fds[0].fd = inotify_fd_; fds[0].events = POLLIN; fds[0].revents = 0; fds[1].fd = breaking_pipe_read_fd_; fds[1].events = POLLIN; fds[1].revents = 0; list_lock->unlock(); int poll_ret = poll( fds, 2, timeout_ms ? timeout_ms : -1 ); list_lock->lock(); if ( poll_ret > 0 ) { if ( fds[0].revents & POLLIN ) { LOG(logDEBUG) << "Pollin for inotify"; char buffer[ INOTIFY_BUFFER_SIZE ] __attribute__ ((aligned(__alignof__(struct inotify_event)))); ssize_t nb = read( inotify_fd_, buffer, sizeof( buffer ) ); if ( nb > 0 ) { ssize_t offset = 0; while ( offset < nb ) { const inotify_event* event = reinterpret_cast( buffer + offset ); offset += processINotifyEvent( event, list, &files_to_notify, files_needing_readding ); } } else { LOG(logWARNING) << "Error reading from inotify " << errno; } } if ( fds[1].revents & POLLIN ) { uint8_t byte; read( breaking_pipe_read_fd_, &byte, sizeof byte ); } } return files_to_notify; } // Treats the passed event and returns the number of bytes used size_t INotifyWatchTowerDriver::processINotifyEvent( const struct inotify_event* event, INotifyObservedFileList* list, std::vector* files_to_notify, std::vector* files_needing_readding ) { LOG(logDEBUG) << "Event received: " << std::hex << event->mask; INotifyObservedFile* file = nullptr; if ( event->mask & ( IN_MODIFY | IN_DELETE_SELF | IN_MOVE_SELF ) ) { LOG(logDEBUG) << "IN_MODIFY | IN_DELETE_SELF | IN_MOVE_SELF for wd " << event->wd; // Retrieve the file file = list->searchByFileOrSymlinkWd( { event->wd }, { event->wd } ); } else if ( event->mask & ( IN_CREATE | IN_MOVED_TO | IN_MOVED_FROM ) ) { LOG(logDEBUG) << "IN_CREATE | IN_MOVED_TO | IN_MOVED_FROM for wd " << event->wd << " name: " << event->name; // Retrieve the file file = list->searchByDirWdAndName( { event->wd }, event->name ); if ( file ) { LOG(logDEBUG) << "Dir change for watched file " << event->name; files_needing_readding->push_back( file ); } } else { LOG(logDEBUG) << "Unexpected event: " << event->mask << " wd " << event->wd; } if ( file ) { LOG(logDEBUG) << "Adding file: " << std::hex << file; files_to_notify->push_back( file ); } return sizeof( struct inotify_event ) + event->len; } void INotifyWatchTowerDriver::interruptWait() { char byte = 'X'; (void) write( breaking_pipe_write_fd_, (void*) &byte, sizeof byte ); } glogg-1.1.4/src/viewtools.cpp0000644000175000017500000000543013076461374015147 0ustar nickonicko/* * Copyright (C) 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include "viewtools.h" #include "log.h" /* ElasticHook */ void ElasticHook::move( int value ) { static constexpr int MAX_POSITION = 2000; if ( timer_id_ == 0 ) timer_id_ = startTimer( TIMER_PERIOD_MS ); int resistance = 0; if ( !held_ && ( position_ * value > 0 ) ) // value and resistance have the same sign resistance = position_ / 8; position_ = std::min( position_ + ( value - resistance ), MAX_POSITION ); if ( !held_ && ( std::chrono::duration_cast ( std::chrono::steady_clock::now() - last_update_ ).count() > TIMER_PERIOD_MS ) ) decreasePosition(); if ( ( ! hooked_ ) && position_ >= hook_threshold_ ) { position_ -= hook_threshold_; hooked_ = true; emit hooked( true ); } else if ( hooked_ && position_ <= - hook_threshold_ ) { position_ += hook_threshold_; hooked_ = false; emit hooked( false ); } if ( position_ < 0 && !isHooked() ) position_ = 0; last_update_ = std::chrono::steady_clock::now(); LOG( logDEBUG ) << "ElasticHook::move: new value " << position_; emit lengthChanged(); } void ElasticHook::timerEvent( QTimerEvent* event ) { if ( !held_ && ( std::chrono::duration_cast ( std::chrono::steady_clock::now() - last_update_ ).count() > TIMER_PERIOD_MS ) ) { decreasePosition(); last_update_ = std::chrono::steady_clock::now(); } } void ElasticHook::decreasePosition() { static constexpr int PROP_RATIO = 10; // position_ -= DECREASE_RATE + ( ( position_/SQUARE_RATIO ) * ( position_/SQUARE_RATIO ) ); if ( std::abs( position_ ) < DECREASE_RATE ) { position_ = 0; killTimer( timer_id_ ); timer_id_ = 0; } else if ( position_ > 0 ) position_ -= DECREASE_RATE + ( position_/PROP_RATIO ); else if ( position_ < 0 ) position_ += DECREASE_RATE - ( position_/PROP_RATIO ); LOG( logDEBUG ) << "ElasticHook::timerEvent: new value " << position_; emit lengthChanged(); } glogg-1.1.4/src/overviewwidget.cpp0000644000175000017500000002050013076461374016161 0ustar nickonicko/* * Copyright (C) 2011, 2012, 2013 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ // This file implements OverviewWidget. This class is responsable for // managing and painting the matches overview widget. #include #include #include #include "log.h" #include "overviewwidget.h" #include "overview.h" // Graphic parameters const int OverviewWidget::LINE_MARGIN = 4; const int OverviewWidget::STEP_DURATION_MS = 30; const int OverviewWidget::INITIAL_TTL_VALUE = 5; #define HIGHLIGHT_XPM_WIDTH 27 #define HIGHLIGHT_XPM_HEIGHT 9 #define S(x) #x #define SX(x) S(x) // width height colours char/pixel // Colours #define HIGHLIGHT_XPM_LEAD_LINE SX(HIGHLIGHT_XPM_WIDTH) " " SX(HIGHLIGHT_XPM_HEIGHT) " 2 1",\ " s mask c none",\ "x c #572F80" const char* const highlight_xpm[][14] = { { HIGHLIGHT_XPM_LEAD_LINE, " ", " ", " xxxxxxxxxxxxxxxxxxxxx ", " xxxxxxxxxxxxxxxxxxxxx ", " xx xx ", " xx xx ", " xx xx ", " xxxxxxxxxxxxxxxxxxxxx ", " xxxxxxxxxxxxxxxxxxxxx ", " ", " ", }, { HIGHLIGHT_XPM_LEAD_LINE, " ", " xxxxxxxxxxxxxxxxxxxxxxx ", " xxxxxxxxxxxxxxxxxxxxxxx ", " xxxxxxxxxxxxxxxxxxxxxxx ", " xxx xxx ", " xxx xxx ", " xxx xxx ", " xxxxxxxxxxxxxxxxxxxxxxx ", " xxxxxxxxxxxxxxxxxxxxxxx ", " xxxxxxxxxxxxxxxxxxxxxxx ", " ", }, { HIGHLIGHT_XPM_LEAD_LINE, " xxxxxxxxxxxxxxxxxxxxxxxxx ", " xxxxxxxxxxxxxxxxxxxxxxxxx ", " xxxxxxxxxxxxxxxxxxxxxxxxx ", " xxxxxxxxxxxxxxxxxxxxxxxxx ", " xxxx xxxx ", " xxxx xxxx ", " xxxx xxxx ", " xxxxxxxxxxxxxxxxxxxxxxxxx ", " xxxxxxxxxxxxxxxxxxxxxxxxx ", " xxxxxxxxxxxxxxxxxxxxxxxxx ", " xxxxxxxxxxxxxxxxxxxxxxxxx ", }, { HIGHLIGHT_XPM_LEAD_LINE, " ", " xxxxxxxxxxxxxxxxxxxxxxx ", " xxxxxxxxxxxxxxxxxxxxxxx ", " xxxxxxxxxxxxxxxxxxxxxxx ", " xxx xxx ", " xxx xxx ", " xxx xxx ", " xxxxxxxxxxxxxxxxxxxxxxx ", " xxxxxxxxxxxxxxxxxxxxxxx ", " xxxxxxxxxxxxxxxxxxxxxxx ", " ", }, { HIGHLIGHT_XPM_LEAD_LINE, " ", " ", " xxxxxxxxxxxxxxxxxxxxx ", " xxxxxxxxxxxxxxxxxxxxx ", " xx xx ", " xx xx ", " xx xx ", " xxxxxxxxxxxxxxxxxxxxx ", " xxxxxxxxxxxxxxxxxxxxx ", " ", " ", }, { HIGHLIGHT_XPM_LEAD_LINE, " ", " ", " ", " xxxxxxxxxxxxxxxxxxx ", " x x ", " x x ", " x x ", " xxxxxxxxxxxxxxxxxxx ", " ", " ", " ", }, }; OverviewWidget::OverviewWidget( QWidget* parent ) : QWidget( parent ), highlightTimer_() { overview_ = NULL; setBackgroundRole( QPalette::Window ); // Highlight highlightedLine_ = -1; highlightedTTL_ = 0; // We should be hidden by default (e.g. for the FilteredView) hide(); } void OverviewWidget::paintEvent( QPaintEvent* /* paintEvent */ ) { static const QColor match_color("red"); static const QColor mark_color("dodgerblue"); static const QPixmap highlight_pixmap[] = { QPixmap( highlight_xpm[0] ), QPixmap( highlight_xpm[1] ), QPixmap( highlight_xpm[2] ), QPixmap( highlight_xpm[3] ), QPixmap( highlight_xpm[4] ), QPixmap( highlight_xpm[5] ), }; // We must be hidden until we have an Overview assert( overview_ != NULL ); overview_->updateView( height() ); { QPainter painter( this ); painter.fillRect( painter.viewport(), painter.background() ); // The line separating from the main view painter.setPen( palette().color(QPalette::Text) ); painter.drawLine( 0, 0, 0, height() ); // The 'match' lines painter.setPen( match_color ); foreach (Overview::WeightedLine line, *(overview_->getMatchLines()) ) { painter.setOpacity( ( 1.0 / Overview::WeightedLine::WEIGHT_STEPS ) * ( line.weight() + 1 ) ); // (allow multiple matches to look 'darker' than a single one.) painter.drawLine( 1 + LINE_MARGIN, line.position(), width() - LINE_MARGIN - 1, line.position() ); } // The 'mark' lines painter.setPen( mark_color ); foreach (Overview::WeightedLine line, *(overview_->getMarkLines()) ) { painter.setOpacity( ( 1.0 / Overview::WeightedLine::WEIGHT_STEPS ) * ( line.weight() + 1 ) ); // (allow multiple matches to look 'darker' than a single one.) painter.drawLine( 1 + LINE_MARGIN, line.position(), width() - LINE_MARGIN - 1, line.position() ); } // The 'view' lines painter.setOpacity( 1 ); painter.setPen( palette().color(QPalette::Text) ); std::pair view_lines = overview_->getViewLines(); painter.drawLine( 1, view_lines.first, width(), view_lines.first ); painter.drawLine( 1, view_lines.second, width(), view_lines.second ); // The highlight if ( highlightedLine_ >= 0 ) { /* QPen highlight_pen( palette().color(QPalette::Text) ); highlight_pen.setWidth( 4 - highlightedTTL_ ); painter.setOpacity( 1 ); painter.setPen( highlight_pen ); painter.drawRect( 2, position - 2, width() - 2 - 2, 4 ); */ int position = overview_->yFromFileLine( highlightedLine_ ); painter.drawPixmap( ( width() - HIGHLIGHT_XPM_WIDTH ) / 2, position - ( HIGHLIGHT_XPM_HEIGHT / 2 ), highlight_pixmap[ INITIAL_TTL_VALUE - highlightedTTL_ ] ); } } } void OverviewWidget::mousePressEvent( QMouseEvent* mouseEvent ) { if ( mouseEvent->button() == Qt::LeftButton ) handleMousePress( mouseEvent->y() ); } void OverviewWidget::mouseMoveEvent( QMouseEvent* mouseEvent ) { if ( mouseEvent->buttons() |= Qt::LeftButton ) handleMousePress( mouseEvent->y() ); } void OverviewWidget::handleMousePress( int position ) { int line = overview_->fileLineFromY( position ); LOG(logDEBUG) << "OverviewWidget::handleMousePress y=" << position << " line=" << line; emit lineClicked( line ); } void OverviewWidget::highlightLine( qint64 line ) { highlightTimer_.stop(); highlightedLine_ = line; highlightedTTL_ = INITIAL_TTL_VALUE; update(); highlightTimer_.start( STEP_DURATION_MS, this ); } void OverviewWidget::removeHighlight() { highlightTimer_.stop(); highlightedLine_ = -1; update(); } void OverviewWidget::timerEvent( QTimerEvent* event ) { if ( event->timerId() == highlightTimer_.timerId() ) { LOG(logDEBUG) << "OverviewWidget::timerEvent"; if ( highlightedTTL_ > 0 ) { --highlightedTTL_; update(); } else { highlightTimer_.stop(); } } else { QObject::timerEvent( event ); } } glogg-1.1.4/src/persistable.h0000644000175000017500000000220513076461374015073 0ustar nickonicko/* * Copyright (C) 2011 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef PERSISTABLE_H #define PERSISTABLE_H class QSettings; // Must be implemented by classes which could be saved to persistent // storage by PersistentInfo. class Persistable { public: virtual ~Persistable() {} // Must be implemented to save/retrieve from Qt Settings virtual void saveToStorage( QSettings& settings ) const = 0; virtual void retrieveFromStorage( QSettings& settings ) = 0; }; #endif glogg-1.1.4/src/marks.h0000644000175000017500000000622713076461374013703 0ustar nickonicko/* * Copyright (C) 2011 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef MARKS_H #define MARKS_H #include #include // Class encapsulating a single mark // Contains the line number the mark is identifying. class Mark { public: Mark( int line ) { lineNumber_ = line; }; // Accessors int lineNumber() const { return lineNumber_; } private: int lineNumber_; }; // A list of marks, i.e. line numbers optionally associated to an // identifying character. class Marks { public: // Create an empty Marks Marks(); // Add a mark at the given line, optionally identified by the given char // If a mark for this char already exist, the previous one is replaced. // It will happily add marks anywhere, even at stupid indexes. void addMark( qint64 line, QChar mark = QChar() ); // Get the (unique) mark identified by the passed char. qint64 getMark( QChar mark ) const; // Returns wheither the passed line has a mark on it. bool isLineMarked( qint64 line ) const; // Delete the mark identified by the passed char. void deleteMark( QChar mark ); // Delete the mark present on the passed line or do nothing if there is // none. void deleteMark( qint64 line ); // Get the line marked identified by the index (in this list) passed. qint64 getLineMarkedByIndex( int index ) const { return marks_[index].lineNumber(); } // Return the total number of marks int size() const { return marks_.size(); } // Completely clear the marks list. void clear(); // Iterator // Provide a const_iterator for the client to iterate through the marks. class const_iterator { public: const_iterator( QList::const_iterator iter ) { internal_iter_ = iter; } const_iterator( const const_iterator& original ) { internal_iter_ = original.internal_iter_; } const Mark& operator*() { return *internal_iter_; } const Mark* operator->() { return &(*internal_iter_); } bool operator!=( const const_iterator& other ) const { return ( internal_iter_ != other.internal_iter_ ); } const_iterator& operator++() { ++internal_iter_ ; return *this; } private: QList::const_iterator internal_iter_; }; const_iterator begin() const { return const_iterator( marks_.begin() ); } const_iterator end() const { return const_iterator( marks_.end() ); } private: // List of marks. QList marks_; }; #endif glogg-1.1.4/src/platformfilewatcher.h0000644000175000017500000000426613076461374016631 0ustar nickonicko/* * Copyright (C) 2014, 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef PLATFORMFILEWATCHER_H #define PLATFORMFILEWATCHER_H #include "filewatcher.h" #include #ifdef _WIN32 # include "winwatchtowerdriver.h" #else # include "inotifywatchtowerdriver.h" #endif #include "watchtower.h" class INotifyWatchTower; // Please note that due to the implementation of the constructor // this class is not thread safe and shall always be used from the main UI thread. class PlatformFileWatcher : public FileWatcher { Q_OBJECT public: // Create the empty object PlatformFileWatcher(); // Destroy the object ~PlatformFileWatcher(); void addFile( const QString& fileName ); void removeFile( const QString& fileName ); // Set the polling interval (0 means disabled) void setPollingInterval( uint32_t interval_ms ); signals: void fileChanged( const QString& ); private: #ifdef _WIN32 # ifdef HAS_TEMPLATE_ALIASES using PlatformWatchTower = WatchTower; # else typedef WatchTower PlatformWatchTower; # endif #else # ifdef HAS_TEMPLATE_ALIASES using PlatformWatchTower = WatchTower; # else typedef WatchTower PlatformWatchTower; # endif #endif // The following variables are protected by watched_files_mutex_ QString watched_file_name_; // Reference to the (unique) watchtower. static std::shared_ptr watch_tower_; std::shared_ptr notification_; }; #endif glogg-1.1.4/src/filteredview.cpp0000644000175000017500000000473713076461374015616 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2012 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ // This file implements the FilteredView concrete class. // Most of the actual drawing and event management is done in AbstractLogView // Only behaviour specific to the filtered (bottom) view is implemented here. #include #include "filteredview.h" FilteredView::FilteredView( LogFilteredData* newLogData, const QuickFindPattern* const quickFindPattern, QWidget* parent ) : AbstractLogView( newLogData, quickFindPattern, parent ) { // We keep a copy of the filtered data for fast lookup of the line type logFilteredData_ = newLogData; } void FilteredView::setVisibility( Visibility visi ) { assert( logFilteredData_ ); LogFilteredData::Visibility data_visibility = LogFilteredData::MarksAndMatches; switch ( visi ) { case MarksOnly: data_visibility = LogFilteredData::MarksOnly; break; case MatchesOnly: data_visibility = LogFilteredData::MatchesOnly; break; case MarksAndMatches: data_visibility = LogFilteredData::MarksAndMatches; break; }; logFilteredData_->setVisibility( data_visibility ); updateData(); } // For the filtered view, a line is always matching! AbstractLogView::LineType FilteredView::lineType( int lineNumber ) const { LogFilteredData::FilteredLineType type = logFilteredData_->filteredLineTypeByIndex( lineNumber ); if ( type == LogFilteredData::Mark ) return Marked; else return Match; } qint64 FilteredView::displayLineNumber( int lineNumber ) const { // Display a 1-based index return logFilteredData_->getMatchingLineNumber( lineNumber ) + 1; } qint64 FilteredView::maxDisplayLineNumber() const { return logFilteredData_->getNbTotalLines(); } glogg-1.1.4/src/overviewwidget.h0000644000175000017500000000371713076461374015641 0ustar nickonicko/* * Copyright (C) 2011, 2012, 2013 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef OVERVIEWWIDGET_H #define OVERVIEWWIDGET_H #include #include class Overview; class OverviewWidget : public QWidget { Q_OBJECT public: OverviewWidget( QWidget* parent = 0 ); // Associate the widget with an Overview object. void setOverview( Overview* overview ) { overview_ = overview; } public slots: // Sent when a match at the line passed must be highlighted in // the overview void highlightLine( qint64 line ); void removeHighlight(); protected: void paintEvent( QPaintEvent* paintEvent ); void mousePressEvent( QMouseEvent* mouseEvent ); void mouseMoveEvent( QMouseEvent* mouseEvent ); void timerEvent( QTimerEvent* event ); signals: // Sent when the user click on a line in the Overview. void lineClicked( int line ); private: // Constants static const int LINE_MARGIN; static const int STEP_DURATION_MS; static const int INITIAL_TTL_VALUE; Overview* overview_; // Highlight: // Which line is higlighted, or -1 if none int highlightedLine_; // Number of step until the highlight become static int highlightedTTL_; QBasicTimer highlightTimer_; void handleMousePress( int position ); }; #endif glogg-1.1.4/src/log.h0000644000175000017500000001173513076461374013347 0ustar nickonicko/* * Copyright (C) 2009, 2010 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef __LOG_H__ #define __LOG_H__ #include #include #include // Modify here! //#define FILELOG_MAX_LEVEL logDEBUG inline std::string NowTime(); enum TLogLevel {logERROR, logWARNING, logINFO, logDEBUG, logDEBUG1, logDEBUG2, logDEBUG3, logDEBUG4}; template class Log { public: Log(); virtual ~Log(); std::ostringstream& Get(TLogLevel level = logINFO, const std::string& sourceFile = "", int lineNumber = 0); public: static TLogLevel ReportingLevel() { return reportingLevel; } static std::string ToString(TLogLevel level); static TLogLevel FromString(const std::string& level); static void setReportingLevel(TLogLevel level) { reportingLevel = level; } template friend class Log; protected: std::ostringstream os; private: static TLogLevel reportingLevel; Log(const Log&); Log& operator =(const Log&); }; template TLogLevel Log::reportingLevel = logDEBUG4; template Log::Log() { } template std::ostringstream& Log::Get(TLogLevel level, const std::string& sourceFile, int lineNumber) { os << "- " << NowTime(); os << " " << ToString(level); os << " " << sourceFile << ":" << lineNumber << ": "; os << std::string(level > logDEBUG ? level - logDEBUG : 0, '\t'); return os; } template Log::~Log() { os << std::endl; T::Output(os.str()); } template std::string Log::ToString(TLogLevel level) { static const char* const buffer[] = {"ERROR", "WARNING", "INFO", "DEBUG", "DEBUG1", "DEBUG2", "DEBUG3", "DEBUG4"}; return buffer[level]; } template TLogLevel Log::FromString(const std::string& level) { if (level == "DEBUG4") return logDEBUG4; if (level == "DEBUG3") return logDEBUG3; if (level == "DEBUG2") return logDEBUG2; if (level == "DEBUG1") return logDEBUG1; if (level == "DEBUG") return logDEBUG; if (level == "INFO") return logINFO; if (level == "WARNING") return logWARNING; if (level == "ERROR") return logERROR; Log().Get(logWARNING) << "Unknown logging level '" << level << "'. Using INFO level as default."; return logINFO; } class Output2FILE { public: static FILE*& Stream(); static void Output(const std::string& msg); }; inline FILE*& Output2FILE::Stream() { static FILE* pStream = stderr; return pStream; } inline void Output2FILE::Output(const std::string& msg) { FILE* pStream = Stream(); if (!pStream) return; fprintf(pStream, "%s", msg.c_str()); fflush(pStream); } #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) # if defined (BUILDING_FILELOG_DLL) # define FILELOG_DECLSPEC __declspec (dllexport) # elif defined (USING_FILELOG_DLL) # define FILELOG_DECLSPEC __declspec (dllimport) # else # define FILELOG_DECLSPEC # endif // BUILDING_DBSIMPLE_DLL #else # define FILELOG_DECLSPEC #endif // _WIN32 //class FILELOG_DECLSPEC FILELog : public Log {}; typedef Log FILELog; #ifndef FILELOG_MAX_LEVEL #define FILELOG_MAX_LEVEL logDEBUG #endif #define FILE_LOG(level) \ if (level > FILELOG_MAX_LEVEL) ;\ else if (level > FILELog::ReportingLevel() || !Output2FILE::Stream()) ; \ else FILELog().Get(level, __FILE__, __LINE__) #define LOG(level) FILE_LOG(level) #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) #include inline std::string NowTime() { const int MAX_LEN = 200; char buffer[MAX_LEN]; if (GetTimeFormatA(LOCALE_USER_DEFAULT, 0, 0, "HH':'mm':'ss", buffer, MAX_LEN) == 0) return "Error in NowTime()"; char result[100] = {0}; static DWORD first = GetTickCount(); std::sprintf(result, "%s.%03ld", buffer, (long)(GetTickCount() - first) % 1000); return result; } #else #include inline std::string NowTime() { char buffer[11]; time_t t; time(&t); tm r; strftime(buffer, sizeof(buffer), "%T", localtime_r(&t, &r)); struct timeval tv; gettimeofday(&tv, 0); char result[100] = {0}; std::sprintf(result, "%s.%03ld", buffer, (long)tv.tv_usec / 1000); return result; } #endif //WIN32 #endif //__LOG_H__ glogg-1.1.4/src/quickfindwidget.cpp0000644000175000017500000001373213076461374016301 0ustar nickonicko/* * Copyright (C) 2010, 2013 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include "log.h" #include #include #include #include #include #include "configuration.h" #include "qfnotifications.h" #include "quickfindwidget.h" const int QuickFindWidget::NOTIFICATION_TIMEOUT = 5000; const QString QFNotification::REACHED_EOF = "Reached end of file, no occurence found."; const QString QFNotification::REACHED_BOF = "Reached beginning of file, no occurence found."; QuickFindWidget::QuickFindWidget( QWidget* parent ) : QWidget( parent ) { // ui_.setupUi( this ); // setFocusProxy(ui_.findEdit); // setProperty("topBorder", true); QHBoxLayout *layout = new QHBoxLayout( this ); layout->setMargin( 0 ); layout->setSpacing( 6 ); closeButton_ = setupToolButton( QLatin1String(""), QLatin1String( ":/images/darkclosebutton.png" ) ); layout->addWidget( closeButton_ ); editQuickFind_ = new QLineEdit( this ); // FIXME: set MinimumSize might be to constraining editQuickFind_->setMinimumSize( QSize( 150, 0 ) ); layout->addWidget( editQuickFind_ ); ignoreCaseCheck_ = new QCheckBox( "Ignore &case" ); layout->addWidget( ignoreCaseCheck_ ); previousButton_ = setupToolButton( QLatin1String("Previous"), QLatin1String( ":/images/arrowup.png" ) ); layout->addWidget( previousButton_ ); nextButton_ = setupToolButton( QLatin1String("Next"), QLatin1String( ":/images/arrowdown.png" ) ); layout->addWidget( nextButton_ ); notificationText_ = new QLabel( "" ); // FIXME: set MinimumSize might be too constraining int width = QFNotification::maxWidth( notificationText_ ); notificationText_->setMinimumSize( width, 0 ); layout->addWidget( notificationText_ ); setMinimumWidth( minimumSizeHint().width() ); // Behaviour connect( closeButton_, SIGNAL( clicked() ), SLOT( closeHandler() ) ); connect( editQuickFind_, SIGNAL( textEdited( QString ) ), this, SLOT( textChanged() ) ); connect( ignoreCaseCheck_, SIGNAL( stateChanged( int ) ), this, SLOT( textChanged() ) ); /* connect( editQuickFind_. SIGNAL( textChanged( QString ) ), this, SLOT( updateButtons() ) ); */ connect( editQuickFind_, SIGNAL( returnPressed() ), this, SLOT( returnHandler() ) ); connect( previousButton_, SIGNAL( clicked() ), this, SLOT( doSearchBackward() ) ); connect( nextButton_, SIGNAL( clicked() ), this, SLOT( doSearchForward() ) ); // Notification timer: notificationTimer_ = new QTimer( this ); notificationTimer_->setSingleShot( true ); connect( notificationTimer_, SIGNAL( timeout() ), this, SLOT( notificationTimeout() ) ); } void QuickFindWidget::userActivate() { userRequested_ = true; QWidget::show(); editQuickFind_->setFocus( Qt::ShortcutFocusReason ); } // // SLOTS // void QuickFindWidget::changeDisplayedPattern( const QString& newPattern ) { editQuickFind_->setText( newPattern ); } void QuickFindWidget::notify( const QFNotification& message ) { LOG(logDEBUG) << "QuickFindWidget::notify()"; notificationText_->setText( message.message() ); QWidget::show(); notificationTimer_->start( NOTIFICATION_TIMEOUT ); } void QuickFindWidget::clearNotification() { LOG(logDEBUG) << "QuickFindWidget::clearNotification()"; notificationText_->setText( "" ); } // User clicks forward arrow void QuickFindWidget::doSearchForward() { LOG(logDEBUG) << "QuickFindWidget::doSearchForward()"; // The user has clicked on a button, so we assume she wants // the widget to stay visible. userRequested_ = true; emit patternConfirmed( editQuickFind_->text(), isIgnoreCase() ); emit searchForward(); } // User clicks backward arrow void QuickFindWidget::doSearchBackward() { LOG(logDEBUG) << "QuickFindWidget::doSearchBackward()"; // The user has clicked on a button, so we assume she wants // the widget to stay visible. userRequested_ = true; emit patternConfirmed( editQuickFind_->text(), isIgnoreCase() ); emit searchBackward(); } // Close and search when the user presses Return void QuickFindWidget::returnHandler() { emit patternConfirmed( editQuickFind_->text(), isIgnoreCase() ); // Close the widget userRequested_ = false; this->hide(); emit close(); } // Close and reset flag when the user clicks 'close' void QuickFindWidget::closeHandler() { userRequested_ = false; this->hide(); emit close(); emit cancelSearch(); } void QuickFindWidget::notificationTimeout() { // We close the widget if the user hasn't explicitely requested it. if ( userRequested_ == false ) this->hide(); } void QuickFindWidget::textChanged() { emit patternUpdated( editQuickFind_->text(), isIgnoreCase() ); } // // Private functions // QToolButton* QuickFindWidget::setupToolButton( const QString &text, const QString &icon) { QToolButton *toolButton = new QToolButton(this); toolButton->setText(text); toolButton->setAutoRaise(true); toolButton->setIcon(QIcon(icon)); toolButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); return toolButton; } bool QuickFindWidget::isIgnoreCase() const { return ( ignoreCaseCheck_->checkState() == Qt::Checked ); } glogg-1.1.4/src/watchtowerlist.cpp0000644000175000017500000000003613076461374016174 0ustar nickonicko#include "watchtowerlist.h" glogg-1.1.4/src/savedsearches.cpp0000644000175000017500000000775613076461374015751 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2011 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ // This file implements class SavedSearch #include #include #include "log.h" #include "savedsearches.h" const int SavedSearches::SAVEDSEARCHES_VERSION = 1; const int SavedSearches::maxNumberOfRecentSearches = 50; SavedSearches::SavedSearches() : savedSearches_() { qRegisterMetaTypeStreamOperators( "SavedSearches" ); } void SavedSearches::addRecent( const QString& text ) { // We're not interested in blank lines if ( text.isEmpty() ) return; // Remove any copy of the about to be added text savedSearches_.removeAll( text ); // Add at the front savedSearches_.push_front( text ); // Trim the list if it's too long while (savedSearches_.size() > maxNumberOfRecentSearches) savedSearches_.pop_back(); } QStringList SavedSearches::recentSearches() const { return savedSearches_; } // // Operators for serialization // QDataStream& operator<<( QDataStream& out, const SavedSearches& object ) { LOG(logDEBUG) << "<>( QDataStream& in, SavedSearches& object ) { LOG(logDEBUG) << ">>operator from SavedSearches"; in >> object.savedSearches_; return in; } // // Persistable virtual functions implementation // void SavedSearches::saveToStorage( QSettings& settings ) const { LOG(logDEBUG) << "SavedSearches::saveToStorage"; settings.beginGroup( "SavedSearches" ); // Remove everything in case the array is shorter than the previous one settings.remove(""); settings.setValue( "version", SAVEDSEARCHES_VERSION ); settings.beginWriteArray( "searchHistory" ); for (int i = 0; i < savedSearches_.size(); ++i) { settings.setArrayIndex( i ); settings.setValue( "string", savedSearches_.at( i ) ); } settings.endArray(); settings.endGroup(); } void SavedSearches::retrieveFromStorage( QSettings& settings ) { LOG(logDEBUG) << "SavedSearches::retrieveFromStorage"; savedSearches_.clear(); if ( settings.contains( "SavedSearches/version" ) ) { // Unserialise the "new style" stored history settings.beginGroup( "SavedSearches" ); if ( settings.value( "version" ) == SAVEDSEARCHES_VERSION ) { int size = settings.beginReadArray( "searchHistory" ); for (int i = 0; i < size; ++i) { settings.setArrayIndex(i); QString search = settings.value( "string" ).toString(); savedSearches_.append( search ); } settings.endArray(); } else { LOG(logERROR) << "Unknown version of FilterSet, ignoring it..."; } settings.endGroup(); } else { LOG(logWARNING) << "Trying to import legacy (<=0.8.2) saved searches..."; SavedSearches tmp_saved_searches = settings.value( "savedSearches" ).value(); *this = tmp_saved_searches; LOG(logWARNING) << "...imported searches: " << savedSearches_.count() << " elements"; // Remove the old key once migration is done settings.remove( "savedSearches" ); // And replace it with the new one saveToStorage( settings ); settings.sync(); } } glogg-1.1.4/src/quickfindmux.cpp0000644000175000017500000001374113076461374015627 0ustar nickonicko/* * Copyright (C) 2013, 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include "log.h" #include "persistentinfo.h" #include "configuration.h" #include "quickfindmux.h" #include "qfnotifications.h" QuickFindMux::QuickFindMux( std::shared_ptr pattern ) : QObject(), pattern_( pattern ), registeredSearchables_() { selector_ = nullptr; // Forward the pattern's signal to our listeners connect( pattern_.get(), SIGNAL( patternUpdated() ), this, SLOT( notifyPatternChanged() ) ); } // // Public member functions // void QuickFindMux::registerSelector( const QuickFindMuxSelectorInterface* selector ) { LOG(logDEBUG) << "QuickFindMux::registerSelector"; // The selector object we will use when forwarding search requests selector_ = selector; unregisterAllSearchables(); if ( selector ) { for ( auto i: selector_->getAllSearchables() ) registerSearchable( i ); } else { // null selector, all is well, we don't do anything. } } void QuickFindMux::setDirection( QFDirection direction ) { LOG(logDEBUG) << "QuickFindMux::setDirection: new direction: " << direction; currentDirection_ = direction; } // // Public slots // void QuickFindMux::searchNext() { LOG(logDEBUG) << "QuickFindMux::searchNext"; if ( currentDirection_ == Forward ) searchForward(); else searchBackward(); } void QuickFindMux::searchPrevious() { LOG(logDEBUG) << "QuickFindMux::searchPrevious"; if ( currentDirection_ == Forward ) searchBackward(); else searchForward(); } void QuickFindMux::searchForward() { LOG(logDEBUG) << "QuickFindMux::searchForward"; if ( auto searchable = getSearchableWidget() ) searchable->searchForward(); } void QuickFindMux::searchBackward() { LOG(logDEBUG) << "QuickFindMux::searchBackward"; if ( auto searchable = getSearchableWidget() ) searchable->searchBackward(); } void QuickFindMux::setNewPattern( const QString& new_pattern, bool ignore_case ) { static std::shared_ptr config = Persistent( "settings" ); LOG(logDEBUG) << "QuickFindMux::setNewPattern"; // If we must do an incremental search, we do it now if ( config->isQuickfindIncremental() ) { pattern_->changeSearchPattern( new_pattern, ignore_case ); if ( auto searchable = getSearchableWidget() ) { if ( currentDirection_ == Forward ) searchable->incrementallySearchForward(); else searchable->incrementallySearchBackward(); } } } void QuickFindMux::confirmPattern( const QString& new_pattern, bool ignore_case ) { static std::shared_ptr config = Persistent( "settings" ); pattern_->changeSearchPattern( new_pattern, ignore_case ); // if non-incremental, we perform the search now if ( ! config->isQuickfindIncremental() ) { searchNext(); } else { if ( auto searchable = getSearchableWidget() ) searchable->incrementalSearchStop(); } } void QuickFindMux::cancelSearch() { static std::shared_ptr config = Persistent( "settings" ); if ( config->isQuickfindIncremental() ) { if ( auto searchable = getSearchableWidget() ) searchable->incrementalSearchAbort(); } } // // Private slots // void QuickFindMux::changeQuickFind( const QString& new_pattern, QFDirection new_direction ) { pattern_->changeSearchPattern( new_pattern ); setDirection( new_direction ); } void QuickFindMux::notifyPatternChanged() { emit patternChanged( pattern_->getPattern() ); } // // Private member functions // // Use the registered 'selector' to determine where to send the search requests. SearchableWidgetInterface* QuickFindMux::getSearchableWidget() const { LOG(logDEBUG) << "QuickFindMux::getSearchableWidget"; SearchableWidgetInterface* searchable = nullptr; if ( selector_ ) searchable = selector_->getActiveSearchable(); else LOG(logWARNING) << "QuickFindMux::getActiveSearchable() no registered selector"; return searchable; } void QuickFindMux::registerSearchable( QObject* searchable ) { LOG(logDEBUG) << "QuickFindMux::registerSearchable"; // The searchable can change our qf pattern connect( searchable, SIGNAL( changeQuickFind( const QString&, QuickFindMux::QFDirection ) ), this, SLOT( changeQuickFind( const QString&, QuickFindMux::QFDirection ) ) ); // Send us notifications connect( searchable, SIGNAL( notifyQuickFind( const QFNotification& ) ), this, SIGNAL( notify( const QFNotification& ) ) ); // And clear them connect( searchable, SIGNAL( clearQuickFindNotification() ), this, SIGNAL( clearNotification() ) ); // Search can be initiated by the view itself connect( searchable, SIGNAL( searchNext() ), this, SLOT( searchNext() ) ); connect( searchable, SIGNAL( searchPrevious() ), this, SLOT( searchPrevious() ) ); registeredSearchables_.push_back( searchable ); } void QuickFindMux::unregisterAllSearchables() { for ( auto searchable: registeredSearchables_ ) disconnect( searchable, 0, this, 0 ); registeredSearchables_.clear(); } glogg-1.1.4/src/optionsdialog.cpp0000644000175000017500000001515313076461374015772 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2011, 2013 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include #include "optionsdialog.h" #include "log.h" #include "persistentinfo.h" #include "configuration.h" static const uint32_t POLL_INTERVAL_MIN = 10; static const uint32_t POLL_INTERVAL_MAX = 3600000; // Constructor OptionsDialog::OptionsDialog( QWidget* parent ) : QDialog(parent) { setupUi( this ); setupTabs(); setupFontList(); setupRegexp(); // Validators QValidator* polling_interval_validator_ = new QIntValidator( POLL_INTERVAL_MIN, POLL_INTERVAL_MAX, this ); pollIntervalLineEdit->setValidator( polling_interval_validator_ ); connect(buttonBox, SIGNAL( clicked( QAbstractButton* ) ), this, SLOT( onButtonBoxClicked( QAbstractButton* ) ) ); connect(fontFamilyBox, SIGNAL( currentIndexChanged(const QString& ) ), this, SLOT( updateFontSize( const QString& ) )); connect(incrementalCheckBox, SIGNAL( toggled( bool ) ), this, SLOT( onIncrementalChanged() ) ); connect(pollingCheckBox, SIGNAL( toggled( bool ) ), this, SLOT( onPollingChanged() ) ); updateDialogFromConfig(); setupIncremental(); setupPolling(); } // // Private functions // // Setups the tabs depending on the configuration void OptionsDialog::setupTabs() { #ifndef GLOGG_SUPPORTS_POLLING tabWidget->removeTab( 1 ); #endif } // Populates the 'family' ComboBox void OptionsDialog::setupFontList() { QFontDatabase database; // We only show the fixed fonts foreach ( const QString &str, database.families() ) { if ( database.isFixedPitch( str ) ) fontFamilyBox->addItem( str ); } } // Populate the regexp ComboBoxes void OptionsDialog::setupRegexp() { QStringList regexpTypes; regexpTypes << tr("Extended Regexp") << tr("Wildcards") << tr("Fixed Strings"); mainSearchBox->addItems( regexpTypes ); quickFindSearchBox->addItems( regexpTypes ); } // Enable/disable the QuickFind options depending on the state // of the "incremental" checkbox. void OptionsDialog::setupIncremental() { if ( incrementalCheckBox->isChecked() ) { quickFindSearchBox->setCurrentIndex( getRegexpIndex( FixedString ) ); quickFindSearchBox->setEnabled( false ); } else { quickFindSearchBox->setEnabled( true ); } } void OptionsDialog::setupPolling() { pollIntervalLineEdit->setEnabled( pollingCheckBox->isChecked() ); } // Convert a regexp type to its index in the list int OptionsDialog::getRegexpIndex( SearchRegexpType syntax ) const { return static_cast( syntax ); } // Convert the index of a regexp type to its type SearchRegexpType OptionsDialog::getRegexpTypeFromIndex( int index ) const { return static_cast( index );; } // Updates the dialog box using values in global Config() void OptionsDialog::updateDialogFromConfig() { std::shared_ptr config = Persistent( "settings" ); // Main font QFontInfo fontInfo = QFontInfo( config->mainFont() ); int familyIndex = fontFamilyBox->findText( fontInfo.family() ); if ( familyIndex != -1 ) fontFamilyBox->setCurrentIndex( familyIndex ); int sizeIndex = fontSizeBox->findText( QString::number(fontInfo.pointSize()) ); if ( sizeIndex != -1 ) fontSizeBox->setCurrentIndex( sizeIndex ); // Regexp types mainSearchBox->setCurrentIndex( getRegexpIndex( config->mainRegexpType() ) ); quickFindSearchBox->setCurrentIndex( getRegexpIndex( config->quickfindRegexpType() ) ); incrementalCheckBox->setChecked( config->isQuickfindIncremental() ); // Polling pollingCheckBox->setChecked( config->pollingEnabled() ); pollIntervalLineEdit->setText( QString::number( config->pollIntervalMs() ) ); // Last session loadLastSessionCheckBox->setChecked( config->loadLastSession() ); } // // Slots // void OptionsDialog::updateFontSize(const QString& fontFamily) { QFontDatabase database; QString oldFontSize = fontSizeBox->currentText(); QList sizes = database.pointSizes( fontFamily, "" ); fontSizeBox->clear(); foreach (int size, sizes) { fontSizeBox->addItem( QString::number(size) ); } // Now restore the size we had before int i = fontSizeBox->findText(oldFontSize); if ( i != -1 ) fontSizeBox->setCurrentIndex(i); } void OptionsDialog::updateConfigFromDialog() { std::shared_ptr config = Persistent( "settings" ); QFont font = QFont( fontFamilyBox->currentText(), (fontSizeBox->currentText()).toInt() ); config->setMainFont(font); config->setMainRegexpType( getRegexpTypeFromIndex( mainSearchBox->currentIndex() ) ); config->setQuickfindRegexpType( getRegexpTypeFromIndex( quickFindSearchBox->currentIndex() ) ); config->setQuickfindIncremental( incrementalCheckBox->isChecked() ); config->setPollingEnabled( pollingCheckBox->isChecked() ); uint32_t poll_interval = pollIntervalLineEdit->text().toUInt(); if ( poll_interval < POLL_INTERVAL_MIN ) poll_interval = POLL_INTERVAL_MIN; else if (poll_interval > POLL_INTERVAL_MAX ) poll_interval = POLL_INTERVAL_MAX; config->setPollIntervalMs( poll_interval ); config->setLoadLastSession( loadLastSessionCheckBox->isChecked() ); emit optionsChanged(); } void OptionsDialog::onButtonBoxClicked( QAbstractButton* button ) { QDialogButtonBox::ButtonRole role = buttonBox->buttonRole( button ); if ( ( role == QDialogButtonBox::AcceptRole ) || ( role == QDialogButtonBox::ApplyRole ) ) { updateConfigFromDialog(); } if ( role == QDialogButtonBox::AcceptRole ) accept(); else if ( role == QDialogButtonBox::RejectRole ) reject(); } void OptionsDialog::onIncrementalChanged() { setupIncremental(); } void OptionsDialog::onPollingChanged() { setupPolling(); } glogg-1.1.4/src/quickfindwidget.h0000644000175000017500000000526213076461374015745 0ustar nickonicko/* * Copyright (C) 2010, 2013 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef QUICKFINDWIDGET_H #define QUICKFINDWIDGET_H #include #include class QHBoxLayout; class QLineEdit; class QToolButton; class QLabel; class QCheckBox; class QFNotification; enum QFDirection { Forward, Backward, }; class QuickFindWidget : public QWidget { Q_OBJECT public: QuickFindWidget( QWidget* parent = 0 ); // Show the widget with the given direction // when requested by the user (the widget won't timeout) void userActivate(); public slots: // Instructs the widget to change the pattern displayed void changeDisplayedPattern( const QString& newPattern ); // Show the widget for a notification (will timeout) void notify( const QFNotification& message ); // Clear the notification void clearNotification(); private slots: void doSearchForward(); void doSearchBackward(); void returnHandler(); void closeHandler(); void notificationTimeout(); void textChanged(); signals: // Sent when Return is pressed to confirm the pattern // (pattern and ignor_case flag) void patternConfirmed( const QString&, bool ); // Sent every time the pattern is modified // (pattern and ignor_case flag) void patternUpdated( const QString&, bool ); void close(); // Emitted when the user closes the window void cancelSearch(); void searchForward(); void searchBackward(); void searchNext(); private: const static int NOTIFICATION_TIMEOUT; QHBoxLayout* layout_; QToolButton* closeButton_; QToolButton* nextButton_; QToolButton* previousButton_; QLineEdit* editQuickFind_; QCheckBox* ignoreCaseCheck_; QLabel* notificationText_; QToolButton* setupToolButton(const QString &text, const QString &icon); bool isIgnoreCase() const; QTimer* notificationTimer_; QFDirection direction_; // Whether the user explicitely wants us on the screen bool userRequested_; }; #endif glogg-1.1.4/src/config.h0000644000175000017500000000033113076461374014021 0ustar nickonicko/* This is a temporary file to build with qmake, * to be removed when we move to cmake */ #ifndef CONFIG_H_ #define CONFIG_H_ #ifndef _WIN32 #define HAVE_SYMLINK 1 #endif /* #undef HAS_TEMPLATE_ALIASES */ #endif glogg-1.1.4/src/logmainview.cpp0000644000175000017500000000407513076461374015441 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2011, 2013 Nicolas Bonnefon * and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ // This file implements the LogMainView concrete class. // Most of the actual drawing and event management is done in AbstractLogView // Only behaviour specific to the main (top) view is implemented here. #include "logmainview.h" #include "data/logfiltereddata.h" #include "overview.h" LogMainView::LogMainView( const LogData* newLogData, const QuickFindPattern* const quickFindPattern, Overview* overview, OverviewWidget* overview_widget, QWidget* parent) : AbstractLogView( newLogData, quickFindPattern, parent ) { filteredData_ = NULL; // The main data has a real (non NULL) Overview setOverview( overview, overview_widget ); } // Just update our internal record. void LogMainView::useNewFiltering( LogFilteredData* filteredData ) { filteredData_ = filteredData; if ( getOverview() != NULL ) getOverview()->setFilteredData( filteredData_ ); } AbstractLogView::LineType LogMainView::lineType( int lineNumber ) const { if ( filteredData_ != NULL ) { LineType line_type; if ( filteredData_->isLineMarked( lineNumber ) ) line_type = Marked; else if ( filteredData_->isLineInMatchingList( lineNumber ) ) line_type = Match; else line_type = Normal; return line_type; } else return Normal; } glogg-1.1.4/src/watchtower.cpp0000644000175000017500000000003013076461374015272 0ustar nickonicko#include "watchtower.h" glogg-1.1.4/src/filteredview.h0000644000175000017500000000304013076461374015245 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2012 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef FILTEREDVIEW_H #define FILTEREDVIEW_H #include "abstractlogview.h" #include "data/logfiltereddata.h" // Class implementing the filtered (bottom) view widget. class FilteredView : public AbstractLogView { public: FilteredView( LogFilteredData* newLogData, const QuickFindPattern* const quickFindPattern, QWidget* parent = 0 ); // What is visible in the view. enum Visibility { MatchesOnly, MarksOnly, MarksAndMatches }; void setVisibility( Visibility visi ); protected: virtual LineType lineType( int lineNumber ) const; // Number of the filtered line relative to the unfiltered source virtual qint64 displayLineNumber( int lineNumber ) const; virtual qint64 maxDisplayLineNumber() const; private: LogFilteredData* logFilteredData_; }; #endif glogg-1.1.4/src/encodingspeculator.h0000644000175000017500000000276113076461374016455 0ustar nickonicko/* * Copyright (C) 2016 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef ENCODINGSPECULATOR_H #define ENCODINGSPECULATOR_H #include // The encoder speculator tries to determine the likely encoding // of the stream of bytes which is passed to it. class EncodingSpeculator { public: enum class Encoding { ASCII7, ASCII8, UTF8 }; EncodingSpeculator() : state_( State::ASCIIOnly ) {} // Inject one byte into the speculator void inject_byte( uint8_t byte ); // Returns the current guess based on the previously injected bytes Encoding guess() const; private: enum class State { ASCIIOnly, Unknown8Bit, UTF8LeadingByteSeen, ValidUTF8, }; State state_; uint32_t code_point_; int continuation_left_; uint32_t min_value_; }; #endif glogg-1.1.4/src/qfnotifications.h0000644000175000017500000000405013076461374015756 0ustar nickonicko/* * Copyright (C) 2013 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef QFNOTIFICATIONS_H #define QFNOTIFICATIONS_H #include #include #include // Notifications sent by the QF for displaying to the user // and their translation in UI text. class QFNotification { public: virtual QString message() const = 0; // Max width of the message (in pixels) static int maxWidth( const QWidget* widget ) { QFontMetrics fm = widget->fontMetrics(); return qMax( fm.size( Qt::TextSingleLine, REACHED_BOF ).width(), fm.size( Qt::TextSingleLine, REACHED_EOF ).width() ); } protected: static const QString REACHED_EOF; static const QString REACHED_BOF; }; class QFNotificationReachedEndOfFile : public QFNotification { QString message() const { return REACHED_EOF; } }; class QFNotificationReachedBegininningOfFile : public QFNotification { QString message() const { return REACHED_BOF; } }; class QFNotificationProgress : public QFNotification { public: // Constructor taking the progress (in percent) QFNotificationProgress( int progress_percent ) { progressPercent_ = progress_percent; } QString message() const { return QString( QObject::tr("Searching (position %1 %)") .arg( progressPercent_ ) ); } private: int progressPercent_; }; #endif glogg-1.1.4/src/mainwindow.cpp0000644000175000017500000010171513076461374015273 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2011, 2013, 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ // This file implements MainWindow. It is responsible for creating and // managing the menus, the toolbar, and the CrawlerWidget. It also // load/save the settings on opening/closing of the app #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "log.h" #include "mainwindow.h" #include "sessioninfo.h" #include "recentfiles.h" #include "crawlerwidget.h" #include "filtersdialog.h" #include "optionsdialog.h" #include "persistentinfo.h" #include "menuactiontooltipbehavior.h" #include "tabbedcrawlerwidget.h" #include "externalcom.h" // Returns the size in human readable format static QString readableSize( qint64 size ); MainWindow::MainWindow( std::unique_ptr session, std::shared_ptr external_communicator ) : session_( std::move( session ) ), externalCommunicator_( external_communicator ), recentFiles_( Persistent( "recentFiles" ) ), mainIcon_(), signalMux_(), quickFindMux_( session_->getQuickFindPattern() ), mainTabWidget_() #ifdef GLOGG_SUPPORTS_VERSION_CHECKING ,versionChecker_() #endif { createActions(); createMenus(); createToolBars(); // createStatusBar(); setAcceptDrops( true ); // Default geometry const QRect geometry = QApplication::desktop()->availableGeometry( this ); setGeometry( geometry.x() + 20, geometry.y() + 40, geometry.width() - 140, geometry.height() - 140 ); mainIcon_.addFile( ":/images/hicolor/16x16/glogg.png" ); mainIcon_.addFile( ":/images/hicolor/24x24/glogg.png" ); mainIcon_.addFile( ":/images/hicolor/32x32/glogg.png" ); mainIcon_.addFile( ":/images/hicolor/48x48/glogg.png" ); setWindowIcon( mainIcon_ ); readSettings(); // Connect the signals to the mux (they will be forwarded to the // "current" crawlerwidget // Send actions to the crawlerwidget signalMux_.connect( this, SIGNAL( followSet( bool ) ), SIGNAL( followSet( bool ) ) ); signalMux_.connect( this, SIGNAL( optionsChanged() ), SLOT( applyConfiguration() ) ); signalMux_.connect( this, SIGNAL( enteringQuickFind() ), SLOT( enteringQuickFind() ) ); signalMux_.connect( &quickFindWidget_, SIGNAL( close() ), SLOT( exitingQuickFind() ) ); // Actions from the CrawlerWidget signalMux_.connect( SIGNAL( followModeChanged( bool ) ), this, SLOT( changeFollowMode( bool ) ) ); signalMux_.connect( SIGNAL( updateLineNumber( int ) ), this, SLOT( lineNumberHandler( int ) ) ); // Register for progress status bar signalMux_.connect( SIGNAL( loadingProgressed( int ) ), this, SLOT( updateLoadingProgress( int ) ) ); signalMux_.connect( SIGNAL( loadingFinished( LoadingStatus ) ), this, SLOT( handleLoadingFinished( LoadingStatus ) ) ); // Register for checkbox changes signalMux_.connect( SIGNAL( searchRefreshChanged( int ) ), this, SLOT( handleSearchRefreshChanged( int ) ) ); signalMux_.connect( SIGNAL( ignoreCaseChanged( int ) ), this, SLOT( handleIgnoreCaseChanged( int ) ) ); // Configure the main tabbed widget mainTabWidget_.setDocumentMode( true ); mainTabWidget_.setMovable( true ); //mainTabWidget_.setTabShape( QTabWidget::Triangular ); mainTabWidget_.setTabsClosable( true ); connect( &mainTabWidget_, SIGNAL( tabCloseRequested( int ) ), this, SLOT( closeTab( int ) ) ); connect( &mainTabWidget_, SIGNAL( currentChanged( int ) ), this, SLOT( currentTabChanged( int ) ) ); // Establish the QuickFindWidget and mux ( to send requests from the // QFWidget to the right window ) connect( &quickFindWidget_, SIGNAL( patternConfirmed( const QString&, bool ) ), &quickFindMux_, SLOT( confirmPattern( const QString&, bool ) ) ); connect( &quickFindWidget_, SIGNAL( patternUpdated( const QString&, bool ) ), &quickFindMux_, SLOT( setNewPattern( const QString&, bool ) ) ); connect( &quickFindWidget_, SIGNAL( cancelSearch() ), &quickFindMux_, SLOT( cancelSearch() ) ); connect( &quickFindWidget_, SIGNAL( searchForward() ), &quickFindMux_, SLOT( searchForward() ) ); connect( &quickFindWidget_, SIGNAL( searchBackward() ), &quickFindMux_, SLOT( searchBackward() ) ); connect( &quickFindWidget_, SIGNAL( searchNext() ), &quickFindMux_, SLOT( searchNext() ) ); // QuickFind changes coming from the views connect( &quickFindMux_, SIGNAL( patternChanged( const QString& ) ), this, SLOT( changeQFPattern( const QString& ) ) ); connect( &quickFindMux_, SIGNAL( notify( const QFNotification& ) ), &quickFindWidget_, SLOT( notify( const QFNotification& ) ) ); connect( &quickFindMux_, SIGNAL( clearNotification() ), &quickFindWidget_, SLOT( clearNotification() ) ); // Actions from external instances connect( externalCommunicator_.get(), SIGNAL( loadFile( const QString& ) ), this, SLOT( loadFileNonInteractive( const QString& ) ) ); #ifdef GLOGG_SUPPORTS_VERSION_CHECKING // Version checker notification connect( &versionChecker_, SIGNAL( newVersionFound( const QString& ) ), this, SLOT( newVersionNotification( const QString& ) ) ); #endif // Construct the QuickFind bar quickFindWidget_.hide(); QWidget* central_widget = new QWidget(); QVBoxLayout* main_layout = new QVBoxLayout(); main_layout->setContentsMargins( 0, 0, 0, 0 ); main_layout->addWidget( &mainTabWidget_ ); main_layout->addWidget( &quickFindWidget_ ); central_widget->setLayout( main_layout ); setCentralWidget( central_widget ); } void MainWindow::reloadGeometry() { QByteArray geometry; session_->storedGeometry( &geometry ); restoreGeometry( geometry ); } void MainWindow::reloadSession() { int current_file_index = -1; for ( auto open_file: session_->restore( []() { return new CrawlerWidget(); }, ¤t_file_index ) ) { QString file_name = { open_file.first.c_str() }; CrawlerWidget* crawler_widget = dynamic_cast( open_file.second ); assert( crawler_widget ); mainTabWidget_.addTab( crawler_widget, strippedName( file_name ) ); } if ( current_file_index >= 0 ) mainTabWidget_.setCurrentIndex( current_file_index ); } void MainWindow::loadInitialFile( QString fileName ) { LOG(logDEBUG) << "loadInitialFile"; // Is there a file passed as argument? if ( !fileName.isEmpty() ) loadFile( fileName ); } void MainWindow::startBackgroundTasks() { LOG(logDEBUG) << "startBackgroundTasks"; #ifdef GLOGG_SUPPORTS_VERSION_CHECKING versionChecker_.startCheck(); #endif } // // Private functions // const MainWindow::EncodingList MainWindow::encoding_list[] = { { "&Auto" }, { "ASCII / &ISO-8859-1" }, { "&UTF-8" }, }; // Menu actions void MainWindow::createActions() { std::shared_ptr config = Persistent( "settings" ); openAction = new QAction(tr("&Open..."), this); openAction->setShortcut(QKeySequence::Open); openAction->setIcon( QIcon( ":/images/open14.png" ) ); openAction->setStatusTip(tr("Open a file")); connect(openAction, SIGNAL(triggered()), this, SLOT(open())); closeAction = new QAction(tr("&Close"), this); closeAction->setShortcut(tr("Ctrl+W")); closeAction->setStatusTip(tr("Close document")); connect(closeAction, SIGNAL(triggered()), this, SLOT(closeTab())); closeAllAction = new QAction(tr("Close &All"), this); closeAllAction->setStatusTip(tr("Close all documents")); connect(closeAllAction, SIGNAL(triggered()), this, SLOT(closeAll())); // Recent files for (int i = 0; i < MaxRecentFiles; ++i) { recentFileActions[i] = new QAction(this); recentFileActions[i]->setVisible(false); connect(recentFileActions[i], SIGNAL(triggered()), this, SLOT(openRecentFile())); } exitAction = new QAction(tr("E&xit"), this); exitAction->setShortcut(tr("Ctrl+Q")); exitAction->setStatusTip(tr("Exit the application")); connect( exitAction, SIGNAL(triggered()), this, SLOT(close()) ); copyAction = new QAction(tr("&Copy"), this); copyAction->setShortcut(QKeySequence::Copy); copyAction->setStatusTip(tr("Copy the selection")); connect( copyAction, SIGNAL(triggered()), this, SLOT(copy()) ); selectAllAction = new QAction(tr("Select &All"), this); selectAllAction->setShortcut(tr("Ctrl+A")); selectAllAction->setStatusTip(tr("Select all the text")); connect( selectAllAction, SIGNAL(triggered()), this, SLOT( selectAll() ) ); findAction = new QAction(tr("&Find..."), this); findAction->setShortcut(QKeySequence::Find); findAction->setStatusTip(tr("Find the text")); connect( findAction, SIGNAL(triggered()), this, SLOT( find() ) ); overviewVisibleAction = new QAction( tr("Matches &overview"), this ); overviewVisibleAction->setCheckable( true ); overviewVisibleAction->setChecked( config->isOverviewVisible() ); connect( overviewVisibleAction, SIGNAL( toggled( bool ) ), this, SLOT( toggleOverviewVisibility( bool )) ); lineNumbersVisibleInMainAction = new QAction( tr("Line &numbers in main view"), this ); lineNumbersVisibleInMainAction->setCheckable( true ); lineNumbersVisibleInMainAction->setChecked( config->mainLineNumbersVisible() ); connect( lineNumbersVisibleInMainAction, SIGNAL( toggled( bool ) ), this, SLOT( toggleMainLineNumbersVisibility( bool )) ); lineNumbersVisibleInFilteredAction = new QAction( tr("Line &numbers in filtered view"), this ); lineNumbersVisibleInFilteredAction->setCheckable( true ); lineNumbersVisibleInFilteredAction->setChecked( config->filteredLineNumbersVisible() ); connect( lineNumbersVisibleInFilteredAction, SIGNAL( toggled( bool ) ), this, SLOT( toggleFilteredLineNumbersVisibility( bool )) ); followAction = new QAction( tr("&Follow File"), this ); followAction->setShortcut(Qt::Key_F); followAction->setCheckable(true); connect( followAction, SIGNAL(toggled( bool )), this, SIGNAL(followSet( bool )) ); reloadAction = new QAction( tr("&Reload"), this ); reloadAction->setShortcut(QKeySequence::Refresh); reloadAction->setIcon( QIcon(":/images/reload14.png") ); signalMux_.connect( reloadAction, SIGNAL(triggered()), SLOT(reload()) ); stopAction = new QAction( tr("&Stop"), this ); stopAction->setIcon( QIcon(":/images/stop14.png") ); stopAction->setEnabled( true ); signalMux_.connect( stopAction, SIGNAL(triggered()), SLOT(stopLoading()) ); filtersAction = new QAction(tr("&Filters..."), this); filtersAction->setStatusTip(tr("Show the Filters box")); connect( filtersAction, SIGNAL(triggered()), this, SLOT(filters()) ); optionsAction = new QAction(tr("&Options..."), this); optionsAction->setStatusTip(tr("Show the Options box")); connect( optionsAction, SIGNAL(triggered()), this, SLOT(options()) ); aboutAction = new QAction(tr("&About"), this); aboutAction->setStatusTip(tr("Show the About box")); connect( aboutAction, SIGNAL(triggered()), this, SLOT(about()) ); aboutQtAction = new QAction(tr("About &Qt"), this); aboutQtAction->setStatusTip(tr("Show the Qt library's About box")); connect( aboutQtAction, SIGNAL(triggered()), this, SLOT(aboutQt()) ); encodingGroup = new QActionGroup( this ); for ( int i = 0; i < CrawlerWidget::ENCODING_MAX; ++i ) { encodingAction[i] = new QAction( tr( encoding_list[i].name ), this ); encodingAction[i]->setCheckable( true ); encodingGroup->addAction( encodingAction[i] ); } encodingAction[0]->setStatusTip(tr("Automatically detect the file's encoding")); encodingAction[0]->setChecked( true ); connect( encodingGroup, SIGNAL( triggered( QAction* ) ), this, SLOT( encodingChanged( QAction* ) ) ); } void MainWindow::createMenus() { fileMenu = menuBar()->addMenu( tr("&File") ); fileMenu->addAction( openAction ); fileMenu->addAction( closeAction ); fileMenu->addAction( closeAllAction ); fileMenu->addSeparator(); for (int i = 0; i < MaxRecentFiles; ++i) { fileMenu->addAction( recentFileActions[i] ); recentFileActionBehaviors[i] = new MenuActionToolTipBehavior(recentFileActions[i], fileMenu, this); } fileMenu->addSeparator(); fileMenu->addAction( exitAction ); editMenu = menuBar()->addMenu( tr("&Edit") ); editMenu->addAction( copyAction ); editMenu->addAction( selectAllAction ); editMenu->addSeparator(); editMenu->addAction( findAction ); viewMenu = menuBar()->addMenu( tr("&View") ); viewMenu->addAction( overviewVisibleAction ); viewMenu->addSeparator(); viewMenu->addAction( lineNumbersVisibleInMainAction ); viewMenu->addAction( lineNumbersVisibleInFilteredAction ); viewMenu->addSeparator(); viewMenu->addAction( followAction ); viewMenu->addSeparator(); viewMenu->addAction( reloadAction ); toolsMenu = menuBar()->addMenu( tr("&Tools") ); toolsMenu->addAction( filtersAction ); toolsMenu->addSeparator(); toolsMenu->addAction( optionsAction ); encodingMenu = menuBar()->addMenu( tr("En&coding") ); encodingMenu->addAction( encodingAction[0] ); encodingMenu->addSeparator(); for ( int i = 1; i < CrawlerWidget::ENCODING_MAX; ++i ) { encodingMenu->addAction( encodingAction[i] ); } menuBar()->addSeparator(); helpMenu = menuBar()->addMenu( tr("&Help") ); helpMenu->addAction( aboutAction ); } void MainWindow::createToolBars() { infoLine = new InfoLine(); infoLine->setFrameStyle( QFrame::WinPanel | QFrame::Sunken ); infoLine->setLineWidth( 0 ); lineNbField = new QLabel( ); lineNbField->setText( "Line 0" ); lineNbField->setAlignment( Qt::AlignLeft | Qt::AlignVCenter ); lineNbField->setMinimumSize( lineNbField->fontMetrics().size( 0, "Line 0000000") ); toolBar = addToolBar( tr("&Toolbar") ); toolBar->setIconSize( QSize( 14, 14 ) ); toolBar->setMovable( false ); toolBar->addAction( openAction ); toolBar->addAction( reloadAction ); toolBar->addWidget( infoLine ); toolBar->addAction( stopAction ); toolBar->addWidget( lineNbField ); } // // Slots // // Opens the file selection dialog to select a new log file void MainWindow::open() { QString defaultDir = "."; // Default to the path of the current file if there is one if ( auto current = currentCrawlerWidget() ) { std::string current_file = session_->getFilename( current ); QFileInfo fileInfo = QFileInfo( QString( current_file.c_str() ) ); defaultDir = fileInfo.path(); } QString fileName = QFileDialog::getOpenFileName(this, tr("Open file"), defaultDir, tr("All files (*)")); if (!fileName.isEmpty()) loadFile(fileName); } // Opens a log file from the recent files list void MainWindow::openRecentFile() { QAction* action = qobject_cast(sender()); if (action) loadFile(action->data().toString()); } // Close current tab void MainWindow::closeTab() { int currentIndex = mainTabWidget_.currentIndex(); if ( currentIndex >= 0 ) { closeTab(currentIndex); } } // Close all tabs void MainWindow::closeAll() { while ( mainTabWidget_.count() ) { closeTab(0); } } // Select all the text in the currently selected view void MainWindow::selectAll() { CrawlerWidget* current = currentCrawlerWidget(); if ( current ) current->selectAll(); } // Copy the currently selected line into the clipboard void MainWindow::copy() { static QClipboard* clipboard = QApplication::clipboard(); CrawlerWidget* current = currentCrawlerWidget(); if ( current ) { clipboard->setText( current->getSelectedText() ); // Put it in the global selection as well (X11 only) clipboard->setText( current->getSelectedText(), QClipboard::Selection ); } } // Display the QuickFind bar void MainWindow::find() { displayQuickFindBar( QuickFindMux::Forward ); } // Opens the 'Filters' dialog box void MainWindow::filters() { FiltersDialog dialog(this); signalMux_.connect(&dialog, SIGNAL( optionsChanged() ), SLOT( applyConfiguration() )); dialog.exec(); signalMux_.disconnect(&dialog, SIGNAL( optionsChanged() ), SLOT( applyConfiguration() )); } // Opens the 'Options' modal dialog box void MainWindow::options() { OptionsDialog dialog(this); signalMux_.connect(&dialog, SIGNAL( optionsChanged() ), SLOT( applyConfiguration() )); dialog.exec(); signalMux_.disconnect(&dialog, SIGNAL( optionsChanged() ), SLOT( applyConfiguration() )); } // Opens the 'About' dialog box. void MainWindow::about() { QMessageBox::about(this, tr("About glogg"), tr("

glogg " GLOGG_VERSION "

" "

A fast, advanced log explorer." #ifdef GLOGG_COMMIT "

Built " GLOGG_DATE " from " GLOGG_COMMIT #endif "

http://glogg.bonnefon.org/

" "

Copyright © 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Nicolas Bonnefon and other contributors" "

You may modify and redistribute the program under the terms of the GPL (version 3 or later)." ) ); } // Opens the 'About Qt' dialog box. void MainWindow::aboutQt() { } void MainWindow::encodingChanged( QAction* action ) { int i = 0; for ( i = 0; i < CrawlerWidget::ENCODING_MAX; ++i ) if ( action == encodingAction[i] ) break; LOG(logDEBUG) << "encodingChanged, encoding " << i; currentCrawlerWidget()->setEncoding( static_cast( i ) ); updateInfoLine(); } void MainWindow::toggleOverviewVisibility( bool isVisible ) { std::shared_ptr config = Persistent( "settings" ); config->setOverviewVisible( isVisible ); emit optionsChanged(); } void MainWindow::toggleMainLineNumbersVisibility( bool isVisible ) { std::shared_ptr config = Persistent( "settings" ); config->setMainLineNumbersVisible( isVisible ); emit optionsChanged(); } void MainWindow::toggleFilteredLineNumbersVisibility( bool isVisible ) { std::shared_ptr config = Persistent( "settings" ); config->setFilteredLineNumbersVisible( isVisible ); emit optionsChanged(); } void MainWindow::changeFollowMode( bool follow ) { followAction->setChecked( follow ); } void MainWindow::lineNumberHandler( int line ) { // The line number received is the internal (starts at 0) lineNbField->setText( tr( "Line %1" ).arg( line + 1 ) ); } void MainWindow::updateLoadingProgress( int progress ) { LOG(logDEBUG) << "Loading progress: " << progress; QString current_file = session_->getFilename( currentCrawlerWidget() ).c_str(); // We ignore 0% and 100% to avoid a flash when the file (or update) // is very short. if ( progress > 0 && progress < 100 ) { infoLine->setText( current_file + tr( " - Indexing lines... (%1 %)" ).arg( progress ) ); infoLine->displayGauge( progress ); stopAction->setEnabled( true ); reloadAction->setEnabled( false ); } } void MainWindow::handleLoadingFinished( LoadingStatus status ) { LOG(logDEBUG) << "handleLoadingFinished success=" << ( status == LoadingStatus::Successful ); // No file is loading loadingFileName.clear(); if ( status == LoadingStatus::Successful ) { updateInfoLine(); infoLine->hideGauge(); stopAction->setEnabled( false ); reloadAction->setEnabled( true ); // Now everything is ready, we can finally show the file! currentCrawlerWidget()->show(); } else { if ( status == LoadingStatus::NoMemory ) { QMessageBox alertBox; alertBox.setText( "Not enough memory." ); alertBox.setInformativeText( "The system does not have enough \ memory to hold the index for this file. The file will now be closed." ); alertBox.setIcon( QMessageBox::Critical ); alertBox.exec(); } closeTab( mainTabWidget_.currentIndex() ); } // mainTabWidget_.setEnabled( true ); } void MainWindow::handleSearchRefreshChanged( int state ) { auto config = Persistent( "settings" ); config->setSearchAutoRefreshDefault( state == Qt::Checked ); } void MainWindow::handleIgnoreCaseChanged( int state ) { auto config = Persistent( "settings" ); config->setSearchIgnoreCaseDefault( state == Qt::Checked ); } void MainWindow::closeTab( int index ) { auto widget = dynamic_cast( mainTabWidget_.widget( index ) ); assert( widget ); widget->stopLoading(); mainTabWidget_.removeTab( index ); session_->close( widget ); delete widget; } void MainWindow::currentTabChanged( int index ) { LOG(logDEBUG) << "currentTabChanged"; if ( index >= 0 ) { CrawlerWidget* crawler_widget = dynamic_cast( mainTabWidget_.widget( index ) ); signalMux_.setCurrentDocument( crawler_widget ); quickFindMux_.registerSelector( crawler_widget ); // New tab is set up with fonts etc... emit optionsChanged(); // Update the menu bar updateMenuBarFromDocument( crawler_widget ); // Update the title bar updateTitleBar( QString( session_->getFilename( crawler_widget ).c_str() ) ); } else { // No tab left signalMux_.setCurrentDocument( nullptr ); quickFindMux_.registerSelector( nullptr ); infoLine->hideGauge(); infoLine->clear(); updateTitleBar( QString() ); } } void MainWindow::changeQFPattern( const QString& newPattern ) { quickFindWidget_.changeDisplayedPattern( newPattern ); } void MainWindow::loadFileNonInteractive( const QString& file_name ) { LOG(logDEBUG) << "loadFileNonInteractive( " << file_name.toStdString() << " )"; loadFile( file_name ); // Try to get the window to the front // This is a bit of a hack but has been tested on: // Qt 5.3 / Gnome / Linux // Qt 4.8 / Win7 #ifdef _WIN32 // Hack copied from http://qt-project.org/forums/viewthread/6164 ::SetWindowPos((HWND) effectiveWinId(), HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); ::SetWindowPos((HWND) effectiveWinId(), HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); #else Qt::WindowFlags window_flags = windowFlags(); window_flags |= Qt::WindowStaysOnTopHint; setWindowFlags( window_flags ); #endif activateWindow(); raise(); #ifndef _WIN32 window_flags = windowFlags(); window_flags &= ~Qt::WindowStaysOnTopHint; setWindowFlags( window_flags ); #endif showNormal(); } void MainWindow::newVersionNotification( const QString& new_version ) { LOG(logDEBUG) << "newVersionNotification( " << new_version.toStdString() << " )"; QMessageBox msgBox; msgBox.setText( QString( "A new version of glogg (%1) is available for download

" "http://glogg.bonnefon.org/download.html" ).arg( new_version ) ); msgBox.exec(); } // // Events // // Closes the application void MainWindow::closeEvent( QCloseEvent *event ) { writeSettings(); event->accept(); } // Accepts the drag event if it looks like a filename void MainWindow::dragEnterEvent( QDragEnterEvent* event ) { if ( event->mimeData()->hasFormat( "text/uri-list" ) ) event->acceptProposedAction(); } // Tries and loads the file if the URL dropped is local void MainWindow::dropEvent( QDropEvent* event ) { QList urls = event->mimeData()->urls(); if ( urls.isEmpty() ) return; QString fileName = urls.first().toLocalFile(); if ( fileName.isEmpty() ) return; loadFile( fileName ); } void MainWindow::keyPressEvent( QKeyEvent* keyEvent ) { LOG(logDEBUG4) << "keyPressEvent received"; switch ( (keyEvent->text())[0].toLatin1() ) { case '/': displayQuickFindBar( QuickFindMux::Forward ); break; case '?': displayQuickFindBar( QuickFindMux::Backward ); break; default: keyEvent->ignore(); } if ( !keyEvent->isAccepted() ) QMainWindow::keyPressEvent( keyEvent ); } // // Private functions // // Create a CrawlerWidget for the passed file, start its loading // and update the title bar. // The loading is done asynchronously. bool MainWindow::loadFile( const QString& fileName ) { LOG(logDEBUG) << "loadFile ( " << fileName.toStdString() << " )"; // First check if the file is already open... CrawlerWidget* existing_crawler = dynamic_cast( session_->getViewIfOpen( fileName.toStdString() ) ); if ( existing_crawler ) { // ... and switch to it. mainTabWidget_.setCurrentWidget( existing_crawler ); return true; } // Load the file loadingFileName = fileName; try { CrawlerWidget* crawler_widget = dynamic_cast( session_->open( fileName.toStdString(), []() { return new CrawlerWidget(); } ) ); assert( crawler_widget ); // We won't show the widget until the file is fully loaded crawler_widget->hide(); // We disable the tab widget to avoid having someone switch // tab during loading. (maybe FIXME) //mainTabWidget_.setEnabled( false ); int index = mainTabWidget_.addTab( crawler_widget, strippedName( fileName ) ); // Setting the new tab, the user will see a blank page for the duration // of the loading, with no way to switch to another tab mainTabWidget_.setCurrentIndex( index ); // Update the recent files list // (reload the list first in case another glogg changed it) GetPersistentInfo().retrieve( "recentFiles" ); recentFiles_->addRecent( fileName ); GetPersistentInfo().save( "recentFiles" ); updateRecentFileActions(); } catch ( FileUnreadableErr ) { LOG(logDEBUG) << "Can't open file " << fileName.toStdString(); return false; } LOG(logDEBUG) << "Success loading file " << fileName.toStdString(); return true; } // Strips the passed filename from its directory part. QString MainWindow::strippedName( const QString& fullFileName ) const { return QFileInfo( fullFileName ).fileName(); } // Return the currently active CrawlerWidget, or NULL if none CrawlerWidget* MainWindow::currentCrawlerWidget() const { auto current = dynamic_cast( mainTabWidget_.currentWidget() ); return current; } // Update the title bar. void MainWindow::updateTitleBar( const QString& file_name ) { QString shownName = tr( "Untitled" ); if ( !file_name.isEmpty() ) shownName = strippedName( file_name ); setWindowTitle( tr("%1 - %2").arg(shownName).arg(tr("glogg")) #ifdef GLOGG_COMMIT + " (dev build " GLOGG_VERSION ")" #endif ); } // Updates the actions for the recent files. // Must be called after having added a new name to the list. void MainWindow::updateRecentFileActions() { QStringList recent_files = recentFiles_->recentFiles(); for ( int j = 0; j < MaxRecentFiles; ++j ) { if ( j < recent_files.count() ) { QString text = tr("&%1 %2").arg(j + 1).arg(strippedName(recent_files[j])); recentFileActions[j]->setText( text ); recentFileActions[j]->setToolTip( recent_files[j] ); recentFileActions[j]->setData( recent_files[j] ); recentFileActions[j]->setVisible( true ); } else { recentFileActions[j]->setVisible( false ); } } // separatorAction->setVisible(!recentFiles.isEmpty()); } // Update our menu bar to match the settings of the crawler // (used when the tab is changed) void MainWindow::updateMenuBarFromDocument( const CrawlerWidget* crawler ) { auto encoding = crawler->encodingSetting(); encodingAction[static_cast( encoding )]->setChecked( true ); bool follow = crawler->isFollowEnabled(); followAction->setChecked( follow ); } // Update the top info line from the session void MainWindow::updateInfoLine() { QLocale defaultLocale; // Following should always work as we will only receive enter // this slot if there is a crawler connected. QString current_file = session_->getFilename( currentCrawlerWidget() ).c_str(); uint64_t fileSize; uint32_t fileNbLine; QDateTime lastModified; session_->getFileInfo( currentCrawlerWidget(), &fileSize, &fileNbLine, &lastModified ); if ( lastModified.isValid() ) { const QString date = defaultLocale.toString( lastModified, QLocale::NarrowFormat ); infoLine->setText( tr( "%1 (%2 - %3 lines - modified on %4 - %5)" ) .arg(current_file).arg(readableSize(fileSize)) .arg(fileNbLine).arg( date ) .arg(currentCrawlerWidget()->encodingText()) ); } else { infoLine->setText( tr( "%1 (%2 - %3 lines - %4)" ) .arg(current_file).arg(readableSize(fileSize)) .arg(fileNbLine) .arg(currentCrawlerWidget()->encodingText()) ); } } // Write settings to permanent storage void MainWindow::writeSettings() { // Save the session // Generate the ordered list of widgets and their topLine std::vector< std::tuple> > widget_list; for ( int i = 0; i < mainTabWidget_.count(); ++i ) { auto view = dynamic_cast( mainTabWidget_.widget( i ) ); widget_list.push_back( std::make_tuple( view, 0UL, view->context() ) ); } session_->save( widget_list, saveGeometry() ); // User settings GetPersistentInfo().save( QString( "settings" ) ); } // Read settings from permanent storage void MainWindow::readSettings() { // Get and restore the session // GetPersistentInfo().retrieve( QString( "session" ) ); // SessionInfo session = Persistent( "session" ); /* * FIXME: should be in the session crawlerWidget->restoreState( session.crawlerState() ); */ // History of recent files GetPersistentInfo().retrieve( QString( "recentFiles" ) ); updateRecentFileActions(); // GetPersistentInfo().retrieve( QString( "settings" ) ); GetPersistentInfo().retrieve( QString( "filterSet" ) ); } void MainWindow::displayQuickFindBar( QuickFindMux::QFDirection direction ) { LOG(logDEBUG) << "MainWindow::displayQuickFindBar"; // Warn crawlers so they can save the position of the focus in order // to do incremental search in the right view. emit enteringQuickFind(); quickFindMux_.setDirection( direction ); quickFindWidget_.userActivate(); } // Returns the size in human readable format static QString readableSize( qint64 size ) { static const QString sizeStrs[] = { QObject::tr("B"), QObject::tr("KiB"), QObject::tr("MiB"), QObject::tr("GiB"), QObject::tr("TiB") }; QLocale defaultLocale; unsigned int i; double humanSize = size; for ( i=0; i+1 < (sizeof(sizeStrs)/sizeof(QString)) && (humanSize/1024.0) >= 1024.0; i++ ) humanSize /= 1024.0; if ( humanSize >= 1024.0 ) { humanSize /= 1024.0; i++; } QString output; if ( i == 0 ) // No decimal part if we display straight bytes. output = defaultLocale.toString( (int) humanSize ); else output = defaultLocale.toString( humanSize, 'f', 1 ); output += QString(" ") + sizeStrs[i]; return output; } glogg-1.1.4/src/crawlerwidget.cpp0000644000175000017500000010176413076461374015766 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ // This file implements the CrawlerWidget class. // It is responsible for creating and managing the two views and all // the UI elements. It implements the connection between the UI elements. // It also interacts with the sets of data (full and filtered). #include "log.h" #include #include #include #include #include #include #include #include #include #include "crawlerwidget.h" #include "quickfindpattern.h" #include "overview.h" #include "infoline.h" #include "savedsearches.h" #include "quickfindwidget.h" #include "persistentinfo.h" #include "configuration.h" // Palette for error signaling (yellow background) const QPalette CrawlerWidget::errorPalette( QColor( "yellow" ) ); // Implementation of the view context for the CrawlerWidget class CrawlerWidgetContext : public ViewContextInterface { public: // Construct from the stored string representation CrawlerWidgetContext( const char* string ); // Construct from the value passsed CrawlerWidgetContext( QList sizes, bool ignore_case, bool auto_refresh ) : sizes_( sizes ), ignore_case_( ignore_case ), auto_refresh_( auto_refresh ) {} // Implementation of the ViewContextInterface function std::string toString() const; // Access the Qt sizes array for the QSplitter QList sizes() const { return sizes_; } bool ignoreCase() const { return ignore_case_; } bool autoRefresh() const { return auto_refresh_; } private: QList sizes_; bool ignore_case_; bool auto_refresh_; }; // Constructor only does trivial construction. The real work is done once // the data is attached. CrawlerWidget::CrawlerWidget( QWidget *parent ) : QSplitter( parent ), overview_() { logData_ = nullptr; logFilteredData_ = nullptr; quickFindPattern_ = nullptr; savedSearches_ = nullptr; qfSavedFocus_ = nullptr; // Until we have received confirmation loading is finished, we // should consider we are loading something. loadingInProgress_ = true; // and it's the first time firstLoadDone_ = false; nbMatches_ = 0; dataStatus_ = DataStatus::OLD_DATA; currentLineNumber_ = 0; } // The top line is first one on the main display int CrawlerWidget::getTopLine() const { return logMainView->getTopLine(); } QString CrawlerWidget::getSelectedText() const { if ( filteredView->hasFocus() ) return filteredView->getSelection(); else return logMainView->getSelection(); } void CrawlerWidget::selectAll() { activeView()->selectAll(); } CrawlerWidget::Encoding CrawlerWidget::encodingSetting() const { return encodingSetting_; } bool CrawlerWidget::isFollowEnabled() const { return logMainView->isFollowEnabled(); } QString CrawlerWidget::encodingText() const { return encoding_text_; } // Return a pointer to the view in which we should do the QuickFind SearchableWidgetInterface* CrawlerWidget::doGetActiveSearchable() const { return activeView(); } // Return all the searchable widgets (views) std::vector CrawlerWidget::doGetAllSearchables() const { std::vector searchables = { logMainView, filteredView }; return searchables; } // Update the state of the parent void CrawlerWidget::doSendAllStateSignals() { emit updateLineNumber( currentLineNumber_ ); if ( !loadingInProgress_ ) emit loadingFinished( LoadingStatus::Successful ); } // // Public slots // void CrawlerWidget::stopLoading() { logFilteredData_->interruptSearch(); logData_->interruptLoading(); } void CrawlerWidget::reload() { searchState_.resetState(); logFilteredData_->clearSearch(); filteredView->updateData(); printSearchInfoMessage(); logData_->reload(); // A reload is considered as a first load, // this is to prevent the "new data" icon to be triggered. firstLoadDone_ = false; } void CrawlerWidget::setEncoding( Encoding encoding ) { encodingSetting_ = encoding; updateEncoding(); update(); } // // Protected functions // void CrawlerWidget::doSetData( std::shared_ptr log_data, std::shared_ptr filtered_data ) { logData_ = log_data.get(); logFilteredData_ = filtered_data.get(); } void CrawlerWidget::doSetQuickFindPattern( std::shared_ptr qfp ) { quickFindPattern_ = qfp; } void CrawlerWidget::doSetSavedSearches( std::shared_ptr saved_searches ) { savedSearches_ = saved_searches; // We do setup now, assuming doSetData has been called before // us, that's not great really... setup(); } void CrawlerWidget::doSetViewContext( const char* view_context ) { LOG(logDEBUG) << "CrawlerWidget::doSetViewContext: " << view_context; CrawlerWidgetContext context = { view_context }; setSizes( context.sizes() ); ignoreCaseCheck->setCheckState( context.ignoreCase() ? Qt::Checked : Qt::Unchecked ); auto auto_refresh_check_state = context.autoRefresh() ? Qt::Checked : Qt::Unchecked; searchRefreshCheck->setCheckState( auto_refresh_check_state ); // Manually call the handler as it is not called when changing the state programmatically searchRefreshChangedHandler( auto_refresh_check_state ); } std::shared_ptr CrawlerWidget::doGetViewContext() const { auto context = std::make_shared( sizes(), ( ignoreCaseCheck->checkState() == Qt::Checked ), ( searchRefreshCheck->checkState() == Qt::Checked ) ); return static_cast>( context ); } // // Slots // void CrawlerWidget::startNewSearch() { // Record the search line in the recent list // (reload the list first in case another glogg changed it) GetPersistentInfo().retrieve( "savedSearches" ); savedSearches_->addRecent( searchLineEdit->currentText() ); GetPersistentInfo().save( "savedSearches" ); // Update the SearchLine (history) updateSearchCombo(); // Call the private function to do the search replaceCurrentSearch( searchLineEdit->currentText() ); } void CrawlerWidget::stopSearch() { logFilteredData_->interruptSearch(); searchState_.stopSearch(); printSearchInfoMessage(); } // When receiving the 'newDataAvailable' signal from LogFilteredData void CrawlerWidget::updateFilteredView( int nbMatches, int progress ) { LOG(logDEBUG) << "updateFilteredView received."; if ( progress == 100 ) { // Searching done printSearchInfoMessage( nbMatches ); searchInfoLine->hideGauge(); // De-activate the stop button stopButton->setEnabled( false ); } else { // Search in progress // We ignore 0% and 100% to avoid a flash when the search is very short if ( progress > 0 ) { searchInfoLine->setText( tr("Search in progress (%1 %)... %2 match%3 found so far.") .arg( progress ) .arg( nbMatches ) .arg( nbMatches > 1 ? "es" : "" ) ); searchInfoLine->displayGauge( progress ); } } // If more (or less, e.g. come back to 0) matches have been found if ( nbMatches != nbMatches_ ) { nbMatches_ = nbMatches; // Recompute the content of the filtered window. filteredView->updateData(); // Update the match overview overview_.updateData( logData_->getNbLine() ); // New data found icon changeDataStatus( DataStatus::NEW_FILTERED_DATA ); // Also update the top window for the coloured bullets. update(); } } void CrawlerWidget::jumpToMatchingLine(int filteredLineNb) { int mainViewLine = logFilteredData_->getMatchingLineNumber(filteredLineNb); logMainView->selectAndDisplayLine(mainViewLine); // FIXME: should be done with a signal. } void CrawlerWidget::updateLineNumberHandler( int line ) { currentLineNumber_ = line; emit updateLineNumber( line ); } void CrawlerWidget::markLineFromMain( qint64 line ) { if ( line < logData_->getNbLine() ) { if ( logFilteredData_->isLineMarked( line ) ) logFilteredData_->deleteMark( line ); else logFilteredData_->addMark( line ); // Recompute the content of the filtered window. filteredView->updateData(); // Update the match overview overview_.updateData( logData_->getNbLine() ); // Also update the top window for the coloured bullets. update(); } } void CrawlerWidget::markLineFromFiltered( qint64 line ) { if ( line < logFilteredData_->getNbLine() ) { qint64 line_in_file = logFilteredData_->getMatchingLineNumber( line ); if ( logFilteredData_->filteredLineTypeByIndex( line ) == LogFilteredData::Mark ) logFilteredData_->deleteMark( line_in_file ); else logFilteredData_->addMark( line_in_file ); // Recompute the content of the filtered window. filteredView->updateData(); // Update the match overview overview_.updateData( logData_->getNbLine() ); // Also update the top window for the coloured bullets. update(); } } void CrawlerWidget::applyConfiguration() { std::shared_ptr config = Persistent( "settings" ); QFont font = config->mainFont(); LOG(logDEBUG) << "CrawlerWidget::applyConfiguration"; // Whatever font we use, we should NOT use kerning font.setKerning( false ); font.setFixedPitch( true ); #if QT_VERSION > 0x040700 // Necessary on systems doing subpixel positionning (e.g. Ubuntu 12.04) font.setStyleStrategy( QFont::ForceIntegerMetrics ); #endif logMainView->setFont(font); filteredView->setFont(font); logMainView->setLineNumbersVisible( config->mainLineNumbersVisible() ); filteredView->setLineNumbersVisible( config->filteredLineNumbersVisible() ); overview_.setVisible( config->isOverviewVisible() ); logMainView->refreshOverview(); logMainView->updateDisplaySize(); logMainView->update(); filteredView->updateDisplaySize(); filteredView->update(); // Polling interval logData_->setPollingInterval( config->pollingEnabled() ? config->pollIntervalMs() : 0 ); // Update the SearchLine (history) updateSearchCombo(); } void CrawlerWidget::enteringQuickFind() { LOG(logDEBUG) << "CrawlerWidget::enteringQuickFind"; // Remember who had the focus (only if it is one of our views) QWidget* focus_widget = QApplication::focusWidget(); if ( ( focus_widget == logMainView ) || ( focus_widget == filteredView ) ) qfSavedFocus_ = focus_widget; else qfSavedFocus_ = nullptr; } void CrawlerWidget::exitingQuickFind() { // Restore the focus once the QFBar has been hidden if ( qfSavedFocus_ ) qfSavedFocus_->setFocus(); } void CrawlerWidget::loadingFinishedHandler( LoadingStatus status ) { loadingInProgress_ = false; // We need to refresh the main window because the view lines on the // overview have probably changed. overview_.updateData( logData_->getNbLine() ); // FIXME, handle topLine // logMainView->updateData( logData_, topLine ); logMainView->updateData(); // Shall we Forbid starting a search when loading in progress? // searchButton->setEnabled( false ); // searchButton->setEnabled( true ); // See if we need to auto-refresh the search if ( searchState_.isAutorefreshAllowed() ) { if ( searchState_.isFileTruncated() ) // We need to restart the search replaceCurrentSearch( searchLineEdit->currentText() ); else logFilteredData_->updateSearch(); } // Set the encoding for the views updateEncoding(); emit loadingFinished( status ); // Also change the data available icon if ( firstLoadDone_ ) changeDataStatus( DataStatus::NEW_DATA ); else firstLoadDone_ = true; } void CrawlerWidget::fileChangedHandler( LogData::MonitoredFileStatus status ) { // Handle the case where the file has been truncated if ( status == LogData::Truncated ) { // Clear all marks (TODO offer the option to keep them) logFilteredData_->clearMarks(); if ( ! searchInfoLine->text().isEmpty() ) { // Invalidate the search logFilteredData_->clearSearch(); filteredView->updateData(); searchState_.truncateFile(); printSearchInfoMessage(); nbMatches_ = 0; } } } // Returns a pointer to the window in which the search should be done AbstractLogView* CrawlerWidget::activeView() const { QWidget* activeView; // Search in the window that has focus, or the window where 'Find' was // called from, or the main window. if ( filteredView->hasFocus() || logMainView->hasFocus() ) activeView = QApplication::focusWidget(); else activeView = qfSavedFocus_; if ( activeView ) { AbstractLogView* view = qobject_cast( activeView ); return view; } else { LOG(logWARNING) << "No active view, defaulting to logMainView"; return logMainView; } } void CrawlerWidget::searchForward() { LOG(logDEBUG) << "CrawlerWidget::searchForward"; activeView()->searchForward(); } void CrawlerWidget::searchBackward() { LOG(logDEBUG) << "CrawlerWidget::searchBackward"; activeView()->searchBackward(); } void CrawlerWidget::searchRefreshChangedHandler( int state ) { searchState_.setAutorefresh( state == Qt::Checked ); printSearchInfoMessage( logFilteredData_->getNbMatches() ); } void CrawlerWidget::searchTextChangeHandler() { // We suspend auto-refresh searchState_.changeExpression(); printSearchInfoMessage( logFilteredData_->getNbMatches() ); } void CrawlerWidget::changeFilteredViewVisibility( int index ) { QStandardItem* item = visibilityModel_->item( index ); FilteredView::Visibility visibility = static_cast< FilteredView::Visibility>( item->data().toInt() ); filteredView->setVisibility( visibility ); } void CrawlerWidget::addToSearch( const QString& string ) { QString text = searchLineEdit->currentText(); if ( text.isEmpty() ) text = string; else { // Escape the regexp chars from the string before adding it. text += ( '|' + QRegExp::escape( string ) ); } searchLineEdit->setEditText( text ); // Set the focus to lineEdit so that the user can press 'Return' immediately searchLineEdit->lineEdit()->setFocus(); } void CrawlerWidget::mouseHoveredOverMatch( qint64 line ) { qint64 line_in_mainview = logFilteredData_->getMatchingLineNumber( line ); overviewWidget_->highlightLine( line_in_mainview ); } void CrawlerWidget::activityDetected() { changeDataStatus( DataStatus::OLD_DATA ); } // // Private functions // // Build the widget and connect all the signals, this must be done once // the data are attached. void CrawlerWidget::setup() { setOrientation(Qt::Vertical); assert( logData_ ); assert( logFilteredData_ ); // The views bottomWindow = new QWidget; overviewWidget_ = new OverviewWidget(); logMainView = new LogMainView( logData_, quickFindPattern_.get(), &overview_, overviewWidget_ ); filteredView = new FilteredView( logFilteredData_, quickFindPattern_.get() ); overviewWidget_->setOverview( &overview_ ); overviewWidget_->setParent( logMainView ); // Connect the search to the top view logMainView->useNewFiltering( logFilteredData_ ); // Construct the visibility button visibilityModel_ = new QStandardItemModel( this ); QStandardItem *marksAndMatchesItem = new QStandardItem( tr( "Marks and matches" ) ); QPixmap marksAndMatchesPixmap( 16, 10 ); marksAndMatchesPixmap.fill( Qt::gray ); marksAndMatchesItem->setIcon( QIcon( marksAndMatchesPixmap ) ); marksAndMatchesItem->setData( FilteredView::MarksAndMatches ); visibilityModel_->appendRow( marksAndMatchesItem ); QStandardItem *marksItem = new QStandardItem( tr( "Marks" ) ); QPixmap marksPixmap( 16, 10 ); marksPixmap.fill( Qt::blue ); marksItem->setIcon( QIcon( marksPixmap ) ); marksItem->setData( FilteredView::MarksOnly ); visibilityModel_->appendRow( marksItem ); QStandardItem *matchesItem = new QStandardItem( tr( "Matches" ) ); QPixmap matchesPixmap( 16, 10 ); matchesPixmap.fill( Qt::red ); matchesItem->setIcon( QIcon( matchesPixmap ) ); matchesItem->setData( FilteredView::MatchesOnly ); visibilityModel_->appendRow( matchesItem ); QListView *visibilityView = new QListView( this ); visibilityView->setMovement( QListView::Static ); visibilityView->setMinimumWidth( 170 ); // Only needed with custom style-sheet visibilityBox = new QComboBox(); visibilityBox->setModel( visibilityModel_ ); visibilityBox->setView( visibilityView ); // Select "Marks and matches" by default (same default as the filtered view) visibilityBox->setCurrentIndex( 0 ); // TODO: Maybe there is some way to set the popup width to be // sized-to-content (as it is when the stylesheet is not overriden) in the // stylesheet as opposed to setting a hard min-width on the view above. visibilityBox->setStyleSheet( " \ QComboBox:on {\ padding: 1px 2px 1px 6px;\ width: 19px;\ } \ QComboBox:!on {\ padding: 1px 2px 1px 7px;\ width: 19px;\ height: 16px;\ border: 1px solid gray;\ } \ QComboBox::drop-down::down-arrow {\ width: 0px;\ border-width: 0px;\ } \ " ); // Construct the Search Info line searchInfoLine = new InfoLine(); searchInfoLine->setFrameStyle( QFrame::WinPanel | QFrame::Sunken ); searchInfoLine->setLineWidth( 1 ); searchInfoLineDefaultPalette = searchInfoLine->palette(); ignoreCaseCheck = new QCheckBox( "Ignore &case" ); searchRefreshCheck = new QCheckBox( "Auto-&refresh" ); // Construct the Search line searchLabel = new QLabel(tr("&Text: ")); searchLineEdit = new QComboBox; searchLineEdit->setEditable( true ); searchLineEdit->setCompleter( 0 ); searchLineEdit->addItems( savedSearches_->recentSearches() ); searchLineEdit->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Minimum ); searchLineEdit->setSizeAdjustPolicy( QComboBox::AdjustToMinimumContentsLengthWithIcon ); searchLabel->setBuddy( searchLineEdit ); searchButton = new QToolButton(); searchButton->setText( tr("&Search") ); searchButton->setAutoRaise( true ); stopButton = new QToolButton(); stopButton->setIcon( QIcon(":/images/stop14.png") ); stopButton->setAutoRaise( true ); stopButton->setEnabled( false ); QHBoxLayout* searchLineLayout = new QHBoxLayout; searchLineLayout->addWidget(searchLabel); searchLineLayout->addWidget(searchLineEdit); searchLineLayout->addWidget(searchButton); searchLineLayout->addWidget(stopButton); searchLineLayout->setContentsMargins(6, 0, 6, 0); stopButton->setSizePolicy( QSizePolicy( QSizePolicy::Maximum, QSizePolicy::Maximum ) ); searchButton->setSizePolicy( QSizePolicy( QSizePolicy::Maximum, QSizePolicy::Maximum ) ); QHBoxLayout* searchInfoLineLayout = new QHBoxLayout; searchInfoLineLayout->addWidget( visibilityBox ); searchInfoLineLayout->addWidget( searchInfoLine ); searchInfoLineLayout->addWidget( ignoreCaseCheck ); searchInfoLineLayout->addWidget( searchRefreshCheck ); // Construct the bottom window QVBoxLayout* bottomMainLayout = new QVBoxLayout; bottomMainLayout->addLayout(searchLineLayout); bottomMainLayout->addLayout(searchInfoLineLayout); bottomMainLayout->addWidget(filteredView); bottomMainLayout->setContentsMargins(2, 1, 2, 1); bottomWindow->setLayout(bottomMainLayout); addWidget( logMainView ); addWidget( bottomWindow ); // Default splitter position (usually overridden by the config file) QList splitterSizes; splitterSizes += 400; splitterSizes += 100; setSizes( splitterSizes ); // Default search checkboxes auto config = Persistent( "settings" ); searchRefreshCheck->setCheckState( config->isSearchAutoRefreshDefault() ? Qt::Checked : Qt::Unchecked ); // Manually call the handler as it is not called when changing the state programmatically searchRefreshChangedHandler( searchRefreshCheck->checkState() ); ignoreCaseCheck->setCheckState( config->isSearchIgnoreCaseDefault() ? Qt::Checked : Qt::Unchecked ); // Connect the signals connect(searchLineEdit->lineEdit(), SIGNAL( returnPressed() ), searchButton, SIGNAL( clicked() )); connect(searchLineEdit->lineEdit(), SIGNAL( textEdited( const QString& ) ), this, SLOT( searchTextChangeHandler() )); connect(searchButton, SIGNAL( clicked() ), this, SLOT( startNewSearch() ) ); connect(stopButton, SIGNAL( clicked() ), this, SLOT( stopSearch() ) ); connect(visibilityBox, SIGNAL( currentIndexChanged( int ) ), this, SLOT( changeFilteredViewVisibility( int ) ) ); connect(logMainView, SIGNAL( newSelection( int ) ), logMainView, SLOT( update() ) ); connect(filteredView, SIGNAL( newSelection( int ) ), this, SLOT( jumpToMatchingLine( int ) ) ); connect(filteredView, SIGNAL( newSelection( int ) ), filteredView, SLOT( update() ) ); connect(logMainView, SIGNAL( updateLineNumber( int ) ), this, SLOT( updateLineNumberHandler( int ) ) ); connect(logMainView, SIGNAL( markLine( qint64 ) ), this, SLOT( markLineFromMain( qint64 ) ) ); connect(filteredView, SIGNAL( markLine( qint64 ) ), this, SLOT( markLineFromFiltered( qint64 ) ) ); connect(logMainView, SIGNAL( addToSearch( const QString& ) ), this, SLOT( addToSearch( const QString& ) ) ); connect(filteredView, SIGNAL( addToSearch( const QString& ) ), this, SLOT( addToSearch( const QString& ) ) ); connect(filteredView, SIGNAL( mouseHoveredOverLine( qint64 ) ), this, SLOT( mouseHoveredOverMatch( qint64 ) ) ); connect(filteredView, SIGNAL( mouseLeftHoveringZone() ), overviewWidget_, SLOT( removeHighlight() ) ); // Follow option (up and down) connect(this, SIGNAL( followSet( bool ) ), logMainView, SLOT( followSet( bool ) ) ); connect(this, SIGNAL( followSet( bool ) ), filteredView, SLOT( followSet( bool ) ) ); connect(logMainView, SIGNAL( followModeChanged( bool ) ), this, SIGNAL( followModeChanged( bool ) ) ); connect(filteredView, SIGNAL( followModeChanged( bool ) ), this, SIGNAL( followModeChanged( bool ) ) ); // Detect activity in the views connect(logMainView, SIGNAL( activity() ), this, SLOT( activityDetected() ) ); connect(filteredView, SIGNAL( activity() ), this, SLOT( activityDetected() ) ); connect( logFilteredData_, SIGNAL( searchProgressed( int, int ) ), this, SLOT( updateFilteredView( int, int ) ) ); // Sent load file update to MainWindow (for status update) connect( logData_, SIGNAL( loadingProgressed( int ) ), this, SIGNAL( loadingProgressed( int ) ) ); connect( logData_, SIGNAL( loadingFinished( LoadingStatus ) ), this, SLOT( loadingFinishedHandler( LoadingStatus ) ) ); connect( logData_, SIGNAL( fileChanged( LogData::MonitoredFileStatus ) ), this, SLOT( fileChangedHandler( LogData::MonitoredFileStatus ) ) ); // Search auto-refresh connect( searchRefreshCheck, SIGNAL( stateChanged( int ) ), this, SLOT( searchRefreshChangedHandler( int ) ) ); // Advise the parent the checkboxes have been changed // (for maintaining default config) connect( searchRefreshCheck, SIGNAL( stateChanged( int ) ), this, SIGNAL( searchRefreshChanged( int ) ) ); connect( ignoreCaseCheck, SIGNAL( stateChanged( int ) ), this, SIGNAL( ignoreCaseChanged( int ) ) ); } // Create a new search using the text passed, replace the currently // used one and destroy the old one. void CrawlerWidget::replaceCurrentSearch( const QString& searchText ) { // Interrupt the search if it's ongoing logFilteredData_->interruptSearch(); // We have to wait for the last search update (100%) // before clearing/restarting to avoid having remaining results. // FIXME: this is a bit of a hack, we call processEvents // for Qt to empty its event queue, including (hopefully) // the search update event sent by logFilteredData_. It saves // us the overhead of having proper sync. QApplication::processEvents( QEventLoop::ExcludeUserInputEvents ); nbMatches_ = 0; // Clear and recompute the content of the filtered window. logFilteredData_->clearSearch(); filteredView->updateData(); // Update the match overview overview_.updateData( logData_->getNbLine() ); if ( !searchText.isEmpty() ) { // Determine the type of regexp depending on the config QRegExp::PatternSyntax syntax; static std::shared_ptr config = Persistent( "settings" ); switch ( config->mainRegexpType() ) { case Wildcard: syntax = QRegExp::Wildcard; break; case FixedString: syntax = QRegExp::FixedString; break; default: syntax = QRegExp::RegExp2; break; } // Set the pattern case insensitive if needed Qt::CaseSensitivity case_sensitivity = Qt::CaseSensitive; if ( ignoreCaseCheck->checkState() == Qt::Checked ) case_sensitivity = Qt::CaseInsensitive; // Constructs the regexp QRegExp regexp( searchText, case_sensitivity, syntax ); if ( regexp.isValid() ) { // Activate the stop button stopButton->setEnabled( true ); // Start a new asynchronous search logFilteredData_->runSearch( regexp ); // Accept auto-refresh of the search searchState_.startSearch(); } else { // The regexp is wrong logFilteredData_->clearSearch(); filteredView->updateData(); searchState_.resetState(); // Inform the user QString errorMessage = tr("Error in expression: "); errorMessage += regexp.errorString(); searchInfoLine->setPalette( errorPalette ); searchInfoLine->setText( errorMessage ); } } else { searchState_.resetState(); printSearchInfoMessage(); } } // Updates the content of the drop down list for the saved searches, // called when the SavedSearch has been changed. void CrawlerWidget::updateSearchCombo() { const QString text = searchLineEdit->lineEdit()->text(); searchLineEdit->clear(); searchLineEdit->addItems( savedSearches_->recentSearches() ); // In case we had something that wasn't added to the list (blank...): searchLineEdit->lineEdit()->setText( text ); } // Print the search info message. void CrawlerWidget::printSearchInfoMessage( int nbMatches ) { QString text; switch ( searchState_.getState() ) { case SearchState::NoSearch: // Blank text is fine break; case SearchState::Static: text = tr("%1 match%2 found.").arg( nbMatches ) .arg( nbMatches > 1 ? "es" : "" ); break; case SearchState::Autorefreshing: text = tr("%1 match%2 found. Search is auto-refreshing...").arg( nbMatches ) .arg( nbMatches > 1 ? "es" : "" ); break; case SearchState::FileTruncated: case SearchState::TruncatedAutorefreshing: text = tr("File truncated on disk, previous search results are not valid anymore."); break; } searchInfoLine->setPalette( searchInfoLineDefaultPalette ); searchInfoLine->setText( text ); } // Change the data status and, if needed, advise upstream. void CrawlerWidget::changeDataStatus( DataStatus status ) { if ( ( status != dataStatus_ ) && (! ( dataStatus_ == DataStatus::NEW_FILTERED_DATA && status == DataStatus::NEW_DATA ) ) ) { dataStatus_ = status; emit dataStatusChanged( dataStatus_ ); } } // Determine the right encoding and set the views. void CrawlerWidget::updateEncoding() { static const char* latin1_encoding = "iso-8859-1"; static const char* utf8_encoding = "utf-8"; const char* encoding; switch ( encodingSetting_ ) { case ENCODING_AUTO: switch ( logData_->getDetectedEncoding() ) { case EncodingSpeculator::Encoding::ASCII7: encoding = latin1_encoding; encoding_text_ = tr( "US-ASCII" ); break; case EncodingSpeculator::Encoding::ASCII8: encoding = latin1_encoding; encoding_text_ = tr( "ISO-8859-1" ); break; case EncodingSpeculator::Encoding::UTF8: encoding = utf8_encoding; encoding_text_ = tr( "UTF-8" ); break; } break; case ENCODING_UTF8: encoding = utf8_encoding; encoding_text_ = tr( "Displayed as UTF-8" ); break; case ENCODING_ISO_8859_1: default: encoding = latin1_encoding; encoding_text_ = tr( "Displayed as ISO-8859-1" ); break; } logData_->setDisplayEncoding( encoding ); logMainView->forceRefresh(); logFilteredData_->setDisplayEncoding( encoding ); filteredView->forceRefresh(); } // // SearchState implementation // void CrawlerWidget::SearchState::resetState() { state_ = NoSearch; } void CrawlerWidget::SearchState::setAutorefresh( bool refresh ) { autoRefreshRequested_ = refresh; if ( refresh ) { if ( state_ == Static ) state_ = Autorefreshing; /* else if ( state_ == FileTruncated ) state_ = TruncatedAutorefreshing; */ } else { if ( state_ == Autorefreshing ) state_ = Static; else if ( state_ == TruncatedAutorefreshing ) state_ = FileTruncated; } } void CrawlerWidget::SearchState::truncateFile() { if ( state_ == Autorefreshing || state_ == TruncatedAutorefreshing ) { state_ = TruncatedAutorefreshing; } else { state_ = FileTruncated; } } void CrawlerWidget::SearchState::changeExpression() { if ( state_ == Autorefreshing ) state_ = Static; } void CrawlerWidget::SearchState::stopSearch() { if ( state_ == Autorefreshing ) state_ = Static; } void CrawlerWidget::SearchState::startSearch() { if ( autoRefreshRequested_ ) state_ = Autorefreshing; else state_ = Static; } /* * CrawlerWidgetContext */ CrawlerWidgetContext::CrawlerWidgetContext( const char* string ) { QRegExp regex = QRegExp( "S(\\d+):(\\d+)" ); if ( regex.indexIn( string ) > -1 ) { sizes_ = { regex.cap(1).toInt(), regex.cap(2).toInt() }; LOG(logDEBUG) << "sizes_: " << sizes_[0] << " " << sizes_[1]; } else { LOG(logWARNING) << "Unrecognised view size: " << string; // Default values; sizes_ = { 100, 400 }; } QRegExp case_refresh_regex = QRegExp( "IC(\\d+):AR(\\d+)" ); if ( case_refresh_regex.indexIn( string ) > -1 ) { ignore_case_ = ( case_refresh_regex.cap(1).toInt() == 1 ); auto_refresh_ = ( case_refresh_regex.cap(2).toInt() == 1 ); LOG(logDEBUG) << "ignore_case_: " << ignore_case_ << " auto_refresh_: " << auto_refresh_; } else { LOG(logWARNING) << "Unrecognised case/refresh: " << string; ignore_case_ = false; auto_refresh_ = false; } } std::string CrawlerWidgetContext::toString() const { char string[160]; snprintf( string, sizeof string, "S%d:%d:IC%d:AR%d", sizes_[0], sizes_[1], ignore_case_, auto_refresh_ ); return { string }; } glogg-1.1.4/src/session.h0000644000175000017500000001136313076461374014246 0ustar nickonicko/* * Copyright (C) 2013, 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef SESSION_H #define SESSION_H #include #include #include #include #include #include #include #include #include "quickfindpattern.h" class ViewInterface; class ViewContextInterface; class LogData; class LogFilteredData; class SavedSearches; // File unreadable error class FileUnreadableErr {}; // The session is responsible for maintaining the list of open log files // and their association with Views. // It also maintains the domain objects which are common to all log files // (SavedSearches, FileHistory, QFPattern...) class Session { public: Session(); ~Session(); // No copy/assignment please Session( const Session& ) = delete; Session& operator =( const Session& ) = delete; // Return the view associated to a file if it is open // The filename must be strictly identical to trigger a match // (no match in case of e.g. relative vs. absolute pathname. ViewInterface* getViewIfOpen( const std::string& file_name ) const; // Open a new file, starts its asynchronous loading, and construct a new // view for it (the caller passes a factory to build the concrete view) // The ownership of the view is given to the caller // Throw exceptions if the file is already open or if it cannot be open. ViewInterface* open( const std::string& file_name, std::function view_factory ); // Close the file identified by the view passed // Throw an exception if it does not exist. void close( const ViewInterface* view ); // Open all the files listed in the stored session // (see ::open) // returns a vector of pairs (file_name, view) and the index of the // current file (or -1 if none). std::vector> restore( std::function view_factory, int *current_file_index ); // Save the session to persistent storage. An ordered list of // (view, topline, ViewContextInterface) is passed, this is because only // the main window know the order in which the views are presented to // the user (it might have changed since file were opened). // Also, the geometry information is passed as an opaque string. void save( std::vector< std::tuple> > view_list, const QByteArray& geometry ); // Get the geometry string from persistent storage for this session. void storedGeometry( QByteArray* geometry ) const; // Get the file name for the passed view. std::string getFilename( const ViewInterface* view ) const; // Get the size (in bytes) and number of lines in the current file. // The file is identified by the view attached to it. void getFileInfo( const ViewInterface* view, uint64_t* fileSize, uint32_t* fileNbLine, QDateTime* lastModified ) const; // Get a (non-const) reference to the QuickFind pattern. std::shared_ptr getQuickFindPattern() const { return quickFindPattern_; } private: struct OpenFile { std::string fileName; std::shared_ptr logData; std::shared_ptr logFilteredData; ViewInterface* view; }; // Open a file without checking if it is existing/readable ViewInterface* openAlways( const std::string& file_name, std::function view_factory, const char* view_context ); // Find an open file from its associated view OpenFile* findOpenFileFromView( const ViewInterface* view ); const OpenFile* findOpenFileFromView( const ViewInterface* view ) const; // List of open files typedef std::unordered_map OpenFileMap; OpenFileMap openFiles_; // Global search history std::shared_ptr savedSearches_; // Global quickfind pattern std::shared_ptr quickFindPattern_; }; #endif glogg-1.1.4/src/filterset.cpp0000644000175000017500000001460413076461374015120 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2011 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ // This file implements classes Filter and FilterSet #include #include #include "log.h" #include "filterset.h" const int FilterSet::FILTERSET_VERSION = 1; Qt::CaseSensitivity getCaseSensivity( bool ignoreCase ) { return ignoreCase ? Qt::CaseInsensitive : Qt::CaseSensitive; } Filter::Filter() { } Filter::Filter(const QString& pattern, bool ignoreCase, const QString& foreColorName, const QString& backColorName ) : regexp_( pattern, getCaseSensivity( ignoreCase ) ), foreColorName_( foreColorName ), backColorName_( backColorName ), enabled_( true ) { LOG(logDEBUG) << "New Filter, fore: " << foreColorName_.toStdString() << " back: " << backColorName_.toStdString(); } QString Filter::pattern() const { return regexp_.pattern(); } void Filter::setPattern( const QString& pattern ) { regexp_.setPattern( pattern ); } bool Filter::ignoreCase() const { return regexp_.caseSensitivity() == Qt::CaseInsensitive; } void Filter::setIgnoreCase( bool ignoreCase ) { regexp_.setCaseSensitivity( getCaseSensivity(ignoreCase) ); } const QString& Filter::foreColorName() const { return foreColorName_; } void Filter::setForeColor( const QString& foreColorName ) { foreColorName_ = foreColorName; } const QString& Filter::backColorName() const { return backColorName_; } void Filter::setBackColor( const QString& backColorName ) { backColorName_ = backColorName; } int Filter::indexIn( const QString& string ) const { return regexp_.indexIn( string ); } // // Operators for serialization // QDataStream& operator<<( QDataStream& out, const Filter& object ) { LOG(logDEBUG) << "<>( QDataStream& in, Filter& object ) { LOG(logDEBUG) << ">>operator from Filter"; in >> object.regexp_; in >> object.foreColorName_; in >> object.backColorName_; return in; } // Default constructor FilterSet::FilterSet() { qRegisterMetaTypeStreamOperators( "Filter" ); qRegisterMetaTypeStreamOperators( "FilterSet" ); qRegisterMetaTypeStreamOperators( "FilterSet::FilterList" ); } bool FilterSet::matchLine( const QString& line, QColor* foreColor, QColor* backColor ) const { for ( QList::const_iterator i = filterList.constBegin(); i != filterList.constEnd(); i++ ) { if ( i->indexIn( line ) != -1 ) { foreColor->setNamedColor( i->foreColorName() ); backColor->setNamedColor( i->backColorName() ); return true; } } return false; } // // Operators for serialization // QDataStream& operator<<( QDataStream& out, const FilterSet& object ) { LOG(logDEBUG) << "<>( QDataStream& in, FilterSet& object ) { LOG(logDEBUG) << ">>operator from FilterSet"; in >> object.filterList; return in; } // // Persistable virtual functions implementation // void Filter::saveToStorage( QSettings& settings ) const { LOG(logDEBUG) << "Filter::saveToStorage"; settings.setValue( "regexp", regexp_.pattern() ); settings.setValue( "ignore_case", regexp_.caseSensitivity() == Qt::CaseInsensitive); settings.setValue( "fore_colour", foreColorName_ ); settings.setValue( "back_colour", backColorName_ ); } void Filter::retrieveFromStorage( QSettings& settings ) { LOG(logDEBUG) << "Filter::retrieveFromStorage"; regexp_ = QRegExp( settings.value( "regexp" ).toString(), getCaseSensivity( settings.value( "ignore_case", false ).toBool() ) ); foreColorName_ = settings.value( "fore_colour" ).toString(); backColorName_ = settings.value( "back_colour" ).toString(); } void FilterSet::saveToStorage( QSettings& settings ) const { LOG(logDEBUG) << "FilterSet::saveToStorage"; settings.beginGroup( "FilterSet" ); // Remove everything in case the array is shorter than the previous one settings.remove(""); settings.setValue( "version", FILTERSET_VERSION ); settings.beginWriteArray( "filters" ); for (int i = 0; i < filterList.size(); ++i) { settings.setArrayIndex(i); filterList[i].saveToStorage( settings ); } settings.endArray(); settings.endGroup(); } void FilterSet::retrieveFromStorage( QSettings& settings ) { LOG(logDEBUG) << "FilterSet::retrieveFromStorage"; filterList.clear(); if ( settings.contains( "FilterSet/version" ) ) { settings.beginGroup( "FilterSet" ); if ( settings.value( "version" ) == FILTERSET_VERSION ) { int size = settings.beginReadArray( "filters" ); for (int i = 0; i < size; ++i) { settings.setArrayIndex(i); Filter filter; filter.retrieveFromStorage( settings ); filterList.append( filter ); } settings.endArray(); } else { LOG(logERROR) << "Unknown version of FilterSet, ignoring it..."; } settings.endGroup(); } else { LOG(logWARNING) << "Trying to import legacy (<=0.8.2) filters..."; FilterSet tmp_filter_set = settings.value( "filterSet" ).value(); *this = tmp_filter_set; LOG(logWARNING) << "...imported filterset: " << filterList.count() << " elements"; // Remove the old key once migration is done settings.remove( "filterSet" ); // And replace it with the new one saveToStorage( settings ); settings.sync(); } } glogg-1.1.4/src/marks.cpp0000644000175000017500000000424513076461374014234 0ustar nickonicko/* * Copyright (C) 2011 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include "marks.h" #include "log.h" #include "utils.h" // This file implements the list of marks for a file. // It is implemented as a QList which is kept in order when inserting, // it is simpler than something fancy like a heap, and insertion are // not done very often anyway. Oh and we need to iterate through the // list, disqualifying a straight heap. Marks::Marks() : marks_() { } void Marks::addMark( qint64 line, QChar mark ) { // Look for the index immediately before int index; if ( ! lookupLineNumber< QList >( marks_, line, &index ) ) { // If a mark is not already set for this line LOG(logDEBUG) << "Inserting mark at line " << line << " (index " << index << ")"; marks_.insert( index, Mark( line ) ); } else { LOG(logERROR) << "Trying to add an existing mark at line " << line; } // 'mark' is not used yet mark = mark; } qint64 Marks::getMark( QChar mark ) const { // 'mark' is not used yet mark = mark; return 0; } bool Marks::isLineMarked( qint64 line ) const { int index; return lookupLineNumber< QList >( marks_, line, &index ); } void Marks::deleteMark( QChar mark ) { // 'mark' is not used yet mark = mark; } void Marks::deleteMark( qint64 line ) { int index; if ( lookupLineNumber< QList >( marks_, line, &index ) ) { marks_.removeAt( index ); } } void Marks::clear() { marks_.clear(); } glogg-1.1.4/src/viewinterface.h0000644000175000017500000000553613076461374015423 0ustar nickonicko/* * Copyright (C) 2013, 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef VIEWINTERFACE_H #define VIEWINTERFACE_H #include class LogData; class LogFilteredData; class SavedSearches; class QuickFindPattern; // ViewContextInterface represents the private information // the concrete view will be able to save and restore. // It can be marshalled to persistent storage. class ViewContextInterface { public: virtual ~ViewContextInterface() {} virtual std::string toString() const = 0; }; // ViewInterface represents a high-level view on a log file. // This a pure virtual class (interface) which is subclassed // for each type of view. class ViewInterface { public: // Set the log data and filtered data to associate to this view // Ownership stay with the caller but is shared void setData( std::shared_ptr log_data, std::shared_ptr filtered_data ) { doSetData( log_data, filtered_data ); } // Set the (shared) quickfind pattern object void setQuickFindPattern( std::shared_ptr qfp ) { doSetQuickFindPattern( qfp ); } // Set the (shared) search history object void setSavedSearches( std::shared_ptr saved_searches ) { doSetSavedSearches( saved_searches ); } // For save/restore of the context void setViewContext( const char* view_context ) { doSetViewContext( view_context ); } // (returned object ownership is transferred to the caller) std::shared_ptr context( void ) const { return doGetViewContext(); } // To allow polymorphic destruction virtual ~ViewInterface() {} protected: // Virtual functions (using NVI) virtual void doSetData( std::shared_ptr log_data, std::shared_ptr filtered_data ) = 0; virtual void doSetQuickFindPattern( std::shared_ptr qfp ) = 0; virtual void doSetSavedSearches( std::shared_ptr saved_searches ) = 0; virtual void doSetViewContext( const char* view_context ) = 0; virtual std::shared_ptr doGetViewContext( void ) const = 0; }; #endif glogg-1.1.4/src/qtfilewatcher.h0000644000175000017500000000362113076461374015423 0ustar nickonicko/* * Copyright (C) 2010 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef QTFILEWATCHER_H #define QTFILEWATCHER_H #include "filewatcher.h" #include // This class encapsulate Qt's QFileSystemWatcher and additionally support // watching a file that doesn't exist yet (the class will watch the owning // directory) // Only supports one file at the moment. class QtFileWatcher : public FileWatcher { Q_OBJECT public: // Create an empty object QtFileWatcher(); // Destroy the object ~QtFileWatcher(); // Adds the file to the list of file to watch // (do nothing if a file is already monitored) void addFile( const QString& fileName ); // Removes the file to the list of file to watch // (do nothing if said file is not monitored) void removeFile( const QString& fileName ); signals: // Sent when the file on disk has changed in any way. void fileChanged( const QString& ); private slots: void fileChangedOnDisk( const QString& filename ); void directoryChangedOnDisk( const QString& filename ); private: enum MonitoringState { None, FileExists, FileRemoved }; QFileSystemWatcher qtFileWatcher_; QString fileMonitored_; MonitoringState monitoringState_; }; #endif glogg-1.1.4/src/session.cpp0000644000175000017500000001445313076461374014604 0ustar nickonicko/* * Copyright (C) 2013, 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include "session.h" #include "log.h" #include #include #include #include "viewinterface.h" #include "persistentinfo.h" #include "savedsearches.h" #include "sessioninfo.h" #include "data/logdata.h" #include "data/logfiltereddata.h" Session::Session() { GetPersistentInfo().retrieve( QString( "savedSearches" ) ); // Get the global search history (it remains the property // of the Persistent) savedSearches_ = Persistent( "savedSearches" ); quickFindPattern_ = std::make_shared(); } Session::~Session() { // FIXME Clean up all the data objects... } ViewInterface* Session::getViewIfOpen( const std::string& file_name ) const { auto result = std::find_if( openFiles_.begin(), openFiles_.end(), [&](const std::pair& o) { return ( o.second.fileName == file_name ); } ); if ( result != openFiles_.end() ) return result->second.view; else return nullptr; } ViewInterface* Session::open( const std::string& file_name, std::function view_factory ) { ViewInterface* view = nullptr; QFileInfo fileInfo( file_name.c_str() ); if ( fileInfo.isReadable() ) { return openAlways( file_name, view_factory, nullptr ); } else { throw FileUnreadableErr(); } return view; } void Session::close( const ViewInterface* view ) { openFiles_.erase( openFiles_.find( view ) ); } void Session::save( std::vector< std::tuple> > view_list, const QByteArray& geometry ) { LOG(logDEBUG) << "Session::save"; std::vector session_files; for ( auto view: view_list ) { const ViewInterface* view_object; uint64_t top_line; std::shared_ptr view_context; std::tie( view_object, top_line, view_context ) = view; const OpenFile* file = findOpenFileFromView( view_object ); assert( file ); LOG(logDEBUG) << "Saving " << file->fileName << " in session."; session_files.push_back( { file->fileName, top_line, view_context->toString() } ); } std::shared_ptr session = Persistent( "session" ); session->setOpenFiles( session_files ); session->setGeometry( geometry ); GetPersistentInfo().save( QString( "session" ) ); } std::vector> Session::restore( std::function view_factory, int *current_file_index ) { GetPersistentInfo().retrieve( QString( "session" ) ); std::shared_ptr session = Persistent( "session" ); std::vector session_files = session->openFiles(); LOG(logDEBUG) << "Session returned " << session_files.size(); std::vector> result; for ( auto file: session_files ) { LOG(logDEBUG) << "Create view for " << file.fileName; ViewInterface* view = openAlways( file.fileName, view_factory, file.viewContext.c_str() ); result.push_back( { file.fileName, view } ); } *current_file_index = -1; return result; } void Session::storedGeometry( QByteArray* geometry ) const { GetPersistentInfo().retrieve( QString( "session" ) ); std::shared_ptr session = Persistent( "session" ); *geometry = session->geometry(); } std::string Session::getFilename( const ViewInterface* view ) const { const OpenFile* file = findOpenFileFromView( view ); assert( file ); return file->fileName; } void Session::getFileInfo( const ViewInterface* view, uint64_t* fileSize, uint32_t* fileNbLine, QDateTime* lastModified ) const { const OpenFile* file = findOpenFileFromView( view ); assert( file ); *fileSize = file->logData->getFileSize(); *fileNbLine = file->logData->getNbLine(); *lastModified = file->logData->getLastModifiedDate(); } /* * Private methods */ ViewInterface* Session::openAlways( const std::string& file_name, std::function view_factory, const char* view_context ) { // Create the data objects auto log_data = std::make_shared(); auto log_filtered_data = std::shared_ptr( log_data->getNewFilteredData() ); ViewInterface* view = view_factory(); view->setData( log_data, log_filtered_data ); view->setQuickFindPattern( quickFindPattern_ ); view->setSavedSearches( savedSearches_ ); if ( view_context ) view->setViewContext( view_context ); // Insert in the hash openFiles_.insert( { view, { file_name, log_data, log_filtered_data, view } } ); // Start loading the file log_data->attachFile( QString( file_name.c_str() ) ); return view; } Session::OpenFile* Session::findOpenFileFromView( const ViewInterface* view ) { assert( view ); OpenFile* file = &( openFiles_.at( view ) ); // OpenfileMap::at might throw out_of_range but since a view MUST always // be attached to a file, we don't handle it! return file; } const Session::OpenFile* Session::findOpenFileFromView( const ViewInterface* view ) const { assert( view ); const OpenFile* file = &( openFiles_.at( view ) ); // OpenfileMap::at might throw out_of_range but since a view MUST always // be attached to a file, we don't handle it! return file; } glogg-1.1.4/src/filterset.h0000644000175000017500000000711113076461374014560 0ustar nickonicko/* * Copyright (C) 2009, 2010 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef FILTERSET_H #define FILTERSET_H #include #include #include #include "persistable.h" // Represents a filter, i.e. a regexp and the colors matching text // should be rendered in. class Filter { public: // Construct an uninitialized Filter (when reading from a config file) Filter(); Filter(const QString& pattern, bool ignoreCase, const QString& foreColor, const QString& backColor ); // Tests the string passed for a match, returns a value just like // QRegExp::indexIn (i.e. -1 if no match) int indexIn( const QString& string ) const; // Accessor functions QString pattern() const; void setPattern( const QString& pattern ); bool ignoreCase() const; void setIgnoreCase( bool ignoreCase ); const QString& foreColorName() const; void setForeColor( const QString& foreColorName ); const QString& backColorName() const; void setBackColor( const QString& backColorName ); // Operators for serialization // (must be kept to migrate filters from <=0.8.2) friend QDataStream& operator<<( QDataStream& out, const Filter& object ); friend QDataStream& operator>>( QDataStream& in, Filter& object ); // Reads/writes the current config in the QSettings object passed void saveToStorage( QSettings& settings ) const; void retrieveFromStorage( QSettings& settings ); private: QRegExp regexp_; QString foreColorName_; QString backColorName_; bool enabled_; }; // Represents an ordered set of filters to be applied to each line displayed. class FilterSet : public Persistable { public: // Construct an empty filter set FilterSet(); // Returns weither the passed line match a filter of the set, // if so, it returns the fore/back colors the line should use. // Ownership of the colors is transfered to the caller. bool matchLine( const QString& line, QColor* foreColor, QColor* backColor ) const; // Reads/writes the current config in the QSettings object passed virtual void saveToStorage( QSettings& settings ) const; virtual void retrieveFromStorage( QSettings& settings ); // Should be private really, but I don't know how to have // it recognised by QVariant then. typedef QList FilterList; // Operators for serialization // (must be kept to migrate filters from <=0.8.2) friend QDataStream& operator<<( QDataStream& out, const FilterSet& object ); friend QDataStream& operator>>( QDataStream& in, FilterSet& object ); private: static const int FILTERSET_VERSION; FilterList filterList; // To simplify this class interface, FilterDialog can access our // internal structure directly. friend class FiltersDialog; }; Q_DECLARE_METATYPE(Filter) Q_DECLARE_METATYPE(FilterSet) Q_DECLARE_METATYPE(FilterSet::FilterList) #endif glogg-1.1.4/src/versionchecker.cpp0000644000175000017500000001235313076461374016130 0ustar nickonicko/* * Copyright (C) 2014 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include "versionchecker.h" #include "persistentinfo.h" #include "log.h" #if defined(_WIN64) # define GLOGG_OS "win64" #elif defined(_WIN32) # define GLOGG_OS "win32" #elif defined(__APPLE__) # define GLOGG_OS "OSX" #elif defined(__linux__) # define GLOGG_OS "linux" #else # define GLOGG_OS "other" #endif const char* VersionChecker::VERSION_URL = "http://gloggversion.bonnefon.org/latest"; const uint64_t VersionChecker::CHECK_INTERVAL_S = 3600 * 24 * 7; /* 7 days */ namespace { bool isVersionNewer( const QString& current, const QString& new_version ); }; VersionCheckerConfig::VersionCheckerConfig() { enabled_ = true; next_deadline_ = 0; } void VersionCheckerConfig::retrieveFromStorage( QSettings& settings ) { if ( settings.contains( "versionchecker.enabled" ) ) enabled_ = settings.value( "versionchecker.enabled" ).toBool(); if ( settings.contains( "versionchecker.nextDeadline" ) ) next_deadline_ = settings.value( "versionchecker.nextDeadline" ).toLongLong(); } void VersionCheckerConfig::saveToStorage( QSettings& settings ) const { settings.setValue( "versionchecker.enabled", enabled_ ); settings.setValue( "versionchecker.nextDeadline", static_cast( next_deadline_ ) ); } VersionChecker::VersionChecker() : QObject(), manager_( this ) { } VersionChecker::~VersionChecker() { } void VersionChecker::startCheck() { LOG(logDEBUG) << "VersionChecker::startCheck()"; GetPersistentInfo().retrieve( "versionChecker" ); auto config = Persistent( "versionChecker" ); if ( config->versionCheckingEnabled() ) { // Check the deadline has been reached if ( config->nextDeadline() < std::time( nullptr ) ) { connect( &manager_, SIGNAL( finished( QNetworkReply* ) ), this, SLOT( downloadFinished( QNetworkReply* ) ) ); QNetworkRequest request; request.setUrl( QUrl( VERSION_URL ) ); request.setRawHeader( "User-Agent", "glogg-" GLOGG_VERSION " (" GLOGG_OS ")" ); manager_.get( request ); } else { LOG(logDEBUG) << "Deadline not reached yet, next check in " << std::difftime( config->nextDeadline(), std::time( nullptr ) ); } } } void VersionChecker::downloadFinished( QNetworkReply* reply ) { LOG(logDEBUG) << "VersionChecker::downloadFinished()"; if ( reply->error() == QNetworkReply::NoError ) { QString new_version = QString( reply->read( 256 ) ).remove( '\n' ); LOG(logDEBUG) << "Latest version is " << new_version.toStdString(); if ( isVersionNewer( QString( GLOGG_VERSION ), new_version ) ) { LOG(logDEBUG) << "Sending new version notification"; emit newVersionFound( new_version ); } } else { LOG(logWARNING) << "Download failed: err " << reply->error(); } reply->deleteLater(); // Extend the deadline auto config = Persistent( "versionChecker" ); config->setNextDeadline( std::time( nullptr ) + CHECK_INTERVAL_S ); GetPersistentInfo().save( "versionChecker" ); } namespace { bool isVersionNewer( const QString& current, const QString& new_version ) { QRegExp version_regex( "(\\d+)\\.(\\d+)\\.(\\d+)(-(\\S+))?", Qt::CaseSensitive, QRegExp::RegExp2 ); // Main version is the three first digits // Add is the part after '-' if there unsigned current_main_version = 0; unsigned current_add_version = 0; unsigned new_main_version = 0; unsigned new_add_version = 0; if ( version_regex.indexIn( current ) != -1 ) { current_main_version = version_regex.cap(3).toInt() + version_regex.cap(2).toInt() * 100 + version_regex.cap(1).toInt() * 10000; current_add_version = version_regex.cap(5).isEmpty() ? 0 : 1; } if ( version_regex.indexIn( new_version ) != -1 ) { new_main_version = version_regex.cap(3).toInt() + version_regex.cap(2).toInt() * 100 + version_regex.cap(1).toInt() * 10000; new_add_version = version_regex.cap(5).isEmpty() ? 0 : 1; } LOG(logDEBUG) << "Current version: " << current_main_version; LOG(logDEBUG) << "New version: " << new_main_version; // We only consider the main part for testing for now return new_main_version > current_main_version; } }; glogg-1.1.4/src/savedsearches.h0000644000175000017500000000360313076461374015401 0ustar nickonicko/* * Copyright (C) 2009, 2011 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef SAVEDSEARCHES_H #define SAVEDSEARCHES_H #include #include #include #include "persistable.h" // Keeps track of the previously used searches and allows the application // to retrieve them. class SavedSearches : public Persistable { public: // Creates an empty set of saved searches SavedSearches(); // Adds the passed search to the list of recently used searches void addRecent( const QString& text ); // Returns a list of recent searches (newer first) QStringList recentSearches() const; // Operators for serialization // (only for migrating pre 0.8.2 settings, will be removed) friend QDataStream& operator<<( QDataStream& out, const SavedSearches& object ); friend QDataStream& operator>>( QDataStream& in, SavedSearches& object ); // Reads/writes the current config in the QSettings object passed void saveToStorage( QSettings& settings ) const; void retrieveFromStorage( QSettings& settings ); private: static const int SAVEDSEARCHES_VERSION; static const int maxNumberOfRecentSearches; QStringList savedSearches_; }; Q_DECLARE_METATYPE(SavedSearches) #endif glogg-1.1.4/src/optionsdialog.ui0000644000175000017500000002177213076461374015631 0ustar nickonicko OptionsDialog 0 0 515 477 0 0 Options true 0 General 0 0 Font QLayout::SetDefaultConstraint Family: 0 0 Size: 0 0 0 0 Search options Main search type: 0 0 QuickFind search type: 0 0 Qt::LeftToRight Incremental QuickFind Session options Load last session Qt::Vertical 20 0 true Advanced File polling Enable Polling Time Interval (ms): 16777215 16777215 Qt::ImhDigitsOnly Files will be polled at the interval specified here. Qt::AutoText true Qt::Vertical 20 40 Qt::Horizontal QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok 10 10 true true true glogg-1.1.4/src/platformfilewatcher.cpp0000644000175000017500000000342713076461374017162 0ustar nickonicko/* * Copyright (C) 2010, 2014, 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include "platformfilewatcher.h" #include "log.h" std::shared_ptr PlatformFileWatcher::watch_tower_; PlatformFileWatcher::PlatformFileWatcher() : FileWatcher() { // Caution, this is NOT thread-safe or re-entrant! if ( !watch_tower_ ) { watch_tower_ = std::make_shared(); } } PlatformFileWatcher::~PlatformFileWatcher() { } void PlatformFileWatcher::addFile( const QString& fileName ) { LOG(logDEBUG) << "FileWatcher::addFile " << fileName.toStdString(); watched_file_name_ = fileName; notification_ = std::make_shared( watch_tower_->addFile( fileName.toStdString(), [this, fileName] { emit fileChanged( fileName ); } ) ); } void PlatformFileWatcher::removeFile( const QString& fileName ) { LOG(logDEBUG) << "FileWatcher::removeFile " << fileName.toStdString(); notification_ = nullptr; } void PlatformFileWatcher::setPollingInterval( uint32_t interval_ms ) { watch_tower_->setPollingInterval( interval_ms ); } glogg-1.1.4/src/infoline.h0000644000175000017500000000306313076461374014364 0ustar nickonicko/* * Copyright (C) 2009, 2010 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef INFOLINE_H #define INFOLINE_H #include #include // Information line with integrated completion gauge // used for the file name and the search results. class InfoLine : public QLabel { public: // Default constructor InfoLine(); // Display the gauge in the background with the passed value (0-100) // This function doesn't change the text of the widget. void displayGauge( int completion ); // Hide the gauge and make the widget like a normal QLabel void hideGauge(); protected: void paintEvent(QPaintEvent* paintEvent); private: // The original palette of the QLabel QPalette origPalette_; // Color of the background const QColor backgroundColor_; // Color of the darkened background (left part of the gauge) const QColor darkBackgroundColor_; }; #endif glogg-1.1.4/src/tabbedcrawlerwidget.cpp0000644000175000017500000001277513076461374017133 0ustar nickonicko/* * Copyright (C) 2014, 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include "tabbedcrawlerwidget.h" #include #include #include "crawlerwidget.h" #include "log.h" TabbedCrawlerWidget::TabbedCrawlerWidget() : QTabWidget(), olddata_icon_( ":/images/olddata_icon.png" ), newdata_icon_( ":/images/newdata_icon.png" ), newfiltered_icon_( ":/images/newfiltered_icon.png" ), myTabBar_() { #ifdef WIN32 myTabBar_.setStyleSheet( "QTabBar::tab {\ height: 20px; " "} " "QTabBar::close-button {\ height: 6px; width: 6px;\ subcontrol-origin: padding;\ subcontrol-position: left;\ }" ); #else // On GTK style, it looks better with a smaller font myTabBar_.setStyleSheet( "QTabBar::tab {" " height: 20px; " " font-size: 9pt; " "} " "QTabBar::close-button {\ height: 6px; width: 6px;\ subcontrol-origin: padding;\ subcontrol-position: left;\ }" ); #endif setTabBar( &myTabBar_ ); myTabBar_.hide(); } // I know hiding non-virtual functions from the base class is bad form // and I do it here out of pure laziness: I don't want to encapsulate // QTabBar with all signals and all just to implement this very simple logic. // Maybe one day that should be done better... int TabbedCrawlerWidget::addTab( QWidget* page, const QString& label ) { int index = QTabWidget::addTab( page, label ); if ( auto crawler = dynamic_cast( page ) ) { // Mmmmhhhh... new Qt5 signal syntax create tight coupling between // us and the sender, baaaaad.... // Listen for a changing data status: connect( crawler, &CrawlerWidget::dataStatusChanged, [ this, index ]( DataStatus status ) { setTabDataStatus( index, status ); } ); } // Display the icon QLabel* icon_label = new QLabel(); icon_label->setPixmap( olddata_icon_.pixmap( 11, 12 ) ); icon_label->setAlignment( Qt::AlignCenter ); myTabBar_.setTabButton( index, QTabBar::RightSide, icon_label ); LOG(logDEBUG) << "addTab, count = " << count(); LOG(logDEBUG) << "width = " << olddata_icon_.pixmap( 11, 12 ).devicePixelRatio(); if ( count() > 1 ) myTabBar_.show(); return index; } void TabbedCrawlerWidget::removeTab( int index ) { QTabWidget::removeTab( index ); if ( count() <= 1 ) myTabBar_.hide(); } void TabbedCrawlerWidget::mouseReleaseEvent( QMouseEvent *event) { LOG(logDEBUG) << "TabbedCrawlerWidget::mouseReleaseEvent"; if (event->button() == Qt::MidButton) { int tab = this->myTabBar_.tabAt( event->pos() ); if (-1 != tab) { emit tabCloseRequested( tab ); } } } void TabbedCrawlerWidget::keyPressEvent( QKeyEvent* event ) { const auto mod = event->modifiers(); const auto key = event->key(); LOG(logDEBUG) << "TabbedCrawlerWidget::keyPressEvent"; // Ctrl + tab if ( ( mod == Qt::ControlModifier && key == Qt::Key_Tab ) || ( mod == ( Qt::ControlModifier | Qt::AltModifier | Qt::KeypadModifier ) && key == Qt::Key_Right ) ) { setCurrentIndex( ( currentIndex() + 1 ) % count() ); } // Ctrl + shift + tab else if ( ( mod == ( Qt::ControlModifier | Qt::ShiftModifier ) && key == Qt::Key_Tab ) || ( mod == ( Qt::ControlModifier | Qt::AltModifier | Qt::KeypadModifier ) && key == Qt::Key_Left ) ) { setCurrentIndex( ( currentIndex() - 1 >= 0 ) ? currentIndex() - 1 : count() - 1 ); } // Ctrl + numbers else if ( mod == Qt::ControlModifier && ( key >= Qt::Key_1 && key <= Qt::Key_8 ) ) { int new_index = key - Qt::Key_0; if ( new_index <= count() ) setCurrentIndex( new_index - 1 ); } // Ctrl + 9 else if ( mod == Qt::ControlModifier && key == Qt::Key_9 ) { setCurrentIndex( count() - 1 ); } else if ( mod == Qt::ControlModifier && (key == Qt::Key_Q || key == Qt::Key_W) ) { emit tabCloseRequested( currentIndex() ); } else { QTabWidget::keyPressEvent( event ); } } void TabbedCrawlerWidget::setTabDataStatus( int index, DataStatus status ) { LOG(logDEBUG) << "TabbedCrawlerWidget::setTabDataStatus " << index; QLabel* icon_label = dynamic_cast( myTabBar_.tabButton( index, QTabBar::RightSide ) ); if ( icon_label ) { const QIcon* icon; switch ( status ) { case DataStatus::OLD_DATA: icon = &olddata_icon_; break; case DataStatus::NEW_DATA: icon = &newdata_icon_; break; case DataStatus::NEW_FILTERED_DATA: icon = &newfiltered_icon_; break; } icon_label->setPixmap ( icon->pixmap(12,12) ); } } glogg-1.1.4/src/crawlerwidget.h0000644000175000017500000002372213076461374015430 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2011, 2013, 2014, 2015 Nicolas Bonnefon * and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef CRAWLERWIDGET_H #define CRAWLERWIDGET_H #include #include #include #include #include #include #include #include #include "logmainview.h" #include "filteredview.h" #include "data/logdata.h" #include "data/logfiltereddata.h" #include "viewinterface.h" #include "signalmux.h" #include "overview.h" #include "loadingstatus.h" class InfoLine; class QuickFindPattern; class SavedSearches; class QStandardItemModel; class OverviewWidget; // Implements the central widget of the application. // It includes both windows, the search line, the info // lines and various buttons. class CrawlerWidget : public QSplitter, public QuickFindMuxSelectorInterface, public ViewInterface, public MuxableDocumentInterface { Q_OBJECT public: CrawlerWidget( QWidget *parent=0 ); // Get the line number of the first line displayed. int getTopLine() const; // Get the selected text as a string (from the main window) QString getSelectedText() const; // Display the QFB at the bottom, remembering where the focus was void displayQuickFindBar( QuickFindMux::QFDirection direction ); // Instructs the widget to select all the text in the window the user // is interacting with void selectAll(); enum Encoding { ENCODING_AUTO = 0, ENCODING_ISO_8859_1, ENCODING_UTF8, ENCODING_MAX, }; Encoding encodingSetting() const; // Returns whether follow is enabled in this crawler bool isFollowEnabled() const; // Get the text description of the encoding effectively used, // suitable to display to the user. QString encodingText() const; public slots: // Stop the asynchoronous loading of the file if one is in progress // The file is identified by the view attached to it. void stopLoading(); // Reload the displayed file void reload(); // Set the encoding void setEncoding( Encoding encoding ); protected: // Implementation of the ViewInterface functions virtual void doSetData( std::shared_ptr log_data, std::shared_ptr filtered_data ); virtual void doSetQuickFindPattern( std::shared_ptr qfp ); virtual void doSetSavedSearches( std::shared_ptr saved_searches ); virtual void doSetViewContext( const char* view_context ); virtual std::shared_ptr doGetViewContext( void ) const; // Implementation of the mux selector interface // (for dispatching QuickFind to the right widget) virtual SearchableWidgetInterface* doGetActiveSearchable() const; virtual std::vector doGetAllSearchables() const; // Implementation of the MuxableDocumentInterface virtual void doSendAllStateSignals(); signals: // Sent to signal the client load has progressed, // passing the completion percentage. void loadingProgressed( int progress ); // Sent to the client when the loading has finished // weither succesfull or not. void loadingFinished( LoadingStatus status ); // Sent when follow mode is enabled/disabled void followSet( bool checked ); // Sent up to the MainWindow to enable/disable the follow mode void followModeChanged( bool follow ); // Sent up when the current line number is updated void updateLineNumber( int line ); // "auto-refresh" check has been changed void searchRefreshChanged( int state ); // "ignore case" check has been changed void ignoreCaseChanged( int state ); // Sent when the data status (whether new not seen data are // available) has changed void dataStatusChanged( DataStatus status ); private slots: // Instructs the widget to start a search using the current search line. void startNewSearch(); // Stop the currently ongoing search (if one exists) void stopSearch(); // Instructs the widget to reconfigure itself because Config() has changed. void applyConfiguration(); // QuickFind is being entered, save the focus for incremental qf. void enteringQuickFind(); // QuickFind is being closed. void exitingQuickFind(); // Called when new data must be displayed in the filtered window. void updateFilteredView( int nbMatches, int progress ); // Called when a new line has been selected in the filtered view, // to instruct the main view to jump to the matching line. void jumpToMatchingLine( int filteredLineNb ); // Called when the main view is on a new line number void updateLineNumberHandler( int line ); // Mark a line that has been clicked on the main (top) view. void markLineFromMain( qint64 line ); // Mark a line that has been clicked on the filtered (bottom) view. void markLineFromFiltered( qint64 line ); void loadingFinishedHandler( LoadingStatus status ); // Manages the info lines to inform the user the file has changed. void fileChangedHandler( LogData::MonitoredFileStatus ); void searchForward(); void searchBackward(); // Called when the checkbox for search auto-refresh is changed void searchRefreshChangedHandler( int state ); // Called when the text on the search line is modified void searchTextChangeHandler(); // Called when the user change the visibility combobox void changeFilteredViewVisibility( int index ); // Called when the user add the string to the search void addToSearch( const QString& string ); // Called when a match is hovered on in the filtered view void mouseHoveredOverMatch( qint64 line ); // Called when there was activity in the views void activityDetected(); private: // State machine holding the state of the search, used to allow/disallow // auto-refresh and inform the user via the info line. class SearchState { public: enum State { NoSearch, Static, Autorefreshing, FileTruncated, TruncatedAutorefreshing, }; SearchState() { state_ = NoSearch; autoRefreshRequested_ = false; } // Reset the state (no search active) void resetState(); // The user changed auto-refresh request void setAutorefresh( bool refresh ); // The file has been truncated (stops auto-refresh) void truncateFile(); // The expression has been changed (stops auto-refresh) void changeExpression(); // The search has been stopped (stops auto-refresh) void stopSearch(); // The search has been started (enable auto-refresh) void startSearch(); // Get the state in order to display the proper message State getState() const { return state_; } // Is auto-refresh allowed bool isAutorefreshAllowed() const { return ( state_ == Autorefreshing || state_ == TruncatedAutorefreshing ); } bool isFileTruncated() const { return ( state_ == FileTruncated || state_ == TruncatedAutorefreshing ); } private: State state_; bool autoRefreshRequested_; }; // Private functions void setup(); void replaceCurrentSearch( const QString& searchText ); void updateSearchCombo(); AbstractLogView* activeView() const; void printSearchInfoMessage( int nbMatches = 0 ); void changeDataStatus( DataStatus status ); void updateEncoding(); // Palette for error notification (yellow background) static const QPalette errorPalette; LogMainView* logMainView; QWidget* bottomWindow; QLabel* searchLabel; QComboBox* searchLineEdit; QToolButton* searchButton; QToolButton* stopButton; FilteredView* filteredView; QComboBox* visibilityBox; InfoLine* searchInfoLine; QCheckBox* ignoreCaseCheck; QCheckBox* searchRefreshCheck; OverviewWidget* overviewWidget_; QVBoxLayout* bottomMainLayout; QHBoxLayout* searchLineLayout; QHBoxLayout* searchInfoLineLayout; // Default palette to be remembered QPalette searchInfoLineDefaultPalette; std::shared_ptr savedSearches_; // Reference to the QuickFind Pattern (not owned) std::shared_ptr quickFindPattern_; LogData* logData_; LogFilteredData* logFilteredData_; qint64 logFileSize_; QWidget* qfSavedFocus_; // Search state (for auto-refresh and truncation) SearchState searchState_; // Matches overview Overview overview_; // Model for the visibility selector QStandardItemModel* visibilityModel_; // Last main line number received qint64 currentLineNumber_; // Are we loading something? // Set to false when we receive a completion message from the LogData bool loadingInProgress_; // Is it not the first time we are loading something? bool firstLoadDone_; // Current number of matches int nbMatches_; // the current dataStatus (whether we have new, not seen, data) DataStatus dataStatus_; // Current encoding setting; Encoding encodingSetting_ = ENCODING_AUTO; QString encoding_text_; }; #endif glogg-1.1.4/src/overview.h0000644000175000017500000000770713076461374014440 0ustar nickonicko/* * Copyright (C) 2011, 2012 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef OVERVIEW_H #define OVERVIEW_H #include #include class LogFilteredData; // Class implementing the logic behind the matches overview bar. // This class converts the matches found in a LogFilteredData in // a screen dependent set of coloured lines, which is cached. // This class is not a UI class, actual display is left to the client. // // This class is NOT thread-safe. class Overview { public: // A line with a position in pixel and a weight (darkness) class WeightedLine { public: static const int WEIGHT_STEPS = 3; WeightedLine() { pos_ = 0; weight_ = 0; } // (Necessary for QVector) WeightedLine( int pos ) { pos_ = pos; weight_ = 0; } int position() const { return pos_; } int weight() const { return weight_; } void load() { weight_ = qMin( weight_ + 1, WEIGHT_STEPS - 1 ); } private: int pos_; int weight_; }; Overview(); ~Overview(); // Associate the passed filteredData to this Overview void setFilteredData( const LogFilteredData* logFilteredData ); // Signal the overview its attached LogFilteredData has been changed and // the overview must be updated with the provided total number // of line of the file. void updateData( int totalNbLine ); // Set the visibility flag of this overview. void setVisible( bool visible ) { visible_ = visible; dirty_ = visible; } // Update the current position in the file (to draw the view line) void updateCurrentPosition( int firstLine, int lastLine ) { topLine_ = firstLine; nbLines_ = lastLine - firstLine; } // Returns weither this overview is visible. bool isVisible() { return visible_; } // Signal the overview the height of the display has changed, triggering // an update of its cache. void updateView( int height ); // Returns a list of lines (between 0 and 'height') representing matches. // (pointer returned is valid until next call to update*() const QVector* getMatchLines() const; // Returns a list of lines (between 0 and 'height') representing marks. // (pointer returned is valid until next call to update*() const QVector* getMarkLines() const; // Return a pair of lines (between 0 and 'height') representing the current view. std::pair getViewLines() const; // Return the line number corresponding to the passed overview y coordinate. int fileLineFromY( int y ) const; // Return the y coordinate corresponding to the passed line number. int yFromFileLine( int file_line ) const; private: // List of matches associated with this Overview. const LogFilteredData* logFilteredData_; // Total number of lines in the file. int linesInFile_; // Whether the overview is visible. bool visible_; // First and last line currently viewed. int topLine_; int nbLines_; // Current height of view window. int height_; // Does the cache (matchesLines, markLines) need to be recalculated. int dirty_; // List of lines representing matches and marks (are shared with the client) QVector matchLines_; QVector markLines_; void recalculatesLines(); }; #endif glogg-1.1.4/src/selection.cpp0000644000175000017500000001302713076461374015102 0ustar nickonicko/* * Copyright (C) 2010, 2013 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ // This file implements Selection. // This class implements the selection handling. No check is made on // the validity of the selection, it must be handled by the caller. // There are three types of selection, only one type might be active // at any time. #include "selection.h" #include "data/abstractlogdata.h" Selection::Selection() { selectedLine_ = -1; selectedPartial_.line = -1; selectedPartial_.startColumn = 0; selectedPartial_.endColumn = 0; selectedRange_.startLine = -1; selectedRange_.endLine = 0; } void Selection::selectPortion( int line, int start_column, int end_column ) { // First unselect any whole line or range selectedLine_ = -1; selectedRange_.startLine = -1; selectedPartial_.line = line; selectedPartial_.startColumn = qMin ( start_column, end_column ); selectedPartial_.endColumn = qMax ( start_column, end_column ); } void Selection::selectRange( int start_line, int end_line ) { // First unselect any whole line and portion selectedLine_ = -1; selectedPartial_.line = -1; selectedRange_.startLine = qMin ( start_line, end_line ); selectedRange_.endLine = qMax ( start_line, end_line ); selectedRange_.firstLine = start_line; } void Selection::selectRangeFromPrevious( int line ) { int previous_line; if ( selectedLine_ >= 0 ) previous_line = selectedLine_; else if ( selectedRange_.startLine >= 0 ) previous_line = selectedRange_.firstLine; else if ( selectedPartial_.line >= 0 ) previous_line = selectedPartial_.line; else previous_line = 0; selectRange( previous_line, line ); } void Selection::crop( int last_line ) { if ( selectedLine_ > last_line ) selectedLine_ = -1; if ( selectedPartial_.line > last_line ) selectedPartial_.line = -1; if ( selectedRange_.endLine > last_line ) selectedRange_.endLine = last_line; if ( selectedRange_.startLine > last_line ) selectedRange_.startLine = last_line; }; bool Selection::getPortionForLine( int line, int* start_column, int* end_column ) const { if ( selectedPartial_.line == line ) { *start_column = selectedPartial_.startColumn; *end_column = selectedPartial_.endColumn; return true; } else { return false; } } bool Selection::isLineSelected( int line ) const { if ( line == selectedLine_ ) return true; else if ( selectedRange_.startLine >= 0 ) return ( ( line >= selectedRange_.startLine ) && ( line <= selectedRange_.endLine ) ); else return false; } qint64 Selection::selectedLine() const { return selectedLine_; } QList Selection::getLines() const { QList selection; if ( selectedLine_ >= 0 ) selection.append( selectedLine_ ); else if ( selectedPartial_.line >= 0 ) selection.append( selectedPartial_.line ); else if ( selectedRange_.startLine >= 0 ) for ( int i = selectedRange_.startLine; i <= selectedRange_.endLine; i++ ) selection.append( i ); return selection; } // The tab behaviour is a bit odd at the moment, full lines are not expanded // but partials (part of line) are, they probably should not ideally. QString Selection::getSelectedText( const AbstractLogData* logData ) const { QString text; if ( selectedLine_ >= 0 ) { text = logData->getLineString( selectedLine_ ); } else if ( selectedPartial_.line >= 0 ) { text = logData->getExpandedLineString( selectedPartial_.line ). mid( selectedPartial_.startColumn, ( selectedPartial_.endColumn - selectedPartial_.startColumn ) + 1 ); } else if ( selectedRange_.startLine >= 0 ) { QStringList list = logData->getLines( selectedRange_.startLine, selectedRange_.endLine - selectedRange_.startLine + 1 ); text = list.join( "\n" ); } return text; } FilePosition Selection::getNextPosition() const { qint64 line = 0; int column = 0; if ( selectedLine_ >= 0 ) { line = selectedLine_ + 1; } else if ( selectedRange_.startLine >= 0 ) { line = selectedRange_.endLine + 1; } else if ( selectedPartial_.line >= 0 ) { line = selectedPartial_.line; column = selectedPartial_.endColumn + 1; } return FilePosition( line, column ); } FilePosition Selection::getPreviousPosition() const { qint64 line = 0; int column = 0; if ( selectedLine_ >= 0 ) { line = selectedLine_; } else if ( selectedRange_.startLine >= 0 ) { line = selectedRange_.startLine; } else if ( selectedPartial_.line >= 0 ) { line = selectedPartial_.line; column = qMax( selectedPartial_.startColumn - 1, 0 ); } return FilePosition( line, column ); } glogg-1.1.4/src/data/0000755000175000017500000000000013076461374013317 5ustar nickonickoglogg-1.1.4/src/data/linepositionarray.h0000644000175000017500000000630413076461374017246 0ustar nickonicko/* * Copyright (C) 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef LINEPOSITIONARRAY_H #define LINEPOSITIONARRAY_H #include #include "data/compressedlinestorage.h" typedef std::vector SimpleLinePositionStorage; // This class is a list of end of lines position, // in addition to a list of uint64_t (positions within the files) // it can keep track of whether the final LF was added (for non-LF terminated // files) and remove it when more data are added. template class LinePosition { public: template friend class LinePosition; // Default constructor LinePosition() : array() { fakeFinalLF_ = false; } // Copy constructor (slow: deleted) LinePosition( const LinePosition& orig ) = delete; // Move assignement LinePosition& operator=( LinePosition&& orig ) { array = std::move( orig.array ); fakeFinalLF_ = orig.fakeFinalLF_; return *this; } // Add a new line position at the given position // Invariant: pos must be greater than the previous one // (this is NOT checked!) inline void append( uint64_t pos ) { if ( fakeFinalLF_ ) array.pop_back(); array.push_back( pos ); fakeFinalLF_ = false; } // Size of the array inline int size() const { return array.size(); } // Extract an element inline uint64_t at( int i ) const { return array.at( i ); } inline uint64_t operator[]( int i ) const { return array.at( i ); } // Set the presence of a fake final LF // Must be used after 'append'-ing a fake LF at the end. void setFakeFinalLF( bool finalLF=true ) { fakeFinalLF_ = finalLF; } // Add another list to this one, removing any fake LF on this list. // Invariant: all pos in other must be greater than any pos in this // (this is NOT checked!) void append_list( const LinePosition& other ) { // If our final LF is fake, we remove it if ( fakeFinalLF_ ) this->array.pop_back(); // Append the arrays this->array.append_list( other.array ); //array += other.array; // In case the 'other' object has a fake LF this->fakeFinalLF_ = other.fakeFinalLF_; } private: Storage array; bool fakeFinalLF_; }; // Use the non-optimised storage typedef LinePosition FastLinePositionArray; //typedef LinePosition LinePositionArray; typedef LinePosition LinePositionArray; #endif glogg-1.1.4/src/data/compressedlinestorage.cpp0000644000175000017500000003327013076461374020431 0ustar nickonicko/* * Copyright (C) 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include #include #include "utils.h" #include "data/compressedlinestorage.h" #ifdef HAVE_HTONS # include #else # define htons(a) glogg_htons(a) #endif namespace { // Functions to manipulate blocks // Create a new 32 bits block of the passed size, // initialised at the passed position char* block32_new( int block_size, uint32_t initial_position, char** block_ptr ) { // malloc a block of the maximum possible size // (where every line is >16384) char* ptr = static_cast( malloc( 4 + block_size * 6 ) ); if ( ptr ) { // Write the initial_position *(reinterpret_cast(ptr)) = initial_position; *block_ptr = ptr + 4; } return ptr; } // Create a new 64 bits block of the passed size, // initialised at the passed position char* block64_new( int block_size, uint64_t initial_position, char** block_ptr ) { // malloc a block of the maximum possible size // (where every line is >16384) char* ptr = static_cast( malloc( 8 + block_size * 10 ) ); if ( ptr ) { // Write the initial_position *(reinterpret_cast(ptr)) = initial_position; *block_ptr = ptr + 8; } return ptr; } // Add a one byte relative delta (0-127) and inc pointer // First bit is always 0 void block_add_one_byte_relative( char** ptr, uint8_t value ) { **ptr = value; *ptr += sizeof( value ); } // Add a two bytes relative delta (0-16383) and inc pointer // First 2 bits are always 10 void block_add_two_bytes_relative( char** ptr, uint16_t value ) { // Stored in big endian format in order to recognise the initial pattern: // 10xx xxxx xxxx xxxx // HO byte | LO byte *(reinterpret_cast(*ptr)) = htons( value | (1 << 15) ); *ptr += sizeof( value ); } // Add a signal and a 4 bytes absolute position and inc pointer void block32_add_absolute( char** ptr, uint32_t value ) { // 2 bytes marker (actually only the first two bits are tested) *(reinterpret_cast(*ptr)) = 0xFF; *ptr += sizeof( uint16_t ); // Absolute value (machine endian) *(reinterpret_cast(*ptr)) = value; *ptr += sizeof( uint32_t ); } // Initialise the passed block for reading, returning // the initial position and a pointer to the second entry. uint64_t block32_initial_pos( char* block, char** ptr ) { *ptr = block + sizeof( uint32_t ); return *(reinterpret_cast(block)); } // Give the next position in the block based on the previous // position, then increase the pointer. uint64_t block32_next_pos( char** ptr, uint64_t previous_pos ) { uint64_t pos = previous_pos; uint8_t byte = **ptr; ++(*ptr); if ( ! ( byte & 0x80 ) ) { // High order bit is 0 pos += byte; } else if ( ( byte & 0xC0 ) == 0x80 ) { // We need to read the low order byte uint8_t lo_byte = **ptr; ++(*ptr); // Remove the starting 10b byte &= ~0xC0; // And form the displacement (stored as big endian) pos += ( (uint16_t) byte << 8 ) | (uint16_t) lo_byte; } else { // Skip the next byte (not used) ++(*ptr); // And read the new absolute pos (machine endian) pos = *(reinterpret_cast(*ptr)); *ptr += sizeof( uint32_t ); } return pos; } // Add a signal and a 8 bytes absolute position and inc pointer void block64_add_absolute( char** ptr, uint64_t value ) { // This is unaligned, can cause problem on some CPUs // 2 bytes marker (actually only the first two bits are tested) *(reinterpret_cast(*ptr)) = 0xFF; *ptr += sizeof( uint16_t ); // Absolute value (machine endian) *(reinterpret_cast(*ptr)) = value; *ptr += sizeof( uint64_t ); } // Initialise the passed block for reading, returning // the initial position and a pointer to the second entry. uint64_t block64_initial_pos( char* block, char** ptr ) { *ptr = block + sizeof( uint64_t ); return *(reinterpret_cast(block)); } // Give the next position in the block based on the previous // position, then increase the pointer. uint64_t block64_next_pos( char** ptr, uint64_t previous_pos ) { uint64_t pos = previous_pos; uint8_t byte = **ptr; ++(*ptr); if ( ! ( byte & 0x80 ) ) { // High order bit is 0 pos += byte; } else if ( ( byte & 0xC0 ) == 0x80 ) { // We need to read the low order byte uint8_t lo_byte = **ptr; ++(*ptr); // Remove the starting 10b byte &= ~0xC0; // And form the displacement (stored as big endian) pos += ( (uint16_t) byte << 8 ) | (uint16_t) lo_byte; } else { // Skip the next byte (not used) ++(*ptr); // And read the new absolute pos (machine endian) pos = *(reinterpret_cast(*ptr)); *ptr += sizeof( uint64_t ); } return pos; } } void CompressedLinePositionStorage::move_from( CompressedLinePositionStorage&& orig ) { nb_lines_ = orig.nb_lines_; first_long_line_ = orig.first_long_line_; current_pos_ = orig.current_pos_; block_pointer_ = orig.block_pointer_; previous_block_pointer_ = orig.previous_block_pointer_; orig.nb_lines_ = 0; } // Move constructor CompressedLinePositionStorage::CompressedLinePositionStorage( CompressedLinePositionStorage&& orig ) : block32_index_( std::move( orig.block32_index_ ) ), block64_index_( std::move( orig.block64_index_ ) ) { move_from( std::move( orig ) ); } void CompressedLinePositionStorage::free_blocks() { for ( char* block : block32_index_ ) { void* p = static_cast( block ); free( p ); } for ( char* block : block64_index_ ) { void* p = static_cast( block ); free( p ); } } // Move assignement CompressedLinePositionStorage& CompressedLinePositionStorage::operator=( CompressedLinePositionStorage&& orig ) { free_blocks(); block32_index_ = std::move( orig.block32_index_ ); block64_index_ = std::move( orig.block64_index_ ); move_from( std::move( orig ) ); return *this; } CompressedLinePositionStorage::~CompressedLinePositionStorage() { free_blocks(); } // template void CompressedLinePositionStorage::append( uint64_t pos ) { // Lines must be stored in order assert( ( pos > current_pos_ ) || ( pos == 0 ) ); // Save the pointer in case we need to "pop_back" previous_block_pointer_ = block_pointer_; bool store_in_big = false; if ( pos > UINT32_MAX ) { store_in_big = true; if ( first_long_line_ == UINT32_MAX ) { // First "big" end of line, we will start a new (64) block first_long_line_ = nb_lines_; block_pointer_ = nullptr; } } if ( ! block_pointer_ ) { // We need to start a new block if ( ! store_in_big ) block32_index_.push_back( block32_new( BLOCK_SIZE, pos, &block_pointer_ ) ); else block64_index_.push_back( block64_new( BLOCK_SIZE, pos, &block_pointer_ ) ); } else { uint64_t delta = pos - current_pos_; if ( delta < 128 ) { // Code relative on one byte block_add_one_byte_relative( &block_pointer_, delta ); } else if ( delta < 16384 ) { // Code relative on two bytes block_add_two_bytes_relative( &block_pointer_, delta ); } else { // Code absolute if ( ! store_in_big ) block32_add_absolute( &block_pointer_, pos ); else block64_add_absolute( &block_pointer_, pos ); } } current_pos_ = pos; ++nb_lines_; if ( ! store_in_big ) { if ( nb_lines_ % BLOCK_SIZE == 0 ) { // We have finished the block // Let's reduce its size to what is actually used int block_index = nb_lines_ / BLOCK_SIZE - 1; char* block = block32_index_[block_index]; // We allocate extra space for the last element in case it // is replaced by an absolute value in the future (following a pop_back) size_t new_size = ( previous_block_pointer_ + sizeof( uint16_t ) + sizeof( uint32_t ) ) - block; void* new_location = realloc( block, new_size ); if ( new_location ) block32_index_[block_index] = static_cast( new_location ); block_pointer_ = nullptr; } } else { if ( ( nb_lines_ - first_long_line_ ) % BLOCK_SIZE == 0 ) { // We have finished the block // Let's reduce its size to what is actually used int block_index = ( nb_lines_ - first_long_line_ ) / BLOCK_SIZE - 1; char* block = block64_index_[block_index]; // We allocate extra space for the last element in case it // is replaced by an absolute value in the future (following a pop_back) size_t new_size = ( previous_block_pointer_ + sizeof( uint16_t ) + sizeof( uint64_t ) ) - block; void* new_location = realloc( block, new_size ); if ( new_location ) block64_index_[block_index] = static_cast( new_location ); block_pointer_ = nullptr; } } } // template uint64_t CompressedLinePositionStorage::at( uint32_t index ) const { char* ptr; uint64_t position; Cache* last_read = last_read_.getPtr(); if ( index < first_long_line_ ) { if ( ( index == last_read->index + 1 ) && ( index % BLOCK_SIZE != 0 ) ) { position = last_read->position; ptr = last_read->ptr; position = block32_next_pos( &ptr, position ); } else { char* block = block32_index_[ index / BLOCK_SIZE ]; position = block32_initial_pos( block, &ptr ); for ( uint32_t i = 0; i < index % BLOCK_SIZE; i++ ) { // Go through all the lines in the block till the one we want position = block32_next_pos( &ptr, position ); } } } else { const uint32_t index_in_64 = index - first_long_line_; if ( ( index == last_read->index + 1 ) && ( index_in_64 % BLOCK_SIZE != 0 ) ) { position = last_read->position; ptr = last_read->ptr; position = block64_next_pos( &ptr, position ); } else { char* block = block64_index_[ index_in_64 / BLOCK_SIZE ]; position = block64_initial_pos( block, &ptr ); for ( uint32_t i = 0; i < index_in_64 % BLOCK_SIZE; i++ ) { // Go through all the lines in the block till the one we want position = block64_next_pos( &ptr, position ); } } } // Populate our cache ready for next consecutive read last_read->index = index; last_read->position = position; last_read->ptr = ptr; return position; } void CompressedLinePositionStorage::append_list( const std::vector& positions ) { // This is not very clever, but caching should make it // reasonably fast. for ( uint32_t i = 0; i < positions.size(); i++ ) append( positions.at( i ) ); } // template void CompressedLinePositionStorage::pop_back() { // Removing the last entered data, there are two cases if ( previous_block_pointer_ ) { // The last append was a normal entry in an existing block, // so we can just revert the pointer block_pointer_ = previous_block_pointer_; previous_block_pointer_ = nullptr; } else { // A new block has been created for the last entry, we need // to de-alloc it. if ( first_long_line_ == UINT32_MAX ) { // If we try to pop_back() twice, we're dead! assert( ( nb_lines_ - 1 ) % BLOCK_SIZE == 0 ); char* block = block32_index_.back(); block32_index_.pop_back(); free( block ); } else { // If we try to pop_back() twice, we're dead! assert( ( nb_lines_ - first_long_line_ - 1 ) % BLOCK_SIZE == 0 ); char* block = block64_index_.back(); block64_index_.pop_back(); free( block ); } block_pointer_ = nullptr; } --nb_lines_; current_pos_ = at( nb_lines_ - 1 ); } glogg-1.1.4/src/data/logdataworkerthread.cpp0000644000175000017500000002416513076461374020070 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2014, 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include #include "log.h" #include "logdata.h" #include "logdataworkerthread.h" // Size of the chunk to read (5 MiB) const int IndexOperation::sizeChunk = 5*1024*1024; qint64 IndexingData::getSize() const { QMutexLocker locker( &dataMutex_ ); return indexedSize_; } int IndexingData::getMaxLength() const { QMutexLocker locker( &dataMutex_ ); return maxLength_; } LineNumber IndexingData::getNbLines() const { QMutexLocker locker( &dataMutex_ ); return linePosition_.size(); } qint64 IndexingData::getPosForLine( LineNumber line ) const { QMutexLocker locker( &dataMutex_ ); return linePosition_.at( line ); } EncodingSpeculator::Encoding IndexingData::getEncodingGuess() const { QMutexLocker locker( &dataMutex_ ); return encoding_; } void IndexingData::addAll( qint64 size, int length, const FastLinePositionArray& linePosition, EncodingSpeculator::Encoding encoding ) { QMutexLocker locker( &dataMutex_ ); indexedSize_ += size; maxLength_ = qMax( maxLength_, length ); linePosition_.append_list( linePosition ); encoding_ = encoding; } void IndexingData::clear() { maxLength_ = 0; indexedSize_ = 0; linePosition_ = LinePositionArray(); encoding_ = EncodingSpeculator::Encoding::ASCII7; } LogDataWorkerThread::LogDataWorkerThread( IndexingData* indexing_data ) : QThread(), mutex_(), operationRequestedCond_(), nothingToDoCond_(), fileName_(), indexing_data_( indexing_data ) { terminate_ = false; interruptRequested_ = false; operationRequested_ = NULL; } LogDataWorkerThread::~LogDataWorkerThread() { { QMutexLocker locker( &mutex_ ); terminate_ = true; operationRequestedCond_.wakeAll(); } wait(); } void LogDataWorkerThread::attachFile( const QString& fileName ) { QMutexLocker locker( &mutex_ ); // to protect fileName_ fileName_ = fileName; } void LogDataWorkerThread::indexAll() { QMutexLocker locker( &mutex_ ); // to protect operationRequested_ LOG(logDEBUG) << "FullIndex requested"; // If an operation is ongoing, we will block while ( (operationRequested_ != NULL) ) nothingToDoCond_.wait( &mutex_ ); interruptRequested_ = false; operationRequested_ = new FullIndexOperation( fileName_, indexing_data_, &interruptRequested_, &encodingSpeculator_ ); operationRequestedCond_.wakeAll(); } void LogDataWorkerThread::indexAdditionalLines() { QMutexLocker locker( &mutex_ ); // to protect operationRequested_ LOG(logDEBUG) << "AddLines requested"; // If an operation is ongoing, we will block while ( (operationRequested_ != NULL) ) nothingToDoCond_.wait( &mutex_ ); interruptRequested_ = false; operationRequested_ = new PartialIndexOperation( fileName_, indexing_data_, &interruptRequested_, &encodingSpeculator_ ); operationRequestedCond_.wakeAll(); } void LogDataWorkerThread::interrupt() { LOG(logDEBUG) << "Load interrupt requested"; // No mutex here, setting a bool is probably atomic! interruptRequested_ = true; } // This is the thread's main loop void LogDataWorkerThread::run() { QMutexLocker locker( &mutex_ ); forever { while ( (terminate_ == false) && (operationRequested_ == NULL) ) operationRequestedCond_.wait( &mutex_ ); LOG(logDEBUG) << "Worker thread signaled"; // Look at what needs to be done if ( terminate_ ) return; // We must die if ( operationRequested_ ) { connect( operationRequested_, SIGNAL( indexingProgressed( int ) ), this, SIGNAL( indexingProgressed( int ) ) ); // Run the operation try { if ( operationRequested_->start() ) { LOG(logDEBUG) << "... finished copy in workerThread."; emit indexingFinished( LoadingStatus::Successful ); } else { emit indexingFinished( LoadingStatus::Interrupted ); } } catch ( std::bad_alloc& ba ) { LOG(logERROR) << "Out of memory whilst indexing!"; emit indexingFinished( LoadingStatus::NoMemory ); } delete operationRequested_; operationRequested_ = NULL; nothingToDoCond_.wakeAll(); } } } // // Operations implementation // IndexOperation::IndexOperation( const QString& fileName, IndexingData* indexingData, bool* interruptRequest, EncodingSpeculator* encodingSpeculator ) : fileName_( fileName ) { interruptRequest_ = interruptRequest; indexing_data_ = indexingData; encoding_speculator_ = encodingSpeculator; } void IndexOperation::doIndex( IndexingData* indexing_data, EncodingSpeculator* encoding_speculator, qint64 initialPosition ) { qint64 pos = initialPosition; // Absolute position of the start of current line qint64 end = 0; // Absolute position of the end of current line int additional_spaces = 0; // Additional spaces due to tabs QFile file( fileName_ ); if ( file.open( QIODevice::ReadOnly ) ) { // Count the number of lines and max length // (read big chunks to speed up reading from disk) file.seek( pos ); while ( !file.atEnd() ) { FastLinePositionArray line_positions; int max_length = 0; if ( *interruptRequest_ ) // a bool is always read/written atomically isn't it? break; // Read a chunk of 5MB const qint64 block_beginning = file.pos(); const QByteArray block = file.read( sizeChunk ); // Count the number of lines in each chunk qint64 pos_within_block = 0; while ( pos_within_block != -1 ) { pos_within_block = qMax( pos - block_beginning, 0LL); // Looking for the next \n, expanding tabs in the process do { if ( pos_within_block < block.length() ) { const char c = block.at(pos_within_block); encoding_speculator->inject_byte( c ); if ( c == '\n' ) break; else if ( c == '\t' ) additional_spaces += AbstractLogData::tabStop - ( ( ( block_beginning - pos ) + pos_within_block + additional_spaces ) % AbstractLogData::tabStop ) - 1; pos_within_block++; } else { pos_within_block = -1; } } while ( pos_within_block != -1 ); // When a end of line has been found... if ( pos_within_block != -1 ) { end = pos_within_block + block_beginning; const int length = end-pos + additional_spaces; if ( length > max_length ) max_length = length; pos = end + 1; additional_spaces = 0; line_positions.append( pos ); } } // Update the shared data indexing_data->addAll( block.length(), max_length, line_positions, encoding_speculator->guess() ); // Update the caller for progress indication int progress = ( file.size() > 0 ) ? pos*100 / file.size() : 100; emit indexingProgressed( progress ); } // Check if there is a non LF terminated line at the end of the file qint64 file_size = file.size(); if ( !*interruptRequest_ && file_size > pos ) { LOG( logWARNING ) << "Non LF terminated file, adding a fake end of line"; FastLinePositionArray line_position; line_position.append( file_size + 1 ); line_position.setFakeFinalLF(); indexing_data->addAll( 0, 0, line_position, encoding_speculator->guess() ); } } else { // TODO: Check that the file is seekable? // If the file cannot be open, we do as if it was empty LOG(logWARNING) << "Cannot open file " << fileName_.toStdString(); emit indexingProgressed( 100 ); } } // Called in the worker thread's context bool FullIndexOperation::start() { LOG(logDEBUG) << "FullIndexOperation::start(), file " << fileName_.toStdString(); LOG(logDEBUG) << "FullIndexOperation: Starting the count..."; emit indexingProgressed( 0 ); // First empty the index indexing_data_->clear(); doIndex( indexing_data_, encoding_speculator_, 0 ); LOG(logDEBUG) << "FullIndexOperation: ... finished counting." "interrupt = " << *interruptRequest_; return ( *interruptRequest_ ? false : true ); } bool PartialIndexOperation::start() { LOG(logDEBUG) << "PartialIndexOperation::start(), file " << fileName_.toStdString(); qint64 initial_position = indexing_data_->getSize(); LOG(logDEBUG) << "PartialIndexOperation: Starting the count at " << initial_position << " ..."; emit indexingProgressed( 0 ); doIndex( indexing_data_, encoding_speculator_, initial_position ); LOG(logDEBUG) << "PartialIndexOperation: ... finished counting."; return ( *interruptRequest_ ? false : true ); } glogg-1.1.4/src/data/abstractlogdata.cpp0000644000175000017500000000435713076461374017173 0ustar nickonicko/* * Copyright (C) 2009, 2010 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ // This file implements AbstractLogData. // This base class is primarily an interface class and should not // implement anything. // It exists so that AbstractLogView can manipulate an abtract set of data // (either full or filtered). #include "abstractlogdata.h" AbstractLogData::AbstractLogData() { } // Simple wrapper in order to use a clean Template Method QString AbstractLogData::getLineString( qint64 line ) const { return doGetLineString(line); } // Simple wrapper in order to use a clean Template Method QString AbstractLogData::getExpandedLineString( qint64 line ) const { return doGetExpandedLineString(line); } // Simple wrapper in order to use a clean Template Method QStringList AbstractLogData::getLines( qint64 first_line, int number ) const { return doGetLines( first_line, number ); } // Simple wrapper in order to use a clean Template Method QStringList AbstractLogData::getExpandedLines( qint64 first_line, int number ) const { return doGetExpandedLines( first_line, number ); } // Simple wrapper in order to use a clean Template Method qint64 AbstractLogData::getNbLine() const { return doGetNbLine(); } // Simple wrapper in order to use a clean Template Method int AbstractLogData::getMaxLength() const { return doGetMaxLength(); } // Simple wrapper in order to use a clean Template Method int AbstractLogData::getLineLength( qint64 line ) const { return doGetLineLength( line ); } void AbstractLogData::setDisplayEncoding( const char* encoding ) { doSetDisplayEncoding( encoding ); } glogg-1.1.4/src/data/logfiltereddataworkerthread.cpp0000644000175000017500000002024613076461374021603 0ustar nickonicko/* * Copyright (C) 2009, 2010 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include #include "log.h" #include "logfiltereddataworkerthread.h" #include "logdata.h" // Number of lines in each chunk to read const int SearchOperation::nbLinesInChunk = 5000; void SearchData::getAll( int* length, SearchResultArray* matches, qint64* lines) const { QMutexLocker locker( &dataMutex_ ); *length = maxLength_; *lines = nbLinesProcessed_; // This is a copy (potentially slow) *matches = matches_; } void SearchData::setAll( int length, SearchResultArray&& matches ) { QMutexLocker locker( &dataMutex_ ); maxLength_ = length; matches_ = matches; } void SearchData::addAll( int length, const SearchResultArray& matches, LineNumber lines ) { QMutexLocker locker( &dataMutex_ ); maxLength_ = qMax( maxLength_, length ); nbLinesProcessed_ = lines; // This does a copy as we want the final array to be // linear. matches_.insert( std::end( matches_ ), std::begin( matches ), std::end( matches ) ); } LineNumber SearchData::getNbMatches() const { QMutexLocker locker( &dataMutex_ ); return matches_.size(); } // This function starts searching from the end since we use it // to remove the final match. void SearchData::deleteMatch( LineNumber line ) { QMutexLocker locker( &dataMutex_ ); SearchResultArray::iterator i = matches_.end(); while ( i != matches_.begin() ) { i--; const LineNumber this_line = i->lineNumber(); if ( this_line == line ) { matches_.erase(i); break; } // Exit if we have passed the line number to look for. if ( this_line < line ) break; } } void SearchData::clear() { QMutexLocker locker( &dataMutex_ ); maxLength_ = 0; nbLinesProcessed_ = 0; matches_.clear(); } LogFilteredDataWorkerThread::LogFilteredDataWorkerThread( const LogData* sourceLogData ) : QThread(), mutex_(), operationRequestedCond_(), nothingToDoCond_(), searchData_() { terminate_ = false; interruptRequested_ = false; operationRequested_ = NULL; sourceLogData_ = sourceLogData; } LogFilteredDataWorkerThread::~LogFilteredDataWorkerThread() { { QMutexLocker locker( &mutex_ ); terminate_ = true; operationRequestedCond_.wakeAll(); } wait(); } void LogFilteredDataWorkerThread::search( const QRegExp& regExp ) { QMutexLocker locker( &mutex_ ); // to protect operationRequested_ LOG(logDEBUG) << "Search requested"; // If an operation is ongoing, we will block while ( (operationRequested_ != NULL) ) nothingToDoCond_.wait( &mutex_ ); interruptRequested_ = false; operationRequested_ = new FullSearchOperation( sourceLogData_, regExp, &interruptRequested_ ); operationRequestedCond_.wakeAll(); } void LogFilteredDataWorkerThread::updateSearch( const QRegExp& regExp, qint64 position ) { QMutexLocker locker( &mutex_ ); // to protect operationRequested_ LOG(logDEBUG) << "Search requested"; // If an operation is ongoing, we will block while ( (operationRequested_ != NULL) ) nothingToDoCond_.wait( &mutex_ ); interruptRequested_ = false; operationRequested_ = new UpdateSearchOperation( sourceLogData_, regExp, &interruptRequested_, position ); operationRequestedCond_.wakeAll(); } void LogFilteredDataWorkerThread::interrupt() { LOG(logDEBUG) << "Search interruption requested"; // No mutex here, setting a bool is probably atomic! interruptRequested_ = true; // We wait for the interruption to be done { QMutexLocker locker( &mutex_ ); while ( (operationRequested_ != NULL) ) nothingToDoCond_.wait( &mutex_ ); } } // This will do an atomic copy of the object void LogFilteredDataWorkerThread::getSearchResult( int* maxLength, SearchResultArray* searchMatches, qint64* nbLinesProcessed ) { searchData_.getAll( maxLength, searchMatches, nbLinesProcessed ); } // This is the thread's main loop void LogFilteredDataWorkerThread::run() { QMutexLocker locker( &mutex_ ); forever { while ( (terminate_ == false) && (operationRequested_ == NULL) ) operationRequestedCond_.wait( &mutex_ ); LOG(logDEBUG) << "Worker thread signaled"; // Look at what needs to be done if ( terminate_ ) return; // We must die if ( operationRequested_ ) { connect( operationRequested_, SIGNAL( searchProgressed( int, int ) ), this, SIGNAL( searchProgressed( int, int ) ) ); // Run the search operation operationRequested_->start( searchData_ ); LOG(logDEBUG) << "... finished copy in workerThread."; emit searchFinished(); delete operationRequested_; operationRequested_ = NULL; nothingToDoCond_.wakeAll(); } } } // // Operations implementation // SearchOperation::SearchOperation( const LogData* sourceLogData, const QRegExp& regExp, bool* interruptRequest ) : regexp_( regExp ), sourceLogData_( sourceLogData ) { interruptRequested_ = interruptRequest; } void SearchOperation::doSearch( SearchData& searchData, qint64 initialLine ) { const qint64 nbSourceLines = sourceLogData_->getNbLine(); int maxLength = 0; int nbMatches = searchData.getNbMatches(); SearchResultArray currentList = SearchResultArray(); // Ensure no re-alloc will be done currentList.reserve( nbLinesInChunk ); LOG(logDEBUG) << "Searching from line " << initialLine << " to " << nbSourceLines; for ( qint64 i = initialLine; i < nbSourceLines; i += nbLinesInChunk ) { if ( *interruptRequested_ ) break; const int percentage = ( i - initialLine ) * 100 / ( nbSourceLines - initialLine ); emit searchProgressed( nbMatches, percentage ); const QStringList lines = sourceLogData_->getLines( i, qMin( nbLinesInChunk, (int) ( nbSourceLines - i ) ) ); LOG(logDEBUG) << "Chunk starting at " << i << ", " << lines.size() << " lines read."; int j = 0; for ( ; j < lines.size(); j++ ) { if ( regexp_.indexIn( lines[j] ) != -1 ) { // FIXME: increase perf by removing temporary const int length = sourceLogData_->getExpandedLineString(i+j).length(); if ( length > maxLength ) maxLength = length; currentList.push_back( MatchingLine( i+j ) ); nbMatches++; } } // After each block, copy the data to shared data // and update the client searchData.addAll( maxLength, currentList, i+j ); currentList.clear(); } emit searchProgressed( nbMatches, 100 ); } // Called in the worker thread's context void FullSearchOperation::start( SearchData& searchData ) { // Clear the shared data searchData.clear(); doSearch( searchData, 0 ); } // Called in the worker thread's context void UpdateSearchOperation::start( SearchData& searchData ) { qint64 initial_line = initialPosition_; if ( initial_line >= 1 ) { // We need to re-search the last line because it might have // been updated (if it was not LF-terminated) --initial_line; // In case the last line matched, we don't want it to match twice. searchData.deleteMatch( initial_line ); } doSearch( searchData, initial_line ); } glogg-1.1.4/src/data/logfiltereddata.cpp0000644000175000017500000002637113076461374017166 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2011, 2012, 2013 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ // This file implements LogFilteredData // It stores a pointer to the LogData that created it, // so should always be destroyed before the LogData. #include "log.h" #include #include #include #include "utils.h" #include "logdata.h" #include "marks.h" #include "logfiltereddata.h" // Creates an empty set. It must be possible to display it without error. // FIXME LogFilteredData::LogFilteredData() : AbstractLogData(), matching_lines_(), currentRegExp_(), visibility_(), filteredItemsCache_(), workerThread_( nullptr ), marks_() { /* Prevent any more searching */ maxLength_ = 0; maxLengthMarks_ = 0; searchDone_ = true; visibility_ = MarksAndMatches; filteredItemsCacheDirty_ = true; } // Usual constructor: just copy the data, the search is started by runSearch() LogFilteredData::LogFilteredData( const LogData* logData ) : AbstractLogData(), matching_lines_( SearchResultArray() ), currentRegExp_(), visibility_(), filteredItemsCache_(), workerThread_( logData ), marks_() { // Starts with an empty result list maxLength_ = 0; maxLengthMarks_ = 0; nbLinesProcessed_ = 0; sourceLogData_ = logData; searchDone_ = false; visibility_ = MarksAndMatches; filteredItemsCacheDirty_ = true; // Forward the update signal connect( &workerThread_, SIGNAL( searchProgressed( int, int ) ), this, SLOT( handleSearchProgressed( int, int ) ) ); // Starts the worker thread workerThread_.start(); } LogFilteredData::~LogFilteredData() { // FIXME // workerThread_.stop(); } // // Public functions // // Run the search and send newDataAvailable() signals. void LogFilteredData::runSearch( const QRegExp& regExp ) { LOG(logDEBUG) << "Entering runSearch"; clearSearch(); currentRegExp_ = regExp; workerThread_.search( currentRegExp_ ); } void LogFilteredData::updateSearch() { LOG(logDEBUG) << "Entering updateSearch"; workerThread_.updateSearch( currentRegExp_, nbLinesProcessed_ ); } void LogFilteredData::interruptSearch() { LOG(logDEBUG) << "Entering interruptSearch"; workerThread_.interrupt(); } void LogFilteredData::clearSearch() { currentRegExp_ = QRegExp(); matching_lines_.clear(); maxLength_ = 0; maxLengthMarks_ = 0; nbLinesProcessed_ = 0; filteredItemsCacheDirty_ = true; } qint64 LogFilteredData::getMatchingLineNumber( int matchNum ) const { qint64 matchingLine = findLogDataLine( matchNum ); return matchingLine; } // Scan the list for the 'lineNumber' passed bool LogFilteredData::isLineInMatchingList( qint64 lineNumber ) { int index; // Not used return lookupLineNumber( matching_lines_, lineNumber, &index); } LineNumber LogFilteredData::getNbTotalLines() const { return sourceLogData_->getNbLine(); } LineNumber LogFilteredData::getNbMatches() const { return matching_lines_.size(); } LineNumber LogFilteredData::getNbMarks() const { return marks_.size(); } LogFilteredData::FilteredLineType LogFilteredData::filteredLineTypeByIndex( int index ) const { // If we are only showing one type, the line is there because // it is of this type. if ( visibility_ == MatchesOnly ) return Match; else if ( visibility_ == MarksOnly ) return Mark; else { // If it is MarksAndMatches, we have to look. // Regenerate the cache if needed if ( filteredItemsCacheDirty_ ) regenerateFilteredItemsCache(); return filteredItemsCache_[ index ].type(); } } // Delegation to our Marks object void LogFilteredData::addMark( qint64 line, QChar mark ) { if ( ( line >= 0 ) && ( line < sourceLogData_->getNbLine() ) ) { marks_.addMark( line, mark ); maxLengthMarks_ = qMax( maxLengthMarks_, sourceLogData_->getLineLength( line ) ); filteredItemsCacheDirty_ = true; } else LOG(logERROR) << "LogFilteredData::addMark\ trying to create a mark outside of the file."; } qint64 LogFilteredData::getMark( QChar mark ) const { return marks_.getMark( mark ); } bool LogFilteredData::isLineMarked( qint64 line ) const { return marks_.isLineMarked( line ); } void LogFilteredData::deleteMark( QChar mark ) { marks_.deleteMark( mark ); filteredItemsCacheDirty_ = true; // FIXME: maxLengthMarks_ } void LogFilteredData::deleteMark( qint64 line ) { marks_.deleteMark( line ); filteredItemsCacheDirty_ = true; // Now update the max length if needed if ( sourceLogData_->getLineLength( line ) >= maxLengthMarks_ ) { LOG(logDEBUG) << "deleteMark recalculating longest mark"; maxLengthMarks_ = 0; for ( Marks::const_iterator i = marks_.begin(); i != marks_.end(); ++i ) { LOG(logDEBUG) << "line " << i->lineNumber(); maxLengthMarks_ = qMax( maxLengthMarks_, sourceLogData_->getLineLength( i->lineNumber() ) ); } } } void LogFilteredData::clearMarks() { marks_.clear(); filteredItemsCacheDirty_ = true; maxLengthMarks_ = 0; } void LogFilteredData::setVisibility( Visibility visi ) { visibility_ = visi; } // // Slots // void LogFilteredData::handleSearchProgressed( int nbMatches, int progress ) { LOG(logDEBUG) << "LogFilteredData::handleSearchProgressed matches=" << nbMatches << " progress=" << progress; // searchDone_ = true; workerThread_.getSearchResult( &maxLength_, &matching_lines_, &nbLinesProcessed_ ); filteredItemsCacheDirty_ = true; emit searchProgressed( nbMatches, progress ); } LineNumber LogFilteredData::findLogDataLine( LineNumber lineNum ) const { LineNumber line = std::numeric_limits::max(); if ( visibility_ == MatchesOnly ) { if ( lineNum < matching_lines_.size() ) { line = matching_lines_[lineNum].lineNumber(); } else { LOG(logERROR) << "Index too big in LogFilteredData: " << lineNum; } } else if ( visibility_ == MarksOnly ) { if ( lineNum < marks_.size() ) line = marks_.getLineMarkedByIndex( lineNum ); else LOG(logERROR) << "Index too big in LogFilteredData: " << lineNum; } else { // Regenerate the cache if needed if ( filteredItemsCacheDirty_ ) regenerateFilteredItemsCache(); if ( lineNum < filteredItemsCache_.size() ) line = filteredItemsCache_[ lineNum ].lineNumber(); else LOG(logERROR) << "Index too big in LogFilteredData: " << lineNum; } return line; } // Implementation of the virtual function. QString LogFilteredData::doGetLineString( qint64 lineNum ) const { qint64 line = findLogDataLine( lineNum ); QString string = sourceLogData_->getLineString( line ); return string; } // Implementation of the virtual function. QString LogFilteredData::doGetExpandedLineString( qint64 lineNum ) const { qint64 line = findLogDataLine( lineNum ); QString string = sourceLogData_->getExpandedLineString( line ); return string; } // Implementation of the virtual function. QStringList LogFilteredData::doGetLines( qint64 first_line, int number ) const { QStringList list; for ( int i = first_line; i < first_line + number; i++ ) { list.append( doGetLineString( i ) ); } return list; } // Implementation of the virtual function. QStringList LogFilteredData::doGetExpandedLines( qint64 first_line, int number ) const { QStringList list; for ( int i = first_line; i < first_line + number; i++ ) { list.append( doGetExpandedLineString( i ) ); } return list; } // Implementation of the virtual function. qint64 LogFilteredData::doGetNbLine() const { qint64 nbLines; if ( visibility_ == MatchesOnly ) nbLines = matching_lines_.size(); else if ( visibility_ == MarksOnly ) nbLines = marks_.size(); else { // Regenerate the cache if needed (hopefully most of the time // it won't be necessarily) if ( filteredItemsCacheDirty_ ) regenerateFilteredItemsCache(); nbLines = filteredItemsCache_.size(); } return nbLines; } // Implementation of the virtual function. int LogFilteredData::doGetMaxLength() const { int max_length; if ( visibility_ == MatchesOnly ) max_length = maxLength_; else if ( visibility_ == MarksOnly ) max_length = maxLengthMarks_; else max_length = qMax( maxLength_, maxLengthMarks_ ); return max_length; } // Implementation of the virtual function. int LogFilteredData::doGetLineLength( qint64 lineNum ) const { qint64 line = findLogDataLine( lineNum ); return sourceLogData_->getExpandedLineString( line ).length(); } void LogFilteredData::doSetDisplayEncoding( const char* encoding ) { LOG(logDEBUG) << "AbstractLogData::setDisplayEncoding: " << encoding; } // TODO: We might be a bit smarter and not regenerate the whole thing when // e.g. stuff is added at the end of the search. void LogFilteredData::regenerateFilteredItemsCache() const { LOG(logDEBUG) << "regenerateFilteredItemsCache"; filteredItemsCache_.clear(); filteredItemsCache_.reserve( matching_lines_.size() + marks_.size() ); // (it's an overestimate but probably not by much so it's fine) auto i = matching_lines_.cbegin(); Marks::const_iterator j = marks_.begin(); while ( ( i != matching_lines_.cend() ) || ( j != marks_.end() ) ) { qint64 next_mark = ( j != marks_.end() ) ? j->lineNumber() : std::numeric_limits::max(); qint64 next_match = ( i != matching_lines_.cend() ) ? i->lineNumber() : std::numeric_limits::max(); // We choose a Mark over a Match if a line is both, just an arbitrary choice really. if ( next_mark <= next_match ) { // LOG(logDEBUG) << "Add mark at " << next_mark; filteredItemsCache_.push_back( FilteredItem( next_mark, Mark ) ); if ( j != marks_.end() ) ++j; if ( ( next_mark == next_match ) && ( i != matching_lines_.cend() ) ) ++i; // Case when it's both match and mark. } else { // LOG(logDEBUG) << "Add match at " << next_match; filteredItemsCache_.push_back( FilteredItem( next_match, Match ) ); if ( i != matching_lines_.cend() ) ++i; } } filteredItemsCacheDirty_ = false; LOG(logDEBUG) << "finished regenerateFilteredItemsCache"; } glogg-1.1.4/src/data/logdata.h0000644000175000017500000001440413076461374015106 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2013, 2014, 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef LOGDATA_H #define LOGDATA_H #include #include #include #include #include #include #include #include #include "utils.h" #include "abstractlogdata.h" #include "logdataworkerthread.h" #include "filewatcher.h" #include "loadingstatus.h" class LogFilteredData; // Thrown when trying to attach an already attached LogData class CantReattachErr {}; // Represents a complete set of data to be displayed (ie. a log file content) // This class is thread-safe. class LogData : public AbstractLogData { Q_OBJECT public: // Creates an empty LogData LogData(); // Destroy an object ~LogData(); enum MonitoredFileStatus { Unchanged, DataAdded, Truncated }; // Attaches the LogData to a file on disk // It starts the asynchronous indexing and returns (almost) immediately // Attaching to a non existant file works and the file is reported // to be empty. // Reattaching is forbidden and will throw. void attachFile( const QString& fileName ); // Interrupt the loading and report a null file. // Does nothing if no loading in progress. void interruptLoading(); // Creates a new filtered data. // ownership is passed to the caller LogFilteredData* getNewFilteredData() const; // Returns the size if the file in bytes qint64 getFileSize() const; // Returns the last modification date for the file. // Null if the file is not on disk. QDateTime getLastModifiedDate() const; // Throw away all the file data and reload/reindex. void reload(); // Update the polling interval (in ms, 0 means disabled) void setPollingInterval( uint32_t interval_ms ); // Get the auto-detected encoding for the indexed text. EncodingSpeculator::Encoding getDetectedEncoding() const; signals: // Sent during the 'attach' process to signal progress // percent being the percentage of completion. void loadingProgressed( int percent ); // Signal the client the file is fully loaded and available. void loadingFinished( LoadingStatus status ); // Sent when the file on disk has changed, will be followed // by loadingProgressed if needed and then a loadingFinished. void fileChanged( LogData::MonitoredFileStatus status ); private slots: // Consider reloading the file when it changes on disk updated void fileChangedOnDisk(); // Called when the worker thread signals the current operation ended void indexingFinished( LoadingStatus status ); private: // This class models an indexing operation. // It exists to permit LogData to delay the operation if another // one is ongoing (operations are asynchronous) class LogDataOperation { public: LogDataOperation( const QString& fileName ) : filename_( fileName ) {} // Permit each child to have its destructor virtual ~LogDataOperation() {}; void start( LogDataWorkerThread& workerThread ) const { doStart( workerThread ); } const QString& getFilename() const { return filename_; } protected: virtual void doStart( LogDataWorkerThread& workerThread ) const = 0; QString filename_; }; // Attaching a new file (change name + full index) class AttachOperation : public LogDataOperation { public: AttachOperation( const QString& fileName ) : LogDataOperation( fileName ) {} ~AttachOperation() {}; protected: void doStart( LogDataWorkerThread& workerThread ) const; }; // Reindexing the current file class FullIndexOperation : public LogDataOperation { public: FullIndexOperation() : LogDataOperation( QString() ) {} ~FullIndexOperation() {}; protected: void doStart( LogDataWorkerThread& workerThread ) const; }; // Indexing part of the current file (from fileSize) class PartialIndexOperation : public LogDataOperation { public: PartialIndexOperation() : LogDataOperation( QString() ) {} ~PartialIndexOperation() {}; protected: void doStart( LogDataWorkerThread& workerThread ) const; }; std::shared_ptr fileWatcher_; MonitoredFileStatus fileChangedOnDisk_; // Implementation of virtual functions QString doGetLineString( qint64 line ) const override; QString doGetExpandedLineString( qint64 line ) const override; QStringList doGetLines( qint64 first, int number ) const override; QStringList doGetExpandedLines( qint64 first, int number ) const override; qint64 doGetNbLine() const override; int doGetMaxLength() const override; int doGetLineLength( qint64 line ) const override; void doSetDisplayEncoding( const char* encoding ) override; void enqueueOperation( std::shared_ptr newOperation ); void startOperation(); QString indexingFileName_; std::unique_ptr attached_file_; // Indexing data, read by us, written by the worker thread IndexingData indexing_data_; QDateTime lastModifiedDate_; std::shared_ptr currentOperation_; std::shared_ptr nextOperation_; // Codec to decode text QTextCodec* codec_; // To protect the file: mutable QMutex fileMutex_; // (are mutable to allow 'const' function to touch it, // while remaining const) // When acquiring both, data should be help before locking file. LogDataWorkerThread workerThread_; }; Q_DECLARE_METATYPE( LogData::MonitoredFileStatus ); #endif glogg-1.1.4/src/data/logfiltereddata.h0000644000175000017500000001437213076461374016631 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2011, 2012 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef LOGFILTEREDDATA_H #define LOGFILTEREDDATA_H #include #include #include #include #include #include #include "abstractlogdata.h" #include "logfiltereddataworkerthread.h" #include "marks.h" class LogData; class Marks; // A list of matches found in a LogData, it stores all the matching lines, // which can be accessed using the AbstractLogData interface, together with // the original line number where they were found. // Constructing such objet does not start the search. // This object should be constructed by a LogData. class LogFilteredData : public AbstractLogData { Q_OBJECT public: // Creates an empty LogFilteredData LogFilteredData(); // Constructor used by LogData LogFilteredData( const LogData* logData ); ~LogFilteredData(); // Starts the async search, sending newDataAvailable() when new data found. // If a search is already in progress this function will block until // it is done, so the application should call interruptSearch() first. void runSearch( const QRegExp& regExp ); // Add to the existing search, starting at the line when the search was // last stopped. Used when the file on disk has been added too. void updateSearch(); // Interrupt the running search if one is in progress. // Nothing is done if no search is in progress. void interruptSearch(); // Clear the search and the list of results. void clearSearch(); // Returns the line number in the original LogData where the element // 'index' was found. qint64 getMatchingLineNumber( int index ) const; // Returns whether the line number passed is in our list of matching ones. bool isLineInMatchingList( qint64 lineNumber ); // Returns the number of lines in the source log data LineNumber getNbTotalLines() const; // Returns the number of matches (independently of the visibility) LineNumber getNbMatches() const; // Returns the number of marks (independently of the visibility) LineNumber getNbMarks() const; // Returns the reason why the line at the passed index is in the filtered // data. It can be because it is either a mark or a match. enum FilteredLineType { Match, Mark }; FilteredLineType filteredLineTypeByIndex( int index ) const; // Marks interface (delegated to a Marks object) // Add a mark at the given line, optionally identified by the given char // If a mark for this char already exist, the previous one is replaced. void addMark( qint64 line, QChar mark = QChar() ); // Get the (unique) mark identified by the passed char. qint64 getMark( QChar mark ) const; // Returns wheither the passed line has a mark on it. bool isLineMarked( qint64 line ) const; // Delete the mark identified by the passed char. void deleteMark( QChar mark ); // Delete the mark present on the passed line or do nothing if there is // none. void deleteMark( qint64 line ); // Completely clear the marks list. void clearMarks(); // Changes what the AbstractLogData returns via its getXLines/getNbLines // API. enum Visibility { MatchesOnly, MarksOnly, MarksAndMatches }; void setVisibility( Visibility visibility ); signals: // Sent when the search has progressed, give the number of matches (so far) // and the percentage of completion void searchProgressed( int nbMatches, int progress ); private slots: void handleSearchProgressed( int NbMatches, int progress ); private: class FilteredItem; // Implementation of virtual functions QString doGetLineString( qint64 line ) const; QString doGetExpandedLineString( qint64 line ) const; QStringList doGetLines( qint64 first, int number ) const; QStringList doGetExpandedLines( qint64 first, int number ) const; qint64 doGetNbLine() const; int doGetMaxLength() const; int doGetLineLength( qint64 line ) const; void doSetDisplayEncoding( const char* encoding ); // List of the matching line numbers SearchResultArray matching_lines_; const LogData* sourceLogData_; QRegExp currentRegExp_; bool searchDone_; int maxLength_; int maxLengthMarks_; // Number of lines of the LogData that has been searched for: qint64 nbLinesProcessed_; Visibility visibility_; // Cache used to combine Marks and Matches // when visibility_ == MarksAndMatches // (QVector store actual objects instead of pointers) mutable std::vector filteredItemsCache_; mutable bool filteredItemsCacheDirty_; LogFilteredDataWorkerThread workerThread_; Marks marks_; // Utility functions LineNumber findLogDataLine( LineNumber lineNum ) const; void regenerateFilteredItemsCache() const; }; // A class representing a Mark or Match. // Conceptually it should be a base class for Mark and MatchingLine, // but we implement it this way for performance reason as we create plenty of // those everytime we refresh the cache. // Specifically it allows to store this in the cache by value instead // of pointer (less small allocations and no RTTI). class LogFilteredData::FilteredItem { public: // A default ctor seems to be necessary for QVector FilteredItem() { lineNumber_ = 0; } FilteredItem( LineNumber lineNumber, FilteredLineType type ) { lineNumber_ = lineNumber; type_ = type; } LineNumber lineNumber() const { return lineNumber_; } FilteredLineType type() const { return type_; } private: LineNumber lineNumber_; FilteredLineType type_; }; #endif glogg-1.1.4/src/data/logfiltereddataworkerthread.h0000644000175000017500000001305113076461374021244 0ustar nickonicko/* * Copyright (C) 2009, 2010 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef LOGFILTEREDDATAWORKERTHREAD_H #define LOGFILTEREDDATAWORKERTHREAD_H #include #include #include #include #include #include class LogData; // Line number are unsigned 32 bits for now. typedef uint32_t LineNumber; // Class encapsulating a single matching line // Contains the line number the line was found in and its content. class MatchingLine { public: MatchingLine( LineNumber line ) { lineNumber_ = line; }; // Accessors LineNumber lineNumber() const { return lineNumber_; } private: LineNumber lineNumber_; }; // This is an array of matching lines. // It shall be implemented for random lookup speed, so // a fixed "in-place" array (vector) is probably fine. typedef std::vector SearchResultArray; // This class is a mutex protected set of search result data. // It is thread safe. class SearchData { public: SearchData() : dataMutex_(), matches_(), maxLength_(0) { } // Atomically get all the search data void getAll( int* length, SearchResultArray* matches, qint64* nbLinesProcessed ) const; // Atomically set all the search data // (overwriting the existing) // (the matches are always moved) void setAll( int length, SearchResultArray&& matches ); // Atomically add to all the existing search data. void addAll( int length, const SearchResultArray& matches, LineNumber nbLinesProcessed ); // Get the number of matches LineNumber getNbMatches() const; // Delete the match for the passed line (if it exist) void deleteMatch( LineNumber line ); // Atomically clear the data. void clear(); private: mutable QMutex dataMutex_; SearchResultArray matches_; int maxLength_; LineNumber nbLinesProcessed_; }; class SearchOperation : public QObject { Q_OBJECT public: SearchOperation( const LogData* sourceLogData, const QRegExp& regExp, bool* interruptRequest ); virtual ~SearchOperation() { } // Start the search operation, returns true if it has been done // and false if it has been cancelled (results not copied) virtual void start( SearchData& result ) = 0; signals: void searchProgressed( int percent, int nbMatches ); protected: static const int nbLinesInChunk; // Implement the common part of the search, passing // the shared results and the line to begin the search from. void doSearch( SearchData& result, qint64 initialLine ); bool* interruptRequested_; const QRegExp regexp_; const LogData* sourceLogData_; }; class FullSearchOperation : public SearchOperation { public: FullSearchOperation( const LogData* sourceLogData, const QRegExp& regExp, bool* interruptRequest ) : SearchOperation( sourceLogData, regExp, interruptRequest ) {} virtual void start( SearchData& result ); }; class UpdateSearchOperation : public SearchOperation { public: UpdateSearchOperation( const LogData* sourceLogData, const QRegExp& regExp, bool* interruptRequest, qint64 position ) : SearchOperation( sourceLogData, regExp, interruptRequest ), initialPosition_( position ) {} virtual void start( SearchData& result ); private: qint64 initialPosition_; }; // Create and manage the thread doing loading/indexing for // the creating LogData. One LogDataWorkerThread is used // per LogData instance. // Note everything except the run() function is in the LogData's // thread. class LogFilteredDataWorkerThread : public QThread { Q_OBJECT public: LogFilteredDataWorkerThread( const LogData* sourceLogData ); ~LogFilteredDataWorkerThread(); // Start the search with the passed regexp void search( const QRegExp& regExp ); // Continue the previous search starting at the passed position // in the source file (line number) void updateSearch( const QRegExp& regExp, qint64 position ); // Interrupts the search if one is in progress void interrupt(); // Returns a copy of the current indexing data void getSearchResult( int* maxLength, SearchResultArray* searchMatches, qint64* nbLinesProcessed ); signals: // Sent during the indexing process to signal progress // percent being the percentage of completion. void searchProgressed( int percent, int nbMatches ); // Sent when indexing is finished, signals the client // to copy the new data back. void searchFinished(); protected: void run(); private: const LogData* sourceLogData_; // Mutex to protect operationRequested_ and friends QMutex mutex_; QWaitCondition operationRequestedCond_; QWaitCondition nothingToDoCond_; // Set when the thread must die bool terminate_; bool interruptRequested_; SearchOperation* operationRequested_; // Shared indexing data SearchData searchData_; }; #endif glogg-1.1.4/src/data/compressedlinestorage.h0000644000175000017500000001300113076461374020064 0ustar nickonicko/* * Copyright (C) 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include #include #include "threadprivatestore.h" // This class is a compressed storage backend for LinePositionArray // It emulates the interface of a vector, but take advantage of the nature // of the stored data (increasing end of line addresses) to apply some // compression in memory, while still providing fast, constant-time look-up. /* The current algorithm takes advantage of the fact most lines are reasonably * short, it codes each line on: * - Line < 127 bytes : 1 byte * - 127 < line < 16383 : 2 bytes * - line > 16383 : 6 bytes (or 10 bytes) * Uncompressed backend stores line on 4 bytes or 8 bytes. * * The algorithm is quite simple, the file is first divided in two parts: * - The lines whose end are located before UINT32_MAX * - The lines whose end are located after UINT32_MAX * Those end of lines are stored separately in the table32 and the table64 * respectively. * * The EOL list is then divided in blocks of BLOCK_SIZE (128) lines. * A block index vector (per table) contains pointers to each block. * * Each block is then defined as such: * Block32 (sizes in byte) * 00 - Absolute EOF address (4 bytes) * 04 - ( 0xxx xxxx if second line is < 127 ) (1 byte, relative) * - ( 10xx xxxx * xxxx xxxx if second line is < 16383 ) (2 bytes, relative) * - ( 1111 1111 * xxxx xxxx * xxxx xxxx if second line is > 16383 ) (6 bytes, absolute) * ... * (126 more lines) * * Block64 (sizes in byte) * 00 - Absolute EOF address (8 bytes) * 08 - ( 0xxx xxxx if second line is < 127 ) (1 byte, relative) * - ( 10xx xxxx * xxxx xxxx if second line is < 16383 ) (2 bytes, relative) * - ( 1111 1111 * xxxx xxxx * xxxx xxxx * xxxx xxxx * xxxx xxxx if second line is > 16383 ) (10 bytes, absolute) * ... * (126 more lines) * * Absolute addressing has been adopted for line > 16383 to bound memory usage in case * of pathologically long (MBs or GBs) lines, even if it is a bit less efficient for * long-ish (30 KB) lines. * * The table32 always starts at 0, the table64 starts at first_long_line_ */ #ifndef COMPRESSEDLINESTORAGE_H #define COMPRESSEDLINESTORAGE_H #define BLOCK_SIZE 256 //template class CompressedLinePositionStorage { public: // Default constructor CompressedLinePositionStorage() { nb_lines_ = 0; first_long_line_ = UINT32_MAX; current_pos_ = 0; block_pointer_ = nullptr; previous_block_pointer_ = nullptr; } // Copy constructor would be slow, delete! CompressedLinePositionStorage( const CompressedLinePositionStorage& orig ) = delete; // Move constructor CompressedLinePositionStorage( CompressedLinePositionStorage&& orig ); // Move assignement CompressedLinePositionStorage& operator=( CompressedLinePositionStorage&& orig ); // Destructor ~CompressedLinePositionStorage(); // Append the passed end-of-line to the storage void append( uint64_t pos ); void push_back( uint64_t pos ) { append( pos ); } // Size of the array uint32_t size() const { return nb_lines_; } // Element at index uint64_t at( uint32_t i ) const; // Add one list to the other void append_list( const std::vector& positions ); // Pop the last element of the storage void pop_back(); private: // Utility for move ctor/assign void move_from( CompressedLinePositionStorage&& orig ); void free_blocks(); // The two indexes std::vector block32_index_; std::vector block64_index_; // Total number of lines in storage uint32_t nb_lines_; // Current position (position of the end of the last line added) uint64_t current_pos_; // Address of the next position (not yet written) within the current // block. nullptr means there is no current block (previous block // finished or no data) char* block_pointer_; // The index of the first line whose end is stored in a block64 // Initialised at UINT32_MAX, meaning "unset" // this is the origin point for all calculations in block64 uint32_t first_long_line_; // For pop_back: // Previous pointer to block element, it is restored when we // "pop_back" the last element. // A null pointer here means pop_back need to free the block // that has just been created. char* previous_block_pointer_; // Cache the last position read // This is to speed up consecutive reads (whole page) struct Cache { Cache() { index = UINT32_MAX - 1U; position = 0; ptr = nullptr; } uint32_t index; uint64_t position; char* ptr; }; mutable ThreadPrivateStore last_read_; // = { UINT32_MAX - 1U, 0, nullptr }; // mutable Cache last_read; }; #endif glogg-1.1.4/src/data/logdata.cpp0000644000175000017500000002741513076461374015447 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2013, 2014, 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ // This file implements LogData, the content of a log file. #include #include #include #include "log.h" #include "logdata.h" #include "logfiltereddata.h" #if defined(GLOGG_SUPPORTS_INOTIFY) || defined(WIN32) #include "platformfilewatcher.h" #else #include "qtfilewatcher.h" #endif // Implementation of the 'start' functions for each operation void LogData::AttachOperation::doStart( LogDataWorkerThread& workerThread ) const { LOG(logDEBUG) << "Attaching " << filename_.toStdString(); workerThread.attachFile( filename_ ); workerThread.indexAll(); } void LogData::FullIndexOperation::doStart( LogDataWorkerThread& workerThread ) const { LOG(logDEBUG) << "Reindexing (full)"; workerThread.indexAll(); } void LogData::PartialIndexOperation::doStart( LogDataWorkerThread& workerThread ) const { LOG(logDEBUG) << "Reindexing (partial)"; workerThread.indexAdditionalLines(); } // Constructs an empty log file. // It must be displayed without error. LogData::LogData() : AbstractLogData(), indexing_data_(), fileMutex_(), workerThread_( &indexing_data_ ) { // Start with an "empty" log attached_file_ = nullptr; currentOperation_ = nullptr; nextOperation_ = nullptr; codec_ = QTextCodec::codecForName( "ISO-8859-1" ); #if defined(GLOGG_SUPPORTS_INOTIFY) || defined(WIN32) fileWatcher_ = std::make_shared(); #else fileWatcher_ = std::make_shared(); #endif // Initialise the file watcher connect( fileWatcher_.get(), SIGNAL( fileChanged( const QString& ) ), this, SLOT( fileChangedOnDisk() ) ); // Forward the update signal connect( &workerThread_, SIGNAL( indexingProgressed( int ) ), this, SIGNAL( loadingProgressed( int ) ) ); connect( &workerThread_, SIGNAL( indexingFinished( LoadingStatus ) ), this, SLOT( indexingFinished( LoadingStatus ) ) ); // Starts the worker thread workerThread_.start(); } LogData::~LogData() { // Remove the current file from the watch list if ( attached_file_ ) fileWatcher_->removeFile( attached_file_->fileName() ); // FIXME // workerThread_.stop(); } // // Public functions // void LogData::attachFile( const QString& fileName ) { LOG(logDEBUG) << "LogData::attachFile " << fileName.toStdString(); if ( attached_file_ ) { // We cannot reattach throw CantReattachErr(); } attached_file_.reset( new QFile( fileName ) ); attached_file_->open( QIODevice::ReadOnly ); std::shared_ptr operation( new AttachOperation( fileName ) ); enqueueOperation( std::move( operation ) ); } void LogData::interruptLoading() { workerThread_.interrupt(); } qint64 LogData::getFileSize() const { return indexing_data_.getSize(); } QDateTime LogData::getLastModifiedDate() const { return lastModifiedDate_; } // Return an initialised LogFilteredData. The search is not started. LogFilteredData* LogData::getNewFilteredData() const { LogFilteredData* newFilteredData = new LogFilteredData( this ); return newFilteredData; } void LogData::reload() { workerThread_.interrupt(); enqueueOperation( std::make_shared() ); } void LogData::setPollingInterval( uint32_t interval_ms ) { fileWatcher_->setPollingInterval( interval_ms ); } // // Private functions // // Add an operation to the queue and perform it immediately if // there is none ongoing. void LogData::enqueueOperation( std::shared_ptr new_operation ) { if ( currentOperation_ == nullptr ) { // We do it immediately currentOperation_ = new_operation; startOperation(); } else { // An operation is in progress... // ... we schedule the attach op for later nextOperation_ = new_operation; } } // Performs the current operation asynchronously, a indexingFinished // signal will be received when it's finished. void LogData::startOperation() { if ( currentOperation_ ) { LOG(logDEBUG) << "startOperation found something to do."; // Let the operation do its stuff currentOperation_->start( workerThread_ ); } } // // Slots // void LogData::fileChangedOnDisk() { LOG(logDEBUG) << "signalFileChanged"; const QString name = attached_file_->fileName(); QFileInfo info( name ); // Need to open the file in case it was absent attached_file_->open( QIODevice::ReadOnly ); std::shared_ptr newOperation; qint64 file_size = indexing_data_.getSize(); LOG(logDEBUG) << "current fileSize=" << file_size; LOG(logDEBUG) << "info file_->size()=" << info.size(); if ( info.size() < file_size ) { fileChangedOnDisk_ = Truncated; LOG(logINFO) << "File truncated"; newOperation = std::make_shared(); } else if ( fileChangedOnDisk_ != DataAdded ) { fileChangedOnDisk_ = DataAdded; LOG(logINFO) << "New data on disk"; newOperation = std::make_shared(); } if ( newOperation ) enqueueOperation( newOperation ); lastModifiedDate_ = info.lastModified(); emit fileChanged( fileChangedOnDisk_ ); // TODO: fileChangedOnDisk_, fileSize_ } void LogData::indexingFinished( LoadingStatus status ) { LOG(logDEBUG) << "indexingFinished: " << ( status == LoadingStatus::Successful ) << ", found " << indexing_data_.getNbLines() << " lines."; if ( status == LoadingStatus::Successful ) { // Start watching we watch the file for updates fileChangedOnDisk_ = Unchanged; fileWatcher_->addFile( attached_file_->fileName() ); // Update the modified date/time if the file exists lastModifiedDate_ = QDateTime(); QFileInfo fileInfo( *attached_file_ ); if ( fileInfo.exists() ) lastModifiedDate_ = fileInfo.lastModified(); } // FIXME be cleverer here as a notification might have arrived whilst we // were indexing. fileChangedOnDisk_ = Unchanged; LOG(logDEBUG) << "Sending indexingFinished."; emit loadingFinished( status ); // So now the operation is done, let's see if there is something // else to do, in which case, do it! assert( currentOperation_ ); currentOperation_ = std::move( nextOperation_ ); nextOperation_.reset(); if ( currentOperation_ ) { LOG(logDEBUG) << "indexingFinished is performing the next operation"; startOperation(); } } // // Implementation of virtual functions // qint64 LogData::doGetNbLine() const { return indexing_data_.getNbLines(); } int LogData::doGetMaxLength() const { return indexing_data_.getMaxLength(); } int LogData::doGetLineLength( qint64 line ) const { if ( line >= indexing_data_.getNbLines() ) { return 0; /* exception? */ } int length = doGetExpandedLineString( line ).length(); return length; } void LogData::doSetDisplayEncoding( const char* encoding ) { LOG(logDEBUG) << "AbstractLogData::setDisplayEncoding: " << encoding; codec_ = QTextCodec::codecForName( encoding ); } QString LogData::doGetLineString( qint64 line ) const { if ( line >= indexing_data_.getNbLines() ) { return 0; /* exception? */ } fileMutex_.lock(); attached_file_->seek( (line == 0) ? 0 : indexing_data_.getPosForLine( line-1 ) ); QString string = codec_->toUnicode( attached_file_->readLine() ); fileMutex_.unlock(); string.chop( 1 ); return string; } QString LogData::doGetExpandedLineString( qint64 line ) const { if ( line >= indexing_data_.getNbLines() ) { return 0; /* exception? */ } fileMutex_.lock(); attached_file_->seek( (line == 0) ? 0 : indexing_data_.getPosForLine( line-1 ) ); QByteArray rawString = attached_file_->readLine(); fileMutex_.unlock(); QString string = untabify( codec_->toUnicode( rawString ) ); string.chop( 1 ); return string; } // Note this function is also called from the LogFilteredDataWorker thread, so // data must be protected because they are changed in the main thread (by // indexingFinished). QStringList LogData::doGetLines( qint64 first_line, int number ) const { QStringList list; const qint64 last_line = first_line + number - 1; // LOG(logDEBUG) << "LogData::doGetLines first_line:" << first_line << " nb:" << number; if ( number == 0 ) { return QStringList(); } if ( last_line >= indexing_data_.getNbLines() ) { LOG(logWARNING) << "LogData::doGetLines Lines out of bound asked for"; return QStringList(); /* exception? */ } fileMutex_.lock(); const qint64 first_byte = (first_line == 0) ? 0 : indexing_data_.getPosForLine( first_line-1 ); const qint64 last_byte = indexing_data_.getPosForLine( last_line ); // LOG(logDEBUG) << "LogData::doGetLines first_byte:" << first_byte << " last_byte:" << last_byte; attached_file_->seek( first_byte ); QByteArray blob = attached_file_->read( last_byte - first_byte ); fileMutex_.unlock(); qint64 beginning = 0; qint64 end = 0; for ( qint64 line = first_line; (line <= last_line); line++ ) { end = indexing_data_.getPosForLine( line ) - first_byte; // LOG(logDEBUG) << "Getting line " << line << " beginning " << beginning << " end " << end; QByteArray this_line = blob.mid( beginning, end - beginning - 1 ); // LOG(logDEBUG) << "Line is: " << QString( this_line ).toStdString(); list.append( codec_->toUnicode( this_line ) ); beginning = end; } return list; } QStringList LogData::doGetExpandedLines( qint64 first_line, int number ) const { QStringList list; const qint64 last_line = first_line + number - 1; if ( number == 0 ) { return QStringList(); } if ( last_line >= indexing_data_.getNbLines() ) { LOG(logWARNING) << "LogData::doGetExpandedLines Lines out of bound asked for"; return QStringList(); /* exception? */ } fileMutex_.lock(); const qint64 first_byte = (first_line == 0) ? 0 : indexing_data_.getPosForLine( first_line-1 ); const qint64 last_byte = indexing_data_.getPosForLine( last_line ); // LOG(logDEBUG) << "LogData::doGetExpandedLines first_byte:" << first_byte << " last_byte:" << last_byte; attached_file_->seek( first_byte ); QByteArray blob = attached_file_->read( last_byte - first_byte ); fileMutex_.unlock(); qint64 beginning = 0; qint64 end = 0; for ( qint64 line = first_line; (line <= last_line); line++ ) { end = indexing_data_.getPosForLine( line ) - first_byte; // LOG(logDEBUG) << "Getting line " << line << " beginning " << beginning << " end " << end; QByteArray this_line = blob.mid( beginning, end - beginning - 1 ); // LOG(logDEBUG) << "Line is: " << QString( this_line ).toStdString(); list.append( untabify( codec_->toUnicode( this_line ) ) ); beginning = end; } return list; } EncodingSpeculator::Encoding LogData::getDetectedEncoding() const { return indexing_data_.getEncodingGuess(); } glogg-1.1.4/src/data/logdataworkerthread.h0000644000175000017500000001322213076461374017525 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2014, 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef LOGDATAWORKERTHREAD_H #define LOGDATAWORKERTHREAD_H #include #include #include #include #include #include "loadingstatus.h" #include "linepositionarray.h" #include "encodingspeculator.h" #include "utils.h" // This class is a thread-safe set of indexing data. class IndexingData { public: IndexingData() : dataMutex_(), linePosition_(), maxLength_(0), indexedSize_(0), encoding_(EncodingSpeculator::Encoding::ASCII7) { } // Get the total indexed size qint64 getSize() const; // Get the length of the longest line int getMaxLength() const; // Get the total number of lines LineNumber getNbLines() const; // Get the position (in byte from the beginning of the file) // of the end of the passed line. qint64 getPosForLine( LineNumber line ) const; // Get the guessed encoding for the content. EncodingSpeculator::Encoding getEncodingGuess() const; // Atomically add to all the existing // indexing data. void addAll( qint64 size, int length, const FastLinePositionArray& linePosition, EncodingSpeculator::Encoding encoding ); // Completely clear the indexing data. void clear(); private: mutable QMutex dataMutex_; LinePositionArray linePosition_; int maxLength_; qint64 indexedSize_; EncodingSpeculator::Encoding encoding_; }; class IndexOperation : public QObject { Q_OBJECT public: IndexOperation( const QString& fileName, IndexingData* indexingData, bool* interruptRequest, EncodingSpeculator* encodingSpeculator ); virtual ~IndexOperation() { } // Start the indexing operation, returns true if it has been done // and false if it has been cancelled (results not copied) virtual bool start() = 0; signals: void indexingProgressed( int ); protected: static const int sizeChunk; // Returns the total size indexed // Modify the passed linePosition and maxLength void doIndex( IndexingData* linePosition, EncodingSpeculator* encodingSpeculator, qint64 initialPosition ); QString fileName_; bool* interruptRequest_; IndexingData* indexing_data_; EncodingSpeculator* encoding_speculator_; }; class FullIndexOperation : public IndexOperation { public: FullIndexOperation( const QString& fileName, IndexingData* indexingData, bool* interruptRequest, EncodingSpeculator* speculator ) : IndexOperation( fileName, indexingData, interruptRequest, speculator ) { } virtual bool start(); }; class PartialIndexOperation : public IndexOperation { public: PartialIndexOperation( const QString& fileName, IndexingData* indexingData, bool* interruptRequest, EncodingSpeculator* speculator ) : IndexOperation( fileName, indexingData, interruptRequest, speculator ) { } virtual bool start(); }; // Create and manage the thread doing loading/indexing for // the creating LogData. One LogDataWorkerThread is used // per LogData instance. // Note everything except the run() function is in the LogData's // thread. class LogDataWorkerThread : public QThread { Q_OBJECT public: // Pass a pointer to the IndexingData (initially empty) // This object will change it when indexing (IndexingData must be thread safe!) LogDataWorkerThread( IndexingData* indexing_data ); ~LogDataWorkerThread(); // Attaches to a file on disk. Attaching to a non existant file // will work, it will just appear as an empty file. void attachFile( const QString& fileName ); // Instructs the thread to start a new full indexing of the file, sending // signals as it progresses. void indexAll(); // Instructs the thread to start a partial indexing (starting at // the end of the file as indexed). void indexAdditionalLines(); // Interrupts the indexing if one is in progress void interrupt(); // Returns a copy of the current indexing data void getIndexingData( qint64* indexedSize, int* maxLength, LinePositionArray* linePosition ); signals: // Sent during the indexing process to signal progress // percent being the percentage of completion. void indexingProgressed( int percent ); // Sent when indexing is finished, signals the client // to copy the new data back. void indexingFinished( LoadingStatus status ); protected: void run(); private: void doIndexAll(); // Mutex to protect operationRequested_ and friends QMutex mutex_; QWaitCondition operationRequestedCond_; QWaitCondition nothingToDoCond_; QString fileName_; // Set when the thread must die bool terminate_; bool interruptRequested_; IndexOperation* operationRequested_; // Pointer to the owner's indexing data (we modify it) IndexingData* indexing_data_; // To guess the encoding EncodingSpeculator encodingSpeculator_; }; #endif glogg-1.1.4/src/data/threadprivatestore.h0000644000175000017500000000476513076461374017423 0ustar nickonicko/* * Copyright (C) 2015 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef THREADPRIVATESTORE_H #define THREADPRIVATESTORE_H #include #include #include #include "log.h" // This template stores data that is private to the calling thread // in a completely wait-free manner. template class ThreadPrivateStore { public: // Construct an empty ThreadPrivateStore ThreadPrivateStore() : nb_threads_( 0 ) { // Last one is a guard for ( int i=0; i<(MAX_THREADS+1); ++i ) thread_ids_[i] = 0; } // Conversion to a T operator T() const { return get(); } // Getter (thread-safe, wait-free) T get() const { return data_[threadIndex()]; } T* getPtr() { return &data_[threadIndex()]; } // Setter (thread-safe, wait-free) void set( const T& value ) { data_[threadIndex()] = value; } private: // Nb of threads that have registered int nb_threads_; // The actual data array (one element per thread from 0 to nb_threads_) T data_[MAX_THREADS]; mutable std::atomic thread_ids_[MAX_THREADS+1]; int threadIndex() const { int i; for ( i=0; thread_ids_[i]; ++i ) { if ( thread_ids_[i].load() == std::hash()(std::this_thread::get_id() ) ) { return i; } } // Current thread is missing, let's add it size_t thread_id = std::hash()( std::this_thread::get_id() ); while ( i < MAX_THREADS ) { size_t expected = 0; if ( thread_ids_[i++].compare_exchange_weak( expected, thread_id ) ) { LOG(logDEBUG) << "Created thread for " << thread_id << " at index " << i-1; return i-1; } } assert( 1 ); return 0; } }; #endif glogg-1.1.4/src/data/abstractlogdata.h0000644000175000017500000001045113076461374016630 0ustar nickonicko/* * Copyright (C) 2009, 2010 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef ABSTRACTLOGDATA_H #define ABSTRACTLOGDATA_H #include #include #include // Base class representing a set of data. // It can be either a full set or a filtered set. class AbstractLogData : public QObject { Q_OBJECT public: AbstractLogData(); // Permit each child to have its destructor virtual ~AbstractLogData() {}; // Returns the line passed as a QString QString getLineString( qint64 line ) const; // Returns the line passed as a QString, with tabs expanded QString getExpandedLineString( qint64 line ) const; // Returns a set of lines as a QStringList QStringList getLines( qint64 first_line, int number ) const; // Returns a set of lines with tabs expanded QStringList getExpandedLines( qint64 first_line, int number ) const; // Returns the total number of lines qint64 getNbLine() const; // Returns the visible length of the longest line // Tabs are expanded int getMaxLength() const; // Returns the visible length of the passed line // Tabs are expanded int getLineLength( qint64 line ) const; // Set the view to use the passed encoding for display void setDisplayEncoding( const char* encoding_name ); // Length of a tab stop static const int tabStop = 8; protected: // Internal function called to get a given line virtual QString doGetLineString( qint64 line ) const = 0; // Internal function called to get a given line virtual QString doGetExpandedLineString( qint64 line ) const = 0; // Internal function called to get a set of lines virtual QStringList doGetLines( qint64 first_line, int number ) const = 0; // Internal function called to get a set of expanded lines virtual QStringList doGetExpandedLines( qint64 first_line, int number ) const = 0; // Internal function called to get the number of lines virtual qint64 doGetNbLine() const = 0; // Internal function called to get the maximum length virtual int doGetMaxLength() const = 0; // Internal function called to get the line length virtual int doGetLineLength( qint64 line ) const = 0; // Internal function called to set the encoding virtual void doSetDisplayEncoding( const char* encoding ) = 0; static inline QString untabify( const QString& line ) { QString untabified_line; int total_spaces = 0; for ( int j = 0; j < line.length(); j++ ) { if ( line[j] == '\t' ) { int spaces = tabStop - ( ( j + total_spaces ) % tabStop ); // LOG(logDEBUG4) << "Replacing tab at char " << j << " (" << spaces << " spaces)"; QString blanks( spaces, QChar(' ') ); untabified_line.append( blanks ); total_spaces += spaces - 1; } else { untabified_line.append( line[j] ); } } return untabified_line; } static inline QString untabify( const char* line ) { QString untabified_line; int total_spaces = 0; for ( const char* i = line; *i != '\0'; i++ ) { if ( *i == '\t' ) { int spaces = tabStop - ( ( (i - line) + total_spaces ) % tabStop ); // LOG(logDEBUG4) << "Replacing tab at char " << j << " (" << spaces << " spaces)"; QString blanks( spaces, QChar(' ') ); untabified_line.append( blanks ); total_spaces += spaces - 1; } else { untabified_line.append( *i ); } } return untabified_line; } }; #endif glogg-1.1.4/src/logmainview.h0000644000175000017500000000302713076461374015102 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2011, 2013 Nicolas Bonnefon * and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef LOGMAINVIEW_H #define LOGMAINVIEW_H #include "abstractlogview.h" #include "data/logdata.h" // Class implementing the main (top) view widget. class LogMainView : public AbstractLogView { public: LogMainView( const LogData* newLogData, const QuickFindPattern* const quickFindPattern, Overview* overview, OverviewWidget* overview_widget, QWidget* parent = 0 ); // Configure the view to use the passed filtered list // (used for couloured bullets) // Should be NULL or the empty LFD if no filtering is used void useNewFiltering( LogFilteredData* filteredData ); protected: // Implements the virtual function virtual LineType lineType( int lineNumber ) const; private: LogFilteredData* filteredData_; }; #endif glogg-1.1.4/src/signalmux.h0000644000175000017500000000602213076461374014566 0ustar nickonicko/* * Copyright (C) 2013 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef SIGNALMUX_H #define SIGNALMUX_H // This class multiplexes Qt signals exchanged between one main // window and several 'document' widgets. // The main window register signals with the mux (instead of directly // with the document) and then just notify the mux when the 'current // document' changes /* +---------------------------------------+ | | | Main Window |--+ | | | +---------------------------------------+ | + ^ | v + | +---------------------------------------+ | | MUX |<-+ +---------------------------------------+ +----------^ + | +----------+ | v +--+-----+ +--------+ +-------+ +-------+ | | | | | | | | | Doc 1 | | Doc 2 | | Doc 3 | | Doc 4 | | | | | | | | | +--------+ +--------+ +-------+ +-------+ */ // Largely inspired by http://doc.qt.digia.com/qq/qq08-action-multiplexer.html #include #include class QObject; class MuxableDocumentInterface { public: // Send all signals refering to a state of the document to update // the parent widget. void sendAllStateSignals() { doSendAllStateSignals(); } protected: virtual void doSendAllStateSignals() = 0; }; class SignalMux { public: SignalMux(); // Connect an 'downstream' signal void connect(QObject *sender, const char *signal, const char *slot); void disconnect(QObject *sender, const char *signal, const char *slot); // Connect an 'upstream' signal void connect(const char *signal, QObject *receiver, const char *slot); void disconnect(const char *signal, QObject *receiver, const char *slot); // Change the current document void setCurrentDocument( QObject* current_document ); private: struct Connection { QPointer source; QPointer sink; const char* signal; const char* slot; }; void connect( const Connection& connection); void disconnect( const Connection& connection); std::list connectionList_; QPointer currentDocument_; }; #endif glogg-1.1.4/src/winfilewatcher.cpp0000644000175000017500000000044713076461374016132 0ustar nickonicko#include "winfilewatcher.h" // Create the empty object WinFileWatcher::WinFileWatcher() : FileWatcher() { } // Destroy the object WinFileWatcher::~WinFileWatcher() { } void WinFileWatcher::addFile( const QString& fileName ) { } void WinFileWatcher::removeFile( const QString& fileName ) { } glogg-1.1.4/src/abstractlogview.h0000644000175000017500000003165213076461374015766 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2011, 2012, 2013 Nicolas Bonnefon * and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #ifndef ABSTRACTLOGVIEW_H #define ABSTRACTLOGVIEW_H #include #include #ifdef GLOGG_PERF_MEASURE_FPS # include "perfcounter.h" #endif #include "selection.h" #include "quickfind.h" #include "overviewwidget.h" #include "quickfindmux.h" #include "viewtools.h" class QMenu; class QAction; class AbstractLogData; class LineChunk { public: enum ChunkType { Normal, Highlighted, Selected, }; LineChunk( int first_col, int end_col, ChunkType type ); int start() const { return start_; } int end() const { return end_; } ChunkType type() const { return type_; } // Returns 'true' if the selection is part of this chunk // (at least partially), if so, it should be replaced by the list returned QList select( int selection_start, int selection_end ) const; private: int start_; int end_; ChunkType type_; }; // Utility class for syntax colouring. // It stores the chunks of line to draw // each chunk having a different colour class LineDrawer { public: LineDrawer( const QColor& back_color) : list(), backColor_( back_color ) { }; // Add a chunk of line using the given colours. // Both first_col and last_col are included // An empty chunk will be ignored. // the first column will be set to 0 if negative // The column are relative to the screen void addChunk( int first_col, int last_col, QColor fore, QColor back ); void addChunk( const LineChunk& chunk, QColor fore, QColor back ); // Draw the current line of text using the given painter, // in the passed block (in pixels) // The line must be cut to fit on the screen. // leftExtraBackgroundPx is the an extra margin to start drawing // the coloured // background, going all the way to the element // left of the line looks better. void draw( QPainter& painter, int xPos, int yPos, int line_width, const QString& line, int leftExtraBackgroundPx ); private: class Chunk { public: // Create a new chunk Chunk( int start, int length, QColor fore, QColor back ) : start_( start ), length_( length ), foreColor_ ( fore ), backColor_ ( back ) { }; // Accessors int start() const { return start_; } int length() const { return length_; } const QColor& foreColor() const { return foreColor_; } const QColor& backColor() const { return backColor_; } private: int start_; int length_; QColor foreColor_; QColor backColor_; }; QList list; QColor backColor_; }; // Utility class representing a buffer for number entered on the keyboard // The buffer keep at most 7 digits, and reset itself after a timeout. class DigitsBuffer : public QObject { Q_OBJECT public: DigitsBuffer(); // Reset the buffer. void reset(); // Add a single digit to the buffer (discarded if it's not a digit), // the timeout timer is reset. void add( char character ); // Get the content of the buffer (0 if empty) and reset it. int content(); protected: void timerEvent( QTimerEvent* event ); private: // Duration of the timeout in milliseconds. static const int timeout_; QString digits_; QBasicTimer timer_; }; class Overview; // Base class representing the log view widget. // It can be either the top (full) or bottom (filtered) view. class AbstractLogView : public QAbstractScrollArea, public SearchableWidgetInterface { Q_OBJECT public: // Constructor of the widget, the data set is passed. // The caller retains ownership of the data set. // The pointer to the QFP is used for colouring and QuickFind searches AbstractLogView( const AbstractLogData* newLogData, const QuickFindPattern* const quickFind, QWidget* parent=0 ); ~AbstractLogView(); // Refresh the widget when the data set has changed. void updateData(); // Instructs the widget to update it's content geometry, // used when the font is changed. void updateDisplaySize(); // Return the line number of the top line of the view int getTopLine() const; // Return the text of the current selection. QString getSelection() const; // Instructs the widget to select the whole text. void selectAll(); bool isFollowEnabled() const { return followMode_; } protected: virtual void mousePressEvent( QMouseEvent* mouseEvent ); virtual void mouseMoveEvent( QMouseEvent* mouseEvent ); virtual void mouseReleaseEvent( QMouseEvent* ); virtual void mouseDoubleClickEvent( QMouseEvent* mouseEvent ); virtual void timerEvent( QTimerEvent* timerEvent ); virtual void changeEvent( QEvent* changeEvent ); virtual void paintEvent( QPaintEvent* paintEvent ); virtual void resizeEvent( QResizeEvent* resizeEvent ); virtual void scrollContentsBy( int dx, int dy ); virtual void keyPressEvent( QKeyEvent* keyEvent ); virtual void wheelEvent( QWheelEvent* wheelEvent ); virtual bool event( QEvent * e ); // Must be implemented to return wether the line number is // a match, a mark or just a normal line (used for coloured bullets) enum LineType { Normal, Marked, Match }; virtual LineType lineType( int lineNumber ) const = 0; // Line number to display for line at the given index virtual qint64 displayLineNumber( int lineNumber ) const; virtual qint64 maxDisplayLineNumber() const; // Get the overview associated with this view, or NULL if there is none Overview* getOverview() const { return overview_; } // Set the Overview and OverviewWidget void setOverview( Overview* overview, OverviewWidget* overview_widget ); signals: // Sent when a new line has been selected by the user. void newSelection(int line); // Sent up to the MainWindow to enable/disable the follow mode void followModeChanged( bool enabled ); // Sent when the view wants the QuickFind widget pattern to change. void changeQuickFind( const QString& newPattern, QuickFindMux::QFDirection newDirection ); // Sent up when the current line number is updated void updateLineNumber( int line ); // Sent up when quickFind wants to show a message to the user. void notifyQuickFind( const QFNotification& message ); // Sent up when quickFind wants to clear the notification. void clearQuickFindNotification(); // Sent when the view ask for a line to be marked // (click in the left margin). void markLine( qint64 line ); // Sent up when the user wants to add the selection to the search void addToSearch( const QString& selection ); // Sent up when the mouse is hovered over a line's margin void mouseHoveredOverLine( qint64 line ); // Sent up when the mouse leaves a line's margin void mouseLeftHoveringZone(); // Sent up for view initiated quickfind searches void searchNext(); void searchPrevious(); // Sent up when the user has moved within the view void activity(); public slots: // Makes the widget select and display the passed line. // Scrolling as necessary void selectAndDisplayLine( int line ); // Use the current QFP to go and select the next match. virtual void searchForward(); // Use the current QFP to go and select the previous match. virtual void searchBackward(); // Use the current QFP to go and select the next match (incremental) virtual void incrementallySearchForward(); // Use the current QFP to go and select the previous match (incremental) virtual void incrementallySearchBackward(); // Stop the current incremental search (typically when user press return) virtual void incrementalSearchStop(); // Abort the current incremental search (typically when user press esc) virtual void incrementalSearchAbort(); // Signals the follow mode has been enabled. void followSet( bool checked ); // Signal the on/off status of the overview has been changed. void refreshOverview(); // Make the view jump to the specified line, regardless of weither it // is on the screen or not. // (does NOT emit followDisabled() ) void jumpToLine( int line ); // Configure the setting of whether to show line number margin void setLineNumbersVisible( bool lineNumbersVisible ); // Force the next refresh to fully redraw the view by invalidating the cache. // To be used if the data might have changed. void forceRefresh(); private slots: void handlePatternUpdated(); void addToSearch(); void findNextSelected(); void findPreviousSelected(); void copy(); private: // Graphic parameters static constexpr int OVERVIEW_WIDTH = 27; static constexpr int HOOK_THRESHOLD = 600; static constexpr int PULL_TO_FOLLOW_HOOKED_HEIGHT = 10; // Width of the bullet zone, including decoration int bulletZoneWidthPx_; // Total size of all margins and decorations in pixels int leftMarginPx_; // Digits buffer (for numeric keyboard entry) DigitsBuffer digitsBuffer_; // Follow mode bool followMode_; // ElasticHook for follow mode ElasticHook followElasticHook_; // Whether to show line numbers or not bool lineNumbersVisible_; // Pointer to the CrawlerWidget's data set const AbstractLogData* logData; // Pointer to the Overview object Overview* overview_; // Pointer to the OverviewWidget, this class doesn't own it, // but is responsible for displaying it (for purely aesthetic // reasons). OverviewWidget* overviewWidget_; bool selectionStarted_; // Start of the selection (characters) QPoint selectionStartPos_; // Current end of the selection (characters) QPoint selectionCurrentEndPos_; QBasicTimer autoScrollTimer_; // Hovering state // Last line that has been hoovered on, -1 if none qint64 lastHoveredLine_; // Marks (left margin click) bool markingClickInitiated_; qint64 markingClickLine_; Selection selection_; // Position of the view, those are crucial to control drawing // firstLine gives the position of the view, // lastLineAligned == true make the bottom of the last line aligned // rather than the top of the top one. LineNumber firstLine; bool lastLineAligned; int firstCol; // Text handling int charWidth_; int charHeight_; // Popup menu QMenu* popupMenu_; QAction* copyAction_; QAction* findNextAction_; QAction* findPreviousAction_; QAction* addToSearchAction_; // Pointer to the CrawlerWidget's QFP object const QuickFindPattern* const quickFindPattern_; // Our own QuickFind object QuickFind quickFind_; #ifdef GLOGG_PERF_MEASURE_FPS // Performance measurement PerfCounter perfCounter_; #endif // Vertical offset (in pixels) at which the first line of text is written int drawingTopOffset_ = 0; // Cache pixmap and associated info struct TextAreaCache { QPixmap pixmap_; bool invalid_; int first_line_; int last_line_; int first_column_; }; struct PullToFollowCache { QPixmap pixmap_; int nb_columns_; }; TextAreaCache textAreaCache_ = { {}, true, 0, 0, 0 }; PullToFollowCache pullToFollowCache_ = { {}, 0 }; LineNumber getNbVisibleLines() const; int getNbVisibleCols() const; QPoint convertCoordToFilePos( const QPoint& pos ) const; int convertCoordToLine( int yPos ) const; int convertCoordToColumn( int xPos ) const; void displayLine( LineNumber line ); void moveSelection( int y ); void jumpToStartOfLine(); void jumpToEndOfLine(); void jumpToRightOfScreen(); void jumpToTop(); void jumpToBottom(); void selectWordAtPosition( const QPoint& pos ); void createMenu(); void considerMouseHovering( int x_pos, int y_pos ); // Search functions (for n/N) void searchUsingFunction( qint64 (QuickFind::*search_function)() ); void updateScrollBars(); void drawTextArea( QPaintDevice* paint_device, int32_t delta_y ); QPixmap drawPullToFollowBar( int width, float pixel_ratio ); void disableFollow(); // Utils functions bool isCharWord( char c ); void updateGlobalSelection(); }; #endif glogg-1.1.4/osx_installer.json0000644000175000017500000000045413076461374015403 0ustar nickonicko{ "title": "glogg", "icon": "images/glogg.icns", "background": "images/osx_installer.png", "icon-size": 128, "contents": [ { "x": 415, "y": 234, "type": "link", "path": "/Applications" }, { "x": 150, "y": 234, "type": "file", "path": "release/glogg.app" } ] } glogg-1.1.4/glogg.nsi0000644000175000017500000001325313076461374013435 0ustar nickonicko# NSIS script creating the Windows installer for glogg # Is passed to the script using -DVERSION=$(git describe) on the command line !ifndef VERSION !define VERSION 'anonymous-build' !endif # Headers !include "MUI2.nsh" !include "FileAssociation.nsh" # General OutFile "glogg-${VERSION}-setup.exe" XpStyle on SetCompressor /SOLID lzma ; Registry key to keep track of the directory we are installed in !ifdef ARCH32 InstallDir "$PROGRAMFILES\glogg" !else InstallDir "$PROGRAMFILES64\glogg" !endif InstallDirRegKey HKLM Software\glogg "" ; glogg icon ; !define MUI_ICON glogg.ico RequestExecutionLevel admin Name "glogg" Caption "glogg ${VERSION} Setup" # Pages !define MUI_WELCOMEPAGE_TITLE "Welcome to the glogg ${VERSION} Setup Wizard" !define MUI_WELCOMEPAGE_TEXT "This wizard will guide you through the installation of glogg\ , a fast, advanced log explorer.$\r$\n$\r$\n\ glogg and the Qt libraries are released under the GPL, see \ the COPYING file.$\r$\n$\r$\n$_CLICK" ; MUI_FINISHPAGE_LINK_LOCATION "http://nsis.sf.net/" !insertmacro MUI_PAGE_WELCOME ;!insertmacro MUI_PAGE_LICENSE "COPYING" # !ifdef VER_MAJOR & VER_MINOR & VER_REVISION & VER_BUILD... !insertmacro MUI_PAGE_COMPONENTS !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_PAGE_FINISH !insertmacro MUI_UNPAGE_WELCOME !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES !insertmacro MUI_UNPAGE_FINISH # Languages !insertmacro MUI_LANGUAGE "English" # Installer sections Section "glogg" glogg ; Prevent this section from being unselected SectionIn RO SetOutPath $INSTDIR File release\glogg.exe File COPYING File README.md ; Create the 'sendto' link CreateShortCut "$SENDTO\glogg.lnk" "$INSTDIR\glogg,exe" "" "$INSTDIR\glogg.exe" 0 ; Register as an otion (but not main handler) for some files (.txt, .Log, .cap) WriteRegStr HKCR "Applications\glogg.exe" "" "" WriteRegStr HKCR "Applications\glogg.exe\shell" "" "open" WriteRegStr HKCR "Applications\glogg.exe\shell\open" "FriendlyAppName" "glogg" WriteRegStr HKCR "Applications\glogg.exe\shell\open\command" "" '"$INSTDIR\glogg.exe" "%1"' WriteRegStr HKCR "*\OpenWithList\glogg.exe" "" "" WriteRegStr HKCR ".txt\OpenWithList\glogg.exe" "" "" WriteRegStr HKCR ".Log\OpenWithList\glogg.exe" "" "" WriteRegStr HKCR ".cap\OpenWithList\glogg.exe" "" "" ; Register uninstaller WriteRegExpandStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\glogg"\ "UninstallString" '"$INSTDIR\Uninstall.exe"' WriteRegExpandStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\glogg"\ "InstallLocation" "$INSTDIR" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\glogg" "DisplayName" "glogg" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\glogg" "DisplayVersion" "${VERSION}" WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\glogg" "NoModify" "1" WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\glogg" "NoRepair" "1" ; Create uninstaller WriteUninstaller "$INSTDIR\Uninstall.exe" SectionEnd Section "Qt5 Runtime libraries" qtlibs SetOutPath $INSTDIR File release\Qt5Core.dll File release\Qt5Gui.dll File release\Qt5Network.dll File release\Qt5Widgets.dll File release\libwinpthread-1.dll SetOutPath $INSTDIR\platforms File release\qwindows.dll SectionEnd Section "Create Start menu shortcut" shortcut SetShellVarContext all CreateShortCut "$SMPROGRAMS\glogg.lnk" "$INSTDIR\glogg.exe" "" "$INSTDIR\glogg.exe" 0 SectionEnd Section /o "Associate with .log files" associate ${registerExtension} "$INSTDIR\glogg.exe" ".log" "Log file" SectionEnd # Descriptions !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN !insertmacro MUI_DESCRIPTION_TEXT ${glogg} "The core files required to use glogg." !insertmacro MUI_DESCRIPTION_TEXT ${qtlibs} "Needed by glogg, you have to install these unless \ you already have the Qt5 development kit installed." !insertmacro MUI_DESCRIPTION_TEXT ${shortcut} "Create a shortcut in the Start menu for glogg." !insertmacro MUI_DESCRIPTION_TEXT ${associate} "Make glogg the default viewer for .log files." !insertmacro MUI_FUNCTION_DESCRIPTION_END # Uninstaller Section "Uninstall" Delete "$INSTDIR\Uninstall.exe" Delete "$INSTDIR\glogg.exe" Delete "$INSTDIR\README" Delete "$INSTDIR\COPYING" Delete "$INSTDIR\mingwm10.dll" Delete "$INSTDIR\libgcc_s_dw2-1.dll" Delete "$INSTDIR\QtCore4.dll" Delete "$INSTDIR\QtGui4.dll" Delete "$INSTDIR\QtNetwork4.dll" Delete "$INSTDIR\Qt5Core.dll" Delete "$INSTDIR\Qt5Gui.dll" Delete "$INSTDIR\Qt5Network.dll" Delete "$INSTDIR\Qt5Widgets.dll" Delete "$INSTDIR\libwinpthread-1.dll" Delete "$INSTDIR\platforms\qwindows.dll" RMDir "$INSTDIR" ; Remove settings in %appdata% Delete "$APPDATA\glogg\glogg.ini" RMDir "$APPDATA\glogg" DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\glogg" ; Remove settings in the registry (from glogg < 0.9) DeleteRegKey HKCU "Software\glogg" ; Remove the file associations ${unregisterExtension} ".log" "Log file" DeleteRegKey HKCR "*\OpenWithList\glogg.exe" DeleteRegKey HKCR ".txt\OpenWithList\glogg.exe" DeleteRegKey HKCR ".Log\OpenWithList\glogg.exe" DeleteRegKey HKCR ".cap\OpenWithList\glogg.exe" DeleteRegKey HKCR "Applications\glogg.exe\shell\open\command" DeleteRegKey HKCR "Applications\glogg.exe\shell\open" DeleteRegKey HKCR "Applications\glogg.exe\shell" DeleteRegKey HKCR "Applications\glogg.exe" ; Remove the shortcut, if any SetShellVarContext all Delete "$SMPROGRAMS\glogg.lnk" SectionEnd glogg-1.1.4/release-win64-x.sh0000755000175000017500000000243413076461374015013 0ustar nickonicko#!/bin/bash # Build glogg for win32 using the cross-compiler QTXDIR=$HOME/qt-x-win32 QTVERSION=5.5.1-64 BOOSTDIR=$QTXDIR/boost_1_50_0 rm release debug .obj .ui .moc -rf make clean if [ "$1" == "debug" ]; then echo "Building a debug version" qmake-qt4 glogg.pro -spec win64-x-g++ -r CONFIG+="debug win32 rtti no-dbus version_checker" BOOST_PATH=$BOOSTDIR elif [ -z "$VERSION" ]; then echo "Building default version" qmake glogg.pro -spec win64-x-g++ -r CONFIG+="release win32 rtti no-dbus version_checker" BOOST_PATH=$BOOSTDIR QMAKE_CXXFLAGS="-m64" CROSS_COMPILE="x86_64-w64-mingw32-" else echo "Building version $VERSION-x86_64" qmake glogg.pro -spec win64-x-g++ -r CONFIG+="release win32 rtti no-dbus version_checker" BOOST_PATH=$BOOSTDIR VERSION="$VERSION-x86_64" QMAKE_CXXFLAGS="-m64" CROSS_COMPILE="x86_64-w64-mingw32-" fi make -j3 cp $QTXDIR/$QTVERSION/bin/{Qt5Core,Qt5Gui,Qt5Network,Qt5Widgets}.dll release/ cp $QTXDIR/$QTVERSION/plugins/platforms/qwindows.dll release/ cp $QTXDIR/$QTVERSION/bin/{Qt5Core,Qt5Gui,Qt5Network,Qt5Widgets}d.dll debug/ cp /usr/x86_64-w64-mingw32/lib/libwinpthread-1.dll release/ if [ -z "$VERSION" ]; then VERSION=`git describe`; fi echo Generating installer for glogg-$VERSION wine $QTXDIR/NSIS/makensis -DVERSION="$VERSION-x86_64" glogg.nsi glogg-1.1.4/glogg.rc0000644000175000017500000000006213076461374013242 0ustar nickonickoIDI_ICON1 ICON DISCARDABLE "glogg48.ico" glogg-1.1.4/TODO0000644000175000017500000000102213076461374012302 0ustar nickonickoNew features: - Line number in filtered view - Matches overview (right bar) - Bookmarks - Multi files - Improved savable search text - Save position with latest files (not just the last one) - Non modal filter dialog - On/off search item window - Drag 'n drop in filters Fixes: - Error in regexp - Yellow background for errors/file changed - Keep colours (partially) of selected text - Greater number of latest files - Long file name makes the status bar extend Perf: - Search slows down when there is a large number of matches glogg-1.1.4/release-win32.sh0000755000175000017500000000133013076461374014533 0ustar nickonicko#!/bin/bash BOOSTDIR=$HOME/boost_1_43_0 PATH=/cygdrive/c/qt/2010.02.1/qt/bin:/cygdrive/c/qt/2010.02.1/mingw/bin/:$PATH if [ -z "$VERSION" ]; then echo "Building default version" qmake glogg.pro -spec win32-g++ -r CONFIG+=release BOOST_PATH=$BOOSTDIR else echo "Building version $VERSION" qmake glogg.pro -spec win32-g++ -r CONFIG+=release BOOST_PATH=$BOOSTDIR VERSION="$VERSION" fi mingw32-make cp /cygdrive/c/qt/2010.02.1/qt/bin/{QtCore4,QtGui4}.dll release/ cp /cygdrive/c/qt/2010.02.1/mingw/bin/{mingwm10,libgcc_s_dw2-1}.dll release/ if [ -z "$VERSION" ]; then VERSION=`git describe`; fi echo Generating installer for glogg-$VERSION /cygdrive/c/Program\ Files/NSIS/makensis -DVERSION=$VERSION glogg.nsi glogg-1.1.4/COPYING0000644000175000017500000010451313076461374012656 0ustar nickonicko GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . glogg-1.1.4/tests/0000755000175000017500000000000013076461374012761 5ustar nickonickoglogg-1.1.4/tests/linepositionarrayTest.cpp0000644000175000017500000001265213076461374020106 0ustar nickonicko#include "gmock/gmock.h" #include "config.h" #include "log.h" #include "data/linepositionarray.h" using namespace std; using namespace testing; class LinePositionArraySmall: public testing::Test { public: LinePositionArray line_array; LinePositionArraySmall() { line_array.append( 4 ); line_array.append( 8 ); line_array.append( 10 ); // A longer (>128) line line_array.append( 345 ); // An even longer (>16384) line line_array.append( 20000 ); // And a short one again line_array.append( 20020 ); } }; TEST_F( LinePositionArraySmall, HasACorrectSize ) { ASSERT_THAT( line_array.size(), Eq( 6 ) ); } TEST_F( LinePositionArraySmall, RememberAddedLines ) { ASSERT_THAT( line_array[0], Eq( 4 ) ); ASSERT_THAT( line_array[1], Eq( 8 ) ); ASSERT_THAT( line_array[2], Eq( 10 ) ); ASSERT_THAT( line_array[3], Eq( 345 ) ); ASSERT_THAT( line_array[4], Eq( 20000 ) ); ASSERT_THAT( line_array[5], Eq( 20020 ) ); // This one again to eliminate caching effects ASSERT_THAT( line_array[3], Eq( 345 ) ); } TEST_F( LinePositionArraySmall, FakeLFisNotKeptWhenAddingAfterIt ) { line_array.setFakeFinalLF(); ASSERT_THAT( line_array[5], Eq( 20020 ) ); line_array.append( 20030 ); ASSERT_THAT( line_array[5], Eq( 20030 ) ); } class LinePositionArrayConcatOperation: public LinePositionArraySmall { public: FastLinePositionArray other_array; LinePositionArrayConcatOperation() { other_array.append( 150000 ); other_array.append( 150023 ); } }; TEST_F( LinePositionArrayConcatOperation, SimpleConcat ) { line_array.append_list( other_array ); ASSERT_THAT( line_array.size(), Eq( 8 ) ); ASSERT_THAT( line_array[0], Eq( 4 ) ); ASSERT_THAT( line_array[1], Eq( 8 ) ); ASSERT_THAT( line_array[5], Eq( 20020 ) ); ASSERT_THAT( line_array[6], Eq( 150000 ) ); ASSERT_THAT( line_array[7], Eq( 150023 ) ); } TEST_F( LinePositionArrayConcatOperation, DoesNotKeepFakeLf ) { line_array.setFakeFinalLF(); ASSERT_THAT( line_array[5], Eq( 20020 ) ); line_array.append_list( other_array ); ASSERT_THAT( line_array[5], Eq( 150000 ) ); ASSERT_THAT( line_array.size(), Eq( 7 ) ); } class LinePositionArrayLong: public testing::Test { public: LinePositionArray line_array; LinePositionArrayLong() { // Add 126 lines (of various sizes for ( int i = 0; i < 127; i++ ) line_array.append( i * 4 ); // Line no 127 line_array.append( 514 ); } }; TEST_F( LinePositionArrayLong, LineNo128HasRightValue ) { line_array.append( 524 ); ASSERT_THAT( line_array[128], Eq( 524 ) ); } TEST_F( LinePositionArrayLong, FakeLFisNotKeptWhenAddingAfterIt ) { for ( uint64_t i = 0; i < 1000; ++i ) { uint64_t pos = 524LL + i*35LL; line_array.append( pos ); line_array.setFakeFinalLF(); ASSERT_THAT( line_array[128 + i], Eq( pos ) ); line_array.append( pos + 21LL ); ASSERT_THAT( line_array[128 + i], Eq( pos + 21LL ) ); } } class LinePositionArrayBig: public testing::Test { public: LinePositionArray line_array; LinePositionArrayBig() { line_array.append( 4 ); line_array.append( 8 ); // A very big line line_array.append( UINT32_MAX - 10 ); line_array.append( (uint64_t) UINT32_MAX + 10LL ); line_array.append( (uint64_t) UINT32_MAX + 30LL ); line_array.append( (uint64_t) 2*UINT32_MAX ); line_array.append( (uint64_t) 2*UINT32_MAX + 10LL ); line_array.append( (uint64_t) 2*UINT32_MAX + 1000LL ); line_array.append( (uint64_t) 3*UINT32_MAX ); } }; TEST_F( LinePositionArrayBig, IsTheRightSize ) { ASSERT_THAT( line_array.size(), 9 ); } TEST_F( LinePositionArrayBig, HasRightData ) { ASSERT_THAT( line_array[0], Eq( 4 ) ); ASSERT_THAT( line_array[1], Eq( 8 ) ); ASSERT_THAT( line_array[2], Eq( UINT32_MAX - 10 ) ); ASSERT_THAT( line_array[3], Eq( (uint64_t) UINT32_MAX + 10LL ) ); ASSERT_THAT( line_array[4], Eq( (uint64_t) UINT32_MAX + 30LL ) ); ASSERT_THAT( line_array[5], Eq( (uint64_t) 2LL*UINT32_MAX ) ); ASSERT_THAT( line_array[6], Eq( (uint64_t) 2LL*UINT32_MAX + 10LL ) ); ASSERT_THAT( line_array[7], Eq( (uint64_t) 2LL*UINT32_MAX + 1000LL ) ); ASSERT_THAT( line_array[8], Eq( (uint64_t) 3LL*UINT32_MAX ) ); } TEST_F( LinePositionArrayBig, FakeLFisNotKeptWhenAddingAfterIt ) { for ( uint64_t i = 0; i < 1000; ++i ) { uint64_t pos = 3LL*UINT32_MAX + 524LL + i*35LL; line_array.append( pos ); line_array.setFakeFinalLF(); ASSERT_THAT( line_array[9 + i], Eq( pos ) ); line_array.append( pos + 21LL ); ASSERT_THAT( line_array[9 + i], Eq( pos + 21LL ) ); } } class LinePositionArrayBigConcat: public testing::Test { public: LinePositionArray line_array; FastLinePositionArray other_array; LinePositionArrayBigConcat() { line_array.append( 4 ); line_array.append( 8 ); other_array.append( UINT32_MAX + 10 ); other_array.append( UINT32_MAX + 30 ); } }; TEST_F( LinePositionArrayBigConcat, SimpleBigConcat ) { line_array.append_list( other_array ); ASSERT_THAT( line_array.size(), Eq( 4 ) ); ASSERT_THAT( line_array[0], Eq( 4 ) ); ASSERT_THAT( line_array[1], Eq( 8 ) ); ASSERT_THAT( line_array[2], Eq( UINT32_MAX + 10 ) ); ASSERT_THAT( line_array[3], Eq( UINT32_MAX + 30 ) ); } glogg-1.1.4/tests/coverage.sh0000755000175000017500000000054213076461374015114 0ustar nickonicko#!/bin/bash FILES="logdata.cpp logfiltereddata.cpp logdataworkerthread.cpp logfiltereddataworkerthread.cpp" for i in $FILES; do gcov -b $i | perl -e "while(<>) { if (/^File '.*$i'/) { \$print = 1; } if ( \$print ) { if (/:creating '/) { \$print = 0; print \"\n\" } else { print; } } }" done mkdir coverage mv *.gcov coverage/ rm *.{gcda,gcno} glogg-1.1.4/tests/testlogdata.h0000644000175000017500000000076013076461374015450 0ustar nickonicko#include #include #include class TestLogData: public QObject { Q_OBJECT private slots: void initTestCase(); void simpleLoad(); void multipleLoad(); void changingFile(); void sequentialRead(); void sequentialReadExpanded(); void randomPageRead(); void randomPageReadExpanded(); public slots: void loadingFinished(); private: bool generateDataFiles(); }; glogg-1.1.4/tests/main.cpp0000644000175000017500000000047613076461374014420 0ustar nickonicko#include #include "testlogdata.h" #include "testlogfiltereddata.h" int main(int argc, char** argv) { QApplication app(argc, argv); int retval(0); retval += QTest::qExec(&TestLogData(), argc, argv); retval += QTest::qExec(&TestLogFilteredData(), argc, argv); return (retval ? 1 : 0); } glogg-1.1.4/tests/itests.cpp0000644000175000017500000000040413076461374014776 0ustar nickonicko#include "gmock/gmock.h" #include int main(int argc, char *argv[]) { QApplication a( argc, argv ); ::testing::InitGoogleTest(&argc, argv); int iReturn = RUN_ALL_TESTS(); // qDebug()<<"rcode:"< #include "winfilewatcher.h" using namespace std; class FileWatcherBehaviour: public testing::Test { public: shared_ptr file_watcher; void SetUp() override { #ifdef _WIN32 // file_watcher = make_shared(); #endif } }; TEST_F( FileWatcherBehaviour, DetectsAnAppendedFile ) { EXPECT_EQ(1, 1); } glogg-1.1.4/tests/tests.pro0000644000175000017500000000201013076461374014636 0ustar nickonickoTEMPLATE = app DEPENDPATH += ./ ../ INCLUDEPATH += ./ ../ DESTDIR = ./ CONFIG += qtestlib debug QMAKE_CXXFLAGS += -O2 -fpermissive isEmpty(TMPDIR) { DEFINES = TMPDIR=\\\"/tmp\\\" } else { DEFINES = TMPDIR=\\\"$${TMPDIR}\\\" } mac { CONFIG -= app_bundle } TARGET = logcrawler_tests HEADERS += testlogdata.h testlogfiltereddata.h\ ../src/data/logdata.h ../src/data/logfiltereddata.h ../src/data/logdataworkerthread.h\ ../src/data/abstractlogdata.h ../src/data/logfiltereddataworkerthread.h\ ../src/platformfilewatcher.h ../src/marks.h SOURCES += testlogdata.cpp testlogfiltereddata.cpp \ ../src/data/abstractlogdata.cpp ../src/data/logdata.cpp ../src/main.cpp\ ../src/data/logfiltereddata.cpp ../src/data/logdataworkerthread.cpp\ ../src/data/logfiltereddataworkerthread.cpp\ ../src/filewatcher.cpp ../src/marks.cpp coverage:QMAKE_CXXFLAGS += -g -fprofile-arcs -ftest-coverage -O0 coverage:QMAKE_LFLAGS += -fprofile-arcs -ftest-coverage prof:QMAKE_CXXFLAGS += -pg prof:QMAKE_LFLAGS += -pg glogg-1.1.4/tests/test_utils.h0000644000175000017500000000255413076461374015337 0ustar nickonicko#ifndef TEST_UTILS_H #define TEST_UTILS_H #include "gmock/gmock.h" #include #include struct TestTimer { TestTimer() : TestTimer( ::testing::UnitTest::GetInstance()->current_test_info()->test_case_name() ) { text_ += std::string {"."} + std::string {::testing::UnitTest::GetInstance()->current_test_info()->name() }; } TestTimer(const std::string& text) : Start { std::chrono::system_clock::now() } , text_ {text} {} virtual ~TestTimer() { using namespace std; Stop = chrono::system_clock::now(); Elapsed = chrono::duration_cast(Stop - Start); cout << endl << text_ << " elapsed time = " << Elapsed.count() * 0.001 << "ms" << endl; } std::chrono::time_point Start; std::chrono::time_point Stop; std::chrono::microseconds Elapsed; std::string text_; }; class SafeQSignalSpy : public QSignalSpy { public: template SafeQSignalSpy( Args&&... args ) : QSignalSpy( std::forward(args)... ) {} bool safeWait( int timeout = 10000 ) { // If it has already been received bool result = count() > 0; if ( ! result ) { result = wait( timeout ); } return result; } }; #endif glogg-1.1.4/tests/CMakeLists.txt0000644000175000017500000001057713076461374015533 0ustar nickonickoINCLUDE (CheckSymbolExists) INCLUDE (CheckCXXSourceCompiles) INCLUDE (CheckCXXCompilerFlag) cmake_minimum_required(VERSION 2.8.12) project(glogg_tests) # Qt find_program(QT_QMAKE_EXECUTABLE NAMES qmake PATHS $ENV{QT_DIR}/bin) find_package(Qt5Widgets REQUIRED) find_package(Qt5Test REQUIRED) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) check_cxx_compiler_flag(-Wall HAS_WALL) if(HAS_WALL) set(PARANOID_FLAGS "${PARANOID_FLAGS} -Wall") endif(HAS_WALL) # Compiler check_cxx_compiler_flag("-std=c++11" COMPILER_SUPPORTS_CXX11) check_cxx_compiler_flag("-std=c++0x" COMPILER_SUPPORTS_CXX0X) if(COMPILER_SUPPORTS_CXX11) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") elseif(COMPILER_SUPPORTS_CXX0X) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x") else() message(STATUS "The compiler ${CMAKE_CXX_COMPILER} has no C++11 support.") endif() set(CMAKE_REQUIRED_FLAGS "-std=c++11") check_cxx_source_compiles(" #include int main(int, int) { auto test = std::make_unique(); return 0; } " HAVE_MAKE_UNIQUE) check_cxx_source_compiles(" #include struct Base { virtual void func() {} }; struct Derived : public Base { void func() override {} } int main(int, int) { return 0; } " HAVE_OVERRIDE) configure_file(../config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h) # Configuration check_symbol_exists( symlink "unistd.h" HAVE_SYMLINK ) check_symbol_exists( htons "arpa/inet.h" HAVE_HTONS ) # find_library(WS2_LIBRARY NAMES ws2_32) # Libs set(LIBS gmock gtest) # Setup testing enable_testing() include_directories( $ENV{GMOCK_HOME}/include $ENV{GMOCK_HOME}/gtest/include $ENV{BOOST_ROOT}/ ../src/ ) link_directories($ENV{GMOCK_HOME}/mybuild $ENV{GMOCK_HOME}/mybuild/gtest) # Sources set(glogg_SOURCES ../src/session.cpp ../src/data/abstractlogdata.cpp ../src/data/logdata.cpp ../src/data/logfiltereddata.cpp ../src/data/logfiltereddataworkerthread.cpp ../src/data/logdataworkerthread.cpp ../src/data/compressedlinestorage.cpp ../src/mainwindow.cpp ../src/crawlerwidget.cpp ../src/abstractlogview.cpp ../src/logmainview.cpp ../src/filteredview.cpp ../src/optionsdialog.cpp ../src/persistentinfo.cpp ../src/configuration.cpp ../src/filtersdialog.cpp ../src/filterset.cpp ../src/savedsearches.cpp ../src/infoline.cpp ../src/menuactiontooltipbehavior.cpp ../src/selection.cpp ../src/quickfind.cpp ../src/quickfindpattern.cpp ../src/quickfindwidget.cpp ../src/sessioninfo.cpp ../src/recentfiles.cpp ../src/overview.cpp ../src/overviewwidget.cpp ../src/marks.cpp ../src/quickfindmux.cpp ../src/signalmux.cpp ../src/qtfilewatcher.cpp ../src/tabbedcrawlerwidget.cpp ../src/filewatcher.cpp ../src/watchtowerlist.cpp ../src/watchtower.cpp ../src/viewtools.cpp ../src/encodingspeculator.cpp # ../src/platformfilewatcher.cpp ) set(glogg_HEADERS ../src/mainwindow.h ../src/marks.h ../src/filewatcher.h) # Unit tests set(glogg_UTESTS # watchtowerTest.cpp linepositionarrayTest.cpp encodingspeculatorTest.cpp ) # Integration tests set(glogg_ITESTS logdataTest.cpp logfiltereddataTest.cpp ) # Performance tests set(glogg_PTESTS logdataPerfTest.cpp logfiltereddataPerfTest.cpp ) # Options if (WIN32) set(FileWatcherEngine_SOURCES ../src/winwatchtowerdriver.cpp ) elseif (LINUX) # set(FileWatcherEngine_SOURCES ../src/inotifywatchtowerdriver.cpp) endif (WIN32) # Compiler flags set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${PARANOID_FLAGS} -fPIC -DGLOGG_VERSION=\\\"unit_tests\\\" -g -gdwarf-2") # Add test cpp file add_executable(glogg_tests ${glogg_SOURCES} ${FileWatcherEngine_SOURCES} ${glogg_UTESTS} ) # Link test executable against gtest & gtest_main target_link_libraries(glogg_tests ${LIBS} gtest_main pthread Qt5::Widgets) add_executable(glogg_itests ${glogg_SOURCES} ${FileWatcherEngine_SOURCES} ${glogg_ITESTS} itests.cpp ) target_link_libraries(glogg_itests ${LIBS} pthread Qt5::Widgets Qt5::Test) add_executable(glogg_ptests ${glogg_SOURCES} ${FileWatcherEngine_SOURCES} ${glogg_PTESTS} itests.cpp ) target_link_libraries(glogg_ptests ${LIBS} pthread Qt5::Widgets Qt5::Test) add_test( NAME glogg_tests COMMAND glogg_tests ) glogg-1.1.4/tests/watchtowerTest.cpp0000644000175000017500000003610313076461374016517 0ustar nickonicko#include "gmock/gmock.h" #include "config.h" #include #include #include #include #include #include #include #include "log.h" #include "watchtower.h" #ifdef _WIN32 # include "winwatchtowerdriver.h" using PlatformWatchTower = WatchTower; #else # include "inotifywatchtowerdriver.h" using PlatformWatchTower = WatchTower; #endif using namespace std; using namespace testing; class WatchTowerBehaviour: public testing::Test { public: shared_ptr watch_tower = make_shared(); const char* createTempName() { const char* name; #if _WIN32 name = _tempnam( "c:\\temp", "glogg_test" ); #else name = tmpnam( nullptr ); #endif return name; } string createTempEmptyFile( string file_name = "" ) { const char* name; if ( ! file_name.empty() ) { name = file_name.c_str(); } else { // I know tmpnam is bad but I need control over the file // and it is the only one which exits on Windows. name = createTempName(); } int fd = creat( name, S_IRUSR | S_IWUSR ); close( fd ); return string( name ); } string getNonExistingFileName() { #if _WIN32 return string( _tempnam( "c:\\temp", "inexistant" ) ); #else return string( tmpnam( nullptr ) ); #endif } WatchTowerBehaviour() { // Default to quiet, but increase to debug FILELog::setReportingLevel( logERROR ); } }; TEST_F( WatchTowerBehaviour, AcceptsAnExistingFileToWatch ) { auto file_name = createTempEmptyFile(); auto registration = watch_tower->addFile( file_name, [] (void) { } ); } TEST_F( WatchTowerBehaviour, AcceptsANonExistingFileToWatch ) { auto registration = watch_tower->addFile( getNonExistingFileName(), [] (void) { } ); } /*****/ class WatchTowerSingleFile: public WatchTowerBehaviour { public: static const int TIMEOUT; mutex mutex_; condition_variable cv_; string file_name; Registration registration; int notification_received = 0; Registration registerFile( const string& filename ) { weak_ptr weakHeartbeat( heartbeat_ ); auto reg = watch_tower->addFile( filename, [this, weakHeartbeat] (void) { // Ensure the fixture object is still alive using the heartbeat if ( auto keep = weakHeartbeat.lock() ) { unique_lock lock(mutex_); ++notification_received; cv_.notify_one(); } } ); return reg; } WatchTowerSingleFile() : heartbeat_( shared_ptr( (void*) 0xDEADC0DE, [] (void*) {} ) ) { file_name = createTempEmptyFile(); registration = registerFile( file_name ); } ~WatchTowerSingleFile() { remove( file_name.c_str() ); } bool waitNotificationReceived( int number = 1, int timeout_ms = TIMEOUT ) { unique_lock lock(mutex_); bool result = ( cv_.wait_for( lock, std::chrono::milliseconds(timeout_ms), [this, number] { return notification_received >= number; } ) ); // Reinit the notification notification_received = 0; return result; } void appendDataToFile( const string& file_name ) { static const char* string = "Test line\n"; int fd = open( file_name.c_str(), O_WRONLY | O_APPEND ); write( fd, (void*) string, strlen( string ) ); close( fd ); } private: // Heartbeat ensures the object is still alive shared_ptr heartbeat_; }; #ifdef _WIN32 const int WatchTowerSingleFile::TIMEOUT = 2000; #else const int WatchTowerSingleFile::TIMEOUT = 20; #endif TEST_F( WatchTowerSingleFile, SignalsWhenAWatchedFileIsAppended ) { appendDataToFile( file_name ); ASSERT_TRUE( waitNotificationReceived() ); } TEST_F( WatchTowerSingleFile, SignalsWhenAWatchedFileIsRemoved) { remove( file_name.c_str() ); ASSERT_TRUE( waitNotificationReceived() ); } TEST_F( WatchTowerSingleFile, SignalsWhenADeletedFileReappears ) { remove( file_name.c_str() ); waitNotificationReceived(); createTempEmptyFile( file_name ); ASSERT_TRUE( waitNotificationReceived() ); } TEST_F( WatchTowerSingleFile, SignalsWhenAReappearedFileIsAppended ) { remove( file_name.c_str() ); waitNotificationReceived(); createTempEmptyFile( file_name ); waitNotificationReceived(); appendDataToFile( file_name ); ASSERT_TRUE( waitNotificationReceived() ); } TEST_F( WatchTowerSingleFile, StopSignalingWhenWatchDeleted ) { auto second_file_name = createTempEmptyFile(); std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) ); // Ensure file creation has been 'digested' { auto second_registration = registerFile( second_file_name ); appendDataToFile( second_file_name ); ASSERT_TRUE( waitNotificationReceived() ); } // The registration will be removed here. appendDataToFile( second_file_name ); ASSERT_FALSE( waitNotificationReceived() ); remove( second_file_name.c_str() ); } TEST_F( WatchTowerSingleFile, SignalsWhenSameFileIsFollowedMultipleTimes ) { auto second_file_name = createTempEmptyFile(); for ( int i = 0; i < 100; i++ ) { auto second_registration = registerFile( second_file_name ); appendDataToFile( second_file_name ); ASSERT_TRUE( waitNotificationReceived() ); } // The registration will be removed here. appendDataToFile( second_file_name ); ASSERT_FALSE( waitNotificationReceived() ); remove( second_file_name.c_str() ); } TEST_F( WatchTowerSingleFile, TwoWatchesOnSameFileYieldsTwoNotifications ) { auto second_registration = registerFile( file_name ); appendDataToFile( file_name ); ASSERT_TRUE( waitNotificationReceived( 2 ) ); } TEST_F( WatchTowerSingleFile, RemovingOneWatchOfTwoStillYieldsOneNotification ) { { auto second_registration = registerFile( file_name ); } appendDataToFile( file_name ); ASSERT_TRUE( waitNotificationReceived( 1 ) ); } TEST_F( WatchTowerSingleFile, RenamingTheFileYieldsANotification ) { auto new_file_name = createTempName(); rename( file_name.c_str(), new_file_name ); ASSERT_TRUE( waitNotificationReceived() ); rename( new_file_name, file_name.c_str() ); } TEST_F( WatchTowerSingleFile, RenamingAFileToTheWatchedNameYieldsANotification ) { remove( file_name.c_str() ); waitNotificationReceived(); std::string new_file_name = createTempEmptyFile(); appendDataToFile( new_file_name ); rename( new_file_name.c_str(), file_name.c_str() ); ASSERT_TRUE( waitNotificationReceived() ); } /*****/ #ifdef HAVE_SYMLINK class WatchTowerSymlink: public WatchTowerSingleFile { public: string symlink_name; void SetUp() override { file_name = createTempEmptyFile(); symlink_name = createTempEmptyFile(); remove( symlink_name.c_str() ); symlink( file_name.c_str(), symlink_name.c_str() ); registration = registerFile( symlink_name ); } void TearDown() override { remove( symlink_name.c_str() ); remove( file_name.c_str() ); } }; TEST_F( WatchTowerSymlink, AppendingToTheSymlinkYieldsANotification ) { appendDataToFile( symlink_name ); ASSERT_TRUE( waitNotificationReceived() ); } TEST_F( WatchTowerSymlink, AppendingToTheTargetYieldsANotification ) { appendDataToFile( file_name ); ASSERT_TRUE( waitNotificationReceived() ); } TEST_F( WatchTowerSymlink, RemovingTheSymlinkYieldsANotification ) { remove( symlink_name.c_str() ); ASSERT_TRUE( waitNotificationReceived() ); } TEST_F( WatchTowerSymlink, RemovingTheTargetYieldsANotification ) { remove( file_name.c_str() ); ASSERT_TRUE( waitNotificationReceived() ); } TEST_F( WatchTowerSymlink, ReappearingSymlinkYieldsANotification ) { auto new_target = createTempEmptyFile(); remove( symlink_name.c_str() ); waitNotificationReceived(); symlink( new_target.c_str(), symlink_name.c_str() ); ASSERT_TRUE( waitNotificationReceived() ); remove( new_target.c_str() ); } TEST_F( WatchTowerSymlink, DataAddedInAReappearingSymlinkYieldsANotification ) { auto new_target = createTempEmptyFile(); remove( symlink_name.c_str() ); waitNotificationReceived(); symlink( new_target.c_str(), symlink_name.c_str() ); waitNotificationReceived(); appendDataToFile( new_target ); ASSERT_TRUE( waitNotificationReceived() ); remove( new_target.c_str() ); } #endif //HAVE_SYMLINK /*****/ TEST( WatchTowerLifetime, RegistrationCanBeDeletedWhenWeAreDead ) { auto mortal_watch_tower = new PlatformWatchTower(); auto reg = mortal_watch_tower->addFile( "/tmp/test_file", [] (void) { } ); delete mortal_watch_tower; // reg will be destroyed after the watch_tower } /*****/ class WatchTowerDirectories: public WatchTowerSingleFile { public: string second_dir_name; string second_file_name; string third_file_name; Registration registration_two; Registration registration_three; WatchTowerDirectories() { second_dir_name = createTempDir(); second_file_name = createTempEmptyFileInDir( second_dir_name ); third_file_name = createTempEmptyFileInDir( second_dir_name ); } ~WatchTowerDirectories() { remove( third_file_name.c_str() ); remove( second_file_name.c_str() ); removeDir( second_dir_name ); } string createTempDir() { #ifdef _WIN32 static int counter = 1; char temp_dir[255]; GetTempPath( sizeof temp_dir, temp_dir ); string dir_name = string { temp_dir } + string { "\\test" } + to_string( counter++ ); mkdir( dir_name.c_str() ); return dir_name; #else char dir_template[] = "/tmp/XXXXXX"; return { mkdtemp( dir_template ) }; #endif } string createTempEmptyFileInDir( const string& dir ) { static int counter = 1; return createTempEmptyFile( dir + std::string { "/temp" } + to_string( counter++ ) ); } void removeDir( const string& name ) { rmdir( name.c_str() ); } }; TEST_F( WatchTowerDirectories, FollowThreeFilesInTwoDirs ) { registration_two = registerFile( second_file_name ); registration_three = registerFile( third_file_name ); ASSERT_THAT( watch_tower->numberWatchedDirectories(), Eq( 2 ) ); } TEST_F( WatchTowerDirectories, FollowTwoFilesInTwoDirs ) { registration_two = registerFile( second_file_name ); { auto temp_registration_three = registerFile( third_file_name ); } ASSERT_THAT( watch_tower->numberWatchedDirectories(), Eq( 2 ) ); } TEST_F( WatchTowerDirectories, FollowOneFileInOneDir ) { { auto temp_registration_two = registerFile( second_file_name ); auto temp_registration_three = registerFile( third_file_name ); ASSERT_THAT( watch_tower->numberWatchedDirectories(), Eq( 2 ) ); } ASSERT_THAT( watch_tower->numberWatchedDirectories(), Eq( 1 ) ); } /*****/ class WatchTowerInexistantDirectory: public WatchTowerDirectories { public: WatchTowerInexistantDirectory() { test_dir = createTempDir(); rmdir( test_dir.c_str() ); } ~WatchTowerInexistantDirectory() { rmdir( test_dir.c_str() ); } string test_dir; }; TEST_F( WatchTowerInexistantDirectory, LaterCreatedDirIsFollowed ) { /* Dir (and file) don't exist */ auto file_name = createTempEmptyFileInDir( test_dir ); { auto registration = registerFile( file_name ); #ifdef _WIN32 mkdir( test_dir.c_str() ); #else mkdir( test_dir.c_str(), 0777 ); #endif createTempEmptyFile( file_name ); } auto registration2 = registerFile( file_name ); appendDataToFile( file_name ); ASSERT_TRUE( waitNotificationReceived() ); remove( file_name.c_str() ); } /*****/ #ifdef _WIN32 class WinNotificationInfoListTest : public testing::Test { public: using Action = WinNotificationInfo::Action; struct Buffer { uint32_t next_addr; uint32_t action; uint32_t filename_length; wchar_t filename[13]; }; static struct Buffer buffer[2]; WinNotificationInfoList list { reinterpret_cast( buffer ), sizeof( buffer ) }; WinNotificationInfoList::iterator iterator { std::begin( list ) }; }; struct WinNotificationInfoListTest::Buffer WinNotificationInfoListTest::buffer[] = { { 40, 1, 26, L"Filename.txt" }, { 0, 3, 18, L"file2.txt" } }; TEST_F( WinNotificationInfoListTest, FirstNotificationCanBeObtained ) { auto notification = *iterator; ASSERT_THAT( ¬ification, NotNull() ); } TEST_F( WinNotificationInfoListTest, FirstNotificationHasRightAction ) { ASSERT_THAT( (*iterator).action(), Eq( Action::ADDED ) ); } TEST_F( WinNotificationInfoListTest, FirstNotificationHasRightFileName ) { ASSERT_THAT( (*iterator).fileName(), Eq( std::wstring( L"Filename.txt\0", 13 ) ) ); } TEST_F( WinNotificationInfoListTest, SecondNotificationCanBeObtained ) { ++iterator; auto notification = *iterator; ASSERT_THAT( ¬ification, NotNull() ); } TEST_F( WinNotificationInfoListTest, SecondNotificationIsCorrect ) { iterator++; ASSERT_THAT( iterator->action(), Eq( Action::MODIFIED ) ); ASSERT_THAT( iterator->fileName(), Eq( L"file2.txt" ) ); } TEST_F( WinNotificationInfoListTest, CanBeIteratedByFor ) { for ( auto notification : list ) { notification.action(); } } #endif /*****/ #ifdef _WIN32 class WatchTowerPolling : public WatchTowerSingleFile { public: WatchTowerPolling() : WatchTowerSingleFile() { // FILELog::setReportingLevel( logDEBUG ); fd_ = open( file_name.c_str(), O_WRONLY | O_APPEND ); } ~WatchTowerPolling() { close( fd_ ); } void appendDataToFileWoClosing() { static const char* string = "Test line\n"; write( fd_, (void*) string, strlen( string ) ); } int fd_; }; TEST_F( WatchTowerPolling, OpenFileDoesNotGenerateImmediateNotification ) { appendDataToFileWoClosing(); ASSERT_FALSE( waitNotificationReceived() ); } TEST_F( WatchTowerPolling, OpenFileYieldsAPollNotification ) { std::this_thread::sleep_for( std::chrono::milliseconds( 500 ) ); watch_tower->setPollingInterval( 500 ); appendDataToFileWoClosing(); ASSERT_TRUE( waitNotificationReceived() ); } TEST_F( WatchTowerPolling, UnchangedFileDoesNotYieldANotification ) { watch_tower->setPollingInterval( 500 ); ASSERT_FALSE( waitNotificationReceived() ); } TEST_F( WatchTowerPolling, FileYieldsAnImmediateNotification ) { watch_tower->setPollingInterval( 4000 ); appendDataToFile( file_name ); ASSERT_TRUE( waitNotificationReceived( 1, 2000 ) ); } TEST_F( WatchTowerPolling, PollIsDelayedIfImmediateNotification ) { watch_tower->setPollingInterval( 500 ); appendDataToFile( file_name ); waitNotificationReceived(); appendDataToFileWoClosing(); std::this_thread::sleep_for( std::chrono::milliseconds( 400 ) ); ASSERT_FALSE( waitNotificationReceived( 1, 250 ) ); ASSERT_TRUE( waitNotificationReceived() ); } #endif glogg-1.1.4/tests/testlogdata.cpp0000644000175000017500000001172113076461374016002 0ustar nickonicko/* * Copyright (C) 2009, 2010 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include #include #include #include "testlogdata.h" #include "logdata.h" #if !defined( TMPDIR ) #define TMPDIR "/tmp" #endif static const qint64 VBL_NB_LINES = 4999999LL; static const int VBL_LINE_PER_PAGE = 70; static const char* vbl_format="LOGDATA is a part of glogg, we are going to test it thoroughly, this is line\t\t%07d\n"; static const int VBL_LINE_LENGTH = (76+2+7) ; // Without the final '\n' ! static const int VBL_VISIBLE_LINE_LENGTH = (76+8+4+7); // Without the final '\n' ! static const qint64 SL_NB_LINES = 5000LL; static const int SL_LINE_PER_PAGE = 70; static const char* sl_format="LOGDATA is a part of glogg, we are going to test it thoroughly, this is line %06d\n"; static const int SL_LINE_LENGTH = 83; // Without the final '\n' ! static const char* partial_line_begin = "123... beginning of line."; static const char* partial_line_end = " end of line 123.\n"; void TestLogData::initTestCase() { QVERIFY( generateDataFiles() ); } void TestLogData::simpleLoad() { LogData logData; QSignalSpy progressSpy( &logData, SIGNAL( loadingProgressed( int ) ) ); // Register for notification file is loaded connect( &logData, SIGNAL( loadingFinished( bool ) ), this, SLOT( loadingFinished() ) ); QBENCHMARK { logData.attachFile( TMPDIR "/verybiglog.txt" ); // Wait for the loading to be done { QApplication::exec(); } } // Disconnect all signals disconnect( &logData, 0 ); } void TestLogData::multipleLoad() { LogData logData; QSignalSpy finishedSpy( &logData, SIGNAL( loadingFinished( bool ) ) ); // Register for notification file is loaded connect( &logData, SIGNAL( loadingFinished( bool ) ), this, SLOT( loadingFinished() ) ); // Start loading the VBL logData.attachFile( TMPDIR "/verybiglog.txt" ); // Immediately interrupt the loading logData.interruptLoading(); // and wait for the signal QApplication::exec(); // Check we have an empty file QCOMPARE( finishedSpy.count(), 1 ); // TODO: check loadingFinished arg == false QCOMPARE( logData.getNbLine(), 0LL ); QCOMPARE( logData.getMaxLength(), 0 ); QCOMPARE( logData.getFileSize(), 0LL ); // Restart the VBL logData.attachFile( TMPDIR "/verybiglog.txt" ); // Ensure the counting has started { QMutex mutex; QWaitCondition sleep; // sleep.wait( &mutex, 10 ); } // Load the SL (should block until VBL is fully indexed) logData.attachFile( TMPDIR "/smalllog.txt" ); // and wait for the 2 signals (one for each file) QApplication::exec(); QApplication::exec(); // Check we have the small log loaded QCOMPARE( finishedSpy.count(), 3 ); QCOMPARE( logData.getNbLine(), SL_NB_LINES ); QCOMPARE( logData.getMaxLength(), SL_LINE_LENGTH ); QCOMPARE( logData.getFileSize(), SL_NB_LINES * (SL_LINE_LENGTH+1LL) ); // Restart the VBL again logData.attachFile( TMPDIR "/verybiglog.txt" ); // Immediately interrupt the loading logData.interruptLoading(); // and wait for the signal QApplication::exec(); // Check the small log has been restored QCOMPARE( finishedSpy.count(), 4 ); QCOMPARE( logData.getNbLine(), SL_NB_LINES ); QCOMPARE( logData.getMaxLength(), SL_LINE_LENGTH ); QCOMPARE( logData.getFileSize(), SL_NB_LINES * (SL_LINE_LENGTH+1LL) ); // Disconnect all signals disconnect( &logData, 0 ); } // // Private functions // void TestLogData::loadingFinished() { QApplication::quit(); } bool TestLogData::generateDataFiles() { char newLine[90]; QFile file( TMPDIR "/verybiglog.txt" ); if ( file.open( QIODevice::WriteOnly ) ) { for (int i = 0; i < VBL_NB_LINES; i++) { snprintf(newLine, 89, vbl_format, i); file.write( newLine, qstrlen(newLine) ); } } else { return false; } file.close(); file.setFileName( TMPDIR "/smalllog.txt" ); if ( file.open( QIODevice::WriteOnly ) ) { for (int i = 0; i < SL_NB_LINES; i++) { snprintf(newLine, 89, sl_format, i); file.write( newLine, qstrlen(newLine) ); } } else { return false; } file.close(); return true; } glogg-1.1.4/tests/test-results.txt0000644000175000017500000000443013076461374016201 0ustar nickonicko page 99999 page 9999 LogData size 9999 Legacy 1305.5 11.009 1,048,576B 16,777,216B QListString 0.055 0.0027 3,777,200B 18,644,700B Memory test: everything commented except RandomPage Memory using top: 99999 (8.5Mib file): legacy 44m,20m qliststr 33m,11m 4999999 (428MiB file): legacy 464m,442m qliststr 1090m,1.0g 'Legacy' implementation: ********* Start testing of TestLogData ********* Config: Using QTest library 4.5.0, Qt 4.5.0 PASS : TestLogData::initTestCase() - 14:12:30.621 DEBUG: Found 99999 lines. - 14:12:30.639 DEBUG: Found 99999 lines. - 14:12:30.707 DEBUG: Found 99999 lines. - 14:12:30.717 DEBUG: Found 99999 lines. - 14:12:30.728 DEBUG: Found 99999 lines. RESULT : TestLogData::simpleLoad(): 10 msec per iteration (total: 21, iterations: 2) PASS : TestLogData::simpleLoad() - 14:12:30.780 DEBUG: Found 99999 lines. RESULT : TestLogData::sequentialRead(): 650,750 msec per iteration (total: 650750, iterations: 1) PASS : TestLogData::sequentialRead() - 14:23:21.586 DEBUG: Found 99999 lines. RESULT : TestLogData::randomPageRead(): 1,305,466 msec per iteration (total: 1305466, iterations: 1) PASS : TestLogData::randomPageRead() PASS : TestLogData::cleanupTestCase() Totals: 5 passed, 0 failed, 0 skipped ********* Finished testing of TestLogData ********* With 9999 lines: RESULT : TestLogData::simpleLoad(): 1.3 msec per iteration (total: 21, iterations: 16) PASS : TestLogData::simpleLoad() - 14:53:26.232 DEBUG: Found 9999 lines. RESULT : TestLogData::sequentialRead(): 6,475 msec per iteration (total: 6475, iterations: 1) PASS : TestLogData::sequentialRead() - 14:53:32.713 DEBUG: Found 9999 lines. RESULT : TestLogData::randomPageRead(): 11,009 msec per iteration (total: 11009, iterations: 1) PASS : TestLogData::randomPageRead() PASS : TestLogData::cleanupTestCase() Totals: 5 passed, 0 failed, 0 skipped Memory: 1,048,576B Tab expansion fix: before: TestLogData::sequentialRead(): 58,011 msec TestLogData::randomPageRead(): 11,376 msec after step 1: TestLogData::sequentialRead(): 55,662 msec TestLogData::sequentialReadExpanded(): 91,141 msec TestLogData::randomPageRead(): 10,592 msec TestLogData::randomPageReadExpanded(): 183,567 msec glogg-1.1.4/tests/testlogfiltereddata.cpp0000644000175000017500000005110213076461374017516 0ustar nickonicko/* * Copyright (C) 2009, 2010, 2011 Nicolas Bonnefon and other contributors * * This file is part of glogg. * * glogg 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. * * glogg 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 glogg. If not, see . */ #include #include #include #include "testlogfiltereddata.h" #include "logdata.h" #include "logfiltereddata.h" #if !defined( TMPDIR ) #define TMPDIR "/tmp" #endif static const qint64 ML_NB_LINES = 15000LL; static const char* ml_format="LOGDATA is a part of glogg, we are going to test it thoroughly, this is line\t\t%06d\n"; static const int ML_VISIBLE_LINE_LENGTH = (76+8+4+6); // Without the final '\n' ! static const qint64 SL_NB_LINES = 2000LL; static const char* sl_format="LOGDATA is a part of glogg, we are going to test it thoroughly, this is line %06d\n"; static const char* partial_line_begin = "123... beginning of line."; static const char* partial_line_end = " end of line 123.\n"; static const char* partial_nonmatching_line_begin = "Beginning of line."; TestLogFilteredData::TestLogFilteredData() : QObject(), loadingFinishedMutex_(), searchProgressedMutex_(), loadingFinishedCondition_(), searchProgressedCondition_() { loadingFinished_received_ = false; loadingFinished_read_ = false; searchProgressed_received_ = false; searchProgressed_read_ = false; } void TestLogFilteredData::initTestCase() { QVERIFY( generateDataFiles() ); } void TestLogFilteredData::simpleSearch() { logData_ = new LogData(); // Register for notification file is loaded connect( logData_, SIGNAL( loadingFinished( bool ) ), this, SLOT( loadingFinished() ) ); filteredData_ = logData_->getNewFilteredData(); connect( filteredData_, SIGNAL( searchProgressed( int, int ) ), this, SLOT( searchProgressed( int, int ) ) ); QFuture future = QtConcurrent::run(this, &TestLogFilteredData::simpleSearchTest); QApplication::exec(); disconnect( filteredData_, 0 ); disconnect( logData_, 0 ); delete filteredData_; delete logData_; } void TestLogFilteredData::simpleSearchTest() { // First load the tests file logData_->attachFile( TMPDIR "/mediumlog.txt" ); // Wait for the loading to be done waitLoadingFinished(); QCOMPARE( logData_->getNbLine(), ML_NB_LINES ); signalLoadingFinishedRead(); // Now perform a simple search qint64 matches[] = { 0, 15, 20, 135 }; QBENCHMARK { // Start the search filteredData_->runSearch( QRegExp( "123" ) ); // And check we receive data in 4 chunks (the first being empty) for ( int i = 0; i < 4; i++ ) { std::pair progress = waitSearchProgressed(); // FIXME: The test for this is unfortunately not reliable // (race conditions) // QCOMPARE( (qint64) progress.first, matches[i] ); signalSearchProgressedRead(); } } QCOMPARE( filteredData_->getNbLine(), matches[3] ); // Check the search QCOMPARE( filteredData_->isLineInMatchingList( 123 ), true ); QCOMPARE( filteredData_->isLineInMatchingList( 124 ), false ); QCOMPARE( filteredData_->getMaxLength(), ML_VISIBLE_LINE_LENGTH ); QCOMPARE( filteredData_->getLineLength( 12 ), ML_VISIBLE_LINE_LENGTH ); QCOMPARE( filteredData_->getNbLine(), 135LL ); // Line beyond limit QCOMPARE( filteredData_->isLineInMatchingList( 60000 ), false ); QCOMPARE( filteredData_->getMatchingLineNumber( 0 ), 123LL ); // Now let's try interrupting a search filteredData_->runSearch( QRegExp( "123" ) ); // ... wait for two chunks. waitSearchProgressed(); signalSearchProgressedRead(); // and interrupt! filteredData_->interruptSearch(); { std::pair progress; do { progress = waitSearchProgressed(); signalSearchProgressedRead(); } while ( progress.second < 100 ); // (because there is no guarantee when the search is // interrupted, we are not sure how many chunk of result // we will get.) } QApplication::quit(); } void TestLogFilteredData::multipleSearch() { logData_ = new LogData(); // Register for notification file is loaded connect( logData_, SIGNAL( loadingFinished( bool ) ), this, SLOT( loadingFinished() ) ); filteredData_ = logData_->getNewFilteredData(); connect( filteredData_, SIGNAL( searchProgressed( int, int ) ), this, SLOT( searchProgressed( int, int ) ) ); QFuture future = QtConcurrent::run(this, &TestLogFilteredData::multipleSearchTest); QApplication::exec(); disconnect( filteredData_, 0 ); disconnect( logData_, 0 ); delete filteredData_; delete logData_; } void TestLogFilteredData::multipleSearchTest() { // First load the tests file logData_->attachFile( TMPDIR "/smalllog.txt" ); // Wait for the loading to be done waitLoadingFinished(); QCOMPARE( logData_->getNbLine(), SL_NB_LINES ); signalLoadingFinishedRead(); // Performs two searches in a row // Start the search, and immediately another one // (the second call should block until the first search is done) filteredData_->runSearch( QRegExp( "1234" ) ); filteredData_->runSearch( QRegExp( "123" ) ); for ( int i = 0; i < 3; i++ ) { waitSearchProgressed(); signalSearchProgressedRead(); } // We should have the result for the 2nd search after the last chunk waitSearchProgressed(); QCOMPARE( filteredData_->getNbLine(), 12LL ); signalSearchProgressedRead(); // Now a tricky one: we run a search and immediately attach a new file /* FIXME: sometimes we receive loadingFinished before searchProgressed * -> deadlock in the test. filteredData_->runSearch( QRegExp( "123" ) ); waitSearchProgressed(); signalSearchProgressedRead(); logData_->attachFile( TMPDIR "/mediumlog.txt" ); // We don't expect meaningful results but it should not crash! for ( int i = 0; i < 1; i++ ) { waitSearchProgressed(); signalSearchProgressedRead(); } */ sleep(10); QApplication::quit(); } void TestLogFilteredData::updateSearch() { logData_ = new LogData(); // Register for notification file is loaded connect( logData_, SIGNAL( loadingFinished( bool ) ), this, SLOT( loadingFinished() ) ); filteredData_ = logData_->getNewFilteredData(); connect( filteredData_, SIGNAL( searchProgressed( int, int ) ), this, SLOT( searchProgressed( int, int ) ) ); QFuture future = QtConcurrent::run(this, &TestLogFilteredData::updateSearchTest); QApplication::exec(); disconnect( filteredData_, 0 ); disconnect( logData_, 0 ); delete filteredData_; delete logData_; } void TestLogFilteredData::updateSearchTest() { // First load the tests file logData_->attachFile( TMPDIR "/smalllog.txt" ); // Wait for the loading to be done waitLoadingFinished(); QCOMPARE( logData_->getNbLine(), SL_NB_LINES ); signalLoadingFinishedRead(); // Perform a first search filteredData_->runSearch( QRegExp( "123" ) ); for ( int i = 0; i < 2; i++ ) { waitSearchProgressed(); signalSearchProgressedRead(); } // Check the result QCOMPARE( filteredData_->getNbLine(), 12LL ); sleep(1); QWARN("Starting stage 2"); // Add some data to the file char newLine[90]; QFile file( TMPDIR "/smalllog.txt" ); if ( file.open( QIODevice::Append ) ) { for (int i = 0; i < 3000; i++) { snprintf(newLine, 89, sl_format, i); file.write( newLine, qstrlen(newLine) ); } // To test the edge case when the final line is not complete and matching file.write( partial_line_begin, qstrlen( partial_line_begin ) ); } file.close(); // Let the system do the update (there might be several ones) do { waitLoadingFinished(); signalLoadingFinishedRead(); } while ( logData_->getNbLine() < 5001LL ); sleep(1); // Start an update search filteredData_->updateSearch(); for ( int i = 0; i < 2; i++ ) { waitSearchProgressed(); signalSearchProgressedRead(); } // Check the result QCOMPARE( logData_->getNbLine(), 5001LL ); QCOMPARE( filteredData_->getNbLine(), 26LL ); QWARN("Starting stage 3"); // Add a couple more lines, including the end of the unfinished one. if ( file.open( QIODevice::Append ) ) { file.write( partial_line_end, qstrlen( partial_line_end ) ); for (int i = 0; i < 20; i++) { snprintf(newLine, 89, sl_format, i); file.write( newLine, qstrlen(newLine) ); } // To test the edge case when the final line is not complete and not matching file.write( partial_nonmatching_line_begin, qstrlen( partial_nonmatching_line_begin ) ); } file.close(); // Let the system do the update waitLoadingFinished(); signalLoadingFinishedRead(); // Start an update search filteredData_->updateSearch(); for ( int i = 0; i < 2; i++ ) { waitSearchProgressed(); signalSearchProgressedRead(); } // Check the result QCOMPARE( logData_->getNbLine(), 5022LL ); QCOMPARE( filteredData_->getNbLine(), 26LL ); QWARN("Starting stage 4"); // Now test the case where a match is found at the end of an updated line. if ( file.open( QIODevice::Append ) ) { file.write( partial_line_end, qstrlen( partial_line_end ) ); for (int i = 0; i < 20; i++) { snprintf(newLine, 89, sl_format, i); file.write( newLine, qstrlen(newLine) ); } } file.close(); // Let the system do the update waitLoadingFinished(); signalLoadingFinishedRead(); // Start an update search filteredData_->updateSearch(); for ( int i = 0; i < 2; i++ ) { waitSearchProgressed(); signalSearchProgressedRead(); } // Check the result QCOMPARE( logData_->getNbLine(), 5042LL ); QCOMPARE( filteredData_->getNbLine(), 27LL ); QApplication::quit(); } void TestLogFilteredData::marks() { logData_ = new LogData(); // Register for notification file is loaded connect( logData_, SIGNAL( loadingFinished( bool ) ), this, SLOT( loadingFinished() ) ); filteredData_ = logData_->getNewFilteredData(); connect( filteredData_, SIGNAL( searchProgressed( int, int ) ), this, SLOT( searchProgressed( int, int ) ) ); QFuture future = QtConcurrent::run(this, &TestLogFilteredData::marksTest); QApplication::exec(); disconnect( filteredData_, 0 ); disconnect( logData_, 0 ); delete filteredData_; delete logData_; } void TestLogFilteredData::marksTest() { // First load the tests file logData_->attachFile( TMPDIR "/smalllog.txt" ); // Wait for the loading to be done waitLoadingFinished(); QCOMPARE( logData_->getNbLine(), SL_NB_LINES ); signalLoadingFinishedRead(); // First check no line is marked for ( int i = 0; i < SL_NB_LINES; i++ ) QVERIFY( filteredData_->isLineMarked( i ) == false ); // Try to create some "out of limit" marks filteredData_->addMark( -10 ); filteredData_->addMark( SL_NB_LINES + 25 ); // Check no line is marked still for ( int i = 0; i < SL_NB_LINES; i++ ) QVERIFY( filteredData_->isLineMarked( i ) == false ); // Create a couple of unnamed marks filteredData_->addMark( 10 ); filteredData_->addMark( 44 ); // This one will also be a match filteredData_->addMark( 25 ); // Check they are marked QVERIFY( filteredData_->isLineMarked( 10 ) ); QVERIFY( filteredData_->isLineMarked( 25 ) ); QVERIFY( filteredData_->isLineMarked( 44 ) ); // But others are not QVERIFY( filteredData_->isLineMarked( 15 ) == false ); QVERIFY( filteredData_->isLineMarked( 20 ) == false ); QCOMPARE( filteredData_->getNbLine(), 3LL ); // Performs a search QSignalSpy progressSpy( filteredData_, SIGNAL( searchProgressed( int, int ) ) ); filteredData_->runSearch( QRegExp( "0000.4" ) ); for ( int i = 0; i < 1; i++ ) { waitSearchProgressed(); signalSearchProgressedRead(); } // We should have the result of the search and the marks waitSearchProgressed(); QCOMPARE( filteredData_->getNbLine(), 12LL ); signalSearchProgressedRead(); QString startline = "LOGDATA is a part of glogg, we are going to test it thoroughly, this is line "; QCOMPARE( filteredData_->getLineString(0), startline + "000004" ); QCOMPARE( filteredData_->getLineString(1), startline + "000010" ); QCOMPARE( filteredData_->getLineString(2), startline + "000014" ); QCOMPARE( filteredData_->getLineString(3), startline + "000024" ); QCOMPARE( filteredData_->getLineString(4), startline + "000025" ); QCOMPARE( filteredData_->getLineString(5), startline + "000034" ); QCOMPARE( filteredData_->getLineString(6), startline + "000044" ); QCOMPARE( filteredData_->getLineString(7), startline + "000054" ); QCOMPARE( filteredData_->getLineString(8), startline + "000064" ); QCOMPARE( filteredData_->getLineString(9), startline + "000074" ); QCOMPARE( filteredData_->getLineString(10), startline + "000084" ); QCOMPARE( filteredData_->getLineString(11), startline + "000094" ); filteredData_->setVisibility( LogFilteredData::MatchesOnly ); QCOMPARE( filteredData_->getNbLine(), 10LL ); QCOMPARE( filteredData_->getLineString(0), startline + "000004" ); QCOMPARE( filteredData_->getLineString(1), startline + "000014" ); QCOMPARE( filteredData_->getLineString(2), startline + "000024" ); QCOMPARE( filteredData_->getLineString(3), startline + "000034" ); QCOMPARE( filteredData_->getLineString(4), startline + "000044" ); QCOMPARE( filteredData_->getLineString(5), startline + "000054" ); QCOMPARE( filteredData_->getLineString(6), startline + "000064" ); QCOMPARE( filteredData_->getLineString(7), startline + "000074" ); QCOMPARE( filteredData_->getLineString(8), startline + "000084" ); QCOMPARE( filteredData_->getLineString(9), startline + "000094" ); filteredData_->setVisibility( LogFilteredData::MarksOnly ); QCOMPARE( filteredData_->getNbLine(), 3LL ); QCOMPARE( filteredData_->getLineString(0), startline + "000010" ); QCOMPARE( filteredData_->getLineString(1), startline + "000025" ); QCOMPARE( filteredData_->getLineString(2), startline + "000044" ); // Another test with marks only filteredData_->clearSearch(); filteredData_->clearMarks(); filteredData_->setVisibility( LogFilteredData::MarksOnly ); filteredData_->addMark(18); filteredData_->addMark(19); filteredData_->addMark(20); QCOMPARE( filteredData_->getMatchingLineNumber(0), 18LL ); QCOMPARE( filteredData_->getMatchingLineNumber(1), 19LL ); QCOMPARE( filteredData_->getMatchingLineNumber(2), 20LL ); QApplication::quit(); } void TestLogFilteredData::lineLength() { logData_ = new LogData(); // Register for notification file is loaded connect( logData_, SIGNAL( loadingFinished( bool ) ), this, SLOT( loadingFinished() ) ); filteredData_ = logData_->getNewFilteredData(); connect( filteredData_, SIGNAL( searchProgressed( int, int ) ), this, SLOT( searchProgressed( int, int ) ) ); QFuture future = QtConcurrent::run(this, &TestLogFilteredData::lineLengthTest); QApplication::exec(); disconnect( filteredData_, 0 ); disconnect( logData_, 0 ); delete filteredData_; delete logData_; } void TestLogFilteredData::lineLengthTest() { // Line length tests logData_->attachFile( TMPDIR "/length_test.txt" ); // Wait for the loading to be done waitLoadingFinished(); QCOMPARE( logData_->getNbLine(), 4LL ); signalLoadingFinishedRead(); // Performs a search (the two middle lines matche) filteredData_->setVisibility( LogFilteredData::MatchesOnly ); filteredData_->runSearch( QRegExp( "longer" ) ); std::pair progress; do { progress = waitSearchProgressed(); signalSearchProgressedRead(); QWARN("progress"); } while ( progress.second < 100 ); filteredData_->addMark( 3 ); QCOMPARE( filteredData_->getNbLine(), 2LL ); QCOMPARE( filteredData_->getMaxLength(), 40 ); filteredData_->setVisibility( LogFilteredData::MarksAndMatches ); QCOMPARE( filteredData_->getNbLine(), 3LL ); QCOMPARE( filteredData_->getMaxLength(), 103 ); filteredData_->setVisibility( LogFilteredData::MarksOnly ); QCOMPARE( filteredData_->getNbLine(), 1LL ); QCOMPARE( filteredData_->getMaxLength(), 103 ); filteredData_->addMark( 0 ); QCOMPARE( filteredData_->getNbLine(), 2LL ); QCOMPARE( filteredData_->getMaxLength(), 103 ); filteredData_->deleteMark( 3 ); QCOMPARE( filteredData_->getNbLine(), 1LL ); QCOMPARE( filteredData_->getMaxLength(), 27 ); filteredData_->setVisibility( LogFilteredData::MarksAndMatches ); QCOMPARE( filteredData_->getMaxLength(), 40 ); QApplication::quit(); } // // Private functions // void TestLogFilteredData::loadingFinished() { QMutexLocker locker( &loadingFinishedMutex_ ); QWARN("loadingFinished"); loadingFinished_received_ = true; loadingFinished_read_ = false; loadingFinishedCondition_.wakeOne(); // Wait for the test thread to read the signal while ( ! loadingFinished_read_ ) loadingFinishedCondition_.wait( locker.mutex() ); } void TestLogFilteredData::searchProgressed( int nbMatches, int completion ) { QMutexLocker locker( &searchProgressedMutex_ ); QWARN("searchProgressed"); searchProgressed_received_ = true; searchProgressed_read_ = false; searchLastMatches_ = nbMatches; searchLastProgress_ = completion; searchProgressedCondition_.wakeOne(); // Wait for the test thread to read the signal while ( ! searchProgressed_read_ ) searchProgressedCondition_.wait( locker.mutex() ); } std::pair TestLogFilteredData::waitSearchProgressed() { QMutexLocker locker( &searchProgressedMutex_ ); while ( ! searchProgressed_received_ ) searchProgressedCondition_.wait( locker.mutex() ); QWARN("searchProgressed Received"); return std::pair(searchLastMatches_, searchLastProgress_); } void TestLogFilteredData::waitLoadingFinished() { QMutexLocker locker( &loadingFinishedMutex_ ); while ( ! loadingFinished_received_ ) loadingFinishedCondition_.wait( locker.mutex() ); QWARN("loadingFinished Received"); } void TestLogFilteredData::signalSearchProgressedRead() { QMutexLocker locker( &searchProgressedMutex_ ); searchProgressed_received_ = false; searchProgressed_read_ = true; searchProgressedCondition_.wakeOne(); } void TestLogFilteredData::signalLoadingFinishedRead() { QMutexLocker locker( &loadingFinishedMutex_ ); loadingFinished_received_ = false; loadingFinished_read_ = true; loadingFinishedCondition_.wakeOne(); } bool TestLogFilteredData::generateDataFiles() { char newLine[90]; QFile file( TMPDIR "/mediumlog.txt" ); if ( file.open( QIODevice::WriteOnly ) ) { for (int i = 0; i < ML_NB_LINES; i++) { snprintf(newLine, 89, ml_format, i); file.write( newLine, qstrlen(newLine) ); } } else { return false; } file.close(); file.setFileName( TMPDIR "/smalllog.txt" ); if ( file.open( QIODevice::WriteOnly ) ) { for (int i = 0; i < SL_NB_LINES; i++) { snprintf(newLine, 89, sl_format, i); file.write( newLine, qstrlen(newLine) ); } } else { return false; } file.close(); file.setFileName( TMPDIR "/length_test.txt" ); if ( file.open( QIODevice::WriteOnly ) ) { file.write( "This line is 27 characters.\n" ); file.write( "This line is longer: 36 characters.\n" ); file.write( "This line is even longer: 40 characters.\n" ); file.write( "This line is very long, it's actually hard to count but it is\ probably something around 103 characters.\n" ); } file.close(); return true; } glogg-1.1.4/tests/cmake.bat0000644000175000017500000000042613076461374014533 0ustar nickonickorem Generate makefiles suitable to make the tests on Windows/MinGW mkdir build cd build set QT_DIR=Y:\5.3.2-64 set CMAKE_PREFIX_PATH=%QT_DIR% set GMOCK_HOME=Y:\gmock-1.7.0 set QT_QPA_PLATFORM_PLUGIN_PATH=%QT_DIR%\plugins\platforms cmake -G "MinGW Makefiles" .. mingw32-make glogg-1.1.4/tests/encodingspeculatorTest.cpp0000644000175000017500000001362013076461374020217 0ustar nickonicko#include "gmock/gmock.h" #include "config.h" #include "log.h" #include "encodingspeculator.h" using namespace std; using namespace testing; class EncodingSpeculatorBehaviour: public testing::Test { public: EncodingSpeculator speculator; EncodingSpeculatorBehaviour() { } }; TEST_F( EncodingSpeculatorBehaviour, DefaultAsPureAscii ) { ASSERT_THAT( speculator.guess(), Eq( EncodingSpeculator::Encoding::ASCII7 ) ); } TEST_F( EncodingSpeculatorBehaviour, RecognisePureAscii ) { for ( uint8_t i = 0; i < 127; ++i ) speculator.inject_byte( i ); ASSERT_THAT( speculator.guess(), Eq( EncodingSpeculator::Encoding::ASCII7 ) ); } TEST_F( EncodingSpeculatorBehaviour, RecogniseRandom8bitEncoding ) { for ( uint8_t i = 0; i < 127; ++i ) speculator.inject_byte( i ); speculator.inject_byte( 0xFF ); ASSERT_THAT( speculator.guess(), Eq( EncodingSpeculator::Encoding::ASCII8 ) ); } pair utf8encode2bytes( uint16_t code_point ) { uint8_t cp_low = static_cast(code_point & 0xFF); uint8_t cp_hi = static_cast((code_point & 0xFF00) >> 8); uint8_t first_byte = 0xC0 | ( ( cp_hi & 0x7F ) << 2 ) | ( ( cp_low & 0xC0 ) >> 6 ); uint8_t second_byte = 0x80 | ( cp_low & 0x3F ); return { first_byte, second_byte }; } vector utf8encodeMultiBytes( uint32_t code_point ) { vector bytes = {}; if ( code_point <= 0xFFFF ) { uint8_t lead = static_cast( 0xE0 | ( ( code_point & 0xF000 ) >> 12 ) ); bytes.push_back( lead ); bytes.push_back( 0x80 | ( code_point & 0x0FC0 ) >> 6 ); bytes.push_back( 0x80 | ( code_point & 0x3F ) ); } else if ( code_point <= 0x1FFFFF ) { uint8_t lead = static_cast( 0xF0 | ( ( code_point & 0x1C0000 ) >> 18 ) ); bytes.push_back( lead ); bytes.push_back( 0x80 | ( code_point & 0x3F000 ) >> 12 ); bytes.push_back( 0x80 | ( code_point & 0x00FC0 ) >> 6 ); bytes.push_back( 0x80 | ( code_point & 0x0003F ) ); } return bytes; } TEST_F( EncodingSpeculatorBehaviour, RecogniseTwoBytesUTF8 ) { // All the code points encodable as 2 bytes. for ( uint16_t i = 0x80; i < ( 1 << 11 ); ++i ) { auto utf8_bytes = utf8encode2bytes( i ); // cout << bitset<8>(first_byte) << " " << bitset<8>(second_byte) << endl; speculator.inject_byte( utf8_bytes.first ); speculator.inject_byte( utf8_bytes.second ); } ASSERT_THAT( speculator.guess(), Eq( EncodingSpeculator::Encoding::UTF8 ) ); } TEST_F( EncodingSpeculatorBehaviour, RecogniseTwoBytesUTF8With7bitsInterleaved ) { // All the code points encodable as 2 bytes. for ( uint16_t i = 0x80; i < ( 1 << 11 ); ++i ) { auto utf8_bytes = utf8encode2bytes( i ); speculator.inject_byte( ' ' ); speculator.inject_byte( utf8_bytes.first ); speculator.inject_byte( utf8_bytes.second ); } ASSERT_THAT( speculator.guess(), Eq( EncodingSpeculator::Encoding::UTF8 ) ); } TEST_F( EncodingSpeculatorBehaviour, RecogniseIncompleteTwoBytesUTF8 ) { // All the code points encodable as 2 bytes. for ( uint16_t i = 0x80; i < ( 1 << 11 ); ++i ) { auto utf8_bytes = utf8encode2bytes( i ); speculator.inject_byte( ' ' ); speculator.inject_byte( utf8_bytes.first ); speculator.inject_byte( utf8_bytes.second ); } // Lead byte only speculator.inject_byte( 0xCF ); ASSERT_THAT( speculator.guess(), Eq( EncodingSpeculator::Encoding::ASCII8 ) ); } TEST_F( EncodingSpeculatorBehaviour, RecogniseIncorrectTwoBytesUTF8 ) { // All the code points encodable as 2 bytes. for ( uint16_t i = 0x80; i < ( 1 << 11 ); ++i ) { auto utf8_bytes = utf8encode2bytes( i ); speculator.inject_byte( ' ' ); speculator.inject_byte( utf8_bytes.first ); speculator.inject_byte( utf8_bytes.second ); } // Lead byte speculator.inject_byte( 0xCF ); // Incorrect continuation byte (should start with 1) speculator.inject_byte( 0x00 ); ASSERT_THAT( speculator.guess(), Eq( EncodingSpeculator::Encoding::ASCII8 ) ); } TEST_F( EncodingSpeculatorBehaviour, RecogniseOverlong2BytesUTF8 ) { speculator.inject_byte( ' ' ); speculator.inject_byte( 0xC1 ); speculator.inject_byte( 0xBF ); ASSERT_THAT( speculator.guess(), Eq( EncodingSpeculator::Encoding::ASCII8 ) ); } TEST_F( EncodingSpeculatorBehaviour, RecogniseThreeBytesUTF8 ) { for ( uint32_t i = 0x800; i <= 0xFFFF; ++i ) { auto utf8_bytes = utf8encodeMultiBytes( i ); speculator.inject_byte( ' ' ); for ( uint8_t byte: utf8_bytes ) { // cout << hex << i << " " << static_cast( byte ) << endl; speculator.inject_byte( byte ); } } ASSERT_THAT( speculator.guess(), Eq( EncodingSpeculator::Encoding::UTF8 ) ); } TEST_F( EncodingSpeculatorBehaviour, RecogniseOverlong3BytesUTF8 ) { speculator.inject_byte( ' ' ); speculator.inject_byte( 0xA0 ); speculator.inject_byte( 0x80 ); speculator.inject_byte( 0x80 ); ASSERT_THAT( speculator.guess(), Eq( EncodingSpeculator::Encoding::ASCII8 ) ); } TEST_F( EncodingSpeculatorBehaviour, RecogniseFourBytesUTF8 ) { for ( uint32_t i = 0x10000; i <= 0x1FFFFF; ++i ) { auto utf8_bytes = utf8encodeMultiBytes( i ); speculator.inject_byte( ' ' ); for ( uint8_t byte: utf8_bytes ) { // cout << hex << i << " " << static_cast( byte ) << endl; speculator.inject_byte( byte ); } } ASSERT_THAT( speculator.guess(), Eq( EncodingSpeculator::Encoding::UTF8 ) ); } TEST_F( EncodingSpeculatorBehaviour, RecogniseOverlong4BytesUTF8 ) { speculator.inject_byte( ' ' ); speculator.inject_byte( 0xF0 ); speculator.inject_byte( 0x80 ); speculator.inject_byte( 0x80 ); speculator.inject_byte( 0x80 ); ASSERT_THAT( speculator.guess(), Eq( EncodingSpeculator::Encoding::ASCII8 ) ); } glogg-1.1.4/tests/testlogfiltereddata.h0000644000175000017500000000263113076461374017166 0ustar nickonicko#include #include #include class LogData; class LogFilteredData; class TestLogFilteredData: public QObject { Q_OBJECT public: TestLogFilteredData(); private slots: void initTestCase(); void simpleSearch(); void multipleSearch(); void marks(); void lineLength(); void updateSearch(); public slots: void loadingFinished(); void searchProgressed( int completion, int nbMatches ); private: bool generateDataFiles(); void simpleSearchTest(); void multipleSearchTest(); void updateSearchTest(); void marksTest(); void lineLengthTest(); std::pair waitSearchProgressed(); void waitLoadingFinished(); void signalSearchProgressedRead(); void signalLoadingFinishedRead(); LogData* logData_; LogFilteredData* filteredData_; // Synchronisation variables (protected by the two mutexes) bool loadingFinished_received_; bool loadingFinished_read_; bool searchProgressed_received_; bool searchProgressed_read_; int searchLastMatches_; int searchLastProgress_; QMutex loadingFinishedMutex_; QMutex searchProgressedMutex_; QWaitCondition loadingFinishedCondition_; QWaitCondition searchProgressedCondition_; }; glogg-1.1.4/tests/logfiltereddataTest.cpp0000644000175000017500000000351213076461374017460 0ustar nickonicko#include #include #include "log.h" #include "test_utils.h" #include "data/logdata.h" #include "data/logfiltereddata.h" #include "gmock/gmock.h" #define TMPDIR "/tmp" static const qint64 SL_NB_LINES = 5000LL; static const int SL_LINE_PER_PAGE = 70; static const char* sl_format="LOGDATA is a part of glogg, we are going to test it thoroughly, this is line %06d\n"; static const int SL_LINE_LENGTH = 83; // Without the final '\n' ! class MarksBehaviour : public testing::Test { public: LogData log_data; SafeQSignalSpy endSpy; LogFilteredData* filtered_data = nullptr; MarksBehaviour() : endSpy( &log_data, SIGNAL( loadingFinished( LoadingStatus ) ) ) { generateDataFiles(); log_data.attachFile( TMPDIR "/smalllog.txt" ); endSpy.safeWait( 10000 ); filtered_data = log_data.getNewFilteredData(); } bool generateDataFiles() { char newLine[90]; QFile file( TMPDIR "/smalllog.txt" ); if ( file.open( QIODevice::WriteOnly ) ) { for (int i = 0; i < SL_NB_LINES; i++) { snprintf(newLine, 89, sl_format, i); file.write( newLine, qstrlen(newLine) ); } } else { return false; } file.close(); return true; } }; TEST_F( MarksBehaviour, marksIgnoresOutOfLimitLines ) { filtered_data->addMark( -10 ); filtered_data->addMark( SL_NB_LINES + 25 ); // Check we still have no mark for ( int i = 0; i < SL_NB_LINES; i++ ) ASSERT_FALSE( filtered_data->isLineMarked( i ) ); } TEST_F( MarksBehaviour, marksAreStored ) { filtered_data->addMark( 10 ); filtered_data->addMark( 25 ); // Check the marks have been put ASSERT_TRUE( filtered_data->isLineMarked( 10 ) ); ASSERT_TRUE( filtered_data->isLineMarked( 25 ) ); } glogg-1.1.4/tests/logfiltereddataPerfTest.cpp0000644000175000017500000000714613076461374020304 0ustar nickonicko#include #include #include "log.h" #include "test_utils.h" #include "data/logdata.h" #include "data/logfiltereddata.h" #include "gmock/gmock.h" #define TMPDIR "/tmp" static const qint64 VBL_NB_LINES = 4999999LL; static const int VBL_LINE_PER_PAGE = 70; static const char* vbl_format="LOGDATA is a part of glogg, we are going to test it thoroughly, this is line\t\t%07d\n"; static const int VBL_LINE_LENGTH = (76+2+7) ; // Without the final '\n' ! static const int VBL_VISIBLE_LINE_LENGTH = (76+8+4+7); // Without the final '\n' ! class PerfLogFilteredData : public testing::Test { public: PerfLogFilteredData() : log_data_(), filtered_data_( log_data_.getNewFilteredData() ), progressSpy( filtered_data_.get(), SIGNAL( searchProgressed( int, int ) ) ) { FILELog::setReportingLevel( logERROR ); generateDataFiles(); QSignalSpy endSpy( &log_data_, SIGNAL( loadingFinished( LoadingStatus ) ) ); log_data_.attachFile( TMPDIR "/verybiglog.txt" ); if ( ! endSpy.wait( 999000 ) ) std::cerr << "Unable to attach the file!"; } LogData log_data_; std::unique_ptr filtered_data_; QSignalSpy progressSpy; void search() { int percent = 0; do { if ( progressSpy.wait( 10000 ) ) percent = qvariant_cast(progressSpy.last().at(1)); else std::cout << "Missed...\n"; // std::cout << "Progress " << percent << std::endl; } while ( percent < 100 ); } bool generateDataFiles() { char newLine[90]; QFile file( TMPDIR "/verybiglog.txt" ); if ( file.open( QIODevice::WriteOnly ) ) { for (int i = 0; i < VBL_NB_LINES; i++) { snprintf(newLine, 89, vbl_format, i); file.write( newLine, qstrlen(newLine) ); } } else { return false; } file.close(); return true; } }; TEST_F( PerfLogFilteredData, allMatchingSearch ) { { TestTimer t; filtered_data_->runSearch( QRegExp( "glogg.*this" ) ); search(); } ASSERT_THAT( filtered_data_->getNbLine(), VBL_NB_LINES ); } TEST_F( PerfLogFilteredData, someMatchingSearch ) { { TestTimer t; filtered_data_->runSearch( QRegExp( "1?3|34" ) ); search(); } ASSERT_THAT( filtered_data_->getNbLine(), 2874236 ); } TEST_F( PerfLogFilteredData, noneMatchingSearch ) { { TestTimer t; filtered_data_->runSearch( QRegExp( "a1?3|(w|f)f34|blah" ) ); search(); } ASSERT_THAT( filtered_data_->getNbLine(), 0 ); } TEST_F( PerfLogFilteredData, browsingSearchResults ) { filtered_data_->runSearch( QRegExp( "1?3|34" ) ); search(); ASSERT_THAT( filtered_data_->getNbLine(), 2874236 ); // Read page by page from the beginning and the end, using the QStringList // function std::cout << "Start reading..." << std::endl; QStringList list; { TestTimer t; const int nb_results = filtered_data_->getNbLine(); for (int page = 0; page < (nb_results/VBL_LINE_PER_PAGE)-1; page++) { list = filtered_data_->getExpandedLines( page * VBL_LINE_PER_PAGE, VBL_LINE_PER_PAGE ); ASSERT_THAT(list.count(), VBL_LINE_PER_PAGE); int page_from_end = (nb_results/VBL_LINE_PER_PAGE) - page - 1; list = filtered_data_->getExpandedLines( page_from_end * VBL_LINE_PER_PAGE, VBL_LINE_PER_PAGE ); ASSERT_THAT(list.count(), VBL_LINE_PER_PAGE); } } } glogg-1.1.4/tests/logdataTest.cpp0000644000175000017500000001320613076461374015742 0ustar nickonicko#include #include #include #include "log.h" #include "test_utils.h" #include "data/logdata.h" #include "gmock/gmock.h" #define TMPDIR "/tmp" static const qint64 SL_NB_LINES = 5000LL; static const int SL_LINE_PER_PAGE = 70; static const char* sl_format="LOGDATA is a part of glogg, we are going to test it thoroughly, this is line %06d\n"; static const int SL_LINE_LENGTH = 83; // Without the final '\n' ! static const char* partial_line_begin = "123... beginning of line."; static const char* partial_line_end = " end of line 123.\n"; class LogDataChanging : public testing::Test { public: }; TEST_F( LogDataChanging, changingFile ) { char newLine[90]; LogData log_data; SafeQSignalSpy finishedSpy( &log_data, SIGNAL( loadingFinished( LoadingStatus ) ) ); SafeQSignalSpy progressSpy( &log_data, SIGNAL( loadingProgressed( int ) ) ); SafeQSignalSpy changedSpy( &log_data, SIGNAL( fileChanged( LogData::MonitoredFileStatus ) ) ); // Generate a small file QFile file( TMPDIR "/changingfile.txt" ); if ( file.open( QIODevice::WriteOnly ) ) { for (int i = 0; i < 200; i++) { snprintf(newLine, 89, sl_format, i); file.write( newLine, qstrlen(newLine) ); } } file.close(); // Start loading it log_data.attachFile( TMPDIR "/changingfile.txt" ); // and wait for the signal ASSERT_TRUE( finishedSpy.safeWait() ); // Check we have the small file ASSERT_THAT( finishedSpy.count(), 1 ); ASSERT_THAT( log_data.getNbLine(), 200LL ); ASSERT_THAT( log_data.getMaxLength(), SL_LINE_LENGTH ); ASSERT_THAT( log_data.getFileSize(), 200 * (SL_LINE_LENGTH+1LL) ); // Add some data to it if ( file.open( QIODevice::Append ) ) { for (int i = 0; i < 200; i++) { snprintf(newLine, 89, sl_format, i); file.write( newLine, qstrlen(newLine) ); } // To test the edge case when the final line is not complete file.write( partial_line_begin, qstrlen( partial_line_begin ) ); } file.close(); // and wait for the signals ASSERT_TRUE( finishedSpy.wait( 1000 ) ); // Check we have a bigger file ASSERT_THAT( changedSpy.count(), 1 ); ASSERT_THAT( finishedSpy.count(), 2 ); ASSERT_THAT( log_data.getNbLine(), 401LL ); ASSERT_THAT( log_data.getMaxLength(), SL_LINE_LENGTH ); ASSERT_THAT( log_data.getFileSize(), (qint64) (400 * (SL_LINE_LENGTH+1LL) + strlen( partial_line_begin ) ) ); { SafeQSignalSpy finishedSpy( &log_data, SIGNAL( loadingFinished( LoadingStatus ) ) ); // Add a couple more lines, including the end of the unfinished one. if ( file.open( QIODevice::Append ) ) { file.write( partial_line_end, qstrlen( partial_line_end ) ); for (int i = 0; i < 20; i++) { snprintf(newLine, 89, sl_format, i); file.write( newLine, qstrlen(newLine) ); } } file.close(); // and wait for the signals ASSERT_TRUE( finishedSpy.wait( 1000 ) ); // Check we have a bigger file ASSERT_THAT( changedSpy.count(), 2 ); ASSERT_THAT( finishedSpy.count(), 1 ); ASSERT_THAT( log_data.getNbLine(), 421LL ); ASSERT_THAT( log_data.getMaxLength(), SL_LINE_LENGTH ); ASSERT_THAT( log_data.getFileSize(), (qint64) ( 420 * (SL_LINE_LENGTH+1LL) + strlen( partial_line_begin ) + strlen( partial_line_end ) ) ); } { SafeQSignalSpy finishedSpy( &log_data, SIGNAL( loadingFinished( LoadingStatus ) ) ); // Truncate the file QVERIFY( file.open( QIODevice::WriteOnly ) ); file.close(); // and wait for the signals ASSERT_TRUE( finishedSpy.safeWait() ); // Check we have an empty file ASSERT_THAT( changedSpy.count(), 3 ); ASSERT_THAT( finishedSpy.count(), 1 ); ASSERT_THAT( log_data.getNbLine(), 0LL ); ASSERT_THAT( log_data.getMaxLength(), 0 ); ASSERT_THAT( log_data.getFileSize(), 0LL ); } } class LogDataBehaviour : public testing::Test { public: LogDataBehaviour() { generateDataFiles(); } bool generateDataFiles() { char newLine[90]; QFile file( TMPDIR "/smalllog.txt" ); if ( file.open( QIODevice::WriteOnly ) ) { for (int i = 0; i < SL_NB_LINES; i++) { snprintf(newLine, 89, sl_format, i); file.write( newLine, qstrlen(newLine) ); } } else { return false; } file.close(); return true; } }; TEST_F( LogDataBehaviour, interruptLoadYieldsAnEmptyFile ) { LogData log_data; SafeQSignalSpy endSpy( &log_data, SIGNAL( loadingFinished( LoadingStatus ) ) ); // Start loading the VBL log_data.attachFile( TMPDIR "/verybiglog.txt" ); // Immediately interrupt the loading log_data.interruptLoading(); ASSERT_TRUE( endSpy.safeWait( 10000 ) ); // Check we have an empty file ASSERT_THAT( endSpy.count(), 1 ); QList arguments = endSpy.takeFirst(); ASSERT_THAT( arguments.at(0).toInt(), static_cast( LoadingStatus::Interrupted ) ); ASSERT_THAT( log_data.getNbLine(), 0LL ); ASSERT_THAT( log_data.getMaxLength(), 0 ); ASSERT_THAT( log_data.getFileSize(), 0LL ); } TEST_F( LogDataBehaviour, cannotBeReAttached ) { LogData log_data; SafeQSignalSpy endSpy( &log_data, SIGNAL( loadingFinished( LoadingStatus ) ) ); log_data.attachFile( TMPDIR "/smalllog.txt" ); endSpy.safeWait( 10000 ); ASSERT_THROW( log_data.attachFile( TMPDIR "/verybiglog.txt" ), CantReattachErr ); } glogg-1.1.4/tests/logdataPerfTest.cpp0000644000175000017500000001062513076461374016561 0ustar nickonicko#include #include #include "log.h" #include "test_utils.h" #include "data/logdata.h" #include "gmock/gmock.h" #define TMPDIR "/tmp" static const qint64 VBL_NB_LINES = 4999999LL; static const int VBL_LINE_PER_PAGE = 70; static const char* vbl_format="LOGDATA is a part of glogg, we are going to test it thoroughly, this is line\t\t%07d\n"; static const int VBL_LINE_LENGTH = (76+2+7) ; // Without the final '\n' ! static const int VBL_VISIBLE_LINE_LENGTH = (76+8+4+7); // Without the final '\n' ! class PerfLogData : public testing::Test { public: PerfLogData() { FILELog::setReportingLevel( logERROR ); generateDataFiles(); } bool generateDataFiles() { char newLine[90]; QFile file( TMPDIR "/verybiglog.txt" ); if ( file.open( QIODevice::WriteOnly ) ) { for (int i = 0; i < VBL_NB_LINES; i++) { snprintf(newLine, 89, vbl_format, i); file.write( newLine, qstrlen(newLine) ); } } else { return false; } file.close(); return true; } }; TEST_F( PerfLogData, simpleLoad ) { LogData log_data; QSignalSpy progressSpy( &log_data, SIGNAL( loadingProgressed( int ) ) ); QSignalSpy endSpy( &log_data, SIGNAL( loadingFinished( LoadingStatus ) ) ); { TestTimer t; log_data.attachFile( TMPDIR "/verybiglog.txt" ); ASSERT_TRUE( endSpy.wait( 20000 ) ); } ASSERT_THAT( log_data.getNbLine(), VBL_NB_LINES ); ASSERT_THAT( log_data.getMaxLength(), VBL_VISIBLE_LINE_LENGTH ); ASSERT_THAT( log_data.getLineLength( 123 ), VBL_VISIBLE_LINE_LENGTH ); ASSERT_THAT( log_data.getFileSize(), VBL_NB_LINES * (VBL_LINE_LENGTH+1LL) ); // Blocks of 5 MiB + 1 for the start notification (0%) ASSERT_THAT( progressSpy.count(), log_data.getFileSize() / (5LL*1024*1024) + 2 ); ASSERT_THAT( endSpy.count(), 1 ); QList arguments = endSpy.takeFirst(); ASSERT_THAT( arguments.at(0).toInt(), static_cast( LoadingStatus::Successful ) ); } class PerfLogDataRead : public PerfLogData { public: PerfLogDataRead() : PerfLogData(), log_data(), endSpy( &log_data, SIGNAL( loadingFinished( LoadingStatus ) ) ) { log_data.attachFile( TMPDIR "/verybiglog.txt" ); endSpy.wait( 20000 ); } LogData log_data; SafeQSignalSpy endSpy; }; TEST_F( PerfLogDataRead, sequentialRead ) { ASSERT_THAT( log_data.getNbLine(), VBL_NB_LINES ); // Read all lines sequentially QString s; { TestTimer t; for (int i = 0; i < VBL_NB_LINES; i++) { s = log_data.getLineString(i); } } } TEST_F( PerfLogDataRead, sequentialReadExpanded ) { ASSERT_THAT( log_data.getNbLine(), VBL_NB_LINES ); // Read all lines sequentially (expanded) QString s; { TestTimer t; for (int i = 0; i < VBL_NB_LINES; i++) { s = log_data.getExpandedLineString(i); } } } TEST_F( PerfLogDataRead, randomPageRead ) { ASSERT_THAT( log_data.getNbLine(), VBL_NB_LINES ); // Read page by page from the beginning and the end, using the QStringList // function QStringList list; { TestTimer t; for (int page = 0; page < (VBL_NB_LINES/VBL_LINE_PER_PAGE)-1; page++) { list = log_data.getLines( page*VBL_LINE_PER_PAGE, VBL_LINE_PER_PAGE ); ASSERT_THAT(list.count(), VBL_LINE_PER_PAGE); int page_from_end = (VBL_NB_LINES/VBL_LINE_PER_PAGE) - page - 1; list = log_data.getLines( page_from_end*VBL_LINE_PER_PAGE, VBL_LINE_PER_PAGE ); ASSERT_THAT(list.count(), VBL_LINE_PER_PAGE); } } } TEST_F( PerfLogDataRead, randomPageReadExpanded ) { ASSERT_THAT( log_data.getNbLine(), VBL_NB_LINES ); // Read page by page from the beginning and the end, using the QStringList // function QStringList list; { TestTimer t; for (int page = 0; page < (VBL_NB_LINES/VBL_LINE_PER_PAGE)-1; page++) { list = log_data.getExpandedLines( page*VBL_LINE_PER_PAGE, VBL_LINE_PER_PAGE ); ASSERT_THAT(list.count(), VBL_LINE_PER_PAGE); int page_from_end = (VBL_NB_LINES/VBL_LINE_PER_PAGE) - page - 1; list = log_data.getExpandedLines( page_from_end*VBL_LINE_PER_PAGE, VBL_LINE_PER_PAGE ); ASSERT_THAT(list.count(), VBL_LINE_PER_PAGE); } } } glogg-1.1.4/.tarball-version0000644000175000017500000000000613076461374014720 0ustar nickonicko1.1.4 glogg-1.1.4/FileAssociation.nsh0000644000175000017500000001105013076461374015402 0ustar nickonicko/* _____________________________________________________________________________ File Association _____________________________________________________________________________ Based on code taken from http://nsis.sourceforge.net/File_Association Usage in script: 1. !include "FileAssociation.nsh" 2. [Section|Function] ${FileAssociationFunction} "Param1" "Param2" "..." $var [SectionEnd|FunctionEnd] FileAssociationFunction=[RegisterExtension|UnRegisterExtension] _____________________________________________________________________________ ${RegisterExtension} "[executable]" "[extension]" "[description]" "[executable]" ; executable which opens the file format ; "[extension]" ; extension, which represents the file format to open ; "[description]" ; description for the extension. This will be display in Windows Explorer. ; ${UnRegisterExtension} "[extension]" "[description]" "[extension]" ; extension, which represents the file format to open ; "[description]" ; description for the extension. This will be display in Windows Explorer. ; _____________________________________________________________________________ Macros _____________________________________________________________________________ Change log window verbosity (default: 3=no script) Example: !include "FileAssociation.nsh" !insertmacro RegisterExtension ${FileAssociation_VERBOSE} 4 # all verbosity !insertmacro UnRegisterExtension ${FileAssociation_VERBOSE} 3 # no script */ !ifndef FileAssociation_INCLUDED !define FileAssociation_INCLUDED !include Util.nsh !verbose push !verbose 3 !ifndef _FileAssociation_VERBOSE !define _FileAssociation_VERBOSE 3 !endif !verbose ${_FileAssociation_VERBOSE} !define FileAssociation_VERBOSE `!insertmacro FileAssociation_VERBOSE` !verbose pop !macro FileAssociation_VERBOSE _VERBOSE !verbose push !verbose 3 !undef _FileAssociation_VERBOSE !define _FileAssociation_VERBOSE ${_VERBOSE} !verbose pop !macroend !macro RegisterExtensionCall _EXECUTABLE _EXTENSION _DESCRIPTION !verbose push !verbose ${_FileAssociation_VERBOSE} Push `${_DESCRIPTION}` Push `${_EXTENSION}` Push `${_EXECUTABLE}` ${CallArtificialFunction} RegisterExtension_ !verbose pop !macroend !macro UnRegisterExtensionCall _EXTENSION _DESCRIPTION !verbose push !verbose ${_FileAssociation_VERBOSE} Push `${_EXTENSION}` Push `${_DESCRIPTION}` ${CallArtificialFunction} UnRegisterExtension_ !verbose pop !macroend !define RegisterExtension `!insertmacro RegisterExtensionCall` !define un.RegisterExtension `!insertmacro RegisterExtensionCall` !macro RegisterExtension !macroend !macro un.RegisterExtension !macroend !macro RegisterExtension_ !verbose push !verbose ${_FileAssociation_VERBOSE} Exch $R2 ;exe Exch Exch $R1 ;ext Exch Exch 2 Exch $R0 ;desc Exch 2 Push $0 Push $1 ReadRegStr $1 HKCR $R1 "" ; read current file association StrCmp "$1" "" NoBackup ; is it empty StrCmp "$1" "$R0" NoBackup ; is it our own WriteRegStr HKCR $R1 "backup_val" "$1" ; backup current value NoBackup: WriteRegStr HKCR $R1 "" "$R0" ; set our file association ReadRegStr $0 HKCR $R0 "" StrCmp $0 "" 0 Skip WriteRegStr HKCR "$R0" "" "$R0" WriteRegStr HKCR "$R0\shell" "" "open" WriteRegStr HKCR "$R0\DefaultIcon" "" "$R2,0" Skip: WriteRegStr HKCR "$R0\shell\open\command" "" '"$R2" "%1"' WriteRegStr HKCR "$R0\shell\edit" "" "Edit $R0" WriteRegStr HKCR "$R0\shell\edit\command" "" '"$R2" "%1"' Pop $1 Pop $0 Pop $R2 Pop $R1 Pop $R0 !verbose pop !macroend !define UnRegisterExtension `!insertmacro UnRegisterExtensionCall` !define un.UnRegisterExtension `!insertmacro UnRegisterExtensionCall` !macro UnRegisterExtension !macroend !macro un.UnRegisterExtension !macroend !macro UnRegisterExtension_ !verbose push !verbose ${_FileAssociation_VERBOSE} Exch $R1 ;desc Exch Exch $R0 ;ext Exch Push $0 Push $1 ReadRegStr $1 HKCR $R0 "" StrCmp $1 $R1 0 NoOwn ; only do this if we own it ReadRegStr $1 HKCR $R0 "backup_val" StrCmp $1 "" 0 Restore ; if backup="" then delete the whole key DeleteRegKey HKCR $R0 Goto NoOwn Restore: WriteRegStr HKCR $R0 "" $1 DeleteRegValue HKCR $R0 "backup_val" DeleteRegKey HKCR $R1 ;Delete key with association name settings NoOwn: Pop $1 Pop $0 Pop $R1 Pop $R0 !verbose pop !macroend !endif # !FileAssociation_INCLUDED glogg-1.1.4/config.h.in0000644000175000017500000000133713076461374013646 0ustar nickonicko/*-------------------------------------------------------------------------- * This file is autogenerated from config.h.in * during the cmake configuration of your project. If you need to make changes * edit the original file NOT THIS FILE. * --------------------------------------------------------------------------*/ #ifndef _CONFIGURATION_HEADER_GUARD_H_ #define _CONFIGURATION_HEADER_GUARD_H_ /* Whether the 'symlink' function exists */ #cmakedefine HAVE_SYMLINK 1 /* Whether C++14 std::make_unique is available */ #cmakedefine HAVE_MAKE_UNIQUE 1 /* Whether the keyword override is available */ #cmakedefine HAVE_OVERRIDE 1 /* Whether the compiler supports template aliases */ #cmakedefine HAVE_TEMPLATE_ALIASES 1 #endif glogg-1.1.4/.gitignore0000644000175000017500000000107713076461374013614 0ustar nickonicko# git-ls-files --others --exclude-from=.git/info/exclude # Lines that start with '#' are comments. # For a project mostly in C, the following would be a good set of # exclude patterns (uncomment them if you want to use them): *.[oa] *~ moc_*.cpp ui_*.h .*.sw? qrc_glogg.cpp Makefile glogg glogg_tests massif.out* core.* doc/*.html # File generated by Qt Creator glogg.config glogg.creator glogg.creator.user glogg.files glogg.includes glogg.pro.user Makefile.Debug Makefile.Release debug/ release/ object_script.glogg.Debug object_script.glogg.Release # Tests tests/build/ glogg-1.1.4/release-source.sh0000755000175000017500000000101713076461374015073 0ustar nickonicko#!/bin/bash VERSION=$(git describe | sed -e "s/^v//") echo "echo \"$VERSION\" > .tarball-version" echo 'touch --date="$(git log -n 1 --pretty=format:%ci)" .tarball-version' echo "tmp_tar=\"$(mktemp)\"" echo "git archive --format=tar --prefix=glogg-$VERSION/ v$VERSION >\$tmp_tar" echo "tmp_dir=\"$(mktemp -d)\"" echo "tar xf \$tmp_tar --directory \$tmp_dir" echo "cp -p .tarball-version \$tmp_dir/glogg-$VERSION/" echo "tar czf glogg-$VERSION.tar.gz --directory \$tmp_dir glogg-$VERSION/" echo "rm .tarball-version \$tmp_tar" glogg-1.1.4/doc/0000755000175000017500000000000013076461374012364 5ustar nickonickoglogg-1.1.4/doc/documentation.markdown0000644000175000017500000001131213076461374016777 0ustar nickonicko ## Getting started _glogg_ can be started from the command line, optionally passing the file to open as an argument, or via the desktop environment's menu or file association. If no file name is passed, _glogg_ will initially open the last used file. The main window is divided in three parts : the top displays the log file. The bottom part, called the "filtered view", shows the results of the search. The line separating the two contains the regular expression used as a filter. Entering a new regular expression, or a simple search term, will update the bottom view, displaying the results of the search. The lines matching the search criteria are listed in order in the results, and are marked with a red circle in both windows. ## Exploring log files Regular expressions are a powerful way to extract the information you are interested in from the log file. _glogg_ uses _extended regular expressions_. One of the most useful regexp feature when exploring logs is the _alternation_, using parentheses and the | operator. It searches for several alternatives, permitting to display several line types in the filtered window, in the same order they appear in the log file. For example, to check that every connection opened is also closed, one can use an expression similar to: `Entering (Open|Close)Connection` Any 'open' call without a matching 'close' will immediately be obvious in the filtered window. The alternation also works with the whole search line. For example if you also want to know what kind of connection has been opened: `Entering (Open|Close)Connection|Created a .* connection` `.*` will match any sequence of character on a single line, but _glogg_ will only display lines with a space and the word `connection` somewhere after `Created a` In addition to the filtered window, the match overview on the right hand side of the screen offers a view of the position of matches in the log file. Matches are showed as small red lines. ## Using filters _Filters_ can colorize some lines of the log being displayed, for example to draw attention to lines indicating an error, or to associate a color with each sort of event. Any number of filter can be defined in the 'Filters' configuration dialog, each using a regexp against which lines will be matched. For each line, all filters are tried in order and the fore and back colors of the first successful filter are applied. ## Marking lines in the log file In addition to regexp matches, _glogg_ enable the user to mark any interesting line in the log. To do this, click on the round bullet in the left margin in front of the line that needs to be marked. Marks are combined with matches and showed in the filtered window. They also appears as blue lines in the match overview. ## Browsing changing log files _glogg_ can display and search through logs while they are written to disk, as it might be the case when debugging a running program or server. The log is automatically updated when it grows, but the 'Auto-refresh' option must be enabled if you want the search results to be automatically refreshed as well. The 'f' key might be used to follow the end of the file as it grows (_a la_ `tail -f`). ## Settings ### Font The font used to display the log file. A clear, monospace font (like the free, open source, [DejaVu Mono](http://www.dejavu-fonts.org/) for example) is recommended. ### Search options Determines which type of regular expression _glogg_ will use when filtering lines for the bottom window, and when using QuickFind. * Extended Regexp: the default, uses regular expressions similar to those used by Perl * Wildcards: uses wildcards (\*, ? and []) in a similar fashion as a Unix shell * Fixed Strings: searches for the text exactly as it is written, no character is special ## Keyboard commands _glogg_ keyboard commands try to approximatively emulate the default bindings used by the classic Unix utilities _vi_ and _less_. The main commands are:
arrows scroll one line up/down or one column left/right
[number] j/k move the selection 'number' (or one) line down/up
h/l scroll left/right
[number] g jump to the line number given or the first one if no number is entered
G jump to the last line of the file (selecting it)
/ start a quickfind search in the current screen
n/N repeat the previous quickfind search forward/backward
*/# search for the next/previous occurence of the currently selected text
f activate 'follow' mode, which keep the display as the tail of the file (like "tail -f")
glogg-1.1.4/finish.sed0000644000175000017500000000072413076461374013577 0ustar nickonicko# Add header/footer around the Markdown generated documentation # Copied from Damian Cugley (http://www.alleged.org.uk/2005/marky/) 1 { h i\ \ \ s/h2>/title>/g rmetadata.inc a\ \ rheader.inc } 2 { x p x } $ { rfooter.inc a\ \ } glogg-1.1.4/tools/0000755000175000017500000000000013076461374012757 5ustar nickonickoglogg-1.1.4/tools/perfmeter.pl0000755000175000017500000000174413076461374015316 0ustar nickonicko#!/usr/bin/perl # Take a debug log from logcrawler and output some perf statistics # Can be plotted by echo "plot [ ] [0:0.1] 'foo.data'; pause mouse key;" | gnuplot - # Or an histogram: # plot './qvector.data' using ((floor($1/50)+0.5)*50):(1) smooth frequency w histeps, './qvar_default.data' using ((floor($1/50)+0.5)*50):(1) smooth frequency w histeps, './qvar_50000.data' using ((floor($1/50)+0.5)*50):(1) smooth frequency w histeps # Better: plot './0.6.0-3.data' using ((floor($1/0.005)+0.1)*0.005):(0.1) smooth frequency w histeps while (<>) { strip; if (/(\d\d\.\d\d\d) DEBUG4.*paintEvent.*firstLine=(\d+) lastLine=(\d+) /) { if ( ($3 - $2) > 35 ) { $beginning = $1; $first_line = $2; } } elsif (/(\d\d\.\d\d\d) DEBUG4.*End/) { if ($beginning) { $time = $1 - $beginning; # print "$first_line $time\n"; if ($time > 0) { print "$time\n"; } } } } glogg-1.1.4/tools/timer.pl0000755000175000017500000000100113076461374014427 0ustar nickonicko#!/usr/bin/perl use Time::Local; while (<>) { chomp; print "$_\n"; if (/^- (\d*):(\d*):(\d*)\.(\d*) /) { $time = timelocal($3, $2, $1, (localtime)[3,4,5]) + ($4/1000.0); # print "$time\n"; if (/DEBUG: FullIndexOperation: Starting the count\.\.\./) { $startcount = $time; print "$startcount\n"; } elsif (/DEBUG: FullIndexOperation: \.\.\. finished counting\./) { print "Counting: ",$time-$startcount,"\n"; } } } glogg-1.1.4/tools/genlogs.sh0000755000175000017500000000073013076461374014754 0ustar nickonicko#!/bin/bash # Generate a set of test logs # 5 million lines - 1, 429Mb perl -e 'for ($i = 0; $i < 4999999; $i++) { printf "LOGDATA is a part of LogCrawler, we are going to test it thoroughly, this is line %06d\n", $i; }'\ >/tmp/verybiglog.txt # 100 million lines - 1, Mb # perl -e 'for ($i = 0; $i < 100000000; $i++) # { printf "LOGDATA is a part of LogCrawler, we are going to test it thoroughly, this is line %07d\n", $i; }'\ # >~/stupidlybiglog.txt glogg-1.1.4/glogg.pro0000644000175000017500000001735713076461374013455 0ustar nickonicko# ------------------------------------------------- # glogg # ------------------------------------------------- # Debug builds: qmake CONFIG+=debug # Release builds: qmake TARGET = glogg TEMPLATE = app QT += network greaterThan(QT_MAJOR_VERSION, 4): QT += core widgets win32:Debug:CONFIG += console # Necessary when cross-compiling: win32:Release:QMAKE_LFLAGS += "-Wl,-subsystem,windows" # Input SOURCES += \ src/main.cpp \ src/session.cpp \ src/data/abstractlogdata.cpp \ src/data/logdata.cpp \ src/data/logfiltereddata.cpp \ src/data/logfiltereddataworkerthread.cpp \ src/data/logdataworkerthread.cpp \ src/data/compressedlinestorage.cpp \ src/mainwindow.cpp \ src/crawlerwidget.cpp \ src/abstractlogview.cpp \ src/logmainview.cpp \ src/filteredview.cpp \ src/optionsdialog.cpp \ src/persistentinfo.cpp \ src/configuration.cpp \ src/filtersdialog.cpp \ src/filterset.cpp \ src/savedsearches.cpp \ src/infoline.cpp \ src/menuactiontooltipbehavior.cpp \ src/selection.cpp \ src/quickfind.cpp \ src/quickfindpattern.cpp \ src/quickfindwidget.cpp \ src/sessioninfo.cpp \ src/recentfiles.cpp \ src/overview.cpp \ src/overviewwidget.cpp \ src/marks.cpp \ src/quickfindmux.cpp \ src/signalmux.cpp \ src/tabbedcrawlerwidget.cpp \ src/viewtools.cpp \ src/encodingspeculator.cpp \ INCLUDEPATH += src/ HEADERS += \ src/data/abstractlogdata.h \ src/data/logdata.h \ src/data/logfiltereddata.h \ src/data/logfiltereddataworkerthread.h \ src/data/logdataworkerthread.h \ src/data/threadprivatestore.h \ src/data/compressedlinestorage.h \ src/data/linepositionarray.h \ src/mainwindow.h \ src/session.h \ src/viewinterface.h \ src/crawlerwidget.h \ src/logmainview.h \ src/log.h \ src/filteredview.h \ src/abstractlogview.h \ src/optionsdialog.h \ src/persistentinfo.h \ src/configuration.h \ src/filtersdialog.h \ src/filterset.h \ src/savedsearches.h \ src/infoline.h \ src/filewatcher.h \ src/selection.h \ src/quickfind.h \ src/quickfindpattern.h \ src/quickfindwidget.h \ src/sessioninfo.h \ src/persistable.h \ src/recentfiles.h \ src/menuactiontooltipbehavior.h \ src/overview.h \ src/overviewwidget.h \ src/marks.h \ src/qfnotifications.h \ src/quickfindmux.h \ src/signalmux.h \ src/tabbedcrawlerwidget.h \ src/loadingstatus.h \ src/externalcom.h \ src/viewtools.h \ src/encodingspeculator.h \ isEmpty(BOOST_PATH) { message(Building using system dynamic Boost libraries) macx { # Path for brew installed libs INCLUDEPATH += /usr/local/include LIBS += -L/usr/local/lib -lboost_program_options-mt } else { LIBS += -lboost_program_options } } else { message(Building using static Boost libraries at $$BOOST_PATH) SOURCES += $$BOOST_PATH/libs/program_options/src/*.cpp \ $$BOOST_PATH/libs/smart_ptr/src/*.cpp INCLUDEPATH += $$BOOST_PATH } FORMS += src/optionsdialog.ui FORMS += src/filtersdialog.ui macx { # Icon for Mac ICON = images/glogg.icns } else { # For Windows icon RC_FILE = glogg.rc } RESOURCES = glogg.qrc # Build HTML documentation (if 'markdown' is available) system(type markdown >/dev/null) { MARKDOWN += doc/documentation.markdown } else { message("markdown not found, HTML doc will not be generated") } doc_processor.name = markdown doc_processor.input = MARKDOWN doc_processor.output = doc/${QMAKE_FILE_BASE}.html doc_processor.commands = markdown ${QMAKE_FILE_NAME} | \ sed -f finish.sed >${QMAKE_FILE_OUT} doc_processor.CONFIG += target_predeps doc_processor.variable_out = doc.files QMAKE_EXTRA_COMPILERS += doc_processor # Install (for unix) icon16.path = $$PREFIX/share/icons/hicolor/16x16/apps icon16.files = images/hicolor/16x16/glogg.png icon32.path = $$PREFIX/share/icons/hicolor/32x32/apps icon32.files = images/hicolor/32x32/glogg.png icon_svg.path = $$PREFIX/share/icons/hicolor/scalable/apps icon_svg.files = images/hicolor/scalable/glogg.svg doc.path = $$PREFIX/share/doc/glogg doc.files += README COPYING desktop.path = $$PREFIX/share/applications desktop.files = glogg.desktop target.path = $$PREFIX/bin INSTALLS = target icon16 icon32 icon_svg doc desktop # Build directories CONFIG(debug, debug|release) { DESTDIR = debug } else { DESTDIR = release } OBJECTS_DIR = $${OUT_PWD}/.obj/$${DESTDIR}-shared MOC_DIR = $${OUT_PWD}/.moc/$${DESTDIR}-shared UI_DIR = $${OUT_PWD}/.ui/$${DESTDIR}-shared # Debug symbols even in release build QMAKE_CXXFLAGS = -g # Which compiler are we using system( $${QMAKE_CXX} --version | grep -e " 4\\.[7-9]" ) || macx { message ( "g++ version 4.7 or newer, supports C++11" ) CONFIG += C++11 } else { CONFIG += C++0x } # Extra compiler arguments # QMAKE_CXXFLAGS += -Weffc++ QMAKE_CXXFLAGS += -Wextra C++0x:QMAKE_CXXFLAGS += -std=c++0x C++11:QMAKE_CXXFLAGS += -std=c++11 GPROF { QMAKE_CXXFLAGS += -pg QMAKE_LFLAGS += -pg } isEmpty(LOG_LEVEL) { CONFIG(debug, debug|release) { DEFINES += FILELOG_MAX_LEVEL=\"logDEBUG4\" } else { DEFINES += FILELOG_MAX_LEVEL=\"logDEBUG\" } } else { message("Using specified log level: $$LOG_LEVEL") DEFINES += FILELOG_MAX_LEVEL=\"$$LOG_LEVEL\" } macx { QMAKE_CXXFLAGS += -stdlib=libc++ QMAKE_LFLAGS += -stdlib=libc++ QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.6 } # Official builds can be generated with `qmake VERSION="1.2.3"' isEmpty(VERSION):system(date >/dev/null) { system([ -f .tarball-version ]) { QMAKE_CXXFLAGS += -DGLOGG_VERSION=\\\"`cat .tarball-version`\\\" } else { QMAKE_CXXFLAGS += -DGLOGG_DATE=\\\"`date +'\"%F\"'`\\\" QMAKE_CXXFLAGS += -DGLOGG_VERSION=\\\"`git describe`\\\" QMAKE_CXXFLAGS += -DGLOGG_COMMIT=\\\"`git rev-parse --short HEAD`\\\" } } else { QMAKE_CXXFLAGS += -DGLOGG_VERSION=\\\"$$VERSION\\\" } # Optional features (e.g. CONFIG+=no-dbus) system(pkg-config --exists QtDBus):!no-dbus { message("Support for D-BUS will be included") QT += dbus QMAKE_CXXFLAGS += -DGLOGG_SUPPORTS_DBUS SOURCES += src/dbusexternalcom.cpp HEADERS += src/dbusexternalcom.h } else { message("Support for D-BUS will NOT be included") win32 || macx { message("Support for cross-platform IPC will be included") QMAKE_CXXFLAGS += -DGLOGG_SUPPORTS_SOCKETIPC SOURCES += src/socketexternalcom.cpp HEADERS += src/socketexternalcom.h } } # Version checking version_checker { message("Version checker will be included") QT += network QMAKE_CXXFLAGS += -DGLOGG_SUPPORTS_VERSION_CHECKING SOURCES += src/versionchecker.cpp HEADERS += src/versionchecker.h } else { message("Version checker will NOT be included") } # File watching linux-g++ || linux-g++-64 { CONFIG += inotify } win32 { message("File watching using Windows") SOURCES += src/platformfilewatcher.cpp src/winwatchtowerdriver.cpp src/watchtower.cpp src/watchtowerlist.cpp HEADERS += src/platformfilewatcher.h src/winwatchtowerdriver.h src/watchtower.h src/watchtowerlist.h QMAKE_CXXFLAGS += -DGLOGG_SUPPORTS_POLLING } else { inotify { message("File watching using inotify") QMAKE_CXXFLAGS += -DGLOGG_SUPPORTS_INOTIFY SOURCES += src/platformfilewatcher.cpp src/inotifywatchtowerdriver.cpp src/watchtower.cpp src/watchtowerlist.cpp HEADERS += src/platformfilewatcher.h src/inotifywatchtowerdriver.h src/watchtower.h src/watchtowerlist.h } else { message("File watching using Qt") SOURCES += src/qtfilewatcher.cpp HEADERS += src/qtfilewatcher.h } } # Performance measurement perf { QMAKE_CXXFLAGS += -DGLOGG_PERF_MEASURE_FPS } glogg-1.1.4/glogg48.ico0000644000175000017500000002267613076461374013603 0ustar nickonicko00 %(0` d !@#^ZZLrWttssb,,& `& `& `& `& `& `& `& `& `& `& `& `& `& `& `& `& `)/)*3-Q>;;ywwywwsqq n263􎒑􅌋YaZ;;TQQa^^|zz^ZZmkknwqqppKMJ9B9ݩӧ lgddEBBdaaqpp ebb|zzdaaswsqppDHB?DAyDhggB??_\\wvvÛĠZWWJPHnspgmjada^aZ_d^KPJ>}}a^^y䲃庑mJ_?!Z<#W<#ZZWWHD䲅軒ÞʬӸջʭ庒q0\!ebbtyw, >mkkϵ3g-䴆轗ÞíƲۺֲ֬դw'õTQQw}w, >wGWp9ϧٺwZBM0J-M/W3_5h;s?)anmm)&&|, >tssBa ^ʺ̺jNzV9W6Z5W/\-GVJGGMJJMJJMJJ^ZZ, >#ؗZҩӵ軒䵈}۞g,, >!päqaZTJ!qp, >)z<Ǭvw9)&, >VW޻W;6kg3qp|z, >a\լÚޠdܚ^HyYVV, > yAջ޵p3k-KHH}}|}}|}}|zz, >tssW/ӭкǬֲM#s)&&, >kjj&υDӵ̒\a9_\\, >ǻ,\ÚմmT /wvv, >W/k#zgWQB!j6GDMJMJKJKJKJKHZZ, >mkkʭj5-0m;̴/-/,, >MPMmmm, >^b^, >EJG^ZZ200200,))$##! ! ! ! ! ! ! ! ! ! ! , >DHDnmm)'')'')'')'')'')'')'')'')'')'')'')'')'')'')'', >^b^, >QVQ, >DHD, >TZVnsq$!!>wzywzywzywzywzywzywzywzywzywzywzywzywzywzywzyTWV2656;96;96;96;96;96;96;96;9050>ͱ>ͱ>ͱ>皛>^^^_<>qss >K$.YZZ $$$$$$$$$$$$$$$$$$$$$$$$?glogg-1.1.4/images/0000755000175000017500000000000013076461374013064 5ustar nickonickoglogg-1.1.4/images/arrowdown.png0000644000175000017500000000055213076461374015616 0ustar nickonickoPNG  IHDRasBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDAT8͒=N1 F#Ӎ~:FG4H܀Pq . ;Jpl쀐X\Eω8$A8a^3U}l&/4ff_!s.N)5p+"{#=B)11RJ`5'oL)i)7 `q\W"齿(=Y]f "vT˵jڶ-E{^ klo)IENDB`glogg-1.1.4/images/arrowup.png0000644000175000017500000000054013076461374015270 0ustar nickonickoPNG  IHDRasBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDAT8ݒ1n0 E?䑴 p}zޡ{ѥS)E hU^.va jx?RݱGaW-["6 image/svg+xml glogg glogg-1.1.4/images/newfiltered_icon@2x.png0000644000175000017500000000034113076461374017462 0ustar nickonickoPNG  IHDR KbKGD pHYs  tIME4TnIDATH ~=mh72Β@rWۇ:V\=H1-HҶ高y䓷LaRR/NK8N֎c E06)۷'X%Fa1&d>! Mݓyg+?%IENDB`glogg-1.1.4/images/up.png0000644000175000017500000000126413076461374014221 0ustar nickonickoPNG  IHDRĴl;gAMAOX2tEXtSoftwareAdobe ImageReadyqe<FIDAT8˵ԿOAYA 9-QƆ!D BJ!++mB ! Ƙ%THqp[yd̛7*;;a.7XDN/EDF@FUm"r9cx$"A~ig$]D|?< ρQf+` h[Y? ,QX"BU%Te]VZz (њ)灼b<㲪/'6+mY@6,bnuK/0eeWV+C} ܺ`WM,x4 cT*Jn;cWz~4|zwIENDB`glogg-1.1.4/images/reload14@2x.png0000644000175000017500000000165013076461374015561 0ustar nickonickoPNG  IHDRr ߔbKGD pHYsxxJtIME /:5IDATHݖM[U&&/yy.:AbKՅHt-ԅq՝ ([*)"F -Φh7 II;.&Elmg&E˹;s}I$b^P+:H VbTfΖުD"^ ǀ}5JS2@K->T~3"Sb ݩQ=LJo 8J񧀘/VVv'oXkۖtZ/fkjT/aIۍ.x8 ǫkͳJeQD}y Nل=E8Uʫd:$@Rfm6oGM#5S,ԪF!OܴW@pƓ%j5gvK*}I};}a"o/oUn-m)$d? rP2 4CA7Wܡr2+׾m4j[ATTjJ53@I TY)F@;ȝs9xG 8߽z:v^/ XXXcIcHεڧ ؔH^0 LJF}vfK*,06 \4ZWY=:8nڵ+4- L[w iA8–%K PB\T*2_T'T) 15I'x?!$fzIENDB`glogg-1.1.4/images/newfiltered_icon.png0000644000175000017500000000027213076461374017113 0ustar nickonickoPNG  IHDR GbKGD pHYs  tIME;IGIDAT(͏ Ī;NPDTI‹6$A m ?bn L g&BEv`#@VIENDB`glogg-1.1.4/images/osx_installer.png0000644000175000017500000012734113076461374016470 0ustar nickonickoPNG  IHDR&`'sBIT|d pHYs.#.#x?vtEXtSoftwarewww.inkscape.org< IDATxy]Uy wHr3Aa Qq@(b־vr(jjVADZַb߷Z bDT, SHHr 㞽ky{k}νg=Jλ*}%f$57c tzʎϺjXṶ$ܹ4P[_zٹVu~FD0?88zG>0nyS[oͪמ K}yHDe%v{c9]\uq G#_KH5Q+xOfbJ7#E(aW+? $cuҗ@R+< eae0O bVc8kP!x}l޿*Bxф@un3?*815k@vп:BN3[CBƼ\RCno^ Qv u{B{t3>qss&DN]+^޸P7 (I iIC %>Ew@aSHlԑps>\# !6q~%S+>ycT'ҢР;(PZ #;PjX* ( Dz :sD(,8 }hRNJ)=n "^ @8'ws2B5eL/)` "RU\K5ER=xWQ`cX'oiL=A^La )*m#XG+uD#y80š* "& @d1QD齑\x?/\хUET|)^ ų"S HdAV%g+ q3 1|Ts4k`uUh ]q+RP!<*>/RQ\yBѯ2rY$#HzkR0[q" $ˠ&OQM`QI TfZ]5jHqyL1GBIHE# qRY#QGG J 1¹=GU9Z"\W8"5EV$SPi5jGi.*'J?1ܚt #H\k\#1*D\COq !X8D|aSYPƱ  *&j7RbPQH@iUJt"0wEץpN dHsRGoUDDt!P&\r3+G>?\ TLR>bqsTbiJpK惙[-6U qU qQT ## ]oQeV}9t5I /R}r *B5%H񥢨PTF&Y Pem )!U$5:"UH?#0b" A QС@?4T^{&xQXJ)hXO?@+Eś🨪'kT*|H*KNY"!!D:x0 k7-8kBGUQI@ESMR\(}Ӏ)_$s!m H$_Ȝ :U$,U}]uF ZX@b'4TP P\O`V%Um/Ihu$4"sl'x{뷅 .& Ue-d0AE}AJ*kJԀZS#X%> $1HT Kw:ɍ)#USB@TuEfi(%1TILͭUH|I,uDg*W9@B\1V^F/YAOXjR>)R=yL:"1C=ANJBJbs QTKUąbr2Tw%k` 2.~~\JPQ@q)S=i ezPO LTM\mH*QG S5>`ī*X 1W~ 0fMIJ+!@Eśb1To@i4#RO $6:xWG* !ƘÇjg`בQT%*+6X|rH1RQAV5SHЯ//Oaf_~P BT<0zV]U%-&j6TQT<*>~z F%H4sf Ĥ l=.lU)|VG8GjОTHE7D (K=1lٴMd@"QG*bÅM^Akx2oh1 heKC)JT^EOJ ;slE`DmU q#!@D @l~|(nW8"UTTO RQlT4O@I`Aᵺ.tVPP%GR1xKטj `g&4B;+\؄5i=Pllz^b *ԔҜ\EQHHX'|to m 񮎐)0IT".!ĤսIEh8^MEVZM )^Tj~$;DQO UImˉH|#Jiiܸ[!2Յ쵗X $&yTbW*4O?J]@qڞPO 1$N6}$gH`/;3P=\B>`q+6b@JHE%ͣC@IRO "WI*K۔cT!"!$4@CޮBr mH^c& d6.؀+5ERT\%E!ͣCPT ;6IL }6*hzGBˆoUD\ԅ  W5N \A2xU5'ئzD#.6g1Ǫ' WTR*UH0uD1UFDr!ϫ.\V%+;u+@OLMq ):EՇπBOdI6 szҠoS7$i}HB#`D\(AHL8$8v"¶+ئzLTmJb?eQOČUS7ێ\PaD $Ց0R*R e\F:zM5\!nR.XrX *CʇRxKt{ I'{4!*ۯy߼.^}_i9$R5`#!nЅy!ݛuLi#^Z#/}Zxq .W[UE *5E7RlTilFszGؤv*Pbqnp5p>ΪLD $-b H 0DB |G%o"n5K8iÊ%TSL!ŕ#c(1wṮƕ; ؼ\^9Xԍs/ "^% $*kD1HU RK[>GHnZVpߧ)h) ŧRr<E5L=T' PRJt4a#H׸RG@ yebu~ǰkG&z(J5e%35E)2O+Ŧ丟&Z=52rnt %UI)}G2DG #T1:x &az-ZɐZ9p +BMQ\.$0D$}>M 8LڔJl@X|ŠjZDM)t#7*J:c#KrK𗠬T<1U؁2u!"67nB4$HOz.ҹy&(PQQS*Jt,:B==116l$tFؽ0Vgi@QaHǪBl M\GXx*Gg)&)%HQ*JH@I[`V{T hcί%( 'qbptoUi&i@R||RG`SƘ D[@-C=qeᤡ %S7*ݏ=;uKmTūLpdGz~d쉇FxG<עeMHZ{F6rdM ݔ9a'v+&zoM1(+ݽ3w>0R1͵2po"zGxf\{\(}(iz'zbev_]C߼hc7_}ޝ?3wch)~/{3u6mk?mך}@Iw` #gZ`{uǤiwHv=Evk x؃zGۙ8H%Eȁ}y}@"KӘܬC5(xT anMs5q\eAH?yHPq)EPCKPI-;YXF((1Mz?}|3CKҝY'x MaHk{GOؾW'\ъNj{nf4G!W|5vZKuEL~p{iZM<@0Iτ9 !Ba\V8NgSXg~TiXWzDžzlliF'—}l}pOck8 Q7w%FsфJx7۝b 9۞r^H푭w{/Ւ.P3]{ k I:Жkms`xk\ ׼g/ǓZsg}c`nևī:FlUG :|b Q5dmkina%Аz^.(Q|( mup EN4\]IH$IgYvo O/O $k.eNtj& Jj' }߱l>S^K5q49/<˯4G_r3paG^[mM HڊkL?ҳh(ZrK>=zui/xϽk^7Yk 1 `$:Q1WC5tWbx Š2(})bGUQ*j K(CzLJzڐ?Nô؎<7.9z>qQw?ܵ}鶧wW^׿?Ę^:)(.[]Wgse'u|_[+^u?v;CKыKƾKOz'8@}c#dζ~9c~wCƭ?϶ƎMw_weG?i,߭5_0X d_!{T X`ETLאbPxeO=_w" %IiÞ-/5dO㞸{o{Ʒozz>G 1J94Y7?ZҴ;>ZG77[{w30q÷{ѮÞG̾oO9ӲyT>E8Ѯ#mwgy9t`֝BkF/|-qhJsPۮ#e})]{w/c}9k~ ~fu9>;UOdϾZG~^bޱyZ|]_RFy?*E-* H~Y^|m9 5yl֫br,g.dG[y 8̻75^›O4-^o5zoz?HԷ\wY>#{3=qW@Ӟ^=^rϲß=N%[ϢewgĞm'?[~~ tТeum:_r ߥ'6 Htn@M>h^P%_lL+)x9$Lj Ev5d)됍C¿*r89u&#/CL8<JJs #:.XvS{x{X@~Lp!#@>Ƿ?|򹃟q:/,;K>чoyS6d<%zc`354 |FݚS Zy)0\t^jL7!|i7mS6@" "Khi,R41n:ƷarCV/kr{+K ?? RcdYTE4JJ&ţ?\yץe54XԎd2grJ`Չ/}HwshKPpeeG2[ ӞZF~3s3lv}~0`6ݳ?rO)>G@(_ر3._,7^랼'H/xcb FTtCBv6k*L\\W4TBBn>1'2lm'6ʁ-wss'ݙǎ~k޻܁݋v' Ǯ¥μ6ٺ Rfԅ[TZawl/W7+o|? 55v_חCw4nnO4=iH|O~~H?q?n{j}?qi6$́ݭL9kʩ4yΜ: 9Yʣ_a^T{ݳ"xIjH'f`QQ!!EwvۋӚX9ѕS`G}猷wd2YI1vڋi/OG[}Xa]֓:ҭ?|Ncݻ _;49u&齣'M-M[79璋;l8{^vş'^6SY\uog]3*0D*~Zںmw~Zj"`q+*RTze]zl(PTw_Ys6[Jb T\CSdo9m}c-w'ϹOׄ4,wњM~|z`~ؒmO|m JВ??>>- IDATEuMԳ߳#HeG|9eSMR6ta'Kא;R:HGcX89e' SS,WG S/uN:ܼ1ܴEGrry^ɥZPT[izGa.pRBԎ^l]񌊝FPdrϵ>sr擲jY{~oO3֚CgnHHG8~7JzOtlFVdB }a6>9dQŭ~n=S>$ ۰p>7g]Px){oZZ}14ahF<_O?Yq硍BPGT15:ʈRx[)ݤ8 &Tc6 @RbXU+5BIIT4O?((I6w($DՎ.ŤrB++`"ҧؖjGx+?v6TE W{?i@gz!7~>x߬N7'ف6K+?ǣ94r߳^[A\#aX|zՒC1~ލ`O{~ޙ I<vC}`4ec$F͵#: B Aߤ-÷``A>L!\Ta2А"ucMv=uǟ{5֜Ee~Ǥ5{ooGgC/˞L9>vZ#=O!U,?~V2ݓ-ihъGl/o璝ӷכ]z%\O5k ,|3=qt'c|뗏>]ꅊ$(RG* lIQ>BC,KnX3?bXis%`zǙz9*#kt#@.we"m(r90*Pņoػ>wC^zKyGgwZm)=;OW9kP̼=?r|HG3o6$$}`r9`[-۞ҋh ,ǓJѦ_}qŭ__28ɢzxhdo8CqޭOY{f- ݔI&dFBBTR8/5ΠGԑySg*gr1Vژcw5+Cޘy/ 8NO'5j`,{`w[H=a͗]; '̖YT %i+o}RZn=O}#*#[xS~r܋{!'IJBEJ'i.Z9C~_uwagz0 y ,\#N{M$d>zEˏ}jזSn'ߤ0Nmɱ-_b?S7}Y0n{d҃gmMw#+{=n^z o?>=M[Ck(uX (!!O F˝{BY~\)UM%DrjIw$_ :(?q=]vQkr1XQ$Im򀧾g L~xU'{O;3wP3ZW/ЀA;UEEMURdn E"AW0̎ɕ bNQz9{kdLDPb͕ (y+o"k63(i-I1ݙ޻=5_k4/=z7\)b\cxw>|.X}` EۻE鉱d hy?< ؙXM^osxo7ww{v'&wv;FV}˥ŻvU рWkgtĔ>ҀD-UOHIΫBDž_U8!wډNѸ?; ]y'~>?ʻ.浫l<KuR3˛zAdi(AꇮơS>Tb(2H]w\TNJ]bM:WR9C%<H;o^?N}p#/?\_~MI鎇nZt?D6^PBA _;ޔ=~ι~zֱ+ؙ޻59v$Im_}!yv5~ϭ3.y{[tX!Uɯ-1u0>}=k#2(iב&k O\;NTqCxQ[xvJ Z1MLw>yנ.>nױ7?eeIMYXϩ Ι_}i]-@tN__T +(zTv*ԗN3[{3}~66q@IE:ɤO;zDNFŧ޳P=p;JЏ5F 7ݣ~Yc}s5X1iZ&n{GÛB R=&n6sCAtNe [WbXep_N(#l&hfUNM)aXx1d^zϳ&ǎ%'mC/͵;x3^?s798VqPR#=i/أ~oo;Qd//^ ;YpG7+jO4mI>7:/O%)]Cą:G磕]S),9ڛ闋2} yUEEM!75T3"L<^Drf}'p6%:L=-\0k J.||A%.~oB,$XiM-عjjʜO|%?It=>2EZ\iMw,ڳt]/?߳cӺ/uZt :`悝zc5535vԞ'OIB*id1_8CKa>ꈬ&-@IJ(|6a6:^[I5'sZzU0!J7P=(Ol'1N$(fn6|O{}YqHu?\%;}vl{pƏ]Ŏ*]ß?uzߧ;GεpaK\#Q"K<22 )"l_iyg8a #ߦXp\􅮠DGI9M ;_zm%&ɒʛ_/yp'=|"_|ksScO\\K~No|chC Kص_|5{Ww;$Sŷş_||>@Z#.@Bd{I/cH>$d"b *Qa*ܹ {x*Jz2''"=3&$MҼ{ڮG8fjϓu nQk iX/og"q).'v'tH;{k|N~nwzQ{k ||խGmTQI"_He0bx0k fLEXBT\C @4/yN$XDji>Vt`W16q sW $UJDAx"NtmXXa*> U6P[DT]cķe&"=sJ]nT) (񞶙@FtAD B4#f- [1yS`ŠTb%z""w2W$9=V½5(O%uRqQ% #FCB#18W't%BW%wbVO|b NJ3%CMHd l +\PCHX;I`8['O"8:ė&u[%1MT $È*Q؟Sߠb)POLN\6bӁ&l b,T%yJI4S7U$$&uD#+qU+q,%D(U'plM C2%XnP% $. q"6ab ,*RE@1QObOu8I\{z5(',u@%񑶱I٘#È a&Rp E[E@ R\JIU; 'R8iW/0CI|$ִ-L`$$T!Jˇ6b +2HUR\(UJ'|'"$f(5OP-uSJ H<#`DD|BH +A!@1NXw|'pZGN`WBެPRPbbpׁjQR~\|/H1IJtꉏξ 'EI&1C J4m3}@L-ET@JR;|'}N L_`bvZ JL*nB$6@bĵ:Fk )$L{8"UE+P"ROHT'v-_/()Jt+o(k?I(/ij:"!Q-sxq+!%*aj'D&r2hbA ?ԍJb%C: 0=o5.T XJ (]kcLIyNW^ :#'nvJX%!M .Uĕ: #6DxSsb=hV@PMA EP\wdv {l:0S3%եn$KuFX "CujX^SRTT'u|MćuJP"KO:uc^o IL@FA1#Fe)"kP1GI压ԎP=L*82@ Kp?@I?+D'm@F\!LpP'Т*bT;8!+.otek(̪ob0Kݨ$i@@FX*".d mq , ߀2N\5aӁgfXD&!|%&fWePJbHtS52QEL!6Ű7}1hz|JD=Il8Q4R:OײW,x_S7 *i&( "c_ݐ-x"cDڙ10j_I(<*6H(:{5N/T@JbTpb+qfX-0qޯNJ(+kh(I7i](T$s "W焬Ɓ:h[AIQ@ R\GjgNϙ*$D 'uCI_I<$H1 D|W@CcmOd.X@JH%4THN\Uj k7)|.}%&8Pb11RIB:؂ С 4 D@Rb$DNepb2&B0q^WӫdJ0K@%Htx1ـ/f %pq+ Eԓ S;po_R8ͮ&PͮTaDg?2l&Xn*)e`B (!Փ[8PNXkRS߄ &!KJ|UƚTPRJ$RȠFTA B41OqiOiB!(ZRAz{jg_ ̰6~NrӵUVL8)vqu Hu $`H6`qJ -\XR*k8IpEZT7t H $o)_f׾Ob2mc2B EVJ(Dwүp⫌jLLS8sJjIľ~@1}JLݸN:F "U D-mAD(|cpbbUxob)Q Ei _I WJ恄}~<>@D!1m,c@%EiS@TډNt2:Hd_N(J8:esJl$U@ 1RGLaDDx@BzL'T`%0!PM=w8~׍r0g 'uJza%U@bP0 "bG0ֹXS[@M=|ąߤNL<%>p|pWRIq"=#M`1PQb/y8bu7 I."=%pl|%CFIO( 񕶩 H|ˆBbBsPRzbl o"Ct\}xNj`_ICn9pdc q # b !Cc6 bФAXAJP=pb݀-NkL0q0VJ]JUa t `t *!j@ N2']NŔqaM^sUzz=KJX]]g(% $ HnEVL@E)V*)RO R;fS:<Մ &hx5, ݯDJc]B H IDATS JUDKPEr ! :#eF94a"TQ<J(q_ej' 8 kߤ $9X0(W2WOR%+u@B BtAw8 PсeE5SzRd.Il~^Jjդ&^C4Rٯ` bXTW@b`ą"@PLNTJu eNgrՒ|{Iݨb PNۄ1[i3e3c/jM)$F)*J%#3'}ҩ&Ԓ*˃%c+ѩw(qq(:FL .uZAERBz2W$J)kUD LZ2R86} JL$i@F0-P@o@ N:xj2SV LRK,H-b()S(QRIL6DD&(C8FzujGw"؉N(>: B ʻե3_ëZa1_I()EPF3g6N(IF$?*0RiXY |%ckһ&zk !M)KE0r  fNz?HymL`N<8wp2D*uE4IDi! HsdDʂ<{Z[C28Ցv) 9 ߷vf8h7-{E$]jIW _ 03Ւ7fՍJH$!0RED ;ͧ6獀%%jj"Hxw S;ʉNz2~Ix7ƢԐ~PKgux9[3LR8R8mWJtK}$΁R)*ƒUh.d?~~2í2Z} ʰȔ('ِ\=f~Eꉂڲwv kr&-A!.RKmRRq=D؂jB!Bb.&- 94򱾪ĩH-axQOnL*nT HLM\t/.p`2haKaVU$JoʼnDA=15ź6Gk>I8є6VmvPC%0 "QnHTl!1Npb$phy)qpddʟZ<.BzO3`2 ɼZ^-:-Xn|m\($ 8;8/W?ߴ m%%FAqvtKcWNL̰s%3U# 9 jIםZbbxE{ֹ)BpSÝ#f(1IHDflUɟ^$0g23 ; R9xZ@F XF k/GBfV(a(B [(/fcXFY1XxcT3,/BJQ0cIRL+&Uϫ%twe+QUK\pbg~IGԑ(F B4tTA#h+ %E[E((PY!)aP# 3<ߢ qZ:$ʢb2WLsI-qlxE 4$ ծ$OEG"TH2 >G~@OL__B( ,Sr aJA5[j&_HO%'WL]^y4SsQ칑+k$V@"H@4M3V2chY63ʚdba5*'4ĺ0쩣ZBlxͅ}Cu^jJDQo\ԍ.h+$r IhIC*$644J|)"UP!%=(^Dd5VN P8@)'V6Vg^ajLq%ǃvug ϖUD$0UID] )D 1Y- ǫ@ ѩ)wzրzjgRQ]8CV}16{#=OU}KjqѯDfJ\ITUݴ $WUY5E'SJxEquI)}t1 _tz*'4NHk R-N J2TX6l $agAdhX)~^o*l()4KeUN +t̰!S:Z5DŽ!M"K_5XBDvQ4T[ p}%<2(!CCJ!pHdi%b?{ `xFzG)ND8VR)kUzMJb]Fiq"TS(b*.UpH16?ÁۧĥdfM- 1dlQX%9n$q@E)* Pϼ'I'.D؅|%sI5K VU"RD)x'Z[A mDΙ=GpbY K% $ a-|77XTA%bz1Td'yxL0Áֻj>U>J$]i PuD**-SydvZe +)WyNJt$>THhQ VRP' 2N&[Rb߄9V7Q J=t\&F: .nƩ$bUӫ sRKR804cv JLT[ aDD=dB ]EŃSOx4wN; ͰTHĦBPMEUB8PgLDQ"qupU Q  A BU@'a+ʐPXNX]8a}=/t&ytߪR` 6t|n2aiTJV6aTB5#a:P FRAu@)ڀޡP‰+3,NS:"7ziV\YRS5ǝ.PspR8)nqJ4$Z*` ޼HJ@E):bhN<V'CRJG0. $ 2|W(Eak01QUG%%)PM!C0ԍJ $2Ŀ78|%*fWP!lIetRJzs!EPc-!4իu~jRMx}MJ:g.D4W#%Dؖ #js SZcxU _I?A.`dc B#1`bX~^@UOX'ppvbaJSMTçjA:ʆ ~}-SZR KD;#T %2H6f `dx=XױZ )SO pO 30^46t{/%PMf&tN%IH,'\WEE>~4:y+7*89Ʊ ELDQOxDBubL}r`E(;lnz1^(tTۄ}bT0T9o}&Dx5zl٨/12aQANjZ:^ FW(l _F.[5~/9<zc* OA^TQUN :PM a:UٰpϤrA1+r"ȉ1%FD+b4cD k(n @R 69k%; PpK %8H(I}MB$_9`S+_5#2FHӫj35jI/D^0fU19Ξ "8A@04F|L`vEǠR>K*cS5pBo2T'0ˆ|eHaf$&Ԏ ht&JitNEwRO[Pc.T|Z91m| #D1TSKhA%=O Jad |P=Q(7jYht@ bBuS?oh+sb `7q؊y8FEW/Ʊ jI/TPª92(Yi%"n+xU찪uJV6Px?|hmup{s5^eNA'Ivt*#&U~HB_=Dibag0pJT`f_qn蝟\:pD\I" |DLXTJMT\*, Oy%C8Vj j(}CH%H>>HY5ųܛMynX[;.f1sxN(E(SxiؠdK }2ENJ%,d[IXt{Uho+}CI$, g +U8 %`U}&J l?VT@bak1ZJc]R*WSE knIC4愜1c `=JO0̰IퟣJ؊̩2be7q_(̯s5R41dy¤pTPPr1oT7!VzTCf݇C;X7ijw$7kܳ h,T8&;AA $!zLpq]aC ۰|,1UDZE1$ FXr 5jKsuwՈ*"&E9^I/|c^L/%`xK#$- ,dha||#0o[(4Jw}iХYM$1}UWĥcV? =U"*2mq|fbb/ ˵4)pfۋuӍִzc VbhZ4 |U&e)m WHcmyA g7u8oDRW6Dc>TBquLH:G9:d1=uYuvPU,ow} n\B>I qB))wqJ8UOo%lUD, j1SBG7vRZb4:|=&LF1$d)ByZ`d HݻgK/!"\<;_5CEYpjx7!3ߓ{x ˮuwWGAy9y&ԯy~a\ܙFdueE{I2'v#^t_'G -CC<:sC%;e{@R*^n@B>~A( `7\H߯e(CU/q`lnҀK{! bx0JǠĤ8[CK[n IMĴ_ $M[ee9#j 6 'lGNk[ɾ$UHY(᭟%jm, K൉P](XatxNoUo2LhXKi ⡥w@20t)fvnfz dd  3qڸ`4\ .$XULD /Y`9p,b?tDH v:bRHtWUjhд\Ɓ"IޤOAKr[8KE]*CIW0+tְoyr0Z3A!BZ2K/_ 8K Sgs>^xMd5ZK M\ {ԑ;qhO!i "]q'J tj5Y Ll*&`MY89h-uX;@RRDY$%j4SOk&򹜩2DAgQ&h h#MGuZ*nXu:G:gUár8ΐuPQQB ߳=W櫚/ᅪUTfJL:|4_$ŀa !hi` Ɋv U-0j"ЭW7`lІX !EU$HT^Ԓ喦WFqT%`| /)!%FdXEUN̪Evk̀%cN I=DÒa^e==i49ScHW 92Մ5 C, ,AU<~.J2:RK8KF,5\ UlUb@S5Y[CahC,p'@żsD%̡&cݷ$Z"IXj%%_\)ZaDίZӈˢxASb0GJΩLD=w@uŷ$ׄkPZ|tl$|'(zO/֩3Џb*k\=cx5(pT#F%Z"T3xT8V 8:@7tT~P\֜|VT3$SezM :HH&@A9^/Փ3FS\s#{ ֒a^4:#XUњvHD5I d)1 IB@f!e# !w*VioBM)6`$s(R8pSc~e1ɌZË 233h!&q UٚV_%HEFX~ 'sH}'L% xL S 4TJdihpt%^96Z;Կ%"DW*0V(`5pΉ@5ܦ^'#-!օo@P6t0*@YUъϯ%)J* Jp\pL+qb%."PG5hP8jC ҒglϜ Bgb)RA:TՔ@kIsZ^#k'j| @R $Vs9 Fi* JBpt"ZExL㨆jk@^T>1>actgRI:gim_Vt(MͰpR |C 2ޱ :MK \oqC*t`PIʡ&t>Gj\O+V 4Y, >ț?Oɠ!Giu>.O0y;W}) &Njғ3!~|rsPpMN'ͅ|IO؀ `"'YbMdsQ1|‘?iOUL<9=?*r U (MO)% Dj* {B1 Џ+ɠ񕫘PQTNl딒2q,T*r qIʸZo9fυ"1 '_SD,qs9T&ē1 j'bP)2@)%|뢑7R6_e+O @Ȑ PG5 U@j (ܔKR39TXg) (FM%>PN8@q%ţXL5X+&x9̊(d-W ʪMkƊ^C6_k 1=./m8Ɛ` L!ann$䆀d1^dȯ^kg/LH=`~n$:;B7H+)$MI%YAЅ(J  cڷ63(T[C@ ҡ[ ]jRK*h _;BAǖvenGXUNo9(H1cy" S7 H@|'Օ?WUEx0R0:5O#kS(115r\N!"0: {TQ,vƾϛ`}3H{Rt>Sf~xNŵ6yvV O5q'VHhdds@lƛɢu"v|w+|wn:E0 %M}(a>f¡öoGL{#9|ӶÖhBtBK5Qa9]uIqK¾:}Rx-B a~XuH/tGmK}.Uq $~PID:W8Xih,4gb[6쵻kbW-]f_l#,a&%ĀQzfAwX jHeYʋckdeY7- C)Ëv&FIxuᑣg؊tpK& ^6N`%:-|%p±-9^N㨖 g!N@Z%$7%h:'$(&.R:"a}+'\S`((+K,Vrz1Imp%F=7}6˺Lɂ{ :24@ S%J'7ޡ!R8 p]#"4@n:ׂh}lU5a A)<< 23,97 )RPɂ?!8hmO_pؓ5I\*ⴍકAՒ`xJA`XW ^ /Z 38ζ 5aD*i"j6\5]{kXY@_ Ps{~' nț1Hr OJ/gni:kE}pW_ɱOp !Y0N1mmpS73-bWk/ćZJ鈮`߻1?o>jĹL{qkD,9y C J$HJǠJSukvz)(** PTRJjʊxZGVyU35K=Q֮GE!U#?IP (q*P1gՒr0+7{ppAKWp5y~[ '>'M8eX@ P!0%^tˏkU'+]HU7ԍJ !Mqq'goI%±^Ej @a.2: _}MtB @ UJ NZj @=' RQ3Hɢ+=`aAǜ{Qk<{^w7rL R7AjwWweW+Q%."r $v# q0WU5aiс '8ꉰH=QPPA ,h9sI%jlE25~C " # 0 C%LFW/PW=Ci'N4jCcGXTphgդʇE7Q  p}' +/5Hj) d O]j;r'w<{ԩ* #W 1Ϋoj{ Q3$Zu>i&:#B)ߪPTM|auR::{pwNx['A! ezPTl9K|_V%iEcMN'>< RYa,pOx~G+D6q/p~nU3(M<*)/=Kzcc[B>%#X{0nFMJ5S8W(pt P '^#6I("4w3p\pTtOpPB-TPN_C,i =3ÛY5<^ V' V{~ |O5uԬ>k׽wiW/K3,`U/?#S,4Q;{@K :et-ho!=XF,C ҟ|]8cp(Tް U@(PB}:{ؤp<G+k@y-Ed{JlP5~iVϗ*DhVlx; C#&vmyOGgk0 # SeHXItn7AJ+p]] ]0Gi^Ui'\,_]gW>wR#$[%YxîJ[8 @cWXj ˒:0 N|V5'P6wRhJLEy[_C5]Z]ȯ&WI(TIϓBɽ-N5xŷm&kVMrasyKDzV-aHսgw$(!s'Ozb4*ʗ>긴|;ﳷ PEQ $e`L(QxvS;Zc1`r%C>*UMJ,u6Ŗj4]<~W)POJO PQW oۿ u$U!QQIR<* IZB~#@J2~?][ }fdǃ]rUeAù^+l鴘7\kC>[GHkJHH灏{r_Z=^ %'0Ҳl :qAu#)~N , %-]'V%ep &ǃss\qiOՄt$ǚ&tZ!NZ1 uLj!N`'_z2Mi@PEy ~SՏ^ZncxKTHv:PI*Ii+ɵaWkmᔪ%RquIդag} f(O NԷ5y_yo#ᅇ //^ a]E5 z0bZH>ѵ%@ŰvM+Pm-YRK2rҖO59Nh '9-P}+ N @~koL+ŭR *Rb/Q5mHЇXHŽT_H(+‘\^&p|-_Ȝ7ygMpb_^Kkg@7HLP 1#>xʣLOgox%@ RcadXH@A%ycJZ-'`8\qMV8 k]?D8&("ե *%@3d~zt~v苫U' D uV7JX$Hxa5Z:w JN8,@-q=m c~/}鵯Vd۟{;BbD@ FK UIJԆ]Y~9&}8o‰k18EZ;p,~ mOx]IZ1o}#y5N=WD #Y횤8 䟺 X %n0(@T_m Ǭ/YIhP' IDAT'ځ'cMmY22 x?pW?oe}lt"k # -O%Æ5ϕ5x֚uQ0Z:V=5-;p$^%l{X Ղ϶hI{NÏ<5 wjży?/x!yti VR֯kn(Y imK( L\5\ &B)Ӣ#ŰNnΖp]3֖ ( )T%܊&vGns$G؈:s-@I* o\BIu+1kwD#^‰AaѧD=()gVg惯A<ӊ  Hm *{s?|r\$ٰk\ \o&ޖ6]y2OhpRqoje\S:p +wO|VM;^W>O?Z/̤ FH m[7%tv_$|EPj k[:͛~prq܉裠)~j+?Oh;r|w?/=?& z* RI 䍽 :AkPvVlM8¸9؄V @obV5s׿+x(n41W-8̮Ywgl\8q_\' UOOka;qoz߾l~&b~ @9vu8۱vaA56ވNw7{͚ 0f.4N] //_ɶW~/_iCI2x&AHT27 Oִj݀伐/9D,4:끒JP29 h6>mW`-6Z N\s7O)ͅpbp~t8?.8MLweN`R@T@2H&vT<%h>w}o_Զ's^mcbn#0#> Àk3{[7~ڒP[{ !36;r.dw ߝHN'}6p؜^aNX18ik' ||pNzbn',@@DM*P--{ك} 96ɶ''o~+Y8IPeRk&")vK_@ޔkznJJ&-m;g=Wb[P1Im $n q3>G 'm߇UOJ'o휟d>[=>umȇ) 9$MU'Qm'{S˟3Ԏk@g 9ߚ]| Ӻu %)qN^q2Ĥ̊AMl "N\+g҆'' ;鬞ڀTf2Y +9vQ|OW>E?f{gMD }RMa@m*$u녒-<ܫN8CP2Zhg9s%{}1Ν7& ÖIp3;i@S@6T@ t$߄֏N7ٟo6;_AB@KPB9:S$* 4h@ɛzrBI8c9oR: [[zRgMp ,'v'3 (C XrK_=fsg^{R m&_+$2n>$s %ӵҰsżOpj( ̇b!>wNЧB=Ijd]S (=H 6,Dp1x*>?O$fW-'Ǝ`6>=ֽ6? fmc H3[?q U*gtv_{CTذ{sf R` H|Snfn(qcck%(qՒֽ=o=S!Dr2% !llo)ʉO{(vzvPA6AJ~@VR7r`B'wo8Cwֱ릠q0eXX!|{$~䆅O]aWϛypN [;p Z 1 MktmwW7>t5KHWD@v $H%֍'o0 %kW0dRs/Tñ ϝ|$82@:XTfZ˟o‹Oל~n}œ?7ǁaKZ#кam9Ugnl݌ET).b"(ח@ɎLR, 0ˁK8iڱkKPR2t<R T=Dg֛j!#?BDFఀdmѶqU$%Z7}U( .Ľ8ڱs{k[⮭QQ )TƜ ˡX.k " #֮ kS`$߳m*} $XR0?tN#-ٵc̹_WC='6HK)JaeR0V!c `2gGE=QIfn.Ds> LNk-'vb>ICPOch(P)*jŶCZM@(#CPPI\D5OpPP[8&殟f; ?cW8Q;}Z=x"-TTi1&ЅiQh~@k[n|>J6w[8p}RgX'O0{[UkG;X@:q:B ێ \Zm `HJ2ٹu;JkPL Bg Nf>Z;8e`E* A h+! 2A#d9 u0]?H(*SI\OHCPP߲P:R5@S;]F"[*yC\1V"P#Jb 4#v*IPV֍f( L7K8YxzPO*J0V@=@95R Ƃj H,wk mfRI|>{ϓؾ%{3(}8vb>%Kw T̡+nxZʘS4΁K-tc HgˁoDN@em_J2]r:Ca}ϩP -2P7 ;+Ơ@?ķe{Ǎ_(6*!2X.hBEiT60}XH|c `YJ),0K8JɆXok|3dAPzʤ#;Z2x?6_D~ J{o ]TIDPzļ/oiPa‰+Ϸ6'~϶[wߥV)w@dKL6h,@ l 9ejx0ul\ԀDd DnƎ! Nf*$Z;xr'րh( K)V^X!!LMIkTkZ$J'Pyo=u/&ؠ-,=w85 )?ݓ7F8 <1-Kysq#j!$)'D+8p uQIr)%8 %z8;+ T(PךLMM1V +aF/,:!yWXF~~ča[ ̑,⪷nJ8|8i= :OhTOB(9q@#2RcKȴ&.B .dŬhJ2uDcLH<%m|A%qck3@@ƍy[ϴ/_[;zwȀ"*UQ-@R.j>Z,wM 6 ͥ #P@_ $rf&4&0_ @[zr$S'wQQ=@TTʲaõBD%Ekun qj9jr}[7R\(9߲Nx&fNՓqM@5EW )TZ ^Jim5 *K $n#pc/~ HRֺ97>(K11| ?d s'PڦPjzC+:D4pbEB<;@)^k |PI|~кi%;`sgpQ '_λuNf̝v|Z܁N흌R:ijʸNNAJ8XkFê[= Zˆ϶H|k֠|%0 bMpŦz2i({+h3h@!TmؚϺF]+K%F',0N5Kv\Iow@b@VA d}C;u|1knLj2ٰ3`4:Z;OzbHRO* I` %| Z0X-H1b4NTTO`N@ R|I\EhIʱ5=cͰۜ4A$Ƕ%`DZ{(@84I-&2>Ma[bdXs /k2i* A$GpY9 @5Ϙ&xk$>f* [7P?c3 ws'zk롞G( A ZVļ>UdM09y'˅"EudV ~*ͤ@<;Lj kVOd'R@4Nh徴2@+Im̻YkH f3ud$u$/䧭h519N}~fec'36{S'PaY 7ǖ!ٞ\+֎옳kWkV]Frµ:&>1bHHWH 9o7Nf_}?`+ǧSo_][ǥM-||@BR0Fk ԑݬ DbBT97FJRUJZ7 6J VI8H36CYI 4R :8b؀ 5Hq?!5ET:'"qh,g}נ c 0Ց ?@\ـ+u_L(1pbp6tc'flo;!Lj]P=1Ȁ"mGN>C@ɉaŬAJ)9qSj -`R dr?mk̡ _wu0rH,Fʺ.mR PIF?)S` A 0|ؐ-P 2[s.`c.Z;0n8>5z(0kfkzJZ3) b'q#?rb=Rg[ /Hl/ ~ H2f7čSҶ LJҲu3ͬ7@b}q'g$o+!s'ay$׫#+ (y`6 %ۉy IčZ5lxjCDAYRthی~R,ėGu35"p2y(ܣ}J;[5mŬ׆P+U*kRSOjJU$Z \ Fb5ˆ71IL%)/sg.Z;b!mhb'sv->5J(k@qθ^R Niǔ4Ρk-,- "7?Oč.rS9ls'-bMG!mΟ~G (@LEI5/VSFTobUˁ1nXUBxK#:Z]m1@'ITI2N[K$$@!S܉iځ>ICyBrB3ؐVS5ԕ +R^!1MhPNhY2xooՀ?jn#F u7@Rs$!dYٺdK ;s'C = Nc@1b2Zg#AD5@%l& -i-8l+(źd qU`dHRZ6?GbxT(v'vwROxG{M991kx 5?2+)~ ^ ܴ-k8Z<&- "vVM0^vM(FW iնZ$mǶO6jOT(zI(WO#;1Th)?l T /?{ Ϻ@DUd_1՞ cF<8kW'B%pFz`/VN >'Ci1  mZƜZӶȠS+xP|@}V E*OH 2Y `doM@"^OXJغqOO PZN`,'P6;~`2\II<AJ fN܉#ST<\íK+L6b+ *"D| -9Rٮ#EU1fbƽ) t1 o@`SOl8֭Q22_ Ô4@qJ/RBmPRQ%01ubqsbO]Pd**%)GŲ9Q5Aʓ3^Zk$nC_m1 RIl֍x*Gl$"Z>5% wf3 zǭwR (&f(R@ns#J4oiM\sZAH,D _!1ǍUH\s$Цm3UI [7g̝NNzn5SO`}6 yLb%z|}q; Pb(9iZed i4>BR9U[4VܚY 7hx6Z@%MfLlf(;DZ;-{ tjP~fR|5H&)B m Kr- O㬾!$;*bέK Fd]mJb9"IDATm8) 'L{G1RP\E1q!E(JMn2Ҡ4k9%bUB3PC)DtXT@Jm($c,"*},>5ej>`,Nc 'w]{+ yRd1P MEH5GJ!-Ɯ %|xs̅zr\ߥ`591$IíTum\j?`mp6wʝ;֎S@Y{'@~{.O<΀X/oO@bD~vc,:) JIr?n%Ֆ>vGF<9%jQCsH,nő=? TI'Fs'Ϙ,}L2>'ñ&^oD :4T132jY HkjH51b&K<5!x f[7!Y !I5%` (nL\Iwd\>5'&FMkcdNcM^PLѹPP@=-I숚4 VXjꩵVO֊^JZ*AÈ#4;k'j&"jm5Gem諒5Ls'S;EB-=Zw`@q8!eH;ɑ)/jWVm0k#) :)G${jrԑ1/k@ˑ$m*)vL4Xm9Ev6O,'j3բ%6(>>VŠ*0̀$]3v 6`?3s[;@z¼P],VA!R_8zsl ?ҧntVq::T(sC FǪ[ @߶oMJbq2l!86'wrVOh$͟xckz2P91Hk ꉉ[^EGZRg Z{~bseWRƶɻ0l;9Z1[HN1^:2˗õS6. hm%*}:caY c'L=Yi{ e!]EV/J|;I!MN: BSe匁 5SG$иm$3Z;[i0ڨ'3(PgT`s|ڠbJ?*"-P|oK29-rX1>稀Dmc_QINkgZ;z1P{T-;+?3W|&M|k31aXzv8*tN?DMa9f:2ϗ57OY@$-XL%l΅Y >kKPOR+yP~{@EV]/ )\HvbP+R`);P8fܝ:{C!)#Khlod|;\%'z2ƈݰm\eTbBYځUXG)Hu+uRT] mZ;D ifqs`S5\]ħTmFDTDpn^1AK;ñNM'|fmo-j)B\SkP4-O4̊ThZʍhw '4X ȯ3D*25I7v/ Ijٴ#67S%k}7SOddȟLG:u(bl{]1s(EPQR|q!>2ܒs?2ڰ"֥-r7LC8дNFUu|ojHIƮǍ'YFH|kZIJMmD=cHe{g`lw]fg*~gH?׍OSUܺ&~iee:Xꆤj!?cIYzI^kkPO c\AΦbz{U_Ζ)Yc.p2hb;blq$ x P@!eƆ'n!dYǀbr+"?%2p.5c15j v蚕ΏV$NmLҝ 4leYTEECJp Nr[M )ǃK[3` D."l-#zxL5f2̐5u/iیO;Cl`'бa WQ _TЃ:Aʼn/] :z%hQ: <]KtNWEd$B9N$E=Scl8vW(chScP^W6(<ˁ̤@Q'_}bFbZ a# 7ZhƦf&y#SEfyI4aګ#nɛ~ ]\_-6 &hwLR{Ǭ)yzkjB@s(C -#@ xg07P-ud*VVbLAkfM1 `ö čVF̢#f*跃FfS>ćIENDB`glogg-1.1.4/images/olddata_icon.png0000644000175000017500000000026413076461374016214 0ustar nickonickoPNG  IHDR GbKGD pHYs  tIME NAIDAT(α Q_%2YїE+$T;j؟ ._޸Q9-KL(IENDB`glogg-1.1.4/images/open14.png0000644000175000017500000000053313076461374014701 0ustar nickonickoPNG  IHDRH-bKGD pHYs f f =tIME(%6IDAT(=NBA=  ,HИh(ق ES(&c ̱ $Hh57Gf̅Hp QaxVΟňգ⋍]ZI>ߓ ɼ#f:]Bpc#}}H`\3(TcS]:,3z)s+ yp$Hێܯru2u|xo&H]~F綊gIIENDB`glogg-1.1.4/images/glogg.icns0000644000175000017500000176102213076461374015052 0ustar nickonickoicnsic08PNG  IHDR\rf iCCPICC Profile8U]hU>sg#$Sl4t? % V46nI6"dΘ83OEP|1Ŀ (>/ % (>P苦;3ie|{g蹪X-2s=+WQ+]L6O w[C{_F qb Uvz?Zb1@/zcs>~if,ӈUSjF 1_Mjbuݠpamhmçϙ>a\+5%QKFkm}ۖ?ޚD\!~6,-7SثŜvķ5Z;[rmS5{yDyH}r9|-ăFAJjI.[/]mK 7KRDrYQO-Q||6 (0 MXd(@h2_f<:”_δ*d>e\c?~,7?& ك^2Iq2"y@g|UP`o@IDATxu[3@d(i( s>}KG\lY?{mlkײ,ʶd$")0ȉyyշ鿪nݺP@QE %P@QE %P@QWP 4՛x`'K`S=嗻Z@ӆ pw55_19\;5?sωYX( w߹c!CEGǏw^3))PU{]_@T> :FW2^7gμ +^~egϮ(Ms~'hɗN\Q03[U\pqq^7=<$ÇǤG##%bhK K3)v[`a gǜ9L%A M/^%u0a⚖lք跿oKfei @ŋyT>d$ \J@uiA~5l2(HR P@QCQϢH  `TT̢ 0Z,J`@~w1Urhl,pZsyUد]}4kԩ+y܈)vxuꫯ8Yn@V cd…~x)2+ܹSrsy p?Z^(đ##`1iL[פX%ɧc1./|%yk~w`HXlP l۶_۹kg! *˔ 7ƍ??6y,@j}_ɇ/`qāiDߥ.@Zo.<N+f]h?F2@JC`kor--_TJ+ k,|&,B4ʊ .r'BTa/J@Kഴ;w8t R[lq|Lt!Ꮏ(e7"W%P,Z+\@J4L yWN <3\(atww SlE4W`4" E P  ` (P P(*BP%0JP4%0B%pY]vDN9zȝ:}r(e%4W (J`@1{uV(@V^  `YVhXQ{%P(WgE@ ZBT$qlX~Jv_bxշƒ[m]V ੧=j9׿۽{sEp'ױ(eF{Lqh*V~Da%Pg u'J/ȋjXIfQ5$dPIQ[ J20mi¬.W P_[1jn>q~z|Ҥr8t~;ї? ;s=zĝ9s6ԶQQϓ(>y D/(X~eZ[[6(\>pQw^gnwtJkc`P KyiNܕ"e~'~b8Icbw=z;v[Z?2:~x7eڄ l#ݮjC~ {/^VXf̘^O-eXܴa R]xdx[oI"5k 7{l}&OZZZTƴ@Lrzs{P%;wNGݑ#GdqZ?ݲeJM^ر)0RrǎW^9}A?7o[pkoow(z{V0e E|rU :tueJ!>xMnwWJ'~,+xlV7bsΝwN* !]`RӥKS_)% P6(š5kTlv嘢dSO=^uw2Ҍwcl O$dQg6C+Wꐚ~: uzw?onڴIӟ}ԩǟpsqݮa+OcM':;;E8׹s=yz[= z6d湙3a`cas LS8n&w5׸'OjokacǎG[d{tD&{c I$h/˺ehzjj*rײ@<a@90ౕBEs[nŽkkd54B#mO*'IxXb 5M캻zR\kV}gav١wTa1"%X ꫺*${」b4w+ӑ]V G>,[r-nRkz(`1|t̐[-{B@pܘiej72@A`ܟs=x︽{6:G7\Dp'IxKXh^V ` ~nӅ1Vؙ'BSn/>/Přף P'5PxT8Mv l]b(n^`_?txK3[KLCsNPkܵElE{ѝ%:a4qؒ4N-Td=ܣ[?Uy_}5wjf)E+|^d:3 SZi 0Ȳw s̻#:zݾc]wwwç䈯= ƉMfMjuW͙$f Dq*ű&Ԅ̙3ghP li/͛7;p# 9}AQ&3D 4Vҽ~uUPP)BIU gH ">d6"!@ k#[s>^u=z>dWʎ~}tnˉ8)-+fxvd[b4Qb0; /2<<}}La1_>l$4V_K:㯟O-q5r72˄¿԰[oUCf4iؼ{#ŷO-I#qTXx{e|sbk0{buʃ~Z[7Mu?#eF[/x,GSЖ>>R3BQYǻu!,e3~U}=1kG]y$Cc`Ӿ~{-".A 0%`ŧ{ zƹoM.#3n%(W.ŕ l~󩔪+@%~[Cɼp㥝u^hz/o\LMfy0ֲZ' 4Bܙt]p'q[NVh#ydawY͟YHޠu쌎x_L,_>+9X ^= #K*' PCۜ?;CW:z։^UdzIQ8s@eSP>%4ǧe5̘~_Wiȕz~zAߔE61 _=ۘ%gWuKWسn䮛5Ac2MxDve{.}r{a +X>kJ5Sr;rp=:1GrPtf[>y "CR,)?:‹0<|v_M{;ܱ2x Gn4MrQȄ:77C. >1z\ǹ.)~V9mn 6votCv~}%%`2dxTٲE= |H\G5F㯍Ob>B)gyF_t1o[lgOUt}{K`)nMK٥ۇFl93{Սn뿑MLqV>@ v:3]Kbdұas7wvp[D=:+Fo W?*![@V @7lؠK앗]%ozyuV./fjKS6|]FbkI菊p^z8^B`wvO^B5҃_jdYosE X308o([sLvϔ9g݉s"$uksO˝>-~$ӁwNvj7wznK^̵k庱UI$uG)>y5,@N8EwKߎ`K4r|C[wJj/wS' 'im@kvM+ oA1qx "^ԉn攉K/8xBN)Ga>ϗ'~G.,';M]DY@ ېptWxp"RW%w$!KW)L e× 26񄌖|{ߋ3Ͽ;EVC`܂m.~w߼}ks6wyer=X0:=ȴ 7cB(:;B >Bz„&4[YA7 ?5rAҳfYuЏxB@>3)(Ompt  ٳ'pʏcwEx 譸R;_Y^I۟~kͫ^9uswYn*DB^A{eIOZbϏc7pJc4Z0Uz2/=?` Lu?nn{zc˿m,Pfe;keȟUh]K _mk|K>fh_1ب,@Tcπ*n襁fz3s_Ml>}7e m1-C}z}&Ę"<ՌkNy.8swI9ݿƭZ~E/}c[`$;eNޙK?νET* JLx'a, #<‰p/?y/*Μzsx^nYb"yKgBXaȹrQkc#4Usb<`! Ky~@@ys 1#f2Q>i[Y*[ڕ)R4< 59f?NbOh4SGsk̗}\7~{SDnGP iP U,H2Fes2wXT!8Qz"1i 7GC8v&b:-8'?L%0Ajgdc x4@dŸgC:!̙*r'L_KW~yʏ lr>+b3qCG>P9YZVY$\h~8TznYa+/)RYMf~maZhvPh8gw!Fm‰s>aAX3d?GND#-K?z{|w.˝r *v{o=adBi3j 7-`z|[Vp'NwrȀ{yhنRik*7 otvvdF*$Ȯ[dIQ6D @Q( z@-!/MJ/Ž˜mvȡAGiCh 7?OBwNO g|͜9wZ6Ru[KmD@P.&ǙӯX)oSV7|)E=s,c|2YXS%x.%*70ЋgOLE,|Î|Bza3--̟}Q͞-(|į@fw*E<`eoKݩ &:;/˗/OC~/# h11@+VE)cSK!9gN5gNKٍK䃟"1i;yh xw)&N_Vy!醕{?U*\).6}#%1pC2#bP|P%F y׮MkV.956!!ig8^ܟ%3e,\2_tRǏE)Y^ qfƤzIߣz" @!nx1'*xKciByΕ/ MJ*C>Mn $$Lkk[׊tf>??K^ !s⏹' ;48sSr_dܽ.J/iSz$$aQ{jaW͞ ' ƅ_}z?^)DgBBk„o-Ǚ N%ݐ^!d?맫??$CyP&H Es=s9ܾdM+rUy5Xqd$ 8ᧂ&"HHp{DXK\PFQ GkbFN {bdGOX ])Wmڟ<ũ{beuB!Z-iNe aTDe|Cte>!H<}4&u0_y*fx7> 7_wZ^¦ Ev/o^--XU"c8<0pK)w h'qx/t3/)v(,X ?oۼW^ 6̧~Ӡ X^F LY .R0ÇJK!7r 5|DL )ܟT.䒏LmY}Nc pF?FW͢q/?63FQ?F%G 'J+ x1 yo&ȅb2;5\1Zȃ Tq&gd+F0f4ٳg뫨4emzkזW/J^tXnUӸwK~)2bP0 !{6'h"~p|,0EW׎&H?H_B=e0j z۳go )O:)mA1grܥ| rQWصkW\ R@}&ur|͈Z66FqqH-xl~BUT> 4ZrJGFҤrے?eG Ä͚MAh#tIX83)Rʫg+F~hLa#:TU!X0"92#}cH jxPȄE 0aXȗ4EqC*8T•5(U*H:=%'-YuU΀uucr7| i>g؍P3g^i`(;ohoܞ>{|3 mYcwDmj"Q_YX4|_a*0` C ɺzb%r+$.\ ;x^I ,;E.MIF_;zݖ7.9cJꋩ[(SND|f<4'S{,ᾬ!|ٿ?9uƑ_&ZZ#Lx,+ u O I;cR,*'&$ jWJE> $=mRlJ{mA- 4DGg~"$XD&Xʏ/ V,NO6nO@͎LX֌ :-|/$ m>×a; Ѓ:-*7i/Fd;ڀg o=^>Q[ ir=v\cE iT˾OOBj壤wMWkz < ۑQMEpSxSbo #r_L|rV.jwoH>ez!;1e5a.T(.M @H٪gBϧ jg7tFaNÃ=փ@xWn'0CF4D͈b4X{g2&r,QQ^~}HMB~T1* M|z`hX 4h2}*w)wp+;IdjO$z k_[,#Mu#NR LL`(YN+ѰgɥY.6 LM<\䉩Bo?1}8<\&z(W@q#Ќ0b<N#zwtus:@fPfOK7,PoLLPgJ<Glse4V49i@{>PrX3H[ 2LA)FUF{Xa/BoPeg,QfHK7ڌn(ye"R,^L~*Ӓ*޻"Z !!fms1_O$o3~NvNٔ ^M 蕂b{3:I=⾠D^iwk GM_<ĭS/4U&3yEQÖgS ,z3=)Sk兕c=ˡe>xqC.HHmi6PS?GJ L%'a!is,.}mY ~%LA B 1\^R̟xP1'%>q#:/ʢ(ݘMB!RDUfJ3f' .|hPG+ `;yQсIq(=BGڑRC 0PS#k86rlЩ9_mmpJ&ǐW@| EKD^Dž(; D#7Ǘ<~Pt?ai\ hO+}Qw4i}@ N+yDPSB-sBP"h( Oeֻ2͇Z=J(rB`:ǣ4y#f! ' 4l_Q푑@D@"\BAzokO/H=Miyfvޮg&$?O(X|肼+>5KDb!B ~VơM>x"-Ap4'1eI(P;L[j&KC}-l§ZŽY@*< 1-/%`O6Όs_!y/*|p6$K q"PWLm=,hUqCB۽ˉHٔG}UN5 $Z-DqJ|&'M M1UͩC~6\yS'=|0S-M50ؓү=rJvF*k4Y7(U~G$ m,> EOz0_B0xmA(DbtJڟRQ&~#j\"oDOƉ[8˱_VI*d ! _JIڦ`VL L27pf^J,K$PnmXWiTCߓ 5 ܼk@ <{?u7Ӟ+$;CU`ifiKڊC-F-J|x{R`zz N(IIjf5|.!2CN dr#z*3ɐd&ӣ'-ہ!Xxv`M}6IeZ e}yX>)r4eu(çA6jE*#/-#p($&Y 0`WOɓJE~ά6NtgΞ*[$JJ)%rD!xT( ofByQ aҩz{<^huFwwJ1&@:O+#C#]P\.h2gт#pTL6цql*. G裴.59|ŷKv1`Wni8Yâ0UiT>$~@x,taZ27ymib-(Qg-ffG]RU ŐUD,aEvz!)Y= #'^xM&*kDA?Y6 8pv@c(4kġ |TMs*'RM+1-b??71S /;v<0koFcN4AIJN",y FH$U9@?qAv0^y]8AP%1l3'?j"^4r/K 3,h, i$j??;-/\-"Wo|P狲AYKKƟrouwUk;ğu{o{ݒ%b2@Chp1jYQ#Jj5&E2ATh 'y@W%AO0ަXPʵOpa5{B6L o % {VhH[D*0+!푗rΞ;t%dHXu?Eeb_88C)hJx3)rG 0lj $Zʬ}t&UoG?)̼ԆYZ. xt-NZi]+<@[cYDHpDĝ =>ƈ["nsEIql5(F +3W\o!/?%<)+~n af09cYgp፭cMc5`%#7&\xہCЎ_kfח^sp츑u*“4y0"C0Tg.$*|D4#)ΣFnfXO9z?iB_0R^IiCz}/M\P!.+.1`$WQ*,CAV!}< Dո>d2_xaGOcB '&|o4ܵח0Jq U0Yؓ_- ͖}꼗J0>*!!^Ϯ>ߌͧ|M26HRR)r㏧6(*A3*Ҍ-ڳ1N;{Mhr^UtQ ʝJFGMB)ɫ/x4CWX_&(m~nC%a}8%mB-cǛ:'ӎfN)t&@G|7IWskZYnЧmm5!MJpNl{} 'H^3_*q9B.ol ^2-)4)T8HR ^BٯZc. /=yo=|P>X"}i-:>M8s@@^#Ow$/]&,eC$S<`v e=8| o\CjP#I J Hɣpd4ڍ`lj|owt̟]}5jq>?C^QQ{`"`YUsbpF)_v3!U4DDR-O(#lN & Ǝo㥘(~%P2sK ԍ)ܤ0e'iˇHjم>:Q Vn_)r*'B-<0`@3_zwcZ0IAoFSۤ(ŏ!7܈kϘ*t&(("e,?p<1 b% HuA-۹uvu'τ2 {`/u+2 q*?+Y_15m|a)=(6UoT30}Т2Gg>DšT4M@Iu+)hd.]&ەo- *@>>Щp@h@z"$䜠Q tJTŁ*[qkxcd4XBR bALL:v{'sc4F rbn ŋڈ< B+utE%d9$KGVdwyclDx @Qb43pu+4ɟAbf;ݙ>ia"JzS8Qa8ڠ!3fFQ#I]9{zU[:iVKd*oh8<([$bK 'au{t"Ms*B$!a)A2Py1$p0Q8&?J<١9}{svkŐ 0`jZ#QOT'S'e@i LdkiT6:2!_<j0MK: ʿn O? Rc]mqu)̗z 5yٚSz$CD"A뎲#k$b7 KBb"ɇdA\1N$Ϝut[/"̄^F ؈GA0nu dB(Ho*JЄ#&(>S ꇿ$w[>jI67=s) '?#2!zMR͏3KR0ԉ+2n E\2>v8 8qmOQ21pLBxXgLﯮ!" wtLAS { x0ydD8ExS،"=شmپ۝5 e|JXGR4-&I/>Uį,#I2A.@IDATx=EPҩ*"2@fnzr )rXǀUєŋЈh<֓dۀoZno= A,*.qbѓ]zY9ki‹h V^*ו^97)ynKt^ %Mԧ+΄UG$P OO$hW-B~"8xgӽWi ,oP^j1fPzjPe[GIYٳ.~U:f(m&u״Lȉwsgʖ_Ш# ɏfR. \Q-yz| G1> x^>=5 Jyخ. )3;.8 j :;$xN8A/p䁩o)D ym`oS3݄7tvkTwL s?o<#`W ì@Wr[,ITMXjUZO(˧[twQ={4W ƾ!@X&qHzśA9f)<yũ "KIZ*4I84^Ka><\/,A Y.at>M>5A MFimՌP1:wc&ub-!/xM05.o9 w(lU&Pvps+Q"y//dT/Z8DaVS ^\@"B#Ð16C~ ?Bʄ<&8p` ?BHϿ@ZUɓ'SZ`Aj7fRj 6k3z/g.ǭF2]m 5| hxiӦx8/F*%áW_}6dԩi4nԟ?`0S}yB?߯b[oYᤙ 7-o3fO2 [ͽ:-9t=6I?5V'(zbAHm>xFމܟvk ^(~]c?8)41ģPOW(X]z{'Olſlp m$&Qj\KCքi=<SӠVl:ߗЇR$Gӈb"?^>S/_>ChJ}q;+5ܷ݊Ԝj2WͻuoߞDZj*#Jn Q0L4?Urk %euO$) R+B[G@ 0E{W:3exwտ~R[_۬AZ\tsk!4^ y*V9Ua0^R^q0}! {f4>.!]_GFQEd^L!Arb6zF4{wD?u|=}S/yw^6j=zDp+gN] m䦛nR6y jA @*> ^1'0䧤r2?eB9sce`1ʜ>}6646z'F7,{<ثG mݬ)ҐU"Q`,5J0\_!"?%}ۻw7B.EBEcnȣxn%ba#~qB:);.oF9lfusy/{Μ9f,l93Fؑs'j4PNvk_2B%iDze˴(^Sެ:K@( 7?,ݓIiKAK@r2ÒE6PY ,y!"?x$g8#gmBR"R Fe߳JЬ1p4ux/>|=r7@aPM+=E)jdΫqxW"D'n~凿O/|#wkx9`aatoir'4ܵ]03"z6of1U."0^1B,qp&4C{a0 >̝O!'^Y9#tTT<:ݑiN+W=s= - Z1mUW^6[!S4ݸZ;6?<ŦKA #h"B.1HS03r ^bc̪tz ݝ>ReїmQ٭?\ nyaCaT2C~> 7Q1s'd|(8s(9/"є(-Ν+ ,<+/ }J洷f.8r}oByFB"0"-Zc =!&'"*Q!l.+4F1>Jyzq[zRu*Ņ_oDiNB!(rjq4aZO.mBV1fE@ɔ̢ṙEJJROR9h'PUޙ75|Z/ $+__Jw˖-WB9(hѢi, e ~j_y;{yWbɗ/BMVt>\",khRO1;`QvBBuh9ۋ~h4D/b)+6_RQעY#t(by_oܷpӧSc=[7nn(Ku-DB?XTC+(TlE}}%kBU{ [*c;|;rI GWUaQՄZN[e-gg@OBz݃ Cd[Y:׍<.;| @=rKߙ5evl,XH 7쉺T@uMw(1 =Ŏ B%Ƃ Ӄp>:wF Q-zۏށmZPzflQɲhYX-S8dWH5@ |qz2ӯs@&zێFRܷs]z'11}|4֭[S[n²gȺpYjJ<,l56) .$ʌpM?¬ wJ]$׋7Is)Gp3h#0|'Tij" zX'׸6E(|Dן<C8^Ge:\B O_~79;oܷ-[gǂbJ)o8}O(cQE&QT5 &4Ǭ|4H/$+aOvNn;N<凞L ?~,fo]Fm*ݸqcJHXlXhML3yaX@6aI'rBM _fb/" |mqe@- e'M}$gc2Qؘ `: p7?ǭnaRO?mI ܂STTEi$[тDxF(BQ\ yQ^׽-9lY??ʭ뽳2e4"֝/ϼ#~{ք>fTr%&źd~n$Sx7JJc8{{)̳zjw['̲a\( QР C=mu[/S_?ƽeTTF}F$Zy!v!zG@z.^^vx5Wߗ~ᆚ,u3#2 t,m5wjay<ȋrթ7U&ڕd <#0rmFEKpdM M(ƉgEI*%?OTX_wu5=jz-FYBSّ@O?:. 9S"7.t,p7((XBȺp{ABh=B‚1`FE>ܱSg]=SC,p^yrCE p7ZʒmX4/|4ZduS~ G:ҽF{]5׸(Ġ|oT e[G^! `<%= Uַ__jCBiP7p ;ׁ{Oo;ΡsB͢ < 1=5rц?z"918;ݞ#ɔ~}M[+%k11dҸW{y} jMGΖF~_4E +&V>ͻb:$7vD!Զ߻ݛ惊2d!.5xpwygY3^? q:O)&3v w57 [nJAeHe+ck__"#!i4̥|k_slh 4N.EP9K@>#ޝ=m֠N fQ ewB,7/׆s{}nvw5܊EBJRڦb7 DBCOX:JCCX?ӭC|F#ri9mL_QrzFDB8;7˗Lf0`XݚY;?"㕀_{UeP).@_߸;ï|+=5Qw}xcZZZZ&^6Tiz8 ?xLF1yϞ'hя~֯_;f(Ҋ@t%+l`wOu廃*cܼ>S&p+TM8 #bmKjO/}rx3nmJ.d$i߿]y@gBٳ'wz[K~&j6HOz~CGݾk|g .Dzl{ 1;KGǿvx[^)w~Ze>c6LHr}Gt=X}9x`Q>O>ddE;zj`=?O,=Tء[n%ۣ\xqd9[EY`^ OF;YJsn]l0^>%JIJOw09>$O/}+ ,셫h9ff{\ jY+m5:?-ޏOsnvonzD'rdH6dF*p}Ç,* {=5mes6_җu5NQ&,q$؄?+Y< SBJ X7`2`qTQ'o4 KsZ cλ @Y,al[d!4ˏcgʨ.O(PVfL”3^|ޝ>sZ?' ~ $n>:~|N&cյnDUMۚE׼bopOvO;w,J|P5Tاza"8|e#҆8 u85…c( .#`2=JjdlAq$@õ!& &&d33` [ zF @V"0Cޟgg/t;dzݧN~\.=oDw|3wrQ9@zF9(, ~DYr r& s!F+B̯d,3r5 g=oҤN)")cUD/^!.Bʇ#FKO3D56%*Џp+#LVлFp7oC͍i('7 ̷zssr=gt .{虞\z-8S C%svtCY3g/ c\N W/-gQDQ0bHXS2Z@HBx6m@03gPMM &< Cjc9VBbSТ Ӈrkfc4}4K=߃)~{ y+a=lY̛s`xEʦrcx /~3B9p r%|%? &OXEFYh'ӕ:&:@['K\ϖ.K+PDkzihRb$dJEq7~zd5kuLV&#؄5*O|vOoo~SzIѦ4pp3“B:+ya?26Bꃞb4 9 ۗjY*xeqy )4wܾ޵iHJ:Q`.O{n _p[xmU(;/ (pŸ8z2R(ٚ)ϪRR;)w#g SgM/_ ;iW*a5~𛐛;iJ?/My+;4x{^Dat#$<y@0y=nJ8ʆa8BO´ K:lZaBYꈯ/YBcq^ ~H9_LSTL0yLC{#_~VאQ8yիT=!7|cr8S!4pz*&AVph2Y #4CinM &AbE(B_1 .6NK<[:_5R%B3²CZfrtہd{q叝YDdd0qi˕0\~#d{ٓRxx*.jmi?DçXx(yn&PVVRY%Cϐm@ 24 (PՀJZz/kL[BDvhˆ 4V>j»/ ة|=zrBox<)B:Ez\ OF }{»x?zpLcxjã@~P<}J(r_8Vh儖Ëx.ȱj@Xul8G*=~S2wgKKkbߺ;:ŗ ̓U--%888 ń{6@hU2KE?0La aW>??8>,<ʋB #Q&ai()r='M|giI ;J Fo4P.c_k}n3f,T#gya_| x^oiE?Dȩ8~zYΛ#ԡvzSY7O=(Bj7zֈ,E~q ?\Q ʀ)S;h<0 @^QvL;&`&a^͔b>4w&k%%oM4ƄLšRru@p7@Vpy编U+Ai{,X#Y(6R5"{Fp%lL(T48<&Y!ϺM!P`v 1[#07 ea6џ'}tGu/bوl2=\nw4!"܌a oF׭[ L8Lj"Qf{VЫ)n]dbj5w?OTg䜣Bd/Up]mM;~z? 7\&YϺ {n:Y`‡F GاNuMCOO駟"ӠP``XTcyv6R Ml,Fg'F[ίV<|j,nߔA9%ف#G'|~n4NHϐZ ?! ?~ KP =8˜I5 b?v>\I/ g(>Ge^2qd 䓑mnB < -ZŤ @it cafWs26O9E`n^4^fB]zS:{7$ehj.3?P@׮]spnBgny=J2aid5L~ ?~f;WG7-2 e޾Z(Iv%PTI>#i@Q!ؘnr@Y'== О}}CC7tv츉afg8wx w&i7L„eCf97;tyٰid'/_'Äs+h) V*,-m$6 >CV<<7 9wVYCQ2>l,֛D !D^gCuY|ۯBN# 5  y8l/ꤜ`^nBk&qPu7%.ְ\"?CB?̏^7Vzgy3v3<;M&H7nnN> o7avs@ g̘[iluuui#[osz!6W|}f<ҷJ%znH4X᧽ֽ=!;oZ3ҖY ?iH3b#6oHrpe ?|m(YJ2?HM1Q0C V9^~ɉƿaОGHaǣ‡ %L 0iY@kk>,('ʅ)o/A&?`Bc`OPVCUqP!QiML4~M%Zj1!h!ٵؘ֬"1@$k "Emh5/‡ts9sνܳmޝ9s33<3һYhuɁf48xk /XR j[?=i':Ӏ `x*жF6Wܛωc =FiK}9X,;}9 n>$CjY\/3$\@I|6 q.p7/Nǽjt{/ܺgSQ?$Px[@~hرw8%c%H1݀7|N7 TU}_F v{ỽy:Gc "O:%wH;Zmv^4||Q L7C83@ׄqQ!>^?(K˭pnVC̀YկݻV tÀ v;ٶ^wL] &(T? %gIП,h/ ͧYY@H>J?N!p9/V@Zi>8ҺF *c烟[ Q{( tճϦ }s>0:ne? UIU[픯Zh pY^x.x9Fw|z&G-i_mxbv "b:X*{~|>?7b7!AE#dcW@p/I7q, uEO^>9  w{|X04M.+62皍džFYbks*bJrD/:=??&`|_?y?ʯ<ʷ^𷺻iiܰŕ 1,ܯ_i ;M3`~p`s2Лp )0BN*׎mi}.o1*sĀޝ]!@K,P.|Wp.(lxgǸ^uѸrw&;:^'&r.26&1zr~vx0pA`>>a'zh g;г]#Ǐ~殮O|i<9HEq~kҠoyi[~:זf2qo^/][OgHقL#vX0NAN+64׎msO _ʻKlI,@bŊDgYO4k M0o6cǪ;ǎhTtzYUiLt4}gxx?ӵY\ȧ97eh-YY\/>K[CN~tU^~ll4,j[ā~1> ?W\X==mYsa~P\BŨ?e! nmybb@Jtsu|Oti5ny7|?OE4{GpsmxkG)W6i 64~ޱ^d~_`lac͛77(&68&}b|S/lٲdU*Oa}vڕzgR}Wr.Hy&{֥"kXm|n|Y_ƽO#򸠵x?.C|qoյ(*n.ȿGZi%;ř*|ppWo]ǨkKgs}*vH~ã#[%nN,_ڗHﶣ~`GB1{?NŝlqSBz}m7ɳ|򤌁>K}+?K9TO5074g:-QϦhpܝʇ:OFUj:>lj'ojժDF7AÌL I.``^lY>=?e8q iD+__ H;h4ӃT=zy;Y{✽w 1ż 9B|>~%!{uڴ1pGXMwQM~#<T)H,˔GYf607pͥ>vfTi/5vi(:z~ 1D@~ml|t L3^~e#6 r#"m6N֭#w?g͕E`Q!9}$Wk.Yr\ 7O/=ķ'ޱ8PI'_`p s?`9{lŘs~_|'%.`wĂg-m#ȁJHx\f BcV78l(C05#CX`OEa=cȁS= ?Ou:@ګc{0'Y`K_&!qc=/"",63/w 8z.q?1 Ũȁ=4'?js|]@p¬&@A4Л 1"b;SO=O3ȁȁ=4Td ݴ0D |BGǦ" }Bݗ(1-r ρYxiM9OuXա~gM} Eϩ:-HQ%"a~k]w ]orBacK!|hb=?l#}Z/ٱc@ZJ@W3_xg^ ѻq>|! vޝ;a#DV] ^@$<9_G֣=B| :|v^܃25C88PIxU>dyᇓHH##|~} y晠9+s]GxÑgw!/M˿ s=:`>B"<0aUA܈bѶi;ﷴݷlٹsg" Ҕ g1199/* nmZX0ٞ˷}уc_ˋ Ƨ9Pb΁lr^e]߭ogӦMn|k8v,HQ}ro9\ >g^:@B}=>h7rcY i1*r r"x ,P ю@+(@_:w~|\C<]w 299PK/po/Y!pJ{5*Aۓ5IDATO\+aTLOXeuIENDB`ic09MPNG  IHDRx iCCPICC Profile8U]hU>sg#$Sl4t? % V46nI6"dΘ83OEP|1Ŀ (>/ % (>P苦;3ie|{g蹪X-2s=+WQ+]L6O w[C{_F qb Uvz?Zb1@/zcs>~if,ӈUSjF 1_Mjbuݠpamhmçϙ>a\+5%QKFkm}ۖ?ޚD\!~6,-7SثŜvķ5Z;[rmS5{yDyH}r9|-ăFAJjI.[/]mK 7KRDrYQO-Q||6 (0 MXd(@h2_f<:”_δ*d>e\c?~,7?& ك^2Iq2"y@g|UP`o@IDATxiu&=o}H"Xve[88DLLwn'ܳ8gmk:ncu,Y$RI( } }}ނ7;*nֽ.'߻2Oݎ:N@#Pp=Kdwv:0">33}k(݅@ew϶H[(ܶqi+诪Us>gL߆E@P33c=6(@uTpV7?z_ݻwO@"S6_Sߥ'6Khn Y֊:Ehk?|YoHuS: œ.,U[EZ3я.x.@ ЖٳgSs`TruMA:J6h;9ɝ%7asXhr/?o``jHCH]@[:W^M|ٲfu]pwqʕTٚtS&0om7]>QߘB|챏?/'rLM &f(QrN}ΜtI"0Yt*H(@O94h^NQP:;z!ZsAhozؿvM))@̮o6詧_VXzb>00ŋK> t_vM`_}mٳ}ƍ]6mHW 零oj?VW5 07L1}?{gK_dskk4A5FN~{n;H:Yݮ{{5fjE=ر|̎Zk?yE@h33]$wXu:_IsPm"wvRJT;jR5CP& 7gg ԍpF\r9Ȉ'>e/Z˫ "\r<'˗2aVe2tq:]ܹԴ">]f"0\253={GSf ߿UHk="#03{)#m=۴'T-E@Puڴ__|Esbm"(݉NtgjE@PLȄG3E@PD@Wm"(":h"("ННRE@P2Ч2ND}:TgE8##ř3uaQb'# =+@+)Vu("(m:m!"(" hZ"("ff("(@@Vu("(m:m!"(" hZ"("ff("(@@Vu("(m:m!"(" hZ"("fڬCDўt=*"uZr=Uw}2_~ɼΛ*(@!Sc"("PTPE@P: u:T_E@PED("ttZ"(@P*BPE@4S}E@P PU"("ii=*"( @ E@P!03Ӻ&uwm"tjP}@uX$E@P&!5:PTPE@hW_%TRE@ CʀuQ("0W0)Hfɗ.]2SSSFoY]Z"/ᇎjS4d޽\x U铝pAm_z%SOV6E`vogMoE' E`zk; \j5_V!(" t#A)"h}\GZ֨(@O#=_GڣT E@Pz5:>}("еMԅ~C/"t1WNݬ@;(@!~4Ju:#"(]@hkwЮ=z)"Ёʫo.T{GuSEN6} zIuTEP$$E`O>cX%n hƍؿ}6pHO"s];ռ0ǁ~2/88ppq{6H~yZBHF@2.BUSzJ`dϟ?oN>͟Eov`G n3ϴ#>:Rm֟f@@ )"+WѣG͉'1gΜadq;`G!ΝT Lѫ'c0kD, x=PjZgj?f9sw<p ̙ß!nF2&$9<} 4@Go)Z! @Ν37at¼yA\~Gh-Bz{ fVf#\ d޸N*/ver2cw2 5 S3h׶w'Ƴ*ufwڕ}Y_~ɼ0yfcC0XCM 0UV\a֬Yc.]:7H $47P"?Pɋ̎N]U+QQ/߻w/@ (ϟw##:ćJ)]k"G _|`">p$Ch"m6dF @Fcz}tSZnMmKaoa_ pw<66f/^ql 1| pg]>;~+p\x\pSģ0U~f^z%̆ Be5Ύ Oj蔤I薞v"cy_;_}< /"7'dSG9") |e[na *9Ʒ6;m'InЍm z\z5W6az+Ve˖>d`΀}/+j~8# ?1gSSN3pIs%H]q8?{[o5ѐjڰ͎NIt#MnUm#7xypݯ^ڬ],Y!A+sWzcǎFG,<={#G;4##vZUB;;6?O\nO=܃z ?_ ,h*i8  Xa#oupyaڮ ڰ$fG$M@p/"/Dg׭[g6ns׀ vHc `w+W47t/(#zz L˼۴F6k]52wv mXS&@SzۈQU>l4w6my}1Xk0gN֭[ٳg͡Cxqe=NMMhͫw5w}ٱV^DW@F돷5Ŵ^z׻0O=/^+, Nwǜ8>Vc(\C: !j*2;̇~hǏNt?SR*dƪ|   `q||@ke@}pܲe $8 x,'䩘(KY"57Cl5:~tXa _~eǜрmXÅ~,ÂVY| KG׫8c#G^Y0*G#0L`z,bdl>lk<-pw4Uvur^h|gІ'8;:%i4uz;&̫Ћ#7#|(m0PoEe僁|}QIx +XS c e`1#Tx4@F+z}h:D@ )c7lAa t;y~pc  <.W# Fp,#oed y< 33 ڰ$%gG$MfP@τF` ϧ!iJàrE@Cc8w_ ~F>=p0-gX6pdl;kAQ7}âe{/4chώ>q-4]:(MG;g,p?]'"`h^>d$,ÔA#}8^  X$o &BUOaգB;;6? )I"@-&mAmrW 5YS×><4(vzb^}wq%[HuO03p F0ʀͅ{Wx oCxO?)1ahvkl]aeh"Psn _9 セF|Qvo}3U`:w(OIaz .`W^yԴoyG&ȩBhxgІ'|OIiI4vL4EN o޼,w;{Ub~Hcku6#aT.0 G®XPO=i>Ĕ@hoh4?17X)-Jzat}1{%34Ruw} koqɮ? @ib1Tn6lPUnzFK0e8t0 Ea=jj>COfg]hoh֟oorCvU;蝾nba.y0Gk.]cΘۇǼv;k7̅+ʵismbLM>tJq< 12qpng SOJpZ36Mp^.s/m]0ކ|13U/rF7:?] p!^{t*wٺ?e=s;wω͹)39UCs‘fyfArѐYdجZU)2蔞P=aؼw|?.e)\31Pe*9NJUZ"kF^{?iqC;n[f:5,xJ8W ޽HYZ𵅋 `hv{*ЖkktjU:*{} g-Emڴ‚/1z+'eZ`׋~Yt|zJVA};1Ĩu ' /;h=n$`,: _QTk-Tz$GNF?cwJo<2~p JWUx/7;we0;B0=w2KF8pK?~LY|G?o+{^- Z4&^/od+7|}Zj֡@Lcg1я~wCSwe%**.J3_{yXkvpp3#s0Ctx UsiD'gxc 5 JWfUʻӶt,w-%Z/?9b1;W?,kuk3$¢k?pa@q{B  UzBd)44'iu7Жou*OwrН:kϛ[ʐoY:p׏;F.J/*~~@7#/7Gf0Dg^?fGH.\1V9M8UIک ѫ'c/7:bac֌Q2O``͚5رcG1pS^L#Mkިn)/IJh7|}dUzP.Ku+.,8<x K?{;s ̺cfЌYq)>hh*[L# !4įgp \rdݦH.J)L G,7`U? 8%pĉ\&#rXMd m7_F붲J mOCࢍa,9~cl=دc_Y/)@%K^hl^l^,[~1Y`!Ҥwq0ia蠁G4-w|'g,BR<RdL>ȻӼpFIk~TYPUV7E,SD^QzIŰm&?ǝٕ1U?|GE;_p/57o\f[l66ݤn m]4?3fOoNy:j:mX _f5l6CYc_^[/c?K~F+"i<7,37o^j*4E}Z;d>QwNtwC<5_y7On>#:ʒ %p}tQ3??C# 7~Y-:\Ovnw2-5/NCwn_e0ߋ'~dՐoKS' bG^~w~~ { c@88`M>[џ 4갃! _U4Жk ^@/rڈa~ *]yGs\̛7%LMa mxtLMN&-ϹEe<|xi<91^9l1w>_>T~{?h@^yX C~: jG&Akr6)g25\[S(v,u>,7(^ecXCU a탳yزܳc ݡ6Ý'F/;˅#f2C#<~ /_jj(i/~ڿ8S s53~~.__ _[0L_7'K:LkݻןXדx3ڎŤyO`M`*iȻAwLzyy>ߞ;ͬ|թ|Dseй}7+|'}x6fAVdA#>.> }0?~U wm0K@WѨJx*f ˠw9s61C'wrr\0n.3pesRkiƥ̆%f߹WCͿ1o7@"#9'g>gnsb1UD^W*aPXuÆwDLQ*Ⴠ;/ 7/piZ\? Jٰ޵Ѭ[QjR hv2FG޿G"| #\A"g/MӚF\5-Dٰkڼqf*Ѷ_7/mqA D ;(~򓟰q Z#PVJ"}]#9~f04LȮ?I|:?I'࣡LU{#ȣX 2_w[4\&!mդkQ^oz)[c dv "8>O\1+ *ysJrN/]3'/\3ǫYC<۹M>9t|A^[7h:[^"N֢RZ8橆o|KNXE):M\q}4:i(=O̧^6cX-b?z_}V/4޳ٌչy.7,5Kdž"FV\e#M@0Cy9'y}pJN;̡W.5ع#)/>z{yf+ 7n dɂ# ΀< !L ぜ$L ?HY E1i(_mtaۆ}c~mszd4@hIG֥༽; +G?uIK6~$돗>T>.:EPq`e~Z{g_;[=</7K _Z?/pǼuV[-4#՚ؘKWO;`rPַ6 mKGW-6,1׌*Azd>y3cSbjl>xp^h {VlG ?ck8/Uoh gFzßV/ԋ\;tY-]v-g㎪x?~?iC{~OJea3}0THD2ɴg՝tʹ&xIrD-]0hܼGځF-_f%=1P4̿o3plyA&nހ$,^[oehV6|ڤ6d/Q3R>.@5* 6sǎA{0_di ]?~ ؅pVGn[W.]:CVuVE© y:@9{//Lѫ[o5ؼ*OhV _6Iق?)Wzg\I-PZP/r]^n߾}?]voPjyܑy+[NlۜO; ߱~yfzM1=1$ȇufӦAOؼxy*`A!+Y!/l37^@;˯-=~e<%7+ طUn'b 4~#tWM5D$bCr7TN@@PzU?RaشiYnYd_VάL_>uPv3|`+m/[#}+1%!􏀃|'J=~8D, xlA&̶H-dY.%DqJ.umXllt/5_m"/tpFlTd WxG<P$F=@pOزeK*.EĆ.7ߣtiu}fݸ7笟oj3lpY8O.\FJD,c&(ԕ$,4c†oK'><=n7y^\4`IXϲpBպ뮻 r^nQE&LUohcTk.Ѿ^z`gwda_?L6*Nn1' Oa֯,6 Y# ]`Trǔ54z2@b-fуHD_Sk20OT bD}R]{.?GBl7ۘ -e2x$pϞQ3藮c6gvqn5|5RZ6mOV}pa|R?B] ؚNiX_6x_|^]~SK,\ef#m{gcIwkPΣK|','&,u~XqQ#XWx-Rp<7a!e=8ݹ~Q$Ѩn^ņҪ5Ie/Q-W\QRDӗio?ӅVRѣƍ7JK.N:y@ho7EKƿ؎+ȇ{h?G ,U.bu缠 Z_`gu~{)31 (-җc+~@(1l.)pY"G*Txæxpyx<&si1-9 R`c+֮ୁ >6̧3!?C$֟WVSmVtM`@;3=c0ipc_.e3%O?w+|rj|th{.~ lTa!'ŭ"yHrًJģ/h$AP\~J/O7Itc}ټZZc # }:9"˖ $ix 7V7{pY4FRy*O`A 7o;5ߨ7T~ Z>X"@.D̀;"̯̿X$1?ٯȻ74["wg A'Z$2G˒ $-J.܆1%V>`*"'D 97ѫ 3GsdрySw^y35~y Ԝ)WRK)%#q *K")l"A4M>)1r\mMG0|fz}#+ ~<'b[ "Y5=5<#$pom-@IDATF0 ~2aYPE/Q֕<)kTY>A--_ J%n^?av?LqmO߽ 8†W سgO\HkjO:anYXxGwBl7"1}ݻ:ӗZyJl`bH 06X@R9ohRV62ovnaQs6޼-%Ғ/ǠccCP`c?Ḳ3>J8zp%o'p${jVF4sYuT 4ёtp\rbt|N"w`1[WW;Rt}aqn 'M߸= N@ޓ-XxaK͊|\|)QU*d!;)6^? o.+S]֡Ec-?x< w?\\Dk?۸ѷh;K2Q،Mx[ZT"KeYe^ >]G$鑤&MѲEm[VS}dWd\eE3O>{`쁑F ŻBO;ods"o6_~ Ƀj|uzPcދ.ChG\KZ,}t:6-@vLja5mu367Wng}Dk-+.Y}bXxW-5R{Eۃ34_9aC>Omg e[i/~ڲa0a 9T`P\zv0I~ݗNqPe4;ȩ|"D1c\+htZ~dt(bkVHf|6oޜyX;?6T;2H֧.'AKU:U!rp7MmSܷiW FzSrz?/-+[(L AEJ_'9/xh$=T{ 3D+MwQ@ ۺ[:S<v 4Jx3}!3$F/@! NOZYTFk׳ 'am$ʙKd=.6/!_h+9NqIbXP!U;*iq1"& Hc RG$ 9^ֿy$sh>έK\D>м9TEmQ ϥjRQ( -!,TeflT  C/eI%wANoN;>$m J9#~rsb!ן5+6K"#~\ eS3DncY]h>J=ryemڴ/%3H-$rώ4mټi=;\OފuȠecjbapQ/ɈUD)-}]fIXrM8cuH2F(L wu8-0[8HD\-}I.&dg,A^ 0P-wpL82Rn|TP D僴τ:G{N_~pMt#,  6e8Xm> Rt=4G)-S xwM]Ů+' !GP~v޼|1$FH"a1/$̏Jcʉ,%菋@:"80yd8s|qmIaX:ACÕ 9DQ6|pQC \ib H_V,kFd("O@߼EpNuBLgn^=JC+ծSЩ=W@oذ!"!0 "/`/4o['%/6짜]YlD:_@|? PEX,X=C>SqE,"oXP6ڶ  ĸBL.D!sQ8>1W' )~ 0+(Q*)u/A1&4(N@{qu*W}Bt)=QL.[^:h96))ֿ@FٷN箲w:3@Syafb u@N@#>].E)!i1T6էJ Kx~2 Ѕ@1_O$$-GpБ8{Z|)2\:Q!.n(]) n9Sh\WS+a7gsPdw@8pwG0 lsw;*V~}*Vk".Z^޷o_b֮]ٕL^G.Z~]\z <ΗV߶y50(y[$%+"Y|Y$&bN>)WZG)O!-@`! aV%Ei+JD9laI~$Duf/,]y ȧ2"qbg\~\-PB僧fDyIg̣ll}5JGFN(d%el;LiL "d +bu3ڜoQKT{ܝw1rwqK)kSٷEk;4)xͲ1&61z78-p`~R((|1`RИ$N,B-13Nh/ۈ ~$zA崓6\(rc! de1)rIw ؕI |OLpY$ 2Np%5c^^ dF? l4B~`A +*fRg\o$m&A񰔫˿Jq_4w #i-V@$:̥馛7e{+O \2Ά/9|8}B=8鰯 9?,urћSF=@vrܹs#[-+]Ęc{ 逍rQK%-eKibjxi0p5 ,Ȁ$83ዂ&2ҮX'XB|(Xm-tWUHS$"ō21"ߊt !D`HHTG'";nZL#/=z%bљFo{}YSrfJOרtYH_j_xr2I$ ^I5;gJu?KEXBJrd8+9I9 'V6$Hh4}!w30Leart3\ %Ȯ8KylXmlUFq88[ 35+Rgv`,M@t TC]O3l)ן$: ͩXݽ@ѣGS/A$Őgme]Գ wH1̔: E8.GmiDls"e,_| d?+Dmyc$HvQd!Y8sZ{:0](DL O Q چ/M 6!= eÎ-$ʾv99i)#/t w[Iѥرm%x1?)SѴ:]v:<~K&}Wts0GJ.b Gx xDdѶK2pV рzɧZ[8/x?[5/-r%G_4E I[H!ǘ+p6R8p,vDЂa9|ɜ1K[,<{m5|fֻ(E k\}Rͩ?:pEE(~!yJD|(ʾ/&~싮c%AbK\|MD>TXS`f@pZU ťdY2[-<Σ8lJ5t! N4=*>6kqt NB"'֬o/|F-G:~#oȋO[MNOJWca:)SVS mP8{i; ᮃY-GB b( bIH&!aa "e" ҡ461G:7 V1~a Dcrc:kC "chK",2Blu|Wt'(n!A) *ؼ6{g4E5Lk(5;Wv"iNEtTA@A i.@|GڈJ?bw8ye^@ z,[h/H*9B@GA198\w$"s#."-usJ5fTOEBi,*9T"!FHK^ '-?pcLAa-Z 3g4N^NCvi9r}Ul^BTځiOdƧ@pXeI'{Eeק9gkHJ##\@Uq}TVJIgiv3CKȅ#;>' W&d91>grDTOZH/dI3"1ں jdbrt⟕eG1"(lDer:/"h&R 26.G8έF5 "-`7wdiy oIu_pĶQeтs0T)SRV=ǁ=zk/^]_g1/^,NLNOw(?K<,1u811/!vVޙ{85( qɩ=셈l4`I~Y'}](gU 3o `p,%'8VJ%GI,*(LzM[H1+vʈeQ9%d54 J&_i~b l.\`n!\{X+'qu!7% %#hӢES  =l\J/O>ukZA?9[0 PW))9`_.FHKrzECM)E R"'\XlipYnPx Ywdk-QXB"( C PY%c(b8`E_T'<(TFoՎ"/5w5e:E(I#G s;a[<^+;~Y%VnpD& vY}+ y ei(\SN%]g%!O| 6j[Қ]ȋ=3%oN]8w>?Fp䋩y({zb/ Q*8`q}u$Kr @a辩chTp&$I>Fdz9kMQu$*r#R'"(B0t`7~$!ڇy(H٨AJpq)%&L)| ]`Wr\/>:$ kZX4N Chc $O/k6} }w`5,;\2H`:!/3<޿^4`>tV B@ga bF9Z.&`\N ʱ^%G#I92+# ۯ cm!Wq"vq!b~(gUta)d}D)ZlI\VdRetrVq!̴9s?]a L0b:aŭ^8.?GurctLQ? aV'qoA>i,?Y=Ad 5B5Z.c .tF!WFC#1(%F"1 Tc)7XW yw1+іbpu\KdPX4g$hNLӈ6V#Puڐ @R(-q8r#~jؒlce,_hg`?0p&['8p-SR:KCG [2esOYʟ@QO'CIy>);$k҂##{@ Εt_\䉓\~9.=^:@.r"_( +w%DV^`aP΁%A͵]9VR-Jp,ĭD&;`%D 3fGxj;'ٹNu$;a8@.ChB$t54{ș\CRޚ#s/ЈmfhB,I eH*vS\W8RL3=KsL+,gVzjgԅvE(~ǁ;( lhW8i_@x߼V~GÝ|I8_-:Bl-k.`FdxV5@FQz9.M<14|#8"W| @9'c\28D`<j3 b%^u k'^07yn2Tx꼑Nr9pPsqD9F X\-|X8nF: Yl T8 iMI:u]ңr>4gEiP%~/FIt]bQ?Ag@g=Iܩ_|+;s.%AvC P2`XXJZ=N)rmb|%dFd:P#d,4HU $ 5|$h/'$bh Xc`s8,R\9PAeSV 5#.֗<wD^|tt{,U`cٗ#gvֈ@x /=<2x6؜㭱"]Og^e*AO0g[%uw|tt4?_$E `Y8~YB28E۔y.x)oc锅|Y"Ya&ATIFF`囤 |!nk()|6& 9?y mpA|[C(cP6ќ@ʻ?>4@yg_6Sd<9PGd#N3lYI)ìV+f3NtʆiTF#3ry~3lkYzv y#gM&P{YTڕ6\}"?SO}#P{9Nkg[ẻ_@*di 1RȻ++10:tf]Nj5Vr".r# EDT.82?X(/ѹ0YgmCI=A9ɳ-uz@&乴Ye2l _,爍K.s6$]hpM@6y4jl7Gؓž"M_|Q~cwf>ʏ/.%d/b( .?`NtXGby'؜b"CLǷ}|'qܭ>ƿTpGg,4+ߨYX0)0*"X[DrFy]4p|B4cN+89f^&4~ |`܈Sz z"mcgO]_ewijH[܊ 2K3Jӥ`UBq#! ҦX8Fd%im8 m{@ su?8=%/]ElKLG >OlFCr#!"J#` Ɏ+r",!J ~-qv'j G(q|m8X@UgeKds:&cG \9-'l6uL_GΤ(HIɢ%!,d+m;;J8I#@h4[Fځ@|-; hqX ˱&I֪]>g{߽ou9U Uv㹡XǰĐ={bwސ,~LsCQ | LaȒB$7R=[zMoD-&s"N)mo`~\ʲ㽑2[hRWazKCAA䋖Znu=u7!XhGҕqݶ0ݲ_ 6ʢ>ո2(FUi;ڹ$u-Q.K4n<W4a G^-W7noč[=7'^h/Vـ%h/ z3=#݉롷4%>pfhMFrd@F?CZW3l !)`"RhY*IT+΁MN | S0(^JIayqB0`i-0+ߝ1Kʒ:ܝh"E@[2<:twwqol'J =ߴgޜ9VM֍tͦF6f.[q @{A-hb;뻇^qt4boɶi˼hUڷoͫe](PB‹&TjC 9КcOjf8'>S)]A6m{\ 6 yj–V8KX r*هTd|;q&^EVK]$EMYu=?N~3%ש}"M)/'Xʓh>~?WȘUr[ƺ|F5xp|zU³_>w޿Xw1nPq TFg7Ͷev [݊oh '?90!K~ɖ `w\(yp)pOܴHjHC n_rVٮd÷l8=mݑ'O;C6dVZ.(E^\!#pZ,o{(]rp۳w485=Z) Lg4%;/6">};6ʾ𸉊܏ t7o.=-6V. wX3ɤ ,q'p<ݼw`UbBwӬ9\@D/9'(KQXSpc!6 i8LHJ%׀.5s|)Spa7i%(I|AJ鶢n* /1K^{59X Ll(7/Ё@oW'<P![y۶o۱GǺ߸LϐfN$*?9q `- &]Gx ܎- ?kJAB?xh>@(L FtPTw`-h*YmZMtW;-{zVP,h\_d,ћ84R+#W캿?,dpnT}Z / GEsa%5b_lx(&DsC@Aًxꀌ՜(}兺{\2 QflʃͶ[&[ɨ?w·ӧgjYhmΜ[}`wnJ{ bN&<?b%ަ9#֤TPܤ}<`]+iݹAuZ|A+|K5a)žJA<}_Tr>:!)QH x p$tzY[W0 fhMnݾI;eqAa:$V -[OkS'<OFn5n~_[X?cwSd;Dשm[e;[pqδk7 &cKܙEaOuy3B)D.Y \2~%֪P2$V܊.ݗwwiH0LN>׬ƄTR8\&/؎JJ\,a%vcǻ_׈H iHF^&4]]aImzbFRm_ 4ĹX}{wt˒)~32=gPVo6U|rs:s:U\bdضx- xivX}4_B(>SZ<6tKb x ,nx2cPD:+$#m`Θn7Fn,6};n]~=\6dQՍ$ ]TnKK #XzʶD5| k :U7v1E=O}Oy7z+L[\As8O^c͂v4tN-|e72,>p4y*\Ycܟ?6bb9Z2l1s&I# :aiWyrLr*FC1$ A6dT@EQ%(G%S u#9X|"TIPERTyY,L-)|#zZ-'H:!r-⎝5 PV&¶qZ1r. :XuK=lH|سSciХmzM|xzU۶-Ap;>Ecloh}~ћ"@IDATג]ꑿ1hm?Y>b=Y'a 1ZgjP2:njKAԿ *NALMȁ8.`)63=nED/Qc<f NE n(߹8RNX_ sC4#!]rߨ@_)A}I<_,OkN uw_/wxݘE[cg7"+k#z$_+3k9zezݗnxҔʝVAX?kO0 J?Ȟ:SڃI\ƽ,ۦo/zDk5iWG yk=?iC 5R2nȞ]:KXx,NAxf^F7›"Б8@ `}bw.8z{ڧTqLO:dDWzm99/esTD#%U[֣#G4nEu&7~#'vz`eWM5ף$)y$kd3%JTcesU?nǺŗ0Xb,I%xy<)E-Il͉ݱgSgFO 4#@"-_,<2[K[MnbHbӹjZޅc#lagu`;lM7uVYHZoV`;܂7bo@S;D8gҒobjrPY/&jzʣn:Z]$ ]ps|vBG8Hh/VՀ[ ȝ"Òژ6A(Ţ]Wsѐ,(Y{I~gg?cd`>p'Znǔ FYȢ,I8K`c'9lh6v))z9%KCJ=NdQڥK;Jmt;׿5{{}Im86;#b;ꁭ:Poʷm:{`NN<5卙3qԑT&d2bΝN6kGy$ W'H(!m̋>6Bya\eJcUnY7HiWGCW/S oյH ggGǶH;xM :0Kз llc@&#Q虩1! AJHf%25[MA z$TS P%3wCxz8Rf/r6};rk췆ݟ5^@koQu}nٵy *88y Rf# '(A5EW#D;>qz t 4D>sȀΌI-2_1CrpIsT@Θ\)(3c<8+z0%dglI~Eľ4TPi`Ce)a9.P [@_/{kE (!}jɟ9ciرiTj7}3lkZq>fm GF Y{v Z76I`v=]Ay{&^sӖ;G9{@a2Ѽܬ)<$X[QtVtfER`֗.@0& ADTA^@n'D AhRC劃Qz]sCRIU҄`:jWhac,y#8lGYQ7KwRМJ5lOӯ^ZW)3{-mS?^ڨGەٹᤗ35#m7ZXl˧F_\r8Af+H:!إc 沮qFS.fCY)Ւv Vtj(!e&aj~ Y?t H UJ=J,${=._¨g`$܆/R4/Zkb9erM'7)Ǻ ɐ}= ]9aT)9˱$M,_cz-E^+*D'ܞi˛W:U3t s[ބeg.\X }Nc%}eH =㈍€Đ}k؀s~!kR9xa +Ȇ^^E"6WY%ce5vXt%V4S~B((H,Uy߽Mߒ&a.hBn(;v\?8٩)K ,`ԝ1A|Ai=(ΙxłoO-6gsv}EҷMO&;zݨߒ3qʙ,Us-x6ԓ؂ppxuɜ ˃LQ\4&ˤ`V8b1hqC wD "Z`K $TEŷA"a]A$dJM@gXt#뱒'O[Jc//OdRt, _#qO*>.3G:dde|m%Z4*u1~Ct+?FʾO~+՘!$Vmۿ*^bnGmη}W.ةc9* ^hSx~SύNmfWQ^=m vnD*FDaGW`Q$|L)& p[ئO%Y)_%B`E)ȪݢMUƤ+iT@9eس$aHHZ&l GM./\[$Nç~,,@iruy`j|Ǩ |SvPNflN%:Jblnqkqm6>UcTok=b5 &}jݱcV*>L~ɯV>Fm%CGNtożn9O1-*>|ow἟v.x>)h)I=pDW.Vy}*v DK0RS 7F~7:@x; *b;hZ)d@0fVp`~No7eProU?4iF0\tU@A)eAj3W\KKeh u0,MuzJw߶w[}'f <6Հ5iSoʹ 9ʃ{vlrw2o-g=x5vZ9]xвG9}f%_wj3{it9tYJP<BN", 1"Wz"vVϗ37kJВd/P`Yp52oKC)W.Z0g w~~Q=R@"}ۥY6` ,Eq6MZw|A`WigDӋPHZ{񞸁IrN&t/z3ʝ$mA?`(SMl o}#sfwIyFLW_64ڤ9ZP#k6z y)4?^-M cjRsqAI>#`SU4/%RqF! 1/~P @,l\##=d\ F ޗ`͉Tf[;9*-,mFtDr{ \4znWcV8>uh ߴ8^;Sgst Z nM–6diP8ub늶InztPE&.ԩS }1u`j"C}wݼ&#w햚de;e@߶ T?S'X !2f/Cˀ?yK&rr7*DA +EBu ]6/򤇔R!H_V BP5`O>GNcoJE=!BLE[ԐW.Y{\Bbi>ݢWB_t4'ț5羉cGHW r:3wе,~9&%7|s=06o߾曳z6mFU~7}>O\]vu`Ϟ= Ugնi=)ϏP;nQ!WzL)(>2&%\_&c^ 9v.4+?}մ"BGXzǶTS< TSZ+,ctG~e{¶Ov@q8oI/Tq =E#\޷eutI|Ԩnf;A 67)K2pѰhY=mh]|&ʴj<[~GA37ʾSՐ^2g ەM_؆b$vبoc LAY<}+;ߍ_}Pu>04޴[ Pa+y~R%t$RV_Y$nHOo\EJm'AG֮b4tr0нҬ^!=t W^z53onwNta_~ mPaEwWG;(" TU)b[7g#{{ T=uiJOwX`l ~4y'-l▱uq6~t'};_{l >9J3_[$ѻʤ^eAɓB^xU;=7w>%w=ˀ|B胞Q(gt}s~*7cZ^Ji׭(4Usۖ+ `@9$԰Dn2T"$ <ĉ&XN$Uo CR4\~T3Ͽ=vBЅV&9jUdX$7eCL 7ԈuI8fI?-Ml 5 #&8ʼUSY0X=*-,l>jZ7"oo H}|s lF(=lƺ8076qtNlAzv5W\eapd[qۡujK\Iѣ,*A e)4h+{/=ىCz PvOLr8uMFAlcݟ(_\,,I,gIH]/q PUbm eRO}sԯ9P};zZ+Xw.$S9b9 3C\ڒFmNSim=qz`0b6+{K ico :0;nm, ՗rg>RQ_3 e obKE Շ$kՌC6SmD]}d,MnTrE1x>+f߯L41 /w~y{ (lڣ ]'lHT"JdRUi8Ө6E\ #lUGҗrRd;96Wh,/x_D`4ۜ@+SbؐAש*ڵX>}xYq[kM_]ܶ0L˛m?~|r{,=Bz\\MĜ##4=qڂ>=*G𰡓.!%EG(r9JSb-dq&CE&hVH}fpc&S~Ձr>Ng[B:ة?3ļ[طʏp*xlnoc7HBM/JrBz,*_E4JEhžYw[ّWsZ0k;VP?q".ƚlsjeby QkZ[4|;--6`y%pa͙ =v֙ؤZ *]I4U\',Rj&iAIIQ.>J灃I[1Ml?lOܴgj<7ZlHSW_㮅rۏk_ m}5Z$d#6h{=x}хLM@Ni'[;X. =hSFiD m1`W1nxC9 &kel!&]j^O5sH{@Su?DmDrfZBL3<ܽ)sx:;*6/-kdWe^ѫ iLa);Zr=Y,b=dBS l{k"P#Ya%+'ic^o |Vna_32kWz,07~p> ;ѲYvdЊG^?b"|a + Q vҞ>aodI3C9/p _ĭ)n͊l}G=VN%-d0 R+]Sv_my6/heKݑ#Ǻ'te##,W9 ? + ek hUuE*0J-Μd膁Hɣ]4u ɩ@YЯ|7nЮjn}^([{_BؼX6ОXJ_+7oN S;1 -P;BËEjV6B١+o@VPۆ3G':j[O{ )N-;׿ThM@ozDGzQhv6_l)AZ'o=\g_NPjSWJS ܅]7*-ÿ8$(r,w)>"v(-'ۓ2R1q Դ4ϳeڼ=0fiE5C~J:thu؊Oۘ+wtx&;[}5--2|f;ct;6i7IҾ$DN !yCZ#UEq$19.S~Y7Z }e&o|1{LlЍ.ma9) @7T⌘|0}4\2XrO fa$ۤ#%"/Mw߶z[4%blHjcF#SXscmڦ``MgͰ0>=5kjwN|WUx!"g/TLٟNA%x~T'S;fUGG1sW4 F+rGr3da+\sb|'G!)ޜGl d%Y6՜K'&]~mjK[k?Zv-;`ֶIk?U֬~=Am?\j;M-ny`s竇NNݡk|6J`xV];o> `t=rIR{QVUxG_Tc粮72NjIxp7rN233yMNJfb'Zl%пW\]cs{Q1;2<ܢ_P rMYh^wL{;xlxd^QއW6uL迥v_b[$~Zt^(u)̢ԍE3t&ˠaA}[ozX=Gneϟs{в̪GݿycީOډc9 œl"|{wwo;CpS+hlJ"Z(^L\T=Ȼ~9mp<ȡd>Ic=&vh\I'7Jq}n;J*<_Or':B`^zNoʜF5]iad., L;i[@ ~ {fS73?-JWkۯb 0Wj}Xk_dSg~*wW-x`ey_޺ș{BTnn~#P>xv?&<{BGs[ ڥK@5W-=I})H[PSr~mҾJ H24L.P{߶Ûv/0<;Om)_tq2<~E'(pa,Z]$JNȪ']Hh\]=.i׽c-9zw;i9>PSũn=H™'׿iU$f{19ӫSzPh ^FSнHku>'2$W) oFl?9gθ2iZuL>ouK.{l@3_Wq_.x@Gٷ_oI=̎5Zw}OQ姟?h0Dv^|>Gsvf[lMГYFUAĪŗ(TpW"7XWu*Y7x|=ۏ3˛\(>[FnyuxYr'tyx|]Ǫ;sp V*,(~$YQ A^ۊ_)c@x.Z =O}# mJc;`9mɑ$GO?W3vL8Lu@AV_xd,nB~۳DPH_ mgW΋µgy7_7Ft@bȩs@$y^TRU,a \%f*ǩn|r(n=}&3o\ĵhZzd>ݮ-Xw~nnL{ȽZ^W SO k?>qGTy=)ޭ˫wz쾆ͤ=Li\wKTLlA{]=&7[4#u14,+@kVѾ6ikj=+z\%z2@uw Q5j?s鯡)"K@L'yU9YAII@s?Zeѐ>bjhϘh/e.~->u:[\].= 3eF|]Z,y{ +:`Gf/~g]z@h@Sm\goSzvނ3L!QfYL<,^޸gݢ8O!S'3<6DrLDr0G>d.PDX,<}AI7L(  /-׻Gbk,_;~ZCk"ê4v/.]+9cNSuSU|'6k؊{}|bNvkx;ߩ߿^Tdz Mj?\|Xwęn~Yua9dPz-ע7c<2e"[Li}oQ A\Qn_⅒Bz#Xu(Бxhr8uH9L> %yXrwP3BW桰:M)Og,_#ؕ'pL^6kn=/&m?}k xhrl׍HmzSTӰzeaS&n= 3̀dsk_ڂbNK4NYZ=zww%OO~۠Yv`F'?O rN[{k_~o.x9 Q,rocfAMu܀WtG.;rTm, b7&<$}ۍ ^C9q&X0h5.] @fi|?4>GVYSumCj tB|ţ>#d>n6O:fٜF1b+ٜ}p4K/-X>sF'ŵS#'9YiէO_#5;~cpsp;/kzuNhۗQخ[ 2V>|^X{^A;XS?4&KMD_Sla2caD R rïcG< LuЅ\7ؔ=純da9ZiN.9GO7VJ]J?[xvNj=}-ՂK>zv;)mL#Sl˵_A}+ rf}vezC.)Sz$11o uU/އ _pkZᙆZ~bNj[/S駍%bJ9D^vjҶőTbqHT5ik W=ŧ]F*&\Wؓ'OpP?wt<ʺ*qcϜVf5kVH2}`lPnVBDX۶=pwt<&HvÑ̀cgj#Տּ,*ީ^98Ԝ]ScWEUy}Ie gXO-ǔij%"TܥDY"8&%L:tH RӤXe_G,#$b=z]۔`~9 U;TQ;4YSVMeA7:-Bd̰z'ocyÄ23&7# ttV_aҷ}R*49$86OWYt%ͅpYD7@|_o1,b{g=/q# ,cY^۾,ct"vJd[޶yNƬW甎9|ul N^q?>&vAΣ<̂ \ :B_w5# ZE72Τ(~)d ,π&;pnJ eHFK\H )"J'A/!)s<߾{SN>9kwQoOX8' V႒ CyLwW?rS+c^?Ӟ燷|s<-}ҖvG=JJm״lwmCS19A}U]gX>] ^*NT}cDI.}dvO.n>ȝ]5RM]fEud5ˈ4`dK9SUxVzGRCqhp!QP ~vSB򃏳z»c j!ݡm?0P Kb鴪hȾp5N*jk}#_KHa4?}Q3zl?zzVݒeKmm(zYVG[_G-/m=hӎP{68&D)6o_+p LsX_^8zt `|hK?线__ͻ/.y|%lڡS @a)S]>qQ>zLlu"bEaYe[;(6>+@ tEҍ^? 1qI"oH7+TF z`ߗn-=wWx@QܗJԋd0}T{^8k} ȜkjW+m[ZZeZf턾 G7:q N}5uF`զ|+~Ddwַ߽]wݵd2j_8w=Eogww['2J#+ƠC@jgX^x$lW9ۏ"'m؄ IEŏ@BiZ v9#>_: q#|G b̾2!8g С` vBC) MV] t/HME* 3r7D/?۬UgUy@n:tc=5bKy6n_m}b[C)Ilo@98Y< 6R$區$W6hXby !lOJ`Xm-ԩQdΧ6է.\gSOFjDC0i)[JrWq nua8$l,xZ"('X{zL(/Aq0OT{mF4e5eQOݐ'lE2i*Tz.q>62x`ҁ=x]-?Õ"朣dT8bTXe{dZ[a܎1̍L O#"LJ7m209 ЦrBsL%ncO:{G&ste@24s\s+@a^⌒6D+rZd5o>*b-=V]ghv\葌Uj\s1'Y=覬bi:Uz4ZnH.\>{’ߛ(s+w//ښJ[0~ߖSVsQ4^c֫s+t*~XvM_g3XQ[oviu}?f1&&19 0t^<ڝ䧄-DS1{Sd6/O9gHGiU(ԥۇunDh| # OAtX =cE]b/W{FKVW#(3W#`nxYo-yzxQq+p<dznNEs[c};\T^~n^\dvvt9V:WrGtخgl6FwNN\W/2$k4|,7!掴,^į~ER_zX]tlH0g D4b 4@ZsBު̗(6 ;ł~I*x74AˈiBMMvA!X ~8ݥA-psNt o|*9oЊL&H"Mz 0%RQ^.1y\KKϪ/k}'eƞygW.oM)uZZ59W\maM_TEo&cn:mO>‘ ԋxFzLމ pͫ7=|\9p޲l;-4(:&k 1/*K} Pek45?hr+AW T-a@IDAT@V8D "-3@eu;eAoDGZJ9EHVm:}! ї]r6&B;/<[&b?Tc A7ޘu.ӣ_멶~Sim5YƼ:>}JhXul'63l0m`EsS?9vh?|gY=|1W/tι)2`b" o`G*3f$O;R u멣N>ؑ>H)zCljrBGD`~\!VHaS!|avn4hB _9^2ܣ}m.<jEc휒iWگؒ36H涿27jo6#M\ mٟ8caP?Ds 'U{׹gU` 985ۚYVlJ OTLN(rMDSјn|8KVKΈfI[GRBO)SOaاJo' vscf? uW".}BRD{:\_2|}3P(sck{,r< 뵧7>cO Ms_LX[Ns5f: u;U@& (yPX;v c|믗ɴn*59 |&땦PhUAL<' z/tyM<{p # 2z2c!>5G=&M+eU> ŗ^~_SKDҟU!r7k}E K!;M9N$v@ְ'Fo#.\Ś=t 2hL=}O-h{j?P5xur 9Vsg__-]&f)'~'1|K*m؊@kz7Kd(J ,-#7}_# N1*==?`??or EGg^;s}8 %@FH<4E]c"p*臘A=r*F@EG*iJ1J4㾨&.+ڎ\ TĶ|.=0T~KHBœGˤ3~q?gco {.-Z*:ja޶3#꛾mzw{cGD˦i?6qlc98M]GE//cO>B\PP"pBF%ZEUףpa$=NIeFoDpu| C/?eR@(+XT "0r|x: FT Bh!8pKl D(g+ԵmclN u7"  %v 2i Njq/aV?O j=Svs?NH,L[ss0=}4~̩Fˏ¶[m@ c=IINA_pT/}iC&B&>venj*ݴgg?xg!Ȁy݉39f|`}zLE'!*FaB(n,"'yW ?'`|Ija'Ow{\k1e&RO?mߦ! 9)5ht"tO(bg'zPُGN&.g?IGs)7]{Q݈m3~& VQ|<۠ l`Hd0Tʷ^$c4SKB\XPVqKiLN)#F=!8 SF2CoIol4㛠ge%cO }*?8Uij8LTB~m /Si 3Gfζ>Ŵudz`+0E\&G碫l?|6fҘlo~1 Ji=M{w&9̅6F6s<sL߿ ;%mh{Ө`=@DJ.+ "VNjbqq Wre#^I_z݀DC@ Zt!D..rZe_/fH] T/>݉vӟ`/E]|ĩ|eۗg={n܉iwÛz/G#*mc%-Q.9[ ӥEŷF!}*Z]Nݠi:&lsc<&eBu+9ͺLAboiW?wS陗ws-p"e0f5fFʹCuՓ ruz._L(ihD2#p>I&atF%V sHIGjsQ/_WA |) |vQ9,*\aUoM?)EW7t)t-  )ǐ< 6ʏ֏޸j#Flf=: CDh-S&*JãXT$sCEv]JF?5QK|tLlrQ?VsќF$LM<_܁ʷ;})}N?JP5%jh#G wi\PWU?xƸF}qف#xCS?s<0R)4\6d^6(!'M.'F);لObڟK:V:8lMqW_qVOA?gxXR{N3DYk.=O[߰:~v@;u03;wpQ郛8)VD>} _@A阐 /gLሯM<ƚج;y elKcbXZewag(TB.|j0J`K+K!^?e+; ?xhG\bJilQXǰc 1mVcײ뾣 tEەA`GGKop7~qSϵW s+{IС.'p=Н3+YCd`L2#RёMBhvqᔍz]־]@ ?L,ETmmM;Sz|ì'U8#$1rHexu>sM}N& ՗w:?oo{n7Ӊ3WܔZ'`e;ϸ7٠ǫyߘVf)tvc~2`߽֒gwwnΊs-!/~w~?Iߌe.fQ쮻ۣ5;.ݻgjJ\TMG>I{ŎLR@}:~ lkPi$~vɖPCݿ'dPld08@ClFU@9vD&PpLףsja_XE&fFF.$ @cKI9A)u^u>tv&\eE2oBߍJ<&=gu_~wC69͘^vݿe{ _S6Cm_~S|TOaoַ+uu<躽7d UqڟVYg/6 M6raވ:<__~Q7ї>L{eN@`;gkh?'7~V?7_? 8 FxCFHHY({? /FbM  ۠ؗ2jBnhąieUB \B|xTN\r~%\) VkGk/?w1'qfSaՇk<: cGS BF oVTumc8ɽI|7rڐ<5M]ȶ]}&U2YoF ?y=};Ͳ2GM̹\n7݃N#}ٿrwg l϶}_?y>x?R_B;ITL~I l7I%A/eltt'ԓb! a?H} lEϢ˹?[-r_>=<^.Qb+{^1%V0wv|}[FL (⦿M`qK8/%/ƿo':py?\}ڇ9)kiثck_c(yhr듳! ߺxҮ߲`<}lQf1lAŃޜ2 pM-MGWݑ]=/'&/-8uԠ=J|򃢏!1jTXdQ5 >N\=8:2'{D#j°kIrpHCQxaHpy+ DH>ݦ{Vܝ7;M$ˏL͑<#<|Z͍?ϺgY[t={y(-sXVXQثe_woLt yCtZvmڼ>WNyz4999 GME߿_e85[K {`oY+< W?~_ѢF{uI3@L5\ ;T!h>.m`ҙ"h:I^.H'Kȶro^}i5`U)lZpi)9풢'!tʳ;/g4O|⇸(7zayڎ~!+ϻUs#T9++ݫ0ivy衇~Z= ɔ@'Toz-y~ўSݗYaǓu>}!CJNO%y#+(v2hf+4z7}P@~ԉD^s |>p]KbE#84p kqbET_\'k?'Ӈe9`:'q t,9\ mpihiWZ׊q==&DK7ݴws~o`3vln]9mkO7//wL6uy{~an7ynj8gr%c3,"V%y>_ڿJu1B٧?hE 3xHyE/c=!}0Ɛ(!BN1aA#Y. z{:h ݣ\JB?'o E;.Hڤ.}h?\mz_ Ryus?7q ȷ7BGks#t^ :rcrT15?˱d=mw{op._om9?FAI y&0$H dI]_{SN_>sN?5ZUvUUMf{;t%I2"5LՌs /*&"O 37\ywt:lE[;y%kqM O Ÿa _6Q~34ũEC8HjZ4)yjݤojr(=@_a7T~],[:<_#U\`F/Ԅ&( CiG-mٷ&{koDUd_z3;>tVG=Fa!M$bQ F;/[~<.0xD4Q9qRX݀&HS+zo&8.~hu>6_zzWHO !`WVC '|LA5q6/1Ugrٴ~취+"4>v\s< He% dc 􎚵KOn~%zqŏd}fc4L!;YOaD{3 AGU 0njk׌Y9ZQ5x~7IRj0cՔ  7kȣ(XM]YcϬl怗|_yqoW zR\&+:O=/;ʚ?JsO<ln~D-{Uz:6+{f#@]RepXF !J~{xE*8&\ݲQm|Zkۘ Gu/7Px61z52tLΜ9#n#(/dah#0vjW ##mzwY7=bNy׾r03-A.Zf3j*5DI4W"vEZ< 3v]v 'ʜI;g9*._n<@#.oz F1L G}}HNew/]49YwSE o37s?jaN)܍uky_I30b(4h+(7xq DvA'B>Ty`h( =.4ðg hѢk_ru3FVFPGغ@|!VXa:]s,PݸEnN0WQHz$'bb}wM[ϛӇ,#𽲩SE ^}O LXPjE7B"FB _.W/ o'636@a/y#4'/);2vZ6[teҚU8H6S^xF UD.v yY]X7bT᷂`Р[*mI*Vy?d[gù[D7Z=vQ*I^Qr4e'SejG+`4]7LGvw P ojgYafDܺ1#( U@,0J+ڼCw0Wlg}}WW#=;}]eyQWxD,0G䝓 WWh_*N5Q?I\ mZ?\d,R+q?!Iyyq+{ -ϘGN' %@Y$B%?C3/. 7K,eQm/s~Vplɼ~V K6l<~wFcTRvAB'ęWOT׿uvW)]?A- ~vf$RnIg4vGa~1͉AΟ2JnnKwM.:X>nRE3g_Cv"(4`ʟz (l)N+8vz (Rb>$MZq\VUʻT[#w5>ہƑ{z!5-屦"Hm,Ydۉ'D~T2?fOSbկ~[*ߞgdn}6!L]Ÿ)x? |_.2PultTȎoF`!%:yf9f8Y$4a\fGx_;A*zYg]^y} (&lCHFh[[b /.u_t/1~BZҌ8nj5=l>],úe˗ew}g$( e'p7&vW LVh Z]lI0EK=Qi|g_JC//œUӹq 0'a?(#Gf1=%9)eK[wW/ɣpU[eԧde`LsavّOܸe[A?<ؘbs.O:l7#Y`{)C1§'Iⱔp¨5a~\pbzq\v/Ws/{JYl^~ èѯФж ޺ᤓ \X;@d?fuQ%3^i#<9ԥ 4Z>[3mB\M[ڕU([Κ.[߈^H,p--3-}51y;~[<2k׮딱iG}QaB! '}5P~!V׍Sn&K=i>D xi}c9ēyOZ6Iv$][(".7?)TH.P^pGƝyYf\GӧOeڴ {+0Se tvv߷i0[ccDOw*Uӻtuأ`+"0xcWgw>:[fh%cwN5//={W+}6YU:'_P@<3\![&*5MY؞um};D)*Alm?n\.csWƈ{c'~ qMekQ?4o W*[JQgFNʼKؓDu]rƏ~x&/6qZ+b[3TLFͫd7T̘j@3 mBYi\:|n,%=6C2#/cV`[f>=LN8xr|},&ql# >#Iv 3OeKfg@(( Ɣ7~!}8C@ffQ^؀7>wn_*ZCN+0U6rNƓN::{˦rjT-W:)OuOԉ"@k7Br„Oh;/rsy9sߙiW(#! 5}$ y^rQbT(}}\η? {?#/ɉB}KG8-;v {+:C3B߂w}Qƒ|~F!6_n7/FutPD:ƐPy ,`qtcN8xQ&͘2Qt\S}凐CջE o'9ng#cpI`D^\+{⣰//?#Odqn>82W #/fcm&/) \܉Fg8 ]Z7F]л4|B(ĺA ˫~ WèLTźI'6 k82\<Z!< 5!Te˖eϯ<i1g t Jq8G9U ^v;O2rSpg/~ 3Ja ; Ff90qĐ7C|1(+OXxmS-M`1{N'oj6K}\U2{7ԏfi`Z,$A@3_ aM:_;S ۶NܾߨIhnlQW^YM~tlCf-P?Wf w㑮 {L:fucgY 7Ϛf1!ȴlt?q҄20C*I@k{x d?ȥ/}1r.f;M,nr?B *f?y~0muf}EtOB\Xw+h.A5ƌ4ej6a܄ a2]6VMyxcÛ.U Wy<+_JXwmou?&E¿L*! Q6ӟf'r4t#Elf؈l eO+W˞oz ǘ2֏U!0lDe;MU8U48,L\=mǧcԎ)~g>~LeNF#+A]W jQa "<o~Iַv'J@B#,:>mCe_~1B> A ?@}@YN8SF+J Nhb6Xh>䆤7Y(+Hc prۦfo+I\X7b6 'h}Ť erȟЖf)= >QcYw u}u@ڝM5n7m6]Q AvpӠ(=Web1×Cx_]Jnc u.ŸsW!f`W|5}mð3DUun͔}otK~(L.JrTmtH^H(؇{9U#} B(fVlߐ+GS?޼~vajVĆC76>[w)D nƩ2FAkN1@VOÞgz5odz6.v?f$6o.ܲHe*?&5k^"6i ^iDEEi,(_IrܯT3UGx7ͭ6?-C*CpQb}ai6vT`QS}&˸ܪzfV]+k֤2_HZ `TS2~_MN1?J4bSO=(fucqIzoG*}4/ ^`?ϧkV;f{mo (@~S#{ m?wm(H7A ޘ/֏IR}Ʀ N;!n@cpj~!xv#3ژhA0 r5day䑌k5lv‰ߚVTe#}S/ܾo0VD3:颜=MI h1&B?-kpu[ngLϦe4m7O_^z竮`R^g/|ԬIvQFb\i˨Gpڱ=fD[$_Mv6;"2FV\W7p5aݗx"TpTm&LqQ|x%8]8V넉0XB(7@rlu7$p!8n=4 p0mZmکjM>n[+soylGwQ @GاdU'N@:b4NG?:9վT*_M7n~P!V};qt @駟69@_/ M{#Y,0,`x≦Q\pbЋS r< Z vv']qw7-YCG ^uf3fM]]\_dޔ٫mVgY0>m1W!#Cqɇ6Dm|?Y`z1v6|) _̙3c=6j?ybTˇ2@nC=*<#=I:>i&>qXZbGֶUTF17Z1mڋ '/*cHb]wg9.ܑr![WNWӀ.a*մBxC"f|wrS4aJ`Ͻ=<V@48!&Ax; #zݠ&EQ؜/ŏ\~_F~kfX^5 N:vuC/6 6͐.ihնnL ,n6m _aLhtwo;WtcTBm~WAYfƁj_ΐ@C@vW%3]ܶ qm;FYfAS?n>;*Tؼkļжuk(P,4@A Pv2`jtoJwi]d*Y^{ڇ UnppetC9R~jҞOT·ێƫr@Mr/ 䒴ՈT`=&G*ҡ=4Jc<~8!%~%4]/٭in">*$^ F1N؋p1e_I2`# ><>hC)fd6F7> _ IP$M mqU4N;G\[P"#OCW 5U L?ԮqE8lV_%lM7F+=q I'4E:2#[*ן)dF0z6\Іe78H7m-md8Kf:;nEvҙE@r-f2TI(/6nVXECz<2 5XS'@btJ/&(>|[?a̐0g٥i>~#+|\7qn;ҠUӿ \^TӾC@m/mشUo]v.\#UIxu&*Ƃ P38h|48a!m)kKW%*6Bf M;"-o|;Zo ~PB!ݓd=JX2R/#Š}2>o@V My(Q6{mSA?R*h\Mn 7V K3CM׍iluScǎ//~O~=nRΝ>K7#șFkgI ôL۟_v_3?,?$JFL_2LloP%* !4:mxؙ@әШqnGH!k7vQLp`P//fK>XgAp]`Z (73# <5+lhhVruqDcP!{hAm͢p?c"wNM>'яXO @4XF}qL?@?vFp %}+NwWT6;LYW>F~l$O ]L!35Qc Pxl 5| Y)uEn 7VЌ˃D=uZ0mmB++=m6=cV4u嚓bs)WCYZÀ)~Q!IeEAj#c^c#g` |+x:*vv7yІ 3BT*m' ȣ/$n6c% W^1ms1{o}EU>j\21=}tg`Yފ(/LB#v$ÒGmS?ijqiFkT=Mbok\tƺUk~^%OXcPeOr PR d2Ÿ)av,#  $|Ÿ)V?a qNa6cG1m7~nCyF(vgIX6c3_ߦ&ņ>O>9{}}. tx؈OR2,@+@}{ ؙ@ײc&XgsuNNl4ZIGK}4m+~YFKÔvOQe꿧͗Xx9]$Rjr#44)CFߒܗ`F8vc· t vS7#Ucɡ6Lf씍nmU&(5Q.4a|^wUfv* &7G科.s.z衇ϰS._Yf L3sX?~S]>Ruy>F?fXjձ<`~{6tӤź1Ti V٩iW7'v<>z_GmƗ?o]/Y\&~A5$ h1TO:A N]#fuMqa3w휛f>"!oȨ6DLa>6a!郋pc. p|ˮSiKndhv2B%×Bo?/͡*(*(g42o9ei TJٳZhۗN,AkR_ڶ۟Etƺ}teMqO'Rm+b$"4|˻q.·'"GPV涽*/mQh*׆]fB80b}v+_L=v?sҥ/ҴߋM| f>-K(.8X`x߷u ACf6Oێ(Oظ>Z>ڮ_.lOG>xIyTc5ˊf? &2tRsЂ.܇R]r}vs]z(#=Z×0enPE=EU<2~nhƩo-_O!2`@6/AUqÕƶrtdo〃?%}0o;ئsIv{;N'5pqmH?UQbF٬ }qFY1q8v%K޾=_>/~ +pQoAȫgfn/A6n.#s 8C:C- 8.~k}f4}ۆs_ץwbJ[/e=lǴ}FCCY,= YglY=lmCLw2O8mpf|U;vǍ_&~NQ8KNl;ϻy%1LPLoX>"lo~NOTp\b¨j ( e*$gjڰ0c픃Q;vm9nm/ 6 r-7FQwSCqY:ᣎ!D ,`EO"Lf+hV䫊F^=,oäKs:ږ%jnၟw!]V~&5d \{tN!FΔn9:~v &,V+. 42mhjw}䗙 :mMqk|-Sa@X/4g ~Ow Q⌠Pg !ޢ0`y;^R ]AP7,oqvю]_0x:6mjoݍ뎡M#gƍW](6*.z80H#A7V@qsعɌs 42>\;qtϯ'=~贱C >ʪ4Qd)Lxk%ݹU&yOlp^]mwؾ=?Mc`S*u$kCq\v16=>?‚©U]msyQƋuV?#8Q.۫9"Lc Q wJE8#ؑ?Ë=2Uuauf|Y< Kkwv3HQ;$t f7x1Zu?ipV PƏ >\ 0/=hekhc@٨ {ڻ\Gb=6 SyS8(3#Guwq3= @/@}pz09,c4)#c9vq<1?B-˄94p4j#lNz}v6!u4>Mcz.XV(&8!Ъ]aV&*V4h[@n~b Wmˮ[t]7x_ۦѤywK 6"1Vr90"_J9);6H&>#<<ܢhtx|>t?Ÿѱ l5|U&..ġqB;^hkkNTu s2Png(p qqk덛|C1n0<**놓A/ n94>OmgK}PZێ[m]rC'_2쨰2FTGVdR/?آ܀ǎgO>`1 "O; ŸWSrs""D+E%KրCYM&.O.HX(ٷ~%_8Mπ'Ԅ+ܪ$QS>he-*an4v J~8Gۭ}U) ֵ5ҵ2B_~8>2F1wT/N80,Kߒr^ t>?X*1cJCfv4S|u&^~_4Oj)!]`A~M`h?p^}up-Qث0Sܸ;KP V\}k?~nAD9v_xW~70^tA TaL*(A6*5Ga6i;6Δs=gC @f,2q/`7xۭ\a$,5qŸ侀EG&?-8{ OUS/mp݄~}upA0Giˍy @i~pi_Ѭ;iqo9}jޥW 4(N @LO\}pᏀVPO'ڹ矯 n x4m)i6^UVէa?匄^5mXXӏiHUA~7͞Hb@~o/E8Ue48i,tςU-~K嗬aj%2q{>\ŸF?W/Lcm/! |m]ڗ/_^g?\@q٫* $ހFZb9(wj3Pv[uMnn!_ڶM[ؓK>W6\{iWũr\[W,y9J y/gֿ''#>z:k\d 6í\PJmKO6PN3h\JU;3 @sGc@O7|Er?b"7[H1F/n"վDOҧ&nl]h FhG@n6 }_"P$s6]Cpbhlc~Eo !E;/]zj5 e#*c^{UǮp' ^C8QfGk$= h(Eoo*bZ{dE[;L˫fzhGEsXZ_uź }-h4f1^q&N?_з۹4lۜmy# {Kr"E,y4́!sÚiE`=̙3N8__b "̡W8dB LY]o#6ts! tj t_"/}C=Hs\%AEQZߓV *7‰ o͡JCXnžiSŊ@7^^_rz&VBgGGB(yO۽1]X`cFC6(RFX1jYv^բd y睷7Q*{@Nx? ο^*1CxdBfvF^d' -H#gdaRJˀ }N$Zw-z}V?M*VlO_'_MCmimV}4>ȍNQ84N{wl8oyµˁ!ȽD,l;u4Fڑ̚5?x>;Gϯ'ڰ0]ֆBme&qW.\h6a8ʚK!gq!uWU© o 7V ' 7sA({s\ 5G`Sh_Qƿ (|lkf_d?vҋ* oCJ;wRyoТ"Tr`\iC(v¹Ai {B}6f1Ÿ+yOJ9W?,}LFF?<_5  7,Ak^r+R4foe#h hB|UnO a%W\._^&(@5OoTJ-Iە Cb@6Cxu|{o=k瀐Ʈ&v*man۫m\klNA:e&cʔ)ۨqoݺ=G9b:ow֭ftZ+xCxш}lZFG]^4 \~[DC6mqӀ֭[䭒cE!nK\ĕפO_:#i; Gypn"q4wX0tasR*1Cx1Sf6#mcw?=c0esn3E/o覜5tځ&u 0ׯV.4S@&(ݴ9l|>1EWٺmKqFKaΖv>4o%wp?Z*篅]ӊXƆ6Q|pƍ av#WkOVO2'aw?ߌQ"i"7 >{@IGhUEg_{ hvI"5۴(*[0(Mž֠+/DEoܼ!Ch:l4fL@*"OB]'NEs?WN֣+ xԮ+c~. O_vҙ)buY rCXĿBlAz|ՠB JqiGQ_vZ__ݾ|4Cp8[hk dF74A\6i[1Lݺ5L%3ߐ_ כxey+ K)/k qR@[q-9wpgM(rm~ |~*q?j~!v;U_feͮ}4ea?1F6"IioedA7+"'ï@M=,pVThTgũrP M[x¼^o?~> y{IKP{VoAysHP&qof|;Ͽ+=9v ?*,|cXF8Aɋ~CcW;Sx -}v_B}MC ho)v K%9w#OtGkyӡSJQU8UЯqm+Y7u yu<*ů/퉋MѯG]n߰$*SO*7xd/ 91>|_8qJ |;w,FugagpPdD/Diܩ #xѳ  1];8G m{r@0EPk#V^3&.녇?lQ$# PVBp_^T0i\~E8E4g4]7u]׭q!q\[iڦc5Ts!:5iX.˃ߘQuXsa`޳!(-&4~\ 0]K~f c>mc(nȗp0:>.z_-kx S?ҊS62Gx6C>@Ē%K:L5hePEPVvg &Qz4\nW.V<5p 纕p/[.\m s`iM;NO}4m3j%n6}[WAf.^.txHhƷv^290 ?Z47!>EXڍ?}]/Q(J>1BЈLhL6-_w2MQ<B_?x6yGNg"( +4zۇSFW<[RS|L7u۸jwqbܡeijTa 8YBM϶i[a(ڵz - D-}]hA~*"/2\L_tEV?+xl itfJOvx/ ov2Ÿr[j>Ÿ{9 -7KHDkT- *8.n;p_Z@q(*زqmaSiĹp'N#^^u VNߎt%^[;f鏘 eGM_KyoK&g$M}%(F3L;YgTfB!=͌]䝅v~sŊ^ y%$P#|Mmv­U-RQ֐:ZSnqn}>?;m\8UnS6k4U֩'϶5m}JF# ;=5Y .з~úl+͐x8oHڶq5i_!X;枔׼& =x:aXOTuOnݫqo.8 8U.|l 979cGݜ+57!_>?P{zvw(!v]KORuy !|K8%G9=H.J# F y{̒e3`/B'ch=0Y(իW|F ,DZYbG+.drI0/,CIaÂegIK;Yw_:~I m; VĉN8!;c#y$eEg "{lXԑ ~jw8 yEl+Ο1/Bhu6-yf i̧|$cBqǵb%USQp@J_Ȟ#ͺygxW:lDp?_mOy_M#B?#].-?E󇇂\W_}55H$āK d +OZT5؎sx' qu)χ?aq{@ {キY?\Pol#tbe>}G\tUF:҄ w!:DtwǎKCC)=f &m󪋮Zapc K"MRҜ見` rD ulvpȪCŦ?h( 7P\)*w_vL\I>XtOğ$gIDBgLqCdāa3/Q>{Lz]U8d`odDޝ6mZv衇ѿ `t={׌w@8W&5Y ..Baʟ=#{I}@c _Nݑu"ڳ8*IcƎ)BB.Fym /:s&{Mܽf>ls^YuQtWdUgi.P_f2?&{"a{7ߜ{ϟ/#Y88ύen%?I+{wcYR~JC DaW/ w5*ϴxkE?.b6ܱ*4ޙ3g#{eB_ES5k1+_ 3{7#!Z0\fe?1 7ʰ{G6oWzX0@x{\8``%pD }aP_f2G"f!#?iC$7oG#"C)6 '͐>fyՎ_ ?Mr` wxAi/ˀQ2?cw7/ H+ԎGk-90 o֭EAGi'M'^B_O~;VQWāāāāā O8W޳l:WuMpgl`D1aGh BDFHx)rAA(Հm4d3S#i#}z@@@@@4Lx&_.]jFu0q='?Coذ]\|ysloc"%āāāāāVr`2-G3EV_nQ 2::ɭM6e ce\ ։2)2^BOHHHHh)Tnܿ+]wݕ_>cO@0`)[2B( fQp18UyE)!'$$$$I&!}3${\ld&ࠃ2Fe=kU'X>`ڟU~6ᏼn~1 rṿ/o"J2bz/\0c]vB3@/Se `r2cƌ ~8 ?ބI+'$$$$:@8`XpK;wq&~H>F(|ō=!4Oo~c$x; ^Pb~_O5q q q q q`90 %eξ_(_LjXog#4Wg_x}; /;E 43 BsZ@@@@@+90( YIR}yh1(`%e^%Evd&kb/ډ>xLB;l23iFekяc~w?|MG|_r;q q q q q80h \uDawK>WsC>Q~33`\ģC9qg;K\sMđUDGd888880hNtnN9IwNUڇzhvgg @AF3Gs7O⎄-"D"%āāāāāA@[(pO?@)J7erHv 'st)}|#}xᇣn2ϕ5_&$$$$ <̃Eh*ַr[̙c6 >~60ꫯf+W4q^|)F888880hh+.̝;w?7Y4V8"e2p7DD88888Vh;L\+V6,==SR^b9Ж @\*W=ا^K^LNƁvV D 8NQsg#$Sl4t? % V46nI6"dΘ83OEP|1Ŀ (>/ % (>P苦;3ie|{g蹪X-2s=+WQ+]L6O w[C{_F qb Uvz?Zb1@/zcs>~if,ӈUSjF 1_Mjbuݠpamhmçϙ>a\+5%QKFkm}ۖ?ޚD\!~6,-7SثŜvķ5Z;[rmS5{yDyH}r9|-ăFAJjI.[/]mK 7KRDrYQO-Q||6 (0 MXd(@h2_f<:”_δ*d>e\c?~,7?& ك^2Iq2"y@g|UP`o@IDATxw\yoN"' " H@iy,h<v=[SjvfZ'?fkgMqO$HJ2EH`@ r7\6xqqq}<}cƂ                                                           @>l+WMLLVUMe2-UU)@@ d2WwƸ:@O n ޽{WLNm6ؒT5ӎ  @2O~wo< @I<#'&2_q. =" ~254! '3'f?   Py׾/sMTD@ D://2k{UX@(uu~{d @ՅoR-ܙe#&S#r@@0;e,pŖ-[?=qxa[6 [`6Ϙ䯼  {{o~m鞕׌-@&.r3Iæ>  T{]9qI!B@zˤ/?-5@@4 ۽{4ՙ"K R }^29YP)-  UU_u  IH%FGpJɏ B@ d}=@@Ih?c 8"  >7NhIʾ@Lj4+;A@J Kݰŕ:>E@NILNfީ I`|5qj0ʊD  ]v SbN5@@ .Uz 1BVMLLga_گX]]]C@@ 7n\??O?ȚL|go6C@ pH$&&&ڊ3o8 ;D@ bۿe]]]CKV_tm[l @HL6[׊ݔ@@; O߿UUU-w[@(@DzT5Kؚ5555R  0uֽ9*ɌU7@-@&3QoVxg&swp@-( EWrlaA@ :G);%A@@@$|B   q ֣    @@@ 8eG@@@ P   @HĹ(;   >Hb5@@@,@ έG@@@)@'!   gqn=ʎ   O>X @@@8 sQv@@@| j   Y@[#   SO(VC@@@ $z@@@$|B   q ֣    @@@@M wtܴ3gXOO[Fؔ@{)#hݻѫnoɾLƺǭ:s~l,sg?٠mY @ m$na;z;uuuuwv Ό{<7mvI[XtUU_ݻUUG탵@"@ tTɡ@"&@y,  @޽{mKQ@ PSEl&  @Eܴ_B`U@@ Q,eBO@l  @4ܨƍ,BK@/^,`mVE@,0lΝMQ.!eCV&%{o/^(V>@#044d611^[1СC60PDn2ծrUA6B ͛}߿|Ab7x Е6obw_Q[f-[mna濲K.%Hܦ@ $|~\ZZZlǎ  @U!jSy~G&3RԆl K>mpptb+VZUUu1.WY>@ zW6K|K4;|m?v9|𡮦bgH@ q.V[[sqŦ  %0b7,d;w\hQ3L6r,E@y#[;\UUޮ1f  P@]]qt ~ & @\ҏkQn@@@  P"   Wqm9ʍ   @$ bU@@@*@ -G@@@(@@X   @\Hĵ(7   (U@@@ F@@@`*   q ז    @,VE@@@ $r@@@HŪ   U@\[r#   P X@@@ kQn@@@  P"   Wqm9ʍ   @5˪)LA" t>yxs9j?1Q3\SS`  #܏;uꔻT~ 4 @\Ξ=g,Sp\/(x"6t1FvI&S{d㢲EM@T Hqwvv;c/_"O瀪# PnL&#.ЕLtwH18J@{ccvaM]Y@@J d@{w=X_o/;W+U$]I3k/] g@*)Y54dmd)86 dInu겗_~zzzfS@@7@?ڽ*_J$O@4ow^D@!Ž{P @ 9$Ӗw'0Wܹ#o  @ o߮{"U( \@Oymӌ,  @\4`t4c=2SN@ $B%ʕ+GK # dj#" vU>c  qp=ܳgϜ"Q V L7o޴K.7v TDfrrT"@H$Eȑ&Й @@ d2۷&~% @HD=+/?v TJ _Ys\@ )$Ғ3CCC3^)  Oɪ,9F#@WE%|k֬UVx'r3@VV^m}}}︮֯Pvlp9k(fsMF Wܘ@ ~ ceݺGv9! @͛oWLo,qV\i[<EmF HܢHփb:u붲b\v`  XSSSP`˖mvȡ"ʝiعsgGؘM@ ~ ?gknnMؘVZ @zꬦWnذa|ll֎a2ܺufwJ;tWg3@ ϜЏpQ56(珶 UV6B@O`ҥJXSn;zmn>>>$n(L|y +k(0_1jl  D_@ۈ"   P     6   ,@dBv   @HD(!   % (    }o#J   @$J&d   D_@ۈ"   P     6   ,@dBv   @HD(!   % (    }o#J   @$J&d   D_@ۈ"   P     6   ,@dBv   @HD(!   % (    }o#J   @$J&d   D_@ۈ"   P     6   ,@dBv   @HD(!   % (    }o#J   @$J&d   D_@ۈ"   P     6   ,@dBv   @j_DJ@t>#;|t DI@Jhnnz@.@ -D"%011a $G%5A]!.    f   .@`vE@@@ $ьT@@@Hû   $B@"J    0 }x@@@DHD3R @@@f 0"   hF*   $f]@@@!@ H%@@@]>   @"H$    ݇w@@to @ V   @lHĶ(8   HbM@@@b+@ MG@@@/@k"   [m:    X@@@ mQp@@@ oŚ   V@l#   _+D@@@ $bt@@@$[&    ۦ    ߊ5@@@ 6G@@@ V   @lHĶ(8   HbM@@@b+@ MG@@@/@k"   [m:    X@@@ Ķ رöo^#sH@@@4u 2L kM@@@  { R~@@@|*   ]@[#   C$VA@@@ $ނ@@@$|    q2qoAʏc==e#)A*-߿¥ 5[n)(mm |jO_Q X=iQ5jQP X ;}v!Jl]  ,(vW{&'ݔH@F՘؄o^oU7>sϑ KF3V ѲcHؘ ;60@=mM@@+2ПͿ??p)ZHTC#@<N>e{vxVR# @Z26{4,`[<@f<-?S٩x@*&09o27G7W8$", &0::j?}Q+A@ 쫪|{k v P&wvglŪ  @6Ws\_PHD3Pn?翱F @M`Uո;;m{ v PkN;~̮\ZR=@H@o6 @G~2%# ?LLeHA$ beHPM )4{y)~M O@`u7_Ww̗y $Izt$U ω@ %gΜNIM& Y`2Oi; eO 4$@ Dr+H@H@kke2Ԕ*FY!Qnʆ   @@$d7   DY@[!       Q ֡l   $@ Hv   @HDu(        eQnʆ   @@$d7   DY@[!       Q ֡l   $@ Hv   @HDu(        eQnʆ   @@$d7   DY@[!       Q ֡l   $@ Hv   @HDu(        eQnʆ   @@5 vOE]Zɓ@@ $T/XիWn,ࣃvV! V!mz*   &ijm   Zmz*   &ijm   Zmz*   &ijm   Zmz*   &ijm   Zmz*   &ijm   Zmz*   &ijm   Zmz*   &ijm   Zmz*   &ijm   Zmz*   &ijm   Zmz*   &ijm  e,!9 yHeE@@@t Cp5=!  HAVL   [1m̄ kP   UʴG'@ !  jH   @rՑ?y-HF;R @@(@nO_&p (@@@ <G @<ډR"  @#hzMdž   &ij$ԕ@Z:   @hE8;M@h@@.ǽM\~in}^׭ >CCC),%E@r?4pv\ea Ipႝ?>)աyKQ?srQmʅ7-^(?$*@H~Ӎ@ 8v=̚"@`B@@thtՒ@ڛ"  4_oO#aH$1    fHı(3  -kWEalbSR   (@_" Zn>   ~rcdHF;R @@@n  ƥj  ] 7'Og"'֧  $T 7WE T˧ P   r2la @@(@nB@,B"  l?l^Nlwj  @"rU?K% 8);D@@rp ߶  R 7*? T fe@@@б@원   hyj $  "0`D HtR9@@-ǻE)}%HTRc#  @^_E| j   P<%$@@*8Lb@@ ""  t?mN/@@@> ? *D@@ wEcJ P"$S x}UU珫L/O7ިXXoioN=MLLx$O`WO!"DM WUU{AL g^^Ԣdϟ֩T9. P)zl@i$Jck@dT_ɠln P 4[ 7ۀ[@۟#wt}z9v*SܝJLݏؘכ wm^A u9{G |*  q߼z{{oMg)A(!RUU1 #Q%tO UEr  e[rC _X@3W\Wڵkmpp%J5B]:j%JHfS+*?MOem@ ^@(@WW_<www#Onٷ= %!u  %[n@$f @gg]x._DX@=uJ^JdO)?MO3x@HTΞ##P}/^[___ʩ@]]wS]5ɠ##SIH'!pw7=trw3@$-@ ;w͛7ZS%M2XWW\ q6 "?MOii}7qk1ʋDH@]uvܹs^?44Qr L]Y oBao؀z @2rj'UrH$-  Pa/?{VA9loYAb#ɨw4T`xxw&dA |ە$_@ۘ(hѢ[_-'u{"ThEgO>ngҥKhRA { $z\/ =kJa˗n,8ViLfvX=@6lMMjjI_W(9sxXHѭ* o٦` @4V…vℂޯkkk 4]//f/7}%Qdfy}Mo=cݷV\Lc@nB'_? #?SyoJT\'ر{> ]^uˎUWp}^ϕz ))uX-4炒 I^(9{wS]K,Y$զnF +(I3&3<]Fx-$. ]@A1rJ 5Ƽ g.ϝr䃟EΔܺ{yO\QBѣma/P__*PN#?MO`~7=F~4X'Z$]@gu_A.mⶶ6UW ]t_z;{ufO}?{luy^Eg~5g zdk}_uSn===ܼy8`Z6nyXԀ5oita ^P"@ pϟÇx~vsv/{\Eʎ{ ^ڧ, PckQ=mdJdop"Nn ,wW82"'ozӂ1: ݶd @K'GwW``޼y`_~g{ 𳏃 Kn"v3z\ %lb \uϗPR@:::䀒qYnܸao}-[6{>R~ʉ@rtZ2#: t @Q~o|LqQ; q#uotVwN _cqRx(N0{ н^n+W6S"A=^jr֍::tj9¶oa/z)'_?M#?h'J|}!(- -ZmRl7,wPRD=*fPWzM(! s@DžnZ k׮nJD5-ٳΙۼ9 W;ZWȭɯ E_@ۈ"w>ku_W`EV]OޥlF4nZl*J h&֯_P%K['ji(Ƀ5kքr)(֝2!P@7=AmQL ։ %Fn ;}]Z%(_tw&Vgg +RSYl jÅ z͛7{Po%4@o?vx֮%6<'_?m#?Ol7J)xyX%)@j˗{lg~,lɁJ)ΗPP@94)>Jd{DŰ~򓟸9& s֡ ~o+ȯW@|ێ#@ Μ9cMV+H} u9<[.P!,PjHnZ4{ ]+ZJ(v{]<nEj$7]AmnnNH$MX`wf7#{9YW^w꺯1 3lxK>f{h@%*;wk\9PE=wJc#P@7=mM I -5C ˴f*wzY5_jw;u:٠bHQj P2@I}6n 8ydE+wM6ڃ>x .#BK1 ~?( 2K)  -Mΐt ^gAf/WY9N2)}+@i Xti#G>>;;LީGL%1'?MO`f5yzH9TX@ikXlu_lYqJy?]*;>gn goǎ &%/5W@h~>b<ҁ,DQ 7MWP[.'*|ocjW_}N8Qx]Z[[Sݴ5fgA+HWzC nذ֯_MLM-ѣe~8<@7=Ami` H_Sc^w] {Q`n:+![._nr{T?^v3]A7~VקqZ3yt P2@mtR%.\P 2κzxmim}y<~T҅s`t HgSk(իWG?>E+{yIЮi_It9{St'ݲ&`n 7=:I]ToFgTgߕ?7&رcr@|𡛧ݻǛ @+'ͭvJ_K =5G2L[oe =lnnkOgW_u?=r|TVYἻ&t$nrP%<4Py9%%^zeh[g>i a ~毿`H@BPp!* xi)8Q EB^A0n&t9_IWO/!4}?V,Y⵷}}}׶mۼЄQl׮GrcK 7MWP[?.#?"H9@PkSI }+؏[-grnӗlR@@I|ɢD/ZD`Jh؋ʝg߰c>s>E:SozӘ1:L 0] @ Sg_뭡1_Q߸q78_ OX:n n*St\>7@IDAT3{}Zl"@s]BP.J()9(gM큼oDՓrES 7MWP[?.#?"L3Ex!TYSCmG5kk )Ӓ u&UA+^+z6,\Л40z|'vܹp1]*qY{ꩧ 0͆ lߏ_zhw p'^G| (ojhŬ)8SЯ_Ec7Pl?WLsa&Ew7o޼5YC=dk׮C^/r w.dDzWˈ ~? Hn 'uoD/_n[n;sf ?1nlFmuuoBAƯ)EM$ u 0|Ix>|cc?˗?o'_?m#?~X!PS]cX+hxfٟ蘺NnQ>A1 -;::l%3@T<Ӝ*ҥK44@c9}߱{x=yqL/7]Amn_w p7!G`g.g쩂Z7z]8M6ʕ+sduVfTd+PԄl^{(^U@9T"@PtGmQOUhH+O?5]JǯiwE6Lry jOȏ P BX?|T$:[xښ[; *l ,b}R:Ӫ3jk{ n +V*\g-$*&TիW_N: x}PJp ~毿vI AŨ J@Dhs]:,X~* Ңޯ1QjʕEWoL7>JKhUꭠ%%(4I&=T5?Ճ%E3JLF'z/rueG1݁w(]@,]ޯ.Z*Hڲek~0:3,D7!EIe˖ycM^ r?co˩ڇ~dW\gy  lO$è1b oŚ 20+Pd#3WtV1GMQH7 wݢ0+9a?loqǎDW˕+W矷~ڛ0 jϖ0*~|XHV p]Og^%ZA"o(u<;[ P(8|wKX)(t[uװG;=җd};v,.K/#<% G[ ֏|J_Gj0$bxhpc5{_Ό{gDuVgpYR@)]M`ڼ3nSE{Ͻ;ܸqJh8oc/_NY4;3]ߧ1*~XH X jT`sN,L.uWoJOG7-rd Avx@Y9ʒZ($w^o^#G#\Ξ=k/=_xƻgb M.ﶕOQc:.@K\ (tӦM6f6D3+']M3ܔ^+^ޥ4<@=tTo%#4I.4IίY/^l^ %x=Ւ67KFPesgW?F@p$dO SlA.:K|EƢE3+s5M_.qG@K%4(AVDz$׮]zo'OB97^ w{c3f3%?KN`{v_maT| ֓!@l\|ٛCkmzmT'K/j16݄x?$JYpu jQ]vy/O P` ʽ S$3.w ڕkFǢ b$r¶z[`]B`<%lfiZPM+ *?wﶓ'OڡCB PL~2Ǻ]߹%O+v!@ U:M|Բb o|)1<3zRj ž,( 74fN7iz/fokn m͒[,m]dwx%S"@y |PWQkz=~}PKcG ug1wO/z7y=6l7 z\tKkќ:JHO?) a-[o[}MؘZR3KEvyK -C@d]uW7_uיƙl`9xhAi0^|><: luo"J)NVRs^޶nVUmKh˅ (\w( 2h޽{O>cǎ +. 0+LW i2՜/&$J ubQЯ?,L+P}tSg4_+`ފL%AxëMV-jzlqhQ-r)1!(Pꑣ!K.yfJ@PE3 to+2K]5LFOlqi)ʉ̿z[ζnݚwL3Zi&$,l8z;qP(A/P ^ee-QwĂCCst(WAB[˖-7_@t\n> )Řdyb>%li Jŋ 0ܹӖ/_S2,tl``;NtػuݦX*/+dWغ-}sM/`%͹sL:]:ziH. Hꅰo>RƢNw^[R@_aea%Ml:4X^}Xg㌢@+rc:zplU):V >|\۽qz mѰ]3@c^4/q}#x=ºT V{%Oe@iWQ6lmd ah fP*(P _g {1o"Pb@(h%ti aw.vLW]+gA ҃HP7]^pϦoB[~ d=4F4۶.rt_Yr,Jhܸq#Vg5k~w{ +/Úc/Is.7qzlYzrm5J*ap" мP=4/+^oo$ 0/GajmdEF /{GԥVgJ]Mwv*ۅ)I>Eg?e/wC)cHCjnK6,/Xl +jhlWݪ_|ٻ,=o{I0nc O9D~(~LyKLF`n'è H 3"@t\zhl]˄ܗ-Y;󽠟iwՏُ޿j7z`1|9{EohYji~ P+d'TO w޽{+ja, 𴛌03 H{H1*Cf1 F]@fWo. ,dԗ3G?cZ/ȸU/Y6CKlڹ[ U>l2o]!@=4=J}RM̀a^4JKiZ Sm؆0p o`@:;;*5#/ЄQ];ިrEX@shn,htԶV[]Rk8z,\ *а7zqCjqB L:_J1Tp ?GG#Y3㗺hBݻw{rܗ&',Əu*湂6.k񘓠ziYcuk-c q^MԺ׺xv]m͍ǧ~$u=qIt+W\1hOw޺`+Gm|ꢯ?}-+Wn( %jܧqOL)^WLiM2p?FqTRHo@ u"W~׵S4MћLZjWY{]ښcM.op ˵?6nC~M=521u?RO%:(TM񎻲ztCjmdUsѣ$@ JAY@ G@dZݥ.+V&וWW Eh׎\flq]Z]`5NɝOʢ4TRWz\'{zcxz y?jEewD{HΝL쥥KP*Q$lRKo55{ɰc罤F td2ذ }R+9ݺum۶Pqa-a]]]a~x'gxuxE[lVg՛7Fhoxl54 Cb]K}cBKL?pWO}[9߲z,Z(oOlrkJgO<%4Aa&wK-%# aOe %Ai_t)ˆ lywq֢?Y]y}c[ ڛmŒ6[`*|xɓU3P=t{ЁK}v%gnn륓w\*{]>0*yB)2/_&TؿWTۿlpɍ,%jPO @E2OQQl hf*@*-[l905ZtPg˜2wzuwf1?pZ[lv[ݖbk@ؾW\Ϩs D~jkK,B>!ŋ^OG$J&gzhN{9Vudfۄaieעh,Ez\ϵpSGQ+7umEg?pGʇWmWWv.q+??͛7M=4kIFKmےJ <oo\t9QIwCp(4Ȳ$' c'}%[@ۗ!WWR/w BcՍ6E?ȋ-P+gc7EhLU l㚅X_3U4;n؆umEܧ\πO;F쌻db_ke-J0VUC%4'@w'Ljː%=t[cZ^Zlmd&i VDP@]:Yo/ߢ1H+E]5@gY6tvy^п=$L}}m`l>.9\#nX"WY.PE_ 5Ĩ&W:$ 믛t97%KԆZ zrErYtQ֔r!wqv'z K=Kmڴ).Va I*gNiEa5+s+]; .q,CSM4n;9fc9^eEN_^cW٫$SVTI M@Y֫.C71z-^$62.ob2[xdI"@ *-A9b!tc N޳G{׶mۖwƴDea7Po]KEavz۴fmZ*|V6vO> .jj=]E`>6l]RˍoO]-j[|~O=º҈~ 5P@C4);'АbZu0~5%' O,寬 stR-gkVʕ+mǎyٹW:ۦ3: W]Wz?9ƛ*w>WSQcn P#8REW |/kl_Tbx(&2U@P@=x7-$G v0p0K^}W.0 ]\@?TJ K/x- <Ν;μ=o7!_aaTLF`n'è*)hd@t^x4~Egy䑼~lϝX྾>vZIbNc 7=#=^۾~m^G7|>i}]Rpגz{hQ'`޻:l#%.Wvօ}ঢ়u@RR2襹e$-&J~YNPI[e|~E+&# a#E 6EF :3|ڵ+o𯮰Ƣ uFOg*/gm]KWӟ y~mr]!."[[oK6E%N/ 9I^t_~GGk%x@}]e9*54W+n'(hs@V@V{(gs@jw&Z(qG7flϯwx>9ScN?Pc}moqںos Z{]kꩠf3Wg&ݠqww%etwbܯq7LcrrFU"FQq!ݏyOwNc]NK\Ρ҃B8r>us_gNCмLJL}?hh>h 06e 7 Y2p>FQ Pa ԉ'޽gϞת& ! [协o؟ils:{~"vRM Tc=WOZַvL޿/^b?640l}68~hp$гT~z"sYD5DĞݻ~mr 6c^DADeUJ, ؿj*Ӑcǎ)uty~]勿BL_|gY"Q'[@9)8u%댿Ez\ 2$ߟs^׻3mZߝXgsښ洺ZO S*Sk7/O.v0/^K@T[+޼-t9ǁ!tޞ>w_'ӪQC| p}8q]?yގ^WYkSFI5 @PbҥKɰW͛7Xrܹ5>Ze ~ sY7 qDPJ >F %/GC=w-uVA/nXt]^gE7޻vm?ʋ-6]}}7[pSUR$e:r{o*śm+_S뷶tA+N74n OM`}J[OWu⸨֚vW xʠ!wLM?|}_^!+d'ԕRtiDA}^hnnһLJS/bwm[40E_J,?F h?=#ULqnj_Eg٨Tl/|fͶ%m6;g@ I7K7YY=o/^Ϊn;׳]Sys09,000z[gg3_7[(߫on^ }rO<%;bGן\i_,+Zc(J~dOE_IQ%@~G_;M7ZYy>yVOKO\CФKj*\` ] p7_}^`ALLmϼ ng%bX#Ewǹ>Ӎ 0 e(B $T }x 4<[*ߘrDȭ_ɵ9>޵J5-\+{Dy@.AW 3ew{Al/L)ˊ+Ԏ4|}< '?)ǾB盖b 'F_%F8uqr98vxj ?MLLYj&;b aʛs;Ivb\nu7 e\.qvBlXBL(^{ DxAP&߶`ԛ%UL9,OV-5!M/xm]R|]z *n j}$<>Xcus6;uI*z݅(&}x f4a#J|駟sskž}QVy0bԼU>n_%FY&GzFʯY2G G`D׾ƋNYl p܅tߗ jU3.` `:?os-]5 %=)tD(m[qN[m'bkS27":]Y cUڱnzLgc|Ѻd-#6*s,NrIn{sTOm "~x)e`) fV8p@SX[O5)HpaqLYh nJ*X)}Ev2HطA&ToIzy.Q* rWI7 hI#BmۊŻ% ߶F0QϪjE-(uF8{Y\>%YT1h˟Mg> rdqoG='_z#$ݼU:11UberG m~dazGd˖-bNxUVE8Iؚ dqsW'H)?Eg-0%~zqd|?U=VCX` Bo#ċ#U;$@NunESA-]X= X0Xno_#`/|+?]>OLH_8+?Z;N 9,DJKk `g|#fgдf?E$@+dv2P@SI1h?ܥJUE :5o!1zS[1pIkؾ9^IFRb8֙ A[TlVZVPcjb4mӊD+A ḽ8E`0~j\r%.*jyR_jbL9ox†{U+t#%+1*„8^ ڊcZR>c씳GazN\$إ;GsFuh:5`\G(t"zVJq#‡`PT0孷NQJȡ`5OFvvǨW9Y&G }@fEgϊ{q>\xՔqAS.c6'I.7Ȃm>JXVmRAU wG NNN?.^~b#o!ĶJЛQw,#]@w9 >Bկ~͛7ݻwG455b.t;wӅKψ׺{`\?M\m\poPez|lCtղK-V@{^^iMI2~Jh2Ek v*5075]%baP⣇7_~1"n!v?ڀvm,Xo6^cOu PSg\{}tjQA,[8W Ǩ,#0ៃA߯Voҁ+W.US/֠ʃϼ-~krpWSg#{wew|qxuFN{pB?|$6ƭa`&BlŊ-O]ȶpBYu[.xhj m/S.~6l/$W_{;_tI>vC,+bMY*/Y⋤v*Y Cfcm)t<m|3z/,~4M7Ϡxx>V*Ŋڃ*?_yN|6 yʎ,x}:UNŤk!lbal0L)5dtmzI$ⵡ`PX9T}wzwʉv|}w{@&wo.%kʂ>{4%dr,$~͒~wyK@- 18u\d'rrLG7ԁSRS>7;)߿(s" ܍Dj=^8% ;䯦q lB8l[ë;A$G`C 6#0";87;}wz Nq lǟC&ulO8oJ:[O_bre$P]hF&?}ۭj-aF\ #VD_<`EHD߲AX8i lG!m;,T Uuq@OX+]mYvT;AΔ; FxTYma$m_%#FĨJO‹@Xx4(G iԣ>s͚5b׮]Q]zOdA vd'sRĂC =~ @fgIV=`rW :“{7N-sr퉐Y9&dM$#X(; bAK.M΂r*}vj&9>CI_h"a[I[W=n_%N*2 ;W}a3.G GA~6^PGa1X6 S^1 Ece_zV>wZ%ٙ ?*+O_+-=չ'M!䨥)H蚼\ OSAJ@,[qcHUu ypt^d N)n8ҚP,YQ00˲/~jع9$gko_&RgGZ%(ÇŊ*/^(﷎fHؚJюS_1ҋ,8"0ܿ#ƹ92^ 45)o߾}QUL#M9S^^H\8vbOmtߔwWf!2N&X < VUT˸Ob^ ^7 HOf8隵=G*@J:"wU۶@ǂ(#6S\"v{sJ.\!~<#޹xD= ?R₝~Lr|g3%J3i*v]UNd6###p} \ Q7O)/_tlMu\Xr.VO]6nF.O~Y(= cYf^wMǤ]HF8+2p8?AʔUm:eԛ`1+ ZY.wU U a?ѝ qzzV\` 7<rRz(Ŗ-[Zare£u ~%HּU"=1 #FUze_;9}#k~\L`޽㑀TSGi)iWŽ?y+%ڱiFgђK07vkz :!Il(:ל6CuMԠIjMV{<=譸8<;BA j 4+ :Bam&&S0 >NqǾx } [vbT`̌o=٣\./g<|ƽ甼Ǩ/UEY^FwbB~FXlՈ R=Z?>%Et?/;]qk57H伦I0Y'KGtv!&[jдtTx< IK0>>ӱR@:ZJT{j Tz0 Lq5;tX<>wm{ 1~8s5W zR RO2u#Z$5 aIecܗt6kcTKYaG ,@@:{lp`_h~AApwRLz˿?t,kV_Cb|hNvQTz^0!#.!eսQᑐD8ߴdc >@ef<礋Dd"FxA@Us|dqʲXCe |Mk"հ-࿒ovÍ7͜£\ႀ۷oOͣS-w~Ĩ4w~ĨJOL@00~9V`nR_vPXwR\9s&T!7{S`῔eiABȽwȻKUs]{@U-O185Vw8XE9En$FLTi<k^ 8-7L s1=b\3ey ?㱔=Xxا(ގ.)C=`F¸&wH \T8 / ``᝔ܣAG. ]\SSMak1Vc?Imk"b,o1=Wu"߭`i^_N $x4f @DDc k(*^PV YYdI ^sSc:c6_nnfeR X; CO:w4C?1`$l #iǨW4~"E E'rAT㢰IٱcX:~' υb[.]Jx5st Fcnj+j<+%mTl[V ]jĻ<=jńz>4+%pY(e:ŜBPb픿e iS}VӫG|Y.x$3ey?xtDn_!>*29U"D),`5g\-jÂP]E~_&)BOX?uU'f&VTl4=-P5ŷ~Xu¯MHIe lu lت>.w(ɵ=-Nu(ċ>\K֯ݷo7ת~uA0nz~qF?F\h<>!G`Fw>WB;t=/U<{| W-ƾ-ɟ8;wƓd]9嚊mIlmNTNyɱz`|Ƅ'љHjjL x6Pֵ+8/>6_h}CR즂u^ٓl|}gf Oں=1'&vHY;G G`@d?cǎDZ.\/^ LW}r֎cbHJ%2MzǓ'<)N`һY#t\w'I.VxZ .ݤtWƨ,89"M<056ʙ>?ѝk.?z,¸tח)=rA@g1ǨX{c:rRD bwqLXO mN8}tSzk/][\RmgruuW >\^>j$p۞(kj * 2:N. Ho))m\ ̪4iY#+ov7ǿ]Vh#|kݺubÆ u Jey;ئ6uF|F;FʉUEhn6nQHpɪ[nҥKSK HtvB^H9!q{ͪж oJyc0kĆmJD0=^Bĸ\~!`-W^mM:nΔR&y:9Cij\2UnOs= ݠ_Լ~A #c("T0d2a|(/=%<"zg8-'lY;.^}J]W_˗_.Y7nX۷PaŊbbb[ȑ#Q&B|/\zȚWmH8kߊwɀ8 A8rsF ,S;#XnCP1``zj3P7{S|yMJSƗ"윐(6Nϝ~XUO3za#4@H*@+ǥJ%2h@c<)ԬZJEed,gm1^]BbJ"Jӑc`%l?; :TPV"[J\\G֙2'\&~;ԋq1qᲜ]uª1WO_z+_|~gݚveʕ4?߭wGm'MM@KFa:tp:; ?.sTa58}Y`jmr\v/_tφUbLG ( P<JJ &q<˹QNzWNV:o6 ~g1wxz12;G5 1<ڷ"=H{_ÿ{( 鉉UlA=')?kaD ˆKE@ӻ˖-Xݹ+Ac?~|>>/^YGTvbj"d*$[sMJ`i}&.RʨLUCvEbfƤT#LH֒ƷWY"GسQL/QfiA,ܙ߿%c=%F]h|)O~}G;y`O.G y^=ewrp- w*~jmY;&6ͦ_Nmod0H5R+}1\?O`rCQNoDnV3 -[DlcUwsYX ,bj=pfaTeU=Nؓx?>C9OM8]E tٌ#0r8>(H૾bX "4kn5rػ)E-10.¶Emݖ_*YE?Y3P /c9==r#wJC9D̙fH㢍_q08ۖUr꣯W${㸼b$lcV̐r";/b9tpTB[Me`'btk׮%ق ؖ-[zh@(rkv+׊@_+\Y8GD B".f-  |Esz'aÂM  P,R sG:ug3@$iG+,4#zb[9)#4Zs͸YvOի1Vk>tBtc1˖譽R#kvwP a\!ѣ£Lg*O2t errl[>fZLUڅ7n޼)}Z:1a g_^ц1 o~ dҼ@w,;#P/krpnذXxaW(mydOi2'Ɩ6i% t}Y2`OQm{_"[M _2"v ] 02 Y GXr{rQ%:m.ᡭ}虷#G6;#d)g`3UL ʷa'A_?fYc4#jv4X&r(VO% VǪozܘX;!wfQpd۴X\$qWğ]pqjT@GtclS?quFP G`֣$Z)Q\bLֲܶqOt̊ĕ㷈=qʍ֫oFY$ṹ9vZ֦b`fP8mu`5d"THL@"wr4]b'T6 "L)=w%(^}utmHBdYa1څ(ͭnLqX IV pMՃ'R@`1lڶI(޽{Z} F,#Q%O"i-O4ecrF6Msڷo_a )O*T0t&xHnԾEޝ?}e)l/vdV87Cqp Jo\T5ռx|? KPg<м| ѧi2t.ѓ>F`&HpRzRSW j*S-"ftu`FAG1><0g9F,qBE:_KiV޵a\L&Z\\5T켵ڝG{,KtUBF<Ë%+ 80*!kq^rDװ-PM-̓{@P冩N2ԆQQIfZ—L4k+5>~BaKic?tQ~OLXR-Qca3wxʖs\E,;wv\5)0XpibE+.*8area) mݚoQpp˔P-HJ,v}ŪI'cp&N`Դt':rPo5@R?3F5^-Ȑ6F06S`kDR"N*HHlf Yx@$H"9ߛpZOV:ʜ| `4kػg/J]dx˼lڴgfשYuT eu X?"y`Q&S71֭*_0p?SjwJM4߽~EU+ 2c)vU݀rr@[)1eUZXLS}UA>;v}m2 G \]Xz<<9 @fTU\И<: Z oľMrAեdW)"^O?ǵR,@ҾgϞeq0lWfPPvF!y`B!G qpݻ S"K&n7 .=#1?vS馓ک1qbUZKZd z5@PT],-nXn~ XmZ+1#dT4]7/ 4*Mf&5Èz9aF?(MV;vzDwq@ğ}, %b$X2L$ N$0 4XlZ(lPMW8Ģ5lQ 0i%{L#dъ}"߆taAR1?8G_޹yH@ s0Džͧߑ2=[~A G ipҥ+BܹS` Vp+рAWג\~@&xvL6zF0).N #iۤ…Qi1|'jEztE"9Gט/r`&̪FÓɯNhyKMP^h?HA?"I!64H`1H>ï%|`h?WQ*0bWF@ծ\SGXj -j0maMC !mx)jjtmަA>)[|;c>h2UwUE *E]?{땢 4_z\MV gKn %\1l`9XضIb:iUm4lˮslDFjre㠖 .o/J#5|=p'#f! CLJ W5)"Rxy Xd$keIEa*SUKJX0FLdU[:`1_=w[=\lATkٔѬ)ɼ@woz޲B@XHg3eG7I=?/&x$<:'z-Xٶ02y:kO evWZG*eFob0<ɅjAN R+ ipjm ixe㨞>gu&Ȫik  ɼSRD$:"|MmAZZ`t>Q<*S-\KU7,W/ZrWWR\wy)[Ucc>1^ "G$((#/N,4b_ &n*z4N-arݫ;?~3 {f40Aig㪾+|{C^~Ϫ6-j &HXShK+d4KiQ#4J:N>R5_2BK:(YE>H@1"h?jeZ]]Q4c:]kk+cѵ"Q8|E6g@%R )*nXG <0rEcq.>#gJ!+lݗR-vè|)-@QWe2Z_YGolLB&Ib11Nd i/@֫ܙ5`(+"+#<WHWM#a)PL1P6 ] C],&i?su=Q5u|"@XD';wuF+37Yoǎ ږ&~<ߊ$5⯙7I."|CsM]zai *щKڝZZGD@zk̉M NF].D4i9&.ΗwΔ4Ii<.߆ˤJNI%D(qΰI}24eq6M[G(Yj}$qxqnLL/dƙ+'N 5,yMɉ͛HQ~>{#09[4t1Vbھ}{ څ <Ž~ûge/*ڷ#]W@qT$[DWɪ9 U\P@H򄡥Հ3Y\PP,K4x>NzHj!S+xPۄ1@4`/f"%PaB]"$ JxL|A FV6 vkfhfϓr{|rAon^au_$g\MťKzܢK#]1؇g PR6\oJ Iw2輍?:o1ڎhoCB+~dIDq !e>B9Su0 YoV&:MCގI%زs< ]D,9.>c䃕P 8Q 4<ќL9pH,a~WGԧ'g{uэVmݙh}csyF0[жݹţ W3Uv3 .Eqs'SEȑ#Q*xꩧjA0qA o<#6_݃u%Gx Knx@'A*PDIG)*!"ѳ?hи9>j(H\Pԁ48-#xAP L>9R)p~E̔iH8:ED[vRڮIRFJxgCax0=XX(jYR.B0.Nsz'_}u;O&~bf&D+4oFmrqʕںYJWE6L{zuft Gr/sF.O?tm0rn90]<ߺmٺaIڹA[%?@a {IEJ*`MȃdUx#^QڃWZxDC4z<17 :r\zJ̞:mNH*[H-R48xRÚ0RWo0A0f bUe3-C_(x,e¾i 0߹x]k4}1| ;vb1ơ-#MiQS 7F{N9'0yqTt]SU\}[?S&kwQj-~zf6hA>E/`M 5%!Y'")2GFDO\MɦvŦ ;^';֍L&ǯY7PbW}Vy)xKp/c%Uw/Z] #]Mvn$"0CKbk׮>oݺPSqRt1׏Uae؋߽eN+WGȮ U},DU[L D.3bL]cD@bdRdV Ϲ&1,S)uCC?u@0VݤRR׏ϋ'kZI,[>*%J5W{rҊ5ĽcuzVk8_ P w \I0-*}gNX 2[7~Ē۷pQ~.?PM#5ldf1jrVZt4m2lb?_\~իVmx&`BL b 㚚axB4&H"xRO,j]YQ>@^ƀmk ]A*YM6iSHtkT$ËK!O#: RuU `t*HA1b (FMT*|$eHj(Xxc2b\]L4.JB~]"P/4eVvX:܍}uYlkb%ٶmTe ަ_/;~՗{Fknju>9rر](N kgޮPᶝvVGvoyL+n+dh;OZ cȣDP@2B% bP6)g^Dy` f XpO uv ^w{hNT28#0"4ž[vNTy4P+m{麸'oTa޳AL"ZY2N"vqo!<$lEdc2iR- %x@ZhAsh.(oJA0pT #i/ V .YRbipLedd]$,R,VtK@F<4L7ݶbhEjCE43,6jX7V.^wCmi߱v `skxݿy:8Y4b`@;XlPq$ej Ïr􇤼SH"ɉ\X8}8uN[ǝLR/B& <}~.]1--m\hU༅\Jz#ꃶiSn$Rn^GÍH$;5"4Yo͚5"24^ɘܸl yε[,wgSٵaqEqhd%&O|\p-FPڵ] tB7HHW4yA>^5u)I]OmQ[ ^njEstoZHTpS/ !ꃱ,`[_^5}e*4&|Y#WI5 ׈>*N#`=3a J=<4'!2~c/#N0d@ȯҎ=P6p]˄ 2_<Íh${5#t>:}X@rd{HnYG%m ztLI .( LU.xt9鮅z \ɰPHx@)Qfa_BK U{T-[pRJA 貄|=BFT7BMeXK(0<Ī |a c_@u8wWzm+`˚F 4;9Y3 u`uqTU"Wn/Wsc+Yˠe/F}˙Q5v;`pL]Q\f2a 9sƜ-vZz 8p[n.}&ߚvcc*}N*ֲL@]F7ctW_}U?_M$e(qv$\a[ 6+LH閸mۭOeEHA\2% G.s#dc4ITL zP¥hqcʉ@Cԗ-W'X!kU=A/2Z-c8 lkٚNRhn ,I Clч$XD%6fpPD0iOg_z4$=753@-?چQb$w #G^4|M?Y,[n2(]zux l2l_{Q_A yƍb޽ #@IDATӫ4=<ߞt5*7FU<29u#W,##(>lmu<[ApWFe/.l1:4lWz1z Ժ<`®jLDJ'6 8&E2 "7Br 9+lxqTA6*D .IX"L(CiU}kV>7ȝ1z~`L"@^8(V")sA+w7^)b{MՈ~ѵiKWojL_18 :z1)h* L_!~ K_w$=T=oV9+U:is87zcy1\ɍ r.^xf]rgXG&KԊ𬷷q\|Tz9Y@5ffIbT-rWg:|`AJ UI/nqzmm!I\r IEicI N2r\ȓRu),j;|8 6i[w`]I=%V5ᖫ=Еz_+5Jq6jpwAnxerD ^pxAbz  =*mWFƳaYhDGc P|RJF1JӔ2.mud~7?3yt,Ɛh*;J|@76ӣGJ,Rk'G|z,} ph:.kul[-G2b)-uUUVC Ul|톡] m@j|YC ȭ۔v/G)lPz۶mm`.O8aۃXooYYXo~M> %J>1!),_~A1*g;ݵ=yXB2nl˰F.CŒKtJ$H?k&iȄ$|7am8Bٲ% aJA|A)cQK4:jD%!$2.JVuFYhCty_\"SER.^*8ktd~jrw\Hc'k)ce2xEi ?~x?|翞' 3*Nw*12 %y`܏Eedx7oL>2wޖfhbۺ5ԴG~I$/b|+Gv\ gk>.bnΣMVtZFyy:lAT :%ɨ~09ވ_#{i!e#l8[AKɒf V8Dנ\$]>^5"ŌP!cɸa@qrKJ!zXpO+D@]Ր&*M˶##[ӧ+7صÎEa۔ ߪYNj\Q:}5SHUz]G(,t6s_tΞ=+~ނM- .ni\L7ysӗ\D_&v'3ƍ8+bgʗ\Kx,A>Òg -CWN@SF@̢0z$E)*q$%[R!h_5WQ0J^ӀGzb*\Ks8z&l,-ޝE5F90ȞrG ^US,xuIze Q&NLY iC]1HC]1Sx|ڊ%[GRnhH .h.i\B+' K% FBp&GR+9QҺhNޟUWT{6qzl\_:aUV_1!߲ud!Z!vQ#trX-!"0QKr?(j) WsC0Qj{GG4gϯ_>wl([@ĈF@U-|^FץY>Qg8ʔ2VBK4u$ݒ&E4&RK7r, m]#Eogh袟֜T}DK :J/wa0~5UQtC䟤=ʔ{)ܩ86K%%lu8rj@+#XE4`" $Vv )<,ELH,;$|9 T5$FMAUTM8C,\ĕ<%3qj4y Ĉ=j;lF'Q~t/e<Xt缎@]$U_40R≗i7jؖx뿝&G ^pXiphb JN&zox!t<@:"ۢ8%*Mבnqe^!Hz5ty*0 $_4a2Б轀!EU*x:礘A6)=*’~ zG:tW )Ê,|yo 1}֑=RrhϗNp庵MHJ=%dIb89\쐳ڔ^=ӥbs)F13նzeik8eb4?_%FYfE ,3;"puyXm-v~ (""K],]1zNm(qGYQbGpB jH(AE4H(1YVN )JADCqW /uap'4|TKP98d]\["kjj ds #ɫ\<0N6t(BBA:} - zTΟnH[RSs-b9+cOP`}Y`nRwM;s8w٠G[>_%h~Jb@bBsG?իWIKx)vP-v10;%/q2]SȖ h ¡GlTl8Y@! `)kQq/A/ahu;hL[@N͐ՑY%9%(.^ϙ tH"#T/0߁+Ek]MqH$o[*ALYk*(Io԰^EV)HA<I Xhl]!hư5@SAT/|?/^53`)XJXEv@oӓcl>|mqMn ]~fC>QY) ρJ0e<~@.Eu'N_/|4#ڭ:?NpP81W#Gn7&`s92yTEJPFѢ b=MËuRɓ hh( opNKij#P40KGҔx ́kU_mCUMW+"N~-tHKAu%O\Amh,WafâbjVE:7LH_A{:n՛/V(]b``joVES: _kb392W{w1,#' #d\?]¿"X)2yLvS:C㬕‰ir z$zFH2G W x,a@DUC1xq^X\EI\,Lh1PF3">eP L[DڭY*' [1TpF9?ꔜæ:wFtD ;LvahsP}g=} 1v@ln]^x\|փH~͏WQ#0xxkyőKu9ATJcعKv9 e"b{-4(q\rɨVт*.,x |ZJ隌3I XjUEIJz  "h6kES/:VW O଺"7831++t^xYicqMOӴ"Ro!-PK"XysAH?+_-[LCKA/$eT)B& +X y-sXg0LYHp ݔBjAِVCH:S`8Z_;u&`uE33`Lfzw-cG/#_QGTv(8 ^Y+cBv8-u`b4?_%FY&GE XZEL,+W9RNK9ũw&jl_+d=eٺ`EmvQva`yL^g^ZYTzUI FMC{0aQՅa*02Mv uuo&'BLJ`R~EU'@@DTˆ;=eqqm.e{a@<`1F%,0O|=IPn!S%4I>!_Rp6H7Sݩ'VW d@u<ߋWn(چ.ۺlH0JNᴬ/]W,}M{ U32ܾ<n4?IT@ldb~\'WnSVre޻eiP2mҭ9'TuTpJYA.Hi-??%ʞԃɆ v l]$}(i/FWydo˗&>.g\Q\|5xԛM], ׁJ>?_%FY&G8y86#0={V|X`M^m3[DsXkEwӜi^C(/zFީLLHy; Efpj+K+gUC`H(y I iZ5 )1QYÁ$I [O$tn Jq^Ё3:N^j"# kV{cQmSf;"⾻e>k񵰤z ɓoX|vf7Ϫkuμ@ק/;#m;\δؿ|̓\!OM[5~K Ct`q\|Q<4]hQ~LQ)HsuWIe>^$hx9uRiЃ^̭=)+ Q7Ր\i_'L\bʶJL瀜 Lj3_no_3vNVɚGŁIC%?߿1V?)yLײE [ecB/P!l͆h"rKQMI) /X=4NL4fQ+-,98K=0WUr lyD- ֶLb0C)nj-漹/UlIb3}o!(\頝r =-r&%L;pQ s!eIbWI<{qb=N2c…3emm3#I1™<ͼWO^lO nN}-5)%??q:΃rXϗ1223>賱 ܐ |gh>;>CԮm%'G/M)-J?Q0';GX6kH+AO8IIT LV=|d2J$0̶qP`)(bF=po ;ީ00C5qir#wJbHq 9Ѐu#){0K@{٣QnfGlHSX>o:߯a^ϗ1ò ,~I[9 / W%\IK7[f09 'ji|?"vөVu*ELZŵz:V>z [Pz(t!t E.^v80O4=mqFƁ1/0ZnLvs'较`(ù߽.~tTP0$@ǎ%@LF|UtY {[0FB;(p ;QCbqCO[-}zT?7U}nfJ?''@iZakPKx`xq]/kٓFyug?et54'O6؎)w#{f}mgnҥʇ[m%I~*~S Q}b)$ZYy/-p@|OMN,VFڐ&icenh8b} j8,fD>iZӑ,cQvie2ۚdA~1c<ԗi%ACE18{)bXC;ŃA8D6݅bĻored^Co9%-ƥ`k#ƙ#wZ-|ZKW?d+~eߑi7%/?4q]O9=zݞf` \ %^`f ؜c|}5<`t7~ j|\e3-?|G>yӟ8qVP7ûV41awW2tŋX bh]B*C}1YMŪ0zd (6(a@1v#]WCCñjЬ:n,u@RCyauMp;cK %f:x[b@|uDe]2هm)5 4%Ნ=(5uQzAmSf`01ֵ1'2"fNC2zak_!\;@>\[Pr@eƺ_sXx0~?X9$~w3C9o|9+I^| nF6/&G }3B\OKW1#e(]ݤpsҐ̷lu-R;dIr,^񯛭wٳ~#C4C(A6d%h#иNfr3a7h9!88@\ :lsm/=)0l -b|bqC[6:cir)[╻?2覑9+.h$-Rjۤ.:Kޫ\Nǘl*q2Kး4d?Z(<]ܶ@" qiVUh;~rX`z5LJ<2w4{䈷P 7 ^4y_P i\o KkNO>=KwnyG1Y',_ԕ0\'CQ]Id}cZVAIt2E>hx0eK5>9FliAUF@q׆mc=5 جϜ-m]?ek:fJ2yxN"WK]9G`G%nTvjB7u^de7J?Gnf7-oVPF.!uP|G3]!uxSeAJ K?|YTy?;sCSu~  pa|9cGfǕKx2W~~ʭqu5~Gsxs?~u$oԠ5cF8*,ҶI(FnD͖hPV.Zt@Lf~bͫ)Fc9 R rsIshHwi-&B;-f 2&U: QԱ] vjX@'!r)"y]eʸaBPz2x̌=Ȑ`T%}ˆBhj^x_\e)֠hAjGhy_Ǿ_{O<,x ÍNu :k(? ,;tZc"f5]3pT?sMz'MPiJkl/. ۗa6jkpІ&@)A-wC/Mn0Uv q팶E,՝5;Q4ڶΏH!](g.Mv^|6 oC!Xb@Z|R7eJ}<?&Pf&|? z8 j式w[ sp)^oeOOhaKzzݎo%d lA=70ċ2? دe}0Qc~|/?=/8՟v0`wZ缮MZaw^3R$H?ʋr,r.1N;Z&yU?$n炌:-ShOV7ཅnpN,*XĚ$j6Љ ޽XfK>N6kQ ǤNZǼarS E M-Èt&y" jP\hT3h4V~䈲+I S<иNc.lǟo] /;5?p_9at(8K.wݎ/͞ Z x ^8y;59ݚS{w},o6׹Oݻl~Ѽﲎ޹惇CN|m/5,J2981皤!j=#CB-9:$ƲdK[\ܔ،Ђ =,zfYxf17h'@/ 4#^utS$ ^ġ6(hm']Wl~Rt ?@[ǾqZΪ)QT tp"D .ƊyP#N/R̯̽1 ] ӟwsr{Qsn[0=_|ͽA. \t.@"ž79.( \=|HNw%;^oq?ߖݼD٨~vvi*O,ȽV'%:H:s CQw[v[1!bߊ{=5ne`|  jeBezs8Ce2[ Al&]d.͗A\삻+:NhGagĦQ}!{5%#9xǃZt;*z#jУPF]Q%vQ2[- ɝdiV4G6~_?n!zR~Mj`<#F0rt;vS9z \E/.倭 G& <&m?_~<#S!FGVҫr:/hz( ,D\:P5:PZA$P 8:wCXdcZHXGp6tD`K63#{o)B: *d{,մȰ-0^kǿ6P`TSk)Mrd8CyA*>))!^6#՞Y`; 6?qaވRB.4\;䴳 |xh#12xӰgaSEz'֎1I>՗O^l>0uz+q=Ǿv̛]Ը/¨ec@ah]`yjf,se|}_?WT/R"͎Ty¨N|>IAm̜Wt!0ts<ħ5߼a,.}u#nr38_Ƨ~x!O]5D<?~GnRY:W?yÃ+VZ \XXu-knrNRy'v\ {7&N ^rCh0o=B˜So<H䵨"0Kx55aK %Zzc_uӀ23]C$.8 3$B(w|SYfCQ W G_^ƈҧI|;v^,Kt +ѭjlǡX))381?")B0e|Zc$8X.~SfGl<9/>@l]tnj1 gm؍cHSV߿yx748m+AAS 7ϾڽLIOZ9wp G6q}m]o^q[]:$_gb$oyrtf`_.^a^F/he ?9{d4,`,4kus{IAQַſ'>,r"aYQ2*)X+W 72Ƃ,hv6`ۀm!C9V?&FɆ=IB-zKD tK :c ml[Z?3Nct3&z:״nsx!`%7n= .fC ޒ P?) O޼1N1FcSMc ؃/ σ<^s`dcxKZCqF3cBn$[Ow2?p~pηqq]Jes%8n'G'y+Uf`W.DX{`}q+|uf c'Cˁ!>K}ן=n~h+ oOhvMF|/)lT~ߧCd>qdc*h6IlZ ڗHGrk\$CQ~$liζ"-r;dEtd+1!\B$v4_U*;K11/`ۤ`EGhi+X 6vQɁ`@>e0e<NV隢p&wRA Io!NЅ^RF?x ^"ʀ? @IDAT@&ZCl8J=h3Af~ 9fw6lxB|lBs׼]>1>P8n~9_ces{){b%<.8_$ @5ןSS>_GK}Wn"&yNo{ Qj}˞SRm;Ђň]<]GRNAz&K _ d%@a@.Slh^ e]Y*Ccr)"x FX{|Go/CU?sa^6"Yy,3f$qirQh @䠂SluXF!+Q29h+J'_ěq Fo>_&2;0,z4%'%:%}H 1(Vg J8/)OK9\=NN?` J>|%7oWW嚁ž7:fi5]3p 9@\;s&g}9gxćN9WJkr- i(s} eo|Wk! 5`!2OCr st`mO0$ Y u2.҇ŨQgˤzAX /4y`חO}\?dI vI=*uu -n-.Ɠ~`BV.ʘ$N04MMKF(#Gբ}e>kdDTNKI0@HvC A ٧qd#WDc9 R,ܯr5@ѣ19I5#O#?d@!z9pŭ \x\w2Wzʬ8SN}񴠗zټƯ߉?$Z[]`CMLԛ&o^-WK{}`4١q&@v[{3Š䴌JBDR[I0O3rM]8b܃<|/ q Ċ\bD{w r?< ce 4Q} $6MeKeecA]:g兪5AQsn'DN9M_! W妄hBƇ$JdK~)Iہ1L[|!_i׹(Ӿ5}} WyOry?$sԟˇx\1kE<@2&."_繃-uV5cIwFIRo?7aoEkz&imC/LPw&88$JdF[P t6;%M6!&CrH')M1|Wۦ^3|a3/K^˓pبݤ~cVwRBUΓsOKZr̔5 15 "z-om\ L yaDRjo| @!PL1%TpC!3$ B%1XL%S]'h:qwKy*ܰ~-!c>-G\N2I؊a|)"ݰT؝~rB{ @8fW{aĬOxX{_8yi7=ASq{ZKo`$uk^x0K)7 bl6t=<_*ۜӑYe= &lcs?* :t4Wx^@xJ6.6j Dom${xpl)6v&ɭ;߿[\8DE(=&cb%; XS*d $M0>:eEr` S:5h킻@Q&H+l {a^~x6_>y1$=w'\z'|x98gv8< vAs^yh+n| Uwt9f3wڏ29x!oK}g6<=}a{wO:@WCZժ\*Dq$; )سF6E*&sVx!LmrU اs4G-c;-l C2/6p N6f|*OZ2FBq`\!RV #ON>m!b>h@pmXp|b\r2ؕaώ4Z_n:Ç!Th]z];fmۢ*j!I%Ї!fb)PDXc}6RcQ|M!P ^~hw·Ň5n^gy!cGےg7cJRoFk? ?Rsy0>ۙ}4K7Z]JF[mO=orI2uhDW "D#|o-d`b{)B"w赒  o0`^6AaQIU3b]vp'ؤ?! `ː[VKYQ]e'0<8/jɧ82&Ŧsĸ֢:`СCɂqt0%Rl =(xQy6f0P E7 (ub{?I3G0i29#hq$/Hy6>rWԏ>7[4k9p:wkNKr o9BusH+f8^lү?-۵D:]3pJR@꥽ʘ9vJ3&z<^O=D`pDt1Eϋ 0ۻel!"'DX :-vA@ *CaBy@u'"?A͌^ }L-#d^-5ylmRS L=0Gb;ߠCKD%A^ǎ<2h\S\{6Jhͫ^DHU4|.BuԎʇMCS(Li19rOhFAu0Ү>:LA`t{y?ny?_8>WJ D9d9!1CvZ?C2b.#bו+w2:[S&F82Cs&dcWs'8Fn nWjʰf8/|{'E~ph?A>>W7zG'_MJ9 {.%Y~ޕ& ͙lKr"pd]wh,4(Z;c;ն<-6a/`3pd? (j F[py(ppo7!;^m?bp*)z (zmȻQjhq&d$|Bx\؛-ZqyH}0LǔȲ8KEZ'yDb,:# rK0% |N@rkv>Mъ%L"w27N:ā10F H;]9wyվdzSxOwE1[:D1;p@,Ռ]BKGC0g8d3.m T愃9 `aTcڻ^ oOC0͐]qB vA!9Z1w>dJƋZ  oh4ܭߧYz>x^|\<~wQ y(y5ĩ0*h;&de[:(=A~jxHAGA-BV0Py,'Q()jFTBJKY߹VT,v6Z,ֿPY5}IF6+Agy̰c;aV\"Y\BhG  MK&vN:ks9@j\5$6|~YǘP'?:hZ#zz+Ԑs/ix䁶b$w4w} H.A\71rT4BTR?g +@Y}"XRpQj?bCWE_Ry|5!{Ӏ!sgpni9sIFWyt#gkw8Ljv#rhK}_>?$o̕bBpE'l$m]MP;FřW:H=tƠMhz롆I4F0ه`@5`B .&CKLlZ\5DQ+:(&ͮp<۶%>7 f۳ Mc m Ēs.#&ZEIRjNQ7z5l!b"şⳏŁbr{>(d0#psL-eT5&:`*Yӯ7O_RmpCw") n/L| @gxdb9*װO~ E 4 zl X%2QþncI7Zyh8uh%x8 ؖ/cݜs3q#"pR5o;ǾyWcׄhj| kdSd_|3oxߋ$7- -8sxbnSR{Buyr@*B`C@5m@]Ub @AⶎaX9pZ0alek)!CI=M( XhLa{= mjB0Wl5>e)L!ìKDˡ216'wE,:R`6x6+F`v* D;} ;<l| RbD>xS8KB !K^~mB=/zHX>f| ~s53:GԻ^[9tP?IB)c]1 KWũvO15 fOԉ6|>sy _hqwْya*fA!U/ڜav<@`6t=Bd!Z:C EhCGuz'pEOP{I&Jqid^,qSl>I Om96 V5F(X0N_sÀ P|.mч*-͟}`\4 '(lH#h %DŽcHpF.,`~BӤň۵IqF#!+w:G`?5?etY*0 M?ס?e!湪mpDy:x y] ̻߈4u[ۍt b8<8csg)gNݟ]1{N#'jk 8&@ _r=\) )`c,FH  -ʤ㔠8ij$# >11=C'9tlׂglcuYm&(#6E3Iulh6_IP ǯT4=aA%Q"J݇8 sԦ t4:o36MBŬ@glm\qO CH*":}@Sh|Ǘ$-09nifqвvw}gso3Ͽ $ѻ>];OntYG~WsN[<?+V*7v՝WŨ{,ۊ_3p C&4E!9q'0}pw\Cۊy#2@C71a&YiO0aJ(ɀ3 6Ɉnp0pW3䭇 >>,C ͡=0,jnrFН 8tMyt|poI/;?1:ЌhCeZ׹ ߵP1%.e9&8PdKH7Aty+# @ SGgeebo[yۇ387zŁ'nWF?Sߗ7Ȭ* m_Gq9ebE]z.^鼡 kY3p2p':^X'x;{䣅)g'%9+@i:SDt9~Q%+ՆbЦ`(BNJ1='q1@@hۓ-*BdJime'm0b~?ŐqoCfR%֙7ڜʹ(ZRbâ?Ȫ}qϱ ef 1XiFY ;5R,(_C3g`sB@3,?l?_8[i.RJit .%!Tc\"LKhȲnY܇z EIɗ }E09$i1*ؾ?x8&+փ-;K8vwyyG\|7^N$۔m-ޖu2p W/z_{ ^cX32 ϱ{|A#j==#\}i/oz4Kdr$|ˉ<( CM1w2],Qy =GUA80e8"'Eh]QzzӅLBmis1?_IJ o[`T[#<(4AOQo˩ [ciƛa`@^ʒT-D/'$b8$b>#D\j6OvE16 NPv*AƘdeGlH ~a`$H%=[4n27m=TH9/w(\Am-oC㥾KfFƁ6S.<Ù,._UDGz~C[ $b̮:eosʣ>h>˰ Ytc+|5/N1yo>b JrcỀ4.9̼@RؚĄj+h* ٖ@5Fd\{n!T$エ1-juG.#SVy4ֵv.({c*"53_4s&(#/Pw)o\BS-Qc#CIpq Do[Cxh1jǛ]T.^;tbA#Z! g0T|A&(8G3qfgC"'@š0& sLv8  9y!!,0ATS;yDLxYk]qUtP6 |)Іаħ_tc|) 'EFAx/# c2g0xf%w >趂X6?jos'')9ABVnzsyb<&)c;.V?D/~կ~uչ 9*s 7?y7&L'G;^/g9y =1{jIu94_ȁ,\;b14.}8@\0Z *EIG*Cr @ ;[:_`%N&QMKpض,՘Ij&g,*T6+Ib.#·e4cE`}GbA M%9:<ȬA/Os/}>ԉՋj]z%®r#,Sb\ac%@Wytmb,H%PHFz@Ucb;(x H$dI01N<ռ{W_'Fr,ӱ xkO<CWkg/pg<%9 nd=M Fc}hd=8o hלW҆WٰW[pNlq*|2(aR(PT>KaǾWƓ[cmyW維%84ح=&G~|lk.'squ$^@51n|.EbXٳ2D&g|7X<LPʹ.n;_>"46Aswϩ4֜fxO-mϧ0d(7£*`[hAu3fQA8.^Z|/=rlD .x=1>G,AH0kYH 0bN[kƖo \844wYAxHf@*j*?#s _LV~,ɥh(e7b55 %DQ?&V`)е\?sC} =, {4oamu-88 L[*"svog 3wdL-C7Tn Ã>f`_4vy}w9/$G2l{jCC0qwk'w&=7ϟ]w-8&@hkN /zZdٺl =g@@|FC0͑Bq }\ tD[}b+$QP{ʻ1?`ye Ć YdjɤKȲJ֝TM1%Ôen>3 >1ڨ|.N4 _W:I+::h5Ҍmo?ǣ"%b:3|M̜ Ěm1? *'C G CB{р> ^;0>Pc/>!^nL܍t'^۠}Ͼ񵑮sf">/3E/yU@I\ S~?Bxg999c~!j]=]ƠH7˛*!pu1*9Bv/9r6&"OC}󗐡R%V /sjȻӼ!7X?P -J<6A[The+F>o\ʄy3$=XM5߹F|HISUq;,&+S8ǐ6Ђc(*?X$vܛ(Mp%j(' J0 B3PM@`޴S|.v=/l^>0*#a2oY\49e,Z(G"b#1 E=#ozFߡ:T6Qfy6/xM- h .XD\G.ĺ? Qu2 @|@6kx/@ 88x h=H`_ؔ[3E9 ( gH%DÃ񻛪wϛ ι33zp~?홳6q'a>/a_ c*y=o'b&Yg ,c?p̻a_K_^޼~S>[s>pn]ͩh-WWL-r`\䐙Ocdm.F6I䣯5zB'|D,G6bc{j,Pۈ!Oq7U[(aI#Gdy8%TV&n @h\f}79J1D^BP2%Ch:mSKvR ]n6?$X5%[~F9x_G.p< |P\ryw1DZ 8R-&W83vAr.N'Pb } 2*NJ^Kuxyc^;t uu߾=v'̷yޏH ]3e`cߎq/ȼ]cl5;3p;P,}{s{s 9]4r>-kϵn;a[WtrF}I<p؍!ȝ [温)- gqmj,1v5|dJ*խ/7 6̎:$ Rذ]'!G~Q2d8uERAKpL$N)@d$aXC>Dr%~bp5@CBCL#xEGN63Fz4*fۅ޽$Ǖ q^hi(RxL_U6Fst+7S{#<>]'Z ///\/Э#8+K}q@ݫ.45Ɗ 0SĜ VFH Su ,ʤwMKk d/naSb-F8Tu[M;#]#-46e `mE !hC7W:FyjQoBo zo!gp: /ms_g1[,GOb@x7"1p[7>o-8pMsјFxj V<[Pn}k;e] Y1*BqdMWza)P6Dd O؂@C "t izHt@r6+8ƽ<#p1HQ@ZBk|a{'8' 6QJ =`o` 0'9k\қNA |M~: pd4D4rQ+y* 5ܹ~gF?Ӽ6jGZ8{\kY3px1;ċF_5י/_6=3>sd/R M.BYw Lc]q9Xp鰖)K yڒ&6Xb,Wi q"b8amÑhqQcwQ~nca@:=RT*4~|0nޟ)øfΈ)APP- h4'B¢%T:@cUD͸4@ G} #Ƣ9DBzM C|8c Yk?>'ރ>F=>n_9-GS}c_(5 %Isxvw9y0L^VےW^1C7-^ab7Ћc3 ѯAp!@SUӠڭ@JSGSq,ʸXy㠉 $Bnĉ?AO% eLyȁ^t? 4Bး]&Z|X%E6J\Q|-_&8h < thxXnSyo\]dd6S4tc„JN4e B 4:g<| 񼦷Tōto91H7=Ǝ3<hhB (CNw,UP~`p(e[2%DL$! M.`Iqp}%,ď@]>5Ϲc^;7/f?CSxC} ƿLʏ2~WM/n|i5^O@IDATw8Vd׌ׇН3w(0A[GMDDºł@r1E 7.5!+UB!4^ݏ| vrJhvk\w}j#4Ia47m'xQ^cpך}$mp7̰΢yrX1-PZL10n4 haǥjhGD)rtLn`huN3ll6=y[٥+i@$>HhPd_'! ǘ6\-`'J/'0پm+ߤNN aOqNsC)ǐ5Cedߛ+K=yc;eW/9_̎=6ՙ͑t9_8kbN菋"e];ΏԣnS(oT)f&a<ɅOfZ867ƆjۗM'/G b Ő#7m4~Hk\qQ8T`qj)N-d^h7QPC^ u4 @E3¦i@&w)Hec0q[U[AH}s&6|Q CM1A bq|NsMi<}:~QY]z@dɯ  b.e~rCq\Ѯ7/w[uU/d7X#:WNy ݹ"Zw;Bw9_C; fm{0<9E=SHH`$*)8[ sLkoy>'mp~vKsrN%jeޙKy.Ixۊu3~ E8,%'1y<5ҫ"Glk''ְh`P|`c;AxNȡq¼Y4)٥]LӛA IRs0PP - Nv: eлx~kVp?4hg+ |*IoA>6Y 6qi5wh/<۟MipqhtRLK^E6_GdM./[^ MR4(@yɦC'LCcivk8I\Q4CqwBf=4V ??9ؚ}ԁ,Tb*x3y+}K_[].0s9?8QGkr{|r/<`| F>_7*5k3$fנO~;Û;蘰z"ۅn"cA^u5 Jј}Fj7͡0WMʦ@BҚKfL`)FZ hx٧Uu']/@˳soN{Cxp!ئ8$,aEC砅(Fs˄8-C0Hj\7o:` _45l`;A#yKcB-n1 RFuK*[SEЎ"HU? Z{rI]З,7+M\;2O,tnI|G9=p&5i{E;>U9Zuk|A/ιC+|C <_C}Lu+khªw F$Q! S nyCw AW<ۘDV %챂""f':6 H saHäJo.$sO:BҼ¬5גSn-lN2;!| )5ޛ6Ba_nj'&5+m&lx8-*I y-I4Ю>h\k0c@-yĀ$BM%6EKQe]Qm&3?xц&z0 P ir2@-5!ۖA&?']"m0c ٍx_nJA1>%bxBxݖxHd 4fcKȯmRd\_O祅5w80FoUX¦)UoJH.\}M&܏a*ojY+t'%?IrȘΑ_9f<$c=rA?lz)~yH Ƈljˉ8 k/ԒB 96RC7PޛmKrׂ )QHJWVwKtC~Cks%$PgT۶EddFPUdWeٶm{Q5!B@:A0`ʮv!ֆ&CUQf@Ә.6rOMGg4.0wA&_t};4DתAUشɿ5 tQ#ă|1Զibt ĥpH0>kC%}KU:DI8rlںӴŅe†ş( m ?a;o_d?1{//݃9ui,v+uտj/ BtWԚ3gIĹ&0g$ݩx"H4iY~.Hi5II}9,]֍ *-0*(X4G&Z@.K`dN°搯Zg]u[ΰO Ε $l Q/(c",-5scm&os|~+\9c"M8(_H'ȘFw)K=،q-y m_O}WұsUqyj4?6W8ڡ<{Dy`e9?ױ$f`W.6vwv9>k_;8I|p{v7keɕ j\ه_R|׏8[~QV(TR:gJJ-o&"OK*/xTtCb1I+R,r8c#'NGx9`2MZeh | ej\EEluim4s$ԏ/N&cm@CK?5 u-"#c4 }VUH/$7<<~G 0Tϱa</eS?+hfHG#O"'S0-pri/鰵Bo, (KoC Bzic Q- JSyN}[y}lھ[j;P/:frƿd9zaaKƹb{.WR=sy|Oᬮ_`z뭃y&*9eNLcݛL >^M5ykt >?|y}>C +jbހٹ4V*V(N CAldD#nP]](p rKA$|gx =5 䦂 햃"X~ꠀF$e!qBami'KBsM@ adixvL$HqQ.bБ`ٷu; Ea-MAqmj3hr8g/c PKVBc as=@B nZ/a\i0(ZC}5~6ab9jۄOwV1}_c(`k IҟWlAE!^z;G/e/ɒ똁Kyo*E]Dtq1_@^;&B/#}o+Y<6'0]jo4^=`);~AV;ʩ=„ ˮE}}$Ba";j6z)h;Fc/8۩ >rI>p %ۮ)94B G } dH>80*ͤ#9chr9t¬0(z䓜N r'_nn}ȶhb|g[ aK4:Nљ㋚8vn(E xQ5țmNKǨ6G)&Ċڐ[MYm@W>иmNYkdIH Vlzdd8|>p} #5ǵ[w#5Zeɸ9zY㿜clIW̋< JO9Yek1xYgxO}F]J7o?2%ow ʉ 9.XBPam:c1tmxÅN$ d\YVV˅v]KtlᓵCeh͵ ts^9ǶdMhE%PwV X@Ҝ7{Q3lJhK9g LD$`O773am_>Mm,hO}d`Hvf6BbBJC|l2ilG 14|J4GyklOW8qNz>q hi3%#{hɸV뒁K{32&u'/&x^sYOS9ѓ)ڣe0qEGO?~m}ԜֻXtֶMx&I,G3ᶢ LxB_ ZZ0WX2_ь+ ƓxN5HoB hF %ȎVj[ebb' } $EjiHhWlZtzڟQFMqxA!p->/Ƌs AlÎ}Vag6$9+?22L^CA׺V8zةSgVf?*I絟:PW\=}cqt`3mI]Mᑝ]<* ]x#8]s2p, 0.87bq3yu a@1=.X٨$n]Lᄉ9BS0V^!m]6GW#/(&[lq3.0@у=.alkkP#(.ߨ4 7u) SPNQ+|? (P,R]'9Oh(-E0+6̝e60jBDCD|6c>Vòg5PWVXmB&.eayQePXu4#iWЍOe|\3H<{tG8.E 8b䥏 (( uG }<¶s {G~-Br =[*Δ T\cxjsr[l vwri#A_ {;Grqż.p p_uheU"o-U>u"f?GFwG"'O ! Y9t97Mxi Џ{SgQ]MCژ:3IСvJ`('aMX GQc.TfFI7&d~윊8SYfʐkCD Pr/e XE rv.Yktyl;XziC?:BR\ Q,Ki`_LbTڐ QH"" .X@")vK&w:oayW;Zei QɾxrYřÆ+,`?zM}s>x>֭po/v^q;֗dŬ@.7~ >If-kJysOy.~):n?zL}^vyMf4 5Q;u[XxRDB9f8l6Avau#)B3kA4E@2aՃ`Ӗg--سD?䢐|t0 (hP7a*5+E~(NO g|gA@?lv+qPF!L`(I<".q(//7\cΡgRQTNmOtz*2;1#'~s!!xoBL}F ;.6N|Շ1(v}>OY;)_Kcqȵn5Fs*Ə{v5>~f_] leE>S'bv S"0#cvL~S/~/5B3e3xw 9Z֓pd[2V-1$8r]ׄ7nʦo80&;͍Z 10/`4@tH4 ;k02_[,#fq nq%^e[LXlm߰2PAG1-2RShލ1(8 @Xj"y_S+p)m&p\"|r,v9i<9Vk[11xs<`#wP%GHqRc#rkۑ_wҏBaC'}w:oIels>XbtP8_=ؗ9ܜOHWc'^q3h ד _N1!lGP;Zhb).ID_!A1#BMѵSsaenB. g!AI~ð a}ia@Bߥ:@#" mP۲6),0&muvjm<Oh?|XBN]_8%K_4c?]Nr'M`tNcM&5L aI' ۑŰ-G'<<Ǵo<~n#ࠔpA<=e֠M.@ж>u_=z8;?I &~)o oKp ǘXک{\p o)Yfr5IJH52 X)|YZ2;݀D8Wp"A!Sם( J[n${]4gv iA8A44&ȖrpIiU[A х%ƶyh0€ur[GS}GD`T(xq]#Gp>"'wP_y|˥P&q&V>[Eиm^jyQ/XuFBjyF\@o.چ: tY@@HȱOF8b H۸??K?:2F hW|NY1bz52[D;7l;G/z}UE<gǞ!(JtlFChZښTvnjeê1zķm,+bǠ'p$al3#Ku1tZLV ylG_y;Oj \`W} rqv,TeoÓKDD'O/X6%X޸s?މ1fb7wc(G%W.DpFeb Z0Šmء c1EM> [bd0#$|J:s tZ, t, S{|Pcphsv,Fx`}_clvAa^qulrvO.vW<\aegxwrI̒ >ǧN}GAN1֭O#>j^D&tYZ A6%ioiǙEp!b1ޡ&*lr\QJ-蹸((ܷq!Y۶%SD .FH]_T.l\7 T[ %Uݠ"ȣvkShOc>,?zl9 CÜ<AEJS5s哎r#^3U_eۻmlm+S昞}Ÿ߸|?hN]gc_űϊt sBumukd" Ni_$gY3"3DzDA&S~4jcIY M|J1Z6ȼI'䚦M$F)^/p%X'fAP-lAlTԖQ=>PӞ 0켓P($(\ ZwPzaθim#AwO1=ibu8/6jhFl2s@qNF-}"a_H\=0/!k0'*}!ZC „`$u,ydͣZSU!ƥI iwaTf%y9u^r-_k}AY,h}(tkh{OWe}pߣx-qhZ#9zuԴ8V뙁K66.l 2uslUf sM&HR%ǼϏ%v}Gno͸` 'ϬsrͿdB/~_r{o VOx;N+/fWK4@ʷ}=JBdn泵M@(f*),1۰&IJg"@ oɶ!$V4 Pm%jcbS@F @u5)azsCĭJAU3OJ&1ŚHu Mlqx}X9vJW~Q\_KT;W) L;>>;ob·Vӽi%@wƁ5.X~qC c+f?旿Y<"?pn|2dEO>>{j,_?MJ`ѢI8ƨ| eg_#HE蟵q t57(k`i(b }0941-:PG!WX-KBPm@ 6T ~B)S.7!UQӫK "Mu5_ln؍}xBޅ*y!Y!c<ڴqPzmD'G07>88kh4W0 a7$qF Hm3)xoH8ި7H^߲lnǗ_T9C{|t @9H(Ͽ:up{ѿ 1~Ax9_d}LV1_&/ !#D_)䫧M ?&2L(3$I= "|6&6ecbMfvEb:;e  i"`6=r5vn-0GaZkVE)_I}lhTQm!ںڥ+ "f4&ZWM01T %#=hdmh`QU4bPB(h#ިi =ajx8iq ke*= lBǛ[!!k1Ij <٥Aa>QUܛ!Wxq}i$܄cbrCfn4F{tTh4 );~k"жe̓۵.l[Sׅn``M|8h-jcw!G<]/j0@_d>n [~n rA_D+\3{KY>N&y ޿sÿ( ;n@XES~0PO1XH "!V'X- zO_}CykW86_u hӌERbI%dSѬqm7 jC$1xbDNYB˼g1YvH=7;;{w;2ضwCF{B]#ݸe"̓f9_E’x=Uw(ޚβc|;Db)GnZן#]KkR6EjM]`SF\8K0EF/!Nhr )(dKRM?!ssƓaWQXEdH@Oh!&j2' +xeqwXkO|LGq)%@XWGe=C'Y0T؅l)aج.6n5 8c|hx0^1$>DkjD*{8# 9 mG޾!dG }Q>Cӎ"QԱ-8)s=x_ {|8 v.v;8(.&賿 N@|s7052"z<џ{|Cu jN ?<}e``,%C\OZշAq_ p fN'V#ppb17Th0u0/麲Z T7u$4 [C@[þ2 $mD6"^;qSXRT6Vd Y4`޵vF>;dnÅ4k!bP|8} s8FSGԥK 2+m]#G1"WTHY4ŸNȴ`Yٗ ;})mEC<73ؓNZͣ6/o[얼k!3V^d6~A \zƉ%^z7%^+1(|qc9sݤ>L޹o.$B։,lBc4Frc_]( ~B?dT!Qޤݨ9<Řdj\ xI)xC AqZ:8%cg__ P0܉$(gȅ  X` iL ̜3#EbxeCKD#6M=cYf>TQԅA:AB9%N}rA8WmƪŶtl3:Oi=X%qkV_z9G!?y5r'~,`tm9j?5S,u<W)KbK iu3ۇC>owfwuw`ܺ'x'Ԓ=; =|&:i.>)剆qaps[3I4/-6KSRUaPKclbQ4wMǰF1#,bg> c/]I2,"-`iBp-0dgH+wj}N2e>{f @Sij?$c Tfg, H4byl.;B.9|Tq/>~ҊTDa>DQi+8'"~3 %%PfqP">j1jۄ]. F t3rOwihHjy#>rJ~8O f!wP4PvXbd=./A;U ;_} å=+hIՕ l d7c\K,؃  VtbF.{n5!HHꉷԐdYil M"|^腌⪃> Ԇ@X-:aBI+D^( t<@"9TAḏ_nl]G3̗!F!LyZ0 h9:,ER j&rՐC㩿,Sv kF1+چ{O77mϞ"rrqyöNX% ]Eݏ|w蓒IUX.̆HI\ADc!6X-.ez Fu.Z?e؏uĒ{|䡨e8Zk|-X-m ٍ[˾Ty>Έmɫ}^e}=`_ƹ]X~Q/*1u_QQvwr?vwg=71i Lʞ=; @oo;wy+?O(Mjg}'?h{x 2Msq)@IDATRj Т;j6hE Vq[:ic5 N߄h.b+l\,}a!MӞc&n!5ٕ{|]~Pl:$(bx8֐ V@hҷ I+r󸅣) oK<>i4H#Q e;oСxi 'a3WC ǖ1föEN ӶH_vQ2a/$ ֡O\c)hA*ăRxϧ_#wcM]Ԩ6sxǟ}|>צU{f*|O9cVњWKy0N0֮5W$W#~xQ4ߎG}\M &I0F1rY+'9MASI`)yNm6ʾVj/i2REX5eT f|i&.zT}u!j6daE:ͯӏSK l~"m;iA%u5̆z8m@R;Kgu_mnܼ1QGǑFfц(o&7lFNɖA[p3H(V+c |]vϱ=}:ƃ?bnag7+E]gxU;[OзIjDwAyT7xZNY=Aө{~9x8ZX|(n認_l^ȱg%|W~ 7.<5:<X:%IMj>N>9m>v dXӒIcK:V$zclsX#KCqv7Q +Y6sжq+xo9pڽ&~vU6ZN򢡔\, !G"=i>/{KڴxtS73dEi5vLm>}兜a+M$K<(qVqh'n/ecS Fr6l y;iZ1J@n[m1T7o_1>G h[Dx|vN*7-/̦@Qh1!9e܊xz5 XĴp ERaQ>q; aMiȱ%9^IbFS<FU)@I;GDZ}z@:mr'7;~NN}7Kcx\Ż3_Q #!ep2iN}a?'u~U|sBPݼ_07ASe%+҇ݰ)rJYѬ1XF 5Mf{6͚i*iZ^&u%ՠ"S:bJkZ5a}xB^~ABUtW9K7>zO~  ُED7p4T   cJ)<-l@"t塌9R$i)[3t|@*8ܼm0Eχ1P!>moD[FmV{XF%S?e =:KvC6N]vR_O&T認/9qqsվN; g_U4ܶ R4}1>񲿊:i9EQ_;a,߹x?c6[!7`cix؛*|SOCA] :I,`*7 cn@l @+ B=`)xE$XBPC 2Bjk_plߴcP]{?b+./-smxOM!0B_4 ؂9R~ K?ܽW-q0þ, +-66uԇϨi~n P-O'*C/6)k>>3紹Pčm:xELPk@Mn1ZE3nBprS rdvS`?A׮tha XMAVѓ!QjUm /T&%y] Yxۖ<X@C,TB_#v)'7UDj ߴ͎u "%|_DO'~l~c~qI๕vQ~4hkP BY#O 'ފs"-6%.΅NWhJvQwp@հ\5E5Gz6 +em!̊.@ Ek]aN}ܢ\XNy7rqx1$J5׍]>Uw[^Uf`.`וrr {/aܾ}{ H|Sqk79? :Y߸Ybq?m)4Q-]*䍺PZ=.tbbO㷙A6AJДhЇ `-n\S [y Ġ]`6``ț2ibPe;9f F6l.ER_±paYixG_m~عx+C48?YB^%6Fڀ FDN?I*I1XƉNY1pB/UQ |Qs@ZA8w y.rНmon=>^L2ؿx95Mc%x1u>+%dࢾ0wzV\‘wೊyha0?:Կu;qc;eM9 qM_.>^C#a~ d3Duʛ_LRLDS$ԸDRP RrSbIm,vHVY"6Ei,_ё;UY:fLb.9~iuf! z/ ΐ$q7Nlj!=o> al}: }KrAӚ N -j@oÃA(۷xDGwoƾq1ATPj(͡QtexMսVA18nÙs $)^q!~˚4}<=~䩺B^^9="]9xD^X .7 N]ݳȮMe8;wR y@z} MlݐC5Ch!fNLu*M}T'2 'puجtMet8vGceeMU̫]¶D kSTk~!7W!⿭7(ڡSbA3?&umCLK [%q+ w~0>"3 бS}kV~Iʳs$dciǏ6JóFYQg <hXJgŷ%̃#x1.פ"b)+zIqӕ ִaЂ7Ȝ+ff;(b| 6"9~?>?׿,6)YʇOo?ݖw Eҏ҇yi\ȫ Q#S4Ks)Cdrъ]3P2p 0v+ (qV9ccݬjq-,}P0^xO dI:9Ɍ!]Y~ K̬ ߌU'Fq:dcC}\15è$G FSͿ,7+ɻOiɠLmƶf.>J1W90ѬO6 Q ,eoOXԶo77_@j&B@2Tр]ZCNC% Օ ];P&0C|taۘfŠ6Eݭb[!]I톴Kt:N`|sķ՟~y3Zp_1xkw{]{辽0A<%vVK@.Lq:t >:]2;XqE>NY1_.]Gw2"UIPZ =86jڡ/b.R9ܲaТS֝=~zK3 )@-зo qB=5-r'"|1q+o`mѵNl},G!8c -LnG^!)# jڶp%/nA29 3osqKqLKOqe w*~yu0szp-TYzdRG]Ɉ_[Z \ "@N#X5/vwq_?xĩ#N*A](z 4 bR mD'C$Y5 -z5El&XLˡ%n6cP a}}(-"ƨG:s6b.ҡi|/TOJ bCYg=xx~G$X>{YetqLs0:8,y" s lhld0j}^|b43[iR|*C4IW$nS%e'Yj @o-xUq?/=ȵSY6qL{.i?^/:̔eK8=UfqrJ 's%qo޼v,&T뽆Eyd T>8MGO~9se֚"$x W>"~ !.v胔>G5M^^Ru{(¦/!6,Z:ehb񂆛^A)~*Oū< r277iy Dx0Z4Bq5-*^x6lnƯz@7jR6/lBKl(ё1!kD0( 6*'wյ!EiVQ]5!Cg0ghZ DMiq;0jMi<_sO `.}@ |eyf`NӆX/38۵d%%'cƑk7N|lq诺LJի930E>6\7P &bͽ1Y~>7fn'rB|7=}0,S@l-*:jcE_Ջ1b/MHf.nsmSЌC 7!~Q_)x' X: -~-$p1* mbd Fhb@l?&$'ŀ(zM| ?~ߗ1/!չX7 d'pRh0*[0%'PK-h"À:㝋C}i/#{ igWr-e>. $N ?ȻE()HGѻO>b7ƞc߮c@6 y'phץ~{x*qBr8A^ \qq"N.}Aƽ븼ou;|\;K#>: WU7lݴKl g_ck5N ìuELڑ_F^(y3oVZT a nNgzH*33n1ב 5'vS@NƶQ9EsFSW#t@hSBm.un|o~'@c2N3I3c =p ~&/h6L`BoM R$ZokAo#>y|s(I"{p/|V<7˩2o"^x91lN9f*]ەU4 N*.1hL<҄5*_Wx¿'Su-}NLNc;ҧ?ܬizmME}zUJIT,"[?@5X8x5db?r˗騮lSf $AsqbQo!f8Ӿ^9t,CErވ oط(ci&RQ} 5֙^/'_}k-I#$>AGJ5d ^siтn#Y?]q2tZ? {V66 m;!vSxoR{r/axwE_Ġwf(m(]{*8掵%%}cgOׯcPnt.n~aqm=*]3pu2(u?կC1wyn_WXY[xI=yl| a*hZ `"HA&nฆ þzC YK0p}mo j5<0Tldzn0fFS-NJ6=R_` >A)'#/._h},/}bn.ƖhQ ⣦MK6Pj$oquwuuls<"kwiXl.Y<p!W؀22y_͏^eGVne?8=2Mىhלl :G']]Z5*,H#_i\`?q HT;DQA k)hzp]%يc([ssO5- B #G≮' s-d7/fN;3$ñt ͽC2篞xVɀ}ހW^Msq~-C5d=غnax_`xumtsONMHw:c9ōwhl{/։t6%&RHFE C-QeAhصlCy΋0h\jݭɺs!Kǀ-Ԇ VI>G蝱qigR .-,'6PyxͿ?3?ܸuxZK^iPKr(MG٦*cE6B~VoμiScfg[¦m ئL4ǎɲv2Ulu*NlGا5Z\<t/pr;p̽q|ƹ>ſϻSaJ^r /L698@6GsQU; x \.x U]^dx1t9m6s9{1w?x_ߦeKH)pn!RS lv21qg2cMA ,/~l$-`w^Wͅzw!+Uegk,$S[Y6uM0@WnDB1Lyo76/?/ʠOjz7>8*b"*NnVYÒ(FBoC0JOoɬrDXYpǡpVHѰRufi*Fn)R0ut=bYẏ`:t2 {>|țeP\#,k駟. c#p| a9Ir#o?V]7 D\]i'l'ŗ~7_Gru& P !^@ 2UaeB[eLz<s|"lBѮP>+HR0{|dP.7'[TU*""*z|#%Q5,beƥΤPcȩȑgp]ZYH2T^tn)unPhנv[9_o~>l.dp>}_~]izx/_pƍa1;V3xN9̜\-%Ym.pn얊q3P?'9su{\ ߟݾKnrx<{=2~_ǘҙ;qe"΢[[woގf}_8>|#!C .C.MCX2EG0-@d[ʪ!.$jS^-4ņ{[h 2)\iq_X/< L+Ѣex??}yɝ|h_+VG+VΙ>c5V>qs9AlHlK=`'DjP< BmNqBJl3M&y蓦_-̑ v!D1 p))z;.khCl\g} rZ]Hl;#x|w=s#_eHnN-ȭs}wO>d^09漏jǝ7BH"auJg_܍ם͇￷nls͑XdKv[ktS4!r ΍/*5X4\bRo@ [|o\² T՞}m D6bs͓M`Za (2v$b٢L6 qKu"0$ؗg(љ|@# FyVÐMHeM v,$eᶶ-Xh̽ s=TKZ6< ɖ8&/L%6`(R^@X7o(-N,'w/rz[+9<J30wfN.Wkk 8Q7ܶ nO\p`UHE,qdNjLpΜ9L<;N)o6ߏ}6Umz /PglsN76w|nn>ͷ仛_vXqJec.%i?d-0&焋kB!ӥVG VJxnz̖g|i_1NwܿѣyM̩ӳh&s,~7k݉_xE.|ڗۢ.YxLP&j7B˺*59$-Mo:S_v k]wMiF.ҜvQ2Y2I1ȥ^hm_rXsO? Gs~QIZkK 9Nz͠>lM I e~ٗ% JDFTsm84ilK~ۺK|ٱn0ܑN]#v'(v&]l [IB|Wɍ{-=7]M=vXϽ윟?>k /N\vjgڿ .Γ0]0/6>{|_S6>nsؼ6w7۩q@Ҧ .#W ;molۛߍm #cu'JϿp0Tr1y 5K7S!8ՆU& ;a|Iq ¸'C zdSјgs΃ͳx0+q@jIs6El 19pCm;ǻ:a a1FۆR :#҅]԰ tqN4ncTv1:k) ݶF>N v=l6|sd׾]lszaN՟Ǯ5/"]ЯUfe//&^|/ oǍVS~{omR?咏qtÙ`ERie,4?z<l>wyt$BXb2^셦rwDkU[.\rMl*5Y dlE֡pS뾠Nð,{n}b~ͯ~;6x +-,r$>Ǔ&X-)%%SM[OڕltΑ(xx-8V6"[@6>s $ʡ p7 hIj4,-n]-q1I;b@DS,KdMFi##P[?Gq({ʜ TK_|q/ˌ=˯݌9h׋KK0;)^bǪ[33p)pZ ]5W3_I)n.s5soE<~J<?ǭipf>`'Ǯs Xklɸ &Zt ?}} ;o/ y\J悖k ^ıka0rK#}lh٪L1Uua0 9TQ<[@}{|ݺsw >ddTTC2u iۑ%(6;o[9 8|eq%:@_h#z8a %[Yא# *Tc8(l9ҮZ]=ũ ;ݖ-A.nS1}/E_7 cc<^fp԰{㹾\svź_NOs9=F29UJpe q; jfeo?ͯE}( 3Gfg͟-؉XR\ Q$eФH56cY=Ąq~žpLDeˊ”(2  ;@c z&SVw:{q<^Сk1:&w+HUa<$Qy U`܀4}Z&@7']68(z~2)=L0S8F \ >2V7h撁mÇ{;85׌6ԁU_~p@7k珞sS%{7nd nA cWKn(О>'@d\ 'S ܼ ˩G hM×(L6$}Q@ZnVZT6[ |yN}Vcqq%8Bʟ8r/4Vm9Ei}"R҄cRJIUgL$ $Z\eNE"rqxaL>V:y+ 1Ab+KMOMR=X}Y_jHmyudwڕTrf3ߦ'jpssZx3`qfJ)̔c|uڼm/%1g5#{=3 'ÇuP֯jU2s3[(>d OB(3/ӎ|BfmTe3[^O ";)0?Þqn(C ʭ\r(sB r6o?'Inm<8S*sbq tCq}&^M``P Ա6iO U(]|Lc%xZv '{O'+p"Tv:ەհ,x7)_怜tܴMxj&sM^l XD;1â[2S/WG#T.N>p`%~Î[5@IDATcTSk<09"\ވM̌N5˟?+SQrnL)9N֮9YK9:saXm\TN`D(Nf\!Q %!%IVgږ"FHK/MUɛ>@"X"V չ{&(},tpɿ˥%Re|zkĽۭ'](iGk#Q_~X[u39ʺ㎩[sSfsMf nS)=x0S8Ƹȗ ̝}r a*N_='oqC^$g>W^ן'j'}қ@)n>tMsƫ7ﯯԟz_Oo6̯h~'eN꜖o.o91 2'/\*'.ep2lR ?LCH G2=h c/d^’ځ(0HՌ:q=e.94l5f$~Kzn$X.Ix5rC cQ:Pn~TT!PD MT((u M[FAP2h46 bJ)TRb$ R.~:u.hH_~t~l{M` ϫݾ(#spbn 3)1ɧ>cn>905od`3p=yjj<;&^{$np<_-gG֦9-k4y0qXgwAZP[ `g Va ^'$ Np:ݒ(3xL0AqpJ.]Y_mi_ggb p[9Ad}5n /JDŽx ϖ,7\@h+GzwT[#5ȏ6q&rU^00#%hHAlyqmv9._ֈ)됚j@K.j7N`7?&+GHX / {@$5usɦO:ꪫf?ɍyx9f*<և4MDza{ 2|<' x3wFA8%dKCț`D Pp::$FK\U*ͽcr3e_3jrs#7H!aqQ)7)Qt觷]X"TaJᥢqh{^6W^׏r_\>e MKͷs?וbL@_߶pŸ \ l~BGXϽ1#N(В-rO;,?5Yמp}ͯ05\}]<ܑwS@c̨퀁Fjs4I`L8N8g!挣&e,tlT4_ץg0Cx09uM/2[;s@b˳ ~'\ ׸Do<+ʒ܊:jQ>.NN11j$sGpr c=$(KLf)7 `W8QYDy5>Rov6r &E1Arh%r`"`rvrA38,v| _~&aw&Gc˰OeOF~[7disYe:WJwga}oM遟LhǾN3bsqd`ߙrw u6Ļw_SO,vL7o'oaxhA^eS)+r&'mt ;eಚqs&]+L˪ @_F:Nm8ehDW]uUVE΋a?ND;0eK0&\ @"sʡU3'fwla] (hB} ))`cHw,x=JT7? QmA9iA؜\8tbx,~8u d ꭩϜ8P.* !9`x(JSة5A% Y kcq_xx?K<ݓЭRp3fnxLq1>|x49+3٦La6ᣖc*V,e lx<&Au/;u/㩚#Gĝ6J\S|^[q#IH BqɳͳG} t݁sw__mz?կf~mfbݷkfk0mYc_8|i.ؑ Ȝ|*;ɒ \1A@ߊ>]w)(Nles= |~Oijf)fՃtje7oC ??u“.VͷxF;Ln[b䕟>,m*O}k5qDž1ƿ \ |ȿ=iM)B&r^2p2Ps|s=zK/UNsꫯ5O\u`u'?p ]P[{;u0p;vߊRdʆ}d'֋P?%i'jI_Aq' Jk%Yˇisep"VxΌsǶqCꩱ Zk-͎$!?$%%+e'Pd/b!T'ϼ >Ad@r'p?nؤԞv m[}ϑۃm[Q.m8$3AO<ķd/VL2iְa;hP{/> w]yp[ N><[ kͨ** v`}A{f#=a{r'66s^;CW-RMt 5c_{*l^y)بW^}Obj|O5'p-ܾLv߾zʸ,|mE{qd`_n)aX-űlQ !r'W UYma1!}~?;Ø~D{cIDr6on셤 Ul4~{*=! o4̔}Bc{u؅y~?mU#'9BDScᓛ_{rdz<W_(ho(6֣RVsQC}N;z! Y`Gvz[ʄ,%ۘz\ N8Q5oS F]}fKmX}S ɸl;#}Q $c!u6ܹU~w} ihȢFG2`t UXVyPwJ߰g棃9Vv!UƿL _N"JkS>v9OĴ\_v5D. pe_7-Z5_[=_} _>'}YYd{&6 Cum@rkf]}n'2cc܆N 1_4vX{LSc;k33ai;~AAN5e.g-HEcmu1.u \Jtm3ZQVaG61yMCDsm?N Qל춇;#vw]t e~_peeXVaEd`<@(Eս|6KyXdKv0?zlյ'6)x|狷nLk6GJiq1Q1SPC3ς9v|Y:rwۑ {q|D򊞱s'9@ϗ}ɫSA"mzl'&xkgJG:qAVvx<51GÎ5>Ġ&><<zo|iۭ oǓϟ??s^-忓"GceJ)L36pHXn"ٶ/~;-)_{b,A.g>}2u~뭷|cRSN Sx% kHz|'w%&h1a;@}d52c[y\Фp'w&ȝճdώb 9갸LR)#2k͙s>gjO\}yO߼WMZ qͷ0lud-I ۥa'S|.d )̔f,Ł_%m-hg`nIHU7sإd`k3Yq]f{g\ `Z L{ͯx楷w;ngIveOXb/(ʴ4ߞ#VnɴN(sq̎H(FH.X#a4(KNXA)#)o eRmR -~mUo*T&OAK7/K~ 6.0'8h0ov6ڰu]wmȰy^Ʋ_eqUc.1ޝ☊&mv/4cI\t[y[7,ݫb [ne/%%+e55N7fs]˟9x큩Gڿб@t,+Ij[mv6}$Fh} kQw.@NNx >2#碍A:Xxz%N߿-Wți-ܜ0B.ob*/6wc Rx0Hfr#RmҎ, sQKc%ˤT 0⑧lN}x-^;> V8}S^[sy3} "2.9Cfl^9;mzøzd`ϟo5喕H Tf`5th\'.[?2;ð';|pah!Mj“2opV4L u ,!{YY]*'VU&B:꣖@|݂IwY[/e8ejn|@e0@A!* 2JN +[܇uLUvp 7ݶꨏ,t=_HӸF|U'u9 AcKn'~(oZ͡ܔF縄8 _p sMOqSu{َL.#ީbW;|uר_20=69 3h la8Fƃg12d!. /\q`M7x;(nDb˱pfFnҲ*9%lgHn%]ofdU>[BW MNvGqLsc;IO-ʖ *” %q"C@֞Nyq,̍˩RESP]BJi dҡK\!"_;VƟ32PcCc9m*s\T<ڷ E=6ɤp |}~omYS I4XKVfG+8A(|ՒnNc?xL݁#odDs]p5oͯxךc+\'EGR3O~[N:#}y5h۞D[dtf-[e'¤

ZbׂKm%Ax5OPg>UzIp @1[J=A !;YS`OXA`lVGl[b8=>a!+:jڎt96 a_qG͟ }}i9c4#G~% [.+Mf55oew|CW磽fM.%e ne-Uv,2Ko\kk|W ĉ(UBY9o}ց O}iY:+{N+.h11&W}Yn!Gglٯ1-;{r2tħtP.bm\{e/·[ x-a+.H֌W؈ BA._ČS O(db\',iKµ8Zh܂Y<_ȃf+|~֞p~啚:^'?qF Vf\c,\(YGz۴B3@'M\Kp0ƳPd%_5]}+P?0fiL 0NG}P8I AE^w5\@ [^D=l1 bٰ5w+2ڜ<}Юn9.:L_~rJtm+Y+'zRv[_oe;/}K4Bj \vYS aSoʝ5aObG/(8>/:Nё tckǼ5[)u>O 6 c;1¥5@" b #wOE.OtPod^)STNaBmi|K11bU苂&Q(8Wl|K>tCwq?S@[yi۱O–mžMhCh[K _F uMߘY*Wwaz;Tc_Ǐ7ǎYY~w4y E~ߜ%ok0X.t=,%O'=r `;;nmюooWp jA*wc5cixf;k>sm+_|(ŕ*ձ`岂az!pr͍AOTQm##@QGYa@(Y<ŸF]KaK06iNmf>3`_I3'N|\F6tePLG,5m=)pWjO%@Llݔ *LFjMr #wQG;m4TÈ|Ii swOmw7^{ бMSel*OZEz|Ѯ6 +sq섏lCc1&[-͂30#w?}GmYQxX2{Wp"2¥aAu?xLzɓ'뮻n;_T`6[Aaګ#́<Ͽ.+788`Զ\gR @a[fg12~d©?gٷ)/\܏C.>`1zec!? Fr >=/㺕BҷN(@*ħynKmsyv'j<T@z(((ˈk6(z 6;%B]Xl yC^Öb62W O~rsVu(jwy9zhs뭷NT#A~.~1/{c{o{qoʯ4~ arTv4!˛n|cf 42ڲC*GLS0#EfLd1b}2em("q&AoNN=ck-?X xd(dV|My"17c>QTұIx)򑒸G{0& oXR[<BTYDƥ /KBKόSҀ q(I*Y^4-6#Jd1Z"JnA5dN.<ߊLko@/NokqWt~[ov{65/ ~15a[fHBXqE/e3pAp!ݓO>Y5<ܴ,{Ҥk;͙s6yڄ'& #$H%\tb$ݝbK~;Cuzcy`5WlZpY_>xQNrq@-YO86J@}O_VR\9%K86ū/Qm!bg t.*78M|'Xۆ`=]ܲ V:$@<:.C#kĞd~ =SxiorYqDG~OZDmy(ڇjP%4 &v-bgl^{{7_|C ;Coq;9/W/y=_w .16`Qfg'Uz\zKv6{%Τ;ڪ-Ipc'5[#4.v e[>^W a:& \:qPS#5?-{."J_sP5,q&8d&V#WaD0e4-7u~HA(BƐD[HQ[+Fh^bt%l)8qG{̉ʭԱ?3p3XV5ӇTfH#kV)`ܖUvN(xcCc3>"c',`^ sŀ.j8"s͟(Z. fuJ|?3SX՞xW6)}7&q.2;5(-?t-خ a-4v|!͐]ihb?ڋ;B|l聥?iC`3D{G7h.{]#i} k8ov̕NBJksbLı|r/Ɛbq2z|1WpLN H4vp1ֵ@SN? /{os3l#ZM.'&hW[ܯXY8ۛ̕Z.]RBD-%'!A>Ť?Sh |SJ~0j=Um `ל߸񃜚x|Cⶡݶ/z-eF%eT^gvыdp6ߚsF>~p߱`GKv!sGiOTEO w(ntmME8J%brH\vʜUnHdqbf]Yڕi$ S2詰L̎zӸ|]AsHv16@a 1t_b8<`ԙwCQK1%/0 J7>;ȴ("ѼTa_iexsܹdoFEm˼Skz$.Z'8j 끴nF>h\Hϟo6C?s/c-|2Cި?מp}ok;\ovp6:WO><컅Ӿ~[Y@N:e#' %NHrGq+&q^}1@-M/O_IOVcFAT`Gp ɼt0ǟ*P%Ci!؁|z+Ko2j=#P8ɛYi"Nq696ҍ "IS鵁N~벤D|0)@J2vX()h6Qh?KG7ySe1pҴj]{<*#tS0}FRBc+\S)}#g&_SZ۶GH5{˝|HI?|mXZ20W㎂G-l:wpr4TSʹ~q;Z?;wm^~@IDN0>HICr JI⢠<<`3Bv夭2er sQ&6ȌaSXsآ@>@B ڔ7#+L_;~Ajc(. :I=ϰpC@-\CCbі uhlT6vjUؓ$4\ΛAhڤۑ6_lni>+WQprϘل̙3{ F<t?qY)9ceJ)L31n<_۵^$K3/ND'&v|u/e~W^8QgC-:d6B+ C-"v6W=>25ЧN13U m{ryU.buMQ &~"aXd| 7E'"}P0 , >VADqeء024| ^ oz<ц>l6+!jx\bh=ﰋb;F'^xyyx7f:rY5C_{araQ,Ζt_q8#46Y˘4ˢ_20}qCLgҏnb.)\W_zw{2'4Q\+½pl!4*4cl;&Hqrf~NFդ ǰ~j1MHQmU&O k0]*›Idȡ898[qcGO cИ8`25Ƹ.Mq|\[feL*K}5R* 5t꽆>}8SoyW])ǒhb.x#D-<ı1]Gp+f -8 IT=sas Ѭ% zf"fl^&02nK[2P=@vN{Ԭ f_dW5y䑪SNUa@W]uU\Kj|ag$PBeKꪵȸ П,,$j}8\_ǭ.e5ӒEFK.N|8Srh;?xdbmRj8䇎A>S[[m|„qB5pS3E FϤ29^[wЏ1_n 8ӘƬP&dV*'W F?˃Q)"uTpl8,ܐžqQmv5}i^qA$':\)Q?\Joܘ},{y~q?Cu y]mOm֤{clՌec?o۶Ÿȶ7)vy|!s,+$^/O8p@5xMURo馶hn~Km ?Q YkяJ|cwerr9yd* 's[jo zln!6T/)^UQp PԠ9'kb¼i-ʏxbmjPHW}$~9Р$kA_p_.wX[M80i:8C;]jx:ɾ>G цFk"riO&>䅞,Q9 uh9TU|3*U[x%$QW5O>VKs|ueӲ}yZ7 z< \X=~]7q9F4,2{)_b\fi/8 Iؑ,5a o=psPSS+*UiskFgcظqm8%-Œ*\f\I86DqHd1nB*g4ut6%$>Eb 屁Cm2R`M'''qvUEXM<x<i1˘{f;HmJ"cH7tNRaL٬=q'y<)ihtGE s _N1-/@߉44c n /f:ir8fb9p5WP4F&ƍ| ۊ2DzfO;D60m)g̛g0CW$q.qA@IDATg,qElܽ'0~UiјJH|惓\x{s.g}}kާYMǬ_|P_/;e埄X&e)ed]I@`Kڞ풁 \/[ҰG [6x|С^ip m~aݚ*`M |e2{ mn >%97dG<}v1'N<3B?8x VS,NO qr1Lne 0akoȓƜb3s8J9uұs?lΜyx"Uj@ ;o~ Q['?5زHJ9I8=`vh!l3B5f "XqY"bkX vc_'WyāG?m QqQ6 F2Ыojna:,XHN ʢg^^)XM]_ݶ~>Ⱥꫫt]A_n?+%͜pq718vϷǿcJNa[l]3'2)$!yvGeˇvG}Q:O 7xjy'~jbp;um\Eqrw?ڜ=ۈ(_y\s6n&Sv.(EyP65Y2A'zՆi˼+"KyR[,!'dE8 M C ŬsXxs&S@"}5rߪ8qF5E]ةjrK6?/Ij ? 6)^Y5I q얓b'J8C'GBNpI񕱒Ofg\Ct H]T#I1q<sS˓"?9%ͷIy߾nRW\m*'s_y96I,sX%%9{eMΧ0SzDT)#qߐEd`, -nukbX^<79 ,#1GfU?я_?ܺ9[n//"T dgg<-VC|f_ I)>4=6,#>}UE!gC4 .V1D 1:x'Ty 8F*q;1;[`va/Jq@y[ox98 wy&f8us0q3 G|pVZNqdKkd`?.vfݶi/yAPQuO>d/*گhqN\U/w۵Нtƶ2- 8-:cMMyA]xy*4 &mƉwЫ<` ><cR }xA1BkLp"C tW(Ԇ@`RM'&ăm zW_}]~m9'088ОL^>3`.Yl58ɸآ>.8Dt27R1H#BYpgyڶ+e6g*rg>Ɩ VJBlRDN : xm)ч r) o,*,Gē>OR4?SΑ8i3(ޙǗɘp"($.hA~U`rd"d>-94(H#=5;S}ai VҥX{cI B3#aPǐa;e5c6]Enj9;+hpp2T[[oռ}nזoϵ*sqllǦe'8j5\˶z3mP6fa؃/')૽m_2/3:otZ4X`纔Qs;;?z/?zQ@ <:Il smAͱ 9wұь'2ŤADGkXNUi ŚNtl3Mʼn 33 )aSAB'\b .&0f.c/78TTaqm7( ֕2ܜĈՆ>Ϲ4/kmxG'b&U-L'HO@ h1d {^fu"cH2uD ܇$Is(B}G n!\-?sϾsToLMx-9 Z^$m@ ƱSSqı5T5 f|GەKiڮȖh B(eu9?"'>mB@Vg}KD?0JK lU,psXF"8 D>o*'^;9/mh'0̈ U7шr֊PPǢы`x\ :RQ(A"hȌ$ U?>Έ /qe$1*S"NӺ<\$-D904%.LKXdkfCvqpy4u:-McE8I<2 HW&\6vg6QbwiZ[H]*-2E*Kl\b`[Na+B>Vh5ɚ;Q[5&S7'bM(FFj<0:S|xӀ(O bۤ,iA#r;`{ol*pժ?7ޯr3<3D|ۿZS#45S>ȣnǑ|Öf@ Чr3ERmQHK((> ˘q}_|^Nr 8D'?!Zw7x&\奷5G9_q00`iԞ ω9rH5}*' j87L2C;+5)?t5c06jS/'%>|ْ.6逖|5?ĝ*uܷMFPGCe6:2m젨MsǦI'e( !S[}?L6Ptf"o*$n Z1џƢ&6I7U«qj_ݹ-"3l,:>' ak/͝H b-'+] Ac#`$`L ZN6 3lN(F2H%kH-NL7`q$)50 CIO Qh>˞zi,"+Wuj/|w}{lD yͫ{Z`iY8 Αۃ)?s7刑wqQ Ld`ϟ_B^ ^K 0<6XƁ֏cRxYpgꫯN1\wrI}dyey(A`< #D\qrelGsʺ6zZ"D*N:AjN}hn091eq' rd;sX2wNy΀F"ySAQDb|y2 8g G|yK=ȁEe"@o2.Wͽ*l<2|;: @d 4͗4?.# '=9E=>m O%8J#"Zop;xgp[4WP U]wu^uU>Zgpoˇ~3 m(SRG Ǧ>ݒU30^eUȩtLk !{4tWqO|;T{Toپ/e",?~ioܙ=-?ApItnMkI8䵵D<۩N{is< Dn ƗRā[z _}}sz̙bݜצ5Щ8.6c'86q4[K20M`C;+/[Q|XnZaҡ쮒эTv7=w>2ή%\Kf5\~ͫj ٛ`gs~?_5!7ӂqC5_yWx39|w+p 6m')&#Ʃr)/&^4ƝiĔn QF>SmP{WP|ƋTZ4- I%,cѫ$vWz঻ %A rw#Vb5jo+ڠ8r ~Uub q+$Ud>D8)W~@$#f<; vcr`00֐XS@9(mlJa:.57y~*P̗>]*ol|x}EN/x cK} &R X>|FA{HOOcg1oWU.[Ps'R'=<ӼPfd`c9 eRk> fƉDI,Zn^4a80Wzky"_K(dʧV2 MȬ*MUӣ;wyfxPi z`g T Z2{pe'.8Z&2ܢ׾XW9-8`t?ʩB%AU]cHJNQZ)PdAG BA<*s8?\  j^y啕oϘQ򺼾ql:☊&Y` t3O`^(F:c:`僭uXK['FWı H03 f!C嗅# _G>P4o#c s 䍲,NPLf(N0]7.ZOwqnPm8*.-O÷`7ɘCN\$M>Jl A}rW)kH)k"/o*_Ư7R*q< QFZYʼn9?>'P 3 r)ُUf`(UMf:>鈁.v42Qc̦}(eq+Aə;61x?ǖ{_пUy2yx9UcǦc SSc1c_'8G=avzohv,}9x!p \ *oFmݟٟU g[9 nsڽsR6_<4T%Egtc^  MړiZfJoCaN=ۆ.9r4&v"pI> Fbr('4Fꀽ*Ϙ ܲǿr~`Ѐ*p#PxAŗ7C uLhy0׭Np7>6үZFƘl)9=afWX|C ֦?d[5`o, }X8$Zc#:Ed>' fR?N {i˽so?ohw_P鮇w]WFyG~`><G6- G ׫,YSS˭&)ѷ9z,(3/N`H='ר:5O e7W|D.% ෠qs G}P 7ߜ1ٸ?i5go%<Ҝ:rǑ>yG΃L}tK{ D :nJy}y톺m@ 놻1# >- EN0ЯIٗ񛝴6N &JdC4Z\ޤO bCuQ߸ 1`/7f" s's`4ߠKgˍvM&0C{f9^:|-nˢ$qF ƥEƤ AfcL"(sd .i YkE\-5x*$'d8X;̬;i 6G T.9vb/d`_%Nm==lxX`);+ޡoQmm[KH/џk6xyɧ?g[_AnUNBl⛟2_qLF_d݁혏 b1f`޽.e|p6G{y_1:;G Vmigz|[桇R;H]Cٖ'Bq+n/ >p?f|x@)9$S$UXwYOD,ϗPXbS/`"t8kbҚbiuq&q}0Ɨb;4zXbN ;<#2s޴? ț2kX<  >.вg]+})4&{ |}|BAWKr`T$T3֖`&w/sSj?C8G.,P _ב7k?|y,27Zȱ BrT}NppR!.m!Ơ˖+ԃ$ ( UEͳ₍g)Nwn̘0> g;!ǦXX+X-T ml@)१\SGUԶqԷzIn^=N6r3w}b}K馛:KG:->Gu`\'|ȷplf'8|L1)̔ò+9]-KVV+A#6SzϾJLڸ3m9/mw ~Vv?:)}{ͽSSOƄdSyp{ozgW5ߐ)p%T``lKD`&T?->յ8˟f"B@,'sXT7Tͥ|cJT\MU/qٷAӢ6h_"UY`Je&Y8N)4vs.kx3ƬgX.0w[[!,5Rb"@(/Yeצ+ i)sd+1MpyGmD@fF8ypq-n/"SvNѲ8L}bc̢i~V9f6Qnm~C7gzazN$F!#_ ۔X6cěrLٕٗmY[ 8 ,]L"r+cwv}Or9*A)rum-R ~w]ݑap_ժ8Y0=x8rD~kƂ'r=v:/sGmۿ9 \``-[FR *DxjGDKhnyKLcuUѫ/& ą8]+] Kz:ՊK@j%GC#HyYPjWRW)ZWHČar)^cOG>CZ䵈5mNRNm*v bu Vϼ4/Kj7*|s"N$-a=X%=p_YZQfl&)̔Lz/o|{ՋpXuS.%O9_x}lsdsZNYWv?{o.IqF{7K4[/44[7`$@Y[F/c?{7}~ Zϖ,yl=۲hlڑm n^}OɌ[UUU{"q2+2NddhT~FM)JFv==sz7.H|A fe˖;* s . HǏw ~[}MU0˽{`kߖW.rm/Kvűc)H:DV{N,*.7]R8G;'RZ Lyi .d$WmWzk>//S6jhK̃ pnBV [b$HTP1p=[Hz/41h/r!O2FE%|"Z_JGQ1&j W'D ɰ=ыP憢/H夌A*QFnB^̓<:3Cr,;ot!'ן/&P+;b3)S{;no`X[wN[4Scn%#{Њ#&_eȶU(}qYmPK][[K )aLN1*WxoWŜv^=<P~}sO(SZqv X0!GaO,&HI <ÅO >=e@6K2\+k օ6D6%vtAon$is-hY'h}zK疝8o%їXBL(pm)35k{Q=e(vt'E[?z%q(F֑Z*hz/VH\5䫕+_uĝouVzM h#y\4Js[,g)ƍ@\5zݵ^[<+oGP8c };W͌e|pZּB7l/_~ߥnꤱIe=F}  u)U(o rLɱ9zjσuFG4) .Ʌכq{t 0+ ix_T[Bl ?n,1 @c5ڟ)TAJ\ƀrFJ)ޓCY+`Qע|yR @aGL[͞=-Xpܙ&f\N\n=|=@ ٟ\/ZH,4/([oU[Ϝ9ӱUw&#e/~6% 4> Ri#p4J:T4LR^ Z0 GB⼙^Oλ YZ@I^ d RPjH_Z j) z3#P* |x#8x SzEx+.cDWIP|{ѥz!$烯4@Va b3f&[/}bahd~i9>=z2x*C _lo0Ԅt۶mN=jB1EaÆ!ǫCA®}Cb*Yh;lZ^[ʣ)bGdRݢ+6;ƱYeP}rנo}kA}M!`1yt &HXH*3͛ϟC+= Pt&ܴ |h&wu]&d@{u%A}nLPh!/x>t@Q^#Wّ$`ӔK~z@01F-$@%/}S}fD$'[K&?lRr:HRDX\xG͛T`S"tV ]?AGIDn]{R[$ 7 p(L3H xͼHY <ʇ5L pFLmшFŹngbKQ[?}[8{Z Ҳ"xiDs!iƫv3o_y[)BcSFsY{S %ym-Җ<wʐA9FT~w W <"7p\1 olc1 :Ncd޶ MзzשlV{v3"3&ynl{?|X%/9m=Iҥ2;^I@i4&boCjMleS*ɽDVoP`'`U@p %[ޯXy[xN6O~1q^'58'LDDōL{DEG0Uqz#"#UPDaQ^X.! @@K4K2 Ӫ';IH?!P.GpIc"¯:pRe"ۛ v^߷d}k zi|s]Y:'҆">-Edd=S%iB)xo*׬0tj67߆FPaSXb @9m8MdtZjqP2k!-B𬿿P̤|w>xȊ0_kw?dy*ζ[[lGpb ;+s6MWzmi l~Gg?v|Xo_Y?3fr>V3ɨQM:Օ<5&w~w}IB?n?]^xp[j@r#㯜lu^>t2rWy SdL 8rEXؤdKPlhQP MBWG^W^yV@F~zC#mS,׃SxuFd?FznMx_Y7p'e'VW" y5+Y ,饲 !`#;Huuv(" zhQ#lf7cx:d GUߕm'=}.(qکᵀ,/QIe ډ;gJ\J_XKG=%F(ck{cByyzd!V^ᵊFNX`,R^m6zYh@kyzZKM3Xc9@x(hR RyBL"`j !Aؐ#cc(ėڔс,J3A2WO1}Fb`WL)ZMu&C-\f2tXWoӡӧO/&|N}__g=(-_#~+PjlT2nȶn`-)/B3Ps it|5]e+Y䎩/rFtc cڜ #O 'K}Lk`7ۿug5rXݼ <z2aPn„ ]y3u k)bF[47lwW=~DR1(J5FAO%%F6 @#,4߄D:< /tc2-Rс.,L2@I{P|F~Jt\լPV*(B /L)7*/_1y,e:$WU.vLiU_IBĩ/;#`%@IDATo J@LI1k^\?ҭ۞UN_\:-sKbÿiӦcR%b@+sKw݂ATzxQ_&ҾL_7^oUy06Nz!.A?pVy >{,\ rnLKLeĦ@,ɓ'wd_z׬f;½M^xK Djp\r=/;̀5J^~LA$tڧ^LNm2DLx$?HMRIjGkA~>~D3R 5 d 5 qrM6تHmȊpl7 x> ?dQ+T„8xR+QF3oT<@}1tR&_W@ZL=>v҅ Nqg'~4x9o5aSӢORvs=ٶ~[`Zoft*|܉z叼c_Ď"2Yj5ϑ?fLGr1kAUG{`'vmj3@ xҮ@:m7Y͋x饗ܚ5k 2; |SLWms8?Rk/ ؒpHC'-U=qA""^)[轒zRvyN8a {Ţm ^O(p:rSTQe [bh% !@y_H/{cGZ~4lk U2T$S-|E)|ftXaaEZ)0m9$U^`--V+,0-D@G>M5J|9]7N7Xy1u#ڿmזd8 QJFr_KzEg Yy]J &߷}ߝX>?0atG(aL ?C tV=½ql%~o% xi6DXmڴYL>fz9 E>˃7go#|%#";+8),F%u hH^!AYÞ{9^_r¿GNI}fbK 6$gč;P)*?6nܨGF9b‹`cwj V<̎^{+FkO.Ft_ y;U뷹%ooN>)_ }40o4ZnPO%<JTjF3AuTO7@ Rq-qз|#lIi݂W;ԮzIBˇvAnzIa)Ȭ3$RE(^! ѝMټUss9i7=Gro]kx/ԝG77\m5de'v';wɎ(BL<yД ^+n?Ŋz>>6v\2tֽfUx X"u>n=jiGrS]&|Ĉ#3A&2g8$ ldu~O5*<Њ|I/}Ifs+(-tNO|,z#> ( /2,i8qb+iJgÆ lh*rBZv;\+7v\u?(!y29`y8Bp[q4U;Ҁ{&X":+yb-L(Cf z9T5Lov#Ԁ>'Ds맽 - QET4 0!Ы\_⃧5Be(q0>%x:>J͝;Hy7ݴ(3f(<1Z[EU>[hQ雱"K/4V)r*yxh(Y?x}_c~dFVCnfO-M7]uPVޛpgtFj$|')4TǢJkw/kK1,#-Ay܂ g<ۇ݄i`ygqF ]}1 `τ"1o&$EReW"!A߿]twgɳbP4C> є= ", X28c;Z_7OU|IgV XaBY /> vuhVuU TOi(=wEUW9bD*Y`fVVaD1IB]CPد!dzej?{s_9mjFK0O|hL L~!]ʀvݬMt2Pk.myIZgLt }{[qw r`;o Xnq3NXcMoS;>,B-<<%ѢaV%<\I,=3lb[ Jmfç=(UoX?2< ˍ >' SR0Srg*ި,ubC iBO%Y2[=}13O/丣_UQ3bkЏT"ȿOD1*Sdw[_'N3=Ԏ-/;J;oRvF:1Z[[=]MaN#,9%OhZSABS`p)&L</X^k@rr<<> > N@J%RPJ^o^tكV%T$`T׫(_衁jmhW4`VyJQs/F"*Ю|c#+uw;S'ܕM[Mg0۪z|{4u*R^ 퍛FOeIYb8[SCBjs఻%,gI'N8ܹM0dٲeɃn:W;j/S$9%턀(C`ݖ-zv?7sD*pvhp1C(^"S!ytg zQ ^;2$kcF Pɍ SaR 6yD*ד adF-ۋ` TiʧАAmDRQԮtkz %Wjxy*Gq{roa]͙r挮aⒻe'vW0ŠQuL^q*ȵ%&W[i~]Yvw/K%p|0nLtq^/9d :.! '8Oʁ({tT` $ɊҤpyaA4 qfL)uSQ!в ~;"{lwXʽHƍr׿9]ŝi?N6O?}jv!]YeL#yxhe腎2dl;vyxէ,t}h G/Hߩ/c)/׷r`]^h DVajw_p5YlU;=]|3^P_sai'Gϸ}"'o"2;lrk~= mB>9Nr7V_^H-BpL,7C(k'hY(kmҢs5{  =zK`ld5^G~BَMi5؍ 'PFqI`$ՄDQ9i%:fQ"1JO|#=<ѥp̄ T셖l<'2 qJLQM*sܖLms6?8ߝ~R:X!FQŋwm\YȾFy4y~1T I!i7]S,^ǥ"]+ږQ19tp /,+۷}ӟvK,Q#`z{ѻ1y.v:wع[]Nf̈́xfIgy(S'#wie]z}EѕݭWqO91 a ;نP"uHP;=gL/w$NX H\"D-L"ȟ:E82PQK+ ZO'X_T'@rl_ifHo{Z+V (U 򯪀)Z[@k2 ӫDVo|*[krxT6*?-~`Aow^4sl4}t7mJN6ImUݫ*[!z8Qf~wO2rN7mOI20pq/%%R9 I>Nz @jGyK qڵ}Sr+WԖpBNs=m޼YePg;3͞u;anII`Ook3fAn'wؚG|s>W{屔ߴe}e_{#$c=@,:|99w`(^ gk1`"/T \߆ ew$zZ[ L%xA BRh!UԢQj&2gȵ]Μ0 Nx ~U*(Z,NRz >NhLR7PyŖ}/mpO, w3,)_qVןr6屢n$u&IlsϵÚ*o餬Wˣ'S[> +Hrt]q#tʧJ jƟ-JqUiz#ߖʂ>m?qfpQo7Y8n… -EeA"$l``2VȊ|06ql5sLL(a%@7'wi3vnb2T@ppe ,ʎ?$l`O (  arr+g2X!z y-V3z8.I!V`22( LMF $cSJ ʹEV/D2j0 ֞D >L՛m# X{ ^&(N[e o<'fٛAy]偃-]B)c%NPn?Vo6EsV {t#]y 1v^5ӛGGvM=B?1!S\!>3aTk1,B=~@t/[Gab+lXNdiy-cX>8JlHԫjI P>Xi[ 6lH#KZc۷owWGXʇIݻw$n,{Ln uԱ9E{A_x[LwŅ3(Mʐ=߅Ad0)ө1^i<>Y0Ǎ^IكU(}Y<`8x=e=)VJ$S}%*mr$(ä}"*lK_zB6Oƣ|q lRQ~4KϮuwHtp4;?s:ȥ}!KH[n>WvFⱲxX[1,.ͣW2br:Y.W jK3K"^Oe2'];YqtK T=:PH~zCE፶'r"tر}Ν'c /˜>4~ԨzK$MDb-ˏ~$.3ʭ`Lϳ^ziK;Av S·)wzFW=xI{$$C[?fH.=dnΛFu0ns2o@Q$#e7bE uўKcxN`Lt"'#W-2buR@ IR?6ѝ'Hk2𾡤A?L_&P)̟  9o<Aۺ,wןݵgcBRb/b}n7L¶.]ߌk_?έGGTMPYsxp2?B\P:ׯ6RjyV(g[2>^*@tmNoIGbcgP#+վ"Pay^zI|3"Re]W{/QYiP㐑HKAb0U)ᅐBPq`R 7872͍{zĽrٲ+d13Z{,'xokO<ѵKDv@BMxTV2/7qMݸ>$|?ݽtߙKH'*ޡjGNJAV@!^C8̯q`mjiko1/6a?8Fr2esNf._|qKwo6Sfs<^Ǧ|Ir^cVr׉ӣ],y'u7rԈߴ =<{$o> h:r`E/.;]4wꉓIYI(1Py_Rj<_L5L$w62;kiT&v ìsE#/1MNq=[V%'@wBAG95CvܑpU?IG%&zx,6tf_~5!Nޖb9ږjeyGug܍creg}vP 6 +b71?Rno9vX^Ӎj}ML^;ҠIܡG"HInA&Mye~m58֑#G+rz=wE33OǙ uK6!`I˞,P!4NΣGA7odd421ZBDԼ ӟIjH@)|6d2y:Ɗ&`xRŁ:­{k{bF-2A-L7wq\ZZ=uj1gIc|,d9}Fn]#<<IF:r FkꍂzF/^9H`0~Ew ʱc`"WoƌWw{_s9vMָ;Isf93;H%;C@yd )x/Jw j)CI}a06$hOk)=$ y.!RnŶlRWS-QUʫ8 7$qW^WnusD OuW{RϬN4Ǘx~4nŊǮ|fT{ϣbgvd_q"IF81mZVI5)򮻿?`(cKsTaO$~v6qȩ~Juz'Mzٌ:zPַWB BMXP}Ϸjnn8b0^}tyi m呈+BK]]5LtxrL9Mv}v#ܾ~:?xX6L4֝?dSgSOʤ_ &Izpp+@aBH O"XP2Pug:~j # ׌w.!*Pցj˱'E/& <v=^ym{A- c| +7vd3^%+b[>=E֯_d[ǩX,#<<.Cӎ4UX]?T3]?c4(빪tyޒ塖 uéD\ dV-xK Gz*2R9Гz5d  _0 v-R<$8+owV햮U3.9Nr!;]2P4Ao7(koUbKu2+ w7]UCGd⯤Tp0?e`qyڄ!e*Ri5t/]ekw*բx@5P(3{7n\ǯ3f~晧ܯʯfK -͞=[7z^n&-ȷ \YBʀx`Vi-GXF}'7μvܩ1cF@3ϧ19^qE:iX>8=!C&kzmݸeE~볫рdenY'*=9/si,Ch3W&TNPDbFפ"ڭUݰTQxV]YDTSARA#붹ek8w"}to`CUI;R)r!&_i:մٺI 1.aq=-ӗI}MonL bIF%n]TTT,>rWX+?Aӗғww; Bq, :8&#xuyΜ9ǁ$3P4E>փJe˖5]ʄ:0lA';묳tCn&Oh٢y(eLh}Jl g7>;?ݑ#ܪ#'o8aRY'ӦW ';6pR82MyG&?BM5Z/4~`R20SXa/x^C̣W2q@kKy:_[8b6IIYFw̔*lNY|}u|б /": nZo߾ͽkr3k;=דw1~U#b;>s,rdyr[Άylgqn|`haW1 `AxỴ̀A>ˣfbtwCW,M?x] Oq:Ke#MǴY޼}{u9qxwܼ3Npg͘Ǝ+gΙcrQ&URzX=C<`SnUg{<Z;t1dj~#՜4@#bwN?+2=CvwvgNn|+FTXI$g@&jSљGdiw[6^{c2t!.ɁpP7Dz۱<[eeÿNZ4?<𶢴 t]wAJP ̕bk˫Ntߩĝڈcs̙%Mw"X-sO?]J ;<]>n..h ,}g d 69q`ϟ?_sҗy;ewx<b[7y$ 馾X-\]94,>U wL:U(0N6|$IWeմגRZI O \a"QؼeD9Mj͑~M<*E÷q^fH/. mٲT6qV8> &ltY[ >ji;<&_ɨq0i'>ofp:)Ӿ?w=<)%\ <#`PbHvhde {; w\p1nnU+,uo`N7jk;XmƝNhr-o<<ϰy;C\.yeK.IW\ Mx+vx7tAowx}@/ӥsOp4+w_6m?!0y;}d7SV 6}ǒ/CKNx9>ggRMI!]AI)I-381B/U: #d)wݎi N>pi}ƍ)gƁXvvR!hgP*V#w۶uoE}7ߜh:JF!}c&ʎ}|׳e&/xQ FvVW1,z_L5z 6=PMx|c}T~?"$og!زnݗ;SXg…,aNU&73ig%gcqK$/u;ʊ~??*6psQz(ˣ#>&_^(J \&k>}_^^=mz%ɞR:ȎpՔwe [wfr7CϠ@?~K.qlT$%)ulΝk Nn"(O̤NS K@C/vgWr.q׉7,8E_IJ;{Gd< &&&3nw"n߁:EpH,'`HwA ubxg>CS ǩ`%LwN?3A_p f)?dOm ǭq^;덂}ZmW$܆ ܓ+>vZlj<~^>$XT&>lH4FYFNA0KW.$)u4ύz`z0[EyܱCm9lŝFI&X }7ǜWmqNu@.6^5^F]9{P?þ=l Pɘ56)c1O?H Y>nVکflRzUW_<dϿ^,;T@3p-3Mw:O,gEG Yf馛kE#]Sy)&<Q+>y-u[o ('׃A[ 3u} qwV\S֋l"Cn7ܺuklcu\m;(v<WL&Y [&Ǒ#~VVaEmgj2 [O??lxQ;wwٓc?E]_#8KG׮][7'/66oW>sʏɔN>/2t9N&ڌ}x L01 /?DxC/ov.~]t|y`lw<*rIbiGwϗxq`.~i E>z@`Fn:}v^]_+Vh."Âڊs7AX'z H^$V0Da3^Y=)K['eݯc"7,ZX@=t{聞#ϙ:v='q'J'7񭰲p˓C̩W+?pWUJrq4Zb`YLEʫVҝVyx׃A[OF?KyE_'wZMY;젍HHx,2l$(Klغ=d{fVڦ2T2SOﮜ{3j苘rd%}Q a ZI&nAz4yXxtU\YR 'XY 'h#ǞJGy1zyJ`FLoy1 Os&lw]k_??bwX ^Hbd2UFky V6]Om?*!7m n>V@ <ЮE΃Z~Ra`B'Mp?y{ئIO&׶05od OrWH3;vWk}Ү,7LU=7x+&py)&392A"ĀDye@^FTFK^/no2@2fؤGqw;쳋t %L& p`/Ԛ=}8w;fex|y\`{z{7)j<7{Zgb/ ^*u0|$q^[iː @3d('?s#*@5ж=c^П"xR1k,3e+B?~a fl@c帎 gz7`zFh}ULl{uW\q+`8+hwL>$ k{oo8t[n[v~Vl%k뮦kcYLp Lӝ/5Uvue( ']z~[+'լ/b2cqkщFhϣ×!,cygL_qVYmT\YB}=å=$37'( Tr\GXal2]jVVh_UZXNNngx=\M7Q`Q"anF<Ii FM3j[((o`eoU;R`J{M.& Ck@k9roҗ[?[dNvj_~&6kW6KyxxhCQmBw@z`d9l\eU֕?1+8.~k I~x>ҥK5peJ ވ~0d`K M񰻝zŠ}{_G [ ۱wIh@7kdGqcF Κ^sLuKةVm^Ɂ{4[?'9jƳsSEOb#{׿DE^ )qy4K/Mt3<k]?QA<=˵:mdVlGπYp _}=|ۡ%&0nw/{} n+< a֭yF0R<lz٦'˒,iO'v;{ٷw$߸m{k!2myxLƻ93&:6VNZodB 2'xgq.&Я<<j'_x2^]V6'q Ҏ8?t_~nhC n ,%&`D2yIx{Cb%2!plk߷:on;wp;1| k tsS'qS&e&~O (fqWJ^=;Y&xF/w g<<ʣ'#~=9M8Y>S1ެ?6 j`8@V/+_{%2;lj p 竁]xo'rNFݩzvQ6B[DqM=~ab8yox?ڵk5a3#1fU:`Ns(<Lg|@8$o%8t,wHfҀ8i˜#;e$Y?U2Q1ub{}"f„ ([w1qŊW^I%=@7Ν[oGj2Q 1Mʤ:|Q޷y#γTfA3\z341p%箳%H^WU$>5*3@e'Њv#'u+ &_/xM'mG^Y P2+x !7LJX>Lj*A`;`s0sq3׍/L=S]y[o{軚N>eT+]x҄I:΀޸#Fv3*j8e2wcTEdZJ'4qy,&. gd"e˖-Ӂѓ [mDߊz2BGNq cIУǒ]|k-%!auw9hɒl>LPJZ'6ef;ry7?Eݐ_+=3fԢ9&RD˓Bd‡ ANUf U.K,?^ w#竺QUya,\EXdaRp̹m_ve#&SL?.:ʖso6߾%ꖻ30>}n}dv}%5UNOOU_=7[~C4uI<}9}I@s=$[+cq܋k|7x.WL,ZET_qIn:k_-H+#'Lzqy TXn lْ&rC<ds¯hq Zf`;]pQ~6ci0ͲrPdw78> :3jS+og!݁`睶\wwG>\Yg@%Lλ<}r_m4XnHhDO'MgڄNtX[vld\qEWn`mU#}Dn믜ri闋r_7p&[& =dkud8` jҬ?=+h-O6^EGY'{6iEE<%_e>T>!/Kń~'|wG'y5  h؃rs3Ʊ]Uv:(f71-;w'V-F1>c21R!u5c,\w\Uq{/Nbe/m0b+^u/ \8w3%[Ĥ~fsDf~>ςg!(_႖K__$m^pU>pCy,-,6/Uc7-8߾s:IBiQ'=}(_&x~l'ϳ1+y v^Oa12kiee[6 rHa)688e^op/}(%{gGZu-!1|c21X+g]6\Ɲ >bN{,ɧ-F}9/Y<=Mg5ff =@0=mhMu\2">d!0NLZ??;_ Y]Ŏ>AeӦM [4kB|NCJĽ{k_lۥ} 9(,NEb!;;rX^6I ,;N׿JMN_|=댵YLtp4ph$~r˖. 3Rg]}lV2X+\>Ȇ4>fk^ܵuG t: [[~ pgƹVE"|n4 1H93Ϥ&љ_W6ǎ9昁ۘO;EP. <.-'ȅ4tdM;oEO+JSFC* lVzlN`&^xwޛMabag||RTw"]݂,8\8Fs _dNWTTLL?:ʼX.1pMcȢ M86-.4GDN+\zm>C*ixρ y[!μS7Ƴ](ղn09{|7UF*dc|'t۲}Cw}8e>w)D'Ak"UD'O$/ش.,6E[S/x^pcj죃: JWZ8_}u{&]]')l̖nmPl~8)'e~~-G_;H^mTc]x]goQI-CU7-cy"l8y۸iЋ삼46X(L+AdBxYM_&Ͷݫ&t01 u;>晧yzwwtF̗;G`;0s`l@pN:$K5Hu!3@}[2*x(uP]Wytdxe҈ Ǧa-aAiq+*0=;WK9M5N+=iQ۴MYv}~FxS{,ӿžo|]gb3&SD㮃{ʫf'RtֵI2!Mc108Y\|Ae|K#<闌 Bei6}(߶c[Vko't01 [0s>:n޻c8ǟcq7pxԑG;rG 0Y҄ q\ 5mŤ\8td'Ţp4ڍWsxE(Aw}wr.(y5!7Po"qW/KsI+OVy ڼ-h4!O\'.t--˾j>듧aiU݃LwK9Ȣ@dH28&[J%rU%W2{a+^NX ZyӀs J~0ݝ][V_m#h!],+rq.m nK~XԠ@᪓~mg6^}FIo/΃;,qCX0&7cy4Q2:VnN -Neik[fq+#=E娛f{+O|}rwonȔgg= >J>V.78nvL7w}'ǟ2Ɍ&4mN^<3h **yt0uihuqic21~:FG唎ZDUI y/Vr^ES+qɉ4KY<lYlp#ϟxw5袋n4l93l?$>B 埭6р8<@u\-d /ˤ{7o¢#H^8HܟP`@ |XXG7y/| )X x́z@ fTִsڍ2m*6y' V %YyBL&GL&Ɵ&ԕ&œ?;?4m_B,Nh Z!-K奡lo:v@Mff=5 ˖n}plE'cuQ'DFVjR6@1,8+ 9zڈq@9h~:9$N - + [sVWS6 c{?OY">@l j>(l kfX{![r1hqR۸ų4C+wI$+.-?DٺU'vl_(K: Y^+O{G_T |K_zq4gʽȾ-9Sեʜ~-O[ I2-I.'tdC<<0:n/f C!3Lz P`QF8ư6ߞkݙ,p..x=dQcz,=9{'9g``|-sodb|;Veic21~cb6-H7w~iB_:46Y22rcՑla4noGwmȯ ,LUC[T |K_sZ!3wU9xzä"Lq^IPrEt&8!?t+`<*:,-̣#ˣhqg>T{ .kZ^ZFWX0<]>g;>Qev-[2F W:묁Wy%&;&w:[-opEFZ\4,4>e[fuʷ 4;vnwW0yU ɏo\|wv6: {.ڿSNRGO-؂yg&O2c4EtNַ,8( :G{f}|ZQ86lHAW>$,]4 'HS{|XHæU,Z<製N;v+A[@3YZ'8\8;p/7֯~F}Nb|dbNǠc6˚90qn{&Ƴ|ɇhV|ɉfuᤁWdm;?%6ה bX2onmt]i&WXusuε_cg'1>=Z聺+UEi!=D˓ͣC?@֡WՅCm~K-ߧc7nw2zG}T逸Jt)wߝb*wjXZNyb[N]P[lpqx&?[p)7,|ȶ~hTXw @>"Yܗ/ Aӕ`;OV!~K|0lwwiZoo:-0t~>7WvW6jX^6yfjji޵88&dA8I)Ҵ鮛.!܇y&l+?eQRm=PRGhRa!:{ޏ_m[>O-t^V0q}{7 /%k5O@]L p_xoM}45|O=T3ׯ;0\pglYld74kvp8% xeiE:4:銕'鎥۱1'|v@ t e4%2;:7ff>M 縭698Ά^z)ْMLBx'G, o>_}э8&;O82Y'0+V]glh4:`|r%,B8c\ؓ{LjQvG gHs89؇΍ ͱ{L߄QD9Gc(p{B*?L!GZ<\|-O8Fο|J zn?хZjJ顿p;t/usSǩZWs+~n?ҝW(-<ż;,)a пbe7bƯ>O&Tn(!">:/$?ꨣ /j8\/rRG-,j$C^%,qs y?>S`XMEM){-of*=~nX1~bPsv\)n۔PCrK:Bu[ow衇^&qԉ'\@qC~]hG{CN&،Vb+笰H1.OLs6&Oqvq'y6-X5/r/G-8rU. -s5XYnMt !z*&dɓ˿O\^]k׮MY(ܗ]P|878:6 SyBtvt &}oox㍉K,o9YشiSbktS8\ ,P٫ X r`\OJ`/vbvfC+&_L&Ɵ&Qצ#vo2eU9.69VIӈ.!=4vr_ml'WTY ƚVQ^ pu{f͟qaʕs={;'`_a!,[|٪U~9a2Nl֠d<±∣+$ѫ@BW :5r! SP4Et=:(ӟ i밐 VJnSO=G$aaLW.\|sd>.{;ydY>/,r<=_Y ѯf>/ 1>ic21~#m٫W:L]i/F~NI=240Wäپs{otps5дNҰ@4lr,pWpwO͞Ʃ㖾uN?sBpGfы*8B!ZlH/ӆ/-矼yJȧ.\E}|c7.BY}}xON8āCcO|ǎt Iv{^MB)أֵ0x} 8s4}Q]VKƥ^;~ o{wgoTXOg@IDATgqvoS/hLZy8%_4LeWwpMZ_w2ktbn@+fYmo{>kp2Ogr-c'Ir8m{hC ٘;i0Ć}$'=Udm:?S w-rHV:(<?Cp)-GHo|#Y(9+FOq @CpAd%Px.&\FmNo~}q~- 1>ic21~cb6c6]!۳ /SAKB}5NZSJcT[w:y]?źܦ4.= N-.z"ag<͌ @])3,S:`g>O~:TqAl ªt9#{<aǦS"(MǶgm{C tJ߂^2?-KֺN7~矧0;BOf|jPB矧RJ#(@q`V޴U\a**tP4?V qglrJǾw$?^[ᓁw}wST)].t[Cc=\k֬I'v)|&'5`)ZI(7}>'q,789"UH^cNǠb6GmS As m(c&}Ov8>h늹nQq?in;N\ ":U+ iW]GX;Zڨ!_yU6hKO)ɿ H??W[#>,DiyrD023_5pXn<-*'҅tӅ/u]=vlyպuX⳩CTYd eV?W ȣrN^`~Ne~6$x1䤟 = 11&t W3q N[,W*MQ9qg#8C}{vlnɺU,]Gy.${ܼW2SX` SYwy/'JSFl|!Ye/dۤS nmBt}EU9l (&^Y+(nO!>[/:p) Q۠:UlG>98o sx,;pi ;B쳐&J__4U2x=dϗda{(9K,?S4=:l/;9[?o.Usi\wqGb;tTER&>~~'X=ƚP(EǤ {qlgZGz:ꦥ * ͦo*M:|s\F C; --mI aw]ʇq<֮]0d`c{&F, H =HZ-ټrAU; a.'j ǖZ) GFx?&\2r2,#W7} u͏rY_/ TumՁ[lIgŀСiخ eK_8ek׿w!SF@OL?M:˗5@Ϲju}8A&RUNY5 ; j,;/9ItaYhU '?:jqy"U8l\~}fM2,q&:者&ϹB"KB!Z,t_էɮM}]_ePM`iEǹGgcCP|҈Vc/0? 3QYX9}6d!-8L(IV2|ǝW>8spv kqJOynʂ׾PK~I$uI GJg!TUCSfӇJ9vr罟d4,0!%ɷs/s2|r}])p7mڔ-5I':E>ꈇC4BϓEU [G~gp/K7ÓS=G-B4Wcݺ5 '~Ί / 9HNilպT/O}q_`!.x=09OdA^H׶nm-“~~E{(٬.E>ek @>/_Pb:GfǮ-ߛM{5dS(]fn`L?}_z?jn혤? 8Egwq*CvYBy*E:4 mnY;]< KFPtASG>t;w߷rU0IH&c ˏ+ZWɿ39!VGJЙN`w \ӴE_w[k63[;eRol,>jKJݸ9䓋swatMvװ{p_nW?BOژLlb&lZGo6Ŕgh AъsjKѧY?Dk#0:Y$e[aŊ7AXP@]I/,3/rC4Ы;puiEi 6$v~%+E?ijGZ'‡|y~;ʏ%U7  8>x.|"QuGP)8I3.*;4{Eb2YDE|q6&w:[ f\l!@)oU|U-鉥љfέ|e3˞M?T@ t ՉZmQAN5 ,|vMf,_:.dbt&8 Jsmiy*MvS\0LNp;WA#acECVN2>?[mi~enwg-)Wzɵ!EvY&.[swr_s~|!N[8'(7G dèefYo,[kQ^"]M񔗅Wy,!~K3/͎]{s=#~ geÏKJY[(eN(dwߕíy? /y.dbtzq8dr !Zl;;)n!uqMw.q,Mi֏#J7$2Zju-GA˰ #w_ġ~؁v Adjm E- 8'S6`qיلϥdN5h?>9#F9&;&SD, MKLW6ׅ7!-!=Mhuc?DrڳiS.g>ݿr/1 t 1 ufܓ?voUhl=#gx)ArVVUdtW20-l<]8\lB\cuZmyNӓ|v#㕡p~|c6)M+ Yl;ӓh"ۅުi|*:z(-N.B(pȢ PX" ͳӅCK<5펾. ᓰ8v٢6&7cy4QΥL;a2!h>rg,>?M?4y3]?v;g.x\r,Phn<ӷW\M6uyUϓ?0a=cpWxaԡPVD/"y&ZrjOKpySNq e,&_L&oB(hKADz Ahm[)ߤ+'m;;N:hN[[nz_ᶖyE]LqJ~헼( M:HNiB{|zic21~#K^5o&&'<'G͔*b+oKx謚w nĿן4&v(gn_~snv8xw6աKA| :/hRa!V4ѡ=ArU*d˿O]Ы?mCP47IXpɳ8CX·*?wđUQ:WSGL84{MvVaϫ:vz7nL>~&d;Y]~6W֌9;B>"#TP-ɳLmPivNo+YO>_9䓷<|'?7I;@9 t $ܓϺ̺-&uХs*ų'pZ8H.䌇h C< |Wq^m:l$X%GuYnͽ@R)î7AI.chixgq4~MY|58G-6lH^`w:-Y?.6m/'),[_l @tKGOxc嫼VC-&u?Z s֭[\TaxTOilzSuR]u|-~dqQЀn! :=N [CwYByd)E=CttII/yPdE]wyo#/'+赿pJ_^sU3OXFvdX;쳓58A6ٜzh 䬎I[Wrq[ͳSOL~f1شN9}yv! ۚ[P!hiEl0Eʆ!ZiܽQիVo>:t@@DKO9#\ڳ EA 5AqA!:8<9F^M9V ]4;Pۋ۸xIX!e!TN@~ ‹/솪A3x H 7_UZ,{b4pCثD{*c$ԟ/* I? rUvڶ(壻ӑpg=mu5)ϷQ l}@XhqظD"M3~]O šu@e t M6 .`Xݚ<'?WY瘠8Pm\[ZU(t0NqA*¦$`ډ2<@J i',9}O6 EW:o|:yui{.s97!9'ś-[ NC::XS>8RrBL&GL.<:X ]4G.;*ƜDU -`(.ZqN\Z\yh1~HO,M?N?͞=VTs5iS,gV5\:MKixEݓ׹'O+lFw(hB>FArJ⸷%W-̣#+S tH tN'~'z?|J/znƿ2d=r1&z&#l:8,Ю<힞phym:6R^!< QI1<(g dCv\oa=ټȏZKOhuY\zcP>1M;܉s-~ϕso~p;[<Lhv[[hIH\z绊~]ԭ0O{8|(h B>Frz\s- l2:tQ_M'/np?G\y&ci­OS\W^IDn#$6V9pn:ycv<$|>0p@דּL.v,?MxjuYˋA,c21~cb6˟}q6,jL -/w$U&eq])5*y8 ,L9ť,h410:˦ٱk{on sgv}[M&i㦄<啉JX?D]Mh|WPe&886@?3zUh2"I- !gN Or@%a1a\l|U,҇3BxŽ(Q}#o"NȆiiX=u蓳}gqte5V&T$Agb \d`y4 oPat&VNߵ~\ ZY;Z$UBgJjr _`Ij^,(Xq>h9] ,yC4ځ ^TƅWLs1M(qrXG?xr`t+0e[| 7.JJ1Iu*M.o e7 /lGy$Nt(3&SL]~yt:@6LK:ַiqҾ.>.-@%}h6_~ZKW-i#MYr]?tc&XuQ t QMο65>p2ZPaCtNmt **yL-}ZQ:< %EPP:^X|xtEmA +$_1aYעy<ƱME!'mL&Rq4#;XgxO wq`#l~h2k>aJy89gK!0WZbaҴS|5WOu^1fC|.Y @a_ yoB3;).H[XNXd<ꨣ9锾#<&F>YBǶ5I+ϯ&)頍ƭAEǤs\5`ܥiȎ;YW_6M lh->PMO:}1MS$oH.FCzbibat*ܺW\@"őQ; X[(0$.bz]?^<9~~g6e4_η|Ʌt 9!Zli fv@S\aoX::##=> %`k}AۂLuE~[nitxzm_ 'S%لh~51>b214XJuvӸD]rʷx.|VwOe=Za<-8ظm,=.9L39\gn߹u;6o\~X4lwqY:V7h?@ּPEt|翊^EB!ZOgE}[6/̣' o}6=KIGQ]u[ߠƨ4IߣU^N}gK<6CCQ,DE* eI#]VgV?l9x?"Ew,e\sԱdHp5\x,;Y t;ssg]yz݊xPë~z|0 vs܋/LB9B-O6^EGHV natK 4VF<ȩ=P-uH7}ڄ.?g>sf6Cx>dVU/WZQѸ՟{-b21>c21~cb6_*6ώ?Yg?{fy'4Ϲ/8z8ZlzYC4utpOwwff.>]*8j+f݉wp_$0K7|uթ|婧Z8!$&/Uq>lj!]UUdtW`e/lg«IW5h&JK.> >ۺu.ƎO=T@ZJzj m46Ur-? L׊BOژLlb&l:<(g~94'DY *cJyT& -/ EWf^z~S1Ynb:nn` ~]5nuבuām| '#[9~Ƕ l<Dps?htZ(.(@>+& ٖ-[)տTS?i5D2kj)El77^Y2&;&w:[(f\l՘'xz۸c}fi#:>Lr6gM8mꠌ pE]}ytN|B>_tA~ƴ!' ty#ȫvMcm,% ʖ;iR$6̳hOL]~yt:@6LKAd}dtdY1.$S~`yi}]ųr C=x bVOoK0288[[6ϯ~ߵuB֍PN[[v\?TPӔHr8j)Mgnd5( lANy`c9&9T A<]4W Ļ~"Zou:9vb[[Mby mm4nWYdLL]~yt:@6LK#tgMKk8VLe}@Dz^8Ƃ+b2X?witvi8׼ +=7TNR@0- W쏹kp~^}[g`'x̶rt!+=D#MzY ϣ㕡S 6dl:_. }=$mZ7|Zݚʴycڦ \ @%$A$kq;}NQI1<(g dCb|d:h,pn6/gWyٳ {@>OHӤٹ=m;y.0k6~R[w t,P_e]QZ=Ys# a86I',q ut>!SYAWUtdChv`&VeݸqcoՉceAgK?sym8za#S'psqt~۳ՠƭ]EǤs\5}\eroDwgj[')xѸ^4Qlzh[Kh10i'߈kݙ_qo0?xbZ|ewxg-- m'tDzCwOs?4@]ȖrK1[r*y`@ytdx1:m{챙3>/s5bBN|;]1?ag8fui86o1t,NCi<.U5ՑB[_ !ٸ׿{.9[PIiJkY5M,0:ٵggoB֒s͕|a`~+o8N]g.:?u-!F؝zgTh2ԅl:dzaq;JR4A ¦X +D@W2K/y4y]lݸI[oẺ/}a?]yk^|v38#~|^Lt4٩r s-]UUdB xp 񆡳s':QK0.~|z[l :׿o9<0?r>!3ڗ+~FVXV٬.܎W8{X5tҴ{x*ۗםHfΧ8KYX?Dk#MY{g8Da粙;zl+??`[tz; Z[(4Ϣ2qؕ7b[n@ gx"0d* 9HېGz _w$'%T6kr2}P]|<-[!~F:'wmwW[E: L1l+bs?W_[<~uY`"HUS9Hr7$G"C una<^U:;76lؐ@iO!}Ϲ wB>ʷ Wп:I _]O'BKFVXV,o¦ȣrиZ߿SIտS[ܗ~*gx]H"~{<pfgl&xZ,?DC-&FrVdۿr̿s-ө,0Ƣp d:v W_}XR߻>6Yu3\VߟĖN2>.2rL.;V@IDATѷG~?Qigl6>' 9>Nz0˳SsU}\-&M:U#-.tX ƨnxڿjժɿ~K6h>;NKTZ3ћЁn R^N*ixmc{`[Z[Vi@xWW7>l3w}کF[ZD\v[.ŤKem4) 1>ic2uM0-Γ8Z&Nx՘){xӴa^6tr #;*6c'5pT-Cil\z3^u5a]V7AZ.M.v^"?  x,q҉_|?2U*dUN >>2>xtvn}IB4KZ-ʽfnm}  B19Ѹ՟{*b21>c2uM0-_dci1U\đJ~oDz^xvׯ_tN~gvsM54SEe--.AdJ+X2<^UzHwm͚5:ʇ2s{ndο+&t_/.d|ʹV\ ,C!e@MiusCMhWz f/3;J[G6;wGdN|)rOSb|dbiұ: TuTG m}d/o_:/߾\eʑ|.|T/ XM !}NqW`vu+ t ^zIeT"mC9bŊ5ic21~cb6l<ۗY'kK|zxϦrch?p<'I! qJuJݵ3ffƲKCatڝnw5=0v7o]]Nu*J=Uf] >?[qSN9%wRP: "(,o¦ȣrix19'_> _߷._ekWsNCe ԖG`i~)lqܲ}<|: 5?[]'˕+W&OC N?sՓFn"+tܫ8Ud)gH>D˓e0qU餫2s1!m?-80{<׼FiSgɱ@0rOuvY]';NhpG;\QnPO.y.KCoH6ބ,[<ᄄ aFWy#P'x}U|ٵ[ݶxK4ߎu+luuoaCkdɻӑm}{lQuiRl|G*}@bi-I~#//O?]՝YeyZ~V_Fz;w> mes{]~ՕW=r>knsueSOPq7n۰aCИhb ʣ[ʀ7ᠷοO#=O5yB^3!?*W.+.0yryU_j|y[6ifv&qSd-՟dOBOژL]~yt:@6LKAdO:ʖqU=ou~yy|RoBӝcmݱ,i=yO/KI[)W]uwv@gΆptî\[>3NH %`n!N43e[*N;B!Zl=O `E]}袜{lo'z[xBt ZbοM4wy=wՔӟpb'qɗ͸9?o"NȆolG,/XOľyI4Joeo_WʱT`hweo P+hu3_!?۹uo5Y˨h--doJב~z_v&gBA mtt"ϻV&C4t!ZlЕkw^P> ]W\{^qFHsw,L"|~-tM5wrՕɭnMRAmi6&㏋I)kawzgq޿/?;ܪeNC,*ä)ҹc]&{gg~2,: L+XbKQʃ:hx䧟~z4By?\K`J-J'm488,H /Q@{{[lYx@p>KjNO863|cdISgm0nLL?M:R]'4PV7Ҹ%Ų /$ ӦT4g뮞;[t,E& Kʹ'wVU!*:ggxO) s7w,-T2WX9+~tX<':)ަb;g㵀Pd tWwIrC<[<.8FyxΝΖ-[܊p-6C*1L?9QH Tj謟@W'ĕSG,ky0sK?2w˽x]ݶv>Ji}omzq?=u욖Umz7}+ߎn?Qly6)Wq9w}dfIap ákFgPc':C @eX_*ɱ<6Csd&EՍ'ՁwMqujcFkkUQr-@0׮][ q7 q@!+By{\}OWquʩcW2l,:srK[w=v}?7W+ uLڱWЮ [NY).m@pwi8oy\]&&29=sB\Q~;QǏ!k>.}s߮m9E>n<0ްicȑcL^\mV7oǿDW_}#,Zm'g:4&)8~rUZٿ!~k<^4X>evݨ6xP[_K~;HWpTN +Mʿ_)+9 iS-i,Fr֎ÜsdQuitJcдm62.BQuqhe{EU_XXF+h"TF{=9 qu>uyxZHƞ_^ʍcwMUnۍzʵ^8NG\y5ZDG:/*WIDWy$HC+O},giiiϨqqӨ-~kWL͞Y-̯z{ V^?6:^:&ϖ߮k4~cttt?b; #AY\y-mN=Gwȍ<λ@H _|7iӦMae`p_+O!g#mQ_ear/m6_ sdQuyn=1j5Vviی79+ȿ`_lU?6e sc `*-VÙosd9U5ɓ'~y$Dgu3Ϝ>O~1se+e򞏊f3YnjtTju:\Ӽ'W'ܶN#N\ym[DGxuI^'GJy^ڤ8}?-;~oyO_W}&jvrS 5*žr;vv;mm0s=0Gvy,kl'2?="O9H & rCRmTLժgqYvrp4q\˰ܾ}:渇ɢFka_Qeyc@u.i_v&VZ#'I1AFG1=~yz휹 yYtt>aAIq:ʡZZ'N\y\?N\g'>v~gmTfϼ_{ު}y[w5 H{`_3lE}p{U]A;-@9y>P'챸륵^,vvu/L>4xۿyjܗmr n馫K2axX7M?N$qvi8)yœ0YukvS}"eytaɿC4.uTW^q::: SOb2.b7V=IFn}~BwoLf3kfa7XaleItՉ*UGWzquV8:x${Q;$gmU3ewΞ9}>͓@3, #N8ӧMR=ӕl꧎c4 ?󏓆>&Nۇ9Q(9t_Quan Oea]'iɮ;iҤ1\˓Z'NXҏMœ,'KqO7Ǒ)'r,bw2Grz}E'YC3j"G\yCYMWPGpLCGPgX>8`~C}FjҼ[yMf/oa -ͻ]O[[\^mKuyW^ZuT/svu7̵Αܲly}ڶk-'#dQt;Ow;̹ 6=< HM+B͘12BZ"_g'aS?^VTҠ'|vearEȑ#WVnգqVܖquʡ/I`>NGTyp'g #rzQhyi[&zٺSݖi٪>&{ˤۮ[n෕-Xcc;ev:X Xc]ݨӇaf͜@I'|>,L[l܆]teKd'ZҘ]nYNt/L%/n=QeyV{74 k_xmO?.:(e6}dkI3 ݰr~nuTqT+N-OKGvڰǗDG\>lMT)Y,d{4y;]6~;vޟ6Z*j݄{G= t]wuJg|wxm{LshM{QZK:S0m <֯B/wY2A$g8Ѓk$/;9_1cTaR`РA Ç;o kIÙLI5ṆMX0YT(y=i `YM8egXuh>M2p0Y =sAq!yZ׿B Pif=#im[^C`>Z.CeA]rgIe(V$6ĕ,NGt e~z΃wH;g-9##C5][@zױ~a2?0(@9qTZǎw:ڝɌY#v;s mلH +W-'?}_4Wm]x vtGxaD8ݻפ`Σ-p?COyQq2_tQe1&RݪGckr(DŽSVFxN:C}ekV{d_g!ʮWӍ:Q.NG9{'H8 n?/ԯd}KΈ#|lWГA}7vDݻ+㇃DH rp*ki4utա'1;켦Vf* զIȿ`_lv:6lurO[J ˳pKG3uD~Vimi}|>ϞujӡQ:Z?ԩee(F=%nqxOթ2{GaNenK۶촩)QnW=Qe^=Sf?< rO]N?oiy3t_6$ڏ`X*Ggq?8wwyIa 4v/K.s~`)9HPL߿ٹsvXN+~yuaSOlv >v^QrGceĶ^c@bm[arp_{n{LeǏ{+Vł;L {MYb/ئ}|d IudGU+z$:ԩfeq:'N~Dձ[TGjc;jMϋapGhvm[vh4p&Sp|v-vo^L߮{77q Z#ZT~#qcM:u8X꯲]ȱyc}sTJU$@9h iӦR=v#W\~>G_pqeg{„ 慫۶m<+emWjnUߜX>bƙKd0I'R>i1>R}97nzpcӦM**4~/6q9@n~g+}b2KZ%YC:lձWn׍J+k˓HRWquʋ8nav?ۖ)C`;^%~TWm(Fcn;g-W5}g;ރi2ǃzCv h ;-3gmmP# @NjNs2jK/eUW]|wtWI=6 ܺu^r g>fLW +K+4m8%O:mʢڡql޼[cq**:4իOm=$ŦҶQ$k(Km']5$uB}:l$Saf۰.j2(ۦROnwUen0I;弄cNk?zOmޢwӝ6L5 sL±D\D:j?*0q98ޏsUm>]T-ĕm:l$ёF\;M{,VO(khqdU$@$@Eif-;:N^E翱 (f&3c馛)M:4^4I{;fvL}tv5mAG^a2WGYct0*[[#0i$)L2my;nk׮u}6\ryT#}GK.]VQrz#4% 3TVZs n3gevH'P3ii5ٳg;4oP)!y-dz1 G6qp򋒫3m@a58q]4r[g2<?7VXq?Y&V:Yd51HH@aCq5Qy̜\רG"@S6m{e̩8vǽ1 N/ƣ(g>L&S=L1lyMGQUVWqk ȿ`_lm+L&gۏdԺ',_l2]^3Ώ@wKҖnN䇔HH_!N`,_1:A$P//~K/PqyqƙGFu3FH0Tl~o1>i[[2my;ƭp?xP\T~,A(+MWbZϗN$@MK?&1Ȱd`ݳgFD$P@c~Yc[zRyxlܹs+9rd!' a^86CwN:y7׿q`g,* =lg˹K fO-.lArH$@&YfyyXDz{!C| TV`72ƍjLf_GgQּ8mA_[5joھZe=Tr"x{XabॗJ{2V!A`=7a6HHn8xPzf4G%3.tm4L@hiN=ӧOFҩ+?Llgs?0uTs0ߖk ]t5[Xv*G|s?cCC?)Q)7 >}XģHHƈΠ!?-뎻B: pH #i`]8y8}.23q9e!V#L~O2>|v4VۚcMk;Ç͕o롦l7?T17]#̑ gm՝f~X}~6sfU$ TVTG⪫|g/1iz}y{c8rGpbD>kڎUEp^e ?[6(;xa'xDp˖-ζmP@?~gh#ܻg\> E` @ 9C,}_̙?Ho 4&/drD+ԓ11™4i}vLNH-m <k52M'kdOعs'}߾}_xqφ, fO3|L} @R~o~p%72>qW+~7~'4| 3Y]zWűUfG7_4a=z'<F~t?0@mvVnݻٴiSe@u`"O\EE?޾zjЋ&PwGnߗ&o-JhHH "W ~6ujܯ-m9b?g CV?$}z{[yНɓ'xA_CPg}Յ+:)r۾#hĚME{ng<"Ѯ#G:/ohբ_#F֨?1!o$D%Š$@$@e'N8-NM[.5_6+_Jeh@_v eu@@iHHˏ>I/ӃNw˿<,ͣzu J!WSrWVW5$dw;֙giyu5FqTYr1)zMBi*C c쁀W0OˍTIDAT_+L0NsϬZJ.W$@$@e#^.`b`Ɂ@|-N[irٚys^ G!'[j+1%ǏĹkj8B5mih ߷fvն;03YreY`"?|^|m~T=gIHB 7)-{tffb@.F 0ʪ hi]2q}%ѻjq>Z]Hr!PrdƌIv%7n=!?kzz|;vT~Գ~;U|n Wu&:\clMWWXQʍ2O.ZN$Cҗ43M:2h\@,' 8  h[0AZΗ& li-78--;}--Crie=vzG ~K&O~8U$۵^|6;g]+X띎ӯꜫ^ʿUr8ǎ,_.G//     (@MkTr;}b.Isa?84 `;H/ԷYacEƆ BL:,[,C:x0 E&!*-<,5G]rVև8>cI#ǭ\pAr>;ִ]_ {2`uC?\q;,}./>2&     248&tDggՕy]=S9^kzjP]!r-GZC![ -G+`HHHHH@CMϡiщ4.&5q.x$r袋#GLa:P̖G56ѣG;6lpnݪ͊c߾}4΢ Q? %PI/rEЙʣ7[C͡ClkW9K~$W?zj4c     cM=Y:H7bJ S܎c>-Xjގ5\5VW+0:,GqZF{ȕydhHHHHHT\⌉#8$͕|{@^j=sq^`2l8uTG63RFZGjmYuP~s_ӶسsTTh,g-^P;TN$@$@$@$@$@Y p@VM~„ >@ӈcZ֡:5F?yGJsei1#     :'PiYwuumҴjv4 qtP$uvlڴrLP,[9v+(?v}¢E- @~j@J3vVN?38{+ï:[nmvl޼YEʕ^T7qJiY|R K&4xu<Ӵ6؃(+jZcנiζmz=q…N/Kl& ?Y/Ց @j@qSMrq٬x֧b/c}9ҙ2e\ҚXGɵ1c` uurNw˕3X#    (@C@!q-z@-n3v؊ E:7nce|c l9d?rq{-˝ÇBcϽ]+ @h Y0N+h^pq5{P?:I^`cֲC9֭52i$Gq'xI]dɬS/ +Lk '˺ۤY}>s&N>]MtI}`bȑ^6a?FNp0b3 .Y`A "-6L TK$@$@$@$@$Pri?c5;}Z]v}\UMcÿ0&u5Ƙ5}6ߟãD%*$@$@$@$@$@$аv@6*GmU#lٲJ     z`ҥ{~$YvK/fpX;::ҪZK^ lDf#ik.aW~asF&* @$8 ~_ 0t~{KsB]/M퀕GEwI+/iIHHHHH?h wUWǏk:N۽DŒa@W_}/;=ڬX*>4d2=IӈuIHHHHHY lX{ԩ'ôcņW_}ٽm) u8yNP:n*p&Gqc3LHHHHH4\=Dޥ~svfg[[y$"&FhmmuV\޽, ˭r%evHHHHHHM50Nq^凞+ww}U6#l_ Uou?V?L2ő{=@ yCs-((U 4>r/ipK/5Mc 1q߾}f߁ 8G-kߑ}u, @#hzdӼH:VaS@ hWq_bdWY/ 2 4<[#*Kw*|WGX-hw%OWzjz{úu^e<Axtf!_HHHHH#zӛ޴B9=pof3+ssᩧr~6B,pgHHHHHH2h 6:u. kطl@ 'Aggv\ '|J*IHHHHH) 4lW&9-߱c[.rgQyo;bp+~3ExEFHHHHH@SO`$*tpagu=\g„ f<8ϘpuÖo߾_,Y,C$@$@$@$@$@B'p~G_z{eeq/x-[f?rH$ Ci8&nĊ$@$@$@$@$@$@ 鉇S{3f+$`ܸqG>7dXpgD&*0ItϽ{:wi&d { PA!     f# x㍷ȕ_(@JaÆ9fMW sq˝Gy]˻dK$@$@$@$@$@$@@?'KѿgٿꪫL8lȐ! (>ㅥ G}< X^p~~ѢE C$@$@$@$@$@$8#+RI0p;ѥ`e@J~O=?jkN~8kkV$@$@$@$@$@$@ip ּyZRtkHq.V3 ѣGWtbB/L >bpU___cR`ڵ?n[9 qhHHHHHH p }~(Ne]-oqqyg\HHHHH'r:2p[yS:QF}{KO     h6O6mĀfj'1R^wF%f      c(jsg#$Sl4t? % V46nI6"dΘ83OEP|1Ŀ (>/ % (>P苦;3ie|{g蹪X-2s=+WQ+]L6O w[C{_F qb Uvz?Zb1@/zcs>~if,ӈUSjF 1_Mjbuݠpamhmçϙ>a\+5%QKFkm}ۖ?ޚD\!~6,-7SثŜvķ5Z;[rmS5{yDyH}r9|-ăFAJjI.[/]mK 7KRDrYQO-Q||6 (0 MXd(@h2_f<:”_δ*d>e\c?~,7?& ك^2Iq2"y@g|UP`o IDATX W PTweAy. "A|D ByVjLS63dtd>LSGk;I4V%y<e] {nsq &%ý9Rϟ8;0Rjk*h!3gު+}L&R)^Vsmm'!hTfp R"8n2uCa۶ر3^hx<~x>011D]"aF,c„i ǎgg ;nwO/a R dӓ=K7z{H3ȷYCEBb4'-,+$G]7@\N0ݘۨl@"}A;@0c[x&@8fTNqJ <Pw} &,`3\3 S!Iq-4oB!k 40 M"/x.H("_̉%N`u7vvue`٨hB]Fžo@^(<"#e"<1@))I@9~6Qj 2Hrʞ N!yX92!L ]~pc &F`nl,8u2n; + SӍp=@{)[ 0xm#{ "SUA_{1cnltn6Bʶ%~ >>+A2&@G5?| bӧ(. 5-«ۗ`4c%gʘ.bˆOPrf=BO/Ux<_d/y,|e`#7w2&% }J~s YT-@0䡫`@KHCDtTנ|%L_\$nt"+O({c&BpPؤC=+G d &0:QMn)*/`a*x p]`3 d.`qR%Db [Su%i1@vnU\'j| Y2*Pa7Im4)7\$ju?ΒM" !Ppؽ ;ڂ‚4ج-8zC>'r9<>1z,bdg݃SGq/ #3s:N: bQ0vڵK!]OĨ$$q70[xe]qZjOALddX;X;G z:J"(~O-d7RMv!`s\-W\2څ ;7+)-LPNWMo"NO(,5O⽽hW`uXX/RmqN  #0;;nIȘBWJ,+ZׯZ\ZNϖC&W`l|ts+jAM]=6=8Xt<„---޾L.'*-- 8JK̟?Nl钋D(^"s5D~Rc'A4*(*\tw.~uٯ'(@Rd0:V)[Eq]NΜ-!1aNG#$Ͻ}Chk"n}/AG`JU\P%DKB p8(˪l6㏁DcW FTP.3e1D",xո\{f`p`oLDqdf6l?nݣ&L߾fKS/Fk''sjiwJ/^0Z"JG6Obİ^B>Iocџgv(6Ⱥ c(-[ӹfzV.mǶ˒%r f4QIENDB`ic12PNG  IHDR@@iq iCCPICC Profile8U]hU>sg#$Sl4t? % V46nI6"dΘ83OEP|1Ŀ (>/ % (>P苦;3ie|{g蹪X-2s=+WQ+]L6O w[C{_F qb Uvz?Zb1@/zcs>~if,ӈUSjF 1_Mjbuݠpamhmçϙ>a\+5%QKFkm}ۖ?ޚD\!~6,-7SثŜvķ5Z;[rmS5{yDyH}r9|-ăFAJjI.[/]mK 7KRDrYQO-Q||6 (0 MXd(@h2_f<:”_δ*d>e\c?~,7?& ك^2Iq2"y@g|UP`o'IDATx[ypu?o! $I$V3El6nONH#$qNt\hvص!up`lc(tޓsO]{ι>,VZjoo}>/p)آ'Zno~ QUUuQzޞ?~0$I&򦦦Sw/`[ l.'gA">_ߵ{?>u.b@$7Qf͚%>_~bHJJʐ1 '/Weee-ؼO;vsH10+$77a(M9:T`Nwvv\j+..>yL}D‡$Né _G4O|1jjsYSq?x[7|X_.v+jqˆ_R|X{|+}X8^jQCio@DFwTF z^tR1+Tc6A@ xVooGcW[-a }R+'a9SLJt,X!|Iů>$ pQ&Ni"u.ph;bHUl6mn/apZLJrIOUWڇX&zMC=83p+?~L}|4;BPKgBM$znwNX\.L2%7ǭ+ 55U㎸|yዸucEPTDZoxm.9$ @WW|gg?›w}s=jSgF{[G@"`Y\fPҢjJM^^L41a7Y4xI.4^r:*k.qdҥ#4M$eR%< -\@8H)[>kko{YW` xpB0O6MIEYY;0/7<$>|l$I0%,XĒr})NIoWN_jR}uć;!9n,%w'r:UA)ر},(EZޥ!EĿr>cbI,qㆌF1cP+wtt]#_6}t[!ER>/q,I?.TȞ7dLp=RrIJAf؄ *OoC!a `xD3gΨ}ﭭ![v}%{|aNΑdvZB>)m>izsٸf,%&LGmy7%slIIrP&ܬh>KDF'=P?46o VRl<51KA󋄰 L5ĝ喂9rW.yeE H'{};ydRׄsm\M ϝ;'k׮UUwb6Y}O,,Ζ4pSB4$YXR0$/%I~Y[^"9tQ򳓔0gP'd9t@$V30J&OCCTYLg87^e_ ?3BmW uR:1IyEHYl[ȑ~)P~ . |vJ>_Th^qb{;dk28] $c5Arbǀh|7%Ӂ")VIwOosɓ("6q83KUSbObG% ٩?07M&*6/(\ L(Xf l7ĎX%1ovW;d| i@ }B45[$ AE\xP AiyԊµ`@/=JDIS^%{?@rCӲGU'B)8L[U…u%5ى'Tv{>C7 ,% zn`X#6l'(um&S"f%M% xŋ+3=df% [i},A0xI1RLΰ@'HCtnʎURr{2oRCia4р:Sy幞ԅP^6ܛ )'E z L.S s>7uPc_r0IIvz-^=A$!ifD}R SӔȃt9w{6 Dko뗌Tͮhki@6kG/h%C\)n).B\ +C|oVzֆ !p pr? ٿ(fM&,B$ ZugT& gv ǢiƂvpD̘.vCxH v hyxY,W^U[zH˱ۈ[bٻw={Vn޼.&/ح+V[>np~l|O+@ Un: &Dk2Mtzh ӷ?<7|03#]bメ9ۼ$$T"USJ"Oeȧ =b ?vGWI6"B$Ԁϗ T\ /!'?}TJ&JZZ0DyAc?w*dTŰb#hs[prbqZm0"?Ν;*NIow~M:tYݳtN|kN$;RZ663t^t¥eδw5!ȳeE+`LɴcΐԱGfVV\fl4lpT3ߺu\xQ]d0gPܹsq'VeI8yZ:)%'dHF`8SkG\(2TxJ^8u!|E;pY)-*é* +WZ_۹s{+0&հ{W^;=CrssSf {ĩrG^hrG|!K2!%/(0>=˗/ˡC< ϭ& ;18h<#35 2-^~ѱd }niGGw oKO>]LwgtH<Ŏ  02~OfLI6{օ h5wFq {2 ^0ƒLink䩧ZB"&MOO͛_z)8|wٲ$\&<OyK䩴߶mڻϗLUO/3`rzg_z{CE˶$^/@Jwg(8VDN~D+NWFMK_U,_< \z{ŊkIQ\=I(_uG?r3{VPQRzMKz5W^w)+%NQzxPbLz駞yk4cQ+ > v7TlBy")INL=SB?v28|4+g{N1VjjI2xpg}}p`>x_F3Ay=OFSW߿COġ3 Abpk¿:r|L &=n,-t.ε=\^h'!ƕ7n,~dڝSqD C,ߣ'i<U X^&cp{bo`5݅+.5~(7yȕf ,'蟰*F `sg#$Sl4t? % V46nI6"dΘ83OEP|1Ŀ (>/ % (>P苦;3ie|{g蹪X-2s=+WQ+]L6O w[C{_F qb Uvz?Zb1@/zcs>~if,ӈUSjF 1_Mjbuݠpamhmçϙ>a\+5%QKFkm}ۖ?ޚD\!~6,-7SثŜvķ5Z;[rmS5{yDyH}r9|-ăFAJjI.[/]mK 7KRDrYQO-Q||6 (0 MXd(@h2_f<:”_δ*d>e\c?~,7?& ك^2Iq2"y@g|UP`o@IDATxu[3@d(i( s>}KG\lY?{mlkײ,ʶd$")0ȉyyշ鿪nݺP@QE %P@QE %P@QWP 4՛x`'K`S=嗻Z@ӆ pw55_19\;5?sωYX( w߹c!CEGǏw^3))PU{]_@T> :FW2^7gμ +^~egϮ(Ms~'hɗN\Q03[U\pqq^7=<$ÇǤG##%bhK K3)v[`a gǜ9L%A M/^%u0a⚖lք跿oKfei @ŋyT>d$ \J@uiA~5l2(HR P@QCQϢH  `TT̢ 0Z,J`@~w1Urhl,pZsyUد]}4kԩ+y܈)vxuꫯ8Yn@V cd…~x)2+ܹSrsy p?Z^(đ##`1iL[פX%ɧc1./|%yk~w`HXlP l۶_۹kg! *˔ 7ƍ??6y,@j}_ɇ/`qāiDߥ.@Zo.<N+f]h?F2@JC`kor--_TJ+ k,|&,B4ʊ .r'BTa/J@Kഴ;w8t R[lq|Lt!Ꮏ(e7"W%P,Z+\@J4L yWN <3\(atww SlE4W`4" E P  ` (P P(*BP%0JP4%0B%pY]vDN9zȝ:}r(e%4W (J`@1{uV(@V^  `YVhXQ{%P(WgE@ ZBT$qlX~Jv_bxշƒ[m]V ੧=j9׿۽{sEp'ױ(eF{Lqh*V~Da%Pg u'J/ȋjXIfQ5$dPIQ[ J20mi¬.W P_[1jn>q~z|Ҥr8t~;ї? ;s=zĝ9s6ԶQQϓ(>y D/(X~eZ[[6(\>pQw^gnwtJkc`P KyiNܕ"e~'~b8Icbw=z;v[Z?2:~x7eڄ l#ݮjC~ {/^VXf̘^O-eXܴa R]xdx[oI"5k 7{l}&OZZZTƴ@Lrzs{P%;wNGݑ#GdqZ?ݲeJM^ر)0RrǎW^9}A?7o[pkoow(z{V0e E|rU :tueJ!>xMnwWJ'~,+xlV7bsΝwN* !]`RӥKS_)% P6(š5kTlv嘢dSO=^uw2Ҍwcl O$dQg6C+Wꐚ~: uzw?onڴIӟ}ԩǟpsqݮa+OcM':;;E8׹s=yz[= z6d湙3a`cas LS8n&w5׸'OjokacǎG[d{tD&{c I$h/˺ehzjj*rײ@<a@90ౕBEs[nŽkkd54B#mO*'IxXb 5M캻zR\kV}gav١wTa1"%X ꫺*${」b4w+ӑ]V G>,[r-nRkz(`1|t̐[-{B@pܘiej72@A`ܟs=x︽{6:G7\Dp'IxKXh^V ` ~nӅ1Vؙ'BSn/>/Přף P'5PxT8Mv l]b(n^`_?txK3[KLCsNPkܵElE{ѝ%:a4qؒ4N-Td=ܣ[?Uy_}5wjf)E+|^d:3 SZi 0Ȳw s̻#:zݾc]wwwç䈯= ƉMfMjuW͙$f Dq*ű&Ԅ̙3ghP li/͛7;p# 9}AQ&3D 4Vҽ~uUPP)BIU gH ">d6"!@ k#[s>^u=z>dWʎ~}tnˉ8)-+fxvd[b4Qb0; /2<<}}La1_>l$4V_K:㯟O-q5r72˄¿԰[oUCf4iؼ{#ŷO-I#qTXx{e|sbk0{buʃ~Z[7Mu?#eF[/x,GSЖ>>R3BQYǻu!,e3~U}=1kG]y$Cc`Ӿ~{-".A 0%`ŧ{ zƹoM.#3n%(W.ŕ l~󩔪+@%~[Cɼp㥝u^hz/o\LMfy0ֲZ' 4Bܙt]p'q[NVh#ydawY͟YHޠu쌎x_L,_>+9X ^= #K*' PCۜ?;CW:z։^UdzIQ8s@eSP>%4ǧe5̘~_Wiȕz~zAߔE61 _=ۘ%gWuKWسn䮛5Ac2MxDve{.}r{a +X>kJ5Sr;rp=:1GrPtf[>y "CR,)?:‹0<|v_M{;ܱ2x Gn4MrQȄ:77C. >1z\ǹ.)~V9mn 6votCv~}%%`2dxTٲE= |H\G5F㯍Ob>B)gyF_t1o[lgOUt}{K`)nMK٥ۇFl93{Սn뿑MLqV>@ v:3]Kbdұas7wvp[D=:+Fo W?*![@V @7lؠK앗]%ozyuV./fjKS6|]FbkI菊p^z8^B`wvO^B5҃_jdYosE X308o([sLvϔ9g݉s"$uksO˝>-~$ӁwNvj7wznK^̵k庱UI$uG)>y5,@N8EwKߎ`K4r|C[wJj/wS' 'im@kvM+ oA1qx "^ԉn攉K/8xBN)Ga>ϗ'~G.,';M]DY@ ېptWxp"RW%w$!KW)L e× 26񄌖|{ߋ3Ͽ;EVC`܂m.~w߼}ks6wyer=X0:=ȴ 7cB(:;B >Bz„&4[YA7 ?5rAҳfYuЏxB@>3)(Ompt  ٳ'pʏcwEx 譸R;_Y^I۟~kͫ^9uswYn*DB^A{eIOZbϏc7pJc4Z0Uz2/=?` Lu?nn{zc˿m,Pfe;keȟUh]K _mk|K>fh_1ب,@Tcπ*n襁fz3s_Ml>}7e m1-C}z}&Ę"<ՌkNy.8swI9ݿƭZ~E/}c[`$;eNޙK?νET* JLx'a, #<‰p/?y/*Μzsx^nYb"yKgBXaȹrQkc#4Usb<`! Ky~@@ys 1#f2Q>i[Y*[ڕ)R4< 59f?NbOh4SGsk̗}\7~{SDnGP iP U,H2Fes2wXT!8Qz"1i 7GC8v&b:-8'?L%0Ajgdc x4@dŸgC:!̙*r'L_KW~yʏ lr>+b3qCG>P9YZVY$\h~8TznYa+/)RYMf~maZhvPh8gw!Fm‰s>aAX3d?GND#-K?z{|w.˝r *v{o=adBi3j 7-`z|[Vp'NwrȀ{yhنRik*7 otvvdF*$Ȯ[dIQ6D @Q( z@-!/MJ/Ž˜mvȡAGiCh 7?OBwNO g|͜9wZ6Ru[KmD@P.&ǙӯX)oSV7|)E=s,c|2YXS%x.%*70ЋgOLE,|Î|Bza3--̟}Q͞-(|į@fw*E<`eoKݩ &:;/˗/OC~/# h11@+VE)cSK!9gN5gNKٍK䃟"1i;yh xw)&N_Vy!醕{?U*\).6}#%1pC2#bP|P%F y׮MkV.956!!ig8^ܟ%3e,\2_tRǏE)Y^ qfƤzIߣz" @!nx1'*xKciByΕ/ MJ*C>Mn $$Lkk[׊tf>??K^ !s⏹' ;48sSr_dܽ.J/iSz$$aQ{jaW͞ ' ƅ_}z?^)DgBBk„o-Ǚ N%ݐ^!d?맫??$CyP&H Es=s9ܾdM+rUy5Xqd$ 8ᧂ&"HHp{DXK\PFQ GkbFN {bdGOX ])Wmڟ<ũ{beuB!Z-iNe aTDe|Cte>!H<}4&u0_y*fx7> 7_wZ^¦ Ev/o^--XU"c8<0pK)w h'qx/t3/)v(,X ?oۼW^ 6̧~Ӡ X^F LY .R0ÇJK!7r 5|DL )ܟT.䒏LmY}Nc pF?FW͢q/?63FQ?F%G 'J+ x1 yo&ȅb2;5\1Zȃ Tq&gd+F0f4ٳg뫨4emzkזW/J^tXnUӸwK~)2bP0 !{6'h"~p|,0EW׎&H?H_B=e0j z۳go )O:)mA1grܥ| rQWصkW\ R@}&ur|͈Z66FqqH-xl~BUT> 4ZrJGFҤrے?eG Ä͚MAh#tIX83)Rʫg+F~hLa#:TU!X0"92#}cH jxPȄE 0aXȗ4EqC*8T•5(U*H:=%'-YuU΀uucr7| i>g؍P3g^i`(;ohoܞ>{|3 mYcwDmj"Q_YX4|_a*0` C ɺzb%r+$.\ ;x^I ,;E.MIF_;zݖ7.9cJꋩ[(SND|f<4'S{,ᾬ!|ٿ?9uƑ_&ZZ#Lx,+ u O I;cR,*'&$ jWJE> $=mRlJ{mA- 4DGg~"$XD&Xʏ/ V,NO6nO@͎LX֌ :-|/$ m>×a; Ѓ:-*7i/Fd;ڀg o=^>Q[ ir=v\cE iT˾OOBj壤wMWkz < ۑQMEpSxSbo #r_L|rV.jwoH>ez!;1e5a.T(.M @H٪gBϧ jg7tFaNÃ=փ@xWn'0CF4D͈b4X{g2&r,QQ^~}HMB~T1* M|z`hX 4h2}*w)wp+;IdjO$z k_[,#Mu#NR LL`(YN+ѰgɥY.6 LM<\䉩Bo?1}8<\&z(W@q#Ќ0b<N#zwtus:@fPfOK7,PoLLPgJ<Glse4V49i@{>PrX3H[ 2LA)FUF{Xa/BoPeg,QfHK7ڌn(ye"R,^L~*Ӓ*޻"Z !!fms1_O$o3~NvNٔ ^M 蕂b{3:I=⾠D^iwk GM_<ĭS/4U&3yEQÖgS ,z3=)Sk兕c=ˡe>xqC.HHmi6PS?GJ L%'a!is,.}mY ~%LA B 1\^R̟xP1'%>q#:/ʢ(ݘMB!RDUfJ3f' .|hPG+ `;yQсIq(=BGڑRC 0PS#k86rlЩ9_mmpJ&ǐW@| EKD^Dž(; D#7Ǘ<~Pt?ai\ hO+}Qw4i}@ N+yDPSB-sBP"h( Oeֻ2͇Z=J(rB`:ǣ4y#f! ' 4l_Q푑@D@"\BAzokO/H=Miyfvޮg&$?O(X|肼+>5KDb!B ~VơM>x"-Ap4'1eI(P;L[j&KC}-l§ZŽY@*< 1-/%`O6Όs_!y/*|p6$K q"PWLm=,hUqCB۽ˉHٔG}UN5 $Z-DqJ|&'M M1UͩC~6\yS'=|0S-M50ؓү=rJvF*k4Y7(U~G$ m,> EOz0_B0xmA(DbtJڟRQ&~#j\"oDOƉ[8˱_VI*d ! _JIڦ`VL L27pf^J,K$PnmXWiTCߓ 5 ܼk@ <{?u7Ӟ+$;CU`ifiKڊC-F-J|x{R`zz N(IIjf5|.!2CN dr#z*3ɐd&ӣ'-ہ!Xxv`M}6IeZ e}yX>)r4eu(çA6jE*#/-#p($&Y 0`WOɓJE~ά6NtgΞ*[$JJ)%rD!xT( ofByQ aҩz{<^huFwwJ1&@:O+#C#]P\.h2gт#pTL6цql*. G裴.59|ŷKv1`Wni8Yâ0UiT>$~@x,taZ27ymib-(Qg-ffG]RU ŐUD,aEvz!)Y= #'^xM&*kDA?Y6 8pv@c(4kġ |TMs*'RM+1-b??71S /;v<0koFcN4AIJN",y FH$U9@?qAv0^y]8AP%1l3'?j"^4r/K 3,h, i$j??;-/\-"Wo|P狲AYKKƟrouwUk;ğu{o{ݒ%b2@Chp1jYQ#Jj5&E2ATh 'y@W%AO0ަXPʵOpa5{B6L o % {VhH[D*0+!푗rΞ;t%dHXu?Eeb_88C)hJx3)rG 0lj $Zʬ}t&UoG?)̼ԆYZ. xt-NZi]+<@[cYDHpDĝ =>ƈ["nsEIql5(F +3W\o!/?%<)+~n af09cYgp፭cMc5`%#7&\xہCЎ_kfח^sp츑u*“4y0"C0Tg.$*|D4#)ΣFnfXO9z?iB_0R^IiCz}/M\P!.+.1`$WQ*,CAV!}< Dո>d2_xaGOcB '&|o4ܵח0Jq U0Yؓ_- ͖}꼗J0>*!!^Ϯ>ߌͧ|M26HRR)r㏧6(*A3*Ҍ-ڳ1N;{Mhr^UtQ ʝJFGMB)ɫ/x4CWX_&(m~nC%a}8%mB-cǛ:'ӎfN)t&@G|7IWskZYnЧmm5!MJpNl{} 'H^3_*q9B.ol ^2-)4)T8HR ^BٯZc. /=yo=|P>X"}i-:>M8s@@^#Ow$/]&,eC$S<`v e=8| o\CjP#I J Hɣpd4ڍ`lj|owt̟]}5jq>?C^QQ{`"`YUsbpF)_v3!U4DDR-O(#lN & Ǝo㥘(~%P2sK ԍ)ܤ0e'iˇHjم>:Q Vn_)r*'B-<0`@3_zwcZ0IAoFSۤ(ŏ!7܈kϘ*t&(("e,?p<1 b% HuA-۹uvu'τ2 {`/u+2 q*?+Y_15m|a)=(6UoT30}Т2Gg>DšT4M@Iu+)hd.]&ەo- *@>>Щp@h@z"$䜠Q tJTŁ*[qkxcd4XBR bALL:v{'sc4F rbn ŋڈ< B+utE%d9$KGVdwyclDx @Qb43pu+4ɟAbf;ݙ>ia"JzS8Qa8ڠ!3fFQ#I]9{zU[:iVKd*oh8<([$bK 'au{t"Ms*B$!a)A2Py1$p0Q8&?J<١9}{svkŐ 0`jZ#QOT'S'e@i LdkiT6:2!_<j0MK: ʿn O? Rc]mqu)̗z 5yٚSz$CD"A뎲#k$b7 KBb"ɇdA\1N$Ϝut[/"̄^F ؈GA0nu dB(Ho*JЄ#&(>S ꇿ$w[>jI67=s) '?#2!zMR͏3KR0ԉ+2n E\2>v8 8qmOQ21pLBxXgLﯮ!" wtLAS { x0ydD8ExS،"=شmپ۝5 e|JXGR4-&I/>Uį,#I2A.@IDATx=EPҩ*"2@fnzr )rXǀUєŋЈh<֓dۀoZno= A,*.qbѓ]zY9ki‹h V^*ו^97)ynKt^ %Mԧ+΄UG$P OO$hW-B~"8xgӽWi ,oP^j1fPzjPe[GIYٳ.~U:f(m&u״Lȉwsgʖ_Ш# ɏfR. \Q-yz| G1> x^>=5 Jyخ. )3;.8 j :;$xN8A/p䁩o)D ym`oS3݄7tvkTwL s?o<#`W ì@Wr[,ITMXjUZO(˧[twQ={4W ƾ!@X&qHzśA9f)<yũ "KIZ*4I84^Ka><\/,A Y.at>M>5A MFimՌP1:wc&ub-!/xM05.o9 w(lU&Pvps+Q"y//dT/Z8DaVS ^\@"B#Ð16C~ ?Bʄ<&8p` ?BHϿ@ZUɓ'SZ`Aj7fRj 6k3z/g.ǭF2]m 5| hxiӦx8/F*%áW_}6dԩi4nԟ?`0S}yB?߯b[oYᤙ 7-o3fO2 [ͽ:-9t=6I?5V'(zbAHm>xFމܟvk ^(~]c?8)41ģPOW(X]z{'Olſlp m$&Qj\KCքi=<SӠVl:ߗЇR$Gӈb"?^>S/_>ChJ}q;+5ܷ݊Ԝj2WͻuoߞDZj*#Jn Q0L4?Urk %euO$) R+B[G@ 0E{W:3exwտ~R[_۬AZ\tsk!4^ y*V9Ua0^R^q0}! {f4>.!]_GFQEd^L!Arb6zF4{wD?u|=}S/yw^6j=zDp+gN] m䦛nR6y jA @*> ^1'0䧤r2?eB9sce`1ʜ>}6646z'F7,{<ثG mݬ)ҐU"Q`,5J0\_!"?%}ۻw7B.EBEcnȣxn%ba#~qB:);.oF9lfusy/{Μ9f,l93Fؑs'j4PNvk_2B%iDze˴(^Sެ:K@( 7?,ݓIiKAK@r2ÒE6PY ,y!"?x$g8#gmBR"R Fe߳JЬ1p4ux/>|=r7@aPM+=E)jdΫqxW"D'n~凿O/|#wkx9`aatoir'4ܵ]03"z6of1U."0^1B,qp&4C{a0 >̝O!'^Y9#tTT<:ݑiN+W=s= - Z1mUW^6[!S4ݸZ;6?<ŦKA #h"B.1HS03r ^bc̪tz ݝ>ReїmQ٭?\ nyaCaT2C~> 7Q1s'd|(8s(9/"є(-Ν+ ,<+/ }J洷f.8r}oByFB"0"-Zc =!&'"*Q!l.+4F1>Jyzq[zRu*Ņ_oDiNB!(rjq4aZO.mBV1fE@ɔ̢ṙEJJROR9h'PUޙ75|Z/ $+__Jw˖-WB9(hѢi, e ~j_y;{yWbɗ/BMVt>\",khRO1;`QvBBuh9ۋ~h4D/b)+6_RQעY#t(by_oܷpӧSc=[7nn(Ku-DB?XTC+(TlE}}%kBU{ [*c;|;rI GWUaQՄZN[e-gg@OBz݃ Cd[Y:׍<.;| @=rKߙ5evl,XH 7쉺T@uMw(1 =Ŏ B%Ƃ Ӄp>:wF Q-zۏށmZPzflQɲhYX-S8dWH5@ |qz2ӯs@&zێFRܷs]z'11}|4֭[S[n²gȺpYjJ<,l56) .$ʌpM?¬ wJ]$׋7Is)Gp3h#0|'Tij" zX'׸6E(|Dן<C8^Ge:\B O_~79;oܷ-[gǂbJ)o8}O(cQE&QT5 &4Ǭ|4H/$+aOvNn;N<凞L ?~,fo]Fm*ݸqcJHXlXhML3yaX@6aI'rBM _fb/" |mqe@- e'M}$gc2Qؘ `: p7?ǭnaRO?mI ܂STTEi$[тDxF(BQ\ yQ^׽-9lY??ʭ뽳2e4"֝/ϼ#~{ք>fTr%&źd~n$Sx7JJc8{{)̳zjw['̲a\( QР C=mu[/S_?ƽeTTF}F$Zy!v!zG@z.^^vx5Wߗ~ᆚ,u3#2 t,m5wjay<ȋrթ7U&ڕd <#0rmFEKpdM M(ƉgEI*%?OTX_wu5=jz-FYBSّ@O?:. 9S"7.t,p7((XBȺp{ABh=B‚1`FE>ܱSg]=SC,p^yrCE p7ZʒmX4/|4ZduS~ G:ҽF{]5׸(Ġ|oT e[G^! `<%= Uַ__jCBiP7p ;ׁ{Oo;ΡsB͢ < 1=5rц?z"918;ݞ#ɔ~}M[+%k11dҸW{y} jMGΖF~_4E +&V>ͻb:$7vD!Զ߻ݛ惊2d!.5xpwygY3^? q:O)&3v w57 [nJAeHe+ck__"#!i4̥|k_slh 4N.EP9K@>#ޝ=m֠N fQ ewB,7/׆s{}nvw5܊EBJRڦb7 DBCOX:JCCX?ӭC|F#ri9mL_QrzFDB8;7˗Lf0`XݚY;?"㕀_{UeP).@_߸;ï|+=5Qw}xcZZZZ&^6Tiz8 ?xLF1yϞ'hя~֯_;f(Ҋ@t%+l`wOu廃*cܼ>S&p+TM8 #bmKjO/}rx3nmJ.d$i߿]y@gBٳ'wz[K~&j6HOz~CGݾk|g .Dzl{ 1;KGǿvx[^)w~Ze>c6LHr}Gt=X}9x`Q>O>ddE;zj`=?O,=Tء[n%ۣ\xqd9[EY`^ OF;YJsn]l0^>%JIJOw09>$O/}+ ,셫h9ff{\ jY+m5:?-ޏOsnvonzD'rdH6dF*p}Ç,* {=5mes6_җu5NQ&,q$؄?+Y< SBJ X7`2`qTQ'o4 KsZ cλ @Y,al[d!4ˏcgʨ.O(PVfL”3^|ޝ>sZ?' ~ $n>:~|N&cյnDUMۚE׼bopOvO;w,J|P5Tاza"8|e#҆8 u85…c( .#`2=JjdlAq$@õ!& &&d33` [ zF @V"0Cޟgg/t;dzݧN~\.=oDw|3wrQ9@zF9(, ~DYr r& s!F+B̯d,3r5 g=oҤN)")cUD/^!.Bʇ#FKO3D56%*Џp+#LVлFp7oC͍i('7 ̷zssr=gt .{虞\z-8S C%svtCY3g/ c\N W/-gQDQ0bHXS2Z@HBx6m@03gPMM &< Cjc9VBbSТ Ӈrkfc4}4K=߃)~{ y+a=lY̛s`xEʦrcx /~3B9p r%|%? &OXEFYh'ӕ:&:@['K\ϖ.K+PDkzihRb$dJEq7~zd5kuLV&#؄5*O|vOoo~SzIѦ4pp3“B:+ya?26Bꃞb4 9 ۗjY*xeqy )4wܾ޵iHJ:Q`.O{n _p[xmU(;/ (pŸ8z2R(ٚ)ϪRR;)w#g SgM/_ ;iW*a5~𛐛;iJ?/My+;4x{^Dat#$<y@0y=nJ8ʆa8BO´ K:lZaBYꈯ/YBcq^ ~H9_LSTL0yLC{#_~VאQ8yիT=!7|cr8S!4pz*&AVph2Y #4CinM &AbE(B_1 .6NK<[:_5R%B3²CZfrtہd{q叝YDdd0qi˕0\~#d{ٓRxx*.jmi?DçXx(yn&PVVRY%Cϐm@ 24 (PՀJZz/kL[BDvhˆ 4V>j»/ ة|=zrBox<)B:Ez\ OF }{»x?zpLcxjã@~P<}J(r_8Vh儖Ëx.ȱj@Xul8G*=~S2wgKKkbߺ;:ŗ ̓U--%888 ń{6@hU2KE?0La aW>??8>,<ʋB #Q&ai()r='M|giI ;J Fo4P.c_k}n3f,T#gya_| x^oiE?Dȩ8~zYΛ#ԡvzSY7O=(Bj7zֈ,E~q ?\Q ʀ)S;h<0 @^QvL;&`&a^͔b>4w&k%%oM4ƄLšRru@p7@Vpy编U+Ai{,X#Y(6R5"{Fp%lL(T48<&Y!ϺM!P`v 1[#07 ea6џ'}tGu/bوl2=\nw4!"܌a oF׭[ L8Lj"Qf{VЫ)n]dbj5w?OTg䜣Bd/Up]mM;~z? 7\&YϺ {n:Y`‡F GاNuMCOO駟"ӠP``XTcyv6R Ml,Fg'F[ίV<|j,nߔA9%ف#G'|~n4NHϐZ ?! ?~ KP =8˜I5 b?v>\I/ g(>Ge^2qd 䓑mnB < -ZŤ @it cafWs26O9E`n^4^fB]zS:{7$ehj.3?P@׮]spnBgny=J2aid5L~ ?~f;WG7-2 e޾Z(Iv%PTI>#i@Q!ؘnr@Y'== О}}CC7tv츉afg8wx w&i7L„eCf97;tyٰid'/_'Äs+h) V*,-m$6 >CV<<7 9wVYCQ2>l,֛D !D^gCuY|ۯBN# 5  y8l/ꤜ`^nBk&qPu7%.ְ\"?CB?̏^7Vzgy3v3<;M&H7nnN> o7avs@ g̘[iluuui#[osz!6W|}f<ҷJ%znH4X᧽ֽ=!;oZ3ҖY ?iH3b#6oHrpe ?|m(YJ2?HM1Q0C V9^~ɉƿaОGHaǣ‡ %L 0iY@kk>,('ʅ)o/A&?`Bc`OPVCUqP!QiML4~M%Zj1!h!ٵؘ֬"1@$k "Emh5/‡ts9sνܳmޝ9s33<3һYhuɁf48xk /XR j[?=i':Ӏ `x*жF6Wܛωc =FiK}9X,;}9 n>$CjY\/3$\@I|6 q.p7/Nǽjt{/ܺgSQ?$Px[@~hرw8%c%H1݀7|N7 TU}_F v{ỽy:Gc "O:%wH;Zmv^4||Q L7C83@ׄqQ!>^?(K˭pnVC̀YկݻV tÀ v;ٶ^wL] &(T? %gIП,h/ ͧYY@H>J?N!p9/V@Zi>8ҺF *c烟[ Q{( tճϦ }s>0:ne? UIU[픯Zh pY^x.x9Fw|z&G-i_mxbv "b:X*{~|>?7b7!AE#dcW@p/I7q, uEO^>9  w{|X04M.+62皍džFYbks*bJrD/:=??&`|_?y?ʯ<ʷ^𷺻iiܰŕ 1,ܯ_i ;M3`~p`s2Лp )0BN*׎mi}.o1*sĀޝ]!@K,P.|Wp.(lxgǸ^uѸrw&;:^'&r.26&1zr~vx0pA`>>a'zh g;г]#Ǐ~殮O|i<9HEq~kҠoyi[~:זf2qo^/][OgHقL#vX0NAN+64׎msO _ʻKlI,@bŊDgYO4k M0o6cǪ;ǎhTtzYUiLt4}gxx?ӵY\ȧ97eh-YY\/>K[CN~tU^~ll4,j[ā~1> ?W\X==mYsa~P\BŨ?e! nmybb@Jtsu|Oti5ny7|?OE4{GpsmxkG)W6i 64~ޱ^d~_`lac͛77(&68&}b|S/lٲdU*Oa}vڕzgR}Wr.Hy&{֥"kXm|n|Y_ƽO#򸠵x?.C|qoյ(*n.ȿGZi%;ř*|ppWo]ǨkKgs}*vH~ã#[%nN,_ڗHﶣ~`GB1{?NŝlqSBz}m7ɳ|򤌁>K}+?K9TO5074g:-QϦhpܝʇ:OFUj:>lj'ojժDF7AÌL I.``^lY>=?e8q iD+__ H;h4ӃT=zy;Y{✽w 1ż 9B|>~%!{uڴ1pGXMwQM~#<T)H,˔GYf607pͥ>vfTi/5vi(:z~ 1D@~ml|t L3^~e#6 r#"m6N֭#w?g͕E`Q!9}$Wk.Yr\ 7O/=ķ'ޱ8PI'_`p s?`9{lŘs~_|'%.`wĂg-m#ȁJHx\f BcV78l(C05#CX`OEa=cȁS= ?Ou:@ګc{0'Y`K_&!qc=/"",63/w 8z.q?1 Ũȁ=4'?js|]@p¬&@A4Л 1"b;SO=O3ȁȁ=4Td ݴ0D |BGǦ" }Bݗ(1-r ρYxiM9OuXա~gM} Eϩ:-HQ%"a~k]w ]orBacK!|hb=?l#}Z/ٱc@ZJ@W3_xg^ ѻq>|! vޝ;a#DV] ^@$<9_G֣=B| :|v^܃25C88PIxU>dyᇓHH##|~} y晠9+s]GxÑgw!/M˿ s=:`>B"<0aUA܈bѶi;ﷴݷlٹsg" Ҕ g1199/* nmZX0ٞ˷}уc_ˋ Ƨ9Pb΁lr^e]߭ogӦMn|k8v,HQ}ro9\ >g^:@B}=>h7rcY i1*r r"x ,P ю@+(@_:w~|\C<]w 299PK/po/Y!pJ{5*Aۓ5IDATO\+aTLOXeuIENDB`ic14MPNG  IHDRx iCCPICC Profile8U]hU>sg#$Sl4t? % V46nI6"dΘ83OEP|1Ŀ (>/ % (>P苦;3ie|{g蹪X-2s=+WQ+]L6O w[C{_F qb Uvz?Zb1@/zcs>~if,ӈUSjF 1_Mjbuݠpamhmçϙ>a\+5%QKFkm}ۖ?ޚD\!~6,-7SثŜvķ5Z;[rmS5{yDyH}r9|-ăFAJjI.[/]mK 7KRDrYQO-Q||6 (0 MXd(@h2_f<:”_δ*d>e\c?~,7?& ك^2Iq2"y@g|UP`o@IDATxiu&=o}H"Xve[88DLLwn'ܳ8gmk:ncu,Y$RI( } }}ނ7;*nֽ.'߻2Oݎ:N@#Pp=Kdwv:0">33}k(݅@ew϶H[(ܶqi+诪Us>gL߆E@P33c=6(@uTpV7?z_ݻwO@"S6_Sߥ'6Khn Y֊:Ehk?|YoHuS: œ.,U[EZ3я.x.@ ЖٳgSs`TruMA:J6h;9ɝ%7asXhr/?o``jHCH]@[:W^M|ٲfu]pwqʕTٚtS&0om7]>QߘB|챏?/'rLM &f(QrN}ΜtI"0Yt*H(@O94h^NQP:;z!ZsAhozؿvM))@̮o6詧_VXzb>00ŋK> t_vM`_}mٳ}ƍ]6mHW 零oj?VW5 07L1}?{gK_dskk4A5FN~{n;H:Yݮ{{5fjE=ر|̎Zk?yE@h33]$wXu:_IsPm"wvRJT;jR5CP& 7gg ԍpF\r9Ȉ'>e/Z˫ "\r<'˗2aVe2tq:]ܹԴ">]f"0\253={GSf ߿UHk="#03{)#m=۴'T-E@Puڴ__|Esbm"(݉NtgjE@PLȄG3E@PD@Wm"(":h"("ННRE@P2Ч2ND}:TgE8##ř3uaQb'# =+@+)Vu("(m:m!"(" hZ"("ff("(@@Vu("(m:m!"(" hZ"("ff("(@@Vu("(m:m!"(" hZ"("fڬCDўt=*"uZr=Uw}2_~ɼΛ*(@!Sc"("PTPE@P: u:T_E@PED("ttZ"(@P*BPE@4S}E@P PU"("ii=*"( @ E@P!03Ӻ&uwm"tjP}@uX$E@P&!5:PTPE@hW_%TRE@ CʀuQ("0W0)Hfɗ.]2SSSFoY]Z"/ᇎjS4d޽\x U铝pAm_z%SOV6E`vogMoE' E`zk; \j5_V!(" t#A)"h}\GZ֨(@O#=_GڣT E@Pz5:>}("еMԅ~C/"t1WNݬ@;(@!~4Ju:#"(]@hkwЮ=z)"Ёʫo.T{GuSEN6} zIuTEP$$E`O>cX%n hƍؿ}6pHO"s];ռ0ǁ~2/88ppq{6H~yZBHF@2.BUSzJ`dϟ?oN>͟Eov`G n3ϴ#>:Rm֟f@@ )"+WѣG͉'1gΜadq;`G!ΝT Lѫ'c0kD, x=PjZgj?f9sw<p ̙ß!nF2&$9<} 4@Go)Z! @Ν37at¼yA\~Gh-Bz{ fVf#\ d޸N*/ver2cw2 5 S3h׶w'Ƴ*ufwڕ}Y_~ɼ0yfcC0XCM 0UV\a֬Yc.]:7H $47P"?Pɋ̎N]U+QQ/߻w/@ (ϟw##:ćJ)]k"G _|`">p$Ch"m6dF @Fcz}tSZnMmKaoa_ pw<66f/^ql 1| pg]>;~+p\x\pSģ0U~f^z%̆ Be5Ύ Oj蔤I薞v"cy_;_}< /"7'dSG9") |e[na *9Ʒ6;m'InЍm z\z5W6az+Ve˖>d`΀}/+j~8# ?1gSSN3pIs%H]q8?{[o5ѐjڰ͎NIt#MnUm#7xypݯ^ڬ],Y!A+sWzcǎFG,<={#G;4##vZUB;;6?O\nO=܃z ?_ ,h*i8  Xa#oupyaڮ ڰ$fG$M@p/"/Dg׭[g6ns׀ vHc `w+W47t/(#zz L˼۴F6k]52wv mXS&@SzۈQU>l4w6my}1Xk0gN֭[ٳg͡Cxqe=NMMhͫw5w}ٱV^DW@F돷5Ŵ^z׻0O=/^+, Nwǜ8>Vc(\C: !j*2;̇~hǏNt?SR*dƪ|   `q||@ke@}pܲe $8 x,'䩘(KY"57Cl5:~tXa _~eǜрmXÅ~,ÂVY| KG׫8c#G^Y0*G#0L`z,bdl>lk<-pw4Uvur^h|gІ'8;:%i4uz;&̫Ћ#7#|(m0PoEe僁|}QIx +XS c e`1#Tx4@F+z}h:D@ )c7lAa t;y~pc  <.W# Fp,#oed y< 33 ڰ$%gG$MfP@τF` ϧ!iJàrE@Cc8w_ ~F>=p0-gX6pdl;kAQ7}âe{/4chώ>q-4]:(MG;g,p?]'"`h^>d$,ÔA#}8^  X$o &BUOaգB;;6? )I"@-&mAmrW 5YS×><4(vzb^}wq%[HuO03p F0ʀͅ{Wx oCxO?)1ahvkl]aeh"Psn _9 セF|Qvo}3U`:w(OIaz .`W^yԴoyG&ȩBhxgІ'|OIiI4vL4EN o޼,w;{Ub~Hcku6#aT.0 G®XPO=i>Ĕ@hoh4?17X)-Jzat}1{%34Ruw} koqɮ? @ib1Tn6lPUnzFK0e8t0 Ea=jj>COfg]hoh֟oorCvU;蝾nba.y0Gk.]cΘۇǼv;k7̅+ʵismbLM>tJq< 12qpng SOJpZ36Mp^.s/m]0ކ|13U/rF7:?] p!^{t*wٺ?e=s;wω͹)39UCs‘fyfArѐYdجZU)2蔞P=aؼw|?.e)\31Pe*9NJUZ"kF^{?iqC;n[f:5,xJ8W ޽HYZ𵅋 `hv{*ЖkktjU:*{} g-Emڴ‚/1z+'eZ`׋~Yt|zJVA};1Ĩu ' /;h=n$`,: _QTk-Tz$GNF?cwJo<2~p JWUx/7;we0;B0=w2KF8pK?~LY|G?o+{^- Z4&^/od+7|}Zj֡@Lcg1я~wCSwe%**.J3_{yXkvpp3#s0Ctx UsiD'gxc 5 JWfUʻӶt,w-%Z/?9b1;W?,kuk3$¢k?pa@q{B  UzBd)44'iu7Жou*OwrН:kϛ[ʐoY:p׏;F.J/*~~@7#/7Gf0Dg^?fGH.\1V9M8UIک ѫ'c/7:bac֌Q2O``͚5رcG1pS^L#Mkިn)/IJh7|}dUzP.Ku+.,8<x K?{;s ̺cfЌYq)>hh*[L# !4įgp \rdݦH.J)L G,7`U? 8%pĉ\&#rXMd m7_F붲J mOCࢍa,9~cl=دc_Y/)@%K^hl^l^,[~1Y`!Ҥwq0ia蠁G4-w|'g,BR<RdL>ȻӼpFIk~TYPUV7E,SD^QzIŰm&?ǝٕ1U?|GE;_p/57o\f[l66ݤn m]4?3fOoNy:j:mX _f5l6CYc_^[/c?K~F+"i<7,37o^j*4E}Z;d>QwNtwC<5_y7On>#:ʒ %p}tQ3??C# 7~Y-:\Ovnw2-5/NCwn_e0ߋ'~dՐoKS' bG^~w~~ { c@88`M>[џ 4갃! _U4Жk ^@/rڈa~ *]yGs\̛7%LMa mxtLMN&-ϹEe<|xi<91^9l1w>_>T~{?h@^yX C~: jG&Akr6)g25\[S(v,u>,7(^ecXCU a탳yزܳc ݡ6Ý'F/;˅#f2C#<~ /_jj(i/~ڿ8S s53~~.__ _[0L_7'K:LkݻןXדx3ڎŤyO`M`*iȻAwLzyy>ߞ;ͬ|թ|Dseй}7+|'}x6fAVdA#>.> }0?~U wm0K@WѨJx*f ˠw9s61C'wrr\0n.3pesRkiƥ̆%f߹WCͿ1o7@"#9'g>gnsb1UD^W*aPXuÆwDLQ*Ⴠ;/ 7/piZ\? Jٰ޵Ѭ[QjR hv2FG޿G"| #\A"g/MӚF\5-Dٰkڼqf*Ѷ_7/mqA D ;(~򓟰q Z#PVJ"}]#9~f04LȮ?I|:?I'࣡LU{#ȣX 2_w[4\&!mդkQ^oz)[c dv "8>O\1+ *ysJrN/]3'/\3ǫYC<۹M>9t|A^[7h:[^"N֢RZ8橆o|KNXE):M\q}4:i(=O̧^6cX-b?z_}V/4޳ٌչy.7,5Kdž"FV\e#M@0Cy9'y}pJN;̡W.5ع#)/>z{yf+ 7n dɂ# ΀< !L ぜ$L ?HY E1i(_mtaۆ}c~mszd4@hIG֥༽; +G?uIK6~$돗>T>.:EPq`e~Z{g_;[=</7K _Z?/pǼuV[-4#՚ؘKWO;`rPַ6 mKGW-6,1׌*Azd>y3cSbjl>xp^h {VlG ?ck8/Uoh gFzßV/ԋ\;tY-]v-g㎪x?~?iC{~OJea3}0THD2ɴg՝tʹ&xIrD-]0hܼGځF-_f%=1P4̿o3plyA&nހ$,^[oehV6|ڤ6d/Q3R>.@5* 6sǎA{0_di ]?~ ؅pVGn[W.]:CVuVE© y:@9{//Lѫ[o5ؼ*OhV _6Iق?)Wzg\I-PZP/r]^n߾}?]voPjyܑy+[NlۜO; ߱~yfzM1=1$ȇufӦAOؼxy*`A!+Y!/l37^@;˯-=~e<%7+ طUn'b 4~#tWM5D$bCr7TN@@PzU?RaشiYnYd_VάL_>uPv3|`+m/[#}+1%!􏀃|'J=~8D, xlA&̶H-dY.%DqJ.umXllt/5_m"/tpFlTd WxG<P$F=@pOزeK*.EĆ.7ߣtiu}fݸ7笟oj3lpY8O.\FJD,c&(ԕ$,4c†oK'><=n7y^\4`IXϲpBպ뮻 r^nQE&LUohcTk.Ѿ^z`gwda_?L6*Nn1' Oa֯,6 Y# ]`Trǔ54z2@b-fуHD_Sk20OT bD}R]{.?GBl7ۘ -e2x$pϞQ3藮c6gvqn5|5RZ6mOV}pa|R?B] ؚNiX_6x_|^]~SK,\ef#m{gcIwkPΣK|','&,u~XqQ#XWx-Rp<7a!e=8ݹ~Q$Ѩn^ņҪ5Ie/Q-W\QRDӗio?ӅVRѣƍ7JK.N:y@ho7EKƿ؎+ȇ{h?G ,U.bu缠 Z_`gu~{)31 (-җc+~@(1l.)pY"G*Txæxpyx<&si1-9 R`c+֮ୁ >6̧3!?C$֟WVSmVtM`@;3=c0ipc_.e3%O?w+|rj|th{.~ lTa!'ŭ"yHrًJģ/h$AP\~J/O7Itc}ټZZc # }:9"˖ $ix 7V7{pY4FRy*O`A 7o;5ߨ7T~ Z>X"@.D̀;"̯̿X$1?ٯȻ74["wg A'Z$2G˒ $-J.܆1%V>`*"'D 97ѫ 3GsdрySw^y35~y Ԝ)WRK)%#q *K")l"A4M>)1r\mMG0|fz}#+ ~<'b[ "Y5=5<#$pom-@IDATF0 ~2aYPE/Q֕<)kTY>A--_ J%n^?av?LqmO߽ 8†W سgO\HkjO:anYXxGwBl7"1}ݻ:ӗZyJl`bH 06X@R9ohRV62ovnaQs6޼-%Ғ/ǠccCP`c?Ḳ3>J8zp%o'p${jVF4sYuT 4ёtp\rbt|N"w`1[WW;Rt}aqn 'M߸= N@ޓ-XxaK͊|\|)QU*d!;)6^? o.+S]֡Ec-?x< w?\\Dk?۸ѷh;K2Q،Mx[ZT"KeYe^ >]G$鑤&MѲEm[VS}dWd\eE3O>{`쁑F ŻBO;ods"o6_~ Ƀj|uzPcދ.ChG\KZ,}t:6-@vLja5mu367Wng}Dk-+.Y}bXxW-5R{Eۃ34_9aC>Omg e[i/~ڲa0a 9T`P\zv0I~ݗNqPe4;ȩ|"D1c\+htZ~dt(bkVHf|6oޜyX;?6T;2H֧.'AKU:U!rp7MmSܷiW FzSrz?/-+[(L AEJ_'9/xh$=T{ 3D+MwQ@ ۺ[:S<v 4Jx3}!3$F/@! NOZYTFk׳ 'am$ʙKd=.6/!_h+9NqIbXP!U;*iq1"& Hc RG$ 9^ֿy$sh>έK\D>м9TEmQ ϥjRQ( -!,TeflT  C/eI%wANoN;>$m J9#~rsb!ן5+6K"#~\ eS3DncY]h>J=ryemڴ/%3H-$rώ4mټi=;\OފuȠecjbapQ/ɈUD)-}]fIXrM8cuH2F(L wu8-0[8HD\-}I.&dg,A^ 0P-wpL82Rn|TP D僴τ:G{N_~pMt#,  6e8Xm> Rt=4G)-S xwM]Ů+' !GP~v޼|1$FH"a1/$̏Jcʉ,%菋@:"80yd8s|qmIaX:ACÕ 9DQ6|pQC \ib H_V,kFd("O@߼EpNuBLgn^=JC+ծSЩ=W@oذ!"!0 "/`/4o['%/6짜]YlD:_@|? PEX,X=C>SqE,"oXP6ڶ  ĸBL.D!sQ8>1W' )~ 0+(Q*)u/A1&4(N@{qu*W}Bt)=QL.[^:h96))ֿ@FٷN箲w:3@Syafb u@N@#>].E)!i1T6էJ Kx~2 Ѕ@1_O$$-GpБ8{Z|)2\:Q!.n(]) n9Sh\WS+a7gsPdw@8pwG0 lsw;*V~}*Vk".Z^޷o_b֮]ٕL^G.Z~]\z <ΗV߶y50(y[$%+"Y|Y$&bN>)WZG)O!-@`! aV%Ei+JD9laI~$Duf/,]y ȧ2"qbg\~\-PB僧fDyIg̣ll}5JGFN(d%el;LiL "d +bu3ڜoQKT{ܝw1rwqK)kSٷEk;4)xͲ1&61z78-p`~R((|1`RИ$N,B-13Nh/ۈ ~$zA崓6\(rc! de1)rIw ؕI |OLpY$ 2Np%5c^^ dF? l4B~`A +*fRg\o$m&A񰔫˿Jq_4w #i-V@$:̥馛7e{+O \2Ά/9|8}B=8鰯 9?,urћSF=@vrܹs#[-+]Ęc{ 逍rQK%-eKibjxi0p5 ,Ȁ$83ዂ&2ҮX'XB|(Xm-tWUHS$"ō21"ߊt !D`HHTG'";nZL#/=z%bљFo{}YSrfJOרtYH_j_xr2I$ ^I5;gJu?KEXBJrd8+9I9 'V6$Hh4}!w30Leart3\ %Ȯ8KylXmlUFq88[ 35+Rgv`,M@t TC]O3l)ן$: ͩXݽ@ѣGS/A$Őgme]Գ wH1̔: E8.GmiDls"e,_| d?+Dmyc$HvQd!Y8sZ{:0](DL O Q چ/M 6!= eÎ-$ʾv99i)#/t w[Iѥرm%x1?)SѴ:]v:<~K&}Wts0GJ.b Gx xDdѶK2pV рzɧZ[8/x?[5/-r%G_4E I[H!ǘ+p6R8p,vDЂa9|ɜ1K[,<{m5|fֻ(E k\}Rͩ?:pEE(~!yJD|(ʾ/&~싮c%AbK\|MD>TXS`f@pZU ťdY2[-<Σ8lJ5t! N4=*>6kqt NB"'֬o/|F-G:~#oȋO[MNOJWca:)SVS mP8{i; ᮃY-GB b( bIH&!aa "e" ҡ461G:7 V1~a Dcrc:kC "chK",2Blu|Wt'(n!A) *ؼ6{g4E5Lk(5;Wv"iNEtTA@A i.@|GڈJ?bw8ye^@ z,[h/H*9B@GA198\w$"s#."-usJ5fTOEBi,*9T"!FHK^ '-?pcLAa-Z 3g4N^NCvi9r}Ul^BTځiOdƧ@pXeI'{Eeק9gkHJ##\@Uq}TVJIgiv3CKȅ#;>' W&d91>grDTOZH/dI3"1ں jdbrt⟕eG1"(lDer:/"h&R 26.G8έF5 "-`7wdiy oIu_pĶQeтs0T)SRV=ǁ=zk/^]_g1/^,NLNOw(?K<,1u811/!vVޙ{85( qɩ=셈l4`I~Y'}](gU 3o `p,%'8VJ%GI,*(LzM[H1+vʈeQ9%d54 J&_i~b l.\`n!\{X+'qu!7% %#hӢES  =l\J/O>ukZA?9[0 PW))9`_.FHKrzECM)E R"'\XlipYnPx Ywdk-QXB"( C PY%c(b8`E_T'<(TFoՎ"/5w5e:E(I#G s;a[<^+;~Y%VnpD& vY}+ y ei(\SN%]g%!O| 6j[Қ]ȋ=3%oN]8w>?Fp䋩y({zb/ Q*8`q}u$Kr @a辩chTp&$I>Fdz9kMQu$*r#R'"(B0t`7~$!ڇy(H٨AJpq)%&L)| ]`Wr\/>:$ kZX4N Chc $O/k6} }w`5,;\2H`:!/3<޿^4`>tV B@ga bF9Z.&`\N ʱ^%G#I92+# ۯ cm!Wq"vq!b~(gUta)d}D)ZlI\VdRetrVq!̴9s?]a L0b:aŭ^8.?GurctLQ? aV'qoA>i,?Y=Ad 5B5Z.c .tF!WFC#1(%F"1 Tc)7XW yw1+іbpu\KdPX4g$hNLӈ6V#Puڐ @R(-q8r#~jؒlce,_hg`?0p&['8p-SR:KCG [2esOYʟ@QO'CIy>);$k҂##{@ Εt_\䉓\~9.=^:@.r"_( +w%DV^`aP΁%A͵]9VR-Jp,ĭD&;`%D 3fGxj;'ٹNu$;a8@.ChB$t54{ș\CRޚ#s/ЈmfhB,I eH*vS\W8RL3=KsL+,gVzjgԅvE(~ǁ;( lhW8i_@x߼V~GÝ|I8_-:Bl-k.`FdxV5@FQz9.M<14|#8"W| @9'c\28D`<j3 b%^u k'^07yn2Tx꼑Nr9pPsqD9F X\-|X8nF: Yl T8 iMI:u]ңr>4gEiP%~/FIt]bQ?Ag@g=Iܩ_|+;s.%AvC P2`XXJZ=N)rmb|%dFd:P#d,4HU $ 5|$h/'$bh Xc`s8,R\9PAeSV 5#.֗<wD^|tt{,U`cٗ#gvֈ@x /=<2x6؜㭱"]Og^e*AO0g[%uw|tt4?_$E `Y8~YB28E۔y.x)oc锅|Y"Ya&ATIFF`囤 |!nk()|6& 9?y mpA|[C(cP6ќ@ʻ?>4@yg_6Sd<9PGd#N3lYI)ìV+f3NtʆiTF#3ry~3lkYzv y#gM&P{YTڕ6\}"?SO}#P{9Nkg[ẻ_@*di 1RȻ++10:tf]Nj5Vr".r# EDT.82?X(/ѹ0YgmCI=A9ɳ-uz@&乴Ye2l _,爍K.s6$]hpM@6y4jl7Gؓž"M_|Q~cwf>ʏ/.%d/b( .?`NtXGby'؜b"CLǷ}|'qܭ>ƿTpGg,4+ߨYX0)0*"X[DrFy]4p|B4cN+89f^&4~ |`܈Sz z"mcgO]_ewijH[܊ 2K3Jӥ`UBq#! ҦX8Fd%im8 m{@ su?8=%/]ElKLG >OlFCr#!"J#` Ɏ+r",!J ~-qv'j G(q|m8X@UgeKds:&cG \9-'l6uL_GΤ(HIɢ%!,d+m;;J8I#@h4[Fځ@|-; hqX ˱&I֪]>g{߽ou9U Uv㹡XǰĐ={bwސ,~LsCQ | LaȒB$7R=[zMoD-&s"N)mo`~\ʲ㽑2[hRWazKCAA䋖Znu=u7!XhGҕqݶ0ݲ_ 6ʢ>ո2(FUi;ڹ$u-Q.K4n<W4a G^-W7noč[=7'^h/Vـ%h/ z3=#݉롷4%>pfhMFrd@F?CZW3l !)`"RhY*IT+΁MN | S0(^JIayqB0`i-0+ߝ1Kʒ:ܝh"E@[2<:twwqol'J =ߴgޜ9VM֍tͦF6f.[q @{A-hb;뻇^qt4boɶi˼hUڷoͫe](PB‹&TjC 9КcOjf8'>S)]A6m{\ 6 yj–V8KX r*هTd|;q&^EVK]$EMYu=?N~3%ש}"M)/'Xʓh>~?WȘUr[ƺ|F5xp|zU³_>w޿Xw1nPq TFg7Ͷev [݊oh '?90!K~ɖ `w\(yp)pOܴHjHC n_rVٮd÷l8=mݑ'O;C6dVZ.(E^\!#pZ,o{(]rp۳w485=Z) Lg4%;/6">};6ʾ𸉊܏ t7o.=-6V. wX3ɤ ,q'p<ݼw`UbBwӬ9\@D/9'(KQXSpc!6 i8LHJ%׀.5s|)Spa7i%(I|AJ鶢n* /1K^{59X Ll(7/Ё@oW'<P![y۶o۱GǺ߸LϐfN$*?9q `- &]Gx ܎- ?kJAB?xh>@(L FtPTw`-h*YmZMtW;-{zVP,h\_d,ћ84R+#W캿?,dpnT}Z / GEsa%5b_lx(&DsC@Aًxꀌ՜(}兺{\2 QflʃͶ[&[ɨ?w·ӧgjYhmΜ[}`wnJ{ bN&<?b%ަ9#֤TPܤ}<`]+iݹAuZ|A+|K5a)žJA<}_Tr>:!)QH x p$tzY[W0 fhMnݾI;eqAa:$V -[OkS'<OFn5n~_[X?cwSd;Dשm[e;[pqδk7 &cKܙEaOuy3B)D.Y \2~%֪P2$V܊.ݗwwiH0LN>׬ƄTR8\&/؎JJ\,a%vcǻ_׈H iHF^&4]]aImzbFRm_ 4ĹX}{wt˒)~32=gPVo6U|rs:s:U\bdضx- xivX}4_B(>SZ<6tKb x ,nx2cPD:+$#m`Θn7Fn,6};n]~=\6dQՍ$ ]TnKK #XzʶD5| k :U7v1E=O}Oy7z+L[\As8O^c͂v4tN-|e72,>p4y*\Ycܟ?6bb9Z2l1s&I# :aiWyrLr*FC1$ A6dT@EQ%(G%S u#9X|"TIPERTyY,L-)|#zZ-'H:!r-⎝5 PV&¶qZ1r. :XuK=lH|سSciХmzM|xzU۶-Ap;>Ecloh}~ћ"@IDATג]ꑿ1hm?Y>b=Y'a 1ZgjP2:njKAԿ *NALMȁ8.`)63=nED/Qc<f NE n(߹8RNX_ sC4#!]rߨ@_)A}I<_,OkN uw_/wxݘE[cg7"+k#z$_+3k9zezݗnxҔʝVAX?kO0 J?Ȟ:SڃI\ƽ,ۦo/zDk5iWG yk=?iC 5R2nȞ]:KXx,NAxf^F7›"Б8@ `}bw.8z{ڧTqLO:dDWzm99/esTD#%U[֣#G4nEu&7~#'vz`eWM5ף$)y$kd3%JTcesU?nǺŗ0Xb,I%xy<)E-Il͉ݱgSgFO 4#@"-_,<2[K[MnbHbӹjZޅc#lagu`;lM7uVYHZoV`;܂7bo@S;D8gҒobjrPY/&jzʣn:Z]$ ]ps|vBG8Hh/VՀ[ ȝ"Òژ6A(Ţ]Wsѐ,(Y{I~gg?cd`>p'Znǔ FYȢ,I8K`c'9lh6v))z9%KCJ=NdQڥK;Jmt;׿5{{}Im86;#b;ꁭ:Poʷm:{`NN<5卙3qԑT&d2bΝN6kGy$ W'H(!m̋>6Bya\eJcUnY7HiWGCW/S oյH ggGǶH;xM :0Kз llc@&#Q虩1! AJHf%25[MA z$TS P%3wCxz8Rf/r6};rk췆ݟ5^@koQu}nٵy *88y Rf# '(A5EW#D;>qz t 4D>sȀΌI-2_1CrpIsT@Θ\)(3c<8+z0%dglI~Eľ4TPi`Ce)a9.P [@_/{kE (!}jɟ9ciرiTj7}3lkZq>fm GF Y{v Z76I`v=]Ay{&^sӖ;G9{@a2Ѽܬ)<$X[QtVtfER`֗.@0& ADTA^@n'D AhRC劃Qz]sCRIU҄`:jWhac,y#8lGYQ7KwRМJ5lOӯ^ZW)3{-mS?^ڨGەٹᤗ35#m7ZXl˧F_\r8Af+H:!إc 沮qFS.fCY)Ւv Vtj(!e&aj~ Y?t H UJ=J,${=._¨g`$܆/R4/Zkb9erM'7)Ǻ ɐ}= ]9aT)9˱$M,_cz-E^+*D'ܞi˛W:U3t s[ބeg.\X }Nc%}eH =㈍€Đ}k؀s~!kR9xa +Ȇ^^E"6WY%ce5vXt%V4S~B((H,Uy߽Mߒ&a.hBn(;v\?8٩)K ,`ԝ1A|Ai=(ΙxłoO-6gsv}EҷMO&;zݨߒ3qʙ,Us-x6ԓ؂ppxuɜ ˃LQ\4&ˤ`V8b1hqC wD "Z`K $TEŷA"a]A$dJM@gXt#뱒'O[Jc//OdRt, _#qO*>.3G:dde|m%Z4*u1~Ct+?FʾO~+՘!$Vmۿ*^bnGmη}W.ةc9* ^hSx~SύNmfWQ^=m vnD*FDaGW`Q$|L)& p[ئO%Y)_%B`E)ȪݢMUƤ+iT@9eس$aHHZ&l GM./\[$Nç~,,@iruy`j|Ǩ |SvPNflN%:Jblnqkqm6>UcTok=b5 &}jݱcV*>L~ɯV>Fm%CGNtożn9O1-*>|ow἟v.x>)h)I=pDW.Vy}*v DK0RS 7F~7:@x; *b;hZ)d@0fVp`~No7eProU?4iF0\tU@A)eAj3W\KKeh u0,MuzJw߶w[}'f <6Հ5iSoʹ 9ʃ{vlrw2o-g=x5vZ9]xвG9}f%_wj3{it9tYJP<BN", 1"Wz"vVϗ37kJВd/P`Yp52oKC)W.Z0g w~~Q=R@"}ۥY6` ,Eq6MZw|A`WigDӋPHZ{񞸁IrN&t/z3ʝ$mA?`(SMl o}#sfwIyFLW_64ڤ9ZP#k6z y)4?^-M cjRsqAI>#`SU4/%RqF! 1/~P @,l\##=d\ F ޗ`͉Tf[;9*-,mFtDr{ \4znWcV8>uh ߴ8^;Sgst Z nM–6diP8ub늶InztPE&.ԩS }1u`j"C}wݼ&#w햚de;e@߶ T?S'X !2f/Cˀ?yK&rr7*DA +EBu ]6/򤇔R!H_V BP5`O>GNcoJE=!BLE[ԐW.Y{\Bbi>ݢWB_t4'ț5羉cGHW r:3wе,~9&%7|s=06o߾曳z6mFU~7}>O\]vu`Ϟ= Ugնi=)ϏP;nQ!WzL)(>2&%\_&c^ 9v.4+?}մ"BGXzǶTS< TSZ+,ctG~e{¶Ov@q8oI/Tq =E#\޷eutI|Ԩnf;A 67)K2pѰhY=mh]|&ʴj<[~GA37ʾSՐ^2g ەM_؆b$vبoc LAY<}+;ߍ_}Pu>04޴[ Pa+y~R%t$RV_Y$nHOo\EJm'AG֮b4tr0нҬ^!=t W^z53onwNta_~ mPaEwWG;(" TU)b[7g#{{ T=uiJOwX`l ~4y'-l▱uq6~t'};_{l >9J3_[$ѻʤ^eAɓB^xU;=7w>%w=ˀ|B胞Q(gt}s~*7cZ^Ji׭(4Usۖ+ `@9$԰Dn2T"$ <ĉ&XN$Uo CR4\~T3Ͽ=vBЅV&9jUdX$7eCL 7ԈuI8fI?-Ml 5 #&8ʼUSY0X=*-,l>jZ7"oo H}|s lF(=lƺ8076qtNlAzv5W\eapd[qۡujK\Iѣ,*A e)4h+{/=ىCz PvOLr8uMFAlcݟ(_\,,I,gIH]/q PUbm eRO}sԯ9P};zZ+Xw.$S9b9 3C\ڒFmNSim=qz`0b6+{K ico :0;nm, ՗rg>RQ_3 e obKE Շ$kՌC6SmD]}d,MnTrE1x>+f߯L41 /w~y{ (lڣ ]'lHT"JdRUi8Ө6E\ #lUGҗrRd;96Wh,/x_D`4ۜ@+SbؐAש*ڵX>}xYq[kM_]ܶ0L˛m?~|r{,=Bz\\MĜ##4=qڂ>=*G𰡓.!%EG(r9JSb-dq&CE&hVH}fpc&S~Ձr>Ng[B:ة?3ļ[طʏp*xlnoc7HBM/JrBz,*_E4JEhžYw[ّWsZ0k;VP?q".ƚlsjeby QkZ[4|;--6`y%pa͙ =v֙ؤZ *]I4U\',Rj&iAIIQ.>J灃I[1Ml?lOܴgj<7ZlHSW_㮅rۏk_ m}5Z$d#6h{=x}хLM@Ni'[;X. =hSFiD m1`W1nxC9 &kel!&]j^O5sH{@Su?DmDrfZBL3<ܽ)sx:;*6/-kdWe^ѫ iLa);Zr=Y,b=dBS l{k"P#Ya%+'ic^o |Vna_32kWz,07~p> ;ѲYvdЊG^?b"|a + Q vҞ>aodI3C9/p _ĭ)n͊l}G=VN%-d0 R+]Sv_my6/heKݑ#Ǻ'te##,W9 ? + ek hUuE*0J-Μd膁Hɣ]4u ɩ@YЯ|7nЮjn}^([{_BؼX6ОXJ_+7oN S;1 -P;BËEjV6B١+o@VPۆ3G':j[O{ )N-;׿ThM@ozDGzQhv6_l)AZ'o=\g_NPjSWJS ܅]7*-ÿ8$(r,w)>"v(-'ۓ2R1q Դ4ϳeڼ=0fiE5C~J:thu؊Oۘ+wtx&;[}5--2|f;ct;6i7IҾ$DN !yCZ#UEq$19.S~Y7Z }e&o|1{LlЍ.ma9) @7T⌘|0}4\2XrO fa$ۤ#%"/Mw߶z[4%blHjcF#SXscmڦ``MgͰ0>=5kjwN|WUx!"g/TLٟNA%x~T'S;fUGG1sW4 F+rGr3da+\sb|'G!)ޜGl d%Y6՜K'&]~mjK[k?Zv-;`ֶIk?U֬~=Am?\j;M-ny`s竇NNݡk|6J`xV];o> `t=rIR{QVUxG_Tc粮72NjIxp7rN233yMNJfb'Zl%пW\]cs{Q1;2<ܢ_P rMYh^wL{;xlxd^QއW6uL迥v_b[$~Zt^(u)̢ԍE3t&ˠaA}[ozX=Gneϟs{в̪GݿycީOډc9 œl"|{wwo;CpS+hlJ"Z(^L\T=Ȼ~9mp<ȡd>Ic=&vh\I'7Jq}n;J*<_Or':B`^zNoʜF5]iad., L;i[@ ~ {fS73?-JWkۯb 0Wj}Xk_dSg~*wW-x`ey_޺ș{BTnn~#P>xv?&<{BGs[ ڥK@5W-=I})H[PSr~mҾJ H24L.P{߶Ûv/0<;Om)_tq2<~E'(pa,Z]$JNȪ']Hh\]=.i׽c-9zw;i9>PSũn=H™'׿iU$f{19ӫSzPh ^FSнHku>'2$W) oFl?9gθ2iZuL>ouK.{l@3_Wq_.x@Gٷ_oI=̎5Zw}OQ姟?h0Dv^|>Gsvf[lMГYFUAĪŗ(TpW"7XWu*Y7x|=ۏ3˛\(>[FnyuxYr'tyx|]Ǫ;sp V*,(~$YQ A^ۊ_)c@x.Z =O}# mJc;`9mɑ$GO?W3vL8Lu@AV_xd,nB~۳DPH_ mgW΋µgy7_7Ft@bȩs@$y^TRU,a \%f*ǩn|r(n=}&3o\ĵhZzd>ݮ-Xw~nnL{ȽZ^W SO k?>qGTy=)ޭ˫wz쾆ͤ=Li\wKTLlA{]=&7[4#u14,+@kVѾ6ikj=+z\%z2@uw Q5j?s鯡)"K@L'yU9YAII@s?Zeѐ>bjhϘh/e.~->u:[\].= 3eF|]Z,y{ +:`Gf/~g]z@h@Sm\goSzvނ3L!QfYL<,^޸gݢ8O!S'3<6DrLDr0G>d.PDX,<}AI7L(  /-׻Gbk,_;~ZCk"ê4v/.]+9cNSuSU|'6k؊{}|bNvkx;ߩ߿^Tdz Mj?\|Xwęn~Yua9dPz-ע7c<2e"[Li}oQ A\Qn_⅒Bz#Xu(Бxhr8uH9L> %yXrwP3BW桰:M)Og,_#ؕ'pL^6kn=/&m?}k xhrl׍HmzSTӰzeaS&n= 3̀dsk_ڂbNK4NYZ=zww%OO~۠Yv`F'?O rN[{k_~o.x9 Q,rocfAMu܀WtG.;rTm, b7&<$}ۍ ^C9q&X0h5.] @fi|?4>GVYSumCj tB|ţ>#d>n6O:fٜF1b+ٜ}p4K/-X>sF'ŵS#'9YiէO_#5;~cpsp;/kzuNhۗQخ[ 2V>|^X{^A;XS?4&KMD_Sla2caD R rïcG< LuЅ\7ؔ=純da9ZiN.9GO7VJ]J?[xvNj=}-ՂK>zv;)mL#Sl˵_A}+ rf}vezC.)Sz$11o uU/އ _pkZᙆZ~bNj[/S駍%bJ9D^vjҶőTbqHT5ik W=ŧ]F*&\Wؓ'OpP?wt<ʺ*qcϜVf5kVH2}`lPnVBDX۶=pwt<&HvÑ̀cgj#Տּ,*ީ^98Ԝ]ScWEUy}Ie gXO-ǔij%"TܥDY"8&%L:tH RӤXe_G,#$b=z]۔`~9 U;TQ;4YSVMeA7:-Bd̰z'ocyÄ23&7# ttV_aҷ}R*49$86OWYt%ͅpYD7@|_o1,b{g=/q# ,cY^۾,ct"vJd[޶yNƬW甎9|ul N^q?>&vAΣ<̂ \ :B_w5# ZE72Τ(~)d ,π&;pnJ eHFK\H )"J'A/!)s<߾{SN>9kwQoOX8' V႒ CyLwW?rS+c^?Ӟ燷|s<-}ҖvG=JJm״lwmCS19A}U]gX>] ^*NT}cDI.}dvO.n>ȝ]5RM]fEud5ˈ4`dK9SUxVzGRCqhp!QP ~vSB򃏳z»c j!ݡm?0P Kb鴪hȾp5N*jk}#_KHa4?}Q3zl?zzVݒeKmm(zYVG[_G-/m=hӎP{68&D)6o_+p LsX_^8zt `|hK?线__ͻ/.y|%lڡS @a)S]>qQ>zLlu"bEaYe[;(6>+@ tEҍ^? 1qI"oH7+TF z`ߗn-=wWx@QܗJԋd0}T{^8k} ȜkjW+m[ZZeZf턾 G7:q N}5uF`զ|+~Ddwַ߽]wݵd2j_8w=Eogww['2J#+ƠC@jgX^x$lW9ۏ"'m؄ IEŏ@BiZ v9#>_: q#|G b̾2!8g С` vBC) MV] t/HME* 3r7D/?۬UgUy@n:tc=5bKy6n_m}b[C)Ilo@98Y< 6R$區$W6hXby !lOJ`Xm-ԩQdΧ6է.\gSOFjDC0i)[JrWq nua8$l,xZ"('X{zL(/Aq0OT{mF4e5eQOݐ'lE2i*Tz.q>62x`ҁ=x]-?Õ"朣dT8bTXe{dZ[a܎1̍L O#"LJ7m209 ЦrBsL%ncO:{G&ste@24s\s+@a^⌒6D+rZd5o>*b-=V]ghv\葌Uj\s1'Y=覬bi:Uz4ZnH.\>{’ߛ(s+w//ښJ[0~ߖSVsQ4^c֫s+t*~XvM_g3XQ[oviu}?f1&&19 0t^<ڝ䧄-DS1{Sd6/O9gHGiU(ԥۇunDh| # OAtX =cE]b/W{FKVW#(3W#`nxYo-yzxQq+p<dznNEs[c};\T^~n^\dvvt9V:WrGtخgl6FwNN\W/2$k4|,7!掴,^į~ER_zX]tlH0g D4b 4@ZsBު̗(6 ;ł~I*x74AˈiBMMvA!X ~8ݥA-psNt o|*9oЊL&H"Mz 0%RQ^.1y\KKϪ/k}'eƞygW.oM)uZZ59W\maM_TEo&cn:mO>‘ ԋxFzLމ pͫ7=|\9p޲l;-4(:&k 1/*K} Pek45?hr+AW T-a@IDAT@V8D "-3@eu;eAoDGZJ9EHVm:}! ї]r6&B;/<[&b?Tc A7ޘu.ӣ_멶~Sim5YƼ:>}JhXul'63l0m`EsS?9vh?|gY=|1W/tι)2`b" o`G*3f$O;R u멣N>ؑ>H)zCljrBGD`~\!VHaS!|avn4hB _9^2ܣ}m.<jEc휒iWگؒ36H涿27jo6#M\ mٟ8caP?Ds 'U{׹gU` 985ۚYVlJ OTLN(rMDSјn|8KVKΈfI[GRBO)SOaاJo' vscf? uW".}BRD{:\_2|}3P(sck{,r< 뵧7>cO Ms_LX[Ns5f: u;U@& (yPX;v c|믗ɴn*59 |&땦PhUAL<' z/tyM<{p # 2z2c!>5G=&M+eU> ŗ^~_SKDҟU!r7k}E K!;M9N$v@ְ'Fo#.\Ś=t 2hL=}O-h{j?P5xur 9Vsg__-]&f)'~'1|K*m؊@kz7Kd(J ,-#7}_# N1*==?`??or EGg^;s}8 %@FH<4E]c"p*臘A=r*F@EG*iJ1J4㾨&.+ڎ\ TĶ|.=0T~KHBœGˤ3~q?gco {.-Z*:ja޶3#꛾mzw{cGD˦i?6qlc98M]GE//cO>B\PP"pBF%ZEUףpa$=NIeFoDpu| C/?eR@(+XT "0r|x: FT Bh!8pKl D(g+ԵmclN u7"  %v 2i Njq/aV?O j=Svs?NH,L[ss0=}4~̩Fˏ¶[m@ c=IINA_pT/}iC&B&>venj*ݴgg?xg!Ȁy݉39f|`}zLE'!*FaB(n,"'yW ?'`|Ija'Ow{\k1e&RO?mߦ! 9)5ht"tO(bg'zPُGN&.g?IGs)7]{Q݈m3~& VQ|<۠ l`Hd0Tʷ^$c4SKB\XPVqKiLN)#F=!8 SF2CoIol4㛠ge%cO }*?8Uij8LTB~m /Si 3Gfζ>Ŵudz`+0E\&G碫l?|6fҘlo~1 Ji=M{w&9̅6F6s<sL߿ ;%mh{Ө`=@DJ.+ "VNjbqq Wre#^I_z݀DC@ Zt!D..rZe_/fH] T/>݉vӟ`/E]|ĩ|eۗg={n܉iwÛz/G#*mc%-Q.9[ ӥEŷF!}*Z]Nݠi:&lsc<&eBu+9ͺLAboiW?wS陗ws-p"e0f5fFʹCuՓ ruz._L(ihD2#p>I&atF%V sHIGjsQ/_WA |) |vQ9,*\aUoM?)EW7t)t-  )ǐ< 6ʏ֏޸j#Flf=: CDh-S&*JãXT$sCEv]JF?5QK|tLlrQ?VsќF$LM<_܁ʷ;})}N?JP5%jh#G wi\PWU?xƸF}qف#xCS?s<0R)4\6d^6(!'M.'F);لObڟK:V:8lMqW_qVOA?gxXR{N3DYk.=O[߰:~v@;u03;wpQ郛8)VD>} _@A阐 /gLሯM<ƚج;y elKcbXZewag(TB.|j0J`K+K!^?e+; ?xhG\bJilQXǰc 1mVcײ뾣 tEەA`GGKop7~qSϵW s+{IС.'p=Н3+YCd`L2#RёMBhvqᔍz]־]@ ?L,ETmmM;Sz|ì'U8#$1rHexu>sM}N& ՗w:?oo{n7Ӊ3WܔZ'`e;ϸ7٠ǫyߘVf)tvc~2`߽֒gwwnΊs-!/~w~?Iߌe.fQ쮻ۣ5;.ݻgjJ\TMG>I{ŎLR@}:~ lkPi$~vɖPCݿ'dPld08@ClFU@9vD&PpLףsja_XE&fFF.$ @cKI9A)u^u>tv&\eE2oBߍJ<&=gu_~wC69͘^vݿe{ _S6Cm_~S|TOaoַ+uu<躽7d UqڟVYg/6 M6raވ:<__~Q7ї>L{eN@`;gkh?'7~V?7_? 8 FxCFHHY({? /FbM  ۠ؗ2jBnhąieUB \B|xTN\r~%\) VkGk/?w1'qfSaՇk<: cGS BF oVTumc8ɽI|7rڐ<5M]ȶ]}&U2YoF ?y=};Ͳ2GM̹\n7݃N#}ٿrwg l϶}_?y>x?R_B;ITL~I l7I%A/eltt'ԓb! a?H} lEϢ˹?[-r_>=<^.Qb+{^1%V0wv|}[FL (⦿M`qK8/%/ƿo':py?\}ڇ9)kiثck_c(yhr듳! ߺxҮ߲`<}lQf1lAŃޜ2 pM-MGWݑ]=/'&/-8uԠ=J|򃢏!1jTXdQ5 >N\=8:2'{D#j°kIrpHCQxaHpy+ DH>ݦ{Vܝ7;M$ˏL͑<#<|Z͍?ϺgY[t={y(-sXVXQثe_woLt yCtZvmڼ>WNyz4999 GME߿_e85[K {`oY+< W?~_ѢF{uI3@L5\ ;T!h>.m`ҙ"h:I^.H'Kȶro^}i5`U)lZpi)9풢'!tʳ;/g4O|⇸(7zayڎ~!+ϻUs#T9++ݫ0ivy衇~Z= ɔ@'Toz-y~ўSݗYaǓu>}!CJNO%y#+(v2hf+4z7}P@~ԉD^s |>p]KbE#84p kqbET_\'k?'Ӈe9`:'q t,9\ mpihiWZ׊q==&DK7ݴws~o`3vln]9mkO7//wL6uy{~an7ynj8gr%c3,"V%y>_ڿJu1B٧?hE 3xHyE/c=!}0Ɛ(!BN1aA#Y. z{:h ݣ\JB?'o E;.Hڤ.}h?\mz_ Ryus?7q ȷ7BGks#t^ :rcrT15?˱d=mw{op._om9?FAI y&0$H dI]_{SN_>sN?5ZUvUUMf{;t%I2"5LՌs /*&"O 37\ywt:lE[;y%kqM O Ÿa _6Q~34ũEC8HjZ4)yjݤojr(=@_a7T~],[:<_#U\`F/Ԅ&( CiG-mٷ&{koDUd_z3;>tVG=Fa!M$bQ F;/[~<.0xD4Q9qRX݀&HS+zo&8.~hu>6_zzWHO !`WVC '|LA5q6/1Ugrٴ~취+"4>v\s< He% dc 􎚵KOn~%zqŏd}fc4L!;YOaD{3 AGU 0njk׌Y9ZQ5x~7IRj0cՔ  7kȣ(XM]YcϬl怗|_yqoW zR\&+:O=/;ʚ?JsO<ln~D-{Uz:6+{f#@]RepXF !J~{xE*8&\ݲQm|Zkۘ Gu/7Px61z52tLΜ9#n#(/dah#0vjW ##mzwY7=bNy׾r03-A.Zf3j*5DI4W"vEZ< 3v]v 'ʜI;g9*._n<@#.oz F1L G}}HNew/]49YwSE o37s?jaN)܍uky_I30b(4h+(7xq DvA'B>Ty`h( =.4ðg hѢk_ru3FVFPGغ@|!VXa:]s,PݸEnN0WQHz$'bb}wM[ϛӇ,#𽲩SE ^}O LXPjE7B"FB _.W/ o'636@a/y#4'/);2vZ6[teҚU8H6S^xF UD.v yY]X7bT᷂`Р[*mI*Vy?d[gù[D7Z=vQ*I^Qr4e'SejG+`4]7LGvw P ojgYafDܺ1#( U@,0J+ڼCw0Wlg}}WW#=;}]eyQWxD,0G䝓 WWh_*N5Q?I\ mZ?\d,R+q?!Iyyq+{ -ϘGN' %@Y$B%?C3/. 7K,eQm/s~Vplɼ~V K6l<~wFcTRvAB'ęWOT׿uvW)]?A- ~vf$RnIg4vGa~1͉AΟ2JnnKwM.:X>nRE3g_Cv"(4`ʟz (l)N+8vz (Rb>$MZq\VUʻT[#w5>ہƑ{z!5-屦"Hm,Ydۉ'D~T2?fOSbկ~[*ߞgdn}6!L]Ÿ)x? |_.2PultTȎoF`!%:yf9f8Y$4a\fGx_;A*zYg]^y} (&lCHFh[[b /.u_t/1~BZҌ8nj5=l>],úe˗ew}g$( e'p7&vW LVh Z]lI0EK=Qi|g_JC//œUӹq 0'a?(#Gf1=%9)eK[wW/ɣpU[eԧde`LsavّOܸe[A?<ؘbs.O:l7#Y`{)C1§'Iⱔp¨5a~\pbzq\v/Ws/{JYl^~ èѯФж ޺ᤓ \X;@d?fuQ%3^i#<9ԥ 4Z>[3mB\M[ڕU([Κ.[߈^H,p--3-}51y;~[<2k׮딱iG}QaB! '}5P~!V׍Sn&K=i>D xi}c9ēyOZ6Iv$][(".7?)TH.P^pGƝyYf\GӧOeڴ {+0Se tvv߷i0[ccDOw*Uӻtuأ`+"0xcWgw>:[fh%cwN5//={W+}6YU:'_P@<3\![&*5MY؞um};D)*Alm?n\.csWƈ{c'~ qMekQ?4o W*[JQgFNʼKؓDu]rƏ~x&/6qZ+b[3TLFͫd7T̘j@3 mBYi\:|n,%=6C2#/cV`[f>=LN8xr|},&ql# >#Iv 3OeKfg@(( Ɣ7~!}8C@ffQ^؀7>wn_*ZCN+0U6rNƓN::{˦rjT-W:)OuOԉ"@k7Br„Oh;/rsy9sߙiW(#! 5}$ y^rQbT(}}\η? {?#/ɉB}KG8-;v {+:C3B߂w}Qƒ|~F!6_n7/FutPD:ƐPy ,`qtcN8xQ&͘2Qt\S}凐CջE o'9ng#cpI`D^\+{⣰//?#Odqn>82W #/fcm&/) \܉Fg8 ]Z7F]л4|B(ĺA ˫~ WèLTźI'6 k82\<Z!< 5!Te˖eϯ<i1g t Jq8G9U ^v;O2rSpg/~ 3Ja ; Ff90qĐ7C|1(+OXxmS-M`1{N'oj6K}\U2{7ԏfi`Z,$A@3_ aM:_;S ۶NܾߨIhnlQW^YM~tlCf-P?Wf w㑮 {L:fucgY 7Ϛf1!ȴlt?q҄20C*I@k{x d?ȥ/}1r.f;M,nr?B *f?y~0muf}EtOB\Xw+h.A5ƌ4ej6a܄ a2]6VMyxcÛ.U Wy<+_JXwmou?&E¿L*! Q6ӟf'r4t#Elf؈l eO+W˞oz ǘ2֏U!0lDe;MU8U48,L\=mǧcԎ)~g>~LeNF#+A]W jQa "<o~Iַv'J@B#,:>mCe_~1B> A ?@}@YN8SF+J Nhb6Xh>䆤7Y(+Hc prۦfo+I\X7b6 'h}Ť erȟЖf)= >QcYw u}u@ڝM5n7m6]Q AvpӠ(=Web1×Cx_]Jnc u.ŸsW!f`W|5}mð3DUun͔}otK~(L.JrTmtH^H(؇{9U#} B(fVlߐ+GS?޼~vajVĆC76>[w)D nƩ2FAkN1@VOÞgz5odz6.v?f$6o.ܲHe*?&5k^"6i ^iDEEi,(_IrܯT3UGx7ͭ6?-C*CpQb}ai6vT`QS}&˸ܪzfV]+k֤2_HZ `TS2~_MN1?J4bSO=(fucqIzoG*}4/ ^`?ϧkV;f{mo (@~S#{ m?wm(H7A ޘ/֏IR}Ʀ N;!n@cpj~!xv#3ژhA0 r5day䑌k5lv‰ߚVTe#}S/ܾo0VD3:颜=MI h1&B?-kpu[ngLϦe4m7O_^z竮`R^g/|ԬIvQFb\i˨Gpڱ=fD[$_Mv6;"2FV\W7p5aݗx"TpTm&LqQ|x%8]8V넉0XB(7@rlu7$p!8n=4 p0mZmکjM>n[+soylGwQ @GاdU'N@:b4NG?:9վT*_M7n~P!V};qt @駟69@_/ M{#Y,0,`x≦Q\pbЋS r< Z vv']qw7-YCG ^uf3fM]]\_dޔ٫mVgY0>m1W!#Cqɇ6Dm|?Y`z1v6|) _̙3c=6j?ybTˇ2@nC=*<#=I:>i&>qXZbGֶUTF17Z1mڋ '/*cHb]wg9.ܑr![WNWӀ.a*մBxC"f|wrS4aJ`Ͻ=<V@48!&Ax; #zݠ&EQ؜/ŏ\~_F~kfX^5 N:vuC/6 6͐.ihնnL ,n6m _aLhtwo;WtcTBm~WAYfƁj_ΐ@C@vW%3]ܶ qm;FYfAS?n>;*Tؼkļжuk(P,4@A Pv2`jtoJwi]d*Y^{ڇ UnppetC9R~jҞOT·ێƫr@Mr/ 䒴ՈT`=&G*ҡ=4Jc<~8!%~%4]/٭in">*$^ F1N؋p1e_I2`# ><>hC)fd6F7> _ IP$M mqU4N;G\[P"#OCW 5U L?ԮqE8lV_%lM7F+=q I'4E:2#[*ן)dF0z6\Іe78H7m-md8Kf:;nEvҙE@r-f2TI(/6nVXECz<2 5XS'@btJ/&(>|[?a̐0g٥i>~#+|\7qn;ҠUӿ \^TӾC@m/mشUo]v.\#UIxu&*Ƃ P38h|48a!m)kKW%*6Bf M;"-o|;Zo ~PB!ݓd=JX2R/#Š}2>o@V My(Q6{mSA?R*h\Mn 7V K3CM׍iluScǎ//~O~=nRΝ>K7#șFkgI ôL۟_v_3?,?$JFL_2LloP%* !4:mxؙ@әШqnGH!k7vQLp`P//fK>XgAp]`Z (73# <5+lhhVruqDcP!{hAm͢p?c"wNM>'яXO @4XF}qL?@?vFp %}+NwWT6;LYW>F~l$O ]L!35Qc Pxl 5| Y)uEn 7VЌ˃D=uZ0mmB++=m6=cV4u嚓bs)WCYZÀ)~Q!IeEAj#c^c#g` |+x:*vv7yІ 3BT*m' ȣ/$n6c% W^1ms1{o}EU>j\21=}tg`Yފ(/LB#v$ÒGmS?ijqiFkT=Mbok\tƺUk~^%OXcPeOr PR d2Ÿ)av,#  $|Ÿ)V?a qNa6cG1m7~nCyF(vgIX6c3_ߦ&ņ>O>9{}}. tx؈OR2,@+@}{ ؙ@ײc&XgsuNNl4ZIGK}4m+~YFKÔvOQe꿧͗Xx9]$Rjr#44)CFߒܗ`F8vc· t vS7#Ucɡ6Lf씍nmU&(5Q.4a|^wUfv* &7G科.s.z衇ϰS._Yf L3sX?~S]>Ruy>F?fXjձ<`~{6tӤź1Ti V٩iW7'v<>z_GmƗ?o]/Y\&~A5$ h1TO:A N]#fuMqa3w휛f>"!oȨ6DLa>6a!郋pc. p|ˮSiKndhv2B%×Bo?/͡*(*(g42o9ei TJٳZhۗN,AkR_ڶ۟Etƺ}teMqO'Rm+b$"4|˻q.·'"GPV涽*/mQh*׆]fB80b}v+_L=v?sҥ/ҴߋM| f>-K(.8X`x߷u ACf6Oێ(Oظ>Z>ڮ_.lOG>xIyTc5ˊf? &2tRsЂ.܇R]r}vs]z(#=Z×0enPE=EU<2~nhƩo-_O!2`@6/AUqÕƶrtdo〃?%}0o;ئsIv{;N'5pqmH?UQbF٬ }qFY1q8v%K޾=_>/~ +pQoAȫgfn/A6n.#s 8C:C- 8.~k}f4}ۆs_ץwbJ[/e=lǴ}FCCY,= YglY=lmCLw2O8mpf|U;vǍ_&~NQ8KNl;ϻy%1LPLoX>"lo~NOTp\b¨j ( e*$gjڰ0c픃Q;vm9nm/ 6 r-7FQwSCqY:ᣎ!D ,`EO"Lf+hV䫊F^=,oäKs:ږ%jnၟw!]V~&5d \{tN!FΔn9:~v &,V+. 42mhjw}䗙 :mMqk|-Sa@X/4g ~Ow Q⌠Pg !ޢ0`y;^R ]AP7,oqvю]_0x:6mjoݍ뎡M#gƍW](6*.z80H#A7V@qsعɌs 42>\;qtϯ'=~贱C >ʪ4Qd)Lxk%ݹU&yOlp^]mwؾ=?Mc`S*u$kCq\v16=>?‚©U]msyQƋuV?#8Q.۫9"Lc Q wJE8#ؑ?Ë=2Uuauf|Y< Kkwv3HQ;$t f7x1Zu?ipV PƏ >\ 0/=hekhc@٨ {ڻ\Gb=6 SyS8(3#Guwq3= @/@}pz09,c4)#c9vq<1?B-˄94p4j#lNz}v6!u4>Mcz.XV(&8!Ъ]aV&*V4h[@n~b Wmˮ[t]7x_ۦѤywK 6"1Vr90"_J9);6H&>#<<ܢhtx|>t?Ÿѱ l5|U&..ġqB;^hkkNTu s2Png(p qqk덛|C1n0<**놓A/ n94>OmgK}PZێ[m]rC'_2쨰2FTGVdR/?آ܀ǎgO>`1 "O; ŸWSrs""D+E%KրCYM&.O.HX(ٷ~%_8Mπ'Ԅ+ܪ$QS>he-*an4v J~8Gۭ}U) ֵ5ҵ2B_~8>2F1wT/N80,Kߒr^ t>?X*1cJCfv4S|u&^~_4Oj)!]`A~M`h?p^}up-Qث0Sܸ;KP V\}k?~nAD9v_xW~70^tA TaL*(A6*5Ga6i;6Δs=gC @f,2q/`7xۭ\a$,5qŸ侀EG&?-8{ OUS/mp݄~}upA0Giˍy @i~pi_Ѭ;iqo9}jޥW 4(N @LO\}pᏀVPO'ڹ矯 n x4m)i6^UVէa?匄^5mXXӏiHUA~7͞Hb@~o/E8Ue48i,tςU-~K嗬aj%2q{>\ŸF?W/Lcm/! |m]ڗ/_^g?\@q٫* $ހFZb9(wj3Pv[uMnn!_ڶM[ؓK>W6\{iWũr\[W,y9J y/gֿ''#>z:k\d 6í\PJmKO6PN3h\JU;3 @sGc@O7|Er?b"7[H1F/n"վDOҧ&nl]h FhG@n6 }_"P$s6]Cpbhlc~Eo !E;/]zj5 e#*c^{UǮp' ^C8QfGk$= h(Eoo*bZ{dE[;L˫fzhGEsXZ_uź }-h4f1^q&N?_з۹4lۜmy# {Kr"E,y4́!sÚiE`=̙3N8__b "̡W8dB LY]o#6ts! tj t_"/}C=Hs\%AEQZߓV *7‰ o͡JCXnžiSŊ@7^^_rz&VBgGGB(yO۽1]X`cFC6(RFX1jYv^բd y睷7Q*{@Nx? ο^*1CxdBfvF^d' -H#gdaRJˀ }N$Zw-z}V?M*VlO_'_MCmimV}4>ȍNQ84N{wl8oyµˁ!ȽD,l;u4Fڑ̚5?x>;Gϯ'ڰ0]ֆBme&qW.\h6a8ʚK!gq!uWU© o 7V ' 7sA({s\ 5G`Sh_Qƿ (|lkf_d?vҋ* oCJ;wRyoТ"Tr`\iC(v¹Ai {B}6f1Ÿ+yOJ9W?,}LFF?<_5  7,Ak^r+R4foe#h hB|UnO a%W\._^&(@5OoTJ-Iە Cb@6Cxu|{o=k瀐Ʈ&v*man۫m\klNA:e&cʔ)ۨqoݺ=G9b:ow֭ftZ+xCxш}lZFG]^4 \~[DC6mqӀ֭[䭒cE!nK\ĕפO_:#i; Gypn"q4wX0tasR*1Cx1Sf6#mcw?=c0esn3E/o覜5tځ&u 0ׯV.4S@&(ݴ9l|>1EWٺmKqFKaΖv>4o%wp?Z*篅]ӊXƆ6Q|pƍ av#WkOVO2'aw?ߌQ"i"7 >{@IGhUEg_{ hvI"5۴(*[0(Mž֠+/DEoܼ!Ch:l4fL@*"OB]'NEs?WN֣+ xԮ+c~. O_vҙ)buY rCXĿBlAz|ՠB JqiGQ_vZ__ݾ|4Cp8[hk dF74A\6i[1Lݺ5L%3ߐ_ כxey+ K)/k qR@[q-9wpgM(rm~ |~*q?j~!v;U_feͮ}4ea?1F6"IioedA7+"'ï@M=,pVThTgũrP M[x¼^o?~> y{IKP{VoAysHP&qof|;Ͽ+=9v ?*,|cXF8Aɋ~CcW;Sx -}v_B}MC ho)v K%9w#OtGkyӡSJQU8UЯqm+Y7u yu<*ů/퉋MѯG]n߰$*SO*7xd/ 91>|_8qJ |;w,FugagpPdD/Diܩ #xѳ  1];8G m{r@0EPk#V^3&.녇?lQ$# PVBp_^T0i\~E8E4g4]7u]׭q!q\[iڦc5Ts!:5iX.˃ߘQuXsa`޳!(-&4~\ 0]K~f c>mc(nȗp0:>.z_-kx S?ҊS62Gx6C>@Ē%K:L5hePEPVvg &Qz4\nW.V<5p 纕p/[.\m s`iM;NO}4m3j%n6}[WAf.^.txHhƷv^290 ?Z47!>EXڍ?}]/Q(J>1BЈLhL6-_w2MQ<B_?x6yGNg"( +4zۇSFW<[RS|L7u۸jwqbܡeijTa 8YBM϶i[a(ڵz - D-}]hA~*"/2\L_tEV?+xl itfJOvx/ ov2Ÿr[j>Ÿ{9 -7KHDkT- *8.n;p_Z@q(*زqmaSiĹp'N#^^u VNߎt%^[;f鏘 eGM_KyoK&g$M}%(F3L;YgTfB!=͌]䝅v~sŊ^ y%$P#|Mmv­U-RQ֐:ZSnqn}>?;m\8UnS6k4U֩'϶5m}JF# ;=5Y .з~úl+͐x8oHڶq5i_!X;枔׼& =x:aXOTuOnݫqo.8 8U.|l 979cGݜ+57!_>?P{zvw(!v]KORuy !|K8%G9=H.J# F y{̒e3`/B'ch=0Y(իW|F ,DZYbG+.drI0/,CIaÂegIK;Yw_:~I m; VĉN8!;c#y$eEg "{lXԑ ~jw8 yEl+Ο1/Bhu6-yf i̧|$cBqǵb%USQp@J_Ȟ#ͺygxW:lDp?_mOy_M#B?#].-?E󇇂\W_}55H$āK d +OZT5؎sx' qu)χ?aq{@ {キY?\Pol#tbe>}G\tUF:҄ w!:DtwǎKCC)=f &m󪋮Zapc K"MRҜ見` rD ulvpȪCŦ?h( 7P\)*w_vL\I>XtOğ$gIDBgLqCdāa3/Q>{Lz]U8d`odDޝ6mZv衇ѿ `t={׌w@8W&5Y ..Baʟ=#{I}@c _Nݑu"ڳ8*IcƎ)BB.Fym /:s&{Mܽf>ls^YuQtWdUgi.P_f2?&{"a{7ߜ{ϟ/#Y88ύen%?I+{wcYR~JC DaW/ w5*ϴxkE?.b6ܱ*4ޙ3g#{eB_ES5k1+_ 3{7#!Z0\fe?1 7ʰ{G6oWzX0@x{\8``%pD }aP_f2G"f!#?iC$7oG#"C)6 '͐>fyՎ_ ?Mr` wxAi/ˀQ2?cw7/ H+ԎGk-90 o֭EAGi'M'^B_O~;VQWāāāāā O8W޳l:WuMpgl`D1aGh BDFHx)rAA(Հm4d3S#i#}z@@@@@4Lx&_.]jFu0q='?Coذ]\|ysloc"%āāāāāVr`2-G3EV_nQ 2::ɭM6e ce\ ։2)2^BOHHHHh)Tnܿ+]wݕ_>cO@0`)[2B( fQp18UyE)!'$$$$I&!}3${\ld&ࠃ2Fe=kU'X>`ڟU~6ᏼn~1 rṿ/o"J2bz/\0c]vB3@/Se `r2cƌ ~8 ?ބI+'$$$$:@8`XpK;wq&~H>F(|ō=!4Oo~c$x; ^Pb~_O5q q q q q`90 %eξ_(_LjXog#4Wg_x}; /;E 43 BsZ@@@@@+90( YIR}yh1(`%e^%Evd&kb/ډ>xLB;l23iFekяc~w?|MG|_r;q q q q q80h \uDawK>WsC>Q~33`\ģC9qg;K\sMđUDGd888880hNtnN9IwNUڇzhvgg @AF3Gs7O⎄-"D"%āāāāāA@[(pO?@)J7erHv 'st)}|#}xᇣn2ϕ5_&$$$$ <̃Eh*ַr[̙c6 >~60ꫯf+W4q^|)F888880hh+.̝;w?7Y4V8"e2p7DD88888Vh;L\+V6,==SR^b9Ж @\*W=ا^K^LNƁvV D 8NQ͹nYIw^8=mwٯC#KA!9\k>ֈjg9,AabPυs~Rf ;GbZkTP! IMFbRUف_,"Z^K(*+)&j]W DN 9F<A&=R3Jֺ͉+Gv GՃ) Gqz}vg_33$"+ G秂aabcJGuu(G߰.G{LHOROJKHM+Gß|԰[\>/XVtTZ+GݶbǼ+GtzJ.(<|ػC +Gٹ8nȿO?ƿg+Gn.\X#HϚ,eѷgNFABE5+q8:6#Jo[:Zqs!/rAn2+2+6 7޿#znPTn_\M"DٻnYIodfvݲI #KA!9\k>ԇmh<$',AabPτt}S{ ;GbZlVO$ IMFbRWe0&Z^K,.,.,.-(!q]W DN @FCE.@^MJ؉<Gπ G߃) GJ373+ G뵗ops[+G̪9G迗:GͮbX^a_[X_:GдÞݽlgP0Ddct`h:G|:GͲƯZ.53.,:GߪԺʁ:Gޡ߂꽊:Gߝ櫹ʔ\UT:G߯שêڕ:IǪݼԤ'7Gҽά¼4IΣԵϟewn4Hګޞ{u~KLL2Jهꯨ0/Ѝk\c fi%(.(S2诽çDž1j6iɽȳ]#KSB[R۝[:6"Lb&Qۚr;~6]gy,uvo;6 _ogo"izK=- ef3+>@?A=;5+x3kp 3 NXis327{rqrrxM}q~ɹ7$@˩Ϯm㮪٢}p~ษyso٤`L~动Q.脛I.RވRz\8`dĭAƛ~lF0Q~z{etĹP@l?M |}?mM* @37{rqrrxM}q~ɹ;$@˪Юn㮬˿٢sysmඳsqȺح|~m䜿޶gRۡڏr]aôAŜkTq}z{~ƻW@l?P |}?pU2 @37{rqrrxM}q~ʹ7$@˨άm䯪ʾ٣zvtnىtrɴֿ՜~򿨥⽩RfŮAţՕ}|}~ŹPBnAP }BiM. @3it32`EI99<==;<=<><:<><I<>d<;<<>Ν\gllP<;<><><><=<><=;;:<>Ξy<;<=@ <>dnxxn<< <>Ҁ<; <>ӿ<; <>ӿ<; <>ӿ<; <>Ӆ<; <>Ӆܫه<; <>Ӆܫه<; <>Ӆܫه<; <>Ӆܫه<; <>Ӆૺއ<; <>ӖDDx <>ӕυ{ <>Ӕ~{x <>Ӕ{{z <>Ӆ{{z <>Ӆ܋{{z <>Ӆ܋{{z <>Ӆ܋{{z <>Ӆ܋{{z <>Ӆ{{z <>Ӕ{{z <>Ӕ{{z <>Ӕ{{z <>Ӆ{{z <>Ӆ܋{w{z <>Ӆ܋{w{z <>Ӆ܋{wxxw{z <>Ӆ܋{ʹ|w{z <>Ӆ {Ờ{z <>Ӕ{ѥͦ{z <>Ӕ {ߠإ{z <>Ӕ{ 갋̐{z <>Ӆ 襊{z <>Ӆ܋ ݄օ ѐߊ{z <>Ӆ܉ ݀ڸhK502FcځꚊԇ{z <>Ӆ܈߀Ɓ@0;yх{z <>Ӆ܇r2003Ni{lS60eт{z <>Ӆנڋ500Kv~Q102|܀{z <>ӏU00KǻR10I{z <>ӎ֊ܵ@07tҺ}<08𚊛{z <>Ӎܰ70FҸO02䏊{z <>Ӎ5ܲ70Tɤ^02NJ{z <>Ӆ%?0Tͫ d05{z <>Ӆ܂'N0Eȥ V0@҃{z <>Ӆ܂7}0;uttrA0gwvu{z <>Ӆ܂9ܷ21fzussk50厊~vvu{z <>Ӆ܁)W0Vu|¿s X0Ewu{z <>Ӆ)ܦ09us =0݋zu{z <>ӉY0q{{e0D𠉊{z <>Ӊ̊ܺ08$̲B0{z <>Ӊ{0dͪ p0dދ{z <>ӈ.B0̢ 33ꛊ{z <>ӅÊܺ0? N0ﮊ{z <>Ӆܱܐ0U$ͬh0zŠف{z <>Ӆ-ܱj0hutse0RՊyuutt{z <>Ӆ,ܱO0yts q18|ut{z <>Ӆܱ91ÿ߀vtstts 90㖊~t{z <>Ӆ/֊0:Žtstts C0㞊t{z <>Ӈ+0@ǻ{yyxw G0㢊~Ą{z <>Ӈ0EƸՀ*ջU0㦊{z <>Ӈ튷0DŶރøS0⨊{z <>Ӈ0?ôނʻN0᥊{z <>Ӆъ16݀ހ;F0ߡ{z <>Ӆܱ@0ځ܀݀˾70ޜ{z <>Ӆ'ܱY0tܸtutssts n0@ݒ~tsځ{z <>&ܱx0`۶xtutstts _0`ي{ttsڀ{z <>"ܱܡ0Mڳtsttssts O0̊wts {z <>'ٸ15wڱ~ttuttuutsts rs=0㻊s {z <>쀩W0~v|ˈtutstsrssg0@ਊs {z <>;ܑ0Sv|¿c0yܔ {z <>;|71v|70͊Ё{z <>=xv0\v|m0_᱊{z <>;~41wu{60ܖ{z <>܁9u|0Cv{Y0fŊ҂{z <>܁vA0U|f07ܠ{z <>܂qܭ11h~tsttstsi50̊|sttuuvvw xxy{z <>܂sw|07~tstststtstm;0fܠtsttuuvvwwxxyyz{z <<֐snd0;}tststm>0LÊ|ssttuuvwwxxyyzz{|=<;GHzَejoqײ ý!@><<=<><:<><I<>d<;<<>Ν\gllP<;<><><><=<><=;;:<>Ξy<;<=@ <>dnxxn<< <>Ҁ<; <>ӿ<; <>ӿ<; <>ӿ<; <>Ӆ<; <>Ӆܫه<; <>Ӆܫه<; <>Ӆܫه<; <>Ӆܫه<; <>Ӆૺއ<; <>ӖDDx <>ӕЇ}~ <>Ӕ} <>Ӕ}}} <>Ӆ}}} <>Ӆ܋}}} <>Ӆ܋}}} <>Ӆ܋}}} <>Ӆ܋}}} <>Ӆ}}} <>Ӕ}}} <>Ӕ}}} <>Ӕ}}} <>Ӆ}}} <>Ӆ܋}w}} <>Ӆ܋}w}} <>Ӆ܋}wxxw}} <>Ӆ܋}ʹ|w}} <>Ӆ }Ờ}} <>Ӕ}ѥͦ}} <>Ӕ }ߠإ}} <>Ӕ} 갋̐}} <>Ӆ 襊}} <>Ӆ܋ ݄ׅ ѐDž}} <>Ӆ܉ ݀âvfcdrہꚊ}wv}} <>Ӆ܈߀͜ockƀ}wv}} <>Ӆ܇͑eccf{~}tfcǀ~wv}} <>Ӆנڣgccxsdcd|wv߂}} <>ӏ}ccxǻwdcu}} <>ӎ֊ochоmch𚊛}} <>Ӎ2ܽhcuλ|ce䏊}} <>Ӎܿhc ceNJ}} <>Ӆmcŀ cg}} <>Ӆ܂'xcrؾ cn҃}} <>Ӆ܂7ܙckǢocwvu}} <>Ӆ܂9ddÔfc厊~vvu}} <>Ӆ܁~c crwu}} <>ӅܶcjǼ mc݋zu}} <>Ӊ܀cʹcq𠉊}} <>Ӊ̊cjɀrc}} <>Ӊܘcú cދ}} <>ӈӅÊco׀Խ|cﮊ}} <>Ӆܱܦc#ɿcŠ}} <>Ӆܱ܌c%c{Պ}} <>Ӆܱyc ch}} <>Ӆܱicmc㖊Ł}} <>Ӆ֊ckwc㞊}} <>Ӈcp|c㢊}} <>Ӈct¿c㦊}} <>Ӈ튷cs!~c⨊}} <>Ӈ co{c᥊}} <>Ӆ ъdh "ucߡ}} <>Ӆܱochcޜ}} <>Ӆ@ܱ܀ccoݒ~tsځ}} <>%ܱܕc忸cي{ttsڀ}} <>#ܱܲcy佶xc̊wts }} <>"ٸdh㺮lc㻊s }} <>쀩~c؀ٖcoਊs }} <>ܧc³؀cܔ }} <>;|hd»hc͊Ё}} <>=xܔcc᱊}} <>~fc׀÷gcܖ}} <>܁uܙcq׀ÃcŊ҂}} <>܁voc|ˀ Т chܠ}} <>܂qܻddϏfc̊|sttuuvvw xxy}} <>܂swܙchΏjcܠtsttuuvvwwxxyyz}} <<֐snڇck̏lcwÊ|ssttuuvwwxxyyzz}~=<;<jn~ckˏ"jctؔssttuuvvwwxxyyz { ::<<:<eb{Հcfʸ0hcvܩ:<Veؐccz̀0Ƚcc:<ݍNfܪhcdʀ3Ƚgce̎㊊;<ԸrHc͇ccd}Ɂ1ecc}Ԕ홊9<;<Hgܿdc qsdc)|ԗɓ<;<HlʜyctĀҖ <<Hm˗ހ ѝɒ<<rHtl݉#ܺ"7GHzَejoqײ ý!@><837:8=7:8:7:8<I7:a87897:͛XchhL8;7:897:877:89;;:7:͜w83 7:alvvl89 7:Ҁ86 7:ӿ86 7:ӿ86 7:ӿ86 7:Ӆ86 7:Ӆܫه86 7:Ӆܫه86 7:Ӆܫه86 7:Ӆܫه86 7:Ӆૺއ86 7:Ӗ@Bx 7:ӕ΂xy 7:Ӕ{x 7:Ӕxxw 7:Ӆxxw 7:Ӆ܋xxw 7:Ӆ܋xxw 7:Ӆ܋xxw 7:Ӆ܋xxw 7:Ӆxxw 7:Ӕxxw 7:Ӕxxw 7:Ӕxxw 7:Ӆxxw 7:Ӆ܋xvxw 7:Ӆ܋xvxw 7:Ӆ܋xvwwvxw 7:Ӆ܋xʹ{vxw 7:Ӆ xຜxw 7:ӔxХͦxw 7:Ӕ xߠإxw 7:Ӕx 갋̐xw 7:Ӆ ~ 襊xw 7:Ӆ܋ ݄څ ѐƆxw 7:Ӆ܉ ݀ςꚊ|vuu߂xw 7:Ӆ܈߀վҀ}vu߂xw 7:Ӆ܇չҀ}vu߀xw 7:Ӆנڸ{vuxw 7:ӏ֯̀ Ǿxw 7:ӎ֊ϩ܀ʩ𚊛xw 7:ӍΦֳ䏊xw 7:ӍΦٻNJցxw 7:ӅԨ xw 7:Ӆ܂ڭـ рԂxw 7:Ӆ܂(ܼ׽ vuxw 7:Ӆ܂Ф Գ厊}uxw 7:Ӆ܁ ܰԁ vuxw 7:ӅʣųЂ ݋zuxw 7:Ӊܱжς𠈊xw 7:Ӊ ̊ѣ xw 7:Ӊܼ ؀ ģދтxw 7:ӈ۩դꛊxw 7:ӅÊѣٲﮊxw 7:ӅܱãՂ Šفxw 7:Ӆܱܶ#˩Պxtxw 7:Ӆܱܮ#η{txw 7:ӅܱܦȀ㖊~txw 7:Ӆ֊أ#˱㞊txw 7:Ӈ֣#ĥ㢊~ăxw 7:Ӈӣٵ㦊xw 7:Ӈ튷ԣހٴ⨊xw 7:Ӈ֣ڱ᥊xw 7:Ӆъڣ܀ ٮߡxw 7:ܱ҅ܩ ߁݁צޜxw 7:ܱܱ҅'ݒ~ttsځxw 7:ܱܻĽ&ي{sڀxw 7:ܱȣĽ#ƾ̊ws xw 7:ٸףƭĸ㻊s xw 7:<쀩ܰЬ椑ਊs xw 7:ãЫށڻܔ xw 7:|إЫ߀ئ͊Ѐxw 7:xܺϬ᱊xw 7:~Ԥ߀եܖxw 7:܁uܼހ߀ܵŊ҂xw 7:܁vة߁Ჩܠxw 7:܂q̣᠕̊|sttuuvvw xxyxw 7:܂swܼހᡖܠtsttuuvvwwxxyyzxw 78֎pn۴܀ࢗÊ|ssttuuvwwxxyyzzxy98jnٰހ#ؔssttuuvvwwxxyyz {|| :79988eb{ٱĀހɂǀ-ȼܩ{78Ve ڸ݀0׶|78݊"Nfĥ߀.ؾ̎⃄~88ԸpHcմ؁݀2ɶԔ딄|9898Hgβ ¾)ԗƎ<897HlԾрҖ {<8Hm˗ހ Հɒ<8rHtl݉#ܺ"78oʿ^Hvimv Ѡ ÿ8:FHzَejoqײ ý!@988=GH}baqڈrwƀ:ԴѼ!EGH}nY^}|v{Ҋҁ 0FHJmQUl嵄{Ǻ̀ %FHKgHM[xɡ,ǸJHKbHHJwx9ѹ!HGN\HHIwx,IHQYHwx)GHTVHwx-𧄓IHXQHHIwx)GH[OHHIwx-隄GH^MHHG wx.瓄HH`IHHJwx~ ⇄GH`ɌIHHIxx} ̈́HNѢHIzxyJHѯHHGFxzGHĥHKyxzHIڢ}H@HJ߃ wHHI@HHμsHHG3GHށบnHHGU@HbjHHIHHcHHHWHHGDHHOfHI3Hl8mkLMLLLLLLLLLLLLLLNCdd k| hh%h- hȻ/hZhXhXhXhXhXhXhXhXhXfXdXbX`WaUQ`RwFS;!3_CWdJ- O(Xj#Qname icons8mkF~] AܔR?0A+ѵ=+t8mk@ jy.V{j얀kk&kHkk-k.kCkqkQkckckckckckckckckckckmk_k kVk^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^k^fc?s5SUUxS}SN}$d"IMMb'h:DDS*!(70rXL 9fXe ?_XrKRXmWFX\];X4k6Xc-X/#U),eia(SUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUZfZ uAnve5glogg-1.1.4/images/olddata_icon@2x.png0000644000175000017500000000024413076461374016564 0ustar nickonickoPNG  IHDR KbKGD pHYs  tIME(4m^1IDATHc`P6g >C%MF#m4UT1*Fa #IENDB`glogg-1.1.4/images/minus.png0000644000175000017500000000065513076461374014733 0ustar nickonickoPNG  IHDRw=sBIT|d pHYsvv}ՂtEXtSoftwarewww.inkscape.org<*IDATH1KAɅHb킢T_&"haJ'γػKx0f 8!ǁ>YUH*L/O_rGj9T } S@۱sLp?ׯpXi=~$TO>aCjAT 2΁t13ϱ~XfL+yr^gygFkĄ\\onztu ޵ZcAD>i]Oq`@6^}U0"z&q\ NVxg1@J^9 0 R ŕ6]y-#sLApYc\vȘi`IENDB`glogg-1.1.4/images/hicolor/0000755000175000017500000000000013076461374014523 5ustar nickonickoglogg-1.1.4/images/hicolor/16x16/0000755000175000017500000000000013076461374015310 5ustar nickonickoglogg-1.1.4/images/hicolor/16x16/glogg.png0000644000175000017500000000206713076461374017122 0ustar nickonickoPNG  IHDRagAMA|Q cHRMz%u0`:oIDATxb477ssvv/@ Yi666+Wܲer 4_Ν9@ SAׯ_trKH29X3lc M߽{kǏ >g`bbb/s}}}'?|to"b9Ts~=~VV #ϟ?JK|3P9ϩ3w@5fx #f77wgN3|ۗ `U14fPUP`  W&/^1lzXo0ᗦjf&F =7  b"  1L)iv-~3_f`})v6> ?2ɰ#C[#ŻZb`av82 &X \,X{ ,?XM0n`j _0 @ _1|-0Y} ? ޝb;b`2w`e~(=~5ߏ םbPd`bf@L>y̙W ΋W?dg|6Ù;o< )ϐ*+1I1@|@ BPP~ΜsΝ;`aep;Ý71x 0peetׯ_ϙqO>}dl'wᄒ5V_?u@]|Y0,_d, [*)νwct9" /Y Cݻ8hflrD0ď:LӈIHYYCPQx5+0ğ={!ϰknMz" L(߿0`jp1L,$@!!!Б<0u4`Ęa~ze3g΂e^^>L66Hy…b߽ghkkg}SH&/0<@-=T#5]c X."ݻwbW}p@b``cHsgsf4i `l]*(dx`dSIM̐Ǡ t<@Y+1|_Hgؾf/Ͽ ^cPƠ:EAPPAA]e7];`@L( Px"*/gXp>wy ߀!0o".00C/6 LBL&?Ý2e2kpicccsgAݏ;ϴ4۷A4RȊ`M  Zzܤ(ch_ÐĠ& ,>M>p &c7#Ġ,'Űg>X < O>!Rd2{ wL`.'P9r`EV !!d;20Dfnc0 6bWca`g`a@͗l̰u J6Ж-u JXawd`&@v%8Yx qP xOOwpz?~Pٗ \l ?7^&4þNOQ~p`nn10&y8&]3 f3عO>}15@(@˷o89ܹ$t-YYY}}]###;w23AQA!{ #C"8D9~ĉw *@/@Xӧ=@ZA=҅>>>LoTUUVQQ)@ ށ|p" 6l`8Cww0Py8y5C `*O21ON`yEE>77ֻw(.F'C`I!snL^^^uuU@N#@INP3g&ֱTPRGA5˗>|xv~Ăɸ<@X=JBZ۝Y@jj* bb`ǃt/֞Y9810t-pl e;Ͻ0yUN7\@vPd,3 s++zAi]KKQ;7իyM׸ 8@x33e榌4rhn==yZt|+L 9}ȔǏ޼7-DN [~FP e*qq1_|,:~ OD@ `p?9 2t IPׯ _!-G/0N<|  x1 @Ʌ EDiyժU4/,,P] ?)@bf]]!/x daaa QQq߾}w> ECD41@rR8)8aHD)IENDB`glogg-1.1.4/images/hicolor/scalable/0000755000175000017500000000000013076461374016271 5ustar nickonickoglogg-1.1.4/images/hicolor/scalable/glogg.svg0000644000175000017500000020265213076461374020120 0ustar nickonicko image/svg+xml Jakub Steiner input keyboard keys peripheral HID http://jimmac.musichall.cz/ image/svg+xml glogg-1.1.4/images/hicolor/24x24/0000755000175000017500000000000013076461374015306 5ustar nickonickoglogg-1.1.4/images/hicolor/24x24/glogg.png0000644000175000017500000000371113076461374017115 0ustar nickonickoPNG  IHDRw=gAMA|Q cHRMz%u0`:oTIDATxbTRR7/&&FBm˖>|xݻp9m{ff1x Qo~XYYN:z۶UK z _|c`bbb`ccJ+Sӷo噘r&)Z76 @|A]]ӧo ǎ]l;;12xx؊nݺ[(oP ?v8`2#Yɓ  7oֱLebfF/k@wff칋  66 e`aaYr|TFF&_AcdxCd\f9PY@aXRȿ@X8};838,#H=pPd`cge`gf @ƍ{fgx0L4˰bf2 bB l@qښݭA?p$ JJr@_p3| 1˪10cGƿ$Yd~6T?2S<° /feghZ b?0̠*(.Ԥ! CG6<} qP*f(P<CRC0W?0C܃ s132&OqaAs#-/?tE6.> )Z  2l  < J 0([24X0g5#xh@> LR bx\P_ ~1{^n;2ܽ&AA?V p΅@|قW};%eq? o~3|bp_b5$pu~np  "[ << /cx/Ç_n_p#03q2||G h+` ** ,2 +ddāg=172 .e8v>맯xW Ɔ |d`bkA`ӧ/> L` ~A Q. >ag+/7Çw, ) ?a~6??q`7p %Km+ )ð{ '`߿å˯N^~ͰCu+/0H2DKދ ߿}X^11|_ `}֒% |tƌiq7ǢpDVO9fc#)+Le\ j">,UEo, >X=} ! ?fq))܊r /lhmi bf`eWl޼}ٳ ݾxr\^NNV!! wYYE`*f8zto=ZIIŋ }aebaw1ÅKghkGq>(r::QR2 {,yy9Xs޽!|{(6&0} ՛X/]>fbf${ 33VMM*C}}/v| (X5778 Lb12 : X3ެWGO X}qǾ}V {:F-PML Bx, ދ[ԴvIENDB`glogg-1.1.4/images/hicolor/32x32/0000755000175000017500000000000013076461374015304 5ustar nickonickoglogg-1.1.4/images/hicolor/32x32/glogg.png0000644000175000017500000000565013076461374017117 0ustar nickonickoPNG  IHDR szzgAMA|Q cHRMz%u0`:o 3IDATxb1006fgFF 3ӧϿ޹s4;;o>Eٹ44Oׯ^1{w,@,{'ï_5 89X>V NN ll ?b8xíۏ881C Wl{wJ: u5U2ӗO>[^`f>}ʕ{x`t:й_jkrxq'b#lǎ_aMLӆ9 p<77/+Õk7GX,jAn@a #n|pC~~^o3@ MM} |4l`C? ȱ_``g()g3$`~Pƿ Ofx%a\@u :85~f2&6l8C pF ""@a ?feRPf+de_>~/=A0}(,™pS`k$F)ɫ2p ߿~3:A!%Fۗ; ܮ^a#f` bbpp0ƛ0eb!(Cm?3{`rg;1lN?0ݎAaۊ a6 !?10"gXj?ѣ?ϐZz!.ߎ9I@ݳ zc[w3FdX1ᯧ Cѳ lw}=-b ;bB::  J* }gԍ%Jr>c; _ '&ap0bxvÕgM$@Xse@H 1Ы 51  5Ё`Mx$1dfYA1Ha eɠŠ- G\_011c@% X΁9v`(- X~K`:,sO6}?ΰ&ʧ>p=fxy?C LXYX@*CDbAr3(Pf`ay50*D땏߀ #0x9^|p/Gn߼ 1AKeorBV0@0bxi P"./!pU %48y>p};2Gfv>>.1\3 `ud ,-EBx"5DuΝ 7O45dY%;0<_d#On-Qݫ0|ӽ@_ !>988C7Ew \}pwg?3p13`ܾoeX83?Ll } RRb:] 1_[@,O<}'||\H0]voPSpp?sO^<0g<??203| 3C[{,ÆϘðh|p}/UUm'3p۝L,0˗/<8 x;Gp{==)[/3h`Wd&+ ì9 O]֖Wq,<mLL9ɰsGA _?ٳ뾬̣GձYXr#Ơ&g&`+&`&g]gp1.03xx80p s`;QX7;qaѲ ]QS]tb>HTRR# *3 .ή  << ^X1v͇9\E_خ /-pKW2<~~3?7 匌, @a͚o,->u^z7#Jf/v. oq1p{F_fx Ӈy9wܸp>, (g99<<\@_[__.-_ b` L&F-aַ0<|ϟ Ba@eo4EP@C>|xLϾxq/ޫ_*]Tn, o~ R>vÍ[Wӥ_>ձpoeu~@ Y=Q| N`x8qׇ/7xY?sXnKpeDr?~"".nذc'0*2`rff6))NÇ+\{5oV<{|o0v7](L̬,\l\NvV׭߁S9@z͚œZM...΃wNZis(Y9aT.(2a`bw: 9 2IENDB`glogg-1.1.4/images/stop14.png0000644000175000017500000000054113076461374014724 0ustar nickonickoPNG  IHDRH-bKGD pHYs eB|tIME88 IDAT(N0 P|NˁhbBh$>lh!Ү&洪)Q";1Ţ͆5wRfARF(y k{uR{8yUȲv.aKahWu!}q@^NѿkHź/#JB%lνvT>Ԕ\qG7*; Wev?Xx_ @*܈>ʑIENDB`glogg-1.1.4/images/down.png0000644000175000017500000000112213076461374014535 0ustar nickonickoPNG  IHDRĴl;gAMAOX2tEXtSoftwareAdobe ImageReadyqe<IDAT8O`[i;7̖hB8͋(HH nB#:cƏ9T@<)r/}jKg<}oYt&i)HF%(EF lxO-oxlP3zwQ4M=)칝^4xڽ!ǏC='/hwv9LKVQ=xGXr9:vQ*^vmTb/>ebսu־ϵ _`D*r`:x#P)v2Pqdd܉;#4Mlq3K9lil+RXxؤ*JS8%m1躏<=|ZQ7oJ #tV BIENDB`glogg-1.1.4/images/glogg.iconset/0000755000175000017500000000000013076461374015626 5ustar nickonickoglogg-1.1.4/images/glogg.iconset/.DS_Store0000644000175000017500000001400413076461374017310 0ustar nickonickoBud1%  @ @ @ @ E%DSDB` @ @ @glogg-1.1.4/images/glogg.iconset/icon_16x16.png0000644000175000017500000000206713076461374020136 0ustar nickonickoPNG  IHDRagAMA|Q cHRMz%u0`:oIDATxb477ssvv/@ Yi666+Wܲer 4_Ν9@ SAׯ_trKH29X3lc M߽{kǏ >g`bbb/s}}}'?|to"b9Ts~=~VV #ϟ?JK|3P9ϩ3w@5fx #f77wgN3|ۗ `U14fPUP`  W&/^1lzXo0ᗦjf&F =7  b"  1L)iv-~3_f`})v6> ?2ɰ#C[#ŻZb`av82 &X \,X{ ,?XM0n`j _0 @ _1|-0Y} ? ޝb;b`2w`e~(=~5ߏ םbPd`bf@L>y̙W ΋W?dg|6Ù;o< )ϐ*+1I1@|@ BPP~ΜsΝ;`aep;Ý71x 0peetׯ_ϙqO>Eٹ44Oׯ^1{w,@,{'ï_5 89X>V NN ll ?b8xíۏ881C Wl{wJ: u5U2ӗO>[^`f>}ʕ{x`t:й_jkrxq'b#lǎ_aMLӆ9 p<77/+Õk7GX,jAn@a #n|pC~~^o3@ MM} |4l`C? ȱ_``g()g3$`~Pƿ Ofx%a\@u :85~f2&6l8C pF ""@a ?feRPf+de_>~/=A0}(,™pS`k$F)ɫ2p ߿~3:A!%Fۗ; ܮ^a#f` bbpp0ƛ0eb!(Cm?3{`rg;1lN?0ݎAaۊ a6 !?10"gXj?ѣ?ϐZz!.ߎ9I@ݳ zc[w3FdX1ᯧ Cѳ lw}=-b ;bB::  J* }gԍ%Jr>c; _ '&ap0bxvÕgM$@Xse@H 1Ы 51  5Ё`Mx$1dfYA1Ha eɠŠ- G\_011c@% X΁9v`(- X~K`:,sO6}?ΰ&ʧ>p=fxy?C LXYX@*CDbAr3(Pf`ay50*D땏߀ #0x9^|p/Gn߼ 1AKeorBV0@0bxi P"./!pU %48y>p};2Gfv>>.1\3 `ud ,-EBx"5DuΝ 7O45dY%;0<_d#On-Qݫ0|ӽ@_ !>988C7Ew \}pwg?3p13`ܾoeX83?Ll } RRb:] 1_[@,O<}'||\H0]voPSpp?sO^<0g<??203| 3C[{,ÆϘðh|p}/UUm'3p۝L,0˗/<8 x;Gp{==)[/3h`Wd&+ ì9 O]֖Wq,<mLL9ɰsGA _?ٳ뾬̣GձYXr#Ơ&g&`+&`&g]gp1.03xx80p s`;QX7;qaѲ ]QS]tb>HTRR# *3 .ή  << ^X1v͇9\E_خ /-pKW2<~~3?7 匌, @a͚o,->u^z7#Jf/v. oq1p{F_fx Ӈy9wܸp>, (g99<<\@_[__.-_ b` L&F-aַ0<|ϟ Ba@eo4EP@C>|xLϾxq/ޫ_*]Tn, o~ R>vÍ[Wӥ_>ձpoeu~@ Y=Q| N`x8qׇ/7xY?sXnKpeDr?~"".nذc'0*2`rff6))NÇ+\{5oV<{|o0v7](L̬,\l\NvV׭߁S9@z͚œZM...΃wNZis(Y9aT.(2a`bw: 9 2IENDB`glogg-1.1.4/images/glogg.iconset/icon_256x256.png0000644000175000017500000007110213076461374020306 0ustar nickonickoPNG  IHDR\rfsBIT|d pHYsIIEtEXtSoftwarewww.inkscape.org<tEXtAuthorJakub Steiner/"tEXtSourcehttp://jimmac.musichall.cz/i IDATxi}'̺>} HBCD!fvgy~3oߌ~[[fzVH;l -I 8n}]!3"#2B# ]3>|Çd |>ZXʽj0_@?J1BP=&A)529sfpc~8~OrwǺ@%;}sC‰':ѕ茏u@B?E>VTvA1O601 8rY̤y5Vg=Jis䥗^W>< `pp?;fKݴF_7R-`Y`h_>o`u ``!^{'.e>B>64Kݙ%PJg}ǏٶڽX2 mfl۶j}QYm۶AUҊ0mO=WE+%ctOӳ.RincP6 cӓO>B`0y^rl M!’1Ð%z0T0bRG @QH"EY2H|,;| 8_T*PpX4-F ` cǎKHH$~eª1J)._ >a +r~cVWLǏyO/ej pպkjwfaU||/_Z[X |l` 3>60||XWSSS¢!Vs\+ EQ|fcM>60||>cg>|l` 𱁱Rئ>V#GV >|)+P+0 333> r\(ِldp/~yjQh +,!FF~)4{{J5J*J`Oίv|np _%޻ gV3>XJ%>4M[X;SB/U]K3%/!I]ч͚V` 0<2,+C!tc#C5\tQVC =3#wvvܽ{G,^\~ϰ8:c`]1UI W>|,|'JepXPv7fVX,v7| H3cg>|l` 3>60,}V%pdtSӓ+~_>lڴiUV>|<*| 3>60||XP ` KB:,Jt_a+~OD >}zU2ؽݗpF'q]3`: O,R+X|X$:ǂ|XǼgB1]J 4M2b8TUPE;a:tݜMT*>,3E6 0::4'h4Ex|t]GA*T4<_ 3DR~Eoo/<陹/X" UU`t]G\F\Z KB:cW徣c[Եlw7h @]]"0BU*T*J%E 9U~UUF۶mCCCq-  -1 .Zr z{fԄ477x`@KoUU‰_id2111QU ܾ}oF{{8-[؈ `EA)ŝ;w`f[WUhkkCgg'Rb"Ř(뚛uV躎R|>!appӞm`xxdOi\~C6u]Nq[tz)lݺuY\بR_xw044:DZ}vlٲT BK@ddf>^!ײ穩i::ZZZpS^/ ]2!ԩS}\CC݋-[uuu Ru]i4 JSJ}@`p^(H&8tvڅI\t n݂iRݱ1ꏰyfRRRaaͩFՅV>|/^č7\þ>Gk׮GRcz>>XR(h8bANJFgڵ MMM㳶:r[CREÈH$̐`0455pܹ#1Rӧ½{pɚ|_'?Y@EdU]vahiiA2|>t:=BBH& B6mڄ޽gΜĄTK8q8o߾R"sc]1p2ēO>n477#~պ#N#NL]T&&&PWWW9Dk.… | ro&&''SOjK 3Ga5 Gt:%Q5@l _aX8pp;txT!HR8qzzz ccrT 1u^H!L`ilL;yi"Q.188(hGEgg'<PJ133(Ÿʖ7V@X/`xBI{XXES26Ŧ:SQ̗s<]M(؈vd<(Q|&F2eQ^g92g@XÇ%W*ѣGֆ&Ou7clllުџӸxoՠbb9m7օpx[mk->(Hd2hllDCCԄ'O"Jޓ 'Јϥhh^X4>X "%yf<hooG"p]c&&&Z_oM-ߞS2޼<7/BQv'm8;`@)$,Z[[]uuux'P__z L+LWit,c~'~g !8x vڅvD"5\cccsg N]DaY? j_Wz4gmjP.100 B!ڵ X ON(/' : R3yP(Ç9r[lAgggtDՅ5ݲ|? |ATDA* PRBVM" @e@IfJ& :&K:YiP1Wؾ&։Mhot37<%ؼy3^xʄL&-gEG>O7 rU*~Cd Kd]^n-pi!#Hތތ9 Jq>Oju#b悔]2 !hoo'> ??Wr"_Y&K怦i7k!W?z3Ճmmf'lW-@{0967Vt6XL$@)M!t|8d NcI7WbttLx!?WT0o bjS/ZúbK J)^y)شi:::LWz+w0VEo6qu buQ"#Xf Bi:*ZC+kf e д얨Ot8">נ;(@o.wNngtxLLLRF^Ƙ'KvR lj$>ԂvB\Ɲ;w;e"·|c@t4lvO2( Acp ԩS8L($嬸U%tNz|⩭HCЙak[Q`cPLF@Y"3B80"JXF\Dcbޜo޻ҏKF$&;×wߍ=nMfrrJ"0oߎl6gJ9sL,%EԮKS!0 և/@2AUo( x#n4b{{ДP (8x~ٽeH:?~yG;P/8vލH嚦᭷^ВՔկ} @ ~|AայȈTL^5q_> nw9݂'7! )| ߃Ta"'BA;:]pf'- К%|>|xL)띥R)۷`qժ}}4ׯ 4~m~с%^ s^EߘL~SA*'VڐJ9DϙPN r'@shX{P= 8 >;6KdK|'0 e2'|ҵܹ_xfQfpRx׿3 *23ÈD"\6`㵨K7p\f -q I< (fn$=?!gcį '`SKPo'e@UxlWT^tnQ.1>>.B!twwRx뭷\@K3ºb,+B?W^>X,VJJva+wp1[Susqˉh9q5!.4,bκXˈ $UzD'߂;ۥf+߻Ld20Fٳ]| 8nݺŏk_ gX,\=k H)qWW t}>.kMן;HH~-oK!*oqj,'P5 ڣr]aMP@$bwW2ȗD|kOe5|{7!G"xȈ3mvPkA=k֕LNNrǔ(8pkʏ%t̵qûRY,r<=*.I{n(B=I@uvLf;)Ur  #2=#e䞫.m* D"<._l-gm= kt]*r߉6uRqeL'P >ѥ$8`/ QTy;ǔFEA 3+LԬ)SY]Xub=J) +Юkw֫>=t[:9LCo}8p|_T7!JT ;w%{Er٪UX']CC=ݻ aC3q҇B!޽Px\r7,,WU}=1ّ@4;3acSWQ.]h9j}Gɸ(u}Okɘ,}ϕ0 WO2D{{K D1\~[lP033m`0DQ8USڗ;`ݷ9XX~M0:x=!ҧHqg: .lJuuT!Ŝ!pN.{oC7 p'Q*xAW]H H376GwbC2Qoݺ@KR|RYs}[d4Tl;Sso=lo;;v$Vm=c ?Cd:$&"q.7po F!er~b瀈zlڴɵ2;c]13nO.!l۶ uX}߹2{9~㻚] (mAKCdf=[3dJm nq-Ad$!VsOD*4P4 C9U |~W=g ?#BQBW->:J>X4  /0g{WWQtJeS WΞؽ}}(k&SiM[gH<3*P؜('rFLg"T(ř`!Ә3t{{Wwz8~xW0 d27Rd2[ƍ0,NK%ku̅r,-`&k~akx|gc uWZ㼭)@A`6YTm@,Omx̌lHHĵF  y: tmh BhmmɐDd^!mm:t4%Ykb!<_L=(PAg5jg, р61,aImSpC Fط%+O89YړP4EioX,.S۶mu076y& B3eW_XWB{;]*WbC)7Y 5!d7al.jbѳۄoPGPwծa7 N8 Zo,i:N]=-K\.' X IDAT`hkkC0%OUOP_(JTJ5!ioD^NZ )a{- mC'!2*c؄-Ab.49' 8K^b' \NM0:::z(ل1IIboNp8f@ 6 ߔ;.TK\L9: 9 s\spNV"|h} _ J$F0߽aaxe._,CwwT^G\M֣\0 ={IjWW!Rɥ_?#E%a^~B65Hv4D58#3ۙ8s.p뜽{OB$*.+{ĥZGPÎMr΀woNpR(߆yb-QWR\_[ `jjJJASwfoa;{5О63GfV95Rx4'yۅ&a3/ȌB\ʕ tCklL-ɧe\(T*T*Q@/Jm RKJ:cp}D"p8lN*`aHpoFZ__AKc\A#R' ^H*pta*x;l.+؉HZO;{"(%b݌7 -QoW]ed02^ah?EulT/b1ӀRJIjp)ZpxSJ' ^9o ΩT##RY2ii|EAܷc_BmZ[! pvmnuD, `F(L\`2#aEܱLιP*x<444 Hzdd{q b4J10[!=L̥u.7NC]RŠK}PmI/FzbH/E#3՟&u8xITQ\{L 0:),ٚ gpj5GjKEQ$ =hK%B;Eŗ))@ W(m I= [ I:"0m3&$2Jld>:eZaٹB 0:b9+)TlZ@hl` 6(p{ ǶX5Ma=fiol6tc"أh Fkzv_Ȩ1(`ish <%clp M3(x'0@0̖F́ g/9CeA9`j3·kQ"3ADD[(߁ `װzfo50.Zޟ`2f1js4]ߎ/Yo~vSE3Q^vEǡòRB \0YɓyǢ1Š($k U9\n`rghal[af#9k%fYKęJ)TU5Xq \[?*t,F0ϚA5QJ04%G{ cZ/mya ᥂n˽\%Y |֜!} 걠XX/!{'KD|uJ;^ 2`tn?]ui2Ipa"dj3( B!ĒeEn\ؾQ3GX1^_/]' 7vl͈(/%"8u+:Ҍc 9opu}͂YQ` %`/ `bn`s}e B.4X_&`+o:bv?7"s_H&}[v/J= P|@a tiyXZpQ(WgS,+ip2fPA(Ѱ[YRi-ja"tP,Zc0@]kP~ ~RJAmM1`$"gR)W y:/Μ PfTY̎T&~5Y @ 6E8{bNyq; xASy[S 8!^/&\Nxm'k@~!ׁBKg&%sGk3" UXZ; r8rEշ rK%a1)bV9y8KI@+8WvtPMx,Z-MQՅ1Ab.Q,0` AWD~mho5p9ψl۞3"72ej?@.@ПGQ/Dd]0(4/R5Ų44cA)*#Sh=G f8Ô*Sй5A0,qqpdJ-GPn7i q}ڞ1 vpߎg%UGM @ `hj cGuaX, }fc zG ?-e u/7.8RZ:S"@TKֳu`RMcvzfb*?ZTƝi:5~b =#!if~5UKXT \Ol85@M0uT-Y`Xe^^m(-v (RQ:lC,m*#>?Ph<ɤgRjobdقMqg* as̒o,s>,?i՘:4 ]PeuZ[5MSP(Ul`PkDd Cٵ-hޠ39[PAkLĊHf@սGk%g2?/cb "5|1$q6>bI=<8ݲ {bq~)k":Ma:!,w쟃Z)9EXͻ}sp1Raw!XuB%AjJ }_o@)__ 7#\ /sA9׺`Rsu% "w!D1 iPIpaux5$ΎGwY%8~-mb|j3"<4GDcX6%ˎg ?Ρ-N[;p;WRQƬQm&"ًeBs)#H0 I*1ZT6SÏ,;=<,xR LF ʔZEÃALL m/\0،SI(93$IBM>lKm M0 w\\D1=7n^CT aE?\q `Hl&o3%B=o0"w`_Zq.Ap%ZX3J)~5M^ǀ6N;AOPBݠN3,:.) !< I}ر4޸!l+\kE4((vt2'Dce J ' G2k-3[sj7afv/9I{/LEGvT`d"(߀@*(UUcE(W(ج*v]LJx)f%;g0UhHd?K !ppMMMXdz`w.Z5Aa;۷3BQ#B!ڀO=._7|aavOFf= Mo{NBzN}^\E%_ʞ9w1fOMMI6NӦa!\1UMrTDOO(g<`zE'phk20p֐۳05"$T$W*&7mR0_ືy*gU'S+O;.1) D~]ci1*b}5/Y,^VHfݻwciI?!!=6 4O z 0 g;Ue[(|e6H CZ&,iw 8IB~(;\ Ѩ[h}TWQ |.$`φHRIq"߿-g?` pih1Z5wOC ysUSYhN(AgDy,E9W 1l6 ?([ĉ\0{v)Eo:8SR>%+bl=#U1xYrիN QJQWWCIo޼;wR|>DŽFK$Ҁ Se[b(i]('s0c(5]iWزAEP@^aXM+ vsfH)Ĵ`6C,\Lt c07~+]] Wu,R̪“u,چ"65(,QBMzaL1 T t`bbMMMd2H$ppXjW>ډ7.`Z)޽2,v'DVٳyI[L KBJkjCj-õ!?.%bZjH@0S"~F`xfMbSJ=͛6o{]HX3w'WsIb^J{g+W5 r`(7ozr8Љ-ň;n"e[g6:oN=X۝B0"SGp֌?/RX& x@Iq^Gw\KRgh\Mgg sb)2\swsE`@A0O1`0fܹfG"/ك-gQa9w1g-mhkY9]k)/x $w68C"vi fA-A9a6KȪ(>۷o~;vH26jt^c/|QͥKpIJ%ES`T?{k|Knj^c):ˠEq̶&N B! yɧ@!܇nk n渳>^ @Hpv@ pWً<َO$J)KBFGG022qd|:h |9XTN@bfIy>oI"@cc#&''CSSSTPuuu0,B IDATdǶ{>^ES¥[Tds1qSC z{{1p<+dgߘ;bݟqb_o ߓخ(~lBIetǸ{.@ СC tvYq,c"^ANenT*\xuf%.Jr"K2 |[BdpUw\upLeqKXv%x-/2u?Tj.f$oa4W(0 ;6^:\R}JP jծv<)ٟڢZ\L&șyII{QU᠂+1 M.<(z>؛مSLRĞJ6! D9[~3?d 5" [#=~c)Kh4=N vK( 9b {a6?J2MhvX,|>?Y(&*-M)-PJ԰ؾ};g̙3`cxD¥JW>)J1SKD Cb6lVqc++',@P` F,ԑ`:&sM3 Q/CvUUESSD,rO̸X.?YK@`a(J7+ Mn {\v,ˎe2J2 oU@2Dkk+gss3ٵPwNnd#eg,ML&;oLJſ|yЦ=p0FmyBF@ I 9._|J~/x|LZ4t!dzE(%EQ hu]'ak8 gÁf ^ضms&pYlٲ`###LiPE!7+śC>wr7S+=Vc&&UYGbchTjwH:$F*!& _؍/ y[kxk]?,x=-YN~{1(KXxefQ#C2 WeB!R)oR'k,|޵;j~. "0dN( =ֳGGGU⊨z]z cjjJ\/^֭[ɧE& M${!nw 5|N@P]vD7@WB`+hG) k8#d/4psMT_zf3Nonbll߸qCrLWBVg1YfP8C㮊+9}gϞ [ mTu\_x :_:::G\+vw%Ѱ'rwEdˎ}, 5‡lI+^`/ 6Czv4Mh%,(c7G/;ߌ_=*kciܸqNd2m?6"?BA`%ün^(_D!WULMMyn ̙3xgP.1::*eQxwݕ~]Cho~xnlHJ{m [RF{nR8 Y*?S,UYA%J{3b8{3yk/kmmue]d2ܔ2 ϟw1@ -[ݘS-*5drV/p'WbyJRT\F=-R֦T@zfkիW(|y0ى={`ӦMۯ:0t]G6E(@Lmqoş|:H 5;؅J[枭N~nn!JDl f"} gK}~drNEwg?Ym>*b]6ز>j[ 0Wk_`8}tsǎX,$Z%| T*.O;vlJ0d ى!IY:?7ڡxCg@Gs䠣B-,՟܁oG δc~ad#z:1@VRK:׋o!vΔ\ss+coR0gt!O=v) z%xf(Bq0/3g(߽{7}YףP(̚LDQb1ύ)0:::\1=MFW>ě{=c[/af !>QP"D) C7G}'w?lO)0gBΝs]]]سg-];j-MM,u:NB, œ14MbRrh.M0004!E[pgua說 ___m{ @;wXB[nڵk;\E %|c]xb{]<Z 8߇Պx$(&5s6bPA{ c}`00ޅ[q1 <ň_\K O?4~9 z)۲ p0aosTbBbRfwEPMP4 # H)]R;oCQ<ضmT*&''1000 188+WĉhmmL8Ȉk:0cccmg ^~o}!m!W(_}ؽ9'wa[W#=o֪Cb.1 AiUM2e|psbuS_>Q{{kzԋ]Alݺ?b>ʵ oCܞ]fKpu0 0h2o}k͛S^uݻWd||L U , 7 K,$ !<ҧRwѣGy:1P(AtvvJ^Q!RkOwt/ߙrE:ŵvuvv7`[w#vv7 62;83L `gzI~8I0[-1|FX mmmiU/|OOx≚;MJK/o(ok< Sǎ==Nm;wnΝjCjz:TǛ7(˿K?ePDCCHB$cjj=}EKDF]D0::r0v7G{_؃ۃY|t>u-q(.!Mq6E[C ͍Q!C  *rcƦ al,c>> 7˱ 1%xE{2oh… T 'N d% z1mQjTS(q㦛W=XJJOt Jug`6躎~x7xY$A$w"N$^|7oÇ%i P.188&!D"lڴ vygg0ߞQ-PJ18ueeU P,(j:ߨs/=юhqYZ,%o?ܿ/^ty({9^kI/TSj`|| n\ÇW̹s1!ʂlK~vg~\g*0+ ̙3,c׮]hkkQ|"ߺu \*;뺎>+! <4 (hiiq &qOmc=ΖLϞ܉BB7_DC*kۤħN(T*UU[K޽{<뒈P({NڍQݟ4*]ΪT4%P DYY(p(H$p8b4*jR%JEdsYLOMc|rd +/y{cǎ%$( =B)=Ɏ;;$w((J?c?%Il߾]]]hiip֭[J}JtwwKaCCCd2h)6d055ŷ&P¯=݅_9ډ;Y|pwO}itu H@$HҐ)ZlCIʲc˙k$Ess3ɉx<8u2% JcRIQ(N$Vb[uUx >]][WFTaDIMwa݅R]| _jE(aۘhy^^|> 5 8Ba}6SUqn1<DsB\k(_D].l:wrQsssv } F7,?V&bUy(9Sa͖$ra 1|m= PU[*y^;v ==ͨkbڵSȧ&ww7pBҫ$A$LMMi n|߿ީ)u]:0C^fX9xpc1ܸز2f a@XL)\NN>ZS`GAN"P'|w`)fTeOKdfmV :11a;zQŅs2#$I( ~쿚=CE@fAeW^޽{c0\58CAA  z1??TYʡ|M$N'󑓓^5&''u9i\pAWDA--- ٤͠Y;5UdAawiw)Y>L(\D X%Q19L)eCwD_5==+**7*uEQDWW$IJq뫍D PJ]Z !>'O}݇;vhssspݚIHp8p8Xf a `2^3o 0==禔=n{I*ᗮqcW;t8c*F@eD"HIzՇB |եVŪѕ$ B$ QM!( X TR?`D"xE]֓A׫kjjrبhͱ 4gC[2LMM)ҥK={!Ģ(b||pݺrY6U3dL-7#'ZJŢL~t:p8zzAfM,^/.]Ň=Ƹ,Gw_"7'v{jRo:A#]emO?= 2_&6&,^{_Ezl6lݺU̳P(nH:쁖(D"Uכ'pIɜ [ny%!h&LDfJ)""ɮ/,& ~?^!)FWWfߟ,:h UJju(VX^ܢ(.Dznx?M颖 +b yBH.Ձ3* Ujw8rs=Zغu+v;Yʤ&-_<+vzH$/>fJkA099) ??_WMhB(d(b~~sssM1|駆zh.aݨm=ݓ)6"43Ht?h eĊ]<D ۫x4yfBͦ<| -lFF`ݺu8AիW!Iƃl6<)AUUxM J:4XS055vLNm555صkbЖj #  #?ɐ? ~nV, xܹOzh_^QgٿհFp{VUC~χk׮b$SuFayy9t¡@ΝӅysկpu%tCCCASSR A&9r4,lqvvybp8k.,7z7jJ>acw}KE+b~j}{~?6cE{~b?82b>ToٲE)e䟛C?VkR}VkdEErrr4gmN~tه{9|GxuZ\~7oDee%nݪĨ&`Dz_}/Y9=2/ ` >}}}omm-vie8*3^ͅ@2$ p@וKVVMR_|*O8!p8ظq&oX0>>!]occ!횮`0vPJqaqb ~^f կbbbϟGwwa>44p"bڵ@& Ifgg1;;ILLL$ V˒ J15ɆFBH_IFXU凈D6mڄ444np>˄TWW+"(c ׸ʉ}?0JݻqE>t ꕽ^2k1Sjv(kƄap8 cӦMQѺm,)u%, jZ_x=CHcrco߾SJy6l@^^6l@K&߬-[,TWW+Պs!("wA}MQ][QQ#(Bn5XeԔNjlPPP:iOF I#!$_pE= ހƊX|>O(ϰuN555pJ& &&&nodV+t|hooxwNLߑu%;,o؈ƅ:100b0S0MIDAT(..Fee%*++Q\}TY%1Hj#HǑ DCŁx _crrrPQQTWWX,g87n% }]l6TWW+F9tttyۚpya ?qƓɢ;wΝ; 0<_EvTUU_$̠ V%YƻXFjjkk((cnnN D"AP mp8 vvJ$r#}>@G9)&D6O?avR+d<u(..FMM ʔkowkGsáԨitwwbhUQ[[ 3$ߥ6{vE<₷#X]2S$};\Yzmn oB%1UnxP(t غ磾^CM^~n##rE5'&& BΝ;gtaBS'62ДqrOOP$j RΥ&c OmbjϏYisoԝ'Of4Ffd t%I2 xcXg-`޽5ǽ n [РtYJ%E%%%(..ք,ڵkPJ;m6۞^{mx)mBton5=[HY{?1Ka~p477?B \ 6lBmá!0T$KT/IJKKvu>===G__ƌ.#_}%ڪn%GxZ1;#`@4EQD 7kU#ig 7{R@744b pHs? 0uo~g\Ȩhnn~Ry9CCCV+^emtN a J/m4Buadl? _V&z,˰X,@^Fuj#@)EUUN36 otZ/ܹǏxX,݄S/3#C*!FRzoo޶7'?UH52O I}}џUZ),>+W͛I苗gp8B!f̙3a|,NLx͐ȓLl1ڶaBꞵj(d>tǒ ( DO?x@7ljjR$ s*x,GǙ3g E)Q[[ܻۡ$.a]vѷC~e=;l-ѱoTARP9:DB! mXoЌe]o$"`tbļx#W#<B>Bo߾}x'ϯF8ִ]J{EK~%?{eS{˅*M1,G1??WK)?}r%bYdڐrc׭[n&MJ; Y{_e͛3$?q&Y|֐waoPXXnRtyyy EQD^^***2###ĭ[ataYԩS|!Ydq!e@)0ܨ-]V0<<>C҇O>ݕEds.K`m|H/V$Et*>k022;R)x ,)G6-@*333퉇j*Œ^q%Y 'JƍL+U8_^G>A|7:bkkD_Ew8Қftstny^F)ޓ'OΦsYdq#-@)Ufͼx2m5jH\N<):ӧU]Yd&ҍNђ{O0p\}$IBww7̙3 g`Z[[3$Ew&2^$1>>p:p8:8͆\EA$tvvjⷿa91ܹ2)Ew*KI_H$)lݺUѭV v8eRttt`ffO6+(zߝ={l"Do..ƍ gNP|.__=t7,#m۶m{WcAƍECCC(ĉx䭏>p?,]"m˺ ^Iƍ<۞v\O:Al$IFs.P( DݦAu @A- hAHAD B  ZAт  AA $ Yw%ͤP(`zz Fw :ݓSCquQYjZ}T '׾5.%mT }t7]$ O>,,~x`b-mJ$oK/=!-ŋu0Ala~`||}6 ֌ §~x3]A4x睷ϓX'^|^cǎu4'HVR))H Q_\.}kM $iW\F&it7ئpwypA)@PۦKAb@)gLMMZ& p8Q -JP@oo/D8_EړCӡDƄޗ_~9t5u 0M)M/~W jnsN9D Sɜs avvp4šCEϞ=?l7B/Dє NF{{lBdr=]#-kt7jsٽ{^w  ɓ~R V{O bӔX,sD& "^zFgs|OR)CAl2  AA $ !@A- hAHAD B  ZAт  )+n&b㘙A*bґNgCg?6RK$I($8/N\[Nˉ-G 9nܸK>F6mtw VVVB>pCviA`P(d Ǐxo_[o bh!B~ ~x?AT Fw rs짘otWVnQdꫯҊcĖkW111n t& 6ᄖBW^it7p.y饗bARd(Y "?N-5 nܿmmm 6T*GƐ  >L̙M s.ɴWL1'>Us|!ZAZbA`0jnkk8G'JLDi)ɤM ܦ [Hkl{c/JzOncu`>okSFc3$A"/N׾F!@4-s\r .}AFw |~tj`\ of_z}khhJfgg_3|;{wVp|sXGP@h:&&Fw !/ɫJ3@h*8T]!b{`}ri4$soTB'}~܍H#!@4 أFw @wo?lt' iFw V]h$$ ASN4 DS055BnZ0*@*D4d@ ~shoB$IӷHZ$nRDSd㧎 ۄH$x[Z֣!)A4ktET*Rol@RhRΝ;Yjn~] h)h  ZAт  AA $ YĶ#NQ]m–$ l! hAHAD B  ZAт  AA $ !@A- hAHAD B  ZZ IikkkzFw  ФC4Q7ڂP3V)R X,BbbH€F~$&''133,,,X,j pB) vr|>_EVA@*&&&1==Fwi0rr"B\|Ns@&G p}=|) $, Z '$H$4 J4 $abbcccxp T]j8 s\.\.|>O7$G6 ŏ. ܹ۷o7n0 DQT42rC?:;;'>us{  IRsn߾ hkkC[[ ~?<^/^/<|>_MQId4,$$WP$&&&Fg3`0a$6DK2??k׮{uMpc!hPH1~ U k~@TKcyEr9$I cyy( uoX^^qC5ւ2EիK~D{{;0A(+޽Vz5JP(AffQ#o@ILLL`bbHO~fG:F:*fgg155u%fY\t ׮]q$M2 bۑNqܸqc͆?bǎS~?4 r"sL&~ٳT SSSX( z*nܸ㩧h2ω͡y ?@ @lrΝ;ϯ)appÈD"JӢ cLtuu!͢H&Jb׮]͛C8za bc OȐ dM2ߏb1%  (q@6EWWvލE|SNannn71::@ A z7׋ntuu8p?~{aee63 Μ97nu!0%Y]];#wt^8(vQ,{B'/+W+-[y@}kl ,۱k.LOOݻe/#o@[ +H[ 9>#/JDgر1|>DQeF//+ Y n ׋X,C"ܹǏ|xo駟Ƒ#OҰ ed27QgFo> (e򷷷o\tuo$$)Une,_aED"F޽{q횅@Xq]8q=uv~)$1Ǐ{w8>T z{{2;::6yd2d2Rحr!(A Oq hH~:jjkii ?q~ iyD-п$l k.oggg] ?td rrֆ@ Piy1D"%"b~~W\v8~&&&ODwwh^OY!@4-^^Qr(ž}rrсP(THT*z. LȑneΌ`)k%Attt`bb׮]i2駟SO477$SNi:Ven8 pξ}}}Ҳ%F/M&蛑畕n7Bкk#nD"֭[}Ip<~[z@2[DSáC%g=F\(-ޯ'|X\\G8F[[ۚEA__B^/4,033~p+YS?*kS& @4w܉ÇrAEtvv;ܟfǑH$2-o"/$22r`0H$Gʕ+xr9OqA-1$@/2 $IX]Yk(L[[{9ttt(\׃?H`yylvm"bXZZB(B4]"I `X .\e]%|ٶ >2P,&&&ۇڇSO)^,[JrDKKK5"cee+++hoo_ y1E?~<իWLNN[ 'Y*^wvnBhdu^GE?RظkaU,--y"OQ(J8Ε1<{yDxB~\6#H HY0ى@ ˅.?\?|XŞminWݹɓ(Q4o}H aiiQ< |>*^X*B8p1n),g+m4+;%.X ϧAV8Φp6l<#Ca`wo Raee]]]5/x " >C^/VoA,h.n 6I0>>nHH$_|Qcсh4ZR6trn/EܜX$mW&/e\_[]xvi]?dP(0==6tuuTANx<8y$._::w߅#Sf4Mam)HNP2qxg,^|5??_SyZg9)t9xCxvO{$4:::D'"xO#ʕ+j6̺\v8/D@~SX[ĆR(cĻQ>|XyNy"\KsESWUH^7 wp>^:Ѕw~S$I<bXMNn0pEGEхoGJMX Uڜo,$ #a||2i1G*\~?z{{k$ sssuOYK3HdZ$p"SM폦1sOum L&qtvvT?+"ٳ& ^/G:(d7Ćm /@(BwwwMT (a~2޼0-o{o&wۧz3`YR)tww;2100A+̙3X]]=/)n8X#_f3MamyHuX,ǃ_~h4 ,--մx-Lt @+"a.>U-gE"9#'鼄t#]H%ijV҆t87K^r@)7 ͢q"N>hiJp#I9dk>c#A @ԕbh;pq%[/E*$<΁sx01p!6f]+"Yt"Eij5t>.n|A"B5E‰'pi,,,؞D]$߈&ml$MGoT7$!I?~lN '5Mf?{;nb! v1eB"ڽ"ڽHO<[J1(`>niC?j*뻺u/;7~>o/ ``vG+|f)xEK"x5{+<)(9~xn :<_~:vȯ IDAT % r99 0==cǎ󘘘=oIp_Y׵Xިn e@ _~Y15zsfpu;noþ.lGmcGvGuÏDws>1H<a ;#$k"`jj ?<Ν;H̻\x3¯l0؈n Ф0jʆo$333S~?N8,[˔/9fgg=ʃe|^Z;`PDGxK7 `P`Џ>Iituu9(G&''/̙3xL77$5s-:ut'P{{c/{}Ye7b)`8apEWtxp1s=^Yҵg̯ w?s 5F!I2} 8v}GH8`Ϣy ?|ƿ?&fggo&䉢cǎ)p8Gm 8vrM|Dat G$XZ `AT/XP˭MԊpf*TB|n<;jočX\\DXDW}4A^?`bb/}]bAmADE6i^O| {H5N}t*c =2?8zX SSS|7`b!״P 03mHp>JF˿iޫ\ƑILdJ~+)7`Gȅ p%Y(y߾/>ۇ3k)9Gww. K/SNYV1 㿋/.Լh>w: js7xr=Ç@)QBc>]{jݢ=h?ЋPX1zUmS~iD_6?:TESXYIay)Ī2kaӃ6[x+5|?:?_k( 9bs^z m Gv|KP'?@hjWSj+l-H5{Y>::Q%ϫQ!ZQ럏Gj>3}LׅXԇXćhqA3_W=N5 @G{+HX\a1R"W5v:Z7q$M%O*_߻邫QIDq />)1~qN8m*yD s-;wt__> 2.Zj[x\ }}Ew1@6|鍌|=˂@yU@^>D@ձ#px\z>D}(9W3gOK{s<0~幾-Gp8>|W\=O>Aww7ۇ׿U @8byy?OLAP1jcn&*fkN(*'G&WR3i:_E$ůw-ǐ8J_KF Ԙ!rE<ؘ9OFwwDz ?@^D"Ix Ӭ|˅cǎv(- mczzzM&VϾq&E8:_k2.QΞ ^9؋ww++y.NU"LQ^&ӊ~s}Eh'vs;[׎=C>=Co}8-sSdc‘#G(QeZƟփaӧ15e>3(s~c/ifffME~Nk _va.xjh8o#y$:^n;v %=a}έ_ mm7<' ’GzR$iƿ_ſǵ?8/v?"ڼ~Ȱw þ0<.5+H1jn佛} L& ?3ő۵G;_822fڣC̀.<󎾷7nGjSks $Se.8p@y%qGZ=?hU.WC50+{7 (YߪMՂHXEDXsM,aفu?y{G`&g߸ڢDr i'Ipmmmb$?{}=P*$)?LnM_4UV"Nc~~|[O_ho~zizpb騄2\_+l:;8<E$H r `.uL,VǠX,bzzѱX . WWYd~M>俱ߺ w>GUVzʦg'ڻî@0<ű}Up`T '"@sP(=5u<" c/h]g͡XJo֖ l@ 1Q>Gp޽cD#~QE.OS###Pzx:3k͢Yͩ1G21Ɣ#G_ A~BX[oe׮]JUƘ|I7߻ '_~9_Zv#FW\j/1e 󶕣"j(@a@ P9Ĺn9u%2v9!x>;/:_sP(8zD"pxꩧm+Y׿ B B… XZZ2pA}GGmsYH˩1Md.Ďg!dƀ;8.9G@ 0{Uj1Th0kU}1<`@۶0$ }!Dgp0>l-V?_aI&XY_3J +R.]2O^3CICMYYYٳgMsQ\#^/Q{XSDOy\#a_r(yv 1Uҙ39eN(/-r6_;VV`g#^:2X:\5mKO[GuիW _w a-Ԥz199i`FOOI?Ncypw?h%WOpAxf46 ch\󫴏30aZOޫf=F4kv6P"ܛYu\IKG9ǥ[ӶǾ~zC5χP(U>|.\lX,/|~~  c6;t4IriBp#<_{?h#>}1gkǷ\x<_7xUsM"vF j K\#8+f{{w>/: A044hG#D[ ,8p@ɆQŚV;w{`8ƀ/؏ۀR {5 zF>ZeFa2^ {k^K1gGk>3+_95&|G 9rQ9gX@!qPwVwttw]L/eq}] D< ޲rkQCPIUʛ7I.@MvsլR-`OzD/ۍN<4G+q D"xɄq=Q7;bΈ1 yF_E!ǡMc82Gsy`r^JT/7/]+Rv:Wҷ+U+߈ܾvI_ W^Hqj/>]? aeqq`28K.YN+_n,7 ~Rk 6@} ТH}12^WYGlDŢi #nټ?~$IX\\ىqܹݳlxH:B\IZ%ҨBi/oqqqd?m[CBt(0%kjhāB5 K)D~#ƮvWVVlu׋60ư~6cMSu}4#$Z1BEkjB@fk>f px$+px(~Queiz"U+FLiS^)%'V;;; "|U)f@ lڵKLl(cq}p]:C[*wG嬎cs[?$ٛlsd2  kv f$kcϷvzofHsӱz˅{*~?</R)~"M}=Q&|X#N QTe#{vt#}O"!A} E1{Y:*dEA3#!bP0^Juݚ*Nœ$cfZ,ɑ68.<1^{U-FMhvp'\WmbN&P V؈D5;ppW/ܮ{'^_4(bA>wΌ;*/ VRGx 6Ao@kz*h1(jx^x^S(t|o^wqQ`88ujlhM̯lzcyƠnsq6̮(.Co]P˟>tҒ(@,sT(7|o%HkTNԲ_E &`(le/__z]CR"\_ G h/9dz:_Dp`G`RS(ly<eZ={lE6`{oEHfzƘAv<~5DTL9{kXQ`80w"^تa_CV2yHUo^9VsUN+[Cx3I^Ik[Z v0W*$Zl6tx,tppmm9F4ӥE G %`o_umxg_lȃj!`l.WoU_*Afh_g.zȀ &4QU3A )ouyYwf)GEcL8k(pm}G^V@`~ `ZO&N;ߺ8YĿ|=}ADA6O}y0-sd6*}~r]5`reDUN*4e \=Y!@`AN`5m'Fa;v8Wl}WkHHw @$NM\jS?Ӱr4 ]ap1(OAiOQ6YiZ'p6\wdmMK\/w Ys^51,0Jc'DDFGR٢Q&#LZ ApR oHxܴ:f(l6x~8u`7S~g;fvya$V}\e 2+BAWJh1`Ab,b*7ԑ́C^u&,=}^۲o^ª+v3cpΝ;mK .@YOƿ ФȞE=~̲}>5].?%~pzK4*]}y׮NE, ns)O+z%IPmyjzzM@}IV߰gnoo>"[mߏt:]vabZ\dK[l _?6<@!r 1(A؍d~uύ{L֚wUFP?^1Y$@nG{}VOA+ʞZ B:JЋ7\ۢ|D%)PelC1cd7ۘ쇆4t:b~V͋{& IDATzm#(׽2 {c(B7Ԇ_o+G)ύhicq2*#WuJ$Acw!HaeUJP!G,J mTxټ\/M ^e1=88(PFsA^+A`cw\Sދh?'c6كBO{)˺lQawf9P~S%ʃK8Uy}@ӆ[7>^iYUX_IW0z23}%=WcnVD crRH+Tnyi2*Hnbamq_vTX,"N;|moE( pdp=}dv$IrdUnTYshgoph*٩l٢5U̧W6Le *iC'CU,˥$Ziv_Bm+bDkە#dPԈƁWc6$E˟<@0Wo+_#UtcI6gm[_7=o%ǙOa{H$,d>(X,bhhy<|K^!Q=M>͂6Ν;!{}_N3C~x0)L~r{mơlxe9LF:gUBB}cE,.4}-r31{Js*댾QwiWP)|AB αZO6yGm%I'@ߒ<300` 2u2h_W26dllh=ldCi7-wa сN\oi_Ufb6 podirf|e}%ԭx! (u*A^f%#Ԫ@ӐV0>2PR0 STަs> j/KhZ^K3fXH8c===59so~kC`bwݚ pݖm9M{ڼQ(-֎AW{]\@ϫMPBգS(ǽY6b6ՉqgU2>Ti}@/F;иNIg`*a\ hea6/-蕌 V8 06fT6@^?6sn۫ ޿Ԧb(?I5l`,K04šN~6{6>WopISSjۭj0Wy6pSJGp}$Yj6$LVL1w-YW{ﺵ;A^?6crr4iOX]IR^Gk:/3ۡu\ OoJsW f6tyU_(M -׾ΨT+0nzU7_đ)S<$ 6v,_yr~@yq 3cʿ~ۢ@BSSe =$A@Ov6dtCp0;ud*xŃ.G6WL^e+ 1WՆZx/4cڎ> >b+F[Y~[VjbЙvҎs״)Of{#pW^,l]aW_2Pm+P۩W4By<tv ImLo$f$u#$IrMarz\t%3^yõ"@V1p"rN,gU벑/Ts ~=rtBI${a *VU r4XCg #Ϫ>}0L1:i&lD݃ǜ-uxV'b} ' UބAdrRfG$XGж* a)[[`iEZé" a3jo>ZF}~(Y=4V>"j_ (wy)rT>di)pH@uX;lws|I{kx<ʽ8I\\\B:!C`111aP@.g? ,Yb^m^M/Tms PޮkD!|83VڑY~ az[W KcQ^*?SE'IF._D6o>(7$_"?Do"qYҸ@ߗ>f4k6lrjNj/W@53:&?*ky)}XrK(J嵉Oεc2LUk>=Vw@< \FKXƜkېs S8X``9`= p0]< L5c~bF .M6(ʪlF?$Y1pwp4s4" C` P Q~*.OÓ~=җ=ԉ|FS3fT RYH=_zoQ)P &wy!Ϋ\A~N"`62W8Z!0͚k"lNjyUgo%*s"IŢl6kT(Mb`CR{P1I1buuoѥ4vcǠ,̄ވ_;7gߪlx)2^=8];+vA̪7t+*)Y(JJOsmg?ZTL(G  r|:2xsC\t^M^jrkzT[xe`y:ˁU\1iܒޫWԔf*H>R)Xhew(!-(X5@՘;tּՂDG!*/_:DdJ+Ty&h3<F^Pv Չ:ƪORyHG ArVR9PC1߲/u4H(ͨe w#2NIT:ҦwhD PΙ5PI}v@NZOpn+WM#o&?\ks95jsƴWN]u?.(/٥նgznkZUZQ-FIEV0A}+`kTUJ,}ȁfzq;SoXZ8d2~GW>bSQk铉` ;p6E1RVI,XAz cexU"zUoW܇~z}̆ W ꋙPH.`4iR yXWZ OsT*Ƹj/Ӽd}P@)A6\ݢl5m̃r\c+}Sޗ4Ʋ:|/}ZT-٤&[ִ -J2%d "BaؔƀKА5(%ť V=_8qNčdWUt۽7,kVPrIP+Fuȼa[Lpۈ~"Z)f( bt% :-'GK7WR$0$N!>Ԟ={ +X<ԓ$; hBO_,o"㩹d[H.G林2:\p4:xɴCF͛JYmKLfp! ӵf`5@4pUqBڹ1yJ FtQ՗,#K6O1)Sƚ 2''!?| dM$OEɊFTe | eߞr) Up$Χm=%Ӥ־-+{;B(:ǔ.6O<`mId!3%!%`C)E Ü#Abh(`/Ie 5!-Pk=RiҭbMfNL@e<?|@QҠ/mW :K,HʘZ9%-#"_pB;Kܢ/@%ZC?xO'ӢhSՠ8mM̟_XG+ϋ%yOPN`|-SG%ťh-Mm `#6&xJa{ӫ7g"-ne,hR~w_ ߾.J 3(e6-$dtm@ˁ!GfM >t28Gd.^s 3:hZ?`5b0TP/JuVlYqlJ!>-Au׍zܩM\|5TUmg?,@3U:fN7cBn3I;WV14T:fu!l?Ldd3]`̾<gg<OX@$@K}ι_PC%zr>k5V6x-00RɁR#i/OK>Z1amx ʆ~7 ԿNCVuD`$% f?5j:YKXn RwC@$@)&lj_rX1+`tO}vxk",x-'Zߗ,d<dKv0pyPH4'U'bʵ dK3(9@ ˄tU*}U>.STe:dXk򜴊ןcS^Ljؚb2[5L2_n+M$Db PL({LiZ i|O;b]ֈH(`+Zo?WOx݅ysoI d0\J.gMPzdU%[NL, qǴ{ ^֝@y^_ja I@BL( G~%R}C(r(`h$Zg9֕r,o|1|߼h<+@H6hu3ptZz =z/Lũd!HDE'1L& NFbP"Hbk֗fx,7xl ݕ5Â.GAHb;uZQE|F ZUPqoͱ>I`ߕ.HǙ[,.eXt?tdBdwj&A)q%h8 {X(UL\h5?!0Ks}Og| YbDY*[?ݑDV+|@+`-I1\vWz #byg LOOŋs7pߞ 9rTyõ Fy[__G4P u $b6 |C#J~/O>6VEp&”sZgb\:o]RC<_$ϭSK 4U IΠ;]/H^_EiX:0XQu<{* qƣ]u 9Z^1^F9Uz2 мvC/E,YSph<)UXJG304wMؤZ,k*I Ω$?o')n})܋:R15ͫwsRXhv}ނ:ZǴUn^l@ˋ _Hd=|F\ZZO= nՊʈַohW\_nW i G 2$eX]_lVDB!M]ls@(at:@rT;)1A^P 趺!:8pN_[eQ:.'@)|T/5ì IDAT˷B`Y$& &ȝ@?N* B5Uic4[Y[hth|[ @7@ 0L$(mHIج f`uH!qp\۽"Gf"IMe<呌tמ7uWtl#5YCݻ)Jlk5Ϥ +&+w) XX5`5 o?q }-" )/Mi3aJؤ!cj:6:Uy#2@wphgdK@(6ҿ%]z=1OzY0fVkgDg 6 !0y-]; REZJ+zͺnkႺ{^zIIZ%%2|hkuW1u1DKKMbN[  \8K  (_j#g-Ni[m'q;?{@H;/Se}%.Hp+v%kFx~e9ȏO s i]zL"I[AP=}`/ʷ"ļr+`w62v@0iCfiF)Zm_!Y?56%(Y?Z[kd$(6Er>K(?m躍"GuNt]v!"X[X3Zk-1ϝio=Mў(1= i!*THe 3UvVCbB2k)+ĺ.ʈveӹF"i649? 6FSr.ۄvG)5Xcv&,Bq&R1 Nƒ#)qu&K2׻'H׎xDiT V -ߘ_ޔq_p)gǠ/%PfTy<u3_6_2H@ 0\Hi]`Vd(\ >4}A}sxJ7Ir)%m8C40V;vfP w1|Ly jt NS'it : TWG kn+zl5yV=Ѻ<_`>>^ȿ}dYLyOAHS7)$gq͔_|-i E*Z9a R'Ӥʚٖ-LUFӅP&zNjk@A UX7Hn 1f\iʚmm;}>k $dS۬S @esֳ,*蔰:㲵#*Or_ KeI7^̠T&y&=xJ$*䔫~R6W6Hm/:ersJ*r]ɓʟ p5 w3FՆS -qfjYǚ!I@7ŀ%]7v@Zu8H{ev۽w&;/),w_Q^ ٨YZWIceHs ( I) "' HQ"6: oLY9Begx84a$%Fcvn4uX7fORv xq)| c](Eu= @Y]f )T1[<`ER7Wv+2/d 0 ,t!bn: 9O{}cL @ٖ.5}`sB(9)ʻޜH8 ЦOHg #Hfסڹ@H]7wڼ@ 2 ȀAeʳVp\A2-!A3&6 })^(Ytq) @B?. ^ v`!pCLf{ 1(5?ʠ,Si`ɂY"n1>11d34rF٧hr'O[MA@׫YXmkI1Aq zuw/孼ggI$Iu0@7o `/=Y@_)vO,؀2e`Fi6 v@PA^E6-.$)Z;׈,wSJVA"a6ڭG M^Y酃ݴ>Hk9̱ŕu7c:lBY3 Z>y*:#/<}@Cgk Ct hgrMs;:?&7S2_jUپ+=6+Y.)eP@h18k͖|&@qj/<8KӾA^ΓhiNQ%J"F9@DvmIJ0@z+-@K3Qtn[UD" KtMVFZDgY+:A8$h-WQ0\@ffO=˛%Jtx?6Yc}RUJr?tG,vb@\!v@VH6L?%_2d>xQ:hi ՗āWÓE>f#nT>В.sw?ESRfdeWiC&K!V'e^CL3uiJ\U Xb \oJ!Yg-V+XHWzҗ-I֮bf3u`hn 1ǧk:-T7*+`c ;O ؈.FuYyq>p:N5ܯnY|MaTz + V@99J4PQ @yf;ZaAXhhy|x:em%eNd"KK,FkNSf5vpW@&H ,kfn 4SB\}HFxi!pfF,Og%E 'r AJDDz W孭e(k:F nMimN,V[m"~:lҼv,$iV[nelxzHs!K-7.; h}ٺl; '6FD _KKKi7~^{ '5)]vڭxDyA=KaJkG}.0(`\uL3y9EJtPf9'?kސ R$ kaf,+4:us\&2᷑wį81 \qѻ3} }f+N/cv&%oȢEQrrj%H@z 3NkK5q 45hv׶]V=䗶qt{% 0.KX_OlgLvSU ʚk%?-ÃEp}T@٥$ZF vښ+` "4Rrl&EGzzBae[jw#]ɟ} 8'Wpd?w ┳Y@@"P5X@<_ge-_?0QLBZ`ppsz } @l;SM4 `7>'y]SNJXf&Zn k ;(sQ_%d+0@) Z.&`Nk$H=RXX ٩!Vr)O ,4,ωy0jz]GK,aТ,>.܀t?0HucQD<,8e {@;cytߙ%=[r93G"[QЧJ%;%~lO:.Iy[2i<ֈY[ O^YDU>X KͅkcNr㛛EjqP{ 4:ꍖ;\$BF>0y3!qLT>^2_5?l ! K.2FQe`+ _{]/ tKK,@=I(BR :Vd(CO_Zi(j(l}R(HY E^5R+&4yE刈~ (g-4\m5ö]6Zjv'*?;CBwˬ֧y=8pxsjėL>&MT,J# 2J[8gg-)G?@&s2$똟/k>wni}ȕ](j5H,򷔼}pwei5lL%*d]dV%+I 7)Mڭ)K+XY *mi‚|H3p4LOO(\oQcΔO{e@Wpu[m+(Cmx]Gd͏HwK +BM7HvQ4 +hclt~Y/y}''>gyۡΒ/=rO x`p%3,$4uZc~j7Y[g`֥+iO)3z~i~vo$nC>?ӱ=$|r}Qh<-ycz׮JzEyH0*o:N<ျ 65{ﲂ\5)D%H9)@au=v:{"Ц{&NYndj!78Qu!E6q{oa]IDH_;^@vIjcO XӀov.;rlMލ@qcuu5uY*‘hU|qGhZc~N19K>aj7_|ى8>見oI̠).#IcS4ZDզ&M'w$A%|w,+ vY^8I%Vy,({ "GHxsu| h,.`c@9Pl_ݗIg>288耦J<}h4ܒth~zW ϟ`) Dz . af Hr"&H@O&"؀p1# SXBDOͱvwo]zV N`h㾠}V"ɇ;*2Qi#:ui-hO*Gf;EVw=nTvq׺)]J[|ɛZHrX;n@h lqV+f$Xd@3\0kbjn x^= kM$":;wk|ǵhC|K=7ouhq?B${d3 vi1gYly=ΓG0X׏HϒP`t{l-lNzVf@=(jֽJ_4U* YQT"NN@:>w5DI8b D$Lq)^Yss 6r.QDAF5f]/ n3   EV2n=0{n~a=̘b8B֮M6@IP <ƚ"06WB+j}MZ7_d;đ~k,/͋WV Ȏ@صH@37901exsE%@=b ,GZV6 d8z)}@ogIn@ `MpV~j%fP9" ͒^e@Q> Zfa&;(WjtT*D:.^!WMF %麐v< XMSibpg-MY,}+[,cdh ;V+\Lt |(^~;ZF(qwKz؏f! w3gsAFT="ejeqF `ut7R?֘]X{x|0NiOKo$#gb5jg{)6Vh9^d: ].JW7/{|C=#2Yvt#,9(R2oT1G=A4o߾:tt22+^`HJ݋.d,Ν8:n+"ъlXDjjCܞUt[Trq@KkDV˫X^]*Z0%YN%SfS:H^K 1͐G6%]Ϻ )X@b``Y"nKMֿWewN(pI\x1unyy+++s7iZ`& >O.]Fj/9INfwU*UPPMɐ_J)@5vl 9Ȓ _z2 6\HPwlÍ7RHp6`5L[og8߼29Sx|,:1ZZX!-R pɉ)e(|R< ߩ @`gs?1K_5Ϩ5 h[ Lۓ%jH zҮ*_fޕl I.ܪ{K^Jfl;v ###)m|. 044ThWֱglȂjY!aCg P qQPns"I%)h'3 k}¹sꫯu:LOOFQHB K3{1><Zk|}46ݔ<딭jL$Rjg"mL2HkP!iO 2搬5큱&N:jT0+,Xk&_ʶdN@A>|q_N/%OܲHgffJy<jZa /{IjdW4/vf@Q~a;\v-~}}|^R)*=o>pqYze68n챕 $Dv'8&8-yOϬFDZJYR6s[W`oX `!IfwV~V%͛w;BMd7@̀z$ΝCEApՔ1Y|$7}@?ꕷݲvg(ͷdI ٌ8 b W%1C]Щ/푩ܚ͟?z0_caaKe;v0M9^mw/AtX%$= HKd_]}L7{wJ_SڌDQQ'7cޚ]
퓭]:m$wF׷m >};|PnIv]y"3u޿o&RF++ m?f3U[9RL@7Z7 Z.^KO}HtjdyX[E%;&vs^>x|"2־dG&wJrn48MR,͜2}=&{y7 I8PWh46m?uTts_{'qLJK/-}r+͛7SIZk#ކr)u\asɉ4ϻ|':N (h&Wzu+@LdO?l.;>^\٣c8zOn^kS  Rz`šeH-I'M}=rRڹ"N }Ga!"de>1푿co1n_kJ/ȇO <]kjp~~jVF'NJSH|˶/1*?6mYu_ZZ`GPjR ˸~zjZYY}&Zӗq\xw17 /_Pp|,Nm0beKp^$ \p6Ժhw/թUa_%2 z2畑=ӵrkQ~b7ndRZc"m*"O|>1sy|P?,J#ϓ'$/3_/|5pƕ_)AJ6ԂZvWסZf1 |[yץ'n{ ]?ixbjS.\|ddͫX]/Υ H6|W!EZGu^,>1[#P0("+8Yf_Pny/"Тr^Ȫot?o{[ķd45^u,krԞT AޏW9uꔷVoFy-3 Gvzm]e12_w_x'R 's?|[Vp˗ |w<}Iw.H,!ǎ˴\p!Z…KmKϝP~靥&ϵ N^Mb'?| t+1_Z5Zk[פ@>My1XHD[v,s;cZ4Ѱ"2˶}2{ <,zW/^.Ϟ:3Gr4" ̔ 䱑2X/D 5* -GC`}}ov 2V=#U|cxI4 R@䏜7_ ܭVM 9_Xdht`k?%C.`r! %%"Hb7&<*{W264}x}Z,WWVͽnnn`=-8x`^C-L CCCV-\wky)gJNi@y(@9@#evV8u$ ֢͎E@1K$l[PgRAm`Y:a+FRm @9?y/Ssx7P$ų'0>\ѣGsݷony-RSw_JnCɣ̿fdx(0s&IY9`"dWDF&5굫;6#D̂. qD#yKJ6&XA˿L @k$ɨUrY'>dAX+|bN#i fpЪ?mΗ'7cDGy?S?~3&] ]^5r1::{ΝJip9'Bh^Hj&&&ߣ'S^MwV"L i0%1H8Ujv 17; gQ Z!׻j^_}85}gהԔH/ɢ1% {٩bK:E.C(T߉8@)9HOArAr!Ļ[x-,...)^6[>%9Wn;rәn2LNNR?+idxi'@+# ߂,8n[+ʥ@:6w6C~t"ݛTzB(@' ڷ#ϚvH% QMYin/k\(F~CGCZ Hfҥz!ɚ9U)[ʣ.lMz\+@Eٳg&''3I/ͦ)0K(CdV3&l7c\KlKYfExSsBS: DZ @$=JOEg lҠPQj.l&~ &- s]Jx{(jxVr|0~vQ tB֭NS:/jVd'{<;Z rSA߿'wYz.\H]s YRϟރCf;2ݶ@Cf3SFSZOOV IpؘiP`^ OR7e`5@mitmD$-qm: 5njJ*¯,޽{EQ.u4E._Zŏ(5xx8rvwy2HnYэ?|)W.w9{l&h6x饗R,,,nZj-ju7>}J#\ʺvJ74i+bh3S gP]ß3[Lj`jGӢB8Vc L44c]>u Y|ܷK^fXsI]1w t˔ː9M@wdȞ={L[oannιFk:­Td/=WRE@f b8+pC3/O'@$mOZ t~u ŏƆo~L@ Nl6q !gFe7i-cQ@l'дLEh<33Ůz(8ާ!J@.}qٳ '8??ᓟs|uuz=w:ׯ>?Q~u/]̟iWqӨ T\p3f5rR7͏UjCDТ<:dOLN _o{:%w_L$D-/%`zɱ^82]+MLZkܩ+u)_&044bB}뭷JmC15^hw+2 k1,(4b#Zk+T (RHbҳMzh#JX-wÙVk׮gff V:c,5|u -Xs )`+NױٞrE۲ IDAT %@P]!Js]mTiޔoP>;xEmkԗڿNG…嬸}b~Eؿ比zt+WW Yެ7iX 2r'. 0mdsk͊oWWuO˹sEQ& 7j2299Yj Ft뼚 ݭ]< 8}D =@TΠk d$՚mļ -=2<ɇǒ"T6 W*N,^ȺZ?-|/)u{|^5MKǙ믿^jf}ػf\yyW+8vIov|Tt={ n>5 #Y"^G>E>k׮~ ~/)$o][M#,ԜZe A?+j <HC?VIi\[O^AZeɌ|;|COX-9s M.@ KZck@?9`5:^HA,]V\6Vs~7، T;hL߉e:@q])e$s=`EDN(t:ZTeSO=ճ~i]^}U;wǴָ}6xRСCu+v#񹏟xja 1PgF%J Y*9 |i(jր$aKkOf%$~nI8 +eM9JY[@8h;` H ӄ@۟@Q>_SɵnuYLJ?aZ溤׃jlUF:Ά ەG5"b2t*Q6oO?dii1ZZZ ޽Uqܣ*a3)c{ [@} +S |랇-`V&IӷDI$=U:J/߈(bnu /߇#{B3k1;;ʕ+{]DP-ZFEzhd`lllvmmɵ^ җ~O>~+7cy9y=0P@-< of8ݺu .\O>pa߿ffs™Nj?J Uu6Y`<Q6Z?՗$u 3AtlH&JdЦL-0%! ro3&4  WGRO#Nj8pjE &xReRTPUۥoDRYqޣeկ~uДRQpص5|[/n0Wy29y+]U_ %fɷ-:u {K ("y 7E"9ΟS߹:ٟ|õ*ʚ8hJ [$kx`Gw0!`m^+mL4((P[g4WqM&}F ii\@6ӢV1Q T_)|}KKY[[  .ZWkRZVqPF@0:4U_~s ׂ3}Qp PZX]/sՖMw"m50RU8δ{ k`O?a]=n=X ; t{Qf2 /O‡, .]!g։ʟiتc@d KYy[[׾F*67~ffԞccc *~`e?O?[ Úfm>9 %\ǁnKmw {V;Ә];*&aZǪ E~bޤA'hO"mozl5ODǽW*JVe{?Iח7 ϟ{GG=UjJ%2_Zm}8s%pʕjRɆ>ǏǁgLLL```Q^gGWVV044TJçḚRT8o%p+ɏ>\b<5I/6`kFWZcxd#F6tC$&렙1"p?e>f? [e3it9EW^zkN)W?#.@BDq[ne_}һ;w{!1[VQ2'$_Wc: "#Q Œ1Q Ν;7ov9qx }ժc~~>j1::c=j H@uܺu ǏOYnܸQj&A)Ƀ#ÿ\z*ٝ:W?O>}CID[$@qelL + \tYt8Ŀr%#$Dz"dp\OՃ0_^WC^nx'}/7HI&Y^^ Z;Np%<8ynq~s.$X)cM/e@~˗˹&ӧO㳟,FGG1<<\*rz#2<+\/7xk 3x"i<~J1ٓ 2{ 3"yhѴ:!"A"Jo}Yhau$% .ZkwKI~=.?$MNNNZ3^{-85$?+Wyl֟+ZcV{R}=+F孷Źsl6|?8PV122b?ㅚ{Twm9RUTpرҁԙI|W;xF,+&w7Nġq5[mH@b {$ 25ӻD ( Z D`bDUs=.Pփ`t4^8{*V7R}8>6ff;=ի|r*<6ػwJ~1c֩} |ۍdv%ZkeDwˁ9΁NSt:QF'OSfl_ݮ]^{ _h4r݋|F#q+PTg۷+aJv c9܋L))Շ i[ؖjY W/]b9&elhͧg˙I:rmܺu++>Cdw<|>`o O3Д׷6MdQEZ^G۾DLdFQJZ t:g$T*Cʭ>s6~~V+155'ƌ_ @2pee׮]8>l8N```f8p8Ht)YՁ3g _Fx~Cؓǰgl% H1S_Y`su 0AN  ]@ K,Slh`΄E.c o__3-.R=1SgqhOW)Ç&v7o7M|Μˁ%TF;^&[맿du)k̡.H)-MtE:cq(tEJ)MZR*qYe@؂^xN J)?SSS8p\}]WȥP`/MdU୐|x޸V:Kq5W/NGGt}kn1c| a` X&lț,da,6DyM˻@gC6,x lȖ-[-[sHs==wU=GOTTwW5=R}iMO=utݏ:,S4cwi2 Ԩy& (.\P]h4NQ+~ ȟΣ6*җX&2*zzzo}K|mȿ _\}>B(xwQ]]NXVx^0 :zKKKb3NnzmKSھW<7wtԢֹ2!в J62}@BAeQaW+}Êl n ˒;R.ᵾ9JKÄp#[ H aPYY"Lj&˗UYTT|ҳ+Dv#_YPgf_\U!19( қK`2љD8i @Uwm'v*(())QkPJoV G"XAkm1R[2a7€!# P0C{06+*%ZkHL&ØIͿW_}UoeY| l]HG[ĶC83+9][ Q78 Rz1LhՀ#Y%p8Ly0ˊ/!Ё*|()J^x%NR KJ. /t?B'b@GhhK/:@p5탷9 RtR{^Yם"o.\HڮUUU|سgb=%X[[SZFGGGʴ-R Fmm-wd2aǎp\pk]-%1OOal.s@ &6JYM*iAӂR3#lS?Rm A5 X IJ7?8.w-!J?XJ 9r TUUfVrbqq10v/ IDATk N455$櫏BcmcK/uQX~>+l $zΜ`wYSSݻWѧnLOO19d2Ie0QSSSn5!(//jf3 !;p-e42iZcm:iV3 C̦+2@AJ(_110 8?X gEEE\rrݿk c޽o5RiI_ 019o\̦H a>;m?pw{u PHm-|>/W&P544H߿_q%y}}}iiYp2\6*UUUi*?lЀexrk Vix, )XXXH8ۍkr 555V}H]'1Ƕ_q`xd}k0pV;FPOU8|pdKRJKQtP~2;v_M]t[yyYIYYBY?f O H$#M?ղfî]Rw}ZSDB!,..U0ka}k/\Yjf}mW0 b|w,TO +LM .\I󯪪BwwwťED < G E(MH$ iEeY, #kf0`Y#w&&Fhqoז<ܫ4g"`#BGZD;nfHN6 BߟPlQz2N )E\pAGȿA\zw[L&M_&%fdlaFB`6AI|>V=pz~_P\ˉ /K9|p/qB1!LT*B*Xt:ם_b6P7x0??{ X޽p,ˢK1gZo @Rφ@,L)4*++x2m-`jj ՚ȃR8N`uuUJhƎ;܈ˣ+87GW.,Th]ekw93˲1==_]yѣҔ[o6]ג+aY6[[} [m!8wz:tABEcUU2?˲+LMMJ&ž={ `2٩GU"ߏKvK  FQ2NLL'? ? "Q~TTT.$bAMM VWW*` Dp/`J< hJ /PE0,n>k58mF؍pXpڍpPSjA#j|`0t*ְbDž 4uѣR `U` ,EŰ,q:8*xfHğJ/^WՔ6";tvvn2}'"HL |%e٘罺8|0>ߏmZZҼ'QFrkZ f3N'GZ>~aiiI/jN񬬬đ#G6YZOn4Oێ0̰x!\/ bʺ+i0w^x<4D俴1Br'>'5OA(/|KKK8tb\|Ja4Q^^Rf%NF0p:iA)*VVVT \R ```@p^SS#Gυ˷L ,ff58C)E(TeA"c_p ]AXa0 "a$ yY]M*Xa~GYE{{;<ʰM1>>CI +~IrW\9ĉPp:D$&+YaZp8` bZ:,ҥK^=l5]7Fض<Ŕz<->[`Y#,&3f F3X( ('J)A>^.0  Kd,%H?L&h}?l/?c4{nx^TVVC5bbb"'U%J)V4Ꮢ bYXXXOS|Ν;>;Qz(--EqqqFiш2!!vv= %aͩxJ  0/GTHvVX[[Cuu5%򟙙TJˉH>t: pYLMM}{_4@AU+J 1VP~>/rÅa``ٲy. ^W^axxXɟaSwb<>Xv-\>hT"|R[ĿJD/ȟ93D|KۓqqS 0J`0XMDG&C@R~_\\,رC-'|p5,..fUӏ_VKJ˥A@|XZZ4`$*z)۷w}wJu8B k@a$]D"aB!鳕B0R-wJM<ۍUMM)0Ust{Eo_m,J_2gw@Xc}J&{$4h%qQFgpS >S޶n*A _?vڵdPYYP(&i9~ Zuyy9~V_\TT$mH$F p\Ew$'4("`qqn%%%(**ʪЬfdqH$ wf01L K1Zgee/_N{Άf\CLZ֍Q/ +m'fyWO>}cNln*RLF}KKK?B6Og0INCt: ڊMm~JN%A,6 r@\_XX1jO?;w#+A\]tS-ȣſsI\ ȳ) Z)8\zccci} nÁlbW{ kZ@ '.u7ѱZA#ᾓ qS P֢O2 sKvl8;wĎ;Qz9䟎@ACC8('w9SԂ?]-X ajj wy'vޭA p8l[B,㰶Ǔt6dƕ+Wh`?AFtNĮMm_~/'dZVp"*kuP9MqS ϟx ; fia62d2AٳG^D t.,jSg!msT,9wBZ{ $ Monn^TUU%E`ْ@_,3X\\į~+஻/Td2v=As J26~?}]LMM˲;[OVEF6LLM/6p^EM';= ABH Y,E0LUvсʘȟ8 ,P_Wi愚5`0p8˗/H_[W 9J׮]$:::٩Y].F#v6w#y3EA all,#,XbFu5!KYZYi{l?/n0t]9tP0iZ@j- !,Y6T~?P(gzW0 aXV$˗/KBM*'>?&RJDRwٓ Dx1?9 䦖A(FFF2R :;;٩]=cdpySK$m_%_+A?4>O?pS /}T|(  Ռ`v3 }IB0ND`P*th4E/rI36 @^p_җK/̙3jn+WEQ %e;[Oilp8,6{%n`rr2sfqREB50|j@麒RpˆI_K?8KJ A$7 nj@sέP-www !سgL&`77```@"\e& ---1>nx{ރ14wa`l3 8|0:;;_j>Q]ftttBS8n< D';aYx#7K199Ѵģ wuk$>մQ#0*AAhd_w~?j} ?~A~  `X,8B188( dCӏߖ)[,477K}@lxeeCCCX,px'!%fD `PZZ~xw3ri>yǵkpua׮]hiiYa A$m|;rabbSSSYJu]P8$: SfˤbݽqlLɪCGglڨ!%'`mm ,Ok_lDKKKRfw])1o00E۷xߞWז[!\.^{5\t ---hmmEeeeNߏILLLf_Áo= v!r!* PVp>_.<jp8:v죃MGt@; J0!h4 @v,/`?A`ZCUDGFFk׮r2رa%P* }8p^|E[)ϣ_tf (+KqCbffsssҜ قlƁwޘRr")MmR@6+O)?>`0޿̷k) *qߥ~IH`EEE1#Dd_YYȈ?䯴OXDā$ڵkhǃqS DV%Fj/+W(Ou~ `ppEUU*v˘LV5}F)THB05d('Ld:ȗkmiyXI$8z !pX,hllD0āRwyycccҶl !LJEEEhlVPNER`||\:VVV011! r#aPZUܠǏ]w݉_~E:(qPzF#jjjPSS 8 CB Fc4M.cXގ}%%~m7ptOq!Ǔ8fff@ib_msM㝎?Uee%~|C[o˗/gTG~/F6ݎ8Nm^H$"%X[[j^rݻ سG6Tc%#l*_ˏ۵b_@AA)ǡ LwvvYM/yy9jkk366yP`,@T^Qd$2u&nСCqۚsY2+tLeaaXbAar yCP'Ks,l !M)o>1p%#^5„TKa >vưY$88q°ϔ&k'|yGuu5UfffڧTeS IDATWNZ Ԩ&Q,,,R e100M-eI*2ط9S~혜ozšdL8fpN! ;wDssRuaAM-6lXC?MOf 'N0y8]UUv;A@}}=v?caaA5k jTVV"~A0<<,n4HC>$l6s5ǒ=waS Bߏ~'^Mmkmmm[в1ۓS-J^'Z>BWᝏ}'  8q}d2RFOFcccX^^Zj_65$JEYY*yCCCR=ш7|S GJ BoliTȗGfnm݆e ahh(F[WM݈B@qq1ЀM@3}D[?Ugq@"MO?Q^$?ч[:Mp_@<رCLODJ %g*d倫^q-sAnQtRҀ:B7}}[}qAWb~eA@ `MWz_yG(P}ɇϏ=|_/7pȑa؝ !͠4*ٳ5551mDA~2%hnnnP֪qAA>/F#y[II JJJggr ˅x<UhRXaEڎZוC2olS#7&Wly:`D}zR[qUA:=z !,DmAKK x0@ee_${%yY @ll6U 'ŋ4}aڈ?f3jkkQ[[Jߏ`0P(`0`0(!D'`6aZ"88NX,}J}f^(}uEAԤSDUBHRW@1[J&a&;N}R.$M'=zt:&j0 Z[[q޽{QQQ!ODp/x&ڿhٰZE0wE !xOOϫZ3 j3j?ASg6 6M^ HBE x>] BJ,nO&Bc>xA@UNS I/FP;:8iy^ .$M%=z=^ "`0`(-(N#slhj ZZZ`6U V|G0g}-'~-`fsU2o;Z.(q`kɟ['M/'/ q4DmXEss3"XŁ6<%"p8f?yi_e"1HY[[CF"f~gxPX/lF ܇O&-E&)%8¾TtOB8z<(Qш&pш.m4OF`y>'@z/_7hiihTE8O룿NgTk5A. _e+ϩԐV?sFC9u6-Cd^8vҟ$jc6`2.OF~!'@}M&ZZZvcpp<σۍ!MϝRo4?OOndQ:1rKۅ@ FԸ6nGVJgN=|*Y .$ -twwR} 'eX,qX,պ1m2|JJl6E*곲" 4\.h}~b}(ݦ1ߨįm;vmr"$j'PuiZH)z2U; !79 anGuu5xfCWWצ(mm CCCfصV`F/--axxXYk}9Os7Xj<8U[]/>>^ #F XPB=m-: );vSJ`Á ݎDOꪤ"/`?J)l6b_) XcllL" h}/Ν; ^֫}ck֓ߍއ?VOOq@FksL "©o<c<]% N;(?%k#VmpF㆗@NBDK'/~pb^N3771`a,.jB'Ν;YW oi]IB+*\$2U/B85 bO>@fI Rd˥Io)3 e\v-k~r~AP\\A%Ӓd2a``ZͥK) svzjVz]ڤ3cm<+~JaBӏ=/#3' ^d*++ؿD"'2/,,`bbBږ퀿t _.'q%3lƕ+W4;w}>}:8**&յG!^W>~w6Ⱥsȟ8šRĩ~:rm/8qxGLv555R- ݻWOEʚҾ ڵk%˗/זK{PkZv luZn}x5!H9_@Dg#w {+^z`[ ǎ3{B>]]]W]]vNF?[1gB* Rbaa@4 ŋ1uՀR===_ec8`*ѵBcǪJ 7"A9mcGn:m+9r> ! _WWݻwOF4J䟮 ߮AX,xW>G)M_X6ɴUOB[GcU~?W w'5Ց]lK, PJߛ !ҋ\__]vOFX\\Xfi嚚TTT$$vhh. `Z+4E o-s|նr= gΟ C.;O)Nt BrEp -MlҀ:rm%?~RzR/QFEjiiAss?nwNKj% ZtB~皾,#5k, |\W4lrHW9?zkɇ{ Am2gYMMM sΌ`0ZV҇RFD'?˲xWO%lv~km뉎u)C/(>ιy[S" ]pa>_ɿ-,G,F444@޽x— R $/~hnnFQQ*R!pႦN)uSJ;sLdx A3}hɱpqL x>Q7zo{o@GQcnLd2^"={FڟH'Hq~?'"&PZB^`0B8˚m\9ssJ,pˮ"/Bql ]n` 1cCN_&5㧾|r<t-twwA)PbA]]!:%oGG*+7dD0 8 `ւ:0hnn(NE@}}}B`@W\Aș3gz`>5q|v BTmr>V]53fb Їp$@? A}*; V> )Dm(+ې_OF`@KK4E~}}} 񠿿_룿N)ș3gF _T⠜9!lh]3Ls! $u5ۆ1dL. _pMkv(N=|:БopoDml6!XŁP\\ `ߟ188p8_M XEkk+F4%#5#`0rahhH |gJ@z5L*I=epTZ/_}ӧHljueڇ2"io}CU}n4Lbxji/O<`&/ N40&jSTT$aYp:_.B! 㸬1jj_\_]]x˲õk״>7 !G{9mCRo}f[E+ZrNl[L}o,$6(Kz"| 2S{V/}In((_Hr](--``/A H^Ki_5JF#Z[[lK+~XavKd4111)  .Z~.d_Dji@>slXEwFcP$_?S?%Audc$RoX%ǥ"ߏAPJdlFKKГE3 0 L&O=Tf!fgͤD)MITOMT!+Qt]ZϑP/3?@tJ/kɲFU 9=j $y _>9aG:!tww$1f{ィp:X[[俶adN j9!022J)V+* ~G]VCq FK|Jvr/ߦ5סv6Vh~Z=,dCJ}[2M~0|AB* B)Ζ]O~2t\8rHoNl6sva`%y˘*+e䟎`$R}1:: J)l6z-5NjtR@^&Pu_I=]!C%ߖTğMPs>&Z澾G֯MsO7uy>j]OHk~ofi}`ga' ^HRf _`0`c^v M133yPJQ]6/**BSSӦ(D9\~Rŋ A)~9lP.H?Qjj_nHg[a)v_Ց~uыE { #D(P ONĖ ^!Lt⡇BMM"d2Ȧ_ l >Ӛ́xW i~?ͅ=HS{6?5h/Ǥg/L6=S:l .u.=#k ҽ!?* JgO>r [&{mDv;/,f Պ/" e-/~  ԑ_TTϫ%LJ~EAyCMIz+HmD]rolh]Z^V|V|lt[o\l`4M)-Rg2/~uuu`YVJSp8@ Z[[n\t  WZZzJk׮annp8/5PJýEAyťEŘjUڵs(F!\Wcb{hXq}6A=fsȯKikMLN&z 1Xd5{W''mM%#GK)hԧРC(WٌXnl)&*JE `ll sRM-s gy\8+ҡc`0B (yP9y0["0 ׉:twuW`0͆!ZA UUU A0̀6 / 'Kסk4d6ӔGN~!36HGZHrGy/)+..ƃ>Bl6>`fttt(jJrS[[y` ⋪:BOlo}[!%:nB`ZaU;eEBI#1>'uw 0OV+Vj @;vyLLL$5!L:+A>~̙ZJPWB!:nTXKވR+: y8zh!d 5~ߎ;裏d2~HxX\\LX7;vTs~x^ѫV|g[:O=~dCf,]LQge]G '@pq0 E]K&Y/+|cc#F?"aaZokI3g\Jl1(O 1зTmБKXf s=*V/=PV*1Wh-++þ}`2T}>8.TBv3AǯBá#a0;hJ3g\l1>ģ jztLf3X#KN>r:퍼~[$Y(p8R jpRl !hnnnWE`0p\l6"l6M???/YYYZO)۞?N:tС#WAMի| `@9Ebfgg%/--ٳg1G{{{ԡC7/U1 4=f qz099).))ٳg 2Rz{{[ӡC:I(W,)Ɂ e PZZh 㘝!EEE8{?Y1:tСcyAXQJyN3!BԼ|rB!թ"~J)]y0 ݎ^>ܖ:t!!/CrS* رCHo2!?c===:СC#_JS & 6, $e50 s'7C:t( vISVcf9'*(JUןy reYakZO g}lߋ:tБy!+mA$IX,)Kg *Y  C4?100 ׋˗/k:!dzzz:tБ+My~rm6[m6&W?a 8x|Xﲓ縪""Vj-4M#HWECAQ _]_v6f2dY4M۷׮]C6[o2 N_xbO""Vk}}sOOOcii Bjm`# |U_Axޚr4 b]]]r )%4Pwy~SV<Z9tرN}@Ok144nx<7ڶ 4Q,!D&nݺU+0o꽑I"'N(B]+@__9>xފGJ|aY,*w[XXfgg$?N's3vMhWJj`0 } HBCD!fvgy~3oߌ~[[fzVH;l -I 8n}]!3"#2B# ]3>|Çd |>ZXʽj0_@?J1BP=&A)529sfpc~8~OrwǺ@%;}sC‰':ѕ茏u@B?E>VTvA1O601 8rY̤y5Vg=Jis䥗^W>< `pp?;fKݴF_7R-`Y`h_>o`u ``!^{'.e>B>64Kݙ%PJg}ǏٶڽX2 mfl۶j}QYm۶AUҊ0mO=WE+%ctOӳ.RincP6 cӓO>B`0y^rl M!’1Ð%z0T0bRG @QH"EY2H|,;| 8_T*PpX4-F ` cǎKHH$~eª1J)._ >a +r~cVWLǏyO/ej pպkjwfaU||/_Z[X |l` 3>60||XWSSS¢!Vs\+ EQ|fcM>60||>cg>|l` 𱁱Rئ>V#GV >|)+P+0 333> r\(ِldp/~yjQh +,!FF~)4{{J5J*J`Oίv|np _%޻ gV3>XJ%>4M[X;SB/U]K3%/!I]ч͚V` 0<2,+C!tc#C5\tQVC =3#wvvܽ{G,^\~ϰ8:c`]1UI W>|,|'JepXPv7fVX,v7| H3cg>|l` 3>60,}V%pdtSӓ+~_>lڴiUV>|<*| 3>60||XP ` KB:,Jt_a+~OD >}zU2ؽݗpF'q]3`: O,R+X|X$:ǂ|XǼgB1]J 4M2b8TUPE;a:tݜMT*>,3E6 0::4'h4Ex|t]GA*T4<_ 3DR~Eoo/<陹/X" UU`t]G\F\Z KB:cW徣c[Եlw7h @]]"0BU*T*J%E 9U~UUF۶mCCCq-  -1 .Zr z{fԄ477x`@KoUU‰_id2111QU ܾ}oF{{8-[؈ `EA)ŝ;w`f[WUhkkCgg'Rb"Ř(뚛uV躎R|>!appӞm`xxdOi\~C6u]Nq[tz)lݺuY\بR_xw044:DZ}vlٲT BK@ddf>^!ײ穩i::ZZZpS^/ ]2!ԩS}\CC݋-[uuu Ru]i4 JSJ}@`p^(H&8tvڅI\t n݂iRݱ1ꏰyfRRRaaͩFՅV>|/^č7\þ>Gk׮GRcz>>XR(h8bANJFgڵ MMM㳶:r[CREÈH$̐`0455pܹ#1Rӧ½{pɚ|_'?Y@EdU]vahiiA2|>t:=BBH& B6mڄ޽gΜĄTK8q8o߾R"sc]1p2ēO>n477#~պ#N#NL]T&&&PWWW9Dk.… | ro&&''SOjK 3Ga5 Gt:%Q5@l _aX8pp;txT!HR8qzzz ccrT 1u^H!L`ilL;yi"Q.188(hGEgg'<PJ133(Ÿʖ7V@X/`xBI{XXES26Ŧ:SQ̗s<]M(؈vd<(Q|&F2eQ^g92g@XÇ%W*ѣGֆ&Ou7clllުџӸxoՠbb9m7օpx[mk->(Hd2hllDCCԄ'O"Jޓ 'Јϥhh^X4>X "%yf<hooG"p]c&&&Z_oM-ߞS2޼<7/BQv'm8;`@)$,Z[[]uuux'P__z L+LWit,c~'~g !8x vڅvD"5\cccsg N]DaY? j_Wz4gmjP.100 B!ڵ X ON(/' : R3yP(Ç9r[lAgggtDՅ5ݲ|? |ATDA* PRBVM" @e@IfJ& :&K:YiP1Wؾ&։Mhot37<%ؼy3^xʄL&-gEG>O7 rU*~Cd Kd]^n-pi!#Hތތ9 Jq>Oju#b悔]2 !hoo'> ??Wr"_Y&K怦i7k!W?z3Ճmmf'lW-@{0967Vt6XL$@)M!t|8d NcI7WbttLx!?WT0o bjS/ZúbK J)^y)شi:::LWz+w0VEo6qu buQ"#Xf Bi:*ZC+kf e д얨Ot8">נ;(@o.wNngtxLLLRF^Ƙ'KvR lj$>ԂvB\Ɲ;w;e"·|c@t4lvO2( Acp ԩS8L($嬸U%tNz|⩭HCЙak[Q`cPLF@Y"3B80"JXF\Dcbޜo޻ҏKF$&;×wߍ=nMfrrJ"0oߎl6gJ9sL,%EԮKS!0 և/@2AUo( x#n4b{{ДP (8x~ٽeH:?~yG;P/8vލH嚦᭷^ВՔկ} @ ~|AայȈTL^5q_> nw9݂'7! )| ߃Ta"'BA;:]pf'- К%|>|xL)띥R)۷`qժ}}4ׯ 4~m~с%^ s^EߘL~SA*'VڐJ9DϙPN r'@shX{P= 8 >;6KdK|'0 e2'|ҵܹ_xfQfpRx׿3 *23ÈD"\6`㵨K7p\f -q I< (fn$=?!gcį '`SKPo'e@UxlWT^tnQ.1>>.B!twwRx뭷\@K3ºb,+B?W^>X,VJJva+wp1[Susqˉh9q5!.4,bκXˈ $UzD'߂;ۥf+߻Ld20Fٳ]| 8nݺŏk_ gX,\=k H)qWW t}>.kMן;HH~-oK!*oqj,'P5 ڣr]aMP@$bwW2ȗD|kOe5|{7!G"xȈ3mvPkA=k֕LNNrǔ(8pkʏ%t̵qûRY,r<=*.I{n(B=I@uvLf;)Ur  #2=#e䞫.m* D"<._l-gm= kt]*r߉6uRqeL'P >ѥ$8`/ QTy;ǔFEA 3+LԬ)SY]Xub=J) +Юkw֫>=t[:9LCo}8p|_T7!JT ;w%{Er٪UX']CC=ݻ aC3q҇B!޽Px\r7,,WU}=1ّ@4;3acSWQ.]h9j}Gɸ(u}Okɘ,}ϕ0 WO2D{{K D1\~[lP033m`0DQ8USڗ;`ݷ9XX~M0:x=!ҧHqg: .lJuuT!Ŝ!pN.{oC7 p'Q*xAW]H H376GwbC2Qoݺ@KR|RYs}[d4Tl;Sso=lo;;v$Vm=c ?Cd:$&"q.7po F!er~b瀈zlڴɵ2;c]13nO.!l۶ uX}߹2{9~㻚] (mAKCdf=[3dJm nq-Ad$!VsOD*4P4 C9U |~W=g ?#BQBW->:J>X4  /0g{WWQtJeS WΞؽ}}(k&SiM[gH<3*P؜('rFLg"T(ř`!Ә3t{{Wwz8~xW0 d27Rd2[ƍ0,NK%ku̅r,-`&k~akx|gc uWZ㼭)@A`6YTm@,Omx̌lHHĵF  y: tmh BhmmɐDd^!mm:t4%Ykb!<_L=(PAg5jg, р61,aImSpC Fط%+O89YړP4EioX,.S۶mu076y& B3eW_XWB{;]*WbC)7Y 5!d7al.jbѳۄoPGPwծa7 N8 Zo,i:N]=-K\.' X IDAT`hkkC0%OUOP_(JTJ5!ioD^NZ )a{- mC'!2*c؄-Ab.49' 8K^b' \NM0:::z(ل1IIboNp8f@ 6 ߔ;.TK\L9: 9 s\spNV"|h} _ J$F0߽aaxe._,CwwT^G\M֣\0 ={IjWW!Rɥ_?#E%a^~B65Hv4D58#3ۙ8s.p뜽{OB$*.+{ĥZGPÎMr΀woNpR(߆yb-QWR\_[ `jjJJASwfoa;{5О63GfV95Rx4'yۅ&a3/ȌB\ʕ tCklL-ɧe\(T*T*Q@/Jm RKJ:cp}D"p8lN*`aHpoFZ__AKc\A#R' ^H*pta*x;l.+؉HZO;{"(%b݌7 -QoW]ed02^ah?EulT/b1ӀRJIjp)ZpxSJ' ^9o ΩT##RY2ii|EAܷc_BmZ[! pvmnuD, `F(L\`2#aEܱLιP*x<444 Hzdd{q b4J10[!=L̥u.7NC]RŠK}PmI/FzbH/E#3՟&u8xITQ\{L 0:),ٚ gpj5GjKEQ$ =hK%B;Eŗ))@ W(m I= [ I:"0m3&$2Jld>:eZaٹB 0:b9+)TlZ@hl` 6(p{ ǶX5Ma=fiol6tc"أh Fkzv_Ȩ1(`ish <%clp M3(x'0@0̖F́ g/9CeA9`j3·kQ"3ADD[(߁ `װzfo50.Zޟ`2f1js4]ߎ/Yo~vSE3Q^vEǡòRB \0YɓyǢ1Š($k U9\n`rghal[af#9k%fYKęJ)TU5Xq \[?*t,F0ϚA5QJ04%G{ cZ/mya ᥂n˽\%Y |֜!} 걠XX/!{'KD|uJ;^ 2`tn?]ui2Ipa"dj3( B!ĒeEn\ؾQ3GX1^_/]' 7vl͈(/%"8u+:Ҍc 9opu}͂YQ` %`/ `bn`s}e B.4X_&`+o:bv?7"s_H&}[v/J= P|@a tiyXZpQ(WgS,+ip2fPA(Ѱ[YRi-ja"tP,Zc0@]kP~ ~RJAmM1`$"gR)W y:/Μ PfTY̎T&~5Y @ 6E8{bNyq; xASy[S 8!^/&\Nxm'k@~!ׁBKg&%sGk3" UXZ; r8rEշ rK%a1)bV9y8KI@+8WvtPMx,Z-MQՅ1Ab.Q,0` AWD~mho5p9ψl۞3"72ej?@.@ПGQ/Dd]0(4/R5Ų44cA)*#Sh=G f8Ô*Sй5A0,qqpdJ-GPn7i q}ڞ1 vpߎg%UGM @ `hj cGuaX, }fc zG ?-e u/7.8RZ:S"@TKֳu`RMcvzfb*?ZTƝi:5~b =#!if~5UKXT \Ol85@M0uT-Y`Xe^^m(-v (RQ:lC,m*#>?Ph<ɤgRjobdقMqg* as̒o,s>,?i՘:4 ]PeuZ[5MSP(Ul`PkDd Cٵ-hޠ39[PAkLĊHf@սGk%g2?/cb "5|1$q6>bI=<8ݲ {bq~)k":Ma:!,w쟃Z)9EXͻ}sp1Raw!XuB%AjJ }_o@)__ 7#\ /sA9׺`Rsu% "w!D1 iPIpaux5$ΎGwY%8~-mb|j3"<4GDcX6%ˎg ?Ρ-N[;p;WRQƬQm&"ًeBs)#H0 I*1ZT6SÏ,;=<,xR LF ʔZEÃALL m/\0،SI(93$IBM>lKm M0 w\\D1=7n^CT aE?\q `Hl&o3%B=o0"w`_Zq.Ap%ZX3J)~5M^ǀ6N;AOPBݠN3,:.) !< I}ر4޸!l+\kE4((vt2'Dce J ' G2k-3[sj7afv/9I{/LEGvT`d"(߀@*(UUcE(W(ج*v]LJx)f%;g0UhHd?K !ppMMMXdz`w.Z5Aa;۷3BQ#B!ڀO=._7|aavOFf= Mo{NBzN}^\E%_ʞ9w1fOMMI6NӦa!\1UMrTDOO(g<`zE'phk20p֐۳05"$T$W*&7mR0_ືy*gU'S+O;.1) D~]ci1*b}5/Y,^VHfݻwciI?!!=6 4O z 0 g;Ue[(|e6H CZ&,iw 8IB~(;\ Ѩ[h}TWQ |.$`φHRIq"߿-g?` pih1Z5wOC ysUSYhN(AgDy,E9W 1l6 ?([ĉ\0{v)Eo:8SR>%+bl=#U1xYrիN QJQWWCIo޼;wR|>DŽFK$Ҁ Se[b(i]('s0c(5]iWزAEP@^aXM+ vsfH)Ĵ`6C,\Lt c07~+]] Wu,R̪“u,چ"65(,QBMzaL1 T t`bbMMMd2H$ppXjW>ډ7.`Z)޽2,v'DVٳyI[L KBJkjCj-õ!?.%bZjH@0S"~F`xfMbSJ=͛6o{]HX3w'WsIb^J{g+W5 r`(7ozr8Љ-ň;n"e[g6:oN=X۝B0"SGp֌?/RX& x@Iq^Gw\KRgh\Mgg sb)2\swsE`@A0O1`0fܹfG"/ك-gQa9w1g-mhkY9]k)/x $w68C"vi fA-A9a6KȪ(>۷o~;vH26jt^c/|QͥKpIJ%ES`T?{k|Knj^c):ˠEq̶&N B! yɧ@!܇nk n渳>^ @Hpv@ pWً<َO$J)KBFGG022qd|:h |9XTN@bfIy>oI"@cc#&''CSSSTPuuu0,B IDATdǶ{>^ES¥[Tds1qSC z{{1p<+dgߘ;bݟqb_o ߓخ(~lBIetǸ{.@ СC tvYq,c"^ANenT*\xuf%.Jr"K2 |[BdpUw\upLeqKXv%x-/2u?Tj.f$oa4W(0 ;6^:\R}JP jծv<)ٟڢZ\L&șyII{QU᠂+1 M.<(z>؛مSLRĞJ6! D9[~3?d 5" [#=~c)Kh4=N vK( 9b {a6?J2MhvX,|>?Y(&*-M)-PJ԰ؾ};g̙3`cxD¥JW>)J1SKD Cb6lVqc++',@P` F,ԑ`:&sM3 Q/CvUUESSD,rO̸X.?YK@`a(J7+ Mn {\v,ˎe2J2 oU@2Dkk+gss3ٵPwNnd#eg,ML&;oLJſ|yЦ=p0FmyBF@ I 9._|J~/x|LZ4t!dzE(%EQ hu]'ak8 gÁf ^ضms&pYlٲ`###LiPE!7+śC>wr7S+=Vc&&UYGbchTjwH:$F*!& _؍/ y[kxk]?,x=-YN~{1(KXxefQ#C2 WeB!R)oR'k,|޵;j~. "0dN( =ֳGGGU⊨z]z cjjJ\/^֭[ɧE& M${!nw 5|N@P]vD7@WB`+hG) k8#d/4psMT_zf3Nonbll߸qCrLWBVg1YfP8C㮊+9}gϞ [ mTu\_x :_:::G\+vw%Ѱ'rwEdˎ}, 5‡lI+^`/ 6Czv4Mh%,(c7G/;ߌ_=*kciܸqNd2m?6"?BA`%ün^(_D!WULMMyn ̙3xgP.1::*eQxwݕ~]Cho~xnlHJ{m [RF{nR8 Y*?S,UYA%J{3b8{3yk/kmmue]d2ܔ2 ϟw1@ -[ݘS-*5drV/p'WbyJRT\F=-R֦T@zfkիW(|y0ى={`ӦMۯ:0t]G6E(@Lmqoş|:H 5;؅J[枭N~nn!JDl f"} gK}~drNEwg?Ym>*b]6ز>j[ 0Wk_`8}tsǎX,$Z%| T*.O;vlJ0d ى!IY:?7ڡxCg@Gs䠣B-,՟܁oG δc~ad#z:1@VRK:׋o!vΔ\ss+coR0gt!O=v) z%xf(Bq0/3g(߽{7}YףP(̚LDQb1ύ)0:::\1=MFW>ě{=c[/af !>QP"D) C7G}'w?lO)0gBΝs]]]سg-];j-MM,u:NB, œ14MbRrh.M0004!E[pgua說 ___m{ @;wXB[nڵk;\E %|c]xb{]<Z 8߇Պx$(&5s6bPA{ c}`00ޅ[q1 <ň_\K O?4~9 z)۲ p0aosTbBbRfwEPMP4 # H)]R;oCQ<ضmT*&''1000 188+WĉhmmL8Ȉk:0cccmg ^~o}!m!W(_}ؽ9'wa[W#=o֪Cb.1 AiUM2e|psbuS_>Q{{kzԋ]Alݺ?b>ʵ oCܞ]fKpu0 0h2o}k͛S^uݻWd||L U , 7 K,$ !<ҧRwѣGy:1P(AtvvJ^Q!RkOwt/ߙrE:ŵvuvv7`[w#vv7 62;83L `gzI~8I0[-1|FX mmmiU/|OOx≚;MJK/o(ok< Sǎ==Nm;wnΝjCjz:TǛ7(˿K?ePDCCHB$cjj=}EKDF]D0::r0v7G{_؃ۃY|t>u-q(.!Mq6E[C ͍Q!C  *rcƦ al,c>> 7˱ 1%xE{2oh… T 'N d% z1mQjTS(q㦛W=XJJOt Jug`6躎~x7xY$A$w"N$^|7oÇ%i P.188&!D"lڴ vygg0ߞQ-PJ18ueeU P,(j:ߨs/=юhqYZ,%o?ܿ/^ty({9^kI/TSj`|| n\ÇW̹s1!ʂlK~vg~\g*0+ ̙3,c׮]hkkQ|"ߺu \*;뺎>+! <4 (hiiq &qOmc=ΖLϞ܉BB7_DC*kۤħN(T*UU[K޽{<뒈P({NڍQݟ4*]ΪT4%P DYY(p(H$p8b4*jR%JEdsYLOMc|rd +/y{cǎ%$( =B)=Ɏ;;$w((J?c?%Il߾]]]hiip֭[J}JtwwKaCCCd2h)6d055ŷ&P¯=݅_9ډ;Y|pwO}itu H@$HҐ)ZlCIʲc˙k$Ess3ɉx<8u2% JcRIQ(N$Vb[uUx >]][WFTaDIMwa݅R]| _jE(aۘhy^^|> 5 8Ba}6SUqn1<DsB\k(_D].l:wrQsssv } F7,?V&bUy(9Sa͖$ra 1|m= PU[*y^;v ==ͨkbڵSȧ&ww7pBҫ$A$LMMi n|߿ީ)u]:0C^fX9xpc1ܸز2f a@XL)\NN>ZS`GAN"P'|w`)fTeOKdfmV :11a;zQŅs2#$I( ~쿚=CE@fAeW^޽{c0\58CAA  z1??TYʡ|M$N'󑓓^5&''u9i\pAWDA--- ٤͠Y;5UdAawiw)Y>L(\D X%Q19L)eCwD_5==+**7*uEQDWW$IJq뫍D PJ]Z !>'O}݇;vhssspݚIHp8p8Xf a `2^3o 0==禔=n{I*ᗮqcW;t8c*F@eD"HIzՇB |եVŪѕ$ B$ QM!( X TR?`D"xE]֓A׫kjjrبhͱ 4gC[2LMM)ҥK={!Ģ(b||pݺrY6U3dL-7#'ZJŢL~t:p8zzAfM,^/.]Ň=Ƹ,Gw_"7'v{jRo:A#]emO?= 2_&6&,^{_Ezl6lݺU̳P(nH:쁖(D"Uכ'pIɜ [ny%!h&LDfJ)""ɮ/,& ~?^!)FWWfߟ,:h UJju(VX^ܢ(.Dznx?M颖 +b yBH.Ձ3* Ujw8rs=Zغu+v;Yʤ&-_<+vzH$/>fJkA099) ??_WMhB(d(b~~sssM1|駆zh.aݨm=ݓ)6"43Ht?h eĊ]<D ۫x4yfBͦ<| -lFF`ݺu8AիW!Iƃl6<)AUUxM J:4XS055vLNm555صkbЖj #  #?ɐ? ~nV, xܹOzh_^QgٿհFp{VUC~χk׮b$SuFayy9t¡@ΝӅysկpu%tCCCASSR A&9r4,lqvvybp8k.,7z7jJ>acw}KE+b~j}{~?6cE{~b?82b>ToٲE)e䟛C?VkR}VkdEErrr4gmN~tه{9|GxuZ\~7oDee%nݪĨ&`Dz_}/Y9=2/ ` >}}}omm-vie8*3^ͅ@2$ p@וKVVMR_|*O8!p8ظq&oX0>>!]occ!횮`0vPJqaqb ~^f կbbbϟGwwa>44p"bڵ@& Ifgg1;;ILLL$ V˒ J15ɆFBH_IFXU凈D6mڄ444np>˄TWW+"(c ׸ʉ}?0JݻqE>t ꕽ^2k1Sjv(kƄap8 cӦMQѺm,)u%, jZ_x=CHcrco߾SJy6l@^^6l@K&߬-[,TWW+Պs!("wA}MQ][QQ#(Bn5XeԔNjlPPP:iOF I#!$_pE= ހƊX|>O(ϰuN555pJ& &&&nodV+t|hooxwNLߑu%;,o؈ƅ:100b0S0MIDAT(..Fee%*++Q\}TY%1Hj#HǑ DCŁx _crrrPQQTWWX,g87n% }]l6TWW+F9tttyۚpya ?qƓɢ;wΝ; 0<_EvTUU_$̠ V%YƻXFjjkk((cnnN D"AP mp8 vvJ$r#}>@G9)&D6O?avR+d<u(..FMM ʔkowkGsáԨitwwbhUQ[[ 3$ߥ6{vE<₷#X]2S$};\Yzmn oB%1UnxP(t غ磾^CM^~n##rE5'&& BΝ;gtaBS'62ДqrOOP$j RΥ&c OmbjϏYisoԝ'Of4Ffd t%I2 xcXg-`޽5ǽ n [РtYJ%E%%%(..ք,ڵkPJ;m6۞^{mx)mBton5=[HY{?1Ka~p477?B \ 6lBmá!0T$KT/IJKKvu>===G__ƌ.#_}%ڪn%GxZ1;#`@4EQD 7kU#ig 7{R@744b pHs? 0uo~g\Ȩhnn~Ry9CCCV+^emtN a J/m4Buadl? _V&z,˰X,@^Fuj#@)EUUN36 otZ/ܹǏxX,݄S/3#C*!FRzoo޶7'?UH52O I}}џUZ),>+W͛I苗gp8B!f̙3a|,NLx͐ȓLl1ڶaBꞵj(d>tǒ ( DO?x@7ljjR$ s*x,GǙ3g E)Q[[ܻۡ$.a]vѷC~e=;l-ѱoTARP9:DB! mXoЌe]o$"`tbļx#W#<B>Bo߾}x'ϯF8ִ]J{EK~%?{eS{˅*M1,G1??WK)?}r%bYdڐrc׭[n&MJ; Y{_e͛3$?q&Y|֐waoPXXnRtyyy EQD^^***2###ĭ[ataYԩS|!Ydq!e@)0ܨ-]V0<<>C҇O>ݕEds.K`m|H/V$Et*>k022;R)x ,)G6-@*333퉇j*Œ^q%Y 'JƍL+U8_^G>A|7:bkkD_Ew8Қftstny^F)ޓ'OΦsYdq#-@)Ufͼx2m5jH\N<):ӧU]Yd&ҍNђ{O0p\}$IBww7̙3 g`Z[[3$Ew&2^$1>>p:p8:8͆\EA$tvvjⷿa91ܹ2)Ew*KI_H$)lݺUѭV v8eRttt`ffO6+(zߝ={l"Do..ƍ gNP|.__=t7,#m۶m{WcAƍECCC(ĉxtEXtSoftwarewww.inkscape.org<tEXtAuthorJakub Steiner/"tEXtSourcehttp://jimmac.musichall.cz/i IDATx[p\wbw A]Ż$J$5Fx<ލU9rRuRTgZɓ-?m%^oMegFG3f4 G#Q"9")R;_9yDP"}|?U(@l>?6(G` W_n,㱺 ÌYt. èf5ӒΞ=t&6 lz@6 O@ 4.ֹiEs;,d :q#*g?;r: Sa'},dvɶ|^h_?3 u;,,wީ:u::ujm0YbƀiV=nݺe:Ғ9sf@WlZahG&}JZn3g|Jtc>Ĩ5>>9tVfi9yvs(-U9m{q:zxZTK&h[ƦSNu:ki̙3Nؘ1hA-Sfa ڜax699)ltz0M/ZNK'N蒌Asԃei2$)l,w@=cZ\_g}V3أY`l@Keuz- \-jyyy᝞H?iK Îz^(XK[5xbu OԨa@ Ν7E"߮*fiٽ{/j=0aXZ#By\=3=RkLVƵ_WƞЮ(T*ڵs綖jT.rxPHR:Kض5H;Op/{n@۶u-;Ţq 콖'O>W; ~7{2-a/$<QlP&ћoXm_|៼k. H&?it@Q۷fgΜ 9\`۶>cl2qS`JSN0~3`P'ǏGNhԽ ӣ:T,U,јm_Vu%Md' i}Z:$}:v$~@X֞={j:~]ڲeKS}Q2?jzz , p dYrMb1=zT)Ӭmк"G?Ӛ7 +VHF lۮmFxeY---5ڱc\0 o z^$:zȱc,G"1KLӠxLbcѨ~<844?22?22R3̣ضoڴiaG Т cu Ix4N PۜٳGM(p \(p \(p \(p \(pZ,R>W.SmNG_azz?~f{Nz@3Ų|%ϗb/5/rܹ;wn+L2NEXmg k5 C^[]Q&SNd%]5 gN>.ԇ~ii ÈIzN>wԉeð>{%Q@CQPZ˗'e5Xa={O8 ͷpLЩe2~ŋbl/Ͽ::,//7t:t,?;y%R~7圎U N~APiRw@{mgΜt$.hi)t fR1^}NgNAfgggלnnz)(:'\t:@ 8vԩ9PtD"icl϶]rYm;`C ~̙3>s@Pmkj160_3$m(:T&ݻN1 h~% P(#G5 Î9Q7 Αu(:Ta:xP|xXLHh1GOҕ+j8?~J݃KPtZFx=D8nڹkF3x<}jur4k@5U][ 's*K5dFSilڹ@7o޲w+c(wuuݮ`4âqevMDZt& \(p \(p \(p \(p \(p \(p \(p \(p \(pv'hrr:Fz嗝@Q`Y,r:p .(p \(p \(p \(p 4tp- \(p \(p \(p \(p \(p \|NѣGucQd`ݸ(p \ ԉiJ&JJgZ^^VRv:\.=^>s,-[dS^[y//rNB ,K'u%E#@+&i[@ǟ:ڐ!I!KR0+?j~~^ WTU{zW+xJ!˿_[eO|`hMNw李"fo>__kӡZiW?hQvmbt x JEw NGxg<>?mu:Z|bn޼t_1=g:m߿ĸQjc??v:G_w۷._7N(t zxOCY˲{]s@gm_qgw5\PO1O! ۶{8^q:A_ru:+f;Π/vƐ:Π/uƖ{h> xₖSNh$o t4<1#4-%3(tg[#NG@$uБ=p[ojzz.1T'B[ٺ<ϓO׳ǟնm|:uVW>,47\DB|!_z{z\D?}ݸy]Baf% {lv:@]/:.@ PlC*R>_MКΝ;t:([Kp \(p \(p \9ժՊl`#~ huBAe9l@WW p:\Kp \(p \(p \(p \(p \(p \(pv#4=v@CPcرhW"t (p \(p \(p \(p \(p h v:p40G+bÃV@=6Y h'Pfn(`G.K(+0NA?:  d2N@E#?:1@|X:/44bжrr1Xt0NGPp-f&\Y Wanq:4 +Kv:}:oP8QF#0|5 mYQh[@[bX mY6bԎKҘ@b -Y(fƠc#/1 cc c1cs=WϷ^ @JZ\\T2T"T2T&Y}>L_x,Sժ)2UTWh3G+Yi~~ABX*_B^ SjR1taG,//knnN33THe|>|,XY!PVTPXfNKK˚̌睎x z %_%PTV jpBY_BA333䤲٬ӑa((H`ZQ\gv@ ۶5?qMLL*H8 x_!`*2w\YPJIMLLhbbRbHp;2 eۖ**,r:aP֥T*ibbBZp<0<_\ZT:T; 6mZil21ZNXؘjzz]U*Q Q022cZiҍ7u]2 * K]]]hxyFh},KM֭{w Ðgur)˲Vwe|A|A ںu٭NG%|d=cFP*ҭ[t]U(R8V0T(R(ZV~eFsTUUUU*—/|\,V*:~G}45>>qڵK{ݳRwFhRJEccc뚝u:Ά|>EQb1aEQE"Տ<{'ϧP(Xo۶ [.SPP6U>op*˺v횮]~۷O{V0t::|d=cuE.Jt ]vMR8ݭx<.EQqE"u?a|z|xdC\+,zh޲=LӔm۫fY+ G"Gժr2[:V&iDB>Cm߾]?>,@0_QmۚW455t۫.g%^W }>Rm{ XdR~SwwpeYd2JRoKKKSLԝ;wt h'g6Yg a X,kzj/BU__zzzXW_l׮V6 |k7dzf1d%%IR~:w#۷OO?}@XX@G`ֿh( @tZW\g]oٝѨ߯t@ W7 m=߈#>a>|H[5Y@P@ŋ5;;tU~ui&Eѯ@ zH$ڙFzbKmV\^@XB`޽,KDB󚛛4;;Yرڹs'E|d=cMGmlbbR/^ܼQ$ݟ߼y500x3 cuuEOOu IDATϚuJ%iffF-Ғzpႎ=](1zAmhjjJ}>h_9DV [Z@XTPXt  jttT,K V裏ta۷wp=7/gP@[{ΟD"X0߯<|a( )*<֎h-a(+K@>W.S>Yȑ#J&ս{NLFY]tYG֞={(:|d=c6011?>E2k۶my~a(((69%E`eu@.S6iwu8p&''555٠R}=?AԓOǿQt>7Kx;wV2tx\[nնmyu׻:Z~xpuJ2*jpa-,,hbbB-qGl6.]g}Vwbt$fk>1B-h~~^Ǜ~n߯[jttTk~aF w@oo22L\L4Mjll+Eo+WOhNGYw P@ ?3.]$hǎ}P(xy-//P([GSO=wjlḻ>C]|E=H0_ ht:zKnj|>mۦ]vk Q$iJ&`#*R鴂ۧ{jvvVovl\.~G׮}_<h|d=cmĶm]pAgϞU\n"vޭ۷?rWW\meYV,Ki~eYmIa2 C Ð77Poo2Rժ6oެ͛7kiiInҽ{A$?|]ұc\uBk`_% h9?~H$={'Xs횝mVZUZURY}3MSjUi:2|r|UOOJT,۫g}Vuݦ,ŋ֝;c:}FFFz~@rQ@e>XFڽ{vر> cW*eU*/ jK ΃ETZs\ <)abb*JZZZR.S$ѣGw^ݾ}[wܑeYM͖J={vW8nN|d=cm~t:D"۷OO<Ěp8ގضRRrRJAV B?7 C~_`P@@`P`WzA \.kiiIlVhTݻu-4ͦy&'ĉo߾YDux;͛ =O4޽{;~_.U,U*T,UTjFlV\~hR  R hhhHE@$ѡCgݼyE@XԻᄃo_TooO΍ǬG3Q(NϾ :ڿFFFąB!RP(5{V\f%   ڪX)L&p8Ci޽~ƚ*dvvV9#GI 6Yw P@GPT>_vڿv>BehXTPPZu:Rdzm{`Z mW~ gB:|٣WjbbiyL5>>_~ 5Y@Ǣ b 0y<ܹS_v}PHm3,K|^\aYC@(R4U4mD"Y3<]vҥKJ$M˳u1>|-fk:1F52 C`Daܹ󩯯Oxa4W|k[JI絰@ X,H$`q7kxxXbQDb/tedVM;fffKػo>z\jzoo:xLonb-9_<ҫ(<|d=cCŋ599t 4P2|cWpL^O>OӞ={\U500@ PsoeYfd2iJJ|bZs q---)N+СCڱc.\дܹ 7hM k>1Wr{p1ȑ#k^400rm[|^Lk!IVZ^^Bb]&^^W ^z%MLLҥK* ϑd?Ç3ϴq(+p(*3PHO?#ubQLFlXG{)*Z\\\DU6 ڲe2L訆駟jlll֯fgo2 Yg ГUP#4r:p8YFm۶rRK.m+*󩫫K]]]-E<W4U2T*R ѣGm6]xQLfggW^͛~>8ͳoEXLGCy^mVNR2M8hsjUdRKKKbq|O ǣb1-,,\.k``@7u ]~+]~7sСC =Yg 0G+/ # 껣޽{o߾5oRT*l6˵;۶dd ݭh4.PH[nU*R2WW,,,4e>̬^~Yw >WVeȫ`uqbt|? +{onhT * b:}tUՆf׏~wz{{z.45YaVG e~ah޽zꩧ\l::i۶鴖>R(HhiiIvl5ז-[NH$dYvܩ!} e^ou)۷B1_Qh\/HhqqF3Ϩ~6mڤPeYd2 R,ҒRz{{+D4??Bh4o|qㆮ^cLԻᄃC醜hQw~'hb6_-:Ek׿s^'7 P(hjj.ϧǏkddǼ^X>Bo1]Ƿ:wcIdZR ,^G߾uW7>=W"NǫY>ĄӔsFQmݺU*ˊ:s.\{5IolF;˔V1P\Nu>ɓk.BjʌtfEڢHjl./-ζYݜ߿;ސӫgKsoWm+H ?Ȉfsϩa 4 CoƻlFOvAZu nC#sק^xaAB4PSng۶鴒d[n/FR~w%.ƹqnF#azj@gjSwT*t=uuuvx< )(LJvڥP(?n)=ȒX\:Xjܭ;ߓj/d4==]3<#к500P|JK,֕޹'UpMJ~1#1tpPP8>iymڴ)iaaAmkddDpXկ-hTeb ת3^8p3 mmyyYsssuyGmgU|L&Ŷ :Rvbٿ3vW'יzz[MX ajUVWq~4M̙3zdB/QU@ 9pD%zw ?p_d5,..*HlyN8n@jd^paNi6.7Lt}o2jQ.u޽)`esYE>}Z.\DCI8+٬`qFC0EhwkvD]_{T穇yY"kʦ~9ɼ~[yfXw+/ Ȼ|x^mٲEfxMA$ ەt:[4% o][MI@#pp x9%'ρ |h73J%  !}F!Bհ988Ӊl$Bbq-m>|n`:F.C25% 6T =Ο?oK]w<^8D ;owa43a񬭭affƲ>|kH$X,fyz  :D xy<}~;Cxt{tyxD+b>[rnV2|gw̅I)TGJFe8@۹ր Y_ɑ.Χ/f_~3 LNN۶ۍ^LMMT*'GI^ !HF:Fag43aQyLNNZV;aqJl-iߞ/L"\e/b@ C$3vxSI` n۫[+v䶄oXƥDQ̌5D\.099Rǃo&u#`U:4)f7) `t bеÇud2i򿺺Zϻxm m,uWO9u0ׅ.}S3t8sp̅͒[w҅ J+gI|,..P( Zt*. O<Μ9Uc$7VW=La4>s 0$IԔ:D 9>ϒz,,,P_ei-or-{*a/q`8\F a˅.J[".q']er6W_^7)!NP( LRtt:̙3X[[:&G^oqן30`zzrq GQEh5}Q^x{s>Tsa__G30!Gqb(,;f w>gv}f։fD*Ayr5DQO<7xLXCzz6jL7ιi`30Mqm[?ffgg-{FFFpAD"a_,133\TϾ1-ܞ 1<zpm9|.K0Q<--C,J^<_Z?0Non|<@*.v#Jazz(Twu8b /k&'ILg0&8v;i(~m\rŒ{ȑ#sxPȒ|=: GoMuDup86{e yX/pks9Lo7*`![?!ʷQJ&''mi Lbzz$UklnnR˅3>aPa4`30_$cpp=\,C8$_l6W|{m/q@_wG'/¼C5E`zwrZ@;mPoXƇ_?=ݶyH$ [ ^d333$ @O<Ξ=KNj(!joq`f`0marr?<$ p bh4j\Յw(E|)mᡱbأzR>>lDX{ ds%nE_ڇ@v(||>`vv$! x7-SUϏ@V`ʿYvƍc?Z`<쳖B$~aDeO.:ڪߞO~|vh FX?Tҍ.\].V1\[>i_mEh4J]G"w_@35IJ0*R=L73θqLg030O[ՅSN@ X,fea~~mʿ$4]K홃$&r2şAF9p0.Nob9zCJxp8Suyy("S Q,t)?wUs ~{uzeg83 FKy U3~>}U>=]]]‚-0ß>wKe|ۉ{vt9t:r;t:t9r;BYit8 |u7$QBI!$$B$"$ID>WD.G>WD>W@>_@~|vá\'`$,frXl}34Wc?w(H$DP,Nzzzpq;T<k+L7θiLg030qyܸqt˅{D2) ̇ n!kp|}@x.JrP(wٖv9p`&wCා   8 e 1N~IRӼpŖ 59NGȇ@ȧ-62f6@zumonr7` WF Y__(,155\.8t,ݻGun7R&q`?mb4>οGgt`0lgyy=i8qyÁ :Q3p " "Y/UmSNn6< xOXj (zk%rD y\"€n __h_S*;6F V^|EKFFF022B<Mcyy-a<{q}-\9$x C-(NA6o"N>6: u έ^R9K&P%籴z)0007({o>[K@&rL&1== Ip8c^C6Ko I ~?/)RȿwG23{ F}`[oeU2\$A86-[Ke=[u\:ÃcVD{p)fHd ,#Uq6լB@@&]A!g I  YZ Md[_o_8=C3 qV֌[ 7kcHEhd0??O}׋D"Q xrE}F`0|>_hD"xq%? Tgv6ļJS8Ntʏ`(4~򓟘^`z<<#ĪN===Vamm 6۫glpO؋$fya>Cnzy:HJF&4"EeF:d=AAC=1\G/ZÇ17߾iȕO_o?9kkkyxX,|> Ȫ?:ιsFH$60Vv B'2$`PEEkQ^<<ϯ+++y]]xGOO&'''NiztΝ'?5 Smnp*0埱i`(g޶T8/OO$1 (Ĵ h?oۉx/ {Nq|_1Vƿ7`*z/wwnLSݲb@ 'OR݂|uF'JS8)0+b:rs8ԋJ%LOOTj] o붎Á^#: 'Gbxd,dS*MH͚P˪׀'*T-(:nj w[q?Na0HC/=>N셃A<~|K77U$ 333eb1xT*qcyg?kn6{ļ30 ˼{z|>q$0`ʢR ܚ_C,ͯOѣ-Wغ.ѱ  W@lk`tr_zQF < wfDGz[_@*N7EHfAU~ i r82PHR\ 2ہ{ }`XbyyD"mZ69lnnRY!ֲV.oq}![Qx% j?+ Fh@@d7ԸNG܏cCQ;OɃ}Ϳ;gj"fffzA@2ޫ!6 R._)2[n0埱saaRǦCT122B< /FnoK7{߿l޾TC{G7 ZN#c !/`FЌa@+h*Fڵ: }౧;cCQCUs85я~r>/B䈢MEx傥w+q݈FNqoJ[lX?c 4gΜܜ9O-t:KXYY*,~p }9_AM6KĞ.O pR1@Vު9ʩj\dD;;t!|㳇5 ʑ$?~϶1lll`aHD~ pQcd292ymnp*0ş`a۷o6շn*Azd2o]'?%ѾĞT D!G9Oԡ )ԡ1@c3-{+FEɧҎ(=ہaK顅}.|3q@zmjfggdN'NA\T Y ໯ݵM~,7>w=ƍ-o?q!@C5DSoB٩`֠N7W3 @-@C݁607>)~|0?5v7c"*a|||µk JS800LPK/d$ է[nW& ?}A?-IDnO!*u ʴ Vs `t~l]AB:bT_A}.[͙x-Q- )Y}}}ػw/5pylnnRImg0? FC.\ES}>[?Ponn\<=><} |'~G07A]ǁƻ7HW(pNfM/`F% iTuuCȯް[%k9X\*{^4cl9R $A9rzօ #IvG`i__㭷2շ #s:25kkk-)wo~U ύ$cƍ-'c97uuc!@3[#< u z84Aυ} uْ˷q IDATr9OBw88q5p ܽ{Ly׿sa(xLm5?מBdxw{=X8=Y5'+͡ vj@}˸6h 0M0nmV"4G9 *Q8paKR}cz'!ڬ,..Rp *=zZ*͛3Ng0 C$IxWM 's8bV`~~Z;{@]`*/?9M۠yK V.ι:_#heoUCXh3HFNl!E/^4= p\9| H$3tww߁~cc[2 ^Ά) ]X `*СCs0??okF(~x t+uǻOۗ?CQ64^ʿNiI֮Qj&`*Z]7LkSz\CX_^=;F8S'bd^Pa䏟5ۋYXXUjWWJ.^|mǔ6Ϡ`0 尼lĄbA('S~uu-۸1NUfWȋ0<.{556[#?F  yQ @9Y|3F: S1ne8N>zۘJk(Ks("xܻw/]ݻj`͹@q 3d0h`~,Jjb%*x,^}no$n>=0zcFFe#JqWzsg0mvh7 kmy# "mxz?;:wTNf ]8ѣGȮpi;cMȼ3;``0VY[[30lYo~~Z+kk˟ަ*u>;qĞ.IȔ n3p]G7: }C@LdYH4SۇqgAvq(!u&x_|l=Q:uM*|i?lT377G{TK45!N>0zxkYϠ`0l](3B$By<fIؠ"z(wpl(~^V6_{eE۫ch ӻFρM u58'S HA,AG8@wm\$f$F(Rf˗/0N mnA f`0v9+++(Ms\8xyZJUK?zcE8_xlRqÉoFQX+P}o7`"wVhnnn*|X|4(fL !/]?\ #>PPXohϦM U:IS'l[/%HgL`ʆME+&DVR4K:JfjY5_m_y=F{X|aU6W~a<5lL~- Ѩ122k׮Y vYk3şR]‚)<rViqqJ*ΫwyG}p9OR@3ֽ_zozc"A>MᛥS۵ 5l?۳E {}Pؗ t#@vkmY7T_;S 5`2zםN'߯{^] ,LFEI$?:rz^|^ -vi#+#V!PX Tq?w*EZZɯCql/|g[w98Lhs7,)XYY"KF\.8\Ͱەݜ ?Ά])oO=O#kIhY}բZ 7b+ǃn1 +,̅`MAvo)FڜlOc'`^f|m 2zөK8uvϼ F=Elnnbm==FFFtW$;?\Vh!U/~Ǭs o_9thPFwGPvΑv -n' ]T.y߿W.R'U ƨfinWLg0 .z[y^ݺPh)D|)ev9yS/u 87oBE;r@7ET3l%h~Q@;+]rW>'Az[w_4ԬSIr8BcCCCT VZzvX?af`0v dػwyZv\Rqn'])87bK1Nzkhȇ -m%ビu6WŸt,,Vnןhݵd0v1KKH8p@v7آfF35EMH?ݼb sv2LexPMbq84ॖ΍e`0| + hY׷0?af`0v|T(H$beZU'!J=%_H j v7n\0NV0:@$HnG3G,łd Y^^6MH$H8,h0?af`0vfC5~8N˅666ٳ?:NtDȍډX;8tZ}ѥ˝[;6 %u[W,Gz=xDg+RS*jY2D,,B;׷0?af`0v8Rɔ TJPNQ`yyi9F\E+v@Qak&WO,4fnb_gke '!ies=eٳm (p8I!t:pnGg0h giiB^/LL<֢$Dh} q7aÑ=a8xʚƺƐ_簎UoN-Y5+85`Q)rZ~9O}t -nvx8`h/4d6o*",cxD² ^ok"Nן 30;QMmDӣ{^œ<>l>ꁄcނ0|QOyhCRX]0ݾNwejNT=e"*Z;PQ{Iےl$S N; 6>N{s!^^="K*EƅH$I%,ݦLk7`ІbNcccn7<=ש,.lJߣ"9LS ٭0![jÜG*)#Lno~| 3 cu@UzVr݊㴆dRꃰfwzKFZ oYrhiXOO"v¼ ]0C1=_oٹoR)80MpUEJPr+hW& fӐPx.ܻkHOS5 hp Rfp9IAI)PAڬSbv)p泥D zYjP(X>vnןf`0v(tڔ}ttTg}ѺRIm Y YA=txB=FX4H|i(^"%*WOn7};꽬_ɨs3Tuԅ';FұQd7NuޔB'pغI1)yqL@^Ъ<@ONmU%IM߯4|i"RPŔvÔnء x<=O#_E*[LAIכppObd>~98M H6Po=_V:,娜e5:=oPgK<` "H2!a8>/%p >]X. T8-˭x5;4֌>.ښȈn1%A43Q^JkK|NZP҃n"Fo\1Arl~%0 @Te4o/Sm\#VjMP%R~~}$Nշ2ֆ (դKu?'nBڄ"oP^Cr]*Iev-}t.)*Шv*uϞ=xx9mNS 3zӉ!<Rq@IF:|Aw^KEV#`("c79n@Y-z^ޤmH*S; ŘגBI(NI^ʶz}9Iv쌼A? SnPVƔߤ\\R:q6Ͼ5M*䬯S. 3F>Ft<[լ$E/r?CXhq6ߥsU(&`Lo<]7H8 ūq.J=<\.cNw'` f`0vfB5A}t~*vx [t^ QPn(^]ꯌwe?Z\u`6Xs+mԆuvS%{z*\ҙZFQ*-R-}@~IVۭ~ sE: G=^vrpI xuOS%gmm֭pXp`xxز R ׷0?nAhJnIRs&xTs/X)s;0/FaF5}=ݻvK v[k9gJڭhTNg"O:^,꿮AŰj=#N e[w?Y#AS9RwIUudAB>Ǣ7B$_$Tȑ$J@ )WL\.]YLk7`t` ̆tT3i~,YG+goao=Pld Eh {C 30;l6\.tz 4]U`}hYGP⠊!`/ܸ+:T41@+K}Jml´sMEUr/rU5J!=u ,"S,*)')kD(rkoV(| y5)Ns\=k֢V IDATyV !(OGpM ݹ \b;R(\.oK8F4$qpLk+ FgCf`0vf*4{^$I4r9SFt[#}aD }p;_Ff({S;J1 p*-52I&zM5TT)7L[)͹+3bQ1j+[2w*sLѤ <~FKaP=0b`SఏVs>J"]-T*Q1꒢h4kfР7S0( U~iuC!qxŒjt}S4ŏގ1TZ7bjЀ_H O#GʅIuFS+y-ՒPY}-@9Z9STOk08NHQyɮQR "۔EHm޸!xKx9 3RRCYn7g|`4Lw^^ ףX,"65/#|TOrҩ< Uc:2͠'(zm)(RS uVrڹ:&'Q-u\ z~O~S{_ /P& ̎]g-Z@mUo!BV⍵WgS9}ts.W!SI6Az*\m/7ΐU1RGmJ>V>ryN{$ic }f2q\uIZWӜۂHR;j3יՄ}'fwy.+z)ͬL:$# aeeE ?\g(.d,\hD{c-&%t&`t0G#G= w~t!sr$Id2dx*r>~ Cђ1YӖpPaUF@aT/Si\m_/]t*zJPoj_oBؕ:諜Ԋ*2=~zj&UR 9'?Mzv+=(ZE"r<[)Zw㷦I?766P(,m+|>oC29iYS-&ecG5' "SAr}jݷLE0@Ppx8BЏl[>m6>/iI&;y|'q.%d| k8)URI#$M݂ÖS4&"ԆH";P3XT^=rcan|'+^~-i@}u4`c$f`01Bׯ_o_^N>  E% uF;hcI#@D8RU"-{dJ?AU)BȔ+S]RY5{mmUWƘ Uit{Tj Uf녺xy#"s>͒[-F@GvT[/𣷦@yCAECzTI& Ќ"x-wMܚ5H{шm_ }\~|>nwu1LA'O>oz^\:T32G28YȾx^\#ʠPzxFApbUԍ $ү7y-y^}U)*$yZlLgIQ'{kzFȯSQsVh>?Im\( Jݗ4Q2\hZSiJgWrpeŒjh8@Ri8nF3wѽ=b]ζ703u Z-Ss,rfӥTnt+#7+N/Ȉ@P9Tq~B+oʭ#~ҹ/rF!HS9|²K U$L7AԛPm Zre 46 N YVBf߉gP}9Nj5N=n֣~xajY)lϞ= 63XN+Fw G F30Md2{nyixJDQB:sW-90܍hغCΑdn\Ӷa*ιFF]yTW!N-uqcjNKVR@inuYW:9IZ+mQ6VWUl/gyBz }^P.4*T*WɗkR=ԊbZƚyf+ir6TWQL?*US;@5Iv}[Eo_5ؐns/[kX&k^c簷wGҸ6FaF52 Jz$Ij{K-`_o2؆,,,`n-=<6bg( (׾ r%>J3*ݥWLr/]GU#rC?OUV"HJ = ^s5^q0~sڴzrjz|ur ,ՇTS~UU '= ~֗3 jw]WGpr&rSzGoE)z- X.iFVlR T(dճ.I)ϕW2Xɯ*kP)ry5kjm-bU! /thjȬ&N~rb"k~P2!RR9@,-܁p0p VEJgnV e! A8H$I2ehO@+ Xݰe'SQh``E01${$ O+՜rEՀ-EU8Mx>ɹ+Cc\F" $?6DdT!+V9ݨBjon,?2Ï$%Q&*ϰ$INzDGDܛ\0rDl\_>I/~ssb.3XZZR===uC=OF; UOzejmՏT/U9vyoPyA@oUUR`G$)REb6r%E@oVk7VIˊZ Wo*'X[bnoߡ& W~ߘm1IiFH7n4ݯH^d(V%T=1mei+P>_o A:G\酈W7n KF5mS"%BYVw "y *p%r9E?9x Z [%Eϊ9T;@g\S7[j%uGIR?2B0"/U#j툪L :uNXˋg Zld U[cjjoqhb7B{a7`l#΃t8HRA\9/ |rFBݺFW/龩{8޻uIribu鮾 $P3;+Js>dt7ju@άfx ݍR]LC}fQdWdG"zU cd=Oj=.BJ.}̎CxƔ@zq Ů.n %b?kdJD%pŕ.6( `f͹@otx0~?}5֭_?T$/b^rI'ѵ<^2Q*TJM| v^_L߿)GOӏ>|.jI':ϔaǾ*@b!HggzЪNo~T2b9w,{{bR K,ٔrkyuC %qIޓO,8()׷pJkofmb4ͧSz-0Z|55&2 `El*&3Tn]e6ڍWyUlJTS,4{G:g ?xB}~S\H`@$} UՄ`[gE 0+ݡPN=i:jSI: Q]J^\9a`2n yZEl?;NF3s,@1&]5`_dt@7%`>x:a_.D5KP;Mg[G...諯|(IXkNXd" a~SLҾ]Fŕz.՛VN ?cwgBreJܻw>|8J$z<wUu@*; _'D=?7(?3z}>=?>||__{YSNGy,`b2Zvoy}f [ z!5`EDxӑ(VS`Kca:"Qg)i"gvF8}pB׎wh<Pۥ GkN ohpo~:> `WPFQv~ituyʛUTJkS~֭[k(kٴJ鯧ܿs>|~v~T JW0<[[k/\DasZr Bt˥2tTիqj`!cq0.Nd(ԅ4|dRH'YxD7%,Z,ŖdKj)q]Ͼ7(ZOïrL=liTgM3 Mmݨ*&RedSwr;OQ:ݿC1?cׄNx ΤC} aۃOyz$KDx%kWoӲa' VH UukA+ \jf׌P F{{񢿆֓OLҶ I2_f'N !uߧkwoO?w)YG^D1Di?.DƮp^|7_!2_FTɥUl\^^g}vU% u>_<>DD)Od?{s='$F?TJL¿tySQV4`(j툐Y| JSYx Y: ji @uCթzr3p"R,Re>˵{IƭB}xp@g(n3WN(KO'j- XMNfUC 2*…O_!8x5O.ZxWe,֝}<(n"\)+ K.T#(1Anj\h~ys~`4@yJ VM?|D[tqȢiv)EM9 ?_+{}G2cKh7_JTJ-MGcNK5ew݌.`zsvbxz,S0ԕHnA7LOMX,opqq%(UuH4אyܝWTJ-M(CZ{0';q-XL9 )CI`AޥBX:&)8En6,frAǝ*2p|+f\R_ݤ~qTP30kSJ]Cbbjmמ `qqm!GOp$Q.[Z>̧im"_|~fnݨ*K%TbpҔ? x>~2 P.||*z$3+(YTL!!1eϭb9E^?Ҍڛ{+;thw3Is /Zw3{/Zh;>,dIIl]fON~V67d}O2%A'xkI[ǽ߾˫ޱQcտwCLłUZQ*UI%TRy}wkyݹs711z(^gO^%^*'kP wpib@ݤC9;qA-ˀ;(~-ͱ0ΆpTȰ[mOD=+pn[$^hDw<^W$6出TNt0x,^~HDN/ M|qJ͊FXzfou_d0>\ҧ_O;C:::O5 `XW_y 7gnh^tR *UT!Soݹ}LZ)VKt:$MT&'v\P.]rlO sx ]Y6vo~Ͽȶt%m1~X')W?(h"CKCZ0-s$MT1BŅ4^^-jAbh׊ٗ`gOHr. ?۷rïȚfx؉y^SFrs7|FFTN*Pʖ6_^^n钱?|||~ H". bԙJ`[B@|e][0,#Csl"=Yb2mJd*N.Ѕc¢~%Hr`@rq2.DԒ|(/ $na)ψO^4%~J@ߺ.?I' oi9~i{+_N4 {wV:h7_R *UT>ҧf_p7gWOx9J_D֘Z.2onv 9@(qG tb$Na >J :>(M&o:#tsmgxzڢ<@ G6"z JyL8=SڔIH Sc(#R(`,;KӋ)oY =S" е8$@B;v)=Q:_ @T|mEIt(n{6ڍW2TJ-MWyAJ(`^]]mWߌX>Bwu5qл@  r _WPK~,GpY|8}t#wU/Re e}2HN7/m̓!usO مA_yˆUN.}Ȣma<#%۔lWrUE]:K,.N@d<z& ]AyVr&;8ɮLM *1u.=hInf$ǭooDžZ` ]"Ǔ7_&pF |mRd 1[TrbDI? J~7l\4~8̀O$%Gw>޴BYE q\rk^8?R4+ #QGJD#mvtD'*d0_輷@Kĉ}4+@R|.O =ɒ4Ev&${6OϯzglZ)HPԔd(0"Jk\ ?z/?V ؅j/^L͎Awch}7 wg<7S*U_z}}b駟nz@~|YȽGh\nH>iFB\b.)!DhipwN<9BrC ՛GGLpɷop4eY541v]#)(W6-QK\%o/Ҭ]`4s=Ȇַ`t{IBMx`Og>?|~6Y` !}|| ^D>}:ps7|FFT9ٛ))%*;+H8X,&{L|po[PpBD%G'p(x`g5n5$ }G8q& ;Sie`_ P΍O+I.ɢ6(oY.^IgS"[믄Um4HP LhheyݥԔKWu<|u¶ {k!Mh)n@,cJ'#|sIzv"k(1c4#zӧOGjc䘩_UK@@}% T7)k癛2/7ORR vCfS70 tZw. Pu4&p<ɠ ֐ F+@9"K,^#]|`lAuQ1)Bq5VWL@,0@uJ%-j轎$WmYH̑2NуۣtqӶPɪk|ن9ovw6W=Ped۷oݻ% ;::ރl@0m})kD-b$ۥ./>'gT4t˘9*Mz^CO ǚ aQђ[+9dN(EzA2aUel`C?íHKRoVJWm?nV^>!Jm;>#('&#{GjB`:VO*^o׍xqn s3r7_>xJm29<<$".`j?c V@ ;0yڢ-$8I?_ɓYw,$@Юb~IGěۗOA>Y34#v$ p:nϔ9'|\H(i:dMPm[2vsHLX#$&$@ tBq쥮0VF9f N@NuzJDkݨ*'{AyOD *UvAoyppp@kY0d;Xo\o(@@jk%pxǫmNEבhtLJj\ 02"]A?:B(4 % rFE)PCؿs#?xcVVWue.Mb" ߉u\ڠ{]XSi+!,t$g$@C 9CRo&GO++Mxh7ar7O*2F(ElBzlgJ%-Fx}h\(:s6 p1~VZRc~n{P^scg{2l {%%LXpZ?*פϋ/I_LE|G:jR;I!oбn9w9e[Ir_=>]?gH^o`gd4L7_Q~Ws?RHrTD *M?(hTD߾9wH0ۚPI{f#DzL@|8_\/']T%nWUv=_FW*2F *G`$WOLX7e*_CDaX]c:Bm GH3$fBORۂVLt#^*9LDxS1Sֽ K PN9 ^ַ`\guănGJJ}DӨ0~ зIwo.U|]"$T Z$|rO^W>X=8z ޢtS.hQ7; 7Q!r$R*(Ӭr"s2Jd0`Q4/_o7'ȽKEPtn@g]^?ME(ü=+rYOS>(^tDIGof AIƱZLr%Yp.s^*?UIw9@i*/ؙhO v[i0kH_&5,pi){OaY^m_c)maZ[t`Dt@@J'5,vu=R,gto"GD{|>x8M>9::;wFzEǣl*s]:>O43OTm <~y*s&{#`L?oན^O&r|x@'V7!s2 y;Fyyy<Ȋ \ eWxā\p.ih0פ--h)Y !<'PǴ֥D׭TLaB3q V%C(wKˆoKD:V`n$sc$)Ar-V h/G2Ĝ_9ܮe0둓/oyf:I) w/k/?Ol"-GgLjEU^LX_lxN;LJ?nʉz@aJFRA, /۲IUr;0l>qX_s{,+ރ `@PRZ YUD_[X}-'] I{@ }Gu%eĒפ=m\ Y|yEߜGafl*"X6Km-oj"W,,F0gߗa(BBwJP˼uq cqA3;V.7# \7ݕH= @?Ul(J6c߽E -p-ErDE`C .&iіѸ; Anw Kj=n@[7;|\G3rcʮ#;Q4s.{S GH>?Q uɐyUmJ BJLw"Z$[ 3g16  &6Ф݁~WG]ڞq_刭\QƮ/ӵFIz] >F}FTlSeWJ]b`h= C>(S_,>{z:JGNdi~CbgoQOy4-nk oPkxvsؔ#O|aAK liG+}(8萃":W<!` Y"_`'Y"[㹶mή <$ џ|׆Gp'I!! '4ܾ+ dQ9y8xm09AQ"=z4JHDG^On Q3r7?DȪ+@*.BlTܜLwrL'"ɀ׺$byG'sIxΗH0b} IDAT2Et;BQPp;(208@D^v&]Dz&']Fs״{R[elgҽDR3iZöHXI}?du 7k:[\PbԂ>=$@KH uHi1.{%\.G]oߟlݧT.LFTʺv$UZل8f)wOfƻIcuIrf@viBɦѩI\Я<*-)&G4` Бջ) qU =|+߳Xl+y]1|YɁh_,Kfg`?g"0voFQ6N#j_"ݻ7:3r7?D뤯ReS `U?RedU(/Ȕ?{:.dt:r{b`G~âeu/fCSTM _]6CP>; :yAB\bA7v  Qɧm Q}]QX*B>hD]=9EotO:;vvۑJ=Rv`fzoԂs$YpoD@NIIAme0}M} ?v!Ͼ7 ei%R`u>`7:杨F_d/N?J e`*?F8}ݓ"̏I&\y2)DˇJefe42 sOtā^1#tlrVm{^ɧw]jc(/of]%.מ=#(kb[9%pwaVVFĿc?e:a,.yEHHрpzKͷ>w ‱ ` I 2{Ìo{ KT>LE,KJ߹mx1rw?D֯Utc*UvE`6HSE<ɭ ǷJ@/x$fơ]Is2>,6%w{9 {bz='=Er@ZF~^9WMa\_S'KK?E'[|Zf1)Ô\,51CJI0rM CᜑPM=6D uIHpFd5Igl}Q`T (cRJul'5 ;=ߟ6rS/JaR&;ܿeךa#CE0Y7|6b~PʱO=y8+`8 c'#N=DPnU؍fGSS9.m>Rw sr(!Pʭx‚K%`ٶI؂=\pߜ(n+:T. y` 0mB^Hl%dFX#-P:0|*nue.v;9Ha~o݇fmtS}!JxJuL)⍝-@JDf89>_ ]o/ )*p'B=T:b`FɋvjO <ؠfߏBԅ7戩B0zr41c`T=K^߇(RL.Ѷx6}ą!yh?~g K 5N$ܙ"q$uX@DhDlck@ؔc4n':R{7Wi7|FFfٟWyWd H%K}:lpΝacO.[t8K%"HD 9Tmg'\ y3Eki᳼^%5p(7cwx"0m(WQAxLhUFcs9++%*os[#PןCF8qA]u[1M$*,Я'i4K::'LtyF,JҢ%m>{Y0NQO_P!YOk@8Ree!m٨M'Srg L uSn' A}O5#KlI,^?χ&гɳ @6<ͅ9y:8L`6u~;Ν;$JtPo>#wCdtc`>%UR6?HS/e-ON]/#`=]z/tK} By4(nVB '`G@E6&/zh:Bk$ũTRY nԖE>YhSEچ;hWRƕ-=)\ X2L>NX1\́{F@s0 Z1 Pl ܽvxj.7sO ":`7 }wCd6ڞoUvT[t7D9UJuDüDH/ a6,;޿m@bX芯*+$]S+(g \bvQYt=w*J*UxFa["w Ǵ) ?Å!%Nd.|HQTmj0pcYIB K&Ûg֟q9&RXєb┈SjNO$דl(>mYQo"]H@X c#$xEd Muݾ}l:V|FFfgͽr/DT^}Lll0TFޙ8>-y y)¤yJk}j $@ x(Eo@chcss {N1DVz?@/3}Ɵ]uF e B {M$S_#Nb?gh:yLI]mّpHvjگ~K %NF8‱6В bz .2մzayMU`(t; $xEݥ&d^8ׯM0TYG_|o*U&9l*S<9@{{\j ~W4 P£,+Q8' e=82qoI[YU_ONsh_&_1ⷘ@/N]rWp͐5P:x"#M'k ~lҨ e:-q|oB^fk?6=hTN&SDd=8hrm3AW'i# : Ё5̴%})iTM{>ӏ"]E -3FLtZHӵ ͇Kzv߰EL5ջE{亦sMr'@4(GF]!n1ͻvTՂ7?>:fsT !t=M] yAv2dJI, N0R5Qb+~IaBC,pP GX]: DG;'' !P ]8mE e2<<}?yF+Ԡ e@ڜ/< Zўy}`^XY 0 ]qv4/;sGlizΖ&@GoߌRdM-G蚋8:^1@ mV6q` I&|ǭ)) /D:t#$EB`s'}z /̈́^עInĦ^tG-D̨m ㅊi^ej)j9QJ9g$Bg.tH3@Μ/.OhGSHлi$HjH`ub].hx>x*5-.E{tud'M 0]&Jgje0fc][edI*UGa'''+l 0z"k ܵ,i0U\y86g sEB9f0Ģ$Ae) )+ ߪ|_9%xVt[@1w EL5R_!x`bS{@j %W6z~/* 1bAJiKvyw{r$|P:H^y׺EȭVVwlC@}WCw:&7n'%}FmMr0%S=/߼ d$oJ7Ñ:Ф.Vad9*j];ם7w*w{~\U^@?}MF>%1MђN$RP= !mtP?lX3)c zcyu4p0}z5Iƾx=r&{slSSڶèAb 1ζpDD߼f !͇ A'?W'o2Wʶ&ݺuke`zV.uL@@с8"~!NZ~"R^ZRX?LȄݟYz(l_-4^ Щ B֪- S0u(RMq#6/\ #c.z8`Yf; f^z2ђ{Hu< `hRNKF1}S: ?<Z9` m_xtTٞ [ٟafٟW̻5JTL/tIt:91)#fޜOpܽ}ܻrxx@z#"B%pEx̑V_(6{m۳g8^!*N%gɰ)E; Qk'nu4A!Pbc|+D8D_v3iʑNPN.1,-jpD+K\M(hO_wYPS@ٲ rzT;J %O)'%Z& n I.:\Gubp3ӮTTDFc"BWIWݺuk(oH _:^^_OZy&cy}|pMo{ C]^^4(l{L:8W.R[; c)$ﲷ: gB"`0v鱮 6M`g;I :&#菶K*SǷ R 28]"/"ԂGC@/ѳ$߿Zd%+?ހj$) LtF&/!?J,HG3ȗZ2 Qf"FS#u?߯T~ :3K56i۵B>#NnϳA1& {{@14Dp8L- GRA9# qy 4Fi3`dh':|wLl~ jbd=QεSյ'SV 7m|zLF K-䬲7lyi݂cc3c*ckt8Ϻ> VmlM[F>{Թ!]ݔ+_ٻ UFݷHGRe^)o߾];5SތHC" pMRhSx[=&b׌L8sfhl/E;WQ8Fb"8E[O1)eB~|P IDATkA_̯͍dC-8aU"k l9ԑ/&fQ2O@Z$O"hF́8_Œ|osKH$x&ԤU)H#L:zz2$:S.5B2#6:!&GLம?c\{S*U@DUlEɐb"n:mNǯ8 7L6j0 ])Mxy P@ ~J&b9^x -Mj6 Clz9=Tn;.`^[٦y(\ݰ0/x.H~s\JBdQ(/ذ Kt-1cNyUyI Ŀw:<n=K?|޾ X ӛh" OG㵇zH[b)& wh\H/1kd;F0.sÆTi٭Y *(I<>>/-8<8Q|!J2炻3w }̠ @"PHԽ@|TAyh!Ÿĉi`hq9hitxPy˅*KAw\xK!֯3) CV8'fῶoZػ nj3ĦHSZ:ݚ Hop!"\L[ ; R6q@ABPJip~˫q+9kC~~7\?0 ʵ7o$ʿ9U Vh*(GGzD\2| 7pl{=۔=SB|)^ 3 Q|I: z*A XnGk۱ZČ[?l#/nLjpHZ~(Z-{,ٽ#]@X>L.W7_;nK*#H|КD dgyps PxghxL爿Xi; * o6d .~Sl+Գ^]_*(k?<<-m1“x RX{!y`尖dB$^~YͿ0Vŧu5 E R%r«žYU9% W|hHwI<'7|575b o6o,BP[mz!3V$!c1rc<{]򢽍`KYPŌz CjU m WWJh b%6%Rrۢ.G7r2q|c\{P>ԵWH|X5J1n:^uۋ-BI`~T@=[HiClz\MMC>o4x<+4 0j[Q:k-FJ널m`_)4PX![>:WJ:  `[ ٽۧv~|gj=LOmufm\gks{8s%k+ǥMrk5*v*ߴoc?;\ ϫT*s9nWe;0.8W2^ybx2}8d͛oVR)h M?c87vG g Y֐?>;z٢MwhoLQvUGpau*"NSWqznj*a#%m61mo,?œ9|>'f;@nMp{mq?boߒkf:jMGI~RD+k= OjΓ}7az=փڎ}.i?@!tL/Kⶀ]h`H!k x*n:-6"]_N]5Uyv:X23>i0rs l\n 1!D(ÃRC~gDI`磫 Llc=p"/И|M |B}zdApI~riec<$%@bSfb7//`$s&8hX6N sv@ݸml]$VVۈ% "ω5קfH/%jL.HՈ^ զӿH5ߐDl'&TjcT!{A`?V=u-*(WW/ f}cceIܕ~2c2v\aws`U$ lP84#E $ z 1ahxdLt`}Be)sɔ.@->zweW$U^>UlM uR^Gzfp$ yݠMrzG? :&C!U6jkpB#z3VTxB6㘏|zKd~b1u@} }T0WHSL݇)AJZvBHM,FN.sޭ/ &_- ` .qRڄq#)$ V˟{_Y{|ѕn rC>/r4r͕)#]aS]j?XjT!{AP[UN6!l"sN7.Li:.X~QFh:ri$0r1=򷳼vGW*9d=%$J ]};B=Hd1WjbRȑ-B.[pҾ&}JL7Mqy((7  NYM:j\Kx~MX ';1!WKD"Ayf+.vh#%@9^mFS6PqO@6#&.;#t[jQxrsH06d4C~3v `݀;)U{ ev\6$!  )޶uߤvVQ!]]HSIpp+GΤ:Z$J6 1l뎏AJD/O/laKZ`N0CAl_E`1bĥ3vDƩC߈3Mz(h5_y"3 O.W2XVo)PAu$)SXi\-Bm@\< #~) eІKJ6( 6 `H^;IFq0!k \dJ}{qE߽>K aOf76EMB`ݓ >Co#$#A]km&A{JLDH~$ WE8!,9mYL \2%yX\ќ?daء6W1;[~"ruDLUr"sU7RW Nyn뀐m0U"7v)fNY"nzQG~,/^oz%A"Bv/`l ?HՋ_>C2nᜒVr*eyKgߝђ`$,[&Q>aImyA{LY MYi|> Qx+SM-~pO^am*#dw&˪>%UJ&`纉PJ)Ղu  /Զ~(7_־:g3ˌBBz(֥D@9螑(]* Y|-)tLBH /MYB$9Fu I)nYDGLHU@`C 6T8ȋi+&f/l>-@nP DD*뭇{@>rNIiטuI"V#`lt>e롉L_,Z[i:@N Zȕ:M {G80&phc|فVw+b/# ו%ݨk%imPrm#:͆SRޭF2l[{vkL%c#sD0b*:(Mlq,As鋠)DyE4&K?e jq!CXFfm$wl+➕X~?K 8~ǿ bF(.w-dN& +Y9ՓhXҳg$Lh65!MđM^EO?qvvj4#J5#KnzQgUt%-GL=`Ye lUd?DrAj4;ՔU&}!QhSSedx}xSk;ؖ.Zx 3寄̧AV (C~Hd^i5K> ldwr%j^JDjwM!bZ(7JcaaG= +yӏy+'LAqO5q.>9&+_( W@3=}y^L)=p ȁmsM,1?[ 3NA֤h(gw(GdC7_scc#P+?cN!~y7SH_#UAGDUllxt9sDj!""@%%.(W. ț9ْ{2;ڲ GC58-п}` \VԁLj@5Ad6RM)V J_i$?K91v[-xީf~4 [q85{dlIӣ 52Am#T-ebjbzFHN ,5Xc cwBHuXI qh7gtz~@{69n>3HH.tZL7g6 A$x;ɿ$2yymE΅KOVddbn:0'q}~ W5wyL* Re+e'-SI-ЉmKqAJ 6Sy8oW8(D_ٙDW&&zw٬ȈLM)DQdQdFM,Z5@44!#z,WTt7!+ȅ.ԡ<~B+yHV,FX!@ nHbXG  wPӼMFd»ۢSF|M!0t C$okU&}!@3KFUܘ G 虻\,a>[=Tx ([ A ˋZPʈr+fh-k>f eE2֑ⱄZ/ܔ'8w[^w7'WNCNX^QIlkYgW !/Yd .rI߼>keNBYndY#-,o >G# e㪩9e$H_?h-ĘgO)_pv^odWuj*s7l ReSDXLՒܐY@h%sH pSo5PV-&K q,CCrvXETf&ͧjþ#Lɏ(rC_%K2{'Wk q_ rD@@5D\-YC1LЎ% eH$2& !`C@s`y#I>^3tQI)C u@%0YXڞ56]/uݟko?D6S]er7@?S[2M(n3$X}}{d}B{j.?C*?"yeoTJ 2ПRk@mpS78. a{~]ZČ]ÿK:@ey[('cz^!!"\?m%2 4B((/s:mKk. |bJ C @:tpwT_!(;{4VDĻ描[evF-# {w(+G{z#ȵ7*7Q"nBe[{ \*(ۼ T©,ɱmvNPHZ!Eu٢:Ap"e۝ pnབ|Wo ˙O?7 mXɗ14$y?{ N[hTZ6+V-y{I,$IB9PX̠'D3=KvY'+`eq»9 Xڲʻ){ X_ py9la<ن)WvfCrM= A ȱCʌWO5x(*,\ (준2aU􁬞NVU}gEY׭۾`āYFw0EhSWũal &AVZeMYN. =w14 @ O82(LHX&EQ0qCbz I;#N`^z$]?S>m^-UK8-JmM0FƄ2]LKbI6ԎyKh- T{׻G 6 ,ݭ[tK 'd( xF\:332׍{bFa4/1{/]E\V@Jek%$2_,mݭ=YMter HrvtU#C綀T@:s ]idb&Rx 1& ɊuM>m4w+r@/M RekY'`l"qx|-;\>xڂ(Epd6eu6$\, ~HEI!$dCg.DK=BF6. pmN"֎$&=rκdB}-aјľ.ͫSZ2}I 'IIͽX(̛1 sd8$AOwz{+9I7];sC;wk?ئ  +f͒X؇k͍$\2RlVVMtOw03m-#dczߡg^(M2kͨ*Vq'Ld7~FC9~oFD#oF;pp;ȘU 3La@ێ{`Z?bv.2=|r`A+72t764gi ZU!wԛMlɌQ S}=Pe$ߩlYFVPO- ?. (![05\( 2BqK]mO G<>@L΁KHs`3Ѥ?{~$݅'ַiюED FI}K#<&cFIQ`C~f=ȳ)35t<]g$~7PR[-Rur)N,0p:1P=Uɧq=1%_n`=Y=EZN٩%$ X Q _pR6tT)3;N3/@bUb:l_D,x6@}> h`bh4JΗC/Ys8MM8Gj\(pSkvH-r*2A J p`NA[4h2,/Gu')+ZٸPCv`IWw .4 ,muS!hAYRc/J+@4N9֟]r)6#द̇`:bl{a?Wf #|~!nݺՋyј.2|gۯs@q&_"Hxؚ &… ;؟XOE q..^0Gh"EJu:F΀AH:#%#MC-ژ4533RZɖ`ɾ(=?*'NX{k O9vFV27%A݂'r!Ĉ1:{@L,ay֦n{GH)52d766F&'V0pgj>+^`a|F>gpj 5;D\<!>wQ_F`0d ^y1#X)pKYqo8-RSR q\6CAI'^9#|tDtoe6bMGWg5.xcHW%1Kx G$ëEɿR9 `_,Bk5tJ$*ш~NChC!0Kxk5Uh͝C>{-#ڔp /<^ A7қCOʖ Bʳo%]sDGO1ps??4@9ݼ]] M﻾"VVf#!d$_gAwNFƔX@3fǹ`Q԰፹xvr1?ɱ#;P!¯C3NV^b 4?j})/9bU=DOiIBTԕMA&DD4$Ԏ*RNZWmgm@\@✶""S,79'0ꫡE>.oI͉*Mc96 /4%҄ʔ絓Va!DKQ ,$"wlsx׼WzI䑲d_ u rv:Nt[Ŵc]_[ή`v{~es~SBHȘUL:qG@% :?8&8O^M4>w^t ]ɭ%"_I%t`aָ@tul&UY8f,#?ͅhخ)uzt!tQI=HR%Qьqscњ$?΁0mfp@ x@ür9Y7=(#21l'diD5|yYÁԝwہ/;\;{ݦ< h0{%1xegdX@SȘE0@E1k~BM;a]t'dJZyegElVI6C@b `pT+ڱ̊:B59iP4 O'lkV)(V4b͕ޙ† JITnuz%k[{C@xvFj1˗d[!̥2}pkެy{ǑmG5=Ou1bb-QJgX[Y+7NiWl{7>[a@P_d4IϘeLe?88HJu @@7A(mr=hu^0G koY"N 19-o`q/vD1>T'n?v6T p??1D=2NDhn!5u`)`iu'{>@uu9Ggƕj݊驠5HخB)_6DhDI\ [6f`TZs#\'Crb( 6"edOArKWt̓і:ɣNZI]cV.f`}{G?+paicwC[ @y+[}n,ptBÓCG Gl^ Ƶ)4m[CRwdA@W*d 09GDҋl T.;F@x4hgl_ދz%z~-lb'0HӨ="q?@74+T[KO@I$tFX0 xD>;CIlW "l *1-K1mbĈb#+HR=*ɿPkI3M1O|5ڛR" jYDQAAZB5fSPg#~~l=H+1,P$}< k%k {{OEuS~S$Нj0OÇ{tY#}`ii W^ mLׯ32NbAFYXZZh4qx l>*δkJ GB3{g51$dSG+;C xH / 6ABܕCJ<Ѵ% 1i/K +\|T~{5tSƣ(qxLB3jz2ChFac7nG D2FZP vĐYMt8gHH@5P=:`kr^U.8YS]"cqMj Dޝ~~S8{kIF_ujK:u WE3V%V"LZBkRNɘ9LcH-ZN/_66 x^{t=Vnr(-dsh Y |9j{z_* AhNҊ1x;Kb'=>PnLrt IDAṬYata>ĒrRm^3Cʯ-G|hjWR [I]'Ctu"NUt'}('`)&n޹Uݰ3гToptlF<1JC'v;j;1hIJM gA!F˪TG5 D>$P<)`FsF~;j5Hqnh4k~L0?3f0B4}fX ˭4t(z1t m xH&'P\\'CWKccN+g\2,Ph>f.-=-_(",T̓}zHz-rӠڑaqPFVsa[Fl1>_G*MعPGpC*k|A/ շBѐ"L#O\.KG=.߈+Lw yjk,: {[pvĐ(H;f: {VLٍX0Fl`?HB/2?^5ڴ!Hq vS8w}Ɠm^&p4)9wHnKw$BSCD7O`.S2循6ȄkQo?5 5h]b^}L?ðY^ 5C22(ɔ&׌vFt)^:@_<ފFۻ(#)k+;%s 0s/T0P&xqoG(ENRÇGvt頖Q 2~6AKX7p"u+s2.k5_|FDBu~zJ v(w?{J5<f#e 2$li} ~_/ (52?N H^5.Uw=4ͮ&HT3 "&;2 9j2p?)/ dz @"3,R@5&3Uz$6;L"޼rSG :g$9tm!Á9l:#K`*T&)g>|TJG%DJ!`G IrS˹T$TDd0u(O{6pEGh>̀c dBC|ɗ`e)1?QUQ:gE{tH1BiT1M_~bGh "{VmQ{=r)S<-;)ۋM'; o^9)qO$@kP7+ucD_!ǵ@b(1X]]iOt[iW/􏛽Ryڗ!PonEǨT@*7Vu юu' , |#`aBa%0(T’uJE_k}n ,_iKE8lgAi})wѧqp8567hny Ȧu 0@ YƁ,e>Ӧ7;_cjO ːvԀȻLVm렛14'S MhgHnșlaB}&w;[e)F4;t@nFAx3R^x2. !U,#sin^K )Q{OOQ+Ի(OUu(M $ ttJ-MOQG 4{I$)7 ƐSAzvU~b ]~-xaZco~%nݹIQ0_ޟ@ѩbt`^mˤmWKIAq}7DȲ^hZwBD ODžƏE.4ӏWp(Zmۥޓ8 ْ1^g2b @ WJiUNnȘIE>u^[% aEOVx+H1:| ByeƌhpC&\#v]ucK4v^$j6A5&eMBW8ڶ `&e]HF=mFߍ5h~7n}{}z{b{?Hy b ~ YLmI aG>(XqL%—%XuSQvtAwr-Dwvvzs4C+'Ix2 (q1b@?Gd2VVV0@;;;8| [@:t~)oha_ڶBHV}l_Q(`! ޣ/T1HCB$ -jtnFS/?P"~Oe!܏2:`Q"ߐP$SU0#"u! +i|ڄ%~.1Oj\tM.DQϿI >UQNʯ<8<'_g׿h$*RbмbSlhXnb{<S觼/Guɢ>tK yvL(եJѧyA@YI8uf5YdhNNL'?)9 PGy@4iLmV0n^)4[ǙSGExI6>Pt] _{{,!7i dgTr'Jh)d! *y&c& H9<ZTKs@ڎMyvH2%߮mb|A: Ci@u{ַן|1Ճ(5S<#m|tQ@RMʠWi>L]jBsw82Y_fZ)8bAޑ;jkxkUq8)b\k侔so0H<-8/zf􏦅CF'O$X@LT/|:w1}t :{`=k([{8W̤-4.ȼW囆Fs-5bVBPI]Vėyh} XVe5j-K3iHSg=;+r`\9$ q0_(ՅƃG( ]IPg @vFtis XTE˓s5;~:B5JH^f2t'B;d_Z3xvuz ')D<-̂׿~W0 \'6s4%s4>u{'%%>كPhJG)m"lj@-ճ.J *D'-}e= r Ofe 1N&j@4?(l}7Cۋ+ommOWh<*Ӆh}HC15x3m2@D{79p$ 192̟וQ1'O5[|;5-؄&Er0z0K^N jȘ  [(w4O(NFFW:Un[0 &Μi͊헻 n(t uO46}@9Jdnn4 H Ovo;7:dFE C-O%Dl5(Ѥud-s@4&9~JfC{S5F;卑+L⅒ǭoa{2)U:OP'F/UuZl +mB3jp]>mjب!0<[⃏GOo qQ GAdZ>l;i9@RU(b5H9-h<ԗ$Z;{EZn/0 :̢uw ty&eY ?< [Șu>}@=70,8&B+~Z7/ҽi Ήk١ #!C@H)*|s{|1qOϰ'Nu;M  pB9SG5&4zhA%cɧ$ޑ|u6~xp( uVJ) :C@U@GYKY[i6 ~Q:7`ZjZa^'F9B se|!6yɱ#C(f^=WM8-}alᤈ}IsE3NjEɫw}g!lܐS,7w* 4.&2J٬?@&ɑ04m;'V%:IKWI9)b_` ՗]'S U%K|{/?Q d~4HC@~$c. =Jj ߷ R42A"wm" 0^擿Xsm#6 _^^ 4SRT^-OǺd9 0 ~o]0:vR[WLLH9[;Z=,d 8aؓl?LByd~Ks3!|M LT]w+kC~oT?<\50r{-ͱ&%#[[x]k$?۸2iFMtI`r!ӆ $XZF aM;Fnrx\ uPJ5F6`2zi3-O>o1s]$0#kkkFбG%o̤i+KdyU=DNi`sǥnp>+"QR -L/"Dxah݄Bvws`{ڢ>~֘`)P]WtR1~˯mZW-Vjpkh"ޔWiLAH &Du6_ H=˪7`Bv6Vӝd;n06lͳ0+ľ/@ R#22bXm˷H8<<,=d/-F#kdH1(~x>>ڃϓ#ͺ<jmnaRh\\8w.U$4 ]!7دquR[NCb*bi]dN u -2hB7^1-]Y5+\{+>7_αlhҪ !wYa1M+kL{؝Pph$kZ=Av2!LMb yMYq\4^)kdfMǵh ona4FK#ӯ"4x[?8pr)w?7*{A?E~iu$YF4Ta:H .@I2-JɓPsL3 l?3qa2T &,d<.N/_ > ܼs{UR#}FnQ.^HvP iFX&EZ6 +!w(n! $DwoksR_D no`rxmow{Nt?LNtdFERd2`#*4=7"߬)z*nݺ7mHIg j E7x2}q#\z~ujP@YXMpx4k+ זUeYV|4Քg o[)"ZIEP1_i6[Fj"ʌ,kM& #ln`{w;jʓ 3=@xT5XQUM7^Tayr)B, G, zpYz21SuCM|CyUN##h<`DtjHMmhWcRtw[uoЇ }nfd," M22׮]!vc6KKKr68w_ԗRG}~*$dP<Gx}1FTe!򈰆AN>Hz`Yii[×/r6PP[Fepm5w_W|wa{g D{)!6A0=B<ƫ#^$@nզmLΠF:Rmur9=譑!`:u\۱.Q:lNa } [4t a7A4@!de$&;fūedd4% kK0 k' &I' _ؚ:ǻ9f=kyPpxWpjmy W(TX<4orBRIJF-c^j~!ѼUJ Fvv1M"{D1D)L +%#Z)t(L"`u+;Uc{ZBEVм:(:|_cx!Ɉ]` hhiBW vzJ'j4h{[Pk#f2iUso #)|dVWWK/Ν;MPY0 5CːA _/l`a4x2p% <ʒ$p$pul-$hrI62چW:-Wzh4>vvwwБGUTIG:bzYҮ<^@ӕdjY2lm{ڈ^kPmuLN:tD'WI(LR~v;p4 NAuc&I#N&Z^םn2@0|WOFڵk>ӧexp+J/^:k7e|oF9Lƀ` u,(9F Ɠc.:2"TbK+ wV—ۑh4_J- ~54&;=]%KkS:C#k@P#ƷQaJbur"5VK!f8SX`S/n}o4ROqF6ٝO-{ټ2=mA2ږq'b?Gdt"ɟ3 N׮]~m|:qykkݷ/2{ɑc蛺=M\P&?íC|h>íCla8xR#%Qb'֜}B̿Zft%8Ow8w`kwwã1Χ/V.ܚу79Hu֎4&Y<΃\ X}2:بۧ>BZ/،&FѤ\ ~ XY>Dd[N6222fهܬ;cdzp]曍x<2~߹$㓯nުߙ{{kˉx'#`iՁ+,V4Pf_3G!֝.m'Ƥ(Px2AI1dRrY/3kbSX6$l?EGuF*` ^{M#5L=KO D1$\-h^~#3[_6v&EoCrYᓱ,iCuoOS߹)#eÇ[C̊>O X4,v6,lC x*0pdb?;VWWY촸v4._X'S웇م`²cO(p8xbR V  KJaiIa KKʡ׶EQ()Qh ]<C(%|YoQ]ʥNC$%%2 S,m8A 7͢05}MhmNNsDcHV_WB??0SkKqDžY 4RgH]=z4~!ٟ^x 4dYq2`A x=1 o>K&-?߼(^[@腼˪w2L&>~?Cۣ'E+O,7fCҵ ky\3| ZLS'܈P|EBih"(I"24Fv d&(hȩUŐFƏGJ ha8&'6 4nD |V((h|@Xomb> yפ!;) !E52Ӹqw ;ӯEt 5fRO)Ga2`0k:sOYo%@ ȘU m޶.c;ؾi߹64w _~ve} y2QPoh<{1o)J7e7"/K\zBY)o"lB /b_?:˘@/_#ϟGY )}m2`A Js1+mo3qӧjZw^o}}+xZvwO'{z (`q@8c\\[zy} t:G.{5-<-l9:'ȳ/ϝY{Eds^z7Y 5^YÂpQg3Ο?o#N/`ZݽOnr^^O vخ~B]{HnJK"h71jޗ$}h>XEs0{dQ^ƠkΞxݶ{i\ы>]"̔ /;x0!DEFW̕M7c&1OV~jȘW+vyppTFuR-u_&|">q>AN=R?m/%t*=~}_E՝-N7|,|{}ܾө䵕%;ɰzX&{`g&#TF7]үqrcCl@qF(byuX]]жXƳ^>mEuWe[<evޤ5)賓6FS i5dt8w] EQ$-v*bmm =\o!{EMʘ1W{@Qg,ɘki Y+N.u_>uAC3ԗBVu'[,Nͬ~#xMznIY% { ǴP+X Vʱ݋,V[Hs?[Y]R 6ʕ+ɚ,hI,&bQ !0;d9}iZkv>oyydY7w7q(>Wio 1^]Yf2@j}~0|im;#@9p̟剑ٺ5fx}å8)qkaZvnH3Gp_}}~<c24+қm?Fv[<dzجu¢rwr9O3%|>|Iewsco+g;o|} ;H:׳:EoYdHYӶaD.l&[/qsl?#&@4t ?5EdyZ$Fa%NDu!BM>qAe;$`}}3I sK.6u? y^qFbhE14٘w1=;ﴎZIe CEi wԩSGx1l@}cEoAټ{HڕzI[ş'( Pm=Z<ܞ#Oy&{MW-iA3x|]Tx:[&Ϻ/`eg1L,fxQU^}dc1BO!Y4sE1HHydOY BNW{ 6V:Z!R?vB~WE$N]Cb0ygARNXg< Q +4,*#UOPTvGܰ8ӟ`ⓢ'CE,^tO׶_z>J5(? 4F(戆wk&2N>eH1k&c0` G1XXX]]mE`gg: @)՛`yIOrg9u{ޥ3~{wԪל2yɲH'~:Sy@d+NpʒyeFjtn.khnwU)[sS_m# ,^@CB?۾|}.eA!wvsp#L91&1$cƺOƜa Q caE_|Th4JڇKKKX__$Q5hG>jg{'٦o@8k w mfH{yrK/ɴ2(b0 `4saX%+-.& DsBb'|ML3 N`n"'ۄ|Kٿ~{Gikrg@:0qNeQ\z7YDg/ԽeR'ʘ;,"ߚkqM 4.+ ovxn͢ tTS*IȤ? V"y'f/2b]@%ZA#DqWE(vH%qÃbN?iRbm搆Xf/Ƭ8X҆c~Q.5 ScX %2fO _7Mgjov}UQ5sN.8qy"ԋ" ied b% cO?0_'Yi}}Z'W0Ndu1]{LSnPZ?{ u9GCLCP3cF/^lm/2py1`ӧOwapi,qN\ /Ƒ^Vt^\g0&Wl>۰̀G2Hs!帜(# ڜ4c:ВYOT?i}ZLOM҉BGk X}/|_%5$U8?._{\veiܺuSY+++x饗zYqɨŬuB 63jƌA)Gڑ5䅔8{Ŧ_q?6/n@2`p_q`FLFkd)@z?S bJhCMx-hh&PQXpICM\zM" 50irISE287j @ԙ@FC] u?Ʈ_S\em[Ig6φW0u__JaR[mmmɓ'} r z6^ Y!$ڷfd"3sԑY{id6~(]m,INN˽[WWuͷn{wEI>d;U8*mPZxc#9h8z|L⑎G1hm;Q l+R9o#~07[ 2>7'^uojo?}Taʅ$5!=-DX?a yiO +y3}mm1];Mx{2^Y!'QƼyg?>vvچ^yܺu'v죏>›o(_k}9s1mKܹsl-,Iku֧Bn'6=-@ΐ 6'ξ~2jfijx㋵ GZ:)]ב}a+ޞVJ!f*.-D;bhh8`( .Qb\eX{ѺkKx~6A9C=LΩXFTm?1r2,w@-ZG_cZqc:umt'"c^KFdNb0`wC92Ɲ;wzFtYe3W{1ݻ^x$@VWW<߳9|M7#'_:HFҠ[j?1(3}2!T z @y"F#_SGEQ^|E#D(ځ_ʀܹ A//lQ Kj$SHDˮ'MF2H -qTtʋ(#,_H87z@Ug 9#Ȇm_)ak!b[ME҈V L0]c6dCWvXyrSMi)EO!n;<oqQ?}K=-oR۷owފ7hIʸ -^$]xX@K@F:2lllgOZO!:uW/{&RFK!|h8EϗSݚh=Ou^`_GMpYkLPe$Ѩ1c R2? RG'#-h"(S,.o(i盍 !i9:=sNoq Hpg$P>?<z(y^"j<1i^;X*ю&$)dG B6zՍvQ1L<<|7u^43u^<_y4۽鯔µkjRָY#XBd21k}4c0z B.\~Fh_*dƁ^666z3+p9a>;ͽՂ~?$H /W<ّcEB 5LDbyW_(iT kX(N7p~ `cNQӥ΄C+N-.Sro ȿM+xv>d~t dDkc ov7m\oς1GGG ݸq#9R W\S"g{`ֆ'1|V$#2ff2ҥKڑs.gߧqxԝ ?:1ec}/oy!$ )2ff#(, `/ˤ21;ziuT?ڋ_7*Njc\8˓1]J~ə9"VEzYmV/vG{+S/Hwhc0D9_yW&97hȿ ]ҿν mH;fn*9^?Ϋ}{ jD׌+dGݔc|~UOP?h4p8LJ{޽^֚1X]]իY& Q2IF4&ȨB0Fg.\ٳg2ܼyRd[[[~ۯ#ƽVUP 4pvyqm#a9;LdR>yI$fs؃=D&2 X1y|:Cg Iyrt?iSWzK{AD_ύ'y2r- rl }=UU70҅ 1njKV{gϞ`0H#-+W_}ՇoY {OZI,Λ1W{`0h S2%GE> hκc}ov Kq [GABhD '>\u*wXJg\8FJWaMPh`:1&$6}`<3znӼKc%2|ﮡiY[ 0J`D _)2_Ax]zO01w$a|S 3vވ܌Z75 *,--ӭ$]g D~_Y$M煨'|G{HZ͘/̽pI?9og/_ F/;;;Ilmm%{D=\rr^EAGu<:;$k>G4b9y\L#9c{Po/O@;T ᶤ&ğx@7!SBwrk@mf 7D*le>`ZUm:EzT_QU/(:A(!6kPI"-H&\Hv0%н9O ]3B/o?'_? dh߼z?^d6_~e;wiӬEqeןb:qa! .xf0 K/&I`8~ T槯^d]â6~6E 1bHaB/~@5,qz@ P9~CN i4L^;G=ʓ<hBJy/In}[05|ɿ>MLJ/iP++ WE»C.y!#Fဃ/Gc_?{?^dr/6x7nt.wFfd>k2Ngd0Qҟ<{(I^U]UOwOi b#l|^/ؾLJcÆl$HH 14hFoh}xdEKeVevWoNMUgF|VEDW6mڄd2Xg3C1l$;Ù0Q0sR)I΋m^ NyRk-wY$Tl;~Bĕd=H!im!TEέBlB$A!"$m#;U FE5rQOQ TdHϠnI.-3ϑN:_T\2wb *Y9czCYў*8EE ﶤ])A 9Uk߅e+NlCS}x]u*vs\h?ÚB-ZX;Q1 ɿ01"C&Aoo/~<9⫝ sB sWbvJf&ɰ{7hqNif:[oAJVy"uHx!9hc}qg'Syt6[o+&:4YUe! ,̪O4CfX)%-r$k&ϩzhUT8P/ծK]s2P ڽ=,'AW9sLmR,--F Mcq5 ňqA!ԩSWXbL";~Flm Jㅗs&GMJv-BZ bK#:ns 9\}םP"Qڪ+jcu{mZRbO ^VQZ h:UԢ%_y?pDb9L܃9S/AA7ώla&#~;mcs dxp<63336KЃjA5Eʯ"T(WF2 FBSS:;;#sg}W;|޷XP ׇU|$OX ТNכȫd;͞$pp7UriLJdIfYv|N0jlC-7@s~~B%W.[@!ơ$ڲ%&=I]`k,j8]+eGKdlmrZ O~0e;0\;߼ mMvD–\.d~aG+n;aD%- ۽?:nT3gY^Z;:1 N9S*0 qLLLuj'_MRԊj*CLubTEǂ@k39>|W;B!Tv$}=7bc9pR+#4BXۯ-pc5&D0mB9|cIR3ˆ4)B詴t.`|[]2rذX<hP$>0Ŵ/6C9K8wס:O+\Y IDATK*Wvp@(ϏT,A L7<:ɽ%<܉+! o$lŖ@[[[(v|?{l( XDZ!~׊Z O FZ^@l /^aEnH&NJ&@" K9l{e|IVEV8"Γ|&ʑPιT!(= "Qf,g!KIz)($JԟIW5_=z{V'{ s.Cvz^J<֞M^ NC^T:gF8χWƭlMf t8333]L5F-f?6*Eب!*cj^H$$#F<!b3<0zuL`{KohfjcMmFv"g7dqtFVȼi9 G:&ՆS[*W=C8DK;~̫.g@c94}f?:(Bڲsq^5obYz:Cuئv͖q'[lS]U] Ls A 7mkCH$/l6hsBiWڣBvkՔ96b^|ԼPS) Tb"Fصk؋ϥ/]!_̄2 c ]]]ۡ6ckO8y LԍGmN$ZEg4C8M8vҎAHg,,:@%B=RkrtY3lKPD[%\vW!9XGۗjs9(>S1PpgSM9\Zx9nZG5`v8;]&+(ڐL&}gleoq sss|rx]"WJQQ2ĨW@HcF[[2zʗH9x(>g2"T9~f2dh6O `fI69enp;PɞS.'mH  e Q78/f>DYYb cj]V)YB.Q^O:$-&]Q@FES +'KGZ{追TԺ?걩]r8(׻NT EP(]'O!Rn1bE(v!?!9cߴ:dH[jcg-3 K+el Yɴ|Ɉ_b2%XU?ܶrJd02tۂdC.m("UT>_P'+f%@YQN%4+,A. ͺ~F ̀9Gdw^_l/5_U z\?^ 7hޛ]&:/ 3{n:B m|>y姧100Jl65)RfڣV 61*"8(#5u֡30B>g+&44f:IV8I^K7PNjcPNE% U;aLiB@+O| UBPIqo 'B -@u,!dE%FII$kH9JS;׮7!`>迨rRgFlxݎM7n `fՅbkjj*P4㡴+9D~cۨq ȱÆcT1_1v 8W;+++.o,]hhh~NtfB7`Jth1S? @"qWZS"ZIA(*? @5n8ӎ WP!0-t1*NBv1@l*sG u!}b׋FXU24d(יދU9$B/LNSu2 nX\\ 48FGGuU(wl^f5lTFa"H,ĈA܌ns&rDwԔRX~}hQ+Ts'~tG 8?bSTQ5IHv ppA spY۷!Gkk+B񻹹9ФS;6˳830+ap {Jq Q,1ȴvRG!ϊs+"KzYAi;@`q108DPWd-91K{p$釕$fC]8'hۜPE[[fN 6&sI?22ZAft3r2~{R0ŗU/ɮԣ*wjya]9〚]#">GנB8:PglMs=KІQ .9ɓ'Cib-Wm7lĈ% /H$\8 P /C@^b# P^xXcgJ,X]l߾D"p`>k?O|ֲىd2e {7:Z[bΏaxrQF(Q/J!?Zm[\Ӧᆈ"k~[ =fTUA+:PajY^N'ϻ ɗ=˗3,pQg,˭:Q^Z(zUO?Na@tJ[dR  }{'1_>4{A'rJaee\.T~Q+dw5lQm#FpԼH$TQk0Ƹx1}c'a.t DB#j6I-Aȿ<88{Y\.PN&r֜k71/!RΠĝnL~8c6A!S?K!pn%$P @ARN"9F3&@t"0ZI%*H9RWuVğn:FO@nXvNB 3Ԯ 'L+lb88 =۰?D6iINR{T0j٨e}\1@I?ﴯwo+M"9_c}v9&4J(~766bݺunc >mڜ=ErYӈcA?' h]BȰ2!"EDjS;>"S! 2{AO*h,&|+㤌 * Hd~8!M!ېהCO/w!]Kbj]fA*w =Į"G1MC3݌mS}T"A`fѱxxͽxp +8瘜 /D}˫%[ 6 V_3H wP:=5F-۷omaaO?8 o# o?;3CG`i} Ν&,NoK%ezSzEM{!cqj~q Z] d6@nq>sH!ΒՆ  8D0]_5ʴ64_[Uh#cCYf oP@[S z~_>R17O:Pڧ~BTcjԟ"ǨW_)9W {iR̸2Iv 8x DhT*j Ru ѻ`]pw* ҆";!*b삂5'?g85L ?oO@kk+ry\gffpŲtC PMF6*mcM1bb@T32NJ6^ s]Ĩj&I;zzzBO;ܹcccrenGFFp)\%2 ccc V,..ٚk7>'B]k`t܄$w8 87iu`I"%p',.r7@*3L)C$~{Ji{8!j"`.Q$YWkVuD6'ta}mtǿ|[9i*R`jD^܋mg%V꿓P, c߉ٰUѡT_\ӣ3w?ػ%TT ;v,bjP6FհQ,)1bCUfj?ݣEA&d:i羟I 1_k7"A{{;/Bf~PECm6444l+b~~wuuaii)-!44/o u &0ȏ [Y5V *ȫKa2bf(BAAiGk&,F0CqY9'CLG(πJEy*YsS޲V3}F//kDY%䟔–2 , 6y.,yVWEϏo7T1tuu=rm]r%^ ImgcCJPfXYYYN&W|Y͋T^Yqy`bHFD__nvuW;~-Bсt:sw ?JlB!t@uڨ~Y cT8geɺatӁ}ґfse%2<Ȏ5 #A1b;ԧRJRMIyM"MNNFlIHF@ S{''i 96P8,rM D=/( h#\QwD4\FITMu~I/؉ǙK1Ǖs3%*ކ߈rpzS& /LΌW" ;{?Dp9sl64Uj5p-ڨb3_WWmFJI)ՀV{ aGLχڱkooGSSSh|m;pӶPi|{G\\"jʳxW"4+ J'R)ZOH=9hSˈpYYIacjQK~("Co+L""aPe5碮$ 2K'®hGqZ1)ׄ$K\9hr/hC%ege0R> 6Q >- H4{]~uȤ~itvv s΅q8 l(I _-"FXCVÆ*e WQ[ ϟşGpoF}}}ȞYĄtLL#"JMRWs1j wu:!2cO?׼5C}}}hUuwwc`` p Çۍ?4n$jpl_)-!l{د$L#=$ɜlɝZ5 2i+>)!/_MQrY gG2\Ơ l{XFzES\^WF(rN~Z֍f#N!EU8r#~ Ǡp*0m}ĨlZ6 C F|KC>_)dD"DIH9 /oFBTWF56L!?[1jU!-44ԒG>}O+J! lي}nBSS#ݎGSSÝ 8FH&x{ߋ|3ɿx?z(v܉ *@"@oo/\jԨ!G~:|/`b.s{y //}csrXɗ(@P E¶7eF3 $It@ 3"1x|yY(8!<hkq^-Q^'3wG֠ zJ_%N#|}MSU jʂY3<{9%Dm  IDAT666-I BYϞ㜇2;}.?&1Wx133S8|%###wttq:]T,dsw #T$PiZUC$e2 Q'l"u`p Xjq6/J.98”ZuB*(r.Ks1'2!H;WH?tu/YxQE=^vJ21"Jy.AՓ]W9<MJic~xwf,'}B?/'?99 .qH RZ!y!H(ÅB(QC%VgMqb~~7} OR~?&J4M M$){?{!2p+(Hyl#|Vt}Zo\b8FT wv*c`"@UC>ǃ>???DZ"}||GlKI+,U; }=o~xΠPvu2n5O^B3W'>/3 p.mFܵEJ)g#c $1fS]cEK nb玤$f ?Ρ~{hZ'_Oy+ֱOV4@.O1dȇ;n{^1Hӥ D.dzOlh~,,,@5Dci㪈W@LsEU ?|2Zk?7_rZ1"ӧJɿ~###}E>/ 5iFv۞ֽۑHI?rz)_Y⼫ )\Nop%*Ea'B"ݗDFGNz4wD (.  (="ҕv^${QSOIwEZ*0h 3yA c,{oůik$;;;C9h`b1333g΄@>/kٿjP6Fe6s+Ϩe!G(RcJqlv/^kVkG_\TwmB>oRoF "YxHlK燎cvgآг1:}a^-+u8@\E"C5l(y&fR)Bk'^*H6A[bc~+#"Q""_$aZ@sL>%;FuTjO9,2?S* j9]+ILD̟oݎԄ e={SSSr`1Hf5EclT8FO EQC=N //a"Ls5#رctoB_^---xmklGuq9)<%ETa@'`>ePsր`;:5#0&R4 S (-L[ҿCY{tY _'NrTHC_UsX>!z[[7ݾuldB3 qC'~;#y3 zzzB!\%q_5JX}.xGQt}FSq*xǗ_ٵ% KЇz-E_pf~ַ`'9*ҒGJ+7wޱ#; g]ΓhaT4! I]&ȞJ%g Ax% :X2<}8ThHE`/m@fשS'; 崙z~"+(jwG왻ut rU@|s.3JW oΝUC[[$:& $zPLX6{cՍ76?c ===]\Td2} F4ͅ)|;wx~-M 2b/fgE̴_O'[:!1I?yP]#b:F^sQVsB6J񬈃oձ[; &<)pAa Ԟ8jfԊXa߁[5 N^1L&Oe@U E'|~601Ykɥmыf1=\x٪#… 8uVVV0::S"@__B]q@{ף!_,r+Ѭ56<|j_@K.&H:cV_bJLΰt NĦEǵk.IUOy.oe>QKٮ>ʔCIp+C ŋ]%߈MHеNΓ(D?ohyeVXP9)4/ ?B6:wmϼ?" HRڝ(ki:u*T_8tvv,W_;TdSH!!ĨMԂ8x"ƺ]ƖZ PH:Lu &cǎ᳟,|q:3ڊf477rjS( _Ǐ͛^XXTI h:ܼ={ob.Y( |\=چt:ieB"&Q?VڴB"T((r1]Xu-Jf@[q-ErhDQaO-E}Z6~H[yaƬxAiKPY*ϛ$^"NԎd|{9G._FD7̉~Ѭ*"2C<~H#zOZQ+~^ 6E~8O ,jF( 8Y|;K& DK>_` {ۻ'N7m=OTfk?y@ohh@d?H^2*M/0P(`$2L4188IwߖV|'4x^;_{Z-I1!ɔW=¯H 0ѻ Ko*8v OlB-%Y v .mvK;?\ˉ[q#'勢XOOBUcj~?q/> m+6} ~!ѣGCZ[[qNbkF-̗K:[z1jNL_;_3 Vֺ7B"M&e;" Oرcۇ۷˱1W%c9XYYqGFFa):L&> E"kſ\tS/㫏mL(Fx,^$x\ֱ'V.&Eh4/(dFB30/)-|M;e i$w)ڃӨHSğ3Yd_rqN?bɠ䟞?nsgFO\>6~]ʝ,% 677.NVn ׼5wCך6FYr:6X!dT5Ƶj\{#<(' l߾;dGMt:y+1R)R)444 #&2 CaM P$"ؖF_gpa(h_S;vInq(#G-."`كZzT_#CM.XۜѴM`VI cΤz 9*&h?-# (@d+oD&Qab}"μ|J7|mYl#mg.e}nd2d],RǸ ?;77'n;vwߍnkrd2)Z HLKKK2f1>>a===DȤлˏ] >E`Ͻx۱9 ok$\{+eXD>P4Li)@6IV@{  /sJw8(q&TBD)oO7hIC?,,O_±c-m\6‹9oJDӖw2DՈĨ:«9??GommwߍnIC}}}c+c d2f( E* ԄnF"$ xVllHg,KóGJ&k0kV~Iaq~B("@: Rȅ h!"d0ay r;nSVGM#,itj~OO3Ipֹum3$pGϏᑧ_Dv)4)|HD=nooue!q>}:dLر6m:m6F }m1*oD,\ELca{<!r %ŸNQ___p"@ss3 Յ>qVss38eϜo:Ѝuͷ 2a p {vl_GRըŰPXR@pBS;l>B4JIso#. r.HK 2s˜ 6`ӦMrT*%'khhH]1455aaaCCCظq#`Bcc#^K_؋ _ #"baiy<>57ߺ[֩WTEPҨK+"~cJցldh DU'!vܲ63#eε2P˔#$ѧFM;~ !h:])p M7 v~ex7tܻ'N[.ts  ٳ!{ergj'wfVVl'!9seDu}dx!^D, @"I@ßٟ*'vؘݻL3#݋Wkkk$KV !dY pЀ"lpf/|߆z+52boX ث"ӜN"Yi(h+F !QzW!#+@֩%:~ .R\~s'1@ gV(p$\``k p,.A<1Ba~* oxFKKK$ưX^,}ِ=2L&7SvrWk6jϫF9Q)QDQѝ"(SSS>K.Ĩ9Ǟ={_WWoW*a0LMNd( 022г2 "-܄/N Łx͛ܐ"xsaD"40(~fԛLSp x [)H;r.z6?2nMώ*N #w2_ؐŔ4m ϜcϽ\ahIۍ!1D911!"СCYYUzoLڵQ 6E˚rr]JU_D XX%0f+!Rca xjv\ܼ8ZLLr,^]G?Q (r7ވAi?`:0==^z Dggg3񱱱AOOO$" 088Y:[n?Njxht+0 ΎKx^$b)6}Ƹ E"άϦ["YDlAlabաCg nS$\0M1~s3QQ IDAT^:Ez:'ܢҨ&Д0p~peL/=ƭ? ~~c hll$˛7sÇc~>qӦM$NbZ^B_۾o_fftW& =8D"93\~(99GN/ϯ'lQV+''}___1ҷ///cpphllDWWzzzV}ED"!EOOOmR))DK޶oj?Xx@\?x% ^o^uR"D+-RF%i,&:R_L6,VF>Ӟ%~m)d,NLA2$l_?sq+fy/$ۄ$":H؟TO:=؈׽uNb٨?!8D=hԟ"bj& Y 5XXExR~/x~$8&\ҥKG>)IMz Iؿo_\\˗188lذ!NĒ`;6a ci)pc܊8lm[៾Wë"U $Ȩ<JeN' .Ė%J&Ԯ.(r xu2Iu\N&4!BO!PDI-/μ<Ǟ}ÓO{]Νػ)KRz{{C_D`ffe™3gBc $t{z~l6>gQJ+?&$3I*A __NEY71*ٳgя~sss6"3ưo>%Ԅz|?9瘘$:;;oFT*t:9$ tvvF2D?FGG#KZzs2*çq۾>r]/2d" *4oÊQ~x&؅@g%+n=V9Hj)]p*b_ɔL.bC)IDH817 8v~?>z3fxa]c eg#sss+2<%x+^!vrۈmףaEUd>{z{w,& ιB%/{ԗ&V'?I,/aUB4vܩtpM7FUJ<,rr1?H`pp.\ݻYGY@L 8==D"h@{{;ꔕ&|7ߟ ˆ='dOu57IDTdXܻ&mr@(vlb)) XaCFFI(r:i,XxwkoN5ϼu0]ӓØ](oMp`[~;޼:ծ[.L#]P('?IdJLoxMbՋ}[k.e9{q|ӟnЇ>UXXE!#ZݿPێ'ħ>)[4ؾ}2.7xAOf̌3h9|EtuunC[[RT$b@"NNN1kr҂T*H  wf۲yḼ * p~/b[_n7v$1/YT6Ӱ8 TY t]  H?Z #µۿ[&l޼YIA__1!dY#<n;wD:F& ]C}}=011֭[jp+߶SxৃkF8.N4[2yO/nӍ*xBlv0F fQ,U tӲ% 8+\cȾ֦5_?Ef'  >{KݺuUւ޽{}i/}||/2r'wNqq LLLxrWLB+++a HӫBdRIxC Ssx%+n[V$6/mH۳KH2AaVe L"'"ֹ륄~΢fnaN,%}%I?P f# x^8?E.?lM㿿enձjmF=?ctts… !zbؿrNôQ+~^+6WC33x#\17\wu JqEZAШ 066'|z+8HӡGĤa`rrB!1bIE;܄k?\J!/ع1;7֦4۲ݍ&I:$ a%"Ш'"E0Ƥ-l NIՎe3(87c_:ϓ/(Vl/Nȹ1 MDE$!ͬNhllDOOO xN?~Pjd|cɣ/09 gz}tM_2F{zz҂lZS{ƍصko:'oҷॗ^>ݾahll{bR@\KsP;~zctttUH0?x NVx]Cx7ᦝݸqG'ꋙ&Ն2{(Pt!B! MjiD|p,*8w|c1ω!ϱA 8?0qq~47ߺWݶHVh?ij>xJ73[ hVFTQvζ;01o8c,ՈXe= g)_5Lae 6;JGʺ| 9<@)߸q#2^el߾ݷ 0 \peEs=z{ņ :dR0 ݡ :qF #[:Zv ?xi& t&^mnK;vo@kSڼȐ۳V 0?2_,^'hM}gzO)/IJ_Z)4sR}LMx}X$i;d`aa!;pKe]lzXkބʉum9|obxdFg >ܶH /8*kWRn`>GȎ D[ncL!۷oǖ-[|ۢ$_ܶy>}3?aԗӧO0 lڴ l6Nw&A>yDT* 6`ll,epuq6|0upC  S00"pN܁&%(Z~nb,5π0˕`-=i#"tnf~g/O̥IFP :6md27|bb!CKKKx'j,QؼyZ'k#jA~eQ} EWK'+QU` W?==_m]\5k|({۷o\Ybn׮]ظqoA N8Hn6U6m22fnhhafBooodzzzzЀU2$} _ef}Uk_co]k?s*Xi?~wyÆ ,֭[7'ƑxCgr9ٳ}}}m!8y$C}ESSS%[}}}N%VĜ"%>_ oŽ6liVdɢXE(BhK%C]4UA8`7Qɾ`D>3;CY\9O;7n;Y 1tuu%v883hee?#t|;m+Tm\6t~d2h˗r.GX.-E˟TG-G00MՎNz۝: ҎkG GܑuÝЪ"=J:`\*vz8`Uܞ &c7ⷎ` h 1LhkkS|9\ABV2j8ΝSUL&>={d 4]vU܏F+()L&4XR@`6I&>:::v 0hooG(rMV dߍsO{3_ii *ίXnv{mh٘x(`T>w@i;" fZ Ry9_"_c)\~8FE8@?2o6ލScƱŜ/ j|]ܓ]V#>sM l6~d29YAŋe/X pQe;G]5uцQG+q_~чx{5[(ַ:@wTȓO;o(\ﶨOM0}Ő\smQD0 `hhn";1<qΝ;0L^(j,[pR.`0ى@ _P@ 87ߛFXq?hrq40a3j2faa3`a9u,E"#O!K!O!Ma%5j PaWⳇ:qr&#Sv8477XK8 ;G\p 0<6m⮶65}GڻsYWKuNNá;*^>Wﶨ.377{NMʚi00<< )F?|uu5^Dcl`&PG"~$jB]Fh=-8P)/`1b E)`3a1aa׾kF# ÀI  @8@ 3OpRƓ9pƻzF|PNӈ?Xmiif"\| 8QI΍bC{F~vv._mm"{nNU_^=|U¾zE-޴r=QH.w?'M,bddDrR񿲲jZ/uG:\QN4F#RI>WWWqT%dY|5_H(ⱝMtÏ7.a~hh3]^ yOh"2V8A2VWWu(вuwfQڹl; [oÏavnPohPHP%FGdkB?_>I|_USg0000%f3FFF$w*E099~96lFޕskX!X~g vֻ=ʑ{3.ȼ?Rp8Ί"p8.;"RL+{4c Y[mvv[nE<jyFrBZEшa,//)<ՃGzp{!/e:HN/`O 5AtnIC)VVV+;LXYYQ^1 NOq\Ngmn.; Ȯ* ZbE3U x‚"aqyՓ2 z mm5( S5}ZqfíYXrXΚ`zH0eGsWȴ<VWCC"ÿ ջ :Aw(ȹso=#g2FhP42R)DG?__+mjjZzvchhHT߹s5믆C@@n7ɤjT 333hiin@tz>2) 1F$++x2>ЧՌc{Z斗{C ^o~"x Fq9&c'N@ggg嵳v"Fx<.۩HHqa``0  hXsq<W($R8G"@2G4C<[zCQJ۠S|)\>xe靂@% (E·7[`Hv)j?*"[[[t:B^/#9/zĿ8ryymmm2 SU;YRGR , J`3pl[0'5/?qNmisq`hƶ k4 ]p`meBCΝ;X$A1(ѣG]rF-#K;J9t*ɥzA>x*>TSv;vLEInh 9׀oGEluvvbdV " !y&6bL `~~8ꑙP(P($kYU$ tnf! beeEcJdGn,&pyr'Wpna}-vބ=i_Cm^)@@Ғ!`ΝS=(;v 뤜/^ l'EQ(x\slPtN!r{wN_V^WWX]noodA055߯91_iy9`6 v؁͛7We߿Y΄J( n~_3rivpvyޘW&&]<ӋC;:jBV^f72I$X\\T4|xqq.\I$ϧ>9-n]Cw4* /A;tQNk 7zK{`&+< ۷ol|YN7 5'+^/۫_Stؿ?8P-1??Á暭;92ZZZrl`)LF:@0>{~o!pbM.uaweYx^+TK0ϧl۷o?If{ ;v*ka Oi\;t*aC9__wU͛!BV[lt~1_y# 5Ĩc+Z8>+t:!.]p8ǏP(TueYttt beeE+ M. nr)#wW1vo)^[S6 CwaF߸\R-B0::7n(fȋ<ӂ(oDb:ƃVӐ4֓_l/qo۶m[^xll p.b^)9"@wwwբvmbee<*nMMM:JbZaZUO,&#'zgu!0v!ܜ|~[ 66ٰہ}.8LZ~DBͯ<.]9lb߾}سgOu٨S0:(a1Z.(eLh̞N-(uYNx<WHoݺ===Η*EqFGGF2bb_'  LV=nAI;!K/~x)VQٌΆs4FwV)L/E0n͇1m%&-VlncK ;lZ@I,+˲pݰuٶ nݚ0 %'+% G"IZu!3$c&m ॗ^‰'$Pd28Nx^YKʅi\.\.b1-F#\.G]" pEUh4ɓhoo-vQ҆S-ŖקW:5ӐdFy&yAٶXE7 dCB uWʆr کT o6O}J1h4σłd2PeEQZ I$XZZB"P4^ Ud©SZX-n {9t*a [` m~"/(zXҷ]% 4&@%E?q !t3(RBQ[4MEO2}U㲿f}}}YGшvI6*`7nH Z+eC _a`0">VE\J׮]SO=^E@a~~6 ͚fY---hnnF$j-#S{X&#࣏>R$T, N> [FݍN-ܛu MӰk/Ӛ3#n G>x#u4~)uy>X>\'@(zA2D4MϷ|LR{6 Yd2addDDSߏ,e_J|R&)*v[exq KD"b1x<\.IY( vv! ! !Jջi:ai8S-E<ŋb&Á~lYR6N~9[t EQ0&MMXS^E/T&s'!uSL]nĽP< @(ծַs'[;e ٌI^eLMM˵_ D~r3#*EqDo`ppP)rd`MMMu_=ǃD"P(p8\pg@Qk!v6MΪ\8|>_֔-|xȥ'O,@T+QlHQ1a_34 R[NA `ߓ7Ěa6[@ꗈ[H\;m~QEwDJ wN/OON<)9v-..buu^d2d2"  6tPݮ~HR|&$ƍ+V~;v=N z=Ȭ7p1L'?l)ǃ@xsiGq1 `\{M`W" e D@"P·yp#ؑT'_kZ2~~w"{8#˾n0j,޽[ y5Fl*%K&s Ruۅ8~8n*㘙f\jz8EXko8F$ِK 7hN~ͦY= VVV UH/_Wƣ>ZQu#Pj 0p{rf>s2_ Eb:p<2kC1 @x\i `0`0h0aָ6SZ[Gg AQ: )Co]b߳;W_}uPh{{;,KVN'$ \/vK߿uWzJ|)dNj8A@<-m2ďcܻw?b]aw:hjjҬ30hnnF2LGu]Gy V+l6,$!UO!B>߻w~aM[4Mؾ}{cUP7 4obct9*r{9֜<߲ؒC39}^0404a0s[qI(!.& fYSHQK<_^1m(5K:a4 I gn3Ezc2megG3$LK?W>' /[ytG!Bbٌ*yBU4#N}&>IgrG8'("mmmsQﯙW!/VDzlzdng@`\n8*ϴ+`߾}xG陎p/!@A*B"@"@2D"НEi2l}rUǕ+WkrL6oތGqՒވ6Fc>/Q6ӶR2RN.ȭ/?ĔO |Ki:XP }nЬ2w''s(͛Guvvbǎm\_hIU˵=y\2,J|5ĿyW\ݻwOH~U ,S@]&i Ÿ8\~nRDUM8pv]^ Y6FcES͊!N(ՙJqF BNzX"Ҋ/~+_QڨN;)Бj| W 7U Cڙp977Yn+hƖ-[:ضMZJ)?Wcuu&"_-brQTB$e?,.~frl,--ᥗ^1<_Z2^}U sBrbrfr--! BDDLrVbAțb-Im#NPX,`0HD%!֭[~!5,JVĮ?ƨJ$GW(/n9VXR 77n딧zO~V @BCQl dP7 m--b󕖫)vuJu yT{Jq+FGGq:tHjbb`f]Mj_ IDAT !p@@pJ zj:H18z(6mڔW%l(} V7 +9T"rej jΉ%XkrQuY ÇnSvջ-JBӴbq|(ӌF#6mڔ%)Ž;Q>H$0::D"_*J:rłR[%ˇݻw/\9_h4gđ#GҫF=8KG##T=x<8q"Hdk(0%D@bRG@9JiZxB#?\): XP M EVB& ===yΝ."Ua_ˣ-&XE___#B'h#!D㥗^޽{1<>N6MRg[G'D"UaB^>`nm08x`"uQ)KPS&n%+Pj[Jmr| D /PyT: Xsn d@%ݎ֬a188KK|D066Up!+u$L&W,3Bdz#ղ8t$Rhn(LOGX X]]~ø~zݲرcyz-ducT(gR~^r#OMOA$JӛhaXg};9]xBw[``7 {2y2 ʛ{YR"Xy8xA5_L 1_9fY/'`||<-29^MxWc<#ZeJ!Bݮ'I<"BP]E$FGGqZA4FFFhE6(Ъ*lTBs8x^Fg BmF*(]Thf2Y>nn;g41<< Qj0AcC 1_ijŦM0Lk~9瀈 Ywޅo/n7obػwoExeaZaȀ qD"D"u" {Ғxn: L~FT#en Be&zBbI`b[Oy:8{l/q74gβ&BdOkii[eYHZ011lZPCWzjEVB8)?3e?|\u½Uw R)\v SSSؿ?l"4dd@! X,ui$ɴtB߿QD"V8x?-hC @K1R~62_.'LsQ|BS_>WTt:9{}'"EQ)ZBdּhٌX, {)>SSSYuZ/\ 1_inG__l/~"d2+d(7рN"?a:y%6*n$ٳgc޽An4MVUU tԃX,H$h4;LOOc||ṕb?Fz0s:&D&ӞZ^H2-mSðDwl Ξp@Hw(M_ L&A:::`2e:˸yfVF+9"X@oo"!oN{ҟ/`ٵ[ }٨>?OTwGAQL&, , fs]ۦSD"N뚽ZׇÇ=k]bV0@LaXʹVzD~r}׫swN% g`%_|???;w.մr'=1"(eFd'WB~/ҥKxT6~?~###yx;;{)&PCW0x<*3s bX^ヒd2)Kl ws?pv؁~qㆪ?lii ۱o> B֖bL$( f9!>e@E!H&X,i/"ܹ Dz7'q3bCkT*r)y-Տ(_\/n SWsϫ$ 0 _>|))(d1 rӉ⿜ȗ",K=755V@<G2qҥqr ɘц—%\|z^n%ۑ[7??7|DWWNp-!E L&SKFT*v/G-8wD^"zrpatttdkMnhbI PB u)r3Ț_ʴ !%ki PܰκGwf_xᅷ~mD'(l?GӴlO4neՊ;w`~~^3! _Iykk+?a47gy>YݙxEصkx \r%ˆ(u<Ԅݻwc͚e8.Ĝh˲S2K\QD"y&޽[2aiQntj]C)(^KBz5DV<+0hS lsPrA+*o1PG\[?~rf oSl[ǥ)Z|56yC$VLQmh=@5 @v޼pVڵK/V~M,//t|hnn.+m3#q!Jf{[ouR6ӆbʲ+͛7+_ ~g?RvTb1::m۶a׮]Y_Bw.s`0_ dm$<σypފT*nD~.}EQL&tuu#3T#Af|6SJ .K/U{LY,t^{5LOOg}r Qz/~׮]CB!oyn­[v}vl޼Y !$=SA޽:>q<"vDn~6[B}b#;`Xelo*3ϴ crmL&tvv捰b˖-eϯV߸q󕖫!)/$J,Ku/d}J5dkAՈa ٳ/~QvB%̲@ ˗/իƶm2TJ(½{pΝQtW!T&7 *Zu4xRIi^|S߀.ud;tTg#`\[yJ _ly#kNK=V 1_9EaӦMm$8RTկ[oe \#EvG*&,ԩS8t^u|G鿵X;[ _aڵ EWhdZklJ&8*(9'%YIJo|+f0^_;tT̙3Ax@gكt:z:۶mCwwwT ccci_jUKWrMjUD֯ƍ8..;w<ϫ&]ngO玎LLLw~̭}ffW\AOO.3>IR,&Boo/<"l4J;a0(W >u(9ē1])*𩃏1pu O>@y@\[. MMMY2sN=/L$@W!al޼fS@ DZdvv|>_zrh,uU}vlٲ.]￟vPRnSnݺ۷ol6}}}?q},--g2xGV-dQ)C+Ddv =CI# j:M`9hFpH+RK"shvfCQ- =_3`BAh'xKP7VGE9sB,rmoihj*XPZwT*r FSZ///͛E |>_A*ڐ#zu$ m6qؿQ\|/_NGT(Q.`rrSSS`DGG,ٷJE#U'˵ڴa߾},=3Mk"S 5!ŦTIWJ֋F՜H%4{_ # O?B\[mmmya R񿰰;wwA0TU:vey}0'[,#… rpWWsX@GG<hߏE,..6(&J )6FPUoh=Q569C/+JO81mqo:uGw(ӧ?@TGGG^f!\|q rbyYO_󕖛faYܻw/1 ~oV]nODa)V|=@jtN5PO?% ʭV+FFFr5 OQT:\fE~ZRun===mnY5֭[XZZ*(AxY)~ UU- <*1 ={`׮]ĥK0??YylN  q޽tlnfjfK/w?: "" "_]xk.lݺ5+zLkQmȷPHy9QZNvn!a>o.;tAwTij>˄N]F#m6FFFʎRqΝy6{I>jlȅ)Ո̭45t3䃷ȿI<쳖P(}kd23r0<<\6wzz333uJ\OhW҂J<ƍ R` IDATy@"ӡjVGs3s~GÇ155k׮J9H$H$jv=|Aoo/mۆz(Tۆ|R$BUµXlDT JeM?W_?S;tTCwԩSP(*ڲX,hoo{8555applVB>};w`~~^U |5_ =,Ea||P(<`-{tt4os)(~ՈήEid 0 i 裏pub1YJ|.vۍ[b0'Ժ@܈6rl%Ef%"Pm\BZ #O.uj)ɧ?iqo8(זjE[[[ރw.)BpM,--iFWZȯ䜎477 m3)T[/ORC$)q /ک)%+}CʵT?nw:*`rr1fffdj? Zǖ-[bh] 6ԳQvf?Q638RJQK՛E {;wfN:E9uT+?(jX-ݎS[[J>@I~MWjRc ^7]^hyNz}2xQ 199YS]=# Pb0 v܉;w"crrſNհ,^lڴ ]]]ePkb?. M+wOLF*zL)$9U3EБ)ȩSzh~N';;;cGiRſ=)0&''$q-(&R)*3ˤqH$066^ J~G2)2_xW? iznc޽ػw/~?&&&pƍtZиiѯX6\#^Xa F(% YY٠],fWQ;tjԩSn7nw^yWWo^r"X D0W!t:Krdb1!H,--Ν;%#mU * ?xWmpp!:t~E(* (?@GL&`ӦM鯧m6ϬS6$B7a_}??x{8[=O 'eE)+Ǜc%+vvvH|͊:t{7JělI^I\KH`Dʦ%Ye(3I|$@xϻ fzg3}鮪߯ﯪ!OX<1v@Q[hll,߸q#6o.["3r%IB$bE|6z 1lܸ %D"$ɂ^H)4Jsg}o 9#ڊ_xyű#(\S DQ6~a/\Վ1Ԏ*%t<;Xc/̂O,۷o16 LXf {{{Sn)_Lg&CJg`2JK011Q)F1!gE`{)/sr_ׯ>1\t d{pnذa6lؠ?jv v럛6$3[#m(|9u$IB"p?k?~pzŀp( <&::: ov_hr:~Kn@҆QnVr@@UW͓cxxtZQ?7o,zhLJLqފkVgSSmۊm۶"Nʕti .]NhTS~chmmڵkvZtttr9\kZI6"3ǯg%3R@f5,D%IB<.h3~Obe@;v9>Bծ Ntw/L&qY$ Gy6znjY@EE9G"8=tQS@u~@=փpq c||JmchiiAgg' ;]9FsP @ŒKWz'[Ǭ'ÞȋT ,(5J\A ǓwߍD"g"L:BͷJx>ׯڵky¾ y\.YDGGG NwF6ԅ<_XfΗ?ȬF=rQ B:T 7)u9YP/'M*f``7= j_}hoo/Z\E>J9F9nVxk g~3g`ii=b[(P褥β>)x9c mmmhkkÖ-[,&&&p5ܸqCuu C477 hkkCssslr.F|T u5rVOjQ'SgC3GRX_q\:\.s=hmm-Z_MgfOUG[zؼy3nwX \xQUۈŬ]n <8882[0믗஻R)ܼyӸy&n޼HAx^5{}vTZ٨v![\,\73[=Mت!_#-e&k.?rh^nr Bvx0vڵkK{hj*>`)H$ιfגoW+|L_~=+8ux{$.\:4j\8a{< $a~~>/(h4ZC^?uuuhjjBcc#܌f]ܑ gۨt;b_q.1WIUV͈ti)U@3'8pUO8Xeر9KYǃkЀXJQlT k}Iz+ҁppHcz+)+)˩L'-/1{ث!B?94jAzm۶=LF,Ν;("ՎzzzzT'P_^}\DE:u &矟ܙr PiwJCV"cqqxDDxjhh@[[b֭[~jD_v /_,VyXn@n}r1Zbvo Uni_WFl,cDgW{6 V&ok r`?MFS=pH@T G!jjjBKKb֭[rT#3roK[)_M#EQfgg_~022zwN8aq8T6/j NfhZڹZlzsf8ԟzzךb%[^ .=p:EÞDD5a¥co!KKoii1$rYKA4 bǚmèh7Jt:H$*gffǎs_g)|GskUf] N:W'eWI쯤Wbٖ4JLw\J)fBv$q |޽{'t}pi( PCw Z[[Xֆ{';Yj^(nܸ hVsՅ_iJ0<<_˗/f9: ş>|6 _nQ6V(JeuW^WE*+v\%HbH#orܿs߹2rr =r7C[[ :::pw}`.\TŇsb^okb͚5ſRy2D8F4UW\kT Bjr0K0ʯAke&8“u;]٨vQJ+}yZ @XPzG 嶫|9ul?i`O*.qﰧQmPؾ}]Xoc e]]]뮻Li9Ν;Gy6z7l؀՗zJD"g"+/ƍps;JGMK~N#P3Tp0#o# tșAVWk^9DIv<~Zf}.Mf(Pر>y@; ~lݺu;TV.%Im]ό7S7nDss%?!#+;###QGLD𥡡gv8OZK:.ٰևr{aYa `%?ůyUk@ȗS#B29B|_2`엓&@300V#vA@gg'|>_AƍyWF$⢭=f+ 3SSS%?"#H(;Hsss$9Bj3hֈu5V%B(B;Q6(&_O @n[O 7mp]給ſZ%S$ZD Peǃc;ݭZWR)D"M K! ^e5_뭷Av>|άPz@3ta4N/VP2D 5ښ[?-H"ޝ{D\e M T;v2%.CWWW×1 ]]]u|rD¶{lX)\.`ȈFApIRxo?|p92H;{mTK;2F~@/J:-9U5 ˩H #ɽ1.~PJX~>w}70"80RTżcz=z{{zy<)?T8.c 'NP`6t#Gܙe ' ӅY-ڨv"r+5ued:X8Es]'* ;v<9cv-[ڪ@9?Fߒ[))Kxj @@)_zrV<zHd:d}-S}sD?QY(lp7j( *\.{hnnVoײYBNŪ7*|>f'l,GNNNŋ_E/$&sYpyO_ӅٰFjD@rZMZX׾jSDS1H"'vUHN΅ 5Ѐ|ۍ[Q!IRż^Vy:re`˗_:Rz-R/8m3⚕giZ OitaF6*gZiĆAkf@ԯ|9u48}??k!Dy\  sFm566*x<ضmUsss͢)+ż`0J Ŀ@1qi[?%A}-wfkߖӄȅEӅpjigcb\9]^ @KO D3'dsfVSSZZZ }>n݊`0ZWI禋ܹsTBgכ ۛj"\%\vMgϪ&sEy_k)"Sw2}pYuPY6dÙ6˝d(e_J~Ӧ~2o5K'vNc"O < ?6"lGiq\! 0}gX \x@3/6szlݗ$ ###붉xZ=Z8MZKyN&3W5'+0߆68 IDATl_jb_Y+U<'7חtڼǘ!r>zϝ ˩#JiDKv.O'OJh@yᇻDQ|1ň:::744`֭n3XK>c rJٽ\1V[.(=8msEQ0fgg+WڵkpΟBB-yZRxN;juji'z^y֭wܡZ׈,!7??_!k/c 6mBccc zĿZy,C8F"(n7~ӟ:aQ|$ h3'0Z%P? Q=WN# m+?5yg!_NT:x2Z!8cޓ:&6cǎOrο@y>~tvv*>7n܈͛7#srKKKŸSR6mB}}}mn=|y<ii HDqI"ǃ_0{xpp5;9ԫ6^-8]ܑag+oNH< `n[YǓ1$Ӆ6pZGv=r%D5@MP,EOOj]-"_K̛e"_1ly6R\p8(].?Q~clB,wpW \+Ԍ"Ծ#nÈ򄱽\^_vH9@P|WRS!pTܿk.[f8&j200U<F֦[:D5CرcǓ?6j1yV^IШ"H$lfkż|˅>ly6R\?;;aHR6sg?A|Q:4j@u/jF-Q-$P^yZ_m#~ 2,M]J y$!Wj }}] QP9ʨLw]|FkO$o7ˆb [nſ͛7q9E/"x [?<,I҃CCC;HgsvNfNQ-$ڏ1K reϩ[eJ cVOiēъ {v ;vW5ښ)>Apw]D~R~,C$A*kj17񠯯^7[gkO)jj ϟWT oa6g瞛a!w06N_Zk@ܗ@@&O<"G^8Ǟ v._x cb;F씂s< OitaF6*gZiĆAl~v;Ig;va&Iߵ3iA֧_<#-~ ˅.Ew/?==U[j]p/vj^?W__^ؘw\|}8MZ\X8dvm)4G>8ͤMmL˷.*W~J~KurۉyV3RךTQG,D4$ ߏ/r.^YvڰR̗c6mʶ-_(&'' ~ۍsajʞ9O ts.Bm -8A@ڵag+_oq_ Op ʓZ%Kٌ'bfާ+U(|p99݈ۍ.݅?A0ĶmMʟWRah7+`҂ 6dSʗ%I1==xD"(f9: Y-/*VK 5 V 'O-,&ȋ cȼ;p gl/eYiDz_vߪi#+e mgsf|e)~q+9s\)˵ Ml!( |r{~RFDBl߾W9Fx^ttt(lݺ5\.zDZkpʕyVyuڰnݺly6R\/IFFFӧ177WPfBg=6g92٨0;jN5ذ;V(w~;gȄrwyАlWb??.Wʖ몉}`G_3;]_b(#bwJ7 j hdǎwsΏXkĎCggg =455a֭%r1C;xw**+ Yk׮͖ns((bxxXQ|>XZZ*( Έ8CgnEta !kR_ vjrž>ܽsߟY .g߿U0(TWW&Jjs]&.S_s6rB'~z"ӧſëjxĿ}9ga✫It;F3Bp}i!?rlюr(aVo e[gqsj%J3 u2rY5S%1^xܞv)StKX-8AI}b'  q[ijjBKKbٺupw(ijYMcy2+`ӦMhjj*x'SˏbÊ{<'?KN\ÇiՌžՀjѕ+"+m= ׂ<NՎ)Jn]WNs^L+3߯z *lSi噾hℼ.[+EQR|ɔ-s]q`m!Yx(hV[[9/vgsssy&ɤaJDm7ˆ"TAۋ@ `G$(~'"N<3 Օ0<<\ c8uꔖ[ N 0߰j= 3%܍0jF}m5C{/x~r]yaZϱlIEUwaѼO_$DQK$Lt~cn AP;>9Q;6mWU\D0D0ƍF199ׯgg/&H$׻_,j17v߯*<cXXX@8V?Lt:Gp$(vf($cGps=T_1aOk[.^7V v)c))y-!BBarIowTXft+_ |>_^~ l'7nF<G$A*rW;,of^?55p\lM(cpp<&SPmG]0Z~7Ŀ!\W)Mj[NA崾`Gy8ܜ `A`f/iDQDK]Y'W%A۷ox@;[l<C ]% \]+^,--axxtjzĿj^7oS8on|y<nܸ .(Ius :vCZTݬE7]~} "cUpM/.j:~e_^kF{+Wn{fff᾵`Mj?-]D<3ͷ́1Ɯ!UΪ0#;ظ~6;-Ŀ͛|2n޼Rb^ 5o[apׯcll˸z755'N.;@XBE?P=_~C0W(7ìv/?n~m3z}W=ءn'F;$I%bƖJ. $7?Ξ+ 4ju#63LÜ1`00VAk$Rof@17cp\b\{J;111afc~^ bTWGgʋ]['fr3)ʭe_N{W+svvÍ) Oi,Es~yL2OnAXx >|ӟM@:F"m۰i&?~333Ŀb^o@ ^+W{}>"ao Vʫ!/j@ F?FlԊڑzG,ZWN;݈_y}=~bQSDQD4X<~ācia={+ *~q[˖-?Ay X&STv`~>}pXQW: `כ ۛ,!S{\rŋq߯Ny s.[C4K" w5lMl#hcOqlK|A.'P؎*cbrfH&4dѢ7$ZBD@m*]w݅/~yB_lق.kvӓ͓o9FGG1bq S{ 9;CY-Sh\=Ύ.3P)8M)*bb AmJ RFJF}0+qu|f]0՞E2{'J7 c?PWNŎ|_̛jL&񵽽Їczz!755aƍyMײ=w GA? IDAT~ohc+CCCcƔ +?w0j0CÎv凮>kg9ZG2UTOOܲRCݗ֠kyڵ -8O{g" ,gUH9u}>/n%vz-\rb˯[܌ 6]/Ŀ$I8wn,8 W_E2i˰>;2cҬxO_X~[osnzYAww7-dpXqUgpC7r8A*1g;C8v}bJ7 jc(//;Ȧ=|>i%"Jn [lӧԄ|{7l؀VK<G8F</J%Iz^8m8ȀWAn۱2E%` |Xa?-G}tM* @rojj¾} zKV\&&&DLΡz7n܈_Op@MgFA?5idsP(:oʾ^t; {`3`k`|V-ATN?y䑼%LsDQiS*ىd21C9_k>IUhp AJp=#8a?O J7 ˵omѮ{1KND-S#@%̡ O=TvWכ 09 6~LLLcACC%qqH` Fxmx1?88x./TAu|>< Ov7HŸ 5tرcGV3MmW?l޼MMM__7y C}}Wz\t W\1vs᱃rH(oqOUA1A){tcp>5; 5k>WK?A_W^U `Hx<͞zzϟ/~?.\qc^;xƍ Y<74 cn AQn^K~Ȁg&fcQudmqсE,,,. x<l޼yĿZgAp7n wN8Ato;c}⎻6ݟA ~[;U1cp7}am -j9a+ϧ{@E86n܈X>}}}٠\ׯ_X[o7o: {E]p=jsh q L "\.UHkBq_ 0JC=tSgӦMعs's_ZZ*Hiʕ+rm~eWf0Ssq5\t444_⢡뫃o Q>CPBAr%`ɏ]8#(p?g~|g"QA[~_nx Ru g!"$QrfaAp ` Lޝ{O[ LMc/}T>[lED°czcX_t ׮]+x饗8oB?a*?ww}KJ7 (9tˋ5/GIv VV` 3pKmkmm.MK-8b Ymmmַp۰՘;Flڴ)}R6מR~f;::ɂkԄcǎ5r#wpF׿u_SܼD b]:f.ΙK`r ,Sx\Y $5oeMAǹ}?q3H$ ٨ÈF|Fddp---8z]$ӡP_pFAAaGM#Cg`㮻.1W/l1Ŀ$IQ8r]? WIAAa'5߯ΦM,7$L"Fcc#LzڰaÆl[[La/   "CcmmmFH&mTuY/ގue /oZ;OȊ˨Ĕ$Iz.AAAj.9M7n $ɪl[gg'b[H8{,Qx^K&]\W^xv9$  ȥ֕>dL1eq-gXv!_ww7:;;~dgϞb޹e|:c8|v9$  SSG}4`:g_$hȆhjj*[k׮E{{{֧/6XZZ;/ۍD"Ǐ~T$/   B ^o+bӺ񶒙 0Æ XfM zFq9sh4|<o~T89 /p.AAAjZ@hnn`|@- MW]^bިb8{,x޹~̙3]r](fC   bT@ Z d]`r1ӃqhgϞE";@ qaSM~z:th.AAA$I-zonnc $IyⴖXf nwC3A@OO/վ_ZZٳgJpE\`'v?X!AAAj*L?l $I*zA@__ȏUWϐghll._lɵQoܸk   B+5`y?ԑs\۲%+04557ĵk|9O8q"Uh   Y :?C Nhψߟ-'ϠE`dd ڊ'N`zzk_/J~qm'm3ׯ_h#Gǭ8}9 >m3   3clV9@XfM:eGV[g||cccD(*X"DgAAAfQS3z^ZZ`hooW@7oV,Wڗs\Rc 8|pd}*    ̤> d ]EZhhhE<cLd~Z> Ckk+>l$QLJh?   R$ |)9=@5Ғ',.\( CCCvE HAAQTr IffU3 -+ޛcܹsqF˅z=zԚ,d1Ћv9$  R(N뭓]62X=K f7n$˙`dd?ŋ/ڦůK˃   *j*r.ԙjdu ~zg/52@$D"Ȍ ^sKY>ˢ( /pAAASSKzLLL0pݫ3χ>+EQD8\Ϻ:b1mA#G   jjQF<>>xXe~?~?LO8{,|ALOOɓ֝X>o C   j10K.!Lf1fȆSd~cԾOR8{,|666bbbgΜax   ĉf, ,/WJH;LϿ6{<] ZXn=9SΣ󧇆~=   5xx8+@,G/οuL@E$)_CCCKZP(gAAAʴ ;pqIILiKfHV/AQgλ1lذ*"8c$  Xd?[_ΊV3Er준  r47ny0ư~zn_)u3a)y0"""""5 P(*8uΝ `~c;;;[Լ\.Bxޢ #\lE\r%> MpѢ]'u]% QPJu@&3<`fdWʾ_!ٮ+.\@4ͻT E+e=mY_ؾ}A!Dx&''6dSs]fe_鰿XaLNN}_WWY89 aCv$"""""ZKVHS~?-r^t O<<nBnhT>dƍō7{Fq̙ľt]_n""""""g 䍷I$ػwoޔx<^AvEm_L*n޼w 6O*z0+KJDDDDD\B<ܵXnz!E˲Jֶ/ƺui4MB! FUU^/<n-3R8s ._w-yBBͥo%"""""r6Gt@&y@===NL&iWWW&%xPUUe˦?+155w}ƍ8{,"]n; g HDDDDD9p)!o Yk&ۗw@"ȝͿiyG#>8y$m{}!s====9555o[EDQtvvmgggsU:˲`&RbF?~W^+D"B0,} QqT`sJ ]ݝ7W*˲sŎX,L&|Ph%Թ}zADDDDDTEHd)R J?n!ġCgٕu]G09:?`^uNy*E&A,[,>k??QpjSSa!eGUT* ###J뿴#Q%jy>4%X_308}Q!d iLvdrюAtuuu90::B(x>66i@J"R sssHR %B|_v#"""""r D~Bs%O1<<~>i²,h!D-L&x<N{a``T^uv$"""""rG hjj <֭[8u6n܈Xt: M ꝵB"X||{J'nYsa+ S8RJz%I8}4$~?HRv.@v[Ç 333䢔)oK@""""""Y68с^wU4䂍?DQtwwŋvRΠDDDDDDNS)>ߠ IDAT҂xr\`*B*Ztq;v̮)YW, >3(ӰpW{L˅VaÆ kUUUp%h&44,Z}*B?BJ\B9 hooR~رc>~V:0P)4d) `ppx|E1 4"x& #8Ӱݻw'N ؈V.w]J )%B>J)},}L\?+ܹs1<<{VW˕ӰpX,wKGJ[b۶mhjj¦Mzl[044! L: 1]D/y';~dWL ߏ͛7{Q__/]H$033k׮a|||2K!a4Bey瀈X o(~Ur !dž5PJurrӧO˝ p^B@AxKN?~D"'lr@@UY#fR1 *`vlY~;U*gu]?UD.M;U*i{{{Gʝc z@usY%Ro'"""""U [ Υ>RJlƅr'BDDDDDD P$@Eܹl[o߿uEHDDDDD`,Ν;?&* ~̹X(@ <  !iY *w.DDDDDDtw*nsmBrSRx!"""""avlYk~W|) e> }Td0_aM"PIENDB`glogg-1.1.4/images/glogg.iconset/icon_512x512.png0000644000175000017500000020612113076461374020275 0ustar nickonickoPNG  IHDRxsBIT|d pHYsѲtEXtSoftwarewww.inkscape.org<tEXtAuthorJakub Steiner/"tEXtSourcehttp://jimmac.musichall.cz/i IDATxy}}uw]}Un 7H@K<[ޝc؈ {#φgzlc(:(J( A&nȷTeVfV^]U0]˗Յ}{AAAAAAAAAAAAAѤlW_ A+˧NJ4/p|]1q#A1ҥsz!H]3<=l քs69֙3gfn;/?^m0c=a#Bz]_P#(<ߜ䭏>p?,]"m˺ ^Iƍ<۞v\O:Al$IFs.P( DݦAu @A- hAHAD B  ZAт  AA $ Yw%ͤP(`zz Fw :ݓSCquQYjZ}T '׾5.%mT }t7]$ O>,,~x`b-mJ$oK/=!-ŋu0Ala~`||}6 ֌ §~x3]A4x睷ϓX'^|^cǎu4'HVR))H Q_\.}kM $iW\F&it7ئpwypA)@PۦKAb@)gLMMZ& p8Q -JP@oo/D8_EړCӡDƄޗ_~9t5u 0M)M/~W jnsN9D Sɜs avvp4šCEϞ=?l7B/Dє NF{{lBdr=]#-kt7jsٽ{^w  ɓ~R V{O bӔX,sD& "^zFgs|OR)CAl2  AA $ !@A- hAHAD B  ZAт  )+n&b㘙A*bґNgCg?6RK$I($8/N\[Nˉ-G 9nܸK>F6mtw VVVB>pCviA`P(d Ǐxo_[o bh!B~ ~x?AT Fw rs짘otWVnQdꫯҊcĖkW111n t& 6ᄖBW^it7p.y饗bARd(Y "?N-5 nܿmmm 6T*GƐ  >L̙M s.ɴWL1'>Us|!ZAZbA`0jnkk8G'JLDi)ɤM ܦ [Hkl{c/JzOncu`>okSFc3$A"/N׾F!@4-s\r .}AFw |~tj`\ of_z}khhJfgg_3|;{wVp|sXGP@h:&&Fw !/ɫJ3@h*8T]!b{`}ri4$soTB'}~܍H#!@4 أFw @wo?lt' iFw V]h$$ ASN4 DS055BnZ0*@*D4d@ ~shoB$IӷHZ$nRDSd㧎 ۄH$x[Z֣!)A4ktET*Rol@RhRΝ;Yjn~] h)h  ZAт  AA $ YĶ#NQ]m–$ l! hAHAD B  ZAт  AA $ !@A- hAHAD B  ZZ IikkkzFw  ФC4Q7ڂP3V)R X,BbbH€F~$&''133,,,X,j pB) vr|>_EVA@*&&&1==Fwi0rr"B\|Ns@&G p}=|) $, Z '$H$4 J4 $abbcccxp T]j8 s\.\.|>O7$G6 ŏ. ܹ۷o7n0 DQT42rC?:;;'>us{  IRsn߾ hkkC[[ ~?<^/^/<|>_MQId4,$$WP$&&&Fg3`0a$6DK2??k׮{uMpc!hPH1~ U k~@TKcyEr9$I cyy( uoX^^qC5ւ2EիK~D{{;0A(+޽Vz5JP(AffQ#o@ILLL`bbHO~fG:F:*fgg155u%fY\t ׮]q$M2 bۑNqܸqc͆?bǎS~?4 r"sL&~ٳT SSSX( z*nܸ㩧h2ω͡y ?@ @lrΝ;ϯ)appÈD"JӢ cLtuu!͢H&Jb׮]͛C8za bc OȐ dM2ߏb1%  (q@6EWWvލE|SNannn71::@ A z7׋ntuu8p?~{aee63 Μ97nu!0%Y]];#wt^8(vQ,{B'/+W+-[y@}kl ,۱k.LOOݻe/#o@[ +H[ 9>#/JDgر1|>DQeF//+ Y n ׋X,C"ܹǏ|xo駟Ƒ#OҰ ed27QgFo> (e򷷷o\tuo$$)Une,_aED"F޽{q횅@Xq]8q=uv~)$1Ǐ{w8>T z{{2;::6yd2d2Rحr!(A Oq hH~:jjkii ?q~ iyD-п$l k.oggg] ?td rrֆ@ Piy1D"%"b~~W\v8~&&&ODwwh^OY!@4-^^Qr(ž}rrсP(THT*z. LȑneΌ`)k%Attt`bb׮]i2駟SO477$SNi:Ven8 pξ}}}Ҳ%F/M&蛑畕n7Bкk#nD"֭[}Ip<~[z@2[DSáC%g=F\(-ޯ'|X\\G8F[[ۚEA__B^/4,033~p+YS?*kS& @4w܉ÇrAEtvv;ܟfǑH$2-o"/$22r`0H$Gʕ+xr9OqA-1$@/2 $IX]Yk(L[[{9ttt(\׃?H`yylvm"bXZZB(B4]"I `X .\e]%|ٶ >2P,&&&ۇڇSO)^,[JrDKKK5"cee+++hoo_ y1E?~<իWLNN[ 'Y*^wvnBhdu^GE?RظkaU,--y"OQ(J8Ε1<{yDxB~\6#H HY0ى@ ˅.?\?|XŞminWݹɓ(Q4o}H aiiQ< |>*^X*B8p1n),g+m4+;%.X ϧAV8Φp6l<#Ca`wo Raee]]]5/x " >C^/VoA,h.n 6I0>>nHH$_|Qcсh4ZR6trn/EܜX$mW&/e\_[]xvi]?dP(0==6tuuTANx<8y$._::w߅#Sf4Mam)HNP2qxg,^|5??_SyZg9)t9xCxvO{$4:::D'"xO#ʕ+j6̺\v8/D@~SX[ĆR(cĻQ>|XyNy"\KsESWUH^7 wp>^:Ѕw~S$I<bXMNn0pEGEхoGJMX Uڜo,$ #a||2i1G*\~?z{{k$ sssuOYK3HdZ$p"SM폦1sOum L&qtvvT?+"ٳ& ^/G:(d7Ćm /@(BwwwMT (a~2޼0-o{o&wۧz3`YR)tww;2100A+̙3X]]=/)n8X#_f3MamyHuX,ǃ_~h4 ,--մx-Lt @+"a.>U-gE"9#'鼄t#]H%ijV҆t87K^r@)7 ͢q"N>hiJp#I9dk>c#A @ԕbh;pq%[/E*$<΁sx01p!6f]+"Yt"Eij5t>.n|A"B5E‰'pi,,,؞D]$߈&ml$MGoT7$!I?~lN '5Mf?{;nb! v1eB"ڽ"ڽHO<[J1(`>niC?j*뻺u/;7~>o/ ``vG+|f)xEK"x5{+<)(9~xn :<_~:vȯ IDAT % r99 0==cǎ󘘘=oIp_Y׵Xިn e@ _~Y15zsfpu;noþ.lGmcGvGuÏDws>1H<a ;#$k"`jj ?<Ν;H̻\x3¯l0؈n Ф0jʆo$333S~?N8,[˔/9fgg=ʃe|^Z;`PDGxK7 `P`Џ>Iituu9(G&''/̙3xL77$5s-:ut'P{{c/{}Ye7b)`8apEWtxp1s=^Yҵg̯ w?s 5F!I2} 8v}GH8`Ϣy ?|ƿ?&fggo&䉢cǎ)p8Gm 8vrM|Dat G$XZ `AT/XP˭MԊpf*TB|n<;jočX\\DXDW}4A^?`bb/}]bAmADE6i^O| {H5N}t*c =2?8zX SSS|7`b!״P 03mHp>JF˿iޫ\ƑILdJ~+)7`Gȅ p%Y(y߾/>ۇ3k)9Gww. K/SNYV1 㿋/.Լh>w: js7xr=Ç@)QBc>]{jݢ=h?ЋPX1zUmS~iD_6?:TESXYIay)Ī2kaӃ6[x+5|?:?_k( 9bs^z m Gv|KP'?@hjWSj+l-H5{Y>::Q%ϫQ!ZQ럏Gj>3}LׅXԇXćhqA3_W=N5 @G{+HX\a1R"W5v:Z7q$M%O*_߻邫QIDq />)1~qN8m*yD s-;wt__> 2.Zj[x\ }}Ew1@6|鍌|=˂@yU@^>D@ձ#px\z>D}(9W3gOK{s<0~幾-Gp8>|W\=O>Aww7ۇ׿U @8byy?OLAP1jcn&*fkN(*'G&WR3i:_E$ůw-ǐ8J_KF Ԙ!rE<ؘ9OFwwDz ?@^D"Ix Ӭ|˅cǎv(- mczzzM&VϾq&E8:_k2.QΞ ^9؋ww++y.NU"LQ^&ӊ~s}Eh'vs;[׎=C>=Co}8-sSdc‘#G(QeZƟփaӧ15e>3(s~c/ifffME~Nk _va.xjh8o#y$:^n;v %=a}έ_ mm7<' ’GzR$iƿ_ſǵ?8/v?"ڼ~Ȱw þ0<.5+H1jn佛} L& ?3ő۵G;_822fڣC̀.<󎾷7nGjSks $Se.8p@y%qGZ=?hU.WC50+{7 (YߪMՂHXEDXsM,aفu?y{G`&g߸ڢDr i'Ipmmmb$?{}=P*$)?LnM_4UV"Nc~~|[O_ho~zizpb騄2\_+l:;8<E$H r `.uL,VǠX,bzzѱX . WWYd~M>俱ߺ w>GUVzʦg'ڻî@0<ű}Up`T '"@sP(=5u<" c/h]g͡XJo֖ l@ 1Q>Gp޽cD#~QE.OS###Pzx:3k͢Yͩ1G21Ɣ#G_ A~BX[oe׮]JUƘ|I7߻ '_~9_Zv#FW\j/1e 󶕣"j(@a@ P9Ĺn9u%2v9!x>;/:_sP(8zD"pxꩧm+Y׿ B B… XZZ2pA}GGmsYH˩1Md.Ďg!dƀ;8.9G@ 0{Uj1Th0kU}1<`@۶0$ }!Dgp0>l-V?_aI&XY_3J +R.]2O^3CICMYYYٳgMsQ\#^/Q{XSDOy\#a_r(yv 1Uҙ39eN(/-r6_;VV`g#^:2X:\5mKO[GuիW _w a-Ԥz199i`FOOI?Ncypw?h%WOpAxf46 ch\󫴏30aZOޫf=F4kv6P"ܛYu\IKG9ǥ[ӶǾ~zC5χP(U>|.\lX,/|~~  c6;t4IriBp#<_{?h#>}1gkǷ\x<_7xUsM"vF j K\#8+f{{w>/: A044hG#D[ ,8p@ɆQŚV;w{`8ƀ/؏ۀR {5 zF>ZeFa2^ {k^K1gGk>3+_95&|G 9rQ9gX@!qPwVwttw]L/eq}] D< ޲rkQCPIUʛ7I.@MvsլR-`OzD/ۍN<4G+q D"xɄq=Q7;bΈ1 yF_E!ǡMc82Gsy`r^JT/7/]+Rv:Wҷ+U+߈ܾvI_ W^Hqj/>]? aeqq`28K.YN+_n,7 ~Rk 6@} ТH}12^WYGlDŢi #nټ?~$IX\\ىqܹݳlxH:B\IZ%ҨBi/oqqqd?m[CBt(0%kjhāB5 K)D~#ƮvWVVlu׋60ư~6cMSu}4#$Z1BEkjB@fk>f px$+px(~Queiz"U+FLiS^)%'V;;; "|U)f@ lڵKLl(cq}p]:C[*wG嬎cs[?$ٛlsd2  kv f$kcϷvzofHsӱz˅{*~?</R)~"M}=Q&|X#N QTe#{vt#}O"!A} E1{Y:*dEA3#!bP0^Juݚ*Nœ$cfZ,ɑ68.<1^{U-FMhvp'\WmbN&P V؈D5;ppW/ܮ{'^_4(bA>wΌ;*/ VRGx 6Ao@kz*h1(jx^x^S(t|o^wqQ`88ujlhM̯lzcyƠnsq6̮(.Co]P˟>tҒ(@,sT(7|o%HkTNԲ_E &`(le/__z]CR"\_ G h/9dz:_Dp`G`RS(ly<eZ={lE6`{oEHfzƘAv<~5DTL9{kXQ`80w"^تa_CV2yHUo^9VsUN+[Cx3I^Ik[Z v0W*$Zl6tx,tppmm9F4ӥE G %`o_umxg_lȃj!`l.WoU_*Afh_g.zȀ &4QU3A )ouyYwf)GEcL8k(pm}G^V@`~ `ZO&N;ߺ8YĿ|=}ADA6O}y0-sd6*}~r]5`reDUN*4e \=Y!@`AN`5m'Fa;v8Wl}WkHHw @$NM\jS?Ӱr4 ]ap1(OAiOQ6YiZ'p6\wdmMK\/w Ys^51,0Jc'DDFGR٢Q&#LZ ApR oHxܴ:f(l6x~8u`7S~g;fvya$V}\e 2+BAWJh1`Ab,b*7ԑ́C^u&,=}^۲o^ª+v3cpΝ;mK .@YOƿ ФȞE=~̲}>5].?%~pzK4*]}y׮NE, ns)O+z%IPmyjzzM@}IV߰gnoo>"[mߏt:]vabZ\dK[l _?6<@!r 1(A؍d~uύ{L֚wUFP?^1Y$@nG{}VOA+ʞZ B:JЋ7\ۢ|D%)PelC1cd7ۘ쇆4t:b~V͋{& IDATzm#(׽2 {c(B7Ԇ_o+G)ύhicq2*#WuJ$Acw!HaeUJP!G,J mTxټ\/M ^e1=88(PFsA^+A`cw\Sދh?'c6كBO{)˺lQawf9P~S%ʃK8Uy}@ӆ[7>^iYUX_IW0z23}%=WcnVD crRH+Tnyi2*Hnbamq_vTX,"N;|moE( pdp=}dv$IrdUnTYshgoph*٩l٢5U̧W6Le *iC'CU,˥$Ziv_Bm+bDkە#dPԈƁWc6$E˟<@0Wo+_#UtcI6gm[_7=o%ǙOa{H$,d>(X,bhhy<|K^!Q=M>͂6Ν;!{}_N3C~x0)L~r{mơlxe9LF:gUBB}cE,.4}-r31{Js*댾QwiWP)|AB αZO6yGm%I'@ߒ<300` 2u2h_W26dllh=ldCi7-wa сN\oi_Ufb6 podirf|e}%ԭx! (u*A^f%#Ԫ@ӐV0>2PR0 STަs> j/KhZ^K3fXH8c===59so~kC`bwݚ pݖm9M{ڼQ(-֎AW{]\@ϫMPBգS(ǽY6b6ՉqgU2>Ti}@/F;иNIg`*a\ hea6/-蕌 V8 06fT6@^?6sn۫ ޿Ԧb(?I5l`,K04šN~6{6>WopISSjۭj0Wy6pSJGp}$Yj6$LVL1w-YW{ﺵ;A^?6crr4iOX]IR^Gk:/3ۡu\ OoJsW f6tyU_(M -׾ΨT+0nzU7_đ)S<$ 6v,_yr~@yq 3cʿ~ۢ@BSSe =$A@Ov6dtCp0;ud*xŃ.G6WL^e+ 1WՆZx/4cڎ> >b+F[Y~[VjbЙvҎs״)Of{#pW^,l]aW_2Pm+P۩W4By<tv ImLo$f$u#$IrMarz\t%3^yõ"@V1p"rN,gU벑/Ts ~=rtBI${a *VU r4XCg #Ϫ>}0L1:i&lD݃ǜ-uxV'b} ' UބAdrRfG$XGж* a)[[`iEZé" a3jo>ZF}~(Y=4V>"j_ (wy)rT>di)pH@uX;lws|I{kx<ʽ8I\\\B:!C`111aP@.g? ,Yb^m^M/Tms PޮkD!|83VڑY~ az[W KcQ^*?SE'IF._D6o>(7$_"?Do"qYҸ@ߗ>f4k6lrjNj/W@53:&?*ky)}XrK(J嵉Oεc2LUk>=Vw@< \FKXƜkېs S8X``9`= p0]< L5c~bF .M6(ʪlF?$Y1pwp4s4" C` P Q~*.OÓ~=җ=ԉ|FS3fT RYH=_zoQ)P &wy!Ϋ\A~N"`62W8Z!0͚k"lNjyUgo%*s"IŢl6kT(Mb`CR{P1I1buuoѥ4vcǠ,̄ވ_;7gߪlx)2^=8];+vA̪7t+*)Y(JJOsmg?ZTL(G  r|:2xsC\t^M^jrkzT[xe`y:ˁU\1iܒޫWԔf*H>R)Xhew(!-(X5@՘;tּՂDG!*/_:DdJ+Ty&h3<F^Pv Չ:ƪORyHG ArVR9PC1߲/u4H(ͨe w#2NIT:ҦwhD PΙ5PI}v@NZOpn+WM#o&?\ks95jsƴWN]u?.(/٥նgznkZUZQ-FIEV0A}+`kTUJ,}ȁfzq;SoXZ8d2~GW>bSQk铉` ;p6E1RVI,XAz cexU"zUoW܇~z}̆ W ꋙPH.`4iR yXWZ OsT*Ƹj/Ӽd}P@)A6\ݢl5m̃r\c+}Sޗ4Ʋ:|/}ZT-٤&[ִ -J2%d "BaؔƀKА5(%ť V=_8qNčdWUt۽7,kVPrIP+Fuȼa[Lpۈ~"Z)f( bt% :-'GK7WR$0$N!>Ԟ={ +X<ԓ$; hBO_,o"㩹d[H.G林2:\p4:xɴCF͛JYmKLfp! ӵf`5@4pUqBڹ1yJ FtQ՗,#K6O1)Sƚ 2''!?| dM$OEɊFTe | eߞr) Up$Χm=%Ӥ־-+{;B(:ǔ.6O<`mId!3%!%`C)E Ü#Abh(`/Ie 5!-Pk=RiҭbMfNL@e<?|@QҠ/mW :K,HʘZ9%-#"_pB;Kܢ/@%ZC?xO'ӢhSՠ8mM̟_XG+ϋ%yOPN`|-SG%ťh-Mm `#6&xJa{ӫ7g"-ne,hR~w_ ߾.J 3(e6-$dtm@ˁ!GfM >t28Gd.^s 3:hZ?`5b0TP/JuVlYqlJ!>-Au׍zܩM\|5TUmg?,@3U:fN7cBn3I;WV14T:fu!l?Ldd3]`̾<gg<OX@$@K}ι_PC%zr>k5V6x-00RɁR#i/OK>Z1amx ʆ~7 ԿNCVuD`$% f?5j:YKXn RwC@$@)&lj_rX1+`tO}vxk",x-'Zߗ,d<dKv0pyPH4'U'bʵ dK3(9@ ˄tU*}U>.STe:dXk򜴊ןcS^Ljؚb2[5L2_n+M$Db PL({LiZ i|O;b]ֈH(`+Zo?WOx݅ysoI d0\J.gMPzdU%[NL, qǴ{ ^֝@y^_ja I@BL( G~%R}C(r(`h$Zg9֕r,o|1|߼h<+@H6hu3ptZz =z/Lũd!HDE'1L& NFbP"Hbk֗fx,7xl ݕ5Â.GAHb;uZQE|F ZUPqoͱ>I`ߕ.HǙ[,.eXt?tdBdwj&A)q%h8 {X(UL\h5?!0Ks}Og| YbDY*[?ݑDV+|@+`-I1\vWz #byg LOOŋs7pߞ 9rTyõ Fy[__G4P u $b6 |C#J~/O>6VEp&”sZgb\:o]RC<_$ϭSK 4U IΠ;]/H^_EiX:0XQu<{* qƣ]u 9Z^1^F9Uz2 мvC/E,YSph<)UXJG304wMؤZ,k*I Ω$?o')n})܋:R15ͫwsRXhv}ނ:ZǴUn^l@ˋ _Hd=|F\ZZO= nՊʈַohW\_nW i G 2$eX]_lVDB!M]ls@(at:@rT;)1A^P 趺!:8pN_[eQ:.'@)|T/5ì IDAT˷B`Y$& &ȝ@?N* B5Uic4[Y[hth|[ @7@ 0L$(mHIج f`uH!qp\۽"Gf"IMe<呌tמ7uWtl#5YCݻ)Jlk5Ϥ +&+w) XX5`5 o?q }-" )/Mi3aJؤ!cj:6:Uy#2@wphgdK@(6ҿ%]z=1OzY0fVkgDg 6 !0y-]; REZJ+zͺnkႺ{^zIIZ%%2|hkuW1u1DKKMbN[  \8K  (_j#g-Ni[m'q;?{@H;/Se}%.Hp+v%kFx~e9ȏO s i]zL"I[AP=}`/ʷ"ļr+`w62v@0iCfiF)Zm_!Y?56%(Y?Z[kd$(6Er>K(?m躍"GuNt]v!"X[X3Zk-1ϝio=Mў(1= i!*THe 3UvVCbB2k)+ĺ.ʈveӹF"i649? 6FSr.ۄvG)5Xcv&,Bq&R1 Nƒ#)qu&K2׻'H׎xDiT V -ߘ_ޔq_p)gǠ/%PfTy<u3_6_2H@ 0\Hi]`Vd(\ >4}A}sxJ7Ir)%m8C40V;vfP w1|Ly jt NS'it : TWG kn+zl5yV=Ѻ<_`>>^ȿ}dYLyOAHS7)$gq͔_|-i E*Z9a R'Ӥʚٖ-LUFӅP&zNjk@A UX7Hn 1f\iʚmm;}>k $dS۬S @esֳ,*蔰:㲵#*Or_ KeI7^̠T&y&=xJ$*䔫~R6W6Hm/:ersJ*r]ɓʟ p5 w3FՆS -qfjYǚ!I@7ŀ%]7v@Zu8H{ev۽w&;/),w_Q^ ٨YZWIceHs ( I) "' HQ"6: oLY9Begx84a$%Fcvn4uX7fORv xq)| c](Eu= @Y]f )T1[<`ER7Wv+2/d 0 ,t!bn: 9O{}cL @ٖ.5}`sB(9)ʻޜH8 ЦOHg #Hfסڹ@H]7wڼ@ 2 ȀAeʳVp\A2-!A3&6 })^(Ytq) @B?. ^ v`!pCLf{ 1(5?ʠ,Si`ɂY"n1>11d34rF٧hr'O[MA@׫YXmkI1Aq zuw/孼ggI$Iu0@7o `/=Y@_)vO,؀2e`Fi6 v@PA^E6-.$)Z;׈,wSJVA"a6ڭG M^Y酃ݴ>Hk9̱ŕu7c:lBY3 Z>y*:#/<}@Cgk Ct hgrMs;:?&7S2_jUپ+=6+Y.)eP@h18k͖|&@qj/<8KӾA^ΓhiNQ%J"F9@DvmIJ0@z+-@K3Qtn[UD" KtMVFZDgY+:A8$h-WQ0\@ffO=˛%Jtx?6Yc}RUJr?tG,vb@\!v@VH6L?%_2d>xQ:hi ՗āWÓE>f#nT>В.sw?ESRfdeWiC&K!V'e^CL3uiJ\U Xb \oJ!Yg-V+XHWzҗ-I֮bf3u`hn 1ǧk:-T7*+`c ;O ؈.FuYyq>p:N5ܯnY|MaTz + V@99J4PQ @yf;ZaAXhhy|x:em%eNd"KK,FkNSf5vpW@&H ,kfn 4SB\}HFxi!pfF,Og%E 'r AJDDz W孭e(k:F nMimN,V[m"~:lҼv,$iV[nelxzHs!K-7.; h}ٺl; '6FD _KKKi7~^{ '5)]vڭxDyA=KaJkG}.0(`\uL3y9EJtPf9'?kސ R$ kaf,+4:us\&2᷑wį81 \qѻ3} }f+N/cv&%oȢEQrrj%H@z 3NkK5q 45hv׶]V=䗶qt{% 0.KX_OlgLvSU ʚk%?-ÃEp}T@٥$ZF vښ+` "4Rrl&EGzzBae[jw#]ɟ} 8'Wpd?w ┳Y@@"P5X@<_ge-_?0QLBZ`ppsz } @l;SM4 `7>'y]SNJXf&Zn k ;(sQ_%d+0@) Z.&`Nk$H=RXX ٩!Vr)O ,4,ωy0jz]GK,aТ,>.܀t?0HucQD<,8e {@;cytߙ%=[r93G"[QЧJ%;%~lO:.Iy[2i<ֈY[ O^YDU>X KͅkcNr㛛EjqP{ 4:ꍖ;\$BF>0y3!qLT>^2_5?l ! K.2FQe`+ _{]/ tKK,@=I(BR :Vd(CO_Zi(j(l}R(HY E^5R+&4yE刈~ (g-4\m5ö]6Zjv'*?;CBwˬ֧y=8pxsjėL>&MT,J# 2J[8gg-)G?@&s2$똟/k>wni}ȕ](j5H,򷔼}pwei5lL%*d]dV%+I 7)Mڭ)K+XY *mi‚|H3p4LOO(\oQcΔO{e@Wpu[m+(Cmx]Gd͏HwK +BM7HvQ4 +hclt~Y/y}''>gyۡΒ/=rO x`p%3,$4uZc~j7Y[g`֥+iO)3z~i~vo$nC>?ӱ=$|r}Qh<-ycz׮JzEyH0*o:N<ျ 65{ﲂ\5)D%H9)@au=v:{"Ц{&NYndj!78Qu!E6q{oa]IDH_;^@vIjcO XӀov.;rlMލ@qcuu5uY*‘hU|qGhZc~N19K>aj7_|ى8>見oI̠).#IcS4ZDզ&M'w$A%|w,+ vY^8I%Vy,({ "GHxsu| h,.`c@9Pl_ݗIg>288耦J<}h4ܒth~zW ϟ`) Dz . af Hr"&H@O&"؀p1# SXBDOͱvwo]zV N`h㾠}V"ɇ;*2Qi#:ui-hO*Gf;EVw=nTvq׺)]J[|ɛZHrX;n@h lqV+f$Xd@3\0kbjn x^= kM$":;wk|ǵhC|K=7ouhq?B${d3 vi1gYly=ΓG0X׏HϒP`t{l-lNzVf@=(jֽJ_4U* YQT"NN@:>w5DI8b D$Lq)^Yss 6r.QDAF5f]/ n3   EV2n=0{n~a=̘b8B֮M6@IP <ƚ"06WB+j}MZ7_d;đ~k,/͋WV Ȏ@صH@37901exsE%@=b ,GZV6 d8z)}@ogIn@ `MpV~j%fP9" ͒^e@Q> Zfa&;(WjtT*D:.^!WMF %麐v< XMSibpg-MY,}+[,cdh ;V+\Lt |(^~;ZF(qwKz؏f! w3gsAFT="ejeqF `ut7R?֘]X{x|0NiOKo$#gb5jg{)6Vh9^d: ].JW7/{|C=#2Yvt#,9(R2oT1G=A4o߾:tt22+^`HJ݋.d,Ν8:n+"ъlXDjjCܞUt[Trq@KkDV˫X^]*Z0%YN%SfS:H^K 1͐G6%]Ϻ )X@b``Y"nKMֿWewN(pI\x1unyy+++s7iZ`& >O.]Fj/9INfwU*UPPMɐ_J)@5vl 9Ȓ _z2 6\HPwlÍ7RHp6`5L[og8߼29Sx|,:1ZZX!-R pɉ)e(|R< ߩ @`gs?1K_5Ϩ5 h[ Lۓ%jH zҮ*_fޕl I.ܪ{K^Jfl;v ###)m|. 044ThWֱglȂjY!aCg P qQPns"I%)h'3 k}¹sꫯu:LOOFQHB K3{1><Zk|}46ݔ<딭jL$Rjg"mL2HkP!iO 2搬5큱&N:jT0+,Xk&_ʶdN@A>|q_N/%OܲHgffJy<jZa /{IjdW4/vf@Q~a;\v-~}}|^R)*=o>pqYze68n챕 $Dv'8&8-yOϬFDZJYR6s[W`oX `!IfwV~V%͛w;BMd7@̀z$ΝCEApՔ1Y|$7}@?ꕷݲvg(ͷdI ٌ8 b W%1C]Щ/푩ܚ͟?z0_caaKe;v0M9^mw/AtX%$= HKd_]}L7{wJ_SڌDQQ'7cޚ]
퓭]:m$wF׷m >};|PnIv]y"3u޿o&RF++ m?f3U[9RL@7Z7 Z.^KO}HtjdyX[E%;&vs^>x|"2־dG&wJrn48MR,͜2}=&{y7 I8PWh46m?uTts_{'qLJK/-}r+͛7SIZk#ކr)u\asɉ4ϻ|':N (h&Wzu+@LdO?l.;>^\٣c8zOn^kS  Rz`šeH-I'M}=rRڹ"N }Ga!"de>1푿co1n_kJ/ȇO <]kjp~~jVF'NJSH|˶/1*?6mYu_ZZ`GPjR ˸~zjZYY}&Zӗq\xw17 /_Pp|,Nm0beKp^$ \p6Ժhw/թUa_%2 z2畑=ӵrkQ~b7ndRZc"m*"O|>1sy|P?,J#ϓ'$/3_/|5pƕ_)AJ6ԂZvWסZf1 |[yץ'n{ ]?ixbjS.\|ddͫX]/Υ H6|W!EZGu^,>1[#P0("+8Yf_Pny/"Тr^Ȫot?o{[ķd45^u,krԞT AޏW9uꔷVoFy-3 Gvzm]e12_w_x'R 's?|[Vp˗ |w<}Iw.H,!ǎ˴\p!Z…KmKϝP~靥&ϵ N^Mb'?| t+1_Z5Zk[פ@>My1XHD[v,s;cZ4Ѱ"2˶}2{ <,zW/^.Ϟ:3Gr4" ̔ 䱑2X/D 5* -GC`}}ov 2V=#U|cxI4 R@䏜7_ ܭVM 9_Xdht`k?%C.`r! %%"Hb7&<*{W264}x}Z,WWVͽnnn`=-8x`^C-L CCCV-\wky)gJNi@y(@9@#evV8u$ ֢͎E@1K$l[PgRAm`Y:a+FRm @9?y/Ssx7P$ų'0>\ѣGsݷony-RSw_JnCɣ̿fdx(0s&IY9`"dWDF&5굫;6#D̂. qD#yKJ6&XA˿L @k$ɨUrY'>dAX+|bN#i fpЪ?mΗ'7cDGy?S?~3&] ]^5r1::{ΝJip9'Bh^Hj&&&ߣ'S^MwV"L i0%1H8Ujv 17; gQ Z!׻j^_}85}gהԔH/ɢ1% {٩bK:E.C(T߉8@)9HOArAr!Ļ[x-,...)^6[>%9Wn;rәn2LNNR?+idxi'@+# ߂,8n[+ʥ@:6w6C~t"ݛTzB(@' ڷ#ϚvH% QMYin/k\(F~CGCZ Hfҥz!ɚ9U)[ʣ.lMz\+@Eٳg&''3I/ͦ)0K(CdV3&l7c\KlKYfExSsBS: DZ @$=JOEg lҠPQj.l&~ &- s]Jx{(jxVr|0~vQ tB֭NS:/jVd'{<;Z rSA߿'wYz.\H]s YRϟރCf;2ݶ@Cf3SFSZOOV IpؘiP`^ OR7e`5@mitmD$-qm: 5njJ*¯,޽{EQ.u4E._Zŏ(5xx8rvwy2HnYэ?|)W.w9{l&h6x饗R,,,nZj-ju7>}J#\ʺvJ74i+bh3S gP]ß3[Lj`jGӢB8Vc L44c]>u Y|ܷK^fXsI]1w t˔ː9M@wdȞ={L[oannιFk:­Td/=WRE@f b8+pC3/O'@$mOZ t~u ŏƆo~L@ Nl6q !gFe7i-cQ@l'дLEh<33Ůz(8ާ!J@.}qٳ '8??ᓟs|uuz=w:ׯ>?Q~u/]̟iWqӨ T\p3f5rR7͏UjCDТ<:dOLN _o{:%w_L$D-/%`zɱ^82]+MLZkܩ+u)_&044bB}뭷JmC15^hw+2 k1,(4b#Zk+T (RHbҳMzh#JX-wÙVk׮gff V:c,5|u -Xs )`+NױٞrE۲ IDAT %@P]!Js]mTiޔoP>;xEmkԗڿNG…嬸}b~Eؿ比zt+WW Yެ7iX 2r'. 0mdsk͊oWWuO˹sEQ& 7j2299Yj Ft뼚 ݭ]< 8}D =@TΠk d$՚mļ -=2<ɇǒ"T6 W*N,^ȺZ?-|/)u{|^5MKǙ믿^jf}ػf\yyW+8vIov|Tt={ n>5 #Y"^G>E>k׮~ ~/)$o][M#,ԜZe A?+j <HC?VIi\[O^AZeɌ|;|COX-9s M.@ KZck@?9`5:^HA,]V\6Vs~7، T;hL߉e:@q])e$s=`EDN(t:ZTeSO=ճ~i]^}U;wǴָ}6xRСCu+v#񹏟xja 1PgF%J Y*9 |i(jր$aKkOf%$~nI8 +eM9JY[@8h;` H ӄ@۟@Q>_SɵnuYLJ?aZ溤׃jlUF:Ά ەG5"b2t*Q6oO?dii1ZZZ ޽Uqܣ*a3)c{ [@} +S |랇-`V&IӷDI$=U:J/߈(bnu /߇#{B3k1;;ʕ+{]DP-ZFEzhd`lllvmmɵ^ җ~O>~+7cy9y=0P@-< of8ݺu .\O>pa߿ffs™Nj?J Uu6Y`<Q6Z?՗$u 3AtlH&JdЦL-0%! ro3&4  WGRO#Nj8pjE &xReRTPUۥoDRYqޣeկ~uДRQpص5|[/n0Wy29y+]U_ %fɷ-:u {K ("y 7E"9ΟS߹:ٟ|õ*ʚ8hJ [$kx`Gw0!`m^+mL4((P[g4WqM&}F ii\@6ӢV1Q T_)|}KKY[[  .ZWkRZVqPF@0:4U_~s ׂ3}Qp PZX]/sՖMw"m50RU8δ{ k`O?a]=n=X ; t{Qf2 /O‡, .]!g։ʟiتc@d KYy[[׾F*67~ffԞccc *~`e?O?[ Úfm>9 %\ǁnKmw {V;Ә];*&aZǪ E~bޤA'hO"mozl5ODǽW*JVe{?Iח7 ϟ{GG=UjJ%2_Zm}8s%pʕjRɆ>ǏǁgLLL```Q^gGWVV044TJçḚRT8o%p+ɏ>\b<5I/6`kFWZcxd#F6tC$&렙1"p?e>f? [e3it9EW^zkN)W?#.@BDq[ne_}һ;w{!1[VQ2'$_Wc: "#Q Œ1Q Ν;7ov9qx }ժc~~>j1::c=j H@uܺu ǏOYnܸQj&A)Ƀ#ÿ\z*ٝ:W?O>}CID[$@qelL + \tYt8Ŀr%#$Dz"dp\OՃ0_^WC^nx'}/7HI&Y^^ Z;Np%<8ynq~s.$X)cM/e@~˗˹&ӧO㳟,FGG1<<\*rz#2<+\/7xk 3x"i<~J1ٓ 2{ 3"yhѴ:!"A"Jo}Yhau$% .ZkwKI~=.?$MNNNZ3^{-85$?+Wyl֟+ZcV{R}=+F孷Źsl6|?8PV122b?ㅚ{Twm9RUTpرҁԙI|W;xF,+&w7Nġq5[mH@b {$ 25ӻD ( Z D`bDUs=.Pփ`t4^8{*V7R}8>6ff;=ի|r*<6ػwJ~1c֩} |ۍdv%ZkeDwˁ9΁NSt:QF'OSfl_ݮ]^{ _h4r݋|F#q+PTg۷+aJv c9܋L))Շ i[ؖjY W/]b9&elhͧg˙I:rmܺu++>Cdw<|>`o O3Д׷6MdQEZ^G۾DLdFQJZ t:g$T*Cʭ>s6~~V+155'ƌ_ @2pee׮]8>l8N```f8p8Ht)YՁ3g _Fx~Cؓǰgl% H1S_Y`su 0AN  ]@ K,Slh`΄E.c o__3-.R=1SgqhOW)Ç&v7o7M|Μˁ%TF;^&[맿du)k̡.H)-MtE:cq(tEJ)MZR*qYe@؂^xN J)?SSS8p\}]WȥP`/MdU୐|x޸V:Kq5W/NGGt}kn1c| a` X&lț,da,6DyM˻@gC6,x lȖ-[-[sHs==wU=GOTTwW5=R}iMO=utݏ:,S4cwi2 Ԩy& (.\P]h4NQ+~ ȟΣ6*җX&2*zzzo}K|mȿ _\}>B(xwQ]]NXVx^0 :zKKKb3NnzmKSھW<7wtԢֹ2!в J62}@BAeQaW+}Êl n ˒;R.ᵾ9JKÄp#[ H aPYY"Lj&˗UYTT|ҳ+Dv#_YPgf_\U!19( қK`2љD8i @Uwm'v*(())QkPJoV G"XAkm1R[2a7€!# P0C{06+*%ZkHL&ØIͿW_}UoeY| l]HG[ĶC83+9][ Q78 Rz1LhՀ#Y%p8Ly0ˊ/!Ё*|()J^x%NR KJ. /t?B'b@GhhK/:@p5탷9 RtR{^Yם"o.\HڮUUU|سgb=%X[[SZFGGGʴ-R Fmm-wd2aǎp\pk]-%1OOal.s@ &6JYM*iAӂR3#lS?Rm A5 X IJ7?8.w-!J?XJ 9r TUUfVrbqq10v/ IDATk N455$櫏BcmcK/uQX~>+l $zΜ`wYSSݻWѧnLOO19d2Ie0QSSSn5!(//jf3 !;p-e42iZcm:iV3 C̦+2@AJ(_110 8?X gEEE\rrݿk c޽o5RiI_ 019o\̦H a>;m?pw{u PHm-|>/W&P544H߿_q%y}}}iiYp2\6*UUUi*?lЀexrk Vix, )XXXH8ۍkr 555V}H]'1Ƕ_q`xd}k0pV;FPOU8|pdKRJKQtP~2;v_M]t[yyYIYYBY?f O H$#M?ղfî]Rw}ZSDB!,..U0ka}k/\Yjf}mW0 b|w,TO +LM .\I󯪪BwwwťED < G E(MH$ iEeY, #kf0`Y#w&&Fhqoז<ܫ4g"`#BGZD;nfHN6 BߟPlQz2N )E\pAGȿA\zw[L&M_&%fdlaFB`6AI|>V=pz~_P\ˉ /K9|p/qB1!LT*B*Xt:ם_b6P7x0??{ X޽p,ˢK1gZo @Rφ@,L)4*++x2m-`jj ՚ȃR8N`uuUJhƎ;܈ˣ+87GW.,Th]ekw93˲1==_]yѣҔ[o6]ג+aY6[[} [m!8wz:tABEcUU2?˲+LMMJ&ž={ `2٩GU"ߏKvK  FQ2NLL'? ? "Q~TTT.$bAMM VWW*` Dp/`J< hJ /PE0,n>k58mF؍pXpڍpPSjA#j|`0t*ְbDž 4uѣR `U` ,EŰ,q:8*xfHğJ/^WՔ6";tvvn2}'"HL |%e٘罺8|0>ߏmZZҼ'QFrkZ f3N'GZ>~aiiI/jN񬬬đ#G6YZOn4Oێ0̰x!\/ bʺ+i0w^x<4D俴1Br'>'5OA(/|KKK8tb\|Ja4Q^^Rf%NF0p:iA)*VVVT \R ```@p^SS#Gυ˷L ,ff58C)E(TeA"c_p ]AXa0 "a$ yY]M*Xa~GYE{{;<ʰM1>>CI +~IrW\9ĉPp:D$&+YaZp8` bZ:,ҥK^=l5]7Fض<Ŕz<->[`Y#,&3f F3X( ('J)A>^.0  Kd,%H?L&h}?l/?c4{nx^TVVC5bbb"'U%J)V4Ꮢ bYXXXOS|Ν;>;Qz(--EqqqFiш2!!vv= %aͩxJ  0/GTHvVX[[Cuu5%򟙙TJˉH>t: pYLMM}{_4@AU+J 1VP~>/rÅa``ٲy. ^W^axxXɟaSwb<>Xv-\>hT"|R[ĿJD/ȟ93D|KۓqqS 0J`0XMDG&C@R~_\\,رC-'|p5,..fUӏ_VKJ˥A@|XZZ4`$*z)۷w}wJu8B k@a$]D"aB!鳕B0R-wJM<ۍUMM)0Ust{Eo_m,J_2gw@Xc}J&{$4h%qQFgpS >S޶n*A _?vڵdPYYP(&i9~ Zuyy9~V_\TT$mH$F p\Ew$'4("`qqn%%%(**ʪЬfdqH$ wf01L K1Zgee/_N{Άf\CLZ֍Q/ +m'fyWO>}cNln*RLF}KKK?B6Og0INCt: ڊMm~JN%A,6 r@\_XX1jO?;w#+A\]tS-ȣſsI\ ȳ) Z)8\zccci} nÁlbW{ kZ@ '.u7ѱZA#ᾓ qS P֢O2 sKvl8;wĎ;Qz9䟎@ACC8('w9SԂ?]-X ajj wy'vޭA p8l[B,㰶Ǔt6dƕ+Wh`?AFtNĮMm_~/'dZVp"*kuP9MqS ϟx ; fia62d2AٳG^D t.,jSg!msT,9wBZ{ $ Monn^TUU%E`ْ@_,3X\\į~+஻/Td2v=As J26~?}]LMM˲;[OVEF6LLM/6p^EM';= ABH Y,E0LUvсʘȟ8 ,P_Wi愚5`0p8˗/H_[W 9J׮]$:::٩Y].F#v6w#y3EA all,#,XbFu5!KYZYi{l?/n0t]9tP0iZ@j- !,Y6T~?P(gzW0 aXV$˗/KBM*'>?&RJDRwٓ Dx1?9 䦖A(FFF2R :;;٩]=cdpySK$m_%_+A?4>O?pS /}T|(  Ռ`v3 }IB0ND`P*th4E/rI36 @^p_җK/̙3jn+WEQ %e;[Oilp8,6{%n`rr2sfqREB50|j@麒RpˆI_K?8KJ A$7 nj@sέP-www !سgL&`77```@"\e& ---1>nx{ރ14wa`l3 8|0:;;_j>Q]ftttBS8n< D';aYx#7K199Ѵģ wuk$>մQ#0*AAhd_w~?j} ?~A~  `X,8B188( dCӏߖ)[,477K}@lxeeCCCX,px'!%fD `PZZ~xw3ri>yǵkpua׮]hiiYa A$m|;rabbSSSYJu]P8$: SfˤbݽqlLɪCGglڨ!%'`mm ,Ok_lDKKKRfw])1o00E۷xߞWז[!\.^{5\t ---hmmEeeeNߏILLLf_Áo= v!r!* PVp>_.<jp8:v죃MGt@; J0!h4 @v,/`?A`ZCUDGFFk׮r2رa%P* }8p^|E[)ϣ_tf (+KqCbffsssҜ قlƁwޘRr")MmR@6+O)?>`0޿̷k) *qߥ~IH`EEE1#Dd_YYȈ?䯴OXDā$ڵkhǃqS DV%Fj/+W(Ou~ `ppEUU*v˘LV5}F)THB05d('Ld:ȗkmiyXI$8z !pX,hllD0āRwyycccҶl !LJEEEhlVPNER`||\:VVV011! r#aPZUܠǏ]w݉_~E:(qPzF#jjjPSS 8 CB Fc4M.cXގ}%%~m7ptOq!Ǔ8fff@ib_msM㝎?Uee%~|C[o˗/gTG~/F6ݎ8Nm^H$"%X[[j^rݻ سG6Tc%#l*_ˏ۵b_@AA)ǡ LwvvYM/yy9jkk366yP`,@T^Qd$2u&nСCqۚsY2+tLeaaXbAar yCP'Ks,l !M)o>1p%#^5„TKa >vưY$88q°ϔ&k'|yGuu5UfffڧTeS IDATWNZ Ԩ&Q,,,R e100M-eI*2ط9S~혜ozšdL8fpN! ;wDssRuaAM-6lXC?MOf 'N0y8]UUv;A@}}=v?caaA5k jTVV"~A0<<,n4HC>$l6s5ǒ=waS Bߏ~'^Mmkmmm[в1ۓS-J^'Z>BWᝏ}'  8q}d2RFOFcccX^^Zj_65$JEYY*yCCCR=ш7|S GJ BoliTȗGfnm݆e ahh(F[WM݈B@qq1ЀM@3}D[?Ugq@"MO?Q^$?ч[:Mp_@<رCLODJ %g*d倫^q-sAnQtRҀ:B7}}[}qAWb~eA@ `MWz_yG(P}ɇϏ=|_/7pȑa؝ !͠4*ٳ5551mDA~2%hnnnP֪qAA>/F#y[II JJJggr ˅x<UhRXaEڎZוC2olS#7&Wly:`D}zR[qUA:=z !,DmAKK x0@ee_${%yY @ll6U 'ŋ4}aڈ?f3jkkQ[[Jߏ`0P(`0`0(!D'`6aZ"88NX,}J}f^(}uEAԤSDUBHRW@1[J&a&;N}R.$M'=zt:&j0 Z[[q޽{QQQ!ODp/x&ڿhٰZE0wE !xOOϫZ3 j3j?ASg6 6M^ HBE x>] BJ,nO&Bc>xA@UNS I/FP;:8iy^ .$M%=z=^ "`0`(-(N#slhj ZZZ`6U V|G0g}-'~-`fsU2o;Z.(q`kɟ['M/'/ q4DmXEss3"XŁ6<%"p8f?yi_e"1HY[[CF"f~gxPX/lF ܇O&-E&)%8¾TtOB8z<(Qш&pш.m4OF`y>'@z/_7hiihTE8O룿NgTk5A. _e+ϩԐV?sFC9u6-Cd^8vҟ$jc6`2.OF~!'@}M&ZZZvcpp<σۍ!MϝRo4?OOndQ:1rKۅ@ FԸ6nGVJgN=|*Y .$ -twwR} 'eX,qX,պ1m2|JJl6E*곲" 4\.h}~b}(ݦ1ߨįm;vmr"$j'PuiZH)z2U; !79 anGuu5xfCWWצ(mm CCCfصV`F/--axxXYk}9Os7Xj<8U[]/>>^ #F XPB=m-: );vSJ`Á ݎDOꪤ"/`?J)l6b_) XcllL" h}/Ν; ^֫}ck֓ߍއ?VOOq@FksL "©o<c<]% N;(?%k#VmpF㆗@NBDK'/~pb^N3771`a,.jB'Ν;YW oi]IB+*\$2U/B85 bO>@fI Rd˥Io)3 e\v-k~r~AP\\A%Ӓd2a``ZͥK) svzjVz]ڤ3cm<+~JaBӏ=/#3' ^d*++ؿD"'2/,,`bbBږ퀿t _.'q%3lƕ+W4;w}>}:8**&յG!^W>~w6Ⱥsȟ8šRĩ~:rm/8qxGLv555R- ݻWOEʚҾ ڵk%˗/זK{PkZv luZn}x5!H9_@Dg#w {+^z`[ ǎ3{B>]]]W]]vNF?[1gB* Rbaa@4 ŋ1uՀR===_ec8`*ѵBcǪJ 7"A9mcGn:m+9r> ! _WWݻwOF4J䟮 ߮AX,xW>G)M_X6ɴUOB[GcU~?W w'5Ց]lK, PJߛ !ҋ\__]vOFX\\Xfi嚚TTT$$vhh. `Z+4E o-s|նr= gΟ C.;O)Nt BrEp -MlҀ:rm%?~RzR/QFEjiiAss?nwNKj% ZtB~皾,#5k, |\W4lrHW9?zkɇ{ Am2gYMMM sΌ`0ZV҇RFD'?˲xWO%lv~km뉎u)C/(>ιy[S" ]pa>_ɿ-,G,F444@޽x— R $/~hnnFQQ*R!pႦN)uSJ;sLdx A3}hɱpqL x>Q7zo{o@GQcnLd2^"={FڟH'Hq~?'"&PZB^`0B8˚m\9ssJ,pˮ"/Bql ]n` 1cCN_&5㧾|r<t-twwA)PbA]]!:%oGG*+7dD0 8 `ւ:0hnn(NE@}}}B`@W\Aș3gz`>5q|v BTmr>V]53fb Їp$@? A}*; V> )Dm(+ې_OF`@KK4E~}}} 񠿿_룿N)ș3gF _T⠜9!lh]3Ls! $u5ۆ1dL. _pMkv(N=|:БopoDml6!XŁP\\ `ߟ188p8_M XEkk+F4%#5#`0rahhH |gJ@z5L*I=epTZ/_}ӧHljueڇ2"io}CU}n4Lbxji/O<`&/ N40&jSTT$aYp:_.B! 㸬1jj_\_]]x˲õk״>7 !G{9mCRo}f[E+ZrNl[L}o,$6(Kz"| 2S{V/}In((_Hr](--``/A H^Ki_5JF#Z[[lK+~XavKd4111)  .Z~.d_Dji@>slXEwFcP$_?S?%Audc$RoX%ǥ"ߏAPJdlFKKГE3 0 L&O=Tf!fgͤD)MITOMT!+Qt]ZϑP/3?@tJ/kɲFU 9=j $y _>9aG:!tww$1f{ィp:X[[俶adN j9!022J)V+* ~G]VCq FK|Jvr/ߦ5סv6Vh~Z=,dCJ}[2M~0|AB* B)Ζ]O~2t\8rHoNl6sva`%y˘*+e䟎`$R}1:: J)l6z-5NjtR@^&Pu_I=]!C%ߖTğMPs>&Z澾G֯MsO7uy>j]OHk~ofi}`ga' ^HRf _`0`c^v M133yPJQ]6/**BSSӦ(D9\~Rŋ A)~9lP.H?Qjj_nHg[a)v_Ց~uыE { #D(P ONĖ ^!Lt⡇BMM"d2Ȧ_ l >Ӛ́xW i~?ͅ=HS{6?5h/Ǥg/L6=S:l .u.=#k ҽ!?* JgO>r [&{mDv;/,f Պ/" e-/~  ԑ_TTϫ%LJ~EAyCMIz+HmD]rolh]Z^V|V|lt[o\l`4M)-Rg2/~uuu`YVJSp8@ Z[[n\t  WZZzJk׮annp8/5PJýEAyťEŘjUڵs(F!\Wcb{hXq}6A=fsȯKikMLN&z 1Xd5{W''mM%#GK)hԧРC(WٌXnl)&*JE `ll sRM-s gy\8+ҡc`0B (yP9y0["0 ׉:twuW`0͆!ZA UUU A0̀6 / 'Kסk4d6ӔGN~!36HGZHrGy/)+..ƃ>Bl6>`fttt(jJrS[[y` ⋪:BOlo}[!%:nB`ZaU;eEBI#1>'uw 0OV+Vj @;vyLLL$5!L:+A>~̙ZJPWB!:nTXKވR+: y8zh!d 5~ߎ;裏d2~HxX\\LX7;vTs~x^ѫV|g[:O=~dCf,]LQge]G '@pq0 E]K&Y/+|cc#F?"aaZokI3g\Jl1(O 1зTmБKXf s=*V/=PV*1Wh-++þ}`2T}>8.TBv3AǯBá#a0;hJ3g\l1>ģ jztLf3X#KN>r:퍼~[$Y(p8R jpRl !hnnnWE`0p\l6"l6M???/YYYZO)۞?N:tС#WAMի| `@9Ebfgg%/--ٳg1G{{{ԡC7/U1 4=f qz099).))ٳg 2Rz{{[ӡC:I(W,)Ɂ e PZZh 㘝!EEE8{?Y1:tСcyAXQJyN3!BԼ|rB!թ"~J)]y0 ݎ^>ܖ:t!!/CrS* رCHo2!?c===:СC#_JS & 6, $e50 s'7C:t( vISVcf9'*(JUןy reYakZO g}lߋ:tБy!+mA$IX,)Kg *Y  C4?100 ׋˗/k:!dzzz:tБ+My~rm6[m6&W?a 8x|Xﲓ縪""Vj-4M#HWECAQ _]_v6f2dY4M۷׮]C6[o2 N_xbO""Vk}}sOOOcii Bjm`# |U_Axޚr4 b]]]r )%4Pwy~SV<Z9tرN}@Ok144nx<7ڶ 4Q,!D&nݺU+0o꽑I"'N(B]+@__9>xފGJ|aY,*w[XXfgg$?N's3vMhWJj`0 Eٹ44Oׯ^1{w,@,{'ï_5 89X>V NN ll ?b8xíۏ881C Wl{wJ: u5U2ӗO>[^`f>}ʕ{x`t:й_jkrxq'b#lǎ_aMLӆ9 p<77/+Õk7GX,jAn@a #n|pC~~^o3@ MM} |4l`C? ȱ_``g()g3$`~Pƿ Ofx%a\@u :85~f2&6l8C pF ""@a ?feRPf+de_>~/=A0}(,™pS`k$F)ɫ2p ߿~3:A!%Fۗ; ܮ^a#f` bbpp0ƛ0eb!(Cm?3{`rg;1lN?0ݎAaۊ a6 !?10"gXj?ѣ?ϐZz!.ߎ9I@ݳ zc[w3FdX1ᯧ Cѳ lw}=-b ;bB::  J* }gԍ%Jr>c; _ '&ap0bxvÕgM$@Xse@H 1Ы 51  5Ё`Mx$1dfYA1Ha eɠŠ- G\_011c@% X΁9v`(- X~K`:,sO6}?ΰ&ʧ>p=fxy?C LXYX@*CDbAr3(Pf`ay50*D땏߀ #0x9^|p/Gn߼ 1AKeorBV0@0bxi P"./!pU %48y>p};2Gfv>>.1\3 `ud ,-EBx"5DuΝ 7O45dY%;0<_d#On-Qݫ0|ӽ@_ !>988C7Ew \}pwg?3p13`ܾoeX83?Ll } RRb:] 1_[@,O<}'||\H0]voPSpp?sO^<0g<??203| 3C[{,ÆϘðh|p}/UUm'3p۝L,0˗/<8 x;Gp{==)[/3h`Wd&+ ì9 O]֖Wq,<mLL9ɰsGA _?ٳ뾬̣GձYXr#Ơ&g&`+&`&g]gp1.03xx80p s`;QX7;qaѲ ]QS]tb>HTRR# *3 .ή  << ^X1v͇9\E_خ /-pKW2<~~3?7 匌, @a͚o,->u^z7#Jf/v. oq1p{F_fx Ӈy9wܸp>, (g99<<\@_[__.-_ b` L&F-aַ0<|ϟ Ba@eo4EP@C>|xLϾxq/ޫ_*]Tn, o~ R>vÍ[Wӥ_>ձpoeu~@ Y=Q| N`x8qׇ/7xY?sXnKpeDr?~"".nذc'0*2`rff6))NÇ+\{5oV<{|o0v7](L̬,\l\NvV׭߁S9@z͚œZM...΃wNZis(Y9aT.(2a`bw: 9 2IENDB`glogg-1.1.4/images/glogg.iconset/icon_32x32@2x.png0000644000175000017500000001232713076461374020504 0ustar nickonickoPNG  IHDR@@iqsBIT|d pHYsttfxtEXtSoftwarewww.inkscape.org<tEXtAuthorJakub Steiner/"tEXtSourcehttp://jimmac.musichall.cz/iIDATx՛{p\}?޽]V- `Ld߼ ~_ܦsϦܳW&ZJ1/ \^8D"1~ 0%;GIs͛78vx:vȔkJ)GMkݚ?$?9=sjTUTa۶ݷAӴBo>_=+'###92u2t9>>jkkSc#G[e@JI"1sP&e:j4MX%M9rr\Xix^n=GMs4MgO&3dȍ<~ks @8СC3ΎϓH:嬴~znv1Kp78ٶMad3?)%JI,;p.z3yyylذaqOOgֲe^/B,bpp.)))rapAAu 8 fw:D"^z%\.7֭#id˲eݺuDQ:;;ٿ?uuu45t^üop8L{{$宻H$B?/BL꺎륱F9`-PQR<n&QVVx~ e֙<s'OX~=CCC K s(t \hd$a B$XL;mY:(--S4?O_'gA@ {m^5`2AN03h?w%K+ )EJatszzc8 [J37t:~\J >}["`ppyth+ ׅh+Af߅ض lY ȑX|in7̞o(@@ ˗i;Gw0;o►B'B X2Iʲ,ӲI$e._?t3yyQ~g9p`X4-07adD"455ٱ"+h,6P )MI9n(RPRAG#| .rb՜=̠ qU h;GuE!ŹT(HP :. -lӖ$ik/x7񇷕Sr7 ٰ&񨨪u.***HRϝ5RU(B@* J@hiХ1fM[b؊(,[ڊw|JJJ0MEOc4a !a8ѣG$H0J[;ADžr )IK-%?5D f9@")_8."{I8 G*5A,`֚wq<e`Y&ʑA 2@XH$XBb)8(fFlj(RJl&d?m9|9ee -831%)J)jdÑζm#&'w)TWSY,L,,laE%l$ SaӒCgY+B_kF8cڵttt DJp$L[}P(@*x'2AexDyHV''h$ot:%}2%9ٕ0 , MӲ &’˲V2-4AE[*EҴ TFaJDa*/k"[4ɱݬI:+sx 0qL$h&B4M#kar"e:]M8TSƴlFb(%ѐc6#ݟcI׉d2;m;[̂(&<'26`ÓLaCuyN"$f*Ep(IłblH@ tPJpLi"r .b+PBMGht:J8HJ`i$?q֖vu]]]Z?MӲ* s~ѫ%%%>|jyyk s,o0K %H--I%c} ZJ(siT#׸wu~˗/M"u!3+_SSöm&xk@!d2Q,ɃnjjjfΝtvv|ra^=g[p9m 8f*KA Z:E$_ZAIqmN"`hhhG[[4jC>15`ƾl²ƭV`b~xXr%$r(-- M"<|\ PxAeH`0 9O5P\N>Mgg'dxFm!c XZTT4mA2,, L~_ĉlڴj$ *'u~떗e844ބ6AzI}C1~E(j-cqE<Ȳehjj`&P)I(t =)]CPerY9{,'OdլX˲ -nk(7yg*`ah4hMprVAIŋywe Lke7^E Pok`Ϟ}--ڏ^|qG //U2+OGcc#äR)D"200@MM QSw03t8{!A<%)uRswT6|"L… ݻ]׹1 c3$ \NW? $q"H&xRY?X"[/limtR'|r[xq6~FGG BsZ[[ R,^@"lt:!77)%twws شiNsjŒzrs}H[bKd*E"pg`S~?X Edw\WUUJ)ӆ* zD">}J{9-[ƪUa?H O& j*V\Vu]#//<41MMıRf HI.C\fwNN7nZYYI0DAQQ999tBioos} ٽ{7ϟj|>]ױ,H$0}}} h"֬YCnng4/i8FX,F<'2TN__ZoV hnnu/lAQQ.P(G9vXUK/hxشi6l… \x.bdÁ㡰jjjx<3 =>ud*9hbDQF##(P%۷[նqƵ|¤  QXXm:tSNuq~Ň夾'3Ym[/Jb"TCB0mtwضNKKJ!zVT*ž}hooiw+=JJ2ʉi~&4SRI$ebRY##ale(GyOqMz7E2۽rrr(**B)E8x<޽{oY]vfưR˯19ɯg~X9֬lKL̔0DD)%9pn׿`.0zFѣGzY())!{n.]]vE3chh2L`Y֬u+ɖp ]qN}ӴH$R)"0x˶mԷ_ӵdZ[[~lnn&OOODxasBIT|d pHYs$$P$tEXtSoftwarewww.inkscape.org<tEXtAuthorJakub Steiner/"tEXtSourcehttp://jimmac.musichall.cz/i IDATxyu߿s}ckM E"APf%۱sVb9qHȲ'Q',KĖA@PZ`],1;WLw꙽o"~dcfw{ޫxGCtȑ#Qf6vÇzʺX La{キV4v?ɉou[& @ p+t… L&={.;S_j9uG#J%TsS0 FGGut߲,!}C*cR] Vn]xt TTT,K޶- ~BEB.bep x.AXl ߿kuɓ'oBqt_MaX,իWuptq6o&###f~ 5wd 蝀Bc@ UK7?W L&fdd!4BL^Rbn~%0@EE[n]ԵWg b1._LguѨ_A(:\&v&!N;~`0HQQPh,)+n RZZkܹtwwXz5Dx<4MC4,rt:\t>$d`0ƍپ};2_-w\p5^~e4&vE}}=`pVwJ1 S\\LCCv"Jq N߲,RLL&i+x<|>^/`C(bӦM4773<Xn%iH$a"SUU? /ݼ⋴s}~;H}!n pҥE]{z'`pl6Ȉ;YLȚFyva`hp8NWSSO<ŋy饗vý1я" `%%XEWWT[r!@?3ѩ~^:;h|vX> fp~p@C}RSUUůʯpQ:::8ZxMgn_ێ {nO8FJI4\:3+]?7eoXUa'GOR5 r$+a̰N$72$9>6fuU˲$HPSSC8瞣犊 LP; (//gӦMs'ĉR) lB%Z%'z+]dsz_Ua՗5vDv6z̯Tl?$@X,blQ{W-yO#~d2Iww7uuu~>/paˢ xnQV6w܋/[ne߾}.{{{]~x<=};@*VD{⠏ͫ) x Hib$cc)4H*k7N7{9yu{&Cd2x<΍7xÔjˢνYg9qt/^ɓ466~RN qӗg{=ܷk Vzh "DfR"i9E*ĒL΢o4MO4I6gQ{Gyzi>̾r9z{{Yj@#Go~Qzys %+e Axb:Dy=^Kscsx0>Mk+hMJ4hB Є@;ί9xuU!v+2Wוi+%\l6Koo/RJJJJxz[oMy6aPnL<-رcd2Omm-`nobdM64VRQqߖ"EtZ؊ ( >ˢ84A}ymM}}:o#;k,ɟ| ^0 7UWW8{}}}s-ߡcE{{{`˖-[]1 MO_`Y&ueAv)CD^ߔ#P aAasc)}&2!ow>[z\4@s}B!عs'W^W_}|g˜c[ Rr50۶m# )%ٜ}X2KUZbR4MIu^ҥIJ[vo[S m1 gdžJS\1%?Aq``0ȡCM47N0}!LR:7}W(L;wRUUeYb179|I{6D4AU-%? X ͒3M,tkZiZrcA]C nG3ٱv>MnDzto6/||JwϞw/f~?i8?x!`kSGli,FjL֢9.8fAh8q)k#zy׻ޅh6ii7U3 ~q2 ׯȕ~{qװMZVE}H uyu mDŽ:26JP4xRJee;R0rgaXy/Ma , vZ; f[K:;6~~sc%sފ-)b+;RVRC:nsUe^"!/4?ߒ($SLjiyyUdllR***b```ipr>9+[b^t )% L } .#ٺD< ttbJ@Tkw`BZr_"p,Cos`sx G>솢sc^|P(Deek8#j/ע;auN߮6IaWm֤OftlQyttFv4;ri[!t^[ݎć[*Z[[Ų,j[-kK~~omiu iULZϟ,m"54a51 hSӶ"W-—s_Fʉ'0M t]GJIɬ֟$:ȏ_H@EHWL22NI׽H9q<6 >U5Ř5{aM0Rz 1 Cz9l6%bdd˗/0la ﶪ0vvٌ+$x\ `&_|\y9nْ<|_KŌ]rE&f.UB{/ؒ,|O/qwKZ”&)ϙFbWnұ$EeAp%\6Ų,k{<;̗yfeKl6 p4AQΈ{=n4δ,((@PM -1=ѕ>0 =$RHp B A Z QT 6 S4M,#Bss3iL&9R`@H$B$RygB @lI71m-09F{{߶A3A(dKx3ֵ\L{d˭fѵTHJTiSd ?}^"{6pЋe]2p] gB:A)Ѥ=~ $38^t(67Hmi 4t~{Vf<>+g:7cNb=b\tyi9YR_@8AKP&ke ,N3瀵 .vD4n:CӒW 崿sn66s_P{ eNޠ{FURTT^=.;~6:4<2M9jPRumllā[fci4w-Ŝ0\H~6 vVK{ omNX"C:C;4[8I&&m,_>XG=;Ms [nXP`jDQ. uu/h{URQTW֕n!Tx:k%$’NvX9Y`8#Jv]!mbZ<,<$O*ZnbVPˢg7Q@uaX'}^xH nY Y:%u/;jv|jeE8K 4L>$*\خc$*ӎH滥7X]WеRMhv"꺞kf k)\^"^A ,~0OQ#*@Ghp vL&K[{x¾М}ogDs=%0&E>NٹqG q曼˪sJO>9!?@hAq4@%?x<7&MU4O0`drZEI(S~ #7xdZN7[f!NDl| eG #KίMw-89& 5NEq!CaP]|顈_ݭȼA ksg0MӮqj77}ñj[ #ccvHY@&дL,˙ 2qbW;eUV}D)^M іV5!ҵcX(C-j,S57Q@cy뭷`0#Ne7".« 8} nU#AǢvQIhXE:!:2pd.B=p'q4vs(`2GD ̶@eg2 .fyBYl9P !(+>¥qƳlZ]_230jsī_G'),.^аjsE aag\Whhaq^qC2QZc*;rL԰wfP ?Xy#p6rg7:;;ٳg\׋0 lR8TZ9i ݣq3k<5e`0H[[{%NX__ϋop[:4MKl.DjU1d,!!^Ǟ/L=¸/Adi?t}Uvs6uU6tt\:o}>)nab [UUTV4b4M4{jH޺tW/ 39_& /S@풔Mw.)`WWW,))p\Z`C|2\sα~z~?bx;|o/r{SZh)gțْkYjNS'۽: â,g~y A@ @ee%BIӜ9s꟏OVP1yO~8s8R"!x&b'N|4cƍ$I2 TTTP]]|7j1sx]kػroiya-7pvB,|@~Ru-G~2YUAGPuqΜ9~vE$Յ+ꦨ#dj@kooPy+_ %%%<ر/_&CXyIDATH:.ſfv^>;s':h64m WVVSD X7:8nw-_R3륾C:fhhn:;;رcwO, c3%IJ3,d2BD"MRiź7~iS[[͛_ /X40L&_"b'bgs5@Y$`~OXȞ MJOdy\'.͙x4Ch\N< =3%u)lL!DMC]]]E=l0rfժUl޼zt] Yf p8L]]###DQe2\'pYSWU47Qْf)!Fzm/#D"31 $o\@ C=4g삓x".ʚ%.xtp{j5MsG/7җL&O~B2N/V'Ke$I.]A*$zG1/xׯ:tR6 a_#U k@d6R/}cǎa6mDuu5suwMPt: 555X2Aee%PF(cccli,}d 4/=Ϯ9dd,X}ڲח*8P^^&FQFFFH&+q4M燐nNgM$L??IL +C425\??477D"1~0q555 [oJvݰRw͠j~Ռij\sc0ɠ 1Eb AQSwOA(d2 Nĉ}ǝf-mb&Dz$\)e32L%2iqTPNJ>… U۶mcڵTVV:1`%rhb<#dY)++sW'iiʊkC0`U GGGEJI{{;o6ezyhjjwsJʩ(@Jșv)yδGcp cL#Ro f Wpg/N&_U;v`ڵnիd2)zi2 aP]]a c& w---SRRBII S\\Lqq;at> !&MteH$8uD"<ÔKclڸywUN&i9,1H'¢ȑ#?ѱ'NԞ8q]GӴ@0XA!ݻwDYYqUrpx jrSΝ;G.s-_|K.qADE 󅚧0al6K[[mmmn%ڵk9x 1k2T"r&!JLoJ+P~3[fzzfYn˲k^oq޽466e]r˲M*JJJftuuq9D'~y֬YΝ;v#q@JJBA4mtH\v+W `v[jT]/#tW/Ę_G>XbI6k?pi !KSSjʕ+SLd2R)^/">Əc^y:::ꢪfzh&a``.w5˶mرc^wI.-TŠbJ0KuOLEǿx/7?`MƲ#4yM۷իWDGiTha_]WWG8&p%\BWWmUW8#M۷۷siN: ###g?jYF^MR5>>### hnnf֭RK (;tX+8 mHel x>G,SJВGi4ydzj߾}455 `Y~٬w&\pN^/pۑĤM޽{ٳgȈW~VZźuhhh55{ȗa:~6Cp}Il\WWv`{˲~x˚5kdY:::܉ o+\zt6~mzzzi״JԜ MXv-k׮4-#166䭌/׋륨ʨrKlŝlX45T"F~?T+WӟTѣG|}V"wZ%;[65;it]I0 qE|_&|ȈI ł5Ç'VQQQpr/}S >Ex5ի2^{5p#ǎ{zVqsQOV-j+‘<fi#EH*wOJ|o$,Ge)7JKK7o@ @" *_12^Mӈtvv/G}n{O~2BlRZrgSNt["ȹ.lZ O?BhP̛>YQQoܸ7@ 0S< !\⏏T1!=lm{~JI{3݉(#e)bē㮟]72c^ p.RuuF~?X)*r\_4BŸq/R#ǎ\P|] +Tt F}}ִ/\@xbd:QXW3?|hT>?߭FOP*_(؈h4JWW?"\i|x'?kkkԧ>Eyy9RJ:::z_U*Ѐay:?WgkT5~TjL}T:&'O\&)U6(#0m ?'>{qwm<Ü9s!Ĵq}OCCegy饗O !>zh\ vQY!xtJ1)R~S_er=XC=VۗavNF pW2M>:;;]K)O[s ]+H);Ah6O~˷00MA!8pB7@Qp8LUU'Kgg' ]~0{$|s_O=Yr+ɒ%-R$y=HiqRB)0uJv4HZ,===tttn}ɋt?B+кkbffB 9rA4JKKu-ꢭͭB|?J=m`в,} @4%q Z[[9<Rʿ'Oi]R͂5@rrׯ_ٳ_K3j0~ڷo>p8L"5Ͷrtvvr!=vؿb&g׮]6lؐ?Jؾ};ڔ}w]_.cǎ}&.~z}---nr$ gΜOO(?&.sN6Dc~Ϯ]'HpB7}/hbY1ن㭪4/iNycǎ9ͱ 2nIENDB`glogg-1.1.4/images/osx_installer@2x.png0000644000175000017500000030155713076461374017045 0ustar nickonickoPNG  IHDRL#sBIT|d pHYs\F\FCAtEXtSoftwarewww.inkscape.org< IDATxwU߹e&靄4HP{oPĆ "(*v)H { $H!)-#{7̙9g̜9;g޽k_C2%!ʊz@X)?b zq3OHC-Ƀ˘Øǹ>WVs{b1\oи/VzJ87Ṯ>*F,ȈDH9);ݲ%\E:ע?Ǽ7KgHv6/^[l FqAz"_ym,R*a+*@_<Ors06NdЖ SG%(1e7IB I"HPPYWLd+hjV@% E@}&x"d5)1D(8RUboPB'n \ $"AH8Gd0 =%!yׂpyoUd?s)ED'x9׉'R N7V!bEUb@ 8~ω()Kn@d. k-| W>' PqlQP̮Ny"9?HDz o& S&U; M]єP<[9%M8f$lD",mU]2NdZH+nO0ڍ`(\'!'KvTND9aj6pLvnr]k^~a%&1} . (roUA:ԉT[4pET̳&"tGxdGu>'$"8YaU<_t cDh&`\sFTɍ$e Ae` 1#sy <PN4*% t*1:>8'Da30*(!c@ ƇtuH D{@/ "b$Jϖuz "J QXxD4,U:PL*ѶOIv 3(DHbI4rH#{{ 1MϬ;s@R(n,+qDuFpBjZJ"8m Lճl\%"/12lnIA#@[LH$g]˻q[LyP @ <a bK@pkctP<.)^7J@Fn&*$H4pH#H`8>]I0`^(wt'JvN9 8q σdFEIe:[VW J8P@E3 A$pF"E 'Pҗu`at* POOtv'|ulI 2,Ubo(PB$ qT /@  . G F"|a+n[5 (q*1$h>'~$6.e:HthJPB%*$) G8PĀ>v`3)A &"I]'*hYp0/Ne:EUO%0 ( @RȂ#a#Q!Jw$`* %E@> +<:RcKq5.ӱ`%DĀc$O7D; 2@"d5Fq?t.؀ W"ȍB Dp('L%;~[JNGV\N\c.J0RD&JDD $/9FTݠFl P R>K@.BEGDǒծ?}N'"75Lp9q+b MdМ_f]%Sb@ }|,e7ZI $a8R(P@#}ΰ y('*Kw“4@o`'? #8ѢL2nD~ڹJLcpl((IDBt>Ih &R&#j- .(Px?AT;: Nt)Āqeh Kanj@%Q%Kn$H$&(dJd_WTT.scY.b~'N091p0$xh*ѹƀrlMA$I<`i%0 %࡛P R@ (oxB %0I.H]0"r^ 1+gWTH A Oȅv,'`r. ;8ADRqǐI**AJc)1MXnDIܨ$sbD@IX$!~$a$a#Q"|_~1F *@h"ƒ}vp?l$ e:&S,q`ՠ4WIP*1G>$̅{EBAT,S%WHK E4DBPt'^[nu"\'%_*pkGtM 4s c7t5}<*H0$HN&Dւ>ȼ`"haH7TV4 5 E6DQPx`'XKvr}N%=t0Q5&tTMv(w]2ۂŸITCQ.%T$%dGĀ I6 StM''@8 SW APR0ٻ%HBSj`rȆ#B1Ĉ$[ *kQ0; ~'\'as M 筈u.Q JFcW*o<`%$AĒ# IGd‘0Bd ndA־b("](M]'a,Q N07]Cݐ#oIk\%Ph&Tr H:H0שbh]P2(A!>Ntvp7,MtL%8t -1NZnIEr`(K6@ `q\N5?ѮN NB[#mke%8J4/ڧĀ JIP%a$J4 v}"|Xw 7kRd@2]'ቨW׉r bN"_#mUB:]$,7PBA(DcHhG$E\P,רٝ ɗT (2](<+ur& ր~9b*mҗ 3*a8X^D^\(XM! f @1P('zq~&K@ 7*EL` Q^Oxur(b`.mslD",14%nrJD`tD `#Q#T$<}P\([a,"(~J'a'<{T\{, sK?vDZnɡ&U"S2NXAnII{@"0A@+U/`\ R@EI4׉re:%xHB ZHB ;,t$8;q" d^#Z8*anEP0O“l3:xkUe:br/x M]PR_(Yv#M"&lD uN"ȁFaP"\(SdO0ҵ&Nu 8q#8A_#)jI%k\%D\vM"/n$Dź#A7D95#.>7`)! ""}wu»ωQ'ط!VVۤj5W K>%HӲ}IBHGBH>O?hL .8('$%;~S.0$,Km +mwT%W)qÀH**$H/`D>WFͳ)9SRPx :('t2&~Jt]%8:Jtm9(`_\FHp~ '@y`#zW "ԅ> UH)-#ω'0 e:j6q&*a|~N~'+wHH&r2D} w2ʂ[](Wa,R\)nU@1,E}NlI NTlG(+1noIezKhj\%tcP-AD@o)wSd *BT%7DH?'A[ۙNt|Èd]{MTA I1T_c` :X+\+. )]7 BJuDUfIItۄ6I@ N18ͩJp*+4x^,E^L)A))@Bi I go@D6quR(}` `)R Bl xSɎH׉''t"6)>.{$W 7Pb@ K^yJIT-jEX' 2FbAO4Ϧ#T!|SCEebKvx5]c =8ѲLGW fXCcW b@CJ $a_%4G,a1##Dt;#byV,ܞ @1Ŀ[#kNM8pނXۄsNa:!%\%`I؛ЧĀJhfH" `_#<5ZϪMh=T1 IDAT0L 1x<7:~r`'. '*Q1iKt*QKY0/kЉwٍ$ +$ FxAsS ;+".^/BL RP@0(@ߓ0,:P4,+LJLGfSؠMhKMZ`Qh*U┃oP*$Q$]$!$*@ ?Pdq{iLe>N %YF"}" ,1=>+8ѡnIAN񋤜Dp@ , aav(1OIJ{l@$M!y( >#(BQ("y ׉rN q'p91%8p+-oJ\a@1*̀ F];2,$u1O *ҞQmOd%0rPDdM!IaY˄#үY!r> ]{= oD Pdp{d 8P 6$de:&*:9UC8Jp8]$,}JTd\1IIxH0,e*`F%z aWI*~A )DPORI"':l +m"D$) *yJ"\%,$H)AJt/A& # qZ8"WF-=EzH)\(!J`} u,=oLGfS퇹6(I4q4+P v%a/1@I!IX\$tpD0 uo&KRLF,)j>O: 8adbD7T# en;N9+PHJptnwP.A 67%ħPA`DwP{*kt/aq'vѩ2-{ptoM_ Ʈ!w+1;>PMb c\].PA%kUmu5] ՐG]3| yrW\G\C6bm;Vt5ohQMW2&dw]A_aT_%2nkG03%Q,b( Fx"a,s~N' dNMx56!=;,n%:yȔ8/෭xvɳպuS吞3}x/?$j l^ʑMu MB dU'Up µk6fݹk>˶m;u6ݼ?Ń&=V|A?~͇mn#K ?>`s?9eӳ.y%,ItsXE"h F$BA!}!< )2A 1"I`x"߉.Y;\é)*'hVK S#l<N.V' -ѡzӯJŋ_7xs66"eu}Asq۶$.I>y宻7#^;DM]]gS_/fjxK)m+g־}ݶ@)hJ@FI${1 :`(E "sG8^bY\"gˢULQQd?\'AuT9PF߆%:'.ѡ%ixwc*ش/,ӫdUBk\4~}ƺQ6stq>skOYɿa+}f}ݙSbͺ`2( җD3"@Jy6 0 #-sTH9߾DCnEPҝ Dd,]SN,[r߄L'nl aE5IM0EiߌY5EˊH'ˀ%..:xb qYݲ7~q;95o)>sXbYDy5QmnS/vuyw%1+Iz=^cOՒ46F81l帽oDBER/g~']' $P sI'L%:^}M`n%8T[l鳷f&Q/&RnFE !F gp^F 1.{ɎJp Al2n;H-C O㷹kX4 Ku4s;7^^~~_gqZ9K[9y |wp=[׾3) ,bA8YSOd%SIƖ<}^2yⳲ^'o8o7l7/Y1spδuTgdc?u_{sZRɿo ֧qݼV,wԿ;rZGo-P7k.hټRLwC^c?s(%7^# S֘&0b`<7*rp(zODN91]t6RunI9I$wwwΉ+ +nmӝϦ_Ҵaᗜ^œM_udMޙTkk_'^VowmzL5F)վ ˮ4}æ&W+vK;Vw!MCőWSfSWgc阊C<7o3x"|!/nkg8KTl9ᦹ n8^/nbYDc.v~w|ُ>׻lOȫ^zz$H=19 `#RRXh-35y#Z^%mWd>o#(k$-~< Y?H9.zQx%xht%6 a&96ʋ#X1UÒus= $+87nCXXX>G gI @6#߼2uߊ蛿9l/XbYas)/5 ,`vخn/[^n_e=_ﵘZOǺdU݂cox/XPg`[7^Ϧ;ϹoB Rח4mb Jyķ%gt_oި/|[>ym+fBKSɂ KHU@1*;7Fp#w|ܺ~`|5'L90*ΉA?2r02# .mϐ| p=^bՓx_x="}g}滀brxzSNSu85Xσv7@.YOqZ3;iq;`%\HXi sq]>C uGœC[S6|y;m݂m+߹\ˊ{5]Rpgt:ۅ^q\8=o캑ڧ_ Mm۪6,xi,+vwj?Vp@u2x&>sjN8Q8l3G:uGP0P 0$E61PP^BUbx [dO: 8$UߩvX 46)48!4) $Mw¡DjOzuNs%*6=ny痟3,QOu6Mض(MJ(%KF)XF #[J%Ņ"PIJ s PBq/t'D'HGƑl$VI:.K8s:{yup}77-xR{Fy`Ӌ=j08\,mkȊʼn@v(1ikio[jg3DSY;M%(`J $ $0DW!^b6,^ϗ.@EE%@Q>Q ONяĀ&tDۤ82nW'1N@h\J~rXY-QӖ^عƍTHagV˦GMTPpn&<^%LwG.cQU1s\rmEqˈm_si<y}Q8M[M:/I, }!O:LGbz u+gsEJ vfq24⹠+S5~OWЏ$p-z~Gt5»oE7oym9G@Z_lb+*y c#GpX3q9aɁX=RX&x ؂W{&4̲p xQ_ӻ'rᯩa4y=erz-3f23ONu%_3psa%5mHfӝ]fbaSuKSd<!c` };ʨCrl9wg6%_ͼsΡSjwlڛF/0tv}G{'7|uɊOj>(aK-˔C>*! J1]E ]*XijL sR7! G&2w u'\=Yw~s`RŹ,S,zjrD+;kf>=-W(MͫWI#6XȦH6c{YIo[qEiQ$>C{9i<ˊuM:㶂gC#^ jOݐͤ,[Wͫs6P"_I$LǗzY׸$ˀPH} Tt)\('q(gG'x Ok8! {m"{7ބ<m"ka9 Mz yݵ(VK\)9I%Tys9_|捽iyۚou9_m;KlYY?ad!S͛+܎'ʚEJUoF׳kæ5Ζ/?va3R;7vMGM,71̉W$ӾsݙO]7fm빩]5޺S-SHcYxԳn鵰^9˖梲^0G1 9HsRDwT 8\DL6X!MDMt)ѡ9.048)PX_AJH1:ek{䅶:lۉ\~fиȝV,ڹ~SZ.?:1"c~&XL6vOD띳4(x@5G:߼MrzݶeVs r̀1>yaUlj IDATm;kްhOȦ&aź&i>>5/eт7LΩs8㑷:{N:6ڼyW7/LIcg̡o=o<дqEăl;ٴΦM7,{9xYժC/oU^J0A7w )$ 3 Q F0̓QܞM0/HQ QDfxuc0=KNaǀ>Dn:>va+q&;p&=qtE} }@@)۠-O Kd7w%Y1 qW!^^E%* ~ث_{혠Cy@-@6/ռm#WIE%1m;Ē%=NcД㛦kc-}W7~7uʏk((Vvג;rPRe6XDxP42F!g{ SIs Zd5^' SSzϤ|Ȋ%:J7p P(lJP,v~TϷj)1@@rN ) XbHDVS؂X"D6q= R14kG ,q5hҌEKr*h&!-DDd3D /%ț~!`/yk>~J۶UНh)ϰL<{o tjIO\vwel]f9t{{ z3:6z.M4W_P;xٓNA{u- [ͧlYv*,2ɊO*~0t{ޯxJP" 8` I"a(Wb~-Vޓ! &xuw\'xZ|njDICn(h Kt Ǐ$-K*fm_}G/-3hEM=Ww؊]&jJ%@ɮwqЌmeaw˶H7O_ 'WINo+ϋ^4|ZΦGNƲEVVsyqx쵫iz-rm]ǜcWLsRV:YVi߲ʺ bn>pg^BJID$C\WbT(,@D N=kyLljw~rrk=7:ֆ_p;&O6/su2{8RQ?1^褳~[ Ų`J^uʷZ6.Ɋuv_k}M,Ӏ5pe M" GP !Qo@@] +D«tG&'7L8Ρ(;=.:}^7lrS,8_Y;Ls3r߾8U^w;6^^ّ?\IdGV_}6Y(sڭl򭊾Cfx[QC)y+ۖ)SUp ͟yaNmg3`C@h9< x|1 aԛc9>!'1emK纤oG2sb|'ýr(m=caGb` ] fp۞c%oݎHsYc3lgiްnL/56pܑ} D9^g͟~9[Ը.iEY`YC 4, 9uʂjF:/GX"`sI"A da|HhP'av:PdOxrwhI.vtdMd5u- Mx`ktk}ſt<wɧ?9:m%ʷ}otME+Iƞڙʪ;`*יTK!lviỳƌIfڛulMw `<7ȟO^oN|$0[W{ Kq[gӆSuq7(% ?kYr$ q-QȈC">xmg;]x4`X3;|Gό_ΩK,ɩyüʙ:ݳ- bp۷>8e*j8T0\%'.{mU2'|ч|3H J& Itv`$qH@FP < 6Tr'Ӏcyς@A^`b`ޙq]?j?xGҺJc圚Y?bڜ,+֑_XonZR PHr;U] ^&U~Vg!;yC8J(M"FW @"(b& H! B $lĀ#4 N6&.cp&=1M'X3lboq4oźUuKZoL8u#m%ܘiӺ}u[<; ԟ~ӯJ};0`:u*ϯ'QݐI^iռygXu.xYjN?bڌyctf`@ &vL8bI'Rh,Pa9O<1t\)qB+KtT@k&ֹOp/j;ϩ}97u6o9 V&YgyqGx[N~kn,׬ߝzҖ\mg}܎~s}᎟9[]%Ŋe޼wʝ}}'fYdE%5Ƽ=/1JRflgw͹(&$+ oopoޯ?i/ЂI$Q HdߠCn@Q0 ,ΰ:7'ƀpML.h L;)ղ=1:rn>kK\V˪z.T/8%9Ym~7۶8fkV zʟ[ϒ=rɉ;=+4\vxyʺ?vO=HbߛѴh; Xn+ϼ{^dzĸIItp jvZ%EțS;< 38q 8 aF)qФ%m^r[#X2s޸˲A4Qjw,:/bl]rgnvmG$$DwhHX+ 1D_y@ E 6xupb&t $ kM IHa 9\[Ɲcci%ˊuzW3ikӷL(y1kv92ZX5Gky`ڏOkÊ3΁l-+YD}~!'r'ɚL.FZSy8gݭ ¡DX}ش3t%Oۿ;#m~%Uw\֏  `@ DI $#DW8b`\P+@P <׉'b106M@>01Tmӌ)@Q_pN W1hȰ4hٴ;C#YVz̡^:/X8WsSD}x/Њ'mJ8$  @D{%ս2Tt)a(:0N 8"$ a5 &!% KNy󼎋W8nf)K9dMՠ=*6-Φ;7,غuЎhܰOi/'V,vE?ÖDɀ%\%{܍#oj߹tU+j}IKGjʚTƥ]M5ѴqLgӖ]l'V<+1pcq?zy_]?¬Xϐ7j}Z\s=ڛ6۶5o[]׺eΦ#:7ݾs?'q0,e7S#A DtS8` 9EPt'0DI6q=.D%:`&ֹvЀ`ɪ%xYՆǃ/qZ暷ڱݚE =;6+c>+~PWI2\%|`ɂ'Kx\\#Cijg[:h`I˳:b_U\&6_uR=M?i޼8pvvyW-u`gI1L:C.ǜ<})m8t`qtWj mmhw HL1=3~`$y<[%kIי7jLIӊ.pxN\ckQ|\CInφsel &">GuZe6OoXKLu|suM@S65zCm3A}=בM7CLx9cO E#9vm WbAyYȳ.ᑟ1=tɭd|{N#s;~e2PTk,2zaS4fXB~nAi߲BЕ.Ċ`Z%fΩXBmg;[d.}5ʹPGuo"Neyi{3{A=NxhbYYS3u\gNpRkIi֍˞{﷍ۚ-P^JN4D fuç.(i`%9=IwG())dgJ |aݱq'~\VH6/TMu*_(KfCpk}ͦ. )(#$Q G"-C%.D Q\s>@ ʋ<N8~sSn1 Ps98 Nc&%k܂h,1WMcЮ!=SMh*0Xp^l/ϴ,zx5xγ!(?%8Gx#+NV7z6{h]H,Y#b _okГI']SdA&paZ5 UEs/ [pkL$/J53_k5G3CoSA>%j{7Mso#ߜnO:?YQ%>%Koj5-qzg> (ᝣJTaIf^%.dwuU3dOyhVu9~uNPU7)N>haWK{W9t|wK޼wl,YI=7ngɳ?3?jkX%s)֠Xog]C=qʶ4bs&Wy;l_Xo<{  PmOyƤn SnEڦ9?Qyz0%)C M|5/筇y i~"U;{m!e[,sϹx{㢭elkz$nYcbٚsKb.[z|%sV"S]kĞsL&Ģfý`v8w.v8ȽQ~^y/9K^r2X Le˧yk~?z}O&JlPk÷|gJK[$,+ֹG]>%zaw6m:clYMy~ԆQ)WG\m|.޸?,+o>7/7|ȥ~6*Ϭ>u6V]N=F2Xox.rN.(oug)noO c; 4~ w91I.s"`% QeWS-c[XN+hbV&I@6[f <u]ʳSKJeK}d;]˲b`YV,`ellܶ_æ#x_>K[6-Xz~`g*l;#JVտ{/V]/[T4tUUwPGx( `X%AbINv PDmx,|r p" Q0 MJ%-nĀR]mw8Qw <Q2XӲ78|`S692^s_J^Ȥom]KyD(Yga=uGK|&Pb I09bPB;pD@pId,.1BE%(<1Y UL оvxиMhq I!01Qŋ-ͮsϹ?ltbSN+nyH,QJc?rmm;=7Xyo='gϦivMuÞ>~^?l߳J ( .lD[H ȇ'1 $ U Q%3A8'E;IQ'm⬨hR$b6\7S]-cYS*[Y7|n{o"T%810JR0޳k˲)zY~#z=>(NZ9!K^E;֝ }M*^Vr#;/9C0)1YsĐ- Q 0: }01av|OH(138ѼL@ 4I.`b``Isv;׼Jwێ}؊%Zꆿ5Kx+r*QͿ];6$Y͛hM[iduC_>3]=iRgl&hNV6Yú={xYUA78e xB,oAj(p &(LQR8@O(v .4omal/Xmo`Ih`I67~sTUSmcSݭ=7ӳbdeʲ+>!% ,!T { l֟l0%86ݹXDsfyYUF<렯de߻aoѰl)+@JEj tp7qhBC`-$|q%QWzL{.!"򛰃$@PR" Qmi]%(.C$MaUMMXr3_h;>MnM I`X3% XBKDqJ&1_\pD2)%EPa)(@׉'EV#mݛKK+*!@j`b` [>4;#t%>A q\~IP"MzH4$a,r'\']`&y)Q}MBpHI/0#%8Zc@?EHwH$ 8e0ҍ-/ ܭdE@tp0DDGfAkƽqc`\b_.]eJ((nC$p$C ZDC%ĀdD7h3)1Єer,hbX(6|K|/W s,D(5֠$D$ W@ 1<Nr 8 [CXMeIn%v1{l sJd7u5$0IB"ᕎ@EDPѮ : 4!oD5hBL<&d`B5hݛb/uPA*(ПD'H"G 1"I`qNL۹J,ѽ_hlJ%4<&InXB%^i\%8a/$t3 3L? 3L Qt'hJvBNۄX]&!n+eaMzU LnEsǫ4Jdp*1as%~wQJ@(`$C0 EWOppJpCCX%:4q&Pc`W% K==,1ğHpI0L`ĀVAE%$$#F$ ( ƒNc"':@<&75Єn\Y:XKq ,)%Q+ $jSٍH7 HV@bHxDEwxpe:Q+1Є,Mv:> ,)E0]]9,ASQP "J $'$p9,UV"PDpb&mkwahB;&txKbS|%ck K0*R~c@!(@;zFt*[VDAEG-8,з$ ,!c`[ %", K FW%EԟD$ ; q& '@*<1.1fgyߵ߯s|xl q&HR|i i(Q&RPR+ j#JiK0`S@ -!)6-ǭHݔ$p3dyvx~ϳ^Yk׺zK:3}{޳߾1=4٬uFh"9=gthL@D%yk,_#u/k*iJjnI4ȞO^S}H d!1n5 \-*@E#tlp!,W2D$~c$I%$i H(ptkCR 4( Di>)'\'N4D 4=ΠIޞ`dn\I0U,KX־>"8皫dq BpĠȂ*%`#T(O NTNӹaMhM:^ߝ% &H,Y,U#~cD7(P潆AOL Dxb$6&4G&k4NsV%%k,! *9X* $qHX@JDP*M1EM I%ФyIdZL؀IޒnJ^ DHOQWskMWI/d nZDGX@(&@E9<1pqR:љ+c k1Є L-`#, iXR+!~c$!P H@D^Dቁtp2\LgA C0D3,K YdɠgH"YOT#m()}D)<$J [HtS&ZzKF+yK .W’]U2ŠD!$${=u")BEh'/շ,]`I~}(muD)$i HZ"}h(7x4p2dI&MX 앶ЄL$zK6QY`I~z%]Gpzt( '&D ^ ,Kl0%Q Mܴ&ɣuIA_%9ݹl驷DSɫu `ֱFp\%=%I#9R ,, ,mR)cJMKe;4&8 {K edWÔֈ4rh( I$GL*( #<1pp p"i6Np}6 IDATWMesgLD,Iݏ+%*HnC2j\$GJP*VPDQPbת: h1hك4!?' wh2z +0%3Zo `F %Dpzrߨ%$$d8@ (ቁG&Dt 540㻖hN20i鹷D3, +K"k, U4P̂u{$l xfQ%LIQO`>NȎp2zLgM1hn9-$ѽLTEqd:zKZ’JaI8 \%IUbij D;!E;L$“j'mt dgѡI}&,dQh%,Ө}%"8 p~*( h$Y``H}e0PAɈneMIosF3I1Q`2 ,I+%K1XB[\%i%$7HHc(r9Kyk@T4l8ɸB(c[C HVi,0"c%k6lY`I^BZd頤Hh#X@PD Cde_nTm2Hp$ӱ0LZEsI w­zK=`a`kk+SzVn I#Cd%V" 5DO Nmyd &2h34&aup[`J]UXhsp@V61 W0E)DQ O'c:nBX& duFdQ^`(eҎzKD$Ep tJC( az %62QZu0cdn&QM09g}&5rf% 5(Y $\zG` u/)Pk$}m%}%rWIMkP"57H2 H.k7@7HיQ }JCclw~'cWsv/E,~xkV!{ NN.&/S1AhQhrx}rpu8NH=wr1h~X7؅XSnJ}kg =Yקl}Iy>d]]b&sJzM"~/^laB(uDD,كaij0,YbGWI PRM$$jHDZJKE%}RuDI/nNZ'8Mb%ƛ2I k椬9L,fwQN&HP\+K t*Ԃ$RiCn}׏hUE@ɊH“h #8Z S "X&l> ̬i%K,I@ɮjjvpBq@RGȫWkAB(ɈdpIAG69GXF4g]ۢ8kj,eqN\% %I  !N(;'\!!P ubdJछAUФq=.!z׿'v0dQ-9[b$maH]%@7ID ‘@uf+P!WdDj_&Mf6h2{J`GDIĢ8\%u ֗%J MII&) P5h!nRS PÓŁ&#4W7DY H}&9Ċ^ 9zA+e"N1, āÒ7-A؍:HH@,p P`Jֵ[i\cp & ֢9{&^ K["5>X 4!>:!,+1W f%0HJv**@8C"8%cJ͡3Yd4'LZ{ -%Vz%%= \%G%I  # R @O4 AZ-}&K,t!l%K&%K(( h#9;QВ֯ OF'6A⵵A *2txIEKP5Xދ,Y(ii HM𣖔@\Mddin&,&.hb}&IZ0tķG wa&VEqժd %KDqz+y5X/}%*R P".H HI-\^:p61h`DK4'e/I<m `. Eu\6Pj$ORQWK‡-((i&$ZɒHLP|︅%%VZR;vJIw?۟!$ZYҺQn)BBR^`MeΪقْ=9UUF e?^ `׌˻:z<2wga%K%#JZ걛ڐ! 1(RW͝)) uqһD-41Djp؞LexF9 0QTۣ]ҰD3,%K dWsN/DQ&wn CRRJ9)c`] P,ĉ봊tJM#:9&x?woϩg]7wHne"W}Lb*2)+}U*,I^@HBñQ8zKbE'!K$3,YBA]$8"E35&GUR}wv,) @`Ġ TJ\*Dlr8LJp6Q­K|{L.a.z.Yrg*,rh%5]w Jv Z$rEt;~H \*7S%γqv:ĨNNiI@<؏,>c}&kjrXɎc9-CwK$3J5ź8Ek*%T-ts(%,% GPeړs C`%9RŅvN1q SD'MJ":;M("OΙq6DsR[ָ eMW$skY%kZ.7zdҎ\-ٜK宀:X2#h$8#Z!Z)WJ ÉP:i8m"]+4,@&\&XsxD,5 c..D(N2F,od(F%J%BDB P0(4c$}JA0b9Rhe3;qĜrIYk#enSEB~oq4q2ypeR e+nIaNGr-{%cwU5G*.IS`ɾ`ɂ\%A ;$l(*u2T)Sq'M !CuFtpf7NF&>H-hS4'EFsZ.er~@TyK_u;"K>d>z#lE4zo]ĹbP$^H-8UH8O+e;!uT.M-Z+R<_&kK`%F [,}=WAx]K)ݜ׺uv͎F :L]H.)R wI%'cX@PRh#<)-|ϑQ|ϵD!8O\']:N&1Zn&KrJHDK4'& 7L:pu1w2&^=u..FѢ$W=ֈ$vί)[]`WKA7(9I!, #$W),"P<$7)'%1nA4h%Ah$& ќ#vXSn Gkägg1AN sD%$8ħ⸢Kk**y>>xidɮ d$ڠ D 8@86.5j99'&-}&CLIz 2)$h1,ɱ8?1®J%-^{[<}DX(-"K J[K[RaI\P"&$QH>Spv-K(I.8udDtHeMҤ-=eŕdב&qLz;{p]FܠswI%Sq=g`X*d`PI"}-A P*“खDD 4S 4)ԨaWESsF(VEX2v0E㱦LWʥ.E83%ڥ]X3]KGSk 0Xr*ʠPS}3_ O'n4AЄ NΩPhN:*JF^,gVJ\&$- R,i v Vq1Qe"I"S$aXKM}%=JJ@ ;$a#ƙo}n MR&DtTBw3̌˄G]&#-SI8N1u^%E]+$fIG#M$U@ɞ@ iCR[Pa($x":I'Ln_Duj&ugQ4Gĕ寳eAI%#Keǜ*Ώ:%ix,I9`FA $$`uB)3njhN"hɅ&csUe=bxi.b9K%WRI[eDAU8"Uqpk$S8J^%RXR 0@Vmޟ;_q|ቤDHMM3{[K4I˄[ SDG Hrjabqc 4'{] Y]+ģfQޒdXaIj 4 !*s(8u04?hF4eBL,ˋx! Lqc9&%eG\1CbwIQǷKYUa%di%\%٠D$iFJ3HOf urI'5&LPjh)hW `% e˩rr%F HMz8dq%Wg1 š#%JNHRG~q#zkaW QR vL''U[d)qrTɞ*2ᐹL4 Xlph %=8U؃%Ssz$鲷CXRU($-Ȝ)sPPFpRMxDG+4s\Ib9 x"w8bq* C{GD8 bKT]RY.ũ[XR($Y<.y-h D(s'8v C3q1e}Ѝ4Li=-1&K@An IG#UO]"&F.)[S$gÒMePIpDp]7 U2 +'Y865@Ϥj4@^heb܎GKsvp)9RZRIqA`q4SħXkwIfQs(%Yq2XD!I iFJ,H>'DMHBV3xḭG]&Î !6ㅵ*Xɬ8,#A^I.H5*%Qq({%b, ֐G@*\y  ۄ!4.#]SaLᖂ= sK!HÅ0 p/i;K%qI$a-j’]%"$ Hb"x N&L2 4q&Y*EhciRZ]&WcV~XI*9B WӀq4UzKv.䬙zNao K5:,Ɍ’HR \кO(84Bۻ.`3渪Q+iIn]&03cR/)VTIT8hQiq^ũU%Ǐc FpoD}W \0\# 0NHmb+ 4)7U2]T&x5xazLzR/U%q^qP$]]R)Gkw Trwz&/E܉]pFQxR M{M+\9e6p.k=atb91? {c:IM_q6'^5HEuo’VܟO"I 9YoxB;M!O:9Dm"kM W]&XNrcY!azLIώ/Q )pH95ѭg]_mM{r[-%"xD@\;,Rr~8bJW}uǑTe%>7DUz%=hdG0F8\wI(F%97'IM0ěo~26}!yr'cݠ)ёӲ$Kќ\3\*ˤe9b9`f\#h=Ŕ'V" Y=$$nȢSLw 5&(@+Elģ\XrJ$pVb١H$HDs$ `9fXD,_/P!HH)FZYWMْ{dVC8^Gp`-zh*y Kz%pD@/<)p̂W5*&SsrՃ+E實t-yp.dǤWtTDP+/)R:bqsjGw (Go܇MQi'oEỲ{xմp8ឥ"!8΀(Hd ]% JF$s> 8Qmh2Q19'BV<=]&KRy}5~(S'='\Y=DW§Cq()5t$t0Gq$K$"8)38qKתG>;hpX=MVj{HB%^c*AX+xM;,)p$iHRk0pR6 0@= MRq]&&&qGbak%H1J"AJ4E'i.|?d$%M ɉ]R a}Ò7En>=Xr-Xolw xcrם+s q}K)srUFU4frlî1ri9^iࠍ9uE!۾W>G4>q`2X9 9Vj깿Dzp(o*-#=FkJ^]Ն%H2(@!ɜNr=hIФdFl : J4bXlpX$'qwbwTDr$)Dz*|v(-w%kkZhC1'ͅ:R_V8Gqk1KbuHDq ^3E%ŠD $ӽo<$p@> .,=&B)2txQ{uu~7MOI%>Vx Ha9,G1Us ^Fr /IKtʎxzKbM,cK>vA6jEe!Ny~ºq)qRq,1U2g]t1Hb@a@'(G#uޙyI7ך>94f4M̈́4'"5ZqDqTRqsF #8!Gɽ;ߋ/~ܽn qR6I5p=ȝʍ氹LEqhDzLL&&&ɤE]8qh|GBq8B88 Qm[ N9<?#S*9#5xSI9hz-LDdKבEh6! _["DuH]s%K}_D=:)Et$&3~$L,XNO[E DbFS4105V(0VuK4׼7( 7w,6]Lw}%IOIj}F4qu?dGSmq8~V-$P=w~[S}CO5aٍI5e7߬h~_A8#KdKj’{H%$9=L&&˄.$Tg~%`#{38uhj`\KA6!uOMK:N(NR`I(Hig߁uj=lM2{Mv:Mgx^19~N{OzI"=& pbF ?pSf?Si1nM@tؤEsk!)/Q#-q2ԉ8s*%P H>(}{؅&sR 25KM_5-O4)FU,TY. IDATYJQi85tpiVGo56?ZIMɈqawdZB$I>c&0ezTx6}herN L9#r&ʝ㕆¸z4!e:8`NTNZob#lRX_ d[J11j 9 #4IK<>C"LX,)$mrќk2!*6^Xnb}P5;P6u~QWmYK_kPd*iz.s/Ij%%|;%>9njR=T @98p5Bej7%>ki$V6qǫѾ҇B8GsGK.]U7MrKr _D/)㰨dotpr`$BK??y{Nu$f8M(ќd1Lc9^)1\&7:RN>7Ǟ/8djQi.^SEFӇ3aMJGIW%""z>=}${. zL4-~5'e&b& 1"MFTuhVO 4iP%RBow9/ L!$˄U&٤i1 "”9&V ˩001`eŢL ~xVdBz*|/;CSA_XvhBw,'Ai96QF +}֛%.^z9*J_M8LI%9qߏu2,8{-` |ݩ ded0iEܾ?|:UI)aHa _[;S[)?U⯄c `=csR1b(i"`AɄIw*=Yd2I0֯ٓWNjx5J:Qوb}l LtCq䦥4ud< 2$Eѽ~ L&ɤ@L8d2Lh/&d2*&&ɴ=LzcƯ.FL&xL&01L&l~3לceu&z&:~vLeȀd2-D7\Sz8^WH Y'cB,{BBnSЦs{MLa? L&n^Eћy}_hx^#[石?;zľA8у=xS LhIPLL&*7Χywow#`A 6<2XT֫{b lFbȺXq01LKeЩ3-+bNqXwmG,\ 媙Aoi=~=z[7г ~:ALdYvdK5Evq%Vj:B Y K%w~D%DYf|fd+qKѤ%[Ldvlsހɔ(կߤ>z̢GvԷˈc9@!4 f@Z޸%Y\w"_c$za#ZdJ*^4cg7! m@5&Lz7Cn;}V'ۃcu /H_9HDa/~g1}X=cqۗ_[ӿ՗}uh@ Nz>49NXrȂ%;jX;zri:e]n~U`SzZDWˌ,ciZ:~iH4Ⱥ;b(~ucOKc9wuR䖿hfiQT?p1z?vX)QTVY3_HML&jI@*T"9 &e2IJ#řCqmI9:O)~ֳ^len<'Sq9` &w;[qq:/~BhqךC1BU瘃D4ER׵Qi&G{yBܜO_qݘܬ:"X O` &OiK|zdt?z/ɕQOu'#&4CiR~U`+75tZX`T sԌ+G1"g`#|:kcشlbP$NЙj )Z׏yԾ)XK-]8T5C?lQM;L4L&rbU&tTSi>;]/s0)J.3ɒi)sܱ@^Os@cEUxT,܉ f4;/Jͅ7!E>`fEHqdIkfh"53NNQ#ʤGW,z++ô NIOI.g @'jDxNJ2WDoWqؤ]] K}[)R D4MqtU4Z f$ҟA-M,b@8ɝϡ 稜wC4h29[":?38 a D"]ªq\~/|BNOWMit_XaH.Jjj۟w^S͝(#)6)Ë()V@ec.4\&ќZ}&@42zM 8hË=¸6UMA xUXJHe88K/UcDL<5C;LL&W{s0#ZDNIXLIτ'@'nQҎK$8 ,ath+{ 8<{pM"_)+|JSLQM+Z7j+JJsWbyb5o`4U%Oc6SDTܽLt)?a0~c8^Oy9yaIޒECK(8 _H6!= L\uU |,M(7R)$GNhN`6hF<"p!)$@s*I+*Òd%p!'u_WL}@w+wRҤ\iX)UM 4A2_EL** (ijNB ir@9 \lBOnma]{GJ2)#rAP KE)j.Y8j ʍmȺZSK6!goBNId}JtO7jF 6)[%@_sNqN;-G_:uZwks*MBX }V,d+9PTA 7 %%E1wI,rX:* ܳ=g u٭Jz;L\6`M1ձr8#ǁd1Bbfj23<:4x&38*bx[ ފUUőK68q䑬U`#j1=RU%g<Lj|``Wc+Ob}UMo[t&TIwiwV{MۤxQsB;<*J]KGq62w>wqɅ?K/U` Mha4)'W=.j!QO`pOYZk'\ie@I%>9Pr'q ++P8Ed#4|XA'$/aWF4G\c5%I==8Dq`ȭM57L$ @jMZ`,P I)(f\%J+',i^:gYJJKr]=A#uIROJ(e R)~(=0R4JI3NgR <$u̕X?ujIҚau|53&05I4AnͲrMp s260xaq1FpX85GtP8Paq~ ,z_⪧Q 9%ڤ:VZ,V3)auazMnAzi95=1Zb eTp-Ja08h!D "z$ (%A(S2N%jEqpKqb*'d^XIzrT ?%kRNZs(\txvTrpL$1f}jN(tI:@uRo9Hȃ%Yy`OQ\wXԌ5^8N)#Vsda9$ c)X{1!!' iO^*2VAO9K`)E@Y 0mo\o]xNJ:OzS:$w2ם-t JK{KzuXg"q1U/;vxz,&#>M1\ Vre'he"Ҝ&8MDIi nqw\p;PZ( $[BBW `8 , ណq1~S+H 5͂Jb=&"Ey5,2q_%6M9$&J4&kDNx@7Dd c’R`(IZ&K!;8XH9{R-L4Oʉi+184V$וQ{Zer i9_w$n'\erX4hT5 POyC0%9:<; צ@B@/,-al]JRew thwGE@ T09F3r XNH'XI;FaXNתQ01Gbpr4`jw]¨j)ypNs}߁f=(pd*%5zK%T. @7cGYGbq/O]=Uccfpr"ʀ#LR;f8ER}& L 'H'@T-2C UEp}ħbX[UBr8y1KS߲e4 :8c?o~~H2*IU%%(a 0)pxaIɚ[wI^b6s9s070BXAXo6s\qI|<070IT I`Nh"Ld`ӵÄ20Ф `d&A 02b8a$L4L l֮ M>p& 8 PJ)ٿev ocVd+-0XT']!%Aw 70QKNɌ;c䑂J0\_&rĔ `Hu80 g.eru]&@9426$sjNhMypT'L/=¥G'}_$G$(gv`d]BGU1Kds0QK׏8ztl:X IDAT r(y7F?5k.1MĝԐS*#F=0IUrNJ9CR&?{Uꔉ|78R`hpװڹ9{ ڹ|{7S޼z7>U-XM |K`u~?s?o=?w8{$aYˌ^u`5;.؉+KRD%Iր%)5F&d ,~8táS^`u\iR@ng9=7@ iөZNVÉ9)(15Zzs"X@~N2&ӛrX`u$J l)?Oˋ}|8C_}:HjU`Tk^1DqrtlőPkwQ>SPZ[q8&nw @t{}+H7$횩‰:j2y)w};6f.YE2ٗ6sD7No@9M-4M'@eu&\'%Γ\qV#>?};˳?o{_ʱ,ǣs> *I_}_K%%^zK$Dq#\o_]=㕆{/{eApCĜ =&J ".2]6^xq;Sn"e©]&(.1þsGajI).z(eB74X;MznqnI牫9sXGg t~~O>ʿ߿ANȜ$@s@ @%.(ÒzK\ipJ[w wDgi_q oնK^[e$}xV7t7) g8M iRmRq:IqpOBr?ˋV+_gOJybM%5]%% K^Rš% 1Kf=6U׹,s%{8&դ'7Af\-hehF ZexO^2I(Og @'@;zzfwt_|q<HUT,r(Nad#(a!I==pß==UdIni0GR@Cve;XlͨBiJ&3StjMpROҿcZ.E/W}oC9'=%n I*W``X 9b#'bP |Q{%]қ,C ܌&j"H#{pX I9w\&<Ŋ$;bvE:i#eĽ9&9n<(ݷkyóW{>o))yc$@ uσm%Q]1wIp=W!𐽯O^N=ĞXN"zV%zރ +NɵTPMO}7SgaE5&_L N\'{@" R|hO__$$_g!P$oJ]%uGd Kb-&T2ADSlhTI0BN p:#<@"k ^+]~ǾqQ9eF 0JJ`I1%߿К^)2wEz3H}^8N&jpS-=^b9m՛ˤ]&_q4τ֣ޡIxMԆ'[B.DX+͟?F5#$A PU2,U.)\S1-]bey_oq2XQ+s쨅$(>8LRϫM#:%nMLgr"8T!GG')./W'ϳN>THIMNP*ꔻKV}KTD8;Kc9L,8]/*-#v3K4gG}&ATsT'@O <9ا\3UoC?Q49@&c @r+wGK%!]%\v[p߻ir;oBA9rG[oz&{V]kVG;pgwxCE^oz.~_Xsv=9{]z}a`#= %V53^`n5=;/: ,csEϮy_bud 9uuuOw(弣sΏpt؝ϱ<= X]cuK{C:gX]auSwVwn6qשn@Xmm?B|S_Go/^'om[~'+?z~̿Wz|g赙W;Փ]} vn2O_&]Zg]=~i?8'@6\Òg$e~; | rL`&?w}k`[dAF~JKiCvw:e:WPzz;osxe7w=5&2\&;2VhNyR}&Kr&@] 8d\'@Bl'}t|7}z\^`w[/zĮxs;_=CXfrx<^ Vp0w}["cCQ'pO?<:%\s罚p̞ܓR&x%Hgik-u>_];})Wt5^M/|?/bo<o_FOPħ$xE)%X4,GqKR>vGE`[]T/9f%U:LLkZ$iM%ʨغ6NIqH:M͍irCs](6 t" 08i:*rp'O+ōǟyV_֟z?FA2M:JFrK\Ò9HhpwIת,Z&-z1CEI{\k>`uқo?9HGt4ti8'M~2Da)O蟖$+~k &Ivxb7@Ii{% %.TŤ=SSH`L9Xv,QAƣm<I[8Oc&稦dc `2& LL2^3wX.4@4~\MAsK PPNT ///&[?{.QPLPȻJ%k[V.Iq?k..I^/xԅ%@ds4 Hhe]X\&kv2twAcjP &@ i?˿g;{ٯ~@ PU, ,^WV3$ej%5 ˙ȀD2:LhrN&@ND(&bpd#zA7}w/q, ݀@w'tn`\ X X(r@d83k.OaM2F ~./'=򗤶z#QV5検~FZurN"XF\S[2Ig Dr=9lU4S~8zGPk|3O_>}]"7SE\BWTJ,P[+]SװPnC{KZGuX,Z2HA+esLzpEsLRזtSKaHpyvO१ ]OFV($٪*eDD%@Q@yfMsה%le:70ٜg"w5eeg,Ntt08d:[ 0 @9{G۾z/A|K̿] lSo@ *Kr%`$Xw G#,dQ_Woҿ Y?P|_dh",*$=HDsRD\aIIgڗS"4*97>5b:QaqVnTugwGZ!7}7H{f+'v%sћ P*Ɍ,{S48` EK"©a Y9%A)w~o\^osQz~[akzl2KE(\ouzg4;:XcuyLsxgC>1O5c:$5}: 43;X%@V>9ɱV7={xk_^̼SyVGN-S=:qg2K<ϓ6w hW {iۜCX/v/oR֛!;$`2^ X߬ #vb$px("܌#CGtyM&s&3J&.\@}Jp}sqNΏPMZ< <'deH*[Okqy^__u o3_HM@x!l!m I@ @ߔsxߔKÒǑd*yMqfro O@&raH$cMJBrJ%;(WKK] 0qsLUeLe2J4TEsQ2&t4Ir$8MqGtjMxnIDy_Dž)ts// KF>//S_w?ZAUw or\% \K}DqshAͰHqomZNt2a1s|{P;fd̈́9{4M ,m:Ny y*DI؝7ćs'19I~7 PQ%K6kāToIa`5ck"xKv  \Z*b93kw}#4&@1$cќYi5 4I{D'|lI4t` xeU""GR!1VATT@Pl-)KK,rB> -%bU;U+(*X(DR d*HH) Gss<}}>dZg֞* Y͇5/jHS7W=?I0n @qX JkcA tX)Qz+.fVGr$&xKJxU>`޷4.E2QK4 & &&@i NO@&@Q))TUϺ>Obo+GN_t?%:(V\%XX4 "'ǠV.٭c6.q& -GUcФLAyIPLi.(֚y!My5"O@ @;Įa tw[jkX\DC LL)]&ui$3M\@6!DX@y3r??^qKez#Wl~%@]H*JÒܒyU̩jtww 3L@$50T.f2ޚ3VKLC : >G\NlU `_ԇX< &*KK@hv%Fu] ᯚv2\Xk\ղM :4Q:8u2@DA=ʇ\<Mp{_=w?]@! tPbaɶ:,Hܒ޹KZ%` zt \.yLTu MrL@<D%1sMDV&Pɠ\eP wGp5teW{o| $$I@T.D (J&9_7GX}nI Ds.7X3]cvFY\&L"b.53Y7@sMb K@f+twIk1d]`eH2(eb\]&; MfҚr[՜ d'JBPè}͞p7V]]x A Zn`D"`W(ڂaX}+tw/F.ْ4w 3UUcwlem 9I& uI N=xePJb3hqйkXWmrt5_*nj>w?W|! IDATK?54HUA.NW oJT{Uwx/@%=(Ta.%=sҁ6fw9;<$(~nhb&@OBJ}ϼy|ԮE>ONVVWV;-@ >V{xϤaTOLE.5ؾSm>)k}v?wuiq̀{}M^4{TNYN)WgXjWcVG.qw'ǬΠ<}GC՚S))f#w,uz$)<>bZo̩ qY:/|gq}t|jhڿ/`ttJϾ˛ZxYuқN~>TK'''}}}3{3p>FgCs7KPEGFbpX_fj;<8~oKNV%ӘSi}T_%.9j_]by򽱽bfsL9LLF.f4mͱ-84]In 6m:As|^q/ݮCx"o<kW'uwpM},Q͠-7vu%)oL $OkwXL>V՞. 4գ~61gh:51qEhꉞgndN(4; W6y=kN,]]a;8?f}7'.1 |],/PY`I (]1:,U, C^{[%F+vA}vzvɠp`_15.l.-L`>lxԭ9@&@&;y,u kZU.px7cj ^{3.[ޢT2@r@~P UlWno%r`qz%\wI ^]bzGLieRT]&=}, 5_5 &&}NZt IiV 'o.$FWWN]to~O})c$(Ѯ% U"OkGA:,nFuKbadKFMeA=~ !Ru7r<:jާTeR!?\G"y1оH:7Vi߼],z07<;psx@0(7Yu=D k~җtXUCc~8׳IQFY xצi1uZp2w3,ɩq yVVؘ*|r+[IOB`I 7d }}gj>y)(p$c C^77OHLghHh7 .{:4 &^&1'XuD'?snu||| ]]u?xg HrA1( 1> J< qĶ+ sQi> Kv}0ȝ[\3t3,UL v]fjɚQeCVT 1s\B@$QĨMnqI@|:4&ۼKNt{o}v||tK|]]鵺~_o[$u@3oZj9u%eC^7$z+΍$%ْ]&zFi!7rF "ߡɍ{}[tTEg'%W6]H 9$1I%>7Y]%҇ŀoqʭ[anVso \93K`LL2yR\&k88WR8JkF$ӡIAhEЮ^zʬóu蝲HI *(7pנp"a˺r yu\ܒ%_zЫV{((]&$&G̀kBb(4jitj9V١ Cn,Ca>81}K[yʫUy7$hI6(5(iUWpWX,iyȫrMIG Zqz !XK@L$L\& 16Er}I&; ?s 4=dؿn\P$; z*s~e@5 IqH=4*)0:,dsK.j%f.k5 1gIÙjkN:5th2DhB-:U& M!:~ wUHGۮ8$n$[n݌z@M Wnk];,hߦ^qx$0]&΁Fxp49* M&MvpdoImJԸIW/b]t)I]$rLcB% JWo© KB57XK!Y4<5]-Lije RkNKC`čnCϽhEM|yM Nr99oUgWWa;h$ I$iU" K>cXZV$\#.]lIK՞Spr.}Ɯrn'{V5LyP+ikLC-&<8buXgu;3 5:8Td\>ղsOb_\}՞i_pϱ:k _/Gru{/^;βڿhd[\7Zkn\[qBۊ$gkuKޫ2sIZf8Du5UXNr;M\jm"~ 8e˼*R:HqOĖå&PQ7\%[p:,9/Ī@XRչ%s+N[ODnjMilə,9I8/%EL\kN:TqI9B&#&PE'lHצ#dՁ%G31IM{_l dU۟U:,Q %n5]!⸨R0$e;6ˤ5s5'?T6$;4;3hnЁ`h2W6g.p9̇թ ?UGWWaC!IfH"@JKϱdY y-|r\[qr,݂upIԘe3ˬ bNcGt3g2%<X9ÉnHα:]L=JsM`{fVHl\Y#) 9 sY'\1ŏjW]x[:ۡ"瑸䲥0O3J (osU2 &u+D^ ĝِ⸨ƠW_w-*,"eC5)LTJ1d 7\7ܲdsMM2z+ȿ%_tuju]oS}%&rqt@Wɐ;U3MM3%5B:8{ъ"Y+M&51(Na5ǯ$ۡɍ]thA¦mRMG78Y'?8|]]tppv GJ$Q-7m7!3JjY5n-8 K.wsKLq -Q8yc\#< J2Lr 95*g,n& @sMRM[Mb q N<Şy* or[[mH II 0W%]%1(3$W%u3HX"`nIv~í8.*9u 0$`JkNj@hdKR 6uN$oV.Q;З?/$$PtPbq$ڂ#&p%LJr’wKlIhsKBT'a~voə$9֜\&ͤ5'4 QvhFt l&!C^N*N|'\}ş,+s65!b@:ςBKfKr\;U,qܒT茻LjL|hRQY[s"{<p&Aq3]7 ti(47thqa!~٦#8p?YdN~UNu IM2#P~Sk+ȁ%C%ؙ5u6sKz+NV:L,Ymeb \&Фl9#y_!M$3Q)9s&%&ОD8)ڮ[>}[N|c>1usjrBc ա>'cnǥ.q-/Z"$.'&@WS-?elafYKsJ$ #D獼*xSC $,D:2zÒ^)8:ӥv_wtlQK6һLkDG$C\4 .M\DUO&˃& mb Y@俿z_}>EGgourr|{8?j84X;uupM!zs\l~(LY2$UK%P\%0Xzuz`.X`jTXyZ!8U\NY oQL[ 4YzkxBD7bZt*OtG3y-VlM_Mkע_{ˏ^tmn#PLHnF1[%0wÒ])AA$,1[8%`*ONX`95\&ycc6՚ QJthRih[{ ,m~MGeni(P旤+ϼsgZN_7W?flwIJ@r! , wW(Gl nKv%ynj[qXEyZsqgҚcۡ[ǖ*BnmP T' @}q#%LJ\|Oski[_on#P6nJ@(F`I;,U%_sK\JkLL:4C3ke&0Sp78dPa2('L)k?>n8S]N{_GI$Pq>(nJ m tXaqϜXVAJ` 5g*[kNhC`]I9GUS&}&.86 d(Q C:X9+9O>]N{?(nnhJd+KTZ<,YǕ K cVA~zk.f#9L%rdO-h(74n֞DYp2ePROWYy~˾t @:@! Tvb%a2Gy%5~%k݈+%P]`mL\Ɣ Mfњ3+b!v.hCZ:8*8<eP$HK\~ɷ>ǖ˸ DL[]u-/{3AACAA TnmpݻaZ]۫ud{kd& 5M31DfnЁEMPJS W^5W{O_q<᫾w)WT$ / @0d#,9Q*LavPXr%`T8€ c&XvhhU@w8Nur PD+GP0?yrI:(A$u$%РiJ/ XbwX"gn w0ޚ)QW40$4Xhb[tMZP2DQIXy ~kעѹ?K~OzR# LHbA!_+׽%y,@%<5`գ!-53Ak*3v95.<⡆FMAhHk3(ua-;S(āW%.|{^qg>ioymhh0F8e $!m7IJmW m1ha>5,nXw*գ_rkΨD}>D9thbYd@;nX&8'uu X $}N͂O?YN.uY H ,PQ\%:עauOd{y` ȸBx!306ߚ YTܨM=DInUHRgOO.]N]~ŋ%"D/׀#IՓ ,t%a˞X:n,q뽇>dsL@d+>)9hk thbi)  @؜}(ULT_+s'Q.^x/% j#P  IF Jp% 5Gy%aI)XrZqjUlDL @A([sTy3:41Ԕ|tp85D?"Omj7ɟ􋎏nU#N_|~s>`d$ @9P hUb?ʼDo-knr9OB{<9be5ILLXiɧ\@=V1F!+5CGOIgt7Ils<8bM\~.\jC>'{gU(498Y9zXs /3t*9VVhrZhr|+49:` MWWpr*4^@/LNP?@T)һNMiZDwXyCvϻ;^@4,&zЏ;G @HbtPV (>D}CaXMfya2cBoѩ9.إ&pdvlJRtM IDATWĎ:1V)cvogΝSZt:?)o ov# ,(2V dddI$$G%k~}pxV斨XvkthR!}XkZ\hn @v  P )*BkϼgG箿k*:8s?+UH*@2)J`ٮ;`IkOavl7hBymun ؊&v}\nq;y&`Lաɶ:4úsq@]pe眸9(VgJ߷Oܻ 3MAۿ;n 2Uj@e -J4yShUstX}`ܷ-0䝪U.Uy ^47pҡ=S!ad1['[(*%+ ]w_>쵟\;uy•k`@u\$$P|osT6:,%`ܒ&[sZ&!.ZTdW-Chl 46Nq#Zvh%쵫=5W=VM6|NOXZt#PLr@^&zu6Ő K$+Ku-@sK6IkzoKDRkk=SռnC Ir&-&0Cp\'[1 Pu| W_׮Cޏ{] ?XAȠECQJ4jU3:,qW,%ĝp&}^*'4ݺ Mq@6N5]Ɯ*uW=v|{:tmS#4Tk J@PnQK:,qg}b斌%&ߚ@LT:4◆&\hm"t@;hU)a-s)~5]N5G~=c\p $P$,P7І)Oy% h+X5p}!W-L@Xk(ngաGM&%&0?p\'6d M3v8 Zq}kex3PNKWp2' 8^Ss0d|8RJvuPx[5㒀?o͏H%qx+4_X A"OTK$6(-8S׺:,ٍ+Iĥ%jz@Hh(tCSATyI=YH&?#Š'&.PX:MtdcNnN1G/xub'}ҥފs$N!@20_}r MoEϺ/_ B FJ"q1$q{:(ƙK =ĸ}yݩše&XF䈕$~fnQ)pЖD9!+5ǵ\IrB>5i2-: m0+& t@UxbJ/ r)P[>v8\~򔷽"I0(ȠLd/BJ dK2(Pu]s̸(@PApKA(epM>dLr3qg*h:40$:GsfDF@yx(LMo2Vs_}2tMn_wu'K4L*H&E$lP oL}btW>JU a@X"|TA3LrؠIǼ*IۡI܇J&`o֊q@m:v(O}n;x@!.J7@W=OOT:Z? .ypbT*@.I!撀 H@ ~+XR&K|վKK, %/9NygҡYM@ԵI&~5 e[tD#I' uu Olo1e,Zd,^{JC /5@"# @H@kI>UCKuuX␫!Ss0Bjh5G<%TxBA:0e5QEMT;8:r Qt= ~nL:u%5O~sO]~<bpd;3IBQWZ]`y%a %9n1&r$VX%mkNy&!7D+9^7Cs&nQn $p\'P@>(Hq0}r|tҵtp73.^ X^ w/ nK:(1Ŋ%N$4~ Īİ$ĦX3D/Wky&V+\$lth_@anL((O @Dŀ]V ^<+ +?8SJ@$(q357W %[p,daIHQmXby %UnqlQ.v-AݜB0hwV ݠ@aa6tm:XuF N@& @EG RIwN 5?8<?|/~ F@86<3$|8[:,k=3kƍ%;g҇JR$&0'0Xߢx nSvgWM 8Y(OdÅk0H3L@+qvZ;99`uG>kw\],a8n 0 H Pb$ J@~ *eᮺԅ%oVk>>0.t*ylN@" ("yA(q3UfX"Ub\'y(~%\`6%KNX}UoM\o& b R@(6m: N < 'P@=2(ԍuGq>hKJވwK<42d9l4ܠ M\l?+'6M6k 2 u@Yx >`0/K|ޗ V[|r )`XbȠʀ! s2A HoKbcC,q)",Q)ܒV7! K-5Sz=T9u\ˡ>65`䢰<{ t~N@d_Gѹ`[k#tx=WNK<]6'p2NjXdDwZ{Pr}W<_>:<{L7ZVǗ_ϼϏBc S"cUҪdH2$A)_P m aLXRC^kgRibE/uiMhH.N nбxP@!Wm&l9NsNKp'\'yΡc:D@a;Gv-WmOL~K+䦷<(h 2=RhHT) Ȅ$A[49cA 4*f`ITdwM%5o|TibVKDUjor&n*5&_NkM*ILN rtInS,ԮP u@ sl (Se>W>r\r;##v3D'5SI %NFRߜJL:,q&a L 4\&`&U晬vh{&ڛkm 4:y“AA^?tދn/{3˩5#E8 @%rvW$Z\o%6`b70>v"4q=wh2ٻIhy&7@Ay_O~'GZqxgE}[ሑp02(% @9]%aI%58}othEC9IK&0_HN@ڄ'  ~g S^}ɇ'O=9>/_~t;tӮZ]_N]x_9_/ oz\mYbn@Y@ADJ4y$]9:,QK K,0&M$ u ;4Q46n4k<|څ(S9+y/9:wmWOn Z!:u@rrAD (]% ǧ;pm^:,q]ws\DC0X@&; vh24 Mrt Mf NcK'6<T@)]T@=@$lPޔ`)J% hqaQlǞ&СMu[SXJ\wXFKXi  n5TեR1h2ɑEyk&/7~qj5&מ:qneP)]T @h@$A tPU[KnYK%[%j$D]7 d/h$Oh#Qm:8ڗP]'9O @YtQI0p$AcsT$ZJ <f3]%Nh]%,Yǎ%`& Mߜ&*\Il&|'дڂ' @y2Ô.JCAp$dTK% uW^’ĘߤDlfs0kz;4 M쟙 N@NMPi2V*W-(2H  I~ tPhzؘ4_"4qCS:46I:8K'YU( jh.ᢤPA"O&AZJ|TyjJ#c.g%y` L Xo΁Mt _ΥE'6f4q%m' u:@1*H 7ə |7 tP>"_pFނᰤp׀s%&СIHMs&/Mt]:@Xhh(CΖINfМIHlA[ـȠL`d$> 2IjY (Jֹz zp,CoيRNa h thRS&ur6M%m⵾s uU'y}s(աLe!ce#@@*oNHr$AIUb8uϑc];=~k thRS(45+1E/ IDAT4:vIH4!ʠ0eWTL #0s@2-uP% 'jkG%\%<{*9$&a X thRS6h\bj.^'&1|6 NҜ:4(1AMæB@dPn ThI+%P~NIdΖ]%Np@%N5T@,0sK4ghb\ס[m_m'aI61N@<2`ҥPa2V(HtI ! MuA ̼f+3nUٰ$w &G%;$ ]` N&X1Nv.@}ZCd0eV"$TD0C$$jn{m70WkJ׆%1cךV'_:O (@&MAї95-::YbZvvg8RX`MK-M1PdP88c% AI$:tۜ~JuXVNa M46 ħ_& M'eÓZXq)Lʢ@Dy"8Y2$J@QYJF,0'g+.!NǨa thҡ9\5&|9t$&vC%5):Ti[ȠX.P @%PT%|KpYpM|LKT5mD4iKI`uİ?e"^P3'9jčlىY'"EmaTQj2V* @GxH璀HbE9~ˀ%shĹ`B׺Au95qŐCK97q-p 4OɩEsC ZrR,opdP% Ȇ$ d$0PA^K%lpCS7@ZXzϝ|l,!4_0XZ yKX;H28ߦ㐻up@ϒT䪵T2+1g(A;Ss$Ib8H $բ@Sވ|ԢKn_&ujZ=NCDMA4垣$Em:^qBg' '0xmADE,@':ڀ$&J_% QE!TP(M(6&Q*ѫK$+im'~fXrj@&ivh"rwcMb% ۲N*PghXl =L*l)5ԊAJ7Lr@n]:DwıV]' @W DZx% ~ + IQ$ADQΨsZp 1rw7, 0M:4q[tk2nm曀\p⽯a 'JMia 4TbT{ԈUPdPUe@! r@%N ub[pJJtXrԘWWS2X `he& `:F 4q&q1$MMXj$umPvb7% Cl["c%3$0H2xA>_T*IG@ :]a%څ&_ @k2Sj sMܭ N|]:<ɭe'~2BAȔ6aTYE$c-7J䌊AQ)ȴ40ĸW :ҡ.OMhM[x S@({& @,!xN Dp$[G$@&m7xs%y$YMy0]l@}hT&hoЙ-:5YjhM:8qoMO65(< 3dI&|ؔZ$Ўdg&y_%y3AZplp8M8ǬV~82/İw `G1dW-@'bu , huE+Y:ȡB\$PfJZd8p$f$1;(ʹXW%[p , &ֽ̡`A~hTi+ñ0~ M@&y:2y 9]~|?#x2cxRkq'P&~/v BX6H ՒsPi0r$$ඛQ-9*ɽ`Ih w.chf l@tq trXk-S N|t id+ub'^x>gؚ1" @: :L钯X( F@f IlrQEZtu@%y~UU3WP` j&=$%4С4-'Xhh6sr 86HQ;#vbN ]g7dȶ;} A0SlT`N`dPˀچ$fJ|\%Nkg*İd#pnD7\_m~D\ΐc݀:v Tw<JсJWRcȠA$мVP讒ނ9DsN24aVh@hmB&N<%]gx*<1䀁\ ePj2HJ\{7k.ڇ$P %J6"r-U+Q )dl,ZtM R  ,tpLq}]'[{3O}y tezy á;w S"Z# H̠;n$c%}NPU2{WÆXp0ekGA5 @ޫMTjMZn*o,OucB! T'Aq'PC~*nP2VG:!cEHCLejՐ$Z)*q^ 4 s|Ԍmb&(VA\ZL{#[t @X(6r6>NLBby(d@l:vFN* OP-298+:I+E 2Vw h%IJ"-Ur數֢|VkfZrh _ :Ҡ Pk]mGt `ءU-5bĦCwA$8@mB\^tbe*>TjUE# Tr@Hunb*SMLѱjxgo'}uJp9L5G%GaȠP$zI]$l(s JJI (eltP+a Ca&Xb.}v \Hwp7& yZ' ]7@@!x[dl߁`2UG,y2Rr=Lt,!c"( H@$*d99 IP\@#P$UΥ@-8PJvrC㺌sMF_DMJ考 ;Ă*ܕ iqȎw2 H P̓+.!P{E AIr $>byt7c$uJ|lYm*+ذjP]`Egw* (fU5j=!hEXB޹& nA_&s/xr; fUPuGw P Yȃ( *.2] IDATI SeWMI%0eq$%뻫$YMZXRU%46暠vjсeM28:^t 1<&'Ӽ>DvY@ p"WgiQ~piMdDHr$l`J,J`t%lCFZtDmm[:8NtEb;ŏP Pυ ?_2H>Jys*,2^|kǨC$Iկϩt8l3`&`wlEa] t m8Q'P]*NlETd6 Q" J* ̣ ,A9B%'$ Vnp%ely WI,(yׂ3զgG¹&^y}sC36iA n1!81'P]\'P^\ Ķ@U~PP D 73L$eWTpHRV1"v!g[&J\(*&%;e&[ta]~_.[ m:NB#!| u _ˎUB}[yQt 綽 f.煭Õ$}S֢l`Gs] Ȇ$M JFy~3w-8<SP LBעuVwu8e_MuRhIQpr\'r;TN FWV' )*b^;lRqy.HTKK.Iyt{yv@m:P~ 4 NBPaѲ uK.he*y $0HW4rpi!R9a(HD6$Yn JgZd[7]%;uTU2"An1BUp<&}1:ZhӁypwE$@ s3JP@H"gs rxe%*9v 1Hy+sCn J@Wd. v58 ȿEgi&>n)m:,pX(]GUK- 'Puǥ93D^F3{< ` 9թ$@@,/$І$% ק}\%N \Zpvb6dX[c lA 6MNkױՒu着h}27JB7HE1&ZςI[h|H$ƳgPx)(*qoX*;Zt@@XAe CaQQM oԁN|%uu[v JGt@h&&`*ZIu*最>Iu]x LLB/`JPdʡK^;' $7 @8MRYwT]o,~S]pWIP@zMnmܮb-'X?x_:;;c5<=(̅%tWJ U$q}HP7P*Nv nZs 䦽Du1 NkAXO@N^Bx&Li|qÁ'uH!I n%kl ([~ eY &Y@%8yr4N)#&Ii:DxgI u\n B;%ݠyRV~JN/ZJ޽wX+ b,4@XF`Ed}FHgIAɛ)Եu_cۤ;nxFL'˷:&M}- |npx}$B=u*9܈/u訕 @.vIcQH]C8rJ>qPo#<$Wggڿ[&1_IH_ 9'urO{{$Hʅ4BIoZKPVOO6M## H H"70@pPR)i] &c:O'Q lN Oz?o3|IO›>w$H4P% >T|"XqVr.0GRqH#mDBM$׺2U#~SpIN`:':}Nj\zP` DI:4ÔX-/W\vo{^pH]W($whpNIsYLWWMrjlD\/'9}# ̍~k uIJS}vТ[7E`l84k &%= Jj\zk*PRZWhu@?861T\s'Ό:IU'} (0׍l4Cfo 4̜C.IAIw>T~j&umk8 u@ǐXXف}𤫎" ( Q`%QMNm9H˦5_@UVs.PB;TAI&枋\%^hmmz(lFWLT[j6[޸:Nxyl(Hɔt[<H;5zj0$a I@gI@ Z}NIJP&_${0ߤlw%8{sN@ߐXOrѓv`St'\ @%u#d \5꧃CZVH@$w\=vSjᠤRd %Aoټ&& ;ߤl 8.Q?-=N} Ol>'%QM ڞƘOz7Zt2(iҞ=q#u9I􃒙\P3(,7>>ۄ‚NNfN2kX8$.4 4,'0>G1A )AڡJJASFfP$Lvl!Lv4N%}1sn3&=n~b:$'apk: : OkUO`=@}1Xn arMJ 0$H`$H=P <$ࠤZJvpvom3b:$88}'0g@,D%N`MdpRs2vG֖` {RP妭5MR $D`)`tGP@GAP&i,2{> <^>5SM%py6ѬNg T$0X'प+y ؁'0Y w`?@II;DKLƠPgغqyD #C)=MRj#z40%}NIR$(XVM@'8 uRRen4h\IDATN։iv~x G<  PQ*p;\G6[ti%HvY"Ap!ɛ$$ 8(( VMA c:p|5.5l sld ܻO%604'M <m)A7Z2[j~: 6ZT9QBbY"A$$/rH"<5$PlzaX?'w(oVhb덁wYq'}2Tb QTXoM߉%`EM PA EV3 8$IH*rzb7()*UMy(,،h' NZԛ= B{?  \(PQH-P%Væ\e;ԤYϹV 0$V;TG R!IRr]x]DP ) J{,q@PX)9b@l|ٓ]'p.<eP@ % Ҁ ^]!Q`H`p@:$'݀7 8(^SOߙPD-xIENDB`glogg-1.1.4/images/newdata_icon@2x.png0000644000175000017500000000033013076461374016573 0ustar nickonickoPNG  IHDR KbKGD pHYs  tIMEJeIDATHc`P6g >C%MF#?5ͤY =e#p$#E)9NI1hA g$YɲLG YIENDB`glogg-1.1.4/images/open14@2x.png0000644000175000017500000000117713076461374015260 0ustar nickonickoPNG  IHDRr ߔbKGD pHYs&tIME(X  IDATH?hQ??.坃 "`D;8:(ID`AMMcP?SPlr?mlsI|=NXBʘ-r lqnH+Sn^.Btbb}&L%[$Ǭob3ʏW+TuFo.t&6d,YuVmsх>$V 4*wl2~a$rjuoQwPks3|Ep$ӽ~LuU^*?ժ<՗RtR:D&rVy(`(1ɗt%WȘX5]%-{aRQQiqԏn:/F<L|cJ_}?.vR`pD77'lŶϳ 5B 7bLJWU`} &%˙ryv<&M3IENDB`glogg-1.1.4/release-osx.sh0000755000175000017500000000167013076461374014411 0ustar nickonicko#!/bin/bash # Build glogg for OSX and make a DMG installer # (uses https://github.com/LinusU/node-appdmg) # # brew install node # npm install -g appdmg # # QTDIR is built -static QTDIR=$HOME/Sandbox/qt-5.5.1-release-static BOOSTDIR=$HOME/Sandbox/boost_1_59_0 make clean if [ ! -d "$BOOSTDIR" ]; then echo $BOOSTDIR not found. exit 1 elif [ -z "$VERSION" ]; then echo Please specify a version to build: VERSION=1.2.3 $0 exit 1 else $QTDIR/qtbase/bin/qmake glogg.pro LIBS+="-dead_strip" CONFIG+="release no-dbus version_checker" BOOST_PATH=$BOOSTDIR VERSION="$VERSION" fi make -j8 dsymutil release/glogg.app/Contents/MacOS/glogg mv release/glogg.app/Contents/MacOS/glogg.dSYM release/glogg-$VERSION.dSYM sed -e "s/\"glogg\"/\"glogg $VERSION\"/" osx_installer.json >osx_${VERSION}_installer.json rm glogg_${VERSION}_installer.dmg appdmg osx_${VERSION}_installer.json glogg_${VERSION}_installer.dmg rm osx_${VERSION}_installer.json glogg-1.1.4/glogg.desktop0000644000175000017500000000064513076461374014316 0ustar nickonicko[Desktop Entry] Name=glogg GenericName=Log file browser Exec=glogg %F Icon=glogg Type=Application Comment=A smart interactive log explorer. Terminal=false Categories=Qt;Utility;TextTools;Development; MimeType=text/plain; Actions=Session;NewInstance; [Desktop Action Session] Exec=glogg --load-session %F Name=Open With Previous Session [Desktop Action NewInstance] Exec=glogg --multi %F Name=Open In A New glogg Window glogg-1.1.4/INSTALL.win.md0000644000175000017500000001146713076461374014054 0ustar nickonicko# Building glogg for Windows As often on Windows, building _glogg_ is more complicated than we would like. The method we use to test and generate the builds available on glogg.bonnefon.org is to cross-build _glogg_ from a Debian/Ubuntu machine. The compiler currently used is MinGW-W64 4.6.3, which is available on Debian unstable and Ubuntu 12.04 LTS: apt-get install g++-mingw-w64-i686 There are two dependencies that must be built for Windows: - Qt, we currently use version 4.8.2 - Boost, we currently use version 1.50.0 Once the dependencies are installed (see below), a script takes care of the building and the packaging: ./release-win32-x.sh ## Building Qt on Windows It is allegedly possible to cross-compile Qt from Windows but after a lot of frustration, the _glogg_ team is building it natively on Windows (using a Windows 7 64bit VM in VirtualBox) and then using the binary generated from the Linux machine. Amazingly, it works as long as we are using a similar version of gcc (MinGW-W64) on both machines. Here are instructions to do the build in a Windows 7 VM: - Download the source from qt website (tested 4.8.2), be sure to download it with DOS end-of-line conventions (zip archive instead of tar.bz2 or tar.gz). - Install the .NET Framework 4 (http://www.microsoft.com/en-us/download/details.aspx?id=17718) - Install the Windows SDK (http://www.microsoft.com/en-us/download/details.aspx?id=8279) - Install mingw-w64 from TDM-GCC (tdm64-gcc, tested with 4.6.1). - Extract the Qt source in c:\qt\4.8.2 If building a 32 bits version of Qt (what we do for _glogg_): - Modify qt/4.8.2/mkspecs/win32-g++/qmake.conf to add `-m32` to `QMAKE_CFLAGS` and `QMAKE_LFLAGS` - Modify qt/4.8.2/mkspecs/win32-g++/qmake.conf to replace: `QMAKE_RC = windres -F pe-i386` - (optionally make other changes here to improve performances) Build from the MinGW command prompt: configure.exe -platform win32-g++-4.6 -no-phonon -no-phonon-backend -no-webkit -fast -opensource -shared -no-qt3support -no-sql-sqlite -no-openvg -no-gif -no-opengl -no-scripttools -qt-style-windowsxp -qt-style-windowsvista mingw32-make - copy the whole `qt/4.8.2` to the linux machine in `~/qt-x-win32/qt_win/4.8.2` ### Creating the cross-compiled Qt target - Install ~/qt-x-win32 created before Then create a copy of the standard gcc target to create the new cross-gcc target: cd /usr/share/qt4/mkspecs/ sudo cp -a win32-g++ win32-x-g++ and modify it to point to the cross-compiler and our local version of Windows Qt: sudo sed -i -re 's/ (gcc|g\+\+)/ i686-w64-mingw32-\1/' win32-x-g++/qmake.conf sudo sed -i -re '/QMAKE_SH/iQMAKE_SH=1' win32-x-g++/qmake.conf sudo sed -i -re 's/QMAKE_COPY_DIR.*$/QMAKE_COPY_DIR = cp -r/' win32-x-g++/qmake.conf sudo sed -i -re '/QMAKE_LFLAGS/s/$/ -mwindows -static-libgcc -static-libstdc++/' win32-x-g++/qmake.conf sudo sed -i -re 's/QMAKE_RC.*$/QMAKE_RC = i686-w64-mingw32-windres/' win32-x-g++/qmake.conf sudo sed -i -re 's/QMAKE_STRIP\s.*$/QMAKE_STRIP = i686-w64-mingw32-strip/' win32-x-g++/qmake.conf sudo sed -i -re 's/\.exe//' win32-x-g++/qmake.conf sudo sed -i -re 's/QMAKE_INCDIR\s.*$/QMAKE_INCDIR = \/usr\/i686-w64-mingw32\/include/' win32-x-g++/qmake.conf sudo sed -i -re 's/QMAKE_INCDIR_QT\s.*$/QMAKE_INCDIR_QT = \/home\/$USER\/qt_win\/4.8.2\/include/' win32-x-g++/qmake.conf sudo sed -i -re 's/QMAKE_LIBDIR_QT\s.*$/QMAKE_LIBDIR_QT = \/home\/$USER\/qt_win\/4.8.2\/lib/' win32-x-g++/qmake.conf Now you can build a hello world to test Qt: mkdir /tmp/testqt cd /tmp/testqt echo '#include #include int main(int argc, char *argv[]) { QApplication app(argc, argv); QPushButton hello("Hello world!"); hello.resize(100, 30); hello.show(); return app.exec(); }' >main.cpp qmake -project qmake -spec win32-x-g++ -r CONFIG+=release make ## Building Boost on Windows Download the source from boost.org (tested with 1.50.0), DOS EOL mandatory! Extract it in a Windows VM Edit bootstrap.bat to read: call .\build.bat mingw %*... set toolset=gcc And from the MinGW prompt: bootstrap b2 toolset=gcc address-model=32 variant=debug,release link=static,shared threading=multi install - Copy the whole c:\boost_1_50_0 to the linux machine to ~/qt-x-win32/boost_1_50_0 ## (optional) Install NSIS If _wine_ and the NSIS compiler (available from [here](http://nsis.sourceforge.net/Main_Page)) are available, the script will generate the installer for _glogg_. The NSIS compiler should be installed in `~/qt-x-win32/NSIS`. ## Building _glogg_ From this point, building _glogg_ is hopefully straightforward: ./release-win32-x.sh The `release-win32-x.sh` script might need some changes if you use different paths for the dependencies. The object file is in `./release/` glogg-1.1.4/README.md0000644000175000017500000000403213076461374013075 0ustar nickonicko glogg - the fast, smart log explorer ===================================== glogg is a multi-platform GUI application that helps browse and search through long and complex log files. It is designed with programmers and system administrators in mind and can be seen as a graphical, interactive combination of grep and less. ## Main features * Runs on Unix-like systems, Windows and Mac thanks to Qt * Provides a second window showing the result of the current search * Reads UTF-8 and ISO-8859-1 files * Supports grep/egrep like regular expressions * Colorizes the log and search results * Displays a context view of where in the log the lines of interest are * Is fast and reads the file directly from disk, without loading it into memory * Is open source, released under the GPL ## Requirements * GCC version 4.8.0 or later * Qt libraries (version 5.2.0 or later) * Boost "program-options" development libraries * Markdown HTML processor (optional, to generate HTML documentation) glogg version 0.9.X still support older versions of gcc and Qt if you need to build on an older platform. ## Building The build system uses qmake. Building and installation is done this way: ``` tar xzf glogg-X.X.X.tar.gz cd glogg-X.X.X qmake make make install INSTALL_ROOT=/usr/local (as root if needed) ``` `qmake BOOST_PATH=/path/to/boost/` will statically compile the required parts of the Boost libraries whose source are found at the specified path. The path should be the directory where the tarball from www.boost.org is extracted. (use this method on Windows or if Boost is not available on the system) The documentation is built and installed automatically if 'markdown' is found. ## Tests The tests are built using CMake, and require Qt5 and the Google Mocks source. ``` cd tests mkdir build cd build export QT_DIR=/path/to/qt/if/non/standard export GMOCK_HOME=/path/to/gmock cmake .. make ./glogg_tests ``` ## Contact Please visit glogg's website: http://glogg.bonnefon.org/ The development mailing list is hosted at http://groups.google.co.uk/group/glogg-devel glogg-1.1.4/release-win32-x.sh0000755000175000017500000000205313076461374015003 0ustar nickonicko#!/bin/bash # Build glogg for win32 using the cross-compiler QTXDIR=$HOME/qt-x-win32 QTVERSION=4.8.6-32 BOOSTDIR=$QTXDIR/boost_1_50_0 make clean if [ "$1" == "debug" ]; then echo "Building a debug version" qmake-qt4 glogg.pro -spec win32-x-g++ -r CONFIG+="debug win32 rtti no-dbus version_checker" BOOST_PATH=$BOOSTDIR elif [ -z "$VERSION" ]; then echo "Building default version" qmake-qt4 glogg.pro -spec win32-x-g++ -r CONFIG+="release win32 rtti no-dbus version_checker" BOOST_PATH=$BOOSTDIR else echo "Building version $VERSION" qmake-qt4 glogg.pro -spec win32-x-g++ -r CONFIG+="release win32 rtti no-dbus version_checker" BOOST_PATH=$BOOSTDIR VERSION="$VERSION" fi make -j3 cp $QTXDIR/$QTVERSION/lib/{QtCore4,QtGui4,QtNetwork4}.dll release/ cp $QTXDIR/$QTVERSION/lib/{QtCored4,QtGuid4,QtNetworkd4}.dll debug/ cp /usr/i686-w64-mingw32/lib/libwinpthread-1.dll release/ if [ -z "$VERSION" ]; then VERSION=`git describe`; fi echo Generating installer for glogg-$VERSION wine $QTXDIR/NSIS/makensis -DVERSION=$VERSION -DARCH32 glogg.nsi