Salome HOME
[bos #40644][CEA](2024-T1) Feauture search.
authordish <dmitrii.shvydkoi@opencascade.com>
Mon, 26 Feb 2024 14:10:49 +0000 (14:10 +0000)
committerdish <dmitrii.shvydkoi@opencascade.com>
Mon, 26 Feb 2024 14:10:49 +0000 (14:10 +0000)
Add feature search dialog.

src/Qtx/CMakeLists.txt
src/Qtx/QtxFeatureSearch.cxx [new file with mode: 0644]
src/Qtx/QtxFeatureSearch.h [new file with mode: 0644]
src/SUIT/SUIT_ShortcutMgr.cxx
src/SUIT/SUIT_ShortcutMgr.h

index f95706ef16d400bab4cb09507e04b52fe32668ab..9d1ae2f6371631a35cc0e0d41af61ac410b372e8 100644 (file)
@@ -53,6 +53,7 @@ SET(_moc_HEADERS
   QtxDockWidget.h
   QtxDoubleSpinBox.h
   QtxDoubleSpinSlider.h
+  QtxFeatureSearch.h
   QtxFontEdit.h
   QtxGridBox.h
   QtxGroupBox.h
@@ -147,6 +148,7 @@ SET(_other_SOURCES
   QtxDoubleSpinBox.cxx
   QtxDoubleSpinSlider.cxx
   QtxEvalExpr.cxx
+  QtxFeatureSearch.cxx
   QtxFontEdit.cxx
   QtxGridBox.cxx
   QtxGroupBox.cxx
diff --git a/src/Qtx/QtxFeatureSearch.cxx b/src/Qtx/QtxFeatureSearch.cxx
new file mode 100644 (file)
index 0000000..6a87cfe
--- /dev/null
@@ -0,0 +1,262 @@
+// Copyright (C) 2007-2024  CEA, EDF, OPEN CASCADE
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+//
+// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
+//
+
+#include "QtxFeatureSearch.h"
+
+#include <QAction>
+#include <QWidget>
+#include <QLayout>
+#include <QList>
+#include <QMap>
+
+#include <QCollator>
+
+#include <QCheckBox>
+#include <QLineEdit>
+#include <QBrush>
+#include <QColor>
+#include <QHeaderView>
+
+#include <algorithm>
+
+
+QtxFoundActionTree::QtxFoundActionTree()
+{}
+
+
+QtxFoundActionTreeItem::QtxFoundActionTreeItem(const QString& theModuleID)
+: QTreeWidgetItem(), myModuleID(theModuleID)
+{ }
+
+QString QtxFoundActionTreeItem::name() const
+{
+  return text(QtxFoundActionTree::ElementIdx::Name);
+}
+
+QString QtxFoundActionTreeItem::toolTip() const
+{
+  return text(QtxFoundActionTree::ElementIdx::ToolTip);
+}
+
+
+QtxFoundActionTreeFolder::QtxFoundActionTreeFolder(const QString& theModuleID)
+: QtxFoundActionTreeItem(theModuleID)
+{
+  QFont f = font(QtxFoundActionTree::ElementIdx::Name);
+  f.setBold(true);
+  setFont(QtxFoundActionTree::ElementIdx::Name, f);
+  setText(QtxFoundActionTree::ElementIdx::Name, theModuleID);
+}
+
+void QtxFoundActionTreeFolder::setAssets(std::shared_ptr<const SUIT_ActionAssets> theAssets, const QString& theLang)
+{
+  if (!theAssets)
+    return;
+
+  setIcon(QtxFoundActionTree::ElementIdx::Name, theAssets->myIcon);
+
+  const auto& ldaMap = theAssets->myLangDependentAssets;
+  if (ldaMap.empty()) {
+    setText(QtxFoundActionTree::ElementIdx::Name, myModuleID);
+    return;
+  }
+
+  auto itLDA = ldaMap.find(theLang);
+  if (itLDA == ldaMap.end())
+    itLDA = ldaMap.begin();
+
+  const SUIT_ActionAssets::LangDependentAssets& lda = itLDA->second;
+  const QString& name = lda.myName.isEmpty() ? myModuleID : lda.myName;
+  setText(QtxFoundActionTree::ElementIdx::Name, name);
+}
+
+QString QtxFoundActionTreeFolder::getValue(QtxFoundActionTree::SortKey theKey) const
+{
+  switch (theKey) {
+    case QtxFoundActionTree::SortKey::ID:
+      return myModuleID;
+    case QtxFoundActionTree::SortKey::Name:
+      return name();
+    case QtxFoundActionTree::SortKey::ToolTip:
+      return toolTip();
+    default:
+      return QString();
+  }
+}
+
+bool QtxFoundActionTreeFolder::isEnabled() const
+{
+  return true;
+}
+
+
+QtxFoundActionTreeAction::QtxFoundActionTreeAction(const QString& theModuleID, const QString& theInModuleActionID)
+: QtxFoundActionTreeItem(theModuleID), myInModuleActionID(theInModuleActionID)
+{
+  setText(QtxFoundActionTree::ElementIdx::Name, theInModuleActionID);
+}
+
+/*static*/ QtxFoundActionTreeAction* QtxFoundActionTreeAction::create(const QString& theModuleID, const QString& theInModuleActionID)
+{
+  if (theInModuleActionID.isEmpty()) {
+    ShCutDbg("QtxFoundActionTreeItem: attempt to create item with empty action ID.");
+    return nullptr;
+  }
+
+  return new QtxFoundActionTreeAction(theModuleID, theInModuleActionID);
+}
+
+void QtxFoundActionTreeAction::setAssets(std::shared_ptr<const SUIT_ActionAssets> theAssets, const QString& theLang)
+{
+  if (!theAssets)
+    return;
+
+  setIcon(QtxFoundActionTree::ElementIdx::Name, theAssets->myIcon);
+
+  const auto& ldaMap = theAssets->myLangDependentAssets;
+  if (ldaMap.empty()) {
+    setText(QtxFoundActionTree::ElementIdx::Name, myInModuleActionID);
+    return;
+  }
+
+  auto itLDA = ldaMap.find(theLang);
+  if (itLDA == ldaMap.end())
+    itLDA = ldaMap.begin();
+
+  const SUIT_ActionAssets::LangDependentAssets& lda = itLDA->second;
+  const QString& name = lda.myName.isEmpty() ? myInModuleActionID : lda.myName;
+  setText(QtxFoundActionTree::ElementIdx::Name, name);
+
+  const QString& actionToolTip = lda.myToolTip.isEmpty() ? name : lda.myToolTip;
+  setText(QtxFoundActionTree::ElementIdx::ToolTip, actionToolTip);
+
+  if (isEnabled()) {
+      setToolTip(
+      QtxFoundActionTree::ElementIdx::Name,
+      QtxFoundActionTree::tr("Double click to start")
+    );
+
+    setToolTip(
+      QtxFoundActionTree::ElementIdx::ToolTip,
+      QtxFoundActionTree::tr("Double click to start")
+    );
+  }
+  else {
+    static const QBrush greyedOutBrush = QBrush(Qt::gray);
+    setForeground(QtxFoundActionTree::ElementIdx::Name, greyedOutBrush);
+    setForeground(QtxFoundActionTree::ElementIdx::ToolTip, greyedOutBrush);
+  }
+}
+
+QString QtxFoundActionTreeAction::getValue(QtxFoundActionTree::SortKey theKey) const
+{
+  switch (theKey) {
+    case QtxFoundActionTree::SortKey::ID:
+      return myInModuleActionID;
+    case QtxFoundActionTree::SortKey::Name:
+      return name();
+    case QtxFoundActionTree::SortKey::ToolTip:
+      return toolTip();
+    default:
+      return QString();
+  }
+}
+
+bool QtxFoundActionTreeAction::isEnabled() const
+{
+  const auto& actions = SUIT_ShortcutMgr::get()->getActions(myModuleID, myInModuleActionID);
+  return std::find_if(actions.begin(), actions.end(), [](const QAction* const theAction){ return theAction->isEnabled(); }) != actions.end();
+}
+
+void QtxFoundActionTreeAction::trigger() const
+{
+  const auto& actions = SUIT_ShortcutMgr::get()->getActions(myModuleID, myInModuleActionID);
+  for (const auto& action : actions) {
+    if (action->isEnabled())
+      action->trigger();
+  }
+}
+
+
+QtxFeatureSearchDialog::QtxFeatureSearchDialog(QWidget* theParent)
+: QDialog(theParent)
+{
+  setMinimumWidth(500);
+  setWindowTitle(tr("Search action"));
+  QVBoxLayout* layout = new QVBoxLayout(this);
+
+  myQueryLineEdit = new QLineEdit(this);
+  layout->addWidget(myQueryLineEdit);
+  myQueryLineEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
+  setFocusProxy(myQueryLineEdit);
+
+  QHBoxLayout* searchOptionsLayout = new QHBoxLayout(this);
+  layout->addLayout(searchOptionsLayout);
+  myIncludeUnavailableActionsCB = new QCheckBox(tr("Unavailable actions"), this);
+  myIncludeUnavailableActionsCB->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+  myIncludeUnavailableActionsCB->setCheckState(Qt::CheckState::Checked);
+  myIncludeInactiveModulesCB = new QCheckBox(tr("Inactive modules"), this);
+  myIncludeInactiveModulesCB->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+  myIncludeInactiveModulesCB->setCheckState(Qt::CheckState::Unchecked);
+  searchOptionsLayout->addWidget(myIncludeUnavailableActionsCB);
+  searchOptionsLayout->addWidget(myIncludeInactiveModulesCB);
+
+  myFoundActionsTree = new QtxFoundActionTree();
+  layout->addWidget(myFoundActionsTree);
+
+  connect(myQueryLineEdit, SIGNAL(textChanged(const QString&)), this, SLOT(onQueryChanged(const QString&)));
+  connect(myIncludeUnavailableActionsCB, SIGNAL(stateChanged(Qt::CheckState)), this, SLOT(onSearchOptionUnavailableActionsChanged(Qt::CheckState)));
+  connect(myIncludeInactiveModulesCB, SIGNAL(stateChanged(Qt::CheckState)), this, SLOT(onSearchOptionInactiveModulesChanged(Qt::CheckState)));
+}
+
+void QtxFeatureSearchDialog::setActiveModuleID(const QString& theModuleID)
+{
+  if(myActionSearcher.setIncludedModuleIDs(std::set<QString>({SUIT_ShortcutMgr::ROOT_MODULE_ID, myActiveModuleID})))
+    updateUI();
+}
+
+void QtxFeatureSearchDialog::onQueryChanged(const QString& theQuery)
+{
+  if (myActionSearcher.setQuery(theQuery))
+    updateUI();
+}
+
+void QtxFeatureSearchDialog::onSearchOptionUnavailableActionsChanged(Qt::CheckState theState)
+{
+  if (myActionSearcher.includeDisabledActions(theState == Qt::CheckState::Checked))
+    updateUI();
+}
+
+void QtxFeatureSearchDialog::onSearchOptionInactiveModulesChanged(Qt::CheckState theState)
+{
+  bool resultsChanged = false;
+  if (theState == Qt::CheckState::Checked)
+    resultsChanged = myActionSearcher.setIncludedModuleIDs(SUIT_ShortcutMgr::get()->getShortcutContainer().getIDsOfAllModules());
+  else
+    resultsChanged = myActionSearcher.setIncludedModuleIDs(std::set<QString>({SUIT_ShortcutMgr::ROOT_MODULE_ID, myActiveModuleID}));
+
+  if (resultsChanged)
+    updateUI();
+}
+
+void QtxFeatureSearchDialog::updateUI()
+{
+
+}
+
diff --git a/src/Qtx/QtxFeatureSearch.h b/src/Qtx/QtxFeatureSearch.h
new file mode 100644 (file)
index 0000000..ea68270
--- /dev/null
@@ -0,0 +1,191 @@
+// Copyright (C) 2007-2024  CEA, EDF, OPEN CASCADE
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+//
+// See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
+//
+
+#ifndef QTXFEATURESEARCH_H
+#define QTXFEATURESEARCH_H
+
+#include "Qtx.h"
+#include <QDialog>
+#include <QFrame>
+#include <QTreeWidget>
+#include "SUIT_ShortcutMgr.h"
+#include <memory>
+#include <map>
+#include <set>
+#include <functional>
+
+
+class QtxFoundActionTreeItem;
+class QtxFoundActionTreeFolder;
+class QtxFoundActionTreeAction;;
+
+
+class QTX_EXPORT QtxFoundActionTree : public QTreeWidget
+{
+  Q_OBJECT
+
+public:
+  enum ElementIdx {
+    Name = 0,
+    ToolTip = 1
+  };
+
+  enum class SortKey {
+    ID,
+    Name,
+    ToolTip
+  };
+
+  enum class SortOrder {
+    Ascending,
+    Descending
+  };
+
+  QtxFoundActionTree();
+  QtxFoundActionTree(const QtxFoundActionTree&) = delete;
+  QtxFoundActionTree& operator=(const QtxFoundActionTree&) = delete;
+  virtual ~QtxFoundActionTree() = default;
+
+  void sort(QtxFoundActionTree::SortKey theKey, QtxFoundActionTree::SortOrder theOrder);
+
+private:
+  void updateItems();
+  std::pair<QtxFoundActionTreeFolder*, int> findModuleFolderItem(const QString& theModuleID) const;
+
+  std::set<QtxFoundActionTreeItem*, std::function<bool(QtxFoundActionTreeItem*, QtxFoundActionTreeItem*)>> getSortedChildren(QtxFoundActionTreeFolder* theParentItem);
+
+  void insertChild(
+    QtxFoundActionTreeFolder* theParentItem,
+    std::set<QtxFoundActionTreeItem*, std::function<bool(QtxFoundActionTreeItem*, QtxFoundActionTreeItem*)>>& theSortedChildren,
+    QtxFoundActionTreeItem* theChildItem
+  );
+
+private slots:
+  void onItemDoubleClicked(QTreeWidgetItem* theWidgetItem, int theColIdx);
+
+public:
+  static const QList<std::pair<QtxFoundActionTree::SortKey, QtxFoundActionTree::SortOrder>> DEFAULT_SORT_SCHEMA;
+
+private:
+  QtxFoundActionTree::SortKey mySortKey;
+  QtxFoundActionTree::SortOrder mySortOrder;
+};
+
+
+class QtxFoundActionTreeItem : public QTreeWidgetItem
+{
+public:
+  enum Type {
+    Folder = 0,
+    Action = 1,
+  };
+
+protected:
+  QtxFoundActionTreeItem(const QString& theModuleID);
+
+public:
+  virtual ~QtxFoundActionTreeItem() = default;
+  virtual QtxFoundActionTreeItem::Type type() const = 0;
+
+  virtual void setAssets(std::shared_ptr<const SUIT_ActionAssets> theAssets, const QString& theLang) = 0;
+  QString name() const;
+  QString toolTip() const;
+
+  virtual QString getValue(QtxFoundActionTree::SortKey theKey) const = 0;
+
+  virtual bool isEnabled() const = 0;
+
+public:
+  const QString myModuleID;
+};
+
+
+class QtxFoundActionTreeFolder : public QtxFoundActionTreeItem
+{
+public:
+  QtxFoundActionTreeFolder(const QString& theModuleID);
+  virtual ~QtxFoundActionTreeFolder() = default;
+  virtual QtxFoundActionTreeItem::Type type() const { return QtxFoundActionTreeItem::Type::Folder; };
+
+  virtual void setAssets(std::shared_ptr<const SUIT_ActionAssets> theAssets, const QString& theLang);
+
+  virtual QString getValue(QtxFoundActionTree::SortKey theKey) const;
+
+  virtual bool isEnabled() const;
+};
+
+
+class QtxFoundActionTreeAction : public QtxFoundActionTreeItem
+{
+private:
+  QtxFoundActionTreeAction(const QString& theModuleID, const QString& theInModuleActionID);
+
+public:
+  static QtxFoundActionTreeAction* create(const QString& theModuleID, const QString& theInModuleActionID);
+  virtual ~QtxFoundActionTreeAction() = default;
+  virtual QtxFoundActionTreeItem::Type type() const { return QtxFoundActionTreeItem::Type::Action; };
+
+  virtual void setAssets(std::shared_ptr<const SUIT_ActionAssets> theAssets, const QString& theLang);
+
+  virtual QString getValue(QtxFoundActionTree::SortKey theKey) const;
+
+  virtual bool isEnabled() const;
+
+  void trigger() const;
+
+  const QString myInModuleActionID;
+};
+
+
+class QCheckBox;
+class QLineEdit;
+class QLabel;
+class QPushButton;
+
+
+class QTX_EXPORT QtxFeatureSearchDialog : public QDialog
+{
+  Q_OBJECT
+
+public:
+  QtxFeatureSearchDialog(QWidget* theParent);
+  QtxFeatureSearchDialog(const QtxFeatureSearchDialog&) = delete;
+  QtxFeatureSearchDialog& operator=(const QtxFeatureSearchDialog&) = delete;
+  virtual ~QtxFeatureSearchDialog() = default;
+
+  void setActiveModuleID(const QString& theModuleID);
+
+private slots:
+  void onQueryChanged(const QString& theKeyword);
+  void onSearchOptionUnavailableActionsChanged(Qt::CheckState theState);
+  void onSearchOptionInactiveModulesChanged(Qt::CheckState theState);
+
+private:
+  void updateUI();
+
+  QLineEdit* myQueryLineEdit;
+  QCheckBox* myIncludeUnavailableActionsCB;
+  QCheckBox* myIncludeInactiveModulesCB;
+  QtxFoundActionTree* myFoundActionsTree;
+
+  QString myActiveModuleID;
+  SUIT_ActionSearcher myActionSearcher;
+};
+
+#endif // QTXFEATURESEARCH_H
index bfae01ec37a16085c86d705c67fba665d7801f2e..8be5094cca35e7cf3bb6ba02a38e435d4d9f2a4f 100644 (file)
@@ -78,7 +78,7 @@ static const QKeySequence NO_KEYSEQUENCE = QKeySequence(QString(""));
 static const QString NO_ACTION = QString("");
 /** Separates tokens in action ID. */
 static const QString TOKEN_SEPARATOR = QString("/");
-static const QString ROOT_MODULE_ID = QString("");
+/*static*/ const QString SUIT_ShortcutMgr::ROOT_MODULE_ID = QString("");
 static const QString META_ACTION_PREFIX = QString("#");
 
 /** Prefix of names of shortcut setting sections in preference files. */
@@ -158,7 +158,7 @@ public:
       moduleShortcuts[theInModuleActionID] = theKeySequence.toString();
 
       const QString fileName = theModuleID + DevTools::SHORTCUTS_OF_META_SUFFIX;
-      const QString sectionName = SECTION_NAME_PREFIX + DevTools::XML_SECTION_TOKENS_SEPARATOR + ROOT_MODULE_ID;
+      const QString sectionName = SECTION_NAME_PREFIX + DevTools::XML_SECTION_TOKENS_SEPARATOR + SUIT_ShortcutMgr::ROOT_MODULE_ID;
       std::map<QString, std::map<QString, QString>> sections;
       sections[sectionName] = moduleShortcuts;
       writeToXMLFile(fileName, sections);
@@ -182,7 +182,7 @@ public:
     const QAction* theAction
   ) {
     if (SUIT_ShortcutMgr::isInModuleMetaActionID(theInModuleActionID)) {
-      QString actionID = SUIT_ShortcutMgr::makeActionID(ROOT_MODULE_ID, theInModuleActionID);
+      QString actionID = SUIT_ShortcutMgr::makeActionID(SUIT_ShortcutMgr::ROOT_MODULE_ID, theInModuleActionID);
       // { actionID, assets } []
       auto& moduleAssets = myAssetsOfMetaActions[theModuleID];
 
@@ -490,21 +490,21 @@ public:
 
 SUIT_ShortcutContainer::SUIT_ShortcutContainer()
 {
-  myShortcuts.emplace(ROOT_MODULE_ID, std::map<QKeySequence, QString>());
-  myShortcutsInversed.emplace(ROOT_MODULE_ID, std::map<QString, QKeySequence>());
+  myShortcuts.emplace(SUIT_ShortcutMgr::ROOT_MODULE_ID, std::map<QKeySequence, QString>());
+  myShortcutsInversed.emplace(SUIT_ShortcutMgr::ROOT_MODULE_ID, std::map<QString, QKeySequence>());
 }
 
 std::set<QString> SUIT_ShortcutContainer::getIDsOfInterferingModules(const QString& theModuleID) const
 {
   std::set<QString> IDsOfInterferingModules;
-  if (theModuleID == ROOT_MODULE_ID) {
+  if (theModuleID == SUIT_ShortcutMgr::ROOT_MODULE_ID) {
     for (const auto& moduleIDAndShortcuts : myShortcuts) {
       IDsOfInterferingModules.emplace(moduleIDAndShortcuts.first);
     }
   }
   else {
-    IDsOfInterferingModules.emplace(ROOT_MODULE_ID);
-    if (theModuleID != ROOT_MODULE_ID)
+    IDsOfInterferingModules.emplace(SUIT_ShortcutMgr::ROOT_MODULE_ID);
+    if (theModuleID != SUIT_ShortcutMgr::ROOT_MODULE_ID)
       IDsOfInterferingModules.emplace(theModuleID);
   }
   return IDsOfInterferingModules;
@@ -532,7 +532,7 @@ std::set<std::pair<QString, QString>> SUIT_ShortcutContainer::setShortcut(QStrin
   }
 
   if (SUIT_ShortcutMgr::isInModuleMetaActionID(theInModuleActionID))
-    theModuleID = ROOT_MODULE_ID;
+    theModuleID = SUIT_ShortcutMgr::ROOT_MODULE_ID;
 
   auto itModuleShortcuts = myShortcuts.find(theModuleID);
   auto itModuleShortcutsInversed = myShortcutsInversed.find(theModuleID);
@@ -629,7 +629,7 @@ std::set<std::pair<QString, QString>> SUIT_ShortcutContainer::getConflicts(
     return std::set<std::pair<QString, QString>>();
 
   if (SUIT_ShortcutMgr::isInModuleMetaActionID(theInModuleActionID))
-    theModuleID = ROOT_MODULE_ID;
+    theModuleID = SUIT_ShortcutMgr::ROOT_MODULE_ID;
 
   { // Check if the shortcut is set.
     const auto itModuleShortcuts = myShortcuts.find(theModuleID);
@@ -663,7 +663,7 @@ std::set<std::pair<QString, QString>> SUIT_ShortcutContainer::getConflicts(
 const QKeySequence& SUIT_ShortcutContainer::getKeySequence(QString theModuleID, const QString& theInModuleActionID) const
 {
   if (SUIT_ShortcutMgr::isInModuleMetaActionID(theInModuleActionID))
-    theModuleID = ROOT_MODULE_ID;
+    theModuleID = SUIT_ShortcutMgr::ROOT_MODULE_ID;
 
   const auto itModuleShortcutsInversed = myShortcutsInversed.find(theModuleID);
   if (itModuleShortcutsInversed == myShortcutsInversed.end())
@@ -680,7 +680,7 @@ const QKeySequence& SUIT_ShortcutContainer::getKeySequence(QString theModuleID,
 bool SUIT_ShortcutContainer::hasShortcut(QString theModuleID, const QString& theInModuleActionID) const
 {
   if (SUIT_ShortcutMgr::isInModuleMetaActionID(theInModuleActionID))
-    theModuleID = ROOT_MODULE_ID;
+    theModuleID = SUIT_ShortcutMgr::ROOT_MODULE_ID;
 
   const auto itModuleShortcutsInversed = myShortcutsInversed.find(theModuleID);
   if (itModuleShortcutsInversed == myShortcutsInversed.end())
@@ -1079,7 +1079,7 @@ SUIT_ShortcutMgr::~SUIT_ShortcutMgr()
     else
       ShCutDbg("Discovered shortcut modules: \"" + moduleIDs.join("\", \"") + ".");
   }
-  moduleIDs.push_front(ROOT_MODULE_ID); // Resource manager filters out empty section suffices.
+  moduleIDs.push_front(SUIT_ShortcutMgr::ROOT_MODULE_ID); // Resource manager filters out empty section suffices.
   moduleIDs.removeDuplicates();
 
   for (size_t i = 0; i < moduleIDs.size(); i++) {
@@ -1102,7 +1102,7 @@ SUIT_ShortcutMgr::~SUIT_ShortcutMgr()
       if (
         !SUIT_ShortcutMgr::isInModuleActionIDValid(inModuleActionID) ||
         !keySequence.first ||
-        SUIT_ShortcutMgr::isInModuleMetaActionID(inModuleActionID) && moduleID != ROOT_MODULE_ID
+        SUIT_ShortcutMgr::isInModuleMetaActionID(inModuleActionID) && moduleID != SUIT_ShortcutMgr::ROOT_MODULE_ID
       ) {
         std::list<std::pair<QString, QString>>& moduleInvalidShortcuts = invalidShortcuts[moduleID];
         moduleInvalidShortcuts.push_back(std::pair<QString, QString>(inModuleActionID, keySequenceString));
@@ -1353,7 +1353,7 @@ void SUIT_ShortcutMgr::registerAction(const QString& theActionID, QAction* theAc
   else {
     ShCutDbg(
       "Action with ID \"" +
-      (SUIT_ShortcutMgr::isInModuleMetaActionID(inModuleActionID) ? ROOT_MODULE_ID + TOKEN_SEPARATOR + inModuleActionID : theActionID) +
+      (SUIT_ShortcutMgr::isInModuleMetaActionID(inModuleActionID) ? SUIT_ShortcutMgr::ROOT_MODULE_ID + TOKEN_SEPARATOR + inModuleActionID : theActionID) +
       "\" is not added to default resource files."
     );
     auto conflicts = myShortcutContainer.setShortcut(moduleID, inModuleActionID, theAction->shortcut(), false);
@@ -1600,11 +1600,26 @@ std::shared_ptr<const SUIT_ActionAssets> SUIT_ShortcutMgr::getActionAssets(const
 
 std::shared_ptr<const SUIT_ActionAssets> SUIT_ShortcutMgr::getActionAssets(const QString& theActionID) const
 {
-  const auto it = myActionAssets.find(theActionID);
-  if (it == myActionAssets.end())
+  const auto moduleIDAndActionID = SUIT_ShortcutMgr::splitIntoModuleIDAndInModuleID(theActionID);
+  const QString& moduleID = moduleIDAndActionID.first;
+  const QString& inModuleActionID = moduleIDAndActionID.second;
+
+  if (inModuleActionID.isEmpty()) {
+    ShCutDbg() && ShCutDbg("Attempt to get assets of an action with invalid ID \"" + theActionID + "\".");
     return std::shared_ptr<const SUIT_ActionAssets>(nullptr);
-  else
-    return it->second;
+  }
+
+  const auto itModuleActionAssets = myActionAssets.find(moduleID);
+  if (itModuleActionAssets == myActionAssets.end())
+    return std::shared_ptr<const SUIT_ActionAssets>(nullptr);
+  else {
+    const auto moduleActionAssets = itModuleActionAssets->second;
+    const auto itActionAssets = moduleActionAssets.find(inModuleActionID);
+    if (itActionAssets == moduleActionAssets.end())
+      return std::shared_ptr<const SUIT_ActionAssets>(nullptr);
+    else
+      return itActionAssets->second;
+  }
 }
 
 QString SUIT_ShortcutMgr::getActionName(const QString& theModuleID, const QString& theInModuleActionID, const QString& theLang) const
@@ -1615,8 +1630,13 @@ QString SUIT_ShortcutMgr::getActionName(const QString& theModuleID, const QStrin
     return actionID;
   }
 
-  const auto itActionAssets = myActionAssets.find(actionID);
-  if (itActionAssets != myActionAssets.end() && !itActionAssets->second->myLangDependentAssets.empty()) {
+  const auto itModuleActionAssets = myActionAssets.find(theModuleID);
+  if (itModuleActionAssets == myActionAssets.end())
+    return actionID;
+
+  const auto moduleActionAssets = itModuleActionAssets->second;
+  const auto itActionAssets = moduleActionAssets.find(theInModuleActionID);
+  if (itActionAssets != moduleActionAssets.end() && !itActionAssets->second->myLangDependentAssets.empty()) {
     const auto& ldaMap = itActionAssets->second->myLangDependentAssets;
     if (ldaMap.empty())
       return theInModuleActionID;
@@ -1839,7 +1859,11 @@ void SUIT_ShortcutMgr::setAssetsFromResources(QString theLanguage)
       QJsonObject object = document.object();
       SUIT_ActionAssets actionAssets;
       for (const QString& actionID : object.keys()) {
-        if (!SUIT_ShortcutMgr::isActionIDValid(actionID)) {
+        const auto moduleIDAndActionID = SUIT_ShortcutMgr::splitIntoModuleIDAndInModuleID(actionID);
+        const QString& moduleID = moduleIDAndActionID.first;
+        const QString& inModuleActionID = moduleIDAndActionID.second;
+
+        if (inModuleActionID.isEmpty()) {
           ShCutDbg("Action asset file \"" + path + "\" contains invalid action ID \"" + actionID + "\".");
           continue;
         }
@@ -1875,10 +1899,11 @@ void SUIT_ShortcutMgr::setAssetsFromResources(QString theLanguage)
           #endif
         }
 
-        auto itAssets = myActionAssets.find(actionID);
-        if (itAssets == myActionAssets.end()) {
+        auto& moduleActionAssets = myActionAssets[moduleID];
+        auto itAssets = moduleActionAssets.find(inModuleActionID);
+        if (itAssets == moduleActionAssets.end()) {
           auto pAssets = std::shared_ptr<SUIT_ActionAssets>(new SUIT_ActionAssets(actionAssets));
-          itAssets = myActionAssets.emplace(actionID, pAssets).first;
+          itAssets = moduleActionAssets.emplace(actionID, pAssets).first;
         }
         else
           itAssets->second->merge(actionAssets, true);
@@ -1893,11 +1918,14 @@ void SUIT_ShortcutMgr::setAssetsFromResources(QString theLanguage)
   #ifdef SHORTCUT_MGR_DBG
   ShCutDbg("Parsed assets: ");
   QJsonObject object;
-  for (const auto& actionIDAndAssets : myActionAssets) {
-    actionIDAndAssets.second->toJSON(object);
-    QJsonDocument doc(object);
-    QString strJson = doc.toJson(QJsonDocument::Indented);
-    ShCutDbg(actionIDAndAssets.first + " : " +  strJson);
+  for (const auto& moduleIDAndAssets : myActionAssets) {
+    for (const auto& actionIDAndAssets : moduleIDAndAssets.second) {
+      actionIDAndAssets.second->toJSON(object);
+      QJsonDocument doc(object);
+      QString strJson = doc.toJson(QJsonDocument::Indented);
+      const QString actionID = SUIT_ShortcutMgr::makeActionID(moduleIDAndAssets.first, actionIDAndAssets.first);
+      ShCutDbg(actionID + " : " +  strJson);
+    }
   }
   #endif
 
@@ -1906,7 +1934,7 @@ void SUIT_ShortcutMgr::setAssetsFromResources(QString theLanguage)
     const auto assets = std::shared_ptr<SUIT_ActionAssets>(new SUIT_ActionAssets());
     auto& lda = assets->myLangDependentAssets[DEFAULT_LANG];
 
-    if (moduleID == ROOT_MODULE_ID) {
+    if (moduleID == SUIT_ShortcutMgr::ROOT_MODULE_ID) {
       lda.myName = tr("General");
 
       { // Load icon.
@@ -1936,4 +1964,516 @@ void SUIT_ShortcutMgr::setAssetsFromResources(QString theLanguage)
 
     myModuleAssets.emplace(moduleID, std::move(assets));
   }
+}
+
+
+
+SUIT_SentenceMatcher::SUIT_SentenceMatcher()
+{
+  myUseExactWordOrder = false;
+  myUseFuzzyWords = true;
+  myIsCaseSensitive = false;
+}
+
+void SUIT_SentenceMatcher::setUseExactWordOrder(bool theOn)
+{
+  if (myUseExactWordOrder == theOn)
+    return;
+
+  myUseExactWordOrder = theOn;
+  if (!myWords || theOn)
+    return;
+
+  if (!myPermutatedSentences) {
+    myPermutatedSentences.reset(new QList<QStringList>());
+    SUIT_SentenceMatcher::makePermutatedSentences(*myWords, *myPermutatedSentences);
+  }
+
+  if (myUseFuzzyWords && !myFuzzyPermutatedSentences) {
+    myFuzzyPermutatedSentences.reset(new QList<QStringList>());
+    SUIT_SentenceMatcher::makePermutatedSentences(*myFuzzyWords, *myFuzzyPermutatedSentences);
+  }
+}
+
+void SUIT_SentenceMatcher::setUseFuzzyWords(bool theOn)
+{
+  if (myUseFuzzyWords == theOn)
+    return;
+
+  myUseFuzzyWords = theOn;
+  if (!myWords || !theOn || myFuzzyWords)
+    return;
+
+  myFuzzyWords.reset(new QStringList());
+  SUIT_SentenceMatcher::makeFuzzyWords(*myWords, *myFuzzyWords);
+
+  if (!myUseExactWordOrder) {
+    myFuzzyPermutatedSentences.reset(new QList<QStringList>());
+    SUIT_SentenceMatcher::makePermutatedSentences(*myFuzzyWords, *myFuzzyPermutatedSentences);
+  }
+}
+
+void SUIT_SentenceMatcher::setCaseSensitive(bool theOn)
+{
+  myIsCaseSensitive = theOn;
+}
+
+void SUIT_SentenceMatcher::setQuery(QString theQuery)
+{
+  theQuery = theQuery.simplified();
+  if (theQuery == myQuery)
+    return;
+
+  myQuery = theQuery;
+
+  { // Set exact words.
+    if (myWords)
+      myWords->clear();
+    else
+      myWords.reset(new QStringList());
+
+    *myWords = theQuery.split(" ", QString::SkipEmptyParts);
+  }
+
+  { // Set permutated sentences.
+    if (myUseExactWordOrder)
+      myPermutatedSentences.reset(nullptr);
+    else {
+      if (!myPermutatedSentences)
+        myPermutatedSentences.reset(new QList<QStringList>());
+
+      SUIT_SentenceMatcher::makePermutatedSentences(*myWords, *myPermutatedSentences);
+    }
+  }
+
+  // Set fuzzy words and sentences.
+  if (myUseFuzzyWords) {
+    if (!myFuzzyWords)
+      myFuzzyWords.reset(new QStringList());
+
+    SUIT_SentenceMatcher::makeFuzzyWords(*myWords, *myFuzzyWords);
+
+    if (myUseExactWordOrder)
+      myFuzzyPermutatedSentences.reset(nullptr);
+    else {
+      if (!myFuzzyPermutatedSentences)
+        myFuzzyPermutatedSentences.reset(new QList<QStringList>());
+
+      SUIT_SentenceMatcher::makePermutatedSentences(*myFuzzyWords, *myFuzzyPermutatedSentences);
+    }
+  }
+  else {
+    myFuzzyWords.reset(nullptr);
+    myFuzzyPermutatedSentences.reset(nullptr);
+  }
+}
+
+size_t SUIT_SentenceMatcher::match(const QString& theInputString) const
+{
+  size_t n = 0;
+  if (myUseExactWordOrder) {
+    n = SUIT_SentenceMatcher::match(theInputString, *myWords, myIsCaseSensitive);
+    if (n > 0)
+      return n;
+
+    if (myUseFuzzyWords) {
+      n = SUIT_SentenceMatcher::match(theInputString, *myFuzzyWords, myIsCaseSensitive);
+      if (n > 0)
+        return n;
+    }
+  }
+  else /* if match with permutated query sentences */ {
+    n = SUIT_SentenceMatcher::match(theInputString, *myPermutatedSentences, myIsCaseSensitive);
+    if (n > 0)
+      return n;
+
+    if (myUseFuzzyWords) {
+      n = SUIT_SentenceMatcher::match(theInputString, *myFuzzyPermutatedSentences, myIsCaseSensitive);
+      if (n > 0)
+        return n;
+    }
+  }
+
+  return n;
+}
+
+/*static*/ bool SUIT_SentenceMatcher::makePermutatedSentences(const QStringList& theWords, QList<QStringList>& theSentences)
+{
+  theSentences.clear();
+  theSentences.push_back(theWords);
+  QStringList nextPerm = theWords;
+  QStringList prevPerm = theWords;
+
+  bool hasNextPerm = true;
+  bool hasPrevPerm = true;
+
+  while (hasNextPerm || hasPrevPerm) {
+    if (hasNextPerm)
+      hasNextPerm = std::next_permutation(nextPerm.begin(), nextPerm.end());
+
+    if (hasNextPerm && !theSentences.contains(nextPerm))
+      theSentences.push_back(nextPerm);
+
+    if (hasPrevPerm)
+      hasPrevPerm = std::prev_permutation(prevPerm.begin(), prevPerm.end());
+
+    if (hasPrevPerm && !theSentences.contains(prevPerm))
+      theSentences.push_back(prevPerm);
+  }
+
+  return theSentences.size() > 1;
+}
+
+/*static*/ void SUIT_SentenceMatcher::makeFuzzyWords(const QStringList& theWords, QStringList& theFuzzyWords)
+{
+  theFuzzyWords.clear();
+  for (const QString& word : theWords) {
+    QString fuzzyWord;
+    for (int i = 0; i < word.size(); i++) {
+      fuzzyWord += word[i];
+      fuzzyWord += "\\w*";
+    }
+    theFuzzyWords.push_back(fuzzyWord);
+  }
+}
+
+/*static*/ size_t SUIT_SentenceMatcher::matchWithSentenceIgnoreEndings(const QString& theInputString, const QStringList& theSentence, bool theCaseSensitive)
+{
+  QRegExp regExp("^" + theSentence.join("\\w*\\W+"), theCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive);
+  if (theInputString.contains(regExp))
+    return theSentence.size();
+  else
+    return 0;
+}
+
+/*static*/ size_t SUIT_SentenceMatcher::matchWithSentencesIgnoreEndings(const QString& theInputString, const QList<QStringList>& theSentences, bool theCaseSensitive)
+{
+  for (const QStringList& sentence : theSentences) {
+    if (SUIT_SentenceMatcher::matchWithSentenceIgnoreEndings(theInputString, sentence, theCaseSensitive))
+      return sentence.size();
+  }
+  return 0;
+}
+
+/*static*/ size_t SUIT_SentenceMatcher::matchAtLeastOneWord(const QString& theInputString, const QStringList& theWords, bool theCaseSensitive)
+{
+  size_t n = 0;
+  for (const QString& word : theWords) {
+    // The same input word can be counted multiple times. Nobody cares.
+    if (theInputString.contains(QRegExp(word, theCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive)))
+      n++;
+  }
+  return n;
+}
+
+/*static*/ size_t SUIT_SentenceMatcher::match(
+  const QString& theInputString,
+  const QStringList& theSentence,
+  bool theCaseSensitive
+) {
+  size_t n = SUIT_SentenceMatcher::matchWithSentenceIgnoreEndings(theInputString, theSentence, theCaseSensitive);
+  if (n > 0)
+    return n;
+
+  return SUIT_SentenceMatcher::matchAtLeastOneWord(theInputString, theSentence, theCaseSensitive);
+}
+
+/*static*/ size_t SUIT_SentenceMatcher::match(
+  const QString& theInputString,
+  const QList<QStringList>& theSentences,
+  bool theCaseSensitive
+) {
+  size_t n = SUIT_SentenceMatcher::matchWithSentencesIgnoreEndings(theInputString, theSentences, theCaseSensitive);
+  if (n > 0)
+    return n;
+
+  if (theSentences.size())
+    return SUIT_SentenceMatcher::matchAtLeastOneWord(theInputString, theSentences[0], theCaseSensitive);
+  else
+    return 0;
+}
+
+
+SUIT_ActionSearcher::AssetsAndSearchData::AssetsAndSearchData(std::shared_ptr<SUIT_ActionAssets> theAssets, size_t theNumOfMatchingWords)
+: myAssets(theAssets), myNumOfMatchingWords(theNumOfMatchingWords)
+{}
+
+SUIT_ActionSearcher::SUIT_ActionSearcher()
+{
+  myIncludedModuleIDs = { SUIT_ShortcutMgr::ROOT_MODULE_ID };
+  myIncludeDisabledActions = false;
+  myFieldsToMatch = { SUIT_ActionSearcher::MatchField::Name, SUIT_ActionSearcher::MatchField::ToolTip };
+  myMatcher.setCaseSensitive(false);
+  myMatcher.setUseExactWordOrder(false);
+  myMatcher.setUseFuzzyWords(true);
+}
+
+bool SUIT_ActionSearcher::setIncludedModuleIDs(std::set<QString> theIncludedModuleIDs)
+{
+  if (myIncludedModuleIDs == theIncludedModuleIDs)
+    return false;
+
+  myIncludedModuleIDs = theIncludedModuleIDs;
+
+  bool res = false;
+  // Erase search results with excluded modules. Erase IDs of modules, which are already in search results, from theIncludedModuleIDs.
+  for (auto itFound = mySearchResults.begin(); itFound != mySearchResults.end(); ) {
+    const auto itModuleID = theIncludedModuleIDs.find(itFound->first);
+    if (itModuleID == theIncludedModuleIDs.end()) {
+      mySearchResults.erase(itFound);
+      res = true;
+    }
+    else {
+      itFound++;
+      theIncludedModuleIDs.erase(itModuleID);
+    }
+  }
+
+  // Filter assets of added modules.
+  const auto& allAssets = SUIT_ShortcutMgr::get()->getActionAssets();
+  for (const auto& moduleIDAndAssets : allAssets) {
+    const QString& moduleID = moduleIDAndAssets.first;
+    const auto& actionIDsAndAssets = moduleIDAndAssets.second;
+    if (theIncludedModuleIDs.find(moduleID) == theIncludedModuleIDs.end())
+      continue;
+
+    for (const auto& actionIDAndAssets : actionIDsAndAssets) {
+      const QString& inModuleActionID = actionIDAndAssets.first;
+      const size_t n = matchAction(moduleID, inModuleActionID, actionIDAndAssets.second);
+      if (n > 0) {
+        mySearchResults[moduleID][inModuleActionID] = SUIT_ActionSearcher::AssetsAndSearchData(actionIDAndAssets.second, n);
+        res = true;
+      }
+    }
+  }
+
+  return res;
+}
+
+bool SUIT_ActionSearcher::includeDisabledActions(bool theOn)
+{
+  if (myIncludeDisabledActions == theOn)
+    return false;
+
+  myIncludeDisabledActions = theOn;
+
+  if (myIncludeDisabledActions)
+    return extendResults();
+  else
+    return filterResults().first;
+}
+
+bool SUIT_ActionSearcher::setFieldsToMatch(const std::set<SUIT_ActionSearcher::MatchField>& theFields)
+{
+  if (myFieldsToMatch == theFields)
+    return false;
+
+  if (theFields.empty()) {
+    myFieldsToMatch = theFields;
+    mySearchResults.clear();
+    return true;
+  }
+
+  bool narrows = true;
+  for (const SUIT_ActionSearcher::MatchField field : theFields) {
+    if (myFieldsToMatch.find(field) == myFieldsToMatch.end()) {
+      narrows = false;
+      break;
+    }
+  }
+
+  bool extends = true;
+  for (const SUIT_ActionSearcher::MatchField field : myFieldsToMatch) {
+    if (theFields.find(field) == theFields.end()) {
+      extends = false;
+      break;
+    }
+  }
+
+  myFieldsToMatch = theFields;
+
+  if (narrows)
+    return filterResults().first;
+  else if (extends)
+    return extendResults();
+  else
+    return filter().first;
+}
+
+bool SUIT_ActionSearcher::setCaseSensitive(bool theOn)
+{
+  if (myMatcher.isCaseSensitive() == theOn)
+    return false;
+
+  myMatcher.setCaseSensitive(theOn);
+
+  if (theOn)
+    return filterResults().first;
+  else
+    return extendResults();
+}
+
+bool SUIT_ActionSearcher::setQuery(const QString& theQuery)
+{
+  if (theQuery.simplified() == myMatcher.getQuery().simplified())
+    return false;
+
+  myMatcher.setQuery(theQuery);
+  return filter().first;
+}
+
+const std::map<QString, std::map<QString, SUIT_ActionSearcher::AssetsAndSearchData>>& SUIT_ActionSearcher::getSearchResults() const
+{
+  return mySearchResults;
+}
+
+std::pair<bool, bool> SUIT_ActionSearcher::filter()
+{
+  auto res = std::pair<bool, bool>(false, false);
+
+  for (const auto& moduleIDAndAssets : SUIT_ShortcutMgr::get()->getActionAssets()) {
+    const auto& moduleID = moduleIDAndAssets.first;
+    if (myIncludedModuleIDs.find(moduleID) == myIncludedModuleIDs.end())
+      continue;
+
+    const auto& actionIDsAndAssets = moduleIDAndAssets.second;
+
+    auto itFoundModuleIDAndAssets = mySearchResults.find(moduleID);
+    for (const auto& actionIDAndAssets : actionIDsAndAssets) {
+      const QString& inModuleActionID = actionIDAndAssets.first;
+
+      if (itFoundModuleIDAndAssets != mySearchResults.end()) {
+        auto& foundActionIDsAndAssets = itFoundModuleIDAndAssets->second;
+        auto itFoundActionIDAndAssets = foundActionIDsAndAssets.find(inModuleActionID);
+        if (itFoundActionIDAndAssets != foundActionIDsAndAssets.end()) {
+          // Action is already in search results.
+          SUIT_ActionSearcher::AssetsAndSearchData& aAndD = itFoundActionIDAndAssets->second;
+          const size_t n = matchAction(moduleID, inModuleActionID, aAndD.myAssets);
+          if (n > 0) {
+            if (n != aAndD.myNumOfMatchingWords) {
+              aAndD.myNumOfMatchingWords = n;
+              res.second = true;
+            }
+          }
+          else /* if n == 0 */ {
+            foundActionIDsAndAssets.erase(itFoundActionIDAndAssets);
+            res.first = true;
+          }
+          continue;
+        }
+      }
+
+      const size_t n = matchAction(moduleID, inModuleActionID, actionIDAndAssets.second);
+      if (n > 0) {
+        if (itFoundModuleIDAndAssets == mySearchResults.end())
+          itFoundModuleIDAndAssets = mySearchResults.emplace(moduleID, std::map<QString, SUIT_ActionSearcher::AssetsAndSearchData>()).first;
+
+        itFoundModuleIDAndAssets->second.emplace(inModuleActionID, SUIT_ActionSearcher::AssetsAndSearchData(actionIDAndAssets.second, n));
+        res.first = true;
+      }
+    }
+  }
+
+  return res;
+}
+
+std::pair<bool, bool> SUIT_ActionSearcher::filterResults()
+{
+  auto res = std::pair<bool, bool>(false, false);
+
+  for (auto itFoundModuleIDAndAssets = mySearchResults.begin(); itFoundModuleIDAndAssets != mySearchResults.end(); ) {
+    const QString& moduleID = itFoundModuleIDAndAssets->first;
+    auto& actionIDsAndAssets = itFoundModuleIDAndAssets->second;
+    for (auto itActionIDAndAssets = actionIDsAndAssets.begin(); itActionIDAndAssets != actionIDsAndAssets.end(); ) {
+      const QString& inModuleActionID = itActionIDAndAssets->first;
+      SUIT_ActionSearcher::AssetsAndSearchData& assetsAndSearchData = itActionIDAndAssets->second;
+      const size_t n = matchAction(moduleID, inModuleActionID, assetsAndSearchData.myAssets);
+      if (n == 0) {
+        actionIDsAndAssets.erase(itActionIDAndAssets);
+        res.first = true;
+      }
+      else {
+        if (assetsAndSearchData.myNumOfMatchingWords != n) {
+          assetsAndSearchData.myNumOfMatchingWords = n;
+          res.second = true;
+        }
+        itActionIDAndAssets++;
+      }
+    }
+
+    if (actionIDsAndAssets.empty())
+      mySearchResults.erase(itFoundModuleIDAndAssets);
+    else
+      itFoundModuleIDAndAssets++;
+  }
+
+  return res;
+}
+
+bool SUIT_ActionSearcher::extendResults()
+{
+  bool res = false;
+  for (const auto& moduleIDAndAssets : SUIT_ShortcutMgr::get()->getActionAssets()) {
+    const auto& moduleID = moduleIDAndAssets.first;
+    if (myIncludedModuleIDs.find(moduleID) == myIncludedModuleIDs.end())
+      continue;
+
+    const auto& actionIDsAndAssets = moduleIDAndAssets.second;
+
+    auto itFoundModuleIDAndAssets = mySearchResults.find(moduleID);
+    for (const auto& actionIDAndAssets : actionIDsAndAssets) {
+      const QString& inModuleActionID = actionIDAndAssets.first;
+
+      if (itFoundModuleIDAndAssets != mySearchResults.end()) {
+        const auto& foundActionIDsAndAssets = itFoundModuleIDAndAssets->second;
+        if (foundActionIDsAndAssets.find(inModuleActionID) != foundActionIDsAndAssets.end())
+          continue; // Action is already in search results.
+      }
+
+      const size_t n = matchAction(moduleID, inModuleActionID, actionIDAndAssets.second);
+      if (n > 0) {
+        if (itFoundModuleIDAndAssets == mySearchResults.end())
+          itFoundModuleIDAndAssets = mySearchResults.emplace(moduleID, std::map<QString, SUIT_ActionSearcher::AssetsAndSearchData>()).first;
+
+        itFoundModuleIDAndAssets->second.emplace(inModuleActionID, SUIT_ActionSearcher::AssetsAndSearchData(actionIDAndAssets.second, n));
+        res = true;
+      }
+    }
+  }
+  return res;
+}
+
+size_t SUIT_ActionSearcher::matchAction(const QString& theModuleID, const QString& theInModuleActionID, std::shared_ptr<SUIT_ActionAssets> theAssets)
+{
+  if (!myIncludeDisabledActions) {
+    const auto& actions = SUIT_ShortcutMgr::get()->getActions(theModuleID, theInModuleActionID);
+    const bool actionEnabled = std::find_if(actions.begin(), actions.end(), [](const QAction* const theAction){ return theAction->isEnabled(); } ) != actions.end();
+    if (!actionEnabled)
+      return false;
+  }
+
+  for (const auto& langAndLDA : theAssets->myLangDependentAssets) {
+    if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::ToolTip) != myFieldsToMatch.end()) {
+      if (myMatcher.match(langAndLDA.second.myToolTip))
+        return true;
+    }
+
+    if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::Name) != myFieldsToMatch.end()) {
+      if (myMatcher.match(langAndLDA.second.myName))
+        return true;
+    }
+  }
+
+  if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::ID) != myFieldsToMatch.end()) {
+    if (myMatcher.match(SUIT_ShortcutMgr::makeActionID(theModuleID, theInModuleActionID)))
+      return true;
+  }
+
+  if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::KeySequence) != myFieldsToMatch.end()) {
+    const QString keySequence = SUIT_ShortcutMgr::get()->getKeySequence(theModuleID, theInModuleActionID).toString();
+    if (myMatcher.match(keySequence))
+      return true;
+  }
+
+  return false;
 }
\ No newline at end of file
index c8debfb81b1094fb5681980cfbe95fb7954c80b6..ea4d287d4aa843a9d70c708ef4a99f4f59c2a500 100644 (file)
@@ -27,6 +27,7 @@
 
 #include <QObject>
 #include <QString>
+#include <QStringList>
 #include <QIcon>
 #include <map>
 #include <set>
@@ -43,7 +44,7 @@ class QJsonObject;
 #endif
 
 // Define SHORTCUT_MGR_DBG to enable SUIT_ShortcutMgr debug logging.
-// #define SHORTCUT_MGR_DBG
+#define SHORTCUT_MGR_DBG
 /*! \returns true, if SUIT_ShortcutMgr debug logging is enabled. */
 SUIT_EXPORT extern inline bool ShCutDbg() {
 #ifdef SHORTCUT_MGR_DBG
@@ -238,6 +239,8 @@ protected:
   virtual ~SUIT_ShortcutMgr();
 
 public:
+  static const QString ROOT_MODULE_ID;
+
   /*! \brief Create new singleton-instance of shortcut manager, if it has not been created. */
   static void Init();
 
@@ -366,6 +369,10 @@ public:
 
   std::shared_ptr<const SUIT_ActionAssets> getActionAssets(const QString& theActionID) const;
 
+  std::map<QString, std::map<QString, std::shared_ptr<SUIT_ActionAssets>>> getActionAssets() const { return myActionAssets; }
+
+  std::map<QString, std::shared_ptr<SUIT_ActionAssets>> getModuleAssets() const { return myModuleAssets; }
+
   /*! \brief Retrieves action name, if the asset was loaded using \ref setAssetsFromResources(). If theLang is empty, it is effectively current language. */
   QString getActionName(const QString& theModuleID, const QString& theInModuleActionID, const QString& theLang = "") const;
 
@@ -462,13 +469,162 @@ private:
   Sets of moduleIDs and inModuleActionIDs may NOT be equal for myActions and myShortcutContainer.
   */
 
-  /* {actionID, assets}[] */
-  std::map<QString, std::shared_ptr<SUIT_ActionAssets>> myActionAssets;
+  /* { moduleID, {inModuleActionID, assets}[] }[] */
+  std::map<QString, std::map<QString, std::shared_ptr<SUIT_ActionAssets>>> myActionAssets;
 
   /* {moduleID, assets}[] */
   mutable std::map<QString, std::shared_ptr<SUIT_ActionAssets>> myModuleAssets;
 };
 
+
+/*!
+  \class SUIT_SentenceMatcher
+  \brief Approximate string matcher, treats strings as sentences composed of words.
+*/
+class SUIT_EXPORT SUIT_SentenceMatcher
+{
+public:
+  /*! Default config:
+    Exact word order = false;
+    Fuzzy words = true;
+    Case sensitive = false;
+    Query = ""; // matches nothing.
+  */
+  SUIT_SentenceMatcher();
+
+  void setUseExactWordOrder(bool theOn);
+  void setUseFuzzyWords(bool theOn);
+  void setCaseSensitive(bool theOn);
+  inline bool isCaseSensitive() const { return myIsCaseSensitive; };
+
+  /*! \param theQuery should not be regex. */
+  void setQuery(QString theQuery);
+
+  inline const QString& getQuery() const { return myQuery; };
+
+  /*! \returns number of matched words. */
+  size_t match(const QString& theInputString) const;
+
+private:
+  static bool makePermutatedSentences(const QStringList& theWords, QList<QStringList>& theSentences);
+  static void makeFuzzyWords(const QStringList& theWords, QStringList& theFuzzyWords);
+
+  static size_t matchWithSentenceIgnoreEndings(const QString& theInputString, const QStringList& theSentence, bool theCaseSensitive);
+  static size_t matchWithSentencesIgnoreEndings(const QString& theInputString, const QList<QStringList>& theSentences, bool theCaseSensitive);
+
+  static size_t matchAtLeastOneWord(const QString& theInputString, const QStringList& theWords, bool theCaseSensitive);
+
+  static size_t match(
+    const QString& theInputString,
+    const QStringList& theSentence,
+    bool theCaseSensitive
+  );
+
+  static size_t match(
+    const QString& theInputString,
+    const QList<QStringList>& theSentences,
+    bool theCaseSensitive
+  );
+
+  bool myUseExactWordOrder; // If false, try to match with sentences, composed of query's words in different orders.
+  bool myUseFuzzyWords; // Try to match with sentences, composed of query's truncated words.
+  bool myIsCaseSensitive;
+  QString myQuery;
+
+  std::unique_ptr<QStringList> myWords; // It is also original search sentence.
+  std::unique_ptr<QList<QStringList>> myPermutatedSentences;
+
+  std::unique_ptr<QStringList> myFuzzyWords; // Regexes.
+  std::unique_ptr<QList<QStringList>> myFuzzyPermutatedSentences;
+};
+
+
+typedef std::map<QString, std::map<QString, std::shared_ptr<SUIT_ActionAssets>>> SUIT_ActionAssetsMap2;
+
+
+/*!
+  \class SUIT_ActionSearcher
+  \brief Searches in data, provided in action asset files and shortcut preferences.
+*/
+class SUIT_EXPORT SUIT_ActionSearcher
+{
+public:
+  enum MatchField {
+    ID,
+    Name,
+    ToolTip,
+    KeySequence
+  };
+
+  struct AssetsAndSearchData
+  {
+    AssetsAndSearchData() = default;
+    AssetsAndSearchData(std::shared_ptr<SUIT_ActionAssets> theAssets, size_t theNumOfMatchingWords);
+
+    std::shared_ptr<SUIT_ActionAssets> myAssets;
+    size_t myNumOfMatchingWords;
+  };
+
+  /*! Default config:
+      Included modules' IDs = { ROOT_MODULE_ID };
+      Include disabled actions = false;
+      Fields to match = { Name, Tooltip };
+      Case sensitive = false;
+      Fuzzy matching = true;
+      Query = ""; // matches everything.
+  */
+  SUIT_ActionSearcher();
+  SUIT_ActionSearcher(const SUIT_ActionSearcher&) = delete;
+  SUIT_ActionSearcher& operator=(const SUIT_ActionSearcher&) = delete;
+  virtual ~SUIT_ActionSearcher() = default;
+
+  /*! \returns true, if set of results is changed. */
+  bool setIncludedModuleIDs(std::set<QString> theIncludedModuleIDs);
+
+  /*! \returns true, if set of results is changed. */
+  bool includeDisabledActions(bool theOn);
+  inline bool areDisabledActionsIncluded() const {return myIncludeDisabledActions;};
+
+  /*! \returns true, if set of results is changed. */
+  bool setFieldsToMatch(const std::set<SUIT_ActionSearcher::MatchField>& theFields);
+
+  /*! \returns true, if set of results is changed. */
+  bool setCaseSensitive(bool theOn);
+
+  /*! \returns true, if set of results is changed. */
+  bool setQuery(const QString& theQuery);
+  inline const QString& getQuery() const {return myMatcher.getQuery();};
+
+  const std::map<QString, std::map<QString, SUIT_ActionSearcher::AssetsAndSearchData>>& getSearchResults() const;
+
+
+private:
+  /*! \brief Applies filter to all actions, provided in asset files for SUIT_ShortcutMgr.
+  \returns { true, _ } if set of results is changed; { _ , true } if number of matching words is changed for at least one result. */
+  std::pair<bool, bool> filter();
+
+  /*! \brief Applies filter to search results only.
+  \returns { true, _ } if set of results is shrunk; { _ , true } if number of matching words is changed for at least one result. */
+  std::pair<bool, bool> filterResults();
+
+  /*! \brief Applies filter only to actions, which are not in search results.
+  \returns True, if set of results is extended. */
+  bool extendResults();
+
+  size_t matchAction(const QString& theModuleID, const QString& theInModuleActionID, std::shared_ptr<SUIT_ActionAssets> theAssets);
+
+
+  std::set<QString> myIncludedModuleIDs;
+  bool myIncludeDisabledActions;
+
+  std::set<SUIT_ActionSearcher::MatchField> myFieldsToMatch;
+  SUIT_SentenceMatcher myMatcher;
+
+  /* { moduleID, {inModuleActionID, assetsAndSearchData}[] }[]. */
+  std::map<QString, std::map<QString, SUIT_ActionSearcher::AssetsAndSearchData>> mySearchResults;
+};
+
+
 #if defined WIN32
 #pragma warning( default: 4251 )
 #endif