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
24KShortcutsEditorDelegate::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
62void 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
92QSize 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
100void 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
198void 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
217void 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
229bool 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
288void KShortcutsEditorDelegate::keySequenceChanged(const QKeySequence &seq)
289{
290 QVariant ret = QVariant::fromValue(value: seq);
291 Q_EMIT shortcutChanged(ret, m_editingIndex);
292}
293
294void KShortcutsEditorDelegate::setCheckActionCollections(const QList<KActionCollection *> &checkActionCollections)
295{
296 m_checkActionCollections = checkActionCollections;
297}
298
299#if 0
300//slot
301void 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
311void KShortcutsEditorDelegate::rockerGestureChanged(const KRockerGesture &gest)
312{
313 QVariant ret = QVariant::fromValue(gest);
314 Q_EMIT shortcutChanged(ret, m_editingIndex);
315}
316#endif
317

source code of kxmlgui/src/kshortcutseditordelegate.cpp