]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/LayoutBox.cpp
Fix bug #11410.
[lyx.git] / src / frontends / qt / LayoutBox.cpp
1 /**
2  * \file qt/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 override;
67         ///
68         void drawDisplay(QPainter * painter, QStyleOptionViewItem const & opt,
69                 const QRect & /*rect*/, const QString & text ) const override;
70         ///
71         QSize sizeHint(QStyleOptionViewItem const & opt,
72                 QModelIndex const & index) const override;
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 = nullptr)
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_(nullptr),
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.boundingRect(category).width();
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 #if QT_VERSION < 0x060000
304         QRegExp pattern(charFilterRegExpC(f));
305 #else
306         QRegularExpression pattern(charFilterRegExpC(f));
307 #endif
308         r.replace(pattern, "<u><b>\\1</b></u>");
309         return r;
310 }
311
312
313 void LayoutBox::Private::setFilter(QString const & s)
314 {
315         // exit early if nothing has to be done
316         if (filter_ == s)
317                 return;
318
319         bool enabled = p->view()->updatesEnabled();
320         p->view()->setUpdatesEnabled(false);
321
322         // remember old selection
323         int sel = p->currentIndex();
324         if (sel != -1)
325                 lastSel_ = filterModel_->mapToSource(filterModel_->index(sel, 0)).row();
326
327         filter_ = s;
328 #if QT_VERSION < 0x060000
329         filterModel_->setFilterRegExp(charFilterRegExp(filter_));
330 #else
331         filterModel_->setFilterRegularExpression(charFilterRegExp(filter_));
332 #endif
333         countCategories();
334
335         // restore old selection
336         if (lastSel_ != -1) {
337                 QModelIndex i = filterModel_->mapFromSource(model_->index(lastSel_, 0));
338                 if (i.isValid())
339                         p->setCurrentIndex(i.row());
340         }
341
342         if (p->view()->isVisible()) {
343                 p->QComboBox::showPopup();
344                 if (!s.isEmpty())
345                         owner_.message(bformat(_("Filtering layouts with \"%1$s\". "
346                                                  "Press ESC to remove filter."),
347                                                qstring_to_ucs4(s)));
348                 else
349                         owner_.message(_("Enter characters to filter the layout list."));
350         }
351
352         p->view()->setUpdatesEnabled(enabled);
353 }
354
355
356 LayoutBox::LayoutBox(GuiView & owner)
357         : d(new Private(this, owner))
358 {
359         setSizeAdjustPolicy(QComboBox::AdjustToContents);
360         setFocusPolicy(Qt::ClickFocus);
361         setMinimumWidth(sizeHint().width());
362         setMaxVisibleItems(100);
363
364         setModel(d->filterModel_);
365
366         // for the filtering we have to intercept characters
367         view()->installEventFilter(this);
368         view()->setItemDelegateForColumn(0, d->layoutItemDelegate_);
369
370         QObject::connect(this, SIGNAL(activated(int)),
371                 this, SLOT(selected(int)));
372
373         updateContents(true);
374 }
375
376
377 LayoutBox::~LayoutBox() {
378         delete d;
379 }
380
381
382 void LayoutBox::Private::countCategories()
383 {
384         int n = filterModel_->rowCount();
385         visibleCategories_ = 0;
386         if (n == 0 || !lyxrc.group_layouts)
387                 return;
388
389         // skip the "Standard" category
390         QString prevCat = model_->index(0, 2).data().toString();
391
392         // count categories
393         for (int i = 0; i < n; ++i) {
394                 QString cat = filterModel_->index(i, 2).data().toString();
395                 if (cat != prevCat)
396                         ++visibleCategories_;
397                 prevCat = cat;
398         }
399 }
400
401
402 void LayoutBox::showPopup()
403 {
404         lastCurrentIndex_ = currentIndex();
405         d->owner_.message(_("Enter characters to filter the layout list."));
406
407         bool enabled = view()->updatesEnabled();
408         view()->setUpdatesEnabled(false);
409         d->resetFilter();
410         QComboBox::showPopup();
411         view()->setUpdatesEnabled(enabled);
412 }
413
414
415 bool LayoutBox::eventFilter(QObject * o, QEvent * e)
416 {
417         if (e->type() != QEvent::KeyPress)
418                 return QComboBox::eventFilter(o, e);
419
420         QKeyEvent * ke = static_cast<QKeyEvent*>(e);
421         bool modified = (ke->modifiers() == Qt::ControlModifier)
422                 || (ke->modifiers() == Qt::AltModifier)
423                 || (ke->modifiers() == Qt::MetaModifier);
424
425         switch (ke->key()) {
426         case Qt::Key_Escape:
427                 if (!modified && !d->filter_.isEmpty()) {
428                         d->resetFilter();
429                         setCurrentIndex(lastCurrentIndex_);
430                         return true;
431                 }
432                 break;
433         case Qt::Key_Backspace:
434                 if (!modified) {
435                         // cut off one character
436                         d->setFilter(d->filter_.left(d->filter_.length() - 1));
437                 }
438                 break;
439         default:
440                 if (modified || ke->text().isEmpty())
441                         break;
442                 // find chars for the filter string
443                 QString s;
444                 for (int i = 0; i < ke->text().length(); ++i) {
445                         QChar c = ke->text()[i];
446                         if (c.isLetterOrNumber()
447                             || c.isSymbol()
448                             || c.isPunct()
449                             || c.category() == QChar::Separator_Space) {
450                                 s += c;
451                         }
452                 }
453                 if (!s.isEmpty()) {
454                         // append new chars to the filter string
455                         d->setFilter(d->filter_ + s);
456                         return true;
457                 }
458                 break;
459         }
460
461         return QComboBox::eventFilter(o, e);
462 }
463
464
465 void LayoutBox::setIconSize(QSize size)
466 {
467 #ifdef Q_OS_MAC
468         bool small = size.height() < 20;
469         setAttribute(Qt::WA_MacSmallSize, small);
470         setAttribute(Qt::WA_MacNormalSize, !small);
471 #else
472         (void)size; // suppress warning
473 #endif
474 }
475
476
477 void LayoutBox::set(docstring const & layout)
478 {
479         d->resetFilter();
480
481         if (!d->text_class_)
482                 return;
483
484         if (!d->text_class_->hasLayout(layout))
485                 return;
486
487         Layout const & lay = (*d->text_class_)[layout];
488         QString newLayout = toqstr(lay.name());
489
490         // If the layout is obsolete, use the new one instead.
491         docstring const & obs = lay.obsoleted_by();
492         if (!obs.empty())
493                 newLayout = toqstr(obs);
494
495         int const curItem = currentIndex();
496         QModelIndex const mindex =
497                 d->filterModel_->mapToSource(d->filterModel_->index(curItem, 1));
498         QString const & currentLayout = d->model_->itemFromIndex(mindex)->text();
499         if (newLayout == currentLayout) {
500                 LYXERR(Debug::GUI, "Already had " << newLayout << " selected.");
501                 return;
502         }
503
504         QList<QStandardItem *> r = d->model_->findItems(newLayout, Qt::MatchExactly, 1);
505         if (r.empty()) {
506                 LYXERR0("Trying to select non existent layout type " << newLayout);
507                 return;
508         }
509
510         setCurrentIndex(d->filterModel_->mapFromSource(r.first()->index()).row());
511 }
512
513
514 void LayoutBox::addItemSort(docstring const & item, docstring const & category,
515         bool sorted, bool sortedByCat, bool unknown)
516 {
517         QString qitem = toqstr(item);
518         docstring const loc_item = translateIfPossible(item);
519         QString titem = unknown ? toqstr(bformat(_("%1$s (unknown)"), loc_item))
520                                 : toqstr(loc_item);
521         QString qcat = toqstr(translateIfPossible(category));
522
523         QList<QStandardItem *> row;
524         row.append(new QStandardItem(titem));
525         row.append(new QStandardItem(qitem));
526         row.append(new QStandardItem(qcat));
527
528         // the first entry is easy
529         int const end = d->model_->rowCount();
530         if (end == 0) {
531                 d->model_->appendRow(row);
532                 return;
533         }
534
535         // find category
536         int i = 0;
537         if (sortedByCat) {
538                 while (i < end && d->model_->item(i, 2)->text() != qcat)
539                         ++i;
540         }
541
542         // skip the Standard layout
543         if (i == 0)
544                 ++i;
545
546         // the simple unsorted case
547         if (!sorted) {
548                 if (sortedByCat) {
549                         // jump to the end of the category group
550                         while (i < end && d->model_->item(i, 2)->text() == qcat)
551                                 ++i;
552                         d->model_->insertRow(i, row);
553                 } else
554                         d->model_->appendRow(row);
555                 return;
556         }
557
558         // find row to insert the item, after the separator if it exists
559         if (i < end) {
560                 // find alphabetic position
561                 while (i != end
562                        && d->model_->item(i, 0)->text().localeAwareCompare(titem) < 0
563                        && (!sortedByCat || d->model_->item(i, 2)->text() == qcat))
564                         ++i;
565         }
566
567         d->model_->insertRow(i, row);
568 }
569
570
571 void LayoutBox::updateContents(bool reset)
572 {
573         d->resetFilter();
574         BufferView const * bv = d->owner_.currentBufferView();
575         if (!bv) {
576                 d->model_->clear();
577                 setEnabled(false);
578                 setMinimumWidth(sizeHint().width());
579                 d->text_class_.reset();
580                 d->inset_ = nullptr;
581                 return;
582         }
583         // we'll only update the layout list if the text class has changed
584         // or we've moved from one inset to another
585         DocumentClassConstPtr text_class = bv->buffer().params().documentClassPtr();
586         Inset const * inset = &(bv->cursor().innerText()->inset());
587         if (!reset && d->text_class_ == text_class && d->inset_ == inset) {
588                 set(bv->cursor().innerParagraph().layout().name());
589                 return;
590         }
591
592         d->inset_ = inset;
593         d->text_class_ = text_class;
594
595         d->model_->clear();
596         DocumentClass::const_iterator lit = d->text_class_->begin();
597         DocumentClass::const_iterator len = d->text_class_->end();
598
599         for (; lit != len; ++lit) {
600                 docstring const & name = lit->name();
601                 bool const useEmpty = d->inset_->forcePlainLayout() || d->inset_->usePlainLayout();
602                 // if this inset requires the empty layout, we skip the default
603                 // layout
604                 if (name == d->text_class_->defaultLayoutName() && d->inset_ && useEmpty)
605                         continue;
606                 // if it doesn't require the empty layout, we skip it
607                 if (name == d->text_class_->plainLayoutName() && d->inset_ && !useEmpty)
608                         continue;
609                 // obsoleted layouts are skipped as well
610                 if (!lit->obsoleted_by().empty())
611                         continue;
612                 addItemSort(name, lit->category(), lyxrc.sort_layouts,
613                                 lyxrc.group_layouts, lit->isUnknown());
614         }
615
616         set(d->owner_.currentBufferView()->cursor().innerParagraph().layout().name());
617         d->countCategories();
618
619         setMinimumWidth(sizeHint().width());
620         setEnabled(!bv->buffer().isReadonly() &&
621                 lyx::getStatus(FuncRequest(LFUN_LAYOUT)).enabled());
622 }
623
624
625 void LayoutBox::selected(int index)
626 {
627         // get selection
628         QModelIndex mindex = d->filterModel_->mapToSource(
629                 d->filterModel_->index(index, 1));
630         docstring layoutName = qstring_to_ucs4(
631                 d->model_->itemFromIndex(mindex)->text());
632         d->owner_.setFocus();
633
634         if (!d->text_class_) {
635                 updateContents(false);
636                 d->resetFilter();
637                 return;
638         }
639
640         // find corresponding text class
641         if (d->text_class_->hasLayout(layoutName)) {
642                 FuncRequest const func(LFUN_LAYOUT, layoutName, FuncRequest::TOOLBAR);
643                 lyx::dispatch(func);
644                 updateContents(false);
645                 d->resetFilter();
646                 return;
647         }
648         LYXERR0("ERROR (layoutSelected): layout " << layoutName << " not found!");
649 }
650
651
652 QString const & LayoutBox::filter() const
653 {
654         return d->filter_;
655 }
656
657 } // namespace frontend
658 } // namespace lyx
659
660 #include "moc_LayoutBox.cpp"