diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index aaae96398..ad1c62a0b 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -65,6 +65,7 @@ set(HEADERS plugins/interfaces/filter_plugin.h plugins/interfaces/io_plugin.h plugins/interfaces/render_plugin.h + plugins/action_searcher.h plugins/meshlab_plugin_type.h plugins/plugin_manager.h python/function.h @@ -81,8 +82,7 @@ set(HEADERS ml_selection_buffers.h ml_thread_safe_memory_info.h mlapplication.h - mlexception.h - searcher.h) + mlexception.h) set(SOURCES ml_document/helpers/mesh_document_state_data.cpp @@ -107,6 +107,7 @@ set(SOURCES plugins/interfaces/decorate_plugin.cpp plugins/interfaces/filter_plugin.cpp plugins/interfaces/io_plugin.cpp + plugins/action_searcher.cpp plugins/meshlab_plugin_type.cpp plugins/plugin_manager.cpp python/function.cpp @@ -121,8 +122,7 @@ set(SOURCES filterscript.cpp ml_selection_buffers.cpp ml_thread_safe_memory_info.cpp - mlapplication.cpp - searcher.cpp) + mlapplication.cpp) set(RESOURCES meshlab-common.qrc) diff --git a/src/common/plugins/action_searcher.cpp b/src/common/plugins/action_searcher.cpp new file mode 100644 index 000000000..5450e26f3 --- /dev/null +++ b/src/common/plugins/action_searcher.cpp @@ -0,0 +1,176 @@ +/***************************************************************************** + * MeshLab o o * + * A versatile mesh processing toolbox o o * + * _ O _ * + * Copyright(C) 2005-2022 \/)\/ * + * Visual Computing Lab /\/| * + * ISTI - Italian National Research Council | * + * \ * + * All rights reserved. * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 General Public License (http://www.gnu.org/licenses/gpl.txt) * + * for more details. * + * * + ****************************************************************************/ + +#include "action_searcher.h" + +#include +#include + +#include "interfaces/filter_plugin.h" + +ActionSearcher::ActionSearcher() +{ +} + +void ActionSearcher::clear() +{ + titleActionsMap.clear(); +} + +/** + * @brief Adds the given action to the ActionSearcher, allowing queries to it. + * @param action + */ +void ActionSearcher::addAction(QAction *action, bool usePythonFilterNames) +{ + if (action != nullptr) { + if (!usePythonFilterNames) { + // add title to the action map + QString title = action->text(); + title = title.toLower(); + title.remove(ignexp); + QStringList res = title.split(sepexp, Qt::SkipEmptyParts); + res.removeDuplicates(); + addSubStrings(res); + for (const QString& str : qAsConst(res)) { + titleActionsMap[str].push_back(action); + } + } + else { + // if the action is a filter, we should add also the python name to the search + QObject* parent = action->parent(); + FilterPlugin* fp = qobject_cast(parent); + if (fp) { + QString title = fp->pythonFilterName(action); + title.replace("_", " "); + title.remove(ignexp); + QStringList res = title.split(sepexp, Qt::SkipEmptyParts); + res.removeDuplicates(); + addSubStrings(res); + for (const QString& str : qAsConst(res)) { + titleActionsMap[str].push_back(action); + } + } + } + + // add info to the action map + QString info = action->toolTip(); + info = info.toLower(); + info.remove(ignexp); + QStringList res = info.split(sepexp, Qt::SkipEmptyParts); + res.removeDuplicates(); + addSubStrings(res); + for (const QString& str : qAsConst(res)) { + infoActionsMap[str].push_back(action); + } + } +} + +/** + * @brief Performs a query using the inputString and returns an array containing the best matching + * actions matching to the input string. + * + * The array will have maximum size equal to maxNumberactions. + * @param inputString: query string + * @param maxNumberActions: maximum size of the output array + * @return array containing the best matching actions + */ +std::vector ActionSearcher::bestMatchingActions(QString inputString, int maxNumberActions) const +{ + std::vector res; + + // clean the input string + inputString = inputString.toLower(); + inputString.replace("_", " "); // to allow python search + inputString.remove(ignexp); + + // split the input string + QStringList inputList = inputString.split(sepexp, Qt::SkipEmptyParts); + inputList.removeDuplicates(); + + const float bonuspertitleword = 1.0f / std::pow(10,inputList.size()); + + // store the ranking for each action + std::map actionRanking; + + // for each input string + for (const QString& str : qAsConst(inputList)) { + auto it = titleActionsMap.find(str); + // if matches in a title of an action + if (it != titleActionsMap.end()) { + for (QAction* act : it->second) { // for each matching action, increment its ranking + actionRanking[act] += bonuspertitleword; + } + } + it = infoActionsMap.find(str); + // if matches in a info of an action + if (it != infoActionsMap.end()) { + for (QAction* act : it->second) { // for each matching action, increment its ranking + actionRanking[act]++; + } + } + } + + std::map> rankingMap; // flipped map of actionRanking + // populate the flipped map + for (const auto& p : actionRanking) { + // for each ranking, push another action havint that ranking + rankingMap[p.second].push_back(p.first); + } + // at the end, the map will store the actions stored by ranking + + int count = 0; // used to stop the number of inserted actions in the resulting vector + + // reverse iteration: start from the biggest ranking + for (auto it = rankingMap.rbegin(); it != rankingMap.rend() && count < maxNumberActions; ++it) { + auto v1 = it->second; + // need to sort properly actions having the same ranking, since they would be sorted using + // memory addresses, that are not deterministic: we use action titles + std::sort(v1.begin(), v1.end(), ActionComparator()); + // insert the sorted actions to the result vector + res.insert(res.end(), v1.begin(), v1.end()); + count += it->second.size(); + } + // if at the end the number exceeded the maximum number, we truncate the array + if (count >= maxNumberActions) { + res.resize(maxNumberActions); + } + return res; +} + +void ActionSearcher::addSubStrings(QStringList &res) +{ + QStringList resWithPrefix; + foreach(QString str, res) + { + QString strPref = str; + resWithPrefix.push_back(strPref); + for(int i=0;i +#include +#include +#include + +class ActionSearcher +{ +public: + ActionSearcher(); + + void clear(); + void addAction(QAction* action, bool usePythonFilterNames = false); + + std::vector bestMatchingActions(QString inputString, int maxNumberActions) const; + +private: + const QRegExp sepexp = QRegExp("\\W+"); + const QRegExp ignexp = QRegExp( + "\\b(an|the|of|it|as|in|by|and|or|for)\\b|\\b[a-z]\\b|'s\\b|\\.|<[^>]*>"); + + // map that stores, for each string, all the actions that store that string in their titles + std::map> titleActionsMap; + + // map that stores, for each stirng, all the actions that store that stirng in their info + std::map> infoActionsMap; + + struct ActionComparator { + bool operator()(QAction* a1, QAction* a2) { + return a1->text() < a2->text(); + } + }; + + static void addSubStrings(QStringList& res); +}; + +#endif // ACTION_SEARCHER_H diff --git a/src/common/searcher.cpp b/src/common/searcher.cpp deleted file mode 100644 index 37754dd4c..000000000 --- a/src/common/searcher.cpp +++ /dev/null @@ -1,151 +0,0 @@ -#include "searcher.h" -#include "mlexception.h" - -#include - -WordActionsMap::WordActionsMap() -:wordacts() -{ - -} - -void WordActionsMap::addWordsPerAction(QAction& act,const QStringList& words) -{ - foreach(QString word,words) - wordacts[word].push_back(&act); -} - -void WordActionsMap::removeActionReferences(QAction& act ) -{ - for(QMap >::iterator it = wordacts.begin();it != wordacts.end();++it) - it.value().removeAll(&act); -} - -bool WordActionsMap::getActionsPerWord( const QString& word,QList& res ) const -{ - QMap< QString,QList >::const_iterator it = wordacts.find(word); - if (it != wordacts.end()) - { - res = it.value(); - return true; - } - return false; -} - -void WordActionsMap::clear() -{ - wordacts.clear(); -} - -WordActionsMapAccessor::WordActionsMapAccessor() -:map(),sepexp(),ignexp() -{ - sepexp.setPattern("\\W+"); - ignexp.setPattern("\\b(an|the|of|it|as|in|by|and|or|for)\\b|\\b[a-z]\\b|'s\\b|\\.|<[^>]*>"); -} - -void WordActionsMapAccessor::addWordsPerAction(QAction& act,const QString& st ) -{ - QStringList wlist; - purifiedSplit(st,wlist); - addSubStrings(wlist); - map.addWordsPerAction(act,wlist); -} - -int WordActionsMapAccessor::rankedMatchesPerInputString( const QString& input,RankedMatches& rm ) const -{ - QStringList inputlist; - purifiedSplit(input,inputlist); - return rm.computeRankedMatches(inputlist,map); -} - -void WordActionsMapAccessor::purifiedSplit( const QString& input,QStringList& res ) const -{ - res.clear(); - QString tmp = input; - tmp = tmp.toLower(); - tmp.remove(ignexp); - res = tmp.split(sepexp,QString::SkipEmptyParts); - res.removeDuplicates(); -} - -void WordActionsMapAccessor::addSubStrings( QStringList& res ) const -{ - QStringList resWithPrefix; - foreach(QString str, res) - { - QString strPref = str; - resWithPrefix.push_back(strPref); - for(int i=0;i wordmatchesperaction; - ranking.clear(); - int inputstsize = inputst.size(); - ranking.resize(inputstsize); - float bonuspertitleword = 0.0f; - if (matchesontitlearemoreimportant) - bonuspertitleword = 1.0f / std::pow(10,inputstsize); - foreach(const QString& st,inputst) - { - QList res; - bool found = map.getActionsPerWord(st,res); - if (found) - { - foreach(QAction* act, res) - { - ++wordmatchesperaction[act]; - QString title = act->text().toLower().trimmed(); - if (title.contains(st)) - wordmatchesperaction[act] = wordmatchesperaction[act] + bonuspertitleword; - } - } - } - - QMap > rankedmatches; - for (QMap::iterator it = wordmatchesperaction.begin(); it != wordmatchesperaction.end(); ++it) - rankedmatches[it.value()].push_back(it.key()); - - int maxindex = -1; - for(QMap >::iterator it = rankedmatches.end() - 1;it != rankedmatches.begin() - 1;--it) - { - int index = std::floor(it.key()) - 1; - if (index >= ranking.size()) - { - throw InvalidInvariantException("WARNING! Index contained in wordmatchesperaction it's out-of-bound."); - return 0; - } - if (index > maxindex) - maxindex = index; - ranking[index].append(it.value()); - } - return maxindex + 1; -} - -void RankedMatches::getActionsWithNMatches( const int n,QList& res ) -{ - res.clear(); - int index = n -1; - if ((index >= ranking.size()) || (n < 1)) - { - throw InvalidInvariantException(QString("WARNING! Parameter n MUST be in the range [1..") + QString::number(ranking.size()) + "]."); - return; - } - res = ranking[index]; -} - - diff --git a/src/common/searcher.h b/src/common/searcher.h deleted file mode 100644 index ce01d4d2a..000000000 --- a/src/common/searcher.h +++ /dev/null @@ -1,58 +0,0 @@ -#ifndef SEARCHER_H -#define SEARCHER_H - -#include -#include -#include -#include -#include -#include -#include - -class WordActionsMap -{ -public: - WordActionsMap(); - void addWordsPerAction(QAction& act,const QStringList& words); - void removeActionReferences(QAction& act); - bool getActionsPerWord( const QString& word,QList& res ) const; - void clear(); -private: - QMap > wordacts; -}; - -class RankedMatches; - -class WordActionsMapAccessor -{ -public: - WordActionsMapAccessor(); - void addWordsPerAction(QAction& act,const QString& st); - inline void removeActionReferences(QAction& act) {map.removeActionReferences(act);} - inline void setSeparator(const QRegExp& sep) {sepexp = sep;} - inline void setIgnoredWords(const QRegExp& ign) {ignexp = ign;} - int rankedMatchesPerInputString(const QString& input,RankedMatches& rm) const; - inline QRegExp separtor() const {return sepexp;} - inline QRegExp ignored() const {return ignexp;} - void clear() {map.clear(); sepexp = QRegExp(); ignexp = QRegExp();} - -private: - void purifiedSplit(const QString& input,QStringList& res) const; - void addSubStrings(QStringList& res) const; - WordActionsMap map; - QRegExp sepexp; - QRegExp ignexp; -}; - -class RankedMatches -{ -public: - RankedMatches(); - void getActionsWithNMatches(const int n,QList& res); -private: - friend int WordActionsMapAccessor::rankedMatchesPerInputString(const QString& input,RankedMatches& rm) const; - int computeRankedMatches(const QStringList& inputst,const WordActionsMap& map,bool matchesontitlearemoreimportant = true); - QVector > ranking; -}; - -#endif diff --git a/src/meshlab/additionalgui.cpp b/src/meshlab/additionalgui.cpp index 64550695f..ed52e6557 100644 --- a/src/meshlab/additionalgui.cpp +++ b/src/meshlab/additionalgui.cpp @@ -8,7 +8,7 @@ #include #include -SearchMenu::SearchMenu(const WordActionsMapAccessor& wm,const int max,QWidget* parent,const int fixedwidth) +SearchMenu::SearchMenu(const ActionSearcher& wm,const int max,QWidget* parent,const int fixedwidth) :MenuWithToolTip(QString(),parent),searchline(NULL),wama(wm),maxres(max),fixedwidthsize(fixedwidth) { searchline = new MenuLineEdit(this); @@ -24,29 +24,14 @@ SearchMenu::SearchMenu(const WordActionsMapAccessor& wm,const int max,QWidget* p void SearchMenu::getResults(const QString& text,QList& result) { - try - { - RankedMatches rm; - int ii = wama.rankedMatchesPerInputString(text,rm); - int inserted = 0; - while(ii > 0) - { - QList myacts; - rm.getActionsWithNMatches(ii,myacts); - if (inserted + myacts.size() > maxres) - myacts = myacts.mid(0,myacts.size() - (inserted + myacts.size() - maxres)); - result.append(myacts); - QAction* sep = new QAction(this); - sep->setSeparator(true); - result.append(sep); - inserted += myacts.size(); - --ii; - } - } - catch(InvalidInvariantException& e) - { - qDebug() << "WARNING!!!!!!!!!!!!!!!!!!!" << e.what() << "\n"; - } + std::vector rm = wama.bestMatchingActions(text, 15); + + for (QAction* act : rm) { + result.append(act); + } + QAction* sep = new QAction(this); + sep->setSeparator(true); + result.append(sep); } void SearchMenu::updateGUI( const QList& results ) diff --git a/src/meshlab/additionalgui.h b/src/meshlab/additionalgui.h index bf5667dc5..c076803de 100644 --- a/src/meshlab/additionalgui.h +++ b/src/meshlab/additionalgui.h @@ -20,7 +20,7 @@ #include #include #include -#include "../common/searcher.h" +#include "../common/plugins/action_searcher.h" #include #include #include @@ -75,7 +75,7 @@ class SearchMenu : public MenuWithToolTip { Q_OBJECT public: - SearchMenu(const WordActionsMapAccessor& wm,const int max,QWidget* parent,const int fixedwidth = -1); + SearchMenu(const ActionSearcher& wm,const int max,QWidget* parent,const int fixedwidth = -1); int& searchLineWidth(); void clearResults(); QSize sizeHint () const; @@ -84,7 +84,7 @@ protected: void resizeEvent ( QResizeEvent * event); private: MenuLineEdit* searchline; - const WordActionsMapAccessor& wama; + const ActionSearcher& wama; int maxres; int fixedwidthsize; diff --git a/src/meshlab/mainwindow.h b/src/meshlab/mainwindow.h index 3f06f612e..0df44c450 100644 --- a/src/meshlab/mainwindow.h +++ b/src/meshlab/mainwindow.h @@ -361,7 +361,7 @@ public: QMenu* rasterLayerMenu() { return filterMenuRasterLayer; } private: - WordActionsMapAccessor wama; + ActionSearcher wama; //////// ToolBars /////////////// QToolBar* mainToolBar; QToolBar* decoratorToolBar; diff --git a/src/meshlab/mainwindow_Init.cpp b/src/meshlab/mainwindow_Init.cpp index 56bd70b14..b1c79cdd5 100644 --- a/src/meshlab/mainwindow_Init.cpp +++ b/src/meshlab/mainwindow_Init.cpp @@ -36,7 +36,6 @@ #include #include #include "mainwindow.h" -#include #include #include #include @@ -692,8 +691,7 @@ void MainWindow::initMenuForSearching(QMenu* menu) void MainWindow::initItemForSearching(QAction* act) { - QString tx = act->text() + " " + act->toolTip(); - wama.addWordsPerAction(*act, tx); + wama.addAction(act); } QString MainWindow::getDecoratedFileName(const QString& name)