1 /***********************************************************************
2  *
3  * Copyright (C) 2014-2019 wereturtle
4  * Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Graeme Gott <graeme@gottcode.org>
5  * Copyright (C) Dmitry Shachnev 2012
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  *
20  ***********************************************************************/
21 
22 #include <QTextStream>
23 #include <QString>
24 #include <QMimeData>
25 #include <QScrollBar>
26 #include <QTextBoundaryFinder>
27 #include <QHeaderView>
28 #include <QMenu>
29 #include <QChar>
30 #include <QTimer>
31 #include <QColor>
32 #include <QApplication>
33 #include <QDesktopWidget>
34 #include <QUrl>
35 #include <QPixmap>
36 #include <QPainter>
37 #include <QPainterPath>
38 #include <QFileInfo>
39 #include <QDir>
40 
41 #include "ColorHelper.h"
42 #include "MarkdownEditor.h"
43 #include "MarkdownStates.h"
44 #include "MarkdownTokenizer.h"
45 #include "MarkdownHighlighter.h"
46 #include "spelling/dictionary_ref.h"
47 #include "spelling/dictionary_manager.h"
48 #include "spelling/spell_checker.h"
49 
50 #define GW_TEXT_FADE_FACTOR 1.5
51 
MarkdownEditor(TextDocument * textDocument,QWidget * parent)52 MarkdownEditor::MarkdownEditor
53 (
54     TextDocument* textDocument,
55     QWidget* parent
56 )
57     : QPlainTextEdit(parent),
58         textDocument(textDocument),
59         dictionary(DictionaryManager::instance().requestDictionary()),
60         autoMatchEnabled(true),
61         bulletPointCyclingEnabled(true),
62         mouseButtonDown(false)
63 {
64     setDocument(textDocument);
65 
66     highlighter = new MarkdownHighlighter(this);
67 
68     setAcceptDrops(true);
69 
70     preferredLayout = new QGridLayout();
71     preferredLayout->setSpacing(0);
72     preferredLayout->setMargin(0);
73     preferredLayout->setContentsMargins(0, 0, 0, 0);
74     preferredLayout->addWidget(this, 0, 0);
75 
76     blockquoteRegex.setPattern("^ {0,3}(>\\s*)+");
77     numberedListRegex.setPattern("^\\s*([0-9]+)[.)]\\s+");
78     bulletListRegex.setPattern("^\\s*[+*-]\\s+");
79     taskListRegex.setPattern("^\\s*[-*+] \\[([x ])\\]\\s+");
80     emptyBlockquoteRegex.setPattern("^ {0,3}(>\\s*)+$");
81     emptyNumberedListRegex.setPattern("^\\s*([0-9]+)[.)]\\s+$");
82     emptyBulletListRegex.setPattern("^\\s*[+*-]\\s+$");
83     emptyTaskListRegex.setPattern("^\\s*[-*+] \\[([x ])\\]\\s+$");
84 
85     this->setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
86     this->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
87 
88     // Make sure QPlainTextEdit does not draw a cursor.  (We'll paint it manually.)
89     setCursorWidth(0);
90 
91     setCenterOnScroll(true);
92     ensureCursorVisible();
93     spellCheckEnabled = false;
94     installEventFilter(this);
95     viewport()->installEventFilter(this);
96     hemingwayModeEnabled = false;
97     focusMode = FocusModeDisabled;
98     insertSpacesForTabs = false;
99     setTabulationWidth(4);
100     editorWidth = EditorWidthMedium;
101     editorCorners = InterfaceStyleRounded;
102 
103     markupPairs.insert('"', '"');
104     markupPairs.insert('\'', '\'');
105     markupPairs.insert('(', ')');
106     markupPairs.insert('[', ']');
107     markupPairs.insert('{', '}');
108     markupPairs.insert('*', '*');
109     markupPairs.insert('_', '_');
110     markupPairs.insert('`', '`');
111     markupPairs.insert('<', '>');
112 
113     // Set automatching for the above markup pairs to be
114     // enabled by default.
115     //
116     autoMatchFilter.insert('"', true);
117     autoMatchFilter.insert('\'', true);
118     autoMatchFilter.insert('(', true);
119     autoMatchFilter.insert('[', true);
120     autoMatchFilter.insert('{', true);
121     autoMatchFilter.insert('*', true);
122     autoMatchFilter.insert('_', true);
123     autoMatchFilter.insert('`', true);
124     autoMatchFilter.insert('<', true);
125 
126     nonEmptyMarkupPairs.insert('*', '*');
127     nonEmptyMarkupPairs.insert('_', '_');
128     nonEmptyMarkupPairs.insert('<', '>');
129 
130     connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(onCursorPositionChanged()));
131     connect(this->document(), SIGNAL(contentsChange(int,int,int)), this, SLOT(onContentsChanged(int,int,int)));
132     connect(this, SIGNAL(selectionChanged()), this, SLOT(onSelectionChanged()));
133 
134     addWordToDictionaryAction = new QAction(tr("Add word to dictionary"), this);
135     checkSpellingAction = new QAction(tr("Check spelling..."), this);
136 
137     typingPausedSignalSent = true;
138     typingHasPaused = true;
139 
140     typingTimer = new QTimer(this);
141     connect
142     (
143         typingTimer,
144         SIGNAL(timeout()),
145         this,
146         SLOT(checkIfTypingPaused())
147     );
148     typingTimer->start(1000);
149 
150     typingPausedScaledSignalSent = true;
151     scaledTypingHasPaused = true;
152 
153     scaledTypingTimer = new QTimer(this);
154     connect
155     (
156         scaledTypingTimer,
157         SIGNAL(timeout()),
158         this,
159         SLOT(checkIfTypingPausedScaled())
160     );
161     scaledTypingTimer->start(1000);
162 
163     setColorScheme
164     (
165         QColor(Qt::black),
166         QColor(Qt::white),
167         QColor(Qt::black),
168         QColor(Qt::blue),
169         QColor(Qt::black),
170         QColor(Qt::black),
171         QColor(Qt::black),
172         QColor(Qt::black),
173         QColor(Qt::red)
174     );
175 
176     textCursorVisible = true;
177 
178     cursorBlinkTimer = new QTimer(this);
179     connect(cursorBlinkTimer, SIGNAL(timeout()), this, SLOT(toggleCursorBlink()));
180     cursorBlinkTimer->start(500);
181 }
182 
~MarkdownEditor()183 MarkdownEditor::~MarkdownEditor()
184 {
185 
186 }
187 
paintEvent(QPaintEvent * event)188 void MarkdownEditor::paintEvent(QPaintEvent* event)
189 {
190     QPainter painter(viewport());
191     QRect viewportRect = viewport()->rect();
192     painter.fillRect(viewportRect, Qt::transparent);
193 
194     QPointF offset(contentOffset());
195     QTextBlock block = firstVisibleBlock();
196 
197     bool firstVisible = true;
198 
199     QRectF blockAreaRect; // Code or block quote rect.
200     bool inBlockArea = false;
201     BlockType blockType = BlockTypeNone;
202     bool clipTop = false;
203     bool drawBlock = false;
204     int dy = 0;
205     bool done = false;
206 
207     int cornerRadius = 5;
208 
209     if (InterfaceStyleSquare == editorCorners)
210     {
211         cornerRadius = 0;
212     }
213 
214     // Draw text block area backgrounds for code blocks and block quotes.
215     // The backgrounds are drawn per each block area (consisting of multiple
216     // text blocks or lines), rather than one rectangle area per text block/
217     // line in case there are margins between each text block.  This way,
218     // the background will extend to cover the margins between text blocks
219     // as well.
220     //
221     // NOTE: Algorithm for looping through text blocks is a partial lift from
222     //       Qt's QPlainTextEdit paintEvent() code. Please refer to the
223     //       LGPL v. 3 license for the original Qt code.
224     //
225     while (block.isValid() && !done)
226     {
227         QRectF r = blockBoundingRect(block).translated(offset);
228 
229         // If the block begins a new text block area...
230         if (!inBlockArea && atBlockAreaStart(block, blockType))
231         {
232             blockAreaRect = r;
233             dy = 0;
234             inBlockArea = true;
235 
236             BlockType prevType;
237 
238             // If this is the first visible block within the viewport
239             // and if the previous block is part of the text block area,
240             // then the rectangle to draw for the block area will have
241             // its top clipped by the viewport and will need to be
242             // drawn specially.
243             //
244             if
245             (
246                 firstVisible
247                 && atBlockAreaStart(block.previous(), prevType)
248                 && (blockType == prevType)
249             )
250             {
251                 clipTop = true;
252             }
253         }
254         // Else if the block ends a text block area...
255         else if (inBlockArea && atBlockAreaEnd(block, blockType))
256         {
257             drawBlock = true;
258             inBlockArea = false;
259             blockAreaRect.setHeight(dy);
260         }
261 
262         // If the block is at the end of the document and ends a text
263         // block area...
264         //
265         if (inBlockArea && (block == this->document()->lastBlock()))
266         {
267             drawBlock = true;
268             inBlockArea = false;
269             dy += r.height();
270             blockAreaRect.setHeight(dy);
271         }
272 
273         offset.ry() += r.height();
274         dy += r.height();
275 
276         // If this is the last text block visible within the viewport...
277         if (offset.y() > viewportRect.height())
278         {
279             if (inBlockArea)
280             {
281                 blockAreaRect.setHeight(dy);
282                 drawBlock = true;
283             }
284 
285             // Finished drawing.
286             done = true;
287         }
288 
289         if (drawBlock)
290         {
291             painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
292             painter.setPen(Qt::NoPen);
293             painter.setBrush(QBrush(blockColor));
294 
295             // If the first visible block is "clipped" such that the previous block
296             // is part of the text block area, then only draw a rectangle with the
297             // bottom corners rounded, and with the top corners square to reflect
298             // that the first visible block is part of a larger block of text.
299             //
300             if (clipTop)
301             {
302                 QPainterPath path;
303                 path.setFillRule(Qt::WindingFill);
304                 path.addRoundedRect(blockAreaRect, cornerRadius, cornerRadius);
305                 qreal adjustedHeight = blockAreaRect.height() / 2;
306                 path.addRect(blockAreaRect.adjusted(0, 0, 0, -adjustedHeight));
307                 painter.drawPath(path.simplified());
308                 clipTop = false;
309             }
310             // Else draw the entire rectangle with all corners rounded.
311             else
312             {
313                 painter.drawRoundedRect(blockAreaRect, cornerRadius, cornerRadius);
314             }
315 
316             drawBlock = false;
317         }
318 
319         // This fixes the RTL bug of QPlainTextEdit
320         // https://bugreports.qt.io/browse/QTBUG-7516.
321         //
322         // Credit goes to Patrizio Bekerle (qmarkdowntextedit) for discovering
323         // this workaround.
324         //
325         if (block.text().isRightToLeft())
326         {
327             QTextLayout* layout = block.layout();
328             QTextOption opt = document()->defaultTextOption();
329             opt = QTextOption(Qt::AlignRight);
330             opt.setTextDirection(Qt::RightToLeft);
331             layout->setTextOption(opt);
332         }
333 
334         block = block.next();
335         firstVisible = false;
336     }
337 
338     painter.end();
339 
340     // Draw the visible editor text.
341     QPlainTextEdit::paintEvent(event);
342 
343     // Draw the text cursor/caret.
344     if (textCursorVisible && this->hasFocus())
345     {
346         // Get the cursor rect so that we have the ideal height for it,
347         // and then set it to be 2 pixels wide.  (The width will be zero,
348         // because we set it to be that in the constructor so that
349         // QPlainTextEdit will not draw another cursor underneath this one.)
350         //
351         QRect r = cursorRect();
352         r.setWidth(2);
353 
354         QPainter painter(viewport());
355         painter.fillRect(r, QBrush(cursorColor));
356         painter.end();
357     }
358 }
359 
setDictionary(const QString & language)360 void MarkdownEditor::setDictionary(const QString& language)
361 {
362     dictionary = DictionaryManager::instance().requestDictionary(language);
363     highlighter->setDictionary(dictionary);
364 }
365 
getPreferredLayout()366 QLayout* MarkdownEditor::getPreferredLayout()
367 {
368     return preferredLayout;
369 }
370 
getHemingwayModeEnabled() const371 bool MarkdownEditor::getHemingwayModeEnabled() const
372 {
373     return hemingwayModeEnabled;
374 }
375 
376 /**
377  * Sets whether Hemingway mode is enabled.
378  */
setHemingWayModeEnabled(bool enabled)379 void MarkdownEditor::setHemingWayModeEnabled(bool enabled)
380 {
381     hemingwayModeEnabled = enabled;
382 }
383 
getFocusMode()384 FocusMode MarkdownEditor::getFocusMode()
385 {
386     return focusMode;
387 }
388 
setFocusMode(FocusMode mode)389 void MarkdownEditor::setFocusMode(FocusMode mode)
390 {
391     focusMode = mode;
392 
393     if (FocusModeDisabled != mode)
394     {
395         connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(focusText()));
396         connect(this, SIGNAL(selectionChanged()), this, SLOT(focusText()));
397         connect(this, SIGNAL(textChanged()), this, SLOT(focusText()));
398         focusText();
399     }
400     else
401     {
402         disconnect(this, SIGNAL(cursorPositionChanged()), this, SLOT(focusText()));
403         disconnect(this, SIGNAL(selectionChanged()), this, SLOT(focusText()));
404         disconnect(this, SIGNAL(textChanged()), this, SLOT(focusText()));
405         this->setExtraSelections(QList<QTextEdit::ExtraSelection>());
406     }
407 }
408 
setColorScheme(const QColor & defaultTextColor,const QColor & backgroundColor,const QColor & markupColor,const QColor & linkColor,const QColor & headingColor,const QColor & emphasisColor,const QColor & blockquoteColor,const QColor & codeColor,const QColor & spellingErrorColor)409 void MarkdownEditor::setColorScheme
410 (
411     const QColor& defaultTextColor,
412     const QColor& backgroundColor,
413     const QColor& markupColor,
414     const QColor& linkColor,
415     const QColor& headingColor,
416     const QColor& emphasisColor,
417     const QColor& blockquoteColor,
418     const QColor& codeColor,
419     const QColor& spellingErrorColor
420 )
421 {
422     highlighter->setColorScheme
423     (
424         defaultTextColor,
425         backgroundColor,
426         markupColor,
427         linkColor,
428         headingColor,
429         emphasisColor,
430         blockquoteColor,
431         codeColor,
432         spellingErrorColor
433     );
434 
435     this->cursorColor = linkColor;
436 
437     blockColor = defaultTextColor;
438 
439     int blockAlpha = 20;
440 
441     if (backgroundColor.alpha() < 255)
442     {
443         blockAlpha = 18;
444     }
445     else if (ColorHelper::getLuminance(blockColor) < 0.5)
446     {
447         blockAlpha = 10;
448     }
449 
450     blockColor.setAlpha(blockAlpha);
451 
452     QColor fadedForegroundColor = defaultTextColor;
453     fadedForegroundColor.setAlpha(100);
454 
455     fadeColor = QBrush(fadedForegroundColor);
456     focusText();
457 }
458 
setAspect(EditorAspect aspect)459 void MarkdownEditor::setAspect(EditorAspect aspect)
460 {
461     this->aspect = aspect;
462 }
463 
setFont(const QString & family,double pointSize)464 void MarkdownEditor::setFont(const QString& family, double pointSize)
465 {
466     QFont font(family, pointSize);
467     QPlainTextEdit::setFont(font);
468     highlighter->setFont(family, pointSize);
469     setTabulationWidth(tabWidth);
470 }
471 
setShowTabsAndSpacesEnabled(bool enabled)472 void MarkdownEditor::setShowTabsAndSpacesEnabled(bool enabled)
473 {
474     QTextOption option = textDocument->defaultTextOption();
475 
476     if (enabled)
477     {
478         option.setFlags(option.flags() | QTextOption::ShowTabsAndSpaces);
479     }
480     else
481     {
482         option.setFlags(option.flags() & ~QTextOption::ShowTabsAndSpaces);
483     }
484 
485     textDocument->setDefaultTextOption(option);
486 }
487 
setupPaperMargins(int width)488 void MarkdownEditor::setupPaperMargins(int width)
489 {
490     if (EditorWidthFull == editorWidth)
491     {
492         preferredLayout->setContentsMargins(0, 0, 0, 0);
493         setViewportMargins(0, 0, 0, 0);
494 
495         return;
496     }
497 
498     int screenWidth = QApplication::desktop()->screenGeometry().width();
499     int proposedEditorWidth = width;
500     int margin = 0;
501 
502     switch (editorWidth)
503     {
504         case EditorWidthNarrow:
505             proposedEditorWidth = screenWidth / 3;
506             break;
507         case EditorWidthMedium:
508             proposedEditorWidth = screenWidth / 2;
509             break;
510         case EditorWidthWide:
511             proposedEditorWidth = 2 * (screenWidth / 3);
512             break;
513         default:
514             break;
515     }
516 
517     if (proposedEditorWidth <= width)
518     {
519         margin = (width - proposedEditorWidth) / 2;
520     }
521 
522     if (EditorAspectStretch == aspect)
523     {
524         preferredLayout->setContentsMargins(0, 0, 0, 0);
525         setViewportMargins(margin, 20, margin, 0);
526     }
527     else
528     {
529         preferredLayout->setContentsMargins(margin, 20, margin, 20);
530         setViewportMargins(10, 10, 10, 10);
531     }
532 }
533 
dragEnterEvent(QDragEnterEvent * e)534 void MarkdownEditor::dragEnterEvent(QDragEnterEvent* e)
535 {
536     if (e->mimeData()->hasUrls())
537     {
538         e->acceptProposedAction();
539     }
540     else
541     {
542         QPlainTextEdit::dragEnterEvent(e);
543     }
544 }
545 
dragMoveEvent(QDragMoveEvent * e)546 void MarkdownEditor::dragMoveEvent(QDragMoveEvent* e)
547 {
548     e->acceptProposedAction();
549 }
550 
dragLeaveEvent(QDragLeaveEvent * e)551 void MarkdownEditor::dragLeaveEvent(QDragLeaveEvent* e)
552 {
553     e->accept();
554 }
555 
dropEvent(QDropEvent * e)556 void MarkdownEditor::dropEvent(QDropEvent* e)
557 {
558     if (e->mimeData()->hasUrls() && (e->mimeData()->urls().size() == 1))
559     {
560         e->acceptProposedAction();
561 
562         QUrl url = e->mimeData()->urls().first();
563         QString path = url.toLocalFile();
564         bool isRelativePath = false;
565 
566         QFileInfo fileInfo(path);
567         QString fileExtension = fileInfo.suffix().toLower();
568 
569         QTextCursor dropCursor = cursorForPosition(e->pos());
570 
571         // If the file extension indicates an image type, then insert an
572         // image link into the text.
573         if
574         (
575             (fileExtension == "jpg") ||
576             (fileExtension == "jpeg") ||
577             (fileExtension == "gif") ||
578             (fileExtension == "bmp") ||
579             (fileExtension == "png") ||
580             (fileExtension == "tif") ||
581             (fileExtension == "tiff") ||
582             (fileExtension == "svg")
583         )
584         {
585             if (!textDocument->isNew())
586             {
587                 QFileInfo docInfo(textDocument->getFilePath());
588 
589                 if (docInfo.exists())
590                 {
591                     path = docInfo.dir().relativeFilePath(path);
592                     isRelativePath = true;
593                 }
594             }
595 
596             if (!isRelativePath)
597             {
598                 path = url.toString();
599             }
600 
601             dropCursor.insertText(QString("![](%1)").arg(path));
602 
603             // We have to call the super class so that clean up occurs,
604             // otherwise the editor's cursor will freeze.  We also have to use
605             // a dummy drop event with dummy MIME data, otherwise the parent
606             // class will insert the file path into the document.
607             //
608             QMimeData* dummyMimeData = new QMimeData();
609             dummyMimeData->setText("");
610             QDropEvent* dummyEvent =
611                 new QDropEvent
612                 (
613                     e->pos(),
614                     e->possibleActions(),
615                     dummyMimeData,
616                     e->mouseButtons(),
617                     e->keyboardModifiers()
618                 );
619             QPlainTextEdit::dropEvent(dummyEvent);
620 
621             delete dummyEvent;
622             delete dummyMimeData;
623         }
624         // Else insert URL path as normal, using the parent class.
625         else
626         {
627             QPlainTextEdit::dropEvent(e);
628         }
629     }
630     else
631     {
632         QPlainTextEdit::dropEvent(e);
633     }
634 }
635 
636 /*
637  * This method contains a code snippet that was lifted and modified from ReText
638  */
keyPressEvent(QKeyEvent * e)639 void MarkdownEditor::keyPressEvent(QKeyEvent* e)
640 {
641     int key = e->key();
642 
643     QTextCursor cursor(this->textCursor());
644 
645     switch (key)
646     {
647         case Qt::Key_Return:
648             if (!cursor.hasSelection())
649             {
650                 if (e->modifiers() & Qt::ShiftModifier)
651                 {
652                     // Insert Markdown-style line break
653                     cursor.insertText("  ");
654                     highlighter->rehighlightBlock(cursor.block());
655                 }
656 
657                 if (e->modifiers() & Qt::ControlModifier)
658                 {
659                     cursor.insertText("\n");
660                 }
661                 else
662                 {
663                     handleCarriageReturn();
664                 }
665             }
666             else
667             {
668                 QPlainTextEdit::keyPressEvent(e);
669             }
670             break;
671         case Qt::Key_Delete:
672             if (!hemingwayModeEnabled)
673             {
674                 QPlainTextEdit::keyPressEvent(e);
675             }
676             break;
677         case Qt::Key_Backspace:
678             if (!hemingwayModeEnabled)
679             {
680                 if (!handleBackspaceKey())
681                 {
682                     QPlainTextEdit::keyPressEvent(e);
683                 }
684             }
685             break;
686         case Qt::Key_Tab:
687             if (!handleWhitespaceInEmptyMatch('\t'))
688             {
689                 indentText();
690             }
691             break;
692         case Qt::Key_Backtab:
693             unindentText();
694             break;
695         case Qt::Key_Space:
696             if (!handleWhitespaceInEmptyMatch(' '))
697             {
698                 QPlainTextEdit::keyPressEvent(e);
699             }
700             break;
701         default:
702             if (e->text().size() == 1)
703             {
704                 QChar ch = e->text().at(0);
705 
706                 if (!handleEndPairCharacterTyped(ch) && !insertPairedCharacters(ch))
707                 {
708                     QPlainTextEdit::keyPressEvent(e);
709                 }
710             }
711             else
712             {
713                 QPlainTextEdit::keyPressEvent(e);
714             }
715             break;
716     }
717 }
718 
eventFilter(QObject * watched,QEvent * event)719 bool MarkdownEditor::eventFilter(QObject* watched, QEvent* event)
720 {
721     if (event->type() == QEvent::MouseButtonPress)
722     {
723         mouseButtonDown = true;
724     }
725     else if (event->type() == QEvent::MouseButtonRelease)
726     {
727         mouseButtonDown = false;
728     }
729     else if (event->type() == QEvent::MouseButtonDblClick)
730     {
731         mouseButtonDown = true;
732     }
733 
734     if (event->type() != QEvent::ContextMenu || !spellCheckEnabled || this->isReadOnly())
735     {
736         return QPlainTextEdit::eventFilter(watched, event);
737     }
738     else
739     {
740         // Check spelling of text block under mouse
741         QContextMenuEvent* contextEvent = static_cast<QContextMenuEvent*>(event);
742 
743         // If the context menu event was triggered by pressing the menu key,
744         // use the current text cursor rather than the event position to get
745         // a cursor position, since the event position is the mouse position
746         // rather than the text cursor position.
747         //
748         if (QContextMenuEvent::Keyboard == contextEvent->reason())
749         {
750             cursorForWord = this->textCursor();
751         }
752         // Else process as mouse event.
753         //
754         else
755         {
756             cursorForWord = cursorForPosition(contextEvent->pos());
757         }
758 
759         QTextCharFormat::UnderlineStyle spellingErrorUnderlineStyle =
760             (QTextCharFormat::UnderlineStyle)
761             QApplication::style()->styleHint
762             (
763                 QStyle::SH_SpellCheckUnderlineStyle
764             );
765 
766         // Get the formatting for the cursor position under the mouse,
767         // and see if it has the spell check error underline style.
768         //
769         bool wordHasSpellingError = false;
770         int blockPosition = cursorForWord.positionInBlock();
771         QList<QTextLayout::FormatRange> formatList =
772                 cursorForWord.block().layout()->additionalFormats();
773         int mispelledWordStartPos = 0;
774         int mispelledWordLength = 0;
775 
776         for (int i = 0; i < formatList.length(); i++)
777         {
778             QTextLayout::FormatRange formatRange = formatList[i];
779 
780             if
781             (
782                 (blockPosition >= formatRange.start)
783                 && (blockPosition <= (formatRange.start + formatRange.length))
784                 && (formatRange.format.underlineStyle() == spellingErrorUnderlineStyle)
785             )
786             {
787                 mispelledWordStartPos = formatRange.start;
788                 mispelledWordLength = formatRange.length;
789                 wordHasSpellingError = true;
790                 break;
791             }
792         }
793 
794         // The word under the mouse is spelled correctly, so use the default
795         // processing for the context menu and return.
796         //
797         if (!wordHasSpellingError)
798         {
799             return QPlainTextEdit::eventFilter(watched, event);
800         }
801 
802         // Select the misspelled word.
803         cursorForWord.movePosition
804         (
805             QTextCursor::PreviousCharacter,
806             QTextCursor::MoveAnchor,
807             blockPosition - mispelledWordStartPos
808         );
809         cursorForWord.movePosition
810         (
811             QTextCursor::NextCharacter,
812             QTextCursor::KeepAnchor,
813             mispelledWordLength
814         );
815 
816         wordUnderMouse = cursorForWord.selectedText();
817         QStringList suggestions = dictionary.suggestions(wordUnderMouse);
818         QMenu* popupMenu = createStandardContextMenu();
819         QAction* firstAction = popupMenu->actions().first();
820 
821         spellingActions.clear();
822 
823         if (!suggestions.empty())
824         {
825             for (int i = 0; i < suggestions.size(); i++)
826             {
827                 QAction* suggestionAction = new QAction(suggestions[i], this);
828 
829                 // Need the following line because KDE Plasma 5 will insert a hidden ampersand
830                 // into the menu text as a keyboard accelerator.  Go off of the data in the
831                 // QAction rather than the text to avoid this.
832                 //
833                 suggestionAction->setData(suggestions[i]);
834 
835                 spellingActions.append(suggestionAction);
836                 popupMenu->insertAction(firstAction, suggestionAction);
837             }
838         }
839         else
840         {
841             QAction* noSuggestionsAction =
842                 new QAction(tr("No spelling suggestions found"), this);
843             noSuggestionsAction->setEnabled(false);
844             spellingActions.append(noSuggestionsAction);
845             popupMenu->insertAction(firstAction, noSuggestionsAction);
846         }
847 
848         popupMenu->insertSeparator(firstAction);
849         popupMenu->insertAction(firstAction, addWordToDictionaryAction);
850         popupMenu->insertSeparator(firstAction);
851         popupMenu->insertAction(firstAction, checkSpellingAction);
852         popupMenu->insertSeparator(firstAction);
853 
854         // Show menu
855         connect(popupMenu, SIGNAL(triggered(QAction*)), this, SLOT(suggestSpelling(QAction*)));
856 
857         QPoint menuPos;
858 
859         // If event was triggered by a key press, use the text cursor
860         // coordinates to display the popup menu.
861         //
862         if (QContextMenuEvent::Keyboard == contextEvent->reason())
863         {
864             QRect cr = this->cursorRect();
865             menuPos.setX(cr.x());
866             menuPos.setY(cr.y() + (cr.height() / 2));
867             menuPos = viewport()->mapToGlobal(menuPos);
868         }
869         // Else use the mouse coordinates from the context menu event.
870         //
871         else
872         {
873             menuPos = viewport()->mapToGlobal(contextEvent->pos());
874         }
875 
876         popupMenu->exec(menuPos);
877 
878         delete popupMenu;
879 
880         for (int i = 0; i < spellingActions.size(); i++)
881         {
882             delete spellingActions[i];
883         }
884 
885         spellingActions.clear();
886 
887         return true;
888     }
889 }
890 
wheelEvent(QWheelEvent * e)891 void MarkdownEditor::wheelEvent(QWheelEvent *e)
892 {
893     Qt::KeyboardModifiers modifier = e->modifiers();
894 
895 #if QT_VERSION >= 0x050000
896     int numDegrees = 0;
897 
898     QPoint angleDelta = e->angleDelta();
899 
900     if (!angleDelta.isNull())
901     {
902         numDegrees = angleDelta.y();
903     }
904 #else
905     int numDegrees = e->delta();
906 #endif
907 
908     if ((Qt::ControlModifier == modifier) && (0 != numDegrees))
909     {
910         int fontSize = this->font().pointSize();
911 
912         if (numDegrees > 0)
913         {
914             fontSize += 1;
915         }
916         else
917         {
918             fontSize -= 1;
919         }
920 
921         // check for negative value
922         if (fontSize <= 0)
923         {
924             fontSize = 1;
925         }
926 
927         setFont(this->font().family(), fontSize);
928         emit fontSizeChanged(fontSize);
929     }
930     else
931     {
932         QPlainTextEdit::wheelEvent(e);
933     }
934 }
935 
navigateDocument(const int pos)936 void MarkdownEditor::navigateDocument(const int pos)
937 {
938     QTextCursor cursor = this->textCursor();
939     cursor.setPosition(pos);
940     this->setTextCursor(cursor);
941     this->activateWindow();
942 }
943 
bold()944 void MarkdownEditor::bold()
945 {
946     insertFormattingMarkup("**");
947 }
948 
italic()949 void MarkdownEditor::italic()
950 {
951     insertFormattingMarkup("*");
952 }
953 
strikethrough()954 void MarkdownEditor::strikethrough()
955 {
956     insertFormattingMarkup("~~");
957 }
958 
insertComment()959 void MarkdownEditor::insertComment()
960 {
961     QTextCursor cursor = this->textCursor();
962 
963     if (cursor.hasSelection())
964     {
965         QString text = cursor.selectedText();
966         text = QString("<!-- " + text + " -->");
967         cursor.insertText(text);
968     }
969     else
970     {
971         cursor.insertText("<!--  -->");
972         cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, 4);
973         this->setTextCursor(cursor);
974     }
975 }
976 
createBulletListWithAsteriskMarker()977 void MarkdownEditor::createBulletListWithAsteriskMarker()
978 {
979     insertPrefixForBlocks("* ");
980 }
981 
createBulletListWithMinusMarker()982 void MarkdownEditor::createBulletListWithMinusMarker()
983 {
984     insertPrefixForBlocks("- ");
985 }
986 
createBulletListWithPlusMarker()987 void MarkdownEditor::createBulletListWithPlusMarker()
988 {
989     insertPrefixForBlocks("+ ");
990 }
991 
createNumberedListWithPeriodMarker()992 void MarkdownEditor::createNumberedListWithPeriodMarker()
993 {
994     createNumberedList('.');
995 }
996 
createNumberedListWithParenthesisMarker()997 void MarkdownEditor::createNumberedListWithParenthesisMarker()
998 {
999     createNumberedList(')');
1000 }
1001 
createTaskList()1002 void MarkdownEditor::createTaskList()
1003 {
1004     insertPrefixForBlocks("- [ ] ");
1005 }
1006 
createBlockquote()1007 void MarkdownEditor::createBlockquote()
1008 {
1009     insertPrefixForBlocks("> ");
1010 }
1011 
1012 // Algorithm lifted from ReText.
removeBlockquote()1013 void MarkdownEditor::removeBlockquote()
1014 {
1015     QTextCursor cursor = this->textCursor();
1016     QTextBlock block;
1017     QTextBlock end;
1018 
1019     if (cursor.hasSelection())
1020     {
1021         block = this->document()->findBlock(cursor.selectionStart());
1022         end = this->document()->findBlock(cursor.selectionEnd()).next();
1023     }
1024     else
1025     {
1026         block = cursor.block();
1027         end = block.next();
1028     }
1029 
1030     cursor.beginEditBlock();
1031 
1032     while (block != end)
1033     {
1034         cursor.setPosition(block.position());
1035 
1036         if (this->document()->characterAt(cursor.position()) == '>')
1037         {
1038             cursor.deleteChar();
1039 
1040             // Delete any spaces that follow the '>' character, to clean up the
1041             // paragraph.
1042             //
1043             while (this->document()->characterAt(cursor.position()) == ' ')
1044             {
1045                 cursor.deleteChar();
1046             }
1047         }
1048 
1049         block = block.next();
1050     }
1051 
1052     cursor.endEditBlock();
1053 }
1054 
1055 // Algorithm lifted from ReText.
indentText()1056 void MarkdownEditor::indentText()
1057 {
1058     QTextCursor cursor = this->textCursor();
1059 
1060     if (cursor.hasSelection())
1061     {
1062         QTextBlock block = this->document()->findBlock(cursor.selectionStart());
1063         QTextBlock end = this->document()->findBlock(cursor.selectionEnd()).next();
1064 
1065         cursor.beginEditBlock();
1066 
1067         while (block != end)
1068         {
1069             cursor.setPosition(block.position());
1070 
1071             if (this->insertSpacesForTabs)
1072             {
1073                 QString indentText = "";
1074 
1075                 for (int i = 0; i < tabWidth; i++)
1076                 {
1077                     indentText += QString(" ");
1078                 }
1079 
1080                 cursor.insertText(indentText);
1081             }
1082             else
1083             {
1084                 cursor.insertText("\t");
1085             }
1086 
1087             block = block.next();
1088         }
1089 
1090         cursor.endEditBlock();
1091     }
1092     else
1093     {
1094         int indent = tabWidth;
1095         QString indentText = "";
1096         QRegularExpressionMatch match;
1097 
1098         cursor.beginEditBlock();
1099 
1100         switch (cursor.block().userState())
1101         {
1102             case MarkdownStateNumberedList:
1103                 match = emptyNumberedListRegex.match(cursor.block().text());
1104 
1105                 if (match.hasMatch())
1106                 {
1107                     QStringList capture = match.capturedTexts();
1108 
1109                     // Restart numbering for the nested list.
1110                     if (capture.size() == 2)
1111                     {
1112                         QRegularExpression numberRegex("\\d+");
1113 
1114                         cursor.movePosition(QTextCursor::StartOfBlock);
1115                         cursor.movePosition
1116                         (
1117                             QTextCursor::EndOfBlock,
1118                             QTextCursor::KeepAnchor
1119                         );
1120 
1121                         QString replacementText = cursor.selectedText();
1122                         replacementText =
1123                             replacementText.replace
1124                             (
1125                                 numberRegex,
1126                                 "1"
1127                             );
1128 
1129                         cursor.insertText(replacementText);
1130                         cursor.movePosition(QTextCursor::StartOfBlock);
1131                     }
1132                 }
1133                 break;
1134             case MarkdownStateBulletPointList:
1135             {
1136                 if (emptyTaskListRegex.match(cursor.block().text()).hasMatch())
1137                 {
1138                     cursor.movePosition(QTextCursor::StartOfBlock);
1139                 }
1140                 else if (emptyBulletListRegex.match(cursor.block().text()).hasMatch())
1141                 {
1142                     if (bulletPointCyclingEnabled)
1143                     {
1144                         QChar oldBulletPoint = cursor.block().text().trimmed().at(0);
1145                         QChar newBulletPoint = oldBulletPoint;
1146                         {
1147                             if (oldBulletPoint == '*')
1148                             {
1149                                 newBulletPoint = '-';
1150                             }
1151                             else if (oldBulletPoint == '-')
1152                             {
1153                                 newBulletPoint = '+';
1154                             }
1155                             else
1156                             {
1157                                 newBulletPoint = '*';
1158                             }
1159                         }
1160 
1161                         cursor.movePosition(QTextCursor::StartOfBlock);
1162                         cursor.movePosition
1163                         (
1164                             QTextCursor::EndOfBlock,
1165                             QTextCursor::KeepAnchor
1166                         );
1167 
1168                         QString replacementText = cursor.selectedText();
1169                         replacementText =
1170                             replacementText.replace
1171                             (
1172                                 oldBulletPoint,
1173                                 newBulletPoint
1174                             );
1175                         cursor.insertText(replacementText);
1176                     }
1177 
1178                     cursor.movePosition(QTextCursor::StartOfBlock);
1179                 }
1180 
1181                 break;
1182             }
1183             default:
1184                 indent = tabWidth - (cursor.positionInBlock() % tabWidth);
1185                 break;
1186         }
1187 
1188         if (this->insertSpacesForTabs)
1189         {
1190             for (int i = 0; i < indent; i++)
1191             {
1192                 indentText += QString(" ");
1193             }
1194         }
1195         else
1196         {
1197             indentText = "\t";
1198         }
1199 
1200         cursor.insertText(indentText);
1201         cursor.endEditBlock();
1202     }
1203 }
1204 
1205 // Algorithm lifted from ReText.
unindentText()1206 void MarkdownEditor::unindentText()
1207 {
1208     QTextCursor cursor = this->textCursor();
1209     QTextBlock block;
1210     QTextBlock end;
1211 
1212     if (cursor.hasSelection())
1213     {
1214         block = this->document()->findBlock(cursor.selectionStart());
1215         end = this->document()->findBlock(cursor.selectionEnd()).next();
1216     }
1217     else
1218     {
1219         block = cursor.block();
1220         end = block.next();
1221     }
1222 
1223     cursor.beginEditBlock();
1224 
1225     while (block != end)
1226     {
1227         cursor.setPosition(block.position());
1228 
1229         if (this->document()->characterAt(cursor.position()) == '\t')
1230         {
1231             cursor.deleteChar();
1232         }
1233         else
1234         {
1235             int pos = 0;
1236 
1237             while
1238             (
1239                 (this->document()->characterAt(cursor.position()) == ' ')
1240                 && (pos < tabWidth)
1241             )
1242             {
1243                 pos += 1;
1244                 cursor.deleteChar();
1245             }
1246         }
1247 
1248         block = block.next();
1249     }
1250 
1251     if
1252     (
1253         (MarkdownStateBulletPointList == cursor.block().userState())
1254         && (emptyBulletListRegex.match(cursor.block().text()).hasMatch())
1255         && bulletPointCyclingEnabled
1256     )
1257     {
1258         QChar oldBulletPoint = cursor.block().text().trimmed().at(0);
1259         QChar newBulletPoint;
1260 
1261         if (oldBulletPoint == '*')
1262         {
1263             newBulletPoint = '+';
1264         }
1265         else if (oldBulletPoint == '-')
1266         {
1267             newBulletPoint = '*';
1268         }
1269         else
1270         {
1271             newBulletPoint = '-';
1272         }
1273 
1274         cursor.movePosition(QTextCursor::StartOfBlock);
1275         cursor.movePosition
1276         (
1277             QTextCursor::EndOfBlock,
1278             QTextCursor::KeepAnchor
1279         );
1280 
1281         QString replacementText = cursor.selectedText();
1282         replacementText =
1283             replacementText.replace
1284             (
1285                 oldBulletPoint,
1286                 newBulletPoint
1287             );
1288         cursor.insertText(replacementText);
1289     }
1290 
1291 
1292     cursor.endEditBlock();
1293 }
1294 
toggleTaskComplete()1295 bool MarkdownEditor::toggleTaskComplete()
1296 {
1297     QTextCursor cursor = textCursor();
1298     QTextBlock block;
1299     QTextBlock end;
1300 
1301     if (cursor.hasSelection())
1302     {
1303         block = this->document()->findBlock(cursor.selectionStart());
1304         end = this->document()->findBlock(cursor.selectionEnd()).next();
1305     }
1306     else
1307     {
1308         block = cursor.block();
1309         end = block.next();
1310     }
1311 
1312     cursor.beginEditBlock();
1313 
1314     while (block != end)
1315     {
1316         QRegularExpressionMatch match;
1317 
1318         if
1319         (
1320             (block.userState() == MarkdownStateBulletPointList)
1321             && (block.text().indexOf(taskListRegex, 0, &match) == 0)
1322         )
1323         {
1324             QStringList capture = match.capturedTexts();
1325 
1326             if (capture.size() == 2)
1327             {
1328                 QChar value = capture.at(1)[0];
1329                 QChar replacement;
1330                 int index = block.text().indexOf(" [");
1331 
1332                 if (index >= 0)
1333                 {
1334                     index += 2;
1335                 }
1336 
1337                 if (value == 'x')
1338                 {
1339                     replacement = ' ';
1340                 }
1341                 else
1342                 {
1343                     replacement = 'x';
1344                 }
1345 
1346                 cursor.setPosition(block.position());
1347                 cursor.movePosition(QTextCursor::StartOfBlock);
1348                 cursor.movePosition
1349                 (
1350                     QTextCursor::Right,
1351                     QTextCursor::MoveAnchor,
1352                     index
1353                 );
1354 
1355                 cursor.deleteChar();
1356                 cursor.insertText(replacement);
1357             }
1358         }
1359 
1360         block = block.next();
1361     }
1362 
1363     cursor.endEditBlock();
1364     return true;
1365 }
1366 
setEnableLargeHeadingSizes(bool enable)1367 void MarkdownEditor::setEnableLargeHeadingSizes(bool enable)
1368 {
1369     highlighter->setEnableLargeHeadingSizes(enable);
1370 }
1371 
setBlockquoteStyle(const BlockquoteStyle style)1372 void MarkdownEditor::setBlockquoteStyle(const BlockquoteStyle style)
1373 {
1374     highlighter->setBlockquoteStyle(style);
1375 }
1376 
setHighlightLineBreaks(bool enable)1377 void MarkdownEditor::setHighlightLineBreaks(bool enable)
1378 {
1379     highlighter->setHighlightLineBreaks(enable);
1380 }
1381 
setAutoMatchEnabled(bool enable)1382 void MarkdownEditor::setAutoMatchEnabled(bool enable)
1383 {
1384     autoMatchEnabled = enable;
1385 }
1386 
setAutoMatchEnabled(const QChar openingCharacter,bool enabled)1387 void MarkdownEditor::setAutoMatchEnabled(const QChar openingCharacter, bool enabled)
1388 {
1389     autoMatchFilter.insert(openingCharacter, enabled);
1390 }
1391 
setBulletPointCyclingEnabled(bool enable)1392 void MarkdownEditor::setBulletPointCyclingEnabled(bool enable)
1393 {
1394     bulletPointCyclingEnabled = enable;
1395 }
1396 
setUseUnderlineForEmphasis(bool enable)1397 void MarkdownEditor::setUseUnderlineForEmphasis(bool enable)
1398 {
1399     highlighter->setUseUnderlineForEmphasis(enable);
1400 }
1401 
setInsertSpacesForTabs(bool enable)1402 void MarkdownEditor::setInsertSpacesForTabs(bool enable)
1403 {
1404     insertSpacesForTabs = enable;
1405 }
1406 
setTabulationWidth(int width)1407 void MarkdownEditor::setTabulationWidth(int width)
1408 {
1409     QFontMetrics fontMetrics(font());
1410     tabWidth = width;
1411     this->setTabStopWidth(fontMetrics.width(QChar(' ')) * tabWidth);
1412 }
1413 
setEditorWidth(EditorWidth width)1414 void MarkdownEditor::setEditorWidth(EditorWidth width)
1415 {
1416     editorWidth = width;
1417 }
1418 
setEditorCorners(InterfaceStyle corners)1419 void MarkdownEditor::setEditorCorners(InterfaceStyle corners)
1420 {
1421     editorCorners = corners;
1422 }
1423 
runSpellChecker()1424 void MarkdownEditor::runSpellChecker()
1425 {
1426     if (this->spellCheckEnabled)
1427     {
1428         SpellChecker::checkDocument(this, highlighter, dictionary);
1429     }
1430     else
1431     {
1432         SpellChecker::checkDocument(this, NULL, dictionary);
1433     }
1434 }
1435 
setSpellCheckEnabled(const bool enabled)1436 void MarkdownEditor::setSpellCheckEnabled(const bool enabled)
1437 {
1438     spellCheckEnabled = enabled;
1439     highlighter->setSpellCheckEnabled(enabled);
1440 }
1441 
increaseFontSize()1442 void MarkdownEditor::increaseFontSize()
1443 {
1444     int fontSize = this->font().pointSize() + 1;
1445 
1446     setFont(this->font().family(), fontSize);
1447     emit fontSizeChanged(fontSize);
1448 
1449 }
1450 
decreaseFontSize()1451 void MarkdownEditor::decreaseFontSize()
1452 {
1453     int fontSize = this->font().pointSize() - 1;
1454 
1455     // check for negative value
1456     if (fontSize <= 0)
1457     {
1458         fontSize = 1;
1459     }
1460 
1461     setFont(this->font().family(), fontSize);
1462     emit fontSizeChanged(fontSize);
1463 }
1464 
suggestSpelling(QAction * action)1465 void MarkdownEditor::suggestSpelling(QAction* action)
1466 {
1467     if (action == addWordToDictionaryAction)
1468     {
1469         this->setTextCursor(cursorForWord);
1470         dictionary.addToPersonal(wordUnderMouse);
1471         this->highlighter->rehighlight();
1472     }
1473     else if (action == checkSpellingAction)
1474     {
1475         this->setTextCursor(cursorForWord);
1476         SpellChecker::checkDocument(this, highlighter, dictionary);
1477     }
1478     else if (spellingActions.contains(action))
1479     {
1480         cursorForWord.insertText(action->data().toString());
1481     }
1482 }
1483 
onContentsChanged(int position,int charsAdded,int charsRemoved)1484 void MarkdownEditor::onContentsChanged(int position, int charsAdded, int charsRemoved)
1485 {
1486     Q_UNUSED(position)
1487     Q_UNUSED(charsAdded)
1488     Q_UNUSED(charsRemoved)
1489 
1490     // Don't use the textChanged() or contentsChanged() (no parameters) signals
1491     // for checking if the typingResumed() signal needs to be emitted.  These
1492     // two signals are emitted even when the text formatting changes (i.e.,
1493     // when the QSyntaxHighlighter formats the text). Instead, use QTextDocument's
1494     // onContentsChanged(int, int, int) signal, which is only emitted when the
1495     // document text actually changes.
1496     //
1497     if (typingHasPaused || scaledTypingHasPaused)
1498     {
1499         typingHasPaused = false;
1500         scaledTypingHasPaused = false;
1501         typingPausedSignalSent = false;
1502         typingPausedScaledSignalSent = false;
1503         emit typingResumed();
1504     }
1505 }
1506 
onSelectionChanged()1507 void MarkdownEditor::onSelectionChanged()
1508 {
1509     QTextCursor cursor = this->textCursor();
1510 
1511     if (cursor.hasSelection())
1512     {
1513         emit textSelected
1514         (
1515             cursor.selectedText(),
1516             cursor.selectionStart(),
1517             cursor.selectionEnd()
1518         );
1519     }
1520     else
1521     {
1522         emit textDeselected();
1523     }
1524 }
1525 
focusText()1526 void MarkdownEditor::focusText()
1527 {
1528     if (FocusModeDisabled != focusMode)
1529     {
1530         QTextEdit::ExtraSelection beforeFadedSelection;
1531         QTextEdit::ExtraSelection afterFadedSelection;
1532         beforeFadedSelection.format.setForeground(fadeColor);
1533         beforeFadedSelection.cursor = this->textCursor();
1534         afterFadedSelection.format.setForeground(fadeColor);
1535         afterFadedSelection.cursor = this->textCursor();
1536 
1537         bool canFadePrevious = false;
1538 
1539         QList<QTextEdit::ExtraSelection> selections;
1540 
1541         switch (focusMode)
1542         {
1543             case FocusModeCurrentLine: // Current line
1544                 beforeFadedSelection.cursor.movePosition(QTextCursor::StartOfLine);
1545                 canFadePrevious = beforeFadedSelection.cursor.movePosition(QTextCursor::Up);
1546                 beforeFadedSelection.cursor.movePosition(QTextCursor::EndOfLine);
1547                 beforeFadedSelection.cursor.movePosition(QTextCursor::Start, QTextCursor::KeepAnchor);
1548 
1549                 if (canFadePrevious)
1550                 {
1551                     selections.append(beforeFadedSelection);
1552                 }
1553 
1554                 afterFadedSelection.cursor.movePosition(QTextCursor::EndOfLine);
1555                 afterFadedSelection.cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
1556                 selections.append(afterFadedSelection);
1557                 break;
1558 
1559             case FocusModeThreeLines: // Current line and previous two lines
1560                 beforeFadedSelection.cursor.movePosition(QTextCursor::StartOfLine);
1561                 canFadePrevious = beforeFadedSelection.cursor.movePosition(QTextCursor::Up, QTextCursor::MoveAnchor, 2);
1562                 beforeFadedSelection.cursor.movePosition(QTextCursor::EndOfLine);
1563                 beforeFadedSelection.cursor.movePosition(QTextCursor::Start, QTextCursor::KeepAnchor);
1564 
1565                 if (canFadePrevious)
1566                 {
1567                     selections.append(beforeFadedSelection);
1568                 }
1569 
1570                 afterFadedSelection.cursor.movePosition(QTextCursor::Down);
1571                 afterFadedSelection.cursor.movePosition(QTextCursor::EndOfLine);
1572                 afterFadedSelection.cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
1573                 selections.append(afterFadedSelection);
1574                 break;
1575 
1576             case FocusModeParagraph: // Current paragraph
1577                 canFadePrevious = beforeFadedSelection.cursor.movePosition(QTextCursor::StartOfBlock);
1578                 beforeFadedSelection.cursor.movePosition(QTextCursor::Start, QTextCursor::KeepAnchor);
1579                 selections.append(beforeFadedSelection);
1580                 afterFadedSelection.cursor.movePosition(QTextCursor::EndOfBlock);
1581                 afterFadedSelection.cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
1582                 selections.append(afterFadedSelection);
1583                 break;
1584 
1585             case FocusModeSentence: // Current sentence
1586             {
1587                 QTextBoundaryFinder boundaryFinder(QTextBoundaryFinder::Sentence, this->textCursor().block().text());
1588                 int currentPos = this->textCursor().positionInBlock();
1589                 int lastSentencePos = 0;
1590                 int nextSentencePos = 0;
1591 
1592                 boundaryFinder.setPosition(currentPos);
1593                 lastSentencePos = boundaryFinder.toPreviousBoundary();
1594                 boundaryFinder.setPosition(currentPos);
1595                 nextSentencePos = boundaryFinder.toNextBoundary();
1596 
1597                 if (lastSentencePos < 0)
1598                 {
1599                     beforeFadedSelection.cursor.movePosition(QTextCursor::StartOfBlock);
1600                 }
1601                 else
1602                 {
1603                     beforeFadedSelection.cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, currentPos - lastSentencePos);
1604                 }
1605 
1606                 beforeFadedSelection.cursor.movePosition(QTextCursor::Start, QTextCursor::KeepAnchor);
1607                 selections.append(beforeFadedSelection);
1608 
1609                 if (nextSentencePos < 0)
1610                 {
1611                     afterFadedSelection.cursor.movePosition(QTextCursor::EndOfBlock);
1612                 }
1613                 else
1614                 {
1615                     afterFadedSelection.cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, nextSentencePos - currentPos);
1616                 }
1617 
1618                 afterFadedSelection.cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
1619                 selections.append(afterFadedSelection);
1620 
1621                 break;
1622             }
1623             // `FocusModeTypewriter` implicitly handeled here as we don't highlight anything but center the current line.
1624             default:
1625                 break;
1626         }
1627 
1628         this->setExtraSelections(selections);
1629     }
1630 }
1631 
checkIfTypingPaused()1632 void MarkdownEditor::checkIfTypingPaused()
1633 {
1634     if (typingHasPaused && !typingPausedSignalSent)
1635     {
1636         typingPausedSignalSent = true;
1637         emit typingPaused();
1638     }
1639 
1640     typingTimer->stop();
1641     typingTimer->start(1000);
1642 
1643     typingHasPaused = true;
1644 }
1645 
checkIfTypingPausedScaled()1646 void MarkdownEditor::checkIfTypingPausedScaled()
1647 {
1648     if (scaledTypingHasPaused && !typingPausedScaledSignalSent)
1649     {
1650         typingPausedScaledSignalSent = true;
1651         emit typingPausedScaled();
1652     }
1653 
1654     // Scale timer interval based on document size.
1655     int interval = (document()->characterCount() / 30000) * 20;
1656 
1657     if (interval > 1000)
1658     {
1659         interval = 1000;
1660     }
1661     else if (interval < 20)
1662     {
1663         interval = 20;
1664     }
1665 
1666     scaledTypingTimer->stop();
1667     scaledTypingTimer->start(interval);
1668 
1669     scaledTypingHasPaused = true;
1670 }
1671 
spellCheckFinished(int result)1672 void MarkdownEditor::spellCheckFinished(int result)
1673 {
1674     Q_UNUSED(result)
1675 
1676     highlighter->rehighlight();
1677 }
1678 
onCursorPositionChanged()1679 void MarkdownEditor::onCursorPositionChanged()
1680 {
1681     if (!mouseButtonDown)
1682     {
1683         QRect cursor = this->cursorRect();
1684         QRect viewport = this->viewport()->rect();
1685         int bottom = viewport.bottom() - this->fontMetrics().height();
1686 
1687         if
1688         (
1689             (focusMode != FocusModeDisabled) ||
1690             (cursor.bottom() >= bottom) ||
1691             (cursor.top() <= viewport.top())
1692         )
1693         {
1694             centerCursor();
1695         }
1696     }
1697 
1698     // Set the text cursor back to visible and reset the blink timer so that
1699     // the cursor is always visible whenever it moves to a new position.
1700     //
1701     textCursorVisible = true;
1702     cursorBlinkTimer->stop();
1703     cursorBlinkTimer->start();
1704 
1705     // Update widget to ensure cursor is drawn.
1706     update();
1707 
1708     emit cursorPositionChanged(this->textCursor().position());
1709 }
1710 
toggleCursorBlink()1711 void MarkdownEditor::toggleCursorBlink()
1712 {
1713     textCursorVisible = !textCursorVisible;
1714     update();
1715 }
1716 
handleCarriageReturn()1717 void MarkdownEditor::handleCarriageReturn()
1718 {
1719     QString autoInsertText = "";
1720     QTextCursor cursor = this->textCursor();
1721     bool endList = false;
1722 
1723     if (cursor.positionInBlock() < (cursor.block().length() - 1))
1724     {
1725         autoInsertText = getPriorIndentation();
1726 
1727         if (cursor.positionInBlock() < autoInsertText.length())
1728         {
1729             autoInsertText.truncate(cursor.positionInBlock());
1730         }
1731     }
1732     else
1733     {
1734         QRegularExpressionMatch match;
1735 
1736         switch (cursor.block().userState())
1737         {
1738             case MarkdownStateNumberedList:
1739             {
1740                 autoInsertText = getPriorMarkdownBlockItemStart(numberedListRegex, match);
1741                 QStringList capture = match.capturedTexts();
1742 
1743                 if (!autoInsertText.isEmpty() && (capture.size() == 2))
1744                 {
1745                     // If the line of text is an empty list item, end the list.
1746                     if (cursor.block().text().length() == autoInsertText.length())
1747                     {
1748                         endList = true;
1749                     }
1750                     // Else auto-increment the list number.
1751                     else
1752                     {
1753                         QRegularExpression numberRegex("\\d+");
1754                         int number = capture.at(1).toInt();
1755                         number++;
1756                         autoInsertText =
1757                             autoInsertText.replace
1758                             (
1759                                 numberRegex,
1760                                 QString("%1").arg(number)
1761                             );
1762                     }
1763                 }
1764                 else
1765                 {
1766                     autoInsertText = getPriorIndentation();
1767                 }
1768                 break;
1769             }
1770             case MarkdownStateBulletPointList:
1771                 // Check for GFM task list before checking for bullet point.
1772                 autoInsertText = getPriorMarkdownBlockItemStart(taskListRegex, match);
1773 
1774                 // If the string is empty, then it wasn't a GFM task list item.
1775                 // Treat it as a normal bullet point.
1776                 //
1777                 if (autoInsertText.isEmpty())
1778                 {
1779                     autoInsertText = getPriorMarkdownBlockItemStart(bulletListRegex, match);
1780 
1781                     if (autoInsertText.isEmpty())
1782                     {
1783                         autoInsertText = getPriorIndentation();
1784                     }
1785                     // If the line of text is an empty list item, end the list.
1786                     else if (cursor.block().text().length() == autoInsertText.length())
1787                     {
1788                         endList = true;
1789                     }
1790                 }
1791                 else // string not empty - GFM task list item
1792                 {
1793                     // If the line of text is an empty list item, end the list.
1794                     if (cursor.block().text().length() == autoInsertText.length())
1795                     {
1796                         endList = true;
1797                     }
1798                     else
1799                     {
1800                         // In case the previous line had a completed task with
1801                         // an X checking it off, make sure a completed task
1802                         // isn't added as the new task (remove the x and replace
1803                         // with a space).
1804                         //
1805                         autoInsertText = autoInsertText.replace('x', ' ');
1806                     }
1807                 }
1808                 break;
1809             case MarkdownStateBlockquote:
1810                 autoInsertText = getPriorMarkdownBlockItemStart(blockquoteRegex, match);
1811                 break;
1812             default:
1813                 autoInsertText = getPriorIndentation();
1814                 break;
1815         }
1816     }
1817 
1818     if (endList)
1819     {
1820         autoInsertText = getPriorIndentation();
1821         cursor.movePosition(QTextCursor::StartOfBlock);
1822         cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
1823         cursor.insertText(autoInsertText);
1824         autoInsertText = "";
1825     }
1826 
1827     cursor.insertText(QString("\n") + autoInsertText);
1828     this->ensureCursorVisible();
1829 }
1830 
handleBackspaceKey()1831 bool MarkdownEditor::handleBackspaceKey()
1832 {
1833     QTextCursor cursor = textCursor();
1834 
1835     if (cursor.hasSelection())
1836     {
1837         return false;
1838     }
1839 
1840     int backtrackIndex = -1;
1841 
1842     switch (cursor.block().userState())
1843     {
1844         case MarkdownStateNumberedList:
1845         {
1846             if (emptyNumberedListRegex.match(textCursor().block().text()).hasMatch())
1847             {
1848                 backtrackIndex = cursor.block().text().indexOf(QRegularExpression("\\d"));
1849             }
1850             break;
1851         }
1852         case MarkdownStateBulletPointList:
1853             if
1854             (
1855                 emptyBulletListRegex.match(cursor.block().text()).hasMatch()
1856                 || emptyTaskListRegex.match(cursor.block().text()).hasMatch()
1857             )
1858             {
1859                 backtrackIndex = cursor.block().text().indexOf(QRegularExpression("[+*-]"));
1860             }
1861             break;
1862         case MarkdownStateBlockquote:
1863             if (emptyBlockquoteRegex.match(cursor.block().text()).hasMatch())
1864             {
1865                 backtrackIndex = cursor.block().text().lastIndexOf('>');
1866             }
1867             break;
1868         default:
1869             // If the first character in an automatched set is being
1870             // deleted, then delete the second matching one along with it.
1871             //
1872             if (autoMatchEnabled && (cursor.positionInBlock() > 0))
1873             {
1874                 QString blockText = cursor.block().text();
1875 
1876                 if (cursor.positionInBlock() < blockText.length())
1877                 {
1878                     QChar currentChar = blockText[cursor.positionInBlock()];
1879                     QChar previousChar = blockText[cursor.positionInBlock() - 1];
1880 
1881                     if (markupPairs.value(previousChar) == currentChar)
1882                     {
1883                         cursor.movePosition(QTextCursor::Left);
1884                         cursor.movePosition
1885                         (
1886                             QTextCursor::Right,
1887                             QTextCursor::KeepAnchor,
1888                             2
1889                         );
1890                         cursor.removeSelectedText();
1891                         return true;
1892                     }
1893                 }
1894             }
1895             break;
1896     }
1897 
1898     if (backtrackIndex >= 0)
1899     {
1900         cursor.movePosition(QTextCursor::StartOfBlock);
1901         cursor.movePosition
1902         (
1903             QTextCursor::Right,
1904             QTextCursor::MoveAnchor,
1905             backtrackIndex
1906         );
1907 
1908         cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
1909         cursor.removeSelectedText();
1910         return true;
1911     }
1912 
1913     return false;
1914 }
1915 
1916 // Algorithm lifted from ReText.
insertPrefixForBlocks(const QString & prefix)1917 void MarkdownEditor::insertPrefixForBlocks(const QString& prefix)
1918 {
1919     QTextCursor cursor = this->textCursor();
1920     QTextBlock block;
1921     QTextBlock end;
1922 
1923     if (cursor.hasSelection())
1924     {
1925         block = this->document()->findBlock(cursor.selectionStart());
1926         end = this->document()->findBlock(cursor.selectionEnd()).next();
1927     }
1928     else
1929     {
1930         block = cursor.block();
1931         end = block.next();
1932     }
1933 
1934     cursor.beginEditBlock();
1935 
1936     while (block != end)
1937     {
1938         cursor.setPosition(block.position());
1939         cursor.insertText(prefix);
1940         block = block.next();
1941     }
1942 
1943     cursor.endEditBlock();
1944 }
1945 
createNumberedList(const QChar marker)1946 void MarkdownEditor::createNumberedList(const QChar marker)
1947 {
1948     QTextCursor cursor = this->textCursor();
1949     QTextBlock block;
1950     QTextBlock end;
1951 
1952     if (cursor.hasSelection())
1953     {
1954         block = this->document()->findBlock(cursor.selectionStart());
1955         end = this->document()->findBlock(cursor.selectionEnd()).next();
1956     }
1957     else
1958     {
1959         block = cursor.block();
1960         end = block.next();
1961     }
1962 
1963     cursor.beginEditBlock();
1964 
1965     int number = 1;
1966 
1967     while (block != end)
1968     {
1969         cursor.setPosition(block.position());
1970         cursor.insertText(QString("%1").arg(number) + marker + " ");
1971         block = block.next();
1972         number++;
1973     }
1974 
1975     cursor.endEditBlock();
1976 }
1977 
insertPairedCharacters(const QChar firstChar)1978 bool MarkdownEditor::insertPairedCharacters(const QChar firstChar)
1979 {
1980     if
1981     (
1982         autoMatchEnabled
1983         && markupPairs.contains(firstChar)
1984         && autoMatchFilter.value(firstChar)
1985     )
1986     {
1987         QChar lastChar = markupPairs.value(firstChar);
1988         QTextCursor cursor = this->textCursor();
1989         QTextBlock block;
1990         QTextBlock end;
1991 
1992         if (cursor.hasSelection())
1993         {
1994             block = this->document()->findBlock(cursor.selectionStart());
1995             end = this->document()->findBlock(cursor.selectionEnd());
1996 
1997             // Only surround selection with matched characters if the
1998             // selection belongs to the same block.
1999             //
2000             if (block == end)
2001             {
2002                 cursor.beginEditBlock();
2003                 cursor.setPosition(cursor.selectionStart());
2004                 cursor.insertText(firstChar);
2005                 cursor.setPosition(textCursor().selectionEnd());
2006                 cursor.insertText(lastChar);
2007                 cursor.endEditBlock();
2008 
2009                 cursor = this->textCursor();
2010                 cursor.setPosition(cursor.selectionStart());
2011                 cursor.setPosition
2012                 (
2013                     textCursor().selectionEnd() - 1,
2014                     QTextCursor::KeepAnchor
2015                 );
2016                 setTextCursor(cursor);
2017                 return true;
2018             }
2019         }
2020         else
2021         {
2022             // Get the previous character.  Ensure that it is whitespace.
2023             int blockPos = cursor.positionInBlock();
2024             bool doMatch = true;
2025 
2026             // If not at the beginning of the line...
2027             if (blockPos > 0)
2028             {
2029                 blockPos--;
2030 
2031                 if (!cursor.block().text()[blockPos].isSpace())
2032                 {
2033                     // If the previous character is not whitespace, allow
2034                     // character matching only for parentheses and similar
2035                     // characters that need matching even if preceeded by
2036                     // non-whitespace (i.e., for mathematical or computer
2037                     // science expressions).  Otherwise, do not match the
2038                     // opening character.
2039                     //
2040                     switch (firstChar.toLatin1())
2041                     {
2042                         case '(':
2043                         case '[':
2044                         case '{':
2045                         case '<':
2046                             break;
2047                         default:
2048                             doMatch = false;
2049                             break;
2050                     }
2051                 }
2052             }
2053 
2054             if (doMatch)
2055             {
2056                 cursor.insertText(firstChar);
2057                 cursor.insertText(lastChar);
2058                 cursor.movePosition(QTextCursor::PreviousCharacter);
2059                 setTextCursor(cursor);
2060                 return true;
2061             }
2062         }
2063     }
2064 
2065     return false;
2066 }
2067 
handleEndPairCharacterTyped(const QChar ch)2068 bool MarkdownEditor::handleEndPairCharacterTyped(const QChar ch)
2069 {
2070     QTextCursor cursor = this->textCursor();
2071 
2072     bool lookAhead = false;
2073 
2074     if (autoMatchEnabled && !cursor.hasSelection())
2075     {
2076         QList<QChar> values = markupPairs.values();
2077 
2078         if (values.contains(ch))
2079         {
2080             QChar key = markupPairs.key(ch);
2081 
2082             if (autoMatchFilter.value(key))
2083             {
2084                 lookAhead = true;
2085             }
2086         }
2087     }
2088 
2089     if (lookAhead)
2090     {
2091         QTextCursor cursor = this->textCursor();
2092         QString text = cursor.block().text();
2093         int pos = cursor.positionInBlock();
2094 
2095         if (pos < (text.length()))
2096         {
2097             // Look ahead to the character after the cursor position. If it
2098             // matches the character that was entered, then move the cursor
2099             // one position forward.
2100             //
2101             if (text[pos] == ch)
2102             {
2103                 cursor.movePosition(QTextCursor::NextCharacter);
2104                 setTextCursor(cursor);
2105                 return true;
2106             }
2107         }
2108     }
2109 
2110     return false;
2111 }
2112 
handleWhitespaceInEmptyMatch(const QChar whitespace)2113 bool MarkdownEditor::handleWhitespaceInEmptyMatch(const QChar whitespace)
2114 {
2115     QTextCursor cursor = this->textCursor();
2116     QTextBlock block = cursor.block();
2117     QString text = block.text();
2118     int pos = cursor.positionInBlock();
2119 
2120     if
2121     (
2122         (text.length() > 0) &&
2123         (pos > 0) &&
2124         (pos < text.length()) &&
2125         nonEmptyMarkupPairs.contains(text[pos - 1]) &&
2126         (text[pos] == nonEmptyMarkupPairs.value(text[pos - 1]))
2127     )
2128     {
2129         cursor.deleteChar();
2130         cursor.insertText(whitespace);
2131         return true;
2132     }
2133 
2134     return false;
2135 }
2136 
insertFormattingMarkup(const QString & markup)2137 void MarkdownEditor::insertFormattingMarkup(const QString& markup)
2138 {
2139     QTextCursor cursor = this->textCursor();
2140 
2141     if (cursor.hasSelection())
2142     {
2143         int start = cursor.selectionStart();
2144         int end = cursor.selectionEnd() + markup.length();
2145         QTextCursor c = cursor;
2146         c.beginEditBlock();
2147         c.setPosition(start);
2148         c.insertText(markup);
2149         c.setPosition(end);
2150         c.insertText(markup);
2151         c.endEditBlock();
2152         cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::QTextCursor::KeepAnchor, markup.length());
2153         this->setTextCursor(cursor);
2154     }
2155     else
2156     {
2157         // Insert markup twice (for opening and closing around the cursor),
2158         // and then move the cursor to be between the pair.
2159         //
2160         cursor.beginEditBlock();
2161         cursor.insertText(markup);
2162         cursor.insertText(markup);
2163         cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, markup.length());
2164         cursor.endEditBlock();
2165         this->setTextCursor(cursor);
2166     }
2167 }
2168 
getPriorIndentation()2169 QString MarkdownEditor::getPriorIndentation()
2170 {
2171     QString indent = "";
2172     QTextCursor cursor = this->textCursor();
2173     QTextBlock block = cursor.block();
2174 
2175     QString text = block.text();
2176 
2177     for (int i = 0; i < text.length(); i++)
2178     {
2179         if (text[i].isSpace())
2180         {
2181             indent += text[i];
2182         }
2183         else
2184         {
2185             return indent;
2186         }
2187     }
2188 
2189     return indent;
2190 }
2191 
getPriorMarkdownBlockItemStart(const QRegularExpression & itemRegex,QRegularExpressionMatch & match)2192 QString MarkdownEditor::getPriorMarkdownBlockItemStart
2193 (
2194     const QRegularExpression& itemRegex,
2195     QRegularExpressionMatch& match
2196 )
2197 {
2198     QTextCursor cursor = this->textCursor();
2199     QTextBlock block = cursor.block();
2200 
2201     QString text = block.text();
2202 
2203     if ((text.indexOf(itemRegex, 0, &match) >= 0) && match.hasMatch())
2204     {
2205         return match.captured();
2206     }
2207 
2208     return QString("");
2209 }
2210 
atBlockAreaStart(const QTextBlock & block,MarkdownEditor::BlockType & type) const2211 bool MarkdownEditor::atBlockAreaStart(const QTextBlock& block, MarkdownEditor::BlockType& type) const
2212 {
2213     if (!block.isValid())
2214     {
2215         type = BlockTypeNone;
2216         return false;
2217     }
2218 
2219     if (atCodeBlockStart(block))
2220     {
2221         type = BlockTypeCode;
2222         return true;
2223     }
2224 
2225     if (isBlockquote(block))
2226     {
2227         type = BlockTypeQuote;
2228         return true;
2229     }
2230 
2231     type = BlockTypeNone;
2232     return false;
2233 }
2234 
atBlockAreaEnd(const QTextBlock & block,const MarkdownEditor::BlockType type) const2235 bool MarkdownEditor::atBlockAreaEnd(const QTextBlock& block, const MarkdownEditor::BlockType type) const
2236 {
2237     switch (type)
2238     {
2239         case BlockTypeCode:
2240             return atCodeBlockEnd(block);
2241         case BlockTypeQuote:
2242             return !isBlockquote(block);
2243         default:
2244             return true;
2245     }
2246 }
2247 
atCodeBlockStart(const QTextBlock & block) const2248 bool MarkdownEditor::atCodeBlockStart(const QTextBlock& block) const
2249 {
2250     return
2251         (
2252             (MarkdownStateCodeBlock == block.userState())
2253             || (MarkdownStateInGithubCodeFence == block.userState())
2254             || (MarkdownStateInPandocCodeFence == block.userState())
2255         );
2256 }
2257 
atCodeBlockEnd(const QTextBlock & block) const2258 bool MarkdownEditor::atCodeBlockEnd(const QTextBlock& block) const
2259 {
2260     return
2261         (
2262             (MarkdownStateCodeBlock != block.userState())
2263             && (MarkdownStateInPandocCodeFence != block.userState())
2264             && (MarkdownStateInGithubCodeFence != block.userState())
2265             && (MarkdownStateCodeFenceEnd != block.userState())
2266         );
2267 }
2268 
isBlockquote(const QTextBlock & block) const2269 bool MarkdownEditor::isBlockquote(const QTextBlock& block) const
2270 {
2271     return (MarkdownStateBlockquote == block.userState());
2272 }
2273