OSDN Git Service

8f9127f0c38afd61f1cd5c6fa3acdb086da3348e
[qt-creator-jp/qt-creator-jp.git] / src / plugins / texteditor / codeassist / genericproposalwidget.cpp
1 /**************************************************************************
2 **
3 ** This file is part of Qt Creator
4 **
5 ** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies).
6 **
7 ** Contact: Nokia Corporation (info@qt.nokia.com)
8 **
9 **
10 ** GNU Lesser General Public License Usage
11 **
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.
18 **
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.
22 **
23 ** Other Usage
24 **
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.
27 **
28 ** If you have questions regarding the use of this file, please contact
29 ** Nokia at info@qt.nokia.com.
30 **
31 **************************************************************************/
32
33 #include "genericproposalwidget.h"
34 #include "iassistprovider.h"
35 #include "igenericproposalmodel.h"
36 #include "iassistproposalitem.h"
37 #include "genericproposal.h"
38 #include "codeassistant.h"
39
40 #include <texteditor/texteditorsettings.h>
41 #include <texteditor/completionsettings.h>
42
43 #include <utils/faketooltip.h>
44
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>
59
60 namespace TextEditor {
61
62 // ------------
63 // ModelAdapter
64 // ------------
65 class ModelAdapter : public QAbstractListModel
66 {
67     Q_OBJECT
68
69 public:
70     ModelAdapter(IGenericProposalModel *completionModel, QWidget *parent);
71
72     virtual int rowCount(const QModelIndex &) const;
73     virtual QVariant data(const QModelIndex &index, int role) const;
74
75 private:
76     IGenericProposalModel *m_completionModel;
77 };
78
79 ModelAdapter::ModelAdapter(IGenericProposalModel *completionModel, QWidget *parent)
80     : QAbstractListModel(parent)
81     , m_completionModel(completionModel)
82 {}
83
84 int ModelAdapter::rowCount(const QModelIndex &) const
85 {
86     return m_completionModel->size();
87 }
88
89 QVariant ModelAdapter::data(const QModelIndex &index, int role) const
90 {
91     if (index.row() >= m_completionModel->size())
92         return QVariant();
93
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());
100     }
101
102     return QVariant();
103 }
104
105 // ------------------------
106 // GenericProposalInfoFrame
107 // ------------------------
108 class GenericProposalInfoFrame : public Utils::FakeToolTip
109 {
110 public:
111     GenericProposalInfoFrame(QWidget *parent = 0)
112         : Utils::FakeToolTip(parent), m_label(new QLabel(this))
113     {
114         QVBoxLayout *layout = new QVBoxLayout(this);
115         layout->setMargin(0);
116         layout->setSpacing(0);
117         layout->addWidget(m_label);
118
119         m_label->setForegroundRole(QPalette::ToolTipText);
120         m_label->setBackgroundRole(QPalette::ToolTipBase);
121     }
122
123     void setText(const QString &text)
124     {
125         m_label->setText(text);
126     }
127
128 private:
129     QLabel *m_label;
130 };
131
132 // -----------------------
133 // GenericProposalListView
134 // -----------------------
135 class GenericProposalListView : public QListView
136 {
137 public:
138     GenericProposalListView(QWidget *parent) : QListView(parent) {}
139
140     QSize calculateSize() const;
141     QPoint infoFramePos() const;
142
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); }
149 };
150
151 QSize GenericProposalListView::calculateSize() const
152 {
153     static const int maxVisibleItems = 10;
154
155     // Determine size by calculating the space of the visible items
156     int visibleItems = model()->rowCount();
157     if (visibleItems > maxVisibleItems)
158         visibleItems = maxVisibleItems;
159
160     const QStyleOptionViewItem &option = viewOptions();
161     QSize shint;
162     for (int i = 0; i < visibleItems; ++i) {
163         QSize tmp = itemDelegate()->sizeHint(option, model()->index(i, 0));
164         if (shint.width() < tmp.width())
165             shint = tmp;
166     }
167     shint.rheight() *= visibleItems;
168
169     return shint;
170 }
171
172 QPoint GenericProposalListView::infoFramePos() const
173 {
174     const QRect &r = rectForIndex(currentIndex());
175     QPoint p((parentWidget()->mapToGlobal(
176                     parentWidget()->rect().topRight())).x() + 3,
177             mapToGlobal(r.topRight()).y() - verticalOffset()
178             );
179     return p;
180 }
181
182 // ----------------------------
183 // GenericProposalWidgetPrivate
184 // ----------------------------
185 class GenericProposalWidgetPrivate : public QObject
186 {
187     Q_OBJECT
188
189 public:
190     GenericProposalWidgetPrivate(QWidget *completionWidget);
191
192     const QWidget *m_underlyingWidget;
193     GenericProposalListView *m_completionListView;
194     IGenericProposalModel *m_model;
195     QRect m_displayRect;
196     bool m_isSynchronized;
197     bool m_explicitlySelected;
198     AssistReason m_reason;
199     bool m_gotContent;
200     QPointer<GenericProposalInfoFrame> m_infoFrame;
201     QTimer m_infoTimer;
202     CodeAssistant *m_assistant;
203
204 public slots:
205     void handleActivation(const QModelIndex &modelIndex);
206     void maybeShowInfoTip();
207 };
208
209 GenericProposalWidgetPrivate::GenericProposalWidgetPrivate(QWidget *completionWidget)
210     : m_underlyingWidget(0)
211     , m_completionListView(new GenericProposalListView(completionWidget))
212     , m_model(0)
213     , m_isSynchronized(true)
214     , m_explicitlySelected(false)
215     , m_gotContent(false)
216     , m_assistant(0)
217 {
218     connect(m_completionListView, SIGNAL(activated(QModelIndex)),
219             this, SLOT(handleActivation(QModelIndex)));
220
221     m_infoTimer.setInterval(1000);
222     m_infoTimer.setSingleShot(true);
223     connect(&m_infoTimer, SIGNAL(timeout()), SLOT(maybeShowInfoTip()));
224 }
225
226 void GenericProposalWidgetPrivate::handleActivation(const QModelIndex &modelIndex)
227 {
228     static_cast<GenericProposalWidget *>
229             (m_completionListView->parent())->notifyActivation(modelIndex.row());
230 }
231
232 void GenericProposalWidgetPrivate::maybeShowInfoTip()
233 {
234     const QModelIndex &current = m_completionListView->currentIndex();
235     if (!current.isValid())
236         return;
237
238     const QString &infoTip = current.data(Qt::WhatsThisRole).toString();
239     if (infoTip.isEmpty()) {
240         delete m_infoFrame.data();
241         m_infoTimer.setInterval(200);
242         return;
243     }
244
245     if (m_infoFrame.isNull())
246         m_infoFrame = new GenericProposalInfoFrame(m_completionListView);
247
248     m_infoFrame->move(m_completionListView->infoFramePos());
249     m_infoFrame->setText(infoTip);
250     m_infoFrame->adjustSize();
251     m_infoFrame->show();
252     m_infoFrame->raise();
253
254     m_infoTimer.setInterval(0);
255 }
256
257 // ------------------------
258 // GenericProposalWidget
259 // ------------------------
260 GenericProposalWidget::GenericProposalWidget()
261     : m_d(new GenericProposalWidgetPrivate(this))
262 {
263 #ifdef Q_WS_MAC
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);
268 #else
269     // This improves the look with QGTKStyle.
270     setFrameStyle(m_d->m_completionListView->frameStyle());
271 #endif
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);
279
280     QVBoxLayout *layout = new QVBoxLayout(this);
281     layout->setMargin(0);
282     layout->addWidget(m_d->m_completionListView);
283
284     m_d->m_completionListView->installEventFilter(this);
285
286     setObjectName(QLatin1String("m_popupFrame"));
287     setMinimumSize(1, 1);
288 }
289
290 GenericProposalWidget::~GenericProposalWidget()
291 {
292     delete m_d->m_model;
293 }
294
295 void GenericProposalWidget::setAssistant(CodeAssistant *assistant)
296 {
297     m_d->m_assistant = assistant;
298 }
299
300 void GenericProposalWidget::setReason(AssistReason reason)
301 {
302     m_d->m_reason = reason;
303 }
304
305 void GenericProposalWidget::setUnderlyingWidget(const QWidget *underlyingWidget)
306 {
307     setFont(underlyingWidget->font());
308     m_d->m_underlyingWidget = underlyingWidget;
309 }
310
311 void GenericProposalWidget::setModel(IAssistProposalModel *model)
312 {
313     delete m_d->m_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));
316
317     connect(m_d->m_completionListView->selectionModel(),
318             SIGNAL(currentChanged(QModelIndex,QModelIndex)),
319             &m_d->m_infoTimer,
320             SLOT(start()));
321 }
322
323 void GenericProposalWidget::setDisplayRect(const QRect &rect)
324 {
325     m_d->m_displayRect = rect;
326 }
327
328 void GenericProposalWidget::setIsSynchronized(bool isSync)
329 {
330     m_d->m_isSynchronized = isSync;
331 }
332
333 void GenericProposalWidget::showProposal(const QString &prefix)
334 {
335     ensurePolished();
336     if (!prefix.isEmpty())
337         m_d->m_gotContent = true;
338     m_d->m_model->removeDuplicates();
339     if (!updateAndCheck(prefix))
340         return;
341     show();
342     m_d->m_completionListView->setFocus();
343 }
344
345 void GenericProposalWidget::updateProposal(const QString &prefix)
346 {
347     if (!isVisible())
348         return;
349     updateAndCheck(prefix);
350 }
351
352 void GenericProposalWidget::closeProposal()
353 {
354     abort();
355 }
356
357 void GenericProposalWidget::notifyActivation(int index)
358 {
359     abort();
360     emit proposalItemActivated(m_d->m_model->proposalItem(index));
361 }
362
363 void GenericProposalWidget::abort()
364 {
365     if (isVisible())
366         close();
367     deleteLater();
368 }
369
370 bool GenericProposalWidget::updateAndCheck(const QString &prefix)
371 {
372     // Keep track in the case there has been an explicit selection.
373     int preferredItemId = -1;
374     if (m_d->m_explicitlySelected)
375         preferredItemId =
376                 m_d->m_model->persistentId(m_d->m_completionListView->currentIndex().row());
377
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())) {
384         abort();
385         return false;
386     }
387     if (m_d->m_model->isSortable())
388         m_d->m_model->sort();
389     m_d->m_completionListView->reset();
390
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);
398                 break;
399             }
400         }
401     }
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;
406     }
407
408     if (TextEditorSettings::instance()->completionSettings().m_partiallyComplete
409             && m_d->m_reason == ExplicitlyInvoked
410             && m_d->m_gotContent
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()) {
415                 abort();
416                 emit proposalItemActivated(item);
417                 return false;
418             }
419         }
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);
424         }
425     }
426
427     updatePositionAndSize();
428     return true;
429 }
430
431 void GenericProposalWidget::updatePositionAndSize()
432 {
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;
437
438     // Determine the position, keeping the popup on the screen
439     const QDesktopWidget *desktop = QApplication::desktop();
440 #ifdef Q_WS_MAC
441     const QRect screen = desktop->availableGeometry(desktop->screenNumber(m_d->m_underlyingWidget));
442 #else
443     const QRect screen = desktop->screenGeometry(desktop->screenNumber(m_d->m_underlyingWidget));
444 #endif
445
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);
453 }
454
455 bool GenericProposalWidget::eventFilter(QObject *o, QEvent *e)
456 {
457     if (e->type() == QEvent::FocusOut) {
458         abort();
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()));
467         }
468 #endif
469         if (m_d->m_infoFrame)
470             m_d->m_infoFrame->close();
471         return true;
472     } else if (e->type() == QEvent::ShortcutOverride) {
473         QKeyEvent *ke = static_cast<QKeyEvent *>(e);
474         switch (ke->key()) {
475         case Qt::Key_N:
476         case Qt::Key_P:
477             if (ke->modifiers() == Qt::ControlModifier) {
478                 e->accept();
479                 return true;
480             }
481         }
482     } else if (e->type() == QEvent::KeyPress) {
483         m_d->m_gotContent = false;
484         QKeyEvent *ke = static_cast<QKeyEvent *>(e);
485         switch (ke->key()) {
486         case Qt::Key_Escape:
487             abort();
488             return true;
489
490         case Qt::Key_N:
491         case Qt::Key_P:
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);
501                 return true;
502             }
503             m_d->m_gotContent = true;
504             break;
505
506         case Qt::Key_Tab:
507         case Qt::Key_Return:
508 #if defined(QT_MAC_USE_COCOA) || !defined(Q_OS_DARWIN)
509             abort();
510             if (m_d->m_completionListView->currentIndex().isValid())
511                 emit proposalItemActivated(m_d->m_model->proposalItem(
512                                               m_d->m_completionListView->currentIndex().row()));
513 #endif
514             return true;
515
516         case Qt::Key_Up:
517             m_d->m_explicitlySelected = true;
518             if (!ke->isAutoRepeat() && m_d->m_completionListView->isFirstRowSelected()) {
519                 m_d->m_completionListView->selectLastRow();
520                 return true;
521             }
522             return false;
523
524         case Qt::Key_Down:
525             m_d->m_explicitlySelected = true;
526             if (!ke->isAutoRepeat() && m_d->m_completionListView->isLastRowSelected()) {
527                 m_d->m_completionListView->selectFirstRow();
528                 return true;
529             }
530             return false;
531
532         case Qt::Key_Enter:
533         case Qt::Key_PageDown:
534         case Qt::Key_PageUp:
535             return false;
536
537         case Qt::Key_Right:
538         case Qt::Key_Left:
539         case Qt::Key_Home:
540         case Qt::Key_End:
541         case Qt::Key_Backspace:
542             // We want these navigation keys to work in the editor.
543             break;
544
545         default:
546             // Only forward keys that insert text and refine the completion.
547             if (ke->text().isEmpty())
548                 return true;
549             m_d->m_gotContent = true;
550             break;
551         }
552
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)) {
561                 abort();
562                 emit proposalItemActivated(item);
563                 return true;
564             }
565         }
566
567         QApplication::sendEvent(const_cast<QWidget *>(m_d->m_underlyingWidget), e);
568         if (isVisible())
569             m_d->m_assistant->notifyChange();
570
571         return true;
572     }
573     return false;
574 }
575
576 #include "genericproposalwidget.moc"
577
578 } // TextEditor