1 /**
2  * \file GuiViewSource.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author John Levon
7  * \author Bo Peng
8  * \author Abdelrazak Younes
9  *
10  * Full author contact details are available in file CREDITS.
11  */
12 
13 #include <config.h>
14 
15 #include "GuiApplication.h"
16 #include "GuiViewSource.h"
17 #include "LaTeXHighlighter.h"
18 #include "qt_helpers.h"
19 
20 #include "BufferParams.h"
21 #include "BufferView.h"
22 #include "Cursor.h"
23 #include "Format.h"
24 #include "FuncRequest.h"
25 #include "LyX.h"
26 #include "Paragraph.h"
27 #include "TexRow.h"
28 
29 #include "support/debug.h"
30 #include "support/lassert.h"
31 #include "support/docstream.h"
32 #include "support/docstring_list.h"
33 #include "support/gettext.h"
34 
35 #include <boost/crc.hpp>
36 
37 #include <QBoxLayout>
38 #include <QComboBox>
39 #include <QScrollBar>
40 #include <QSettings>
41 #include <QTextCursor>
42 #include <QTextDocument>
43 #include <QVariant>
44 
45 using namespace std;
46 
47 namespace lyx {
48 namespace frontend {
49 
ViewSourceWidget(QWidget * parent)50 ViewSourceWidget::ViewSourceWidget(QWidget * parent)
51 	:	QWidget(parent),
52 		document_(new QTextDocument(this)),
53 		highlighter_(new LaTeXHighlighter(document_))
54 {
55 	setupUi(this);
56 
57 	connect(contentsCO, SIGNAL(activated(int)),
58 		this, SLOT(contentsChanged()));
59 	connect(autoUpdateCB, SIGNAL(toggled(bool)),
60 		updatePB, SLOT(setDisabled(bool)));
61 	connect(autoUpdateCB, SIGNAL(toggled(bool)),
62 		this, SLOT(contentsChanged()));
63 	connect(masterPerspectiveCB, SIGNAL(toggled(bool)),
64 		this, SLOT(contentsChanged()));
65 	connect(updatePB, SIGNAL(clicked()),
66 		this, SIGNAL(needUpdate()));
67 	connect(outputFormatCO, SIGNAL(activated(int)),
68 		this, SLOT(setViewFormat(int)));
69 
70 	// setting a document at this point trigger an assertion in Qt
71 	// so we disable the signals here:
72 	document_->blockSignals(true);
73 	viewSourceTV->setDocument(document_);
74 	// reset selections
75 	setText();
76 	document_->blockSignals(false);
77 	viewSourceTV->setReadOnly(true);
78 	///dialog_->viewSourceTV->setAcceptRichText(false);
79 	// this is personal. I think source code should be in fixed-size font
80 	viewSourceTV->setFont(guiApp->typewriterSystemFont());
81 	// again, personal taste
82 	viewSourceTV->setWordWrapMode(QTextOption::NoWrap);
83 
84 	// catch double click events
85 	viewSourceTV->viewport()->installEventFilter(this);
86 }
87 
88 
getContent(BufferView const & view,Buffer::OutputWhat output,docstring & str,string const & format,bool master)89 void ViewSourceWidget::getContent(BufferView const & view,
90 			Buffer::OutputWhat output, docstring & str, string const & format,
91 			bool master)
92 {
93 	// get the *top* level paragraphs that contain the cursor,
94 	// or the selected text
95 	pit_type par_begin;
96 	pit_type par_end;
97 
98 	if (!view.cursor().selection()) {
99 		par_begin = view.cursor().bottom().pit();
100 		par_end = par_begin;
101 	} else {
102 		par_begin = view.cursor().selectionBegin().bottom().pit();
103 		par_end = view.cursor().selectionEnd().bottom().pit();
104 	}
105 	if (par_begin > par_end)
106 		swap(par_begin, par_end);
107 	odocstringstream ostr;
108 	texrow_ = view.buffer()
109 		.getSourceCode(ostr, format, par_begin, par_end + 1, output, master);
110 	//ensure that the last line can always be selected in its full width
111 	str = ostr.str() + "\n";
112 }
113 
114 
setText(QString const & qstr)115 bool ViewSourceWidget::setText(QString const & qstr)
116 {
117 	bool const changed = document_->toPlainText() != qstr;
118 	viewSourceTV->setExtraSelections(QList<QTextEdit::ExtraSelection>());
119 	if (changed)
120 		document_->setPlainText(qstr);
121 	return changed;
122 }
123 
124 
contentsChanged()125 void ViewSourceWidget::contentsChanged()
126 {
127 	if (autoUpdateCB->isChecked())
128 		Q_EMIT needUpdate();
129 }
130 
131 
setViewFormat(int const index)132 void ViewSourceWidget::setViewFormat(int const index)
133 {
134 	outputFormatCO->setCurrentIndex(index);
135 	string format = fromqstr(outputFormatCO->itemData(index).toString());
136 	if (view_format_ != format) {
137 		view_format_ = format;
138 		Q_EMIT needUpdate();
139 	}
140 }
141 
142 
updateDelay() const143 int ViewSourceWidget::updateDelay() const
144 {
145 	const int long_delay = 400;
146 	const int short_delay = 60;
147 	// a shorter delay if just the current paragraph is shown
148 	return (contentsCO->currentIndex() == 0) ? short_delay : long_delay;
149 }
150 
151 
scheduleUpdate()152 void GuiViewSource::scheduleUpdate()
153 {
154 	update_timer_->start(widget_->updateDelay());
155 }
156 
157 
scheduleUpdateNow()158 void GuiViewSource::scheduleUpdateNow()
159 {
160 	update_timer_->start(0);
161 }
162 
163 
realUpdateView()164 void GuiViewSource::realUpdateView()
165 {
166 	widget_->updateView(bufferview());
167 	updateTitle();
168 }
169 
170 
updateView(BufferView const * bv)171 void ViewSourceWidget::updateView(BufferView const * bv)
172 {
173 	if (!bv) {
174 		setText();
175 		setEnabled(false);
176 		return;
177 	}
178 
179 	setEnabled(true);
180 
181 	// we will try to get that much space around the cursor
182 	int const v_margin = 3;
183 	int const h_margin = 10;
184 	// we will try to preserve this
185 	int const h_scroll = viewSourceTV->horizontalScrollBar()->value();
186 
187 	Buffer::OutputWhat output = Buffer::CurrentParagraph;
188 	if (contentsCO->currentIndex() == 1)
189 		output = Buffer::FullSource;
190 	else if (contentsCO->currentIndex() == 2)
191 		output = Buffer::OnlyPreamble;
192 	else if (contentsCO->currentIndex() == 3)
193 		output = Buffer::OnlyBody;
194 
195 	docstring content;
196 	getContent(*bv, output, content, view_format_,
197 	           masterPerspectiveCB->isChecked());
198 	QString old = document_->toPlainText();
199 	QString qcontent = toqstr(content);
200 	if (guiApp->currentView()->develMode()) {
201 		// output tex<->row correspondences in the source panel if the "-dbg latex"
202 		// option is given.
203 		if (texrow_ && lyx::lyxerr.debugging(Debug::LATEX)) {
204 			QStringList list = qcontent.split(QChar('\n'));
205 			docstring_list dlist;
206 			for (QStringList::const_iterator it = list.begin(); it != list.end(); ++it)
207 				dlist.push_back(from_utf8(fromqstr(*it)));
208 			texrow_->prepend(dlist);
209 			qcontent.clear();
210 			for (docstring_list::iterator it = dlist.begin(); it != dlist.end(); ++it)
211 				qcontent += toqstr(*it) + '\n';
212 		}
213 	}
214 
215 	// prevent gotoCursor()
216 	QSignalBlocker blocker(viewSourceTV);
217 	bool const changed = setText(qcontent);
218 
219 	if (changed && !texrow_) {
220 		// position-to-row is unavailable
221 		// we jump to the first modification
222 		int length = min(old.length(), qcontent.length());
223 		int pos = 0;
224 		for (; pos < length && old.at(pos) == qcontent.at(pos); ++pos) {}
225 		QTextCursor c = QTextCursor(viewSourceTV->document());
226 		//get some space below the cursor
227 		c.setPosition(pos);
228 		c.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor,v_margin);
229 		viewSourceTV->setTextCursor(c);
230 		//get some space on the right of the cursor
231 		viewSourceTV->horizontalScrollBar()->setValue(h_scroll);
232 		c.setPosition(pos);
233 		const int block = c.blockNumber();
234 		for (int i = h_margin; i && block == c.blockNumber(); --i) {
235 			c.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor);
236 		}
237 		c.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor);
238 		viewSourceTV->setTextCursor(c);
239 		//back to the position
240 		c.setPosition(pos);
241 		//c.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor,1);
242 		viewSourceTV->setTextCursor(c);
243 
244 	} else if (texrow_) {
245 		// Use the available position-to-row conversion to highlight
246 		// the current selection in the source
247 		std::pair<int,int> rows = texrow_->rowFromCursor(bv->cursor());
248 		int const beg_row = rows.first;
249 		int const end_row = rows.second;
250 
251 		QTextCursor c = QTextCursor(viewSourceTV->document());
252 
253 		c.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor,
254 					   beg_row - 1);
255 		const int beg_sel = c.position();
256 		//get some space above the cursor
257 		c.movePosition(QTextCursor::PreviousBlock, QTextCursor::MoveAnchor,
258 					   v_margin);
259 		viewSourceTV->setTextCursor(c);
260 		c.setPosition(beg_sel, QTextCursor::MoveAnchor);
261 
262 		c.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor,
263 					   end_row - beg_row +1);
264 		const int end_sel = c.position();
265 		//get some space below the cursor
266 		c.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor,
267 					   v_margin - 1);
268 		viewSourceTV->setTextCursor(c);
269 		c.setPosition(end_sel, QTextCursor::KeepAnchor);
270 
271 		viewSourceTV->setTextCursor(c);
272 
273 		//the real highlighting is done with an ExtraSelection
274 		QTextCharFormat format;
275 		{
276 		// We create a new color with the lightness of AlternateBase and
277 		// the hue and saturation of Highlight
278 		QPalette palette = viewSourceTV->palette();
279 		QBrush alt = palette.alternateBase();
280 		QColor high = palette.highlight().color().toHsl();
281 		QColor col = QColor::fromHsl(high.hue(),
282 		                             high.hslSaturation(),
283 		                             alt.color().lightness());
284 		alt.setColor(col);
285 		format.setBackground(alt);
286 		}
287 		format.setProperty(QTextFormat::FullWidthSelection, true);
288 		QTextEdit::ExtraSelection sel;
289 		sel.format = format;
290 		sel.cursor = c;
291 		viewSourceTV->setExtraSelections(
292 			QList<QTextEdit::ExtraSelection>() << sel);
293 
294 		//clean up
295 		c.clearSelection();
296 		viewSourceTV->setTextCursor(c);
297 		viewSourceTV->horizontalScrollBar()->setValue(h_scroll);
298 	} // else if (texrow)
299 }
300 
301 
currentFormatName(BufferView const * bv) const302 docstring ViewSourceWidget::currentFormatName(BufferView const * bv) const
303 {
304 	// Compute the actual format used
305 	string const format = !bv ? ""
306 		: flavor2format(bv->buffer().params().getOutputFlavor(view_format_));
307 	Format const * f = theFormats().getFormat(format.empty() ? view_format_ : format);
308 	return f ? f->prettyname() : from_utf8(view_format_);
309 }
310 
311 
eventFilter(QObject * obj,QEvent * ev)312 bool ViewSourceWidget::eventFilter(QObject * obj, QEvent * ev)
313 {
314 	// this event filter is installed on the viewport of the QTextView
315 	if (obj == viewSourceTV->viewport() &&
316 	    ev->type() == QEvent::MouseButtonDblClick) {
317 		goToCursor();
318 		return true;
319 	}
320 	return false;
321 }
322 
323 
goToCursor() const324 void ViewSourceWidget::goToCursor() const
325 {
326 	if (!texrow_)
327 		return;
328 	int row = viewSourceTV->textCursor().blockNumber() + 1;
329 	dispatch(texrow_->goToFuncFromRow(row));
330 }
331 
332 
333 
updateDefaultFormat(BufferView const & bv)334 void ViewSourceWidget::updateDefaultFormat(BufferView const & bv)
335 {
336 	QSignalBlocker blocker(outputFormatCO);
337 	outputFormatCO->clear();
338 	outputFormatCO->addItem(qt_("Default"),
339 	                        QVariant(QString("default")));
340 
341 	int index = 0;
342 	for (string const & fmt_name : bv.buffer().params().backends()) {
343 		Format const * fmt = theFormats().getFormat(fmt_name);
344 		if (!fmt) {
345 			LYXERR0("Can't find format for backend " << fmt_name << "!");
346 			continue;
347 		}
348 		QString const pretty = toqstr(translateIfPossible(fmt->prettyname()));
349 		outputFormatCO->addItem(pretty, QVariant(toqstr(fmt_name)));
350 		if (fmt_name == view_format_)
351 			index = outputFormatCO->count() - 1;
352 	}
353 	setViewFormat(index);
354 }
355 
356 
resizeEvent(QResizeEvent * event)357 void ViewSourceWidget::resizeEvent (QResizeEvent * event)
358 {
359 	QSize const & formSize = formLayout->sizeHint();
360 	// minimize the size of the part that contains the buttons
361 	if (width() * formSize.height() < height() * formSize.width()) {
362 		layout_->setDirection(QBoxLayout::TopToBottom);
363 	} else {
364 		layout_->setDirection(QBoxLayout::LeftToRight);
365 	}
366 	QWidget::resizeEvent(event);
367 }
368 
369 
saveSession(QSettings & settings,QString const & session_key) const370 void ViewSourceWidget::saveSession(QSettings & settings, QString const & session_key) const
371 {
372 	settings.setValue(session_key + "/output", toqstr(view_format_));
373 	settings.setValue(session_key + "/contents", contentsCO->currentIndex());
374 	settings.setValue(session_key + "/autoupdate", autoUpdateCB->isChecked());
375 	settings.setValue(session_key + "/masterview",
376 					  masterPerspectiveCB->isChecked());
377 }
378 
379 
restoreSession(QString const & session_key)380 void ViewSourceWidget::restoreSession(QString const & session_key)
381 {
382 	QSettings settings;
383 	view_format_ = fromqstr(settings.value(session_key + "/output", 0)
384 	                        .toString());
385 	contentsCO->setCurrentIndex(settings
386 								.value(session_key + "/contents", 0)
387 								.toInt());
388 	masterPerspectiveCB->setChecked(settings
389 									.value(session_key + "/masterview", false)
390 									.toBool());
391 	bool const checked = settings
392 		.value(session_key + "/autoupdate", true)
393 		.toBool();
394 	autoUpdateCB->setChecked(checked);
395 	if (checked)
396 		Q_EMIT needUpdate();
397 }
398 
399 
GuiViewSource(GuiView & parent,Qt::DockWidgetArea area,Qt::WindowFlags flags)400 GuiViewSource::GuiViewSource(GuiView & parent,
401 		Qt::DockWidgetArea area, Qt::WindowFlags flags)
402 	: DockView(parent, "view-source", qt_("Code Preview"), area, flags),
403 	  widget_(new ViewSourceWidget(this)),
404 	  update_timer_(new QTimer(this))
405 {
406 	setWidget(widget_);
407 
408 	// setting the update timer
409 	update_timer_->setSingleShot(true);
410 	connect(update_timer_, SIGNAL(timeout()),
411 	        this, SLOT(realUpdateView()));
412 
413 	connect(widget_, SIGNAL(needUpdate()), this, SLOT(scheduleUpdateNow()));
414 }
415 
416 
onBufferViewChanged()417 void GuiViewSource::onBufferViewChanged()
418 {
419 	widget_->setText();
420 	widget_->setEnabled((bool)bufferview());
421 }
422 
423 
updateView()424 void GuiViewSource::updateView()
425 {
426 	if (widget_->autoUpdateCB->isChecked()) {
427 		widget_->setEnabled((bool)bufferview());
428 		scheduleUpdate();
429 	}
430 	widget_->masterPerspectiveCB->setEnabled(buffer().parent());
431 	updateTitle();
432 }
433 
434 
enableView(bool enable)435 void GuiViewSource::enableView(bool enable)
436 {
437 	widget_->setEnabled((bool)bufferview());
438 	if (bufferview())
439 		widget_->updateDefaultFormat(*bufferview());
440 	if (!enable)
441 		// In the opposite case, updateView() will be called anyway.
442 		widget_->contentsChanged();
443 }
444 
445 
initialiseParams(string const &)446 bool GuiViewSource::initialiseParams(string const & /*source*/)
447 {
448 	updateTitle();
449 	return true;
450 }
451 
452 
updateTitle()453 void GuiViewSource::updateTitle()
454 {
455 	docstring const format = widget_->currentFormatName(bufferview());
456 	QString const title = format.empty() ? qt_("Code Preview")
457 		: qt_("%1[[preview format name]] Preview")
458 		  .arg(toqstr(translateIfPossible(format)));
459 	setTitle(title);
460 	setWindowTitle(title);
461 }
462 
463 
saveSession(QSettings & settings) const464 void GuiViewSource::saveSession(QSettings & settings) const
465 {
466 	Dialog::saveSession(settings);
467 	widget_->saveSession(settings, sessionKey());
468 }
469 
470 
restoreSession()471 void GuiViewSource::restoreSession()
472 {
473 	DockView::restoreSession();
474 	widget_->restoreSession(sessionKey());
475 }
476 
477 
createGuiViewSource(GuiView & lv)478 Dialog * createGuiViewSource(GuiView & lv)
479 {
480 	return new GuiViewSource(lv);
481 }
482 
483 
484 } // namespace frontend
485 } // namespace lyx
486 
487 #include "moc_GuiViewSource.cpp"
488