]> git.lyx.org Git - lyx.git/blob - src/frontends/qt4/LayoutBox.cpp
Use <cstdint> instead of <boost/cstdint.hpp>
[lyx.git] / src / frontends / qt4 / LayoutBox.cpp
1 /**
2  * \file qt4/LayoutBox.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Lars Gullik Bjønnes
7  * \author John Levon
8  * \author Jean-Marc Lasgouttes
9  * \author Angus Leeming
10  * \author Stefan Schimanski
11  * \author Abdelrazak Younes
12  *
13  * Full author contact details are available in file CREDITS.
14  */
15
16 #include <config.h>
17
18 #include "LayoutBox.h"
19
20 #include "GuiView.h"
21 #include "qt_helpers.h"
22
23 #include "Buffer.h"
24 #include "BufferParams.h"
25 #include "BufferView.h"
26 #include "Cursor.h"
27 #include "DocumentClassPtr.h"
28 #include "FuncRequest.h"
29 #include "FuncStatus.h"
30 #include "LyX.h"
31 #include "LyXRC.h"
32 #include "Paragraph.h"
33 #include "TextClass.h"
34
35 #include "insets/InsetText.h"
36
37 #include "support/debug.h"
38 #include "support/gettext.h"
39 #include "support/lassert.h"
40 #include "support/lstrings.h"
41
42 #include <QAbstractTextDocumentLayout>
43 #include <QHeaderView>
44 #include <QItemDelegate>
45 #include <QPainter>
46 #include <QRegExp>
47 #include <QSortFilterProxyModel>
48 #include <QStandardItemModel>
49 #include <QTextFrame>
50
51 using namespace std;
52 using namespace lyx::support;
53
54 namespace lyx {
55 namespace frontend {
56
57
58 class LayoutItemDelegate : public QItemDelegate {
59 public:
60         ///
61         explicit LayoutItemDelegate(LayoutBox * layout)
62                 : QItemDelegate(layout), layout_(layout)
63         {}
64         ///
65         void paint(QPainter * painter, QStyleOptionViewItem const & option,
66                 QModelIndex const & index) const;
67         ///
68         void drawDisplay(QPainter * painter, QStyleOptionViewItem const & opt,
69                 const QRect & /*rect*/, const QString & text ) const;
70         ///
71         QSize sizeHint(QStyleOptionViewItem const & opt,
72                 QModelIndex const & index) const;
73
74 private:
75         ///
76         void drawCategoryHeader(QPainter * painter, QStyleOptionViewItem const & opt,
77                 QString const & category) const;
78         ///
79         QString underlineFilter(QString const & s) const;
80         ///
81         LayoutBox * layout_;
82 };
83
84
85 class GuiLayoutFilterModel : public QSortFilterProxyModel {
86 public:
87         ///
88         GuiLayoutFilterModel(QObject * parent = 0)
89                 : QSortFilterProxyModel(parent)
90         {}
91
92         ///
93         void triggerLayoutChange()
94         {
95                 layoutAboutToBeChanged();
96                 layoutChanged();
97         }
98 };
99
100
101 /////////////////////////////////////////////////////////////////////
102 //
103 // LayoutBox::Private
104 //
105 /////////////////////////////////////////////////////////////////////
106
107 class LayoutBox::Private
108 {
109         /// noncopyable
110         Private(Private const &);
111         void operator=(Private const &);
112 public:
113         Private(LayoutBox * parent, GuiView & gv) : p(parent), owner_(gv),
114                 inset_(0),
115                 // set the layout model with two columns
116                 // 1st: translated layout names
117                 // 2nd: raw layout names
118                 model_(new QStandardItemModel(0, 2, p)),
119                 filterModel_(new GuiLayoutFilterModel(p)),
120                 lastSel_(-1),
121                 layoutItemDelegate_(new LayoutItemDelegate(parent)),
122                 visibleCategories_(0)
123         {
124                 filterModel_->setSourceModel(model_);
125         }
126
127         void resetFilter() { setFilter(QString()); }
128         ///
129         void setFilter(QString const & s);
130         ///
131         void countCategories();
132         ///
133         LayoutBox * p;
134         ///
135         GuiView & owner_;
136         ///
137         DocumentClassConstPtr text_class_;
138         ///
139         Inset const * inset_;
140
141         /// the layout model: 1st column translated, 2nd column raw layout name
142         QStandardItemModel * model_;
143         /// the proxy model filtering \c model_
144         GuiLayoutFilterModel * filterModel_;
145         /// the (model-) index of the last successful selection
146         int lastSel_;
147         /// the character filter
148         QString filter_;
149         ///
150         LayoutItemDelegate * layoutItemDelegate_;
151         ///
152         unsigned visibleCategories_;
153 };
154
155
156 static QString category(QAbstractItemModel const & model, int row)
157 {
158         return model.data(model.index(row, 2), Qt::DisplayRole).toString();
159 }
160
161
162 static int headerHeight(QStyleOptionViewItem const & opt)
163 {
164         return opt.fontMetrics.height() * 8 / 10;
165 }
166
167
168 void LayoutItemDelegate::paint(QPainter * painter, QStyleOptionViewItem const & option,
169                                                            QModelIndex const & index) const
170 {
171         QStyleOptionViewItem opt = option;
172
173         // default background
174         painter->fillRect(opt.rect, opt.palette.color(QPalette::Base));
175
176         // category header?
177         if (lyxrc.group_layouts) {
178                 QSortFilterProxyModel const * model =
179                         static_cast<QSortFilterProxyModel const *>(index.model());
180
181                 QString stdCat = category(*model->sourceModel(), 0);
182                 QString cat = category(*index.model(), index.row());
183
184                 // not the standard layout and not the same as in the previous line?
185                 if (stdCat != cat
186                         && (index.row() == 0 || cat != category(*index.model(), index.row() - 1))) {
187                         painter->save();
188
189                         // draw unselected background
190                         QStyle::State state = opt.state;
191                         opt.state = opt.state & ~QStyle::State_Selected;
192                         drawBackground(painter, opt, index);
193                         opt.state = state;
194
195                         // draw category header
196                         drawCategoryHeader(painter, opt,
197                                 category(*index.model(), index.row()));
198
199                         // move rect down below header
200                         opt.rect.setTop(opt.rect.top() + headerHeight(opt));
201
202                         painter->restore();
203                 }
204         }
205
206         QItemDelegate::paint(painter, opt, index);
207 }
208
209
210 void LayoutItemDelegate::drawDisplay(QPainter * painter, QStyleOptionViewItem const & opt,
211                                                                          const QRect & /*rect*/, const QString & text ) const
212 {
213         QString utext = underlineFilter(text);
214
215         // Draw the rich text.
216         painter->save();
217         QColor col = opt.palette.text().color();
218         if (opt.state & QStyle::State_Selected)
219                 col = opt.palette.highlightedText().color();
220         QAbstractTextDocumentLayout::PaintContext context;
221         context.palette.setColor(QPalette::Text, col);
222
223         QTextDocument doc;
224         doc.setDefaultFont(opt.font);
225         doc.setHtml(utext);
226
227         QTextFrameFormat fmt = doc.rootFrame()->frameFormat();
228         fmt.setMargin(0);
229         doc.rootFrame()->setFrameFormat(fmt);
230
231         painter->translate(opt.rect.x() + 5,
232                 opt.rect.y() + (opt.rect.height() - opt.fontMetrics.height()) / 2);
233         doc.documentLayout()->draw(painter, context);
234         painter->restore();
235 }
236
237
238 QSize LayoutItemDelegate::sizeHint(QStyleOptionViewItem const & opt,
239                                                                    QModelIndex const & index) const
240 {
241         QSize size = QItemDelegate::sizeHint(opt, index);
242         if (!lyxrc.group_layouts)
243                 return size;
244
245         // Add space for the category headers.
246         QSortFilterProxyModel const * const model =
247                 static_cast<QSortFilterProxyModel const *>(index.model());
248         QString const stdCat = category(*model->sourceModel(), 0);
249         QString const cat = category(*index.model(), index.row());
250
251         // There is no header for the stuff at the top.
252         if (stdCat == cat)
253                 return size;
254
255         if (index.row() == 0 || cat != category(*index.model(), index.row() - 1))
256                 size.setHeight(size.height() + headerHeight(opt));
257         return size;
258 }
259
260
261 void LayoutItemDelegate::drawCategoryHeader(QPainter * painter, QStyleOptionViewItem const & opt,
262                                                                                         QString const & category) const
263 {
264         // slightly blended color
265         QColor lcol = opt.palette.text().color();
266         lcol.setAlpha(127);
267         painter->setPen(lcol);
268
269         // set 80% scaled, bold font
270         QFont font = opt.font;
271         font.setBold(true);
272         font.setWeight(QFont::Black);
273         font.setPointSize(opt.font.pointSize() * 8 / 10);
274         painter->setFont(font);
275
276         // draw the centered text
277         QFontMetrics fm(font);
278         int w = fm.width(category);
279         int x = opt.rect.x() + (opt.rect.width() - w) / 2;
280         int y = opt.rect.y() + fm.ascent();
281         int left = x;
282         int right = x + w;
283         painter->drawText(x, y, category);
284
285         // the vertical position of the line: middle of lower case chars
286         int ymid = y - 1 - fm.xHeight() / 2; // -1 for the baseline
287
288         // draw the horizontal line
289         if (!category.isEmpty()) {
290                 painter->drawLine(opt.rect.x(), ymid, left - 1, ymid);
291                 painter->drawLine(right + 1, ymid, opt.rect.right(), ymid);
292         } else
293                 painter->drawLine(opt.rect.x(), ymid, opt.rect.right(), ymid);
294 }
295
296
297 QString LayoutItemDelegate::underlineFilter(QString const & s) const
298 {
299         QString const & f = layout_->filter();
300         if (f.isEmpty())
301                 return s;
302         QString r(s);
303         QRegExp pattern(charFilterRegExpC(f));
304         r.replace(pattern, "<u><b>\\1</b></u>");
305         return r;
306 }
307
308
309 void LayoutBox::Private::setFilter(QString const & s)
310 {
311         // exit early if nothing has to be done
312         if (filter_ == s)
313                 return;
314
315         bool enabled = p->view()->updatesEnabled();
316         p->view()->setUpdatesEnabled(false);
317
318         // remember old selection
319         int sel = p->currentIndex();
320         if (sel != -1)
321                 lastSel_ = filterModel_->mapToSource(filterModel_->index(sel, 0)).row();
322
323         filter_ = s;
324         filterModel_->setFilterRegExp(charFilterRegExp(filter_));
325         countCategories();
326
327         // restore old selection
328         if (lastSel_ != -1) {
329                 QModelIndex i = filterModel_->mapFromSource(model_->index(lastSel_, 0));
330                 if (i.isValid())
331                         p->setCurrentIndex(i.row());
332         }
333
334         if (p->view()->isVisible()) {
335                 p->QComboBox::showPopup();
336                 if (!s.isEmpty())
337                         owner_.message(bformat(_("Filtering layouts with \"%1$s\". "
338                                                  "Press ESC to remove filter."),
339                                                qstring_to_ucs4(s)));
340                 else
341                         owner_.message(_("Enter characters to filter the layout list."));
342         }
343
344         p->view()->setUpdatesEnabled(enabled);
345 }
346
347
348 LayoutBox::LayoutBox(GuiView & owner)
349         : d(new Private(this, owner))
350 {
351         setSizeAdjustPolicy(QComboBox::AdjustToContents);
352         setFocusPolicy(Qt::ClickFocus);
353         setMinimumWidth(sizeHint().width());
354         setMaxVisibleItems(100);
355
356         setModel(d->filterModel_);
357
358         // for the filtering we have to intercept characters
359         view()->installEventFilter(this);
360         view()->setItemDelegateForColumn(0, d->layoutItemDelegate_);
361
362         QObject::connect(this, SIGNAL(activated(int)),
363                 this, SLOT(selected(int)));
364
365         updateContents(true);
366 }
367
368
369 LayoutBox::~LayoutBox() {
370         delete d;
371 }
372
373
374 void LayoutBox::Private::countCategories()
375 {
376         int n = filterModel_->rowCount();
377         visibleCategories_ = 0;
378         if (n == 0 || !lyxrc.group_layouts)
379                 return;
380
381         // skip the "Standard" category
382         QString prevCat = model_->index(0, 2).data().toString();
383
384         // count categories
385         for (int i = 0; i < n; ++i) {
386                 QString cat = filterModel_->index(i, 2).data().toString();
387                 if (cat != prevCat)
388                         ++visibleCategories_;
389                 prevCat = cat;
390         }
391 }
392
393
394 void LayoutBox::showPopup()
395 {
396         d->owner_.message(_("Enter characters to filter the layout list."));
397
398         bool enabled = view()->updatesEnabled();
399         view()->setUpdatesEnabled(false);
400         d->resetFilter();
401         QComboBox::showPopup();
402         view()->setUpdatesEnabled(enabled);
403 }
404
405
406 bool LayoutBox::eventFilter(QObject * o, QEvent * e)
407 {
408         if (e->type() != QEvent::KeyPress)
409                 return QComboBox::eventFilter(o, e);
410
411         QKeyEvent * ke = static_cast<QKeyEvent*>(e);
412         bool modified = (ke->modifiers() == Qt::ControlModifier)
413                 || (ke->modifiers() == Qt::AltModifier)
414                 || (ke->modifiers() == Qt::MetaModifier);
415
416         switch (ke->key()) {
417         case Qt::Key_Escape:
418                 if (!modified && !d->filter_.isEmpty()) {
419                         d->resetFilter();
420                         return true;
421                 }
422                 break;
423         case Qt::Key_Backspace:
424                 if (!modified) {
425                         // cut off one character
426                         d->setFilter(d->filter_.left(d->filter_.length() - 1));
427                 }
428                 break;
429         default:
430                 if (modified || ke->text().isEmpty())
431                         break;
432                 // find chars for the filter string
433                 QString s;
434                 for (int i = 0; i < ke->text().length(); ++i) {
435                         QChar c = ke->text()[i];
436                         if (c.isLetterOrNumber()
437                             || c.isSymbol()
438                             || c.isPunct()
439                             || c.category() == QChar::Separator_Space) {
440                                 s += c;
441                         }
442                 }
443                 if (!s.isEmpty()) {
444                         // append new chars to the filter string
445                         d->setFilter(d->filter_ + s);
446                         return true;
447                 }
448                 break;
449         }
450
451         return QComboBox::eventFilter(o, e);
452 }
453
454
455 void LayoutBox::setIconSize(QSize size)
456 {
457 #ifdef Q_OS_MAC
458         bool small = size.height() < 20;
459         setAttribute(Qt::WA_MacSmallSize, small);
460         setAttribute(Qt::WA_MacNormalSize, !small);
461 #else
462         (void)size; // suppress warning
463 #endif
464 }
465
466
467 void LayoutBox::set(docstring const & layout)
468 {
469         d->resetFilter();
470
471         if (!d->text_class_)
472                 return;
473
474         if (!d->text_class_->hasLayout(layout))
475                 return;
476
477         Layout const & lay = (*d->text_class_)[layout];
478         QString newLayout = toqstr(lay.name());
479
480         // If the layout is obsolete, use the new one instead.
481         docstring const & obs = lay.obsoleted_by();
482         if (!obs.empty())
483                 newLayout = toqstr(obs);
484
485         int const curItem = currentIndex();
486         QModelIndex const mindex =
487                 d->filterModel_->mapToSource(d->filterModel_->index(curItem, 1));
488         QString const & currentLayout = d->model_->itemFromIndex(mindex)->text();
489         if (newLayout == currentLayout) {
490                 LYXERR(Debug::GUI, "Already had " << newLayout << " selected.");
491                 return;
492         }
493
494         QList<QStandardItem *> r = d->model_->findItems(newLayout, Qt::MatchExactly, 1);
495         if (r.empty()) {
496                 LYXERR0("Trying to select non existent layout type " << newLayout);
497                 return;
498         }
499
500         setCurrentIndex(d->filterModel_->mapFromSource(r.first()->index()).row());
501 }
502
503
504 void LayoutBox::addItemSort(docstring const & item, docstring const & category,
505         bool sorted, bool sortedByCat, bool unknown)
506 {
507         QString qitem = toqstr(item);
508         docstring const loc_item = translateIfPossible(item);
509         QString titem = unknown ? toqstr(bformat(_("%1$s (unknown)"), loc_item))
510                                 : toqstr(loc_item);
511         QString qcat = toqstr(translateIfPossible(category));
512
513         QList<QStandardItem *> row;
514         row.append(new QStandardItem(titem));
515         row.append(new QStandardItem(qitem));
516         row.append(new QStandardItem(qcat));
517
518         // the first entry is easy
519         int const end = d->model_->rowCount();
520         if (end == 0) {
521                 d->model_->appendRow(row);
522                 return;
523         }
524
525         // find category
526         int i = 0;
527         if (sortedByCat) {
528                 while (i < end && d->model_->item(i, 2)->text() != qcat)
529                         ++i;
530         }
531
532         // skip the Standard layout
533         if (i == 0)
534                 ++i;
535
536         // the simple unsorted case
537         if (!sorted) {
538                 if (sortedByCat) {
539                         // jump to the end of the category group
540                         while (i < end && d->model_->item(i, 2)->text() == qcat)
541                                 ++i;
542                         d->model_->insertRow(i, row);
543                 } else
544                         d->model_->appendRow(row);
545                 return;
546         }
547
548         // find row to insert the item, after the separator if it exists
549         if (i < end) {
550                 // find alphabetic position
551                 while (i != end
552                        && d->model_->item(i, 0)->text().localeAwareCompare(titem) < 0
553                        && (!sortedByCat || d->model_->item(i, 2)->text() == qcat))
554                         ++i;
555         }
556
557         d->model_->insertRow(i, row);
558 }
559
560
561 void LayoutBox::updateContents(bool reset)
562 {
563         d->resetFilter();
564         BufferView const * bv = d->owner_.currentBufferView();
565         if (!bv) {
566                 d->model_->clear();
567                 setEnabled(false);
568                 setMinimumWidth(sizeHint().width());
569                 d->text_class_.reset();
570                 d->inset_ = 0;
571                 return;
572         }
573         // we'll only update the layout list if the text class has changed
574         // or we've moved from one inset to another
575         DocumentClassConstPtr text_class = bv->buffer().params().documentClassPtr();
576         Inset const * inset = &(bv->cursor().innerText()->inset());
577         if (!reset && d->text_class_ == text_class && d->inset_ == inset) {
578                 set(bv->cursor().innerParagraph().layout().name());
579                 return;
580         }
581
582         d->inset_ = inset;
583         d->text_class_ = text_class;
584
585         d->model_->clear();
586         DocumentClass::const_iterator lit = d->text_class_->begin();
587         DocumentClass::const_iterator len = d->text_class_->end();
588
589         for (; lit != len; ++lit) {
590                 docstring const & name = lit->name();
591                 bool const useEmpty = d->inset_->forcePlainLayout() || d->inset_->usePlainLayout();
592                 // if this inset requires the empty layout, we skip the default
593                 // layout
594                 if (name == d->text_class_->defaultLayoutName() && d->inset_ && useEmpty)
595                         continue;
596                 // if it doesn't require the empty layout, we skip it
597                 if (name == d->text_class_->plainLayoutName() && d->inset_ && !useEmpty)
598                         continue;
599                 // obsoleted layouts are skipped as well
600                 if (!lit->obsoleted_by().empty())
601                         continue;
602                 addItemSort(name, lit->category(), lyxrc.sort_layouts,
603                                 lyxrc.group_layouts, lit->isUnknown());
604         }
605
606         set(d->owner_.currentBufferView()->cursor().innerParagraph().layout().name());
607         d->countCategories();
608
609         setMinimumWidth(sizeHint().width());
610         setEnabled(!bv->buffer().isReadonly() &&
611                 lyx::getStatus(FuncRequest(LFUN_LAYOUT)).enabled());
612 }
613
614
615 void LayoutBox::selected(int index)
616 {
617         // get selection
618         QModelIndex mindex = d->filterModel_->mapToSource(
619                 d->filterModel_->index(index, 1));
620         docstring layoutName = qstring_to_ucs4(
621                 d->model_->itemFromIndex(mindex)->text());
622         d->owner_.setFocus();
623
624         if (!d->text_class_) {
625                 updateContents(false);
626                 d->resetFilter();
627                 return;
628         }
629
630         // find corresponding text class
631         if (d->text_class_->hasLayout(layoutName)) {
632                 FuncRequest const func(LFUN_LAYOUT, layoutName, FuncRequest::TOOLBAR);
633                 lyx::dispatch(func);
634                 updateContents(false);
635                 d->resetFilter();
636                 return;
637         }
638         LYXERR0("ERROR (layoutSelected): layout " << layoutName << " not found!");
639 }
640
641
642 QString const & LayoutBox::filter() const
643 {
644         return d->filter_;
645 }
646
647 } // namespace frontend
648 } // namespace lyx
649
650 #include "moc_LayoutBox.cpp"