]> git.lyx.org Git - lyx.git/blob - src/frontends/qt4/GuiCompleter.cpp
20ad50f4717495a7a172c0d8e53f4710b343fed3
[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         setModel(new GuiCompletionModel(this, cur.inset().completionList(cur)));
368         
369         // show popup
370         if (popupUpdate)
371                 updatePopup(cur);
372
373         // restore old selection
374         setCurrentCompletion(old);
375         
376         // if popup is not empty, the new selection will
377         // be our last valid one
378         QString const & s = currentCompletion();
379         if (s.length() > 0)
380                 last_selection_ = s;
381         else
382                 last_selection_ = old;
383         
384         // show inline completion
385         if (inlineUpdate)
386                 updateInline(cur, currentCompletion());
387 }
388
389
390 void GuiCompleter::showPopup(Cursor & cur)
391 {
392         if (!popupPossible(cur))
393                 return;
394         
395         updateModel(cur, true, inlineVisible());
396         updatePrefix(cur);
397 }
398         
399
400 void GuiCompleter::showInline(Cursor & cur)
401 {
402         if (!inlinePossible(cur))
403                 return;
404         
405         updateModel(cur, popupVisible(), true);
406         updatePrefix(cur);
407 }
408
409
410 void GuiCompleter::showPopup()
411 {
412         Cursor cur = gui_->bufferView().cursor();
413         cur.updateFlags(Update::None);
414         
415         showPopup(cur);
416
417         // redraw if needed
418         if (cur.disp_.update())
419                 gui_->bufferView().processUpdateFlags(cur.disp_.update());
420 }
421
422
423 void GuiCompleter::showInline()
424 {
425         Cursor cur = gui_->bufferView().cursor();
426         cur.updateFlags(Update::None);
427         
428         showInline(cur);
429
430         // redraw if needed
431         if (cur.disp_.update())
432                 gui_->bufferView().processUpdateFlags(cur.disp_.update());
433 }
434
435
436 void GuiCompleter::activate()
437 {
438         if (!popupVisible() && !inlineVisible())
439                 return;
440
441         // Complete with current selection in the popup.
442         QString s = currentCompletion();
443         popup()->hide();
444         popupActivated(s);
445 }
446
447
448 void GuiCompleter::tab()
449 {
450         BufferView * bv = &gui_->bufferView();
451         Cursor cur = bv->cursor();
452         cur.updateFlags(Update::None);
453         
454         // check that inline completion is active
455         if (!inlineVisible()) {
456                 // try to activate the inline completion
457                 if (cur.inset().inlineCompletionSupported(cur)) {
458                         showInline();
459                         
460                         // show popup without delay because the completion was not unique
461                         if (lyxrc.completion_popup_after_complete
462                             && !popupVisible()
463                             && popup()->model()->rowCount() > 1)
464                                 popup_timer_.start(0);
465
466                         return;
467                 }
468                 // or try popup
469                 if (!popupVisible() && cur.inset().completionSupported(cur)) {
470                         showPopup();
471                         return;
472                 }
473                 
474                 return;
475         }
476         
477         // If completion is active, at least complete by one character
478         docstring prefix = cur.inset().completionPrefix(cur);
479         docstring completion = from_utf8(fromqstr(currentCompletion()));
480         if (completion.size() <= prefix.size()) {
481                 // finalize completion
482                 cur.inset().insertCompletion(cur, docstring(), true);
483                 popup()->hide();
484                 updateVisibility(false, false);
485                 return;
486         }
487         docstring nextchar = completion.substr(prefix.size(), 1);
488         if (!cur.inset().insertCompletion(cur, nextchar, false))
489                 return;
490         updatePrefix(cur);
491
492         // try to complete as far as it is unique
493         docstring longestCompletion = longestUniqueCompletion();
494         prefix = cur.inset().completionPrefix(cur);
495         docstring postfix = longestCompletion.substr(min(longestCompletion.size(), prefix.size()));
496         cur.inset().insertCompletion(cur, postfix, false);
497         old_cursor_ = bv->cursor();
498         updatePrefix(cur);
499
500         // show popup without delay because the completion was not unique
501         if (lyxrc.completion_popup_after_complete
502             && !popupVisible()
503             && popup()->model()->rowCount() > 1)
504                 popup_timer_.start(0);
505
506         // redraw if needed
507         if (cur.disp_.update())
508                 gui_->bufferView().processUpdateFlags(cur.disp_.update());
509 }
510
511
512 QString GuiCompleter::currentCompletion() const
513 {
514         if (!popup()->selectionModel()->hasSelection())
515                 return QString();
516
517         // Not sure if this is bug in Qt: currentIndex() always 
518         // return the first element in the list.
519         QModelIndex idx = popup()->currentIndex();
520         return popup()->model()->data(idx, Qt::EditRole).toString();
521 }
522
523
524 void GuiCompleter::setCurrentCompletion(QString const & s)
525 {       
526         QAbstractItemModel const & model = *popup()->model();
527         size_t n = model.rowCount();
528         if (n == 0)
529                 return;
530
531         // select the first if s is empty
532         if (s.length() == 0) {
533                 updateLock_++;
534                 popup()->setCurrentIndex(model.index(0, 0));
535                 updateLock_--;
536                 return;
537         }
538
539         // iterate through list until the s is found
540         // FIXME: there must be a better way than this iteration
541         size_t i;
542         for (i = 0; i < n; ++i) {
543                 QString const & is
544                 = model.data(model.index(i, 0), Qt::EditRole).toString();
545                 if (is == s)
546                         break;
547         }
548
549         // select the first if none was found
550         if (i == n)
551                 i = 0;
552
553         updateLock_++;
554         popup()->setCurrentIndex(model.index(i, 0));
555         updateLock_--;
556 }
557
558
559 docstring GuiCompleter::longestUniqueCompletion() const {
560         QAbstractItemModel const & model = *popup()->model();
561         QString s = currentCompletion();
562         size_t n = model.rowCount();
563
564         // iterate through the completions and cut off where s differs
565         for (size_t i = 0; i < n && s.length() > 0; ++i) {
566                 QString const & is
567                 = model.data(model.index(i, 0), Qt::EditRole).toString();
568
569                 // find common prefix
570                 size_t j;
571                 size_t isn = is.length();
572                 size_t sn = s.length();
573                 for (j = 0; j < isn && j < sn; ++j) {
574                         if (s.at(j) != is.at(j))
575                                 break;
576                 }
577                 s = s.left(j);
578         }
579
580         return from_utf8(fromqstr(s));
581 }
582
583
584 void GuiCompleter::popupActivated(const QString & completion)
585 {
586         Cursor cur = gui_->bufferView().cursor();
587         cur.updateFlags(Update::None);
588         
589         docstring prefix = cur.inset().completionPrefix(cur);
590         docstring postfix = from_utf8(fromqstr(completion.mid(prefix.length())));
591         cur.inset().insertCompletion(cur, postfix, true);
592         updateVisibility(cur, false);
593         
594         if (cur.disp_.update())
595                 gui_->bufferView().processUpdateFlags(cur.disp_.update());
596 }
597
598
599 void GuiCompleter::popupHighlighted(const QString & completion)
600 {
601         if (updateLock_ > 0)
602                 return;
603
604         Cursor cur = gui_->bufferView().cursor();
605         cur.updateFlags(Update::None);
606         
607         updateInline(cur, completion);
608         
609         if (cur.disp_.update())
610                 gui_->bufferView().processUpdateFlags(cur.disp_.update());
611 }
612
613 } // namespace frontend
614 } // namespace lyx
615
616 #include "GuiCompleter_moc.cpp"