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