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 | Q_OBJECT |
93 | public: |
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 | |
116 | protected: |
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 | |
152 | private: |
153 | QString m_pattern; |
154 | mutable bool m_hasActionsWithIcons = false; |
155 | }; |
156 | // END CommandBarFilterModel |
157 | |
158 | static 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 | |
171 | class CommandBarStyleDelegate final : public QStyledItemDelegate |
172 | { |
173 | Q_OBJECT |
174 | public: |
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 | |
289 | public Q_SLOTS: |
290 | void setFilterString(const QString &text) |
291 | { |
292 | m_filterString = text; |
293 | } |
294 | |
295 | private: |
296 | QString m_filterString; |
297 | }; |
298 | |
299 | class ShortcutStyleDelegate final : public QStyledItemDelegate |
300 | { |
301 | Q_OBJECT |
302 | public: |
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 | |
409 | private: |
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 |
478 | class KCommandBarPrivate |
479 | { |
480 | public: |
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 | |
512 | void 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 = act->menu()) { |
519 | auto = 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 | |
553 | void 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 | |
563 | QStringList KCommandBarPrivate::lastUsedActions() const |
564 | { |
565 | return m_model.lastUsedActions(); |
566 | } |
567 | // END KCommandBarPrivate |
568 | |
569 | // BEGIN KCommandBar |
570 | KCommandBar::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 | */ |
683 | KCommandBar::~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 | |
698 | void 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 | |
710 | void 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 | |
734 | bool 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 | |