Browse Source

refactor VEditTab

1. Make VEditTab an abstract class.
2. Use VMdTab inheriting from VEditTab for Markdown file.
3. Use VHtmlTab inheriting from VEditTab for Html file.
Le Tan 8 years ago
parent
commit
306b3cca92
11 changed files with 1273 additions and 785 deletions
  1. 6 2
      src/src.pro
  2. 1 1
      src/vedit.cpp
  3. 1 0
      src/veditarea.h
  4. 31 666
      src/vedittab.cpp
  5. 67 102
      src/vedittab.h
  6. 20 4
      src/veditwindow.cpp
  7. 246 0
      src/vhtmltab.cpp
  8. 81 0
      src/vhtmltab.h
  9. 17 10
      src/vmainwindow.cpp
  10. 673 0
      src/vmdtab.cpp
  11. 130 0
      src/vmdtab.h

+ 6 - 2
src/src.pro

@@ -62,7 +62,9 @@ SOURCES += main.cpp\
     vcodeblockhighlighthelper.cpp \
     vwebview.cpp \
     vimagepreviewer.cpp \
-    vexporter.cpp
+    vexporter.cpp \
+    vmdtab.cpp \
+    vhtmltab.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -111,7 +113,9 @@ HEADERS  += vmainwindow.h \
     vcodeblockhighlighthelper.h \
     vwebview.h \
     vimagepreviewer.h \
-    vexporter.h
+    vexporter.h \
+    vmdtab.h \
+    vhtmltab.h
 
 RESOURCES += \
     vnote.qrc \

+ 1 - 1
src/vedit.cpp

@@ -568,7 +568,7 @@ void VEdit::contextMenuEvent(QContextMenuEvent *p_event)
     if (!textCursor().hasSelection()) {
         VEditTab *editTab = dynamic_cast<VEditTab *>(parent());
         V_ASSERT(editTab);
-        if (editTab->getIsEditMode()) {
+        if (editTab->isEditMode()) {
             QAction *saveExitAct = new QAction(QIcon(":/resources/icons/save_exit.svg"),
                                                tr("&Save Changes And Read"), this);
             saveExitAct->setToolTip(tr("Save changes and exit edit mode"));

+ 1 - 0
src/veditarea.h

@@ -19,6 +19,7 @@ class VNote;
 class VFile;
 class VDirectory;
 class VFindReplaceDialog;
+class QLabel;
 
 class VEditArea : public QWidget, public VNavigationMode
 {

+ 31 - 666
src/vedittab.cpp

@@ -1,42 +1,14 @@
-#include <QtWidgets>
-#include <QWebChannel>
-#include <QFileInfo>
-#include <QXmlStreamReader>
 #include "vedittab.h"
-#include "vedit.h"
-#include "vdocument.h"
-#include "vnote.h"
-#include "utils/vutils.h"
-#include "vpreviewpage.h"
-#include "hgmarkdownhighlighter.h"
-#include "vconfigmanager.h"
-#include "vmarkdownconverter.h"
-#include "vnotebook.h"
-#include "vtoc.h"
-#include "vmdedit.h"
-#include "dialog/vfindreplacedialog.h"
-#include "veditarea.h"
-#include "vconstants.h"
-#include "vwebview.h"
+#include <QApplication>
+#include <QWheelEvent>
 
-extern VConfigManager vconfig;
-
-VEditTab::VEditTab(VFile *p_file, OpenFileMode p_mode, QWidget *p_parent)
-    : QStackedWidget(p_parent), m_file(p_file), isEditMode(false),
-      webPreviewer(NULL), document(p_file, this),
-      mdConverterType(vconfig.getMdConverterType()), m_fileModified(false),
-      m_editArea(NULL)
+VEditTab::VEditTab(VFile *p_file, VEditArea *p_editArea, QWidget *p_parent)
+    : QWidget(p_parent), m_file(p_file), m_isEditMode(false),
+      m_modified(false), m_editArea(p_editArea)
 {
-    tableOfContent.filePath = p_file->retrivePath();
-    curHeader.filePath = p_file->retrivePath();
-    Q_ASSERT(!m_file->isOpened());
-    m_file->open();
-    setupUI();
-    if (p_mode == OpenFileMode::Edit) {
-        showFileEditMode();
-    } else {
-        showFileReadMode();
-    }
+    m_toc.filePath = m_file->retrivePath();
+    m_curHeader.filePath = m_file->retrivePath();
+
     connect(qApp, &QApplication::focusChanged,
             this, &VEditTab::handleFocusChanged);
 }
@@ -48,667 +20,60 @@ VEditTab::~VEditTab()
     }
 }
 
-void VEditTab::init(VEditArea *p_editArea)
-{
-    m_editArea = p_editArea;
-}
-
-void VEditTab::setupUI()
-{
-    switch (m_file->getDocType()) {
-    case DocType::Markdown:
-        if (m_file->isModifiable()) {
-            m_textEditor = new VMdEdit(m_file, &document, mdConverterType, this);
-            connect(dynamic_cast<VMdEdit *>(m_textEditor), &VMdEdit::headersChanged,
-                    this, &VEditTab::updateTocFromHeaders);
-            connect(dynamic_cast<VMdEdit *>(m_textEditor), &VMdEdit::statusChanged,
-                    this, &VEditTab::noticeStatusChanged);
-            connect(m_textEditor, SIGNAL(curHeaderChanged(int, int)),
-                    this, SLOT(updateCurHeader(int, int)));
-            connect(m_textEditor, &VEdit::textChanged,
-                    this, &VEditTab::handleTextChanged);
-            connect(m_textEditor, &VEdit::saveAndRead,
-                    this, &VEditTab::saveAndRead);
-            connect(m_textEditor, &VEdit::discardAndRead,
-                    this, &VEditTab::discardAndRead);
-            m_textEditor->reloadFile();
-            addWidget(m_textEditor);
-        } else {
-            m_textEditor = NULL;
-        }
-        setupMarkdownPreview();
-        break;
-
-    case DocType::Html:
-        m_textEditor = new VEdit(m_file, this);
-        connect(m_textEditor, &VEdit::textChanged,
-                this, &VEditTab::handleTextChanged);
-        connect(m_textEditor, &VEdit::saveAndRead,
-                this, &VEditTab::saveAndRead);
-        connect(m_textEditor, &VEdit::discardAndRead,
-                this, &VEditTab::discardAndRead);
-        connect(m_textEditor, &VEdit::editNote,
-                this, &VEditTab::editFile);
-        m_textEditor->reloadFile();
-        addWidget(m_textEditor);
-        webPreviewer = NULL;
-        break;
-
-    default:
-        qWarning() << "unknown doc type" << int(m_file->getDocType());
-        Q_ASSERT(false);
-    }
-}
-
-void VEditTab::handleTextChanged()
-{
-    Q_ASSERT(m_file->isModifiable());
-    if (m_fileModified) {
-        return;
-    }
-    noticeStatusChanged();
-}
-
-void VEditTab::noticeStatusChanged()
-{
-    m_fileModified = m_file->isModified();
-    emit statusChanged();
-}
-
-void VEditTab::showFileReadMode()
-{
-    isEditMode = false;
-    int outlineIndex = curHeader.m_outlineIndex;
-    switch (m_file->getDocType()) {
-    case DocType::Html:
-        m_textEditor->setReadOnly(true);
-        break;
-
-    case DocType::Markdown:
-        if (mdConverterType == MarkdownConverterType::Hoedown) {
-            previewByConverter();
-        } else {
-            document.updateText();
-            updateTocFromHtml(document.getToc());
-        }
-        setCurrentWidget(webPreviewer);
-        clearSearchedWordHighlight();
-        scrollPreviewToHeader(outlineIndex);
-        break;
-
-    default:
-        qWarning() << "unknown doc type" << int(m_file->getDocType());
-        Q_ASSERT(false);
-    }
-    noticeStatusChanged();
-}
-
-void VEditTab::scrollPreviewToHeader(int p_outlineIndex)
-{
-    Q_ASSERT(p_outlineIndex >= 0);
-    if (p_outlineIndex < tableOfContent.headers.size()) {
-        QString anchor = tableOfContent.headers[p_outlineIndex].anchor;
-        qDebug() << "scroll preview to" << p_outlineIndex << anchor;
-        if (!anchor.isEmpty()) {
-            document.scrollToAnchor(anchor.mid(1));
-        }
-    }
-}
-
-void VEditTab::previewByConverter()
-{
-    VMarkdownConverter mdConverter;
-    QString toc;
-    QString html = mdConverter.generateHtml(m_file->getContent(),
-                                            vconfig.getMarkdownExtensions(),
-                                            toc);
-    document.setHtml(html);
-    updateTocFromHtml(toc);
-}
-
-void VEditTab::showFileEditMode()
-{
-    if (!m_file->isModifiable()) {
-        return;
-    }
-    isEditMode = true;
-
-    // beginEdit() may change curHeader.
-    int outlineIndex = curHeader.m_outlineIndex;
-    m_textEditor->beginEdit();
-    setCurrentWidget(m_textEditor);
-    if (m_file->getDocType() == DocType::Markdown) {
-        dynamic_cast<VMdEdit *>(m_textEditor)->scrollToHeader(outlineIndex);
-    }
-    m_textEditor->setFocus();
-    noticeStatusChanged();
-}
-
-bool VEditTab::closeFile(bool p_forced)
-{
-    if (p_forced && isEditMode) {
-        // Discard buffer content
-        m_textEditor->reloadFile();
-        m_textEditor->endEdit();
-        showFileReadMode();
-    } else {
-        readFile();
-    }
-    return !isEditMode;
-}
-
-void VEditTab::editFile()
-{
-    if (isEditMode || !m_file->isModifiable()) {
-        return;
-    }
-
-    showFileEditMode();
-}
-
-void VEditTab::readFile()
-{
-    if (!isEditMode) {
-        return;
-    }
-
-    if (m_textEditor && m_textEditor->isModified()) {
-        // Prompt to save the changes
-        int ret = VUtils::showMessage(QMessageBox::Information, tr("Information"),
-                                      tr("Note <span style=\"%1\">%2</span> has been modified.")
-                                        .arg(vconfig.c_dataTextStyle).arg(m_file->getName()),
-                                      tr("Do you want to save your changes?"),
-                                      QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
-                                      QMessageBox::Save, this);
-        switch (ret) {
-        case QMessageBox::Save:
-            saveFile();
-            // Fall through
-        case QMessageBox::Discard:
-            m_textEditor->reloadFile();
-            break;
-        case QMessageBox::Cancel:
-            // Nothing to do if user cancel this action
-            return;
-        default:
-            qWarning() << "wrong return value from QMessageBox:" << ret;
-            return;
-        }
-    }
-
-    if (m_textEditor) {
-        m_textEditor->endEdit();
-    }
-
-    showFileReadMode();
-}
-
-bool VEditTab::saveFile()
-{
-    if (!isEditMode || !m_textEditor->isModified()) {
-        return true;
-    }
-
-    bool ret;
-    // Make sure the file already exists. Temporary deal with cases when user delete or move
-    // a file.
-    QString filePath = m_file->retrivePath();
-    if (!QFile(filePath).exists()) {
-        qWarning() << filePath << "being written has been removed";
-        VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to save note."),
-                            tr("File <span style=\"%1\">%2</span> being written has been removed.")
-                              .arg(vconfig.c_dataTextStyle).arg(filePath),
-                            QMessageBox::Ok, QMessageBox::Ok, this);
-        return false;
-    }
-    m_textEditor->saveFile();
-    ret = m_file->save();
-    if (!ret) {
-        VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to save note."),
-                            tr("Fail to write to disk when saving a note. Please try it again."),
-                            QMessageBox::Ok, QMessageBox::Ok, this);
-        m_textEditor->setModified(true);
-    }
-    noticeStatusChanged();
-    return ret;
-}
-
-void VEditTab::saveAndRead()
-{
-    saveFile();
-    readFile();
-}
-
-void VEditTab::discardAndRead()
-{
-    readFile();
-}
-
-void VEditTab::setupMarkdownPreview()
-{
-    const QString &jsHolder = c_htmlJSHolder;
-    const QString &extraHolder = c_htmlExtraHolder;
-
-    webPreviewer = new VWebView(m_file, this);
-    connect(webPreviewer, &VWebView::editNote,
-            this, &VEditTab::editFile);
-
-    VPreviewPage *page = new VPreviewPage(webPreviewer);
-    webPreviewer->setPage(page);
-    webPreviewer->setZoomFactor(vconfig.getWebZoomFactor());
-
-    QWebChannel *channel = new QWebChannel(webPreviewer);
-    channel->registerObject(QStringLiteral("content"), &document);
-    connect(&document, &VDocument::tocChanged,
-            this, &VEditTab::updateTocFromHtml);
-    connect(&document, SIGNAL(headerChanged(const QString&)),
-            this, SLOT(updateCurHeader(const QString &)));
-    connect(&document, &VDocument::keyPressed,
-            this, &VEditTab::handleWebKeyPressed);
-    page->setWebChannel(channel);
-
-    QString jsFile, extraFile;
-    switch (mdConverterType) {
-    case MarkdownConverterType::Marked:
-        jsFile = "qrc" + VNote::c_markedJsFile;
-        extraFile = "<script src=\"qrc" + VNote::c_markedExtraFile + "\"></script>\n";
-        break;
-
-    case MarkdownConverterType::Hoedown:
-        jsFile = "qrc" + VNote::c_hoedownJsFile;
-        // Use Marked to highlight code blocks.
-        extraFile = "<script src=\"qrc" + VNote::c_markedExtraFile + "\"></script>\n";
-        break;
-
-    case MarkdownConverterType::MarkdownIt:
-        jsFile = "qrc" + VNote::c_markdownitJsFile;
-        extraFile = "<script src=\"qrc" + VNote::c_markdownitExtraFile + "\"></script>\n" +
-                    "<script src=\"qrc" + VNote::c_markdownitAnchorExtraFile + "\"></script>\n" +
-                    "<script src=\"qrc" + VNote::c_markdownitTaskListExtraFile + "\"></script>\n";
-        break;
-
-    case MarkdownConverterType::Showdown:
-        jsFile = "qrc" + VNote::c_showdownJsFile;
-        extraFile = "<script src=\"qrc" + VNote::c_showdownExtraFile + "\"></script>\n" +
-                    "<script src=\"qrc" + VNote::c_showdownAnchorExtraFile + "\"></script>\n";
-
-        break;
-
-    default:
-        Q_ASSERT(false);
-    }
-
-    if (vconfig.getEnableMermaid()) {
-        extraFile += "<link rel=\"stylesheet\" type=\"text/css\" href=\"qrc" + VNote::c_mermaidCssFile +
-                     "\"/>\n" + "<script src=\"qrc" + VNote::c_mermaidApiJsFile + "\"></script>\n" +
-                     "<script>var VEnableMermaid = true;</script>\n";
-    }
-
-    if (vconfig.getEnableMathjax()) {
-        extraFile += "<script type=\"text/x-mathjax-config\">"
-                     "MathJax.Hub.Config({\n"
-                     "                    tex2jax: {inlineMath: [['$','$'], ['\\\\(','\\\\)']]},\n"
-                     "                    showProcessingMessages: false,\n"
-                     "                    messageStyle: \"none\"});\n"
-                     "</script>\n"
-                     "<script type=\"text/javascript\" async src=\"" + VNote::c_mathjaxJsFile + "\"></script>\n" +
-                     "<script>var VEnableMathjax = true;</script>\n";
-    }
-
-    if (vconfig.getEnableImageCaption()) {
-        extraFile += "<script>var VEnableImageCaption = true;</script>\n";
-    }
-
-    QString htmlTemplate = VNote::s_markdownTemplate;
-    htmlTemplate.replace(jsHolder, jsFile);
-    if (!extraFile.isEmpty()) {
-        htmlTemplate.replace(extraHolder, extraFile);
-    }
-
-    webPreviewer->setHtml(htmlTemplate, m_file->getBaseUrl());
-    addWidget(webPreviewer);
-}
-
 void VEditTab::focusTab()
 {
-    currentWidget()->setFocus();
+    focusChild();
     emit getFocused();
 }
 
-void VEditTab::handleFocusChanged(QWidget * /* old */, QWidget *now)
-{
-    if (isChild(now)) {
-        if (now == this) {
-            // When VEditTab get focus, it should focus to current widget.
-            currentWidget()->setFocus();
-        }
-        emit getFocused();
-    }
-}
-
-void VEditTab::updateTocFromHtml(const QString &tocHtml)
+bool VEditTab::isEditMode() const
 {
-    if (isEditMode) {
-        return;
-    }
-    tableOfContent.type = VHeaderType::Anchor;
-    QVector<VHeader> &headers = tableOfContent.headers;
-    headers.clear();
-
-    if (!tocHtml.isEmpty()) {
-        QXmlStreamReader xml(tocHtml);
-        if (xml.readNextStartElement()) {
-            if (xml.name() == "ul") {
-                parseTocUl(xml, headers, 1);
-            } else {
-                qWarning() << "TOC HTML does not start with <ul>";
-            }
-        }
-        if (xml.hasError()) {
-            qWarning() << "fail to parse TOC in HTML";
-            return;
-        }
-    }
-
-    tableOfContent.filePath = m_file->retrivePath();
-    tableOfContent.valid = true;
-
-    emit outlineChanged(tableOfContent);
+    return m_isEditMode;
 }
 
-void VEditTab::updateTocFromHeaders(const QVector<VHeader> &headers)
+bool VEditTab::isModified() const
 {
-    if (!isEditMode) {
-        return;
-    }
-    tableOfContent.type = VHeaderType::LineNumber;
-    tableOfContent.headers = headers;
-    tableOfContent.filePath = m_file->retrivePath();
-    tableOfContent.valid = true;
-
-    emit outlineChanged(tableOfContent);
+    return m_modified;
 }
 
-void VEditTab::parseTocUl(QXmlStreamReader &xml, QVector<VHeader> &headers, int level)
+VFile *VEditTab::getFile() const
 {
-    Q_ASSERT(xml.isStartElement() && xml.name() == "ul");
-
-    while (xml.readNextStartElement()) {
-        if (xml.name() == "li") {
-            parseTocLi(xml, headers, level);
-        } else {
-            qWarning() << "TOC HTML <ul> should contain <li>" << xml.name();
-            break;
-        }
-    }
+    return m_file;
 }
 
-void VEditTab::parseTocLi(QXmlStreamReader &xml, QVector<VHeader> &headers, int level)
+void VEditTab::handleFocusChanged(QWidget * /* p_old */, QWidget *p_now)
 {
-    Q_ASSERT(xml.isStartElement() && xml.name() == "li");
+    if (p_now == this) {
+        // When VEditTab get focus, it should focus to current widget.
+        focusChild();
 
-    if (xml.readNextStartElement()) {
-        if (xml.name() == "a") {
-            QString anchor = xml.attributes().value("href").toString();
-            QString name;
-            if (xml.readNext()) {
-                if (xml.tokenString() == "Characters") {
-                    name = xml.text().toString();
-                } else if (!xml.isEndElement()) {
-                    qWarning() << "TOC HTML <a> should be ended by </a>" << xml.name();
-                    return;
-                }
-                VHeader header(level, name, anchor, -1);
-                headers.append(header);
-            } else {
-                // Error
-                return;
-            }
-        } else if (xml.name() == "ul") {
-            // Such as header 3 under header 1 directly
-            VHeader header(level, "[EMPTY]", "#", -1);
-            headers.append(header);
-            parseTocUl(xml, headers, level + 1);
-        } else {
-            qWarning() << "TOC HTML <li> should contain <a> or <ul>" << xml.name();
-            return;
-        }
-    }
-
-    while (xml.readNext()) {
-        if (xml.isEndElement()) {
-            if (xml.name() == "li") {
-                return;
-            }
-            continue;
-        }
-        if (xml.name() == "ul") {
-            // Nested unordered list
-            parseTocUl(xml, headers, level + 1);
-        } else {
-            return;
-        }
+        emit getFocused();
+    } else if (isAncestorOf(p_now)) {
+        emit getFocused();
     }
 }
 
 void VEditTab::requestUpdateCurHeader()
 {
-    emit curHeaderChanged(curHeader);
+    emit curHeaderChanged(m_curHeader);
 }
 
 void VEditTab::requestUpdateOutline()
 {
-    checkToc();
-    emit outlineChanged(tableOfContent);
+    emit outlineChanged(m_toc);
 }
 
-void VEditTab::scrollToAnchor(const VAnchor &anchor)
-{
-    if (anchor == curHeader) {
-        return;
-    }
-    curHeader = anchor;
-    if (isEditMode) {
-        if (anchor.lineNumber > -1) {
-            m_textEditor->scrollToLine(anchor.lineNumber);
-        }
-    } else {
-        if (!anchor.anchor.isEmpty()) {
-            document.scrollToAnchor(anchor.anchor.mid(1));
-        }
-    }
-}
-
-void VEditTab::updateCurHeader(const QString &anchor)
+void VEditTab::wheelEvent(QWheelEvent *p_event)
 {
-    if (isEditMode || curHeader.anchor.mid(1) == anchor) {
-        return;
-    }
-    curHeader = VAnchor(m_file->retrivePath(), "#" + anchor, -1);
-    if (!anchor.isEmpty()) {
-        if (checkToc()) {
-            emit outlineChanged(tableOfContent);
-        }
-        const QVector<VHeader> &headers = tableOfContent.headers;
-        for (int i = 0; i < headers.size(); ++i) {
-            if (headers[i].anchor == curHeader.anchor) {
-                curHeader.m_outlineIndex = i;
-                break;
-            }
-        }
-        emit curHeaderChanged(curHeader);
-    }
-}
+    QPoint angle = p_event->angleDelta();
+    Qt::KeyboardModifiers modifiers = p_event->modifiers();
+    if (!angle.isNull() && (angle.y() != 0) && (modifiers & Qt::ControlModifier)) {
+        // Zoom in/out current tab.
+        zoom(angle.y() > 0);
 
-void VEditTab::updateCurHeader(int p_lineNumber, int p_outlineIndex)
-{
-    if (!isEditMode || curHeader.lineNumber == p_lineNumber) {
+        p_event->accept();
         return;
     }
-    if (checkToc()) {
-        emit outlineChanged(tableOfContent);
-    }
-    curHeader = VAnchor(m_file->retrivePath(), "", p_lineNumber);
-    curHeader.m_outlineIndex = p_outlineIndex;
-    if (p_lineNumber > -1) {
-        emit curHeaderChanged(curHeader);
-    }
-}
-
-void VEditTab::insertImage()
-{
-    qDebug() << "insert image";
-    if (!isEditMode) {
-        return;
-    }
-    m_textEditor->insertImage();
-}
 
-void VEditTab::findText(const QString &p_text, uint p_options, bool p_peek,
-                        bool p_forward)
-{
-    if (isEditMode || !webPreviewer) {
-        if (p_peek) {
-            m_textEditor->peekText(p_text, p_options);
-        } else {
-            m_textEditor->findText(p_text, p_options, p_forward);
-        }
-    } else {
-        findTextInWebView(p_text, p_options, p_peek, p_forward);
-    }
-}
-
-void VEditTab::replaceText(const QString &p_text, uint p_options,
-                           const QString &p_replaceText, bool p_findNext)
-{
-    if (isEditMode) {
-        m_textEditor->replaceText(p_text, p_options, p_replaceText, p_findNext);
-    }
-}
-
-void VEditTab::replaceTextAll(const QString &p_text, uint p_options,
-                              const QString &p_replaceText)
-{
-    if (isEditMode) {
-        m_textEditor->replaceTextAll(p_text, p_options, p_replaceText);
-    }
-}
-
-void VEditTab::findTextInWebView(const QString &p_text, uint p_options,
-                                 bool /* p_peek */, bool p_forward)
-{
-    Q_ASSERT(webPreviewer);
-    QWebEnginePage::FindFlags flags;
-    if (p_options & FindOption::CaseSensitive) {
-        flags |= QWebEnginePage::FindCaseSensitively;
-    }
-    if (!p_forward) {
-        flags |= QWebEnginePage::FindBackward;
-    }
-    webPreviewer->findText(p_text, flags);
-}
-
-QString VEditTab::getSelectedText() const
-{
-    if (isEditMode || !webPreviewer) {
-        QTextCursor cursor = m_textEditor->textCursor();
-        return cursor.selectedText();
-    } else {
-        return webPreviewer->selectedText();
-    }
-}
-
-void VEditTab::clearSearchedWordHighlight()
-{
-    if (webPreviewer) {
-        webPreviewer->findText("");
-    }
-    if (m_textEditor) {
-        m_textEditor->clearSearchedWordHighlight();
-    }
-}
-
-bool VEditTab::checkToc()
-{
-    bool ret = false;
-    if (tableOfContent.filePath != m_file->retrivePath()) {
-        tableOfContent.filePath = m_file->retrivePath();
-        ret = true;
-    }
-    return ret;
-}
-
-void VEditTab::handleWebKeyPressed(int p_key, bool p_ctrl, bool /* p_shift */)
-{
-    Q_ASSERT(webPreviewer);
-    switch (p_key) {
-    // Esc
-    case 27:
-        m_editArea->getFindReplaceDialog()->closeDialog();
-        break;
-
-    // Dash
-    case 189:
-        if (p_ctrl) {
-            // Zoom out.
-            zoomWebPage(false);
-        }
-        break;
-
-    // Equal
-    case 187:
-        if (p_ctrl) {
-            // Zoom in.
-            zoomWebPage(true);
-        }
-        break;
-
-    // 0
-    case 48:
-        if (p_ctrl) {
-            // Recover zoom.
-            webPreviewer->setZoomFactor(1);
-        }
-        break;
-
-    default:
-        break;
-    }
-}
-
-void VEditTab::wheelEvent(QWheelEvent *p_event)
-{
-    if (!isEditMode && webPreviewer) {
-        QPoint angle = p_event->angleDelta();
-        Qt::KeyboardModifiers modifiers = p_event->modifiers();
-        if (!angle.isNull() && (angle.y() != 0) && (modifiers & Qt::ControlModifier)) {
-            zoomWebPage(angle.y() > 0);
-            p_event->accept();
-            return;
-        }
-    }
     p_event->ignore();
 }
-
-void VEditTab::zoomWebPage(bool p_zoomIn, qreal p_step)
-{
-    Q_ASSERT(webPreviewer);
-    qreal curFactor = webPreviewer->zoomFactor();
-    qreal newFactor = p_zoomIn ? curFactor + p_step : curFactor - p_step;
-    if (newFactor < c_webZoomFactorMin) {
-        newFactor = c_webZoomFactorMin;
-    } else if (newFactor > c_webZoomFactorMax) {
-        newFactor = c_webZoomFactorMax;
-    }
-    webPreviewer->setZoomFactor(newFactor);
-}
-
-VWebView *VEditTab::getWebViewer() const
-{
-    return webPreviewer;
-}
-
-MarkdownConverterType VEditTab::getMarkdownConverterType() const
-{
-    return mdConverterType;
-}

+ 67 - 102
src/vedittab.h

@@ -1,136 +1,101 @@
 #ifndef VEDITTAB_H
 #define VEDITTAB_H
 
-#include <QStackedWidget>
+#include <QWidget>
 #include <QString>
 #include <QPointer>
-#include "vconstants.h"
-#include "vdocument.h"
-#include "vmarkdownconverter.h"
-#include "vconfigmanager.h"
-#include "vedit.h"
 #include "vtoc.h"
 #include "vfile.h"
 
-class VWebView;
-class VNote;
-class QXmlStreamReader;
 class VEditArea;
 
-class VEditTab : public QStackedWidget
+// VEditTab is the base class of an edit tab inside VEditWindow.
+class VEditTab : public QWidget
 {
     Q_OBJECT
+
 public:
-    VEditTab(VFile *p_file, OpenFileMode p_mode, QWidget *p_parent = 0);
-    ~VEditTab();
-    void init(VEditArea *p_editArea);
-    bool closeFile(bool p_forced);
-    // Enter read mode
-    void readFile();
-    // Save file
-    bool saveFile();
-
-    inline bool getIsEditMode() const;
-    inline bool isModified() const;
+    VEditTab(VFile *p_file, VEditArea *p_editArea, QWidget *p_parent = 0);
+
+    virtual ~VEditTab();
+
+    // Close current tab.
+    // @p_forced: if true, discard the changes.
+    virtual bool closeFile(bool p_forced) = 0;
+
+    // Enter read mode.
+    virtual void readFile() = 0;
+
+    // Save file.
+    virtual bool saveFile() = 0;
+
+    bool isEditMode() const;
+
+    bool isModified() const;
+
     void focusTab();
-    void requestUpdateOutline();
-    void requestUpdateCurHeader();
-    void scrollToAnchor(const VAnchor& anchor);
-    inline VFile *getFile();
-    void insertImage();
+
+    virtual void requestUpdateOutline();
+
+    virtual void requestUpdateCurHeader();
+
+    // Scroll to anchor @p_anchor.
+    virtual void scrollToAnchor(const VAnchor& p_anchor) = 0;
+
+    VFile *getFile() const;
+
+    // User requests to insert image.
+    virtual void insertImage() = 0;
+
     // Search @p_text in current note.
-    void findText(const QString &p_text, uint p_options, bool p_peek,
-                  bool p_forward = true);
+    virtual void findText(const QString &p_text, uint p_options, bool p_peek,
+                          bool p_forward = true) = 0;
+
     // Replace @p_text with @p_replaceText in current note.
-    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);
-    QString getSelectedText() const;
-    void clearSearchedWordHighlight();
+    virtual void replaceText(const QString &p_text, uint p_options,
+                             const QString &p_replaceText, bool p_findNext) = 0;
+
+    virtual void replaceTextAll(const QString &p_text, uint p_options,
+                                const QString &p_replaceText) = 0;
 
-    VWebView *getWebViewer() const;
+    // Return selected text.
+    virtual QString getSelectedText() const = 0;
 
-    MarkdownConverterType getMarkdownConverterType() const;
+    virtual void clearSearchedWordHighlight() = 0;
 
 public slots:
     // Enter edit mode
-    void editFile();
+    virtual void editFile() = 0;
 
 protected:
     void wheelEvent(QWheelEvent *p_event) Q_DECL_OVERRIDE;
 
-signals:
-    void getFocused();
-    void outlineChanged(const VToc &toc);
-    void curHeaderChanged(const VAnchor &anchor);
-    void statusChanged();
+    // Called when VEditTab get focus. Should focus the proper child widget.
+    virtual void focusChild() {}
 
-private slots:
-    void handleFocusChanged(QWidget *old, QWidget *now);
-    void updateTocFromHtml(const QString &tocHtml);
-    void updateCurHeader(const QString &anchor);
-    void updateCurHeader(int p_lineNumber, int p_outlineIndex);
-    void updateTocFromHeaders(const QVector<VHeader> &headers);
-    void handleTextChanged();
-    void noticeStatusChanged();
-    void handleWebKeyPressed(int p_key, bool p_ctrl, bool p_shift);
-    void saveAndRead();
-    void discardAndRead();
-
-private:
-    void setupUI();
-    void showFileReadMode();
-    void showFileEditMode();
-    void setupMarkdownPreview();
-    void previewByConverter();
-    inline bool isChild(QObject *obj);
-    void parseTocUl(QXmlStreamReader &xml, QVector<VHeader> &headers, int level);
-    void parseTocLi(QXmlStreamReader &xml, QVector<VHeader> &headers, int level);
-    void scrollPreviewToHeader(int p_outlineIndex);
-    void findTextInWebView(const QString &p_text, uint p_options, bool p_peek,
-                           bool p_forward);
-    // Check if @tableOfContent is outdated (such as renaming the file).
-    // Return true if we need to update toc.
-    bool checkToc();
-    void zoomWebPage(bool p_zoomIn, qreal p_step = 0.25);
+    // Called to zoom in/out content.
+    virtual void zoom(bool p_zoomIn, qreal p_step = 0.25) = 0;
 
+    // File related to this tab.
     QPointer<VFile> m_file;
-    bool isEditMode;
-    VEdit *m_textEditor;
-    VWebView *webPreviewer;
-    VDocument document;
-    MarkdownConverterType mdConverterType;
-    VToc tableOfContent;
-    VAnchor curHeader;
-    bool m_fileModified;
+    bool m_isEditMode;
+    bool m_modified;
+    VToc m_toc;
+    VAnchor m_curHeader;
     VEditArea *m_editArea;
-};
 
-inline bool VEditTab::getIsEditMode() const
-{
-    return isEditMode;
-}
+signals:
+    void getFocused();
 
-inline bool VEditTab::isModified() const
-{
-    return m_textEditor->isModified();
-}
+    void outlineChanged(const VToc &p_toc);
 
-inline bool VEditTab::isChild(QObject *obj)
-{
-    while (obj) {
-        if (obj == this) {
-            return true;
-        }
-        obj = obj->parent();
-    }
-    return false;
-}
-
-inline VFile *VEditTab::getFile()
-{
-    return m_file;
-}
+    void curHeaderChanged(const VAnchor &p_anchor);
+
+    void statusChanged();
+
+private slots:
+    // Called when app focus changed.
+    void handleFocusChanged(QWidget *p_old, QWidget *p_now);
+};
 
 #endif // VEDITTAB_H

+ 20 - 4
src/veditwindow.cpp

@@ -9,6 +9,8 @@
 #include "vmainwindow.h"
 #include "veditarea.h"
 #include "vopenedlistmenu.h"
+#include "vmdtab.h"
+#include "vhtmltab.h"
 
 extern VConfigManager vconfig;
 
@@ -144,6 +146,7 @@ int VEditWindow::openFile(VFile *p_file, OpenFileMode p_mode)
         goto out;
     }
     idx = openFileInTab(p_file, p_mode);
+
 out:
     setCurrentIndex(idx);
     focusWindow();
@@ -242,8 +245,21 @@ bool VEditWindow::closeAllFiles(bool p_forced)
 
 int VEditWindow::openFileInTab(VFile *p_file, OpenFileMode p_mode)
 {
-    VEditTab *editor = new VEditTab(p_file, p_mode);
-    editor->init(m_editArea);
+    VEditTab *editor = NULL;
+    switch (p_file->getDocType()) {
+    case DocType::Markdown:
+        editor = new VMdTab(p_file, m_editArea, p_mode, this);
+        break;
+
+    case DocType::Html:
+        editor = new VHtmlTab(p_file, m_editArea, p_mode, this);
+        break;
+
+    default:
+        V_ASSERT(false);
+        break;
+    }
+
     connect(editor, &VEditTab::getFocused,
             this, &VEditWindow::getFocused);
     connect(editor, &VEditTab::outlineChanged,
@@ -325,7 +341,7 @@ void VEditWindow::noticeTabStatus(int p_index)
 
     VEditTab *editor = getTab(p_index);
     const VFile *file = editor->getFile();
-    bool editMode = editor->getIsEditMode();
+    bool editMode = editor->isEditMode();
     emit tabStatusChanged(file, editor, editMode);
 }
 
@@ -333,7 +349,7 @@ void VEditWindow::updateTabInfo(int p_index)
 {
     VEditTab *editor = getTab(p_index);
     const VFile *file = editor->getFile();
-    bool editMode = editor->getIsEditMode();
+    bool editMode = editor->isEditMode();
 
     setTabText(p_index, generateTabText(p_index, file->getName(),
                                         file->isModified(), file->isModifiable()));

+ 246 - 0
src/vhtmltab.cpp

@@ -0,0 +1,246 @@
+#include <QtWidgets>
+#include <QFileInfo>
+#include "vhtmltab.h"
+#include "vedit.h"
+#include "utils/vutils.h"
+#include "vconfigmanager.h"
+#include "vnotebook.h"
+#include "dialog/vfindreplacedialog.h"
+#include "veditarea.h"
+#include "vconstants.h"
+
+extern VConfigManager vconfig;
+
+VHtmlTab::VHtmlTab(VFile *p_file, VEditArea *p_editArea,
+                   OpenFileMode p_mode, QWidget *p_parent)
+    : VEditTab(p_file, p_editArea, p_parent)
+{
+    V_ASSERT(m_file->getDocType() == DocType::Html);
+
+    m_file->open();
+
+    setupUI();
+
+    if (p_mode == OpenFileMode::Edit) {
+        showFileEditMode();
+    } else {
+        showFileReadMode();
+    }
+}
+
+void VHtmlTab::setupUI()
+{
+    m_editor = new VEdit(m_file, this);
+    connect(m_editor, &VEdit::textChanged,
+            this, &VHtmlTab::handleTextChanged);
+    connect(m_editor, &VEdit::saveAndRead,
+            this, &VHtmlTab::saveAndRead);
+    connect(m_editor, &VEdit::discardAndRead,
+            this, &VHtmlTab::discardAndRead);
+    connect(m_editor, &VEdit::editNote,
+            this, &VHtmlTab::editFile);
+    m_editor->reloadFile();
+
+    QVBoxLayout *mainLayout = new QVBoxLayout();
+    mainLayout->addWidget(m_editor);
+    setLayout(mainLayout);
+}
+
+void VHtmlTab::handleTextChanged()
+{
+    V_ASSERT(m_file->isModifiable());
+
+    if (m_modified) {
+        return;
+    }
+
+    noticeStatusChanged();
+}
+
+void VHtmlTab::noticeStatusChanged()
+{
+    m_modified = m_file->isModified();
+
+    emit statusChanged();
+}
+
+void VHtmlTab::showFileReadMode()
+{
+    m_isEditMode = false;
+
+    m_editor->setReadOnly(true);
+
+    noticeStatusChanged();
+}
+
+void VHtmlTab::showFileEditMode()
+{
+    if (!m_file->isModifiable()) {
+        return;
+    }
+
+    m_isEditMode = true;
+
+    m_editor->beginEdit();
+    m_editor->setFocus();
+
+    noticeStatusChanged();
+}
+
+bool VHtmlTab::closeFile(bool p_forced)
+{
+    if (p_forced && m_isEditMode) {
+        // Discard buffer content
+        m_editor->reloadFile();
+        m_editor->endEdit();
+
+        showFileReadMode();
+    } else {
+        readFile();
+    }
+
+    return !m_isEditMode;
+}
+
+void VHtmlTab::editFile()
+{
+    if (m_isEditMode || !m_file->isModifiable()) {
+        return;
+    }
+
+    showFileEditMode();
+}
+
+void VHtmlTab::readFile()
+{
+    if (!m_isEditMode) {
+        return;
+    }
+
+    if (m_editor && m_editor->isModified()) {
+        // Prompt to save the changes.
+        int ret = VUtils::showMessage(QMessageBox::Information, tr("Information"),
+                                      tr("Note <span style=\"%1\">%2</span> has been modified.")
+                                        .arg(vconfig.c_dataTextStyle).arg(m_file->getName()),
+                                      tr("Do you want to save your changes?"),
+                                      QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
+                                      QMessageBox::Save, this);
+        switch (ret) {
+        case QMessageBox::Save:
+            saveFile();
+            // Fall through
+
+        case QMessageBox::Discard:
+            m_editor->reloadFile();
+            break;
+
+        case QMessageBox::Cancel:
+            // Nothing to do if user cancel this action
+            return;
+
+        default:
+            qWarning() << "wrong return value from QMessageBox:" << ret;
+            return;
+        }
+    }
+
+    if (m_editor) {
+        m_editor->endEdit();
+    }
+
+    showFileReadMode();
+}
+
+bool VHtmlTab::saveFile()
+{
+    if (!m_isEditMode || !m_editor->isModified()) {
+        return true;
+    }
+
+    bool ret;
+    // Make sure the file already exists. Temporary deal with cases when user delete or move
+    // a file.
+    QString filePath = m_file->retrivePath();
+    if (!QFileInfo::exists(filePath)) {
+        qWarning() << filePath << "being written has been removed";
+        VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to save note."),
+                            tr("File <span style=\"%1\">%2</span> being written has been removed.")
+                              .arg(vconfig.c_dataTextStyle).arg(filePath),
+                            QMessageBox::Ok, QMessageBox::Ok, this);
+        return false;
+    }
+
+    m_editor->saveFile();
+    ret = m_file->save();
+    if (!ret) {
+        VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to save note."),
+                            tr("Fail to write to disk when saving a note. Please try it again."),
+                            QMessageBox::Ok, QMessageBox::Ok, this);
+        m_editor->setModified(true);
+    }
+
+    noticeStatusChanged();
+
+    return ret;
+}
+
+void VHtmlTab::saveAndRead()
+{
+    saveFile();
+    readFile();
+}
+
+void VHtmlTab::discardAndRead()
+{
+    readFile();
+}
+
+void VHtmlTab::scrollToAnchor(const VAnchor &p_anchor)
+{
+}
+
+void VHtmlTab::insertImage()
+{
+}
+
+void VHtmlTab::findText(const QString &p_text, uint p_options, bool p_peek,
+                        bool p_forward)
+{
+    if (p_peek) {
+        m_editor->peekText(p_text, p_options);
+    } else {
+        m_editor->findText(p_text, p_options, p_forward);
+    }
+}
+
+void VHtmlTab::replaceText(const QString &p_text, uint p_options,
+                           const QString &p_replaceText, bool p_findNext)
+{
+    if (m_isEditMode) {
+        m_editor->replaceText(p_text, p_options, p_replaceText, p_findNext);
+    }
+}
+
+void VHtmlTab::replaceTextAll(const QString &p_text, uint p_options,
+                              const QString &p_replaceText)
+{
+    if (m_isEditMode) {
+        m_editor->replaceTextAll(p_text, p_options, p_replaceText);
+    }
+}
+
+QString VHtmlTab::getSelectedText() const
+{
+    QTextCursor cursor = m_editor->textCursor();
+    return cursor.selectedText();
+}
+
+void VHtmlTab::clearSearchedWordHighlight()
+{
+    m_editor->clearSearchedWordHighlight();
+}
+
+void VHtmlTab::zoom(bool p_zoomIn, qreal p_step)
+{
+}
+

+ 81 - 0
src/vhtmltab.h

@@ -0,0 +1,81 @@
+#ifndef VHTMLTAB_H
+#define VHTMLTAB_H
+
+#include <QString>
+#include <QPointer>
+#include "vedittab.h"
+#include "vconstants.h"
+
+class VEdit;
+
+class VHtmlTab : public VEditTab
+{
+    Q_OBJECT
+
+public:
+    VHtmlTab(VFile *p_file, VEditArea *p_editArea, OpenFileMode p_mode, QWidget *p_parent = 0);
+
+    // Close current tab.
+    // @p_forced: if true, discard the changes.
+    bool closeFile(bool p_forced) Q_DECL_OVERRIDE;
+
+    // Enter read mode.
+    // Will prompt user to save the changes.
+    void readFile() Q_DECL_OVERRIDE;
+
+    // Save file.
+    bool saveFile() Q_DECL_OVERRIDE;
+
+    // Scroll to anchor @p_anchor.
+    void scrollToAnchor(const VAnchor& p_anchor) Q_DECL_OVERRIDE;
+
+    void insertImage() Q_DECL_OVERRIDE;
+
+    // Search @p_text in current note.
+    void findText(const QString &p_text, uint p_options, bool p_peek,
+                  bool p_forward = true) Q_DECL_OVERRIDE;
+
+    // Replace @p_text with @p_replaceText in current note.
+    void replaceText(const QString &p_text, uint p_options,
+                     const QString &p_replaceText, bool p_findNext) Q_DECL_OVERRIDE;
+
+    void replaceTextAll(const QString &p_text, uint p_options,
+                        const QString &p_replaceText) Q_DECL_OVERRIDE;
+
+    QString getSelectedText() const Q_DECL_OVERRIDE;
+
+    void clearSearchedWordHighlight() Q_DECL_OVERRIDE;
+
+public slots:
+    // Enter edit mode.
+    void editFile() Q_DECL_OVERRIDE;
+
+private slots:
+    // Handle text changed in m_editor.
+    void handleTextChanged();
+
+    // Emit statusChanged() signal to notify that status of this tab has changed.
+    void noticeStatusChanged();
+
+    // m_editor requests to save changes and enter read mode.
+    void saveAndRead();
+
+    // m_editor requests to discard changes and enter read mode.
+    void discardAndRead();
+
+private:
+    // Setup UI.
+    void setupUI();
+
+    // Show the file content in read mode.
+    void showFileReadMode();
+
+    // Show the file content in edit mode.
+    void showFileEditMode();
+
+    // Called to zoom in/out content.
+    void zoom(bool p_zoomIn, qreal p_step = 0.25) Q_DECL_OVERRIDE;
+
+    VEdit *m_editor;
+};
+#endif // VHTMLTAB_H

+ 17 - 10
src/vmainwindow.cpp

@@ -19,6 +19,7 @@
 #include "vedittab.h"
 #include "vwebview.h"
 #include "vexporter.h"
+#include "vmdtab.h"
 
 extern VConfigManager vconfig;
 
@@ -1408,16 +1409,19 @@ void VMainWindow::printNote()
 
     V_ASSERT(m_curTab);
 
-    VWebView *webView = m_curTab->getWebViewer();
+    if (m_curFile->getDocType() == DocType::Markdown) {
+        VMdTab *mdTab = dynamic_cast<VMdTab *>((VEditTab *)m_curTab);
+        VWebView *webView = mdTab->getWebViewer();
 
-    V_ASSERT(webView);
+        V_ASSERT(webView);
 
-    if (webView->hasSelection()) {
-        dialog.addEnabledOption(QAbstractPrintDialog::PrintSelection);
-    }
+        if (webView->hasSelection()) {
+            dialog.addEnabledOption(QAbstractPrintDialog::PrintSelection);
+        }
 
-    if (dialog.exec() != QDialog::Accepted) {
-        return;
+        if (dialog.exec() != QDialog::Accepted) {
+            return;
+        }
     }
 }
 
@@ -1426,8 +1430,11 @@ void VMainWindow::exportAsPDF()
     V_ASSERT(m_curTab);
     V_ASSERT(m_curFile);
 
-    VExporter exporter(m_curTab->getMarkdownConverterType(), this);
-    exporter.exportNote(m_curFile, ExportType::PDF);
-    exporter.exec();
+    if (m_curFile->getDocType() == DocType::Markdown) {
+        VMdTab *mdTab = dynamic_cast<VMdTab *>((VEditTab *)m_curTab);
+        VExporter exporter(mdTab->getMarkdownConverterType(), this);
+        exporter.exportNote(m_curFile, ExportType::PDF);
+        exporter.exec();
+    }
 }
 

+ 673 - 0
src/vmdtab.cpp

@@ -0,0 +1,673 @@
+#include <QtWidgets>
+#include <QWebChannel>
+#include <QFileInfo>
+#include <QXmlStreamReader>
+#include "vmdtab.h"
+#include "vdocument.h"
+#include "vnote.h"
+#include "utils/vutils.h"
+#include "vpreviewpage.h"
+#include "hgmarkdownhighlighter.h"
+#include "vconfigmanager.h"
+#include "vmarkdownconverter.h"
+#include "vnotebook.h"
+#include "vtoc.h"
+#include "vmdedit.h"
+#include "dialog/vfindreplacedialog.h"
+#include "veditarea.h"
+#include "vconstants.h"
+#include "vwebview.h"
+
+extern VConfigManager vconfig;
+
+VMdTab::VMdTab(VFile *p_file, VEditArea *p_editArea,
+               OpenFileMode p_mode, QWidget *p_parent)
+    : VEditTab(p_file, p_editArea, p_parent), m_editor(NULL), m_webViewer(NULL),
+      m_document(NULL), m_mdConType(vconfig.getMdConverterType())
+{
+    V_ASSERT(m_file->getDocType() == DocType::Markdown);
+
+    m_file->open();
+
+    setupUI();
+
+    if (p_mode == OpenFileMode::Edit) {
+        showFileEditMode();
+    } else {
+        showFileReadMode();
+    }
+}
+
+void VMdTab::setupUI()
+{
+    m_stacks = new QStackedLayout(this);
+
+    setupMarkdownViewer();
+
+    if (m_file->isModifiable()) {
+        m_editor = new VMdEdit(m_file, m_document, m_mdConType, this);
+        connect(dynamic_cast<VMdEdit *>(m_editor), &VMdEdit::headersChanged,
+                this, &VMdTab::updateTocFromHeaders);
+        connect(dynamic_cast<VMdEdit *>(m_editor), &VMdEdit::statusChanged,
+                this, &VMdTab::noticeStatusChanged);
+        connect(m_editor, SIGNAL(curHeaderChanged(int, int)),
+                this, SLOT(updateCurHeader(int, int)));
+        connect(m_editor, &VEdit::textChanged,
+                this, &VMdTab::handleTextChanged);
+        connect(m_editor, &VEdit::saveAndRead,
+                this, &VMdTab::saveAndRead);
+        connect(m_editor, &VEdit::discardAndRead,
+                this, &VMdTab::discardAndRead);
+
+        m_editor->reloadFile();
+        m_stacks->addWidget(m_editor);
+    } else {
+        m_editor = NULL;
+    }
+
+    setLayout(m_stacks);
+}
+
+void VMdTab::handleTextChanged()
+{
+    V_ASSERT(m_file->isModifiable());
+
+    if (m_modified) {
+        return;
+    }
+
+    noticeStatusChanged();
+}
+
+void VMdTab::noticeStatusChanged()
+{
+    m_modified = m_file->isModified();
+
+    emit statusChanged();
+}
+
+void VMdTab::showFileReadMode()
+{
+    m_isEditMode = false;
+
+    int outlineIndex = m_curHeader.m_outlineIndex;
+    if (m_mdConType == MarkdownConverterType::Hoedown) {
+        viewWebByConverter();
+    } else {
+        m_document->updateText();
+        updateTocFromHtml(m_document->getToc());
+    }
+
+    m_stacks->setCurrentWidget(m_webViewer);
+    clearSearchedWordHighlight();
+    scrollWebViewToHeader(outlineIndex);
+
+    noticeStatusChanged();
+}
+
+void VMdTab::scrollWebViewToHeader(int p_outlineIndex)
+{
+    V_ASSERT(p_outlineIndex >= 0);
+
+    if (p_outlineIndex < m_toc.headers.size()) {
+        QString anchor = m_toc.headers[p_outlineIndex].anchor;
+        if (!anchor.isEmpty()) {
+            m_document->scrollToAnchor(anchor.mid(1));
+        }
+    }
+}
+
+void VMdTab::viewWebByConverter()
+{
+    VMarkdownConverter mdConverter;
+    QString toc;
+    QString html = mdConverter.generateHtml(m_file->getContent(),
+                                            vconfig.getMarkdownExtensions(),
+                                            toc);
+    m_document->setHtml(html);
+    updateTocFromHtml(toc);
+}
+
+void VMdTab::showFileEditMode()
+{
+    if (!m_file->isModifiable()) {
+        return;
+    }
+
+    m_isEditMode = true;
+
+    // beginEdit() may change m_curHeader.
+    int outlineIndex = m_curHeader.m_outlineIndex;
+
+    m_editor->beginEdit();
+    m_stacks->setCurrentWidget(m_editor);
+
+    dynamic_cast<VMdEdit *>(m_editor)->scrollToHeader(outlineIndex);
+    m_editor->setFocus();
+
+    noticeStatusChanged();
+}
+
+bool VMdTab::closeFile(bool p_forced)
+{
+    if (p_forced && m_isEditMode) {
+        // Discard buffer content
+        m_editor->reloadFile();
+        m_editor->endEdit();
+
+        showFileReadMode();
+    } else {
+        readFile();
+    }
+
+    return !m_isEditMode;
+}
+
+void VMdTab::editFile()
+{
+    if (m_isEditMode || !m_file->isModifiable()) {
+        return;
+    }
+
+    showFileEditMode();
+}
+
+void VMdTab::readFile()
+{
+    if (!m_isEditMode) {
+        return;
+    }
+
+    if (m_editor && m_editor->isModified()) {
+        // Prompt to save the changes.
+        int ret = VUtils::showMessage(QMessageBox::Information, tr("Information"),
+                                      tr("Note <span style=\"%1\">%2</span> has been modified.")
+                                        .arg(vconfig.c_dataTextStyle).arg(m_file->getName()),
+                                      tr("Do you want to save your changes?"),
+                                      QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
+                                      QMessageBox::Save, this);
+        switch (ret) {
+        case QMessageBox::Save:
+            saveFile();
+            // Fall through
+
+        case QMessageBox::Discard:
+            m_editor->reloadFile();
+            break;
+
+        case QMessageBox::Cancel:
+            // Nothing to do if user cancel this action
+            return;
+
+        default:
+            qWarning() << "wrong return value from QMessageBox:" << ret;
+            return;
+        }
+    }
+
+    if (m_editor) {
+        m_editor->endEdit();
+    }
+
+    showFileReadMode();
+}
+
+bool VMdTab::saveFile()
+{
+    if (!m_isEditMode || !m_editor->isModified()) {
+        return true;
+    }
+
+    bool ret;
+    // Make sure the file already exists. Temporary deal with cases when user delete or move
+    // a file.
+    QString filePath = m_file->retrivePath();
+    if (!QFileInfo::exists(filePath)) {
+        qWarning() << filePath << "being written has been removed";
+        VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to save note."),
+                            tr("File <span style=\"%1\">%2</span> being written has been removed.")
+                              .arg(vconfig.c_dataTextStyle).arg(filePath),
+                            QMessageBox::Ok, QMessageBox::Ok, this);
+        return false;
+    }
+
+    m_editor->saveFile();
+    ret = m_file->save();
+    if (!ret) {
+        VUtils::showMessage(QMessageBox::Warning, tr("Warning"), tr("Fail to save note."),
+                            tr("Fail to write to disk when saving a note. Please try it again."),
+                            QMessageBox::Ok, QMessageBox::Ok, this);
+        m_editor->setModified(true);
+    }
+
+    noticeStatusChanged();
+
+    return ret;
+}
+
+void VMdTab::saveAndRead()
+{
+    saveFile();
+    readFile();
+}
+
+void VMdTab::discardAndRead()
+{
+    readFile();
+}
+
+QString VMdTab::fillHtmlTemplate() const
+{
+    const QString &jsHolder = c_htmlJSHolder;
+    const QString &extraHolder = c_htmlExtraHolder;
+
+    QString jsFile, extraFile;
+    switch (m_mdConType) {
+    case MarkdownConverterType::Marked:
+        jsFile = "qrc" + VNote::c_markedJsFile;
+        extraFile = "<script src=\"qrc" + VNote::c_markedExtraFile + "\"></script>\n";
+        break;
+
+    case MarkdownConverterType::Hoedown:
+        jsFile = "qrc" + VNote::c_hoedownJsFile;
+        // Use Marked to highlight code blocks.
+        extraFile = "<script src=\"qrc" + VNote::c_markedExtraFile + "\"></script>\n";
+        break;
+
+    case MarkdownConverterType::MarkdownIt:
+        jsFile = "qrc" + VNote::c_markdownitJsFile;
+        extraFile = "<script src=\"qrc" + VNote::c_markdownitExtraFile + "\"></script>\n" +
+                    "<script src=\"qrc" + VNote::c_markdownitAnchorExtraFile + "\"></script>\n" +
+                    "<script src=\"qrc" + VNote::c_markdownitTaskListExtraFile + "\"></script>\n";
+        break;
+
+    case MarkdownConverterType::Showdown:
+        jsFile = "qrc" + VNote::c_showdownJsFile;
+        extraFile = "<script src=\"qrc" + VNote::c_showdownExtraFile + "\"></script>\n" +
+                    "<script src=\"qrc" + VNote::c_showdownAnchorExtraFile + "\"></script>\n";
+
+        break;
+
+    default:
+        Q_ASSERT(false);
+    }
+
+    if (vconfig.getEnableMermaid()) {
+        extraFile += "<link rel=\"stylesheet\" type=\"text/css\" href=\"qrc" + VNote::c_mermaidCssFile +
+                     "\"/>\n" + "<script src=\"qrc" + VNote::c_mermaidApiJsFile + "\"></script>\n" +
+                     "<script>var VEnableMermaid = true;</script>\n";
+    }
+
+    if (vconfig.getEnableMathjax()) {
+        extraFile += "<script type=\"text/x-mathjax-config\">"
+                     "MathJax.Hub.Config({\n"
+                     "                    tex2jax: {inlineMath: [['$','$'], ['\\\\(','\\\\)']]},\n"
+                     "                    showProcessingMessages: false,\n"
+                     "                    messageStyle: \"none\"});\n"
+                     "</script>\n"
+                     "<script type=\"text/javascript\" async src=\"" + VNote::c_mathjaxJsFile + "\"></script>\n" +
+                     "<script>var VEnableMathjax = true;</script>\n";
+    }
+
+    if (vconfig.getEnableImageCaption()) {
+        extraFile += "<script>var VEnableImageCaption = true;</script>\n";
+    }
+
+    QString htmlTemplate = VNote::s_markdownTemplate;
+    htmlTemplate.replace(jsHolder, jsFile);
+    if (!extraFile.isEmpty()) {
+        htmlTemplate.replace(extraHolder, extraFile);
+    }
+
+    return htmlTemplate;
+}
+
+void VMdTab::setupMarkdownViewer()
+{
+    m_webViewer = new VWebView(m_file, this);
+    connect(m_webViewer, &VWebView::editNote,
+            this, &VMdTab::editFile);
+
+    VPreviewPage *page = new VPreviewPage(m_webViewer);
+    m_webViewer->setPage(page);
+    m_webViewer->setZoomFactor(vconfig.getWebZoomFactor());
+
+    m_document = new VDocument(m_file, m_webViewer);
+
+    QWebChannel *channel = new QWebChannel(m_webViewer);
+    channel->registerObject(QStringLiteral("content"), m_document);
+    connect(m_document, &VDocument::tocChanged,
+            this, &VMdTab::updateTocFromHtml);
+    connect(m_document, SIGNAL(headerChanged(const QString&)),
+            this, SLOT(updateCurHeader(const QString &)));
+    connect(m_document, &VDocument::keyPressed,
+            this, &VMdTab::handleWebKeyPressed);
+    page->setWebChannel(channel);
+
+    m_webViewer->setHtml(fillHtmlTemplate(), m_file->getBaseUrl());
+
+    m_stacks->addWidget(m_webViewer);
+}
+
+static void parseTocUl(QXmlStreamReader &p_xml, QVector<VHeader> &p_headers,
+                       int p_level);
+
+static void parseTocLi(QXmlStreamReader &p_xml, QVector<VHeader> &p_headers, int p_level)
+{
+    Q_ASSERT(p_xml.isStartElement() && p_xml.name() == "li");
+
+    if (p_xml.readNextStartElement()) {
+        if (p_xml.name() == "a") {
+            QString anchor = p_xml.attributes().value("href").toString();
+            QString name;
+            if (p_xml.readNext()) {
+                if (p_xml.tokenString() == "Characters") {
+                    name = p_xml.text().toString();
+                } else if (!p_xml.isEndElement()) {
+                    qWarning() << "TOC HTML <a> should be ended by </a>" << p_xml.name();
+                    return;
+                }
+
+                VHeader header(p_level, name, anchor, -1);
+                p_headers.append(header);
+            } else {
+                // Error
+                return;
+            }
+        } else if (p_xml.name() == "ul") {
+            // Such as header 3 under header 1 directly
+            VHeader header(p_level, "[EMPTY]", "#", -1);
+            p_headers.append(header);
+            parseTocUl(p_xml, p_headers, p_level + 1);
+        } else {
+            qWarning() << "TOC HTML <li> should contain <a> or <ul>" << p_xml.name();
+            return;
+        }
+    }
+
+    while (p_xml.readNext()) {
+        if (p_xml.isEndElement()) {
+            if (p_xml.name() == "li") {
+                return;
+            }
+            continue;
+        }
+        if (p_xml.name() == "ul") {
+            // Nested unordered list
+            parseTocUl(p_xml, p_headers, p_level + 1);
+        } else {
+            return;
+        }
+    }
+}
+
+static void parseTocUl(QXmlStreamReader &p_xml, QVector<VHeader> &p_headers,
+                       int p_level)
+{
+    Q_ASSERT(p_xml.isStartElement() && p_xml.name() == "ul");
+
+    while (p_xml.readNextStartElement()) {
+        if (p_xml.name() == "li") {
+            parseTocLi(p_xml, p_headers, p_level);
+        } else {
+            qWarning() << "TOC HTML <ul> should contain <li>" << p_xml.name();
+            break;
+        }
+    }
+}
+
+static bool parseTocHtml(const QString &p_tocHtml,
+                         QVector<VHeader> &p_headers)
+{
+    if (!p_tocHtml.isEmpty()) {
+        QXmlStreamReader xml(p_tocHtml);
+        if (xml.readNextStartElement()) {
+            if (xml.name() == "ul") {
+                parseTocUl(xml, p_headers, 1);
+            } else {
+                qWarning() << "TOC HTML does not start with <ul>";
+            }
+        }
+
+        if (xml.hasError()) {
+            qWarning() << "fail to parse TOC in HTML";
+            return false;
+        }
+    }
+
+    return true;
+}
+
+void VMdTab::updateTocFromHtml(const QString &p_tocHtml)
+{
+    if (m_isEditMode) {
+        return;
+    }
+
+    m_toc.type = VHeaderType::Anchor;
+    m_toc.headers.clear();
+
+    if (!parseTocHtml(p_tocHtml, m_toc.headers)) {
+        return;
+    }
+
+    m_toc.filePath = m_file->retrivePath();
+    m_toc.valid = true;
+
+    emit outlineChanged(m_toc);
+}
+
+void VMdTab::updateTocFromHeaders(const QVector<VHeader> &p_headers)
+{
+    if (!m_isEditMode) {
+        return;
+    }
+
+    m_toc.type = VHeaderType::LineNumber;
+    m_toc.headers = p_headers;
+    m_toc.filePath = m_file->retrivePath();
+    m_toc.valid = true;
+
+    emit outlineChanged(m_toc);
+}
+
+void VMdTab::scrollToAnchor(const VAnchor &p_anchor)
+{
+    if (p_anchor == m_curHeader) {
+        return;
+    }
+
+    m_curHeader = p_anchor;
+    if (m_isEditMode) {
+        if (p_anchor.lineNumber > -1) {
+            m_editor->scrollToLine(p_anchor.lineNumber);
+        }
+    } else {
+        if (!p_anchor.anchor.isEmpty()) {
+            m_document->scrollToAnchor(p_anchor.anchor.mid(1));
+        }
+    }
+}
+
+void VMdTab::updateCurHeader(const QString &p_anchor)
+{
+    if (m_isEditMode || m_curHeader.anchor.mid(1) == p_anchor) {
+        return;
+    }
+
+    m_curHeader = VAnchor(m_file->retrivePath(), "#" + p_anchor, -1);
+    if (!p_anchor.isEmpty()) {
+        const QVector<VHeader> &headers = m_toc.headers;
+        for (int i = 0; i < headers.size(); ++i) {
+            if (headers[i].anchor == m_curHeader.anchor) {
+                m_curHeader.m_outlineIndex = i;
+                break;
+            }
+        }
+
+        emit curHeaderChanged(m_curHeader);
+    }
+}
+
+void VMdTab::updateCurHeader(int p_lineNumber, int p_outlineIndex)
+{
+    if (!m_isEditMode || m_curHeader.lineNumber == p_lineNumber) {
+        return;
+    }
+
+    m_curHeader = VAnchor(m_file->retrivePath(), "", p_lineNumber);
+    m_curHeader.m_outlineIndex = p_outlineIndex;
+    if (p_lineNumber > -1) {
+        emit curHeaderChanged(m_curHeader);
+    }
+}
+
+void VMdTab::insertImage()
+{
+    if (!m_isEditMode) {
+        return;
+    }
+
+    m_editor->insertImage();
+}
+
+void VMdTab::findText(const QString &p_text, uint p_options, bool p_peek,
+                      bool p_forward)
+{
+    if (m_isEditMode || !m_webViewer) {
+        if (p_peek) {
+            m_editor->peekText(p_text, p_options);
+        } else {
+            m_editor->findText(p_text, p_options, p_forward);
+        }
+    } else {
+        findTextInWebView(p_text, p_options, p_peek, p_forward);
+    }
+}
+
+void VMdTab::replaceText(const QString &p_text, uint p_options,
+                         const QString &p_replaceText, bool p_findNext)
+{
+    if (m_isEditMode) {
+        m_editor->replaceText(p_text, p_options, p_replaceText, p_findNext);
+    }
+}
+
+void VMdTab::replaceTextAll(const QString &p_text, uint p_options,
+                            const QString &p_replaceText)
+{
+    if (m_isEditMode) {
+        m_editor->replaceTextAll(p_text, p_options, p_replaceText);
+    }
+}
+
+void VMdTab::findTextInWebView(const QString &p_text, uint p_options,
+                               bool /* p_peek */, bool p_forward)
+{
+    V_ASSERT(m_webViewer);
+
+    QWebEnginePage::FindFlags flags;
+    if (p_options & FindOption::CaseSensitive) {
+        flags |= QWebEnginePage::FindCaseSensitively;
+    }
+
+    if (!p_forward) {
+        flags |= QWebEnginePage::FindBackward;
+    }
+
+    m_webViewer->findText(p_text, flags);
+}
+
+QString VMdTab::getSelectedText() const
+{
+    if (m_isEditMode || !m_webViewer) {
+        QTextCursor cursor = m_editor->textCursor();
+        return cursor.selectedText();
+    } else {
+        return m_webViewer->selectedText();
+    }
+}
+
+void VMdTab::clearSearchedWordHighlight()
+{
+    if (m_webViewer) {
+        m_webViewer->findText("");
+    }
+
+    if (m_editor) {
+        m_editor->clearSearchedWordHighlight();
+    }
+}
+
+void VMdTab::handleWebKeyPressed(int p_key, bool p_ctrl, bool /* p_shift */)
+{
+    V_ASSERT(m_webViewer);
+
+    switch (p_key) {
+    // Esc
+    case 27:
+        m_editArea->getFindReplaceDialog()->closeDialog();
+        break;
+
+    // Dash
+    case 189:
+        if (p_ctrl) {
+            // Zoom out.
+            zoomWebPage(false);
+        }
+        break;
+
+    // Equal
+    case 187:
+        if (p_ctrl) {
+            // Zoom in.
+            zoomWebPage(true);
+        }
+        break;
+
+    // 0
+    case 48:
+        if (p_ctrl) {
+            // Recover zoom.
+            m_webViewer->setZoomFactor(1);
+        }
+        break;
+
+    default:
+        break;
+    }
+}
+
+void VMdTab::zoom(bool p_zoomIn, qreal p_step)
+{
+    if (m_isEditMode) {
+        // TODO
+    } else {
+        zoomWebPage(p_zoomIn, p_step);
+    }
+}
+
+void VMdTab::zoomWebPage(bool p_zoomIn, qreal p_step)
+{
+    V_ASSERT(m_webViewer);
+
+    qreal curFactor = m_webViewer->zoomFactor();
+    qreal newFactor = p_zoomIn ? curFactor + p_step : curFactor - p_step;
+    if (newFactor < c_webZoomFactorMin) {
+        newFactor = c_webZoomFactorMin;
+    } else if (newFactor > c_webZoomFactorMax) {
+        newFactor = c_webZoomFactorMax;
+    }
+
+    m_webViewer->setZoomFactor(newFactor);
+}
+
+VWebView *VMdTab::getWebViewer() const
+{
+    return m_webViewer;
+}
+
+MarkdownConverterType VMdTab::getMarkdownConverterType() const
+{
+    return m_mdConType;
+}

+ 130 - 0
src/vmdtab.h

@@ -0,0 +1,130 @@
+#ifndef VMDTAB_H
+#define VMDTAB_H
+
+#include <QString>
+#include <QPointer>
+#include "vedittab.h"
+#include "vconstants.h"
+#include "vmarkdownconverter.h"
+#include "vconfigmanager.h"
+
+class VWebView;
+class QStackedLayout;
+class VEdit;
+class VDocument;
+
+class VMdTab : public VEditTab
+{
+    Q_OBJECT
+
+public:
+    VMdTab(VFile *p_file, VEditArea *p_editArea, OpenFileMode p_mode, QWidget *p_parent = 0);
+
+    // Close current tab.
+    // @p_forced: if true, discard the changes.
+    bool closeFile(bool p_forced) Q_DECL_OVERRIDE;
+
+    // Enter read mode.
+    // Will prompt user to save the changes.
+    void readFile() Q_DECL_OVERRIDE;
+
+    // Save file.
+    bool saveFile() Q_DECL_OVERRIDE;
+
+    // Scroll to anchor @p_anchor.
+    void scrollToAnchor(const VAnchor& p_anchor) Q_DECL_OVERRIDE;
+
+    void insertImage() Q_DECL_OVERRIDE;
+
+    // Search @p_text in current note.
+    void findText(const QString &p_text, uint p_options, bool p_peek,
+                  bool p_forward = true) Q_DECL_OVERRIDE;
+
+    // Replace @p_text with @p_replaceText in current note.
+    void replaceText(const QString &p_text, uint p_options,
+                     const QString &p_replaceText, bool p_findNext) Q_DECL_OVERRIDE;
+
+    void replaceTextAll(const QString &p_text, uint p_options,
+                        const QString &p_replaceText) Q_DECL_OVERRIDE;
+
+    QString getSelectedText() const Q_DECL_OVERRIDE;
+
+    void clearSearchedWordHighlight() Q_DECL_OVERRIDE;
+
+    VWebView *getWebViewer() const;
+
+    MarkdownConverterType getMarkdownConverterType() const;
+
+public slots:
+    // Enter edit mode.
+    void editFile() Q_DECL_OVERRIDE;
+
+private slots:
+    // Handle text changed in m_editor.
+    void handleTextChanged();
+
+    // Emit statusChanged() signal to notify that status of this tab has changed.
+    void noticeStatusChanged();
+
+    // Update m_toc according to @p_tocHtml for read mode.
+    void updateTocFromHtml(const QString &p_tocHtml);
+
+    // Update m_toc accroding to @p_headers for edit mode.
+    void updateTocFromHeaders(const QVector<VHeader> &p_headers);
+
+    // Web viewer requests to update current header.
+    void updateCurHeader(const QString &p_anchor);
+
+    // Editor requests to update current header.
+    void updateCurHeader(int p_lineNumber, int p_outlineIndex);
+
+    // Handle key press event in Web view.
+    void handleWebKeyPressed(int p_key, bool p_ctrl, bool p_shift);
+
+    // m_editor requests to save changes and enter read mode.
+    void saveAndRead();
+
+    // m_editor requests to discard changes and enter read mode.
+    void discardAndRead();
+
+private:
+    // Setup UI.
+    void setupUI();
+
+    // Show the file content in read mode.
+    void showFileReadMode();
+
+    // Show the file content in edit mode.
+    void showFileEditMode();
+
+    // Generate HTML template for Web view.
+    QString fillHtmlTemplate() const;
+
+    // Setup Markdown viewer.
+    void setupMarkdownViewer();
+
+    // Use VMarkdownConverter (hoedown) to generate the Web view.
+    void viewWebByConverter();
+
+    // Scroll Web view to given header.
+    // @p_outlineIndex is the index in m_toc.headers.
+    void scrollWebViewToHeader(int p_outlineIndex);
+
+    // Search text in Web view.
+    void findTextInWebView(const QString &p_text, uint p_options, bool p_peek,
+                           bool p_forward);
+
+    // Called to zoom in/out content.
+    void zoom(bool p_zoomIn, qreal p_step = 0.25) Q_DECL_OVERRIDE;
+
+    // Zoom Web View.
+    void zoomWebPage(bool p_zoomIn, qreal p_step = 0.25);
+
+    VEdit *m_editor;
+    VWebView *m_webViewer;
+    VDocument *m_document;
+    MarkdownConverterType m_mdConType;
+
+    QStackedLayout *m_stacks;
+};
+#endif // VMDTAB_H