lyx_mirror/src/frontends/qt4/GuiCompleter.cpp
Georg Baum 7adc6125f7 Fix missing symbols in math completer
The math icons for the symbol image in the math completer were hardcoded to the
command names. This is wrong for some icons for various reasons, e.g. the case
insensitivity of windows file systems. Therefore we have to use the replacement
list which is also used for the toolbar icons. Bug #3538 is not closed because
of this problem, but IMHO it has nothing to do with this bug, it is a more
general one.
2015-02-14 20:32:25 +01:00

911 lines
22 KiB
C++

/**
* \file GuiCompleter.cpp
* This file is part of LyX, the document processor.
* Licence details can be found in the file COPYING.
*
* \author Stefan Schimanski
*
* Full author contact details are available in file CREDITS.
*/
#include <config.h>
#include "GuiCompleter.h"
#include "Buffer.h"
#include "BufferView.h"
#include "CompletionList.h"
#include "Cursor.h"
#include "Dimension.h"
#include "GuiWorkArea.h"
#include "GuiView.h"
#include "LyX.h"
#include "LyXRC.h"
#include "Paragraph.h"
#include "qt_helpers.h"
#include "version.h"
#include "support/lassert.h"
#include "support/debug.h"
#include <QApplication>
#include <QHeaderView>
#include <QKeyEvent>
#include <QPainter>
#include <QPixmapCache>
#include <QScrollBar>
#include <QItemDelegate>
#include <QTreeView>
#include <QTimer>
using namespace std;
using namespace lyx::support;
namespace lyx {
namespace frontend {
class CompleterItemDelegate : public QItemDelegate
{
public:
explicit CompleterItemDelegate(QObject * parent)
: QItemDelegate(parent)
{}
~CompleterItemDelegate()
{}
protected:
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
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<QPixmap>(value);
// draw
painter->save();
drawBackground(painter, opt, index);
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
{
public:
///
GuiCompletionModel(QObject * parent, CompletionList const * l)
: QAbstractListModel(parent), list_(l)
{}
///
~GuiCompletionModel() { delete list_; }
///
void setList(CompletionList const * l) {
beginResetModel();
delete list_;
list_ = l;
endResetModel();
}
///
bool sorted() const
{
if (list_)
return list_->sorted();
return false;
}
///
int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const
{
return 2;
}
///
int rowCount(const QModelIndex & /*parent*/ = QModelIndex()) const
{
if (list_ == 0)
return 0;
return list_->size();
}
///
QVariant data(const QModelIndex & index, int role) const
{
if (list_ == 0)
return QVariant();
if (index.row() < 0 || index.row() >= rowCount())
return QVariant();
if (role != Qt::DisplayRole && role != Qt::EditRole)
return QVariant();
if (index.column() == 0)
return toqstr(list_->data(index.row()));
if (index.column() != 1)
return QVariant();
// get icon from cache
QPixmap scaled;
QString const name = ":" + toqstr(list_->icon(index.row()));
if (name == ":")
return scaled;
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);
}
QPixmapCache::insert("completion" + name, scaled);
}
return scaled;
}
private:
/// owned by us
CompletionList const * list_;
};
GuiCompleter::GuiCompleter(GuiWorkArea * gui, QObject * parent)
: QCompleter(parent), gui_(gui), old_cursor_(0), updateLock_(0),
inlineVisible_(false), popupVisible_(false),
modelActive_(false)
{
// Setup the completion popup
model_ = new GuiCompletionModel(this, 0);
setModel(model_);
setCompletionMode(QCompleter::PopupCompletion);
setCaseSensitivity(Qt::CaseSensitive);
setWidget(gui_);
// create the popup
QTreeView *listView = new QTreeView;
listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
listView->setSelectionBehavior(QAbstractItemView::SelectRows);
listView->setSelectionMode(QAbstractItemView::SingleSelection);
listView->header()->hide();
listView->setIndentation(0);
listView->setUniformRowHeights(true);
setPopup(listView);
itemDelegate_ = new CompleterItemDelegate(this);
popup()->setItemDelegate(itemDelegate_);
// create timeout timers
popup_timer_.setSingleShot(true);
inline_timer_.setSingleShot(true);
connect(this, SIGNAL(highlighted(const QString &)),
this, SLOT(popupHighlighted(const QString &)));
connect(this, SIGNAL(activated(const QString &)),
this, SLOT(popupActivated(const QString &)));
connect(&popup_timer_, SIGNAL(timeout()),
this, SLOT(showPopup()));
connect(&inline_timer_, SIGNAL(timeout()),
this, SLOT(showInline()));
}
GuiCompleter::~GuiCompleter()
{
popup()->hide();
}
bool GuiCompleter::eventFilter(QObject * watched, QEvent * e)
{
// hijack back the tab key from the popup
// (which stole it from the workspace before)
if (e->type() == QEvent::KeyPress && popupVisible()) {
QKeyEvent *ke = static_cast<QKeyEvent *>(e);
switch (ke->key()) {
case Qt::Key_Tab:
tab();
ke->accept();
return true;
default: break;
}
}
return QCompleter::eventFilter(watched, e);
}
bool GuiCompleter::popupPossible(Cursor const & cur) const
{
return QApplication::activeWindow()
&& gui_->hasFocus()
&& cur.inset().completionSupported(cur);
}
bool GuiCompleter::inlinePossible(Cursor const & cur) const
{
return cur.inset().inlineCompletionSupported(cur);
}
bool GuiCompleter::uniqueCompletionAvailable() const
{
if (!modelActive_)
return false;
size_t n = popup()->model()->rowCount();
if (n > 1 || n == 0)
return false;
// if there is exactly one, we have to check whether it is a
// real completion, i.e. longer than the current prefix.
if (completionPrefix() == currentCompletion())
return false;
return true;
}
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 popupVisible_;
}
bool GuiCompleter::inlineVisible() const
{
// 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_;
}
void GuiCompleter::updateVisibility(Cursor & cur, bool start, bool keep)
{
// parameters which affect the completion
bool moved = cur != old_cursor_;
if (moved)
old_cursor_ = cur;
bool const possiblePopupState = popupPossible(cur);
bool const possibleInlineState = inlinePossible(cur);
// we moved or popup state is not ok for popup?
if ((moved && !keep) || !possiblePopupState)
hidePopup();
// we moved or inline state is not ok for inline completion?
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(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(int(lyxrc.completion_inline_delay * 1000));
else if (cur.inMathed() && !lyxrc.completion_inline_math) {
// no inline completion, hence a metrics update is needed
if (!(cur.result().screenUpdate() & Update::Force))
cur.screenUpdateFlags(cur.result().screenUpdate() | Update::SinglePar);
}
// 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.screenUpdateFlags(Update::None);
updateVisibility(cur, start, keep);
if (cur.result().screenUpdate())
gui_->bufferView().processUpdateFlags(cur.result().screenUpdate());
}
void GuiCompleter::updatePrefix(Cursor const & cur)
{
// get new prefix. Do nothing if unchanged
QString newPrefix = toqstr(cur.inset().completionPrefix(cur));
if (newPrefix == completionPrefix())
return;
// value which should be kept selected
QString old = currentCompletion();
if (old.length() == 0)
old = last_selection_;
// update completer to new prefix
setCompletionPrefix(newPrefix);
// update popup because its size might have changed
if (popupVisible())
updatePopup(cur);
// restore old selection
setCurrentCompletion(old);
// if popup is not empty, the new selection will
// be our last valid one
QString const & s = currentCompletion();
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())
updateInline(cur, s);
}
void GuiCompleter::updateInline(Cursor const & cur, QString const & completion)
{
if (!cur.inset().inlineCompletionSupported(cur))
return;
// compute postfix
docstring prefix = cur.inset().completionPrefix(cur);
docstring postfix = qstring_to_ucs4(completion.mid(prefix.length()));
// shorten it if necessary
if (lyxrc.completion_inline_dots != -1
&& postfix.size() > unsigned(lyxrc.completion_inline_dots))
postfix = postfix.substr(0, lyxrc.completion_inline_dots - 1) + "...";
// set inline completion at cursor position
size_t uniqueTo = max(longestUniqueCompletion().size(), prefix.size());
gui_->bufferView().setInlineCompletion(cur, cur, postfix, uniqueTo - prefix.size());
inlineVisible_ = true;
}
void GuiCompleter::updatePopup(Cursor const & cur)
{
if (!cur.inset().completionSupported(cur))
return;
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)
|| !cur.bv().paragraphVisible(cur)) {
popupVisible_ = false;
return;
}
// get dimensions of completion prefix
Dimension dim;
int x;
int y;
cur.inset().completionPosAndDim(cur, x, y, dim);
// 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);
// 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<QTreeView *>(popup());
listView->header()->setStretchLastSection(false);
setSectionResizeMode(listView->header(), 0, QHeaderView::Stretch);
setSectionResizeMode(listView->header(), 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 const & cur = gui_->bufferView().cursor();
if (!popupPossible(cur) && !inlinePossible(cur))
return;
updateModel(cur, false, false);
}
void GuiCompleter::updateModel(Cursor const & 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
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);
// restore old selection
setCurrentCompletion(old);
// if popup is not empty, the new selection will
// be our last valid one
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());
}
void GuiCompleter::showPopup(Cursor const & cur)
{
if (!popupPossible(cur))
return;
updateModel(cur, true, inlineVisible());
}
void GuiCompleter::asyncHidePopup()
{
popup()->hide();
if (!inlineVisible())
model_->setList(0);
}
void GuiCompleter::showInline(Cursor const & cur)
{
if (!inlinePossible(cur))
return;
updateModel(cur, popupVisible(), true);
}
void GuiCompleter::hideInline(Cursor const & cur)
{
gui_->bufferView().setInlineCompletion(cur, DocIterator(cur.buffer()), 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.screenUpdateFlags(Update::None);
showPopup(cur);
// redraw if needed
if (cur.result().screenUpdate())
gui_->bufferView().processUpdateFlags(cur.result().screenUpdate());
}
void GuiCompleter::showInline()
{
Cursor cur = gui_->bufferView().cursor();
cur.screenUpdateFlags(Update::None);
showInline(cur);
// redraw if needed
if (cur.result().screenUpdate())
gui_->bufferView().processUpdateFlags(cur.result().screenUpdate());
}
void GuiCompleter::hidePopup()
{
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::hideInline()
{
Cursor cur = gui_->bufferView().cursor();
cur.screenUpdateFlags(Update::None);
hideInline(cur);
// redraw if needed
if (cur.result().screenUpdate())
gui_->bufferView().processUpdateFlags(cur.result().screenUpdate());
}
void GuiCompleter::activate()
{
if (!popupVisible() && !inlineVisible())
tab();
else
popupActivated(currentCompletion());
}
void GuiCompleter::tab()
{
BufferView * bv = &gui_->bufferView();
Cursor cur = bv->cursor();
cur.screenUpdateFlags(Update::None);
// check that inline completion is active
if (!inlineVisible() && !uniqueCompletionAvailable()) {
// 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
if (!popupVisible() && cur.inset().completionSupported(cur)) {
showPopup();
return;
}
return;
}
// Make undo possible
cur.beginUndoGroup();
cur.recordUndo();
// If completion is active, at least complete by one character
docstring prefix = cur.inset().completionPrefix(cur);
docstring completion = qstring_to_ucs4(currentCompletion());
if (completion.size() <= prefix.size()) {
// finalize completion
cur.inset().insertCompletion(cur, docstring(), true);
// hide popup and inline completion
hidePopup();
hideInline(cur);
updateVisibility(false, false);
cur.endUndoGroup();
return;
}
docstring nextchar = completion.substr(prefix.size(), 1);
if (!cur.inset().insertCompletion(cur, nextchar, false)) {
cur.endUndoGroup();
return;
}
updatePrefix(cur);
// try to complete as far as it is unique
docstring longestCompletion = longestUniqueCompletion();
prefix = cur.inset().completionPrefix(cur);
docstring postfix = longestCompletion.substr(min(longestCompletion.size(), prefix.size()));
cur.inset().insertCompletion(cur, postfix, false);
old_cursor_ = bv->cursor();
updatePrefix(cur);
// 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);
// redraw if needed
if (cur.result().screenUpdate())
gui_->bufferView().processUpdateFlags(cur.result().screenUpdate());
cur.endUndoGroup();
}
QString GuiCompleter::currentCompletion() const
{
if (!popup()->selectionModel()->hasSelection())
return QString();
// Not sure if this is bug in Qt: currentIndex() always
// return the first element in the list.
QModelIndex idx = popup()->currentIndex();
return popup()->model()->data(idx, Qt::EditRole).toString();
}
void GuiCompleter::setCurrentCompletion(QString const & s)
{
QAbstractItemModel const & model = *popup()->model();
size_t n = model.rowCount();
if (n == 0)
return;
// select the first if s is empty
if (s.length() == 0) {
updateLock_++;
popup()->setCurrentIndex(model.index(0, 0));
updateLock_--;
return;
}
// find old selection in model
size_t i;
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;
// we can try to recover
LASSERT(i <= n, i = 0);
}
// select the first if none was found
if (i == n)
i = 0;
updateLock_++;
popup()->setCurrentIndex(model.index(i, 0));
updateLock_--;
}
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();
size_t n = model.rowCount();
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);
}
}
return qstring_to_ucs4(s);
}
void GuiCompleter::popupActivated(const QString & completion)
{
Cursor cur = gui_->bufferView().cursor();
cur.screenUpdateFlags(Update::None);
cur.beginUndoGroup();
cur.recordUndo();
docstring prefix = cur.inset().completionPrefix(cur);
docstring postfix = qstring_to_ucs4(completion.mid(prefix.length()));
cur.inset().insertCompletion(cur, postfix, true);
hidePopup();
hideInline(cur);
if (cur.result().screenUpdate())
gui_->bufferView().processUpdateFlags(cur.result().screenUpdate());
cur.endUndoGroup();
}
void GuiCompleter::popupHighlighted(const QString & completion)
{
if (updateLock_ > 0)
return;
Cursor cur = gui_->bufferView().cursor();
cur.screenUpdateFlags(Update::None);
if (inlineVisible())
updateInline(cur, completion);
if (cur.result().screenUpdate())
gui_->bufferView().processUpdateFlags(cur.result().screenUpdate());
}
} // namespace frontend
} // namespace lyx
#include "moc_GuiCompleter.cpp"