1 /**************************************************************************
3 ** This file is part of Qt Creator
5 ** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies).
7 ** Contact: Nokia Corporation (qt-info@nokia.com)
11 ** This file contains pre-release code and may not be distributed.
12 ** You may use this file in accordance with the terms and conditions
13 ** contained in the Technology Preview License Agreement accompanying
16 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 2.1 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 2.1 requirements
23 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
25 ** In addition, as a special exception, Nokia gives you certain additional
26 ** rights. These rights are described in the Nokia Qt LGPL Exception
27 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
29 ** If you have questions regarding the use of this file, please contact
30 ** Nokia at qt-info@nokia.com.
32 **************************************************************************/
36 #include "locatorwidget.h"
37 #include "locatorplugin.h"
38 #include "locatorconstants.h"
39 #include "ilocatorfilter.h"
41 #include <extensionsystem/pluginmanager.h>
42 #include <coreplugin/icore.h>
43 #include <coreplugin/modemanager.h>
44 #include <coreplugin/actionmanager/actionmanager.h>
45 #include <coreplugin/actionmanager/command.h>
46 #include <coreplugin/coreconstants.h>
47 #include <coreplugin/fileiconprovider.h>
48 #include <utils/filterlineedit.h>
49 #include <utils/qtcassert.h>
50 #include <qtconcurrent/runextensions.h>
52 #include <QtCore/QtConcurrentRun>
53 #include <QtCore/QFileInfo>
54 #include <QtCore/QFile>
55 #include <QtCore/QTimer>
56 #include <QtCore/QSettings>
57 #include <QtCore/QEvent>
58 #include <QtGui/QAction>
59 #include <QtGui/QApplication>
60 #include <QtGui/QContextMenuEvent>
61 #include <QtGui/QHBoxLayout>
62 #include <QtGui/QHeaderView>
63 #include <QtGui/QKeyEvent>
64 #include <QtGui/QLabel>
65 #include <QtGui/QLineEdit>
66 #include <QtGui/QMenu>
67 #include <QtGui/QMouseEvent>
68 #include <QtGui/QPushButton>
69 #include <QtGui/QScrollBar>
70 #include <QtGui/QTreeView>
72 Q_DECLARE_METATYPE(Locator::ILocatorFilter*)
73 Q_DECLARE_METATYPE(Locator::FilterEntry)
78 /* A model to represent the Locator results. */
79 class LocatorModel : public QAbstractListModel
82 LocatorModel(QObject *parent = 0)
83 : QAbstractListModel(parent)
84 // , mDisplayCount(64)
87 int rowCount(const QModelIndex &parent = QModelIndex()) const;
88 int columnCount(const QModelIndex &parent = QModelIndex()) const;
89 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
91 void setEntries(const QList<FilterEntry> &entries);
92 //void setDisplayCount(int count);
95 mutable QList<FilterEntry> mEntries;
99 class CompletionList : public QTreeView
102 CompletionList(QWidget *parent = 0);
104 void updatePreferredSize();
105 QSize preferredSize() const { return m_preferredSize; }
107 #if defined(Q_OS_WIN)
108 void focusOutEvent (QFocusEvent * event) {
109 if (event->reason() == Qt::ActiveWindowFocusReason)
115 int index = currentIndex().row();
117 if (index >= model()->rowCount(QModelIndex())) {
121 setCurrentIndex(model()->index(index, 0));
125 int index = currentIndex().row();
129 index = model()->rowCount(QModelIndex()) - 1;
131 setCurrentIndex(model()->index(index, 0));
135 QSize m_preferredSize;
138 } // namespace Internal
140 uint qHash(const FilterEntry &entry)
142 if (entry.internalData.canConvert(QVariant::String))
143 return QT_PREPEND_NAMESPACE(qHash)(entry.internalData.toString());
144 return QT_PREPEND_NAMESPACE(qHash)(entry.internalData.constData());
147 } // namespace Locator
149 using namespace Locator;
150 using namespace Locator::Internal;
152 // =========== LocatorModel ===========
154 int LocatorModel::rowCount(const QModelIndex & /* parent */) const
156 return mEntries.size();
159 int LocatorModel::columnCount(const QModelIndex &parent) const
161 return parent.isValid() ? 0 : 2;
165 * When asked for the icon via Qt::DecorationRole, the LocatorModel lazily
166 * resolves and caches the Greehouse-specific file icon when
167 * FilterEntry::resolveFileIcon is true. FilterEntry::internalData is assumed
168 * to be the filename.
170 QVariant LocatorModel::data(const QModelIndex &index, int role) const
172 if (!index.isValid() || index.row() >= mEntries.size())
175 if (role == Qt::DisplayRole || role == Qt::ToolTipRole) {
176 if (index.column() == 0) {
177 return mEntries.at(index.row()).displayName;
178 } else if (index.column() == 1) {
179 return mEntries.at(index.row()).extraInfo;
181 } else if (role == Qt::DecorationRole && index.column() == 0) {
182 FilterEntry &entry = mEntries[index.row()];
183 if (entry.resolveFileIcon && entry.displayIcon.isNull()) {
184 entry.resolveFileIcon = false;
186 Core::FileIconProvider::instance()->icon(QFileInfo(entry.internalData.toString()));
188 return entry.displayIcon;
189 } else if (role == Qt::ForegroundRole && index.column() == 1) {
191 } else if (role == Qt::UserRole) {
192 return qVariantFromValue(mEntries.at(index.row()));
198 void LocatorModel::setEntries(const QList<FilterEntry> &entries)
204 void LocatorModel::setDisplayCount(int count)
206 // TODO: This method is meant to be used for increasing the number of items displayed at the
207 // user's request. There is however no way yet for the user to request this.
208 if (count == mDisplayCount)
211 const int displayedOld = qMin(mDisplayCount, mEntries.size());
212 const int displayedNew = qMin(count, mEntries.size());
214 if (displayedNew < displayedOld)
215 beginRemoveRows(QModelIndex(), displayedNew - 1, displayedOld - 1);
216 else if (displayedNew > displayedOld)
217 beginInsertRows(QModelIndex(), displayedOld - 1, displayedNew - 1);
219 mDisplayCount = count;
221 if (displayedNew < displayedOld)
223 else if (displayedNew > displayedOld)
228 // =========== CompletionList ===========
230 CompletionList::CompletionList(QWidget *parent)
233 setRootIsDecorated(false);
234 setUniformRowHeights(true);
235 setMaximumWidth(900);
237 header()->setStretchLastSection(true);
238 // This is too slow when done on all results
239 //header()->setResizeMode(QHeaderView::ResizeToContents);
240 setWindowFlags(Qt::ToolTip);
242 if (horizontalScrollBar())
243 horizontalScrollBar()->setAttribute(Qt::WA_MacMiniSize);
244 if (verticalScrollBar())
245 verticalScrollBar()->setAttribute(Qt::WA_MacMiniSize);
249 void CompletionList::updatePreferredSize()
251 //header()->setStretchLastSection(false);
252 //updateGeometries();
254 const QStyleOptionViewItem &option = viewOptions();
255 const QSize shint = itemDelegate()->sizeHint(option, model()->index(0, 0));
257 const int visibleItems = model()->rowCount();
259 // TODO: Look into enabling preferred height as well. Current problem is that this doesn't
260 // work nicely from the user perspective if the list is popping up instead of down.
261 //const int h = shint.height() * visibleItems;
263 const QScrollBar *vscroll = verticalScrollBar();
264 int preferredWidth = header()->length() + frameWidth() * 2
265 + (vscroll->isVisibleTo(this) ? vscroll->width() : 0);
266 const int diff = preferredWidth - width();
268 // Avoid resizing the list widget when there are no results or when the preferred size is
269 // only a little smaller than our current size
270 if (visibleItems == 0 || (diff > -100 && diff < 0))
271 preferredWidth = width();
274 m_preferredSize = QSize(730, //qMax(600, preferredWidth),
275 shint.height() * 17 + frameWidth() * 2);
276 //header()->setStretchLastSection(true);
279 // =========== LocatorWidget ===========
281 LocatorWidget::LocatorWidget(LocatorPlugin *qop) :
282 m_locatorPlugin(qop),
283 m_locatorModel(new LocatorModel(this)),
284 m_completionList(new CompletionList(this)),
285 m_filterMenu(new QMenu(this)),
286 m_refreshAction(new QAction(tr("Refresh"), this)),
287 m_configureAction(new QAction(tr("Configure..."), this)),
288 m_fileLineEdit(new Utils::FilterLineEdit)
290 // Explicitly hide the completion list popup.
291 m_completionList->hide();
293 setFocusProxy(m_fileLineEdit);
294 setWindowTitle(tr("Locate..."));
296 QSizePolicy sizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
297 sizePolicy.setHorizontalStretch(0);
298 sizePolicy.setVerticalStretch(0);
299 setSizePolicy(sizePolicy);
300 setMinimumSize(QSize(200, 0));
302 QHBoxLayout *layout = new QHBoxLayout(this);
304 layout->setMargin(0);
305 layout->addWidget(m_fileLineEdit);
307 setWindowIcon(QIcon(QLatin1String(":/locator/images/locator.png")));
308 QPixmap image(Core::Constants::ICON_MAGNIFIER);
309 m_fileLineEdit->setButtonPixmap(Utils::FancyLineEdit::Left, image);
310 m_fileLineEdit->setPlaceholderText(tr("Type to locate"));
311 m_fileLineEdit->setButtonToolTip(Utils::FancyLineEdit::Left, tr("Options"));
312 m_fileLineEdit->setFocusPolicy(Qt::ClickFocus);
313 m_fileLineEdit->setButtonVisible(Utils::FancyLineEdit::Left, true);
314 // We set click focus since otherwise you will always get two popups
315 m_fileLineEdit->setButtonFocusPolicy(Utils::FancyLineEdit::Left, Qt::ClickFocus);
316 m_fileLineEdit->setAttribute(Qt::WA_MacShowFocusRect, false);
318 m_fileLineEdit->installEventFilter(this);
319 this->installEventFilter(this);
321 m_completionList->setModel(m_locatorModel);
322 m_completionList->header()->resizeSection(0, 300);
323 m_completionList->updatePreferredSize();
324 m_completionList->resize(m_completionList->preferredSize());
326 m_filterMenu->addAction(m_refreshAction);
327 m_filterMenu->addAction(m_configureAction);
329 m_fileLineEdit->setButtonMenu(Utils::FancyLineEdit::Left, m_filterMenu);
331 connect(m_refreshAction, SIGNAL(triggered()), m_locatorPlugin, SLOT(refresh()));
332 connect(m_configureAction, SIGNAL(triggered()), this, SLOT(showConfigureDialog()));
333 connect(m_fileLineEdit, SIGNAL(textChanged(const QString&)),
334 this, SLOT(showPopup()));
335 connect(m_completionList, SIGNAL(activated(QModelIndex)),
336 this, SLOT(acceptCurrentEntry()));
338 m_entriesWatcher = new QFutureWatcher<FilterEntry>(this);
339 connect(m_entriesWatcher, SIGNAL(finished()), SLOT(updateEntries()));
341 m_showPopupTimer = new QTimer(this);
342 m_showPopupTimer->setInterval(100);
343 m_showPopupTimer->setSingleShot(true);
344 connect(m_showPopupTimer, SIGNAL(timeout()), SLOT(showPopupNow()));
347 void LocatorWidget::updateFilterList()
349 m_filterMenu->clear();
351 // update actions and menu
352 Core::ActionManager *am = Core::ICore::instance()->actionManager();
353 QMap<QString, QAction *> actionCopy = m_filterActionMap;
354 m_filterActionMap.clear();
355 // register new actions, update existent
356 foreach (ILocatorFilter *filter, m_locatorPlugin->filters()) {
357 if (filter->shortcutString().isEmpty() || filter->isHidden())
359 QString locatorId = QLatin1String("Locator.") + filter->id();
361 Core::Command *cmd = 0;
362 if (!actionCopy.contains(filter->id())) {
363 // register new action
364 action = new QAction(filter->displayName(), this);
365 cmd = am->registerAction(action, locatorId,
366 Core::Context(Core::Constants::C_GLOBAL));
367 cmd->setAttribute(Core::Command::CA_UpdateText);
368 connect(action, SIGNAL(triggered()), this, SLOT(filterSelected()));
369 action->setData(qVariantFromValue(filter));
371 action = actionCopy.take(filter->id());
372 action->setText(filter->displayName());
373 cmd = am->command(locatorId);
375 m_filterActionMap.insert(filter->id(), action);
376 m_filterMenu->addAction(cmd->action());
379 // unregister actions that are deleted now
380 foreach (const QString &id, actionCopy.keys()) {
381 am->unregisterAction(actionCopy.value(id), QString(QLatin1String("Locator.") + id));
383 qDeleteAll(actionCopy);
385 m_filterMenu->addSeparator();
386 m_filterMenu->addAction(m_refreshAction);
387 m_filterMenu->addAction(m_configureAction);
390 bool LocatorWidget::eventFilter(QObject *obj, QEvent *event)
392 if (obj == m_fileLineEdit && event->type() == QEvent::KeyPress) {
393 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
394 switch (keyEvent->key()) {
398 case Qt::Key_PageDown:
399 showCompletionList();
400 QApplication::sendEvent(m_completionList, event);
404 acceptCurrentEntry();
407 m_completionList->hide();
410 m_completionList->next();
412 case Qt::Key_Backtab:
413 m_completionList->previous();
418 } else if (obj == m_fileLineEdit && event->type() == QEvent::FocusOut) {
419 #if defined(Q_OS_WIN)
420 QFocusEvent *fev = static_cast<QFocusEvent*>(event);
421 if (fev->reason() != Qt::ActiveWindowFocusReason ||
422 (fev->reason() == Qt::ActiveWindowFocusReason && !m_completionList->isActiveWindow()))
424 m_completionList->hide();
425 } else if (obj == m_fileLineEdit && event->type() == QEvent::FocusIn) {
427 } else if (obj == this && event->type() == QEvent::ShortcutOverride) {
428 QKeyEvent *ke = static_cast<QKeyEvent *>(event);
429 if (ke->key() == Qt::Key_Escape && !ke->modifiers()) {
431 QTimer::singleShot(0, Core::ModeManager::instance(), SLOT(setFocusToCurrentMode()));
435 return QWidget::eventFilter(obj, event);
438 void LocatorWidget::showCompletionList()
440 const int border = m_completionList->frameWidth();
441 const QSize size = m_completionList->preferredSize();
442 const QRect rect(mapToGlobal(QPoint(-border, -size.height() - border)), size);
443 m_completionList->setGeometry(rect);
444 m_completionList->show();
447 void LocatorWidget::showPopup()
449 m_showPopupTimer->start();
452 void LocatorWidget::showPopupNow()
454 m_showPopupTimer->stop();
455 updateCompletionList(m_fileLineEdit->text());
456 showCompletionList();
459 QList<ILocatorFilter*> LocatorWidget::filtersFor(const QString &text, QString &searchText)
461 QList<ILocatorFilter*> filters = m_locatorPlugin->filters();
462 const int whiteSpace = text.indexOf(QLatin1Char(' '));
465 prefix = text.left(whiteSpace);
466 if (!prefix.isEmpty()) {
467 prefix = prefix.toLower();
468 QList<ILocatorFilter *> prefixFilters;
469 foreach (ILocatorFilter *filter, filters) {
470 if (prefix == filter->shortcutString()) {
471 searchText = text.mid(whiteSpace+1);
472 prefixFilters << filter;
475 if (!prefixFilters.isEmpty())
476 return prefixFilters;
479 QList<ILocatorFilter*> activeFilters;
480 foreach (ILocatorFilter *filter, filters)
481 if (filter->isIncludedByDefault())
482 activeFilters << filter;
483 return activeFilters;
486 static void filter_helper(QFutureInterface<Locator::FilterEntry> &entries, QList<ILocatorFilter *> filters, QString searchText)
488 QSet<FilterEntry> alreadyAdded;
489 const bool checkDuplicates = (filters.size() > 1);
490 foreach (ILocatorFilter *filter, filters) {
491 if (entries.isCanceled())
494 foreach (const FilterEntry &entry, filter->matchesFor(entries, searchText)) {
495 if (checkDuplicates && alreadyAdded.contains(entry))
497 entries.reportResult(entry);
499 alreadyAdded.insert(entry);
504 void LocatorWidget::updateCompletionList(const QString &text)
507 const QList<ILocatorFilter*> filters = filtersFor(text, searchText);
509 // cancel the old future
510 m_entriesWatcher->future().cancel();
511 m_entriesWatcher->future().waitForFinished();
513 QFuture<FilterEntry> future = QtConcurrent::run(filter_helper, filters, searchText);
514 m_entriesWatcher->setFuture(future);
517 void LocatorWidget::updateEntries()
519 if (m_entriesWatcher->future().isCanceled())
522 const QList<FilterEntry> entries = m_entriesWatcher->future().results();
523 m_locatorModel->setEntries(entries);
524 if (m_locatorModel->rowCount() > 0) {
525 m_completionList->setCurrentIndex(m_locatorModel->index(0, 0));
528 m_completionList->updatePreferredSize();
532 void LocatorWidget::acceptCurrentEntry()
534 if (!m_completionList->isVisible())
536 const QModelIndex index = m_completionList->currentIndex();
537 if (!index.isValid())
539 const FilterEntry entry = m_locatorModel->data(index, Qt::UserRole).value<FilterEntry>();
540 m_completionList->hide();
541 entry.filter->accept(entry);
544 void LocatorWidget::show(const QString &text, int selectionStart, int selectionLength)
547 m_fileLineEdit->setText(text);
548 if (!m_fileLineEdit->hasFocus())
549 m_fileLineEdit->setFocus();
553 if (selectionStart >= 0) {
554 m_fileLineEdit->setSelection(selectionStart, selectionLength);
555 if (selectionLength == 0) // make sure the cursor is at the right position (Mac-vs.-rest difference)
556 m_fileLineEdit->setCursorPosition(selectionStart);
558 m_fileLineEdit->selectAll();
562 void LocatorWidget::filterSelected()
564 QString searchText = tr("<type here>");
565 QAction *action = qobject_cast<QAction*>(sender());
566 QTC_ASSERT(action, return);
567 ILocatorFilter *filter = action->data().value<ILocatorFilter*>();
568 QTC_ASSERT(filter, return);
569 QString currentText = m_fileLineEdit->text().trimmed();
570 // add shortcut string at front or replace existing shortcut string
571 if (!currentText.isEmpty()) {
572 searchText = currentText;
573 foreach (ILocatorFilter *otherfilter, m_locatorPlugin->filters()) {
574 if (currentText.startsWith(otherfilter->shortcutString() + QLatin1Char(' '))) {
575 searchText = currentText.mid(otherfilter->shortcutString().length()+1);
580 show(filter->shortcutString() + QLatin1Char(' ') + searchText,
581 filter->shortcutString().length() + 1,
582 searchText.length());
583 updateCompletionList(m_fileLineEdit->text());
584 m_fileLineEdit->setFocus();
587 void LocatorWidget::showEvent(QShowEvent *event)
589 QWidget::showEvent(event);
592 void LocatorWidget::showConfigureDialog()
594 Core::ICore::instance()->showOptionsDialog(Constants::LOCATOR_CATEGORY,
595 Constants::FILTER_OPTIONS_PAGE);