From 847e3d621d8f4748d4a7b5461c0ceeb647fd0d90 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Sat, 12 Dec 2020 20:34:43 -0800 Subject: [PATCH] support find and replace (#1593) TODO: we may need to transform the `QRegularExpression` usage to the `RegExp` in JS. --- libs/vtextedit | 2 +- src/core/editorconfig.h | 2 + src/core/global.h | 9 +- src/core/htmltemplatehelper.cpp | 11 +- src/core/htmltemplatehelper.h | 5 - src/core/widgetconfig.cpp | 1 + src/data/core/vnotex.json | 14 +- src/data/extra/docs/en/about_vnotex.txt | 2 +- src/data/extra/extra.qrc | 2 + .../themes/native/editor-highlight.theme | 32 +---- .../extra/themes/native/text-editor.theme | 15 ++ src/data/extra/themes/native/web.css | 10 ++ src/data/extra/web/js/mark.js/README.md | 3 + src/data/extra/web/js/mark.js/mark.min.js | 13 ++ src/data/extra/web/js/markdownviewer.js | 4 + src/data/extra/web/js/markjs.js | 128 ++++++++++++++++++ src/data/extra/web/js/nodelinemapper.js | 2 +- src/data/extra/web/js/utils.js | 10 ++ src/data/extra/web/js/vnotex.js | 10 ++ src/utils/pathutils.cpp | 2 +- src/utils/utils.cpp | 5 + src/utils/utils.h | 2 + src/widgets/editors/markdownvieweradapter.cpp | 34 +++++ src/widgets/editors/markdownvieweradapter.h | 21 +++ src/widgets/findandreplacewidget.cpp | 74 ++++++++-- src/widgets/findandreplacewidget.h | 16 +++ src/widgets/markdownviewwindow.cpp | 62 ++++++++- src/widgets/markdownviewwindow.h | 12 ++ src/widgets/textviewwindow.cpp | 25 ++++ src/widgets/textviewwindow.h | 10 ++ src/widgets/textviewwindowhelper.h | 54 ++++++++ src/widgets/viewarea.cpp | 10 +- src/widgets/viewwindow.cpp | 122 +++++++++++++++++ src/widgets/viewwindow.h | 38 +++++- 34 files changed, 690 insertions(+), 72 deletions(-) create mode 100644 src/data/extra/web/js/mark.js/README.md create mode 100644 src/data/extra/web/js/mark.js/mark.min.js create mode 100644 src/data/extra/web/js/markjs.js diff --git a/libs/vtextedit b/libs/vtextedit index 85585710..86cf8e0e 160000 --- a/libs/vtextedit +++ b/libs/vtextedit @@ -1 +1 @@ -Subproject commit 85585710ce04eaa01ec82d7d7dc4c82fed0dde41 +Subproject commit 86cf8e0e6d840b923dc30046d12ab3c6e634f6b9 diff --git a/src/core/editorconfig.h b/src/core/editorconfig.h index 0ef248e3..ff21a233 100644 --- a/src/core/editorconfig.h +++ b/src/core/editorconfig.h @@ -46,6 +46,8 @@ namespace vnotex Outline, RichPaste, FindAndReplace, + FindNext, + FindPrevious, MaxShortcut }; Q_ENUM(Shortcut) diff --git a/src/core/global.h b/src/core/global.h index aa7aa6a5..bcd13888 100644 --- a/src/core/global.h +++ b/src/core/global.h @@ -64,10 +64,11 @@ namespace vnotex enum FindOption { None = 0, - CaseSensitive = 0x1U, - WholeWordOnly = 0x2U, - RegularExpression = 0x4U, - IncrementalSearch = 0x8U + FindBackward = 0x1U, + CaseSensitive = 0x2U, + WholeWordOnly = 0x4U, + RegularExpression = 0x8U, + IncrementalSearch = 0x10U }; Q_DECLARE_FLAGS(FindOptions, FindOption); diff --git a/src/core/htmltemplatehelper.cpp b/src/core/htmltemplatehelper.cpp index a7d9f6bc..e73f919a 100644 --- a/src/core/htmltemplatehelper.cpp +++ b/src/core/htmltemplatehelper.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -16,11 +17,11 @@ HtmlTemplateHelper::Template HtmlTemplateHelper::s_markdownViewerTemplate; QString WebGlobalOptions::toJavascriptObject() const { return QStringLiteral("window.vxOptions = {\n") - + QString("webPlantUml: %1,\n").arg(boolToString(m_webPlantUml)) - + QString("webGraphviz: %1,\n").arg(boolToString(m_webGraphviz)) - + QString("constrainImageWidthEnabled: %1,\n").arg(boolToString(m_constrainImageWidthEnabled)) - + QString("protectFromXss: %1,\n").arg(boolToString(m_protectFromXss)) - + QString("sectionNumberEnabled: %1\n").arg(boolToString(m_sectionNumberEnabled)) + + QString("webPlantUml: %1,\n").arg(Utils::boolToString(m_webPlantUml)) + + QString("webGraphviz: %1,\n").arg(Utils::boolToString(m_webGraphviz)) + + QString("constrainImageWidthEnabled: %1,\n").arg(Utils::boolToString(m_constrainImageWidthEnabled)) + + QString("protectFromXss: %1,\n").arg(Utils::boolToString(m_protectFromXss)) + + QString("sectionNumberEnabled: %1\n").arg(Utils::boolToString(m_sectionNumberEnabled)) + QStringLiteral("}"); } diff --git a/src/core/htmltemplatehelper.h b/src/core/htmltemplatehelper.h index a252ee0b..f1fc7db0 100644 --- a/src/core/htmltemplatehelper.h +++ b/src/core/htmltemplatehelper.h @@ -20,11 +20,6 @@ namespace vnotex bool m_protectFromXss = false; - QString boolToString(bool p_val) const - { - return p_val ? QStringLiteral("true") : QStringLiteral("false"); - } - QString toJavascriptObject() const; }; diff --git a/src/core/widgetconfig.cpp b/src/core/widgetconfig.cpp index 507cc236..fc15272c 100644 --- a/src/core/widgetconfig.cpp +++ b/src/core/widgetconfig.cpp @@ -28,6 +28,7 @@ QJsonObject WidgetConfig::toJson() const { QJsonObject obj; obj[QStringLiteral("outline_auto_expanded_level")] = m_outlineAutoExpandedLevel; + obj[QStringLiteral("find_and_replace_options")] = static_cast(m_findAndReplaceOptions); return obj; } diff --git a/src/data/core/vnotex.json b/src/data/core/vnotex.json index f52acdba..9e3646e0 100644 --- a/src/data/core/vnotex.json +++ b/src/data/core/vnotex.json @@ -51,7 +51,9 @@ "TypeTable" : "Ctrl+/", "Outline" : "Ctrl+G, O", "RichPaste" : "Ctrl+Shift+V", - "FindAndReplace" : "Ctrl+F" + "FindAndReplace" : "Ctrl+F", + "FindNext" : "F3", + "FindPrevious" : "Shift+F3" } }, "text_editor" : { @@ -195,6 +197,14 @@ "web/js/turndown/turndown-plugin-gfm.js", "web/js/turndown.js" ] + }, + { + "name" : "mark.js", + "enabled" : true, + "scripts" : [ + "web/js/mark.js/mark.min.js", + "web/js/markjs.js" + ] } ] }, @@ -226,6 +236,6 @@ "//comment" : "Level of the heading in outline that should expand to automatically (1-6)", "outline_auto_expanded_level" : 6, "//comment" : "Default find options in FindAndReplace", - "find_and_replace_options" : 8 + "find_and_replace_options" : 16 } } diff --git a/src/data/extra/docs/en/about_vnotex.txt b/src/data/extra/docs/en/about_vnotex.txt index 6d7888e0..def88b95 100644 --- a/src/data/extra/docs/en/about_vnotex.txt +++ b/src/data/extra/docs/en/about_vnotex.txt @@ -1,5 +1,5 @@

-VNoteX is designed to be a pleasant note-taking platform, refactored from VNote, which is an open source note-taking application for Markdown since 2016. VNote will share most of the code base with VNoteX since version 3.0 and continue to be open source. +VNoteX is designed to be a pleasant note-taking platform, refactored from VNote, which is an open source note-taking application for Markdown since 2016. VNote shares most of the code base with VNoteX since version 3.0 and continue to be open source.

Source code of VNote could be found at GitHub.

diff --git a/src/data/extra/extra.qrc b/src/data/extra/extra.qrc index 93204fa9..2fade3d1 100644 --- a/src/data/extra/extra.qrc +++ b/src/data/extra/extra.qrc @@ -63,6 +63,8 @@ web/js/turndown/turndown.js web/js/turndown/turndown-plugin-gfm.js web/js/turndown.js + web/js/mark.js/mark.min.js + web/js/markjs.js syntax-highlighting/themes/markdown-default.theme syntax-highlighting/themes/default.theme syntax-highlighting/themes/breeze-dark.theme diff --git a/src/data/extra/themes/native/editor-highlight.theme b/src/data/extra/themes/native/editor-highlight.theme index 3fcf3d35..630ac909 100644 --- a/src/data/extra/themes/native/editor-highlight.theme +++ b/src/data/extra/themes/native/editor-highlight.theme @@ -140,35 +140,5 @@ "text-color" : "#006e28", "selected-text-color" : "#006e28" } - }, - "editor-colors": { - "background-color" : "#ffffff", - "code-folding" : "#94caef", - "bracket-matching" : "#ffff00", - "current-line" : "#f8f7f6", - "icon-border" : "#f0f0f0", - "indentation-line" : "#d2d2d2", - "line-numbers" : "#a0a0a0", - "current-line-number" : "#1e1e1e", - "mark-bookmark" : "#0000ff", - "mark-breakpoint-active" : "#ff0000", - "mark-breakpoint-reached" : "#ffff00", - "mark-breakpoint-disabled" : "#ff00ff", - "mark-execution" : "#a0a0a4", - "mark-warning" : "#00ff00", - "mark-error" : "#ff0000", - "modified-lines" : "#fdbc4b", - "replace-highlight" : "#00ff00", - "saved-lines" : "#2ecc71", - "search-highlight" : "#ffff00", - "selection" : "#94caef", - "separator" : "#898887", - "spell-checking" : "#bf0303", - "tab-marker" : "#d2d2d2", - "template-background" : "#d6d2d0", - "template-placeholder" : "#baf8ce", - "template-focused-placeholder" : "#76da98", - "template-read-only-placeholder" : "#f6e6e6", - "word-wrap-marker" : "#ededed" - } + } } diff --git a/src/data/extra/themes/native/text-editor.theme b/src/data/extra/themes/native/text-editor.theme index 6b304bb5..015c1c18 100644 --- a/src/data/extra/themes/native/text-editor.theme +++ b/src/data/extra/themes/native/text-editor.theme @@ -39,6 +39,21 @@ }, "FoldingHighlight" : { "text-color" : "#ffa9c4f5" + }, + "IncrementalSearch" : { + "//comment" : "Incremental search highlight", + "text-color" : "#222222", + "background-color" : "#ce93d8" + }, + "Search" : { + "//comment" : "Search highlight", + "text-color" : "#222222", + "background-color" : "#4db6ac" + }, + "SearchUnderCursor" : { + "//comment" : "Search highlight under cursor", + "text-color" : "#222222", + "background-color" : "#66bb6a" } }, "//comment" : "Override the Text style in editor-styles", diff --git a/src/data/extra/themes/native/web.css b/src/data/extra/themes/native/web.css index 52259656..ffc5a773 100644 --- a/src/data/extra/themes/native/web.css +++ b/src/data/extra/themes/native/web.css @@ -290,3 +290,13 @@ span.modal-close:hover, span.modal-close:focus { color: #222222; } + +#vx-content span.vx-search-match { + color: #222222; + background-color: #4db6ac; +} + +#vx-content span.vx-current-search-match { + color: #222222; + background-color: #66bb6a; +} diff --git a/src/data/extra/web/js/mark.js/README.md b/src/data/extra/web/js/mark.js/README.md new file mode 100644 index 00000000..1da41d82 --- /dev/null +++ b/src/data/extra/web/js/mark.js/README.md @@ -0,0 +1,3 @@ +# [mark.js](https://github.com/julmot/mark.js) +v8.11.1 +Julian Kühnel diff --git a/src/data/extra/web/js/mark.js/mark.min.js b/src/data/extra/web/js/mark.js/mark.min.js new file mode 100644 index 00000000..5608e6bb --- /dev/null +++ b/src/data/extra/web/js/mark.js/mark.min.js @@ -0,0 +1,13 @@ +/*!*************************************************** +* mark.js v9.0.0 +* https://markjs.io/ +* Copyright (c) 2014–2018, Julian Kühnel +* Released under the MIT license https://git.io/vwTVl +*****************************************************/ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Mark=t()}(this,function(){"use strict";function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(t)}function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){for(var n=0;n1&&void 0!==arguments[1])||arguments[1],o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[],i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:5e3;t(this,e),this.ctx=n,this.iframes=r,this.exclude=o,this.iframesTimeout=i}return r(e,[{key:"getContexts",value:function(){var e=[];return(void 0!==this.ctx&&this.ctx?NodeList.prototype.isPrototypeOf(this.ctx)?Array.prototype.slice.call(this.ctx):Array.isArray(this.ctx)?this.ctx:"string"==typeof this.ctx?Array.prototype.slice.call(document.querySelectorAll(this.ctx)):[this.ctx]:[]).forEach(function(t){var n=e.filter(function(e){return e.contains(t)}).length>0;-1!==e.indexOf(t)||n||e.push(t)}),e}},{key:"getIframeContents",value:function(e,t){var n,r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){};try{var o=e.contentWindow;if(n=o.document,!o||!n)throw new Error("iframe inaccessible")}catch(e){r()}n&&t(n)}},{key:"isIframeBlank",value:function(e){var t="about:blank",n=e.getAttribute("src").trim();return e.contentWindow.location.href===t&&n!==t&&n}},{key:"observeIframeLoad",value:function(e,t,n){var r=this,o=!1,i=null,a=function a(){if(!o){o=!0,clearTimeout(i);try{r.isIframeBlank(e)||(e.removeEventListener("load",a),r.getIframeContents(e,t,n))}catch(e){n()}}};e.addEventListener("load",a),i=setTimeout(a,this.iframesTimeout)}},{key:"onIframeReady",value:function(e,t,n){try{"complete"===e.contentWindow.document.readyState?this.isIframeBlank(e)?this.observeIframeLoad(e,t,n):this.getIframeContents(e,t,n):this.observeIframeLoad(e,t,n)}catch(e){n()}}},{key:"waitForIframes",value:function(e,t){var n=this,r=0;this.forEachIframe(e,function(){return!0},function(e){r++,n.waitForIframes(e.querySelector("html"),function(){--r||t()})},function(e){e||t()})}},{key:"forEachIframe",value:function(t,n,r){var o=this,i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){},a=t.querySelectorAll("iframe"),s=a.length,c=0;a=Array.prototype.slice.call(a);var u=function(){--s<=0&&i(c)};s||u(),a.forEach(function(t){e.matches(t,o.exclude)?u():o.onIframeReady(t,function(e){n(t)&&(c++,r(e)),u()},u)})}},{key:"createIterator",value:function(e,t,n){return document.createNodeIterator(e,t,n,!1)}},{key:"createInstanceOnIframe",value:function(t){return new e(t.querySelector("html"),this.iframes)}},{key:"compareNodeIframe",value:function(e,t,n){if(e.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_PRECEDING){if(null===t)return!0;if(t.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_FOLLOWING)return!0}return!1}},{key:"getIteratorNode",value:function(e){var t=e.previousNode();return{prevNode:t,node:null===t?e.nextNode():e.nextNode()&&e.nextNode()}}},{key:"checkIframeFilter",value:function(e,t,n,r){var o=!1,i=!1;return r.forEach(function(e,t){e.val===n&&(o=t,i=e.handled)}),this.compareNodeIframe(e,t,n)?(!1!==o||i?!1===o||i||(r[o].handled=!0):r.push({val:n,handled:!0}),!0):(!1===o&&r.push({val:n,handled:!1}),!1)}},{key:"handleOpenIframes",value:function(e,t,n,r){var o=this;e.forEach(function(e){e.handled||o.getIframeContents(e.val,function(e){o.createInstanceOnIframe(e).forEachNode(t,n,r)})})}},{key:"iterateThroughNodes",value:function(e,t,n,r,o){for(var i,a,s,c=this,u=this.createIterator(t,e,r),l=[],h=[];s=void 0,s=c.getIteratorNode(u),a=s.prevNode,i=s.node;)this.iframes&&this.forEachIframe(t,function(e){return c.checkIframeFilter(i,a,e,l)},function(t){c.createInstanceOnIframe(t).forEachNode(e,function(e){return h.push(e)},r)}),h.push(i);h.forEach(function(e){n(e)}),this.iframes&&this.handleOpenIframes(l,e,n,r),o()}},{key:"forEachNode",value:function(e,t,n){var r=this,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){},i=this.getContexts(),a=i.length;a||o(),i.forEach(function(i){var s=function(){r.iterateThroughNodes(e,i,t,n,function(){--a<=0&&o()})};r.iframes?r.waitForIframes(i,s):s()})}}],[{key:"matches",value:function(e,t){var n="string"==typeof t?[t]:t,r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.oMatchesSelector||e.webkitMatchesSelector;if(r){var o=!1;return n.every(function(t){return!r.call(e,t)||(o=!0,!1)}),o}return!1}}]),e}(),a= +/* */ +function(){function e(n){t(this,e),this.opt=o({},{diacritics:!0,synonyms:{},accuracy:"partially",caseSensitive:!1,ignoreJoiners:!1,ignorePunctuation:[],wildcards:"disabled"},n)}return r(e,[{key:"create",value:function(e){return"disabled"!==this.opt.wildcards&&(e=this.setupWildcardsRegExp(e)),e=this.escapeStr(e),Object.keys(this.opt.synonyms).length&&(e=this.createSynonymsRegExp(e)),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),this.opt.diacritics&&(e=this.createDiacriticsRegExp(e)),e=this.createMergedBlanksRegExp(e),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.createJoinersRegExp(e)),"disabled"!==this.opt.wildcards&&(e=this.createWildcardsRegExp(e)),e=this.createAccuracyRegExp(e),new RegExp(e,"gm".concat(this.opt.caseSensitive?"":"i"))}},{key:"sortByLength",value:function(e){return e.sort(function(e,t){return e.length===t.length?e>t?1:-1:t.length-e.length})}},{key:"escapeStr",value:function(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}},{key:"createSynonymsRegExp",value:function(e){var t=this,n=this.opt.synonyms,r=this.opt.caseSensitive?"":"i",o=this.opt.ignoreJoiners||this.opt.ignorePunctuation.length?"\0":"";for(var i in n)if(n.hasOwnProperty(i)){var a=Array.isArray(n[i])?n[i]:[n[i]];a.unshift(i),(a=this.sortByLength(a).map(function(e){return"disabled"!==t.opt.wildcards&&(e=t.setupWildcardsRegExp(e)),e=t.escapeStr(e)}).filter(function(e){return""!==e})).length>1&&(e=e.replace(new RegExp("(".concat(a.map(function(e){return t.escapeStr(e)}).join("|"),")"),"gm".concat(r)),o+"(".concat(a.map(function(e){return t.processSynonyms(e)}).join("|"),")")+o))}return e}},{key:"processSynonyms",value:function(e){return(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),e}},{key:"setupWildcardsRegExp",value:function(e){return(e=e.replace(/(?:\\)*\?/g,function(e){return"\\"===e.charAt(0)?"?":""})).replace(/(?:\\)*\*/g,function(e){return"\\"===e.charAt(0)?"*":""})}},{key:"createWildcardsRegExp",value:function(e){var t="withSpaces"===this.opt.wildcards;return e.replace(/\u0001/g,t?"[\\S\\s]?":"\\S?").replace(/\u0002/g,t?"[\\S\\s]*?":"\\S*")}},{key:"setupIgnoreJoinersRegExp",value:function(e){return e.replace(/[^(|)\\]/g,function(e,t,n){var r=n.charAt(t+1);return/[(|)\\]/.test(r)||""===r?e:e+"\0"})}},{key:"createJoinersRegExp",value:function(e){var t=[],n=this.opt.ignorePunctuation;return Array.isArray(n)&&n.length&&t.push(this.escapeStr(n.join(""))),this.opt.ignoreJoiners&&t.push("\\u00ad\\u200b\\u200c\\u200d"),t.length?e.split(/\u0000+/).join("[".concat(t.join(""),"]*")):e}},{key:"createDiacriticsRegExp",value:function(e){var t=this.opt.caseSensitive?"":"i",n=this.opt.caseSensitive?["aàáảãạăằắẳẵặâầấẩẫậäåāą","AÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ","cçćč","CÇĆČ","dđď","DĐĎ","eèéẻẽẹêềếểễệëěēę","EÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ","iìíỉĩịîïī","IÌÍỈĨỊÎÏĪ","lł","LŁ","nñňń","NÑŇŃ","oòóỏõọôồốổỗộơởỡớờợöøō","OÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ","rř","RŘ","sšśșş","SŠŚȘŞ","tťțţ","TŤȚŢ","uùúủũụưừứửữựûüůū","UÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ","yýỳỷỹỵÿ","YÝỲỶỸỴŸ","zžżź","ZŽŻŹ"]:["aàáảãạăằắẳẵặâầấẩẫậäåāąAÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ","cçćčCÇĆČ","dđďDĐĎ","eèéẻẽẹêềếểễệëěēęEÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ","iìíỉĩịîïīIÌÍỈĨỊÎÏĪ","lłLŁ","nñňńNÑŇŃ","oòóỏõọôồốổỗộơởỡớờợöøōOÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ","rřRŘ","sšśșşSŠŚȘŞ","tťțţTŤȚŢ","uùúủũụưừứửữựûüůūUÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ","yýỳỷỹỵÿYÝỲỶỸỴŸ","zžżźZŽŻŹ"],r=[];return e.split("").forEach(function(o){n.every(function(n){if(-1!==n.indexOf(o)){if(r.indexOf(n)>-1)return!1;e=e.replace(new RegExp("[".concat(n,"]"),"gm".concat(t)),"[".concat(n,"]")),r.push(n)}return!0})}),e}},{key:"createMergedBlanksRegExp",value:function(e){return e.replace(/[\s]+/gim,"[\\s]+")}},{key:"createAccuracyRegExp",value:function(e){var t=this,n=this.opt.accuracy,r="string"==typeof n?n:n.value,o="string"==typeof n?[]:n.limiters,i="";switch(o.forEach(function(e){i+="|".concat(t.escapeStr(e))}),r){case"partially":default:return"()(".concat(e,")");case"complementary":return i="\\s"+(i||this.escapeStr("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~¡¿")),"()([^".concat(i,"]*").concat(e,"[^").concat(i,"]*)");case"exactly":return"(^|\\s".concat(i,")(").concat(e,")(?=$|\\s").concat(i,")")}}}]),e}(),s= +/* */ +function(){function n(e){t(this,n),this.ctx=e,this.ie=!1;var r=window.navigator.userAgent;(r.indexOf("MSIE")>-1||r.indexOf("Trident")>-1)&&(this.ie=!0)}return r(n,[{key:"log",value:function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"debug",r=this.opt.log;this.opt.debug&&"object"===e(r)&&"function"==typeof r[n]&&r[n]("mark.js: ".concat(t))}},{key:"getSeparatedKeywords",value:function(e){var t=this,n=[];return e.forEach(function(e){t.opt.separateWordSearch?e.split(" ").forEach(function(e){e.trim()&&-1===n.indexOf(e)&&n.push(e)}):e.trim()&&-1===n.indexOf(e)&&n.push(e)}),{keywords:n.sort(function(e,t){return t.length-e.length}),length:n.length}}},{key:"isNumeric",value:function(e){return Number(parseFloat(e))==e}},{key:"checkRanges",value:function(e){var t=this;if(!Array.isArray(e)||"[object Object]"!==Object.prototype.toString.call(e[0]))return this.log("markRanges() will only accept an array of objects"),this.opt.noMatch(e),[];var n=[],r=0;return e.sort(function(e,t){return e.start-t.start}).forEach(function(e){var o=t.callNoMatchOnInvalidRanges(e,r),i=o.start,a=o.end;o.valid&&(e.start=i,e.length=a-i,n.push(e),r=a)}),n}},{key:"callNoMatchOnInvalidRanges",value:function(e,t){var n,r,o=!1;return e&&void 0!==e.start?(r=(n=parseInt(e.start,10))+parseInt(e.length,10),this.isNumeric(e.start)&&this.isNumeric(e.length)&&r-t>0&&r-n>0?o=!0:(this.log("Ignoring invalid or overlapping range: "+"".concat(JSON.stringify(e))),this.opt.noMatch(e))):(this.log("Ignoring invalid range: ".concat(JSON.stringify(e))),this.opt.noMatch(e)),{start:n,end:r,valid:o}}},{key:"checkWhitespaceRanges",value:function(e,t,n){var r,o=!0,i=n.length,a=t-i,s=parseInt(e.start,10)-a;return(r=(s=s>i?i:s)+parseInt(e.length,10))>i&&(r=i,this.log("End range automatically set to the max value of ".concat(i))),s<0||r-s<0||s>i||r>i?(o=!1,this.log("Invalid range: ".concat(JSON.stringify(e))),this.opt.noMatch(e)):""===n.substring(s,r).replace(/\s+/g,"")&&(o=!1,this.log("Skipping whitespace only range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:s,end:r,valid:o}}},{key:"getTextNodes",value:function(e){var t=this,n="",r=[];this.iterator.forEachNode(NodeFilter.SHOW_TEXT,function(e){r.push({start:n.length,end:(n+=e.textContent).length,node:e})},function(e){return t.matchesExclude(e.parentNode)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT},function(){e({value:n,nodes:r})})}},{key:"matchesExclude",value:function(e){return i.matches(e,this.opt.exclude.concat(["script","style","title","head","html"]))}},{key:"wrapRangeInTextNode",value:function(e,t,n){var r=this.opt.element?this.opt.element:"mark",o=e.splitText(t),i=o.splitText(n-t),a=document.createElement(r);return a.setAttribute("data-markjs","true"),this.opt.className&&a.setAttribute("class",this.opt.className),a.textContent=o.textContent,o.parentNode.replaceChild(a,o),i}},{key:"wrapRangeInMappedTextNode",value:function(e,t,n,r,o){var i=this;e.nodes.every(function(a,s){var c=e.nodes[s+1];if(void 0===c||c.start>t){if(!r(a.node))return!1;var u=t-a.start,l=(n>a.end?a.end:n)-a.start,h=e.value.substr(0,a.start),f=e.value.substr(l+a.start);if(a.node=i.wrapRangeInTextNode(a.node,u,l),e.value=h+f,e.nodes.forEach(function(t,n){n>=s&&(e.nodes[n].start>0&&n!==s&&(e.nodes[n].start-=l),e.nodes[n].end-=l)}),n-=l,o(a.node.previousSibling,a.start),!(n>a.end))return!1;t=a.end}return!0})}},{key:"wrapGroups",value:function(e,t,n,r){return r((e=this.wrapRangeInTextNode(e,t,t+n)).previousSibling),e}},{key:"separateGroups",value:function(e,t,n,r,o){for(var i=t.length,a=1;a-1&&r(t[a],e)&&(e=this.wrapGroups(e,s,t[a].length,o))}return e}},{key:"wrapMatches",value:function(e,t,n,r,o){var i=this,a=0===t?0:t+1;this.getTextNodes(function(t){t.nodes.forEach(function(t){var o;for(t=t.node;null!==(o=e.exec(t.textContent))&&""!==o[a];){if(i.opt.separateGroups)t=i.separateGroups(t,o,a,n,r);else{if(!n(o[a],t))continue;var s=o.index;if(0!==a)for(var c=1;c { + this.clearCache(); + }); + } + + // @p_options: { + // findBackward, + // caseSensitive, + // wholeWordOnly, + // regularExpression + // } + findText(p_text, p_options) { + if (!this.markjs) { + this.markjs = new Mark(this.container); + } + + if (!p_text) { + // Clear the cache and highlight. + this.clearCache(); + return; + } + + if (this.findInCache(p_text, p_options)) { + return; + } + + // A new find. + this.clearCache(); + + let callbackFunc = function(markjs, text, options) { + let _markjs = markjs; + let _text = text; + let _options = options; + return function(totalMatches) { + if (!_markjs.matchedNodes) { + _markjs.matchedNodes = _markjs.container.getElementsByClassName(_markjs.className); + } + + // Update cache. + _markjs.cache = { + text: _text, + options: _options, + currentIdx: -1 + } + + _markjs.updateCurrentMatch(_text, !_options.findBackward); + }; + } + let opt = { + 'element': 'span', + 'className': this.className, + 'caseSensitive': p_options.caseSensitive, + 'accuracy': p_options.wholeWordOnly ? 'exactly' : 'partially', + 'done': callbackFunc(this, p_text, p_options) + } + + if (p_options.regularExpression) { + // TODO: may need transformation from QRegularExpression to RegExp. + this.markjs.markRegExp(new RegExp(p_text), opt); + } else { + this.markjs.mark(p_text, opt); + } + } + + clearCache() { + if (!this.markjs) { + return; + } + + this.cache = null; + this.markjs.unmark(); + } + + findInCache(p_text, p_options) { + if (!this.cache) { + return false; + } + + if (this.cache.text === p_text + && this.cache.options.caseSensitive == p_options.caseSensitive + && this.cache.options.wholeWordOnly == p_options.wholeWordOnly + && this.cache.options.regularExpression == p_options.regularExpression) { + // Matched. Move current match forward or backward. + this.updateCurrentMatch(p_text, !p_options.findBackward); + return true; + } + + return false; + } + + updateCurrentMatch(p_text, p_forward) { + let matches = this.matchedNodes.length; + if (matches == 0) { + this.adapter.showFindResult(p_text, 0, 0); + return; + } + if (this.cache.currentIdx >= 0) { + this.matchedNodes[this.cache.currentIdx].classList.remove(this.currentMatchClassName); + } + if (p_forward) { + this.cache.currentIdx += 1; + if (this.cache.currentIdx >= matches) { + this.cache.currentIdx = 0; + } + } else { + this.cache.currentIdx -= 1; + if (this.cache.currentIdx < 0) { + this.cache.currentIdx = matches - 1; + } + } + let node = this.matchedNodes[this.cache.currentIdx]; + node.classList.add(this.currentMatchClassName); + if (!Utils.isVisible(node)) { + node.scrollIntoView(); + } + this.adapter.showFindResult(p_text, matches, this.cache.currentIdx); + } +} diff --git a/src/data/extra/web/js/nodelinemapper.js b/src/data/extra/web/js/nodelinemapper.js index 63cfb5d7..f1f2a549 100644 --- a/src/data/extra/web/js/nodelinemapper.js +++ b/src/data/extra/web/js/nodelinemapper.js @@ -42,7 +42,7 @@ class NodeLineMapper { this.headingNodes = this.container.querySelectorAll("h1, h2, h3, h4, h5, h6"); let headings = []; let needSectionNumber = window.vxOptions.sectionNumberEnabled; - let regExp = /^(?:\d\.)+ /; + let regExp = /^\d(?:\.\d)*\.? /; for (let i = 0; i < this.headingNodes.length; ++i) { let node = this.headingNodes[i]; headings.push({ diff --git a/src/data/extra/web/js/utils.js b/src/data/extra/web/js/utils.js index 3a414a0a..9f733676 100644 --- a/src/data/extra/web/js/utils.js +++ b/src/data/extra/web/js/utils.js @@ -99,4 +99,14 @@ class Utils { height: rect.height }; } + + static isVisible(p_node) { + let rect = p_node.getBoundingClientRect(); + let vrect = this.viewPortRect(); + if (rect.top < 0 || rect.left < 0 + || rect.bottom > vrect.height || rect.right > vrect.width) { + return false; + } + return true; + } } diff --git a/src/data/extra/web/js/vnotex.js b/src/data/extra/web/js/vnotex.js index 844826ed..d9b73613 100644 --- a/src/data/extra/web/js/vnotex.js +++ b/src/data/extra/web/js/vnotex.js @@ -50,6 +50,8 @@ class VNoteX extends EventEmitter { this.crossCopyer = new CrossCopy(this); + this.searcher = new MarkJs(this, this.contentContainer); + this.initialized = true; // Signal out. @@ -250,6 +252,14 @@ class VNoteX extends EventEmitter { window.vxMarkdownAdapter.setCrossCopyResult(p_id, p_timeStamp, p_html); } + findText(p_text, p_options) { + this.searcher.findText(p_text, p_options); + } + + showFindResult(p_text, p_totalMatches, p_currentMatchIndex) { + window.vxMarkdownAdapter.setFindText(p_text, p_totalMatches, p_currentMatchIndex); + } + static detectOS() { let osName="Unknown OS"; if (navigator.appVersion.indexOf("Win")!=-1) { diff --git a/src/utils/pathutils.cpp b/src/utils/pathutils.cpp index fa97b2cf..a79836f4 100644 --- a/src/utils/pathutils.cpp +++ b/src/utils/pathutils.cpp @@ -62,7 +62,7 @@ QString PathUtils::concatenateFilePath(const QString &p_dirPath, const QString & QString PathUtils::dirName(const QString &p_path) { - Q_ASSERT(QFileInfo(p_path).isDir()); + Q_ASSERT(!QFileInfo::exists(p_path) || QFileInfo(p_path).isDir()); return QDir(p_path).dirName(); } diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index 51ab5fd8..9c0dfb2c 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -112,3 +112,8 @@ bool Utils::fuzzyEqual(qreal p_a, qreal p_b) { return std::abs(p_a - p_b) < std::pow(10, -6); } + +QString Utils::boolToString(bool p_val) +{ + return p_val ? QStringLiteral("true") : QStringLiteral("false"); +} diff --git a/src/utils/utils.h b/src/utils/utils.h index 209c222d..6fea811b 100644 --- a/src/utils/utils.h +++ b/src/utils/utils.h @@ -52,6 +52,8 @@ namespace vnotex qreal p_scaleFactor); static bool fuzzyEqual(qreal p_a, qreal p_b); + + static QString boolToString(bool p_val); }; } // ns vnotex diff --git a/src/widgets/editors/markdownvieweradapter.cpp b/src/widgets/editors/markdownvieweradapter.cpp index 4b546e86..ed08c6d9 100644 --- a/src/widgets/editors/markdownvieweradapter.cpp +++ b/src/widgets/editors/markdownvieweradapter.cpp @@ -55,6 +55,16 @@ MarkdownViewerAdapter::Heading MarkdownViewerAdapter::Heading::fromJson(const QJ p_obj.value(QStringLiteral("anchor")).toString()); } +QJsonObject MarkdownViewerAdapter::FindOption::toJson() const +{ + QJsonObject obj; + obj["findBackward"] = m_findBackward; + obj["caseSensitive"] = m_caseSensitive; + obj["wholeWordOnly"] = m_wholeWordOnly; + obj["regularExpression"] = m_regularExpression; + return obj; +} + MarkdownViewerAdapter::MarkdownViewerAdapter(QObject *p_parent) : QObject(p_parent) { @@ -268,3 +278,27 @@ void MarkdownViewerAdapter::setCrossCopyResult(quint64 p_id, quint64 p_timeStamp { emit crossCopyReady(p_id, p_timeStamp, p_html); } + +void MarkdownViewerAdapter::findText(const QString &p_text, FindOptions p_options) +{ + FindOption opts; + if (p_options & vnotex::FindOption::FindBackward) { + opts.m_findBackward = true; + } + if (p_options & vnotex::FindOption::CaseSensitive) { + opts.m_caseSensitive = true; + } + if (p_options & vnotex::FindOption::WholeWordOnly) { + opts.m_wholeWordOnly = true; + } + if (p_options & vnotex::FindOption::RegularExpression) { + opts.m_regularExpression = true; + } + + emit findTextRequested(p_text, opts.toJson()); +} + +void MarkdownViewerAdapter::setFindText(const QString &p_text, int p_totalMatches, int p_currentMatchIndex) +{ + emit findTextReady(p_text, p_totalMatches, p_currentMatchIndex); +} diff --git a/src/widgets/editors/markdownvieweradapter.h b/src/widgets/editors/markdownvieweradapter.h index 46527db3..7e252dd6 100644 --- a/src/widgets/editors/markdownvieweradapter.h +++ b/src/widgets/editors/markdownvieweradapter.h @@ -78,6 +78,19 @@ namespace vnotex QString m_anchor; }; + struct FindOption + { + QJsonObject toJson() const; + + bool m_findBackward = false; + + bool m_caseSensitive = false; + + bool m_wholeWordOnly = false; + + bool m_regularExpression = false; + }; + explicit MarkdownViewerAdapter(QObject *p_parent = nullptr); virtual ~MarkdownViewerAdapter(); @@ -102,6 +115,8 @@ namespace vnotex const QStringList &getCrossCopyTargets() const; + void findText(const QString &p_text, FindOptions p_options); + // Functions to be called from web side. public slots: void setReady(bool p_ready); @@ -142,6 +157,8 @@ namespace vnotex void setCrossCopyResult(quint64 p_id, quint64 p_timeStamp, const QString &p_html); + void setFindText(const QString &p_text, int p_totalMatches, int p_currentMatchIndex); + // Signals to be connected at web side. signals: // Current Markdown text is updated. @@ -173,6 +190,8 @@ namespace vnotex const QString &p_baseUrl, const QString &p_html); + void findTextRequested(const QString &p_text, const QJsonObject &p_options); + // Signals to be connected at cpp side. signals: void graphPreviewDataReady(const PreviewData &p_data); @@ -193,6 +212,8 @@ namespace vnotex void crossCopyReady(quint64 p_id, quint64 p_timeStamp, const QString &p_html); + void findTextReady(const QString &p_text, int p_totalMatches, int p_currentMatchIndex); + private: void scrollToLine(int p_lineNumber); diff --git a/src/widgets/findandreplacewidget.cpp b/src/widgets/findandreplacewidget.cpp index 3ec7516a..84a12bab 100644 --- a/src/widgets/findandreplacewidget.cpp +++ b/src/widgets/findandreplacewidget.cpp @@ -30,7 +30,7 @@ FindAndReplaceWidget::FindAndReplaceWidget(QWidget *p_parent) m_findTextTimer->setInterval(500); connect(m_findTextTimer, &QTimer::timeout, this, [this]() { - emit findTextChanged(m_findLineEdit->text(), m_options); + emit findTextChanged(getFindText(), getOptions()); }); setupUI(); @@ -155,6 +155,7 @@ void FindAndReplaceWidget::setupUI() void FindAndReplaceWidget::close() { hide(); + emit closed(); } void FindAndReplaceWidget::setReplaceEnabled(bool p_enabled) @@ -198,47 +199,82 @@ void FindAndReplaceWidget::keyPressEvent(QKeyEvent *p_event) void FindAndReplaceWidget::findNext() { - + m_findTextTimer->stop(); + auto text = m_findLineEdit->text(); + if (text.isEmpty()) { + return; + } + emit findNextRequested(text, m_options); } void FindAndReplaceWidget::findPrevious() { - + m_findTextTimer->stop(); + auto text = m_findLineEdit->text(); + if (text.isEmpty()) { + return; + } + emit findNextRequested(text, m_options | FindOption::FindBackward); } void FindAndReplaceWidget::updateFindOptions() { - m_options = FindOption::None; + if (m_optionCheckBoxMuted) { + return; + } + + FindOptions options = FindOption::None; if (m_caseSensitiveCheckBox->isChecked()) { - m_options |= FindOption::CaseSensitive; + options |= FindOption::CaseSensitive; } if (m_wholeWordOnlyCheckBox->isChecked()) { - m_options |= FindOption::WholeWordOnly; + options |= FindOption::WholeWordOnly; } if (m_regularExpressionCheckBox->isChecked()) { - m_options |= FindOption::RegularExpression; + options |= FindOption::RegularExpression; } if (m_incrementalSearchCheckBox->isChecked()) { - m_options |= FindOption::IncrementalSearch; + options |= FindOption::IncrementalSearch; } + if (options == m_options) { + return; + } + m_options = options; ConfigMgr::getInst().getWidgetConfig().setFindAndReplaceOptions(m_options); + m_findTextTimer->start(); } void FindAndReplaceWidget::replace() { - + m_findTextTimer->stop(); + auto text = m_findLineEdit->text(); + if (text.isEmpty()) { + return; + } + emit replaceRequested(text, m_options, m_replaceLineEdit->text()); } void FindAndReplaceWidget::replaceAndFind() { - + m_findTextTimer->stop(); + auto text = m_findLineEdit->text(); + if (text.isEmpty()) { + return; + } + emit replaceRequested(text, m_options, m_replaceLineEdit->text()); + emit findNextRequested(text, m_options); } void FindAndReplaceWidget::replaceAll() { - + m_findTextTimer->stop(); + auto text = m_findLineEdit->text(); + if (text.isEmpty()) { + return; + } + emit replaceAllRequested(text, m_options, m_replaceLineEdit->text()); } void FindAndReplaceWidget::setFindOptions(FindOptions p_options) @@ -247,11 +283,13 @@ void FindAndReplaceWidget::setFindOptions(FindOptions p_options) return; } - m_options = p_options; + m_optionCheckBoxMuted = true; + m_options = p_options & ~FindOption::FindBackward; m_caseSensitiveCheckBox->setChecked(m_options & FindOption::CaseSensitive); m_wholeWordOnlyCheckBox->setChecked(m_options & FindOption::WholeWordOnly); m_regularExpressionCheckBox->setChecked(m_options & FindOption::RegularExpression); m_incrementalSearchCheckBox->setChecked(m_options & FindOption::IncrementalSearch); + m_optionCheckBoxMuted = false; } void FindAndReplaceWidget::open(const QString &p_text) @@ -264,4 +302,16 @@ void FindAndReplaceWidget::open(const QString &p_text) m_findLineEdit->setFocus(); m_findLineEdit->selectAll(); + + emit opened(); +} + +QString FindAndReplaceWidget::getFindText() const +{ + return m_findLineEdit->text(); +} + +FindOptions FindAndReplaceWidget::getOptions() const +{ + return m_options; } diff --git a/src/widgets/findandreplacewidget.h b/src/widgets/findandreplacewidget.h index ae3e60f0..73e48844 100644 --- a/src/widgets/findandreplacewidget.h +++ b/src/widgets/findandreplacewidget.h @@ -25,9 +25,23 @@ namespace vnotex void close(); + QString getFindText() const; + + FindOptions getOptions() const; + signals: void findTextChanged(const QString &p_text, FindOptions p_options); + void findNextRequested(const QString &p_text, FindOptions p_options); + + void replaceRequested(const QString &p_text, FindOptions p_options, const QString &p_replaceText); + + void replaceAllRequested(const QString &p_text, FindOptions p_options, const QString &p_replaceText); + + void closed(); + + void opened(); + protected: void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE; @@ -66,6 +80,8 @@ namespace vnotex FindOptions m_options = FindOption::None; QTimer *m_findTextTimer = nullptr; + + bool m_optionCheckBoxMuted = false; }; } diff --git a/src/widgets/markdownviewwindow.cpp b/src/widgets/markdownviewwindow.cpp index 4e7afcc7..d8d8717e 100644 --- a/src/widgets/markdownviewwindow.cpp +++ b/src/widgets/markdownviewwindow.cpp @@ -20,7 +20,6 @@ #include #include #include "editors/markdowneditor.h" -#include "textviewwindow.h" #include "textviewwindowhelper.h" #include "editors/markdownviewer.h" #include "editors/editormarkdownvieweradapter.h" @@ -28,6 +27,7 @@ #include "dialogs/deleteconfirmdialog.h" #include "outlineprovider.h" #include "toolbarhelper.h" +#include "findandreplacewidget.h" using namespace vnotex; @@ -82,6 +82,10 @@ void MarkdownViewWindow::setupUI() void MarkdownViewWindow::setMode(Mode p_mode) { setModeInternal(p_mode); + + if (m_findAndReplace && m_findAndReplace->isVisible()) { + m_findAndReplace->setReplaceEnabled(m_mode != Mode::Read); + } } void MarkdownViewWindow::setModeInternal(Mode p_mode) @@ -278,6 +282,7 @@ void MarkdownViewWindow::setupToolBar() toolBar->addSeparator(); ToolBarHelper::addSpacer(toolBar); + addAction(toolBar, ViewWindowToolBarHelper::FindAndReplace); addAction(toolBar, ViewWindowToolBarHelper::Outline); } @@ -418,6 +423,8 @@ void MarkdownViewWindow::setupViewer() m_outlineProvider->setCurrentHeadingIndex(this->adapter()->getCurrentHeadingIndex()); } }); + connect(adapter, &MarkdownViewerAdapter::findTextReady, + this, &ViewWindow::showFindResult); } void MarkdownViewWindow::syncTextEditorFromBuffer(bool p_syncPositionFromReadMode) @@ -792,3 +799,56 @@ void MarkdownViewWindow::zoom(bool p_zoomIn) textEditorConfig.setZoomDelta(m_editor->zoomDelta()); showZoomDelta(m_editor->zoomDelta()); } + +void MarkdownViewWindow::handleFindTextChanged(const QString &p_text, FindOptions p_options) +{ + if (m_mode == Mode::Read) { + adapter()->findText(p_text, p_options); + } else { + TextViewWindowHelper::handleFindTextChanged(this, p_text, p_options); + } +} + +void MarkdownViewWindow::handleFindNext(const QString &p_text, FindOptions p_options) +{ + if (m_mode == Mode::Read) { + if (p_options & FindOption::IncrementalSearch) { + adapter()->findText(p_text, p_options); + } + } else { + TextViewWindowHelper::handleFindNext(this, p_text, p_options); + } +} + +void MarkdownViewWindow::handleReplace(const QString &p_text, FindOptions p_options, const QString &p_replaceText) +{ + if (m_mode == Mode::Read) { + VNoteX::getInst().showStatusMessageShort(tr("Replace is not supported in read mode")); + } else { + TextViewWindowHelper::handleReplace(this, p_text, p_options, p_replaceText); + } +} + +void MarkdownViewWindow::handleReplaceAll(const QString &p_text, FindOptions p_options, const QString &p_replaceText) +{ + if (m_mode == Mode::Read) { + VNoteX::getInst().showStatusMessageShort(tr("Replace is not supported in read mode")); + } else { + TextViewWindowHelper::handleReplaceAll(this, p_text, p_options, p_replaceText); + } +} + +void MarkdownViewWindow::handleFindAndReplaceWidgetClosed() +{ + if (m_editor) { + TextViewWindowHelper::handleFindAndReplaceWidgetClosed(this); + } else { + adapter()->findText("", FindOption::None); + } +} + +void MarkdownViewWindow::handleFindAndReplaceWidgetOpened() +{ + Q_ASSERT(m_findAndReplace); + m_findAndReplace->setReplaceEnabled(m_mode != Mode::Read); +} diff --git a/src/widgets/markdownviewwindow.h b/src/widgets/markdownviewwindow.h index 3db50739..cc436df6 100644 --- a/src/widgets/markdownviewwindow.h +++ b/src/widgets/markdownviewwindow.h @@ -49,6 +49,18 @@ namespace vnotex void handleTypeAction(TypeAction p_action) Q_DECL_OVERRIDE; + void handleFindTextChanged(const QString &p_text, FindOptions p_options) Q_DECL_OVERRIDE; + + void handleFindNext(const QString &p_text, FindOptions p_options) Q_DECL_OVERRIDE; + + void handleReplace(const QString &p_text, FindOptions p_options, const QString &p_replaceText) Q_DECL_OVERRIDE; + + void handleReplaceAll(const QString &p_text, FindOptions p_options, const QString &p_replaceText) Q_DECL_OVERRIDE; + + void handleFindAndReplaceWidgetClosed() Q_DECL_OVERRIDE; + + void handleFindAndReplaceWidgetOpened() Q_DECL_OVERRIDE; + protected: void syncEditorFromBuffer() Q_DECL_OVERRIDE; diff --git a/src/widgets/textviewwindow.cpp b/src/widgets/textviewwindow.cpp index bfa58449..8a8f0c54 100644 --- a/src/widgets/textviewwindow.cpp +++ b/src/widgets/textviewwindow.cpp @@ -181,3 +181,28 @@ void TextViewWindow::zoom(bool p_zoomIn) textEditorConfig.setZoomDelta(m_editor->zoomDelta()); showZoomDelta(m_editor->zoomDelta()); } + +void TextViewWindow::handleFindTextChanged(const QString &p_text, FindOptions p_options) +{ + TextViewWindowHelper::handleFindTextChanged(this, p_text, p_options); +} + +void TextViewWindow::handleFindNext(const QString &p_text, FindOptions p_options) +{ + TextViewWindowHelper::handleFindNext(this, p_text, p_options); +} + +void TextViewWindow::handleReplace(const QString &p_text, FindOptions p_options, const QString &p_replaceText) +{ + TextViewWindowHelper::handleReplace(this, p_text, p_options, p_replaceText); +} + +void TextViewWindow::handleReplaceAll(const QString &p_text, FindOptions p_options, const QString &p_replaceText) +{ + TextViewWindowHelper::handleReplaceAll(this, p_text, p_options, p_replaceText); +} + +void TextViewWindow::handleFindAndReplaceWidgetClosed() +{ + TextViewWindowHelper::handleFindAndReplaceWidgetClosed(this); +} diff --git a/src/widgets/textviewwindow.h b/src/widgets/textviewwindow.h index 5e57e16a..1b1712af 100644 --- a/src/widgets/textviewwindow.h +++ b/src/widgets/textviewwindow.h @@ -33,6 +33,16 @@ namespace vnotex void handleBufferChangedInternal() Q_DECL_OVERRIDE; + void handleFindTextChanged(const QString &p_text, FindOptions p_options) Q_DECL_OVERRIDE; + + void handleFindNext(const QString &p_text, FindOptions p_options) Q_DECL_OVERRIDE; + + void handleReplace(const QString &p_text, FindOptions p_options, const QString &p_replaceText) Q_DECL_OVERRIDE; + + void handleReplaceAll(const QString &p_text, FindOptions p_options, const QString &p_replaceText) Q_DECL_OVERRIDE; + + void handleFindAndReplaceWidgetClosed() Q_DECL_OVERRIDE; + protected: void syncEditorFromBuffer() Q_DECL_OVERRIDE; diff --git a/src/widgets/textviewwindowhelper.h b/src/widgets/textviewwindowhelper.h index 76386a59..3d8b0e10 100644 --- a/src/widgets/textviewwindowhelper.h +++ b/src/widgets/textviewwindowhelper.h @@ -127,6 +127,60 @@ namespace vnotex return editorConfig; } + + static vte::FindFlags toEditorFindFlags(FindOptions p_options) + { + vte::FindFlags flags; + if (p_options & FindOption::FindBackward) { + flags |= vte::FindFlag::FindBackward; + } + if (p_options & FindOption::CaseSensitive) { + flags |= vte::FindFlag::CaseSensitive; + } + if (p_options & FindOption::WholeWordOnly) { + flags |= vte::FindFlag::WholeWordOnly; + } + if (p_options & FindOption::RegularExpression) { + flags |= vte::FindFlag::RegularExpression; + } + return flags; + } + + template + static void handleFindTextChanged(_ViewWindow *p_win, const QString &p_text, FindOptions p_options) + { + if (p_options & FindOption::IncrementalSearch) { + p_win->m_editor->peekText(p_text, toEditorFindFlags(p_options)); + } + } + + template + static void handleFindNext(_ViewWindow *p_win, const QString &p_text, FindOptions p_options) + { + const auto result = p_win->m_editor->findText(p_text, toEditorFindFlags(p_options)); + p_win->showFindResult(p_text, result.m_totalMatches, result.m_currentMatchIndex); + } + + template + static void handleReplace(_ViewWindow *p_win, const QString &p_text, FindOptions p_options, const QString &p_replaceText) + { + const auto result = p_win->m_editor->replaceText(p_text, toEditorFindFlags(p_options), p_replaceText); + p_win->showReplaceResult(p_text, result.m_totalMatches); + } + + template + static void handleReplaceAll(_ViewWindow *p_win, const QString &p_text, FindOptions p_options, const QString &p_replaceText) + { + const auto result = p_win->m_editor->replaceAll(p_text, toEditorFindFlags(p_options), p_replaceText); + p_win->showReplaceResult(p_text, result.m_totalMatches); + } + + template + static void handleFindAndReplaceWidgetClosed(_ViewWindow *p_win) + { + p_win->m_editor->clearIncrementalSearchHighlight(); + p_win->m_editor->clearSearchHighlight(); + } }; } diff --git a/src/widgets/viewarea.cpp b/src/widgets/viewarea.cpp index b58b8304..626f0439 100644 --- a/src/widgets/viewarea.cpp +++ b/src/widgets/viewarea.cpp @@ -756,9 +756,8 @@ void ViewArea::setupGlobalShortcuts() // CloseTab. { - QKeySequence kseq(coreConfig.getShortcut(CoreConfig::CloseTab)); - if (!kseq.isEmpty()) { - auto shortcut = new QShortcut(kseq, this); + auto shortcut = WidgetUtils::createShortcut(coreConfig.getShortcut(CoreConfig::CloseTab), this); + if (shortcut) { connect(shortcut, &QShortcut::activated, this, [this]() { auto win = getCurrentViewWindow(); @@ -771,9 +770,8 @@ void ViewArea::setupGlobalShortcuts() // LocateNode. { - QKeySequence kseq(coreConfig.getShortcut(CoreConfig::LocateNode)); - if (!kseq.isEmpty()) { - auto shortcut = new QShortcut(kseq, this); + auto shortcut = WidgetUtils::createShortcut(coreConfig.getShortcut(CoreConfig::LocateNode), this); + if (shortcut) { connect(shortcut, &QShortcut::activated, this, [this]() { auto win = getCurrentViewWindow(); diff --git a/src/widgets/viewwindow.cpp b/src/widgets/viewwindow.cpp index 50f716c8..512a3d8d 100644 --- a/src/widgets/viewwindow.cpp +++ b/src/widgets/viewwindow.cpp @@ -816,6 +816,29 @@ void ViewWindow::updateEditReadDiscardActionState(EditReadDiscardAction *p_act) void ViewWindow::setupShortcuts() { + const auto &editorConfig = ConfigMgr::getInst().getEditorConfig(); + + // FindNext. + { + auto shortcut = WidgetUtils::createShortcut(editorConfig.getShortcut(EditorConfig::FindNext), this, Qt::WidgetWithChildrenShortcut); + if (shortcut) { + connect(shortcut, &QShortcut::activated, + this, [this]() { + findNextOnLastFind(true); + }); + } + } + + // FindPrevious. + { + auto shortcut = WidgetUtils::createShortcut(editorConfig.getShortcut(EditorConfig::FindPrevious), this, Qt::WidgetWithChildrenShortcut); + if (shortcut) { + connect(shortcut, &QShortcut::activated, + this, [this]() { + findNextOnLastFind(false); + }); + } + } } void ViewWindow::wheelEvent(QWheelEvent *p_event) @@ -849,6 +872,23 @@ void ViewWindow::showFindAndReplaceWidget() if (!m_findAndReplace) { m_findAndReplace = new FindAndReplaceWidget(this); m_mainLayout->addWidget(m_findAndReplace); + + // Connect it to slots. + connect(m_findAndReplace, &FindAndReplaceWidget::findTextChanged, + this, &ViewWindow::handleFindTextChanged); + connect(m_findAndReplace, &FindAndReplaceWidget::findNextRequested, + this, &ViewWindow::findNext); + connect(m_findAndReplace, &FindAndReplaceWidget::replaceRequested, + this, &ViewWindow::replace); + connect(m_findAndReplace, &FindAndReplaceWidget::replaceAllRequested, + this, &ViewWindow::replaceAll); + connect(m_findAndReplace, &FindAndReplaceWidget::closed, + this, [this]() { + setFocus(); + handleFindAndReplaceWidgetClosed(); + }); + connect(m_findAndReplace, &FindAndReplaceWidget::opened, + this, &ViewWindow::handleFindAndReplaceWidgetOpened); } m_findAndReplace->open(QString()); @@ -881,3 +921,85 @@ bool ViewWindow::findAndReplaceWidgetVisible() const { return m_findAndReplace && m_findAndReplace->isVisible(); } + +void ViewWindow::handleFindTextChanged(const QString &p_text, FindOptions p_options) +{ +} + +void ViewWindow::handleFindNext(const QString &p_text, FindOptions p_options) +{ +} + +void ViewWindow::handleReplace(const QString &p_text, FindOptions p_options, const QString &p_replaceText) +{ +} + +void ViewWindow::handleReplaceAll(const QString &p_text, FindOptions p_options, const QString &p_replaceText) +{ +} + +void ViewWindow::handleFindAndReplaceWidgetClosed() +{ +} + +void ViewWindow::handleFindAndReplaceWidgetOpened() +{ +} + +void ViewWindow::findNextOnLastFind(bool p_forward) +{ + // Check if need to update the find info. + if (m_findAndReplace && m_findAndReplace->isVisible()) { + m_findInfo.m_text = m_findAndReplace->getFindText(); + m_findInfo.m_options = m_findAndReplace->getOptions(); + } + + if (m_findInfo.m_text.isEmpty()) { + return; + } + + if (p_forward) { + handleFindNext(m_findInfo.m_text, m_findInfo.m_options & ~FindOption::FindBackward); + } else { + handleFindNext(m_findInfo.m_text, m_findInfo.m_options | FindOption::FindBackward); + } +} + +void ViewWindow::findNext(const QString &p_text, FindOptions p_options) +{ + m_findInfo.m_text = p_text; + m_findInfo.m_options = p_options; + handleFindNext(p_text, p_options); +} + +void ViewWindow::replace(const QString &p_text, FindOptions p_options, const QString &p_replaceText) +{ + m_findInfo.m_text = p_text; + m_findInfo.m_options = p_options; + handleReplace(p_text, p_options, p_replaceText); +} + +void ViewWindow::replaceAll(const QString &p_text, FindOptions p_options, const QString &p_replaceText) +{ + m_findInfo.m_text = p_text; + m_findInfo.m_options = p_options; + handleReplaceAll(p_text, p_options, p_replaceText); +} + +void ViewWindow::showFindResult(const QString &p_text, int p_totalMatches, int p_currentMatchIndex) +{ + if (p_totalMatches == 0) { + VNoteX::getInst().showStatusMessageShort(tr("Pattern not found: %1").arg(p_text)); + } else { + VNoteX::getInst().showStatusMessageShort(tr("Match found: %1/%2").arg(p_currentMatchIndex + 1).arg(p_totalMatches)); + } +} + +void ViewWindow::showReplaceResult(const QString &p_text, int p_totalReplaces) +{ + if (p_totalReplaces == 0) { + VNoteX::getInst().showStatusMessageShort(tr("Pattern not found: %1").arg(p_text)); + } else { + VNoteX::getInst().showStatusMessageShort(tr("Replaced %n match(es)", "", p_totalReplaces)); + } +} diff --git a/src/widgets/viewwindow.h b/src/widgets/viewwindow.h index faf2e2e5..03ee5978 100644 --- a/src/widgets/viewwindow.h +++ b/src/widgets/viewwindow.h @@ -77,6 +77,12 @@ namespace vnotex public slots: virtual void handleEditorConfigChange() = 0; + void findNext(const QString &p_text, FindOptions p_options); + + void replace(const QString &p_text, FindOptions p_options, const QString &p_replaceText); + + void replaceAll(const QString &p_text, FindOptions p_options, const QString &p_replaceText); + signals: // Emit when the attached buffer is changed. void bufferChanged(); @@ -130,6 +136,18 @@ namespace vnotex // Handle all kinds of type action. virtual void handleTypeAction(TypeAction p_action); + virtual void handleFindTextChanged(const QString &p_text, FindOptions p_options); + + virtual void handleFindNext(const QString &p_text, FindOptions p_options); + + virtual void handleReplace(const QString &p_text, FindOptions p_options, const QString &p_replaceText); + + virtual void handleReplaceAll(const QString &p_text, FindOptions p_options, const QString &p_replaceText); + + virtual void handleFindAndReplaceWidgetClosed(); + + virtual void handleFindAndReplaceWidgetOpened(); + protected: void setCentralWidget(QWidget *p_widget); @@ -183,6 +201,11 @@ namespace vnotex bool findAndReplaceWidgetVisible() const; + // @p_currentMatchIndex: 0-based. + static void showFindResult(const QString &p_text, int p_totalMatches, int p_currentMatchIndex); + + static void showReplaceResult(const QString &p_text, int p_totalReplaces); + static ViewWindow::Mode modeFromOpenParameters(const FileOpenParameters &p_paras); QSharedPointer m_statusWidget; @@ -196,7 +219,16 @@ namespace vnotex Mode m_mode = Mode::Invalid; + // Managed by QObject. + FindAndReplaceWidget *m_findAndReplace = nullptr; + private: + struct FindInfo + { + QString m_text; + FindOptions m_options; + }; + void setupUI(); void initIcons(); @@ -236,6 +268,8 @@ namespace vnotex }; int checkFileMissingOrChangedOutside(); + void findNextOnLastFind(bool p_forward = true); + static ViewWindow::TypeAction toolBarActionToTypeAction(ViewWindowToolBarHelper::Action p_action); Buffer *m_buffer = nullptr; @@ -269,8 +303,8 @@ namespace vnotex // Whether check file missing or changed outside. bool m_fileChangeCheckEnabled = true; - // Managed by QObject. - FindAndReplaceWidget *m_findAndReplace = nullptr; + // Last find info. + FindInfo m_findInfo; static QIcon s_savedIcon; static QIcon s_modifiedIcon;