| 1 | /* |
| 2 | This file is part of the KDE libraries |
| 3 | SPDX-FileCopyrightText: 1998 Mark Donohoe <donohoe@kde.org> |
| 4 | SPDX-FileCopyrightText: 1997 Nicolas Hadacek <hadacek@kde.org> |
| 5 | SPDX-FileCopyrightText: 1998 Matthias Ettrich <ettrich@kde.org> |
| 6 | SPDX-FileCopyrightText: 2001 Ellis Whitehead <ellis@kde.org> |
| 7 | SPDX-FileCopyrightText: 2006 Hamish Rodda <rodda@kde.org> |
| 8 | SPDX-FileCopyrightText: 2007 Roberto Raggi <roberto@kdevelop.org> |
| 9 | SPDX-FileCopyrightText: 2007 Andreas Hartmetz <ahartmetz@gmail.com> |
| 10 | SPDX-FileCopyrightText: 2008 Michael Jansen <kde@michael-jansen.biz> |
| 11 | |
| 12 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 13 | */ |
| 14 | #include "kshortcutsdialog_p.h" |
| 15 | |
| 16 | #include <QAction> |
| 17 | #include <QApplication> |
| 18 | #include <QHeaderView> |
| 19 | #include <QKeyEvent> |
| 20 | #include <QLabel> |
| 21 | #include <QPainter> |
| 22 | #include <QTreeWidgetItemIterator> |
| 23 | |
| 24 | KShortcutsEditorDelegate::KShortcutsEditorDelegate(QTreeWidget *parent, bool allowLetterShortcuts) |
| 25 | : KExtendableItemDelegate(parent) |
| 26 | , m_allowLetterShortcuts(allowLetterShortcuts) |
| 27 | { |
| 28 | Q_ASSERT(qobject_cast<QAbstractItemView *>(parent)); |
| 29 | |
| 30 | const QSize indicatorSize(16, 16); |
| 31 | const qreal dpr = parent->devicePixelRatioF(); |
| 32 | QPixmap pixmap(indicatorSize * dpr); |
| 33 | |
| 34 | pixmap.fill(fillColor: QColor(Qt::transparent)); |
| 35 | pixmap.setDevicePixelRatio(dpr); |
| 36 | QPainter p(&pixmap); |
| 37 | QStyleOption option; |
| 38 | option.rect = QRect(QPoint(0, 0), indicatorSize); |
| 39 | |
| 40 | bool isRtl = QApplication::isRightToLeft(); |
| 41 | QApplication::style()->drawPrimitive(pe: isRtl ? QStyle::PE_IndicatorArrowLeft : QStyle::PE_IndicatorArrowRight, opt: &option, p: &p); |
| 42 | p.end(); |
| 43 | setExtendPixmap(pixmap); |
| 44 | |
| 45 | pixmap.fill(fillColor: QColor(Qt::transparent)); |
| 46 | pixmap.setDevicePixelRatio(dpr); |
| 47 | p.begin(&pixmap); |
| 48 | QApplication::style()->drawPrimitive(pe: QStyle::PE_IndicatorArrowDown, opt: &option, p: &p); |
| 49 | p.end(); |
| 50 | setContractPixmap(pixmap); |
| 51 | |
| 52 | parent->installEventFilter(filterObj: this); |
| 53 | |
| 54 | // Listen to activation signals |
| 55 | // connect(parent, SIGNAL(activated(QModelIndex)), this, SLOT(itemActivated(QModelIndex))); |
| 56 | connect(sender: parent, signal: &QAbstractItemView::clicked, context: this, slot: &KShortcutsEditorDelegate::itemActivated); |
| 57 | |
| 58 | // Listen to collapse signals |
| 59 | connect(sender: parent, signal: &QTreeView::collapsed, context: this, slot: &KShortcutsEditorDelegate::itemCollapsed); |
| 60 | } |
| 61 | |
| 62 | void KShortcutsEditorDelegate::stealShortcut(const QKeySequence &seq, QAction *action) |
| 63 | { |
| 64 | QTreeWidget *view = static_cast<QTreeWidget *>(parent()); |
| 65 | |
| 66 | // Iterate over all items |
| 67 | QTreeWidgetItemIterator it(view, QTreeWidgetItemIterator::NoChildren); |
| 68 | |
| 69 | for (; (*it); ++it) { |
| 70 | KShortcutsEditorItem *item = dynamic_cast<KShortcutsEditorItem *>(*it); |
| 71 | if (item && item->data(column: 0, role: ObjectRole).value<QObject *>() == action) { |
| 72 | // We found the action, snapshot the current state. Steal the |
| 73 | // shortcut. We will save the change later. |
| 74 | const QList<QKeySequence> cut = action->shortcuts(); |
| 75 | const QKeySequence primary = cut.isEmpty() ? QKeySequence() : cut.at(i: 0); |
| 76 | const QKeySequence alternate = cut.size() <= 1 ? QKeySequence() : cut.at(i: 1); |
| 77 | |
| 78 | if (primary.matches(seq) != QKeySequence::NoMatch // |
| 79 | || seq.matches(seq: primary) != QKeySequence::NoMatch) { |
| 80 | item->setKeySequence(column: LocalPrimary, seq: QKeySequence()); |
| 81 | } |
| 82 | |
| 83 | if (alternate.matches(seq) != QKeySequence::NoMatch // |
| 84 | || seq.matches(seq: alternate) != QKeySequence::NoMatch) { |
| 85 | item->setKeySequence(column: LocalAlternate, seq: QKeySequence()); |
| 86 | } |
| 87 | break; |
| 88 | } |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | QSize KShortcutsEditorDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const |
| 93 | { |
| 94 | QSize ret(KExtendableItemDelegate::sizeHint(option, index)); |
| 95 | ret.rheight() += 4; |
| 96 | return ret; |
| 97 | } |
| 98 | |
| 99 | // slot |
| 100 | void KShortcutsEditorDelegate::itemActivated(const QModelIndex &_index) |
| 101 | { |
| 102 | // As per our constructor our parent *is* a QTreeWidget |
| 103 | QTreeWidget *view = static_cast<QTreeWidget *>(parent()); |
| 104 | QModelIndex index(_index); |
| 105 | |
| 106 | KShortcutsEditorItem *item = KShortcutsEditorPrivate::itemFromIndex(w: view, index); |
| 107 | if (!item) { |
| 108 | // that probably was a non-leaf (type() !=ActionItem) item |
| 109 | return; |
| 110 | } |
| 111 | |
| 112 | int column = index.column(); |
| 113 | if (column == Name) { |
| 114 | // If user click in the name column activate the (Global|Local)Primary |
| 115 | // column if possible. |
| 116 | if (!view->header()->isSectionHidden(logicalIndex: LocalPrimary)) { |
| 117 | column = LocalPrimary; |
| 118 | } else if (!view->header()->isSectionHidden(logicalIndex: GlobalPrimary)) { |
| 119 | column = GlobalPrimary; |
| 120 | } else { |
| 121 | // do nothing. |
| 122 | } |
| 123 | index = index.sibling(arow: index.row(), acolumn: column); |
| 124 | view->selectionModel()->select(index, command: QItemSelectionModel::SelectCurrent); |
| 125 | } |
| 126 | |
| 127 | // Check if the models wants us to edit the item at index |
| 128 | if (!index.data(arole: ShowExtensionIndicatorRole).toBool()) { |
| 129 | return; |
| 130 | } |
| 131 | |
| 132 | if (!isExtended(index)) { |
| 133 | // we only want maximum ONE extender open at any time. |
| 134 | if (m_editingIndex.isValid()) { |
| 135 | KShortcutsEditorItem *oldItem = KShortcutsEditorPrivate::itemFromIndex(w: view, index: m_editingIndex); |
| 136 | Q_ASSERT(oldItem); // here we really expect nothing but a real KShortcutsEditorItem |
| 137 | |
| 138 | oldItem->setNameBold(false); |
| 139 | contractItem(index: m_editingIndex); |
| 140 | } |
| 141 | |
| 142 | m_editingIndex = index; |
| 143 | QWidget *viewport = static_cast<QAbstractItemView *>(parent())->viewport(); |
| 144 | |
| 145 | if (column >= LocalPrimary && column <= GlobalAlternate) { |
| 146 | ShortcutEditWidget *editor = new ShortcutEditWidget(viewport, |
| 147 | index.data(arole: DefaultShortcutRole).value<QKeySequence>(), |
| 148 | index.data(arole: ShortcutRole).value<QKeySequence>(), |
| 149 | m_allowLetterShortcuts); |
| 150 | if (column == GlobalPrimary) { |
| 151 | QObject *action = index.data(arole: ObjectRole).value<QObject *>(); |
| 152 | editor->setAction(action); |
| 153 | editor->setMultiKeyShortcutsAllowed(false); |
| 154 | QString componentName = action->property(name: "componentName" ).toString(); |
| 155 | if (componentName.isEmpty()) { |
| 156 | componentName = QCoreApplication::applicationName(); |
| 157 | } |
| 158 | editor->setComponentName(componentName); |
| 159 | } |
| 160 | |
| 161 | m_editor = editor; |
| 162 | // For global shortcuts check against the kde standard shortcuts |
| 163 | if (column == GlobalPrimary || column == GlobalAlternate) { |
| 164 | editor->setCheckForConflictsAgainst(KKeySequenceWidget::LocalShortcuts | KKeySequenceWidget::GlobalShortcuts |
| 165 | | KKeySequenceWidget::StandardShortcuts); |
| 166 | } |
| 167 | |
| 168 | editor->setCheckActionCollections(m_checkActionCollections); |
| 169 | |
| 170 | connect(sender: editor, signal: &ShortcutEditWidget::keySequenceChanged, context: this, slot: &KShortcutsEditorDelegate::keySequenceChanged); |
| 171 | connect(sender: editor, signal: &ShortcutEditWidget::stealShortcut, context: this, slot: &KShortcutsEditorDelegate::stealShortcut); |
| 172 | |
| 173 | } else if (column == RockerGesture) { |
| 174 | m_editor = new QLabel(QStringLiteral("A lame placeholder" ), viewport); |
| 175 | |
| 176 | } else if (column == ShapeGesture) { |
| 177 | m_editor = new QLabel(QStringLiteral("<i>A towel</i>" ), viewport); |
| 178 | |
| 179 | } else { |
| 180 | return; |
| 181 | } |
| 182 | |
| 183 | m_editor->installEventFilter(filterObj: this); |
| 184 | item->setNameBold(true); |
| 185 | extendItem(extender: m_editor, index); |
| 186 | |
| 187 | } else { |
| 188 | // the item is extended, and clicking on it again closes it |
| 189 | item->setNameBold(false); |
| 190 | contractItem(index); |
| 191 | view->selectionModel()->select(index, command: QItemSelectionModel::Clear); |
| 192 | m_editingIndex = QModelIndex(); |
| 193 | m_editor = nullptr; |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | // slot |
| 198 | void KShortcutsEditorDelegate::itemCollapsed(const QModelIndex &index) |
| 199 | { |
| 200 | if (!m_editingIndex.isValid()) { |
| 201 | return; |
| 202 | } |
| 203 | |
| 204 | const QAbstractItemModel *model = index.model(); |
| 205 | for (int row = 0; row < model->rowCount(parent: index); ++row) { |
| 206 | for (int col = 0; col < index.model()->columnCount(parent: index); ++col) { |
| 207 | QModelIndex colIndex = model->index(row, column: col, parent: index); |
| 208 | |
| 209 | if (colIndex == m_editingIndex) { |
| 210 | itemActivated(index: m_editingIndex); // this will *close* the item's editor because it's already open |
| 211 | } |
| 212 | } |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | // slot |
| 217 | void KShortcutsEditorDelegate::hiddenBySearchLine(QTreeWidgetItem *item, bool hidden) |
| 218 | { |
| 219 | if (!hidden || !item) { |
| 220 | return; |
| 221 | } |
| 222 | QTreeWidget *view = static_cast<QTreeWidget *>(parent()); |
| 223 | QTreeWidgetItem *editingItem = KShortcutsEditorPrivate::itemFromIndex(w: view, index: m_editingIndex); |
| 224 | if (editingItem == item) { |
| 225 | itemActivated(index: m_editingIndex); // this will *close* the item's editor because it's already open |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | bool KShortcutsEditorDelegate::eventFilter(QObject *o, QEvent *e) |
| 230 | { |
| 231 | if (o == m_editor) { |
| 232 | // Prevent clicks in the empty part of the editor widget from closing the editor |
| 233 | // because they would propagate to the itemview and be interpreted as a click in |
| 234 | // an item's rect. That in turn would lead to an itemActivated() call, closing |
| 235 | // the current editor. |
| 236 | |
| 237 | switch (e->type()) { |
| 238 | case QEvent::MouseButtonPress: |
| 239 | case QEvent::MouseButtonRelease: |
| 240 | case QEvent::MouseButtonDblClick: |
| 241 | return true; |
| 242 | default: |
| 243 | return false; |
| 244 | } |
| 245 | } else if (o == parent()) { |
| 246 | // Make left/right cursor keys switch items instead of operate the scroll bar |
| 247 | // (subclassing QtreeView/Widget would be cleaner but much more of a hassle) |
| 248 | // Note that in our case we know that the selection mode is SingleSelection, |
| 249 | // so we don't have to ask QAbstractItemView::selectionCommand() et al. |
| 250 | |
| 251 | if (e->type() != QEvent::KeyPress) { |
| 252 | return false; |
| 253 | } |
| 254 | QKeyEvent *ke = static_cast<QKeyEvent *>(e); |
| 255 | QTreeWidget *view = static_cast<QTreeWidget *>(parent()); |
| 256 | QItemSelectionModel *selection = view->selectionModel(); |
| 257 | QModelIndex index = selection->currentIndex(); |
| 258 | |
| 259 | switch (ke->key()) { |
| 260 | case Qt::Key_Space: |
| 261 | case Qt::Key_Select: |
| 262 | // we are not using the standard "open editor" mechanism of QAbstractItemView, |
| 263 | // so let's emulate that here. |
| 264 | itemActivated(index: index); |
| 265 | return true; |
| 266 | case Qt::Key_Left: |
| 267 | index = index.sibling(arow: index.row(), acolumn: index.column() - 1); |
| 268 | break; |
| 269 | case Qt::Key_Right: |
| 270 | index = index.sibling(arow: index.row(), acolumn: index.column() + 1); |
| 271 | break; |
| 272 | default: |
| 273 | return false; |
| 274 | } |
| 275 | // a cursor key was pressed |
| 276 | if (index.isValid()) { |
| 277 | selection->setCurrentIndex(index, command: QItemSelectionModel::ClearAndSelect); |
| 278 | // ### using PositionAtCenter for now; |
| 279 | // EnsureVisible has no effect which seems to be a bug. |
| 280 | view->scrollTo(index, hint: QAbstractItemView::PositionAtCenter); |
| 281 | } |
| 282 | return true; |
| 283 | } |
| 284 | return false; |
| 285 | } |
| 286 | |
| 287 | // slot |
| 288 | void KShortcutsEditorDelegate::keySequenceChanged(const QKeySequence &seq) |
| 289 | { |
| 290 | QVariant ret = QVariant::fromValue(value: seq); |
| 291 | Q_EMIT shortcutChanged(ret, m_editingIndex); |
| 292 | } |
| 293 | |
| 294 | void KShortcutsEditorDelegate::setCheckActionCollections(const QList<KActionCollection *> &checkActionCollections) |
| 295 | { |
| 296 | m_checkActionCollections = checkActionCollections; |
| 297 | } |
| 298 | |
| 299 | #if 0 |
| 300 | //slot |
| 301 | void KShortcutsEditorDelegate::shapeGestureChanged(const KShapeGesture &gest) |
| 302 | { |
| 303 | //this is somewhat verbose because the gesture types are not "built in" to QVariant |
| 304 | QVariant ret = QVariant::fromValue(gest); |
| 305 | Q_EMIT shortcutChanged(ret, m_editingIndex); |
| 306 | } |
| 307 | #endif |
| 308 | |
| 309 | #if 0 |
| 310 | //slot |
| 311 | void KShortcutsEditorDelegate::rockerGestureChanged(const KRockerGesture &gest) |
| 312 | { |
| 313 | QVariant ret = QVariant::fromValue(gest); |
| 314 | Q_EMIT shortcutChanged(ret, m_editingIndex); |
| 315 | } |
| 316 | #endif |
| 317 | |