1 /**************************************************************************
3 ** This file is part of Qt Creator
5 ** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies).
7 ** Contact: Nokia Corporation (info@qt.nokia.com)
10 ** GNU Lesser General Public License Usage
12 ** This file may be used under the terms of the GNU Lesser General Public
13 ** License version 2.1 as published by the Free Software Foundation and
14 ** appearing in the file LICENSE.LGPL included in the packaging of this file.
15 ** Please review the following information to ensure the GNU Lesser General
16 ** Public License version 2.1 requirements will be met:
17 ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
19 ** In addition, as a special exception, Nokia gives you certain additional
20 ** rights. These rights are described in the Nokia Qt LGPL Exception
21 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
25 ** Alternatively, this file may be used in accordance with the terms and
26 ** conditions contained in a signed written agreement between you and Nokia.
28 ** If you have questions regarding the use of this file, please contact
29 ** Nokia at info@qt.nokia.com.
31 **************************************************************************/
33 #include "genericproposalwidget.h"
34 #include "iassistprovider.h"
35 #include "igenericproposalmodel.h"
36 #include "iassistproposalitem.h"
37 #include "genericproposal.h"
38 #include "codeassistant.h"
40 #include <texteditor/texteditorsettings.h>
41 #include <texteditor/completionsettings.h>
43 #include <utils/faketooltip.h>
45 #include <QtCore/QRect>
46 #include <QtCore/QLatin1String>
47 #include <QtCore/QAbstractListModel>
48 #include <QtCore/QPointer>
49 #include <QtCore/QDebug>
50 #include <QtCore/QTimer>
51 #include <QtGui/QApplication>
52 #include <QtGui/QVBoxLayout>
53 #include <QtGui/QListView>
54 #include <QtGui/QAbstractItemView>
55 #include <QtGui/QScrollBar>
56 #include <QtGui/QKeyEvent>
57 #include <QtGui/QDesktopWidget>
58 #include <QtGui/QLabel>
60 namespace TextEditor {
65 class ModelAdapter : public QAbstractListModel
70 ModelAdapter(IGenericProposalModel *completionModel, QWidget *parent);
72 virtual int rowCount(const QModelIndex &) const;
73 virtual QVariant data(const QModelIndex &index, int role) const;
76 IGenericProposalModel *m_completionModel;
79 ModelAdapter::ModelAdapter(IGenericProposalModel *completionModel, QWidget *parent)
80 : QAbstractListModel(parent)
81 , m_completionModel(completionModel)
84 int ModelAdapter::rowCount(const QModelIndex &) const
86 return m_completionModel->size();
89 QVariant ModelAdapter::data(const QModelIndex &index, int role) const
91 if (index.row() >= m_completionModel->size())
94 if (role == Qt::DisplayRole) {
95 return m_completionModel->text(index.row());
96 } else if (role == Qt::DecorationRole) {
97 return m_completionModel->icon(index.row());
98 } else if (role == Qt::WhatsThisRole) {
99 return m_completionModel->detail(index.row());
105 // ------------------------
106 // GenericProposalInfoFrame
107 // ------------------------
108 class GenericProposalInfoFrame : public Utils::FakeToolTip
111 GenericProposalInfoFrame(QWidget *parent = 0)
112 : Utils::FakeToolTip(parent), m_label(new QLabel(this))
114 QVBoxLayout *layout = new QVBoxLayout(this);
115 layout->setMargin(0);
116 layout->setSpacing(0);
117 layout->addWidget(m_label);
119 m_label->setForegroundRole(QPalette::ToolTipText);
120 m_label->setBackgroundRole(QPalette::ToolTipBase);
123 void setText(const QString &text)
125 m_label->setText(text);
132 // -----------------------
133 // GenericProposalListView
134 // -----------------------
135 class GenericProposalListView : public QListView
138 GenericProposalListView(QWidget *parent) : QListView(parent) {}
140 QSize calculateSize() const;
141 QPoint infoFramePos() const;
143 int rowSelected() const { return currentIndex().row(); }
144 bool isFirstRowSelected() const { return rowSelected() == 0; }
145 bool isLastRowSelected() const { return rowSelected() == model()->rowCount() - 1; }
146 void selectRow(int row) { setCurrentIndex(model()->index(row, 0)); }
147 void selectFirstRow() { selectRow(0); }
148 void selectLastRow() { selectRow(model()->rowCount() - 1); }
151 QSize GenericProposalListView::calculateSize() const
153 static const int maxVisibleItems = 10;
155 // Determine size by calculating the space of the visible items
156 int visibleItems = model()->rowCount();
157 if (visibleItems > maxVisibleItems)
158 visibleItems = maxVisibleItems;
160 const QStyleOptionViewItem &option = viewOptions();
162 for (int i = 0; i < visibleItems; ++i) {
163 QSize tmp = itemDelegate()->sizeHint(option, model()->index(i, 0));
164 if (shint.width() < tmp.width())
167 shint.rheight() *= visibleItems;
172 QPoint GenericProposalListView::infoFramePos() const
174 const QRect &r = rectForIndex(currentIndex());
175 QPoint p((parentWidget()->mapToGlobal(
176 parentWidget()->rect().topRight())).x() + 3,
177 mapToGlobal(r.topRight()).y() - verticalOffset()
182 // ----------------------------
183 // GenericProposalWidgetPrivate
184 // ----------------------------
185 class GenericProposalWidgetPrivate : public QObject
190 GenericProposalWidgetPrivate(QWidget *completionWidget);
192 const QWidget *m_underlyingWidget;
193 GenericProposalListView *m_completionListView;
194 IGenericProposalModel *m_model;
196 bool m_isSynchronized;
197 bool m_explicitlySelected;
198 AssistReason m_reason;
200 QPointer<GenericProposalInfoFrame> m_infoFrame;
202 CodeAssistant *m_assistant;
205 void handleActivation(const QModelIndex &modelIndex);
206 void maybeShowInfoTip();
209 GenericProposalWidgetPrivate::GenericProposalWidgetPrivate(QWidget *completionWidget)
210 : m_underlyingWidget(0)
211 , m_completionListView(new GenericProposalListView(completionWidget))
213 , m_isSynchronized(true)
214 , m_explicitlySelected(false)
215 , m_gotContent(false)
218 connect(m_completionListView, SIGNAL(activated(QModelIndex)),
219 this, SLOT(handleActivation(QModelIndex)));
221 m_infoTimer.setInterval(1000);
222 m_infoTimer.setSingleShot(true);
223 connect(&m_infoTimer, SIGNAL(timeout()), SLOT(maybeShowInfoTip()));
226 void GenericProposalWidgetPrivate::handleActivation(const QModelIndex &modelIndex)
228 static_cast<GenericProposalWidget *>
229 (m_completionListView->parent())->notifyActivation(modelIndex.row());
232 void GenericProposalWidgetPrivate::maybeShowInfoTip()
234 const QModelIndex ¤t = m_completionListView->currentIndex();
235 if (!current.isValid())
238 const QString &infoTip = current.data(Qt::WhatsThisRole).toString();
239 if (infoTip.isEmpty()) {
240 delete m_infoFrame.data();
241 m_infoTimer.setInterval(200);
245 if (m_infoFrame.isNull())
246 m_infoFrame = new GenericProposalInfoFrame(m_completionListView);
248 m_infoFrame->move(m_completionListView->infoFramePos());
249 m_infoFrame->setText(infoTip);
250 m_infoFrame->adjustSize();
252 m_infoFrame->raise();
254 m_infoTimer.setInterval(0);
257 // ------------------------
258 // GenericProposalWidget
259 // ------------------------
260 GenericProposalWidget::GenericProposalWidget()
261 : m_d(new GenericProposalWidgetPrivate(this))
264 if (m_d->m_completionListView->horizontalScrollBar())
265 m_d->m_completionListView->horizontalScrollBar()->setAttribute(Qt::WA_MacMiniSize);
266 if (m_d->m_completionListView->verticalScrollBar())
267 m_d->m_completionListView->verticalScrollBar()->setAttribute(Qt::WA_MacMiniSize);
269 // This improves the look with QGTKStyle.
270 setFrameStyle(m_d->m_completionListView->frameStyle());
272 m_d->m_completionListView->setFrameStyle(QFrame::NoFrame);
273 m_d->m_completionListView->setAttribute(Qt::WA_MacShowFocusRect, false);
274 m_d->m_completionListView->setUniformItemSizes(true);
275 m_d->m_completionListView->setSelectionBehavior(QAbstractItemView::SelectItems);
276 m_d->m_completionListView->setSelectionMode(QAbstractItemView::SingleSelection);
277 m_d->m_completionListView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
278 m_d->m_completionListView->setMinimumSize(1, 1);
280 QVBoxLayout *layout = new QVBoxLayout(this);
281 layout->setMargin(0);
282 layout->addWidget(m_d->m_completionListView);
284 m_d->m_completionListView->installEventFilter(this);
286 setObjectName(QLatin1String("m_popupFrame"));
287 setMinimumSize(1, 1);
290 GenericProposalWidget::~GenericProposalWidget()
295 void GenericProposalWidget::setAssistant(CodeAssistant *assistant)
297 m_d->m_assistant = assistant;
300 void GenericProposalWidget::setReason(AssistReason reason)
302 m_d->m_reason = reason;
305 void GenericProposalWidget::setUnderlyingWidget(const QWidget *underlyingWidget)
307 setFont(underlyingWidget->font());
308 m_d->m_underlyingWidget = underlyingWidget;
311 void GenericProposalWidget::setModel(IAssistProposalModel *model)
314 m_d->m_model = static_cast<IGenericProposalModel *>(model);
315 m_d->m_completionListView->setModel(new ModelAdapter(m_d->m_model, m_d->m_completionListView));
317 connect(m_d->m_completionListView->selectionModel(),
318 SIGNAL(currentChanged(QModelIndex,QModelIndex)),
323 void GenericProposalWidget::setDisplayRect(const QRect &rect)
325 m_d->m_displayRect = rect;
328 void GenericProposalWidget::setIsSynchronized(bool isSync)
330 m_d->m_isSynchronized = isSync;
333 void GenericProposalWidget::showProposal(const QString &prefix)
336 if (!prefix.isEmpty())
337 m_d->m_gotContent = true;
338 m_d->m_model->removeDuplicates();
339 if (!updateAndCheck(prefix))
342 m_d->m_completionListView->setFocus();
345 void GenericProposalWidget::updateProposal(const QString &prefix)
349 updateAndCheck(prefix);
352 void GenericProposalWidget::closeProposal()
357 void GenericProposalWidget::notifyActivation(int index)
360 emit proposalItemActivated(m_d->m_model->proposalItem(index));
363 void GenericProposalWidget::abort()
370 bool GenericProposalWidget::updateAndCheck(const QString &prefix)
372 // Keep track in the case there has been an explicit selection.
373 int preferredItemId = -1;
374 if (m_d->m_explicitlySelected)
376 m_d->m_model->persistentId(m_d->m_completionListView->currentIndex().row());
378 // Filter, sort, etc.
379 m_d->m_model->reset();
380 if (!prefix.isEmpty())
381 m_d->m_model->filter(prefix);
382 if (m_d->m_model->size() == 0
383 || (m_d->m_model->size() == 1 && prefix == m_d->m_model->proposalPrefix())) {
387 if (m_d->m_model->isSortable())
388 m_d->m_model->sort();
389 m_d->m_completionListView->reset();
391 // Try to find the previosly explicit selection (if any). If we can find the item set it
392 // as the current. Otherwise (it might have been filtered out) select the first row.
393 if (m_d->m_explicitlySelected) {
394 Q_ASSERT(preferredItemId != -1);
395 for (int i = 0; i < m_d->m_model->size(); ++i) {
396 if (m_d->m_model->persistentId(i) == preferredItemId) {
397 m_d->m_completionListView->selectRow(i);
402 if (!m_d->m_completionListView->currentIndex().isValid()) {
403 m_d->m_completionListView->selectFirstRow();
404 if (m_d->m_explicitlySelected)
405 m_d->m_explicitlySelected = false;
408 if (TextEditorSettings::instance()->completionSettings().m_partiallyComplete
409 && m_d->m_reason == ExplicitlyInvoked
411 && m_d->m_isSynchronized) {
412 if (m_d->m_model->size() == 1) {
413 IAssistProposalItem *item = m_d->m_model->proposalItem(0);
414 if (item->implicitlyApplies()) {
416 emit proposalItemActivated(item);
420 if (m_d->m_model->supportsPrefixExpansion()) {
421 const QString &proposalPrefix = m_d->m_model->proposalPrefix();
422 if (proposalPrefix.length() > prefix.length())
423 emit prefixExpanded(proposalPrefix);
427 updatePositionAndSize();
431 void GenericProposalWidget::updatePositionAndSize()
433 const QSize &shint = m_d->m_completionListView->calculateSize();
434 const int fw = frameWidth();
435 const int width = shint.width() + fw * 2 + 30;
436 const int height = shint.height() + fw * 2;
438 // Determine the position, keeping the popup on the screen
439 const QDesktopWidget *desktop = QApplication::desktop();
441 const QRect screen = desktop->availableGeometry(desktop->screenNumber(m_d->m_underlyingWidget));
443 const QRect screen = desktop->screenGeometry(desktop->screenNumber(m_d->m_underlyingWidget));
446 QPoint pos = m_d->m_displayRect.bottomLeft();
447 pos.rx() -= 16 + fw; // Space for the icons
448 if (pos.y() + height > screen.bottom())
449 pos.setY(m_d->m_displayRect.top() - height);
450 if (pos.x() + width > screen.right())
451 pos.setX(screen.right() - width);
452 setGeometry(pos.x(), pos.y(), width, height);
455 bool GenericProposalWidget::eventFilter(QObject *o, QEvent *e)
457 if (e->type() == QEvent::FocusOut) {
459 #if defined(Q_OS_DARWIN) && ! defined(QT_MAC_USE_COCOA)
460 QFocusEvent *fe = static_cast<QFocusEvent *>(e);
461 if (fe->reason() == Qt::OtherFocusReason) {
462 // Qt/carbon workaround
463 // focus out is received before the key press event.
464 if (m_d->m_completionListView->currentIndex().isValid())
465 emit proposalItemActivated(m_d->m_model->proposalItem(
466 m_d->m_completionListView->currentIndex().row()));
469 if (m_d->m_infoFrame)
470 m_d->m_infoFrame->close();
472 } else if (e->type() == QEvent::ShortcutOverride) {
473 QKeyEvent *ke = static_cast<QKeyEvent *>(e);
477 if (ke->modifiers() == Qt::ControlModifier) {
482 } else if (e->type() == QEvent::KeyPress) {
483 m_d->m_gotContent = false;
484 QKeyEvent *ke = static_cast<QKeyEvent *>(e);
492 // select next/previous completion
493 m_d->m_explicitlySelected = true;
494 if (ke->modifiers() == Qt::ControlModifier) {
495 int change = (ke->key() == Qt::Key_N) ? 1 : -1;
496 int nrows = m_d->m_model->size();
497 int row = m_d->m_completionListView->currentIndex().row();
498 int newRow = (row + change + nrows) % nrows;
499 if (newRow == row + change || !ke->isAutoRepeat())
500 m_d->m_completionListView->selectRow(newRow);
503 m_d->m_gotContent = true;
508 #if defined(QT_MAC_USE_COCOA) || !defined(Q_OS_DARWIN)
510 if (m_d->m_completionListView->currentIndex().isValid())
511 emit proposalItemActivated(m_d->m_model->proposalItem(
512 m_d->m_completionListView->currentIndex().row()));
517 m_d->m_explicitlySelected = true;
518 if (!ke->isAutoRepeat() && m_d->m_completionListView->isFirstRowSelected()) {
519 m_d->m_completionListView->selectLastRow();
525 m_d->m_explicitlySelected = true;
526 if (!ke->isAutoRepeat() && m_d->m_completionListView->isLastRowSelected()) {
527 m_d->m_completionListView->selectFirstRow();
533 case Qt::Key_PageDown:
541 case Qt::Key_Backspace:
542 // We want these navigation keys to work in the editor.
546 // Only forward keys that insert text and refine the completion.
547 if (ke->text().isEmpty())
549 m_d->m_gotContent = true;
553 if (ke->text().length() == 1
554 && m_d->m_completionListView->currentIndex().isValid()
555 && m_d->m_reason == ExplicitlyInvoked
556 && qApp->focusWidget() == o) {
557 const QChar &typedChar = ke->text().at(0);
558 IAssistProposalItem *item =
559 m_d->m_model->proposalItem(m_d->m_completionListView->currentIndex().row());
560 if (item->prematurelyApplies(typedChar)) {
562 emit proposalItemActivated(item);
567 QApplication::sendEvent(const_cast<QWidget *>(m_d->m_underlyingWidget), e);
569 m_d->m_assistant->notifyChange();
576 #include "genericproposalwidget.moc"