File indexing completed on 2024-06-23 05:20:47

0001 /*
0002    Copyright (C) 2012, 2013 by Glad Deschrijver <glad.deschrijver@gmail.com>
0003 
0004    This program is free software; you can redistribute it and/or
0005    modify it under the terms of the GNU General Public License as
0006    published by the Free Software Foundation; either version 2 of
0007    the License or (at your option) version 3 or any later version
0008    accepted by the membership of KDE e.V. (or its successor approved
0009    by the membership of KDE e.V.), which shall act as a proxy
0010    defined in Section 14 of version 3 of the license.
0011 
0012    This program is distributed in the hope that it will be useful,
0013    but WITHOUT ANY WARRANTY; without even the implied warranty of
0014    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0015    GNU General Public License for more details.
0016 
0017    You should have received a copy of the GNU General Public License
0018    along with this program.  If not, see <http://www.gnu.org/licenses/>.
0019 */
0020 
0021 #include "ShortcutConfigWidget.h"
0022 
0023 #ifndef QT_NO_SHORTCUT
0024 
0025 #include <QKeyEvent>
0026 #include <QMessageBox>
0027 #include <QSettings>
0028 
0029 #include "ShortcutHandler.h"
0030 #include "Common/SettingsCategoryGuard.h"
0031 #include "UiUtils/IconLoader.h"
0032 
0033 namespace Gui
0034 {
0035 
0036 ShortcutConfigWidget::ShortcutConfigWidget(QWidget *parent)
0037     : QWidget(parent)
0038     , m_shortcutsShouldBeRestored(false)
0039 {
0040     ui.setupUi(this);
0041     setWindowTitle(tr("Configure Shortcuts") + QLatin1String(" - ") + tr("Trojitá"));
0042 
0043     ui.shortcutTreeWidget->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
0044     ui.shortcutTreeWidget->header()->setSectionResizeMode(1, QHeaderView::Stretch);
0045     ui.shortcutTreeWidget->setUniformRowHeights(true); // all rows have the same height
0046     ui.shortcutTreeWidget->installEventFilter(this);
0047 
0048     ui.gridLayout->setColumnStretch(2, 1); // make sure the buttons are not too large
0049     ui.gridLayout->setContentsMargins(0, 0, 0, 0);
0050 
0051     setFocusProxy(ui.shortcutTreeWidget);
0052 
0053     connect(ui.searchLineEdit, &QLineEdit::textChanged, this, &ShortcutConfigWidget::searchItems);
0054     connect(ui.clearPushButton, &QAbstractButton::clicked, this, &ShortcutConfigWidget::clearShortcut);
0055     connect(ui.useDefaultPushButton, &QAbstractButton::clicked, this, &ShortcutConfigWidget::restoreDefaultShortcut);
0056 }
0057 
0058 ShortcutConfigWidget::~ShortcutConfigWidget()
0059 {
0060 }
0061 
0062 QPushButton *ShortcutConfigWidget::clearButton()
0063 {
0064     return ui.clearPushButton;
0065 }
0066 
0067 QPushButton *ShortcutConfigWidget::useDefaultButton()
0068 {
0069     return ui.useDefaultPushButton;
0070 }
0071 
0072 /***************************************************************************/
0073 
0074 void ShortcutConfigWidget::setExclusivityGroups(const QList<QStringList> &groups)
0075 {
0076     // this function must be called after all actions are added
0077     // we set a unique ID for each exclusivity group and we set the data of each toplevel item to the ID of the group to which it belongs
0078     // the ID must be a negative number, since eventFilter() assumes that the ID is either a negative number or the index of the corresponding action in m_actions
0079     m_exclusivityGroups = groups;
0080     const int topLevelItemCount = ui.shortcutTreeWidget->topLevelItemCount();
0081     for (int i = 0; i < topLevelItemCount; ++i) {
0082         QTreeWidgetItem *topLevelItem = ui.shortcutTreeWidget->topLevelItem(i);
0083         const QString parentId = topLevelItem->text(0);
0084         for (int j = 0; j < m_exclusivityGroups.size(); ++j)
0085             if (m_exclusivityGroups.at(j).contains(parentId))
0086                 topLevelItem->setData(1, Qt::UserRole, -j-1);
0087     }
0088 }
0089 
0090 void ShortcutConfigWidget::addItem(const QString &actionName, const QString &text, const QString &shortcut, const QIcon &icon, const QString &parentId)
0091 {
0092     // search correct toplevel item
0093     int topLevelItemNumber = -1;
0094     const int topLevelItemCount = ui.shortcutTreeWidget->topLevelItemCount();
0095     for (int i = 0; i < topLevelItemCount; ++i) {
0096         if (ui.shortcutTreeWidget->topLevelItem(i)->text(0) == parentId) {
0097             topLevelItemNumber = i;
0098             break;
0099         }
0100     }
0101     if (topLevelItemNumber < 0) { // toplevel item with name parentId doesn't exist yet, so create
0102         QTreeWidgetItem *item = new QTreeWidgetItem(ui.shortcutTreeWidget);
0103         item->setText(0, parentId);
0104         topLevelItemNumber = topLevelItemCount;
0105         item->setSizeHint(1, QSize(0, 1.5 * qApp->fontMetrics().height())); // since the second column is stretchable, it doesn't matter that the width of the size hint is set to 0
0106         item->setData(1, Qt::UserRole, QString());
0107     }
0108 
0109     // create item
0110     QString textWithoutAccelerator = text;
0111     QTreeWidgetItem *item = new QTreeWidgetItem(ui.shortcutTreeWidget->topLevelItem(topLevelItemNumber));
0112     item->setText(0, textWithoutAccelerator.remove(QLatin1Char('&')));
0113     item->setIcon(0, icon);
0114     item->setText(1, shortcut);
0115     item->setData(1, Qt::UserRole, actionName); // store objectName of the current action
0116 }
0117 
0118 void ShortcutConfigWidget::setActionDescriptions(const QHash<QString, ActionDescription> &actionDescriptions)
0119 {
0120     m_actionDescriptions = actionDescriptions;
0121     ui.shortcutTreeWidget->scrollToItem(ui.shortcutTreeWidget->invisibleRootItem()->child(0));
0122     ui.shortcutTreeWidget->clear();
0123     for (QHash<QString, ActionDescription>::const_iterator it = m_actionDescriptions.constBegin(); it != m_actionDescriptions.constEnd(); ++it) {
0124         ActionDescription actionDescription = it.value();
0125         addItem(it.key(), actionDescription.text, actionDescription.shortcut, UiUtils::loadIcon(actionDescription.iconName), actionDescription.parentId);
0126     }
0127     ui.shortcutTreeWidget->expandAll();
0128 }
0129 
0130 /***************************************************************************/
0131 
0132 void ShortcutConfigWidget::searchItems(const QString &text)
0133 {
0134     const int topLevelItemCount = ui.shortcutTreeWidget->topLevelItemCount();
0135     for (int i = 0; i < topLevelItemCount; ++i) {
0136         QTreeWidgetItem *topLevelItem = ui.shortcutTreeWidget->topLevelItem(i);
0137         const int childCount = topLevelItem->childCount();
0138         for (int j = 0; j < childCount; ++j) {
0139             QTreeWidgetItem *childItem = topLevelItem->child(j);
0140             if (childItem->text(0).contains(text, Qt::CaseInsensitive) || childItem->text(1).contains(text, Qt::CaseInsensitive))
0141                 childItem->setHidden(false);
0142             else
0143                 childItem->setHidden(true);
0144         }
0145     }
0146 }
0147 
0148 /***************************************************************************/
0149 
0150 bool ShortcutConfigWidget::eventFilter(QObject *obj, QEvent *event)
0151 {
0152     Q_UNUSED(obj);
0153     if (event->type() != QEvent::KeyPress)
0154         return false;
0155 
0156     QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
0157     QString keySequence;
0158     // don't allow modifiers to be handled as single keys and skip CapsLock and NumLock
0159     if (keyEvent->key() == Qt::Key_Control || keyEvent->key() == Qt::Key_Shift
0160             || keyEvent->key() == Qt::Key_Meta || keyEvent->key() == Qt::Key_Alt
0161             || keyEvent->key() == Qt::Key_Super_L || keyEvent->key() == Qt::Key_AltGr
0162             || keyEvent->key() == Qt::Key_CapsLock || keyEvent->key() == Qt::Key_NumLock)
0163         return false;
0164     // skip some particular keys (note that Qt::Key_Up and friends are used to navigate the list, in order to avoid that they interfere with the shortcut changing, we skip them)
0165     if (keyEvent->modifiers() == Qt::NoModifier
0166             && (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down
0167                 || keyEvent->key() == Qt::Key_Left || keyEvent->key() == Qt::Key_Right
0168                 || keyEvent->key() == Qt::Key_PageUp || keyEvent->key() == Qt::Key_PageDown
0169                 || keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab
0170                 || keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Escape))
0171         return false;
0172     if (keyEvent->key() == Qt::Key_Backtab) // the above doesn't catch "Shift+Tab"
0173         return false;
0174 
0175     // create string representation of the new shortcut
0176     if (keyEvent->modifiers() & Qt::ControlModifier)
0177         keySequence += tr("Ctrl+", "Shortcut modifier");
0178     if (keyEvent->modifiers() & Qt::AltModifier)
0179         keySequence += tr("Alt+", "Shortcut modifier");
0180     if (keyEvent->modifiers() & Qt::ShiftModifier)
0181         keySequence += tr("Shift+", "Shortcut modifier");
0182     keySequence += QKeySequence(keyEvent->key()).toString(QKeySequence::PortableText);
0183 
0184     // replace shortcut in the list (but not yet for real, this is done when the user accepts the dialog)
0185     QTreeWidgetItem *currentItem = ui.shortcutTreeWidget->currentItem();
0186     if (!currentItem) // this is the case when ui.shortcutTreeWidget is empty
0187         return false;
0188     const QString actionName = currentItem->data(1, Qt::UserRole).toString();
0189     if (actionName.isEmpty()) // this is the case when a toplevel item is selected
0190         return false;
0191     // test whether the new shortcut is already defined for another action, if yes then ask the user to set an empty shortcut for the old action
0192     const QVariant parentId = currentItem->parent()->data(1, Qt::UserRole);
0193     const int topLevelItemCount = ui.shortcutTreeWidget->topLevelItemCount();
0194     for (int i = 0; i < topLevelItemCount; ++i) {
0195         QTreeWidgetItem *topLevelItem = ui.shortcutTreeWidget->topLevelItem(i);
0196         if (topLevelItem->data(1, Qt::UserRole) != parentId) // only deal with actions in the same exclusivity group
0197             continue;
0198         const int childCount = topLevelItem->childCount();
0199         for (int j = 0; j < childCount; ++j) {
0200             QTreeWidgetItem *childItem = topLevelItem->child(j);
0201             if (keySequence == childItem->text(1) && childItem->data(1, Qt::UserRole).toString() != actionName) {
0202                 QMessageBox::StandardButton result = QMessageBox::warning(this,
0203                                                      tr("Shortcut Conflicts") + QLatin1String(" - ") + tr("Trojitá"),
0204                                                      tr("<p>The \"%1\" shortcut is ambiguous with the following shortcut:</p>"
0205                                                         "<p>%2</p><p>Do you want to assign an empty shortcut to this action?</p>")
0206                                                      .arg(keySequence, childItem->text(0)),
0207                                                      QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok);
0208                 if (result == QMessageBox::Ok)
0209                     childItem->setText(1, QString());
0210                 else
0211                     return false;
0212             }
0213         }
0214     }
0215     // finally we can set the new shortcut
0216     currentItem->setText(1, keySequence);
0217     return true;
0218 }
0219 
0220 void ShortcutConfigWidget::clearShortcut()
0221 {
0222     QTreeWidgetItem *currentItem = ui.shortcutTreeWidget->currentItem();
0223     const QString actionName = currentItem->data(1, Qt::UserRole).toString();
0224     if (actionName.isEmpty())
0225         return;
0226     currentItem->setText(1, QString());
0227 }
0228 
0229 void ShortcutConfigWidget::restoreDefaultShortcut()
0230 {
0231     QTreeWidgetItem *currentItem = ui.shortcutTreeWidget->currentItem();
0232     const QString actionName = currentItem->data(1, Qt::UserRole).toString();
0233     if (actionName.isEmpty())
0234         return;
0235     currentItem->setText(1, m_actionDescriptions[actionName].defaultShortcut);
0236 }
0237 
0238 /***************************************************************************/
0239 
0240 void ShortcutConfigWidget::accept()
0241 {
0242     // set shortcuts for real (but they are not yet saved in the settings on disk, this is done in writeSettings())
0243     const int topLevelItemCount = ui.shortcutTreeWidget->topLevelItemCount();
0244     for (int i = 0; i < topLevelItemCount; ++i) {
0245         QTreeWidgetItem *topLevelItem = ui.shortcutTreeWidget->topLevelItem(i);
0246         const int childCount = topLevelItem->childCount();
0247         for (int j = 0; j < childCount; ++j) {
0248             QTreeWidgetItem *childItem = topLevelItem->child(j);
0249             const QString actionName = childItem->data(1, Qt::UserRole).toString();
0250             m_actionDescriptions[actionName].shortcut = childItem->text(1);
0251         }
0252     }
0253     writeSettings();
0254     Q_EMIT(shortcutsChanged(m_actionDescriptions));
0255 }
0256 
0257 void ShortcutConfigWidget::reject()
0258 {
0259     if (!m_shortcutsShouldBeRestored)
0260         return;
0261 
0262     // restore unsaved shortcuts in the tree widget
0263     const int topLevelItemCount = ui.shortcutTreeWidget->topLevelItemCount();
0264     for (int i = 0; i < topLevelItemCount; ++i) {
0265         QTreeWidgetItem *topLevelItem = ui.shortcutTreeWidget->topLevelItem(i);
0266         const int childCount = topLevelItem->childCount();
0267         for (int j = 0; j < childCount; ++j) {
0268             QTreeWidgetItem *childItem = topLevelItem->child(j);
0269             const QString actionName = childItem->data(1, Qt::UserRole).toString();
0270             childItem->setText(1, m_actionDescriptions[actionName].shortcut);
0271         }
0272     }
0273     m_shortcutsShouldBeRestored = false;
0274 }
0275 
0276 void ShortcutConfigWidget::showEvent(QShowEvent *event)
0277 {
0278     Q_UNUSED(event);
0279     reject(); // restore unsaved shortcuts in the tree widget before the configuration dialog is shown again
0280     m_shortcutsShouldBeRestored = true;
0281     ui.shortcutTreeWidget->sortByColumn(0, Qt::AscendingOrder);
0282 }
0283 
0284 /***************************************************************************/
0285 
0286 /**
0287  * \see ShortcutHandler::readSettings()
0288  */
0289 void ShortcutConfigWidget::writeSettings()
0290 {
0291     QSettings *settings = ShortcutHandler::instance()->settingsObject();
0292     Q_ASSERT_X(settings, "ShortcutHandler", "no QSettings object found: a settings object should first be set using setSettingsObject() and then readSettings() should be called when initializing your program; note that this QSettings object should exist during the entire lifetime of the ShortcutHandler object and therefore not be deleted first");
0293     Common::SettingsCategoryGuard guard(settings, QStringLiteral("ShortcutHandler"));
0294     settings->remove(QString());
0295     settings->beginWriteArray(QStringLiteral("Shortcuts"));
0296     int index = 0;
0297     for (QHash<QString, ActionDescription>::const_iterator it = m_actionDescriptions.constBegin(); it != m_actionDescriptions.constEnd(); ++it) {
0298         ActionDescription actionDescription = it.value();
0299         const QString shortcut = actionDescription.shortcut;
0300         if (shortcut != actionDescription.defaultShortcut) {
0301             settings->setArrayIndex(index);
0302             settings->setValue(QStringLiteral("Action"), it.key());
0303             settings->setValue(QStringLiteral("Shortcut"), actionDescription.shortcut);
0304             ++index;
0305         }
0306     }
0307     settings->endArray();
0308 }
0309 
0310 } // namespace Gui
0311 
0312 #endif // QT_NO_SHORTCUT