completionwidget.cpp 13.6 KB
Newer Older
1
/**************************************************************************
con's avatar
con committed
2
3
4
**
** This file is part of Qt Creator
**
hjk's avatar
hjk committed
5
** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies).
con's avatar
con committed
6
**
7
** Contact: Nokia Corporation (qt-info@nokia.com)
con's avatar
con committed
8
**
9
** Commercial Usage
10
**
11
12
13
14
** Licensees holding valid Qt Commercial licenses may use this file in
** accordance with the Qt Commercial License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Nokia.
15
**
16
** GNU Lesser General Public License Usage
17
**
18
19
20
21
22
23
** Alternatively, 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.
24
**
25
** If you are unsure which license is appropriate for your use, please
hjk's avatar
hjk committed
26
** contact the sales department at http://qt.nokia.com/contact.
con's avatar
con committed
27
**
28
**************************************************************************/
hjk's avatar
hjk committed
29

con's avatar
con committed
30
31
32
33
34
#include "completionwidget.h"
#include "completionsupport.h"
#include "icompletioncollector.h"

#include <texteditor/itexteditable.h>
35
36

#include <utils/faketooltip.h>
hjk's avatar
hjk committed
37
#include <utils/qtcassert.h>
con's avatar
con committed
38
39
40

#include <QtCore/QEvent>
#include <QtGui/QApplication>
41
42
#include <QtGui/QDesktopWidget>
#include <QtGui/QKeyEvent>
con's avatar
con committed
43
#include <QtGui/QVBoxLayout>
con's avatar
con committed
44
#include <QtGui/QScrollBar>
mae's avatar
mae committed
45
46
#include <QtGui/QLabel>
#include <QtGui/QStylePainter>
47
#include <QtGui/QToolTip>
con's avatar
con committed
48
49
50
51
52
53
54
55

#include <limits.h>

using namespace TextEditor;
using namespace TextEditor::Internal;

#define NUMBER_OF_VISIBLE_ITEMS 10

56
57
58
namespace TextEditor {
namespace Internal {

con's avatar
con committed
59
60
61
class AutoCompletionModel : public QAbstractListModel
{
public:
62
    AutoCompletionModel(QObject *parent);
con's avatar
con committed
63
64
65
66
67
68
69
70
71
72
73
74
75

    inline const CompletionItem &itemAt(const QModelIndex &index) const
    { return m_items.at(index.row()); }

    void setItems(const QList<CompletionItem> &items);

    int rowCount(const QModelIndex &parent = QModelIndex()) const;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;

private:
    QList<CompletionItem> m_items;
};

mae's avatar
mae committed
76

77
78
class CompletionInfoFrame : public Utils::FakeToolTip
{
mae's avatar
mae committed
79
80
public:
    CompletionInfoFrame(QWidget *parent = 0) :
81
82
        Utils::FakeToolTip(parent),
        m_label(new QLabel(this))
mae's avatar
mae committed
83
    {
84
85
86
87
        QVBoxLayout *layout = new QVBoxLayout(this);
        layout->setMargin(0);
        layout->setSpacing(0);
        layout->addWidget(m_label);
mae's avatar
mae committed
88

89
90
        m_label->setForegroundRole(QPalette::ToolTipText);
        m_label->setBackgroundRole(QPalette::ToolTipBase);
mae's avatar
mae committed
91
    }
92
93

    void setText(const QString &text)
mae's avatar
mae committed
94
    {
95
        m_label->setText(text);
mae's avatar
mae committed
96
    }
97
98
99

private:
    QLabel *m_label;
mae's avatar
mae committed
100
101
102
};


103
104
105
106
107
} // namespace Internal
} // namespace TextEditor


AutoCompletionModel::AutoCompletionModel(QObject *parent)
con's avatar
con committed
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
    : QAbstractListModel(parent)
{
}

void AutoCompletionModel::setItems(const QList<CompletionItem> &items)
{
    m_items = items;
    reset();
}

int AutoCompletionModel::rowCount(const QModelIndex &) const
{
    return m_items.count();
}

QVariant AutoCompletionModel::data(const QModelIndex &index, int role) const
{
    if (index.row() >= m_items.count())
        return QVariant();

    if (role == Qt::DisplayRole) {
129
        return itemAt(index).text;
con's avatar
con committed
130
    } else if (role == Qt::DecorationRole) {
131
        return itemAt(index).icon;
mae's avatar
mae committed
132
    } else if (role == Qt::WhatsThisRole) {
133
        return itemAt(index).details;
con's avatar
con committed
134
135
136
137
138
    }

    return QVariant();
}

139

con's avatar
con committed
140
CompletionWidget::CompletionWidget(CompletionSupport *support, ITextEditable *editor)
141
142
    : QFrame(0, Qt::Popup),
      m_support(support),
143
144
      m_editor(editor),
      m_completionListView(new CompletionListView(support, editor, this))
145
146
147
148
{
    // We disable the frame on this list view and use a QFrame around it instead.
    // This improves the look with QGTKStyle.
#ifndef Q_WS_MAC
149
    setFrameStyle(m_completionListView->frameStyle());
150
#endif
151
    m_completionListView->setFrameStyle(QFrame::NoFrame);
152
153
154
155

    setObjectName(QLatin1String("m_popupFrame"));
    setAttribute(Qt::WA_DeleteOnClose);
    setMinimumSize(1, 1);
156
    setFont(editor->widget()->font());
157
158
159
160
161
162
163
164
165
166
167
168
169

    QVBoxLayout *layout = new QVBoxLayout(this);
    layout->setMargin(0);

    layout->addWidget(m_completionListView);
    setFocusProxy(m_completionListView);

    connect(m_completionListView, SIGNAL(itemSelected(TextEditor::CompletionItem)),
            this, SIGNAL(itemSelected(TextEditor::CompletionItem)));
    connect(m_completionListView, SIGNAL(completionListClosed()),
            this, SIGNAL(completionListClosed()));
    connect(m_completionListView, SIGNAL(activated(QModelIndex)),
            SLOT(closeList(QModelIndex)));
170
171
    connect(editor, SIGNAL(contentsChangedBecauseOfUndo()),
            this, SLOT(closeList()));
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
}

CompletionWidget::~CompletionWidget()
{
}

void CompletionWidget::setQuickFix(bool quickFix)
{
    m_completionListView->setQuickFix(quickFix);
}

void CompletionWidget::setCompletionItems(const QList<TextEditor::CompletionItem> &completionitems)
{
    m_completionListView->setCompletionItems(completionitems);
}

void CompletionWidget::closeList(const QModelIndex &index)
{
    m_completionListView->closeList(index);
    close();
}

void CompletionWidget::showCompletions(int startPos)
{
196
    ensurePolished();
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
    updatePositionAndSize(startPos);
    show();
    setFocus();
}

void CompletionWidget::updatePositionAndSize(int startPos)
{
    // Determine size by calculating the space of the visible items
    QAbstractItemModel *model = m_completionListView->model();
    int visibleItems = model->rowCount();
    if (visibleItems > NUMBER_OF_VISIBLE_ITEMS)
        visibleItems = NUMBER_OF_VISIBLE_ITEMS;

    const QStyleOptionViewItem &option = m_completionListView->viewOptions();

    QSize shint;
    for (int i = 0; i < visibleItems; ++i) {
        QSize tmp = m_completionListView->itemDelegate()->sizeHint(option, model->index(i, 0));
        if (shint.width() < tmp.width())
            shint = tmp;
    }

    const int fw = frameWidth();
    const int width = shint.width() + fw * 2 + 30;
    const int height = shint.height() * visibleItems + fw * 2;

    // Determine the position, keeping the popup on the screen
    const QRect cursorRect = m_editor->cursorRect(startPos);
    const QDesktopWidget *desktop = QApplication::desktop();

    QWidget *editorWidget = m_editor->widget();

#ifdef Q_WS_MAC
    const QRect screen = desktop->availableGeometry(desktop->screenNumber(editorWidget));
#else
    const QRect screen = desktop->screenGeometry(desktop->screenNumber(editorWidget));
#endif

    QPoint pos = cursorRect.bottomLeft();
    pos.rx() -= 16 + fw;    // Space for the icons

    if (pos.y() + height > screen.bottom())
        pos.setY(cursorRect.top() - height);

    if (pos.x() + width > screen.right())
        pos.setX(screen.right() - width);

    setGeometry(pos.x(), pos.y(), width, height);
}

CompletionListView::CompletionListView(CompletionSupport *support, ITextEditable *editor, CompletionWidget *completionWidget)
    : QListView(completionWidget),
con's avatar
con committed
249
      m_blockFocusOut(false),
Roberto Raggi's avatar
Roberto Raggi committed
250
      m_quickFix(false),
con's avatar
con committed
251
252
      m_editor(editor),
      m_editorWidget(editor->widget()),
253
254
      m_completionWidget(completionWidget),
      m_model(new AutoCompletionModel(this)),
con's avatar
con committed
255
256
      m_support(support)
{
hjk's avatar
hjk committed
257
    QTC_ASSERT(m_editorWidget, return);
con's avatar
con committed
258

259
260
261
262
    m_infoTimer.setInterval(1000);
    m_infoTimer.setSingleShot(true);
    connect(&m_infoTimer, SIGNAL(timeout()), SLOT(maybeShowInfoTip()));

263
    setAttribute(Qt::WA_MacShowFocusRect, false);
con's avatar
con committed
264
265
266
267
    setUniformItemSizes(true);
    setSelectionBehavior(QAbstractItemView::SelectItems);
    setSelectionMode(QAbstractItemView::SingleSelection);
    setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
268
    setMinimumSize(1, 1);
269
    setModel(m_model);
con's avatar
con committed
270
271
272
273
274
275
#ifdef Q_WS_MAC
    if (horizontalScrollBar())
        horizontalScrollBar()->setAttribute(Qt::WA_MacMiniSize);
    if (verticalScrollBar())
        verticalScrollBar()->setAttribute(Qt::WA_MacMiniSize);
#endif
276
277
278
279
}

CompletionListView::~CompletionListView()
{
con's avatar
con committed
280
281
}

mae's avatar
mae committed
282
283
284
285
286
287
288
289
290
void CompletionListView::maybeShowInfoTip()
{
    QModelIndex current = currentIndex();
    if (!current.isValid())
        return;
    QString infoTip = current.data(Qt::WhatsThisRole).toString();

    if (infoTip.isEmpty()) {
        delete m_infoFrame.data();
291
        m_infoTimer.setInterval(200);
mae's avatar
mae committed
292
293
294
295
296
297
298
299
300
301
        return;
    }

    if (m_infoFrame.isNull())
        m_infoFrame = new CompletionInfoFrame(this);


    QRect r = rectForIndex(current);
    m_infoFrame->move(
            (parentWidget()->mapToGlobal(
302
                    parentWidget()->rect().topRight())).x() + 3,
mae's avatar
mae committed
303
304
305
306
307
            mapToGlobal(r.topRight()).y() - verticalOffset()
            );
    m_infoFrame->setText(infoTip);
    m_infoFrame->adjustSize();
    m_infoFrame->show();
mae's avatar
mae committed
308
    m_infoFrame->raise();
309
310

    m_infoTimer.setInterval(0);
mae's avatar
mae committed
311
312
313
314
315
}

void CompletionListView::currentChanged(const QModelIndex &current, const QModelIndex &previous)
{
    QListView::currentChanged(current, previous);
316
    m_infoTimer.start();
mae's avatar
mae committed
317
318
319
}


320
bool CompletionListView::event(QEvent *e)
con's avatar
con committed
321
322
323
324
325
326
{
    if (m_blockFocusOut)
        return QListView::event(e);

    bool forwardKeys = true;
    if (e->type() == QEvent::FocusOut) {
327
328
329
330
331
332
333
334
335
        QModelIndex index;
#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.
            index = currentIndex();
        }
#endif
336
        m_completionWidget->closeList(index);
337
        m_infoFrame->close();
con's avatar
con committed
338
        return true;
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
    } else if (e->type() == QEvent::ShortcutOverride) {
        QKeyEvent *ke = static_cast<QKeyEvent *>(e);
        switch (ke->key()) {
        case Qt::Key_N:
        case Qt::Key_P:
            // select next/previous completion
            if (ke->modifiers() == Qt::ControlModifier)
            {
                e->accept();
                int change = (ke->key() == Qt::Key_N) ? 1 : -1;
                int nrows = model()->rowCount();
                int row = currentIndex().row();
                int newRow = (row + change + nrows) % nrows;
                if (newRow == row + change || !ke->isAutoRepeat())
                    setCurrentIndex(m_model->index(newRow));
                return true;
            }
        }
con's avatar
con committed
357
358
359
    } else if (e->type() == QEvent::KeyPress) {
        QKeyEvent *ke = static_cast<QKeyEvent *>(e);
        switch (ke->key()) {
360
361
362
363
364
365
        case Qt::Key_N:
        case Qt::Key_P:
            // select next/previous completion - so don't pass on to editor
            if (ke->modifiers() == Qt::ControlModifier)
                forwardKeys = false;
            break;
366

con's avatar
con committed
367
        case Qt::Key_Escape:
368
            m_completionWidget->closeList();
con's avatar
con committed
369
            return true;
370

con's avatar
con committed
371
372
        case Qt::Key_Right:
        case Qt::Key_Left:
373
374
375
        case Qt::Key_Home:
        case Qt::Key_End:
            // We want these navigation keys to work in the editor, so forward them
con's avatar
con committed
376
            break;
377

con's avatar
con committed
378
379
380
        case Qt::Key_Tab:
        case Qt::Key_Return:
            //independently from style, accept current entry if return is pressed
381
            if (qApp->focusWidget() == this)
382
                m_completionWidget->closeList(currentIndex());
con's avatar
con committed
383
            return true;
384

con's avatar
con committed
385
        case Qt::Key_Up:
386
387
            if (!ke->isAutoRepeat()
                && currentIndex().row() == 0) {
388
389
390
391
392
                setCurrentIndex(model()->index(model()->rowCount()-1, 0));
                return true;
            }
            forwardKeys = false;
            break;
393

con's avatar
con committed
394
        case Qt::Key_Down:
395
396
            if (!ke->isAutoRepeat()
                && currentIndex().row() == model()->rowCount()-1) {
397
398
399
                setCurrentIndex(model()->index(0, 0));
                return true;
            }
400
401
402
            forwardKeys = false;
            break;

con's avatar
con committed
403
404
405
406
407
        case Qt::Key_Enter:
        case Qt::Key_PageDown:
        case Qt::Key_PageUp:
            forwardKeys = false;
            break;
408

con's avatar
con committed
409
        default:
410
411
412
            // if a key is forwarded, completion widget is re-opened and selected item is reset to first,
            // so only forward keys that insert text and refine the completed item
            forwardKeys = !ke->text().isEmpty();
con's avatar
con committed
413
414
415
            break;
        }

Roberto Raggi's avatar
Roberto Raggi committed
416
        if (forwardKeys && ! m_quickFix) {
con's avatar
con committed
417
418
419
420
421
422
423
424
425
426
427
428
429
            m_blockFocusOut = true;
            QApplication::sendEvent(m_editorWidget, e);
            m_blockFocusOut = false;

            // Have the completion support update the list of items
            m_support->autoComplete(m_editor, false);

            return true;
        }
    }
    return QListView::event(e);
}

430
void CompletionListView::keyboardSearch(const QString &search)
con's avatar
con committed
431
{
432
    Q_UNUSED(search)
con's avatar
con committed
433
434
}

435
void CompletionListView::setQuickFix(bool quickFix)
Roberto Raggi's avatar
Roberto Raggi committed
436
437
438
439
{
    m_quickFix = quickFix;
}

440
void CompletionListView::setCompletionItems(const QList<TextEditor::CompletionItem> &completionItems)
con's avatar
con committed
441
{
442
    m_model->setItems(completionItems);
con's avatar
con committed
443
444
445
446
447
448

    // Select the first of the most relevant completion items
    int relevance = INT_MIN;
    int mostRelevantIndex = 0;
    for (int i = 0; i < completionItems.size(); ++i) {
        const CompletionItem &item = completionItems.at(i);
449
450
        if (item.relevance > relevance) {
            relevance = item.relevance;
con's avatar
con committed
451
452
453
454
455
456
457
            mostRelevantIndex = i;
        }
    }

    setCurrentIndex(m_model->index(mostRelevantIndex));
}

458
void CompletionListView::closeList(const QModelIndex &index)
con's avatar
con committed
459
{
460
    m_blockFocusOut = true;
461

462
463
    if (index.isValid())
        emit itemSelected(m_model->itemAt(index));
con's avatar
con committed
464

465
    emit completionListClosed();
con's avatar
con committed
466

467
    m_blockFocusOut = false;
con's avatar
con committed
468
}