]> git.lyx.org Git - lyx.git/blob - src/frontends/qt4/GuiCompleter.cpp
3e2cdcd2ae6b86f42ba21d95f87908afb22e9214
[lyx.git] / src / frontends / qt4 / GuiCompleter.cpp
1 /**
2  * \file GuiCompleter.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Stefan Schimanski
7  *
8  * Full author contact details are available in file CREDITS.
9  */
10
11 #include <config.h>
12
13 #include "GuiWorkArea.h"
14
15 #include "Buffer.h"
16 #include "BufferView.h"
17 #include "Cursor.h"
18 #include "Dimension.h"
19 #include "FuncRequest.h"
20 #include "GuiView.h"
21 #include "LyXFunc.h"
22 #include "LyXRC.h"
23 #include "version.h"
24
25 #include "support/debug.h"
26
27 #include <QApplication>
28 #include <QAbstractListModel>
29 #include <QHeaderView>
30 #include <QPainter>
31 #include <QPixmapCache>
32 #include <QScrollBar>
33 #include <QItemDelegate>
34 #include <QTreeView>
35 #include <QTimer>
36
37 using namespace std;
38 using namespace lyx::support;
39
40 namespace lyx {
41 namespace frontend {
42
43
44 class PixmapItemDelegate : public QItemDelegate {
45 public:
46         explicit PixmapItemDelegate(QObject *parent = 0)
47         : QItemDelegate(parent) {}
48
49 protected:
50         void paint(QPainter *painter, const QStyleOptionViewItem &option,
51                    const QModelIndex &index) const
52         {
53                 QStyleOptionViewItemV3 opt = setOptions(index, option);
54                 QVariant value = index.data(Qt::DisplayRole);
55                 QPixmap pixmap = qvariant_cast<QPixmap>(value);
56                 const QSize size = pixmap.size();
57
58                 // draw
59                 painter->save();
60                 drawBackground(painter, opt, index);
61                 painter->drawPixmap(option.rect.left() + (16 - size.width()) / 2,
62                         option.rect.top() + (option.rect.height() - size.height()) / 2,
63                         pixmap);
64                 drawFocus(painter, opt, option.rect);
65                 painter->restore();
66         }
67 };
68
69
70 class GuiCompletionModel : public QAbstractListModel {
71 public:
72         ///
73         GuiCompletionModel(QObject * parent, Inset::CompletionListPtr l)
74         : QAbstractListModel(parent), list(l) {}
75         ///
76         int columnCount(const QModelIndex & parent = QModelIndex()) const
77         {
78                 return 2;
79         }
80         ///
81         int rowCount(const QModelIndex & parent = QModelIndex()) const
82         {
83                 if (list.get() == 0)
84                         return 0;
85                 else
86                         return list->size();
87         }
88
89         ///
90         QVariant data(const QModelIndex & index, int role) const
91         {
92                 if (list.get() == 0)
93                         return QVariant();
94
95                 if (index.row() < 0 || index.row() >= rowCount())
96                         return QVariant();
97
98                 if (role != Qt::DisplayRole && role != Qt::EditRole)
99                     return QVariant();
100                     
101                 if (index.column() == 0)
102                         return toqstr(list->data(index.row()));
103                 else if (index.column() == 1) {
104                         // get icon from cache
105                         QPixmap scaled;
106                         QString const name = ":" + toqstr(list->icon(index.row()));
107                         if (!QPixmapCache::find("completion" + name, scaled)) {
108                                 // load icon from disk
109                                 QPixmap p = QPixmap(name);
110
111                                 // scale it to 16x16 or smaller
112                                 scaled = p.scaled(min(16, p.width()), min(16, p.height()), 
113                                         Qt::KeepAspectRatio, Qt::SmoothTransformation);
114                                 QPixmapCache::insert("completion" + name, scaled);
115                         }
116                         return scaled;
117                 }
118                 return QVariant();
119         }
120
121 private:
122         ///
123         Inset::CompletionListPtr list;
124 };
125
126
127 GuiCompleter::GuiCompleter(GuiWorkArea * gui, QObject * parent)
128         : QCompleter(parent), gui_(gui), updateLock_(0)
129 {
130         // Setup the completion popup
131         setModel(new GuiCompletionModel(this, Inset::CompletionListPtr()));
132         setCompletionMode(QCompleter::PopupCompletion);
133         setWidget(gui_);
134         
135         // create the popup
136         QTreeView *listView = new QTreeView;
137         listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
138         listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
139         listView->setSelectionBehavior(QAbstractItemView::SelectRows);
140         listView->setSelectionMode(QAbstractItemView::SingleSelection);
141         listView->header()->hide();
142         listView->setIndentation(0);
143         setPopup(listView);
144         popup()->setItemDelegateForColumn(1, new PixmapItemDelegate(popup()));
145         
146         // create timeout timers
147         popup_timer_.setSingleShot(true);
148         inline_timer_.setSingleShot(true);
149         connect(this, SIGNAL(highlighted(const QString &)),
150                 this, SLOT(popupHighlighted(const QString &)));
151         connect(this, SIGNAL(activated(const QString &)),
152                 this, SLOT(popupActivated(const QString &)));
153         connect(&popup_timer_, SIGNAL(timeout()),
154                 this, SLOT(showPopup()));
155         connect(&inline_timer_, SIGNAL(timeout()),
156                 this, SLOT(showInline()));
157 }
158
159
160 GuiCompleter::~GuiCompleter()
161 {
162         popup()->hide();
163 }
164
165
166 bool GuiCompleter::eventFilter(QObject * watched, QEvent * e)
167 {
168         // hijack back the tab key from the popup
169         // (which stole it from the workspace before)
170         if (e->type() == QEvent::KeyPress && popupVisible()) {
171                 QKeyEvent *ke = static_cast<QKeyEvent *>(e);
172                 switch (ke->key()) {
173                 case Qt::Key_Tab:
174                         tab();
175                         ke->accept();
176                         return true;
177                 default: break;
178                 }
179         }
180         
181         return QCompleter::eventFilter(watched, e);
182 }
183
184
185 bool GuiCompleter::popupPossible(Cursor const & cur) const
186 {
187         return QApplication::activeWindow()
188                 && gui_->hasFocus()
189                 && cur.inset().completionSupported(cur);
190 }
191
192
193 bool GuiCompleter::inlinePossible(Cursor const & cur) const
194 {
195         return cur.inset().inlineCompletionSupported(cur);
196 }
197
198
199 bool GuiCompleter::popupVisible() const
200 {
201         return popup()->isVisible();
202 }
203
204
205 bool GuiCompleter::inlineVisible() const
206 {
207         return !gui_->bufferView().inlineCompletionPos().empty();
208 }
209
210
211 void GuiCompleter::updateVisibility(Cursor & cur, bool start, bool keep, bool cursorInView)
212 {
213         // parameters which affect the completion
214         bool moved = cur != old_cursor_;
215         if (moved)
216                 old_cursor_ = cur;
217
218         bool possiblePopupState = popupPossible(cur) && cursorInView;
219         bool possibleInlineState = inlinePossible(cur) && cursorInView;
220
221         // we moved or popup state is not ok for popup?
222         if ((moved && !keep) || !possiblePopupState) {
223                 // stop an old completion timer
224                 if (popup_timer_.isActive())
225                         popup_timer_.stop();
226
227                 // hide old popup
228                 if (popupVisible())
229                         popup()->hide();
230         }
231
232         // we moved or inline state is not ok for inline completion?
233         if ((moved && !keep) || !possibleInlineState) {
234                 // stop an old completion timer
235                 if (inline_timer_.isActive())
236                         inline_timer_.stop();
237
238                 // hide old inline completion
239                 if (inlineVisible())
240                         gui_->bufferView().setInlineCompletion(cur, DocIterator(), docstring());
241         }
242
243         // we inserted something and are in a possible popup state?
244         if (!popupVisible() && possiblePopupState && start
245                 && cur.inset().automaticPopupCompletion())
246                 popup_timer_.start(lyxrc.completion_popup_delay * 1000.0);
247
248         // we inserted something and are in a possible inline completion state?
249         if (!inlineVisible() && possibleInlineState && start
250                 && cur.inset().automaticInlineCompletion())
251                 inline_timer_.start(lyxrc.completion_inline_delay * 1000.0);
252
253         // update prefix if popup is visible or if it will be visible soon
254         if (popupVisible() || inlineVisible()
255             || popup_timer_.isActive() || inline_timer_.isActive())
256                 updatePrefix(cur);
257 }
258
259
260 void GuiCompleter::updateVisibility(bool start, bool keep)
261 {
262         Cursor cur = gui_->bufferView().cursor();
263         cur.updateFlags(Update::None);
264         
265         updateVisibility(cur, start, keep);
266         
267         if (cur.disp_.update())
268                 gui_->bufferView().processUpdateFlags(cur.disp_.update());
269 }
270
271
272 void GuiCompleter::updatePrefix(Cursor & cur)
273 {
274         // get new prefix. Do nothing if unchanged
275         QString newPrefix = toqstr(cur.inset().completionPrefix(cur));
276         if (newPrefix == completionPrefix())
277                 return;
278         
279         // value which should be kept selected
280         QString old = currentCompletion();
281         if (old.length() == 0)
282                 old = last_selection_;
283         
284         // update completer to new prefix
285         setCompletionPrefix(newPrefix);
286         
287         // update popup because its size might have changed
288         if (popupVisible())
289                 updatePopup(cur);
290
291         // restore old selection
292         setCurrentCompletion(old);
293         
294         // if popup is not empty, the new selection will
295         // be our last valid one
296         QString const & s = currentCompletion();
297         if (s.length() > 0)
298                 last_selection_ = s;
299         else
300                 last_selection_ = old;
301         
302         // update inline completion because the default
303         // completion string might have changed
304         if (inlineVisible())
305                 updateInline(cur, s);
306 }
307
308
309 void GuiCompleter::updateInline(Cursor & cur, QString const & completion)
310 {
311         if (!cur.inset().inlineCompletionSupported(cur))
312                 return;
313         
314         // compute postfix
315         docstring prefix = cur.inset().completionPrefix(cur);
316         docstring postfix = from_utf8(fromqstr(completion.mid(prefix.length())));
317         
318         // shorten it if necessary
319         if (lyxrc.completion_inline_dots != -1
320             && postfix.size() > unsigned(lyxrc.completion_inline_dots))
321                 postfix = postfix.substr(0, lyxrc.completion_inline_dots - 1) + "...";
322
323         // set inline completion at cursor position
324         size_t uniqueTo = max(longestUniqueCompletion().size(), prefix.size());
325         gui_->bufferView().setInlineCompletion(cur, cur, postfix, uniqueTo - prefix.size());
326 }
327
328
329 void GuiCompleter::updatePopup(Cursor & cur)
330 {
331         if (!cur.inset().completionSupported(cur))
332                 return;
333         
334         if (completionCount() == 0)
335                 return;
336         
337         // get dimensions of completion prefix
338         Dimension dim;
339         int x;
340         int y;
341         cur.inset().completionPosAndDim(cur, x, y, dim);
342         QRect insetRect = QRect(x, y - dim.ascent() - 3, 200, dim.height() + 6);
343         
344         // show/update popup
345         complete(insetRect);
346         QTreeView * p = static_cast<QTreeView *>(popup());
347         p->setColumnWidth(0, popup()->width() - 22 - p->verticalScrollBar()->width());
348 }
349
350
351 void GuiCompleter::updateModel(Cursor & cur, bool popupUpdate, bool inlineUpdate)
352 {
353         // value which should be kept selected
354         QString old = currentCompletion();
355         if (old.length() == 0)
356                 old = last_selection_;
357         
358         // set new model
359         setModel(new GuiCompletionModel(this, cur.inset().completionList(cur)));
360         
361         // show popup
362         if (popupUpdate)
363                 updatePopup(cur);
364
365         // restore old selection
366         setCurrentCompletion(old);
367         
368         // if popup is not empty, the new selection will
369         // be our last valid one
370         QString const & s = currentCompletion();
371         if (s.length() > 0)
372                 last_selection_ = s;
373         else
374                 last_selection_ = old;
375         
376         // show inline completion
377         if (inlineUpdate)
378                 updateInline(cur, currentCompletion());
379 }
380
381
382 void GuiCompleter::showPopup(Cursor & cur)
383 {
384         if (!popupPossible(cur))
385                 return;
386         
387         updateModel(cur, true, inlineVisible());
388         updatePrefix(cur);
389 }
390         
391
392 void GuiCompleter::showInline(Cursor & cur)
393 {
394         if (!inlinePossible(cur))
395                 return;
396         
397         updateModel(cur, popupVisible(), true);
398         updatePrefix(cur);
399 }
400
401
402 void GuiCompleter::showPopup()
403 {
404         Cursor cur = gui_->bufferView().cursor();
405         cur.updateFlags(Update::None);
406         
407         showPopup(cur);
408
409         // redraw if needed
410         if (cur.disp_.update())
411                 gui_->bufferView().processUpdateFlags(cur.disp_.update());
412 }
413
414
415 void GuiCompleter::showInline()
416 {
417         Cursor cur = gui_->bufferView().cursor();
418         cur.updateFlags(Update::None);
419         
420         showInline(cur);
421
422         // redraw if needed
423         if (cur.disp_.update())
424                 gui_->bufferView().processUpdateFlags(cur.disp_.update());
425 }
426
427
428 void GuiCompleter::activate()
429 {
430         if (!popupVisible() && !inlineVisible())
431                 return;
432
433         // Complete with current selection in the popup.
434         QString s = currentCompletion();
435         popup()->hide();
436         popupActivated(s);
437 }
438
439
440 void GuiCompleter::tab()
441 {
442         BufferView * bv = &gui_->bufferView();
443         Cursor cur = bv->cursor();
444         cur.updateFlags(Update::None);
445         
446         // check that inline completion is active
447         if (!inlineVisible()) {
448                 // try to activate the inline completion
449                 if (cur.inset().inlineCompletionSupported(cur)) {
450                         showInline();
451                         return;
452                 }
453                 // or try popup
454                 if (!popupVisible() && cur.inset().completionSupported(cur)) {
455                         showPopup();
456                         return;
457                 }
458                 
459                 return;
460         }
461         
462         // If completion is active, at least complete by one character
463         docstring prefix = cur.inset().completionPrefix(cur);
464         docstring completion = from_utf8(fromqstr(currentCompletion()));
465         if (completion.size() <= prefix.size()) {
466                 // finalize completion
467                 cur.inset().insertCompletion(cur, docstring(), true);
468                 popup()->hide();
469                 updateVisibility(false, false);
470                 return;
471         }
472         docstring nextchar = completion.substr(prefix.size(), 1);
473         if (!cur.inset().insertCompletion(cur, nextchar, false))
474                 return;
475         updatePrefix(cur);
476
477         // try to complete as far as it is unique
478         docstring longestCompletion = longestUniqueCompletion();
479         prefix = cur.inset().completionPrefix(cur);
480         docstring postfix = longestCompletion.substr(min(longestCompletion.size(), prefix.size()));
481         cur.inset().insertCompletion(cur, postfix, false);
482         old_cursor_ = bv->cursor();
483         updatePrefix(cur);
484
485         // show popup without delay because the completion was not unique
486         if (lyxrc.completion_popup_after_complete
487             && !popupVisible()
488             && popup()->model()->rowCount() > 1)
489                 popup_timer_.start(0);
490
491         // redraw if needed
492         if (cur.disp_.update())
493                 gui_->bufferView().processUpdateFlags(cur.disp_.update());
494 }
495
496
497 QString GuiCompleter::currentCompletion() const
498 {
499         if (!popup()->selectionModel()->hasSelection())
500                 return QString();
501
502         // Not sure if this is bug in Qt: currentIndex() always 
503         // return the first element in the list.
504         QModelIndex idx = popup()->currentIndex();
505         return popup()->model()->data(idx, Qt::EditRole).toString();
506 }
507
508
509 void GuiCompleter::setCurrentCompletion(QString const & s)
510 {       
511         QAbstractItemModel const & model = *popup()->model();
512         size_t n = model.rowCount();
513         if (n == 0)
514                 return;
515
516         // select the first if s is empty
517         if (s.length() == 0) {
518                 updateLock_++;
519                 popup()->setCurrentIndex(model.index(0, 0));
520                 updateLock_--;
521                 return;
522         }
523
524         // iterate through list until the s is found
525         // FIXME: there must be a better way than this iteration
526         size_t i;
527         for (i = 0; i < n; ++i) {
528                 QString const & is
529                 = model.data(model.index(i, 0), Qt::EditRole).toString();
530                 if (is == s)
531                         break;
532         }
533
534         // select the first if none was found
535         if (i == n)
536                 i = 0;
537
538         updateLock_++;
539         popup()->setCurrentIndex(model.index(i, 0));
540         updateLock_--;
541 }
542
543
544 docstring GuiCompleter::longestUniqueCompletion() const {
545         QAbstractItemModel const & model = *popup()->model();
546         QString s = currentCompletion();
547         size_t n = model.rowCount();
548
549         // iterate through the completions and cut off where s differs
550         for (size_t i = 0; i < n && s.length() > 0; ++i) {
551                 QString const & is
552                 = model.data(model.index(i, 0), Qt::EditRole).toString();
553
554                 // find common prefix
555                 size_t j;
556                 size_t isn = is.length();
557                 size_t sn = s.length();
558                 for (j = 0; j < isn && j < sn; ++j) {
559                         if (s.at(j) != is.at(j))
560                                 break;
561                 }
562                 s = s.left(j);
563         }
564
565         return from_utf8(fromqstr(s));
566 }
567
568
569 void GuiCompleter::popupActivated(const QString & completion)
570 {
571         Cursor cur = gui_->bufferView().cursor();
572         cur.updateFlags(Update::None);
573         
574         docstring prefix = cur.inset().completionPrefix(cur);
575         docstring postfix = from_utf8(fromqstr(completion.mid(prefix.length())));
576         cur.inset().insertCompletion(cur, postfix, true);
577         updateVisibility(cur, false);
578         
579         if (cur.disp_.update())
580                 gui_->bufferView().processUpdateFlags(cur.disp_.update());
581 }
582
583
584 void GuiCompleter::popupHighlighted(const QString & completion)
585 {
586         if (updateLock_ > 0)
587                 return;
588
589         Cursor cur = gui_->bufferView().cursor();
590         cur.updateFlags(Update::None);
591         
592         updateInline(cur, completion);
593         
594         if (cur.disp_.update())
595                 gui_->bufferView().processUpdateFlags(cur.disp_.update());
596 }
597
598 } // namespace frontend
599 } // namespace lyx
600
601 #include "GuiCompleter_moc.cpp"