locatorwidget.cpp 17.6 KB
Newer Older
1
/**************************************************************************
con's avatar
con committed
2
3
4
**
** This file is part of Qt Creator
**
hjk's avatar
hjk committed
5
** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies).
con's avatar
con committed
6
**
7
** Contact: Nokia Corporation (qt-info@nokia.com)
con's avatar
con committed
8
**
9
** Commercial Usage
10
**
11
12
13
14
** Licensees holding valid Qt Commercial licenses may use this file in
** accordance with the Qt Commercial License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Nokia.
15
**
16
** GNU Lesser General Public License Usage
17
**
18
19
20
21
22
23
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
24
**
25
** If you are unsure which license is appropriate for your use, please
hjk's avatar
hjk committed
26
** contact the sales department at http://qt.nokia.com/contact.
con's avatar
con committed
27
**
28
**************************************************************************/
con's avatar
con committed
29
30
31

#include <qglobal.h>

con's avatar
con committed
32
namespace Locator {
con's avatar
con committed
33
34
35
36
struct FilterEntry;
}

QT_BEGIN_NAMESPACE
con's avatar
con committed
37
unsigned int qHash(const Locator::FilterEntry &entry);
con's avatar
con committed
38
39
QT_END_NAMESPACE

40
#include "locatorwidget.h"
con's avatar
con committed
41
#include "locatorplugin.h"
con's avatar
con committed
42
#include "locatorconstants.h"
con's avatar
con committed
43
44
45
46
47
48

#include <extensionsystem/pluginmanager.h>
#include <coreplugin/icore.h>
#include <coreplugin/modemanager.h>
#include <coreplugin/coreconstants.h>
#include <coreplugin/fileiconprovider.h>
49
#include <utils/filterlineedit.h>
hjk's avatar
hjk committed
50
#include <utils/qtcassert.h>
51
#include <qtconcurrent/runextensions.h>
con's avatar
con committed
52

53
#include <QtCore/QtConcurrentRun>
con's avatar
con committed
54
55
56
57
#include <QtCore/QFileInfo>
#include <QtCore/QFile>
#include <QtCore/QTimer>
#include <QtCore/QSettings>
58
#include <QtCore/QEvent>
con's avatar
con committed
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <QtGui/QAction>
#include <QtGui/QApplication>
#include <QtGui/QContextMenuEvent>
#include <QtGui/QHBoxLayout>
#include <QtGui/QHeaderView>
#include <QtGui/QKeyEvent>
#include <QtGui/QLabel>
#include <QtGui/QLineEdit>
#include <QtGui/QMenu>
#include <QtGui/QMouseEvent>
#include <QtGui/QPushButton>
#include <QtGui/QScrollBar>
#include <QtGui/QTreeView>

con's avatar
con committed
73
74
Q_DECLARE_METATYPE(Locator::ILocatorFilter*);
Q_DECLARE_METATYPE(Locator::FilterEntry);
con's avatar
con committed
75

con's avatar
con committed
76
namespace Locator {
con's avatar
con committed
77
78
namespace Internal {

con's avatar
con committed
79
/* A model to represent the Locator results. */
con's avatar
con committed
80
class LocatorModel : public QAbstractListModel
con's avatar
con committed
81
82
{
public:
con's avatar
con committed
83
    LocatorModel(QObject *parent = 0)
con's avatar
con committed
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
        : QAbstractListModel(parent)
//        , mDisplayCount(64)
    {}

    int rowCount(const QModelIndex &parent = QModelIndex()) const;
    int columnCount(const QModelIndex &parent = QModelIndex()) const;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;

    void setEntries(const QList<FilterEntry> &entries);
    //void setDisplayCount(int count);

private:
    mutable QList<FilterEntry> mEntries;
    //int mDisplayCount;
};

class CompletionList : public QTreeView
{
public:
    CompletionList(QWidget *parent = 0);

    void updatePreferredSize();
    QSize preferredSize() const { return m_preferredSize; }

Tobias Hunger's avatar
Tobias Hunger committed
108
109
110
111
112
113
114
#if defined(Q_OS_WIN)
    void focusOutEvent (QFocusEvent * event)  {
        if (event->reason() == Qt::ActiveWindowFocusReason)
            hide();
    }
#endif

con's avatar
con committed
115
116
117
118
119
private:
    QSize m_preferredSize;
};

} // namespace Internal
con's avatar
con committed
120
} // namespace Locator
con's avatar
con committed
121

con's avatar
con committed
122
123
using namespace Locator;
using namespace Locator::Internal;
con's avatar
con committed
124
125
126
127
128
129
130
131
132
133
134

QT_BEGIN_NAMESPACE
uint qHash(const FilterEntry &entry)
{
    if (entry.internalData.canConvert(QVariant::String))
        return qHash(entry.internalData.toString());
    return qHash(entry.internalData.constData());
}
QT_END_NAMESPACE


con's avatar
con committed
135
// =========== LocatorModel ===========
con's avatar
con committed
136

con's avatar
con committed
137
int LocatorModel::rowCount(const QModelIndex & /* parent */) const
con's avatar
con committed
138
139
140
141
{
    return mEntries.size();
}

con's avatar
con committed
142
int LocatorModel::columnCount(const QModelIndex &parent) const
con's avatar
con committed
143
144
145
146
147
{
    return parent.isValid() ? 0 : 2;
}

/*!
con's avatar
con committed
148
 * When asked for the icon via Qt::DecorationRole, the LocatorModel lazily
con's avatar
con committed
149
150
151
152
 * resolves and caches the Greehouse-specific file icon when
 * FilterEntry::resolveFileIcon is true. FilterEntry::internalData is assumed
 * to be the filename.
 */
con's avatar
con committed
153
QVariant LocatorModel::data(const QModelIndex &index, int role) const
con's avatar
con committed
154
155
156
157
{
    if (!index.isValid() || index.row() >= mEntries.size())
        return QVariant();

158
    if (role == Qt::DisplayRole  || role == Qt::ToolTipRole) {
con's avatar
con committed
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
        if (index.column() == 0) {
            return mEntries.at(index.row()).displayName;
        } else if (index.column() == 1) {
            return mEntries.at(index.row()).extraInfo;
        }
    } else if (role == Qt::DecorationRole && index.column() == 0) {
        FilterEntry &entry = mEntries[index.row()];
        if (entry.resolveFileIcon && entry.displayIcon.isNull()) {
            entry.resolveFileIcon = false;
            entry.displayIcon =
                 Core::FileIconProvider::instance()->icon(QFileInfo(entry.internalData.toString()));
        }
        return entry.displayIcon;
    } else if (role == Qt::ForegroundRole && index.column() == 1) {
        return Qt::darkGray;
    } else if (role == Qt::UserRole) {
        return qVariantFromValue(mEntries.at(index.row()));
    }

    return QVariant();
}

con's avatar
con committed
181
void LocatorModel::setEntries(const QList<FilterEntry> &entries)
con's avatar
con committed
182
183
184
185
186
{
    mEntries = entries;
    reset();
}
#if 0
con's avatar
con committed
187
void LocatorModel::setDisplayCount(int count)
con's avatar
con committed
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
{
    // TODO: This method is meant to be used for increasing the number of items displayed at the
    // user's request. There is however no way yet for the user to request this.
    if (count == mDisplayCount)
        return;

    const int displayedOld = qMin(mDisplayCount, mEntries.size());
    const int displayedNew = qMin(count, mEntries.size());

    if (displayedNew < displayedOld)
        beginRemoveRows(QModelIndex(), displayedNew - 1, displayedOld - 1);
    else if (displayedNew > displayedOld)
        beginInsertRows(QModelIndex(), displayedOld - 1, displayedNew - 1);

    mDisplayCount = count;

    if (displayedNew < displayedOld)
        endRemoveRows();
    else if (displayedNew > displayedOld)
        endInsertRows();
}
#endif

// =========== CompletionList ===========

CompletionList::CompletionList(QWidget *parent)
    : QTreeView(parent)
{
    setRootIsDecorated(false);
    setUniformRowHeights(true);
    setMaximumWidth(900);
    header()->hide();
    header()->setStretchLastSection(true);
    // This is too slow when done on all results
    //header()->setResizeMode(QHeaderView::ResizeToContents);
    setWindowFlags(Qt::ToolTip);
con's avatar
con committed
224
225
226
227
228
229
#ifdef Q_WS_MAC
    if (horizontalScrollBar())
        horizontalScrollBar()->setAttribute(Qt::WA_MacMiniSize);
    if (verticalScrollBar())
        verticalScrollBar()->setAttribute(Qt::WA_MacMiniSize);
#endif
con's avatar
con committed
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
}

void CompletionList::updatePreferredSize()
{
    //header()->setStretchLastSection(false);
    //updateGeometries();

    const QStyleOptionViewItem &option = viewOptions();
    const QSize shint = itemDelegate()->sizeHint(option, model()->index(0, 0));
#if 0
    const int visibleItems = model()->rowCount();

    // TODO: Look into enabling preferred height as well. Current problem is that this doesn't
    // work nicely from the user perspective if the list is popping up instead of down.
    //const int h = shint.height() * visibleItems;

    const QScrollBar *vscroll = verticalScrollBar();
    int preferredWidth = header()->length() + frameWidth() * 2
                         + (vscroll->isVisibleTo(this) ? vscroll->width() : 0);
    const int diff = preferredWidth - width();

    // Avoid resizing the list widget when there are no results or when the preferred size is
    // only a little smaller than our current size
    if (visibleItems == 0 || (diff > -100 && diff < 0))
        preferredWidth = width();
#endif

257
    m_preferredSize = QSize(730, //qMax(600, preferredWidth),
con's avatar
con committed
258
259
260
261
                            shint.height() * 17 + frameWidth() * 2);
    //header()->setStretchLastSection(true);
}

262
// =========== LocatorWidget ===========
con's avatar
con committed
263

con's avatar
con committed
264
265
LocatorWidget::LocatorWidget(LocatorPlugin *qop) :
     m_locatorPlugin(qop),
con's avatar
con committed
266
     m_locatorModel(new LocatorModel(this)),
con's avatar
con committed
267
268
269
270
     m_completionList(new CompletionList(this)),
     m_filterMenu(new QMenu(this)),
     m_refreshAction(new QAction(tr("Refresh"), this)),
     m_configureAction(new QAction(tr("Configure..."), this)),
271
     m_fileLineEdit(new Utils::FilterLineEdit)
con's avatar
con committed
272
{
273
    // Explicitly hide the completion list popup.
con's avatar
con committed
274
275
    m_completionList->hide();

276
    setFocusProxy(m_fileLineEdit);
277
    setWindowTitle(tr("Locate..."));
con's avatar
con committed
278
279
280
281
282
283
284
285
286
287
288
289
    resize(200, 90);
    QSizePolicy sizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
    sizePolicy.setHorizontalStretch(0);
    sizePolicy.setVerticalStretch(0);
    setSizePolicy(sizePolicy);
    setMinimumSize(QSize(200, 0));

    QHBoxLayout *layout = new QHBoxLayout(this);
    setLayout(layout);
    layout->setMargin(0);
    layout->addWidget(m_fileLineEdit);

290
    setWindowIcon(QIcon(QLatin1String(":/locator/images/locator.png")));
con's avatar
con committed
291
    QPixmap image(Core::Constants::ICON_MAGNIFIER);
292
    m_fileLineEdit->setButtonPixmap(Utils::FancyLineEdit::Left, image);
293
    m_fileLineEdit->setPlaceholderText(tr("Type to locate"));
294
    m_fileLineEdit->setButtonToolTip(Utils::FancyLineEdit::Left, tr("Options"));
con's avatar
con committed
295
    m_fileLineEdit->setFocusPolicy(Qt::ClickFocus);
296
    m_fileLineEdit->setButtonVisible(Utils::FancyLineEdit::Left, true);
297
    // We set click focus since otherwise you will always get two popups
298
    m_fileLineEdit->setButtonFocusPolicy(Utils::FancyLineEdit::Left, Qt::ClickFocus);
299
    m_fileLineEdit->setAttribute(Qt::WA_MacShowFocusRect, false);
con's avatar
con committed
300
301
302
303

    m_fileLineEdit->installEventFilter(this);
    this->installEventFilter(this);

con's avatar
con committed
304
    m_completionList->setModel(m_locatorModel);
con's avatar
con committed
305
306
307
308
309
310
311
    m_completionList->header()->resizeSection(0, 300);
    m_completionList->updatePreferredSize();
    m_completionList->resize(m_completionList->preferredSize());

    m_filterMenu->addAction(m_refreshAction);
    m_filterMenu->addAction(m_configureAction);

312
    m_fileLineEdit->setButtonMenu(Utils::FancyLineEdit::Left, m_filterMenu);
con's avatar
con committed
313

con's avatar
con committed
314
    connect(m_refreshAction, SIGNAL(triggered()), m_locatorPlugin, SLOT(refresh()));
con's avatar
con committed
315
    connect(m_configureAction, SIGNAL(triggered()), this, SLOT(showConfigureDialog()));
316
    connect(m_fileLineEdit, SIGNAL(textChanged(const QString&)),
317
        this, SLOT(showPopup()));
con's avatar
con committed
318
319
    connect(m_completionList, SIGNAL(activated(QModelIndex)),
            this, SLOT(acceptCurrentEntry()));
320
321
322
323
324
325
326
327

    m_entriesWatcher = new QFutureWatcher<FilterEntry>(this);
    connect(m_entriesWatcher, SIGNAL(finished()), SLOT(updateEntries()));

    m_showPopupTimer = new QTimer(this);
    m_showPopupTimer->setInterval(100);
    m_showPopupTimer->setSingleShot(true);
    connect(m_showPopupTimer, SIGNAL(timeout()), SLOT(showPopupNow()));
con's avatar
con committed
328
329
}

330
void LocatorWidget::updateFilterList()
con's avatar
con committed
331
332
{
    m_filterMenu->clear();
con's avatar
con committed
333
    foreach (ILocatorFilter *filter, m_locatorPlugin->filters()) {
334
        if (!filter->shortcutString().isEmpty() && !filter->isHidden()) {
335
            QAction *action = m_filterMenu->addAction(filter->displayName(), this, SLOT(filterSelected()));
con's avatar
con committed
336
337
338
339
340
341
342
343
            action->setData(qVariantFromValue(filter));
        }
    }
    m_filterMenu->addSeparator();
    m_filterMenu->addAction(m_refreshAction);
    m_filterMenu->addAction(m_configureAction);
}

344
bool LocatorWidget::eventFilter(QObject *obj, QEvent *event)
con's avatar
con committed
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
{
    if (obj == m_fileLineEdit && event->type() == QEvent::KeyPress) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
        switch (keyEvent->key()) {
        case Qt::Key_Up:
        case Qt::Key_Down:
        case Qt::Key_PageUp:
        case Qt::Key_PageDown:
            showCompletionList();
            QApplication::sendEvent(m_completionList, event);
            return true;
        case Qt::Key_Enter:
        case Qt::Key_Return:
            acceptCurrentEntry();
            return true;
        case Qt::Key_Escape:
            m_completionList->hide();
            return true;
        default:
            break;
        }
    } else if (obj == m_fileLineEdit && event->type() == QEvent::FocusOut) {
Tobias Hunger's avatar
Tobias Hunger committed
367
368
369
370
371
372
#if defined(Q_OS_WIN)
        QFocusEvent *fev = static_cast<QFocusEvent*>(event);
        if (fev->reason() != Qt::ActiveWindowFocusReason ||
            (fev->reason() == Qt::ActiveWindowFocusReason && !m_completionList->isActiveWindow()))
#endif
            m_completionList->hide();
con's avatar
con committed
373
    } else if (obj == m_fileLineEdit && event->type() == QEvent::FocusIn) {
374
        showPopup();
con's avatar
con committed
375
376
377
378
379
380
381
382
383
384
385
    } else if (obj == this && event->type() == QEvent::ShortcutOverride) {
        QKeyEvent *ke = static_cast<QKeyEvent *>(event);
        if (ke->key() == Qt::Key_Escape && !ke->modifiers()) {
            event->accept();
            QTimer::singleShot(0, Core::ModeManager::instance(), SLOT(setFocusToCurrentMode()));
            return true;
        }
    }
    return QWidget::eventFilter(obj, event);
}

386
void LocatorWidget::showCompletionList()
con's avatar
con committed
387
388
389
390
391
392
393
394
{
    const int border = m_completionList->frameWidth();
    const QSize size = m_completionList->preferredSize();
    const QRect rect(mapToGlobal(QPoint(-border, -size.height() - border)), size);
    m_completionList->setGeometry(rect);
    m_completionList->show();
}

395
void LocatorWidget::showPopup()
con's avatar
con committed
396
{
397
398
399
400
401
402
    m_showPopupTimer->start();
}

void LocatorWidget::showPopupNow()
{
    m_showPopupTimer->stop();
403
    updateCompletionList(m_fileLineEdit->text());
con's avatar
con committed
404
405
406
    showCompletionList();
}

407
QList<ILocatorFilter*> LocatorWidget::filtersFor(const QString &text, QString &searchText)
con's avatar
con committed
408
{
con's avatar
con committed
409
    QList<ILocatorFilter*> filters = m_locatorPlugin->filters();
410
    const int whiteSpace = text.indexOf(QLatin1Char(' '));
con's avatar
con committed
411
412
413
414
415
    QString prefix;
    if (whiteSpace >= 0)
        prefix = text.left(whiteSpace);
    if (!prefix.isEmpty()) {
        prefix = prefix.toLower();
416
        foreach (ILocatorFilter *filter, filters) {
con's avatar
con committed
417
418
            if (prefix == filter->shortcutString()) {
                searchText = text.mid(whiteSpace+1);
419
                return QList<ILocatorFilter*>() << filter;
con's avatar
con committed
420
421
422
423
            }
        }
    }
    searchText = text;
424
425
    QList<ILocatorFilter*> activeFilters;
    foreach (ILocatorFilter *filter, filters)
426
        if (filter->isIncludedByDefault())
con's avatar
con committed
427
428
429
430
            activeFilters << filter;
    return activeFilters;
}

431
static void filter_helper(QFutureInterface<FilterEntry> &entries, QList<ILocatorFilter *> filters, QString searchText)
con's avatar
con committed
432
433
434
{
    QSet<FilterEntry> alreadyAdded;
    const bool checkDuplicates = (filters.size() > 1);
435
    foreach (ILocatorFilter *filter, filters) {
436
437
438
        if (entries.isCanceled())
            break;

con's avatar
con committed
439
440
441
        foreach (const FilterEntry &entry, filter->matchesFor(searchText)) {
            if (checkDuplicates && alreadyAdded.contains(entry))
                continue;
442
443
            //entries.append(entry);
            entries.reportResult(entry);
con's avatar
con committed
444
445
446
447
            if (checkDuplicates)
                alreadyAdded.insert(entry);
        }
    }
448
449
450
451
452
453
454
}

void LocatorWidget::updateCompletionList(const QString &text)
{
    QString searchText;
    const QList<ILocatorFilter*> filters = filtersFor(text, searchText);

455
    // cancel the old future
456
    m_entriesWatcher->future().cancel();
457
458

    QFuture<FilterEntry> future = QtConcurrent::run(filter_helper, filters, searchText);
459
    m_entriesWatcher->setFuture(future);
460
    future.waitForFinished();
461
462
463
464
465
466
467
468
}

void LocatorWidget::updateEntries()
{
    if (m_entriesWatcher->future().isCanceled())
        return;

    const QList<FilterEntry> entries = m_entriesWatcher->future().results();
con's avatar
con committed
469
470
471
    m_locatorModel->setEntries(entries);
    if (m_locatorModel->rowCount() > 0) {
        m_completionList->setCurrentIndex(m_locatorModel->index(0, 0));
con's avatar
con committed
472
473
474
475
476
477
    }
#if 0
    m_completionList->updatePreferredSize();
#endif
}

478
void LocatorWidget::acceptCurrentEntry()
con's avatar
con committed
479
480
481
482
483
484
{
    if (!m_completionList->isVisible())
        return;
    const QModelIndex index = m_completionList->currentIndex();
    if (!index.isValid())
        return;
con's avatar
con committed
485
    const FilterEntry entry = m_locatorModel->data(index, Qt::UserRole).value<FilterEntry>();
con's avatar
con committed
486
487
488
489
    m_completionList->hide();
    entry.filter->accept(entry);
}

490
void LocatorWidget::show(const QString &text, int selectionStart, int selectionLength)
con's avatar
con committed
491
{
492
493
    if (!text.isEmpty())
        m_fileLineEdit->setText(text);
494
495
496
497
498
    if (!m_fileLineEdit->hasFocus())
        m_fileLineEdit->setFocus();
    else
        showPopup();

con's avatar
con committed
499
    if (selectionStart >= 0) {
con's avatar
con committed
500
        m_fileLineEdit->setSelection(selectionStart, selectionLength);
con's avatar
con committed
501
502
503
        if (selectionLength == 0) // make sure the cursor is at the right position (Mac-vs.-rest difference)
            m_fileLineEdit->setCursorPosition(selectionStart);
    } else {
504
        m_fileLineEdit->selectAll();
con's avatar
con committed
505
    }
con's avatar
con committed
506
507
}

508
void LocatorWidget::filterSelected()
con's avatar
con committed
509
{
510
    QString searchText = tr("<type here>");
con's avatar
con committed
511
    QAction *action = qobject_cast<QAction*>(sender());
hjk's avatar
hjk committed
512
    QTC_ASSERT(action, return);
513
    ILocatorFilter *filter = action->data().value<ILocatorFilter*>();
hjk's avatar
hjk committed
514
    QTC_ASSERT(filter, return);
515
516
517
518
    QString currentText = m_fileLineEdit->text().trimmed();
    // add shortcut string at front or replace existing shortcut string
    if (!currentText.isEmpty()) {
        searchText = currentText;
con's avatar
con committed
519
        foreach (ILocatorFilter *otherfilter, m_locatorPlugin->filters()) {
520
            if (currentText.startsWith(otherfilter->shortcutString() + QLatin1Char(' '))) {
521
522
523
524
525
                searchText = currentText.mid(otherfilter->shortcutString().length()+1);
                break;
            }
        }
    }
526
    show(filter->shortcutString() + QLatin1Char(' ') + searchText,
con's avatar
con committed
527
         filter->shortcutString().length() + 1,
528
         searchText.length());
con's avatar
con committed
529
530
531
532
    updateCompletionList(m_fileLineEdit->text());
    m_fileLineEdit->setFocus();
}

533
void LocatorWidget::showEvent(QShowEvent *event)
con's avatar
con committed
534
535
536
537
{
    QWidget::showEvent(event);
}

538
void LocatorWidget::showConfigureDialog()
con's avatar
con committed
539
{
con's avatar
con committed
540
    Core::ICore::instance()->showOptionsDialog(Constants::LOCATOR_CATEGORY,
con's avatar
con committed
541
542
          Constants::FILTER_OPTIONS_PAGE);
}