From 404b5329a1ac10228650686bae3f7742358e60c0 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Thu, 26 Oct 2017 19:36:12 +0800 Subject: [PATCH] replace VEdit and VMdEdit with VEditor and VMdEditor --- src/src.pro | 12 +- src/utils/veditutils.cpp | 87 ++++ src/utils/veditutils.h | 9 + src/utils/vvim.cpp | 156 +++--- src/utils/vvim.h | 6 +- src/vconstants.h | 8 - src/vedit.cpp | 38 -- src/vedit.h | 53 +- src/veditconfig.cpp | 44 ++ src/veditconfig.h | 48 ++ src/veditoperations.cpp | 15 +- src/veditoperations.h | 6 +- src/veditor.cpp | 917 ++++++++++++++++++++++++++++++++++ src/veditor.h | 372 ++++++++++++++ src/vfilelist.cpp | 4 +- src/vimagepreviewer.h | 2 +- src/vimageresourcemanager.cpp | 2 +- src/vlinenumberarea.h | 3 +- src/vmainwindow.cpp | 3 + src/vmainwindow.h | 4 + src/vmdedit.cpp | 3 + src/vmdeditoperations.cpp | 92 ++-- src/vmdeditoperations.h | 2 +- src/vmdeditor.cpp | 862 ++++++++++++++++++++++++++++++++ src/vmdeditor.h | 212 ++++++++ src/vmdtab.cpp | 39 +- src/vmdtab.h | 12 +- src/vplaintextedit.cpp | 39 +- src/vplaintextedit.h | 11 + src/vpreviewmanager.cpp | 316 ++++++++++++ src/vpreviewmanager.h | 135 +++++ 31 files changed, 3240 insertions(+), 272 deletions(-) create mode 100644 src/veditconfig.cpp create mode 100644 src/veditconfig.h create mode 100644 src/veditor.cpp create mode 100644 src/veditor.h create mode 100644 src/vmdeditor.cpp create mode 100644 src/vmdeditor.h create mode 100644 src/vpreviewmanager.cpp create mode 100644 src/vpreviewmanager.h diff --git a/src/src.pro b/src/src.pro index 72a2e355..c731c02f 100644 --- a/src/src.pro +++ b/src/src.pro @@ -84,7 +84,11 @@ SOURCES += main.cpp\ dialog/vinsertlinkdialog.cpp \ vplaintextedit.cpp \ vimageresourcemanager.cpp \ - vlinenumberarea.cpp + vlinenumberarea.cpp \ + veditor.cpp \ + vmdeditor.cpp \ + veditconfig.cpp \ + vpreviewmanager.cpp HEADERS += vmainwindow.h \ vdirectorytree.h \ @@ -156,7 +160,11 @@ HEADERS += vmainwindow.h \ dialog/vinsertlinkdialog.h \ vplaintextedit.h \ vimageresourcemanager.h \ - vlinenumberarea.h + vlinenumberarea.h \ + veditor.h \ + vmdeditor.h \ + veditconfig.h \ + vpreviewmanager.h RESOURCES += \ vnote.qrc \ diff --git a/src/utils/veditutils.cpp b/src/utils/veditutils.cpp index 789f42c1..50c87e34 100644 --- a/src/utils/veditutils.cpp +++ b/src/utils/veditutils.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "vutils.h" @@ -418,6 +419,92 @@ void VEditUtils::scrollBlockInPage(QTextEdit *p_edit, p_edit->ensureCursorVisible(); } +void VEditUtils::scrollBlockInPage(QPlainTextEdit *p_edit, + int p_blockNum, + int p_dest) +{ + QTextDocument *doc = p_edit->document(); + QTextCursor cursor = p_edit->textCursor(); + if (p_blockNum >= doc->blockCount()) { + p_blockNum = doc->blockCount() - 1; + } + + QTextBlock block = doc->findBlockByNumber(p_blockNum); + + int pib = cursor.positionInBlock(); + if (cursor.block().blockNumber() != p_blockNum) { + // Move the cursor to the block. + if (pib >= block.length()) { + pib = block.length() - 1; + } + + cursor.setPosition(block.position() + pib); + p_edit->setTextCursor(cursor); + } + + // Scroll to let current cursor locate in proper position. + p_edit->ensureCursorVisible(); + QScrollBar *vsbar = p_edit->verticalScrollBar(); + + if (!vsbar || !vsbar->isVisible()) { + // No vertical scrollbar. No need to scroll. + return; + } + + QRect rect = p_edit->cursorRect(); + int height = p_edit->rect().height(); + QScrollBar *sbar = p_edit->horizontalScrollBar(); + if (sbar && sbar->isVisible()) { + height -= sbar->height(); + } + + switch (p_dest) { + case 0: + { + // Top. + while (rect.y() > 0 && vsbar->value() < vsbar->maximum()) { + vsbar->setValue(vsbar->value() + vsbar->singleStep()); + rect = p_edit->cursorRect(); + } + + break; + } + + case 1: + { + // Center. + height = qMax(height / 2, 1); + if (rect.y() > height) { + while (rect.y() > height && vsbar->value() < vsbar->maximum()) { + vsbar->setValue(vsbar->value() + vsbar->singleStep()); + rect = p_edit->cursorRect(); + } + } else if (rect.y() < height) { + while (rect.y() < height && vsbar->value() > vsbar->minimum()) { + vsbar->setValue(vsbar->value() - vsbar->singleStep()); + rect = p_edit->cursorRect(); + } + } + + break; + } + + case 2: + // Bottom. + while (rect.y() < height && vsbar->value() > vsbar->minimum()) { + vsbar->setValue(vsbar->value() - vsbar->singleStep()); + rect = p_edit->cursorRect(); + } + + break; + + default: + break; + } + + p_edit->ensureCursorVisible(); +} + bool VEditUtils::isListBlock(const QTextBlock &p_block, int *p_seq) { QString text = p_block.text(); diff --git a/src/utils/veditutils.h b/src/utils/veditutils.h index bc0e3919..ce4eebd6 100644 --- a/src/utils/veditutils.h +++ b/src/utils/veditutils.h @@ -6,6 +6,7 @@ class QTextDocument; class QTextEdit; +class QPlainTextEdit; // Utils for text edit. class VEditUtils @@ -113,6 +114,14 @@ public: int p_blockNum, int p_dest); + // Scroll block @p_blockNum into the visual window. + // @p_dest is the position of the window: 0 for top, 1 for center, 2 for bottom. + // @p_blockNum is based on 0. + // Will set the cursor to the block. + static void scrollBlockInPage(QPlainTextEdit *p_edit, + int p_blockNum, + int p_dest); + // Check if @p_block is a auto list block. // @p_seq will be the seq number of the ordered list, or -1. // Returns true if it is an auto list block. diff --git a/src/utils/vvim.cpp b/src/utils/vvim.cpp index 5d9e1c6a..2ed52705 100644 --- a/src/utils/vvim.cpp +++ b/src/utils/vvim.cpp @@ -9,7 +9,7 @@ #include #include #include "vconfigmanager.h" -#include "vedit.h" +#include "veditor.h" #include "utils/veditutils.h" #include "vconstants.h" @@ -106,8 +106,8 @@ static QString keyToString(int p_key, int p_modifiers) } } -VVim::VVim(VEdit *p_editor) - : QObject(p_editor), m_editor(p_editor), +VVim::VVim(VEditor *p_editor) + : QObject(p_editor->getEditor()), m_editor(p_editor), m_editConfig(&p_editor->getConfig()), m_mode(VimMode::Invalid), m_resetPositionInBlock(true), m_regName(c_unnamedRegister), m_leaderKey(Key(Qt::Key_Space)), m_replayLeaderSequence(false), @@ -119,7 +119,7 @@ VVim::VVim(VEdit *p_editor) initRegisters(); - connect(m_editor, &VEdit::selectionChangedByMouse, + connect(m_editor->object(), &VEditorObject::selectionChangedByMouse, this, &VVim::selectionToVisualMode); } @@ -472,13 +472,13 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) // See if we need to cancel auto indent. bool cancelAutoIndent = false; if (p_autoIndentPos && *p_autoIndentPos > -1) { - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); cancelAutoIndent = VEditUtils::needToCancelAutoIndent(*p_autoIndentPos, cursor); if (cancelAutoIndent) { autoIndentPos = -1; VEditUtils::deleteIndentAndListMark(cursor); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); } } @@ -501,7 +501,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) QChar reg = keyToRegisterName(keyInfo); if (!reg.isNull()) { // Insert register content. - m_editor->insertPlainText(getRegister(reg).read()); + m_editor->insertPlainTextW(getRegister(reg).read()); } goto clear_accept; @@ -565,7 +565,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) // Expecting a mark name to create a mark. if (keyInfo.isAlphabet() && modifiers == Qt::NoModifier) { m_keys.clear(); - m_marks.setMark(keyToChar(key, modifiers), m_editor->textCursor()); + m_marks.setMark(keyToChar(key, modifiers), m_editor->textCursorW()); } goto clear_accept; @@ -804,7 +804,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) setMode(VimMode::Insert, false); } } else if (modifiers == Qt::ShiftModifier) { - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); if (m_mode == VimMode::Normal) { // Insert at the first non-space character. VEditUtils::moveCursorFirstNonSpaceCharacter(cursor, QTextCursor::MoveAnchor); @@ -815,7 +815,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) 1); } - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); setMode(VimMode::Insert); } else if (isControlModifier(modifiers)) { // Ctrl+I, jump to next location. @@ -856,29 +856,29 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) // Enter Insert mode. // Move cursor back one character. if (m_mode == VimMode::Normal) { - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); V_ASSERT(!cursor.hasSelection()); if (!cursor.atBlockEnd()) { cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 1); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); } setMode(VimMode::Insert); } } else if (modifiers == Qt::ShiftModifier) { // Insert at the end of line. - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); if (m_mode == VimMode::Normal) { cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor, 1); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); } else if (m_mode == VimMode::Visual || m_mode == VimMode::VisualLine) { if (!cursor.atBlockEnd()) { cursor.clearSelection(); cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 1); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); } } @@ -894,7 +894,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) // Insert a new block under/above current block and enter insert mode. bool insertAbove = modifiers == Qt::ShiftModifier; if (m_mode == VimMode::Normal) { - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); cursor.beginEditBlock(); cursor.movePosition(insertAbove ? QTextCursor::StartOfBlock : QTextCursor::EndOfBlock, @@ -919,7 +919,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) } cursor.endEditBlock(); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); if (textInserted) { autoIndentPos = cursor.position(); @@ -1104,7 +1104,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) } else if (m_keys.isEmpty() && !hasActionToken()) { if (m_mode == VimMode::Visual || m_mode == VimMode::VisualLine) { // u/U for tolower and toupper selected text. - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); cursor.beginEditBlock(); // Different from Vim: // If there is no selection in Visual mode, we do nothing. @@ -1116,7 +1116,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) convertCaseOfSelectedText(cursor, toLower); cursor.endEditBlock(); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); setMode(VimMode::Normal); break; @@ -1143,7 +1143,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) } else if (checkPendingKey(Key(Qt::Key_G))) { // gu/gU, ToLower/ToUpper action. if (m_mode == VimMode::Visual || m_mode == VimMode::VisualLine) { - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); cursor.beginEditBlock(); // Different from Vim: // If there is no selection in Visual mode, we do nothing. @@ -1155,7 +1155,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) convertCaseOfSelectedText(cursor, toLower); cursor.endEditBlock(); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); setMode(VimMode::Normal); break; } @@ -1338,7 +1338,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) // Clear selection and enter normal mode. bool ret = clearSelection(); if (!ret && checkMode(VimMode::Normal)) { - emit m_editor->requestCloseFindReplaceDialog(); + emit m_editor->object()->requestCloseFindReplaceDialog(); } setMode(VimMode::Normal); @@ -1367,9 +1367,9 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) setMode(mode); if (m_mode == VimMode::VisualLine) { - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); expandSelectionToWholeLines(cursor); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); } } @@ -1681,8 +1681,8 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) } else { // The first >/<, an Action. if (m_mode == VimMode::Visual || m_mode == VimMode::VisualLine) { - QTextCursor cursor = m_editor->textCursor(); - VEditUtils::indentSelectedBlocks(m_editor->document(), + QTextCursor cursor = m_editor->textCursorW(); + VEditUtils::indentSelectedBlocks(m_editor->documentW(), cursor, m_editConfig->m_tabSpaces, !unindent); @@ -1857,7 +1857,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) // xx%, jump to a certain line (percentage of the documents). // Change the repeat from percentage to line number. Token *token = getRepeatToken(); - int bn = percentageToBlockNumber(m_editor->document(), token->m_repeat); + int bn = percentageToBlockNumber(m_editor->documentW(), token->m_repeat); if (bn == -1) { break; } else { @@ -2148,7 +2148,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) clear_accept: resetState(); - m_editor->makeBlockVisible(m_editor->textCursor().block()); + m_editor->makeBlockVisible(m_editor->textCursorW().block()); accept: ret = true; @@ -2353,7 +2353,7 @@ void VVim::processMoveAction(QList &p_tokens) return; } - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); if (m_resetPositionInBlock) { positionInBlock = cursor.positionInBlock(); } @@ -2389,7 +2389,7 @@ void VVim::processMoveAction(QList &p_tokens) expandSelectionToWholeLines(cursor); } - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); } } @@ -3383,8 +3383,8 @@ void VVim::processDeleteAction(QList &p_tokens) return; } - QTextCursor cursor = m_editor->textCursor(); - QTextDocument *doc = m_editor->document(); + QTextCursor cursor = m_editor->textCursorW(); + QTextDocument *doc = m_editor->documentW(); bool hasMoved = false; QTextCursor::MoveMode moveMode = QTextCursor::KeepAnchor; @@ -3598,7 +3598,7 @@ void VVim::processDeleteAction(QList &p_tokens) exit: if (hasMoved) { - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); } } @@ -3616,8 +3616,8 @@ void VVim::processCopyAction(QList &p_tokens) return; } - QTextCursor cursor = m_editor->textCursor(); - QTextDocument *doc = m_editor->document(); + QTextCursor cursor = m_editor->textCursorW(); + QTextDocument *doc = m_editor->documentW(); int oriPos = cursor.position(); bool changed = false; QTextCursor::MoveMode moveMode = QTextCursor::KeepAnchor; @@ -3810,7 +3810,7 @@ void VVim::processCopyAction(QList &p_tokens) exit: if (changed) { - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); } } @@ -3853,7 +3853,7 @@ void VVim::processPasteAction(QList &p_tokens, bool p_pasteBefore) bool changed = false; int nrBlock = 0; int restorePos = -1; - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); cursor.beginEditBlock(); // Different from Vim: @@ -3942,7 +3942,7 @@ void VVim::processPasteAction(QList &p_tokens, bool p_pasteBefore) cursor.endEditBlock(); if (changed) { - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); } qDebug() << "text pasted" << text; @@ -3962,8 +3962,8 @@ void VVim::processChangeAction(QList &p_tokens) return; } - QTextCursor cursor = m_editor->textCursor(); - QTextDocument *doc = m_editor->document(); + QTextCursor cursor = m_editor->textCursorW(); + QTextDocument *doc = m_editor->documentW(); bool hasMoved = false; QTextCursor::MoveMode moveMode = QTextCursor::KeepAnchor; @@ -4226,7 +4226,7 @@ void VVim::processChangeAction(QList &p_tokens) int pos = cursor.selectionStart(); bool allDeleted = false; if (pos == 0) { - QTextBlock block = m_editor->document()->lastBlock(); + QTextBlock block = m_editor->documentW()->lastBlock(); if (block.position() + block.length() - 1 == cursor.selectionEnd()) { allDeleted = true; } @@ -4243,7 +4243,7 @@ void VVim::processChangeAction(QList &p_tokens) exit: if (hasMoved) { - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); } setMode(VimMode::Insert); @@ -4263,8 +4263,8 @@ void VVim::processIndentAction(QList &p_tokens, bool p_isIndent) return; } - QTextCursor cursor = m_editor->textCursor(); - QTextDocument *doc = m_editor->document(); + QTextCursor cursor = m_editor->textCursorW(); + QTextDocument *doc = m_editor->documentW(); if (to.isRange()) { bool changed = selectRange(cursor, doc, to.m_range, repeat); @@ -4402,8 +4402,8 @@ void VVim::processToLowerAction(QList &p_tokens, bool p_toLower) return; } - QTextCursor cursor = m_editor->textCursor(); - QTextDocument *doc = m_editor->document(); + QTextCursor cursor = m_editor->textCursorW(); + QTextDocument *doc = m_editor->documentW(); bool changed = false; QTextCursor::MoveMode moveMode = QTextCursor::KeepAnchor; int oriPos = cursor.position(); @@ -4500,7 +4500,7 @@ void VVim::processToLowerAction(QList &p_tokens, bool p_toLower) exit: if (changed) { - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); } } @@ -4517,10 +4517,10 @@ void VVim::processUndoAction(QList &p_tokens) repeat = to.m_repeat; } - QTextDocument *doc = m_editor->document(); + QTextDocument *doc = m_editor->documentW(); int i = 0; for (i = 0; i < repeat && doc->isUndoAvailable(); ++i) { - m_editor->undo(); + m_editor->undoW(); } message(tr("Undo %1 %2").arg(i).arg(i > 1 ? tr("changes") : tr("change"))); @@ -4539,10 +4539,10 @@ void VVim::processRedoAction(QList &p_tokens) repeat = to.m_repeat; } - QTextDocument *doc = m_editor->document(); + QTextDocument *doc = m_editor->documentW(); int i = 0; for (i = 0; i < repeat && doc->isRedoAvailable(); ++i) { - m_editor->redo(); + m_editor->redoW(); } message(tr("Redo %1 %2").arg(i).arg(i > 1 ? tr("changes") : tr("change"))); @@ -4550,7 +4550,7 @@ void VVim::processRedoAction(QList &p_tokens) void VVim::processRedrawLineAction(QList &p_tokens, int p_dest) { - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); int repeat = cursor.block().blockNumber(); if (!p_tokens.isEmpty()) { Token to = p_tokens.takeFirst(); @@ -4562,7 +4562,7 @@ void VVim::processRedrawLineAction(QList &p_tokens, int p_dest) repeat = to.m_repeat - 1; } - VEditUtils::scrollBlockInPage(m_editor, repeat, p_dest); + m_editor->scrollBlockInPage(repeat, p_dest); } void VVim::processJumpLocationAction(QList &p_tokens, bool p_next) @@ -4578,7 +4578,7 @@ void VVim::processJumpLocationAction(QList &p_tokens, bool p_next) repeat = to.m_repeat; } - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); Location loc; if (p_next) { while (m_locations.hasNext() && repeat > 0) { @@ -4593,7 +4593,7 @@ void VVim::processJumpLocationAction(QList &p_tokens, bool p_next) } if (loc.isValid()) { - QTextDocument *doc = m_editor->document(); + QTextDocument *doc = m_editor->documentW(); if (loc.m_blockNumber >= doc->blockCount()) { message(tr("Mark has invalid line number")); return; @@ -4607,11 +4607,11 @@ void VVim::processJumpLocationAction(QList &p_tokens, bool p_next) if (!m_editor->isBlockVisible(block)) { // Scroll the block to the center of screen. - VEditUtils::scrollBlockInPage(m_editor, block.blockNumber(), 1); + m_editor->scrollBlockInPage(block.blockNumber(), 1); } cursor.setPosition(block.position() + pib); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); } } @@ -4643,7 +4643,7 @@ void VVim::processReplaceAction(QList &p_tokens) // If repeat is greater than the number of left characters in current line, // do nothing. // In visual mode, repeat is ignored. - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); cursor.beginEditBlock(); if (checkMode(VimMode::Normal)) { // Select the characters to be replaced. @@ -4659,7 +4659,7 @@ void VVim::processReplaceAction(QList &p_tokens) cursor.endEditBlock(); if (changed) { - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); setMode(VimMode::Normal); } } @@ -4683,7 +4683,7 @@ void VVim::processReverseCaseAction(QList &p_tokens) // If repeat is greater than the number of left characters in current line, // just change the actual number of left characters. // In visual mode, repeat is ignored and reverse the selected text. - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); cursor.beginEditBlock(); if (checkMode(VimMode::Normal)) { // Select the characters to be replaced. @@ -4699,7 +4699,7 @@ void VVim::processReverseCaseAction(QList &p_tokens) cursor.endEditBlock(); if (changed) { - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); setMode(VimMode::Normal); } } @@ -4726,12 +4726,12 @@ void VVim::processJoinAction(QList &p_tokens, bool p_modifySpaces) // In visual mode, repeat is ignored and join the highlighted lines. int firstBlock = -1; int blockCount = repeat; - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); cursor.beginEditBlock(); if (checkMode(VimMode::Normal)) { firstBlock = cursor.block().blockNumber(); } else { - QTextDocument *doc = m_editor->document(); + QTextDocument *doc = m_editor->documentW(); firstBlock = doc->findBlock(cursor.selectionStart()).blockNumber(); int lastBlock = doc->findBlock(cursor.selectionEnd()).blockNumber(); blockCount = lastBlock - firstBlock + 1; @@ -4741,17 +4741,17 @@ void VVim::processJoinAction(QList &p_tokens, bool p_modifySpaces) cursor.endEditBlock(); if (changed) { - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); setMode(VimMode::Normal); } } bool VVim::clearSelection() { - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); if (cursor.hasSelection()) { cursor.clearSelection(); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); return true; } @@ -4760,8 +4760,8 @@ bool VVim::clearSelection() int VVim::blockCountOfPageStep() const { - int lineCount = m_editor->document()->blockCount(); - QScrollBar *bar = m_editor->verticalScrollBar(); + int lineCount = m_editor->documentW()->blockCount(); + QScrollBar *bar = m_editor->verticalScrollBarW(); int steps = (bar->maximum() - bar->minimum() + bar->pageStep()); int pageLineCount = lineCount * (bar->pageStep() * 1.0 / steps); return pageLineCount; @@ -4781,7 +4781,7 @@ void VVim::selectionToVisualMode(bool p_hasText) void VVim::expandSelectionToWholeLines(QTextCursor &p_cursor) { - QTextDocument *doc = m_editor->document(); + QTextDocument *doc = m_editor->documentW(); int curPos = p_cursor.position(); int anchorPos = p_cursor.anchor(); QTextBlock curBlock = doc->findBlock(curPos); @@ -5031,12 +5031,12 @@ void VVim::deleteSelectedText(QTextCursor &p_cursor, bool p_clearEmptyBlock) void VVim::copySelectedText(bool p_addNewLine) { - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); if (cursor.hasSelection()) { cursor.beginEditBlock(); copySelectedText(cursor, p_addNewLine); cursor.endEditBlock(); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); } } @@ -5318,15 +5318,15 @@ bool VVim::executeCommand(const QString &p_cmd) }else if (p_cmd.size() == 1) { if (p_cmd == "w") { // :w, save current file. - emit m_editor->saveNote(); + emit m_editor->object()->saveNote(); msg = tr("Note has been saved"); } else if (p_cmd == "q") { // :q, quit edit mode. - emit m_editor->discardAndRead(); + emit m_editor->object()->discardAndRead(); msg = tr("Quit"); } else if (p_cmd == "x") { // :x, save if there is any change and quit edit mode. - emit m_editor->saveAndRead(); + emit m_editor->object()->saveAndRead(); msg = tr("Quit with note having been saved"); } else { validCommand = false; @@ -5335,11 +5335,11 @@ bool VVim::executeCommand(const QString &p_cmd) if (p_cmd == "wq") { // :wq, save change and quit edit mode. // We treat it same as :x. - emit m_editor->saveAndRead(); + emit m_editor->object()->saveAndRead(); msg = tr("Quit with note having been saved"); } else if (p_cmd == "q!") { // :q!, discard change and quit edit mode. - emit m_editor->discardAndRead(); + emit m_editor->object()->discardAndRead(); msg = tr("Quit"); } else { validCommand = false; @@ -5437,7 +5437,7 @@ bool VVim::processLeaderSequence(const Key &p_key) clearSearchHighlight(); } else if (p_key == Key(Qt::Key_W)) { // w, save note - emit m_editor->saveNote(); + emit m_editor->object()->saveNote(); message(tr("Note has been saved")); } else { validSequence = false; @@ -5624,7 +5624,7 @@ void VVim::processTitleJump(const QList &p_tokens, bool p_forward, int p_ return; } - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); if (m_editor->jumpTitle(p_forward, p_relativeLevel, repeat)) { // Record current location. m_locations.addLocation(cursor); diff --git a/src/utils/vvim.h b/src/utils/vvim.h index 7101d7c0..4e14be3d 100644 --- a/src/utils/vvim.h +++ b/src/utils/vvim.h @@ -8,7 +8,7 @@ #include #include "vutils.h" -class VEdit; +class VEditor; class QKeyEvent; class VEditConfig; class QKeyEvent; @@ -26,7 +26,7 @@ class VVim : public QObject { Q_OBJECT public: - explicit VVim(VEdit *p_editor); + explicit VVim(VEditor *p_editor); // Struct for a location. struct Location @@ -801,7 +801,7 @@ private: Register &getRegister(QChar p_regName) const; void setRegister(QChar p_regName, const QString &p_val); - VEdit *m_editor; + VEditor *m_editor; const VEditConfig *m_editConfig; VimMode m_mode; diff --git a/src/vconstants.h b/src/vconstants.h index 217e7b4b..972b6b1b 100644 --- a/src/vconstants.h +++ b/src/vconstants.h @@ -95,14 +95,6 @@ enum HighlightBlockState Comment }; -enum class LineNumberType -{ - None = 0, - Absolute, - Relative, - CodeBlock -}; - // Pages to open on start up. enum class StartupPageType { diff --git a/src/vedit.cpp b/src/vedit.cpp index 9866bc56..ffe6debc 100644 --- a/src/vedit.cpp +++ b/src/vedit.cpp @@ -18,44 +18,6 @@ extern VNote *g_vnote; extern VMetaWordManager *g_mwMgr; -void VEditConfig::init(const QFontMetrics &p_metric, - bool p_enableHeadingSequence) -{ - update(p_metric); - - // Init configs that do not support update later. - m_enableVimMode = g_config->getEnableVimMode(); - - if (g_config->getLineDistanceHeight() <= 0) { - m_lineDistanceHeight = 0; - } else { - m_lineDistanceHeight = g_config->getLineDistanceHeight() * VUtils::calculateScaleFactor(); - } - - m_highlightWholeBlock = m_enableVimMode; - - m_enableHeadingSequence = p_enableHeadingSequence; -} - -void VEditConfig::update(const QFontMetrics &p_metric) -{ - if (g_config->getTabStopWidth() > 0) { - m_tabStopWidth = g_config->getTabStopWidth() * p_metric.width(' '); - } else { - m_tabStopWidth = 0; - } - - m_expandTab = g_config->getIsExpandTab(); - - if (m_expandTab && (g_config->getTabStopWidth() > 0)) { - m_tabSpaces = QString(g_config->getTabStopWidth(), ' '); - } else { - m_tabSpaces = "\t"; - } - - m_cursorLineBg = QColor(g_config->getEditorCurrentLineBg()); -} - VEdit::VEdit(VFile *p_file, QWidget *p_parent) : QTextEdit(p_parent), m_file(p_file), m_editOps(NULL), m_enableInputMethod(true) diff --git a/src/vedit.h b/src/vedit.h index ff58524f..10d52c78 100644 --- a/src/vedit.h +++ b/src/vedit.h @@ -11,6 +11,8 @@ #include #include "vconstants.h" #include "vnotefile.h" +#include "veditconfig.h" +#include "veditor.h" class VEditOperations; class QLabel; @@ -21,55 +23,6 @@ class QResizeEvent; class QSize; class QWidget; -enum class SelectionId { - CurrentLine = 0, - SelectedWord, - SearchedKeyword, - SearchedKeywordUnderCursor, - IncrementalSearchedKeyword, - TrailingSapce, - MaxSelection -}; - -class VEditConfig { -public: - VEditConfig() : m_tabStopWidth(0), - m_tabSpaces("\t"), - m_enableVimMode(false), - m_highlightWholeBlock(false), - m_lineDistanceHeight(0), - m_enableHeadingSequence(false) - {} - - void init(const QFontMetrics &p_metric, - bool p_enableHeadingSequence); - - // Only update those configs which could be updated online. - void update(const QFontMetrics &p_metric); - - // Width in pixels. - int m_tabStopWidth; - - bool m_expandTab; - - // The literal string for Tab. It is spaces if Tab is expanded. - QString m_tabSpaces; - - bool m_enableVimMode; - - // The background color of cursor line. - QColor m_cursorLineBg; - - // Whether highlight a visual line or a whole block. - bool m_highlightWholeBlock; - - // Line distance height in pixels. - int m_lineDistanceHeight; - - // Whether enable auto heading sequence. - bool m_enableHeadingSequence; -}; - class LineNumberArea; class VEdit : public QTextEdit @@ -98,7 +51,7 @@ public: // User has enter the content to search, but does not enter the "find" button yet. bool peekText(const QString &p_text, uint p_options, bool p_forward = true); - // If @p_cursor is not now, set the position of @p_cursor instead of current + // If @p_cursor is not null, set the position of @p_cursor instead of current // cursor. bool findText(const QString &p_text, uint p_options, bool p_forward, QTextCursor *p_cursor = NULL, diff --git a/src/veditconfig.cpp b/src/veditconfig.cpp new file mode 100644 index 00000000..9866d7fa --- /dev/null +++ b/src/veditconfig.cpp @@ -0,0 +1,44 @@ +#include "veditconfig.h" + +#include "vconfigmanager.h" +#include "utils/vutils.h" + +extern VConfigManager *g_config; + +void VEditConfig::init(const QFontMetrics &p_metric, + bool p_enableHeadingSequence) +{ + update(p_metric); + + // Init configs that do not support update later. + m_enableVimMode = g_config->getEnableVimMode(); + + if (g_config->getLineDistanceHeight() <= 0) { + m_lineDistanceHeight = 0; + } else { + m_lineDistanceHeight = g_config->getLineDistanceHeight() * VUtils::calculateScaleFactor(); + } + + m_highlightWholeBlock = m_enableVimMode; + + m_enableHeadingSequence = p_enableHeadingSequence; +} + +void VEditConfig::update(const QFontMetrics &p_metric) +{ + if (g_config->getTabStopWidth() > 0) { + m_tabStopWidth = g_config->getTabStopWidth() * p_metric.width(' '); + } else { + m_tabStopWidth = 0; + } + + m_expandTab = g_config->getIsExpandTab(); + + if (m_expandTab && (g_config->getTabStopWidth() > 0)) { + m_tabSpaces = QString(g_config->getTabStopWidth(), ' '); + } else { + m_tabSpaces = "\t"; + } + + m_cursorLineBg = QColor(g_config->getEditorCurrentLineBg()); +} diff --git a/src/veditconfig.h b/src/veditconfig.h new file mode 100644 index 00000000..af1803b0 --- /dev/null +++ b/src/veditconfig.h @@ -0,0 +1,48 @@ +#ifndef VEDITCONFIG_H +#define VEDITCONFIG_H + +#include +#include +#include + + +class VEditConfig { +public: + VEditConfig() : m_tabStopWidth(0), + m_tabSpaces("\t"), + m_enableVimMode(false), + m_highlightWholeBlock(false), + m_lineDistanceHeight(0), + m_enableHeadingSequence(false) + {} + + void init(const QFontMetrics &p_metric, + bool p_enableHeadingSequence); + + // Only update those configs which could be updated online. + void update(const QFontMetrics &p_metric); + + // Width in pixels. + int m_tabStopWidth; + + bool m_expandTab; + + // The literal string for Tab. It is spaces if Tab is expanded. + QString m_tabSpaces; + + bool m_enableVimMode; + + // The background color of cursor line. + QColor m_cursorLineBg; + + // Whether highlight a visual line or a whole block. + bool m_highlightWholeBlock; + + // Line distance height in pixels. + int m_lineDistanceHeight; + + // Whether enable auto heading sequence. + bool m_enableHeadingSequence; +}; + +#endif // VEDITCONFIG_H diff --git a/src/veditoperations.cpp b/src/veditoperations.cpp index 9a666a69..e11887aa 100644 --- a/src/veditoperations.cpp +++ b/src/veditoperations.cpp @@ -1,18 +1,21 @@ #include #include #include -#include "vedit.h" +#include "veditor.h" #include "veditoperations.h" #include "vconfigmanager.h" #include "utils/vutils.h" extern VConfigManager *g_config; -VEditOperations::VEditOperations(VEdit *p_editor, VFile *p_file) - : QObject(p_editor), m_editor(p_editor), m_file(p_file), - m_editConfig(&p_editor->getConfig()), m_vim(NULL) +VEditOperations::VEditOperations(VEditor *p_editor, VFile *p_file) + : QObject(p_editor->getEditor()), + m_editor(p_editor), + m_file(p_file), + m_editConfig(&p_editor->getConfig()), + m_vim(NULL) { - connect(m_editor, &VEdit::configUpdated, + connect(m_editor->object(), &VEditorObject::configUpdated, this, &VEditOperations::handleEditConfigUpdated); if (m_editConfig->m_enableVimMode) { @@ -29,7 +32,7 @@ VEditOperations::VEditOperations(VEdit *p_editor, VFile *p_file) void VEditOperations::insertTextAtCurPos(const QString &p_text) { - m_editor->insertPlainText(p_text); + m_editor->insertPlainTextW(p_text); } VEditOperations::~VEditOperations() diff --git a/src/veditoperations.h b/src/veditoperations.h index 3c0e4ac5..3af62c13 100644 --- a/src/veditoperations.h +++ b/src/veditoperations.h @@ -8,7 +8,7 @@ #include "vfile.h" #include "utils/vvim.h" -class VEdit; +class VEditor; class VEditConfig; class QMimeData; class QKeyEvent; @@ -17,7 +17,7 @@ class VEditOperations: public QObject { Q_OBJECT public: - VEditOperations(VEdit *p_editor, VFile *p_file); + VEditOperations(VEditor *p_editor, VFile *p_file); virtual ~VEditOperations(); @@ -64,7 +64,7 @@ private: protected: void insertTextAtCurPos(const QString &p_text); - VEdit *m_editor; + VEditor *m_editor; QPointer m_file; VEditConfig *m_editConfig; VVim *m_vim; diff --git a/src/veditor.cpp b/src/veditor.cpp new file mode 100644 index 00000000..2137aee9 --- /dev/null +++ b/src/veditor.cpp @@ -0,0 +1,917 @@ +#include "veditor.h" + +#include +#include + +#include "vconfigmanager.h" +#include "utils/vutils.h" +#include "utils/veditutils.h" +#include "veditoperations.h" +#include "dialog/vinsertlinkdialog.h" +#include "utils/vmetawordmanager.h" + +extern VConfigManager *g_config; + +extern VMetaWordManager *g_mwMgr; + +VEditor::VEditor(VFile *p_file, QWidget *p_editor) + : m_editor(p_editor), + m_object(new VEditorObject(this, p_editor)), + m_file(p_file), + m_editOps(nullptr), + m_document(nullptr), + m_enableInputMethod(true) +{ +} + +VEditor::~VEditor() +{ + if (m_file && m_document) { + QObject::disconnect(m_document, &QTextDocument::modificationChanged, + (VFile *)m_file, &VFile::setModified); + } +} + +void VEditor::init() +{ + const int labelTimerInterval = 500; + const int extraSelectionHighlightTimer = 500; + const int labelSize = 64; + + m_document = documentW(); + + m_selectedWordColor = QColor(g_config->getEditorSelectedWordBg()); + m_searchedWordColor = QColor(g_config->getEditorSearchedWordBg()); + m_searchedWordCursorColor = QColor(g_config->getEditorSearchedWordCursorBg()); + m_incrementalSearchedWordColor = QColor(g_config->getEditorIncrementalSearchedWordBg()); + m_trailingSpaceColor = QColor(g_config->getEditorTrailingSpaceBg()); + + QPixmap wrapPixmap(":/resources/icons/search_wrap.svg"); + m_wrapLabel = new QLabel(m_editor); + m_wrapLabel->setPixmap(wrapPixmap.scaled(labelSize, labelSize)); + m_wrapLabel->hide(); + m_labelTimer = new QTimer(m_editor); + m_labelTimer->setSingleShot(true); + m_labelTimer->setInterval(labelTimerInterval); + QObject::connect(m_labelTimer, &QTimer::timeout, + m_object, &VEditorObject::labelTimerTimeout); + + m_highlightTimer = new QTimer(m_editor); + m_highlightTimer->setSingleShot(true); + m_highlightTimer->setInterval(extraSelectionHighlightTimer); + QObject::connect(m_highlightTimer, &QTimer::timeout, + m_object, &VEditorObject::doHighlightExtraSelections); + + m_extraSelections.resize((int)SelectionId::MaxSelection); + + QObject::connect(m_document, &QTextDocument::modificationChanged, + (VFile *)m_file, &VFile::setModified); + + updateFontAndPalette(); + + m_config.init(QFontMetrics(m_editor->font()), false); + updateEditConfig(); +} + +void VEditor::labelTimerTimeout() +{ + m_wrapLabel->hide(); +} + +void VEditor::doHighlightExtraSelections() +{ + int nrExtra = m_extraSelections.size(); + Q_ASSERT(nrExtra == (int)SelectionId::MaxSelection); + QList extraSelects; + for (int i = 0; i < nrExtra; ++i) { + extraSelects.append(m_extraSelections[i]); + } + + setExtraSelectionsW(extraSelects); +} + +void VEditor::updateEditConfig() +{ + m_config.update(QFontMetrics(m_editor->font())); + + if (m_config.m_tabStopWidth > 0) { + setTabStopWidthW(m_config.m_tabStopWidth); + } + + emit m_object->configUpdated(); +} + +void VEditor::highlightOnCursorPositionChanged() +{ + static QTextCursor lastCursor; + + QTextCursor cursor = textCursorW(); + if (lastCursor.isNull() || cursor.blockNumber() != lastCursor.blockNumber()) { + highlightCurrentLine(); + highlightTrailingSpace(); + } else { + // Judge whether we have trailing space at current line. + QString text = cursor.block().text(); + if (text.rbegin()->isSpace()) { + highlightTrailingSpace(); + } + + // Handle word-wrap in one block. + // Highlight current line if in different visual line. + if ((lastCursor.positionInBlock() - lastCursor.columnNumber()) != + (cursor.positionInBlock() - cursor.columnNumber())) { + highlightCurrentLine(); + } + } + + lastCursor = cursor; +} + +void VEditor::highlightCurrentLine() +{ + QList &selects = m_extraSelections[(int)SelectionId::CurrentLine]; + if (g_config->getHighlightCursorLine()) { + // Need to highlight current line. + selects.clear(); + + // A long block maybe splited into multiple visual lines. + QTextEdit::ExtraSelection select; + select.format.setBackground(m_config.m_cursorLineBg); + select.format.setProperty(QTextFormat::FullWidthSelection, true); + + QTextCursor cursor = textCursorW(); + if (m_config.m_highlightWholeBlock) { + cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor, 1); + QTextBlock block = cursor.block(); + int blockEnd = block.position() + block.length(); + int pos = -1; + while (cursor.position() < blockEnd && pos != cursor.position()) { + QTextEdit::ExtraSelection newSelect = select; + newSelect.cursor = cursor; + selects.append(newSelect); + + pos = cursor.position(); + cursor.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor, 1); + } + } else { + cursor.clearSelection(); + select.cursor = cursor; + selects.append(select); + } + } else { + // Need to clear current line highlight. + if (selects.isEmpty()) { + return; + } + + selects.clear(); + } + + highlightExtraSelections(true); +} + +// Do not highlight trailing spaces with current cursor right behind. +static void trailingSpaceFilter(VEditor *p_editor, QList &p_result) +{ + QTextCursor cursor = p_editor->textCursorW(); + if (!cursor.atBlockEnd()) { + return; + } + + int cursorPos = cursor.position(); + for (auto it = p_result.begin(); it != p_result.end(); ++it) { + if (it->cursor.selectionEnd() == cursorPos) { + p_result.erase(it); + + // There will be only one. + return; + } + } +} + +void VEditor::highlightTrailingSpace() +{ + if (!g_config->getEnableTrailingSpaceHighlight()) { + QList &selects = m_extraSelections[(int)SelectionId::TrailingSapce]; + if (!selects.isEmpty()) { + selects.clear(); + highlightExtraSelections(true); + } + return; + } + + QTextCharFormat format; + format.setBackground(m_trailingSpaceColor); + QString text("\\s+$"); + highlightTextAll(text, + FindOption::RegularExpression, + SelectionId::TrailingSapce, + format, + trailingSpaceFilter); +} + +void VEditor::highlightExtraSelections(bool p_now) +{ + m_highlightTimer->stop(); + if (p_now) { + doHighlightExtraSelections(); + } else { + m_highlightTimer->start(); + } +} + +void VEditor::highlightTextAll(const QString &p_text, + uint p_options, + SelectionId p_id, + QTextCharFormat p_format, + void (*p_filter)(VEditor *, + QList &)) +{ + QList &selects = m_extraSelections[(int)p_id]; + if (!p_text.isEmpty()) { + selects.clear(); + + QList occurs = findTextAll(p_text, p_options); + for (int i = 0; i < occurs.size(); ++i) { + QTextEdit::ExtraSelection select; + select.format = p_format; + select.cursor = occurs[i]; + selects.append(select); + } + } else { + if (selects.isEmpty()) { + return; + } + selects.clear(); + } + + if (p_filter) { + p_filter(this, selects); + } + + highlightExtraSelections(); +} + +QList VEditor::findTextAll(const QString &p_text, uint p_options) +{ + QList results; + if (p_text.isEmpty()) { + return results; + } + + // Options + QTextDocument::FindFlags findFlags; + bool caseSensitive = false; + if (p_options & FindOption::CaseSensitive) { + findFlags |= QTextDocument::FindCaseSensitively; + caseSensitive = true; + } + + if (p_options & FindOption::WholeWordOnly) { + findFlags |= QTextDocument::FindWholeWords; + } + + // Use regular expression + bool useRegExp = false; + QRegExp exp; + if (p_options & FindOption::RegularExpression) { + useRegExp = true; + exp = QRegExp(p_text, + caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive); + } + + int startPos = 0; + QTextCursor cursor; + while (true) { + if (useRegExp) { + cursor = m_document->find(exp, startPos, findFlags); + } else { + cursor = m_document->find(p_text, startPos, findFlags); + } + + if (cursor.isNull()) { + break; + } else { + results.append(cursor); + startPos = cursor.selectionEnd(); + } + } + + return results; +} + +void VEditor::highlightSelectedWord() +{ + QList &selects = m_extraSelections[(int)SelectionId::SelectedWord]; + if (!g_config->getHighlightSelectedWord()) { + if (!selects.isEmpty()) { + selects.clear(); + highlightExtraSelections(true); + } + + return; + } + + QString text = textCursorW().selectedText().trimmed(); + if (text.isEmpty() || wordInSearchedSelection(text)) { + selects.clear(); + highlightExtraSelections(true); + return; + } + + QTextCharFormat format; + format.setBackground(m_selectedWordColor); + highlightTextAll(text, + FindOption::CaseSensitive, + SelectionId::SelectedWord, + format); +} + +bool VEditor::wordInSearchedSelection(const QString &p_text) +{ + QString text = p_text.trimmed(); + QList &selects = m_extraSelections[(int)SelectionId::SearchedKeyword]; + for (int i = 0; i < selects.size(); ++i) { + QString searchedWord = selects[i].cursor.selectedText(); + if (text == searchedWord.trimmed()) { + return true; + } + } + + return false; +} + +bool VEditor::isModified() const +{ + Q_ASSERT(m_file ? (m_file->isModified() == m_document->isModified()) + : true); + return m_document->isModified(); +} + +void VEditor::setModified(bool p_modified) +{ + m_document->setModified(p_modified); + if (m_file) { + m_file->setModified(p_modified); + } +} + +void VEditor::insertImage() +{ + if (m_editOps) { + m_editOps->insertImage(); + } +} + +void VEditor::insertLink() +{ + if (!m_editOps) { + return; + } + + QString text; + QString linkText, linkUrl; + QTextCursor cursor = textCursorW(); + if (cursor.hasSelection()) { + text = VEditUtils::selectedText(cursor).trimmed(); + // Only pure space is accepted. + QRegExp reg("[\\S ]*"); + if (reg.exactMatch(text)) { + QUrl url = QUrl::fromUserInput(text, + m_file->fetchBasePath()); + QRegExp urlReg("[\\.\\\\/]"); + if (url.isValid() + && text.contains(urlReg)) { + // Url. + linkUrl = text; + } else { + // Text. + linkText = text; + } + } + } + + VInsertLinkDialog dialog(QObject::tr("Insert Link"), + "", + "", + linkText, + linkUrl, + m_editor); + if (dialog.exec() == QDialog::Accepted) { + linkText = dialog.getLinkText(); + linkUrl = dialog.getLinkUrl(); + Q_ASSERT(!linkText.isEmpty() && !linkUrl.isEmpty()); + + m_editOps->insertLink(linkText, linkUrl); + } +} + +bool VEditor::peekText(const QString &p_text, uint p_options, bool p_forward) +{ + if (p_text.isEmpty()) { + makeBlockVisible(m_document->findBlock(textCursorW().selectionStart())); + highlightIncrementalSearchedWord(QTextCursor()); + return false; + } + + bool wrapped = false; + QTextCursor retCursor; + bool found = findTextHelper(p_text, + p_options, + p_forward, + p_forward ? textCursorW().position() + 1 + : textCursorW().position(), + wrapped, + retCursor); + if (found) { + makeBlockVisible(m_document->findBlock(retCursor.selectionStart())); + highlightIncrementalSearchedWord(retCursor); + } + + return found; +} + +bool VEditor::findText(const QString &p_text, + uint p_options, + bool p_forward, + QTextCursor *p_cursor, + QTextCursor::MoveMode p_moveMode) +{ + clearIncrementalSearchedWordHighlight(); + + if (p_text.isEmpty()) { + clearSearchedWordHighlight(); + return false; + } + + QTextCursor cursor = textCursorW(); + bool wrapped = false; + QTextCursor retCursor; + int matches = 0; + int start = p_forward ? cursor.position() + 1 : cursor.position(); + if (p_cursor) { + start = p_forward ? p_cursor->position() + 1 : p_cursor->position(); + } + + bool found = findTextHelper(p_text, p_options, p_forward, start, + wrapped, retCursor); + if (found) { + Q_ASSERT(!retCursor.isNull()); + if (wrapped) { + showWrapLabel(); + } + + if (p_cursor) { + p_cursor->setPosition(retCursor.selectionStart(), p_moveMode); + } else { + cursor.setPosition(retCursor.selectionStart(), p_moveMode); + setTextCursorW(cursor); + } + + highlightSearchedWord(p_text, p_options); + highlightSearchedWordUnderCursor(retCursor); + matches = m_extraSelections[(int)SelectionId::SearchedKeyword].size(); + } else { + clearSearchedWordHighlight(); + } + + if (matches == 0) { + emit m_object->statusMessage(QObject::tr("Found no match")); + } else { + emit m_object->statusMessage(QObject::tr("Found %1 %2").arg(matches) + .arg(matches > 1 ? QObject::tr("matches") + : QObject::tr("match"))); + } + + return found; +} + +void VEditor::highlightIncrementalSearchedWord(const QTextCursor &p_cursor) +{ + QList &selects = m_extraSelections[(int)SelectionId::IncrementalSearchedKeyword]; + if (!g_config->getHighlightSearchedWord() || !p_cursor.hasSelection()) { + if (!selects.isEmpty()) { + selects.clear(); + highlightExtraSelections(true); + } + + return; + } + + selects.clear(); + QTextEdit::ExtraSelection select; + select.format.setBackground(m_incrementalSearchedWordColor); + select.cursor = p_cursor; + selects.append(select); + + highlightExtraSelections(true); +} + +// Use QPlainTextEdit::find() instead of QTextDocument::find() because the later has +// bugs in searching backward. +bool VEditor::findTextHelper(const QString &p_text, + uint p_options, + bool p_forward, + int p_start, + bool &p_wrapped, + QTextCursor &p_cursor) +{ + p_wrapped = false; + bool found = false; + + // Options + QTextDocument::FindFlags findFlags; + bool caseSensitive = false; + if (p_options & FindOption::CaseSensitive) { + findFlags |= QTextDocument::FindCaseSensitively; + caseSensitive = true; + } + + if (p_options & FindOption::WholeWordOnly) { + findFlags |= QTextDocument::FindWholeWords; + } + + if (!p_forward) { + findFlags |= QTextDocument::FindBackward; + } + + // Use regular expression + bool useRegExp = false; + QRegExp exp; + if (p_options & FindOption::RegularExpression) { + useRegExp = true; + exp = QRegExp(p_text, + caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive); + } + + // Store current state of the cursor. + QTextCursor cursor = textCursorW(); + if (cursor.position() != p_start) { + if (p_start < 0) { + p_start = 0; + } else if (p_start > m_document->characterCount()) { + p_start = m_document->characterCount(); + } + + QTextCursor startCursor = cursor; + startCursor.setPosition(p_start); + setTextCursorW(startCursor); + } + + while (!found) { + if (useRegExp) { + found = findW(exp, findFlags); + } else { + found = findW(p_text, findFlags); + } + + if (p_wrapped) { + break; + } + + if (!found) { + // Wrap to the other end of the document to search again. + p_wrapped = true; + QTextCursor wrapCursor = textCursorW(); + if (p_forward) { + wrapCursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); + } else { + wrapCursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); + } + + setTextCursorW(wrapCursor); + } + } + + if (found) { + p_cursor = textCursorW(); + } + + // Restore the original cursor. + setTextCursorW(cursor); + + return found; +} + +void VEditor::clearIncrementalSearchedWordHighlight(bool p_now) +{ + QList &selects = m_extraSelections[(int)SelectionId::IncrementalSearchedKeyword]; + if (selects.isEmpty()) { + return; + } + + selects.clear(); + highlightExtraSelections(p_now); +} + +void VEditor::clearSearchedWordHighlight() +{ + clearIncrementalSearchedWordHighlight(false); + clearSearchedWordUnderCursorHighlight(false); + + QList &selects = m_extraSelections[(int)SelectionId::SearchedKeyword]; + if (selects.isEmpty()) { + return; + } + + selects.clear(); + highlightExtraSelections(true); +} + +void VEditor::clearSearchedWordUnderCursorHighlight(bool p_now) +{ + QList &selects = m_extraSelections[(int)SelectionId::SearchedKeywordUnderCursor]; + if (selects.isEmpty()) { + return; + } + + selects.clear(); + highlightExtraSelections(p_now); +} + +void VEditor::showWrapLabel() +{ + int labelW = m_wrapLabel->width(); + int labelH = m_wrapLabel->height(); + int x = (m_editor->width() - labelW) / 2; + int y = (m_editor->height() - labelH) / 2; + if (x < 0) { + x = 0; + } + + if (y < 0) { + y = 0; + } + + m_wrapLabel->move(x, y); + m_wrapLabel->show(); + m_labelTimer->stop(); + m_labelTimer->start(); +} + +void VEditor::highlightSearchedWord(const QString &p_text, uint p_options) +{ + QList &selects = m_extraSelections[(int)SelectionId::SearchedKeyword]; + if (!g_config->getHighlightSearchedWord() || p_text.isEmpty()) { + if (!selects.isEmpty()) { + selects.clear(); + highlightExtraSelections(true); + } + + return; + } + + QTextCharFormat format; + format.setBackground(m_searchedWordColor); + highlightTextAll(p_text, p_options, SelectionId::SearchedKeyword, format); +} + +void VEditor::highlightSearchedWordUnderCursor(const QTextCursor &p_cursor) +{ + QList &selects = m_extraSelections[(int)SelectionId::SearchedKeywordUnderCursor]; + if (!p_cursor.hasSelection()) { + if (!selects.isEmpty()) { + selects.clear(); + highlightExtraSelections(true); + } + + return; + } + + selects.clear(); + QTextEdit::ExtraSelection select; + select.format.setBackground(m_searchedWordCursorColor); + select.cursor = p_cursor; + selects.append(select); + + highlightExtraSelections(true); +} + +void VEditor::replaceText(const QString &p_text, + uint p_options, + const QString &p_replaceText, + bool p_findNext) +{ + QTextCursor cursor = textCursorW(); + bool wrapped = false; + QTextCursor retCursor; + bool found = findTextHelper(p_text, + p_options, true, + cursor.position(), + wrapped, + retCursor); + if (found) { + if (retCursor.selectionStart() == cursor.position()) { + // Matched. + retCursor.beginEditBlock(); + retCursor.insertText(p_replaceText); + retCursor.endEditBlock(); + setTextCursorW(retCursor); + } + + if (p_findNext) { + findText(p_text, p_options, true); + } + } +} + +void VEditor::replaceTextAll(const QString &p_text, + uint p_options, + const QString &p_replaceText) +{ + // Replace from the start to the end and restore the cursor. + QTextCursor cursor = textCursorW(); + int nrReplaces = 0; + QTextCursor tmpCursor = cursor; + tmpCursor.setPosition(0); + setTextCursorW(tmpCursor); + int start = tmpCursor.position(); + while (true) { + bool wrapped = false; + QTextCursor retCursor; + bool found = findTextHelper(p_text, + p_options, + true, + start, + wrapped, + retCursor); + if (!found) { + break; + } else { + if (wrapped) { + // Wrap back. + break; + } + + nrReplaces++; + retCursor.beginEditBlock(); + retCursor.insertText(p_replaceText); + retCursor.endEditBlock(); + setTextCursorW(retCursor); + start = retCursor.position(); + } + } + + // Restore cursor position. + cursor.clearSelection(); + setTextCursorW(cursor); + qDebug() << "replace all" << nrReplaces << "occurences"; + + emit m_object->statusMessage(QObject::tr("Replace %1 %2").arg(nrReplaces) + .arg(nrReplaces > 1 ? QObject::tr("occurences") + : QObject::tr("occurence"))); +} + +void VEditor::evaluateMagicWords() +{ + QString text; + QTextCursor cursor = textCursorW(); + if (!cursor.hasSelection()) { + // Get the WORD in current cursor. + int start, end; + VEditUtils::findCurrentWORD(cursor, start, end); + + if (start == end) { + return; + } else { + cursor.setPosition(start); + cursor.setPosition(end, QTextCursor::KeepAnchor); + } + } + + text = VEditUtils::selectedText(cursor); + Q_ASSERT(!text.isEmpty()); + QString evaText = g_mwMgr->evaluate(text); + if (text != evaText) { + qDebug() << "evaluateMagicWords" << text << evaText; + + cursor.insertText(evaText); + + if (m_editOps) { + m_editOps->setVimMode(VimMode::Insert); + } + + setTextCursorW(cursor); + } +} + +void VEditor::setReadOnlyAndHighlightCurrentLine(bool p_readonly) +{ + setReadOnlyW(p_readonly); + highlightCurrentLine(); +} + +bool VEditor::handleMousePressEvent(QMouseEvent *p_event) +{ + if (p_event->button() == Qt::LeftButton + && p_event->modifiers() == Qt::ControlModifier + && !textCursorW().hasSelection()) { + m_oriMouseX = p_event->x(); + m_oriMouseY = p_event->y(); + m_readyToScroll = true; + m_mouseMoveScrolled = false; + p_event->accept(); + return true; + } + + m_readyToScroll = false; + m_mouseMoveScrolled = false; + + return false; +} + +bool VEditor::handleMouseReleaseEvent(QMouseEvent *p_event) +{ + if (m_mouseMoveScrolled || m_readyToScroll) { + viewportW()->setCursor(Qt::IBeamCursor); + m_readyToScroll = false; + m_mouseMoveScrolled = false; + p_event->accept(); + return true; + } + + m_readyToScroll = false; + m_mouseMoveScrolled = false; + + return false; +} + +bool VEditor::handleMouseMoveEvent(QMouseEvent *p_event) +{ + const int threshold = 5; + + if (m_readyToScroll) { + int deltaX = p_event->x() - m_oriMouseX; + int deltaY = p_event->y() - m_oriMouseY; + + if (qAbs(deltaX) >= threshold || qAbs(deltaY) >= threshold) { + m_oriMouseX = p_event->x(); + m_oriMouseY = p_event->y(); + + if (!m_mouseMoveScrolled) { + m_mouseMoveScrolled = true; + viewportW()->setCursor(Qt::SizeAllCursor); + } + + QScrollBar *verBar = verticalScrollBarW(); + QScrollBar *horBar = horizontalScrollBarW(); + if (verBar->isVisible()) { + verBar->setValue(verBar->value() - deltaY); + } + + if (horBar->isVisible()) { + horBar->setValue(horBar->value() - deltaX); + } + } + + p_event->accept(); + return true; + } + + return false; +} + +void VEditor::requestUpdateVimStatus() +{ + if (m_editOps) { + m_editOps->requestUpdateVimStatus(); + } else { + emit m_object->vimStatusUpdated(NULL); + } +} + +bool VEditor::handleInputMethodQuery(Qt::InputMethodQuery p_query, + QVariant &p_var) const +{ + if (p_query == Qt::ImEnabled) { + p_var = m_enableInputMethod; + return true; + } + + return false; +} + +void VEditor::setInputMethodEnabled(bool p_enabled) +{ + if (m_enableInputMethod != p_enabled) { + m_enableInputMethod = p_enabled; + + QInputMethod *im = QGuiApplication::inputMethod(); + im->reset(); + + // Ask input method to query current state, which will call inputMethodQuery(). + im->update(Qt::ImEnabled); + } +} + +void VEditor::decorateText(TextDecoration p_decoration) +{ + if (m_editOps) { + m_editOps->decorateText(p_decoration); + } +} + +void VEditor::updateConfig() +{ + updateEditConfig(); +} diff --git a/src/veditor.h b/src/veditor.h new file mode 100644 index 00000000..4c9ab8ab --- /dev/null +++ b/src/veditor.h @@ -0,0 +1,372 @@ +#ifndef VEDITOR_H +#define VEDITOR_H + +#include +#include +#include +#include +#include + +#include "veditconfig.h" +#include "vfile.h" + +class QWidget; +class VEditorObject; +class VEditOperations; +class QTimer; +class QLabel; +class VVim; + + +enum class SelectionId { + CurrentLine = 0, + SelectedWord, + SearchedKeyword, + SearchedKeywordUnderCursor, + IncrementalSearchedKeyword, + TrailingSapce, + MaxSelection +}; + + +// Abstract class for an edit. +// Should inherit this class as well as QPlainTextEdit or QTextEdit. +// Will replace VEdit eventually. +class VEditor +{ +public: + explicit VEditor(VFile *p_file, QWidget *p_editor); + + virtual ~VEditor(); + + void highlightCurrentLine(); + + virtual void beginEdit() = 0; + + virtual void endEdit() = 0; + + // Save buffer content to VFile. + virtual void saveFile() = 0; + + virtual void reloadFile() = 0; + + virtual bool scrollToBlock(int p_blockNumber) = 0; + + bool isModified() const; + + void setModified(bool p_modified); + + // User requests to insert an image. + void insertImage(); + + // User requests to insert a link. + void insertLink(); + + // Used for incremental search. + // User has enter the content to search, but does not enter the "find" button yet. + bool peekText(const QString &p_text, uint p_options, bool p_forward = true); + + // If @p_cursor is not null, set the position of @p_cursor instead of current + // cursor. + bool findText(const QString &p_text, + uint p_options, + bool p_forward, + QTextCursor *p_cursor = nullptr, + QTextCursor::MoveMode p_moveMode = QTextCursor::MoveAnchor); + + void replaceText(const QString &p_text, + uint p_options, + const QString &p_replaceText, + bool p_findNext); + + void replaceTextAll(const QString &p_text, + uint p_options, + const QString &p_replaceText); + + // Scroll the content to make @p_block visible. + // If the @p_block is too long to hold in one page, just let it occupy the + // whole page. + // Will not change current cursor. + virtual void makeBlockVisible(const QTextBlock &p_block) = 0; + + // Clear IncrementalSearchedKeyword highlight. + void clearIncrementalSearchedWordHighlight(bool p_now = true); + + // Clear SearchedKeyword highlight. + void clearSearchedWordHighlight(); + + // Clear SearchedKeywordUnderCursor Highlight. + void clearSearchedWordUnderCursorHighlight(bool p_now = true); + + // Evaluate selected text or cursor word as magic words. + void evaluateMagicWords(); + + VFile *getFile() const; + + VEditConfig &getConfig(); + + // Request to update Vim status. + void requestUpdateVimStatus(); + + // Jump to a title. + // @p_forward: jump forward or backward. + // @p_relativeLevel: 0 for the same level as current header; + // negative value for upper level; + // positive value is ignored. + // Returns true if the jump succeeded. + virtual bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) = 0; + + void setInputMethodEnabled(bool p_enabled); + + // Insert decoration markers or decorate selected text. + void decorateText(TextDecoration p_decoration); + + virtual bool isBlockVisible(const QTextBlock &p_block) = 0; + + VEditorObject *object() const; + + QWidget *getEditor() const; + + // Scroll block @p_blockNum into the visual window. + // @p_dest is the position of the window: 0 for top, 1 for center, 2 for bottom. + // @p_blockNum is based on 0. + // Will set the cursor to the block. + virtual void scrollBlockInPage(int p_blockNum, int p_dest) = 0; + + // Update config according to global configurations. + virtual void updateConfig(); + +// Wrapper functions for QPlainTextEdit/QTextEdit. +// Ends with W to distinguish it from the original interfaces. +public: + virtual void setExtraSelectionsW(const QList &p_selections) = 0; + + virtual QTextDocument *documentW() const = 0; + + virtual void setTabStopWidthW(int p_width) = 0; + + virtual QTextCursor textCursorW() const = 0; + + virtual void setTextCursorW(const QTextCursor &p_cursor) = 0; + + virtual void moveCursorW(QTextCursor::MoveOperation p_operation, + QTextCursor::MoveMode p_mode = QTextCursor::MoveAnchor) = 0; + + virtual QScrollBar *verticalScrollBarW() const = 0; + + virtual QScrollBar *horizontalScrollBarW() const = 0; + + virtual bool findW(const QString &p_exp, + QTextDocument::FindFlags p_options = QTextDocument::FindFlags()) = 0; + + virtual bool findW(const QRegExp &p_exp, + QTextDocument::FindFlags p_options = QTextDocument::FindFlags()) = 0; + + virtual void setReadOnlyW(bool p_ro) = 0; + + virtual QWidget *viewportW() const = 0; + + virtual void insertPlainTextW(const QString &p_text) = 0; + + virtual void undoW() = 0; + + virtual void redoW() = 0; + +protected: + void init(); + + virtual void updateFontAndPalette() = 0; + + // Update m_config according to VConfigManager. + void updateEditConfig(); + + // Do some highlight on cursor position changed. + void highlightOnCursorPositionChanged(); + + // Highlight selected text. + void highlightSelectedWord(); + + bool wordInSearchedSelection(const QString &p_text); + + // Set read-only property and highlight current line. + void setReadOnlyAndHighlightCurrentLine(bool p_readonly); + + // Handle the mouse press event of m_editor. + // Returns true if no further process is needed. + bool handleMousePressEvent(QMouseEvent *p_event); + + bool handleMouseReleaseEvent(QMouseEvent *p_event); + + bool handleMouseMoveEvent(QMouseEvent *p_event); + + bool handleInputMethodQuery(Qt::InputMethodQuery p_query, + QVariant &p_var) const; + + QWidget *m_editor; + + VEditorObject *m_object; + + QPointer m_file; + + VEditOperations *m_editOps; + + VEditConfig m_config; + +private: + friend class VEditorObject; + + void highlightTrailingSpace(); + + // Trigger the timer to request highlight. + // If @p_now is true, stop the timer and highlight immediately. + void highlightExtraSelections(bool p_now = false); + + // @p_fileter: a function to filter out highlight results. + void highlightTextAll(const QString &p_text, + uint p_options, + SelectionId p_id, + QTextCharFormat p_format, + void (*p_filter)(VEditor *, + QList &) = NULL); + + // Find all the occurences of @p_text. + QList findTextAll(const QString &p_text, uint p_options); + + // Highlight @p_cursor as the incremental searched keyword. + void highlightIncrementalSearchedWord(const QTextCursor &p_cursor); + + // Find @p_text in the document starting from @p_start. + // Returns true if @p_text is found and set @p_cursor to indicate + // the position. + // Will NOT change current cursor. + bool findTextHelper(const QString &p_text, + uint p_options, + bool p_forward, + int p_start, + bool &p_wrapped, + QTextCursor &p_cursor); + + void showWrapLabel(); + + void highlightSearchedWord(const QString &p_text, uint p_options); + + // Highlight @p_cursor as the searched keyword under cursor. + void highlightSearchedWordUnderCursor(const QTextCursor &p_cursor); + + QLabel *m_wrapLabel; + QTimer *m_labelTimer; + + QTextDocument *m_document; + + // doHighlightExtraSelections() will highlight these selections. + // Selections are indexed by SelectionId. + QVector > m_extraSelections; + + QColor m_selectedWordColor; + QColor m_searchedWordColor; + QColor m_searchedWordCursorColor; + QColor m_incrementalSearchedWordColor; + QColor m_trailingSpaceColor; + + // Timer for extra selections highlight. + QTimer *m_highlightTimer; + + bool m_readyToScroll; + bool m_mouseMoveScrolled; + int m_oriMouseX; + int m_oriMouseY; + + // Whether enable input method. + bool m_enableInputMethod; + +// Functions for private slots. +private: + void labelTimerTimeout(); + + // Do the real work to highlight extra selections. + void doHighlightExtraSelections(); +}; + + +// Since one class could not inherit QObject multiple times, we use this class +// for VEditor to signal/slot. +class VEditorObject : public QObject +{ + Q_OBJECT +public: + explicit VEditorObject(VEditor *p_editor, QObject *p_parent = nullptr) + : QObject(p_parent), m_editor(p_editor) + { + } + +signals: + // Emit when editor config has been updated. + void configUpdated(); + + // Emit when want to show message in status bar. + void statusMessage(const QString &p_msg); + + // Request VEditTab to save and exit edit mode. + void saveAndRead(); + + // Request VEditTab to discard and exit edit mode. + void discardAndRead(); + + // Request VEditTab to edit current note. + void editNote(); + + // Request VEditTab to save this file. + void saveNote(); + + // Selection changed by mouse. + void selectionChangedByMouse(bool p_hasSelection); + + // Emit when Vim status updated. + void vimStatusUpdated(const VVim *p_vim); + + // Emit when all initialization is ready. + void ready(); + + // Request the edit tab to close find and replace dialog. + void requestCloseFindReplaceDialog(); + +private slots: + // Timer for find-wrap label. + void labelTimerTimeout() + { + m_editor->labelTimerTimeout(); + } + + // Do the real work to highlight extra selections. + void doHighlightExtraSelections() + { + m_editor->doHighlightExtraSelections(); + } + +private: + friend class VEditor; + + VEditor *m_editor; +}; + +inline VFile *VEditor::getFile() const +{ + return m_file; +} + +inline VEditConfig &VEditor::getConfig() +{ + return m_config; +} + +inline VEditorObject *VEditor::object() const +{ + return m_object; +} + +inline QWidget *VEditor::getEditor() const +{ + return m_editor; +} + +#endif // VEDITOR_H diff --git a/src/vfilelist.cpp b/src/vfilelist.cpp index 54621ab1..fed5d1ed 100644 --- a/src/vfilelist.cpp +++ b/src/vfilelist.cpp @@ -10,7 +10,7 @@ #include "utils/vutils.h" #include "vnotefile.h" #include "vconfigmanager.h" -#include "vmdedit.h" +#include "vmdeditor.h" #include "vmdtab.h" #include "dialog/vconfirmdeletiondialog.h" #include "dialog/vsortdialog.h" @@ -376,7 +376,7 @@ void VFileList::newFile() if (contentInserted) { const VMdTab *tab = dynamic_cast(editArea->getCurrentTab()); if (tab) { - VMdEdit *edit = dynamic_cast(tab->getEditor()); + VMdEditor *edit = tab->getEditor(); if (edit && edit->getFile() == file) { QTextCursor cursor = edit->textCursor(); cursor.movePosition(QTextCursor::End); diff --git a/src/vimagepreviewer.h b/src/vimagepreviewer.h index d99dce5d..81326c17 100644 --- a/src/vimagepreviewer.h +++ b/src/vimagepreviewer.h @@ -84,7 +84,7 @@ private: int m_endPos; QString m_linkUrl; - // Whether it is a image block. + // Whether it is an image block. bool m_isBlock; // The previewed image ID if this link has been previewed. diff --git a/src/vimageresourcemanager.cpp b/src/vimageresourcemanager.cpp index 1f3673fc..b4eeb6f3 100644 --- a/src/vimageresourcemanager.cpp +++ b/src/vimageresourcemanager.cpp @@ -44,7 +44,7 @@ void VImageResourceManager::updateBlockInfos(const QVector &p_b // Clear unused images. for (auto it = m_images.begin(); it != m_images.end();) { - if (!m_images.contains(it.key())) { + if (!usedImages.contains(it.key())) { // Remove the image. it = m_images.erase(it); } else { diff --git a/src/vlinenumberarea.h b/src/vlinenumberarea.h index 8d0bf18c..014151b4 100644 --- a/src/vlinenumberarea.h +++ b/src/vlinenumberarea.h @@ -13,7 +13,8 @@ enum class LineNumberType None = 0, Absolute, Relative, - CodeBlock + CodeBlock, + Invalid }; diff --git a/src/vmainwindow.cpp b/src/vmainwindow.cpp index 225598dd..966aa4c2 100644 --- a/src/vmainwindow.cpp +++ b/src/vmainwindow.cpp @@ -1630,6 +1630,7 @@ void VMainWindow::initEditorLineNumberMenu(QMenu *p_menu) } g_config->setEditorLineNumber(p_action->data().toInt()); + emit editorConfigUpdated(); }); int lineNumberMode = g_config->getEditorLineNumber(); @@ -2280,6 +2281,8 @@ void VMainWindow::enableImagePreview(bool p_checked) void VMainWindow::enableImagePreviewConstraint(bool p_checked) { g_config->setEnablePreviewImageConstraint(p_checked); + + emit editorConfigUpdated(); } void VMainWindow::enableImageConstraint(bool p_checked) diff --git a/src/vmainwindow.h b/src/vmainwindow.h index 799eb0a6..7e3274e1 100644 --- a/src/vmainwindow.h +++ b/src/vmainwindow.h @@ -86,6 +86,10 @@ public: // Prompt user for new notebook if there is no notebook. void promptNewNotebookIfEmpty(); +signals: + // Emit when editor related configurations were changed by user. + void editorConfigUpdated(); + private slots: void importNoteFromFile(); void viewSettings(); diff --git a/src/vmdedit.cpp b/src/vmdedit.cpp index 3ac6067e..647fa68e 100644 --- a/src/vmdedit.cpp +++ b/src/vmdedit.cpp @@ -66,6 +66,8 @@ VMdEdit::VMdEdit(VFile *p_file, VDocument *p_vdoc, MarkdownConverterType p_type, } }); + // Comment out these lines since we use VMdEditor to replace VMdEdit. + /* m_editOps = new VMdEditOperations(this, m_file); connect(m_editOps, &VEditOperations::statusMessage, @@ -78,6 +80,7 @@ VMdEdit::VMdEdit(VFile *p_file, VDocument *p_vdoc, MarkdownConverterType p_type, connect(QApplication::clipboard(), &QClipboard::changed, this, &VMdEdit::handleClipboardChanged); + */ updateFontAndPalette(); diff --git a/src/vmdeditoperations.cpp b/src/vmdeditoperations.cpp index c3115ab5..acf261a2 100644 --- a/src/vmdeditoperations.cpp +++ b/src/vmdeditoperations.cpp @@ -16,10 +16,10 @@ #include "dialog/vinsertimagedialog.h" #include "dialog/vselectdialog.h" #include "utils/vutils.h" -#include "vedit.h" +#include "veditor.h" #include "vdownloader.h" #include "vfile.h" -#include "vmdedit.h" +#include "vmdeditor.h" #include "vconfigmanager.h" #include "utils/vvim.h" #include "utils/veditutils.h" @@ -28,7 +28,7 @@ extern VConfigManager *g_config; const QString VMdEditOperations::c_defaultImageTitle = ""; -VMdEditOperations::VMdEditOperations(VEdit *p_editor, VFile *p_file) +VMdEditOperations::VMdEditOperations(VEditor *p_editor, VFile *p_file) : VEditOperations(p_editor, p_file), m_autoIndentPos(-1) { } @@ -40,7 +40,9 @@ bool VMdEditOperations::insertImageFromMimeData(const QMimeData *source) return false; } VInsertImageDialog dialog(tr("Insert Image From Clipboard"), - c_defaultImageTitle, "", (QWidget *)m_editor); + c_defaultImageTitle, + "", + m_editor->getEditor()); dialog.setBrowseable(false); dialog.setImage(image); if (dialog.exec() == QDialog::Accepted) { @@ -78,7 +80,7 @@ void VMdEditOperations::insertImageFromQImage(const QString &title, const QStrin errStr, QMessageBox::Ok, QMessageBox::Ok, - (QWidget *)m_editor); + m_editor->getEditor()); return; } @@ -87,7 +89,7 @@ void VMdEditOperations::insertImageFromQImage(const QString &title, const QStrin qDebug() << "insert image" << title << filePath; - VMdEdit *mdEditor = dynamic_cast(m_editor); + VMdEditor *mdEditor = dynamic_cast(m_editor); Q_ASSERT(mdEditor); mdEditor->imageInserted(filePath); } @@ -118,7 +120,7 @@ void VMdEditOperations::insertImageFromPath(const QString &title, const QString errStr, QMessageBox::Ok, QMessageBox::Ok, - (QWidget *)m_editor); + m_editor->getEditor()); return; } @@ -127,7 +129,7 @@ void VMdEditOperations::insertImageFromPath(const QString &title, const QString qDebug() << "insert image" << title << filePath; - VMdEdit *mdEditor = dynamic_cast(m_editor); + VMdEditor *mdEditor = dynamic_cast(m_editor); Q_ASSERT(mdEditor); mdEditor->imageInserted(filePath); } @@ -156,7 +158,7 @@ bool VMdEditOperations::insertImageFromURL(const QUrl &imageUrl) VInsertImageDialog dialog(title, c_defaultImageTitle, - imagePath, (QWidget *)m_editor); + imagePath, m_editor->getEditor()); dialog.setBrowseable(false, true); if (isLocal) { dialog.setImage(image); @@ -186,7 +188,7 @@ bool VMdEditOperations::insertImageFromURL(const QUrl &imageUrl) bool VMdEditOperations::insertImage() { VInsertImageDialog dialog(tr("Insert Image From File"), - c_defaultImageTitle, "", (QWidget *)m_editor); + c_defaultImageTitle, "", m_editor->getEditor()); if (dialog.exec() == QDialog::Accepted) { QString title = dialog.getImageTitleInput(); QString imagePath = dialog.getPathInput(); @@ -393,10 +395,10 @@ bool VMdEditOperations::handleKeyBracketLeft(QKeyEvent *p_event) // 1. If there is any selection, clear it. // 2. Otherwise, ignore this event and let parent handles it. if (p_event->modifiers() == Qt::ControlModifier) { - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); if (cursor.hasSelection()) { cursor.clearSelection(); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); p_event->accept(); return true; } @@ -407,18 +409,18 @@ bool VMdEditOperations::handleKeyBracketLeft(QKeyEvent *p_event) bool VMdEditOperations::handleKeyTab(QKeyEvent *p_event) { - QTextDocument *doc = m_editor->document(); + QTextDocument *doc = m_editor->documentW(); QString text(m_editConfig->m_tabSpaces); if (p_event->modifiers() == Qt::NoModifier) { - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); if (cursor.hasSelection()) { m_autoIndentPos = -1; cursor.beginEditBlock(); // Indent each selected line. VEditUtils::indentSelectedBlocks(doc, cursor, text, true); cursor.endEditBlock(); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); } else { // If it is a Tab key following auto list, increase the indent level. QTextBlock block = cursor.block(); @@ -433,7 +435,7 @@ bool VMdEditOperations::handleKeyTab(QKeyEvent *p_event) } blockCursor.endEditBlock(); // Change m_autoIndentPos to let it can be repeated. - m_autoIndentPos = m_editor->textCursor().position(); + m_autoIndentPos = m_editor->textCursorW().position(); } else { // Just insert "tab". insertTextAtCurPos(text); @@ -454,8 +456,8 @@ bool VMdEditOperations::handleKeyBackTab(QKeyEvent *p_event) m_autoIndentPos = -1; return false; } - QTextDocument *doc = m_editor->document(); - QTextCursor cursor = m_editor->textCursor(); + QTextDocument *doc = m_editor->documentW(); + QTextCursor cursor = m_editor->textCursorW(); QTextBlock block = doc->findBlock(cursor.selectionStart()); bool continueAutoIndent = false; int seq = -1; @@ -474,7 +476,7 @@ bool VMdEditOperations::handleKeyBackTab(QKeyEvent *p_event) cursor.endEditBlock(); if (continueAutoIndent) { - m_autoIndentPos = m_editor->textCursor().position(); + m_autoIndentPos = m_editor->textCursorW().position(); } else { m_autoIndentPos = -1; } @@ -486,7 +488,7 @@ bool VMdEditOperations::handleKeyH(QKeyEvent *p_event) { if (p_event->modifiers() == Qt::ControlModifier) { // Ctrl+H, equal to backspace. - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); cursor.deletePreviousChar(); p_event->accept(); @@ -499,7 +501,7 @@ bool VMdEditOperations::handleKeyU(QKeyEvent *p_event) { if (p_event->modifiers() == Qt::ControlModifier) { // Ctrl+U, delete till the start of line. - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); bool ret; if (cursor.atBlockStart()) { ret = cursor.movePosition(QTextCursor::PreviousWord, QTextCursor::KeepAnchor); @@ -520,7 +522,7 @@ bool VMdEditOperations::handleKeyW(QKeyEvent *p_event) { if (p_event->modifiers() == Qt::ControlModifier) { // Ctrl+W, delete till the start of previous word. - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); if (cursor.hasSelection()) { cursor.removeSelectedText(); } else { @@ -539,10 +541,10 @@ bool VMdEditOperations::handleKeyEsc(QKeyEvent *p_event) { // 1. If there is any selection, clear it. // 2. Otherwise, ignore this event and let parent handles it. - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); if (cursor.hasSelection()) { cursor.clearSelection(); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); p_event->accept(); return true; } @@ -560,7 +562,7 @@ bool VMdEditOperations::handleKeyReturn(QKeyEvent *p_event) // Insert two spaces and a new line. m_autoIndentPos = -1; - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); cursor.beginEditBlock(); cursor.removeSelectedText(); cursor.insertText(" "); @@ -575,11 +577,11 @@ bool VMdEditOperations::handleKeyReturn(QKeyEvent *p_event) if (m_autoIndentPos > -1) { // Cancel the auto indent/list if the pos is the same and cursor is at // the end of a block. - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); if (VEditUtils::needToCancelAutoIndent(m_autoIndentPos, cursor)) { m_autoIndentPos = -1; VEditUtils::deleteIndentAndListMark(cursor); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); return true; } } @@ -589,7 +591,7 @@ bool VMdEditOperations::handleKeyReturn(QKeyEvent *p_event) if (g_config->getAutoIndent()) { handled = true; - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); bool textInserted = false; cursor.beginEditBlock(); cursor.removeSelectedText(); @@ -603,9 +605,9 @@ bool VMdEditOperations::handleKeyReturn(QKeyEvent *p_event) } cursor.endEditBlock(); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); if (textInserted) { - m_autoIndentPos = m_editor->textCursor().position(); + m_autoIndentPos = m_editor->textCursorW().position(); } } @@ -648,8 +650,8 @@ void VMdEditOperations::changeListBlockSeqNumber(QTextBlock &p_block, int p_seq) bool VMdEditOperations::insertTitle(int p_level) { - QTextDocument *doc = m_editor->document(); - QTextCursor cursor = m_editor->textCursor(); + QTextDocument *doc = m_editor->documentW(); + QTextCursor cursor = m_editor->textCursorW(); int firstBlock = cursor.block().blockNumber(); int lastBlock = firstBlock; @@ -667,7 +669,7 @@ bool VMdEditOperations::insertTitle(int p_level) } cursor.endEditBlock(); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); return true; } @@ -713,7 +715,7 @@ void VMdEditOperations::decorateText(TextDecoration p_decoration) void VMdEditOperations::decorateBold() { - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); cursor.beginEditBlock(); if (cursor.hasSelection()) { // Insert ** around the selected text. @@ -745,12 +747,12 @@ void VMdEditOperations::decorateBold() } cursor.endEditBlock(); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); } void VMdEditOperations::decorateItalic() { - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); cursor.beginEditBlock(); if (cursor.hasSelection()) { // Insert * around the selected text. @@ -782,12 +784,12 @@ void VMdEditOperations::decorateItalic() } cursor.endEditBlock(); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); } void VMdEditOperations::decorateInlineCode() { - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); cursor.beginEditBlock(); if (cursor.hasSelection()) { // Insert ` around the selected text. @@ -819,14 +821,14 @@ void VMdEditOperations::decorateInlineCode() } cursor.endEditBlock(); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); } void VMdEditOperations::decorateCodeBlock() { const QString marker("```"); - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); cursor.beginEditBlock(); if (cursor.hasSelection()) { // Insert ``` around the selected text. @@ -899,12 +901,12 @@ void VMdEditOperations::decorateCodeBlock() } cursor.endEditBlock(); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); } void VMdEditOperations::decorateStrikethrough() { - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); cursor.beginEditBlock(); if (cursor.hasSelection()) { // Insert ~~ around the selected text. @@ -936,16 +938,16 @@ void VMdEditOperations::decorateStrikethrough() } cursor.endEditBlock(); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); } bool VMdEditOperations::insertLink(const QString &p_linkText, const QString &p_linkUrl) { QString link = QString("[%1](%2)").arg(p_linkText).arg(p_linkUrl); - QTextCursor cursor = m_editor->textCursor(); + QTextCursor cursor = m_editor->textCursorW(); cursor.insertText(link); - m_editor->setTextCursor(cursor); + m_editor->setTextCursorW(cursor); setVimMode(VimMode::Insert); diff --git a/src/vmdeditoperations.h b/src/vmdeditoperations.h index 50333e5b..ae88c360 100644 --- a/src/vmdeditoperations.h +++ b/src/vmdeditoperations.h @@ -15,7 +15,7 @@ class VMdEditOperations : public VEditOperations { Q_OBJECT public: - VMdEditOperations(VEdit *p_editor, VFile *p_file); + VMdEditOperations(VEditor *p_editor, VFile *p_file); bool insertImageFromMimeData(const QMimeData *source) Q_DECL_OVERRIDE; diff --git a/src/vmdeditor.cpp b/src/vmdeditor.cpp new file mode 100644 index 00000000..90881000 --- /dev/null +++ b/src/vmdeditor.cpp @@ -0,0 +1,862 @@ +#include "vmdeditor.h" + +#include +#include +#include + +#include "vdocument.h" +#include "utils/veditutils.h" +#include "vedittab.h" +#include "hgmarkdownhighlighter.h" +#include "vcodeblockhighlighthelper.h" +#include "vmdeditoperations.h" +#include "vtableofcontent.h" +#include "utils/veditutils.h" +#include "dialog/vselectdialog.h" +#include "dialog/vconfirmdeletiondialog.h" +#include "vtextblockdata.h" +#include "vorphanfile.h" +#include "vnotefile.h" +#include "vpreviewmanager.h" + +extern VConfigManager *g_config; + +VMdEditor::VMdEditor(VFile *p_file, + VDocument *p_doc, + MarkdownConverterType p_type, + QWidget *p_parent) + : VPlainTextEdit(p_parent), + VEditor(p_file, this), + m_mdHighlighter(NULL), + m_freshEdit(true) +{ + Q_ASSERT(p_file->getDocType() == DocType::Markdown); + + VEditor::init(); + + // Hook functions from VEditor. + connect(this, &VPlainTextEdit::cursorPositionChanged, + this, [this]() { + highlightOnCursorPositionChanged(); + }); + + connect(this, &VPlainTextEdit::selectionChanged, + this, [this]() { + highlightSelectedWord(); + }); + // End. + + m_mdHighlighter = new HGMarkdownHighlighter(g_config->getMdHighlightingStyles(), + g_config->getCodeBlockStyles(), + g_config->getMarkdownHighlightInterval(), + document()); + + connect(m_mdHighlighter, &HGMarkdownHighlighter::headersUpdated, + this, &VMdEditor::updateHeaders); + + // After highlight, the cursor may trun into non-visible. We should make it visible + // in this case. + connect(m_mdHighlighter, &HGMarkdownHighlighter::highlightCompleted, + this, [this]() { + makeBlockVisible(textCursor().block()); + }); + + m_cbHighlighter = new VCodeBlockHighlightHelper(m_mdHighlighter, + p_doc, + p_type); + + m_previewMgr = new VPreviewManager(this); + connect(m_mdHighlighter, &HGMarkdownHighlighter::imageLinksUpdated, + m_previewMgr, &VPreviewManager::imageLinksUpdated); + connect(m_previewMgr, &VPreviewManager::requestUpdateImageLinks, + m_mdHighlighter, &HGMarkdownHighlighter::updateHighlight); + + m_editOps = new VMdEditOperations(this, m_file); + connect(m_editOps, &VEditOperations::statusMessage, + m_object, &VEditorObject::statusMessage); + connect(m_editOps, &VEditOperations::vimStatusUpdated, + m_object, &VEditorObject::vimStatusUpdated); + + connect(this, &VPlainTextEdit::cursorPositionChanged, + this, &VMdEditor::updateCurrentHeader); + + updateFontAndPalette(); + + updateConfig(); +} + +void VMdEditor::updateFontAndPalette() +{ + setFont(g_config->getMdEditFont()); + setPalette(g_config->getMdEditPalette()); +} + +void VMdEditor::beginEdit() +{ + updateFontAndPalette(); + + updateConfig(); + + initInitImages(); + + setModified(false); + + setReadOnlyAndHighlightCurrentLine(false); + + emit statusChanged(); + + updateHeaders(m_mdHighlighter->getHeaderRegions()); + + if (m_freshEdit) { + m_freshEdit = false; + emit m_object->ready(); + } +} + +void VMdEditor::endEdit() +{ + setReadOnlyAndHighlightCurrentLine(true); + clearUnusedImages(); +} + +void VMdEditor::saveFile() +{ + Q_ASSERT(m_file->isModifiable()); + + if (!document()->isModified()) { + return; + } + + m_file->setContent(toPlainText()); + setModified(false); +} + +void VMdEditor::reloadFile() +{ + const QString &content = m_file->getContent(); + setPlainText(content); + + setModified(false); +} + +bool VMdEditor::scrollToBlock(int p_blockNumber) +{ + QTextBlock block = document()->findBlockByNumber(p_blockNumber); + if (block.isValid()) { + VEditUtils::scrollBlockInPage(this, block.blockNumber(), 0); + moveCursor(QTextCursor::EndOfBlock); + return true; + } + + return false; +} + +// Get the visual offset of a block. +#define GETVISUALOFFSETY ((int)(contentOffset().y() + rect.y())) + +void VMdEditor::makeBlockVisible(const QTextBlock &p_block) +{ + if (!p_block.isValid() || !p_block.isVisible()) { + return; + } + + QScrollBar *vbar = verticalScrollBar(); + if (!vbar || !vbar->isVisible()) { + // No vertical scrollbar. No need to scroll. + return; + } + + int height = rect().height(); + QScrollBar *hbar = horizontalScrollBar(); + if (hbar && hbar->isVisible()) { + height -= hbar->height(); + } + + bool moved = false; + + QRectF rect = blockBoundingGeometry(p_block); + int y = GETVISUALOFFSETY; + int rectHeight = (int)rect.height(); + + // Handle the case rectHeight >= height. + if (rectHeight >= height) { + if (y <= 0) { + if (y + rectHeight < height) { + // Need to scroll up. + while (y + rectHeight < height && vbar->value() > vbar->minimum()) { + moved = true; + vbar->setValue(vbar->value() - vbar->singleStep()); + rect = blockBoundingGeometry(p_block); + rectHeight = (int)rect.height(); + y = GETVISUALOFFSETY; + } + } + } else { + // Need to scroll down. + while (y > 0 && vbar->value() < vbar->maximum()) { + moved = true; + vbar->setValue(vbar->value() + vbar->singleStep()); + rect = blockBoundingGeometry(p_block); + rectHeight = (int)rect.height(); + y = GETVISUALOFFSETY; + } + } + + if (moved) { + qDebug() << "scroll to make huge block visible"; + } + + return; + } + + while (y < 0 && vbar->value() > vbar->minimum()) { + qDebug() << y << vbar->value() << vbar->minimum() << rectHeight; + moved = true; + vbar->setValue(vbar->value() - vbar->singleStep()); + rect = blockBoundingGeometry(p_block); + rectHeight = (int)rect.height(); + y = GETVISUALOFFSETY; + } + + if (moved) { + qDebug() << "scroll page down to make block visible"; + return; + } + + while (y + rectHeight > height && vbar->value() < vbar->maximum()) { + moved = true; + vbar->setValue(vbar->value() + vbar->singleStep()); + rect = blockBoundingGeometry(p_block); + rectHeight = (int)rect.height(); + y = GETVISUALOFFSETY; + } + + if (moved) { + qDebug() << "scroll page up to make block visible"; + } +} + +void VMdEditor::contextMenuEvent(QContextMenuEvent *p_event) +{ + QMenu *menu = createStandardContextMenu(); + menu->setToolTipsVisible(true); + + const QList actions = menu->actions(); + + if (!textCursor().hasSelection()) { + VEditTab *editTab = dynamic_cast(parent()); + Q_ASSERT(editTab); + if (editTab->isEditMode()) { + QAction *saveExitAct = new QAction(QIcon(":/resources/icons/save_exit.svg"), + tr("&Save Changes And Read"), + menu); + saveExitAct->setToolTip(tr("Save changes and exit edit mode")); + connect(saveExitAct, &QAction::triggered, + this, [this]() { + emit m_object->saveAndRead(); + }); + + QAction *discardExitAct = new QAction(QIcon(":/resources/icons/discard_exit.svg"), + tr("&Discard Changes And Read"), + menu); + discardExitAct->setToolTip(tr("Discard changes and exit edit mode")); + connect(discardExitAct, &QAction::triggered, + this, [this]() { + emit m_object->discardAndRead(); + }); + + menu->insertAction(actions.isEmpty() ? NULL : actions[0], discardExitAct); + menu->insertAction(discardExitAct, saveExitAct); + if (!actions.isEmpty()) { + menu->insertSeparator(actions[0]); + } + } + } + + menu->exec(p_event->globalPos()); + delete menu; +} + +void VMdEditor::mousePressEvent(QMouseEvent *p_event) +{ + if (handleMousePressEvent(p_event)) { + return; + } + + VPlainTextEdit::mousePressEvent(p_event); + + emit m_object->selectionChangedByMouse(textCursor().hasSelection()); +} + +void VMdEditor::mouseReleaseEvent(QMouseEvent *p_event) +{ + if (handleMouseReleaseEvent(p_event)) { + return; + } + + VPlainTextEdit::mousePressEvent(p_event); +} + +void VMdEditor::mouseMoveEvent(QMouseEvent *p_event) +{ + if (handleMouseMoveEvent(p_event)) { + return; + } + + VPlainTextEdit::mouseMoveEvent(p_event); + + emit m_object->selectionChangedByMouse(textCursor().hasSelection()); +} + +QVariant VMdEditor::inputMethodQuery(Qt::InputMethodQuery p_query) const +{ + QVariant ret; + if (handleInputMethodQuery(p_query, ret)) { + return ret; + } + + return VPlainTextEdit::inputMethodQuery(p_query); +} + +bool VMdEditor::isBlockVisible(const QTextBlock &p_block) +{ + if (!p_block.isValid() || !p_block.isVisible()) { + return false; + } + + QScrollBar *vbar = verticalScrollBar(); + if (!vbar || !vbar->isVisible()) { + // No vertical scrollbar. + return true; + } + + int height = rect().height(); + QScrollBar *hbar = horizontalScrollBar(); + if (hbar && hbar->isVisible()) { + height -= hbar->height(); + } + + QRectF rect = blockBoundingGeometry(p_block); + int y = GETVISUALOFFSETY; + int rectHeight = (int)rect.height(); + + return (y >= 0 && y < height) || (y < 0 && y + rectHeight > 0); +} + +static void addHeaderSequence(QVector &p_sequence, int p_level, int p_baseLevel) +{ + Q_ASSERT(p_level >= 1 && p_level < p_sequence.size()); + if (p_level < p_baseLevel) { + p_sequence.fill(0); + return; + } + + ++p_sequence[p_level]; + for (int i = p_level + 1; i < p_sequence.size(); ++i) { + p_sequence[i] = 0; + } +} + +static QString headerSequenceStr(const QVector &p_sequence) +{ + QString res; + for (int i = 1; i < p_sequence.size(); ++i) { + if (p_sequence[i] != 0) { + res = res + QString::number(p_sequence[i]) + '.'; + } else if (res.isEmpty()) { + continue; + } else { + break; + } + } + + return res; +} + +static void insertSequenceToHeader(QTextBlock p_block, + QRegExp &p_reg, + QRegExp &p_preReg, + const QString &p_seq) +{ + if (!p_block.isValid()) { + return; + } + + QString text = p_block.text(); + bool matched = p_reg.exactMatch(text); + Q_ASSERT(matched); + + matched = p_preReg.exactMatch(text); + Q_ASSERT(matched); + + int start = p_reg.cap(1).length() + 1; + int end = p_preReg.cap(1).length(); + + Q_ASSERT(start <= end); + + QTextCursor cursor(p_block); + cursor.setPosition(p_block.position() + start); + if (start != end) { + cursor.setPosition(p_block.position() + end, QTextCursor::KeepAnchor); + } + + if (p_seq.isEmpty()) { + cursor.removeSelectedText(); + } else { + cursor.insertText(p_seq + ' '); + } +} + +void VMdEditor::updateHeaders(const QVector &p_headerRegions) +{ + QTextDocument *doc = document(); + + QVector headers; + QVector headerBlockNumbers; + QVector headerSequences; + if (!p_headerRegions.isEmpty()) { + headers.reserve(p_headerRegions.size()); + headerBlockNumbers.reserve(p_headerRegions.size()); + headerSequences.reserve(p_headerRegions.size()); + } + + // Assume that each block contains only one line + // Only support # syntax for now + QRegExp headerReg(VUtils::c_headerRegExp); + int baseLevel = -1; + for (auto const & reg : p_headerRegions) { + QTextBlock block = doc->findBlock(reg.m_startPos); + if (!block.isValid()) { + continue; + } + + if (!block.contains(reg.m_endPos - 1)) { + continue; + } + + if ((block.userState() == HighlightBlockState::Normal) + && headerReg.exactMatch(block.text())) { + int level = headerReg.cap(1).length(); + VTableOfContentItem header(headerReg.cap(2).trimmed(), + level, + block.blockNumber(), + headers.size()); + headers.append(header); + headerBlockNumbers.append(block.blockNumber()); + headerSequences.append(headerReg.cap(3)); + + if (baseLevel == -1) { + baseLevel = level; + } else if (baseLevel > level) { + baseLevel = level; + } + } + } + + m_headers.clear(); + + bool autoSequence = m_config.m_enableHeadingSequence + && !isReadOnly() + && m_file->isModifiable(); + int headingSequenceBaseLevel = g_config->getHeadingSequenceBaseLevel(); + if (headingSequenceBaseLevel < 1 || headingSequenceBaseLevel > 6) { + headingSequenceBaseLevel = 1; + } + + QVector seqs(7, 0); + QRegExp preReg(VUtils::c_headerPrefixRegExp); + int curLevel = baseLevel - 1; + for (int i = 0; i < headers.size(); ++i) { + VTableOfContentItem &item = headers[i]; + while (item.m_level > curLevel + 1) { + curLevel += 1; + + // Insert empty level which is an invalid header. + m_headers.append(VTableOfContentItem(c_emptyHeaderName, + curLevel, + -1, + m_headers.size())); + if (autoSequence) { + addHeaderSequence(seqs, curLevel, headingSequenceBaseLevel); + } + } + + item.m_index = m_headers.size(); + m_headers.append(item); + curLevel = item.m_level; + if (autoSequence) { + addHeaderSequence(seqs, item.m_level, headingSequenceBaseLevel); + + QString seqStr = headerSequenceStr(seqs); + if (headerSequences[i] != seqStr) { + // Insert correct sequence. + insertSequenceToHeader(doc->findBlockByNumber(headerBlockNumbers[i]), + headerReg, + preReg, + seqStr); + } + } + } + + emit headersChanged(m_headers); + + updateCurrentHeader(); +} + +void VMdEditor::updateCurrentHeader() +{ + emit currentHeaderChanged(textCursor().block().blockNumber()); +} + +void VMdEditor::initInitImages() +{ + m_initImages = VUtils::fetchImagesFromMarkdownFile(m_file, + ImageLink::LocalRelativeInternal); +} + +void VMdEditor::clearUnusedImages() +{ + QVector images = VUtils::fetchImagesFromMarkdownFile(m_file, + ImageLink::LocalRelativeInternal); + + QVector unusedImages; + + if (!m_insertedImages.isEmpty()) { + for (int i = 0; i < m_insertedImages.size(); ++i) { + const ImageLink &link = m_insertedImages[i]; + + if (link.m_type != ImageLink::LocalRelativeInternal) { + continue; + } + + int j; + for (j = 0; j < images.size(); ++j) { + if (VUtils::equalPath(link.m_path, images[j].m_path)) { + break; + } + } + + // This inserted image is no longer in the file. + if (j == images.size()) { + unusedImages.push_back(link.m_path); + } + } + + m_insertedImages.clear(); + } + + for (int i = 0; i < m_initImages.size(); ++i) { + const ImageLink &link = m_initImages[i]; + + V_ASSERT(link.m_type == ImageLink::LocalRelativeInternal); + + int j; + for (j = 0; j < images.size(); ++j) { + if (VUtils::equalPath(link.m_path, images[j].m_path)) { + break; + } + } + + // Original local relative image is no longer in the file. + if (j == images.size()) { + unusedImages.push_back(link.m_path); + } + } + + if (!unusedImages.isEmpty()) { + if (g_config->getConfirmImagesCleanUp()) { + QVector items; + for (auto const & img : unusedImages) { + items.push_back(ConfirmItemInfo(img, + img, + img, + NULL)); + + } + + QString text = tr("Following images seems not to be used in this note anymore. " + "Please confirm the deletion of these images."); + + QString info = tr("Deleted files could be found in the recycle " + "bin of this note.
" + "Click \"Cancel\" to leave them untouched."); + + VConfirmDeletionDialog dialog(tr("Confirm Cleaning Up Unused Images"), + text, + info, + items, + true, + true, + true, + this); + + unusedImages.clear(); + if (dialog.exec()) { + items = dialog.getConfirmedItems(); + g_config->setConfirmImagesCleanUp(dialog.getAskAgainEnabled()); + + for (auto const & item : items) { + unusedImages.push_back(item.m_name); + } + } + } + + for (int i = 0; i < unusedImages.size(); ++i) { + bool ret = false; + if (m_file->getType() == FileType::Note) { + const VNoteFile *tmpFile = dynamic_cast((VFile *)m_file); + ret = VUtils::deleteFile(tmpFile->getNotebook(), unusedImages[i], false); + } else if (m_file->getType() == FileType::Orphan) { + const VOrphanFile *tmpFile = dynamic_cast((VFile *)m_file); + ret = VUtils::deleteFile(tmpFile, unusedImages[i], false); + } else { + Q_ASSERT(false); + } + + if (!ret) { + qWarning() << "fail to delete unused original image" << unusedImages[i]; + } else { + qDebug() << "delete unused image" << unusedImages[i]; + } + } + } + + m_initImages.clear(); +} + +void VMdEditor::keyPressEvent(QKeyEvent *p_event) +{ + if (m_editOps && m_editOps->handleKeyPressEvent(p_event)) { + return; + } + + VPlainTextEdit::keyPressEvent(p_event); +} + +bool VMdEditor::canInsertFromMimeData(const QMimeData *p_source) const +{ + return p_source->hasImage() + || p_source->hasUrls() + || VPlainTextEdit::canInsertFromMimeData(p_source); +} + +void VMdEditor::insertFromMimeData(const QMimeData *p_source) +{ + VSelectDialog dialog(tr("Insert From Clipboard"), this); + dialog.addSelection(tr("Insert As Image"), 0); + dialog.addSelection(tr("Insert As Text"), 1); + + if (p_source->hasImage()) { + // Image data in the clipboard + if (p_source->hasText()) { + if (dialog.exec() == QDialog::Accepted) { + if (dialog.getSelection() == 1) { + // Insert as text. + Q_ASSERT(p_source->hasText() && p_source->hasImage()); + VPlainTextEdit::insertFromMimeData(p_source); + return; + } + } else { + return; + } + } + + m_editOps->insertImageFromMimeData(p_source); + return; + } else if (p_source->hasUrls()) { + QList urls = p_source->urls(); + if (urls.size() == 1 && VUtils::isImageURL(urls[0])) { + if (dialog.exec() == QDialog::Accepted) { + // FIXME: After calling dialog.exec(), p_source->hasUrl() returns false. + if (dialog.getSelection() == 0) { + // Insert as image. + m_editOps->insertImageFromURL(urls[0]); + return; + } + + QMimeData newSource; + newSource.setUrls(urls); + VPlainTextEdit::insertFromMimeData(&newSource); + return; + } else { + return; + } + } + } else if (p_source->hasText()) { + QString text = p_source->text(); + if (VUtils::isImageURLText(text)) { + // The text is a URL to an image. + if (dialog.exec() == QDialog::Accepted) { + if (dialog.getSelection() == 0) { + // Insert as image. + QUrl url(text); + if (url.isValid()) { + m_editOps->insertImageFromURL(QUrl(text)); + } + return; + } + } else { + return; + } + } + + Q_ASSERT(p_source->hasText()); + } + + VPlainTextEdit::insertFromMimeData(p_source); +} + +void VMdEditor::imageInserted(const QString &p_path) +{ + ImageLink link; + link.m_path = p_path; + if (m_file->useRelativeImageFolder()) { + link.m_type = ImageLink::LocalRelativeInternal; + } else { + link.m_type = ImageLink::LocalAbsolute; + } + + m_insertedImages.append(link); +} + +bool VMdEditor::scrollToHeader(int p_blockNumber) +{ + if (p_blockNumber < 0) { + return false; + } + + return scrollToBlock(p_blockNumber); +} + +int VMdEditor::indexOfCurrentHeader() const +{ + if (m_headers.isEmpty()) { + return -1; + } + + int blockNumber = textCursor().block().blockNumber(); + for (int i = m_headers.size() - 1; i >= 0; --i) { + if (!m_headers[i].isEmpty() + && m_headers[i].m_blockNumber <= blockNumber) { + return i; + } + } + + return -1; +} + +bool VMdEditor::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) +{ + if (m_headers.isEmpty()) { + return false; + } + + QTextCursor cursor = textCursor(); + int cursorLine = cursor.block().blockNumber(); + int targetIdx = -1; + // -1: skip level check. + int targetLevel = 0; + int idx = indexOfCurrentHeader(); + if (idx == -1) { + // Cursor locates at the beginning, before any headers. + if (p_relativeLevel < 0 || !p_forward) { + return false; + } + } + + int delta = 1; + if (!p_forward) { + delta = -1; + } + + bool firstHeader = true; + for (targetIdx = idx == -1 ? 0 : idx; + targetIdx >= 0 && targetIdx < m_headers.size(); + targetIdx += delta) { + const VTableOfContentItem &header = m_headers[targetIdx]; + if (header.isEmpty()) { + continue; + } + + if (targetLevel == 0) { + // The target level has not been init yet. + Q_ASSERT(firstHeader); + targetLevel = header.m_level; + if (p_relativeLevel < 0) { + targetLevel += p_relativeLevel; + if (targetLevel < 1) { + // Invalid level. + return false; + } + } else if (p_relativeLevel > 0) { + targetLevel = -1; + } + } + + if (targetLevel == -1 || header.m_level == targetLevel) { + if (firstHeader + && (cursorLine == header.m_blockNumber + || p_forward) + && idx != -1) { + // This header is not counted for the repeat. + firstHeader = false; + continue; + } + + if (--p_repeat == 0) { + // Found. + break; + } + } else if (header.m_level < targetLevel) { + // Stop by higher level. + return false; + } + + firstHeader = false; + } + + if (targetIdx < 0 || targetIdx >= m_headers.size()) { + return false; + } + + // Jump to target header. + int line = m_headers[targetIdx].m_blockNumber; + if (line > -1) { + QTextBlock block = document()->findBlockByNumber(line); + if (block.isValid()) { + cursor.setPosition(block.position()); + setTextCursor(cursor); + return true; + } + } + + return false; +} + +void VMdEditor::scrollBlockInPage(int p_blockNum, int p_dest) +{ + VEditUtils::scrollBlockInPage(this, p_blockNum, p_dest); +} + +void VMdEditor::updatePlainTextEditConfig() +{ + m_previewMgr->setPreviewEnabled(g_config->getEnablePreviewImages()); + setBlockImageEnabled(g_config->getEnablePreviewImages()); + + setImageWidthConstrainted(g_config->getEnablePreviewImageConstraint()); + + int lineNumber = g_config->getEditorLineNumber(); + if (lineNumber < (int)LineNumberType::None || lineNumber >= (int)LineNumberType::Invalid) { + lineNumber = (int)LineNumberType::None; + } + + setLineNumberType((LineNumberType)lineNumber); + setLineNumberColor(g_config->getEditorLineNumberFg(), + g_config->getEditorLineNumberBg()); +} + +void VMdEditor::updateConfig() +{ + updatePlainTextEditConfig(); + updateEditConfig(); +} diff --git a/src/vmdeditor.h b/src/vmdeditor.h new file mode 100644 index 00000000..f8fe4951 --- /dev/null +++ b/src/vmdeditor.h @@ -0,0 +1,212 @@ +#ifndef VMDEDITOR_H +#define VMDEDITOR_H + +#include +#include +#include +#include + +#include "vplaintextedit.h" +#include "veditor.h" +#include "vconfigmanager.h" +#include "vtableofcontent.h" +#include "veditoperations.h" +#include "vconfigmanager.h" +#include "utils/vutils.h" + +class HGMarkdownHighlighter; +class VCodeBlockHighlightHelper; +class VDocument; +class VPreviewManager; + +class VMdEditor : public VPlainTextEdit, public VEditor +{ + Q_OBJECT +public: + VMdEditor(VFile *p_file, + VDocument *p_doc, + MarkdownConverterType p_type, + QWidget *p_parent = nullptr); + + void beginEdit() Q_DECL_OVERRIDE; + + void endEdit() Q_DECL_OVERRIDE; + + void saveFile() Q_DECL_OVERRIDE; + + void reloadFile() Q_DECL_OVERRIDE; + + bool scrollToBlock(int p_blockNumber) Q_DECL_OVERRIDE; + + void makeBlockVisible(const QTextBlock &p_block) Q_DECL_OVERRIDE; + + QVariant inputMethodQuery(Qt::InputMethodQuery p_query) const Q_DECL_OVERRIDE; + + bool isBlockVisible(const QTextBlock &p_block) Q_DECL_OVERRIDE; + + // An image has been inserted. The image is relative. + // @p_path is the absolute path of the inserted image. + void imageInserted(const QString &p_path); + + // Scroll to header @p_blockNumber. + // Return true if @p_blockNumber is valid to scroll to. + bool scrollToHeader(int p_blockNumber); + + void scrollBlockInPage(int p_blockNum, int p_dest) Q_DECL_OVERRIDE; + + void updateConfig() Q_DECL_OVERRIDE; + +public slots: + bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) Q_DECL_OVERRIDE; + +// Wrapper functions for QPlainTextEdit/QTextEdit. +public: + void setExtraSelectionsW(const QList &p_selections) Q_DECL_OVERRIDE + { + setExtraSelections(p_selections); + } + + QTextDocument *documentW() const Q_DECL_OVERRIDE + { + return document(); + } + + void setTabStopWidthW(int p_width) Q_DECL_OVERRIDE + { + setTabStopWidth(p_width); + } + + QTextCursor textCursorW() const Q_DECL_OVERRIDE + { + return textCursor(); + } + + void moveCursorW(QTextCursor::MoveOperation p_operation, + QTextCursor::MoveMode p_mode) Q_DECL_OVERRIDE + { + moveCursor(p_operation, p_mode); + } + + QScrollBar *verticalScrollBarW() const Q_DECL_OVERRIDE + { + return verticalScrollBar(); + } + + QScrollBar *horizontalScrollBarW() const Q_DECL_OVERRIDE + { + return horizontalScrollBar(); + } + + void setTextCursorW(const QTextCursor &p_cursor) Q_DECL_OVERRIDE + { + setTextCursor(p_cursor); + } + + bool findW(const QString &p_exp, + QTextDocument::FindFlags p_options = QTextDocument::FindFlags()) Q_DECL_OVERRIDE + { + return find(p_exp, p_options); + } + + bool findW(const QRegExp &p_exp, + QTextDocument::FindFlags p_options = QTextDocument::FindFlags()) Q_DECL_OVERRIDE + { + return find(p_exp, p_options); + } + + void setReadOnlyW(bool p_ro) Q_DECL_OVERRIDE + { + setReadOnly(p_ro); + } + + QWidget *viewportW() const Q_DECL_OVERRIDE + { + return viewport(); + } + + void insertPlainTextW(const QString &p_text) Q_DECL_OVERRIDE + { + insertPlainText(p_text); + } + + void undoW() Q_DECL_OVERRIDE + { + undo(); + } + + void redoW() Q_DECL_OVERRIDE + { + redo(); + } + +signals: + // Signal when headers change. + void headersChanged(const QVector &p_headers); + + // Signal when current header change. + void currentHeaderChanged(int p_blockNumber); + + // Signal when the status of VMdEdit changed. + // Will be emitted by VImagePreviewer for now. + void statusChanged(); + +protected: + void updateFontAndPalette() Q_DECL_OVERRIDE; + + void contextMenuEvent(QContextMenuEvent *p_event) Q_DECL_OVERRIDE; + + // Used to implement dragging mouse with Ctrl and left button pressed to scroll. + void mousePressEvent(QMouseEvent *p_event) Q_DECL_OVERRIDE; + + void mouseReleaseEvent(QMouseEvent *p_event) Q_DECL_OVERRIDE; + + void mouseMoveEvent(QMouseEvent *p_event) Q_DECL_OVERRIDE; + + void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE; + + bool canInsertFromMimeData(const QMimeData *p_source) const Q_DECL_OVERRIDE; + + void insertFromMimeData(const QMimeData *p_source) Q_DECL_OVERRIDE; + +private slots: + // Update m_headers according to elements. + void updateHeaders(const QVector &p_headerRegions); + + // Update current header according to cursor position. + // When there is no header in current cursor, will signal an invalid header. + void updateCurrentHeader(); + +private: + // Update the config of VPlainTextEdit according to global configurations. + void updatePlainTextEditConfig(); + + // Get the initial images from file before edit. + void initInitImages(); + + // Clear two kind of images according to initial images and current images: + // 1. Newly inserted images which are deleted later; + // 2. Initial images which are deleted; + void clearUnusedImages(); + + // Index in m_headers of current header which contains the cursor. + int indexOfCurrentHeader() const; + + HGMarkdownHighlighter *m_mdHighlighter; + + VCodeBlockHighlightHelper *m_cbHighlighter; + + VPreviewManager *m_previewMgr; + + // Image links inserted while editing. + QVector m_insertedImages; + + // Image links right at the beginning of the edit. + QVector m_initImages; + + // Mainly used for title jump. + QVector m_headers; + + bool m_freshEdit; +}; + +#endif // VMDEDITOR_H diff --git a/src/vmdtab.cpp b/src/vmdtab.cpp index eb696908..e111890d 100644 --- a/src/vmdtab.cpp +++ b/src/vmdtab.cpp @@ -12,11 +12,14 @@ #include "vmarkdownconverter.h" #include "vnotebook.h" #include "vtableofcontent.h" -#include "vmdedit.h" #include "dialog/vfindreplacedialog.h" #include "veditarea.h" #include "vconstants.h" #include "vwebview.h" +#include "vmdeditor.h" +#include "vmainwindow.h" + +extern VMainWindow *g_mainWin; extern VConfigManager *g_config; @@ -124,7 +127,7 @@ bool VMdTab::scrollEditorToHeader(const VHeaderPointer &p_header) return false; } - VMdEdit *mdEdit = dynamic_cast(getEditor()); + VMdEditor *mdEdit = getEditor(); int blockNumber = -1; if (p_header.isValid()) { @@ -185,8 +188,7 @@ void VMdTab::showFileEditMode() m_isEditMode = true; - VMdEdit *mdEdit = dynamic_cast(getEditor()); - V_ASSERT(mdEdit); + VMdEditor *mdEdit = getEditor(); mdEdit->beginEdit(); m_stacks->setCurrentWidget(mdEdit); @@ -376,34 +378,35 @@ void VMdTab::setupMarkdownViewer() void VMdTab::setupMarkdownEditor() { Q_ASSERT(!m_editor); - qDebug() << "create Markdown editor"; - m_editor = new VMdEdit(m_file, m_document, m_mdConType, this); - connect(dynamic_cast(m_editor), &VMdEdit::headersChanged, + m_editor = new VMdEditor(m_file, m_document, m_mdConType, this); + connect(m_editor, &VMdEditor::headersChanged, this, &VMdTab::updateOutlineFromHeaders); - connect(dynamic_cast(m_editor), SIGNAL(currentHeaderChanged(int)), + connect(m_editor, SIGNAL(currentHeaderChanged(int)), this, SLOT(updateCurrentHeader(int))); - connect(dynamic_cast(m_editor), &VMdEdit::statusChanged, + connect(m_editor, &VMdEditor::statusChanged, this, &VMdTab::updateStatus); - connect(m_editor, &VEdit::textChanged, + connect(m_editor, &VMdEditor::textChanged, this, &VMdTab::updateStatus); - connect(m_editor, &VEdit::cursorPositionChanged, + connect(m_editor, &VMdEditor::cursorPositionChanged, this, &VMdTab::updateStatus); - connect(m_editor, &VEdit::saveAndRead, + connect(g_mainWin, &VMainWindow::editorConfigUpdated, + m_editor, &VMdEditor::updateConfig); + connect(m_editor->object(), &VEditorObject::saveAndRead, this, &VMdTab::saveAndRead); - connect(m_editor, &VEdit::discardAndRead, + connect(m_editor->object(), &VEditorObject::discardAndRead, this, &VMdTab::discardAndRead); - connect(m_editor, &VEdit::saveNote, + connect(m_editor->object(), &VEditorObject::saveNote, this, &VMdTab::saveFile); - connect(m_editor, &VEdit::statusMessage, + connect(m_editor->object(), &VEditorObject::statusMessage, this, &VEditTab::statusMessage); - connect(m_editor, &VEdit::vimStatusUpdated, + connect(m_editor->object(), &VEditorObject::vimStatusUpdated, this, &VEditTab::vimStatusUpdated); - connect(m_editor, &VEdit::requestCloseFindReplaceDialog, + connect(m_editor->object(), &VEditorObject::requestCloseFindReplaceDialog, this, [this]() { this->m_editArea->getFindReplaceDialog()->closeDialog(); }); - connect(m_editor, SIGNAL(ready(void)), + connect(m_editor->object(), SIGNAL(ready(void)), this, SLOT(restoreFromTabInfo(void))); enableHeadingSequence(m_enableHeadingSequence); diff --git a/src/vmdtab.h b/src/vmdtab.h index 03dca941..bc674905 100644 --- a/src/vmdtab.h +++ b/src/vmdtab.h @@ -10,8 +10,8 @@ class VWebView; class QStackedLayout; -class VEdit; class VDocument; +class VMdEditor; class VMdTab : public VEditTab { @@ -55,7 +55,7 @@ public: VWebView *getWebViewer() const; - VEdit *getEditor() const; + VMdEditor *getEditor() const; MarkdownConverterType getMarkdownConverterType() const; @@ -148,13 +148,13 @@ private: void focusChild() Q_DECL_OVERRIDE; // Get the markdown editor. If not init yet, init and return it. - VEdit *getEditor(); + VMdEditor *getEditor(); // Restore from @p_fino. // Return true if succeed. bool restoreFromTabInfo(const VEditTabInfo &p_info) Q_DECL_OVERRIDE; - VEdit *m_editor; + VMdEditor *m_editor; VWebView *m_webViewer; VDocument *m_document; MarkdownConverterType m_mdConType; @@ -165,7 +165,7 @@ private: QStackedLayout *m_stacks; }; -inline VEdit *VMdTab::getEditor() +inline VMdEditor *VMdTab::getEditor() { if (m_editor) { return m_editor; @@ -175,7 +175,7 @@ inline VEdit *VMdTab::getEditor() } } -inline VEdit *VMdTab::getEditor() const +inline VMdEditor *VMdTab::getEditor() const { return m_editor; } diff --git a/src/vplaintextedit.cpp b/src/vplaintextedit.cpp index 134dc871..7d33d463 100644 --- a/src/vplaintextedit.cpp +++ b/src/vplaintextedit.cpp @@ -14,10 +14,11 @@ const int VPlainTextEdit::c_minimumImageWidth = 100; enum class BlockState { - Normal = 1, + Normal = 0, CodeBlockStart, CodeBlock, - CodeBlockEnd + CodeBlockEnd, + Comment }; @@ -79,6 +80,8 @@ void VPlainTextEdit::updateBlockImages(const QVector &p_blocksI { if (m_blockImageEnabled) { m_imageMgr->updateBlockInfos(p_blocksInfo, m_maximumImageWidth); + + update(); } } @@ -298,6 +301,9 @@ void VPlainTextEdit::drawImageOfBlock(const QTextBlock &p_block, qMax(info->m_imageHeight, tmpRect.height() - oriHeight)); p_painter->drawPixmap(targetRect, *image); + + auto *layout = getLayout(); + emit layout->documentSizeChanged(layout->documentSize()); } QRectF VPlainTextEdit::originalBlockBoundingRect(const QTextBlock &p_block) const @@ -322,14 +328,23 @@ void VPlainTextEdit::setBlockImageEnabled(bool p_enabled) void VPlainTextEdit::setImageWidthConstrainted(bool p_enabled) { m_imageWidthConstrainted = p_enabled; + + updateImageWidth(); + + auto *layout = getLayout(); + emit layout->documentSizeChanged(layout->documentSize()); } -void VPlainTextEdit::resizeEvent(QResizeEvent *p_event) +void VPlainTextEdit::updateImageWidth() { bool needUpdate = false; if (m_imageWidthConstrainted) { - const QSize &si = p_event->size(); - m_maximumImageWidth = si.width(); + int viewWidth = viewport()->size().width(); + m_maximumImageWidth = viewWidth - 10; + if (m_maximumImageWidth < 0) { + m_maximumImageWidth = viewWidth; + } + needUpdate = true; } else if (m_maximumImageWidth != INT_MAX) { needUpdate = true; @@ -339,6 +354,11 @@ void VPlainTextEdit::resizeEvent(QResizeEvent *p_event) if (needUpdate) { m_imageMgr->updateImageWidth(m_maximumImageWidth); } +} + +void VPlainTextEdit::resizeEvent(QResizeEvent *p_event) +{ + updateImageWidth(); QPlainTextEdit::resizeEvent(p_event); @@ -368,9 +388,8 @@ void VPlainTextEdit::paintLineNumberArea(QPaintEvent *p_event) } int blockNumber = block.blockNumber(); - int offsetY = (int)contentOffset().y(); - QRectF rect = blockBoundingRect(block); - int top = offsetY + (int)rect.y(); + QRectF rect = blockBoundingGeometry(block); + int top = (int)(contentOffset().y() + rect.y()); int bottom = top + (int)rect.height(); int eventTop = p_event->rect().top(); int eventBtm = p_event->rect().bottom(); @@ -496,7 +515,9 @@ void VPlainTextEdit::updateLineNumberAreaMargin() width = m_lineNumberArea->calculateWidth(); } - setViewportMargins(width, 0, 0, 0); + if (width != viewportMargins().left()) { + setViewportMargins(width, 0, 0, 0); + } } void VPlainTextEdit::updateLineNumberArea() diff --git a/src/vplaintextedit.h b/src/vplaintextedit.h index 031777c9..11a32d71 100644 --- a/src/vplaintextedit.h +++ b/src/vplaintextedit.h @@ -83,6 +83,8 @@ public: void setLineNumberType(LineNumberType p_type); + void setLineNumberColor(const QColor &p_foreground, const QColor &p_background); + // The minimum width of an image in pixels. static const int c_minimumImageWidth; @@ -111,6 +113,8 @@ private: VPlainTextDocumentLayout *getLayout() const; + void updateImageWidth(); + // Widget to display line number area. VLineNumberArea *m_lineNumberArea; @@ -176,4 +180,11 @@ inline void VPlainTextEdit::setLineNumberType(LineNumberType p_type) updateLineNumberArea(); } +inline void VPlainTextEdit::setLineNumberColor(const QColor &p_foreground, + const QColor &p_background) +{ + m_lineNumberArea->setForegroundColor(p_foreground); + m_lineNumberArea->setBackgroundColor(p_background); +} + #endif // VPLAINTEXTEDIT_H diff --git a/src/vpreviewmanager.cpp b/src/vpreviewmanager.cpp new file mode 100644 index 00000000..6e3d2f78 --- /dev/null +++ b/src/vpreviewmanager.cpp @@ -0,0 +1,316 @@ +#include "vpreviewmanager.h" + +#include +#include +#include +#include +#include +#include "vconfigmanager.h" +#include "utils/vutils.h" +#include "vdownloader.h" +#include "hgmarkdownhighlighter.h" +#include "vtextblockdata.h" + +extern VConfigManager *g_config; + +VPreviewManager::VPreviewManager(VMdEditor *p_editor) + : QObject(p_editor), + m_editor(p_editor), + m_previewEnabled(false) +{ + m_blockImageInfo.resize(PreviewSource::Invalid); + + m_downloader = new VDownloader(this); + connect(m_downloader, &VDownloader::downloadFinished, + this, &VPreviewManager::imageDownloaded); +} + +void VPreviewManager::imageLinksUpdated(const QVector &p_imageRegions) +{ + if (!m_previewEnabled) { + return; + } + + m_imageRegions = p_imageRegions; + + previewImages(); +} + +void VPreviewManager::imageDownloaded(const QByteArray &p_data, const QString &p_url) +{ + if (!m_previewEnabled) { + return; + } + + auto it = m_urlToName.find(p_url); + if (it == m_urlToName.end()) { + return; + } + + QString name = it.value(); + m_urlToName.erase(it); + + if (m_editor->containsImage(name) || name.isEmpty()) { + return; + } + + QPixmap image; + image.loadFromData(p_data); + + if (!image.isNull()) { + m_editor->addImage(name, image); + qDebug() << "downloaded image inserted in resource manager" << p_url << name; + emit requestUpdateImageLinks(); + } +} + +void VPreviewManager::setPreviewEnabled(bool p_enabled) +{ + if (m_previewEnabled != p_enabled) { + m_previewEnabled = p_enabled; + + if (!m_previewEnabled) { + clearPreview(); + } + } +} + +void VPreviewManager::clearPreview() +{ + for (int i = 0; i < m_blockImageInfo.size(); ++i) { + m_blockImageInfo[i].clear(); + } + + updateEditorBlockImages(); +} + +void VPreviewManager::previewImages() +{ + QVector imageLinks; + fetchImageLinksFromRegions(imageLinks); + + updateBlockImageInfo(imageLinks); + + updateEditorBlockImages(); +} + +// Returns true if p_text[p_start, p_end) is all spaces. +static bool isAllSpaces(const QString &p_text, int p_start, int p_end) +{ + int len = qMin(p_text.size(), p_end); + for (int i = p_start; i < len; ++i) { + if (!p_text[i].isSpace()) { + return false; + } + } + + return true; +} + +void VPreviewManager::fetchImageLinksFromRegions(QVector &p_imageLinks) +{ + p_imageLinks.clear(); + + if (m_imageRegions.isEmpty()) { + return; + } + + p_imageLinks.reserve(m_imageRegions.size()); + + QTextDocument *doc = m_editor->document(); + + for (int i = 0; i < m_imageRegions.size(); ++i) { + VElementRegion ® = m_imageRegions[i]; + QTextBlock block = doc->findBlock(reg.m_startPos); + if (!block.isValid()) { + continue; + } + + int blockStart = block.position(); + int blockEnd = blockStart + block.length() - 1; + QString text = block.text(); + Q_ASSERT(reg.m_endPos <= blockEnd); + ImageLinkInfo info(reg.m_startPos, + reg.m_endPos, + block.blockNumber(), + calculateBlockMargin(block)); + if ((reg.m_startPos == blockStart + || isAllSpaces(text, 0, reg.m_startPos - blockStart)) + && (reg.m_endPos == blockEnd + || isAllSpaces(text, reg.m_endPos - blockStart, blockEnd - blockStart))) { + // Image block. + info.m_isBlock = true; + info.m_linkUrl = fetchImagePathToPreview(text, info.m_linkShortUrl); + } else { + // Inline image. + info.m_isBlock = false; + info.m_linkUrl = fetchImagePathToPreview(text.mid(reg.m_startPos - blockStart, + reg.m_endPos - reg.m_startPos), + info.m_linkShortUrl); + } + + if (info.m_linkUrl.isEmpty()) { + continue; + } + + p_imageLinks.append(info); + + qDebug() << "image region" << i + << info.m_startPos << info.m_endPos << info.m_blockNumber + << info.m_linkShortUrl << info.m_linkUrl << info.m_isBlock; + } +} + +QString VPreviewManager::fetchImageUrlToPreview(const QString &p_text) +{ + QRegExp regExp(VUtils::c_imageLinkRegExp); + + int index = regExp.indexIn(p_text); + if (index == -1) { + return QString(); + } + + int lastIndex = regExp.lastIndexIn(p_text); + if (lastIndex != index) { + return QString(); + } + + return regExp.capturedTexts()[2].trimmed(); +} + +QString VPreviewManager::fetchImagePathToPreview(const QString &p_text, QString &p_url) +{ + p_url = fetchImageUrlToPreview(p_text); + if (p_url.isEmpty()) { + return p_url; + } + + const VFile *file = m_editor->getFile(); + + QString imagePath; + QFileInfo info(file->fetchBasePath(), p_url); + + if (info.exists()) { + if (info.isNativePath()) { + // Local file. + imagePath = QDir::cleanPath(info.absoluteFilePath()); + } else { + imagePath = p_url; + } + } else { + QString decodedUrl(p_url); + VUtils::decodeUrl(decodedUrl); + QFileInfo dinfo(file->fetchBasePath(), decodedUrl); + if (dinfo.exists()) { + if (dinfo.isNativePath()) { + // Local file. + imagePath = QDir::cleanPath(dinfo.absoluteFilePath()); + } else { + imagePath = p_url; + } + } else { + QUrl url(p_url); + imagePath = url.toString(); + } + } + + return imagePath; +} + +void VPreviewManager::updateBlockImageInfo(const QVector &p_imageLinks) +{ + QVector &blockInfos = m_blockImageInfo[PreviewSource::ImageLink]; + blockInfos.clear(); + + for (int i = 0; i < p_imageLinks.size(); ++i) { + const ImageLinkInfo &link = p_imageLinks[i]; + + // Skip inline images. + if (!link.m_isBlock) { + continue; + } + + QString name = imageResourceName(link); + if (name.isEmpty()) { + continue; + } + + VBlockImageInfo info(link.m_blockNumber, name, link.m_margin); + blockInfos.push_back(info); + } +} + +QString VPreviewManager::imageResourceName(const ImageLinkInfo &p_link) +{ + QString name = p_link.m_linkShortUrl; + if (m_editor->containsImage(name) + || name.isEmpty()) { + return name; + } + + // Add it to the resource. + QString imgPath = p_link.m_linkUrl; + QFileInfo info(imgPath); + QPixmap image; + if (info.exists()) { + // Local file. + image = QPixmap(imgPath); + } else { + // URL. Try to download it. + m_downloader->download(imgPath); + m_urlToName.insert(imgPath, name); + } + + if (image.isNull()) { + return QString(); + } + + m_editor->addImage(name, image); + return name; +} + +void VPreviewManager::updateEditorBlockImages() +{ + // TODO: need to combine all preview sources. + Q_ASSERT(m_blockImageInfo.size() == 1); + + m_editor->updateBlockImages(m_blockImageInfo[PreviewSource::ImageLink]); +} + +int VPreviewManager::calculateBlockMargin(const QTextBlock &p_block) +{ + static QHash spaceWidthOfFonts; + + if (!p_block.isValid()) { + return 0; + } + + QString text = p_block.text(); + int nrSpaces = 0; + for (int i = 0; i < text.size(); ++i) { + if (!text[i].isSpace()) { + break; + } else if (text[i] == ' ') { + ++nrSpaces; + } else if (text[i] == '\t') { + nrSpaces += m_editor->tabStopWidth(); + } + } + + if (nrSpaces == 0) { + return 0; + } + + int spaceWidth = 0; + QFont font = p_block.charFormat().font(); + QString fontName = font.toString(); + auto it = spaceWidthOfFonts.find(fontName); + if (it != spaceWidthOfFonts.end()) { + spaceWidth = it.value(); + } else { + spaceWidth = QFontMetrics(font).width(' '); + spaceWidthOfFonts.insert(fontName, spaceWidth); + } + + return spaceWidth * nrSpaces; +} diff --git a/src/vpreviewmanager.h b/src/vpreviewmanager.h new file mode 100644 index 00000000..c361e510 --- /dev/null +++ b/src/vpreviewmanager.h @@ -0,0 +1,135 @@ +#ifndef VPREVIEWMANAGER_H +#define VPREVIEWMANAGER_H + +#include +#include +#include +#include +#include +#include "hgmarkdownhighlighter.h" +#include "vmdeditor.h" + +class VDownloader; + + +class VPreviewManager : public QObject +{ + Q_OBJECT +public: + explicit VPreviewManager(VMdEditor *p_editor); + + void setPreviewEnabled(bool p_enabled); + + // Clear all the preview. + void clearPreview(); + +public slots: + // Image links were updated from the highlighter. + void imageLinksUpdated(const QVector &p_imageRegions); + +signals: + // Request highlighter to update image links. + void requestUpdateImageLinks(); + +private slots: + // Non-local image downloaded for preview. + void imageDownloaded(const QByteArray &p_data, const QString &p_url); + +private: + // Sources of the preview. + enum PreviewSource + { + ImageLink = 0, + Invalid + }; + + struct ImageLinkInfo + { + ImageLinkInfo() + : m_startPos(-1), + m_endPos(-1), + m_blockNumber(-1), + m_margin(0), + m_isBlock(false) + { + } + + ImageLinkInfo(int p_startPos, + int p_endPos, + int p_blockNumber, + int p_margin) + : m_startPos(p_startPos), + m_endPos(p_endPos), + m_blockNumber(p_blockNumber), + m_margin(p_margin), + m_isBlock(false) + { + } + + int m_startPos; + + int m_endPos; + + int m_blockNumber; + + // Left margin of this block in pixels. + int m_margin; + + // Short URL within the () of ![](). + // Used as the ID of the image. + QString m_linkShortUrl; + + // Full URL of the link. + QString m_linkUrl; + + // Whether it is an image block. + bool m_isBlock; + }; + + // Start to preview images according to image links. + void previewImages(); + + // According to m_imageRegions, fetch the image link Url. + // @p_imageRegions: output. + void fetchImageLinksFromRegions(QVector &p_imageLinks); + + // Fetch the image link's URL if there is only one link. + QString fetchImageUrlToPreview(const QString &p_text); + + // Fetch teh image's full path if there is only one image link. + // @p_url: contains the short URL in ![](). + QString fetchImagePathToPreview(const QString &p_text, QString &p_url); + + void updateBlockImageInfo(const QVector &p_imageLinks); + + // Get the name of the image in the resource manager. + // Will add the image to the resource manager if not exists. + // Returns empty if fail to add the image to the resource manager. + QString imageResourceName(const ImageLinkInfo &p_link); + + // Ask the editor to preview images. + void updateEditorBlockImages(); + + // Calculate the block margin (prefix spaces) in pixels. + int calculateBlockMargin(const QTextBlock &p_block); + + VMdEditor *m_editor; + + VDownloader *m_downloader; + + // Whether preview is enabled. + bool m_previewEnabled; + + // Regions of all the image links. + QVector m_imageRegions; + + // All preview images and information. + // Each preview source corresponds to one vector. + QVector> m_blockImageInfo; + + // Map from URL to name in the resource manager. + // Used for downloading images. + QHash m_urlToName; +}; + +#endif // VPREVIEWMANAGER_H