1 // Copyright (C) 2007-2024 CEA, EDF, OPEN CASCADE
3 // This library is free software; you can redistribute it and/or
4 // modify it under the terms of the GNU Lesser General Public
5 // License as published by the Free Software Foundation; either
6 // version 2.1 of the License, or (at your option) any later version.
8 // This library is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 // Lesser General Public License for more details.
13 // You should have received a copy of the GNU Lesser General Public
14 // License along with this library; if not, write to the Free Software
15 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 // See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
20 #include "SUIT_FindActionDialog.h"
27 #include <QApplication>
35 #include <QHeaderView>
42 SUIT_FindActionDialog::SUIT_FindActionDialog(QWidget* theParent)
46 setWindowTitle(tr("Find action"));
47 QVBoxLayout* layout = new QVBoxLayout(this);
49 myQueryLineEdit = new QLineEdit(this);
50 layout->addWidget(myQueryLineEdit);
51 myQueryLineEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
52 setFocusProxy(myQueryLineEdit);
54 QHBoxLayout* searchOptionsLayout = new QHBoxLayout(this);
55 layout->addLayout(searchOptionsLayout);
56 myIncludeUnavailableActionsCB = new QCheckBox(tr("Unavailable actions"), this);
57 myIncludeUnavailableActionsCB->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
58 myIncludeUnavailableActionsCB->setCheckState(Qt::CheckState::Checked);
59 myActionSearcher.includeDisabledActions(true);
60 myIncludeInactiveModulesCB = new QCheckBox(tr("Inactive modules"), this);
61 myIncludeInactiveModulesCB->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
62 myIncludeInactiveModulesCB->setCheckState(Qt::CheckState::Unchecked);
63 searchOptionsLayout->addWidget(myIncludeUnavailableActionsCB);
64 searchOptionsLayout->addWidget(myIncludeInactiveModulesCB);
66 myFoundActionsTree = new SUIT_FoundActionTree(this);
67 layout->addWidget(myFoundActionsTree);
69 connect(myQueryLineEdit, SIGNAL(textChanged(const QString&)), this, SLOT(onQueryChanged(const QString&)));
70 connect(myIncludeUnavailableActionsCB, SIGNAL(stateChanged(int)), this, SLOT(onSearchOptionUnavailableActionsChanged(int)));
71 connect(myIncludeInactiveModulesCB, SIGNAL(stateChanged(int)), this, SLOT(onSearchOptionInactiveModulesChanged(int)));
73 myQueryLineEdit->installEventFilter(myFoundActionsTree);
76 void SUIT_FindActionDialog::setActiveModuleID(const QString& theModuleID)
78 myActiveModuleID = theModuleID;
79 if(myActionSearcher.setIncludedModuleIDs(std::set<QString>({SUIT_ShortcutMgr::ROOT_MODULE_ID, myActiveModuleID})))
83 void SUIT_FindActionDialog::onQueryChanged(const QString& theQuery)
85 if (myActionSearcher.setQuery(theQuery))
89 void SUIT_FindActionDialog::onSearchOptionUnavailableActionsChanged(int theState)
91 if (myActionSearcher.includeDisabledActions(theState == Qt::CheckState::Checked))
95 void SUIT_FindActionDialog::onSearchOptionInactiveModulesChanged(int theState)
97 bool resultsChanged = false;
98 if (theState == Qt::CheckState::Checked) {
99 myIncludeUnavailableActionsCB->setDisabled(true);
100 myIncludeUnavailableActionsCB->setCheckState(Qt::CheckState::Checked);
101 resultsChanged = myActionSearcher.setIncludedModuleIDs(SUIT_ShortcutMgr::get()->getShortcutContainer().getIDsOfAllModules());
104 myIncludeUnavailableActionsCB->setDisabled(false);
105 resultsChanged = myActionSearcher.setIncludedModuleIDs(std::set<QString>({SUIT_ShortcutMgr::ROOT_MODULE_ID, myActiveModuleID}));
112 void SUIT_FindActionDialog::updateUI()
114 myFoundActionsTree->updateItems(myActionSearcher.getSearchResults());
119 SUIT_FoundActionTree::SUIT_FoundActionTree(SUIT_FindActionDialog* theParent)
120 : QTreeWidget(theParent)
123 setSelectionMode(QAbstractItemView::SingleSelection);
124 setSortingEnabled(false);
125 header()->setSectionResizeMode(QHeaderView::Interactive);
127 QMap<int, QString> labelMap;
128 labelMap[SUIT_FoundActionTree::ElementIdx::Name] = SUIT_FindActionDialog::tr("Action");
129 labelMap[SUIT_FoundActionTree::ElementIdx::ToolTip] = SUIT_FindActionDialog::tr("Description");
130 setHeaderLabels(labelMap.values());
132 setExpandsOnDoubleClick(false); // Implemented manually.
133 setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
134 setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
136 setColumnWidth(SUIT_FoundActionTree::ElementIdx::Name, 120);
137 setColumnWidth(SUIT_FoundActionTree::ElementIdx::Name, 250);
138 setMinimumHeight(300);
140 setWindowFlags(windowFlags() | Qt::FramelessWindowHint);
142 mySortKey = SUIT_FoundActionTree::SortKey::MatchMetrics;
143 mySortOrder = SUIT_FoundActionTree::SortOrder::Ascending;
145 connect(this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*, int)), this, SLOT(onItemExecuted(QTreeWidgetItem*, int)));
148 /*! \brief Compensates lack of std::distance(), which is introduced in C++17.
149 \returns -1, if theIt does not belong to the */
150 template <class Container>
152 const Container& theContainer,
153 const typename Container::iterator& theIt
155 auto it = theContainer.begin();
157 while (it != theContainer.end()) {
167 void SUIT_FoundActionTree::updateItems(const std::map<QString, std::map<QString, SUIT_ActionSearcher::AssetsAndSearchData>>& theAssets)
169 std::set<QString> shownModuleIDs; // To sort module-items by their IDs.
171 // Remove shown module items, if updated search results have no matching actions from these modules.
172 for (int moduleIdx = 0; moduleIdx < topLevelItemCount(); ) {
173 SUIT_FoundActionTreeModule* moduleItem = static_cast<SUIT_FoundActionTreeModule*>(topLevelItem(moduleIdx));
174 myModuleItemExpansionStates[moduleItem->myModuleID] = moduleItem->isExpanded();
176 const auto itUpdatedAssetsOfShownModule = theAssets.find(moduleItem->myModuleID);
177 if (itUpdatedAssetsOfShownModule == theAssets.end()) {
178 delete takeTopLevelItem(moduleIdx);
182 if (itUpdatedAssetsOfShownModule->second.empty()) {
183 delete takeTopLevelItem(moduleIdx);
187 shownModuleIDs.emplace(moduleItem->myModuleID);
191 const auto shortcutMgr = SUIT_ShortcutMgr::get();
192 const QString lang = SUIT_ShortcutMgr::getLang();
194 SUIT_FoundActionTreeAction* preselectedActionItem = nullptr;
196 for (const auto& moduleIDAndAssets : theAssets) {
197 const QString& moduleID = moduleIDAndAssets.first;
198 const auto& moduleAssets = moduleIDAndAssets.second;
199 if (moduleAssets.empty())
202 const auto moduleItemAndIdx = findModuleItem(moduleID);
203 SUIT_FoundActionTreeModule* moduleItem = moduleItemAndIdx.first;
205 moduleItem = new SUIT_FoundActionTreeModule(moduleID);
206 moduleItem->setAssetsAndSearchData(SUIT_ActionSearcher::AssetsAndSearchData(shortcutMgr->getModuleAssets(moduleID)), lang);
208 const auto emplaceRes = shownModuleIDs.emplace(moduleID);
209 insertTopLevelItem(indexOf(shownModuleIDs, emplaceRes.first), moduleItem);
211 moduleItem->setFlags(Qt::ItemIsEnabled);
213 const auto itExpansionState = myModuleItemExpansionStates.find(moduleID);
214 if (itExpansionState == myModuleItemExpansionStates.end())
215 moduleItem->setExpanded(true); // Make module item expanded at first appearance.
217 moduleItem->setExpanded(itExpansionState->second);
219 else /* if the tree has the module-item */ {
220 const auto actionItems = moduleItem->takeChildren();
221 for (const auto actionItem : actionItems) {
226 // Fill module item with action items.
227 auto sortedActionItems = createActionSetWithComparator();
228 for (const auto& actionIDAndAssets : moduleAssets) {
229 const QString& inModuleActionID = actionIDAndAssets.first;
230 const SUIT_ActionSearcher::AssetsAndSearchData& assetsAndSearchData = actionIDAndAssets.second;
232 auto actionItem = SUIT_FoundActionTreeAction::create(moduleID, inModuleActionID);
234 ShCutDbg("SUIT_FoundActionTree can't create child item for action ID = \"" + SUIT_ShortcutMgr::makeActionID(moduleID, inModuleActionID) + "\".");
238 actionItem->setAssetsAndSearchData(assetsAndSearchData, lang);
239 sortedActionItems.emplace(actionItem);
242 SUIT_FoundActionTreeAction* preselectedActionItemCand = nullptr;
243 for (const auto actionItem : sortedActionItems) {
244 moduleItem->addChild(actionItem);
246 // Consider first ranked available action in the module (if user did not collapsed it) as a candidate for preselected action.
247 if (!preselectedActionItemCand && moduleItem->isExpanded() && actionItem->isEnabledBufferedValue())
248 preselectedActionItemCand = actionItem;
251 if (preselectedActionItem) {
252 if (preselectedActionItemCand) {
253 if (preselectedActionItemCand->matchMetrics() < preselectedActionItem->matchMetrics())
254 preselectedActionItem = preselectedActionItemCand;
258 preselectedActionItem = preselectedActionItemCand;
261 if (preselectedActionItem)
262 setCurrentItem(preselectedActionItem);
265 void SUIT_FoundActionTree::sort(SUIT_FoundActionTree::SortKey theKey, SUIT_FoundActionTree::SortOrder theOrder)
267 if (theKey == mySortKey && theOrder == mySortOrder)
271 mySortOrder = theOrder;
273 for (int moduleIdx = 0; moduleIdx < topLevelItemCount(); moduleIdx++) {
274 const auto moduleItem = static_cast<SUIT_FoundActionTreeModule*>(topLevelItem(moduleIdx));
276 auto sortedActionItems = createActionSetWithComparator();
277 for (int childIdx = 0; childIdx < moduleItem->childCount(); childIdx++) {
278 SUIT_FoundActionTreeAction* const actionItem = static_cast<SUIT_FoundActionTreeAction*>(moduleItem->child(childIdx));
279 sortedActionItems.emplace(actionItem);
282 moduleItem->takeChildren();
284 for (const auto actionItem : sortedActionItems) {
285 moduleItem->addChild(actionItem);
290 void SUIT_FoundActionTree::keyPressEvent(QKeyEvent* theEvent)
292 const auto key = theEvent->key();
293 const auto selectedItem = currentItem();
294 if ((key == Qt::Key_Enter || key == Qt::Key_Return) && selectedItem)
295 onItemExecuted(selectedItem, SUIT_FoundActionTree::ElementIdx::Name);
297 QTreeWidget::keyPressEvent(theEvent);
300 bool SUIT_FoundActionTree::eventFilter(QObject* theQObject, QEvent* theEvent)
302 if (theEvent->type() == QEvent::KeyPress) {
303 QKeyEvent* const keyEvent = static_cast<QKeyEvent*>(theEvent);
304 const auto key = keyEvent->key();
306 if (key == Qt::Key_Enter || key == Qt::Key_Return || key == Qt::Key_Up || key == Qt::Key_Down) {
307 keyPressEvent(keyEvent);
315 std::pair<SUIT_FoundActionTreeModule*, int> SUIT_FoundActionTree::findModuleItem(const QString& theModuleID) const
317 for (int moduleIdx = 0; moduleIdx < topLevelItemCount(); moduleIdx++) {
318 SUIT_FoundActionTreeModule* moduleItem = static_cast<SUIT_FoundActionTreeModule*>(topLevelItem(moduleIdx));
319 if (moduleItem->myModuleID == theModuleID)
320 return std::pair<SUIT_FoundActionTreeModule*, int>(moduleItem, moduleIdx);
322 return std::pair<SUIT_FoundActionTreeModule*, int>(nullptr, -1);
325 template <typename Float>
326 bool approximatelyEqual(Float a, Float b, Float relativeTol = std::numeric_limits<Float>::epsilon())
328 return std::abs(a - b) <= ( (std::abs(a) < std::abs(b) ? std::abs(b) : std::abs(a)) * relativeTol);
331 std::set<SUIT_FoundActionTreeAction*, std::function<bool(SUIT_FoundActionTreeAction*, SUIT_FoundActionTreeAction*)>> SUIT_FoundActionTree::createActionSetWithComparator() const
333 QList<std::pair<SUIT_FoundActionTree::SortKey, SUIT_FoundActionTree::SortOrder>> sortSchema = SUIT_FoundActionTree::DEFAULT_SORT_SCHEMA;
335 for (auto itSameKey = sortSchema.begin(); itSameKey != sortSchema.end(); itSameKey++) {
336 if (itSameKey->first == mySortKey) {
337 sortSchema.erase(itSameKey);
341 sortSchema.push_front(std::pair<SUIT_FoundActionTree::SortKey, SUIT_FoundActionTree::SortOrder>(mySortKey, mySortOrder));
344 static const QCollator collator;
345 const std::function<bool(SUIT_FoundActionTreeAction*, SUIT_FoundActionTreeAction*)> comparator =
346 [sortSchema, &collator](const SUIT_FoundActionTreeAction* theItemA, const SUIT_FoundActionTreeAction* theItemB) {
347 for (const auto& keyAndOrder : sortSchema) {
348 const QVariant fieldOfA = theItemA->getValue(keyAndOrder.first);
349 const QVariant fieldOfB = theItemB->getValue(keyAndOrder.first);
351 bool* const fieldOfAIsDouble = new bool(false);
352 bool* const fieldOfBIsDouble = new bool(false);
353 const double matchMetricsA = fieldOfA.toDouble(fieldOfAIsDouble);
354 const double matchMetricsB = fieldOfB.toDouble(fieldOfBIsDouble);
355 if (*fieldOfAIsDouble && *fieldOfBIsDouble) {
356 if (!approximatelyEqual(matchMetricsA, matchMetricsB)) {
357 const double res = matchMetricsA - matchMetricsB;
358 return keyAndOrder.second == SUIT_FoundActionTree::SortOrder::Ascending ? res < 0 : res > 0;
362 const int res = collator.compare(fieldOfA.toString(), fieldOfB.toString());
364 return keyAndOrder.second == SUIT_FoundActionTree::SortOrder::Ascending ? res < 0 : res > 0;
370 return std::set<SUIT_FoundActionTreeAction*, std::function<bool(SUIT_FoundActionTreeAction*, SUIT_FoundActionTreeAction*)>>(comparator);
373 void SUIT_FoundActionTree::onItemExecuted(QTreeWidgetItem* theItem, int theColIdx)
375 SUIT_FoundActionTreeItem* const item = static_cast<SUIT_FoundActionTreeItem*>(theItem);
376 if (item->type() == SUIT_FoundActionTreeItem::Type::Action) {
377 SUIT_FoundActionTreeAction* const actionItem = static_cast<SUIT_FoundActionTreeAction*>(theItem);
378 if (actionItem->trigger())
379 static_cast<SUIT_FindActionDialog*>(parentWidget())->accept();
381 else /* if (item->type() == SUIT_FoundActionTreeItem::Type::Module) */ {
382 item->setExpanded(!item->isExpanded());
386 /*static*/ const QList<std::pair<SUIT_FoundActionTree::SortKey, SUIT_FoundActionTree::SortOrder>> SUIT_FoundActionTree::DEFAULT_SORT_SCHEMA =
388 {SUIT_FoundActionTree::SortKey::MatchMetrics, SUIT_FoundActionTree::SortOrder::Ascending},
389 {SUIT_FoundActionTree::SortKey::Name, SUIT_FoundActionTree::SortOrder::Ascending},
390 {SUIT_FoundActionTree::SortKey::ToolTip, SUIT_FoundActionTree::SortOrder::Ascending},
391 {SUIT_FoundActionTree::SortKey::ID, SUIT_FoundActionTree::SortOrder::Ascending}
395 SUIT_FoundActionTreeItem::SUIT_FoundActionTreeItem(const QString& theModuleID)
396 : QTreeWidgetItem(), myModuleID(theModuleID)
399 QString SUIT_FoundActionTreeItem::name() const
401 return text(SUIT_FoundActionTree::ElementIdx::Name);
404 QString SUIT_FoundActionTreeItem::toolTip() const
406 return text(SUIT_FoundActionTree::ElementIdx::ToolTip);
410 SUIT_FoundActionTreeModule::SUIT_FoundActionTreeModule(const QString& theModuleID)
411 : SUIT_FoundActionTreeItem(theModuleID)
413 QFont f = font(SUIT_FoundActionTree::ElementIdx::Name);
415 setFont(SUIT_FoundActionTree::ElementIdx::Name, f);
416 setText(SUIT_FoundActionTree::ElementIdx::Name, theModuleID);
419 void SUIT_FoundActionTreeModule::setAssetsAndSearchData(const SUIT_ActionSearcher::AssetsAndSearchData& theAssetsAndSD, const QString& theLang)
421 if (!theAssetsAndSD.myAssets)
424 setIcon(SUIT_FoundActionTree::ElementIdx::Name, theAssetsAndSD.myAssets->myIcon);
426 const auto& ldaMap = theAssetsAndSD.myAssets->myLangDependentAssets;
427 if (ldaMap.empty()) {
428 setText(SUIT_FoundActionTree::ElementIdx::Name, myModuleID);
432 auto itLDA = ldaMap.find(theLang);
433 if (itLDA == ldaMap.end())
434 itLDA = ldaMap.begin();
436 const SUIT_ActionAssets::LangDependentAssets& lda = itLDA->second;
437 const QString& name = lda.myName.isEmpty() ? myModuleID : lda.myName;
438 setText(SUIT_FoundActionTree::ElementIdx::Name, name);
441 QVariant SUIT_FoundActionTreeModule::getValue(SUIT_FoundActionTree::SortKey theKey) const
444 case SUIT_FoundActionTree::SortKey::MatchMetrics:
446 case SUIT_FoundActionTree::SortKey::ID:
448 case SUIT_FoundActionTree::SortKey::Name:
450 case SUIT_FoundActionTree::SortKey::ToolTip:
457 bool SUIT_FoundActionTreeModule::isEnabled() const
463 SUIT_FoundActionTreeAction::SUIT_FoundActionTreeAction(const QString& theModuleID, const QString& theInModuleActionID)
464 : SUIT_FoundActionTreeItem(theModuleID), myInModuleActionID(theInModuleActionID),
465 myMatchMetrics(std::numeric_limits<double>::infinity()), myIsEnabledBufferedValue(false)
467 setText(SUIT_FoundActionTree::ElementIdx::Name, theInModuleActionID);
470 /*static*/ SUIT_FoundActionTreeAction* SUIT_FoundActionTreeAction::create(const QString& theModuleID, const QString& theInModuleActionID)
472 if (theInModuleActionID.isEmpty()) {
473 ShCutDbg("SUIT_FoundActionTreeItem: attempt to create item with empty action ID.");
477 return new SUIT_FoundActionTreeAction(theModuleID, theInModuleActionID);
480 void SUIT_FoundActionTreeAction::setAssetsAndSearchData(const SUIT_ActionSearcher::AssetsAndSearchData& theAssetsAndSD, const QString& theLang)
482 if (!theAssetsAndSD.myAssets)
485 setIcon(SUIT_FoundActionTree::ElementIdx::Name, theAssetsAndSD.myAssets->myIcon);
487 const auto& ldaMap = theAssetsAndSD.myAssets->myLangDependentAssets;
488 if (ldaMap.empty()) {
489 setText(SUIT_FoundActionTree::ElementIdx::Name, myInModuleActionID);
493 auto itLDA = ldaMap.find(theLang);
494 if (itLDA == ldaMap.end())
495 itLDA = ldaMap.begin();
497 const SUIT_ActionAssets::LangDependentAssets& lda = itLDA->second;
498 const QString& name = lda.myName.isEmpty() ? myInModuleActionID : lda.myName;
499 setText(SUIT_FoundActionTree::ElementIdx::Name, name);
501 setText(SUIT_FoundActionTree::ElementIdx::ToolTip, lda.myToolTip);
505 SUIT_FoundActionTree::ElementIdx::Name,
506 SUIT_FoundActionTree::tr("Double click to start")
510 SUIT_FoundActionTree::ElementIdx::ToolTip,
511 SUIT_FoundActionTree::tr("Double click to start")
515 static const QBrush greyedOutBrush = QBrush(Qt::gray);
516 setForeground(SUIT_FoundActionTree::ElementIdx::Name, greyedOutBrush);
517 setForeground(SUIT_FoundActionTree::ElementIdx::ToolTip, greyedOutBrush);
520 myMatchMetrics = theAssetsAndSD.matchMetrics();
523 QVariant SUIT_FoundActionTreeAction::getValue(SUIT_FoundActionTree::SortKey theKey) const
526 case SUIT_FoundActionTree::SortKey::MatchMetrics:
527 return myMatchMetrics;
528 case SUIT_FoundActionTree::SortKey::ID:
529 return myInModuleActionID;
530 case SUIT_FoundActionTree::SortKey::Name:
532 case SUIT_FoundActionTree::SortKey::ToolTip:
539 bool SUIT_FoundActionTreeAction::isEnabled() const
541 const auto& actions = SUIT_ShortcutMgr::get()->getActions(myModuleID, myInModuleActionID);
542 myIsEnabledBufferedValue = std::find_if(actions.begin(), actions.end(), [](const QAction* const theAction){ return theAction->isEnabled(); }) != actions.end();
543 return myIsEnabledBufferedValue;
546 bool SUIT_FoundActionTreeAction::trigger() const
549 const auto& actions = SUIT_ShortcutMgr::get()->getActions(myModuleID, myInModuleActionID);
550 for (const auto& action : actions) {
551 if (action->isEnabled()) {