Salome HOME
[bos #40644][CEA](2024-T1) Feature 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(inModuleActionID, 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 (theOn) {
1985     myPermutatedSentences.clear();
1986     myFuzzyPermutatedSentences.clear();
1987     return;
1988   }
1989
1990   if (myPermutatedSentences.isEmpty())
1991     SUIT_SentenceMatcher::makePermutatedSentences(myWords, myPermutatedSentences);
1992
1993   if (myUseFuzzyWords && myFuzzyPermutatedSentences.isEmpty())
1994     SUIT_SentenceMatcher::makePermutatedSentences(myFuzzyWords, myFuzzyPermutatedSentences);
1995 }
1996
1997 void SUIT_SentenceMatcher::setUseFuzzyWords(bool theOn)
1998 {
1999   if (myUseFuzzyWords == theOn)
2000     return;
2001
2002   myUseFuzzyWords = theOn;
2003   if (myWords.isEmpty() || !theOn) {
2004     myFuzzyWords.clear();
2005     myFuzzyPermutatedSentences.clear();
2006     return;
2007   }
2008
2009   myFuzzyWords.clear();
2010   SUIT_SentenceMatcher::makeFuzzyWords(myWords, myFuzzyWords);
2011
2012   if (!myUseExactWordOrder) {
2013     myFuzzyPermutatedSentences.clear();
2014     SUIT_SentenceMatcher::makePermutatedSentences(myFuzzyWords, myFuzzyPermutatedSentences);
2015   }
2016 }
2017
2018 void SUIT_SentenceMatcher::setCaseSensitive(bool theOn)
2019 {
2020   myIsCaseSensitive = theOn;
2021 }
2022
2023 void SUIT_SentenceMatcher::setQuery(QString theQuery)
2024 {
2025   theQuery = theQuery.simplified();
2026   if (theQuery == myQuery)
2027     return;
2028
2029   myQuery = theQuery;
2030   myWords = theQuery.split(" ", QString::SkipEmptyParts);
2031
2032   { // Set permutated sentences.
2033     myPermutatedSentences.clear();
2034     if (!myUseExactWordOrder)
2035       SUIT_SentenceMatcher::makePermutatedSentences(myWords, myPermutatedSentences);
2036   }
2037
2038   // Set fuzzy words and sentences.
2039   myFuzzyWords.clear();
2040   myFuzzyPermutatedSentences.clear();
2041
2042   if (myUseFuzzyWords) {
2043     SUIT_SentenceMatcher::makeFuzzyWords(myWords, myFuzzyWords);
2044     if (!myUseExactWordOrder)
2045       SUIT_SentenceMatcher::makePermutatedSentences(myFuzzyWords, myFuzzyPermutatedSentences);
2046   }
2047 }
2048
2049 double SUIT_SentenceMatcher::match(const QString& theInputString) const
2050 {
2051   int n = 0;
2052   if (myUseExactWordOrder) {
2053     n = SUIT_SentenceMatcher::match(theInputString, myWords, myIsCaseSensitive);
2054     if (n != theInputString.length() && myUseFuzzyWords) {
2055       const int nFuzzy = SUIT_SentenceMatcher::match(theInputString, myFuzzyWords, myIsCaseSensitive);
2056       if (nFuzzy > n)
2057         n = nFuzzy;
2058     }
2059   }
2060   else /* if match with permutated query sentences */ {
2061     n = SUIT_SentenceMatcher::match(theInputString, myPermutatedSentences, myIsCaseSensitive);
2062     if (n != theInputString.length() && myUseFuzzyWords) {
2063       const int nFuzzy = SUIT_SentenceMatcher::match(theInputString, myFuzzyPermutatedSentences, myIsCaseSensitive);
2064       if (nFuzzy > n)
2065         n = nFuzzy;
2066     }
2067   }
2068
2069   if (n <= 0)
2070     return std::numeric_limits<double>::infinity();
2071
2072   const auto strLength = theInputString.length() > myQuery.length() ? theInputString.length() : myQuery.length();
2073
2074   if (n > strLength)
2075     return 0; // Exact match or almost exact.
2076
2077   return double(strLength - n);
2078 }
2079
2080 QString SUIT_SentenceMatcher::toString() const
2081 {
2082   QString res = QString("myUseExactWordOrder: ") + (myUseExactWordOrder ? "true" : "false") + ";\n";
2083   res += QString("myUseFuzzyWords: ") + (myUseFuzzyWords ? "true" : "false") + ";\n";
2084   res += QString("myIsCaseSensitive: ") + (myIsCaseSensitive ? "true" : "false") + ";\n";
2085   res += QString("myQuery: ") + myQuery + ";\n";
2086   res += QString("myWords: ") + myWords.join(", ") + ";\n";
2087   res += QString("myFuzzyWords: ") + myFuzzyWords.join(", ") + ";\n";
2088
2089   res += "myPermutatedSentences:\n";
2090   for (const auto& sentence : myPermutatedSentences) {
2091     res += "\t" + sentence.join(", ") + ";\n";
2092   }
2093
2094   res += "myFuzzyPermutatedSentences:\n";
2095   for (const auto& sentence : myFuzzyPermutatedSentences) {
2096     res += "\t" + sentence.join(", ") + ";\n";
2097   }
2098
2099   res += ".";
2100   return res;
2101 }
2102
2103 /*static*/ bool SUIT_SentenceMatcher::makePermutatedSentences(const QStringList& theWords, QList<QStringList>& theSentences)
2104 {
2105   theSentences.clear();
2106   theSentences.push_back(theWords);
2107   QStringList nextPerm = theWords;
2108   QStringList prevPerm = theWords;
2109
2110   bool hasNextPerm = true;
2111   bool hasPrevPerm = true;
2112
2113   while (hasNextPerm || hasPrevPerm) {
2114     if (hasNextPerm)
2115       hasNextPerm = std::next_permutation(nextPerm.begin(), nextPerm.end());
2116
2117     if (hasNextPerm && !theSentences.contains(nextPerm))
2118       theSentences.push_back(nextPerm);
2119
2120     if (hasPrevPerm)
2121       hasPrevPerm = std::prev_permutation(prevPerm.begin(), prevPerm.end());
2122
2123     if (hasPrevPerm && !theSentences.contains(prevPerm))
2124       theSentences.push_back(prevPerm);
2125   }
2126
2127   return theSentences.size() > 1;
2128 }
2129
2130 /*static*/ void SUIT_SentenceMatcher::makeFuzzyWords(const QStringList& theWords, QStringList& theFuzzyWords)
2131 {
2132   theFuzzyWords.clear();
2133   for (const QString& word : theWords) {
2134     QString fuzzyWord;
2135     for (int i = 0; i < word.size(); i++) {
2136       fuzzyWord += word[i];
2137       fuzzyWord += "\\w*";
2138     }
2139     theFuzzyWords.push_back(fuzzyWord);
2140   }
2141 }
2142
2143 /*static*/ int SUIT_SentenceMatcher::matchWithSentenceIgnoreEndings(const QString& theInputString, const QStringList& theSentence, bool theCaseSensitive)
2144 {
2145   const QRegExp regExp("^" + theSentence.join("\\w*\\W+"), theCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive);
2146   regExp.indexIn(theInputString);
2147   const int matchMetrics = regExp.matchedLength();
2148   return matchMetrics > 0 ? matchMetrics : 0;
2149 }
2150
2151 /*static*/ int SUIT_SentenceMatcher::matchWithSentencesIgnoreEndings(const QString& theInputString, const QList<QStringList>& theSentences, bool theCaseSensitive)
2152 {
2153   int res = 0;
2154   for (const QStringList& sentence : theSentences) {
2155     const int matchMetrics = SUIT_SentenceMatcher::matchWithSentenceIgnoreEndings(theInputString, sentence, theCaseSensitive);
2156     if (matchMetrics > res) {
2157       res = matchMetrics;
2158       if (res == theInputString.length())
2159         return res;
2160     }
2161   }
2162   return res;
2163 }
2164
2165 /*static*/ int SUIT_SentenceMatcher::matchAtLeastOneWord(const QString& theInputString, const QStringList& theWords, bool theCaseSensitive)
2166 {
2167   int res = 0;
2168   for (const QString& word : theWords) {
2169     const auto regExp = QRegExp(word, theCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive);
2170     regExp.indexIn(theInputString);
2171     const int matchMetrics = regExp.matchedLength();
2172     // The same input word can be counted multiple times. Nobody cares.
2173     if (matchMetrics > 0)
2174       res += matchMetrics;
2175   }
2176   return res;
2177 }
2178
2179 /*static*/ int SUIT_SentenceMatcher::match(
2180   const QString& theInputString,
2181   const QStringList& theSentence,
2182   bool theCaseSensitive
2183 ) {
2184   int res = SUIT_SentenceMatcher::matchWithSentenceIgnoreEndings(theInputString, theSentence, theCaseSensitive);
2185   if (res == theInputString.length())
2186     return res;
2187
2188   const int matchMetrics = SUIT_SentenceMatcher::matchAtLeastOneWord(theInputString, theSentence, theCaseSensitive);
2189   if (matchMetrics > res)
2190     res = matchMetrics;
2191
2192   return res;
2193 }
2194
2195 /*static*/ int SUIT_SentenceMatcher::match(
2196   const QString& theInputString,
2197   const QList<QStringList>& theSentences,
2198   bool theCaseSensitive
2199 ) {
2200   int res = SUIT_SentenceMatcher::matchWithSentencesIgnoreEndings(theInputString, theSentences, theCaseSensitive);
2201   if (res == theInputString.length())
2202     return res;
2203
2204   if (theSentences.size() > 0) {
2205     const int matchMetrics = SUIT_SentenceMatcher::matchAtLeastOneWord(theInputString, theSentences[0], theCaseSensitive);
2206     if (matchMetrics > res)
2207       res = matchMetrics;
2208   }
2209
2210   return res;
2211 }
2212
2213
2214 SUIT_ActionSearcher::AssetsAndSearchData::AssetsAndSearchData(std::shared_ptr<const SUIT_ActionAssets> theAssets, double theMatchMetrics)
2215 : myAssets(theAssets), myMatchMetrics(theMatchMetrics)
2216 {
2217   if (theMatchMetrics < 0) {
2218     myMatchMetrics = std::numeric_limits<double>::infinity();
2219     ShCutDbg("WARNING: SUIT_ActionSearcher::AssetsAndSearchData: match metrics < 0. INF is assigned instead.");
2220   }
2221 }
2222
2223 void SUIT_ActionSearcher::AssetsAndSearchData::setMatchMetrics(double theMatchMetrics)
2224 {
2225   if (theMatchMetrics < 0) {
2226     myMatchMetrics = std::numeric_limits<double>::infinity();
2227     ShCutDbg("WARNING: SUIT_ActionSearcher::AssetsAndSearchData: match metrics < 0. INF is assigned instead.");
2228     return;
2229   }
2230
2231   myMatchMetrics = theMatchMetrics;
2232 }
2233
2234 void SUIT_ActionSearcher::AssetsAndSearchData::toJSON(QJsonObject& oJsonObject) const
2235 {
2236   oJsonObject["myMatchMetrics"] = myMatchMetrics;
2237
2238   if (myAssets) {
2239     QJsonObject assetsJSON;
2240     myAssets->toJSON(assetsJSON);
2241     oJsonObject["myAssets"] = assetsJSON;
2242   }
2243 }
2244
2245 QString SUIT_ActionSearcher::AssetsAndSearchData::toString() const
2246 {
2247   QJsonObject json;
2248   toJSON(json);
2249   QJsonDocument doc(json);
2250   return QString(doc.toJson(QJsonDocument::Indented));
2251 }
2252
2253 SUIT_ActionSearcher::SUIT_ActionSearcher()
2254 {
2255   myIncludedModuleIDs = { SUIT_ShortcutMgr::ROOT_MODULE_ID };
2256   myIncludeDisabledActions = false;
2257   myFieldsToMatch = { SUIT_ActionSearcher::MatchField::Name, SUIT_ActionSearcher::MatchField::ToolTip };
2258   myMatcher.setCaseSensitive(false);
2259   myMatcher.setUseExactWordOrder(false);
2260   myMatcher.setUseFuzzyWords(true);
2261 }
2262
2263 bool SUIT_ActionSearcher::setIncludedModuleIDs(std::set<QString> theIncludedModuleIDs)
2264 {
2265   ShCutDbg("SUIT_ActionSearcher::setIncludedModuleIDs");
2266
2267   if (myIncludedModuleIDs == theIncludedModuleIDs)
2268     return false;
2269
2270   myIncludedModuleIDs = theIncludedModuleIDs;
2271
2272   bool res = false;
2273   // Erase search results from excluded modules. Erase IDs of modules, which are already in search results, from theIncludedModuleIDs.
2274   for (auto itFound = mySearchResults.begin(); itFound != mySearchResults.end(); ) {
2275     const auto itModuleID = theIncludedModuleIDs.find(itFound->first);
2276     if (itModuleID == theIncludedModuleIDs.end()) {
2277       itFound = mySearchResults.erase(itFound);
2278       res = true;
2279     }
2280     else {
2281       itFound++;
2282       theIncludedModuleIDs.erase(itModuleID);
2283     }
2284   }
2285
2286   // Filter assets of added modules.
2287   const auto& allAssets = SUIT_ShortcutMgr::get()->getActionAssets();
2288   for (const auto& moduleIDAndAssets : allAssets) {
2289     const QString& moduleID = moduleIDAndAssets.first;
2290     const auto& actionIDsAndAssets = moduleIDAndAssets.second;
2291     if (theIncludedModuleIDs.find(moduleID) == theIncludedModuleIDs.end())
2292       continue;
2293
2294     for (const auto& actionIDAndAssets : actionIDsAndAssets) {
2295       const QString& inModuleActionID = actionIDAndAssets.first;
2296       const double matchMetrics = matchAction(moduleID, inModuleActionID, actionIDAndAssets.second);
2297       if (matchMetrics < std::numeric_limits<double>::infinity()) {
2298         mySearchResults[moduleID][inModuleActionID] = SUIT_ActionSearcher::AssetsAndSearchData(actionIDAndAssets.second, matchMetrics);
2299         res = true;
2300       }
2301     }
2302   }
2303
2304   ShCutDbg() && ShCutDbg(toString());
2305
2306   return res;
2307 }
2308
2309 bool SUIT_ActionSearcher::includeDisabledActions(bool theOn)
2310 {
2311   ShCutDbg("SUIT_ActionSearcher::includeDisabledActions");
2312
2313   if (myIncludeDisabledActions == theOn)
2314     return false;
2315
2316   myIncludeDisabledActions = theOn;
2317
2318   bool res;
2319   if (myIncludeDisabledActions)
2320     res = extendResults();
2321   else
2322     res = filterResults().first;
2323
2324   ShCutDbg() && ShCutDbg(toString());
2325   return res;
2326 }
2327
2328 bool SUIT_ActionSearcher::setFieldsToMatch(const std::set<SUIT_ActionSearcher::MatchField>& theFields)
2329 {
2330   if (myFieldsToMatch == theFields)
2331     return false;
2332
2333   if (theFields.empty()) {
2334     myFieldsToMatch = theFields;
2335     mySearchResults.clear();
2336     return true;
2337   }
2338
2339   bool narrows = true;
2340   for (const SUIT_ActionSearcher::MatchField field : theFields) {
2341     if (myFieldsToMatch.find(field) == myFieldsToMatch.end()) {
2342       narrows = false;
2343       break;
2344     }
2345   }
2346
2347   bool extends = true;
2348   for (const SUIT_ActionSearcher::MatchField field : myFieldsToMatch) {
2349     if (theFields.find(field) == theFields.end()) {
2350       extends = false;
2351       break;
2352     }
2353   }
2354
2355   myFieldsToMatch = theFields;
2356
2357   bool res;
2358   if (narrows)
2359     res = filterResults().first;
2360   else if (extends)
2361     res = extendResults();
2362   else
2363     res = filter().first;
2364
2365   ShCutDbg() && ShCutDbg(toString());
2366   return res;
2367 }
2368
2369 bool SUIT_ActionSearcher::setCaseSensitive(bool theOn)
2370 {
2371   if (myMatcher.isCaseSensitive() == theOn)
2372     return false;
2373
2374   myMatcher.setCaseSensitive(theOn);
2375
2376   bool res;
2377   if (theOn)
2378     res = filterResults().first;
2379   else
2380     res = extendResults();
2381
2382   ShCutDbg() && ShCutDbg(toString());
2383   return res;
2384 }
2385
2386 bool SUIT_ActionSearcher::setQuery(const QString& theQuery)
2387 {
2388   ShCutDbg("SUIT_ActionSearcher::setQuery");
2389
2390   if (theQuery.simplified() == myMatcher.getQuery().simplified())
2391     return false;
2392
2393   myMatcher.setQuery(theQuery);
2394   bool res = filter().first;
2395   ShCutDbg() && ShCutDbg(toString());
2396   return res;
2397 }
2398
2399 const std::map<QString, std::map<QString, SUIT_ActionSearcher::AssetsAndSearchData>>& SUIT_ActionSearcher::getSearchResults() const
2400 {
2401   return mySearchResults;
2402 }
2403
2404 std::pair<bool, bool> SUIT_ActionSearcher::filter()
2405 {
2406   ShCutDbg("SUIT_ActionSearcher::filter()");
2407
2408   auto res = std::pair<bool, bool>(false, false);
2409
2410   for (const auto& moduleIDAndAssets : SUIT_ShortcutMgr::get()->getActionAssets()) {
2411     const auto& moduleID = moduleIDAndAssets.first;
2412     if (myIncludedModuleIDs.find(moduleID) == myIncludedModuleIDs.end())
2413       continue;
2414
2415     const auto& actionIDsAndAssets = moduleIDAndAssets.second;
2416
2417     auto itFoundModuleIDAndAssets = mySearchResults.find(moduleID);
2418     for (const auto& actionIDAndAssets : actionIDsAndAssets) {
2419       const QString& inModuleActionID = actionIDAndAssets.first;
2420
2421       if (itFoundModuleIDAndAssets != mySearchResults.end()) {
2422         auto& foundActionIDsAndAssets = itFoundModuleIDAndAssets->second;
2423         auto itFoundActionIDAndAssets = foundActionIDsAndAssets.find(inModuleActionID);
2424         if (itFoundActionIDAndAssets != foundActionIDsAndAssets.end()) {
2425           // Action is already in search results.
2426           SUIT_ActionSearcher::AssetsAndSearchData& aAndD = itFoundActionIDAndAssets->second;
2427           const double matchMetrics = matchAction(moduleID, inModuleActionID, aAndD.myAssets);
2428           if (matchMetrics < std::numeric_limits<double>::infinity()) {
2429             if (matchMetrics != aAndD.matchMetrics()) {
2430               aAndD.setMatchMetrics(matchMetrics);
2431               res.second = true;
2432             }
2433           }
2434           else /* if n == 0 */ {
2435             foundActionIDsAndAssets.erase(itFoundActionIDAndAssets);
2436             res.first = true;
2437           }
2438           continue;
2439         }
2440       }
2441
2442       const double matchMetrics = matchAction(moduleID, inModuleActionID, actionIDAndAssets.second);
2443       if (matchMetrics < std::numeric_limits<double>::infinity()) {
2444         if (itFoundModuleIDAndAssets == mySearchResults.end())
2445           itFoundModuleIDAndAssets = mySearchResults.emplace(moduleID, std::map<QString, SUIT_ActionSearcher::AssetsAndSearchData>()).first;
2446
2447         itFoundModuleIDAndAssets->second.emplace(inModuleActionID, SUIT_ActionSearcher::AssetsAndSearchData(actionIDAndAssets.second, matchMetrics));
2448         res.first = true;
2449       }
2450     }
2451   }
2452
2453   return res;
2454 }
2455
2456 std::pair<bool, bool> SUIT_ActionSearcher::filterResults()
2457 {
2458   auto res = std::pair<bool, bool>(false, false);
2459
2460   for (auto itFoundModuleIDAndAssets = mySearchResults.begin(); itFoundModuleIDAndAssets != mySearchResults.end(); ) {
2461     const QString& moduleID = itFoundModuleIDAndAssets->first;
2462     auto& actionIDsAndAssets = itFoundModuleIDAndAssets->second;
2463     for (auto itActionIDAndAssets = actionIDsAndAssets.begin(); itActionIDAndAssets != actionIDsAndAssets.end(); ) {
2464       const QString& inModuleActionID = itActionIDAndAssets->first;
2465       SUIT_ActionSearcher::AssetsAndSearchData& assetsAndSearchData = itActionIDAndAssets->second;
2466       const double matchMetrics = matchAction(moduleID, inModuleActionID, assetsAndSearchData.myAssets);
2467       if (matchMetrics == std::numeric_limits<double>::infinity()) {
2468         itActionIDAndAssets = actionIDsAndAssets.erase(itActionIDAndAssets);
2469         res.first = true;
2470       }
2471       else {
2472         if (assetsAndSearchData.matchMetrics() != matchMetrics) {
2473           assetsAndSearchData.setMatchMetrics(matchMetrics);
2474           res.second = true;
2475         }
2476         itActionIDAndAssets++;
2477       }
2478     }
2479
2480     if (actionIDsAndAssets.empty())
2481       itFoundModuleIDAndAssets = mySearchResults.erase(itFoundModuleIDAndAssets);
2482     else
2483       itFoundModuleIDAndAssets++;
2484   }
2485
2486   return res;
2487 }
2488
2489 bool SUIT_ActionSearcher::extendResults()
2490 {
2491   ShCutDbg("SUIT_ActionSearcher::extendResults()");
2492
2493   bool res = false;
2494   for (const auto& moduleIDAndAssets : SUIT_ShortcutMgr::get()->getActionAssets()) {
2495     const auto& moduleID = moduleIDAndAssets.first;
2496     if (myIncludedModuleIDs.find(moduleID) == myIncludedModuleIDs.end())
2497       continue;
2498
2499     const auto& actionIDsAndAssets = moduleIDAndAssets.second;
2500
2501     auto itFoundModuleIDAndAssets = mySearchResults.find(moduleID);
2502     for (const auto& actionIDAndAssets : actionIDsAndAssets) {
2503       const QString& inModuleActionID = actionIDAndAssets.first;
2504
2505       if (itFoundModuleIDAndAssets != mySearchResults.end()) {
2506         const auto& foundActionIDsAndAssets = itFoundModuleIDAndAssets->second;
2507         if (foundActionIDsAndAssets.find(inModuleActionID) != foundActionIDsAndAssets.end())
2508           continue; // Action is already in search results.
2509       }
2510
2511       ShCutDbg() && ShCutDbg("SUIT_ActionSearcher::extendResults(): " + moduleID + "/" + inModuleActionID + "." );
2512       const double matchMetrics = matchAction(moduleID, inModuleActionID, actionIDAndAssets.second);
2513       if (matchMetrics < std::numeric_limits<double>::infinity()) {
2514         ShCutDbg("SUIT_ActionSearcher::extendResults(): match, metrics = " + QString::fromStdString(std::to_string(matchMetrics)));
2515         if (itFoundModuleIDAndAssets == mySearchResults.end())
2516           itFoundModuleIDAndAssets = mySearchResults.emplace(moduleID, std::map<QString, SUIT_ActionSearcher::AssetsAndSearchData>()).first;
2517
2518         itFoundModuleIDAndAssets->second.emplace(inModuleActionID, SUIT_ActionSearcher::AssetsAndSearchData(actionIDAndAssets.second, matchMetrics));
2519         res = true;
2520       }
2521     }
2522   }
2523   return res;
2524 }
2525
2526 double SUIT_ActionSearcher::matchAction(const QString& theModuleID, const QString& theInModuleActionID, std::shared_ptr<const SUIT_ActionAssets> theAssets)
2527 {
2528   if (!theAssets) {
2529     ShCutDbg("WARNING: SUIT_ActionSearcher::matchAction: theAssets is nullptr.");
2530     return std::numeric_limits<double>::infinity();
2531   }
2532
2533   if (!myIncludeDisabledActions) {
2534     const auto& actions = SUIT_ShortcutMgr::get()->getActions(theModuleID, theInModuleActionID);
2535     const bool actionEnabled = std::find_if(actions.begin(), actions.end(), [](const QAction* const theAction){ return theAction->isEnabled(); } ) != actions.end();
2536     if (!actionEnabled)
2537       return std::numeric_limits<double>::infinity();
2538   }
2539
2540   double res = std::numeric_limits<double>::infinity();
2541
2542   for (const auto& langAndLDA : theAssets->myLangDependentAssets) {
2543     if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::ToolTip) != myFieldsToMatch.end()) {
2544       const double matchMetrics = myMatcher.match(langAndLDA.second.myToolTip);
2545       if (matchMetrics < res)
2546         res = matchMetrics;
2547     }
2548
2549     if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::Name) != myFieldsToMatch.end()) {
2550       const double matchMetrics = myMatcher.match(langAndLDA.second.myName);
2551       if (matchMetrics < res)
2552         res = matchMetrics;
2553     }
2554   }
2555
2556   if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::ID) != myFieldsToMatch.end()) {
2557     const double matchMetrics = myMatcher.match(SUIT_ShortcutMgr::makeActionID(theModuleID, theInModuleActionID));
2558     if (matchMetrics < res)
2559         res = matchMetrics;
2560   }
2561
2562   if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::KeySequence) != myFieldsToMatch.end()) {
2563     const QString keySequence = SUIT_ShortcutMgr::get()->getKeySequence(theModuleID, theInModuleActionID).toString();
2564     const double matchMetrics = myMatcher.match(keySequence);
2565     if (matchMetrics < res)
2566         res = matchMetrics;
2567   }
2568
2569   return res;
2570 }
2571
2572 QString SUIT_ActionSearcher::toString() const
2573 {
2574   QString res;
2575
2576   res += "myMatcher: {\n";
2577   res += myMatcher.toString();
2578   res += "};\n";
2579
2580   res += "myIncludedModuleIDs: ";
2581   for (const QString& moduleID : myIncludedModuleIDs) {
2582     res += moduleID + ", ";
2583   }
2584   res += ";\n";
2585
2586   res += QString("myIncludeDisabledActions: ") + (myIncludeDisabledActions ? "true" : "false") + ";\n";
2587
2588   res += "myFieldsToMatch: ";
2589   for (const auto& field : myFieldsToMatch) {
2590     res += QString::number(int(field)) + ", ";
2591   }
2592   res += ";\n";
2593
2594   res += "mySearchResults:\n";
2595   for (const auto& moduleIDAndAssets : mySearchResults ) {
2596     res += "\tModule ID: " + moduleIDAndAssets.first + ":\n";
2597     for (const auto& actionIDAndAssets : moduleIDAndAssets.second) {
2598       const auto& assetsAndSearchData = actionIDAndAssets.second;
2599       res += "\t\tAction ID: " + actionIDAndAssets.first + ": {";
2600       res += "\t\t: " + actionIDAndAssets.second.toString();
2601       res += "\t\t}";
2602     }
2603   }
2604
2605   return res;
2606 }