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