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 }