Browse Source

use QWebEngineView to display markdown files

Thanks to [marked JavaScript library](https://github.com/chjj/marked) by
Christopher Jeffrey. The
[style sheet](http://kevinburke.bitbucket.org/markdowncss/markdown.css)
was created by Kevin Burke.

Signed-off-by: Le Tan <[email protected]>
Le Tan 9 years ago
parent
commit
7bce2cb298
21 changed files with 950 additions and 51 deletions
  1. 9 3
      VNote.pro
  2. 260 0
      resources/markdown.css
  3. 3 0
      resources/post_template.html
  4. 8 0
      resources/pre_template.html
  5. 430 0
      resources/qwebchannel.js
  6. 32 0
      resources/template.html
  7. 35 0
      utils/vutils.cpp
  8. 15 0
      utils/vutils.h
  9. 27 0
      vdocument.cpp
  10. 24 0
      vdocument.h
  11. 1 1
      vedit.cpp
  12. 41 33
      veditor.cpp
  13. 5 2
      veditor.h
  14. 1 1
      vmainwindow.cpp
  15. 5 1
      vnote.cpp
  16. 4 1
      vnote.h
  17. 10 4
      vnote.qrc
  18. 19 0
      vpreviewpage.cpp
  19. 16 0
      vpreviewpage.h
  20. 4 3
      vtabwidget.cpp
  21. 1 2
      vtabwidget.h

+ 9 - 3
VNote.pro

@@ -4,7 +4,7 @@
 #
 #-------------------------------------------------
 
-QT       += core gui
+QT       += core gui webenginewidgets webchannel
 
 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
 
@@ -24,7 +24,10 @@ SOURCES += main.cpp\
     vtabwidget.cpp \
     vedit.cpp \
     veditor.cpp \
-    vnotefile.cpp
+    vnotefile.cpp \
+    vdocument.cpp \
+    utils/vutils.cpp \
+    vpreviewpage.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -38,7 +41,10 @@ HEADERS  += vmainwindow.h \
     vedit.h \
     veditor.h \
     vconstants.h \
-    vnotefile.h
+    vnotefile.h \
+    vdocument.h \
+    utils/vutils.h \
+    vpreviewpage.h
 
 RESOURCES += \
     vnote.qrc

+ 260 - 0
resources/markdown.css

@@ -0,0 +1,260 @@
+body{
+    margin: 0 auto;
+    font-family: Georgia, Palatino, serif;
+    color: #444444;
+    line-height: 1;
+    max-width: 960px;
+    padding: 30px;
+}
+h1, h2, h3, h4 {
+    color: #111111;
+    font-weight: 400;
+}
+h1, h2, h3, h4, h5, p {
+    margin-bottom: 24px;
+    padding: 0;
+}
+h1 {
+    font-size: 48px;
+}
+h2 {
+    font-size: 36px;
+    /* The bottom margin is small. It's designed to be used with gray meta text
+     * below a post title. */
+    margin: 24px 0 6px;
+}
+h3 {
+    font-size: 24px;
+}
+h4 {
+    font-size: 21px;
+}
+h5 {
+    font-size: 18px;
+}
+a {
+    color: #0099ff;
+    margin: 0;
+    padding: 0;
+    vertical-align: baseline;
+}
+a:hover {
+    text-decoration: none;
+    color: #ff6600;
+}
+a:visited {
+    color: purple;
+}
+ul, ol {
+    padding: 0;
+    margin: 0;
+}
+li {
+    line-height: 24px;
+}
+li ul, li ul {
+    margin-left: 24px;
+}
+p, ul, ol {
+    font-size: 16px;
+    line-height: 24px;
+    max-width: 540px;
+}
+pre {
+    padding: 0px 24px;
+    max-width: 800px;
+    white-space: pre-wrap;
+}
+code {
+    font-family: Consolas, Monaco, Andale Mono, monospace;
+    line-height: 1.5;
+    font-size: 13px;
+}
+aside {
+    display: block;
+    float: right;
+    width: 390px;
+}
+blockquote {
+    border-left:.5em solid #eee;
+    padding: 0 2em;
+    margin-left:0;
+    max-width: 476px;
+}
+blockquote  cite {
+    font-size:14px;
+    line-height:20px;
+    color:#bfbfbf;
+}
+blockquote cite:before {
+    content: '\2014 \00A0';
+}
+
+blockquote p {
+    color: #666;
+    max-width: 460px;
+}
+hr {
+    width: 540px;
+    text-align: left;
+    margin: 0 auto 0 0;
+    color: #999;
+}
+
+/* Code below this line is copyright Twitter Inc. */
+
+button,
+input,
+select,
+textarea {
+  font-size: 100%;
+  margin: 0;
+  vertical-align: baseline;
+  *vertical-align: middle;
+}
+button, input {
+  line-height: normal;
+  *overflow: visible;
+}
+button::-moz-focus-inner, input::-moz-focus-inner {
+  border: 0;
+  padding: 0;
+}
+button,
+input[type="button"],
+input[type="reset"],
+input[type="submit"] {
+  cursor: pointer;
+  -webkit-appearance: button;
+}
+input[type=checkbox], input[type=radio] {
+  cursor: pointer;
+}
+/* override default chrome & firefox settings */
+input:not([type="image"]), textarea {
+  -webkit-box-sizing: content-box;
+  -moz-box-sizing: content-box;
+  box-sizing: content-box;
+}
+
+input[type="search"] {
+  -webkit-appearance: textfield;
+  -webkit-box-sizing: content-box;
+  -moz-box-sizing: content-box;
+  box-sizing: content-box;
+}
+input[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+label,
+input,
+select,
+textarea {
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-size: 13px;
+  font-weight: normal;
+  line-height: normal;
+  margin-bottom: 18px;
+}
+input[type=checkbox], input[type=radio] {
+  cursor: pointer;
+  margin-bottom: 0;
+}
+input[type=text],
+input[type=password],
+textarea,
+select {
+  display: inline-block;
+  width: 210px;
+  padding: 4px;
+  font-size: 13px;
+  font-weight: normal;
+  line-height: 18px;
+  height: 18px;
+  color: #808080;
+  border: 1px solid #ccc;
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+}
+select, input[type=file] {
+  height: 27px;
+  line-height: 27px;
+}
+textarea {
+  height: auto;
+}
+
+/* grey out placeholders */
+:-moz-placeholder {
+  color: #bfbfbf;
+}
+::-webkit-input-placeholder {
+  color: #bfbfbf;
+}
+
+input[type=text],
+input[type=password],
+select,
+textarea {
+  -webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
+  -moz-transition: border linear 0.2s, box-shadow linear 0.2s;
+  transition: border linear 0.2s, box-shadow linear 0.2s;
+  -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+  -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+input[type=text]:focus, input[type=password]:focus, textarea:focus {
+  outline: none;
+  border-color: rgba(82, 168, 236, 0.8);
+  -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
+  -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
+  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
+}
+
+/* buttons */
+button {
+  display: inline-block;
+  padding: 4px 14px;
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-size: 13px;
+  line-height: 18px;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+  -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+  background-color: #0064cd;
+  background-repeat: repeat-x;
+  background-image: -khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd));
+  background-image: -moz-linear-gradient(top, #049cdb, #0064cd);
+  background-image: -ms-linear-gradient(top, #049cdb, #0064cd);
+  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd));
+  background-image: -webkit-linear-gradient(top, #049cdb, #0064cd);
+  background-image: -o-linear-gradient(top, #049cdb, #0064cd);
+  background-image: linear-gradient(top, #049cdb, #0064cd);
+  color: #fff;
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+  border: 1px solid #004b9a;
+  border-bottom-color: #003f81;
+  -webkit-transition: 0.1s linear all;
+  -moz-transition: 0.1s linear all;
+  transition: 0.1s linear all;
+  border-color: #0064cd #0064cd #003f81;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+}
+button:hover {
+  color: #fff;
+  background-position: 0 -15px;
+  text-decoration: none;
+}
+button:active {
+  -webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+  -moz-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+  box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+button::-moz-focus-inner {
+  padding: 0;
+  border: 0;
+}

+ 3 - 0
resources/post_template.html

@@ -0,0 +1,3 @@
+</div>
+</body>
+</html>

+ 8 - 0
resources/pre_template.html

@@ -0,0 +1,8 @@
+<!doctype html>
+<html lang="en">
+<meta charset="utf-8">
+<head>
+  <link rel="stylesheet" type="text/css" href=":/resources/markdown.css">
+</head>
+<body>
+  <div id="placeholder">

+ 430 - 0
resources/qwebchannel.js

@@ -0,0 +1,430 @@
+/****************************************************************************
+**
+** Copyright (C) 2016 The Qt Company Ltd.
+** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, [email protected], author Milian Wolff <[email protected]>
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtWebChannel module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+**   * Redistributions of source code must retain the above copyright
+**     notice, this list of conditions and the following disclaimer.
+**   * Redistributions in binary form must reproduce the above copyright
+**     notice, this list of conditions and the following disclaimer in
+**     the documentation and/or other materials provided with the
+**     distribution.
+**   * Neither the name of The Qt Company Ltd nor the names of its
+**     contributors may be used to endorse or promote products derived
+**     from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+"use strict";
+
+var QWebChannelMessageTypes = {
+    signal: 1,
+    propertyUpdate: 2,
+    init: 3,
+    idle: 4,
+    debug: 5,
+    invokeMethod: 6,
+    connectToSignal: 7,
+    disconnectFromSignal: 8,
+    setProperty: 9,
+    response: 10,
+};
+
+var QWebChannel = function(transport, initCallback)
+{
+    if (typeof transport !== "object" || typeof transport.send !== "function") {
+        console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." +
+                      " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send));
+        return;
+    }
+
+    var channel = this;
+    this.transport = transport;
+
+    this.send = function(data)
+    {
+        if (typeof(data) !== "string") {
+            data = JSON.stringify(data);
+        }
+        channel.transport.send(data);
+    }
+
+    this.transport.onmessage = function(message)
+    {
+        var data = message.data;
+        if (typeof data === "string") {
+            data = JSON.parse(data);
+        }
+        switch (data.type) {
+            case QWebChannelMessageTypes.signal:
+                channel.handleSignal(data);
+                break;
+            case QWebChannelMessageTypes.response:
+                channel.handleResponse(data);
+                break;
+            case QWebChannelMessageTypes.propertyUpdate:
+                channel.handlePropertyUpdate(data);
+                break;
+            default:
+                console.error("invalid message received:", message.data);
+                break;
+        }
+    }
+
+    this.execCallbacks = {};
+    this.execId = 0;
+    this.exec = function(data, callback)
+    {
+        if (!callback) {
+            // if no callback is given, send directly
+            channel.send(data);
+            return;
+        }
+        if (channel.execId === Number.MAX_VALUE) {
+            // wrap
+            channel.execId = Number.MIN_VALUE;
+        }
+        if (data.hasOwnProperty("id")) {
+            console.error("Cannot exec message with property id: " + JSON.stringify(data));
+            return;
+        }
+        data.id = channel.execId++;
+        channel.execCallbacks[data.id] = callback;
+        channel.send(data);
+    };
+
+    this.objects = {};
+
+    this.handleSignal = function(message)
+    {
+        var object = channel.objects[message.object];
+        if (object) {
+            object.signalEmitted(message.signal, message.args);
+        } else {
+            console.warn("Unhandled signal: " + message.object + "::" + message.signal);
+        }
+    }
+
+    this.handleResponse = function(message)
+    {
+        if (!message.hasOwnProperty("id")) {
+            console.error("Invalid response message received: ", JSON.stringify(message));
+            return;
+        }
+        channel.execCallbacks[message.id](message.data);
+        delete channel.execCallbacks[message.id];
+    }
+
+    this.handlePropertyUpdate = function(message)
+    {
+        for (var i in message.data) {
+            var data = message.data[i];
+            var object = channel.objects[data.object];
+            if (object) {
+                object.propertyUpdate(data.signals, data.properties);
+            } else {
+                console.warn("Unhandled property update: " + data.object + "::" + data.signal);
+            }
+        }
+        channel.exec({type: QWebChannelMessageTypes.idle});
+    }
+
+    this.debug = function(message)
+    {
+        channel.send({type: QWebChannelMessageTypes.debug, data: message});
+    };
+
+    channel.exec({type: QWebChannelMessageTypes.init}, function(data) {
+        for (var objectName in data) {
+            var object = new QObject(objectName, data[objectName], channel);
+        }
+        // now unwrap properties, which might reference other registered objects
+        for (var objectName in channel.objects) {
+            channel.objects[objectName].unwrapProperties();
+        }
+        if (initCallback) {
+            initCallback(channel);
+        }
+        channel.exec({type: QWebChannelMessageTypes.idle});
+    });
+};
+
+function QObject(name, data, webChannel)
+{
+    this.__id__ = name;
+    webChannel.objects[name] = this;
+
+    // List of callbacks that get invoked upon signal emission
+    this.__objectSignals__ = {};
+
+    // Cache of all properties, updated when a notify signal is emitted
+    this.__propertyCache__ = {};
+
+    var object = this;
+
+    // ----------------------------------------------------------------------
+
+    this.unwrapQObject = function(response)
+    {
+        if (response instanceof Array) {
+            // support list of objects
+            var ret = new Array(response.length);
+            for (var i = 0; i < response.length; ++i) {
+                ret[i] = object.unwrapQObject(response[i]);
+            }
+            return ret;
+        }
+        if (!response
+            || !response["__QObject*__"]
+            || response.id === undefined) {
+            return response;
+        }
+
+        var objectId = response.id;
+        if (webChannel.objects[objectId])
+            return webChannel.objects[objectId];
+
+        if (!response.data) {
+            console.error("Cannot unwrap unknown QObject " + objectId + " without data.");
+            return;
+        }
+
+        var qObject = new QObject( objectId, response.data, webChannel );
+        qObject.destroyed.connect(function() {
+            if (webChannel.objects[objectId] === qObject) {
+                delete webChannel.objects[objectId];
+                // reset the now deleted QObject to an empty {} object
+                // just assigning {} though would not have the desired effect, but the
+                // below also ensures all external references will see the empty map
+                // NOTE: this detour is necessary to workaround QTBUG-40021
+                var propertyNames = [];
+                for (var propertyName in qObject) {
+                    propertyNames.push(propertyName);
+                }
+                for (var idx in propertyNames) {
+                    delete qObject[propertyNames[idx]];
+                }
+            }
+        });
+        // here we are already initialized, and thus must directly unwrap the properties
+        qObject.unwrapProperties();
+        return qObject;
+    }
+
+    this.unwrapProperties = function()
+    {
+        for (var propertyIdx in object.__propertyCache__) {
+            object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]);
+        }
+    }
+
+    function addSignal(signalData, isPropertyNotifySignal)
+    {
+        var signalName = signalData[0];
+        var signalIndex = signalData[1];
+        object[signalName] = {
+            connect: function(callback) {
+                if (typeof(callback) !== "function") {
+                    console.error("Bad callback given to connect to signal " + signalName);
+                    return;
+                }
+
+                object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
+                object.__objectSignals__[signalIndex].push(callback);
+
+                if (!isPropertyNotifySignal && signalName !== "destroyed") {
+                    // only required for "pure" signals, handled separately for properties in propertyUpdate
+                    // also note that we always get notified about the destroyed signal
+                    webChannel.exec({
+                        type: QWebChannelMessageTypes.connectToSignal,
+                        object: object.__id__,
+                        signal: signalIndex
+                    });
+                }
+            },
+            disconnect: function(callback) {
+                if (typeof(callback) !== "function") {
+                    console.error("Bad callback given to disconnect from signal " + signalName);
+                    return;
+                }
+                object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
+                var idx = object.__objectSignals__[signalIndex].indexOf(callback);
+                if (idx === -1) {
+                    console.error("Cannot find connection of signal " + signalName + " to " + callback.name);
+                    return;
+                }
+                object.__objectSignals__[signalIndex].splice(idx, 1);
+                if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) {
+                    // only required for "pure" signals, handled separately for properties in propertyUpdate
+                    webChannel.exec({
+                        type: QWebChannelMessageTypes.disconnectFromSignal,
+                        object: object.__id__,
+                        signal: signalIndex
+                    });
+                }
+            }
+        };
+    }
+
+    /**
+     * Invokes all callbacks for the given signalname. Also works for property notify callbacks.
+     */
+    function invokeSignalCallbacks(signalName, signalArgs)
+    {
+        var connections = object.__objectSignals__[signalName];
+        if (connections) {
+            connections.forEach(function(callback) {
+                callback.apply(callback, signalArgs);
+            });
+        }
+    }
+
+    this.propertyUpdate = function(signals, propertyMap)
+    {
+        // update property cache
+        for (var propertyIndex in propertyMap) {
+            var propertyValue = propertyMap[propertyIndex];
+            object.__propertyCache__[propertyIndex] = propertyValue;
+        }
+
+        for (var signalName in signals) {
+            // Invoke all callbacks, as signalEmitted() does not. This ensures the
+            // property cache is updated before the callbacks are invoked.
+            invokeSignalCallbacks(signalName, signals[signalName]);
+        }
+    }
+
+    this.signalEmitted = function(signalName, signalArgs)
+    {
+        invokeSignalCallbacks(signalName, signalArgs);
+    }
+
+    function addMethod(methodData)
+    {
+        var methodName = methodData[0];
+        var methodIdx = methodData[1];
+        object[methodName] = function() {
+            var args = [];
+            var callback;
+            for (var i = 0; i < arguments.length; ++i) {
+                if (typeof arguments[i] === "function")
+                    callback = arguments[i];
+                else
+                    args.push(arguments[i]);
+            }
+
+            webChannel.exec({
+                "type": QWebChannelMessageTypes.invokeMethod,
+                "object": object.__id__,
+                "method": methodIdx,
+                "args": args
+            }, function(response) {
+                if (response !== undefined) {
+                    var result = object.unwrapQObject(response);
+                    if (callback) {
+                        (callback)(result);
+                    }
+                }
+            });
+        };
+    }
+
+    function bindGetterSetter(propertyInfo)
+    {
+        var propertyIndex = propertyInfo[0];
+        var propertyName = propertyInfo[1];
+        var notifySignalData = propertyInfo[2];
+        // initialize property cache with current value
+        // NOTE: if this is an object, it is not directly unwrapped as it might
+        // reference other QObject that we do not know yet
+        object.__propertyCache__[propertyIndex] = propertyInfo[3];
+
+        if (notifySignalData) {
+            if (notifySignalData[0] === 1) {
+                // signal name is optimized away, reconstruct the actual name
+                notifySignalData[0] = propertyName + "Changed";
+            }
+            addSignal(notifySignalData, true);
+        }
+
+        Object.defineProperty(object, propertyName, {
+            configurable: true,
+            get: function () {
+                var propertyValue = object.__propertyCache__[propertyIndex];
+                if (propertyValue === undefined) {
+                    // This shouldn't happen
+                    console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__);
+                }
+
+                return propertyValue;
+            },
+            set: function(value) {
+                if (value === undefined) {
+                    console.warn("Property setter for " + propertyName + " called with undefined value!");
+                    return;
+                }
+                object.__propertyCache__[propertyIndex] = value;
+                webChannel.exec({
+                    "type": QWebChannelMessageTypes.setProperty,
+                    "object": object.__id__,
+                    "property": propertyIndex,
+                    "value": value
+                });
+            }
+        });
+
+    }
+
+    // ----------------------------------------------------------------------
+
+    data.methods.forEach(addMethod);
+
+    data.properties.forEach(bindGetterSetter);
+
+    data.signals.forEach(function(signal) { addSignal(signal, false); });
+
+    for (var name in data.enums) {
+        object[name] = data.enums[name];
+    }
+}
+
+//required for use with nodejs
+if (typeof module === 'object') {
+    module.exports = {
+        QWebChannel: QWebChannel
+    };
+}

+ 32 - 0
resources/template.html

@@ -0,0 +1,32 @@
+<!doctype html>
+<html lang="en">
+<meta charset="utf-8">
+<head>
+  <link rel="stylesheet" type="text/css" href="markdown.css">
+  <script src="../utils/marked/marked.min.js"></script>
+  <script src="qwebchannel.js"></script>
+</head>
+<body>
+  <div id="placeholder"></div>
+  <script>
+  'use strict';
+
+  var placeholder = document.getElementById('placeholder');
+
+  var updateText = function(text) {
+      placeholder.innerHTML = marked(text);
+  }
+
+  new QWebChannel(qt.webChannelTransport,
+    function(channel) {
+      var content = channel.objects.content;
+      updateText(content.text);
+      content.textChanged.connect(updateText);
+    }
+  );
+  </script>
+</body>
+</html>
+
+
+

+ 35 - 0
utils/vutils.cpp

@@ -0,0 +1,35 @@
+#include "vutils.h"
+#include <QFile>
+#include <QDebug>
+
+VUtils::VUtils()
+{
+
+}
+
+QString VUtils::readFileFromDisk(const QString &filePath)
+{
+    QFile file(filePath);
+    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+        qWarning() << "error: fail to read file" << filePath;
+        return QString();
+    }
+    QString fileText(file.readAll());
+    file.close();
+    qDebug() << "read file content:" << filePath;
+    return fileText;
+}
+
+bool VUtils::writeFileToDisk(const QString &filePath, const QString &text)
+{
+    QFile file(filePath);
+    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
+        qWarning() << "error: fail to open file" << filePath << "to write to";
+        return false;
+    }
+    QTextStream stream(&file);
+    stream << text;
+    file.close();
+    qDebug() << "write file content:" << filePath;
+    return true;
+}

+ 15 - 0
utils/vutils.h

@@ -0,0 +1,15 @@
+#ifndef VUTILS_H
+#define VUTILS_H
+
+#include <QString>
+
+class VUtils
+{
+public:
+    VUtils();
+
+    static QString readFileFromDisk(const QString &filePath);
+    static bool writeFileToDisk(const QString &filePath, const QString &text);
+};
+
+#endif // VUTILS_H

+ 27 - 0
vdocument.cpp

@@ -0,0 +1,27 @@
+#include "vdocument.h"
+
+#include <QtDebug>
+
+VDocument::VDocument(QObject *parent) : QObject(parent)
+{
+
+}
+
+VDocument::VDocument(const QString &text, QObject *parent)
+    : QObject(parent)
+{
+    m_text = text;
+}
+
+void VDocument::setText(const QString &text)
+{
+    if (text == m_text)
+        return;
+    m_text = text;
+    emit textChanged(m_text);
+}
+
+QString VDocument::getText()
+{
+    return m_text;
+}

+ 24 - 0
vdocument.h

@@ -0,0 +1,24 @@
+#ifndef VDOCUMENT_H
+#define VDOCUMENT_H
+
+#include <QObject>
+#include <QString>
+
+class VDocument : public QObject
+{
+    Q_OBJECT
+    Q_PROPERTY(QString text MEMBER m_text NOTIFY textChanged)
+public:
+    explicit VDocument(QObject *parent = 0);
+    VDocument(const QString &text, QObject *parent = 0);
+    void setText(const QString &text);
+    QString getText();
+
+signals:
+    void textChanged(const QString &text);
+
+private:
+    QString m_text;
+};
+
+#endif // VDOCUMENT_H

+ 1 - 1
vedit.cpp

@@ -37,7 +37,7 @@ void VEdit::beginSave()
         noteFile->content = toHtml();
         break;
     case DocType::Markdown:
-
+        noteFile->content = toPlainText();
         break;
     default:
         qWarning() << "error: unknown doc type" << int(noteFile->docType);

+ 41 - 33
veditor.cpp

@@ -1,14 +1,20 @@
 #include <QtWidgets>
 #include <QTextBrowser>
+#include <QWebChannel>
+#include <QWebEngineView>
 #include "veditor.h"
 #include "vedit.h"
+#include "vdocument.h"
+#include "vnote.h"
+#include "utils/vutils.h"
+#include "vpreviewpage.h"
 
 VEditor::VEditor(const QString &path, const QString &name, bool modifiable,
                  QWidget *parent)
     : QStackedWidget(parent)
 {
     DocType docType = isMarkdown(name) ? DocType::Markdown : DocType::Html;
-    QString fileText = readFileFromDisk(QDir(path).filePath(name));
+    QString fileText = VUtils::readFileFromDisk(QDir(path).filePath(name));
     noteFile = new VNoteFile(path, name, fileText, docType, modifiable);
 
     isEditMode = false;
@@ -26,9 +32,22 @@ VEditor::~VEditor()
 void VEditor::setupUI()
 {
     textEditor = new VEdit(noteFile);
-    textBrowser = new QTextBrowser();
-    addWidget(textBrowser);
     addWidget(textEditor);
+
+    switch (noteFile->docType) {
+    case DocType::Markdown:
+        setupMarkdownPreview();
+        textBrowser = NULL;
+        break;
+
+    case DocType::Html:
+        textBrowser = new QTextBrowser();
+        addWidget(textBrowser);
+        webPreviewer = NULL;
+        break;
+    default:
+        qWarning() << "error: unknown doc type" << int(noteFile->docType);
+    }
 }
 
 bool VEditor::isMarkdown(const QString &name)
@@ -48,47 +67,21 @@ bool VEditor::isMarkdown(const QString &name)
     return false;
 }
 
-QString VEditor::readFileFromDisk(const QString &filePath)
-{
-    QFile file(filePath);
-    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
-        qWarning() << "error: fail to read file" << filePath;
-        return QString();
-    }
-    QString fileText(file.readAll());
-    file.close();
-    qDebug() << "read file content:" << filePath;
-    return fileText;
-}
-
-bool VEditor::writeFileToDisk(const QString &filePath, const QString &text)
-{
-    QFile file(filePath);
-    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
-        qWarning() << "error: fail to open file" << filePath << "to write to";
-        return false;
-    }
-    QTextStream stream(&file);
-    stream << text;
-    file.close();
-    qDebug() << "write file content:" << filePath;
-    return true;
-}
-
 void VEditor::showFileReadMode()
 {
     isEditMode = false;
     switch (noteFile->docType) {
     case DocType::Html:
         textBrowser->setHtml(noteFile->content);
+        setCurrentWidget(textBrowser);
         break;
     case DocType::Markdown:
-
+        document.setText(noteFile->content);
+        setCurrentWidget(webPreviewer);
         break;
     default:
         qWarning() << "error: unknown doc type" << int(noteFile->docType);
     }
-    setCurrentWidget(textBrowser);
 }
 
 void VEditor::showFileEditMode()
@@ -109,6 +102,7 @@ void VEditor::editFile()
     if (isEditMode || !noteFile->modifiable) {
         return;
     }
+
     showFileEditMode();
 }
 
@@ -152,7 +146,7 @@ bool VEditor::saveFile()
         return true;
     }
     textEditor->beginSave();
-    bool ret = writeFileToDisk(QDir(noteFile->path).filePath(noteFile->name),
+    bool ret = VUtils::writeFileToDisk(QDir(noteFile->path).filePath(noteFile->name),
                                noteFile->content);
     if (!ret) {
         QMessageBox msgBox(QMessageBox::Warning, tr("Fail to save to file"),
@@ -165,3 +159,17 @@ bool VEditor::saveFile()
     textEditor->endSave();
     return true;
 }
+
+void VEditor::setupMarkdownPreview()
+{
+    webPreviewer = new QWebEngineView(this);
+    VPreviewPage *page = new VPreviewPage(this);
+    webPreviewer->setPage(page);
+
+    QWebChannel *channel = new QWebChannel(this);
+    channel->registerObject(QStringLiteral("content"), &document);
+    page->setWebChannel(channel);
+    webPreviewer->setUrl(QUrl(VNote::templateUrl));
+
+    addWidget(webPreviewer);
+}

+ 5 - 2
veditor.h

@@ -5,9 +5,11 @@
 #include <QString>
 #include "vconstants.h"
 #include "vnotefile.h"
+#include "vdocument.h"
 
 class QTextBrowser;
 class VEdit;
+class QWebEngineView;
 
 class VEditor : public QStackedWidget
 {
@@ -25,16 +27,17 @@ public:
 
 private:
     bool isMarkdown(const QString &name);
-    QString readFileFromDisk(const QString &filePath);
-    bool writeFileToDisk(const QString &filePath, const QString &text);
     void setupUI();
     void showFileReadMode();
     void showFileEditMode();
+    void setupMarkdownPreview();
 
     VNoteFile *noteFile;
     bool isEditMode;
     QTextBrowser *textBrowser;
     VEdit *textEditor;
+    QWebEngineView *webPreviewer;
+    VDocument document;
 };
 
 #endif // VEDITOR_H

+ 1 - 1
vmainwindow.cpp

@@ -47,7 +47,7 @@ void VMainWindow::setupUI()
     fileList->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding);
 
     // Editor tab widget
-    tabs = new VTabWidget(VNote::welcomePageUrl);
+    tabs = new VTabWidget();
     tabs->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
     tabs->setTabBarAutoHide(true);
 

+ 5 - 1
vnote.cpp

@@ -1,10 +1,14 @@
 #include <QSettings>
 #include <QDebug>
 #include "vnote.h"
+#include "utils/vutils.h"
 
 const QString VNote::orgName = QString("tamlok");
 const QString VNote::appName = QString("VNote");
-const QString VNote::welcomePageUrl = QString(":/resources/welcome.html");
+const QString VNote::welcomePagePath = QString(":/resources/welcome.html");
+const QString VNote::preTemplatePath = QString(":/resources/pre_template.html");
+const QString VNote::postTemplatePath = QString(":/resources/post_template.html");
+const QString VNote::templateUrl = QString("qrc:/resources/template.html");
 
 VNote::VNote()
     : curNotebookIndex(0)

+ 4 - 1
vnote.h

@@ -19,7 +19,8 @@ public:
 
     static const QString orgName;
     static const QString appName;
-    static const QString welcomePageUrl;
+    static const QString welcomePagePath;
+    static const QString templateUrl;
 
 private:
     // Write notebooks section of global config
@@ -29,6 +30,8 @@ private:
 
     QVector<VNotebook> notebooks;
     int curNotebookIndex;
+    static const QString preTemplatePath;
+    static const QString postTemplatePath;
 };
 
 #endif // VNOTE_H

+ 10 - 4
vnote.qrc

@@ -1,5 +1,11 @@
-<!DOCTYPE RCC><RCC version="1.0">
-<qresource>
-    <file>resources/welcome.html</file>
-</qresource>
+<RCC>
+    <qresource prefix="/">
+        <file>resources/welcome.html</file>
+        <file>resources/qwebchannel.js</file>
+        <file>resources/template.html</file>
+        <file>resources/markdown.css</file>
+        <file>utils/marked/marked.min.js</file>
+        <file>resources/post_template.html</file>
+        <file>resources/pre_template.html</file>
+    </qresource>
 </RCC>

+ 19 - 0
vpreviewpage.cpp

@@ -0,0 +1,19 @@
+#include "vpreviewpage.h"
+
+#include <QDesktopServices>
+
+VPreviewPage::VPreviewPage(QWidget *parent) : QWebEnginePage(parent)
+{
+
+}
+
+bool VPreviewPage::acceptNavigationRequest(const QUrl &url,
+                                          QWebEnginePage::NavigationType /*type*/,
+                                          bool /*isMainFrame*/)
+{
+    // Only allow qrc:/index.html.
+    if (url.scheme() == QString("qrc"))
+        return true;
+    QDesktopServices::openUrl(url);
+    return false;
+}

+ 16 - 0
vpreviewpage.h

@@ -0,0 +1,16 @@
+#ifndef VPREVIEWPAGE_H
+#define VPREVIEWPAGE_H
+
+#include <QWebEnginePage>
+
+class VPreviewPage : public QWebEnginePage
+{
+    Q_OBJECT
+public:
+    explicit VPreviewPage(QWidget *parent = 0);
+
+protected:
+    bool acceptNavigationRequest(const QUrl &url, NavigationType type, bool isMainFrame);
+};
+
+#endif // VPREVIEWPAGE_H

+ 4 - 3
vtabwidget.cpp

@@ -2,9 +2,10 @@
 #include <QtDebug>
 #include "vtabwidget.h"
 #include "veditor.h"
+#include "vnote.h"
 
-VTabWidget::VTabWidget(const QString &welcomePageUrl, QWidget *parent)
-    : QTabWidget(parent), welcomePageUrl(welcomePageUrl)
+VTabWidget::VTabWidget(QWidget *parent)
+    : QTabWidget(parent)
 {
     setTabsClosable(true);
     connect(tabBar(), &QTabBar::tabCloseRequested,
@@ -15,7 +16,7 @@ VTabWidget::VTabWidget(const QString &welcomePageUrl, QWidget *parent)
 
 void VTabWidget::openWelcomePage()
 {
-    int idx = openFileInTab(welcomePageUrl, "", false);
+    int idx = openFileInTab(VNote::welcomePagePath, "", false);
     setTabText(idx, "Welcome to VNote");
     setTabToolTip(idx, "VNote");
 }

+ 1 - 2
vtabwidget.h

@@ -9,7 +9,7 @@ class VTabWidget : public QTabWidget
 {
     Q_OBJECT
 public:
-    explicit VTabWidget(const QString &welcomePageUrl, QWidget *parent = 0);
+    explicit VTabWidget(QWidget *parent = 0);
 
 signals:
 
@@ -28,7 +28,6 @@ private:
     int appendTabWithData(QWidget *page, const QString &label, const QJsonObject &tabData);
     int findTabByFile(const QString &path, const QString &name);
     int openFileInTab(const QString &path, const QString &name, bool modifiable);
-    QString welcomePageUrl;
 };
 
 #endif // VTABWIDGET_H