X-Git-Url: https://git.lyx.org/gitweb/?a=blobdiff_plain;f=src%2Ffrontends%2Fqt4%2FGuiCompleter.cpp;h=4b592dcad72961e8f3ed0c4de1aa19f45298442b;hb=dba608e1d7ce6220bc20470899927a461ee2394c;hp=fff172ff2c4a474d4b669819d07bd184d328385f;hpb=1bf7b59d60a30aed7d418cb63e9f12610635f7e6;p=lyx.git diff --git a/src/frontends/qt4/GuiCompleter.cpp b/src/frontends/qt4/GuiCompleter.cpp index fff172ff2c..4b592dcad7 100644 --- a/src/frontends/qt4/GuiCompleter.cpp +++ b/src/frontends/qt4/GuiCompleter.cpp @@ -10,22 +10,25 @@ #include -#include "GuiWorkArea.h" +#include "GuiCompleter.h" #include "Buffer.h" #include "BufferView.h" +#include "CompletionList.h" #include "Cursor.h" #include "Dimension.h" #include "FuncRequest.h" +#include "GuiWorkArea.h" #include "GuiView.h" #include "LyXFunc.h" #include "LyXRC.h" +#include "Paragraph.h" #include "version.h" +#include "support/lassert.h" #include "support/debug.h" #include -#include #include #include #include @@ -40,56 +43,89 @@ using namespace lyx::support; namespace lyx { namespace frontend { - -class PixmapItemDelegate : public QItemDelegate { +class CompleterItemDelegate : public QItemDelegate +{ public: - explicit PixmapItemDelegate(QObject *parent = 0) - : QItemDelegate(parent) {} + explicit CompleterItemDelegate(QObject * parent) + : QItemDelegate(parent) + {} + + ~CompleterItemDelegate() + {} protected: + void drawDisplay(QPainter * painter, + QStyleOptionViewItem const & option, + QRect const & rect, QString const & text) const + { + QItemDelegate::drawDisplay(painter, option, rect, text); + } + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { - QStyleOptionViewItemV3 opt = setOptions(index, option); + if (index.column() == 0) { + QItemDelegate::paint(painter, option, index); + return; + } + QStyleOptionViewItem opt = setOptions(index, option); QVariant value = index.data(Qt::DisplayRole); QPixmap pixmap = qvariant_cast(value); - const QSize size = pixmap.size(); - + // draw painter->save(); drawBackground(painter, opt, index); - painter->drawPixmap(option.rect.left() + (16 - size.width()) / 2, - option.rect.top() + (option.rect.height() - size.height()) / 2, - pixmap); + if (!pixmap.isNull()) { + const QSize size = pixmap.size(); + painter->drawPixmap(option.rect.left() + (16 - size.width()) / 2, + option.rect.top() + (option.rect.height() - size.height()) / 2, + pixmap); + } drawFocus(painter, opt, option.rect); painter->restore(); } }; - -class GuiCompletionModel : public QAbstractListModel { +class GuiCompletionModel : public QAbstractListModel +{ public: /// - GuiCompletionModel(QObject * parent, Inset::CompletionListPtr l) - : QAbstractListModel(parent), list(l) {} + GuiCompletionModel(QObject * parent, CompletionList const * l) + : QAbstractListModel(parent), list_(l) + {} + /// + ~GuiCompletionModel() { delete list_; } + /// + void setList(CompletionList const * l) { + delete list_; + list_ = l; + reset(); + } + /// + bool sorted() const + { + if (list_) + return list_->sorted(); + return false; + } /// - int columnCount(const QModelIndex & parent = QModelIndex()) const + int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const { return 2; } /// - int rowCount(const QModelIndex & parent = QModelIndex()) const + int rowCount(const QModelIndex & /*parent*/ = QModelIndex()) const { - if (list.get() == 0) + if (list_ == 0) return 0; else - return list->size(); + return list_->size(); } /// QVariant data(const QModelIndex & index, int role) const { - if (list.get() == 0) + if (list_ == 0) return QVariant(); if (index.row() < 0 || index.row() >= rowCount()) @@ -99,18 +135,21 @@ public: return QVariant(); if (index.column() == 0) - return toqstr(list->data(index.row())); - else if (index.column() == 1) { + return toqstr(list_->data(index.row())); + + if (index.column() == 1) { // get icon from cache QPixmap scaled; - QString const name = ":" + toqstr(list->icon(index.row())); + QString const name = ":" + toqstr(list_->icon(index.row())); if (!QPixmapCache::find("completion" + name, scaled)) { // load icon from disk QPixmap p = QPixmap(name); + if (!p.isNull()) { + // scale it to 16x16 or smaller + scaled = p.scaled(min(16, p.width()), min(16, p.height()), + Qt::KeepAspectRatio, Qt::SmoothTransformation); + } - // scale it to 16x16 or smaller - scaled = p.scaled(min(16, p.width()), min(16, p.height()), - Qt::KeepAspectRatio, Qt::SmoothTransformation); QPixmapCache::insert("completion" + name, scaled); } return scaled; @@ -119,15 +158,19 @@ public: } private: - Inset::CompletionListPtr list; + /// owned by us + CompletionList const * list_; }; GuiCompleter::GuiCompleter(GuiWorkArea * gui, QObject * parent) - : QCompleter(parent), gui_(gui) + : QCompleter(parent), gui_(gui), updateLock_(0), + inlineVisible_(false), popupVisible_(false), + modelActive_(false) { // Setup the completion popup - setModel(new GuiCompletionModel(this, Inset::CompletionListPtr())); + model_ = new GuiCompletionModel(this, 0); + setModel(model_); setCompletionMode(QCompleter::PopupCompletion); setWidget(gui_); @@ -139,8 +182,11 @@ GuiCompleter::GuiCompleter(GuiWorkArea * gui, QObject * parent) listView->setSelectionMode(QAbstractItemView::SingleSelection); listView->header()->hide(); listView->setIndentation(0); + listView->setUniformRowHeights(true); setPopup(listView); - popup()->setItemDelegateForColumn(1, new PixmapItemDelegate(popup())); + + itemDelegate_ = new CompleterItemDelegate(this); + popup()->setItemDelegate(itemDelegate_); // create timeout timers popup_timer_.setSingleShot(true); @@ -195,15 +241,37 @@ bool GuiCompleter::inlinePossible(Cursor const & cur) const } +bool GuiCompleter::completionAvailable() const +{ + if (!modelActive_) + return false; + + size_t n = popup()->model()->rowCount(); + + // if there is exactly one, we have to check whether it is a + // real completion, i.e. longer than the current prefix. + if (n == 1 && completionPrefix() == currentCompletion()) + return false; + + return n > 0; +} + + bool GuiCompleter::popupVisible() const { - return popup()->isVisible(); + return popupVisible_; } bool GuiCompleter::inlineVisible() const { - return !gui_->bufferView().inlineCompletionPos().empty(); + // In fact using BufferView::inlineCompletionPos.empty() should be + // here. But unfortunately this information is not good enough + // because destructive operations like backspace might invalidate + // inlineCompletionPos. But then the completion should stay visible + // (i.e. reshown on the next update). Hence be keep this information + // in the inlineVisible_ variable. + return inlineVisible_; } @@ -218,50 +286,41 @@ void GuiCompleter::updateVisibility(Cursor & cur, bool start, bool keep, bool cu bool possibleInlineState = inlinePossible(cur) && cursorInView; // we moved or popup state is not ok for popup? - if ((moved && !keep) || !possiblePopupState) { - // stop an old completion timer - if (popup_timer_.isActive()) - popup_timer_.stop(); - - // hide old popup - if (popupVisible()) - popup()->hide(); - } + if ((moved && !keep) || !possiblePopupState) + hidePopup(cur); // we moved or inline state is not ok for inline completion? - if ((moved && !keep) || !possibleInlineState) { - // stop an old completion timer - if (inline_timer_.isActive()) - inline_timer_.stop(); - - // hide old inline completion - if (inlineVisible()) { - gui_->bufferView().setInlineCompletion(DocIterator(), docstring()); - cur.updateFlags(Update::Force | Update::SinglePar); - } - } + if ((moved && !keep) || !possibleInlineState) + hideInline(cur); // we inserted something and are in a possible popup state? if (!popupVisible() && possiblePopupState && start && cur.inset().automaticPopupCompletion()) - popup_timer_.start(lyxrc.completion_popup_delay * 1000.0); + popup_timer_.start(int(lyxrc.completion_popup_delay * 1000)); // we inserted something and are in a possible inline completion state? if (!inlineVisible() && possibleInlineState && start && cur.inset().automaticInlineCompletion()) - inline_timer_.start(lyxrc.completion_inline_delay * 1000.0); + inline_timer_.start(int(lyxrc.completion_inline_delay * 1000)); - // update prefix if popup is visible or if it will be visible soon - if (popupVisible() || inlineVisible() - || popup_timer_.isActive() || inline_timer_.isActive()) - updatePrefix(cur); + // update prefix if any completion is possible + bool modelActive = modelActive_ && model()->rowCount() > 0; + if (possiblePopupState || possibleInlineState) { + if (modelActive) + updatePrefix(cur); + else + updateAvailability(); + } } void GuiCompleter::updateVisibility(bool start, bool keep) { Cursor cur = gui_->bufferView().cursor(); + cur.updateFlags(Update::None); + updateVisibility(cur, start, keep); + if (cur.disp_.update()) gui_->bufferView().processUpdateFlags(cur.disp_.update()); } @@ -281,7 +340,7 @@ void GuiCompleter::updatePrefix(Cursor & cur) // update completer to new prefix setCompletionPrefix(newPrefix); - + // update popup because its size might have changed if (popupVisible()) updatePopup(cur); @@ -292,11 +351,13 @@ void GuiCompleter::updatePrefix(Cursor & cur) // if popup is not empty, the new selection will // be our last valid one QString const & s = currentCompletion(); - if (s.length() > 0) - last_selection_ = s; - else - last_selection_ = old; - + if (popupVisible() || inlineVisible()) { + if (s.length() > 0) + last_selection_ = s; + else + last_selection_ = old; + } + // update inline completion because the default // completion string might have changed if (inlineVisible()) @@ -311,7 +372,7 @@ void GuiCompleter::updateInline(Cursor & cur, QString const & completion) // compute postfix docstring prefix = cur.inset().completionPrefix(cur); - docstring postfix = from_utf8(fromqstr(completion.mid(prefix.length()))); + docstring postfix = qstring_to_ucs4(completion.mid(prefix.length())); // shorten it if necessary if (lyxrc.completion_inline_dots != -1 @@ -320,8 +381,8 @@ void GuiCompleter::updateInline(Cursor & cur, QString const & completion) // set inline completion at cursor position size_t uniqueTo = max(longestUniqueCompletion().size(), prefix.size()); - gui_->bufferView().setInlineCompletion(cur, postfix, uniqueTo - prefix.size()); - cur.updateFlags(Update::Force | Update::SinglePar); + gui_->bufferView().setInlineCompletion(cur, cur, postfix, uniqueTo - prefix.size()); + inlineVisible_ = true; } @@ -330,36 +391,98 @@ void GuiCompleter::updatePopup(Cursor & cur) if (!cur.inset().completionSupported(cur)) return; - if (completionCount() == 0) + popupVisible_ = true; + + if (completionCount() == 0) { + QTimer::singleShot(0, popup(), SLOT(hide())); return; - + } + + QTimer::singleShot(0, this, SLOT(asyncUpdatePopup())); +} + + +void GuiCompleter::asyncUpdatePopup() +{ + Cursor cur = gui_->bufferView().cursor(); + if (!cur.inset().completionSupported(cur)) { + popupVisible_ = false; + return; + } + // get dimensions of completion prefix Dimension dim; int x; int y; cur.inset().completionPosAndDim(cur, x, y, dim); - QRect insetRect = QRect(x, y - dim.ascent() - 3, 200, dim.height() + 6); - // show/update popup - complete(insetRect); - QTreeView * p = static_cast(popup()); - p->setColumnWidth(0, popup()->width() - 22 - p->verticalScrollBar()->width()); + // and calculate the rect of the popup + QRect rect; + if (popup()->layoutDirection() == Qt::RightToLeft) + rect = QRect(x + dim.width() - 200, y - dim.ascent() - 3, 200, dim.height() + 6); + else + rect = QRect(x, y - dim.ascent() - 3, 200, dim.height() + 6); - // update highlight - updateInline(cur, currentCompletion()); + // Resize the columns in the popup. + // This should really be in the constructor. But somehow the treeview + // has a bad memory about it and we have to tell him again and again. + QTreeView * listView = static_cast(popup()); + listView->header()->setStretchLastSection(false); + listView->header()->setResizeMode(0, QHeaderView::Stretch); + listView->header()->setResizeMode(1, QHeaderView::Fixed); + listView->header()->resizeSection(1, 22); + + // show/update popup + complete(rect); } +void GuiCompleter::updateAvailability() +{ + // this should really only be of interest if no completion is + // visible yet, i.e. especially if automatic completion is disabled. + if (inlineVisible() || popupVisible()) + return; + Cursor & cur = gui_->bufferView().cursor(); + if (!popupPossible(cur) && !inlinePossible(cur)) + return; + + updateModel(cur, false, false); +} + + void GuiCompleter::updateModel(Cursor & cur, bool popupUpdate, bool inlineUpdate) { // value which should be kept selected QString old = currentCompletion(); if (old.length() == 0) old = last_selection_; - + + + // set whether rtl + bool rtl = false; + if (cur.inTexted()) { + Paragraph const & par = cur.paragraph(); + Font const font = + par.getFontSettings(cur.bv().buffer().params(), cur.pos()); + rtl = font.isVisibleRightToLeft(); + } + popup()->setLayoutDirection(rtl ? Qt::RightToLeft : Qt::LeftToRight); + // set new model - setModel(new GuiCompletionModel(this, cur.inset().completionList(cur))); - + CompletionList const * list = cur.inset().createCompletionList(cur); + model_->setList(list); + modelActive_ = true; + if (list->sorted()) + setModelSorting(QCompleter::CaseSensitivelySortedModel); + else + setModelSorting(QCompleter::UnsortedModel); + + // set prefix + QString newPrefix = toqstr(cur.inset().completionPrefix(cur)); + if (newPrefix != completionPrefix()) + setCompletionPrefix(newPrefix); + // show popup if (popupUpdate) updatePopup(cur); @@ -369,12 +492,14 @@ void GuiCompleter::updateModel(Cursor & cur, bool popupUpdate, bool inlineUpdate // if popup is not empty, the new selection will // be our last valid one - QString const & s = currentCompletion(); - if (s.length() > 0) - last_selection_ = s; - else - last_selection_ = old; - + if (popupVisible() || inlineVisible()) { + QString const & s = currentCompletion(); + if (s.length() > 0) + last_selection_ = s; + else + last_selection_ = old; + } + // show inline completion if (inlineUpdate) updateInline(cur, currentCompletion()); @@ -387,9 +512,36 @@ void GuiCompleter::showPopup(Cursor & cur) return; updateModel(cur, true, inlineVisible()); - updatePrefix(cur); } + + +void GuiCompleter::hidePopup(Cursor &) +{ + popupVisible_ = false; + + if (popup_timer_.isActive()) + popup_timer_.stop(); + + // hide popup asynchronously because we might be here inside of + // LFUN dispatchers. Hiding a popup can trigger a focus event on the + // workarea which then redisplays the cursor. But the metrics are not + // yet up to date such that the coord cache has not all insets yet. The + // cursorPos methods would triggers asserts in the coord cache then. + QTimer::singleShot(0, this, SLOT(asyncHidePopup())); + // mark that the asynchronous part will reset the model + if (!inlineVisible()) + modelActive_ = false; +} + + +void GuiCompleter::asyncHidePopup() +{ + popup()->hide(); + if (!inlineVisible()) + model_->setList(0); +} + void GuiCompleter::showInline(Cursor & cur) { @@ -397,13 +549,40 @@ void GuiCompleter::showInline(Cursor & cur) return; updateModel(cur, popupVisible(), true); - updatePrefix(cur); +} + + +void GuiCompleter::hideInline(Cursor & cur) +{ + gui_->bufferView().setInlineCompletion(cur, DocIterator(), docstring()); + inlineVisible_ = false; + + if (inline_timer_.isActive()) + inline_timer_.stop(); + + // Trigger asynchronous part of hideInline. We might be + // in a dispatcher here and the setModel call might + // trigger focus events which is are not healthy here. + QTimer::singleShot(0, this, SLOT(asyncHideInline())); + + // mark that the asynchronous part will reset the model + if (!popupVisible()) + modelActive_ = false; +} + + +void GuiCompleter::asyncHideInline() +{ + if (!popupVisible()) + model_->setList(0); } void GuiCompleter::showPopup() { Cursor cur = gui_->bufferView().cursor(); + cur.updateFlags(Update::None); + showPopup(cur); // redraw if needed @@ -415,6 +594,8 @@ void GuiCompleter::showPopup() void GuiCompleter::showInline() { Cursor cur = gui_->bufferView().cursor(); + cur.updateFlags(Update::None); + showInline(cur); // redraw if needed @@ -423,28 +604,59 @@ void GuiCompleter::showInline() } +void GuiCompleter::hidePopup() +{ + Cursor cur = gui_->bufferView().cursor(); + cur.updateFlags(Update::None); + + hidePopup(cur); + + // redraw if needed + if (cur.disp_.update()) + gui_->bufferView().processUpdateFlags(cur.disp_.update()); +} + + +void GuiCompleter::hideInline() +{ + Cursor cur = gui_->bufferView().cursor(); + cur.updateFlags(Update::None); + + hideInline(cur); + + // redraw if needed + if (cur.disp_.update()) + gui_->bufferView().processUpdateFlags(cur.disp_.update()); +} + + void GuiCompleter::activate() { if (!popupVisible() && !inlineVisible()) return; - // Complete with current selection in the popup. - QString s = currentCompletion(); - popup()->hide(); - popupActivated(s); + popupActivated(currentCompletion()); } void GuiCompleter::tab() { BufferView * bv = &gui_->bufferView(); - Cursor & cur = bv->cursor(); - + Cursor cur = bv->cursor(); + cur.updateFlags(Update::None); + // check that inline completion is active if (!inlineVisible()) { // try to activate the inline completion if (cur.inset().inlineCompletionSupported(cur)) { showInline(); + + // show popup without delay because the completion was not unique + if (lyxrc.completion_popup_after_complete + && !popupVisible() + && popup()->model()->rowCount() > 1) + popup_timer_.start(0); + return; } // or try popup @@ -458,11 +670,14 @@ void GuiCompleter::tab() // If completion is active, at least complete by one character docstring prefix = cur.inset().completionPrefix(cur); - docstring completion = from_utf8(fromqstr(currentCompletion())); + docstring completion = qstring_to_ucs4(currentCompletion()); if (completion.size() <= prefix.size()) { // finalize completion cur.inset().insertCompletion(cur, docstring(), true); - popup()->hide(); + + // hide popup and inline completion + hidePopup(cur); + hideInline(cur); updateVisibility(false, false); return; } @@ -504,7 +719,7 @@ QString GuiCompleter::currentCompletion() const void GuiCompleter::setCurrentCompletion(QString const & s) -{ +{ QAbstractItemModel const & model = *popup()->model(); size_t n = model.rowCount(); if (n == 0) @@ -512,60 +727,144 @@ void GuiCompleter::setCurrentCompletion(QString const & s) // select the first if s is empty if (s.length() == 0) { + updateLock_++; popup()->setCurrentIndex(model.index(0, 0)); + updateLock_--; return; } - // iterate through list until the s is found - // FIXME: there must be a better way than this iteration + // find old selection in model size_t i; - for (i = 0; i < n; ++i) { - QString const & is - = model.data(model.index(i, 0), Qt::EditRole).toString(); - if (is == s) - break; + if (modelSorting() == QCompleter::UnsortedModel) { + // In unsorted models, iterate through list until the s is found + for (i = 0; i < n; ++i) { + QString const & is + = model.data(model.index(i, 0), Qt::EditRole).toString(); + if (is == s) + break; + } + } else { + // In sorted models, do binary search for s. + int l = 0; + int r = n - 1; + while (r >= l && l < int(n)) { + size_t mid = (r + l) / 2; + QString const & mids + = model.data(model.index(mid, 0), + Qt::EditRole).toString(); + + // left or right? + // FIXME: is this really the same order that the docstring + // from the CompletionList has? + int c = s.compare(mids, Qt::CaseSensitive); + if (c == 0) { + l = mid; + break; + } else if (l == r) { + l = n; + break; + } else if (c > 0) + // middle is not far enough + l = mid + 1; + else + // middle is too far + r = mid - 1; + } + + // loop was left without finding anything + if (r < l) + i = n; + else + i = l; + LASSERT(i <= n, /**/); } // select the first if none was found if (i == n) i = 0; + updateLock_++; popup()->setCurrentIndex(model.index(i, 0)); + updateLock_--; } -docstring GuiCompleter::longestUniqueCompletion() const { +size_t commonPrefix(QString const & s1, QString const & s2) +{ + // find common prefix + size_t j; + size_t n1 = s1.length(); + size_t n2 = s2.length(); + for (j = 0; j < n1 && j < n2; ++j) { + if (s1.at(j) != s2.at(j)) + break; + } + return j; +} + + +docstring GuiCompleter::longestUniqueCompletion() const +{ QAbstractItemModel const & model = *popup()->model(); - QString s = currentCompletion(); size_t n = model.rowCount(); - - // iterate through the completions and cut off where s differs - for (size_t i = 0; i < n && s.length() > 0; ++i) { - QString const & is - = model.data(model.index(i, 0), Qt::EditRole).toString(); - - // find common prefix - size_t j; - size_t isn = is.length(); - size_t sn = s.length(); - for (j = 0; j < isn && j < sn; ++j) { - if (s.at(j) != is.at(j)) - break; + if (n == 0) + return docstring(); + QString s = model.data(model.index(0, 0), Qt::EditRole).toString(); + + if (modelSorting() == QCompleter::UnsortedModel) { + // For unsorted model we cannot do more than iteration. + // Iterate through the completions and cut off where s differs + for (size_t i = 0; i < n && s.length() > 0; ++i) { + QString const & is + = model.data(model.index(i, 0), Qt::EditRole).toString(); + + s = s.left(commonPrefix(is, s)); + } + } else { + // For sorted models we can do binary search multiple times, + // each time to find the first string which has s not as prefix. + size_t i = 0; + while (i < n && s.length() > 0) { + // find first string that does not have s as prefix + // via binary search in [i,n-1] + size_t r = n - 1; + do { + // get common prefix with the middle string + size_t mid = (r + i) / 2; + QString const & mids + = model.data(model.index(mid, 0), + Qt::EditRole).toString(); + size_t oldLen = s.length(); + size_t len = commonPrefix(mids, s); + s = s.left(len); + + // left or right? + if (oldLen == len) { + // middle is not far enough + i = mid + 1; + } else { + // middle is maybe too far + r = mid; + } + } while (r - i > 0 && i < n); } - s = s.left(j); } - return from_utf8(fromqstr(s)); + return qstring_to_ucs4(s); } void GuiCompleter::popupActivated(const QString & completion) { - Cursor & cur = gui_->bufferView().cursor(); + Cursor cur = gui_->bufferView().cursor(); + cur.updateFlags(Update::None); + docstring prefix = cur.inset().completionPrefix(cur); - docstring postfix = from_utf8(fromqstr(completion.mid(prefix.length()))); + docstring postfix = qstring_to_ucs4(completion.mid(prefix.length())); cur.inset().insertCompletion(cur, postfix, true); - updateVisibility(cur, false); + hidePopup(cur); + hideInline(cur); + if (cur.disp_.update()) gui_->bufferView().processUpdateFlags(cur.disp_.update()); } @@ -573,8 +872,15 @@ void GuiCompleter::popupActivated(const QString & completion) void GuiCompleter::popupHighlighted(const QString & completion) { + if (updateLock_ > 0) + return; + Cursor cur = gui_->bufferView().cursor(); - updateInline(cur, completion); + cur.updateFlags(Update::None); + + if (inlineVisible()) + updateInline(cur, completion); + if (cur.disp_.update()) gui_->bufferView().processUpdateFlags(cur.disp_.update()); }