/* SPDX-FileCopyrightText: 2021 Waqar Ahmed SPDX-License-Identifier: LGPL-2.0-or-later */ #include "kcommandbar.h" #include "kcommandbarmodel_p.h" #include "kconfigwidgets_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static QRect getCommandBarBoundingRect(KCommandBar *commandBar) { QWidget *parentWidget = commandBar->parentWidget(); Q_ASSERT(parentWidget); const QMainWindow *mainWindow = qobject_cast(parentWidget); if (!mainWindow) { return parentWidget->geometry(); } QRect boundingRect = mainWindow->contentsRect(); // exclude the menu bar from the bounding rect if (const QWidget *menuWidget = mainWindow->menuWidget()) { if (!menuWidget->isHidden()) { boundingRect.setTop(boundingRect.top() + menuWidget->height()); } } // exclude the status bar from the bounding rect if (const QStatusBar *statusBar = mainWindow->findChild()) { if (!statusBar->isHidden()) { boundingRect.setBottom(boundingRect.bottom() - statusBar->height()); } } // exclude any undocked toolbar from the bounding rect const QList toolBars = mainWindow->findChildren(); for (QToolBar *toolBar : toolBars) { if (toolBar->isHidden() || toolBar->isFloating()) { continue; } switch (mainWindow->toolBarArea(toolBar)) { case Qt::TopToolBarArea: boundingRect.setTop(std::max(boundingRect.top(), toolBar->geometry().bottom())); break; case Qt::RightToolBarArea: boundingRect.setRight(std::min(boundingRect.right(), toolBar->geometry().left())); break; case Qt::BottomToolBarArea: boundingRect.setBottom(std::min(boundingRect.bottom(), toolBar->geometry().top())); break; case Qt::LeftToolBarArea: boundingRect.setLeft(std::max(boundingRect.left(), toolBar->geometry().right())); break; default: break; } } return boundingRect; } // BEGIN CommandBarFilterModel class CommandBarFilterModel final : public QSortFilterProxyModel { public: CommandBarFilterModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) { connect(this, &CommandBarFilterModel::modelAboutToBeReset, this, [this]() { m_hasActionsWithIcons = false; }); } bool hasActionsWithIcons() const { return m_hasActionsWithIcons; } Q_SLOT void setFilterString(const QString &string) { // MUST reset the model here, we want to repopulate // invalidateFilter() will not work here beginResetModel(); m_pattern = string; endResetModel(); } protected: bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override { const int scoreLeft = sourceLeft.data(KCommandBarModel::Score).toInt(); const int scoreRight = sourceRight.data(KCommandBarModel::Score).toInt(); if (scoreLeft == scoreRight) { const QString textLeft = sourceLeft.data().toString(); const QString textRight = sourceRight.data().toString(); return textRight.localeAwareCompare(textLeft) < 0; } return scoreLeft < scoreRight; } bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override { const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); bool accept = false; if (m_pattern.isEmpty()) { accept = true; } else { const QString row = index.data(Qt::DisplayRole).toString(); KFuzzyMatcher::Result resAction = KFuzzyMatcher::match(m_pattern, row); sourceModel()->setData(index, resAction.score, KCommandBarModel::Score); accept = resAction.matched; } if (accept && !m_hasActionsWithIcons) { m_hasActionsWithIcons |= !index.data(Qt::DecorationRole).isNull(); } return accept; } private: QString m_pattern; mutable bool m_hasActionsWithIcons = false; }; // END CommandBarFilterModel class CommandBarStyleDelegate final : public QStyledItemDelegate { public: CommandBarStyleDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) { } /** * Paints a single item's text */ static void paintItemText(QPainter *p, const QString &textt, const QRect &rect, const QStyleOptionViewItem &options, QList formats) { QString text = options.fontMetrics.elidedText(textt, Qt::ElideRight, rect.width()); // set formats and font QTextLayout textLayout(text, options.font); formats.append(textLayout.formats()); textLayout.setFormats(formats); // set alignment, rtls etc QTextOption textOption; textOption.setTextDirection(options.direction); textOption.setAlignment(QStyle::visualAlignment(options.direction, options.displayAlignment)); textLayout.setTextOption(textOption); // layout the text textLayout.beginLayout(); QTextLine line = textLayout.createLine(); if (!line.isValid()) { return; } const int lineWidth = rect.width(); line.setLineWidth(lineWidth); line.setPosition(QPointF(0, 0)); textLayout.endLayout(); /** * get "Y" so that we can properly V-Center align the text in row */ const int y = QStyle::alignedRect(Qt::LeftToRight, Qt::AlignVCenter, textLayout.boundingRect().size().toSize(), rect).y(); // draw the text const QPointF pos(rect.x(), y); textLayout.draw(p, pos); } void paint(QPainter *painter, const QStyleOptionViewItem &opt, const QModelIndex &index) const override { painter->save(); /** * Draw everything, (widget, icon etc) except the text */ QStyleOptionViewItem option = opt; initStyleOption(&option, index); option.text.clear(); // clear old text QStyle *style = option.widget->style(); style->drawControl(QStyle::CE_ItemViewItem, &option, painter, option.widget); const int hMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, &option, option.widget); QRect textRect = option.rect; const CommandBarFilterModel *model = static_cast(index.model()); if (model->hasActionsWithIcons()) { const int iconWidth = option.decorationSize.width() + (hMargin * 2); if (option.direction == Qt::RightToLeft) { textRect.adjust(0, 0, -iconWidth, 0); } else { textRect.adjust(iconWidth, 0, 0, 0); } } const QString original = index.data().toString(); QStringView str = original; int componentIdx = original.indexOf(QLatin1Char(':')); int actionNameStart = 0; if (componentIdx > 0) { actionNameStart = componentIdx + 2; // + 2 because there is a space after colon str = str.mid(actionNameStart); } QList formats; if (componentIdx > 0) { QTextCharFormat gray; gray.setForeground(option.palette.placeholderText()); formats.append({0, componentIdx, gray}); } QTextCharFormat fmt; fmt.setForeground(option.palette.link()); fmt.setFontWeight(QFont::Bold); /** * Highlight matches from fuzzy matcher */ const auto fmtRanges = KFuzzyMatcher::matchedRanges(m_filterString, str); QTextCharFormat f; f.setForeground(option.palette.link()); formats.reserve(formats.size() + fmtRanges.size()); std::transform(fmtRanges.begin(), fmtRanges.end(), std::back_inserter(formats), [f, actionNameStart](const KFuzzyMatcher::Range &fr) { return QTextLayout::FormatRange{fr.start + actionNameStart, fr.length, f}; }); textRect.adjust(hMargin, 0, -hMargin, 0); paintItemText(painter, original, textRect, option, std::move(formats)); painter->restore(); } public Q_SLOTS: void setFilterString(const QString &text) { m_filterString = text; } private: QString m_filterString; }; class ShortcutStyleDelegate final : public QStyledItemDelegate { public: ShortcutStyleDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) { } void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { // draw background option.widget->style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter); const QString shortcutString = index.data().toString(); if (shortcutString.isEmpty()) { return; } const ShortcutSegments shortcutSegments = splitShortcut(shortcutString); if (shortcutSegments.isEmpty()) { return; } struct Button { int textWidth; QString text; }; // compute the width of each shortcut segment QList