]> git.lyx.org Git - features.git/blob - src/frontends/qt/CategorizedCombo.cpp
Guard against possible referencing null.
[features.git] / src / frontends / qt / CategorizedCombo.cpp
1 /**
2  * \file qt/CategorizedCombo.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 Jürgen Spitzmüller
12  * \author Abdelrazak Younes
13  *
14  * Full author contact details are available in file CREDITS.
15  */
16
17 #include <config.h>
18
19 #include "CategorizedCombo.h"
20
21 #include "qt_helpers.h"
22
23 #include "support/debug.h"
24 #include "support/gettext.h"
25 #include "support/lassert.h"
26 #include "support/lstrings.h"
27 #include "support/qstring_helpers.h"
28
29 #include <QAbstractTextDocumentLayout>
30 #include <QComboBox>
31 #include <QHeaderView>
32 #include <QItemDelegate>
33 #include <QPainter>
34 #include <QSortFilterProxyModel>
35 #include <QStandardItemModel>
36 #include <QTextFrame>
37 #if QT_VERSION >= 0x060000
38 #include <QtCore5Compat/QRegExp>
39 #endif
40
41 using namespace lyx::support;
42
43 namespace lyx {
44 namespace frontend {
45
46
47 class CCItemDelegate : public QItemDelegate {
48 public:
49         ///
50         explicit CCItemDelegate(CategorizedCombo * cc)
51                 : QItemDelegate(cc), cc_(cc)
52         {}
53         ///
54         void paint(QPainter * painter, QStyleOptionViewItem const & option,
55                 QModelIndex const & index) const override;
56         ///
57         void drawDisplay(QPainter * painter, QStyleOptionViewItem const & opt,
58                 const QRect & /*rect*/, const QString & text ) const override;
59         ///
60         QSize sizeHint(QStyleOptionViewItem const & opt,
61                 QModelIndex const & index) const override;
62
63 private:
64         ///
65         void drawCategoryHeader(QPainter * painter, QStyleOptionViewItem const & opt,
66                 QString const & category) const;
67         ///
68         QString underlineFilter(QString const & s) const;
69         ///
70         CategorizedCombo * cc_;
71 };
72
73
74 class CCFilterModel : public QSortFilterProxyModel {
75 public:
76         ///
77         CCFilterModel(QObject * parent = nullptr)
78                 : QSortFilterProxyModel(parent)
79         {}
80 };
81
82
83 /////////////////////////////////////////////////////////////////////
84 //
85 // CategorizedCombo::Private
86 //
87 /////////////////////////////////////////////////////////////////////
88
89 struct CategorizedCombo::Private
90 {
91         Private(CategorizedCombo * parent) : p(parent),
92                 // set the model with four columns
93                 // 1st: translated item names
94                 // 2nd: raw names
95                 // 3rd: category
96                 // 4th: availability (bool)
97                 model_(new QStandardItemModel(0, 4, p)),
98                 filterModel_(new CCFilterModel(p)),
99                 lastSel_(-1),
100                 CCItemDelegate_(new CCItemDelegate(parent)),
101                 visibleCategories_(0), inShowPopup_(false)
102         {
103                 filterModel_->setSourceModel(model_);
104         }
105
106         void resetFilter() { setFilter(QString()); }
107         ///
108         void setFilter(QString const & s);
109         ///
110         void countCategories();
111         ///
112         CategorizedCombo * p;
113
114         /** the layout model:
115          * 1st column: translated GUI name,
116          * 2nd column: raw item name,
117          * 3rd column: category,
118          * 4th column: availability
119         **/
120         QStandardItemModel * model_;
121         /// the proxy model filtering \c model_
122         CCFilterModel * filterModel_;
123         /// the (model-) index of the last successful selection
124         int lastSel_;
125         /// the character filter
126         QString filter_;
127         ///
128         CCItemDelegate * CCItemDelegate_;
129         ///
130         unsigned visibleCategories_;
131         ///
132         bool inShowPopup_;
133 };
134
135
136 static QString categoryCC(QAbstractItemModel const & model, int row)
137 {
138         return model.data(model.index(row, 2), Qt::DisplayRole).toString();
139 }
140
141
142 static int headerHeightCC(QStyleOptionViewItem const & opt)
143 {
144         return opt.fontMetrics.height();
145 }
146
147
148 void CCItemDelegate::paint(QPainter * painter, QStyleOptionViewItem const & option,
149                            QModelIndex const & index) const
150 {
151         QStyleOptionViewItem opt = option;
152
153         // default background
154         painter->fillRect(opt.rect, opt.palette.color(QPalette::Base));
155
156         QString cat = categoryCC(*index.model(), index.row());
157
158         // not the same as in the previous line?
159         if (cc_->d->visibleCategories_ > 0
160             && (index.row() == 0 || cat != categoryCC(*index.model(), index.row() - 1))) {
161                 painter->save();
162
163                 // draw unselected background
164                 QStyle::State state = opt.state;
165                 opt.state = opt.state & ~QStyle::State_Selected;
166                 drawBackground(painter, opt, index);
167                 opt.state = state;
168
169                 // draw category header
170                 drawCategoryHeader(painter, opt,
171                         categoryCC(*index.model(), index.row()));
172
173                 // move rect down below header
174                 opt.rect.setTop(opt.rect.top() + headerHeightCC(opt));
175
176                 painter->restore();
177         }
178
179         QItemDelegate::paint(painter, opt, index);
180 }
181
182
183 void CCItemDelegate::drawDisplay(QPainter * painter, QStyleOptionViewItem const & opt,
184                                  const QRect & /*rect*/, const QString & text) const
185 {
186         QString utext = underlineFilter(text);
187
188         // Draw the rich text.
189         painter->save();
190         QColor col = opt.palette.text().color();
191         // grey out unavailable items
192         if (text.startsWith(qt_("Unavailable:")))
193                 col = opt.palette.color(QPalette::Disabled, QPalette::Text);
194         if (opt.state & QStyle::State_Selected)
195                 col = opt.palette.highlightedText().color();
196         QAbstractTextDocumentLayout::PaintContext context;
197         context.palette.setColor(QPalette::Text, col);
198
199         QTextDocument doc;
200         doc.setDefaultFont(opt.font);
201         doc.setHtml(utext);
202
203         QTextFrameFormat fmt = doc.rootFrame()->frameFormat();
204         fmt.setMargin(0);
205         doc.rootFrame()->setFrameFormat(fmt);
206
207         painter->translate(opt.rect.x() + 5,
208                 opt.rect.y() + (opt.rect.height() - opt.fontMetrics.height()) / 2);
209         doc.documentLayout()->draw(painter, context);
210         painter->restore();
211 }
212
213
214 QSize CCItemDelegate::sizeHint(QStyleOptionViewItem const & opt,
215                                QModelIndex const & index) const
216 {
217         QSize size = QItemDelegate::sizeHint(opt, index);
218
219         /// QComboBox uses the first row height to estimate the
220         /// complete popup height during QComboBox::showPopup().
221         /// To avoid scrolling we have to sneak in space for the headers.
222         /// So we tweak this value accordingly. It's not nice, but the
223         /// only possible way it seems.
224         // Add space for the category headers here
225         QString cat = categoryCC(*index.model(), index.row());
226         if (index.row() == 0 || cat != categoryCC(*index.model(), index.row() - 1)) {
227                 size.setHeight(size.height() + headerHeightCC(opt));
228         }
229
230         return size;
231 }
232
233
234 void CCItemDelegate::drawCategoryHeader(QPainter * painter, QStyleOptionViewItem const & opt,
235                                         QString const & category) const
236 {
237         // slightly blended color
238         QColor lcol = opt.palette.text().color();
239         lcol.setAlpha(127);
240         painter->setPen(lcol);
241
242         // set 80% scaled, bold font
243         QFont font = opt.font;
244         font.setBold(true);
245         font.setWeight(QFont::Black);
246         font.setPointSize(opt.font.pointSize() * 8 / 10);
247         painter->setFont(font);
248
249         // draw the centered text
250         QFontMetrics fm(font);
251         int w = fm.boundingRect(category).width();
252         int x = opt.rect.x() + (opt.rect.width() - w) / 2;
253         int y = opt.rect.y() + 3 * fm.ascent() / 2;
254         int left = x;
255         int right = x + w;
256         painter->drawText(x, y, category);
257
258         // the vertical position of the line: middle of lower case chars
259         int ymid = y - 1 - fm.xHeight() / 2; // -1 for the baseline
260
261         // draw the horizontal line
262         if (!category.isEmpty()) {
263                 painter->drawLine(opt.rect.x(), ymid, left - 1, ymid);
264                 painter->drawLine(right + 1, ymid, opt.rect.right(), ymid);
265         } else
266                 painter->drawLine(opt.rect.x(), ymid, opt.rect.right(), ymid);
267 }
268
269
270 QString CCItemDelegate::underlineFilter(QString const & s) const
271 {
272         QString const & f = cc_->filter();
273         if (f.isEmpty())
274                 return s;
275         QString r(s);
276 #if QT_VERSION < 0x060000
277         QRegExp pattern(charFilterRegExpC(f));
278 #else
279         QRegularExpression pattern(charFilterRegExpC(f));
280 #endif
281         r.replace(pattern, "<u><b>\\1</b></u>");
282         return r;
283 }
284
285
286 void CategorizedCombo::Private::setFilter(QString const & s)
287 {
288         bool enabled = p->view()->updatesEnabled();
289         p->view()->setUpdatesEnabled(false);
290
291         // remember old selection
292         int sel = p->currentIndex();
293         if (sel != -1)
294                 lastSel_ = filterModel_->mapToSource(filterModel_->index(sel, 0)).row();
295
296         filter_ = s;
297 #if QT_VERSION < 0x060000
298         filterModel_->setFilterRegExp(charFilterRegExp(filter_));
299 #else
300         filterModel_->setFilterRegularExpression(charFilterRegExp(filter_));
301 #endif
302         countCategories();
303
304         // restore old selection
305         if (lastSel_ != -1) {
306                 QModelIndex i = filterModel_->mapFromSource(model_->index(lastSel_, 0));
307                 if (i.isValid())
308                         p->setCurrentIndex(i.row());
309         }
310
311         // Workaround to resize to content size
312         // FIXME: There must be a better way. The QComboBox::AdjustToContents)
313         //        does not help.
314         if (p->view()->isVisible()) {
315                 // call QComboBox::showPopup. But set the inShowPopup_ flag to switch on
316                 // the hack in the item delegate to make space for the headers.
317                 // We do not call our implementation of showPopup because that
318                 // would reset the filter again. This is only needed if the user clicks
319                 // on the QComboBox.
320                 LASSERT(!inShowPopup_, /**/);
321                 inShowPopup_ = true;
322                 p->QComboBox::showPopup();
323                 inShowPopup_ = false;
324         }
325
326         p->view()->setUpdatesEnabled(enabled);
327 }
328
329
330 CategorizedCombo::CategorizedCombo(QWidget * parent)
331         : QComboBox(parent), d(new Private(this))
332 {
333         setSizeAdjustPolicy(QComboBox::AdjustToContents);
334         setMinimumWidth(sizeHint().width());
335         setMaxVisibleItems(100);
336
337         setModel(d->filterModel_);
338
339         // for the filtering we have to intercept characters
340         view()->installEventFilter(this);
341         view()->setItemDelegateForColumn(0, d->CCItemDelegate_);
342
343         updateCombo();
344 }
345
346
347 CategorizedCombo::~CategorizedCombo() {
348         delete d;
349 }
350
351
352 void CategorizedCombo::Private::countCategories()
353 {
354         int n = filterModel_->rowCount();
355         visibleCategories_ = 0;
356         if (n == 0)
357                 return;
358
359         QString prevCat = model_->index(0, 2).data().toString();
360
361         // count categories
362         for (int i = 1; i < n; ++i) {
363                 QString cat = filterModel_->index(i, 2).data().toString();
364                 if (cat != prevCat)
365                         ++visibleCategories_;
366                 prevCat = cat;
367         }
368 }
369
370
371 void CategorizedCombo::showPopup()
372 {
373         bool enabled = view()->updatesEnabled();
374         view()->setUpdatesEnabled(false);
375
376         d->resetFilter();
377
378         // call QComboBox::showPopup. But set the inShowPopup_ flag to switch on
379         // the hack in the item delegate to make space for the headers.
380         LASSERT(!d->inShowPopup_, /**/);
381         d->inShowPopup_ = true;
382         QComboBox::showPopup();
383         d->inShowPopup_ = false;
384
385         view()->setUpdatesEnabled(enabled);
386 }
387
388
389 bool CategorizedCombo::eventFilter(QObject * o, QEvent * e)
390 {
391         if (e->type() != QEvent::KeyPress)
392                 return QComboBox::eventFilter(o, e);
393
394         QKeyEvent * ke = static_cast<QKeyEvent*>(e);
395         bool modified = (ke->modifiers() == Qt::ControlModifier)
396                 || (ke->modifiers() == Qt::AltModifier)
397                 || (ke->modifiers() == Qt::MetaModifier);
398
399         switch (ke->key()) {
400         case Qt::Key_Escape:
401                 if (!modified && !d->filter_.isEmpty()) {
402                         d->resetFilter();
403                         return true;
404                 }
405                 break;
406         case Qt::Key_Backspace:
407                 if (!modified) {
408                         // cut off one character
409                         d->setFilter(d->filter_.left(d->filter_.length() - 1));
410                 }
411                 break;
412         default:
413                 if (modified || ke->text().isEmpty())
414                         break;
415                 // find chars for the filter string
416                 QString s;
417                 for (int i = 0; i < ke->text().length(); ++i) {
418                         QChar c = ke->text()[i];
419                         if (c.isLetterOrNumber()
420                             || c.isSymbol()
421                             || c.isPunct()
422                             || c.category() == QChar::Separator_Space) {
423                                 s += c;
424                         }
425                 }
426                 if (!s.isEmpty()) {
427                         // append new chars to the filter string
428                         d->setFilter(d->filter_ + s);
429                         return true;
430                 }
431                 break;
432         }
433
434         return QComboBox::eventFilter(o, e);
435 }
436
437
438 void CategorizedCombo::setIconSize(QSize size)
439 {
440 #ifdef Q_OS_MAC
441         bool small = size.height() < 20;
442         setAttribute(Qt::WA_MacSmallSize, small);
443         setAttribute(Qt::WA_MacNormalSize, !small);
444 #else
445         (void)size; // suppress warning
446 #endif
447 }
448
449
450 bool CategorizedCombo::set(QString const & item)
451 {
452         d->resetFilter();
453
454         int const curItem = currentIndex();
455         QModelIndex const mindex =
456                 d->filterModel_->mapToSource(d->filterModel_->index(curItem, 1));
457         QString const & currentItem = d->model_->itemFromIndex(mindex)->text();
458         if (item == currentItem) {
459                 LYXERR(Debug::GUI, "Already had " << item << " selected.");
460                 return true;
461         }
462
463         QList<QStandardItem *> r = d->model_->findItems(item, Qt::MatchExactly, 1);
464         if (r.empty()) {
465                 LYXERR0("Trying to select non existent layout type " << item);
466                 return false;
467         }
468
469         setCurrentIndex(d->filterModel_->mapFromSource(r.first()->index()).row());
470         return true;
471 }
472
473
474 void CategorizedCombo::addItemSort(QString const & item, QString const & guiname,
475                                    QString const & category, QString const & tooltip,
476                                    bool sorted, bool sortedByCat, bool sortCats,
477                                    bool available)
478 {
479         QString titem = available ? guiname
480                                   : toqstr(bformat(_("Unavailable: %1$s"),
481                                                    qstring_to_ucs4(guiname)));
482         bool const uncategorized = category.isEmpty();
483         QString qcat = uncategorized ? qt_("Uncategorized") : category;
484
485         QList<QStandardItem *> row;
486         QStandardItem * gui = new QStandardItem(titem);
487         if (!tooltip.isEmpty())
488                 gui->setToolTip(tooltip);
489         row.append(gui);
490         row.append(new QStandardItem(item));
491         row.append(new QStandardItem(qcat));
492         row.append(new QStandardItem(available));
493
494         // the first entry is easy
495         int const end = d->model_->rowCount();
496         if (end == 0) {
497                 d->model_->appendRow(row);
498                 return;
499         }
500
501         // find category
502         int i = 0;
503         if (sortedByCat) {
504                 // If sortCats == true, sort categories alphabetically, uncategorized at the end.
505                 while (i < end && d->model_->item(i, 2)->text() != qcat
506                        && (!sortCats
507                            || (!uncategorized && d->model_->item(i, 2)->text().localeAwareCompare(qcat) < 0
508                                && d->model_->item(i, 2)->text() != qt_("Uncategorized"))
509                            || (uncategorized && d->model_->item(i, 2)->text() != qt_("Uncategorized"))))
510                         ++i;
511         }
512
513         // the simple unsorted case
514         if (!sorted) {
515                 if (sortedByCat) {
516                         // jump to the end of the category group
517                         while (i < end && d->model_->item(i, 2)->text() == qcat)
518                                 ++i;
519                         d->model_->insertRow(i, row);
520                 } else
521                         d->model_->appendRow(row);
522                 return;
523         }
524
525         // find row to insert the item, after the separator if it exists
526         if (i < end) {
527                 // find alphabetic position, unavailable at the end
528                 while (i != end
529                        && ((available && d->model_->item(i, 0)->text().localeAwareCompare(titem) < 0)
530                            || ((!available && d->model_->item(i, 3))
531                                || d->model_->item(i, 0)->text().localeAwareCompare(titem) < 0))
532                        && (!sortedByCat || d->model_->item(i, 2)->text() == qcat))
533                         ++i;
534         }
535
536         d->model_->insertRow(i, row);
537 }
538
539
540 QString CategorizedCombo::getData(int row) const
541 {
542         int srow = d->filterModel_->mapToSource(d->filterModel_->index(row, 1)).row();
543         return d->model_->data(d->model_->index(srow, 1), Qt::DisplayRole).toString();
544 }
545
546
547 void CategorizedCombo::reset()
548 {
549         d->resetFilter();
550         d->model_->clear();
551 }
552
553 void CategorizedCombo::resetFilter()
554 {
555         d->resetFilter();
556 }
557
558
559 void CategorizedCombo::updateCombo()
560 {
561         d->countCategories();
562
563         // needed to recalculate size hint
564         hide();
565         setMinimumWidth(sizeHint().width());
566         show();
567 }
568
569
570 QString const & CategorizedCombo::filter() const
571 {
572         return d->filter_;
573 }
574
575 } // namespace frontend
576 } // namespace lyx
577
578
579 #include "moc_CategorizedCombo.cpp"