Salome HOME
[bos #40644][CEA](2024-T1) Feauture search.
[modules/gui.git] / src / SUIT / SUIT_ShortcutMgr.cxx
1 // Copyright (C) 2007-2024  CEA, EDF, OPEN CASCADE
2 //
3 // Copyright (C) 2003-2007  OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN,
4 // CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS
5 //
6 // This library is free software; you can redistribute it and/or
7 // modify it under the terms of the GNU Lesser General Public
8 // License as published by the Free Software Foundation; either
9 // version 2.1 of the License, or (at your option) any later version.
10 //
11 // This library is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 // Lesser General Public License for more details.
15 //
16 // You should have received a copy of the GNU Lesser General Public
17 // License along with this library; if not, write to the Free Software
18 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
19 //
20 // See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
21 //
22
23 #include "SUIT_ShortcutMgr.h"
24
25 #include "SUIT_Session.h"
26 #include "SUIT_ResourceMgr.h"
27 #include "SUIT_MessageBox.h"
28
29 #include <QAction>
30 #include <QtxAction.h>
31
32 #include <QApplication>
33 #include <QActionEvent>
34 #include <QKeySequence>
35 #include <QDebug>
36 #include <QJsonObject>
37 #include <QJsonDocument>
38 #include <QJsonParseError>
39 #include <QFile>
40 #include <QProcessEnvironment>
41 #include <QRegExp>
42 #include <Qtx.h>
43
44 #include <list>
45
46
47 #include <iostream>
48 #include <string>
49 const std::wstring SHORTCUT_MGR_LOG_PREFIX = L"SHORTCUT_MGR_DBG: ";
50 bool ShCutDbg(const QString& theString)
51 {
52   if (ShCutDbg()) {
53     std::wcout << SHORTCUT_MGR_LOG_PREFIX << theString.toStdWString() << std::endl;
54     return true;
55   }
56   return false;
57 }
58 bool ShCutDbg(const char* src)
59 {
60   if (ShCutDbg()) {
61     std::wcout << SHORTCUT_MGR_LOG_PREFIX << std::wstring(src, src + strlen(src)) << std::endl;
62     return true;
63   }
64   return false;
65 }
66
67 void Warning(const QString& theString)
68 {
69   std::wcout << theString.toStdWString() << std::endl;
70 }
71 void Warning(const char* src)
72 {
73   std::wcout << std::wstring(src, src + strlen(src)) << std::endl;
74 }
75
76
77 static const QKeySequence NO_KEYSEQUENCE = QKeySequence(QString(""));
78 static const QString NO_ACTION = QString("");
79 /** Separates tokens in action ID. */
80 static const QString TOKEN_SEPARATOR = QString("/");
81 /*static*/ const QString SUIT_ShortcutMgr::ROOT_MODULE_ID = QString("");
82 static const QString META_ACTION_PREFIX = QString("#");
83
84 /** Prefix of names of shortcut setting sections in preference files. */
85 static const QString SECTION_NAME_PREFIX = QString("shortcuts");
86
87
88 const QString DEFAULT_LANG = QString("en");
89 const QStringList LANG_PRIORITY_LIST = QStringList({DEFAULT_LANG, "fr"});
90 const QString LANG_SECTION = QString("language");
91
92 static const QString SECTION_NAME_ACTION_ASSET_FILE_PATHS = QString("action_assets");
93
94
95
96 /**
97  * Uncomment this, to start collecting all shortcuts and action assets (1),
98  * from instances of QtxActions, if a shortcut or action assets are absent in resource/asset files.
99  *
100  * (1) Set required language in the application settings and run features of interest.
101  * For all actions from these features, their assets will be dumped to appropriate places in dump files.
102  *
103  * Content of dump files is appended on every run. Files are located in "<APP_DIR>/shortcut_mgr_dev/".
104 */
105 // #define SHORTCUT_MGR_DEVTOOLS
106 #ifdef SHORTCUT_MGR_DEVTOOLS
107 #include <QDir>
108 #include <QFile>
109 #include <QFileInfo>
110 #include <QTextStream>
111 #include "QtxMap.h"
112 #include <functional>
113 #ifndef QT_NO_DOM
114 #include <QDomDocument>
115 #include <QDomElement>
116 #include <QDomNode>
117 #endif // QT_NO_DOM
118
119 /*! \brief Generates XML files with appearing at runtime shortcuts,
120     using key sequences of QActions passed to the shortcut manager,
121     and JSON files with assets of QtxActions passed to the shortcut manager.
122     Content of these files can be easily copied to resource/asset files. */
123 class DevTools
124 {
125 private:
126   DevTools() : myActionsWithInvalidIDsFile(nullptr) {};
127   DevTools(const DevTools&) = delete;
128   void operator=(const DevTools&) = delete;
129
130 public:
131   ~DevTools()
132   {
133     for (const auto& fileNameAndPtrs : myXMLFilesAndDocs) {
134       delete fileNameAndPtrs.second.second;
135       delete fileNameAndPtrs.second.first;
136     }
137
138     for (const auto& fileNameAndPtrs : myJSONFilesAndDocs) {
139       delete fileNameAndPtrs.second.second;
140       delete fileNameAndPtrs.second.first;
141     }
142   }
143
144   static DevTools* get() {
145     if (!DevTools::instance)
146       DevTools::instance = new DevTools();
147
148     return DevTools::instance;
149   }
150
151   void collectShortcut(
152     const QString& theModuleID,
153     const QString& theInModuleActionID,
154     const QKeySequence& theKeySequence
155   ) {
156     if (SUIT_ShortcutMgr::isInModuleMetaActionID(theInModuleActionID)) {
157       auto& moduleShortcuts = myShortcutsOfMetaActions[theModuleID];
158       moduleShortcuts[theInModuleActionID] = theKeySequence.toString();
159
160       const QString fileName = theModuleID + DevTools::SHORTCUTS_OF_META_SUFFIX;
161       const QString sectionName = SECTION_NAME_PREFIX + DevTools::XML_SECTION_TOKENS_SEPARATOR + SUIT_ShortcutMgr::ROOT_MODULE_ID;
162       std::map<QString, std::map<QString, QString>> sections;
163       sections[sectionName] = moduleShortcuts;
164       writeToXMLFile(fileName, sections);
165     }
166     else {
167       auto& moduleShortcuts = myShortcuts[theModuleID];
168       moduleShortcuts[theInModuleActionID] = theKeySequence.toString();
169
170       const QString fileName = theModuleID + DevTools::SHORTCUTS_SUFFIX;
171       const QString sectionName = SECTION_NAME_PREFIX + DevTools::XML_SECTION_TOKENS_SEPARATOR + theModuleID;
172       std::map<QString, std::map<QString, QString>> sections;
173       sections[sectionName] = moduleShortcuts;
174       writeToXMLFile(fileName, sections);
175     }
176   }
177
178   void collectAssets(
179     const QString& theModuleID,
180     const QString& theInModuleActionID,
181     const QString& theLang,
182     const QAction* theAction
183   ) {
184     if (SUIT_ShortcutMgr::isInModuleMetaActionID(theInModuleActionID)) {
185       QString actionID = SUIT_ShortcutMgr::makeActionID(SUIT_ShortcutMgr::ROOT_MODULE_ID, theInModuleActionID);
186       // { actionID, assets } []
187       auto& moduleAssets = myAssetsOfMetaActions[theModuleID];
188
189       auto& actionAssets = moduleAssets[actionID];
190       actionAssets.myLangDependentAssets[theLang].myName = theAction->text();
191       actionAssets.myLangDependentAssets[theLang].myToolTip = theAction->statusTip();
192
193       const QString fileName = theModuleID + DevTools::ASSETS_OF_META_SUFFIX;
194       writeToJSONFile(fileName, actionID, actionAssets);
195     }
196     else {
197       QString actionID = SUIT_ShortcutMgr::makeActionID(theModuleID, theInModuleActionID);
198       // { actionID, assets } []
199       auto& moduleAssets = myAssets[theModuleID];
200
201       auto& actionAssets = moduleAssets[actionID];
202       actionAssets.myLangDependentAssets[theLang].myName = theAction->text();
203       actionAssets.myLangDependentAssets[theLang].myToolTip = theAction->statusTip();
204
205       const QString fileName = theModuleID + DevTools::ASSETS_SUFFIX;
206       writeToJSONFile(fileName, actionID, actionAssets);
207     }
208   }
209
210   void collectShortcutAndAssets(const QtxAction* const theAction)
211   {
212     const auto moduleIDAndActionID = SUIT_ShortcutMgr::splitIntoModuleIDAndInModuleID(theAction->ID());
213     if (moduleIDAndActionID.second.isEmpty())
214       return;
215
216     if (!SUIT_ShortcutMgr::get()->getShortcutContainer().hasShortcut(moduleIDAndActionID.first, moduleIDAndActionID.second))
217       collectShortcut(moduleIDAndActionID.first, moduleIDAndActionID.second, theAction->shortcut());
218
219     { // Collect action assets, if they are not provided in asset files.
220       SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr();
221       if (!resMgr) {
222         Warning("DevTools for SUIT_ShortcutMgr can't retrieve resource manager!");
223         return;
224       }
225
226       const QString lang = resMgr->stringValue(LANG_SECTION, LANG_SECTION);
227       if (lang.isEmpty())
228         return;
229
230       const auto& assetsInResources = SUIT_ShortcutMgr::getActionAssetsFromResources(theAction->ID());
231       if (assetsInResources.first && assetsInResources.second.myLangDependentAssets.find(lang) != assetsInResources.second.myLangDependentAssets.end())
232         return;
233
234       collectAssets(moduleIDAndActionID.first, moduleIDAndActionID.second, lang, theAction);
235     }
236   }
237
238 private:
239   /*! Appends new entries to content of dump files. */
240   bool writeToXMLFile(const QString& theFileName, const std::map<QString, std::map<QString, QString>>& theSections)
241   {
242 #ifdef QT_NO_DOM
243   Warning("DebugTools for SUIT_ShortcutMgr can't create XML - #QT_NO_DOM is defined.");
244   return false;
245 #else QT_NO_DOM
246     static const QString DOC_TAG = "document";
247     static const QString SECTION_TAG = "section";
248     static const QString PARAMETER_TAG = "parameter";
249     static const QString NAME_ATTR = "name";
250     static const QString VAL_ATTR = "value";
251
252     const auto itFileAndDoc = myXMLFilesAndDocs.find(theFileName);
253     if (itFileAndDoc == myXMLFilesAndDocs.end()) {
254       const QString fullPath = DevTools::SAVE_PATH + theFileName + ".xml";
255       if (!Qtx::mkDir(QFileInfo(fullPath).absolutePath())) {
256         myXMLFilesAndDocs[theFileName] = std::pair<QFile*, QDomDocument*>(nullptr, nullptr);
257         return false;
258       }
259
260       QFile* file = new QFile(fullPath);
261       if (!file->open(QFile::ReadWrite | QIODevice::Text)) {
262         delete file;
263         myXMLFilesAndDocs[theFileName] = std::pair<QFile*, QDomDocument*>(nullptr, nullptr);
264         return false;
265       }
266
267       QDomDocument* dom = new QDomDocument(DOC_TAG);
268       QTextStream instream(file);
269       dom->setContent(instream.readAll());
270       myXMLFilesAndDocs[theFileName] = std::pair<QFile*, QDomDocument*>(file, dom);
271     }
272     else if (itFileAndDoc->second.first == nullptr) {
273       return false;
274     }
275
276     const auto fileAndDom = myXMLFilesAndDocs[theFileName];
277     QFile* const file = fileAndDom.first;
278     QDomDocument* const dom = fileAndDom.second;
279
280     QDomElement doc = dom->documentElement();
281     if (doc.isNull()) {
282       *dom = QDomDocument(DOC_TAG);
283       doc = dom->createElement(DOC_TAG);
284       dom->appendChild(doc);
285     }
286
287     static const std::function<void(const std::map<QString, QString>&, QDomDocument&, QDomElement&)> mergeParamsToSection =
288     [&](const std::map<QString, QString>& parameters, QDomDocument& dom, QDomElement& sectionInDom)
289     {
290       for (const std::pair<QString, QString>& nameAndVal : parameters) {
291         const QString& paramName = nameAndVal.first;
292         const QString& paramVal = nameAndVal.second;
293         bool fileHasParam = false;
294         for (QDomElement paramInDom = sectionInDom.firstChildElement(PARAMETER_TAG); !paramInDom.isNull(); paramInDom = paramInDom.nextSiblingElement(PARAMETER_TAG)) {
295           const QString paramNameInDom = paramInDom.attribute(NAME_ATTR);
296           if (paramName == paramNameInDom) {
297             const QString paramValInDom = paramInDom.attribute(VAL_ATTR);
298             if (paramValInDom != paramVal) {
299               QDomElement replaceElement = dom.createElement(PARAMETER_TAG);
300               replaceElement.setAttribute(NAME_ATTR, paramName);
301               replaceElement.setAttribute(VAL_ATTR, paramVal);
302               sectionInDom.replaceChild(replaceElement, paramInDom);
303             }
304
305             fileHasParam = true;
306             break;
307           }
308         }
309         if (!fileHasParam) {
310           QDomElement newParam = dom.createElement(PARAMETER_TAG);
311           newParam.setAttribute(NAME_ATTR, paramName);
312           newParam.setAttribute(VAL_ATTR, paramVal);
313           sectionInDom.insertAfter(newParam, sectionInDom.lastChildElement(PARAMETER_TAG));
314         }
315       }
316       return;
317     };
318
319     for (const auto& sectionNameAndParams : theSections) {
320       const QString& sectionName = sectionNameAndParams.first;
321       const std::map<QString, QString>& parameters = sectionNameAndParams.second;
322
323       bool fileHasSection = false;
324       for (QDomElement sectionInDom = doc.firstChildElement(SECTION_TAG); !sectionInDom.isNull(); sectionInDom = sectionInDom.nextSiblingElement(SECTION_TAG)) {
325         QString sectionNameInDom = sectionInDom.attribute(NAME_ATTR);
326         if (sectionNameInDom == sectionName) {
327           mergeParamsToSection(parameters, *dom, sectionInDom);
328           fileHasSection = true;
329           break;
330         }
331       }
332
333       if (!fileHasSection) {
334         QDomElement newSection = dom->createElement(SECTION_TAG);
335         newSection.setAttribute(NAME_ATTR, sectionName);
336         doc.insertAfter(newSection, doc.lastChildElement(SECTION_TAG));
337         mergeParamsToSection(parameters, *dom, newSection);
338       }
339     }
340
341     file->resize(0);
342     QTextStream outstream(file);
343     outstream << dom->toString();
344
345     return true;
346 #endif // QT_NO_DOM
347   }
348
349   /*! Appends new entries to content of dump files. */
350   bool writeToJSONFile(const QString& theFileName, const QString& theActionID, const SUIT_ActionAssets& theAssets)
351   {
352     const auto itFileAndDoc = myJSONFilesAndDocs.find(theFileName);
353     if (itFileAndDoc == myJSONFilesAndDocs.end()) {
354       const QString fullPath = DevTools::SAVE_PATH + theFileName + ".json";
355       if (!Qtx::mkDir(QFileInfo(fullPath).absolutePath())) {
356         myJSONFilesAndDocs[theFileName] = std::pair<QFile*, QJsonDocument*>(nullptr, nullptr);
357         return false;
358       }
359
360       const bool fileExisted = QFileInfo::exists(fullPath);
361       QFile* file = new QFile(fullPath);
362       if (!file->open(QFile::ReadWrite | QIODevice::Text)) {
363         delete file;
364         myJSONFilesAndDocs[theFileName] = std::pair<QFile*, QJsonDocument*>(nullptr, nullptr);
365         return false;
366       }
367
368       QJsonParseError jsonError;
369       QJsonDocument* document = new QJsonDocument(QJsonDocument::fromJson(file->readAll(), &jsonError));
370       if (jsonError.error != QJsonParseError::NoError && fileExisted) {
371         Warning("SUIT_ShortcutMgr: error during parsing of action asset dump file \"" + fullPath + "\"!");
372         delete file;
373         delete document;
374         myJSONFilesAndDocs[theFileName] = std::pair<QFile*, QJsonDocument*>(nullptr, nullptr);
375         return false;
376       }
377
378       if (!document->isObject()) {
379         document->setObject(QJsonObject());
380         file->resize(0);
381         QTextStream outstream(file);
382         outstream << document->toJson(QJsonDocument::Indented);
383       }
384
385       myJSONFilesAndDocs[theFileName] = std::pair<QFile*, QJsonDocument*>(file, document);
386     }
387     else if (itFileAndDoc->second.first == nullptr) {
388       return false;
389     }
390
391     const auto fileAndDoc = myJSONFilesAndDocs[theFileName];
392     QFile* const file = fileAndDoc.first;
393     QJsonDocument* const document = fileAndDoc.second;
394
395     QJsonObject rootJSON = document->object();
396     QJsonObject actionAssetsJSON = rootJSON[theActionID].toObject();
397     SUIT_ActionAssets actionAssets;
398     actionAssets.fromJSON(actionAssetsJSON);
399     actionAssets.merge(theAssets, true /*theOverride*/);
400     actionAssets.toJSON(actionAssetsJSON);
401     rootJSON[theActionID] = actionAssetsJSON;
402     document->setObject(rootJSON);
403
404     file->resize(0);
405     QTextStream outstream(file);
406     outstream << document->toJson(QJsonDocument::Indented);
407
408     return true;
409   }
410
411 public:
412   void collectAssetsOfActionWithInvalidID(const QAction* const theAction)
413   {
414     SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr();
415     if (!resMgr) {
416       Warning("DevTools for SUIT_ShortcutMgr can't retrieve resource manager!");
417       return;
418     }
419
420     const QString lang = resMgr->stringValue(LANG_SECTION, LANG_SECTION);
421     if (lang.isEmpty())
422       return;
423
424     if (!myActionsWithInvalidIDsFile) {
425       const QString fullPath = DevTools::SAVE_PATH + lang + DevTools::INVALID_ID_ACTIONS_SUFFIX + ".csv";
426       if (!Qtx::mkDir(QFileInfo(fullPath).absolutePath()))
427         return;
428
429       myActionsWithInvalidIDsFile = new QFile(fullPath);
430       if (!myActionsWithInvalidIDsFile->open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
431         delete myActionsWithInvalidIDsFile;
432         myActionsWithInvalidIDsFile = nullptr;
433         return;
434       }
435
436       QTextStream ostream(myActionsWithInvalidIDsFile);
437       ostream << "text\t" << "tool tip\t" << "status tip\t" << "key sequence\t" << "QtxAction?\t" << "ID\n";
438       ostream.flush();
439     }
440
441     QTextStream ostream(myActionsWithInvalidIDsFile);
442     const auto aQtxAction = qobject_cast<const QtxAction*>(theAction);
443     ostream << theAction->text() << "\t" << theAction->toolTip() << "\t" << theAction->statusTip() << "\t"
444     << theAction->shortcut().toString() << "\t" << (aQtxAction ? "yes\t" : "no\t") << (aQtxAction ? aQtxAction->ID() + "\n" : "\n");
445     ostream.flush();
446   }
447
448   static const QString SAVE_PATH;
449   static const QString SHORTCUTS_SUFFIX;
450   static const QString SHORTCUTS_OF_META_SUFFIX;
451   static const QString ASSETS_SUFFIX;
452   static const QString ASSETS_OF_META_SUFFIX;
453   static const QString INVALID_ID_ACTIONS_SUFFIX;
454
455   static DevTools* instance;
456   static const QString XML_SECTION_TOKENS_SEPARATOR;
457
458   /** { moduleID, { inModuleActionID, keySequence }[] }[]. keySequence can be empty. */
459   std::map<QString, std::map<QString, QString>> myShortcuts;
460
461   /** { moduleID, { inModuleActionID, keySequence }[] }[]. keySequence can be empty. */
462   std::map<QString, std::map<QString, QString>> myShortcutsOfMetaActions;
463
464   /** { moduleID, { actionID, assets }[] }[] */
465   std::map<QString, std::map<QString, SUIT_ActionAssets>> myAssets;
466
467   /** { moduleID, { actionID, assets }[] }[] */
468   std::map<QString, std::map<QString, SUIT_ActionAssets>> myAssetsOfMetaActions;
469
470 #ifndef QT_NO_DOM
471   // { filename, {file, domDoc} }[]
472   std::map<QString, std::pair<QFile*, QDomDocument*>> myXMLFilesAndDocs;
473 #endif // QT_NO_DOM
474   // { filename, {file, jsonDoc} }[]
475   std::map<QString, std::pair<QFile*, QJsonDocument*>> myJSONFilesAndDocs;
476
477   QFile* myActionsWithInvalidIDsFile;
478 };
479 /*static*/ DevTools* DevTools::instance = nullptr;
480 /*static*/ const QString DevTools::SAVE_PATH = "shortcut_mgr_dev/";
481 /*static*/ const QString DevTools::INVALID_ID_ACTIONS_SUFFIX = "_actions_with_invalid_IDs";
482 /*static*/ const QString DevTools::XML_SECTION_TOKENS_SEPARATOR = ":";
483 /*static*/ const QString DevTools::SHORTCUTS_SUFFIX = "_shortcuts";
484 /*static*/ const QString DevTools::SHORTCUTS_OF_META_SUFFIX = "_shortcuts_of_meta_actions";
485 /*static*/ const QString DevTools::ASSETS_SUFFIX = "_assets";
486 /*static*/ const QString DevTools::ASSETS_OF_META_SUFFIX = "_assets_of_meta_actions";
487 #endif // SHORTCUT_MGR_DEVTOOLS
488
489
490
491 SUIT_ShortcutContainer::SUIT_ShortcutContainer()
492 {
493   myShortcuts.emplace(SUIT_ShortcutMgr::ROOT_MODULE_ID, std::map<QKeySequence, QString>());
494   myShortcutsInversed.emplace(SUIT_ShortcutMgr::ROOT_MODULE_ID, std::map<QString, QKeySequence>());
495 }
496
497 std::set<QString> SUIT_ShortcutContainer::getIDsOfInterferingModules(const QString& theModuleID) const
498 {
499   std::set<QString> IDsOfInterferingModules;
500   if (theModuleID == SUIT_ShortcutMgr::ROOT_MODULE_ID) {
501     for (const auto& moduleIDAndShortcuts : myShortcuts) {
502       IDsOfInterferingModules.emplace(moduleIDAndShortcuts.first);
503     }
504   }
505   else {
506     IDsOfInterferingModules.emplace(SUIT_ShortcutMgr::ROOT_MODULE_ID);
507     if (theModuleID != SUIT_ShortcutMgr::ROOT_MODULE_ID)
508       IDsOfInterferingModules.emplace(theModuleID);
509   }
510   return IDsOfInterferingModules;
511 }
512
513 std::set<QString> SUIT_ShortcutContainer::getIDsOfAllModules() const
514 {
515   std::set<QString> res;
516   for (const auto& moduleIDAndShortcuts : myShortcutsInversed) {
517     res.emplace(moduleIDAndShortcuts.first);
518   }
519   return res;
520 }
521
522 std::set<std::pair<QString, QString>> SUIT_ShortcutContainer::setShortcut(QString theModuleID, const QString& theInModuleActionID, const QKeySequence& theKeySequence, bool theOverride)
523 {
524   if (!SUIT_ShortcutMgr::isModuleIDValid(theModuleID)) {
525     ShCutDbg() && ShCutDbg("Attempt to define a shortcut using invalid module ID = \"" + theModuleID + "\".");
526     return std::set<std::pair<QString, QString>>();
527   }
528
529   if (!SUIT_ShortcutMgr::isInModuleActionIDValid(theInModuleActionID)) {
530     ShCutDbg() && ShCutDbg("Attempt to define a shortcut using invalid in-module action ID = \"" + theInModuleActionID + "\".");
531     return std::set<std::pair<QString, QString>>();
532   }
533
534   if (SUIT_ShortcutMgr::isInModuleMetaActionID(theInModuleActionID))
535     theModuleID = SUIT_ShortcutMgr::ROOT_MODULE_ID;
536
537   auto itModuleShortcuts = myShortcuts.find(theModuleID);
538   auto itModuleShortcutsInversed = myShortcutsInversed.find(theModuleID);
539   if (itModuleShortcuts == myShortcuts.end()) {
540     itModuleShortcuts = myShortcuts.emplace(theModuleID, std::map<QKeySequence, QString>()).first;
541     itModuleShortcutsInversed = myShortcutsInversed.emplace(theModuleID, std::map<QString, QKeySequence>()).first;
542   }
543
544   std::map<QKeySequence, QString>& moduleShortcuts = itModuleShortcuts->second;
545   std::map<QString, QKeySequence>& moduleShortcutsInversed = itModuleShortcutsInversed->second;
546
547   if (theKeySequence.isEmpty()) {
548     // Disable shortcut.
549
550     auto itShortcutInversed = moduleShortcutsInversed.find(theInModuleActionID);
551     if (itShortcutInversed == moduleShortcutsInversed.end()) {
552       // No key sequence was mapped to the action earlier.
553       // Set disabled shortcut.
554       moduleShortcutsInversed.emplace(theInModuleActionID, NO_KEYSEQUENCE);
555       return std::set<std::pair<QString, QString>>();
556     }
557     else /* if keySequence was mapped to the action earlier. */ {
558       QKeySequence& keySequence = itShortcutInversed->second;
559
560       moduleShortcuts.erase(keySequence);
561       keySequence = NO_KEYSEQUENCE;
562
563       return std::set<std::pair<QString, QString>>();
564     }
565   }
566
567   { // Check if the shortcut is already set.
568     const auto itShortcut = moduleShortcuts.find(theKeySequence);
569     if (itShortcut != moduleShortcuts.end()) {
570       if (itShortcut->second == theInModuleActionID) {
571         // The shortcut was set earlier. Nothing to change.
572         return std::set<std::pair<QString, QString>>();
573       }
574     }
575   }
576
577   auto conflictingActionIDs = std::set<std::pair<QString, QString>>();
578   { // Look for conflicting shortcuts with the same key sequence from interfering modules.
579     std::set<QString> IDsOfInterferingModules = getIDsOfInterferingModules(theModuleID);
580     for (const QString& IDOfInterferingModule : IDsOfInterferingModules) {
581       std::map<QKeySequence, QString>& shortcutsOfInterferingModule = myShortcuts.at(IDOfInterferingModule);
582       auto itConflictingShortcut = shortcutsOfInterferingModule.find(theKeySequence);
583       if (itConflictingShortcut != shortcutsOfInterferingModule.end()) {
584         const QString& conflictingActionID = itConflictingShortcut->second;
585
586         conflictingActionIDs.insert(std::pair<QString, QString>(IDOfInterferingModule, conflictingActionID));
587
588         if (theOverride) {
589           // Disable conflicting shortcuts.
590           std::map<QString, QKeySequence>& shortcutsOfInterferingModuleInversed = myShortcutsInversed.at(IDOfInterferingModule);
591           shortcutsOfInterferingModuleInversed[conflictingActionID] = NO_KEYSEQUENCE;
592           shortcutsOfInterferingModule.erase(itConflictingShortcut);
593         }
594       }
595     }
596
597     if (!theOverride && !conflictingActionIDs.empty())
598       return conflictingActionIDs;
599   }
600
601   { // Ensure, that the module has only shortcut for the action ID.
602     auto itShortcutInversed = moduleShortcutsInversed.find(theInModuleActionID);
603     if (itShortcutInversed != moduleShortcutsInversed.end()) {
604       // Redefine key sequence for existing action.
605
606       QKeySequence& keySequence = itShortcutInversed->second;
607
608       moduleShortcuts.erase(keySequence);
609       moduleShortcuts[theKeySequence] = theInModuleActionID;
610
611       keySequence = theKeySequence;
612     }
613     else /* if the action has not been added earlier. */ {
614       moduleShortcuts[theKeySequence] = theInModuleActionID;
615       moduleShortcutsInversed[theInModuleActionID] = theKeySequence;
616     }
617   }
618
619   return conflictingActionIDs;
620 }
621
622 std::set<std::pair<QString, QString>> SUIT_ShortcutContainer::getConflicts(
623   QString theModuleID,
624   const QString& theInModuleActionID,
625   const QKeySequence& theKeySequence
626 ) const
627 {
628   if (theKeySequence.isEmpty())
629     return std::set<std::pair<QString, QString>>();
630
631   if (SUIT_ShortcutMgr::isInModuleMetaActionID(theInModuleActionID))
632     theModuleID = SUIT_ShortcutMgr::ROOT_MODULE_ID;
633
634   { // Check if the shortcut is set.
635     const auto itModuleShortcuts = myShortcuts.find(theModuleID);
636     if (itModuleShortcuts != myShortcuts.end()) {
637       const std::map<QKeySequence, QString>& moduleShortcuts = itModuleShortcuts->second;
638       const auto itShortcut = moduleShortcuts.find(theKeySequence);
639       if (itShortcut != moduleShortcuts.end()) {
640         if (itShortcut->second == theInModuleActionID) {
641           // The shortcut is set => no conflicts.
642           return std::set<std::pair<QString, QString>>();
643         }
644       }
645     }
646   }
647
648   auto conflictingActionIDs = std::set<std::pair<QString, QString>>();
649   { // Look for conflicting shortcuts with the same key sequence from interfering modules.
650     std::set<QString> IDsOfInterferingModules = getIDsOfInterferingModules(theModuleID);
651     for (const QString& IDOfInterferingModule : IDsOfInterferingModules) {
652       const std::map<QKeySequence, QString>& shortcutsOfInterferingModule = myShortcuts.at(IDOfInterferingModule);
653       const auto itConflictingShortcut = shortcutsOfInterferingModule.find(theKeySequence);
654       if (itConflictingShortcut != shortcutsOfInterferingModule.end()) {
655         const QString& conflictingActionID = itConflictingShortcut->second;
656         conflictingActionIDs.insert(std::pair<QString, QString>(IDOfInterferingModule, conflictingActionID));
657       }
658     }
659   }
660   return conflictingActionIDs;
661 }
662
663 const QKeySequence& SUIT_ShortcutContainer::getKeySequence(QString theModuleID, const QString& theInModuleActionID) const
664 {
665   if (SUIT_ShortcutMgr::isInModuleMetaActionID(theInModuleActionID))
666     theModuleID = SUIT_ShortcutMgr::ROOT_MODULE_ID;
667
668   const auto itModuleShortcutsInversed = myShortcutsInversed.find(theModuleID);
669   if (itModuleShortcutsInversed == myShortcutsInversed.end())
670     return NO_KEYSEQUENCE;
671
672   const auto& moduleShortcutsInversed = itModuleShortcutsInversed->second;
673   const auto itShortcutInversed = moduleShortcutsInversed.find(theInModuleActionID);
674   if (itShortcutInversed == moduleShortcutsInversed.end())
675     return NO_KEYSEQUENCE;
676
677   return itShortcutInversed->second;
678 }
679
680 bool SUIT_ShortcutContainer::hasShortcut(QString theModuleID, const QString& theInModuleActionID) const
681 {
682   if (SUIT_ShortcutMgr::isInModuleMetaActionID(theInModuleActionID))
683     theModuleID = SUIT_ShortcutMgr::ROOT_MODULE_ID;
684
685   const auto itModuleShortcutsInversed = myShortcutsInversed.find(theModuleID);
686   if (itModuleShortcutsInversed == myShortcutsInversed.end())
687     return false;
688
689   const auto& moduleShortcutsInversed = itModuleShortcutsInversed->second;
690   const auto itShortcutInversed = moduleShortcutsInversed.find(theInModuleActionID);
691   if (itShortcutInversed == moduleShortcutsInversed.end())
692     return false;
693
694   return true;
695 }
696
697 const std::map<QString, QKeySequence>& SUIT_ShortcutContainer::getModuleShortcutsInversed(const QString& theModuleID) const
698 {
699   static const std::map<QString, QKeySequence> EMPTY_RES;
700   const auto it = myShortcutsInversed.find(theModuleID);
701   if (it == myShortcutsInversed.end())
702     return EMPTY_RES;
703
704   return it->second;
705 }
706
707 const std::map<QString, QKeySequence> SUIT_ShortcutContainer::getModuleShortcutsInversed(const QString& theModuleID, const QString& theActionIDPrefix) const
708 {
709   const auto it = myShortcutsInversed.find(theModuleID);
710   if (it == myShortcutsInversed.end())
711     return std::map<QString, QKeySequence>();
712
713   std::map<QString, QKeySequence> shortcutsInversed;
714   for (const auto& existingShortcut : it->second) {
715     if (existingShortcut.first.startsWith(theActionIDPrefix))
716       shortcutsInversed[existingShortcut.first] = existingShortcut.second;
717   }
718   return shortcutsInversed;
719 }
720
721 QString SUIT_ShortcutContainer::toString() const
722 {
723   QString text;
724   text += "Shortcuts inversed:\n";
725   for (auto it = myShortcutsInversed.begin(); it != myShortcutsInversed.end(); it++) {
726     const QString& moduleID = it->first;
727     const auto& moduleShortcuts = it->second;
728     text += (it == myShortcutsInversed.begin() ? "\"" : "\n\"")  + moduleID + "\"";
729     for (const auto& shortcut : moduleShortcuts) {
730       text += "\n\t\"" + shortcut.first + "\"\t\"" + shortcut.second.toString() + "\"";
731     }
732   }
733   text += "\nShortcuts:\n";
734   for (auto it = myShortcuts.begin(); it != myShortcuts.end(); it++) {
735     const QString& moduleID = it->first;
736     const auto& moduleShortcuts = it->second;
737     text += (it == myShortcuts.begin() ? "\"" : "\n\"")  + moduleID + "\"";
738     for (const auto& shortcut : moduleShortcuts) {
739       text += "\n\t\"" + shortcut.first.toString() + "\"\t\"" + shortcut.second + "\"";
740     }
741   }
742   return text;
743 }
744
745 /*static*/ const QString SUIT_ActionAssets::LangDependentAssets::PROP_ID_NAME = "name";
746 /*static*/ const QString SUIT_ActionAssets::LangDependentAssets::PROP_ID_TOOLTIP = "tooltip";
747
748 bool SUIT_ActionAssets::LangDependentAssets::fromJSON(const QJsonObject& theJsonObject)
749 {
750   myName    = theJsonObject[SUIT_ActionAssets::LangDependentAssets::PROP_ID_NAME].toString();
751   myToolTip = theJsonObject[SUIT_ActionAssets::LangDependentAssets::PROP_ID_TOOLTIP].toString();
752
753   if (myName.isEmpty())
754     myName = myToolTip;
755
756   return !myName.isEmpty();
757 }
758
759 void SUIT_ActionAssets::LangDependentAssets::toJSON(QJsonObject& oJsonObject) const
760 {
761   oJsonObject[SUIT_ActionAssets::LangDependentAssets::PROP_ID_NAME] = myName;
762   oJsonObject[SUIT_ActionAssets::LangDependentAssets::PROP_ID_TOOLTIP] = myToolTip;
763 }
764
765 /*static*/ const QString SUIT_ActionAssets::STRUCT_ID = "SUIT_ActionAssets";
766 /*static*/ const QString SUIT_ActionAssets::PROP_ID_LANG_DEPENDENT_ASSETS = "langDependentAssets";
767 /*static*/ const QString SUIT_ActionAssets::PROP_ID_ICON_PATH = "iconPath";
768
769 bool SUIT_ActionAssets::fromJSON(const QJsonObject& theJsonObject)
770 {
771   myLangDependentAssets.clear();
772
773   auto lda = SUIT_ActionAssets::LangDependentAssets();
774   const auto& langToLdaJson = theJsonObject[SUIT_ActionAssets::PROP_ID_LANG_DEPENDENT_ASSETS].toObject();
775   for (const QString& lang : langToLdaJson.keys()) {
776     if (!lda.fromJSON(langToLdaJson[lang].toObject()))
777       continue;
778
779     myLangDependentAssets[lang] = lda;
780   }
781
782   myIconPath = theJsonObject[SUIT_ActionAssets::PROP_ID_ICON_PATH].toString();
783
784   return !myLangDependentAssets.empty();
785 }
786
787 void SUIT_ActionAssets::toJSON(QJsonObject& oJsonObject) const
788 {
789   auto langDependentAssetsJSON = QJsonObject();
790
791   auto langDependentAssetsItemJSON = QJsonObject();
792   for (const auto& langAndLDA : myLangDependentAssets) {
793     langAndLDA.second.toJSON(langDependentAssetsItemJSON);
794     langDependentAssetsJSON[langAndLDA.first] = langDependentAssetsItemJSON;
795   }
796   oJsonObject[SUIT_ActionAssets::PROP_ID_LANG_DEPENDENT_ASSETS] = langDependentAssetsJSON;
797
798   oJsonObject[SUIT_ActionAssets::PROP_ID_ICON_PATH] = myIconPath;
799 }
800
801 QString SUIT_ActionAssets::toString() const
802 {
803   QJsonObject jsonObject;
804   toJSON(jsonObject);
805   return QString::fromStdString(QJsonDocument(jsonObject).toJson(QJsonDocument::Indented).toStdString());
806 }
807
808 QStringList SUIT_ActionAssets::getLangs() const
809 {
810   QStringList langs;
811
812   for (const auto& langAndAssets : myLangDependentAssets) {
813     langs.push_back(langAndAssets.first);
814   }
815
816   return langs;
817 }
818
819 void SUIT_ActionAssets::clearAllLangsExcept(const QString& theLang)
820 {
821   for (auto it = myLangDependentAssets.begin(); it != myLangDependentAssets.end();) {
822     if (it->first == theLang)
823       it++;
824     else
825       it = myLangDependentAssets.erase(it);
826   }
827 }
828
829 void SUIT_ActionAssets::merge(const SUIT_ActionAssets& theOther, bool theOverride)
830 {
831   for (const auto& otherLangAndLDA : theOther.myLangDependentAssets) {
832     const QString& lang = otherLangAndLDA.first;
833     const auto& otherLDA = otherLangAndLDA.second;
834     auto& thisLDA = myLangDependentAssets[lang];
835
836     if (thisLDA.myName.isEmpty() || theOverride && !otherLDA.myName.isEmpty())
837       thisLDA.myName = otherLDA.myName;
838
839     if (thisLDA.myToolTip.isEmpty() || theOverride && !otherLDA.myToolTip.isEmpty())
840       thisLDA.myToolTip = otherLDA.myToolTip;
841   }
842
843   if (theOverride)
844     myIconPath = theOther.myIconPath;
845 }
846
847 std::map<QString, std::map<QString, QKeySequence>> SUIT_ShortcutContainer::merge(
848   const SUIT_ShortcutContainer& theOther,
849   bool theOverride,
850   bool theTreatAbsentIncomingAsDisabled
851 ) {
852   std::map<QString, std::map<QString, QKeySequence>> changesOfThis;
853
854   for (const auto& shortcutsInversedOfOtherPair : theOther.myShortcutsInversed) {
855     const QString& moduleIDOther = shortcutsInversedOfOtherPair.first;
856     const auto& shortcutsInversedOther = shortcutsInversedOfOtherPair.second;
857     for (const auto& shortcutInversedOther : shortcutsInversedOther) {
858       const QString& inModuleActionIDOther = shortcutInversedOther.first;
859       const QKeySequence& keySequenceOther = shortcutInversedOther.second;
860       if (theOverride) {
861         if (hasShortcut(moduleIDOther, inModuleActionIDOther) && getKeySequence(moduleIDOther, inModuleActionIDOther) == keySequenceOther) {
862           continue;
863         }
864         else /* if this has no shortcut for the action  or  if this has a shortcut for the action, but the key sequence differs. */ {
865           const auto disabledActionsOfThis = setShortcut(moduleIDOther, inModuleActionIDOther, keySequenceOther, true);
866           changesOfThis[moduleIDOther][inModuleActionIDOther] = keySequenceOther;
867           for (const auto& disabledActionOfThis : disabledActionsOfThis) {
868             changesOfThis[disabledActionOfThis.first][disabledActionOfThis.second] = NO_KEYSEQUENCE;
869           }
870         }
871       }
872       else /* if (!theOverride) */ {
873         if (hasShortcut(moduleIDOther, inModuleActionIDOther))
874           continue;
875         else {
876           const auto conflictingActionsOfThis = setShortcut(moduleIDOther, inModuleActionIDOther, keySequenceOther, false);
877           if (conflictingActionsOfThis.empty()) {
878             changesOfThis[moduleIDOther][inModuleActionIDOther] = keySequenceOther;
879           }
880           else /* if this has no shortcut for the action, but the incoming key sequence conflicts with others shortcuts. */ {
881             changesOfThis[moduleIDOther][inModuleActionIDOther] = NO_KEYSEQUENCE;
882           }
883         }
884       }
885     }
886   }
887
888   if (theOverride && theTreatAbsentIncomingAsDisabled) {
889     // Disable existing shortcuts, if they are absent in theOther.
890     for (auto& shortcutsInversedPair : myShortcutsInversed) {
891       const QString& moduleID = shortcutsInversedPair.first;
892       auto& moduleShortcutsInversed = shortcutsInversedPair.second;
893       for (auto& inversedShortcut : moduleShortcutsInversed) {
894         if (theOther.hasShortcut(moduleID, inversedShortcut.first))
895           continue;
896
897         if (inversedShortcut.second.isEmpty())
898           continue; // Existing shortcut is already disabled.
899
900         auto itShortcutsPair = myShortcuts.find(moduleID);
901         if (itShortcutsPair == myShortcuts.end())
902           continue; // The check is an overhead in an error-free designed class, but let be just in case.
903
904         auto& moduleShortcuts = itShortcutsPair->second;
905         moduleShortcuts.erase(inversedShortcut.second);
906         inversedShortcut.second = NO_KEYSEQUENCE;
907         changesOfThis[moduleID][inversedShortcut.first] = NO_KEYSEQUENCE;
908       }
909     }
910   }
911
912   return changesOfThis;
913 }
914
915
916 SUIT_ShortcutMgr* SUIT_ShortcutMgr::myShortcutMgr = nullptr;
917
918 SUIT_ShortcutMgr::SUIT_ShortcutMgr()
919 : QObject()
920 {
921   qApp->installEventFilter( this );
922 }
923
924 SUIT_ShortcutMgr::~SUIT_ShortcutMgr()
925 {
926   qApp->removeEventFilter( this );
927 }
928
929 /*static*/ void SUIT_ShortcutMgr::Init()
930 {
931   if( myShortcutMgr == nullptr) {
932     myShortcutMgr = new SUIT_ShortcutMgr();
933     myShortcutMgr->setShortcutsFromPreferences();
934   }
935 }
936
937 /*static*/ SUIT_ShortcutMgr* SUIT_ShortcutMgr::get()
938 {
939   Init();
940   return myShortcutMgr;
941 }
942
943 /*static*/ bool SUIT_ShortcutMgr::isKeySequenceValid(const QKeySequence& theKeySequence)
944 {
945   // TODO Perform check whether a key sequence is platform-compatible.
946   return true;
947 }
948
949 /*static*/ std::pair<bool, QKeySequence> SUIT_ShortcutMgr::toKeySequenceIfValid(const QString& theKeySequenceString)
950 {
951   auto res = std::pair<bool, QKeySequence>(false, QKeySequence());
952
953   try {
954     res.second = QKeySequence::fromString(theKeySequenceString);
955     if (res.second.toString() != theKeySequenceString)
956       return std::pair<bool, QKeySequence>(false, QKeySequence());
957
958     if (!SUIT_ShortcutMgr::isKeySequenceValid(res.second))
959       return std::pair<bool, QKeySequence>(false, QKeySequence());
960   }
961   catch (...) {
962     return std::pair<bool, QKeySequence>(false, QKeySequence());
963   }
964
965   res.first = true;
966   return res;
967 }
968
969 /*static*/ bool SUIT_ShortcutMgr::isModuleIDValid(const QString& theModuleID)
970 {
971   if (theModuleID.contains(TOKEN_SEPARATOR))
972     return false;
973
974   if (theModuleID.simplified() != theModuleID)
975     return false;
976
977   return true;
978 }
979
980 /*static*/ bool SUIT_ShortcutMgr::isInModuleActionIDValid(const QString& theInModuleActionID)
981 {
982   QStringList tokens = theInModuleActionID.split(TOKEN_SEPARATOR);
983    for (QStringList::size_type i = 0; i < tokens.length(); i++) {
984     const QString simplifiedToken = tokens[i].simplified();
985     if (
986       simplifiedToken.isEmpty() ||
987       simplifiedToken != tokens[i] ||
988       i == 0 && simplifiedToken == META_ACTION_PREFIX ||
989       i != 0 && simplifiedToken.startsWith(META_ACTION_PREFIX)
990     )
991       return false;
992   }
993   return true;
994 }
995
996 /*static*/ bool SUIT_ShortcutMgr::isInModuleMetaActionID(const QString& theInModuleActionID)
997 {
998   return theInModuleActionID.startsWith(META_ACTION_PREFIX);
999 }
1000
1001 /*static*/ std::pair<QString, QString> SUIT_ShortcutMgr::splitIntoModuleIDAndInModuleID(const QString& theActionID)
1002 {
1003   QStringList tokens = theActionID.split(TOKEN_SEPARATOR);
1004   if (tokens.length() < 2)
1005     return std::pair<QString, QString>();
1006
1007   auto res = std::pair<QString, QString>();
1008
1009   if (tokens[0].simplified() != tokens[0])
1010     return std::pair<QString, QString>();
1011
1012   res.first = tokens[0];
1013   tokens.pop_front();
1014
1015   for (QStringList::size_type i = 0; i < tokens.length(); i++) {
1016     const QString simplifiedToken = tokens[i].simplified();
1017     if (
1018       simplifiedToken.isEmpty() ||
1019       simplifiedToken != tokens[i] ||
1020       i == 0 && simplifiedToken == META_ACTION_PREFIX ||
1021       i != 0 && simplifiedToken.startsWith(META_ACTION_PREFIX)
1022     )
1023       return std::pair<QString, QString>();
1024   }
1025   res.second = tokens.join(TOKEN_SEPARATOR);
1026
1027   return res;
1028 }
1029
1030 /*static*/ bool SUIT_ShortcutMgr::isActionIDValid(const QString& theActionID)
1031 {
1032   return !SUIT_ShortcutMgr::splitIntoModuleIDAndInModuleID(theActionID).second.isEmpty();
1033 }
1034
1035 /*static*/ QString SUIT_ShortcutMgr::makeActionID(const QString& theModuleID, const QString& theInModuleActionID)
1036 {
1037   if (!SUIT_ShortcutMgr::isModuleIDValid(theModuleID))
1038     return QString();
1039
1040   if (!isInModuleActionIDValid(theInModuleActionID))
1041     return QString();
1042
1043   return theModuleID + TOKEN_SEPARATOR + theInModuleActionID;
1044 }
1045
1046 /*static*/ void SUIT_ShortcutMgr::fillContainerFromPreferences(SUIT_ShortcutContainer& theContainer, bool theDefaultOnly)
1047 {
1048   ShCutDbg() && ShCutDbg("Retrieving preferences from resources.");
1049
1050   SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr();
1051   if (!resMgr) {
1052     Warning("SUIT_ShortcutMgr can't retrieve resource manager!");
1053     return;
1054   }
1055
1056   const auto resMgrWorkingModeBefore = resMgr->workingMode();
1057   if (theDefaultOnly)
1058     resMgr->setWorkingMode(QtxResourceMgr::IgnoreUserValues);
1059
1060   /** List of modules with invalid IDs. */
1061   QStringList invalidModuleIDs;
1062
1063   /** { moduleID, {inModuleActionID, keySequence}[] }[] */
1064   std::map<QString, std::list<std::pair<QString, QString>>> invalidShortcuts;
1065
1066   /**
1067    * Shortcuts, which have not been set, because they are in conflict with previously parsed shortcuts.
1068    * { moduleID, {inModuleActionID, keySequence}[] }[] */
1069   std::map<QString, std::list<std::pair<QString, QKeySequence>>> conflicts;
1070
1071   // Resource manager strips leading and trailing whitespaces from subsections' names.
1072   // And then it is not able to retrieve parametes from that subsections,
1073   // because parsed subsection names differ from the ones in resource file.
1074   // Anyway, it does not affect operability of ShortcutMgr.
1075   QStringList moduleIDs = resMgr->subSections(SECTION_NAME_PREFIX, true);
1076   if (ShCutDbg()) {
1077     if (moduleIDs.isEmpty())
1078       ShCutDbg("No discovered shortcut modules.");
1079     else
1080       ShCutDbg("Discovered shortcut modules: \"" + moduleIDs.join("\", \"") + ".");
1081   }
1082   moduleIDs.push_front(SUIT_ShortcutMgr::ROOT_MODULE_ID); // Resource manager filters out empty section suffices.
1083   moduleIDs.removeDuplicates();
1084
1085   for (size_t i = 0; i < moduleIDs.size(); i++) {
1086     const auto& moduleID = moduleIDs[i];
1087     if (!SUIT_ShortcutMgr::isModuleIDValid(moduleID)) {
1088       invalidModuleIDs.push_back(moduleID);
1089       continue;
1090     }
1091
1092     const QString sectionName = SECTION_NAME_PREFIX + resMgr->sectionsToken() + moduleID;
1093     QStringList moduleActionIDs = resMgr->parameters(sectionName);
1094
1095     for(const QString& inModuleActionID : moduleActionIDs) {
1096       QString keySequenceString = QString("");
1097       resMgr->value(sectionName, inModuleActionID, keySequenceString);
1098       const auto keySequence = SUIT_ShortcutMgr::toKeySequenceIfValid(keySequenceString);
1099
1100       ShCutDbg() && ShCutDbg("Shortcut discovered: \"" + moduleID + "\"\t\"" + inModuleActionID + "\"\t\"" + keySequenceString + "\".");
1101
1102       if (
1103         !SUIT_ShortcutMgr::isInModuleActionIDValid(inModuleActionID) ||
1104         !keySequence.first ||
1105         SUIT_ShortcutMgr::isInModuleMetaActionID(inModuleActionID) && moduleID != SUIT_ShortcutMgr::ROOT_MODULE_ID
1106       ) {
1107         std::list<std::pair<QString, QString>>& moduleInvalidShortcuts = invalidShortcuts[moduleID];
1108         moduleInvalidShortcuts.push_back(std::pair<QString, QString>(inModuleActionID, keySequenceString));
1109         continue;
1110       }
1111
1112       const auto shortcutConflicts = theContainer.setShortcut(moduleID, inModuleActionID, keySequence.second, false /*override*/);
1113       if (!shortcutConflicts.empty()) {
1114         auto& moduleConflicts = conflicts[moduleID];
1115         moduleConflicts.push_back(std::pair<QString, QKeySequence>(inModuleActionID, keySequence.second));
1116       }
1117     }
1118   }
1119
1120   if (!invalidModuleIDs.isEmpty() || !invalidShortcuts.empty() || !conflicts.empty())
1121   { // Prepare report and show warning.
1122     QString report;
1123     if (!invalidModuleIDs.isEmpty()) {
1124       report += tr("Invalid module IDs") + ":";
1125       for (const QString& invalidModuleID : invalidModuleIDs) {
1126         report += "\n\t\"" + invalidModuleID + "\"" ;
1127       }
1128     }
1129
1130     if (!invalidShortcuts.empty()) {
1131       if (!report.isEmpty())
1132         report += "\n\n";
1133
1134       report += tr("Invalid shortcuts") + ":";
1135       for (const auto& moduleAndShortcuts : invalidShortcuts) {
1136         report += "\n\t\"" + moduleAndShortcuts.first + "\"";
1137         const std::list<std::pair<QString, QString>>& moduleShortcuts = moduleAndShortcuts.second;
1138         for (const auto& shortcut : moduleShortcuts) {
1139           report += "\n\t\t\"" + shortcut.first + "\"\t\"" + shortcut.second + "\"";
1140         }
1141       }
1142     }
1143
1144     if (!conflicts.empty()) {
1145       if (!report.isEmpty())
1146         report += "\n\n";
1147
1148       report += tr("These shortcuts have not been set to theContainer, because they conflict with previously parsed ones") + ":";
1149       for (const auto& moduleAndShortcuts : conflicts) {
1150         report += "\n\t\"" + moduleAndShortcuts.first + "\"";
1151
1152         const std::list<std::pair<QString, QKeySequence>>& moduleShortcuts = moduleAndShortcuts.second;
1153         for (const auto& shortcut : moduleShortcuts) {
1154           report += "\n\t\t\"" + shortcut.first + "\"\t\"" + shortcut.second.toString() + "\"";
1155         }
1156       }
1157     }
1158
1159     report += "\n.";
1160
1161     const auto text = tr("Invalid shortcuts in preferences");
1162     const auto informativeText = tr("Fix the following entries in the preference files manually");
1163     if (!theDefaultOnly) {
1164       // If user preferences are accounted, show warning in UI.
1165       SUIT_Application* app = SUIT_Session::session()->activeApplication();
1166       if (app && app->desktop()) {
1167         // Is not compiled without cast or with static_cast<QWidget*>.
1168         QMessageBox msgBox((QWidget*)app->desktop());
1169         msgBox.setIcon(QMessageBox::Warning);
1170         msgBox.setTextFormat(Qt::RichText);
1171         msgBox.setText("<b>" + text + "</b>");
1172         msgBox.setInformativeText(informativeText + ":");
1173         msgBox.setWindowFlags(Qt::WindowType::Popup);
1174         msgBox.setSizePolicy(QSizePolicy::Expanding,QSizePolicy::Expanding);
1175         msgBox.setDetailedText(report);
1176         msgBox.setStandardButtons(QMessageBox::Ok);
1177         msgBox.setDefaultButton(QMessageBox::Ok);
1178         msgBox.setMinimumWidth(600);
1179         msgBox.exec();
1180       }
1181     }
1182     Warning(text + ". " + informativeText + ":\n" + report);
1183   }
1184
1185   if (theDefaultOnly)
1186     resMgr->setWorkingMode(resMgrWorkingModeBefore);
1187
1188   ShCutDbg() && ShCutDbg("theContainer holds following shortcuts:\n" + theContainer.toString());
1189 }
1190
1191 QString substituteBashVars(const QString& theString)
1192 {
1193   QString res = theString;
1194   const auto env = QProcessEnvironment::systemEnvironment();
1195   int pos = 0;
1196   QRegExp rx("\\$\\{([^\\}]+)\\}"); // Match substrings enclosed by "${" and "}".
1197   rx.setMinimal(true); // Set search to non-greedy.
1198   while((pos = rx.indexIn(res, pos)) != -1) {
1199     QString capture = rx.cap(1);
1200     QString subst = env.value(capture);
1201     ShCutDbg("capture = " + capture);
1202     ShCutDbg("subst   = " + subst);
1203     res.replace("${" + capture + "}", subst);
1204     pos += rx.matchedLength();
1205   }
1206   return res;
1207 }
1208
1209 QString substitutePowerShellVars(const QString& theString)
1210 {
1211   QString res = theString;
1212   int pos = 0;
1213   QRegExp rx("%([^%]+)%"); // Match substrings enclosed by "%".
1214   rx.setMinimal(true); // Set search to non-greedy.
1215   while((pos = rx.indexIn(res, pos)) != -1) {
1216     QString capture = rx.cap(1);
1217     QString subst = Qtx::getenv(capture.toUtf8().constData());
1218     ShCutDbg("capture = " + capture);
1219     ShCutDbg("subst   = " + subst);
1220     res.replace("%" + capture + "%", subst);
1221     pos += rx.matchedLength();
1222   }
1223   return res;
1224 }
1225
1226 QString substituteVars(const QString& theString)
1227 {
1228   QString str = substituteBashVars(theString);
1229   return substitutePowerShellVars(str);
1230 }
1231
1232 /*static*/ std::pair<bool, SUIT_ActionAssets> SUIT_ShortcutMgr::getActionAssetsFromResources(const QString& theActionID)
1233 {
1234   auto res = std::pair<bool, SUIT_ActionAssets>(false, SUIT_ActionAssets());
1235
1236   SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr();
1237   if (!resMgr) {
1238     Warning("SUIT_ShortcutMgr can't retrieve resource manager!");
1239     return res;
1240   }
1241
1242   QStringList actionAssetFilePaths = resMgr->parameters(SECTION_NAME_ACTION_ASSET_FILE_PATHS);
1243   for (const QString& actionAssetFilePath : actionAssetFilePaths) {
1244     const QString path = substituteVars(actionAssetFilePath);
1245     QFile actionAssetFile(path);
1246     if (!actionAssetFile.open(QIODevice::ReadOnly)) {
1247       Warning("SUIT_ShortcutMgr can't open action asset file \"" + path + "\"!");
1248       continue;
1249     }
1250
1251     QJsonParseError jsonError;
1252     QJsonDocument document = QJsonDocument::fromJson(actionAssetFile.readAll(), &jsonError);
1253     actionAssetFile.close();
1254     if(jsonError.error != QJsonParseError::NoError) {
1255       Warning("SUIT_ShortcutMgr: error during parsing of action asset file \"" + path + "\"!");
1256       continue;
1257     }
1258
1259     if(!document.isObject()) {
1260       Warning("SUIT_ShortcutMgr: empty action asset file \"" + path + "\"!");
1261       continue;
1262     }
1263
1264     QJsonObject object = document.object();
1265     if (object.keys().indexOf(theActionID) == -1)
1266       continue;
1267
1268     SUIT_ActionAssets actionAssets;
1269     if (!actionAssets.fromJSON(object[theActionID].toObject())) {
1270       ShCutDbg("Action asset file \"" + path + "\" contains invalid action assets with ID \"" + theActionID + "\".");
1271       continue;
1272     }
1273
1274     res.second.merge(actionAssets, true);
1275   }
1276
1277   res.first = true;
1278   return res;
1279 }
1280
1281
1282 /*static*/ QString SUIT_ShortcutMgr::getLang()
1283 {
1284   SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr();
1285   if (!resMgr) {
1286     Warning("SUIT_ShortcutMgr can't retrieve resource manager!");
1287     return DEFAULT_LANG;
1288   }
1289
1290   return resMgr->stringValue(LANG_SECTION, LANG_SECTION, DEFAULT_LANG);
1291 }
1292
1293
1294 void SUIT_ShortcutMgr::registerAction(const QString& theActionID, QAction* theAction)
1295 {
1296   const auto moduleIDAndActionID = splitIntoModuleIDAndInModuleID(theActionID);
1297   const QString& moduleID = moduleIDAndActionID.first;
1298   const QString& inModuleActionID = moduleIDAndActionID.second;
1299
1300   if (inModuleActionID.isEmpty()) {
1301     ShCutDbg() && ShCutDbg("Attempt to register an action \"" + theAction->toolTip() + "\" with invalid ID \"" + theActionID + "\".");
1302     if (theAction->shortcut() != NO_KEYSEQUENCE)
1303       theAction->setShortcut(NO_KEYSEQUENCE);
1304
1305     return;
1306   }
1307
1308   { // If an action with the same memory address was registered earlier,
1309     // clear all data about it to start registering procedure from scratch.
1310     auto itPreviousModuleAndActionID = myActionIDs.find(theAction);
1311     if (itPreviousModuleAndActionID != myActionIDs.end()) {
1312       // Clear the data from myActions.
1313       const auto& previousModuleAndActionID = itPreviousModuleAndActionID->second;
1314       auto itActions = myActions.find(previousModuleAndActionID.first);
1315       if (itActions != myActions.end()) {
1316         std::map<QString, std::set<QAction*>>& moduleActions = itActions->second;
1317         auto itModuleActions = moduleActions.find(previousModuleAndActionID.second);
1318         if (itModuleActions != moduleActions.end()) {
1319           std::set<QAction*>& registeredActions = itModuleActions->second;
1320           registeredActions.erase(theAction);
1321         }
1322       }
1323
1324       myActionIDs.erase(itPreviousModuleAndActionID);
1325     }
1326   }
1327
1328   auto itActions = myActions.find(moduleID);
1329   if (itActions == myActions.end()) {
1330     itActions = myActions.emplace(moduleID, std::map<QString, std::set<QAction*>>()).first;
1331   }
1332
1333   std::map<QString, std::set<QAction*>>& moduleActions = itActions->second;
1334   auto itModuleActions = moduleActions.find(inModuleActionID);
1335   if (itModuleActions != moduleActions.end()) {
1336     std::set<QAction*>& registeredActions = itModuleActions->second;
1337     const bool actionIsNew = registeredActions.emplace(theAction).second;
1338     if (actionIsNew)
1339       myActionIDs[theAction] = moduleIDAndActionID;
1340   }
1341   else {
1342     std::set<QAction*>& registeredActions = moduleActions[inModuleActionID];
1343     registeredActions.emplace(theAction);
1344     myActionIDs[theAction] = moduleIDAndActionID;
1345   }
1346
1347   connect(theAction, SIGNAL(destroyed(QObject*)), this, SLOT (onActionDestroyed(QObject*)));
1348
1349   if (myShortcutContainer.hasShortcut(moduleID, inModuleActionID)) {
1350     const QKeySequence& keySequence = getKeySequence(moduleID, inModuleActionID);
1351     theAction->setShortcut(keySequence);
1352   }
1353   else {
1354     ShCutDbg(
1355       "Action with ID \"" +
1356       (SUIT_ShortcutMgr::isInModuleMetaActionID(inModuleActionID) ? SUIT_ShortcutMgr::ROOT_MODULE_ID + TOKEN_SEPARATOR + inModuleActionID : theActionID) +
1357       "\" is not added to default resource files."
1358     );
1359     auto conflicts = myShortcutContainer.setShortcut(moduleID, inModuleActionID, theAction->shortcut(), false);
1360     if (!conflicts.empty())
1361       theAction->setShortcut(NO_KEYSEQUENCE); // Unbind any key sequence, if it was bound outside of the class and interferes with other shortcuts.
1362   }
1363 }
1364
1365 void SUIT_ShortcutMgr::registerAction(QtxAction* theAction)
1366 {
1367   registerAction(theAction->ID(), theAction);
1368 }
1369
1370 std::set<QAction*> SUIT_ShortcutMgr::getActions(const QString& theModuleID, const QString& theInModuleActionID) const
1371 {
1372   if (SUIT_ShortcutMgr::isInModuleMetaActionID(theInModuleActionID)) {
1373     std::set<QAction*> actions;
1374     for (const auto& actionAndID : myActionIDs) {
1375       if (actionAndID.second.second == theInModuleActionID)
1376         actions.emplace(actionAndID.first);
1377     }
1378     return actions;
1379   }
1380   else {
1381     const auto itActions = myActions.find(theModuleID);
1382     if (itActions == myActions.end())
1383       return std::set<QAction*>();
1384
1385     const std::map<QString, std::set<QAction*>>& moduleActions = itActions->second;
1386     const auto itModuleActions = moduleActions.find(theInModuleActionID);
1387     if (itModuleActions == moduleActions.end())
1388       return std::set<QAction*>();
1389
1390     return itModuleActions->second;
1391   }
1392 }
1393
1394 std::pair<QString, QString> SUIT_ShortcutMgr::getModuleIDAndInModuleID(const QAction* theAction) const {
1395   const auto it = myActionIDs.find(const_cast<QAction*>(theAction));
1396   if (it == myActionIDs.end())
1397     return std::pair<QString, QString>();
1398
1399   return it->second;
1400 }
1401
1402 bool SUIT_ShortcutMgr::hasAction(const QAction* theAction) const
1403 {
1404   return myActionIDs.find(const_cast<QAction*>(theAction)) != myActionIDs.end();
1405 }
1406
1407 QString SUIT_ShortcutMgr::getActionID(const QAction* theAction) const
1408 {
1409   const auto it = myActionIDs.find(const_cast<QAction*>(theAction));
1410   if (it == myActionIDs.end())
1411     return QString();
1412
1413   return SUIT_ShortcutMgr::makeActionID(it->second.first, it->second.second);
1414 }
1415
1416 void SUIT_ShortcutMgr::setActionsOfModuleEnabled(const QString& theModuleID, const bool theEnable) const
1417 {
1418   const auto itModuleActions = myActions.find(theModuleID);
1419   if (itModuleActions == myActions.end())
1420     return;
1421
1422   SUIT_Application* app = SUIT_Session::session()->activeApplication();
1423   if (!app)
1424     return;
1425
1426   const std::map<QString, std::set<QAction*>>& moduleActions = itModuleActions->second;
1427   for (const auto& idAndActions : moduleActions) {
1428     const std::set<QAction*>& actions = idAndActions.second;
1429     for (QAction* const action : actions) {
1430       if (action->parentWidget() == (QWidget*)app->desktop()) // Is not compiled without cast or with static_cast<QWidget*>.
1431         action->setEnabled(theEnable);
1432     }
1433   }
1434 }
1435
1436 void SUIT_ShortcutMgr::setActionsWithPrefixInIDEnabled(const QString& theInModuleActionIDPrefix, bool theEnable) const
1437 {
1438   SUIT_Application* app = SUIT_Session::session()->activeApplication();
1439   if (!app)
1440     return;
1441
1442   for (const std::pair<QAction*, std::pair<QString, QString>>& actionAndID : myActionIDs) {
1443     QAction* const action = actionAndID.first;
1444     // Is not compiled without cast or with static_cast<QWidget*>.
1445     if (action->parentWidget() == (QWidget*)app->desktop()) {
1446       const QString& inModuleActionID = actionAndID.second.second;
1447       if (inModuleActionID.startsWith(theInModuleActionIDPrefix))
1448         action->setEnabled(theEnable);
1449     }
1450   }
1451 }
1452
1453 void SUIT_ShortcutMgr::setSectionEnabled(const QString& theInModuleActionIDPrefix, bool theEnable) const
1454 {
1455   setActionsWithPrefixInIDEnabled(theInModuleActionIDPrefix, theEnable);
1456 }
1457
1458 void SUIT_ShortcutMgr::rebindActionsToKeySequences() const
1459 {
1460   ShCutDbg() && ShCutDbg("SUIT_ShortcutMgr::rebindActionsToKeySequences()");
1461   for (const std::pair<QAction*, std::pair<QString, QString>>& actionAndID : myActionIDs) {
1462     actionAndID.first->setShortcut(getKeySequence(actionAndID.second.first, actionAndID.second.second));
1463   }
1464 }
1465
1466 void SUIT_ShortcutMgr::updateShortcuts() const
1467 {
1468   rebindActionsToKeySequences();
1469 }
1470
1471 std::set<std::pair<QString, QString>> SUIT_ShortcutMgr::setShortcut(const QString& theActionID, const QKeySequence& theKeySequence, bool theOverride)
1472 {
1473   const auto moduleIDAndActionID = splitIntoModuleIDAndInModuleID(theActionID);
1474   const QString& moduleID = moduleIDAndActionID.first;
1475   const QString& inModuleActionID = moduleIDAndActionID.second;
1476
1477   if (inModuleActionID.isEmpty()) {
1478     ShCutDbg() && ShCutDbg("Attempt to set shortcut with invalid action ID \"" + theActionID + "\".");
1479     return std::set<std::pair<QString, QString>>();
1480   }
1481
1482   return setShortcutNoIDChecks(moduleID, inModuleActionID, theKeySequence, theOverride);
1483 }
1484
1485 std::set<std::pair<QString, QString>> SUIT_ShortcutMgr::setShortcut(const QString& theModuleID, const QString& theInModuleActionID, const QKeySequence& theKeySequence, bool theOverride)
1486 {
1487   if (!SUIT_ShortcutMgr::isModuleIDValid(theModuleID)) {
1488     ShCutDbg() && ShCutDbg("Attempt to set shortcut with invalid module ID \"" + theModuleID + "\".");
1489     return std::set<std::pair<QString, QString>>();
1490   }
1491
1492   if (!SUIT_ShortcutMgr::isInModuleActionIDValid(theInModuleActionID)) {
1493     ShCutDbg() && ShCutDbg("Attempt to set shortcut with invalid in-module action ID \"" + theInModuleActionID + "\".");
1494     return std::set<std::pair<QString, QString>>();
1495   }
1496
1497   return setShortcutNoIDChecks(theModuleID, theInModuleActionID, theKeySequence, theOverride);
1498 }
1499
1500 const SUIT_ShortcutContainer& SUIT_ShortcutMgr::getShortcutContainer() const
1501 {
1502   return myShortcutContainer;
1503 }
1504
1505 void SUIT_ShortcutMgr::mergeShortcutContainer(const SUIT_ShortcutContainer& theContainer, bool theOverride, bool theTreatAbsentIncomingAsDisabled)
1506 {
1507   ShCutDbg() && ShCutDbg("ShortcutMgr merges shortcut container...");
1508   const auto changes = myShortcutContainer.merge(theContainer, theOverride, theTreatAbsentIncomingAsDisabled);
1509   ShCutDbg() && ShCutDbg("ShortcutMgr keeps following shortcuts:\n" + myShortcutContainer.toString());
1510
1511   // Turn off hotkeys for disabled shortcuts.
1512   for (const auto& moduleIDAndChanges : changes) {
1513     const QString& moduleID = moduleIDAndChanges.first;
1514     const auto& moduleChanges = moduleIDAndChanges.second;
1515     for (const std::pair<QString, QKeySequence>& modifiedShortcut : moduleChanges) {
1516       if (modifiedShortcut.second == NO_KEYSEQUENCE) {
1517         const std::set<QAction*> actions = getActions(moduleID, modifiedShortcut.first);
1518         for (QAction* const action : actions) {
1519           action->setShortcut(NO_KEYSEQUENCE);
1520         }
1521       }
1522     }
1523   }
1524
1525   // Turn on hotkeys for enabled shortcuts.
1526   for (const auto& moduleIDAndChanges : changes) {
1527     const QString& moduleID = moduleIDAndChanges.first;
1528     const auto& moduleChanges = moduleIDAndChanges.second;
1529     for (const std::pair<QString, QKeySequence>& modifiedShortcut : moduleChanges) {
1530       if (modifiedShortcut.second != NO_KEYSEQUENCE) {
1531         const std::set<QAction*> actions = getActions(moduleID, modifiedShortcut.first);
1532         for (QAction* const action : actions) {
1533           action->setShortcut(modifiedShortcut.second);
1534         }
1535       }
1536     }
1537   }
1538
1539   SUIT_ShortcutMgr::saveShortcutsToPreferences(changes);
1540 }
1541
1542 QKeySequence SUIT_ShortcutMgr::getKeySequence(const QString& theModuleID, const QString& theInModuleActionID) const
1543 {
1544   return myShortcutContainer.getKeySequence(theModuleID, theInModuleActionID);
1545 }
1546
1547 const std::map<QString, QKeySequence>& SUIT_ShortcutMgr::getModuleShortcutsInversed(const QString& theModuleID) const
1548 {
1549   return myShortcutContainer.getModuleShortcutsInversed(theModuleID);
1550 }
1551
1552 std::set<QString> SUIT_ShortcutMgr::getShortcutModuleIDs() const
1553 {
1554   return myShortcutContainer.getIDsOfAllModules();
1555 }
1556
1557 std::set<QString> SUIT_ShortcutMgr::getIDsOfInterferingModules(const QString& theModuleID) const
1558 {
1559   return myShortcutContainer.getIDsOfInterferingModules(theModuleID);
1560 }
1561
1562 std::shared_ptr<const SUIT_ActionAssets> SUIT_ShortcutMgr::getModuleAssets(const QString& theModuleID) const
1563 {
1564   const auto itModuleAssets = myModuleAssets.find(theModuleID);
1565   if (itModuleAssets == myModuleAssets.end()) {
1566     auto assets = std::shared_ptr<SUIT_ActionAssets>(new SUIT_ActionAssets());
1567     auto lda = SUIT_ActionAssets::LangDependentAssets();
1568     lda.myName = theModuleID; // At least something meaningful.
1569
1570     assets->myLangDependentAssets.emplace(SUIT_ShortcutMgr::getLang(), lda);
1571     return assets;
1572   }
1573   return itModuleAssets->second;
1574 }
1575
1576 QString SUIT_ShortcutMgr::getModuleName(const QString& theModuleID, const QString& theLang) const
1577 {
1578   const auto assets = getModuleAssets(theModuleID);
1579   const auto& ldaMap = assets->myLangDependentAssets;
1580   if (ldaMap.empty())
1581     return theModuleID;
1582
1583   auto itLang = ldaMap.find(theLang.isEmpty() ? SUIT_ShortcutMgr::getLang() : theLang);
1584   if (itLang == ldaMap.end())
1585     itLang = ldaMap.begin(); // Get name in any language.
1586
1587   const auto& name = itLang->second.myName;
1588   return name.isEmpty() ? theModuleID : name;
1589 }
1590
1591 std::shared_ptr<const SUIT_ActionAssets> SUIT_ShortcutMgr::getActionAssets(const QString& theModuleID, const QString& theInModuleActionID) const
1592 {
1593   const QString actionID = SUIT_ShortcutMgr::makeActionID(theModuleID, theInModuleActionID);
1594   if (actionID.isEmpty()) {
1595     ShCutDbg() && ShCutDbg("Can't get action assets: either/both module ID \"" + theModuleID + "\" or/and in-module action ID \"" + theInModuleActionID + "\" is/are invalid.");
1596     return std::shared_ptr<const SUIT_ActionAssets>(nullptr);
1597   }
1598   return getActionAssets(actionID);
1599 }
1600
1601 std::shared_ptr<const SUIT_ActionAssets> SUIT_ShortcutMgr::getActionAssets(const QString& theActionID) const
1602 {
1603   const auto moduleIDAndActionID = SUIT_ShortcutMgr::splitIntoModuleIDAndInModuleID(theActionID);
1604   const QString& moduleID = moduleIDAndActionID.first;
1605   const QString& inModuleActionID = moduleIDAndActionID.second;
1606
1607   if (inModuleActionID.isEmpty()) {
1608     ShCutDbg() && ShCutDbg("Attempt to get assets of an action with invalid ID \"" + theActionID + "\".");
1609     return std::shared_ptr<const SUIT_ActionAssets>(nullptr);
1610   }
1611
1612   const auto itModuleActionAssets = myActionAssets.find(moduleID);
1613   if (itModuleActionAssets == myActionAssets.end())
1614     return std::shared_ptr<const SUIT_ActionAssets>(nullptr);
1615   else {
1616     const auto moduleActionAssets = itModuleActionAssets->second;
1617     const auto itActionAssets = moduleActionAssets.find(inModuleActionID);
1618     if (itActionAssets == moduleActionAssets.end())
1619       return std::shared_ptr<const SUIT_ActionAssets>(nullptr);
1620     else
1621       return itActionAssets->second;
1622   }
1623 }
1624
1625 QString SUIT_ShortcutMgr::getActionName(const QString& theModuleID, const QString& theInModuleActionID, const QString& theLang) const
1626 {
1627   const QString actionID = SUIT_ShortcutMgr::makeActionID(theModuleID, theInModuleActionID);
1628   if (actionID.isEmpty()) {
1629     ShCutDbg() && ShCutDbg("Can't get action name: either/both module ID \"" + theModuleID + "\" or/and in-module action ID \"" + theInModuleActionID + "\" is/are invalid.");
1630     return actionID;
1631   }
1632
1633   const auto itModuleActionAssets = myActionAssets.find(theModuleID);
1634   if (itModuleActionAssets == myActionAssets.end())
1635     return actionID;
1636
1637   const auto moduleActionAssets = itModuleActionAssets->second;
1638   const auto itActionAssets = moduleActionAssets.find(theInModuleActionID);
1639   if (itActionAssets != moduleActionAssets.end() && !itActionAssets->second->myLangDependentAssets.empty()) {
1640     const auto& ldaMap = itActionAssets->second->myLangDependentAssets;
1641     if (ldaMap.empty())
1642       return theInModuleActionID;
1643
1644     auto itLang = ldaMap.find(theLang.isEmpty() ? SUIT_ShortcutMgr::getLang() : theLang);
1645     if (itLang == ldaMap.end())
1646       itLang = ldaMap.begin(); // Get name in any language.
1647
1648     const auto& name = itLang->second.myName;
1649     return name.isEmpty() ? theInModuleActionID : name;
1650   }
1651   else /* if action assets have not been loaded. */ {
1652     // Try to get action->text() and use it as a name.
1653
1654     // Pitfall of the approach: at the time this code block is called, the action may not exist.
1655     // Moreover, an action with such an ID may not even have been created at the time of calling this method.
1656     // Thus, even buffering of assets of every action ever created at runtime does not guarantee,
1657     // that the assets will be available at any point in the life of the application,
1658     // unless the assets are added to dedicated section in an asset file.
1659
1660     const auto actions = getActions(theModuleID, theInModuleActionID);
1661     for (const auto& action : actions) {
1662       if (!action->text().isEmpty())
1663         return action->text();
1664     }
1665     return theInModuleActionID;
1666   }
1667 }
1668
1669 void SUIT_ShortcutMgr::onActionDestroyed(QObject* theObject)
1670 {
1671   QAction* action = static_cast<QAction*>(theObject);
1672
1673   auto itID = myActionIDs.find(action);
1674   if (itID == myActionIDs.end())
1675     return;
1676
1677   const QString& moduleID = itID->second.first;
1678   const QString& inModuleActionID = itID->second.second;
1679
1680   auto itModuleActions = myActions.find(moduleID);
1681   if (itModuleActions != myActions.end()) {
1682     std::map<QString, std::set<QAction*>>& moduleActions = itModuleActions->second;
1683     auto itActions = moduleActions.find(inModuleActionID);
1684     if (itActions != moduleActions.end()) {
1685       std::set<QAction*>& actions = itActions->second;
1686       actions.erase(action);
1687     }
1688   }
1689
1690   myActionIDs.erase(itID);
1691 }
1692
1693 bool SUIT_ShortcutMgr::eventFilter(QObject* theObject, QEvent* theEvent)
1694 {
1695   if (theEvent) {
1696     if (theEvent->type() == QEvent::ActionAdded) {
1697       auto anActionEvent = static_cast<QActionEvent*>(theEvent);
1698
1699       QtxAction* aQtxAction = qobject_cast<QtxAction*>(anActionEvent->action());
1700       if (aQtxAction) {
1701 #ifdef SHORTCUT_MGR_DBG
1702         {
1703           const auto moduleIDAndActionID = splitIntoModuleIDAndInModuleID(aQtxAction->ID());
1704           if (moduleIDAndActionID.second.isEmpty())
1705             ShCutDbg("ActionAdded event, but ID of the action is invalid. Action name = \"" + aQtxAction->toolTip() + "\", ID = \"" + aQtxAction->ID() + "\".");
1706           else if (!myShortcutContainer.hasShortcut(moduleIDAndActionID.first, moduleIDAndActionID.second))
1707             ShCutDbg("ActionAdded event, but shortcut container has no shortcut for the action. It is ok, if preference files has not been parsed yet. Action ID = \"" + moduleIDAndActionID.second + "\".");
1708         }
1709 #endif//SHORTCUT_MGR_DBG
1710 #ifdef SHORTCUT_MGR_DEVTOOLS
1711         {
1712           DevTools::get()->collectShortcutAndAssets(aQtxAction);
1713           const auto moduleIDAndActionID = splitIntoModuleIDAndInModuleID(aQtxAction->ID());
1714           if (moduleIDAndActionID.second.isEmpty())
1715             DevTools::get()->collectAssetsOfActionWithInvalidID(aQtxAction);
1716         }
1717 #endif//SHORTCUT_MGR_DEVTOOLS
1718               registerAction(aQtxAction);
1719       }
1720       else {
1721         QAction* aQAction = qobject_cast<QAction*>(anActionEvent->action());
1722 #ifdef SHORTCUT_MGR_DEVTOOLS
1723         if (aQAction)
1724           DevTools::get()->collectAssetsOfActionWithInvalidID(aQAction);
1725 #endif//SHORTCUT_MGR_DEVTOOLS
1726         if (aQAction && aQAction->shortcut() != NO_KEYSEQUENCE) {
1727 #ifdef SHORTCUT_MGR_DBG
1728           ShCutDbg("ActionAdded event, but the added action is not QtxAction and bound to non-empty key sequence. name: \"" + aQAction->toolTip() + "\".");
1729 #endif//SHORTCUT_MGR_DBG
1730           // Since non-QtxAction has no ID, it is impossible to properly manage its shortcut.
1731           // And the shortcut may interfere with managed ones.
1732           aQAction->setShortcut(NO_KEYSEQUENCE);
1733         }
1734       }
1735     }
1736   }
1737
1738   return QObject::eventFilter(theObject, theEvent);
1739 }
1740
1741 std::set<std::pair<QString, QString>> SUIT_ShortcutMgr::setShortcutNoIDChecks(const QString& theModuleID, const QString& theInModuleActionID, const QKeySequence& theKeySequence, bool theOverride)
1742 {
1743   std::set<std::pair<QString, QString>> disabledShortcutsIDs = myShortcutContainer.setShortcut(theModuleID, theInModuleActionID, theKeySequence, theOverride);
1744
1745   if (theOverride || disabledShortcutsIDs.empty()) {
1746     // Bind actions to corresponding modified key sequences. Save changes to preferences.
1747
1748     /** { moduleID, {inModuleActionID, keySequence}[] }[] */
1749     std::map<QString, std::map<QString, QKeySequence>> modifiedShortcuts;
1750
1751     for (const auto& moduleIDAndActionID : disabledShortcutsIDs) {
1752       // Unbind actions of disabled shortcuts.
1753
1754       const QString& moduleID = moduleIDAndActionID.first;
1755       const QString& inModuleActionID = moduleIDAndActionID.second;
1756
1757       std::map<QString, QKeySequence>& modifiedModuleShortcuts = modifiedShortcuts[moduleID];
1758       modifiedModuleShortcuts[inModuleActionID] = NO_KEYSEQUENCE;
1759
1760       const std::set<QAction*> actions = getActions(moduleID, inModuleActionID);
1761       for (QAction* const action : actions) {
1762         action->setShortcut(NO_KEYSEQUENCE);
1763       }
1764     }
1765
1766     { // Bind actions to theKeySequence.
1767       std::map<QString, QKeySequence>& modifiedModuleShortcuts = modifiedShortcuts[theModuleID];
1768       modifiedModuleShortcuts[theInModuleActionID] = theKeySequence;
1769
1770       const std::set<QAction*> actions = getActions(theModuleID, theInModuleActionID);
1771       for (QAction* const action : actions) {
1772         action->setShortcut(theKeySequence);
1773       }
1774     }
1775
1776     SUIT_ShortcutMgr::saveShortcutsToPreferences(modifiedShortcuts);
1777   }
1778
1779   return disabledShortcutsIDs;
1780 }
1781
1782 void SUIT_ShortcutMgr::setShortcutsFromPreferences()
1783 {
1784   ShCutDbg() && ShCutDbg("ShortcutMgr is initializing...");
1785
1786   SUIT_ShortcutContainer container;
1787   SUIT_ShortcutMgr::fillContainerFromPreferences(container, false /*theDefaultOnly*/);
1788   mergeShortcutContainer(container, true /*theOverrde*/, false /*theTreatAbsentIncomingAsDisabled*/);
1789   setAssetsFromResources();
1790
1791   ShCutDbg() && ShCutDbg("ShortcutMgr has been initialized.");
1792 }
1793
1794 /*static*/ void SUIT_ShortcutMgr::saveShortcutsToPreferences(const std::map<QString, std::map<QString, QKeySequence>>& theShortcutsInversed)
1795 {
1796   ShCutDbg() && ShCutDbg("Saving preferences to resources.");
1797
1798   SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr();
1799   if (!resMgr) {
1800     Warning("SUIT_ShortcutMgr can't retrieve resource manager!");
1801     return;
1802   }
1803
1804   for (const auto& moduleIDAndShortcutsInversed : theShortcutsInversed) {
1805     const auto& moduleID = moduleIDAndShortcutsInversed.first;
1806     const auto& moduleShortcutsInversed = moduleIDAndShortcutsInversed.second;
1807     for (const auto& shortcutInversed : moduleShortcutsInversed) {
1808       if (shortcutInversed.first.isEmpty()) {
1809         ShCutDbg("Attempt to serialize a shortcut with empty action ID.");
1810         continue;
1811       }
1812
1813       const QString sectionName = SECTION_NAME_PREFIX + resMgr->sectionsToken() + moduleID;
1814       resMgr->setValue(sectionName, shortcutInversed.first, shortcutInversed.second.toString());
1815
1816       ShCutDbg() && ShCutDbg("Saving shortcut: \"" + moduleID + "\"\t\"" + shortcutInversed.first + "\"\t\"" + shortcutInversed.second.toString() + "\"");
1817     }
1818   }
1819 }
1820
1821 void SUIT_ShortcutMgr::setAssetsFromResources(QString theLanguage)
1822 {
1823   ShCutDbg() && ShCutDbg("Retrieving action assets.");
1824
1825   SUIT_ResourceMgr* resMgr = SUIT_Session::session()->resourceMgr();
1826   if (!resMgr) {
1827     Warning("SUIT_ShortcutMgr can't retrieve resource manager!");
1828     return;
1829   }
1830
1831   if (theLanguage.isEmpty())
1832     theLanguage = resMgr->stringValue(LANG_SECTION, LANG_SECTION, DEFAULT_LANG);
1833
1834   QStringList langPriorityList = LANG_PRIORITY_LIST;
1835   langPriorityList.push_front(theLanguage);
1836   langPriorityList.removeDuplicates();
1837
1838   QStringList actionAssetFilePaths = resMgr->parameters(SECTION_NAME_ACTION_ASSET_FILE_PATHS);
1839 #ifdef SHORTCUT_MGR_DBG
1840   ShCutDbg("Asset files: " + actionAssetFilePaths.join(", ") + ".");
1841 #endif
1842   for (const QString& actionAssetFilePath : actionAssetFilePaths) {
1843     const QString path = substituteVars(actionAssetFilePath);
1844     QFile actionAssetFile(path);
1845     if (!actionAssetFile.open(QIODevice::ReadOnly)) {
1846       Warning("SUIT_ShortcutMgr can't open action asset file \"" + path + "\"!");
1847       continue;
1848     }
1849
1850     QJsonParseError jsonError;
1851     QJsonDocument document = QJsonDocument::fromJson(actionAssetFile.readAll(), &jsonError);
1852     actionAssetFile.close();
1853     if(jsonError.error != QJsonParseError::NoError) {
1854         Warning("SUIT_ShortcutMgr: error during parsing of action asset file \"" + path + "\"!");
1855         continue;
1856     }
1857
1858     if(document.isObject()) {
1859       QJsonObject object = document.object();
1860       SUIT_ActionAssets actionAssets;
1861       for (const QString& actionID : object.keys()) {
1862         const auto moduleIDAndActionID = SUIT_ShortcutMgr::splitIntoModuleIDAndInModuleID(actionID);
1863         const QString& moduleID = moduleIDAndActionID.first;
1864         const QString& inModuleActionID = moduleIDAndActionID.second;
1865
1866         if (inModuleActionID.isEmpty()) {
1867           ShCutDbg("Action asset file \"" + path + "\" contains invalid action ID \"" + actionID + "\".");
1868           continue;
1869         }
1870
1871         if (!actionAssets.fromJSON(object[actionID].toObject())) {
1872           ShCutDbg("Action asset file \"" + path + "\" contains invalid action assets with ID \"" + actionID + "\".");
1873           continue;
1874         }
1875
1876         const bool nameInCurLangExists = actionAssets.myLangDependentAssets.find(theLanguage) != actionAssets.myLangDependentAssets.end();
1877         if (nameInCurLangExists) {
1878           actionAssets.clearAllLangsExcept(theLanguage);
1879         }
1880         else {
1881           bool nameInLinguaFrancaExists = false;
1882           QString usedLanguage = QString();
1883           for (int i = 1; i < langPriorityList.length(); i++) {
1884             nameInLinguaFrancaExists = actionAssets.myLangDependentAssets.find(langPriorityList[i]) != actionAssets.myLangDependentAssets.end();
1885             if (nameInLinguaFrancaExists) {
1886               usedLanguage = langPriorityList[i];
1887               actionAssets.clearAllLangsExcept(usedLanguage);
1888               break;
1889             }
1890           }
1891
1892           #ifdef SHORTCUT_MGR_DBG
1893           if (nameInLinguaFrancaExists)
1894             ShCutDbg("Can't find assets for action with ID \"" + actionID + "\" at current (" + theLanguage + ") language. Assets in " + usedLanguage + " is used for the action." );
1895           else {
1896             ShCutDbg("Can't find assets for action with ID \"" + actionID + "\". Tried " + langPriorityList.join(", ") + " languages." );
1897             continue;
1898           }
1899           #endif
1900         }
1901
1902         auto& moduleActionAssets = myActionAssets[moduleID];
1903         auto itAssets = moduleActionAssets.find(inModuleActionID);
1904         if (itAssets == moduleActionAssets.end()) {
1905           auto pAssets = std::shared_ptr<SUIT_ActionAssets>(new SUIT_ActionAssets(actionAssets));
1906           itAssets = moduleActionAssets.emplace(actionID, pAssets).first;
1907         }
1908         else
1909           itAssets->second->merge(actionAssets, true);
1910
1911         const auto& assets = itAssets->second;
1912         if (!assets->myIconPath.isEmpty() && assets->myIcon.isNull())
1913           assets->myIcon = QIcon(substituteVars(assets->myIconPath));
1914       }
1915     }
1916   }
1917
1918   #ifdef SHORTCUT_MGR_DBG
1919   ShCutDbg("Parsed assets: ");
1920   QJsonObject object;
1921   for (const auto& moduleIDAndAssets : myActionAssets) {
1922     for (const auto& actionIDAndAssets : moduleIDAndAssets.second) {
1923       actionIDAndAssets.second->toJSON(object);
1924       QJsonDocument doc(object);
1925       QString strJson = doc.toJson(QJsonDocument::Indented);
1926       const QString actionID = SUIT_ShortcutMgr::makeActionID(moduleIDAndAssets.first, actionIDAndAssets.first);
1927       ShCutDbg(actionID + " : " +  strJson);
1928     }
1929   }
1930   #endif
1931
1932   // Fill myModuleAssets.
1933   for (const auto& moduleID : myShortcutContainer.getIDsOfAllModules()) {
1934     const auto assets = std::shared_ptr<SUIT_ActionAssets>(new SUIT_ActionAssets());
1935     auto& lda = assets->myLangDependentAssets[DEFAULT_LANG];
1936
1937     if (moduleID == SUIT_ShortcutMgr::ROOT_MODULE_ID) {
1938       lda.myName = tr("General");
1939
1940       { // Load icon.
1941         QString dirPath;
1942         if (resMgr->value("resources", "LightApp", dirPath)) {
1943           assets->myIconPath = dirPath + (!dirPath.isEmpty() && dirPath[dirPath.length() - 1] == "/" ? "" : "/") + "icon_default.png";
1944           assets->myIcon = QIcon(substituteVars(assets->myIconPath));
1945         }
1946       }
1947     }
1948     else {
1949       QString moduleName = moduleID;
1950       resMgr->value(moduleID, "name", moduleName);
1951       lda.myName = moduleName;
1952
1953       resMgr->value(moduleID, "description", lda.myToolTip);
1954
1955       { // Load icon.
1956         QString dirPath;
1957         QString fileName;
1958         if (resMgr->value("resources", moduleID, dirPath) && resMgr->value(moduleID, "icon", fileName)) {
1959           assets->myIconPath = dirPath + (!dirPath.isEmpty() && dirPath[dirPath.length() - 1] == "/" ? "" : "/") + fileName;
1960           assets->myIcon = QIcon(substituteVars(assets->myIconPath));
1961         }
1962       }
1963     }
1964
1965     myModuleAssets.emplace(moduleID, std::move(assets));
1966   }
1967 }
1968
1969
1970
1971 SUIT_SentenceMatcher::SUIT_SentenceMatcher()
1972 {
1973   myUseExactWordOrder = false;
1974   myUseFuzzyWords = true;
1975   myIsCaseSensitive = false;
1976 }
1977
1978 void SUIT_SentenceMatcher::setUseExactWordOrder(bool theOn)
1979 {
1980   if (myUseExactWordOrder == theOn)
1981     return;
1982
1983   myUseExactWordOrder = theOn;
1984   if (!myWords || theOn)
1985     return;
1986
1987   if (!myPermutatedSentences) {
1988     myPermutatedSentences.reset(new QList<QStringList>());
1989     SUIT_SentenceMatcher::makePermutatedSentences(*myWords, *myPermutatedSentences);
1990   }
1991
1992   if (myUseFuzzyWords && !myFuzzyPermutatedSentences) {
1993     myFuzzyPermutatedSentences.reset(new QList<QStringList>());
1994     SUIT_SentenceMatcher::makePermutatedSentences(*myFuzzyWords, *myFuzzyPermutatedSentences);
1995   }
1996 }
1997
1998 void SUIT_SentenceMatcher::setUseFuzzyWords(bool theOn)
1999 {
2000   if (myUseFuzzyWords == theOn)
2001     return;
2002
2003   myUseFuzzyWords = theOn;
2004   if (!myWords || !theOn || myFuzzyWords)
2005     return;
2006
2007   myFuzzyWords.reset(new QStringList());
2008   SUIT_SentenceMatcher::makeFuzzyWords(*myWords, *myFuzzyWords);
2009
2010   if (!myUseExactWordOrder) {
2011     myFuzzyPermutatedSentences.reset(new QList<QStringList>());
2012     SUIT_SentenceMatcher::makePermutatedSentences(*myFuzzyWords, *myFuzzyPermutatedSentences);
2013   }
2014 }
2015
2016 void SUIT_SentenceMatcher::setCaseSensitive(bool theOn)
2017 {
2018   myIsCaseSensitive = theOn;
2019 }
2020
2021 void SUIT_SentenceMatcher::setQuery(QString theQuery)
2022 {
2023   theQuery = theQuery.simplified();
2024   if (theQuery == myQuery)
2025     return;
2026
2027   myQuery = theQuery;
2028
2029   { // Set exact words.
2030     if (myWords)
2031       myWords->clear();
2032     else
2033       myWords.reset(new QStringList());
2034
2035     *myWords = theQuery.split(" ", QString::SkipEmptyParts);
2036   }
2037
2038   { // Set permutated sentences.
2039     if (myUseExactWordOrder)
2040       myPermutatedSentences.reset(nullptr);
2041     else {
2042       if (!myPermutatedSentences)
2043         myPermutatedSentences.reset(new QList<QStringList>());
2044
2045       SUIT_SentenceMatcher::makePermutatedSentences(*myWords, *myPermutatedSentences);
2046     }
2047   }
2048
2049   // Set fuzzy words and sentences.
2050   if (myUseFuzzyWords) {
2051     if (!myFuzzyWords)
2052       myFuzzyWords.reset(new QStringList());
2053
2054     SUIT_SentenceMatcher::makeFuzzyWords(*myWords, *myFuzzyWords);
2055
2056     if (myUseExactWordOrder)
2057       myFuzzyPermutatedSentences.reset(nullptr);
2058     else {
2059       if (!myFuzzyPermutatedSentences)
2060         myFuzzyPermutatedSentences.reset(new QList<QStringList>());
2061
2062       SUIT_SentenceMatcher::makePermutatedSentences(*myFuzzyWords, *myFuzzyPermutatedSentences);
2063     }
2064   }
2065   else {
2066     myFuzzyWords.reset(nullptr);
2067     myFuzzyPermutatedSentences.reset(nullptr);
2068   }
2069 }
2070
2071 size_t SUIT_SentenceMatcher::match(const QString& theInputString) const
2072 {
2073   size_t n = 0;
2074   if (myUseExactWordOrder) {
2075     n = SUIT_SentenceMatcher::match(theInputString, *myWords, myIsCaseSensitive);
2076     if (n > 0)
2077       return n;
2078
2079     if (myUseFuzzyWords) {
2080       n = SUIT_SentenceMatcher::match(theInputString, *myFuzzyWords, myIsCaseSensitive);
2081       if (n > 0)
2082         return n;
2083     }
2084   }
2085   else /* if match with permutated query sentences */ {
2086     n = SUIT_SentenceMatcher::match(theInputString, *myPermutatedSentences, myIsCaseSensitive);
2087     if (n > 0)
2088       return n;
2089
2090     if (myUseFuzzyWords) {
2091       n = SUIT_SentenceMatcher::match(theInputString, *myFuzzyPermutatedSentences, myIsCaseSensitive);
2092       if (n > 0)
2093         return n;
2094     }
2095   }
2096
2097   return n;
2098 }
2099
2100 /*static*/ bool SUIT_SentenceMatcher::makePermutatedSentences(const QStringList& theWords, QList<QStringList>& theSentences)
2101 {
2102   theSentences.clear();
2103   theSentences.push_back(theWords);
2104   QStringList nextPerm = theWords;
2105   QStringList prevPerm = theWords;
2106
2107   bool hasNextPerm = true;
2108   bool hasPrevPerm = true;
2109
2110   while (hasNextPerm || hasPrevPerm) {
2111     if (hasNextPerm)
2112       hasNextPerm = std::next_permutation(nextPerm.begin(), nextPerm.end());
2113
2114     if (hasNextPerm && !theSentences.contains(nextPerm))
2115       theSentences.push_back(nextPerm);
2116
2117     if (hasPrevPerm)
2118       hasPrevPerm = std::prev_permutation(prevPerm.begin(), prevPerm.end());
2119
2120     if (hasPrevPerm && !theSentences.contains(prevPerm))
2121       theSentences.push_back(prevPerm);
2122   }
2123
2124   return theSentences.size() > 1;
2125 }
2126
2127 /*static*/ void SUIT_SentenceMatcher::makeFuzzyWords(const QStringList& theWords, QStringList& theFuzzyWords)
2128 {
2129   theFuzzyWords.clear();
2130   for (const QString& word : theWords) {
2131     QString fuzzyWord;
2132     for (int i = 0; i < word.size(); i++) {
2133       fuzzyWord += word[i];
2134       fuzzyWord += "\\w*";
2135     }
2136     theFuzzyWords.push_back(fuzzyWord);
2137   }
2138 }
2139
2140 /*static*/ size_t SUIT_SentenceMatcher::matchWithSentenceIgnoreEndings(const QString& theInputString, const QStringList& theSentence, bool theCaseSensitive)
2141 {
2142   QRegExp regExp("^" + theSentence.join("\\w*\\W+"), theCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive);
2143   if (theInputString.contains(regExp))
2144     return theSentence.size();
2145   else
2146     return 0;
2147 }
2148
2149 /*static*/ size_t SUIT_SentenceMatcher::matchWithSentencesIgnoreEndings(const QString& theInputString, const QList<QStringList>& theSentences, bool theCaseSensitive)
2150 {
2151   for (const QStringList& sentence : theSentences) {
2152     if (SUIT_SentenceMatcher::matchWithSentenceIgnoreEndings(theInputString, sentence, theCaseSensitive))
2153       return sentence.size();
2154   }
2155   return 0;
2156 }
2157
2158 /*static*/ size_t SUIT_SentenceMatcher::matchAtLeastOneWord(const QString& theInputString, const QStringList& theWords, bool theCaseSensitive)
2159 {
2160   size_t n = 0;
2161   for (const QString& word : theWords) {
2162     // The same input word can be counted multiple times. Nobody cares.
2163     if (theInputString.contains(QRegExp(word, theCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive)))
2164       n++;
2165   }
2166   return n;
2167 }
2168
2169 /*static*/ size_t SUIT_SentenceMatcher::match(
2170   const QString& theInputString,
2171   const QStringList& theSentence,
2172   bool theCaseSensitive
2173 ) {
2174   size_t n = SUIT_SentenceMatcher::matchWithSentenceIgnoreEndings(theInputString, theSentence, theCaseSensitive);
2175   if (n > 0)
2176     return n;
2177
2178   return SUIT_SentenceMatcher::matchAtLeastOneWord(theInputString, theSentence, theCaseSensitive);
2179 }
2180
2181 /*static*/ size_t SUIT_SentenceMatcher::match(
2182   const QString& theInputString,
2183   const QList<QStringList>& theSentences,
2184   bool theCaseSensitive
2185 ) {
2186   size_t n = SUIT_SentenceMatcher::matchWithSentencesIgnoreEndings(theInputString, theSentences, theCaseSensitive);
2187   if (n > 0)
2188     return n;
2189
2190   if (theSentences.size())
2191     return SUIT_SentenceMatcher::matchAtLeastOneWord(theInputString, theSentences[0], theCaseSensitive);
2192   else
2193     return 0;
2194 }
2195
2196
2197 SUIT_ActionSearcher::AssetsAndSearchData::AssetsAndSearchData(std::shared_ptr<SUIT_ActionAssets> theAssets, size_t theNumOfMatchingWords)
2198 : myAssets(theAssets), myNumOfMatchingWords(theNumOfMatchingWords)
2199 {}
2200
2201 SUIT_ActionSearcher::SUIT_ActionSearcher()
2202 {
2203   myIncludedModuleIDs = { SUIT_ShortcutMgr::ROOT_MODULE_ID };
2204   myIncludeDisabledActions = false;
2205   myFieldsToMatch = { SUIT_ActionSearcher::MatchField::Name, SUIT_ActionSearcher::MatchField::ToolTip };
2206   myMatcher.setCaseSensitive(false);
2207   myMatcher.setUseExactWordOrder(false);
2208   myMatcher.setUseFuzzyWords(true);
2209 }
2210
2211 bool SUIT_ActionSearcher::setIncludedModuleIDs(std::set<QString> theIncludedModuleIDs)
2212 {
2213   if (myIncludedModuleIDs == theIncludedModuleIDs)
2214     return false;
2215
2216   myIncludedModuleIDs = theIncludedModuleIDs;
2217
2218   bool res = false;
2219   // Erase search results with excluded modules. Erase IDs of modules, which are already in search results, from theIncludedModuleIDs.
2220   for (auto itFound = mySearchResults.begin(); itFound != mySearchResults.end(); ) {
2221     const auto itModuleID = theIncludedModuleIDs.find(itFound->first);
2222     if (itModuleID == theIncludedModuleIDs.end()) {
2223       mySearchResults.erase(itFound);
2224       res = true;
2225     }
2226     else {
2227       itFound++;
2228       theIncludedModuleIDs.erase(itModuleID);
2229     }
2230   }
2231
2232   // Filter assets of added modules.
2233   const auto& allAssets = SUIT_ShortcutMgr::get()->getActionAssets();
2234   for (const auto& moduleIDAndAssets : allAssets) {
2235     const QString& moduleID = moduleIDAndAssets.first;
2236     const auto& actionIDsAndAssets = moduleIDAndAssets.second;
2237     if (theIncludedModuleIDs.find(moduleID) == theIncludedModuleIDs.end())
2238       continue;
2239
2240     for (const auto& actionIDAndAssets : actionIDsAndAssets) {
2241       const QString& inModuleActionID = actionIDAndAssets.first;
2242       const size_t n = matchAction(moduleID, inModuleActionID, actionIDAndAssets.second);
2243       if (n > 0) {
2244         mySearchResults[moduleID][inModuleActionID] = SUIT_ActionSearcher::AssetsAndSearchData(actionIDAndAssets.second, n);
2245         res = true;
2246       }
2247     }
2248   }
2249
2250   return res;
2251 }
2252
2253 bool SUIT_ActionSearcher::includeDisabledActions(bool theOn)
2254 {
2255   if (myIncludeDisabledActions == theOn)
2256     return false;
2257
2258   myIncludeDisabledActions = theOn;
2259
2260   if (myIncludeDisabledActions)
2261     return extendResults();
2262   else
2263     return filterResults().first;
2264 }
2265
2266 bool SUIT_ActionSearcher::setFieldsToMatch(const std::set<SUIT_ActionSearcher::MatchField>& theFields)
2267 {
2268   if (myFieldsToMatch == theFields)
2269     return false;
2270
2271   if (theFields.empty()) {
2272     myFieldsToMatch = theFields;
2273     mySearchResults.clear();
2274     return true;
2275   }
2276
2277   bool narrows = true;
2278   for (const SUIT_ActionSearcher::MatchField field : theFields) {
2279     if (myFieldsToMatch.find(field) == myFieldsToMatch.end()) {
2280       narrows = false;
2281       break;
2282     }
2283   }
2284
2285   bool extends = true;
2286   for (const SUIT_ActionSearcher::MatchField field : myFieldsToMatch) {
2287     if (theFields.find(field) == theFields.end()) {
2288       extends = false;
2289       break;
2290     }
2291   }
2292
2293   myFieldsToMatch = theFields;
2294
2295   if (narrows)
2296     return filterResults().first;
2297   else if (extends)
2298     return extendResults();
2299   else
2300     return filter().first;
2301 }
2302
2303 bool SUIT_ActionSearcher::setCaseSensitive(bool theOn)
2304 {
2305   if (myMatcher.isCaseSensitive() == theOn)
2306     return false;
2307
2308   myMatcher.setCaseSensitive(theOn);
2309
2310   if (theOn)
2311     return filterResults().first;
2312   else
2313     return extendResults();
2314 }
2315
2316 bool SUIT_ActionSearcher::setQuery(const QString& theQuery)
2317 {
2318   if (theQuery.simplified() == myMatcher.getQuery().simplified())
2319     return false;
2320
2321   myMatcher.setQuery(theQuery);
2322   return filter().first;
2323 }
2324
2325 const std::map<QString, std::map<QString, SUIT_ActionSearcher::AssetsAndSearchData>>& SUIT_ActionSearcher::getSearchResults() const
2326 {
2327   return mySearchResults;
2328 }
2329
2330 std::pair<bool, bool> SUIT_ActionSearcher::filter()
2331 {
2332   auto res = std::pair<bool, bool>(false, false);
2333
2334   for (const auto& moduleIDAndAssets : SUIT_ShortcutMgr::get()->getActionAssets()) {
2335     const auto& moduleID = moduleIDAndAssets.first;
2336     if (myIncludedModuleIDs.find(moduleID) == myIncludedModuleIDs.end())
2337       continue;
2338
2339     const auto& actionIDsAndAssets = moduleIDAndAssets.second;
2340
2341     auto itFoundModuleIDAndAssets = mySearchResults.find(moduleID);
2342     for (const auto& actionIDAndAssets : actionIDsAndAssets) {
2343       const QString& inModuleActionID = actionIDAndAssets.first;
2344
2345       if (itFoundModuleIDAndAssets != mySearchResults.end()) {
2346         auto& foundActionIDsAndAssets = itFoundModuleIDAndAssets->second;
2347         auto itFoundActionIDAndAssets = foundActionIDsAndAssets.find(inModuleActionID);
2348         if (itFoundActionIDAndAssets != foundActionIDsAndAssets.end()) {
2349           // Action is already in search results.
2350           SUIT_ActionSearcher::AssetsAndSearchData& aAndD = itFoundActionIDAndAssets->second;
2351           const size_t n = matchAction(moduleID, inModuleActionID, aAndD.myAssets);
2352           if (n > 0) {
2353             if (n != aAndD.myNumOfMatchingWords) {
2354               aAndD.myNumOfMatchingWords = n;
2355               res.second = true;
2356             }
2357           }
2358           else /* if n == 0 */ {
2359             foundActionIDsAndAssets.erase(itFoundActionIDAndAssets);
2360             res.first = true;
2361           }
2362           continue;
2363         }
2364       }
2365
2366       const size_t n = matchAction(moduleID, inModuleActionID, actionIDAndAssets.second);
2367       if (n > 0) {
2368         if (itFoundModuleIDAndAssets == mySearchResults.end())
2369           itFoundModuleIDAndAssets = mySearchResults.emplace(moduleID, std::map<QString, SUIT_ActionSearcher::AssetsAndSearchData>()).first;
2370
2371         itFoundModuleIDAndAssets->second.emplace(inModuleActionID, SUIT_ActionSearcher::AssetsAndSearchData(actionIDAndAssets.second, n));
2372         res.first = true;
2373       }
2374     }
2375   }
2376
2377   return res;
2378 }
2379
2380 std::pair<bool, bool> SUIT_ActionSearcher::filterResults()
2381 {
2382   auto res = std::pair<bool, bool>(false, false);
2383
2384   for (auto itFoundModuleIDAndAssets = mySearchResults.begin(); itFoundModuleIDAndAssets != mySearchResults.end(); ) {
2385     const QString& moduleID = itFoundModuleIDAndAssets->first;
2386     auto& actionIDsAndAssets = itFoundModuleIDAndAssets->second;
2387     for (auto itActionIDAndAssets = actionIDsAndAssets.begin(); itActionIDAndAssets != actionIDsAndAssets.end(); ) {
2388       const QString& inModuleActionID = itActionIDAndAssets->first;
2389       SUIT_ActionSearcher::AssetsAndSearchData& assetsAndSearchData = itActionIDAndAssets->second;
2390       const size_t n = matchAction(moduleID, inModuleActionID, assetsAndSearchData.myAssets);
2391       if (n == 0) {
2392         actionIDsAndAssets.erase(itActionIDAndAssets);
2393         res.first = true;
2394       }
2395       else {
2396         if (assetsAndSearchData.myNumOfMatchingWords != n) {
2397           assetsAndSearchData.myNumOfMatchingWords = n;
2398           res.second = true;
2399         }
2400         itActionIDAndAssets++;
2401       }
2402     }
2403
2404     if (actionIDsAndAssets.empty())
2405       mySearchResults.erase(itFoundModuleIDAndAssets);
2406     else
2407       itFoundModuleIDAndAssets++;
2408   }
2409
2410   return res;
2411 }
2412
2413 bool SUIT_ActionSearcher::extendResults()
2414 {
2415   bool res = false;
2416   for (const auto& moduleIDAndAssets : SUIT_ShortcutMgr::get()->getActionAssets()) {
2417     const auto& moduleID = moduleIDAndAssets.first;
2418     if (myIncludedModuleIDs.find(moduleID) == myIncludedModuleIDs.end())
2419       continue;
2420
2421     const auto& actionIDsAndAssets = moduleIDAndAssets.second;
2422
2423     auto itFoundModuleIDAndAssets = mySearchResults.find(moduleID);
2424     for (const auto& actionIDAndAssets : actionIDsAndAssets) {
2425       const QString& inModuleActionID = actionIDAndAssets.first;
2426
2427       if (itFoundModuleIDAndAssets != mySearchResults.end()) {
2428         const auto& foundActionIDsAndAssets = itFoundModuleIDAndAssets->second;
2429         if (foundActionIDsAndAssets.find(inModuleActionID) != foundActionIDsAndAssets.end())
2430           continue; // Action is already in search results.
2431       }
2432
2433       const size_t n = matchAction(moduleID, inModuleActionID, actionIDAndAssets.second);
2434       if (n > 0) {
2435         if (itFoundModuleIDAndAssets == mySearchResults.end())
2436           itFoundModuleIDAndAssets = mySearchResults.emplace(moduleID, std::map<QString, SUIT_ActionSearcher::AssetsAndSearchData>()).first;
2437
2438         itFoundModuleIDAndAssets->second.emplace(inModuleActionID, SUIT_ActionSearcher::AssetsAndSearchData(actionIDAndAssets.second, n));
2439         res = true;
2440       }
2441     }
2442   }
2443   return res;
2444 }
2445
2446 size_t SUIT_ActionSearcher::matchAction(const QString& theModuleID, const QString& theInModuleActionID, std::shared_ptr<SUIT_ActionAssets> theAssets)
2447 {
2448   if (!myIncludeDisabledActions) {
2449     const auto& actions = SUIT_ShortcutMgr::get()->getActions(theModuleID, theInModuleActionID);
2450     const bool actionEnabled = std::find_if(actions.begin(), actions.end(), [](const QAction* const theAction){ return theAction->isEnabled(); } ) != actions.end();
2451     if (!actionEnabled)
2452       return false;
2453   }
2454
2455   for (const auto& langAndLDA : theAssets->myLangDependentAssets) {
2456     if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::ToolTip) != myFieldsToMatch.end()) {
2457       if (myMatcher.match(langAndLDA.second.myToolTip))
2458         return true;
2459     }
2460
2461     if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::Name) != myFieldsToMatch.end()) {
2462       if (myMatcher.match(langAndLDA.second.myName))
2463         return true;
2464     }
2465   }
2466
2467   if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::ID) != myFieldsToMatch.end()) {
2468     if (myMatcher.match(SUIT_ShortcutMgr::makeActionID(theModuleID, theInModuleActionID)))
2469       return true;
2470   }
2471
2472   if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::KeySequence) != myFieldsToMatch.end()) {
2473     const QString keySequence = SUIT_ShortcutMgr::get()->getKeySequence(theModuleID, theInModuleActionID).toString();
2474     if (myMatcher.match(keySequence))
2475       return true;
2476   }
2477
2478   return false;
2479 }