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

source code of kconfigwidgets/src/kcommandbar.cpp