From a2ee5413a1e090052fd0c1bff1b9d6d475250fcd Mon Sep 17 00:00:00 2001 From: Le Tan Date: Fri, 9 Mar 2018 22:47:53 +0800 Subject: [PATCH] support search - Ctrl+E C to toggle the search dock; --- src/dialog/vexportdialog.cpp | 23 +- src/dialog/vexportdialog.h | 2 +- src/dialog/vfindreplacedialog.cpp | 6 +- src/dialog/vfindreplacedialog.h | 1 + src/isearchengine.h | 34 ++ src/resources/icons/clear_search.svg | 10 + src/resources/icons/note_item.svg | 15 + src/resources/icons/search.svg | 10 + src/resources/icons/search_advanced.svg | 14 + src/resources/icons/search_console.svg | 12 + .../themes/v_moonlight/v_moonlight.qss | 222 +++---- src/resources/themes/v_pure/v_pure.qss | 232 ++++---- src/resources/vnote.ini | 11 + src/src.pro | 14 +- src/utils/vutils.cpp | 17 +- src/vconfigmanager.cpp | 5 +- src/vconfigmanager.h | 39 +- src/vdirectorytree.cpp | 4 +- src/vedit.cpp | 1 + src/veditarea.cpp | 14 + src/veditarea.h | 3 + src/vmainwindow.cpp | 113 +++- src/vmainwindow.h | 30 +- src/vmdeditor.cpp | 2 + src/vnote.cpp | 11 + src/vnote.h | 4 + src/vnote.qrc | 5 + src/vopenedlistmenu.cpp | 1 - src/vsearch.cpp | 418 +++++++++++++ src/vsearch.h | 162 ++++++ src/vsearchconfig.h | 273 +++++++++ src/vsearchengine.cpp | 255 ++++++++ src/vsearchengine.h | 89 +++ src/vsearcher.cpp | 548 ++++++++++++++++++ src/vsearcher.h | 108 ++++ src/vsearchresulttree.cpp | 143 +++++ src/vsearchresulttree.h | 39 ++ src/vsimplesearchinput.h | 14 + src/vtextedit.cpp | 15 +- src/vtextedit.h | 4 + src/vtreewidget.h | 14 + 41 files changed, 2668 insertions(+), 269 deletions(-) create mode 100644 src/isearchengine.h create mode 100644 src/resources/icons/clear_search.svg create mode 100644 src/resources/icons/note_item.svg create mode 100644 src/resources/icons/search.svg create mode 100644 src/resources/icons/search_advanced.svg create mode 100644 src/resources/icons/search_console.svg create mode 100644 src/vsearch.cpp create mode 100644 src/vsearch.h create mode 100644 src/vsearchconfig.h create mode 100644 src/vsearchengine.cpp create mode 100644 src/vsearchengine.h create mode 100644 src/vsearcher.cpp create mode 100644 src/vsearcher.h create mode 100644 src/vsearchresulttree.cpp create mode 100644 src/vsearchresulttree.h diff --git a/src/dialog/vexportdialog.cpp b/src/dialog/vexportdialog.cpp index 2dd12eda..da4a581c 100644 --- a/src/dialog/vexportdialog.cpp +++ b/src/dialog/vexportdialog.cpp @@ -75,7 +75,7 @@ void VExportDialog::setupUI() // Notes to export. m_srcCB = VUtils::getComboBox(); m_srcCB->setToolTip(tr("Choose notes to export")); - m_srcCB->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon); + m_srcCB->setSizeAdjustPolicy(QComboBox::AdjustToContents); connect(m_srcCB, SIGNAL(currentIndexChanged(int)), this, SLOT(handleCurrentSrcChanged(int))); @@ -580,7 +580,7 @@ void VExportDialog::startExport() if (opt.m_outputSuffix.isEmpty() || opt.m_cmd.isEmpty() - || opt.m_allInOne && opt.m_folderSep.isEmpty()) { + || (opt.m_allInOne && opt.m_folderSep.isEmpty())) { appendLogLine(tr("Invalid configurations for custom export.")); m_inExport = false; m_exportBtn->setEnabled(true); @@ -1274,11 +1274,11 @@ int VExportDialog::doExportCustomAllInOne(const QList &p_files, QWidget *VExportDialog::setupCustomAdvancedSettings() { // Source format. - m_customSrcFormatCB = VUtils::getComboBox(); + m_customSrcFormatCB = VUtils::getComboBox(this); m_customSrcFormatCB->setToolTip(tr("Choose format of the input")); // Output suffix. - m_customSuffixEdit = new VLineEdit(); + m_customSuffixEdit = new VLineEdit(this); m_customSuffixEdit->setPlaceholderText(tr("Without the preceding dot")); m_customSuffixEdit->setToolTip(tr("Suffix of the output file without the preceding dot")); QValidator *validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp), @@ -1288,11 +1288,12 @@ QWidget *VExportDialog::setupCustomAdvancedSettings() QLabel *tipsLabel = new QLabel(tr("%0 for the input file; " "%1 for the output file; " "%2 for the rendering CSS style file; " - "%3 for the input file directory.")); + "%3 for the input file directory."), + this); tipsLabel->setWordWrap(true); // Enable All In One. - m_customAllInOneCB = new QCheckBox(tr("Enable All In One")); + m_customAllInOneCB = new QCheckBox(tr("Enable All In One"), this); m_customAllInOneCB->setToolTip(tr("Pass a list of input files to the custom command")); connect(m_customAllInOneCB, &QCheckBox::stateChanged, this, [this](int p_state) { @@ -1302,13 +1303,13 @@ QWidget *VExportDialog::setupCustomAdvancedSettings() }); // Input directory separator. - m_customFolderSepEdit = new VLineEdit(); + m_customFolderSepEdit = new VLineEdit(this); m_customFolderSepEdit->setPlaceholderText(tr("Separator to concatenate input files directories")); m_customFolderSepEdit->setToolTip(tr("Separator to concatenate input files directories")); m_customFolderSepEdit->setEnabled(false); // Target file name for all in one. - m_customTargetFileNameEdit = new VLineEdit(); + m_customTargetFileNameEdit = new VLineEdit(this); m_customTargetFileNameEdit->setPlaceholderText(tr("Empty to use the name of the first source file")); m_customTargetFileNameEdit->setToolTip(tr("Name of the generated All-In-One file")); validator = new QRegExpValidator(QRegExp(VUtils::c_fileNameRegExp), @@ -1317,7 +1318,7 @@ QWidget *VExportDialog::setupCustomAdvancedSettings() m_customTargetFileNameEdit->setEnabled(false); // Cmd edit. - m_customCmdEdit = new QPlainTextEdit(); + m_customCmdEdit = new QPlainTextEdit(this); m_customCmdEdit->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); QString cmdExamp("pandoc --resource-path=.:\"%3\" --css=\"%2\" -s -o \"%1\" \"%0\""); m_customCmdEdit->setPlaceholderText(cmdExamp); @@ -1348,12 +1349,12 @@ QWidget *VExportDialog::setupCustomAdvancedSettings() QWidget *wid = new QWidget(); wid->setLayout(advLayout); - m_customCmdEdit->setMaximumHeight(100); + m_customCmdEdit->setMaximumHeight(m_customSrcFormatCB->height() * 3); return wid; } -int VExportDialog::outputAsHTML(QString &p_outputFolder, +int VExportDialog::outputAsHTML(const QString &p_outputFolder, QString *p_errMsg, QList *p_outputFiles) { diff --git a/src/dialog/vexportdialog.h b/src/dialog/vexportdialog.h index 7ebd3718..d9d33b19 100644 --- a/src/dialog/vexportdialog.h +++ b/src/dialog/vexportdialog.h @@ -368,7 +368,7 @@ private: ExportFormat currentFormat() const; - int outputAsHTML(QString &p_outputFolder, + int outputAsHTML(const QString &p_outputFolder, QString *p_errMsg = NULL, QList *p_outputFiles = NULL); diff --git a/src/dialog/vfindreplacedialog.cpp b/src/dialog/vfindreplacedialog.cpp index b43e7e87..11a3f763 100644 --- a/src/dialog/vfindreplacedialog.cpp +++ b/src/dialog/vfindreplacedialog.cpp @@ -49,7 +49,7 @@ void VFindReplaceDialog::setupUI() m_replaceFindBtn->setProperty("FlatBtn", true); 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); @@ -199,9 +199,9 @@ void VFindReplaceDialog::handleFindTextChanged(const QString &p_text) void VFindReplaceDialog::advancedBtnToggled(bool p_checked) { if (p_checked) { - m_advancedBtn->setText("B&asic <<"); + m_advancedBtn->setText(tr("B&asic <<<")); } else { - m_advancedBtn->setText("&Advanced <<"); + m_advancedBtn->setText(tr("&Advanced >>>")); } m_caseSensitiveCheck->setVisible(p_checked); diff --git a/src/dialog/vfindreplacedialog.h b/src/dialog/vfindreplacedialog.h index 3d2edb4c..8b6d09b3 100644 --- a/src/dialog/vfindreplacedialog.h +++ b/src/dialog/vfindreplacedialog.h @@ -48,6 +48,7 @@ private slots: private: void setupUI(); + // Bit OR of FindOption uint m_options; bool m_replaceAvailable; diff --git a/src/isearchengine.h b/src/isearchengine.h new file mode 100644 index 00000000..3ccb4c82 --- /dev/null +++ b/src/isearchengine.h @@ -0,0 +1,34 @@ +#ifndef ISEARCHENGINE_H +#define ISEARCHENGINE_H + +#include +#include + +#include "vsearchconfig.h" + +// Abstract class for search engine. +class ISearchEngine : public QObject +{ + Q_OBJECT +public: + explicit ISearchEngine(QObject *p_parent = nullptr) + : QObject(p_parent) + { + } + + virtual void search(const QSharedPointer &p_config, + const QSharedPointer &p_result) = 0; + + virtual void stop() = 0; + + virtual void clear() = 0; + +signals: + void finished(const QSharedPointer &p_result); + + void resultItemAdded(const QSharedPointer &p_item); + +protected: + QSharedPointer m_result; +}; +#endif // ISEARCHENGINE_H diff --git a/src/resources/icons/clear_search.svg b/src/resources/icons/clear_search.svg new file mode 100644 index 00000000..59af5e14 --- /dev/null +++ b/src/resources/icons/clear_search.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/resources/icons/note_item.svg b/src/resources/icons/note_item.svg new file mode 100644 index 00000000..b7e13b74 --- /dev/null +++ b/src/resources/icons/note_item.svg @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/src/resources/icons/search.svg b/src/resources/icons/search.svg new file mode 100644 index 00000000..0927bfb2 --- /dev/null +++ b/src/resources/icons/search.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/resources/icons/search_advanced.svg b/src/resources/icons/search_advanced.svg new file mode 100644 index 00000000..71ead32d --- /dev/null +++ b/src/resources/icons/search_advanced.svg @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/src/resources/icons/search_console.svg b/src/resources/icons/search_console.svg new file mode 100644 index 00000000..0a3d2842 --- /dev/null +++ b/src/resources/icons/search_console.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/src/resources/themes/v_moonlight/v_moonlight.qss b/src/resources/themes/v_moonlight/v_moonlight.qss index 7c516d23..fd94e39e 100644 --- a/src/resources/themes/v_moonlight/v_moonlight.qss +++ b/src/resources/themes/v_moonlight/v_moonlight.qss @@ -183,64 +183,18 @@ QDockWidget::close-button:hover, QDockWidget::float-button:hover { /* End DockWidget */ /* QPushButton */ -QPushButton { - color: @pushbutton_fg; - background: @pushbutton_bg; - border: 1px solid @pushbutton_border; - padding: 3px; - min-width: 80px; -} - -QPushButton:focus { - background-color: @pushbutton_focus_bg; -} - -QPushButton:pressed { - background-color: @pushbutton_pressed_bg; -} - -QPushButton:checked { - background-color: @pushbutton_checked_bg; -} - -QPushButton:checked:hover { - background-color: @pushbutton_hover_bg; -} - -QPushButton:hover { - background-color: @pushbutton_hover_bg; -} - -QPushButton:flat { - border: none; -} - -QPushButton:default { - border: 1px solid @pushbutton_default_border; -} - -QPushButton:disabled { - color: @pushbutton_disabled_fg; - background-color: @pushbutton_disabled_bg; -} - -QPushButton::menu-indicator { - image: url(arrow_dropdown.svg); - width: 16px; - height: 16px; -} - QPushButton[SpecialBtn="true"] { color: @pushbutton_specialbtn_fg; background: @pushbutton_specialbtn_bg; } -QPushButton[SpecialBtn="true"]:focus { - background-color: @pushbutton_specialbtn_focus_bg; +QPushButton[SpecialBtn="true"]:default { + border: 1px solid @pushbutton_default_border; + background-color: @pushbutton_specialbtn_bg; } -QPushButton[SpecialBtn="true"]:pressed { - background-color: @pushbutton_specialbtn_pressed_bg; +QPushButton[SpecialBtn="true"]:focus { + background-color: @pushbutton_specialbtn_focus_bg; } QPushButton[SpecialBtn="true"]:checked { @@ -255,16 +209,15 @@ QPushButton[SpecialBtn="true"]:hover { background-color: @pushbutton_specialbtn_hover_bg; } +QPushButton[SpecialBtn="true"]:pressed { + background-color: @pushbutton_specialbtn_pressed_bg; +} + QPushButton[SpecialBtn="true"]:disabled { color: @pushbutton_disabled_fg; background-color: @pushbutton_disabled_bg; } -QPushButton[SpecialBtn="true"]:default { - border: 1px solid @pushbutton_default_border; - background-color: @pushbutton_specialbtn_bg; -} - QPushButton[CornerBtn="true"] { padding: 4px -2px 4px -2px; margin: 0px; @@ -277,14 +230,19 @@ QPushButton[CornerBtn="true"]::menu-indicator { image: none; } -QPushButton[CornerBtn="true"]:hover { - background-color: @pushbutton_cornerbtn_hover_bg; +QPushButton[CornerBtn="true"]:default { + border: 1px solid @pushbutton_default_border; + background-color: transparent; } QPushButton[CornerBtn="true"]:focus { background-color: @pushbutton_cornerbtn_focus_bg; } +QPushButton[CornerBtn="true"]:hover { + background-color: @pushbutton_cornerbtn_hover_bg; +} + QPushButton[CornerBtn="true"]:pressed { background-color: @pushbutton_cornerbtn_pressed_bg; } @@ -294,11 +252,6 @@ QPushButton[CornerBtn="true"]:disabled { background-color: @pushbutton_disabled_bg; } -QPushButton[CornerBtn="true"]:default { - border: 1px solid @pushbutton_default_border; - background-color: transparent; -} - QPushButton[StatusBtn="true"] { font: bold; padding: 0px 2px 0px 2px; @@ -308,14 +261,19 @@ QPushButton[StatusBtn="true"] { min-width: -1; } -QPushButton[StatusBtn="true"]:hover { - background-color: @pushbutton_statusbtn_hover_bg; +QPushButton[StatusBtn="true"]:default { + border: 1px solid @pushbutton_default_border; + background-color: transparent; } QPushButton[StatusBtn="true"]:focus { background-color: @pushbutton_statusbtn_focus_bg;; } +QPushButton[StatusBtn="true"]:hover { + background-color: @pushbutton_statusbtn_hover_bg; +} + QPushButton[StatusBtn="true"]:pressed { background-color: @pushbutton_statusbtn_pressed_bg; } @@ -325,11 +283,6 @@ QPushButton[StatusBtn="true"]:disabled { background-color: @pushbutton_disabled_bg; } -QPushButton[StatusBtn="true"]:default { - border: 1px solid @pushbutton_default_border; - background-color: transparent; -} - QPushButton[FlatBtn="true"] { padding: 4px; margin: 0px; @@ -338,14 +291,19 @@ QPushButton[FlatBtn="true"] { min-width: -1; } -QPushButton[FlatBtn="true"]:hover { - background-color: @pushbutton_flatbtn_hover_bg; +QPushButton[FlatBtn="true"]:default { + border: 1px solid @pushbutton_default_border; + background-color: transparent; } QPushButton[FlatBtn="true"]:focus { background-color: @pushbutton_flatbtn_focus_bg; } +QPushButton[FlatBtn="true"]:hover { + background-color: @pushbutton_flatbtn_hover_bg; +} + QPushButton[FlatBtn="true"]:pressed { background-color: @pushbutton_flatbtn_pressed_bg; } @@ -355,11 +313,6 @@ QPushButton[FlatBtn="true"]:disabled { background-color: @pushbutton_disabled_bg; } -QPushButton[FlatBtn="true"]:default { - border: 1px solid @pushbutton_default_border; - background-color: transparent; -} - QPushButton[SelectionBtn="true"] { padding: 4px 10px 4px 10px; border: none; @@ -369,14 +322,19 @@ QPushButton[SelectionBtn="true"] { min-width: -1; } -QPushButton[SelectionBtn="true"]:hover { - background-color: @pushbutton_selectionbtn_hover_bg; +QPushButton[SelectionBtn="true"]:default { + border: 1px solid @pushbutton_default_border; + background-color: transparent; } QPushButton[SelectionBtn="true"]:focus { background-color: @pushbutton_selectionbtn_focus_bg; } +QPushButton[SelectionBtn="true"]:hover { + background-color: @pushbutton_selectionbtn_hover_bg; +} + QPushButton[SelectionBtn="true"]:pressed { background-color: @pushbutton_selectionbtn_pressed_bg; } @@ -386,11 +344,6 @@ QPushButton[SelectionBtn="true"]:disabled { background-color: @pushbutton_disabled_bg; } -QPushButton[SelectionBtn="true"]:default { - border: 1px solid @pushbutton_default_border; - background-color: transparent; -} - QPushButton[TitleBtn="true"] { padding: 4px; margin: 0px; @@ -399,14 +352,19 @@ QPushButton[TitleBtn="true"] { min-width: -1; } -QPushButton[TitleBtn="true"]:hover { - background-color: @pushbutton_titlebtn_hover_bg; +QPushButton[TitleBtn="true"]:default { + border: 1px solid @pushbutton_default_border; + background-color: @pushbutton_titlebtn_bg; } QPushButton[TitleBtn="true"]:focus { background-color: @pushbutton_titlebtn_focus_bg; } +QPushButton[TitleBtn="true"]:hover { + background-color: @pushbutton_titlebtn_hover_bg; +} + QPushButton[TitleBtn="true"]:pressed { background-color: @pushbutton_titlebtn_pressed_bg; } @@ -416,11 +374,6 @@ QPushButton[TitleBtn="true"]:disabled { background-color: @pushbutton_disabled_bg; } -QPushButton[TitleBtn="true"]:default { - border: 1px solid @pushbutton_default_border; - background-color: @pushbutton_titlebtn_bg; -} - QPushButton[DangerBtn="true"] { color: @pushbutton_dangerbtn_fg; border: none; @@ -428,14 +381,23 @@ QPushButton[DangerBtn="true"] { min-width: -1; } -QPushButton[DangerBtn="true"]:hover { - background-color: @pushbutton_dangerbtn_hover_bg; +QPushButton[DangerBtn="true"]:default { + border: 1px solid @pushbutton_default_border; + background-color: @pushbutton_dangerbtn_bg; } QPushButton[DangerBtn="true"]:focus { background-color: @pushbutton_dangerbtn_focus_bg; } +QPushButton[DangerBtn="true"]:checked { + background-color: @pushbutton_dangerbtn_pressed_bg; +} + +QPushButton[DangerBtn="true"]:hover { + background-color: @pushbutton_dangerbtn_hover_bg; +} + QPushButton[DangerBtn="true"]:pressed { background-color: @pushbutton_dangerbtn_pressed_bg; } @@ -445,11 +407,6 @@ QPushButton[DangerBtn="true"]:disabled { background-color: @pushbutton_disabled_bg; } -QPushButton[DangerBtn="true"]:default { - border: 1px solid @pushbutton_default_border; - background-color: @pushbutton_dangerbtn_bg; -} - QPushButton[ToolBoxActiveBtn="true"] { padding: 4px 10px 4px 4px; margin: 0px; @@ -459,6 +416,11 @@ QPushButton[ToolBoxActiveBtn="true"] { min-width: -1; } +QPushButton[ToolBoxActiveBtn="true"]:default { + border: 1px solid @pushbutton_default_border; + background-color: @pushbutton_toolboxbtn_active_bg; +} + QPushButton[ToolBoxActiveBtn="true"]:focus { background-color: @pushbutton_toolboxbtn_active_focus_bg; } @@ -476,11 +438,6 @@ QPushButton[ToolBoxActiveBtn="true"]:disabled { background-color: @pushbutton_disabled_bg; } -QPushButton[ToolBoxActiveBtn="true"]:default { - border: 1px solid @pushbutton_default_border; - background-color: @pushbutton_toolboxbtn_active_bg; -} - QPushButton[AvatarBtn="true"] { padding: 2px 4px 2px 4px; margin: 0px; @@ -489,14 +446,19 @@ QPushButton[AvatarBtn="true"] { min-width: -1; } -QPushButton[AvatarBtn="true"]:hover { - background-color: @pushbutton_avatarbtn_hover_bg; +QPushButton[AvatarBtn="true"]:default { + border: 1px solid @pushbutton_default_border; + background-color: transparent; } QPushButton[AvatarBtn="true"]:focus { background-color: @pushbutton_avatarbtn_focus_bg;; } +QPushButton[AvatarBtn="true"]:hover { + background-color: @pushbutton_avatarbtn_hover_bg; +} + QPushButton[AvatarBtn="true"]:pressed { background-color: @pushbutton_avatarbtn_pressed_bg; } @@ -506,9 +468,51 @@ QPushButton[AvatarBtn="true"]:disabled { background-color: @pushbutton_disabled_bg; } -QPushButton[AvatarBtn="true"]:default { +QPushButton { + color: @pushbutton_fg; + background: @pushbutton_bg; + border: 1px solid @pushbutton_border; + padding: 3px; + min-width: 80px; +} + +QPushButton:default { border: 1px solid @pushbutton_default_border; - background-color: transparent; +} + +QPushButton:focus { + background-color: @pushbutton_focus_bg; +} + +QPushButton:checked { + background-color: @pushbutton_checked_bg; +} + +QPushButton:checked:hover { + background-color: @pushbutton_hover_bg; +} + +QPushButton:flat { + border: none; +} + +QPushButton:hover { + background-color: @pushbutton_hover_bg; +} + +QPushButton:pressed { + background-color: @pushbutton_pressed_bg; +} + +QPushButton:disabled { + color: @pushbutton_disabled_fg; + background-color: @pushbutton_disabled_bg; +} + +QPushButton::menu-indicator { + image: url(arrow_dropdown.svg); + width: 16px; + height: 16px; } VButtonMenuItem { @@ -544,11 +548,11 @@ VButtonMenuItem[Heading6="true"] { font-size: 14pt; } -VButtonMenuItem:hover { +VButtonMenuItem:focus { background-color: @menubar_item_selected_bg; } -VButtonMenuItem:focus { +VButtonMenuItem:hover { background-color: @menubar_item_selected_bg; } diff --git a/src/resources/themes/v_pure/v_pure.qss b/src/resources/themes/v_pure/v_pure.qss index 52ae6b74..a8cbbd92 100644 --- a/src/resources/themes/v_pure/v_pure.qss +++ b/src/resources/themes/v_pure/v_pure.qss @@ -183,64 +183,18 @@ QDockWidget::close-button:hover, QDockWidget::float-button:hover { /* End DockWidget */ /* QPushButton */ -QPushButton { - color: @pushbutton_fg; - background: @pushbutton_bg; - border: 1px solid @pushbutton_border; - padding: 3px; - min-width: 80px; -} - -QPushButton:focus { - background-color: @pushbutton_focus_bg; -} - -QPushButton:pressed { - background-color: @pushbutton_pressed_bg; -} - -QPushButton:checked { - background-color: @pushbutton_checked_bg; -} - -QPushButton:checked:hover { - background-color: @pushbutton_hover_bg; -} - -QPushButton:hover { - background-color: @pushbutton_hover_bg; -} - -QPushButton:flat { - border: none; -} - -QPushButton:default { - border: 1px solid @pushbutton_default_border; -} - -QPushButton:disabled { - color: @pushbutton_disabled_fg; - background-color: @pushbutton_disabled_bg; -} - -QPushButton::menu-indicator { - image: url(arrow_dropdown.svg); - width: 16px; - height: 16px; -} - QPushButton[SpecialBtn="true"] { color: @pushbutton_specialbtn_fg; background: @pushbutton_specialbtn_bg; } -QPushButton[SpecialBtn="true"]:focus { - background-color: @pushbutton_specialbtn_focus_bg; +QPushButton[SpecialBtn="true"]:default { + border: 1px solid @pushbutton_default_border; + background-color: @pushbutton_specialbtn_bg; } -QPushButton[SpecialBtn="true"]:pressed { - background-color: @pushbutton_specialbtn_pressed_bg; +QPushButton[SpecialBtn="true"]:focus { + background-color: @pushbutton_specialbtn_focus_bg; } QPushButton[SpecialBtn="true"]:checked { @@ -255,16 +209,15 @@ QPushButton[SpecialBtn="true"]:hover { background-color: @pushbutton_specialbtn_hover_bg; } +QPushButton[SpecialBtn="true"]:pressed { + background-color: @pushbutton_specialbtn_pressed_bg; +} + QPushButton[SpecialBtn="true"]:disabled { color: @pushbutton_disabled_fg; background-color: @pushbutton_disabled_bg; } -QPushButton[SpecialBtn="true"]:default { - border: 1px solid @pushbutton_default_border; - background-color: @pushbutton_specialbtn_bg; -} - QPushButton[CornerBtn="true"] { padding: 4px -2px 4px -2px; margin: 0px; @@ -277,14 +230,19 @@ QPushButton[CornerBtn="true"]::menu-indicator { image: none; } -QPushButton[CornerBtn="true"]:hover { - background-color: @pushbutton_cornerbtn_hover_bg; +QPushButton[CornerBtn="true"]:default { + border: 1px solid @pushbutton_default_border; + background-color: transparent; } QPushButton[CornerBtn="true"]:focus { background-color: @pushbutton_cornerbtn_focus_bg; } +QPushButton[CornerBtn="true"]:hover { + background-color: @pushbutton_cornerbtn_hover_bg; +} + QPushButton[CornerBtn="true"]:pressed { background-color: @pushbutton_cornerbtn_pressed_bg; } @@ -294,11 +252,6 @@ QPushButton[CornerBtn="true"]:disabled { background-color: @pushbutton_disabled_bg; } -QPushButton[CornerBtn="true"]:default { - border: 1px solid @pushbutton_default_border; - background-color: transparent; -} - QPushButton[StatusBtn="true"] { font: bold; padding: 0px 2px 0px 2px; @@ -308,14 +261,19 @@ QPushButton[StatusBtn="true"] { min-width: -1; } -QPushButton[StatusBtn="true"]:hover { - background-color: @pushbutton_statusbtn_hover_bg; +QPushButton[StatusBtn="true"]:default { + border: 1px solid @pushbutton_default_border; + background-color: transparent; } QPushButton[StatusBtn="true"]:focus { background-color: @pushbutton_statusbtn_focus_bg;; } +QPushButton[StatusBtn="true"]:hover { + background-color: @pushbutton_statusbtn_hover_bg; +} + QPushButton[StatusBtn="true"]:pressed { background-color: @pushbutton_statusbtn_pressed_bg; } @@ -325,11 +283,6 @@ QPushButton[StatusBtn="true"]:disabled { background-color: @pushbutton_disabled_bg; } -QPushButton[StatusBtn="true"]:default { - border: 1px solid @pushbutton_default_border; - background-color: transparent; -} - QPushButton[FlatBtn="true"] { padding: 4px; margin: 0px; @@ -338,14 +291,19 @@ QPushButton[FlatBtn="true"] { min-width: -1; } -QPushButton[FlatBtn="true"]:hover { - background-color: @pushbutton_flatbtn_hover_bg; +QPushButton[FlatBtn="true"]:default { + border: 1px solid @pushbutton_default_border; + background-color: transparent; } QPushButton[FlatBtn="true"]:focus { background-color: @pushbutton_flatbtn_focus_bg; } +QPushButton[FlatBtn="true"]:hover { + background-color: @pushbutton_flatbtn_hover_bg; +} + QPushButton[FlatBtn="true"]:pressed { background-color: @pushbutton_flatbtn_pressed_bg; } @@ -355,11 +313,6 @@ QPushButton[FlatBtn="true"]:disabled { background-color: @pushbutton_disabled_bg; } -QPushButton[FlatBtn="true"]:default { - border: 1px solid @pushbutton_default_border; - background-color: transparent; -} - QPushButton[SelectionBtn="true"] { padding: 4px 10px 4px 10px; border: none; @@ -369,14 +322,19 @@ QPushButton[SelectionBtn="true"] { min-width: -1; } -QPushButton[SelectionBtn="true"]:hover { - background-color: @pushbutton_selectionbtn_hover_bg; +QPushButton[SelectionBtn="true"]:default { + border: 1px solid @pushbutton_default_border; + background-color: transparent; } QPushButton[SelectionBtn="true"]:focus { background-color: @pushbutton_selectionbtn_focus_bg; } +QPushButton[SelectionBtn="true"]:hover { + background-color: @pushbutton_selectionbtn_hover_bg; +} + QPushButton[SelectionBtn="true"]:pressed { background-color: @pushbutton_selectionbtn_pressed_bg; } @@ -386,11 +344,6 @@ QPushButton[SelectionBtn="true"]:disabled { background-color: @pushbutton_disabled_bg; } -QPushButton[SelectionBtn="true"]:default { - border: 1px solid @pushbutton_default_border; - background-color: transparent; -} - QPushButton[TitleBtn="true"] { padding: 4px; margin: 0px; @@ -399,14 +352,19 @@ QPushButton[TitleBtn="true"] { min-width: -1; } -QPushButton[TitleBtn="true"]:hover { - background-color: @pushbutton_titlebtn_hover_bg; +QPushButton[TitleBtn="true"]:default { + border: 1px solid @pushbutton_default_border; + background-color: @pushbutton_titlebtn_bg; } QPushButton[TitleBtn="true"]:focus { background-color: @pushbutton_titlebtn_focus_bg; } +QPushButton[TitleBtn="true"]:hover { + background-color: @pushbutton_titlebtn_hover_bg; +} + QPushButton[TitleBtn="true"]:pressed { background-color: @pushbutton_titlebtn_pressed_bg; } @@ -416,11 +374,6 @@ QPushButton[TitleBtn="true"]:disabled { background-color: @pushbutton_disabled_bg; } -QPushButton[TitleBtn="true"]:default { - border: 1px solid @pushbutton_default_border; - background-color: @pushbutton_titlebtn_bg; -} - QPushButton[DangerBtn="true"] { color: @pushbutton_dangerbtn_fg; border: none; @@ -428,21 +381,25 @@ QPushButton[DangerBtn="true"] { min-width: -1; } -QPushButton[DangerBtn="true"]:hover { - background-color: @pushbutton_dangerbtn_hover_bg; +QPushButton[DangerBtn="true"]:default { + border: 1px solid @pushbutton_default_border; + background-color: @pushbutton_dangerbtn_bg; } QPushButton[DangerBtn="true"]:focus { background-color: @pushbutton_dangerbtn_focus_bg; } -QPushButton[DangerBtn="true"]:pressed { +QPushButton[DangerBtn="true"]:checked { background-color: @pushbutton_dangerbtn_pressed_bg; } -QPushButton[DangerBtn="true"]:default { - border: 1px solid @pushbutton_default_border; - background-color: @pushbutton_dangerbtn_bg; +QPushButton[DangerBtn="true"]:hover { + background-color: @pushbutton_dangerbtn_hover_bg; +} + +QPushButton[DangerBtn="true"]:pressed { + background-color: @pushbutton_dangerbtn_pressed_bg; } QPushButton[DangerBtn="true"]:disabled { @@ -459,28 +416,28 @@ QPushButton[ToolBoxActiveBtn="true"] { min-width: -1; } -QPushButton[ToolBoxActiveBtn="true"]:focus { - background-color: @pushbutton_toolboxbtn_active_focus_bg; +QPushButton[ToolBoxActiveBtn="true"]:default { + border: 1px solid @pushbutton_default_border; + background-color: @pushbutton_toolboxbtn_active_bg; } -QPushButton[ToolBoxActiveBtn="true"]:hover { - background-color: @pushbutton_toolboxbtn_active_hover_bg; +QPushButton[ToolBoxActiveBtn="true"]:focus { + background-color: @pushbutton_toolboxbtn_active_focus_bg; } QPushButton[ToolBoxActiveBtn="true"]:pressed { background-color: @pushbutton_toolboxbtn_active_pressed_bg; } +QPushButton[ToolBoxActiveBtn="true"]:hover { + background-color: @pushbutton_toolboxbtn_active_hover_bg; +} + QPushButton[ToolBoxActiveBtn="true"]:disabled { color: @pushbutton_disabled_fg; background-color: @pushbutton_disabled_bg; } -QPushButton[ToolBoxActiveBtn="true"]:default { - border: 1px solid @pushbutton_default_border; - background-color: @pushbutton_toolboxbtn_active_bg; -} - QPushButton[AvatarBtn="true"] { padding: 2px 4px 2px 4px; margin: 0px; @@ -489,14 +446,19 @@ QPushButton[AvatarBtn="true"] { min-width: -1; } -QPushButton[AvatarBtn="true"]:hover { - background-color: @pushbutton_avatarbtn_hover_bg; -} - QPushButton[AvatarBtn="true"]:focus { background-color: @pushbutton_avatarbtn_focus_bg;; } +QPushButton[AvatarBtn="true"]:default { + border: 1px solid @pushbutton_default_border; + background-color: transparent; +} + +QPushButton[AvatarBtn="true"]:hover { + background-color: @pushbutton_avatarbtn_hover_bg; +} + QPushButton[AvatarBtn="true"]:pressed { background-color: @pushbutton_avatarbtn_pressed_bg; } @@ -506,9 +468,51 @@ QPushButton[AvatarBtn="true"]:disabled { background-color: @pushbutton_disabled_bg; } -QPushButton[AvatarBtn="true"]:default { +QPushButton { + color: @pushbutton_fg; + background: @pushbutton_bg; + border: 1px solid @pushbutton_border; + padding: 3px; + min-width: 80px; +} + +QPushButton:focus { + background-color: @pushbutton_focus_bg; +} + +QPushButton:checked { + background-color: @pushbutton_checked_bg; +} + +QPushButton:checked:hover { + background-color: @pushbutton_hover_bg; +} + +QPushButton:flat { + border: none; +} + +QPushButton:default { border: 1px solid @pushbutton_default_border; - background-color: transparent; +} + +QPushButton:hover { + background-color: @pushbutton_hover_bg; +} + +QPushButton:pressed { + background-color: @pushbutton_pressed_bg; +} + +QPushButton:disabled { + color: @pushbutton_disabled_fg; + background-color: @pushbutton_disabled_bg; +} + +QPushButton::menu-indicator { + image: url(arrow_dropdown.svg); + width: 16px; + height: 16px; } VButtonMenuItem { @@ -544,11 +548,11 @@ VButtonMenuItem[Heading6="true"] { font-size: 14pt; } -VButtonMenuItem:hover { +VButtonMenuItem:focus { background-color: @menubar_item_selected_bg; } -VButtonMenuItem:focus { +VButtonMenuItem:hover { background-color: @menubar_item_selected_bg; } diff --git a/src/resources/vnote.ini b/src/resources/vnote.ini index 044bb6e8..391e6c62 100644 --- a/src/resources/vnote.ini +++ b/src/resources/vnote.ini @@ -149,6 +149,9 @@ enable_compact_mode=true ; Whether enable tools dock widget tools_dock_checked=true +; Whether enable search dock widget +search_dock_checked=false + ; Whether show menu bar menu_bar_checked=true @@ -201,6 +204,10 @@ single_click_close_previous_tab=true ; Whether enable auto wildcard match in simple search like list and tree widgets enable_wildcard_in_simple_search=true +; Search options +; scope,object,target,engine,option,pattern +search_options=4,2,7,0,0,"" + [export] ; Path of the wkhtmltopdf tool wkhtmltopdf=wkhtmltopdf @@ -279,6 +286,8 @@ Find=Ctrl+F FindNext=F3 ; Find previous occurence FindPrevious=Shift+F3 +; Advanced find +AdvancedFind=Ctrl+Alt+F ; Recover last closed file LastClosedFile=Ctrl+Shift+T ; Activate next tab @@ -311,6 +320,8 @@ OnePanelView=P DiscardAndRead=Q ; Toggle Tools dock widget ToolsDock=T +; Toggle Search dock widget +SearchDock=C ; Close current note CloseNote=X ; Show shortcuts help document diff --git a/src/src.pro b/src/src.pro index 49b906f8..5b63bce3 100644 --- a/src/src.pro +++ b/src/src.pro @@ -113,7 +113,11 @@ SOURCES += main.cpp\ vstyleditemdelegate.cpp \ vtreewidget.cpp \ dialog/vexportdialog.cpp \ - vexporter.cpp + vexporter.cpp \ + vsearcher.cpp \ + vsearch.cpp \ + vsearchresulttree.cpp \ + vsearchengine.cpp HEADERS += vmainwindow.h \ vdirectorytree.h \ @@ -214,7 +218,13 @@ HEADERS += vmainwindow.h \ vtreewidget.h \ dialog/vexportdialog.h \ vexporter.h \ - vwordcountinfo.h + vwordcountinfo.h \ + vsearcher.h \ + vsearch.h \ + vsearchresulttree.h \ + isearchengine.h \ + vsearchconfig.h \ + vsearchengine.h RESOURCES += \ vnote.qrc \ diff --git a/src/utils/vutils.cpp b/src/utils/vutils.cpp index 50326ee0..578dcb96 100644 --- a/src/utils/vutils.cpp +++ b/src/utils/vutils.cpp @@ -522,13 +522,18 @@ bool VUtils::isImageURLText(const QString &p_url) qreal VUtils::calculateScaleFactor() { - // const qreal refHeight = 1152; - // const qreal refWidth = 2048; - const qreal refDpi = 96; + static qreal factor = -1; - qreal dpi = QGuiApplication::primaryScreen()->logicalDotsPerInch(); - qreal factor = dpi / refDpi; - return factor < 1 ? 1 : factor; + if (factor < 0) { + const qreal refDpi = 96; + qreal dpi = QGuiApplication::primaryScreen()->logicalDotsPerInch(); + factor = dpi / refDpi; + if (factor < 1) { + factor = 1; + } + } + + return factor; } bool VUtils::realEqual(qreal p_a, qreal p_b) diff --git a/src/vconfigmanager.cpp b/src/vconfigmanager.cpp index a5edcb1c..be53e44a 100644 --- a/src/vconfigmanager.cpp +++ b/src/vconfigmanager.cpp @@ -104,8 +104,6 @@ void VConfigManager::initialize() curRenderBackgroundColor = getConfigFromSettings("global", "current_render_background_color").toString(); - m_toolsDockChecked = getConfigFromSettings("global", "tools_dock_checked").toBool(); - m_findCaseSensitive = getConfigFromSettings("global", "find_case_sensitive").toBool(); m_findWholeWordOnly = getConfigFromSettings("global", @@ -498,11 +496,9 @@ QString VConfigManager::fetchDirConfigFilePath(const QString &p_path) if (!dir.rename(c_obsoleteDirConfigFile, c_dirConfigFile)) { fileName = c_obsoleteDirConfigFile; } - qDebug() << "rename old directory config file:" << fileName; } QString filePath = QDir::cleanPath(dir.filePath(fileName)); - qDebug() << "use directory config file:" << filePath; return filePath; } @@ -1473,6 +1469,7 @@ void VConfigManager::resetConfigurations() void VConfigManager::resetLayoutConfigurations() { resetDefaultConfig("global", "tools_dock_checked"); + resetDefaultConfig("global", "search_dock_checked"); resetDefaultConfig("global", "menu_bar_checked"); resetDefaultConfig("global", "enable_compact_mode"); diff --git a/src/vconfigmanager.h b/src/vconfigmanager.h index d99f7c90..77fba2f7 100644 --- a/src/vconfigmanager.h +++ b/src/vconfigmanager.h @@ -184,6 +184,9 @@ public: bool getToolsDockChecked() const; void setToolsDockChecked(bool p_checked); + bool getSearchDockChecked() const; + void setSearchDockChecked(bool p_checked); + const QByteArray &getMainWindowGeometry() const; void setMainWindowGeometry(const QByteArray &p_geometry); @@ -466,6 +469,9 @@ public: QStringList getCustomExport() const; void setCustomExport(const QStringList &p_exp); + QStringList getSearchOptions() const; + void setSearchOptions(const QStringList &p_opts); + private: // Look up a config from user and default settings. QVariant getConfigFromSettings(const QString §ion, const QString &key) const; @@ -605,8 +611,6 @@ private: QString curRenderBackgroundColor; - bool m_toolsDockChecked; - QByteArray m_mainWindowGeometry; QByteArray m_mainWindowState; QByteArray m_mainSplitterState; @@ -1153,14 +1157,26 @@ inline void VConfigManager::setCurRenderBackgroundColor(const QString &colorName inline bool VConfigManager::getToolsDockChecked() const { - return m_toolsDockChecked; + return getConfigFromSettings("global", "tools_dock_checked").toBool(); } inline void VConfigManager::setToolsDockChecked(bool p_checked) { - m_toolsDockChecked = p_checked; - setConfigToSettings("global", "tools_dock_checked", - m_toolsDockChecked); + setConfigToSettings("global", + "tools_dock_checked", + p_checked); +} + +inline bool VConfigManager::getSearchDockChecked() const +{ + return getConfigFromSettings("global", "search_dock_checked").toBool(); +} + +inline void VConfigManager::setSearchDockChecked(bool p_checked) +{ + setConfigToSettings("global", + "search_dock_checked", + p_checked); } inline const QByteArray& VConfigManager::getMainWindowGeometry() const @@ -2146,4 +2162,15 @@ inline void VConfigManager::setCustomExport(const QStringList &p_exp) { setConfigToSettings("export", "custom_export", p_exp); } + +inline QStringList VConfigManager::getSearchOptions() const +{ + return getConfigFromSettings("global", + "search_options").toStringList(); +} + +inline void VConfigManager::setSearchOptions(const QStringList &p_opts) +{ + setConfigToSettings("global", "search_options", p_opts); +} #endif // VCONFIGMANAGER_H diff --git a/src/vdirectorytree.cpp b/src/vdirectorytree.cpp index 7c8c61d3..3471186e 100644 --- a/src/vdirectorytree.cpp +++ b/src/vdirectorytree.cpp @@ -191,12 +191,14 @@ void VDirectoryTree::setNotebook(VNotebook *p_notebook) void VDirectoryTree::fillTreeItem(QTreeWidgetItem *p_item, VDirectory *p_directory) { + static QIcon itemIcon = VIconUtils::treeViewIcon(":/resources/icons/dir_item.svg"); + int col = 0; QString name = p_directory->getName(); p_item->setText(col, name); p_item->setToolTip(col, name); p_item->setData(col, Qt::UserRole, QVariant::fromValue(p_directory)); - p_item->setIcon(col, VIconUtils::treeViewIcon(":/resources/icons/dir_item.svg")); + p_item->setIcon(col, itemIcon); } void VDirectoryTree::updateDirectoryTree() diff --git a/src/vedit.cpp b/src/vedit.cpp index 341bf2ff..8c911807 100644 --- a/src/vedit.cpp +++ b/src/vedit.cpp @@ -188,6 +188,7 @@ void VEdit::insertLink() "", linkText, linkUrl, + false, this); if (dialog.exec() == QDialog::Accepted) { linkText = dialog.getLinkText(); diff --git a/src/veditarea.cpp b/src/veditarea.cpp index d445401e..71099606 100644 --- a/src/veditarea.cpp +++ b/src/veditarea.cpp @@ -570,6 +570,20 @@ VEditTab *VEditArea::getTab(int p_winIdx, int p_tabIdx) const return win->getTab(p_tabIdx); } +VEditTab *VEditArea::getTab(const VFile *p_file) const +{ + int nrWin = splitter->count(); + for (int winIdx = 0; winIdx < nrWin; ++winIdx) { + VEditWindow *win = getWindow(winIdx); + int tabIdx = win->findTabByFile(p_file); + if (tabIdx != -1) { + return win->getTab(tabIdx); + } + } + + return NULL; +} + QVector VEditArea::getAllTabsInfo() const { QVector tabs; diff --git a/src/veditarea.h b/src/veditarea.h index 9900f0d8..e8b0dc46 100644 --- a/src/veditarea.h +++ b/src/veditarea.h @@ -44,6 +44,9 @@ public: // Return the @p_tabIdx tab in the @p_winIdx window. VEditTab *getTab(int p_winIdx, int p_tabIdx) const; + // Return the tab for @p_file file. + VEditTab *getTab(const VFile *p_file) const; + // Return VEditTabInfo of all edit tabs. QVector getAllTabsInfo() const; diff --git a/src/vmainwindow.cpp b/src/vmainwindow.cpp index e1ca6535..b75ba839 100644 --- a/src/vmainwindow.cpp +++ b/src/vmainwindow.cpp @@ -38,6 +38,7 @@ #include "dialog/vtipsdialog.h" #include "vcart.h" #include "dialog/vexportdialog.h" +#include "vsearcher.h" extern VConfigManager *g_config; @@ -148,6 +149,7 @@ void VMainWindow::registerCaptainAndNavigationTargets() m_captain->registerNavigationTarget(m_toolBox); m_captain->registerNavigationTarget(outline); m_captain->registerNavigationTarget(m_snippetList); + m_captain->registerNavigationTarget(m_searcher); // Register Captain mode targets. m_captain->registerCaptainTarget(tr("AttachmentList"), @@ -174,6 +176,10 @@ void VMainWindow::registerCaptainAndNavigationTargets() g_config->getCaptainShortcutKeySequence("ToolsDock"), this, toggleToolsDockByCaptain); + m_captain->registerCaptainTarget(tr("SearchDock"), + g_config->getCaptainShortcutKeySequence("SearchDock"), + this, + toggleSearchDockByCaptain); m_captain->registerCaptainTarget(tr("CloseNote"), g_config->getCaptainShortcutKeySequence("CloseNote"), this, @@ -1073,6 +1079,17 @@ void VMainWindow::initEditMenu() connect(m_findReplaceAct, &QAction::triggered, this, &VMainWindow::openFindDialog); + QAction *advFindAct = new QAction(tr("Advanced Find"), this); + advFindAct->setToolTip(tr("Advanced find within VNote")); + keySeq = g_config->getShortcutKeySequence("AdvancedFind"); + qDebug() << "set AdvancedFind shortcut to" << keySeq; + advFindAct->setShortcut(QKeySequence(keySeq)); + connect(advFindAct, &QAction::triggered, + this, [this]() { + m_searchDock->setVisible(true); + m_searcher->focusToSearch(); + }); + m_findNextAct = new QAction(tr("Find Next"), this); m_findNextAct->setToolTip(tr("Find next occurence")); keySeq = g_config->getShortcutKeySequence("FindNext"); @@ -1188,6 +1205,8 @@ void VMainWindow::initEditMenu() QMenu *findReplaceMenu = editMenu->addMenu(tr("Find/Replace")); findReplaceMenu->setToolTipsVisible(true); findReplaceMenu->addAction(m_findReplaceAct); + findReplaceMenu->addAction(advFindAct); + findReplaceMenu->addSeparator(); findReplaceMenu->addAction(m_findNextAct); findReplaceMenu->addAction(m_findPreviousAct); findReplaceMenu->addAction(m_replaceAct); @@ -1268,9 +1287,22 @@ void VMainWindow::initEditMenu() void VMainWindow::initDockWindows() { - toolDock = new QDockWidget(tr("Tools"), this); - toolDock->setObjectName("ToolsDock"); - toolDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); + setTabPosition(Qt::LeftDockWidgetArea, QTabWidget::West); + setTabPosition(Qt::RightDockWidgetArea, QTabWidget::East); + setTabPosition(Qt::TopDockWidgetArea, QTabWidget::North); + setTabPosition(Qt::BottomDockWidgetArea, QTabWidget::South); + + setDockNestingEnabled(true); + + initToolsDock(); + initSearchDock(); +} + +void VMainWindow::initToolsDock() +{ + m_toolDock = new QDockWidget(tr("Tools"), this); + m_toolDock->setObjectName("ToolsDock"); + m_toolDock->setAllowedAreas(Qt::AllDockWidgetAreas); // Outline tree. outline = new VOutline(this); @@ -1298,16 +1330,35 @@ void VMainWindow::initDockWindows() ":/resources/icons/cart.svg", tr("Cart")); - toolDock->setWidget(m_toolBox); - addDockWidget(Qt::RightDockWidgetArea, toolDock); + m_toolDock->setWidget(m_toolBox); + addDockWidget(Qt::RightDockWidgetArea, m_toolDock); - QAction *toggleAct = toolDock->toggleViewAction(); + QAction *toggleAct = m_toolDock->toggleViewAction(); toggleAct->setToolTip(tr("Toggle the tools dock widget")); VUtils::fixTextWithCaptainShortcut(toggleAct, "ToolsDock"); m_viewMenu->addAction(toggleAct); } +void VMainWindow::initSearchDock() +{ + m_searchDock = new QDockWidget(tr("Search"), this); + m_searchDock->setObjectName("SearchDock"); + m_searchDock->setAllowedAreas(Qt::AllDockWidgetAreas); + + m_searcher = new VSearcher(this); + + m_searchDock->setWidget(m_searcher); + + addDockWidget(Qt::RightDockWidgetArea, m_searchDock); + + QAction *toggleAct = m_searchDock->toggleViewAction(); + toggleAct->setToolTip(tr("Toggle the search dock widget")); + VUtils::fixTextWithCaptainShortcut(toggleAct, "SearchDock"); + + m_viewMenu->addAction(toggleAct); +} + void VMainWindow::importNoteFromFile() { static QString lastPath = QDir::homePath(); @@ -2162,7 +2213,8 @@ void VMainWindow::saveStateAndGeometry() { g_config->setMainWindowGeometry(saveGeometry()); g_config->setMainWindowState(saveState()); - g_config->setToolsDockChecked(toolDock->isVisible()); + g_config->setToolsDockChecked(m_toolDock->isVisible()); + g_config->setSearchDockChecked(m_searchDock->isVisible()); if (m_panelViewState == PanelViewState::CompactMode) { g_config->setNaviSplitterState(m_naviSplitter->saveState()); @@ -2181,11 +2233,14 @@ void VMainWindow::restoreStateAndGeometry() if (!geometry.isEmpty()) { restoreGeometry(geometry); } + const QByteArray &state = g_config->getMainWindowState(); if (!state.isEmpty()) { restoreState(state); } - toolDock->setVisible(g_config->getToolsDockChecked()); + + m_toolDock->setVisible(g_config->getToolsDockChecked()); + m_searchDock->setVisible(g_config->getSearchDockChecked()); const QByteArray &splitterState = g_config->getMainSplitterState(); if (!splitterState.isEmpty()) { @@ -2260,6 +2315,32 @@ bool VMainWindow::locateFile(VFile *p_file) return ret; } +bool VMainWindow::locateDirectory(VDirectory *p_directory) +{ + bool ret = false; + if (!p_directory) { + return ret; + } + + VNotebook *notebook = p_directory->getNotebook(); + if (notebookSelector->locateNotebook(notebook)) { + while (directoryTree->currentNotebook() != notebook) { + QCoreApplication::sendPostedEvents(); + } + + ret = directoryTree->locateDirectory(p_directory); + } + + // Open the directory and file panels after location. + if (m_panelViewState == PanelViewState::CompactMode) { + compactModeView(); + } else { + twoPanelView(); + } + + return ret; +} + void VMainWindow::handleFindDialogTextChanged(const QString &p_text, uint /* p_options */) { bool enabled = true; @@ -2659,7 +2740,21 @@ bool VMainWindow::toggleToolsDockByCaptain(void *p_target, void *p_data) { Q_UNUSED(p_data); VMainWindow *obj = static_cast(p_target); - obj->toolDock->setVisible(!obj->toolDock->isVisible()); + obj->m_toolDock->setVisible(!obj->m_toolDock->isVisible()); + return true; +} + +bool VMainWindow::toggleSearchDockByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VMainWindow *obj = static_cast(p_target); + bool visible = obj->m_searchDock->isVisible(); + obj->m_searchDock->setVisible(!visible); + if (!visible) { + obj->m_searcher->focusToSearch(); + return false; + } + return true; } diff --git a/src/vmainwindow.h b/src/vmainwindow.h index e41d7de3..a74c2a52 100644 --- a/src/vmainwindow.h +++ b/src/vmainwindow.h @@ -39,6 +39,7 @@ class VButtonWithWidget; class VAttachmentList; class VSnippetList; class VCart; +class VSearcher; class QPrinter; enum class PanelViewState @@ -60,6 +61,9 @@ public: // Returns true if the location succeeds. bool locateFile(VFile *p_file); + // Returns true if the location succeeds. + bool locateDirectory(VDirectory *p_directory); + VFileList *getFileList() const; VEditArea *getEditArea() const; @@ -68,6 +72,10 @@ public: VCart *getCart() const; + VDirectoryTree *getDirectoryTree() const; + + VNotebookSelector *getNotebookSelector() const; + // View and edit the information of @p_file, which is an orphan file. void editOrphanFileInfo(VFile *p_file); @@ -205,6 +213,10 @@ private: void initDockWindows(); + void initToolsDock(); + + void initSearchDock(); + void initRenderBackgroundMenu(QMenu *menu); void initRenderStyleMenu(QMenu *p_menu); @@ -280,6 +292,8 @@ private: static bool toggleToolsDockByCaptain(void *p_target, void *p_data); + static bool toggleSearchDockByCaptain(void *p_target, void *p_data); + static bool closeFileByCaptain(void *p_target, void *p_data); static bool shortcutsHelpByCaptain(void *p_target, void *p_data); @@ -311,7 +325,9 @@ private: VEditArea *editArea; - QDockWidget *toolDock; + QDockWidget *m_toolDock; + + QDockWidget *m_searchDock; // Tool box in the dock widget. VToolBox *m_toolBox; @@ -324,6 +340,9 @@ private: // View and manage cart. VCart *m_cart; + // Advanced search. + VSearcher *m_searcher; + VFindReplaceDialog *m_findReplaceDialog; VVimCmdLineEdit *m_vimCmd; @@ -453,4 +472,13 @@ inline VCart *VMainWindow::getCart() const return m_cart; } +inline VDirectoryTree *VMainWindow::getDirectoryTree() const +{ + return directoryTree; +} + +inline VNotebookSelector *VMainWindow::getNotebookSelector() const +{ + return notebookSelector; +} #endif // VMAINWINDOW_H diff --git a/src/vmdeditor.cpp b/src/vmdeditor.cpp index e5661434..ad189bc3 100644 --- a/src/vmdeditor.cpp +++ b/src/vmdeditor.cpp @@ -95,6 +95,8 @@ VMdEditor::VMdEditor(VFile *p_file, connect(this, &VTextEdit::cursorPositionChanged, this, &VMdEditor::updateCurrentHeader); + setDisplayScaleFactor(VUtils::calculateScaleFactor()); + updateFontAndPalette(); updateConfig(); diff --git a/src/vnote.cpp b/src/vnote.cpp index 2300086a..204be5cf 100644 --- a/src/vnote.cpp +++ b/src/vnote.cpp @@ -314,6 +314,17 @@ VDirectory *VNote::getInternalDirectory(const QString &p_path) } +VNotebook *VNote::getNotebook(const QString &p_path) +{ + for (auto & nb : m_notebooks) { + if (VUtils::equalPath(nb->getPath(), p_path)) { + return nb; + } + } + + return NULL; +} + void VNote::freeOrphanFiles() { for (int i = 0; i < m_externalFiles.size();) { diff --git a/src/vnote.h b/src/vnote.h index 7a068299..a5b6dc61 100644 --- a/src/vnote.h +++ b/src/vnote.h @@ -95,6 +95,10 @@ public: // Otherwise, returns NULL. VDirectory *getInternalDirectory(const QString &p_path); + // Given the path of a file, try to find it in all notebooks. + // Returns a VNotebook struct if it is the root folder of a notebook. + VNotebook *getNotebook(const QString &p_path); + void freeOrphanFiles(); // @p_renderBg: background color, empty to not specify given color. diff --git a/src/vnote.qrc b/src/vnote.qrc index 76a18e11..d4af266b 100644 --- a/src/vnote.qrc +++ b/src/vnote.qrc @@ -243,5 +243,10 @@ resources/themes/v_moonlight/arrow_dropdown_disabled.svg resources/themes/v_pure/arrow_dropdown_disabled.svg resources/themes/v_white/arrow_dropdown_disabled.svg + resources/icons/clear_search.svg + resources/icons/search.svg + resources/icons/search_advanced.svg + resources/icons/search_console.svg + resources/icons/note_item.svg diff --git a/src/vopenedlistmenu.cpp b/src/vopenedlistmenu.cpp index d0f7d082..016bf5d1 100644 --- a/src/vopenedlistmenu.cpp +++ b/src/vopenedlistmenu.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include "veditwindow.h" #include "vnotefile.h" diff --git a/src/vsearch.cpp b/src/vsearch.cpp new file mode 100644 index 00000000..3bf00c3e --- /dev/null +++ b/src/vsearch.cpp @@ -0,0 +1,418 @@ +#include "vsearch.h" + +#include "utils/vutils.h" +#include "vfile.h" +#include "vdirectory.h" +#include "vnotebook.h" +#include "veditarea.h" +#include "vmainwindow.h" +#include "vtableofcontent.h" +#include "vsearchengine.h" + +extern VMainWindow *g_mainWin; + +VSearch::VSearch(QObject *p_parent) + : QObject(p_parent), + m_askedToStop(false), + m_engine(NULL) +{ +} + +QSharedPointer VSearch::search(const QVector &p_files) +{ + Q_ASSERT(!askedToStop()); + + QSharedPointer result(new VSearchResult(this)); + + if (p_files.isEmpty()) { + return result; + } + + if (!testTarget(VSearchConfig::Note)) { + qDebug() << "search is not applicable for note"; + result->m_state = VSearchState::Success; + return result; + } + + result->m_state = VSearchState::Busy; + + for (auto const & it : p_files) { + if (!it) { + continue; + } + + searchFirstPhase(it, result, true); + + if (askedToStop()) { + qDebug() << "asked to cancel the search"; + result->m_state = VSearchState::Cancelled; + break; + } + } + + if (result->m_state == VSearchState::Busy) { + result->m_state = VSearchState::Success; + } + + return result; +} + +QSharedPointer VSearch::search(VDirectory *p_directory) +{ + Q_ASSERT(!askedToStop()); + + QSharedPointer result(new VSearchResult(this)); + + if (!p_directory) { + return result; + } + + if ((!testTarget(VSearchConfig::Note) + && !testTarget(VSearchConfig::Folder)) + || testObject(VSearchConfig::Outline)) { + qDebug() << "search is not applicable for folder"; + result->m_state = VSearchState::Success; + return result; + } + + result->m_state = VSearchState::Busy; + + searchFirstPhase(p_directory, result); + + if (result->hasSecondPhaseItems()) { + searchSecondPhase(result); + } else if (result->m_state == VSearchState::Busy) { + result->m_state = VSearchState::Success; + } + + return result; +} + +QSharedPointer VSearch::search(const QVector &p_notebooks) +{ + Q_ASSERT(!askedToStop()); + + QSharedPointer result(new VSearchResult(this)); + + if (p_notebooks.isEmpty()) { + return result; + } + + if (testObject(VSearchConfig::Outline)) { + qDebug() << "search is not applicable for notebook"; + result->m_state = VSearchState::Success; + return result; + } + + result->m_state = VSearchState::Busy; + + for (auto const & nb : p_notebooks) { + if (!nb) { + continue; + } + + searchFirstPhase(nb, result); + + if (askedToStop()) { + qDebug() << "asked to cancel the search"; + result->m_state = VSearchState::Cancelled; + break; + } + } + + if (result->hasSecondPhaseItems()) { + searchSecondPhase(result); + } else if (result->m_state == VSearchState::Busy) { + result->m_state = VSearchState::Success; + } + + return result; +} + +void VSearch::searchFirstPhase(VFile *p_file, + const QSharedPointer &p_result, + bool p_searchContent) +{ + Q_ASSERT(testTarget(VSearchConfig::Note)); + + QString name = p_file->getName(); + if (!m_patternReg.isEmpty()) { + if (!matchOneLine(name, m_patternReg)) { + return; + } + } + + QString filePath = p_file->fetchPath(); + if (testObject(VSearchConfig::Name)) { + if (matchOneLine(name, m_searchReg)) { + VSearchResultItem *item = new VSearchResultItem(VSearchResultItem::Note, + VSearchResultItem::LineNumber, + name, + filePath); + QSharedPointer pitem(item); + emit resultItemAdded(pitem); + } + } + + if (testObject(VSearchConfig::Outline)) { + VSearchResultItem *item = searchForOutline(p_file); + if (item) { + QSharedPointer pitem(item); + emit resultItemAdded(pitem); + } + } + + if (testObject(VSearchConfig::Content)) { + // Search content in first phase. + if (p_searchContent) { + VSearchResultItem *item = searchForContent(p_file); + if (item) { + QSharedPointer pitem(item); + emit resultItemAdded(pitem); + } + } else { + // Add an item for second phase process. + p_result->addSecondPhaseItem(filePath); + } + } +} + +void VSearch::searchFirstPhase(VDirectory *p_directory, + const QSharedPointer &p_result) +{ + if (!testTarget(VSearchConfig::Note) + && !testTarget(VSearchConfig::Folder)) { + return; + } + + bool opened = p_directory->isOpened(); + if (!opened && !p_directory->open()) { + p_result->logError(QString("Fail to open folder %1.").arg(p_directory->fetchRelativePath())); + p_result->m_state = VSearchState::Fail; + return; + } + + if (testTarget(VSearchConfig::Folder) + && testObject(VSearchConfig::Name)) { + QString text = p_directory->getName(); + if (matchOneLine(text, m_searchReg)) { + VSearchResultItem *item = new VSearchResultItem(VSearchResultItem::Folder, + VSearchResultItem::LineNumber, + text, + p_directory->fetchPath()); + QSharedPointer pitem(item); + emit resultItemAdded(pitem); + } + } + + // Search files. + if (testTarget(VSearchConfig::Note)) { + for (auto const & file : p_directory->getFiles()) { + if (askedToStop()) { + qDebug() << "asked to cancel the search"; + p_result->m_state = VSearchState::Cancelled; + goto exit; + } + + searchFirstPhase(file, p_result); + } + } + + // Search subfolders. + for (auto const & dir : p_directory->getSubDirs()) { + if (askedToStop()) { + qDebug() << "asked to cancel the search"; + p_result->m_state = VSearchState::Cancelled; + goto exit; + } + + searchFirstPhase(dir, p_result); + } + +exit: + if (!opened) { + p_directory->close(); + } +} + +void VSearch::searchFirstPhase(VNotebook *p_notebook, + const QSharedPointer &p_result) +{ + bool opened = p_notebook->isOpened(); + if (!opened && !p_notebook->open()) { + p_result->logError(QString("Fail to open notebook %1.").arg(p_notebook->getName())); + p_result->m_state = VSearchState::Fail; + return; + } + + if (testTarget(VSearchConfig::Notebook) + && testObject(VSearchConfig::Name)) { + QString text = p_notebook->getName(); + if (matchOneLine(text, m_searchReg)) { + VSearchResultItem *item = new VSearchResultItem(VSearchResultItem::Notebook, + VSearchResultItem::LineNumber, + text, + p_notebook->getPath()); + QSharedPointer pitem(item); + emit resultItemAdded(pitem); + } + } + + if (!testTarget(VSearchConfig::Note) + && !testTarget(VSearchConfig::Folder)) { + goto exit; + } + + // Search for subfolders. + for (auto const & dir : p_notebook->getRootDir()->getSubDirs()) { + if (askedToStop()) { + qDebug() << "asked to cancel the search"; + p_result->m_state = VSearchState::Cancelled; + goto exit; + } + + searchFirstPhase(dir, p_result); + } + +exit: + if (!opened) { + p_notebook->close(); + } +} + +VSearchResultItem *VSearch::searchForOutline(const VFile *p_file) const +{ + VEditTab *tab = g_mainWin->getEditArea()->getTab(p_file); + if (!tab) { + return NULL; + } + + const VTableOfContent &toc = tab->getOutline(); + const QVector &table = toc.getTable(); + VSearchResultItem *item = NULL; + for (auto const & it: table) { + if (it.isEmpty()) { + continue; + } + + if (!matchOneLine(it.m_name, m_searchReg)) { + continue; + } + + if (!item) { + item = new VSearchResultItem(VSearchResultItem::Note, + VSearchResultItem::OutlineIndex, + p_file->getName(), + p_file->fetchPath()); + } + + VSearchResultSubItem sitem(it.m_index, it.m_name); + item->m_matches.append(sitem); + } + + return item; +} + +VSearchResultItem *VSearch::searchForContent(const VFile *p_file) const +{ + Q_ASSERT(p_file->isOpened()); + const QString &content = p_file->getContent(); + if (content.isEmpty()) { + return NULL; + } + + VSearchResultItem *item = NULL; + int lineNum = 1; + int pos = 0; + int size = content.size(); + QRegExp newLineReg = QRegExp("\\n|\\r\\n|\\r"); + Qt::CaseSensitivity cs = testOption(VSearchConfig::CaseSensitive) + ? Qt::CaseSensitive : Qt::CaseInsensitive; + while (pos < size) { + int idx = content.indexOf(newLineReg, pos); + if (idx == -1) { + idx = size; + } + + if (idx > pos) { + QString lineText = content.mid(pos, idx - pos); + bool matched = false; + if (m_contentSearchReg.isEmpty()) { + matched = lineText.contains(m_config->m_keyword, cs); + } else { + matched = (m_contentSearchReg.indexIn(lineText) != -1); + } + + if (matched) { + if (!item) { + item = new VSearchResultItem(VSearchResultItem::Note, + VSearchResultItem::LineNumber, + p_file->getName(), + p_file->fetchPath()); + } + + VSearchResultSubItem sitem(lineNum, lineText); + item->m_matches.append(sitem); + } + } + + if (idx == size) { + break; + } + + pos = idx + newLineReg.matchedLength(); + ++lineNum; + } + + return item; +} + +void VSearch::searchSecondPhase(const QSharedPointer &p_result) +{ + delete m_engine; + m_engine = NULL; + + switch (m_config->m_engine) { + case VSearchConfig::Internal: + { + m_engine = new VSearchEngine(this); + m_engine->search(m_config, p_result); + break; + } + + default: + p_result->m_state = VSearchState::Success; + break; + } + + if (m_engine) { + connect(m_engine, &ISearchEngine::finished, + this, &VSearch::finished); + connect(m_engine, &ISearchEngine::resultItemAdded, + this, &VSearch::resultItemAdded); + } +} + +void VSearch::clear() +{ + m_config.clear(); + + if (m_engine) { + m_engine->clear(); + + delete m_engine; + m_engine = NULL; + } + + m_askedToStop = false; +} + +void VSearch::stop() +{ + qDebug() << "VSearch asked to stop"; + m_askedToStop = true; + + if (m_engine) { + m_engine->stop(); + } +} diff --git a/src/vsearch.h b/src/vsearch.h new file mode 100644 index 00000000..24e43b16 --- /dev/null +++ b/src/vsearch.h @@ -0,0 +1,162 @@ +#ifndef VSEARCH_H +#define VSEARCH_H + +#include +#include +#include +#include +#include + +#include "vsearchconfig.h" + +class VFile; +class VDirectory; +class VNotebook; +class ISearchEngine; + + +class VSearch : public QObject +{ + Q_OBJECT +public: + explicit VSearch(QObject *p_parent = nullptr); + + void setConfig(QSharedPointer p_config); + + // Search list of files for CurrentNote and OpenedNotes. + QSharedPointer search(const QVector &p_files); + + // Search folder for CurrentFolder. + QSharedPointer search(VDirectory *p_directory); + + // Search folder for CurrentNotebook and AllNotebooks. + QSharedPointer search(const QVector &p_notebooks); + + // Clear resources after a search completed. + void clear(); + + void stop(); + +signals: + // Emitted when a new item added as result. + void resultItemAdded(const QSharedPointer &p_item); + + // Emitted when async task finished. + void finished(const QSharedPointer &p_result); + +private: + bool askedToStop() const; + + // @p_searchContent: whether search content in first phase. + void searchFirstPhase(VFile *p_file, + const QSharedPointer &p_result, + bool p_searchContent = false); + + void searchFirstPhase(VDirectory *p_directory, + const QSharedPointer &p_result); + + void searchFirstPhase(VNotebook *p_notebook, + const QSharedPointer &p_result); + + bool testTarget(VSearchConfig::Target p_target) const; + + bool testObject(VSearchConfig::Object p_object) const; + + bool testOption(VSearchConfig::Option p_option) const; + + bool matchOneLine(const QString &p_text, const QRegExp &p_reg) const; + + VSearchResultItem *searchForOutline(const VFile *p_file) const; + + VSearchResultItem *searchForContent(const VFile *p_file) const; + + void searchSecondPhase(const QSharedPointer &p_result); + + bool m_askedToStop; + + QSharedPointer m_config; + + ISearchEngine *m_engine; + + // Search reg used for name, outline, tag. + QRegExp m_searchReg; + + // Search reg used for content. + // We use raw string to speed up if it is empty. + QRegExp m_contentSearchReg; + + // Wildcard reg to for file name pattern. + QRegExp m_patternReg; +}; + +inline bool VSearch::askedToStop() const +{ + QCoreApplication::processEvents(); + return m_askedToStop; +} + +inline void VSearch::setConfig(QSharedPointer p_config) +{ + m_config = p_config; + + // Compile reg. + const QString &keyword = m_config->m_keyword; + m_contentSearchReg = QRegExp(); + if (keyword.isEmpty()) { + m_searchReg = QRegExp(); + return; + } + + Qt::CaseSensitivity cs = testOption(VSearchConfig::CaseSensitive) + ? Qt::CaseSensitive : Qt::CaseInsensitive; + if (testOption(VSearchConfig::RegularExpression)) { + m_searchReg = QRegExp(keyword, cs); + m_contentSearchReg = QRegExp(keyword, cs); + } else { + if (testOption(VSearchConfig::Fuzzy)) { + QString wildcardText(keyword.size() * 2 + 1, '*'); + for (int i = 0, j = 1; i < keyword.size(); ++i, j += 2) { + wildcardText[j] = keyword[i]; + } + + m_searchReg = QRegExp(wildcardText, cs, QRegExp::Wildcard); + } else { + QString pattern = QRegExp::escape(keyword); + if (testOption(VSearchConfig::WholeWordOnly)) { + pattern = "\\b" + pattern + "\\b"; + + // We only use m_contentSearchReg when WholeWordOnly is checked. + m_contentSearchReg = QRegExp(pattern, cs); + } + + m_searchReg = QRegExp(pattern, cs); + } + } + + if (m_config->m_pattern.isEmpty()) { + m_patternReg = QRegExp(); + } else { + m_patternReg = QRegExp(m_config->m_pattern, Qt::CaseInsensitive, QRegExp::Wildcard); + } +} + +inline bool VSearch::testTarget(VSearchConfig::Target p_target) const +{ + return p_target & m_config->m_target; +} + +inline bool VSearch::testObject(VSearchConfig::Object p_object) const +{ + return p_object & m_config->m_object; +} + +inline bool VSearch::testOption(VSearchConfig::Option p_option) const +{ + return p_option & m_config->m_option; +} + +inline bool VSearch::matchOneLine(const QString &p_text, const QRegExp &p_reg) const +{ + return p_reg.indexIn(p_text) != -1; +} +#endif // VSEARCH_H diff --git a/src/vsearchconfig.h b/src/vsearchconfig.h new file mode 100644 index 00000000..8281e5f9 --- /dev/null +++ b/src/vsearchconfig.h @@ -0,0 +1,273 @@ +#ifndef VSEARCHCONFIG_H +#define VSEARCHCONFIG_H + +#include +#include +#include + + +struct VSearchConfig +{ + enum Scope + { + NoneScope = 0, + CurrentNote, + OpenedNotes, + CurrentFolder, + CurrentNotebook, + AllNotebooks + }; + + enum Object + { + NoneObject = 0, + Name = 0x1UL, + Content = 0x2UL, + Outline = 0x4UL, + Tag = 0x8UL + }; + + enum Target + { + NoneTarget = 0, + Note = 0x1UL, + Folder = 0x2UL, + Notebook = 0x4UL + }; + + enum Engine + { + Internal = 0 + }; + + enum Option + { + NoneOption = 0, + CaseSensitive = 0x1UL, + WholeWordOnly = 0x2UL, + Fuzzy = 0x4UL, + RegularExpression = 0x8UL + }; + + VSearchConfig() + : VSearchConfig(Scope::NoneScope, + Object::NoneObject, + Target::NoneTarget, + Engine::Internal, + Option::NoneOption, + "", + "") + { + } + + + VSearchConfig(int p_scope, + int p_object, + int p_target, + int p_engine, + int p_option, + const QString &p_keyword, + const QString &p_pattern) + : m_scope(p_scope), + m_object(p_object), + m_target(p_target), + m_engine(p_engine), + m_option(p_option), + m_keyword(p_keyword), + m_pattern(p_pattern) + { + } + + QStringList toConfig() const + { + QStringList str; + str << QString::number(m_scope); + str << QString::number(m_object); + str << QString::number(m_target); + str << QString::number(m_engine); + str << QString::number(m_option); + str << m_pattern; + + return str; + } + + static VSearchConfig fromConfig(const QStringList &p_str) + { + VSearchConfig config; + if (p_str.size() != 6) { + return config; + } + + config.m_scope = p_str[0].toInt(); + config.m_object = p_str[1].toInt(); + config.m_target = p_str[2].toInt(); + config.m_engine = p_str[3].toInt(); + config.m_option = p_str[4].toInt(); + config.m_pattern = p_str[5]; + + return config; + } + + int m_scope; + int m_object; + int m_target; + int m_engine; + int m_option; + + QString m_keyword; + + // Wildcard pattern to filter file. + QString m_pattern; +}; + + +struct VSearchResultSubItem +{ + VSearchResultSubItem() + : m_lineNumber(-1) + { + } + + VSearchResultSubItem(int p_lineNumber, + const QString &p_text) + : m_lineNumber(p_lineNumber), + m_text(p_text) + { + } + + int m_lineNumber; + + QString m_text; +}; + + +struct VSearchResultItem +{ + enum ItemType + { + None = 0, + Note, + Folder, + Notebook + }; + + + enum MatchType + { + LineNumber = 0, + OutlineIndex + }; + + + VSearchResultItem() + : m_type(ItemType::None), + m_matchType(MatchType::LineNumber) + { + } + + VSearchResultItem(VSearchResultItem::ItemType p_type, + VSearchResultItem::MatchType p_matchType, + const QString &p_text, + const QString &p_path) + : m_type(p_type), + m_matchType(p_matchType), + m_text(p_text), + m_path(p_path) + { + } + + bool isEmpty() const + { + return m_type == ItemType::None; + } + + QString toString() const + { + return QString("item text: [%1] path: [%2] subitems: %3") + .arg(m_text) + .arg(m_path) + .arg(m_matches.size()); + } + + + ItemType m_type; + + MatchType m_matchType; + + // Text to displayed. If empty, use @m_path instead. + QString m_text; + + // Path of the target. + QString m_path; + + // Matched places within this item. + QList m_matches; +}; + + +class VSearch; + + +enum class VSearchState +{ + Idle = 0, + Busy, + Success, + Fail, + Cancelled +}; + + +struct VSearchResult +{ + friend class VSearch; + + VSearchResult(VSearch *p_search) + : m_state(VSearchState::Idle), + m_search(p_search) + { + } + + bool hasError() const + { + return !m_errMsg.isEmpty(); + } + + void logError(const QString &p_err) + { + if (m_errMsg.isEmpty()) { + m_errMsg = p_err; + } else { + m_errMsg = "\n" + p_err; + } + } + + void addSecondPhaseItem(const QString &p_item) + { + m_secondPhaseItems.append(p_item); + } + + QString toString() const + { + QString str = QString("search result: state %1 err %2") + .arg((int)m_state) + .arg(!m_errMsg.isEmpty()); + return str; + } + + bool hasSecondPhaseItems() const + { + return !m_secondPhaseItems.isEmpty(); + } + + VSearchState m_state; + + QString m_errMsg; + + QStringList m_secondPhaseItems; + +private: + VSearch *m_search; +}; + +#endif // VSEARCHCONFIG_H diff --git a/src/vsearchengine.cpp b/src/vsearchengine.cpp new file mode 100644 index 00000000..669888ef --- /dev/null +++ b/src/vsearchengine.cpp @@ -0,0 +1,255 @@ +#include "vsearchengine.h" + +#include +#include +#include + +#include "utils/vutils.h" + +VSearchEngineWorker::VSearchEngineWorker(QObject *p_parent) + : QThread(p_parent), + m_stop(0), + m_state(VSearchState::Idle) +{ +} + +void VSearchEngineWorker::setData(const QStringList &p_files, + const QRegExp &p_reg, + const QString &p_keyword, + Qt::CaseSensitivity p_cs) +{ + m_files = p_files; + m_reg = p_reg; + m_keyword = p_keyword; + m_caseSensitivity = p_cs; +} + +void VSearchEngineWorker::stop() +{ + m_stop.store(1); +} + +void VSearchEngineWorker::run() +{ + qDebug() << "worker" << QThread::currentThreadId() << m_files.size(); + + QMimeDatabase mimeDatabase; + m_state = VSearchState::Busy; + + for (auto const & fileName : m_files) { + if (m_stop.load() == 1) { + m_state = VSearchState::Cancelled; + qDebug() << "worker" << QThread::currentThreadId() << "is asked to stop"; + break; + } + + const QMimeType mimeType = mimeDatabase.mimeTypeForFile(fileName); + if (mimeType.isValid() && !mimeType.inherits(QStringLiteral("text/plain"))) { + appendError(tr("Skip binary file %1.").arg(fileName)); + continue; + } + + VSearchResultItem *item = searchFile(fileName); + if (item) { + emit resultItemReady(item); + } + } + + if (m_state == VSearchState::Busy) { + m_state = VSearchState::Success; + } +} + +VSearchResultItem *VSearchEngineWorker::searchFile(const QString &p_fileName) +{ + QFile file(p_fileName); + if (!file.open(QIODevice::ReadOnly)) { + return NULL; + } + + int lineNum = 1; + VSearchResultItem *item = NULL; + QString line; + QTextStream in(&file); + while (!in.atEnd()) { + if (m_stop.load() == 1) { + m_state = VSearchState::Cancelled; + qDebug() << "worker" << QThread::currentThreadId() << "is asked to stop"; + break; + } + + bool matched = false; + line = in.readLine(); + if (m_reg.isEmpty()) { + if (line.contains(m_keyword, m_caseSensitivity)) { + matched = true; + } + } else if (m_reg.indexIn(line) != -1) { + matched = true; + } + + if (matched) { + if (!item) { + item = new VSearchResultItem(VSearchResultItem::Note, + VSearchResultItem::LineNumber, + VUtils::fileNameFromPath(p_fileName), + p_fileName); + } + + VSearchResultSubItem sitem(lineNum, line); + item->m_matches.append(sitem); + } + + ++lineNum; + } + + return item; +} + + +VSearchEngine::VSearchEngine(QObject *p_parent) + : ISearchEngine(p_parent), + m_finishedWorkers(0) +{ +} + +void VSearchEngine::search(const QSharedPointer &p_config, + const QSharedPointer &p_result) +{ + int numThread = QThread::idealThreadCount(); + if (numThread < 1) { + numThread = 1; + } + + const QStringList items = p_result->m_secondPhaseItems; + Q_ASSERT(!items.isEmpty()); + if (items.size() < numThread) { + numThread = items.size(); + } + + m_result = p_result; + + QRegExp reg = compileRegExpFromConfig(p_config); + Qt::CaseSensitivity cs = (p_config->m_option & VSearchConfig::CaseSensitive) + ? Qt::CaseSensitive : Qt::CaseInsensitive; + + clearAllWorkers(); + m_workers.reserve(numThread); + m_finishedWorkers = 0; + int totalSize = m_result->m_secondPhaseItems.size(); + int step = totalSize / numThread; + int remain = totalSize % numThread; + + for (int i = 0; i < numThread; ++i) { + int start = i * step; + if (start >= totalSize) { + break; + } + + int len = step; + if (remain) { + ++len; + --remain; + } + + if (start + len > totalSize) { + len = totalSize - start; + } + + VSearchEngineWorker *th = new VSearchEngineWorker(this); + th->setData(m_result->m_secondPhaseItems.mid(start, len), + reg, + p_config->m_keyword, + cs); + connect(th, &VSearchEngineWorker::finished, + this, &VSearchEngine::handleWorkerFinished); + connect(th, &VSearchEngineWorker::resultItemReady, + this, [this](VSearchResultItem *p_item) { + emit resultItemAdded(QSharedPointer(p_item)); + }); + + m_workers.append(th); + th->start(); + } + + qDebug() << "schedule tasks to threads" << m_workers.size() << totalSize << step; +} + +QRegExp VSearchEngine::compileRegExpFromConfig(const QSharedPointer &p_config) const +{ + const QString &keyword = p_config->m_keyword; + Qt::CaseSensitivity cs = (p_config->m_option & VSearchConfig::CaseSensitive) + ? Qt::CaseSensitive : Qt::CaseInsensitive; + if (p_config->m_option & VSearchConfig::RegularExpression) { + return QRegExp(keyword, cs); + } else if (p_config->m_option & VSearchConfig::WholeWordOnly) { + QString pattern = QRegExp::escape(keyword); + pattern = "\\b" + pattern + "\\b"; + return QRegExp(pattern, cs); + } else { + return QRegExp(); + } +} + +void VSearchEngine::stop() +{ + qDebug() << "VSearchEngine asked to stop"; + for (auto const & th : m_workers) { + th->stop(); + } +} + +void VSearchEngine::handleWorkerFinished() +{ + ++m_finishedWorkers; + + qDebug() << m_finishedWorkers << "workers finished"; + if (m_finishedWorkers == m_workers.size()) { + VSearchState state = VSearchState::Success; + + for (auto const & th : m_workers) { + if (th->m_state == VSearchState::Fail) { + if (state != VSearchState::Cancelled) { + state = VSearchState::Fail; + } + } else if (th->m_state == VSearchState::Cancelled) { + state = VSearchState::Cancelled; + } + + if (!th->m_error.isEmpty()) { + m_result->logError(th->m_error); + } + + Q_ASSERT(th->isFinished()); + th->deleteLater(); + } + + m_workers.clear(); + m_finishedWorkers = 0; + + m_result->m_state = state; + qDebug() << "SearchEngine finished" << (int)state; + emit finished(m_result); + } +} + +void VSearchEngine::clear() +{ + clearAllWorkers(); + + m_finishedWorkers = 0; + + m_result.clear(); +} + +void VSearchEngine::clearAllWorkers() +{ + for (auto const & th : m_workers) { + th->quit(); + th->wait(); + + delete th; + } + + m_workers.clear(); +} diff --git a/src/vsearchengine.h b/src/vsearchengine.h new file mode 100644 index 00000000..33948830 --- /dev/null +++ b/src/vsearchengine.h @@ -0,0 +1,89 @@ +#ifndef VSEARCHENGINE_H +#define VSEARCHENGINE_H + +#include +#include +#include +#include "isearchengine.h" + +class VSearchEngineWorker : public QThread +{ + Q_OBJECT + + friend class VSearchEngine; + +public: + explicit VSearchEngineWorker(QObject *p_parent = nullptr); + + void setData(const QStringList &p_files, + const QRegExp &p_reg, + const QString &p_keyword, + Qt::CaseSensitivity p_cs); + +public slots: + void stop(); + +signals: + void resultItemReady(VSearchResultItem *p_item); + +protected: + void run() Q_DECL_OVERRIDE; + +private: + void appendError(const QString &p_err); + + VSearchResultItem *searchFile(const QString &p_fileName); + + QAtomicInt m_stop; + + QStringList m_files; + + QRegExp m_reg; + + QString m_keyword; + + Qt::CaseSensitivity m_caseSensitivity; + + VSearchState m_state; + + QString m_error; +}; + +inline void VSearchEngineWorker::appendError(const QString &p_err) +{ + if (m_error.isEmpty()) { + m_error = p_err; + } else { + m_error = "\n" + p_err; + } +} + + +class VSearchEngine : public ISearchEngine +{ + Q_OBJECT +public: + explicit VSearchEngine(QObject *p_parent = nullptr); + + void search(const QSharedPointer &p_config, + const QSharedPointer &p_result) Q_DECL_OVERRIDE; + + void stop() Q_DECL_OVERRIDE; + + void clear() Q_DECL_OVERRIDE; + +private slots: + void handleWorkerFinished(); + +private: + // Returns an empty object if raw string is preferred. + QRegExp compileRegExpFromConfig(const QSharedPointer &p_config) const; + + void clearAllWorkers(); + + int m_finishedWorkers; + + QVector m_workers; +}; + +#endif // VSEARCHENGINE_H diff --git a/src/vsearcher.cpp b/src/vsearcher.cpp new file mode 100644 index 00000000..a87ced44 --- /dev/null +++ b/src/vsearcher.cpp @@ -0,0 +1,548 @@ +#include "vsearcher.h" + +#include +#include + +#include "vlineedit.h" +#include "utils/vutils.h" +#include "utils/viconutils.h" +#include "vsearch.h" +#include "vsearchresulttree.h" +#include "vmainwindow.h" +#include "veditarea.h" +#include "vdirectorytree.h" +#include "vdirectory.h" +#include "vnotebookselector.h" +#include "vnotebook.h" +#include "vnote.h" +#include "vconfigmanager.h" + +extern VMainWindow *g_mainWin; + +extern VNote *g_vnote; + +extern VConfigManager *g_config; + +VSearcher::VSearcher(QWidget *p_parent) + : QWidget(p_parent), + m_inSearch(false), + m_askedToStop(false), + m_search(this) +{ + setupUI(); + + initUIFields(); + + handleInputChanged(); + + connect(&m_search, &VSearch::resultItemAdded, + m_results, &VSearchResultTree::addResultItem); + connect(&m_search, &VSearch::finished, + this, &VSearcher::handleSearchFinished); +} + +void VSearcher::setupUI() +{ + // Search button. + m_searchBtn = new QPushButton(VIconUtils::buttonIcon(":/resources/icons/search.svg"), "", this); + m_searchBtn->setToolTip(tr("Search")); + m_searchBtn->setProperty("FlatBtn", true); + connect(m_searchBtn, &QPushButton::clicked, + this, &VSearcher::startSearch); + + // Clear button. + m_clearBtn = new QPushButton(VIconUtils::buttonDangerIcon(":/resources/icons/clear_search.svg"), "", this); + m_clearBtn->setToolTip(tr("Clear Results")); + m_clearBtn->setProperty("FlatBtn", true); + connect(m_clearBtn, &QPushButton::clicked, + this, [this]() { + m_results->clearResults(); + }); + + // Advanced button. + m_advBtn = new QPushButton(VIconUtils::buttonIcon(":/resources/icons/search_advanced.svg"), + "", + this); + m_advBtn->setToolTip(tr("Advanced Settings")); + m_advBtn->setProperty("FlatBtn", true); + m_advBtn->setCheckable(true); + connect(m_advBtn, &QPushButton::toggled, + this, [this](bool p_checked) { + m_advWidget->setVisible(p_checked); + }); + + // Console button. + m_consoleBtn = new QPushButton(VIconUtils::buttonIcon(":/resources/icons/search_console.svg"), + "", + this); + m_consoleBtn->setToolTip(tr("Console")); + m_consoleBtn->setProperty("FlatBtn", true); + m_consoleBtn->setCheckable(true); + connect(m_consoleBtn, &QPushButton::toggled, + this, [this](bool p_checked) { + m_consoleEdit->setVisible(p_checked); + }); + + m_numLabel = new QLabel(this); + + QHBoxLayout *btnLayout = new QHBoxLayout(); + btnLayout->addWidget(m_searchBtn); + btnLayout->addWidget(m_clearBtn); + btnLayout->addWidget(m_advBtn); + btnLayout->addWidget(m_consoleBtn); + btnLayout->addStretch(); + btnLayout->addWidget(m_numLabel); + btnLayout->setContentsMargins(0, 0, 3, 0); + + // Keyword. + m_keywordCB = VUtils::getComboBox(this); + m_keywordCB->setEditable(true); + m_keywordCB->setLineEdit(new VLineEdit(this)); + m_keywordCB->setToolTip(tr("Keywords to search for")); + connect(m_keywordCB, &QComboBox::currentTextChanged, + this, &VSearcher::handleInputChanged); + connect(m_keywordCB->lineEdit(), &QLineEdit::returnPressed, + this, &VSearcher::animateSearchClick); + m_keywordCB->completer()->setCaseSensitivity(Qt::CaseSensitive); + + // Scope. + m_searchScopeCB = VUtils::getComboBox(this); + m_searchScopeCB->setToolTip(tr("Scope to search")); + connect(m_searchScopeCB, static_cast(&QComboBox::currentIndexChanged), + this, &VSearcher::handleInputChanged); + + // Object. + m_searchObjectCB = VUtils::getComboBox(this); + m_searchObjectCB->setToolTip(tr("Object to search")); + connect(m_searchObjectCB, static_cast(&QComboBox::currentIndexChanged), + this, &VSearcher::handleInputChanged); + + // Target. + m_searchTargetCB = VUtils::getComboBox(this); + m_searchTargetCB->setToolTip(tr("Target to search")); + connect(m_searchTargetCB, static_cast(&QComboBox::currentIndexChanged), + this, &VSearcher::handleInputChanged); + + // Pattern. + m_filePatternCB = VUtils::getComboBox(this); + m_filePatternCB->setEditable(true); + m_filePatternCB->setLineEdit(new VLineEdit(this)); + m_filePatternCB->setToolTip(tr("Wildcard pattern to filter the files to be searched")); + m_filePatternCB->completer()->setCaseSensitivity(Qt::CaseSensitive); + + // Engine. + m_searchEngineCB = VUtils::getComboBox(this); + m_searchEngineCB->setToolTip(tr("Engine to execute the search")); + + // Case sensitive. + m_caseSensitiveCB = new QCheckBox(tr("&Case sensitive"), this); + + // Whole word only. + m_wholeWordOnlyCB = new QCheckBox(tr("&Whole word only"), this); + + // Fuzzy search. + m_fuzzyCB = new QCheckBox(tr("&Fuzzy search"), this); + m_fuzzyCB->setToolTip(tr("Not available for content search")); + connect(m_fuzzyCB, &QCheckBox::stateChanged, + this, [this](int p_state) { + bool checked = p_state == Qt::Checked; + m_wholeWordOnlyCB->setEnabled(!checked); + }); + + // Regular expression. + m_regularExpressionCB = new QCheckBox(tr("Re&gular expression"), this); + connect(m_regularExpressionCB, &QCheckBox::stateChanged, + this, [this](int p_state) { + bool checked = p_state == Qt::Checked; + m_wholeWordOnlyCB->setEnabled(!checked); + m_fuzzyCB->setEnabled(!checked); + }); + + QFormLayout *advLayout = new QFormLayout(); + advLayout->addRow(tr("File pattern:"), m_filePatternCB); + advLayout->addRow(tr("Engine:"), m_searchEngineCB); + advLayout->addRow(m_caseSensitiveCB); + advLayout->addRow(m_wholeWordOnlyCB); + advLayout->addRow(m_fuzzyCB); + advLayout->addRow(m_regularExpressionCB); + advLayout->setContentsMargins(0, 0, 0, 0); + + m_advWidget = new QWidget(this); + m_advWidget->setLayout(advLayout); + m_advWidget->hide(); + + // Progress bar. + m_proBar = new QProgressBar(this); + m_proBar->setRange(0, 0); + + // Cancel button. + m_cancelBtn = new QPushButton(VIconUtils::buttonIcon(":/resources/icons/close.svg"), + "", + this); + m_cancelBtn->setToolTip(tr("Cancel")); + m_cancelBtn->setProperty("FlatBtn", true); + connect(m_cancelBtn, &QPushButton::clicked, + this, [this]() { + if (m_inSearch) { + appendLogLine(tr("Cancelling the export...")); + m_askedToStop = true; + m_search.stop(); + } + }); + + QHBoxLayout *proLayout = new QHBoxLayout(); + proLayout->addWidget(m_proBar); + proLayout->addWidget(m_cancelBtn); + proLayout->setContentsMargins(0, 0, 0, 0); + + // Console. + m_consoleEdit = new QPlainTextEdit(this); + m_consoleEdit->setReadOnly(true); + m_consoleEdit->setLineWrapMode(QPlainTextEdit::WidgetWidth); + m_consoleEdit->setProperty("LineEdit", true); + m_consoleEdit->setPlaceholderText(tr("Output logs will be shown here")); + m_consoleEdit->setMaximumHeight(m_searchScopeCB->height() * 2); + m_consoleEdit->hide(); + + // List. + m_results = new VSearchResultTree(this); + m_results->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + connect(m_results, &VSearchResultTree::countChanged, + this, [this](int p_count) { + m_clearBtn->setEnabled(p_count > 0); + updateNumLabel(p_count); + }); + + QFormLayout *formLayout = new QFormLayout(); + formLayout->addRow(tr("Keywords:"), m_keywordCB); + formLayout->addRow(tr("Scope:"), m_searchScopeCB); + formLayout->addRow(tr("Object:"), m_searchObjectCB); + formLayout->addRow(tr("Target:"), m_searchTargetCB); + formLayout->setContentsMargins(0, 0, 0, 0); + + QVBoxLayout *mainLayout = new QVBoxLayout(); + mainLayout->addLayout(btnLayout); + mainLayout->addLayout(formLayout); + mainLayout->addWidget(m_advWidget); + mainLayout->addLayout(proLayout); + mainLayout->addWidget(m_consoleEdit); + mainLayout->addWidget(m_results); + + setLayout(mainLayout); +} + +void VSearcher::initUIFields() +{ + VSearchConfig config = VSearchConfig::fromConfig(g_config->getSearchOptions()); + + // Scope. + m_searchScopeCB->addItem(tr("Current Note"), VSearchConfig::CurrentNote); + m_searchScopeCB->addItem(tr("Opened Notes"), VSearchConfig::OpenedNotes); + m_searchScopeCB->addItem(tr("Current Folder"), VSearchConfig::CurrentFolder); + m_searchScopeCB->addItem(tr("Current Notebook"), VSearchConfig::CurrentNotebook); + m_searchScopeCB->addItem(tr("All Notebooks"), VSearchConfig::AllNotebooks); + m_searchScopeCB->setCurrentIndex(m_searchScopeCB->findData(config.m_scope)); + + // Object. + m_searchObjectCB->addItem(tr("Name"), VSearchConfig::Name); + m_searchObjectCB->addItem(tr("Content"), VSearchConfig::Content); + m_searchObjectCB->addItem(tr("Outline"), VSearchConfig::Outline); + m_searchObjectCB->setCurrentIndex(m_searchObjectCB->findData(config.m_object)); + + // Target. + m_searchTargetCB->addItem(tr("Note"), VSearchConfig::Note); + m_searchTargetCB->addItem(tr("Folder"), VSearchConfig::Folder); + m_searchTargetCB->addItem(tr("Notebook"), VSearchConfig::Notebook); + m_searchTargetCB->addItem(tr("All"), + VSearchConfig::Note + | VSearchConfig:: Folder + | VSearchConfig::Notebook); + m_searchTargetCB->setCurrentIndex(m_searchTargetCB->findData(config.m_target)); + + // Engine. + m_searchEngineCB->addItem(tr("Internal"), VSearchConfig::Internal); + m_searchEngineCB->setCurrentIndex(m_searchEngineCB->findData(config.m_engine)); + + // Pattern. + m_filePatternCB->setCurrentText(config.m_pattern); + + m_caseSensitiveCB->setChecked(config.m_option & VSearchConfig::CaseSensitive); + m_wholeWordOnlyCB->setChecked(config.m_option & VSearchConfig::WholeWordOnly); + m_fuzzyCB->setChecked(config.m_option & VSearchConfig::Fuzzy); + m_regularExpressionCB->setChecked(config.m_option & VSearchConfig::RegularExpression); + + setProgressVisible(false); + + m_clearBtn->setEnabled(false); +} + +void VSearcher::updateItemToComboBox(QComboBox *p_comboBox) +{ + QString text = p_comboBox->currentText(); + if (!text.isEmpty() && p_comboBox->findText(text) == -1) { + p_comboBox->addItem(text); + } +} + +void VSearcher::setProgressVisible(bool p_visible) +{ + m_proBar->setVisible(p_visible); + m_cancelBtn->setVisible(p_visible); +} + +void VSearcher::appendLogLine(const QString &p_text) +{ + m_consoleEdit->appendPlainText(">>> " + p_text); + m_consoleEdit->ensureCursorVisible(); + QCoreApplication::sendPostedEvents(); +} + +void VSearcher::showMessage(const QString &p_text) const +{ + g_mainWin->showStatusMessage(p_text); + QCoreApplication::sendPostedEvents(); +} + +void VSearcher::handleInputChanged() +{ + bool readyToSearch = true; + + // Keyword. + QString keyword = m_keywordCB->currentText(); + readyToSearch = !keyword.isEmpty(); + + if (readyToSearch) { + // Other targets are only available for Name. + int obj = m_searchObjectCB->currentData().toInt(); + if (obj != VSearchConfig::Name) { + int target = m_searchTargetCB->currentData().toInt(); + if (!(target & VSearchConfig::Note)) { + readyToSearch = false; + } + } + + if (readyToSearch && obj == VSearchConfig::Outline) { + // Outline is available only for CurrentNote and OpenedNotes. + int scope = m_searchScopeCB->currentData().toInt(); + if (scope != VSearchConfig::CurrentNote + && scope != VSearchConfig::OpenedNotes) { + readyToSearch = false; + } + } + } + + m_searchBtn->setEnabled(readyToSearch); +} + +void VSearcher::startSearch() +{ + if (m_inSearch) { + return; + } + + m_searchBtn->setEnabled(false); + setProgressVisible(true); + m_results->clearResults(); + m_askedToStop = false; + m_inSearch = true; + m_consoleEdit->clear(); + appendLogLine(tr("Search started.")); + + updateItemToComboBox(m_keywordCB); + updateItemToComboBox(m_filePatternCB); + + QSharedPointer config(new VSearchConfig(m_searchScopeCB->currentData().toInt(), + m_searchObjectCB->currentData().toInt(), + m_searchTargetCB->currentData().toInt(), + m_searchEngineCB->currentData().toInt(), + getSearchOption(), + m_keywordCB->currentText(), + m_filePatternCB->currentText())); + m_search.setConfig(config); + + g_config->setSearchOptions(config->toConfig()); + + QSharedPointer result; + switch (config->m_scope) { + case VSearchConfig::CurrentNote: + { + QVector files; + files.append(g_mainWin->getCurrentFile()); + if (files[0]) { + QString msg(tr("Search current note %1.").arg(files[0]->getName())); + appendLogLine(msg); + showMessage(msg); + } + + result = m_search.search(files); + break; + } + + case VSearchConfig::OpenedNotes: + { + QVector tabs = g_mainWin->getEditArea()->getAllTabsInfo(); + QVector files; + files.reserve(tabs.size()); + for (auto const & ta : tabs) { + files.append(ta.m_editTab->getFile()); + } + + result = m_search.search(files); + break; + } + + case VSearchConfig::CurrentFolder: + { + VDirectory *dir = g_mainWin->getDirectoryTree()->currentDirectory(); + if (dir) { + QString msg(tr("Search current folder %1.").arg(dir->getName())); + appendLogLine(msg); + showMessage(msg); + } + + result = m_search.search(dir); + break; + } + + case VSearchConfig::CurrentNotebook: + { + QVector notebooks; + notebooks.append(g_mainWin->getNotebookSelector()->currentNotebook()); + if (notebooks[0]) { + QString msg(tr("Search current notebook %1.").arg(notebooks[0]->getName())); + appendLogLine(msg); + showMessage(msg); + } + + result = m_search.search(notebooks); + break; + } + + case VSearchConfig::AllNotebooks: + { + const QVector ¬ebooks = g_vnote->getNotebooks(); + result = m_search.search(notebooks); + break; + } + + default: + break; + } + + handleSearchFinished(result); +} + +void VSearcher::handleSearchFinished(const QSharedPointer &p_result) +{ + Q_ASSERT(m_inSearch); + Q_ASSERT(p_result->m_state != VSearchState::Idle); + + qDebug() << "handleSearchFinished" << (int)p_result->m_state; + + QString msg; + switch (p_result->m_state) { + case VSearchState::Busy: + msg = tr("Search is on going."); + appendLogLine(msg); + return; + + case VSearchState::Success: + msg = tr("Search succeeded."); + appendLogLine(msg); + showMessage(msg); + break; + + case VSearchState::Fail: + msg = tr("Search failed."); + appendLogLine(msg); + showMessage(msg); + VUtils::showMessage(QMessageBox::Warning, + tr("Warning"), + tr("Search failed."), + p_result->m_errMsg, + QMessageBox::Ok, + QMessageBox::Ok, + this); + break; + + case VSearchState::Cancelled: + Q_ASSERT(m_askedToStop); + msg = tr("User cancelled the search. Aborted!"); + appendLogLine(msg); + showMessage(msg); + m_askedToStop = false; + break; + + default: + break; + } + + if (p_result->m_state != VSearchState::Fail + && p_result->hasError()) { + VUtils::showMessage(QMessageBox::Warning, + tr("Warning"), + tr("Errors found during search."), + p_result->m_errMsg, + QMessageBox::Ok, + QMessageBox::Ok, + this); + } + + m_search.clear(); + + m_inSearch = false; + m_searchBtn->setEnabled(true); + setProgressVisible(false); +} + +void VSearcher::animateSearchClick() +{ + m_searchBtn->animateClick(); +} + +int VSearcher::getSearchOption() const +{ + int ret = VSearchConfig::NoneOption; + + if (m_caseSensitiveCB->isChecked()) { + ret |= VSearchConfig::CaseSensitive; + } + + if (m_wholeWordOnlyCB->isChecked()) { + ret |= VSearchConfig::WholeWordOnly; + } + + if (m_fuzzyCB->isChecked()) { + ret |= VSearchConfig::Fuzzy; + } + + if (m_regularExpressionCB->isChecked()) { + ret |= VSearchConfig::RegularExpression; + } + + return ret; +} + +void VSearcher::updateNumLabel(int p_count) +{ + m_numLabel->setText(tr("%1 Items").arg(p_count)); +} + +void VSearcher::focusToSearch() +{ + m_keywordCB->setFocus(Qt::OtherFocusReason); +} + +void VSearcher::showNavigation() +{ + VNavigationMode::showNavigation(m_results); +} + +bool VSearcher::handleKeyNavigation(int p_key, bool &p_succeed) +{ + static bool secondKey = false; + return VNavigationMode::handleKeyNavigation(m_results, + secondKey, + p_key, + p_succeed); +} diff --git a/src/vsearcher.h b/src/vsearcher.h new file mode 100644 index 00000000..6f8c231b --- /dev/null +++ b/src/vsearcher.h @@ -0,0 +1,108 @@ +#ifndef VSEARCHER_H +#define VSEARCHER_H + +#include +#include + +#include "vsearch.h" + +#include "vnavigationmode.h" + +class QComboBox; +class QCheckBox; +class QPushButton; +class QLabel; +class VSearchResultTree; +class QProgressBar; +class QPlainTextEdit; + +class VSearcher : public QWidget, public VNavigationMode +{ + Q_OBJECT +public: + explicit VSearcher(QWidget *p_parent = nullptr); + + void focusToSearch(); + + // Implementations for VNavigationMode. + void showNavigation() Q_DECL_OVERRIDE; + bool handleKeyNavigation(int p_key, bool &p_succeed) Q_DECL_OVERRIDE; + +private slots: + void handleSearchFinished(const QSharedPointer &p_result); + +private: + void startSearch(); + + void setupUI(); + + void initUIFields(); + + void setProgressVisible(bool p_visible); + + void appendLogLine(const QString &p_text); + + void handleInputChanged(); + + void animateSearchClick(); + + void updateItemToComboBox(QComboBox *p_comboBox); + + // Get the OR of the search options. + int getSearchOption() const; + + void updateNumLabel(int p_count); + + void showMessage(const QString &p_text) const; + + QComboBox *m_keywordCB; + + // All notebooks, current notebook, and so on. + QComboBox *m_searchScopeCB; + + // Name, content, tag. + QComboBox *m_searchObjectCB; + + // Notebook, folder, note. + QComboBox *m_searchTargetCB; + + QComboBox *m_filePatternCB; + + QComboBox *m_searchEngineCB; + + QCheckBox *m_caseSensitiveCB; + + QCheckBox *m_wholeWordOnlyCB; + + QCheckBox *m_fuzzyCB; + + QCheckBox *m_regularExpressionCB; + + QPushButton *m_searchBtn; + + QPushButton *m_clearBtn; + + QPushButton *m_advBtn; + + QPushButton *m_consoleBtn; + + QLabel *m_numLabel; + + QWidget *m_advWidget; + + VSearchResultTree *m_results; + + QProgressBar *m_proBar; + + QPushButton *m_cancelBtn; + + QPlainTextEdit *m_consoleEdit; + + bool m_inSearch; + + bool m_askedToStop; + + VSearch m_search; +}; + +#endif // VSEARCHER_H diff --git a/src/vsearchresulttree.cpp b/src/vsearchresulttree.cpp new file mode 100644 index 00000000..c121188e --- /dev/null +++ b/src/vsearchresulttree.cpp @@ -0,0 +1,143 @@ +#include "vsearchresulttree.h" + +#include "utils/viconutils.h" +#include "vnote.h" +#include "vmainwindow.h" +#include "vnotebookselector.h" + +extern VNote *g_vnote; + +extern VMainWindow *g_mainWin; + +VSearchResultTree::VSearchResultTree(QWidget *p_parent) + : VTreeWidget(p_parent) +{ + setColumnCount(1); + setHeaderHidden(true); + setExpandsOnDoubleClick(false); + + setSimpleSearchMatchFlags(getSimpleSearchMatchFlags() & ~Qt::MatchRecursive); + + m_noteIcon = VIconUtils::treeViewIcon(":/resources/icons/note_item.svg"); + m_folderIcon = VIconUtils::treeViewIcon(":/resources/icons/dir_item.svg"); + m_notebookIcon = VIconUtils::treeViewIcon(":/resources/icons/notebook_item.svg"); + + connect(this, &VTreeWidget::itemActivated, + this, &VSearchResultTree::handleItemActivated); +} + +void VSearchResultTree::updateResults(const QList > &p_items) +{ + clearResults(); + + for (auto const & it : p_items) { + appendItem(it); + } + + emit countChanged(topLevelItemCount()); +} + +void VSearchResultTree::addResultItem(const QSharedPointer &p_item) +{ + appendItem(p_item); + + emit countChanged(topLevelItemCount()); +} + +void VSearchResultTree::clearResults() +{ + clearAll(); + + m_data.clear(); + + emit countChanged(topLevelItemCount()); +} + +void VSearchResultTree::appendItem(const QSharedPointer &p_item) +{ + m_data.append(p_item); + + QTreeWidgetItem *item = new QTreeWidgetItem(this); + item->setData(0, Qt::UserRole, m_data.size() - 1); + item->setText(0, p_item->m_text.isEmpty() ? p_item->m_path : p_item->m_text); + item->setToolTip(0, p_item->m_path); + + switch (p_item->m_type) { + case VSearchResultItem::Note: + item->setIcon(0, m_noteIcon); + break; + + case VSearchResultItem::Folder: + item->setIcon(0, m_folderIcon); + break; + + case VSearchResultItem::Notebook: + item->setIcon(0, m_notebookIcon); + break; + + default: + break; + } + + for (auto const & it: p_item->m_matches) { + QTreeWidgetItem *subItem = new QTreeWidgetItem(item); + QString text; + if (it.m_lineNumber > -1) { + text = QString("[%1] %2").arg(it.m_lineNumber).arg(it.m_text); + } else { + text = it.m_text; + } + + subItem->setText(0, text); + subItem->setToolTip(0, it.m_text); + } +} + +void VSearchResultTree::handleItemActivated(QTreeWidgetItem *p_item, int p_column) +{ + Q_UNUSED(p_column); + if (!p_item) { + return; + } + + QTreeWidgetItem *topItem = p_item; + if (p_item->parent()) { + topItem = p_item->parent(); + } + + int idx = topItem->data(0, Qt::UserRole).toInt(); + Q_ASSERT(idx >= 0 && idx < m_data.size()); + + const QSharedPointer &resItem = m_data[idx]; + switch (resItem->m_type) { + case VSearchResultItem::Note: + { + QStringList files(resItem->m_path); + g_mainWin->openFiles(files); + break; + } + + case VSearchResultItem::Folder: + { + VDirectory *dir = g_vnote->getInternalDirectory(resItem->m_path); + if (dir) { + g_mainWin->locateDirectory(dir); + } + + break; + } + + case VSearchResultItem::Notebook: + { + VNotebook *nb = g_vnote->getNotebook(resItem->m_path); + if (nb) { + g_mainWin->getNotebookSelector()->locateNotebook(nb); + } + + break; + } + + default: + break; + } +} diff --git a/src/vsearchresulttree.h b/src/vsearchresulttree.h new file mode 100644 index 00000000..d5f3c35d --- /dev/null +++ b/src/vsearchresulttree.h @@ -0,0 +1,39 @@ +#ifndef VSEARCHRESULTTREE_H +#define VSEARCHRESULTTREE_H + +#include + +#include "vtreewidget.h" +#include "vsearch.h" + + +class VSearchResultTree : public VTreeWidget +{ + Q_OBJECT +public: + explicit VSearchResultTree(QWidget *p_parent = nullptr); + + void updateResults(const QList > &p_items); + + void clearResults(); + +public slots: + void addResultItem(const QSharedPointer &p_item); + +signals: + void countChanged(int p_count); + +private slots: + void handleItemActivated(QTreeWidgetItem *p_item, int p_column); + +private: + void appendItem(const QSharedPointer &p_item); + + QVector > m_data; + + QIcon m_noteIcon; + QIcon m_folderIcon; + QIcon m_notebookIcon; +}; + +#endif // VSEARCHRESULTTREE_H diff --git a/src/vsimplesearchinput.h b/src/vsimplesearchinput.h index 39a561bb..8aa92d0e 100644 --- a/src/vsimplesearchinput.h +++ b/src/vsimplesearchinput.h @@ -47,6 +47,10 @@ public: void setNavigationKeyEnabled(bool p_enabled); + void setMatchFlags(Qt::MatchFlags p_flags); + + Qt::MatchFlags getMatchFlags() const; + signals: // Search mode is triggered. void triggered(bool p_inSearchMode); @@ -101,4 +105,14 @@ inline void VSimpleSearchInput::setNavigationKeyEnabled(bool p_enabled) { m_navigationKeyEnabled = p_enabled; } + +inline void VSimpleSearchInput::setMatchFlags(Qt::MatchFlags p_flags) +{ + m_matchFlags = p_flags; +} + +inline Qt::MatchFlags VSimpleSearchInput::getMatchFlags() const +{ + return m_matchFlags; +} #endif // VSIMPLESEARCHINPUT_H diff --git a/src/vtextedit.cpp b/src/vtextedit.cpp index cfa21421..9598714e 100644 --- a/src/vtextedit.cpp +++ b/src/vtextedit.cpp @@ -43,6 +43,8 @@ void VTextEdit::init() { setAcceptRichText(false); + m_defaultCursorWidth = 1; + m_lineNumberType = LineNumberType::None; m_blockImageEnabled = false; @@ -60,6 +62,8 @@ void VTextEdit::init() docLayout->setVirtualCursorBlockWidth(VIRTUAL_CURSOR_BLOCK_WIDTH); + docLayout->setCursorWidth(m_defaultCursorWidth); + connect(docLayout, &VTextDocumentLayout::cursorBlockWidthUpdated, this, [this](int p_width) { if (p_width != cursorWidth() @@ -361,7 +365,7 @@ void VTextEdit::setCursorBlockMode(CursorBlock p_mode) layout->setCursorBlockMode(m_cursorBlockMode); layout->clearLastCursorBlockWidth(); setCursorWidth(m_cursorBlockMode != CursorBlock::None ? VIRTUAL_CURSOR_BLOCK_WIDTH - : 1); + : m_defaultCursorWidth); layout->updateBlockByNumber(textCursor().blockNumber()); } } @@ -388,3 +392,12 @@ void VTextEdit::relayout() { getLayout()->relayout(); } + +void VTextEdit::setDisplayScaleFactor(qreal p_factor) +{ + m_defaultCursorWidth = p_factor + 0.5; + + setCursorWidth(m_cursorBlockMode != CursorBlock::None ? VIRTUAL_CURSOR_BLOCK_WIDTH + : m_defaultCursorWidth); + getLayout()->setCursorWidth(m_defaultCursorWidth); +} diff --git a/src/vtextedit.h b/src/vtextedit.h index e34ed263..c17b525d 100644 --- a/src/vtextedit.h +++ b/src/vtextedit.h @@ -65,6 +65,8 @@ public: void relayout(); + void setDisplayScaleFactor(qreal p_factor); + protected: void resizeEvent(QResizeEvent *p_event) Q_DECL_OVERRIDE; @@ -91,6 +93,8 @@ private: CursorBlock m_cursorBlockMode; bool m_highlightCursorLineBlock; + + int m_defaultCursorWidth; }; inline void VTextEdit::setLineNumberType(LineNumberType p_type) diff --git a/src/vtreewidget.h b/src/vtreewidget.h index 5f19694e..fb44d7aa 100644 --- a/src/vtreewidget.h +++ b/src/vtreewidget.h @@ -21,6 +21,10 @@ public: // Clear tree widget as well as other data. void clearAll(); + void setSimpleSearchMatchFlags(Qt::MatchFlags p_flags); + + Qt::MatchFlags getSimpleSearchMatchFlags() const; + // Implement ISimpleSearch. virtual QList searchItems(const QString &p_text, Qt::MatchFlags p_flags) const Q_DECL_OVERRIDE; @@ -65,4 +69,14 @@ private: QTimer *m_searchColdTimer; }; + +inline void VTreeWidget::setSimpleSearchMatchFlags(Qt::MatchFlags p_flags) +{ + m_searchInput->setMatchFlags(p_flags); +} + +inline Qt::MatchFlags VTreeWidget::getSimpleSearchMatchFlags() const +{ + return m_searchInput->getMatchFlags(); +} #endif // VTREEWIDGET_H