cf12defd28
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
761 lines
25 KiB
C++
761 lines
25 KiB
C++
/*
|
|
SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>
|
|
|
|
SPDX-License-Identifier: LGPL-2.0-or-later
|
|
*/
|
|
#include "kcommandbar.h"
|
|
#include "kcommandbarmodel_p.h"
|
|
#include "kconfigwidgets_debug.h"
|
|
|
|
#include <QAction>
|
|
#include <QCoreApplication>
|
|
#include <QGraphicsOpacityEffect>
|
|
#include <QHeaderView>
|
|
#include <QKeyEvent>
|
|
#include <QLabel>
|
|
#include <QLineEdit>
|
|
#include <QMainWindow>
|
|
#include <QMenu>
|
|
#include <QPainter>
|
|
#include <QPointer>
|
|
#include <QScreen>
|
|
#include <QSortFilterProxyModel>
|
|
#include <QStatusBar>
|
|
#include <QStyledItemDelegate>
|
|
#include <QTextLayout>
|
|
#include <QToolBar>
|
|
#include <QTreeView>
|
|
#include <QVBoxLayout>
|
|
|
|
#include <KConfigGroup>
|
|
#include <KFuzzyMatcher>
|
|
#include <KLocalizedString>
|
|
#include <KSharedConfig>
|
|
|
|
static QRect getCommandBarBoundingRect(KCommandBar *commandBar)
|
|
{
|
|
QWidget *parentWidget = commandBar->parentWidget();
|
|
Q_ASSERT(parentWidget);
|
|
|
|
const QMainWindow *mainWindow = qobject_cast<const QMainWindow *>(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<QStatusBar *>()) {
|
|
if (!statusBar->isHidden()) {
|
|
boundingRect.setBottom(boundingRect.bottom() - statusBar->height());
|
|
}
|
|
}
|
|
|
|
// exclude any undocked toolbar from the bounding rect
|
|
const QList<QToolBar *> toolBars = mainWindow->findChildren<QToolBar *>();
|
|
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<QTextLayout::FormatRange> 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<const CommandBarFilterModel *>(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<QTextLayout::FormatRange> 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<Button> btns;
|
|
btns.reserve(shortcutSegments.count());
|
|
const int hMargin = horizontalMargin(option);
|
|
for (const QString &text : shortcutSegments) {
|
|
int textWidth = option.fontMetrics.horizontalAdvance(text);
|
|
textWidth += 2 * hMargin;
|
|
btns.append({textWidth, text});
|
|
}
|
|
|
|
int textHeight = option.fontMetrics.lineSpacing();
|
|
// this happens on gnome so we manually decrease the height a bit
|
|
if (textHeight == option.rect.height()) {
|
|
textHeight -= 4;
|
|
}
|
|
|
|
const int y = option.rect.y() + (option.rect.height() - textHeight) / 2;
|
|
int x;
|
|
if (option.direction == Qt::RightToLeft) {
|
|
x = option.rect.x() + hMargin;
|
|
} else {
|
|
x = option.rect.right() - shortcutDrawingWidth(option, shortcutSegments, hMargin) - hMargin;
|
|
}
|
|
|
|
painter->save();
|
|
painter->setPen(option.palette.buttonText().color());
|
|
painter->setRenderHint(QPainter::Antialiasing);
|
|
for (int i = 0, n = btns.count(); i < n; ++i) {
|
|
const Button &button = btns.at(i);
|
|
|
|
QRect outputRect(x, y, button.textWidth, textHeight);
|
|
|
|
// an even element indicates that it is a key
|
|
if (i % 2 == 0) {
|
|
painter->save();
|
|
painter->setPen(Qt::NoPen);
|
|
|
|
// draw rounded rect shadow
|
|
auto shadowRect = outputRect.translated(0, 1);
|
|
painter->setBrush(option.palette.shadow());
|
|
painter->drawRoundedRect(shadowRect, 3.0, 3.0);
|
|
|
|
// draw rounded rect itself
|
|
painter->setBrush(option.palette.window());
|
|
painter->drawRoundedRect(outputRect, 3.0, 3.0);
|
|
|
|
painter->restore();
|
|
}
|
|
|
|
// draw shortcut segment
|
|
painter->drawText(outputRect, Qt::AlignCenter, button.text);
|
|
|
|
x += outputRect.width();
|
|
}
|
|
|
|
painter->restore();
|
|
}
|
|
|
|
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
|
|
{
|
|
if (index.isValid() && index.column() == KCommandBarModel::Column_Shortcut) {
|
|
const QString shortcut = index.data().toString();
|
|
if (!shortcut.isEmpty()) {
|
|
const ShortcutSegments shortcutSegments = splitShortcut(shortcut);
|
|
if (!shortcutSegments.isEmpty()) {
|
|
const int hMargin = horizontalMargin(option);
|
|
int width = shortcutDrawingWidth(option, shortcutSegments, hMargin);
|
|
|
|
// add left and right margins
|
|
width += 2 * hMargin;
|
|
|
|
return QSize(width, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
return QStyledItemDelegate::sizeHint(option, index);
|
|
}
|
|
|
|
private:
|
|
using ShortcutSegments = QStringList;
|
|
|
|
// split shortcut into segments i.e. will return
|
|
// ["Ctrl", "+", "A", ", ", "Ctrl", "+", "K"] for "Ctrl+A, Ctrl+K"
|
|
// twice as fast as using regular expressions
|
|
static ShortcutSegments splitShortcut(const QString &shortcut)
|
|
{
|
|
ShortcutSegments segments;
|
|
if (!shortcut.isEmpty()) {
|
|
const int shortcutLength = shortcut.length();
|
|
int start = 0;
|
|
for (int i = 0; i < shortcutLength; ++i) {
|
|
const QChar c = shortcut.at(i);
|
|
if (c == QLatin1Char('+')) {
|
|
if (i > start) {
|
|
segments << shortcut.mid(start, i - start);
|
|
}
|
|
segments << shortcut.at(i);
|
|
start = i + 1;
|
|
} else if (c == QLatin1Char(',')) {
|
|
if (i > start) {
|
|
segments << shortcut.mid(start, i - start);
|
|
start = i;
|
|
}
|
|
const int j = i + 1;
|
|
if (j < shortcutLength && shortcut.at(j) == QLatin1Char(' ')) {
|
|
segments << shortcut.mid(start, j - start + 1);
|
|
i = j;
|
|
} else {
|
|
segments << shortcut.at(i);
|
|
}
|
|
start = i + 1;
|
|
}
|
|
}
|
|
if (start < shortcutLength) {
|
|
segments << shortcut.mid(start);
|
|
}
|
|
|
|
// check we have successfully parsed the shortcut
|
|
if (segments.isEmpty()) {
|
|
qCWarning(KCONFIG_WIDGETS_LOG) << "Splitting shortcut failed" << shortcut;
|
|
}
|
|
}
|
|
|
|
return segments;
|
|
}
|
|
|
|
// returns the width needed to draw the shortcut
|
|
static int shortcutDrawingWidth(const QStyleOptionViewItem &option, const ShortcutSegments &shortcutSegments, int hMargin)
|
|
{
|
|
int width = 0;
|
|
if (!shortcutSegments.isEmpty()) {
|
|
width = option.fontMetrics.horizontalAdvance(shortcutSegments.join(QString()));
|
|
|
|
// add left and right margins for each segment
|
|
width += shortcutSegments.count() * 2 * hMargin;
|
|
}
|
|
|
|
return width;
|
|
}
|
|
|
|
int horizontalMargin(const QStyleOptionViewItem &option) const
|
|
{
|
|
return option.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin, &option) + 2;
|
|
}
|
|
};
|
|
|
|
// BEGIN KCommandBarPrivate
|
|
class KCommandBarPrivate
|
|
{
|
|
public:
|
|
QTreeView m_treeView;
|
|
QLineEdit m_lineEdit;
|
|
KCommandBarModel m_model;
|
|
CommandBarFilterModel m_proxyModel;
|
|
|
|
/**
|
|
* selects first item in treeview
|
|
*/
|
|
void reselectFirst()
|
|
{
|
|
const QModelIndex index = m_proxyModel.index(0, 0);
|
|
m_treeView.setCurrentIndex(index);
|
|
}
|
|
|
|
/**
|
|
* blocks signals before clearing line edit to ensure
|
|
* we don't trigger filtering / sorting
|
|
*/
|
|
void clearLineEdit()
|
|
{
|
|
const QSignalBlocker blocker(m_lineEdit);
|
|
m_lineEdit.clear();
|
|
}
|
|
|
|
void slotReturnPressed(KCommandBar *q);
|
|
|
|
void setLastUsedActions();
|
|
|
|
QStringList lastUsedActions() const;
|
|
};
|
|
|
|
void KCommandBarPrivate::slotReturnPressed(KCommandBar *q)
|
|
{
|
|
auto act = m_proxyModel.data(m_treeView.currentIndex(), Qt::UserRole).value<QAction *>();
|
|
if (act) {
|
|
// if the action is a menu, we take all its actions
|
|
// and reload our dialog with these instead.
|
|
if (auto menu = act->menu()) {
|
|
auto menuActions = menu->actions();
|
|
KCommandBar::ActionGroup ag;
|
|
|
|
// if there are no actions, trigger load actions
|
|
// this happens with some menus that are loaded on demand
|
|
if (menuActions.size() == 0) {
|
|
Q_EMIT menu->aboutToShow();
|
|
ag.actions = menu->actions();
|
|
}
|
|
|
|
QString groupName = KLocalizedString::removeAcceleratorMarker(act->text());
|
|
ag.name = groupName;
|
|
|
|
m_model.refresh({ag});
|
|
reselectFirst();
|
|
/**
|
|
* We want the "textChanged" signal here
|
|
* so that proxy model triggers filtering again
|
|
* so don't use d->clearLineEdit()
|
|
*/
|
|
m_lineEdit.clear();
|
|
return;
|
|
} else {
|
|
m_model.actionTriggered(act->text());
|
|
q->hide();
|
|
act->trigger();
|
|
}
|
|
}
|
|
|
|
clearLineEdit();
|
|
q->hide();
|
|
q->deleteLater();
|
|
}
|
|
|
|
void KCommandBarPrivate::setLastUsedActions()
|
|
{
|
|
auto cfg = KSharedConfig::openStateConfig();
|
|
KConfigGroup cg(cfg, QStringLiteral("General"));
|
|
|
|
QStringList actionNames = cg.readEntry(QStringLiteral("CommandBarLastUsedActions"), QStringList());
|
|
|
|
return m_model.setLastUsedActions(actionNames);
|
|
}
|
|
|
|
QStringList KCommandBarPrivate::lastUsedActions() const
|
|
{
|
|
return m_model.lastUsedActions();
|
|
}
|
|
// END KCommandBarPrivate
|
|
|
|
// BEGIN KCommandBar
|
|
KCommandBar::KCommandBar(QWidget *parent)
|
|
: QFrame(parent)
|
|
, d(new KCommandBarPrivate)
|
|
{
|
|
setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
|
|
setProperty("_breeze_force_frame", true);
|
|
|
|
QGraphicsDropShadowEffect *e = new QGraphicsDropShadowEffect(this);
|
|
e->setColor(palette().color(QPalette::Dark));
|
|
e->setOffset(0, 4);
|
|
e->setBlurRadius(48);
|
|
setGraphicsEffect(e);
|
|
|
|
QVBoxLayout *layout = new QVBoxLayout();
|
|
layout->setSpacing(0);
|
|
layout->setContentsMargins(QMargins());
|
|
setLayout(layout);
|
|
|
|
layout->addWidget(&d->m_lineEdit);
|
|
d->m_lineEdit.setClearButtonEnabled(true);
|
|
d->m_lineEdit.addAction(QIcon::fromTheme(QStringLiteral("search")), QLineEdit::LeadingPosition);
|
|
d->m_lineEdit.setFrame(false);
|
|
d->m_lineEdit.setTextMargins(QMargins() + style()->pixelMetric(QStyle::PM_ButtonMargin));
|
|
setFocusProxy(&d->m_lineEdit);
|
|
|
|
layout->addWidget(&d->m_treeView);
|
|
d->m_treeView.setTextElideMode(Qt::ElideLeft);
|
|
d->m_treeView.setUniformRowHeights(true);
|
|
d->m_treeView.setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags(Qt::TopEdge)));
|
|
|
|
CommandBarStyleDelegate *delegate = new CommandBarStyleDelegate(this);
|
|
ShortcutStyleDelegate *del = new ShortcutStyleDelegate(this);
|
|
d->m_treeView.setItemDelegateForColumn(KCommandBarModel::Column_Command, delegate);
|
|
d->m_treeView.setItemDelegateForColumn(KCommandBarModel::Column_Shortcut, del);
|
|
|
|
connect(&d->m_lineEdit, &QLineEdit::returnPressed, this, [this]() {
|
|
d->slotReturnPressed(this);
|
|
});
|
|
connect(&d->m_lineEdit, &QLineEdit::textChanged, &d->m_proxyModel, &CommandBarFilterModel::setFilterString);
|
|
connect(&d->m_lineEdit, &QLineEdit::textChanged, delegate, &CommandBarStyleDelegate::setFilterString);
|
|
connect(&d->m_lineEdit, &QLineEdit::textChanged, this, [this]() {
|
|
d->m_treeView.viewport()->update();
|
|
d->reselectFirst();
|
|
});
|
|
connect(&d->m_treeView, &QTreeView::clicked, this, [this]() {
|
|
d->slotReturnPressed(this);
|
|
});
|
|
|
|
d->m_proxyModel.setSourceModel(&d->m_model);
|
|
d->m_treeView.setSortingEnabled(true);
|
|
d->m_treeView.setModel(&d->m_proxyModel);
|
|
|
|
d->m_treeView.header()->setMinimumSectionSize(0);
|
|
d->m_treeView.header()->setStretchLastSection(false);
|
|
d->m_treeView.header()->setSectionResizeMode(KCommandBarModel::Column_Command, QHeaderView::Stretch);
|
|
d->m_treeView.header()->setSectionResizeMode(KCommandBarModel::Column_Shortcut, QHeaderView::ResizeToContents);
|
|
|
|
parent->installEventFilter(this);
|
|
d->m_treeView.installEventFilter(this);
|
|
d->m_lineEdit.installEventFilter(this);
|
|
|
|
d->m_treeView.setHeaderHidden(true);
|
|
d->m_treeView.setRootIsDecorated(false);
|
|
d->m_treeView.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
d->m_treeView.setSelectionMode(QTreeView::SingleSelection);
|
|
|
|
QLabel *placeholderLabel = new QLabel;
|
|
placeholderLabel->setAlignment(Qt::AlignCenter);
|
|
placeholderLabel->setTextInteractionFlags(Qt::NoTextInteraction);
|
|
placeholderLabel->setWordWrap(true);
|
|
// To match the size of a level 2 Heading/KTitleWidget
|
|
QFont placeholderLabelFont = placeholderLabel->font();
|
|
placeholderLabelFont.setPointSize(qRound(placeholderLabelFont.pointSize() * 1.3));
|
|
placeholderLabel->setFont(placeholderLabelFont);
|
|
// Match opacity of QML placeholder label component
|
|
QGraphicsOpacityEffect *opacityEffect = new QGraphicsOpacityEffect(placeholderLabel);
|
|
opacityEffect->setOpacity(0.5);
|
|
placeholderLabel->setGraphicsEffect(opacityEffect);
|
|
|
|
QHBoxLayout *placeholderLayout = new QHBoxLayout;
|
|
placeholderLayout->addWidget(placeholderLabel);
|
|
d->m_treeView.setLayout(placeholderLayout);
|
|
|
|
connect(&d->m_proxyModel, &CommandBarFilterModel::modelReset, this, [this, placeholderLabel]() {
|
|
if (d->m_proxyModel.rowCount() > 0) {
|
|
placeholderLabel->hide();
|
|
} else {
|
|
if (d->m_model.rowCount() == 0) {
|
|
placeholderLabel->setText(i18n("No commands to display"));
|
|
} else {
|
|
placeholderLabel->setText(i18n("No commands matching the filter"));
|
|
}
|
|
placeholderLabel->show();
|
|
}
|
|
});
|
|
|
|
setHidden(true);
|
|
|
|
// Migrate last used action config to new location
|
|
KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("General"));
|
|
if (cg.hasKey("CommandBarLastUsedActions")) {
|
|
const QStringList actionNames = cg.readEntry("CommandBarLastUsedActions", QStringList());
|
|
|
|
KConfigGroup stateCg(KSharedConfig::openStateConfig(), QStringLiteral("General"));
|
|
stateCg.writeEntry(QStringLiteral("CommandBarLastUsedActions"), actionNames);
|
|
|
|
cg.deleteEntry(QStringLiteral("CommandBarLastUsedActions"));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Destructor defined here to make unique_ptr work
|
|
*/
|
|
KCommandBar::~KCommandBar()
|
|
{
|
|
auto lastUsedActions = d->lastUsedActions();
|
|
auto cfg = KSharedConfig::openStateConfig();
|
|
KConfigGroup cg(cfg, QStringLiteral("General"));
|
|
cg.writeEntry("CommandBarLastUsedActions", lastUsedActions);
|
|
|
|
// Explicitly remove installed event filters of children of d-pointer
|
|
// class, otherwise while KCommandBar is being torn down, an event could
|
|
// fire and the eventFilter() accesses d, which would cause a crash
|
|
// bug 452527
|
|
d->m_treeView.removeEventFilter(this);
|
|
d->m_lineEdit.removeEventFilter(this);
|
|
}
|
|
|
|
void KCommandBar::setActions(const QList<ActionGroup> &actions)
|
|
{
|
|
// First set last used actions in the model
|
|
d->setLastUsedActions();
|
|
|
|
d->m_model.refresh(actions);
|
|
d->reselectFirst();
|
|
|
|
show();
|
|
setFocus();
|
|
}
|
|
|
|
void KCommandBar::show()
|
|
{
|
|
const QRect boundingRect = getCommandBarBoundingRect(this);
|
|
|
|
static constexpr int minWidth = 500;
|
|
const int maxWidth = boundingRect.width();
|
|
const int preferredWidth = maxWidth / 2.4;
|
|
|
|
static constexpr int minHeight = 250;
|
|
const int maxHeight = boundingRect.height();
|
|
const int preferredHeight = maxHeight / 2;
|
|
|
|
const QSize size{std::min(maxWidth, std::max(preferredWidth, minWidth)), std::min(maxHeight, std::max(preferredHeight, minHeight))};
|
|
|
|
setFixedSize(size);
|
|
|
|
// set the position to the top-center of the parent
|
|
// just below the menubar/toolbar (if any)
|
|
const QPoint position{boundingRect.center().x() - size.width() / 2, boundingRect.y() + 6};
|
|
move(position);
|
|
|
|
QWidget::show();
|
|
}
|
|
|
|
bool KCommandBar::eventFilter(QObject *obj, QEvent *event)
|
|
{
|
|
if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) {
|
|
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
|
|
if (obj == &d->m_lineEdit) {
|
|
const int key = keyEvent->key();
|
|
const bool forward2list = (key == Qt::Key_Up) || (key == Qt::Key_Down) || (key == Qt::Key_PageUp) || (key == Qt::Key_PageDown);
|
|
if (forward2list) {
|
|
QCoreApplication::sendEvent(&d->m_treeView, event);
|
|
return true;
|
|
}
|
|
} else if (obj == &d->m_treeView) {
|
|
const int key = keyEvent->key();
|
|
const bool forward2input = (key != Qt::Key_Up) && (key != Qt::Key_Down) && (key != Qt::Key_PageUp) && (key != Qt::Key_PageDown)
|
|
&& (key != Qt::Key_Tab) && (key != Qt::Key_Backtab);
|
|
if (forward2input) {
|
|
QCoreApplication::sendEvent(&d->m_lineEdit, event);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (keyEvent->key() == Qt::Key_Escape) {
|
|
hide();
|
|
deleteLater();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// hide on focus out, if neither input field nor list have focus!
|
|
else if (event->type() == QEvent::FocusOut && isVisible() && !(d->m_lineEdit.hasFocus() || d->m_treeView.hasFocus())) {
|
|
d->clearLineEdit();
|
|
deleteLater();
|
|
hide();
|
|
return true;
|
|
}
|
|
|
|
// handle resizing
|
|
if (parent() == obj && event->type() == QEvent::Resize) {
|
|
show();
|
|
}
|
|
|
|
return QWidget::eventFilter(obj, event);
|
|
}
|
|
// END KCommandBar
|
|
|
|
#include "moc_kcommandbar.cpp"
|