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"
25 #include "support/debug.h"
27 #include <QApplication>
28 #include <QAbstractListModel>
29 #include <QHeaderView>
31 #include <QPixmapCache>
33 #include <QItemDelegate>
38 using namespace lyx::support;
44 class PixmapItemDelegate : public QItemDelegate {
46 explicit PixmapItemDelegate(QObject *parent = 0)
47 : QItemDelegate(parent) {}
50 void paint(QPainter *painter, const QStyleOptionViewItem &option,
51 const QModelIndex &index) const
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();
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,
64 drawFocus(painter, opt, option.rect);
70 class GuiCompletionModel : public QAbstractListModel {
73 GuiCompletionModel(QObject * parent, Inset::CompletionListPtr l)
74 : QAbstractListModel(parent), list(l) {}
76 int columnCount(const QModelIndex & parent = QModelIndex()) const
81 int rowCount(const QModelIndex & parent = QModelIndex()) const
90 QVariant data(const QModelIndex & index, int role) const
95 if (index.row() < 0 || index.row() >= rowCount())
98 if (role != Qt::DisplayRole && role != Qt::EditRole)
101 if (index.column() == 0)
102 return toqstr(list->data(index.row()));
103 else if (index.column() == 1) {
104 // get icon from cache
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);
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);
122 Inset::CompletionListPtr list;
126 GuiCompleter::GuiCompleter(GuiWorkArea * gui, QObject * parent)
127 : QCompleter(parent), gui_(gui)
129 // Setup the completion popup
130 setModel(new GuiCompletionModel(this, Inset::CompletionListPtr()));
131 setCompletionMode(QCompleter::PopupCompletion);
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);
143 popup()->setItemDelegateForColumn(1, new PixmapItemDelegate(popup()));
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()));
159 GuiCompleter::~GuiCompleter()
165 bool GuiCompleter::eventFilter(QObject * watched, QEvent * e)
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);
180 return QCompleter::eventFilter(watched, e);
184 bool GuiCompleter::popupPossible(Cursor const & cur) const
186 return QApplication::activeWindow()
188 && cur.inset().completionSupported(cur);
192 bool GuiCompleter::inlinePossible(Cursor const & cur) const
194 return cur.inset().inlineCompletionSupported(cur);
198 bool GuiCompleter::popupVisible() const
200 return popup()->isVisible();
204 bool GuiCompleter::inlineVisible() const
206 return !gui_->bufferView().inlineCompletionPos().empty();
210 void GuiCompleter::updateVisibility(Cursor & cur, bool start, bool keep, bool cursorInView)
212 // parameters which affect the completion
213 bool moved = cur != old_cursor_;
217 bool possiblePopupState = popupPossible(cur) && cursorInView;
218 bool possibleInlineState = inlinePossible(cur) && cursorInView;
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())
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();
237 // hide old inline completion
238 if (inlineVisible()) {
239 gui_->bufferView().setInlineCompletion(DocIterator(), docstring());
240 cur.updateFlags(Update::Force | Update::SinglePar);
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);
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);
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())
261 void GuiCompleter::updateVisibility(bool start, bool keep)
263 Cursor cur = gui_->bufferView().cursor();
264 updateVisibility(cur, start, keep);
265 if (cur.disp_.update())
266 gui_->bufferView().processUpdateFlags(cur.disp_.update());
270 void GuiCompleter::updatePrefix(Cursor & cur)
272 // get new prefix. Do nothing if unchanged
273 QString newPrefix = toqstr(cur.inset().completionPrefix(cur));
274 if (newPrefix == completionPrefix())
277 // value which should be kept selected
278 QString old = currentCompletion();
279 if (old.length() == 0)
280 old = last_selection_;
282 // update completer to new prefix
283 setCompletionPrefix(newPrefix);
285 // update popup because its size might have changed
289 // restore old selection
290 setCurrentCompletion(old);
292 // if popup is not empty, the new selection will
293 // be our last valid one
294 QString const & s = currentCompletion();
298 last_selection_ = old;
300 // update inline completion because the default
301 // completion string might have changed
303 updateInline(cur, s);
307 void GuiCompleter::updateInline(Cursor & cur, QString const & completion)
309 if (!cur.inset().inlineCompletionSupported(cur))
313 docstring prefix = cur.inset().completionPrefix(cur);
314 docstring postfix = from_utf8(fromqstr(completion.mid(prefix.length())));
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) + "...";
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);
328 void GuiCompleter::updatePopup(Cursor & cur)
330 if (!cur.inset().completionSupported(cur))
333 if (completionCount() == 0)
336 // get dimensions of completion prefix
340 cur.inset().completionPosAndDim(cur, x, y, dim);
341 QRect insetRect = QRect(x, y - dim.ascent() - 3, 200, dim.height() + 6);
345 QTreeView * p = static_cast<QTreeView *>(popup());
346 p->setColumnWidth(0, popup()->width() - 22 - p->verticalScrollBar()->width());
349 updateInline(cur, currentCompletion());
353 void GuiCompleter::updateModel(Cursor & cur, bool popupUpdate, bool inlineUpdate)
355 // value which should be kept selected
356 QString old = currentCompletion();
357 if (old.length() == 0)
358 old = last_selection_;
361 setModel(new GuiCompletionModel(this, cur.inset().completionList(cur)));
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();
376 last_selection_ = old;
378 // show inline completion
380 updateInline(cur, currentCompletion());
384 void GuiCompleter::showPopup(Cursor & cur)
386 if (!popupPossible(cur))
389 updateModel(cur, true, inlineVisible());
394 void GuiCompleter::showInline(Cursor & cur)
396 if (!inlinePossible(cur))
399 updateModel(cur, popupVisible(), true);
404 void GuiCompleter::showPopup()
406 Cursor cur = gui_->bufferView().cursor();
410 if (cur.disp_.update())
411 gui_->bufferView().processUpdateFlags(cur.disp_.update());
415 void GuiCompleter::showInline()
417 Cursor cur = gui_->bufferView().cursor();
421 if (cur.disp_.update())
422 gui_->bufferView().processUpdateFlags(cur.disp_.update());
426 void GuiCompleter::activate()
428 if (!popupVisible() && !inlineVisible())
431 // Complete with current selection in the popup.
432 QString s = currentCompletion();
438 void GuiCompleter::tab()
440 BufferView * bv = &gui_->bufferView();
441 Cursor & cur = bv->cursor();
443 // check that inline completion is active
444 if (!inlineVisible()) {
445 // try to activate the inline completion
446 if (cur.inset().inlineCompletionSupported(cur)) {
451 if (!popupVisible() && cur.inset().completionSupported(cur)) {
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);
466 updateVisibility(false, false);
469 docstring nextchar = completion.substr(prefix.size(), 1);
470 if (!cur.inset().insertCompletion(cur, nextchar, false))
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();
482 // show popup without delay because the completion was not unique
483 if (lyxrc.completion_popup_after_complete
485 && popup()->model()->rowCount() > 1)
486 popup_timer_.start(0);
489 if (cur.disp_.update())
490 gui_->bufferView().processUpdateFlags(cur.disp_.update());
494 QString GuiCompleter::currentCompletion() const
496 if (!popup()->selectionModel()->hasSelection())
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();
506 void GuiCompleter::setCurrentCompletion(QString const & s)
508 QAbstractItemModel const & model = *popup()->model();
509 size_t n = model.rowCount();
513 // select the first if s is empty
514 if (s.length() == 0) {
515 popup()->setCurrentIndex(model.index(0, 0));
519 // iterate through list until the s is found
520 // FIXME: there must be a better way than this iteration
522 for (i = 0; i < n; ++i) {
524 = model.data(model.index(i, 0), Qt::EditRole).toString();
529 // select the first if none was found
533 popup()->setCurrentIndex(model.index(i, 0));
537 docstring GuiCompleter::longestUniqueCompletion() const {
538 QAbstractItemModel const & model = *popup()->model();
539 QString s = currentCompletion();
540 size_t n = model.rowCount();
542 // iterate through the completions and cut off where s differs
543 for (size_t i = 0; i < n && s.length() > 0; ++i) {
545 = model.data(model.index(i, 0), Qt::EditRole).toString();
547 // find common prefix
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))
558 return from_utf8(fromqstr(s));
562 void GuiCompleter::popupActivated(const QString & completion)
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());
574 void GuiCompleter::popupHighlighted(const QString & completion)
576 Cursor cur = gui_->bufferView().cursor();
577 updateInline(cur, completion);
578 if (cur.disp_.update())
579 gui_->bufferView().processUpdateFlags(cur.disp_.update());
582 } // namespace frontend
585 #include "GuiCompleter_moc.cpp"