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