Krita Source Code Documentation
Loading...
Searching...
No Matches
katecommandbar.cpp
Go to the documentation of this file.
1/*
2 SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6#include "katecommandbar.h"
7#include "commandmodel.h"
8
9#include <QAction>
10#include <QCoreApplication>
11#include <QKeyEvent>
12#include <QLineEdit>
13#include <QPainter>
14#include <QSortFilterProxyModel>
15#include <QStyledItemDelegate>
16#include <QTextDocument>
17#include <QTreeView>
18#include <QVBoxLayout>
19#include <QDebug>
20
21#include <kactioncollection.h>
22#include <KLocalizedString>
23
24#include <kfts_fuzzy_match.h>
25
26class CommandBarFilterModel : public QSortFilterProxyModel
27{
28public:
29 CommandBarFilterModel(QObject *parent = nullptr)
30 : QSortFilterProxyModel(parent)
31 {
32 }
33
34 Q_SLOT void setFilterString(const QString &string)
35 {
36 beginResetModel();
37 m_pattern = string;
38 endResetModel();
39 }
40
41protected:
42 bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override
43 {
44 const int l = sourceLeft.data(CommandModel::Score).toInt();
45 const int r = sourceRight.data(CommandModel::Score).toInt();
46 return l < r;
47 }
48
49 bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
50 {
51 if (m_pattern.isEmpty()) {
52 return true;
53 }
54
55 int score = 0;
56 const auto idx = sourceModel()->index(sourceRow, 0, sourceParent);
57 if (idx.isValid()){
58 const QString row = idx.data(Qt::DisplayRole).toString();
59 int pos = row.indexOf(QLatin1Char(':'));
60 if (pos < 0) {
61 return false;
62 }
63 const QString actionName = row.mid(pos + 2);
64 const bool res = kfts::fuzzy_match_sequential(m_pattern, actionName, score);
65 sourceModel()->setData(idx, score, CommandModel::Score);
66 return res;
67 }
68 return false;
69 }
70
71private:
72 QString m_pattern;
73};
74
75class CommandBarStyleDelegate : public QStyledItemDelegate
76{
77public:
78 CommandBarStyleDelegate(QObject *parent = nullptr)
79 : QStyledItemDelegate(parent)
80 {
81 }
82
83 void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
84 {
85 QStyleOptionViewItem options = option;
86 initStyleOption(&options, index);
87
88 QTextDocument doc;
89
90 const auto original = index.data().toString();
91
92 const auto strs = index.data().toString().split(QLatin1Char(':'));
93 QString str = strs.at(1);
94 const QString nameColor = option.palette.color(QPalette::Link).name();
95 kfts::to_fuzzy_matched_display_string(m_filterString, str, QString("<b style=\"color:%1;\">").arg(nameColor), QString("</b>"));
96
97 const QString component = QString("<span style=\"color: %1;\"><b>").arg(nameColor) + strs.at(0) + QString(":</b> </span>");
98
99 doc.setHtml(component + str);
100 doc.setDocumentMargin(2);
101
102 painter->save();
103
104 // paint background
105 if (option.state & QStyle::State_Selected) {
106 painter->fillRect(option.rect, option.palette.highlight());
107 } else {
108 painter->fillRect(option.rect, option.palette.base());
109 }
110
111 options.text = QString(); // clear old text
112 options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter, options.widget);
113
114 // fix stuff for rtl
115 // QTextDocument doesn't work with RTL text out of the box so we give it a hand here by increasing
116 // the text width to our rect size. Icon displacement is also calculated here because 'translate()'
117 // later will not work.
118 const bool rtl = original.isRightToLeft();
119 if (rtl) {
120 auto r = options.widget->style()->subElementRect(QStyle::SE_ItemViewItemText, &options, options.widget);
121 auto hasIcon = index.data(Qt::DecorationRole).value<QIcon>().isNull();
122 if (hasIcon) {
123 doc.setTextWidth(r.width() - 25);
124 } else {
125 doc.setTextWidth(r.width());
126 }
127 }
128
129 // draw text
130 painter->translate(option.rect.x(), option.rect.y());
131 // leave space for icon
132
133 if (!rtl) {
134 painter->translate(25, 0);
135 }
136
137 doc.drawContents(painter);
138
139 painter->restore();
140 }
141
142public Q_SLOTS:
143 void setFilterString(const QString &text)
144 {
145 m_filterString = text;
146 }
147
148private:
150};
151
152class ShortcutStyleDelegate : public QStyledItemDelegate
153{
154public:
155 ShortcutStyleDelegate(QObject *parent = nullptr)
156 : QStyledItemDelegate(parent)
157 {
158 }
159
160 void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
161 {
162 QStyleOptionViewItem options = option;
163 initStyleOption(&options, index);
164 painter->save();
165
166 const auto shortcutString = index.data().toString();
167
168 // paint background
169 if (option.state & QStyle::State_Selected) {
170 painter->fillRect(option.rect, option.palette.highlight());
171 } else {
172 painter->fillRect(option.rect, option.palette.base());
173 }
174
175 options.text = QString(); // clear old text
176 options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter, options.widget);
177
178 if (!shortcutString.isEmpty()) {
179 // collect rects for each word
181 const auto list = [&shortcutString] {
182 auto list = shortcutString.split(QLatin1Char('+'), Qt::SkipEmptyParts);
183 if (shortcutString.endsWith(QLatin1String("+"))) {
184 list.append(QStringLiteral("+"));
185 }
186 return list;
187 }();
188 btns.reserve(list.size());
189 for (const QString &text : list) {
190 QRect r = option.fontMetrics.boundingRect(text);
191 r.setWidth(r.width() + 8);
192 btns.append({r, text});
193 }
194
195 const auto plusRect = option.fontMetrics.boundingRect(QLatin1Char('+'));
196
197 // draw them
198 int x = option.rect.x();
199 const int y = option.rect.y();
200 const int plusY = option.rect.y() + plusRect.height() / 2;
201 const int total = btns.size();
202
203 // make sure our rects are nicely V-center aligned in the row
204 painter->translate(QPoint(0, (option.rect.height() - btns.at(0).first.height()) / 2));
205
206 int i = 0;
207 painter->setRenderHint(QPainter::Antialiasing);
208 for (const auto &btn : btns) {
209 painter->setPen(Qt::NoPen);
210 const QRect &rect = btn.first;
211
212 QRect buttonRect(x, y, rect.width(), rect.height());
213
214 // draw rounded rect shadow
215 auto shadowRect = buttonRect.translated(0, 1);
216 painter->setBrush(option.palette.shadow());
217 painter->drawRoundedRect(shadowRect, 3, 3);
218
219 // draw rounded rect itself
220 painter->setBrush(option.palette.button());
221 painter->drawRoundedRect(buttonRect, 3, 3);
222
223 // draw text inside rounded rect
224 painter->setPen(option.palette.buttonText().color());
225 painter->drawText(buttonRect, Qt::AlignCenter, btn.second);
226
227 // draw '+'
228 if (i + 1 < total) {
229 x += rect.width() + 5;
230 painter->drawText(QPoint(x, plusY + (rect.height() / 2)), QString("+"));
231 x += plusRect.width() + 5;
232 }
233 i++;
234 }
235 }
236
237 painter->restore();
238 }
239};
240
242 : QMenu(parent)
243{
244 QVBoxLayout *layout = new QVBoxLayout();
245 layout->setSpacing(0);
246 layout->setContentsMargins(4, 4, 4, 4);
247 setLayout(layout);
248
249 m_lineEdit = new QLineEdit(this);
250 setFocusProxy(m_lineEdit);
251
252 layout->addWidget(m_lineEdit);
253
254 m_treeView = new QTreeView();
255 layout->addWidget(m_treeView, 1);
256 m_treeView->setTextElideMode(Qt::ElideMiddle);
257 m_treeView->setUniformRowHeights(true);
258
259 m_model = new CommandModel(this);
260
263 m_treeView->setItemDelegateForColumn(0, delegate);
264 m_treeView->setItemDelegateForColumn(1, del);
265
267 m_proxyModel->setFilterRole(Qt::DisplayRole);
268 m_proxyModel->setSortRole(CommandModel::Score);
269 m_proxyModel->setFilterKeyColumn(0);
270
271 connect(m_lineEdit, &QLineEdit::returnPressed, this, &KateCommandBar::slotReturnPressed);
273 connect(m_lineEdit, &QLineEdit::textChanged, delegate, &CommandBarStyleDelegate::setFilterString);
274 connect(m_lineEdit, &QLineEdit::textChanged, this, [this]() {
275 m_treeView->viewport()->update();
277 });
278 connect(m_treeView, &QTreeView::clicked, this, &KateCommandBar::slotReturnPressed);
279
280 m_proxyModel->setSourceModel(m_model);
281 m_treeView->setSortingEnabled(true);
282 m_treeView->setModel(m_proxyModel);
283
284 m_treeView->installEventFilter(this);
285 m_lineEdit->installEventFilter(this);
286
287 m_treeView->setHeaderHidden(true);
288 m_treeView->setRootIsDecorated(false);
289 m_treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
290 m_treeView->setSelectionMode(QTreeView::SingleSelection);
291
292 setHidden(true);
293}
294
295void KateCommandBar::updateBar(const QList<KisKActionCollection *> &actionCollections, int totalActions)
296{
299
301 actionList.reserve(totalActions);
302
303 for (const auto collection : actionCollections) {
304
305 if (collection->componentName().contains("disposable")) {
306 m_disposableActionCollections << collection;
307 }
308
309 const QList<QAction *> collectionActions = collection->actions();
310 const QString componentName = collection->componentDisplayName();
311 for (const auto action : collectionActions) {
312 // sanity + empty check ensures displayable actions and removes ourself
313 // from the action list
314 if (action && action->isEnabled() && !action->text().isEmpty()) {
315 actionList.append({componentName, action});
316 }
317 }
318 }
319
320
321
322
323 m_model->refresh(std::move(actionList));
325
327 show();
328 setFocus();
329}
330
331bool KateCommandBar::eventFilter(QObject *obj, QEvent *event)
332{
333 // catch key presses + shortcut overrides to allow to have ESC as application wide shortcut, too, see bug 409856
334 if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) {
335 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
336 if (obj == m_lineEdit) {
337 const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp)
338 || (keyEvent->key() == Qt::Key_PageDown);
339 if (forward2list) {
340 QCoreApplication::sendEvent(m_treeView, event);
341 return true;
342 }
343
344 if (keyEvent->key() == Qt::Key_Escape) {
345 m_lineEdit->clear();
346 keyEvent->accept();
347 hide();
348 return true;
349 }
350 } else {
351 const bool forward2input = (keyEvent->key() != Qt::Key_Up) && (keyEvent->key() != Qt::Key_Down) && (keyEvent->key() != Qt::Key_PageUp)
352 && (keyEvent->key() != Qt::Key_PageDown) && (keyEvent->key() != Qt::Key_Tab) && (keyEvent->key() != Qt::Key_Backtab);
353 if (forward2input) {
354 QCoreApplication::sendEvent(m_lineEdit, event);
355 return true;
356 }
357 }
358 }
359
360 // hide on focus out, if neither input field nor list have focus!
361 else if (event->type() == QEvent::FocusOut && !(m_lineEdit->hasFocus() || m_treeView->hasFocus())) {
362 m_lineEdit->clear();
363 hide();
364 return true;
365 }
366
367 return QWidget::eventFilter(obj, event);
368}
369
371{
372 auto act = m_proxyModel->data(m_treeView->currentIndex(), Qt::UserRole).value<QAction *>();
373 if (act) {
374 // if the action is a menu, we take all its actions
375 // and reload our dialog with these instead.
376 if (auto menu = act->menu()) {
377 auto menuActions = menu->actions();
379 list.reserve(menuActions.size());
380
381 // if there are no actions, trigger load actions
382 // this happens with some menus that are loaded on demand
383 if (menuActions.size() == 0) {
384 Q_EMIT menu->aboutToShow();
385 menuActions = menu->actions();
386 }
387
388 for (auto menuAction : std::as_const(menuActions)) {
389 if (menuAction) {
390 list.append({KLocalizedString::removeAcceleratorMarker(act->text()), menuAction});
391 }
392 }
393 m_model->refresh(list);
394 m_lineEdit->clear();
395 return;
396 } else {
397 act->trigger();
398 }
399 }
400 m_lineEdit->clear();
401 hide();
402}
403
405{
406 QModelIndex index = m_proxyModel->index(0, 0);
407 m_treeView->setCurrentIndex(index);
408}
409
411{
412 m_treeView->resizeColumnToContents(0);
413 m_treeView->resizeColumnToContents(1);
414
415 const QSize centralSize = parentWidget()->size();
416
417 // width: 2.4 of editor, height: 1/2 of editor
418 const QSize viewMaxSize(centralSize.width() / 2.4, centralSize.height() / 2);
419
420 // Position should be central over window
421 const int xPos = std::max(0, (centralSize.width() - viewMaxSize.width()) / 2);
422 const int yPos = std::max(0, (centralSize.height() - viewMaxSize.height()) * 1 / 4);
423
424 const QPoint p(xPos, yPos);
425 move(p + parentWidget()->pos());
426
427 this->setFixedSize(viewMaxSize);
428}
const Params2D p
connect(this, SIGNAL(optionsChanged()), this, SLOT(saveOptions()))
bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override
Q_SLOT void setFilterString(const QString &string)
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
CommandBarFilterModel(QObject *parent=nullptr)
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
CommandBarStyleDelegate(QObject *parent=nullptr)
void setFilterString(const QString &text)
void refresh(QVector< QPair< QString, QAction * > > actionList)
CommandModel * m_model
QTreeView * m_treeView
KateCommandBar(QWidget *parent=nullptr)
void updateBar(const QList< KisKActionCollection * > &actions, int totalActions)
CommandBarFilterModel * m_proxyModel
bool eventFilter(QObject *obj, QEvent *event) override
QLineEdit * m_lineEdit
QVector< KisKActionCollection * > m_disposableActionCollections
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
ShortcutStyleDelegate(QObject *parent=nullptr)
static Q_DECL_UNUSED QString to_fuzzy_matched_display_string(const QString pattern, QString &str, const QString &htmlTag, const QString &htmlTagClose)
get string for display in treeview / listview. This should be used from style delegate....
static Q_DECL_UNUSED bool fuzzy_match_sequential(const QString pattern, const QString str, int &outScore)
This is a special case function which doesn't score separator matches higher than sequential matches....