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