refactor VEditTab

1. Make VEditTab an abstract class.
2. Use VMdTab inheriting from VEditTab for Markdown file.
3. Use VHtmlTab inheriting from VEditTab for Html file.
This commit is contained in:
Le Tan 2017-05-30 20:05:23 +08:00
parent 6a10c7ab3c
commit 306b3cca92
11 changed files with 1284 additions and 796 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

246
src/vhtmltab.cpp Normal file
View File

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

81
src/vhtmltab.h Normal file
View File

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

View File

@ -19,6 +19,7 @@
#include "vedittab.h" #include "vedittab.h"
#include "vwebview.h" #include "vwebview.h"
#include "vexporter.h" #include "vexporter.h"
#include "vmdtab.h"
extern VConfigManager vconfig; extern VConfigManager vconfig;
@ -1408,7 +1409,9 @@ void VMainWindow::printNote()
V_ASSERT(m_curTab); V_ASSERT(m_curTab);
VWebView *webView = m_curTab->getWebViewer(); if (m_curFile->getDocType() == DocType::Markdown) {
VMdTab *mdTab = dynamic_cast<VMdTab *>((VEditTab *)m_curTab);
VWebView *webView = mdTab->getWebViewer();
V_ASSERT(webView); V_ASSERT(webView);
@ -1420,14 +1423,18 @@ void VMainWindow::printNote()
return; return;
} }
} }
}
void VMainWindow::exportAsPDF() void VMainWindow::exportAsPDF()
{ {
V_ASSERT(m_curTab); V_ASSERT(m_curTab);
V_ASSERT(m_curFile); V_ASSERT(m_curFile);
VExporter exporter(m_curTab->getMarkdownConverterType(), this); if (m_curFile->getDocType() == DocType::Markdown) {
VMdTab *mdTab = dynamic_cast<VMdTab *>((VEditTab *)m_curTab);
VExporter exporter(mdTab->getMarkdownConverterType(), this);
exporter.exportNote(m_curFile, ExportType::PDF); exporter.exportNote(m_curFile, ExportType::PDF);
exporter.exec(); exporter.exec();
} }
}

673
src/vmdtab.cpp Normal file
View File

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

130
src/vmdtab.h Normal file
View File

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