Przeglądaj źródła

add dock widget to display outline

1. Support displaying outline of Markdown in read mode;
2. Support navigating by outline using Marked;

Signed-off-by: Le Tan <[email protected]>
Le Tan 9 lat temu
rodzic
commit
ab91f755c0

+ 1 - 1
src/resources/icons/corner_tablist.svg

@@ -4,7 +4,7 @@
 <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
 <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
 	 width="512px" height="512px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
 	 width="512px" height="512px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
 <g>
 <g>
-	<path d="M464,144v288H48V144H464 M480,128H32v320h448V128L480,128z"/>
+	<path d="M480,128H32v320h448V128L480,128z"/>
 	<rect x="72" y="96" width="368" height="16"/>
 	<rect x="72" y="96" width="368" height="16"/>
 	<rect x="104" y="64" width="304" height="16"/>
 	<rect x="104" y="64" width="304" height="16"/>
 </g>
 </g>

+ 23 - 0
src/resources/icons/outline.svg

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
+<g>
+	<g>
+		<path d="M432,80v352H80V80H432 M448,64H64v384h384V64L448,64z"/>
+		<g>
+			<rect x="192" y="152" width="192" height="16"/>
+		</g>
+		<g>
+			<rect x="192" y="248" width="192" height="16"/>
+		</g>
+		<g>
+			<rect x="192" y="344" width="192" height="16"/>
+		</g>
+	</g>
+	<circle cx="144" cy="160" r="16"/>
+	<circle cx="144" cy="256" r="16"/>
+	<circle cx="144" cy="352" r="16"/>
+</g>
+</svg>

+ 29 - 11
src/resources/template.html

@@ -19,9 +19,12 @@
   var placeholder = document.getElementById('placeholder');
   var placeholder = document.getElementById('placeholder');
   var renderer = new marked.Renderer();
   var renderer = new marked.Renderer();
   var toc = []; // Table of contents as a list
   var toc = []; // Table of contents as a list
+  var content; // Channel variable with content
+  var nameCounter = 0;
 
 
   renderer.heading = function (text, level) {
   renderer.heading = function (text, level) {
-      var escapedText = text.toLowerCase().replace(/[^\w]+/g, '-');
+      // Use number to avoid issues with Chinese
+      var escapedText = 'toc_' + nameCounter++;
       toc.push({
       toc.push({
         level: level,
         level: level,
         anchor: escapedText,
         anchor: escapedText,
@@ -44,11 +47,12 @@
 
 
   var markdownToHtml = function (markdown, needToc) {
   var markdownToHtml = function (markdown, needToc) {
       toc = [];
       toc = [];
+      var html = marked(markdown, { renderer: renderer });
+      nameCounter = 0;
       if (needToc) {
       if (needToc) {
-          var html = marked(markdown, { renderer: renderer });
           return html.replace(/<p>\[TOC\]<\/p>/ig, '<div class="vnote-toc"></div>');
           return html.replace(/<p>\[TOC\]<\/p>/ig, '<div class="vnote-toc"></div>');
       } else {
       } else {
-          return marked(markdown);
+          return html;
       }
       }
   };
   };
 
 
@@ -74,7 +78,7 @@
   };
   };
 
 
   var itemToHtml = function (item) {
   var itemToHtml = function (item) {
-      return '<a href=#' + item.anchor + '>' + item.title + '</a>';
+      return '<a href="#' + item.anchor + '">' + item.title + '</a>';
   };
   };
 
 
   // Turn a perfect toc to a tree using <ul>
   // Turn a perfect toc to a tree using <ul>
@@ -118,11 +122,16 @@
       return front;
       return front;
   };
   };
 
 
-  var addToc = function() {
+  var handleToc = function(needToc) {
       var tocTree = tocToTree(toPerfectToc(toc));
       var tocTree = tocToTree(toPerfectToc(toc));
-      var eles = document.getElementsByClassName('vnote-toc');
-      for (var i = 0; i < eles.length; ++i) {
-          eles[i].innerHTML = tocTree;
+      content.setToc(tocTree);
+
+      // Add it to html
+      if (needToc) {
+          var eles = document.getElementsByClassName('vnote-toc');
+          for (var i = 0; i < eles.length; ++i) {
+              eles[i].innerHTML = tocTree;
+          }
       }
       }
   };
   };
 
 
@@ -136,16 +145,25 @@
       var html = markdownToHtml(text, needToc);
       var html = markdownToHtml(text, needToc);
       placeholder.innerHTML = html;
       placeholder.innerHTML = html;
 
 
-      if (needToc) {
-          addToc();
+      handleToc(needToc);
+  };
+
+  var scrollToAnchor = function(anchor) {
+      var eles = document.getElementsByTagName('a');
+      for (var i = 0; i < eles.length; ++i) {
+         if (eles[i].name == anchor) {
+            eles[i].scrollIntoView();
+            break;
+         }
       }
       }
   };
   };
 
 
   new QWebChannel(qt.webChannelTransport,
   new QWebChannel(qt.webChannelTransport,
     function(channel) {
     function(channel) {
-      var content = channel.objects.content;
+      content = channel.objects.content;
       updateText(content.text);
       updateText(content.text);
       content.textChanged.connect(updateText);
       content.textChanged.connect(updateText);
+      content.requestScrollToAnchor.connect(scrollToAnchor);
     }
     }
   );
   );
   </script>
   </script>

+ 6 - 2
src/src.pro

@@ -41,7 +41,9 @@ SOURCES += main.cpp\
     vdownloader.cpp \
     vdownloader.cpp \
     veditarea.cpp \
     veditarea.cpp \
     veditwindow.cpp \
     veditwindow.cpp \
-    vedittab.cpp
+    vedittab.cpp \
+    voutline.cpp \
+    vtoc.cpp
 
 
 HEADERS  += vmainwindow.h \
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
     vdirectorytree.h \
@@ -73,7 +75,9 @@ HEADERS  += vmainwindow.h \
     vdownloader.h \
     vdownloader.h \
     veditarea.h \
     veditarea.h \
     veditwindow.h \
     veditwindow.h \
-    vedittab.h
+    vedittab.h \
+    voutline.h \
+    vtoc.h
 
 
 RESOURCES += \
 RESOURCES += \
     vnote.qrc
     vnote.qrc

+ 21 - 3
src/vdocument.cpp

@@ -1,7 +1,5 @@
 #include "vdocument.h"
 #include "vdocument.h"
 
 
-#include <QtDebug>
-
 VDocument::VDocument(QObject *parent) : QObject(parent)
 VDocument::VDocument(QObject *parent) : QObject(parent)
 {
 {
 
 
@@ -15,8 +13,9 @@ VDocument::VDocument(const QString &text, QObject *parent)
 
 
 void VDocument::setText(const QString &text)
 void VDocument::setText(const QString &text)
 {
 {
-    if (text == m_text)
+    if (text == m_text) {
         return;
         return;
+    }
     m_text = text;
     m_text = text;
     emit textChanged(m_text);
     emit textChanged(m_text);
 }
 }
@@ -25,3 +24,22 @@ QString VDocument::getText()
 {
 {
     return m_text;
     return m_text;
 }
 }
+
+void VDocument::setToc(const QString &toc)
+{
+    if (toc == m_toc) {
+        return;
+    }
+    m_toc = toc;
+    emit tocChanged(m_toc);
+}
+
+QString VDocument::getToc()
+{
+    return m_toc;
+}
+
+void VDocument::scrollToAnchor(const QString &anchor)
+{
+    emit requestScrollToAnchor(anchor);
+}

+ 11 - 0
src/vdocument.h

@@ -8,17 +8,28 @@ class VDocument : public QObject
 {
 {
     Q_OBJECT
     Q_OBJECT
     Q_PROPERTY(QString text MEMBER m_text NOTIFY textChanged)
     Q_PROPERTY(QString text MEMBER m_text NOTIFY textChanged)
+    Q_PROPERTY(QString toc MEMBER m_toc NOTIFY tocChanged)
+
 public:
 public:
     explicit VDocument(QObject *parent = 0);
     explicit VDocument(QObject *parent = 0);
     VDocument(const QString &text, QObject *parent = 0);
     VDocument(const QString &text, QObject *parent = 0);
     void setText(const QString &text);
     void setText(const QString &text);
     QString getText();
     QString getText();
+    QString getToc();
+    void scrollToAnchor(const QString &anchor);
+
+public slots:
+    // Will be called in the HTML side
+    void setToc(const QString &toc);
 
 
 signals:
 signals:
     void textChanged(const QString &text);
     void textChanged(const QString &text);
+    void tocChanged(const QString &toc);
+    void requestScrollToAnchor(const QString &anchor);
 
 
 private:
 private:
     QString m_text;
     QString m_text;
+    QString m_toc;
 };
 };
 
 
 #endif // VDOCUMENT_H
 #endif // VDOCUMENT_H

+ 17 - 0
src/veditarea.cpp

@@ -37,6 +37,8 @@ void VEditArea::insertSplitWindow(int idx)
             this, &VEditArea::handleRemoveSplitRequest);
             this, &VEditArea::handleRemoveSplitRequest);
     connect(win, &VEditWindow::getFocused,
     connect(win, &VEditWindow::getFocused,
             this, &VEditArea::handleWindowFocused);
             this, &VEditArea::handleWindowFocused);
+    connect(win, &VEditWindow::outlineChanged,
+            this, &VEditArea::handleOutlineChanged);
 
 
     int nrWin = splitter->count();
     int nrWin = splitter->count();
     if (nrWin == 1) {
     if (nrWin == 1) {
@@ -163,6 +165,7 @@ void VEditArea::setCurrentWindow(int windowIndex, bool setFocus)
 out:
 out:
     // Update tab status
     // Update tab status
     noticeTabStatus();
     noticeTabStatus();
+    emit outlineChanged(getWindow(windowIndex)->getTabOutline());
 }
 }
 
 
 void VEditArea::noticeTabStatus()
 void VEditArea::noticeTabStatus()
@@ -278,3 +281,17 @@ void VEditArea::handleWindowFocused()
         }
         }
     }
     }
 }
 }
+
+void VEditArea::handleOutlineChanged(const VToc &toc)
+{
+    QObject *winObject = sender();
+    if (splitter->widget(curWindowIndex) == winObject) {
+        emit outlineChanged(toc);
+    }
+}
+
+void VEditArea::handleOutlineItemActivated(const VAnchor &anchor)
+{
+    // Notice current window
+    getWindow(curWindowIndex)->scrollCurTab(anchor);
+}

+ 4 - 0
src/veditarea.h

@@ -12,6 +12,7 @@
 #include <QSplitter>
 #include <QSplitter>
 #include "vnotebook.h"
 #include "vnotebook.h"
 #include "veditwindow.h"
 #include "veditwindow.h"
+#include "vtoc.h"
 
 
 class VNote;
 class VNote;
 
 
@@ -24,6 +25,7 @@ public:
 signals:
 signals:
     void curTabStatusChanged(const QString &notebook, const QString &relativePath,
     void curTabStatusChanged(const QString &notebook, const QString &relativePath,
                              bool editMode, bool modifiable);
                              bool editMode, bool modifiable);
+    void outlineChanged(const VToc &toc);
 
 
 protected:
 protected:
     void mousePressEvent(QMouseEvent *event) Q_DECL_OVERRIDE;
     void mousePressEvent(QMouseEvent *event) Q_DECL_OVERRIDE;
@@ -38,11 +40,13 @@ public slots:
     void saveAndReadFile();
     void saveAndReadFile();
     void handleNotebookRenamed(const QVector<VNotebook> &notebooks, const QString &oldName,
     void handleNotebookRenamed(const QVector<VNotebook> &notebooks, const QString &oldName,
                                const QString &newName);
                                const QString &newName);
+    void handleOutlineItemActivated(const VAnchor &anchor);
 
 
 private slots:
 private slots:
     void handleSplitWindowRequest(VEditWindow *curWindow);
     void handleSplitWindowRequest(VEditWindow *curWindow);
     void handleRemoveSplitRequest(VEditWindow *curWindow);
     void handleRemoveSplitRequest(VEditWindow *curWindow);
     void handleWindowFocused();
     void handleWindowFocused();
+    void handleOutlineChanged(const VToc &toc);
 
 
 private:
 private:
     void setupUI();
     void setupUI();

+ 112 - 0
src/vedittab.cpp

@@ -3,6 +3,7 @@
 #include <QWebChannel>
 #include <QWebChannel>
 #include <QWebEngineView>
 #include <QWebEngineView>
 #include <QFileInfo>
 #include <QFileInfo>
+#include <QXmlStreamReader>
 #include "vedittab.h"
 #include "vedittab.h"
 #include "vedit.h"
 #include "vedit.h"
 #include "vdocument.h"
 #include "vdocument.h"
@@ -13,6 +14,7 @@
 #include "vconfigmanager.h"
 #include "vconfigmanager.h"
 #include "vmarkdownconverter.h"
 #include "vmarkdownconverter.h"
 #include "vnotebook.h"
 #include "vnotebook.h"
+#include "vtoc.h"
 
 
 extern VConfigManager vconfig;
 extern VConfigManager vconfig;
 
 
@@ -117,6 +119,8 @@ void VEditTab::previewByConverter()
     html.replace(tocExp, toc);
     html.replace(tocExp, toc);
     QString completeHtml = VNote::preTemplateHtml + html + VNote::postTemplateHtml;
     QString completeHtml = VNote::preTemplateHtml + html + VNote::postTemplateHtml;
     webPreviewer->setHtml(completeHtml, QUrl::fromLocalFile(noteFile->basePath + QDir::separator()));
     webPreviewer->setHtml(completeHtml, QUrl::fromLocalFile(noteFile->basePath + QDir::separator()));
+    // Hoedown will add '\n' while Marked does not
+    updateTocFromHtml(toc.replace("\n", ""));
 }
 }
 
 
 void VEditTab::showFileEditMode()
 void VEditTab::showFileEditMode()
@@ -216,6 +220,8 @@ void VEditTab::setupMarkdownPreview()
     if (mdConverterType == MarkdownConverterType::Marked) {
     if (mdConverterType == MarkdownConverterType::Marked) {
         QWebChannel *channel = new QWebChannel(this);
         QWebChannel *channel = new QWebChannel(this);
         channel->registerObject(QStringLiteral("content"), &document);
         channel->registerObject(QStringLiteral("content"), &document);
+        connect(&document, &VDocument::tocChanged,
+                this, &VEditTab::updateTocFromHtml);
         page->setWebChannel(channel);
         page->setWebChannel(channel);
         webPreviewer->setHtml(VNote::templateHtml,
         webPreviewer->setHtml(VNote::templateHtml,
                               QUrl::fromLocalFile(noteFile->basePath + QDir::separator()));
                               QUrl::fromLocalFile(noteFile->basePath + QDir::separator()));
@@ -235,3 +241,109 @@ void VEditTab::handleFocusChanged(QWidget *old, QWidget *now)
         emit getFocused();
         emit getFocused();
     }
     }
 }
 }
+
+void VEditTab::updateTocFromHtml(const QString &tocHtml)
+{
+    qDebug() << tocHtml;
+    tableOfContent.type = VHeaderType::Anchor;
+    QVector<VHeader> &headers = tableOfContent.headers;
+    headers.clear();
+
+    QXmlStreamReader xml(tocHtml);
+    if (xml.readNextStartElement()) {
+        if (xml.name() == "ul") {
+            parseTocUl(xml, headers, 1);
+        } else {
+            qWarning() << "error: TOC HTML does not start with <ul>";
+        }
+    }
+    if (xml.hasError()) {
+        qWarning() << "error: fail to parse TOC in HTML";
+        return;
+    }
+
+    tableOfContent.curHeaderIndex = 0;
+    tableOfContent.filePath = QDir::cleanPath(QDir(noteFile->basePath).filePath(noteFile->fileName));
+    tableOfContent.valid = true;
+
+    emit outlineChanged(tableOfContent);
+}
+
+void VEditTab::parseTocUl(QXmlStreamReader &xml, QVector<VHeader> &headers, int level)
+{
+    Q_ASSERT(xml.isStartElement() && xml.name() == "ul");
+
+    while (xml.readNextStartElement()) {
+        if (xml.name() == "li") {
+            parseTocLi(xml, headers, level);
+        } else {
+            qWarning() << "error: TOC HTML <ul> should contain <li>" << xml.name();
+            break;
+        }
+    }
+}
+
+void VEditTab::parseTocLi(QXmlStreamReader &xml, QVector<VHeader> &headers, int level)
+{
+    Q_ASSERT(xml.isStartElement() && xml.name() == "li");
+
+    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() << "error: TOC HTML <a> should be ended by </a>" << xml.name();
+                    return;
+                }
+                VHeader header;
+                header.level = level;
+                header.name = name;
+                header.anchor = anchor;
+                header.lineNumber = -1;
+                headers.append(header);
+            } else {
+                // Error
+                return;
+            }
+        } else {
+            qWarning() << "error: 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;
+        }
+    }
+}
+
+const VToc& VEditTab::getOutline() const
+{
+    return tableOfContent;
+}
+
+void VEditTab::scrollToAnchor(const VAnchor &anchor)
+{
+    if (isEditMode) {
+        if (anchor.lineNumber > -1) {
+
+        }
+    } else {
+        if (!anchor.anchor.isEmpty()) {
+            document.scrollToAnchor(anchor.anchor.mid(1));
+        }
+    }
+}

+ 9 - 0
src/vedittab.h

@@ -9,10 +9,12 @@
 #include "vmarkdownconverter.h"
 #include "vmarkdownconverter.h"
 #include "vconfigmanager.h"
 #include "vconfigmanager.h"
 #include "vedit.h"
 #include "vedit.h"
+#include "vtoc.h"
 
 
 class QTextBrowser;
 class QTextBrowser;
 class QWebEngineView;
 class QWebEngineView;
 class VNote;
 class VNote;
+class QXmlStreamReader;
 
 
 class VEditTab : public QStackedWidget
 class VEditTab : public QStackedWidget
 {
 {
@@ -31,12 +33,16 @@ public:
     inline bool getIsEditMode() const;
     inline bool getIsEditMode() const;
     inline bool isModified() const;
     inline bool isModified() const;
     void focusTab();
     void focusTab();
+    const VToc& getOutline() const;
+    void scrollToAnchor(const VAnchor& anchor);
 
 
 signals:
 signals:
     void getFocused();
     void getFocused();
+    void outlineChanged(const VToc &toc);
 
 
 private slots:
 private slots:
     void handleFocusChanged(QWidget *old, QWidget *now);
     void handleFocusChanged(QWidget *old, QWidget *now);
+    void updateTocFromHtml(const QString &tocHtml);
 
 
 private:
 private:
     bool isMarkdown(const QString &name);
     bool isMarkdown(const QString &name);
@@ -46,6 +52,8 @@ private:
     void setupMarkdownPreview();
     void setupMarkdownPreview();
     void previewByConverter();
     void previewByConverter();
     inline bool isChild(QObject *obj);
     inline bool isChild(QObject *obj);
+    void parseTocUl(QXmlStreamReader &xml, QVector<VHeader> &headers, int level);
+    void parseTocLi(QXmlStreamReader &xml, QVector<VHeader> &headers, int level);
 
 
     VNoteFile *noteFile;
     VNoteFile *noteFile;
     bool isEditMode;
     bool isEditMode;
@@ -54,6 +62,7 @@ private:
     QWebEngineView *webPreviewer;
     QWebEngineView *webPreviewer;
     VDocument document;
     VDocument document;
     MarkdownConverterType mdConverterType;
     MarkdownConverterType mdConverterType;
+    VToc tableOfContent;
 };
 };
 
 
 inline bool VEditTab::getIsEditMode() const
 inline bool VEditTab::getIsEditMode() const

+ 40 - 0
src/veditwindow.cpp

@@ -165,6 +165,8 @@ int VEditWindow::openFileInTab(const QString &notebook, const QString &relativeP
                                     modifiable);
                                     modifiable);
     connect(editor, &VEditTab::getFocused,
     connect(editor, &VEditTab::getFocused,
             this, &VEditWindow::getFocused);
             this, &VEditWindow::getFocused);
+    connect(editor, &VEditTab::outlineChanged,
+            this, &VEditWindow::handleOutlineChanged);
 
 
     QJsonObject tabJson;
     QJsonObject tabJson;
     tabJson["notebook"] = notebook;
     tabJson["notebook"] = notebook;
@@ -290,6 +292,16 @@ void VEditWindow::getTabStatus(QString &notebook, QString &relativePath,
     modifiable = tabJson["modifiable"].toBool();
     modifiable = tabJson["modifiable"].toBool();
 }
 }
 
 
+VToc VEditWindow::getTabOutline() const
+{
+    int idx = currentIndex();
+    if (idx == -1) {
+        return VToc();
+    }
+
+    return getTab(idx)->getOutline();
+}
+
 void VEditWindow::focusWindow()
 void VEditWindow::focusWindow()
 {
 {
     int idx = currentIndex();
     int idx = currentIndex();
@@ -305,6 +317,7 @@ void VEditWindow::handleTabbarClicked(int index)
     // The child will emit getFocused here
     // The child will emit getFocused here
     focusWindow();
     focusWindow();
     noticeTabStatus(index);
     noticeTabStatus(index);
+    emit outlineChanged(getTab(index)->getOutline());
 }
 }
 
 
 void VEditWindow::mousePressEvent(QMouseEvent *event)
 void VEditWindow::mousePressEvent(QMouseEvent *event)
@@ -356,3 +369,30 @@ void VEditWindow::updateTabListMenu()
         menu->addAction(action);
         menu->addAction(action);
     }
     }
 }
 }
+
+void VEditWindow::handleOutlineChanged(const VToc &toc)
+{
+    // Only propagate it if it is current tab
+    int idx = currentIndex();
+    QJsonObject tabJson = tabBar()->tabData(idx).toJsonObject();
+    Q_ASSERT(!tabJson.isEmpty());
+    QString path = vnote->getNotebookPath(tabJson["notebook"].toString());
+    path = QDir::cleanPath(QDir(path).filePath(tabJson["relative_path"].toString()));
+
+    if (toc.filePath == path) {
+        emit outlineChanged(toc);
+    }
+}
+
+void VEditWindow::scrollCurTab(const VAnchor &anchor)
+{
+    int idx = currentIndex();
+    QJsonObject tabJson = tabBar()->tabData(idx).toJsonObject();
+    Q_ASSERT(!tabJson.isEmpty());
+    QString path = vnote->getNotebookPath(tabJson["notebook"].toString());
+    path = QDir::cleanPath(QDir(path).filePath(tabJson["relative_path"].toString()));
+
+    if (path == anchor.filePath) {
+        getTab(idx)->scrollToAnchor(anchor);
+    }
+}

+ 5 - 0
src/veditwindow.h

@@ -8,6 +8,7 @@
 #include <QDir>
 #include <QDir>
 #include "vnotebook.h"
 #include "vnotebook.h"
 #include "vedittab.h"
 #include "vedittab.h"
+#include "vtoc.h"
 
 
 class VNote;
 class VNote;
 class QPushButton;
 class QPushButton;
@@ -33,8 +34,10 @@ public:
     void setRemoveSplitEnable(bool enabled);
     void setRemoveSplitEnable(bool enabled);
     void getTabStatus(QString &notebook, QString &relativePath,
     void getTabStatus(QString &notebook, QString &relativePath,
                       bool &editMode, bool &modifiable) const;
                       bool &editMode, bool &modifiable) const;
+    VToc getTabOutline() const;
     // Focus to current tab's editor
     // Focus to current tab's editor
     void focusWindow();
     void focusWindow();
+    void scrollCurTab(const VAnchor &anchor);
 
 
 protected:
 protected:
     void mousePressEvent(QMouseEvent *event) Q_DECL_OVERRIDE;
     void mousePressEvent(QMouseEvent *event) Q_DECL_OVERRIDE;
@@ -46,6 +49,7 @@ signals:
     void requestRemoveSplit(VEditWindow *curWindow);
     void requestRemoveSplit(VEditWindow *curWindow);
     // This widget or its children get the focus
     // This widget or its children get the focus
     void getFocused();
     void getFocused();
+    void outlineChanged(const VToc &toc);
 
 
 private slots:
 private slots:
     bool handleTabCloseRequest(int index);
     bool handleTabCloseRequest(int index);
@@ -54,6 +58,7 @@ private slots:
     void handleTabbarClicked(int index);
     void handleTabbarClicked(int index);
     void contextMenuRequested(QPoint pos);
     void contextMenuRequested(QPoint pos);
     void tabListJump(QAction *action);
     void tabListJump(QAction *action);
+    void handleOutlineChanged(const VToc &toc);
 
 
 private:
 private:
     void setupCornerWidget();
     void setupCornerWidget();

+ 19 - 1
src/vmainwindow.cpp

@@ -9,6 +9,7 @@
 #include "dialog/vnotebookinfodialog.h"
 #include "dialog/vnotebookinfodialog.h"
 #include "utils/vutils.h"
 #include "utils/vutils.h"
 #include "veditarea.h"
 #include "veditarea.h"
+#include "voutline.h"
 
 
 extern VConfigManager vconfig;
 extern VConfigManager vconfig;
 
 
@@ -22,6 +23,7 @@ VMainWindow::VMainWindow(QWidget *parent)
     initActions();
     initActions();
     initToolBar();
     initToolBar();
     initMenuBar();
     initMenuBar();
+    initDockWindows();
 
 
     updateNotebookComboBox(vnote->getNotebooks());
     updateNotebookComboBox(vnote->getNotebooks());
 }
 }
@@ -283,7 +285,7 @@ void VMainWindow::initMenuBar()
 {
 {
     QMenu *fileMenu = menuBar()->addMenu(tr("&File"));
     QMenu *fileMenu = menuBar()->addMenu(tr("&File"));
     QMenu *editMenu = menuBar()->addMenu(tr("&Edit"));
     QMenu *editMenu = menuBar()->addMenu(tr("&Edit"));
-    QMenu *viewMenu = menuBar()->addMenu(tr("&View"));
+    viewMenu = menuBar()->addMenu(tr("&View"));
     QMenu *markdownMenu = menuBar()->addMenu(tr("&Markdown"));
     QMenu *markdownMenu = menuBar()->addMenu(tr("&Markdown"));
     QMenu *helpMenu = menuBar()->addMenu(tr("&Help"));
     QMenu *helpMenu = menuBar()->addMenu(tr("&Help"));
 
 
@@ -335,6 +337,22 @@ void VMainWindow::initMenuBar()
     helpMenu->addAction(aboutAct);
     helpMenu->addAction(aboutAct);
 }
 }
 
 
+void VMainWindow::initDockWindows()
+{
+    QDockWidget *dock = new QDockWidget(tr("Tools"), this);
+    dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
+    toolBox = new QToolBox(this);
+    outline = new VOutline(this);
+    connect(editArea, &VEditArea::outlineChanged,
+            outline, &VOutline::updateOutline);
+    connect(outline, &VOutline::outlineItemActivated,
+            editArea, &VEditArea::handleOutlineItemActivated);
+    toolBox->addItem(outline, QIcon(":/resources/icons/outline.svg"), tr("Outline"));
+    dock->setWidget(toolBox);
+    addDockWidget(Qt::RightDockWidgetArea, dock);
+    viewMenu->addAction(dock->toggleViewAction());
+}
+
 void VMainWindow::updateNotebookComboBox(const QVector<VNotebook> &notebooks)
 void VMainWindow::updateNotebookComboBox(const QVector<VNotebook> &notebooks)
 {
 {
     notebookComboMuted = true;
     notebookComboMuted = true;

+ 8 - 0
src/vmainwindow.h

@@ -17,6 +17,8 @@ class VNotebook;
 class QActionGroup;
 class QActionGroup;
 class VFileList;
 class VFileList;
 class VEditArea;
 class VEditArea;
+class QToolBox;
+class VOutline;
 
 
 class VMainWindow : public QMainWindow
 class VMainWindow : public QMainWindow
 {
 {
@@ -57,6 +59,7 @@ private:
     void initActions();
     void initActions();
     void initToolBar();
     void initToolBar();
     void initMenuBar();
     void initMenuBar();
+    void initDockWindows();
     bool isConflictWithExistingNotebooks(const QString &name);
     bool isConflictWithExistingNotebooks(const QString &name);
     void initPredefinedColorPixmaps();
     void initPredefinedColorPixmaps();
     void initRenderBackgroundMenu(QMenu *menu);
     void initRenderBackgroundMenu(QMenu *menu);
@@ -80,6 +83,8 @@ private:
     VDirectoryTree *directoryTree;
     VDirectoryTree *directoryTree;
     QSplitter *mainSplitter;
     QSplitter *mainSplitter;
     VEditArea *editArea;
     VEditArea *editArea;
+    QToolBox *toolBox;
+    VOutline *outline;
 
 
     // Actions
     // Actions
     QAction *newNoteAct;
     QAction *newNoteAct;
@@ -105,6 +110,9 @@ private:
     QActionGroup *backgroundColorAct;
     QActionGroup *backgroundColorAct;
     QActionGroup *renderBackgroundAct;
     QActionGroup *renderBackgroundAct;
 
 
+    // Menus
+    QMenu *viewMenu;
+
     QVector<QPixmap> predefinedColorPixmaps;
     QVector<QPixmap> predefinedColorPixmaps;
 };
 };
 
 

+ 20 - 0
src/vnote.cpp

@@ -120,6 +120,8 @@ void VNote::removeNotebook(const QString &name)
     } else {
     } else {
         qDebug() << "delete" << path << "recursively";
         qDebug() << "delete" << path << "recursively";
     }
     }
+
+    notebookPathHash.remove(name);
     emit notebooksDeleted(notebooks, name);
     emit notebooksDeleted(notebooks, name);
 }
 }
 
 
@@ -140,5 +142,23 @@ void VNote::renameNotebook(const QString &name, const QString &newName)
     notebooks[index].setName(newName);
     notebooks[index].setName(newName);
     vconfig.setNotebooks(notebooks);
     vconfig.setNotebooks(notebooks);
 
 
+    notebookPathHash.remove(name);
     emit notebooksRenamed(notebooks, name, newName);
     emit notebooksRenamed(notebooks, name, newName);
 }
 }
+
+QString VNote::getNotebookPath(const QString &name)
+{
+    QString path = notebookPathHash.value(name);
+    if (path.isEmpty()) {
+        for (int i = 0; i < notebooks.size(); ++i) {
+            if (notebooks[i].getName() == name) {
+                path = notebooks[i].getPath();
+                break;
+            }
+        }
+        if (!path.isEmpty()) {
+            notebookPathHash.insert(name, path);
+        }
+    }
+    return path;
+}

+ 4 - 0
src/vnote.h

@@ -6,6 +6,7 @@
 #include <QSettings>
 #include <QSettings>
 #include <QFont>
 #include <QFont>
 #include <QObject>
 #include <QObject>
+#include <QHash>
 #include "vnotebook.h"
 #include "vnotebook.h"
 
 
 enum OpenFileMode {Read = 0, Edit};
 enum OpenFileMode {Read = 0, Edit};
@@ -31,6 +32,8 @@ public:
     void removeNotebook(const QString &name);
     void removeNotebook(const QString &name);
     void renameNotebook(const QString &name, const QString &newName);
     void renameNotebook(const QString &name, const QString &newName);
 
 
+    QString getNotebookPath(const QString &name);
+
 public slots:
 public slots:
     void updateTemplate();
     void updateTemplate();
 
 
@@ -44,6 +47,7 @@ signals:
 
 
 private:
 private:
     QVector<VNotebook> notebooks;
     QVector<VNotebook> notebooks;
+    QHash<QString, QString> notebookPathHash;
 };
 };
 
 
 #endif // VNOTE_H
 #endif // VNOTE_H

+ 1 - 0
src/vnote.qrc

@@ -57,5 +57,6 @@
         <file>resources/icons/corner_menu.svg</file>
         <file>resources/icons/corner_menu.svg</file>
         <file>resources/icons/remove_split.svg</file>
         <file>resources/icons/remove_split.svg</file>
         <file>resources/icons/corner_tablist.svg</file>
         <file>resources/icons/corner_tablist.svg</file>
+        <file>resources/icons/outline.svg</file>
     </qresource>
     </qresource>
 </RCC>
 </RCC>

+ 79 - 0
src/voutline.cpp

@@ -0,0 +1,79 @@
+#include <QDebug>
+#include <QVector>
+#include <QString>
+#include <QJsonObject>
+#include "voutline.h"
+#include "vtoc.h"
+
+VOutline::VOutline(QWidget *parent)
+    : QTreeWidget(parent)
+{
+    setColumnCount(1);
+    setHeaderHidden(true);
+
+    connect(this, &VOutline::itemClicked,
+            this, &VOutline::handleItemClicked);
+}
+
+void VOutline::updateOutline(const VToc &toc)
+{
+    outline = toc;
+    updateTreeFromOutline(outline);
+    expandTree();
+}
+
+void VOutline::updateTreeFromOutline(const VToc &toc)
+{
+    clear();
+
+    if (!toc.valid) {
+        return;
+    }
+    const QVector<VHeader> &headers = toc.headers;
+    int idx = 0;
+    updateTreeByLevel(headers, idx, NULL, NULL, 1);
+}
+
+void VOutline::updateTreeByLevel(const QVector<VHeader> &headers, int &index,
+                                 QTreeWidgetItem *parent, QTreeWidgetItem *last, int level)
+{
+    while (index < headers.size()) {
+        const VHeader &header = headers[index];
+        QTreeWidgetItem *item;
+        if (header.level == level) {
+            if (parent) {
+                item = new QTreeWidgetItem(parent);
+            } else {
+                item = new QTreeWidgetItem(this);
+            }
+            QJsonObject itemJson;
+            itemJson["anchor"] = header.anchor;
+            itemJson["line_number"] = header.lineNumber;
+            item->setData(0, Qt::UserRole, itemJson);
+            item->setText(0, header.name);
+
+            last = item;
+            ++index;
+        } else if (header.level < level) {
+            return;
+        } else {
+            updateTreeByLevel(headers, index, last, NULL, level + 1);
+        }
+    }
+}
+
+void VOutline::expandTree()
+{
+    expandAll();
+}
+
+
+void VOutline::handleItemClicked(QTreeWidgetItem *item, int column)
+{
+    Q_ASSERT(item && column == 0);
+    QJsonObject itemJson = item->data(0, Qt::UserRole).toJsonObject();
+    QString anchor = itemJson["anchor"].toString();
+    int lineNumber = itemJson["line_number"].toInt();
+    qDebug() << "click anchor" << anchor << lineNumber;
+    emit outlineItemActivated(VAnchor(outline.filePath, anchor, lineNumber));
+}

+ 31 - 0
src/voutline.h

@@ -0,0 +1,31 @@
+#ifndef VOUTLINE_H
+#define VOUTLINE_H
+
+#include <QTreeWidget>
+#include "vtoc.h"
+
+class VOutline : public QTreeWidget
+{
+    Q_OBJECT
+public:
+    VOutline(QWidget *parent = 0);
+
+signals:
+    void outlineItemActivated(const VAnchor &anchor);
+
+public slots:
+    void updateOutline(const VToc &toc);
+
+private slots:
+    void handleItemClicked(QTreeWidgetItem *item, int column);
+
+private:
+    void updateTreeFromOutline(const VToc &toc);
+    void updateTreeByLevel(const QVector<VHeader> &headers, int &index, QTreeWidgetItem *parent,
+                           QTreeWidgetItem *last, int level);
+    void expandTree();
+
+    VToc outline;
+};
+
+#endif // VOUTLINE_H

+ 6 - 0
src/vtoc.cpp

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

+ 42 - 0
src/vtoc.h

@@ -0,0 +1,42 @@
+#ifndef VTOC_H
+#define VTOC_H
+
+#include <QString>
+#include <QVector>
+
+enum VHeaderType
+{
+    Anchor = 0,
+    LineNumber
+};
+
+struct VHeader
+{
+    int level;
+    QString name;
+    QString anchor;
+    int lineNumber;
+};
+
+struct VAnchor
+{
+    VAnchor(const QString filePath, const QString &anchor, int lineNumber)
+        : filePath(filePath), anchor(anchor), lineNumber(lineNumber) {}
+    QString filePath;
+    QString anchor;
+    int lineNumber;
+};
+
+class VToc
+{
+public:
+    VToc();
+
+    QVector<VHeader> headers;
+    int curHeaderIndex;
+    int type;
+    QString filePath;
+    bool valid;
+};
+
+#endif // VTOC_H