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
26 #include <QToolButton>
27 #include <QLineEdit>
28 #include <QLabel>
29 #include <QTableWidgetItem>
30 #include <QTextEdit>
31 #include <QMessageBox>
32 #include <QPushButton>
33 #include <QBrush>
34 #include <QColor>
35
36 #include <QKeyEvent>
37 #include <QKeySequence>
38
39
40 #define COLUMN_SIZE  500
41
42
43 QtxKeySequenceEdit::QtxKeySequenceEdit(QWidget* parent)
44 : QFrame(parent)
45 {
46   initialize();
47   myKeySequenceLineEdit->installEventFilter(this);
48 }
49
50 /*! \brief Set a key sequence to edit. */
51 void QtxKeySequenceEdit::setConfirmedKeySequence(const QKeySequence& theKeySequence)
52 {
53   myConfirmedKeySequenceString = theKeySequence.toString();
54   myKeySequenceLineEdit->setText(myConfirmedKeySequenceString);
55   myPrevKeySequenceString = myConfirmedKeySequenceString;
56 }
57
58 void QtxKeySequenceEdit::setEditedKeySequence(const QKeySequence& theKeySequence)
59 {
60   const QString keySequenceString = theKeySequence.toString();
61   myKeySequenceLineEdit->setText(keySequenceString);
62   myPrevKeySequenceString = keySequenceString;
63 }
64
65 QKeySequence QtxKeySequenceEdit::editedKeySequence() const
66 {
67   return QKeySequence::fromString(myKeySequenceLineEdit->text());
68 }
69
70 /*! \returns true, if the edited key sequence differs from confirmed one. */
71 bool QtxKeySequenceEdit::isKeySequenceModified() const
72 {
73   return QKeySequence(myConfirmedKeySequenceString) != editedKeySequence();
74 }
75
76 /*! \brief Set confirmed key sequence to line editor. */
77 void QtxKeySequenceEdit::restoreKeySequence()
78 {
79   myKeySequenceLineEdit->setText(myConfirmedKeySequenceString);
80   myPrevKeySequenceString = myConfirmedKeySequenceString;
81 }
82
83 /*!
84   \brief Gets the key sequence from keys that were pressed
85   \param e a key event
86   \returns a string representation of the key sequence
87 */
88 /*static*/ QString QtxKeySequenceEdit::parseEvent(QKeyEvent* e)
89 {
90   bool isShiftPressed = e->modifiers() & Qt::ShiftModifier;
91   bool isControlPressed = e->modifiers() & Qt::ControlModifier;
92   bool isAltPressed = e->modifiers() & Qt::AltModifier;
93   bool isMetaPressed = e->modifiers() & Qt::MetaModifier;
94   bool isModifiersPressed = isControlPressed || isAltPressed || isMetaPressed; // Do not treat Shift alone as a modifier!
95   int result=0;
96   if(isControlPressed)
97     result += Qt::CTRL;
98   if(isAltPressed)
99     result += Qt::ALT;
100   if(isShiftPressed)
101     result += Qt::SHIFT;
102   if(isMetaPressed)
103     result += Qt::META;
104
105   int aKey = e->key();
106   if ((isValidKey(aKey) && isModifiersPressed) || ((aKey >= Qt::Key_F1) && (aKey <= Qt::Key_F12)))
107     result += aKey;
108
109   return QKeySequence(result).toString();
110 }
111
112 /*!
113   \brief Check if the key event contains a 'valid' key
114   \param theKey the code of the key
115   \returns \c true if the key is 'valid'
116 */
117 /*static*/ bool QtxKeySequenceEdit::isValidKey(int theKey)
118 {
119   if ( theKey == Qt::Key_Underscore || theKey == Qt::Key_Escape ||
120      ( theKey >= Qt::Key_Backspace && theKey <= Qt::Key_Delete ) ||
121      ( theKey >= Qt::Key_Home && theKey <= Qt::Key_PageDown ) ||
122      ( theKey >= Qt::Key_F1 && theKey <= Qt::Key_F12 )  ||
123      ( theKey >= Qt::Key_Space && theKey <= Qt::Key_Asterisk ) ||
124      ( theKey >= Qt::Key_Comma && theKey <= Qt::Key_Question ) ||
125      ( theKey >= Qt::Key_A && theKey <= Qt::Key_AsciiTilde ) )
126     return true;
127   return false;
128 }
129
130 /*! \brief Called when "Clear" button is clicked. */
131 void QtxKeySequenceEdit::onClear()
132 {
133   myKeySequenceLineEdit->setText("");
134   myPrevKeySequenceString = "";
135   emit editingFinished();
136 }
137
138 /*! \brief Called when myKeySequenceLineEdit loses focus. */
139 void QtxKeySequenceEdit::onEditingFinished()
140 {
141   if (myKeySequenceLineEdit->text().endsWith("+"))
142     myKeySequenceLineEdit->setText(myPrevKeySequenceString);
143   else
144     myPrevKeySequenceString = myKeySequenceLineEdit->text();
145     emit editingFinished();
146 }
147
148 /*!
149   \brief Custom event filter.
150   \param obj event receiver object
151   \param event event
152   \returns \c true if further event processing should be stopped
153 */
154 bool QtxKeySequenceEdit::eventFilter(QObject* theObject, QEvent* theEvent)
155 {
156   if (theObject == myKeySequenceLineEdit) {
157     if (theEvent->type() == QEvent::KeyPress) {
158       QKeyEvent* keyEvent = static_cast<QKeyEvent*>(theEvent);
159       QString text = parseEvent(keyEvent);
160       if (keyEvent->key() == Qt::Key_Delete || keyEvent->key() == Qt::Key_Backspace)
161         myKeySequenceLineEdit->setText("");
162       if (!text.isEmpty())
163         myKeySequenceLineEdit->setText(text);
164
165       emit editingStarted();
166       return true;
167     }
168     if (theEvent->type() == QEvent::KeyRelease) {
169       onEditingFinished();
170       return true;
171     }
172   }
173   return false;
174 }
175
176 /*
177   \brief Perform internal intialization.
178 */
179 void QtxKeySequenceEdit::initialize()
180 {
181   static const int PIXMAP_SIZE = 30;
182
183   QHBoxLayout* base = new QHBoxLayout( this );
184   base->setMargin(0);
185   base->setSpacing(5);
186
187   base->addWidget(myKeySequenceLineEdit = new QLineEdit(this));
188   setFocusProxy(myKeySequenceLineEdit);
189
190   QToolButton* clearBtn = new QToolButton();
191   auto clearPixmap = QPixmap(":/images/shortcut_disable.svg");
192   clearPixmap.scaled(QSize(PIXMAP_SIZE, PIXMAP_SIZE), Qt::KeepAspectRatio, Qt::SmoothTransformation);
193   clearBtn->setIcon(clearPixmap);
194   clearBtn->setToolTip(tr("Disable shortcut."));
195   base->addWidget(clearBtn);
196
197   QToolButton* restoreBtn = new QToolButton();
198   auto restorePixmap = QPixmap(":/images/shortcut_restore.svg");
199   restorePixmap.scaled(QSize(PIXMAP_SIZE, PIXMAP_SIZE), Qt::KeepAspectRatio, Qt::SmoothTransformation);
200   restoreBtn->setIcon(restorePixmap);
201   restoreBtn->setToolTip(tr("Restore the currently applied key sequence."));
202   base->addWidget(restoreBtn);
203
204   myKeySequenceLineEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
205   clearBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
206   restoreBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
207
208   connect(clearBtn, SIGNAL(clicked()), this, SLOT(onClear()));
209   connect(restoreBtn, SIGNAL(clicked()), this, SIGNAL(restoreFromShortcutMgrClicked()));
210   connect(myKeySequenceLineEdit, SIGNAL(editingFinished()), this, SLOT(onEditingFinished()));
211 }
212
213
214 /*! \param theParent must not be nullptr. */
215 QtxEditKeySequenceDialog::QtxEditKeySequenceDialog(QtxShortcutTree* theParent)
216 : QDialog(theParent)
217 {
218   setMinimumWidth(500);
219   setWindowTitle(tr("Change key sequence"));
220   QVBoxLayout* layout = new QVBoxLayout(this);
221   myActionName = new QLabel(this);
222   myActionName->setTextFormat(Qt::RichText);
223   myKeySequenceEdit = new QtxKeySequenceEdit(this);
224   myTextEdit = new QTextEdit(this);
225   layout->addWidget(myActionName);
226   layout->addWidget(myKeySequenceEdit);
227   layout->addWidget(myTextEdit);
228   myActionName->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
229   myKeySequenceEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
230   myTextEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
231   myTextEdit->setReadOnly(true);
232   myTextEdit->setAcceptRichText(true);
233   myTextEdit->setPlaceholderText(tr("No conflicts."));
234   setFocusProxy(myKeySequenceEdit);
235
236   QHBoxLayout* buttonLayout = new QHBoxLayout(this);
237   layout->addLayout(buttonLayout);
238   QPushButton* confirmButton = new QPushButton(tr("Confirm"), this);
239   confirmButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
240   QPushButton* cancelButton = new QPushButton(tr("Cancel"), this);
241   cancelButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
242   buttonLayout->addStretch();
243   buttonLayout->addWidget(confirmButton);
244   buttonLayout->addWidget(cancelButton);
245
246   connect(myKeySequenceEdit, SIGNAL(editingStarted()), this, SLOT(onEditingStarted()));
247   connect(myKeySequenceEdit, SIGNAL(editingFinished()), this, SLOT(onEditingFinished()));
248   connect(myKeySequenceEdit, SIGNAL(restoreFromShortcutMgrClicked()), this, SLOT(onRestoreFromShortcutMgr()));
249   connect(confirmButton, SIGNAL(clicked()), this, SLOT(onConfirm()));
250   connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject()));
251 }
252
253 void QtxEditKeySequenceDialog::setModuleAndActionID(const QString& theModuleID, const QString& theInModuleActionID)
254 {
255   myModuleID = theModuleID;
256   myInModuleActionID = theInModuleActionID;
257 }
258
259 const QString& QtxEditKeySequenceDialog::moduleID() const { return myModuleID; }
260 const QString& QtxEditKeySequenceDialog::inModuleActionID() const { return myInModuleActionID; }
261
262 void QtxEditKeySequenceDialog::setModuleAndActionName(const QString& theModuleName, const QString& theActionName)
263 {
264   myActionName->setText("<b>" + theModuleName + "</b>&nbsp;&nbsp;" + theActionName);
265 }
266
267 void QtxEditKeySequenceDialog::setConfirmedKeySequence(const QKeySequence& theSequence)
268 {
269   myKeySequenceEdit->setConfirmedKeySequence(theSequence);
270 }
271
272 QKeySequence QtxEditKeySequenceDialog::editedKeySequence() const
273 {
274   return myKeySequenceEdit->editedKeySequence();
275 }
276
277 int QtxEditKeySequenceDialog::exec()
278 {
279   myKeySequenceEdit->setFocus(Qt::ActiveWindowFocusReason);
280   return QDialog::exec();
281 }
282
283 void QtxEditKeySequenceDialog::onEditingStarted()
284 {
285   myTextEdit->setEnabled(false);
286 }
287
288 void QtxEditKeySequenceDialog::onEditingFinished()
289 {
290   updateConflictsMessage();
291 }
292
293 void QtxEditKeySequenceDialog::onRestoreFromShortcutMgr()
294 {
295   const auto shortcutMgr = SUIT_ShortcutMgr::get();
296   myKeySequenceEdit->setEditedKeySequence(shortcutMgr->getKeySequence(myModuleID, myInModuleActionID));
297   updateConflictsMessage();
298 }
299
300 /*! Updates message with list of actions, whose shortcuts will be disabled on Confirm. */
301 void QtxEditKeySequenceDialog::updateConflictsMessage()
302 {
303   myTextEdit->setEnabled(true);
304   QTextDocument* doc = myTextEdit->document();
305   if (!doc) {
306     doc = new QTextDocument(myTextEdit);
307     myTextEdit->setDocument(doc);
308   }
309
310   if (!myKeySequenceEdit->isKeySequenceModified()) {
311     doc->clear();
312     return;
313   }
314
315   const QKeySequence newKeySequence = editedKeySequence();
316
317   const auto shortcutTree = static_cast<QtxShortcutTree*>(parentWidget());
318   /** {moduleID, inModuleActionID}[] */
319   std::set<std::pair<QString, QString>> conflicts = shortcutTree->shortcutContainer()->getConflicts(myModuleID, myInModuleActionID, newKeySequence);
320   if (!conflicts.empty()) {
321     const auto shortcutMgr = SUIT_ShortcutMgr::get();
322
323     QString report = "<b>" + tr("These shortcuts will be disabled on confirm:") + "</b>";
324     {
325       report += "<ul>";
326       for (const auto& conflict : conflicts) {
327         const QString conflictingModuleName = shortcutMgr->getModuleName(conflict.first);
328         const QString conflictingActionName = shortcutMgr->getActionName(conflict.first, conflict.second);
329         report += "<li><b>" + conflictingModuleName + "</b>&nbsp;&nbsp;" + conflictingActionName + "</li>";
330       }
331       report += "</ul>";
332     }
333     doc->setHtml(report);
334   }
335   else /* if no conflicts */ {
336     doc->clear();
337   }
338 }
339
340 void QtxEditKeySequenceDialog::onConfirm()
341 {
342   if (myKeySequenceEdit->isKeySequenceModified())
343     accept();
344   else
345     reject();
346 }
347
348
349 /*! \param theContainer Share the same container between several trees,
350 to edit them synchronously even without exchange of changes with SUIT_ShortcutMgr.
351 Pass nullptr to create non-synchronized tree. */
352 QtxShortcutTree::QtxShortcutTree(
353   std::shared_ptr<SUIT_ShortcutContainer> theContainer,
354   QWidget* theParent
355 ) : QTreeWidget(theParent),
356 myShortcutContainer(theContainer ? theContainer : std::shared_ptr<SUIT_ShortcutContainer>(new SUIT_ShortcutContainer()))
357 {
358   setColumnCount(2);
359   setSelectionMode(QAbstractItemView::SingleSelection);
360   setColumnWidth(0, COLUMN_SIZE);
361   setSortingEnabled(false); // Items are sorted in the same way, as in ShortcutContainer.
362   headerItem()->setHidden(true);
363   setExpandsOnDoubleClick(false); // Open shortcut editor on double click instead.
364   setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
365   setToolTip(tr("Double click to edit key sequence."));
366   setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
367   myEditDialog = new QtxEditKeySequenceDialog(this);
368
369   this->installEventFilter(this);
370   connect(this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*, int)), this, SLOT(onItemDoubleClicked(QTreeWidgetItem*, int)));
371
372   QtxShortcutTree::instances[myShortcutContainer.get()].emplace(this);
373 }
374
375 QtxShortcutTree::~QtxShortcutTree()
376 {
377   QtxShortcutTree::instances[myShortcutContainer.get()].erase(this);
378   if (QtxShortcutTree::instances[myShortcutContainer.get()].empty())
379     QtxShortcutTree::instances.erase(myShortcutContainer.get());
380 }
381
382 /*! \brief Copies shortcuts from ShortcutMgr. (Re)displays shortcuts of myModuleIDs. */
383 void QtxShortcutTree::setShortcutsFromManager()
384 {
385   const auto shortcutMgr = SUIT_ShortcutMgr::get();
386   *myShortcutContainer = shortcutMgr->getShortcutContainer();
387   // nb! ShortcutMgr never removes shortcuts from its container, only disables.
388
389   updateItems(false /*theHighlightModified*/, true /*theUpdateSyncTrees*/);
390 }
391
392 /*! \brief Copies shortcuts from resources, user files are not accounted. (Re)displays shortcuts of myModuleIDs. */
393 void QtxShortcutTree::setDefaultShortcuts()
394 {
395   SUIT_ShortcutContainer defaultShortcuts;
396   SUIT_ShortcutMgr::fillContainerFromPreferences(defaultShortcuts, true /*theDefaultOnly*/);
397
398   myShortcutContainer->merge(defaultShortcuts, true /*theOverride*/, true /*theTreatAbsentIncomingAsDisabled*/);
399   // nb! SUIT_ShortcutContainer never erases shortcuts, only disables.
400
401   updateItems(true /*theHighlightModified*/, true /*theUpdateSyncTrees*/);
402 }
403
404 /*! \brief Applies pending changes to ShortcutMgr. Updates other instances of QtxShortcutTree. */
405 void QtxShortcutTree::applyChangesToShortcutMgr()
406 {
407   const auto mgr = SUIT_ShortcutMgr::get();
408   mgr->mergeShortcutContainer(*myShortcutContainer);
409
410   // Update non-synchronized with this instances.
411   for (const auto& containerAndSyncTrees : QtxShortcutTree::instances) {
412     if (containerAndSyncTrees.first == myShortcutContainer.get())
413       continue;
414
415     const std::set<QtxShortcutTree*>& syncTrees = containerAndSyncTrees.second;
416     const auto itFirstSyncTree = syncTrees.begin();
417     if (itFirstSyncTree == syncTrees.end())
418       continue;
419
420     (*itFirstSyncTree)->setShortcutsFromManager();
421     const auto editDialog = (*itFirstSyncTree)->myEditDialog;
422     editDialog->setConfirmedKeySequence(mgr->getShortcutContainer().getKeySequence(editDialog->moduleID(), editDialog->inModuleActionID()));
423     editDialog->updateConflictsMessage();
424   }
425 }
426
427 std::shared_ptr<const SUIT_ShortcutContainer> QtxShortcutTree::shortcutContainer() const
428 {
429   return myShortcutContainer;
430 }
431
432 /*! \param If theUpdateSyncTrees, trees sharing the same shortcut container are updated. */
433 void QtxShortcutTree::updateItems(bool theHighlightModified, bool theUpdateSyncTrees)
434 {
435   const auto shortcutMgr = SUIT_ShortcutMgr::get();
436
437   for (const QString& moduleID : myModuleIDs) {
438     const auto& moduleShortcuts = myShortcutContainer->getModuleShortcutsInversed(moduleID);
439     if (moduleShortcuts.empty()) {
440       // Do not display empty module.
441       const auto moduleItemAndIdx = findModuleFolderItem(moduleID);
442       if (moduleItemAndIdx.second >= 0)
443         takeTopLevelItem(moduleItemAndIdx.second);
444
445       continue;
446     }
447
448     const auto moduleItemAndIdx = findModuleFolderItem(moduleID);
449     QtxShortcutTreeItem* moduleItem = moduleItemAndIdx.first;
450     if (!moduleItem) {
451       moduleItem = QtxShortcutTreeItem::createFolderItem(moduleID);
452       moduleItem->setName(shortcutMgr->getModuleName(moduleID));
453       addTopLevelItem(moduleItem);
454       moduleItem->setFlags(Qt::ItemIsEnabled);
455
456       for (const auto& shortcut : moduleShortcuts) {
457         const QString& inModuleActionID = shortcut.first;
458         const QKeySequence& keySequence = shortcut.second;
459         const QString keySequenceString = keySequence.toString();
460
461         auto shortcutItem = QtxShortcutTreeItem::createShortcutItem(moduleID, inModuleActionID);
462         if (!shortcutItem) {
463           ShCutDbg("QtxShortcutTree can't create child item for action ID = \"" + SUIT_ShortcutMgr::makeActionID(moduleID, inModuleActionID) + "\".");
464           continue;
465         }
466
467         shortcutItem->setName(shortcutMgr->getActionName(moduleID, inModuleActionID));
468         shortcutItem->setKeySequence(keySequenceString);
469
470         if (theHighlightModified) {
471           const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, inModuleActionID);
472           shortcutItem->highlightKeySequenceAsModified(keySequence != appliedKeySequence);
473         }
474
475         moduleItem->addChild(shortcutItem);
476       }
477
478       moduleItem->setExpanded(true); // Make tree expanded on first show.
479     }
480     else /* if the tree has the module-item */ {
481       for (int childIdx = 0; childIdx < moduleItem->childCount(); childIdx++) {
482         // Update exisiting items of a module.
483         QtxShortcutTreeItem* const childItem = static_cast<QtxShortcutTreeItem*>(moduleItem->child(childIdx));
484         const auto itShortcut = moduleShortcuts.find(childItem->myInModuleActionID);
485         if (itShortcut == moduleShortcuts.end()) {
486           // Shortcut of the item has been removed from myShortcutContainer - impossible.
487           continue;
488         }
489         const QKeySequence& newKeySequence = itShortcut->second;
490         const QString newKeySequenceString = newKeySequence.toString();
491         if (childItem->keySequence() != newKeySequenceString)
492           childItem->setKeySequence(newKeySequenceString);
493
494         if (theHighlightModified) {
495           const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, childItem->myInModuleActionID);
496           childItem->highlightKeySequenceAsModified(newKeySequence != appliedKeySequence);
497         }
498         else
499           childItem->highlightKeySequenceAsModified(false);
500       }
501
502       // Add new items if myShortcutContainer acquired new shortcuts, which may happen if a developer forgot
503       // to add shortcuts for registered actions to resource files.
504       if (moduleItem->childCount() < moduleShortcuts.size()) {
505         // Module shortcuts and tree items must be ordered with the same comparator. Now it is std::less(inModuleActionID_A, inModuleActionID_B).
506         std::set<QString> actionIDsOfItems;
507         for (int childIdx = 0; childIdx < moduleItem->childCount(); childIdx++) {
508           QtxShortcutTreeItem* const childItem = static_cast<QtxShortcutTreeItem*>(moduleItem->child(childIdx));
509           actionIDsOfItems.emplace(childItem->myInModuleActionID);
510         }
511
512         for (const auto& shortcut : moduleShortcuts) {
513           const QString& inModuleActionID = shortcut.first;
514           const QKeySequence& keySequence = shortcut.second;
515
516           auto itNewActionID = actionIDsOfItems.emplace(inModuleActionID).first;
517           int newItemIdx = 0;
518           // Replace this with std::distance if C++ >= 17.
519           auto it = actionIDsOfItems.begin();
520           while (it != itNewActionID) {
521             it++;
522             newItemIdx++;
523           }
524
525           const auto shortcutItem = QtxShortcutTreeItem::createShortcutItem(moduleID, inModuleActionID);
526           if (!shortcutItem) {
527             ShCutDbg("QtxShortcutTree can't create child item for action ID = \"" + SUIT_ShortcutMgr::makeActionID(moduleID, inModuleActionID) + "\".");
528             continue;
529           }
530
531           shortcutItem->setName(shortcutMgr->getActionName(moduleID, inModuleActionID));
532           shortcutItem->setKeySequence(keySequence.toString());
533
534           if (theHighlightModified) {
535             const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, inModuleActionID);
536             shortcutItem->highlightKeySequenceAsModified(keySequence != appliedKeySequence);
537           }
538
539           moduleItem->insertChild(newItemIdx, shortcutItem);
540         }
541       }
542     }
543   }
544
545   if (theUpdateSyncTrees) {
546     const std::set<QtxShortcutTree*>& syncTrees = QtxShortcutTree::instances[myShortcutContainer.get()];
547     for (const auto syncTree: syncTrees) {
548       if (syncTree == this)
549         continue;
550
551       syncTree->updateItems(theHighlightModified, false /*theUpdateSyncTrees*/);
552       const auto editDialog = syncTree->myEditDialog;
553       editDialog->setConfirmedKeySequence(myShortcutContainer->getKeySequence(editDialog->moduleID(), editDialog->inModuleActionID()));
554       editDialog->updateConflictsMessage();
555     }
556   }
557 }
558
559 /*! \returns Pointer and index of top-level item.
560 If the tree does not contain an item with theModuleID, returns {nullptr, -1}. */
561 std::pair<QtxShortcutTreeItem*, int> QtxShortcutTree::findModuleFolderItem(const QString& theModuleID) const
562 {
563   for (int moduleIdx = 0; moduleIdx < topLevelItemCount(); moduleIdx++) {
564     QtxShortcutTreeItem* moduleItem = static_cast<QtxShortcutTreeItem*>(topLevelItem(moduleIdx));
565     if (moduleItem->myModuleID == theModuleID)
566       return std::pair<QtxShortcutTreeItem*, int>(moduleItem, moduleIdx);
567   }
568   return std::pair<QtxShortcutTreeItem*, int>(nullptr, -1);
569 }
570
571 void QtxShortcutTree::onItemDoubleClicked(QTreeWidgetItem* theItem, int theColIdx)
572 {
573   QtxShortcutTreeItem* const item = static_cast<QtxShortcutTreeItem*>(theItem);
574   // Do not react if folder-item is clicked.
575   if (item->isFolder())
576     return;
577
578   myEditDialog->setModuleAndActionID(item->myModuleID, item->myInModuleActionID);
579   myEditDialog->setModuleAndActionName(static_cast<QtxShortcutTreeItem*>(item->parent())->name(), item->name());
580   myEditDialog->setConfirmedKeySequence(QKeySequence::fromString(item->keySequence()));
581   myEditDialog->updateConflictsMessage();
582   const bool somethingChanged = myEditDialog->exec() == QDialog::Accepted;
583
584   if (!somethingChanged)
585     return;
586
587   const QKeySequence newKeySequence = myEditDialog->editedKeySequence();
588
589   /** { moduleID, inModuleActionID }[] */
590   std::set<std::pair<QString, QString>> disabledActionIDs = myShortcutContainer->setShortcut(item->myModuleID, item->myInModuleActionID, newKeySequence, true /*override*/);
591
592   /** { moduleID, {inModuleActionID, keySequence}[] }[] */
593   std::map<QString, std::map<QString, QString>> changes;
594   changes[item->myModuleID][item->myInModuleActionID] = newKeySequence.toString();
595   for (const auto moduleAndActionID : disabledActionIDs) {
596     changes[moduleAndActionID.first][moduleAndActionID.second] = QString();
597   }
598
599   // Set new key sequences to shortcut items.
600   for (const auto& moduleIDAndChanges : changes) {
601     const QString& moduleID = moduleIDAndChanges.first;
602
603     const auto moduleItemAndIdx = findModuleFolderItem(moduleID);
604     const auto moduleItem = moduleItemAndIdx.first;
605     if (!moduleItem)
606       continue;
607
608     /** {inModuleActionID, newKeySequence}[] */
609     const std::map<QString, QString>& moduleChanges = moduleIDAndChanges.second;
610
611     // Go through module' shortcut items, and highlight those, whose key sequences differ from applied key sequences.
612     for (int childIdx = 0; childIdx < moduleItem->childCount(); childIdx++) {
613       QtxShortcutTreeItem* const childItem = static_cast<QtxShortcutTreeItem*>(moduleItem->child(childIdx));
614       const auto itChange = moduleChanges.find(childItem->myInModuleActionID);
615       if (itChange == moduleChanges.end()) {
616         // The shortcut has not been changed.
617         continue;
618       }
619
620       childItem->setKeySequence(itChange->second);
621
622       const QKeySequence& appliedKeySequence = SUIT_ShortcutMgr::get()->getKeySequence(moduleID, childItem->myInModuleActionID);
623       childItem->highlightKeySequenceAsModified(QKeySequence::fromString(itChange->second) != appliedKeySequence);
624     }
625   }
626 }
627
628 /*static*/ std::map<SUIT_ShortcutContainer*, std::set<QtxShortcutTree*>> QtxShortcutTree::instances =
629 std::map<SUIT_ShortcutContainer*, std::set<QtxShortcutTree*>>();
630
631
632 QtxShortcutTreeItem::QtxShortcutTreeItem(const QString& theModuleID, const QString& theInModuleActionID)
633 : QTreeWidgetItem(), myModuleID(theModuleID), myInModuleActionID(theInModuleActionID)
634 { }
635
636 /*static*/ QtxShortcutTreeItem* QtxShortcutTreeItem::createFolderItem(const QString& theModuleID)
637 {
638   auto item = new QtxShortcutTreeItem(theModuleID, QString());
639
640   QFont font = item->font(ElementIdx::Name);
641   font.setBold(true);
642   item->setFont(ElementIdx::Name, font);
643
644   return item;
645 }
646
647 /*! \returns nullptr if theInModuleActionID is empty. */
648 /*static*/ QtxShortcutTreeItem* QtxShortcutTreeItem::createShortcutItem(const QString& theModuleID, const QString& theInModuleActionID)
649 {
650   if (theInModuleActionID.isEmpty()) {
651     ShCutDbg("QtxShortcutTreeItem: attempt to create item with empty action ID.");
652     return nullptr;
653   }
654
655   return new QtxShortcutTreeItem(theModuleID, theInModuleActionID);
656 }
657
658 bool QtxShortcutTreeItem::isFolder() const
659 {
660   return myInModuleActionID.isEmpty();
661 }
662
663 /*! \brief Highlights text at ElementIdx::KeySequence. */
664 void QtxShortcutTreeItem::highlightKeySequenceAsModified(bool theHighlight)
665 {
666   static const QBrush bgHighlitingBrush = QBrush(Qt::darkGreen);
667   static const QBrush fgHighlitingBrush = QBrush(Qt::white);
668   static const QBrush noBrush = QBrush();
669
670   setBackground(ElementIdx::KeySequence, theHighlight ? bgHighlitingBrush : noBrush);
671   setForeground(ElementIdx::KeySequence, theHighlight ? fgHighlitingBrush : noBrush);
672 }
673
674 void QtxShortcutTreeItem::setName(const QString& theName)
675 {
676   setText(ElementIdx::Name, theName);
677 }
678
679 QString QtxShortcutTreeItem::name() const
680 {
681   return text(ElementIdx::Name);
682 }
683
684 void QtxShortcutTreeItem::setKeySequence(const QString& theKeySequence)
685 {
686   setText(ElementIdx::KeySequence, theKeySequence);
687 }
688
689 QString QtxShortcutTreeItem::keySequence() const
690 {
691   return text(ElementIdx::KeySequence);
692 }