From 2f1971476d666a8c34bca1cdf1a7c0268a6e0c54 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Wed, 3 Jan 2018 19:49:41 +0800 Subject: [PATCH] VMdEditor: support copy selected text as HTML --- src/dialog/vcopytextashtmldialog.cpp | 81 +++++++++++++++++++++ src/dialog/vcopytextashtmldialog.h | 46 ++++++++++++ src/resources/hoedown.js | 12 ++++ src/resources/markdown-it.js | 11 +++ src/resources/markdown_template.html | 2 + src/resources/markdown_template.js | 75 +++++++++++++++++++- src/resources/marked.js | 11 +++ src/resources/showdown.js | 20 ++++++ src/resources/vnote.ini | 9 +++ src/src.pro | 10 ++- src/utils/vutils.cpp | 2 + src/utils/vvim.cpp | 4 +- src/utils/vwebutils.cpp | 58 +++++++++++++++ src/utils/vwebutils.h | 18 +++++ src/vconfigmanager.cpp | 6 ++ src/vconfigmanager.h | 19 +++++ src/vdocument.cpp | 15 ++++ src/vdocument.h | 30 ++++++++ src/vmdeditor.cpp | 57 ++++++++++++--- src/vmdeditor.h | 12 ++++ src/vmdtab.cpp | 24 +++++++ src/vmdtab.h | 2 + src/vwaitingwidget.cpp | 33 +++++++++ src/vwaitingwidget.h | 19 +++++ src/vwebview.cpp | 101 +++++++++++++-------------- src/vwebview.h | 5 +- 26 files changed, 611 insertions(+), 71 deletions(-) create mode 100644 src/dialog/vcopytextashtmldialog.cpp create mode 100644 src/dialog/vcopytextashtmldialog.h create mode 100644 src/utils/vwebutils.cpp create mode 100644 src/utils/vwebutils.h create mode 100644 src/vwaitingwidget.cpp create mode 100644 src/vwaitingwidget.h diff --git a/src/dialog/vcopytextashtmldialog.cpp b/src/dialog/vcopytextashtmldialog.cpp new file mode 100644 index 00000000..d7a7287b --- /dev/null +++ b/src/dialog/vcopytextashtmldialog.cpp @@ -0,0 +1,81 @@ +#include "vcopytextashtmldialog.h" + +#include +#include +#include +#include +#include + +#include "utils/vutils.h" +#include "utils/vclipboardutils.h" +#include "utils/vwebutils.h" +#include "vconfigmanager.h" + +extern VConfigManager *g_config; + +VCopyTextAsHtmlDialog::VCopyTextAsHtmlDialog(const QString &p_text, QWidget *p_parent) + : QDialog(p_parent), m_text(p_text) +{ + setupUI(); +} + +void VCopyTextAsHtmlDialog::setupUI() +{ + QLabel *textLabel = new QLabel(tr("Text:")); + m_textEdit = new QPlainTextEdit(m_text); + m_textEdit->setReadOnly(true); + m_textEdit->setProperty("LineEdit", true); + + m_htmlLabel = new QLabel(tr("HTML:")); + m_htmlViewer = VUtils::getWebEngineView(); + m_htmlViewer->setContextMenuPolicy(Qt::NoContextMenu); + m_htmlViewer->setMinimumSize(600, 400); + + m_infoLabel = new QLabel(tr("Converting text to HTML ...")); + + m_btnBox = new QDialogButtonBox(QDialogButtonBox::Ok); + connect(m_btnBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + m_btnBox->button(QDialogButtonBox::Ok)->setProperty("SpecialBtn", true); + + QVBoxLayout *mainLayout = new QVBoxLayout(); + mainLayout->addWidget(textLabel); + mainLayout->addWidget(m_textEdit); + mainLayout->addWidget(m_htmlLabel); + mainLayout->addWidget(m_htmlViewer); + mainLayout->addWidget(m_infoLabel); + mainLayout->addStretch(); + mainLayout->addWidget(m_btnBox); + + setLayout(mainLayout); + setWindowTitle(tr("Copy Text As HTML")); + + setHtmlVisible(false); +} + +void VCopyTextAsHtmlDialog::setHtmlVisible(bool p_visible) +{ + m_htmlLabel->setVisible(p_visible); + m_htmlViewer->setVisible(p_visible); +} + +void VCopyTextAsHtmlDialog::setConvertedHtml(const QUrl &p_baseUrl, + const QString &p_html) +{ + QString html = QString("%1").arg(p_html); + m_htmlViewer->setHtml(html, p_baseUrl); + setHtmlVisible(true); + + // Fix image source. + if (g_config->getFixImageSrcInWebWhenCopied()) { + VWebUtils::fixImageSrcInHtml(p_baseUrl, html); + } + + QClipboard *clipboard = QApplication::clipboard(); + QMimeData *data = new QMimeData(); + data->setText(m_text); + data->setHtml(html); + VClipboardUtils::setMimeDataToClipboard(clipboard, data, QClipboard::Clipboard); + + QTimer::singleShot(3000, this, &VCopyTextAsHtmlDialog::accept); + m_infoLabel->setText(tr("HTML has been copied. Will be closed in 3 seconds.")); +} diff --git a/src/dialog/vcopytextashtmldialog.h b/src/dialog/vcopytextashtmldialog.h new file mode 100644 index 00000000..93d935b1 --- /dev/null +++ b/src/dialog/vcopytextashtmldialog.h @@ -0,0 +1,46 @@ +#ifndef VCOPYTEXTASHTMLDIALOG_H +#define VCOPYTEXTASHTMLDIALOG_H + +#include +#include + + +class QPlainTextEdit; +class QWebEngineView; +class QDialogButtonBox; +class VWaitingWidget; +class QLabel; + +class VCopyTextAsHtmlDialog : public QDialog +{ + Q_OBJECT +public: + VCopyTextAsHtmlDialog(const QString &p_text, QWidget *p_parent = nullptr); + + void setConvertedHtml(const QUrl &p_baseUrl, const QString &p_html); + + const QString &getText() const; + +private: + void setupUI(); + + void setHtmlVisible(bool p_visible); + + QPlainTextEdit *m_textEdit; + + QLabel *m_htmlLabel; + + QWebEngineView *m_htmlViewer; + + QLabel *m_infoLabel; + + QDialogButtonBox *m_btnBox; + + QString m_text; +}; + +inline const QString &VCopyTextAsHtmlDialog::getText() const +{ + return m_text; +} +#endif // VCOPYTEXTASHTMLDIALOG_H diff --git a/src/resources/hoedown.js b/src/resources/hoedown.js index 80e253c0..67934f1b 100644 --- a/src/resources/hoedown.js +++ b/src/resources/hoedown.js @@ -69,3 +69,15 @@ var highlightText = function(text, id, timeStamp) { var html = marked(text); content.highlightTextCB(html, id, timeStamp); } + +var textToHtml = function(text) { + var html = marked(text); + var container = document.getElementById('text-to-html-div'); + container.innerHTML = html; + + html = getHtmlWithInlineStyles(container); + + container.innerHTML = ""; + + content.textToHtmlCB(text, html); +} diff --git a/src/resources/markdown-it.js b/src/resources/markdown-it.js index b3055cd2..1de7c0cf 100644 --- a/src/resources/markdown-it.js +++ b/src/resources/markdown-it.js @@ -124,3 +124,14 @@ var highlightText = function(text, id, timeStamp) { content.highlightTextCB(html, id, timeStamp); } +var textToHtml = function(text) { + var html = mdit.render(text); + var container = document.getElementById('text-to-html-div'); + container.innerHTML = html; + + html = getHtmlWithInlineStyles(container); + + container.innerHTML = ""; + + content.textToHtmlCB(text, html); +} diff --git a/src/resources/markdown_template.html b/src/resources/markdown_template.html index 4dd4a332..be804b3c 100644 --- a/src/resources/markdown_template.html +++ b/src/resources/markdown_template.html @@ -31,5 +31,7 @@
+ + diff --git a/src/resources/markdown_template.js b/src/resources/markdown_template.js index 4360fd91..05b271ed 100644 --- a/src/resources/markdown_template.js +++ b/src/resources/markdown_template.js @@ -28,6 +28,10 @@ if (typeof VEnableHighlightLineNumber == 'undefined') { VEnableHighlightLineNumber = false; } +if (typeof VStylesToInline == 'undefined') { + VStylesToInline = ''; +} + // Add a caption (using alt text) under the image. var VImageCenterClass = 'img-center'; var VImageCaptionClass = 'img-caption'; @@ -53,6 +57,11 @@ new QWebChannel(qt.webChannelTransport, content.requestHighlightText.connect(highlightText); content.noticeReadyToHighlightText(); } + + if (typeof textToHtml == "function") { + content.requestTextToHtml.connect(textToHtml); + content.noticeReadyToTextToHtml(); + } }); var VHighlightedAnchorClass = 'highlighted-anchor'; @@ -853,4 +862,68 @@ var listContainsRegex = function(strs, exp) { } return false; -} +}; + +var StylesToInline = null; + +var initStylesToInline = function() { + console.log('initStylesToInline'); + StylesToInline = new Map(); + + if (VStylesToInline.length == 0) { + return; + } + + var rules = VStylesToInline.split(','); + for (var i = 0; i < rules.length; ++i) { + var vals = rules[i].split('$'); + if (vals.length != 2) { + continue; + } + + var tags = vals[0].split(':'); + var pros = vals[1].split(':'); + for (var j = 0; j < tags.length; ++j) { + StylesToInline.set(tags[j].toLowerCase(), pros); + } + } +}; + +// Embed the CSS styles of @ele and all its children. +var embedInlineStyles = function(ele) { + var tagName = ele.tagName.toLowerCase(); + var props = StylesToInline.get(tagName); + if (!props) { + props = StylesToInline.get('all'); + + if (!props) { + return; + } + } + + // Embed itself. + var style = window.getComputedStyle(ele, null); + for (var i = 0; i < props.length; ++i) { + var pro = props[i]; + ele.style.setProperty(pro, style.getPropertyValue(pro)); + } + + // Embed children. + var children = ele.children; + for (var i = 0; i < children.length; ++i) { + embedInlineStyles(children[i]); + } +}; + +var getHtmlWithInlineStyles = function(container) { + if (!StylesToInline) { + initStylesToInline(); + } + + var children = container.children; + for (var i = 0; i < children.length; ++i) { + embedInlineStyles(children[i]); + } + + return container.innerHTML; +}; diff --git a/src/resources/marked.js b/src/resources/marked.js index 95e2251a..0eb28ee5 100644 --- a/src/resources/marked.js +++ b/src/resources/marked.js @@ -75,3 +75,14 @@ var highlightText = function(text, id, timeStamp) { content.highlightTextCB(html, id, timeStamp); } +var textToHtml = function(text) { + var html = marked(text); + var container = document.getElementById('text-to-html-div'); + container.innerHTML = html; + + html = getHtmlWithInlineStyles(container); + + container.innerHTML = ""; + + content.textToHtmlCB(text, html); +} diff --git a/src/resources/showdown.js b/src/resources/showdown.js index 8743b305..db29d9c2 100644 --- a/src/resources/showdown.js +++ b/src/resources/showdown.js @@ -109,3 +109,23 @@ var highlightText = function(text, id, timeStamp) { content.highlightTextCB(html, id, timeStamp); } +var textToHtml = function(text) { + var html = renderer.makeHtml(text); + + var parser = new DOMParser(); + var htmlDoc = parser.parseFromString("
" + html + "
", 'text/html'); + highlightCodeBlocks(htmlDoc, false, false); + + html = htmlDoc.getElementById('showdown-container').innerHTML; + + delete parser; + + var container = document.getElementById('text-to-html-div'); + container.innerHTML = html; + + html = getHtmlWithInlineStyles(container); + + container.innerHTML = ""; + + content.textToHtmlCB(text, html); +} diff --git a/src/resources/vnote.ini b/src/resources/vnote.ini index e040d91a..05f32c98 100644 --- a/src/resources/vnote.ini +++ b/src/resources/vnote.ini @@ -196,6 +196,15 @@ mathjax_javascript=https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax. ; Fix local relative image source when copied fix_img_src_when_copied=true +; Styles to be removed when copied in read mode +; style1,style2,style3 +styles_to_remove_when_copied=margin,margin-left,margin-right,padding,padding-left,padding-right + +; CSS properties to embed as inline styles when copied in edit mode +; tag1:tag2:tag3$property1:property2:property3,tag4:tag5$property2:property3 +; "all" for all tags not specified explicitly +styles_to_inline_when_copied=all$border:color:display:font-family:font-size:font-style:white-space:word-spacing:line-height:text-align:text-indent:padding-top:padding-bottom:margin-top:margin-bottom,code$font-family:font-size:line-height:color:display:overfow-x,li$line-height,a$color:vertical-align,pre$display:overflow-y:overflow-x:color:font-size:font-style:font-weight:letter-spacing:text-align:text-indent:word-spacing + [shortcuts] ; Define shortcuts here, with each item in the form "operation=keysequence". ; Leave keysequence empty to disable the shortcut of an operation. diff --git a/src/src.pro b/src/src.pro index 4dd7093f..085e7505 100644 --- a/src/src.pro +++ b/src/src.pro @@ -102,7 +102,10 @@ SOURCES += main.cpp\ vbuttonmenuitem.cpp \ utils/viconutils.cpp \ lineeditdelegate.cpp \ - dialog/vtipsdialog.cpp + dialog/vtipsdialog.cpp \ + dialog/vcopytextashtmldialog.cpp \ + vwaitingwidget.cpp \ + utils/vwebutils.cpp HEADERS += vmainwindow.h \ vdirectorytree.h \ @@ -191,7 +194,10 @@ HEADERS += vmainwindow.h \ vbuttonmenuitem.h \ utils/viconutils.h \ lineeditdelegate.h \ - dialog/vtipsdialog.h + dialog/vtipsdialog.h \ + dialog/vcopytextashtmldialog.h \ + vwaitingwidget.h \ + utils/vwebutils.h RESOURCES += \ vnote.qrc \ diff --git a/src/utils/vutils.cpp b/src/utils/vutils.cpp index 3017d55c..dccc9685 100644 --- a/src/utils/vutils.cpp +++ b/src/utils/vutils.cpp @@ -664,6 +664,8 @@ QString VUtils::generateHtmlTemplate(MarkdownConverterType p_conType, bool p_exp "\n"; } + extraFile += "\n"; + QString htmlTemplate; if (p_exportPdf) { htmlTemplate = VNote::s_markdownTemplatePDF; diff --git a/src/utils/vvim.cpp b/src/utils/vvim.cpp index 2eb4ed70..fb1bae51 100644 --- a/src/utils/vvim.cpp +++ b/src/utils/vvim.cpp @@ -6321,9 +6321,7 @@ void VVim::handleMouseMoved(QMouseEvent *p_event) void VVim::handleMouseReleased(QMouseEvent *p_event) { - Q_UNUSED(p_event); - - if (checkMode(VimMode::Normal)) { + if (checkMode(VimMode::Normal) && p_event->button() == Qt::LeftButton) { QTextCursor cursor = m_editor->textCursorW(); if (cursor.hasSelection()) { return; diff --git a/src/utils/vwebutils.cpp b/src/utils/vwebutils.cpp new file mode 100644 index 00000000..6e86cfc6 --- /dev/null +++ b/src/utils/vwebutils.cpp @@ -0,0 +1,58 @@ +#include "vwebutils.h" + +#include +#include +#include + +VWebUtils::VWebUtils() +{ +} + +bool VWebUtils::fixImageSrcInHtml(const QUrl &p_baseUrl, QString &p_html) +{ + bool changed = false; + +#if defined(Q_OS_WIN) + QUrl::ComponentFormattingOption strOpt = QUrl::EncodeSpaces; +#else + QUrl::ComponentFormattingOption strOpt = QUrl::FullyEncoded; +#endif + + QRegExp reg("( +#include + + +class VWebUtils +{ +public: + // Fix in @p_html. + static bool fixImageSrcInHtml(const QUrl &p_baseUrl, QString &p_html); + +private: + VWebUtils(); +}; + +#endif // VWEBUTILS_H diff --git a/src/vconfigmanager.cpp b/src/vconfigmanager.cpp index 56c07786..0e6956c9 100644 --- a/src/vconfigmanager.cpp +++ b/src/vconfigmanager.cpp @@ -287,6 +287,12 @@ void VConfigManager::initialize() m_fixImageSrcInWebWhenCopied = getConfigFromSettings("web", "fix_img_src_when_copied").toBool(); + + m_stylesToRemoveWhenCopied = getConfigFromSettings("web", + "styles_to_remove_when_copied").toStringList(); + + m_stylesToInlineWhenCopied = getConfigFromSettings("web", + "styles_to_inline_when_copied").toStringList().join(","); } void VConfigManager::initSettings() diff --git a/src/vconfigmanager.h b/src/vconfigmanager.h index 53355c34..cb1a7b80 100644 --- a/src/vconfigmanager.h +++ b/src/vconfigmanager.h @@ -428,6 +428,10 @@ public: bool getFixImageSrcInWebWhenCopied() const; + const QStringList &getStylesToRemoveWhenCopied() const; + + const QString &getStylesToInlineWhenCopied() const; + private: // Look up a config from user and default settings. QVariant getConfigFromSettings(const QString §ion, const QString &key) const; @@ -821,6 +825,12 @@ private: // Whether fix the local relative image src in read mode when copied. bool m_fixImageSrcInWebWhenCopied; + // Styles to be removed when copied in read mode. + QStringList m_stylesToRemoveWhenCopied; + + // The string containing styles to inline when copied in edit mode. + QString m_stylesToInlineWhenCopied; + // The name of the config file in each directory, obsolete. // Use c_dirConfigFile instead. static const QString c_obsoleteDirConfigFile; @@ -1993,4 +2003,13 @@ inline bool VConfigManager::getFixImageSrcInWebWhenCopied() const return m_fixImageSrcInWebWhenCopied; } +inline const QStringList &VConfigManager::getStylesToRemoveWhenCopied() const +{ + return m_stylesToRemoveWhenCopied; +} + +inline const QString &VConfigManager::getStylesToInlineWhenCopied() const +{ + return m_stylesToInlineWhenCopied; +} #endif // VCONFIGMANAGER_H diff --git a/src/vdocument.cpp b/src/vdocument.cpp index 66a9fe29..cee4abad 100644 --- a/src/vdocument.cpp +++ b/src/vdocument.cpp @@ -77,12 +77,27 @@ void VDocument::highlightTextCB(const QString &p_html, int p_id, int p_timeStamp emit textHighlighted(p_html, p_id, p_timeStamp); } +void VDocument::textToHtmlAsync(const QString &p_text) +{ + emit requestTextToHtml(p_text); +} + +void VDocument::textToHtmlCB(const QString &p_text, const QString &p_html) +{ + emit textToHtmlFinished(p_text, p_html); +} + void VDocument::noticeReadyToHighlightText() { m_readyToHighlight = true; emit readyToHighlightText(); } +void VDocument::noticeReadyToTextToHtml() +{ + m_readyToTextToHtml = true; +} + void VDocument::setFile(const VFile *p_file) { m_file = p_file; diff --git a/src/vdocument.h b/src/vdocument.h index 9833ae0b..d6880288 100644 --- a/src/vdocument.h +++ b/src/vdocument.h @@ -29,10 +29,15 @@ public: // Use p_id to identify the result. void highlightTextAsync(const QString &p_text, int p_id, int p_timeStamp); + // Request to convert @p_text to HTML. + void textToHtmlAsync(const QString &p_text); + void setFile(const VFile *p_file); bool isReadyToHighlight() const; + bool isReadyToTextToHtml() const; + public slots: // Will be called in the HTML side @@ -49,9 +54,15 @@ public slots: void setLog(const QString &p_log); void keyPressEvent(int p_key, bool p_ctrl, bool p_shift); void updateText(); + void highlightTextCB(const QString &p_html, int p_id, int p_timeStamp); + void noticeReadyToHighlightText(); + void textToHtmlCB(const QString &p_text, const QString &p_html); + + void noticeReadyToTextToHtml(); + // Web-side handle logics (MathJax etc.) is finished. // But the page may not finish loading, such as images. void finishLogics(); @@ -65,14 +76,25 @@ signals: // @anchor is the id of that anchor, without '#'. void headerChanged(const QString &anchor); + void htmlChanged(const QString &html); + void logChanged(const QString &p_log); + void keyPressed(int p_key, bool p_ctrl, bool p_shift); + void requestHighlightText(const QString &p_text, int p_id, int p_timeStamp); + void textHighlighted(const QString &p_html, int p_id, int p_timeStamp); + void readyToHighlightText(); + void logicsFinished(); + void requestTextToHtml(const QString &p_text); + + void textToHtmlFinished(const QString &p_text, const QString &p_html); + private: QString m_toc; QString m_header; @@ -87,10 +109,18 @@ private: // Whether the web side is ready to handle highlight text request. bool m_readyToHighlight; + + // Whether the web side is ready to convert text to html. + bool m_readyToTextToHtml; }; inline bool VDocument::isReadyToHighlight() const { return m_readyToHighlight; } + +inline bool VDocument::isReadyToTextToHtml() const +{ + return m_readyToTextToHtml; +} #endif // VDOCUMENT_H diff --git a/src/vmdeditor.cpp b/src/vmdeditor.cpp index 56964562..8973a3c5 100644 --- a/src/vmdeditor.cpp +++ b/src/vmdeditor.cpp @@ -19,6 +19,7 @@ #include "vnotefile.h" #include "vpreviewmanager.h" #include "utils/viconutils.h" +#include "dialog/vcopytextashtmldialog.h" extern VConfigManager *g_config; @@ -29,7 +30,8 @@ VMdEditor::VMdEditor(VFile *p_file, : VTextEdit(p_parent), VEditor(p_file, this), m_mdHighlighter(NULL), - m_freshEdit(true) + m_freshEdit(true), + m_textToHtmlDialog(NULL) { Q_ASSERT(p_file->getDocType() == DocType::Markdown); @@ -266,12 +268,19 @@ void VMdEditor::contextMenuEvent(QContextMenuEvent *p_event) QMenu *menu = createStandardContextMenu(); menu->setToolTipsVisible(true); - const QList actions = menu->actions(); + VEditTab *editTab = dynamic_cast(parent()); + Q_ASSERT(editTab); + if (editTab->isEditMode()) { + const QList actions = menu->actions(); - if (!textCursor().hasSelection()) { - VEditTab *editTab = dynamic_cast(parent()); - Q_ASSERT(editTab); - if (editTab->isEditMode()) { + if (textCursor().hasSelection()) { + QAction *copyAsHtmlAct = new QAction(tr("Copy As &HTML without Background"), menu); + copyAsHtmlAct->setToolTip(tr("Copy selected contents as HTML without background styles")); + connect(copyAsHtmlAct, &QAction::triggered, + this, &VMdEditor::handleCopyAsHtmlAction); + + menu->insertAction(actions.isEmpty() ? NULL : actions[0], copyAsHtmlAct); + } else { QAction *saveExitAct = new QAction(VIconUtils::menuIcon(":/resources/icons/save_exit.svg"), tr("&Save Changes And Read"), menu); @@ -292,9 +301,10 @@ void VMdEditor::contextMenuEvent(QContextMenuEvent *p_event) menu->insertAction(actions.isEmpty() ? NULL : actions[0], discardExitAct); menu->insertAction(discardExitAct, saveExitAct); - if (!actions.isEmpty()) { - menu->insertSeparator(actions[0]); - } + } + + if (!actions.isEmpty()) { + menu->insertSeparator(actions[0]); } } @@ -990,3 +1000,32 @@ void VMdEditor::updateInitAndInsertedImages(bool p_fileChanged, UpdateAction p_a } } } + +void VMdEditor::handleCopyAsHtmlAction() +{ + QTextCursor cursor = textCursor(); + Q_ASSERT(cursor.hasSelection()); + + QString text = VEditUtils::selectedText(cursor); + Q_ASSERT(!text.isEmpty()); + + Q_ASSERT(!m_textToHtmlDialog); + m_textToHtmlDialog = new VCopyTextAsHtmlDialog(text, this); + + // For Hoedown, we use marked.js to convert the text to have a general interface. + emit requestTextToHtml(text); + + m_textToHtmlDialog->exec(); + + delete m_textToHtmlDialog; + m_textToHtmlDialog = NULL; +} + +void VMdEditor::textToHtmlFinished(const QString &p_text, + const QUrl &p_baseUrl, + const QString &p_html) +{ + if (m_textToHtmlDialog && m_textToHtmlDialog->getText() == p_text) { + m_textToHtmlDialog->setConvertedHtml(p_baseUrl, p_html); + } +} diff --git a/src/vmdeditor.h b/src/vmdeditor.h index 35aebe57..0ec120e4 100644 --- a/src/vmdeditor.h +++ b/src/vmdeditor.h @@ -5,6 +5,7 @@ #include #include #include +#include #include "vtextedit.h" #include "veditor.h" @@ -17,6 +18,7 @@ class HGMarkdownHighlighter; class VCodeBlockHighlightHelper; class VDocument; class VPreviewManager; +class VCopyTextAsHtmlDialog; class VMdEditor : public VTextEdit, public VEditor { @@ -68,6 +70,8 @@ public: public slots: bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) Q_DECL_OVERRIDE; + void textToHtmlFinished(const QString &p_text, const QUrl &p_baseUrl, const QString &p_html); + // Wrapper functions for QPlainTextEdit/QTextEdit. public: void setExtraSelectionsW(const QList &p_selections) Q_DECL_OVERRIDE @@ -165,6 +169,9 @@ signals: // Will be emitted by VImagePreviewer for now. void statusChanged(); + // Request to convert @p_text to Html. + void requestTextToHtml(const QString &p_text); + protected: void updateFontAndPalette() Q_DECL_OVERRIDE; @@ -191,6 +198,9 @@ private slots: // When there is no header in current cursor, will signal an invalid header. void updateCurrentHeader(); + // Copy selected text as HTML. + void handleCopyAsHtmlAction(); + private: // Update the config of VTextEdit according to global configurations. void updateTextEditConfig(); @@ -222,5 +232,7 @@ private: QVector m_headers; bool m_freshEdit; + + VCopyTextAsHtmlDialog *m_textToHtmlDialog; }; #endif // VMDEDITOR_H diff --git a/src/vmdtab.cpp b/src/vmdtab.cpp index b082ea3e..63ba2199 100644 --- a/src/vmdtab.cpp +++ b/src/vmdtab.cpp @@ -413,6 +413,11 @@ void VMdTab::setupMarkdownViewer() tabIsReady(TabReady::ReadMode); }); + connect(m_document, &VDocument::textToHtmlFinished, + this, [this](const QString &p_text, const QString &p_html) { + Q_ASSERT(m_editor); + m_editor->textToHtmlFinished(p_text, m_webViewer->url(), p_html); + }); page->setWebChannel(channel); @@ -464,6 +469,8 @@ void VMdTab::setupMarkdownEditor() tabIsReady(TabReady::EditMode); }); + connect(m_editor, &VMdEditor::requestTextToHtml, + this, &VMdTab::textToHtmlViaWebView); enableHeadingSequence(m_enableHeadingSequence); m_editor->reloadFile(); @@ -1006,3 +1013,20 @@ void VMdTab::handleFileOrDirectoryChange(bool p_isFile, UpdateAction p_act) m_editor->refreshPreview(); } } + +void VMdTab::textToHtmlViaWebView(const QString &p_text) +{ + int maxRetry = 50; + while (!m_document->isReadyToTextToHtml() && maxRetry > 0) { + qDebug() << "wait for web side ready to convert text to HTML"; + VUtils::sleepWait(100); + --maxRetry; + } + + if (maxRetry == 0) { + qWarning() << "web side is not ready to convert text to HTML"; + return; + } + + m_document->textToHtmlAsync(p_text); +} diff --git a/src/vmdtab.h b/src/vmdtab.h index 755761ac..f2ea34c5 100644 --- a/src/vmdtab.h +++ b/src/vmdtab.h @@ -184,6 +184,8 @@ private: // updateStatus() with only cursor position information. void updateCursorStatus(); + void textToHtmlViaWebView(const QString &p_text); + VMdEditor *m_editor; VWebView *m_webViewer; VDocument *m_document; diff --git a/src/vwaitingwidget.cpp b/src/vwaitingwidget.cpp new file mode 100644 index 00000000..1efd4e64 --- /dev/null +++ b/src/vwaitingwidget.cpp @@ -0,0 +1,33 @@ +#include "vwaitingwidget.h" + +#include +#include +#include +#include + +VWaitingWidget::VWaitingWidget(QWidget *p_parent) + : QWidget(p_parent) +{ + setupUI(); +} + +void VWaitingWidget::setupUI() +{ + QSize imgSize(64, 64); + QLabel *logoLabel = new QLabel(); + logoLabel->setPixmap(QPixmap(":/resources/icons/vnote.svg").scaled(imgSize, Qt::KeepAspectRatio)); + + QHBoxLayout *layout = new QHBoxLayout(); + layout->addStretch(); + layout->addWidget(logoLabel); + layout->addStretch(); + + QVBoxLayout *mainLayout = new QVBoxLayout(); + mainLayout->addStretch(); + mainLayout->addLayout(layout); + mainLayout->addStretch(); + + mainLayout->setContentsMargins(0, 0, 0, 0); + + setLayout(mainLayout); +} diff --git a/src/vwaitingwidget.h b/src/vwaitingwidget.h new file mode 100644 index 00000000..b81fc433 --- /dev/null +++ b/src/vwaitingwidget.h @@ -0,0 +1,19 @@ +#ifndef VWAITINGWIDGET_H +#define VWAITINGWIDGET_H + +#include + +class QLabel; + + +class VWaitingWidget : public QWidget +{ + Q_OBJECT +public: + explicit VWaitingWidget(QWidget *p_parent = nullptr); + +private: + void setupUI(); +}; + +#endif // VWAITINGWIDGET_H diff --git a/src/vwebview.cpp b/src/vwebview.cpp index b6e797ae..eeadd98b 100644 --- a/src/vwebview.cpp +++ b/src/vwebview.cpp @@ -10,12 +10,12 @@ #include #include #include -#include #include #include "vfile.h" #include "utils/vclipboardutils.h" #include "utils/viconutils.h" #include "vconfigmanager.h" +#include "utils/vwebutils.h" extern VConfigManager *g_config; @@ -231,12 +231,50 @@ void VWebView::hideUnusedActions(QMenu *p_menu) static bool removeBackgroundColor(QString &p_html) { - QRegExp reg("(\\s|\")background(-color)?:[^;]+;"); + QRegExp reg("(<[^>]+\\sstyle=[^>]*(\\s|\"))background(-color)?:[^;]+;([^>]*>)"); int size = p_html.size(); - p_html.replace(reg, "\\1"); + p_html.replace(reg, "\\1\\4"); return p_html.size() != size; } +bool VWebView::removeStyles(QString &p_html) +{ + bool changed = false; + + const QStringList &styles = g_config->getStylesToRemoveWhenCopied(); + if (styles.isEmpty()) { + return changed; + } + + QRegExp tagReg("(<[^>]+\\sstyle=[^>]*>)"); + + int pos = 0; + while (pos < p_html.size()) { + int idx = p_html.indexOf(tagReg, pos); + if (idx == -1) { + break; + } + + QString styleStr = tagReg.cap(1); + QString alteredStyleStr = styleStr; + + QString regPatt("(\\s|\")%1:[^;]+;"); + for (auto const & sty : styles) { + QRegExp reg(regPatt.arg(sty)); + alteredStyleStr.replace(reg, "\\1"); + } + + pos = idx + tagReg.matchedLength(); + if (styleStr != alteredStyleStr) { + pos = pos + alteredStyleStr.size() - styleStr.size(); + p_html.replace(idx, tagReg.matchedLength(), alteredStyleStr); + changed = true; + } + } + + return changed; +} + void VWebView::handleCopyWithoutBackgroundAction() { m_needRemoveBackground = true; @@ -280,56 +318,6 @@ void VWebView::handleClipboardChanged(QClipboard::Mode p_mode) } } -bool VWebView::fixImgSrc(QString &p_html) -{ - bool changed = false; - -#if defined(Q_OS_WIN) - QUrl::ComponentFormattingOption strOpt = QUrl::EncodeSpaces; -#else - QUrl::ComponentFormattingOption strOpt = QUrl::FullyEncoded; -#endif - - QRegExp reg("( #include +#include class VFile; class QMenu; @@ -42,11 +43,11 @@ private: const QMimeData *p_mimeData, bool p_removeBackground); - bool fixImgSrc(QString &p_html); - void removeHtmlFromImageData(QClipboard *p_clipboard, const QMimeData *p_mimeData); + bool removeStyles(QString &p_html); + VFile *m_file; // Whether this view has hooked the Copy Image Url action.