From: dish Date: Wed, 15 May 2024 18:23:43 +0000 (+0000) Subject: [bos #40644][CEA](2024-T1) Feature search. X-Git-Url: http://git.salome-platform.org/gitweb/?a=commitdiff_plain;h=a6035aa5ed0bd4ab69d42b9faf2084c55ba774ba;p=modules%2Fgui.git [bos #40644][CEA](2024-T1) Feature search. Add Find Action dialog. Add icons' paths of GUI-actions to resource file. --- diff --git a/src/LightApp/resources/LightApp.xml b/src/LightApp/resources/LightApp.xml index 90062e08a..d831db007 100644 --- a/src/LightApp/resources/LightApp.xml +++ b/src/LightApp/resources/LightApp.xml @@ -287,6 +287,7 @@ + diff --git a/src/Qtx/CMakeLists.txt b/src/Qtx/CMakeLists.txt index f95706ef1..d62b4e1c2 100644 --- a/src/Qtx/CMakeLists.txt +++ b/src/Qtx/CMakeLists.txt @@ -22,10 +22,7 @@ INCLUDE(UseQtExt) # --- options --- # additional include directories -INCLUDE_DIRECTORIES( - ${QT_INCLUDES} - ${PROJECT_SOURCE_DIR}/src/SUIT -) +INCLUDE_DIRECTORIES(${QT_INCLUDES}) # additional preprocessor / compiler flags ADD_DEFINITIONS(${QT_DEFINITIONS}) @@ -73,7 +70,6 @@ SET(_moc_HEADERS QtxPopupMgr.h QtxRubberBand.h QtxSearchTool.h - QtxShortcutEdit.h QtxSlider.h QtxSplash.h QtxToolBar.h @@ -170,7 +166,6 @@ SET(_other_SOURCES QtxResourceMgr.cxx QtxRubberBand.cxx QtxSearchTool.cxx - QtxShortcutEdit.cxx QtxSlider.cxx QtxSplash.cxx QtxToolBar.cxx diff --git a/src/Qtx/QtxPagePrefMgr.cxx b/src/Qtx/QtxPagePrefMgr.cxx index b81bcfb19..a561c46a4 100644 --- a/src/Qtx/QtxPagePrefMgr.cxx +++ b/src/Qtx/QtxPagePrefMgr.cxx @@ -30,12 +30,9 @@ #include "QtxColorButton.h" #include "QtxBiColorTool.h" #include "QtxDoubleSpinBox.h" -#include "QtxShortcutEdit.h" #include "QtxBackgroundTool.h" #include "QtxResourceMgr.h" -#include "SUIT_ShortcutMgr.h" - #include #include #include @@ -4457,67 +4454,6 @@ void QtxPagePrefDateTimeItem::updateDateTime() } -/*! - \brief Creates preference item for editing of key bindings - \param theParent parent preference item. Must not be nullptr. -*/ -QtxPagePrefShortcutTreeItem::QtxPagePrefShortcutTreeItem(QtxPreferenceItem* theParent) - : QtxPagePrefItem(QString(), theParent) -{ - auto container = std::shared_ptr(); - const auto itContainers = QtxPagePrefShortcutTreeItem::shortcutContainers.find(rootItem()); - if (itContainers == QtxPagePrefShortcutTreeItem::shortcutContainers.end()) { - container.reset(new SUIT_ShortcutContainer()); - QtxPagePrefShortcutTreeItem::shortcutContainers.emplace(rootItem(), container); - } - else { - container = itContainers->second.lock(); - if (!container) { - container.reset(new SUIT_ShortcutContainer()); - itContainers->second = container; - } - } - - QtxShortcutTree* tree = new QtxShortcutTree(container); - tree->myModuleIDs = SUIT_ShortcutMgr::get()->getShortcutModuleIDs(); - setWidget(tree); -} - -/*! - \brief Retrieves shortcut preferences from ShortcutMgr. - Updates UI of controlling widget. - \sa store() -*/ -void QtxPagePrefShortcutTreeItem::retrieve() -{ - static_cast(widget())->setShortcutsFromManager(); -} - -/*! - \brief Retrieves shortcut preferences from resource files, ignoring user preferences. - Updates UI of controlling widget. - \sa store() -*/ -void QtxPagePrefShortcutTreeItem::retrieveDefault() -{ - static_cast(widget())->setDefaultShortcuts(); -} - -/*! - \brief Applies modified shortcut preferences to ShortcutMgr. - Updates UI of controlling widget. - And ShortcutMgr, in turn, serilizes shortcut preferences using the resource manager. - \sa retrieve() -*/ -void QtxPagePrefShortcutTreeItem::store() -{ - static_cast(widget())->applyChangesToShortcutMgr(); -} - -/*static*/ std::map> QtxPagePrefShortcutTreeItem::shortcutContainers = -std::map>(); - - /*! \class QtxPagePrefBackgroundItem \brief GUI implementation of the resources item to store background data. diff --git a/src/Qtx/QtxPagePrefMgr.h b/src/Qtx/QtxPagePrefMgr.h index b2b854b42..8acc9036b 100644 --- a/src/Qtx/QtxPagePrefMgr.h +++ b/src/Qtx/QtxPagePrefMgr.h @@ -42,7 +42,6 @@ class QtxGroupBox; class QtxComboBox; class QtxColorButton; class QtxBiColorTool; -class QtxShortcutTree; class QtxBackgroundTool; class QToolBox; @@ -743,37 +742,6 @@ private: QDateTimeEdit* myDateTime; }; - -class SUIT_ShortcutContainer; - - -class QTX_EXPORT QtxPagePrefShortcutTreeItem : public QtxPagePrefItem -{ -public: - QtxPagePrefShortcutTreeItem(QtxPreferenceItem* theParent); - virtual ~QtxPagePrefShortcutTreeItem() = default; - - virtual void retrieve(); - virtual void retrieveDefault(); - virtual void store(); - -private: - QtxShortcutTree* myShortcutTree; - - // { root item (preference window), shortcut container of synchronized trees (widgets within the same window) } - static std::map> shortcutContainers; - /** Why is this? - * Every QtxPagePrefMgr is eventually a preference window. Each preference window has button "Apply". - * When the button is pressed, all descendants of the QtxPagePrefMgr store changes they carry into preferences. - * The pitfall with shortcut trees is as follows: made in independent shortcut trees, changes may conflict, - * and merge of such changes is ambiguous. And the solution is to keep shortcut trees within the same window - * synchronized - all changes being made in a tree of a synchronized bundle are projected to other trees from the bundle - * without interacting with SUIT_ShortcutMgr. - * - * Every time shortcut preferences stored to the ShortcutMgr, all instances of QtxShortcutTree are updated. - */ -}; - class QTX_EXPORT QtxPagePrefBackgroundItem : public QObject, public QtxPageNamedPrefItem { Q_OBJECT diff --git a/src/Qtx/QtxShortcutEdit.cxx b/src/Qtx/QtxShortcutEdit.cxx deleted file mode 100644 index fe845b574..000000000 --- a/src/Qtx/QtxShortcutEdit.cxx +++ /dev/null @@ -1,883 +0,0 @@ -// 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 "QtxShortcutEdit.h" - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include - - -#define COLUMN_SIZE 500 - - -QtxKeySequenceEdit::QtxKeySequenceEdit(QWidget* parent) -: QFrame(parent) -{ - initialize(); - myKeySequenceLineEdit->installEventFilter(this); -} - -/*! \brief Set a key sequence to edit. */ -void QtxKeySequenceEdit::setConfirmedKeySequence(const QKeySequence& theKeySequence) -{ - myConfirmedKeySequenceString = theKeySequence.toString(); - myKeySequenceLineEdit->setText(myConfirmedKeySequenceString); - myPrevKeySequenceString = myConfirmedKeySequenceString; -} - -void QtxKeySequenceEdit::setEditedKeySequence(const QKeySequence& theKeySequence) -{ - const QString keySequenceString = theKeySequence.toString(); - myKeySequenceLineEdit->setText(keySequenceString); - myPrevKeySequenceString = keySequenceString; -} - -QKeySequence QtxKeySequenceEdit::editedKeySequence() const -{ - return QKeySequence::fromString(myKeySequenceLineEdit->text()); -} - -/*! \returns true, if the edited key sequence differs from confirmed one. */ -bool QtxKeySequenceEdit::isKeySequenceModified() const -{ - return QKeySequence(myConfirmedKeySequenceString) != editedKeySequence(); -} - -/*! \brief Set confirmed key sequence to line editor. */ -void QtxKeySequenceEdit::restoreKeySequence() -{ - myKeySequenceLineEdit->setText(myConfirmedKeySequenceString); - myPrevKeySequenceString = myConfirmedKeySequenceString; -} - -/*! - \brief Gets the key sequence from keys that were pressed - \param e a key event - \returns a string representation of the key sequence -*/ -/*static*/ QString QtxKeySequenceEdit::parseEvent(QKeyEvent* e) -{ - bool isShiftPressed = e->modifiers() & Qt::ShiftModifier; - bool isControlPressed = e->modifiers() & Qt::ControlModifier; - bool isAltPressed = e->modifiers() & Qt::AltModifier; - bool isMetaPressed = e->modifiers() & Qt::MetaModifier; - bool isModifiersPressed = isControlPressed || isAltPressed || isMetaPressed; // Do not treat Shift alone as a modifier! - int result=0; - if(isControlPressed) - result += Qt::CTRL; - if(isAltPressed) - result += Qt::ALT; - if(isShiftPressed) - result += Qt::SHIFT; - if(isMetaPressed) - result += Qt::META; - - int aKey = e->key(); - if ((isValidKey(aKey) && isModifiersPressed) || ((aKey >= Qt::Key_F1) && (aKey <= Qt::Key_F12))) - result += aKey; - - return QKeySequence(result).toString(); -} - -/*! - \brief Check if the key event contains a 'valid' key - \param theKey the code of the key - \returns \c true if the key is 'valid' -*/ -/*static*/ bool QtxKeySequenceEdit::isValidKey(int theKey) -{ - if ( theKey == Qt::Key_Underscore || theKey == Qt::Key_Escape || - ( theKey >= Qt::Key_Backspace && theKey <= Qt::Key_Delete ) || - ( theKey >= Qt::Key_Home && theKey <= Qt::Key_PageDown ) || - ( theKey >= Qt::Key_F1 && theKey <= Qt::Key_F12 ) || - ( theKey >= Qt::Key_Space && theKey <= Qt::Key_Asterisk ) || - ( theKey >= Qt::Key_Comma && theKey <= Qt::Key_Question ) || - ( theKey >= Qt::Key_A && theKey <= Qt::Key_AsciiTilde ) ) - return true; - return false; -} - -/*! \brief Called when "Clear" button is clicked. */ -void QtxKeySequenceEdit::onClear() -{ - myKeySequenceLineEdit->setText(""); - myPrevKeySequenceString = ""; - emit editingFinished(); -} - -/*! \brief Called when myKeySequenceLineEdit loses focus. */ -void QtxKeySequenceEdit::onEditingFinished() -{ - if (myKeySequenceLineEdit->text().endsWith("+")) - myKeySequenceLineEdit->setText(myPrevKeySequenceString); - else - myPrevKeySequenceString = myKeySequenceLineEdit->text(); - emit editingFinished(); -} - -/*! - \brief Custom event filter. - \param obj event receiver object - \param event event - \returns \c true if further event processing should be stopped -*/ -bool QtxKeySequenceEdit::eventFilter(QObject* theObject, QEvent* theEvent) -{ - if (theObject == myKeySequenceLineEdit) { - if (theEvent->type() == QEvent::KeyPress) { - QKeyEvent* keyEvent = static_cast(theEvent); - QString text = parseEvent(keyEvent); - if (keyEvent->key() == Qt::Key_Delete || keyEvent->key() == Qt::Key_Backspace) - myKeySequenceLineEdit->setText(""); - if (!text.isEmpty()) - myKeySequenceLineEdit->setText(text); - - emit editingStarted(); - return true; - } - if (theEvent->type() == QEvent::KeyRelease) { - onEditingFinished(); - return true; - } - } - return false; -} - -/* - \brief Perform internal intialization. -*/ -void QtxKeySequenceEdit::initialize() -{ - static const int PIXMAP_SIZE = 30; - - QHBoxLayout* base = new QHBoxLayout( this ); - base->setMargin(0); - base->setSpacing(5); - - base->addWidget(myKeySequenceLineEdit = new QLineEdit(this)); - setFocusProxy(myKeySequenceLineEdit); - - QToolButton* clearBtn = new QToolButton(); - auto clearPixmap = QPixmap(":/images/shortcut_disable.svg"); - clearPixmap.scaled(QSize(PIXMAP_SIZE, PIXMAP_SIZE), Qt::KeepAspectRatio, Qt::SmoothTransformation); - clearBtn->setIcon(clearPixmap); - clearBtn->setToolTip(tr("Disable shortcut.")); - base->addWidget(clearBtn); - - QToolButton* restoreBtn = new QToolButton(); - auto restorePixmap = QPixmap(":/images/shortcut_restore.svg"); - restorePixmap.scaled(QSize(PIXMAP_SIZE, PIXMAP_SIZE), Qt::KeepAspectRatio, Qt::SmoothTransformation); - restoreBtn->setIcon(restorePixmap); - restoreBtn->setToolTip(tr("Restore the currently applied key sequence.")); - base->addWidget(restoreBtn); - - myKeySequenceLineEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); - clearBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - restoreBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - - connect(clearBtn, SIGNAL(clicked()), this, SLOT(onClear())); - connect(restoreBtn, SIGNAL(clicked()), this, SIGNAL(restoreFromShortcutMgrClicked())); - connect(myKeySequenceLineEdit, SIGNAL(editingFinished()), this, SLOT(onEditingFinished())); -} - - -/*! \param theParent must not be nullptr. */ -QtxEditKeySequenceDialog::QtxEditKeySequenceDialog(QtxShortcutTree* theParent) -: QDialog(theParent) -{ - setMinimumWidth(500); - setWindowTitle(tr("Change key sequence")); - QVBoxLayout* layout = new QVBoxLayout(this); - myActionName = new QLabel(this); - myActionName->setTextFormat(Qt::RichText); - myKeySequenceEdit = new QtxKeySequenceEdit(this); - myTextEdit = new QTextEdit(this); - layout->addWidget(myActionName); - layout->addWidget(myKeySequenceEdit); - layout->addWidget(myTextEdit); - myActionName->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); - myKeySequenceEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); - myTextEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); - myTextEdit->setReadOnly(true); - myTextEdit->setAcceptRichText(true); - myTextEdit->setPlaceholderText(tr("No conflicts.")); - setFocusProxy(myKeySequenceEdit); - - QHBoxLayout* buttonLayout = new QHBoxLayout(this); - layout->addLayout(buttonLayout); - QPushButton* confirmButton = new QPushButton(tr("Confirm"), this); - confirmButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - QPushButton* cancelButton = new QPushButton(tr("Cancel"), this); - cancelButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - buttonLayout->addStretch(); - buttonLayout->addWidget(confirmButton); - buttonLayout->addWidget(cancelButton); - - connect(myKeySequenceEdit, SIGNAL(editingStarted()), this, SLOT(onEditingStarted())); - connect(myKeySequenceEdit, SIGNAL(editingFinished()), this, SLOT(onEditingFinished())); - connect(myKeySequenceEdit, SIGNAL(restoreFromShortcutMgrClicked()), this, SLOT(onRestoreFromShortcutMgr())); - connect(confirmButton, SIGNAL(clicked()), this, SLOT(onConfirm())); - connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject())); -} - -void QtxEditKeySequenceDialog::setModuleAndActionID(const QString& theModuleID, const QString& theInModuleActionID) -{ - myModuleID = theModuleID; - myInModuleActionID = theInModuleActionID; -} - -const QString& QtxEditKeySequenceDialog::moduleID() const { return myModuleID; } -const QString& QtxEditKeySequenceDialog::inModuleActionID() const { return myInModuleActionID; } - -void QtxEditKeySequenceDialog::setModuleAndActionName(const QString& theModuleName, const QString& theActionName, const QString& theActionToolTip) -{ - myActionName->setText("" + theModuleName + "  " + theActionName); - myActionName->setToolTip(theActionToolTip); -} - -void QtxEditKeySequenceDialog::setConfirmedKeySequence(const QKeySequence& theSequence) -{ - myKeySequenceEdit->setConfirmedKeySequence(theSequence); -} - -QKeySequence QtxEditKeySequenceDialog::editedKeySequence() const -{ - return myKeySequenceEdit->editedKeySequence(); -} - -int QtxEditKeySequenceDialog::exec() -{ - myKeySequenceEdit->setFocus(Qt::ActiveWindowFocusReason); - return QDialog::exec(); -} - -void QtxEditKeySequenceDialog::onEditingStarted() -{ - myTextEdit->setEnabled(false); -} - -void QtxEditKeySequenceDialog::onEditingFinished() -{ - updateConflictsMessage(); -} - -void QtxEditKeySequenceDialog::onRestoreFromShortcutMgr() -{ - const auto shortcutMgr = SUIT_ShortcutMgr::get(); - myKeySequenceEdit->setEditedKeySequence(shortcutMgr->getKeySequence(myModuleID, myInModuleActionID)); - updateConflictsMessage(); -} - -/*! Updates message with list of actions, whose shortcuts will be disabled on Confirm. */ -void QtxEditKeySequenceDialog::updateConflictsMessage() -{ - myTextEdit->setEnabled(true); - QTextDocument* doc = myTextEdit->document(); - if (!doc) { - doc = new QTextDocument(myTextEdit); - myTextEdit->setDocument(doc); - } - - if (!myKeySequenceEdit->isKeySequenceModified()) { - doc->clear(); - return; - } - - const QKeySequence newKeySequence = editedKeySequence(); - - const auto shortcutTree = static_cast(parentWidget()); - /** {moduleID, inModuleActionID}[] */ - std::set> conflicts = shortcutTree->shortcutContainer()->getConflicts(myModuleID, myInModuleActionID, newKeySequence); - if (!conflicts.empty()) { - const auto shortcutMgr = SUIT_ShortcutMgr::get(); - - QString report = "" + tr("These shortcuts will be disabled on confirm:") + ""; - { - report += "
    "; - for (const auto& conflict : conflicts) { - const QString conflictingModuleName = shortcutMgr->getModuleName(conflict.first); - const QString conflictingActionName = shortcutMgr->getActionName(conflict.first, conflict.second); - report += "
  • " + conflictingModuleName + "  " + conflictingActionName + "
  • "; - } - report += "
"; - } - doc->setHtml(report); - } - else /* if no conflicts */ { - doc->clear(); - } -} - -void QtxEditKeySequenceDialog::onConfirm() -{ - if (myKeySequenceEdit->isKeySequenceModified()) - accept(); - else - reject(); -} - - -/*! \brief Compensates lack of std::distance(), which is introduced in C++17. -\returns -1, if theIt does not belong to the */ -template -size_t indexOf( - const Container& theContainer, - const typename Container::iterator& theIt -) { - auto it = theContainer.begin(); - size_t distance = 0; - while (it != theContainer.end()) { - if (it == theIt) - return distance; - - it++; - distance++; - } - return -1; -} - - -/*! \param theContainer Share the same container between several trees, -to edit them synchronously even without exchange of changes with SUIT_ShortcutMgr. -Pass nullptr to create non-synchronized tree. */ -QtxShortcutTree::QtxShortcutTree( - std::shared_ptr theContainer, - QWidget* theParent -) : QTreeWidget(theParent), -myShortcutContainer(theContainer ? theContainer : std::shared_ptr(new SUIT_ShortcutContainer())), -mySortKey(QtxShortcutTree::SortKey::Name), mySortOrder(QtxShortcutTree::SortOrder::Ascending) -{ - setColumnCount(2); - setSelectionMode(QAbstractItemView::SingleSelection); - setColumnWidth(0, COLUMN_SIZE); - setSortingEnabled(false); // Items are sorted in the same way, as in ShortcutContainer. - header()->setSectionResizeMode(QHeaderView::Interactive); - { - QMap labelMap; - labelMap[QtxShortcutTree::ElementIdx::Name] = tr("Action"); - labelMap[QtxShortcutTree::ElementIdx::KeySequence] = tr("Key sequence"); - setHeaderLabels(labelMap.values()); - } - setExpandsOnDoubleClick(false); // Open shortcut editor on double click instead. - setSortingEnabled(false); - setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - myEditDialog = new QtxEditKeySequenceDialog(this); - - this->installEventFilter(this); - connect(this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*, int)), this, SLOT(onItemDoubleClicked(QTreeWidgetItem*, int))); - - QtxShortcutTree::instances[myShortcutContainer.get()].emplace(this); -} - -QtxShortcutTree::~QtxShortcutTree() -{ - QtxShortcutTree::instances[myShortcutContainer.get()].erase(this); - if (QtxShortcutTree::instances[myShortcutContainer.get()].empty()) - QtxShortcutTree::instances.erase(myShortcutContainer.get()); -} - -/*! \brief Copies shortcuts from ShortcutMgr. (Re)displays shortcuts of myModuleIDs. */ -void QtxShortcutTree::setShortcutsFromManager() -{ - const auto shortcutMgr = SUIT_ShortcutMgr::get(); - *myShortcutContainer = shortcutMgr->getShortcutContainer(); - // nb! ShortcutMgr never removes shortcuts from its container, only disables. - - updateItems(false /*theHighlightModified*/, true /*theUpdateSyncTrees*/); -} - -/*! \brief Copies shortcuts from resources, user files are not accounted. (Re)displays shortcuts of myModuleIDs. */ -void QtxShortcutTree::setDefaultShortcuts() -{ - SUIT_ShortcutContainer defaultShortcuts; - SUIT_ShortcutMgr::fillContainerFromPreferences(defaultShortcuts, true /*theDefaultOnly*/); - - myShortcutContainer->merge(defaultShortcuts, true /*theOverride*/, true /*theTreatAbsentIncomingAsDisabled*/); - // nb! SUIT_ShortcutContainer never erases shortcuts, only disables. - - updateItems(true /*theHighlightModified*/, true /*theUpdateSyncTrees*/); -} - -/*! \brief Applies pending changes to ShortcutMgr. Updates other instances of QtxShortcutTree. */ -void QtxShortcutTree::applyChangesToShortcutMgr() -{ - const auto mgr = SUIT_ShortcutMgr::get(); - mgr->mergeShortcutContainer(*myShortcutContainer); - - // Update non-synchronized with this instances. - for (const auto& containerAndSyncTrees : QtxShortcutTree::instances) { - if (containerAndSyncTrees.first == myShortcutContainer.get()) - continue; - - const std::set& syncTrees = containerAndSyncTrees.second; - const auto itFirstSyncTree = syncTrees.begin(); - if (itFirstSyncTree == syncTrees.end()) - continue; - - (*itFirstSyncTree)->setShortcutsFromManager(); - const auto editDialog = (*itFirstSyncTree)->myEditDialog; - editDialog->setConfirmedKeySequence(mgr->getShortcutContainer().getKeySequence(editDialog->moduleID(), editDialog->inModuleActionID())); - editDialog->updateConflictsMessage(); - } -} - -std::shared_ptr QtxShortcutTree::shortcutContainer() const -{ - return myShortcutContainer; -} - -/*! \brief Does not sort modules. */ -void QtxShortcutTree::sort(QtxShortcutTree::SortKey theKey, QtxShortcutTree::SortOrder theOrder) -{ - if (theKey == mySortKey && theOrder == mySortOrder) - return; - - mySortKey == theKey; - mySortOrder = theOrder; - - for (int moduleIdx = 0; moduleIdx < topLevelItemCount(); moduleIdx++) { - const auto moduleItem = static_cast(topLevelItem(moduleIdx)); - const auto sortedChildren = getSortedChildren(moduleItem); - moduleItem->takeChildren(); - - for (const auto childItem : sortedChildren) { - moduleItem->addChild(childItem); - } - } -} - -/*! \param If theUpdateSyncTrees, trees sharing the same shortcut container are updated. */ -void QtxShortcutTree::updateItems(bool theHighlightModified, bool theUpdateSyncTrees) -{ - const auto shortcutMgr = SUIT_ShortcutMgr::get(); - const QString lang = SUIT_ShortcutMgr::getLang(); - - for (const QString& moduleID : myModuleIDs) { - const auto& moduleShortcuts = myShortcutContainer->getModuleShortcutsInversed(moduleID); - if (moduleShortcuts.empty()) { - // Do not display empty module. - const auto moduleItemAndIdx = findModuleFolderItem(moduleID); - if (moduleItemAndIdx.second >= 0) - delete takeTopLevelItem(moduleItemAndIdx.second); - - continue; - } - - const auto moduleItemAndIdx = findModuleFolderItem(moduleID); - QtxShortcutTreeFolder* moduleItem = moduleItemAndIdx.first; - if (!moduleItem) { - moduleItem = new QtxShortcutTreeFolder(moduleID); - moduleItem->setAssets(shortcutMgr->getModuleAssets(moduleID), lang); - addTopLevelItem(moduleItem); - moduleItem->setFlags(Qt::ItemIsEnabled); - - auto sortedChildren = getSortedChildren(moduleItem); - for (const auto& shortcut : moduleShortcuts) { - const QString& inModuleActionID = shortcut.first; - const QKeySequence& keySequence = shortcut.second; - const QString keySequenceString = keySequence.toString(); - - auto actionItem = QtxShortcutTreeAction::create(moduleID, inModuleActionID); - if (!actionItem) { - ShCutDbg("QtxShortcutTree can't create child item for action ID = \"" + SUIT_ShortcutMgr::makeActionID(moduleID, inModuleActionID) + "\"."); - continue; - } - - actionItem->setAssets(shortcutMgr->getActionAssets(moduleID, inModuleActionID), lang); - actionItem->setKeySequence(keySequenceString); - - if (theHighlightModified) { - const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, inModuleActionID); - actionItem->highlightKeySequenceAsModified(keySequence != appliedKeySequence); - } - - insertChild(moduleItem, sortedChildren, actionItem); - } - - moduleItem->setExpanded(true); // Make tree expanded on first show. - } - else /* if the tree has the module-item */ { - for (int childIdx = 0; childIdx < moduleItem->childCount(); childIdx++) { - // Update exisiting items of a module. - QtxShortcutTreeAction* const childItem = static_cast(moduleItem->child(childIdx)); - const auto itShortcut = moduleShortcuts.find(childItem->myInModuleActionID); - if (itShortcut == moduleShortcuts.end()) { - // Shortcut of the item has been removed from myShortcutContainer - impossible. - continue; - } - const QKeySequence& newKeySequence = itShortcut->second; - const QString newKeySequenceString = newKeySequence.toString(); - if (childItem->keySequence() != newKeySequenceString) - childItem->setKeySequence(newKeySequenceString); - - if (theHighlightModified) { - const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, childItem->myInModuleActionID); - childItem->highlightKeySequenceAsModified(newKeySequence != appliedKeySequence); - } - else - childItem->highlightKeySequenceAsModified(false); - } - - // Add new items if myShortcutContainer acquired new shortcuts, which may happen if a developer forgot - // to add shortcuts for registered actions to resource files. - if (moduleItem->childCount() < moduleShortcuts.size()) { - auto sortedChildren = getSortedChildren(moduleItem); - for (const auto& shortcut : moduleShortcuts) { - const QString& inModuleActionID = shortcut.first; - const auto predicate = [&inModuleActionID](const QtxShortcutTreeItem* const theItem) -> bool { - return static_cast(theItem)->myInModuleActionID == inModuleActionID; - }; - - if (std::find_if(sortedChildren.begin(), sortedChildren.end(), predicate) == sortedChildren.end()) { - const auto actionItem = QtxShortcutTreeAction::create(moduleID, inModuleActionID); - if (!actionItem) { - ShCutDbg("QtxShortcutTree can't create child item for action ID = \"" + SUIT_ShortcutMgr::makeActionID(moduleID, inModuleActionID) + "\"."); - continue; - } - - const QKeySequence& keySequence = shortcut.second; - actionItem->setAssets(shortcutMgr->getActionAssets(moduleID, inModuleActionID), lang); - actionItem->setKeySequence(keySequence.toString()); - - if (theHighlightModified) { - const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, inModuleActionID); - actionItem->highlightKeySequenceAsModified(keySequence != appliedKeySequence); - } - - insertChild(moduleItem, sortedChildren, actionItem); - } - } - } - } - } - - if (theUpdateSyncTrees) { - const std::set& syncTrees = QtxShortcutTree::instances[myShortcutContainer.get()]; - for (const auto syncTree: syncTrees) { - if (syncTree == this) - continue; - - syncTree->updateItems(theHighlightModified, false /*theUpdateSyncTrees*/); - const auto editDialog = syncTree->myEditDialog; - editDialog->setConfirmedKeySequence(myShortcutContainer->getKeySequence(editDialog->moduleID(), editDialog->inModuleActionID())); - editDialog->updateConflictsMessage(); - } - } -} - -/*! \returns Pointer and index of top-level item. -If the tree does not contain an item with theModuleID, returns {nullptr, -1}. */ -std::pair QtxShortcutTree::findModuleFolderItem(const QString& theModuleID) const -{ - for (int moduleIdx = 0; moduleIdx < topLevelItemCount(); moduleIdx++) { - QtxShortcutTreeFolder* moduleItem = static_cast(topLevelItem(moduleIdx)); - if (moduleItem->myModuleID == theModuleID) - return std::pair(moduleItem, moduleIdx); - } - return std::pair(nullptr, -1); -} - -/*! \returns Children of theParentItem being sorted according to current sort mode and order. */ -std::set> QtxShortcutTree::getSortedChildren(QtxShortcutTreeFolder* theParentItem) -{ - QList> sortSchema = QtxShortcutTree::DEFAULT_SORT_SCHEMA; - { - for (auto itSameKey = sortSchema.begin(); itSameKey != sortSchema.end(); itSameKey++) { - if (itSameKey->first == mySortKey) { - sortSchema.erase(itSameKey); - break; - } - } - sortSchema.push_front(std::pair(mySortKey, mySortOrder)); - } - - static const QCollator collator; - const std::function comparator = - [this, sortSchema, &collator](const QtxShortcutTreeItem* theItemA, const QtxShortcutTreeItem* theItemB) { - int res = 0; - for (const auto& keyAndOrder : sortSchema) { - int res = 0; - res = collator.compare(theItemA->getValue(keyAndOrder.first), theItemB->getValue(keyAndOrder.first)); - if (res != 0) - return keyAndOrder.second == QtxShortcutTree::SortOrder::Ascending ? res < 0 : res > 0; - } - return false; - }; - - std::set> sortedChildren(comparator); - for (int childIdx = 0; childIdx < theParentItem->childCount(); childIdx++) { - QtxShortcutTreeAction* const childItem = static_cast(theParentItem->child(childIdx)); - sortedChildren.emplace(childItem); - } - return sortedChildren; -} - -/*! \brief Inserts theChildItem to theParentItem and theSortedChildren. -Does not check whether theSortedChildren are actually child items of theParentItem. -Does not check whether current item sort schema is same as one of theSortedChildren. */ -void QtxShortcutTree::insertChild( - QtxShortcutTreeFolder* theParentItem, - std::set>& theSortedChildren, - QtxShortcutTreeItem* theChildItem -) { - auto emplaceRes = theSortedChildren.emplace(theChildItem); - theParentItem->insertChild(indexOf(theSortedChildren, emplaceRes.first), theChildItem); -} - -void QtxShortcutTree::onItemDoubleClicked(QTreeWidgetItem* theItem, int theColIdx) -{ - { - QtxShortcutTreeItem* const item = static_cast(theItem); - // Do not react if folder-item is clicked. - if (item->type() != QtxShortcutTreeItem::Type::Action) - return; - } - - QtxShortcutTreeAction* const actionItem = static_cast(theItem); - - myEditDialog->setModuleAndActionID(actionItem->myModuleID, actionItem->myInModuleActionID); - QString actionToolTip = actionItem->toolTip(QtxShortcutTree::ElementIdx::Name); - actionToolTip.truncate(actionToolTip.lastIndexOf('\n') + 1); - myEditDialog->setModuleAndActionName( - static_cast(actionItem->parent())->name(), - actionItem->name(), - actionToolTip - ); - myEditDialog->setConfirmedKeySequence(QKeySequence::fromString(actionItem->keySequence())); - myEditDialog->updateConflictsMessage(); - const bool somethingChanged = myEditDialog->exec() == QDialog::Accepted; - - if (!somethingChanged) - return; - - const QKeySequence newKeySequence = myEditDialog->editedKeySequence(); - - /** { moduleID, inModuleActionID }[] */ - std::set> disabledActionIDs = myShortcutContainer->setShortcut(actionItem->myModuleID, actionItem->myInModuleActionID, newKeySequence, true /*override*/); - - /** { moduleID, {inModuleActionID, keySequence}[] }[] */ - std::map> changes; - changes[actionItem->myModuleID][actionItem->myInModuleActionID] = newKeySequence.toString(); - for (const auto moduleAndActionID : disabledActionIDs) { - changes[moduleAndActionID.first][moduleAndActionID.second] = QString(); - } - - // Set new key sequences to shortcut items. - for (const auto& moduleIDAndChanges : changes) { - const QString& moduleID = moduleIDAndChanges.first; - - const auto moduleItemAndIdx = findModuleFolderItem(moduleID); - const auto moduleItem = moduleItemAndIdx.first; - if (!moduleItem) - continue; - - /** {inModuleActionID, newKeySequence}[] */ - const std::map& moduleChanges = moduleIDAndChanges.second; - - // Go through module' shortcut items, and highlight those, whose key sequences differ from applied key sequences. - for (int childIdx = 0; childIdx < moduleItem->childCount(); childIdx++) { - QtxShortcutTreeAction* const childItem = static_cast(moduleItem->child(childIdx)); - const auto itChange = moduleChanges.find(childItem->myInModuleActionID); - if (itChange == moduleChanges.end()) { - // The shortcut has not been changed. - continue; - } - - childItem->setKeySequence(itChange->second); - - const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, childItem->myInModuleActionID); - childItem->highlightKeySequenceAsModified(QKeySequence::fromString(itChange->second) != appliedKeySequence); - } - } -} - -/*static*/ const QList> QtxShortcutTree::DEFAULT_SORT_SCHEMA = -{ - {QtxShortcutTree::SortKey::Name, QtxShortcutTree::SortOrder::Ascending}, - {QtxShortcutTree::SortKey::ToolTip, QtxShortcutTree::SortOrder::Ascending}, - {QtxShortcutTree::SortKey::KeySequence, QtxShortcutTree::SortOrder::Ascending}, - {QtxShortcutTree::SortKey::ID, QtxShortcutTree::SortOrder::Ascending} -}; - -/*static*/ std::map> QtxShortcutTree::instances = -std::map>(); - - - -QtxShortcutTreeItem::QtxShortcutTreeItem(const QString& theModuleID) -: QTreeWidgetItem(), myModuleID(theModuleID) -{ } - -QString QtxShortcutTreeItem::name() const -{ - return text(QtxShortcutTree::ElementIdx::Name); -} - - -QtxShortcutTreeFolder::QtxShortcutTreeFolder(const QString& theModuleID) -: QtxShortcutTreeItem(theModuleID) -{ - QFont f = font(QtxShortcutTree::ElementIdx::Name); - f.setBold(true); - setFont(QtxShortcutTree::ElementIdx::Name, f); - setText(QtxShortcutTree::ElementIdx::Name, theModuleID); -} - -void QtxShortcutTreeFolder::setAssets(std::shared_ptr theAssets, const QString& theLang) -{ - if (!theAssets) - return; - - setIcon(QtxShortcutTree::ElementIdx::Name, theAssets->myIcon); - - const auto& ldaMap = theAssets->myLangDependentAssets; - if (ldaMap.empty()) { - setText(QtxShortcutTree::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(QtxShortcutTree::ElementIdx::Name, name); -} - -QString QtxShortcutTreeFolder::getValue(QtxShortcutTree::SortKey theKey) const -{ - switch (theKey) { - case QtxShortcutTree::SortKey::ID: - return myModuleID; - case QtxShortcutTree::SortKey::Name: - return name(); - case QtxShortcutTree::SortKey::ToolTip: - return name(); - default: - return QString(); - } -} - - -QtxShortcutTreeAction::QtxShortcutTreeAction(const QString& theModuleID, const QString& theInModuleActionID) -: QtxShortcutTreeItem(theModuleID), myInModuleActionID(theInModuleActionID) -{ - setText(QtxShortcutTree::ElementIdx::Name, theInModuleActionID); - setToolTip( - QtxShortcutTree::ElementIdx::Name, - theInModuleActionID + (theInModuleActionID.at(theInModuleActionID.length()-1) == "." ? "\n" : ".\n") + QtxShortcutTree::tr("Double click to edit key sequence.") - ); - setToolTip(QtxShortcutTree::ElementIdx::KeySequence, QtxShortcutTree::tr("Double click to edit key sequence.")); -} - -/*static*/ QtxShortcutTreeAction* QtxShortcutTreeAction::create(const QString& theModuleID, const QString& theInModuleActionID) -{ - if (theInModuleActionID.isEmpty()) { - ShCutDbg("QtxShortcutTreeItem: attempt to create item with empty action ID."); - return nullptr; - } - - return new QtxShortcutTreeAction(theModuleID, theInModuleActionID); -} - -void QtxShortcutTreeAction::setAssets(std::shared_ptr theAssets, const QString& theLang) -{ - if (!theAssets) - return; - - setIcon(QtxShortcutTree::ElementIdx::Name, theAssets->myIcon); - - const auto& ldaMap = theAssets->myLangDependentAssets; - if (ldaMap.empty()) { - setText(QtxShortcutTree::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(QtxShortcutTree::ElementIdx::Name, name); - - const QString& actionToolTip = lda.myToolTip.isEmpty() ? name : lda.myToolTip; - setToolTip( - QtxShortcutTree::ElementIdx::Name, - actionToolTip + (actionToolTip.at(actionToolTip.length()-1) == "." ? "\n" : ".\n") + QtxShortcutTree::tr("Double click to edit key sequence.") - ); -} - -QString QtxShortcutTreeAction::getValue(QtxShortcutTree::SortKey theKey) const -{ - switch (theKey) { - case QtxShortcutTree::SortKey::ID: - return myInModuleActionID; - case QtxShortcutTree::SortKey::Name: - return name(); - case QtxShortcutTree::SortKey::ToolTip: - return toolTip(QtxShortcutTree::ElementIdx::Name); - case QtxShortcutTree::SortKey::KeySequence: - return keySequence(); - default: - return QString(); - } -} - -void QtxShortcutTreeAction::setKeySequence(const QString& theKeySequence) -{ - setText(QtxShortcutTree::ElementIdx::KeySequence, theKeySequence); -} - -QString QtxShortcutTreeAction::keySequence() const -{ - return text(QtxShortcutTree::ElementIdx::KeySequence); -} - -/*! \brief Highlights text at ElementIdx::KeySequence. */ -void QtxShortcutTreeAction::highlightKeySequenceAsModified(bool theHighlight) -{ - static const QBrush bgHighlitingBrush = QBrush(Qt::darkGreen); - static const QBrush fgHighlitingBrush = QBrush(Qt::white); - static const QBrush noBrush = QBrush(); - - setBackground(QtxShortcutTree::ElementIdx::KeySequence, theHighlight ? bgHighlitingBrush : noBrush); - setForeground(QtxShortcutTree::ElementIdx::KeySequence, theHighlight ? fgHighlitingBrush : noBrush); -} \ No newline at end of file diff --git a/src/Qtx/QtxShortcutEdit.h b/src/Qtx/QtxShortcutEdit.h deleted file mode 100644 index 862c80d86..000000000 --- a/src/Qtx/QtxShortcutEdit.h +++ /dev/null @@ -1,265 +0,0 @@ -// 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 QTXSHORTCUTTREE_H -#define QTXSHORTCUTTREE_H - -#include "Qtx.h" -#include -#include -#include -#include "SUIT_ShortcutMgr.h" -#include -#include -#include -#include - - -class QLineEdit; -class QLabel; -class QPushButton; -class QTreeWidgetItem; - -class QTX_EXPORT QtxKeySequenceEdit : public QFrame -{ - Q_OBJECT - -public: - QtxKeySequenceEdit(QWidget* = nullptr); - virtual ~QtxKeySequenceEdit() = default; - - void setConfirmedKeySequence(const QKeySequence&); - void setEditedKeySequence(const QKeySequence&); - QKeySequence editedKeySequence() const; - bool isKeySequenceModified() const; - void restoreKeySequence(); - - static QString parseEvent(QKeyEvent*); - static bool isValidKey(int); - -signals: - void editingStarted(); - void editingFinished(); - void restoreFromShortcutMgrClicked(); - -private slots: - void onClear(); - void onEditingFinished(); - -protected: - virtual bool eventFilter(QObject*, QEvent*); - -private: - void initialize(); - -private: - QLineEdit* myKeySequenceLineEdit; - QString myConfirmedKeySequenceString; - - // Last valid key sequence string from myKeySequenceLineEdit. - QString myPrevKeySequenceString; -}; - - -class QtxShortcutTree; -class QtxShortcutTreeItem; -class QtxShortcutTreeFolder; -class QtxShortcutTreeAction; -class QTextEdit; - - -class QTX_EXPORT QtxEditKeySequenceDialog : public QDialog -{ - Q_OBJECT - -public: - QtxEditKeySequenceDialog(QtxShortcutTree* theParent); - QtxEditKeySequenceDialog(const QtxEditKeySequenceDialog&) = delete; - QtxEditKeySequenceDialog& operator=(const QtxEditKeySequenceDialog&) = delete; - virtual ~QtxEditKeySequenceDialog() = default; - - void setModuleAndActionID(const QString& theModuleID, const QString& theInModuleActionID); - const QString& moduleID() const; - const QString& inModuleActionID() const; - - void setModuleAndActionName(const QString& theModuleName, const QString& theActionName, const QString& theActionToolTip = ""); - - void setConfirmedKeySequence(const QKeySequence& theSequence); - QKeySequence editedKeySequence() const; - - void updateConflictsMessage(); - - int exec(); - -private slots: - void onEditingStarted(); - void onEditingFinished(); - void onRestoreFromShortcutMgr(); - void onConfirm(); - -private: - QString myModuleID; - QString myInModuleActionID; - QLabel* myActionName; - QtxKeySequenceEdit* myKeySequenceEdit; - QTextEdit* myTextEdit; -}; - - -class QTX_EXPORT QtxShortcutTree : public QTreeWidget -{ - Q_OBJECT - -public: - enum ElementIdx { - Name = 0, - KeySequence = 1, // Empty, if item is folder item. - }; - - enum class SortKey { - ID, - Name, - ToolTip, - KeySequence, - }; - - enum class SortOrder { - Ascending, - Descending - }; - - QtxShortcutTree( - std::shared_ptr theContainer = std::shared_ptr(), - QWidget* theParent = nullptr - ); - QtxShortcutTree(const QtxShortcutTree&) = delete; - QtxShortcutTree& operator=(const QtxShortcutTree&) = delete; - virtual ~QtxShortcutTree(); - - void setShortcutsFromManager(); - void setDefaultShortcuts(); - void applyChangesToShortcutMgr(); - - std::shared_ptr shortcutContainer() const; - - void sort(QtxShortcutTree::SortKey theKey, QtxShortcutTree::SortOrder theOrder); - -private: - void updateItems(bool theHighlightModified, bool theUpdateSyncTrees); - std::pair findModuleFolderItem(const QString& theModuleID) const; - - std::set> getSortedChildren(QtxShortcutTreeFolder* theParentItem); - - void insertChild( - QtxShortcutTreeFolder* theParentItem, - std::set>& theSortedChildren, - QtxShortcutTreeItem* theChildItem - ); - -private slots: - void onItemDoubleClicked(QTreeWidgetItem* theWidgetItem, int theColIdx); - -public: - /** Keeps IDs of modules, which will are shown on setShortcutsFromManager(). */ - std::set myModuleIDs; - - static const QList> DEFAULT_SORT_SCHEMA; - -private: - /** Allows to modify plenty of shortcuts and then apply them to SUIT_ShortcutMgr as a batch. */ - const std::shared_ptr myShortcutContainer; - - QtxEditKeySequenceDialog* myEditDialog; - - QtxShortcutTree::SortKey mySortKey; - QtxShortcutTree::SortOrder mySortOrder; - - /** - * Ensures that, if several QtxShortcutTree instances coexist, - * all of them are updated when one of them applies pending changes to SUIT_ShortcutMgr. - * - * Sharing of SUIT_ShortcutContainer allows to keep some trees synchronized even without - * applying changes to SUIT_ShortcutMgr. Why? See QtxPagePrefShortcutTreeItem. - * - * Access is not synchronized in assumption, that all instances live in the same thread. - */ - static std::map> instances; -}; - - -class QtxShortcutTreeItem : public QTreeWidgetItem -{ -public: - enum Type { - Folder = 0, - Action = 1, - }; - -protected: - QtxShortcutTreeItem(const QString& theModuleID); - -public: - virtual ~QtxShortcutTreeItem() = default; - virtual QtxShortcutTreeItem::Type type() const = 0; - - virtual void setAssets(std::shared_ptr theAssets, const QString& theLang) = 0; - QString name() const; - - virtual QString getValue(QtxShortcutTree::SortKey theKey) const = 0; - -public: - const QString myModuleID; -}; - - -class QtxShortcutTreeFolder : public QtxShortcutTreeItem -{ -public: - QtxShortcutTreeFolder(const QString& theModuleID); - virtual ~QtxShortcutTreeFolder() = default; - virtual QtxShortcutTreeItem::Type type() const { return QtxShortcutTreeItem::Type::Folder; }; - - virtual void setAssets(std::shared_ptr theAssets, const QString& theLang); - - virtual QString getValue(QtxShortcutTree::SortKey theKey) const; -}; - - -class QtxShortcutTreeAction : public QtxShortcutTreeItem -{ -private: - QtxShortcutTreeAction(const QString& theModuleID, const QString& theInModuleActionID); - -public: - static QtxShortcutTreeAction* create(const QString& theModuleID, const QString& theInModuleActionID); - virtual ~QtxShortcutTreeAction() = default; - virtual QtxShortcutTreeItem::Type type() const { return QtxShortcutTreeItem::Type::Action; }; - - virtual void setAssets(std::shared_ptr theAssets, const QString& theLang); - - virtual QString getValue(QtxShortcutTree::SortKey theKey) const; - - void setKeySequence(const QString& theKeySequence); - QString keySequence() const; - void highlightKeySequenceAsModified(bool theHighlight); - - const QString myInModuleActionID; -}; - -#endif // QTXSHORTCUTTREE_H diff --git a/src/Qtx/resources/Qtx_msg_fr.ts b/src/Qtx/resources/Qtx_msg_fr.ts index 2ea62824e..d61dfe7fb 100644 --- a/src/Qtx/resources/Qtx_msg_fr.ts +++ b/src/Qtx/resources/Qtx_msg_fr.ts @@ -495,53 +495,4 @@ %1 a été développé en utilisant %2 - - QtxKeySequenceEdit - - Disable shortcut. - Désactivez le raccourci. - - - Restore the currently applied key sequence. - Restaurez la séquence de touches actuellement appliquée. - - - - QtxEditKeySequenceDialog - - Change key sequence - Modifier la séquence de touches - - - No conflicts. - Aucun conflit. - - - Confirm - Confirmer - - - Cancel - Annuler - - - These shortcuts will be disabled on confirm: - Ces raccourcis seront désactivés lors de la confirmation : - - - - QtxShortcutTree - - Action - Action - - - Key sequence - Séquence de touches - - - Double click to edit key sequence. - Double-cliquez pour modifier la séquence de touches. - - diff --git a/src/Qtx/resources/Qtx_msg_ja.ts b/src/Qtx/resources/Qtx_msg_ja.ts index 36900402a..173c81282 100644 --- a/src/Qtx/resources/Qtx_msg_ja.ts +++ b/src/Qtx/resources/Qtx_msg_ja.ts @@ -495,53 +495,4 @@ %1 は %2 を使用して開発されています。 - - QtxKeySequenceEdit - - Disable shortcut. - ショートカットを無効にします。 - - - Restore the currently applied key sequence. - 現在適用されているキー シーケンスを復元します。 - - - - QtxEditKeySequenceDialog - - Change key sequence - キーシーケンスを変更する - - - No conflicts. - 競合はありません。 - - - Confirm - 確認する - - - Cancel - キャンセル - - - These shortcuts will be disabled on confirm: - これらのショートカットは確認時に無効になります。 - - - - QtxShortcutTree - - Action - アクション - - - Key sequence - キーシーケンス - - - Double click to edit key sequence. - ダブルクリックしてキー シーケンスを編集します。 - - diff --git a/src/SUIT/CMakeLists.txt b/src/SUIT/CMakeLists.txt index 6c7c3f6af..95015a57c 100644 --- a/src/SUIT/CMakeLists.txt +++ b/src/SUIT/CMakeLists.txt @@ -46,14 +46,17 @@ SET(_moc_HEADERS SUIT_DataObject.h SUIT_Desktop.h SUIT_FileDlg.h + SUIT_FindActionDialog.h SUIT_LicenseDlg.h SUIT_MessageBox.h SUIT_Operation.h + SUIT_PagePrefShortcutTreeItem.h SUIT_PopupClient.h SUIT_PreferenceMgr.h SUIT_SelectionMgr.h SUIT_Session.h SUIT_ShortcutMgr.h + SUIT_ShortcutTree.h SUIT_Study.h SUIT_TreeModel.h SUIT_ViewManager.h @@ -121,10 +124,12 @@ SET(_other_SOURCES SUIT_ExceptionHandler.cxx SUIT_FileDlg.cxx SUIT_FileValidator.cxx + SUIT_FindActionDialog.cxx SUIT_LicenseDlg.cxx SUIT_MessageBox.cxx SUIT_Operation.cxx SUIT_OverrideCursor.cxx + SUIT_PagePrefShortcutTreeItem.cxx SUIT_PopupClient.cxx SUIT_PreferenceMgr.cxx SUIT_ResourceMgr.cxx @@ -133,6 +138,7 @@ SET(_other_SOURCES SUIT_Selector.cxx SUIT_Session.cxx SUIT_ShortcutMgr.cxx + SUIT_ShortcutTree.cxx SUIT_Study.cxx SUIT_Tools.cxx SUIT_TreeModel.cxx diff --git a/src/SUIT/SUIT_FindActionDialog.cxx b/src/SUIT/SUIT_FindActionDialog.cxx new file mode 100644 index 000000000..1d0795830 --- /dev/null +++ b/src/SUIT/SUIT_FindActionDialog.cxx @@ -0,0 +1,557 @@ +// 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 "SUIT_FindActionDialog.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + + +SUIT_FindActionDialog::SUIT_FindActionDialog(QWidget* theParent) +: QDialog(theParent) +{ + setMinimumWidth(500); + setWindowTitle(tr("Find 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); + myActionSearcher.includeDisabledActions(true); + 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 SUIT_FoundActionTree(this); + layout->addWidget(myFoundActionsTree); + + connect(myQueryLineEdit, SIGNAL(textChanged(const QString&)), this, SLOT(onQueryChanged(const QString&))); + connect(myIncludeUnavailableActionsCB, SIGNAL(stateChanged(int)), this, SLOT(onSearchOptionUnavailableActionsChanged(int))); + connect(myIncludeInactiveModulesCB, SIGNAL(stateChanged(int)), this, SLOT(onSearchOptionInactiveModulesChanged(int))); + + myQueryLineEdit->installEventFilter(myFoundActionsTree); +} + +void SUIT_FindActionDialog::setActiveModuleID(const QString& theModuleID) +{ + myActiveModuleID = theModuleID; + if(myActionSearcher.setIncludedModuleIDs(std::set({SUIT_ShortcutMgr::ROOT_MODULE_ID, myActiveModuleID}))) + updateUI(); +} + +void SUIT_FindActionDialog::onQueryChanged(const QString& theQuery) +{ + if (myActionSearcher.setQuery(theQuery)) + updateUI(); +} + +void SUIT_FindActionDialog::onSearchOptionUnavailableActionsChanged(int theState) +{ + if (myActionSearcher.includeDisabledActions(theState == Qt::CheckState::Checked)) + updateUI(); +} + +void SUIT_FindActionDialog::onSearchOptionInactiveModulesChanged(int theState) +{ + bool resultsChanged = false; + if (theState == Qt::CheckState::Checked) { + myIncludeUnavailableActionsCB->setDisabled(true); + myIncludeUnavailableActionsCB->setCheckState(Qt::CheckState::Checked); + resultsChanged = myActionSearcher.setIncludedModuleIDs(SUIT_ShortcutMgr::get()->getShortcutContainer().getIDsOfAllModules()); + } + else { + myIncludeUnavailableActionsCB->setDisabled(false); + resultsChanged = myActionSearcher.setIncludedModuleIDs(std::set({SUIT_ShortcutMgr::ROOT_MODULE_ID, myActiveModuleID})); + } + + if (resultsChanged) + updateUI(); +} + +void SUIT_FindActionDialog::updateUI() +{ + myFoundActionsTree->updateItems(myActionSearcher.getSearchResults()); +} + + + +SUIT_FoundActionTree::SUIT_FoundActionTree(SUIT_FindActionDialog* theParent) +: QTreeWidget(theParent) +{ + setColumnCount(2); + setSelectionMode(QAbstractItemView::SingleSelection); + setSortingEnabled(false); + header()->setSectionResizeMode(QHeaderView::Interactive); + { + QMap labelMap; + labelMap[SUIT_FoundActionTree::ElementIdx::Name] = SUIT_FindActionDialog::tr("Action"); + labelMap[SUIT_FoundActionTree::ElementIdx::ToolTip] = SUIT_FindActionDialog::tr("Description"); + setHeaderLabels(labelMap.values()); + } + setExpandsOnDoubleClick(false); // Implemented manually. + setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + setColumnWidth(SUIT_FoundActionTree::ElementIdx::Name, 120); + setColumnWidth(SUIT_FoundActionTree::ElementIdx::Name, 250); + setMinimumHeight(300); + + setWindowFlags(windowFlags() | Qt::FramelessWindowHint); + + mySortKey = SUIT_FoundActionTree::SortKey::MatchMetrics; + mySortOrder = SUIT_FoundActionTree::SortOrder::Ascending; + + connect(this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*, int)), this, SLOT(onItemExecuted(QTreeWidgetItem*, int))); +} + +/*! \brief Compensates lack of std::distance(), which is introduced in C++17. +\returns -1, if theIt does not belong to the */ +template +size_t indexOf( + const Container& theContainer, + const typename Container::iterator& theIt +) { + auto it = theContainer.begin(); + size_t distance = 0; + while (it != theContainer.end()) { + if (it == theIt) + return distance; + + it++; + distance++; + } + return -1; +} + +void SUIT_FoundActionTree::updateItems(const std::map>& theAssets) +{ + std::set shownModuleIDs; // To sort module-items by their IDs. + + // Remove shown module items, if updated search results have no matching actions from these modules. + for (int moduleIdx = 0; moduleIdx < topLevelItemCount(); ) { + SUIT_FoundActionTreeModule* moduleItem = static_cast(topLevelItem(moduleIdx)); + myModuleItemExpansionStates[moduleItem->myModuleID] = moduleItem->isExpanded(); + + const auto itUpdatedAssetsOfShownModule = theAssets.find(moduleItem->myModuleID); + if (itUpdatedAssetsOfShownModule == theAssets.end()) { + delete takeTopLevelItem(moduleIdx); + continue; + } + + if (itUpdatedAssetsOfShownModule->second.empty()) { + delete takeTopLevelItem(moduleIdx); + continue; + } + + shownModuleIDs.emplace(moduleItem->myModuleID); + moduleIdx++; + } + + const auto shortcutMgr = SUIT_ShortcutMgr::get(); + const QString lang = SUIT_ShortcutMgr::getLang(); + + SUIT_FoundActionTreeAction* preselectedActionItem = nullptr; + + for (const auto& moduleIDAndAssets : theAssets) { + const QString& moduleID = moduleIDAndAssets.first; + const auto& moduleAssets = moduleIDAndAssets.second; + if (moduleAssets.empty()) + continue; + + const auto moduleItemAndIdx = findModuleItem(moduleID); + SUIT_FoundActionTreeModule* moduleItem = moduleItemAndIdx.first; + if (!moduleItem) { + moduleItem = new SUIT_FoundActionTreeModule(moduleID); + moduleItem->setAssetsAndSearchData(SUIT_ActionSearcher::AssetsAndSearchData(shortcutMgr->getModuleAssets(moduleID)), lang); + + const auto emplaceRes = shownModuleIDs.emplace(moduleID); + insertTopLevelItem(indexOf(shownModuleIDs, emplaceRes.first), moduleItem); + + moduleItem->setFlags(Qt::ItemIsEnabled); + + const auto itExpansionState = myModuleItemExpansionStates.find(moduleID); + if (itExpansionState == myModuleItemExpansionStates.end()) + moduleItem->setExpanded(true); // Make module item expanded at first appearance. + else + moduleItem->setExpanded(itExpansionState->second); + } + else /* if the tree has the module-item */ { + const auto actionItems = moduleItem->takeChildren(); + for (const auto actionItem : actionItems) { + delete actionItem; + } + } + + // Fill module item with action items. + auto sortedActionItems = createActionSetWithComparator(); + for (const auto& actionIDAndAssets : moduleAssets) { + const QString& inModuleActionID = actionIDAndAssets.first; + const SUIT_ActionSearcher::AssetsAndSearchData& assetsAndSearchData = actionIDAndAssets.second; + + auto actionItem = SUIT_FoundActionTreeAction::create(moduleID, inModuleActionID); + if (!actionItem) { + ShCutDbg("SUIT_FoundActionTree can't create child item for action ID = \"" + SUIT_ShortcutMgr::makeActionID(moduleID, inModuleActionID) + "\"."); + continue; + } + + actionItem->setAssetsAndSearchData(assetsAndSearchData, lang); + sortedActionItems.emplace(actionItem); + } + + SUIT_FoundActionTreeAction* preselectedActionItemCand = nullptr; + for (const auto actionItem : sortedActionItems) { + moduleItem->addChild(actionItem); + + // Consider first ranked available action in the module (if user did not collapsed it) as a candidate for preselected action. + if (!preselectedActionItemCand && moduleItem->isExpanded() && actionItem->isEnabledBufferedValue()) + preselectedActionItemCand = actionItem; + } + + if (preselectedActionItem) { + if (preselectedActionItemCand) { + if (preselectedActionItemCand->matchMetrics() < preselectedActionItem->matchMetrics()) + preselectedActionItem = preselectedActionItemCand; + } + } + else + preselectedActionItem = preselectedActionItemCand; + } + + if (preselectedActionItem) + setCurrentItem(preselectedActionItem); +} + +void SUIT_FoundActionTree::sort(SUIT_FoundActionTree::SortKey theKey, SUIT_FoundActionTree::SortOrder theOrder) +{ + if (theKey == mySortKey && theOrder == mySortOrder) + return; + + mySortKey == theKey; + mySortOrder = theOrder; + + for (int moduleIdx = 0; moduleIdx < topLevelItemCount(); moduleIdx++) { + const auto moduleItem = static_cast(topLevelItem(moduleIdx)); + + auto sortedActionItems = createActionSetWithComparator(); + for (int childIdx = 0; childIdx < moduleItem->childCount(); childIdx++) { + SUIT_FoundActionTreeAction* const actionItem = static_cast(moduleItem->child(childIdx)); + sortedActionItems.emplace(actionItem); + } + + moduleItem->takeChildren(); + + for (const auto actionItem : sortedActionItems) { + moduleItem->addChild(actionItem); + } + } +} + +void SUIT_FoundActionTree::keyPressEvent(QKeyEvent* theEvent) +{ + const auto key = theEvent->key(); + const auto selectedItem = currentItem(); + if ((key == Qt::Key_Enter || key == Qt::Key_Return) && selectedItem) + onItemExecuted(selectedItem, SUIT_FoundActionTree::ElementIdx::Name); + else + QTreeWidget::keyPressEvent(theEvent); +} + +bool SUIT_FoundActionTree::eventFilter(QObject* theQObject, QEvent* theEvent) +{ + if (theEvent->type() == QEvent::KeyPress) { + QKeyEvent* const keyEvent = static_cast(theEvent); + const auto key = keyEvent->key(); + + if (key == Qt::Key_Enter || key == Qt::Key_Return || key == Qt::Key_Up || key == Qt::Key_Down) { + keyPressEvent(keyEvent); + return true; + } + } + + return false; +} + +std::pair SUIT_FoundActionTree::findModuleItem(const QString& theModuleID) const +{ + for (int moduleIdx = 0; moduleIdx < topLevelItemCount(); moduleIdx++) { + SUIT_FoundActionTreeModule* moduleItem = static_cast(topLevelItem(moduleIdx)); + if (moduleItem->myModuleID == theModuleID) + return std::pair(moduleItem, moduleIdx); + } + return std::pair(nullptr, -1); +} + +template +bool approximatelyEqual(Float a, Float b, Float relativeTol = std::numeric_limits::epsilon()) +{ + return std::abs(a - b) <= ( (std::abs(a) < std::abs(b) ? std::abs(b) : std::abs(a)) * relativeTol); +} + +std::set> SUIT_FoundActionTree::createActionSetWithComparator() const +{ + QList> sortSchema = SUIT_FoundActionTree::DEFAULT_SORT_SCHEMA; + { + for (auto itSameKey = sortSchema.begin(); itSameKey != sortSchema.end(); itSameKey++) { + if (itSameKey->first == mySortKey) { + sortSchema.erase(itSameKey); + break; + } + } + sortSchema.push_front(std::pair(mySortKey, mySortOrder)); + } + + static const QCollator collator; + const std::function comparator = + [sortSchema, &collator](const SUIT_FoundActionTreeAction* theItemA, const SUIT_FoundActionTreeAction* theItemB) { + for (const auto& keyAndOrder : sortSchema) { + const QVariant fieldOfA = theItemA->getValue(keyAndOrder.first); + const QVariant fieldOfB = theItemB->getValue(keyAndOrder.first); + + bool* const fieldOfAIsDouble = new bool(false); + bool* const fieldOfBIsDouble = new bool(false); + const double matchMetricsA = fieldOfA.toDouble(fieldOfAIsDouble); + const double matchMetricsB = fieldOfB.toDouble(fieldOfBIsDouble); + if (*fieldOfAIsDouble && *fieldOfBIsDouble) { + if (!approximatelyEqual(matchMetricsA, matchMetricsB)) { + const double res = matchMetricsA - matchMetricsB; + return keyAndOrder.second == SUIT_FoundActionTree::SortOrder::Ascending ? res < 0 : res > 0; + } + } + else { + const int res = collator.compare(fieldOfA.toString(), fieldOfB.toString()); + if (res != 0) + return keyAndOrder.second == SUIT_FoundActionTree::SortOrder::Ascending ? res < 0 : res > 0; + } + } + return false; + }; + + return std::set>(comparator); +} + +void SUIT_FoundActionTree::onItemExecuted(QTreeWidgetItem* theItem, int theColIdx) +{ + SUIT_FoundActionTreeItem* const item = static_cast(theItem); + if (item->type() == SUIT_FoundActionTreeItem::Type::Action) { + SUIT_FoundActionTreeAction* const actionItem = static_cast(theItem); + if (actionItem->trigger()) + static_cast(parentWidget())->accept(); + } + else /* if (item->type() == SUIT_FoundActionTreeItem::Type::Module) */ { + item->setExpanded(!item->isExpanded()); + } +} + +/*static*/ const QList> SUIT_FoundActionTree::DEFAULT_SORT_SCHEMA = +{ + {SUIT_FoundActionTree::SortKey::MatchMetrics, SUIT_FoundActionTree::SortOrder::Ascending}, + {SUIT_FoundActionTree::SortKey::Name, SUIT_FoundActionTree::SortOrder::Ascending}, + {SUIT_FoundActionTree::SortKey::ToolTip, SUIT_FoundActionTree::SortOrder::Ascending}, + {SUIT_FoundActionTree::SortKey::ID, SUIT_FoundActionTree::SortOrder::Ascending} +}; + + +SUIT_FoundActionTreeItem::SUIT_FoundActionTreeItem(const QString& theModuleID) +: QTreeWidgetItem(), myModuleID(theModuleID) +{ } + +QString SUIT_FoundActionTreeItem::name() const +{ + return text(SUIT_FoundActionTree::ElementIdx::Name); +} + +QString SUIT_FoundActionTreeItem::toolTip() const +{ + return text(SUIT_FoundActionTree::ElementIdx::ToolTip); +} + + +SUIT_FoundActionTreeModule::SUIT_FoundActionTreeModule(const QString& theModuleID) +: SUIT_FoundActionTreeItem(theModuleID) +{ + QFont f = font(SUIT_FoundActionTree::ElementIdx::Name); + f.setBold(true); + setFont(SUIT_FoundActionTree::ElementIdx::Name, f); + setText(SUIT_FoundActionTree::ElementIdx::Name, theModuleID); +} + +void SUIT_FoundActionTreeModule::setAssetsAndSearchData(const SUIT_ActionSearcher::AssetsAndSearchData& theAssetsAndSD, const QString& theLang) +{ + if (!theAssetsAndSD.myAssets) + return; + + setIcon(SUIT_FoundActionTree::ElementIdx::Name, theAssetsAndSD.myAssets->myIcon); + + const auto& ldaMap = theAssetsAndSD.myAssets->myLangDependentAssets; + if (ldaMap.empty()) { + setText(SUIT_FoundActionTree::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(SUIT_FoundActionTree::ElementIdx::Name, name); +} + +QVariant SUIT_FoundActionTreeModule::getValue(SUIT_FoundActionTree::SortKey theKey) const +{ + switch (theKey) { + case SUIT_FoundActionTree::SortKey::MatchMetrics: + return double(0); + case SUIT_FoundActionTree::SortKey::ID: + return myModuleID; + case SUIT_FoundActionTree::SortKey::Name: + return name(); + case SUIT_FoundActionTree::SortKey::ToolTip: + return toolTip(); + default: + return QString(); + } +} + +bool SUIT_FoundActionTreeModule::isEnabled() const +{ + return true; +} + + +SUIT_FoundActionTreeAction::SUIT_FoundActionTreeAction(const QString& theModuleID, const QString& theInModuleActionID) +: SUIT_FoundActionTreeItem(theModuleID), myInModuleActionID(theInModuleActionID), + myMatchMetrics(std::numeric_limits::infinity()), myIsEnabledBufferedValue(false) +{ + setText(SUIT_FoundActionTree::ElementIdx::Name, theInModuleActionID); +} + +/*static*/ SUIT_FoundActionTreeAction* SUIT_FoundActionTreeAction::create(const QString& theModuleID, const QString& theInModuleActionID) +{ + if (theInModuleActionID.isEmpty()) { + ShCutDbg("SUIT_FoundActionTreeItem: attempt to create item with empty action ID."); + return nullptr; + } + + return new SUIT_FoundActionTreeAction(theModuleID, theInModuleActionID); +} + +void SUIT_FoundActionTreeAction::setAssetsAndSearchData(const SUIT_ActionSearcher::AssetsAndSearchData& theAssetsAndSD, const QString& theLang) +{ + if (!theAssetsAndSD.myAssets) + return; + + setIcon(SUIT_FoundActionTree::ElementIdx::Name, theAssetsAndSD.myAssets->myIcon); + + const auto& ldaMap = theAssetsAndSD.myAssets->myLangDependentAssets; + if (ldaMap.empty()) { + setText(SUIT_FoundActionTree::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(SUIT_FoundActionTree::ElementIdx::Name, name); + + setText(SUIT_FoundActionTree::ElementIdx::ToolTip, lda.myToolTip); + + if (isEnabled()) { + setToolTip( + SUIT_FoundActionTree::ElementIdx::Name, + SUIT_FoundActionTree::tr("Double click to start") + ); + + setToolTip( + SUIT_FoundActionTree::ElementIdx::ToolTip, + SUIT_FoundActionTree::tr("Double click to start") + ); + } + else { + static const QBrush greyedOutBrush = QBrush(Qt::gray); + setForeground(SUIT_FoundActionTree::ElementIdx::Name, greyedOutBrush); + setForeground(SUIT_FoundActionTree::ElementIdx::ToolTip, greyedOutBrush); + } + + myMatchMetrics = theAssetsAndSD.matchMetrics(); +} + +QVariant SUIT_FoundActionTreeAction::getValue(SUIT_FoundActionTree::SortKey theKey) const +{ + switch (theKey) { + case SUIT_FoundActionTree::SortKey::MatchMetrics: + return myMatchMetrics; + case SUIT_FoundActionTree::SortKey::ID: + return myInModuleActionID; + case SUIT_FoundActionTree::SortKey::Name: + return name(); + case SUIT_FoundActionTree::SortKey::ToolTip: + return toolTip(); + default: + return QString(); + } +} + +bool SUIT_FoundActionTreeAction::isEnabled() const +{ + const auto& actions = SUIT_ShortcutMgr::get()->getActions(myModuleID, myInModuleActionID); + myIsEnabledBufferedValue = std::find_if(actions.begin(), actions.end(), [](const QAction* const theAction){ return theAction->isEnabled(); }) != actions.end(); + return myIsEnabledBufferedValue; +} + +bool SUIT_FoundActionTreeAction::trigger() const +{ + bool res = false; + const auto& actions = SUIT_ShortcutMgr::get()->getActions(myModuleID, myInModuleActionID); + for (const auto& action : actions) { + if (action->isEnabled()) { + action->trigger(); + res = true; + } + } + return res; +} \ No newline at end of file diff --git a/src/SUIT/SUIT_FindActionDialog.h b/src/SUIT/SUIT_FindActionDialog.h new file mode 100644 index 000000000..34ffbcb0e --- /dev/null +++ b/src/SUIT/SUIT_FindActionDialog.h @@ -0,0 +1,209 @@ +// Copyright (C) 2007-2024 CEA, EDF, OPEN CASCADE +// +// Copyright (C) 2003-2007 OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN, +// CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS +// +// 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 SUIT_FINDACTIONDIALOG_H +#define SUIT_FINDACTIONDIALOG_H + +#include "SUIT.h" +#include "SUIT_ShortcutMgr.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +class QCheckBox; +class QLineEdit; +class QLabel; +class QPushButton; +class QKeyEvent; +class SUIT_FoundActionTree; + + +class SUIT_EXPORT SUIT_FindActionDialog : public QDialog +{ + Q_OBJECT + +public: + SUIT_FindActionDialog(QWidget* theParent); + SUIT_FindActionDialog(const SUIT_FindActionDialog&) = delete; + SUIT_FindActionDialog& operator=(const SUIT_FindActionDialog&) = delete; + virtual ~SUIT_FindActionDialog() = default; + + void setActiveModuleID(const QString& theModuleID = SUIT_ShortcutMgr::ROOT_MODULE_ID); + +private slots: + void onQueryChanged(const QString& theKeyword); + void onSearchOptionUnavailableActionsChanged(int); + void onSearchOptionInactiveModulesChanged(int); + +private: + void updateUI(); + + QLineEdit* myQueryLineEdit; + QCheckBox* myIncludeUnavailableActionsCB; + QCheckBox* myIncludeInactiveModulesCB; + SUIT_FoundActionTree* myFoundActionsTree; + + QString myActiveModuleID; + SUIT_ActionSearcher myActionSearcher; +}; + + +class SUIT_FoundActionTreeItem; +class SUIT_FoundActionTreeModule; +class SUIT_FoundActionTreeAction; + + +class SUIT_EXPORT SUIT_FoundActionTree : public QTreeWidget +{ + Q_OBJECT + +public: + enum ElementIdx { + Name = 0, + ToolTip = 1 + }; + + enum class SortKey { + MatchMetrics, + ID, + Name, + ToolTip + }; + + enum class SortOrder { + Ascending, + Descending + }; + + SUIT_FoundActionTree(SUIT_FindActionDialog* theParent); + SUIT_FoundActionTree(const SUIT_FoundActionTree&) = delete; + SUIT_FoundActionTree& operator=(const SUIT_FoundActionTree&) = delete; + virtual ~SUIT_FoundActionTree() = default; + + void updateItems(const std::map>& theAssets); + + void sort(SUIT_FoundActionTree::SortKey theKey, SUIT_FoundActionTree::SortOrder theOrder); + + void keyPressEvent(QKeyEvent* theEvent); + +protected: + bool eventFilter(QObject* theQObject, QEvent* theEvent); + +private: + std::pair findModuleItem(const QString& theModuleID) const; + std::set> createActionSetWithComparator() const; + +private slots: + void onItemExecuted(QTreeWidgetItem* theWidgetItem, int theColIdx); + +public: + static const QList> DEFAULT_SORT_SCHEMA; + +private: + SUIT_FoundActionTree::SortKey mySortKey; + SUIT_FoundActionTree::SortOrder mySortOrder; + + /** {moduleID, isExpanded}[] */ + std::map myModuleItemExpansionStates; +}; + + +class SUIT_FoundActionTreeItem : public QTreeWidgetItem +{ +public: + enum Type { + Module = 0, + Action = 1, + }; + +protected: + SUIT_FoundActionTreeItem(const QString& theModuleID); + +public: + virtual ~SUIT_FoundActionTreeItem() = default; + virtual SUIT_FoundActionTreeItem::Type type() const = 0; + + virtual void setAssetsAndSearchData(const SUIT_ActionSearcher::AssetsAndSearchData& theAssetsAndSD, const QString& theLang) = 0; + QString name() const; + QString toolTip() const; + + virtual QVariant getValue(SUIT_FoundActionTree::SortKey theKey) const = 0; + + virtual bool isEnabled() const = 0; + +public: + const QString myModuleID; +}; + + +class SUIT_FoundActionTreeModule : public SUIT_FoundActionTreeItem +{ +public: + SUIT_FoundActionTreeModule(const QString& theModuleID); + virtual ~SUIT_FoundActionTreeModule() = default; + virtual SUIT_FoundActionTreeItem::Type type() const { return SUIT_FoundActionTreeItem::Type::Module; }; + + /*! \brief Search data is unused. */ + virtual void setAssetsAndSearchData(const SUIT_ActionSearcher::AssetsAndSearchData& theAssetsAndSD, const QString& theLang); + + virtual QVariant getValue(SUIT_FoundActionTree::SortKey theKey) const; + + virtual bool isEnabled() const; +}; + + +class SUIT_FoundActionTreeAction : public SUIT_FoundActionTreeItem +{ +private: + SUIT_FoundActionTreeAction(const QString& theModuleID, const QString& theInModuleActionID); + +public: + static SUIT_FoundActionTreeAction* create(const QString& theModuleID, const QString& theInModuleActionID); + virtual ~SUIT_FoundActionTreeAction() = default; + virtual SUIT_FoundActionTreeItem::Type type() const { return SUIT_FoundActionTreeItem::Type::Action; }; + + virtual void setAssetsAndSearchData(const SUIT_ActionSearcher::AssetsAndSearchData& theAssetsAndSD, const QString& theLang); + + virtual QVariant getValue(SUIT_FoundActionTree::SortKey theKey) const; + double matchMetrics() const { return myMatchMetrics; }; + + virtual bool isEnabled() const; + bool isEnabledBufferedValue() const { return myIsEnabledBufferedValue; }; + + bool trigger() const; + + const QString myInModuleActionID; + +private: + double myMatchMetrics; + mutable bool myIsEnabledBufferedValue; +}; + +#endif // SUIT_FINDACTIONDIALOG_H diff --git a/src/SUIT/SUIT_PagePrefShortcutTreeItem.cxx b/src/SUIT/SUIT_PagePrefShortcutTreeItem.cxx new file mode 100644 index 000000000..d05d51167 --- /dev/null +++ b/src/SUIT/SUIT_PagePrefShortcutTreeItem.cxx @@ -0,0 +1,65 @@ +#include "SUIT_PagePrefShortcutTreeItem.h" + +#include "SUIT_ShortcutTree.h" +#include "SUIT_ShortcutMgr.h" + + +/*! + \brief Creates preference item for editing of key bindings + \param theParent parent preference item. Must not be nullptr. +*/ +SUIT_PagePrefShortcutTreeItem::SUIT_PagePrefShortcutTreeItem(QtxPreferenceItem* theParent) + : QtxPagePrefItem(QString(), theParent) +{ + auto container = std::shared_ptr(); + const auto itContainers = SUIT_PagePrefShortcutTreeItem::shortcutContainers.find(rootItem()); + if (itContainers == SUIT_PagePrefShortcutTreeItem::shortcutContainers.end()) { + container.reset(new SUIT_ShortcutContainer()); + SUIT_PagePrefShortcutTreeItem::shortcutContainers.emplace(rootItem(), container); + } + else { + container = itContainers->second.lock(); + if (!container) { + container.reset(new SUIT_ShortcutContainer()); + itContainers->second = container; + } + } + + SUIT_ShortcutTree* tree = new SUIT_ShortcutTree(container); + tree->myModuleIDs = SUIT_ShortcutMgr::get()->getShortcutModuleIDs(); + setWidget(tree); +} + +/*! + \brief Retrieves shortcut preferences from ShortcutMgr. + Updates UI of controlling widget. + \sa store() +*/ +void SUIT_PagePrefShortcutTreeItem::retrieve() +{ + static_cast(widget())->setShortcutsFromManager(); +} + +/*! + \brief Retrieves shortcut preferences from resource files, ignoring user preferences. + Updates UI of controlling widget. + \sa store() +*/ +void SUIT_PagePrefShortcutTreeItem::retrieveDefault() +{ + static_cast(widget())->setDefaultShortcuts(); +} + +/*! + \brief Applies modified shortcut preferences to ShortcutMgr. + Updates UI of controlling widget. + And ShortcutMgr, in turn, serilizes shortcut preferences using the resource manager. + \sa retrieve() +*/ +void SUIT_PagePrefShortcutTreeItem::store() +{ + static_cast(widget())->applyChangesToShortcutMgr(); +} + +/*static*/ std::map> SUIT_PagePrefShortcutTreeItem::shortcutContainers = +std::map>(); \ No newline at end of file diff --git a/src/SUIT/SUIT_PagePrefShortcutTreeItem.h b/src/SUIT/SUIT_PagePrefShortcutTreeItem.h new file mode 100644 index 000000000..653a1e08f --- /dev/null +++ b/src/SUIT/SUIT_PagePrefShortcutTreeItem.h @@ -0,0 +1,64 @@ +// Copyright (C) 2007-2024 CEA, EDF, OPEN CASCADE +// +// Copyright (C) 2003-2007 OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN, +// CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS +// +// 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 SUIT_PAGEPREFSHORTCUTTREEITEM_H +#define SUIT_PAGEPREFSHORTCUTTREEITEM_H + +#include "SUIT.h" +#include + +#include +#include + + +class SUIT_ShortcutTree; +class SUIT_ShortcutContainer; + + +class SUIT_EXPORT SUIT_PagePrefShortcutTreeItem : public QtxPagePrefItem +{ +public: + SUIT_PagePrefShortcutTreeItem(QtxPreferenceItem* theParent); + virtual ~SUIT_PagePrefShortcutTreeItem() = default; + + virtual void retrieve(); + virtual void retrieveDefault(); + virtual void store(); + +private: + SUIT_ShortcutTree* myShortcutTree; + + // { root item (preference window), shortcut container of synchronized trees (widgets within the same window) } + static std::map> shortcutContainers; + /** Why is this? + * Every QtxPagePrefMgr is eventually a preference window. Each preference window has button "Apply". + * When the button is pressed, all descendants of the QtxPagePrefMgr store changes they carry into preferences. + * The pitfall with shortcut trees is as follows: made in independent shortcut trees, changes may conflict, + * and merge of such changes is ambiguous. And the solution is to keep shortcut trees within the same window + * synchronized - all changes being made in a tree of a synchronized bundle are projected to other trees from the bundle + * without interacting with SUIT_ShortcutMgr. + * + * Every time shortcut preferences stored to the ShortcutMgr, all instances of SUIT_ShortcutTree are updated. + */ +}; + +#endif // SUIT_PAGEPREFSHORTCUTTREEITEM_H \ No newline at end of file diff --git a/src/SUIT/SUIT_PreferenceMgr.cxx b/src/SUIT/SUIT_PreferenceMgr.cxx index fa0618058..7e5678266 100644 --- a/src/SUIT/SUIT_PreferenceMgr.cxx +++ b/src/SUIT/SUIT_PreferenceMgr.cxx @@ -21,6 +21,7 @@ // Author: Sergey TELKOV // #include "SUIT_PreferenceMgr.h" +#include "SUIT_PagePrefShortcutTreeItem.h" SUIT_PreferenceMgr::SUIT_PreferenceMgr( QtxResourceMgr* resMgr, QWidget* parent ) : QtxPagePrefMgr( resMgr, parent ), @@ -158,7 +159,7 @@ int SUIT_PreferenceMgr::addItem( const QString& title, const int pId, item = new QtxPagePrefPathListItem( Qtx::PT_Directory, title, parent, sect, param ); break; case ShortcutTree: - item = new QtxPagePrefShortcutTreeItem( parent ); + item = new SUIT_PagePrefShortcutTreeItem( parent ); break; case BiColor: item = new QtxPagePrefBiColorItem( title, parent, sect, param ); diff --git a/src/SUIT/SUIT_ShortcutMgr. ReadMe.md b/src/SUIT/SUIT_ShortcutMgr. ReadMe.md index d0cc5adc8..9ce349bf0 100644 --- a/src/SUIT/SUIT_ShortcutMgr. ReadMe.md +++ b/src/SUIT/SUIT_ShortcutMgr. ReadMe.md @@ -2,11 +2,11 @@ Hot keys must be considered as resources, being shared between all components of an application. E.g. it is unacceptable to have 'Close file' and 'Redo' actions being assigned to the same key sequence. When the SHAPER module is active, the application desktop is active too. The desktop has own hot keys, and they must not interfere with ones of SHAPER. Since the task implies granting users a right to assign shortcuts on their will, the application must track all assigned shortcuts of all modules, prevent intolerable user shortcut modifications and govern actual binding of QActions with key sequences. -`SUIT_ShortcutMgr` handles shortcuts of SALOME desktop and all modules. It is solely responsible and capable to dynamically bind actions with key sequences and (de)serialize shortcut preferences using `SUIT_ResourceMgr`. `SUIT_ShortcutContainer` encapsulates logics of conflict detecting and resolving. `QtxShortcutTree` widget provides GUI to change shortcut preferences conveniently: it allows to remap plenty of shortcuts without applying, displays conflict-resolving dialog, highlights modifications until they are applied (saved into preference files). +`SUIT_ShortcutMgr` handles shortcuts of SALOME desktop and all modules. It is solely responsible and capable to dynamically bind actions with key sequences and (de)serialize shortcut preferences using `SUIT_ResourceMgr`. `SUIT_ShortcutContainer` encapsulates logics of conflict detecting and resolving. `SUIT_ShortcutTree` widget provides GUI to change shortcut preferences conveniently: it allows to remap plenty of shortcuts without applying, displays conflict-resolving dialog, highlights modifications until they are applied (saved into preference files). To (de)serialize shortcut preferences without dependence on language environment, shortcuts must be stored as pairs {action ID, key sequence}, where action IDs must be application-unique. -Since desktop shortcuts may also be changed and interfere with shortcuts of modules, `QtxShortcutTree` should always display desktop shortcuts and shortcuts of all modules altogether, even if some modules are inactive. It means, that `QtxShortcutTree` must be fed not only with shortcut data {action ID, key sequence}[], but also with dictionaries {action ID, action name}[]. `QtxShortcutTree` also requires other action assets - tool tip and icon path. +Since desktop shortcuts may also be changed and interfere with shortcuts of modules, `SUIT_ShortcutTree` should always display desktop shortcuts and shortcuts of all modules altogether, even if some modules are inactive. It means, that `SUIT_ShortcutTree` must be fed not only with shortcut data {action ID, key sequence}[], but also with dictionaries {action ID, action name}[]. `SUIT_ShortcutTree` also requires other action assets - tool tip and icon path. Assets of actions may be retrieved from instances of actions, but there is a pitfall: if a module has not been activated yet, its actions have not been initialized either. Qt Linguist is no help in this case too. To retrieve an action name using `QObject::tr(actionID)`, the `tr(const char*)` method must be called with instance of the class, which is designated as a context for the actionID in *.ts files. And contexts are usually descendants of SUIT_Application and CAM_Module. Again, until a module instance is created, there is no way for `SUIT_ShortcutMgr` to get even a name of a context-class, which an action with an ID belongs to, without any additional data. Straightforward mechanism for loading of action assets in advance has been devised: for all actions, which are bound by default or may be bound by user to hotkeys, assets must be placed into asset files. People who do/refine localizations should keep this in mind and also process JSON files, which are referred in resource files in sections `
`. @@ -77,6 +77,6 @@ Thus, ampersand-shortcuts will appear and be treated in shortcut editor as regul If the second option is preferable, should different ampersand-shortcuts for every target language be placed in resource files? ## Minor issues -1. `QtxShortcutTree` widget does not take the whole available height of preference window, it only takes as mush as its items require. -2. Selection of `QtxShortcutTree`' item shadows "modified" highlighter. Can be fixed by replacing base `QTreeWidget` of `QtxShortcutTree` with `QTreeView`, or may be by applying some style sheet. +1. `SUIT_ShortcutTree` widget does not take the whole available height of preference window, it only takes as mush as its items require. +2. Selection of `SUIT_ShortcutTree`' item shadows "modified" highlighter. Can be fixed by replacing base `QTreeWidget` of `SUIT_ShortcutTree` with `QTreeView`, or may be by applying some style sheet. 3. `SUIT_ShortcutMgr` introduces concept of module, but the first module class is `CAM_Module` is introduced along with `CAM_Application`, which is descendant of `SUIT_Application`. diff --git a/src/SUIT/SUIT_ShortcutMgr.cxx b/src/SUIT/SUIT_ShortcutMgr.cxx index bfae01ec3..1204d32f8 100644 --- a/src/SUIT/SUIT_ShortcutMgr.cxx +++ b/src/SUIT/SUIT_ShortcutMgr.cxx @@ -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> 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()); - myShortcutsInversed.emplace(ROOT_MODULE_ID, std::map()); + myShortcuts.emplace(SUIT_ShortcutMgr::ROOT_MODULE_ID, std::map()); + myShortcutsInversed.emplace(SUIT_ShortcutMgr::ROOT_MODULE_ID, std::map()); } std::set SUIT_ShortcutContainer::getIDsOfInterferingModules(const QString& theModuleID) const { std::set 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> 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> SUIT_ShortcutContainer::getConflicts( return std::set>(); 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> 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>& moduleInvalidShortcuts = invalidShortcuts[moduleID]; moduleInvalidShortcuts.push_back(std::pair(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 SUIT_ShortcutMgr::getActionAssets(const std::shared_ptr 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(nullptr); - else - return it->second; + } + + const auto itModuleActionAssets = myActionAssets.find(moduleID); + if (itModuleActionAssets == myActionAssets.end()) + return std::shared_ptr(nullptr); + else { + const auto moduleActionAssets = itModuleActionAssets->second; + const auto itActionAssets = moduleActionAssets.find(inModuleActionID); + if (itActionAssets == moduleActionAssets.end()) + return std::shared_ptr(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(new SUIT_ActionAssets(actionAssets)); - itAssets = myActionAssets.emplace(actionID, pAssets).first; + itAssets = moduleActionAssets.emplace(inModuleActionID, 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(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,643 @@ 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 (theOn) { + myPermutatedSentences.clear(); + myFuzzyPermutatedSentences.clear(); + return; + } + + if (myPermutatedSentences.isEmpty()) + SUIT_SentenceMatcher::makePermutatedSentences(myWords, myPermutatedSentences); + + if (myUseFuzzyWords && myFuzzyPermutatedSentences.isEmpty()) + SUIT_SentenceMatcher::makePermutatedSentences(myFuzzyWords, myFuzzyPermutatedSentences); +} + +void SUIT_SentenceMatcher::setUseFuzzyWords(bool theOn) +{ + if (myUseFuzzyWords == theOn) + return; + + myUseFuzzyWords = theOn; + if (myWords.isEmpty() || !theOn) { + myFuzzyWords.clear(); + myFuzzyPermutatedSentences.clear(); + return; + } + + myFuzzyWords.clear(); + SUIT_SentenceMatcher::makeFuzzyWords(myWords, myFuzzyWords); + + if (!myUseExactWordOrder) { + myFuzzyPermutatedSentences.clear(); + 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; + myWords = theQuery.split(" ", QString::SkipEmptyParts); + + { // Set permutated sentences. + myPermutatedSentences.clear(); + if (!myUseExactWordOrder) + SUIT_SentenceMatcher::makePermutatedSentences(myWords, myPermutatedSentences); + } + + // Set fuzzy words and sentences. + myFuzzyWords.clear(); + myFuzzyPermutatedSentences.clear(); + + if (myUseFuzzyWords) { + SUIT_SentenceMatcher::makeFuzzyWords(myWords, myFuzzyWords); + if (!myUseExactWordOrder) + SUIT_SentenceMatcher::makePermutatedSentences(myFuzzyWords, myFuzzyPermutatedSentences); + } +} + +double SUIT_SentenceMatcher::match(const QString& theInputString) const +{ + int n = 0; + if (myUseExactWordOrder) { + n = SUIT_SentenceMatcher::match(theInputString, myWords, myIsCaseSensitive); + if (n != theInputString.length() && myUseFuzzyWords) { + const int nFuzzy = SUIT_SentenceMatcher::match(theInputString, myFuzzyWords, myIsCaseSensitive); + if (nFuzzy > n) + n = nFuzzy; + } + } + else /* if match with permutated query sentences */ { + n = SUIT_SentenceMatcher::match(theInputString, myPermutatedSentences, myIsCaseSensitive); + if (n != theInputString.length() && myUseFuzzyWords) { + const int nFuzzy = SUIT_SentenceMatcher::match(theInputString, myFuzzyPermutatedSentences, myIsCaseSensitive); + if (nFuzzy > n) + n = nFuzzy; + } + } + + if (n <= 0) + return std::numeric_limits::infinity(); + + const auto strLength = theInputString.length() > myQuery.length() ? theInputString.length() : myQuery.length(); + + if (n > strLength) + return 0; // Exact match or almost exact. + + return double(strLength - n); +} + +QString SUIT_SentenceMatcher::toString() const +{ + QString res = QString("myUseExactWordOrder: ") + (myUseExactWordOrder ? "true" : "false") + ";\n"; + res += QString("myUseFuzzyWords: ") + (myUseFuzzyWords ? "true" : "false") + ";\n"; + res += QString("myIsCaseSensitive: ") + (myIsCaseSensitive ? "true" : "false") + ";\n"; + res += QString("myQuery: ") + myQuery + ";\n"; + res += QString("myWords: ") + myWords.join(", ") + ";\n"; + res += QString("myFuzzyWords: ") + myFuzzyWords.join(", ") + ";\n"; + + res += "myPermutatedSentences:\n"; + for (const auto& sentence : myPermutatedSentences) { + res += "\t" + sentence.join(", ") + ";\n"; + } + + res += "myFuzzyPermutatedSentences:\n"; + for (const auto& sentence : myFuzzyPermutatedSentences) { + res += "\t" + sentence.join(", ") + ";\n"; + } + + res += "."; + return res; +} + +/*static*/ bool SUIT_SentenceMatcher::makePermutatedSentences(const QStringList& theWords, QList& 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*/ int SUIT_SentenceMatcher::matchWithSentenceIgnoreEndings(const QString& theInputString, const QStringList& theSentence, bool theCaseSensitive) +{ + const QRegExp regExp("^" + theSentence.join("\\w*\\W+"), theCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive); + regExp.indexIn(theInputString); + const int matchMetrics = regExp.matchedLength(); + return matchMetrics > 0 ? matchMetrics : 0; +} + +/*static*/ int SUIT_SentenceMatcher::matchWithSentencesIgnoreEndings(const QString& theInputString, const QList& theSentences, bool theCaseSensitive) +{ + int res = 0; + for (const QStringList& sentence : theSentences) { + const int matchMetrics = SUIT_SentenceMatcher::matchWithSentenceIgnoreEndings(theInputString, sentence, theCaseSensitive); + if (matchMetrics > res) { + res = matchMetrics; + if (res == theInputString.length()) + return res; + } + } + return res; +} + +/*static*/ int SUIT_SentenceMatcher::matchAtLeastOneWord(const QString& theInputString, const QStringList& theWords, bool theCaseSensitive) +{ + int res = 0; + for (const QString& word : theWords) { + const auto regExp = QRegExp(word, theCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive); + regExp.indexIn(theInputString); + const int matchMetrics = regExp.matchedLength(); + // The same input word can be counted multiple times. Nobody cares. + if (matchMetrics > 0) + res += matchMetrics; + } + return res; +} + +/*static*/ int SUIT_SentenceMatcher::match( + const QString& theInputString, + const QStringList& theSentence, + bool theCaseSensitive +) { + int res = SUIT_SentenceMatcher::matchWithSentenceIgnoreEndings(theInputString, theSentence, theCaseSensitive); + if (res == theInputString.length()) + return res; + + const int matchMetrics = SUIT_SentenceMatcher::matchAtLeastOneWord(theInputString, theSentence, theCaseSensitive); + if (matchMetrics > res) + res = matchMetrics; + + return res; +} + +/*static*/ int SUIT_SentenceMatcher::match( + const QString& theInputString, + const QList& theSentences, + bool theCaseSensitive +) { + int res = SUIT_SentenceMatcher::matchWithSentencesIgnoreEndings(theInputString, theSentences, theCaseSensitive); + if (res == theInputString.length()) + return res; + + if (theSentences.size() > 0) { + const int matchMetrics = SUIT_SentenceMatcher::matchAtLeastOneWord(theInputString, theSentences[0], theCaseSensitive); + if (matchMetrics > res) + res = matchMetrics; + } + + return res; +} + + +SUIT_ActionSearcher::AssetsAndSearchData::AssetsAndSearchData(std::shared_ptr theAssets, double theMatchMetrics) +: myAssets(theAssets), myMatchMetrics(theMatchMetrics) +{ + if (theMatchMetrics < 0) { + myMatchMetrics = std::numeric_limits::infinity(); + ShCutDbg("WARNING: SUIT_ActionSearcher::AssetsAndSearchData: match metrics < 0. INF is assigned instead."); + } +} + +void SUIT_ActionSearcher::AssetsAndSearchData::setMatchMetrics(double theMatchMetrics) +{ + if (theMatchMetrics < 0) { + myMatchMetrics = std::numeric_limits::infinity(); + ShCutDbg("WARNING: SUIT_ActionSearcher::AssetsAndSearchData: match metrics < 0. INF is assigned instead."); + return; + } + + myMatchMetrics = theMatchMetrics; +} + +void SUIT_ActionSearcher::AssetsAndSearchData::toJSON(QJsonObject& oJsonObject) const +{ + oJsonObject["myMatchMetrics"] = myMatchMetrics; + + if (myAssets) { + QJsonObject assetsJSON; + myAssets->toJSON(assetsJSON); + oJsonObject["myAssets"] = assetsJSON; + } +} + +QString SUIT_ActionSearcher::AssetsAndSearchData::toString() const +{ + QJsonObject json; + toJSON(json); + QJsonDocument doc(json); + return QString(doc.toJson(QJsonDocument::Indented)); +} + +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 theIncludedModuleIDs) +{ + ShCutDbg("SUIT_ActionSearcher::setIncludedModuleIDs"); + + if (myIncludedModuleIDs == theIncludedModuleIDs) + return false; + + myIncludedModuleIDs = theIncludedModuleIDs; + + bool res = false; + // Erase search results from 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()) { + itFound = 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 double matchMetrics = matchAction(moduleID, inModuleActionID, actionIDAndAssets.second); + if (matchMetrics < std::numeric_limits::infinity()) { + mySearchResults[moduleID][inModuleActionID] = SUIT_ActionSearcher::AssetsAndSearchData(actionIDAndAssets.second, matchMetrics); + res = true; + } + } + } + + ShCutDbg() && ShCutDbg(toString()); + + return res; +} + +bool SUIT_ActionSearcher::includeDisabledActions(bool theOn) +{ + ShCutDbg("SUIT_ActionSearcher::includeDisabledActions"); + + if (myIncludeDisabledActions == theOn) + return false; + + myIncludeDisabledActions = theOn; + + bool res; + if (myIncludeDisabledActions) + res = extendResults(); + else + res = filterResults().first; + + ShCutDbg() && ShCutDbg(toString()); + return res; +} + +bool SUIT_ActionSearcher::setFieldsToMatch(const std::set& 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; + + bool res; + if (narrows) + res = filterResults().first; + else if (extends) + res = extendResults(); + else + res = filter().first; + + ShCutDbg() && ShCutDbg(toString()); + return res; +} + +bool SUIT_ActionSearcher::setCaseSensitive(bool theOn) +{ + if (myMatcher.isCaseSensitive() == theOn) + return false; + + myMatcher.setCaseSensitive(theOn); + + bool res; + if (theOn) + res = filterResults().first; + else + res = extendResults(); + + ShCutDbg() && ShCutDbg(toString()); + return res; +} + +bool SUIT_ActionSearcher::setQuery(const QString& theQuery) +{ + ShCutDbg("SUIT_ActionSearcher::setQuery"); + + if (theQuery.simplified() == myMatcher.getQuery().simplified()) + return false; + + myMatcher.setQuery(theQuery); + bool res = filter().first; + ShCutDbg() && ShCutDbg(toString()); + return res; +} + +const std::map>& SUIT_ActionSearcher::getSearchResults() const +{ + return mySearchResults; +} + +std::pair SUIT_ActionSearcher::filter() +{ + ShCutDbg("SUIT_ActionSearcher::filter()"); + + auto res = std::pair(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 double matchMetrics = matchAction(moduleID, inModuleActionID, aAndD.myAssets); + if (matchMetrics < std::numeric_limits::infinity()) { + if (matchMetrics != aAndD.matchMetrics()) { + aAndD.setMatchMetrics(matchMetrics); + res.second = true; + } + } + else /* if n == 0 */ { + foundActionIDsAndAssets.erase(itFoundActionIDAndAssets); + res.first = true; + } + continue; + } + } + + const double matchMetrics = matchAction(moduleID, inModuleActionID, actionIDAndAssets.second); + if (matchMetrics < std::numeric_limits::infinity()) { + if (itFoundModuleIDAndAssets == mySearchResults.end()) + itFoundModuleIDAndAssets = mySearchResults.emplace(moduleID, std::map()).first; + + itFoundModuleIDAndAssets->second.emplace(inModuleActionID, SUIT_ActionSearcher::AssetsAndSearchData(actionIDAndAssets.second, matchMetrics)); + res.first = true; + } + } + } + + return res; +} + +std::pair SUIT_ActionSearcher::filterResults() +{ + auto res = std::pair(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 double matchMetrics = matchAction(moduleID, inModuleActionID, assetsAndSearchData.myAssets); + if (matchMetrics == std::numeric_limits::infinity()) { + itActionIDAndAssets = actionIDsAndAssets.erase(itActionIDAndAssets); + res.first = true; + } + else { + if (assetsAndSearchData.matchMetrics() != matchMetrics) { + assetsAndSearchData.setMatchMetrics(matchMetrics); + res.second = true; + } + itActionIDAndAssets++; + } + } + + if (actionIDsAndAssets.empty()) + itFoundModuleIDAndAssets = mySearchResults.erase(itFoundModuleIDAndAssets); + else + itFoundModuleIDAndAssets++; + } + + return res; +} + +bool SUIT_ActionSearcher::extendResults() +{ + ShCutDbg("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. + } + + ShCutDbg() && ShCutDbg("SUIT_ActionSearcher::extendResults(): " + moduleID + "/" + inModuleActionID + "." ); + const double matchMetrics = matchAction(moduleID, inModuleActionID, actionIDAndAssets.second); + if (matchMetrics < std::numeric_limits::infinity()) { + ShCutDbg("SUIT_ActionSearcher::extendResults(): match, metrics = " + QString::fromStdString(std::to_string(matchMetrics))); + if (itFoundModuleIDAndAssets == mySearchResults.end()) + itFoundModuleIDAndAssets = mySearchResults.emplace(moduleID, std::map()).first; + + itFoundModuleIDAndAssets->second.emplace(inModuleActionID, SUIT_ActionSearcher::AssetsAndSearchData(actionIDAndAssets.second, matchMetrics)); + res = true; + } + } + } + return res; +} + +double SUIT_ActionSearcher::matchAction(const QString& theModuleID, const QString& theInModuleActionID, std::shared_ptr theAssets) +{ + if (!theAssets) { + ShCutDbg("WARNING: SUIT_ActionSearcher::matchAction: theAssets is nullptr."); + return std::numeric_limits::infinity(); + } + + 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 std::numeric_limits::infinity(); + } + + double res = std::numeric_limits::infinity(); + + for (const auto& langAndLDA : theAssets->myLangDependentAssets) { + if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::ToolTip) != myFieldsToMatch.end()) { + const double matchMetrics = myMatcher.match(langAndLDA.second.myToolTip); + if (matchMetrics < res) + res = matchMetrics; + } + + if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::Name) != myFieldsToMatch.end()) { + const double matchMetrics = myMatcher.match(langAndLDA.second.myName); + if (matchMetrics < res) + res = matchMetrics; + } + } + + if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::ID) != myFieldsToMatch.end()) { + const double matchMetrics = myMatcher.match(SUIT_ShortcutMgr::makeActionID(theModuleID, theInModuleActionID)); + if (matchMetrics < res) + res = matchMetrics; + } + + if (myFieldsToMatch.find(SUIT_ActionSearcher::MatchField::KeySequence) != myFieldsToMatch.end()) { + const QString keySequence = SUIT_ShortcutMgr::get()->getKeySequence(theModuleID, theInModuleActionID).toString(); + const double matchMetrics = myMatcher.match(keySequence); + if (matchMetrics < res) + res = matchMetrics; + } + + return res; +} + +QString SUIT_ActionSearcher::toString() const +{ + QString res; + + res += "myMatcher: {\n"; + res += myMatcher.toString(); + res += "};\n"; + + res += "myIncludedModuleIDs: "; + for (const QString& moduleID : myIncludedModuleIDs) { + res += moduleID + ", "; + } + res += ";\n"; + + res += QString("myIncludeDisabledActions: ") + (myIncludeDisabledActions ? "true" : "false") + ";\n"; + + res += "myFieldsToMatch: "; + for (const auto& field : myFieldsToMatch) { + res += QString::number(int(field)) + ", "; + } + res += ";\n"; + + res += "mySearchResults:\n"; + for (const auto& moduleIDAndAssets : mySearchResults ) { + res += "\tModule ID: " + moduleIDAndAssets.first + ":\n"; + for (const auto& actionIDAndAssets : moduleIDAndAssets.second) { + const auto& assetsAndSearchData = actionIDAndAssets.second; + res += "\t\tAction ID: " + actionIDAndAssets.first + ": {"; + res += "\t\t: " + actionIDAndAssets.second.toString(); + res += "\t\t}"; + } + } + + return res; } \ No newline at end of file diff --git a/src/SUIT/SUIT_ShortcutMgr.h b/src/SUIT/SUIT_ShortcutMgr.h index c8debfb81..27504d976 100644 --- a/src/SUIT/SUIT_ShortcutMgr.h +++ b/src/SUIT/SUIT_ShortcutMgr.h @@ -27,11 +27,13 @@ #include #include +#include #include #include #include #include #include +#include class QAction; class QtxAction; @@ -238,6 +240,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(); @@ -285,7 +289,7 @@ public: \returns {assetsExist, assets}. */ static std::pair getActionAssetsFromResources(const QString& theActionID); - /*! \returns Language being set in resource manager. */ + /*! \returns Language, which is set in resource manager. */ static QString getLang(); @@ -357,8 +361,12 @@ public: if the module is root (theModuleID is empty) - returns all module IDs, otherwise returns ["", theModuleID]. */ std::set getIDsOfInterferingModules(const QString& theModuleID) const; + /*! \returns assets, which describe module's header, not its content. */ std::shared_ptr getModuleAssets(const QString& theModuleID) const; + /*! \returns assets, which describe modules' headers, not their content. */ + std::map> getModuleAssets() const { return myModuleAssets; } + /*! \brief Retrieves module name, if the asset was loaded using \ref setAssetsFromResources(). If theLang is empty, it is effectively current language. */ QString getModuleName(const QString& theModuleID, const QString& theLang = "") const; @@ -366,6 +374,8 @@ public: std::shared_ptr getActionAssets(const QString& theActionID) const; + std::map>> getActionAssets() const { return myActionAssets; } + /*! \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 +472,179 @@ private: Sets of moduleIDs and inModuleActionIDs may NOT be equal for myActions and myShortcutContainer. */ - /* {actionID, assets}[] */ - std::map> myActionAssets; + /* { moduleID, {inModuleActionID, assets}[] }[] */ + std::map>> myActionAssets; /* {moduleID, assets}[] */ mutable std::map> 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 match metrics. The metrics >= 0. INF means mismatch. + The class is unable to differentiate exact match with some approximate matches! */ + double match(const QString& theInputString) const; + + /** \brief For debug. */ + QString toString() const; + +private: + static bool makePermutatedSentences(const QStringList& theWords, QList& theSentences); + static void makeFuzzyWords(const QStringList& theWords, QStringList& theFuzzyWords); + + /*! \returns number of characters in matched words. The number >= 0. */ + static int matchWithSentenceIgnoreEndings(const QString& theInputString, const QStringList& theSentence, bool theCaseSensitive); + /*! \returns number of characters in matched words. The number >= 0. */ + static int matchWithSentencesIgnoreEndings(const QString& theInputString, const QList& theSentences, bool theCaseSensitive); + + /*! \returns number of characters in matched words. The number >= 0. */ + static int matchAtLeastOneWord(const QString& theInputString, const QStringList& theWords, bool theCaseSensitive); + + /*! \returns number of characters in matched words. The number >= 0. */ + static int match( + const QString& theInputString, + const QStringList& theSentence, + bool theCaseSensitive + ); + + /*! \returns number of characters in matched words. The number >= 0. */ + static int match( + const QString& theInputString, + const QList& 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; + + QStringList myWords; // It is also original search sentence. + QList myPermutatedSentences; + + QStringList myFuzzyWords; // Regexes. + QList myFuzzyPermutatedSentences; +}; + + +/*! + \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 + }; + + class AssetsAndSearchData + { + public: + AssetsAndSearchData(std::shared_ptr theAssets = nullptr, double theMatchMetrics = std::numeric_limits::infinity()); + + void setMatchMetrics(double theMatchMetrics); + double matchMetrics() const { return myMatchMetrics; }; + + std::shared_ptr myAssets; + + void toJSON(QJsonObject& oJsonObject) const; + QString toString() const; + + private: + /*! \brief Ideally it should be number of weighted character permutations. Now it is just a number of characters in unmatched words. */ + double myMatchMetrics; + }; + + /*! 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 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& 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>& 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 matching metrics is changed for at least one result. */ + std::pair filter(); + + /*! \brief Applies filter to search results only. + \returns { true, _ } if set of results is shrunk; { _ , true } if matching metrics is changed for at least one result. */ + std::pair filterResults(); + + /*! \brief Applies filter only to actions, which are not in search results. + \returns True, if set of results is extended. */ + bool extendResults(); + + double matchAction(const QString& theModuleID, const QString& theInModuleActionID, std::shared_ptr theAssets); + + QString toString() const; + + + std::set myIncludedModuleIDs; + bool myIncludeDisabledActions; + + std::set myFieldsToMatch; + SUIT_SentenceMatcher myMatcher; + + /* { moduleID, {inModuleActionID, assetsAndSearchData}[] }[]. */ + std::map> mySearchResults; +}; + + #if defined WIN32 #pragma warning( default: 4251 ) #endif diff --git a/src/SUIT/SUIT_ShortcutTree.cxx b/src/SUIT/SUIT_ShortcutTree.cxx new file mode 100644 index 000000000..fd5d66bf2 --- /dev/null +++ b/src/SUIT/SUIT_ShortcutTree.cxx @@ -0,0 +1,880 @@ +// 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 "SUIT_ShortcutTree.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + + +#define COLUMN_SIZE 500 + + +SUIT_KeySequenceEdit::SUIT_KeySequenceEdit(QWidget* parent) +: QFrame(parent) +{ + initialize(); + myKeySequenceLineEdit->installEventFilter(this); +} + +/*! \brief Set a key sequence to edit. */ +void SUIT_KeySequenceEdit::setConfirmedKeySequence(const QKeySequence& theKeySequence) +{ + myConfirmedKeySequenceString = theKeySequence.toString(); + myKeySequenceLineEdit->setText(myConfirmedKeySequenceString); + myPrevKeySequenceString = myConfirmedKeySequenceString; +} + +void SUIT_KeySequenceEdit::setEditedKeySequence(const QKeySequence& theKeySequence) +{ + const QString keySequenceString = theKeySequence.toString(); + myKeySequenceLineEdit->setText(keySequenceString); + myPrevKeySequenceString = keySequenceString; +} + +QKeySequence SUIT_KeySequenceEdit::editedKeySequence() const +{ + return QKeySequence::fromString(myKeySequenceLineEdit->text()); +} + +/*! \returns true, if the edited key sequence differs from confirmed one. */ +bool SUIT_KeySequenceEdit::isKeySequenceModified() const +{ + return QKeySequence(myConfirmedKeySequenceString) != editedKeySequence(); +} + +/*! \brief Set confirmed key sequence to line editor. */ +void SUIT_KeySequenceEdit::restoreKeySequence() +{ + myKeySequenceLineEdit->setText(myConfirmedKeySequenceString); + myPrevKeySequenceString = myConfirmedKeySequenceString; +} + +/*! + \brief Gets the key sequence from keys that were pressed + \param e a key event + \returns a string representation of the key sequence +*/ +/*static*/ QString SUIT_KeySequenceEdit::parseEvent(QKeyEvent* e) +{ + bool isShiftPressed = e->modifiers() & Qt::ShiftModifier; + bool isControlPressed = e->modifiers() & Qt::ControlModifier; + bool isAltPressed = e->modifiers() & Qt::AltModifier; + bool isMetaPressed = e->modifiers() & Qt::MetaModifier; + bool isModifiersPressed = isControlPressed || isAltPressed || isMetaPressed; // Do not treat Shift alone as a modifier! + int result=0; + if(isControlPressed) + result += Qt::CTRL; + if(isAltPressed) + result += Qt::ALT; + if(isShiftPressed) + result += Qt::SHIFT; + if(isMetaPressed) + result += Qt::META; + + int aKey = e->key(); + if ((isValidKey(aKey) && isModifiersPressed) || ((aKey >= Qt::Key_F1) && (aKey <= Qt::Key_F12))) + result += aKey; + + return QKeySequence(result).toString(); +} + +/*! + \brief Check if the key event contains a 'valid' key + \param theKey the code of the key + \returns \c true if the key is 'valid' +*/ +/*static*/ bool SUIT_KeySequenceEdit::isValidKey(int theKey) +{ + if ( theKey == Qt::Key_Underscore || theKey == Qt::Key_Escape || + ( theKey >= Qt::Key_Backspace && theKey <= Qt::Key_Delete ) || + ( theKey >= Qt::Key_Home && theKey <= Qt::Key_PageDown ) || + ( theKey >= Qt::Key_F1 && theKey <= Qt::Key_F12 ) || + ( theKey >= Qt::Key_Space && theKey <= Qt::Key_Asterisk ) || + ( theKey >= Qt::Key_Comma && theKey <= Qt::Key_AsciiTilde ) ) + return true; + return false; +} + +/*! \brief Called when "Clear" button is clicked. */ +void SUIT_KeySequenceEdit::onClear() +{ + myKeySequenceLineEdit->setText(""); + myPrevKeySequenceString = ""; + emit editingFinished(); +} + +/*! \brief Called when myKeySequenceLineEdit loses focus. */ +void SUIT_KeySequenceEdit::onEditingFinished() +{ + if (myKeySequenceLineEdit->text().endsWith("+")) + myKeySequenceLineEdit->setText(myPrevKeySequenceString); + else + myPrevKeySequenceString = myKeySequenceLineEdit->text(); + emit editingFinished(); +} + +/*! + \brief Custom event filter. + \param obj event receiver object + \param event event + \returns \c true if further event processing should be stopped +*/ +bool SUIT_KeySequenceEdit::eventFilter(QObject* theObject, QEvent* theEvent) +{ + if (theObject == myKeySequenceLineEdit) { + if (theEvent->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(theEvent); + QString text = parseEvent(keyEvent); + if (keyEvent->key() == Qt::Key_Delete || keyEvent->key() == Qt::Key_Backspace) + myKeySequenceLineEdit->setText(""); + if (!text.isEmpty()) + myKeySequenceLineEdit->setText(text); + + emit editingStarted(); + return true; + } + if (theEvent->type() == QEvent::KeyRelease) { + onEditingFinished(); + return true; + } + } + return false; +} + +/* + \brief Perform internal intialization. +*/ +void SUIT_KeySequenceEdit::initialize() +{ + static const int PIXMAP_SIZE = 30; + + QHBoxLayout* base = new QHBoxLayout( this ); + base->setMargin(0); + base->setSpacing(5); + + base->addWidget(myKeySequenceLineEdit = new QLineEdit(this)); + setFocusProxy(myKeySequenceLineEdit); + + QToolButton* clearBtn = new QToolButton(); + auto clearPixmap = QPixmap(":/images/shortcut_disable.svg"); + clearPixmap.scaled(QSize(PIXMAP_SIZE, PIXMAP_SIZE), Qt::KeepAspectRatio, Qt::SmoothTransformation); + clearBtn->setIcon(clearPixmap); + clearBtn->setToolTip(tr("Disable shortcut.")); + base->addWidget(clearBtn); + + QToolButton* restoreBtn = new QToolButton(); + auto restorePixmap = QPixmap(":/images/shortcut_restore.svg"); + restorePixmap.scaled(QSize(PIXMAP_SIZE, PIXMAP_SIZE), Qt::KeepAspectRatio, Qt::SmoothTransformation); + restoreBtn->setIcon(restorePixmap); + restoreBtn->setToolTip(tr("Restore the currently applied key sequence.")); + base->addWidget(restoreBtn); + + myKeySequenceLineEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); + clearBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + restoreBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + connect(clearBtn, SIGNAL(clicked()), this, SLOT(onClear())); + connect(restoreBtn, SIGNAL(clicked()), this, SIGNAL(restoreFromShortcutMgrClicked())); + connect(myKeySequenceLineEdit, SIGNAL(editingFinished()), this, SLOT(onEditingFinished())); +} + + +/*! \param theParent must not be nullptr. */ +SUIT_EditKeySequenceDialog::SUIT_EditKeySequenceDialog(SUIT_ShortcutTree* theParent) +: QDialog(theParent) +{ + setMinimumWidth(500); + setWindowTitle(tr("Change key sequence")); + QVBoxLayout* layout = new QVBoxLayout(this); + myActionName = new QLabel(this); + myActionName->setTextFormat(Qt::RichText); + myKeySequenceEdit = new SUIT_KeySequenceEdit(this); + myTextEdit = new QTextEdit(this); + layout->addWidget(myActionName); + layout->addWidget(myKeySequenceEdit); + layout->addWidget(myTextEdit); + myActionName->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); + myKeySequenceEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); + myTextEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + myTextEdit->setReadOnly(true); + myTextEdit->setAcceptRichText(true); + myTextEdit->setPlaceholderText(tr("No conflicts.")); + setFocusProxy(myKeySequenceEdit); + + QHBoxLayout* buttonLayout = new QHBoxLayout(this); + layout->addLayout(buttonLayout); + QPushButton* confirmButton = new QPushButton(tr("Confirm"), this); + confirmButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + QPushButton* cancelButton = new QPushButton(tr("Cancel"), this); + cancelButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + buttonLayout->addStretch(); + buttonLayout->addWidget(confirmButton); + buttonLayout->addWidget(cancelButton); + + connect(myKeySequenceEdit, SIGNAL(editingStarted()), this, SLOT(onEditingStarted())); + connect(myKeySequenceEdit, SIGNAL(editingFinished()), this, SLOT(onEditingFinished())); + connect(myKeySequenceEdit, SIGNAL(restoreFromShortcutMgrClicked()), this, SLOT(onRestoreFromShortcutMgr())); + connect(confirmButton, SIGNAL(clicked()), this, SLOT(onConfirm())); + connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject())); +} + +void SUIT_EditKeySequenceDialog::setModuleAndActionID(const QString& theModuleID, const QString& theInModuleActionID) +{ + myModuleID = theModuleID; + myInModuleActionID = theInModuleActionID; +} + +const QString& SUIT_EditKeySequenceDialog::moduleID() const { return myModuleID; } +const QString& SUIT_EditKeySequenceDialog::inModuleActionID() const { return myInModuleActionID; } + +void SUIT_EditKeySequenceDialog::setModuleAndActionName(const QString& theModuleName, const QString& theActionName, const QString& theActionToolTip) +{ + myActionName->setText("" + theModuleName + "  " + theActionName); + myActionName->setToolTip(theActionToolTip); +} + +void SUIT_EditKeySequenceDialog::setConfirmedKeySequence(const QKeySequence& theSequence) +{ + myKeySequenceEdit->setConfirmedKeySequence(theSequence); +} + +QKeySequence SUIT_EditKeySequenceDialog::editedKeySequence() const +{ + return myKeySequenceEdit->editedKeySequence(); +} + +int SUIT_EditKeySequenceDialog::exec() +{ + myKeySequenceEdit->setFocus(Qt::ActiveWindowFocusReason); + return QDialog::exec(); +} + +void SUIT_EditKeySequenceDialog::onEditingStarted() +{ + myTextEdit->setEnabled(false); +} + +void SUIT_EditKeySequenceDialog::onEditingFinished() +{ + updateConflictsMessage(); +} + +void SUIT_EditKeySequenceDialog::onRestoreFromShortcutMgr() +{ + const auto shortcutMgr = SUIT_ShortcutMgr::get(); + myKeySequenceEdit->setEditedKeySequence(shortcutMgr->getKeySequence(myModuleID, myInModuleActionID)); + updateConflictsMessage(); +} + +/*! Updates message with list of actions, whose shortcuts will be disabled on Confirm. */ +void SUIT_EditKeySequenceDialog::updateConflictsMessage() +{ + myTextEdit->setEnabled(true); + QTextDocument* doc = myTextEdit->document(); + if (!doc) { + doc = new QTextDocument(myTextEdit); + myTextEdit->setDocument(doc); + } + + if (!myKeySequenceEdit->isKeySequenceModified()) { + doc->clear(); + return; + } + + const QKeySequence newKeySequence = editedKeySequence(); + + const auto shortcutTree = static_cast(parentWidget()); + /** {moduleID, inModuleActionID}[] */ + std::set> conflicts = shortcutTree->shortcutContainer()->getConflicts(myModuleID, myInModuleActionID, newKeySequence); + if (!conflicts.empty()) { + const auto shortcutMgr = SUIT_ShortcutMgr::get(); + + QString report = "" + tr("These shortcuts will be disabled on confirm:") + ""; + { + report += "
    "; + for (const auto& conflict : conflicts) { + const QString conflictingModuleName = shortcutMgr->getModuleName(conflict.first); + const QString conflictingActionName = shortcutMgr->getActionName(conflict.first, conflict.second); + report += "
  • " + conflictingModuleName + "  " + conflictingActionName + "
  • "; + } + report += "
"; + } + doc->setHtml(report); + } + else /* if no conflicts */ { + doc->clear(); + } +} + +void SUIT_EditKeySequenceDialog::onConfirm() +{ + if (myKeySequenceEdit->isKeySequenceModified()) + accept(); + else + reject(); +} + + +/*! \brief Compensates lack of std::distance(), which is introduced in C++17. +\returns -1, if theIt does not belong to the */ +template +size_t indexOf( + const Container& theContainer, + const typename Container::iterator& theIt +) { + auto it = theContainer.begin(); + size_t distance = 0; + while (it != theContainer.end()) { + if (it == theIt) + return distance; + + it++; + distance++; + } + return -1; +} + + +/*! \param theContainer Share the same container between several trees, +to edit them synchronously even without exchange of changes with SUIT_ShortcutMgr. +Pass nullptr to create non-synchronized tree. */ +SUIT_ShortcutTree::SUIT_ShortcutTree( + std::shared_ptr theContainer, + QWidget* theParent +) : QTreeWidget(theParent), +myShortcutContainer(theContainer ? theContainer : std::shared_ptr(new SUIT_ShortcutContainer())), +mySortKey(SUIT_ShortcutTree::SortKey::Name), mySortOrder(SUIT_ShortcutTree::SortOrder::Ascending) +{ + setColumnCount(2); + setSelectionMode(QAbstractItemView::SingleSelection); + setColumnWidth(0, COLUMN_SIZE); + setSortingEnabled(false); // Items are sorted in the same way, as in ShortcutContainer. + header()->setSectionResizeMode(QHeaderView::Interactive); + { + QMap labelMap; + labelMap[SUIT_ShortcutTree::ElementIdx::Name] = tr("Action"); + labelMap[SUIT_ShortcutTree::ElementIdx::KeySequence] = tr("Key sequence"); + setHeaderLabels(labelMap.values()); + } + setExpandsOnDoubleClick(false); // Open shortcut editor on double click instead. + setSortingEnabled(false); + setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + myEditDialog = new SUIT_EditKeySequenceDialog(this); + + this->installEventFilter(this); + connect(this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*, int)), this, SLOT(onItemDoubleClicked(QTreeWidgetItem*, int))); + + SUIT_ShortcutTree::instances[myShortcutContainer.get()].emplace(this); +} + +SUIT_ShortcutTree::~SUIT_ShortcutTree() +{ + SUIT_ShortcutTree::instances[myShortcutContainer.get()].erase(this); + if (SUIT_ShortcutTree::instances[myShortcutContainer.get()].empty()) + SUIT_ShortcutTree::instances.erase(myShortcutContainer.get()); +} + +/*! \brief Copies shortcuts from ShortcutMgr. (Re)displays shortcuts of myModuleIDs. */ +void SUIT_ShortcutTree::setShortcutsFromManager() +{ + const auto shortcutMgr = SUIT_ShortcutMgr::get(); + *myShortcutContainer = shortcutMgr->getShortcutContainer(); + // nb! ShortcutMgr never removes shortcuts from its container, only disables. + + updateItems(false /*theHighlightModified*/, true /*theUpdateSyncTrees*/); +} + +/*! \brief Copies shortcuts from resources, user files are not accounted. (Re)displays shortcuts of myModuleIDs. */ +void SUIT_ShortcutTree::setDefaultShortcuts() +{ + SUIT_ShortcutContainer defaultShortcuts; + SUIT_ShortcutMgr::fillContainerFromPreferences(defaultShortcuts, true /*theDefaultOnly*/); + + myShortcutContainer->merge(defaultShortcuts, true /*theOverride*/, true /*theTreatAbsentIncomingAsDisabled*/); + // nb! SUIT_ShortcutContainer never erases shortcuts, only disables. + + updateItems(true /*theHighlightModified*/, true /*theUpdateSyncTrees*/); +} + +/*! \brief Applies pending changes to ShortcutMgr. Updates other instances of SUIT_ShortcutTree. */ +void SUIT_ShortcutTree::applyChangesToShortcutMgr() +{ + const auto mgr = SUIT_ShortcutMgr::get(); + mgr->mergeShortcutContainer(*myShortcutContainer); + + // Update non-synchronized with this instances. + for (const auto& containerAndSyncTrees : SUIT_ShortcutTree::instances) { + if (containerAndSyncTrees.first == myShortcutContainer.get()) + continue; + + const std::set& syncTrees = containerAndSyncTrees.second; + const auto itFirstSyncTree = syncTrees.begin(); + if (itFirstSyncTree == syncTrees.end()) + continue; + + (*itFirstSyncTree)->setShortcutsFromManager(); + const auto editDialog = (*itFirstSyncTree)->myEditDialog; + editDialog->setConfirmedKeySequence(mgr->getShortcutContainer().getKeySequence(editDialog->moduleID(), editDialog->inModuleActionID())); + editDialog->updateConflictsMessage(); + } +} + +std::shared_ptr SUIT_ShortcutTree::shortcutContainer() const +{ + return myShortcutContainer; +} + +/*! \brief Does not sort modules. */ +void SUIT_ShortcutTree::sort(SUIT_ShortcutTree::SortKey theKey, SUIT_ShortcutTree::SortOrder theOrder) +{ + if (theKey == mySortKey && theOrder == mySortOrder) + return; + + mySortKey == theKey; + mySortOrder = theOrder; + + for (int moduleIdx = 0; moduleIdx < topLevelItemCount(); moduleIdx++) { + const auto moduleItem = static_cast(topLevelItem(moduleIdx)); + const auto sortedChildren = getSortedChildren(moduleItem); + moduleItem->takeChildren(); + + for (const auto childItem : sortedChildren) { + moduleItem->addChild(childItem); + } + } +} + +/*! \param If theUpdateSyncTrees, trees sharing the same shortcut container are updated. */ +void SUIT_ShortcutTree::updateItems(bool theHighlightModified, bool theUpdateSyncTrees) +{ + const auto shortcutMgr = SUIT_ShortcutMgr::get(); + const QString lang = SUIT_ShortcutMgr::getLang(); + + for (const QString& moduleID : myModuleIDs) { + const auto& moduleShortcuts = myShortcutContainer->getModuleShortcutsInversed(moduleID); + if (moduleShortcuts.empty()) { + // Do not display empty module. + const auto moduleItemAndIdx = findModuleFolderItem(moduleID); + if (moduleItemAndIdx.second >= 0) + delete takeTopLevelItem(moduleItemAndIdx.second); + + continue; + } + + const auto moduleItemAndIdx = findModuleFolderItem(moduleID); + SUIT_ShortcutTreeFolder* moduleItem = moduleItemAndIdx.first; + if (!moduleItem) { + moduleItem = new SUIT_ShortcutTreeFolder(moduleID); + moduleItem->setAssets(shortcutMgr->getModuleAssets(moduleID), lang); + addTopLevelItem(moduleItem); + moduleItem->setFlags(Qt::ItemIsEnabled); + + auto sortedChildren = getSortedChildren(moduleItem); + for (const auto& shortcut : moduleShortcuts) { + const QString& inModuleActionID = shortcut.first; + const QKeySequence& keySequence = shortcut.second; + const QString keySequenceString = keySequence.toString(); + + auto actionItem = SUIT_ShortcutTreeAction::create(moduleID, inModuleActionID); + if (!actionItem) { + ShCutDbg("SUIT_ShortcutTree can't create child item for action ID = \"" + SUIT_ShortcutMgr::makeActionID(moduleID, inModuleActionID) + "\"."); + continue; + } + + actionItem->setAssets(shortcutMgr->getActionAssets(moduleID, inModuleActionID), lang); + actionItem->setKeySequence(keySequenceString); + + if (theHighlightModified) { + const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, inModuleActionID); + actionItem->highlightKeySequenceAsModified(keySequence != appliedKeySequence); + } + + insertChild(moduleItem, sortedChildren, actionItem); + } + + moduleItem->setExpanded(true); // Make tree expanded on first show. + } + else /* if the tree has the module-item */ { + for (int childIdx = 0; childIdx < moduleItem->childCount(); childIdx++) { + // Update exisiting items of a module. + SUIT_ShortcutTreeAction* const childItem = static_cast(moduleItem->child(childIdx)); + const auto itShortcut = moduleShortcuts.find(childItem->myInModuleActionID); + if (itShortcut == moduleShortcuts.end()) { + // Shortcut of the item has been removed from myShortcutContainer - impossible. + continue; + } + const QKeySequence& newKeySequence = itShortcut->second; + const QString newKeySequenceString = newKeySequence.toString(); + if (childItem->keySequence() != newKeySequenceString) + childItem->setKeySequence(newKeySequenceString); + + if (theHighlightModified) { + const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, childItem->myInModuleActionID); + childItem->highlightKeySequenceAsModified(newKeySequence != appliedKeySequence); + } + else + childItem->highlightKeySequenceAsModified(false); + } + + // Add new items if myShortcutContainer acquired new shortcuts, which may happen if a developer forgot + // to add shortcuts for registered actions to resource files. + if (moduleItem->childCount() < moduleShortcuts.size()) { + auto sortedChildren = getSortedChildren(moduleItem); + for (const auto& shortcut : moduleShortcuts) { + const QString& inModuleActionID = shortcut.first; + const auto predicate = [&inModuleActionID](const SUIT_ShortcutTreeItem* const theItem) -> bool { + return static_cast(theItem)->myInModuleActionID == inModuleActionID; + }; + + if (std::find_if(sortedChildren.begin(), sortedChildren.end(), predicate) == sortedChildren.end()) { + const auto actionItem = SUIT_ShortcutTreeAction::create(moduleID, inModuleActionID); + if (!actionItem) { + ShCutDbg("SUIT_ShortcutTree can't create child item for action ID = \"" + SUIT_ShortcutMgr::makeActionID(moduleID, inModuleActionID) + "\"."); + continue; + } + + const QKeySequence& keySequence = shortcut.second; + actionItem->setAssets(shortcutMgr->getActionAssets(moduleID, inModuleActionID), lang); + actionItem->setKeySequence(keySequence.toString()); + + if (theHighlightModified) { + const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, inModuleActionID); + actionItem->highlightKeySequenceAsModified(keySequence != appliedKeySequence); + } + + insertChild(moduleItem, sortedChildren, actionItem); + } + } + } + } + } + + if (theUpdateSyncTrees) { + const std::set& syncTrees = SUIT_ShortcutTree::instances[myShortcutContainer.get()]; + for (const auto syncTree: syncTrees) { + if (syncTree == this) + continue; + + syncTree->updateItems(theHighlightModified, false /*theUpdateSyncTrees*/); + const auto editDialog = syncTree->myEditDialog; + editDialog->setConfirmedKeySequence(myShortcutContainer->getKeySequence(editDialog->moduleID(), editDialog->inModuleActionID())); + editDialog->updateConflictsMessage(); + } + } +} + +/*! \returns Pointer and index of top-level item. +If the tree does not contain an item with theModuleID, returns {nullptr, -1}. */ +std::pair SUIT_ShortcutTree::findModuleFolderItem(const QString& theModuleID) const +{ + for (int moduleIdx = 0; moduleIdx < topLevelItemCount(); moduleIdx++) { + SUIT_ShortcutTreeFolder* moduleItem = static_cast(topLevelItem(moduleIdx)); + if (moduleItem->myModuleID == theModuleID) + return std::pair(moduleItem, moduleIdx); + } + return std::pair(nullptr, -1); +} + +/*! \returns Children of theParentItem being sorted according to current sort mode and order. */ +std::set> SUIT_ShortcutTree::getSortedChildren(SUIT_ShortcutTreeFolder* theParentItem) +{ + QList> sortSchema = SUIT_ShortcutTree::DEFAULT_SORT_SCHEMA; + { + for (auto itSameKey = sortSchema.begin(); itSameKey != sortSchema.end(); itSameKey++) { + if (itSameKey->first == mySortKey) { + sortSchema.erase(itSameKey); + break; + } + } + sortSchema.push_front(std::pair(mySortKey, mySortOrder)); + } + + static const QCollator collator; + const std::function comparator = + [this, sortSchema, &collator](const SUIT_ShortcutTreeItem* theItemA, const SUIT_ShortcutTreeItem* theItemB) { + for (const auto& keyAndOrder : sortSchema) { + const int res = collator.compare(theItemA->getValue(keyAndOrder.first), theItemB->getValue(keyAndOrder.first)); + if (res != 0) + return keyAndOrder.second == SUIT_ShortcutTree::SortOrder::Ascending ? res < 0 : res > 0; + } + return false; + }; + + std::set> sortedChildren(comparator); + for (int childIdx = 0; childIdx < theParentItem->childCount(); childIdx++) { + SUIT_ShortcutTreeAction* const childItem = static_cast(theParentItem->child(childIdx)); + sortedChildren.emplace(childItem); + } + return sortedChildren; +} + +/*! \brief Inserts theChildItem to theParentItem and theSortedChildren. +Does not check whether theSortedChildren are actually child items of theParentItem. +Does not check whether current item sort schema is same as one of theSortedChildren. */ +void SUIT_ShortcutTree::insertChild( + SUIT_ShortcutTreeFolder* theParentItem, + std::set>& theSortedChildren, + SUIT_ShortcutTreeItem* theChildItem +) { + auto emplaceRes = theSortedChildren.emplace(theChildItem); + theParentItem->insertChild(indexOf(theSortedChildren, emplaceRes.first), theChildItem); +} + +void SUIT_ShortcutTree::onItemDoubleClicked(QTreeWidgetItem* theItem, int theColIdx) +{ + { + SUIT_ShortcutTreeItem* const item = static_cast(theItem); + // Do not react if folder-item is clicked. + if (item->type() != SUIT_ShortcutTreeItem::Type::Action) + return; + } + + SUIT_ShortcutTreeAction* const actionItem = static_cast(theItem); + + myEditDialog->setModuleAndActionID(actionItem->myModuleID, actionItem->myInModuleActionID); + QString actionToolTip = actionItem->toolTip(SUIT_ShortcutTree::ElementIdx::Name); + actionToolTip.truncate(actionToolTip.lastIndexOf('\n') + 1); + myEditDialog->setModuleAndActionName( + static_cast(actionItem->parent())->name(), + actionItem->name(), + actionToolTip + ); + myEditDialog->setConfirmedKeySequence(QKeySequence::fromString(actionItem->keySequence())); + myEditDialog->updateConflictsMessage(); + const bool somethingChanged = myEditDialog->exec() == QDialog::Accepted; + + if (!somethingChanged) + return; + + const QKeySequence newKeySequence = myEditDialog->editedKeySequence(); + + /** { moduleID, inModuleActionID }[] */ + std::set> disabledActionIDs = myShortcutContainer->setShortcut(actionItem->myModuleID, actionItem->myInModuleActionID, newKeySequence, true /*override*/); + + /** { moduleID, {inModuleActionID, keySequence}[] }[] */ + std::map> changes; + changes[actionItem->myModuleID][actionItem->myInModuleActionID] = newKeySequence.toString(); + for (const auto moduleAndActionID : disabledActionIDs) { + changes[moduleAndActionID.first][moduleAndActionID.second] = QString(); + } + + // Set new key sequences to shortcut items. + for (const auto& moduleIDAndChanges : changes) { + const QString& moduleID = moduleIDAndChanges.first; + + const auto moduleItemAndIdx = findModuleFolderItem(moduleID); + const auto moduleItem = moduleItemAndIdx.first; + if (!moduleItem) + continue; + + /** {inModuleActionID, newKeySequence}[] */ + const std::map& moduleChanges = moduleIDAndChanges.second; + + // Go through module' shortcut items, and highlight those, whose key sequences differ from applied key sequences. + for (int childIdx = 0; childIdx < moduleItem->childCount(); childIdx++) { + SUIT_ShortcutTreeAction* const childItem = static_cast(moduleItem->child(childIdx)); + const auto itChange = moduleChanges.find(childItem->myInModuleActionID); + if (itChange == moduleChanges.end()) { + // The shortcut has not been changed. + continue; + } + + childItem->setKeySequence(itChange->second); + + const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, childItem->myInModuleActionID); + childItem->highlightKeySequenceAsModified(QKeySequence::fromString(itChange->second) != appliedKeySequence); + } + } +} + +/*static*/ const QList> SUIT_ShortcutTree::DEFAULT_SORT_SCHEMA = +{ + {SUIT_ShortcutTree::SortKey::Name, SUIT_ShortcutTree::SortOrder::Ascending}, + {SUIT_ShortcutTree::SortKey::ToolTip, SUIT_ShortcutTree::SortOrder::Ascending}, + {SUIT_ShortcutTree::SortKey::KeySequence, SUIT_ShortcutTree::SortOrder::Ascending}, + {SUIT_ShortcutTree::SortKey::ID, SUIT_ShortcutTree::SortOrder::Ascending} +}; + +/*static*/ std::map> SUIT_ShortcutTree::instances = +std::map>(); + + + +SUIT_ShortcutTreeItem::SUIT_ShortcutTreeItem(const QString& theModuleID) +: QTreeWidgetItem(), myModuleID(theModuleID) +{ } + +QString SUIT_ShortcutTreeItem::name() const +{ + return text(SUIT_ShortcutTree::ElementIdx::Name); +} + + +SUIT_ShortcutTreeFolder::SUIT_ShortcutTreeFolder(const QString& theModuleID) +: SUIT_ShortcutTreeItem(theModuleID) +{ + QFont f = font(SUIT_ShortcutTree::ElementIdx::Name); + f.setBold(true); + setFont(SUIT_ShortcutTree::ElementIdx::Name, f); + setText(SUIT_ShortcutTree::ElementIdx::Name, theModuleID); +} + +void SUIT_ShortcutTreeFolder::setAssets(std::shared_ptr theAssets, const QString& theLang) +{ + if (!theAssets) + return; + + setIcon(SUIT_ShortcutTree::ElementIdx::Name, theAssets->myIcon); + + const auto& ldaMap = theAssets->myLangDependentAssets; + if (ldaMap.empty()) { + setText(SUIT_ShortcutTree::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(SUIT_ShortcutTree::ElementIdx::Name, name); +} + +QString SUIT_ShortcutTreeFolder::getValue(SUIT_ShortcutTree::SortKey theKey) const +{ + switch (theKey) { + case SUIT_ShortcutTree::SortKey::ID: + return myModuleID; + case SUIT_ShortcutTree::SortKey::Name: + return name(); + case SUIT_ShortcutTree::SortKey::ToolTip: + return name(); + default: + return QString(); + } +} + + +SUIT_ShortcutTreeAction::SUIT_ShortcutTreeAction(const QString& theModuleID, const QString& theInModuleActionID) +: SUIT_ShortcutTreeItem(theModuleID), myInModuleActionID(theInModuleActionID) +{ + setText(SUIT_ShortcutTree::ElementIdx::Name, theInModuleActionID); + setToolTip( + SUIT_ShortcutTree::ElementIdx::Name, + theInModuleActionID + (theInModuleActionID.at(theInModuleActionID.length()-1) == "." ? "\n" : ".\n") + SUIT_ShortcutTree::tr("Double click to edit key sequence.") + ); + setToolTip(SUIT_ShortcutTree::ElementIdx::KeySequence, SUIT_ShortcutTree::tr("Double click to edit key sequence.")); +} + +/*static*/ SUIT_ShortcutTreeAction* SUIT_ShortcutTreeAction::create(const QString& theModuleID, const QString& theInModuleActionID) +{ + if (theInModuleActionID.isEmpty()) { + ShCutDbg("SUIT_ShortcutTreeItem: attempt to create item with empty action ID."); + return nullptr; + } + + return new SUIT_ShortcutTreeAction(theModuleID, theInModuleActionID); +} + +void SUIT_ShortcutTreeAction::setAssets(std::shared_ptr theAssets, const QString& theLang) +{ + if (!theAssets) + return; + + setIcon(SUIT_ShortcutTree::ElementIdx::Name, theAssets->myIcon); + + const auto& ldaMap = theAssets->myLangDependentAssets; + if (ldaMap.empty()) { + setText(SUIT_ShortcutTree::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(SUIT_ShortcutTree::ElementIdx::Name, name); + + const QString& actionToolTip = lda.myToolTip.isEmpty() ? name : lda.myToolTip; + setToolTip( + SUIT_ShortcutTree::ElementIdx::Name, + actionToolTip + (actionToolTip.at(actionToolTip.length()-1) == "." ? "\n" : ".\n") + SUIT_ShortcutTree::tr("Double click to edit key sequence.") + ); +} + +QString SUIT_ShortcutTreeAction::getValue(SUIT_ShortcutTree::SortKey theKey) const +{ + switch (theKey) { + case SUIT_ShortcutTree::SortKey::ID: + return myInModuleActionID; + case SUIT_ShortcutTree::SortKey::Name: + return name(); + case SUIT_ShortcutTree::SortKey::ToolTip: + return toolTip(SUIT_ShortcutTree::ElementIdx::Name); + case SUIT_ShortcutTree::SortKey::KeySequence: + return keySequence(); + default: + return QString(); + } +} + +void SUIT_ShortcutTreeAction::setKeySequence(const QString& theKeySequence) +{ + setText(SUIT_ShortcutTree::ElementIdx::KeySequence, theKeySequence); +} + +QString SUIT_ShortcutTreeAction::keySequence() const +{ + return text(SUIT_ShortcutTree::ElementIdx::KeySequence); +} + +/*! \brief Highlights text at ElementIdx::KeySequence. */ +void SUIT_ShortcutTreeAction::highlightKeySequenceAsModified(bool theHighlight) +{ + static const QBrush bgHighlitingBrush = QBrush(Qt::darkGreen); + static const QBrush fgHighlitingBrush = QBrush(Qt::white); + static const QBrush noBrush = QBrush(); + + setBackground(SUIT_ShortcutTree::ElementIdx::KeySequence, theHighlight ? bgHighlitingBrush : noBrush); + setForeground(SUIT_ShortcutTree::ElementIdx::KeySequence, theHighlight ? fgHighlitingBrush : noBrush); +} \ No newline at end of file diff --git a/src/SUIT/SUIT_ShortcutTree.h b/src/SUIT/SUIT_ShortcutTree.h new file mode 100644 index 000000000..7d2df2238 --- /dev/null +++ b/src/SUIT/SUIT_ShortcutTree.h @@ -0,0 +1,265 @@ +// 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 SUIT_SHORTCUTTREE_H +#define SUIT_SHORTCUTTREE_H + +#include "SUIT.h" +#include +#include +#include +#include "SUIT_ShortcutMgr.h" +#include +#include +#include +#include + + +class QLineEdit; +class QLabel; +class QPushButton; +class QTreeWidgetItem; + +class SUIT_EXPORT SUIT_KeySequenceEdit : public QFrame +{ + Q_OBJECT + +public: + SUIT_KeySequenceEdit(QWidget* = nullptr); + virtual ~SUIT_KeySequenceEdit() = default; + + void setConfirmedKeySequence(const QKeySequence&); + void setEditedKeySequence(const QKeySequence&); + QKeySequence editedKeySequence() const; + bool isKeySequenceModified() const; + void restoreKeySequence(); + + static QString parseEvent(QKeyEvent*); + static bool isValidKey(int); + +signals: + void editingStarted(); + void editingFinished(); + void restoreFromShortcutMgrClicked(); + +private slots: + void onClear(); + void onEditingFinished(); + +protected: + virtual bool eventFilter(QObject*, QEvent*); + +private: + void initialize(); + +private: + QLineEdit* myKeySequenceLineEdit; + QString myConfirmedKeySequenceString; + + // Last valid key sequence string from myKeySequenceLineEdit. + QString myPrevKeySequenceString; +}; + + +class SUIT_ShortcutTree; +class SUIT_ShortcutTreeItem; +class SUIT_ShortcutTreeFolder; +class SUIT_ShortcutTreeAction; +class QTextEdit; + + +class SUIT_EXPORT SUIT_EditKeySequenceDialog : public QDialog +{ + Q_OBJECT + +public: + SUIT_EditKeySequenceDialog(SUIT_ShortcutTree* theParent); + SUIT_EditKeySequenceDialog(const SUIT_EditKeySequenceDialog&) = delete; + SUIT_EditKeySequenceDialog& operator=(const SUIT_EditKeySequenceDialog&) = delete; + virtual ~SUIT_EditKeySequenceDialog() = default; + + void setModuleAndActionID(const QString& theModuleID, const QString& theInModuleActionID); + const QString& moduleID() const; + const QString& inModuleActionID() const; + + void setModuleAndActionName(const QString& theModuleName, const QString& theActionName, const QString& theActionToolTip = ""); + + void setConfirmedKeySequence(const QKeySequence& theSequence); + QKeySequence editedKeySequence() const; + + void updateConflictsMessage(); + + int exec(); + +private slots: + void onEditingStarted(); + void onEditingFinished(); + void onRestoreFromShortcutMgr(); + void onConfirm(); + +private: + QString myModuleID; + QString myInModuleActionID; + QLabel* myActionName; + SUIT_KeySequenceEdit* myKeySequenceEdit; + QTextEdit* myTextEdit; +}; + + +class SUIT_EXPORT SUIT_ShortcutTree : public QTreeWidget +{ + Q_OBJECT + +public: + enum ElementIdx { + Name = 0, + KeySequence = 1, // Empty, if item is folder item. + }; + + enum class SortKey { + ID, + Name, + ToolTip, + KeySequence, + }; + + enum class SortOrder { + Ascending, + Descending + }; + + SUIT_ShortcutTree( + std::shared_ptr theContainer = std::shared_ptr(), + QWidget* theParent = nullptr + ); + SUIT_ShortcutTree(const SUIT_ShortcutTree&) = delete; + SUIT_ShortcutTree& operator=(const SUIT_ShortcutTree&) = delete; + virtual ~SUIT_ShortcutTree(); + + void setShortcutsFromManager(); + void setDefaultShortcuts(); + void applyChangesToShortcutMgr(); + + std::shared_ptr shortcutContainer() const; + + void sort(SUIT_ShortcutTree::SortKey theKey, SUIT_ShortcutTree::SortOrder theOrder); + +private: + void updateItems(bool theHighlightModified, bool theUpdateSyncTrees); + std::pair findModuleFolderItem(const QString& theModuleID) const; + + std::set> getSortedChildren(SUIT_ShortcutTreeFolder* theParentItem); + + void insertChild( + SUIT_ShortcutTreeFolder* theParentItem, + std::set>& theSortedChildren, + SUIT_ShortcutTreeItem* theChildItem + ); + +private slots: + void onItemDoubleClicked(QTreeWidgetItem* theWidgetItem, int theColIdx); + +public: + /** Keeps IDs of modules, which will are shown on setShortcutsFromManager(). */ + std::set myModuleIDs; + + static const QList> DEFAULT_SORT_SCHEMA; + +private: + /** Allows to modify plenty of shortcuts and then apply them to SUIT_ShortcutMgr as a batch. */ + const std::shared_ptr myShortcutContainer; + + SUIT_EditKeySequenceDialog* myEditDialog; + + SUIT_ShortcutTree::SortKey mySortKey; + SUIT_ShortcutTree::SortOrder mySortOrder; + + /** + * Ensures that, if several SUIT_ShortcutTree instances coexist, + * all of them are updated when one of them applies pending changes to SUIT_ShortcutMgr. + * + * Sharing of SUIT_ShortcutContainer allows to keep some trees synchronized even without + * applying changes to SUIT_ShortcutMgr. Why? See SUIT_PagePrefShortcutTreeItem. + * + * Access is not synchronized in assumption, that all instances live in the same thread. + */ + static std::map> instances; +}; + + +class SUIT_ShortcutTreeItem : public QTreeWidgetItem +{ +public: + enum Type { + Folder = 0, + Action = 1, + }; + +protected: + SUIT_ShortcutTreeItem(const QString& theModuleID); + +public: + virtual ~SUIT_ShortcutTreeItem() = default; + virtual SUIT_ShortcutTreeItem::Type type() const = 0; + + virtual void setAssets(std::shared_ptr theAssets, const QString& theLang) = 0; + QString name() const; + + virtual QString getValue(SUIT_ShortcutTree::SortKey theKey) const = 0; + +public: + const QString myModuleID; +}; + + +class SUIT_ShortcutTreeFolder : public SUIT_ShortcutTreeItem +{ +public: + SUIT_ShortcutTreeFolder(const QString& theModuleID); + virtual ~SUIT_ShortcutTreeFolder() = default; + virtual SUIT_ShortcutTreeItem::Type type() const { return SUIT_ShortcutTreeItem::Type::Folder; }; + + virtual void setAssets(std::shared_ptr theAssets, const QString& theLang); + + virtual QString getValue(SUIT_ShortcutTree::SortKey theKey) const; +}; + + +class SUIT_ShortcutTreeAction : public SUIT_ShortcutTreeItem +{ +private: + SUIT_ShortcutTreeAction(const QString& theModuleID, const QString& theInModuleActionID); + +public: + static SUIT_ShortcutTreeAction* create(const QString& theModuleID, const QString& theInModuleActionID); + virtual ~SUIT_ShortcutTreeAction() = default; + virtual SUIT_ShortcutTreeItem::Type type() const { return SUIT_ShortcutTreeItem::Type::Action; }; + + virtual void setAssets(std::shared_ptr theAssets, const QString& theLang); + + virtual QString getValue(SUIT_ShortcutTree::SortKey theKey) const; + + void setKeySequence(const QString& theKeySequence); + QString keySequence() const; + void highlightKeySequenceAsModified(bool theHighlight); + + const QString myInModuleActionID; +}; + +#endif // SUIT_SHORTCUTTREE_H diff --git a/src/SUIT/resources/SUIT_msg_fr.ts b/src/SUIT/resources/SUIT_msg_fr.ts index bdbbb196e..9b8a31906 100644 --- a/src/SUIT/resources/SUIT_msg_fr.ts +++ b/src/SUIT/resources/SUIT_msg_fr.ts @@ -160,6 +160,29 @@ Voulez-vous l'écraser ? Tous les fichiers (*) + + SUIT_FindActionDialog + + Find action + Trouver une action + + + Unavailable actions + Actions indisponibles + + + Inactive modules + Modules inactifs + + + Action + Action + + + Description + Description + + SUIT_ViewWindow @@ -198,4 +221,53 @@ Voulez-vous l'écraser ? Corrigez manuellement les entrées suivantes dans les fichiers de préférences + + SUIT_KeySequenceEdit + + Disable shortcut. + Désactivez le raccourci. + + + Restore the currently applied key sequence. + Restaurez la séquence de touches actuellement appliquée. + + + + SUIT_EditKeySequenceDialog + + Change key sequence + Modifier la séquence de touches + + + No conflicts. + Aucun conflit. + + + Confirm + Confirmer + + + Cancel + Annuler + + + These shortcuts will be disabled on confirm: + Ces raccourcis seront désactivés lors de la confirmation : + + + + SUIT_ShortcutTree + + Action + Action + + + Key sequence + Séquence de touches + + + Double click to edit key sequence. + Double-cliquez pour modifier la séquence de touches. + + diff --git a/src/SUIT/resources/SUIT_msg_ja.ts b/src/SUIT/resources/SUIT_msg_ja.ts index 2fb333637..ad3f28ddb 100644 --- a/src/SUIT/resources/SUIT_msg_ja.ts +++ b/src/SUIT/resources/SUIT_msg_ja.ts @@ -154,6 +154,29 @@ すべてのファイル (*) + + SUIT_FindActionDialog + + Find action + 検索アクション + + + Unavailable actions + 利用できないアクション + + + Inactive modules + 非アクティブなモジュール + + + Action + アクション + + + Description + 説明 + + SUIT_ViewWindow @@ -192,4 +215,53 @@ 設定ファイル内の次のエントリを手動で修正します + + SUIT_KeySequenceEdit + + Disable shortcut. + ショートカットを無効にします。 + + + Restore the currently applied key sequence. + 現在適用されているキー シーケンスを復元します。 + + + + SUIT_EditKeySequenceDialog + + Change key sequence + キーシーケンスを変更する + + + No conflicts. + 競合はありません。 + + + Confirm + 確認する + + + Cancel + キャンセル + + + These shortcuts will be disabled on confirm: + これらのショートカットは確認時に無効になります。 + + + + SUIT_ShortcutTree + + Action + アクション + + + Key sequence + キーシーケンス + + + Double click to edit key sequence. + ダブルクリックしてキー シーケンスを編集します。 + + diff --git a/src/SUIT/resources/action_assets.json b/src/SUIT/resources/action_assets.json index eefb7012c..70952aaec 100644 --- a/src/SUIT/resources/action_assets.json +++ b/src/SUIT/resources/action_assets.json @@ -17,7 +17,7 @@ } }, "/#Viewers/View/Reset": { - "iconPath": "", + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/reset.png", "langDependentAssets": { "en": { "name": "Reset", @@ -390,6 +390,23 @@ } } }, + "/PRP_DESK_FIND_ACTION": { + "iconPath": "", + "langDependentAssets": { + "en": { + "name": "Find action", + "tooltip": "Opens action search dialog" + }, + "fr": { + "name": "Trouver une action", + "tooltip": "Ouvre la boîte de dialogue de recherche d'action" + }, + "ja": { + "name": "検索アクション", + "tooltip": "アクション検索ダイアログを開きます" + } + } + }, "/PRP_DESK_CONNECT": { "iconPath": "", "langDependentAssets": { @@ -459,7 +476,7 @@ } }, "/PRP_DESK_HELP_ABOUT": { - "iconPath": "", + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/about.png", "langDependentAssets": { "en": { "name": "About...", @@ -510,7 +527,7 @@ } }, "/PRP_DESK_WINDOW_HSPLIT": { - "iconPath": "", + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/htile.png", "langDependentAssets": { "en": { "name": "Split Horizontally", @@ -527,7 +544,7 @@ } }, "/PRP_DESK_WINDOW_VSPLIT": { - "iconPath": "", + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/vtile.png", "langDependentAssets": { "en": { "name": "Split Vertically", @@ -578,7 +595,7 @@ } }, "/TOT_DESK_EDIT_PASTE": { - "iconPath": "", + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/paste.png", "langDependentAssets": { "en": { "name": "Paste", @@ -595,7 +612,7 @@ } }, "/TOT_DESK_FILE_CLOSE": { - "iconPath": "", + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/close.png", "langDependentAssets": { "en": { "name": "Close", @@ -629,7 +646,7 @@ } }, "/TOT_DESK_FILE_NEW": { - "iconPath": "", + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/new.png", "langDependentAssets": { "en": { "name": "New", @@ -646,7 +663,7 @@ } }, "/TOT_DESK_FILE_OPEN": { - "iconPath": "", + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/open.png", "langDependentAssets": { "en": { "name": "Open...", @@ -663,7 +680,7 @@ } }, "/TOT_DESK_FILE_SAVE": { - "iconPath": "", + "iconPath": "${GUI_ROOT_DIR}/share/salome/resources/gui/save.png", "langDependentAssets": { "en": { "name": "Save", diff --git a/src/SalomeApp/SalomeApp_Application.cxx b/src/SalomeApp/SalomeApp_Application.cxx index 6dc0e9eb5..a8300ea62 100644 --- a/src/SalomeApp/SalomeApp_Application.cxx +++ b/src/SalomeApp/SalomeApp_Application.cxx @@ -72,6 +72,7 @@ #include #include #include +#include #include @@ -108,6 +109,7 @@ #include #include +#include #include @@ -364,6 +366,11 @@ void SalomeApp_Application::createActions() tr( "MEN_DESK_REGISTRY_DISPLAY" ), tr( "PRP_DESK_REGISTRY_DISPLAY" ), /*Qt::SHIFT+Qt::Key_D*/0, desk, false, this, SLOT( onRegDisplay() ) ); + //! Find action dialog + createAction( FindActionId, tr( "TOT_DESK_FIND_ACTION" ), QIcon(), + tr( "MEN_DESK_FIND_ACTION" ), tr( "PRP_DESK_FIND_ACTION" ), + QKeySequence::UnknownKey, desk, false, this, SLOT( onFindAction() ), "/PRP_DESK_FIND_ACTION" ); + createAction( ConnectId, tr( "TOT_DESK_CONNECT_STUDY" ), QIcon(), tr( "MEN_DESK_CONNECT" ), tr( "PRP_DESK_CONNECT" ), QKeySequence::UnknownKey, desk, false, this, SLOT( onLoadDoc() ), "/PRP_DESK_CONNECT" ); @@ -396,6 +403,7 @@ void SalomeApp_Application::createActions() int toolsMenu = createMenu( tr( "MEN_DESK_TOOLS" ), -1, MenuToolsId, 50 ); createMenu( CatalogGenId, toolsMenu, 10, -1 ); createMenu( RegDisplayId, toolsMenu, 10, -1 ); + createMenu( FindActionId, toolsMenu, 10, -1 ); createMenu( separator(), toolsMenu, -1, 15, -1 ); createExtraActions(); @@ -1575,6 +1583,25 @@ void SalomeApp_Application::onRegDisplay() regWnd->activateWindow(); } +/*!Display Action Search dialog */ +void SalomeApp_Application::onFindAction() +{ + const auto pActiveModule = activeModule(); + if (pActiveModule && pActiveModule->name() == "PARAVIS") { + return; + // ParaViS module has its own action search dialog (Quick Launch dialog). + // Keep this conditional block until ParaViS's actions are not added to ShortcutMgr resource and asset files. + } + + SUIT_FindActionDialog aDlg( desktop() ); + if (pActiveModule) + aDlg.setActiveModuleID(pActiveModule->name()); + else + aDlg.setActiveModuleID(); + + aDlg.exec(); +} + /*!find original object by double click on item */ void SalomeApp_Application::onDblClick( SUIT_DataObject* theObj ) { diff --git a/src/SalomeApp/SalomeApp_Application.h b/src/SalomeApp/SalomeApp_Application.h index bd5c92019..5a78e279e 100644 --- a/src/SalomeApp/SalomeApp_Application.h +++ b/src/SalomeApp/SalomeApp_Application.h @@ -71,7 +71,7 @@ class SALOMEAPPIMPL_EXPORT SalomeApp_Application : public LightApp_Application public: enum { MenuToolsId = 5 }; enum { DumpStudyId = LightApp_Application::UserID, LoadScriptId, PropertiesId, - CatalogGenId, RegDisplayId, SaveGUIStateId, ConnectId, DisconnectId, + CatalogGenId, RegDisplayId, FindActionId, SaveGUIStateId, ConnectId, DisconnectId, UserID }; typedef enum { WT_NoteBook = LightApp_Application::WT_User, @@ -192,6 +192,7 @@ private slots: void onCatalogGen(); void onRegDisplay(); + void onFindAction(); void onOpenWith(); void onExtAction(); diff --git a/src/SalomeApp/resources/SalomeApp_msg_en.ts b/src/SalomeApp/resources/SalomeApp_msg_en.ts index d75e202a2..b130ca897 100644 --- a/src/SalomeApp/resources/SalomeApp_msg_en.ts +++ b/src/SalomeApp/resources/SalomeApp_msg_en.ts @@ -164,6 +164,10 @@ Launch a new session or close the study. MEN_DESK_REGISTRY_DISPLAY Registry &Display + + MEN_DESK_FIND_ACTION + Find action + TOT_DESK_FILE_LOAD_SCRIPT Load python script @@ -237,6 +241,10 @@ Do you want to reload it ? PRP_DESK_REGISTRY_DISPLAY Displays content of the Registry CORBA server + + PRP_DESK_FIND_ACTION + Opens action search dialog + APPCLOSE_DESCRIPTION Do you want to save study before closing? @@ -261,6 +269,10 @@ Do you want to reload it ? TOT_DESK_REGISTRY_DISPLAY Registry display + + TOT_DESK_FIND_ACTION + Find action + OBJ_BROWSER_COLUMN_0 Entry diff --git a/src/SalomeApp/resources/SalomeApp_msg_fr.ts b/src/SalomeApp/resources/SalomeApp_msg_fr.ts index ac9e08e95..e73006c23 100644 --- a/src/SalomeApp/resources/SalomeApp_msg_fr.ts +++ b/src/SalomeApp/resources/SalomeApp_msg_fr.ts @@ -164,6 +164,10 @@ Lancez une nouvelle session ou fermez l'étude en cours. MEN_DESK_REGISTRY_DISPLAY Affichage du registre CORBA + + MEN_DESK_FIND_ACTION + Trouver une action + TOT_DESK_FILE_LOAD_SCRIPT Exécuter un script python @@ -237,6 +241,10 @@ Voulez-vous le recharger ? PRP_DESK_REGISTRY_DISPLAY Visualiser le contenu du registre du serveur CORBA + + PRP_DESK_FIND_ACTION + Ouvre la boîte de dialogue de recherche d'action + APPCLOSE_DESCRIPTION Voulez-vous sauvegarder l'étude avant de quitter ? @@ -261,6 +269,10 @@ Voulez-vous le recharger ? TOT_DESK_REGISTRY_DISPLAY Visualiser le registre CORBA + + TOT_DESK_FIND_ACTION + Trouver une action + OBJ_BROWSER_COLUMN_0 Entrée diff --git a/src/SalomeApp/resources/SalomeApp_msg_ja.ts b/src/SalomeApp/resources/SalomeApp_msg_ja.ts index 7dab9b3a4..6bc1b7851 100644 --- a/src/SalomeApp/resources/SalomeApp_msg_ja.ts +++ b/src/SalomeApp/resources/SalomeApp_msg_ja.ts @@ -164,6 +164,10 @@ MEN_DESK_REGISTRY_DISPLAY レジストリの表示(&D) + + MEN_DESK_FIND_ACTION + 検索アクション + TOT_DESK_FILE_LOAD_SCRIPT Python スクリプトを実行 @@ -236,6 +240,10 @@ PRP_DESK_REGISTRY_DISPLAY CORBAサーバーの登録内容を表示 + + PRP_DESK_FIND_ACTION + アクション検索ダイアログを開きます + APPCLOSE_DESCRIPTION 閉じる、または閉じる前にスタディをアンロードしますか? @@ -260,6 +268,10 @@ TOT_DESK_REGISTRY_DISPLAY レジストリの表示 + + TOT_DESK_FIND_ACTION + 検索アクション + OBJ_BROWSER_COLUMN_0 エントリ