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