locatorwidget.cpp 29.9 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 37
#include <coreplugin/find/searchresulttreeitemdelegate.h>
#include <coreplugin/find/searchresulttreeitemroles.h>
38
#include <coreplugin/icontext.h>
39
#include <coreplugin/mainwindow.h>
40
#include <utils/algorithm.h>
41
#include <utils/appmainwindow.h>
42
#include <utils/asconst.h>
43
#include <utils/fancylineedit.h>
44
#include <utils/hostosinfo.h>
45
#include <utils/itemviews.h>
Eike Ziller's avatar
Eike Ziller committed
46
#include <utils/progressindicator.h>
hjk's avatar
hjk committed
47
#include <utils/qtcassert.h>
48
#include <utils/runextensions.h>
49
#include <utils/stylehelper.h>
50
#include <utils/utilsicons.h>
con's avatar
con committed
51

52
#include <QApplication>
Eike Ziller's avatar
Eike Ziller committed
53
#include <QColor>
54
#include <QDesktopWidget>
55 56 57 58 59 60 61 62 63 64 65 66
#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
67

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

70
namespace Core {
con's avatar
con committed
71 72
namespace Internal {

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

    enum Columns {
        DisplayNameColumn,
        ExtraInfoColumn,
        ColumnCount
    };

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

89
    void clear();
Eike Ziller's avatar
Eike Ziller committed
90 91 92
    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
93

94
    void addEntries(const QList<LocatorFilterEntry> &entries);
con's avatar
con committed
95 96

private:
97
    mutable QList<LocatorFilterEntry> mEntries;
98
    bool hasExtraInfo = false;
99
    QColor mBackgroundColor;
con's avatar
con committed
100 101
};

102 103 104 105 106 107 108 109
class CompletionDelegate : public SearchResultTreeItemDelegate
{
public:
    CompletionDelegate(QObject *parent);

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

110
class CompletionList : public Utils::TreeView
con's avatar
con committed
111 112 113 114
{
public:
    CompletionList(QWidget *parent = 0);

115 116
    void setModel(QAbstractItemModel *model);

117
    void resizeHeaders();
con's avatar
con committed
118

119 120
    void next();
    void previous();
Tobias Hunger's avatar
Tobias Hunger committed
121

122
    void showCurrentItemToolTip();
123

124
    void keyPressEvent(QKeyEvent *event);
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
    bool eventFilter(QObject *watched, QEvent *event);
};

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
147 148
};

con's avatar
con committed
149
// =========== LocatorModel ===========
con's avatar
con committed
150

151
void LocatorModel::clear()
con's avatar
con committed
152
{
153 154
    beginResetModel();
    mEntries.clear();
155
    hasExtraInfo = false;
156 157 158 159 160 161 162
    endResetModel();
}

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

con's avatar
con committed
166
int LocatorModel::columnCount(const QModelIndex &parent) const
con's avatar
con committed
167
{
168 169
    if (parent.isValid())
        return 0;
170
    return hasExtraInfo ? ColumnCount : 1;
con's avatar
con committed
171 172
}

con's avatar
con committed
173
QVariant LocatorModel::data(const QModelIndex &index, int role) const
con's avatar
con committed
174 175 176 177
{
    if (!index.isValid() || index.row() >= mEntries.size())
        return QVariant();

Orgad Shaneh's avatar
Orgad Shaneh committed
178 179
    switch (role) {
    case Qt::DisplayRole:
180
        if (index.column() == DisplayNameColumn)
con's avatar
con committed
181
            return mEntries.at(index.row()).displayName;
182
        else if (index.column() == ExtraInfoColumn)
con's avatar
con committed
183
            return mEntries.at(index.row()).extraInfo;
Orgad Shaneh's avatar
Orgad Shaneh committed
184 185
        break;
    case Qt::ToolTipRole:
186 187 188
        if (mEntries.at(index.row()).extraInfo.isEmpty())
            return QVariant(mEntries.at(index.row()).displayName);
        else
189 190
            return QVariant(mEntries.at(index.row()).displayName
                            + QLatin1String("\n\n") + mEntries.at(index.row()).extraInfo);
Orgad Shaneh's avatar
Orgad Shaneh committed
191 192
        break;
    case Qt::DecorationRole:
193 194
    case ItemDataRoles::ResultIconRole:
        if (index.column() == DisplayNameColumn) {
Orgad Shaneh's avatar
Orgad Shaneh committed
195
            LocatorFilterEntry &entry = mEntries[index.row()];
196
            if (!entry.displayIcon && !entry.fileName.isEmpty())
Orgad Shaneh's avatar
Orgad Shaneh committed
197
                entry.displayIcon = FileIconProvider::icon(entry.fileName);
198
            return entry.displayIcon ? entry.displayIcon.value() : QIcon();
con's avatar
con committed
199
        }
Orgad Shaneh's avatar
Orgad Shaneh committed
200 201
        break;
    case Qt::ForegroundRole:
202
        if (index.column() == ExtraInfoColumn)
Orgad Shaneh's avatar
Orgad Shaneh committed
203 204
            return QColor(Qt::darkGray);
        break;
205
    case ItemDataRoles::ResultItemRole:
con's avatar
con committed
206
        return qVariantFromValue(mEntries.at(index.row()));
207 208 209 210 211 212 213 214 215 216 217 218 219 220
    case ItemDataRoles::ResultBeginColumnNumberRole:
    case ItemDataRoles::SearchTermLengthRole: {
        LocatorFilterEntry &entry = mEntries[index.row()];
        const int highlightColumn = entry.highlightInfo.dataType == LocatorFilterEntry::HighlightInfo::DisplayName
                                                                 ? DisplayNameColumn
                                                                 : ExtraInfoColumn;
        if (highlightColumn == index.column()) {
            const bool startIndexRole = role == ItemDataRoles::ResultBeginColumnNumberRole;
            return startIndexRole ? entry.highlightInfo.startIndex : entry.highlightInfo.length;
        }
        break;
    }
    case ItemDataRoles::ResultHighlightBackgroundColor:
        return mBackgroundColor;
con's avatar
con committed
221 222 223 224 225
    }

    return QVariant();
}

226
void LocatorModel::addEntries(const QList<LocatorFilterEntry> &entries)
con's avatar
con committed
227
{
228 229 230
    beginInsertRows(QModelIndex(), mEntries.size(), mEntries.size() + entries.size() - 1);
    mEntries.append(entries);
    endInsertRows();
231 232 233 234 235 236 237
    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
238 239 240 241 242
}

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

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

261
void CompletionList::setModel(QAbstractItemModel *newModel)
con's avatar
con committed
262
{
Eike Ziller's avatar
Eike Ziller committed
263 264 265 266 267 268 269 270 271
    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);
            disconnect(model(), &QAbstractItemModel::rowsInserted, this, 0);
        }
    };

272
    if (model()) {
Eike Ziller's avatar
Eike Ziller committed
273
        disconnect(model(), 0, this, 0);
274 275 276 277 278
    }
    QTreeView::setModel(newModel);
    if (newModel) {
        connect(newModel, &QAbstractItemModel::columnsInserted,
                this, &CompletionList::resizeHeaders);
Eike Ziller's avatar
Eike Ziller committed
279 280
        connect(newModel, &QAbstractItemModel::rowsInserted,
                this, updateSize);
281 282
    }
}
con's avatar
con committed
283

284
void LocatorPopup::updateGeometry()
285 286 287 288
{
    m_tree->resizeHeaders();
}

289
void TopLeftLocatorPopup::updateGeometry()
290
{
291 292
    QTC_ASSERT(parentWidget(), return);
    const QSize size = preferredSize();
293 294
    const int border = m_tree->frameWidth();
    const QRect rect(parentWidget()->mapToGlobal(QPoint(-border, -size.height() - border)), size);
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
    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();
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
}

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
336
    if (event->type() == QEvent::ParentChange)
337
        updateWindow();
Eike Ziller's avatar
Eike Ziller committed
338 339 340
    // 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);
341 342 343 344 345 346
    return QWidget::event(event);
}

bool LocatorPopup::eventFilter(QObject *watched, QEvent *event)
{
    if (watched == m_window && event->type() == QEvent::Resize)
347
        updateGeometry();
348 349 350
    return QWidget::eventFilter(watched, event);
}

351 352 353 354 355 356 357 358 359 360 361
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()
{
362 363
    if (!isActiveWindow())
        hide();
364 365 366
}

void LocatorPopup::inputLostFocus()
367
{
368 369 370 371
}

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

376 377
LocatorPopup::LocatorPopup(LocatorWidget *locatorWidget, QWidget *parent)
    : QWidget(parent),
378 379
      m_tree(new CompletionList(this)),
      m_inputWidget(locatorWidget)
380
{
381 382
    if (Utils::HostOsInfo::isMacHost())
        m_tree->setFrameStyle(QFrame::NoFrame); // tool tip already includes a frame
383 384 385 386 387 388 389 390 391 392
    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);
393 394
    connect(locatorWidget, &LocatorWidget::showPopup, this, &LocatorPopup::show);
    connect(locatorWidget, &LocatorWidget::hidePopup, this, &LocatorPopup::close);
395 396
    connect(locatorWidget, &LocatorWidget::lostFocus, this, &LocatorPopup::inputLostFocus,
            Qt::QueuedConnection);
397 398 399 400 401 402 403 404 405 406 407 408 409 410
    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);
            });

411
    updateGeometry();
412 413 414 415 416 417 418
}

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

419 420 421 422 423
LocatorWidget *LocatorPopup::inputWidget() const
{
    return m_inputWidget;
}

424 425 426 427 428 429 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
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;
    }
    Utils::TreeView::keyPressEvent(event);
}

487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502
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);
}

503
// =========== LocatorWidget ===========
con's avatar
con committed
504

505
LocatorWidget::LocatorWidget(Locator *locator) :
506 507 508
    m_locatorModel(new LocatorModel(this)),
    m_filterMenu(new QMenu(this)),
    m_refreshAction(new QAction(tr("Refresh"), this)),
Orgad Shaneh's avatar
Orgad Shaneh committed
509
    m_configureAction(new QAction(ICore::msgShowOptionsDialog(), this)),
Orgad Shaneh's avatar
Orgad Shaneh committed
510
    m_fileLineEdit(new Utils::FancyLineEdit)
con's avatar
con committed
511
{
512
    setAttribute(Qt::WA_Hover);
513
    setFocusProxy(m_fileLineEdit);
con's avatar
con committed
514
    resize(200, 90);
Eike Ziller's avatar
Eike Ziller committed
515
    QSizePolicy sizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
con's avatar
con committed
516 517 518 519 520 521 522 523 524 525
    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);

Ulf Hermann's avatar
Ulf Hermann committed
526
    const QPixmap pixmap = Utils::Icons::MAGNIFIER.pixmap();
527
    m_fileLineEdit->setFiltering(true);
528
    m_fileLineEdit->setButtonPixmap(Utils::FancyLineEdit::Left, pixmap);
529
    m_fileLineEdit->setButtonToolTip(Utils::FancyLineEdit::Left, tr("Options"));
con's avatar
con committed
530
    m_fileLineEdit->setFocusPolicy(Qt::ClickFocus);
531
    m_fileLineEdit->setButtonVisible(Utils::FancyLineEdit::Left, true);
532
    // We set click focus since otherwise you will always get two popups
533
    m_fileLineEdit->setButtonFocusPolicy(Utils::FancyLineEdit::Left, Qt::ClickFocus);
534
    m_fileLineEdit->setAttribute(Qt::WA_MacShowFocusRect, false);
con's avatar
con committed
535 536 537 538 539 540 541

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

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

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

Orgad Shaneh's avatar
Orgad Shaneh committed
544
    connect(m_refreshAction, &QAction::triggered,
545
            locator, [locator]() { locator->refresh(); });
Orgad Shaneh's avatar
Orgad Shaneh committed
546 547
    connect(m_configureAction, &QAction::triggered, this, &LocatorWidget::showConfigureDialog);
    connect(m_fileLineEdit, &QLineEdit::textChanged,
548
        this, &LocatorWidget::showPopupDelayed);
549

550
    m_entriesWatcher = new QFutureWatcher<LocatorFilterEntry>(this);
551 552 553 554
    connect(m_entriesWatcher, &QFutureWatcher<LocatorFilterEntry>::resultsReadyAt,
            this, &LocatorWidget::addSearchResults);
    connect(m_entriesWatcher, &QFutureWatcher<LocatorFilterEntry>::finished,
            this, &LocatorWidget::handleSearchFinished);
555

556 557
    m_showPopupTimer.setInterval(100);
    m_showPopupTimer.setSingleShot(true);
Orgad Shaneh's avatar
Orgad Shaneh committed
558
    connect(&m_showPopupTimer, &QTimer::timeout, this, &LocatorWidget::showPopupNow);
Eike Ziller's avatar
Eike Ziller committed
559 560 561 562 563 564 565 566

    m_progressIndicator = new Utils::ProgressIndicator(Utils::ProgressIndicator::Small,
                                                       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);});
567 568 569 570 571 572 573 574 575

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

576
    connect(locator, &Locator::filtersChanged, this, &LocatorWidget::updateFilterList);
577
    updateFilterList();
con's avatar
con committed
578 579
}

580
void LocatorWidget::updatePlaceholderText(Command *command)
Eike Ziller's avatar
Eike Ziller committed
581
{
582 583 584 585 586 587
    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
588 589
}

590
void LocatorWidget::updateFilterList()
con's avatar
con committed
591 592
{
    m_filterMenu->clear();
593 594 595 596 597
    const QList<ILocatorFilter *> filters = Locator::filters();
    for (ILocatorFilter *filter : filters) {
        Command *cmd = ActionManager::command(filter->actionId());
        if (cmd)
            m_filterMenu->addAction(cmd->action());
598
    }
con's avatar
con committed
599 600 601 602 603
    m_filterMenu->addSeparator();
    m_filterMenu->addAction(m_refreshAction);
    m_filterMenu->addAction(m_configureAction);
}

604
bool LocatorWidget::eventFilter(QObject *obj, QEvent *event)
con's avatar
con committed
605
{
606 607 608 609 610 611 612 613 614 615 616
    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) {
617 618 619 620 621 622 623 624 625
        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:
626 627 628 629 630 631
        case Qt::Key_Down:
        case Qt::Key_Tab:
        case Qt::Key_Up:
        case Qt::Key_Backtab:
            emit showPopup();
            emit handleKey(keyEvent);
632
            return true;
633 634 635 636
        case Qt::Key_Home:
        case Qt::Key_End:
            if (Utils::HostOsInfo::isMacHost()
                    != (keyEvent->modifiers() == Qt::KeyboardModifiers(Qt::ControlModifier))) {
637 638
                emit showPopup();
                emit handleKey(keyEvent);
639 640 641
                return true;
            }
            break;
642 643
        case Qt::Key_Enter:
        case Qt::Key_Return:
644
            emit handleKey(keyEvent);
645 646
            return true;
        case Qt::Key_Escape:
647
            emit hidePopup();
648 649 650 651 652 653 654
            return true;
        case Qt::Key_Alt:
            if (keyEvent->modifiers() == Qt::AltModifier) {
                m_possibleToolTipRequest = true;
                return true;
            }
            break;
655 656
        case Qt::Key_P:
        case Qt::Key_N:
657 658 659
            if (keyEvent->modifiers() == Qt::KeyboardModifiers(Utils::HostOsInfo::controlModifier())) {
                emit showPopup();
                emit handleKey(keyEvent);
660 661 662
                return true;
            }
            break;
663 664 665 666 667 668 669
        default:
            break;
        }
    } else if (obj == m_fileLineEdit && event->type() == QEvent::KeyRelease) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
        if (m_possibleToolTipRequest) {
            m_possibleToolTipRequest = false;
670
            if ((keyEvent->key() == Qt::Key_Alt)
671
                    && (keyEvent->modifiers() == Qt::NoModifier)) {
672 673
                emit showCurrentItemToolTip();
                return true;
674 675 676
            }
        }
    } else if (obj == m_fileLineEdit && event->type() == QEvent::FocusOut) {
677
        emit lostFocus();
con's avatar
con committed
678
    } else if (obj == m_fileLineEdit && event->type() == QEvent::FocusIn) {
679 680 681
        QFocusEvent *fev = static_cast<QFocusEvent *>(event);
        if (fev->reason() != Qt::ActiveWindowFocusReason)
            showPopupNow();
682
    } else if (obj == this && event->type() == QEvent::ParentChange) {
683
        emit parentChanged();
con's avatar
con committed
684 685
    } else if (obj == this && event->type() == QEvent::ShortcutOverride) {
        QKeyEvent *ke = static_cast<QKeyEvent *>(event);
686 687 688 689
        switch (ke->key()) {
        case Qt::Key_Escape:
            if (!ke->modifiers()) {
                event->accept();
Orgad Shaneh's avatar
Orgad Shaneh committed
690
                QTimer::singleShot(0, this, &LocatorWidget::setFocusToCurrentMode);
691 692
                return true;
            }
693
            break;
694 695 696 697 698 699 700 701
        case Qt::Key_Alt:
            if (ke->modifiers() == Qt::AltModifier) {
                event->accept();
                return true;
            }
            break;
        default:
            break;
con's avatar
con committed
702 703 704 705 706
        }
    }
    return QWidget::eventFilter(obj, event);
}

707 708
void LocatorWidget::setFocusToCurrentMode()
{
Orgad Shaneh's avatar
Orgad Shaneh committed
709
    ModeManager::setFocusToCurrentMode();
710 711
}

712
void LocatorWidget::showPopupDelayed()
con's avatar
con committed
713
{
714
    m_updateRequested = true;
715
    m_showPopupTimer.start();
716 717 718 719
}

void LocatorWidget::showPopupNow()
{
720
    m_showPopupTimer.stop();
721
    updateCompletionList(m_fileLineEdit->text());
722
    emit showPopup();
con's avatar
con committed
723 724
}

Andre Hartmann's avatar
Andre Hartmann committed
725
QList<ILocatorFilter *> LocatorWidget::filtersFor(const QString &text, QString &searchText)
con's avatar
con committed
726
{
727 728 729 730 731 732 733
    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);
734
    const QList<ILocatorFilter *> filters = Locator::filters();
735 736
    if (whiteSpace >= 0) {
        const QString prefix = text.mid(firstNonSpace, whiteSpace - firstNonSpace).toLower();
737
        QList<ILocatorFilter *> prefixFilters;
738
        foreach (ILocatorFilter *filter, filters) {
con's avatar
con committed
739
            if (prefix == filter->shortcutString()) {
740
                searchText = text.mid(whiteSpace).trimmed();
741
                prefixFilters << filter;
con's avatar
con committed
742 743
            }
        }
744 745
        if (!prefixFilters.isEmpty())
            return prefixFilters;
con's avatar
con committed
746
    }
747
    searchText = text.trimmed();
748
    return Utils::filtered(filters, &ILocatorFilter::isIncludedByDefault);
con's avatar
con committed
749 750
}

Eike Ziller's avatar
Eike Ziller committed
751 752 753 754 755 756 757 758 759 760 761 762 763 764 765
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();
}

766 767
void LocatorWidget::updateCompletionList(const QString &text)
{
768
    m_updateRequested = true;
769 770 771 772 773 774 775 776 777 778
    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
779
    m_showProgressTimer.start();
780
    m_needsClearResult = true;
781
    QString searchText;
Andre Hartmann's avatar
Andre Hartmann committed
782
    const QList<ILocatorFilter *> filters = filtersFor(text, searchText);
783 784 785

    foreach (ILocatorFilter *filter, filters)
        filter->prepareSearch(searchText);
786
    QFuture<LocatorFilterEntry> future = Utils::runAsync(&runSearch, filters, searchText);
787 788 789
    m_entriesWatcher->setFuture(future);
}

790
void LocatorWidget::handleSearchFinished()
791
{
Eike Ziller's avatar
Eike Ziller committed
792 793
    m_showProgressTimer.stop();
    setProgressIndicatorVisible(false);
794
    m_updateRequested = false;
795 796 797
    if (m_rowRequestedForAccept >= 0) {
        acceptEntry(m_rowRequestedForAccept);
        m_rowRequestedForAccept = -1;
798 799
        return;
    }
800
    if (m_entriesWatcher->future().isCanceled()) {
801 802 803
        const QString text = m_requestedCompletionText;
        m_requestedCompletionText.clear();
        updateCompletionList(text);
804
        return;
805
    }
806

807 808 809 810
    if (m_needsClearResult) {
        m_locatorModel->clear();
        m_needsClearResult = false;
    }
811 812
}

813
void LocatorWidget::scheduleAcceptEntry(const QModelIndex &index)
814 815 816 817
{
    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
818
        m_rowRequestedForAccept = index.row();
819 820
        // do not wait for the rest of the search to finish
        m_entriesWatcher->future().cancel();
821
    } else {
822
        acceptEntry(index.row());
823
    }
con's avatar
con committed
824 825
}

826
void LocatorWidget::acceptEntry(int row)
con's avatar
con committed
827
{
828
    if (row < 0 || row >= m_locatorModel->rowCount())
829 830
        return;
    const QModelIndex index = m_locatorModel->index(row, 0);
con's avatar
con committed
831 832
    if (!index.isValid())
        return;
833 834
    const LocatorFilterEntry entry = m_locatorModel->data(index, ItemDataRoles::ResultItemRole).value<LocatorFilterEntry>();
    Q_ASSERT(entry.filter != nullptr);
835 836 837 838 839
    QString newText;
    int selectionStart = -1;
    int selectionLength = 0;
    entry.filter->accept(entry, &newText, &selectionStart, &selectionLength);
    if (newText.isEmpty()) {
840
        emit hidePopup();
841 842
        m_fileLineEdit->clearFocus();
    } else {
843
        showText(newText, selectionStart, selectionLength);
844
    }
con's avatar
con committed
845 846
}

847
void LocatorWidget::showText(const QString &text, int selectionStart, int selectionLength)
con's avatar
con committed
848
{
849 850
    if (!text.isEmpty())
        m_fileLineEdit->setText(text);
851 852
    m_fileLineEdit->setFocus();
    showPopupNow();
853
    ICore::raiseWindow(window());
854

con's avatar
con committed
855
    if (selectionStart >= 0) {
con's avatar
con committed
856
        m_fileLineEdit->setSelection(selectionStart, selectionLength);
con's avatar
con committed
857 858 859
        if (selectionLength == 0) // make sure the cursor is at the right position (Mac-vs.-rest difference)
            m_fileLineEdit->setCursorPosition(selectionStart);
    } else {
860
        m_fileLineEdit->selectAll();
con's avatar
con committed
861
    }
con's avatar
con committed
862 863
}

864
QString LocatorWidget::currentText() const
con's avatar
con committed
865
{
866
    return m_fileLineEdit->text();
con's avatar
con committed
867 868
}

869 870 871 872 873
QAbstractItemModel *LocatorWidget::model() const
{
    return m_locatorModel;
}

874
void LocatorWidget::showConfigureDialog()
con's avatar
con committed
875
{
876
    ICore::showOptionsDialog(Constants::FILTER_OPTIONS_PAGE);
con's avatar
con committed
877
}
878

879 880 881 882 883 884 885 886 887 888 889
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);
890
    if (selectFirst) {
891
        emit selectRow(0);
892 893 894
        if (m_rowRequestedForAccept >= 0)
            m_rowRequestedForAccept = 0;
    }
895 896
}

897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914
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;
}

915 916 917 918 919 920 921 922 923 924
CompletionDelegate::CompletionDelegate(QObject *parent)
    : SearchResultTreeItemDelegate(0, parent)
{
}

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

Orgad Shaneh's avatar
Orgad Shaneh committed
925
} // namespace Internal
926
} // namespace Core