Selaa lähdekoodia

support find and replace (#1593)

TODO: we may need to transform the `QRegularExpression` usage to the `RegExp` in JS.
Le Tan 4 vuotta sitten
vanhempi
sitoutus
847e3d621d

+ 1 - 1
libs/vtextedit

@@ -1 +1 @@
-Subproject commit 85585710ce04eaa01ec82d7d7dc4c82fed0dde41
+Subproject commit 86cf8e0e6d840b923dc30046d12ab3c6e634f6b9

+ 2 - 0
src/core/editorconfig.h

@@ -46,6 +46,8 @@ namespace vnotex
             Outline,
             RichPaste,
             FindAndReplace,
+            FindNext,
+            FindPrevious,
             MaxShortcut
         };
         Q_ENUM(Shortcut)

+ 5 - 4
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);
 

+ 6 - 5
src/core/htmltemplatehelper.cpp

@@ -4,6 +4,7 @@
 
 #include <core/markdowneditorconfig.h>
 #include <core/configmgr.h>
+#include <utils/utils.h>
 #include <utils/fileutils.h>
 #include <utils/pathutils.h>
 #include <core/thememgr.h>
@@ -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("}");
 }
 

+ 0 - 5
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;
     };
 

+ 1 - 0
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<int>(m_findAndReplaceOptions);
     return obj;
 }
 

+ 12 - 2
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
     }
 }

+ 1 - 1
src/data/extra/docs/en/about_vnotex.txt

@@ -1,5 +1,5 @@
 <p>
-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.
 <br/><br/>
 Source code of VNote could be found at <a href="https://github.com/vnotex/vnote">GitHub</a>.
 <br/><br/>

+ 2 - 0
src/data/extra/extra.qrc

@@ -63,6 +63,8 @@
         <file>web/js/turndown/turndown.js</file>
         <file>web/js/turndown/turndown-plugin-gfm.js</file>
         <file>web/js/turndown.js</file>
+        <file>web/js/mark.js/mark.min.js</file>
+        <file>web/js/markjs.js</file>
         <file>syntax-highlighting/themes/markdown-default.theme</file>
         <file>syntax-highlighting/themes/default.theme</file>
         <file>syntax-highlighting/themes/breeze-dark.theme</file>

+ 1 - 31
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"
-    }
+   }
 }

+ 15 - 0
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",

+ 10 - 0
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;
+}

+ 3 - 0
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

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8 - 0
src/data/extra/web/js/mark.js/mark.min.js


+ 4 - 0
src/data/extra/web/js/markdownviewer.js

@@ -39,6 +39,10 @@ new QWebChannel(qt.webChannelTransport,
             window.vnotex.crossCopy(p_id, p_timeStamp, p_target, p_baseUrl, p_html);
         });
 
+        adapter.findTextRequested.connect(function(p_text, p_options) {
+            window.vnotex.findText(p_text, p_options);
+        });
+
         console.log('QWebChannel has been set up');
         if (window.vnotex.initialized) {
             window.vnotex.kickOffMarkdown();

+ 128 - 0
src/data/extra/web/js/markjs.js

@@ -0,0 +1,128 @@
+class MarkJs {
+    constructor(p_adapter, p_container) {
+        this.className = 'vx-search-match';
+        this.currentMatchClassName = 'vx-current-search-match';
+        this.adapter = p_adapter;
+        this.container = p_container;
+        this.markjs = null;
+        this.cache = null;
+        this.matchedNodes = null;
+
+        this.adapter.on('basicMarkdownRendered', () => {
+            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);
+    }
+}

+ 1 - 1
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({

+ 10 - 0
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;
+    }
 }

+ 10 - 0
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) {

+ 1 - 1
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();
 }
 

+ 5 - 0
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");
+}

+ 2 - 0
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
 

+ 34 - 0
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);
+}

+ 21 - 0
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);
 

+ 62 - 12
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;
 }

+ 16 - 0
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;
     };
 }
 

+ 61 - 1
src/widgets/markdownviewwindow.cpp

@@ -20,7 +20,6 @@
 #include <core/vnotex.h>
 #include <core/thememgr.h>
 #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);
+}

+ 12 - 0
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;
 

+ 25 - 0
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);
+}

+ 10 - 0
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;
 

+ 54 - 0
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 <typename _ViewWindow>
+        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 <typename _ViewWindow>
+        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 <typename _ViewWindow>
+        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 <typename _ViewWindow>
+        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 <typename _ViewWindow>
+        static void handleFindAndReplaceWidgetClosed(_ViewWindow *p_win)
+        {
+            p_win->m_editor->clearIncrementalSearchHighlight();
+            p_win->m_editor->clearSearchHighlight();
+        }
     };
 }
 

+ 4 - 6
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();

+ 122 - 0
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));
+    }
+}

+ 36 - 2
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<QWidget> 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;

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä