File indexing completed on 2024-05-12 16:02:26

0001 /*
0002     SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 #include "katecommandbar.h"
0007 #include "commandmodel.h"
0008 
0009 #include <QAction>
0010 #include <QCoreApplication>
0011 #include <QKeyEvent>
0012 #include <QLineEdit>
0013 #include <QPainter>
0014 #include <QPointer>
0015 #include <QPushButton>
0016 #include <QSortFilterProxyModel>
0017 #include <QStyledItemDelegate>
0018 #include <QTextDocument>
0019 #include <QTreeView>
0020 #include <QVBoxLayout>
0021 #include <QDebug>
0022 
0023 #include <kactioncollection.h>
0024 #include <KLocalizedString>
0025 
0026 #include <kfts_fuzzy_match.h>
0027 
0028 class CommandBarFilterModel : public QSortFilterProxyModel
0029 {
0030 public:
0031     CommandBarFilterModel(QObject *parent = nullptr)
0032         : QSortFilterProxyModel(parent)
0033     {
0034     }
0035 
0036     Q_SLOT void setFilterString(const QString &string)
0037     {
0038         beginResetModel();
0039         m_pattern = string;
0040         endResetModel();
0041     }
0042 
0043 protected:
0044     bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override
0045     {
0046         const int l = sourceLeft.data(CommandModel::Score).toInt();
0047         const int r = sourceRight.data(CommandModel::Score).toInt();
0048         return l < r;
0049     }
0050 
0051     bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
0052     {
0053         if (m_pattern.isEmpty()) {
0054             return true;
0055         }
0056 
0057         int score = 0;
0058         const auto idx = sourceModel()->index(sourceRow, 0, sourceParent);
0059         if (idx.isValid()){
0060             const QString row = idx.data(Qt::DisplayRole).toString();
0061             int pos = row.indexOf(QLatin1Char(':'));
0062             if (pos < 0) {
0063                 return false;
0064             }
0065             const QString actionName = row.mid(pos + 2);
0066             const bool res = kfts::fuzzy_match_sequential(m_pattern, actionName, score);
0067             sourceModel()->setData(idx, score, CommandModel::Score);
0068             return res;
0069         }
0070         return false;
0071     }
0072 
0073 private:
0074     QString m_pattern;
0075 };
0076 
0077 class CommandBarStyleDelegate : public QStyledItemDelegate
0078 {
0079 public:
0080     CommandBarStyleDelegate(QObject *parent = nullptr)
0081         : QStyledItemDelegate(parent)
0082     {
0083     }
0084 
0085     void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
0086     {
0087         QStyleOptionViewItem options = option;
0088         initStyleOption(&options, index);
0089 
0090         QTextDocument doc;
0091 
0092         const auto original = index.data().toString();
0093 
0094         const auto strs = index.data().toString().split(QLatin1Char(':'));
0095         QString str = strs.at(1);
0096         const QString nameColor = option.palette.color(QPalette::Link).name();
0097         kfts::to_fuzzy_matched_display_string(m_filterString, str, QString("<b style=\"color:%1;\">").arg(nameColor), QString("</b>"));
0098 
0099         const QString component = QString("<span style=\"color: %1;\"><b>").arg(nameColor) + strs.at(0) + QString(":</b> </span>");
0100 
0101         doc.setHtml(component + str);
0102         doc.setDocumentMargin(2);
0103 
0104         painter->save();
0105 
0106         // paint background
0107         if (option.state & QStyle::State_Selected) {
0108             painter->fillRect(option.rect, option.palette.highlight());
0109         } else {
0110             painter->fillRect(option.rect, option.palette.base());
0111         }
0112 
0113         options.text = QString(); // clear old text
0114         options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter, options.widget);
0115 
0116         // fix stuff for rtl
0117         // QTextDocument doesn't work with RTL text out of the box so we give it a hand here by increasing
0118         // the text width to our rect size. Icon displacement is also calculated here because 'translate()'
0119         // later will not work.
0120         const bool rtl = original.isRightToLeft();
0121         if (rtl) {
0122             auto r = options.widget->style()->subElementRect(QStyle::SE_ItemViewItemText, &options, options.widget);
0123             auto hasIcon = index.data(Qt::DecorationRole).value<QIcon>().isNull();
0124             if (hasIcon) {
0125                 doc.setTextWidth(r.width() - 25);
0126             } else {
0127                 doc.setTextWidth(r.width());
0128             }
0129         }
0130 
0131         // draw text
0132         painter->translate(option.rect.x(), option.rect.y());
0133         // leave space for icon
0134 
0135         if (!rtl) {
0136             painter->translate(25, 0);
0137         }
0138 
0139         doc.drawContents(painter);
0140 
0141         painter->restore();
0142     }
0143 
0144 public Q_SLOTS:
0145     void setFilterString(const QString &text)
0146     {
0147         m_filterString = text;
0148     }
0149 
0150 private:
0151     QString m_filterString;
0152 };
0153 
0154 class ShortcutStyleDelegate : public QStyledItemDelegate
0155 {
0156 public:
0157     ShortcutStyleDelegate(QObject *parent = nullptr)
0158         : QStyledItemDelegate(parent)
0159     {
0160     }
0161 
0162     void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
0163     {
0164         QStyleOptionViewItem options = option;
0165         initStyleOption(&options, index);
0166         painter->save();
0167 
0168         const auto shortcutString = index.data().toString();
0169 
0170         // paint background
0171         if (option.state & QStyle::State_Selected) {
0172             painter->fillRect(option.rect, option.palette.highlight());
0173         } else {
0174             painter->fillRect(option.rect, option.palette.base());
0175         }
0176 
0177         options.text = QString(); // clear old text
0178         options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter, options.widget);
0179 
0180         if (!shortcutString.isEmpty()) {
0181             // collect rects for each word
0182             QVector<QPair<QRect, QString>> btns;
0183             const auto list = [&shortcutString] {
0184 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
0185                 auto list = shortcutString.split(QLatin1Char('+'), QString::SkipEmptyParts);
0186 #else
0187                 auto list = shortcutString.split(QLatin1Char('+'), Qt::SkipEmptyParts);
0188 #endif
0189                 if (shortcutString.endsWith(QLatin1String("+"))) {
0190                     list.append(QStringLiteral("+"));
0191                 }
0192                 return list;
0193             }();
0194             btns.reserve(list.size());
0195             for (const QString &text : list) {
0196                 QRect r = option.fontMetrics.boundingRect(text);
0197                 r.setWidth(r.width() + 8);
0198                 btns.append({r, text});
0199             }
0200 
0201             const auto plusRect = option.fontMetrics.boundingRect(QLatin1Char('+'));
0202 
0203             // draw them
0204             int x = option.rect.x();
0205             const int y = option.rect.y();
0206             const int plusY = option.rect.y() + plusRect.height() / 2;
0207             const int total = btns.size();
0208 
0209             // make sure our rects are nicely V-center aligned in the row
0210             painter->translate(QPoint(0, (option.rect.height() - btns.at(0).first.height()) / 2));
0211 
0212             int i = 0;
0213             painter->setRenderHint(QPainter::Antialiasing);
0214             for (const auto &btn : btns) {
0215                 painter->setPen(Qt::NoPen);
0216                 const QRect &rect = btn.first;
0217 
0218                 QRect buttonRect(x, y, rect.width(), rect.height());
0219 
0220                 // draw rounded rect shadow
0221                 auto shadowRect = buttonRect.translated(0, 1);
0222                 painter->setBrush(option.palette.shadow());
0223                 painter->drawRoundedRect(shadowRect, 3, 3);
0224 
0225                 // draw rounded rect itself
0226                 painter->setBrush(option.palette.button());
0227                 painter->drawRoundedRect(buttonRect, 3, 3);
0228 
0229                 // draw text inside rounded rect
0230                 painter->setPen(option.palette.buttonText().color());
0231                 painter->drawText(buttonRect, Qt::AlignCenter, btn.second);
0232 
0233                 // draw '+'
0234                 if (i + 1 < total) {
0235                     x += rect.width() + 5;
0236                     painter->drawText(QPoint(x, plusY + (rect.height() / 2)), QString("+"));
0237                     x += plusRect.width() + 5;
0238                 }
0239                 i++;
0240             }
0241         }
0242 
0243         painter->restore();
0244     }
0245 };
0246 
0247 KateCommandBar::KateCommandBar(QWidget *parent)
0248     : QMenu(parent)
0249 {
0250     QVBoxLayout *layout = new QVBoxLayout();
0251     layout->setSpacing(0);
0252     layout->setContentsMargins(4, 4, 4, 4);
0253     setLayout(layout);
0254 
0255     m_lineEdit = new QLineEdit(this);
0256     setFocusProxy(m_lineEdit);
0257 
0258     layout->addWidget(m_lineEdit);
0259 
0260     m_treeView = new QTreeView();
0261     layout->addWidget(m_treeView, 1);
0262     m_treeView->setTextElideMode(Qt::ElideMiddle);
0263     m_treeView->setUniformRowHeights(true);
0264 
0265     m_model = new CommandModel(this);
0266 
0267     CommandBarStyleDelegate *delegate = new CommandBarStyleDelegate(this);
0268     ShortcutStyleDelegate *del = new ShortcutStyleDelegate(this);
0269     m_treeView->setItemDelegateForColumn(0, delegate);
0270     m_treeView->setItemDelegateForColumn(1, del);
0271 
0272     m_proxyModel = new CommandBarFilterModel(this);
0273     m_proxyModel->setFilterRole(Qt::DisplayRole);
0274     m_proxyModel->setSortRole(CommandModel::Score);
0275     m_proxyModel->setFilterKeyColumn(0);
0276 
0277     connect(m_lineEdit, &QLineEdit::returnPressed, this, &KateCommandBar::slotReturnPressed);
0278     connect(m_lineEdit, &QLineEdit::textChanged, m_proxyModel, &CommandBarFilterModel::setFilterString);
0279     connect(m_lineEdit, &QLineEdit::textChanged, delegate, &CommandBarStyleDelegate::setFilterString);
0280     connect(m_lineEdit, &QLineEdit::textChanged, this, [this]() {
0281         m_treeView->viewport()->update();
0282         reselectFirst();
0283     });
0284     connect(m_treeView, &QTreeView::clicked, this, &KateCommandBar::slotReturnPressed);
0285 
0286     m_proxyModel->setSourceModel(m_model);
0287     m_treeView->setSortingEnabled(true);
0288     m_treeView->setModel(m_proxyModel);
0289 
0290     m_treeView->installEventFilter(this);
0291     m_lineEdit->installEventFilter(this);
0292 
0293     m_treeView->setHeaderHidden(true);
0294     m_treeView->setRootIsDecorated(false);
0295     m_treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0296     m_treeView->setSelectionMode(QTreeView::SingleSelection);
0297 
0298     setHidden(true);
0299 }
0300 
0301 void KateCommandBar::updateBar(const QList<KisKActionCollection *> &actionCollections, int totalActions)
0302 {
0303     qDeleteAll(m_disposableActionCollections);
0304     m_disposableActionCollections.clear();
0305 
0306     QVector<QPair<QString, QAction *>> actionList;
0307     actionList.reserve(totalActions);
0308 
0309     for (const auto collection : actionCollections) {
0310 
0311         if (collection->componentName().contains("disposable")) {
0312             m_disposableActionCollections << collection;
0313         }
0314 
0315         const QList<QAction *> collectionActions = collection->actions();
0316         const QString componentName = collection->componentDisplayName();
0317         for (const auto action : collectionActions) {
0318             // sanity + empty check ensures displayable actions and removes ourself
0319             // from the action list
0320             if (action && action->isEnabled() && !action->text().isEmpty()) {
0321                 actionList.append({componentName, action});
0322             }
0323         }
0324     }
0325 
0326 
0327 
0328 
0329     m_model->refresh(std::move(actionList));
0330     reselectFirst();
0331 
0332     updateViewGeometry();
0333     show();
0334     setFocus();
0335 }
0336 
0337 bool KateCommandBar::eventFilter(QObject *obj, QEvent *event)
0338 {
0339     // catch key presses + shortcut overrides to allow to have ESC as application wide shortcut, too, see bug 409856
0340     if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) {
0341         QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
0342         if (obj == m_lineEdit) {
0343             const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp)
0344                 || (keyEvent->key() == Qt::Key_PageDown);
0345             if (forward2list) {
0346                 QCoreApplication::sendEvent(m_treeView, event);
0347                 return true;
0348             }
0349 
0350             if (keyEvent->key() == Qt::Key_Escape) {
0351                 m_lineEdit->clear();
0352                 keyEvent->accept();
0353                 hide();
0354                 return true;
0355             }
0356         } else {
0357             const bool forward2input = (keyEvent->key() != Qt::Key_Up) && (keyEvent->key() != Qt::Key_Down) && (keyEvent->key() != Qt::Key_PageUp)
0358                 && (keyEvent->key() != Qt::Key_PageDown) && (keyEvent->key() != Qt::Key_Tab) && (keyEvent->key() != Qt::Key_Backtab);
0359             if (forward2input) {
0360                 QCoreApplication::sendEvent(m_lineEdit, event);
0361                 return true;
0362             }
0363         }
0364     }
0365 
0366     // hide on focus out, if neither input field nor list have focus!
0367     else if (event->type() == QEvent::FocusOut && !(m_lineEdit->hasFocus() || m_treeView->hasFocus())) {
0368         m_lineEdit->clear();
0369         hide();
0370         return true;
0371     }
0372 
0373     return QWidget::eventFilter(obj, event);
0374 }
0375 
0376 void KateCommandBar::slotReturnPressed()
0377 {
0378     auto act = m_proxyModel->data(m_treeView->currentIndex(), Qt::UserRole).value<QAction *>();
0379     if (act) {
0380         // if the action is a menu, we take all its actions
0381         // and reload our dialog with these instead.
0382         if (auto menu = act->menu()) {
0383             auto menuActions = menu->actions();
0384             QVector<QPair<QString, QAction *>> list;
0385             list.reserve(menuActions.size());
0386 
0387             // if there are no actions, trigger load actions
0388             // this happens with some menus that are loaded on demand
0389             if (menuActions.size() == 0) {
0390                 Q_EMIT menu->aboutToShow();
0391                 menuActions = menu->actions();
0392             }
0393 
0394             for (auto menuAction : qAsConst(menuActions)) {
0395                 if (menuAction) {
0396                     list.append({KLocalizedString::removeAcceleratorMarker(act->text()), menuAction});
0397                 }
0398             }
0399             m_model->refresh(list);
0400             m_lineEdit->clear();
0401             return;
0402         } else {
0403             act->trigger();
0404         }
0405     }
0406     m_lineEdit->clear();
0407     hide();
0408 }
0409 
0410 void KateCommandBar::reselectFirst()
0411 {
0412     QModelIndex index = m_proxyModel->index(0, 0);
0413     m_treeView->setCurrentIndex(index);
0414 }
0415 
0416 void KateCommandBar::updateViewGeometry()
0417 {
0418     m_treeView->resizeColumnToContents(0);
0419     m_treeView->resizeColumnToContents(1);
0420 
0421     const QSize centralSize = parentWidget()->size();
0422 
0423     // width: 2.4 of editor, height: 1/2 of editor
0424     const QSize viewMaxSize(centralSize.width() / 2.4, centralSize.height() / 2);
0425 
0426     // Position should be central over window
0427     const int xPos = std::max(0, (centralSize.width() - viewMaxSize.width()) / 2);
0428     const int yPos = std::max(0, (centralSize.height() - viewMaxSize.height()) * 1 / 4);
0429 
0430     const QPoint p(xPos, yPos);
0431     move(p + parentWidget()->pos());
0432 
0433     this->setFixedSize(viewMaxSize);
0434 }