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