katequickopen.cpp 13.8 KB
Newer Older
1
/*
2
    SPDX-FileCopyrightText: 2007, 2009 Joseph Wenninger <jowenn@kde.org>
Waqar Ahmed's avatar
Waqar Ahmed committed
3
    SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>
Christoph Cullmann's avatar
Christoph Cullmann committed
4

5
    SPDX-License-Identifier: LGPL-2.0-or-later
6
7
8
*/

#include "katequickopen.h"
9
#include "katequickopenmodel.h"
Christoph Cullmann's avatar
Christoph Cullmann committed
10

11
#include "kateapp.h"
Dominik Haumann's avatar
Dominik Haumann committed
12
#include "katemainwindow.h"
13
#include "kateviewmanager.h"
14
15
16
17

#include <ktexteditor/document.h>
#include <ktexteditor/view.h>

Michal Humpula's avatar
Michal Humpula committed
18
19
#include <KAboutData>
#include <KActionCollection>
Waqar Ahmed's avatar
Waqar Ahmed committed
20
#include <KConfigGroup>
Michal Humpula's avatar
Michal Humpula committed
21
#include <KLocalizedString>
22
#include <KPluginFactory>
Waqar Ahmed's avatar
Waqar Ahmed committed
23
#include <KSharedConfig>
Michal Humpula's avatar
Michal Humpula committed
24

25
26
#include <QBoxLayout>
#include <QCoreApplication>
Michal Humpula's avatar
Michal Humpula committed
27
28
#include <QEvent>
#include <QFileInfo>
29
30
#include <QHeaderView>
#include <QLabel>
31
#include <QPainter>
Michal Humpula's avatar
Michal Humpula committed
32
#include <QPointer>
33
#include <QSortFilterProxyModel>
Michal Humpula's avatar
Michal Humpula committed
34
#include <QStandardItemModel>
35
36
#include <QStyledItemDelegate>
#include <QTextDocument>
37
#include <QTreeView>
38

39
#include <drawing_utils.h>
40
#include <kfts_fuzzy_match.h>
41

Waqar Ahmed's avatar
Waqar Ahmed committed
42
class QuickOpenFilterProxyModel final : public QSortFilterProxyModel
43
{
44
public:
45
46
47
48
    QuickOpenFilterProxyModel(QObject *parent = nullptr)
        : QSortFilterProxyModel(parent)
    {
    }
49

50
protected:
51
    bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override
52
    {
Waqar Ahmed's avatar
Waqar Ahmed committed
53
        auto sm = sourceModel();
Waqar Ahmed's avatar
Waqar Ahmed committed
54
55
56
57
58
        if (pattern.isEmpty()) {
            const bool l = static_cast<KateQuickOpenModel *>(sm)->isOpened(sourceLeft);
            const bool r = static_cast<KateQuickOpenModel *>(sm)->isOpened(sourceRight);
            return l < r;
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
59
60
        const int l = static_cast<KateQuickOpenModel *>(sm)->idxScore(sourceLeft);
        const int r = static_cast<KateQuickOpenModel *>(sm)->idxScore(sourceRight);
61
62
63
        return l < r;
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
64
    bool filterAcceptsRow(int sourceRow, const QModelIndex &) const override
65
    {
66
        if (pattern.isEmpty()) {
67
            return true;
68
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
69
70
71
72
73
74

        auto sm = static_cast<KateQuickOpenModel *>(sourceModel());
        if (!sm->isValid(sourceRow)) {
            return false;
        }

75
76
77
78
79
80
81
82
83
84
        QStringView fileNameMatchPattern = pattern;
        // When matching path, we want to match the last section of the pattern
        // with filenames. /path/to/file => pattern: file
        if (matchPath) {
            int lastSlash = pattern.lastIndexOf(QLatin1Char('/'));
            if (lastSlash != -1) {
                fileNameMatchPattern = fileNameMatchPattern.mid(lastSlash + 1);
            }
        }

Waqar Ahmed's avatar
Waqar Ahmed committed
85
        const QString &name = sm->idxToFileName(sourceRow);
86
87

        int score = 0;
88
        bool res;
89
        // dont use the QStringView(QString) ctor
90
91
92
93
94
        if (fileNameMatchPattern.isEmpty()) {
            res = true;
        } else {
            res = filterByName(QStringView(name.data(), name.size()), fileNameMatchPattern, score);
        }
Waqar Ahmed's avatar
Waqar Ahmed committed
95

96
97
        // only match file path if needed
        if (matchPath && res) {
98
99
100
101
            int scorep = 0;
            QStringView path{sm->idxToFilePath(sourceRow)};
            bool resp = filterByPath(path, QStringView(pattern.data(), pattern.size()), scorep);
            score += scorep;
102
103
104
            res = resp;
            // zero out the score if didn't match
            score *= res;
105
        }
106

107
108
109
        if (res) {
            // +1 point for opened files
            score += (sm->isOpened(sourceRow));
110
111
112
113
114
115
116

            // extra points if file exists in project root
            // This gives priority to the files at the root
            // of the project over others. This is important
            // because otherwise getting to root files may
            // not be that easy
            if (!matchPath) {
117
                score += (sm->idxToFilePath(sourceRow) == name) * name.size();
118
            }
119
120
        }

Waqar Ahmed's avatar
Waqar Ahmed committed
121
        sm->setScoreForIndex(sourceRow, score);
122

123
        return res;
124
125
126
    }

public Q_SLOTS:
Waqar Ahmed's avatar
Waqar Ahmed committed
127
    bool setFilterText(const QString &text)
128
    {
Waqar Ahmed's avatar
Waqar Ahmed committed
129
130
131
132
133
134
        // we don't want to trigger filtering if the user is just entering line:col
        const auto splitted = text.split(QLatin1Char(':')).at(0);
        if (splitted == pattern) {
            return false;
        }

135
        beginResetModel();
Waqar Ahmed's avatar
Waqar Ahmed committed
136
        pattern = splitted;
137
        matchPath = pattern.contains(QLatin1Char('/'));
138
        endResetModel();
Waqar Ahmed's avatar
Waqar Ahmed committed
139
140

        return true;
141
142
    }

143
private:
144
    static inline bool filterByPath(QStringView path, QStringView pattern, int &score)
145
146
147
148
    {
        return kfts::fuzzy_match(pattern, path, score);
    }

149
    static inline bool filterByName(QStringView name, QStringView pattern, int &score)
150
151
152
153
    {
        return kfts::fuzzy_match(pattern, name, score);
    }

154
private:
155
    QString pattern;
156
    bool matchPath = false;
157
158
};

159
160
class QuickOpenStyleDelegate : public QStyledItemDelegate
{
161
public:
162
    QuickOpenStyleDelegate(QObject *parent = nullptr)
163
        : QStyledItemDelegate(parent)
164
165
    {
    }
166
167
168
169
170
171

    void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        QStyleOptionViewItem options = option;
        initStyleOption(&options, index);

Waqar Ahmed's avatar
Waqar Ahmed committed
172
173
        QString name = index.data(KateQuickOpenModel::FileName).toString();
        QString path = index.data(KateQuickOpenModel::FilePath).toString();
174

175
176
177
178
179
        // only remove suffix, not where it might occur elsewhere
        const QString suffix = QStringLiteral("/") + name;
        if (path.endsWith(suffix)) {
            path.chop(suffix.size());
        }
180

181
182
183
184
185
186
187
188
189
190
        QTextCharFormat fmt;
        fmt.setForeground(options.palette.link().color());
        fmt.setFontWeight(QFont::Bold);

        const int nameLen = name.length();
        // space between name and path
        constexpr int space = 1;
        QVector<QTextLayout::FormatRange> formats;

        // collect formats
191
192
193
        int pos = m_filterString.lastIndexOf(QLatin1Char('/'));
        if (pos > -1) {
            ++pos;
Volker Krause's avatar
Volker Krause committed
194
            auto pattern = QStringView(m_filterString).mid(pos);
195
196
            auto nameFormats = kfts::get_fuzzy_match_formats(pattern, name, 0, fmt);
            formats.append(nameFormats);
197
        } else {
198
199
            auto nameFormats = kfts::get_fuzzy_match_formats(m_filterString, name, 0, fmt);
            formats.append(nameFormats);
200
        }
201
202
203
        QTextCharFormat boldFmt;
        boldFmt.setFontWeight(QFont::Bold);
        boldFmt.setFontPointSize(options.font.pointSize() - 1);
204
        auto pathFormats = kfts::get_fuzzy_match_formats(m_filterString, path, nameLen + space, boldFmt);
205
206
207
        QTextCharFormat gray;
        gray.setForeground(Qt::gray);
        gray.setFontPointSize(options.font.pointSize() - 1);
Christoph Cullmann's avatar
Christoph Cullmann committed
208
        formats.append({nameLen + space, static_cast<int>(path.length()), gray});
209
        formats.append(pathFormats);
210
211
212
213
214
215
216
217
218
219
220
221
222

        painter->save();

        // paint background
        if (option.state & QStyle::State_Selected) {
            painter->fillRect(option.rect, option.palette.highlight());
        } else {
            painter->fillRect(option.rect, option.palette.base());
        }

        options.text = QString(); // clear old text
        options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter, options.widget);

223
224
225
        // space for icon
        painter->translate(25, 0);

226
        // draw text
227
        Utils::paintItemViewText(painter, QString(name + QStringLiteral(" ") + path), options, formats);
228
229
230
231
232

        painter->restore();
    }

public Q_SLOTS:
233
    void setFilterString(const QString &text)
234
235
236
237
238
239
240
241
    {
        m_filterString = text;
    }

private:
    QString m_filterString;
};

242
243
Q_DECLARE_METATYPE(QPointer<KTextEditor::Document>)

Waqar Ahmed's avatar
Waqar Ahmed committed
244
KateQuickOpen::KateQuickOpen(KateMainWindow *mainWindow)
245
    : QMenu(mainWindow)
246
    , m_mainWindow(mainWindow)
Christoph Cullmann's avatar
Christoph Cullmann committed
247
{
248
    // ensure the components have some proper frame
249
    QVBoxLayout *layout = new QVBoxLayout();
250
    layout->setSpacing(0);
251
    layout->setContentsMargins(4, 4, 4, 4);
252
    setLayout(layout);
253

254
    m_inputLine = new QuickOpenLineEdit(this);
255
    setFocusProxy(m_inputLine);
256

257
    layout->addWidget(m_inputLine);
258

259
260
    m_listView = new QTreeView();
    layout->addWidget(m_listView, 1);
261
    m_listView->setTextElideMode(Qt::ElideLeft);
262
    m_listView->setUniformRowHeights(true);
263

Waqar Ahmed's avatar
Waqar Ahmed committed
264
    m_base_model = new KateQuickOpenModel(this);
265

266
    m_model = new QuickOpenFilterProxyModel(this);
267
    m_model->setFilterRole(Qt::DisplayRole);
268
    m_model->setSortRole(KateQuickOpenModel::Score);
269
270
    m_model->setFilterCaseSensitivity(Qt::CaseInsensitive);
    m_model->setSortCaseSensitivity(Qt::CaseInsensitive);
271
    m_model->setFilterKeyColumn(Qt::DisplayRole);
272

273
274
    m_styleDelegate = new QuickOpenStyleDelegate(this);
    m_listView->setItemDelegate(m_styleDelegate);
275

276
    connect(m_inputLine, &QuickOpenLineEdit::textChanged, m_styleDelegate, &QuickOpenStyleDelegate::setFilterString);
Waqar Ahmed's avatar
Waqar Ahmed committed
277
278
279
280
281
282
    connect(m_inputLine, &QuickOpenLineEdit::textChanged, this, [this](const QString &text) {
        if (m_model->setFilterText(text)) {
            m_styleDelegate->setFilterString(text);
            m_listView->viewport()->update();
            reselectFirst(); // hacky way
        }
283
    });
284
    connect(m_inputLine, &QuickOpenLineEdit::returnPressed, this, &KateQuickOpen::slotReturnPressed);
285
    connect(m_inputLine, &QuickOpenLineEdit::listModeChanged, this, &KateQuickOpen::slotListModeChanged);
286

Laurent Montel's avatar
Laurent Montel committed
287
    connect(m_listView, &QTreeView::activated, this, &KateQuickOpen::slotReturnPressed);
Waqar Ahmed's avatar
Waqar Ahmed committed
288
    connect(m_listView, &QTreeView::clicked, this, &KateQuickOpen::slotReturnPressed); // for single click
289
290

    m_listView->setModel(m_model);
291
    m_listView->setSortingEnabled(true);
292
    m_model->setSourceModel(m_base_model);
293

294
295
    m_inputLine->installEventFilter(this);
    m_listView->installEventFilter(this);
296
297
    m_listView->setHeaderHidden(true);
    m_listView->setRootIsDecorated(false);
Waqar Ahmed's avatar
Waqar Ahmed committed
298
299
300
    m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

    setHidden(true);
301

302
    m_base_model->setListMode(m_inputLine->listMode());
303
304

    // fill stuff
305
    updateState();
306
307
}

308
309
bool KateQuickOpen::eventFilter(QObject *obj, QEvent *event)
{
310
311
    // catch key presses + shortcut overrides to allow to have ESC as application wide shortcut, too, see bug 409856
    if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) {
312
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
313
        if (obj == m_inputLine) {
Alexander Lohnau's avatar
Alexander Lohnau committed
314
315
            const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp)
                || (keyEvent->key() == Qt::Key_PageDown);
316
            if (forward2list) {
317
                QCoreApplication::sendEvent(m_listView, event);
318
319
                return true;
            }
320

321
        } else {
Alexander Lohnau's avatar
Alexander Lohnau committed
322
323
            const bool forward2input = (keyEvent->key() != Qt::Key_Up) && (keyEvent->key() != Qt::Key_Down) && (keyEvent->key() != Qt::Key_PageUp)
                && (keyEvent->key() != Qt::Key_PageDown) && (keyEvent->key() != Qt::Key_Tab) && (keyEvent->key() != Qt::Key_Backtab);
324
            if (forward2input) {
325
                QCoreApplication::sendEvent(m_inputLine, event);
326
327
328
329
                return true;
            }
        }
    }
330

331
    return QWidget::eventFilter(obj, event);
332
333
}

334
335
void KateQuickOpen::reselectFirst()
{
336
    int first = 0;
337
    if (m_mainWindow->viewManager()->views().size() > 1 && m_model->rowCount() > 1 && m_inputLine->text().isEmpty()) {
338
        first = 1;
339
    }
340
341

    QModelIndex index = m_model->index(first, 0);
342
343
344
    m_listView->setCurrentIndex(index);
}

345
void KateQuickOpen::updateState()
346
{
347
    m_base_model->refresh(m_mainWindow);
348
    reselectFirst();
Waqar Ahmed's avatar
Waqar Ahmed committed
349

350
    updateViewGeometry();
Waqar Ahmed's avatar
Waqar Ahmed committed
351
352
    show();
    setFocus();
353
}
Christoph Cullmann's avatar
Christoph Cullmann committed
354

355
void KateQuickOpen::slotReturnPressed()
Christoph Cullmann's avatar
Christoph Cullmann committed
356
{
357
358
359
360
    // save current position before opening new url for location history
    KateViewManager *vm = m_mainWindow->viewManager();
    if (vm) {
        if (KTextEditor::View *v = vm->activeView()) {
361
            vm->addPositionToHistory(v->document()->url(), v->cursorPosition());
362
363
364
        }
    }

365
366
367
368
369
370
371
372
    // either get view via document pointer or url
    const QModelIndex index = m_listView->model()->index(m_listView->currentIndex().row(), 0);
    KTextEditor::View *view = nullptr;
    if (auto doc = index.data(KateQuickOpenModel::Document).value<KTextEditor::Document *>()) {
        view = m_mainWindow->activateView(doc);
    } else {
        view = m_mainWindow->wrapper()->openUrl(index.data(Qt::UserRole).toUrl());
    }
Waqar Ahmed's avatar
Waqar Ahmed committed
373
374
375

    const auto strs = m_inputLine->text().split(QLatin1Char(':'));
    if (view && strs.count() > 1) {
376
377
        // helper to convert String => Number
        auto stringToInt = [](const QString &s) {
Waqar Ahmed's avatar
Waqar Ahmed committed
378
            bool ok = false;
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
            const int num = s.toInt(&ok);
            return ok ? num : -1;
        };
        KTextEditor::Cursor cursor = KTextEditor::Cursor::invalid();

        // try to get line
        const int line = stringToInt(strs.at(1));
        cursor.setLine(line - 1);

        // if line is valid, try to see if we have column available as well
        if (line > -1 && strs.count() > 2) {
            const int col = stringToInt(strs.at(2));
            cursor.setColumn(col - 1);
        }

        // do we have valid line at least?
        if (line > -1) {
            view->setCursorPosition(cursor);
Waqar Ahmed's avatar
Waqar Ahmed committed
397
398
399
        }
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
400
    hide();
401
    m_mainWindow->slotWindowActivated();
Waqar Ahmed's avatar
Waqar Ahmed committed
402

403
    // store the new position in location history
Waqar Ahmed's avatar
Waqar Ahmed committed
404
405
    if (view) {
        vm->addPositionToHistory(view->document()->url(), view->cursorPosition());
406
    }
407
}
408

409
void KateQuickOpen::slotListModeChanged(KateQuickOpenModel::List mode)
410
411
{
    m_base_model->setListMode(mode);
412
    // this changes things again, needs refresh, let's go all the way
413
    updateState();
414
415
}

Waqar Ahmed's avatar
Waqar Ahmed committed
416
417
void KateQuickOpen::updateViewGeometry()
{
Christoph Cullmann's avatar
Christoph Cullmann committed
418
    const QSize centralSize = m_mainWindow->size();
Waqar Ahmed's avatar
Waqar Ahmed committed
419

420
421
    // width: 2.4 of editor, height: 1/2 of editor
    const QSize viewMaxSize(centralSize.width() / 2.4, centralSize.height() / 2);
Waqar Ahmed's avatar
Waqar Ahmed committed
422

423
    // Position should be central over window
424
425
    const int xPos = std::max(0, (centralSize.width() - viewMaxSize.width()) / 2);
    const int yPos = std::max(0, (centralSize.height() - viewMaxSize.height()) * 1 / 4);
426
    const QPoint p(xPos, yPos);
Christoph Cullmann's avatar
Christoph Cullmann committed
427
    move(p + m_mainWindow->pos());
428
429

    setFixedSize(viewMaxSize);
430
}