File indexing completed on 2024-04-28 05:49:36

0001 /*
0002     SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 #include "quickdialog.h"
0007 
0008 #include "drawing_utils.h"
0009 #include <QCoreApplication>
0010 #include <QDebug>
0011 #include <QKeyEvent>
0012 #include <QPainter>
0013 #include <QStringListModel>
0014 #include <QTextLayout>
0015 #include <QVBoxLayout>
0016 
0017 #include <KFuzzyMatcher>
0018 
0019 namespace
0020 {
0021 class FuzzyFilterModel final : public QSortFilterProxyModel
0022 {
0023 public:
0024     explicit FuzzyFilterModel(QObject *parent = nullptr)
0025         : QSortFilterProxyModel(parent)
0026     {
0027     }
0028 
0029     bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override
0030     {
0031         if (!m_pattern.isEmpty() && m_scoreRole > -1 && m_scoreRole > Qt::UserRole) {
0032             const int l = sourceLeft.data(m_scoreRole).toInt();
0033             const int r = sourceRight.data(m_scoreRole).toInt();
0034             return l < r;
0035         }
0036         return sourceLeft.row() > sourceRight.row();
0037     }
0038 
0039     bool filterAcceptsRow(int row, const QModelIndex &parent) const override
0040     {
0041         if (m_pattern.isEmpty()) {
0042             return true;
0043         }
0044 
0045         const auto index = sourceModel()->index(row, filterKeyColumn(), parent);
0046         const auto text = index.data(filterRole()).toString();
0047         if (m_filterType == HUDDialog::Fuzzy) {
0048             return KFuzzyMatcher::matchSimple(m_pattern, text);
0049         } else if (m_filterType == HUDDialog::Contains) {
0050             return text.contains(m_pattern, Qt::CaseInsensitive);
0051         } else if (m_filterType == HUDDialog::ScoredFuzzy) {
0052             auto res = KFuzzyMatcher::match(m_pattern, text);
0053             Q_ASSERT(m_scoreRole > -1 && m_scoreRole > Qt::UserRole);
0054             sourceModel()->setData(index, res.score, m_scoreRole);
0055             return res.matched;
0056         }
0057         return false;
0058     }
0059 
0060     void setFilterString(const QString &text)
0061     {
0062         beginResetModel();
0063         m_pattern = text;
0064         endResetModel();
0065     }
0066 
0067     void setFilterType(HUDDialog::FilterType t)
0068     {
0069         m_filterType = t;
0070     }
0071 
0072     void setScoreRole(int role)
0073     {
0074         m_scoreRole = role;
0075     }
0076 
0077 private:
0078     HUDDialog::FilterType m_filterType;
0079     QString m_pattern;
0080     int m_scoreRole = -1;
0081 };
0082 
0083 }
0084 
0085 void HUDStyleDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
0086 {
0087     QStyleOptionViewItem options = option;
0088     initStyleOption(&options, index);
0089 
0090     QString text = index.data(m_displayRole).toString();
0091 
0092     QList<QTextLayout::FormatRange> formats;
0093 
0094     QTextCharFormat fmt;
0095     fmt.setForeground(options.palette.link());
0096     fmt.setFontWeight(QFont::Bold);
0097     auto ranges = KFuzzyMatcher::matchedRanges(m_filterString, text);
0098     QList<QTextLayout::FormatRange> resFmts;
0099     std::transform(ranges.begin(), ranges.end(), std::back_inserter(resFmts), [fmt](const KFuzzyMatcher::Range &fr) {
0100         return QTextLayout::FormatRange{fr.start, fr.length, fmt};
0101     });
0102 
0103     formats.append(resFmts);
0104 
0105     painter->save();
0106 
0107     options.text = QString(); // clear old text
0108     options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter, options.widget);
0109     options.rect.adjust(4, 0, 0, 0);
0110 
0111     Utils::paintItemViewText(painter, text, options, formats);
0112 
0113     painter->restore();
0114 }
0115 
0116 HUDDialog::HUDDialog(QWidget *parent, QWidget *mainWindow)
0117     : QMenu(parent)
0118     , m_mainWindow(mainWindow)
0119     , m_model(new QStringListModel(this))
0120     , m_proxy(new FuzzyFilterModel(this))
0121 {
0122     m_proxy->setSourceModel(m_model);
0123     m_proxy->setFilterRole(Qt::DisplayRole);
0124     m_proxy->setFilterKeyColumn(0);
0125 
0126     m_delegate = new HUDStyleDelegate(this);
0127 
0128     QVBoxLayout *layout = new QVBoxLayout(this);
0129     layout->setSpacing(0);
0130     layout->setContentsMargins(4, 4, 4, 4);
0131 
0132     setFocusProxy(&m_lineEdit);
0133 
0134     layout->addWidget(&m_lineEdit);
0135 
0136     layout->addWidget(&m_treeView, 1);
0137 
0138     m_treeView.setModel(m_proxy);
0139     m_treeView.setTextElideMode(Qt::ElideLeft);
0140     m_treeView.setUniformRowHeights(true);
0141     m_treeView.setItemDelegate(m_delegate);
0142 
0143     connect(&m_lineEdit, &QLineEdit::returnPressed, this, [this] {
0144         slotReturnPressed(m_treeView.currentIndex());
0145     });
0146     // user can add this as necessary
0147     setFilteringEnabled(true);
0148     connect(&m_treeView, &QTreeView::clicked, this, &HUDDialog::slotReturnPressed);
0149     m_treeView.setSortingEnabled(true);
0150 
0151     m_treeView.installEventFilter(this);
0152     m_lineEdit.installEventFilter(this);
0153 
0154     m_treeView.setHeaderHidden(true);
0155     m_treeView.setRootIsDecorated(false);
0156     m_treeView.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0157     m_treeView.setSelectionMode(QTreeView::SingleSelection);
0158 
0159     updateViewGeometry();
0160     setFocus();
0161 }
0162 
0163 HUDDialog::~HUDDialog()
0164 {
0165     m_treeView.removeEventFilter(this);
0166     m_lineEdit.removeEventFilter(this);
0167 }
0168 
0169 void HUDDialog::slotReturnPressed(const QModelIndex &index)
0170 {
0171     Q_EMIT itemExecuted(index);
0172 
0173     clearLineEdit();
0174     hide();
0175 }
0176 
0177 void HUDDialog::setDelegate(HUDStyleDelegate *delegate)
0178 {
0179     m_delegate = delegate;
0180     delete m_treeView.itemDelegate();
0181     m_treeView.setItemDelegate(m_delegate);
0182 }
0183 
0184 void HUDDialog::reselectFirst()
0185 {
0186     const QModelIndex index = m_treeView.model()->index(0, 0);
0187     m_treeView.setCurrentIndex(index);
0188 }
0189 
0190 void HUDDialog::setStringList(const QStringList &strList)
0191 {
0192     if (auto strModel = qobject_cast<QStringListModel *>(m_proxy->sourceModel())) {
0193         strModel->setStringList(strList);
0194     } else {
0195         qWarning() << "You are using a custom model: " << m_model.data() << ", setStringList has no effect";
0196     }
0197 }
0198 
0199 bool HUDDialog::eventFilter(QObject *obj, QEvent *event)
0200 {
0201     // catch key presses + shortcut overrides to allow to have ESC as application wide shortcut, too, see bug 409856
0202     if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) {
0203         QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
0204         if (obj == &m_lineEdit) {
0205             const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp)
0206                 || (keyEvent->key() == Qt::Key_PageDown);
0207             if (forward2list) {
0208                 QCoreApplication::sendEvent(&m_treeView, event);
0209                 return true;
0210             }
0211 
0212             if (keyEvent->key() == Qt::Key_Escape) {
0213                 clearLineEdit();
0214                 keyEvent->accept();
0215                 hide();
0216                 return true;
0217             }
0218         } else {
0219             const bool forward2input = (keyEvent->key() != Qt::Key_Up) && (keyEvent->key() != Qt::Key_Down) && (keyEvent->key() != Qt::Key_PageUp)
0220                 && (keyEvent->key() != Qt::Key_PageDown) && (keyEvent->key() != Qt::Key_Tab) && (keyEvent->key() != Qt::Key_Backtab);
0221             if (forward2input) {
0222                 QCoreApplication::sendEvent(&m_lineEdit, event);
0223                 return true;
0224             }
0225         }
0226     }
0227 
0228     // hide on focus out, if neither input field nor list have focus!
0229     else if (event->type() == QEvent::FocusOut && !(m_lineEdit.hasFocus() || m_treeView.hasFocus())) {
0230         clearLineEdit();
0231         hide();
0232         return true;
0233     }
0234 
0235     return QWidget::eventFilter(obj, event);
0236 }
0237 
0238 void HUDDialog::updateViewGeometry()
0239 {
0240     if (!m_mainWindow)
0241         return;
0242 
0243     const QSize centralSize = m_mainWindow->size();
0244 
0245     // width: 2.4 of editor, height: 1/2 of editor
0246     const QSize viewMaxSize(centralSize.width() / 2.4, centralSize.height() / 2);
0247 
0248     // Position should be central over window
0249     const int xPos = std::max(0, (centralSize.width() - viewMaxSize.width()) / 2);
0250     const int yPos = std::max(0, (centralSize.height() - viewMaxSize.height()) * 1 / 4);
0251     const QPoint p(xPos, yPos);
0252     move(p + m_mainWindow->pos());
0253 
0254     this->setFixedSize(viewMaxSize);
0255 }
0256 
0257 void HUDDialog::clearLineEdit()
0258 {
0259     const QSignalBlocker block(m_lineEdit);
0260     m_lineEdit.clear();
0261 }
0262 
0263 void HUDDialog::setModel(QAbstractItemModel *model, FilterType type, int filterKeyCol, int filterRole, int scoreRole)
0264 {
0265     m_model = model;
0266     m_proxy->setSourceModel(model);
0267     m_proxy->setFilterKeyColumn(filterKeyCol);
0268     m_proxy->setFilterRole(filterRole);
0269     auto proxy = static_cast<FuzzyFilterModel *>(m_proxy.data());
0270     proxy->setFilterType(type);
0271     proxy->setScoreRole(scoreRole);
0272 }
0273 
0274 void HUDDialog::setFilteringEnabled(bool enabled)
0275 {
0276     if (!enabled) {
0277         disconnect(&m_lineEdit, &QLineEdit::textChanged, this, nullptr);
0278         m_treeView.setModel(m_model);
0279     } else {
0280         Q_ASSERT(m_proxy);
0281         if (m_treeView.model() != m_proxy) {
0282             Q_ASSERT(m_proxy->sourceModel());
0283             m_treeView.setModel(m_proxy);
0284         }
0285         connect(&m_lineEdit, &QLineEdit::textChanged, this, [this](const QString &txt) {
0286             static_cast<FuzzyFilterModel *>(m_proxy.data())->setFilterString(txt);
0287             m_delegate->setFilterString(txt);
0288             m_treeView.viewport()->update();
0289             m_treeView.setCurrentIndex(m_treeView.model()->index(0, 0));
0290         });
0291     }
0292 }
0293 
0294 #include "moc_quickdialog.cpp"