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