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