2 * \file GuiCompleter.cpp
3 * This file is part of LyX, the document processor.
4 * Licence details can be found in the file COPYING.
6 * \author Stefan Schimanski
8 * Full author contact details are available in file CREDITS.
13 #include "GuiCompleter.h"
16 #include "BufferView.h"
17 #include "CompletionList.h"
19 #include "Dimension.h"
20 #include "GuiWorkArea.h"
24 #include "Paragraph.h"
25 #include "qt_helpers.h"
28 #include "support/lassert.h"
29 #include "support/debug.h"
31 #include <QApplication>
32 #include <QHeaderView>
35 #include <QPixmapCache>
37 #include <QItemDelegate>
42 using namespace lyx::support;
47 class CompleterItemDelegate : public QItemDelegate
50 explicit CompleterItemDelegate(QObject * parent)
51 : QItemDelegate(parent)
54 ~CompleterItemDelegate()
58 void paint(QPainter *painter, const QStyleOptionViewItem &option,
59 const QModelIndex &index) const
61 if (index.column() == 0) {
62 QItemDelegate::paint(painter, option, index);
65 QStyleOptionViewItem opt = setOptions(index, option);
66 QVariant value = index.data(Qt::DisplayRole);
67 QPixmap pixmap = qvariant_cast<QPixmap>(value);
71 drawBackground(painter, opt, index);
72 if (!pixmap.isNull()) {
73 const QSize size = pixmap.size();
74 painter->drawPixmap(option.rect.left() + (16 - size.width()) / 2,
75 option.rect.top() + (option.rect.height() - size.height()) / 2,
78 drawFocus(painter, opt, option.rect);
83 class GuiCompletionModel : public QAbstractListModel
87 GuiCompletionModel(QObject * parent, CompletionList const * l)
88 : QAbstractListModel(parent), list_(l)
91 ~GuiCompletionModel() { delete list_; }
93 void setList(CompletionList const * l) {
103 return list_->sorted();
107 int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const
112 int rowCount(const QModelIndex & /*parent*/ = QModelIndex()) const
116 return list_->size();
120 QVariant data(const QModelIndex & index, int role) const
125 if (index.row() < 0 || index.row() >= rowCount())
128 if (role != Qt::DisplayRole && role != Qt::EditRole)
131 if (index.column() == 0)
132 return toqstr(list_->data(index.row()));
134 if (index.column() != 1)
137 // get icon from cache
139 QString const name = ":" + toqstr(list_->icon(index.row()));
142 if (!QPixmapCache::find("completion" + name, scaled)) {
143 // load icon from disk
144 QPixmap p = QPixmap(name);
146 // scale it to 16x16 or smaller
147 scaled = p.scaled(min(16, p.width()), min(16, p.height()),
148 Qt::KeepAspectRatio, Qt::SmoothTransformation);
150 QPixmapCache::insert("completion" + name, scaled);
157 CompletionList const * list_;
161 GuiCompleter::GuiCompleter(GuiWorkArea * gui, QObject * parent)
162 : QCompleter(parent), gui_(gui), old_cursor_(0), updateLock_(0),
163 inlineVisible_(false), popupVisible_(false),
166 // Setup the completion popup
167 model_ = new GuiCompletionModel(this, 0);
169 setCompletionMode(QCompleter::PopupCompletion);
170 setCaseSensitivity(Qt::CaseSensitive);
174 QTreeView *listView = new QTreeView;
175 listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
176 listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
177 listView->setSelectionBehavior(QAbstractItemView::SelectRows);
178 listView->setSelectionMode(QAbstractItemView::SingleSelection);
179 listView->header()->hide();
180 listView->setIndentation(0);
181 listView->setUniformRowHeights(true);
184 itemDelegate_ = new CompleterItemDelegate(this);
185 popup()->setItemDelegate(itemDelegate_);
187 // create timeout timers
188 popup_timer_.setSingleShot(true);
189 inline_timer_.setSingleShot(true);
190 connect(this, SIGNAL(highlighted(const QString &)),
191 this, SLOT(popupHighlighted(const QString &)));
192 connect(this, SIGNAL(activated(const QString &)),
193 this, SLOT(popupActivated(const QString &)));
194 connect(&popup_timer_, SIGNAL(timeout()),
195 this, SLOT(showPopup()));
196 connect(&inline_timer_, SIGNAL(timeout()),
197 this, SLOT(showInline()));
201 GuiCompleter::~GuiCompleter()
207 bool GuiCompleter::eventFilter(QObject * watched, QEvent * e)
209 // hijack back the tab key from the popup
210 // (which stole it from the workspace before)
211 if (e->type() == QEvent::KeyPress && popupVisible()) {
212 QKeyEvent *ke = static_cast<QKeyEvent *>(e);
222 return QCompleter::eventFilter(watched, e);
226 bool GuiCompleter::popupPossible(Cursor const & cur) const
228 return QApplication::activeWindow()
230 && cur.inset().completionSupported(cur);
234 bool GuiCompleter::inlinePossible(Cursor const & cur) const
236 return cur.inset().inlineCompletionSupported(cur);
240 bool GuiCompleter::uniqueCompletionAvailable() const
245 size_t n = popup()->model()->rowCount();
249 // if there is exactly one, we have to check whether it is a
250 // real completion, i.e. longer than the current prefix.
251 if (completionPrefix() == currentCompletion())
258 bool GuiCompleter::completionAvailable() const
263 size_t n = popup()->model()->rowCount();
265 // if there is exactly one, we have to check whether it is a
266 // real completion, i.e. longer than the current prefix.
267 if (n == 1 && completionPrefix() == currentCompletion())
274 bool GuiCompleter::popupVisible() const
276 return popupVisible_;
280 bool GuiCompleter::inlineVisible() const
282 // In fact using BufferView::inlineCompletionPos.empty() should be
283 // here. But unfortunately this information is not good enough
284 // because destructive operations like backspace might invalidate
285 // inlineCompletionPos. But then the completion should stay visible
286 // (i.e. reshown on the next update). Hence be keep this information
287 // in the inlineVisible_ variable.
288 return inlineVisible_;
292 void GuiCompleter::updateVisibility(Cursor & cur, bool start, bool keep)
294 // parameters which affect the completion
295 bool moved = cur != old_cursor_;
299 bool const possiblePopupState = popupPossible(cur);
300 bool const possibleInlineState = inlinePossible(cur);
302 // we moved or popup state is not ok for popup?
303 if ((moved && !keep) || !possiblePopupState)
306 // we moved or inline state is not ok for inline completion?
307 if ((moved && !keep) || !possibleInlineState)
310 // we inserted something and are in a possible popup state?
311 if (!popupVisible() && possiblePopupState && start
312 && cur.inset().automaticPopupCompletion())
313 popup_timer_.start(int(lyxrc.completion_popup_delay * 1000));
315 // we inserted something and are in a possible inline completion state?
316 if (!inlineVisible() && possibleInlineState && start
317 && cur.inset().automaticInlineCompletion())
318 inline_timer_.start(int(lyxrc.completion_inline_delay * 1000));
319 else if (cur.inMathed() && !lyxrc.completion_inline_math) {
320 // no inline completion, hence a metrics update is needed
321 if (!(cur.result().screenUpdate() & Update::Force))
322 cur.screenUpdateFlags(cur.result().screenUpdate() | Update::SinglePar);
325 // update prefix if any completion is possible
326 bool modelActive = modelActive_ && model()->rowCount() > 0;
327 if (possiblePopupState || possibleInlineState) {
331 updateAvailability();
336 void GuiCompleter::updateVisibility(bool start, bool keep)
338 Cursor cur = gui_->bufferView().cursor();
339 cur.screenUpdateFlags(Update::None);
341 updateVisibility(cur, start, keep);
343 if (cur.result().screenUpdate())
344 gui_->bufferView().processUpdateFlags(cur.result().screenUpdate());
348 void GuiCompleter::updatePrefix(Cursor const & cur)
350 // get new prefix. Do nothing if unchanged
351 QString newPrefix = toqstr(cur.inset().completionPrefix(cur));
352 if (newPrefix == completionPrefix())
355 // value which should be kept selected
356 QString old = currentCompletion();
357 if (old.length() == 0)
358 old = last_selection_;
360 // update completer to new prefix
361 setCompletionPrefix(newPrefix);
363 // update popup because its size might have changed
367 // restore old selection
368 setCurrentCompletion(old);
370 // if popup is not empty, the new selection will
371 // be our last valid one
372 QString const & s = currentCompletion();
373 if (popupVisible() || inlineVisible()) {
377 last_selection_ = old;
380 // update inline completion because the default
381 // completion string might have changed
383 updateInline(cur, s);
387 void GuiCompleter::updateInline(Cursor const & cur, QString const & completion)
389 if (!cur.inset().inlineCompletionSupported(cur))
393 docstring prefix = cur.inset().completionPrefix(cur);
394 docstring postfix = qstring_to_ucs4(completion.mid(prefix.length()));
396 // shorten it if necessary
397 if (lyxrc.completion_inline_dots != -1
398 && postfix.size() > unsigned(lyxrc.completion_inline_dots))
399 postfix = postfix.substr(0, lyxrc.completion_inline_dots - 1) + "...";
401 // set inline completion at cursor position
402 size_t uniqueTo = max(longestUniqueCompletion().size(), prefix.size());
403 gui_->bufferView().setInlineCompletion(cur, cur, postfix, uniqueTo - prefix.size());
404 inlineVisible_ = true;
408 void GuiCompleter::updatePopup(Cursor const & cur)
410 if (!cur.inset().completionSupported(cur))
413 popupVisible_ = true;
415 if (completionCount() == 0) {
416 QTimer::singleShot(0, popup(), SLOT(hide()));
420 QTimer::singleShot(0, this, SLOT(asyncUpdatePopup()));
424 void GuiCompleter::asyncUpdatePopup()
426 Cursor cur = gui_->bufferView().cursor();
427 if (!cur.inset().completionSupported(cur)
428 || !cur.bv().paragraphVisible(cur)) {
429 popupVisible_ = false;
433 // get dimensions of completion prefix
437 cur.inset().completionPosAndDim(cur, x, y, dim);
439 // and calculate the rect of the popup
441 if (popup()->layoutDirection() == Qt::RightToLeft)
442 rect = QRect(x + dim.width() - 200, y - dim.ascent() - 3, 200, dim.height() + 6);
444 rect = QRect(x, y - dim.ascent() - 3, 200, dim.height() + 6);
446 // Resize the columns in the popup.
447 // This should really be in the constructor. But somehow the treeview
448 // has a bad memory about it and we have to tell him again and again.
449 QTreeView * listView = static_cast<QTreeView *>(popup());
450 listView->header()->setStretchLastSection(false);
451 setSectionResizeMode(listView->header(), 0, QHeaderView::Stretch);
452 setSectionResizeMode(listView->header(), 1, QHeaderView::Fixed);
453 listView->header()->resizeSection(1, 22);
460 void GuiCompleter::updateAvailability()
462 // this should really only be of interest if no completion is
463 // visible yet, i.e. especially if automatic completion is disabled.
464 if (inlineVisible() || popupVisible())
466 Cursor const & cur = gui_->bufferView().cursor();
467 if (!popupPossible(cur) && !inlinePossible(cur))
470 updateModel(cur, false, false);
474 void GuiCompleter::updateModel(Cursor const & cur, bool popupUpdate, bool inlineUpdate)
476 // value which should be kept selected
477 QString old = currentCompletion();
478 if (old.length() == 0)
479 old = last_selection_;
483 if (cur.inTexted()) {
484 Paragraph const & par = cur.paragraph();
486 par.getFontSettings(cur.bv().buffer().params(), cur.pos());
487 rtl = font.isVisibleRightToLeft();
489 popup()->setLayoutDirection(rtl ? Qt::RightToLeft : Qt::LeftToRight);
492 CompletionList const * list = cur.inset().createCompletionList(cur);
493 model_->setList(list);
496 setModelSorting(QCompleter::CaseSensitivelySortedModel);
498 setModelSorting(QCompleter::UnsortedModel);
501 QString newPrefix = toqstr(cur.inset().completionPrefix(cur));
502 if (newPrefix != completionPrefix())
503 setCompletionPrefix(newPrefix);
509 // restore old selection
510 setCurrentCompletion(old);
512 // if popup is not empty, the new selection will
513 // be our last valid one
514 if (popupVisible() || inlineVisible()) {
515 QString const & s = currentCompletion();
519 last_selection_ = old;
522 // show inline completion
524 updateInline(cur, currentCompletion());
528 void GuiCompleter::showPopup(Cursor const & cur)
530 if (!popupPossible(cur))
533 updateModel(cur, true, inlineVisible());
537 void GuiCompleter::asyncHidePopup()
540 if (!inlineVisible())
545 void GuiCompleter::showInline(Cursor const & cur)
547 if (!inlinePossible(cur))
550 updateModel(cur, popupVisible(), true);
554 void GuiCompleter::hideInline(Cursor const & cur)
556 gui_->bufferView().setInlineCompletion(cur, DocIterator(cur.buffer()), docstring());
557 inlineVisible_ = false;
559 if (inline_timer_.isActive())
560 inline_timer_.stop();
562 // Trigger asynchronous part of hideInline. We might be
563 // in a dispatcher here and the setModel call might
564 // trigger focus events which is are not healthy here.
565 QTimer::singleShot(0, this, SLOT(asyncHideInline()));
567 // mark that the asynchronous part will reset the model
569 modelActive_ = false;
573 void GuiCompleter::asyncHideInline()
580 void GuiCompleter::showPopup()
582 Cursor cur = gui_->bufferView().cursor();
583 cur.screenUpdateFlags(Update::None);
588 if (cur.result().screenUpdate())
589 gui_->bufferView().processUpdateFlags(cur.result().screenUpdate());
593 void GuiCompleter::showInline()
595 Cursor cur = gui_->bufferView().cursor();
596 cur.screenUpdateFlags(Update::None);
601 if (cur.result().screenUpdate())
602 gui_->bufferView().processUpdateFlags(cur.result().screenUpdate());
606 void GuiCompleter::hidePopup()
608 popupVisible_ = false;
610 if (popup_timer_.isActive())
613 // hide popup asynchronously because we might be here inside of
614 // LFUN dispatchers. Hiding a popup can trigger a focus event on the
615 // workarea which then redisplays the cursor. But the metrics are not
616 // yet up to date such that the coord cache has not all insets yet. The
617 // cursorPos methods would triggers asserts in the coord cache then.
618 QTimer::singleShot(0, this, SLOT(asyncHidePopup()));
620 // mark that the asynchronous part will reset the model
621 if (!inlineVisible())
622 modelActive_ = false;
626 void GuiCompleter::hideInline()
628 Cursor cur = gui_->bufferView().cursor();
629 cur.screenUpdateFlags(Update::None);
634 if (cur.result().screenUpdate())
635 gui_->bufferView().processUpdateFlags(cur.result().screenUpdate());
639 void GuiCompleter::activate()
641 if (!popupVisible() && !inlineVisible())
644 popupActivated(currentCompletion());
648 void GuiCompleter::tab()
650 BufferView * bv = &gui_->bufferView();
651 Cursor cur = bv->cursor();
652 cur.screenUpdateFlags(Update::None);
654 // check that inline completion is active
655 if (!inlineVisible() && !uniqueCompletionAvailable()) {
656 // try to activate the inline completion
657 if (cur.inset().inlineCompletionSupported(cur)) {
660 // show popup without delay because the completion was not unique
661 if (lyxrc.completion_popup_after_complete
663 && popup()->model()->rowCount() > 1)
664 popup_timer_.start(0);
669 if (!popupVisible() && cur.inset().completionSupported(cur)) {
677 // Make undo possible
678 cur.beginUndoGroup();
681 // If completion is active, at least complete by one character
682 docstring prefix = cur.inset().completionPrefix(cur);
683 docstring completion = qstring_to_ucs4(currentCompletion());
684 if (completion.size() <= prefix.size()) {
685 // finalize completion
686 cur.inset().insertCompletion(cur, docstring(), true);
688 // hide popup and inline completion
691 updateVisibility(false, false);
695 docstring nextchar = completion.substr(prefix.size(), 1);
696 if (!cur.inset().insertCompletion(cur, nextchar, false)) {
702 // try to complete as far as it is unique
703 docstring longestCompletion = longestUniqueCompletion();
704 prefix = cur.inset().completionPrefix(cur);
705 docstring postfix = longestCompletion.substr(min(longestCompletion.size(), prefix.size()));
706 cur.inset().insertCompletion(cur, postfix, false);
707 old_cursor_ = bv->cursor();
710 // show popup without delay because the completion was not unique
711 if (lyxrc.completion_popup_after_complete
713 && popup()->model()->rowCount() > 1)
714 popup_timer_.start(0);
717 if (cur.result().screenUpdate())
718 gui_->bufferView().processUpdateFlags(cur.result().screenUpdate());
723 QString GuiCompleter::currentCompletion() const
725 if (!popup()->selectionModel()->hasSelection())
728 // Not sure if this is bug in Qt: currentIndex() always
729 // return the first element in the list.
730 QModelIndex idx = popup()->currentIndex();
731 return popup()->model()->data(idx, Qt::EditRole).toString();
735 void GuiCompleter::setCurrentCompletion(QString const & s)
737 QAbstractItemModel const & model = *popup()->model();
738 size_t n = model.rowCount();
742 // select the first if s is empty
743 if (s.length() == 0) {
745 popup()->setCurrentIndex(model.index(0, 0));
750 // find old selection in model
752 if (modelSorting() == QCompleter::UnsortedModel) {
753 // In unsorted models, iterate through list until the s is found
754 for (i = 0; i < n; ++i) {
756 = model.data(model.index(i, 0), Qt::EditRole).toString();
761 // In sorted models, do binary search for s.
764 while (r >= l && l < int(n)) {
765 size_t mid = (r + l) / 2;
767 = model.data(model.index(mid, 0),
768 Qt::EditRole).toString();
771 // FIXME: is this really the same order that the docstring
772 // from the CompletionList has?
773 int c = s.compare(mids, Qt::CaseSensitive);
781 // middle is not far enough
788 // loop was left without finding anything
793 // we can try to recover
794 LASSERT(i <= n, i = 0);
797 // select the first if none was found
802 popup()->setCurrentIndex(model.index(i, 0));
807 size_t commonPrefix(QString const & s1, QString const & s2)
809 // find common prefix
811 size_t n1 = s1.length();
812 size_t n2 = s2.length();
813 for (j = 0; j < n1 && j < n2; ++j) {
814 if (s1.at(j) != s2.at(j))
821 docstring GuiCompleter::longestUniqueCompletion() const
823 QAbstractItemModel const & model = *popup()->model();
824 size_t n = model.rowCount();
827 QString s = model.data(model.index(0, 0), Qt::EditRole).toString();
829 if (modelSorting() == QCompleter::UnsortedModel) {
830 // For unsorted model we cannot do more than iteration.
831 // Iterate through the completions and cut off where s differs
832 for (size_t i = 0; i < n && s.length() > 0; ++i) {
834 = model.data(model.index(i, 0), Qt::EditRole).toString();
836 s = s.left(commonPrefix(is, s));
839 // For sorted models we can do binary search multiple times,
840 // each time to find the first string which has s not as prefix.
842 while (i < n && s.length() > 0) {
843 // find first string that does not have s as prefix
844 // via binary search in [i,n-1]
847 // get common prefix with the middle string
848 size_t mid = (r + i) / 2;
850 = model.data(model.index(mid, 0),
851 Qt::EditRole).toString();
852 size_t oldLen = s.length();
853 size_t len = commonPrefix(mids, s);
858 // middle is not far enough
861 // middle is maybe too far
864 } while (r - i > 0 && i < n);
868 return qstring_to_ucs4(s);
872 void GuiCompleter::popupActivated(const QString & completion)
874 Cursor cur = gui_->bufferView().cursor();
875 cur.screenUpdateFlags(Update::None);
877 cur.beginUndoGroup();
880 docstring prefix = cur.inset().completionPrefix(cur);
881 docstring postfix = qstring_to_ucs4(completion.mid(prefix.length()));
882 cur.inset().insertCompletion(cur, postfix, true);
886 if (cur.result().screenUpdate())
887 gui_->bufferView().processUpdateFlags(cur.result().screenUpdate());
892 void GuiCompleter::popupHighlighted(const QString & completion)
897 Cursor cur = gui_->bufferView().cursor();
898 cur.screenUpdateFlags(Update::None);
901 updateInline(cur, completion);
903 if (cur.result().screenUpdate())
904 gui_->bufferView().processUpdateFlags(cur.result().screenUpdate());
907 } // namespace frontend
910 #include "moc_GuiCompleter.cpp"