/************************************************************************** ** ** This file is part of Qt Creator ** ** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies). ** ** Contact: http://www.qt-project.org/ ** ** ** GNU Lesser General Public License Usage ** ** 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. ** ** In addition, as a special exception, Nokia gives you certain additional ** rights. These rights are described in the Nokia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** Other Usage ** ** Alternatively, this file may be used in accordance with the terms and ** conditions contained in a signed written agreement between you and Nokia. ** ** **************************************************************************/ #include #include "locatorwidget.h" #include "locatorplugin.h" #include "locatorconstants.h" #include "ilocatorfilter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(Locator::ILocatorFilter*) Q_DECLARE_METATYPE(Locator::FilterEntry) namespace Locator { namespace Internal { /* A model to represent the Locator results. */ class LocatorModel : public QAbstractListModel { public: LocatorModel(QObject *parent = 0) : 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 &entries); //void setDisplayCount(int count); private: mutable QList mEntries; //int mDisplayCount; }; class CompletionList : public QTreeView { public: CompletionList(QWidget *parent = 0); void updatePreferredSize(); QSize preferredSize() const { return m_preferredSize; } #if defined(Q_OS_WIN) void focusOutEvent (QFocusEvent * event) { if (event->reason() == Qt::ActiveWindowFocusReason) hide(); } #endif void next() { int index = currentIndex().row(); ++index; if (index >= model()->rowCount(QModelIndex())) { // wrap index = 0; } setCurrentIndex(model()->index(index, 0)); } void previous() { int index = currentIndex().row(); --index; if (index < 0) { // wrap index = model()->rowCount(QModelIndex()) - 1; } setCurrentIndex(model()->index(index, 0)); } private: QSize m_preferredSize; }; } // namespace Internal uint qHash(const FilterEntry &entry) { if (entry.internalData.canConvert(QVariant::String)) return QT_PREPEND_NAMESPACE(qHash)(entry.internalData.toString()); return QT_PREPEND_NAMESPACE(qHash)(entry.internalData.constData()); } } // namespace Locator using namespace Locator; using namespace Locator::Internal; // =========== LocatorModel =========== int LocatorModel::rowCount(const QModelIndex & /* parent */) const { return mEntries.size(); } int LocatorModel::columnCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : 2; } /*! * When asked for the icon via Qt::DecorationRole, the LocatorModel lazily * resolves and caches the Greehouse-specific file icon when * FilterEntry::resolveFileIcon is true. FilterEntry::internalData is assumed * to be the filename. */ QVariant LocatorModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() >= mEntries.size()) return QVariant(); if (role == Qt::DisplayRole) { 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::ToolTipRole) { if (mEntries.at(index.row()).extraInfo.isEmpty()) return QVariant(mEntries.at(index.row()).displayName); else return QVariant(mEntries.at(index.row()).displayName + QLatin1String("\n\n") + 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; QString path = entry.internalData.toString(); Core::EditorManager::splitLineNumber(&path); entry.displayIcon = Core::FileIconProvider::instance()->icon(QFileInfo(path)); } 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(); } void LocatorModel::setEntries(const QList &entries) { mEntries = entries; reset(); } // =========== 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); #ifdef Q_OS_MAC if (horizontalScrollBar()) horizontalScrollBar()->setAttribute(Qt::WA_MacMiniSize); if (verticalScrollBar()) verticalScrollBar()->setAttribute(Qt::WA_MacMiniSize); #endif } void CompletionList::updatePreferredSize() { const QStyleOptionViewItem &option = viewOptions(); const QSize shint = itemDelegate()->sizeHint(option, model()->index(0, 0)); m_preferredSize = QSize(730, shint.height() * 17 + frameWidth() * 2); } // =========== LocatorWidget =========== LocatorWidget::LocatorWidget(LocatorPlugin *qop) : m_locatorPlugin(qop), m_locatorModel(new LocatorModel(this)), m_completionList(new CompletionList(this)), m_filterMenu(new QMenu(this)), m_refreshAction(new QAction(tr("Refresh"), this)), m_configureAction(new QAction(tr("Configure..."), this)), m_fileLineEdit(new Utils::FilterLineEdit), m_updateRequested(false), m_acceptRequested(false) { // Explicitly hide the completion list popup. m_completionList->hide(); setFocusProxy(m_fileLineEdit); setWindowTitle(tr("Locate...")); 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); setWindowIcon(QIcon(QLatin1String(":/locator/images/locator.png"))); const QPixmap image = QPixmap(QLatin1String(Core::Constants::ICON_MAGNIFIER)); m_fileLineEdit->setButtonPixmap(Utils::FancyLineEdit::Left, image); m_fileLineEdit->setButtonToolTip(Utils::FancyLineEdit::Left, tr("Options")); m_fileLineEdit->setFocusPolicy(Qt::ClickFocus); m_fileLineEdit->setButtonVisible(Utils::FancyLineEdit::Left, true); // We set click focus since otherwise you will always get two popups m_fileLineEdit->setButtonFocusPolicy(Utils::FancyLineEdit::Left, Qt::ClickFocus); m_fileLineEdit->setAttribute(Qt::WA_MacShowFocusRect, false); m_fileLineEdit->installEventFilter(this); this->installEventFilter(this); m_completionList->setModel(m_locatorModel); 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); m_fileLineEdit->setButtonMenu(Utils::FancyLineEdit::Left, m_filterMenu); connect(m_refreshAction, SIGNAL(triggered()), m_locatorPlugin, SLOT(refresh())); connect(m_configureAction, SIGNAL(triggered()), this, SLOT(showConfigureDialog())); connect(m_fileLineEdit, SIGNAL(textChanged(QString)), this, SLOT(showPopup())); connect(m_completionList, SIGNAL(activated(QModelIndex)), this, SLOT(scheduleAcceptCurrentEntry())); m_entriesWatcher = new QFutureWatcher(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())); } void LocatorWidget::setPlaceholderText(const QString &text) { m_fileLineEdit->setPlaceholderText(text); } void LocatorWidget::updateFilterList() { m_filterMenu->clear(); // update actions and menu QMap actionCopy = m_filterActionMap; m_filterActionMap.clear(); // register new actions, update existent foreach (ILocatorFilter *filter, m_locatorPlugin->filters()) { if (filter->shortcutString().isEmpty() || filter->isHidden()) continue; Core::Id locatorId = Core::Id(QLatin1String("Locator.") + filter->id()); QAction *action = 0; Core::Command *cmd = 0; if (!actionCopy.contains(filter->id())) { // register new action action = new QAction(filter->displayName(), this); cmd = Core::ActionManager::registerAction(action, locatorId, Core::Context(Core::Constants::C_GLOBAL)); cmd->setAttribute(Core::Command::CA_UpdateText); connect(action, SIGNAL(triggered()), this, SLOT(filterSelected())); action->setData(qVariantFromValue(filter)); } else { action = actionCopy.take(filter->id()); action->setText(filter->displayName()); cmd = Core::ActionManager::command(locatorId); } m_filterActionMap.insert(filter->id(), action); m_filterMenu->addAction(cmd->action()); } // unregister actions that are deleted now foreach (const QString &id, actionCopy.keys()) { Core::ActionManager::unregisterAction(actionCopy.value(id), Core::Id(QLatin1String("Locator.") + id)); } qDeleteAll(actionCopy); m_filterMenu->addSeparator(); m_filterMenu->addAction(m_refreshAction); m_filterMenu->addAction(m_configureAction); } bool LocatorWidget::eventFilter(QObject *obj, QEvent *event) { if (obj == m_fileLineEdit && event->type() == QEvent::KeyPress) { if (m_possibleToolTipRequest) m_possibleToolTipRequest = false; if (QToolTip::isVisible()) QToolTip::hideText(); QKeyEvent *keyEvent = static_cast(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: scheduleAcceptCurrentEntry(); return true; case Qt::Key_Escape: m_completionList->hide(); return true; case Qt::Key_Tab: m_completionList->next(); return true; case Qt::Key_Backtab: m_completionList->previous(); return true; case Qt::Key_Alt: if (keyEvent->modifiers() == Qt::AltModifier) { m_possibleToolTipRequest = true; return true; } break; default: break; } } else if (obj == m_fileLineEdit && event->type() == QEvent::KeyRelease) { QKeyEvent *keyEvent = static_cast(event); if (m_possibleToolTipRequest) { m_possibleToolTipRequest = false; if (m_completionList->isVisible() && (keyEvent->key() == Qt::Key_Alt) && (keyEvent->modifiers() == Qt::NoModifier)) { const QModelIndex index = m_completionList->currentIndex(); if (index.isValid()) { QToolTip::showText(m_completionList->pos() + m_completionList->visualRect(index).topRight(), m_locatorModel->data(index, Qt::ToolTipRole).toString()); return true; } } } } else if (obj == m_fileLineEdit && event->type() == QEvent::FocusOut) { #if defined(Q_OS_WIN) QFocusEvent *fev = static_cast(event); if (fev->reason() != Qt::ActiveWindowFocusReason || (fev->reason() == Qt::ActiveWindowFocusReason && !m_completionList->isActiveWindow())) #endif m_completionList->hide(); } else if (obj == m_fileLineEdit && event->type() == QEvent::FocusIn) { showPopupNow(); } else if (obj == this && event->type() == QEvent::ShortcutOverride) { QKeyEvent *ke = static_cast(event); switch (ke->key()) { case Qt::Key_Escape: if (!ke->modifiers()) { event->accept(); QTimer::singleShot(0, this, SLOT(setFocusToCurrentMode())); return true; } case Qt::Key_Alt: if (ke->modifiers() == Qt::AltModifier) { event->accept(); return true; } break; default: break; } } return QWidget::eventFilter(obj, event); } void LocatorWidget::setFocusToCurrentMode() { Core::ModeManager::setFocusToCurrentMode(); } void LocatorWidget::showCompletionList() { 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(); } void LocatorWidget::showPopup() { m_updateRequested = true; m_showPopupTimer->start(); } void LocatorWidget::showPopupNow() { m_showPopupTimer->stop(); updateCompletionList(m_fileLineEdit->text()); showCompletionList(); } QList LocatorWidget::filtersFor(const QString &text, QString &searchText) { QList filters = m_locatorPlugin->filters(); const int whiteSpace = text.indexOf(QLatin1Char(' ')); QString prefix; if (whiteSpace >= 0) prefix = text.left(whiteSpace); if (!prefix.isEmpty()) { prefix = prefix.toLower(); QList prefixFilters; foreach (ILocatorFilter *filter, filters) { if (prefix == filter->shortcutString()) { searchText = text.mid(whiteSpace+1); prefixFilters << filter; } } if (!prefixFilters.isEmpty()) return prefixFilters; } searchText = text; QList activeFilters; foreach (ILocatorFilter *filter, filters) if (filter->isIncludedByDefault()) activeFilters << filter; return activeFilters; } static void filter_helper(QFutureInterface &entries, QList filters, QString searchText) { QSet alreadyAdded; const bool checkDuplicates = (filters.size() > 1); foreach (ILocatorFilter *filter, filters) { if (entries.isCanceled()) break; foreach (const FilterEntry &entry, filter->matchesFor(entries, searchText)) { if (checkDuplicates && alreadyAdded.contains(entry)) continue; entries.reportResult(entry); if (checkDuplicates) alreadyAdded.insert(entry); } } } void LocatorWidget::updateCompletionList(const QString &text) { m_updateRequested = true; QString searchText; const QList filters = filtersFor(text, searchText); // cancel the old future m_entriesWatcher->future().cancel(); m_entriesWatcher->future().waitForFinished(); QFuture future = QtConcurrent::run(filter_helper, filters, searchText); m_entriesWatcher->setFuture(future); } void LocatorWidget::updateEntries() { m_updateRequested = false; if (m_entriesWatcher->future().isCanceled()) { // reset to usable state m_acceptRequested = false; return; } const QList entries = m_entriesWatcher->future().results(); m_locatorModel->setEntries(entries); if (m_locatorModel->rowCount() > 0) { m_completionList->setCurrentIndex(m_locatorModel->index(0, 0)); } #if 0 m_completionList->updatePreferredSize(); #endif if (m_acceptRequested) acceptCurrentEntry(); } void LocatorWidget::scheduleAcceptCurrentEntry() { 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 m_acceptRequested = true; } else { acceptCurrentEntry(); } } void LocatorWidget::acceptCurrentEntry() { m_acceptRequested = false; if (!m_completionList->isVisible()) return; const QModelIndex index = m_completionList->currentIndex(); if (!index.isValid()) return; const FilterEntry entry = m_locatorModel->data(index, Qt::UserRole).value(); m_completionList->hide(); entry.filter->accept(entry); } void LocatorWidget::show(const QString &text, int selectionStart, int selectionLength) { if (!text.isEmpty()) m_fileLineEdit->setText(text); if (!m_fileLineEdit->hasFocus()) m_fileLineEdit->setFocus(); else showPopupNow(); if (selectionStart >= 0) { m_fileLineEdit->setSelection(selectionStart, selectionLength); if (selectionLength == 0) // make sure the cursor is at the right position (Mac-vs.-rest difference) m_fileLineEdit->setCursorPosition(selectionStart); } else { m_fileLineEdit->selectAll(); } } void LocatorWidget::filterSelected() { QString searchText = tr(""); QAction *action = qobject_cast(sender()); QTC_ASSERT(action, return); ILocatorFilter *filter = action->data().value(); QTC_ASSERT(filter, return); QString currentText = m_fileLineEdit->text().trimmed(); // add shortcut string at front or replace existing shortcut string if (!currentText.isEmpty()) { searchText = currentText; foreach (ILocatorFilter *otherfilter, m_locatorPlugin->filters()) { if (currentText.startsWith(otherfilter->shortcutString() + QLatin1Char(' '))) { searchText = currentText.mid(otherfilter->shortcutString().length()+1); break; } } } show(filter->shortcutString() + QLatin1Char(' ') + searchText, filter->shortcutString().length() + 1, searchText.length()); updateCompletionList(m_fileLineEdit->text()); m_fileLineEdit->setFocus(); } void LocatorWidget::showEvent(QShowEvent *event) { QWidget::showEvent(event); } void LocatorWidget::showConfigureDialog() { Core::ICore::showOptionsDialog(QLatin1String(Core::Constants::SETTINGS_CATEGORY_CORE), QLatin1String(Constants::FILTER_OPTIONS_PAGE)); }