Backbone's part of #6330 infinite search when no search backend selected
[mdictionary] / trunk / src / base / backbone / backbone.cpp
1 /*******************************************************************************
2
3     This file is part of mDictionary.
4
5     mDictionary is free software: you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation, either version 3 of the License, or
8     (at your option) any later version.
9
10     mDictionary is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14
15     You should have received a copy of the GNU General Public License
16     along with mDictionary.  If not, see <http://www.gnu.org/licenses/>.
17
18     Copyright 2010 Comarch S.A.
19
20 *******************************************************************************/
21 /*! \file backbone.cpp
22 \brief Backbone/core main file \see Backbone
23
24
25 \author Bartosz Szatkowski <bulislaw@linux.com>
26 */
27
28 #include "backbone.h"
29 #include "ConfigGenerator.h"
30 class ConfigGenerator;
31 #include <QDebug>
32
33 int Backbone::_searchLimit;
34
35 // Sadly QtConcurent::mapped dosent let me use something like calling method of
36 // some class with supplied argument; so i have to sin against art and put
37 // global function and variable so i could supply function with some parametr
38 QString mappedSearch;
39 QList<Translation*> mapSearch(CommonDictInterface *dict) {
40     if(dict)
41         return dict->searchWordList(mappedSearch, Backbone::_searchLimit);
42     return QList<Translation*>();
43 }
44
45
46
47 /*! Smart pointer (kind of) for translation object
48
49     QtConcurent::mapped  use collection of data and one function, what i need is
50     to map signle data object to method calls for multiple objects. TranslationPtr
51     is try to store method call as a data -> moreover QtConcurent allow only for
52     methods without any parameters so TranslationPtr is created with Translation
53     object -> ready to call toHtml() for supplied Translation.
54
55     Another thing is that QtConcurent dont like pointers in data collection
56     so TranslationPtr is way to hide real translation object (pointer for object)
57     */
58 class TranslationPtr {
59     Translation* _tr;
60 public:
61     TranslationPtr(Translation* tr) :_tr(tr) {}
62
63     /*! \return translation text for corresponding Translation object */
64     QString toHtml() const {
65         QString trans;
66         trans = _tr->toHtml();
67         return trans;
68
69     }
70 };
71
72 void Backbone::init() {
73
74    _dictNum = 0;
75    _dir = QDir::homePath() + "/.mdictionary/";
76    if(!_configPath.size())
77        _configPath = _dir + "mdictionary.config";
78    if(!_pluginPath.size())
79        _pluginPath = "/usr/lib/mdictionary";
80
81    //Install default config files
82    ConfigGenerator confGen;
83    confGen.generateCss(_dir + "style.css");
84    confGen.generateDefaultConfig(_configPath);
85
86    loadPrefs(_configPath);
87
88    loadPlugins();
89
90    loadDicts(_configPath);
91
92    connect(&_resultWatcher, SIGNAL(finished()), this, SLOT(translationReady()));
93    connect(&_htmlResultWatcher, SIGNAL(finished()), this,
94            SLOT(htmlTranslationReady()));
95    connect(&_bookmarkWatcher, SIGNAL(finished()), this,
96            SLOT(bookmarksListReady()));
97    connect(&_bookmarkSearchWatcher, SIGNAL(finished()), this,
98            SLOT(translationReady()));
99
100    // In common opinion perfect thread count is cores_number+1 (in qt perfect
101    // thread count is set to cores number
102    QThreadPool::globalInstance()->setMaxThreadCount(
103            QThreadPool::globalInstance()->maxThreadCount()+1);
104
105    _history = new History(_historyLen, this);
106 }
107
108
109
110 Backbone::Backbone(QString pluginPath, QString configPath, bool dry,
111                    QObject *parent)
112     : QObject(parent)
113 {
114     _pluginPath = pluginPath;
115     _configPath = configPath;
116
117     dryRun = false;
118     if(dry)
119         dryRun = true;
120     init();
121 }
122
123
124
125 Backbone::~Backbone()
126 {
127     QListIterator<CommonDictInterface*> it(_dicts.keys());
128
129     while(it.hasNext())
130         delete it.next();
131
132     it = QListIterator<CommonDictInterface*>(_plugins);
133     while(it.hasNext())
134         delete it.next();
135
136     QHashIterator<QString, Translation*> it2(_result);
137     while(it2.hasNext())
138         delete it2.next().value();
139
140 }
141
142
143
144
145 Backbone::Backbone(const Backbone &b) :QObject(b.parent()) {
146     init();
147     _dicts = QHash<CommonDictInterface*, bool > (b._dicts);
148     _plugins = QList<CommonDictInterface* > (b._plugins);
149     _result = QHash<QString, Translation* > (b._result);
150     _searchLimit = b.searchLimit();
151 }
152
153
154
155
156 int Backbone::searchLimit() const {
157     return _searchLimit;
158 }
159
160
161
162 QHash<CommonDictInterface*, bool > Backbone::getDictionaries() {
163     return _dicts;
164 }
165
166
167
168 QList<CommonDictInterface* > Backbone::getPlugins() {
169     return _plugins;
170 }
171
172
173
174 History* Backbone::history() {
175     return _history;
176 }
177
178
179
180 QMultiHash<QString, Translation*> Backbone::result() {
181     return _result;
182 }
183
184
185
186 void Backbone::stopSearching() {
187     if(stopped)
188         return;
189
190     foreach(CommonDictInterface* dict, _dicts.keys())
191         dict->stop();
192     stopped = true;
193     _innerHtmlResult.cancel();
194     _innerResult.cancel();
195     Q_EMIT searchCanceled();
196 }
197
198
199
200 void Backbone::search(QString word){
201     _result.clear();
202     mappedSearch = word.toLower();
203
204     stopped = false;
205
206     // When dictFin and bookmarkFin is set to true then translationReady()
207     // signal is emited see translationReady(),
208     // so when searching only in one of them, coresponding *Fin is set to false
209     // and other to true so program is waiting only for one translation
210     dictFin = !_searchDicts;
211     bookmarkFin = !_searchBookmarks;
212
213     if(!_searchDicts && !_searchBookmarks) {
214         Q_EMIT notify(Notify::Warning, tr("You have to specify where You want "
215                 "to look for translations"));
216         Q_EMIT ready();
217     }
218
219     if (_searchDicts) {
220         _innerResult = QtConcurrent::mapped(activeDicts(), mapSearch);
221         _resultWatcher.setFuture(_innerResult);
222     }
223
224     if(_searchBookmarks) {
225         _innerBookmarks = QtConcurrent::run(_bookmarks,
226                 &Bookmarks::searchWordList, word);
227         _bookmarkSearchWatcher.setFuture(_innerBookmarks);
228     }
229 }
230
231
232
233 void Backbone::selectedDictionaries(QList<CommonDictInterface* > activeDicts) {
234     foreach(CommonDictInterface* dict, _dicts.keys())
235         if(activeDicts.contains(dict))
236             _dicts[dict] = 1;
237         else
238             _dicts[dict] = 0;
239     dictUpdated();
240  }
241
242
243
244 void Backbone::addDictionary(CommonDictInterface *dict, bool active) {
245     addInternalDictionary(dict,active);
246     dictUpdated();
247 }
248
249
250
251  void Backbone::addInternalDictionary(CommonDictInterface* dict, bool active) {
252      dict->setHash(++_dictNum); // Hash must be uniqe in every session but not between
253      _dicts[dict] = active;
254      connect(dict, SIGNAL(settingsChanged()), this, SLOT(dictUpdated()));
255      connect(dict, SIGNAL(notify(Notify::NotifyType,QString)), this,
256              SIGNAL(notify(Notify::NotifyType,QString)));
257  }
258
259
260
261  void Backbone::removeDictionary(CommonDictInterface *dict) {
262      _dicts.remove(dict);
263      dict->clean();
264      delete dict;
265      dictUpdated();
266
267  }
268
269
270
271  void Backbone::quit() {
272     stopSearching();
273     Q_EMIT closeOk();
274 }
275
276
277
278 void Backbone::translationReady() {
279     bool changed = 0; // prevents doubling ready() signal, when both if's are
280                       //  executed in one translationReady() call then second
281                       // translationReady() call doubles ready*() emit
282
283     if(!dictFin && _innerResult.isFinished()) {
284         changed = 1;
285         dictFin = 1;
286         QFutureIterator<QList<Translation*> > it(_innerResult);
287
288         while(it.hasNext()) {
289             QList<Translation* > list = it.next();
290             foreach(Translation* trans, list) {
291                 if(!trans)
292                     continue;
293                 if(!_searchBookmarks)
294                     trans->setBookmark(_bookmarks.
295                             inBookmarks(trans->key()));
296                 _result.insert(trans->key().toLower(), trans);
297            }
298         }
299     }
300
301     if(!bookmarkFin && _innerBookmarks.isFinished()) {
302         changed = 1;
303         bookmarkFin = 1;
304         QList<Translation*> list = _innerBookmarks.result();
305
306         foreach(Translation* trans, list)
307                 _result.insert(trans->key().toLower(), trans);
308     }
309
310     if(!stopped && bookmarkFin && dictFin && changed) {
311         Q_EMIT ready();
312     }
313 }
314
315
316
317
318 QStringList Backbone::getFilesFromDir(QString dir, QStringList nameFilter) {
319     QDir plug(QDir::toNativeSeparators(dir));
320     if(!plug.exists()) {
321         qDebug() << plug.absolutePath() << " folder doesn't exist";
322         Q_EMIT notify(Notify::Warning,
323                 QString("%1 folder doesn't exist.").arg(plug.path()));
324         return QStringList();
325     }
326     plug.setFilter(QDir::Files);
327     QStringList list = plug.entryList(nameFilter);
328
329     for(int i = 0; i < list.size(); i++)
330         list[i] = plug.absoluteFilePath(list.at(i));
331     return list;
332 }
333
334
335 void Backbone::loadPlugins() {
336     if(dryRun)
337         return;
338     QStringList nameFilter;
339     nameFilter << "*.so" << "*.so.*";
340     QStringList files = getFilesFromDir(_pluginPath, nameFilter);
341
342     foreach(QString file, files) {
343         QPluginLoader loader(file);
344         if(!loader.load()) {
345             Q_EMIT notify(Notify::Error,
346                     QString("%1 plugin cannot be loaded: %2.")
347                     .arg(file).arg(loader.errorString()));
348             continue;
349         }
350         QObject *pl = loader.instance();
351
352         bool exists = 0;
353         CommonDictInterface *plugin = qobject_cast<CommonDictInterface*>(pl);
354         foreach(CommonDictInterface* pl, _plugins)
355             if(pl->type() == plugin->type()) {
356                 exists = 1;
357                 break;
358            }
359         if(!exists)
360             _plugins.append(plugin);
361     }
362 }
363
364
365
366 CommonDictInterface* Backbone::plugin(QString type) {
367     foreach(CommonDictInterface* plugin, _plugins)
368         if(plugin->type() == type)
369             return plugin;
370     return 0;
371 }
372
373
374
375 void Backbone::loadPrefs(QString fileName) {
376     if(dryRun)
377         return;
378     QFileInfo file(QDir::toNativeSeparators(fileName));
379     QDir confDir(file.dir());
380     if(!confDir.exists()){
381         qDebug() << "Configuration file doesn't exist ("
382                 << file.filePath() << ")";
383         Q_EMIT notify(Notify::Warning,
384                 QString("%1 configuration file doesn't exist.")
385                 .arg(file.filePath()));
386         return;
387     }
388     QSettings set(file.filePath(), QSettings::IniFormat);
389     _pluginPath = set.value("general/plugin_path", _pluginPath).toString();
390     _historyLen = set.value("general/history_size", 10).toInt();
391     _searchLimit = set.value("general/search_limit", 15).toInt();
392     _searchBookmarks = set.value("general/search_bookmarks",1).toBool();
393     _searchDicts = set.value("general/search_dictionaries",1).toBool();
394     _zoom = set.value("general/zoom", 1.0).toReal();
395 }
396
397
398
399 void Backbone::savePrefs(QSettings *set) {
400     if(dryRun)
401         return;
402     set->setValue("general/plugin_path", _pluginPath);
403     set->setValue("general/history_size", _historyLen);
404     set->setValue("general/search_limit", _searchLimit);
405     set->setValue("general/search_bookmarks", _searchBookmarks);
406     set->setValue("general/search_dictionaries", _searchDicts);
407     set->setValue("general/zoom", _zoom);
408 }
409
410
411
412 void Backbone::loadDicts(QString fileName) {
413     if(dryRun)
414         return;
415
416     QFileInfo file(QDir::toNativeSeparators(fileName));
417     QDir confDir(file.dir());
418     if(!confDir.exists()){
419         qDebug() << "Configuration file doesn't exist ("
420                 << file.filePath() << ")";
421         Q_EMIT notify(Notify::Warning,
422                 QString("%1 configurationfile doesn't exist.")
423                 .arg(file.filePath()));
424         return;
425     }
426
427     QSettings set(file.filePath(), QSettings::IniFormat);
428     QStringList dicts = set.childGroups();
429     foreach(QString dict, dicts) {
430         if(!dict.contains("dictionary_"))
431             continue;
432         CommonDictInterface* plug = plugin
433                                     (set.value(dict + "/type", "").toString());
434         if(!plug) {
435             qDebug() << "Config file error: "
436                     << set.value(dict + "/type", "").toString()
437                     << " doesn't exist";
438             Q_EMIT notify(Notify::Warning,
439                     QString("Configuration file error. %2 plugin doesn't exist.")
440                     .arg(set.value(dict + "/type", "").toString()));
441             continue;
442         }
443         Settings* plugSet = new Settings();
444         set.beginGroup(dict);
445         QStringList items = set.childKeys();
446         foreach(QString item, items) {
447             plugSet->setValue(item, set.value(item, "").toString());
448         }
449         bool active = set.value("active",1).toBool();
450
451         set.endGroup();
452         addInternalDictionary(plug->getNew(plugSet), active);
453     }
454 }
455
456
457
458 void Backbone::dictUpdated() {
459     if(dryRun)
460         return;
461
462     // For convienence this function is called for each change in dictionaries
463     // and each call dumps configuration for all dictionaries into file.
464     // Maybe better way would be to store new/changed configuration but
465     // parsing settings file and figuring out what was changed, in my opinion,
466     // would take more time
467     _history->setMaxSize(_historyLen);
468     QFileInfo file(QDir::toNativeSeparators(_configPath));
469     QDir confDir(file.dir());
470     if(!confDir.exists())
471         confDir.mkpath(file.dir().path());
472     QSettings set(file.filePath(), QSettings::IniFormat);
473     set.clear();
474
475     savePrefs(&set);
476
477     foreach(CommonDictInterface* dict, _dicts.keys()){
478         if(!dict || !dict->settings())
479             continue;
480         saveState(&set, dict->settings(), _dicts[dict], dict->hash());
481     }
482 }
483
484
485
486 void Backbone::saveState(QSettings* set, Settings* plugSet, bool active
487                          , uint hash) {
488     if(dryRun)
489         return;
490     if(!set || !plugSet)
491         return;
492
493     QString section;
494     section.append(QString("dictionary_%1").arg(hash));
495     QList<QString> keys = plugSet->keys();
496     foreach(QString key, keys)
497         set->setValue(section + "/" + key, plugSet->value(key));
498     set->setValue(section + "/active", active);
499 }
500
501
502
503 QStringList Backbone::htmls() {
504     return _htmlResult;
505 }
506
507
508
509 void Backbone::searchHtml(QList<Translation *> translations) {
510     _htmlResult.clear();
511
512     QList<TranslationPtr> dummy;
513     stopped = false;
514     foreach(Translation* tr, translations) {
515          if(containsDict(tr->dict()) || !tr->dict())
516             dummy.append(TranslationPtr(tr));
517   /*      foreach(CommonDictInterface* dict, activeDicts()) {
518             Translation* trans = dict->getTranslationFor(tr->key());
519             if(trans)
520                 dummy.append(TranslationPtr(trans));
521         } */
522     }
523     if(translations.size()>0) {
524         Translation *tr = translations.at(0);
525         foreach(CommonDictInterface* dict, activeDicts()) {
526             Translation* trans = dict->getTranslationFor(tr->key());
527             if(trans)
528                 dummy.append(TranslationPtr(trans));
529         }
530     }
531
532    _innerHtmlResult = QtConcurrent::mapped(dummy,
533                                             &TranslationPtr::toHtml);
534    _htmlResultWatcher.setFuture(_innerHtmlResult);
535 }
536
537
538
539 void Backbone::htmlTranslationReady() {
540
541     QFutureIterator<QString> it(_innerHtmlResult);
542     QSet<QString> uniqe;
543     while(it.hasNext())
544         uniqe.insert(it.next());
545     _htmlResult.clear();
546     _htmlResult = uniqe.toList();
547
548     if(!stopped)
549         Q_EMIT htmlReady();
550
551 }
552
553
554 QList<CommonDictInterface*> Backbone::activeDicts() {
555     QList<CommonDictInterface*>res;
556     foreach(CommonDictInterface* dict, _dicts.keys())
557         if(_dicts[dict])
558             res.append(dict);
559     return res;
560
561 }
562
563
564
565 void Backbone::bookmarksListReady() {
566    _bookmarksResult = _innerBookmarks.result();
567    Q_EMIT bookmarksReady();
568 }
569
570
571
572
573 void Backbone::setSettings(Settings *settings) {
574     _historyLen = settings->value("history_size").toInt();
575     _searchLimit = settings->value("search_limit").toInt();
576     if(settings->value("search_dictionaries") == "true")
577         _searchDicts = 1;
578     else
579         _searchDicts = 0;
580     if(settings->value("search_bookmarks") == "true")
581         _searchBookmarks = 1;
582     else
583         _searchBookmarks = 0;
584     _zoom = settings->value("zoom").toFloat();
585     if(!_zoom)
586         _zoom ++;
587
588     dictUpdated();
589     if(settings)
590         delete settings;
591 }
592
593
594
595
596 Settings* Backbone::settings() {
597     Settings * settings = new Settings();
598     settings->setValue("history_size", QString("%1").arg(_historyLen));
599     settings->setValue("search_limit", QString("%1").arg(_searchLimit));
600     settings->setValue("zoom", QString("%1").arg(_zoom));
601     if(_searchBookmarks)
602         settings->setValue("search_bookmarks", "true");
603     else
604         settings->setValue("search_bookmarks", "false");
605
606     if(_searchDicts)
607         settings->setValue("search_dictionaries", "true");
608     else
609         settings->setValue("search_dictionaries", "false");
610     return settings;
611 }
612
613
614 bool Backbone::containsDict(uint hash) const {
615     QHashIterator<CommonDictInterface*, bool> it(_dicts);
616     if (!hash)
617         return false;
618     while(it.hasNext())
619         if(it.next().key()->hash() == hash)
620             return true;
621     return false;
622 }