]> SALOME platform Git repositories - modules/gui.git/blob - src/SUIT/SUIT_ShortcutTree.cxx
Salome HOME
[bos #40644][CEA](2024-T1) Feature search.
[modules/gui.git] / src / SUIT / SUIT_ShortcutTree.cxx
1 // Copyright (C) 2007-2024  CEA, EDF, OPEN CASCADE
2 //
3 // This library is free software; you can redistribute it and/or
4 // modify it under the terms of the GNU Lesser General Public
5 // License as published by the Free Software Foundation; either
6 // version 2.1 of the License, or (at your option) any later version.
7 //
8 // This library is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11 // Lesser General Public License for more details.
12 //
13 // You should have received a copy of the GNU Lesser General Public
14 // License along with this library; if not, write to the Free Software
15 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
16 //
17 // See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
18 //
19
20 #include "SUIT_ShortcutTree.h"
21
22 #include <QWidget>
23 #include <QLayout>
24 #include <QList>
25 #include <QMap>
26
27 #include <QToolButton>
28 #include <QLineEdit>
29 #include <QLabel>
30 #include <QTableWidgetItem>
31 #include <QTextEdit>
32 #include <QMessageBox>
33 #include <QPushButton>
34 #include <QBrush>
35 #include <QColor>
36 #include <QHeaderView>
37
38 #include <QKeyEvent>
39 #include <QKeySequence>
40 #include <QCollator>
41
42 #include <algorithm>
43
44
45 #define COLUMN_SIZE  500
46
47
48 SUIT_KeySequenceEdit::SUIT_KeySequenceEdit(QWidget* parent)
49 : QFrame(parent)
50 {
51   initialize();
52   myKeySequenceLineEdit->installEventFilter(this);
53 }
54
55 /*! \brief Set a key sequence to edit. */
56 void SUIT_KeySequenceEdit::setConfirmedKeySequence(const QKeySequence& theKeySequence)
57 {
58   myConfirmedKeySequenceString = theKeySequence.toString();
59   myKeySequenceLineEdit->setText(myConfirmedKeySequenceString);
60   myPrevKeySequenceString = myConfirmedKeySequenceString;
61 }
62
63 void SUIT_KeySequenceEdit::setEditedKeySequence(const QKeySequence& theKeySequence)
64 {
65   const QString keySequenceString = theKeySequence.toString();
66   myKeySequenceLineEdit->setText(keySequenceString);
67   myPrevKeySequenceString = keySequenceString;
68 }
69
70 QKeySequence SUIT_KeySequenceEdit::editedKeySequence() const
71 {
72   return QKeySequence::fromString(myKeySequenceLineEdit->text());
73 }
74
75 /*! \returns true, if the edited key sequence differs from confirmed one. */
76 bool SUIT_KeySequenceEdit::isKeySequenceModified() const
77 {
78   return QKeySequence(myConfirmedKeySequenceString) != editedKeySequence();
79 }
80
81 /*! \brief Set confirmed key sequence to line editor. */
82 void SUIT_KeySequenceEdit::restoreKeySequence()
83 {
84   myKeySequenceLineEdit->setText(myConfirmedKeySequenceString);
85   myPrevKeySequenceString = myConfirmedKeySequenceString;
86 }
87
88 /*!
89   \brief Gets the key sequence from keys that were pressed
90   \param e a key event
91   \returns a string representation of the key sequence
92 */
93 /*static*/ QString SUIT_KeySequenceEdit::parseEvent(QKeyEvent* e)
94 {
95   bool isShiftPressed = e->modifiers() & Qt::ShiftModifier;
96   bool isControlPressed = e->modifiers() & Qt::ControlModifier;
97   bool isAltPressed = e->modifiers() & Qt::AltModifier;
98   bool isMetaPressed = e->modifiers() & Qt::MetaModifier;
99   bool isModifiersPressed = isControlPressed || isAltPressed || isMetaPressed; // Do not treat Shift alone as a modifier!
100   int result=0;
101   if(isControlPressed)
102     result += Qt::CTRL;
103   if(isAltPressed)
104     result += Qt::ALT;
105   if(isShiftPressed)
106     result += Qt::SHIFT;
107   if(isMetaPressed)
108     result += Qt::META;
109
110   int aKey = e->key();
111   if ((isValidKey(aKey) && isModifiersPressed) || ((aKey >= Qt::Key_F1) && (aKey <= Qt::Key_F12)))
112     result += aKey;
113
114   return QKeySequence(result).toString();
115 }
116
117 /*!
118   \brief Check if the key event contains a 'valid' key
119   \param theKey the code of the key
120   \returns \c true if the key is 'valid'
121 */
122 /*static*/ bool SUIT_KeySequenceEdit::isValidKey(int theKey)
123 {
124   if ( theKey == Qt::Key_Underscore || theKey == Qt::Key_Escape ||
125      ( theKey >= Qt::Key_Backspace && theKey <= Qt::Key_Delete ) ||
126      ( theKey >= Qt::Key_Home && theKey <= Qt::Key_PageDown ) ||
127      ( theKey >= Qt::Key_F1 && theKey <= Qt::Key_F12 )  ||
128      ( theKey >= Qt::Key_Space && theKey <= Qt::Key_Asterisk ) ||
129      ( theKey >= Qt::Key_Comma && theKey <= Qt::Key_AsciiTilde ) )
130     return true;
131   return false;
132 }
133
134 /*! \brief Called when "Clear" button is clicked. */
135 void SUIT_KeySequenceEdit::onClear()
136 {
137   myKeySequenceLineEdit->setText("");
138   myPrevKeySequenceString = "";
139   emit editingFinished();
140 }
141
142 /*! \brief Called when myKeySequenceLineEdit loses focus. */
143 void SUIT_KeySequenceEdit::onEditingFinished()
144 {
145   if (myKeySequenceLineEdit->text().endsWith("+"))
146     myKeySequenceLineEdit->setText(myPrevKeySequenceString);
147   else
148     myPrevKeySequenceString = myKeySequenceLineEdit->text();
149     emit editingFinished();
150 }
151
152 /*!
153   \brief Custom event filter.
154   \param obj event receiver object
155   \param event event
156   \returns \c true if further event processing should be stopped
157 */
158 bool SUIT_KeySequenceEdit::eventFilter(QObject* theObject, QEvent* theEvent)
159 {
160   if (theObject == myKeySequenceLineEdit) {
161     if (theEvent->type() == QEvent::KeyPress) {
162       QKeyEvent* keyEvent = static_cast<QKeyEvent*>(theEvent);
163       QString text = parseEvent(keyEvent);
164       if (keyEvent->key() == Qt::Key_Delete || keyEvent->key() == Qt::Key_Backspace)
165         myKeySequenceLineEdit->setText("");
166       if (!text.isEmpty())
167         myKeySequenceLineEdit->setText(text);
168
169       emit editingStarted();
170       return true;
171     }
172     if (theEvent->type() == QEvent::KeyRelease) {
173       onEditingFinished();
174       return true;
175     }
176   }
177   return false;
178 }
179
180 /*
181   \brief Perform internal intialization.
182 */
183 void SUIT_KeySequenceEdit::initialize()
184 {
185   static const int PIXMAP_SIZE = 30;
186
187   QHBoxLayout* base = new QHBoxLayout( this );
188   base->setMargin(0);
189   base->setSpacing(5);
190
191   base->addWidget(myKeySequenceLineEdit = new QLineEdit(this));
192   setFocusProxy(myKeySequenceLineEdit);
193
194   QToolButton* clearBtn = new QToolButton();
195   auto clearPixmap = QPixmap(":/images/shortcut_disable.svg");
196   clearPixmap.scaled(QSize(PIXMAP_SIZE, PIXMAP_SIZE), Qt::KeepAspectRatio, Qt::SmoothTransformation);
197   clearBtn->setIcon(clearPixmap);
198   clearBtn->setToolTip(tr("Disable shortcut."));
199   base->addWidget(clearBtn);
200
201   QToolButton* restoreBtn = new QToolButton();
202   auto restorePixmap = QPixmap(":/images/shortcut_restore.svg");
203   restorePixmap.scaled(QSize(PIXMAP_SIZE, PIXMAP_SIZE), Qt::KeepAspectRatio, Qt::SmoothTransformation);
204   restoreBtn->setIcon(restorePixmap);
205   restoreBtn->setToolTip(tr("Restore the currently applied key sequence."));
206   base->addWidget(restoreBtn);
207
208   myKeySequenceLineEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
209   clearBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
210   restoreBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
211
212   connect(clearBtn, SIGNAL(clicked()), this, SLOT(onClear()));
213   connect(restoreBtn, SIGNAL(clicked()), this, SIGNAL(restoreFromShortcutMgrClicked()));
214   connect(myKeySequenceLineEdit, SIGNAL(editingFinished()), this, SLOT(onEditingFinished()));
215 }
216
217
218 /*! \param theParent must not be nullptr. */
219 SUIT_EditKeySequenceDialog::SUIT_EditKeySequenceDialog(SUIT_ShortcutTree* theParent)
220 : QDialog(theParent)
221 {
222   setMinimumWidth(500);
223   setWindowTitle(tr("Change key sequence"));
224   QVBoxLayout* layout = new QVBoxLayout(this);
225   myActionName = new QLabel(this);
226   myActionName->setTextFormat(Qt::RichText);
227   myKeySequenceEdit = new SUIT_KeySequenceEdit(this);
228   myTextEdit = new QTextEdit(this);
229   layout->addWidget(myActionName);
230   layout->addWidget(myKeySequenceEdit);
231   layout->addWidget(myTextEdit);
232   myActionName->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
233   myKeySequenceEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
234   myTextEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
235   myTextEdit->setReadOnly(true);
236   myTextEdit->setAcceptRichText(true);
237   myTextEdit->setPlaceholderText(tr("No conflicts."));
238   setFocusProxy(myKeySequenceEdit);
239
240   QHBoxLayout* buttonLayout = new QHBoxLayout(this);
241   layout->addLayout(buttonLayout);
242   QPushButton* confirmButton = new QPushButton(tr("Confirm"), this);
243   confirmButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
244   QPushButton* cancelButton = new QPushButton(tr("Cancel"), this);
245   cancelButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
246   buttonLayout->addStretch();
247   buttonLayout->addWidget(confirmButton);
248   buttonLayout->addWidget(cancelButton);
249
250   connect(myKeySequenceEdit, SIGNAL(editingStarted()), this, SLOT(onEditingStarted()));
251   connect(myKeySequenceEdit, SIGNAL(editingFinished()), this, SLOT(onEditingFinished()));
252   connect(myKeySequenceEdit, SIGNAL(restoreFromShortcutMgrClicked()), this, SLOT(onRestoreFromShortcutMgr()));
253   connect(confirmButton, SIGNAL(clicked()), this, SLOT(onConfirm()));
254   connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject()));
255 }
256
257 void SUIT_EditKeySequenceDialog::setModuleAndActionID(const QString& theModuleID, const QString& theInModuleActionID)
258 {
259   myModuleID = theModuleID;
260   myInModuleActionID = theInModuleActionID;
261 }
262
263 const QString& SUIT_EditKeySequenceDialog::moduleID() const { return myModuleID; }
264 const QString& SUIT_EditKeySequenceDialog::inModuleActionID() const { return myInModuleActionID; }
265
266 void SUIT_EditKeySequenceDialog::setModuleAndActionName(const QString& theModuleName, const QString& theActionName, const QString& theActionToolTip)
267 {
268   myActionName->setText("<b>" + theModuleName + "</b>&nbsp;&nbsp;" + theActionName);
269   myActionName->setToolTip(theActionToolTip);
270 }
271
272 void SUIT_EditKeySequenceDialog::setConfirmedKeySequence(const QKeySequence& theSequence)
273 {
274   myKeySequenceEdit->setConfirmedKeySequence(theSequence);
275 }
276
277 QKeySequence SUIT_EditKeySequenceDialog::editedKeySequence() const
278 {
279   return myKeySequenceEdit->editedKeySequence();
280 }
281
282 int SUIT_EditKeySequenceDialog::exec()
283 {
284   myKeySequenceEdit->setFocus(Qt::ActiveWindowFocusReason);
285   return QDialog::exec();
286 }
287
288 void SUIT_EditKeySequenceDialog::onEditingStarted()
289 {
290   myTextEdit->setEnabled(false);
291 }
292
293 void SUIT_EditKeySequenceDialog::onEditingFinished()
294 {
295   updateConflictsMessage();
296 }
297
298 void SUIT_EditKeySequenceDialog::onRestoreFromShortcutMgr()
299 {
300   const auto shortcutMgr = SUIT_ShortcutMgr::get();
301   myKeySequenceEdit->setEditedKeySequence(shortcutMgr->getKeySequence(myModuleID, myInModuleActionID));
302   updateConflictsMessage();
303 }
304
305 /*! Updates message with list of actions, whose shortcuts will be disabled on Confirm. */
306 void SUIT_EditKeySequenceDialog::updateConflictsMessage()
307 {
308   myTextEdit->setEnabled(true);
309   QTextDocument* doc = myTextEdit->document();
310   if (!doc) {
311     doc = new QTextDocument(myTextEdit);
312     myTextEdit->setDocument(doc);
313   }
314
315   if (!myKeySequenceEdit->isKeySequenceModified()) {
316     doc->clear();
317     return;
318   }
319
320   const QKeySequence newKeySequence = editedKeySequence();
321
322   const auto shortcutTree = static_cast<SUIT_ShortcutTree*>(parentWidget());
323   /** {moduleID, inModuleActionID}[] */
324   std::set<std::pair<QString, QString>> conflicts = shortcutTree->shortcutContainer()->getConflicts(myModuleID, myInModuleActionID, newKeySequence);
325   if (!conflicts.empty()) {
326     const auto shortcutMgr = SUIT_ShortcutMgr::get();
327
328     QString report = "<b>" + tr("These shortcuts will be disabled on confirm:") + "</b>";
329     {
330       report += "<ul>";
331       for (const auto& conflict : conflicts) {
332         const QString conflictingModuleName = shortcutMgr->getModuleName(conflict.first);
333         const QString conflictingActionName = shortcutMgr->getActionName(conflict.first, conflict.second);
334         report += "<li><b>" + conflictingModuleName + "</b>&nbsp;&nbsp;" + conflictingActionName + "</li>";
335       }
336       report += "</ul>";
337     }
338     doc->setHtml(report);
339   }
340   else /* if no conflicts */ {
341     doc->clear();
342   }
343 }
344
345 void SUIT_EditKeySequenceDialog::onConfirm()
346 {
347   if (myKeySequenceEdit->isKeySequenceModified())
348     accept();
349   else
350     reject();
351 }
352
353
354 /*! \brief Compensates lack of std::distance(), which is introduced in C++17.
355 \returns -1, if theIt does not belong to the  */
356 template <class Container>
357 size_t indexOf(
358   const Container& theContainer,
359   const typename Container::iterator& theIt
360 ) {
361   auto it = theContainer.begin();
362   size_t distance = 0;
363   while (it != theContainer.end()) {
364     if (it == theIt)
365       return distance;
366
367     it++;
368     distance++;
369   }
370   return -1;
371 }
372
373
374 /*! \param theContainer Share the same container between several trees,
375 to edit them synchronously even without exchange of changes with SUIT_ShortcutMgr.
376 Pass nullptr to create non-synchronized tree. */
377 SUIT_ShortcutTree::SUIT_ShortcutTree(
378   std::shared_ptr<SUIT_ShortcutContainer> theContainer,
379   QWidget* theParent
380 ) : QTreeWidget(theParent),
381 myShortcutContainer(theContainer ? theContainer : std::shared_ptr<SUIT_ShortcutContainer>(new SUIT_ShortcutContainer())),
382 mySortKey(SUIT_ShortcutTree::SortKey::Name), mySortOrder(SUIT_ShortcutTree::SortOrder::Ascending)
383 {
384   setColumnCount(2);
385   setSelectionMode(QAbstractItemView::SingleSelection);
386   setColumnWidth(0, COLUMN_SIZE);
387   setSortingEnabled(false); // Items are sorted in the same way, as in ShortcutContainer.
388   header()->setSectionResizeMode(QHeaderView::Interactive);
389   {
390     QMap<int, QString> labelMap;
391     labelMap[SUIT_ShortcutTree::ElementIdx::Name]        = tr("Action");
392     labelMap[SUIT_ShortcutTree::ElementIdx::KeySequence] = tr("Key sequence");
393     setHeaderLabels(labelMap.values());
394   }
395   setExpandsOnDoubleClick(false); // Open shortcut editor on double click instead.
396   setSortingEnabled(false);
397   setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
398   setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
399   myEditDialog = new SUIT_EditKeySequenceDialog(this);
400
401   this->installEventFilter(this);
402   connect(this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*, int)), this, SLOT(onItemDoubleClicked(QTreeWidgetItem*, int)));
403
404   SUIT_ShortcutTree::instances[myShortcutContainer.get()].emplace(this);
405 }
406
407 SUIT_ShortcutTree::~SUIT_ShortcutTree()
408 {
409   SUIT_ShortcutTree::instances[myShortcutContainer.get()].erase(this);
410   if (SUIT_ShortcutTree::instances[myShortcutContainer.get()].empty())
411     SUIT_ShortcutTree::instances.erase(myShortcutContainer.get());
412 }
413
414 /*! \brief Copies shortcuts from ShortcutMgr. (Re)displays shortcuts of myModuleIDs. */
415 void SUIT_ShortcutTree::setShortcutsFromManager()
416 {
417   const auto shortcutMgr = SUIT_ShortcutMgr::get();
418   *myShortcutContainer = shortcutMgr->getShortcutContainer();
419   // nb! ShortcutMgr never removes shortcuts from its container, only disables.
420
421   updateItems(false /*theHighlightModified*/, true /*theUpdateSyncTrees*/);
422 }
423
424 /*! \brief Copies shortcuts from resources, user files are not accounted. (Re)displays shortcuts of myModuleIDs. */
425 void SUIT_ShortcutTree::setDefaultShortcuts()
426 {
427   SUIT_ShortcutContainer defaultShortcuts;
428   SUIT_ShortcutMgr::fillContainerFromPreferences(defaultShortcuts, true /*theDefaultOnly*/);
429
430   myShortcutContainer->merge(defaultShortcuts, true /*theOverride*/, true /*theTreatAbsentIncomingAsDisabled*/);
431   // nb! SUIT_ShortcutContainer never erases shortcuts, only disables.
432
433   updateItems(true /*theHighlightModified*/, true /*theUpdateSyncTrees*/);
434 }
435
436 /*! \brief Applies pending changes to ShortcutMgr. Updates other instances of SUIT_ShortcutTree. */
437 void SUIT_ShortcutTree::applyChangesToShortcutMgr()
438 {
439   const auto mgr = SUIT_ShortcutMgr::get();
440   mgr->mergeShortcutContainer(*myShortcutContainer);
441
442   // Update non-synchronized with this instances.
443   for (const auto& containerAndSyncTrees : SUIT_ShortcutTree::instances) {
444     if (containerAndSyncTrees.first == myShortcutContainer.get())
445       continue;
446
447     const std::set<SUIT_ShortcutTree*>& syncTrees = containerAndSyncTrees.second;
448     const auto itFirstSyncTree = syncTrees.begin();
449     if (itFirstSyncTree == syncTrees.end())
450       continue;
451
452     (*itFirstSyncTree)->setShortcutsFromManager();
453     const auto editDialog = (*itFirstSyncTree)->myEditDialog;
454     editDialog->setConfirmedKeySequence(mgr->getShortcutContainer().getKeySequence(editDialog->moduleID(), editDialog->inModuleActionID()));
455     editDialog->updateConflictsMessage();
456   }
457 }
458
459 std::shared_ptr<const SUIT_ShortcutContainer> SUIT_ShortcutTree::shortcutContainer() const
460 {
461   return myShortcutContainer;
462 }
463
464 /*! \brief Does not sort modules. */
465 void SUIT_ShortcutTree::sort(SUIT_ShortcutTree::SortKey theKey, SUIT_ShortcutTree::SortOrder theOrder)
466 {
467   if (theKey == mySortKey && theOrder == mySortOrder)
468     return;
469
470   mySortKey == theKey;
471   mySortOrder = theOrder;
472
473   for (int moduleIdx = 0; moduleIdx < topLevelItemCount(); moduleIdx++) {
474     const auto moduleItem = static_cast<SUIT_ShortcutTreeFolder*>(topLevelItem(moduleIdx));
475     const auto sortedChildren = getSortedChildren(moduleItem);
476     moduleItem->takeChildren();
477
478     for (const auto childItem : sortedChildren) {
479       moduleItem->addChild(childItem);
480     }
481   }
482 }
483
484 /*! \param If theUpdateSyncTrees, trees sharing the same shortcut container are updated. */
485 void SUIT_ShortcutTree::updateItems(bool theHighlightModified, bool theUpdateSyncTrees)
486 {
487   const auto shortcutMgr = SUIT_ShortcutMgr::get();
488   const QString lang = SUIT_ShortcutMgr::getLang();
489
490   for (const QString& moduleID : myModuleIDs) {
491     const auto& moduleShortcuts = myShortcutContainer->getModuleShortcutsInversed(moduleID);
492     if (moduleShortcuts.empty()) {
493       // Do not display empty module.
494       const auto moduleItemAndIdx = findModuleFolderItem(moduleID);
495       if (moduleItemAndIdx.second >= 0)
496         delete takeTopLevelItem(moduleItemAndIdx.second);
497
498       continue;
499     }
500
501     const auto moduleItemAndIdx = findModuleFolderItem(moduleID);
502     SUIT_ShortcutTreeFolder* moduleItem = moduleItemAndIdx.first;
503     if (!moduleItem) {
504       moduleItem = new SUIT_ShortcutTreeFolder(moduleID);
505       moduleItem->setAssets(shortcutMgr->getModuleAssets(moduleID), lang);
506       addTopLevelItem(moduleItem);
507       moduleItem->setFlags(Qt::ItemIsEnabled);
508
509       auto sortedChildren = getSortedChildren(moduleItem);
510       for (const auto& shortcut : moduleShortcuts) {
511         const QString& inModuleActionID = shortcut.first;
512         const QKeySequence& keySequence = shortcut.second;
513         const QString keySequenceString = keySequence.toString();
514
515         auto actionItem = SUIT_ShortcutTreeAction::create(moduleID, inModuleActionID);
516         if (!actionItem) {
517           ShCutDbg("SUIT_ShortcutTree can't create child item for action ID = \"" + SUIT_ShortcutMgr::makeActionID(moduleID, inModuleActionID) + "\".");
518           continue;
519         }
520
521         actionItem->setAssets(shortcutMgr->getActionAssets(moduleID, inModuleActionID), lang);
522         actionItem->setKeySequence(keySequenceString);
523
524         if (theHighlightModified) {
525           const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, inModuleActionID);
526           actionItem->highlightKeySequenceAsModified(keySequence != appliedKeySequence);
527         }
528
529         insertChild(moduleItem, sortedChildren, actionItem);
530       }
531
532       moduleItem->setExpanded(true); // Make tree expanded on first show.
533     }
534     else /* if the tree has the module-item */ {
535       for (int childIdx = 0; childIdx < moduleItem->childCount(); childIdx++) {
536         // Update exisiting items of a module.
537         SUIT_ShortcutTreeAction* const childItem = static_cast<SUIT_ShortcutTreeAction*>(moduleItem->child(childIdx));
538         const auto itShortcut = moduleShortcuts.find(childItem->myInModuleActionID);
539         if (itShortcut == moduleShortcuts.end()) {
540           // Shortcut of the item has been removed from myShortcutContainer - impossible.
541           continue;
542         }
543         const QKeySequence& newKeySequence = itShortcut->second;
544         const QString newKeySequenceString = newKeySequence.toString();
545         if (childItem->keySequence() != newKeySequenceString)
546           childItem->setKeySequence(newKeySequenceString);
547
548         if (theHighlightModified) {
549           const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, childItem->myInModuleActionID);
550           childItem->highlightKeySequenceAsModified(newKeySequence != appliedKeySequence);
551         }
552         else
553           childItem->highlightKeySequenceAsModified(false);
554       }
555
556       // Add new items if myShortcutContainer acquired new shortcuts, which may happen if a developer forgot
557       // to add shortcuts for registered actions to resource files.
558       if (moduleItem->childCount() < moduleShortcuts.size()) {
559         auto sortedChildren = getSortedChildren(moduleItem);
560         for (const auto& shortcut : moduleShortcuts) {
561           const QString& inModuleActionID = shortcut.first;
562           const auto predicate = [&inModuleActionID](const SUIT_ShortcutTreeItem* const theItem) -> bool {
563             return static_cast<const SUIT_ShortcutTreeAction* const>(theItem)->myInModuleActionID == inModuleActionID;
564           };
565
566           if (std::find_if(sortedChildren.begin(), sortedChildren.end(), predicate) == sortedChildren.end()) {
567             const auto actionItem = SUIT_ShortcutTreeAction::create(moduleID, inModuleActionID);
568             if (!actionItem) {
569               ShCutDbg("SUIT_ShortcutTree can't create child item for action ID = \"" + SUIT_ShortcutMgr::makeActionID(moduleID, inModuleActionID) + "\".");
570               continue;
571             }
572
573             const QKeySequence& keySequence = shortcut.second;
574             actionItem->setAssets(shortcutMgr->getActionAssets(moduleID, inModuleActionID), lang);
575             actionItem->setKeySequence(keySequence.toString());
576
577             if (theHighlightModified) {
578               const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, inModuleActionID);
579               actionItem->highlightKeySequenceAsModified(keySequence != appliedKeySequence);
580             }
581
582             insertChild(moduleItem, sortedChildren, actionItem);
583           }
584         }
585       }
586     }
587   }
588
589   if (theUpdateSyncTrees) {
590     const std::set<SUIT_ShortcutTree*>& syncTrees = SUIT_ShortcutTree::instances[myShortcutContainer.get()];
591     for (const auto syncTree: syncTrees) {
592       if (syncTree == this)
593         continue;
594
595       syncTree->updateItems(theHighlightModified, false /*theUpdateSyncTrees*/);
596       const auto editDialog = syncTree->myEditDialog;
597       editDialog->setConfirmedKeySequence(myShortcutContainer->getKeySequence(editDialog->moduleID(), editDialog->inModuleActionID()));
598       editDialog->updateConflictsMessage();
599     }
600   }
601 }
602
603 /*! \returns Pointer and index of top-level item.
604 If the tree does not contain an item with theModuleID, returns {nullptr, -1}. */
605 std::pair<SUIT_ShortcutTreeFolder*, int> SUIT_ShortcutTree::findModuleFolderItem(const QString& theModuleID) const
606 {
607   for (int moduleIdx = 0; moduleIdx < topLevelItemCount(); moduleIdx++) {
608     SUIT_ShortcutTreeFolder* moduleItem = static_cast<SUIT_ShortcutTreeFolder*>(topLevelItem(moduleIdx));
609     if (moduleItem->myModuleID == theModuleID)
610       return std::pair<SUIT_ShortcutTreeFolder*, int>(moduleItem, moduleIdx);
611   }
612   return std::pair<SUIT_ShortcutTreeFolder*, int>(nullptr, -1);
613 }
614
615 /*! \returns Children of theParentItem being sorted according to current sort mode and order. */
616 std::set<SUIT_ShortcutTreeItem*, std::function<bool(SUIT_ShortcutTreeItem*, SUIT_ShortcutTreeItem*)>> SUIT_ShortcutTree::getSortedChildren(SUIT_ShortcutTreeFolder* theParentItem)
617 {
618   QList<std::pair<SUIT_ShortcutTree::SortKey, SUIT_ShortcutTree::SortOrder>> sortSchema = SUIT_ShortcutTree::DEFAULT_SORT_SCHEMA;
619   {
620     for (auto itSameKey = sortSchema.begin(); itSameKey != sortSchema.end(); itSameKey++) {
621       if (itSameKey->first == mySortKey) {
622         sortSchema.erase(itSameKey);
623         break;
624       }
625     }
626     sortSchema.push_front(std::pair<SUIT_ShortcutTree::SortKey, SUIT_ShortcutTree::SortOrder>(mySortKey, mySortOrder));
627   }
628
629   static const QCollator collator;
630   const std::function<bool(SUIT_ShortcutTreeItem*, SUIT_ShortcutTreeItem*)> comparator =
631   [this, sortSchema, &collator](const SUIT_ShortcutTreeItem* theItemA, const SUIT_ShortcutTreeItem* theItemB) {
632     for (const auto& keyAndOrder : sortSchema) {
633       const int res = collator.compare(theItemA->getValue(keyAndOrder.first), theItemB->getValue(keyAndOrder.first));
634       if (res != 0)
635         return keyAndOrder.second == SUIT_ShortcutTree::SortOrder::Ascending ? res < 0 : res > 0;
636     }
637     return false;
638   };
639
640   std::set<SUIT_ShortcutTreeItem*, std::function<bool(SUIT_ShortcutTreeItem*, SUIT_ShortcutTreeItem*)>> sortedChildren(comparator);
641   for (int childIdx = 0; childIdx < theParentItem->childCount(); childIdx++) {
642     SUIT_ShortcutTreeAction* const childItem = static_cast<SUIT_ShortcutTreeAction*>(theParentItem->child(childIdx));
643     sortedChildren.emplace(childItem);
644   }
645   return sortedChildren;
646 }
647
648 /*! \brief Inserts theChildItem to theParentItem and theSortedChildren.
649 Does not check whether theSortedChildren are actually child items of theParentItem.
650 Does not check whether current item sort schema is same as one of theSortedChildren. */
651 void SUIT_ShortcutTree::insertChild(
652   SUIT_ShortcutTreeFolder* theParentItem,
653   std::set<SUIT_ShortcutTreeItem*, std::function<bool(SUIT_ShortcutTreeItem*, SUIT_ShortcutTreeItem*)>>& theSortedChildren,
654   SUIT_ShortcutTreeItem* theChildItem
655 ) {
656   auto emplaceRes = theSortedChildren.emplace(theChildItem);
657   theParentItem->insertChild(indexOf(theSortedChildren, emplaceRes.first), theChildItem);
658 }
659
660 void SUIT_ShortcutTree::onItemDoubleClicked(QTreeWidgetItem* theItem, int theColIdx)
661 {
662   {
663     SUIT_ShortcutTreeItem* const item = static_cast<SUIT_ShortcutTreeItem*>(theItem);
664     // Do not react if folder-item is clicked.
665     if (item->type() != SUIT_ShortcutTreeItem::Type::Action)
666       return;
667   }
668
669   SUIT_ShortcutTreeAction* const actionItem = static_cast<SUIT_ShortcutTreeAction*>(theItem);
670
671   myEditDialog->setModuleAndActionID(actionItem->myModuleID, actionItem->myInModuleActionID);
672   QString actionToolTip = actionItem->toolTip(SUIT_ShortcutTree::ElementIdx::Name);
673   actionToolTip.truncate(actionToolTip.lastIndexOf('\n') + 1);
674   myEditDialog->setModuleAndActionName(
675     static_cast<SUIT_ShortcutTreeItem*>(actionItem->parent())->name(),
676     actionItem->name(),
677     actionToolTip
678   );
679   myEditDialog->setConfirmedKeySequence(QKeySequence::fromString(actionItem->keySequence()));
680   myEditDialog->updateConflictsMessage();
681   const bool somethingChanged = myEditDialog->exec() == QDialog::Accepted;
682
683   if (!somethingChanged)
684     return;
685
686   const QKeySequence newKeySequence = myEditDialog->editedKeySequence();
687
688   /** { moduleID, inModuleActionID }[] */
689   std::set<std::pair<QString, QString>> disabledActionIDs = myShortcutContainer->setShortcut(actionItem->myModuleID, actionItem->myInModuleActionID, newKeySequence, true /*override*/);
690
691   /** { moduleID, {inModuleActionID, keySequence}[] }[] */
692   std::map<QString, std::map<QString, QString>> changes;
693   changes[actionItem->myModuleID][actionItem->myInModuleActionID] = newKeySequence.toString();
694   for (const auto moduleAndActionID : disabledActionIDs) {
695     changes[moduleAndActionID.first][moduleAndActionID.second] = QString();
696   }
697
698   // Set new key sequences to shortcut items.
699   for (const auto& moduleIDAndChanges : changes) {
700     const QString& moduleID = moduleIDAndChanges.first;
701
702     const auto moduleItemAndIdx = findModuleFolderItem(moduleID);
703     const auto moduleItem = moduleItemAndIdx.first;
704     if (!moduleItem)
705       continue;
706
707     /** {inModuleActionID, newKeySequence}[] */
708     const std::map<QString, QString>& moduleChanges = moduleIDAndChanges.second;
709
710     // Go through module' shortcut items, and highlight those, whose key sequences differ from applied key sequences.
711     for (int childIdx = 0; childIdx < moduleItem->childCount(); childIdx++) {
712       SUIT_ShortcutTreeAction* const childItem = static_cast<SUIT_ShortcutTreeAction*>(moduleItem->child(childIdx));
713       const auto itChange = moduleChanges.find(childItem->myInModuleActionID);
714       if (itChange == moduleChanges.end()) {
715         // The shortcut has not been changed.
716         continue;
717       }
718
719       childItem->setKeySequence(itChange->second);
720
721       const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, childItem->myInModuleActionID);
722       childItem->highlightKeySequenceAsModified(QKeySequence::fromString(itChange->second) != appliedKeySequence);
723     }
724   }
725 }
726
727 /*static*/ const QList<std::pair<SUIT_ShortcutTree::SortKey, SUIT_ShortcutTree::SortOrder>> SUIT_ShortcutTree::DEFAULT_SORT_SCHEMA =
728 {
729   {SUIT_ShortcutTree::SortKey::Name, SUIT_ShortcutTree::SortOrder::Ascending},
730   {SUIT_ShortcutTree::SortKey::ToolTip, SUIT_ShortcutTree::SortOrder::Ascending},
731   {SUIT_ShortcutTree::SortKey::KeySequence, SUIT_ShortcutTree::SortOrder::Ascending},
732   {SUIT_ShortcutTree::SortKey::ID, SUIT_ShortcutTree::SortOrder::Ascending}
733 };
734
735 /*static*/ std::map<SUIT_ShortcutContainer*, std::set<SUIT_ShortcutTree*>> SUIT_ShortcutTree::instances =
736 std::map<SUIT_ShortcutContainer*, std::set<SUIT_ShortcutTree*>>();
737
738
739
740 SUIT_ShortcutTreeItem::SUIT_ShortcutTreeItem(const QString& theModuleID)
741 : QTreeWidgetItem(), myModuleID(theModuleID)
742 { }
743
744 QString SUIT_ShortcutTreeItem::name() const
745 {
746   return text(SUIT_ShortcutTree::ElementIdx::Name);
747 }
748
749
750 SUIT_ShortcutTreeFolder::SUIT_ShortcutTreeFolder(const QString& theModuleID)
751 : SUIT_ShortcutTreeItem(theModuleID)
752 {
753   QFont f = font(SUIT_ShortcutTree::ElementIdx::Name);
754   f.setBold(true);
755   setFont(SUIT_ShortcutTree::ElementIdx::Name, f);
756   setText(SUIT_ShortcutTree::ElementIdx::Name, theModuleID);
757 }
758
759 void SUIT_ShortcutTreeFolder::setAssets(std::shared_ptr<const SUIT_ActionAssets> theAssets, const QString& theLang)
760 {
761   if (!theAssets)
762     return;
763
764   setIcon(SUIT_ShortcutTree::ElementIdx::Name, theAssets->myIcon);
765
766   const auto& ldaMap = theAssets->myLangDependentAssets;
767   if (ldaMap.empty()) {
768     setText(SUIT_ShortcutTree::ElementIdx::Name, myModuleID);
769     return;
770   }
771
772   auto itLDA = ldaMap.find(theLang);
773   if (itLDA == ldaMap.end())
774     itLDA = ldaMap.begin();
775
776   const SUIT_ActionAssets::LangDependentAssets& lda = itLDA->second;
777   const QString& name = lda.myName.isEmpty() ? myModuleID : lda.myName;
778   setText(SUIT_ShortcutTree::ElementIdx::Name, name);
779 }
780
781 QString SUIT_ShortcutTreeFolder::getValue(SUIT_ShortcutTree::SortKey theKey) const
782 {
783   switch (theKey) {
784     case SUIT_ShortcutTree::SortKey::ID:
785       return myModuleID;
786     case SUIT_ShortcutTree::SortKey::Name:
787       return name();
788     case SUIT_ShortcutTree::SortKey::ToolTip:
789       return name();
790     default:
791       return QString();
792   }
793 }
794
795
796 SUIT_ShortcutTreeAction::SUIT_ShortcutTreeAction(const QString& theModuleID, const QString& theInModuleActionID)
797 : SUIT_ShortcutTreeItem(theModuleID), myInModuleActionID(theInModuleActionID)
798 {
799   setText(SUIT_ShortcutTree::ElementIdx::Name, theInModuleActionID);
800   setToolTip(
801     SUIT_ShortcutTree::ElementIdx::Name,
802     theInModuleActionID + (theInModuleActionID.at(theInModuleActionID.length()-1) == "." ? "\n" : ".\n") + SUIT_ShortcutTree::tr("Double click to edit key sequence.")
803   );
804   setToolTip(SUIT_ShortcutTree::ElementIdx::KeySequence, SUIT_ShortcutTree::tr("Double click to edit key sequence."));
805 }
806
807 /*static*/ SUIT_ShortcutTreeAction* SUIT_ShortcutTreeAction::create(const QString& theModuleID, const QString& theInModuleActionID)
808 {
809   if (theInModuleActionID.isEmpty()) {
810     ShCutDbg("SUIT_ShortcutTreeItem: attempt to create item with empty action ID.");
811     return nullptr;
812   }
813
814   return new SUIT_ShortcutTreeAction(theModuleID, theInModuleActionID);
815 }
816
817 void SUIT_ShortcutTreeAction::setAssets(std::shared_ptr<const SUIT_ActionAssets> theAssets, const QString& theLang)
818 {
819   if (!theAssets)
820     return;
821
822   setIcon(SUIT_ShortcutTree::ElementIdx::Name, theAssets->myIcon);
823
824   const auto& ldaMap = theAssets->myLangDependentAssets;
825   if (ldaMap.empty()) {
826     setText(SUIT_ShortcutTree::ElementIdx::Name, myInModuleActionID);
827     return;
828   }
829
830   auto itLDA = ldaMap.find(theLang);
831   if (itLDA == ldaMap.end())
832     itLDA = ldaMap.begin();
833
834   const SUIT_ActionAssets::LangDependentAssets& lda = itLDA->second;
835   const QString& name = lda.myName.isEmpty() ? myInModuleActionID : lda.myName;
836   setText(SUIT_ShortcutTree::ElementIdx::Name, name);
837
838   const QString& actionToolTip = lda.myToolTip.isEmpty() ? name : lda.myToolTip;
839   setToolTip(
840     SUIT_ShortcutTree::ElementIdx::Name,
841     actionToolTip + (actionToolTip.at(actionToolTip.length()-1) == "." ? "\n" : ".\n") + SUIT_ShortcutTree::tr("Double click to edit key sequence.")
842   );
843 }
844
845 QString SUIT_ShortcutTreeAction::getValue(SUIT_ShortcutTree::SortKey theKey) const
846 {
847   switch (theKey) {
848     case SUIT_ShortcutTree::SortKey::ID:
849       return myInModuleActionID;
850     case SUIT_ShortcutTree::SortKey::Name:
851       return name();
852     case SUIT_ShortcutTree::SortKey::ToolTip:
853       return toolTip(SUIT_ShortcutTree::ElementIdx::Name);
854     case SUIT_ShortcutTree::SortKey::KeySequence:
855       return keySequence();
856     default:
857       return QString();
858   }
859 }
860
861 void SUIT_ShortcutTreeAction::setKeySequence(const QString& theKeySequence)
862 {
863   setText(SUIT_ShortcutTree::ElementIdx::KeySequence, theKeySequence);
864 }
865
866 QString SUIT_ShortcutTreeAction::keySequence() const
867 {
868   return text(SUIT_ShortcutTree::ElementIdx::KeySequence);
869 }
870
871 /*! \brief Highlights text at ElementIdx::KeySequence. */
872 void SUIT_ShortcutTreeAction::highlightKeySequenceAsModified(bool theHighlight)
873 {
874   static const QBrush bgHighlitingBrush = QBrush(Qt::darkGreen);
875   static const QBrush fgHighlitingBrush = QBrush(Qt::white);
876   static const QBrush noBrush = QBrush();
877
878   setBackground(SUIT_ShortcutTree::ElementIdx::KeySequence, theHighlight ? bgHighlitingBrush : noBrush);
879   setForeground(SUIT_ShortcutTree::ElementIdx::KeySequence, theHighlight ? fgHighlitingBrush : noBrush);
880 }