Browse Source

refactor outline logics

Le Tan 8 years ago
parent
commit
183b24915a
27 changed files with 933 additions and 720 deletions
  1. 4 4
      src/src.pro
  2. 11 0
      src/vdocument.h
  3. 6 5
      src/vedit.cpp
  4. 3 2
      src/vedit.h
  5. 15 14
      src/veditarea.cpp
  6. 14 6
      src/veditarea.h
  7. 22 31
      src/vedittab.cpp
  8. 21 17
      src/vedittab.h
  9. 12 3
      src/vedittabinfo.h
  10. 38 45
      src/veditwindow.cpp
  11. 20 8
      src/veditwindow.h
  12. 7 7
      src/vfilesessioninfo.cpp
  13. 3 3
      src/vfilesessioninfo.h
  14. 1 16
      src/vhtmltab.cpp
  15. 0 6
      src/vhtmltab.h
  16. 6 3
      src/vmainwindow.cpp
  17. 1 0
      src/vmainwindow.h
  18. 50 90
      src/vmdedit.cpp
  19. 16 16
      src/vmdedit.h
  20. 117 212
      src/vmdtab.cpp
  21. 14 18
      src/vmdtab.h
  22. 55 112
      src/voutline.cpp
  23. 35 18
      src/voutline.h
  24. 176 0
      src/vtableofcontent.cpp
  25. 286 0
      src/vtableofcontent.h
  26. 0 6
      src/vtoc.cpp
  27. 0 78
      src/vtoc.h

+ 4 - 4
src/src.pro

@@ -44,7 +44,6 @@ SOURCES += main.cpp\
     veditwindow.cpp \
     vedittab.cpp \
     voutline.cpp \
-    vtoc.cpp \
     vsingleinstanceguard.cpp \
     vdirectory.cpp \
     vfile.cpp \
@@ -78,7 +77,8 @@ SOURCES += main.cpp\
     vnotefile.cpp \
     vattachmentlist.cpp \
     dialog/vsortdialog.cpp \
-    vfilesessioninfo.cpp
+    vfilesessioninfo.cpp \
+    vtableofcontent.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -108,7 +108,6 @@ HEADERS  += vmainwindow.h \
     veditwindow.h \
     vedittab.h \
     voutline.h \
-    vtoc.h \
     vsingleinstanceguard.h \
     vdirectory.h \
     vfile.h \
@@ -144,7 +143,8 @@ HEADERS  += vmainwindow.h \
     vnotefile.h \
     vattachmentlist.h \
     dialog/vsortdialog.h \
-    vfilesessioninfo.h
+    vfilesessioninfo.h \
+    vtableofcontent.h
 
 RESOURCES += \
     vnote.qrc \

+ 11 - 0
src/vdocument.h

@@ -16,9 +16,15 @@ class VDocument : public QObject
 public:
     // @p_file could be NULL.
     VDocument(const VFile *p_file, QObject *p_parent = 0);
+
     QString getToc();
+
+    // Scroll to @anchor in the web.
+    // @anchor is the id without '#', like "toc_1". If empty, will scroll to top.
     void scrollToAnchor(const QString &anchor);
+
     void setHtml(const QString &html);
+
     // Request to highlight a segment text.
     // Use p_id to identify the result.
     void highlightTextAsync(const QString &p_text, int p_id, int p_timeStamp);
@@ -35,6 +41,7 @@ public slots:
 
     // When the Web view has been scrolled, it will signal current header anchor.
     // Empty @anchor to indicate an invalid header.
+    // The header does not begins with '#'.
     void setHeader(const QString &anchor);
 
     void setLog(const QString &p_log);
@@ -49,8 +56,12 @@ public slots:
 
 signals:
     void textChanged(const QString &text);
+
     void tocChanged(const QString &toc);
+
     void requestScrollToAnchor(const QString &anchor);
+
+    // @anchor is the id of that anchor, without '#'.
     void headerChanged(const QString &anchor);
     void htmlChanged(const QString &html);
     void logChanged(const QString &p_log);

+ 6 - 5
src/vedit.cpp

@@ -4,7 +4,7 @@
 #include "vedit.h"
 #include "vnote.h"
 #include "vconfigmanager.h"
-#include "vtoc.h"
+#include "vtableofcontent.h"
 #include "utils/vutils.h"
 #include "utils/veditutils.h"
 #include "veditoperations.h"
@@ -157,15 +157,16 @@ void VEdit::reloadFile()
     setModified(false);
 }
 
-void VEdit::scrollToLine(int p_lineNumber)
+bool VEdit::scrollToBlock(int p_blockNumber)
 {
-    Q_ASSERT(p_lineNumber >= 0);
-
-    QTextBlock block = document()->findBlockByLineNumber(p_lineNumber);
+    QTextBlock block = document()->findBlockByNumber(p_blockNumber);
     if (block.isValid()) {
         VEditUtils::scrollBlockInPage(this, block.blockNumber(), 0);
         moveCursor(QTextCursor::EndOfBlock);
+        return true;
     }
+
+    return false;
 }
 
 bool VEdit::isModified() const

+ 3 - 2
src/vedit.h

@@ -10,7 +10,6 @@
 #include <QRect>
 #include <QFontMetrics>
 #include "vconstants.h"
-#include "vtoc.h"
 #include "vnotefile.h"
 
 class VEditOperations;
@@ -81,7 +80,9 @@ public:
     virtual void setModified(bool p_modified);
     bool isModified() const;
     virtual void reloadFile();
-    virtual void scrollToLine(int p_lineNumber);
+
+    virtual bool scrollToBlock(int p_blockNumber);
+
     // User requests to insert an image.
     virtual void insertImage();
 

+ 15 - 14
src/veditarea.cpp

@@ -77,9 +77,9 @@ void VEditArea::insertSplitWindow(int idx)
     connect(win, &VEditWindow::getFocused,
             this, &VEditArea::handleWindowFocused);
     connect(win, &VEditWindow::outlineChanged,
-            this, &VEditArea::handleOutlineChanged);
-    connect(win, &VEditWindow::curHeaderChanged,
-            this, &VEditArea::handleCurHeaderChanged);
+            this, &VEditArea::handleWindowOutlineChanged);
+    connect(win, &VEditWindow::currentHeaderChanged,
+            this, &VEditArea::handleWindowCurrentHeaderChanged);
     connect(win, &VEditWindow::statusMessage,
             this, &VEditArea::handleWindowStatusMessage);
     connect(win, &VEditWindow::vimStatusUpdated,
@@ -237,15 +237,13 @@ void VEditArea::updateWindowStatus()
         Q_ASSERT(splitter->count() == 0);
 
         emit tabStatusUpdated(VEditTabInfo());
-        emit outlineChanged(VToc());
-        emit curHeaderChanged(VAnchor());
+        emit outlineChanged(VTableOfContent());
+        emit currentHeaderChanged(VHeaderPointer());
         return;
     }
 
     VEditWindow *win = getWindow(curWindowIndex);
     win->updateTabStatus();
-    win->requestUpdateOutline();
-    win->requestUpdateCurHeader();
 }
 
 bool VEditArea::closeFile(const VFile *p_file, bool p_forced)
@@ -436,26 +434,28 @@ void VEditArea::handleWindowFocused()
     }
 }
 
-void VEditArea::handleOutlineChanged(const VToc &toc)
+void VEditArea::handleWindowOutlineChanged(const VTableOfContent &p_outline)
 {
     QObject *winObject = sender();
     if (splitter->widget(curWindowIndex) == winObject) {
-        emit outlineChanged(toc);
+        emit outlineChanged(p_outline);
     }
 }
 
-void VEditArea::handleCurHeaderChanged(const VAnchor &anchor)
+void VEditArea::handleWindowCurrentHeaderChanged(const VHeaderPointer &p_header)
 {
     QObject *winObject = sender();
     if (splitter->widget(curWindowIndex) == winObject) {
-        emit curHeaderChanged(anchor);
+        emit currentHeaderChanged(p_header);
     }
 }
 
-void VEditArea::handleOutlineItemActivated(const VAnchor &anchor)
+void VEditArea::scrollToHeader(const VHeaderPointer &p_header)
 {
-    // Notice current window
-    getWindow(curWindowIndex)->scrollCurTab(anchor);
+    VEditWindow *win = getCurrentWindow();
+    if (win) {
+        win->scrollToHeader(p_header);
+    }
 }
 
 bool VEditArea::isFileOpened(const VFile *p_file)
@@ -648,6 +648,7 @@ VEditWindow *VEditArea::getCurrentWindow() const
     if (curWindowIndex < 0) {
         return NULL;
     }
+
     return getWindow(curWindowIndex);
 }
 

+ 14 - 6
src/veditarea.h

@@ -12,7 +12,6 @@
 #include <QSplitter>
 #include "vnotebook.h"
 #include "veditwindow.h"
-#include "vtoc.h"
 #include "vnavigationmode.h"
 
 class VNote;
@@ -82,8 +81,11 @@ signals:
     // Emit when current window's tab status updated.
     void tabStatusUpdated(const VEditTabInfo &p_info);
 
-    void outlineChanged(const VToc &toc);
-    void curHeaderChanged(const VAnchor &anchor);
+    // Emit when current window's tab's outline changed.
+    void outlineChanged(const VTableOfContent &p_outline);
+
+    // Emit when current window's tab's current header changed.
+    void currentHeaderChanged(const VHeaderPointer &p_header);
 
     // Emit when want to show message in status bar.
     void statusMessage(const QString &p_msg);
@@ -106,7 +108,10 @@ public slots:
     void saveFile();
     void readFile();
     void saveAndReadFile();
-    void handleOutlineItemActivated(const VAnchor &anchor);
+
+    // Scroll current tab to @p_header.
+    void scrollToHeader(const VHeaderPointer &p_header);
+
     void handleFileUpdated(const VFile *p_file);
     void handleDirectoryUpdated(const VDirectory *p_dir);
     void handleNotebookUpdated(const VNotebook *p_notebook);
@@ -118,8 +123,11 @@ private slots:
 
     void handleRemoveSplitRequest(VEditWindow *curWindow);
     void handleWindowFocused();
-    void handleOutlineChanged(const VToc &toc);
-    void handleCurHeaderChanged(const VAnchor &anchor);
+
+    void handleWindowOutlineChanged(const VTableOfContent &p_outline);
+
+    void handleWindowCurrentHeaderChanged(const VHeaderPointer &p_header);
+
     void handleFindTextChanged(const QString &p_text, uint p_options);
     void handleFindOptionChanged(uint p_options);
     void handleFindNext(const QString &p_text, uint p_options, bool p_forward);

+ 22 - 31
src/vedittab.cpp

@@ -3,12 +3,13 @@
 #include <QWheelEvent>
 
 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)
+    : QWidget(p_parent),
+      m_file(p_file),
+      m_isEditMode(false),
+      m_outline(p_file),
+      m_currentHeader(p_file, -1),
+      m_editArea(p_editArea)
 {
-    m_toc.m_file = m_file;
-    m_curHeader.m_file = m_file;
-
     connect(qApp, &QApplication::focusChanged,
             this, &VEditTab::handleFocusChanged);
 }
@@ -33,7 +34,7 @@ bool VEditTab::isEditMode() const
 
 bool VEditTab::isModified() const
 {
-    return m_modified;
+    return m_file->isModified();
 }
 
 VFile *VEditTab::getFile() const
@@ -53,16 +54,6 @@ void VEditTab::handleFocusChanged(QWidget * /* p_old */, QWidget *p_now)
     }
 }
 
-void VEditTab::requestUpdateCurHeader()
-{
-    emit curHeaderChanged(m_curHeader);
-}
-
-void VEditTab::requestUpdateOutline()
-{
-    emit outlineChanged(m_toc);
-}
-
 void VEditTab::wheelEvent(QWheelEvent *p_event)
 {
     QPoint angle = p_event->angleDelta();
@@ -78,41 +69,41 @@ void VEditTab::wheelEvent(QWheelEvent *p_event)
     p_event->ignore();
 }
 
-void VEditTab::updateStatus()
+VEditTabInfo VEditTab::fetchTabInfo() const
 {
-    m_modified = m_file->isModified();
+    VEditTabInfo info;
+    info.m_editTab = const_cast<VEditTab *>(this);
 
-    emit statusUpdated(fetchTabInfo());
+    return info;
 }
 
-VEditTabInfo VEditTab::fetchTabInfo()
+const VHeaderPointer &VEditTab::getCurrentHeader() const
 {
-    VEditTabInfo info;
-    info.m_editTab = this;
-
-    return info;
+    return m_currentHeader;
 }
 
-VAnchor VEditTab::getCurrentHeader() const
+const VTableOfContent &VEditTab::getOutline() const
 {
-    return m_curHeader;
+    return m_outline;
 }
 
 void VEditTab::tryRestoreFromTabInfo(const VEditTabInfo &p_info)
 {
     if (p_info.m_editTab != this) {
-        // Clear and return.
-        m_infoToRestore.m_editTab = NULL;
+        m_infoToRestore.clear();
         return;
     }
 
     if (restoreFromTabInfo(p_info)) {
-        // Clear and return.
-        m_infoToRestore.m_editTab = NULL;
+        m_infoToRestore.clear();
         return;
     }
 
     // Save it and restore later.
     m_infoToRestore = p_info;
-    qDebug() << "save info for restore later" << p_info.m_anchorIndex;
+}
+
+void VEditTab::updateStatus()
+{
+    emit statusUpdated(fetchTabInfo());
 }

+ 21 - 17
src/vedittab.h

@@ -4,7 +4,7 @@
 #include <QWidget>
 #include <QString>
 #include <QPointer>
-#include "vtoc.h"
+#include "vtableofcontent.h"
 #include "vfile.h"
 #include "utils/vvim.h"
 #include "vedittabinfo.h"
@@ -37,12 +37,9 @@ public:
 
     void focusTab();
 
-    virtual void requestUpdateOutline();
-
-    virtual void requestUpdateCurHeader();
-
-    // Scroll to anchor @p_anchor.
-    virtual void scrollToAnchor(const VAnchor& p_anchor) = 0;
+    // Scroll to @p_header.
+    // Will emit currentHeaderChanged() if @p_header is valid.
+    virtual void scrollToHeader(const VHeaderPointer &p_header) { Q_UNUSED(p_header) }
 
     VFile *getFile() const;
 
@@ -72,21 +69,23 @@ public:
     virtual void decorateText(TextDecoration p_decoration) {Q_UNUSED(p_decoration);}
 
     // Create a filled VEditTabInfo.
-    virtual VEditTabInfo fetchTabInfo();
+    virtual VEditTabInfo fetchTabInfo() const;
+
+    const VTableOfContent &getOutline() const;
 
-    VAnchor getCurrentHeader() const;
+    const VHeaderPointer &getCurrentHeader() const;
 
     // Restore status from @p_info.
     // If this tab is not ready yet, it will restore once it is ready.
     void tryRestoreFromTabInfo(const VEditTabInfo &p_info);
 
+    // Emit signal to update current status.
+    virtual void updateStatus();
+
 public slots:
     // Enter edit mode
     virtual void editFile() = 0;
 
-    // Update status of current tab. Emit statusUpdated().
-    virtual void updateStatus();
-
 protected:
     void wheelEvent(QWheelEvent *p_event) Q_DECL_OVERRIDE;
 
@@ -102,10 +101,15 @@ protected:
 
     // File related to this tab.
     QPointer<VFile> m_file;
+
     bool m_isEditMode;
-    bool m_modified;
-    VToc m_toc;
-    VAnchor m_curHeader;
+
+    // Table of content of this tab.
+    VTableOfContent m_outline;
+
+    // Current header in m_outline of this tab.
+    VHeaderPointer m_currentHeader;
+
     VEditArea *m_editArea;
 
     // Tab info to restore from once ready.
@@ -114,9 +118,9 @@ protected:
 signals:
     void getFocused();
 
-    void outlineChanged(const VToc &p_toc);
+    void outlineChanged(const VTableOfContent &p_outline);
 
-    void curHeaderChanged(const VAnchor &p_anchor);
+    void currentHeaderChanged(const VHeaderPointer &p_header);
 
     // The status of current tab has updates.
     void statusUpdated(const VEditTabInfo &p_info);

+ 12 - 3
src/vedittabinfo.h

@@ -10,10 +10,19 @@ struct VEditTabInfo
           m_cursorBlockNumber(-1),
           m_cursorPositionInBlock(-1),
           m_blockCount(-1),
-          m_anchorIndex(-1)
+          m_headerIndex(-1)
     {
     }
 
+    void clear()
+    {
+        m_editTab = NULL;
+        m_cursorBlockNumber = -1;
+        m_cursorPositionInBlock = -1;
+        m_blockCount = -1;
+        m_headerIndex = -1;
+    }
+
     VEditTab *m_editTab;
 
     // Cursor information. -1 for invalid info.
@@ -21,8 +30,8 @@ struct VEditTabInfo
     int m_cursorPositionInBlock;
     int m_blockCount;
 
-    // Anchor index in outline.
-    int m_anchorIndex;
+    // Header index in outline.
+    int m_headerIndex;
 };
 
 #endif // VEDITTABINFO_H

+ 38 - 45
src/veditwindow.cpp

@@ -466,15 +466,15 @@ void VEditWindow::updateTabStatus(int p_index)
 
     if (p_index == -1) {
         emit tabStatusUpdated(VEditTabInfo());
-        emit outlineChanged(VToc());
-        emit curHeaderChanged(VAnchor());
+        emit outlineChanged(VTableOfContent());
+        emit currentHeaderChanged(VHeaderPointer());
         return;
     }
 
     VEditTab *tab = getTab(p_index);
-    tab->updateStatus();
-    tab->requestUpdateOutline();
-    tab->requestUpdateCurHeader();
+    emit tabStatusUpdated(tab->fetchTabInfo());
+    emit outlineChanged(tab->getOutline());
+    emit currentHeaderChanged(tab->getCurrentHeader());
 }
 
 void VEditWindow::updateTabInfo(int p_index)
@@ -506,26 +506,24 @@ void VEditWindow::updateAllTabsSequence()
     }
 }
 
-// Be requested to report current outline
-void VEditWindow::requestUpdateOutline()
+VTableOfContent VEditWindow::getOutline() const
 {
     int idx = currentIndex();
     if (idx == -1) {
-        emit outlineChanged(VToc());
-        return;
+        return VTableOfContent();
     }
-    getTab(idx)->requestUpdateOutline();
+
+    return getTab(idx)->getOutline();
 }
 
-// Be requested to report current header
-void VEditWindow::requestUpdateCurHeader()
+VHeaderPointer VEditWindow::getCurrentHeader() const
 {
     int idx = currentIndex();
     if (idx == -1) {
-        emit curHeaderChanged(VAnchor());
-        return;
+        return VHeaderPointer();
     }
-    getTab(idx)->requestUpdateCurHeader();
+
+    return getTab(idx)->getCurrentHeader();
 }
 
 // Focus this windows. Try to focus current tab.
@@ -681,44 +679,39 @@ bool VEditWindow::canRemoveSplit()
     return splitter->count() > 1;
 }
 
-void VEditWindow::handleOutlineChanged(const VToc &p_toc)
+void VEditWindow::handleTabOutlineChanged(const VTableOfContent &p_outline)
 {
-    // Only propagate it if it is current tab
-    int idx = currentIndex();
-    if (idx == -1) {
-        emit outlineChanged(VToc());
+    // Only propagate it if it is current tab.
+    VEditTab *tab = getCurrentTab();
+    if (tab) {
+        if (tab->getFile() == p_outline.getFile()) {
+            emit outlineChanged(p_outline);
+        }
+    } else {
+        emit outlineChanged(VTableOfContent());
         return;
     }
-    const VFile *file = getTab(idx)->getFile();
-    if (p_toc.m_file == file) {
-        emit outlineChanged(p_toc);
-    }
 }
 
-void VEditWindow::handleCurHeaderChanged(const VAnchor &p_anchor)
+void VEditWindow::handleTabCurrentHeaderChanged(const VHeaderPointer &p_header)
 {
-    // Only propagate it if it is current tab
-    int idx = currentIndex();
-    if (idx == -1) {
-        emit curHeaderChanged(VAnchor());
+    // Only propagate it if it is current tab.
+    VEditTab *tab = getCurrentTab();
+    if (tab) {
+        if (tab->getFile() == p_header.m_file) {
+            emit currentHeaderChanged(p_header);
+        }
+    } else {
+        emit currentHeaderChanged(VHeaderPointer());
         return;
     }
-    const VFile *file = getTab(idx)->getFile();
-    if (p_anchor.m_file == file) {
-        emit curHeaderChanged(p_anchor);
-    }
 }
 
-void VEditWindow::scrollCurTab(const VAnchor &p_anchor)
+void VEditWindow::scrollToHeader(const VHeaderPointer &p_header)
 {
-    int idx = currentIndex();
-    if (idx == -1) {
-        emit curHeaderChanged(VAnchor());
-        return;
-    }
-    const VFile *file = getTab(idx)->getFile();
-    if (file == p_anchor.m_file) {
-        getTab(idx)->scrollToAnchor(p_anchor);
+    VEditTab *tab = getCurrentTab();
+    if (tab) {
+        tab->scrollToHeader(p_header);
     }
 }
 
@@ -916,9 +909,9 @@ void VEditWindow::connectEditTab(const VEditTab *p_tab)
     connect(p_tab, &VEditTab::getFocused,
             this, &VEditWindow::getFocused);
     connect(p_tab, &VEditTab::outlineChanged,
-            this, &VEditWindow::handleOutlineChanged);
-    connect(p_tab, &VEditTab::curHeaderChanged,
-            this, &VEditWindow::handleCurHeaderChanged);
+            this, &VEditWindow::handleTabOutlineChanged);
+    connect(p_tab, &VEditTab::currentHeaderChanged,
+            this, &VEditWindow::handleTabCurrentHeaderChanged);
     connect(p_tab, &VEditTab::statusUpdated,
             this, &VEditWindow::handleTabStatusUpdated);
     connect(p_tab, &VEditTab::statusMessage,

+ 20 - 8
src/veditwindow.h

@@ -8,7 +8,6 @@
 #include <QDir>
 #include "vnotebook.h"
 #include "vedittab.h"
-#include "vtoc.h"
 #include "vconstants.h"
 #include "vnotefile.h"
 
@@ -32,11 +31,19 @@ public:
     void readFile();
     void saveAndReadFile();
     bool closeAllFiles(bool p_forced);
-    void requestUpdateOutline();
-    void requestUpdateCurHeader();
+
+    // Return outline of current tab.
+    VTableOfContent getOutline() const;
+
+    // Return current header of current tab.
+    VHeaderPointer getCurrentHeader() const;
+
     // Focus to current tab's editor
     void focusWindow();
-    void scrollCurTab(const VAnchor &p_anchor);
+
+    // Scroll current tab to header @p_header.
+    void scrollToHeader(const VHeaderPointer &p_header);
+
     void updateFileInfo(const VFile *p_file);
     void updateDirectoryInfo(const VDirectory *p_dir);
     void updateNotebookInfo(const VNotebook *p_notebook);
@@ -84,8 +91,10 @@ signals:
     void requestRemoveSplit(VEditWindow *curWindow);
     // This widget or its children get the focus
     void getFocused();
-    void outlineChanged(const VToc &toc);
-    void curHeaderChanged(const VAnchor &anchor);
+
+    void outlineChanged(const VTableOfContent &p_outline);
+
+    void currentHeaderChanged(const VHeaderPointer &p_header);
 
     // Emit when want to show message in status bar.
     void statusMessage(const QString &p_msg);
@@ -105,8 +114,11 @@ private slots:
     void handleCurrentIndexChanged(int p_index);
     void contextMenuRequested(QPoint pos);
     void tabListJump(VFile *p_file);
-    void handleOutlineChanged(const VToc &p_toc);
-    void handleCurHeaderChanged(const VAnchor &p_anchor);
+
+    void handleTabOutlineChanged(const VTableOfContent &p_outline);
+
+    void handleTabCurrentHeaderChanged(const VHeaderPointer &p_header);
+
     void updateSplitMenu();
     void tabbarContextMenuRequested(QPoint p_pos);
     void handleLocateAct();

+ 7 - 7
src/vfilesessioninfo.cpp

@@ -3,13 +3,13 @@
 #include <QSettings>
 
 #include "vedittabinfo.h"
-#include "vtoc.h"
+#include "vtableofcontent.h"
 #include "vedittab.h"
 
 
 VFileSessionInfo::VFileSessionInfo()
     : m_mode(OpenFileMode::Read),
-      m_anchorIndex(-1),
+      m_headerIndex(-1),
       m_cursorBlockNumber(-1),
       m_cursorPositionInBlock(-1)
 {
@@ -19,7 +19,7 @@ VFileSessionInfo::VFileSessionInfo(const QString &p_file,
                                    OpenFileMode p_mode)
     : m_file(p_file),
       m_mode(p_mode),
-      m_anchorIndex(-1),
+      m_headerIndex(-1),
       m_cursorBlockNumber(-1),
       m_cursorPositionInBlock(-1)
 {
@@ -32,7 +32,7 @@ VFileSessionInfo VFileSessionInfo::fromEditTabInfo(const VEditTabInfo *p_tabInfo
     VEditTab *tab = p_tabInfo->m_editTab;
     VFileSessionInfo info(tab->getFile()->fetchPath(),
                           tab->isEditMode() ? OpenFileMode::Edit : OpenFileMode::Read);
-    info.m_anchorIndex = p_tabInfo->m_anchorIndex;
+    info.m_headerIndex = p_tabInfo->m_headerIndex;
     info.m_cursorBlockNumber = p_tabInfo->m_cursorBlockNumber;
     info.m_cursorPositionInBlock = p_tabInfo->m_cursorPositionInBlock;
 
@@ -41,7 +41,7 @@ VFileSessionInfo VFileSessionInfo::fromEditTabInfo(const VEditTabInfo *p_tabInfo
 
 void VFileSessionInfo::toEditTabInfo(VEditTabInfo *p_tabInfo) const
 {
-    p_tabInfo->m_anchorIndex = m_anchorIndex;
+    p_tabInfo->m_headerIndex = m_headerIndex;
     p_tabInfo->m_cursorBlockNumber = m_cursorBlockNumber;
     p_tabInfo->m_cursorPositionInBlock = m_cursorPositionInBlock;
 }
@@ -57,7 +57,7 @@ VFileSessionInfo VFileSessionInfo::fromSettings(const QSettings *p_settings)
         info.m_mode = OpenFileMode::Read;
     }
 
-    info.m_anchorIndex = p_settings->value(FileSessionConfig::c_anchorIndex).toInt();
+    info.m_headerIndex = p_settings->value(FileSessionConfig::c_headerIndex).toInt();
     info.m_cursorBlockNumber = p_settings->value(FileSessionConfig::c_cursorBlockNumber).toInt();
     info.m_cursorPositionInBlock = p_settings->value(FileSessionConfig::c_cursorPositionInBlock).toInt();
 
@@ -68,7 +68,7 @@ void VFileSessionInfo::toSettings(QSettings *p_settings) const
 {
     p_settings->setValue(FileSessionConfig::c_file, m_file);
     p_settings->setValue(FileSessionConfig::c_mode, (int)m_mode);
-    p_settings->setValue(FileSessionConfig::c_anchorIndex, m_anchorIndex);
+    p_settings->setValue(FileSessionConfig::c_headerIndex, m_headerIndex);
     p_settings->setValue(FileSessionConfig::c_cursorBlockNumber, m_cursorBlockNumber);
     p_settings->setValue(FileSessionConfig::c_cursorPositionInBlock, m_cursorPositionInBlock);
 }

+ 3 - 3
src/vfilesessioninfo.h

@@ -12,7 +12,7 @@ namespace FileSessionConfig
     static const QString c_mode = "mode";
 
     // Index in outline of the anchor.
-    static const QString c_anchorIndex = "anchor_index";
+    static const QString c_headerIndex = "header_index";
 
     static const QString c_cursorBlockNumber = "cursor_block_number";
     static const QString c_cursorPositionInBlock = "cursor_position_in_block";
@@ -44,8 +44,8 @@ public:
     // Mode of this file in this session.
     OpenFileMode m_mode;
 
-    // Index in outline of the anchor.
-    int m_anchorIndex;
+    // Index in outline of the header.
+    int m_headerIndex;
 
     // Block number of cursor block.
     int m_cursorBlockNumber;

+ 1 - 16
src/vhtmltab.cpp

@@ -32,7 +32,7 @@ void VHtmlTab::setupUI()
 {
     m_editor = new VEdit(m_file, this);
     connect(m_editor, &VEdit::textChanged,
-            this, &VHtmlTab::handleTextChanged);
+            this, &VHtmlTab::updateStatus);
     connect(m_editor, &VEdit::saveAndRead,
             this, &VHtmlTab::saveAndRead);
     connect(m_editor, &VEdit::discardAndRead,
@@ -52,17 +52,6 @@ void VHtmlTab::setupUI()
     setLayout(mainLayout);
 }
 
-void VHtmlTab::handleTextChanged()
-{
-    V_ASSERT(m_file->isModifiable());
-
-    if (m_modified) {
-        return;
-    }
-
-    updateStatus();
-}
-
 void VHtmlTab::showFileReadMode()
 {
     m_isEditMode = false;
@@ -194,10 +183,6 @@ void VHtmlTab::discardAndRead()
     readFile();
 }
 
-void VHtmlTab::scrollToAnchor(const VAnchor & /* p_anchor */)
-{
-}
-
 void VHtmlTab::insertImage()
 {
 }

+ 0 - 6
src/vhtmltab.h

@@ -26,9 +26,6 @@ public:
     // 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.
@@ -53,9 +50,6 @@ public slots:
     void editFile() Q_DECL_OVERRIDE;
 
 private slots:
-    // Handle text changed in m_editor.
-    void handleTextChanged();
-
     // m_editor requests to save changes and enter read mode.
     void saveAndRead();
 

+ 6 - 3
src/vmainwindow.cpp

@@ -1100,13 +1100,16 @@ void VMainWindow::initDockWindows()
     toolDock->setObjectName("tools_dock");
     toolDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
     toolBox = new QToolBox(this);
+
+    // Outline tree.
     outline = new VOutline(this);
     connect(editArea, &VEditArea::outlineChanged,
             outline, &VOutline::updateOutline);
+    connect(editArea, &VEditArea::currentHeaderChanged,
+            outline, &VOutline::updateCurrentHeader);
     connect(outline, &VOutline::outlineItemActivated,
-            editArea, &VEditArea::handleOutlineItemActivated);
-    connect(editArea, &VEditArea::curHeaderChanged,
-            outline, &VOutline::updateCurHeader);
+            editArea, &VEditArea::scrollToHeader);
+
     toolBox->addItem(outline, QIcon(":/resources/icons/outline.svg"), tr("Outline"));
     toolDock->setWidget(toolBox);
     addDockWidget(Qt::RightDockWidgetArea, toolDock);

+ 1 - 0
src/vmainwindow.h

@@ -145,6 +145,7 @@ private slots:
     void handleVimStatusUpdated(const VVim *p_vim);
 
     // Handle the status update of the current tab of VEditArea.
+    // Will be called frequently.
     void handleAreaTabStatusUpdated(const VEditTabInfo &p_info);
 
     // Check the shared memory between different instances to see if we have

+ 50 - 90
src/vmdedit.cpp

@@ -5,7 +5,7 @@
 #include "vmdeditoperations.h"
 #include "vnote.h"
 #include "vconfigmanager.h"
-#include "vtoc.h"
+#include "vtableofcontent.h"
 #include "utils/vutils.h"
 #include "utils/veditutils.h"
 #include "utils/vpreviewutils.h"
@@ -35,7 +35,7 @@ VMdEdit::VMdEdit(VFile *p_file, VDocument *p_vdoc, MarkdownConverterType p_type,
                                                 document());
 
     connect(m_mdHighlighter, &HGMarkdownHighlighter::headersUpdated,
-            this, &VMdEdit::updateOutline);
+            this, &VMdEdit::updateHeaders);
 
     // After highlight, the cursor may trun into non-visible. We should make it visible
     // in this case.
@@ -74,7 +74,7 @@ VMdEdit::VMdEdit(VFile *p_file, VDocument *p_vdoc, MarkdownConverterType p_type,
             this, &VEdit::vimStatusUpdated);
 
     connect(this, &VMdEdit::cursorPositionChanged,
-            this, &VMdEdit::updateCurHeader);
+            this, &VMdEdit::updateCurrentHeader);
 
     connect(QApplication::clipboard(), &QClipboard::changed,
             this, &VMdEdit::handleClipboardChanged);
@@ -111,7 +111,7 @@ void VMdEdit::beginEdit()
         setReadOnly(false);
     }
 
-    updateOutline(m_mdHighlighter->getHeaderRegions());
+    updateHeaders(m_mdHighlighter->getHeaderRegions());
 }
 
 void VMdEdit::endEdit()
@@ -345,43 +345,9 @@ void VMdEdit::clearUnusedImages()
     m_initImages.clear();
 }
 
-int VMdEdit::currentCursorHeader() const
+void VMdEdit::updateCurrentHeader()
 {
-    if (m_headers.isEmpty()) {
-        return -1;
-    }
-
-    int curLine = textCursor().block().firstLineNumber();
-    int i = 0;
-    for (i = m_headers.size() - 1; i >= 0; --i) {
-        if (!m_headers[i].isEmpty()) {
-            if (m_headers[i].lineNumber <= curLine) {
-                break;
-            }
-        }
-    }
-
-    if (i == -1) {
-        return -1;
-    } else {
-        Q_ASSERT(m_headers[i].index == i);
-        return i;
-    }
-}
-
-void VMdEdit::updateCurHeader()
-{
-    if (m_headers.isEmpty()) {
-        return;
-    }
-
-    int idx = currentCursorHeader();
-    if (idx == -1) {
-        emit curHeaderChanged(VAnchor(m_file, "", -1, -1));
-        return;
-    }
-
-    emit curHeaderChanged(VAnchor(m_file, "", m_headers[idx].lineNumber, m_headers[idx].index));
+    emit currentHeaderChanged(textCursor().block().blockNumber());
 }
 
 static void addHeaderSequence(QVector<int> &p_sequence, int p_level, int p_baseLevel)
@@ -448,11 +414,11 @@ static void insertSequenceToHeader(QTextBlock p_block,
     }
 }
 
-void VMdEdit::updateOutline(const QVector<VElementRegion> &p_headerRegions)
+void VMdEdit::updateHeaders(const QVector<VElementRegion> &p_headerRegions)
 {
     QTextDocument *doc = document();
 
-    QVector<VHeader> headers;
+    QVector<VTableOfContentItem> headers;
     QVector<int> headerBlockNumbers;
     QVector<QString> headerSequences;
     if (!p_headerRegions.isEmpty()) {
@@ -480,8 +446,10 @@ void VMdEdit::updateOutline(const QVector<VElementRegion> &p_headerRegions)
         if ((block.userState() == HighlightBlockState::Normal) &&
             headerReg.exactMatch(block.text())) {
             int level = headerReg.cap(1).length();
-            VHeader header(level, headerReg.cap(2).trimmed(),
-                           "", block.firstLineNumber(), headers.size());
+            VTableOfContentItem header(headerReg.cap(2).trimmed(),
+                                       level,
+                                       block.blockNumber(),
+                                       headers.size());
             headers.append(header);
             headerBlockNumbers.append(block.blockNumber());
             headerSequences.append(headerReg.cap(3));
@@ -506,22 +474,25 @@ void VMdEdit::updateOutline(const QVector<VElementRegion> &p_headerRegions)
     QRegExp preReg(VUtils::c_headerPrefixRegExp);
     int curLevel = baseLevel - 1;
     for (int i = 0; i < headers.size(); ++i) {
-        VHeader &item = headers[i];
-        while (item.level > curLevel + 1) {
+        VTableOfContentItem &item = headers[i];
+        while (item.m_level > curLevel + 1) {
             curLevel += 1;
 
             // Insert empty level which is an invalid header.
-            m_headers.append(VHeader(curLevel, c_emptyHeaderName, "", -1, m_headers.size()));
+            m_headers.append(VTableOfContentItem(c_emptyHeaderName,
+                                                 curLevel,
+                                                 -1,
+                                                 m_headers.size()));
             if (autoSequence) {
                 addHeaderSequence(seqs, curLevel, headingSequenceBaseLevel);
             }
         }
 
-        item.index = m_headers.size();
+        item.m_index = m_headers.size();
         m_headers.append(item);
-        curLevel = item.level;
+        curLevel = item.m_level;
         if (autoSequence) {
-            addHeaderSequence(seqs, item.level, headingSequenceBaseLevel);
+            addHeaderSequence(seqs, item.m_level, headingSequenceBaseLevel);
 
             QString seqStr = headerSequenceStr(seqs);
             if (headerSequences[i] != seqStr) {
@@ -536,39 +507,16 @@ void VMdEdit::updateOutline(const QVector<VElementRegion> &p_headerRegions)
 
     emit headersChanged(m_headers);
 
-    updateCurHeader();
+    updateCurrentHeader();
 }
 
-void VMdEdit::scrollToAnchor(const VAnchor &p_anchor)
+bool VMdEdit::scrollToHeader(int p_blockNumber)
 {
-    if (p_anchor.lineNumber == -1
-        || p_anchor.m_outlineIndex < 0) {
-        // Move to the start of document if m_headers is not empty.
-        // Otherwise, there is no outline, so just let it be.
-        if (!m_headers.isEmpty()) {
-            moveCursor(QTextCursor::Start);
-        }
-
-        return;
-    } else if (p_anchor.m_outlineIndex >= m_headers.size()) {
-        return;
-    }
-
-    scrollToLine(p_anchor.lineNumber);
-}
-
-bool VMdEdit::scrollToAnchor(int p_anchorIndex)
-{
-    if (p_anchorIndex >= 0 && p_anchorIndex < m_headers.size()) {
-        int lineNumber = m_headers[p_anchorIndex].lineNumber;
-        if (lineNumber >= 0) {
-            scrollToLine(lineNumber);
-
-            return true;
-        }
+    if (p_blockNumber < 0) {
+        return false;
     }
 
-    return false;
+    return scrollToBlock(p_blockNumber);
 }
 
 QString VMdEdit::toPlainTextWithoutImg()
@@ -742,9 +690,21 @@ void VMdEdit::resizeEvent(QResizeEvent *p_event)
     VEdit::resizeEvent(p_event);
 }
 
-const QVector<VHeader> &VMdEdit::getHeaders() const
+int VMdEdit::indexOfCurrentHeader() const
 {
-    return m_headers;
+    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 VMdEdit::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat)
@@ -754,11 +714,11 @@ bool VMdEdit::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat)
     }
 
     QTextCursor cursor = textCursor();
-    int cursorLine = cursor.block().firstLineNumber();
+    int cursorLine = cursor.block().blockNumber();
     int targetIdx = -1;
     // -1: skip level check.
     int targetLevel = 0;
-    int idx = currentCursorHeader();
+    int idx = indexOfCurrentHeader();
     if (idx == -1) {
         // Cursor locates at the beginning, before any headers.
         if (p_relativeLevel < 0 || !p_forward) {
@@ -775,7 +735,7 @@ bool VMdEdit::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat)
     for (targetIdx = idx == -1 ? 0 : idx;
          targetIdx >= 0 && targetIdx < m_headers.size();
          targetIdx += delta) {
-        const VHeader &header = m_headers[targetIdx];
+        const VTableOfContentItem &header = m_headers[targetIdx];
         if (header.isEmpty()) {
             continue;
         }
@@ -783,7 +743,7 @@ bool VMdEdit::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat)
         if (targetLevel == 0) {
             // The target level has not been init yet.
             Q_ASSERT(firstHeader);
-            targetLevel = header.level;
+            targetLevel = header.m_level;
             if (p_relativeLevel < 0) {
                 targetLevel += p_relativeLevel;
                 if (targetLevel < 1) {
@@ -795,9 +755,9 @@ bool VMdEdit::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat)
             }
         }
 
-        if (targetLevel == -1 || header.level == targetLevel) {
+        if (targetLevel == -1 || header.m_level == targetLevel) {
             if (firstHeader
-                && (cursorLine == header.lineNumber
+                && (cursorLine == header.m_blockNumber
                     || p_forward)
                 && idx != -1) {
                 // This header is not counted for the repeat.
@@ -809,7 +769,7 @@ bool VMdEdit::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat)
                 // Found.
                 break;
             }
-        } else if (header.level < targetLevel) {
+        } else if (header.m_level < targetLevel) {
             // Stop by higher level.
             return false;
         }
@@ -822,9 +782,9 @@ bool VMdEdit::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat)
     }
 
     // Jump to target header.
-    int line = m_headers[targetIdx].lineNumber;
+    int line = m_headers[targetIdx].m_blockNumber;
     if (line > -1) {
-        QTextBlock block = document()->findBlockByLineNumber(line);
+        QTextBlock block = document()->findBlockByNumber(line);
         if (block.isValid()) {
             cursor.setPosition(block.position());
             setTextCursor(cursor);
@@ -851,7 +811,7 @@ void VMdEdit::finishOneAsyncJob(int p_idx)
         m_freshEdit = false;
         emit statusChanged();
 
-        updateOutline(m_mdHighlighter->getHeaderRegions());
+        updateHeaders(m_mdHighlighter->getHeaderRegions());
 
         emit ready();
     }

+ 16 - 16
src/vmdedit.h

@@ -7,7 +7,7 @@
 #include <QColor>
 #include <QClipboard>
 #include <QImage>
-#include "vtoc.h"
+#include "vtableofcontent.h"
 #include "veditoperations.h"
 #include "vconfigmanager.h"
 #include "utils/vutils.h"
@@ -32,35 +32,34 @@ public:
     // @p_path is the absolute path of the inserted image.
     void imageInserted(const QString &p_path);
 
-    void scrollToAnchor(const VAnchor &p_anchor);
-
-    // Scroll to anchor given the the index in outline.
-    // Return true if @p_anchorIndex is valid.
-    bool scrollToAnchor(int p_anchorIndex);
+    // Scroll to header @p_blockNumber.
+    // Return true if @p_blockNumber is valid to scroll to.
+    bool scrollToHeader(int p_blockNumber);
 
     // Like toPlainText(), but remove image preview characters.
     QString toPlainTextWithoutImg();
 
-    const QVector<VHeader> &getHeaders() const;
-
 public slots:
     bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) Q_DECL_OVERRIDE;
 
 signals:
-    void headersChanged(const QVector<VHeader> &headers);
+    // Signal when headers change.
+    void headersChanged(const QVector<VTableOfContentItem> &p_headers);
 
     // Signal when current header change.
-    void curHeaderChanged(VAnchor p_anchor);
+    void currentHeaderChanged(int p_blockNumber);
 
     // Signal when the status of VMdEdit changed.
     // Will be emitted by VImagePreviewer for now.
     void statusChanged();
 
 private slots:
-    void updateOutline(const QVector<VElementRegion> &p_headerRegions);
+    // Update m_headers according to elements.
+    void updateHeaders(const QVector<VElementRegion> &p_headerRegions);
 
+    // Update current header according to cursor position.
     // When there is no header in current cursor, will signal an invalid header.
-    void updateCurHeader();
+    void updateCurrentHeader();
 
     void handleClipboardChanged(QClipboard::Mode p_mode);
 
@@ -99,9 +98,6 @@ private:
     // in the selection. Get the QImage.
     QImage tryGetSelectedImage();
 
-    // Return the header index in m_headers where current cursor locates.
-    int currentCursorHeader() const;
-
     QString getPlainTextWithoutPreviewImage() const;
 
     // Try to get all the regions of preview image within @p_block.
@@ -111,6 +107,9 @@ private:
 
     void finishOneAsyncJob(int p_idx);
 
+    // Index in m_headers of current header which contains the cursor.
+    int indexOfCurrentHeader() const;
+
     HGMarkdownHighlighter *m_mdHighlighter;
     VCodeBlockHighlightHelper *m_cbHighlighter;
     VImagePreviewer *m_imagePreviewer;
@@ -121,7 +120,8 @@ private:
     // Image links right at the beginning of the edit.
     QVector<ImageLink> m_initImages;
 
-    QVector<VHeader> m_headers;
+    // Mainly used for title jump.
+    QVector<VTableOfContentItem> m_headers;
 
     bool m_freshEdit;
 

+ 117 - 212
src/vmdtab.cpp

@@ -11,7 +11,7 @@
 #include "vconfigmanager.h"
 #include "vmarkdownconverter.h"
 #include "vnotebook.h"
-#include "vtoc.h"
+#include "vtableofcontent.h"
 #include "vmdedit.h"
 #include "dialog/vfindreplacedialog.h"
 #include "veditarea.h"
@@ -50,72 +50,109 @@ void VMdTab::setupUI()
     setLayout(m_stacks);
 }
 
-void VMdTab::handleTextChanged()
-{
-    V_ASSERT(m_file->isModifiable());
-
-    if (m_modified) {
-        return;
-    }
-
-    updateStatus();
-}
-
 void VMdTab::showFileReadMode()
 {
     m_isEditMode = false;
 
-    int outlineIndex = m_curHeader.m_outlineIndex;
+    VHeaderPointer header(m_currentHeader);
 
     if (m_mdConType == MarkdownConverterType::Hoedown) {
         viewWebByConverter();
     } else {
         m_document->updateText();
-        updateTocFromHtml(m_document->getToc());
+        updateOutlineFromHtml(m_document->getToc());
     }
 
     m_stacks->setCurrentWidget(m_webViewer);
     clearSearchedWordHighlight();
 
-    scrollWebViewToAnchor(outlineIndex);
+    scrollWebViewToHeader(header);
 
     updateStatus();
 }
 
-bool VMdTab::scrollWebViewToAnchor(int p_anchorIndex, bool p_strict)
+bool VMdTab::scrollWebViewToHeader(const VHeaderPointer &p_header)
 {
-    QString anchor;
+    if (!m_outline.isMatched(p_header)
+        || m_outline.getType() != VTableOfContentType::Anchor) {
+        return false;
+    }
 
-    VAnchor anch(m_file, anchor, -1, p_anchorIndex);
+    if (p_header.isValid()) {
+        const VTableOfContentItem *item = m_outline.getItem(p_header);
+        if (item) {
+            if (item->m_anchor.isEmpty()) {
+                return false;
+            }
 
-    bool validIndex = false;
-    if (p_anchorIndex < m_toc.headers.size() && p_anchorIndex >= 0) {
-        QString tmp = m_toc.headers[p_anchorIndex].anchor;
-        Q_ASSERT(!tmp.isEmpty());
-        anch.anchor = tmp;
-        anchor = tmp.mid(1);
-        validIndex = true;
+            m_currentHeader = p_header;
+            m_document->scrollToAnchor(item->m_anchor);
+        } else {
+            return false;
+        }
+    } else {
+        if (m_outline.isEmpty()) {
+            // Let it be.
+            m_currentHeader = p_header;
+        } else {
+            // Scroll to top.
+            m_currentHeader = p_header;
+            m_document->scrollToAnchor("");
+        }
     }
 
-    if (validIndex || !p_strict) {
-        m_curHeader = anch;
+    emit currentHeaderChanged(m_currentHeader);
+    return true;
+}
 
-        m_document->scrollToAnchor(anchor);
+bool VMdTab::scrollEditorToHeader(const VHeaderPointer &p_header)
+{
+    if (!m_outline.isMatched(p_header)
+        || m_outline.getType() != VTableOfContentType::BlockNumber) {
+        return false;
+    }
 
-        emit curHeaderChanged(m_curHeader);
+    VMdEdit *mdEdit = dynamic_cast<VMdEdit *>(getEditor());
 
+    int blockNumber = -1;
+    if (p_header.isValid()) {
+        const VTableOfContentItem *item = m_outline.getItem(p_header);
+        if (item) {
+            blockNumber = item->m_blockNumber;
+            if (blockNumber == -1) {
+                // Empty item.
+                return false;
+            }
+        } else {
+            return false;
+        }
+    } else {
+        if (m_outline.isEmpty()) {
+            // No outline and scroll to -1 index.
+            // Just let it be.
+            m_currentHeader = p_header;
+            return true;
+        } else {
+            // Has outline and scroll to -1 index.
+            // Scroll to top.
+            blockNumber = 0;
+        }
+    }
+
+    if (mdEdit->scrollToHeader(blockNumber)) {
+        m_currentHeader = p_header;
         return true;
     } else {
         return false;
     }
 }
 
-bool VMdTab::scrollToAnchor(int p_anchorIndex, bool p_strict)
+bool VMdTab::scrollToHeaderInternal(const VHeaderPointer &p_header)
 {
     if (m_isEditMode) {
-        return dynamic_cast<VMdEdit *>(getEditor())->scrollToAnchor(p_anchorIndex);
+        return scrollEditorToHeader(p_header);
     } else {
-        return scrollWebViewToAnchor(p_anchorIndex, p_strict);
+        return scrollWebViewToHeader(p_header);
     }
 }
 
@@ -127,7 +164,7 @@ void VMdTab::viewWebByConverter()
                                             g_config->getMarkdownExtensions(),
                                             toc);
     m_document->setHtml(html);
-    updateTocFromHtml(toc);
+    updateOutlineFromHtml(toc);
 }
 
 void VMdTab::showFileEditMode()
@@ -136,42 +173,28 @@ void VMdTab::showFileEditMode()
         return;
     }
 
+    VHeaderPointer header(m_currentHeader);
+
     m_isEditMode = true;
 
     VMdEdit *mdEdit = dynamic_cast<VMdEdit *>(getEditor());
     V_ASSERT(mdEdit);
 
-    // beginEdit() may change m_curHeader.
-    int outlineIndex = m_curHeader.m_outlineIndex;
-
     mdEdit->beginEdit();
     m_stacks->setCurrentWidget(mdEdit);
 
-    int lineNumber = -1;
-    const QVector<VHeader> &headers = mdEdit->getHeaders();
     // If editor is not init, we need to wait for it to init headers.
     // Generally, beginEdit() will generate the headers. Wait is needed when
     // highlight completion is going to re-generate the headers.
     int nrRetry = 5;
-    while (outlineIndex > -1 && headers.isEmpty() && nrRetry-- > 0) {
+    while (header.m_index > -1 && m_outline.isEmpty() && nrRetry-- > 0) {
         qDebug() << "wait another 100 ms for editor's headers ready";
         VUtils::sleepWait(100);
     }
 
-    if (outlineIndex < 0 || outlineIndex >= headers.size()) {
-        lineNumber = -1;
-        outlineIndex = -1;
-    } else {
-        lineNumber = headers[outlineIndex].lineNumber;
-    }
-
-    VAnchor anchor(m_file, "", lineNumber, outlineIndex);
-
-    mdEdit->scrollToAnchor(anchor);
+    scrollEditorToHeader(header);
 
     mdEdit->setFocus();
-
-    updateStatus();
 }
 
 bool VMdTab::closeFile(bool p_forced)
@@ -273,8 +296,6 @@ bool VMdTab::saveFile()
         m_editor->setModified(true);
     }
 
-    updateStatus();
-
     return ret;
 }
 
@@ -304,9 +325,9 @@ void VMdTab::setupMarkdownViewer()
     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 &)));
+            this, &VMdTab::updateOutlineFromHtml);
+    connect(m_document, SIGNAL(headerChanged(const QString &)),
+            this, SLOT(updateCurrentHeader(const QString &)));
     connect(m_document, &VDocument::keyPressed,
             this, &VMdTab::handleWebKeyPressed);
     connect(m_document, SIGNAL(logicsFinished(void)),
@@ -325,13 +346,13 @@ void VMdTab::setupMarkdownEditor()
     qDebug() << "create Markdown editor";
     m_editor = new VMdEdit(m_file, m_document, m_mdConType, this);
     connect(dynamic_cast<VMdEdit *>(m_editor), &VMdEdit::headersChanged,
-            this, &VMdTab::updateTocFromHeaders);
+            this, &VMdTab::updateOutlineFromHeaders);
+    connect(dynamic_cast<VMdEdit *>(m_editor), SIGNAL(currentHeaderChanged(int)),
+            this, SLOT(updateCurrentHeader(int)));
     connect(dynamic_cast<VMdEdit *>(m_editor), &VMdEdit::statusChanged,
             this, &VMdTab::updateStatus);
-    connect(m_editor, SIGNAL(curHeaderChanged(VAnchor)),
-            this, SLOT(updateCurHeader(VAnchor)));
     connect(m_editor, &VEdit::textChanged,
-            this, &VMdTab::handleTextChanged);
+            this, &VMdTab::updateStatus);
     connect(m_editor, &VEdit::cursorPositionChanged,
             this, &VMdTab::updateStatus);
     connect(m_editor, &VEdit::saveAndRead,
@@ -355,185 +376,71 @@ void VMdTab::setupMarkdownEditor()
     m_stacks->addWidget(m_editor);
 }
 
-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.size());
-                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, c_emptyHeaderName, "#", -1, p_headers.size());
-            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)
+void VMdTab::updateOutlineFromHtml(const QString &p_tocHtml)
 {
     if (m_isEditMode) {
         return;
     }
 
-    m_toc.type = VHeaderType::Anchor;
-    m_toc.headers.clear();
+    m_outline.clear();
 
-    if (!parseTocHtml(p_tocHtml, m_toc.headers)) {
-        return;
+    if (m_outline.parseTableFromHtml(p_tocHtml)) {
+        m_outline.setFile(m_file);
+        m_outline.setType(VTableOfContentType::Anchor);
     }
 
-    m_toc.m_file = m_file;
-    m_toc.valid = true;
+    m_currentHeader.reset();
 
-    emit outlineChanged(m_toc);
+    emit outlineChanged(m_outline);
 }
 
-void VMdTab::updateTocFromHeaders(const QVector<VHeader> &p_headers)
+void VMdTab::updateOutlineFromHeaders(const QVector<VTableOfContentItem> &p_headers)
 {
     if (!m_isEditMode) {
         return;
     }
 
-    m_toc.type = VHeaderType::LineNumber;
-    m_toc.headers = p_headers;
-    m_toc.m_file = m_file;
-    m_toc.valid = true;
+    m_outline.update(m_file,
+                     p_headers,
+                     VTableOfContentType::BlockNumber);
 
-    // Clear current header.
-    m_curHeader = VAnchor(m_file, "", -1, -1);
-    emit curHeaderChanged(m_curHeader);
+    m_currentHeader.reset();
 
-    emit outlineChanged(m_toc);
+    emit outlineChanged(m_outline);
 }
 
-void VMdTab::scrollToAnchor(const VAnchor &p_anchor)
+void VMdTab::scrollToHeader(const VHeaderPointer &p_header)
 {
-    if (p_anchor == m_curHeader) {
-        return;
-    }
-
-    m_curHeader = p_anchor;
-
-    if (m_isEditMode) {
-        dynamic_cast<VMdEdit *>(getEditor())->scrollToAnchor(p_anchor);
-    } else {
-        if (!p_anchor.anchor.isEmpty()) {
-            m_document->scrollToAnchor(p_anchor.anchor.mid(1));
-        }
+    if (m_outline.isMatched(p_header)) {
+        // Scroll only when @p_header is valid.
+        scrollToHeaderInternal(p_header);
     }
 }
 
-void VMdTab::updateCurHeader(const QString &p_anchor)
+void VMdTab::updateCurrentHeader(const QString &p_anchor)
 {
-    if (m_isEditMode || m_curHeader.anchor.mid(1) == p_anchor) {
+    if (m_isEditMode) {
         return;
     }
 
-    m_curHeader = VAnchor(m_file, "#" + 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) {
-                V_ASSERT(headers[i].index == i);
-                m_curHeader.m_outlineIndex = headers[i].index;
-                break;
-            }
-        }
-    }
+    // Find the index of the anchor in outline.
+    int idx = m_outline.indexOfItemByAnchor(p_anchor);
+    m_currentHeader.update(m_file, idx);
 
-    emit curHeaderChanged(m_curHeader);
+    emit currentHeaderChanged(m_currentHeader);
 }
 
-void VMdTab::updateCurHeader(VAnchor p_anchor)
+void VMdTab::updateCurrentHeader(int p_blockNumber)
 {
-    if (m_isEditMode) {
-        if (!p_anchor.anchor.isEmpty() || p_anchor.lineNumber == m_curHeader.lineNumber) {
-            return;
-        }
-    } else {
-        if (p_anchor.lineNumber != -1 || p_anchor.anchor == m_curHeader.anchor) {
-            return;
-        }
+    if (!m_isEditMode) {
+        return;
     }
 
-    m_curHeader = p_anchor;
+    // Find the index of the block number in outline.
+    int idx = m_outline.indexOfItemByBlockNumber(p_blockNumber);
+    m_currentHeader.update(m_file, idx);
 
-    emit curHeaderChanged(m_curHeader);
+    emit currentHeaderChanged(m_currentHeader);
 }
 
 void VMdTab::insertImage()
@@ -705,7 +612,7 @@ void VMdTab::requestUpdateVimStatus()
     }
 }
 
-VEditTabInfo VMdTab::fetchTabInfo()
+VEditTabInfo VMdTab::fetchTabInfo() const
 {
     VEditTabInfo info = VEditTab::fetchTabInfo();
 
@@ -716,7 +623,7 @@ VEditTabInfo VMdTab::fetchTabInfo()
         info.m_blockCount = m_editor->document()->blockCount();
     }
 
-    info.m_anchorIndex = m_curHeader.m_outlineIndex;
+    info.m_headerIndex = m_currentHeader.m_index;
 
     return info;
 }
@@ -730,15 +637,13 @@ void VMdTab::decorateText(TextDecoration p_decoration)
 
 bool VMdTab::restoreFromTabInfo(const VEditTabInfo &p_info)
 {
-    qDebug() << "restoreFromTabInfo" << p_info.m_anchorIndex;
     if (p_info.m_editTab != this) {
         return false;
     }
 
-    // Restore anchor.
-    int anchorIdx = p_info.m_anchorIndex;
-    bool ret = scrollToAnchor(anchorIdx, true);
-
+    // Restore header.
+    VHeaderPointer header(m_file, p_info.m_headerIndex);
+    bool ret = scrollToHeaderInternal(header);
     return ret;
 }
 
@@ -747,5 +652,5 @@ void VMdTab::restoreFromTabInfo()
     restoreFromTabInfo(m_infoToRestore);
 
     // Clear it anyway.
-    m_infoToRestore.m_editTab = NULL;
+    m_infoToRestore.clear();
 }

+ 14 - 18
src/vmdtab.h

@@ -31,8 +31,8 @@ public:
     // Save file.
     bool saveFile() Q_DECL_OVERRIDE;
 
-    // Scroll to anchor @p_anchor.
-    void scrollToAnchor(const VAnchor& p_anchor) Q_DECL_OVERRIDE;
+    // Scroll to @p_header.
+    void scrollToHeader(const VHeaderPointer &p_header) Q_DECL_OVERRIDE;
 
     void insertImage() Q_DECL_OVERRIDE;
 
@@ -63,27 +63,25 @@ public:
     void decorateText(TextDecoration p_decoration) Q_DECL_OVERRIDE;
 
     // Create a filled VEditTabInfo.
-    VEditTabInfo fetchTabInfo() Q_DECL_OVERRIDE;
+    VEditTabInfo fetchTabInfo() const Q_DECL_OVERRIDE;
 
 public slots:
     // Enter edit mode.
     void editFile() Q_DECL_OVERRIDE;
 
 private slots:
-    // Handle text changed in m_editor.
-    void handleTextChanged();
+    // Update m_outline according to @p_tocHtml for read mode.
+    void updateOutlineFromHtml(const QString &p_tocHtml);
 
-    // 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);
+    // Update m_outline accroding to @p_headers for edit mode.
+    void updateOutlineFromHeaders(const QVector<VTableOfContentItem> &p_headers);
 
     // Web viewer requests to update current header.
-    void updateCurHeader(const QString &p_anchor);
+    // @p_anchor is the anchor of the header, like "toc_1".
+    void updateCurrentHeader(const QString &p_anchor);
 
     // Editor requests to update current header.
-    void updateCurHeader(VAnchor p_anchor);
+    void updateCurrentHeader(int p_blockNumber);
 
     // Handle key press event in Web view.
     void handleWebKeyPressed(int p_key, bool p_ctrl, bool p_shift);
@@ -117,16 +115,14 @@ private:
     void viewWebByConverter();
 
     // Scroll Web view to given header.
-    // @p_anchorIndex is the index in m_toc.headers.
-    // @p_strict: if true, scroll only when @p_anchorIndex is valid.
     // Return true if scroll was made.
-    bool scrollWebViewToAnchor(int p_anchorIndex, bool p_strict = false);
+    bool scrollWebViewToHeader(const VHeaderPointer &p_header);
+
+    bool scrollEditorToHeader(const VHeaderPointer &p_header);
 
     // Scroll web/editor to given header.
-    // @p_anchorIndex is the index in m_toc.headers.
-    // @p_strict: if true, scroll only when @p_anchorIndex is valid.
     // Return true if scroll was made.
-    bool scrollToAnchor(int p_anchorIndex, bool p_strict = false);
+    bool scrollToHeaderInternal(const VHeaderPointer &p_header);
 
     // Search text in Web view.
     void findTextInWebView(const QString &p_text, uint p_options, bool p_peek,

+ 55 - 112
src/voutline.cpp

@@ -1,44 +1,39 @@
-#include <QDebug>
 #include <QVector>
 #include <QString>
 #include <QKeyEvent>
 #include <QLabel>
 #include <QCoreApplication>
 #include "voutline.h"
-#include "vtoc.h"
 #include "utils/vutils.h"
 #include "vnote.h"
+#include "vfile.h"
 
 extern VNote *g_vnote;
 
 VOutline::VOutline(QWidget *parent)
-    : QTreeWidget(parent), VNavigationMode()
+    : QTreeWidget(parent),
+      VNavigationMode(),
+      m_muted(false)
 {
     setColumnCount(1);
     setHeaderHidden(true);
     setSelectionMode(QAbstractItemView::SingleSelection);
 
+    // TODO: jump to the header when user click the same item twice.
     connect(this, &VOutline::currentItemChanged,
-            this, &VOutline::handleCurItemChanged);
+            this, &VOutline::handleCurrentItemChanged);
 }
 
-void VOutline::checkOutline(const VToc &p_toc) const
+void VOutline::updateOutline(const VTableOfContent &p_outline)
 {
-    const QVector<VHeader> &headers = p_toc.headers;
-
-    for (int i = 0; i < headers.size(); ++i) {
-        V_ASSERT(headers[i].index == i);
+    if (p_outline == m_outline) {
+        return;
     }
-}
 
-void VOutline::updateOutline(const VToc &toc)
-{
     // Clear current header
-    curHeader = VAnchor();
+    m_currentHeader.clear();
 
-    checkOutline(toc);
-
-    outline = toc;
+    m_outline = p_outline;
 
     updateTreeFromOutline();
 
@@ -49,22 +44,25 @@ void VOutline::updateTreeFromOutline()
 {
     clear();
 
-    if (!outline.valid) {
+    if (m_outline.isEmpty()) {
         return;
     }
 
-    const QVector<VHeader> &headers = outline.headers;
+    const QVector<VTableOfContentItem> &headers = m_outline.getTable();
     int idx = 0;
     updateTreeByLevel(headers, idx, NULL, NULL, 1);
 }
 
-void VOutline::updateTreeByLevel(const QVector<VHeader> &headers, int &index,
-                                 QTreeWidgetItem *parent, QTreeWidgetItem *last, int level)
+void VOutline::updateTreeByLevel(const QVector<VTableOfContentItem> &headers,
+                                 int &index,
+                                 QTreeWidgetItem *parent,
+                                 QTreeWidgetItem *last,
+                                 int level)
 {
     while (index < headers.size()) {
-        const VHeader &header = headers[index];
+        const VTableOfContentItem &header = headers[index];
         QTreeWidgetItem *item;
-        if (header.level == level) {
+        if (header.m_level == level) {
             if (parent) {
                 item = new QTreeWidgetItem(parent);
             } else {
@@ -75,7 +73,7 @@ void VOutline::updateTreeByLevel(const QVector<VHeader> &headers, int &index,
 
             last = item;
             ++index;
-        } else if (header.level < level) {
+        } else if (header.m_level < level) {
             return;
         } else {
             updateTreeByLevel(headers, index, last, NULL, level + 1);
@@ -83,11 +81,11 @@ void VOutline::updateTreeByLevel(const QVector<VHeader> &headers, int &index,
     }
 }
 
-void VOutline::fillItem(QTreeWidgetItem *p_item, const VHeader &p_header)
+void VOutline::fillItem(QTreeWidgetItem *p_item, const VTableOfContentItem &p_header)
 {
-    p_item->setData(0, Qt::UserRole, p_header.index);
-    p_item->setText(0, p_header.name);
-    p_item->setToolTip(0, p_header.name);
+    p_item->setData(0, Qt::UserRole, p_header.m_index);
+    p_item->setText(0, p_header.m_name);
+    p_item->setToolTip(0, p_header.m_name);
 
     if (p_header.isEmpty()) {
         p_item->setForeground(0, QColor("grey"));
@@ -99,127 +97,81 @@ void VOutline::expandTree()
     if (topLevelItemCount() == 0) {
         return;
     }
+
     expandAll();
 }
 
-void VOutline::handleCurItemChanged(QTreeWidgetItem *p_curItem, QTreeWidgetItem * /*p_preItem*/)
+void VOutline::handleCurrentItemChanged(QTreeWidgetItem *p_curItem,
+                                        QTreeWidgetItem * p_preItem)
 {
-    if (!p_curItem) {
-        return;
-    }
-
-    const VHeader *header = getHeaderFromItem(p_curItem);
-    if (!header) {
-        return;
-    }
+    Q_UNUSED(p_preItem);
 
-    VAnchor tmp(outline.m_file, header->anchor, header->lineNumber, header->index);
-    if (tmp == curHeader) {
+    if (!p_curItem) {
         return;
     }
 
-    curHeader = tmp;
+    const VTableOfContentItem *header = getHeaderFromItem(p_curItem);
+    Q_ASSERT(header);
+    m_currentHeader.update(m_outline.getFile(), header->m_index);
 
-    if (!header->isEmpty()) {
-        emit outlineItemActivated(curHeader);
+    if (!header->isEmpty() && !m_muted) {
+        emit outlineItemActivated(m_currentHeader);
     }
 }
 
-void VOutline::updateCurHeader(const VAnchor &anchor)
+void VOutline::updateCurrentHeader(const VHeaderPointer &p_header)
 {
-    if (anchor == curHeader) {
+    if (p_header == m_currentHeader
+        || !m_outline.isMatched(p_header)) {
         return;
     }
 
-    curHeader = anchor;
-    if (outline.type == VHeaderType::Anchor) {
-        selectAnchor(anchor.anchor);
-    } else {
-        // Select by lineNumber.
-        selectLineNumber(anchor.lineNumber);
-    }
+    // Item change should not emit the signal.
+    m_muted = true;
+    m_currentHeader = p_header;
+    selectHeader(m_currentHeader);
+    m_muted = false;
 }
 
-void VOutline::selectAnchor(const QString &anchor)
+void VOutline::selectHeader(const VHeaderPointer &p_header)
 {
     setCurrentItem(NULL);
 
-    if (anchor.isEmpty()) {
+    if (!m_outline.getItem(p_header)) {
         return;
     }
 
     int nrTop = topLevelItemCount();
     for (int i = 0; i < nrTop; ++i) {
-        if (selectAnchorOne(topLevelItem(i), anchor)) {
+        if (selectHeaderOne(topLevelItem(i), p_header)) {
             return;
         }
     }
 }
 
-bool VOutline::selectAnchorOne(QTreeWidgetItem *item, const QString &anchor)
+bool VOutline::selectHeaderOne(QTreeWidgetItem *p_item, const VHeaderPointer &p_header)
 {
-    if (!item) {
+    if (!p_item) {
         return false;
     }
 
-    const VHeader *header = getHeaderFromItem(item);
+    const VTableOfContentItem *header = getHeaderFromItem(p_item);
     if (!header) {
         return false;
     }
 
-    if (header->anchor == anchor) {
-        setCurrentItem(item);
+    if (header->isMatched(p_header)) {
+        setCurrentItem(p_item);
         return true;
     }
 
-    int nrChild = item->childCount();
+    int nrChild = p_item->childCount();
     for (int i = 0; i < nrChild; ++i) {
-        if (selectAnchorOne(item->child(i), anchor)) {
+        if (selectHeaderOne(p_item->child(i), p_header)) {
             return true;
         }
     }
-    return false;
-}
-
-void VOutline::selectLineNumber(int lineNumber)
-{
-    setCurrentItem(NULL);
-
-    if (lineNumber == -1) {
-        return;
-    }
 
-    int nrTop = topLevelItemCount();
-    for (int i = 0; i < nrTop; ++i) {
-        if (selectLineNumberOne(topLevelItem(i), lineNumber)) {
-            return;
-        }
-    }
-}
-
-bool VOutline::selectLineNumberOne(QTreeWidgetItem *item, int lineNumber)
-{
-    if (!item) {
-        return false;
-    }
-
-    const VHeader *header = getHeaderFromItem(item);
-    if (!header) {
-        return false;
-    }
-
-    if (header->lineNumber == lineNumber) {
-        // Select this item
-        setCurrentItem(item);
-        return true;
-    }
-
-    int nrChild = item->childCount();
-    for (int i = 0; i < nrChild; ++i) {
-        if (selectLineNumberOne(item->child(i), lineNumber)) {
-            return true;
-        }
-    }
     return false;
 }
 
@@ -373,17 +325,8 @@ QList<QTreeWidgetItem *> VOutline::getVisibleChildItems(const QTreeWidgetItem *p
     return items;
 }
 
-const VHeader *VOutline::getHeaderFromItem(QTreeWidgetItem *p_item) const
+const VTableOfContentItem *VOutline::getHeaderFromItem(QTreeWidgetItem *p_item) const
 {
-    const VHeader *header = NULL;
-
     int index = p_item->data(0, Qt::UserRole).toInt();
-    if (index < 0 || index >= outline.headers.size()) {
-        return header;
-    }
-
-    header = &(outline.headers[index]);
-    Q_ASSERT(header->index == index);
-
-    return header;
+    return m_outline.getItem(index);
 }

+ 35 - 18
src/voutline.h

@@ -5,11 +5,13 @@
 #include <QVector>
 #include <QMap>
 #include <QChar>
-#include "vtoc.h"
+#include "vtableofcontent.h"
 #include "vnavigationmode.h"
 
 class QLabel;
 
+// Display table of content as a tree and enable user to click an item to
+// jump to that header.
 class VOutline : public QTreeWidget, public VNavigationMode
 {
     Q_OBJECT
@@ -23,45 +25,60 @@ public:
     bool handleKeyNavigation(int p_key, bool &p_succeed) Q_DECL_OVERRIDE;
 
 signals:
-    void outlineItemActivated(const VAnchor &anchor);
+    // Emit when current item changed by user and header of that item is not empty.
+    // Do not worry about infinite recursion.
+    void outlineItemActivated(const VHeaderPointer &p_header);
 
 public slots:
-    void updateOutline(const VToc &toc);
-    void updateCurHeader(const VAnchor &anchor);
+    // Called to update outline and the tree.
+    // Just clear the tree if @p_outline is empty.
+    void updateOutline(const VTableOfContent &p_outline);
+
+    // Called to update current header in the tree.
+    // Will not emit outlineItemActivated().
+    void updateCurrentHeader(const VHeaderPointer &p_header);
 
 protected:
     void keyPressEvent(QKeyEvent *event) Q_DECL_OVERRIDE;
 
 private slots:
-    void handleCurItemChanged(QTreeWidgetItem *p_curItem, QTreeWidgetItem *p_preItem);
+    // Handle current item change even of the tree.
+    // Do not response if m_muted is true.
+    void handleCurrentItemChanged(QTreeWidgetItem *p_curItem, QTreeWidgetItem *p_preItem);
 
 private:
     // Update tree according to outline.
     void updateTreeFromOutline();
 
     // @index: the index in @headers.
-    void updateTreeByLevel(const QVector<VHeader> &headers, int &index, QTreeWidgetItem *parent,
-                           QTreeWidgetItem *last, int level);
+    void updateTreeByLevel(const QVector<VTableOfContentItem> &headers,
+                           int &index,
+                           QTreeWidgetItem *parent,
+                           QTreeWidgetItem *last,
+                           int level);
 
     void expandTree();
-    void selectAnchor(const QString &anchor);
-    bool selectAnchorOne(QTreeWidgetItem *item, const QString &anchor);
-    void selectLineNumber(int lineNumber);
-    bool selectLineNumberOne(QTreeWidgetItem *item, int lineNumber);
+
+    // Set the item corresponding to @p_header as current item.
+    void selectHeader(const VHeaderPointer &p_header);
+
+    bool selectHeaderOne(QTreeWidgetItem *p_item, const VHeaderPointer &p_header);
+
     QList<QTreeWidgetItem *> getVisibleItems() const;
     QList<QTreeWidgetItem *> getVisibleChildItems(const QTreeWidgetItem *p_item) const;
 
     // Fill the info of @p_item.
-    void fillItem(QTreeWidgetItem *p_item, const VHeader &p_header);
-
-    // Check if @p_toc is valid.
-    void checkOutline(const VToc &p_toc) const;
+    void fillItem(QTreeWidgetItem *p_item, const VTableOfContentItem &p_header);
 
     // Return NULL if no corresponding header in outline.
-    const VHeader *getHeaderFromItem(QTreeWidgetItem *p_item) const;
+    const VTableOfContentItem *getHeaderFromItem(QTreeWidgetItem *p_item) const;
+
+    VTableOfContent m_outline;
+
+    VHeaderPointer m_currentHeader;
 
-    VToc outline;
-    VAnchor curHeader;
+    // When true, won't emit outlineItemActivated().
+    bool m_muted;
 
     // Navigation Mode.
     // Map second key to QTreeWidgetItem.

+ 176 - 0
src/vtableofcontent.cpp

@@ -0,0 +1,176 @@
+#include "vtableofcontent.h"
+#include "vconstants.h"
+
+#include <QXmlStreamReader>
+#include <QDebug>
+
+
+VTableOfContent::VTableOfContent()
+    : m_file(NULL), m_type(VTableOfContentType::Anchor)
+{
+}
+
+VTableOfContent::VTableOfContent(const VFile *p_file)
+    : m_file(p_file), m_type(VTableOfContentType::Anchor)
+{
+}
+
+void VTableOfContent::update(const VFile *p_file,
+                             const QVector<VTableOfContentItem> &p_table,
+                             VTableOfContentType p_type)
+{
+    m_file = p_file;
+    m_table = p_table;
+    m_type = p_type;
+}
+
+static bool parseTocUl(QXmlStreamReader &p_xml,
+                       QVector<VTableOfContentItem> &p_table,
+                       int p_level);
+
+static bool parseTocLi(QXmlStreamReader &p_xml,
+                       QVector<VTableOfContentItem> &p_table,
+                       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().mid(1);
+            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 false;
+                }
+
+                VTableOfContentItem header(name, p_level, anchor, p_table.size());
+                p_table.append(header);
+            } else {
+                // Error
+                return false;
+            }
+        } else if (p_xml.name() == "ul") {
+            // Such as header 3 under header 1 directly
+            VTableOfContentItem header(c_emptyHeaderName, p_level, "", p_table.size());
+            p_table.append(header);
+            parseTocUl(p_xml, p_table, p_level + 1);
+        } else {
+            qWarning() << "TOC HTML <li> should contain <a> or <ul>" << p_xml.name();
+            return false;
+        }
+    }
+
+    while (p_xml.readNext()) {
+        if (p_xml.isEndElement()) {
+            if (p_xml.name() == "li") {
+                return true;
+            }
+
+            continue;
+        }
+
+        if (p_xml.name() == "ul") {
+            // Nested unordered list
+            if (!parseTocUl(p_xml, p_table, p_level + 1)) {
+                return false;
+            }
+        } else {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+static bool parseTocUl(QXmlStreamReader &p_xml,
+                       QVector<VTableOfContentItem> &p_table,
+                       int p_level)
+{
+    bool ret = true;
+    Q_ASSERT(p_xml.isStartElement() && p_xml.name() == "ul");
+
+    while (p_xml.readNextStartElement()) {
+        if (p_xml.name() == "li") {
+            if (!parseTocLi(p_xml, p_table, p_level)) {
+                ret = false;
+                break;
+            }
+        } else {
+            qWarning() << "TOC HTML <ul> should contain <li>" << p_xml.name();
+            ret = false;
+            break;
+        }
+    }
+
+    return ret;
+}
+
+bool VTableOfContent::parseTableFromHtml(const QString &p_html)
+{
+    bool ret = true;
+    m_table.clear();
+
+    if (!p_html.isEmpty()) {
+        QXmlStreamReader xml(p_html);
+        if (xml.readNextStartElement()) {
+            if (xml.name() == "ul") {
+                ret = parseTocUl(xml, m_table, 1);
+            } else {
+                qWarning() << "TOC HTML does not start with <ul>" << p_html;
+                ret = false;
+            }
+        }
+
+        if (xml.hasError()) {
+            qWarning() << "fail to parse TOC in HTML" << p_html;
+            ret = false;
+        }
+    }
+
+    return ret;
+}
+
+int VTableOfContent::indexOfItemByAnchor(const QString &p_anchor) const
+{
+    if (p_anchor.isEmpty()
+        || isEmpty()
+        || m_type != VTableOfContentType::Anchor) {
+        return -1;
+    }
+
+    for (int i = 0; i < m_table.size(); ++i) {
+        if (m_table[i].m_anchor == p_anchor) {
+            return i;
+        }
+    }
+
+    return -1;
+}
+
+int VTableOfContent::indexOfItemByBlockNumber(int p_blockNumber) const
+{
+    if (p_blockNumber == -1
+        || isEmpty()
+        || m_type != VTableOfContentType::BlockNumber) {
+        return -1;
+    }
+
+    for (int i = m_table.size() - 1; i >= 0; --i) {
+        if (!m_table[i].isEmpty()
+            && m_table[i].m_blockNumber <= p_blockNumber) {
+            return i;
+        }
+    }
+
+    return -1;
+}
+
+bool VTableOfContent::operator==(const VTableOfContent &p_outline) const
+{
+    return m_file == p_outline.getFile()
+           && m_type == p_outline.getType()
+           && m_table == p_outline.getTable();
+}

+ 286 - 0
src/vtableofcontent.h

@@ -0,0 +1,286 @@
+#ifndef VTABLEOFCONTENT_H
+#define VTABLEOFCONTENT_H
+
+#include <QString>
+#include <QVector>
+
+class VFile;
+
+
+struct VHeaderPointer
+{
+    VHeaderPointer()
+        : m_file(NULL), m_index(-1)
+    {
+    }
+
+    VHeaderPointer(const VFile *p_file, int p_index)
+        : m_file(p_file), m_index(p_index)
+    {
+    }
+
+    bool operator==(const VHeaderPointer &p_header) const
+    {
+        return m_file == p_header.m_file
+               && m_index == p_header.m_index;
+    }
+
+    void reset()
+    {
+        m_index = -1;
+    }
+
+    void clear()
+    {
+        m_file = NULL;
+        reset();
+    }
+
+    void update(const VFile *p_file, int p_index)
+    {
+        m_file = p_file;
+        m_index = p_index;
+    }
+
+    bool isValid() const
+    {
+        return m_index > -1;
+    }
+
+    QString toString() const
+    {
+        return QString("VHeaderPointer file: %1 index: %2")
+                      .arg((long long)m_file)
+                      .arg(m_index);
+    }
+
+    // The corresponding file.
+    const VFile *m_file;
+
+    // Index of the header item in VTableOfContent which this instance points to.
+    int m_index;
+};
+
+
+struct VTableOfContentItem
+{
+    VTableOfContentItem()
+        : m_level(1), m_blockNumber(-1), m_index(-1)
+    {
+    }
+
+    VTableOfContentItem(const QString &p_name,
+                        int p_level,
+                        const QString &p_anchor,
+                        int p_index)
+        : m_name(p_name),
+          m_level(p_level),
+          m_anchor(p_anchor),
+          m_blockNumber(-1),
+          m_index(p_index)
+    {
+    }
+
+    VTableOfContentItem(const QString &p_name,
+                        int p_level,
+                        int p_blockNumber,
+                        int p_index)
+        : m_name(p_name),
+          m_level(p_level),
+          m_blockNumber(p_blockNumber),
+          m_index(p_index)
+    {
+    }
+
+    // Whether it is an empty item.
+    // An empty item points to nothing.
+    bool isEmpty() const
+    {
+        if (m_anchor.isEmpty()) {
+            return m_blockNumber == -1;
+        }
+
+        return false;
+    }
+
+    bool isMatched(const VHeaderPointer &p_header) const
+    {
+        return m_index == p_header.m_index;
+    }
+
+    bool operator==(const VTableOfContentItem &p_item) const
+    {
+        return m_name == p_item.m_name
+               && m_level == p_item.m_level
+               && m_anchor == p_item.m_anchor
+               && m_blockNumber == p_item.m_blockNumber
+               && m_index == p_item.m_index;
+    }
+
+    // Name of the item to display.
+    QString m_name;
+
+    // Level of this item, based on 1.
+    int m_level;
+
+    // Use an anchor to identify the position of this item.
+    QString m_anchor;
+
+    // Use block number to identify the position of this item.
+    // -1 to indicate invalid.
+    int m_blockNumber;
+
+    // Index in VTableOfContent, based on 0.
+    // -1 for invalid value.
+    int m_index;
+};
+
+
+enum class VTableOfContentType
+{
+    Anchor = 0,
+    BlockNumber
+};
+
+
+class VTableOfContent
+{
+public:
+    VTableOfContent();
+
+    VTableOfContent(const VFile *p_file);
+
+    void update(const VFile *p_file,
+                const QVector<VTableOfContentItem> &p_table,
+                VTableOfContentType p_type);
+
+    // Parse m_table from html.
+    bool parseTableFromHtml(const QString &p_html);
+
+    const VFile *getFile() const;
+
+    void setFile(const VFile *p_file);
+
+    VTableOfContentType getType() const;
+
+    void setType(VTableOfContentType p_type);
+
+    void clearTable();
+
+    const QVector<VTableOfContentItem> &getTable() const;
+
+    void setTable(const QVector<VTableOfContentItem> &p_table);
+
+    void clear();
+
+    // Return the index in @m_table of @p_anchor.
+    int indexOfItemByAnchor(const QString &p_anchor) const;
+
+    // Return the last index in @m_table which has smaller block number than @p_blockNumber.
+    int indexOfItemByBlockNumber(int p_blockNumber) const;
+
+    const VTableOfContentItem *getItem(int p_idx) const;
+
+    const VTableOfContentItem *getItem(const VHeaderPointer &p_header) const;
+
+    bool isEmpty() const;
+
+    // Whether @p_header is pointing to this outline.
+    bool isMatched(const VHeaderPointer &p_header) const;
+
+    bool operator==(const VTableOfContent &p_outline) const;
+
+    QString toString() const;
+
+private:
+    // Corresponding file.
+    const VFile *m_file;
+
+    // Table of content.
+    QVector<VTableOfContentItem> m_table;
+
+    // Type of the table of content: by anchor or by block number.
+    VTableOfContentType m_type;
+};
+
+inline VTableOfContentType VTableOfContent::getType() const
+{
+    return m_type;
+}
+
+inline void VTableOfContent::setType(VTableOfContentType p_type)
+{
+    m_type = p_type;
+}
+
+inline void VTableOfContent::clearTable()
+{
+    m_table.clear();
+}
+
+inline const QVector<VTableOfContentItem> &VTableOfContent::getTable() const
+{
+    return m_table;
+}
+
+inline void VTableOfContent::setTable(const QVector<VTableOfContentItem> &p_table)
+{
+    m_table = p_table;
+}
+
+inline void VTableOfContent::clear()
+{
+    m_file = NULL;
+    m_table.clear();
+    m_type = VTableOfContentType::Anchor;
+}
+
+inline void VTableOfContent::setFile(const VFile *p_file)
+{
+    m_file = p_file;
+}
+
+inline const VFile *VTableOfContent::getFile() const
+{
+    return m_file;
+}
+
+inline const VTableOfContentItem *VTableOfContent::getItem(int p_idx) const
+{
+    if (!m_file
+        || p_idx < 0
+        || p_idx >= m_table.size()) {
+        return NULL;
+    }
+
+    return &m_table[p_idx];
+}
+
+inline const VTableOfContentItem *VTableOfContent::getItem(const VHeaderPointer &p_header) const
+{
+    if (p_header.m_file != m_file) {
+        return NULL;
+    }
+
+    return getItem(p_header.m_index);
+}
+
+inline bool VTableOfContent::isEmpty() const
+{
+    return !m_file || m_table.isEmpty();
+}
+
+inline bool VTableOfContent::isMatched(const VHeaderPointer &p_header) const
+{
+    return m_file && m_file == p_header.m_file;
+}
+
+inline QString VTableOfContent::toString() const
+{
+    return QString("VTableOfContent file: %1 isAnchor: %2 tableSize: %3")
+                  .arg((long long)m_file)
+                  .arg(m_type == VTableOfContentType::Anchor)
+                  .arg(m_table.size());
+}
+
+#endif // VTABLEOFCONTENT_H

+ 0 - 6
src/vtoc.cpp

@@ -1,6 +0,0 @@
-#include "vtoc.h"
-
-VToc::VToc()
-    : type(VHeaderType::Anchor), valid(false)
-{
-}

+ 0 - 78
src/vtoc.h

@@ -1,78 +0,0 @@
-#ifndef VTOC_H
-#define VTOC_H
-
-#include <QString>
-#include <QVector>
-
-class VFile;
-
-enum VHeaderType
-{
-    Anchor = 0,
-    LineNumber
-};
-
-struct VHeader
-{
-    VHeader() : level(1), lineNumber(-1), index(-1) {}
-    VHeader(int level, const QString &name, const QString &anchor, int lineNumber, int index)
-        : level(level), name(name), anchor(anchor), lineNumber(lineNumber), index(index) {}
-    int level;
-    QString name;
-    QString anchor;
-    int lineNumber;
-
-    // Index in the outline, based on 0.
-    int index;
-
-    // Whether it is an empty (fake) header.
-    bool isEmpty() const
-    {
-        if (anchor.isEmpty()) {
-            return lineNumber == -1;
-        } else {
-            return anchor == "#";
-        }
-    }
-};
-
-struct VAnchor
-{
-    VAnchor() : m_file(NULL), lineNumber(-1), m_outlineIndex(-1) {}
-
-    VAnchor(const VFile *file, const QString &anchor, int lineNumber, int outlineIndex = -1)
-        : m_file(file), anchor(anchor), lineNumber(lineNumber), m_outlineIndex(outlineIndex) {}
-
-    // The file this anchor points to.
-    const VFile *m_file;
-
-    // The string anchor. For Web view.
-    QString anchor;
-
-    // The line number anchor. For edit view.
-    int lineNumber;
-
-    // Index of the header for this anchor in VToc outline.
-    // Used to translate current header between read and edit mode.
-    int m_outlineIndex;
-
-    bool operator==(const VAnchor &p_anchor) const {
-        return (p_anchor.m_file == m_file
-                && p_anchor.anchor == anchor
-                && p_anchor.lineNumber == lineNumber
-                && p_anchor.m_outlineIndex == m_outlineIndex);
-    }
-};
-
-class VToc
-{
-public:
-    VToc();
-
-    QVector<VHeader> headers;
-    int type;
-    const VFile *m_file;
-    bool valid;
-};
-
-#endif // VTOC_H