]> git.lyx.org Git - lyx.git/blob - src/frontends/qt4/GuiCompleter.cpp
a4cec0dfaf55b7adf8779cdfa8c9d557753b45ab
[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 "Paragraph.h"
24 #include "version.h"
25
26 #include "support/debug.h"
27
28 #include <QApplication>
29 #include <QAbstractListModel>
30 #include <QHeaderView>
31 #include <QPainter>
32 #include <QPixmapCache>
33 #include <QScrollBar>
34 #include <QItemDelegate>
35 #include <QTreeView>
36 #include <QTimer>
37
38 using namespace std;
39 using namespace lyx::support;
40
41 namespace lyx {
42 namespace frontend {
43
44 class RtlItemDelegate : public QItemDelegate {
45 public:
46         explicit RtlItemDelegate(QObject * parent = 0)
47                 : QItemDelegate(parent) {}
48
49 protected:
50         virtual void drawDisplay(QPainter * painter,
51                 QStyleOptionViewItem const & option,
52                 QRect const & rect, QString const & text) const
53         {
54                 // FIXME: do this more elegantly
55                 docstring stltext = qstring_to_ucs4(text);
56                 reverse(stltext.begin(), stltext.end());
57                 QItemDelegate::drawDisplay(painter, option, rect, toqstr(stltext));
58         }
59 };
60
61
62 class PixmapItemDelegate : public QItemDelegate {
63 public:
64         explicit PixmapItemDelegate(QObject *parent = 0)
65         : QItemDelegate(parent) {}
66
67 protected:
68         void paint(QPainter *painter, const QStyleOptionViewItem &option,
69                    const QModelIndex &index) const
70         {
71                 QStyleOptionViewItem opt = setOptions(index, option);
72                 QVariant value = index.data(Qt::DisplayRole);
73                 QPixmap pixmap = qvariant_cast<QPixmap>(value);
74                 
75                 // draw
76                 painter->save();
77                 drawBackground(painter, opt, index);
78                 if (!pixmap.isNull()) {
79                         const QSize size = pixmap.size();
80                         painter->drawPixmap(option.rect.left() + (16 - size.width()) / 2,
81                                 option.rect.top() + (option.rect.height() - size.height()) / 2,
82                                 pixmap);
83                 }
84                 drawFocus(painter, opt, option.rect);
85                 painter->restore();
86         }
87 };
88
89
90 class GuiCompletionModel : public QAbstractListModel {
91 public:
92         ///
93         GuiCompletionModel(QObject * parent,
94                 Inset::CompletionList const * l)
95                 : QAbstractListModel(parent), list_(l) {}
96         ///
97         ~GuiCompletionModel()
98                 { delete list_; }
99         ///
100         bool sorted() const
101         {
102                 if (list_)
103                         return list_->sorted();
104                 else
105                         return false;
106         }
107         ///
108         int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const
109         {
110                 return 2;
111         }
112         ///
113         int rowCount(const QModelIndex & /*parent*/ = QModelIndex()) const
114         {
115                 if (list_ == 0)
116                         return 0;
117                 else
118                         return list_->size();
119         }
120
121         ///
122         QVariant data(const QModelIndex & index, int role) const
123         {
124                 if (list_ == 0)
125                         return QVariant();
126
127                 if (index.row() < 0 || index.row() >= rowCount())
128                         return QVariant();
129
130                 if (role != Qt::DisplayRole && role != Qt::EditRole)
131                     return QVariant();
132                     
133                 if (index.column() == 0)
134                         return toqstr(list_->data(index.row()));
135                 else if (index.column() == 1) {
136                         // get icon from cache
137                         QPixmap scaled;
138                         QString const name = ":" + toqstr(list_->icon(index.row()));
139                         if (!QPixmapCache::find("completion" + name, scaled)) {
140                                 // load icon from disk
141                                 QPixmap p = QPixmap(name);
142                                 if (!p.isNull()) {
143                                         // scale it to 16x16 or smaller
144                                         scaled
145                                         = p.scaled(min(16, p.width()), min(16, p.height()), 
146                                                 Qt::KeepAspectRatio, Qt::SmoothTransformation);
147                                 }
148
149                                 QPixmapCache::insert("completion" + name, scaled);
150                         }
151                         return scaled;
152                 }
153                 return QVariant();
154         }
155
156 private:
157         ///
158         Inset::CompletionList const * list_;
159 };
160
161
162 GuiCompleter::GuiCompleter(GuiWorkArea * gui, QObject * parent)
163         : QCompleter(parent), gui_(gui), updateLock_(0),
164           inlineVisible_(false)
165 {
166         // Setup the completion popup
167         setModel(new GuiCompletionModel(this, 0));
168         setCompletionMode(QCompleter::PopupCompletion);
169         setWidget(gui_);
170         
171         // create the popup
172         QTreeView *listView = new QTreeView;
173         listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
174         listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
175         listView->setSelectionBehavior(QAbstractItemView::SelectRows);
176         listView->setSelectionMode(QAbstractItemView::SingleSelection);
177         listView->header()->hide();
178         listView->setIndentation(0);
179         listView->setUniformRowHeights(true);
180         setPopup(listView);
181         popup()->setItemDelegateForColumn(1, new PixmapItemDelegate(this));
182         rtlItemDelegate_ = new RtlItemDelegate(this);
183         
184         // create timeout timers
185         popup_timer_.setSingleShot(true);
186         inline_timer_.setSingleShot(true);
187         connect(this, SIGNAL(highlighted(const QString &)),
188                 this, SLOT(popupHighlighted(const QString &)));
189         connect(this, SIGNAL(activated(const QString &)),
190                 this, SLOT(popupActivated(const QString &)));
191         connect(&popup_timer_, SIGNAL(timeout()),
192                 this, SLOT(showPopup()));
193         connect(&inline_timer_, SIGNAL(timeout()),
194                 this, SLOT(showInline()));
195 }
196
197
198 GuiCompleter::~GuiCompleter()
199 {
200         popup()->hide();
201 }
202
203
204 bool GuiCompleter::eventFilter(QObject * watched, QEvent * e)
205 {
206         // hijack back the tab key from the popup
207         // (which stole it from the workspace before)
208         if (e->type() == QEvent::KeyPress && popupVisible()) {
209                 QKeyEvent *ke = static_cast<QKeyEvent *>(e);
210                 switch (ke->key()) {
211                 case Qt::Key_Tab:
212                         tab();
213                         ke->accept();
214                         return true;
215                 default: break;
216                 }
217         }
218         
219         return QCompleter::eventFilter(watched, e);
220 }
221
222
223 bool GuiCompleter::popupPossible(Cursor const & cur) const
224 {
225         return QApplication::activeWindow()
226                 && gui_->hasFocus()
227                 && cur.inset().completionSupported(cur);
228 }
229
230
231 bool GuiCompleter::inlinePossible(Cursor const & cur) const
232 {
233         return cur.inset().inlineCompletionSupported(cur);
234 }
235
236
237 bool GuiCompleter::popupVisible() const
238 {
239         return popup()->isVisible();
240 }
241
242
243 bool GuiCompleter::inlineVisible() const
244 {
245         // In fact using BufferView::inlineCompletionPos.empty() should be
246         // here. But unfortunately this information is not good enough
247         // because destructive operations like backspace might invalidate
248         // inlineCompletionPos. But then the completion should stay visible
249         // (i.e. reshown on the next update). Hence be keep this information
250         // in the inlineVisible_ variable.
251         return inlineVisible_;
252 }
253
254
255 void GuiCompleter::updateVisibility(Cursor & cur, bool start, bool keep, bool cursorInView)
256 {
257         // parameters which affect the completion
258         bool moved = cur != old_cursor_;
259         if (moved)
260                 old_cursor_ = cur;
261
262         bool possiblePopupState = popupPossible(cur) && cursorInView;
263         bool possibleInlineState = inlinePossible(cur) && cursorInView;
264
265         // we moved or popup state is not ok for popup?
266         if ((moved && !keep) || !possiblePopupState) {
267                 // stop an old completion timer
268                 if (popup_timer_.isActive())
269                         popup_timer_.stop();
270
271                 // hide old popup
272                 if (popupVisible())
273                         popup()->hide();
274         }
275
276         // we moved or inline state is not ok for inline completion?
277         if ((moved && !keep) || !possibleInlineState) {
278                 // stop an old completion timer
279                 if (inline_timer_.isActive())
280                         inline_timer_.stop();
281
282                 // hide old inline completion
283                 if (inlineVisible()) {
284                         gui_->bufferView().setInlineCompletion(cur, DocIterator(), docstring());
285                         inlineVisible_ = false;
286                 }
287         }
288
289         // we inserted something and are in a possible popup state?
290         if (!popupVisible() && possiblePopupState && start
291                 && cur.inset().automaticPopupCompletion())
292                 popup_timer_.start(int(lyxrc.completion_popup_delay * 1000));
293
294         // we inserted something and are in a possible inline completion state?
295         if (!inlineVisible() && possibleInlineState && start
296                 && cur.inset().automaticInlineCompletion())
297                 inline_timer_.start(int(lyxrc.completion_inline_delay * 1000));
298
299         // update prefix if popup is visible or if it will be visible soon
300         if (popupVisible() || inlineVisible()
301             || popup_timer_.isActive() || inline_timer_.isActive())
302                 updatePrefix(cur);
303 }
304
305
306 void GuiCompleter::updateVisibility(bool start, bool keep)
307 {
308         Cursor cur = gui_->bufferView().cursor();
309         cur.updateFlags(Update::None);
310         
311         updateVisibility(cur, start, keep);
312         
313         if (cur.disp_.update())
314                 gui_->bufferView().processUpdateFlags(cur.disp_.update());
315 }
316
317
318 void GuiCompleter::updatePrefix(Cursor & cur)
319 {
320         // get new prefix. Do nothing if unchanged
321         QString newPrefix = toqstr(cur.inset().completionPrefix(cur));
322         if (newPrefix == completionPrefix())
323                 return;
324         
325         // value which should be kept selected
326         QString old = currentCompletion();
327         if (old.length() == 0)
328                 old = last_selection_;
329         
330         // update completer to new prefix
331         setCompletionPrefix(newPrefix);
332
333         // update popup because its size might have changed
334         if (popupVisible())
335                 updatePopup(cur);
336
337         // restore old selection
338         setCurrentCompletion(old);
339         
340         // if popup is not empty, the new selection will
341         // be our last valid one
342         QString const & s = currentCompletion();
343         if (s.length() > 0)
344                 last_selection_ = s;
345         else
346                 last_selection_ = old;
347         
348         // update inline completion because the default
349         // completion string might have changed
350         if (inlineVisible())
351                 updateInline(cur, s);
352 }
353
354
355 void GuiCompleter::updateInline(Cursor & cur, QString const & completion)
356 {
357         if (!cur.inset().inlineCompletionSupported(cur))
358                 return;
359         
360         // compute postfix
361         docstring prefix = cur.inset().completionPrefix(cur);
362         docstring postfix = from_utf8(fromqstr(completion.mid(prefix.length())));
363         
364         // shorten it if necessary
365         if (lyxrc.completion_inline_dots != -1
366             && postfix.size() > unsigned(lyxrc.completion_inline_dots))
367                 postfix = postfix.substr(0, lyxrc.completion_inline_dots - 1) + "...";
368
369         // set inline completion at cursor position
370         size_t uniqueTo = max(longestUniqueCompletion().size(), prefix.size());
371         gui_->bufferView().setInlineCompletion(cur, cur, postfix, uniqueTo - prefix.size());
372         inlineVisible_ = true;
373 }
374
375
376 void GuiCompleter::updatePopup(Cursor & cur)
377 {
378         if (!cur.inset().completionSupported(cur))
379                 return;
380         
381         if (completionCount() == 0)
382                 return;
383         
384         // get dimensions of completion prefix
385         Dimension dim;
386         int x;
387         int y;
388         cur.inset().completionPosAndDim(cur, x, y, dim);
389         
390         // and calculate the rect of the popup
391         QRect rect;
392         if (popup()->layoutDirection() == Qt::RightToLeft)
393                 rect = QRect(x + dim.width() - 200, y - dim.ascent() - 3, 200, dim.height() + 6);
394         else
395                 rect = QRect(x, y - dim.ascent() - 3, 200, dim.height() + 6);
396         
397         // show/update popup
398         complete(rect);
399         QTreeView * p = static_cast<QTreeView *>(popup());
400         p->setColumnWidth(0, popup()->width() - 22 - p->verticalScrollBar()->width());
401 }
402
403
404 void GuiCompleter::updateModel(Cursor & cur, bool popupUpdate, bool inlineUpdate)
405 {
406         // value which should be kept selected
407         QString old = currentCompletion();
408         if (old.length() == 0)
409                 old = last_selection_;
410
411         // set whether rtl
412         bool rtl = false;
413         if (cur.inTexted()) {
414                 Paragraph const & par = cur.paragraph();
415                 Font const font =
416                 par.getFontSettings(cur.bv().buffer().params(), cur.pos());
417                 rtl = font.isVisibleRightToLeft();
418         }
419         popup()->setLayoutDirection(rtl ? Qt::RightToLeft : Qt::LeftToRight);
420
421         // turn the direction of the strings in the popup.
422         // Qt does not do that itself.
423         popup()->setItemDelegateForColumn(0, rtl ? rtlItemDelegate_ : 0);
424
425         // set new model
426         Inset::CompletionList const * list
427         = cur.inset().createCompletionList(cur);
428         setModel(new GuiCompletionModel(this, list));
429         if (list->sorted())
430                 setModelSorting(QCompleter::CaseSensitivelySortedModel);
431         else
432                 setModelSorting(QCompleter::UnsortedModel);
433
434         // set prefix
435         QString newPrefix = toqstr(cur.inset().completionPrefix(cur));
436         if (newPrefix != completionPrefix())
437                 setCompletionPrefix(newPrefix);
438
439         // show popup
440         if (popupUpdate)
441                 updatePopup(cur);
442
443         // restore old selection
444         setCurrentCompletion(old);
445         
446         // if popup is not empty, the new selection will
447         // be our last valid one
448         QString const & s = currentCompletion();
449         if (s.length() > 0)
450                 last_selection_ = s;
451         else
452                 last_selection_ = old;
453
454         // show inline completion
455         if (inlineUpdate)
456                 updateInline(cur, currentCompletion());
457 }
458
459
460 void GuiCompleter::showPopup(Cursor & cur)
461 {
462         if (!popupPossible(cur))
463                 return;
464         
465         updateModel(cur, true, inlineVisible());
466 }
467
468
469 void GuiCompleter::showInline(Cursor & cur)
470 {
471         if (!inlinePossible(cur))
472                 return;
473         
474         updateModel(cur, popupVisible(), true);
475 }
476
477
478 void GuiCompleter::showPopup()
479 {
480         Cursor cur = gui_->bufferView().cursor();
481         cur.updateFlags(Update::None);
482         
483         showPopup(cur);
484
485         // redraw if needed
486         if (cur.disp_.update())
487                 gui_->bufferView().processUpdateFlags(cur.disp_.update());
488 }
489
490
491 void GuiCompleter::showInline()
492 {
493         Cursor cur = gui_->bufferView().cursor();
494         cur.updateFlags(Update::None);
495         
496         showInline(cur);
497
498         // redraw if needed
499         if (cur.disp_.update())
500                 gui_->bufferView().processUpdateFlags(cur.disp_.update());
501 }
502
503
504 void GuiCompleter::activate()
505 {
506         if (!popupVisible() && !inlineVisible())
507                 return;
508
509         // Complete with current selection in the popup.
510         QString s = currentCompletion();
511         popup()->hide();
512         popupActivated(s);
513 }
514
515
516 void GuiCompleter::tab()
517 {
518         BufferView * bv = &gui_->bufferView();
519         Cursor cur = bv->cursor();
520         cur.updateFlags(Update::None);
521         
522         // check that inline completion is active
523         if (!inlineVisible()) {
524                 // try to activate the inline completion
525                 if (cur.inset().inlineCompletionSupported(cur)) {
526                         showInline();
527                         
528                         // show popup without delay because the completion was not unique
529                         if (lyxrc.completion_popup_after_complete
530                             && !popupVisible()
531                             && popup()->model()->rowCount() > 1)
532                                 popup_timer_.start(0);
533
534                         return;
535                 }
536                 // or try popup
537                 if (!popupVisible() && cur.inset().completionSupported(cur)) {
538                         showPopup();
539                         return;
540                 }
541                 
542                 return;
543         }
544         
545         // If completion is active, at least complete by one character
546         docstring prefix = cur.inset().completionPrefix(cur);
547         docstring completion = from_utf8(fromqstr(currentCompletion()));
548         if (completion.size() <= prefix.size()) {
549                 // finalize completion
550                 cur.inset().insertCompletion(cur, docstring(), true);
551                 
552                 // hide popup and inline completion
553                 popup()->hide();
554                 gui_->bufferView().setInlineCompletion(cur, DocIterator(), docstring());
555                 inlineVisible_ = false;
556                 updateVisibility(false, false);
557                 return;
558         }
559         docstring nextchar = completion.substr(prefix.size(), 1);
560         if (!cur.inset().insertCompletion(cur, nextchar, false))
561                 return;
562         updatePrefix(cur);
563
564         // try to complete as far as it is unique
565         docstring longestCompletion = longestUniqueCompletion();
566         prefix = cur.inset().completionPrefix(cur);
567         docstring postfix = longestCompletion.substr(min(longestCompletion.size(), prefix.size()));
568         cur.inset().insertCompletion(cur, postfix, false);
569         old_cursor_ = bv->cursor();
570         updatePrefix(cur);
571
572         // show popup without delay because the completion was not unique
573         if (lyxrc.completion_popup_after_complete
574             && !popupVisible()
575             && popup()->model()->rowCount() > 1)
576                 popup_timer_.start(0);
577
578         // redraw if needed
579         if (cur.disp_.update())
580                 gui_->bufferView().processUpdateFlags(cur.disp_.update());
581 }
582
583
584 QString GuiCompleter::currentCompletion() const
585 {
586         if (!popup()->selectionModel()->hasSelection())
587                 return QString();
588
589         // Not sure if this is bug in Qt: currentIndex() always 
590         // return the first element in the list.
591         QModelIndex idx = popup()->currentIndex();
592         return popup()->model()->data(idx, Qt::EditRole).toString();
593 }
594
595
596 void GuiCompleter::setCurrentCompletion(QString const & s)
597 {       
598         QAbstractItemModel const & model = *popup()->model();
599         size_t n = model.rowCount();
600         if (n == 0)
601                 return;
602
603         // select the first if s is empty
604         if (s.length() == 0) {
605                 updateLock_++;
606                 popup()->setCurrentIndex(model.index(0, 0));
607                 updateLock_--;
608                 return;
609         }
610
611         // find old selection in model
612         size_t i;
613         if (modelSorting() == QCompleter::UnsortedModel) {
614                 // In unsorted models, iterate through list until the s is found
615                 for (i = 0; i < n; ++i) {
616                         QString const & is
617                         = model.data(model.index(i, 0), Qt::EditRole).toString();
618                         if (is == s)
619                                 break;
620                 }
621         } else {
622                 // In sorted models, do binary search for s.
623                 i = 0;
624                 size_t r = n - 1;
625                 int c;
626                 do {
627                         size_t mid = (r + i) / 2;
628                         QString const & mids
629                         = model.data(model.index(mid, 0),
630                                      Qt::EditRole).toString();
631
632                         // left or right?
633                         // FIXME: is this really the same order that the docstring
634                         // from the CompletionList has?
635                         c = s.compare(mids, Qt::CaseSensitive);
636                         if (c == 0) {
637                                 i = mid;
638                                 break;
639                         } else if (c > 0)
640                                 // middle is not far enough
641                                 i = mid + 1;
642                         else
643                                 // middle is too far
644                                 r = mid;
645
646                 } while (r - i > 0 && i < n);
647
648                 // loop was left with failed comparison?
649                 // i.e. word was not found.
650                 if (c != 0)
651                         i = n;
652         }
653
654         // select the first if none was found
655         if (i == n)
656                 i = 0;
657
658         updateLock_++;
659         popup()->setCurrentIndex(model.index(i, 0));
660         updateLock_--;
661 }
662
663
664 size_t commonPrefix(QString const & s1, QString const & s2)
665 {
666         // find common prefix
667         size_t j;
668         size_t n1 = s1.length();
669         size_t n2 = s2.length();
670         for (j = 0; j < n1 && j < n2; ++j) {
671                 if (s1.at(j) != s2.at(j))
672                         break;
673         }
674         return j;
675 }
676
677
678 docstring GuiCompleter::longestUniqueCompletion() const
679 {
680         QAbstractItemModel const & model = *popup()->model();
681         QString s = currentCompletion();
682         size_t n = model.rowCount();
683
684         if (modelSorting() == QCompleter::UnsortedModel) {
685                 // For unsorted model we cannot do more than iteration.
686                 // Iterate through the completions and cut off where s differs
687                 for (size_t i = 0; i < n && s.length() > 0; ++i) {
688                         QString const & is
689                         = model.data(model.index(i, 0), Qt::EditRole).toString();
690
691                         s = s.left(commonPrefix(is, s));
692                 }
693         } else {
694                 // For sorted models we can do binary search multiple times,
695                 // each time to find the first string which has s not as prefix.
696                 size_t i = 0;
697                 while (i < n && s.length() > 0) {
698                         // find first string that does not have s as prefix
699                         // via binary search in [i,n-1]
700                         size_t r = n - 1;
701                         do {
702                                 // get common prefix with the middle string
703                                 size_t mid = (r + i) / 2;
704                                 QString const & mids
705                                 = model.data(model.index(mid, 0), 
706                                         Qt::EditRole).toString();
707                                 size_t oldLen = s.length();
708                                 size_t len = commonPrefix(mids, s);
709                                 s = s.left(len);
710
711                                 // left or right?
712                                 if (oldLen == len) {
713                                         // middle is not far enough
714                                         i = mid + 1;
715                                 } else {
716                                         // middle is maybe too far
717                                         r = mid;
718                                 }
719                         } while (r - i > 0 && i < n);
720                 }
721         }
722
723         return from_utf8(fromqstr(s));
724 }
725
726
727 void GuiCompleter::popupActivated(const QString & completion)
728 {
729         Cursor cur = gui_->bufferView().cursor();
730         cur.updateFlags(Update::None);
731         
732         docstring prefix = cur.inset().completionPrefix(cur);
733         docstring postfix = from_utf8(fromqstr(completion.mid(prefix.length())));
734         cur.inset().insertCompletion(cur, postfix, true);
735         updateVisibility(cur, false);
736         
737         if (cur.disp_.update())
738                 gui_->bufferView().processUpdateFlags(cur.disp_.update());
739 }
740
741
742 void GuiCompleter::popupHighlighted(const QString & completion)
743 {
744         if (updateLock_ > 0)
745                 return;
746
747         Cursor cur = gui_->bufferView().cursor();
748         cur.updateFlags(Update::None);
749         
750         updateInline(cur, completion);
751         
752         if (cur.disp_.update())
753                 gui_->bufferView().processUpdateFlags(cur.disp_.update());
754 }
755
756 } // namespace frontend
757 } // namespace lyx
758
759 #include "GuiCompleter_moc.cpp"