support find and replace (#1593)

TODO: we may need to transform the `QRegularExpression` usage to the `RegExp` in JS.
This commit is contained in:
Le Tan 2020-12-12 20:34:43 -08:00 committed by GitHub
parent 3d7406ff24
commit 847e3d621d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 690 additions and 72 deletions

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

View File

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

View File

@ -64,10 +64,11 @@ namespace vnotex
enum FindOption enum FindOption
{ {
None = 0, None = 0,
CaseSensitive = 0x1U, FindBackward = 0x1U,
WholeWordOnly = 0x2U, CaseSensitive = 0x2U,
RegularExpression = 0x4U, WholeWordOnly = 0x4U,
IncrementalSearch = 0x8U RegularExpression = 0x8U,
IncrementalSearch = 0x10U
}; };
Q_DECLARE_FLAGS(FindOptions, FindOption); Q_DECLARE_FLAGS(FindOptions, FindOption);

View File

@ -4,6 +4,7 @@
#include <core/markdowneditorconfig.h> #include <core/markdowneditorconfig.h>
#include <core/configmgr.h> #include <core/configmgr.h>
#include <utils/utils.h>
#include <utils/fileutils.h> #include <utils/fileutils.h>
#include <utils/pathutils.h> #include <utils/pathutils.h>
#include <core/thememgr.h> #include <core/thememgr.h>
@ -16,11 +17,11 @@ HtmlTemplateHelper::Template HtmlTemplateHelper::s_markdownViewerTemplate;
QString WebGlobalOptions::toJavascriptObject() const QString WebGlobalOptions::toJavascriptObject() const
{ {
return QStringLiteral("window.vxOptions = {\n") return QStringLiteral("window.vxOptions = {\n")
+ QString("webPlantUml: %1,\n").arg(boolToString(m_webPlantUml)) + QString("webPlantUml: %1,\n").arg(Utils::boolToString(m_webPlantUml))
+ QString("webGraphviz: %1,\n").arg(boolToString(m_webGraphviz)) + QString("webGraphviz: %1,\n").arg(Utils::boolToString(m_webGraphviz))
+ QString("constrainImageWidthEnabled: %1,\n").arg(boolToString(m_constrainImageWidthEnabled)) + QString("constrainImageWidthEnabled: %1,\n").arg(Utils::boolToString(m_constrainImageWidthEnabled))
+ QString("protectFromXss: %1,\n").arg(boolToString(m_protectFromXss)) + QString("protectFromXss: %1,\n").arg(Utils::boolToString(m_protectFromXss))
+ QString("sectionNumberEnabled: %1\n").arg(boolToString(m_sectionNumberEnabled)) + QString("sectionNumberEnabled: %1\n").arg(Utils::boolToString(m_sectionNumberEnabled))
+ QStringLiteral("}"); + QStringLiteral("}");
} }

View File

@ -20,11 +20,6 @@ namespace vnotex
bool m_protectFromXss = false; bool m_protectFromXss = false;
QString boolToString(bool p_val) const
{
return p_val ? QStringLiteral("true") : QStringLiteral("false");
}
QString toJavascriptObject() const; QString toJavascriptObject() const;
}; };

View File

@ -28,6 +28,7 @@ QJsonObject WidgetConfig::toJson() const
{ {
QJsonObject obj; QJsonObject obj;
obj[QStringLiteral("outline_auto_expanded_level")] = m_outlineAutoExpandedLevel; obj[QStringLiteral("outline_auto_expanded_level")] = m_outlineAutoExpandedLevel;
obj[QStringLiteral("find_and_replace_options")] = static_cast<int>(m_findAndReplaceOptions);
return obj; return obj;
} }

View File

@ -51,7 +51,9 @@
"TypeTable" : "Ctrl+/", "TypeTable" : "Ctrl+/",
"Outline" : "Ctrl+G, O", "Outline" : "Ctrl+G, O",
"RichPaste" : "Ctrl+Shift+V", "RichPaste" : "Ctrl+Shift+V",
"FindAndReplace" : "Ctrl+F" "FindAndReplace" : "Ctrl+F",
"FindNext" : "F3",
"FindPrevious" : "Shift+F3"
} }
}, },
"text_editor" : { "text_editor" : {
@ -195,6 +197,14 @@
"web/js/turndown/turndown-plugin-gfm.js", "web/js/turndown/turndown-plugin-gfm.js",
"web/js/turndown.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)", "//comment" : "Level of the heading in outline that should expand to automatically (1-6)",
"outline_auto_expanded_level" : 6, "outline_auto_expanded_level" : 6,
"//comment" : "Default find options in FindAndReplace", "//comment" : "Default find options in FindAndReplace",
"find_and_replace_options" : 8 "find_and_replace_options" : 16
} }
} }

View File

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

View File

@ -63,6 +63,8 @@
<file>web/js/turndown/turndown.js</file> <file>web/js/turndown/turndown.js</file>
<file>web/js/turndown/turndown-plugin-gfm.js</file> <file>web/js/turndown/turndown-plugin-gfm.js</file>
<file>web/js/turndown.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/markdown-default.theme</file>
<file>syntax-highlighting/themes/default.theme</file> <file>syntax-highlighting/themes/default.theme</file>
<file>syntax-highlighting/themes/breeze-dark.theme</file> <file>syntax-highlighting/themes/breeze-dark.theme</file>

View File

@ -140,35 +140,5 @@
"text-color" : "#006e28", "text-color" : "#006e28",
"selected-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"
} }
} }

View File

@ -39,6 +39,21 @@
}, },
"FoldingHighlight" : { "FoldingHighlight" : {
"text-color" : "#ffa9c4f5" "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", "//comment" : "Override the Text style in editor-styles",

View File

@ -290,3 +290,13 @@ span.modal-close:hover,
span.modal-close:focus { span.modal-close:focus {
color: #222222; 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;
}

View File

@ -0,0 +1,3 @@
# [mark.js](https://github.com/julmot/mark.js)
v8.11.1
Julian Kühnel

File diff suppressed because one or more lines are too long

View File

@ -39,6 +39,10 @@ new QWebChannel(qt.webChannelTransport,
window.vnotex.crossCopy(p_id, p_timeStamp, p_target, p_baseUrl, p_html); 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'); console.log('QWebChannel has been set up');
if (window.vnotex.initialized) { if (window.vnotex.initialized) {
window.vnotex.kickOffMarkdown(); window.vnotex.kickOffMarkdown();

View File

@ -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);
}
}

View File

@ -42,7 +42,7 @@ class NodeLineMapper {
this.headingNodes = this.container.querySelectorAll("h1, h2, h3, h4, h5, h6"); this.headingNodes = this.container.querySelectorAll("h1, h2, h3, h4, h5, h6");
let headings = []; let headings = [];
let needSectionNumber = window.vxOptions.sectionNumberEnabled; let needSectionNumber = window.vxOptions.sectionNumberEnabled;
let regExp = /^(?:\d\.)+ /; let regExp = /^\d(?:\.\d)*\.? /;
for (let i = 0; i < this.headingNodes.length; ++i) { for (let i = 0; i < this.headingNodes.length; ++i) {
let node = this.headingNodes[i]; let node = this.headingNodes[i];
headings.push({ headings.push({

View File

@ -99,4 +99,14 @@ class Utils {
height: rect.height 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;
}
} }

View File

@ -50,6 +50,8 @@ class VNoteX extends EventEmitter {
this.crossCopyer = new CrossCopy(this); this.crossCopyer = new CrossCopy(this);
this.searcher = new MarkJs(this, this.contentContainer);
this.initialized = true; this.initialized = true;
// Signal out. // Signal out.
@ -250,6 +252,14 @@ class VNoteX extends EventEmitter {
window.vxMarkdownAdapter.setCrossCopyResult(p_id, p_timeStamp, p_html); 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() { static detectOS() {
let osName="Unknown OS"; let osName="Unknown OS";
if (navigator.appVersion.indexOf("Win")!=-1) { if (navigator.appVersion.indexOf("Win")!=-1) {

View File

@ -62,7 +62,7 @@ QString PathUtils::concatenateFilePath(const QString &p_dirPath, const QString &
QString PathUtils::dirName(const QString &p_path) 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(); return QDir(p_path).dirName();
} }

View File

@ -112,3 +112,8 @@ bool Utils::fuzzyEqual(qreal p_a, qreal p_b)
{ {
return std::abs(p_a - p_b) < std::pow(10, -6); return std::abs(p_a - p_b) < std::pow(10, -6);
} }
QString Utils::boolToString(bool p_val)
{
return p_val ? QStringLiteral("true") : QStringLiteral("false");
}

View File

@ -52,6 +52,8 @@ namespace vnotex
qreal p_scaleFactor); qreal p_scaleFactor);
static bool fuzzyEqual(qreal p_a, qreal p_b); static bool fuzzyEqual(qreal p_a, qreal p_b);
static QString boolToString(bool p_val);
}; };
} // ns vnotex } // ns vnotex

View File

@ -55,6 +55,16 @@ MarkdownViewerAdapter::Heading MarkdownViewerAdapter::Heading::fromJson(const QJ
p_obj.value(QStringLiteral("anchor")).toString()); 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) MarkdownViewerAdapter::MarkdownViewerAdapter(QObject *p_parent)
: 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); 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);
}

View File

@ -78,6 +78,19 @@ namespace vnotex
QString m_anchor; 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); explicit MarkdownViewerAdapter(QObject *p_parent = nullptr);
virtual ~MarkdownViewerAdapter(); virtual ~MarkdownViewerAdapter();
@ -102,6 +115,8 @@ namespace vnotex
const QStringList &getCrossCopyTargets() const; const QStringList &getCrossCopyTargets() const;
void findText(const QString &p_text, FindOptions p_options);
// Functions to be called from web side. // Functions to be called from web side.
public slots: public slots:
void setReady(bool p_ready); void setReady(bool p_ready);
@ -142,6 +157,8 @@ namespace vnotex
void setCrossCopyResult(quint64 p_id, quint64 p_timeStamp, const QString &p_html); 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 to be connected at web side.
signals: signals:
// Current Markdown text is updated. // Current Markdown text is updated.
@ -173,6 +190,8 @@ namespace vnotex
const QString &p_baseUrl, const QString &p_baseUrl,
const QString &p_html); const QString &p_html);
void findTextRequested(const QString &p_text, const QJsonObject &p_options);
// Signals to be connected at cpp side. // Signals to be connected at cpp side.
signals: signals:
void graphPreviewDataReady(const PreviewData &p_data); 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 crossCopyReady(quint64 p_id, quint64 p_timeStamp, const QString &p_html);
void findTextReady(const QString &p_text, int p_totalMatches, int p_currentMatchIndex);
private: private:
void scrollToLine(int p_lineNumber); void scrollToLine(int p_lineNumber);

View File

@ -30,7 +30,7 @@ FindAndReplaceWidget::FindAndReplaceWidget(QWidget *p_parent)
m_findTextTimer->setInterval(500); m_findTextTimer->setInterval(500);
connect(m_findTextTimer, &QTimer::timeout, connect(m_findTextTimer, &QTimer::timeout,
this, [this]() { this, [this]() {
emit findTextChanged(m_findLineEdit->text(), m_options); emit findTextChanged(getFindText(), getOptions());
}); });
setupUI(); setupUI();
@ -155,6 +155,7 @@ void FindAndReplaceWidget::setupUI()
void FindAndReplaceWidget::close() void FindAndReplaceWidget::close()
{ {
hide(); hide();
emit closed();
} }
void FindAndReplaceWidget::setReplaceEnabled(bool p_enabled) void FindAndReplaceWidget::setReplaceEnabled(bool p_enabled)
@ -198,47 +199,82 @@ void FindAndReplaceWidget::keyPressEvent(QKeyEvent *p_event)
void FindAndReplaceWidget::findNext() void FindAndReplaceWidget::findNext()
{ {
m_findTextTimer->stop();
auto text = m_findLineEdit->text();
if (text.isEmpty()) {
return;
}
emit findNextRequested(text, m_options);
} }
void FindAndReplaceWidget::findPrevious() 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() void FindAndReplaceWidget::updateFindOptions()
{ {
m_options = FindOption::None; if (m_optionCheckBoxMuted) {
return;
}
FindOptions options = FindOption::None;
if (m_caseSensitiveCheckBox->isChecked()) { if (m_caseSensitiveCheckBox->isChecked()) {
m_options |= FindOption::CaseSensitive; options |= FindOption::CaseSensitive;
} }
if (m_wholeWordOnlyCheckBox->isChecked()) { if (m_wholeWordOnlyCheckBox->isChecked()) {
m_options |= FindOption::WholeWordOnly; options |= FindOption::WholeWordOnly;
} }
if (m_regularExpressionCheckBox->isChecked()) { if (m_regularExpressionCheckBox->isChecked()) {
m_options |= FindOption::RegularExpression; options |= FindOption::RegularExpression;
} }
if (m_incrementalSearchCheckBox->isChecked()) { 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); ConfigMgr::getInst().getWidgetConfig().setFindAndReplaceOptions(m_options);
m_findTextTimer->start();
} }
void FindAndReplaceWidget::replace() 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() 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() 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) void FindAndReplaceWidget::setFindOptions(FindOptions p_options)
@ -247,11 +283,13 @@ void FindAndReplaceWidget::setFindOptions(FindOptions p_options)
return; return;
} }
m_options = p_options; m_optionCheckBoxMuted = true;
m_options = p_options & ~FindOption::FindBackward;
m_caseSensitiveCheckBox->setChecked(m_options & FindOption::CaseSensitive); m_caseSensitiveCheckBox->setChecked(m_options & FindOption::CaseSensitive);
m_wholeWordOnlyCheckBox->setChecked(m_options & FindOption::WholeWordOnly); m_wholeWordOnlyCheckBox->setChecked(m_options & FindOption::WholeWordOnly);
m_regularExpressionCheckBox->setChecked(m_options & FindOption::RegularExpression); m_regularExpressionCheckBox->setChecked(m_options & FindOption::RegularExpression);
m_incrementalSearchCheckBox->setChecked(m_options & FindOption::IncrementalSearch); m_incrementalSearchCheckBox->setChecked(m_options & FindOption::IncrementalSearch);
m_optionCheckBoxMuted = false;
} }
void FindAndReplaceWidget::open(const QString &p_text) void FindAndReplaceWidget::open(const QString &p_text)
@ -264,4 +302,16 @@ void FindAndReplaceWidget::open(const QString &p_text)
m_findLineEdit->setFocus(); m_findLineEdit->setFocus();
m_findLineEdit->selectAll(); m_findLineEdit->selectAll();
emit opened();
}
QString FindAndReplaceWidget::getFindText() const
{
return m_findLineEdit->text();
}
FindOptions FindAndReplaceWidget::getOptions() const
{
return m_options;
} }

View File

@ -25,9 +25,23 @@ namespace vnotex
void close(); void close();
QString getFindText() const;
FindOptions getOptions() const;
signals: signals:
void findTextChanged(const QString &p_text, FindOptions p_options); 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: protected:
void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE; void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE;
@ -66,6 +80,8 @@ namespace vnotex
FindOptions m_options = FindOption::None; FindOptions m_options = FindOption::None;
QTimer *m_findTextTimer = nullptr; QTimer *m_findTextTimer = nullptr;
bool m_optionCheckBoxMuted = false;
}; };
} }

View File

@ -20,7 +20,6 @@
#include <core/vnotex.h> #include <core/vnotex.h>
#include <core/thememgr.h> #include <core/thememgr.h>
#include "editors/markdowneditor.h" #include "editors/markdowneditor.h"
#include "textviewwindow.h"
#include "textviewwindowhelper.h" #include "textviewwindowhelper.h"
#include "editors/markdownviewer.h" #include "editors/markdownviewer.h"
#include "editors/editormarkdownvieweradapter.h" #include "editors/editormarkdownvieweradapter.h"
@ -28,6 +27,7 @@
#include "dialogs/deleteconfirmdialog.h" #include "dialogs/deleteconfirmdialog.h"
#include "outlineprovider.h" #include "outlineprovider.h"
#include "toolbarhelper.h" #include "toolbarhelper.h"
#include "findandreplacewidget.h"
using namespace vnotex; using namespace vnotex;
@ -82,6 +82,10 @@ void MarkdownViewWindow::setupUI()
void MarkdownViewWindow::setMode(Mode p_mode) void MarkdownViewWindow::setMode(Mode p_mode)
{ {
setModeInternal(p_mode); setModeInternal(p_mode);
if (m_findAndReplace && m_findAndReplace->isVisible()) {
m_findAndReplace->setReplaceEnabled(m_mode != Mode::Read);
}
} }
void MarkdownViewWindow::setModeInternal(Mode p_mode) void MarkdownViewWindow::setModeInternal(Mode p_mode)
@ -278,6 +282,7 @@ void MarkdownViewWindow::setupToolBar()
toolBar->addSeparator(); toolBar->addSeparator();
ToolBarHelper::addSpacer(toolBar); ToolBarHelper::addSpacer(toolBar);
addAction(toolBar, ViewWindowToolBarHelper::FindAndReplace);
addAction(toolBar, ViewWindowToolBarHelper::Outline); addAction(toolBar, ViewWindowToolBarHelper::Outline);
} }
@ -418,6 +423,8 @@ void MarkdownViewWindow::setupViewer()
m_outlineProvider->setCurrentHeadingIndex(this->adapter()->getCurrentHeadingIndex()); m_outlineProvider->setCurrentHeadingIndex(this->adapter()->getCurrentHeadingIndex());
} }
}); });
connect(adapter, &MarkdownViewerAdapter::findTextReady,
this, &ViewWindow::showFindResult);
} }
void MarkdownViewWindow::syncTextEditorFromBuffer(bool p_syncPositionFromReadMode) void MarkdownViewWindow::syncTextEditorFromBuffer(bool p_syncPositionFromReadMode)
@ -792,3 +799,56 @@ void MarkdownViewWindow::zoom(bool p_zoomIn)
textEditorConfig.setZoomDelta(m_editor->zoomDelta()); textEditorConfig.setZoomDelta(m_editor->zoomDelta());
showZoomDelta(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);
}

View File

@ -49,6 +49,18 @@ namespace vnotex
void handleTypeAction(TypeAction p_action) Q_DECL_OVERRIDE; 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: protected:
void syncEditorFromBuffer() Q_DECL_OVERRIDE; void syncEditorFromBuffer() Q_DECL_OVERRIDE;

View File

@ -181,3 +181,28 @@ void TextViewWindow::zoom(bool p_zoomIn)
textEditorConfig.setZoomDelta(m_editor->zoomDelta()); textEditorConfig.setZoomDelta(m_editor->zoomDelta());
showZoomDelta(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);
}

View File

@ -33,6 +33,16 @@ namespace vnotex
void handleBufferChangedInternal() Q_DECL_OVERRIDE; 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: protected:
void syncEditorFromBuffer() Q_DECL_OVERRIDE; void syncEditorFromBuffer() Q_DECL_OVERRIDE;

View File

@ -127,6 +127,60 @@ namespace vnotex
return editorConfig; 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();
}
}; };
} }

View File

@ -756,9 +756,8 @@ void ViewArea::setupGlobalShortcuts()
// CloseTab. // CloseTab.
{ {
QKeySequence kseq(coreConfig.getShortcut(CoreConfig::CloseTab)); auto shortcut = WidgetUtils::createShortcut(coreConfig.getShortcut(CoreConfig::CloseTab), this);
if (!kseq.isEmpty()) { if (shortcut) {
auto shortcut = new QShortcut(kseq, this);
connect(shortcut, &QShortcut::activated, connect(shortcut, &QShortcut::activated,
this, [this]() { this, [this]() {
auto win = getCurrentViewWindow(); auto win = getCurrentViewWindow();
@ -771,9 +770,8 @@ void ViewArea::setupGlobalShortcuts()
// LocateNode. // LocateNode.
{ {
QKeySequence kseq(coreConfig.getShortcut(CoreConfig::LocateNode)); auto shortcut = WidgetUtils::createShortcut(coreConfig.getShortcut(CoreConfig::LocateNode), this);
if (!kseq.isEmpty()) { if (shortcut) {
auto shortcut = new QShortcut(kseq, this);
connect(shortcut, &QShortcut::activated, connect(shortcut, &QShortcut::activated,
this, [this]() { this, [this]() {
auto win = getCurrentViewWindow(); auto win = getCurrentViewWindow();

View File

@ -816,6 +816,29 @@ void ViewWindow::updateEditReadDiscardActionState(EditReadDiscardAction *p_act)
void ViewWindow::setupShortcuts() 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) void ViewWindow::wheelEvent(QWheelEvent *p_event)
@ -849,6 +872,23 @@ void ViewWindow::showFindAndReplaceWidget()
if (!m_findAndReplace) { if (!m_findAndReplace) {
m_findAndReplace = new FindAndReplaceWidget(this); m_findAndReplace = new FindAndReplaceWidget(this);
m_mainLayout->addWidget(m_findAndReplace); 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()); m_findAndReplace->open(QString());
@ -881,3 +921,85 @@ bool ViewWindow::findAndReplaceWidgetVisible() const
{ {
return m_findAndReplace && m_findAndReplace->isVisible(); 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));
}
}

View File

@ -77,6 +77,12 @@ namespace vnotex
public slots: public slots:
virtual void handleEditorConfigChange() = 0; 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: signals:
// Emit when the attached buffer is changed. // Emit when the attached buffer is changed.
void bufferChanged(); void bufferChanged();
@ -130,6 +136,18 @@ namespace vnotex
// Handle all kinds of type action. // Handle all kinds of type action.
virtual void handleTypeAction(TypeAction p_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: protected:
void setCentralWidget(QWidget *p_widget); void setCentralWidget(QWidget *p_widget);
@ -183,6 +201,11 @@ namespace vnotex
bool findAndReplaceWidgetVisible() const; 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); static ViewWindow::Mode modeFromOpenParameters(const FileOpenParameters &p_paras);
QSharedPointer<QWidget> m_statusWidget; QSharedPointer<QWidget> m_statusWidget;
@ -196,7 +219,16 @@ namespace vnotex
Mode m_mode = Mode::Invalid; Mode m_mode = Mode::Invalid;
// Managed by QObject.
FindAndReplaceWidget *m_findAndReplace = nullptr;
private: private:
struct FindInfo
{
QString m_text;
FindOptions m_options;
};
void setupUI(); void setupUI();
void initIcons(); void initIcons();
@ -236,6 +268,8 @@ namespace vnotex
}; };
int checkFileMissingOrChangedOutside(); int checkFileMissingOrChangedOutside();
void findNextOnLastFind(bool p_forward = true);
static ViewWindow::TypeAction toolBarActionToTypeAction(ViewWindowToolBarHelper::Action p_action); static ViewWindow::TypeAction toolBarActionToTypeAction(ViewWindowToolBarHelper::Action p_action);
Buffer *m_buffer = nullptr; Buffer *m_buffer = nullptr;
@ -269,8 +303,8 @@ namespace vnotex
// Whether check file missing or changed outside. // Whether check file missing or changed outside.
bool m_fileChangeCheckEnabled = true; bool m_fileChangeCheckEnabled = true;
// Managed by QObject. // Last find info.
FindAndReplaceWidget *m_findAndReplace = nullptr; FindInfo m_findInfo;
static QIcon s_savedIcon; static QIcon s_savedIcon;
static QIcon s_modifiedIcon; static QIcon s_modifiedIcon;