katequickopen.cpp 10.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>
20
#include <KLineEdit>
Michal Humpula's avatar
Michal Humpula committed
21
#include <KLocalizedString>
22
#include <KPluginFactory>
Michal Humpula's avatar
Michal Humpula committed
23

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

39
#include <kfts_fuzzy_match.h>
40
41
42
43

class QuickOpenFilterProxyModel : public QSortFilterProxyModel
{

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

48
protected:
49
    bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override
50
    {
51
52
        int l = sourceLeft.data(KateQuickOpenModel::Score).toInt();
        int r = sourceRight.data(KateQuickOpenModel::Score).toInt();
53
54
55
        return l < r;
    }

56
57
    bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
    {
58
        if (pattern.isEmpty())
59
            return true;
60
        const QString fileName = sourceModel()->index(sourceRow, 0, sourceParent).data().toString();
Waqar Ahmed's avatar
Waqar Ahmed committed
61
62
63
        const auto nameAndPath = fileName.splitRef(QStringLiteral("{[split]}"));
        const auto name = nameAndPath.at(0);
        const auto path = nameAndPath.at(1);
64
        // match
Waqar Ahmed's avatar
Waqar Ahmed committed
65
66
67
68
        int score1 = 0;
        auto res = kfts::fuzzy_match(pattern, name, score1);
        int score2 = 0;
        auto res1 = kfts::fuzzy_match(pattern, path, score2);
69
70

        // store the score for sorting later
71
        auto idx = sourceModel()->index(sourceRow, 0, sourceParent);
Waqar Ahmed's avatar
Waqar Ahmed committed
72
        sourceModel()->setData(idx, score1 + score2, KateQuickOpenModel::Score);
73

Waqar Ahmed's avatar
Waqar Ahmed committed
74
        return res || res1;
75
76
77
78
79
    }

public Q_SLOTS:
    void setFilterText(const QString& text)
    {
80
        beginResetModel();
81
        pattern = text;
82
        endResetModel();
83
84
85
    }

private:
86
    QString pattern;
87
88
};

89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
class QuickOpenStyleDelegate : public QStyledItemDelegate {

public:
    QuickOpenStyleDelegate(QObject* parent = nullptr)
        : QStyledItemDelegate(parent)
    {}

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

Waqar Ahmed's avatar
Waqar Ahmed committed
105
106
107
108
109
        auto namePath = str.split(QStringLiteral("{[split]}"));
        auto name = namePath.at(0);
        auto path = namePath.at(1);
        kfts::to_fuzzy_matched_display_string(m_filterString, name, QStringLiteral("<b>"), QStringLiteral("</b>"));
        kfts::to_fuzzy_matched_display_string(m_filterString, path, QStringLiteral("<b>"), QStringLiteral("</b>"));
110

Waqar Ahmed's avatar
Waqar Ahmed committed
111
112
        const auto pathFontsize = option.font.pointSize();
        doc.setHtml(QStringLiteral("<span style=\"font-size: %1pt;\">").arg(pathFontsize + 1) + name + QStringLiteral("</span>") + QStringLiteral("<br>") + QStringLiteral("<span style=\"color: gray; font-size: %1pt;\">").arg(pathFontsize) + path + QStringLiteral("</span>"));
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
        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
Waqar Ahmed's avatar
Waqar Ahmed committed
128
        painter->translate(option.rect.x() + 5, option.rect.y());
129
130
131
132
133
134
135
136
137
138
139
140
141
        doc.drawContents(painter);

        painter->restore();
    }

public Q_SLOTS:
    void setFilterString(const QString& text)
    {
        m_filterString = text;
    }

private:
    QString m_filterString;
Waqar Ahmed's avatar
Waqar Ahmed committed
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159

    // QAbstractItemDelegate interface
public:
    QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        QSize size = this->QStyledItemDelegate::sizeHint(option, index);
        static int height = -1;
        if (height > -1) {
            size.setHeight(height);
            return size;
        }

        QFontMetrics metrics(option.font);
        QRect outRect = metrics.boundingRect(QRect(QPoint(0, 0), size), Qt::AlignLeft, option.text);
        height = outRect.height() * 2 + 4;
        size.setHeight(outRect.height() * 2 + 4);
        return size;
    }
160
161
};

162
163
Q_DECLARE_METATYPE(QPointer<KTextEditor::Document>)

Waqar Ahmed's avatar
Waqar Ahmed committed
164
165
KateQuickOpen::KateQuickOpen(KateMainWindow *mainWindow)
    : QWidget(mainWindow)
166
    , m_mainWindow(mainWindow)
Christoph Cullmann's avatar
Christoph Cullmann committed
167
{
168
    QVBoxLayout *layout = new QVBoxLayout();
169
    layout->setSpacing(0);
Laurent Montel's avatar
Laurent Montel committed
170
    layout->setContentsMargins(0, 0, 0, 0);
171
    setLayout(layout);
172

173
    m_inputLine = new KLineEdit();
174
    setFocusProxy(m_inputLine);
Michal Humpula's avatar
Michal Humpula committed
175
    m_inputLine->setPlaceholderText(i18n("Quick Open Search"));
176

177
    layout->addWidget(m_inputLine);
178

179
180
    m_listView = new QTreeView();
    layout->addWidget(m_listView, 1);
181
182
    m_listView->setTextElideMode(Qt::ElideLeft);

183
    m_base_model = new KateQuickOpenModel(m_mainWindow, this);
184

185
    m_model = new QuickOpenFilterProxyModel(this);
186
    m_model->setFilterRole(Qt::DisplayRole);
187
    m_model->setSortRole(KateQuickOpenModel::Score);
188
189
    m_model->setFilterCaseSensitivity(Qt::CaseInsensitive);
    m_model->setSortCaseSensitivity(Qt::CaseInsensitive);
190
    m_model->setFilterKeyColumn(Qt::DisplayRole);
191

192
193
194
    QuickOpenStyleDelegate* delegate = new QuickOpenStyleDelegate(this);
    m_listView->setItemDelegateForColumn(0, delegate);

195
    connect(m_inputLine, &KLineEdit::textChanged, m_model, &QuickOpenFilterProxyModel::setFilterText);
196
197
    connect(m_inputLine, &KLineEdit::textChanged, delegate, &QuickOpenStyleDelegate::setFilterString);
    connect(m_inputLine, &KLineEdit::textChanged, this, [this](){ m_listView->viewport()->update(); });
Laurent Montel's avatar
Laurent Montel committed
198
199
200
    connect(m_inputLine, &KLineEdit::returnPressed, this, &KateQuickOpen::slotReturnPressed);
    connect(m_model, &QSortFilterProxyModel::rowsInserted, this, &KateQuickOpen::reselectFirst);
    connect(m_model, &QSortFilterProxyModel::rowsRemoved, this, &KateQuickOpen::reselectFirst);
201

Laurent Montel's avatar
Laurent Montel committed
202
    connect(m_listView, &QTreeView::activated, this, &KateQuickOpen::slotReturnPressed);
Waqar Ahmed's avatar
Waqar Ahmed committed
203
    connect(m_listView, &QTreeView::clicked, this, &KateQuickOpen::slotReturnPressed); // for single click
204
205

    m_listView->setModel(m_model);
206
    m_listView->setSortingEnabled(true);
207
    m_model->setSourceModel(m_base_model);
208

209
210
    m_inputLine->installEventFilter(this);
    m_listView->installEventFilter(this);
211
212
    m_listView->setHeaderHidden(true);
    m_listView->setRootIsDecorated(false);
Waqar Ahmed's avatar
Waqar Ahmed committed
213
214
215
    m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

    setHidden(true);
216
217
}

218
219
bool KateQuickOpen::eventFilter(QObject *obj, QEvent *event)
{
220
221
    // 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) {
222
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
223
        if (obj == m_inputLine) {
224
            const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp) || (keyEvent->key() == Qt::Key_PageDown);
225
            if (forward2list) {
226
                QCoreApplication::sendEvent(m_listView, event);
227
228
                return true;
            }
229

230
            if (keyEvent->key() == Qt::Key_Escape) {
231
232
                m_mainWindow->slotWindowActivated();
                m_inputLine->clear();
233
                keyEvent->accept();
Waqar Ahmed's avatar
Waqar Ahmed committed
234
                hide();
235
                return true;
236
            }
237
        } else {
238
239
            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);
240
            if (forward2input) {
241
                QCoreApplication::sendEvent(m_inputLine, event);
242
243
244
245
                return true;
            }
        }
    }
246
247
248
249
250

    // 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
251
        hide();
252
253
254
        return true;
    }

255
    return QWidget::eventFilter(obj, event);
256
257
}

258
259
void KateQuickOpen::reselectFirst()
{
260
    int first = 0;
261
    if (m_mainWindow->viewManager()->sortedViews().size() > 1 && m_model->rowCount() > 1)
262
263
264
        first = 1;

    QModelIndex index = m_model->index(first, 0);
265
266
267
    m_listView->setCurrentIndex(index);
}

268
void KateQuickOpen::update()
269
{
270
    m_base_model->refresh();
271
    m_listView->resizeColumnToContents(0);
272
    reselectFirst();
Waqar Ahmed's avatar
Waqar Ahmed committed
273
274
275
276
    updateViewGeometry();

    show();
    setFocus();
277
}
Christoph Cullmann's avatar
Christoph Cullmann committed
278

279
void KateQuickOpen::slotReturnPressed()
Christoph Cullmann's avatar
Christoph Cullmann committed
280
{
Waqar Ahmed's avatar
Waqar Ahmed committed
281
    const auto index = m_listView->model()->index(m_listView->currentIndex().row(), 0);
282
    auto url = index.data(Qt::UserRole).toUrl();
283
    m_mainWindow->wrapper()->openUrl(url);
Waqar Ahmed's avatar
Waqar Ahmed committed
284
    hide();
285
286
    m_mainWindow->slotWindowActivated();
    m_inputLine->clear();
287
}
288

289
void KateQuickOpen::setListMode(KateQuickOpenModel::List mode)
290
291
292
293
{
    m_base_model->setListMode(mode);
}

294
KateQuickOpenModel::List KateQuickOpen::listMode() const
295
296
297
{
    return m_base_model->listMode();
}
Waqar Ahmed's avatar
Waqar Ahmed committed
298
299
300

void KateQuickOpen::updateViewGeometry()
{
301
    const QSize centralSize = m_mainWindow->size();
Waqar Ahmed's avatar
Waqar Ahmed committed
302
303
304
305
306
307
308
309
310
311
312

    // width: 1/3 of editor, height: 1/2 of editor
    const QSize viewMaxSize(centralSize.width() / 3, centralSize.height() / 2);

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

    int frameWidth = this->frameSize().width();
    frameWidth = frameWidth > centralSize.width() / 3 ? centralSize.width() / 3 : frameWidth;

    const int width = viewMaxSize.width();

313
    const QSize viewSize(width < 300 ? 300 : width, // never go below this
Waqar Ahmed's avatar
Waqar Ahmed committed
314
315
                         std::min(std::max(rowHeight * m_base_model->rowCount() + 2 * frameWidth, rowHeight * 6), viewMaxSize.height()));

316
317
318
    // Position should be central over the editor area
    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
319
320
321
322
323
324
325
326
327
328

    move(xPos, yPos);

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

    animation->start();
}