]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/LayoutBox.cpp
Remove a workaround that seems to be useless since Qt 4.8
[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         d->owner_.message(_("Enter characters to filter the layout list."));
405
406         bool enabled = view()->updatesEnabled();
407         view()->setUpdatesEnabled(false);
408         d->resetFilter();
409         QComboBox::showPopup();
410         view()->setUpdatesEnabled(enabled);
411 }
412
413
414 bool LayoutBox::eventFilter(QObject * o, QEvent * e)
415 {
416         if (e->type() != QEvent::KeyPress)
417                 return QComboBox::eventFilter(o, e);
418
419         QKeyEvent * ke = static_cast<QKeyEvent*>(e);
420         bool modified = (ke->modifiers() == Qt::ControlModifier)
421                 || (ke->modifiers() == Qt::AltModifier)
422                 || (ke->modifiers() == Qt::MetaModifier);
423
424         switch (ke->key()) {
425         case Qt::Key_Escape:
426                 if (!modified && !d->filter_.isEmpty()) {
427                         d->resetFilter();
428                         return true;
429                 }
430                 break;
431         case Qt::Key_Backspace:
432                 if (!modified) {
433                         // cut off one character
434                         d->setFilter(d->filter_.left(d->filter_.length() - 1));
435                 }
436                 break;
437         default:
438                 if (modified || ke->text().isEmpty())
439                         break;
440                 // find chars for the filter string
441                 QString s;
442                 for (int i = 0; i < ke->text().length(); ++i) {
443                         QChar c = ke->text()[i];
444                         if (c.isLetterOrNumber()
445                             || c.isSymbol()
446                             || c.isPunct()
447                             || c.category() == QChar::Separator_Space) {
448                                 s += c;
449                         }
450                 }
451                 if (!s.isEmpty()) {
452                         // append new chars to the filter string
453                         d->setFilter(d->filter_ + s);
454                         return true;
455                 }
456                 break;
457         }
458
459         return QComboBox::eventFilter(o, e);
460 }
461
462
463 void LayoutBox::setIconSize(QSize size)
464 {
465 #ifdef Q_OS_MAC
466         bool small = size.height() < 20;
467         setAttribute(Qt::WA_MacSmallSize, small);
468         setAttribute(Qt::WA_MacNormalSize, !small);
469 #else
470         (void)size; // suppress warning
471 #endif
472 }
473
474
475 void LayoutBox::set(docstring const & layout)
476 {
477         d->resetFilter();
478
479         if (!d->text_class_)
480                 return;
481
482         if (!d->text_class_->hasLayout(layout))
483                 return;
484
485         Layout const & lay = (*d->text_class_)[layout];
486         QString newLayout = toqstr(lay.name());
487
488         // If the layout is obsolete, use the new one instead.
489         docstring const & obs = lay.obsoleted_by();
490         if (!obs.empty())
491                 newLayout = toqstr(obs);
492
493         int const curItem = currentIndex();
494         QModelIndex const mindex =
495                 d->filterModel_->mapToSource(d->filterModel_->index(curItem, 1));
496         QString const & currentLayout = d->model_->itemFromIndex(mindex)->text();
497         if (newLayout == currentLayout) {
498                 LYXERR(Debug::GUI, "Already had " << newLayout << " selected.");
499                 return;
500         }
501
502         QList<QStandardItem *> r = d->model_->findItems(newLayout, Qt::MatchExactly, 1);
503         if (r.empty()) {
504                 LYXERR0("Trying to select non existent layout type " << newLayout);
505                 return;
506         }
507
508         setCurrentIndex(d->filterModel_->mapFromSource(r.first()->index()).row());
509 }
510
511
512 void LayoutBox::addItemSort(docstring const & item, docstring const & category,
513         bool sorted, bool sortedByCat, bool unknown)
514 {
515         QString qitem = toqstr(item);
516         docstring const loc_item = translateIfPossible(item);
517         QString titem = unknown ? toqstr(bformat(_("%1$s (unknown)"), loc_item))
518                                 : toqstr(loc_item);
519         QString qcat = toqstr(translateIfPossible(category));
520
521         QList<QStandardItem *> row;
522         row.append(new QStandardItem(titem));
523         row.append(new QStandardItem(qitem));
524         row.append(new QStandardItem(qcat));
525
526         // the first entry is easy
527         int const end = d->model_->rowCount();
528         if (end == 0) {
529                 d->model_->appendRow(row);
530                 return;
531         }
532
533         // find category
534         int i = 0;
535         if (sortedByCat) {
536                 while (i < end && d->model_->item(i, 2)->text() != qcat)
537                         ++i;
538         }
539
540         // skip the Standard layout
541         if (i == 0)
542                 ++i;
543
544         // the simple unsorted case
545         if (!sorted) {
546                 if (sortedByCat) {
547                         // jump to the end of the category group
548                         while (i < end && d->model_->item(i, 2)->text() == qcat)
549                                 ++i;
550                         d->model_->insertRow(i, row);
551                 } else
552                         d->model_->appendRow(row);
553                 return;
554         }
555
556         // find row to insert the item, after the separator if it exists
557         if (i < end) {
558                 // find alphabetic position
559                 while (i != end
560                        && d->model_->item(i, 0)->text().localeAwareCompare(titem) < 0
561                        && (!sortedByCat || d->model_->item(i, 2)->text() == qcat))
562                         ++i;
563         }
564
565         d->model_->insertRow(i, row);
566 }
567
568
569 void LayoutBox::updateContents(bool reset)
570 {
571         d->resetFilter();
572         BufferView const * bv = d->owner_.currentBufferView();
573         if (!bv) {
574                 d->model_->clear();
575                 setEnabled(false);
576                 setMinimumWidth(sizeHint().width());
577                 d->text_class_.reset();
578                 d->inset_ = nullptr;
579                 return;
580         }
581         // we'll only update the layout list if the text class has changed
582         // or we've moved from one inset to another
583         DocumentClassConstPtr text_class = bv->buffer().params().documentClassPtr();
584         Inset const * inset = &(bv->cursor().innerText()->inset());
585         if (!reset && d->text_class_ == text_class && d->inset_ == inset) {
586                 set(bv->cursor().innerParagraph().layout().name());
587                 return;
588         }
589
590         d->inset_ = inset;
591         d->text_class_ = text_class;
592
593         d->model_->clear();
594         DocumentClass::const_iterator lit = d->text_class_->begin();
595         DocumentClass::const_iterator len = d->text_class_->end();
596
597         for (; lit != len; ++lit) {
598                 docstring const & name = lit->name();
599                 bool const useEmpty = d->inset_->forcePlainLayout() || d->inset_->usePlainLayout();
600                 // if this inset requires the empty layout, we skip the default
601                 // layout
602                 if (name == d->text_class_->defaultLayoutName() && d->inset_ && useEmpty)
603                         continue;
604                 // if it doesn't require the empty layout, we skip it
605                 if (name == d->text_class_->plainLayoutName() && d->inset_ && !useEmpty)
606                         continue;
607                 // obsoleted layouts are skipped as well
608                 if (!lit->obsoleted_by().empty())
609                         continue;
610                 addItemSort(name, lit->category(), lyxrc.sort_layouts,
611                                 lyxrc.group_layouts, lit->isUnknown());
612         }
613
614         set(d->owner_.currentBufferView()->cursor().innerParagraph().layout().name());
615         d->countCategories();
616
617         setMinimumWidth(sizeHint().width());
618         setEnabled(!bv->buffer().isReadonly() &&
619                 lyx::getStatus(FuncRequest(LFUN_LAYOUT)).enabled());
620 }
621
622
623 void LayoutBox::selected(int index)
624 {
625         // get selection
626         QModelIndex mindex = d->filterModel_->mapToSource(
627                 d->filterModel_->index(index, 1));
628         docstring layoutName = qstring_to_ucs4(
629                 d->model_->itemFromIndex(mindex)->text());
630         d->owner_.setFocus();
631
632         if (!d->text_class_) {
633                 updateContents(false);
634                 d->resetFilter();
635                 return;
636         }
637
638         // find corresponding text class
639         if (d->text_class_->hasLayout(layoutName)) {
640                 FuncRequest const func(LFUN_LAYOUT, layoutName, FuncRequest::TOOLBAR);
641                 lyx::dispatch(func);
642                 updateContents(false);
643                 d->resetFilter();
644                 return;
645         }
646         LYXERR0("ERROR (layoutSelected): layout " << layoutName << " not found!");
647 }
648
649
650 QString const & LayoutBox::filter() const
651 {
652         return d->filter_;
653 }
654
655 } // namespace frontend
656 } // namespace lyx
657
658 #include "moc_LayoutBox.cpp"