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 <QAbstractListModel>
30 #include <QHeaderView>
32 #include <QPixmapCache>
34 #include <QItemDelegate>
39 using namespace lyx::support;
44 class RtlItemDelegate : public QItemDelegate {
46 explicit RtlItemDelegate(QObject * parent = 0)
47 : QItemDelegate(parent) {}
50 virtual void drawDisplay(QPainter * painter,
51 QStyleOptionViewItem const & option,
52 QRect const & rect, QString const & text) const
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));
61 RtlItemDelegate rtlItemDelegate;
64 class PixmapItemDelegate : public QItemDelegate {
66 explicit PixmapItemDelegate(QObject *parent = 0)
67 : QItemDelegate(parent) {}
70 void paint(QPainter *painter, const QStyleOptionViewItem &option,
71 const QModelIndex &index) const
73 QStyleOptionViewItem opt = setOptions(index, option);
74 QVariant value = index.data(Qt::DisplayRole);
75 QPixmap pixmap = qvariant_cast<QPixmap>(value);
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,
86 drawFocus(painter, opt, option.rect);
92 class GuiCompletionModel : public QAbstractListModel {
95 GuiCompletionModel(QObject * parent,
96 Inset::CompletionList const * l)
97 : QAbstractListModel(parent), list_(l) {}
102 int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const
107 int rowCount(const QModelIndex & /*parent*/ = QModelIndex()) const
112 return list_->size();
116 QVariant data(const QModelIndex & index, int role) const
121 if (index.row() < 0 || index.row() >= rowCount())
124 if (role != Qt::DisplayRole && role != Qt::EditRole)
127 if (index.column() == 0)
128 return toqstr(list_->data(index.row()));
129 else if (index.column() == 1) {
130 // get icon from cache
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);
137 // scale it to 16x16 or smaller
139 = p.scaled(min(16, p.width()), min(16, p.height()),
140 Qt::KeepAspectRatio, Qt::SmoothTransformation);
143 QPixmapCache::insert("completion" + name, scaled);
152 Inset::CompletionList const * list_;
156 GuiCompleter::GuiCompleter(GuiWorkArea * gui, QObject * parent)
157 : QCompleter(parent), gui_(gui), updateLock_(0),
158 inlineVisible_(false)
160 // Setup the completion popup
161 setModel(new GuiCompletionModel(this, 0));
162 setCompletionMode(QCompleter::PopupCompletion);
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);
174 popup()->setItemDelegateForColumn(1, new PixmapItemDelegate(this));
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()));
190 GuiCompleter::~GuiCompleter()
196 bool GuiCompleter::eventFilter(QObject * watched, QEvent * e)
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);
211 return QCompleter::eventFilter(watched, e);
215 bool GuiCompleter::popupPossible(Cursor const & cur) const
217 return QApplication::activeWindow()
219 && cur.inset().completionSupported(cur);
223 bool GuiCompleter::inlinePossible(Cursor const & cur) const
225 return cur.inset().inlineCompletionSupported(cur);
229 bool GuiCompleter::popupVisible() const
231 return popup()->isVisible();
235 bool GuiCompleter::inlineVisible() const
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_;
247 void GuiCompleter::updateVisibility(Cursor & cur, bool start, bool keep, bool cursorInView)
249 // parameters which affect the completion
250 bool moved = cur != old_cursor_;
254 bool possiblePopupState = popupPossible(cur) && cursorInView;
255 bool possibleInlineState = inlinePossible(cur) && cursorInView;
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())
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();
274 // hide old inline completion
275 if (inlineVisible()) {
276 gui_->bufferView().setInlineCompletion(cur, DocIterator(), docstring());
277 inlineVisible_ = false;
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));
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));
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())
298 void GuiCompleter::updateVisibility(bool start, bool keep)
300 Cursor cur = gui_->bufferView().cursor();
301 cur.updateFlags(Update::None);
303 updateVisibility(cur, start, keep);
305 if (cur.disp_.update())
306 gui_->bufferView().processUpdateFlags(cur.disp_.update());
310 void GuiCompleter::updatePrefix(Cursor & cur)
312 // get new prefix. Do nothing if unchanged
313 QString newPrefix = toqstr(cur.inset().completionPrefix(cur));
314 if (newPrefix == completionPrefix())
317 // value which should be kept selected
318 QString old = currentCompletion();
319 if (old.length() == 0)
320 old = last_selection_;
322 // update completer to new prefix
323 setCompletionPrefix(newPrefix);
325 // update popup because its size might have changed
329 // restore old selection
330 setCurrentCompletion(old);
332 // if popup is not empty, the new selection will
333 // be our last valid one
334 QString const & s = currentCompletion();
338 last_selection_ = old;
340 // update inline completion because the default
341 // completion string might have changed
343 updateInline(cur, s);
347 void GuiCompleter::updateInline(Cursor & cur, QString const & completion)
349 if (!cur.inset().inlineCompletionSupported(cur))
353 docstring prefix = cur.inset().completionPrefix(cur);
354 docstring postfix = from_utf8(fromqstr(completion.mid(prefix.length())));
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) + "...";
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;
368 void GuiCompleter::updatePopup(Cursor & cur)
370 if (!cur.inset().completionSupported(cur))
373 if (completionCount() == 0)
376 // get dimensions of completion prefix
380 cur.inset().completionPosAndDim(cur, x, y, dim);
382 // and calculate the rect of the popup
384 if (popup()->layoutDirection() == Qt::RightToLeft)
385 rect = QRect(x + dim.width() - 200, y - dim.ascent() - 3, 200, dim.height() + 6);
387 rect = QRect(x, y - dim.ascent() - 3, 200, dim.height() + 6);
391 QTreeView * p = static_cast<QTreeView *>(popup());
392 p->setColumnWidth(0, popup()->width() - 22 - p->verticalScrollBar()->width());
396 void GuiCompleter::updateModel(Cursor & cur, bool popupUpdate, bool inlineUpdate)
398 // value which should be kept selected
399 QString old = currentCompletion();
400 if (old.length() == 0)
401 old = last_selection_;
405 if (cur.inTexted()) {
406 Paragraph const & par = cur.paragraph();
408 par.getFontSettings(cur.bv().buffer().params(), cur.pos());
409 rtl = font.isVisibleRightToLeft();
411 popup()->setLayoutDirection(rtl ? Qt::RightToLeft : Qt::LeftToRight);
413 // turn the direction of the strings in the popup.
414 // Qt does not do that itself.
415 popup()->setItemDelegateForColumn(0, rtl ? &rtlItemDelegate : 0);
418 Inset::CompletionList const * list
419 = cur.inset().createCompletionList(cur);
420 setModel(new GuiCompletionModel(this, list));
426 // restore old selection
427 setCurrentCompletion(old);
429 // if popup is not empty, the new selection will
430 // be our last valid one
431 QString const & s = currentCompletion();
435 last_selection_ = old;
437 // show inline completion
439 updateInline(cur, currentCompletion());
443 void GuiCompleter::showPopup(Cursor & cur)
445 if (!popupPossible(cur))
448 updateModel(cur, true, inlineVisible());
453 void GuiCompleter::showInline(Cursor & cur)
455 if (!inlinePossible(cur))
458 updateModel(cur, popupVisible(), true);
463 void GuiCompleter::showPopup()
465 Cursor cur = gui_->bufferView().cursor();
466 cur.updateFlags(Update::None);
471 if (cur.disp_.update())
472 gui_->bufferView().processUpdateFlags(cur.disp_.update());
476 void GuiCompleter::showInline()
478 Cursor cur = gui_->bufferView().cursor();
479 cur.updateFlags(Update::None);
484 if (cur.disp_.update())
485 gui_->bufferView().processUpdateFlags(cur.disp_.update());
489 void GuiCompleter::activate()
491 if (!popupVisible() && !inlineVisible())
494 // Complete with current selection in the popup.
495 QString s = currentCompletion();
501 void GuiCompleter::tab()
503 BufferView * bv = &gui_->bufferView();
504 Cursor cur = bv->cursor();
505 cur.updateFlags(Update::None);
507 // check that inline completion is active
508 if (!inlineVisible()) {
509 // try to activate the inline completion
510 if (cur.inset().inlineCompletionSupported(cur)) {
513 // show popup without delay because the completion was not unique
514 if (lyxrc.completion_popup_after_complete
516 && popup()->model()->rowCount() > 1)
517 popup_timer_.start(0);
522 if (!popupVisible() && cur.inset().completionSupported(cur)) {
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);
537 // hide popup and inline completion
539 gui_->bufferView().setInlineCompletion(cur, DocIterator(), docstring());
540 inlineVisible_ = false;
541 updateVisibility(false, false);
544 docstring nextchar = completion.substr(prefix.size(), 1);
545 if (!cur.inset().insertCompletion(cur, nextchar, false))
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();
557 // show popup without delay because the completion was not unique
558 if (lyxrc.completion_popup_after_complete
560 && popup()->model()->rowCount() > 1)
561 popup_timer_.start(0);
564 if (cur.disp_.update())
565 gui_->bufferView().processUpdateFlags(cur.disp_.update());
569 QString GuiCompleter::currentCompletion() const
571 if (!popup()->selectionModel()->hasSelection())
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();
581 void GuiCompleter::setCurrentCompletion(QString const & s)
583 QAbstractItemModel const & model = *popup()->model();
584 size_t n = model.rowCount();
588 // select the first if s is empty
589 if (s.length() == 0) {
591 popup()->setCurrentIndex(model.index(0, 0));
596 // iterate through list until the s is found
597 // FIXME: there must be a better way than this iteration
599 for (i = 0; i < n; ++i) {
601 = model.data(model.index(i, 0), Qt::EditRole).toString();
606 // select the first if none was found
611 popup()->setCurrentIndex(model.index(i, 0));
616 docstring GuiCompleter::longestUniqueCompletion() const {
617 QAbstractItemModel const & model = *popup()->model();
618 QString s = currentCompletion();
619 size_t n = model.rowCount();
621 // iterate through the completions and cut off where s differs
622 for (size_t i = 0; i < n && s.length() > 0; ++i) {
624 = model.data(model.index(i, 0), Qt::EditRole).toString();
626 // find common prefix
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))
637 return from_utf8(fromqstr(s));
641 void GuiCompleter::popupActivated(const QString & completion)
643 Cursor cur = gui_->bufferView().cursor();
644 cur.updateFlags(Update::None);
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);
651 if (cur.disp_.update())
652 gui_->bufferView().processUpdateFlags(cur.disp_.update());
656 void GuiCompleter::popupHighlighted(const QString & completion)
661 Cursor cur = gui_->bufferView().cursor();
662 cur.updateFlags(Update::None);
664 updateInline(cur, completion);
666 if (cur.disp_.update())
667 gui_->bufferView().processUpdateFlags(cur.disp_.update());
670 } // namespace frontend
673 #include "GuiCompleter_moc.cpp"