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