Files
qt-creator/src/plugins/texteditor/codeassist/genericproposalwidget.cpp
Leandro Melo 9e6f387bce Completion: Complete automatically only when in sync
Whith the out-of-GUI completion one can have undesired
effects when invoking the completion, continue typing,
getting automatic content added to the editor
asynchronously, and then noticing that the lastly
input characters were "shifted" to a different
position.

Now we only do partial completions or implicit completions
(when there's a single match) if the proposal widget is
still in synch with the editor. In other words, if the
editor has not received input from the moment the completion
was triggered until it was actually displayed.

Change-Id: I31683bd596c9f75b9a48bb79dafb48eec8f12a3e
Reviewed-on: http://codereview.qt.nokia.com/44
Reviewed-by: Roberto Raggi <roberto.raggi@nokia.com>
2011-05-20 17:11:05 +02:00

578 lines
18 KiB
C++

/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: Nokia Corporation (info@qt.nokia.com)
**
**
** 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.
**
** If you have questions regarding the use of this file, please contact
** Nokia at info@qt.nokia.com.
**
**************************************************************************/
#include "genericproposalwidget.h"
#include "iassistprovider.h"
#include "igenericproposalmodel.h"
#include "iassistproposalitem.h"
#include "genericproposal.h"
#include "codeassistant.h"
#include <texteditor/texteditorsettings.h>
#include <texteditor/completionsettings.h>
#include <utils/faketooltip.h>
#include <QtCore/QRect>
#include <QtCore/QLatin1String>
#include <QtCore/QAbstractListModel>
#include <QtCore/QPointer>
#include <QtCore/QDebug>
#include <QtCore/QTimer>
#include <QtGui/QApplication>
#include <QtGui/QVBoxLayout>
#include <QtGui/QListView>
#include <QtGui/QAbstractItemView>
#include <QtGui/QScrollBar>
#include <QtGui/QKeyEvent>
#include <QtGui/QDesktopWidget>
#include <QtGui/QLabel>
namespace TextEditor {
// ------------
// ModelAdapter
// ------------
class ModelAdapter : public QAbstractListModel
{
Q_OBJECT
public:
ModelAdapter(IGenericProposalModel *completionModel, QWidget *parent);
virtual int rowCount(const QModelIndex &) const;
virtual QVariant data(const QModelIndex &index, int role) const;
private:
IGenericProposalModel *m_completionModel;
};
ModelAdapter::ModelAdapter(IGenericProposalModel *completionModel, QWidget *parent)
: QAbstractListModel(parent)
, m_completionModel(completionModel)
{}
int ModelAdapter::rowCount(const QModelIndex &) const
{
return m_completionModel->size();
}
QVariant ModelAdapter::data(const QModelIndex &index, int role) const
{
if (index.row() >= m_completionModel->size())
return QVariant();
if (role == Qt::DisplayRole) {
return m_completionModel->text(index.row());
} else if (role == Qt::DecorationRole) {
return m_completionModel->icon(index.row());
} else if (role == Qt::WhatsThisRole) {
return m_completionModel->detail(index.row());
}
return QVariant();
}
// ------------------------
// GenericProposalInfoFrame
// ------------------------
class GenericProposalInfoFrame : public Utils::FakeToolTip
{
public:
GenericProposalInfoFrame(QWidget *parent = 0)
: Utils::FakeToolTip(parent), m_label(new QLabel(this))
{
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setMargin(0);
layout->setSpacing(0);
layout->addWidget(m_label);
m_label->setForegroundRole(QPalette::ToolTipText);
m_label->setBackgroundRole(QPalette::ToolTipBase);
}
void setText(const QString &text)
{
m_label->setText(text);
}
private:
QLabel *m_label;
};
// -----------------------
// GenericProposalListView
// -----------------------
class GenericProposalListView : public QListView
{
public:
GenericProposalListView(QWidget *parent) : QListView(parent) {}
QSize calculateSize() const;
QPoint infoFramePos() const;
int rowSelected() const { return currentIndex().row(); }
bool isFirstRowSelected() const { return rowSelected() == 0; }
bool isLastRowSelected() const { return rowSelected() == model()->rowCount() - 1; }
void selectRow(int row) { setCurrentIndex(model()->index(row, 0)); }
void selectFirstRow() { selectRow(0); }
void selectLastRow() { selectRow(model()->rowCount() - 1); }
};
QSize GenericProposalListView::calculateSize() const
{
static const int maxVisibleItems = 10;
// Determine size by calculating the space of the visible items
int visibleItems = model()->rowCount();
if (visibleItems > maxVisibleItems)
visibleItems = maxVisibleItems;
const QStyleOptionViewItem &option = viewOptions();
QSize shint;
for (int i = 0; i < visibleItems; ++i) {
QSize tmp = itemDelegate()->sizeHint(option, model()->index(i, 0));
if (shint.width() < tmp.width())
shint = tmp;
}
shint.rheight() *= visibleItems;
return shint;
}
QPoint GenericProposalListView::infoFramePos() const
{
const QRect &r = rectForIndex(currentIndex());
QPoint p((parentWidget()->mapToGlobal(
parentWidget()->rect().topRight())).x() + 3,
mapToGlobal(r.topRight()).y() - verticalOffset()
);
return p;
}
// ----------------------------
// GenericProposalWidgetPrivate
// ----------------------------
class GenericProposalWidgetPrivate : public QObject
{
Q_OBJECT
public:
GenericProposalWidgetPrivate(QWidget *completionWidget);
const QWidget *m_underlyingWidget;
GenericProposalListView *m_completionListView;
IGenericProposalModel *m_model;
QRect m_displayRect;
bool m_isSynchronized;
bool m_explicitlySelected;
AssistReason m_reason;
bool m_gotContent;
QPointer<GenericProposalInfoFrame> m_infoFrame;
QTimer m_infoTimer;
CodeAssistant *m_assistant;
public slots:
void handleActivation(const QModelIndex &modelIndex);
void maybeShowInfoTip();
};
GenericProposalWidgetPrivate::GenericProposalWidgetPrivate(QWidget *completionWidget)
: m_underlyingWidget(0)
, m_completionListView(new GenericProposalListView(completionWidget))
, m_model(0)
, m_isSynchronized(true)
, m_explicitlySelected(false)
, m_gotContent(false)
, m_assistant(0)
{
connect(m_completionListView, SIGNAL(activated(QModelIndex)),
this, SLOT(handleActivation(QModelIndex)));
m_infoTimer.setInterval(1000);
m_infoTimer.setSingleShot(true);
connect(&m_infoTimer, SIGNAL(timeout()), SLOT(maybeShowInfoTip()));
}
void GenericProposalWidgetPrivate::handleActivation(const QModelIndex &modelIndex)
{
static_cast<GenericProposalWidget *>
(m_completionListView->parent())->notifyActivation(modelIndex.row());
}
void GenericProposalWidgetPrivate::maybeShowInfoTip()
{
const QModelIndex &current = m_completionListView->currentIndex();
if (!current.isValid())
return;
const QString &infoTip = current.data(Qt::WhatsThisRole).toString();
if (infoTip.isEmpty()) {
delete m_infoFrame.data();
m_infoTimer.setInterval(200);
return;
}
if (m_infoFrame.isNull())
m_infoFrame = new GenericProposalInfoFrame(m_completionListView);
m_infoFrame->move(m_completionListView->infoFramePos());
m_infoFrame->setText(infoTip);
m_infoFrame->adjustSize();
m_infoFrame->show();
m_infoFrame->raise();
m_infoTimer.setInterval(0);
}
// ------------------------
// GenericProposalWidget
// ------------------------
GenericProposalWidget::GenericProposalWidget()
: m_d(new GenericProposalWidgetPrivate(this))
{
#ifdef Q_WS_MAC
if (m_d->m_completionListView->horizontalScrollBar())
m_d->m_completionListView->horizontalScrollBar()->setAttribute(Qt::WA_MacMiniSize);
if (m_d->m_completionListView->verticalScrollBar())
m_d->m_completionListView->verticalScrollBar()->setAttribute(Qt::WA_MacMiniSize);
#else
// This improves the look with QGTKStyle.
setFrameStyle(m_d->m_completionListView->frameStyle());
#endif
m_d->m_completionListView->setFrameStyle(QFrame::NoFrame);
m_d->m_completionListView->setAttribute(Qt::WA_MacShowFocusRect, false);
m_d->m_completionListView->setUniformItemSizes(true);
m_d->m_completionListView->setSelectionBehavior(QAbstractItemView::SelectItems);
m_d->m_completionListView->setSelectionMode(QAbstractItemView::SingleSelection);
m_d->m_completionListView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_d->m_completionListView->setMinimumSize(1, 1);
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setMargin(0);
layout->addWidget(m_d->m_completionListView);
m_d->m_completionListView->installEventFilter(this);
setObjectName(QLatin1String("m_popupFrame"));
setMinimumSize(1, 1);
}
GenericProposalWidget::~GenericProposalWidget()
{
delete m_d->m_model;
}
void GenericProposalWidget::setAssistant(CodeAssistant *assistant)
{
m_d->m_assistant = assistant;
}
void GenericProposalWidget::setReason(AssistReason reason)
{
m_d->m_reason = reason;
}
void GenericProposalWidget::setUnderlyingWidget(const QWidget *underlyingWidget)
{
setFont(underlyingWidget->font());
m_d->m_underlyingWidget = underlyingWidget;
}
void GenericProposalWidget::setModel(IAssistProposalModel *model)
{
delete m_d->m_model;
m_d->m_model = static_cast<IGenericProposalModel *>(model);
m_d->m_completionListView->setModel(new ModelAdapter(m_d->m_model, m_d->m_completionListView));
connect(m_d->m_completionListView->selectionModel(),
SIGNAL(currentChanged(QModelIndex,QModelIndex)),
&m_d->m_infoTimer,
SLOT(start()));
}
void GenericProposalWidget::setDisplayRect(const QRect &rect)
{
m_d->m_displayRect = rect;
}
void GenericProposalWidget::setIsSynchronized(bool isSync)
{
m_d->m_isSynchronized = isSync;
}
void GenericProposalWidget::showProposal(const QString &prefix)
{
ensurePolished();
if (!prefix.isEmpty())
m_d->m_gotContent = true;
m_d->m_model->removeDuplicates();
if (!updateAndCheck(prefix))
return;
show();
m_d->m_completionListView->setFocus();
}
void GenericProposalWidget::updateProposal(const QString &prefix)
{
if (!isVisible())
return;
updateAndCheck(prefix);
}
void GenericProposalWidget::closeProposal()
{
abort();
}
void GenericProposalWidget::notifyActivation(int index)
{
abort();
emit proposalItemActivated(m_d->m_model->proposalItem(index));
}
void GenericProposalWidget::abort()
{
if (isVisible())
close();
deleteLater();
}
bool GenericProposalWidget::updateAndCheck(const QString &prefix)
{
// Keep track in the case there has been an explicit selection.
int preferredItemId = -1;
if (m_d->m_explicitlySelected)
preferredItemId =
m_d->m_model->persistentId(m_d->m_completionListView->currentIndex().row());
// Filter, sort, etc.
m_d->m_model->reset();
if (!prefix.isEmpty())
m_d->m_model->filter(prefix);
if (m_d->m_model->size() == 0
|| (m_d->m_model->size() == 1 && prefix == m_d->m_model->proposalPrefix())) {
abort();
return false;
}
if (m_d->m_model->isSortable())
m_d->m_model->sort();
m_d->m_completionListView->reset();
// Try to find the previosly explicit selection (if any). If we can find the item set it
// as the current. Otherwise (it might have been filtered out) select the first row.
if (m_d->m_explicitlySelected) {
Q_ASSERT(preferredItemId != -1);
for (int i = 0; i < m_d->m_model->size(); ++i) {
if (m_d->m_model->persistentId(i) == preferredItemId) {
m_d->m_completionListView->selectRow(i);
break;
}
}
}
if (!m_d->m_completionListView->currentIndex().isValid()) {
m_d->m_completionListView->selectFirstRow();
if (m_d->m_explicitlySelected)
m_d->m_explicitlySelected = false;
}
if (TextEditorSettings::instance()->completionSettings().m_partiallyComplete
&& m_d->m_reason == ExplicitlyInvoked
&& m_d->m_gotContent
&& m_d->m_isSynchronized) {
if (m_d->m_model->size() == 1) {
IAssistProposalItem *item = m_d->m_model->proposalItem(0);
if (item->implicitlyApplies()) {
abort();
emit proposalItemActivated(item);
return false;
}
}
if (m_d->m_model->supportsPrefixExpansion()) {
const QString &proposalPrefix = m_d->m_model->proposalPrefix();
if (proposalPrefix.length() > prefix.length())
emit prefixExpanded(proposalPrefix);
}
}
updatePositionAndSize();
return true;
}
void GenericProposalWidget::updatePositionAndSize()
{
const QSize &shint = m_d->m_completionListView->calculateSize();
const int fw = frameWidth();
const int width = shint.width() + fw * 2 + 30;
const int height = shint.height() + fw * 2;
// Determine the position, keeping the popup on the screen
const QDesktopWidget *desktop = QApplication::desktop();
#ifdef Q_WS_MAC
const QRect screen = desktop->availableGeometry(desktop->screenNumber(m_d->m_underlyingWidget));
#else
const QRect screen = desktop->screenGeometry(desktop->screenNumber(m_d->m_underlyingWidget));
#endif
QPoint pos = m_d->m_displayRect.bottomLeft();
pos.rx() -= 16 + fw; // Space for the icons
if (pos.y() + height > screen.bottom())
pos.setY(m_d->m_displayRect.top() - height);
if (pos.x() + width > screen.right())
pos.setX(screen.right() - width);
setGeometry(pos.x(), pos.y(), width, height);
}
bool GenericProposalWidget::eventFilter(QObject *o, QEvent *e)
{
if (e->type() == QEvent::FocusOut) {
abort();
#if defined(Q_OS_DARWIN) && ! defined(QT_MAC_USE_COCOA)
QFocusEvent *fe = static_cast<QFocusEvent *>(e);
if (fe->reason() == Qt::OtherFocusReason) {
// Qt/carbon workaround
// focus out is received before the key press event.
if (m_d->m_completionListView->currentIndex().isValid())
emit proposalItemActivated(m_d->m_model->proposalItem(
m_d->m_completionListView->currentIndex().row()));
}
#endif
if (m_d->m_infoFrame)
m_d->m_infoFrame->close();
return true;
} else if (e->type() == QEvent::ShortcutOverride) {
QKeyEvent *ke = static_cast<QKeyEvent *>(e);
switch (ke->key()) {
case Qt::Key_N:
case Qt::Key_P:
if (ke->modifiers() == Qt::ControlModifier) {
e->accept();
return true;
}
}
} else if (e->type() == QEvent::KeyPress) {
m_d->m_gotContent = false;
QKeyEvent *ke = static_cast<QKeyEvent *>(e);
switch (ke->key()) {
case Qt::Key_Escape:
abort();
return true;
case Qt::Key_N:
case Qt::Key_P:
// select next/previous completion
m_d->m_explicitlySelected = true;
if (ke->modifiers() == Qt::ControlModifier) {
int change = (ke->key() == Qt::Key_N) ? 1 : -1;
int nrows = m_d->m_model->size();
int row = m_d->m_completionListView->currentIndex().row();
int newRow = (row + change + nrows) % nrows;
if (newRow == row + change || !ke->isAutoRepeat())
m_d->m_completionListView->selectRow(newRow);
return true;
}
m_d->m_gotContent = true;
break;
case Qt::Key_Tab:
case Qt::Key_Return:
#if defined(QT_MAC_USE_COCOA) || !defined(Q_OS_DARWIN)
abort();
if (m_d->m_completionListView->currentIndex().isValid())
emit proposalItemActivated(m_d->m_model->proposalItem(
m_d->m_completionListView->currentIndex().row()));
#endif
return true;
case Qt::Key_Up:
m_d->m_explicitlySelected = true;
if (!ke->isAutoRepeat() && m_d->m_completionListView->isFirstRowSelected()) {
m_d->m_completionListView->selectLastRow();
return true;
}
return false;
case Qt::Key_Down:
m_d->m_explicitlySelected = true;
if (!ke->isAutoRepeat() && m_d->m_completionListView->isLastRowSelected()) {
m_d->m_completionListView->selectFirstRow();
return true;
}
return false;
case Qt::Key_Enter:
case Qt::Key_PageDown:
case Qt::Key_PageUp:
return false;
case Qt::Key_Right:
case Qt::Key_Left:
case Qt::Key_Home:
case Qt::Key_End:
case Qt::Key_Backspace:
// We want these navigation keys to work in the editor.
break;
default:
// Only forward keys that insert text and refine the completion.
if (ke->text().isEmpty())
return true;
m_d->m_gotContent = true;
break;
}
if (ke->text().length() == 1
&& m_d->m_completionListView->currentIndex().isValid()
&& qApp->focusWidget() == o) {
const QChar &typedChar = ke->text().at(0);
IAssistProposalItem *item =
m_d->m_model->proposalItem(m_d->m_completionListView->currentIndex().row());
if (item->prematurelyApplies(typedChar)) {
abort();
emit proposalItemActivated(item);
return true;
}
}
QApplication::sendEvent(const_cast<QWidget *>(m_d->m_underlyingWidget), e);
if (isVisible())
m_d->m_assistant->notifyChange();
return true;
}
return false;
}
#include "genericproposalwidget.moc"
} // TextEditor