1 /************************************************************************
2 **
3 **  Copyright (C) 2019-2021 Doug Massay
4 **  Copyright (C) 2015-2021 Kevin B. Hendricks, Stratford Ontario Canada
5 **  Copyright (C) 2012      John Schember <john@nachtimwald.com>
6 **  Copyright (C) 2012-2013 Dave Heiland
7 **  Copyright (C) 2012      Grant Drake
8 **  Copyright (C) 2009-2011 Strahinja Markovic  <strahinja.markovic@gmail.com>, Nokia Corporation
9 **
10 **  This file is part of Sigil.
11 **
12 **  Sigil is free software: you can redistribute it and/or modify
13 **  it under the terms of the GNU General Public License as published by
14 **  the Free Software Foundation, either version 3 of the License, or
15 **  (at your option) any later version.
16 **
17 **  Sigil is distributed in the hope that it will be useful,
18 **  but WITHOUT ANY WARRANTY; without even the implied warranty of
19 **  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 **  GNU General Public License for more details.
21 **
22 **  You should have received a copy of the GNU General Public License
23 **  along with Sigil.  If not, see <http://www.gnu.org/licenses/>.
24 **
25 *************************************************************************/
26 
27 #include <memory>
28 
29 #include <QtCore/QFileInfo>
30 #include <QtGui/QContextMenuEvent>
31 #include <QtCore/QSignalMapper>
32 #include <QtWidgets/QAction>
33 #include <QtWidgets/QMenu>
34 #include <QtGui/QPainter>
35 #include <QtWidgets/QScrollBar>
36 #include <QtWidgets/QShortcut>
37 #include <QtCore/QXmlStreamReader>
38 #include <QRegularExpression>
39 #include <QRegularExpressionMatch>
40 #include <QRegularExpressionMatchIterator>
41 #include <QPointer>
42 #include <QDebug>
43 
44 #include "BookManipulation/Book.h"
45 #include "BookManipulation/CleanSource.h"
46 #include "BookManipulation/XhtmlDoc.h"
47 #include "MainUI/MainWindow.h"
48 #include "Parsers/GumboInterface.h"
49 // #include "Misc/XHTMLHighlighter.h"
50 #include "Misc/XHTMLHighlighter2.h"
51 #include "Dialogs/ClipEditor.h"
52 #include "Misc/CSSHighlighter.h"
53 #include "Misc/SettingsStore.h"
54 #include "Misc/SpellCheck.h"
55 #include "Misc/HTMLSpellCheck.h"
56 #include "Misc/Utility.h"
57 #include "Parsers/HTMLStyleInfo.h"
58 #include "PCRE/PCRECache.h"
59 #include "ViewEditors/CodeViewEditor.h"
60 #include "ViewEditors/LineNumberArea.h"
61 #include "sigil_constants.h"
62 
63 const int PROGRESS_BAR_MINIMUM_DURATION = 1000;
64 const QString BREAK_TAG_INSERT    = "<hr class=\"sigil_split_marker\" />";
65 
66 static const int TAB_SPACES_WIDTH        = 4;
67 static const int LINE_NUMBER_MARGIN      = 5;
68 
69 static const QString XML_OPENING_TAG        = "(<[^>/][^>]*[^>/]>|<[^>/]>)";
70 static const QString NEXT_CLOSE_TAG_LOCATION = "</\\s*[^>]+>";
71 static const QString NEXT_TAG_LOCATION      = "<[^!>]+>";
72 static const QString TAG_NAME_SEARCH        = "<\\s*([^\\s>]+)";
73 static const QString STYLE_ATTRIBUTE_SEARCH = "style\\s*=\\s*\"[^\"]*\"";
74 static const QString ATTRIBUTE_NAME_POSTFIX_SEARCH = "\\s*=\\s*\"[^\"]*\"";
75 static const QString ATTRIB_VALUES_SEARCH   = "\"([^\"]*)";
76 
77 static const QString OPEN_TAG_STARTS_SELECTION = "^\\s*(<\\s*([a-zA-Z0-9]+)[^>]*>)";
78 static const QString STARTING_INDENT_USED = "(^\\s*)[^\\s]";
79 
80 static const int MAX_SPELLING_SUGGESTIONS = 10;
81 
82 
CodeViewEditor(HighlighterType high_type,bool check_spelling,QWidget * parent)83 CodeViewEditor::CodeViewEditor(HighlighterType high_type, bool check_spelling, QWidget *parent)
84     :
85     QPlainTextEdit(parent),
86     m_isUndoAvailable(false),
87     m_LastBlockCount(0),
88     m_LineNumberAreaBlockNumber(-1),
89     m_LineNumberArea(new LineNumberArea(this)),
90     m_ScrollOneLineUp(new QShortcut(QKeySequence(Qt::ControlModifier + Qt::Key_Up), this, 0, 0, Qt::WidgetShortcut)),
91     m_ScrollOneLineDown(new QShortcut(QKeySequence(Qt::ControlModifier + Qt::Key_Down), this, 0, 0, Qt::WidgetShortcut)),
92     m_isLoadFinished(false),
93     m_DelayedCursorScreenCenteringRequired(false),
94     m_CaretUpdate(QList<ElementIndex>()),
95     m_checkSpelling(check_spelling),
96     m_reformatCSSEnabled(false),
97     m_reformatHTMLEnabled(false),
98     m_lastFindRegex(QString()),
99     m_spellingMapper(new QSignalMapper(this)),
100     m_addSpellingMapper(new QSignalMapper(this)),
101     m_addDictMapper(new QSignalMapper(this)),
102     m_ignoreSpellingMapper(new QSignalMapper(this)),
103     m_clipMapper(new QSignalMapper(this)),
104     m_MarkedTextStart(-1),
105     m_MarkedTextEnd(-1),
106     m_ReplacingInMarkedText(false),
107     m_regen_taglist(true)
108 {
109     if (high_type == CodeViewEditor::Highlight_XHTML) {
110         // m_Highlighter = new XHTMLHighlighter(check_spelling, this);
111         m_Highlighter = new XHTMLHighlighter2(check_spelling, this);
112     } else if (high_type == CodeViewEditor::Highlight_CSS) {
113         m_Highlighter = new CSSHighlighter(this);
114     } else {
115         m_Highlighter = NULL;
116     }
117 
118     setFocusPolicy(Qt::StrongFocus);
119     ConnectSignalsToSlots();
120     SetAppearance();
121 }
122 
~CodeViewEditor()123 CodeViewEditor::~CodeViewEditor()
124 {
125     m_ScrollOneLineUp->deleteLater();
126     m_ScrollOneLineDown->deleteLater();
127 }
128 
SetAppearance()129 void CodeViewEditor::SetAppearance()
130 {
131     SettingsStore settings;
132     if (Utility::IsDarkMode()) {
133         // qDebug() << "IsDarkMode returned: true";
134         m_codeViewAppearance = settings.codeViewDarkAppearance();
135     } else {
136         // qDebug() << "IsDarkMode returned: false";
137         m_codeViewAppearance = settings.codeViewAppearance();
138     }
139 
140     SetAppearanceColors();
141     UpdateLineNumberAreaMargin();
142     HighlightCurrentLine(false);
143     setFrameStyle(QFrame::NoFrame);
144     // Set the Zoom factor but be sure no signals are set because of this.
145     m_CurrentZoomFactor = settings.zoomText();
146     Zoom();
147 }
148 
149 
sizeHint() const150 QSize CodeViewEditor::sizeHint() const
151 {
152     return QSize(16777215, 16777215);
153 }
154 
155 
CustomSetDocument(TextDocument & ndocument)156 void CodeViewEditor::CustomSetDocument(TextDocument &ndocument)
157 {
158     setDocument(&ndocument);
159     ndocument.setModified(false);
160     if (m_Highlighter) {
161         m_Highlighter->setDocument(&ndocument);
162         // The QSyntaxHighlighter will setup a singleShot timer to do the highlighting
163         // in response to setDocument being called. This causes a problem because we
164         // cannot control at what point it finishes, and the textChanged signal of the
165         // QTextDocument gets fired by the syntax highlighting. This in turn causes issues
166         // with our own logic trying to do stuff in response to genunine document changes.
167         // So we will synchronously highlight now, and block signals while doing so.
168         RehighlightDocument();
169     }
170 
171     ResetFont();
172     m_isLoadFinished = true;
173     m_regen_taglist = true;
174     emit DocumentSet();
175 }
176 
DeleteLine()177 void CodeViewEditor::DeleteLine()
178 {
179     if (document()->isEmpty()) {
180         return;
181     }
182 
183     QTextCursor cursor = textCursor();
184     cursor.beginEditBlock();
185     cursor.select(QTextCursor::LineUnderCursor);
186     cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 1);
187     cursor.removeSelectedText();
188     cursor.endEditBlock();
189     emit selectionChanged();
190 }
191 
MarkSelection()192 bool CodeViewEditor::MarkSelection()
193 {
194     if (textCursor().hasSelection()) {
195         m_MarkedTextStart = textCursor().selectionStart();
196         m_MarkedTextEnd = textCursor().selectionEnd();
197         HighlightCurrentLine();
198         return true;
199     }
200     ClearMarkedText();
201     return false;
202 }
203 
ClearMarkedText()204 bool CodeViewEditor::ClearMarkedText()
205 {
206     bool marked = IsMarkedText();
207     m_MarkedTextStart = -1;
208     m_MarkedTextEnd = -1;
209     HighlightCurrentLine(false);
210     return marked;
211 }
212 
HighlightMarkedText()213 void CodeViewEditor::HighlightMarkedText()
214 {
215     QTextCharFormat format;
216 
217     QList<QTextEdit::ExtraSelection> extraSelections;
218     QTextEdit::ExtraSelection selection;
219     selection.cursor = textCursor();
220     selection.format.setUnderlineStyle(QTextCharFormat::DotLine);
221     selection.format.setFontUnderline(true);
222     selection.cursor.clearSelection();
223     selection.cursor.setPosition(0);
224     int textlen = textLength();
225     selection.cursor.setPosition(textlen);
226     extraSelections.append(selection);
227     setExtraSelections(extraSelections);
228     extraSelections.clear();
229 
230     if (m_MarkedTextStart < 0 || m_MarkedTextEnd > textlen) {
231         return;
232     }
233     selection.format.setUnderlineStyle(QTextCharFormat::DotLine);
234     selection.format.setFontUnderline(true);
235     selection.cursor = textCursor();
236     selection.cursor.clearSelection();
237     selection.cursor.setPosition(m_MarkedTextStart);
238     selection.cursor.setPosition(m_MarkedTextEnd, QTextCursor::KeepAnchor);
239     extraSelections.append(selection);
240     setExtraSelections(extraSelections);
241 }
242 
IsMarkedText()243 bool CodeViewEditor::IsMarkedText()
244 {
245     return m_MarkedTextStart >= 0 && m_MarkedTextEnd > 0;
246 }
247 
MoveToMarkedText(Searchable::Direction direction,bool wrap)248 bool CodeViewEditor::MoveToMarkedText(Searchable::Direction direction, bool wrap)
249 {
250     if (!IsMarkedText()) {
251         return false;
252     }
253     int pos = textCursor().position();
254     if (pos >= m_MarkedTextStart && pos <= m_MarkedTextEnd) {
255         return true;
256     }
257 
258     bool moved = false;
259 
260     if (direction == Searchable::Direction_Up) {
261         if (wrap || pos > m_MarkedTextEnd) {
262             pos = m_MarkedTextEnd;
263             moved = true;
264         }
265     } else {
266         if (wrap || pos < m_MarkedTextStart) {
267             pos = m_MarkedTextStart;
268             moved = true;
269         }
270     }
271 
272     if (moved) {
273         QTextCursor cursor = textCursor();
274         cursor.setPosition(pos);
275         setTextCursor(cursor);
276     }
277     return moved;
278 }
279 
CutCodeTags()280 void CodeViewEditor::CutCodeTags()
281 {
282     // If selection starts or ends in the middle of a tag, then do nothing
283     if (!IsCutCodeTagsAllowed()) {
284         return;
285     }
286 
287     QTextCursor cursor = textCursor();
288     int start = cursor.selectionStart();
289     QString selected_text = textCursor().selectedText();
290     QString new_text = StripCodeTags(selected_text);
291     cursor.beginEditBlock();
292     cursor.removeSelectedText();
293     cursor.insertText(new_text);
294     cursor.endEditBlock();
295     cursor.setPosition(start);
296     cursor.setPosition(start + new_text.count(), QTextCursor::KeepAnchor);
297     setTextCursor(cursor);
298 }
299 
CutTagPair()300 void CodeViewEditor::CutTagPair()
301 {
302     // cursor must be in a tag, with no selection active at all
303     if (!IsCutTagPairAllowed()) return;
304 
305     QTextCursor cursor = textCursor();
306     int pos = cursor.selectionStart();
307     int newpos = pos;
308     int open_tag_pos = -1;
309     int open_tag_len = -1;
310     int close_tag_pos = -1;
311     int close_tag_len = -1;
312     int i = m_TagList.findFirstTagOnOrAfter(pos);
313     TagLister::TagInfo ti = m_TagList.at(i);
314     if ((pos >= ti.pos) && (pos < ti.pos + ti.len)) {
315         // removing the body or html tags freaks out QWebEnginePage in Preview
316         if (ti.tname == "body" || ti.tname == "html") return;
317         if(ti.ttype == "end") {
318             newpos = ti.pos - ti.open_len;
319             open_tag_pos = ti.open_pos;
320             open_tag_len = ti.open_len;
321             close_tag_pos = ti.pos;
322             close_tag_len = ti.len;
323         } else { // all others single, begin, xmlheader, doctype, comment, etc
324              newpos = ti.pos;
325              open_tag_pos = ti.pos;
326              open_tag_len = ti.len;
327         }
328         if (ti.ttype == "begin") {
329             int j = m_TagList.findCloseTagForOpen(i);
330             if (j >= 0) {
331                 if (m_TagList.at(j).len != -1) {
332                     close_tag_pos = m_TagList.at(j).pos;
333                     close_tag_len = m_TagList.at(j).len;
334                 }
335             }
336         }
337     }
338     if (open_tag_len != -1 || close_tag_len != -1) {
339         // handle close tag removal first to not mess up position info
340         cursor.beginEditBlock();
341         if (close_tag_len != -1) {
342             cursor.setPosition(close_tag_pos + close_tag_len);
343             cursor.setPosition(close_tag_pos, QTextCursor::KeepAnchor);
344             cursor.removeSelectedText();
345         }
346         if (open_tag_len != -1) {
347             cursor.setPosition(open_tag_pos + open_tag_len);
348             cursor.setPosition(open_tag_pos, QTextCursor::KeepAnchor);
349             cursor.removeSelectedText();
350         }
351         cursor.endEditBlock();
352         cursor.setPosition(newpos);
353         setTextCursor(cursor);
354     }
355 }
356 
TextIsSelected()357 bool CodeViewEditor::TextIsSelected()
358 {
359     return textCursor().hasSelection();
360 }
361 
TextIsSelectedAndNotInStartOrEndTag()362 bool CodeViewEditor::TextIsSelectedAndNotInStartOrEndTag()
363 {
364     if (!textCursor().hasSelection()) {
365         return false;
366     }
367 
368     MaybeRegenerateTagList();
369     QString text = m_TagList.getSource();
370     int pos = textCursor().selectionStart();
371     int end = textCursor().selectionEnd()-1;
372 
373     if ((text[pos] == "<") && (text[end] == ">")) return true;
374 
375     if (IsPositionInTag(pos) || IsPositionInTag(end)) {
376         return false;
377     }
378 
379     return true;
380 }
381 
IsCutCodeTagsAllowed()382 bool CodeViewEditor::IsCutCodeTagsAllowed()
383 {
384     return TextIsSelectedAndNotInStartOrEndTag();
385 }
386 
IsCutTagPairAllowed()387 bool CodeViewEditor::IsCutTagPairAllowed()
388 {
389     if (textCursor().hasSelection()) return false;
390     return IsPositionInTag(textCursor().selectionStart());
391 }
392 
IsInsertClosingTagAllowed()393 bool CodeViewEditor::IsInsertClosingTagAllowed()
394 {
395     int pos = textCursor().selectionStart();
396     if (IsPositionInTag(pos)) {
397         // special case of cursor |<tag>
398         QString text = m_TagList.getSource();
399         if (text[pos] != '<') return false;
400     }
401     return true;
402 }
403 
StripCodeTags(QString text)404 QString CodeViewEditor::StripCodeTags(QString text)
405 {
406     QString new_text;
407     bool in_tag = false;
408 
409     // Remove anything between and including < and >
410     for (int i = 0; i < text.count(); i++) {
411         QChar c = text.at(i);
412 
413         if (!in_tag && c != QChar('<')) {
414             new_text.append(c);
415         }
416 
417         if (c == QChar('<')) {
418             in_tag = true;
419         }
420 
421         if (in_tag && c == QChar('>')) {
422             in_tag = false;
423         }
424     }
425 
426     return new_text;
427 }
428 
SplitSection()429 QString CodeViewEditor::SplitSection()
430 {
431     int split_position = textCursor().position();
432 
433     MaybeRegenerateTagList();
434     QString text = m_TagList.getSource();
435 
436     // Abort splitting the section if user is within a tag - MainWindow will display a status message
437     if (IsPositionInTag(split_position)) {
438         // exempt the case of cursor |<tag>
439         if (text[split_position]!= '<') return QString();
440     }
441 
442     // abort if no body tags exist
443     int bo = m_TagList.findBodyOpenTag();
444     int bc = m_TagList.findBodyCloseTag();
445     if (bo == -1 || bc == -1) return QString();
446 
447     int body_tag_start = m_TagList.at(bo).pos;
448     int body_tag_end   = body_tag_start + m_TagList.at(bo).len;
449     int body_contents_end = m_TagList.at(bc).pos;
450     QString head = text.left(body_tag_start);
451 
452     if (split_position < body_tag_end) {
453         // Cursor is before the start of the body
454         split_position = body_tag_end;
455     }
456     if (split_position > body_contents_end) {
457         // Cursor is after or in the closing body tag
458         split_position = body_contents_end;
459     }
460 
461     const QStringList &opening_tags = GetUnmatchedTagsForBlock(split_position);
462 
463     QString text_segment = "<p>&#160;</p>";
464     if (split_position != body_tag_end) {
465         text_segment = Utility::Substring(body_tag_start, split_position, text);
466     }
467 
468     // This splits off from contents of body from top to the split position
469     // Remove the text that will be in the new section from the View.
470     QTextCursor cursor = textCursor();
471     cursor.beginEditBlock();
472     cursor.setPosition(body_tag_end);
473     cursor.setPosition(split_position, QTextCursor::KeepAnchor);
474     cursor.removeSelectedText();
475 
476     // We add a newline if the next tag is sitting right next to the end of the body tag.
477     if (toPlainText().at(body_tag_end) == QChar('<')) {
478         cursor.insertBlock();
479     }
480 
481     // We identify any open tags for the current caret position, and repeat
482     // those at the caret position to ensure we have valid html
483     if (!opening_tags.isEmpty()) {
484         cursor.insertText(opening_tags.join(""));
485     }
486 
487     cursor.endEditBlock();
488 
489     QString new_section = head + text_segment + "\n</body>\n</html>";
490     return new_section;
491 }
492 
493 
InsertSGFSectionMarker()494 void CodeViewEditor::InsertSGFSectionMarker()
495 {
496     textCursor().insertText(BREAK_TAG_INSERT);
497 }
498 
499 
InsertClosingTag()500 void CodeViewEditor::InsertClosingTag()
501 {
502     if (!IsInsertClosingTagAllowed()) {
503         emit ShowStatusMessageRequest(tr("Cannot insert closing tag at this position."));
504         return;
505     }
506 
507     int pos = textCursor().position(); // was -1 but that is not right
508     const QStringList unmatched_tags = GetUnmatchedTagsForBlock(pos);
509 
510     if (unmatched_tags.isEmpty()) {
511         emit ShowStatusMessageRequest(tr("No open tags found at this position."));
512         return;
513     }
514 
515     QString tag = unmatched_tags.last();
516     QRegularExpression tag_name_search(TAG_NAME_SEARCH);
517     QRegularExpressionMatch mo = tag_name_search.match(tag);
518     int tag_name_index = mo.capturedStart();
519 
520     if (tag_name_index >= 0) {
521         const QString closing_tag = "</" %  mo.captured(1) % ">";
522         textCursor().insertText(closing_tag);
523     }
524 }
525 
526 
LineNumberAreaPaintEvent(QPaintEvent * event)527 void CodeViewEditor::LineNumberAreaPaintEvent(QPaintEvent *event)
528 {
529     QPainter painter(m_LineNumberArea);
530     // Paint the background first
531     painter.fillRect(event->rect(), m_codeViewAppearance.line_number_background_color);
532     // A "block" represents a line of text
533     QTextBlock block = firstVisibleBlock();
534     // Blocks are numbered from zero,
535     // but we count lines of text from one
536     int blockNumber  = block.blockNumber() + 1;
537 
538     // We loop through all the visible and
539     // unobscured blocks and paint line numbers for each
540     while (block.isValid()) {
541         // Getting the Y coordinates for the top of a block.
542         int topY = (int) blockBoundingGeometry(block).translated(contentOffset()).top();
543 
544         // Ignore blocks that are not visible.
545         if (!block.isVisible() || (topY > event->rect().bottom())) {
546             break;
547         }
548 
549         // Draw the number in the line number area.
550         painter.setPen(m_codeViewAppearance.line_number_foreground_color);
551         QString number_to_paint = QString::number(blockNumber);
552         painter.drawText(- LINE_NUMBER_MARGIN,
553                          topY,
554                          m_LineNumberArea->width(),
555                          fontMetrics().height(),
556                          Qt::AlignRight,
557                          number_to_paint
558                         );
559         // Move to the next block and block number.
560         block = block.next();
561         blockNumber++;
562     }
563 }
564 
565 
LineNumberAreaMouseEvent(QMouseEvent * event)566 void CodeViewEditor::LineNumberAreaMouseEvent(QMouseEvent *event)
567 {
568     QTextCursor cursor = cursorForPosition(QPoint(0, event->pos().y()));
569 
570     if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonDblClick) {
571         if (event->button() == Qt::LeftButton) {
572             QTextCursor selection = cursor;
573             selection.setVisualNavigation(true);
574             m_LineNumberAreaBlockNumber = selection.blockNumber();
575             selection.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
576             selection.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
577             setTextCursor(selection);
578         }
579     } else if (m_LineNumberAreaBlockNumber >= 0) {
580         QTextCursor selection = cursor;
581         selection.setVisualNavigation(true);
582 
583         if (event->type() == QEvent::MouseMove) {
584             QTextBlock anchorBlock = document()->findBlockByNumber(m_LineNumberAreaBlockNumber);
585             selection.setPosition(anchorBlock.position());
586 
587             if (cursor.blockNumber() < m_LineNumberAreaBlockNumber) {
588                 selection.movePosition(QTextCursor::EndOfBlock);
589                 selection.movePosition(QTextCursor::Right);
590             }
591 
592             selection.setPosition(cursor.block().position(), QTextCursor::KeepAnchor);
593 
594             if (cursor.blockNumber() >= m_LineNumberAreaBlockNumber) {
595                 selection.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
596                 selection.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
597             }
598         } else {
599             m_LineNumberAreaBlockNumber = -1;
600             return;
601         }
602 
603         setTextCursor(selection);
604     }
605 }
606 
607 
CalculateLineNumberAreaWidth()608 int CodeViewEditor::CalculateLineNumberAreaWidth()
609 {
610     int current_block_count = blockCount();
611     // QTextDocument::setPlainText sets the current block count
612     // to 1 before updating it (for no damn good reason), but
613     // we need it to *not* be 1, ever.
614     int last_line_number = current_block_count != 1 ? current_block_count : m_LastBlockCount;
615     m_LastBlockCount = last_line_number;
616     int num_digits = 1;
617 
618     // We count the number of digits
619     // for the line number of the last line
620     while (last_line_number >= 10) {
621         last_line_number /= 10;
622         num_digits++;
623     }
624 
625 #if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
626     return LINE_NUMBER_MARGIN * 2 + fontMetrics().horizontalAdvance(QChar('0')) * num_digits;
627 #else
628     return LINE_NUMBER_MARGIN * 2 + fontMetrics().width(QChar('0')) * num_digits;
629 #endif
630 
631 }
632 
633 
ReplaceDocumentText(const QString & new_text)634 void CodeViewEditor::ReplaceDocumentText(const QString &new_text)
635 {
636     QTextCursor cursor = textCursor();
637     cursor.beginEditBlock();
638     cursor.select(QTextCursor::Document);
639     cursor.removeSelectedText();
640     cursor.insertText(new_text);
641     cursor.endEditBlock();
642     m_regen_taglist = true; // just in case
643 }
644 
645 
ScrollToTop()646 void CodeViewEditor::ScrollToTop()
647 {
648     verticalScrollBar()->setValue(0);
649 }
650 
651 // Note: using
652 //     QTextCursor cursor(document());
653 // instead of:
654 //     QTextCursor cursor = textCursor();
655 // creates a new text coursor that points to the document top
656 // and so loses the state of the current textCursor if one exists
657 // and this includes any existing highlighting asscoiated with it
658 
ScrollToPosition(int cursor_position,bool center_screen)659 void CodeViewEditor::ScrollToPosition(int cursor_position, bool center_screen)
660 {
661     if (cursor_position < 0) {
662         return;
663     }
664 
665     QTextCursor cursor(document());
666     cursor.setPosition(cursor_position);
667     setTextCursor(cursor);
668 
669     // If height is 0, then the widget is still collapsed
670     // and centering the screen will do squat.
671     if (center_screen) {
672         if (height() > 0) {
673             centerCursor();
674         } else {
675             m_DelayedCursorScreenCenteringRequired = true;
676         }
677 
678         m_CaretUpdate.clear();
679     }
680 }
681 
ScrollToLine(int line)682 void CodeViewEditor::ScrollToLine(int line)
683 {
684     if (line <= 0) {
685         return;
686     }
687 
688     QTextCursor cursor(document());
689     cursor.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor, line - 1);
690     // Make sure the cursor ends up within a tag so that it stays in position on switching to Book View.
691     cursor.movePosition(QTextCursor::NextWord);
692     setTextCursor(cursor);
693 
694     // If height is 0, then the widget is still collapsed
695     // and centering the screen will do squat.
696     if (height() > 0) {
697         centerCursor();
698     } else {
699         m_DelayedCursorScreenCenteringRequired = true;
700     }
701 }
702 
ScrollToFragment(const QString & fragment)703 void CodeViewEditor::ScrollToFragment(const QString &fragment)
704 {
705     if (fragment.isEmpty()) {
706         ScrollToLine(1);
707         return;
708     }
709 
710     QRegularExpression fragment_search("id=\"" % fragment % "\"");
711     int index = toPlainText().indexOf(fragment_search);
712     ScrollToPosition(index);
713 }
714 
715 // return the length in QChars in plain text inside
textLength() const716 int CodeViewEditor::textLength() const
717 {
718     TextDocument * doc = qobject_cast<TextDocument *> (document());
719     return doc->textLength();
720 }
721 
722 // overrides document toPlainText to prevent loss of nbsp
toPlainText() const723 QString CodeViewEditor::toPlainText() const
724 {
725     if (!m_isLoadFinished) return QString();
726     TextDocument * doc = qobject_cast<TextDocument *> (document());
727     return doc->toText();
728 }
729 
730 // overrides createMimeDataFromSelection()
createMimeDataFromSelection() const731 QMimeData *CodeViewEditor::createMimeDataFromSelection() const
732 {
733   QString selected_text = textCursor().selectedText();
734   selected_text = selected_text.replace(QChar::ParagraphSeparator, '\n');
735   QMimeData* md = new QMimeData();
736   md->setText(selected_text);
737   return md;
738 }
739 
IsLoadingFinished()740 bool CodeViewEditor::IsLoadingFinished()
741 {
742     return m_isLoadFinished;
743 }
744 
GetCursorPosition() const745 int CodeViewEditor::GetCursorPosition() const
746 {
747     const int position = textCursor().position();
748     return position;
749 }
750 
GetCursorLine() const751 int CodeViewEditor::GetCursorLine() const
752 {
753     const QTextCursor cursor = textCursor();
754     const QTextBlock block = cursor.block();
755     const int line = block.blockNumber() + 1;
756     return line;
757 }
758 
759 
GetCursorColumn() const760 int CodeViewEditor::GetCursorColumn() const
761 {
762     const QTextCursor cursor = textCursor();
763     const QTextBlock block = cursor.block();
764     const int column = cursor.position() - block.position() + 1;
765     return column;
766 }
767 
768 
SetZoomFactor(float factor)769 void CodeViewEditor::SetZoomFactor(float factor)
770 {
771     SettingsStore settings;
772     settings.setZoomText(factor);
773     m_CurrentZoomFactor = factor;
774     Zoom();
775     emit ZoomFactorChanged(factor);
776 }
777 
778 
GetZoomFactor() const779 float CodeViewEditor::GetZoomFactor() const
780 {
781     SettingsStore settings;
782     return settings.zoomText();
783 }
784 
785 
Zoom()786 void CodeViewEditor::Zoom()
787 {
788     QFont current_font = font();
789     current_font.setPointSizeF(m_codeViewAppearance.font_size * m_CurrentZoomFactor);
790     setFont(current_font);
791     UpdateLineNumberAreaFont(current_font);
792 }
793 
794 
UpdateDisplay()795 void CodeViewEditor::UpdateDisplay()
796 {
797     SettingsStore settings;
798     float stored_factor = settings.zoomText();
799 
800     if (stored_factor != m_CurrentZoomFactor) {
801         m_CurrentZoomFactor = stored_factor;
802         Zoom();
803     }
804 }
805 
GetMisspelledWord(const QString & text,int start_offset,int end_offset,const QString & search_regex,Searchable::Direction search_direction)806 SPCRE::MatchInfo CodeViewEditor::GetMisspelledWord(const QString &text, int start_offset, int end_offset, const QString &search_regex, Searchable::Direction search_direction)
807 {
808     SPCRE::MatchInfo match_info;
809     HTMLSpellCheck::MisspelledWord misspelled_word;
810 
811     if (search_direction == Searchable::Direction_Up) {
812         if (end_offset > 0) {
813             end_offset -= 1;
814         }
815 
816         misspelled_word =  HTMLSpellCheck::GetLastMisspelledWord(text, start_offset, end_offset, search_regex);
817     } else {
818         misspelled_word =  HTMLSpellCheck::GetFirstMisspelledWord(text, start_offset, end_offset, search_regex);
819     }
820 
821     if (!misspelled_word.text.isEmpty()) {
822         match_info.offset.first = misspelled_word.offset - start_offset;
823         match_info.offset.second = match_info.offset.first + misspelled_word.length;
824     }
825 
826     return match_info;
827 }
828 
FindNext(const QString & search_regex,Searchable::Direction search_direction,bool misspelled_words,bool ignore_selection_offset,bool wrap,bool marked_text)829 bool CodeViewEditor::FindNext(const QString &search_regex,
830                               Searchable::Direction search_direction,
831                               bool misspelled_words,
832                               bool ignore_selection_offset,
833                               bool wrap,
834                               bool marked_text)
835 {
836     SPCRE *spcre = PCRECache::instance()->getObject(search_regex);
837     SPCRE::MatchInfo match_info;
838     QString txt = toPlainText();
839     int start_offset = 0;
840     int start = 0;
841     int end = txt.length();
842     if (marked_text) {
843         if (!MoveToMarkedText(search_direction, wrap)) {
844             return false;
845         }
846         start = m_MarkedTextStart;
847         end = m_MarkedTextEnd;
848         start_offset = m_MarkedTextStart;
849     }
850     int selection_offset = GetSelectionOffset(search_direction, ignore_selection_offset, marked_text);
851 
852     if (search_direction == Searchable::Direction_Up) {
853         if (misspelled_words) {
854             match_info = GetMisspelledWord(txt, 0, selection_offset, search_regex, search_direction);
855         } else {
856             match_info = spcre->getLastMatchInfo(Utility::Substring(start, selection_offset, txt));
857         }
858     } else {
859         if (misspelled_words) {
860             match_info = GetMisspelledWord(txt, selection_offset, txt.count(), search_regex, search_direction);
861         } else {
862             match_info = spcre->getFirstMatchInfo(Utility::Substring(selection_offset, end, txt));
863         }
864 
865         start_offset = selection_offset;
866     }
867 
868     if (marked_text) {
869         // If not in marked text it's not a real match.
870         if (match_info.offset.second + start_offset > m_MarkedTextEnd ||
871             match_info.offset.first + start_offset < m_MarkedTextStart) {
872             match_info.offset.first = -1;
873         }
874     }
875 
876     if (match_info.offset.first != -1) {
877         // We will scroll the position on screen in order to ensure the entire block is visible
878         // and if not, then center the match.
879         SelectAndScrollIntoView(match_info.offset.first + start_offset, match_info.offset.second + start_offset,
880                                 search_direction, ignore_selection_offset);
881         // We store our offset after the selection changing event which would otherwise reset it
882         m_lastFindRegex = search_regex;
883         m_lastMatch = match_info;
884         m_lastMatch.offset.first += start_offset;
885         m_lastMatch.offset.second += start_offset;
886         return true;
887     } else if (wrap) {
888         if (FindNext(search_regex, search_direction, misspelled_words, true, false, marked_text)) {
889             ShowWrapIndicator(this);
890             return true;
891         }
892     }
893 
894     return false;
895 }
896 
897 
Count(const QString & search_regex,Searchable::Direction direction,bool wrap,bool marked_text)898 int CodeViewEditor::Count(const QString &search_regex, Searchable::Direction direction, bool wrap, bool marked_text)
899 {
900     SPCRE *spcre = PCRECache::instance()->getObject(search_regex);
901     QString text= toPlainText();
902     int start = 0;
903     int end = text.length();
904 
905     if (marked_text) {
906         if (!MoveToMarkedText(direction, wrap)) {
907             return 0;
908         }
909         start = m_MarkedTextStart;
910         end = m_MarkedTextEnd;
911     }
912     if (!wrap) {
913         if (direction == Searchable::Direction_Up) {
914             text = Utility::Substring(start, textCursor().position(), text);
915         } else {
916             text = Utility::Substring(textCursor().position(), end, text);
917         }
918     } else if (marked_text) {
919         text = Utility::Substring(start, end, text);
920     }
921     return spcre->getEveryMatchInfo(text).count();
922 }
923 
924 
ReplaceSelected(const QString & search_regex,const QString & replacement,Searchable::Direction direction,bool replace_current)925 bool CodeViewEditor::ReplaceSelected(const QString &search_regex, const QString &replacement, Searchable::Direction direction, bool replace_current)
926 {
927     SPCRE *spcre = PCRECache::instance()->getObject(search_regex);
928     int selection_start = textCursor().selectionStart();
929     int selection_end = textCursor().selectionEnd();
930 
931     // It is only safe to do a replace if we have not changed the selection or find text
932     // since we last did a Find.
933     if ((search_regex != m_lastFindRegex) || (m_lastMatch.offset.first == -1)) {
934         return false;
935     }
936 
937     // Convert to plain text or \s won't get newlines
938     const QString &document_text = toPlainText();
939     QString selected_text = Utility::Substring(selection_start, selection_end, document_text);
940     QString replaced_text;
941     bool replacement_made = false;
942     bool in_marked_text = selection_start >= m_MarkedTextStart && selection_end <= m_MarkedTextEnd;
943     if (in_marked_text) {
944         m_ReplacingInMarkedText = true;
945     }
946     int original_text_length = textLength();
947     replacement_made = spcre->replaceText(selected_text, m_lastMatch.capture_groups_offsets, replacement, replaced_text);
948 
949     if (replacement_made) {
950         QTextCursor cursor = textCursor();
951         int start = cursor.position();
952         // Replace the selected text with our replacement text.
953         cursor.beginEditBlock();
954         cursor.removeSelectedText();
955         cursor.insertText(replaced_text);
956         cursor.clearSelection();
957         cursor.endEditBlock();
958 
959         // Select the new text
960         if (replace_current) {
961             if (direction == Searchable::Direction_Up) {
962                 cursor.setPosition(start + replaced_text.length());
963                 cursor.setPosition(start, QTextCursor::KeepAnchor);
964             } else {
965                 cursor.setPosition(selection_start);
966                 cursor.setPosition(selection_start + replaced_text.length(), QTextCursor::KeepAnchor);
967             }
968         } else if (direction == Searchable::Direction_Up) {
969             // Find for next match done after will set selection
970             cursor.setPosition(selection_start);
971         }
972 
973         setTextCursor(cursor);
974 
975         // Adjust size of marked text.
976         if (in_marked_text) {
977             m_ReplacingInMarkedText = false;
978             m_MarkedTextEnd += textLength() - original_text_length;
979         }
980 
981         if (!hasFocus()) {
982             // The replace operation is being performed where focus is elsewhere (like in the F&R combos)
983             // If the user does not click back into the tab, these changes will not be saved yet, which
984             // means if the switch to another tab (such as a BV tab after doing a F&R in CSS) they will
985             // not see the result of those changes. So we will emit a FocusLost event, which will trigger
986             // the saving of the tab content and all associated ResourceModified signals to fire.
987             emit FocusLost(this);
988         }
989     }
990 
991     HighlightCurrentLine();
992     return replacement_made;
993 }
994 
995 
ReplaceAll(const QString & search_regex,const QString & replacement,Searchable::Direction direction,bool wrap,bool marked_text)996 int CodeViewEditor::ReplaceAll(const QString &search_regex,
997                                const QString &replacement,
998                                Searchable::Direction direction,
999                                bool wrap,
1000                                bool marked_text)
1001 {
1002     int count = 0;
1003     QString text = toPlainText();
1004     int original_position = textCursor().position();
1005     int position = original_position;
1006     if (marked_text) {
1007         m_ReplacingInMarkedText = true;
1008         if (!MoveToMarkedText(direction, wrap)) {
1009             return 0;
1010         }
1011         // Restrict replace to the marked area.
1012         text = Utility::Substring(m_MarkedTextStart, m_MarkedTextEnd, text);
1013         position = original_position - m_MarkedTextStart;
1014     }
1015     int marked_text_length = text.length();
1016 
1017     SPCRE *spcre = PCRECache::instance()->getObject(search_regex);
1018     QList<SPCRE::MatchInfo> match_info = spcre->getEveryMatchInfo(text);
1019 
1020     // Run though all match offsets making the replacement in reverse order.
1021     // This way changes in text length won't change the offsets as we make
1022     // our changes.
1023     for (int i = match_info.count() - 1; i >= 0; i--) {
1024         QString replaced_text;
1025         if (!wrap) {
1026             if (direction == Searchable::Direction_Up) {
1027                 if (match_info.at(i).offset.first > position) {
1028                     break;
1029                 }
1030             } else {
1031                 if (match_info.at(i).offset.second < position) {
1032                     break;
1033                 }
1034             }
1035         }
1036 
1037         bool replacement_made = spcre->replaceText(Utility::Substring(match_info.at(i).offset.first, match_info.at(i).offset.second, text), match_info.at(i).capture_groups_offsets, replacement, replaced_text);
1038 
1039         if (replacement_made) {
1040             // Replace the text.
1041             text = text.replace(match_info.at(i).offset.first, match_info.at(i).offset.second - match_info.at(i).offset.first, replaced_text);
1042             count++;
1043         }
1044     }
1045     if (marked_text) {
1046         // Merge the replaced marked text into the original text and adjust the marker.
1047         QString replaced_text = toPlainText();
1048         replaced_text.replace(m_MarkedTextStart, m_MarkedTextEnd - m_MarkedTextStart, text);
1049         m_MarkedTextEnd += text.length() - marked_text_length;
1050         text = replaced_text;
1051     }
1052 
1053     QTextCursor cursor = textCursor();
1054     // Store the cursor position
1055     int cursor_position = cursor.selectionStart();
1056     cursor.beginEditBlock();
1057     // Replace all text in the document with the new text.
1058     cursor.setPosition(cursor_position);
1059     cursor.select(QTextCursor::Document);
1060     cursor.insertText(text);
1061     cursor.endEditBlock();
1062 
1063     // Restore the cursor position
1064     cursor.setPosition(cursor_position);
1065     setTextCursor(cursor);
1066 
1067     HighlightCurrentLine();
1068 
1069     if (marked_text) {
1070         m_ReplacingInMarkedText = false;
1071     }
1072 
1073     if (!hasFocus()) {
1074         // The replace operation is being performed where focus is elsewhere (like in the F&R combos)
1075         // If the user does not click back into the tab, these changes will not be saved yet, which
1076         // means if the switch to another tab (such as a BV tab after doing a F&R in CSS) they will
1077         // not see the result of those changes. So we will emit a FocusLost event, which will trigger
1078         // the saving of the tab content and all associated ResourceModified signals to fire.
1079         emit FocusLost(this);
1080     }
1081 
1082     return count;
1083 }
1084 
ResetLastFindMatch()1085 void CodeViewEditor::ResetLastFindMatch()
1086 {
1087     m_lastMatch.offset.first = -1;
1088 }
1089 
GetSelectedText()1090 QString CodeViewEditor::GetSelectedText()
1091 {
1092     return textCursor().selectedText();
1093 }
1094 
SetUpFindForSelectedText(const QString & search_regex)1095 void CodeViewEditor::SetUpFindForSelectedText(const QString &search_regex)
1096 {
1097     // When a user hits Ctrl+F to load up Find text for the selection, they will want to be
1098     // able to follow that up with a Replace. Since ReplaceSelected() requires the PCRE
1099     // lastMatchInfo to be setup, we must effectively do a Find for the selected text.
1100     // However this is complicated by the fact that the auto-tokenise and other F&R options
1101     // may mean that the search_regex when ReplaceSelected() is called can be different
1102     // to what regex would minimally match the selection. So instead we require this function
1103     // to be passed the regex that is derived from the selected text with current options.
1104     QTextCursor cursor = textCursor();
1105     const int selection_start = cursor.selectionStart();
1106     const int selection_end = cursor.selectionEnd();
1107     cursor.setPosition(selection_start);
1108     setTextCursor(cursor);
1109     bool found = FindNext(search_regex, Searchable::Direction_Down, false, false, false);
1110 
1111     if (!found) {
1112         // We have an edge case where the text selected is not a match for this regex text.
1113         cursor.setPosition(selection_end);
1114         cursor.setPosition(selection_start, QTextCursor::KeepAnchor);
1115         setTextCursor(cursor);
1116     }
1117 }
1118 
1119 // The base class implementation of the print()
1120 // method is not a slot, and we need it as a slot
1121 // for print preview support; so this is just
1122 // a slot wrapper around that function
print(QPagedPaintDevice * printer)1123 void CodeViewEditor::print(QPagedPaintDevice *printer)
1124 {
1125     QPlainTextEdit::print(printer);
1126 }
1127 
1128 // Overridden because we need to update the cursor
1129 // location if a cursor update (from BookView)
1130 // is waiting to be processed
event(QEvent * event)1131 bool CodeViewEditor::event(QEvent *event)
1132 {
1133     // We just return whatever the "real" event handler returns
1134     bool real_return = QPlainTextEdit::event(event);
1135 
1136     // Executing the caret update inside the paint event
1137     // handler causes artifacts on mac. So we do it after
1138     // the event is processed and accepted.
1139     if (event->type() == QEvent::Paint) {
1140         DelayedCursorScreenCentering();
1141     }
1142 
1143     return real_return;
1144 }
1145 
1146 
1147 // Overridden because after updating itself on a resize event,
1148 // the editor needs to update its line number area too
resizeEvent(QResizeEvent * event)1149 void CodeViewEditor::resizeEvent(QResizeEvent *event)
1150 {
1151     // Update self normally
1152     QPlainTextEdit::resizeEvent(event);
1153     QRect contents_area = contentsRect();
1154     // Now update the line number area
1155     m_LineNumberArea->setGeometry(QRect(contents_area.left(),
1156                                         contents_area.top(),
1157                                         CalculateLineNumberAreaWidth(),
1158                                         contents_area.height()
1159                                        )
1160                                  );
1161 }
1162 
1163 
mouseDoubleClickEvent(QMouseEvent * event)1164 void CodeViewEditor::mouseDoubleClickEvent(QMouseEvent *event)
1165 {
1166     // Propagate to base class first then handle locally
1167     QPlainTextEdit::mouseDoubleClickEvent(event);
1168 
1169     // record the initial position in case later changed by doubleclick event
1170     QTextCursor cursor = textCursor();
1171     int pos = cursor.selectionStart();
1172     bool isShift = QApplication::keyboardModifiers() & Qt::ShiftModifier;
1173     bool isAlt = QApplication::keyboardModifiers() & Qt::AltModifier;
1174     // qDebug() << "Modifiers: " << QApplication::keyboardModifiers();
1175 
1176     if (!isShift && !isAlt) return;
1177 
1178     if (!IsPositionInTag(pos)){
1179         return;
1180     }
1181 
1182     // if Shift is used select just the tag's contents, but not the tag itself
1183     // if Alt (option key on macOS) is used select the tag's contents and that tag itself
1184     int open_tag_pos = -1;
1185     int open_tag_len = -1;
1186     int close_tag_pos = -1;
1187     int close_tag_len = -1;
1188     int i = m_TagList.findFirstTagOnOrAfter(pos);
1189     TagLister::TagInfo ti = m_TagList.at(i);
1190     if ((pos >= ti.pos) && (pos < ti.pos + ti.len)) {
1191         if(ti.ttype == "end") {
1192             open_tag_pos = ti.open_pos;
1193             open_tag_len = ti.open_len;
1194             close_tag_pos = ti.pos;
1195             close_tag_len = ti.len;
1196         } else { // all others single, begin, doctype, xmlheader, cdata, etc
1197              open_tag_pos = ti.pos;
1198              open_tag_len = ti.len;
1199         }
1200         if (ti.ttype == "begin") {
1201             int j = m_TagList.findCloseTagForOpen(i);
1202             if (j >= 0) {
1203                 if (m_TagList.at(j).len != -1) {
1204                     close_tag_pos = m_TagList.at(j).pos;
1205                     close_tag_len = m_TagList.at(j).len;
1206                 }
1207             }
1208         }
1209     }
1210     if (open_tag_len != -1 || close_tag_len != -1) {
1211         int selstart;
1212         int selend;
1213         if (isShift) {
1214             selstart = open_tag_pos + open_tag_len;
1215             selend = selstart;
1216             if (close_tag_len != -1) selend = close_tag_pos;
1217         } else {
1218             selstart = open_tag_pos;
1219             selend = open_tag_pos + open_tag_len;
1220             if (close_tag_len != -1) selend = close_tag_pos + close_tag_len;
1221         }
1222         cursor.setPosition(selstart);
1223         cursor.setPosition(selend, QTextCursor::KeepAnchor);
1224         setTextCursor(cursor);
1225     }
1226 }
1227 
1228 
mousePressEvent(QMouseEvent * event)1229 void CodeViewEditor::mousePressEvent(QMouseEvent *event)
1230 {
1231     // When a right-click occurs, move the caret location if this is performed.
1232     // outside the currently selected text.
1233     if (event->button() == Qt::RightButton) {
1234         QTextCursor cursor = cursorForPosition(event->pos());
1235 
1236         if (cursor.position() < textCursor().selectionStart() || cursor.position() > textCursor().selectionEnd()) {
1237             setTextCursor(cursor);
1238         }
1239     }
1240 
1241     // Propagate to base class
1242     QPlainTextEdit::mousePressEvent(event);
1243     // Allow open link with Ctrl-mouseclick - after propagation sets cursor position
1244     bool isCtrl = QApplication::keyboardModifiers() & Qt::ControlModifier;
1245 
1246     if (isCtrl) {
1247         GoToLinkOrStyle();
1248     }
1249 }
1250 
mouseReleaseEvent(QMouseEvent * event)1251 void CodeViewEditor::mouseReleaseEvent(QMouseEvent *event)
1252 {
1253     emit PageClicked();
1254     QPlainTextEdit::mouseReleaseEvent(event);
1255 }
1256 
1257 
1258 // Overridden so we can block the focus out signal for Windows.
1259 // Right clicking and calling the context menu will cause the
1260 // editor to loose focus. When it looses focus the code is checked
1261 // if it is well formed. If it is not a message box is shown asking
1262 // if the user would like to auto correct. This causes the context
1263 // menu to disappear and thus be inaccessible to the user.
1264 
contextMenuEvent(QContextMenuEvent * event)1265 void CodeViewEditor::contextMenuEvent(QContextMenuEvent *event)
1266 {
1267     // Need to use QPointer to prevent crashes on macOS when closing
1268     // parent during qmenu exec.  See discussion at:
1269     // https://www.qtcentre.org/threads/65046-closing-parent-widget-during-QMenu-exec()
1270     QPointer<QMenu> menu = createStandardContextMenu();
1271 
1272     if (m_reformatCSSEnabled) {
1273         AddReformatCSSContextMenu(menu);
1274     }
1275 
1276     if (m_reformatHTMLEnabled) {
1277         AddReformatHTMLContextMenu(menu);
1278     }
1279 
1280     AddMarkSelectionMenu(menu);
1281     AddGoToLinkOrStyleContextMenu(menu);
1282     AddClipContextMenu(menu);
1283 
1284     if (m_checkSpelling) {
1285         AddSpellCheckContextMenu(menu);
1286     }
1287 
1288     if (InViewableImage()) {
1289         AddViewImageContextMenu(menu);
1290     }
1291 
1292     menu->exec(event->globalPos());
1293     if (!menu.isNull()) {
1294         delete menu.data();
1295     }
1296 }
1297 
AddSpellCheckContextMenu(QMenu * menu)1298 bool CodeViewEditor::AddSpellCheckContextMenu(QMenu *menu)
1299 {
1300     // The first action in the menu.
1301     QAction *topAction = 0;
1302 
1303     if (!menu->actions().isEmpty()) {
1304         topAction = menu->actions().at(0);
1305     }
1306 
1307     // We check if offering spelling suggestions is necessary.
1308     //
1309     // If no text is selected we check the position of the cursor and see if it
1310     // is within a misspelled word position range. If so we select it and
1311     // offer spelling suggestions.
1312     //
1313     // If text is already selected we check if it matches a misspelled word
1314     // position range. If so we need to offer spelling suggestions.
1315     bool offer_spelling = false;
1316 
1317     // Ignore spell check if spelling is disabled.
1318     //
1319     // Check for misspelled words by looking at the formatting set by the SyntaxHighlighter.
1320     // By checking the formatting we can get a precalculated list of which words are
1321     // misspelled. This keeps us from having to run the spell check twice. This ensures
1322     // the same words shown on screen (by the SyntaxHighlighter) are the only words
1323     // that act as spell check. Also, this reduces code duplication because we don't
1324     // have the same word detection code in two places (here and the SyntaxHighlighter).
1325     // Plus, we don't have to worry about the detection here detecting differently in
1326     // the situation where the SyntaxHighlighter detection code is changed but the
1327     // code here is not or vice versa.
1328     if (m_checkSpelling) {
1329         // See if we are close to or inside of a misspelled word. If so select it.
1330         const QString &selected_word = GetCurrentWordAtCaret(true);
1331         offer_spelling = !selected_word.isNull() && !selected_word.isEmpty();
1332 
1333         // If a misspelled word is selected try to offer spelling suggestions.
1334         if (offer_spelling) {
1335             SpellCheck *sc = SpellCheck::instance();
1336             QStringList suggestions = sc->suggestPS(selected_word);
1337             QAction *suggestAction = 0;
1338 
1339             // We want to limit the number of suggestions so we don't
1340             // get a huge context menu.
1341             for (int i = 0; i < std::min(suggestions.length(), MAX_SPELLING_SUGGESTIONS); ++i) {
1342                 suggestAction = new QAction(suggestions.at(i), menu);
1343                 connect(suggestAction, SIGNAL(triggered()), m_spellingMapper, SLOT(map()));
1344                 m_spellingMapper->setMapping(suggestAction, suggestions.at(i));
1345 
1346                 // If the menu is empty we need to append rather than insert our actions.
1347                 if (!topAction) {
1348                     menu->addAction(suggestAction);
1349                 } else {
1350                     menu->insertAction(topAction, suggestAction);
1351                 }
1352             }
1353 
1354             // Add a separator to keep our spelling actions differentiated from
1355             // the default menu actions.
1356             if (!suggestions.isEmpty() && topAction) {
1357                 menu->insertSeparator(topAction);
1358             }
1359 
1360             // Allow the user to add the misspelled word to their default user dictionary.
1361             QAction *addToDictAction = new QAction(tr("Add To Default Dictionary"), menu);
1362             connect(addToDictAction, SIGNAL(triggered()), m_addSpellingMapper, SLOT(map()));
1363             m_addSpellingMapper->setMapping(addToDictAction, selected_word);
1364 
1365             if (topAction) {
1366                 menu->insertAction(topAction, addToDictAction);
1367             } else {
1368                 menu->addAction(addToDictAction);
1369             }
1370 
1371             // Allow the user to select a dictionary
1372             QStringList dictionaries = sc->userDictionaries();
1373             QMenu *dictionary_menu = new QMenu(menu);
1374             dictionary_menu->setTitle(tr("Add To Dictionary"));
1375 
1376             if (topAction) {
1377                 menu->insertMenu(topAction, dictionary_menu);
1378             } else {
1379                 menu->addMenu(dictionary_menu);
1380             }
1381 
1382             foreach (QString dict_name, dictionaries) {
1383                 QAction *dictAction = new QAction(dict_name, dictionary_menu);
1384                 connect(dictAction, SIGNAL(triggered()), m_addDictMapper, SLOT(map()));
1385                 QString key = selected_word + "\t" + dict_name;
1386                 m_addDictMapper->setMapping(dictAction, key);
1387                 dictionary_menu->addAction(dictAction);
1388             }
1389 
1390             // Allow the user to ignore misspelled words until the program exits
1391             QAction *ignoreWordAction = new QAction(tr("Ignore"), menu);
1392             connect(ignoreWordAction, SIGNAL(triggered()), m_ignoreSpellingMapper, SLOT(map()));
1393             m_ignoreSpellingMapper->setMapping(ignoreWordAction, selected_word);
1394 
1395             if (topAction) {
1396                 menu->insertAction(topAction, ignoreWordAction);
1397                 menu->insertSeparator(topAction);
1398             } else {
1399                 menu->addAction(ignoreWordAction);
1400             }
1401         }
1402     }
1403 
1404     return offer_spelling;
1405 }
1406 
GetCurrentWordAtCaret(bool select_word)1407 QString CodeViewEditor::GetCurrentWordAtCaret(bool select_word)
1408 {
1409     QTextCursor c = textCursor();
1410 
1411     // See if we are close to or inside of a misspelled word. If so select it.
1412     if (!c.hasSelection()) {
1413         // We cannot use QTextCursor::charFormat because the format is not set directly in
1414         // the document. The QSyntaxHighlighter sets the format in the block layout's
1415         // additionalFormats property. Thus we have to check if the cursor is within
1416         // an additionalFormat for the block and if that format is for a misspelled word.
1417         int pos = c.positionInBlock();
1418         foreach(QTextLayout::FormatRange r, textCursor().block().layout()->formats()) {
1419             if (pos > r.start && pos < r.start + r.length && r.format.underlineStyle() == QTextCharFormat::WaveUnderline/*QTextCharFormat::SpellCheckUnderline*/) {
1420                 if (select_word) {
1421                     c.setPosition(c.block().position() + r.start);
1422                     c.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, r.length);
1423                     setTextCursor(c);
1424                     return c.selectedText();
1425                 } else {
1426                     return toPlainText().mid(c.block().position() + r.start, r.length);
1427                 }
1428             }
1429         }
1430     }
1431     // Check if our selection is a misspelled word.
1432     else {
1433         int selStart = c.selectionStart() - c.block().position();
1434         int selLen = c.selectionEnd() - c.block().position() - selStart;
1435         foreach(QTextLayout::FormatRange r, textCursor().block().layout()->formats()) {
1436             if (r.start == selStart && selLen == r.length && r.format.underlineStyle() == QTextCharFormat::WaveUnderline/*QTextCharFormat::SpellCheckUnderline*/) {
1437                 return c.selectedText();
1438             }
1439         }
1440     }
1441 
1442     return QString();
1443 }
1444 
AddReformatCSSContextMenu(QMenu * menu)1445 void CodeViewEditor::AddReformatCSSContextMenu(QMenu *menu)
1446 {
1447     QAction *topAction = 0;
1448 
1449     if (!menu->actions().isEmpty()) {
1450         topAction = menu->actions().at(0);
1451     }
1452 
1453     QMenu *reformatCSSMenu = new QMenu(tr("Reformat CSS"), menu);
1454 
1455     QAction *multiLineCSSAction = new QAction(tr("Multiple Lines Per Style"), reformatCSSMenu);
1456     QAction *singleLineCSSAction = new QAction(tr("Single Line Per Style"), reformatCSSMenu);
1457     connect(multiLineCSSAction, SIGNAL(triggered()), this, SLOT(ReformatCSSMultiLineAction()));
1458     connect(singleLineCSSAction, SIGNAL(triggered()), this, SLOT(ReformatCSSSingleLineAction()));
1459     reformatCSSMenu->addAction(multiLineCSSAction);
1460     reformatCSSMenu->addAction(singleLineCSSAction);
1461 
1462     if (!topAction) {
1463         menu->addMenu(reformatCSSMenu);
1464     } else {
1465         menu->insertMenu(topAction, reformatCSSMenu);
1466     }
1467 
1468     if (topAction) {
1469         menu->insertSeparator(topAction);
1470     }
1471 }
1472 
AddReformatHTMLContextMenu(QMenu * menu)1473 void CodeViewEditor::AddReformatHTMLContextMenu(QMenu *menu)
1474 {
1475     QAction *topAction = 0;
1476 
1477     if (!menu->actions().isEmpty()) {
1478         topAction = menu->actions().at(0);
1479     }
1480 
1481     QMenu *reformatMenu = new QMenu(tr("Reformat HTML"), menu);
1482 
1483     QAction *cleanAction = new QAction(tr("Mend and Prettify Code"), reformatMenu);
1484     QAction *cleanAllAction = new QAction(tr("Mend and Prettify Code - All HTML Files"), reformatMenu);
1485     QAction *toValidAction = new QAction(tr("Mend Code"), reformatMenu);
1486     QAction *toValidAllAction = new QAction(tr("Mend Code - All HTML Files"), reformatMenu);
1487     connect(cleanAction, SIGNAL(triggered()), this, SLOT(ReformatHTMLCleanAction()));
1488     connect(cleanAllAction, SIGNAL(triggered()), this, SLOT(ReformatHTMLCleanAllAction()));
1489     connect(toValidAction, SIGNAL(triggered()), this, SLOT(ReformatHTMLToValidAction()));
1490     connect(toValidAllAction, SIGNAL(triggered()), this, SLOT(ReformatHTMLToValidAllAction()));
1491     reformatMenu->addAction(cleanAction);
1492     reformatMenu->addAction(cleanAllAction);
1493     reformatMenu->addSeparator();
1494     reformatMenu->addAction(toValidAction);
1495     reformatMenu->addAction(toValidAllAction);
1496 
1497     if (!topAction) {
1498         menu->addMenu(reformatMenu);
1499     } else {
1500         menu->insertMenu(topAction, reformatMenu);
1501     }
1502 
1503     if (topAction) {
1504         menu->insertSeparator(topAction);
1505     }
1506 }
1507 
AddGoToLinkOrStyleContextMenu(QMenu * menu)1508 void CodeViewEditor::AddGoToLinkOrStyleContextMenu(QMenu *menu)
1509 {
1510     QAction *topAction = 0;
1511 
1512     if (!menu->actions().isEmpty()) {
1513         topAction = menu->actions().at(0);
1514     }
1515 
1516     QAction *goToLinkOrStyleAction = new QAction(tr("Go To Link Or Style"), menu);
1517     if (!topAction) {
1518         menu->addAction(goToLinkOrStyleAction);
1519     } else {
1520         menu->insertAction(topAction, goToLinkOrStyleAction);
1521     }
1522 
1523     connect(goToLinkOrStyleAction, SIGNAL(triggered()), this, SLOT(GoToLinkOrStyleAction()));
1524 
1525     if (topAction) {
1526         menu->insertSeparator(topAction);
1527     }
1528 }
1529 
AddViewImageContextMenu(QMenu * menu)1530 void CodeViewEditor::AddViewImageContextMenu(QMenu *menu)
1531 {
1532     QAction *topAction = 0;
1533 
1534     if (!menu->actions().isEmpty()) {
1535         topAction = menu->actions().at(0);
1536     }
1537 
1538     QAction *viewImageAction = new QAction(tr("View Image"), menu);
1539     QAction *openImageAction = new QAction(tr("Open Tab For Image"), menu);
1540 
1541     if (!topAction) {
1542         menu->addAction(viewImageAction);
1543         menu->addAction(openImageAction);
1544     } else {
1545         menu->insertAction(topAction, viewImageAction);
1546         menu->insertAction(topAction, openImageAction);
1547     }
1548 
1549     connect(viewImageAction, SIGNAL(triggered()), this, SLOT(GoToLinkOrStyle()));
1550     connect(openImageAction, SIGNAL(triggered()), this, SLOT(OpenImageAction()));
1551 
1552     if (topAction) {
1553         menu->insertSeparator(topAction);
1554     }
1555 }
1556 
AddMarkSelectionMenu(QMenu * menu)1557 void CodeViewEditor::AddMarkSelectionMenu(QMenu *menu)
1558 {
1559     QAction *topAction = 0;
1560 
1561     if (!menu->actions().isEmpty()) {
1562         topAction = menu->actions().at(0);
1563     }
1564 
1565     QString text = tr("Mark Selected Text");
1566     if (!textCursor().hasSelection() && IsMarkedText()) {
1567         text = tr("Unmark Marked Text");
1568     }
1569     QAction *markSelectionAction = new QAction(text, menu);
1570 
1571     if (!topAction) {
1572         menu->addAction(markSelectionAction);
1573     } else {
1574         menu->insertAction(topAction, markSelectionAction);
1575     }
1576 
1577     connect(markSelectionAction, SIGNAL(triggered()), this, SIGNAL(MarkSelectionRequest()));
1578 
1579     if (topAction) {
1580         menu->insertSeparator(topAction);
1581     }
1582 }
1583 
AddClipContextMenu(QMenu * menu)1584 void CodeViewEditor::AddClipContextMenu(QMenu *menu)
1585 {
1586     QAction *topAction = 0;
1587 
1588     if (!menu->actions().isEmpty()) {
1589         topAction = menu->actions().at(0);
1590     }
1591 
1592     QMenu *clips_menu = new QMenu(menu);
1593     clips_menu->setTitle(tr("Clips"));
1594 
1595     if (topAction) {
1596         menu->insertMenu(topAction, clips_menu);
1597     } else {
1598         menu->addMenu(clips_menu);
1599     }
1600 
1601     CreateMenuEntries(clips_menu, 0, ClipEditorModel::instance()->invisibleRootItem());
1602 
1603     QAction *saveClipAction = new QAction(tr("Add To Clips") + "...", menu);
1604 
1605     if (!topAction) {
1606         menu->addAction(saveClipAction);
1607     } else {
1608         menu->insertAction(topAction, saveClipAction);
1609     }
1610 
1611     connect(saveClipAction, SIGNAL(triggered()), this , SLOT(SaveClipAction()));
1612     saveClipAction->setEnabled(textCursor().hasSelection());
1613 
1614     if (topAction) {
1615         menu->insertSeparator(topAction);
1616     }
1617 }
1618 
CreateMenuEntries(QMenu * parent_menu,QAction * topAction,QStandardItem * item)1619 bool CodeViewEditor::CreateMenuEntries(QMenu *parent_menu, QAction *topAction, QStandardItem *item)
1620 {
1621     QAction *clipAction = 0;
1622     QMenu *group_menu = parent_menu;
1623 
1624     if (!item) {
1625         return false;
1626     }
1627 
1628     if (!item->text().isEmpty()) {
1629         // If item has no children, add entry to the menu, else create menu
1630         if (!item->data().toBool()) {
1631             clipAction = new QAction(item->text(), parent_menu);
1632             connect(clipAction, SIGNAL(triggered()), m_clipMapper, SLOT(map()));
1633             m_clipMapper->setMapping(clipAction, ClipEditorModel::instance()->GetFullName(item));
1634 
1635             if (!topAction) {
1636                 parent_menu->addAction(clipAction);
1637             } else {
1638                 parent_menu->insertAction(topAction, clipAction);
1639             }
1640         } else {
1641             group_menu = new QMenu(parent_menu);
1642             group_menu->setTitle(item->text());
1643 
1644             if (topAction) {
1645                 parent_menu->insertMenu(topAction, group_menu);
1646             } else {
1647                 parent_menu->addMenu(group_menu);
1648             }
1649 
1650             topAction = 0;
1651         }
1652     }
1653 
1654     // Recursively add entries for children
1655     for (int row = 0; row < item->rowCount(); row++) {
1656         CreateMenuEntries(group_menu, topAction, item->child(row, 0));
1657     }
1658 
1659     return item->rowCount() > 0;
1660 }
1661 
SaveClipAction()1662 void CodeViewEditor::SaveClipAction()
1663 {
1664     ClipEditorModel::clipEntry *pendingClipEntryRequest = new ClipEditorModel::clipEntry();
1665     pendingClipEntryRequest->name = "Unnamed Entry";
1666     pendingClipEntryRequest->is_group = false;
1667     QTextCursor cursor = textCursor();
1668     pendingClipEntryRequest->text = cursor.selectedText();
1669     emit OpenClipEditorRequest(pendingClipEntryRequest);
1670 }
1671 
InViewableImage()1672 bool CodeViewEditor::InViewableImage()
1673 {
1674     QString url_name = GetAttribute("src", SRC_TAGS, true);
1675 
1676     if (url_name.isEmpty()) {
1677         // We do not know what namespace may have been used
1678         url_name = GetAttribute("xlink:href", IMAGE_TAGS, true);
1679     }
1680 
1681     return !url_name.isEmpty();
1682 }
1683 
OpenImageAction()1684 void CodeViewEditor::OpenImageAction()
1685 {
1686     QString url_name = GetAttribute("src", SRC_TAGS, true);
1687 
1688     if (url_name.isEmpty()) {
1689         // We do not know what namespace may have been used
1690         url_name = GetAttribute("xlink:href", IMAGE_TAGS, true);
1691     }
1692 
1693     emit LinkClicked(QUrl(url_name));
1694 }
1695 
GoToLinkOrStyleAction()1696 void CodeViewEditor::GoToLinkOrStyleAction()
1697 {
1698     GoToLinkOrStyle();
1699 }
1700 
GoToLinkOrStyle()1701 void CodeViewEditor::GoToLinkOrStyle()
1702 {
1703     QString url_name = GetAttribute("href", ANCHOR_TAGS, true);
1704 
1705     if (url_name.isEmpty()) {
1706         QStringList LINK_TAGS = QStringList() << "link";
1707         url_name = GetAttribute("href", LINK_TAGS, true, false, false);
1708     }
1709 
1710     if (url_name.isEmpty()) {
1711         url_name = GetAttribute("src", SRC_TAGS, true);
1712     }
1713 
1714     if (url_name.isEmpty()) {
1715         // We do not know what namespace may have been used
1716         url_name = GetAttribute("xlink:href", IMAGE_TAGS, true);
1717     }
1718 
1719     if (!url_name.isEmpty()) {
1720 
1721         QUrl url = QUrl(url_name);
1722         QString extension = url_name.right(url_name.length() - url_name.lastIndexOf('.') - 1).toLower();
1723 
1724         if (IMAGE_EXTENSIONS.contains(extension)) {
1725             emit ViewImage(QUrl(url_name));
1726         } else {
1727             emit LinkClicked(QUrl(url_name));
1728         }
1729     } else if (IsPositionInOpeningTag(textCursor().selectionStart())) {
1730         GoToStyleDefinition();
1731     } else {
1732         emit ShowStatusMessageRequest(tr("You must be in an opening HTML tag to use this feature."));
1733     }
1734 }
1735 
GoToStyleDefinition()1736 void CodeViewEditor::GoToStyleDefinition()
1737 {
1738     // Begin by identifying the tag name and selected class style attribute if any
1739     CodeViewEditor::StyleTagElement element = GetSelectedStyleTagElement();
1740 
1741     if (element.name.isEmpty()) {
1742         emit ShowStatusMessageRequest(tr("You must be inside an opening HTML tag to use this feature."));
1743         return;
1744     }
1745 
1746     HTMLStyleInfo htmlcss_info(toPlainText());
1747     CSSInfo::CSSSelector *selector = htmlcss_info.getCSSSelectorForElementClass(element.name, element.classStyle);
1748 
1749     if (!selector) {
1750         // We didn't find the style - escalate as an event to look in linked stylesheets
1751         emit GoToLinkedStyleDefinitionRequest(element.name, element.classStyle);
1752     } else {
1753         // Emit a signal to bookmark our code location, enabling the "Back to" feature
1754         emit BookmarkLinkOrStyleLocationRequest();
1755         // Scroll to the line after bookmarking or we lose our place
1756         ScrollToPosition(selector->pos);
1757     }
1758 }
1759 
AddMisspelledWord()1760 void CodeViewEditor::AddMisspelledWord()
1761 {
1762     if (m_checkSpelling) {
1763         const QString &selected_word = GetCurrentWordAtCaret(false);
1764 
1765         if (!selected_word.isNull() && !selected_word.isEmpty()) {
1766             addToDefaultDictionary(selected_word);
1767             emit SpellingHighlightRefreshRequest();
1768         }
1769     }
1770 }
1771 
IgnoreMisspelledWord()1772 void CodeViewEditor::IgnoreMisspelledWord()
1773 {
1774     if (m_checkSpelling) {
1775         const QString &selected_word = GetCurrentWordAtCaret(false);
1776 
1777         if (!selected_word.isNull() && !selected_word.isEmpty()) {
1778             ignoreWord(selected_word);
1779             emit SpellingHighlightRefreshRequest();
1780         }
1781     }
1782 }
1783 
ReformatCSSMultiLineAction()1784 void CodeViewEditor::ReformatCSSMultiLineAction()
1785 {
1786     ReformatCSS(true);
1787 }
1788 
ReformatCSSSingleLineAction()1789 void CodeViewEditor::ReformatCSSSingleLineAction()
1790 {
1791     ReformatCSS(false);
1792 }
1793 
ReformatHTMLCleanAction()1794 void CodeViewEditor::ReformatHTMLCleanAction()
1795 {
1796     ReformatHTML(false, false);
1797 }
1798 
ReformatHTMLCleanAllAction()1799 void CodeViewEditor::ReformatHTMLCleanAllAction()
1800 {
1801     ReformatHTML(true, false);
1802 }
1803 
ReformatHTMLToValidAction()1804 void CodeViewEditor::ReformatHTMLToValidAction()
1805 {
1806     ReformatHTML(false, true);
1807 }
1808 
ReformatHTMLToValidAllAction()1809 void CodeViewEditor::ReformatHTMLToValidAllAction()
1810 {
1811     ReformatHTML(true, true);
1812 }
1813 
AddToIndex()1814 void CodeViewEditor::AddToIndex()
1815 {
1816     if (!TextIsSelectedAndNotInStartOrEndTag()) {
1817         return;
1818     }
1819 
1820     IndexEditorModel::indexEntry *index = new IndexEditorModel::indexEntry();
1821     QTextCursor cursor = textCursor();
1822     index->pattern = cursor.selectedText();
1823     emit OpenIndexEditorRequest(index);
1824 }
1825 
IsAddToIndexAllowed()1826 bool CodeViewEditor::IsAddToIndexAllowed()
1827 {
1828     return TextIsSelectedAndNotInStartOrEndTag();
1829 }
1830 
IsInsertIdAllowed()1831 bool CodeViewEditor::IsInsertIdAllowed()
1832 {
1833     int pos = textCursor().selectionStart();
1834 
1835     if (!IsPositionInBody(pos)) {
1836         return false;
1837     }
1838 
1839     // Only allow if the closing tag we're in is an "a" tag
1840     QString closing_tag_name = GetClosingTagName(pos);
1841 
1842     if (!closing_tag_name.isEmpty() && !ANCHOR_TAGS.contains(closing_tag_name)) {
1843         return false;
1844     }
1845 
1846     // Only allow if the opening tag we're in is valid for id attribute
1847     QString tag_name = GetOpeningTagName(pos);
1848 
1849     if (!tag_name.isEmpty() && !ID_TAGS.contains(tag_name)) {
1850         return false;
1851     }
1852 
1853     return true;
1854 }
1855 
IsInsertHyperlinkAllowed()1856 bool CodeViewEditor::IsInsertHyperlinkAllowed()
1857 {
1858     int pos = textCursor().selectionStart();
1859 
1860     if (!IsPositionInBody(pos)) {
1861         return false;
1862     }
1863 
1864     // Only allow if the closing tag we're in is an "a" tag
1865     QString closing_tag_name = GetClosingTagName(pos);
1866 
1867     if (!closing_tag_name.isEmpty() && !ANCHOR_TAGS.contains(closing_tag_name)) {
1868         return false;
1869     }
1870 
1871     // Only allow if the opening tag we're in is an "a" tag
1872     QString tag_name = GetOpeningTagName(pos);
1873 
1874     if (!tag_name.isEmpty() && !ANCHOR_TAGS.contains(tag_name)) {
1875         return false;
1876     }
1877 
1878     return true;
1879 }
1880 
IsInsertFileAllowed()1881 bool CodeViewEditor::IsInsertFileAllowed()
1882 {
1883     int pos = textCursor().selectionStart();
1884     if (!IsPositionInBody(pos)) return false;
1885     if (IsPositionInTag(pos)) {
1886         // special case of cursor |<tag>
1887         QString text = m_TagList.getSource();
1888         if (text[pos] != '<') return false;
1889     }
1890     return true;
1891 }
1892 
InsertId(const QString & attribute_value)1893 bool CodeViewEditor::InsertId(const QString &attribute_value)
1894 {
1895     int pos = textCursor().selectionStart();
1896     const QString &element_name = "a";
1897     const QString &attribute_name = "id";
1898     // If we're in an "a" tag we can update the id even if not in the opening tag
1899     QStringList tag_list = ID_TAGS;
1900 
1901     if (GetOpeningTagName(pos).isEmpty()) {
1902         tag_list = ANCHOR_TAGS;
1903     }
1904 
1905     return InsertTagAttribute(element_name, attribute_name, attribute_value, tag_list);
1906 }
1907 
InsertHyperlink(const QString & attribute_value)1908 bool CodeViewEditor::InsertHyperlink(const QString &attribute_value)
1909 {
1910     const QString &element_name = "a";
1911     const QString &attribute_name = "href";
1912 
1913     // HTML safe.
1914     QString safe_attribute_value = attribute_value;
1915     safe_attribute_value.replace("&amp;", "&");
1916     safe_attribute_value.replace("&", "&amp;");
1917     safe_attribute_value.replace("<", "&lt;");
1918     safe_attribute_value.replace(">", "&gt;");
1919 
1920     return InsertTagAttribute(element_name, attribute_name, safe_attribute_value, ANCHOR_TAGS);
1921 }
1922 
InsertTagAttribute(const QString & element_name,const QString & attribute_name,const QString & attribute_value,const QStringList & tag_list,bool ignore_selection)1923 bool CodeViewEditor::InsertTagAttribute(const QString &element_name,
1924                                         const QString &attribute_name,
1925                                         const QString &attribute_value,
1926                                         const QStringList &tag_list,
1927                                         bool ignore_selection)
1928 {
1929     bool inserted = false;
1930 
1931     // Add or update the attribute within the start tag and return if ok
1932     if (!SetAttribute(attribute_name, tag_list, attribute_value, false, true).isEmpty()) {
1933         return true;
1934     }
1935 
1936     // If nothing was inserted, then just insert a new tag with no text as long as we aren't in a tag
1937     if (!textCursor().hasSelection() && !IsPositionInTag(textCursor().position())) {
1938         InsertHTMLTagAroundText(element_name, "/" % element_name, attribute_name % "=\"" % attribute_value % "\"", "");
1939         inserted = true;
1940     } else if (TextIsSelectedAndNotInStartOrEndTag()) {
1941         // Just prepend and append the tag pairs to the text
1942         QString attributes = attribute_name % "=\"" % attribute_value % "\"";
1943         InsertHTMLTagAroundSelection(element_name, "/" % element_name, attributes);
1944         inserted = true;
1945     }
1946 
1947     return inserted;
1948 }
1949 
MarkForIndex(const QString & title)1950 bool CodeViewEditor::MarkForIndex(const QString &title)
1951 {
1952     QString safe_title = title;
1953     // HTML safe.
1954     safe_title.replace("&amp;", "&");
1955     safe_title.replace("&", "&amp;");
1956     safe_title.replace("<", "&lt;");
1957     safe_title.replace(">", "&gt;");
1958 
1959     bool ok = true;
1960     QString selected_text = textCursor().selectedText();
1961     const QString &element_name = "a";
1962     const QString &attribute_name = "class";
1963 
1964     // first see if an achor is the immediate parent of selected text
1965     // and if so get any existing attribute class values to append
1966     // so we do not overwrite them
1967     QString existing_class = GetAttribute("class",ANCHOR_TAGS, false);
1968     QString new_class = SIGIL_INDEX_CLASS;
1969     if (!existing_class.isEmpty()) new_class = new_class + " " + existing_class;
1970 
1971     if (!InsertTagAttribute(element_name, attribute_name, new_class, ANCHOR_TAGS)) {
1972         ok = false;
1973     }
1974 
1975     const QString &second_attribute_name = "title";
1976 
1977     if (!InsertTagAttribute(element_name, second_attribute_name, safe_title, ANCHOR_TAGS, true)) {
1978         ok = false;
1979     }
1980 
1981     return ok;
1982 }
1983 
1984 // Overridden so we can emit the FocusGained() signal.
focusInEvent(QFocusEvent * event)1985 void CodeViewEditor::focusInEvent(QFocusEvent *event)
1986 {
1987     // Why is this needed?
1988     // RehighlightDocument();
1989     emit FocusGained(this);
1990     QPlainTextEdit::focusInEvent(event);
1991     HighlightCurrentLine(false);
1992 }
1993 
1994 
1995 // Overridden so we can emit the FocusLost() signal.
focusOutEvent(QFocusEvent * event)1996 void CodeViewEditor::focusOutEvent(QFocusEvent *event)
1997 {
1998     emit FocusLost(this);
1999     QPlainTextEdit::focusOutEvent(event);
2000     HighlightCurrentLine(false);
2001 }
2002 
EmitFilteredCursorMoved()2003 void CodeViewEditor::EmitFilteredCursorMoved()
2004 {
2005     // Avoid slowdown while selecting text
2006     if (QApplication::mouseButtons() == Qt::NoButton) {
2007         if (m_isLoadFinished) {
2008             emit FilteredCursorMoved();
2009         }
2010     }
2011 }
2012 
TextChangedFilter()2013 void CodeViewEditor::TextChangedFilter()
2014 {
2015     m_regen_taglist = true;
2016 
2017     // Clear marked text to prevent marked area not matching entered text
2018     // if user types text, uses Undo, etc.
2019     if (!m_ReplacingInMarkedText && IsMarkedText()) {
2020         emit ClearMarkedTextRequest();
2021     }
2022 
2023     ResetLastFindMatch();
2024 
2025     if (m_isUndoAvailable) {
2026         emit FilteredTextChanged();
2027     }
2028 }
2029 
RehighlightDocument()2030 void CodeViewEditor::RehighlightDocument()
2031 {
2032     if (!isVisible()) {
2033         return;
2034     }
2035 
2036     // Is this needed,  Why not let it work asynchronously
2037     if (m_Highlighter) {
2038         // We block signals from the document while highlighting takes place,
2039         // because we do not want the contentsChanged() signal to be fired
2040         // which would mark the underlying resource as needing saving.
2041         XHTMLHighlighter2* xhl = qobject_cast<XHTMLHighlighter2*>(m_Highlighter);
2042         // XHTMLHighlighter* xhl = qobject_cast<XHTMLHighlighter*>(m_Highlighter);
2043         CSSHighlighter* chl = qobject_cast<CSSHighlighter*>(m_Highlighter);
2044         document()->blockSignals(true);
2045         if (xhl) {
2046             xhl->do_rehighlight();
2047         } else if (chl) {
2048             chl->do_rehighlight();
2049         }
2050         document()->blockSignals(false);
2051     }
2052 }
2053 
2054 
UpdateUndoAvailable(bool available)2055 void CodeViewEditor::UpdateUndoAvailable(bool available)
2056 {
2057     m_isUndoAvailable = available;
2058 }
2059 
2060 
UpdateLineNumberAreaMargin()2061 void CodeViewEditor::UpdateLineNumberAreaMargin()
2062 {
2063     // The left margin width depends on width of the line number area
2064     setViewportMargins(CalculateLineNumberAreaWidth(), 0, 0, 0);
2065 }
2066 
2067 
UpdateLineNumberArea(const QRect & area_to_update,int vertically_scrolled)2068 void CodeViewEditor::UpdateLineNumberArea(const QRect &area_to_update, int vertically_scrolled)
2069 {
2070     // If the editor scrolled, scroll the line numbers too
2071     if (vertically_scrolled != 0) {
2072         m_LineNumberArea->scroll(0, vertically_scrolled);
2073     } else { // otherwise update the required portion
2074         m_LineNumberArea->update(0, area_to_update.y(), m_LineNumberArea->width(), area_to_update.height());
2075     }
2076 
2077     if (area_to_update.contains(viewport()->rect())) {
2078         UpdateLineNumberAreaMargin();
2079     }
2080 }
2081 
2082 
MaybeRegenerateTagList()2083 void CodeViewEditor::MaybeRegenerateTagList()
2084 {
2085     // calling toPlainText before the initial load is finished causes
2086     // a segfault deep inside QTextDocument
2087     // if (!m_isLoadFinished) return;
2088 
2089     if (m_regen_taglist) {
2090         qDebug() << "regenerating tag list";
2091         m_TagList.reloadLister(toPlainText());
2092         m_regen_taglist = false;
2093     }
2094 }
2095 
2096 
HighlightCurrentLine(bool highlight_tags)2097 void CodeViewEditor::HighlightCurrentLine(bool highlight_tags)
2098 {
2099     QList<QTextEdit::ExtraSelection> extraSelections;
2100 
2101     SettingsStore settings;
2102 
2103     // Draw the full width line color.
2104     QTextEdit::ExtraSelection selection_line;
2105     if (hasFocus()) {
2106         selection_line.format.setBackground(m_codeViewAppearance.line_highlight_color);
2107     } else {
2108         selection_line.format.setBackground(m_codeViewAppearance.line_number_background_color);
2109     }
2110     selection_line.format.setProperty(QTextFormat::FullWidthSelection, true);
2111     selection_line.cursor = textCursor();
2112     selection_line.cursor.clearSelection();
2113     extraSelections.append(selection_line);
2114 
2115     if (highlight_tags && settings.highlightOpenCloseTags()) {
2116 
2117         // If and only if cursor is inside a tag, highlight open and matching close
2118         // current cursor position is just before this char at position pos in text
2119         QString text = toPlainText();
2120         int pos = textCursor().selectionStart();
2121 
2122         // find previous begin tag marker
2123         int pb = text.lastIndexOf('<', pos);
2124 
2125         // find next end tag marker
2126         int ne = text.indexOf('>', pos);
2127 
2128         // find next begin tag marker *after* this char
2129         // and handle case if missing
2130         int nb = text.indexOf('<', pos+1);
2131         if (nb == -1) nb = text.length()+1;
2132 
2133         // in tag if '<' is closer than '>' when search backwards
2134         // and if '>' is closer but than '<' (if it exists) but >= pos  when search forward
2135         if ((pb > text.lastIndexOf('>', pos-1)) && (ne >= pos) && (nb > ne)) {
2136 
2137             // if text has changed since last time regenerate the tag list
2138             MaybeRegenerateTagList();
2139 
2140             // in a tag
2141             int open_tag_pos = -1;
2142             int open_tag_len = -1;
2143             int close_tag_pos = -1;
2144             int close_tag_len = -1;
2145             int i = m_TagList.findFirstTagOnOrAfter(pos);
2146             TagLister::TagInfo ti = m_TagList.at(i);
2147             if ((pos >= ti.pos) && (pos < ti.pos + ti.len)) {
2148                 if(ti.ttype == "end") {
2149                     open_tag_pos = ti.open_pos;
2150                     open_tag_len = ti.open_len;
2151                     close_tag_pos = ti.pos;
2152                     close_tag_len = ti.len;
2153                 } else { // all others: single, begin, xmlheader, doctype, comment, cdata, etc
2154                     open_tag_pos = ti.pos;
2155                     open_tag_len = ti.len;
2156                 }
2157                 if (ti.ttype == "begin") {
2158                     int j = m_TagList.findCloseTagForOpen(i);
2159                     if (j >= 0) {
2160                         if (m_TagList.at(j).len != -1) {
2161                             close_tag_pos = m_TagList.at(j).pos;
2162                             close_tag_len = m_TagList.at(j).len;
2163                         }
2164                     }
2165                 }
2166             }
2167             if (open_tag_len != -1) {
2168                 QTextEdit::ExtraSelection selection_open;
2169                 selection_open.format.setBackground(m_codeViewAppearance.line_number_background_color);
2170                 selection_open.cursor = textCursor();
2171                 selection_open.cursor.clearSelection();
2172                 selection_open.cursor.setPosition(open_tag_pos);
2173                 selection_open.cursor.setPosition(open_tag_pos + open_tag_len, QTextCursor::KeepAnchor);
2174                 extraSelections.append(selection_open);
2175             }
2176             if (close_tag_len != -1) {
2177                 QTextEdit::ExtraSelection selection_close;
2178                 selection_close.format.setBackground(m_codeViewAppearance.line_number_background_color);
2179                 selection_close.cursor = textCursor();
2180                 selection_close.cursor.clearSelection();
2181                 selection_close.cursor.setPosition(close_tag_pos);
2182                 selection_close.cursor.setPosition(close_tag_pos + close_tag_len, QTextCursor::KeepAnchor);
2183                 extraSelections.append(selection_close);
2184             }
2185         }
2186     }
2187 
2188     // Add highlighting of the marked text
2189     if (IsMarkedText()) {
2190         if (m_MarkedTextEnd > textLength()) {
2191             m_MarkedTextEnd = textLength();
2192         }
2193 
2194         QTextEdit::ExtraSelection selection;
2195         selection.format.setBackground(m_codeViewAppearance.line_number_background_color);
2196         selection.cursor = textCursor();
2197         selection.cursor.clearSelection();
2198         selection.cursor.setPosition(m_MarkedTextStart);
2199         selection.cursor.setPosition(m_MarkedTextEnd, QTextCursor::KeepAnchor);
2200         extraSelections.append(selection);
2201     }
2202 
2203     setExtraSelections(extraSelections);
2204     emit selectionChanged();
2205 }
2206 
2207 
ScrollOneLineUp()2208 void CodeViewEditor::ScrollOneLineUp()
2209 {
2210     ScrollByLine(false);
2211 }
2212 
2213 
ScrollOneLineDown()2214 void CodeViewEditor::ScrollOneLineDown()
2215 {
2216     ScrollByLine(true);
2217 }
2218 
2219 
InsertText(const QString & text)2220 void CodeViewEditor::InsertText(const QString &text)
2221 {
2222     QTextCursor c = textCursor();
2223     c.insertText(text);
2224     setTextCursor(c);
2225 }
2226 
HighlightWord(const QString & word,int pos)2227 void CodeViewEditor::HighlightWord(const QString &word, int pos)
2228 {
2229     QTextCursor cursor = textCursor();
2230     cursor.clearSelection();
2231     cursor.setPosition(pos);
2232     cursor.setPosition(pos + word.length(), QTextCursor::KeepAnchor);
2233     setTextCursor(cursor);
2234 }
2235 
RefreshSpellingHighlighting()2236 void CodeViewEditor::RefreshSpellingHighlighting()
2237 {
2238     if (hasFocus()) {
2239         RehighlightDocument();
2240     }
2241 }
2242 
addToUserDictionary(const QString & text)2243 void CodeViewEditor::addToUserDictionary(const QString &text)
2244 {
2245     QString word = text.split("\t")[0];
2246     QString dict_name = text.split("\t")[1];
2247     SpellCheck *sc = SpellCheck::instance();
2248     sc->addToUserDictionary(word, dict_name);
2249     emit SpellingHighlightRefreshRequest();
2250 }
2251 
addToDefaultDictionary(const QString & text)2252 void CodeViewEditor::addToDefaultDictionary(const QString &text)
2253 {
2254     SpellCheck *sc = SpellCheck::instance();
2255     sc->addToUserDictionary(text);
2256     emit SpellingHighlightRefreshRequest();
2257 }
2258 
ignoreWord(const QString & text)2259 void CodeViewEditor::ignoreWord(const QString &text)
2260 {
2261     SpellCheck *sc = SpellCheck::instance();
2262     sc->ignoreWord(text);
2263     emit SpellingHighlightRefreshRequest();
2264 }
2265 
PasteText(const QString & text)2266 void CodeViewEditor::PasteText(const QString &text)
2267 {
2268     InsertText(text);
2269 }
2270 
PasteClipNumber(int clip_number)2271 bool CodeViewEditor::PasteClipNumber(int clip_number)
2272 {
2273     ClipEditorModel::clipEntry *clip = ClipEditorModel::instance()->GetEntryFromNumber(clip_number);
2274     if (!clip) {
2275         return false;
2276     }
2277 
2278     PasteClipEntry(clip);
2279 
2280     return true;
2281 }
2282 
PasteClipEntries(const QList<ClipEditorModel::clipEntry * > & clips)2283 bool CodeViewEditor::PasteClipEntries(const QList<ClipEditorModel::clipEntry *> &clips)
2284 {
2285     bool applied = false;
2286     foreach(ClipEditorModel::clipEntry * clip, clips) {
2287         bool res = PasteClipEntry(clip);
2288         applied = applied | res;
2289     }
2290     return applied;
2291 }
2292 
PasteClipEntryFromName(const QString & name)2293 void CodeViewEditor::PasteClipEntryFromName(const QString &name)
2294 {
2295     ClipEditorModel::clipEntry *clip = ClipEditorModel::instance()->GetEntryFromName(name);
2296     PasteClipEntry(clip);
2297 }
2298 
PasteClipEntry(ClipEditorModel::clipEntry * clip)2299 bool CodeViewEditor::PasteClipEntry(ClipEditorModel::clipEntry *clip)
2300 {
2301     if (!clip || clip->text.isEmpty()) {
2302         return false;
2303     }
2304 
2305     QTextCursor cursor = textCursor();
2306     // Convert to plain text or \s won't get newlines
2307     const QString &document_text = toPlainText();
2308     QString selected_text = Utility::Substring(cursor.selectionStart(), cursor.selectionEnd(), document_text);
2309 
2310     if (selected_text.isEmpty()) {
2311         // Allow users to use the same entry for insert/replace
2312         // Will not handle complicated regex, but good for tags like <p>\1</p>
2313         QString replacement_text = clip->text;
2314         replacement_text.remove(QString("\\1"));
2315         cursor.beginEditBlock();
2316         cursor.removeSelectedText();
2317         cursor.insertText(replacement_text);
2318         cursor.endEditBlock();
2319         setTextCursor(cursor);
2320     } else {
2321         QString search_regex = "(?s)(" + QRegularExpression::escape(selected_text) + ")";
2322         // We must do a Find before we do the Replace in order to ensure the match info that
2323         // ReplaceSelected relies upon is loaded.
2324         cursor.setPosition(cursor.selectionStart());
2325         setTextCursor(cursor);
2326         bool found = FindNext(search_regex, Searchable::Direction_Down, false, false, false);
2327 
2328         if (found) {
2329             ReplaceSelected(search_regex, clip->text, Searchable::Direction_Down, true);
2330         }
2331         cursor.setPosition(cursor.selectionEnd());
2332         setTextCursor(cursor);
2333     }
2334 
2335     return true;
2336 }
2337 
ResetFont()2338 void CodeViewEditor::ResetFont()
2339 {
2340     // Let's try to use our user specified value as our font (default Courier New)
2341     QFont font(m_codeViewAppearance.font_family, m_codeViewAppearance.font_size);
2342     // But just in case, say we want a fixed width font if font is not present
2343     font.setStyleHint(QFont::TypeWriter);
2344     setFont(font);
2345 #if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
2346     setTabStopDistance(TAB_SPACES_WIDTH * QFontMetrics(font).horizontalAdvance(' '));
2347 #else
2348     setTabStopWidth(TAB_SPACES_WIDTH * QFontMetrics(font).width(' '));
2349 #endif
2350     UpdateLineNumberAreaFont(font);
2351 }
2352 
2353 
UpdateLineNumberAreaFont(const QFont & font)2354 void CodeViewEditor::UpdateLineNumberAreaFont(const QFont &font)
2355 {
2356     m_LineNumberArea->setFont(font);
2357     m_LineNumberArea->MyUpdateGeometry();
2358     UpdateLineNumberAreaMargin();
2359 }
2360 
SetAppearanceColors()2361 void CodeViewEditor::SetAppearanceColors()
2362 {
2363 
2364     QPalette app_pal = qApp->palette();
2365     setPalette(app_pal);
2366     return;
2367 }
2368 
2369 
2370 // Center the screen on the cursor/caret location.
2371 // Centering requires fresh information about the
2372 // visible viewport, so we usually call this after
2373 // the paint event has been processed.
DelayedCursorScreenCentering()2374 void CodeViewEditor::DelayedCursorScreenCentering()
2375 {
2376     if (m_DelayedCursorScreenCenteringRequired) {
2377         centerCursor();
2378 #if !defined(Q_OS_WIN32) && !defined(Q_OS_MAC)
2379         // The Code View viewport stopped updating on Linux somewhere
2380         // around Qt5.12.0 in this delayed call to center the cursor.
2381         viewport()->update();
2382 #endif
2383         m_DelayedCursorScreenCenteringRequired = false;
2384     }
2385 }
2386 
2387 
SetDelayedCursorScreenCenteringRequired()2388 void CodeViewEditor::SetDelayedCursorScreenCenteringRequired()
2389 {
2390     m_DelayedCursorScreenCenteringRequired = true;
2391 }
2392 
GetSelectionOffset(Searchable::Direction search_direction,bool ignore_selection_offset,bool marked_text) const2393 int CodeViewEditor::GetSelectionOffset(Searchable::Direction search_direction, bool ignore_selection_offset, bool marked_text) const
2394 {
2395     int offset = 0;
2396     if (search_direction == Searchable::Direction_Up) {
2397         if (ignore_selection_offset) {
2398             if (marked_text) {
2399                 offset = m_MarkedTextEnd;
2400             } else {
2401                 offset = textLength();
2402             }
2403         } else {
2404             offset = textCursor().selectionStart();
2405         }
2406     } else {
2407         if (ignore_selection_offset) {
2408             if (marked_text) {
2409                 offset = m_MarkedTextStart;
2410             } else {
2411                 offset = 0;
2412             }
2413         } else {
2414             offset = textCursor().selectionEnd();
2415         }
2416     }
2417 
2418     return offset;
2419 }
2420 
2421 
ScrollByLine(bool down)2422 void CodeViewEditor::ScrollByLine(bool down)
2423 {
2424     int current_scroll_value = verticalScrollBar()->value();
2425     int move_delta = down ? 1 : -1;
2426     verticalScrollBar()->setValue(current_scroll_value + move_delta);
2427 
2428     if (!contentsRect().contains(cursorRect())) {
2429         if (move_delta > 0) {
2430             moveCursor(QTextCursor::Down);
2431         } else {
2432             moveCursor(QTextCursor::Up);
2433         }
2434     }
2435 }
2436 
GetCaretElementName()2437 QString CodeViewEditor::GetCaretElementName()
2438 {
2439     return m_element_name;
2440 }
2441 
GetCaretLocation()2442 QList<ElementIndex> CodeViewEditor::GetCaretLocation()
2443 {
2444     // We search for the first opening tag *behind* the caret.
2445     // This specifies the element the caret is located in.
2446     int pos = textCursor().position();
2447     int offset = 0;
2448     int len = 0;
2449     QRegularExpression tag(XML_OPENING_TAG);
2450     QRegularExpressionMatchIterator i = tag.globalMatch(toPlainText());
2451     // There is no way to search for the last match (str.lastIndexOf) in a string and also have
2452     // the matched text length. So we search forward for every match (QRegularExpression doesn't
2453     // have a way to search backwards) and only use the last match's info.
2454     while (i.hasNext()) {
2455         QRegularExpressionMatch mo = i.next();
2456         int start = mo.capturedStart();
2457         if (start > pos) {
2458             break;
2459         }
2460         offset = start;
2461         len = mo.capturedLength();
2462     }
2463     QList<ElementIndex> hierarchy = ConvertStackToHierarchy(GetCaretLocationStack(offset + len));
2464 
2465     // determine last block element containing caret
2466     QString element_name;
2467     foreach(ElementIndex ei, hierarchy) {
2468         if (BLOCK_LEVEL_TAGS.contains(ei.name)) {
2469         element_name = ei.name;
2470         }
2471     }
2472     m_element_name = element_name;
2473 
2474     return hierarchy;
2475 }
2476 
2477 
StoreCaretLocationUpdate(const QList<ElementIndex> & hierarchy)2478 void CodeViewEditor::StoreCaretLocationUpdate(const QList<ElementIndex> &hierarchy)
2479 {
2480     m_CaretUpdate = hierarchy;
2481 }
2482 
2483 
GetCaretLocationStack(int offset) const2484 QStack<CodeViewEditor::StackElement> CodeViewEditor::GetCaretLocationStack(int offset) const
2485 {
2486     QString source = toPlainText();
2487     QXmlStreamReader reader(source);
2488     QStack<StackElement> stack;
2489 
2490     while (!reader.atEnd()) {
2491         reader.readNext();
2492 
2493         if (reader.isStartElement()) {
2494             // If we detected the start of a new element, then
2495             // the element currently on the top of the stack
2496             // has one more child element
2497             if (!stack.isEmpty()) {
2498                 stack.top().num_children++;
2499             }
2500 
2501             StackElement new_element;
2502             new_element.name         = reader.name().toString();
2503             new_element.num_children = 0;
2504             stack.push(new_element);
2505 
2506             // Check if this is the element start tag
2507             // we are looking for
2508             if (reader.characterOffset() == offset) {
2509                 break;
2510             }
2511         }
2512         // If we detect the end tag of an element,
2513         // we remove it from the top of the stack
2514         else if (reader.isEndElement()) {
2515             stack.pop();
2516         }
2517     }
2518 
2519     if (reader.hasError()) {
2520         // Just return an empty location.
2521         // Maybe we could return the stack we currently have?
2522         return QStack<StackElement>();
2523     }
2524 
2525     return stack;
2526 }
2527 
2528 
ConvertHierarchyToQWebPath(const QList<ElementIndex> & hierarchy) const2529 QString CodeViewEditor::ConvertHierarchyToQWebPath(const QList<ElementIndex>& hierarchy) const
2530 {
2531     QStringList pathparts;
2532     for (int i=0; i < hierarchy.count(); i++) {
2533         QString part = hierarchy.at(i).name + " " + QString::number(hierarchy.at(i).index);
2534         pathparts.append(part);
2535     }
2536     return pathparts.join(",");
2537 }
2538 
2539 
ConvertStackToHierarchy(const QStack<StackElement> stack) const2540 QList<ElementIndex> CodeViewEditor::ConvertStackToHierarchy(const QStack<StackElement> stack) const
2541 {
2542     QList<ElementIndex> hierarchy;
2543     foreach(StackElement stack_element, stack) {
2544         ElementIndex new_element;
2545         new_element.name  = stack_element.name;
2546         new_element.index = stack_element.num_children - 1;
2547         hierarchy.append(new_element);
2548     }
2549     return hierarchy;
2550 }
2551 
2552 
ConvertHierarchyToCaretMove(const QList<ElementIndex> & hierarchy) const2553 std::tuple<int, int> CodeViewEditor::ConvertHierarchyToCaretMove(const QList<ElementIndex> &hierarchy) const
2554 {
2555     QString source = toPlainText();
2556     QString version = "any_version";
2557     GumboInterface gi = GumboInterface(source, version);
2558     gi.parse();
2559     QString webpath = ConvertHierarchyToQWebPath(hierarchy);
2560     GumboNode* end_node = gi.get_node_from_qwebpath(webpath);
2561     if (!end_node) {
2562       return std::make_tuple(0, 0);
2563     }
2564     unsigned int line = 0;
2565     unsigned int col = 0;
2566     if ((end_node->type == GUMBO_NODE_TEXT) || (end_node->type == GUMBO_NODE_WHITESPACE) ||
2567         (end_node->type == GUMBO_NODE_CDATA) || (end_node->type == GUMBO_NODE_COMMENT)) {
2568         line = end_node->v.text.start_pos.line + 1; // compensate for xml header removed for gumbo
2569         col = end_node->v.text.start_pos.column;
2570     } else if ((end_node->type == GUMBO_NODE_ELEMENT) || (end_node->type == GUMBO_NODE_TEMPLATE)) {
2571         line = end_node->v.element.start_pos.line + 1; // comprensate for xml header removed for gumbo
2572         col = end_node->v.element.start_pos.column;
2573     }
2574     QTextCursor cursor(document());
2575     return std::make_tuple(line - cursor.blockNumber(), col);
2576 }
2577 
2578 
ExecuteCaretUpdate(bool default_to_top)2579 bool CodeViewEditor::ExecuteCaretUpdate(bool default_to_top)
2580 {
2581     // If there's a cursor/caret update waiting (from BookView),
2582     // we update the caret location and reset the update variable
2583     if (m_CaretUpdate.isEmpty()) {
2584         if (default_to_top) {
2585             QTextCursor cursor = textCursor();
2586             cursor.movePosition(QTextCursor::Start);
2587             setTextCursor(cursor);
2588         }
2589 
2590         return false;
2591     }
2592 
2593     QTextCursor cursor(document());
2594     int vertical_lines_move = 0;
2595     int horizontal_chars_move = 0;
2596     // We *have* to do the conversion on-demand since the
2597     // conversion uses toPlainText(), and the text needs to up-to-date.
2598     std::tie(vertical_lines_move, horizontal_chars_move) = ConvertHierarchyToCaretMove(m_CaretUpdate);
2599     cursor.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor, vertical_lines_move - 1);
2600 
2601     for (int i = 1 ; i < horizontal_chars_move ; i++) {
2602         cursor.movePosition(QTextCursor::NextCharacter , QTextCursor::MoveAnchor);
2603         // TODO: cursor.movePosition( QTextCursor::Left, ...) is badly bugged in Qt 4.7.
2604         // Test whether it's fixed when the next version of Qt comes out.
2605         // cursor.movePosition( QTextCursor::Left, QTextCursor::MoveAnchor, horizontal_chars_move );
2606     }
2607 
2608     m_CaretUpdate.clear();
2609     setTextCursor(cursor);
2610     m_DelayedCursorScreenCenteringRequired = true;
2611     return true;
2612 }
2613 
2614 
FormatBlock(const QString & element_name,bool preserve_attributes)2615 void CodeViewEditor::FormatBlock(const QString &element_name, bool preserve_attributes)
2616 {
2617     if (element_name.isEmpty()) {
2618         return;
2619     }
2620 
2621     // create a default selection when no user selection is provided
2622     if (!textCursor().hasSelection()) {
2623         QTextCursor newcursor(textCursor());
2624         newcursor.select(QTextCursor::LineUnderCursor);
2625         QString newtxt = newcursor.selectedText();
2626         int startpos = newcursor.selectionStart();
2627         QString cleantxt(newtxt);
2628         cleantxt = cleantxt.trimmed();
2629         if (cleantxt.startsWith("<")) {
2630              int p = cleantxt.indexOf(">");
2631              if (p > -1) cleantxt = cleantxt.mid(p+1, -1);
2632         }
2633         if (cleantxt.endsWith(">")) {
2634             int p = cleantxt.lastIndexOf("<");
2635             if (p > -1) cleantxt = cleantxt.mid(0, p);
2636         }
2637         int pos =  newtxt.indexOf(cleantxt, 0);
2638         if (pos > 0) startpos = startpos + pos;
2639         newcursor.clearSelection();
2640         newcursor.setPosition(startpos);
2641         newcursor.setPosition(startpos + cleantxt.length(), QTextCursor::KeepAnchor);
2642         setTextCursor(newcursor);
2643     }
2644 
2645     // Emit a selection changed event, so we can make sure the style buttons are updated
2646     // to uncheck any heading buttons check states.
2647     emit selectionChanged();
2648 
2649     // Going to assume that the user is allowed to click anywhere within or just after the block
2650     // Also makes assumptions about being well formed, or else crazy things may happen...
2651     int pos = textCursor().selectionStart();
2652     MaybeRegenerateTagList();
2653     QString text = m_TagList.getSource();
2654 
2655 
2656     if (!IsPositionInBody(pos)) return;
2657 
2658     // find that tag that starts immediately **after** pos and then
2659     // then use its predecessor when working backwards
2660     int i = m_TagList.findLastTagOnOrBefore(pos);
2661     TagLister::TagInfo ti = m_TagList.at(i);
2662     while(i >= 0) {
2663         ti = m_TagList.at(i);
2664 
2665         if (BLOCK_LEVEL_TAGS.contains(ti.tname)) {
2666 
2667             // we do not want a closing block tag if that is where the cursor is now, look earlier
2668             if ((ti.ttype == "end") && ((pos >= ti.pos) && (pos < ti.pos + ti.len))) {
2669                 i--;
2670                 continue;
2671             }
2672 
2673             // special case for body tag or closing tag that we did not start in
2674             // just insert the element around the current selection
2675             if ((ti.tname == "body") || (ti.ttype == "end") || (ti.ttype == "single")) {
2676                 InsertHTMLTagAroundSelection(element_name, "/" % element_name);
2677                 return;
2678             }
2679 
2680             // if we reached here we have an opening block tag we need to replace
2681             QStringRef opening_tag_text(&text, ti.pos, ti.len);
2682             QString all_attributes = TagLister::extractAllAttributes(opening_tag_text);
2683 
2684             // look for matching closing tag from here to the end
2685             int j = i+1;
2686             TagLister::TagInfo et = m_TagList.at(j);
2687             while((et.len != -1) && (et.open_pos != ti.pos)) {
2688                 j++;
2689                 et = m_TagList.at(j);
2690             }
2691             if (et.len == -1) return; // no matching closing tag found
2692 
2693             // ready to now format this block
2694             QString new_opening_tag_text;
2695             if (preserve_attributes && (all_attributes.length() > 0)) {
2696                 new_opening_tag_text = "<" + element_name + " " + all_attributes + ">";
2697             } else {
2698                 new_opening_tag_text = "<" + element_name + ">";
2699             }
2700 
2701             QString new_closing_tag_text = "</" + element_name + ">";
2702             ReplaceTags(ti.pos, ti.pos + ti.len, new_opening_tag_text,
2703                         et.pos, et.pos + et.len, new_closing_tag_text);
2704             return;
2705         }
2706         i--;
2707     }
2708     return;
2709 }
2710 
2711 
InsertHTMLTagAroundText(const QString & left_element_name,const QString & right_element_name,const QString & attributes,const QString & text)2712 void CodeViewEditor::InsertHTMLTagAroundText(const QString &left_element_name,
2713                                              const QString &right_element_name,
2714                                              const QString &attributes,
2715                                              const QString &text)
2716 {
2717     QTextCursor cursor = textCursor();
2718     QString new_attributes = attributes;
2719 
2720     if (!new_attributes.isEmpty()) {
2721         new_attributes.prepend(" ");
2722     }
2723 
2724     const QString &prefix = "<" % left_element_name % new_attributes % ">";
2725     const QString &new_text = prefix % text % "<" % right_element_name % ">";
2726     int selection_start = cursor.selectionStart() + prefix.length();
2727     cursor.beginEditBlock();
2728     cursor.insertText(new_text);
2729     cursor.endEditBlock();
2730     cursor.setPosition(selection_start + text.length());
2731     cursor.setPosition(selection_start, QTextCursor::KeepAnchor);
2732     setTextCursor(cursor);
2733 }
2734 
InsertHTMLTagAroundSelection(const QString & left_element_name,const QString & right_element_name,const QString & attributes)2735 void CodeViewEditor::InsertHTMLTagAroundSelection(const QString &left_element_name,
2736                                                   const QString &right_element_name,
2737                                                   const QString &attributes)
2738 {
2739     QTextCursor cursor = textCursor();
2740     QString new_attributes = attributes;
2741 
2742     if (!new_attributes.isEmpty()) {
2743         new_attributes.prepend(" ");
2744     }
2745 
2746     const QString &selected_text = cursor.selectedText();
2747     const QString &prefix_text = "<" % left_element_name % new_attributes % ">";
2748     const QString &replacement_text = prefix_text % selected_text % "<" % right_element_name % ">";
2749     int selection_start = cursor.selectionStart();
2750     cursor.beginEditBlock();
2751     cursor.insertText(replacement_text);
2752     cursor.endEditBlock();
2753     // Move caret to between the tags to allow the user to start typing/keep selection.
2754     cursor.setPosition(selection_start + prefix_text.length() + selected_text.length());
2755     cursor.setPosition(selection_start + prefix_text.length(), QTextCursor::KeepAnchor);
2756     setTextCursor(cursor);
2757 }
2758 
IsPositionInBody(int pos)2759 bool CodeViewEditor::IsPositionInBody(int pos)
2760 {
2761     MaybeRegenerateTagList();
2762     return m_TagList.isPositionInBody(pos);
2763 }
2764 
2765 // This routine is time critical as it is called a lot
IsPositionInTag(int pos)2766 bool CodeViewEditor::IsPositionInTag(int pos)
2767 {
2768     QString text = toPlainText();
2769     // find previous begin tag marker
2770     int pb = text.lastIndexOf('<', pos);
2771     // find next end tag marker
2772     int ne = text.indexOf('>', pos);
2773 
2774     // find next begin tag marker *after* this char
2775     // and handle case if missing
2776     int nb = text.indexOf('<', pos+1);
2777     if (nb == -1) nb = text.length()+1;
2778 
2779     // in tag if '<' is closer than '>' when search backwards
2780     // and if '>' is closer but than '<' (if it exists) but >= pos  when search forward
2781     if ((pb > text.lastIndexOf('>', pos-1)) && (ne >= pos) && (nb > ne)) {
2782         MaybeRegenerateTagList();
2783         return m_TagList.isPositionInTag(pos);
2784     }
2785     return false;
2786 }
2787 
2788 // OpeningTag is can be a begin tag or a single tag
IsPositionInOpeningTag(int pos)2789 bool CodeViewEditor::IsPositionInOpeningTag(int pos)
2790 {
2791     MaybeRegenerateTagList();
2792     return m_TagList.isPositionInOpenTag(pos);
2793 }
2794 
IsPositionInClosingTag(int pos)2795 bool CodeViewEditor::IsPositionInClosingTag(int pos)
2796 {
2797     MaybeRegenerateTagList();
2798     return m_TagList.isPositionInCloseTag(pos);
2799 }
2800 
GetOpeningTagName(int pos)2801 QString CodeViewEditor::GetOpeningTagName(int pos)
2802 {
2803     QString tag_name;
2804     MaybeRegenerateTagList();
2805     int i = m_TagList.findFirstTagOnOrAfter(pos);
2806     TagLister::TagInfo ti = m_TagList.at(i);
2807     if ((pos >= ti.pos) && (pos < ti.pos + ti.len)) {
2808         if ((ti.ttype == "begin") || (ti.ttype == "single")) tag_name = ti.tname.toLower();
2809     }
2810     return tag_name;
2811 }
2812 
GetClosingTagName(int pos)2813 QString CodeViewEditor::GetClosingTagName(int pos)
2814 {
2815     QString tag_name;
2816     MaybeRegenerateTagList();
2817     int i = m_TagList.findFirstTagOnOrAfter(pos);
2818     TagLister::TagInfo ti = m_TagList.at(i);
2819     if ((pos >= ti.pos) && (pos < ti.pos + ti.len)) {
2820         if (ti.ttype == "end") tag_name = ti.tname.toLower();
2821     }
2822     return tag_name;
2823 }
2824 
2825 
ToggleFormatSelection(const QString & element_name,const QString property_name,const QString property_value)2826 void CodeViewEditor::ToggleFormatSelection(const QString &element_name, const QString property_name, const QString property_value)
2827 {
2828     if (element_name.isEmpty()) {
2829         return;
2830     }
2831 
2832     // Going to assume that the user is allowed to click anywhere within or just after the block
2833     // Also makes assumptions about being well formed, or else crazy things may happen...
2834     if (!textCursor().hasSelection()) {
2835         QTextCursor newcursor(textCursor());
2836         newcursor.select(QTextCursor::WordUnderCursor);
2837         setTextCursor(newcursor);
2838     }
2839 
2840     // Emit a selection changed event, so we can make sure the style buttons are updated
2841     // to uncheck any style buttons check states.
2842     emit selectionChanged();
2843 
2844     int pos = textCursor().selectionStart();
2845 
2846     MaybeRegenerateTagList();
2847 
2848     if (!IsPositionInBody(pos)) {
2849         // We are in an HTML file outside the body element. We might possibly be in an
2850         // inline CSS style so attempt to format that.
2851         if (!property_name.isEmpty()) {
2852             FormatCSSStyle(property_name, property_value);
2853         }
2854         return;
2855     }
2856 
2857     if (IsPositionInTag(textCursor().selectionStart()) ||
2858         IsPositionInTag(textCursor().selectionEnd()-1)) {
2859         // Not allowed to toggle style if caret placed on a tag
2860         return;
2861     }
2862 
2863 
2864     QString text = m_TagList.getSource();
2865     bool in_existing_tag_occurrence = false;
2866 
2867     // Look backwards from the current selection to find whether we are in an open occurrence
2868     // of this tag already within this block.
2869     int i = m_TagList.findLastTagOnOrBefore(pos);
2870 
2871     TagLister::TagInfo ti;
2872     while (i >= 0) {
2873         ti = m_TagList.at(i);
2874         if (ti.len == -1) return;
2875 
2876         if (element_name == ti.tname) {
2877             if (ti.ttype != "end") in_existing_tag_occurrence = true;
2878             break;
2879         } else if (BLOCK_LEVEL_TAGS.contains(ti.tname)) {
2880             // No point in searching any further - we reached the block parent
2881             // without finding an open occurrence of this tag.
2882             break;
2883         }
2884         // Not a tag of interest - keep searching.
2885         i--;
2886     }
2887     if (i < 0) return;
2888 
2889     if (in_existing_tag_occurrence) {
2890         FormatSelectionWithinElement(element_name, i, text);
2891     } else {
2892         // Otherwise assume we are in a safe place to add a wrapper tag.
2893         InsertHTMLTagAroundSelection(element_name, "/" % element_name);
2894     }
2895 }
2896 
2897 // This routine is used solely from within ToggleFormatSelection
FormatSelectionWithinElement(const QString & element_name,int tagno,const QString & text)2898 void CodeViewEditor::FormatSelectionWithinElement(const QString &element_name, int tagno, const QString &text)
2899 {
2900     // We are inside an existing occurrence. Are we immediately adjacent to it?
2901     // e.g. "<b>selected text</b>" should result in "selected text" losing the tags.
2902     // but  "<b>XXXselected textYYY</b> should result in "<b>XXX</b>selected text<b>YYY</b>"
2903     // plus the variations where XXX or YYY may be non-existent to make tag adjacent.
2904     int j = m_TagList.findCloseTagForOpen(tagno);
2905     if (j < 0) return; // no closing tag for this style then not well formed so abort
2906 
2907     TagLister::TagInfo tb = m_TagList.at(tagno);
2908     TagLister::TagInfo te = m_TagList.at(j);
2909 
2910     QTextCursor cursor = textCursor();
2911     int selection_start = cursor.selectionStart();
2912     int selection_end = cursor.selectionEnd();
2913     bool adjacent_to_start = tb.pos + tb.len == selection_start;
2914     bool adjacent_to_end = te.pos == selection_end;
2915 
2916     if (!adjacent_to_start && !adjacent_to_end) {
2917         // We want to put a closing tag at the start and an opening tag after (copying attributes)
2918         // Do NOT copy the '<' or the '>' of the opening tag!
2919         QString opening_tag_text = text.mid(tb.pos+1, tb.len-2).trimmed();
2920         InsertHTMLTagAroundSelection("/" % element_name, opening_tag_text);
2921     } else if ((selection_end == selection_start) && (adjacent_to_start || adjacent_to_end) &&
2922                (te.pos > tb.pos + tb.len)) {
2923         // User is just inside the opening or closing tag with no selection and there is text within
2924         // The tags. Nothing to do in this situation.
2925         // If there is no text e.g. <b></b> and caret between then we will toggle off as per following else.
2926         return;
2927     } else {
2928         QString opening_tag = text.mid(tb.pos, tb.len);
2929         QString closing_tag = text.mid(te.pos, te.len);
2930         QString replacement_text = cursor.selectedText();
2931         cursor.beginEditBlock();
2932         int new_selection_end = selection_end;
2933         int new_selection_start = selection_start;
2934 
2935         if (adjacent_to_start) {
2936             selection_start -= opening_tag.length();
2937             new_selection_start -= opening_tag.length();
2938             new_selection_end -= opening_tag.length();
2939         } else {
2940             replacement_text = closing_tag + replacement_text;
2941             new_selection_start += closing_tag.length();
2942             new_selection_end += closing_tag.length();
2943         }
2944 
2945         if (adjacent_to_end) {
2946             selection_end += closing_tag.length();
2947         } else {
2948             replacement_text.append(opening_tag);
2949         }
2950 
2951         cursor.setPosition(selection_end);
2952         cursor.setPosition(selection_start, QTextCursor::KeepAnchor);
2953         cursor.removeSelectedText();
2954         cursor.insertText(replacement_text);
2955         cursor.setPosition(new_selection_end);
2956         cursor.setPosition(new_selection_start, QTextCursor::KeepAnchor);
2957         cursor.endEditBlock();
2958         setTextCursor(cursor);
2959     }
2960 }
2961 
ReplaceTags(const int & opening_tag_start,const int & opening_tag_end,const QString & opening_tag_text,const int & closing_tag_start,const int & closing_tag_end,const QString & closing_tag_text)2962 void CodeViewEditor::ReplaceTags(const int &opening_tag_start, const int &opening_tag_end, const QString &opening_tag_text,
2963                                  const int &closing_tag_start, const int &closing_tag_end, const QString &closing_tag_text)
2964 {
2965     QTextCursor cursor = textCursor();
2966     cursor.beginEditBlock();
2967     // Replace the end block tag first since that does not affect positions
2968     cursor.setPosition(closing_tag_end);
2969     cursor.setPosition(closing_tag_start, QTextCursor::KeepAnchor);
2970     cursor.removeSelectedText();
2971     cursor.insertText(closing_tag_text);
2972     // Now replace the opening block tag
2973     cursor.setPosition(opening_tag_end);
2974     cursor.setPosition(opening_tag_start, QTextCursor::KeepAnchor);
2975     cursor.removeSelectedText();
2976     cursor.insertText(opening_tag_text);
2977     cursor.endEditBlock();
2978     setTextCursor(cursor);
2979 }
2980 
2981 
2982 
GetSelectedStyleTagElement()2983 CodeViewEditor::StyleTagElement CodeViewEditor::GetSelectedStyleTagElement()
2984 {
2985     // Look at the current cursor position, and return a struct representing the
2986     // name of the element, and the style class name if any under the caret.
2987     // If caret not on a style name, returns the first style class name.
2988     // If no style class specified, only the element tag name will be populated.
2989     CodeViewEditor::StyleTagElement element;
2990     MaybeRegenerateTagList();
2991 
2992     int pos = textCursor().selectionStart();
2993 
2994     if (!IsPositionInOpeningTag(pos)) return element;
2995 
2996     QString text = m_TagList.getSource();
2997 
2998     int i = m_TagList.findLastTagOnOrBefore(pos);
2999     TagLister::TagInfo ti = m_TagList.at(i);
3000     QStringRef tagstring(&text, ti.pos, ti.len);
3001     element.name = ti.tname;
3002 
3003     TagLister::AttInfo attr;
3004     TagLister::parseAttribute(tagstring, "class", attr);
3005     if (attr.pos < 0) {
3006         return element;
3007     }
3008 
3009     // if caret not in a class attribute search only for element
3010     int cstart = ti.pos + attr.pos;
3011     if (pos < cstart || pos >= cstart + attr.len) return element;
3012 
3013     QString avalue = attr.avalue.trimmed();
3014     QStringList vals = avalue.split(' ');
3015     if (vals.length() == 1) {
3016         //just one class value provided use it
3017         element.classStyle = avalue;
3018         return element;
3019     }
3020 
3021     // multiple values present, see if the original cursor as in one of them
3022     // if so use that one, if not return the first
3023     int vstart = ti.pos + attr.vpos;
3024     if (pos < vstart || (pos  >= vstart + attr.vlen )) {
3025         element.classStyle = vals[0];
3026         return element;
3027     }
3028 
3029     // cursor is someplace inside the class value area and multiple values present
3030     int offset = pos - vstart;
3031     avalue = attr.avalue; // untrimmed so it matches vpos and vlen
3032     int k = 0;
3033     int p = avalue.indexOf(' ', 0);
3034     while ((p > 0) && p < offset) {
3035         k++;
3036         p = avalue.indexOf(' ', p+1);
3037     }
3038     if (i >= 0 && k < vals.size()) {
3039         element.classStyle = vals[k];
3040     } else {
3041         element.classStyle = vals[0];
3042     }
3043     return element;
3044 }
3045 
3046 
GetAttributeId()3047 QString CodeViewEditor::GetAttributeId()
3048 {
3049     int pos = textCursor().selectionStart();
3050     QString tag_name = GetOpeningTagName(pos);
3051     // If we're in an opening tag use it for the id, else use a
3052     QStringList tag_list = ID_TAGS;
3053 
3054     if (tag_name.isEmpty()) {
3055         tag_list = ANCHOR_TAGS;
3056     }
3057 
3058     return GetAttribute("id", tag_list, false, true);
3059 }
3060 
GetAttribute(const QString & attribute_name,QStringList tag_list,bool must_be_in_attribute,bool skip_paired_tags,bool must_be_in_body)3061 QString CodeViewEditor::GetAttribute(const QString &attribute_name,
3062                                      QStringList tag_list,
3063                                      bool must_be_in_attribute,
3064                                      bool skip_paired_tags,
3065                                      bool must_be_in_body)
3066 {
3067     return ProcessAttribute(attribute_name, tag_list, QString(),
3068                             false, must_be_in_attribute, skip_paired_tags, must_be_in_body);
3069 }
3070 
3071 
SetAttribute(const QString & attribute_name,QStringList tag_list,const QString & attribute_value,bool must_be_in_attribute,bool skip_paired_tags)3072 QString CodeViewEditor::SetAttribute(const QString &attribute_name,
3073                                      QStringList tag_list,
3074                                      const QString &attribute_value,
3075                                      bool must_be_in_attribute,
3076                                      bool skip_paired_tags)
3077 {
3078     return ProcessAttribute(attribute_name, tag_list, attribute_value,
3079                             true, must_be_in_attribute, skip_paired_tags);
3080 }
3081 
3082 
ProcessAttribute(const QString & attribute_name,QStringList tag_list,const QString & attribute_value,bool set_attribute,bool must_be_in_attribute,bool skip_paired_tags,bool must_be_in_body)3083 QString CodeViewEditor::ProcessAttribute(const QString &attribute_name,
3084                                          QStringList tag_list,
3085                                          const QString &attribute_value,
3086                                          bool set_attribute,
3087                                          bool must_be_in_attribute,
3088                                          bool skip_paired_tags,
3089                                          bool must_be_in_body)
3090 {
3091 
3092     if (attribute_name.isEmpty()) {
3093         return QString();
3094     }
3095 
3096     if (tag_list.count() == 0) {
3097         tag_list = BLOCK_LEVEL_TAGS;
3098     }
3099 
3100     // Makes assumptions about being well formed, or else crazy things may happen...
3101     // Given the code <p>abc</p>, users can click between first < and > and
3102     // one character to the left of the first <.
3103     int pos = textCursor().selectionStart();
3104     int original_position = textCursor().position();
3105 
3106     MaybeRegenerateTagList();
3107     QString text = m_TagList.getSource();
3108 
3109     // The old implementation did not properly handle pi, multi-line comments, cdata
3110     // nor attribute values delimited by single quotes
3111 
3112     if (must_be_in_body && !IsPositionInBody(pos)) return QString();
3113 
3114     // If we're in a closing tag, move to the text between tags to make parsing easier.
3115     if (IsPositionInClosingTag(pos)) {
3116         while (pos > 0 && text[pos] != QChar('<')) {
3117             pos--;
3118         }
3119     }
3120     if (pos < 0) return QString();
3121 
3122     // now find the tag that starts immediately *after* position pos
3123     int i = m_TagList.findLastTagOnOrBefore(pos);
3124     TagLister::TagInfo ti = m_TagList.at(i);
3125     QList<int> paired_tags;
3126     while((i >= 0) && (m_TagList.at(i).tname != "body")) {
3127         ti = m_TagList.at(i);
3128         // qDebug() << " checking the tag: " << ti.tname << ti.ttype << ti.pos;
3129         if (ti.ttype == "end") {
3130             if (skip_paired_tags && !BLOCK_LEVEL_TAGS.contains(ti.tname)) {
3131                 paired_tags << ti.open_pos;
3132             }
3133             if (tag_list.contains(ti.tname) || BLOCK_LEVEL_TAGS.contains(ti.tname)) {
3134                 return QString();
3135             }
3136         } else if ((ti.ttype == "begin") || (ti.ttype == "single")) {
3137             if (skip_paired_tags && paired_tags.contains(ti.pos)) {
3138                 paired_tags.removeOne(ti.pos);
3139             } else {
3140                 // did we found what we want
3141                 if (tag_list.contains(ti.tname) || BLOCK_LEVEL_TAGS.contains(ti.tname)) break;
3142             }
3143         }
3144         // skip all special tags like doctype, cdata, pi, xmlheaders, and comments
3145         i--;
3146     }
3147     if ((i < 0) || !tag_list.contains(ti.tname) || (ti.tname == "body")) return QString();
3148     QStringRef opening_tag_text(&text, ti.pos, ti.len);
3149 
3150     // Now look for the attribute, which may or may not already exist
3151     TagLister::AttInfo ainfo;
3152     TagLister::parseAttribute(opening_tag_text, attribute_name, ainfo);
3153 
3154     // qDebug() << " in tag: " << opening_tag_text;
3155     // qDebug() << " found attribute: " << ainfo.aname << ainfo.avalue << ainfo.pos << ainfo.len;
3156     // set absolute attribute start and end locations in text
3157     int attribute_start = ti.pos + ti.len - 1;  // right before the tag '>'
3158     int attribute_end = attribute_start;
3159     if (ainfo.pos != -1) {
3160         // attribute exists
3161         attribute_start = ti.pos + ainfo.pos - 1; // include single leading space as part of attribute
3162         attribute_end = attribute_start + 1 + ainfo.len; // and compensate when setting the end position
3163         if (must_be_in_attribute && (original_position <= attribute_start  || original_position >= attribute_end)) {
3164             return "";
3165         }
3166     }
3167 
3168     if (!set_attribute) return ainfo.avalue;
3169 
3170     // setting an attribute value to a empty or null value deletes the attribute
3171     // if no attribute found and doing a delete then just return since nopthing to delete
3172     if ((ainfo.pos == -1) && attribute_value.isEmpty()) return QString();
3173 
3174     QString attribute_text;
3175     if (!attribute_value.isEmpty()) {
3176         attribute_text = " " + TagLister::serializeAttribute(attribute_name, attribute_value);
3177     }
3178 
3179     QTextCursor cursor = textCursor();
3180     cursor.beginEditBlock();
3181     cursor.setPosition(attribute_end);
3182     cursor.setPosition(attribute_start, QTextCursor::KeepAnchor);
3183     cursor.removeSelectedText();
3184     if (!attribute_text.isEmpty()) {
3185         cursor.insertText(attribute_text);
3186     }
3187 
3188     // Now place the cursor at the end of this opening tag, taking into account difference in attributes.
3189     cursor.setPosition(ti.pos + ti.len - (attribute_end - attribute_start) + attribute_text.length());
3190     cursor.endEditBlock();
3191     setTextCursor(cursor);
3192     return attribute_value;
3193 }
3194 
3195 
FormatTextDir(const QString & attribute_value)3196 void CodeViewEditor::FormatTextDir(const QString &attribute_value)
3197 {
3198     // Going to assume that the user is allowed to click anywhere within or just after the block
3199     // Also makes assumptions about being well formed, or else crazy things may happen...
3200     int pos = textCursor().selectionStart();
3201     if (!IsPositionInBody(pos)) {
3202         return;
3203     }
3204     // Apply the modified attribute.
3205     SetAttribute("dir", ID_TAGS, attribute_value);
3206 }
3207 
FormatStyle(const QString & property_name,const QString & property_value)3208 void CodeViewEditor::FormatStyle(const QString &property_name, const QString &property_value)
3209 {
3210     if (property_name.isEmpty() || property_value.isEmpty()) {
3211         return;
3212     }
3213 
3214     // Emit a selection changed event, so we can make sure the style buttons are updated
3215     // to uncheck any buttons check states.
3216     emit selectionChanged();
3217 
3218     // Going to assume that the user is allowed to click anywhere within or just after the block
3219     // Also makes assumptions about being well formed, or else crazy things may happen...
3220     int pos = textCursor().selectionStart();
3221 
3222     if (!IsPositionInBody(pos)) {
3223         // Either we are in a CSS file, or we are in an HTML file outside the body element.
3224         // Treat both these cases as trying to find a CSS style on the current line
3225         FormatCSSStyle(property_name, property_value);
3226         return;
3227     }
3228 
3229     // Get the existing style attribute if there is one.
3230     QString style_attribute_value = GetAttribute("style");
3231 
3232     if (style_attribute_value.isNull()) {
3233         // There is no style attribute currently on this tag so just set it.
3234         style_attribute_value = QString("%1: %2;").arg(property_name).arg(property_value);
3235     } else {
3236         // We have an existing style attribute on this tag, need to parse it to rewrite it.
3237         // Apply the name=value replacement getting a list of our new property pairs
3238         QList<HTMLStyleInfo::CSSProperty> css_properties = HTMLStyleInfo::getCSSProperties(style_attribute_value, 0, style_attribute_value.length());
3239         // Apply our property value, adding if not present currently, toggling if it is.
3240         ApplyChangeToProperties(css_properties, property_name, property_value);
3241 
3242         if (css_properties.count() == 0) {
3243             style_attribute_value = QString();
3244         } else {
3245             QStringList property_values;
3246             foreach(HTMLStyleInfo::CSSProperty css_property, css_properties) {
3247                 if (css_property.value.isNull()) {
3248                     property_values.append(css_property.name);
3249                 } else {
3250                     property_values.append(QString("%1: %2").arg(css_property.name).arg(css_property.value));
3251                 }
3252             }
3253             style_attribute_value = QString("%1;").arg(property_values.join("; "));
3254         }
3255     }
3256 
3257     // Apply the modified attribute.
3258     SetAttribute("style", BLOCK_LEVEL_TAGS, style_attribute_value);
3259 }
3260 
FormatCSSStyle(const QString & property_name,const QString & property_value)3261 void CodeViewEditor::FormatCSSStyle(const QString &property_name, const QString &property_value)
3262 {
3263     if (property_name.isEmpty() || property_value.isEmpty()) {
3264         return;
3265     }
3266 
3267     // Emit a selection changed event, so we can make sure the style buttons are updated
3268     // to uncheck any buttons check states.
3269     emit selectionChanged();
3270     // Going to assume that the user is allowed to click anywhere within or just after the block
3271     // Also makes assumptions about being well formed, or else crazy things may happen...
3272     int pos = textCursor().selectionStart();
3273     QString text = toPlainText();
3274     // Valid places to apply this modification are either if:
3275     // (a) the caret is on a line somewhere inside CSS {} parenthesis.
3276     // (b) the caret is on a line declaring a CSS style
3277     const QTextBlock block = textCursor().block();
3278     int bracket_end = text.indexOf(QChar('}'), pos);
3279 
3280     if (bracket_end < 0) {
3281         return;
3282     }
3283 
3284     int bracket_start = text.lastIndexOf(QChar('{'), pos);
3285 
3286     if (bracket_start < 0 || text.lastIndexOf(QChar('}'), pos - 1) > bracket_start) {
3287         // The previous opening parenthesis we found belongs to another CSS style set
3288         // Look for another one after the current position on the same line.
3289         bracket_start = block.text().indexOf('{');
3290 
3291         if (bracket_start >= 0) {
3292             bracket_start += block.position();
3293         } else {
3294             // Some CSS stylesheets put the {} on their own line following the style name.
3295             // Look for a non-empty current line followed by a line starting with parenthesis
3296             const QTextBlock next_block = block.next();
3297 
3298             if (block.text().trimmed().isEmpty() || !next_block.isValid() ||
3299                 !next_block.text().trimmed().startsWith(QChar('{'))) {
3300                 return;
3301             }
3302 
3303             bracket_start = next_block.text().indexOf(QChar('{')) + next_block.position();
3304         }
3305     }
3306 
3307     if (bracket_start > bracket_end) {
3308         // Sanity check for some really weird bracketing (perhaps invalid css)
3309         return;
3310     }
3311 
3312     // Now parse the HTML style content
3313     QList<HTMLStyleInfo::CSSProperty> css_properties = HTMLStyleInfo::getCSSProperties(text, bracket_start + 1, bracket_end);
3314     // Apply our property value, adding if not present currently, toggling if it is.
3315     ApplyChangeToProperties(css_properties, property_name, property_value);
3316     // Figure out the formatting to be applied to these style properties to write prettily
3317     // preserving any multi-line/single line style the CSS had before we changed things.
3318     bool is_single_line_format = (block.position() < bracket_start) && (bracket_end <= (block.position() + block.length()));
3319     const QString &style_attribute_text = HTMLStyleInfo::formatCSSProperties(css_properties, !is_single_line_format);
3320     // Now perform the replacement/insertion of the style properties into the CSS
3321     QTextCursor cursor = textCursor();
3322     cursor.beginEditBlock();
3323     // Replace the end block tag first since that does not affect positions
3324     cursor.setPosition(bracket_end);
3325     cursor.setPosition(bracket_start + 1, QTextCursor::KeepAnchor);
3326     cursor.removeSelectedText();
3327     cursor.insertText(style_attribute_text);
3328     cursor.setPosition(bracket_start);
3329     cursor.endEditBlock();
3330     setTextCursor(cursor);
3331 }
3332 
ApplyChangeToProperties(QList<HTMLStyleInfo::CSSProperty> & css_properties,const QString & property_name,const QString & property_value)3333 void CodeViewEditor::ApplyChangeToProperties(QList<HTMLStyleInfo::CSSProperty > &css_properties, const QString &property_name, const QString &property_value)
3334 {
3335     // Apply our property value, adding if not present currently, toggling if it is.
3336     bool has_property = false;
3337 
3338     for (int i = css_properties.length() - 1; i >= 0; i--) {
3339         HTMLStyleInfo::CSSProperty css_property = css_properties.at(i);
3340 
3341         if (css_property.name.toLower() == property_name) {
3342             has_property = true;
3343 
3344             // We will treat this as a toggle - if we already have the value then remove it
3345             if (css_property.value.toLower() == property_value) {
3346                 css_properties.removeAt(i);
3347                 continue;
3348             } else {
3349                 css_property.value = property_value;
3350             }
3351         }
3352     }
3353 
3354     if (!has_property) {
3355         HTMLStyleInfo::CSSProperty new_property;
3356         new_property.name = property_name;
3357         new_property.value = property_value;
3358         css_properties.append(new_property);
3359     }
3360 }
3361 
3362 // FIXME: Detect the type of document we are editing and handle both
3363 // internal xhtml style elements and external css stylesheets.
3364 // This routine is now only enabled when editing a CSS Stylesheet
ReformatCSS(bool multiple_line_format)3365 void CodeViewEditor::ReformatCSS(bool multiple_line_format)
3366 {
3367     const QString original_text = toPlainText();
3368     // Currently this feature is only enabled for CSS content, no inline HTML
3369     CSSInfo css_info(original_text);
3370     QString new_text = css_info.getReformattedCSSText(multiple_line_format);
3371 
3372     if (original_text != new_text) {
3373         QTextCursor cursor = textCursor();
3374         cursor.beginEditBlock();
3375         cursor.select(QTextCursor::Document);
3376         cursor.insertText(new_text);
3377         cursor.endEditBlock();
3378     }
3379 }
3380 
ReformatHTML(bool all,bool to_valid)3381 void CodeViewEditor::ReformatHTML(bool all, bool to_valid)
3382 {
3383     QString original_text;
3384     QString new_text;
3385     QWidget *mainWindow_w = Utility::GetMainWindow();
3386     MainWindow *mainWindow = qobject_cast<MainWindow *>(mainWindow_w);
3387     if (!mainWindow) {
3388         Utility::DisplayStdErrorDialog("Could not determine main window.");
3389         return;
3390     }
3391     QString version = mainWindow->GetCurrentBook()->GetConstOPF()->GetEpubVersion();
3392 
3393     if (all) {
3394         mainWindow->GetCurrentBook()->ReformatAllHTML(to_valid);
3395 
3396     } else {
3397         original_text = toPlainText();
3398 
3399         if (to_valid) {
3400             new_text = CleanSource::Mend(original_text, version);
3401         } else {
3402             new_text = CleanSource::MendPrettify(original_text, version);
3403         }
3404 
3405         if (original_text != new_text) {
3406             StoreCaretLocationUpdate(GetCaretLocation());
3407             QTextCursor cursor = textCursor();
3408             cursor.beginEditBlock();
3409             cursor.select(QTextCursor::Document);
3410             cursor.insertText(new_text);
3411             cursor.endEditBlock();
3412             ExecuteCaretUpdate();
3413         }
3414     }
3415 }
3416 
RemoveFirstTag(const QString & text,const QString & tagname)3417 QString CodeViewEditor::RemoveFirstTag(const QString &text, const QString &tagname)
3418 {
3419     QString result = text;
3420     int p = result.indexOf(">");
3421     if (p > -1) {
3422         QString tag = result.mid(0,p+1);
3423         if (tag.contains(tagname)) {
3424             result = result.mid(p+1,-1);
3425         }
3426     }
3427     return result;
3428 }
3429 
RemoveLastTag(const QString & text,const QString & tagname)3430 QString CodeViewEditor::RemoveLastTag(const QString &text, const QString &tagname)
3431 {
3432     QString result = text;
3433     int p = result.lastIndexOf("<");
3434     if (p > -1) {
3435         QString tag = result.mid(p,-1);
3436         if (tag.contains(tagname)) {
3437         result = result.mid(0,p);
3438     }
3439     }
3440     return result;
3441 }
3442 
IsSelectionValid(const QString & text)3443 bool CodeViewEditor::IsSelectionValid(const QString & text)
3444 {
3445     // whatever is selected must either be pure text or have balanced
3446     // opening and closing tags (ie. well-formed).
3447     // fastest way to check this is parse the fragment
3448     GumboInterface gi = GumboInterface(text, "any_version");
3449     QList<GumboWellFormedError> results = gi.fragment_error_check();
3450     if (!results.isEmpty()) {
3451       return false;
3452     }
3453     return true;
3454 }
3455 
WrapSelectionInElement(const QString & element,bool unwrap)3456 void CodeViewEditor::WrapSelectionInElement(const QString& element, bool unwrap)
3457 {
3458     QTextCursor cursor = textCursor();
3459     const QString selected_text = cursor.selectedText();
3460 
3461     if (selected_text.isEmpty()) {
3462         return;
3463     }
3464 
3465     QString new_text = selected_text;
3466 
3467     if (!IsSelectionValid(new_text)) return;
3468 
3469     QRegularExpression start_indent(STARTING_INDENT_USED);
3470     QRegularExpressionMatch indent_mo = start_indent.match(new_text);
3471     QString indent;
3472     if (indent_mo.hasMatch()) {
3473         indent = indent_mo.captured(1);
3474     indent = indent.replace("\t","    ");
3475     }
3476 
3477     QRegularExpression open_tag_at_start(OPEN_TAG_STARTS_SELECTION);
3478     QRegularExpressionMatch open_mo = open_tag_at_start.match(new_text);
3479     QString tagname;
3480     if (open_mo.hasMatch()) {
3481         tagname = open_mo.captured(2);
3482     }
3483 
3484     if (unwrap) {
3485         if (tagname == element) {
3486             new_text = RemoveFirstTag(new_text, element);
3487         new_text = RemoveLastTag(new_text, element);
3488         new_text = new_text.trimmed();
3489         new_text = indent + new_text;
3490     }
3491     }
3492     else {
3493         new_text = new_text.trimmed();
3494         QStringList result;
3495     result.append(indent + "<" + element + ">");
3496     result.append(indent + "    " + new_text);
3497     result.append(indent + "</" + element + ">\n");
3498     new_text = result.join('\n');
3499     }
3500 
3501     if (new_text == selected_text) {
3502         return;
3503     }
3504 
3505     const int pos = cursor.selectionStart();
3506     cursor.beginEditBlock();
3507     cursor.removeSelectedText();
3508     cursor.insertText(new_text);
3509     cursor.setPosition(pos + new_text.length());
3510     cursor.setPosition(pos, QTextCursor::KeepAnchor);
3511     cursor.endEditBlock();
3512     setTextCursor(cursor);
3513 }
3514 
3515 
ApplyListToSelection(const QString & element)3516 void CodeViewEditor::ApplyListToSelection(const QString &element)
3517 {
3518     QTextCursor cursor = textCursor();
3519     const QString selected_text = cursor.selectedText();
3520 
3521     if (selected_text.isEmpty()) {
3522         return;
3523     }
3524 
3525     QString new_text = selected_text;
3526 
3527     QRegularExpression start_indent(STARTING_INDENT_USED);
3528     QRegularExpressionMatch indent_mo = start_indent.match(new_text);
3529     QString indent;
3530     if (indent_mo.hasMatch()) {
3531         indent = indent_mo.captured(1);
3532         indent = indent.replace("\t","    ");
3533     }
3534 
3535     QRegularExpression open_tag_at_start(OPEN_TAG_STARTS_SELECTION);
3536     QRegularExpressionMatch open_mo = open_tag_at_start.match(new_text);
3537     QString tagname;
3538     if (open_mo.hasMatch()) {
3539         tagname = open_mo.captured(2);
3540     }
3541 
3542     if (((tagname == "ol") && (element == "ol")) ||
3543         ((tagname == "ul") && (element == "ul"))) {
3544         new_text = RemoveFirstTag(new_text, element);
3545         new_text = RemoveLastTag(new_text, element);
3546         new_text = new_text.trimmed();
3547         // now split remaining text by new lines and
3548         // remove any beginning and ending li tags
3549         QStringList alist = new_text.split(QChar::ParagraphSeparator, QString::SkipEmptyParts);
3550         QStringList result;
3551         foreach(QString aitem, alist) {
3552             result.append(indent + RemoveLastTag(RemoveFirstTag(aitem,"li"), "li"));
3553         }
3554         result.append("");
3555         new_text = result.join("\n");
3556     }
3557     else if ((tagname == "p") || tagname.isEmpty()) {
3558         QStringList alist = new_text.split(QChar::ParagraphSeparator, QString::SkipEmptyParts);
3559         QStringList result;
3560         result.append(indent + "<" + element + ">");
3561         foreach(QString aitem, alist) {
3562             result.append(indent + "    " + "<li>" + aitem.trimmed() + "</li>");
3563         }
3564         result.append(indent + "</" + element + ">\n");
3565         new_text = result.join('\n');
3566     }
3567 
3568     if (new_text == selected_text) {
3569         return;
3570     }
3571 
3572     const int pos = cursor.selectionStart();
3573     cursor.beginEditBlock();
3574     cursor.removeSelectedText();
3575     cursor.insertText(new_text);
3576     cursor.setPosition(pos + new_text.length());
3577     cursor.setPosition(pos, QTextCursor::KeepAnchor);
3578     cursor.endEditBlock();
3579     setTextCursor(cursor);
3580 }
3581 
ApplyCaseChangeToSelection(const Utility::Casing & casing)3582 void CodeViewEditor::ApplyCaseChangeToSelection(const Utility::Casing &casing)
3583 {
3584     QTextCursor cursor = textCursor();
3585     const QString selected_text = cursor.selectedText();
3586 
3587     if (selected_text.isEmpty()) {
3588         return;
3589     }
3590 
3591     // Do not allow user to try to capitalize where selection contains an html tag.
3592     if (selected_text.contains(QChar('<')) || selected_text.contains(QChar('>'))) {
3593         return;
3594     }
3595 
3596     const QString new_text = Utility::ChangeCase(selected_text, casing);
3597 
3598     if (new_text == selected_text) {
3599         return;
3600     }
3601 
3602     const int pos = cursor.selectionStart();
3603     cursor.beginEditBlock();
3604     cursor.removeSelectedText();
3605     cursor.insertText(new_text);
3606     cursor.setPosition(pos + new_text.length());
3607     cursor.setPosition(pos, QTextCursor::KeepAnchor);
3608     cursor.endEditBlock();
3609     setTextCursor(cursor);
3610 }
3611 
3612 
3613 
GetUnmatchedTagsForBlock(int pos)3614 QStringList CodeViewEditor::GetUnmatchedTagsForBlock(int pos)
3615 {
3616     // Given the specified position within the text, keep looking backwards finding
3617     // any tags until we hit all open block tags within the body. Append all the opening tags
3618     // that do not have closing tags together (ignoring self-closing tags)
3619     // and return the opening tags list complete with their attributes contiguously.
3620     // Note: this should *never* include the html, head, or the body opening tags
3621     QStringList opening_tags;
3622     QList<int> paired_tags;
3623     MaybeRegenerateTagList();
3624     QString text = m_TagList.getSource();
3625     int i = m_TagList.findFirstTagOnOrAfter(pos);
3626     // so start looking for unmatched tags starting at i - 1
3627     i--;
3628     if (i < 0) return opening_tags;
3629     while((i >= 0) && (m_TagList.at(i).tname != "body")) {
3630         TagLister::TagInfo ti = m_TagList.at(i);
3631         if (ti.ttype == "end") {
3632             paired_tags << ti.open_pos;
3633         } else if (ti.ttype == "begin") {
3634             if (paired_tags.contains(ti.pos)) {
3635                 paired_tags.removeOne(ti.pos);
3636             } else {
3637                 opening_tags.prepend(text.mid(ti.pos, ti.len));
3638             }
3639         }
3640         // ignore single, and all special tags like doctype, cdata, pi, xmlheaders, and comments
3641         i--;
3642     }
3643     return opening_tags;
3644 }
3645 
ReformatCSSEnabled()3646 bool CodeViewEditor::ReformatCSSEnabled()
3647 {
3648     return m_reformatCSSEnabled;
3649 }
3650 
SetReformatCSSEnabled(bool value)3651 void CodeViewEditor::SetReformatCSSEnabled(bool value)
3652 {
3653     m_reformatCSSEnabled = value;
3654 }
3655 
ReformatHTMLEnabled()3656 bool CodeViewEditor::ReformatHTMLEnabled()
3657 {
3658     return m_reformatHTMLEnabled;
3659 }
3660 
SetReformatHTMLEnabled(bool value)3661 void CodeViewEditor::SetReformatHTMLEnabled(bool value)
3662 {
3663     m_reformatHTMLEnabled = value;
3664 }
3665 
SelectAndScrollIntoView(int start_position,int end_position,Searchable::Direction direction,bool wrapped)3666 void CodeViewEditor::SelectAndScrollIntoView(int start_position, int end_position, Searchable::Direction direction, bool wrapped)
3667 {
3668     // We will scroll the position on screen if necessary in order to ensure that there is a block visible
3669     // before and after the text that will be selected by these positions.
3670     QTextBlock start_block = document()->findBlock(start_position);
3671     QTextBlock end_block = document()->findBlock(end_position);
3672     bool scroll_to_center = false;
3673     QTextCursor cursor = textCursor();
3674 
3675     if (wrapped) {
3676         // Set an initial cursor position at the top or bottom of the screen as appropriate
3677         if (direction == Searchable::Direction_Up) {
3678             cursor.movePosition(QTextCursor::End);
3679             setTextCursor(cursor);
3680         } else {
3681             cursor.movePosition(QTextCursor::Start);
3682             setTextCursor(cursor);
3683         }
3684     }
3685 
3686     if (direction == Searchable::Direction_Up || start_block.blockNumber() < end_block.blockNumber()) {
3687         QTextBlock first_visible_block = firstVisibleBlock();
3688         QTextBlock previous_block = start_block.previous();
3689 
3690         while (previous_block.blockNumber() > 0 && previous_block.text().isEmpty()) {
3691             previous_block = previous_block.previous();
3692         }
3693 
3694         if (!previous_block.isValid()) {
3695             previous_block = start_block;
3696         }
3697 
3698         if (previous_block.blockNumber() < first_visible_block.blockNumber()) {
3699             scroll_to_center = true;
3700         }
3701     }
3702 
3703     if (direction == Searchable::Direction_Down || start_block.blockNumber() < end_block.blockNumber()) {
3704         QTextBlock last_visible_block =  cursorForPosition(QPoint(viewport()->width(), viewport()->height())).block();
3705         QTextBlock next_block = end_block.next();
3706 
3707         while (next_block.blockNumber() > 0 && next_block.blockNumber() < blockCount() - 1 && next_block.text().isEmpty()) {
3708             next_block = next_block.next();
3709         }
3710 
3711         if (!next_block.isValid()) {
3712             next_block = end_block;
3713         }
3714 
3715         if (next_block.blockNumber() > last_visible_block.blockNumber()) {
3716             scroll_to_center = true;
3717         }
3718     }
3719 
3720     if (direction == Searchable::Direction_Up) {
3721         cursor.setPosition(end_position);
3722         cursor.setPosition(start_position, QTextCursor::KeepAnchor);
3723     } else {
3724         cursor.setPosition(start_position);
3725         cursor.setPosition(end_position, QTextCursor::KeepAnchor);
3726     }
3727 
3728     setTextCursor(cursor);
3729 
3730     if (scroll_to_center) {
3731         centerCursor();
3732     }
3733 
3734     // Tell FlowTab to Tell Preview to Sync to this Location
3735     emit PageClicked();
3736 }
3737 
ConnectSignalsToSlots()3738 void CodeViewEditor::ConnectSignalsToSlots()
3739 {
3740     connect(this, SIGNAL(blockCountChanged(int)), this, SLOT(UpdateLineNumberAreaMargin()));
3741     connect(this, SIGNAL(updateRequest(const QRect &, int)), this, SLOT(UpdateLineNumberArea(const QRect &, int)));
3742     connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(HighlightCurrentLine()));
3743     connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(EmitFilteredCursorMoved()));
3744     connect(this, SIGNAL(textChanged()), this, SIGNAL(PageUpdated()));
3745     connect(this, SIGNAL(textChanged()), this, SLOT(TextChangedFilter()));
3746     connect(this, SIGNAL(undoAvailable(bool)), this, SLOT(UpdateUndoAvailable(bool)));
3747     connect(this, SIGNAL(selectionChanged()), this, SLOT(ResetLastFindMatch()));
3748     connect(m_ScrollOneLineUp,   SIGNAL(activated()), this, SLOT(ScrollOneLineUp()));
3749     connect(m_ScrollOneLineDown, SIGNAL(activated()), this, SLOT(ScrollOneLineDown()));
3750     connect(m_spellingMapper, SIGNAL(mapped(const QString &)), this, SLOT(InsertText(const QString &)));
3751     connect(m_addSpellingMapper, SIGNAL(mapped(const QString &)), this, SLOT(addToDefaultDictionary(const QString &)));
3752     connect(m_addDictMapper, SIGNAL(mapped(const QString &)), this, SLOT(addToUserDictionary(const QString &)));
3753     connect(m_ignoreSpellingMapper, SIGNAL(mapped(const QString &)), this, SLOT(ignoreWord(const QString &)));
3754     connect(m_clipMapper, SIGNAL(mapped(const QString &)), this, SLOT(PasteClipEntryFromName(const QString &)));
3755 }
3756