From faeacaff3eac0e22d6b3b1564c3284bb561bd9d1 Mon Sep 17 00:00:00 2001 From: Konstantin Isakov Date: Thu, 29 Jan 2009 19:16:25 +0000 Subject: [PATCH] Implement asyncronous word finding. --- src/article_maker.cc | 3 + src/article_netmgr.cc | 3 + src/dictlock.cc | 25 ++++++ src/dictlock.hh | 24 ++++++ src/goldendict.pro | 6 +- src/mainwindow.cc | 178 +++++++++++++++++----------------------- src/mainwindow.hh | 7 +- src/wordfinder.cc | 186 ++++++++++++++++++++++++++++++++++++++++++ src/wordfinder.hh | 92 +++++++++++++++++++++ 9 files changed, 417 insertions(+), 107 deletions(-) create mode 100644 src/dictlock.cc create mode 100644 src/dictlock.hh create mode 100644 src/wordfinder.cc create mode 100644 src/wordfinder.hh diff --git a/src/article_maker.cc b/src/article_maker.cc index a1e8f80e..c28fdd67 100644 --- a/src/article_maker.cc +++ b/src/article_maker.cc @@ -5,6 +5,7 @@ #include "config.hh" #include "htmlescape.hh" #include "utf8.hh" +#include "dictlock.hh" #include #include @@ -68,6 +69,8 @@ string ArticleMaker::makeDefinitionFor( QString const & inWord, result += ""; + DictLock _; + // Accumulate main forms vector< wstring > alts; diff --git a/src/article_netmgr.cc b/src/article_netmgr.cc index 14e28275..9d0cc0b1 100644 --- a/src/article_netmgr.cc +++ b/src/article_netmgr.cc @@ -2,6 +2,7 @@ * Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */ #include "article_netmgr.hh" +#include "dictlock.hh" using std::string; @@ -68,6 +69,8 @@ bool ArticleNetworkAccessManager::getResource( QUrl const & url, bool search = ( id == "search" ); + DictLock _; + for( unsigned x = 0; x < dictionaries.size(); ++x ) { if ( search || dictionaries[ x ]->getId() == id ) diff --git a/src/dictlock.cc b/src/dictlock.cc new file mode 100644 index 00000000..ec9c8f29 --- /dev/null +++ b/src/dictlock.cc @@ -0,0 +1,25 @@ +/* This file is (c) 2008-2009 Konstantin Isakov + * Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */ + +#include "dictlock.hh" +#include + +namespace +{ + QMutex & mutexInstance() + { + static QMutex mutex; + + return mutex; + } +} + +DictLock::DictLock() +{ + mutexInstance().lock(); +} + +DictLock::~DictLock() +{ + mutexInstance().unlock(); +} diff --git a/src/dictlock.hh b/src/dictlock.hh new file mode 100644 index 00000000..a27082c5 --- /dev/null +++ b/src/dictlock.hh @@ -0,0 +1,24 @@ +/* This file is (c) 2008-2009 Konstantin Isakov + * Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */ + +#ifndef __DICTLOCK_HH_INCLUDED__ +#define __DICTLOCK_HH_INCLUDED__ + +/// This is a one large coarse-grained lock class used to lock all the +/// dictionaries and all their shared containers (usually vectors) in order +/// to work with them concurrently in different threads. Since we do matching +/// in a different thread, we need this, and since matching doesn't happen +/// too much and doesn't typically take a lot of time, there's no need for any +/// more fine-grained locking approaches. +/// Create this object before working with any dictionary objects or their +/// containers which are known to be shared with the other objects. Destory +/// this object when you're done (i.e,, leave the scope where it was declared). +class DictLock +{ +public: + + DictLock(); + ~DictLock(); +}; + +#endif diff --git a/src/goldendict.pro b/src/goldendict.pro index 496684b2..10942e28 100644 --- a/src/goldendict.pro +++ b/src/goldendict.pro @@ -53,7 +53,9 @@ HEADERS += folding.hh \ article_maker.hh \ scanpopup.hh \ articleview.hh \ - externalviewer.hh + externalviewer.hh \ + dictlock.hh \ + wordfinder.hh FORMS += groups.ui dictgroupwidget.ui mainwindow.ui sources.ui initializing.ui\ @@ -65,6 +67,6 @@ SOURCES += folding.cc main.cc dictionary.cc md5.c config.cc sources.cc \ chunkedstorage.cc xdxf2html.cc iconv.cc lsa.cc htmlescape.cc \ dsl.cc dsl_details.cc filetype.cc fsencoding.cc groups.cc \ groups_widgets.cc instances.cc article_maker.cc scanpopup.cc \ - articleview.cc externalviewer.cc + articleview.cc externalviewer.cc dictlock.cc wordfinder.cc RESOURCES += resources.qrc flags.qrc diff --git a/src/mainwindow.cc b/src/mainwindow.cc index ef6a0d0e..946dc104 100644 --- a/src/mainwindow.cc +++ b/src/mainwindow.cc @@ -8,6 +8,7 @@ #include "stardict.hh" #include "lsa.hh" #include "dsl.hh" +#include "dictlock.hh" #include #include #include @@ -25,8 +26,8 @@ MainWindow::MainWindow(): cfg( Config::load() ), articleMaker( dictionaries, groupInstances ), articleNetMgr( this, dictionaries, articleMaker ), + wordFinder( this ), scanPopup( 0, articleNetMgr ), - startLookupTimeout( this ), initializing( 0 ) { ui.setupUi( this ); @@ -58,8 +59,6 @@ MainWindow::MainWindow(): connect( ui.tabWidget, SIGNAL( tabCloseRequested( int ) ), this, SLOT( tabCloseRequested( int ) ) ); - startLookupTimeout.setSingleShot( true ); - ui.tabWidget->setTabsClosable( true ); connect( ui.sources, SIGNAL( activated() ), @@ -71,15 +70,17 @@ MainWindow::MainWindow(): connect( ui.translateLine, SIGNAL( textChanged( QString const & ) ), this, SLOT( translateInputChanged( QString const & ) ) ); - connect( &startLookupTimeout, SIGNAL( timeout() ), - this, SLOT( startLookup() ) ); - connect( ui.wordList, SIGNAL( itemActivated( QListWidgetItem * ) ), this, SLOT( wordListItemActivated( QListWidgetItem * ) ) ); + connect( wordFinder.qobject(), SIGNAL( prefixMatchComplete( WordFinderResults ) ), + this, SLOT( prefixMatchComplete( WordFinderResults ) ) ); + makeDictionaries(); addNewTab(); + + ui.translateLine->setFocus(); } LoadDictionaries::LoadDictionaries( vector< string > const & allFiles_ ): @@ -133,7 +134,10 @@ void LoadDictionaries::indexingDictionary( string const & dictionaryName ) throw void MainWindow::makeDictionaries() { - dictionaries.clear(); + { + DictLock _; + dictionaries.clear(); + } ::Initializing init( this ); @@ -175,7 +179,11 @@ void MainWindow::makeDictionaries() loadDicts.wait(); - dictionaries = loadDicts.getDictionaries(); + { + DictLock _; + + dictionaries = loadDicts.getDictionaries(); + } initializing = 0; @@ -235,6 +243,8 @@ void MainWindow::updateGroupList() groupInstances.clear(); + DictLock _; + for( unsigned x = 0; x < cfg.groups.size(); ++x ) { groupInstances.push_back( Instances::Group( cfg.groups[ x ], dictionaries ) ); @@ -347,119 +357,83 @@ void MainWindow::editSources() void MainWindow::editGroups() { - Groups groups( this, dictionaries, cfg.groups ); - - groups.show(); - - if ( groups.exec() == QDialog::Accepted ) { - cfg.groups = groups.getGroups(); + // We lock all dictionaries during the entire group editing process, since + // the dictionaries might get queried for various infos there + DictLock _; + + Groups groups( this, dictionaries, cfg.groups ); + + groups.show(); - Config::save( cfg ); + if ( groups.exec() == QDialog::Accepted ) + { + cfg.groups = groups.getGroups(); - updateGroupList(); + Config::save( cfg ); + } + else + return; } + + updateGroupList(); } -void MainWindow::translateInputChanged( QString const & ) +void MainWindow::translateInputChanged( QString const & newValue ) { - startLookup(); - //startLookupTimeout.start( 0 ); -} + QString req = newValue.trimmed(); -void MainWindow::startLookup() -{ - QString word = ui.translateLine->text(); + if ( !req.size() ) + { + // An empty request always results in an empty result + prefixMatchComplete( WordFinderResults( req, &getActiveDicts() ) ); - ui.wordList->clear(); - - wstring wordW = word.trimmed().toStdWString(); - - if ( wordW.empty() ) return; - - // Maps lowercased string to the original one. This catches all duplicates - // without case sensitivity - map< wstring, wstring > exactResults, prefixResults; - - // Where to look? - - vector< sptr< Dictionary::Class > > const & activeDicts = getActiveDicts(); - - for( unsigned x = 0; x < activeDicts.size(); ++x ) - { - vector< wstring > exactMatches, prefixMatches; - - activeDicts[ x ]->findExact( wordW, exactMatches, prefixMatches, 200 ); - - for( unsigned y = 0; y < exactMatches.size(); ++y ) - { - wstring lowerCased = Folding::applySimpleCaseOnly( exactMatches[ y ] ); - - pair< map< wstring, wstring >::iterator, bool > insertResult = - exactResults.insert( pair< wstring, wstring >( lowerCased, exactMatches[ y ] ) ); - - if ( !insertResult.second ) - { - // Wasn't inserted since there was already an item -- check the case - if ( insertResult.first->second != exactMatches[ y ] ) - { - // The case is different -- agree on a lowercase version - insertResult.first->second = lowerCased; - } - } - } - - for( unsigned y = 0; y < prefixMatches.size(); ++y ) - { - wstring lowerCased = Folding::applySimpleCaseOnly( prefixMatches[ y ] ); - - pair< map< wstring, wstring >::iterator, bool > insertResult = - prefixResults.insert( pair< wstring, wstring >( lowerCased, prefixMatches[ y ] ) ); - - if ( !insertResult.second ) - { - // Wasn't inserted since there was already an item -- check the case - if ( insertResult.first->second != prefixMatches[ y ] ) - { - // The case is different -- agree on a lowercase version - insertResult.first->second = lowerCased; - } - } - } } - // Do any sort of collation here in the future. For now we just put the - // strings sorted by the map. + ui.wordList->setCursor( Qt::WaitCursor ); - for( map< wstring, wstring >::const_iterator i = exactResults.begin(); - i != exactResults.end(); ++i ) + wordFinder.prefixMatch( req, &getActiveDicts() ); +} + +void MainWindow::prefixMatchComplete( WordFinderResults r ) +{ + if ( r.requestStr != ui.translateLine->text().trimmed() || + r.requestDicts != &getActiveDicts() ) { - ui.wordList->addItem( QString::fromStdWString( i->second ) ); - - QListWidgetItem * item = ui.wordList->item( ui.wordList->count() - 1 ); - - QFont font = item->font(); - //font.setWeight( QFont::Bold ); - - item->setFont( font ); + // Those results are already irrelevant, ignore the result + return; } - if ( prefixResults.size() ) + ui.wordList->setUpdatesEnabled( false ); + + for( unsigned x = 0; x < r.results.size(); ++x ) { - for( map< wstring, wstring >::const_iterator i = prefixResults.begin(); - i != prefixResults.end(); ++i ) - { - ui.wordList->addItem( QString::fromStdWString( i->second ) ); + QListWidgetItem * i = ui.wordList->item( x ); - QListWidgetItem * item = ui.wordList->item( ui.wordList->count() - 1 ); - - QFont font = item->font(); - //font.setStyle( QFont::StyleOblique ); - - item->setFont( font ); - } + if ( !i ) + ui.wordList->addItem( r.results[ x ] ); + else + if ( i->text() != r.results[ x ] ) + i->setText( r.results[ x ] ); } + + while ( ui.wordList->count() > (int) r.results.size() ) + { + // Chop off any extra items that were there + QListWidgetItem * i = ui.wordList->takeItem( ui.wordList->count() - 1 ); + + if ( i ) + delete i; + else + break; + } + + if ( ui.wordList->count() ) + ui.wordList->scrollToItem( ui.wordList->item( 0 ), QAbstractItemView::PositionAtTop ); + + ui.wordList->setUpdatesEnabled( true ); + ui.wordList->unsetCursor(); } void MainWindow::wordListItemActivated( QListWidgetItem * item ) diff --git a/src/mainwindow.hh b/src/mainwindow.hh index 688cc81c..7f93d6af 100644 --- a/src/mainwindow.hh +++ b/src/mainwindow.hh @@ -17,6 +17,7 @@ #include "article_maker.hh" #include "scanpopup.hh" #include "articleview.hh" +#include "wordfinder.hh" using std::string; using std::vector; @@ -67,9 +68,9 @@ private: ArticleMaker articleMaker; ArticleNetworkAccessManager articleNetMgr; - ScanPopup scanPopup; + WordFinder wordFinder; - QTimer startLookupTimeout; + ScanPopup scanPopup; ::Initializing * initializing; @@ -102,7 +103,7 @@ private slots: void indexingDictionary( QString dictionaryName ); void translateInputChanged( QString const & ); - void startLookup(); + void prefixMatchComplete( WordFinderResults ); void wordListItemActivated( QListWidgetItem * ); void showTranslationFor( QString const & ); diff --git a/src/wordfinder.cc b/src/wordfinder.cc new file mode 100644 index 00000000..b47696ba --- /dev/null +++ b/src/wordfinder.cc @@ -0,0 +1,186 @@ +/* This file is (c) 2008-2009 Konstantin Isakov + * Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */ + +#include "wordfinder.hh" +#include "dictlock.hh" +#include "folding.hh" +#include +#include + +using std::vector; +using std::wstring; +using std::map; +using std::pair; + +namespace +{ + struct MetaTypeRegister + { + MetaTypeRegister() + { + qRegisterMetaType< WordFinderResults >( "WordFinderResults" ); + } + }; +} + +WordFinder::WordFinder( QObject * parent ): QThread( parent ), op( NoOp ) +{ + static MetaTypeRegister _; + + start(); +} + +WordFinder::~WordFinder() +{ + // Request termination and wait for it to happen + opMutex.lock(); + + op = QuitOp; + + opCondition.wakeOne(); + + opMutex.unlock(); + + wait(); +} + +void WordFinder::prefixMatch( QString const & str, + std::vector< sptr< Dictionary::Class > > const * dicts ) +{ + opMutex.lock(); + + op = DoPrefixMatch; + prefixMatchString = str; + prefixMatchDicts = dicts; + + opCondition.wakeOne(); + + opMutex.unlock(); +} + +void WordFinder::run() +{ + opMutex.lock(); + + for( ; ; ) + { + // Check the operation requested + + if ( op == NoOp ) + { + opCondition.wait( &opMutex ); + continue; + } + + if ( op == QuitOp ) + break; + + // The only one op value left is DoPrefixMatch + + Q_ASSERT( op == DoPrefixMatch ); + + QString prefixMatchReq = prefixMatchString; + vector< sptr< Dictionary::Class > > const & activeDicts = *prefixMatchDicts; + + op = NoOp; + + opMutex.unlock(); + + map< wstring, wstring > exactResults, prefixResults; + + { + wstring word = prefixMatchReq.toStdWString(); + + DictLock _; + + // Maps lowercased string to the original one. This catches all duplicates + // without case sensitivity + + bool cancel = false; + + for( unsigned x = 0; x < activeDicts.size(); ++x ) + { + vector< wstring > exactMatches, prefixMatches; + + activeDicts[ x ]->findExact( word, exactMatches, prefixMatches, 40 ); + + for( unsigned y = 0; y < exactMatches.size(); ++y ) + { + wstring lowerCased = Folding::applySimpleCaseOnly( exactMatches[ y ] ); + + pair< map< wstring, wstring >::iterator, bool > insertResult = + exactResults.insert( pair< wstring, wstring >( lowerCased, + exactMatches[ y ] ) ); + + if ( !insertResult.second ) + { + // Wasn't inserted since there was already an item -- check the case + if ( insertResult.first->second != exactMatches[ y ] ) + { + // The case is different -- agree on a lowercase version + insertResult.first->second = lowerCased; + } + } + } + + for( unsigned y = 0; y < prefixMatches.size(); ++y ) + { + wstring lowerCased = Folding::applySimpleCaseOnly( prefixMatches[ y ] ); + + pair< map< wstring, wstring >::iterator, bool > insertResult = + prefixResults.insert( pair< wstring, wstring >( lowerCased, + prefixMatches[ y ] ) ); + + if ( !insertResult.second ) + { + // Wasn't inserted since there was already an item -- check the case + if ( insertResult.first->second != prefixMatches[ y ] ) + { + // The case is different -- agree on a lowercase version + insertResult.first->second = lowerCased; + } + } + } + + // Check if we've got an op request -- abort the query then + + opMutex.lock(); + + if ( op != NoOp ) + { + cancel = true; + break; + } + + opMutex.unlock(); + } + + if ( cancel ) + continue; + } + + // Do any sort of collation here in the future. For now we just put the + // strings sorted by the map. + + WordFinderResults r( prefixMatchReq, &activeDicts ); + + r.results.reserve( exactResults.size() + prefixResults.size() ); + + for( map< wstring, wstring >::const_iterator i = exactResults.begin(); + i != exactResults.end(); ++i ) + r.results.push_back( QString::fromStdWString( i->second ) ); + + for( map< wstring, wstring >::const_iterator i = prefixResults.begin(); + i != prefixResults.end(); ++i ) + r.results.push_back( QString::fromStdWString( i->second ) ); + + emit prefixMatchComplete( r ); + + // Continue serving op requests + + opMutex.lock(); + } + + opMutex.unlock(); +} + diff --git a/src/wordfinder.hh b/src/wordfinder.hh new file mode 100644 index 00000000..1ada8031 --- /dev/null +++ b/src/wordfinder.hh @@ -0,0 +1,92 @@ +/* This file is (c) 2008-2009 Konstantin Isakov + * Part of GoldenDict. Licensed under GPLv3 or later, see the LICENSE file */ + +#ifndef __WORDFINDER_HH_INCLUDED__ +#define __WORDFINDER_HH_INCLUDED__ + +#include +#include +#include +#include "dictionary.hh" + + +/// This struct represents results of a WordFinder match operation. +/// We need to separate this since it's needed to register is as a metatype +/// for the signal-slot connections to be able to pass it in queued mode. +struct WordFinderResults +{ + /// The initial request parameters. They are passed back so that it'd be + /// possible to tell the results apart from any previously cancelled + /// operations. + QString requestStr; + std::vector< sptr< Dictionary::Class > > const * requestDicts; + + /// The results themselves + std::vector< QString > results; + + WordFinderResults() + {} + + WordFinderResults( QString const & requestStr_, + std::vector< sptr< Dictionary::Class > > const * requestDicts_ ): + requestStr( requestStr_ ), requestDicts( requestDicts_ ) + {} +}; + +/// This component takes care of finding words in dictionaries asyncronously, +/// in another thread. This means the GUI doesn't get blocked during the +/// sometimes lenghtly process of finding words. +class WordFinder: QThread +{ + Q_OBJECT + +public: + + WordFinder( QObject * parent ); + ~WordFinder(); + + /// Allows accessing the otherwise inaccessible QObject indirect base, + /// which is QThread's base. This is needed to connect signals of the object. + QObject * qobject() + { return this; } + + /// Do the standard prefix-match search in the given list of dictionaries. + /// Some dictionaries might only support exact matches -- for them, only + /// the exact matches would be found. All search results are put into a single + /// list containing the exact matches first, then the prefix ones. Duplicate + /// matches from different dictionaries are merged together. + /// If there already was a prefixMatch operation underway, it gets cancelled + /// and the new one replaces it. + /// The dictionaries and the containing vector get locked during the search + /// using the DictLock object. Be sure you do that too in case you'd want + /// to work with them during the search underway. + void prefixMatch( QString const &, + std::vector< sptr< Dictionary::Class > > const * ); + +signals: + + /// This singal gets emitted from another thread and indicates that the + /// previously requested prefixMatch() operation was finished. See the + /// WordFinderResults structure description for further details. + void prefixMatchComplete( WordFinderResults ); + +private: + + QMutex opMutex; + QWaitCondition opCondition; + + enum Op + { + NoOp, + QuitOp, + DoPrefixMatch + } op; + + QString prefixMatchString; + std::vector< sptr< Dictionary::Class > > const * prefixMatchDicts; + + virtual void run(); +}; + +#endif +