#include "support/lstrings.h"
#include "support/lyxalgo.h" // sorted
+#include <QAbstractItemDelegate>
+#include <QAbstractTextDocumentLayout>
+#include <QApplication>
#include <QComboBox>
+#include <QHeaderView>
+#include <QKeyEvent>
+#include <QList>
+#include <QPainter>
+#include <QPixmap>
+#include <QSortFilterProxyModel>
+#include <QStandardItem>
+#include <QStandardItemModel>
+#include <QTextDocument>
#include <QToolBar>
#include <QToolButton>
-#include <QAction>
-#include <QPixmap>
+#include <QVariant>
#include <boost/assert.hpp>
//
/////////////////////////////////////////////////////////////////////
+class FilterItemDelegate : public QAbstractItemDelegate {
+public:
+ ///
+ explicit FilterItemDelegate(QObject * parent = 0)
+ : QAbstractItemDelegate(parent) {}
+
+ ///
+ void paint(QPainter * painter, QStyleOptionViewItem const & option,
+ QModelIndex const & index) const {
+ QComboBox * combo = static_cast<QComboBox const *>(parent());
+
+ // Draw using the menu item style (this is how QComboBox does it).
+ // But for the rich text drawing below we will call it with an
+ // empty string, and later then draw over it the real string.
+ painter->save();
+ QStyleOptionMenuItem opt = getStyleOption(option, index);
+ QString text = underlineFilter(opt.text);
+ opt.text = QString();
+ painter->eraseRect(option.rect);
+ combo->style()->drawControl(QStyle::CE_MenuItem, &opt, painter, combo->view());
+ painter->restore();
+
+ // don't draw string for separator
+ if (opt.menuItemType == QStyleOptionMenuItem::Separator)
+ return;
+
+ // Draw the rich text.
+ painter->save();
+ QColor col = opt.palette.text().color();
+ if (opt.state & QStyle::State_Selected)
+ col = opt.palette.highlightedText().color();
+ QAbstractTextDocumentLayout::PaintContext context;
+ context.palette.setColor(QPalette::Text, col);
+
+ QTextDocument doc;
+ doc.setDefaultFont(opt.font);
+ doc.setHtml(text);
+ painter->translate(opt.rect.x() + 20, opt.rect.y());
+ doc.documentLayout()->draw(painter, context);
+ painter->restore();
+ }
+
+ ///
+ QSize sizeHint(QStyleOptionViewItem const & option,
+ QModelIndex const & index) const {
+ QComboBox * combo = static_cast<QComboBox const *>(parent());
+
+ QStyleOptionMenuItem opt = getStyleOption(option, index);
+ return combo->style()->sizeFromContents(
+ QStyle::CT_MenuItem, &opt, option.rect.size(), combo);
+ }
+
+private:
+ ///
+ QString underlineFilter(QString const & s) const
+ {
+ // get filter
+ GuiLayoutBox * p = static_cast<GuiLayoutBox *>(parent());
+ QString const & f = p->filter();
+ if (f.isEmpty())
+ return s;
+
+ // step through data item and put "(x)" for every matching character
+ QString r;
+ int lastp = -1;
+ p->filter();
+ for (int i = 0; i < f.length(); ++i) {
+ int p = s.indexOf(f[i], lastp + 1, Qt::CaseInsensitive);
+ BOOST_ASSERT(p != -1);
+ if (lastp == p - 1 && lastp != -1) {
+ // remove ")" and append "x)"
+ r = r.left(r.length() - 4) + s[p] + "</u>";
+ } else {
+ // append "(x)"
+ r += s.mid(lastp + 1, p - lastp - 1);
+ r += QString("<u>") + s[p] + "</u>";
+ }
+ lastp = p;
+ }
+ r += s.mid(lastp + 1);
+ return r;
+ }
+
+ ///
+ QStyleOptionMenuItem getStyleOption(QStyleOptionViewItem const & option,
+ QModelIndex const & index) const
+ {
+ QComboBox * combo = static_cast<QComboBox const *>(parent());
+
+ // create the options for a menu item
+ QStyleOptionMenuItem menuOption;
+ menuOption.palette = QApplication::palette("QMenu");
+ menuOption.state = QStyle::State_Active | QStyle::State_Enabled;
+ if (option.state & QStyle::State_Selected)
+ menuOption.state |= QStyle::State_Selected;
+ menuOption.checkType = QStyleOptionMenuItem::NonExclusive;
+ menuOption.checked = combo->currentIndex() == index.row();
+ menuOption.text = index.model()->data(index, Qt::DisplayRole).toString()
+ .replace(QLatin1Char('&'), QLatin1String("&&"));
+ if (menuOption.text.left(2) == "--")
+ menuOption.menuItemType = QStyleOptionMenuItem::Separator;
+ else
+ menuOption.menuItemType = QStyleOptionMenuItem::Normal;
+ menuOption.tabWidth = 0;
+ menuOption.menuRect = option.rect;
+ menuOption.rect = option.rect;
+ menuOption.font = combo->font();
+ menuOption.fontMetrics = QFontMetrics(menuOption.font);
+
+ return menuOption;
+ }
+};
+
+
+class GuiFilterProxyModel : public QSortFilterProxyModel
+{
+public:
+ ///
+ GuiFilterProxyModel(QObject * parent)
+ : QSortFilterProxyModel(parent) {}
+
+ ///
+ void setCharFilter(QString const & f)
+ {
+ setFilterRegExp(charFilterRegExp(f));
+ dataChanged(index(0, 0), index(rowCount() - 1, 1));
+ }
+
+private:
+ ///
+ QString charFilterRegExp(QString const & filter)
+ {
+ QString re;
+ for (int i = 0; i < filter.length(); ++i)
+ re += ".*" + QRegExp::escape(filter[i]);
+ return re;
+ }
+};
+
+
GuiLayoutBox::GuiLayoutBox(GuiView & owner)
- : owner_(owner)
+ : owner_(owner), filterItemDelegate_(new FilterItemDelegate(this))
{
setSizeAdjustPolicy(QComboBox::AdjustToContents);
setFocusPolicy(Qt::ClickFocus);
setMinimumWidth(sizeHint().width());
setMaxVisibleItems(100);
- QObject::connect(this, SIGNAL(activated(QString)),
- this, SLOT(selected(QString)));
+ // set the layout model with two columns
+ // 1st: translated layout names
+ // 2nd: raw layout names
+ model_ = new QStandardItemModel(0, 2, this);
+ filterModel_ = new GuiFilterProxyModel(this);
+ filterModel_->setSourceModel(model_);
+ filterModel_->setDynamicSortFilter(true);
+ filterModel_->setFilterCaseSensitivity(Qt::CaseInsensitive);
+ setModel(filterModel_);
+
+ // for the filtering we have to intercept characters
+ view()->installEventFilter(this);
+ view()->setItemDelegateForColumn(0, filterItemDelegate_);
+
+ QObject::connect(this, SIGNAL(activated(int)),
+ this, SLOT(selected(int)));
owner_.setLayoutDialog(this);
updateContents(true);
}
+void GuiLayoutBox::setFilter(QString const & s)
+{
+ // remember old selection
+ int sel = currentIndex();
+ if (sel != -1)
+ lastSel_ = filterModel_->mapToSource(filterModel_->index(sel, 0)).row();
+
+ filter_ = s;
+ filterModel_->setCharFilter(s);
+
+ // restore old selection
+ if (lastSel_ != -1) {
+ QModelIndex i = filterModel_->mapFromSource(model_->index(lastSel_, 0));
+ if (i.isValid())
+ setCurrentIndex(i.row());
+ }
+
+ // Workaround to resize to content size
+ // FIXME: There must be a better way. The QComboBox::AdjustToContents)
+ // does not help.
+ if (view()->isVisible())
+ QComboBox::showPopup();
+}
+
+
+void GuiLayoutBox::resetFilter()
+{
+ setFilter(QString());
+}
+
+
+void GuiLayoutBox::showPopup()
+{
+ resetFilter();
+ owner_.message(_("Enter characters to filter the layout list."));
+ QComboBox::showPopup();
+}
+
+
+bool GuiLayoutBox::eventFilter(QObject * o, QEvent * e)
+{
+ if (e->type() != QEvent::KeyPress)
+ return QComboBox::eventFilter(o, e);
+
+ QKeyEvent * ke = static_cast<QKeyEvent*>(e);
+ bool modified = (ke->modifiers() == Qt::ControlModifier)
+ || (ke->modifiers() == Qt::AltModifier)
+ || (ke->modifiers() == Qt::MetaModifier);
+
+ switch (ke->key()) {
+ case Qt::Key_Escape:
+ if (!modified && !filter_.isEmpty()) {
+ resetFilter();
+ return true;
+ }
+ break;
+ case Qt::Key_Backspace:
+ if (!modified) {
+ // cut off one character
+ setFilter(filter_.left(filter_.length() - 1));
+ }
+ break;
+ default:
+ if (modified || ke->text().isEmpty())
+ break;
+ // find chars for the filter string
+ QString s;
+ for (int i = 0; i < ke->text().length(); ++i) {
+ QChar c = ke->text()[i];
+ if (c.isLetterOrNumber()
+ || c.isSymbol()
+ || c.isPunct()
+ || c.category() == QChar::Separator_Space) {
+ s += c;
+ }
+ }
+ if (!s.isEmpty()) {
+ // append new chars to the filter string
+ setFilter(filter_ + s);
+ return true;
+ }
+ break;
+ }
+
+ return QComboBox::eventFilter(o, e);
+}
+
+
void GuiLayoutBox::set(docstring const & layout)
{
+ resetFilter();
+
if (!text_class_)
return;
- QString const & name = toqstr(translateIfPossible(
- (*text_class_)[layout]->name()));
-
+ QString const & name = toqstr((*text_class_)[layout]->name());
if (name == currentText())
return;
- int i = findText(name);
- if (i == -1) {
+ QList<QStandardItem *> r = model_->findItems(name, Qt::MatchExactly, 1);
+ if (r.empty()) {
lyxerr << "Trying to select non existent layout type "
<< fromqstr(name) << endl;
return;
}
- setCurrentIndex(i);
+ setCurrentIndex(filterModel_->mapFromSource(r.first()->index()).row());
}
-void GuiLayoutBox::addItemSort(QString const & item, bool sorted)
+void GuiLayoutBox::addItemSort(docstring const & item, bool sorted)
{
- //FIXME
- //Since we are only storing the text used for display, we have no choice
- //below but to compare translated strings to figure out which layout the
- //user wants. This is not ideal. A better way is the way module names are
- //handled in GuiDocument: viz, the untranslated name can be associated
- //with the item via QComboBox::setItemData(). It may be that this can
- //even be done by passing: addItem(item, untransName).
- int const end = count();
- if (!sorted || end < 2 || item[0].category() != QChar::Letter_Uppercase) {
- addItem(item);
+ QString qitem = toqstr(item);
+ QString titem = toqstr(translateIfPossible(item));
+
+ QList<QStandardItem *> row;
+ row.append(new QStandardItem(titem));
+ row.append(new QStandardItem(qitem));
+
+ // the simple unsorted case
+ int const end = model_->rowCount();
+ if (!sorted || end < 2 || qitem[0].category() != QChar::Letter_Uppercase) {
+ model_->appendRow(row);
return;
}
- // Let the default one be at the beginning
- int i = 1;
- for (setCurrentIndex(i); currentText().localeAwareCompare(item) < 0; ) {
- // e.g. --Separator--
- if (currentText()[0].category() != QChar::Letter_Uppercase)
- break;
- if (++i == end)
- break;
- setCurrentIndex(i);
+ // find row to insert the item, after the separator if it exists
+ int i = 1; // skip the Standard layout
+
+ QList<QStandardItem *> sep = model_->findItems("--", Qt::MatchStartsWith);
+ if (!sep.isEmpty())
+ i = sep.first()->index().row() + 1;
+ if (i < model_->rowCount()) {
+ // find alphabetic position
+ QString is = model_->item(i, 0)->text();
+ while (is.compare(titem) < 0) {
+ // e.g. --Separator--
+ if (is.at(0).category() != QChar::Letter_Uppercase)
+ break;
+ ++i;
+ if (i == end)
+ break;
+ is = model_->item(i, 0)->text();
+ }
}
- insertItem(i, item);
+ model_->insertRow(i, row);
}
void GuiLayoutBox::updateContents(bool reset)
{
+ resetFilter();
+
Buffer const * buffer = owner_.buffer();
if (!buffer) {
- clear();
+ model_->clear();
setEnabled(false);
text_class_ = 0;
inset_ = 0;
return;
}
- TextClass const * text_class = &buffer->params().getTextClass();
- Inset const * inset =
- owner_.view()->cursor().innerParagraph().inInset();
-
// we'll only update the layout list if the text class has changed
// or we've moved from one inset to another
+ DocumentClass const * text_class = &buffer->params().documentClass();
+ Inset const * inset =
+ owner_.view()->cursor().innerParagraph().inInset();
if (!reset && text_class_ == text_class && inset_ == inset) {
set(owner_.view()->cursor().innerParagraph().layout()->name());
return;
inset_ = inset;
text_class_ = text_class;
- clear();
-
+ model_->clear();
for (size_t i = 0; i != text_class_->layoutCount(); ++i) {
Layout const & lt = *text_class_->layout(i);
docstring const & name = lt.name();
if (name == text_class_->emptyLayoutName() && inset &&
!inset->forceEmptyLayout() && !inset->useEmptyLayout())
continue;
- addItemSort(toqstr(translateIfPossible(name)), lyxrc.sort_layouts);
+ addItemSort(name, lyxrc.sort_layouts);
}
set(owner_.view()->cursor().innerParagraph().layout()->name());
// needed to recalculate size hint
hide();
setMinimumWidth(sizeHint().width());
-
setEnabled(!buffer->isReadonly());
show();
}
-void GuiLayoutBox::selected(const QString & str)
+void GuiLayoutBox::selected(int index)
{
+ // get selection
+ QModelIndex mindex = filterModel_->mapToSource(filterModel_->index(index, 1));
+ docstring const name = qstring_to_ucs4(model_->itemFromIndex(mindex)->text());
+
owner_.setFocus();
- updateContents(false);
- if (!text_class_)
+
+ if (!text_class_) {
+ updateContents(false);
+ resetFilter();
return;
+ }
- docstring const name = qstring_to_ucs4(str);
+ // find corresponding text class
for (size_t i = 0; i != text_class_->layoutCount(); ++i) {
docstring const & itname = text_class_->layout(i)->name();
- // FIXME: Comparing translated strings is not ideal.
- // This should be done the way module names are handled
- // in GuiDocument: viz, the untranslated name should be
- // associated with the item via QComboBox::setItemData().
- if (translateIfPossible(itname) == name) {
+ if (itname == name) {
FuncRequest const func(LFUN_LAYOUT, itname,
FuncRequest::TOOLBAR);
theLyXFunc().setLyXView(&owner_);
lyx::dispatch(func);
updateContents(false);
+ resetFilter();
return;
}
}