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 | |