From: Abdelrazak Younes Date: Wed, 12 Aug 2009 21:51:10 +0000 (+0000) Subject: This move out the GuiLayoutBox class out of GuiToolbar where it was a mess. The new... X-Git-Tag: 2.0.0~5762 X-Git-Url: https://git.lyx.org/gitweb/?a=commitdiff_plain;h=5f2429b4e21f80c2e786a270b2df2d912f12b327;p=features.git This move out the GuiLayoutBox class out of GuiToolbar where it was a mess. The new LayoutBox is less of a mess now but still a mess. We need a proper LayoutModel class. git-svn-id: svn://svn.lyx.org/lyx/lyx-devel/trunk@30995 a592a061-630c-0410-9148-cb99ea01b6c8 --- diff --git a/src/frontends/qt4/GuiToolbar.cpp b/src/frontends/qt4/GuiToolbar.cpp index 7b0c20213e..80be9d3b5d 100644 --- a/src/frontends/qt4/GuiToolbar.cpp +++ b/src/frontends/qt4/GuiToolbar.cpp @@ -23,51 +23,24 @@ #include "GuiView.h" #include "IconPalette.h" #include "InsertTableWidget.h" +#include "LayoutBox.h" #include "qt_helpers.h" #include "Toolbars.h" -#include "Buffer.h" -#include "BufferParams.h" -#include "BufferView.h" -#include "Cursor.h" #include "FuncRequest.h" #include "FuncStatus.h" #include "KeyMap.h" -#include "Layout.h" #include "LyXFunc.h" #include "LyXRC.h" -#include "Paragraph.h" -#include "TextClass.h" - -#include "insets/InsetText.h" #include "support/debug.h" -#include "support/filetools.h" #include "support/gettext.h" #include "support/lstrings.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include #include -#include -#include #include #include -#include #include "support/lassert.h" @@ -77,616 +50,6 @@ using namespace lyx::support; namespace lyx { namespace frontend { -///////////////////////////////////////////////////////////////////// -// -// GuiLayoutBox -// -///////////////////////////////////////////////////////////////////// - -class LayoutItemDelegate : public QItemDelegate { -public: - /// - explicit LayoutItemDelegate(QObject * parent = 0) - : QItemDelegate(parent) - {} - - /// - void paint(QPainter * painter, QStyleOptionViewItem const & option, - QModelIndex const & index) const - { - QStyleOptionViewItem opt = option; - - // default background - painter->fillRect(opt.rect, opt.palette.color(QPalette::Base)); - - // category header? - if (lyxrc.group_layouts) { - QSortFilterProxyModel const * model - = static_cast(index.model()); - - QString stdCat = category(*model->sourceModel(), 0); - QString cat = category(*index.model(), index.row()); - - // not the standard layout and not the same as in the previous line? - if (stdCat != cat - && (index.row() == 0 || cat != category(*index.model(), index.row() - 1))) { - painter->save(); - - // draw unselected background - QStyle::State state = opt.state; - opt.state = opt.state & ~QStyle::State_Selected; - drawBackground(painter, opt, index); - opt.state = state; - - // draw category header - drawCategoryHeader(painter, opt, - category(*index.model(), index.row())); - - // move rect down below header - opt.rect.setTop(opt.rect.top() + headerHeight(opt)); - - painter->restore(); - } - } - - QItemDelegate::paint(painter, opt, index); - } - - /// - void drawDisplay(QPainter * painter, QStyleOptionViewItem const & opt, - const QRect & /*rect*/, const QString & text ) const - { - QString utext = underlineFilter(text); - - // 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(utext); - - QTextFrameFormat fmt = doc.rootFrame()->frameFormat(); - fmt.setMargin(0); - doc.rootFrame()->setFrameFormat(fmt); - - painter->translate(opt.rect.x() + 5, - opt.rect.y() + (opt.rect.height() - opt.fontMetrics.height()) / 2); - doc.documentLayout()->draw(painter, context); - painter->restore(); - } - - /// - QSize sizeHint(QStyleOptionViewItem const & opt, - QModelIndex const & index) const - { - GuiLayoutBox * combo = static_cast(parent()); - QSortFilterProxyModel const * model - = static_cast(index.model()); - QSize size = QItemDelegate::sizeHint(opt, index); - - /// QComboBox uses the first row height to estimate the - /// complete popup height during QComboBox::showPopup(). - /// To avoid scrolling we have to sneak in space for the headers. - /// So we tweak this value accordingly. It's not nice, but the - /// only possible way it seems. - if (lyxrc.group_layouts && index.row() == 0 && combo->inShowPopup_) { - int itemHeight = size.height(); - - // we have to show \c cats many headers: - unsigned cats = combo->visibleCategories_; - - // and we have \c n items to distribute the needed space over - unsigned n = combo->model()->rowCount(); - - // so the needed average height (rounded upwards) is: - size.setHeight((headerHeight(opt) * cats + itemHeight * n + n - 1) / n); - return size; - } - - // Add space for the category headers here? - // Not for the standard layout though. - QString stdCat = category(*model->sourceModel(), 0); - QString cat = category(*index.model(), index.row()); - if (lyxrc.group_layouts && stdCat != cat - && (index.row() == 0 || cat != category(*index.model(), index.row() - 1))) { - size.setHeight(size.height() + headerHeight(opt)); - } - - return size; - } - -private: - /// - QString category(QAbstractItemModel const & model, int row) const - { - return model.data(model.index(row, 2), Qt::DisplayRole).toString(); - } - - /// - int headerHeight(QStyleOptionViewItem const & opt) const - { - return opt.fontMetrics.height() * 8 / 10; - } - /// - void drawCategoryHeader(QPainter * painter, QStyleOptionViewItem const & opt, - QString const & category) const - { - // slightly blended color - QColor lcol = opt.palette.text().color(); - lcol.setAlpha(127); - painter->setPen(lcol); - - // set 80% scaled, bold font - QFont font = opt.font; - font.setBold(true); - font.setWeight(QFont::Black); - font.setPointSize(opt.font.pointSize() * 8 / 10); - painter->setFont(font); - - // draw the centered text - QFontMetrics fm(font); - int w = fm.width(category); - int x = opt.rect.x() + (opt.rect.width() - w) / 2; - int y = opt.rect.y() + fm.ascent(); - int left = x; - int right = x + w; - painter->drawText(x, y, category); - - // the vertical position of the line: middle of lower case chars - int ymid = y - 1 - fm.xHeight() / 2; // -1 for the baseline - - // draw the horizontal line - if (!category.isEmpty()) { - painter->drawLine(opt.rect.x(), ymid, left - 1, ymid); - painter->drawLine(right + 1, ymid, opt.rect.right(), ymid); - } else - painter->drawLine(opt.rect.x(), ymid, opt.rect.right(), ymid); - } - - - /// - QString underlineFilter(QString const & s) const - { - // get filter - GuiLayoutBox * p = static_cast(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); - LASSERT(p != -1, /**/); - if (lastp == p - 1 && lastp != -1) { - // remove ")" and append "x)" - r = r.left(r.length() - 4) + s[p] + ""; - } else { - // append "(x)" - r += s.mid(lastp + 1, p - lastp - 1); - r += QString("") + s[p] + ""; - } - lastp = p; - } - r += s.mid(lastp + 1); - return r; - } -}; - - -class GuiLayoutFilterModel : public QSortFilterProxyModel { -public: - /// - GuiLayoutFilterModel(QObject * parent = 0) - : QSortFilterProxyModel(parent) - {} - - /// - void triggerLayoutChange() - { - layoutAboutToBeChanged(); - layoutChanged(); - } -}; - - -GuiLayoutBox::GuiLayoutBox(GuiToolbar * bar, GuiView & owner) - : owner_(owner), bar_(bar), lastSel_(-1), - layoutItemDelegate_(new LayoutItemDelegate(this)), - visibleCategories_(0), inShowPopup_(false) -{ - setSizeAdjustPolicy(QComboBox::AdjustToContents); - setFocusPolicy(Qt::ClickFocus); - setMinimumWidth(sizeHint().width()); - setMaxVisibleItems(100); - - // set the layout model with two columns - // 1st: translated layout names - // 2nd: raw layout names - model_ = new QStandardItemModel(0, 2, this); - filterModel_ = new GuiLayoutFilterModel(this); - filterModel_->setSourceModel(model_); - setModel(filterModel_); - - // for the filtering we have to intercept characters - view()->installEventFilter(this); - view()->setItemDelegateForColumn(0, layoutItemDelegate_); - - QObject::connect(this, SIGNAL(activated(int)), - this, SLOT(selected(int))); - QObject::connect(bar_, SIGNAL(iconSizeChanged(QSize)), - this, SLOT(setIconSize(QSize))); - - owner_.setLayoutDialog(this); - updateContents(true); -} - - -void GuiLayoutBox::setFilter(QString const & s) -{ - bool enabled = view()->updatesEnabled(); - view()->setUpdatesEnabled(false); - - // remember old selection - int sel = currentIndex(); - if (sel != -1) - lastSel_ = filterModel_->mapToSource(filterModel_->index(sel, 0)).row(); - - filter_ = s; - filterModel_->setFilterRegExp(charFilterRegExp(filter_)); - countCategories(); - - // 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()) { - // call QComboBox::showPopup. But set the inShowPopup_ flag to switch on - // the hack in the item delegate to make space for the headers. - // We do not call our implementation of showPopup because that - // would reset the filter again. This is only needed if the user clicks - // on the QComboBox. - LASSERT(!inShowPopup_, /**/); - inShowPopup_ = true; - QComboBox::showPopup(); - inShowPopup_ = false; - - // The item delegate hack is off again. So trigger a relayout of the popup. - filterModel_->triggerLayoutChange(); - - if (!s.isEmpty()) - owner_.message(bformat(_("Filtering layouts with \"%1$s\". " - "Press ESC to remove filter."), - qstring_to_ucs4(s))); - else - owner_.message(_("Enter characters to filter the layout list.")); - } - - view()->setUpdatesEnabled(enabled); -} - - -void GuiLayoutBox::countCategories() -{ - int n = filterModel_->rowCount(); - visibleCategories_ = 0; - if (n == 0 || !lyxrc.group_layouts) - return; - - // skip the "Standard" category - QString prevCat = model_->index(0, 2).data().toString(); - - // count categories - for (int i = 0; i < n; ++i) { - QString cat = filterModel_->index(i, 2).data().toString(); - if (cat != prevCat) - ++visibleCategories_; - prevCat = cat; - } -} - - -QString GuiLayoutBox::charFilterRegExp(QString const & filter) -{ - QString re; - for (int i = 0; i < filter.length(); ++i) { - QChar c = filter[i]; - if (c.isLower()) - re += ".*[" + QRegExp::escape(c) + QRegExp::escape(c.toUpper()) + "]"; - else - re += ".*" + QRegExp::escape(c); - } - return re; -} - - -void GuiLayoutBox::resetFilter() -{ - setFilter(QString()); -} - - -void GuiLayoutBox::showPopup() -{ - owner_.message(_("Enter characters to filter the layout list.")); - - bool enabled = view()->updatesEnabled(); - view()->setUpdatesEnabled(false); - - resetFilter(); - - // call QComboBox::showPopup. But set the inShowPopup_ flag to switch on - // the hack in the item delegate to make space for the headers. - LASSERT(!inShowPopup_, /**/); - inShowPopup_ = true; - QComboBox::showPopup(); - inShowPopup_ = false; - - // The item delegate hack is off again. So trigger a relayout of the popup. - filterModel_->triggerLayoutChange(); - - view()->setUpdatesEnabled(enabled); -} - - -bool GuiLayoutBox::eventFilter(QObject * o, QEvent * e) -{ - if (e->type() != QEvent::KeyPress) - return QComboBox::eventFilter(o, e); - - QKeyEvent * ke = static_cast(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::setIconSize(QSize size) -{ -#ifdef Q_WS_MACX - bool small = size.height() < 20; - setAttribute(Qt::WA_MacSmallSize, small); - setAttribute(Qt::WA_MacNormalSize, !small); -#else - (void)size; // suppress warning -#endif -} - - -void GuiLayoutBox::set(docstring const & layout) -{ - resetFilter(); - - if (!text_class_) - return; - - Layout const & lay = (*text_class_)[layout]; - QString newLayout = toqstr(lay.name()); - - // If the layout is obsolete, use the new one instead. - docstring const & obs = lay.obsoleted_by(); - if (!obs.empty()) - newLayout = toqstr(obs); - - int const curItem = currentIndex(); - QModelIndex const mindex = - filterModel_->mapToSource(filterModel_->index(curItem, 1)); - QString const & currentLayout = model_->itemFromIndex(mindex)->text(); - if (newLayout == currentLayout) { - LYXERR(Debug::GUI, "Already had " << newLayout << " selected."); - return; - } - - QList r = model_->findItems(newLayout, Qt::MatchExactly, 1); - if (r.empty()) { - LYXERR0("Trying to select non existent layout type " << newLayout); - return; - } - - setCurrentIndex(filterModel_->mapFromSource(r.first()->index()).row()); -} - - -void GuiLayoutBox::addItemSort(docstring const & item, docstring const & category, - bool sorted, bool sortedByCat, bool unknown) -{ - QString qitem = toqstr(item); - // FIXME This is wrong for RTL, I'd suppose. - QString titem = toqstr(translateIfPossible(item) + - (unknown ? _(" (unknown)") : from_ascii(""))); - QString qcat = toqstr(translateIfPossible(category)); - - QList row; - row.append(new QStandardItem(titem)); - row.append(new QStandardItem(qitem)); - row.append(new QStandardItem(qcat)); - - // the first entry is easy - int const end = model_->rowCount(); - if (end == 0) { - model_->appendRow(row); - return; - } - - // find category - int i = 0; - if (sortedByCat) { - while (i < end && model_->item(i, 2)->text() != qcat) - ++i; - } - - // skip the Standard layout - if (i == 0) - ++i; - - // the simple unsorted case - if (!sorted) { - if (sortedByCat) { - // jump to the end of the category group - while (i < end && model_->item(i, 2)->text() == qcat) - ++i; - model_->insertRow(i, row); - } else - model_->appendRow(row); - return; - } - - // find row to insert the item, after the separator if it exists - if (i < end) { - // find alphabetic position - while (i != end - && model_->item(i, 0)->text().localeAwareCompare(titem) < 0 - && (!sortedByCat || model_->item(i, 2)->text() == qcat)) - ++i; - } - - model_->insertRow(i, row); -} - - -void GuiLayoutBox::updateContents(bool reset) -{ - resetFilter(); - - Buffer const * buffer = owner_.buffer(); - if (!buffer) { - model_->clear(); - setEnabled(false); - text_class_ = 0; - inset_ = 0; - return; - } - - // 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().innerText()->inset()); - if (!reset && text_class_ == text_class && inset_ == inset) { - set(owner_.view()->cursor().innerParagraph().layout().name()); - return; - } - - inset_ = inset; - text_class_ = text_class; - - model_->clear(); - DocumentClass::const_iterator lit = text_class_->begin(); - DocumentClass::const_iterator len = text_class_->end(); - - for (; lit != len; ++lit) { - docstring const & name = lit->name(); - bool const useEmpty = inset_->forcePlainLayout() || inset_->usePlainLayout(); - // if this inset requires the empty layout, we skip the default - // layout - if (name == text_class_->defaultLayoutName() && inset_ && useEmpty) - continue; - // if it doesn't require the empty layout, we skip it - if (name == text_class_->plainLayoutName() && inset_ && !useEmpty) - continue; - // obsoleted layouts are skipped as well - if (!lit->obsoleted_by().empty()) - continue; - addItemSort(name, lit->category(), lyxrc.sort_layouts, - lyxrc.group_layouts, lit->isUnknown()); - } - - set(owner_.view()->cursor().innerParagraph().layout().name()); - countCategories(); - - // needed to recalculate size hint - hide(); - setMinimumWidth(sizeHint().width()); - setEnabled(!buffer->isReadonly() && - lyx::getStatus(FuncRequest(LFUN_LAYOUT)).enabled()); - show(); -} - - -void GuiLayoutBox::selected(int index) -{ - // get selection - QModelIndex mindex = filterModel_->mapToSource(filterModel_->index(index, 1)); - docstring layoutName = qstring_to_ucs4(model_->itemFromIndex(mindex)->text()); - owner_.setFocus(); - - if (!text_class_) { - updateContents(false); - resetFilter(); - return; - } - - // find corresponding text class - if (text_class_->hasLayout(layoutName)) { - FuncRequest const func(LFUN_LAYOUT, layoutName, FuncRequest::TOOLBAR); - theLyXFunc().setLyXView(&owner_); - lyx::dispatch(func); - updateContents(false); - resetFilter(); - return; - } - LYXERR0("ERROR (layoutSelected): layout " << layoutName << " not found!"); -} - - - -///////////////////////////////////////////////////////////////////// -// -// GuiToolbar -// -///////////////////////////////////////////////////////////////////// - - GuiToolbar::GuiToolbar(ToolbarInfo const & tbinfo, GuiView & owner) : QToolBar(toqstr(tbinfo.gui_name), &owner), visibility_(0), allowauto_(false), owner_(owner), layout_(0), command_buffer_(0), @@ -863,7 +226,7 @@ void GuiToolbar::add(ToolbarItem const & item) addSeparator(); break; case ToolbarItem::LAYOUTS: - layout_ = new GuiLayoutBox(this, owner_); + layout_ = new LayoutBox(this, owner_); addWidget(layout_); break; case ToolbarItem::MINIBUFFER: diff --git a/src/frontends/qt4/GuiToolbar.h b/src/frontends/qt4/GuiToolbar.h index 5c0ea4196e..3802e4adbc 100644 --- a/src/frontends/qt4/GuiToolbar.h +++ b/src/frontends/qt4/GuiToolbar.h @@ -18,15 +18,10 @@ #include "Session.h" -#include -#include #include #include #include -class QSortFilterProxyModel; -class QStandardItemModel; - namespace lyx { class DocumentClass; @@ -39,76 +34,10 @@ class GuiCommandBuffer; class GuiLayoutFilterModel; class GuiToolbar; class GuiView; -class LayoutItemDelegate; +class LayoutBox; class ToolbarInfo; class ToolbarItem; -class GuiLayoutBox : public QComboBox -{ - Q_OBJECT -public: - GuiLayoutBox(GuiToolbar * bar, GuiView &); - - /// select the right layout in the combobox. - void set(docstring const & layout); - /// Populate the layout combobox. - void updateContents(bool reset); - /// Add Item to Layout box according to sorting settings from preferences - void addItemSort(docstring const & item, docstring const & category, - bool sorted, bool sortedByCat, bool unknown); - - /// - void showPopup(); - - /// - bool eventFilter(QObject * o, QEvent * e); - /// - QString const & filter() { return filter_; } - -private Q_SLOTS: - /// - void selected(int index); - /// - void setIconSize(QSize size); - -private: - friend class LayoutItemDelegate; - - /// - void resetFilter(); - /// - void setFilter(QString const & s); - /// - QString charFilterRegExp(QString const & filter); - /// - void countCategories(); - - /// - GuiView & owner_; - /// - GuiToolbar * bar_; - /// - DocumentClass const * text_class_; - /// - Inset const * inset_; - - /// the layout model: 1st column translated, 2nd column raw layout name - QStandardItemModel * model_; - /// the proxy model filtering \c model_ - GuiLayoutFilterModel * filterModel_; - /// the (model-) index of the last successful selection - int lastSel_; - /// the character filter - QString filter_; - /// - LayoutItemDelegate * layoutItemDelegate_; - /// - unsigned visibleCategories_; - /// - bool inShowPopup_; -}; - - class MenuButton : public QToolButton { Q_OBJECT @@ -193,7 +122,7 @@ private: /// GuiView & owner_; /// - GuiLayoutBox * layout_; + LayoutBox * layout_; /// GuiCommandBuffer * command_buffer_; /// diff --git a/src/frontends/qt4/GuiView.cpp b/src/frontends/qt4/GuiView.cpp index 2ac313b417..d933786088 100644 --- a/src/frontends/qt4/GuiView.cpp +++ b/src/frontends/qt4/GuiView.cpp @@ -25,6 +25,7 @@ #include "GuiKeySymbol.h" #include "GuiToc.h" #include "GuiToolbar.h" +#include "LayoutBox.h" #include "Menus.h" #include "TocModel.h" @@ -266,7 +267,7 @@ public: * FIXME: replace that with a proper model so that we are not limited * to only one dialog. */ - GuiLayoutBox * layout_; + LayoutBox * layout_; /// map open_insets_; @@ -1067,7 +1068,7 @@ void GuiView::removeWorkArea(GuiWorkArea * wa) } -void GuiView::setLayoutDialog(GuiLayoutBox * layout) +void GuiView::setLayoutDialog(LayoutBox * layout) { d.layout_ = layout; } diff --git a/src/frontends/qt4/GuiView.h b/src/frontends/qt4/GuiView.h index ed12b53bf4..079a46fb7f 100644 --- a/src/frontends/qt4/GuiView.h +++ b/src/frontends/qt4/GuiView.h @@ -36,7 +36,7 @@ class Cursor; namespace frontend { class Dialog; -class GuiLayoutBox; +class LayoutBox; class GuiToolbar; class GuiWorkArea; class TabWorkArea; @@ -82,7 +82,7 @@ public: bool dispatch(FuncRequest const & cmd); /// - void setLayoutDialog(GuiLayoutBox *); + void setLayoutDialog(LayoutBox *); /// \return the buffer currently shown in this window Buffer * buffer(); diff --git a/src/frontends/qt4/LayoutBox.cpp b/src/frontends/qt4/LayoutBox.cpp new file mode 100644 index 0000000000..99053451e5 --- /dev/null +++ b/src/frontends/qt4/LayoutBox.cpp @@ -0,0 +1,732 @@ +/** + * \file qt4/LayoutBox.cpp + * This file is part of LyX, the document processor. + * Licence details can be found in the file COPYING. + * + * \author Lars Gullik Bjønnes + * \author John Levon + * \author Jean-Marc Lasgouttes + * \author Angus Leeming + * \author Stefan Schimanski + * \author Abdelrazak Younes + * + * Full author contact details are available in file CREDITS. + */ + +#include + +#include "LayoutBox.h" + +#include "Action.h" +#include "GuiView.h" +#include "qt_helpers.h" +#include "Toolbars.h" + +#include "Buffer.h" +#include "BufferParams.h" +#include "BufferView.h" +#include "Cursor.h" +#include "FuncRequest.h" +#include "FuncStatus.h" +#include "Layout.h" +#include "LyXFunc.h" +#include "LyXRC.h" +#include "Paragraph.h" +#include "TextClass.h" + +#include "insets/InsetText.h" + +#include "support/debug.h" +#include "support/filetools.h" +#include "support/gettext.h" +#include "support/lstrings.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "support/lassert.h" + +using namespace std; +using namespace lyx::support; + +namespace lyx { +namespace frontend { + + +class LayoutItemDelegate : public QItemDelegate { +public: + /// + explicit LayoutItemDelegate(LayoutBox * layout) + : QItemDelegate(layout), layout_(layout) + {} + /// + void paint(QPainter * painter, QStyleOptionViewItem const & option, + QModelIndex const & index) const; + /// + void drawDisplay(QPainter * painter, QStyleOptionViewItem const & opt, + const QRect & /*rect*/, const QString & text ) const; + /// + QSize sizeHint(QStyleOptionViewItem const & opt, + QModelIndex const & index) const; + +private: + /// + void drawCategoryHeader(QPainter * painter, QStyleOptionViewItem const & opt, + QString const & category) const; + /// + QString underlineFilter(QString const & s) const; + /// + LayoutBox * layout_; +}; + + +class GuiLayoutFilterModel : public QSortFilterProxyModel { +public: + /// + GuiLayoutFilterModel(QObject * parent = 0) + : QSortFilterProxyModel(parent) + {} + + /// + void triggerLayoutChange() + { + layoutAboutToBeChanged(); + layoutChanged(); + } +}; + + +///////////////////////////////////////////////////////////////////// +// +// LayoutBox::Private +// +///////////////////////////////////////////////////////////////////// + +struct LayoutBox::Private +{ + Private(LayoutBox * parent, GuiView & gv) : p(parent), owner_(gv), + lastSel_(-1), visibleCategories_(0), inShowPopup_(false), + layoutItemDelegate_(new LayoutItemDelegate(parent)), + // set the layout model with two columns + // 1st: translated layout names + // 2nd: raw layout names + model_(new QStandardItemModel(0, 2, p)), + filterModel_(new GuiLayoutFilterModel(p)) + { + filterModel_->setSourceModel(model_); + } + + void resetFilter() { setFilter(QString()); } + /// + void setFilter(QString const & s); + /// + void countCategories(); + /// + LayoutBox * p; + /// + GuiView & owner_; + /// + DocumentClass const * text_class_; + /// + Inset const * inset_; + + /// the layout model: 1st column translated, 2nd column raw layout name + QStandardItemModel * model_; + /// the proxy model filtering \c model_ + GuiLayoutFilterModel * filterModel_; + /// the (model-) index of the last successful selection + int lastSel_; + /// the character filter + QString filter_; + /// + LayoutItemDelegate * layoutItemDelegate_; + /// + unsigned visibleCategories_; + /// + bool inShowPopup_; +}; + + +static QString category(QAbstractItemModel const & model, int row) +{ + return model.data(model.index(row, 2), Qt::DisplayRole).toString(); +} + + +static int headerHeight(QStyleOptionViewItem const & opt) +{ + return opt.fontMetrics.height() * 8 / 10; +} + + +void LayoutItemDelegate::paint(QPainter * painter, QStyleOptionViewItem const & option, + QModelIndex const & index) const +{ + QStyleOptionViewItem opt = option; + + // default background + painter->fillRect(opt.rect, opt.palette.color(QPalette::Base)); + + // category header? + if (lyxrc.group_layouts) { + QSortFilterProxyModel const * model = + static_cast(index.model()); + + QString stdCat = category(*model->sourceModel(), 0); + QString cat = category(*index.model(), index.row()); + + // not the standard layout and not the same as in the previous line? + if (stdCat != cat + && (index.row() == 0 || cat != category(*index.model(), index.row() - 1))) { + painter->save(); + + // draw unselected background + QStyle::State state = opt.state; + opt.state = opt.state & ~QStyle::State_Selected; + drawBackground(painter, opt, index); + opt.state = state; + + // draw category header + drawCategoryHeader(painter, opt, + category(*index.model(), index.row())); + + // move rect down below header + opt.rect.setTop(opt.rect.top() + headerHeight(opt)); + + painter->restore(); + } + } + + QItemDelegate::paint(painter, opt, index); +} + + +void LayoutItemDelegate::drawDisplay(QPainter * painter, QStyleOptionViewItem const & opt, + const QRect & /*rect*/, const QString & text ) const +{ + QString utext = underlineFilter(text); + + // 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(utext); + + QTextFrameFormat fmt = doc.rootFrame()->frameFormat(); + fmt.setMargin(0); + doc.rootFrame()->setFrameFormat(fmt); + + painter->translate(opt.rect.x() + 5, + opt.rect.y() + (opt.rect.height() - opt.fontMetrics.height()) / 2); + doc.documentLayout()->draw(painter, context); + painter->restore(); +} + + +QSize LayoutItemDelegate::sizeHint(QStyleOptionViewItem const & opt, + QModelIndex const & index) const +{ + QSortFilterProxyModel const * model = + static_cast(index.model()); + QSize size = QItemDelegate::sizeHint(opt, index); + + /// QComboBox uses the first row height to estimate the + /// complete popup height during QComboBox::showPopup(). + /// To avoid scrolling we have to sneak in space for the headers. + /// So we tweak this value accordingly. It's not nice, but the + /// only possible way it seems. + if (lyxrc.group_layouts && index.row() == 0 && layout_->d->inShowPopup_) { + int itemHeight = size.height(); + + // we have to show \c cats many headers: + unsigned cats = layout_->d->visibleCategories_; + + // and we have \c n items to distribute the needed space over + unsigned n = layout_->model()->rowCount(); + + // so the needed average height (rounded upwards) is: + size.setHeight((headerHeight(opt) * cats + itemHeight * n + n - 1) / n); + return size; + } + + // Add space for the category headers here? + // Not for the standard layout though. + QString stdCat = category(*model->sourceModel(), 0); + QString cat = category(*index.model(), index.row()); + if (lyxrc.group_layouts && stdCat != cat + && (index.row() == 0 || cat != category(*index.model(), index.row() - 1))) { + size.setHeight(size.height() + headerHeight(opt)); + } + + return size; +} + + +void LayoutItemDelegate::drawCategoryHeader(QPainter * painter, QStyleOptionViewItem const & opt, + QString const & category) const +{ + // slightly blended color + QColor lcol = opt.palette.text().color(); + lcol.setAlpha(127); + painter->setPen(lcol); + + // set 80% scaled, bold font + QFont font = opt.font; + font.setBold(true); + font.setWeight(QFont::Black); + font.setPointSize(opt.font.pointSize() * 8 / 10); + painter->setFont(font); + + // draw the centered text + QFontMetrics fm(font); + int w = fm.width(category); + int x = opt.rect.x() + (opt.rect.width() - w) / 2; + int y = opt.rect.y() + fm.ascent(); + int left = x; + int right = x + w; + painter->drawText(x, y, category); + + // the vertical position of the line: middle of lower case chars + int ymid = y - 1 - fm.xHeight() / 2; // -1 for the baseline + + // draw the horizontal line + if (!category.isEmpty()) { + painter->drawLine(opt.rect.x(), ymid, left - 1, ymid); + painter->drawLine(right + 1, ymid, opt.rect.right(), ymid); + } else + painter->drawLine(opt.rect.x(), ymid, opt.rect.right(), ymid); +} + + +QString LayoutItemDelegate::underlineFilter(QString const & s) const +{ + QString const & f = layout_->filter(); + if (f.isEmpty()) + return s; + + // step through data item and put "(x)" for every matching character + QString r; + int lastp = -1; + layout_->filter(); + for (int i = 0; i < f.length(); ++i) { + int p = s.indexOf(f[i], lastp + 1, Qt::CaseInsensitive); + LASSERT(p != -1, /**/); + if (lastp == p - 1 && lastp != -1) { + // remove ")" and append "x)" + r = r.left(r.length() - 4) + s[p] + ""; + } else { + // append "(x)" + r += s.mid(lastp + 1, p - lastp - 1); + r += QString("") + s[p] + ""; + } + lastp = p; + } + r += s.mid(lastp + 1); + return r; +} + + +static QString charFilterRegExp(QString const & filter) +{ + QString re; + for (int i = 0; i < filter.length(); ++i) { + QChar c = filter[i]; + if (c.isLower()) + re += ".*[" + QRegExp::escape(c) + QRegExp::escape(c.toUpper()) + "]"; + else + re += ".*" + QRegExp::escape(c); + } + return re; +} + + +void LayoutBox::Private::setFilter(QString const & s) +{ + bool enabled = p->view()->updatesEnabled(); + p->view()->setUpdatesEnabled(false); + + // remember old selection + int sel = p->currentIndex(); + if (sel != -1) + lastSel_ = filterModel_->mapToSource(filterModel_->index(sel, 0)).row(); + + filter_ = s; + filterModel_->setFilterRegExp(charFilterRegExp(filter_)); + countCategories(); + + // restore old selection + if (lastSel_ != -1) { + QModelIndex i = filterModel_->mapFromSource(model_->index(lastSel_, 0)); + if (i.isValid()) + p->setCurrentIndex(i.row()); + } + + // Workaround to resize to content size + // FIXME: There must be a better way. The QComboBox::AdjustToContents) + // does not help. + if (p->view()->isVisible()) { + // call QComboBox::showPopup. But set the inShowPopup_ flag to switch on + // the hack in the item delegate to make space for the headers. + // We do not call our implementation of showPopup because that + // would reset the filter again. This is only needed if the user clicks + // on the QComboBox. + LASSERT(!inShowPopup_, /**/); + inShowPopup_ = true; + p->showPopup(); + inShowPopup_ = false; + + // The item delegate hack is off again. So trigger a relayout of the popup. + filterModel_->triggerLayoutChange(); + + if (!s.isEmpty()) + owner_.message(bformat(_("Filtering layouts with \"%1$s\". " + "Press ESC to remove filter."), + qstring_to_ucs4(s))); + else + owner_.message(_("Enter characters to filter the layout list.")); + } + + p->view()->setUpdatesEnabled(enabled); +} + + +LayoutBox::LayoutBox(QToolBar * bar, GuiView & owner) + : d(new Private(this, owner)) +{ + setSizeAdjustPolicy(QComboBox::AdjustToContents); + setFocusPolicy(Qt::ClickFocus); + setMinimumWidth(sizeHint().width()); + setMaxVisibleItems(100); + + setModel(d->filterModel_); + + // for the filtering we have to intercept characters + view()->installEventFilter(this); + view()->setItemDelegateForColumn(0, d->layoutItemDelegate_); + + QObject::connect(this, SIGNAL(activated(int)), + this, SLOT(selected(int))); + QObject::connect(bar, SIGNAL(iconSizeChanged(QSize)), + this, SLOT(setIconSize(QSize))); + + d->owner_.setLayoutDialog(this); + updateContents(true); +} + + +void LayoutBox::Private::countCategories() +{ + int n = filterModel_->rowCount(); + visibleCategories_ = 0; + if (n == 0 || !lyxrc.group_layouts) + return; + + // skip the "Standard" category + QString prevCat = model_->index(0, 2).data().toString(); + + // count categories + for (int i = 0; i < n; ++i) { + QString cat = filterModel_->index(i, 2).data().toString(); + if (cat != prevCat) + ++visibleCategories_; + prevCat = cat; + } +} + + +void LayoutBox::showPopup() +{ + d->owner_.message(_("Enter characters to filter the layout list.")); + + bool enabled = view()->updatesEnabled(); + view()->setUpdatesEnabled(false); + + d->resetFilter(); + + // call QComboBox::showPopup. But set the inShowPopup_ flag to switch on + // the hack in the item delegate to make space for the headers. + LASSERT(!d->inShowPopup_, /**/); + d->inShowPopup_ = true; + QComboBox::showPopup(); + d->inShowPopup_ = false; + + // The item delegate hack is off again. So trigger a relayout of the popup. + d->filterModel_->triggerLayoutChange(); + + view()->setUpdatesEnabled(enabled); +} + + +bool LayoutBox::eventFilter(QObject * o, QEvent * e) +{ + if (e->type() != QEvent::KeyPress) + return QComboBox::eventFilter(o, e); + + QKeyEvent * ke = static_cast(e); + bool modified = (ke->modifiers() == Qt::ControlModifier) + || (ke->modifiers() == Qt::AltModifier) + || (ke->modifiers() == Qt::MetaModifier); + + switch (ke->key()) { + case Qt::Key_Escape: + if (!modified && !d->filter_.isEmpty()) { + d->resetFilter(); + return true; + } + break; + case Qt::Key_Backspace: + if (!modified) { + // cut off one character + d->setFilter(d->filter_.left(d->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 + d->setFilter(d->filter_ + s); + return true; + } + break; + } + + return QComboBox::eventFilter(o, e); +} + + +void LayoutBox::setIconSize(QSize size) +{ +#ifdef Q_WS_MACX + bool small = size.height() < 20; + setAttribute(Qt::WA_MacSmallSize, small); + setAttribute(Qt::WA_MacNormalSize, !small); +#else + (void)size; // suppress warning +#endif +} + + +void LayoutBox::set(docstring const & layout) +{ + d->resetFilter(); + + if (!d->text_class_) + return; + + Layout const & lay = (*d->text_class_)[layout]; + QString newLayout = toqstr(lay.name()); + + // If the layout is obsolete, use the new one instead. + docstring const & obs = lay.obsoleted_by(); + if (!obs.empty()) + newLayout = toqstr(obs); + + int const curItem = currentIndex(); + QModelIndex const mindex = + d->filterModel_->mapToSource(d->filterModel_->index(curItem, 1)); + QString const & currentLayout = d->model_->itemFromIndex(mindex)->text(); + if (newLayout == currentLayout) { + LYXERR(Debug::GUI, "Already had " << newLayout << " selected."); + return; + } + + QList r = d->model_->findItems(newLayout, Qt::MatchExactly, 1); + if (r.empty()) { + LYXERR0("Trying to select non existent layout type " << newLayout); + return; + } + + setCurrentIndex(d->filterModel_->mapFromSource(r.first()->index()).row()); +} + + +void LayoutBox::addItemSort(docstring const & item, docstring const & category, + bool sorted, bool sortedByCat, bool unknown) +{ + QString qitem = toqstr(item); + // FIXME This is wrong for RTL, I'd suppose. + QString titem = toqstr(translateIfPossible(item) + + (unknown ? _(" (unknown)") : from_ascii(""))); + QString qcat = toqstr(translateIfPossible(category)); + + QList row; + row.append(new QStandardItem(titem)); + row.append(new QStandardItem(qitem)); + row.append(new QStandardItem(qcat)); + + // the first entry is easy + int const end = d->model_->rowCount(); + if (end == 0) { + d->model_->appendRow(row); + return; + } + + // find category + int i = 0; + if (sortedByCat) { + while (i < end && d->model_->item(i, 2)->text() != qcat) + ++i; + } + + // skip the Standard layout + if (i == 0) + ++i; + + // the simple unsorted case + if (!sorted) { + if (sortedByCat) { + // jump to the end of the category group + while (i < end && d->model_->item(i, 2)->text() == qcat) + ++i; + d->model_->insertRow(i, row); + } else + d->model_->appendRow(row); + return; + } + + // find row to insert the item, after the separator if it exists + if (i < end) { + // find alphabetic position + while (i != end + && d->model_->item(i, 0)->text().localeAwareCompare(titem) < 0 + && (!sortedByCat || d->model_->item(i, 2)->text() == qcat)) + ++i; + } + + d->model_->insertRow(i, row); +} + + +void LayoutBox::updateContents(bool reset) +{ + d->resetFilter(); + + Buffer const * buffer = d->owner_.buffer(); + if (!buffer) { + d->model_->clear(); + setEnabled(false); + d->text_class_ = 0; + d->inset_ = 0; + return; + } + + // 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 = + &(d->owner_.view()->cursor().innerText()->inset()); + if (!reset && d->text_class_ == text_class && d->inset_ == inset) { + set(d->owner_.view()->cursor().innerParagraph().layout().name()); + return; + } + + d->inset_ = inset; + d->text_class_ = text_class; + + d->model_->clear(); + DocumentClass::const_iterator lit = d->text_class_->begin(); + DocumentClass::const_iterator len = d->text_class_->end(); + + for (; lit != len; ++lit) { + docstring const & name = lit->name(); + bool const useEmpty = d->inset_->forcePlainLayout() || d->inset_->usePlainLayout(); + // if this inset requires the empty layout, we skip the default + // layout + if (name == d->text_class_->defaultLayoutName() && d->inset_ && useEmpty) + continue; + // if it doesn't require the empty layout, we skip it + if (name == d->text_class_->plainLayoutName() && d->inset_ && !useEmpty) + continue; + // obsoleted layouts are skipped as well + if (!lit->obsoleted_by().empty()) + continue; + addItemSort(name, lit->category(), lyxrc.sort_layouts, + lyxrc.group_layouts, lit->isUnknown()); + } + + set(d->owner_.view()->cursor().innerParagraph().layout().name()); + d->countCategories(); + + // needed to recalculate size hint + hide(); + setMinimumWidth(sizeHint().width()); + setEnabled(!buffer->isReadonly() && + lyx::getStatus(FuncRequest(LFUN_LAYOUT)).enabled()); + show(); +} + + +void LayoutBox::selected(int index) +{ + // get selection + QModelIndex mindex = d->filterModel_->mapToSource( + d->filterModel_->index(index, 1)); + docstring layoutName = qstring_to_ucs4( + d->model_->itemFromIndex(mindex)->text()); + d->owner_.setFocus(); + + if (!d->text_class_) { + updateContents(false); + d->resetFilter(); + return; + } + + // find corresponding text class + if (d->text_class_->hasLayout(layoutName)) { + FuncRequest const func(LFUN_LAYOUT, layoutName, FuncRequest::TOOLBAR); + theLyXFunc().setLyXView(&d->owner_); + lyx::dispatch(func); + updateContents(false); + d->resetFilter(); + return; + } + LYXERR0("ERROR (layoutSelected): layout " << layoutName << " not found!"); +} + + +QString const & LayoutBox::filter() const +{ + return d->filter_; +} + +} // namespace frontend +} // namespace lyx + +#include "moc_LayoutBox.cpp" diff --git a/src/frontends/qt4/LayoutBox.h b/src/frontends/qt4/LayoutBox.h new file mode 100644 index 0000000000..e520e3c0a8 --- /dev/null +++ b/src/frontends/qt4/LayoutBox.h @@ -0,0 +1,72 @@ +// -*- C++ -*- +/** + * \file LayoutBox.h + * This file is part of LyX, the document processor. + * Licence details can be found in the file COPYING. + * + * \author Lars Gullik Bjønnes + * \author John Levon + * \author Jean-Marc Lasgouttes + * \author Angus Leeming + * \author Abdelrazak Younes + * + * Full author contact details are available in file CREDITS. + */ + +#ifndef LYX_LAYOUT_BOX_H +#define LYX_LAYOUT_BOX_H + +#include "support/strfwd.h" + +#include + +class QToolBar; + +namespace lyx { + +class DocumentClass; +class Inset; + +namespace frontend { + +class GuiView; +class LayoutItemDelegate; + +class LayoutBox : public QComboBox +{ + Q_OBJECT +public: + LayoutBox(QToolBar *, GuiView &); + + /// select the right layout in the combobox. + void set(docstring const & layout); + /// Populate the layout combobox. + void updateContents(bool reset); + /// Add Item to Layout box according to sorting settings from preferences + void addItemSort(docstring const & item, docstring const & category, + bool sorted, bool sortedByCat, bool unknown); + + /// + void showPopup(); + + /// + bool eventFilter(QObject * o, QEvent * e); + /// + QString const & filter() const; + +private Q_SLOTS: + /// + void selected(int index); + /// + void setIconSize(QSize size); + +private: + friend class LayoutItemDelegate; + struct Private; + Private * const d; +}; + +} // namespace frontend +} // namespace lyx + +#endif // LYX_LAYOUT_BOX_H diff --git a/src/frontends/qt4/Makefile.am b/src/frontends/qt4/Makefile.am index 4f1fc0c2ed..600046334a 100644 --- a/src/frontends/qt4/Makefile.am +++ b/src/frontends/qt4/Makefile.am @@ -132,6 +132,7 @@ SOURCEFILES = \ LengthCombo.cpp \ LyXFileDialog.cpp \ LaTeXHighlighter.cpp \ + LayoutBox.cpp \ Menus.cpp \ PanelStack.cpp \ qt_helpers.cpp \ @@ -227,6 +228,7 @@ MOCHEADER = \ GuiWrap.h \ IconPalette.h \ InsertTableWidget.h \ + LayoutBox.h \ LengthCombo.h \ LyXFileDialog.h \ Menus.h \