locatorwidget.cpp 30.5 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
con's avatar
con committed
2
**
3 4
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
con's avatar
con committed
5
**
hjk's avatar
hjk committed
6
** This file is part of Qt Creator.
con's avatar
con committed
7
**
hjk's avatar
hjk committed
8 9 10 11
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
12 13 14
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
15
**
16 17 18 19 20 21 22
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
con's avatar
con committed
23
**
hjk's avatar
hjk committed
24
****************************************************************************/
con's avatar
con committed
25

hjk's avatar
hjk committed
26
#include "locator.h"
27
#include "locatorwidget.h"
con's avatar
con committed
28
#include "locatorconstants.h"
29
#include "locatorsearchutils.h"
30
#include "ilocatorfilter.h"
con's avatar
con committed
31 32 33

#include <coreplugin/icore.h>
#include <coreplugin/modemanager.h>
34
#include <coreplugin/actionmanager/actionmanager.h>
con's avatar
con committed
35
#include <coreplugin/fileiconprovider.h>
36
#include <coreplugin/icontext.h>
37
#include <coreplugin/mainwindow.h>
38
#include <utils/algorithm.h>
39
#include <utils/appmainwindow.h>
40
#include <utils/asconst.h>
41
#include <utils/fancylineedit.h>
42
#include <utils/highlightingitemdelegate.h>
43
#include <utils/hostosinfo.h>
44
#include <utils/itemviews.h>
Eike Ziller's avatar
Eike Ziller committed
45
#include <utils/progressindicator.h>
hjk's avatar
hjk committed
46
#include <utils/qtcassert.h>
47
#include <utils/runextensions.h>
48
#include <utils/stylehelper.h>
49
#include <utils/utilsicons.h>
con's avatar
con committed
50

51
#include <QApplication>
Eike Ziller's avatar
Eike Ziller committed
52
#include <QColor>
53
#include <QDesktopWidget>
54 55 56 57 58 59 60 61 62 63 64 65
#include <QFileInfo>
#include <QTimer>
#include <QEvent>
#include <QAction>
#include <QApplication>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QKeyEvent>
#include <QMenu>
#include <QScrollBar>
#include <QTreeView>
#include <QToolTip>
con's avatar
con committed
66

67
Q_DECLARE_METATYPE(Core::LocatorFilterEntry)
con's avatar
con committed
68

69 70 71 72
using namespace Utils;

const int LocatorEntryRole = int(HighlightingItemRole::User);

73
namespace Core {
con's avatar
con committed
74 75
namespace Internal {

con's avatar
con committed
76
/* A model to represent the Locator results. */
con's avatar
con committed
77
class LocatorModel : public QAbstractListModel
con's avatar
con committed
78 79
{
public:
80 81 82 83 84 85 86

    enum Columns {
        DisplayNameColumn,
        ExtraInfoColumn,
        ColumnCount
    };

con's avatar
con committed
87
    LocatorModel(QObject *parent = 0)
con's avatar
con committed
88
        : QAbstractListModel(parent)
89
        , mBackgroundColor(Utils::creatorTheme()->color(Utils::Theme::TextColorHighlightBackground).name())
con's avatar
con committed
90 91
    {}

92
    void clear();
Eike Ziller's avatar
Eike Ziller committed
93 94 95
    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
con's avatar
con committed
96

97
    void addEntries(const QList<LocatorFilterEntry> &entries);
con's avatar
con committed
98 99

private:
100
    mutable QList<LocatorFilterEntry> mEntries;
101
    bool hasExtraInfo = false;
102
    QColor mBackgroundColor;
con's avatar
con committed
103 104
};

105
class CompletionDelegate : public HighlightingItemDelegate
106 107 108 109 110 111 112
{
public:
    CompletionDelegate(QObject *parent);

    QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};

113
class CompletionList : public Utils::TreeView
con's avatar
con committed
114 115 116 117
{
public:
    CompletionList(QWidget *parent = 0);

118 119
    void setModel(QAbstractItemModel *model);

120
    void resizeHeaders();
con's avatar
con committed
121

122 123
    void next();
    void previous();
Tobias Hunger's avatar
Tobias Hunger committed
124

125
    void showCurrentItemToolTip();
126

127
    void keyPressEvent(QKeyEvent *event);
128
    bool eventFilter(QObject *watched, QEvent *event);
129 130 131

private:
    QMetaObject::Connection m_updateSizeConnection;
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
};

class TopLeftLocatorPopup : public LocatorPopup
{
public:
    TopLeftLocatorPopup(LocatorWidget *locatorWidget)
        : LocatorPopup(locatorWidget, locatorWidget) {}

protected:
    void updateGeometry() override;
    void inputLostFocus() override;
};

class CenteredLocatorPopup : public LocatorPopup
{
public:
    CenteredLocatorPopup(LocatorWidget *locatorWidget, QWidget *parent)
        : LocatorPopup(locatorWidget, parent) {}

protected:
    void updateGeometry() override;
con's avatar
con committed
153 154
};

con's avatar
con committed
155
// =========== LocatorModel ===========
con's avatar
con committed
156

157
void LocatorModel::clear()
con's avatar
con committed
158
{
159 160
    beginResetModel();
    mEntries.clear();
161
    hasExtraInfo = false;
162 163 164 165 166 167 168
    endResetModel();
}

int LocatorModel::rowCount(const QModelIndex & parent) const
{
    if (parent.isValid())
        return 0;
con's avatar
con committed
169 170 171
    return mEntries.size();
}

con's avatar
con committed
172
int LocatorModel::columnCount(const QModelIndex &parent) const
con's avatar
con committed
173
{
174 175
    if (parent.isValid())
        return 0;
176
    return hasExtraInfo ? ColumnCount : 1;
con's avatar
con committed
177 178
}

con's avatar
con committed
179
QVariant LocatorModel::data(const QModelIndex &index, int role) const
con's avatar
con committed
180 181 182 183
{
    if (!index.isValid() || index.row() >= mEntries.size())
        return QVariant();

Orgad Shaneh's avatar
Orgad Shaneh committed
184 185
    switch (role) {
    case Qt::DisplayRole:
186
        if (index.column() == DisplayNameColumn)
con's avatar
con committed
187
            return mEntries.at(index.row()).displayName;
188
        else if (index.column() == ExtraInfoColumn)
con's avatar
con committed
189
            return mEntries.at(index.row()).extraInfo;
Orgad Shaneh's avatar
Orgad Shaneh committed
190 191
        break;
    case Qt::ToolTipRole:
192 193 194
        if (mEntries.at(index.row()).extraInfo.isEmpty())
            return QVariant(mEntries.at(index.row()).displayName);
        else
195 196
            return QVariant(mEntries.at(index.row()).displayName
                            + QLatin1String("\n\n") + mEntries.at(index.row()).extraInfo);
Orgad Shaneh's avatar
Orgad Shaneh committed
197 198
        break;
    case Qt::DecorationRole:
199
        if (index.column() == DisplayNameColumn) {
Orgad Shaneh's avatar
Orgad Shaneh committed
200
            LocatorFilterEntry &entry = mEntries[index.row()];
201
            if (!entry.displayIcon && !entry.fileName.isEmpty())
Orgad Shaneh's avatar
Orgad Shaneh committed
202
                entry.displayIcon = FileIconProvider::icon(entry.fileName);
203
            return entry.displayIcon ? entry.displayIcon.value() : QIcon();
con's avatar
con committed
204
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
205 206
        break;
    case Qt::ForegroundRole:
207
        if (index.column() == ExtraInfoColumn)
Orgad Shaneh's avatar
Orgad Shaneh committed
208 209
            return QColor(Qt::darkGray);
        break;
210
    case LocatorEntryRole:
con's avatar
con committed
211
        return qVariantFromValue(mEntries.at(index.row()));
212 213
    case int(HighlightingItemRole::StartColumn):
    case int(HighlightingItemRole::Length): {
214 215 216 217 218
        LocatorFilterEntry &entry = mEntries[index.row()];
        const int highlightColumn = entry.highlightInfo.dataType == LocatorFilterEntry::HighlightInfo::DisplayName
                                                                 ? DisplayNameColumn
                                                                 : ExtraInfoColumn;
        if (highlightColumn == index.column()) {
219
            const bool startIndexRole = role == int(HighlightingItemRole::StartColumn);
220 221
            return startIndexRole ? QVariant::fromValue(entry.highlightInfo.starts)
                                  : QVariant::fromValue(entry.highlightInfo.lengths);
222 223 224
        }
        break;
    }
225
    case int(HighlightingItemRole::Background):
226
        return mBackgroundColor;
con's avatar
con committed
227 228 229 230 231
    }

    return QVariant();
}

232
void LocatorModel::addEntries(const QList<LocatorFilterEntry> &entries)
con's avatar
con committed
233
{
234 235 236
    beginInsertRows(QModelIndex(), mEntries.size(), mEntries.size() + entries.size() - 1);
    mEntries.append(entries);
    endInsertRows();
237 238 239 240 241 242 243
    if (hasExtraInfo)
        return;
    if (Utils::anyOf(entries, [](const LocatorFilterEntry &e) { return !e.extraInfo.isEmpty();})) {
        beginInsertColumns(QModelIndex(), 1, 1);
        hasExtraInfo = true;
        endInsertColumns();
    }
con's avatar
con committed
244 245 246 247 248
}

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

CompletionList::CompletionList(QWidget *parent)
249
    : Utils::TreeView(parent)
con's avatar
con committed
250
{
251
    setItemDelegate(new CompletionDelegate(this));
con's avatar
con committed
252 253 254 255 256
    setRootIsDecorated(false);
    setUniformRowHeights(true);
    header()->hide();
    header()->setStretchLastSection(true);
    // This is too slow when done on all results
257
    //header()->setSectionResizeMode(QHeaderView::ResizeToContents);
258 259 260 261 262 263
    if (Utils::HostOsInfo::isMacHost()) {
        if (horizontalScrollBar())
            horizontalScrollBar()->setAttribute(Qt::WA_MacMiniSize);
        if (verticalScrollBar())
            verticalScrollBar()->setAttribute(Qt::WA_MacMiniSize);
    }
264
    installEventFilter(this);
con's avatar
con committed
265 266
}

267
void CompletionList::setModel(QAbstractItemModel *newModel)
con's avatar
con committed
268
{
Eike Ziller's avatar
Eike Ziller committed
269 270 271 272 273
    const auto updateSize = [this] {
        if (model() && model()->rowCount() > 0) {
            const QStyleOptionViewItem &option = viewOptions();
            const QSize shint = itemDelegate()->sizeHint(option, model()->index(0, 0));
            setFixedHeight(shint.height() * 17 + frameWidth() * 2);
274
            disconnect(m_updateSizeConnection);
Eike Ziller's avatar
Eike Ziller committed
275 276 277
        }
    };

278
    if (model()) {
Eike Ziller's avatar
Eike Ziller committed
279
        disconnect(model(), 0, this, 0);
280 281 282 283 284
    }
    QTreeView::setModel(newModel);
    if (newModel) {
        connect(newModel, &QAbstractItemModel::columnsInserted,
                this, &CompletionList::resizeHeaders);
285 286
        m_updateSizeConnection = connect(newModel, &QAbstractItemModel::rowsInserted,
                                         this, updateSize);
287 288
    }
}
con's avatar
con committed
289

290
void LocatorPopup::updateGeometry()
291 292 293 294
{
    m_tree->resizeHeaders();
}

295
void TopLeftLocatorPopup::updateGeometry()
296
{
297 298
    QTC_ASSERT(parentWidget(), return);
    const QSize size = preferredSize();
299 300
    const int border = m_tree->frameWidth();
    const QRect rect(parentWidget()->mapToGlobal(QPoint(-border, -size.height() - border)), size);
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
    setGeometry(rect);
    LocatorPopup::updateGeometry();
}

void CenteredLocatorPopup::updateGeometry()
{
    QTC_ASSERT(parentWidget(), return);
    const QSize size = preferredSize();
    const QSize parentSize = parentWidget()->size();
    const QPoint pos = parentWidget()->mapToGlobal({(parentSize.width() - size.width()) / 2,
                                                    parentSize.height() / 2 - size.height()});
    QRect rect(pos, size);
    // invisible widget doesn't have the right screen set yet, so use the parent widget to
    // check for available geometry
    const QRect available = QApplication::desktop()->availableGeometry(parentWidget());
    if (rect.right() > available.right())
        rect.moveRight(available.right());
    if (rect.bottom() > available.bottom())
        rect.moveBottom(available.bottom());
    if (rect.top() < available.top())
        rect.moveTop(available.top());
    if (rect.left() < available.left())
        rect.moveLeft(available.left());
    setGeometry(rect);
    LocatorPopup::updateGeometry();
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
}

void LocatorPopup::updateWindow()
{
    QWidget *w = parentWidget() ? parentWidget()->window() : nullptr;
    if (m_window != w) {
        if (m_window)
            m_window->removeEventFilter(this);
        m_window = w;
        if (m_window)
            m_window->installEventFilter(this);
    }
}

bool LocatorPopup::event(QEvent *event)
{
Eike Ziller's avatar
Eike Ziller committed
342
    if (event->type() == QEvent::ParentChange)
343
        updateWindow();
Eike Ziller's avatar
Eike Ziller committed
344 345 346
    // completion list resizes after first items are shown --> LayoutRequest
    else if (event->type() == QEvent::Show || event->type() == QEvent::LayoutRequest)
        QTimer::singleShot(0, this, &LocatorPopup::updateGeometry);
347 348 349 350 351 352
    return QWidget::event(event);
}

bool LocatorPopup::eventFilter(QObject *watched, QEvent *event)
{
    if (watched == m_window && event->type() == QEvent::Resize)
353
        updateGeometry();
354 355 356
    return QWidget::eventFilter(watched, event);
}

357 358 359 360 361 362 363 364 365 366 367
QSize LocatorPopup::preferredSize()
{
    static const int MIN_WIDTH = 730;
    const QSize windowSize = m_window ? m_window->size() : QSize(MIN_WIDTH, 0);

    const int width = qMax(MIN_WIDTH, windowSize.width() * 2 / 3);
    return QSize(width, sizeHint().height());
}

void TopLeftLocatorPopup::inputLostFocus()
{
368 369
    if (!isActiveWindow())
        hide();
370 371 372
}

void LocatorPopup::inputLostFocus()
373
{
374 375 376 377
}

void CompletionList::resizeHeaders()
{
378
    header()->resizeSection(0, width() / 2);
379
    header()->resizeSection(1, 0); // last section is auto resized because of stretchLastSection
con's avatar
con committed
380 381
}

382 383
LocatorPopup::LocatorPopup(LocatorWidget *locatorWidget, QWidget *parent)
    : QWidget(parent),
384 385
      m_tree(new CompletionList(this)),
      m_inputWidget(locatorWidget)
386
{
387 388
    if (Utils::HostOsInfo::isMacHost())
        m_tree->setFrameStyle(QFrame::NoFrame); // tool tip already includes a frame
389 390 391 392 393 394 395 396 397 398
    m_tree->setModel(locatorWidget->model());

    auto layout = new QVBoxLayout;
    layout->setSizeConstraint(QLayout::SetMinimumSize);
    setLayout(layout);
    layout->setContentsMargins(0, 0, 0, 0);
    layout->setSpacing(0);
    layout->addWidget(m_tree);

    connect(locatorWidget, &LocatorWidget::parentChanged, this, &LocatorPopup::updateWindow);
399 400
    connect(locatorWidget, &LocatorWidget::showPopup, this, &LocatorPopup::show);
    connect(locatorWidget, &LocatorWidget::hidePopup, this, &LocatorPopup::close);
401 402
    connect(locatorWidget, &LocatorWidget::lostFocus, this, &LocatorPopup::inputLostFocus,
            Qt::QueuedConnection);
403 404 405 406 407 408 409 410 411 412 413 414 415 416
    connect(locatorWidget, &LocatorWidget::selectRow, m_tree, [this](int row) {
        m_tree->setCurrentIndex(m_tree->model()->index(row, 0));
    });
    connect(locatorWidget, &LocatorWidget::showCurrentItemToolTip,
            m_tree, &CompletionList::showCurrentItemToolTip);
    connect(locatorWidget, &LocatorWidget::handleKey, this, [this](QKeyEvent *keyEvent) {
        QApplication::sendEvent(m_tree, keyEvent);
    }, Qt::DirectConnection); // must be handled directly before event is deleted
    connect(m_tree, &QAbstractItemView::activated, locatorWidget,
            [this, locatorWidget](const QModelIndex &index) {
                if (isVisible())
                    locatorWidget->scheduleAcceptEntry(index);
            });

417
    updateGeometry();
418 419 420 421 422 423 424
}

CompletionList *LocatorPopup::completionList() const
{
    return m_tree;
}

425 426 427 428 429
LocatorWidget *LocatorPopup::inputWidget() const
{
    return m_inputWidget;
}

430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
void LocatorPopup::focusOutEvent(QFocusEvent *event) {
    if (event->reason() == Qt::ActiveWindowFocusReason)
        hide();
    QWidget::focusOutEvent(event);
}

void CompletionList::next() {
    int index = currentIndex().row();
    ++index;
    if (index >= model()->rowCount(QModelIndex())) {
        // wrap
        index = 0;
    }
    setCurrentIndex(model()->index(index, 0));
}

void CompletionList::previous() {
    int index = currentIndex().row();
    --index;
    if (index < 0) {
        // wrap
        index = model()->rowCount(QModelIndex()) - 1;
    }
    setCurrentIndex(model()->index(index, 0));
}

void CompletionList::showCurrentItemToolTip()
{
    QTC_ASSERT(model(), return);
    if (!isVisible())
        return;
    const QModelIndex index = currentIndex();
    if (index.isValid()) {
        QToolTip::showText(mapToGlobal(pos() + visualRect(index).topRight()),
                           model()->data(index, Qt::ToolTipRole).toString());
    }
}

void CompletionList::keyPressEvent(QKeyEvent *event)
{
    switch (event->key()) {
    case Qt::Key_Tab:
    case Qt::Key_Down:
        next();
        return;
    case Qt::Key_Backtab:
    case Qt::Key_Up:
        previous();
        return;
    case Qt::Key_P:
    case Qt::Key_N:
        if (event->modifiers() == Qt::KeyboardModifiers(Utils::HostOsInfo::controlModifier())) {
            if (event->key() == Qt::Key_P)
                previous();
            else
                next();
            return;
        }
        break;
489 490 491 492 493 494 495 496 497 498
    case Qt::Key_Return:
    case Qt::Key_Enter:
        // emit activated even if current index is not valid
        // if there are no results yet, this allows activating the first entry when it is available
        // (see LocatorWidget::addSearchResults)
        if (event->modifiers() == 0) {
            emit activated(currentIndex());
            return;
        }
        break;
499 500 501 502
    }
    Utils::TreeView::keyPressEvent(event);
}

503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518
bool CompletionList::eventFilter(QObject *watched, QEvent *event)
{
    if (watched == this && event->type() == QEvent::ShortcutOverride) {
        QKeyEvent *ke = static_cast<QKeyEvent *>(event);
        switch (ke->key()) {
        case Qt::Key_Escape:
            if (!ke->modifiers()) {
                event->accept();
                return true;
            }
            break;
        }
    }
    return Utils::TreeView::eventFilter(watched, event);
}

519
// =========== LocatorWidget ===========
con's avatar
con committed
520

521
LocatorWidget::LocatorWidget(Locator *locator) :
522 523 524
    m_locatorModel(new LocatorModel(this)),
    m_filterMenu(new QMenu(this)),
    m_refreshAction(new QAction(tr("Refresh"), this)),
Orgad Shaneh's avatar
Orgad Shaneh committed
525
    m_configureAction(new QAction(ICore::msgShowOptionsDialog(), this)),
Orgad Shaneh's avatar
Orgad Shaneh committed
526
    m_fileLineEdit(new Utils::FancyLineEdit)
con's avatar
con committed
527
{
528
    setAttribute(Qt::WA_Hover);
529
    setFocusProxy(m_fileLineEdit);
con's avatar
con committed
530
    resize(200, 90);
Eike Ziller's avatar
Eike Ziller committed
531
    QSizePolicy sizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
con's avatar
con committed
532 533 534 535 536 537 538 539 540 541
    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);

542
    const QIcon icon = Utils::Icons::MAGNIFIER.icon();
543
    m_fileLineEdit->setFiltering(true);
544
    m_fileLineEdit->setButtonIcon(Utils::FancyLineEdit::Left, icon);
545
    m_fileLineEdit->setButtonToolTip(Utils::FancyLineEdit::Left, tr("Options"));
con's avatar
con committed
546
    m_fileLineEdit->setFocusPolicy(Qt::ClickFocus);
547
    m_fileLineEdit->setButtonVisible(Utils::FancyLineEdit::Left, true);
548
    // We set click focus since otherwise you will always get two popups
549
    m_fileLineEdit->setButtonFocusPolicy(Utils::FancyLineEdit::Left, Qt::ClickFocus);
550
    m_fileLineEdit->setAttribute(Qt::WA_MacShowFocusRect, false);
con's avatar
con committed
551 552 553 554 555 556 557

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

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

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

Orgad Shaneh's avatar
Orgad Shaneh committed
560
    connect(m_refreshAction, &QAction::triggered,
561
            locator, [locator]() { locator->refresh(); });
Orgad Shaneh's avatar
Orgad Shaneh committed
562 563
    connect(m_configureAction, &QAction::triggered, this, &LocatorWidget::showConfigureDialog);
    connect(m_fileLineEdit, &QLineEdit::textChanged,
564
        this, &LocatorWidget::showPopupDelayed);
565

566
    m_entriesWatcher = new QFutureWatcher<LocatorFilterEntry>(this);
567 568 569 570
    connect(m_entriesWatcher, &QFutureWatcher<LocatorFilterEntry>::resultsReadyAt,
            this, &LocatorWidget::addSearchResults);
    connect(m_entriesWatcher, &QFutureWatcher<LocatorFilterEntry>::finished,
            this, &LocatorWidget::handleSearchFinished);
571

572 573
    m_showPopupTimer.setInterval(100);
    m_showPopupTimer.setSingleShot(true);
Orgad Shaneh's avatar
Orgad Shaneh committed
574
    connect(&m_showPopupTimer, &QTimer::timeout, this, &LocatorWidget::showPopupNow);
Eike Ziller's avatar
Eike Ziller committed
575

576
    m_progressIndicator = new Utils::ProgressIndicator(Utils::ProgressIndicatorSize::Small,
Eike Ziller's avatar
Eike Ziller committed
577 578 579 580 581 582
                                                       m_fileLineEdit);
    m_progressIndicator->raise();
    m_progressIndicator->hide();
    m_showProgressTimer.setSingleShot(true);
    m_showProgressTimer.setInterval(50); // don't show progress for < 50ms tasks
    connect(&m_showProgressTimer, &QTimer::timeout, [this]() { setProgressIndicatorVisible(true);});
583 584 585 586 587 588 589 590 591

    Command *locateCmd = ActionManager::command(Constants::LOCATE);
    if (QTC_GUARD(locateCmd)) {
        connect(locateCmd, &Command::keySequenceChanged, this, [this,locateCmd] {
            updatePlaceholderText(locateCmd);
        });
        updatePlaceholderText(locateCmd);
    }

592
    connect(locator, &Locator::filtersChanged, this, &LocatorWidget::updateFilterList);
593
    updateFilterList();
con's avatar
con committed
594 595
}

596
void LocatorWidget::updatePlaceholderText(Command *command)
Eike Ziller's avatar
Eike Ziller committed
597
{
598 599 600 601 602 603
    QTC_ASSERT(command, return);
    if (command->keySequence().isEmpty())
        m_fileLineEdit->setPlaceholderText(tr("Type to locate"));
    else
        m_fileLineEdit->setPlaceholderText(tr("Type to locate (%1)").arg(
                                        command->keySequence().toString(QKeySequence::NativeText)));
Eike Ziller's avatar
Eike Ziller committed
604 605
}

606
void LocatorWidget::updateFilterList()
con's avatar
con committed
607 608
{
    m_filterMenu->clear();
609 610 611 612 613
    const QList<ILocatorFilter *> filters = Locator::filters();
    for (ILocatorFilter *filter : filters) {
        Command *cmd = ActionManager::command(filter->actionId());
        if (cmd)
            m_filterMenu->addAction(cmd->action());
614
    }
con's avatar
con committed
615 616 617 618 619
    m_filterMenu->addSeparator();
    m_filterMenu->addAction(m_refreshAction);
    m_filterMenu->addAction(m_configureAction);
}

620
bool LocatorWidget::eventFilter(QObject *obj, QEvent *event)
con's avatar
con committed
621
{
622 623 624 625 626 627 628 629 630 631 632
    if (obj == m_fileLineEdit && event->type() == QEvent::ShortcutOverride) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
        switch (keyEvent->key()) {
        case Qt::Key_P:
        case Qt::Key_N:
            if (keyEvent->modifiers() == Qt::KeyboardModifiers(Utils::HostOsInfo::controlModifier())) {
                event->accept();
                return true;
            }
        }
    } else if (obj == m_fileLineEdit && event->type() == QEvent::KeyPress) {
633 634 635 636 637 638 639 640 641
        if (m_possibleToolTipRequest)
            m_possibleToolTipRequest = false;
        if (QToolTip::isVisible())
            QToolTip::hideText();

        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
        switch (keyEvent->key()) {
        case Qt::Key_PageUp:
        case Qt::Key_PageDown:
642 643 644 645 646 647
        case Qt::Key_Down:
        case Qt::Key_Tab:
        case Qt::Key_Up:
        case Qt::Key_Backtab:
            emit showPopup();
            emit handleKey(keyEvent);
648
            return true;
649 650 651 652
        case Qt::Key_Home:
        case Qt::Key_End:
            if (Utils::HostOsInfo::isMacHost()
                    != (keyEvent->modifiers() == Qt::KeyboardModifiers(Qt::ControlModifier))) {
653 654
                emit showPopup();
                emit handleKey(keyEvent);
655 656 657
                return true;
            }
            break;
658 659
        case Qt::Key_Enter:
        case Qt::Key_Return:
660
            emit handleKey(keyEvent);
661 662
            return true;
        case Qt::Key_Escape:
663
            emit hidePopup();
664 665 666 667 668 669 670
            return true;
        case Qt::Key_Alt:
            if (keyEvent->modifiers() == Qt::AltModifier) {
                m_possibleToolTipRequest = true;
                return true;
            }
            break;
671 672
        case Qt::Key_P:
        case Qt::Key_N:
673 674 675
            if (keyEvent->modifiers() == Qt::KeyboardModifiers(Utils::HostOsInfo::controlModifier())) {
                emit showPopup();
                emit handleKey(keyEvent);
676 677 678
                return true;
            }
            break;
679 680 681 682 683 684 685
        default:
            break;
        }
    } else if (obj == m_fileLineEdit && event->type() == QEvent::KeyRelease) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
        if (m_possibleToolTipRequest) {
            m_possibleToolTipRequest = false;
686
            if ((keyEvent->key() == Qt::Key_Alt)
687
                    && (keyEvent->modifiers() == Qt::NoModifier)) {
688 689
                emit showCurrentItemToolTip();
                return true;
690 691 692
            }
        }
    } else if (obj == m_fileLineEdit && event->type() == QEvent::FocusOut) {
693
        emit lostFocus();
con's avatar
con committed
694
    } else if (obj == m_fileLineEdit && event->type() == QEvent::FocusIn) {
695 696 697
        QFocusEvent *fev = static_cast<QFocusEvent *>(event);
        if (fev->reason() != Qt::ActiveWindowFocusReason)
            showPopupNow();
698
    } else if (obj == this && event->type() == QEvent::ParentChange) {
699
        emit parentChanged();
con's avatar
con committed
700 701
    } else if (obj == this && event->type() == QEvent::ShortcutOverride) {
        QKeyEvent *ke = static_cast<QKeyEvent *>(event);
702 703 704 705
        switch (ke->key()) {
        case Qt::Key_Escape:
            if (!ke->modifiers()) {
                event->accept();
Orgad Shaneh's avatar
Orgad Shaneh committed
706
                QTimer::singleShot(0, this, &LocatorWidget::setFocusToCurrentMode);
707 708
                return true;
            }
709
            break;
710 711 712 713 714 715 716 717
        case Qt::Key_Alt:
            if (ke->modifiers() == Qt::AltModifier) {
                event->accept();
                return true;
            }
            break;
        default:
            break;
con's avatar
con committed
718 719 720 721 722
        }
    }
    return QWidget::eventFilter(obj, event);
}

723 724
void LocatorWidget::setFocusToCurrentMode()
{
Orgad Shaneh's avatar
Orgad Shaneh committed
725
    ModeManager::setFocusToCurrentMode();
726 727
}

728
void LocatorWidget::showPopupDelayed()
con's avatar
con committed
729
{
730
    m_updateRequested = true;
731
    m_showPopupTimer.start();
732 733 734 735
}

void LocatorWidget::showPopupNow()
{
736
    m_showPopupTimer.stop();
737
    updateCompletionList(m_fileLineEdit->text());
738
    emit showPopup();
con's avatar
con committed
739 740
}

Andre Hartmann's avatar
Andre Hartmann committed
741
QList<ILocatorFilter *> LocatorWidget::filtersFor(const QString &text, QString &searchText)
con's avatar
con committed
742
{
743 744 745 746 747 748 749
    const int length = text.size();
    int firstNonSpace;
    for (firstNonSpace = 0; firstNonSpace < length; ++firstNonSpace) {
        if (!text.at(firstNonSpace).isSpace())
            break;
    }
    const int whiteSpace = text.indexOf(QChar::Space, firstNonSpace);
750 751
    const QList<ILocatorFilter *> filters = Utils::filtered(Locator::filters(),
                                                            &ILocatorFilter::isEnabled);
752 753
    if (whiteSpace >= 0) {
        const QString prefix = text.mid(firstNonSpace, whiteSpace - firstNonSpace).toLower();
754
        QList<ILocatorFilter *> prefixFilters;
755
        foreach (ILocatorFilter *filter, filters) {
con's avatar
con committed
756
            if (prefix == filter->shortcutString()) {
757
                searchText = text.mid(whiteSpace).trimmed();
758
                prefixFilters << filter;
con's avatar
con committed
759 760
            }
        }
761 762
        if (!prefixFilters.isEmpty())
            return prefixFilters;
con's avatar
con committed
763
    }
764
    searchText = text.trimmed();
765
    return Utils::filtered(filters, &ILocatorFilter::isIncludedByDefault);
con's avatar
con committed
766 767
}

Eike Ziller's avatar
Eike Ziller committed
768 769 770 771 772 773 774 775 776 777 778 779 780 781 782
void LocatorWidget::setProgressIndicatorVisible(bool visible)
{
    if (!visible) {
        m_progressIndicator->hide();
        return;
    }
    const QSize iconSize = m_progressIndicator->sizeHint();
    m_progressIndicator->setGeometry(m_fileLineEdit->button(Utils::FancyLineEdit::Right)->geometry().x()
                                     - iconSize.width(),
                                     (m_fileLineEdit->height() - iconSize.height()) / 2 /*center*/,
                                     iconSize.width(),
                                     iconSize.height());
    m_progressIndicator->show();
}

783 784
void LocatorWidget::updateCompletionList(const QString &text)
{
785
    m_updateRequested = true;
786 787 788 789 790 791 792 793 794 795
    if (m_entriesWatcher->future().isRunning()) {
        // Cancel the old future. We may not just block the UI thread to wait for the search to
        // actually cancel, so try again when the finshed signal of the watcher ends up in
        // updateEntries() (which will call updateCompletionList again with the
        // requestedCompletionText)
        m_requestedCompletionText = text;
        m_entriesWatcher->future().cancel();
        return;
    }

Eike Ziller's avatar
Eike Ziller committed
796
    m_showProgressTimer.start();
797
    m_needsClearResult = true;
798
    QString searchText;
Andre Hartmann's avatar
Andre Hartmann committed
799
    const QList<ILocatorFilter *> filters = filtersFor(text, searchText);
800 801 802

    foreach (ILocatorFilter *filter, filters)
        filter->prepareSearch(searchText);
803
    QFuture<LocatorFilterEntry> future = Utils::runAsync(&runSearch, filters, searchText);
804 805 806
    m_entriesWatcher->setFuture(future);
}

807
void LocatorWidget::handleSearchFinished()
808
{
Eike Ziller's avatar
Eike Ziller committed
809 810
    m_showProgressTimer.stop();
    setProgressIndicatorVisible(false);
811
    m_updateRequested = false;
812 813 814
    if (m_rowRequestedForAccept) {
        acceptEntry(m_rowRequestedForAccept.value());
        m_rowRequestedForAccept.reset();
815 816
        return;
    }
817
    if (m_entriesWatcher->future().isCanceled()) {
818 819 820
        const QString text = m_requestedCompletionText;
        m_requestedCompletionText.clear();
        updateCompletionList(text);
821
        return;
822
    }
823

824 825 826 827
    if (m_needsClearResult) {
        m_locatorModel->clear();
        m_needsClearResult = false;
    }
828 829
}

830
void LocatorWidget::scheduleAcceptEntry(const QModelIndex &index)
831 832 833 834
{
    if (m_updateRequested) {
        // don't just accept the selected entry, since the list is not up to date
        // accept will be called after the update finished
835
        m_rowRequestedForAccept = index.row();
836 837
        // do not wait for the rest of the search to finish
        m_entriesWatcher->future().cancel();
838
    } else {
839
        acceptEntry(index.row());
840
    }
con's avatar
con committed
841 842
}

843
void LocatorWidget::acceptEntry(int row)
con's avatar
con committed
844
{
845
    if (row < 0 || row >= m_locatorModel->rowCount())
846 847
        return;
    const QModelIndex index = m_locatorModel->index(row, 0);
con's avatar
con committed
848 849
    if (!index.isValid())
        return;
850
    const LocatorFilterEntry entry = m_locatorModel->data(index, LocatorEntryRole).value<LocatorFilterEntry>();
851
    Q_ASSERT(entry.filter != nullptr);
852 853 854 855 856
    QString newText;
    int selectionStart = -1;
    int selectionLength = 0;
    entry.filter->accept(entry, &newText, &selectionStart, &selectionLength);
    if (newText.isEmpty()) {
857
        emit hidePopup();
858 859
        m_fileLineEdit->clearFocus();
    } else {
860
        showText(newText, selectionStart, selectionLength);
861
    }
con's avatar
con committed
862 863
}

864
void LocatorWidget::showText(const QString &text, int selectionStart, int selectionLength)
con's avatar
con committed
865
{
866 867
    if (!text.isEmpty())
        m_fileLineEdit->setText(text);
868 869
    m_fileLineEdit->setFocus();
    showPopupNow();
870
    ICore::raiseWindow(window());
871

con's avatar
con committed
872
    if (selectionStart >= 0) {
con's avatar
con committed
873
        m_fileLineEdit->setSelection(selectionStart, selectionLength);
con's avatar
con committed
874 875 876
        if (selectionLength == 0) // make sure the cursor is at the right position (Mac-vs.-rest difference)
            m_fileLineEdit->setCursorPosition(selectionStart);
    } else {
877
        m_fileLineEdit->selectAll();
con's avatar
con committed
878
    }
con's avatar
con committed
879 880
}

881
QString LocatorWidget::currentText() const
con's avatar
con committed
882
{
883
    return m_fileLineEdit->text();
con's avatar
con committed
884 885
}

886 887 888 889 890
QAbstractItemModel *LocatorWidget::model() const
{
    return m_locatorModel;
}

891
void LocatorWidget::showConfigureDialog()
con's avatar
con committed
892
{
893
    ICore::showOptionsDialog(Constants::FILTER_OPTIONS_PAGE);
con's avatar
con committed
894
}
895

896 897 898 899 900 901 902 903 904 905 906
void LocatorWidget::addSearchResults(int firstIndex, int endIndex)
{
    if (m_needsClearResult) {
        m_locatorModel->clear();
        m_needsClearResult = false;
    }
    const bool selectFirst = m_locatorModel->rowCount() == 0;
    QList<LocatorFilterEntry> entries;
    for (int i = firstIndex; i < endIndex; ++i)
        entries.append(m_entriesWatcher->resultAt(i));
    m_locatorModel->addEntries(entries);
907
    if (selectFirst) {
908
        emit selectRow(0);
909
        if (m_rowRequestedForAccept)
910 911
            m_rowRequestedForAccept = 0;
    }
912 913
}

914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931
LocatorWidget *createStaticLocatorWidget(Locator *locator)
{
    auto widget = new LocatorWidget(locator);
    auto popup = new TopLeftLocatorPopup(widget); // owned by widget
    popup->setWindowFlags(Qt::ToolTip);
    return widget;
}

LocatorPopup *createLocatorPopup(Locator *locator, QWidget *parent)
{
    auto widget = new LocatorWidget(locator);
    auto popup = new CenteredLocatorPopup(widget, parent);
    popup->layout()->addWidget(widget);
    popup->setWindowFlags(Qt::Popup);
    popup->setAttribute(Qt::WA_DeleteOnClose);
    return popup;
}

932
CompletionDelegate::CompletionDelegate(QObject *parent)
933
    : HighlightingItemDelegate(0, parent)
934 935 936 937 938
{
}

QSize CompletionDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
939
    return HighlightingItemDelegate::sizeHint(option, index) + QSize(0, 2);
940 941
}

Orgad Shaneh's avatar
Orgad Shaneh committed
942
} // namespace Internal
943
} // namespace Core