diff --git a/libs/vtextedit b/libs/vtextedit index 064a4342..43ed9543 160000 --- a/libs/vtextedit +++ b/libs/vtextedit @@ -1 +1 @@ -Subproject commit 064a434202096f703cbed9162742bbc55911f974 +Subproject commit 43ed95437369d48ff0428661db9ebae711d725e9 diff --git a/src/core/configmgr.cpp b/src/core/configmgr.cpp index 10531a5f..021ee0d3 100644 --- a/src/core/configmgr.cpp +++ b/src/core/configmgr.cpp @@ -25,7 +25,7 @@ using namespace vnotex; #ifndef QT_NO_DEBUG - // #define VX_DEBUG_WEB + #define VX_DEBUG_WEB #endif const QString ConfigMgr::c_orgName = "VNote"; diff --git a/src/core/mainconfig.cpp b/src/core/mainconfig.cpp index bc13a770..e4055c48 100644 --- a/src/core/mainconfig.cpp +++ b/src/core/mainconfig.cpp @@ -7,6 +7,7 @@ #include "coreconfig.h" #include "editorconfig.h" #include "widgetconfig.h" +#include "texteditorconfig.h" #include "markdowneditorconfig.h" using namespace vnotex; @@ -118,4 +119,5 @@ QString MainConfig::getVersion(const QJsonObject &p_jobj) void MainConfig::doVersionSpecificOverride() { // In a new version, we may want to change one value by force. + m_editorConfig->getTextEditorConfig().m_highlightWhitespace = false; } diff --git a/src/core/markdowneditorconfig.h b/src/core/markdowneditorconfig.h index 2de1e3df..84ca9c7a 100644 --- a/src/core/markdowneditorconfig.h +++ b/src/core/markdowneditorconfig.h @@ -241,7 +241,7 @@ namespace vnotex InplacePreviewSources m_inplacePreviewSources = InplacePreviewSource::NoInplacePreview; // View mode in edit mode. - EditViewMode m_editViewMode = EditViewMode::EditPreview; + EditViewMode m_editViewMode = EditViewMode::EditOnly; }; } diff --git a/src/core/texteditorconfig.h b/src/core/texteditorconfig.h index 4e788b00..f070c62d 100644 --- a/src/core/texteditorconfig.h +++ b/src/core/texteditorconfig.h @@ -5,6 +5,8 @@ namespace vnotex { + class MainConfig; + class TextEditorConfig : public IConfig { public: @@ -73,6 +75,8 @@ namespace vnotex void setSpellCheckEnabled(bool p_enabled); private: + friend class MainConfig; + QString lineNumberTypeToString(LineNumberType p_type) const; LineNumberType stringToLineNumberType(const QString &p_str) const; @@ -99,7 +103,7 @@ namespace vnotex int m_tabStopWidth = 4; - bool m_highlightWhitespace = true; + bool m_highlightWhitespace = false; int m_zoomDelta = 0; diff --git a/src/core/theme.h b/src/core/theme.h index fb49aa0c..5fea8622 100644 --- a/src/core/theme.h +++ b/src/core/theme.h @@ -67,6 +67,7 @@ namespace vnotex // Use for MarkdownEditor code block highlight. // If not specified, will use m_editorHighlightTheme. + // Valid only when KSyntaxCodeBlockHighlighter is used. QString m_markdownEditorHighlightTheme; }; diff --git a/src/data/core/vnotex.json b/src/data/core/vnotex.json index 7982d2a4..0ad9f067 100644 --- a/src/data/core/vnotex.json +++ b/src/data/core/vnotex.json @@ -141,7 +141,7 @@ "wrap_mode": "word_anywhere", "expand_tab": true, "tab_stop_width": 4, - "highlight_whitespace": true, + "highlight_whitespace": false, "//comment" : "Positive to zoom in and negative to zoom out", "zoom_delta": 0, "spell_check": false @@ -360,7 +360,7 @@ "//comment" : "imagelink/codeblock/math", "inplace_preview_sources" : "imagelink;codeblock;math", "//comment" : "view mode of edit mode: editonly/editpreview", - "edit_view_mode" : "editpreview" + "edit_view_mode" : "editonly" }, "image_host" : { "hosts" : [ diff --git a/src/data/extra/web/js/markdownviewer.js b/src/data/extra/web/js/markdownviewer.js index c08ef5b5..f9462ad0 100644 --- a/src/data/extra/web/js/markdownviewer.js +++ b/src/data/extra/web/js/markdownviewer.js @@ -35,6 +35,14 @@ new QWebChannel(qt.webChannelTransport, window.vnotex.htmlToMarkdown(p_id, p_timeStamp, p_html); }); + adapter.highlightCodeBlockRequested.connect(function(p_idx, p_timeStamp, p_text) { + window.vnotex.highlightCodeBlock(p_idx, p_timeStamp, p_text); + }); + + adapter.parseStyleSheetRequested.connect(function(p_id, p_styleSheet) { + window.vnotex.parseStyleSheet(p_id, p_styleSheet); + }); + adapter.crossCopyRequested.connect(function(p_id, p_timeStamp, p_target, p_baseUrl, p_html) { window.vnotex.crossCopy(p_id, p_timeStamp, p_target, p_baseUrl, p_html); }); diff --git a/src/data/extra/web/js/vnotex.js b/src/data/extra/web/js/vnotex.js index 87e77f38..82c6a37f 100644 --- a/src/data/extra/web/js/vnotex.js +++ b/src/data/extra/web/js/vnotex.js @@ -270,6 +270,49 @@ class VNoteX extends EventEmitter { window.vxMarkdownAdapter.setMarkdownFromHtml(p_id, p_timeStamp, markdown); } + highlightCodeBlock(p_idx, p_timeStamp, p_text) { + let match = /^```[^\S\n]*(\S+)?\s*\n([\s\S]+)\n```\s*$/.exec(p_text); + if (!match || !match[1] || !match[2]) { + window.vxMarkdownAdapter.setCodeBlockHighlightHtml(p_idx, p_timeStamp, ''); + return; + } + + let lang = match[1]; + let body = match[2]; + + if (Prism && Prism.languages[lang]) { + let html = Prism.highlight(body, Prism.languages[lang], lang); + window.vxMarkdownAdapter.setCodeBlockHighlightHtml(p_idx, p_timeStamp, html); + } else { + window.vxMarkdownAdapter.setCodeBlockHighlightHtml(p_idx, p_timeStamp, ''); + } + } + + parseStyleSheet(p_id, p_styleSheet) { + let doc = document.implementation.createHTMLDocument(''); + let styleEle = document.createElement('style'); + styleEle.textContent = p_styleSheet; + doc.body.appendChild(styleEle); + + let styles = []; + for (let i = 0; i < styleEle.sheet.cssRules.length; ++i) { + let rule = styleEle.sheet.cssRules[i]; + if (rule.type != CSSRule.STYLE_RULE) { + continue; + } + + styles.push({ + selector: rule.selectorText, + color: rule.style.color, + backgroundColor: rule.style.backgroundColor, + fontWeight: rule.style.fontWeight, + fontStyle: rule.style.fontStyle + }); + } + + window.vxMarkdownAdapter.setStyleSheetStyles(p_id, styles); + } + setCrossCopyTargets(p_targets) { window.vxMarkdownAdapter.setCrossCopyTargets(p_targets); } diff --git a/src/fakeaccessible.cpp b/src/fakeaccessible.cpp index 4c4f5376..9b119676 100644 --- a/src/fakeaccessible.cpp +++ b/src/fakeaccessible.cpp @@ -8,24 +8,8 @@ using namespace vnotex; QAccessibleInterface *FakeAccessible::accessibleFactory(const QString &p_className, QObject *p_obj) { // Try to fix non-responsible issue caused by Youdao Dict. - if (p_className == QLatin1String("vnotex::LineEdit") - || p_className == QLatin1String("vnotex::TitleBar") - || p_className == QLatin1String("vnotex::NotebookSelector") - || p_className == QLatin1String("vnotex::TagExplorer") - || p_className == QLatin1String("vnotex::SearchPanel") - || p_className == QLatin1String("vnotex::SnippetPanel") - || p_className == QLatin1String("vnotex::OutlineViewer") - || p_className == QLatin1String("vnotex::TitleToolBar") - || p_className == QLatin1String("vnotex::MainWindow") - || p_className == QLatin1String("vnotex::ViewArea") - || p_className == QLatin1String("vte::VTextEdit") - || p_className == QLatin1String("vte::IndicatorsBorder") - || p_className == QLatin1String("vte::MarkdownEditor") - || p_className == QLatin1String("vte::VMarkdownEditor") - || p_className == QLatin1String("vte::VTextEditor") - || p_className == QLatin1String("vte::ViStatusBar") - || p_className == QLatin1String("vte::StatusIndicator") - || p_className == QLatin1String("vte::ScrollBar")) { + if (p_className.startsWith(QStringLiteral("vnotex::")) + || p_className.startsWith(QStringLiteral("vte::"))) { return new FakeAccessibleInterface(p_obj); } diff --git a/src/utils/callbackpool.cpp b/src/utils/callbackpool.cpp new file mode 100644 index 00000000..b58f5939 --- /dev/null +++ b/src/utils/callbackpool.cpp @@ -0,0 +1,29 @@ +#include "callbackpool.h" + +#include + +using namespace vnotex; + +quint64 CallbackPool::add(const Callback &p_callback) +{ + static quint64 nextId = 0; + quint64 id = nextId++; + m_pool.insert(id, p_callback); + return id; +} + +void CallbackPool::call(quint64 p_id, void *p_data) +{ + auto it = m_pool.find(p_id); + if (it != m_pool.end()) { + it.value()(p_data); + m_pool.erase(it); + } else { + qWarning() << "failed to locate callback in pool with id" << p_id; + } +} + +void CallbackPool::clear() +{ + m_pool.clear(); +} diff --git a/src/utils/callbackpool.h b/src/utils/callbackpool.h new file mode 100644 index 00000000..c33819f9 --- /dev/null +++ b/src/utils/callbackpool.h @@ -0,0 +1,29 @@ +#ifndef CALLBACKPOOL_H +#define CALLBACKPOOL_H + +#include + +#include + +namespace vnotex +{ + // Manage callbacks with id. + class CallbackPool + { + public: + typedef std::function Callback; + + CallbackPool() = default; + + quint64 add(const Callback &p_callback); + + void call(quint64 p_id, void *p_data); + + void clear(); + + private: + QMap m_pool; + }; +} + +#endif // CALLBACKPOOL_H diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index 82f0bf1e..2ade0c72 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -192,3 +192,15 @@ QJsonValue Utils::parseAndReadJson(const QJsonObject &p_obj, const QString &p_ex return val; } + +QColor Utils::toColor(const QString &p_color) +{ + // rgb(123, 123, 123). + QRegularExpression rgbTripleRegExp(R"(^rgb\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\)$)", QRegularExpression::CaseInsensitiveOption); + auto match = rgbTripleRegExp.match(p_color); + if (match.hasMatch()) { + return QColor(match.captured(1).toInt(), match.captured(2).toInt(), match.captured(3).toInt()); + } + + return QColor(p_color); +} diff --git a/src/utils/utils.h b/src/utils/utils.h index 6ac1a7f3..8d847f76 100644 --- a/src/utils/utils.h +++ b/src/utils/utils.h @@ -61,6 +61,8 @@ namespace vnotex // Parse @p_exp into tokens and read the target value from @p_obj. // Format: obj1.obj2.arr[2].obj3. static QJsonValue parseAndReadJson(const QJsonObject &p_obj, const QString &p_exp); + + static QColor toColor(const QString &p_color); }; } // ns vnotex diff --git a/src/utils/utils.pri b/src/utils/utils.pri index d92cb069..9b04e7b9 100644 --- a/src/utils/utils.pri +++ b/src/utils/utils.pri @@ -1,6 +1,7 @@ QT += widgets svg SOURCES += \ + $$PWD/callbackpool.cpp \ $$PWD/contentmediautils.cpp \ $$PWD/docsutils.cpp \ $$PWD/htmlutils.cpp \ @@ -17,6 +18,7 @@ SOURCES += \ $$PWD/clipboardutils.cpp HEADERS += \ + $$PWD/callbackpool.h \ $$PWD/contentmediautils.h \ $$PWD/docsutils.h \ $$PWD/htmlutils.h \ diff --git a/src/widgets/editors/markdowneditor.cpp b/src/widgets/editors/markdowneditor.cpp index 6d48ff69..28e9586e 100644 --- a/src/widgets/editors/markdowneditor.cpp +++ b/src/widgets/editors/markdowneditor.cpp @@ -1057,7 +1057,7 @@ void MarkdownEditor::parseToMarkdownAndPaste() void MarkdownEditor::handleHtmlToMarkdownData(quint64 p_id, TimeStamp p_timeStamp, const QString &p_text) { Q_UNUSED(p_id); - qDebug() << "htmlToMarkdownData" << p_timeStamp; + qDebug() << "htmlToMarkdownData" << p_timeStamp << p_text; if (m_timeStamp == p_timeStamp && !p_text.isEmpty()) { QString text(p_text); diff --git a/src/widgets/editors/markdownvieweradapter.cpp b/src/widgets/editors/markdownvieweradapter.cpp index a0855b7d..2f9401fd 100644 --- a/src/widgets/editors/markdownvieweradapter.cpp +++ b/src/widgets/editors/markdownvieweradapter.cpp @@ -1,6 +1,5 @@ #include "markdownvieweradapter.h" -#include #include #include "../outlineprovider.h" @@ -61,6 +60,35 @@ QJsonObject MarkdownViewerAdapter::FindOption::toJson() const return obj; } +MarkdownViewerAdapter::CssRuleStyle MarkdownViewerAdapter::CssRuleStyle::fromJson(const QJsonObject &p_obj) +{ + CssRuleStyle style; + style.m_selector = p_obj[QStringLiteral("selector")].toString(); + style.m_color = p_obj[QStringLiteral("color")].toString(); + style.m_backgroundColor = p_obj[QStringLiteral("backgroundColor")].toString(); + style.m_fontWeight = p_obj[QStringLiteral("fontWeight")].toString(); + style.m_fontStyle = p_obj[QStringLiteral("fontStyle")].toString(); + return style; +} + +QTextCharFormat MarkdownViewerAdapter::CssRuleStyle::toTextCharFormat() const +{ + QTextCharFormat fmt; + if (!m_color.isEmpty()) { + fmt.setForeground(Utils::toColor(m_color)); + } + if (!m_backgroundColor.isEmpty()) { + fmt.setBackground(QColor(m_color)); + } + if (m_fontWeight.contains(QStringLiteral("bold"))) { + fmt.setFontWeight(QFont::Bold); + } + if (m_fontStyle.contains(QStringLiteral("italic"))) { + fmt.setFontItalic(true); + } + return fmt; +} + MarkdownViewerAdapter::MarkdownViewerAdapter(QObject *p_parent) : QObject(p_parent) { @@ -269,6 +297,11 @@ void MarkdownViewerAdapter::setMarkdownFromHtml(quint64 p_id, quint64 p_timeStam emit htmlToMarkdownReady(p_id, p_timeStamp, p_text); } +void MarkdownViewerAdapter::setCodeBlockHighlightHtml(int p_idx, quint64 p_timeStamp, const QString &p_html) +{ + emit highlightCodeBlockReady(p_idx, p_timeStamp, p_html); +} + void MarkdownViewerAdapter::setCrossCopyTargets(const QJsonArray &p_targets) { m_crossCopyTargets.clear(); @@ -401,3 +434,44 @@ void MarkdownViewerAdapter::renderGraph(quint64 p_id, Q_ASSERT(false); } } + +void MarkdownViewerAdapter::highlightCodeBlock(int p_idx, quint64 p_timeStamp, const QString &p_text) +{ + if (m_viewerReady) { + emit highlightCodeBlockRequested(p_idx, p_timeStamp, p_text); + } else { + m_pendingActions.append([this, p_idx, p_timeStamp, p_text]() { + emit highlightCodeBlockRequested(p_idx, p_timeStamp, p_text); + }); + } +} + +void MarkdownViewerAdapter::setStyleSheetStyles(quint64 p_id, const QJsonArray &p_styles) +{ + QVector ruleStyles; + ruleStyles.reserve(p_styles.size()); + for (int i = 0; i < p_styles.size(); ++i) { + ruleStyles.push_back(CssRuleStyle::fromJson(p_styles[i].toObject())); + } + + m_callbackPool.call(p_id, &ruleStyles); +} + +void MarkdownViewerAdapter::fetchStylesFromStyleSheet(const QString &p_styleSheet, + const std::function *)> &p_callback) +{ + if (p_styleSheet.isEmpty()) { + return; + } + + const quint64 id = m_callbackPool.add([p_callback](void *data) { + p_callback(reinterpret_cast *>(data)); + }); + if (m_viewerReady) { + emit parseStyleSheetRequested(id, p_styleSheet); + } else { + m_pendingActions.append([this, p_styleSheet, id]() { + emit parseStyleSheetRequested(id, p_styleSheet); + }); + } +} diff --git a/src/widgets/editors/markdownvieweradapter.h b/src/widgets/editors/markdownvieweradapter.h index 531c8b3e..52dafa8a 100644 --- a/src/widgets/editors/markdownvieweradapter.h +++ b/src/widgets/editors/markdownvieweradapter.h @@ -6,8 +6,10 @@ #include #include #include +#include #include +#include namespace vnotex { @@ -78,6 +80,23 @@ namespace vnotex bool m_regularExpression = false; }; + struct CssRuleStyle + { + QTextCharFormat toTextCharFormat() const; + + static CssRuleStyle fromJson(const QJsonObject &p_obj); + + QString m_selector; + + QString m_color; + + QString m_backgroundColor; + + QString m_fontWeight; + + QString m_fontStyle; + }; + explicit MarkdownViewerAdapter(QObject *p_parent = nullptr); virtual ~MarkdownViewerAdapter(); @@ -114,6 +133,12 @@ namespace vnotex // Should be called before WebViewer.setHtml(). void reset(); + void highlightCodeBlock(int p_idx, quint64 p_timeStamp, const QString &p_text); + + // Parse style sheet and fetch the styles. + void fetchStylesFromStyleSheet(const QString &p_styleSheet, + const std::function *)> &p_callback); + // Functions to be called from web side. public slots: void setReady(bool p_ready); @@ -152,6 +177,8 @@ namespace vnotex // Set back the result of htmlToMarkdown() call. void setMarkdownFromHtml(quint64 p_id, quint64 p_timeStamp, const QString &p_text); + void setCodeBlockHighlightHtml(int p_idx, quint64 p_timeStamp, const QString &p_html); + void setCrossCopyTargets(const QJsonArray &p_targets); void setCrossCopyResult(quint64 p_id, quint64 p_timeStamp, const QString &p_html); @@ -167,6 +194,8 @@ namespace vnotex const QString &p_lang, const QString &p_text); + void setStyleSheetStyles(quint64 p_id, const QJsonArray &p_styles); + // Signals to be connected at web side. signals: // Current Markdown text is updated. @@ -208,6 +237,10 @@ namespace vnotex const QString &p_format, const QString &p_data); + void highlightCodeBlockRequested(int p_idx, quint64 p_timeStamp, const QString &p_text); + + void parseStyleSheetRequested(quint64 p_id, const QString &p_styleSheet); + // Signals to be connected at cpp side. signals: void graphPreviewDataReady(const PreviewData &p_data); @@ -238,6 +271,8 @@ namespace vnotex const QString &p_content, const QString &p_bodyClassList); + void highlightCodeBlockReady(int p_idx, quint64 p_timeStamp, const QString &p_html); + private: void scrollToLine(int p_lineNumber); @@ -261,6 +296,8 @@ namespace vnotex // Targets supported by cross copy. Set by web. QStringList m_crossCopyTargets; + + CallbackPool m_callbackPool; }; } diff --git a/src/widgets/mainwindow.cpp b/src/widgets/mainwindow.cpp index 3be9695b..c0329462 100644 --- a/src/widgets/mainwindow.cpp +++ b/src/widgets/mainwindow.cpp @@ -383,7 +383,11 @@ void MainWindow::closeEvent(QCloseEvent *p_event) // Avoid geometry corruption caused by fullscreen or minimized window. const auto state = windowState(); if (state & (Qt::WindowMinimized | Qt::WindowFullScreen)) { - showNormal(); + if (m_windowOldState & Qt::WindowMaximized) { + showMaximized(); + } else { + showNormal(); + } } saveStateAndGeometry(); } diff --git a/src/widgets/markdownviewwindow.cpp b/src/widgets/markdownviewwindow.cpp index 680af137..c2b469e1 100644 --- a/src/widgets/markdownviewwindow.cpp +++ b/src/widgets/markdownviewwindow.cpp @@ -17,12 +17,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -359,6 +361,10 @@ void MarkdownViewWindow::setupTextEditor() adapter(), &MarkdownViewerAdapter::htmlToMarkdownRequested); connect(adapter(), &MarkdownViewerAdapter::htmlToMarkdownReady, m_editor, &MarkdownEditor::handleHtmlToMarkdownData); + connect(m_editor, &vte::VMarkdownEditor::externalCodeBlockHighlightRequested, + this, &MarkdownViewWindow::handleExternalCodeBlockHighlightRequest); + connect(adapter(), &MarkdownViewerAdapter::highlightCodeBlockReady, + m_editor, &vte::VMarkdownEditor::handleExternalCodeBlockHighlightData); // Connect outline pipeline. connect(m_editor, &MarkdownEditor::headingsChanged, @@ -1383,3 +1389,52 @@ void MarkdownViewWindow::print() }); } } + +void MarkdownViewWindow::handleExternalCodeBlockHighlightRequest(int p_idx, quint64 p_timeStamp, const QString &p_text) +{ + static bool stylesInitialized = false; + if (!stylesInitialized) { + stylesInitialized = true; + const auto file = VNoteX::getInst().getThemeMgr().getFile(Theme::File::HighlightStyleSheet); + if (file.isEmpty()) { + qWarning() << "no highlight style sheet specified for external code block highlight"; + } else { + QString content; + try { + content = FileUtils::readTextFile(file); + } catch (Exception &e) { + qWarning() << "failed to read highlight style sheet for external code block highlight" << file << e.what(); + } + adapter()->fetchStylesFromStyleSheet(content, [this](const QVector *rules) { + MarkdownEditor::ExternalCodeBlockHighlightStyles styles; + + const QString prefix(".token."); + for (const auto &rule : *rules) { + bool isFirst = true; + QTextCharFormat fmt; + + // Just fetch `.token.attr` styles. + auto selects = rule.m_selector.split(QLatin1Char(',')); + for (const auto &sel : selects) { + const auto ts = sel.trimmed(); + if (!ts.startsWith(prefix)) { + continue; + } + auto classList = ts.mid(prefix.size()).split(QLatin1Char('.')); + for (const auto &cla : classList) { + if (isFirst) { + fmt = rule.toTextCharFormat(); + isFirst = false; + } + styles.insert(cla, fmt); + } + } + } + + MarkdownEditor::setExternalCodeBlockHighlihgtStyles(styles); + }); + } + } + + adapter()->highlightCodeBlock(p_idx, p_timeStamp, p_text); +} diff --git a/src/widgets/markdownviewwindow.h b/src/widgets/markdownviewwindow.h index 08a1d2ce..9136e640 100644 --- a/src/widgets/markdownviewwindow.h +++ b/src/widgets/markdownviewwindow.h @@ -177,6 +177,8 @@ namespace vnotex void syncEditorPositionToPreview(); + void handleExternalCodeBlockHighlightRequest(int p_idx, quint64 p_timeStamp, const QString &p_text); + template static QSharedPointer headingsToOutline(const QVector &p_headings);