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