diff --git a/src/dialog/vfindreplacedialog.cpp b/src/dialog/vfindreplacedialog.cpp index 30781c36..937736bd 100644 --- a/src/dialog/vfindreplacedialog.cpp +++ b/src/dialog/vfindreplacedialog.cpp @@ -2,7 +2,7 @@ #include VFindReplaceDialog::VFindReplaceDialog(QWidget *p_parent) - : QWidget(p_parent) + : QWidget(p_parent), m_options(0), m_replaceAvailable(true) { setupUI(); } @@ -24,25 +24,42 @@ void VFindReplaceDialog::setupUI() // Find QLabel *findLabel = new QLabel(tr("&Find:")); m_findEdit = new QLineEdit(); + m_findEdit->setPlaceholderText(tr("Enter text to search")); findLabel->setBuddy(m_findEdit); - m_findNextBtn = new QPushButton(tr("Find Next")); + m_findNextBtn = new QPushButton(tr("Find &Next")); m_findNextBtn->setProperty("FlatBtn", true); m_findNextBtn->setDefault(true); - m_findPrevBtn = new QPushButton(tr("Find Previous")); + m_findPrevBtn = new QPushButton(tr("Find &Previous")); m_findPrevBtn->setProperty("FlatBtn", true); // Replace QLabel *replaceLabel = new QLabel(tr("&Replace with:")); m_replaceEdit = new QLineEdit(); + m_replaceEdit->setPlaceholderText(tr("Enter text to replace with")); replaceLabel->setBuddy(m_replaceEdit); - m_replaceBtn = new QPushButton(tr("Replace")); + m_replaceBtn = new QPushButton(tr("R&eplace")); m_replaceBtn->setProperty("FlatBtn", true); - m_replaceFindBtn = new QPushButton(tr("Replace && Find")); + m_replaceFindBtn = new QPushButton(tr("Replace && Fin&d")); m_replaceFindBtn->setProperty("FlatBtn", true); - m_replaceAllBtn = new QPushButton(tr("Replace All")); + m_replaceAllBtn = new QPushButton(tr("Replace A&ll")); m_replaceAllBtn->setProperty("FlatBtn", true); - m_advancedBtn = new QPushButton(tr("Advanced")); + m_advancedBtn = new QPushButton(tr("&Advanced >>")); m_advancedBtn->setProperty("FlatBtn", true); + m_advancedBtn->setCheckable(true); + + // Options + m_caseSensitiveCheck = new QCheckBox(tr("&Case sensitive"), this); + connect(m_caseSensitiveCheck, &QCheckBox::stateChanged, + this, &VFindReplaceDialog::optionBoxToggled); + m_wholeWordOnlyCheck = new QCheckBox(tr("&Whole word only"), this); + connect(m_wholeWordOnlyCheck, &QCheckBox::stateChanged, + this, &VFindReplaceDialog::optionBoxToggled); + m_regularExpressionCheck = new QCheckBox(tr("Re&gular expression"), this); + connect(m_regularExpressionCheck, &QCheckBox::stateChanged, + this, &VFindReplaceDialog::optionBoxToggled); + m_incrementalSearchCheck = new QCheckBox(tr("&Incremental search"), this); + connect(m_incrementalSearchCheck, &QCheckBox::stateChanged, + this, &VFindReplaceDialog::optionBoxToggled); QGridLayout *gridLayout = new QGridLayout(); gridLayout->addWidget(findLabel, 0, 0); @@ -55,6 +72,10 @@ void VFindReplaceDialog::setupUI() gridLayout->addWidget(m_replaceFindBtn, 1, 3); gridLayout->addWidget(m_replaceAllBtn, 1, 4); gridLayout->addWidget(m_advancedBtn, 1, 5); + gridLayout->addWidget(m_caseSensitiveCheck, 2, 1); + gridLayout->addWidget(m_wholeWordOnlyCheck, 2, 2); + gridLayout->addWidget(m_regularExpressionCheck, 3, 1); + gridLayout->addWidget(m_incrementalSearchCheck, 3, 2); gridLayout->setColumnStretch(0, 0); gridLayout->setColumnStretch(1, 4); gridLayout->setColumnStretch(2, 1); @@ -62,6 +83,7 @@ void VFindReplaceDialog::setupUI() gridLayout->setColumnStretch(4, 1); gridLayout->setColumnStretch(5, 1); gridLayout->setColumnStretch(6, 3); + QMargins margin = gridLayout->contentsMargins(); margin.setLeft(3); gridLayout->setContentsMargins(margin); @@ -73,31 +95,195 @@ void VFindReplaceDialog::setupUI() setLayout(mainLayout); + m_caseSensitiveCheck->hide(); + m_wholeWordOnlyCheck->hide(); + m_regularExpressionCheck->hide(); + m_incrementalSearchCheck->hide(); + // Signals connect(m_closeBtn, &QPushButton::clicked, this, &VFindReplaceDialog::closeDialog); + connect(m_findEdit, &QLineEdit::textChanged, + this, &VFindReplaceDialog::handleFindTextChanged); + connect(m_advancedBtn, &QPushButton::toggled, + this, &VFindReplaceDialog::advancedBtnToggled); + connect(m_findNextBtn, SIGNAL(clicked(bool)), + this, SLOT(findNext())); + connect(m_findPrevBtn, SIGNAL(clicked(bool)), + this, SLOT(findPrevious())); + connect(m_replaceBtn, SIGNAL(clicked(bool)), + this, SLOT(replace())); + connect(m_replaceFindBtn, SIGNAL(clicked(bool)), + this, SLOT(replaceFind())); + connect(m_replaceAllBtn, SIGNAL(clicked(bool)), + this, SLOT(replaceAll())); } void VFindReplaceDialog::closeDialog() { if (this->isVisible()) { - this->hide(); + hide(); emit dialogClosed(); } } -void VFindReplaceDialog::showEvent(QShowEvent *event) -{ - m_findEdit->selectAll(); - QWidget::showEvent(event); -} - void VFindReplaceDialog::keyPressEvent(QKeyEvent *event) { - if (event->key() == Qt::Key_Escape) { + switch (event->key()) { + case Qt::Key_Escape: event->accept(); closeDialog(); return; + + case Qt::Key_Return: + { + int modifiers = event->modifiers(); + bool shift = false; + if (modifiers == Qt::ShiftModifier) { + shift = true; + } else if (modifiers != Qt::NoModifier) { + break; + } + if (!m_findEdit->hasFocus() && !m_replaceEdit->hasFocus()) { + break; + } + event->accept(); + if (shift) { + findPrevious(); + } else { + findNext(); + } + return; + } + + default: + break; } QWidget::keyPressEvent(event); } + +void VFindReplaceDialog::openDialog(QString p_text) +{ + show(); + if (!p_text.isEmpty()) { + m_findEdit->setText(p_text); + } + m_findEdit->setFocus(); + m_findEdit->selectAll(); +} + +void VFindReplaceDialog::handleFindTextChanged(const QString &p_text) +{ + emit findTextChanged(p_text, m_options); +} + +void VFindReplaceDialog::advancedBtnToggled(bool p_checked) +{ + if (p_checked) { + m_advancedBtn->setText("B&asic <<"); + } else { + m_advancedBtn->setText("&Advanced <<"); + } + + m_caseSensitiveCheck->setVisible(p_checked); + m_wholeWordOnlyCheck->setVisible(p_checked); + m_regularExpressionCheck->setVisible(p_checked); + m_incrementalSearchCheck->setVisible(p_checked); +} + +void VFindReplaceDialog::optionBoxToggled(int p_state) +{ + QObject *obj = sender(); + FindOption opt = FindOption::CaseSensitive; + if (obj == m_caseSensitiveCheck) { + opt = FindOption::CaseSensitive; + } else if (obj == m_wholeWordOnlyCheck) { + opt = FindOption::WholeWordOnly; + } else if (obj == m_regularExpressionCheck) { + opt = FindOption::RegularExpression; + } else { + opt = FindOption::IncrementalSearch; + } + + if (p_state) { + m_options |= opt; + } else { + m_options &= ~opt; + } + emit findOptionChanged(m_options); +} + +void VFindReplaceDialog::setOption(FindOption p_opt, bool p_enabled) +{ + if (p_opt == FindOption::CaseSensitive) { + m_caseSensitiveCheck->setChecked(p_enabled); + } else if (p_opt == FindOption::WholeWordOnly) { + m_wholeWordOnlyCheck->setChecked(p_enabled); + } else if (p_opt == FindOption::RegularExpression) { + m_regularExpressionCheck->setChecked(p_enabled); + } else if (p_opt == FindOption::IncrementalSearch) { + m_incrementalSearchCheck->setChecked(p_enabled); + } else { + Q_ASSERT(false); + } +} + +void VFindReplaceDialog::findNext() +{ + QString text = m_findEdit->text(); + if (text.isEmpty()) { + return; + } + emit findNext(text, m_options, true); +} + +void VFindReplaceDialog::findPrevious() +{ + QString text = m_findEdit->text(); + if (text.isEmpty()) { + return; + } + emit findNext(text, m_options, false); +} + +void VFindReplaceDialog::replace() +{ + QString text = m_findEdit->text(); + if (text.isEmpty() || !m_replaceAvailable) { + return; + } + QString replaceText = m_replaceEdit->text(); + emit replace(text, m_options, replaceText, false); +} + +void VFindReplaceDialog::replaceFind() +{ + QString text = m_findEdit->text(); + if (text.isEmpty() || !m_replaceAvailable) { + return; + } + QString replaceText = m_replaceEdit->text(); + emit replace(text, m_options, replaceText, true); +} + +void VFindReplaceDialog::replaceAll() +{ + QString text = m_findEdit->text(); + if (text.isEmpty() || !m_replaceAvailable) { + return; + } + QString replaceText = m_replaceEdit->text(); + emit replaceAll(text, m_options, replaceText); +} + +void VFindReplaceDialog::updateState(DocType p_docType, bool p_editMode) +{ + if (p_editMode || p_docType == DocType::Html) { + m_wholeWordOnlyCheck->setEnabled(true); + m_regularExpressionCheck->setEnabled(true); + } else { + m_wholeWordOnlyCheck->setEnabled(false); + m_regularExpressionCheck->setEnabled(false); + } + m_replaceAvailable = p_editMode; +} diff --git a/src/dialog/vfindreplacedialog.h b/src/dialog/vfindreplacedialog.h index ff8fc91d..d4ca12ac 100644 --- a/src/dialog/vfindreplacedialog.h +++ b/src/dialog/vfindreplacedialog.h @@ -2,28 +2,64 @@ #define VFINDREPLACEDIALOG_H #include +#include +#include "vconstants.h" class QLineEdit; class QPushButton; +class QCheckBox; + +enum FindOption +{ + CaseSensitive = 0x1U, + WholeWordOnly = 0x2U, + RegularExpression = 0x4U, + IncrementalSearch = 0x8U +}; class VFindReplaceDialog : public QWidget { Q_OBJECT public: explicit VFindReplaceDialog(QWidget *p_parent = 0); + void setOption(FindOption p_opt, bool p_enabled); + // Update the options enabled/disabled state according to current + // edit tab. + void updateState(DocType p_docType, bool p_editMode); signals: void dialogClosed(); + void findTextChanged(const QString &p_text, uint p_options); + void findOptionChanged(uint p_options); + void findNext(const QString &p_text, uint p_options, bool p_forward); + void replace(const QString &p_text, uint p_options, + const QString &p_replaceText, bool p_findNext); + void replaceAll(const QString &p_text, uint p_options, + const QString &p_replaceText); protected: - void showEvent(QShowEvent *event) Q_DECL_OVERRIDE; void keyPressEvent(QKeyEvent *event) Q_DECL_OVERRIDE; public slots: void closeDialog(); + void openDialog(QString p_text = ""); + void findNext(); + void findPrevious(); + void replace(); + void replaceFind(); + void replaceAll(); + +private slots: + void handleFindTextChanged(const QString &p_text); + void advancedBtnToggled(bool p_checked); + void optionBoxToggled(int p_state); private: void setupUI(); + // Bit OR of FindOption + uint m_options; + bool m_replaceAvailable; + QLineEdit *m_findEdit; QLineEdit *m_replaceEdit; QPushButton *m_findNextBtn; @@ -33,6 +69,10 @@ private: QPushButton *m_replaceAllBtn; QPushButton *m_advancedBtn; QPushButton *m_closeBtn; + QCheckBox *m_caseSensitiveCheck; + QCheckBox *m_wholeWordOnlyCheck; + QCheckBox *m_regularExpressionCheck; + QCheckBox *m_incrementalSearchCheck; }; #endif // VFINDREPLACEDIALOG_H diff --git a/src/resources/icons/find_replace.svg b/src/resources/icons/find_replace.svg new file mode 100644 index 00000000..9c51b980 --- /dev/null +++ b/src/resources/icons/find_replace.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/vconfigmanager.cpp b/src/vconfigmanager.cpp index e6a5e7c9..1c486a2d 100644 --- a/src/vconfigmanager.cpp +++ b/src/vconfigmanager.cpp @@ -68,6 +68,15 @@ void VConfigManager::initialize() m_mainSplitterState = getConfigFromSettings("session", "main_splitter_state").toByteArray(); updateMarkdownEditStyle(); + + m_findCaseSensitive = getConfigFromSettings("global", + "find_case_sensitive").toBool(); + m_findWholeWordOnly = getConfigFromSettings("global", + "find_whole_word_only").toBool(); + m_findRegularExpression = getConfigFromSettings("global", + "find_regular_expression").toBool(); + m_findIncrementalSearch = getConfigFromSettings("global", + "find_incremental_search").toBool(); } void VConfigManager::readPredefinedColorsFromSettings() diff --git a/src/vconfigmanager.h b/src/vconfigmanager.h index 225edbc8..3e355dbc 100644 --- a/src/vconfigmanager.h +++ b/src/vconfigmanager.h @@ -99,6 +99,18 @@ public: inline const QByteArray &getMainSplitterState() const; inline void setMainSplitterState(const QByteArray &p_state); + inline bool getFindCaseSensitive() const; + inline void setFindCaseSensitive(bool p_enabled); + + inline bool getFindWholeWordOnly() const; + inline void setFindWholeWordOnly(bool p_enabled); + + inline bool getFindRegularExpression() const; + inline void setFindRegularExpression(bool p_enabled); + + inline bool getFindIncrementalSearch() const; + inline void setFindIncrementalSearch(bool p_enabled); + private: void updateMarkdownEditStyle(); QVariant getConfigFromSettings(const QString §ion, const QString &key); @@ -144,6 +156,12 @@ private: QByteArray m_mainWindowState; QByteArray m_mainSplitterState; + // Find/Replace dialog options + bool m_findCaseSensitive; + bool m_findWholeWordOnly; + bool m_findRegularExpression; + bool m_findIncrementalSearch; + // The name of the config file in each directory static const QString dirConfigFileName; // The name of the default configuration file @@ -373,4 +391,62 @@ inline void VConfigManager::setMainSplitterState(const QByteArray &p_state) setConfigToSettings("session", "main_splitter_state", m_mainSplitterState); } +inline bool VConfigManager::getFindCaseSensitive() const +{ + return m_findCaseSensitive; +} + +inline void VConfigManager::setFindCaseSensitive(bool p_enabled) +{ + if (m_findCaseSensitive == p_enabled) { + return; + } + m_findCaseSensitive = p_enabled; + setConfigToSettings("global", "find_case_sensitive", m_findCaseSensitive); +} + +inline bool VConfigManager::getFindWholeWordOnly() const +{ + return m_findWholeWordOnly; +} + +inline void VConfigManager::setFindWholeWordOnly(bool p_enabled) +{ + if (m_findWholeWordOnly == p_enabled) { + return; + } + m_findWholeWordOnly = p_enabled; + setConfigToSettings("global", "find_whole_word_only", m_findWholeWordOnly); +} + +inline bool VConfigManager::getFindRegularExpression() const +{ + return m_findRegularExpression; +} + +inline void VConfigManager::setFindRegularExpression(bool p_enabled) +{ + if (m_findRegularExpression == p_enabled) { + return; + } + m_findRegularExpression = p_enabled; + setConfigToSettings("global", "find_regular_expression", + m_findRegularExpression); +} + +inline bool VConfigManager::getFindIncrementalSearch() const +{ + return m_findIncrementalSearch; +} + +inline void VConfigManager::setFindIncrementalSearch(bool p_enabled) +{ + if (m_findIncrementalSearch == p_enabled) { + return; + } + m_findIncrementalSearch = p_enabled; + setConfigToSettings("global", "find_incremental_search", + m_findIncrementalSearch); +} + #endif // VCONFIGMANAGER_H diff --git a/src/vedit.cpp b/src/vedit.cpp index c78421cd..61811b56 100644 --- a/src/vedit.cpp +++ b/src/vedit.cpp @@ -1,11 +1,13 @@ #include #include +#include #include "vedit.h" #include "vnote.h" #include "vconfigmanager.h" #include "vtoc.h" #include "utils/vutils.h" #include "veditoperations.h" +#include "dialog/vfindreplacedialog.h" extern VConfigManager vconfig; @@ -82,3 +84,165 @@ void VEdit::insertImage() m_editOps->insertImage(); } } + +bool VEdit::findText(const QString &p_text, uint p_options, bool p_peek, + bool p_forward) +{ + static int startPos = textCursor().selectionStart(); + static int lastPos = startPos; + bool found = false; + + if (p_text.isEmpty() && p_peek) { + // Clear previous selection + QTextCursor cursor = textCursor(); + cursor.clearSelection(); + cursor.setPosition(startPos); + setTextCursor(cursor); + } else { + QTextCursor cursor = textCursor(); + if (p_peek) { + int curPos = cursor.selectionStart(); + if (curPos != lastPos) { + // Cursor has been moved. Just start at current position. + startPos = curPos; + lastPos = curPos; + } else { + // Move cursor to startPos to search. + cursor.setPosition(startPos); + setTextCursor(cursor); + } + } + + // Options + QTextDocument::FindFlags flags; + if (p_options & FindOption::CaseSensitive) { + flags |= QTextDocument::FindCaseSensitively; + } + if (p_options & FindOption::WholeWordOnly) { + flags |= QTextDocument::FindWholeWords; + } + if (!p_forward) { + flags |= QTextDocument::FindBackward; + } + // Use regular expression + if (p_options & FindOption::RegularExpression) { + QRegExp exp(p_text, + p_options & FindOption::CaseSensitive ? + Qt::CaseSensitive : Qt::CaseInsensitive); + found = find(exp, flags); + } else { + found = find(p_text, flags); + } + cursor = textCursor(); + if (!p_peek) { + startPos = cursor.selectionStart(); + } + lastPos = cursor.selectionStart(); + } + return found; +} + +void VEdit::replaceText(const QString &p_text, uint p_options, + const QString &p_replaceText, bool p_findNext) +{ + // Options + QTextDocument::FindFlags flags; + if (p_options & FindOption::CaseSensitive) { + flags |= QTextDocument::FindCaseSensitively; + } + if (p_options & FindOption::WholeWordOnly) { + flags |= QTextDocument::FindWholeWords; + } + + bool useRegExp = false; + QRegExp exp; + if (p_options & FindOption::RegularExpression) { + useRegExp = true; + exp = QRegExp(p_text, + p_options & FindOption::CaseSensitive ? + Qt::CaseSensitive : Qt::CaseInsensitive); + } + + QTextCursor cursor = textCursor(); + if (cursor.hasSelection()) { + // Replace occurs only if the selected text matches @p_text with @p_options. + QTextCursor matchCursor; + if (useRegExp) { + matchCursor = document()->find(exp, cursor.selectionStart(), + flags); + } else { + matchCursor = document()->find(p_text, cursor.selectionStart(), + flags); + } + bool matched = (cursor.selectionStart() == matchCursor.selectionStart()) + && (cursor.selectionEnd() == matchCursor.selectionEnd()); + if (matched) { + cursor.beginEditBlock(); + cursor.removeSelectedText(); + cursor.insertText(p_replaceText); + cursor.endEditBlock(); + setTextCursor(cursor); + } + } + if (p_findNext) { + findText(p_text, p_options, false, true); + } +} + +void VEdit::replaceTextAll(const QString &p_text, uint p_options, + const QString &p_replaceText) +{ + // Options + QTextDocument::FindFlags flags; + if (p_options & FindOption::CaseSensitive) { + flags |= QTextDocument::FindCaseSensitively; + } + if (p_options & FindOption::WholeWordOnly) { + flags |= QTextDocument::FindWholeWords; + } + + bool useRegExp = false; + QRegExp exp; + if (p_options & FindOption::RegularExpression) { + useRegExp = true; + exp = QRegExp(p_text, + p_options & FindOption::CaseSensitive ? + Qt::CaseSensitive : Qt::CaseInsensitive); + } + + QTextCursor cursor = textCursor(); + int pos = cursor.position(); + int nrReplaces = 0; + int startPos = 0; + int lastMatch = -1; + while (true) { + QTextCursor matchCursor; + if (useRegExp) { + matchCursor = document()->find(exp, startPos, flags); + } else { + matchCursor = document()->find(p_text, startPos, flags); + } + if (matchCursor.isNull()) { + break; + } else { + if (matchCursor.selectionStart() <= lastMatch) { + // Wrap back. + break; + } else { + lastMatch = matchCursor.selectionStart(); + } + nrReplaces++; + matchCursor.beginEditBlock(); + matchCursor.removeSelectedText(); + matchCursor.insertText(p_replaceText); + matchCursor.endEditBlock(); + setTextCursor(matchCursor); + startPos = matchCursor.position(); + } + } + // Restore cursor position. + cursor.setPosition(pos); + setTextCursor(cursor); + qDebug() << "replace all" << nrReplaces << "occurencs"; +} + diff --git a/src/vedit.h b/src/vedit.h index 3f5a7ff7..18708360 100644 --- a/src/vedit.h +++ b/src/vedit.h @@ -26,6 +26,12 @@ public: virtual void scrollToLine(int p_lineNumber); // User requests to insert an image. virtual void insertImage(); + virtual bool findText(const QString &p_text, uint p_options, bool p_peek, + bool p_forward); + virtual void replaceText(const QString &p_text, uint p_options, + const QString &p_replaceText, bool p_findNext); + virtual void replaceTextAll(const QString &p_text, uint p_options, + const QString &p_replaceText); protected: QPointer m_file; diff --git a/src/veditarea.cpp b/src/veditarea.cpp index e6bb6d23..695028b7 100644 --- a/src/veditarea.cpp +++ b/src/veditarea.cpp @@ -7,6 +7,8 @@ #include "vfile.h" #include "dialog/vfindreplacedialog.h" +extern VConfigManager vconfig; + VEditArea::VEditArea(VNote *vnote, QWidget *parent) : QWidget(parent), vnote(vnote), curWindowIndex(-1) { @@ -20,6 +22,14 @@ void VEditArea::setupUI() { splitter = new QSplitter(this); m_findReplace = new VFindReplaceDialog(this); + m_findReplace->setOption(FindOption::CaseSensitive, + vconfig.getFindCaseSensitive()); + m_findReplace->setOption(FindOption::WholeWordOnly, + vconfig.getFindWholeWordOnly()); + m_findReplace->setOption(FindOption::RegularExpression, + vconfig.getFindRegularExpression()); + m_findReplace->setOption(FindOption::IncrementalSearch, + vconfig.getFindIncrementalSearch()); QVBoxLayout *mainLayout = new QVBoxLayout(); mainLayout->addWidget(splitter); @@ -31,6 +41,22 @@ void VEditArea::setupUI() setLayout(mainLayout); + connect(m_findReplace, &VFindReplaceDialog::findTextChanged, + this, &VEditArea::handleFindTextChanged); + connect(m_findReplace, &VFindReplaceDialog::findOptionChanged, + this, &VEditArea::handleFindOptionChanged); + connect(m_findReplace, SIGNAL(findNext(const QString &, uint, bool)), + this, SLOT(handleFindNext(const QString &, uint, bool))); + connect(m_findReplace, + SIGNAL(replace(const QString &, uint, const QString &, bool)), + this, + SLOT(handleReplace(const QString &, uint, const QString &, bool))); + connect(m_findReplace, + SIGNAL(replaceAll(const QString &, uint, const QString &)), + this, + SLOT(handleReplaceAll(const QString &, uint, const QString &))); + connect(m_findReplace, &VFindReplaceDialog::dialogClosed, + this, &VEditArea::handleFindDialogClosed); m_findReplace->hide(); } @@ -422,3 +448,80 @@ void VEditArea::moveTab(QWidget *p_widget, int p_fromIdx, int p_toIdx) delete p_widget; } } + +// Only propogate the search in the IncrementalSearch case. +void VEditArea::handleFindTextChanged(const QString &p_text, uint p_options) +{ + VEditTab *tab = currentEditTab(); + if (tab) { + if (p_options & FindOption::IncrementalSearch) { + tab->findText(p_text, p_options, true); + } + } +} + +void VEditArea::handleFindOptionChanged(uint p_options) +{ + qDebug() << "find option changed" << p_options; + vconfig.setFindCaseSensitive(p_options & FindOption::CaseSensitive); + vconfig.setFindWholeWordOnly(p_options & FindOption::WholeWordOnly); + vconfig.setFindRegularExpression(p_options & FindOption::RegularExpression); + vconfig.setFindIncrementalSearch(p_options & FindOption::IncrementalSearch); +} + +void VEditArea::handleFindNext(const QString &p_text, uint p_options, + bool p_forward) +{ + qDebug() << "find next" << p_text << p_options << p_forward; + VEditTab *tab = currentEditTab(); + if (tab) { + tab->findText(p_text, p_options, false, p_forward); + } +} + +void VEditArea::handleReplace(const QString &p_text, uint p_options, + const QString &p_replaceText, bool p_findNext) +{ + qDebug() << "replace" << p_text << p_options << "with" << p_replaceText + << p_findNext; + VEditTab *tab = currentEditTab(); + if (tab) { + tab->replaceText(p_text, p_options, p_replaceText, p_findNext); + } +} + +void VEditArea::handleReplaceAll(const QString &p_text, uint p_options, + const QString &p_replaceText) +{ + qDebug() << "replace all" << p_text << p_options << "with" << p_replaceText; + VEditTab *tab = currentEditTab(); + if (tab) { + tab->replaceTextAll(p_text, p_options, p_replaceText); + } +} + +// Let VEditArea get focus after VFindReplaceDialog is closed. +void VEditArea::handleFindDialogClosed() +{ + if (curWindowIndex == -1) { + setFocus(); + } else { + getWindow(curWindowIndex)->focusWindow(); + } + + // Clear all the selection in Web view. + int nrWin = splitter->count(); + for (int i = 0; i < nrWin; ++i) { + getWindow(i)->clearFindSelectionInWebView(); + } +} + +QString VEditArea::getSelectedText() +{ + VEditTab *tab = currentEditTab(); + if (tab) { + return tab->getSelectedText(); + } else { + return QString(); + } +} diff --git a/src/veditarea.h b/src/veditarea.h index 263942ef..535224ed 100644 --- a/src/veditarea.h +++ b/src/veditarea.h @@ -40,6 +40,8 @@ public: // If fail, just delete the p_widget. void moveTab(QWidget *p_widget, int p_fromIdx, int p_toIdx); inline VFindReplaceDialog *getFindReplaceDialog() const; + // Return selected text of current edit tab. + QString getSelectedText(); signals: void curTabStatusChanged(const VFile *p_file, const VEditTab *p_editTab, bool p_editMode); @@ -66,6 +68,14 @@ private slots: void handleWindowFocused(); void handleOutlineChanged(const VToc &toc); void handleCurHeaderChanged(const VAnchor &anchor); + void handleFindTextChanged(const QString &p_text, uint p_options); + void handleFindOptionChanged(uint p_options); + void handleFindNext(const QString &p_text, uint p_options, bool p_forward); + void handleReplace(const QString &p_text, uint p_options, + const QString &p_replaceText, bool p_findNext); + void handleReplaceAll(const QString &p_text, uint p_options, + const QString &p_replaceText); + void handleFindDialogClosed(); private: void setupUI(); diff --git a/src/vedittab.cpp b/src/vedittab.cpp index eaae2321..c899f5ca 100644 --- a/src/vedittab.cpp +++ b/src/vedittab.cpp @@ -15,6 +15,7 @@ #include "vnotebook.h" #include "vtoc.h" #include "vmdedit.h" +#include "dialog/vfindreplacedialog.h" extern VConfigManager vconfig; @@ -107,6 +108,7 @@ void VEditTab::showFileReadMode() previewByConverter(); } setCurrentWidget(webPreviewer); + clearFindSelectionInWebView(); scrollPreviewToHeader(outlineIndex); break; default: @@ -276,6 +278,7 @@ void VEditTab::setupMarkdownPreview() void VEditTab::focusTab() { currentWidget()->setFocus(); + emit getFocused(); } void VEditTab::handleFocusChanged(QWidget * /* old */, QWidget *now) @@ -455,3 +458,60 @@ void VEditTab::insertImage() } m_textEditor->insertImage(); } + +void VEditTab::findText(const QString &p_text, uint p_options, bool p_peek, + bool p_forward) +{ + if (isEditMode || !webPreviewer) { + m_textEditor->findText(p_text, p_options, p_peek, p_forward); + } else { + findTextInWebView(p_text, p_options, p_peek, p_forward); + } +} + +void VEditTab::replaceText(const QString &p_text, uint p_options, + const QString &p_replaceText, bool p_findNext) +{ + if (isEditMode) { + m_textEditor->replaceText(p_text, p_options, p_replaceText, p_findNext); + } +} + +void VEditTab::replaceTextAll(const QString &p_text, uint p_options, + const QString &p_replaceText) +{ + if (isEditMode) { + m_textEditor->replaceTextAll(p_text, p_options, p_replaceText); + } +} + +void VEditTab::findTextInWebView(const QString &p_text, uint p_options, + bool p_peek, bool p_forward) +{ + Q_ASSERT(webPreviewer); + QWebEnginePage::FindFlags flags; + if (p_options & FindOption::CaseSensitive) { + flags |= QWebEnginePage::FindCaseSensitively; + } + if (!p_forward) { + flags |= QWebEnginePage::FindBackward; + } + webPreviewer->findText(p_text, flags); +} + +QString VEditTab::getSelectedText() const +{ + if (isEditMode || !webPreviewer) { + QTextCursor cursor = m_textEditor->textCursor(); + return cursor.selectedText(); + } else { + return webPreviewer->selectedText(); + } +} + +void VEditTab::clearFindSelectionInWebView() +{ + if (webPreviewer) { + webPreviewer->findText(""); + } +} diff --git a/src/vedittab.h b/src/vedittab.h index c7eb6e63..e1703fd3 100644 --- a/src/vedittab.h +++ b/src/vedittab.h @@ -38,6 +38,16 @@ public: void scrollToAnchor(const VAnchor& anchor); inline VFile *getFile(); void insertImage(); + // Search @p_text in current note. + void findText(const QString &p_text, uint p_options, bool p_peek, + bool p_forward = true); + // Replace @p_text with @p_replaceText in current note. + void replaceText(const QString &p_text, uint p_options, + const QString &p_replaceText, bool p_findNext); + void replaceTextAll(const QString &p_text, uint p_options, + const QString &p_replaceText); + QString getSelectedText() const; + void clearFindSelectionInWebView(); signals: void getFocused(); @@ -65,6 +75,8 @@ private: void parseTocUl(QXmlStreamReader &xml, QVector &headers, int level); void parseTocLi(QXmlStreamReader &xml, QVector &headers, int level); void scrollPreviewToHeader(int p_outlineIndex); + void findTextInWebView(const QString &p_text, uint p_options, bool p_peek, + bool p_forward); QPointer m_file; bool isEditMode; diff --git a/src/veditwindow.cpp b/src/veditwindow.cpp index a43ec819..b9ec677c 100644 --- a/src/veditwindow.cpp +++ b/src/veditwindow.cpp @@ -362,6 +362,7 @@ void VEditWindow::focusWindow() int idx = currentIndex(); if (idx == -1) { setFocus(); + emit getFocused(); return; } getTab(idx)->focusTab(); @@ -375,7 +376,7 @@ void VEditWindow::handleTabbarClicked(int /* index */) void VEditWindow::mousePressEvent(QMouseEvent *event) { - emit getFocused(); + focusWindow(); QTabWidget::mousePressEvent(event); } @@ -705,3 +706,11 @@ void VEditWindow::setCurrentWindow(bool p_current) leftBtn->setIcon(QIcon(":/resources/icons/corner_tablist.svg")); } } + +void VEditWindow::clearFindSelectionInWebView() +{ + int nrTab = count(); + for (int i = 0; i < nrTab; ++i) { + getTab(i)->clearFindSelectionInWebView(); + } +} diff --git a/src/veditwindow.h b/src/veditwindow.h index b4d07081..8c4a45f5 100644 --- a/src/veditwindow.h +++ b/src/veditwindow.h @@ -45,6 +45,7 @@ public: bool addEditTab(QWidget *p_widget); // Set whether it is the current window. void setCurrentWindow(bool p_current); + void clearFindSelectionInWebView(); protected: void mousePressEvent(QMouseEvent *event) Q_DECL_OVERRIDE; diff --git a/src/vmainwindow.cpp b/src/vmainwindow.cpp index edb4d921..5b763ebb 100644 --- a/src/vmainwindow.cpp +++ b/src/vmainwindow.cpp @@ -48,6 +48,7 @@ void VMainWindow::setupUI() editArea = new VEditArea(vnote); editArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + m_findReplaceDialog = editArea->getFindReplaceDialog(); fileList->setEditArea(editArea); directoryTree->setEditArea(editArea); notebookSelector->setEditArea(editArea); @@ -79,10 +80,12 @@ void VMainWindow::setupUI() editArea, &VEditArea::handleFileUpdated); connect(editArea, &VEditArea::curTabStatusChanged, this, &VMainWindow::handleCurTabStatusChanged); + connect(m_findReplaceDialog, &VFindReplaceDialog::findTextChanged, + this, &VMainWindow::handleFindDialogTextChanged); setCentralWidget(mainSplitter); - // TODO: Create and show the status bar - // statusBar(); + // Create and show the status bar + statusBar(); } QWidget *VMainWindow::setupDirectoryPanel() @@ -179,7 +182,7 @@ void VMainWindow::initFileToolBar() newNoteAct = new QAction(QIcon(":/resources/icons/create_note_tb.svg"), tr("New &Note"), this); newNoteAct->setStatusTip(tr("Create a note in current directory")); - newNoteAct->setShortcut(QKeySequence("Ctrl+N")); + newNoteAct->setShortcut(QKeySequence::New); connect(newNoteAct, &QAction::triggered, fileList, &VFileList::newFile); @@ -325,13 +328,49 @@ void VMainWindow::initEditMenu() { QMenu *editMenu = menuBar()->addMenu(tr("&Edit")); - // Inser image. - QAction *insertImageAct = new QAction(QIcon(":/resources/icons/insert_image.svg"), - tr("Insert &Image"), this); - insertImageAct->setStatusTip(tr("Insert an image from file in current note")); - connect(insertImageAct, &QAction::triggered, + // Insert image. + m_insertImageAct = new QAction(QIcon(":/resources/icons/insert_image.svg"), + tr("Insert &Image"), this); + m_insertImageAct->setStatusTip(tr("Insert an image from file in current note")); + connect(m_insertImageAct, &QAction::triggered, this, &VMainWindow::insertImage); + // Find/Replace. + m_findReplaceAct = new QAction(QIcon(":/resources/icons/find_replace.svg"), + tr("Find/Replace"), this); + m_findReplaceAct->setStatusTip(tr("Open Find/Replace dialog to search in current note")); + m_findReplaceAct->setShortcut(QKeySequence::Find); + connect(m_findReplaceAct, &QAction::triggered, + this, &VMainWindow::openFindDialog); + + m_findNextAct = new QAction(tr("Find Next"), this); + m_findNextAct->setStatusTip(tr("Find next occurence")); + m_findNextAct->setShortcut(QKeySequence::FindNext); + connect(m_findNextAct, SIGNAL(triggered(bool)), + m_findReplaceDialog, SLOT(findNext())); + + m_findPreviousAct = new QAction(tr("Find Previous"), this); + m_findPreviousAct->setStatusTip(tr("Find previous occurence")); + m_findPreviousAct->setShortcut(QKeySequence::FindPrevious); + connect(m_findPreviousAct, SIGNAL(triggered(bool)), + m_findReplaceDialog, SLOT(findPrevious())); + + m_replaceAct = new QAction(tr("Replace"), this); + m_replaceAct->setStatusTip(tr("Replace current occurence")); + m_replaceAct->setShortcut(QKeySequence::Replace); + connect(m_replaceAct, SIGNAL(triggered(bool)), + m_findReplaceDialog, SLOT(replace())); + + m_replaceFindAct = new QAction(tr("Replace && Find"), this); + m_replaceFindAct->setStatusTip(tr("Replace current occurence and find the next one")); + connect(m_replaceFindAct, SIGNAL(triggered(bool)), + m_findReplaceDialog, SLOT(replaceFind())); + + m_replaceAllAct = new QAction(tr("Replace All"), this); + m_replaceAllAct->setStatusTip(tr("Replace all occurences in current note")); + connect(m_replaceAllAct, SIGNAL(triggered(bool)), + m_findReplaceDialog, SLOT(replaceAll())); + // Expand Tab into spaces. QAction *expandTabAct = new QAction(tr("&Expand Tab"), this); expandTabAct->setStatusTip(tr("Expand entered tab to spaces")); @@ -364,8 +403,25 @@ void VMainWindow::initEditMenu() this, &VMainWindow::changeHighlightCursorLine); - editMenu->addAction(insertImageAct); + editMenu->addAction(m_insertImageAct); editMenu->addSeparator(); + m_insertImageAct->setEnabled(false); + + QMenu *findReplaceMenu = editMenu->addMenu(tr("Find/Replace")); + findReplaceMenu->addAction(m_findReplaceAct); + findReplaceMenu->addAction(m_findNextAct); + findReplaceMenu->addAction(m_findPreviousAct); + findReplaceMenu->addAction(m_replaceAct); + findReplaceMenu->addAction(m_replaceFindAct); + findReplaceMenu->addAction(m_replaceAllAct); + editMenu->addSeparator(); + m_findReplaceAct->setEnabled(false); + m_findNextAct->setEnabled(false); + m_findPreviousAct->setEnabled(false); + m_replaceAct->setEnabled(false); + m_replaceFindAct->setEnabled(false); + m_replaceAllAct->setEnabled(false); + editMenu->addAction(expandTabAct); if (vconfig.getIsExpandTab()) { expandTabAct->setChecked(true); @@ -588,36 +644,56 @@ void VMainWindow::setRenderBackgroundColor(QAction *action) vnote->updateTemplate(); } -void VMainWindow::updateToolbarFromTabChage(const VFile *p_file, bool p_editMode) +void VMainWindow::updateActionStateFromTabStatusChange(const VFile *p_file, + bool p_editMode) { - qDebug() << "update toolbar"; - if (!p_file) { + if (p_file) { + if (p_editMode) { + editNoteAct->setVisible(false); + saveExitAct->setVisible(true); + saveNoteAct->setVisible(true); + deleteNoteAct->setEnabled(true); + + m_insertImageAct->setEnabled(true); + } else { + editNoteAct->setVisible(true); + saveExitAct->setVisible(false); + saveNoteAct->setVisible(false); + deleteNoteAct->setEnabled(true); + + m_insertImageAct->setEnabled(false); + m_replaceAct->setEnabled(false); + m_replaceFindAct->setEnabled(false); + m_replaceAllAct->setEnabled(false); + } + noteInfoAct->setEnabled(true); + + m_findReplaceAct->setEnabled(true); + } else { editNoteAct->setVisible(false); saveExitAct->setVisible(false); saveNoteAct->setVisible(false); deleteNoteAct->setEnabled(false); - } else if (p_editMode) { - editNoteAct->setVisible(false); - saveExitAct->setVisible(true); - saveNoteAct->setVisible(true); - deleteNoteAct->setEnabled(true); - } else { - editNoteAct->setVisible(true); - saveExitAct->setVisible(false); - saveNoteAct->setVisible(false); - deleteNoteAct->setEnabled(true); - } - - if (p_file) { - noteInfoAct->setEnabled(true); - } else { noteInfoAct->setEnabled(false); + + m_insertImageAct->setEnabled(false); + // Find/Replace + m_findReplaceAct->setEnabled(false); + m_findNextAct->setEnabled(false); + m_findPreviousAct->setEnabled(false); + m_replaceAct->setEnabled(false); + m_replaceFindAct->setEnabled(false); + m_replaceAllAct->setEnabled(false); + m_findReplaceDialog->closeDialog(); } } void VMainWindow::handleCurTabStatusChanged(const VFile *p_file, const VEditTab *p_editTab, bool p_editMode) { - updateToolbarFromTabChage(p_file, p_editMode); + updateActionStateFromTabStatusChange(p_file, p_editMode); + if (p_file) { + m_findReplaceDialog->updateState(p_file->getDocType(), p_editMode); + } QString title; if (p_file) { @@ -761,7 +837,7 @@ void VMainWindow::resizeEvent(QResizeEvent *event) void VMainWindow::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { - editArea->getFindReplaceDialog()->closeDialog(); + m_findReplaceDialog->closeDialog(); event->accept(); return; } @@ -809,3 +885,21 @@ void VMainWindow::locateFile(VFile *p_file) const } } +void VMainWindow::handleFindDialogTextChanged(const QString &p_text, uint /* p_options */) +{ + bool enabled = true; + if (p_text.isEmpty()) { + enabled = false; + } + m_findNextAct->setEnabled(enabled); + m_findPreviousAct->setEnabled(enabled); + m_replaceAct->setEnabled(enabled); + m_replaceFindAct->setEnabled(enabled); + m_replaceAllAct->setEnabled(enabled); +} + +void VMainWindow::openFindDialog() +{ + m_findReplaceDialog->openDialog(editArea->getSelectedText()); +} + diff --git a/src/vmainwindow.h b/src/vmainwindow.h index f63cc69c..ec9f4e32 100644 --- a/src/vmainwindow.h +++ b/src/vmainwindow.h @@ -27,6 +27,7 @@ class QToolBox; class VOutline; class VNotebookSelector; class VAvatar; +class VFindReplaceDialog; class VMainWindow : public QMainWindow { @@ -55,6 +56,8 @@ private slots: void handleCurrentDirectoryChanged(const VDirectory *p_dir); void handleCurrentNotebookChanged(const VNotebook *p_notebook); void insertImage(); + void handleFindDialogTextChanged(const QString &p_text, uint p_options); + void openFindDialog(); protected: void closeEvent(QCloseEvent *event) Q_DECL_OVERRIDE; @@ -83,7 +86,8 @@ private: void initEditorBackgroundMenu(QMenu *menu); void changeSplitterView(int nrPanel); void updateWindowTitle(const QString &str); - void updateToolbarFromTabChage(const VFile *p_file, bool p_editMode); + void updateActionStateFromTabStatusChange(const VFile *p_file, + bool p_editMode); void saveStateAndGeometry(); void restoreStateAndGeometry(); void repositionAvatar(); @@ -103,6 +107,7 @@ private: QToolBox *toolBox; VOutline *outline; VAvatar *m_avatar; + VFindReplaceDialog *m_findReplaceDialog; // Actions QAction *newRootDirAct; @@ -115,6 +120,14 @@ private: QAction *discardExitAct; QAction *expandViewAct; + QAction *m_insertImageAct; + QAction *m_findReplaceAct; + QAction *m_findNextAct; + QAction *m_findPreviousAct; + QAction *m_replaceAct; + QAction *m_replaceFindAct; + QAction *m_replaceAllAct; + // Menus QMenu *viewMenu; diff --git a/src/vmdeditoperations.cpp b/src/vmdeditoperations.cpp index 48351f4a..dccdbc09 100644 --- a/src/vmdeditoperations.cpp +++ b/src/vmdeditoperations.cpp @@ -287,6 +287,7 @@ bool VMdEditOperations::handleKeyBracketLeft(QKeyEvent *p_event) // 1. If it is not in Normal state, just go back to Normal state; // 2. If it is already Normal state, try to clear selection; // 3. Anyway, we accept this event. + bool accept = false; if (p_event->modifiers() == Qt::ControlModifier) { if (m_keyState != KeyState::Normal) { m_pendingTimer->stop(); @@ -299,9 +300,12 @@ bool VMdEditOperations::handleKeyBracketLeft(QKeyEvent *p_event) m_editor->setTextCursor(cursor); } } + accept = true; } - p_event->accept(); - return true; + if (accept) { + p_event->accept(); + } + return accept; } bool VMdEditOperations::handleKeyTab(QKeyEvent *p_event) diff --git a/src/vnote.qrc b/src/vnote.qrc index 599fe364..82cef341 100644 --- a/src/vnote.qrc +++ b/src/vnote.qrc @@ -82,5 +82,6 @@ resources/icons/corner_menu_cur.svg resources/icons/corner_tablist_cur.svg resources/icons/close.svg + resources/icons/find_replace.svg