1 | /* |
2 | SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com> |
3 | |
4 | SPDX-License-Identifier: LGPL-2.0-or-later |
5 | */ |
6 | #include "kcommandbar.h" |
7 | #include "kcommandbarmodel_p.h" |
8 | #include "kconfigwidgets_debug.h" |
9 | |
10 | #include <QAction> |
11 | #include <QCoreApplication> |
12 | #include <QGraphicsOpacityEffect> |
13 | #include <QHeaderView> |
14 | #include <QKeyEvent> |
15 | #include <QLabel> |
16 | #include <QLineEdit> |
17 | #include <QMainWindow> |
18 | #include <QMenu> |
19 | #include <QPainter> |
20 | #include <QPointer> |
21 | #include <QScreen> |
22 | #include <QSortFilterProxyModel> |
23 | #include <QStatusBar> |
24 | #include <QStyledItemDelegate> |
25 | #include <QTextLayout> |
26 | #include <QToolBar> |
27 | #include <QTreeView> |
28 | #include <QVBoxLayout> |
29 | |
30 | #include <KConfigGroup> |
31 | #include <KFuzzyMatcher> |
32 | #include <KLocalizedString> |
33 | #include <KSharedConfig> |
34 | |
35 | static QRect getCommandBarBoundingRect(KCommandBar *commandBar) |
36 | { |
37 | QWidget *parentWidget = commandBar->parentWidget(); |
38 | Q_ASSERT(parentWidget); |
39 | |
40 | const QMainWindow *mainWindow = qobject_cast<const QMainWindow *>(object: parentWidget); |
41 | if (!mainWindow) { |
42 | return parentWidget->geometry(); |
43 | } |
44 | |
45 | QRect boundingRect = mainWindow->contentsRect(); |
46 | |
47 | // exclude the menu bar from the bounding rect |
48 | if (const QWidget * = mainWindow->menuWidget()) { |
49 | if (!menuWidget->isHidden()) { |
50 | boundingRect.setTop(boundingRect.top() + menuWidget->height()); |
51 | } |
52 | } |
53 | |
54 | // exclude the status bar from the bounding rect |
55 | if (const QStatusBar *statusBar = mainWindow->findChild<QStatusBar *>()) { |
56 | if (!statusBar->isHidden()) { |
57 | boundingRect.setBottom(boundingRect.bottom() - statusBar->height()); |
58 | } |
59 | } |
60 | |
61 | // exclude any undocked toolbar from the bounding rect |
62 | const QList<QToolBar *> toolBars = mainWindow->findChildren<QToolBar *>(); |
63 | for (QToolBar *toolBar : toolBars) { |
64 | if (toolBar->isHidden() || toolBar->isFloating()) { |
65 | continue; |
66 | } |
67 | |
68 | switch (mainWindow->toolBarArea(toolbar: toolBar)) { |
69 | case Qt::TopToolBarArea: |
70 | boundingRect.setTop(std::max(a: boundingRect.top(), b: toolBar->geometry().bottom())); |
71 | break; |
72 | case Qt::RightToolBarArea: |
73 | boundingRect.setRight(std::min(a: boundingRect.right(), b: toolBar->geometry().left())); |
74 | break; |
75 | case Qt::BottomToolBarArea: |
76 | boundingRect.setBottom(std::min(a: boundingRect.bottom(), b: toolBar->geometry().top())); |
77 | break; |
78 | case Qt::LeftToolBarArea: |
79 | boundingRect.setLeft(std::max(a: boundingRect.left(), b: toolBar->geometry().right())); |
80 | break; |
81 | default: |
82 | break; |
83 | } |
84 | } |
85 | |
86 | return boundingRect; |
87 | } |
88 | |
89 | // BEGIN CommandBarFilterModel |
90 | class CommandBarFilterModel final : public QSortFilterProxyModel |
91 | { |
92 | public: |
93 | CommandBarFilterModel(QObject *parent = nullptr) |
94 | : QSortFilterProxyModel(parent) |
95 | { |
96 | connect(sender: this, signal: &CommandBarFilterModel::modelAboutToBeReset, context: this, slot: [this]() { |
97 | m_hasActionsWithIcons = false; |
98 | }); |
99 | } |
100 | |
101 | bool hasActionsWithIcons() const |
102 | { |
103 | return m_hasActionsWithIcons; |
104 | } |
105 | |
106 | Q_SLOT void setFilterString(const QString &string) |
107 | { |
108 | // MUST reset the model here, we want to repopulate |
109 | // invalidateFilter() will not work here |
110 | beginResetModel(); |
111 | m_pattern = string; |
112 | endResetModel(); |
113 | } |
114 | |
115 | protected: |
116 | bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override |
117 | { |
118 | const int scoreLeft = sourceLeft.data(arole: KCommandBarModel::Score).toInt(); |
119 | const int scoreRight = sourceRight.data(arole: KCommandBarModel::Score).toInt(); |
120 | if (scoreLeft == scoreRight) { |
121 | const QString textLeft = sourceLeft.data().toString(); |
122 | const QString textRight = sourceRight.data().toString(); |
123 | |
124 | return textRight.localeAwareCompare(s: textLeft) < 0; |
125 | } |
126 | |
127 | return scoreLeft < scoreRight; |
128 | } |
129 | |
130 | bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override |
131 | { |
132 | const QModelIndex index = sourceModel()->index(row: sourceRow, column: 0, parent: sourceParent); |
133 | |
134 | bool accept = false; |
135 | if (m_pattern.isEmpty()) { |
136 | accept = true; |
137 | } else { |
138 | const QString row = index.data(arole: Qt::DisplayRole).toString(); |
139 | KFuzzyMatcher::Result resAction = KFuzzyMatcher::match(pattern: m_pattern, str: row); |
140 | sourceModel()->setData(index, value: resAction.score, role: KCommandBarModel::Score); |
141 | accept = resAction.matched; |
142 | } |
143 | |
144 | if (accept && !m_hasActionsWithIcons) { |
145 | m_hasActionsWithIcons |= !index.data(arole: Qt::DecorationRole).isNull(); |
146 | } |
147 | |
148 | return accept; |
149 | } |
150 | |
151 | private: |
152 | QString m_pattern; |
153 | mutable bool m_hasActionsWithIcons = false; |
154 | }; |
155 | // END CommandBarFilterModel |
156 | |
157 | class CommandBarStyleDelegate final : public QStyledItemDelegate |
158 | { |
159 | public: |
160 | CommandBarStyleDelegate(QObject *parent = nullptr) |
161 | : QStyledItemDelegate(parent) |
162 | { |
163 | } |
164 | |
165 | /** |
166 | * Paints a single item's text |
167 | */ |
168 | static void |
169 | paintItemText(QPainter *p, const QString &textt, const QRect &rect, const QStyleOptionViewItem &options, QList<QTextLayout::FormatRange> formats) |
170 | { |
171 | QString text = options.fontMetrics.elidedText(text: textt, mode: Qt::ElideRight, width: rect.width()); |
172 | |
173 | // set formats and font |
174 | QTextLayout textLayout(text, options.font); |
175 | formats.append(other: textLayout.formats()); |
176 | textLayout.setFormats(formats); |
177 | |
178 | // set alignment, rtls etc |
179 | QTextOption textOption; |
180 | textOption.setTextDirection(options.direction); |
181 | textOption.setAlignment(QStyle::visualAlignment(direction: options.direction, alignment: options.displayAlignment)); |
182 | textLayout.setTextOption(textOption); |
183 | |
184 | // layout the text |
185 | textLayout.beginLayout(); |
186 | |
187 | QTextLine line = textLayout.createLine(); |
188 | if (!line.isValid()) { |
189 | return; |
190 | } |
191 | |
192 | const int lineWidth = rect.width(); |
193 | line.setLineWidth(lineWidth); |
194 | line.setPosition(QPointF(0, 0)); |
195 | |
196 | textLayout.endLayout(); |
197 | |
198 | /** |
199 | * get "Y" so that we can properly V-Center align the text in row |
200 | */ |
201 | const int y = QStyle::alignedRect(direction: Qt::LeftToRight, alignment: Qt::AlignVCenter, size: textLayout.boundingRect().size().toSize(), rectangle: rect).y(); |
202 | |
203 | // draw the text |
204 | const QPointF pos(rect.x(), y); |
205 | textLayout.draw(p, pos); |
206 | } |
207 | |
208 | void paint(QPainter *painter, const QStyleOptionViewItem &opt, const QModelIndex &index) const override |
209 | { |
210 | painter->save(); |
211 | |
212 | /** |
213 | * Draw everything, (widget, icon etc) except the text |
214 | */ |
215 | QStyleOptionViewItem option = opt; |
216 | initStyleOption(option: &option, index); |
217 | option.text.clear(); // clear old text |
218 | QStyle *style = option.widget->style(); |
219 | style->drawControl(element: QStyle::CE_ItemViewItem, opt: &option, p: painter, w: option.widget); |
220 | |
221 | const int hMargin = style->pixelMetric(metric: QStyle::PM_FocusFrameHMargin, option: &option, widget: option.widget); |
222 | |
223 | QRect textRect = option.rect; |
224 | |
225 | const CommandBarFilterModel *model = static_cast<const CommandBarFilterModel *>(index.model()); |
226 | if (model->hasActionsWithIcons()) { |
227 | const int iconWidth = option.decorationSize.width() + (hMargin * 2); |
228 | if (option.direction == Qt::RightToLeft) { |
229 | textRect.adjust(dx1: 0, dy1: 0, dx2: -iconWidth, dy2: 0); |
230 | } else { |
231 | textRect.adjust(dx1: iconWidth, dy1: 0, dx2: 0, dy2: 0); |
232 | } |
233 | } |
234 | |
235 | const QString original = index.data().toString(); |
236 | QStringView str = original; |
237 | int componentIdx = original.indexOf(c: QLatin1Char(':')); |
238 | int actionNameStart = 0; |
239 | if (componentIdx > 0) { |
240 | actionNameStart = componentIdx + 2; |
241 | // + 2 because there is a space after colon |
242 | str = str.mid(pos: actionNameStart); |
243 | } |
244 | |
245 | QList<QTextLayout::FormatRange> formats; |
246 | if (componentIdx > 0) { |
247 | QTextCharFormat gray; |
248 | gray.setForeground(option.palette.placeholderText()); |
249 | formats.append(t: {.start: 0, .length: componentIdx, .format: gray}); |
250 | } |
251 | |
252 | QTextCharFormat fmt; |
253 | fmt.setForeground(option.palette.link()); |
254 | fmt.setFontWeight(QFont::Bold); |
255 | |
256 | /** |
257 | * Highlight matches from fuzzy matcher |
258 | */ |
259 | const auto fmtRanges = KFuzzyMatcher::matchedRanges(pattern: m_filterString, str); |
260 | QTextCharFormat f; |
261 | f.setForeground(option.palette.link()); |
262 | formats.reserve(asize: formats.size() + fmtRanges.size()); |
263 | std::transform(first: fmtRanges.begin(), last: fmtRanges.end(), result: std::back_inserter(x&: formats), unary_op: [f, actionNameStart](const KFuzzyMatcher::Range &fr) { |
264 | return QTextLayout::FormatRange{.start: fr.start + actionNameStart, .length: fr.length, .format: f}; |
265 | }); |
266 | |
267 | textRect.adjust(dx1: hMargin, dy1: 0, dx2: -hMargin, dy2: 0); |
268 | paintItemText(p: painter, textt: original, rect: textRect, options: option, formats: std::move(formats)); |
269 | |
270 | painter->restore(); |
271 | } |
272 | |
273 | public Q_SLOTS: |
274 | void setFilterString(const QString &text) |
275 | { |
276 | m_filterString = text; |
277 | } |
278 | |
279 | private: |
280 | QString m_filterString; |
281 | }; |
282 | |
283 | class ShortcutStyleDelegate final : public QStyledItemDelegate |
284 | { |
285 | public: |
286 | ShortcutStyleDelegate(QObject *parent = nullptr) |
287 | : QStyledItemDelegate(parent) |
288 | { |
289 | } |
290 | |
291 | void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override |
292 | { |
293 | // draw background |
294 | option.widget->style()->drawPrimitive(pe: QStyle::PE_PanelItemViewItem, opt: &option, p: painter); |
295 | |
296 | const QString shortcutString = index.data().toString(); |
297 | if (shortcutString.isEmpty()) { |
298 | return; |
299 | } |
300 | |
301 | const ShortcutSegments shortcutSegments = splitShortcut(shortcut: shortcutString); |
302 | if (shortcutSegments.isEmpty()) { |
303 | return; |
304 | } |
305 | |
306 | struct Button { |
307 | int textWidth; |
308 | QString text; |
309 | }; |
310 | |
311 | // compute the width of each shortcut segment |
312 | QList<Button> btns; |
313 | btns.reserve(asize: shortcutSegments.count()); |
314 | const int hMargin = horizontalMargin(option); |
315 | for (const QString &text : shortcutSegments) { |
316 | int textWidth = option.fontMetrics.horizontalAdvance(text); |
317 | textWidth += 2 * hMargin; |
318 | btns.append(t: {.textWidth: textWidth, .text: text}); |
319 | } |
320 | |
321 | int textHeight = option.fontMetrics.lineSpacing(); |
322 | // this happens on gnome so we manually decrease the height a bit |
323 | if (textHeight == option.rect.height()) { |
324 | textHeight -= 4; |
325 | } |
326 | |
327 | const int y = option.rect.y() + (option.rect.height() - textHeight) / 2; |
328 | int x; |
329 | if (option.direction == Qt::RightToLeft) { |
330 | x = option.rect.x() + hMargin; |
331 | } else { |
332 | x = option.rect.right() - shortcutDrawingWidth(option, shortcutSegments, hMargin) - hMargin; |
333 | } |
334 | |
335 | painter->save(); |
336 | painter->setPen(option.palette.buttonText().color()); |
337 | painter->setRenderHint(hint: QPainter::Antialiasing); |
338 | for (int i = 0, n = btns.count(); i < n; ++i) { |
339 | const Button &button = btns.at(i); |
340 | |
341 | QRect outputRect(x, y, button.textWidth, textHeight); |
342 | |
343 | // an even element indicates that it is a key |
344 | if (i % 2 == 0) { |
345 | painter->save(); |
346 | painter->setPen(Qt::NoPen); |
347 | |
348 | // draw rounded rect shadow |
349 | auto shadowRect = outputRect.translated(dx: 0, dy: 1); |
350 | painter->setBrush(option.palette.shadow()); |
351 | painter->drawRoundedRect(rect: shadowRect, xRadius: 3.0, yRadius: 3.0); |
352 | |
353 | // draw rounded rect itself |
354 | painter->setBrush(option.palette.window()); |
355 | painter->drawRoundedRect(rect: outputRect, xRadius: 3.0, yRadius: 3.0); |
356 | |
357 | painter->restore(); |
358 | } |
359 | |
360 | // draw shortcut segment |
361 | painter->drawText(r: outputRect, flags: Qt::AlignCenter, text: button.text); |
362 | |
363 | x += outputRect.width(); |
364 | } |
365 | |
366 | painter->restore(); |
367 | } |
368 | |
369 | QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override |
370 | { |
371 | if (index.isValid() && index.column() == KCommandBarModel::Column_Shortcut) { |
372 | const QString shortcut = index.data().toString(); |
373 | if (!shortcut.isEmpty()) { |
374 | const ShortcutSegments shortcutSegments = splitShortcut(shortcut); |
375 | if (!shortcutSegments.isEmpty()) { |
376 | const int hMargin = horizontalMargin(option); |
377 | int width = shortcutDrawingWidth(option, shortcutSegments, hMargin); |
378 | |
379 | // add left and right margins |
380 | width += 2 * hMargin; |
381 | |
382 | return QSize(width, 0); |
383 | } |
384 | } |
385 | } |
386 | |
387 | return QStyledItemDelegate::sizeHint(option, index); |
388 | } |
389 | |
390 | private: |
391 | using ShortcutSegments = QStringList; |
392 | |
393 | // split shortcut into segments i.e. will return |
394 | // ["Ctrl", "+", "A", ", ", "Ctrl", "+", "K"] for "Ctrl+A, Ctrl+K" |
395 | // twice as fast as using regular expressions |
396 | static ShortcutSegments splitShortcut(const QString &shortcut) |
397 | { |
398 | ShortcutSegments segments; |
399 | if (!shortcut.isEmpty()) { |
400 | const int shortcutLength = shortcut.length(); |
401 | int start = 0; |
402 | for (int i = 0; i < shortcutLength; ++i) { |
403 | const QChar c = shortcut.at(i); |
404 | if (c == QLatin1Char('+')) { |
405 | if (i > start) { |
406 | segments << shortcut.mid(position: start, n: i - start); |
407 | } |
408 | segments << shortcut.at(i); |
409 | start = i + 1; |
410 | } else if (c == QLatin1Char(',')) { |
411 | if (i > start) { |
412 | segments << shortcut.mid(position: start, n: i - start); |
413 | start = i; |
414 | } |
415 | const int j = i + 1; |
416 | if (j < shortcutLength && shortcut.at(i: j) == QLatin1Char(' ')) { |
417 | segments << shortcut.mid(position: start, n: j - start + 1); |
418 | i = j; |
419 | } else { |
420 | segments << shortcut.at(i); |
421 | } |
422 | start = i + 1; |
423 | } |
424 | } |
425 | if (start < shortcutLength) { |
426 | segments << shortcut.mid(position: start); |
427 | } |
428 | |
429 | // check we have successfully parsed the shortcut |
430 | if (segments.isEmpty()) { |
431 | qCWarning(KCONFIG_WIDGETS_LOG) << "Splitting shortcut failed" << shortcut; |
432 | } |
433 | } |
434 | |
435 | return segments; |
436 | } |
437 | |
438 | // returns the width needed to draw the shortcut |
439 | static int shortcutDrawingWidth(const QStyleOptionViewItem &option, const ShortcutSegments &shortcutSegments, int hMargin) |
440 | { |
441 | int width = 0; |
442 | if (!shortcutSegments.isEmpty()) { |
443 | width = option.fontMetrics.horizontalAdvance(shortcutSegments.join(sep: QString())); |
444 | |
445 | // add left and right margins for each segment |
446 | width += shortcutSegments.count() * 2 * hMargin; |
447 | } |
448 | |
449 | return width; |
450 | } |
451 | |
452 | int horizontalMargin(const QStyleOptionViewItem &option) const |
453 | { |
454 | return option.widget->style()->pixelMetric(metric: QStyle::PM_FocusFrameHMargin, option: &option) + 2; |
455 | } |
456 | }; |
457 | |
458 | // BEGIN KCommandBarPrivate |
459 | class KCommandBarPrivate |
460 | { |
461 | public: |
462 | QTreeView m_treeView; |
463 | QLineEdit m_lineEdit; |
464 | KCommandBarModel m_model; |
465 | CommandBarFilterModel m_proxyModel; |
466 | |
467 | /** |
468 | * selects first item in treeview |
469 | */ |
470 | void reselectFirst() |
471 | { |
472 | const QModelIndex index = m_proxyModel.index(row: 0, column: 0); |
473 | m_treeView.setCurrentIndex(index); |
474 | } |
475 | |
476 | /** |
477 | * blocks signals before clearing line edit to ensure |
478 | * we don't trigger filtering / sorting |
479 | */ |
480 | void clearLineEdit() |
481 | { |
482 | const QSignalBlocker blocker(m_lineEdit); |
483 | m_lineEdit.clear(); |
484 | } |
485 | |
486 | void slotReturnPressed(KCommandBar *q); |
487 | |
488 | void setLastUsedActions(); |
489 | |
490 | QStringList lastUsedActions() const; |
491 | }; |
492 | |
493 | void KCommandBarPrivate::slotReturnPressed(KCommandBar *q) |
494 | { |
495 | auto act = m_proxyModel.data(index: m_treeView.currentIndex(), role: Qt::UserRole).value<QAction *>(); |
496 | if (act) { |
497 | // if the action is a menu, we take all its actions |
498 | // and reload our dialog with these instead. |
499 | if (auto = act->menu()) { |
500 | auto = menu->actions(); |
501 | KCommandBar::ActionGroup ag; |
502 | |
503 | // if there are no actions, trigger load actions |
504 | // this happens with some menus that are loaded on demand |
505 | if (menuActions.size() == 0) { |
506 | Q_EMIT menu->aboutToShow(); |
507 | ag.actions = menu->actions(); |
508 | } |
509 | |
510 | QString groupName = KLocalizedString::removeAcceleratorMarker(label: act->text()); |
511 | ag.name = groupName; |
512 | |
513 | m_model.refresh(actionGroups: {ag}); |
514 | reselectFirst(); |
515 | /** |
516 | * We want the "textChanged" signal here |
517 | * so that proxy model triggers filtering again |
518 | * so don't use d->clearLineEdit() |
519 | */ |
520 | m_lineEdit.clear(); |
521 | return; |
522 | } else { |
523 | m_model.actionTriggered(name: act->text()); |
524 | q->hide(); |
525 | act->trigger(); |
526 | } |
527 | } |
528 | |
529 | clearLineEdit(); |
530 | q->hide(); |
531 | q->deleteLater(); |
532 | } |
533 | |
534 | void KCommandBarPrivate::setLastUsedActions() |
535 | { |
536 | auto cfg = KSharedConfig::openStateConfig(); |
537 | KConfigGroup cg(cfg, QStringLiteral("General" )); |
538 | |
539 | QStringList actionNames = cg.readEntry(QStringLiteral("CommandBarLastUsedActions" ), aDefault: QStringList()); |
540 | |
541 | return m_model.setLastUsedActions(actionNames); |
542 | } |
543 | |
544 | QStringList KCommandBarPrivate::lastUsedActions() const |
545 | { |
546 | return m_model.lastUsedActions(); |
547 | } |
548 | // END KCommandBarPrivate |
549 | |
550 | // BEGIN KCommandBar |
551 | KCommandBar::KCommandBar(QWidget *parent) |
552 | : QFrame(parent) |
553 | , d(new KCommandBarPrivate) |
554 | { |
555 | QGraphicsDropShadowEffect *e = new QGraphicsDropShadowEffect(this); |
556 | e->setColor(palette().color(cr: QPalette::Shadow)); |
557 | e->setOffset(2.); |
558 | e->setBlurRadius(8.); |
559 | setGraphicsEffect(e); |
560 | |
561 | setAutoFillBackground(true); |
562 | setFrameShadow(QFrame::Raised); |
563 | setFrameShape(QFrame::Box); |
564 | |
565 | QVBoxLayout *layout = new QVBoxLayout(); |
566 | layout->setSpacing(0); |
567 | layout->setContentsMargins(left: 2, top: 2, right: 2, bottom: 2); |
568 | setLayout(layout); |
569 | |
570 | setFocusProxy(&d->m_lineEdit); |
571 | |
572 | layout->addWidget(&d->m_lineEdit); |
573 | |
574 | layout->addWidget(&d->m_treeView); |
575 | d->m_treeView.setTextElideMode(Qt::ElideLeft); |
576 | d->m_treeView.setUniformRowHeights(true); |
577 | |
578 | CommandBarStyleDelegate *delegate = new CommandBarStyleDelegate(this); |
579 | ShortcutStyleDelegate *del = new ShortcutStyleDelegate(this); |
580 | d->m_treeView.setItemDelegateForColumn(column: KCommandBarModel::Column_Command, delegate); |
581 | d->m_treeView.setItemDelegateForColumn(column: KCommandBarModel::Column_Shortcut, delegate: del); |
582 | |
583 | connect(sender: &d->m_lineEdit, signal: &QLineEdit::returnPressed, context: this, slot: [this]() { |
584 | d->slotReturnPressed(q: this); |
585 | }); |
586 | connect(sender: &d->m_lineEdit, signal: &QLineEdit::textChanged, context: &d->m_proxyModel, slot: &CommandBarFilterModel::setFilterString); |
587 | connect(sender: &d->m_lineEdit, signal: &QLineEdit::textChanged, context: delegate, slot: &CommandBarStyleDelegate::setFilterString); |
588 | connect(sender: &d->m_lineEdit, signal: &QLineEdit::textChanged, context: this, slot: [this]() { |
589 | d->m_treeView.viewport()->update(); |
590 | d->reselectFirst(); |
591 | }); |
592 | connect(sender: &d->m_treeView, signal: &QTreeView::clicked, context: this, slot: [this]() { |
593 | d->slotReturnPressed(q: this); |
594 | }); |
595 | |
596 | d->m_proxyModel.setSourceModel(&d->m_model); |
597 | d->m_treeView.setSortingEnabled(true); |
598 | d->m_treeView.setModel(&d->m_proxyModel); |
599 | |
600 | d->m_treeView.header()->setMinimumSectionSize(0); |
601 | d->m_treeView.header()->setStretchLastSection(false); |
602 | d->m_treeView.header()->setSectionResizeMode(logicalIndex: KCommandBarModel::Column_Command, mode: QHeaderView::Stretch); |
603 | d->m_treeView.header()->setSectionResizeMode(logicalIndex: KCommandBarModel::Column_Shortcut, mode: QHeaderView::ResizeToContents); |
604 | |
605 | parent->installEventFilter(filterObj: this); |
606 | d->m_treeView.installEventFilter(filterObj: this); |
607 | d->m_lineEdit.installEventFilter(filterObj: this); |
608 | |
609 | d->m_treeView.setHeaderHidden(true); |
610 | d->m_treeView.setRootIsDecorated(false); |
611 | d->m_treeView.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); |
612 | d->m_treeView.setSelectionMode(QTreeView::SingleSelection); |
613 | |
614 | QLabel *placeholderLabel = new QLabel; |
615 | placeholderLabel->setAlignment(Qt::AlignCenter); |
616 | placeholderLabel->setTextInteractionFlags(Qt::NoTextInteraction); |
617 | placeholderLabel->setWordWrap(true); |
618 | placeholderLabel->setText(i18n("No commands matching the filter" )); |
619 | // To match the size of a level 2 Heading/KTitleWidget |
620 | QFont placeholderLabelFont = placeholderLabel->font(); |
621 | placeholderLabelFont.setPointSize(qRound(d: placeholderLabelFont.pointSize() * 1.3)); |
622 | placeholderLabel->setFont(placeholderLabelFont); |
623 | // Match opacity of QML placeholder label component |
624 | QGraphicsOpacityEffect *opacityEffect = new QGraphicsOpacityEffect(placeholderLabel); |
625 | opacityEffect->setOpacity(0.5); |
626 | placeholderLabel->setGraphicsEffect(opacityEffect); |
627 | |
628 | QHBoxLayout *placeholderLayout = new QHBoxLayout; |
629 | placeholderLayout->addWidget(placeholderLabel); |
630 | d->m_treeView.setLayout(placeholderLayout); |
631 | |
632 | connect(sender: &d->m_proxyModel, signal: &CommandBarFilterModel::modelReset, context: this, slot: [this, placeholderLabel]() { |
633 | placeholderLabel->setHidden(d->m_proxyModel.rowCount() > 0); |
634 | }); |
635 | |
636 | setHidden(true); |
637 | |
638 | // Migrate last used action config to new location |
639 | KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("General" )); |
640 | if (cg.hasKey(key: "CommandBarLastUsedActions" )) { |
641 | const QStringList actionNames = cg.readEntry(key: "CommandBarLastUsedActions" , aDefault: QStringList()); |
642 | |
643 | KConfigGroup stateCg(KSharedConfig::openStateConfig(), QStringLiteral("General" )); |
644 | stateCg.writeEntry(QStringLiteral("CommandBarLastUsedActions" ), value: actionNames); |
645 | |
646 | cg.deleteEntry(QStringLiteral("CommandBarLastUsedActions" )); |
647 | } |
648 | } |
649 | |
650 | /** |
651 | * Destructor defined here to make unique_ptr work |
652 | */ |
653 | KCommandBar::~KCommandBar() |
654 | { |
655 | auto lastUsedActions = d->lastUsedActions(); |
656 | auto cfg = KSharedConfig::openStateConfig(); |
657 | KConfigGroup cg(cfg, QStringLiteral("General" )); |
658 | cg.writeEntry(key: "CommandBarLastUsedActions" , value: lastUsedActions); |
659 | |
660 | // Explicitly remove installed event filters of children of d-pointer |
661 | // class, otherwise while KCommandBar is being torn down, an event could |
662 | // fire and the eventFilter() accesses d, which would cause a crash |
663 | // bug 452527 |
664 | d->m_treeView.removeEventFilter(obj: this); |
665 | d->m_lineEdit.removeEventFilter(obj: this); |
666 | } |
667 | |
668 | void KCommandBar::setActions(const QList<ActionGroup> &actions) |
669 | { |
670 | // First set last used actions in the model |
671 | d->setLastUsedActions(); |
672 | |
673 | d->m_model.refresh(actionGroups: actions); |
674 | d->reselectFirst(); |
675 | |
676 | show(); |
677 | setFocus(); |
678 | } |
679 | |
680 | void KCommandBar::show() |
681 | { |
682 | const QRect boundingRect = getCommandBarBoundingRect(commandBar: this); |
683 | |
684 | static constexpr int minWidth = 500; |
685 | const int maxWidth = boundingRect.width(); |
686 | const int preferredWidth = maxWidth / 2.4; |
687 | |
688 | static constexpr int minHeight = 250; |
689 | const int maxHeight = boundingRect.height(); |
690 | const int preferredHeight = maxHeight / 2; |
691 | |
692 | const QSize size{std::min(a: maxWidth, b: std::max(a: preferredWidth, b: minWidth)), std::min(a: maxHeight, b: std::max(a: preferredHeight, b: minHeight))}; |
693 | |
694 | setFixedSize(size); |
695 | |
696 | // set the position to the top-center of the parent |
697 | // just below the menubar/toolbar (if any) |
698 | const QPoint position{boundingRect.center().x() - size.width() / 2, boundingRect.y()}; |
699 | move(position); |
700 | |
701 | QWidget::show(); |
702 | } |
703 | |
704 | bool KCommandBar::eventFilter(QObject *obj, QEvent *event) |
705 | { |
706 | if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) { |
707 | QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); |
708 | if (obj == &d->m_lineEdit) { |
709 | const int key = keyEvent->key(); |
710 | const bool forward2list = (key == Qt::Key_Up) || (key == Qt::Key_Down) || (key == Qt::Key_PageUp) || (key == Qt::Key_PageDown); |
711 | if (forward2list) { |
712 | QCoreApplication::sendEvent(receiver: &d->m_treeView, event); |
713 | return true; |
714 | } |
715 | } else if (obj == &d->m_treeView) { |
716 | const int key = keyEvent->key(); |
717 | const bool forward2input = (key != Qt::Key_Up) && (key != Qt::Key_Down) && (key != Qt::Key_PageUp) && (key != Qt::Key_PageDown) |
718 | && (key != Qt::Key_Tab) && (key != Qt::Key_Backtab); |
719 | if (forward2input) { |
720 | QCoreApplication::sendEvent(receiver: &d->m_lineEdit, event); |
721 | return true; |
722 | } |
723 | } |
724 | |
725 | if (keyEvent->key() == Qt::Key_Escape) { |
726 | hide(); |
727 | deleteLater(); |
728 | return true; |
729 | } |
730 | } |
731 | |
732 | // hide on focus out, if neither input field nor list have focus! |
733 | else if (event->type() == QEvent::FocusOut && isVisible() && !(d->m_lineEdit.hasFocus() || d->m_treeView.hasFocus())) { |
734 | d->clearLineEdit(); |
735 | deleteLater(); |
736 | hide(); |
737 | return true; |
738 | } |
739 | |
740 | // handle resizing |
741 | if (parent() == obj && event->type() == QEvent::Resize) { |
742 | show(); |
743 | } |
744 | |
745 | return QWidget::eventFilter(watched: obj, event); |
746 | } |
747 | // END KCommandBar |
748 | |
749 | #include "moc_kcommandbar.cpp" |
750 | |