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