katequickopen.cpp 12.7 KB
Newer Older
Christoph Cullmann's avatar
Christoph Cullmann committed
1
2
/*  SPDX-License-Identifier: LGPL-2.0-or-later

3
    SPDX-FileCopyrightText: 2007, 2009 Joseph Wenninger <jowenn@kde.org>
Waqar Ahmed's avatar
Waqar Ahmed committed
4
    SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>
Christoph Cullmann's avatar
Christoph Cullmann committed
5

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

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

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

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

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

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

41
#include <kfts_fuzzy_match.h>
42

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

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

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

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

        const QString &name = sm->idxToFileName(sourceRow);
77
78
79

        int score = 0;
        bool res = false;
Waqar Ahmed's avatar
Waqar Ahmed committed
80
81
82
83
84
85
        int scorep = 0, scoren = 0;
        bool resn = filterByName(name, scoren);

        // only match file path if filename got a match
        bool resp = false;
        if (resn || pathLike) {
86
            const QStringView path = sm->idxToFilePath(sourceRow);
Waqar Ahmed's avatar
Waqar Ahmed committed
87
            resp = filterByPath(path, scorep);
88
        }
89

Waqar Ahmed's avatar
Waqar Ahmed committed
90
91
92
93
94
        // store the score for sorting later
        score = scoren + scorep;
        res = resp || resn;

        sm->setScoreForIndex(sourceRow, score);
95

96
        return res;
97
98
99
    }

public Q_SLOTS:
Waqar Ahmed's avatar
Waqar Ahmed committed
100
    bool setFilterText(const QString &text)
101
    {
Waqar Ahmed's avatar
Waqar Ahmed committed
102
103
104
105
106
107
        // 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;
        }

108
        beginResetModel();
Waqar Ahmed's avatar
Waqar Ahmed committed
109
        pattern = splitted;
Waqar Ahmed's avatar
Waqar Ahmed committed
110
        pathLike = pattern.contains(QLatin1Char('/'));
111
        endResetModel();
Waqar Ahmed's avatar
Waqar Ahmed committed
112
113

        return true;
114
115
    }

116
private:
117
    inline bool filterByPath(const QStringView path, int &score) const
118
119
120
121
    {
        return kfts::fuzzy_match(pattern, path, score);
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
122
    inline bool filterByName(const QString &name, int &score) const
123
124
125
126
    {
        return kfts::fuzzy_match(pattern, name, score);
    }

127
private:
128
    QString pattern;
Waqar Ahmed's avatar
Waqar Ahmed committed
129
    bool pathLike = false;
130
131
};

132
133
class QuickOpenStyleDelegate : public QStyledItemDelegate
{
134
public:
135
    QuickOpenStyleDelegate(QObject *parent = nullptr)
136
        : QStyledItemDelegate(parent)
137
138
    {
    }
139
140
141
142
143
144

    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
145
146
        QString name = index.data(KateQuickOpenModel::FileName).toString();
        QString path = index.data(KateQuickOpenModel::FilePath).toString();
147

148
149
        path.remove(QStringLiteral("/") + name);

150
151
        const QString nameColor = option.palette.color(QPalette::Link).name();

152
153
154
155
156
157
158
159
160
161
        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
162
163
164
165
        int pos = m_filterString.lastIndexOf(QLatin1Char('/'));
        if (pos > -1) {
            ++pos;
            auto pattern = m_filterString.midRef(pos);
166
167
            auto nameFormats = kfts::get_fuzzy_match_formats(pattern, name, 0, fmt);
            formats.append(nameFormats);
168
        } else {
169
170
            auto nameFormats = kfts::get_fuzzy_match_formats(m_filterString, name, 0, fmt);
            formats.append(nameFormats);
171
        }
172
173
174
        QTextCharFormat boldFmt;
        boldFmt.setFontWeight(QFont::Bold);
        boldFmt.setFontPointSize(options.font.pointSize() - 1);
175
        auto pathFormats = kfts::get_fuzzy_match_formats(m_filterString, path, nameLen + space, boldFmt);
176
177
178
179
180
        QTextCharFormat gray;
        gray.setForeground(Qt::gray);
        gray.setFontPointSize(options.font.pointSize() - 1);
        formats.append({nameLen + space, path.length(), gray});
        formats.append(pathFormats);
181
182
183
184
185
186
187
188
189
190
191
192
193

        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);

194
195
196
        // space for icon
        painter->translate(25, 0);

197
        // draw text
198
        kfts::paintItemViewText(painter, QString(name + QStringLiteral(" ") + path), options, formats);
199
200
201
202
203

        painter->restore();
    }

public Q_SLOTS:
204
    void setFilterString(const QString &text)
205
206
207
208
209
210
211
212
    {
        m_filterString = text;
    }

private:
    QString m_filterString;
};

213
214
Q_DECLARE_METATYPE(QPointer<KTextEditor::Document>)

Waqar Ahmed's avatar
Waqar Ahmed committed
215
KateQuickOpen::KateQuickOpen(KateMainWindow *mainWindow)
216
    : QMenu(mainWindow)
217
    , m_mainWindow(mainWindow)
Christoph Cullmann's avatar
Christoph Cullmann committed
218
{
219
    // ensure the components have some proper frame
220
    QVBoxLayout *layout = new QVBoxLayout();
221
    layout->setSpacing(0);
222
    layout->setContentsMargins(4, 4, 4, 4);
223
    setLayout(layout);
224

225
    m_inputLine = new QuickOpenLineEdit(this);
226
    setFocusProxy(m_inputLine);
227

228
    layout->addWidget(m_inputLine);
229

230
231
    m_listView = new QTreeView();
    layout->addWidget(m_listView, 1);
232
    m_listView->setTextElideMode(Qt::ElideLeft);
233
    m_listView->setUniformRowHeights(true);
234

Waqar Ahmed's avatar
Waqar Ahmed committed
235
    m_base_model = new KateQuickOpenModel(this);
236

237
    m_model = new QuickOpenFilterProxyModel(this);
238
    m_model->setFilterRole(Qt::DisplayRole);
239
    m_model->setSortRole(KateQuickOpenModel::Score);
240
241
    m_model->setFilterCaseSensitivity(Qt::CaseInsensitive);
    m_model->setSortCaseSensitivity(Qt::CaseInsensitive);
242
    m_model->setFilterKeyColumn(Qt::DisplayRole);
243

244
245
    m_styleDelegate = new QuickOpenStyleDelegate(this);
    m_listView->setItemDelegate(m_styleDelegate);
246

247
    connect(m_inputLine, &QuickOpenLineEdit::textChanged, m_styleDelegate, &QuickOpenStyleDelegate::setFilterString);
Waqar Ahmed's avatar
Waqar Ahmed committed
248
249
250
251
252
253
    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
        }
254
    });
255
    connect(m_inputLine, &QuickOpenLineEdit::returnPressed, this, &KateQuickOpen::slotReturnPressed);
256
    connect(m_inputLine, &QuickOpenLineEdit::listModeChanged, this, &KateQuickOpen::slotListModeChanged);
257

Laurent Montel's avatar
Laurent Montel committed
258
    connect(m_listView, &QTreeView::activated, this, &KateQuickOpen::slotReturnPressed);
Waqar Ahmed's avatar
Waqar Ahmed committed
259
    connect(m_listView, &QTreeView::clicked, this, &KateQuickOpen::slotReturnPressed); // for single click
260
261

    m_listView->setModel(m_model);
262
    m_listView->setSortingEnabled(true);
263
    m_model->setSourceModel(m_base_model);
264

265
266
    m_inputLine->installEventFilter(this);
    m_listView->installEventFilter(this);
267
268
    m_listView->setHeaderHidden(true);
    m_listView->setRootIsDecorated(false);
Waqar Ahmed's avatar
Waqar Ahmed committed
269
270
271
    m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

    setHidden(true);
272

273
    slotListModeChanged(m_inputLine->listMode());
274
275

    // fill stuff
276
    update();
277
278
}

Waqar Ahmed's avatar
Waqar Ahmed committed
279

280
281
bool KateQuickOpen::eventFilter(QObject *obj, QEvent *event)
{
282
283
    // 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) {
284
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
285
        if (obj == m_inputLine) {
Alexander Lohnau's avatar
Alexander Lohnau committed
286
287
            const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp)
                || (keyEvent->key() == Qt::Key_PageDown);
288
            if (forward2list) {
289
                QCoreApplication::sendEvent(m_listView, event);
290
291
                return true;
            }
292

293
        } else {
Alexander Lohnau's avatar
Alexander Lohnau committed
294
295
            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);
296
            if (forward2input) {
297
                QCoreApplication::sendEvent(m_inputLine, event);
298
299
300
301
                return true;
            }
        }
    }
302

303
    return QWidget::eventFilter(obj, event);
304
305
}

306
307
void KateQuickOpen::reselectFirst()
{
308
    int first = 0;
309
    if (m_mainWindow->viewManager()->sortedViews().size() > 1 && m_model->rowCount() > 1) {
310
        first = 1;
311
    }
312
313

    QModelIndex index = m_model->index(first, 0);
314
315
316
    m_listView->setCurrentIndex(index);
}

317
void KateQuickOpen::update()
318
{
319
    m_base_model->refresh(m_mainWindow);
320
    reselectFirst();
Waqar Ahmed's avatar
Waqar Ahmed committed
321

322
    updateViewGeometry();
Waqar Ahmed's avatar
Waqar Ahmed committed
323
324
    show();
    setFocus();
325
}
Christoph Cullmann's avatar
Christoph Cullmann committed
326

327
void KateQuickOpen::slotReturnPressed()
Christoph Cullmann's avatar
Christoph Cullmann committed
328
{
329
330
331
332
333
334
335
336
337
338
339
    const QModelIndex index = m_listView->model()->index(m_listView->currentIndex().row(), 0);
    const QUrl url = index.data(Qt::UserRole).toUrl();

    if (!url.isValid()) {
        return;
    }

    // save current position before opening new url for location history
    KateViewManager *vm = m_mainWindow->viewManager();
    if (vm) {
        if (KTextEditor::View *v = vm->activeView()) {
340
            vm->addPositionToHistory(v->document()->url(), v->cursorPosition());
341
342
343
        }
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
344
345
346
347
    KTextEditor::View *view = m_mainWindow->wrapper()->openUrl(url);

    const auto strs = m_inputLine->text().split(QLatin1Char(':'));
    if (view && strs.count() > 1) {
348
349
        // helper to convert String => Number
        auto stringToInt = [](const QString &s) {
Waqar Ahmed's avatar
Waqar Ahmed committed
350
            bool ok = false;
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
            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
369
370
371
        }
    }

Waqar Ahmed's avatar
Waqar Ahmed committed
372
    hide();
373
    m_mainWindow->slotWindowActivated();
Waqar Ahmed's avatar
Waqar Ahmed committed
374

375
    // store the new position in location history
Waqar Ahmed's avatar
Waqar Ahmed committed
376
377
    if (view) {
        vm->addPositionToHistory(view->document()->url(), view->cursorPosition());
378
    }
379
}
380

381
void KateQuickOpen::slotListModeChanged(KateQuickOpenModel::List mode)
382
383
{
    m_base_model->setListMode(mode);
384
385
    // this changes things again, needs refresh, let's go all the way
    update();
386
387
}

Waqar Ahmed's avatar
Waqar Ahmed committed
388
389
void KateQuickOpen::updateViewGeometry()
{
Christoph Cullmann's avatar
Christoph Cullmann committed
390
    const QSize centralSize = m_mainWindow->size();
Waqar Ahmed's avatar
Waqar Ahmed committed
391

392
393
    // 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
394

Christoph Cullmann's avatar
Christoph Cullmann committed
395
396
397
398
399
400
401
    const int rowHeight = m_listView->sizeHintForRow(0) == -1 ? 0 : m_listView->sizeHintForRow(0);

    const int width = viewMaxSize.width();

    const QSize viewSize(std::max(300, width), // never go below this
                         std::min(std::max(rowHeight * m_base_model->rowCount() + 2, rowHeight * 6), viewMaxSize.height()));

402
    // Position should be central over window
Christoph Cullmann's avatar
Christoph Cullmann committed
403
404
    const int xPos = std::max(0, (centralSize.width() - viewSize.width()) / 2);
    const int yPos = std::max(0, (centralSize.height() - viewSize.height()) * 1 / 4);
Waqar Ahmed's avatar
Waqar Ahmed committed
405

406
    // fix position and size
407
    const QPoint p(xPos, yPos);
Christoph Cullmann's avatar
Christoph Cullmann committed
408
409
    move(p + m_mainWindow->pos());
    setFixedSize(viewSize);
Waqar Ahmed's avatar
Waqar Ahmed committed
410
}