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 "GuiWorkArea.h"
16 #include "BufferView.h"
18 #include "Dimension.h"
19 #include "FuncRequest.h"
23 #include "Paragraph.h"
26 #include "support/debug.h"
28 #include <QApplication>
29 #include <QHeaderView>
31 #include <QPixmapCache>
33 #include <QItemDelegate>
38 using namespace lyx::support;
43 class RtlItemDelegate : public QItemDelegate {
45 explicit RtlItemDelegate(QObject * parent = 0)
46 : QItemDelegate(parent), enabled_(false) {}
48 void setEnabled(bool enabled = true)
54 virtual void drawDisplay(QPainter * painter,
55 QStyleOptionViewItem const & option,
56 QRect const & rect, QString const & text) const
59 QItemDelegate::drawDisplay(painter, option, rect, text);
63 // FIXME: do this more elegantly
64 docstring stltext = qstring_to_ucs4(text);
65 reverse(stltext.begin(), stltext.end());
66 QItemDelegate::drawDisplay(painter, option, rect, toqstr(stltext));
74 class PixmapItemDelegate : public QItemDelegate {
76 explicit PixmapItemDelegate(QObject *parent = 0)
77 : QItemDelegate(parent) {}
80 void paint(QPainter *painter, const QStyleOptionViewItem &option,
81 const QModelIndex &index) const
83 QStyleOptionViewItem opt = setOptions(index, option);
84 QVariant value = index.data(Qt::DisplayRole);
85 QPixmap pixmap = qvariant_cast<QPixmap>(value);
89 drawBackground(painter, opt, index);
90 if (!pixmap.isNull()) {
91 const QSize size = pixmap.size();
92 painter->drawPixmap(option.rect.left() + (16 - size.width()) / 2,
93 option.rect.top() + (option.rect.height() - size.height()) / 2,
96 drawFocus(painter, opt, option.rect);
102 class GuiCompletionModel : public QAbstractListModel {
105 GuiCompletionModel(QObject * parent, Inset::CompletionList const * l)
106 : QAbstractListModel(parent), list_(l) {}
108 ~GuiCompletionModel()
114 return list_->sorted();
119 int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const
124 int rowCount(const QModelIndex & /*parent*/ = QModelIndex()) const
129 return list_->size();
133 QVariant data(const QModelIndex & index, int role) const
138 if (index.row() < 0 || index.row() >= rowCount())
141 if (role != Qt::DisplayRole && role != Qt::EditRole)
144 if (index.column() == 0)
145 return toqstr(list_->data(index.row()));
146 else if (index.column() == 1) {
147 // get icon from cache
149 QString const name = ":" + toqstr(list_->icon(index.row()));
150 if (!QPixmapCache::find("completion" + name, scaled)) {
151 // load icon from disk
152 QPixmap p = QPixmap(name);
154 // scale it to 16x16 or smaller
155 scaled = p.scaled(min(16, p.width()), min(16, p.height()),
156 Qt::KeepAspectRatio, Qt::SmoothTransformation);
159 QPixmapCache::insert("completion" + name, scaled);
168 Inset::CompletionList const * list_;
172 GuiCompleter::GuiCompleter(GuiWorkArea * gui, QObject * parent)
173 : QCompleter(parent), gui_(gui), updateLock_(0),
174 inlineVisible_(false), popupVisible_(false),
177 // Setup the completion popup
178 setModel(new GuiCompletionModel(this, 0));
179 setCompletionMode(QCompleter::PopupCompletion);
183 QTreeView *listView = new QTreeView;
184 listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
185 listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
186 listView->setSelectionBehavior(QAbstractItemView::SelectRows);
187 listView->setSelectionMode(QAbstractItemView::SingleSelection);
188 listView->header()->hide();
189 listView->setIndentation(0);
190 listView->setUniformRowHeights(true);
193 rtlItemDelegate_ = new RtlItemDelegate(this);
194 popup()->setItemDelegateForColumn(0, rtlItemDelegate_);
195 popup()->setItemDelegateForColumn(1, new PixmapItemDelegate(this));
197 // create timeout timers
198 popup_timer_.setSingleShot(true);
199 inline_timer_.setSingleShot(true);
200 connect(this, SIGNAL(highlighted(const QString &)),
201 this, SLOT(popupHighlighted(const QString &)));
202 connect(this, SIGNAL(activated(const QString &)),
203 this, SLOT(popupActivated(const QString &)));
204 connect(&popup_timer_, SIGNAL(timeout()),
205 this, SLOT(showPopup()));
206 connect(&inline_timer_, SIGNAL(timeout()),
207 this, SLOT(showInline()));
211 GuiCompleter::~GuiCompleter()
217 bool GuiCompleter::eventFilter(QObject * watched, QEvent * e)
219 // hijack back the tab key from the popup
220 // (which stole it from the workspace before)
221 if (e->type() == QEvent::KeyPress && popupVisible()) {
222 QKeyEvent *ke = static_cast<QKeyEvent *>(e);
232 return QCompleter::eventFilter(watched, e);
236 bool GuiCompleter::popupPossible(Cursor const & cur) const
238 return QApplication::activeWindow()
240 && cur.inset().completionSupported(cur);
244 bool GuiCompleter::inlinePossible(Cursor const & cur) const
246 return cur.inset().inlineCompletionSupported(cur);
250 bool GuiCompleter::completionAvailable() const
255 size_t n = popup()->model()->rowCount();
257 // if there is exactly one, we have to check whether it is a
258 // real completion, i.e. longer than the current prefix.
259 if (n == 1 && completionPrefix() == currentCompletion())
266 bool GuiCompleter::popupVisible() const
268 return popupVisible_;
272 bool GuiCompleter::inlineVisible() const
274 // In fact using BufferView::inlineCompletionPos.empty() should be
275 // here. But unfortunately this information is not good enough
276 // because destructive operations like backspace might invalidate
277 // inlineCompletionPos. But then the completion should stay visible
278 // (i.e. reshown on the next update). Hence be keep this information
279 // in the inlineVisible_ variable.
280 return inlineVisible_;
284 void GuiCompleter::updateVisibility(Cursor & cur, bool start, bool keep, bool cursorInView)
286 // parameters which affect the completion
287 bool moved = cur != old_cursor_;
291 bool possiblePopupState = popupPossible(cur) && cursorInView;
292 bool possibleInlineState = inlinePossible(cur) && cursorInView;
294 // we moved or popup state is not ok for popup?
295 if ((moved && !keep) || !possiblePopupState)
298 // we moved or inline state is not ok for inline completion?
299 if ((moved && !keep) || !possibleInlineState)
302 // we inserted something and are in a possible popup state?
303 if (!popupVisible() && possiblePopupState && start
304 && cur.inset().automaticPopupCompletion())
305 popup_timer_.start(int(lyxrc.completion_popup_delay * 1000));
307 // we inserted something and are in a possible inline completion state?
308 if (!inlineVisible() && possibleInlineState && start
309 && cur.inset().automaticInlineCompletion())
310 inline_timer_.start(int(lyxrc.completion_inline_delay * 1000));
312 // update prefix if any completion is possible
313 bool modelActive = modelActive_ && model()->rowCount() > 0;
314 if (possiblePopupState || possibleInlineState) {
318 updateAvailability();
323 void GuiCompleter::updateVisibility(bool start, bool keep)
325 Cursor cur = gui_->bufferView().cursor();
326 cur.updateFlags(Update::None);
328 updateVisibility(cur, start, keep);
330 if (cur.disp_.update())
331 gui_->bufferView().processUpdateFlags(cur.disp_.update());
335 void GuiCompleter::updatePrefix(Cursor & cur)
337 // get new prefix. Do nothing if unchanged
338 QString newPrefix = toqstr(cur.inset().completionPrefix(cur));
339 if (newPrefix == completionPrefix())
342 // value which should be kept selected
343 QString old = currentCompletion();
344 if (old.length() == 0)
345 old = last_selection_;
347 // update completer to new prefix
348 setCompletionPrefix(newPrefix);
350 // update popup because its size might have changed
354 // restore old selection
355 setCurrentCompletion(old);
357 // if popup is not empty, the new selection will
358 // be our last valid one
359 QString const & s = currentCompletion();
360 if (popupVisible() || inlineVisible()) {
364 last_selection_ = old;
367 // update inline completion because the default
368 // completion string might have changed
370 updateInline(cur, s);
374 void GuiCompleter::updateInline(Cursor & cur, QString const & completion)
376 if (!cur.inset().inlineCompletionSupported(cur))
380 docstring prefix = cur.inset().completionPrefix(cur);
381 docstring postfix = from_utf8(fromqstr(completion.mid(prefix.length())));
383 // shorten it if necessary
384 if (lyxrc.completion_inline_dots != -1
385 && postfix.size() > unsigned(lyxrc.completion_inline_dots))
386 postfix = postfix.substr(0, lyxrc.completion_inline_dots - 1) + "...";
388 // set inline completion at cursor position
389 size_t uniqueTo = max(longestUniqueCompletion().size(), prefix.size());
390 gui_->bufferView().setInlineCompletion(cur, cur, postfix, uniqueTo - prefix.size());
391 inlineVisible_ = true;
395 void GuiCompleter::updatePopup(Cursor & cur)
397 if (!cur.inset().completionSupported(cur))
400 popupVisible_ = true;
402 if (completionCount() == 0) {
403 QTimer::singleShot(0, popup(), SLOT(hide()));
407 QTimer::singleShot(0, this, SLOT(asyncUpdatePopup()));
411 void GuiCompleter::asyncUpdatePopup()
413 Cursor cur = gui_->bufferView().cursor();
414 if (!cur.inset().completionSupported(cur)) {
415 popupVisible_ = false;
419 // get dimensions of completion prefix
423 cur.inset().completionPosAndDim(cur, x, y, dim);
425 // and calculate the rect of the popup
427 if (popup()->layoutDirection() == Qt::RightToLeft)
428 rect = QRect(x + dim.width() - 200, y - dim.ascent() - 3, 200, dim.height() + 6);
430 rect = QRect(x, y - dim.ascent() - 3, 200, dim.height() + 6);
432 // Resize the columns in the popup.
433 // This should really be in the constructor. But somehow the treeview
434 // has a bad memory about it and we have to tell him again and again.
435 QTreeView * listView = static_cast<QTreeView *>(popup());
436 listView->header()->setStretchLastSection(false);
437 listView->header()->setResizeMode(0, QHeaderView::Stretch);
438 listView->header()->setResizeMode(1, QHeaderView::Fixed);
439 listView->header()->resizeSection(1, 22);
446 void GuiCompleter::updateAvailability()
448 // this should really only be of interest if no completion is
449 // visible yet, i.e. especially if automatic completion is disabled.
450 if (inlineVisible() || popupVisible())
452 Cursor & cur = gui_->bufferView().cursor();
453 if (!popupPossible(cur) && !inlinePossible(cur))
456 updateModel(cur, false, false);
460 void GuiCompleter::updateModel(Cursor & cur, bool popupUpdate, bool inlineUpdate)
462 // value which should be kept selected
463 QString old = currentCompletion();
464 if (old.length() == 0)
465 old = last_selection_;
469 if (cur.inTexted()) {
470 Paragraph const & par = cur.paragraph();
472 par.getFontSettings(cur.bv().buffer().params(), cur.pos());
473 rtl = font.isVisibleRightToLeft();
475 popup()->setLayoutDirection(rtl ? Qt::RightToLeft : Qt::LeftToRight);
477 // turn the direction of the strings in the popup.
478 // Qt does not do that itself.
479 rtlItemDelegate_->setEnabled(rtl);
482 Inset::CompletionList const * list = cur.inset().createCompletionList(cur);
483 setModel(new GuiCompletionModel(this, list));
486 setModelSorting(QCompleter::CaseSensitivelySortedModel);
488 setModelSorting(QCompleter::UnsortedModel);
491 QString newPrefix = toqstr(cur.inset().completionPrefix(cur));
492 if (newPrefix != completionPrefix())
493 setCompletionPrefix(newPrefix);
499 // restore old selection
500 setCurrentCompletion(old);
502 // if popup is not empty, the new selection will
503 // be our last valid one
504 if (popupVisible() || inlineVisible()) {
505 QString const & s = currentCompletion();
509 last_selection_ = old;
512 // show inline completion
514 updateInline(cur, currentCompletion());
518 void GuiCompleter::showPopup(Cursor & cur)
520 if (!popupPossible(cur))
523 updateModel(cur, true, inlineVisible());
527 void GuiCompleter::hidePopup(Cursor & cur)
529 popupVisible_ = false;
531 if (popup_timer_.isActive())
534 // hide popup asynchronously because we might be here inside of
535 // LFUN dispatchers. Hiding a popup can trigger a focus event on the
536 // workarea which then redisplays the cursor. But the metrics are not
537 // yet up to date such that the coord cache has not all insets yet. The
538 // cursorPos methods would triggers asserts in the coord cache then.
539 QTimer::singleShot(0, this, SLOT(asyncHidePopup()));
541 // mark that the asynchronous part will reset the model
542 if (!inlineVisible())
543 modelActive_ = false;
547 void GuiCompleter::asyncHidePopup()
550 if (!inlineVisible())
551 setModel(new GuiCompletionModel(this, 0));
555 void GuiCompleter::showInline(Cursor & cur)
557 if (!inlinePossible(cur))
560 updateModel(cur, popupVisible(), true);
564 void GuiCompleter::hideInline(Cursor & cur)
566 gui_->bufferView().setInlineCompletion(cur, DocIterator(), docstring());
567 inlineVisible_ = false;
569 if (inline_timer_.isActive())
570 inline_timer_.stop();
572 // Trigger asynchronous part of hideInline. We might be
573 // in a dispatcher here and the setModel call might
574 // trigger focus events which is are not healthy here.
575 QTimer::singleShot(0, this, SLOT(asyncHideInline()));
577 // mark that the asynchronous part will reset the model
579 modelActive_ = false;
583 void GuiCompleter::asyncHideInline()
586 setModel(new GuiCompletionModel(this, 0));
590 void GuiCompleter::showPopup()
592 Cursor cur = gui_->bufferView().cursor();
593 cur.updateFlags(Update::None);
598 if (cur.disp_.update())
599 gui_->bufferView().processUpdateFlags(cur.disp_.update());
603 void GuiCompleter::showInline()
605 Cursor cur = gui_->bufferView().cursor();
606 cur.updateFlags(Update::None);
611 if (cur.disp_.update())
612 gui_->bufferView().processUpdateFlags(cur.disp_.update());
616 void GuiCompleter::hidePopup()
618 Cursor cur = gui_->bufferView().cursor();
619 cur.updateFlags(Update::None);
624 if (cur.disp_.update())
625 gui_->bufferView().processUpdateFlags(cur.disp_.update());
629 void GuiCompleter::hideInline()
631 Cursor cur = gui_->bufferView().cursor();
632 cur.updateFlags(Update::None);
637 if (cur.disp_.update())
638 gui_->bufferView().processUpdateFlags(cur.disp_.update());
642 void GuiCompleter::activate()
644 if (!popupVisible() && !inlineVisible())
647 popupActivated(currentCompletion());
651 void GuiCompleter::tab()
653 BufferView * bv = &gui_->bufferView();
654 Cursor cur = bv->cursor();
655 cur.updateFlags(Update::None);
657 // check that inline completion is active
658 if (!inlineVisible()) {
659 // try to activate the inline completion
660 if (cur.inset().inlineCompletionSupported(cur)) {
663 // show popup without delay because the completion was not unique
664 if (lyxrc.completion_popup_after_complete
666 && popup()->model()->rowCount() > 1)
667 popup_timer_.start(0);
672 if (!popupVisible() && cur.inset().completionSupported(cur)) {
680 // If completion is active, at least complete by one character
681 docstring prefix = cur.inset().completionPrefix(cur);
682 docstring completion = from_utf8(fromqstr(currentCompletion()));
683 if (completion.size() <= prefix.size()) {
684 // finalize completion
685 cur.inset().insertCompletion(cur, docstring(), true);
687 // hide popup and inline completion
690 updateVisibility(false, false);
693 docstring nextchar = completion.substr(prefix.size(), 1);
694 if (!cur.inset().insertCompletion(cur, nextchar, false))
698 // try to complete as far as it is unique
699 docstring longestCompletion = longestUniqueCompletion();
700 prefix = cur.inset().completionPrefix(cur);
701 docstring postfix = longestCompletion.substr(min(longestCompletion.size(), prefix.size()));
702 cur.inset().insertCompletion(cur, postfix, false);
703 old_cursor_ = bv->cursor();
706 // show popup without delay because the completion was not unique
707 if (lyxrc.completion_popup_after_complete
709 && popup()->model()->rowCount() > 1)
710 popup_timer_.start(0);
713 if (cur.disp_.update())
714 gui_->bufferView().processUpdateFlags(cur.disp_.update());
718 QString GuiCompleter::currentCompletion() const
720 if (!popup()->selectionModel()->hasSelection())
723 // Not sure if this is bug in Qt: currentIndex() always
724 // return the first element in the list.
725 QModelIndex idx = popup()->currentIndex();
726 return popup()->model()->data(idx, Qt::EditRole).toString();
730 void GuiCompleter::setCurrentCompletion(QString const & s)
732 QAbstractItemModel const & model = *popup()->model();
733 size_t n = model.rowCount();
737 // select the first if s is empty
738 if (s.length() == 0) {
740 popup()->setCurrentIndex(model.index(0, 0));
745 // find old selection in model
747 if (modelSorting() == QCompleter::UnsortedModel) {
748 // In unsorted models, iterate through list until the s is found
749 for (i = 0; i < n; ++i) {
751 = model.data(model.index(i, 0), Qt::EditRole).toString();
756 // In sorted models, do binary search for s.
759 while (r >= l && l < int(n)) {
760 size_t mid = (r + l) / 2;
762 = model.data(model.index(mid, 0),
763 Qt::EditRole).toString();
766 // FIXME: is this really the same order that the docstring
767 // from the CompletionList has?
768 int c = s.compare(mids, Qt::CaseSensitive);
776 // middle is not far enough
783 // loop was left without finding anything
788 BOOST_ASSERT(0 <= i && i <= n);
791 // select the first if none was found
796 popup()->setCurrentIndex(model.index(i, 0));
801 size_t commonPrefix(QString const & s1, QString const & s2)
803 // find common prefix
805 size_t n1 = s1.length();
806 size_t n2 = s2.length();
807 for (j = 0; j < n1 && j < n2; ++j) {
808 if (s1.at(j) != s2.at(j))
815 docstring GuiCompleter::longestUniqueCompletion() const
817 QAbstractItemModel const & model = *popup()->model();
818 size_t n = model.rowCount();
821 QString s = model.data(model.index(0, 0), Qt::EditRole).toString();
823 if (modelSorting() == QCompleter::UnsortedModel) {
824 // For unsorted model we cannot do more than iteration.
825 // Iterate through the completions and cut off where s differs
826 for (size_t i = 0; i < n && s.length() > 0; ++i) {
828 = model.data(model.index(i, 0), Qt::EditRole).toString();
830 s = s.left(commonPrefix(is, s));
833 // For sorted models we can do binary search multiple times,
834 // each time to find the first string which has s not as prefix.
836 while (i < n && s.length() > 0) {
837 // find first string that does not have s as prefix
838 // via binary search in [i,n-1]
841 // get common prefix with the middle string
842 size_t mid = (r + i) / 2;
844 = model.data(model.index(mid, 0),
845 Qt::EditRole).toString();
846 size_t oldLen = s.length();
847 size_t len = commonPrefix(mids, s);
852 // middle is not far enough
855 // middle is maybe too far
858 } while (r - i > 0 && i < n);
862 return from_utf8(fromqstr(s));
866 void GuiCompleter::popupActivated(const QString & completion)
868 Cursor cur = gui_->bufferView().cursor();
869 cur.updateFlags(Update::None);
871 docstring prefix = cur.inset().completionPrefix(cur);
872 docstring postfix = from_utf8(fromqstr(completion.mid(prefix.length())));
873 cur.inset().insertCompletion(cur, postfix, true);
877 if (cur.disp_.update())
878 gui_->bufferView().processUpdateFlags(cur.disp_.update());
882 void GuiCompleter::popupHighlighted(const QString & completion)
887 Cursor cur = gui_->bufferView().cursor();
888 cur.updateFlags(Update::None);
891 updateInline(cur, completion);
893 if (cur.disp_.update())
894 gui_->bufferView().processUpdateFlags(cur.disp_.update());
897 } // namespace frontend
900 #include "GuiCompleter_moc.cpp"