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