1 /**************************************************************************
2 * Otter Browser: Web browser controlled by the user, not vice-versa.
3 * Copyright (C) 2015 - 2019 Michal Dutkiewicz aka Emdek <michal@emdek.pl>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 **************************************************************************/
19 
20 #include "SourceViewerWidget.h"
21 #include "../core/SettingsManager.h"
22 
23 #include <QtCore/QJsonArray>
24 #include <QtCore/QJsonDocument>
25 #include <QtCore/QJsonObject>
26 #include <QtCore/QMetaEnum>
27 #include <QtGui/QPainter>
28 #include <QtGui/QTextBlock>
29 #include <QtWidgets/QScrollBar>
30 
31 namespace Otter
32 {
33 
34 QMap<SyntaxHighlighter::HighlightingSyntax, QMap<SyntaxHighlighter::HighlightingState, QTextCharFormat> > SyntaxHighlighter::m_formats;
35 
SyntaxHighlighter(QTextDocument * parent)36 SyntaxHighlighter::SyntaxHighlighter(QTextDocument *parent) : QSyntaxHighlighter(parent)
37 {
38 	if (m_formats[HtmlSyntax].isEmpty())
39 	{
40 		QFile file(SessionsManager::getReadableDataPath(QLatin1String("syntaxHighlighting.json")));
41 		file.open(QIODevice::ReadOnly);
42 
43 		const QJsonObject syntaxesObject(QJsonDocument::fromJson(file.readAll()).object());
44 		const QMetaEnum highlightingSyntaxEnum(metaObject()->enumerator(metaObject()->indexOfEnumerator(QLatin1String("HighlightingSyntax").data())));
45 		const QMetaEnum highlightingStateEnum(metaObject()->enumerator(metaObject()->indexOfEnumerator(QLatin1String("HighlightingState").data())));
46 
47 		for (int i = 1; i < highlightingSyntaxEnum.keyCount(); ++i)
48 		{
49 			QMap<HighlightingState, QTextCharFormat> formats;
50 			QString syntax(highlightingSyntaxEnum.valueToKey(i));
51 			syntax.chop(6);
52 
53 			const QJsonObject definitionsObject(syntaxesObject.value(syntax).toObject());
54 
55 			for (int j = 0; j < highlightingStateEnum.keyCount(); ++j)
56 			{
57 				QString state(highlightingStateEnum.valueToKey(j));
58 				state.chop(5);
59 
60 				const QJsonObject definitionObject(definitionsObject.value(state).toObject());
61 				const QString foreground(definitionObject.value(QLatin1String("foreground")).toString(QLatin1String("auto")));
62 				const QString fontStyle(definitionObject.value(QLatin1String("fontStyle")).toString(QLatin1String("auto")));
63 				const QString fontWeight(definitionObject.value(QLatin1String("fontWeight")).toString(QLatin1String("auto")));
64 				QTextCharFormat format;
65 
66 				if (foreground != QLatin1String("auto"))
67 				{
68 					format.setForeground(QColor(foreground));
69 				}
70 
71 				if (fontStyle == QLatin1String("italic"))
72 				{
73 					format.setFontItalic(true);
74 				}
75 
76 				if (fontWeight != QLatin1String("auto"))
77 				{
78 					format.setFontWeight((fontWeight == QLatin1String("bold")) ? QFont::Bold : QFont::Normal);
79 				}
80 
81 				if (definitionObject.value(QLatin1String("isUnderlined")).toBool(false))
82 				{
83 					format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
84 				}
85 
86 				formats[static_cast<HighlightingState>(j)] = format;
87 			}
88 
89 			m_formats[static_cast<HighlightingSyntax>(i)] = formats;
90 		}
91 
92 		file.close();
93 	}
94 }
95 
highlightBlock(const QString & text)96 void SyntaxHighlighter::highlightBlock(const QString &text)
97 {
98 	QString buffer;
99 	BlockData currentData;
100 	HighlightingState previousState(static_cast<HighlightingState>(qMax(previousBlockState(), 0)));
101 	HighlightingState currentState(previousState);
102 	int previousStateBegin(0);
103 	int currentStateBegin(0);
104 	int position(0);
105 
106 	if (currentBlock().previous().userData())
107 	{
108 		currentData = *static_cast<BlockData*>(currentBlock().previous().userData());
109 	}
110 
111 	while (position < text.length())
112 	{
113 		buffer.append(text.at(position));
114 
115 		++position;
116 
117 		const bool isEndOfLine(position == text.length());
118 
119 		if (currentState == NoState && text.at(position - 1) == QLatin1Char('<'))
120 		{
121 			currentState = KeywordState;
122 			currentStateBegin = (position - 1);
123 		}
124 		else if ((currentState == KeywordState || currentState == DoctypeState) && text.at(position - 1) == QLatin1Char('>'))
125 		{
126 			currentState = NoState;
127 			currentStateBegin = position;
128 		}
129 		else if (currentState == AttributeState && text.length() > position && text.at(position) == QLatin1Char('>'))
130 		{
131 			currentState = KeywordState;
132 			currentStateBegin = position;
133 		}
134 		else if (currentState == KeywordState && buffer.compare(QLatin1String("!DOCTYPE"), Qt::CaseInsensitive) == 0)
135 		{
136 			currentState = DoctypeState;
137 		}
138 		else if (currentState == KeywordState && buffer == QLatin1String("![CDATA["))
139 		{
140 			currentState = CharacterDataState;
141 		}
142 		else if (currentState == CharacterDataState && buffer.endsWith(QLatin1String("]]>")))
143 		{
144 			currentState = NoState;
145 			currentStateBegin = position;
146 		}
147 		else if (currentState == KeywordState && buffer == QLatin1String("!--"))
148 		{
149 			currentState = CommentState;
150 		}
151 		else if (currentState == CommentState && buffer.endsWith(QLatin1String("-->")))
152 		{
153 			currentState = NoState;
154 			currentStateBegin = position;
155 		}
156 		else if (currentState == KeywordState && (text.at(position - 1) == QLatin1Char('-') || text.at(position - 1).isLetter() || text.at(position - 1).isNumber()) && (position == 1 || (position > 1 && text.at(position - 2).isSpace())))
157 		{
158 			currentState = AttributeState;
159 			currentStateBegin = (position - 1);
160 		}
161 		else if (currentState == AttributeState && !(text.at(position - 1) == QLatin1Char('-') || text.at(position - 1).isLetter() || text.at(position - 1).isNumber()))
162 		{
163 			currentState = KeywordState;
164 			currentStateBegin = (position - 1);
165 		}
166 		else if ((currentState == KeywordState || currentState == DoctypeState || currentState == AttributeState) && (text.at(position - 1) == QLatin1Char('\'') || text.at(position - 1) == QLatin1Char('"')))
167 		{
168 			currentData.context = text.at(position - 1);
169 			currentData.state = currentState;
170 			currentState = ValueState;
171 			currentStateBegin = (position - 1);
172 		}
173 		else if (currentState == ValueState && text.at(position - 1) == currentData.context)
174 		{
175 			currentState = currentData.state;
176 			currentStateBegin = position;
177 			currentData.context.clear();
178 			currentData.state = NoState;
179 		}
180 
181 		if (previousState != currentState || isEndOfLine)
182 		{
183 			setFormat(previousStateBegin, (position - previousStateBegin), m_formats[HtmlSyntax][previousState]);
184 
185 			if (isEndOfLine)
186 			{
187 				setFormat(currentStateBegin, (position - currentStateBegin), m_formats[HtmlSyntax][currentState]);
188 			}
189 
190 			buffer.clear();
191 			previousState = currentState;
192 			previousStateBegin = currentStateBegin;
193 		}
194 	}
195 
196 	if (!currentData.context.isEmpty())
197 	{
198 		BlockData *nextBlockData(new BlockData());
199 		nextBlockData->context = currentData.context;
200 		nextBlockData->state = currentData.state;
201 
202 		setCurrentBlockUserData(nextBlockData);
203 	}
204 
205 	setCurrentBlockState(currentState);
206 }
207 
MarginWidget(SourceViewerWidget * parent)208 MarginWidget::MarginWidget(SourceViewerWidget *parent) : QWidget(parent),
209 	m_sourceViewer(parent),
210 	m_lastClickedLine(-1)
211 {
212 	updateWidth();
213 	setContextMenuPolicy(Qt::NoContextMenu);
214 
215 	connect(m_sourceViewer, &SourceViewerWidget::updateRequest, this, &MarginWidget::updateNumbers);
216 	connect(m_sourceViewer, &SourceViewerWidget::blockCountChanged, this, &MarginWidget::updateWidth);
217 	connect(m_sourceViewer, &SourceViewerWidget::textChanged, this, &MarginWidget::updateWidth);
218 }
219 
paintEvent(QPaintEvent * event)220 void MarginWidget::paintEvent(QPaintEvent *event)
221 {
222 	QPainter painter(this);
223 	painter.fillRect(event->rect(), Qt::transparent);
224 
225 	QTextBlock block(m_sourceViewer->firstVisibleBlock());
226 	int top(m_sourceViewer->blockBoundingGeometry(block).translated(m_sourceViewer->contentOffset()).toRect().top());
227 	int bottom(top + m_sourceViewer->blockBoundingRect(block).toRect().height());
228 	const int right(width() - 5);
229 	const int selectionStart(m_sourceViewer->document()->findBlock(m_sourceViewer->textCursor().selectionStart()).blockNumber());
230 	const int selectionEnd(m_sourceViewer->document()->findBlock(m_sourceViewer->textCursor().selectionEnd()).blockNumber());
231 
232 	while (block.isValid() && top <= event->rect().bottom())
233 	{
234 		if (block.isVisible() && bottom >= event->rect().top())
235 		{
236 			QColor textColor(palette().color(QPalette::Text));
237 			textColor.setAlpha((block.blockNumber() >= selectionStart && block.blockNumber() <= selectionEnd) ? 250 : 150);
238 
239 			painter.setPen(textColor);
240 			painter.drawText(0, top, right, fontMetrics().height(), Qt::AlignRight, QString::number(block.blockNumber() + 1));
241 		}
242 
243 		block = block.next();
244 		top = bottom;
245 		bottom = (top + m_sourceViewer->blockBoundingRect(block).toRect().height());
246 	}
247 }
248 
mousePressEvent(QMouseEvent * event)249 void MarginWidget::mousePressEvent(QMouseEvent *event)
250 {
251 	if (event->button() == Qt::LeftButton)
252 	{
253 		QTextCursor textCursor(m_sourceViewer->cursorForPosition(QPoint(1, event->y())));
254 		textCursor.select(QTextCursor::LineUnderCursor);
255 		textCursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor);
256 
257 		m_lastClickedLine = textCursor.blockNumber();
258 
259 		m_sourceViewer->setTextCursor(textCursor);
260 	}
261 	else
262 	{
263 		QWidget::mousePressEvent(event);
264 	}
265 }
266 
mouseMoveEvent(QMouseEvent * event)267 void MarginWidget::mouseMoveEvent(QMouseEvent *event)
268 {
269 	QTextCursor textCursor(m_sourceViewer->cursorForPosition(QPoint(1, event->y())));
270 	const int currentLine(textCursor.blockNumber());
271 
272 	if (currentLine != m_lastClickedLine)
273 	{
274 		const bool isMovingUp(currentLine < m_lastClickedLine);
275 
276 		textCursor.movePosition((isMovingUp ? QTextCursor::Down : QTextCursor::Up), QTextCursor::KeepAnchor, qAbs(m_lastClickedLine - currentLine - (isMovingUp ? 0 : 1)));
277 
278 		m_sourceViewer->setTextCursor(textCursor);
279 	}
280 }
281 
mouseReleaseEvent(QMouseEvent * event)282 void MarginWidget::mouseReleaseEvent(QMouseEvent *event)
283 {
284 	m_lastClickedLine = -1;
285 
286 	QWidget::mouseReleaseEvent(event);
287 }
288 
updateNumbers(const QRect & rectangle,int offset)289 void MarginWidget::updateNumbers(const QRect &rectangle, int offset)
290 {
291 	if (offset)
292 	{
293 		scroll(0, offset);
294 	}
295 	else
296 	{
297 		update(0, rectangle.y(), width(), rectangle.height());
298 	}
299 }
300 
updateWidth()301 void MarginWidget::updateWidth()
302 {
303 	int digits(1);
304 	int maximum(qMax(1, m_sourceViewer->blockCount()));
305 
306 	while (maximum >= 10)
307 	{
308 		maximum /= 10;
309 
310 		++digits;
311 	}
312 
313 	setFixedWidth(6 + (fontMetrics().width(QLatin1Char('9')) * digits));
314 
315 	m_sourceViewer->setViewportMargins(width(), 0, 0, 0);
316 }
317 
event(QEvent * event)318 bool MarginWidget::event(QEvent *event)
319 {
320 	const bool result(QWidget::event(event));
321 
322 	if (event->type() == QEvent::FontChange)
323 	{
324 		updateWidth();
325 	}
326 
327 	return result;
328 }
329 
SourceViewerWidget(QWidget * parent)330 SourceViewerWidget::SourceViewerWidget(QWidget *parent) : QPlainTextEdit(parent),
331 	m_marginWidget(nullptr),
332 	m_findFlags(WebWidget::NoFlagsFind),
333 	m_findTextResultsAmount(0),
334 	m_zoom(100)
335 {
336 	new SyntaxHighlighter(document());
337 
338 	setZoom(SettingsManager::getOption(SettingsManager::Content_DefaultZoomOption).toInt());
339 	handleOptionChanged(SettingsManager::Interface_ShowScrollBarsOption, SettingsManager::getOption(SettingsManager::Interface_ShowScrollBarsOption));
340 	handleOptionChanged(SettingsManager::SourceViewer_ShowLineNumbersOption, SettingsManager::getOption(SettingsManager::SourceViewer_ShowLineNumbersOption));
341 	handleOptionChanged(SettingsManager::SourceViewer_WrapLinesOption, SettingsManager::getOption(SettingsManager::SourceViewer_WrapLinesOption));
342 
343 	connect(this, &SourceViewerWidget::textChanged, this, &SourceViewerWidget::updateSelection);
344 	connect(this, &SourceViewerWidget::cursorPositionChanged, this, &SourceViewerWidget::updateTextCursor);
345 	connect(SettingsManager::getInstance(), &SettingsManager::optionChanged, this, &SourceViewerWidget::handleOptionChanged);
346 }
347 
resizeEvent(QResizeEvent * event)348 void SourceViewerWidget::resizeEvent(QResizeEvent *event)
349 {
350 	QPlainTextEdit::resizeEvent(event);
351 
352 	if (m_marginWidget)
353 	{
354 		m_marginWidget->setGeometry(QRect(contentsRect().left(), contentsRect().top(), m_marginWidget->width(), contentsRect().height()));
355 	}
356 }
357 
focusInEvent(QFocusEvent * event)358 void SourceViewerWidget::focusInEvent(QFocusEvent *event)
359 {
360 	QPlainTextEdit::focusInEvent(event);
361 
362 	if (event->reason() != Qt::MouseFocusReason && event->reason() != Qt::PopupFocusReason && !m_findText.isEmpty())
363 	{
364 		setTextCursor(m_findTextSelection);
365 	}
366 }
367 
wheelEvent(QWheelEvent * event)368 void SourceViewerWidget::wheelEvent(QWheelEvent *event)
369 {
370 	if (event->modifiers().testFlag(Qt::ControlModifier))
371 	{
372 		setZoom(getZoom() + (event->delta() / 16));
373 
374 		event->accept();
375 
376 		return;
377 	}
378 
379 	QPlainTextEdit::wheelEvent(event);
380 }
381 
handleOptionChanged(int identifier,const QVariant & value)382 void SourceViewerWidget::handleOptionChanged(int identifier, const QVariant &value)
383 {
384 	switch (identifier)
385 	{
386 		case SettingsManager::Interface_ShowScrollBarsOption:
387 			setHorizontalScrollBarPolicy(value.toBool() ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff);
388 			setVerticalScrollBarPolicy(value.toBool() ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff);
389 
390 			break;
391 		case SettingsManager::SourceViewer_ShowLineNumbersOption:
392 			if (value.toBool() && !m_marginWidget)
393 			{
394 				m_marginWidget = new MarginWidget(this);
395 				m_marginWidget->show();
396 				m_marginWidget->setGeometry(QRect(contentsRect().left(), contentsRect().top(), m_marginWidget->width(), contentsRect().height()));
397 			}
398 			else if (!value.toBool() && m_marginWidget)
399 			{
400 				m_marginWidget->deleteLater();
401 				m_marginWidget = nullptr;
402 
403 				setViewportMargins(0, 0, 0, 0);
404 			}
405 
406 			break;
407 		case SettingsManager::SourceViewer_WrapLinesOption:
408 			setLineWrapMode(value.toBool() ? QPlainTextEdit::WidgetWidth : QPlainTextEdit::NoWrap);
409 
410 			break;
411 		default:
412 			break;
413 	}
414 }
415 
updateTextCursor()416 void SourceViewerWidget::updateTextCursor()
417 {
418 	m_findTextAnchor = textCursor();
419 }
420 
updateSelection()421 void SourceViewerWidget::updateSelection()
422 {
423 	QList<QTextEdit::ExtraSelection> extraSelections;
424 
425 	if (m_findText.isEmpty())
426 	{
427 		m_findTextResultsAmount = 0;
428 
429 		setExtraSelections(extraSelections);
430 
431 		return;
432 	}
433 
434 	int findTextResultsAmount(0);
435 	QTextEdit::ExtraSelection currentResultSelection;
436 	currentResultSelection.format.setBackground(QColor(255, 150, 50));
437 	currentResultSelection.format.setProperty(QTextFormat::FullWidthSelection, true);
438 	currentResultSelection.cursor = m_findTextSelection;
439 
440 	extraSelections.append(currentResultSelection);
441 
442 	QTextCursor textCursor(this->textCursor());
443 	textCursor.setPosition(0);
444 
445 	if (m_findFlags.testFlag(WebWidget::HighlightAllFind))
446 	{
447 		QTextDocument::FindFlags nativeFlags;
448 
449 		if (m_findFlags.testFlag(WebWidget::CaseSensitiveFind))
450 		{
451 			nativeFlags |= QTextDocument::FindCaseSensitively;
452 		}
453 
454 		while (!textCursor.isNull())
455 		{
456 			textCursor = document()->find(m_findText, textCursor, nativeFlags);
457 
458 			if (!textCursor.isNull())
459 			{
460 				if (textCursor != m_findTextSelection)
461 				{
462 					QTextEdit::ExtraSelection extraResultSelection;
463 					extraResultSelection.format.setBackground(QColor(255, 255, 0));
464 					extraResultSelection.cursor = textCursor;
465 
466 					extraSelections.append(extraResultSelection);
467 				}
468 
469 				++findTextResultsAmount;
470 			}
471 		}
472 	}
473 
474 	m_findTextResultsAmount = findTextResultsAmount;
475 
476 	setExtraSelections(extraSelections);
477 }
478 
setZoom(int zoom)479 void SourceViewerWidget::setZoom(int zoom)
480 {
481 	if (zoom != m_zoom)
482 	{
483 		m_zoom = zoom;
484 
485 		QFont font(QFontDatabase::systemFont(QFontDatabase::FixedFont));
486 		font.setPointSize(qRound(font.pointSize() * (static_cast<qreal>(zoom) / 100)));
487 
488 		setFont(font);
489 
490 		if (m_marginWidget)
491 		{
492 			m_marginWidget->setFont(font);
493 		}
494 
495 		emit zoomChanged(zoom);
496 	}
497 }
498 
getZoom() const499 int SourceViewerWidget::getZoom() const
500 {
501 	return m_zoom;
502 }
503 
findText(const QString & text,WebWidget::FindFlags flags)504 int SourceViewerWidget::findText(const QString &text, WebWidget::FindFlags flags)
505 {
506 	const bool isTheSame(text == m_findText);
507 
508 	m_findText = text;
509 	m_findFlags = flags;
510 
511 	if (!text.isEmpty())
512 	{
513 		QTextDocument::FindFlags nativeFlags;
514 
515 		if (flags.testFlag(WebWidget::BackwardFind))
516 		{
517 			nativeFlags |= QTextDocument::FindBackward;
518 		}
519 
520 		if (flags.testFlag(WebWidget::CaseSensitiveFind))
521 		{
522 			nativeFlags |= QTextDocument::FindCaseSensitively;
523 		}
524 
525 		QTextCursor findTextCursor(m_findTextAnchor);
526 
527 		if (!isTheSame)
528 		{
529 			findTextCursor = textCursor();
530 		}
531 		else if (!flags.testFlag(WebWidget::BackwardFind))
532 		{
533 			findTextCursor.setPosition(findTextCursor.selectionEnd(), QTextCursor::MoveAnchor);
534 		}
535 
536 		m_findTextAnchor = document()->find(text, findTextCursor, nativeFlags);
537 
538 		if (m_findTextAnchor.isNull())
539 		{
540 			m_findTextAnchor = textCursor();
541 			m_findTextAnchor.setPosition((flags.testFlag(WebWidget::BackwardFind) ? (document()->characterCount() - 1) : 0), QTextCursor::MoveAnchor);
542 			m_findTextAnchor = document()->find(text, m_findTextAnchor, nativeFlags);
543 		}
544 
545 		if (!m_findTextAnchor.isNull())
546 		{
547 			const QTextCursor currentTextCursor(textCursor());
548 
549 			disconnect(this, &SourceViewerWidget::cursorPositionChanged, this, &SourceViewerWidget::updateTextCursor);
550 
551 			setTextCursor(m_findTextAnchor);
552 			ensureCursorVisible();
553 
554 			const QPoint position(horizontalScrollBar()->value(), verticalScrollBar()->value());
555 
556 			setTextCursor(currentTextCursor);
557 
558 			horizontalScrollBar()->setValue(position.x());
559 			verticalScrollBar()->setValue(position.y());
560 
561 			connect(this, &SourceViewerWidget::cursorPositionChanged, this, &SourceViewerWidget::updateTextCursor);
562 		}
563 	}
564 
565 	m_findTextSelection = m_findTextAnchor;
566 
567 	updateSelection();
568 
569 	return m_findTextResultsAmount;
570 }
571 
572 }
573