diff --git a/VNote.pro b/VNote.pro index 85f1ef75..00cce5f1 100644 --- a/VNote.pro +++ b/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 diff --git a/resources/markdown.css b/resources/markdown.css new file mode 100644 index 00000000..149094a2 --- /dev/null +++ b/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; +} diff --git a/resources/post_template.html b/resources/post_template.html new file mode 100644 index 00000000..9943ff0f --- /dev/null +++ b/resources/post_template.html @@ -0,0 +1,3 @@ + + + diff --git a/resources/pre_template.html b/resources/pre_template.html new file mode 100644 index 00000000..aa0205b3 --- /dev/null +++ b/resources/pre_template.html @@ -0,0 +1,8 @@ + + + + + + + +
diff --git a/resources/qwebchannel.js b/resources/qwebchannel.js new file mode 100644 index 00000000..1d84b8e7 --- /dev/null +++ b/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, info@kdab.com, author Milian Wolff +** 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 + }; +} diff --git a/resources/template.html b/resources/template.html new file mode 100644 index 00000000..14a3af1e --- /dev/null +++ b/resources/template.html @@ -0,0 +1,32 @@ + + + + + + + + + +
+ + + + + + diff --git a/utils/vutils.cpp b/utils/vutils.cpp new file mode 100644 index 00000000..d56185aa --- /dev/null +++ b/utils/vutils.cpp @@ -0,0 +1,35 @@ +#include "vutils.h" +#include +#include + +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; +} diff --git a/utils/vutils.h b/utils/vutils.h new file mode 100644 index 00000000..307019fd --- /dev/null +++ b/utils/vutils.h @@ -0,0 +1,15 @@ +#ifndef VUTILS_H +#define VUTILS_H + +#include + +class VUtils +{ +public: + VUtils(); + + static QString readFileFromDisk(const QString &filePath); + static bool writeFileToDisk(const QString &filePath, const QString &text); +}; + +#endif // VUTILS_H diff --git a/vdocument.cpp b/vdocument.cpp new file mode 100644 index 00000000..632284fb --- /dev/null +++ b/vdocument.cpp @@ -0,0 +1,27 @@ +#include "vdocument.h" + +#include + +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; +} diff --git a/vdocument.h b/vdocument.h new file mode 100644 index 00000000..05663e99 --- /dev/null +++ b/vdocument.h @@ -0,0 +1,24 @@ +#ifndef VDOCUMENT_H +#define VDOCUMENT_H + +#include +#include + +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 diff --git a/vedit.cpp b/vedit.cpp index 63dd8112..d72d5175 100644 --- a/vedit.cpp +++ b/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); diff --git a/veditor.cpp b/veditor.cpp index f492faee..dc2de4c7 100644 --- a/veditor.cpp +++ b/veditor.cpp @@ -1,14 +1,20 @@ #include #include +#include +#include #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); +} diff --git a/veditor.h b/veditor.h index 192b2cde..4f5b3230 100644 --- a/veditor.h +++ b/veditor.h @@ -5,9 +5,11 @@ #include #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 diff --git a/vmainwindow.cpp b/vmainwindow.cpp index 751f4f99..d91d3ac0 100644 --- a/vmainwindow.cpp +++ b/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); diff --git a/vnote.cpp b/vnote.cpp index f66f85c8..8d5ead69 100644 --- a/vnote.cpp +++ b/vnote.cpp @@ -1,10 +1,14 @@ #include #include #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) diff --git a/vnote.h b/vnote.h index b06a023c..0b464aba 100644 --- a/vnote.h +++ b/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 notebooks; int curNotebookIndex; + static const QString preTemplatePath; + static const QString postTemplatePath; }; #endif // VNOTE_H diff --git a/vnote.qrc b/vnote.qrc index 37ea16b5..dec14a88 100644 --- a/vnote.qrc +++ b/vnote.qrc @@ -1,5 +1,11 @@ - - - resources/welcome.html - + + + resources/welcome.html + resources/qwebchannel.js + resources/template.html + resources/markdown.css + utils/marked/marked.min.js + resources/post_template.html + resources/pre_template.html + diff --git a/vpreviewpage.cpp b/vpreviewpage.cpp new file mode 100644 index 00000000..69709f7b --- /dev/null +++ b/vpreviewpage.cpp @@ -0,0 +1,19 @@ +#include "vpreviewpage.h" + +#include + +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; +} diff --git a/vpreviewpage.h b/vpreviewpage.h new file mode 100644 index 00000000..38d352f5 --- /dev/null +++ b/vpreviewpage.h @@ -0,0 +1,16 @@ +#ifndef VPREVIEWPAGE_H +#define VPREVIEWPAGE_H + +#include + +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 diff --git a/vtabwidget.cpp b/vtabwidget.cpp index 91d4c1b1..5180ecb3 100644 --- a/vtabwidget.cpp +++ b/vtabwidget.cpp @@ -2,9 +2,10 @@ #include #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"); } diff --git a/vtabwidget.h b/vtabwidget.h index b9aea16f..3c5a05a6 100644 --- a/vtabwidget.h +++ b/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