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"