File indexing completed on 2024-07-14 14:38:41

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 1998 Mark Donohoe <donohoe@kde.org>
0004     SPDX-FileCopyrightText: 1997 Nicolas Hadacek <hadacek@kde.org>
0005     SPDX-FileCopyrightText: 1998 Matthias Ettrich <ettrich@kde.org>
0006     SPDX-FileCopyrightText: 2001 Ellis Whitehead <ellis@kde.org>
0007     SPDX-FileCopyrightText: 2006 Hamish Rodda <rodda@kde.org>
0008     SPDX-FileCopyrightText: 2007 Roberto Raggi <roberto@kdevelop.org>
0009     SPDX-FileCopyrightText: 2007 Andreas Hartmetz <ahartmetz@gmail.com>
0010     SPDX-FileCopyrightText: 2008 Michael Jansen <kde@michael-jansen.biz>
0011 
0012     SPDX-License-Identifier: LGPL-2.0-or-later
0013 */
0014 #include "kshortcutsdialog_p.h"
0015 
0016 #include <QAction>
0017 #include <QApplication>
0018 #include <QHeaderView>
0019 #include <QKeyEvent>
0020 #include <QLabel>
0021 #include <QPainter>
0022 #include <QTreeWidgetItemIterator>
0023 
0024 KShortcutsEditorDelegate::KShortcutsEditorDelegate(QTreeWidget *parent, bool allowLetterShortcuts)
0025     : KExtendableItemDelegate(parent)
0026     , m_allowLetterShortcuts(allowLetterShortcuts)
0027 {
0028     Q_ASSERT(qobject_cast<QAbstractItemView *>(parent));
0029 
0030     const QSize indicatorSize(16, 16);
0031     const qreal dpr = parent->devicePixelRatioF();
0032     QPixmap pixmap(indicatorSize * dpr);
0033 
0034     pixmap.fill(QColor(Qt::transparent));
0035     pixmap.setDevicePixelRatio(dpr);
0036     QPainter p(&pixmap);
0037     QStyleOption option;
0038     option.rect = QRect(QPoint(0, 0), indicatorSize);
0039 
0040     bool isRtl = QApplication::isRightToLeft();
0041     QApplication::style()->drawPrimitive(isRtl ? QStyle::PE_IndicatorArrowLeft : QStyle::PE_IndicatorArrowRight, &option, &p);
0042     p.end();
0043     setExtendPixmap(pixmap);
0044 
0045     pixmap.fill(QColor(Qt::transparent));
0046     pixmap.setDevicePixelRatio(dpr);
0047     p.begin(&pixmap);
0048     QApplication::style()->drawPrimitive(QStyle::PE_IndicatorArrowDown, &option, &p);
0049     p.end();
0050     setContractPixmap(pixmap);
0051 
0052     parent->installEventFilter(this);
0053 
0054     // Listen to activation signals
0055     // connect(parent, SIGNAL(activated(QModelIndex)), this, SLOT(itemActivated(QModelIndex)));
0056     connect(parent, &QAbstractItemView::clicked, this, &KShortcutsEditorDelegate::itemActivated);
0057 
0058     // Listen to collapse signals
0059     connect(parent, &QTreeView::collapsed, this, &KShortcutsEditorDelegate::itemCollapsed);
0060 }
0061 
0062 void KShortcutsEditorDelegate::stealShortcut(const QKeySequence &seq, QAction *action)
0063 {
0064     QTreeWidget *view = static_cast<QTreeWidget *>(parent());
0065 
0066     // Iterate over all items
0067     QTreeWidgetItemIterator it(view, QTreeWidgetItemIterator::NoChildren);
0068 
0069     for (; (*it); ++it) {
0070         KShortcutsEditorItem *item = dynamic_cast<KShortcutsEditorItem *>(*it);
0071         if (item && item->data(0, ObjectRole).value<QObject *>() == action) {
0072             // We found the action, snapshot the current state. Steal the
0073             // shortcut. We will save the change later.
0074             const QList<QKeySequence> cut = action->shortcuts();
0075             const QKeySequence primary = cut.isEmpty() ? QKeySequence() : cut.at(0);
0076             const QKeySequence alternate = cut.size() <= 1 ? QKeySequence() : cut.at(1);
0077 
0078             if (primary.matches(seq) != QKeySequence::NoMatch //
0079                 || seq.matches(primary) != QKeySequence::NoMatch) {
0080                 item->setKeySequence(LocalPrimary, QKeySequence());
0081             }
0082 
0083             if (alternate.matches(seq) != QKeySequence::NoMatch //
0084                 || seq.matches(alternate) != QKeySequence::NoMatch) {
0085                 item->setKeySequence(LocalAlternate, QKeySequence());
0086             }
0087             break;
0088         }
0089     }
0090 }
0091 
0092 QSize KShortcutsEditorDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
0093 {
0094     QSize ret(KExtendableItemDelegate::sizeHint(option, index));
0095     ret.rheight() += 4;
0096     return ret;
0097 }
0098 
0099 // slot
0100 void KShortcutsEditorDelegate::itemActivated(const QModelIndex &_index)
0101 {
0102     // As per our constructor our parent *is* a QTreeWidget
0103     QTreeWidget *view = static_cast<QTreeWidget *>(parent());
0104     QModelIndex index(_index);
0105 
0106     KShortcutsEditorItem *item = KShortcutsEditorPrivate::itemFromIndex(view, index);
0107     if (!item) {
0108         // that probably was a non-leaf (type() !=ActionItem) item
0109         return;
0110     }
0111 
0112     int column = index.column();
0113     if (column == Name) {
0114         // If user click in the name column activate the (Global|Local)Primary
0115         // column if possible.
0116         if (!view->header()->isSectionHidden(LocalPrimary)) {
0117             column = LocalPrimary;
0118         } else if (!view->header()->isSectionHidden(GlobalPrimary)) {
0119             column = GlobalPrimary;
0120         } else {
0121             // do nothing.
0122         }
0123         index = index.sibling(index.row(), column);
0124         view->selectionModel()->select(index, QItemSelectionModel::SelectCurrent);
0125     }
0126 
0127     // Check if the models wants us to edit the item at index
0128     if (!index.data(ShowExtensionIndicatorRole).toBool()) {
0129         return;
0130     }
0131 
0132     if (!isExtended(index)) {
0133         // we only want maximum ONE extender open at any time.
0134         if (m_editingIndex.isValid()) {
0135             KShortcutsEditorItem *oldItem = KShortcutsEditorPrivate::itemFromIndex(view, m_editingIndex);
0136             Q_ASSERT(oldItem); // here we really expect nothing but a real KShortcutsEditorItem
0137 
0138             oldItem->setNameBold(false);
0139             contractItem(m_editingIndex);
0140         }
0141 
0142         m_editingIndex = index;
0143         QWidget *viewport = static_cast<QAbstractItemView *>(parent())->viewport();
0144 
0145         if (column >= LocalPrimary && column <= GlobalAlternate) {
0146             ShortcutEditWidget *editor = new ShortcutEditWidget(viewport,
0147                                                                 index.data(DefaultShortcutRole).value<QKeySequence>(),
0148                                                                 index.data(ShortcutRole).value<QKeySequence>(),
0149                                                                 m_allowLetterShortcuts);
0150             if (column == GlobalPrimary) {
0151                 QObject *action = index.data(ObjectRole).value<QObject *>();
0152                 editor->setAction(action);
0153                 editor->setMultiKeyShortcutsAllowed(false);
0154                 QString componentName = action->property("componentName").toString();
0155                 if (componentName.isEmpty()) {
0156                     componentName = QCoreApplication::applicationName();
0157                 }
0158                 editor->setComponentName(componentName);
0159             }
0160 
0161             m_editor = editor;
0162             // For global shortcuts check against the kde standard shortcuts
0163             if (column == GlobalPrimary || column == GlobalAlternate) {
0164                 editor->setCheckForConflictsAgainst(KKeySequenceWidget::LocalShortcuts | KKeySequenceWidget::GlobalShortcuts
0165                                                     | KKeySequenceWidget::StandardShortcuts);
0166             }
0167 
0168             editor->setCheckActionCollections(m_checkActionCollections);
0169 
0170             connect(editor, &ShortcutEditWidget::keySequenceChanged, this, &KShortcutsEditorDelegate::keySequenceChanged);
0171             connect(editor, &ShortcutEditWidget::stealShortcut, this, &KShortcutsEditorDelegate::stealShortcut);
0172 
0173         } else if (column == RockerGesture) {
0174             m_editor = new QLabel(QStringLiteral("A lame placeholder"), viewport);
0175 
0176         } else if (column == ShapeGesture) {
0177             m_editor = new QLabel(QStringLiteral("<i>A towel</i>"), viewport);
0178 
0179         } else {
0180             return;
0181         }
0182 
0183         m_editor->installEventFilter(this);
0184         item->setNameBold(true);
0185         extendItem(m_editor, index);
0186 
0187     } else {
0188         // the item is extended, and clicking on it again closes it
0189         item->setNameBold(false);
0190         contractItem(index);
0191         view->selectionModel()->select(index, QItemSelectionModel::Clear);
0192         m_editingIndex = QModelIndex();
0193         m_editor = nullptr;
0194     }
0195 }
0196 
0197 // slot
0198 void KShortcutsEditorDelegate::itemCollapsed(const QModelIndex &index)
0199 {
0200     if (!m_editingIndex.isValid()) {
0201         return;
0202     }
0203 
0204     const QAbstractItemModel *model = index.model();
0205     for (int row = 0; row < model->rowCount(index); ++row) {
0206         for (int col = 0; col < index.model()->columnCount(index); ++col) {
0207             QModelIndex colIndex = model->index(row, col, index);
0208 
0209             if (colIndex == m_editingIndex) {
0210                 itemActivated(m_editingIndex); // this will *close* the item's editor because it's already open
0211             }
0212         }
0213     }
0214 }
0215 
0216 // slot
0217 void KShortcutsEditorDelegate::hiddenBySearchLine(QTreeWidgetItem *item, bool hidden)
0218 {
0219     if (!hidden || !item) {
0220         return;
0221     }
0222     QTreeWidget *view = static_cast<QTreeWidget *>(parent());
0223     QTreeWidgetItem *editingItem = KShortcutsEditorPrivate::itemFromIndex(view, m_editingIndex);
0224     if (editingItem == item) {
0225         itemActivated(m_editingIndex); // this will *close* the item's editor because it's already open
0226     }
0227 }
0228 
0229 bool KShortcutsEditorDelegate::eventFilter(QObject *o, QEvent *e)
0230 {
0231     if (o == m_editor) {
0232         // Prevent clicks in the empty part of the editor widget from closing the editor
0233         // because they would propagate to the itemview and be interpreted as a click in
0234         // an item's rect. That in turn would lead to an itemActivated() call, closing
0235         // the current editor.
0236 
0237         switch (e->type()) {
0238         case QEvent::MouseButtonPress:
0239         case QEvent::MouseButtonRelease:
0240         case QEvent::MouseButtonDblClick:
0241             return true;
0242         default:
0243             return false;
0244         }
0245     } else if (o == parent()) {
0246         // Make left/right cursor keys switch items instead of operate the scroll bar
0247         // (subclassing QtreeView/Widget would be cleaner but much more of a hassle)
0248         // Note that in our case we know that the selection mode is SingleSelection,
0249         // so we don't have to ask QAbstractItemView::selectionCommand() et al.
0250 
0251         if (e->type() != QEvent::KeyPress) {
0252             return false;
0253         }
0254         QKeyEvent *ke = static_cast<QKeyEvent *>(e);
0255         QTreeWidget *view = static_cast<QTreeWidget *>(parent());
0256         QItemSelectionModel *selection = view->selectionModel();
0257         QModelIndex index = selection->currentIndex();
0258 
0259         switch (ke->key()) {
0260         case Qt::Key_Space:
0261         case Qt::Key_Select:
0262             // we are not using the standard "open editor" mechanism of QAbstractItemView,
0263             // so let's emulate that here.
0264             itemActivated(index);
0265             return true;
0266         case Qt::Key_Left:
0267             index = index.sibling(index.row(), index.column() - 1);
0268             break;
0269         case Qt::Key_Right:
0270             index = index.sibling(index.row(), index.column() + 1);
0271             break;
0272         default:
0273             return false;
0274         }
0275         // a cursor key was pressed
0276         if (index.isValid()) {
0277             selection->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect);
0278             //### using PositionAtCenter for now;
0279             // EnsureVisible has no effect which seems to be a bug.
0280             view->scrollTo(index, QAbstractItemView::PositionAtCenter);
0281         }
0282         return true;
0283     }
0284     return false;
0285 }
0286 
0287 // slot
0288 void KShortcutsEditorDelegate::keySequenceChanged(const QKeySequence &seq)
0289 {
0290     QVariant ret = QVariant::fromValue(seq);
0291     Q_EMIT shortcutChanged(ret, m_editingIndex);
0292 }
0293 
0294 void KShortcutsEditorDelegate::setCheckActionCollections(const QList<KActionCollection *> &checkActionCollections)
0295 {
0296     m_checkActionCollections = checkActionCollections;
0297 }
0298 
0299 #if 0
0300 //slot
0301 void KShortcutsEditorDelegate::shapeGestureChanged(const KShapeGesture &gest)
0302 {
0303     //this is somewhat verbose because the gesture types are not "built in" to QVariant
0304     QVariant ret = QVariant::fromValue(gest);
0305     Q_EMIT shortcutChanged(ret, m_editingIndex);
0306 }
0307 #endif
0308 
0309 #if 0
0310 //slot
0311 void KShortcutsEditorDelegate::rockerGestureChanged(const KRockerGesture &gest)
0312 {
0313     QVariant ret = QVariant::fromValue(gest);
0314     Q_EMIT shortcutChanged(ret, m_editingIndex);
0315 }
0316 #endif