katequickopen.cpp 12.8 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>
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
27
#include <QBoxLayout>
#include <QCoreApplication>
#include <QDesktopWidget>
Michal Humpula's avatar
Michal Humpula committed
28
29
#include <QEvent>
#include <QFileInfo>
30
31
#include <QHeaderView>
#include <QLabel>
32
#include <QPainter>
Michal Humpula's avatar
Michal Humpula committed
33
#include <QPointer>
34
#include <QSortFilterProxyModel>
Michal Humpula's avatar
Michal Humpula committed
35
#include <QStandardItemModel>
36
37
#include <QStyledItemDelegate>
#include <QTextDocument>
38
#include <QTreeView>
39

40
#include <kfts_fuzzy_match.h>
41
42
43

class QuickOpenFilterProxyModel : public QSortFilterProxyModel
{
44
public:
45
46
47
48
    QuickOpenFilterProxyModel(QObject *parent = nullptr)
        : QSortFilterProxyModel(parent)
    {
    }
49

50
51
52
53
54
    void changeMode(FilterModes m)
    {
        mode = m;
    }

55
protected:
56
    bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override
57
    {
58
59
        int l = sourceLeft.data(KateQuickOpenModel::Score).toInt();
        int r = sourceRight.data(KateQuickOpenModel::Score).toInt();
60
61
62
        return l < r;
    }

63
64
    bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
    {
65
        if (pattern.isEmpty())
66
            return true;
67
        const QString fileName = sourceModel()->index(sourceRow, 0, sourceParent).data().toString();
Waqar Ahmed's avatar
Waqar Ahmed committed
68
        const auto nameAndPath = fileName.splitRef(QStringLiteral("{[split]}"));
69

70
71
        const auto &name = nameAndPath.at(0);
        const auto &path = nameAndPath.at(1);
72
73
74
75
76
77
78
79
        int score = 0;

        bool res = false;
        if (mode == FilterMode::FilterByName) {
            res = filterByName(name, score);
        } else if (mode == FilterMode::FilterByPath) {
            res = filterByPath(path, score);
        } else {
80
            int scorep = 0, scoren = 0;
81
82
83
84
85
86
87
            bool resp = filterByPath(path, scorep);
            bool resn = filterByName(name, scoren);

            // store the score for sorting later
            score = scoren + scorep;
            res = resp || resn;
        }
88

89
        auto idx = sourceModel()->index(sourceRow, 0, sourceParent);
90
        sourceModel()->setData(idx, score, KateQuickOpenModel::Score);
91

92
        return res;
93
94
95
    }

public Q_SLOTS:
96
    void setFilterText(const QString &text)
97
    {
98
        beginResetModel();
99
        pattern = text;
100
        endResetModel();
101
102
    }

103
private:
104
    inline bool filterByPath(const QStringRef &path, int &score) const
105
106
107
108
    {
        return kfts::fuzzy_match(pattern, path, score);
    }

109
    inline bool filterByName(const QStringRef &name, int &score) const
110
111
112
113
    {
        return kfts::fuzzy_match(pattern, name, score);
    }

114
private:
115
    QString pattern;
116
    FilterModes mode;
117
118
};

119
120
class QuickOpenStyleDelegate : public QStyledItemDelegate
{
121
public:
122
    QuickOpenStyleDelegate(QObject *parent = nullptr)
123
        : QStyledItemDelegate(parent)
124
125
    {
    }
126
127
128
129
130
131
132
133
134

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

        QTextDocument doc;

        QString str = index.data().toString();
135

Waqar Ahmed's avatar
Waqar Ahmed committed
136
        auto namePath = str.split(QStringLiteral("{[split]}"));
137
138
139
        QString name = namePath.at(0);
        QString path = namePath.at(1);

140
141
        path.remove(QStringLiteral("/") + name);

142
143
        const QString nameColor = option.palette.color(QPalette::Link).name();

144
        if (mode == FilterMode::FilterByName) {
145
            kfts::to_fuzzy_matched_display_string(m_filterString, name, QStringLiteral("<b style=\"color:%1;\">").arg(nameColor), QStringLiteral("</b>"));
146
147
148
        } else if (mode == FilterMode::FilterByPath) {
            kfts::to_fuzzy_matched_display_string(m_filterString, path, QStringLiteral("<b>"), QStringLiteral("</b>"));
        } else {
149
150
151
152
153
154
155
156
157
158
159
            // check if there's a / separtion in filter string
            // if there is, we use the last part to highlight the
            // filename
            int pos = m_filterString.lastIndexOf(QLatin1Char('/'));
            if (pos > -1) {
                ++pos;
                auto pattern = m_filterString.midRef(pos);
                kfts::to_fuzzy_matched_display_string(pattern, name, QStringLiteral("<b style=\"color:%1;\">").arg(nameColor), QStringLiteral("</b>"));
            } else {
                kfts::to_fuzzy_matched_display_string(m_filterString, name, QStringLiteral("<b style=\"color:%1;\">").arg(nameColor), QStringLiteral("</b>"));
            }
160
161
            kfts::to_fuzzy_matched_display_string(m_filterString, path, QStringLiteral("<b>"), QStringLiteral("</b>"));
        }
162

Waqar Ahmed's avatar
Waqar Ahmed committed
163
        const auto pathFontsize = option.font.pointSize();
164
        doc.setHtml(QStringLiteral("<span style=\"font-size: %1pt;\">").arg(pathFontsize) + name + QStringLiteral("</span>") + QStringLiteral(" &nbsp;") +
165
                    QStringLiteral("<span style=\"color:gray; font-size:%1pt;\">").arg(pathFontsize - 1) + path + QStringLiteral("</span>"));
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
        doc.setDocumentMargin(2);

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

        // draw text
181
182
183
184
        painter->translate(option.rect.x(), option.rect.y());
        if (index.column() == 0) {
            painter->translate(25, 0);
        }
185
186
187
188
189
        doc.drawContents(painter);

        painter->restore();
    }

190
191
192
193
194
    void changeMode(FilterModes m)
    {
        mode = m;
    }

195
public Q_SLOTS:
196
    void setFilterString(const QString &text)
197
198
199
200
201
202
    {
        m_filterString = text;
    }

private:
    QString m_filterString;
203
    FilterModes mode;
204
205
};

206
207
Q_DECLARE_METATYPE(QPointer<KTextEditor::Document>)

Waqar Ahmed's avatar
Waqar Ahmed committed
208
KateQuickOpen::KateQuickOpen(KateMainWindow *mainWindow)
209
    : QMenu(mainWindow)
210
    , m_mainWindow(mainWindow)
Christoph Cullmann's avatar
Christoph Cullmann committed
211
{
212
    // ensure the components have some proper frame
213
    QVBoxLayout *layout = new QVBoxLayout();
214
    layout->setSpacing(0);
215
    layout->setContentsMargins(4, 4, 4, 4);
216
    setLayout(layout);
217

218
    m_inputLine = new QuickOpenLineEdit(this);
219
    setFocusProxy(m_inputLine);
220

221
    layout->addWidget(m_inputLine);
222

223
224
    m_listView = new QTreeView();
    layout->addWidget(m_listView, 1);
225
    m_listView->setTextElideMode(Qt::ElideLeft);
226
    m_listView->setUniformRowHeights(true);
227

228
    m_base_model = new KateQuickOpenModel(m_mainWindow, this);
229

230
    m_model = new QuickOpenFilterProxyModel(this);
231
    m_model->setFilterRole(Qt::DisplayRole);
232
    m_model->setSortRole(KateQuickOpenModel::Score);
233
234
    m_model->setFilterCaseSensitivity(Qt::CaseInsensitive);
    m_model->setSortCaseSensitivity(Qt::CaseInsensitive);
235
    m_model->setFilterKeyColumn(Qt::DisplayRole);
236

237
238
    m_styleDelegate = new QuickOpenStyleDelegate(this);
    m_listView->setItemDelegate(m_styleDelegate);
239

240
241
    connect(m_inputLine, &QuickOpenLineEdit::textChanged, m_model, &QuickOpenFilterProxyModel::setFilterText);
    connect(m_inputLine, &QuickOpenLineEdit::textChanged, m_styleDelegate, &QuickOpenStyleDelegate::setFilterString);
242
243
244
    connect(m_inputLine, &QuickOpenLineEdit::textChanged, this, [this]() {
        m_listView->viewport()->update();
    });
245
246
    connect(m_inputLine, &QuickOpenLineEdit::returnPressed, this, &KateQuickOpen::slotReturnPressed);
    connect(m_inputLine, &QuickOpenLineEdit::filterModeChanged, this, &KateQuickOpen::slotfilterModeChanged);
247
    connect(m_inputLine, &QuickOpenLineEdit::listModeChanged, this, &KateQuickOpen::slotListModeChanged);
Laurent Montel's avatar
Laurent Montel committed
248
249
    connect(m_model, &QSortFilterProxyModel::rowsInserted, this, &KateQuickOpen::reselectFirst);
    connect(m_model, &QSortFilterProxyModel::rowsRemoved, this, &KateQuickOpen::reselectFirst);
250

Laurent Montel's avatar
Laurent Montel committed
251
    connect(m_listView, &QTreeView::activated, this, &KateQuickOpen::slotReturnPressed);
Waqar Ahmed's avatar
Waqar Ahmed committed
252
    connect(m_listView, &QTreeView::clicked, this, &KateQuickOpen::slotReturnPressed); // for single click
253
254

    m_listView->setModel(m_model);
255
    m_listView->setSortingEnabled(true);
256
    m_model->setSourceModel(m_base_model);
257

258
259
    m_inputLine->installEventFilter(this);
    m_listView->installEventFilter(this);
260
261
    m_listView->setHeaderHidden(true);
    m_listView->setRootIsDecorated(false);
Waqar Ahmed's avatar
Waqar Ahmed committed
262
263
264
    m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

    setHidden(true);
265

266
267
268
    // restore settings
    slotfilterModeChanged(m_inputLine->filterMode());
    slotListModeChanged(m_inputLine->listMode());
269
270
}

Waqar Ahmed's avatar
Waqar Ahmed committed
271
272
273
274
275
276
277
278
279
KateQuickOpen::~KateQuickOpen()
{
    KSharedConfig::Ptr cfg = KSharedConfig::openConfig();
    KConfigGroup cg(cfg, "General");

    cg.writeEntry("Quickopen Filter Mode", static_cast<int>(m_filterMode));
    cg.writeEntry("Quickopen List Mode", m_base_model->listMode() == KateQuickOpenModelList::CurrentProject);
}

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) {
286
            const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp) || (keyEvent->key() == Qt::Key_PageDown);
287
            if (forward2list) {
288
                QCoreApplication::sendEvent(m_listView, event);
289
290
                return true;
            }
291

292
            if (keyEvent->key() == Qt::Key_Escape) {
293
294
                m_mainWindow->slotWindowActivated();
                m_inputLine->clear();
295
                keyEvent->accept();
Waqar Ahmed's avatar
Waqar Ahmed committed
296
                hide();
297
                return true;
298
            }
299
        } else {
300
301
            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);
302
            if (forward2input) {
303
                QCoreApplication::sendEvent(m_inputLine, event);
304
305
306
307
                return true;
            }
        }
    }
308
309
310
311
312

    // hide on focus out, if neither input field nor list have focus!
    else if (event->type() == QEvent::FocusOut && !(m_inputLine->hasFocus() || m_listView->hasFocus())) {
        m_mainWindow->slotWindowActivated();
        m_inputLine->clear();
Waqar Ahmed's avatar
Waqar Ahmed committed
313
        hide();
314
315
316
        return true;
    }

317
    return QWidget::eventFilter(obj, event);
318
319
}

320
321
void KateQuickOpen::reselectFirst()
{
322
    int first = 0;
323
    if (m_mainWindow->viewManager()->sortedViews().size() > 1 && m_model->rowCount() > 1)
324
325
326
        first = 1;

    QModelIndex index = m_model->index(first, 0);
327
328
329
    m_listView->setCurrentIndex(index);
}

330
void KateQuickOpen::update()
331
{
332
    m_base_model->refresh();
333
    reselectFirst();
Waqar Ahmed's avatar
Waqar Ahmed committed
334

335
    updateViewGeometry();
Waqar Ahmed's avatar
Waqar Ahmed committed
336
337
    show();
    setFocus();
338
}
Christoph Cullmann's avatar
Christoph Cullmann committed
339

340
void KateQuickOpen::slotReturnPressed()
Christoph Cullmann's avatar
Christoph Cullmann committed
341
{
Waqar Ahmed's avatar
Waqar Ahmed committed
342
    const auto index = m_listView->model()->index(m_listView->currentIndex().row(), 0);
343
    auto url = index.data(Qt::UserRole).toUrl();
344
    m_mainWindow->wrapper()->openUrl(url);
Waqar Ahmed's avatar
Waqar Ahmed committed
345
    hide();
346
347
    m_mainWindow->slotWindowActivated();
    m_inputLine->clear();
348
}
349

350
351
352
353
354
355
356
357
void KateQuickOpen::slotfilterModeChanged(FilterModes mode)
{
    m_filterMode = mode;
    m_model->changeMode(mode);
    m_styleDelegate->changeMode(mode);
    m_model->invalidate();
}

358
void KateQuickOpen::slotListModeChanged(KateQuickOpenModel::List mode)
359
360
361
362
{
    m_base_model->setListMode(mode);
}

Waqar Ahmed's avatar
Waqar Ahmed committed
363
364
void KateQuickOpen::updateViewGeometry()
{
365
    const QSize centralSize = m_mainWindow->size();
Waqar Ahmed's avatar
Waqar Ahmed committed
366

367
368
    // 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
369
370
371
372
373

    const int rowHeight = m_listView->sizeHintForRow(0) == -1 ? 0 : m_listView->sizeHintForRow(0);

    const int width = viewMaxSize.width();

374
375
    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()));
Waqar Ahmed's avatar
Waqar Ahmed committed
376

377
    // Position should be central over window
378
379
    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
380

381
382
    const QPoint p(xPos, yPos);
    move(p + m_mainWindow->pos());
Waqar Ahmed's avatar
Waqar Ahmed committed
383
384
385
386
387
388
389
390

    QPointer<QPropertyAnimation> animation = new QPropertyAnimation(this, "size");
    animation->setDuration(150);
    animation->setStartValue(this->size());
    animation->setEndValue(viewSize);

    animation->start();
}