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