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> </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("&", "&");
1916 safe_attribute_value.replace("&", "&");
1917 safe_attribute_value.replace("<", "<");
1918 safe_attribute_value.replace(">", ">");
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("&", "&");
1955 safe_title.replace("&", "&");
1956 safe_title.replace("<", "<");
1957 safe_title.replace(">", ">");
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