1 /*
2 SPDX-FileCopyrightText: 2014-2019 Dominik Haumann <dhaumann@kde.org>
3 SPDX-FileCopyrightText: 2020 Waqar Ahmed <waqar.17a@gmail.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 #include "gotosymbolwidget.h"
8 #include "gotoglobalsymbolmodel.h"
9 #include "gotosymbolmodel.h"
10 #include "gotosymboltreeview.h"
11 #include "kate_ctags_view.h"
12 #include "tags.h"
13
14 #include <QCoreApplication>
15 #include <QKeyEvent>
16 #include <QLineEdit>
17 #include <QPainter>
18 #include <QPropertyAnimation>
19 #include <QScrollBar>
20 #include <QSortFilterProxyModel>
21 #include <QStyledItemDelegate>
22 #include <QTextDocument>
23 #include <QVBoxLayout>
24
25 #include <KTextEditor/MainWindow>
26 #include <KTextEditor/Message>
27 #include <KTextEditor/View>
28
29 class QuickOpenFilterProxyModel : public QSortFilterProxyModel
30 {
31 public:
QuickOpenFilterProxyModel(QObject * parent=nullptr)32 QuickOpenFilterProxyModel(QObject *parent = nullptr)
33 : QSortFilterProxyModel(parent)
34 {
35 }
36
filterAcceptsRow(int sourceRow,const QModelIndex & sourceParent) const37 bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
38 {
39 const QString fileName = sourceModel()->index(sourceRow, 0, sourceParent).data().toString();
40 for (const QString &str : m_filterStrings) {
41 if (!fileName.contains(str, Qt::CaseInsensitive)) {
42 return false;
43 }
44 }
45 return true;
46 }
47
filterStrings() const48 QStringList filterStrings() const
49 {
50 return m_filterStrings;
51 }
52
53 public Q_SLOTS:
setFilterText(const QString & text)54 void setFilterText(const QString &text)
55 {
56 m_filterStrings = text.split(QLatin1Char(' '), Qt::SkipEmptyParts);
57
58 invalidateFilter();
59 }
60
61 private:
62 QStringList m_filterStrings;
63 };
64
65 class GotoStyleDelegate : public QStyledItemDelegate
66 {
67 public:
GotoStyleDelegate(QObject * parent=nullptr)68 GotoStyleDelegate(QObject *parent = nullptr)
69 : QStyledItemDelegate(parent)
70 {
71 }
72
paint(QPainter * painter,const QStyleOptionViewItem & option,const QModelIndex & index) const73 void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
74 {
75 QStyleOptionViewItem options = option;
76 initStyleOption(&options, index);
77
78 QTextDocument doc;
79
80 QString str = index.data().toString();
81 for (const auto &string : m_filterStrings) {
82 // FIXME: This will skip the letter 'b' if the string
83 // has only one letter so that we don't match inside
84 // <b> tags.
85 if (string == QLatin1String("b")) {
86 continue;
87 }
88 const QRegularExpression re(QStringLiteral("(") + QRegularExpression::escape(string) + QStringLiteral(")"),
89 QRegularExpression::CaseInsensitiveOption);
90 str.replace(re, QStringLiteral("<b>\\1</b>"));
91 }
92
93 auto file = index.data(GotoGlobalSymbolModel::FileUrl).toString();
94 // this will be empty for local symbol mode
95 if (!file.isEmpty()) {
96 str += QStringLiteral(" <span style=\"color: gray;\">") + QFileInfo(file).fileName() + QStringLiteral("</span>");
97 }
98
99 doc.setHtml(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 // draw text
115 painter->translate(option.rect.x(), option.rect.y());
116 if (index.column() == 0) {
117 painter->translate(25, 0);
118 }
119 doc.drawContents(painter);
120
121 painter->restore();
122 }
123
124 public Q_SLOTS:
setFilterStrings(const QString & text)125 void setFilterStrings(const QString &text)
126 {
127 m_filterStrings = text.split(QLatin1Char(' '), Qt::SkipEmptyParts);
128 }
129
130 private:
131 QStringList m_filterStrings;
132 };
133
GotoSymbolWidget(KTextEditor::MainWindow * mainWindow,KateCTagsView * pluginView,QWidget * widget)134 GotoSymbolWidget::GotoSymbolWidget(KTextEditor::MainWindow *mainWindow, KateCTagsView *pluginView, QWidget *widget)
135 : QWidget(widget)
136 , ctagsPluginView(pluginView)
137 , m_mainWindow(mainWindow)
138 , oldPos(-1, -1)
139 {
140 setWindowFlags(Qt::FramelessWindowHint);
141
142 mode = Local;
143
144 m_treeView = new GotoSymbolTreeView(mainWindow, this);
145 m_styleDelegate = new GotoStyleDelegate(this);
146 m_treeView->setItemDelegate(m_styleDelegate);
147 m_lineEdit = new QLineEdit(this);
148
149 setFocusProxy(m_lineEdit);
150
151 m_proxyModel = new QuickOpenFilterProxyModel(this);
152 m_proxyModel->setSortRole(Qt::DisplayRole);
153 m_proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
154 m_proxyModel->setFilterRole(Qt::DisplayRole);
155 m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
156 m_proxyModel->setFilterKeyColumn(0);
157
158 m_symbolsModel = new GotoSymbolModel(this);
159 m_globalSymbolsModel = new GotoGlobalSymbolModel(this);
160
161 m_proxyModel->setSourceModel(m_symbolsModel);
162 m_treeView->setModel(m_proxyModel);
163
164 connect(m_lineEdit, &QLineEdit::textChanged, m_proxyModel, &QuickOpenFilterProxyModel::setFilterText);
165 connect(m_lineEdit, &QLineEdit::textChanged, m_styleDelegate, &GotoStyleDelegate::setFilterStrings);
166 connect(m_lineEdit, &QLineEdit::textChanged, this, [this]() {
167 m_treeView->viewport()->update();
168 });
169 connect(m_lineEdit, &QLineEdit::textChanged, this, &GotoSymbolWidget::loadGlobalSymbols);
170 connect(m_lineEdit, &QLineEdit::returnPressed, this, &GotoSymbolWidget::slotReturnPressed);
171
172 connect(m_treeView, &QTreeView::activated, this, &GotoSymbolWidget::slotReturnPressed);
173 connect(m_proxyModel, &QSortFilterProxyModel::rowsInserted, this, &GotoSymbolWidget::reselectFirst);
174 connect(m_proxyModel, &QSortFilterProxyModel::rowsRemoved, this, &GotoSymbolWidget::reselectFirst);
175
176 QVBoxLayout *layout = new QVBoxLayout();
177 layout->setSpacing(0);
178 layout->setContentsMargins(4, 4, 4, 4);
179 layout->addWidget(m_lineEdit);
180 layout->addWidget(m_treeView);
181 setLayout(layout);
182
183 m_treeView->installEventFilter(this);
184 m_lineEdit->installEventFilter(this);
185 }
186
eventFilter(QObject * obj,QEvent * event)187 bool GotoSymbolWidget::eventFilter(QObject *obj, QEvent *event)
188 {
189 if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) {
190 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
191 if (obj == m_lineEdit) {
192 const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp)
193 || (keyEvent->key() == Qt::Key_PageDown);
194 if (forward2list) {
195 QCoreApplication::sendEvent(m_treeView, event);
196 return true;
197 }
198
199 if (keyEvent->key() == Qt::Key_Escape) {
200 if (oldPos.isValid()) {
201 m_mainWindow->activeView()->setCursorPosition(oldPos);
202 }
203 m_lineEdit->clear();
204 keyEvent->accept();
205 hide();
206 return true;
207 }
208 } else {
209 const bool forward2input = (keyEvent->key() != Qt::Key_Up) && (keyEvent->key() != Qt::Key_Down) && (keyEvent->key() != Qt::Key_PageUp)
210 && (keyEvent->key() != Qt::Key_PageDown) && (keyEvent->key() != Qt::Key_Tab) && (keyEvent->key() != Qt::Key_Backtab);
211 if (forward2input) {
212 QCoreApplication::sendEvent(m_lineEdit, event);
213 return true;
214 }
215 }
216 }
217
218 else if (event->type() == QEvent::FocusOut && !(m_lineEdit->hasFocus() || m_treeView->hasFocus())) {
219 m_lineEdit->clear();
220 hide();
221 return true;
222 }
223
224 return QWidget::eventFilter(obj, event);
225 }
226
showSymbols(const QString & filePath)227 void GotoSymbolWidget::showSymbols(const QString &filePath)
228 {
229 changeMode(Local);
230 oldPos = m_mainWindow->activeView()->cursorPosition();
231 m_symbolsModel->refresh(filePath);
232 updateViewGeometry();
233 reselectFirst();
234 }
235
showGlobalSymbols(const QString & tagFilePath)236 void GotoSymbolWidget::showGlobalSymbols(const QString &tagFilePath)
237 {
238 changeMode(Global);
239 m_tagFile = tagFilePath;
240 updateViewGeometry();
241 }
242
loadGlobalSymbols(const QString & text)243 void GotoSymbolWidget::loadGlobalSymbols(const QString &text)
244 {
245 if (m_tagFile.isEmpty() || !QFileInfo::exists(m_tagFile) || !QFileInfo(m_tagFile).isFile()) {
246 Tags::TagEntry e(i18n("Tags file not found. Please generate one manually or using the CTags plugin"), QString(), QString(), QString());
247 m_globalSymbolsModel->setSymbolsData({e});
248 return;
249 }
250
251 if (text.length() < 3 || mode == Local) {
252 return;
253 }
254
255 QString currentWord = text;
256 Tags::TagList list = Tags::getPartialMatchesNoi8n(m_tagFile, currentWord);
257
258 if (list.isEmpty()) {
259 return;
260 }
261
262 m_globalSymbolsModel->setSymbolsData(std::move(list));
263 updateViewGeometry();
264 reselectFirst();
265 }
266
slotReturnPressed()267 void GotoSymbolWidget::slotReturnPressed()
268 {
269 const auto idx = m_proxyModel->index(m_treeView->currentIndex().row(), 0);
270 if (!idx.isValid()) {
271 return;
272 }
273
274 if (mode == Global) {
275 QString tag = idx.data(Qt::UserRole).toString();
276 QString pattern = idx.data(GotoGlobalSymbolModel::Pattern).toString();
277 QString file = idx.data(GotoGlobalSymbolModel::FileUrl).toString();
278 bool fileFound = true;
279
280 QFileInfo fi(file);
281 QString url;
282 // if the file doesn't exist, try to load it using project base dir
283 if (!fi.exists()) {
284 fileFound = false;
285 QObject *projectView = m_mainWindow->pluginView(QStringLiteral("kateprojectplugin"));
286 QString ret = projectView ? projectView->property("projectBaseDir").toString() : QString();
287 if (!ret.isEmpty() && !ret.endsWith(QLatin1Char('/'))) {
288 ret.append(QLatin1Char('/'));
289 }
290 url = ret + file;
291 fi.setFile(url);
292
293 // check again
294 // not found? use tagFile path as base path
295 if (!fi.exists()) {
296 url.clear();
297 fi.setFile(m_tagFile);
298 QString path = fi.absolutePath();
299 url = path + QStringLiteral("/") + file;
300
301 fi.setFile(url);
302 if (fi.exists()) {
303 fileFound = true;
304 }
305 } else {
306 fileFound = true;
307 }
308 } else {
309 url = file;
310 }
311
312 if (fileFound) {
313 ctagsPluginView->jumpToTag(url, pattern, tag);
314 } else {
315 QString msg = i18n("File for '%1' not found.", tag);
316 auto message = new KTextEditor::Message(msg, KTextEditor::Message::MessageType::Error);
317 if (auto view = m_mainWindow->activeView()) {
318 view->document()->postMessage(message);
319 }
320 }
321
322 } else {
323 int line = idx.data(Qt::UserRole).toInt();
324
325 // try to find the start position of this tag
326 // and put the cursor there
327 QString tag = idx.data().toString();
328 QString textLine = m_mainWindow->activeView()->document()->line(--line);
329 int col = textLine.indexOf(tag.midRef(0, 4));
330 col = col >= 0 ? col : 0;
331 KTextEditor::Cursor c(line, col);
332
333 m_mainWindow->activeView()->setCursorPosition(c);
334 }
335
336 // block signals, so that rowsInserted isn't emitted causing us to loose position
337 const QSignalBlocker blocker(m_proxyModel);
338
339 m_lineEdit->clear();
340 hide();
341 }
342
changeMode(GotoSymbolWidget::Mode newMode)343 void GotoSymbolWidget::changeMode(GotoSymbolWidget::Mode newMode)
344 {
345 mode = newMode;
346 if (mode == Global) {
347 m_proxyModel->setSourceModel(m_globalSymbolsModel);
348 m_treeView->setGlobalMode(true);
349 } else if (mode == Local) {
350 m_proxyModel->setSourceModel(m_symbolsModel);
351 m_treeView->setGlobalMode(false);
352 }
353 }
354
updateViewGeometry()355 void GotoSymbolWidget::updateViewGeometry()
356 {
357 QWidget *window = m_mainWindow->window();
358 const QSize centralSize = window->size();
359
360 // width: 2.4 of editor, height: 1/2 of editor
361 const QSize viewMaxSize(centralSize.width() / 2.4, centralSize.height() / 2);
362
363 const int rowHeight = m_treeView->sizeHintForRow(0) == -1 ? 0 : m_treeView->sizeHintForRow(0);
364
365 int frameWidth = this->frameSize().width();
366 frameWidth = frameWidth > centralSize.width() / 2.4 ? centralSize.width() / 2.4 : frameWidth;
367
368 const int width = viewMaxSize.width();
369
370 const int rowCount = mode == Global ? m_globalSymbolsModel->rowCount() : m_symbolsModel->rowCount();
371
372 const QSize viewSize(width, std::min(std::max(rowHeight * rowCount + 2 * frameWidth, rowHeight * 6), viewMaxSize.height()));
373
374 // Position should be central over the editor area, so map to global from
375 // parent of central widget since the view is positioned in global coords
376 const QPoint centralWidgetPos = window->parentWidget() ? window->mapToGlobal(window->pos()) : window->pos();
377 const int xPos = std::max(0, centralWidgetPos.x() + (centralSize.width() - viewSize.width()) / 2);
378 const int yPos = std::max(0, centralWidgetPos.y() + (centralSize.height() - viewSize.height()) * 1 / 4);
379
380 move(xPos, yPos);
381
382 QPropertyAnimation *animation = new QPropertyAnimation(this, "size");
383 animation->setDuration(150);
384 animation->setStartValue(this->size());
385 animation->setEndValue(viewSize);
386
387 animation->start(QPropertyAnimation::DeleteWhenStopped);
388 }
389
reselectFirst()390 void GotoSymbolWidget::reselectFirst()
391 {
392 QModelIndex index = m_proxyModel->index(0, 0);
393 if (index.isValid()) {
394 m_treeView->setCurrentIndex(index);
395 }
396 }
397